docs/design/web-shell-history-pagination.md
Web Shell restores an existing session through POST /session/:id/load. The
daemon currently converts the complete persisted conversation into replay
updates, bounds the resulting in-memory snapshot by bytes, and returns the
retained snapshot as one compactedReplay response. Web Shell normalizes that
entire response before painting the transcript.
The byte bound prevents unbounded daemon memory, but a long session can still produce a multi-megabyte response and a large synchronous normalization pass. Older events are also represented only by a truncation marker; the persisted transcript pager is not connected to Web Shell.
The existing transcript API pages from the oldest record toward the newest. That order is useful for exports but does not support chat history UX, where the latest messages must paint first and earlier messages are requested on demand.
session/load.History replay already tracks the active persisted ChatRecord while emitting
ACP updates. The replay collector will stamp that record UUID into private
update metadata. This metadata is present only on replayed persisted history;
live events keep their current shape.
The UUID gives the initial load page an exact exclusive boundary. Older paging therefore does not rely on timestamps and cannot duplicate the first retained record when timestamps collide.
The TypeScript SDK adds an optional historyPageSize restore option. The serve
route validates it with the transcript pager's existing range of 1 through 500
and forwards it through the bridge as private ACP load metadata. Servers that
support this contract advertise session_transcript_pagination through
/capabilities.features; Web Shell sends the option only when that feature is
present, so older servers retain their bulk-load behavior.
When the option is absent, load is unchanged. When present, the ACP agent keeps the complete resumed conversation in core but converts only the latest record suffix for the UI replay envelope. The suffix begins at a normal user-turn boundary. The envelope reports whether older active-chain records exist.
The bridge seeds only that page into the session EventBus. Its response still
contains the replay events and the EventBus lastEventId from one load
operation, so events emitted after the snapshot remain available through SSE
without a resume/transcript-read race.
GET /session/:id/transcript and its workspace-qualified form accept an
exclusive beforeRecordId only on the first backward request. The response
uses the existing nextCursor field; the signed cursor records that paging is
backward and freezes file identity, active leaf, byte size, position, and replay
direction.
Backward pages are returned in chronological display order. Each selected page
starts at a normal user-turn boundary. The record limit is therefore a soft page
target: a long turn is returned intact even when it exceeds the requested record
count, so scrolling never reveals only the tail of the previous turn. The
workspace route retains its hard source-byte limit: if a complete turn exceeds
that limit, it returns transcript_page_too_large rather than returning a
partial turn. Forward cursors and responses remain byte-for-byte compatible.
The provider normalizes an older page in an isolated transcript store, allocates non-conflicting block ordinals, and resets the active store with only the resulting blocks prepended. Live side-channel state, active streaming pointers, and the SSE event cursor are preserved.
Older pages are prepended atomically: if a complete page would exceed
maxBlocks, the UI keeps the current transcript unchanged, stops paging, and
reports that earlier records remain persisted but cannot be displayed in the
current view. A complete initial page is never truncated mid-turn; when it
exceeds maxBlocks, the provider retains that page and prevents older paging.
Web Shell opts into a 100-record initial history page. The session provider derives the exclusive boundary from the earliest replay record UUID and exposes history state through a small hook:
When the message list reaches the top, it automatically requests the next page and shows a loading status above the transcript. It records the scroll container height before the request and restores the visual anchor after prepending. A short page that does not fill the viewport automatically advances until the transcript becomes scrollable or history is exhausted. A failed or partial page leaves the current transcript intact, stops automatic retries, and surfaces the existing daemon notice path.
session/load.transcript_snapshot_unavailable; the provider stops paging and leaves
already-rendered content intact.historyPageSize or beforeRecordId retain the
current bulk-load and forward-page contracts.| Layer | Change |
|---|---|
| Core transcript reader | Backward cursor and exclusive record boundary |
| ACP replay | Record UUID metadata and latest-suffix selection |
| ACP bridge / serve route | Paged-load metadata, validation, and hasMore propagation |
| TypeScript SDK | Restore option, backward page option, restored history state |
| WebUI provider | Isolated prepend, page state, stale-request protection |
| Web Shell | Opt-in page size and automatic top-loading behavior |