Most sessions are linear: each message, call or result follows the previous one. ASR requires no pointers for linear sessions and marks the exceptions explicitly: a rewind record when the source records a rewind event, branchesFrom at a real branch point in a source that stores a tree, and heads when the current leaf is not the last record.

Order

Records are ordered by seq (ASR-014). Order is observation order, not context order: a permission decision, a usage report or a file change can sit between two messages without being part of the conversation.

Context-participating and audit-only records

ASR-350 The context-participating record types are exactly message, tool_call, tool_result and compaction. Every other type, including vendor types, is audit-only. Audit-only records are facts about the session. They are never the context predecessor of another record. ASR-351 Structural refs and the branch_summary exception follow the reference-resolution table. A resolved structural target MUST be a context-participating record.

Nodes and revisions

The structure is built over nodes. A node is the revision chain of a context-participating record (see Missing data and revisions): its position is the seq of the chain’s first record, and its content is the chain’s latest revision. A record ID or entity ref resolves to the node containing the record or chain it names. ASR-352 A superseding record is not a new node: it takes the position of the chain it belongs to. A superseding record MUST have the same type and the same entity key as the record it supersedes. It MUST have the same branchesFrom, unless it is a marked rewrite: a record with assembly.mode: "rewrite", which the source wrote again under a new native ID with a marker naming the record it replaces (OpenClaw’s supersedesEntryId). A marked rewrite MAY have a different branchesFrom, which then applies to the whole chain (see Implicit predecessor). OpenClaw rewrites part of a transcript (to shorten a tool result, or after an edit) by writing the affected entries again at the end, each with a new ID and a pointer to the entry it replaces. The registry maps each rewritten entry to the entity of the first entry in its chain, so the rewrite is a revision, and the rewritten entry may hang from a different native parent.
These records come from an OpenClaw session in which the user edited a message. OpenClaw wrote the message again as entry 9a8b7c6d, pointing to entry c4d5e6f7; the revision keeps the first entry’s ID as its messageId. The path index of a node on a path is its zero-based position in that path.

Implicit predecessor

The predecessor of a node is computed from the first record of its chain, r. Let b be the branchesFrom of the chain’s latest marked rewrite, if the chain has one; otherwise b is the branchesFrom of r, if any.
  1. If b is present:
    1. {"record": <ref>}: the predecessor is the node the ref resolves to.
    2. {"record": null}: the predecessor is START (the node begins a new path with an empty context).
    3. {"state": "unknown"}: the predecessor is UNKNOWN.
  2. Otherwise, scan the stream backwards from r (records with lower seq), ignoring superseding records:
    1. On reaching the first record of a context-participating chain m, the predecessor is m’s node.
    2. On reaching a rewind record w, the predecessor is the node w.to resolves to, or START if w.to is null.
    3. On passing a gap record, or a malformed line (ASR-009), record a gap marker on the edge into r’s node and continue scanning.
    4. On reaching the start of the stream, the predecessor is START.
ASR-353 Readers MUST compute predecessors exactly as above.

rewind

A rewind records that the source moved the conversation back to an earlier point: a user rewind, an edit of an earlier prompt, a retry, or a rollback of turns. Records after the rewind continue from its target. Records between the target and the rewind stay in the stream as an abandoned branch.
ASR-354 A rewind MUST contain to and reason. ASR-355 to MUST name the last record that remains. A converter for a source whose rewind names the first removed item (for example a rewind “to” a message that is itself deleted) MUST map it to that item’s predecessor, or to null if it has none. ASR-356 to follows the reference-resolution table. A non-null target MUST be context-participating and have a lower seq. A rewind MUST NOT carry supersedes and MUST NOT be superseded. ASR-357 File restoration is a separate fact. restoreRequested records the request. Observed results of a restoration MUST be recorded as file_change records with cause: {"restore": <ref to the rewind>}, including files the restoration skipped (with status: "skipped") when the source records them.

branchesFrom

branchesFrom marks a real branch point in a source that stores the conversation as a tree without rewind events. It names the record’s actual predecessor.
ASR-358 branchesFrom MUST contain exactly one of record or state, and MUST appear only on context-participating records. state MUST be unknown. A record ref follows the reference-resolution table; its target MUST have a lower seq. ASR-359 A producer MUST NOT write branchesFrom when the record’s predecessor is its implicit predecessor. ASR-360 When the source shows a branch but the predecessor cannot be established, the producer MUST write {"state": "unknown"} and MUST NOT invent an edge.

The mechanical-sibling rule

Several harnesses store messages as a tree with parent pointers: Claude Code (uuid/parentUuid), Pi (id/parentId), OpenHands (parent_id), Devin (parent_node_id), Qwen Code. In these trees most sibling sets are mechanical: parallel tool calls, one response split across several native records, attachments written concurrently, retries after an error. Only a minority are real branches, where an earlier point was resumed and later content abandoned. Copying parent pointers verbatim produces branches that do not exist; ignoring them loses real rewinds. Every converter of a tree-stored source applies the rule below, so that two converters produce the same structure from the same file.

