Skip to article
PlanToCodeDocsGet the app

HandbookArchitecture

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

A row shows once per key, however many inputs hold a copy
A row shows once per key, however many inputs hold a copyYour pending sends, the live overlay, and history pages can each hold a copy of the same row. A client keeps one row per typed key: a history copy wins over a live copy, a pending echo hides once any row carries its operation ID, and two messages with the same text stay two rows because their keys differ. The keys and messages are examples.
useruser-message::op-41
activityitem-7
agent-replyitem-9
useruser-message::op-42
typed keytype, then ID
pending sendoutbox echo
live overlayorigin: live
history pageorigin: canonical
you seeone row per key
npm testrunning · hidden
run it againsending · hidden
run it againsent earlier
run it againsent earlier
npm testexit 0
npm testexit 0
All tests passstreaming
All tests passstreaming
run it againlive row
run it againlive row
“run it again” shows twice: same text, different operation ID. Text is never identity.
Order comes from timestamp, then sourceOrder, then key, never from arrival.
  • 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.

InputAuthorityWhen to reconcile
Canonical history windowThe desktop history route and its boundary token.On initial load, refresh, and older-page splice.
Live overlayDesktop settlementEpoch, revision, and typed live blocks.On valid current-target snapshots or live updates.
Outbox and submitted echoDesktop custody and acceptance; local UI can show pending intent.When the outbox changes or canonical evidence confirms an accepted operation.
Viewport and measurementsThe 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.

The overlay drops a live row only in a snapshot that carries its history row
The overlay drops a live row only in a snapshot that carries its history rowRow A is live on the desktop app and on a phone. When the desktop app loads a latest window whose history page holds A, the desktop removes A from the overlay and raises the settlement epoch in that same response. The phone refuses the next live update because its epoch is newer, keeps its rows, and loads its own latest window, so A turns into a history row without ever disappearing. The dashed bar shows what applying that update would have done. The epoch numbers are examples.
Desktop appchat view
Desktop overlaylive rows
Phonesame session
A · live row
A · history row
holds A · epoch 3
A · live row
A · history row
if applied: A gone
latest window
history with A + overlay without A
epoch 3 → 4
next update · epoch 4
newer epoch: not applied, rows kept
latest window
history with A + overlay without A
  • 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

An older page attaches only at the row its cursor was cut from
An older page attaches only at the row its cursor was cut fromThe cursor remembers the oldest row the window held when it was issued, the seam. The desktop returns the older rows followed by that seam key, with a window revision one higher. A client prepends the rows only while that seam is still in place and the revision is newer, and otherwise drops the page. The next cursor’s seam is the top row of the page. Row names are examples.
run.codexChatLoadPageBefore pageSize · beforeCursor · expectedBoundaryToken
your window
older history, not loaded
item-120oldest row
newer rows
oldestCursor: seam item-120, revision 1
page from the desktop
3 older rowsitem-117 · item-118 · item-119
item-120seam key, not a new row
windowRevision 2 · orderedBlockKeys: older rows, then the seam
window after the splice
item-117
item-118 · item-119
item-120page attached above
newer rows
next oldestCursor: seam item-117, revision 2
seam
Seam missing (on phones: not the oldest row) or revision not newer: the page is dropped. Phones reload the latest window, and the desktop stops paging from that cursor.
Source token differs from expectedBoundaryToken: the desktop sends an empty page with reloadRequired, and every client reloads the latest window.
  • 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 policyValue
Initial timeline page12 entries.
Older page / maximum page100 / 100 entries.
Initial timeline timeout115 seconds.
Paginated timeline timeout60 seconds.
Outbox command acknowledgement timeout20 seconds.
Initial timeline retryAt 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.