Your Agent Session Is a Tree. Your Code Thinks It Is a List.

Your Agent Session Is a Tree. Your Code Thinks It Is a List.

Codex made Item a wire type and thread/fork a protocol call. Pi gave every session entry a parentId. Claude Code compresses the transcript in five stages. Three harnesses, three answers to what the durable unit of agent state actually is, and the answer decides whether running out of context costs you a summary or costs you the work.

September 15, 2026
Harrison Guo
8 min read
AI Engineering Architecture

Codex made Item a wire type. Pi gave every session entry a parentId. Claude Code compresses the transcript in five progressive stages.

Those are three answers to the same question, and it is a question most agent code never asks out loud: what is the durable unit of state here, and what happens to it when you run out of room?

You find out which answer you picked at exactly one moment. Not while things are going well. At the moment the context window fills.

durable unitwhat survives compactionbranching
Claude Codethe transcripta summary, written in placestart a new session
Codexthe Item, a wire typeitems keep their identitythread/fork
Pia session entry with a parentIda branch_summary node in the treenative to the structure
Claude Code
durable unit the transcript
what survives compaction a summary, written in place
branching start a new session
Codex
durable unit the Item, a wire type
what survives compaction items keep their identity
branching thread/fork
Pi
durable unit a session entry with a parentId
what survives compaction a branch_summary node in the tree
branching native to the structure

Everyone eventually sheds context

Start with what is not in dispute.

Every harness in this series hits the ceiling and has to drop something. Pi’s rule is one line, in packages/coding-agent/src/core/compaction/compaction.ts:

export function shouldCompact(contextTokens, contextWindow, settings) {
	return contextTokens > contextWindow - settings.reserveTokens;
}

That is the whole trigger. Are we within the reserve of the ceiling. Claude Code’s pipeline is staged rather than binary, but the pressure is identical.

The interesting difference is not when they shed. It is what is left addressable afterwards.

Claude Code: the unit is the transcript

Claude Code’s answer is progressive compression. I walked through the five-level pipeline when the source leaked, and the design is coherent: as pressure rises, older material is squeezed harder, with the most recent turns preserved at full fidelity.

The durable unit here is the conversation itself. State is whatever survives the squeeze.

That has a real advantage. Compression is a global operation, so it can make good decisions using the whole transcript, and it does not require the rest of the system to model conversation structure at all. The transcript is a sequence, and the pipeline is a function on that sequence.

It also has one specific consequence: compression is one-way. Once a stretch of session has been summarised, the detail underneath it is not addressable any more. You cannot go back to the state before that decision and take a different path, because the state before that decision no longer exists in a form anything can load.

Codex: the unit is the Item

Codex made a different choice, and made it at the protocol layer, which is what makes it interesting. From the app-server README as it stood on 2026-09-01:

Item: Represents user inputs and agent outputs as part of the turn, persisted and used as the context for future conversations. Example items include user message, agent reasoning, agent message, shell command, file edit, etc.

That README has since been replaced with something else entirely, which is worth knowing if you go looking for the paragraph. The types themselves are still there and still typed: ThreadItem and thread/fork live in codex-rs/app-server-protocol/src/protocol/v2/, checked again on 2026-09-08. The documentation moved. The design did not.

Note what is in that list. Agent reasoning is an item. Shell command is an item. File edit is an item. These are not lines in a transcript, they are typed objects with identity, nested inside Turns, nested inside Threads, all three of them wire types in a JSON-RPC schema that the server can generate as TypeScript or JSON Schema on demand.

Once state is addressable, operations on state become API calls rather than heuristics. thread/resume picks a conversation back up. thread/fork creates a new thread id with copied history. ephemeral: true gives you an in-memory thread whose path is null, so it never lands on disk at all.

thread/fork is the one to look at. Branching is not a feature bolted onto a transcript, it is a consequence of items having identity. If your history is a list of objects, copying a prefix is trivial. If your history is a compressed blob, it is not possible.

Recall the ARC-AGI-3 result: retaining the model’s reasoning across turns was worth most of a 25-point swing. In Codex, agent reasoning is an item type. It has somewhere to live, and it survives by default rather than by remembering to keep it.

Pi: the unit is a node

Pi arrived at the same place from the opposite direction, with far less ceremony.

Session entries carry a parentId, nullable, in session-manager.ts. That single field is the whole design. A list is a tree where every node has exactly one child. Adding the pointer costs almost nothing and buys the entire structure.

