My take on subagents, forks and collapsing context
A thought on "where was I?", how we think, and why agents should too.

Most subagent prompts I open carry the task and little of what the parent knew: what was tried, which guess was ruled out, which constraint I gave at the start. The child re-derives all of it. When it reports back, the report is short, and whatever else it understood stays in a transcript nobody opens.
There are two problems in that:
- The child started empty, because everything the parent knew had to be written into a prompt before the work, by a parent whose main job was never to write that prompt.
- The parent delegated at all because the usual advice for anything log-heavy is “run it in a subagent”, so the main context does not fill with logs.
The first is a handoff problem. The second is a bloat problem. I think they are two halves of one missing move.
Terms used below: an episode is one span of work around a local question. A fold replaces a finished episode with a short note plus a handle that points back to the archived span. A fork runs a copy of the agent with the parent’s current context. Recall searches the archive without a model call, and ask puts one question to an archived thread.
The running example is making a project build. The agent runs the build, reads the log, fixes something, and repeats until it passes. Most of that is tooling output. What the rest of the session needs is a few lines:
- it builds
- what was changed
- what is still unchecked.
Everything else was useful while the loop ran and is finished now, but it stays in context competing with whatever comes next.
figure 1: the tangent
Each turn is a block on a list. The build goes off on a tangent below the main thread, then comes back as one block you can reference later.
The main thread so far: system prompt, your request, exploring, the claim query. One block per turn.
Making it build starts a tangent below the main thread. The main thread waits at the dashed link until the tangent is done.
The build fails: error TS2322 in src/client/api.ts, and 400 more lines. All of it is now in context.
A guess: change the field type in src/client/api.ts by hand.
You say no: don't edit generated files. The guess is rejected, and it stays in context anyway.
The agent sees the errors are all in the generated client and regenerates it.
$ pnpm generate && pnpm build
Done in 41s. The tangent now holds ~43K. Nothing marked where it would end.
"Where was I?" The build passes and the next thing is not about the build. Only now can the tangent be named: t4 to t10.
The tangent folds into one new block on the main thread: the note with its handle, then your message verbatim. The full tangent stays archived, reachable via ↪ b2e4.
The main thread continues with the frontend, carrying ~160 tokens instead of ~43K. Compare the note with the original tangent below.
Hover or focus (tab) the note ↪ b2e4 to compare what was inside the tangent with what came back. The bars share one scale.
inside the tangent vs what came back
inside (archived, reachable via ↪ b2e4) 2 runs of $ pnpm build, 400 lines of TS2322 errors, guess: hand-edit api.ts (rejected), your message, the regenerate run
came back, one new block on the main thread ~270× smaller
user: don't edit generated files
Figure 1: the build loop goes off on a tangent, then folds into one note with handle b2e4. The full tangent stays archived and reachable.
Harnesses deal with that by compacting: when the window is nearly full, summarize everything and continue. I think the summary should happen when a piece of work finishes, over that piece only, with the full record kept reachable. The argument borrows from how people already do it, since for most of history a brain was the only thing managing a small working memory.
The workaround most of us already use says the same thing. I used to ask the main agent to keep a markdown file with the context and reasoning so far, and pass the path to every subagent, so they shared one written brain. It helps, and it is me doing by hand what the harness should do. The file can only take three shapes:
- Written once, before the work. It lacks whatever the work reveals.
- Appended to as the work goes. It grows into a log every subagent must read in full, most of it irrelevant. That moves the bloat into a file.
- Rewritten as it goes, so it always holds the current state. This is the best one, and close to what this post proposes.
The third shape is a note without a harness behind it. Nothing tells the agent when to rewrite it, whatever it drops is gone for good, and there is no thread behind it to revisit when the note proves too thin. Those three gaps are what the rest of the post is about.
How I look for knowledge
This is how I, as a person, deal with knowing things.
I remember that a thing exists, not the thing. I know the Postgres docs have a page about lock modes. I could not recite it. Most of what I “know” about a codebase works the same way.
I ask the person who knows. If someone wrote the auth module, I ask them while I read it. They answer with nuance I would miss or take long to reach alone.
I ask someone who was not there. For a review I want someone without my assumptions or bias.
I go deep, then come back with a note. To write a query I load the ORM, the schema, and the one weird thing about that table. When the query is written, that goes away. What stays is the query plus maybe “only works inside the transaction”.
I keep two memories. What I am holding right now, which is tiny, and what I know, which updates as a side effect of working and surfaces by association.
“Where was I?” A teacher goes on a tangent, finishes, says “where was I?”, and continues from before it with only the point kept. Nobody announces a tangent will end in four minutes. You only know it ended once it ended. I will call this the “where was I?” effect, for lack of a name I could find.
That is Figure 1. The main thread is the task. The tangent is a deep dive from (4): expand into everything relevant, produce something, come back carrying only what it produced.
The closest research I found is Event Segmentation Theory: Zacks and Swallow, Event Segmentation, and Kurby and Zacks, Segmentation in the perception and memory of events. People chop continuous activity into events without trying. A boundary is noticed when prediction error spikes, after the next thing stopped being predictable. At the boundary the working model updates and what just happened encodes more strongly into long-term memory. Events nest, and agreed boundaries tend to sit where a subgoal gets done.

