| 1 | # Transcript projection |
| 2 | |
| 3 | [简体中文](TRANSCRIPT_PROJECTION.zh-CN.md) |
| 4 | |
| 5 | Local Desktop and current Serve sessions share `internal/transcript`. The |
| 6 | controller appends each durable envelope, commits its display projection, then |
| 7 | publishes the event outside the commit lock. Planner output, executor output, |
| 8 | prompts and terminal events use this boundary. Provider history remains the |
| 9 | input to model requests; display records never enter those requests. |
| 10 | |
| 11 | The Desktop surface uses the Electron host contract. New transcript methods |
| 12 | and ownership metadata are generated together; DTO names cannot shadow the |
| 13 | TypeScript helpers used by that contract. |
| 14 | |
| 15 | ## Identity and recovery |
| 16 | |
| 17 | The controller reserves a user message ID before planning. The frontend keeps a |
| 18 | local submission echo outside the durable transcript until the canonical row is |
| 19 | installed. `submissionId` correlates one send attempt, `messageId` identifies the |
| 20 | durable message, and the presentation key only preserves a mounted DOM node |
| 21 | during handoff. Durable user rows always use `m:<messageId>`; history mutation, |
| 22 | content lookup, and persistence never use the presentation key. Equal bodies are |
| 23 | allowed and are not duplicate identities. A sampling attempt reserves its |
| 24 | assistant message ID before its first delta; successful persistence uses that ID, |
| 25 | and discard removes only that attempt's owned records. Tool cards use tool call |
| 26 | IDs. |
| 27 | |
| 28 | Local submission handoff is independent of the resident history window. The |
| 29 | follower sends formal user identities before window filtering alongside the |
| 30 | visible projection in one reducer action. A record outside a reader's window |
| 31 | retires its matching echo without inserting a row or requesting tail follow. |
| 32 | Matching prefers an already bound message ID; only unbound echoes may match a |
| 33 | submission ID. Conflicting bindings and different formal message IDs never merge. |
| 34 | |
| 35 | The same transaction records `visibleSubmissionHandoffs` for formal users in the |
| 36 | resulting window. ChatSource uses these hints to inherit a previously mounted |
| 37 | echo's key even when React skips the intermediate binding render. User, process, |
| 38 | and tail nodes share that stable turn key. Hints are pruned to resident formal |
| 39 | users; presentation maps are pruned to mounted groups. Neither survives session |
| 40 | replacement or restores a reclaimed echo. A missing send anchor is not permission |
| 41 | to prepend an echo to unrelated history: placement requires its original boundary |
| 42 | or explicit turn identity. |
| 43 | |
| 44 | A late message-ID binding first reconciles resident records. If the formal user |
| 45 | is no longer resident, the owning follower may use the existing exact-message |
| 46 | history query solely to confirm identity, without installing that page. Reads |
| 47 | are generation-fenced, coalesced per unresolved submission, and retried only after |
| 48 | committed coverage advances or reconnect. An inconclusive read retains the echo. |
| 49 | An identity event alone never starts a read: committed coverage must have advanced |
| 50 | since the follower first observed the unresolved submission. This watermark is |
| 51 | pruned with the submission. The synchronization engine loads on first follow, |
| 52 | and stopping before its module loads cancels that start. |
| 53 | RPC acceptance, failure, and unknown outcome belong to the local submission; |
| 54 | request ownership separately controls runtime state. Only a new explicit send |
| 55 | advances the scroll submission revision. |
| 56 | Snapshots retain unresolved submissions for the same canonical session ID; |
| 57 | a different snapshot owner clears echoes and handoffs and advances the session |
| 58 | generation. Remote callbacks read the state owned by their tab, never another |
| 59 | tab's last rendered state. |
| 60 | |
| 61 | History window and body I/O load on demand. Their routing identity is captured |
| 62 | before the deferred import, and Store generation checks still discard stale |
| 63 | completions. This keeps startup bytes within the existing bundle budget. |
| 64 | |
| 65 | `bench/submission-handoff.mjs` exercises the real follower, bounded Store, reducer, |
| 66 | Composer, and Transcript using 1,000 deterministic turns and 20 paging round trips. |
| 67 | Run it with `--electron` for the isolated Electron host. Its JSON evidence includes |
| 68 | DOM continuity, native selection, reader displacement, viewport writes, resident |
| 69 | entries, and presentation-map sizes. Scripted Electron input is not OS-native input |
| 70 | qualification; the report records that evidence separately. |
| 71 | In Linux CI, `--electron --native-input` runs inside Xvfb and uses X11 XTest |
| 72 | wheel, keyboard and scrollbar input through `xdotool`. The evidence includes |
| 73 | scroll extents and blank-frame samples as well as confirmation-time reader |
| 74 | stability. Run this mode only in an isolated graphical session. |
| 75 | |
| 76 | Terminal projection checkpoints live in the session's |
| 77 | `.transcript-projection.json` sidecar. The checkpoint records the provider prefix |
| 78 | count and digest, session head/rewrite identity, rows, runtime, and covered event |
| 79 | sequence. A failed checkpoint write keeps the WAL projection acknowledgement |
| 80 | pending. Recovery restores a matching checkpoint and replays its retained |
| 81 | suffix without executing tools. It does not seed an autosaved in-flight tail |
| 82 | and then append that same tail again. Existing display sidecars remain readable; |
| 83 | only their legacy migration path may use the old user-hash/occurrence mapping. |
| 84 | |
| 85 | Terminal records retain protocol-recovery tokens, incomplete-read/readiness |
| 86 | details, accepted partial-read receipts, cancellation and failure diagnostics. |
| 87 | Starting a new turn retires earlier recovery actions. Checkpoint capture copies |
| 88 | mutable metadata while sharing immutable body strings; disk encoding stays |
| 89 | outside the projection lock. |
| 90 | |
| 91 | ## Snapshot protocol |
| 92 | |
| 93 | Desktop exposes `TranscriptSnapshotForTab`, `TranscriptPageForTab`, |
| 94 | `TranscriptContentForTab`, and `TranscriptReplayForTab`. Serve exposes GET |
| 95 | `/transcript/snapshot`, `/transcript/page`, `/transcript/content`, and |
| 96 | `/transcript/replay` behind its existing authentication and host checks. |
| 97 | |
| 98 | `ResumeTranscriptSessionForTab` and `OpenChannelTranscriptSessionForTab` return |
| 99 | switch-phase diagnostics after adoption, without building a legacy history page. |
| 100 | The frontend records these only after the matching snapshot commits, including |
| 101 | a separate snapshot-install duration. An older host returning no diagnostics is |
| 102 | reported as unknown (`duplicateLoadCount: null`), never as proof of zero repeats. |
| 103 | Legacy page callers reuse a preload only if it matches the controller's captured |
| 104 | history digest; a changed, fully persisted runtime triggers a fresh durable read. |
| 105 | |
| 106 | Each snapshot includes protocol version, snapshot ID, session/head/rewrite/runtime |
| 107 | identity, projection revision, covered-through sequence, records, active owners, |
| 108 | and pending runtime prompts. Page and content requests carry the same snapshot |
| 109 | ID. Replay carries the identity and the last committed sequence. An expired cut |
| 110 | returns `stale`; a replay identity or retained-range mismatch requires a new |
| 111 | snapshot. Tab and remote-client generations are checked after asynchronous reads. |
| 112 | |
| 113 | The frontend suspends ordered ingress before requesting the snapshot. Mutable |
| 114 | owners' referenced prefixes resolve before rows, runtime and attempt state commit |
| 115 | in one reducer transaction. Only then does event coverage advance. Queued and |
| 116 | replayed events share one commit entry. Status polling supplies replay hints and |
| 117 | ancillary job counts; it cannot install a second transcript/runtime prefix. |
| 118 | |
| 119 | Older pages merge by record/item identity and backend order, including an active |
| 120 | user retained before the newest page. Delayed content patches check both the cut |
| 121 | and intervening item mutations. A page cannot resurrect a discarded attempt. |
| 122 | |
| 123 | Content resolution uses durable message identity. The presentation layer may |
| 124 | carry a local echo's key across canonical handoff, but that key never replaces |
| 125 | the item's `m:<messageId>` identity. Delayed patches target the current durable |
| 126 | item and reject intervening mutations before replacing its preview; a reclaimed |
| 127 | row cannot be recreated by a late patch. |
| 128 | |
| 129 | ## Bounds and compatibility |
| 130 | |
| 131 | Pages default to 120 records and 512 KiB, with a 2 MiB response ceiling. Large |
| 132 | string fields use 4 KiB previews and UTF-8-safe 64 KiB content chunks. Chunk reads |
| 133 | traverse typed fields directly instead of serializing the entire payload. |
| 134 | The same 2 MiB limit covers complete replay responses, including JSON escaping |
| 135 | and envelope overhead. Oversized replay pages request a fresh snapshot; the |
| 136 | covered data remains accessible through content chunks without advancing an |
| 137 | unreceived event cursor. |
| 138 | Snapshots retain at most three cuts under a 64 MiB estimated budget. The current |
| 139 | cut is pinned: a larger session remains readable and evicts older cuts. Settled |
| 140 | strings are shared, while immutable metadata and active prefixes are retained. |
| 141 | |
| 142 | The client retains unresolved previews only; successful expansion releases that |
| 143 | record cache. Active/running tabs are pinned. Inactive caches have a three-tab, |
| 144 | 32 MiB budget. Live gap buffers have a 1,024-event/8 MiB bound; overflow retains |
| 145 | the high-water mark and recovers the suffix from the ledger. |
| 146 | |
| 147 | An old Serve returning an unsupported endpoint/protocol or its HTML index stays |
| 148 | connected through the legacy path. The UI states that synchronization is limited |
| 149 | and reconnects may have missing or repeated content. Capability discovery never |
| 150 | upgrades or restarts Serve. Modern hydration uses metadata-only ancillary reads, |
| 151 | so it does not also download the old full `/history` payload. |
| 152 | |
| 153 | ## Validation |
| 154 | |
| 155 | Root-module tests cover attempt identity, checkpoint write failure, autosave |
| 156 | recovery, immutable cuts, nested content references, large sessions, and HTTP |
| 157 | session binding. Desktop tests cover planner cancellation, old-Serve discovery, |
| 158 | late remote responses, and metadata-only reads. The frontend snapshot client |
| 159 | tests exercise prefix/suffix races, optimistic keys, page ordering, tool pairs, |
| 160 | discard tombstones, and stale content writes through the real reducer. |
| 161 | |
| 162 | Run the root and Desktop Go suites separately. Run frontend `test:typecheck`, |
| 163 | `test:transcript`, `test:remote`, `test:stream`, and the production build. Browser |
| 164 | transcript and app-memory checks, together with native Electron replay, |
| 165 | remain separate acceptance gates; unit tests or cross-compilation do not replace |
| 166 | native evidence. |
| 167 |