Timeline: history pages, live overlay, settlement
How one ordered conversation is assembled from saved history pages, a revisioned live overlay, and pending sends, and why identity rather than text or time decides what exists.
Checked against the source on 17 September 2026
On this page
Three inputs, one list
- Typed keytype + "\0" + id
A history copy and a live copy of the same row carry the same key. A client keeps one row per key and never matches rows by their text.
- History over liveorigin: canonical
When a history copy and a live copy share a key, the client shows the history copy. The desktop drops the live copy from the overlay once that row settles.
- Pending echouser-message::<operationId>
The desktop draws a message you send at once, under the key its history row will have, and hides that echo once any row carries its operation ID.
| Input | Authority | When to reconcile |
|---|---|---|
| Canonical history window | The desktop history route and its boundary token. | On initial load, refresh, and older-page splice. |
| Live overlay | Desktop settlementEpoch, revision, and typed live blocks. | On valid current-target snapshots or live updates. |
| Outbox and submitted echo | Desktop custody and acceptance; local UI can show pending intent. | When the outbox changes or canonical evidence confirms an accepted operation. |
| Viewport and measurements | The selected client view. | When switching target, loading older content, or following new activity. |
Typed identity and canonical order
Each block has a typed identity key, the block type joined to the block ID with a NUL byte. The first figure shows how clients merge rows by that key. After every material merge the one canonical order is re-established by timestamp, then by the desktop-supplied sourceOrder vector, then by the identity key. A row without sourceOrder sorts after rows with it at the same timestamp. Timestamps are order metadata and never deduplication identity.
Desktop reads owned paginated history through thread/items/list, newest first, at most 100 items per sidecar page. It preserves createdAtMs and updatedAtOrdinal, an unsupported public item appears as a diagnostic row that still advances the cursor, and an entry with neither or both of item and unsupportedItem fails the whole page. The boundaryToken a client receives is the SHA-256 of the rollout path, or of owned-thread-store plus the owner ID, so it identifies the history source rather than a page. oldestCursor is the prefix thread-items-v5: followed by base64url JSON that carries the raw sidecar cursor, a generation, that source token, the prior leading block key, and the window revision. Desktop does not parse Codex JSONL and does not query the child’s private databases.
Live overlay revisions and the settlement epoch
A timeline contains canonical history plus live blocks that are not yet fully represented there. The persisted overlay snapshot format is version 2. It stores formatVersion, settlementEpoch, revision, and blocks containing wire data plus settlement metadata. The client receives only settlementEpoch, revision, and wire blocks. Internal settlement identity and provenance stay on desktop.
- Settlement epochliveOverlay.settlementEpoch
Rises only when a live row moves into history. A client takes a higher epoch only from a latest-window snapshot, because only that snapshot also carries the history rows.
- RevisionliveOverlay.revision
One global counter on the desktop. Within an epoch a higher revision replaces the whole overlay, a lower one is ignored, and the same revision with different rows makes the client reload.
- Latest windowrun.codexChatLoad · run.codexChatReconcileSessions
The desktop reads the newest history page, removes overlay rows whose keys that page holds, and returns both in one response. The desktop app calls the same builder.
liveOverlay is { settlementEpoch, revision, blocks } and contains the full unresolved live projection for one target. Within one settlement epoch an empty higher revision clears it. Revisions come from one durable global sequence, and an empty target snapshot is persisted so the target keeps its epoch and revision.
The settlement epoch is a target safety fence. Desktop advances it whenever at least one ordinary live row has left the overlay for canonical history: while building a latest window, after a run’s terminal event, and during restart repair. After a terminal event it records only the session and run, then scans thread/items/list to the end for every overlay identity of that run, retrying visibility up to three times with 100 and 250 millisecond delays. Mobile applies a window and its overlay in one reducer operation. Overlay writes compare an expected revision inside a transaction, the counter is capped at JavaScript’s exact integer limit, 9,007,199,254,740,991, and a stale writer retries against current state rather than overwriting it.
Pagination is a splice contract
- SeamboundaryBlockKeys
The key of the oldest row the window held when the cursor was issued. The desktop reads it back out of the cursor and returns it after the older rows.
- Window revisionwindowRevision
One higher than the revision inside the cursor. A client applies a page only if this is higher than the revision it holds, so the same page never lands twice.
- Cursorthread-items-v5:<base64url>
JSON with the thread ID, the sidecar cursor, a generation, the source token, the seam key, and the revision. Clients send it back as beforeCursor, with their boundaryToken as expectedBoundaryToken.
Older pages come from run.codexChatLoadPageBefore and run.codexChatLoadThreadPageBefore with pageSize, beforeCursor, and expectedBoundaryToken. A cursor whose source token differs from the current source fails with Timeline source changed. One request fetches up to four sidecar pages until one yields a visible block, and a response over 8 MiB fails with errorCode TIMELINE_SNAPSHOT_TOO_LARGE. Phones wait 115 seconds for run.codexChatLoad and run.codexChatLoadThread. The desktop gives those two methods a 95-second dispatch cap around a 90-second handler deadline, and every other method, older pages included, 45 seconds. Its RPC decoder accepts a pageSize up to 640 before the runtime enforces 100.
| Current manifest policy | Value |
|---|---|
| Initial timeline page | 12 entries. |
| Older page / maximum page | 100 / 100 entries. |
| Initial timeline timeout | 115 seconds. |
| Paginated timeline timeout | 60 seconds. |
| Outbox command acknowledgement timeout | 20 seconds. |
| Initial timeline retry | At most 3 retries after the first request, only for the same current timeline target. Base delay 0.75 seconds, maximum 3 seconds. |
Desktop uses a virtualized React timeline
useTimelineVirtualizer adapts the vendored @timeline-virtualizer to stable row keys, row height estimates, measured elements, and inner reading anchors. It anchors short content to the end, follows appends only when autoScrollToLatest is enabled, and can restore a previous reading position and measurement cache. Direct DOM positioning is enabled for scrolling.
WorkspaceTimelineLayers distinguishes the parent conversation from the selected child thread. It assigns viewport ownership to the active target and uses retained state for the parent. Ownership includes a target, viewport ID, activation epoch, mode, and active flag. A selected thread change therefore affects view ownership as well as the row list.
Command rows carry descriptors, not output
A command row carries the command text and either an artifact descriptor or the output Codex retained. Rendering or reconciling a timeline never reads output bytes; an open viewer fetches them separately.
Useful regression cases
- Switch projects while a history request is in flight. The old response must not replace the new target.
- Move live content into canonical history and return an empty overlay with a newer settlement epoch. Old live rows must not reappear.
- Load older messages while the assistant streams. Preserve the reading anchor and avoid duplicate rows.
- Return an unavailable or malformed history response. Preserve readable history and expose recovery rather than showing a false empty conversation.