Pi treats that structure as a first-class thing rather than an implementation detail. Its extension API exposes session_before_fork, session_before_tree and session_tree as events, and a session start carries a reason field whose values include "fork". Extensions get told when the tree changes, which only makes sense if the tree is real.

Alongside it there is an entry type called branch_summary, which is where the two ideas meet: when you leave a branch, its content can be replaced by a summary of it while the branch itself stays in the tree. You have not deleted the path. You have compressed one, and you still know it is there.

Pi’s compaction package is about 1,550 lines across four files, which is the honest price of doing this properly. The summarisation budget is capped at 0.8 * reserveTokens, so the summary is guaranteed to fit in the space that was reserved for it. That is a small detail and a good one. It is the difference between a compaction strategy and a compaction hope.

The extension system leans on the same substrate. Extensions persist their own state into the session as custom entries, which means an extension’s state branches when the session branches, with no extra machinery.

The two axes again

This is the cache coherence frame from earlier this year, one layer down.

The two axes there were fidelity, lossless against lossy, and retrieval, exact against approximate. Agent session state sits in the same space, and what these three teams differ on is where the lossy operation applies.

Claude Code applies it to the timeline. Older material gets less faithful as pressure rises.

Codex and Pi apply it to a branch. The structure stays lossless and addressable, and lossy compression happens inside a node that keeps its identity.

That is why the tree matters even for people who never type a branch command. The structure is what gives compaction somewhere to put its result without destroying the address. Compression against a flat sequence has nowhere to attach a summary except in place of the thing it summarised.

flowchart TB
  subgraph T["a tree: the bad path detaches"]
    direction TB
    r["task defined"] --> a1["explored A"]
    a1 --> a2["A went wrong"]
    r --> b1["branch from the last good node"]
    b1 --> b2["explored B, works"]
    a2 -.- n1["A collapses into a branch_summary.
Still in the tree, still addressable,
and the root was never touched."] end subgraph L["a sequence: compression happens in place"] direction TB s1["task defined"] --> s2["explored A"] s2 --> s3["A went wrong"] s3 --> s4["explored B"] s4 -.- n2["at the ceiling the oldest goes first,
and the oldest is where
the task was defined."] end T ~~~ L

The move you already make by hand

Here is the practical version, and it is the reason I think this is the most underrated design decision in agent systems.

Everyone already branches. When a session goes sideways after forty minutes, you start a fresh one. That is a branch from the root, and you pay for it by re-establishing everything: which files matter, what the constraints are, what you already ruled out.

You do it because the alternative, continuing in a poisoned context, is worse. Bad exploration does not leave. It sits in the window, and the model keeps reading it.

A session tree makes that a cheap operation instead of an expensive one. Return to the last node where things were fine, branch, go a different way. Everything before that node is intact, because it was never compressed away, because it has an address.

The general form of the mistake is treating exploration as free. It is not free. It costs context, and context is the scarcest resource in the system. A harness that cannot discard a bad path without discarding the good prefix is charging you the whole session for every wrong turn.

What to change on Monday

If you maintain an agent and you are not sure which of these you built, check three things.

Do your session records have a parent pointer? If not, add one before you need it. Retrofitting the pointer is easy. Retrofitting it onto six months of stored sessions is not.

When you hit the ceiling, do you truncate or compact? If you slice off the oldest messages, you are deleting the turn where the task was defined, and there is now a public 25-point result on what that costs.

Does the model’s reasoning survive between turns? In Codex it is an item type, so it persists by construction. In most frameworks it is dropped by default and the default is not in the documentation. This is the single highest-value thing to go and check, and it is usually one field.

The unit of state is not a detail you get to decide later. It is the thing that decides what “later” is even able to look at.


Pi and Codex references measured 2026-09-01 against github.com/earendil-works/pi and github.com/openai/codex at that day’s HEAD. Claude Code pipeline details are from the leaked bundle analysed here and cannot be re-verified against the shipping build, which is a compiled binary with minified JavaScript inside.

🎧 More Ways to Consume This Content

I occasionally advise small teams on backend reliability, Go performance, and production AI systems. Learn more: /services

Comments

This space is waiting for your voice.

Comments will be supported shortly. Stay connected for updates!

Preview of future curated comments

This section will display user comments from various platforms like X, Reddit, YouTube, and more. Comments will be curated for quality and relevance.