Inputs

For the agent’s native format, its registry entry names:
  • the native parent of a native record: the field that points to its context predecessor, and any documented logical-parent field used where the native parent is intentionally empty (for example a compaction boundary);
  • the response key: the field that identifies one model response, shared by all native records of that response;
  • which native records are injected, failed, or not context-participating.
A native unit is one native record. When one native record maps to several ASR context-participating records (for example a text block and a tool call), those records form one unit: they are linear among themselves in seq order, and only the unit’s first record is subject to the rule. The unit’s attachment point is its last ASR record: native children of the unit attach after it. For an ASR record x, rk(x) is its response key: the native response key for model output, the response key of its paired tool_call for a tool_result, and none otherwise. x is injected when it is a message with author harness or hook, or origin injected, memory, retrieval or notice. x is a failed attempt when it is an assistant message with status error or interrupted.

Rule

Process native units in observation order. Keep the tip T: the attachment point of the most recently placed unit, initially START. The current path is the active path ending at T. For each unit u with first ASR record r:
  1. Resolve the parent. Let q be the native parent of u. If q names a native record that is not mapped to a context-participating record, replace q by that record’s own native parent, repeatedly. If the native parent is empty and the registry documents a logical parent for this record, use it.
  2. No parent. If q is empty: if T is START, r is linear; otherwise write branchesFrom: {"record": null}. Go to step 7.
  3. Unknown parent. If q names a record that cannot be found in the source, write branchesFrom: {"state": "unknown"}. Go to step 7.
  4. Let p be the attachment point of the unit q names. Linear. If p is T, r is linear. Go to step 7.
  5. Off-path parent. If p is not on the current path, write branchesFrom: {"record": <id of p>}. Go to step 7.
  6. Displaced segment. Let S be the nodes on the current path after p, up to and including T. S is mechanical for u when u’s first record r is injected, or every node s in S satisfies at least one of:
    • M1 (same response): rk(s) is defined and equals rk(p) or rk(r);
    • M2 (injected): s is injected;
    • M3 (failed attempt): s is a failed attempt.
    If S is mechanical, r is linear. Otherwise write branchesFrom: {"record": <id of p>}.
  7. Set T to u’s attachment point.
ASR-361 A converter of a tree-stored source MUST determine branchesFrom exactly by this rule, using the fields the agent’s registry entry names. ASR-362 A converter MUST NOT use content similarity, timestamps or file positions other than observation order to decide whether a sibling is mechanical. Under this rule a regeneration of a complete response (a new response to the same prompt, after the previous response was complete) is a real branch: the earlier response left the context. An automatic retry after a failed attempt is mechanical.

Worked example

Native records of a tree-stored source, in file order (seq = line × 10):
  • r30: parent a1 maps to attachment point r21, which is T: linear.
  • r40: parent a1 maps to r21, but T is r30. S = [r30]; rk(r30) = X = rk(r21): mechanical (M1), linear.
  • r50, r60, r70, r80: each parent is T: linear.
  • r90: parent a3 maps to r60; T is r80. S = [r70, r80]; r70 is a human prompt with no response key: not mechanical. r90 gets branchesFrom: {"record": "r60"}.
  • r100: linear.
The active path from the default head r100 is r10, r20, r21, r30, r40, r50, r60, r90, r100. It keeps the second tool call r30, which the native tree leaves on a dead-end branch even though the model saw it. r70 and r80 are an abandoned branch.

Heads

A head is a leaf from which an active path is followed. session_info.heads lists the current heads; the first is the primary head. Multiple heads represent concurrent lanes or a forest (Pi lanes, Devin message forests). The default head is the predecessor that a context-participating record appended at the end of the stream would have: apply the implicit-predecessor scan from the end of the stream. If the stream ends with a rewind, the default head is the rewind’s target. ASR-363 Effective heads are the non-empty session_info.heads value, or the default head when that value is empty or absent. Readers MUST use the primary head when showing one path. A null first element means the source explicitly selects no primary head; this is neither START nor an unknown head. In that case readers MUST show no default path and MUST NOT promote another lane head. A non-null native head whose target is missing remains unknown: retain its native evidence and use the existing gap or audit fallback. ASR-364 A producer whose source names a current leaf (Claude Code’s last-prompt.leafUuid, Pi’s leaf, OpenHands leaf_event_id, Devin’s main_chain_id) that differs from the default head MUST write session_info.heads, and MUST write a new value whenever it changes.

Active path

