Skip to content

fix(runtime-host): respect transcript continuation boundaries - #5188

Merged
Astro-Han merged 1 commit into
apache:mainfrom
Dante-dan:fix/transcript-page-range-budget
Sep 11, 2026
Merged

Astro-Han merged 1 commit into
apache:mainfrom
Dante-dan:fix/transcript-page-range-budget

Conversation

@Dante-dan

Copy link
Copy Markdown
Contributor

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

  • Added real pager/client regressions for adjacent 161/134-message Turns in both directions and owner/shared projections, a Turn exactly at 256 messages, and a partially read message deferred to the next range.
  • Pager suite: 23 tests passed. The adjacent-Turn and exact-cap regressions fail with the reported RangeError without the fix.
  • Runtime Host suite: 1849 passed, 12 skipped.
  • npm run lint, npm run format:check, npm run build, and npm run typecheck passed.
  • npx knip --workspace apps/desktop and npx knip --workspace packages/ui passed.

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, validation, 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

@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.

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 — the findIndex predicate already requires durable.
  • readSharedDurablePage still stops on record.sequence === rangeBoundarySequence itself, so the boundary trim now has two implementations; the shared half of the new matrix likely passes on main already. Fine to leave and collapse later.
  • The durable branch's continuationMessageLimit no longer carries correctness after the trim, only read size; worth a one-line note so the estimate is not mistaken for a guard again.

@Astro-Han
Astro-Han merged commit aeb51db into apache:main Sep 11, 2026
2 checks passed
Astro-Han added a commit that referenced this pull request Sep 11, 2026
…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
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): a transcript page that spans a Turn boundary can exceed the client's 256-message range cap

2 participants