fix(runtime-host): respect transcript continuation boundaries - #5188
Conversation
Generated-by: Codex
Astro-Han
left a comment
There was a problem hiding this comment.
Approving. This is the right fix at the owner: the range never crosses the Turn boundary the cursor encodes, and that rule lives in the pager, where the cursor meets the storage read. Thanks for tracing it — the diagnosis in #5183 was mine and it was wrong about where the budget went; readRangeEdges does include the selected page. The real cause is what you found: continuationMessageLimit estimates count from sequence distance while durable sequences advance by a stride of 8, so the estimate is ~8× high and the continuation eats the next Turn. Reading the trim point: it always lands at a Turn end and the first deferred row opens the next Turn, so no consumer sees a torn Turn, and the few over-boundary rows main used to return are gone too.
I approved the workflow run, so CI is going now. Three non-blocking notes, none of which should hold the merge:
firstOmitted.kind !== 'durable'cannot be reached — thefindIndexpredicate already requiresdurable.readSharedDurablePagestill stops onrecord.sequence === rangeBoundarySequenceitself, so the boundary trim now has two implementations; thesharedhalf of the new matrix likely passes onmainalready. Fine to leave and collapse later.- The durable branch's
continuationMessageLimitno longer carries correctness after the trim, only read size; worth a one-line note so the estimate is not mistaken for a guard again.
…enderer-window apps/desktop/renderer-architecture.json: generated ledger. Took either side, then regenerated with `check-renderer-architecture.mjs --write`; the app-shell entry settles at the merged source's own count rather than either parent's. Verified with `npm run check:architecture` and with `--base origin/main`, the convention CI uses. packages/ui/src/__tests__/prompt-anchor-rail.test.ts: main deleted the static-markup landmark test here and reintroduced it as a mounted portal test in the sibling file; this branch had instead rewritten it to render under the scroll authority. Took main's deletion, since the mounted version covers the same landmarks through the real layout. Kept this branch's `selectPromptRailTick` import and dropped main's `selectPromptRailActiveTurn`, `selectPromptRailTickForMountedTurn` and `PromptRailFrameScheduler` — this branch deleted all three with the rail's own observer machinery — along with the now-unused `TranscriptScrollAuthorityProvider` import. packages/ui/src/__tests__/prompt-rail-reading-position.test.tsx: took main's `PromptAnchorRail` import and dropped `READING_BAND_TOP_PERCENT`, which no longer exists — this branch deleted the reading-band observer it belonged to. Main's ported portal test rendered the rail bare to assert no inline rail exists before a host does; under this branch the rail reads its tick from the scroll authority and throws without one, so that first render now happens inside `TranscriptScrollAuthorityProvider`. The assertion it makes is unchanged: host absence, not authority absence, is what leaves the rail unrendered. Semantic conflicts, auto-merged but resolved by hand afterwards: apps/desktop/src/main/__tests__/transcript-navigation-pager.test.ts: #5188 bounds a sparse transcript continuation at its client range boundary, so an oversized Turn now fills a range on its own and a reset anchored on the oldest row no longer reaches the tail in one page. The test's invariant is reachability, not page count, so it now asserts the reset ends at the boundary with a newer edge and that the page behind that edge reaches the tail. Both intents kept: #5188's bound and this branch's one-page window read. Read against this branch's intent and left as auto-merged: app-shell.tsx (main's WorkHub return button, shared `sessionsSelected` predicate and single-pending interaction hydration fence sit beside this branch's transcript window props), features/conversation/index.ts, chat-message.css (main reworked sticky activity headers; this branch's transient-row `overflow-anchor` and gap-row removal are disjoint), e2e-budget.json, docs/astryx-surface-file-inventory.md, chat-surface-layout.tsx and chat-view.tsx (the rail host provider nests inside the scroll authority, so the portaled rail still resolves both contexts). Verified on the merge: desktop typecheck, `check:architecture` (plain and `--base origin/main`), root `format:check` and `lint`, `apps/desktop` test:dist (2492 pass) and `packages/ui` test:dist (419 pass), plus `build-storybook` and `smoke:storybook` (333 stories, 360 theme renders). `@maka/mcp` test:dist fails `user wait does not consume the network timeout` reproducibly; every input to that package is byte-identical to origin/main here, so it is inherited rather than introduced. Generated-by: Claude Code
Summary
Keep durable transcript continuations within the Turn boundary encoded in their cursor. Durable message sequences are sparse, so their numeric distance can overestimate the remaining message count and let a continuation pull in the next Turn, exceeding the client's 256-message range limit.
Clip the continuation at its original boundary and resume the next page at the first deferred message. Preserve fragmented-message continuation, normal Turn protection, and the existing protocol limits and wire format.
Fixes #5183
Verification
npm run lint,npm run format:check,npm run build, andnpm run typecheckpassed.npx knip --workspace apps/desktopandnpx knip --workspace packages/uipassed.AI use
Select exactly one:
Tool(s) and scope: OpenAI Codex — diagnosis, implementation, regression tests, validation, and PR text.
Checklist
Does this PR entail a change in behavior?