Skip to content

fix(runtime-host): page oversized transcript Turns - #4433

Merged
Astro-Han merged 2 commits into
apache:mainfrom
Sun-GLiang:fix/4428-oversized-turn
Sep 1, 2026
Merged

fix(runtime-host): page oversized transcript Turns#4433
Astro-Han merged 2 commits into
apache:mainfrom
Sun-GLiang:fix/4428-oversized-turn

Conversation

@Sun-GLiang

@Sun-GLiang Sun-GLiang commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Allow Runtime Host transcript bootstrap to degrade oversized target Turn range metadata to null boundaries, preserving bounded pages and cursor-based continuation. Ordinary Turn and partial-edge Turn protection remains unchanged.

Fixes #4428

Verification

  • npm run astryx:surface-inventory — pass
  • npm run astryx:surface-inventory:test — 15/15 pass
  • npm run lint — pass
  • npm run format:check — pass
  • npm run build:test — pass
  • node --test packages/runtime-host/dist/__tests__/session-transcript-pager.test.js — 18/18 pass
  • npm --workspace @maka/runtime-host run typecheck — pass
  • npm run test:dist — all workspace tests pass
  • git diff --check — pass

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: OpenAI Codex — diagnosis, implementation, regression tests, CI investigation, and PR text.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

@github-actions github-actions Bot added the effort/M Under 500 readable lines label Sep 1, 2026

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — reviewed 073c4455. Approving; four P3s below, none blocking, all fine as follow-ups.

The problem is demonstrated without relying on the new tests. readRangeEdges throws once a single Turn passes SESSION_TRANSCRIPT_RANGE_MAX_MESSAGES (256) or SESSION_TRANSCRIPT_RANGE_MAX_BYTES (16 MiB), and createSessionTranscriptBootstrap maps any non-TranscriptOverlayCapacityError into persistence_failed / "Session transcript is unavailable" — which is the log in #4428 verbatim. Those thresholds are Runtime Host's own, so a user cannot avoid them, and a 257-message tool loop is an ordinary size. The throw arrived with #4244 two days before the report, so this is a reachable regression rather than a hypothetical.

I checked the three claims in the summary rather than taking them:

  • Pages stay bounded. The bound never came from readRangeEdges — bootstrap reads through readDurablePage({ maxMessages: SESSION_TRANSCRIPT_RANGE_MAX_MESSAGES, … }) and continuations through continuationMessageLimit(position) with requirePageByteLimit capping maxBytes at 512 KB. readRangeEdges only trims a selection back to whole-Turn edges, so returning input.selected untouched leaves the reader's own limits in force.
  • Cursor continuation survives. pageFromSelection signs the cursor from selected.next, and cursorRangeBoundarySequence is already null-valued elsewhere, so resolvePosition keeps walking. A 286-message single Turn pages as 256 + 30 with no repeats and no gap.
  • null is inside the consumers' contract. Both production readers already handle it: session-subscription.ts:247 treats a null boundary as reachedBoundary = true, and all three sites in desktop-transcript-replica.ts were already written as page.protectedTurnSequence ?? …. And the degraded path returns at the 257th record, so it does not become a full scan.

"Fails without it" checks out — running the new test file against unmodified main gives 15 pass / 3 fail, and all three failures are the new cases hitting the target RangeError.

P3 — the second production change looks unreachable, and it leaves a dead throw carrying the same message. group[0] is a subset of input.selected, which is already bounded at ≤ 256 messages and ≤ 512 KB by requirePageByteLimit, so the groupStart === 0 branch cannot exceed 256 / 16 MiB; a target Turn over the limit has already returned at the earlier site. The three new cases all fail on main at the first site, and the second has no coverage. That doubles the production surface, and :423 still throws RangeError('Session transcript Turn range exceeds its capacity limit') — the identical string — so anyone grepping it will still read "oversized Turns throw", which is now wrong. Collapsing the limit check to the one authority in the scan loop lets both the second branch and that throw go, and the message disappears from the tree.