Six coarse boundaries viewers agreed on while watching someone pitch a tent: put it down, spread it out, insert the front pole, stake the ends, stake the sides, attach the rain fly. Each is the end of a subgoal. Reproduced for commentary from Zacks and Swallow (2007).
Kurby and Zacks also cite studies where fine segmenters recalled more detail than coarse ones. That secondary result, if it transfers, argues for small episodes over one big summary.
Two differences from my case. Their work is about perceiving events, like watching someone pitch a tent, while mine is about doing a task. And their boundary detection is automatic. For an agent it has to be done on purpose, and the harness has to ask for it.
The same stream, three designs
Take one sequence: user asks for a feature, agent explores, writes a query, runs the build loop, wires the frontend. Here is what happens to it under three designs.
The typical session keeps everything in order. Build logs sit between the query and the frontend work for the rest of the session. Harnesses keep some recent turns verbatim when they compact. OpenCode, for example, keeps the newest N tokens next to the summary1. The main move stays the same: when the window is nearly full, summarize the whole session at once, at a moment chosen by token count.
The subagent moves the build loop out of the main line. The main agent writes a prompt, a fresh agent runs the loop and reports back one message. The main line stays small and its prompt cache is never touched, which is a real win. The child starts from the system prompt plus the task, so it loses what the parent knew but did not write down: which Node version the project wants, the local service tests need, the pinned package, all the small things picked up while reading around. What the child learned beyond its report sits in its own transcript. Some harnesses keep that transcript and let the parent resume it, which is most of what I will call ask, but nothing prompts the parent to use it. Delegation only works if the parent thought of it beforehand, or I asked for it. If it ran the loop inline because it did not expect seven long turns, the logs stay for good, short of restarting from an earlier point.
The tangent from Figure 1 runs the loop inline with what the agent already knew. When the build passes, the agent notices the loop is done and folds it: the main line continues from before the loop with a short note added, and the full loop stays reachable. It never had to predict the loop. It only had to notice it ended.
figure 2: one stream, three designs
The same session in the order the text describes them: what the context holds at each turn, and how big it is.
The same stream under three designs: typical keeps the logs until one compaction, subagent keeps the main line small while the child climbs from scratch, collapse climbs during the loop and drops back to a note with the loop archived.
1st typical
2nd subagentmain child
3rd collapse
The list from the previous section maps onto this. Rows (3) and (4) matter most, because today one tool serves two opposite needs: one should start with what the parent holds and fold back into it, the other should start with less on purpose. The forks section covers that split.
| what I do | what the agent does today | what a fold would add |
|---|---|---|
| (1) remember where, not what | shared memory files, AGENTS.md | a note plus an id that points back to the full thread |
| (2) ask the person who knows | explore subagent, grep the repo again | ask the thread that did the work |
| (3) ask someone not there | reviewer subagent | a separate agent, with less context on purpose |
| (4) go deep, come back | “run it in a subagent” (make it build) | the same agent expands, realizes it is done, folds |
| (5) two memories | the context window, for both | a small active context plus a reachable archive |
| (6) “where was I?” | compaction when the window fills | a fold when the episode ends |
Everything in that table is about one session. It is not a cross-session memory system and does not replace memory files or AGENTS.md, which are written on purpose and usually committed. The archive in row (5) is the opposite: per session, written by the harness without anyone deciding to, verbatim, never meant for the repo. The memory file is my meeting notes. The archive is the recording.
Consolidate at the boundaries
The inference API keeps nothing between calls. The harness resends the message array every turn, the weights stay fixed, and whatever the agent worked out exists only in that array or in side effects like files it wrote. So the array is both memories from (5): what is held right now, and the only thing the model reads. Compaction rewrites it near the limit, over the whole session, with no regard for when anything was learned.
After a build loop, the array holds every failed run and the reasoning behind each guess, including the wrong ones. The next step needs which guess was right, what changed, and what is still unchecked. Compaction prompts ask for roughly that state1, but until they run the logs sit in context on every turn. The prompt cache makes that cheap in money: the provider keeps the computed prefix state and skips reprocessing it. It does not remove those tokens from context. Every new token still attends over all of them, at the cached price.
The array should be rewritten when a local question gets answered or parked, the way my own working memory would be. I will call that span an episode. It starts when a local question becomes active (“why does the build fail?”) and ends when the next action is no longer about that question. With B for the context before, E for the episode with its logs, and A for whatever comes after:
today: B + E + A
proposed: B + fold(E) + AThe bet is that a fold(E) much smaller than E finishes the task about as well, ideally better, because A no longer reasons over irrelevant logs. A differs in both lines, since different context produces different next steps. “About as well” refers to the outcome.
What goes in the note
Everything depends on what fold(E) contains. The note template, used for every fold in this post:
- changed (files, config, external state)
- learned, including caveats
- decided and rejected
- open items
- where to look if this proves insufficient
If that looks like a compaction prompt, it is. OpenCode’s asks for objective, decisions, work state, next move, relevant files, exact paths and error strings1. The content is mostly right. What is wrong is the moment and the scope: over everything when the window fills, instead of over one episode when it ends. The biggest change here is the agent realizing it can fold, not the fold itself.
Caveats, rejected alternatives, open items, and the pointer are the parts a later step can catch on. I will call those the hooks. They are also dropped first, because a good-sounding summary favors conclusions. Say the note says “the claim query works” when the truth was “the claim query works, but only inside a transaction: FOR UPDATE SKIP LOCKED holds the row lock until commit, so outside one, two workers can claim the same job”, and a later step moves the query. The archive has the detail, but the agent will not look, because its active memory says the query works. The pointer does not help. The caveat had to be in the note.
Most raw material for the note is already in the episode, in the agent’s own messages after each tool result: what the output meant, what to try next. Use those as input, not as the note. They lean on the output they reply to and stop making sense once it is gone, and some hold guesses rejected rounds later. The note restates the survivors so they stand alone and quotes anything a later step may need exactly, like an error string or path. Contents in every figure here are placeholders.
figure 3: from tool result to note
The agent's replies are input to the note, not the note: they lean on the log, and some hold guesses that do not survive.
From a 400-line build log plus mid-episode replies to a five-line note: changed, learned, rejected, open, where. Replies are input, not the note.
- agent$ pnpm build
- tool> place@0.1.0 build> tsc -p .src/client/api.ts(12,5): error TS2322: Type 'string' is not assignable to type 'number'.src/client/api.ts(31,5): error TS2322: Type 'string' is not assignable …src/client/api.ts(48,3): error TS2741: Property 'cursor' is missing …(400 more lines)
- size
- ~400 lines
- fate
- stays in archive
the agent replies to it, round by round
- agentchange the field type in src/client/api.ts by hand (rejected later)
- userdon't edit generated files
- agentthe errors are all in the generated client, regenerating it
- agent$ pnpm generate && pnpm build
- tool✓ done in 41s
replies lean on the output above ("the errors"); the first guess did not survive
- size
- ~40 tokens
- fate
- input to the note
build passes · fold(since: "t4")
- changedregenerated src/client from the OpenAPI spec, no source edits
- learnedthe failures came from a stale generated client, not from src
- rejectedediting
src/client/api.tsby hand (overwritten on next generate) - opendoes CI run generate before build? unchecked
- wherethread b2e4
stands on its own; quotes the exact path for later lookups
- size
- ~60 tokens
- fate
- the note
Altmann and Trafton, Memory for goals: an activation-based model, model how people suspend a goal, do something else, and return. I use three points as a checklist, not as a result about agents. A suspended goal is only recovered through a cue:
“Now what was I doing?” can only be answered by generating the right cue as a reminder.
The cue must be encoded with the goal before suspension, and it must be distinctive, not shared with too many goals. That is my paraphrase of their model.
By that checklist, a todo list is a cue written before the work, so it holds intent, not findings, and one list covers everything. A constantly updated todo list gets closer, then it is the state file from the opening again. An AGENTS.md is written with no goal in mind. A note written when the episode ends, by the agent that did the work, about that work, is encoded with what it cues. Their experiments are about people resuming goals, not about notes.
You cannot name the span before it happens
The new part is in (6). You only know the tangent ended once it ended, the same way Zacks and Swallow describe boundary detection after prediction failure.
The end condition is usually easy to name upfront. “Until the build passes” is fine. What cannot be named upfront is the span: which files it will touch, which earlier turns it will depend on, what it will reveal. A subagent needs the span’s inputs, not its end condition, so its starting prompt is a bet about what the span will need. That bet is often cheap and right. When wrong, either the parent under-briefed the child, which then asks back or re-reads what the parent already read, or it delegated two lines of work.
Naming the span after the fact asks two things of the agent that did the work, since here there is no child, only the main agent. Notice the question is done (“the build passes, and the next thing is not about the build”), then say where it started (the first build run, not the earlier query). The harness marks every turn for free, so the start is a choice among existing marks, but a wrong choice folds the query work into the build note or leaves half the logs behind. A wrong fold costs a recall or an ask later, not the work, since the span is archived as it was and user messages stay. The agent must step out of the task, look at its own context, and name the finished span. It breaks the fourth wall. In people that step is automatic. In an agent it has to be done on purpose, and it is the new move the harness needs from the model. A build loop makes it look easy, because a passing build is a clear signal. Open-ended work where the question drifts instead of ending has no such signal, and there the right call is often not to fold yet.
Noticing the build passed is easy, and picking the start is a choice among a handful of marks. The hard part is the note: deciding what a step twenty turns away will need. The “query works” case is exactly that failure, and no boundary detection fixes it. So there are two separate problems: has the local activity ended, and what must survive it. The model has what it needs for the first once asked. The second is partly unknowable, which is why the archive, the hooks, and the ways back exist.
A system prompt can describe the move and its test, and models already make end-of-turn and tool-or-no-tool decisions each step, so “this episode is done” is the same kind of decision. Prompting comes first, and a trained model would do better, the way trained tool calling beats prompted tool calling. The harness generates training data on its own: every time a folded thread is asked something the note should have carried marks a too-thin note, with the caveat that going back is sometimes correct rather than a mistake.
The boundary is a cue, not a certification. Finishing one question can open two, and a late find can revise an earlier conclusion. The test at the boundary is: can the next step proceed from B + fold(E)? If yes, fold. If not, the episode is still open or the note is not good enough. That covers the next step only, not the step twenty turns later, which is why the caveat belongs in the note regardless.
The note is lossy on purpose
The version of me that leaves the SQL query does not carry the database docs along. What I object to is losing detail irreversibly and then treating the summary as the whole truth. So the full episode stays where the agent can reach it, and the note ends with a handle to it. How going back works is its own section.
That changes what counts as failure. With a small active context plus a reachable archive, going back means the system works. What I want low is
tokens carried per turn
+ cost of going back
+ mistakes from detail that was neither carried nor fetchedThe units do not add, so treat it as a cache policy: fold finished episodes, fetch on touch, and let the agent’s judgment about what to keep sit on top.
“Where was I?”
The teacher finishes the tangent and says it out loud. That is the moment I want the agent to have. “The build passes. Where was I?” The agent names the span and writes the note:
fold(since: "t4", note: "...") -> "b2e4"The harness truncates the view to before t4, appends the note with the handle, and files the span under b2e4. The prefix before the span was already there, so returning to the main line costs no new tokens, and it stays a cache hit while the cache lives2. The price is in noticing, choosing since, and writing the note.
figure 4: the fold
The build loop runs inline, then the agent picks where it started and folds it into one block.
The loop runs inline, then folds at t4 into one block: the note with handle b2e4 plus the user’s message verbatim. A second pass shows why the transaction caveat must stay in the note.
- t0systemsystem prompt + tools (~15K)
- t1user“add a background job that exports orders to CSV”
- t2agentexplore: reads src/jobs/, db/schema.ts
- agentclaim query in src/jobs/claim.ts (… FOR UPDATE SKIP LOCKED); caveat: lock held until commit
- agent$ pnpm build
- t5tool✗ src/client/api.ts(12,5): error TS2322: Type 'string' is not assignable to type 'number'.(400 more lines)
- t6agentguess: change the field type in src/client/api.ts by hand
- t7user“don't edit generated files”← re-sent after the note, in the new blockstays where it is, still cached
- agentthe errors are all in the generated client, regenerating it
- t9agent$ pnpm generate && pnpm build
- t10tool✓ done in 41s
- ··agentbuild passes. where was I? fold(since: "t3t4t8", note: "...") -> "b2e4"
notechanged:claim query in src/jobs/claim.ts; regenerated src/client, no source editslearned:the claim query needs a transaction: SKIP LOCKED holds the row lock until committhe failures came from a stale generated client, not from srcrejected:editing src/client/api.ts by hand (overwritten on next generate)open:does CI run generate before build? uncheckedchanged:regenerated src/client from the OpenAPI spec, no source editslearned:the failures came from a stale generated client, not from srcrejected:editing src/client/api.ts by hand (overwritten on next generate)open:does CI run generate before build? uncheckedchanged:regenerated src/client from the OpenAPI spec, no source editsopen:does CI run generate before build? uncheckedwhere:↪ b2e4
- thread b2e4, verbatim, not in the request
- t3agentclaim query in src/jobs/claim.ts (… FOR UPDATE SKIP LOCKED); caveat: lock held until commit
- t4agent$ pnpm build
- t5tool✗ src/client/api.ts(12,5): error TS2322: Type 'string' is not assignable to …
- t6agentguess: change the field type in src/client/api.ts by hand
- t7user“don't edit generated files”
- t8agentthe errors are all in the generated client, regenerating it
- t9agent$ pnpm generate && pnpm build
- t10tool✓ done in 41s
t7user“don't edit generated files”
same fold, two notes: what happens twenty turns later
Left, a note that dropped the transaction caveat. Right, one that kept it. Both archives hold the caveat, neither agent opens them, and the later step goes two different ways.
A note without the caveat
B note with the caveat
notethe claim query works ↪ a91c
- t3agent… FOR UPDATE SKIP LOCKED; caveat: lock held until commit
noteclaim query in src/jobs/claim.ts. only valid inside a transaction: FOR UPDATE SKIP LOCKED holds the row lock until commit. ↪ a91c
- t3agent… FOR UPDATE SKIP LOCKED; caveat: lock held until commit
Handoff is broken, and fork is the first step
This is about the subagent that continues the parent’s own work, like “make it build”. Reviewers are separate and covered below. The parent must produce, before the work, a written version of what the child will need. The goal exists then. The span does not, so the prompt guesses at its inputs. The parent is badly placed to write it: what it knows is spread across hundreds of messages as things noticed along the way, so writing the prompt means first reconstructing its own understanding.
The fix for that problem is small: do not write a prompt, fork.
fork(task) -> result, handleTake the parent’s message array as is, close the open tool call with the task as its result, run the child to completion, and return its final message. The child starts with what the parent holds because it starts as the parent. There is no prompt to write. The prefix is byte-identical to the parent’s, so it is a prompt cache hit on every child turn. This is not the “branch this conversation” button in chat apps, which copies the conversation and leaves two copies. Here the copy exists to come back.
A clean subagent is cheaper per turn, since it carries a 15K system prompt instead of the parent’s prefix. It pays by redoing the parent’s reading. Whether it reaches the same result is what no table can show. The table below models the token side only. It assumes a 100K parent prefix, a 30-turn loop growing context by 150K, cached reads at a tenth of fresh price and writes at 1.25x, and a clean child that re-reads about 30K the parent already read, written once then re-read each turn. The rate column is what each row is billed at, as a multiple of the fresh input price. Token columns are after that rate: the fork’s 3M raw prefix tokens over the loop, read from cache, cost what 300K fresh tokens would. Each child’s first turn, a write, is left out.
| cost | rate | fork | clean subagent |
|---|---|---|---|
| prefix, read from cache on every turn | 0.1x | 300K | 45K |
| loop growth, written once | 1.25x | 188K | 188K |
| loop growth, read from cache as it grows | 0.1x | 225K | 225K |
| re-reading what the parent had read | 1.25x once, then 0.1x | 0 | ~125K |
| total, in fresh-token equivalents | ~713K | ~583K | |
| in dollars, at $3 per million fresh tokens | ~$2.14 | ~$1.75 |
The conclusion is narrow: the prefix row alone suggests a 7x gap, and the loop growth both sides pay brings it to about 1.2x, or 1.6x if the child re-reads nothing. Under these assumptions continuity costs a moderate premium. Whether the clean child matches the result in the same turns is outside the table. The premium widens as sessions lengthen and shrinks as cached reads get cheaper: a tenth is common today, and the newest models on both big providers are at a twentieth or less2. If the parent sat idle past the cache lifetime, the first child turn rewrites the full prefix.
The same prefix is the strongest objection. If bloat hurts reasoning, why hand the child the query work and every stale guess along the way? That is why “run it in a subagent” works today, and the child inherits wrong guesses with right ones and anchors on them like the parent did.
My answer: the fork should inherit the parent’s view, the context after earlier folds, not the raw log. In a folding harness the query episode is already a note when the build starts, so the fork inherits the note and the stale guesses are gone. That covers closed episodes. What is still open is inherited as is. It also changes the table: with the query work folded, the prefix drops from 100K to about 40K, and the fork total lands around 533K, below the clean child. Until folding exists, a fork inherits the raw log and pays for continuity, which suits a long loop and is wasted on a short lookup. Short lookups should keep a clean child, and they are most of what gets delegated today, so fork is for the minority of delegations that continue the parent’s own work. Fork fixes the handoff, and folding makes fork affordable.
Fork and fold end in the same place: a main line grown by one block, an archived span, and the same question of what the block should say. They differ in when and how often they decide. Fork decides before the work and only decides “this is separate”, since the start is fixed. Fold decides after and picks both end and start. A fork transcript is separate from the parent’s, so several can run at once as long as their edits do not collide, since files are still shared. In the simplest scheduling the user can keep talking to the main line during a fold but not during a fork, though that is a harness choice. Fold is closer to how I think it should work and needs the judgment from the previous section. Fork needs no new model judgment, and reuses the fold’s archive, handle, and note format.
figure 5: fork vs fold
Same blocks as the tangent. Both end in the same place; they differ in when the decision is made.
Both start from the same main thread: B, the context before the build, cached.
Fork decides now, before any work: this is separate. The main thread waits; a copy starts as the parent. Fold decides nothing yet; the loop starts inline.
Turn 10. Every turn re-reads B at the cached price, on both sides, plus the loop's own growth.
Turn 20. Same arithmetic on both sides.
Turn 30: the build passes on both sides.
Fork: the copy's last message is the result. Fold: "where was I?" It ended now, and it picks the start among the per-turn marks: t4.
Fork: the result drops onto the main thread as one note. Fold: the span collapses into the same note. Both loops go to the archive.
Same main thread, same archive. Only the moment of the decision differed: before the loop, or after it.
Assumes the same work on both sides. The fork's cost is the parent prefix re-read at the cached price each turn; the clean subagent's extra cost is re-reading what the parent had already read (counted here) and the handoff prompt the parent writes for it (left out). With the parent already folded (B = 40K), fork and fold cost ~533K, less than the clean subagent's ~583K: folding is what makes fork affordable.Model from the post: 30 loop turns (drawn as 5 blocks) growing the context by 150K, cached reads at 1/10, writes at 1.25x, the clean child re-reads ~30K
Fork decides before the loop and runs it on a copy of the parent. Fold runs it inline and decides after, picking the start. Both end with the same main line and archive. Costs use the table above. With the parent already folded both drop below the clean subagent.
Forks and other agents
Once fork exists, which subagents should become forks? Not the reviewer. That is (3), and the reviewer is useful because it lacks my assumptions and bias.
If “sub-agent” meant part of the main agent, today’s subagents would not qualify: they start with only what the parent wrote in the prompt. They are other agents handed the wrong prompt. So two kinds. A fork is part of the same agent: exploration, “make it build”, splitting a task. It starts from the parent’s view and folds back. An other agent is a second one consulted on purpose: review, second opinion, anything where independence is the point. It starts with less, but not nothing, because a reviewer missing my original constraint will miss violations of it. The useful split is to hand it the user’s messages and withhold the agent’s conclusions. A reviewer given the parent’s reasoning for why the diff is correct will mostly agree. It is the same model with the same blind spots, so it is a weaker second opinion than a human colleague, but it is the version available.
Most harnesses run both kinds through one mechanism. Forks start empty when they should inherit, and reviewers sometimes get the parent’s reasoning when they should not.
figure 6: fork vs other agent
A fork starts from the parent's view and folds back. A reviewer gets the user's words and the diff, not the agent's conclusions.
A fork starts from the parent’s view and merges back. A reviewer gets user messages plus the diff without the agent’s reasoning, and catches the transaction bug. Handed the reasoning, it agrees the diff looks good.
- inherited from the parent's view
- systemsystem prompt + tools
- useradd a background job that exports orders to CSV
- noteclaim query in src/jobs/claim.ts. only valid inside a transaction: FOR UPDATE SKIP LOCKED holds the row lock until commit. ↪ a91c
- task
- parentmake it build
- own work
- agent$ pnpm build
- tool✗ src/client/api.ts(12,5): error TS2322 … (400 more lines)
- userdon't edit generated files
- agent$ pnpm generate && pnpm build
- tool✓ done in 41s
- result
- changedregenerated src/client from the OpenAPI spec, no source edits
- learnedthe failures came from a stale generated client, not from src
- rejectedediting src/client/api.ts by hand (overwritten on next generate)
- opendoes CI run generate before build? unchecked
- wherethread ↪ b2e4
→ merges into the main line as its own note; the user's message follows it verbatim
- handed over
- systemsystem prompt + tools
- useradd a background job that exports orders to CSV
- userdon't edit generated files
- diffsrc/jobs/worker.ts
- + const job = await claimJob()
- + await db.transaction(async (tx) => {
- + await exportOrders(tx, job) })
- withheldagentagent's reasoning: withheldinherited from the parent's viewagentclaimJob is safe, SKIP LOCKED prevents double claims
- own work
- agentreads src/jobs/claim.ts, src/jobs/worker.ts
- verdict
- agentfound issues: worker.ts calls claimJob() outside db.transaction(); SKIP LOCKED releases at once, two workers can claim the same job.agentlooks good (agrees with the reasoning it was handed)
→ sits on the main line as a separate message
Going back
Folding is half. The other half is what happens when the note proves insufficient, which it sometimes will, since it was written without knowing every later step. What survives is the note and a handle, and going back has two shapes. Try the cheap one first.
recall(query) -> matching lines, each with the thread it came from
ask(handle, question) -> one answer from that threadrecall searches the archive with no model call: the exact error from a round, or where a function was defined. Plain grep covers most of it, and ranked search (BM25 or embeddings) helps when the agent remembers the idea but not the string. It searches everything the thread held, tool outputs, agent messages, and reasoning where the provider returns it, not only the final report or touched files. It is cheap and should cover most lookups, but only works when the agent roughly knows what to search for.
ask is for judgments: why fix it this way and not the other. The archived span still exists as a context. Append the question to B + E, run one step, return the answer. The span never re-enters the main line. The request keeps the original tools block, since swapping it would miss the cache, and the thread answers without acting. If answering needs fresh reads, that is a fork, not an ask. This is (2), with one honesty note: the same model re-reads its own transcript, so it is closer to that person’s notes than to the person.
An ask re-reads B + E at the cached price if the cache lives, else near full price2. Going back usually happens after the cache lifetime, so full price is the common case today, and I accept that: one full read of a thread is still cheaper than re-deriving what the thread knew. If sessions start querying their archives often, that is the pattern providers price for. The cache section says what would have to change.
figure 7: going back, recall and ask
recall greps the archive with no inference; ask puts one question to an archived thread, and only the answer comes back.
recall(“TS2322”) returns matching lines from b2e4 with no inference. ask(“b2e4”, …) reloads that thread off the main line and returns one answer. Only the answer enters the main line.
- notebuilds. regenerated src/client from the OpenAPI spec … ↪ b2e4
- agentfrontend: wire the export button
- agentrecall("TS2322")
- toolb2e4fold "make it build, 2 rounds"t5toolsrc/client/api.ts(12,5): error TS2322: Type 'string' is not assignable to type 'number'.t6thinking"every TS2322 is under src/client/, nothing in src/jobs" (when the provider returns it)t8agentthe errors are all in the generated client, regenerating it (reply to t5)
- agentrecall("SKIP LOCKED")
- toola91cnoteclaim query in src/jobs/claim.ts. only valid inside a transaction: FOR UPDATE SKIP LOCKED holds the row lock until commit.
cost: no inference. ~300 tokens appended to the main line × $3/MTok ≈ $0.001, warm or cold
- agentfrontend: wire the export button
- agentask("b2e4", "why regenerate instead of fixing the type in api.ts?")
off the main linethread b2e4 - B + Esystem … t1 user … t4 $ pnpm build … t10 ✓ ~64K (cache warm: read at cached price)(cache cold: read at full price)
- toolssame tools block as the thread, so the cache still matches
- askwhy regenerate instead of fixing the type in api.ts? answer without acting. (appended)
- agentone step, writes the answer below
dropped after the answer; b2e4 stays in the archive as it was
- toolapi.ts is generated from the OpenAPI spec; a hand edit is overwritten on the next generate. The spec had changed and the client had not. If the type is wrong, the fix is in the spec, not the client.
Prices assumed: $3 input, $0.30 cached read, $15 output per million tokens.
Folds nest in one direction. A fork can fold inside itself, so an archived fork can hold its own notes and spans, and the archive becomes a tree. A main-line fold never reaches behind an earlier note, so notes on one line never contain each other. The agent need not know any of that. It searches by keyword, not thread, so recall runs flat over every span and reports each line’s thread, and ask takes any handle, however deep.
The case I have least evidence for is when the agent does not know it should go back. The note said the query works, nothing on the main line says otherwise, and the breaking step is three turns away. Defenses, cheapest first:
- The caveat in the note. First defense and weakest, since caveats drop first.
- Habit. The system prompt says folded threads exist,
recallis cheap, and an unknown an earlier thread may have known is worth onerecallbefore guessing. - File touch. Each span’s index lists files and symbols it touched. Touching one again surfaces that span’s note.
- Overlap. Match recent context against the archive each turn and inject “possibly relevant: thread X”. Noisy.
- Me. In an interactive session I notice when something looks wrong and ask. That helps only once it already looks wrong.
- Training. Going back when the note is thin is a habit, and habits are what fine-tuning teaches, the way tool calling went from prompted to trained. The archive and recorded asks are the data.
What comes back is also what was observed then. Reasoning was conditioned on those observations, so if a file changed, “we ruled that out” may no longer hold. The index can store a hash per touched file, and recall and ask can prepend which files changed since.
Putting it together
The moves above need names, so here they are fork, fold, recall and ask, over this data model:
| name | what it is |
|---|---|
| log | one append-only message list per thread: the main line and every fork |
| view | what the model sees: the main log with folded spans replaced by their notes |
| archive | the logs of folded spans and finished forks, with an index of the files and symbols each touched |
| handle | the id of an archived span |
Constraints from the argument rather than taste:
sinceis a turn id the model sees in its view, not a hidden checkpoint. The harness stamps each user message and tool result as it writes it, never after the fact, so stamps never change cached bytes. It snapssinceto the turn boundary and refuses one reaching behind an earlier note.- The note follows the template above (changed, learned with caveats, decided and rejected, open, where) and the harness appends the handle.
- User messages inside a folded span stay on the main line as written, next to the note, with their original turn, so a mid-episode correction does not read as a new instruction. Large pastes get a size cap and a pointer. The note must not rewrite what I said.
- What changed on disk comes from diffing the working tree between
sinceand the fold, not from the model classifying its commands. - The archive is written before the view drops the span.
foldalso has a user slash command. That is the same move without noticing, and the way to test whether the note template holds before asking the model to notice anything.
The slash command is also the simpler alternative to the whole detection story. Folding by hand gets the scope and the timing right with no risk of a false fold, at the price of me noticing instead of the model. A better compaction prompt does not get there, because the problem with compaction is when and over what it runs, not what it asks for. So the manual fold is the baseline. The model folding on its own has to beat it, and if it does not, the manual version is still worth having.
Cache
Everything above assumes prompt caching keeps working, without which cost explodes. Caching matches the longest recently seen request prefix. Both big providers offer two modes: automatic, where the breakpoint follows each request end, and explicit, where you place up to four breakpoints and an entry is written at each2. A harness checkpoint, the per-turn mark used for since, is a log position, not a cache breakpoint. The harness must place a breakpoint at a checkpoint on purpose to get a hit there.
| operation | what changes | cache |
|---|---|---|
| append | a new tail (tool result, fork return, recall result) | hit on everything before it, the tail is written |
| fork | the parent’s prefix plus the task | hit on the prefix if an entry sits at its end. If none does yet, forks started together each write it, since an entry only exists once a response has begun |
| fold (latest span) | truncate to since, append the note | hit on the prefix if an entry was written at since and is still alive, the note is written |
| fold deferred to compaction | everything after an old position | rewritten, once, together with the rest |
| ask | branch from the archive point | hit on B + E if an entry sits there and is still alive, else E is rewritten (B is shared with the live main line) |
Consequences:
- Freeze
toolsandsystemfor the session. On Anthropic they sit before messages in the cached prefix, so atoolschange rewrites everything and asystemchange rewrites everything after it. OpenAI also liststoolsamong prefix-breaking settings. Anything dynamic goes in a trailing block after the last breakpoint. It bills uncached every turn, so keep it small. - Put fork instructions in the task message, not the system prompt.
- Keep one breakpoint at the open episode start. With a single end breakpoint, each request only reads and refreshes the longest match, so during a long loop the
sinceentry expires before the fold. A second breakpoint atsincekeeps it alive. Without it the fold fully rewritesB. foldonly reaches back tosince. Older material waits for the next full compaction, when everything rewrites anyway.
Lifetime matters most. Anthropic defaults to five minutes from last use, an hour at a higher write price. OpenAI’s newest models default to thirty minutes, some older ones up to a day2. An active session needs no more than the default, since turns come within seconds. A harness that queries old threads needs more, and lifetimes keep growing, so I expect that trend to continue. Cheaper cached reads help too, and those keep dropping.
Try it today
I built this as a package for pi: pi-episodes.
pi install npm:pi-episodesIt adds the four tools above plus a short system prompt section on when to use them. Stamps go on every user message and tool result as saved.
foldremoves a stamped span from context and leaves the note. Pi’s own compaction stays as fallback.recallsearches folds, forks, and compacted turns with no model call, ranked, with exact and regex modes.askresumes a folded or forked thread for one question.forkruns a copy of the agent with its current context and tools.
Around them: a working-tree diff per fold, a file-touch cue naming the archived thread when the agent touches a file that thread touched, a staleness line on recall and ask, and an open-items reminder after a fold. /fold folds by hand. Smallest experiment first: fold after a real build loop, then give a task depending on something inside the fold, and watch what the agent does with the note.
What it should find out, the open questions list:
- whether the model notices an episode ended and picks its start unprompted
- whether its notes keep caveats, or fail the “query works” way
- whether it goes back when it should, and whether the file-touch cue helps or adds noise
- whether a fork anchors on the parent’s wrong guesses more than a clean subagent
- whether a smaller, state-shaped context reasons better at all, or that is intuition left over from small windows
The repository has a measurement plan for those. I am working through it and will report back. But I would rather hear how it feels after a week than publish a benchmark I vibed into existence, so if you try it, tell me what the agent did with the notes. The shape still feels right, and now I know part of it has a name.
Footnotes
OpenCode’s compaction has a
keepsetting, “tokens of recent conversation kept verbatim beside the summary”, and its summary template asks for objective, requirements, decisions, work state (completed, active, blocked), next move, relevant files and important context, with a rule to preserve exact file paths, symbols, commands and error strings. ↩ ↩2 ↩3Numbers from the Anthropic and OpenAI prompt caching docs at the time of writing. Entries live five minutes on Anthropic (an hour at a higher write price) and thirty minutes on OpenAI’s newer models, counted from their last use. Reads cost a tenth of the input price on most models and a twentieth or less on the newest, and writes cost 1.25x. An entry that expired is written again once and cached after that. ↩ ↩2 ↩3 ↩4 ↩5