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

Handbook / Architecture · Checked against the source on 2026-10-05 · https://www.hbcode.ai/docs/timeline

## The timeline joins three inputs into one list

**A row shows once per key, however many inputs hold a copy**

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

- **Typed key** (type + "\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 live** (origin: 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 echo** (user-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. The 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 five block types are user, system, activity, agent-reply, and thinking. A block ID is unique only inside its type. The first figure shows how clients merge rows by that key. Timestamps are order metadata and never deduplication identity.

After every material merge a client sorts again: by timestamp, then by the sourceOrder vector from the desktop, then by the identity key. sourceOrder is a short list of numbers that compares element by element. Its first number ranks the source of the row, so a history row sorts before a live row with the same timestamp. A row without sourceOrder sorts after rows with it at the same timestamp.

| Row source | sourceOrder | Where the numbers come from |
| --- | --- | --- |
| History, paginated | [0, 1, ordinal, entryIndex] | The ordinal of the saved item and the index of the row inside that item. Codex pages and Claude pages both use this form. |
| History, legacy Codex read | [0, 0, ordinal, entryIndex] | The position of the item in a thread/resume response. |
| Live row | [2, sequence] | A counter in the desktop process. The row gets its number once, when it first enters the run. |
| Status row of an active run | [2, startedAt] | The start time of the run in milliseconds, so each rebuild gives the same position. |
| Synthetic row | [3, sequence] | A row that the desktop made without a transcript item or a live event. |

Both agents obey one rule for IDs: a live row must carry the ID that its saved row will have. Settlement and the client merge compare keys only. An adapter that gave a live row a different ID would show that row twice.

Desktop reads owned paginated Codex 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 thread ID, the raw sidecar cursor, a generation, that source token, the prior leading block key, and the window revision. A cursor for another thread is rejected. Desktop does not parse Codex JSONL and does not query the child’s private databases.

A Claude session uses the same typed keys, the same order, and the same row types. Only the history source differs. The desktop reads Claude’s saved transcript through the Claude Agent SDK and projects each message into timeline rows. The boundaryToken is the Claude session ID, or the subagent ID for a child thread. oldestCursor is JSON with four fields: session holds that ID, before holds the UUID of the oldest loaded message, anchor holds the leading block key, and revision holds the window revision. The desktop accepts at most 4,096 characters and rejects a cursor whose session differs from the open conversation or subagent. When the transcript no longer holds the before message, the page comes back empty with reloadRequired. A transcript that does not exist loads as an empty conversation. Saved Claude history starts after the last compaction, because the SDK returns only that part.

- [Claude history and the shared timeline rows](https://www.hbcode.ai/docs/claude-agent-sdk.md#history-projection): How a Claude message becomes rows, and which IDs its text, thinking, and tool rows get.

## Live overlay revisions and the settlement epoch

A timeline contains canonical history plus live blocks that are not yet fully represented there. The desktop saves one overlay snapshot per target in the table chat_live_timeline_overlays, with the key app_session_id plus timeline_target_id. The target is the session for the main conversation, and the thread ID or subagent ID for a child. The 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**

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

- **Settlement epoch** (liveOverlay.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.
- **Revision** (liveOverlay.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 window** (run.chatLoad · run.chatReconcileSessions): 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. run.chatReconcileSessions does this for at most 16 sessions in one call.

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, the single row of chat_live_timeline_overlay_revisions, so a deleted target cannot reuse an old revision. An empty target snapshot is persisted so the target keeps its epoch and revision. 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.

The overlay holds two classes of rows. An ordinary live row has a saved counterpart in the agent’s history: a user message, reply text, a command, a tool call. The other rows are ephemeral. No history row ever replaces them.

| Row class | What it is | How it leaves the overlay |
| --- | --- | --- |
| Ordinary live row | A row whose key also appears in saved history. | A history read proves its key. The figure below shows the three reads. |
| Status row | The thinking block with the ID thinking-<runId>. Its phase label comes from the newest activity row: Reasoning, Running command, Editing files, Reading, Using tool, Planning, or Working. | A newer status row for the same target replaces it. A latest window drops it when another run is the current run. The Codex cleanup after a run removes it. |
| Context-compacting marker | The marker that shows while the agent compacts its context. | A latest window drops it when its run is not the current run. The Codex cleanup after a run removes it. |
| Parent progress mirror | A copy of a child thread’s progress in the parent conversation. | A mirror write from another run replaces it. A latest window drops it when another run is the current run. The Codex cleanup after a run removes it. |
| Approval card | An activity row with an approval field. | It stays with its decision. The Codex cleanup after a run removes it with the other ephemeral rows of that run. A Claude run has no such cleanup, so its card stays in the overlay. |

The settlement epoch is a target safety fence. The desktop advances it in the same write that removes at least one ordinary live row because history now holds that row. Ephemeral rows never advance it. A publisher can also retract a row that it no longer shows, because no history row would ever settle such a row.

**A latest window settles a live row for both agents, and only Codex has two more paths**

Four cases of the same live row A on one time axis, with the end of the run and an app restart as fixed moments. When a client loads a latest window during the run, the page holds A and the overlay drops it, for both agents. When no client loads, a Codex run ends with an exact scan that settles A. When the app quits before that scan, the cleanup journal row survives and the repair settles A after the restart. A Claude row stays in the overlay through the end of the run and the restart until the next latest window loads.

- **Latest window** (run.chatLoad · run.chatReconcileSessions): Shared by both agents. The desktop removes every overlay row whose key the loaded page holds, and raises the epoch in the same write.
- **Exact scan** (thread/items/list · 3 scans · 100 ms, 250 ms): Codex only. After the terminal event the desktop scans history to the end for the exact keys of that run. A missed key gets a second scan after 100 ms and a third after 250 ms.
- **Restart repair** (journal row: session, run): Codex only. The journal row names the session and the run. It stays until a scan has settled every row of that run, and the repair starts when a target of the session opens.

Three reads can settle an ordinary row. The first is shared: when a latest window loads, the desktop removes every overlay row whose key that page holds. Both agents call the same function for it, and it is the only read that a Claude session has. The other two are Codex paths. At the end of a Codex run the desktop writes the session and the run into chat_pending_terminal_overlay_cleanups. Then it scans thread/items/list to the end for the exact keys of that run in every target of the session. A scan that misses a key runs again after 100 milliseconds and after 250 milliseconds, three scans in total. The whole cleanup has 2 minutes. If it fails or the app quits, the journal row stays, and the repair runs the same exact scan when a Codex target of that session opens.

Phones take several sessions in one call. run.chatReconcileSessions accepts at most 16 sessions and returns for each the latest window, the overlay, the outbox, and the active run. It allows 256 outbox entries per session, and it checks the 8 MiB limit while the response grows. Mobile applies a window and its overlay in one reducer operation.

## Live delivery: one function sends to three sinks

Every live change leaves the desktop runtime through one function, emit_chat_stream_event. It feeds three sinks: the WebView event chat:stream, the WebView event chat:progress, and the relay event chat:timeline-updated. The event name inside the payload decides which sinks get it.

**One write goes to three sinks, and each sink gets a different part**

A Codex run starts, writes the overlay three times inside 50 milliseconds, stays quiet, and completes. Each write goes to the relay at once. A subscribed desktop window gets one event with the newest snapshot when the 50 millisecond timer ends. After a full 5 second tick with no new write the runtime sends the saved overlay to the relay again, and no window gets that repeat. Only the started and completed events reach chat:progress. The revision numbers are examples.

- **Block snapshot** (event: blocks · liveOverlay): The whole overlay of one target after a write. It goes to the relay and to chat:stream, and never to chat:progress.
- **Coalescing** (50 ms · 128 targets): The first snapshot for a target starts the timer, and a newer snapshot replaces the waiting one. A run event sends the waiting snapshots first.
- **Subscription** (subscriptionId · streamRevision): A window gets snapshots only for the target that it subscribed to. Run events go to every window.
- **Relay event** (chat:timeline-updated): Sent only for a session in which a phone holds interest. The desktop does not coalesce it.
- **Repeat** (every 5 s): For each target of an active run with no new write since the last tick, the runtime reads the saved overlay and sends it to the relay again.

| Payload event | chat:stream (desktop windows) | chat:progress (desktop) | Relay event (phones) |
| --- | --- | --- | --- |
| blocks | Only windows with a live subscription for that target, coalesced for 50 milliseconds. | No. | Yes, at once. |
| started, completed, failed, aborted | Every window. | Yes, as event, appSessionId, runId, timestamp, and operationId. | Yes. A failed event also carries agentErrorInfo. |
| runtime | Every window. | No. | Yes. |
| agent-threads | Every window. | No. | Yes. |

An agent-threads payload has only event, appSessionId, timestamp, and a threadId of null. It tells every client that the agents list of the session changed: a child thread or a subagent started, or one of them has a new status. The client then loads the list again. No timeline row carries this fact. No row stands for a subagent or a workflow, and no row opens a thread. A session sends its first signal at once, and then at most one more signal at the end of a window of 1 second.

A blocks payload is a snapshot: the whole overlay of one target after a write, as liveOverlay. The producer saves the overlay first and sends the saved snapshot, so a client that missed an event loses nothing that the next snapshot does not carry.

A desktop window subscribes to one target at a time. The subscribe call returns the saved overlay, so the window starts from a complete state. After that the runtime sends the window only snapshots of its target and tags each with the subscriptionId and a streamRevision, a counter shared by all windows. The first snapshot for a target starts a 50 millisecond timer. A newer snapshot for the same target replaces the waiting one, and the timer sends one event. At most 128 targets wait at once. One more target sends all of them at once. A run event sends the waiting snapshots first, so a window never sees completed before the last rows.

The relay sink forwards a payload only for a session in which a phone holds interest. A blocks payload must also have a liveOverlay with exactly settlementEpoch, revision, and blocks, a revision above 0, and both numbers inside the exact integer range. The desktop does not coalesce this sink.

A phone can lose an event on the way. For that case a repair timer runs every 5 seconds while a run is active. For each target that had no new write since the last tick, it reads the saved overlay and sends it to the relay again. The repeat does not go to desktop windows. The shared live emitters arm this timer. The Codex adapter sends every row through them. The Claude adapter saves and sends its rows directly and uses a shared emitter only for an approval card, so a Claude run has the timer only after its first card.

- [Timeline events reach only interested phones](https://www.hbcode.ai/docs/device-routing.md#interest): How a phone registers interest in a session and how long the relay keeps it.
- [Claude live runs](https://www.hbcode.ai/docs/claude-agent-sdk.md#live-runs): How a Claude run publishes its rows and ends.

## Approvals use one shared model

An approval request from either agent becomes the same ChatApprovalRequest. It names the request ID, the thread, the turn, the item, and the decisions that this request allows. The desktop refuses a decision that is not in that list.

| Part | Values |
| --- | --- |
| Kind | commandExecution, fileChange, permissions. |
| Decision | accept, acceptForSession, acceptBroadly, decline, cancel. |
| Result | accepted, acceptedForSession, acceptedBroadly, declined, cancelled, resolvedExternally. |
| Card status | pending, then the result. resolvedExternally shows as expired. |

The card is a live activity row of kind event. Its ID starts with approval: and the request ID, followed by the thread ID, the turn ID, and the item ID, each with its length in front. The same ID carries the pending card and the decided card, so the decision replaces the card in place. The card is an ephemeral overlay row.

Clients answer with run.chatResolveApproval, which carries requestId, threadId, turnId, itemId, and decision. It carries no session ID. The dispatch layer asks the Claude adapter first, which answers when it holds an open permission with that request ID. Otherwise the Codex app-server that owns the request settles it.

- [Codex: who answers an approval](https://www.hbcode.ai/docs/codex-turns.md#approvals): The Codex request types, the deadline, and what an expired request does.
- [Claude live runs and approvals](https://www.hbcode.ai/docs/claude-agent-sdk.md#live-runs): How a Claude permission request becomes a card and how the answer returns to the host.

## The desktop writes the words of each row

The desktop owns the words and the facts of an activity row, and each client owns the layout. A client shows the words as they are and makes no words of its own. Desktop tests check these rules on the rows of the shared samples.

| Field | Rule |
| --- | --- |
| title | The headline of the row, for example “Message to Android landing”. |
| subtitle | The context line, for example the summary of a message. It never repeats the title. A command row has none. |
| display.signal | A fact, for example “Exit 1”, “Draft 2”, or “Sent”. It is never a status word, and it never repeats the title or the context line. |
| status | The state of the row: in-progress, completed, failed, or declined. The context line and the signal never say a status word. |
| detail | The text that opens on a tap, with its format: markdown, plaintext, or shell. A plaintext detail has a languageHint only when the text is complete JSON, JSONL, a unified diff, or a FILE <path> dump. A file read has a sourcePath. A message row has no detail. |

Each client shows a plaintext detail without a languageHint and without a sourcePath as plain wrapped text. It guesses no language, colors no keywords, and shows no line numbers.

## Message rows carry one typed message fact

A row that is a message between two sessions or between two agents carries one typed fact, message. An activity row has it as activity.message. A user row that another session sent has it as message. The desktop makes every word of the fact. The desktop window and the phones show these words and make no words of their own.

| Field | Content |
| --- | --- |
| direction | sent or received. |
| kind | message, or task on a received row that gives an agent its task. |
| from, to | The words for the two ends. They are never blank and never an ID. The end that shows the row is “This session”, or “This agent” in an agent thread. |
| peer | The kind of the other end, session or agent, and its agent, codex or claude. The agent is absent only when the call named a session that does not exist. |
| delivery | Only on a sent row whose call ended: sent, needs-approval, or not-sent. |
| failureReason | Only when delivery is not-sent: the first line of the reason, at most 220 characters. |
| link | The other end of the message: appSessionId (absent: this session), threadId (absent: the main timeline), and an opaque anchor. |

The words for the other end:

- Another HB Code session: its title, “Untitled session” for a blank title, or “Unknown session” when the call named no session.
- The lead agent of a Claude session: “Claude”.
- A Claude subagent: its title in the agents list, else “Subagent”.
- A Claude session outside HB Code: its name, else “Another Claude session”.
- Codex: “Codex” for the root thread, and the words of the agent path for an agent thread, for example “Android landing”.

The desktop also makes the title of the row: “Message to <to>” on a sent row, and “Message from <from>” or “Task from <from>” on a received row. A user row has a title only together with message. Each client uses the title as the accessibility label and as the title of the message detail. display.signal is Sent, Needs approval, or Not sent, from delivery. A row has no signal while its call runs. A message row has no detail on the wire. Each client makes the detail from the title, the message fact, and the text of the row.

These rows are message rows: the result of a send_session_message call, the user row that another session sent, a Claude SendMessage call, the messages of the lead agent in a Claude subagent thread, a message from another Claude session, and a message that a Codex thread got from another agent. A send_session_message call that still runs stays the plain call row “Send message to session”.

A row links to its other end when a timeline of HB Code holds that end. A sent session message links to the user row in the recipient session, but only while its delivery is sent. The received user row links to the call row of the sender, in the main timeline or in the agent thread that sent the message. A Claude SendMessage to a subagent of this session links to the subagent thread at the time of the call. A lead message in a Claude subagent thread links to the main timeline at the time of the message. Codex rows and messages from another Claude session have no link. The desktop never compares text to find the other end.

The delivery of a sent session message follows the queue of the recipient. The table chat_session_messages keeps it: sent, needs-approval while the message waits for you, or declined after you removed the message from the queue. A declined message shows as not-sent with the reason “The user removed the message from the queue.” Each change of the queue publishes the row of the sender again.

- [Messages between sessions](https://www.hbcode.ai/docs/message-delivery.md#session-messages): The envelope, the limits, and the hold rules on the desktop.

## How an older page joins the list

**An older page attaches only at the row its cursor was cut from**

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

- **Seam** (boundaryBlockKeys): 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 revision** (windowRevision): 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.
- **Cursor** (thread-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.chatLoadPageBefore and run.chatLoadThreadPageBefore with pageSize, beforeCursor, and expectedBoundaryToken. In a Codex session, a cursor whose source token differs from the current source fails with Timeline source changed, and one request fetches up to four sidecar pages until one yields a visible block. A response over 8 MiB fails with errorCode TIMELINE_SNAPSHOT_TOO_LARGE. Phones wait 115 seconds for run.chatLoad and run.chatLoadThread. 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. |
| Anchored window | 40 entries, for every client. |
| 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. |

> **A client timeout does not cancel the desktop work** The client timeout controls waiting and recovery. It does not cancel desktop execution or prove that a submitted mutation was not saved. Use the outbox and RPC identity rules when an acknowledgement is missing.

## An anchored load opens the other end at its row

A client follows a link in two steps. It opens the session and the thread of the link with its normal open path. Then it loads that timeline with an anchor. run.chatLoad, run.chatLoadThread, chat_load_command, and chat_load_thread_command take one typed anchor.

| Anchor | Source |
| --- | --- |
| {kind: "link", token} | The message.link.anchor of a message row. |
| {kind: "newer", token} | The newerCursor of an earlier answer for the same timeline. |
| {kind: "row", row: {type, id}} | The type and the id of a block of the timeline that loads, as an answer gave them. |

A token is opaque: the prefix ta1. and base64url JSON that the desktop wrote. A client never makes or reads a token. A blank token or a blank row key fails as invalid params on the relay and as a validation error in the desktop window. A token of the other kind, or a text that the desktop did not write, fails with “The timeline anchor is invalid; reload the timeline”.

The answer is the normal load answer with three more fields, which are always present. anchorRow is the block to center and mark. anchorNotice is a sentence of the desktop when the load opened no row, for example “This message is not in the saved history.” or “This row is not in the saved history.” A link load or a row load has exactly one of the two. newerCursor is set when the window is a page in the past. Such a page has no active run, and a client shows no live overlay rows below it. A load with the newer anchor gives the rows that follow the page, with no gap and no overlap. The client repeats this load until an answer has no newerCursor.

The desktop opens every anchored window with 40 rows, the value of mobileChatPolicy.timeline.anchoredPageSize, whatever pageSize the request names. A place in the newest 40 rows gives the normal newest window. For Claude, one host read, historyAround, finds the page that holds the place, and the place is in the middle of that page. For Codex, the desktop reads pages back from the newest page until a page holds the place. A row that is only in the live overlay has no saved place, so a link to it gives a notice.

No client has a Back control or a Forward control. The way back is the link on the row at the other end. In each client the name of the other end in the label line of the row is the link. A tap on another part of the row opens the message detail. When a gap recovery runs while a page in the past shows, the client loads that page again with a row anchor at the row that the reader reads. The reading position stays.

- [iOS: messages between sessions](https://www.hbcode.ai/docs/ios/chat.md#session-messages): The peer link, the exact landing, and the queue on the iPhone.
- [Android: messages between sessions](https://www.hbcode.ai/docs/android/chat.md#session-messages): The peer link, the exact landing, and the queue on Android.

## 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 the agent retained. Rendering or reconciling a timeline never reads output bytes. An open viewer fetches them separately.

- [Command output, documents, and previews](https://www.hbcode.ai/docs/output-and-previews.md): Artifacts, ranged reads, the HTML sandbox, web pages as Markdown, and file transfers.

## 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.
- Send three snapshots for one target inside 50 milliseconds. A subscribed window must get one event with the newest overlay, and a completed event must arrive after it.
- End a Claude run and load no window. The live rows must stay in the overlay and leave with the next latest window.
- 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.
- Follow a link to a row that is older than the newest window, then follow the link on that row back. Each load must open the exact row, and a page in the past must show no live rows below it.