ASR-365 Readers MUST compute active paths with ACTIVE-PATH. ASR-366 A stream in which following predecessors revisits a node is invalid. Readers MUST report it and MUST NOT loop. ASR-367 Records not on the active path remain audit facts. Consumers computing usage, cost, file changes or VCS actions for a session MUST include records that are off the active path. Commits, costs and network effects on abandoned branches still happened.

Context view

The context view applies compactions to the active path and drops entries the model did not see. Rewinds are already reflected in the active path. Each entry of the view has a path index: a node’s index in the active path, or, for a compaction summary, the index of its insertion point. Entries are kept in order of path index; entries with the same index stay in insertion order.
Step 4 leaves out calls that a program made (a code-mode cell running commands): their input and output reached the program, and the model saw only the result of the call that ran the program. Entries inserted at the same index stay in insertion order, so a compaction’s retained entries come before its summary. A retained entry stands for the record as the source carried it into the new context; when its capture is truncated, the model saw a shortened copy. A compaction whose replaced range is unknown leaves the earlier entries in the view, after an unknown-replacement marker: the view then shows more than the model may have had, and a reader must say so. When an inherited-context marker names a parent whose stream is available, a reader MAY replace the marker with the parent’s context view at the inherited point, labeling those entries as inherited. ASR-368 Readers MUST compute context views with CONTEXT-VIEW. Applying a compaction that has no status does not mean that the source reported it as completed. ASR-369 The context view is derived from the recorded evidence and does not prove what the model’s actual request contained. Readers MUST NOT present it as the exact request. Where the source recorded the request, the request is in inference.context (see Inference), and ASR-333 says when it is exact. Resolving every emitted retains ref does not establish an exact reconstruction: the shortened content, injected instructions and the order in which they were combined may be missing. Preserve the full native replacement checkpoint and declare structural fidelity loss when those facts are not represented.

compaction

A compaction records that the source replaced part of the model’s context: by a summary, by pruning or masking entries, by an opaque provider-native state, by a reset, or by inserting the summary of an abandoned branch. The new context can also carry some earlier records forward (Codex keeps a list of earlier user messages). A compaction summary is never a message.
In the first example, records up to r111 are replaced, records r112 to r118 are kept, and the summary is placed between them. Two other forms occur. Codex replaces the whole context with a list of retained earlier messages, some shortened, followed by a summary or an encrypted state. T3 Code records that a compaction happened, with token counts, but not what it replaced:
Capture state key: summary. Kinds: ASR-370 A compaction MUST contain compactionId, kind and replaces. ASR-371 replaces MUST contain exactly one of throughId (a ref), ids (an array of refs, possibly empty) or state (the value unknown). insertAt, when present, MUST contain exactly one of afterId or beforeId. afterId: null places the summary at START, even if no entry remains. beforeId is a non-null ref. keptFromId MUST appear only with throughId. Each entry of retains MUST contain ref, and its capture, when present, MUST be full or truncated. ASR-372 replaces.throughId, keptFromId, insertAt and retains follow the reference-resolution table. Non-null targets MUST have a lower seq and lie on the compaction’s own active path (the path ending at the compaction’s predecessor). The node keptFromId names MUST NOT come after the node throughId names on that path. ASR-373 A compaction summary MUST NOT be emitted as a message, and indexers MUST NOT index it as user-authored text. A converter for a source that stores the summary as a message (for example a user-role record flagged as a compaction summary) folds it into the compaction. ASR-374 A branch_summary MUST NOT erase its source branch: the summarized records stay in the stream, and producers MUST NOT emit tombstone records for them because of the summary. ASR-375 When the source destroyed the replaced history, the compaction MUST have historyRetained: false, and the producer MUST emit a gap record for the destroyed history. ASR-377 When the source records a compaction without recording what it replaced, the producer MUST write replaces: {"state": "unknown"} and MUST NOT guess a range. ASR-378 retains MUST come from a list of carried records that the source keeps. A producer MUST set capture: "truncated" on a retained entry when the source records that the carried copy was shortened (Codex retained_source.complete: false). replaces.ids covers sources whose compaction applies to individual entries: OpenHands’ forgotten event IDs, Goose messages marked invisible to the agent, pruned tool outputs.

Algorithm output for fixtures

Conformance fixtures compare active paths and context views as JSON arrays. ASR-376 An implementation that reports an active path or context view for conformance testing MUST use these entries: Context view of the worked example above, which has no compactions:

Incomplete compaction evidence

A count alone does not identify the replaced records. Use replaces: {"state": "unknown"} and keep the native count. If any target in a recorded replacement set is unavailable, keep the complete set in native evidence and use the unknown state rather than listing only the known subset. A failed summary attempt establishes no context change. Pruning supported by separate evidence still has its own record. For a summary at the start of the context (offset zero), insertAt: {"beforeId": "first-retained"} works when that entry remains. insertAt: {"afterId": null} works when none remains. Both require evidence from the source establishing the summary’s placement.