P3 — the running-Turn case duplicates the terminal-Turn one. Both use the same 286-message fixture and the former's assertions are a subset of the latter's; readRangeEdges never reads rootTurn, and the fixture reader ignores it too, so both land in the same return. #4428 asked for active-Turn coverage if the code path differs — it doesn't. Either drop it, or give it real activeAssistantStreams / overlay so it proves that degradation still holds once the overlay takes budget.

P3 — two assertions in the byte-bound case are tautological. With maxBytes at 512 KB and 600 KB messages, bootstrap can only return one truncated fragment, so fragments.length <= 256 and rawBytes <= 512 * 1024 hold under any implementation. The case earns its place as the only byte-bound coverage; assert.equal(fragments.length, 1) plus a continuation that retrieves the full message would make it say something.

P3 — worth naming what the degradation gives up. With protectedTurnSequence null, residentTurnKey groups this session under one Turn and #evictToBudget can't evict a protected key, so Desktop's resident set grows with the whole Turn rather than being capped by the Host's 256 / 16 MiB. It's bounded by that Turn, recovered by discard() on session switch, and before this change the session could not be opened at all — so the net is still an improvement. But "preserving bounded pages" is only half true, and the other half belongs in the summary with a pointer to #2913.

Evidence boundary: I read the pager against main, both production consumers of the boundary fields, the protocol limits, and the bootstrap error mapping, and ran the test file three ways (PR head 18/18, main 16/16, main production + PR tests 15/3). I did not exercise the real SQLite reader — the fixture ignores maxStoredBytes, so the byte path is verified against the fixture rather than storage — and I did not open a 286-message session in Desktop to confirm the error is gone.


AI-assisted review: drafted with Maka; I verified the boundary consumers, the continuation cursor, and the three-way test run against the branch source myself.

@Astro-Han
Astro-Han merged commit afbcabd into apache:main Sep 1, 2026
4 checks passed
abhinav-phi pushed a commit to abhinav-phi/maka that referenced this pull request Sep 1, 2026
A Turn larger than the Host's own range limits made its Session unopenable. `readRangeEdges` trims a selection back to whole-Turn boundaries, and when the target Turn alone exceeded `SESSION_TRANSCRIPT_RANGE_MAX_MESSAGES` (256) or `SESSION_TRANSCRIPT_RANGE_MAX_BYTES` (16 MiB) it threw `RangeError`. `createSessionTranscriptBootstrap` maps anything that is not a `TranscriptOverlayCapacityError` to `persistence_failed`, so the reader saw "Session transcript is unavailable" and no amount of retrying helped. The limits are the Host's own, so nothing on the client side could avoid them, and a 257-message tool loop is an ordinary size. The throw was added in apache#4244 as the fallback for "not even one Turn fits".

Degrade instead of refusing: when a single Turn cannot be bracketed, return the selection unchanged with null range and protected-Turn boundaries. Nothing else moves. The page stays bounded because the bound never came from this function — bootstrap reads through `readDurablePage` with the range message limit and continuations through `continuationMessageLimit`, with `requirePageByteLimit` capping page bytes at 512 KB. Continuation still works because `pageFromSelection` signs its cursor from the reader's `selected.next`, and a null boundary is already a value both consumers handle: `session-subscription.ts` reads it as "boundary reached", and `desktop-transcript-replica.ts` was already written with `?? fallback` at all three sites. Ordinary Turns and partial-edge Turns take the same branches as before — the conditions are untouched.

An oversized Turn now pages: 286 messages arrive as 256 + 30, with no repeats and no gap.

One consequence worth recording: with a null protected-Turn boundary, Desktop groups the session under a single resident Turn key and cannot evict it, so its resident set is bounded by that Turn rather than by the Host's 256 / 16 MiB. That is a smaller problem than being unable to open the Session at all, and it belongs to the bounded-transcript work in apache#2913.

Fixes apache#4428

Generated-by: OpenAI Codex
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/M Under 500 readable lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(runtime-host): oversized single Turn makes transcript opening fail

2 participants