how it works
Forks
In Claude Code, Leaf hands the user's thread messages to forks: copies of the session's conversation that run beside the session. A fork posts the agent's first words in the thread within a few seconds, whether the session is busy or idle, and a fork with the session's tools can do a small task and answer it while the session carries on with its own work.
Today the first words come from a fork in every Claude Code session, unless
LEAF_FORK_REPLIES=off is set. A fork that does
the work, and takes its thread's later messages, is built, and acts only
with LEAF_FORK_WORK=on in Claude Code's
environment; the rest of the design is planned. Each part below says which,
and What's built and what's planned lists them.
This page is the design's reference, for people and for agents. It explains
the mechanism. The exact rules live in the
session lifetime contract
and in the module headers listed under
Where each rule is written. The research and
measurements behind the design are in
notes/fork-replies.md.
Names on this page
- Main thread
- The session's own conversation, the one the user talks to, as opposed to the forks and subagents made from it.
- Line fork
-
A fork made with
$.model.fork, which resends the main thread's last request with one question from Leaf and returns text in 2 to 3 s. It can't call tools. It writes the working message and sorts the message. - Working fork
-
A background subagent with the main thread's transcript, model and tools
(
$.agent.spawnwithsubagentType: "fork"), namedleaf-<thread>after the thread it works on. It does that thread's work and answers it. - Working line
- The agent's progress message in a thread, saying what it will do about the message, such as “reading the reconnect traces”. It stands until the agent's answer replaces it.
- Waiting messages
- The user's messages in a thread since the agent's last answer there. The newest one owes the thread's answer.
- Kept back
- A new thread message Leaf withholds from the main thread while a line fork writes its working message and sorts it, until 30 s after the thread's newest message, so a message that follows restarts the wait.
- Holds
- A turn holds a thread's messages from the moment it takes the thread, by picking up its messages or answering it, until another turn does. Who holds a message gives the exact rule.
- Take
-
The line fork's judgment that a working fork should do the work
(
take), and the act that gives the thread to one: a turn Leaf opens for the fork picks the thread's messages up. - Hand over, hand back
-
Leaf hands a kept-back message over to the main thread, as the line fork's
hand_overjudgment asks or when keeping it back ends. A working fork hands its thread back to the main thread, withHAND BACK:or by ending unanswered (hand_back). - Pages follower
-
The background process Leaf's hooks module runs for a session that holds a
page. It lists the messages Leaf keeps back, and the module asks a line
fork about each. While it runs it holds the session's fork lane, a lock
whose recorded start tells Leaf since when the follower has been running
(
forks.take_lane), so the code says “lane” where this page says the follower is running. - Turn
- A span of an agent's work that Leaf's records name. The main thread works in its turns, and each run of a working fork is a turn of its own.
- Delivery
-
How Leaf hands messages to a turn: their text and thread, and for the
message that owes an answer, the reply reference its
leaf thread replynames. A delivery too large for a hook goes by pointer, and the agent'sleaf delivery ackrecords it. - Pickup
- The event in the page log that records a turn taking messages in. It names the session and the turn.
- Start
-
The record that the agent is at work on one message or to-do, written by
leaf task startor by a progress reply the agent posts. It names its turn and stops counting when the turn ends. - Clerk's line
- A line Leaf queues for the main thread, which it reads at its next tool boundary or prompt. A line telling it of a to-do a fork opened wakes an idle session.
- To-do
-
Work the agent owes on a thread until it ends it, opened with
leaf thread reply --todo. A to-do is the obligation a significant task becomes.
The path of one thread message
When the user writes in a thread, none of whose waiting messages has reached
the main thread or been handed to it, all of them arrived while the
session's pages follower was running, and no running fork's turn took the
thread, Leaf keeps the new message back from the main thread and asks a line
fork for the working message. From then until the agent answers, the message
reads
On it. The line fork reads the
message against the conversation, writes the working message, and sorts the
message by who should do the work. Leaf posts the line as the agent's and
sends the message where the sort says. If no line comes within
forks.BOUND_S, 30 s, Leaf gives the message to the main thread
anyway, and it does so at once when the line fork fails, when the newest
message owes no reply, or when the pages follower stops.
flowchart TD accTitle: The path of one thread message accDescr: The user writes in a thread. Leaf keeps the message back from the main thread, and it reads On it. A line fork writes the working message and sorts the message. A small task goes to a working fork, which answers in the thread, and the main thread hears what it did. A significant task becomes a to-do, which is planned. Input the main thread needs now goes to the main thread. msg["The user writes in a thread"] kept["Leaf keeps it back; it reads On it"] line["A line fork writes the working message and sorts the message"] fork["A working fork"] answer["It answers in the thread"] told["The main thread hears what it did"] todo["A to-do"] main["The main thread"] msg --> kept --> line line -- small --> fork --> answer --> told line -- significant --> todo line -- "needed now" --> main class todo planned
LEAF_FORK_WORK=on.
Keeping a message back is the one state of a thread that Leaf reads partly
from outside the page log. A hand-over goes in the session's
forks file (forks.HANDED), and whether the pages
follower runs comes from its lease. Every reader asks one function which
messages Leaf keeps back (forks.kept_back), so none disagrees.
The log can't hold this state: keeping back also ends when the follower
stops, which nothing writes, so a live lease decides it whatever the log
says. A lost hand-over record only has the follower hand the message over
again at its next look. Which turn holds a message is different: every
reader has to agree on it, it outlives the fork, and it decides whose writes
stand, so a take is a pickup in the log, and an answer is a reply there that
names its turn. Moving hand-overs into the log too would add an event to
nearly every thread message in a Claude Code session, and change the
protocol, while the lease still decided when keeping back ends.
How the line fork sorts a message
| The message | Where it goes | Status |
|---|---|---|
| Small: a question or task a fork can finish beside the main thread, such as a lookup, research that writes only its own scratch files, or a dispatch to another session | A working fork, which answers it |
built judged take; acted
on only with the flag
|
| Significant: work on the session's files, branch, page or pull requests, or research long enough to need progress of its own | A to-do on the thread, which the main thread schedules |
planned judged hand_over
today, but research that writes only its own scratch files is judged
take
|
| Needed now: the user's answer to a question the main thread asked, or steering its current work | The main thread, at its next tool boundary, or woken if idle |
built judged hand_over
|
Today the line fork judges two ways (forks.ANSWER).
take means a fork with the session's tools could do the work
start to finish beside the main thread: it needs nothing the main thread
is changing or about to change (its files and branch, the page's source,
its pull requests), settles no question the main thread is weighing, and
waits on nothing the session is running. hand_over means the
work belongs with the main thread. Leaf records each judgment in the
page's diagnostics (fork_outcome). Without the flag, Leaf
posts the line and gives every message to the main thread.
The planned sort splits hand_over in two. Input the main
thread needs now can't wait as a to-do, since it changes the work the
to-do would wait behind. Research sits on the line between small and
significant: a fork can do it, but when it runs for minutes it needs
progress lines of its own, so the line fork decides by the message. A
working fork that finds a task bigger than it looked can still make it a
to-do (To-dos).
A working fork takes a message
When the line fork judges take, Leaf posts the working
message, then in one transaction under the page's lock opens a turn for a
working fork and records that turn's pickup of the thread's waiting
messages, with a delivery made for it (forks._take). The
message goes straight from kept back to held by the fork's turn, with no
moment in which neither has it. Leaf's hooks module then spawns the
working fork with that delivery, or resumes the fork that answered the
thread before (Follow-ups and flurries), and
tells Leaf which agent runs the turn (forks.started), which
until then no agent runs (Who a leaf command acts as). The fork works with the session's tools and answers with its own
leaf thread reply, using the reply reference its delivery
names.
built
only with the flag
sequenceDiagram accTitle: A working fork takes a message and answers it accDescr: The user writes. Leaf keeps the message back while a line fork writes the working message and judges take. Leaf posts the line, opens a turn for a working fork, records its pickup and spawns it, and tells the main thread a fork has the thread. A further message reaches the fork at its next tool boundary. The fork replies in the thread. When its run ends, Leaf tells the main thread what it did. participant U as User participant L as Leaf participant W as Working fork participant M as Main U->>L: message Note over L: line fork: the line, take L->>U: working message L->>W: turn, pickup, spawn L-->>M: a fork has it U->>L: another message L->>W: at a tool boundary W->>L: leaf thread reply L->>U: the answer W->>L: run ends L-->>M: what it did
Who holds a message
Leaf ties work to the turn it runs in: a pickup, a start or an agent's reply
names a turn, and a pickup or a start stops counting when that turn ends.
The main thread works in its own turns, and Leaf keeps a turn for each run
of a working fork in the session's agent-turns record
(state.AGENT_TURNS). A fork's turn opens at the take, before
Claude Code starts or resumes the fork, and closes when the hooks module
reports that the fork's run ended. It also lapses when nothing has renewed
it within the working grace, 15 minutes; the fork's tool boundaries renew
it. built
only with the flag
A turn holds a thread from the moment it takes it until another turn does, whether it is the main thread's turn or a fork's. A pickup takes a thread, and so does an answer:
- Of the turns that picked up any of a thread's waiting messages, the one with the newest pickup holds all of them.
- A message no turn has picked up yet is held by the turn that last took its thread, by a pickup or an answer, if that turn is a running fork's. So a running fork keeps its thread's further messages even after it has answered, and the main thread gets them once its own answer took the thread over.
- An answered message is held by the turn that answered it, which the reply names.
Every reader of received input asks one reading which turn holds each
thread, with its waiting messages (forks.page_holding):
delivery, the hooks, the page's activity reading, keeping a thread back for
a line fork, the end of a fork's run and every writer's command. A fork's
turn keeps the main thread off the messages it holds only while it runs,
that is while it is open and renewed. Once it closes or lapses, those
messages are the main thread's to answer, though the closed turn still holds
what it picked up until another turn picks it up.
leaf page state names a holder on each thread and
each answer the agent owes: the main thread, a fork by the name the main
thread reaches it by, or the line fork while Leaf keeps the
thread back for it. A thread's holder is the turn that holds it, with
whether that fork's turn is running, closed or
lapsed. An answer's holder is who owes it now: its thread's
holder while that is the line fork or a running fork, and otherwise the main
thread (holding.owed_by). Delivery, the Stop hook and the watch
read the same reading. A main thread recovering what it owes there, as after
a compaction, leaves a running fork's answer to the fork, and a message a
line fork is judging to it.
built
flowchart TD accTitle: Who has a thread's waiting messages accDescr: A new message is kept back for the line fork. A take gives it to a fork's turn, through the fork's pickup; a hand-over, a failed fork or the 30 s bound gives it to the main thread. If the fork's turn closes or lapses with a message still waiting, the main thread owes it. The fork's reply answers it, and so does the main thread's, whether the main thread owes it or answers over the running fork. kept["Kept back for the line fork"] fork["A running fork turn holds it"] main["The main thread owes it"] done["Answered"] kept -- take --> fork kept -- "hand_over,<br/>failure, 30 s" --> main fork -- "turn closes<br/>or lapses" --> main fork -- "fork<br/>replies" --> done fork -- "main thread<br/>answers over it" --> done main -- "main thread<br/>replies" --> done
One rule for a fork's writes
A fork's turn may act only on the messages it holds or that no turn holds.
One function checks each of its writes as the write is made
(holding.refuse_unheld): its pickups, which are its take, its
tool boundaries and its acknowledgement of a delivery handed to it by
pointer; its replies, including a message in the thread that answers
nothing, which counts against the thread's latest messages; and its
starts. An answered message is held by the turn that answered it, so a
fork may repeat its own answer, but may not answer again a message the
main thread answered, even with the reply reference the fork's delivery
gave it.
When the main thread takes a thread over
While a fork's turn runs, the main thread isn't given the messages the
fork holds. Its deliveries leave them out
(delivery.deliverable), its Stop hook and watch neither keep
its turn from ending over them nor remind it of them
(activity.agent_holds), and its own pickups leave them out
(holding.pickable). While the fork's turn runs, the thread's
further messages go to the fork, before and after it answers.
The main thread is sovereign all the same: it can answer a message the fork holds. Its answer takes the thread over at once, as a pickup would once the fork's turn ended, and the fork's later reply, start or message in that thread is refused. The thread's next message then goes to the main thread while the fork still runs.
Once the fork's turn closes or lapses, a message it left waiting is the main thread's input: the main thread's next delivery hands it over with a reply reference of its own. The main thread's pickup of it is then the newest, so it takes the thread over, and the fork's later pickup, reply or start on that thread is refused.
Who a leaf command acts as
A working fork's shell commands run in the session's environment, so a
leaf command it runs looks like one of the main thread's.
Leaf's PreToolUse hook in hooks.json, which Claude Code runs
before every agent's Bash call that runs leaf, tells them apart
by the agent id in the hook's payload. For an agent the session has run a
turn for, open or ended (state.agent_known), it rewrites the
command to put --agent-id and the agent's id after its leading
leaf (hooks.agent_command). The command still
begins with leaf, so a permission rule allowing
leaf commands still matches it. The mark decides the writes
that name a turn: a reply, a start, a pickup and an acknowledgement. Any
other leaf command, such as a page check, runs as the
session's, marked or not.
built
only with the flag
flowchart TD
accTitle: Who a leaf command acts as
accDescr: A Bash call that runs leaf, from an agent that runs no open turn while a turn Leaf opened awaits its agent, is denied with the instruction to run it again. Otherwise, from an agent the session ran no turn for, it acts as the main thread. From an agent the session ran a turn for, a call where leaf isn't the first word, or runs twice, is denied. Otherwise the hook marks it with the agent's id; it acts in the agent's turn while that turn is open, on what the turn holds, and is refused once the turn has ended.
bash["A Bash call runs leaf"]
waits("Does it run no turn, while a new turn awaits its agent?")
again["Denied: run it again"]
known("Did the session run a turn for this agent?")
main["It acts as the main thread"]
leads("Is leaf its first word, and its only leaf?")
denied["Denied: run each leaf command alone"]
open("Marked with --agent-id. Is the agent's turn open?")
inturn["Its replies and starts act in that turn, on what it holds"]
refused["Every command it runs is refused: the turn has ended"]
bash --> waits
waits -- yes --> again
waits -- no --> known
known -- yes --> leads
known -- no --> main
leads -- no --> denied
leads -- yes --> open
open -- yes --> inturn
open -- no --> refused
| Actor | The command | What it may do |
|---|---|---|
| The main thread | names no agent | act on anything the session owes, in the main thread's turn |
| An agent with an open turn | is marked, and the agent's turn is open | reply, start and acknowledge in its own turn, on the messages it holds or no turn holds (One rule for a fork's writes) |
| An agent whose turn ended | is marked, and the agent has no open turn |
nothing: every command it runs is refused (state.)
|
A subagent the session ran no turn for, such as one of the main thread's own workers, isn't marked and acts as the main thread. The mark follows the agent rather than its turn, so every command of a fork still running after its turn ended is refused rather than taken for the main thread's.
A command that acts as the main thread on a thread a running fork or a line
fork holds, whether the main thread's own, a worker's, a fork's script's or
a terminal's, goes through with the main thread's authority, as you chose on
14 Nov over refusing it. It says beside its result which fork holds the
thread, and for an answer, which takes the thread, that it took it and how
to stop the fork (holding.authorize).
built
A new fork runs before Leaf knows its agent id. The turn Leaf opens at the
take stays unnamed until the hooks module, once Claude Code has spawned the
fork, tells Leaf which agent runs it, and the hook's payload names neither
the turn nor the name the fork was spawned with. So while a turn of the
session awaits its agent (state.unnamed_agent_turns), the hook
denies the leaf command of any agent that runs no open turn,
with the instruction to run it again, since unmarked, a new fork's command
would act as the main thread. A worker of the main thread that runs a
leaf command in that second or so is asked to run it again too.
A resumed fork's agent id Leaf already knows, so it runs its new turn from
the moment Leaf opens it. Where any step of starting a fork fails, the
module ends its turn; a turn left unnamed otherwise, as by a reload of the
module mid-start, stops counting once it lapses.
A marked agent's Bash call with the word leaf anywhere but as
its first word is denied with the instruction to run each
leaf command on its own, as in
cd x && leaf …, if …; then leaf …,
"$(true; leaf …)" or bash -c 'leaf …'. Unmarked,
such a command would act as the main thread, and a mark spliced into it
could land inside quoted text. The rule reads words rather than Bash's
grammar, so it also denies a call that only names leaf, as a
search, an inline python -c or a reply's text naming a
leaf command does; the denial says to write such text or code
to a file with the Write tool and pass the file, and to search with the Grep
tool. A script that runs leaf is out of its reach.
The marking lives in hooks.json rather than Leaf's hooks
module, because Claude Code runs none of a plugin's
tool.call hooks for an agent that plugin spawned (measured at
Claude Code 2.1.322), so the module can't see a fork's tool calls. The
PostToolBatch hook in hooks.json names the fork too: at each of
the fork's tool boundaries it renews the fork's turn and hands it the
thread's further messages (hook_transport.carry_agent_turn),
and it hands the fork nothing else of the main thread's.
When a fork's run ends
As a working fork's run ends, the hooks module tells Leaf
(forks.ended), which closes the fork's turn and judges each
thread by the messages that turn picked up itself and by what the turn
wrote: its replies name it, so Leaf never credits the fork with an answer or
a to-do another turn wrote. The judgment says who owes the thread next. A
message the user took back counts for nothing. Leaf's clerk tells the main
thread of every outcome except taken_over, since the turn that
took the thread over already has it.
built
only with the flag
flowchart TD
accTitle: How Leaf judges a thread when a fork's run ends
accDescr: When a fork's run ends, Leaf closes its turn. If a message it picked up still waits and its closed turn still holds the thread, the outcome is hand_back. If another turn holds one of them or answered it, taken_over. Otherwise the fork answered every one: todo where a reply of its own opened a to-do on the thread that is still open, else answer.
ends["The run ends; Leaf closes the fork's turn"]
q1("Is a message it picked up still waiting, held by its closed turn?")
q2("Does another turn hold one of them, or did another turn answer one?")
q3("Did a reply of its own open a to-do on the thread that is still open?")
hand["hand_back"]
taken["taken_over"]
todo["todo"]
answer["answer"]
ends --> q1
q1 -- yes --> hand
q1 -- no --> q2
q2 -- yes --> taken
q2 -- no --> q3
q3 -- yes --> todo
q3 -- no --> answer
| Outcome | When | The main thread hears |
|---|---|---|
answer |
The fork's own replies answered every message it picked up. | That the fork answered, so it can read the answer and correct it where what it knows shows it wrong; and that the thread owes nothing, where nothing waits in it and no to-do on it is open. A message the user wrote since, which the fork never picked up, Leaf passes on as any new message. |
todo |
As for answer, and a reply of the fork's own opened a
to-do on the thread that is still open, usually its answer.
|
That the fork answered and opened the to-do, named with its text, which the main thread now owes and schedules. If the session is idle, Leaf wakes it. |
hand_back |
A message it picked up still waits, and the closed turn still holds
the thread: the fork gave it back with HAND BACK: and a
reason, failed, or ended without answering.
|
That the fork gave the thread back, and why, and the user's waiting
messages themselves, as new input at its next tool boundary or prompt
(delivery.). If the session is idle,
Leaf wakes it.
|
taken_over |
Another turn picked the thread up since the fork did, or answered one of the messages the fork picked up, as the main thread can while the fork runs. | Nothing: the turn that took the thread over has it. The page's diagnostics name that turn. |
How a fork tells the main thread
Claude Code tells the main thread nothing of a fork a hooks module spawns: no row in its transcript, and no notification or wake when its first run ends. So Leaf tells it, in lines its clerk queues: one as a fork takes a thread, naming the fork so the main thread can SendMessage it, and one as the fork's run ends. A line waits for the main thread's next tool boundary or prompt; a take's line still waiting as the fork's run ends leaves the queue, since the end's line says where the thread stands. Where the run leaves the main thread owing something, the answer to a thread the fork gave back or a to-do the fork opened, Leaf also wakes an idle session for it. The end of a resumed run is different: Claude Code notifies the main thread of it, which starts a turn on an idle session, and that turn reads the queued lines. built only with the flag
The user chose the clerk's line on 2026-11-11, over the fork's own
SendMessage to main. Leaf writes the line from the log, so it
also covers a fork that failed or was stopped before it could write, and
Leaf's own wake comes only when the main thread owes something, after a
first run and a resumed one alike, though Claude Code still wakes it at
every resumed run's end. A fork's SendMessage would name the fork as its
sender and could say what the fork did beyond the outcomes above, but it
would wake the main thread for every answer, and add a second report to a
resumed run's notification.
In a probe, a fork's SendMessage arrived as an agent message and started a
turn on an idle main session 60 ms later. Claude Code offers no third way:
a hooks module can start a fork only with $.agent.spawn,
whose first run never notifies the main thread, since the module's call to
the Agent tool is refused (2.1.322). A resumed run's notification wakes an
idle main thread whichever way tells it; in two live runs that woken turn
took 23 s and 28 s over 4 and 5 model requests, and checked the forks'
answers.
Follow-ups and flurries
Leaf gives a thread's work to one working fork where it can, so a flurry of
messages in one thread usually makes one worker, and a thread never has two.
While a fork's turn runs, the thread's further messages go to it, before and
after it answers, and once its run has ended, a message the line fork judges
take goes to the same fork, resumed. Otherwise a further
message goes wherever its thread already is.
built
only with the flag
flowchart TD
accTitle: Where a further message in a thread goes
accDescr: If a running fork turn holds the thread, by its pickup or its answer, that fork takes the message at its next tool boundary. Otherwise, if the main thread owes the thread, or its answer took the thread from a fork that still runs, it takes the message as any input. Otherwise the message is kept back for the line fork, which sorts it. On a take, if a fork gave the thread's last answer and its turn has closed, that fork is resumed in a new turn; otherwise a new working fork takes it.
more["A further message in a thread"]
q1("Does a running fork turn hold the thread, by its pickup or its answer?")
a1["That fork, at its next tool boundary"]
q2("Does the main thread owe the thread, or did its answer take it from a fork that still runs?")
a2["The main thread, as any input"]
kept["Kept back for the line fork, which sorts it"]
q3("On a take: did a fork whose turn has closed answer it last?")
a3["That fork, resumed in a new turn"]
a4["A new working fork"]
more --> q1
q1 -- yes --> a1
q1 -- no --> q2
q2 -- no --> kept --> q3
q2 -- yes --> a2
q3 -- yes --> a3
q3 -- no --> a4
hand_over goes to the main thread, as any new
message does.
-
While a fork's turn runs, the user's further messages in its thread reach
it at its next tool boundary, inline, or by pointer when they are too
large for the hook, which the fork's own
leaf delivery ackrecords into its turn. That holds after the fork has answered too: its turn holds the thread until another turn takes it, and Leaf keeps nothing back for a second fork in a thread a running fork took, by a pickup or an answer (holding.Holding.took). A message that arrives as the fork's run ends, after its last tool boundary, is kept back once the turn has closed, and goes to the same fork, resumed: no receipt passes a message no turn has taken in, so it is still waiting for its first delivery when the turn closes. -
A message the line fork judges
take, in a thread whose last answer names a fork's turn that has since closed in the session's present generation, goes to that fork again (forks._resumable). Leaf opens the new turn and records its pickup, as a first take does, and the hooks module resumes the fork by its agent id, then tells Leaf it runs the turn. Leaf's message resuming it (forks.resume_prompt) carries the thread's delivery and tells the fork to readleaf page statebefore it answers, since its transcript lacks what the rest of the session did on the page meanwhile. The fork can SendMessage the main thread to ask about it. - A thread the main thread answered last gets a new fork, once the fork it took the thread from has ended, and so does one whose fork ran in an earlier generation. A thread whose fork Claude Code can't resume goes to the main thread instead: the module ends the turn Leaf opened, which gives the thread back. A message the main thread writes in the thread unasked, such as a correction of the fork's answer, answers nothing, so the thread stays the fork's.
- Messages in different threads start a fork each, side by side.
- planned fallback If a fork per thread proves wasteful, one standing fork per session would take every small message in turn: cheaper, but slower when messages in several threads arrive together.
The main thread as coordinator
The main thread stays in charge of the session's work. It hears by name as each fork takes a thread, and can steer the fork, or ask it to stop, with SendMessage, or answer the thread itself, which takes the thread from the fork (When the main thread takes a thread over). It takes its to-dos in the order it judges, and runs work bigger than a message in subagents, as the shipped skill's Stay responsive already asks. A fork is to change neither the session's files nor its plan: the line fork gives a fork only work that needs nothing the main thread is changing, and the working fork's task tells it to change nothing the main thread is changing or about to change. Nothing else enforces it.
flowchart TD accTitle: The main thread coordinates forks, to-dos and subagents accDescr: Working forks tell the main thread what each did, and the main thread can steer them or ask them to stop. A fork can turn work that proved bigger than it looked into a to-do. The main thread schedules to-dos and runs work bigger than a message in subagents. forks["Working forks, one per thread"] main["The main thread"] todos["To-dos on threads"] subagents["Subagents"] forks -- "what each did" --> main main -- "steer, or<br/>ask to stop" --> forks forks -- "bigger than<br/>it looked" --> todos todos -- scheduled --> main main -- "work bigger<br/>than a message" --> subagents
To-dos
built
only with the flag
A working fork that finds the work bigger than a message, such as a change
to the session's files, branch, page or pull requests, or research long
enough to need progress of its own, does what it can without those
changes, answers with what it found, and opens a to-do on the thread with
leaf thread reply --todo, as the main thread does. Its task
(forks.work_prompt) offers that beside
HAND BACK:, which it keeps for a message the main thread has
to act on now: an answer to a question the main thread asked, steering its
current work, or a question it is weighing. The to-do is the session's and
stays open after the fork's turn ends, while the fork's start on it stops
counting with the turn. The main thread hears that the fork opened it, and
an idle session is woken for it (the todo outcome above).
planned A message the line fork sorts as significant becomes a to-do on its thread that the agent owes until it ends it, so the main thread takes it in its own order instead of being interrupted by it. Such a message has no working fork, and who opens its to-do is still to be designed.
What's built and what's planned
| Part | Status |
|---|---|
| Keeping a message back, and the working message | built in every Claude Code session |
The line fork's judgment, take or
hand_over
|
built recorded, acted on only with the flag |
| A working fork in a turn of its own: the take, marking its commands, further messages at its tool boundaries, and its end | built acts only with the flag |
| A to-do a working fork opens | built with the flag |
| Telling the main thread in the clerk's lines | built with the flag |
| The same fork for a thread's follow-ups | built with the flag |
| Sorting three ways: small, significant, needed now | planned |
| A to-do for significant work | planned |
| One standing fork per session | planned fallback if a fork per thread proves wasteful |
| Measuring forks before they become the default: decision, outcome and coherence evals | planned |
Where each rule is written
- Session lifetime, “Turns and generations”: the main thread's turns and generations, the agent turns beside them, and who holds a message.
-
src/leaf/holding.py: which turn of a session holds each thread message, the main thread's or an agent's, and what a turn may do about one another turn holds;reading,refuse_unheld,authorizeandpickablehold the rules. -
src/leaf/forks.py: its header holds the states a thread moves through and why the line fork runs as it does;_takeandendedhold the rules this page names for a working fork's take and end. -
src/leaf/clerk.py:queueandwaking, the clerk's lines and which of them wake an idle session; andsrc/leaf/session.py:watch_between_turns, what the watch wakes the session for. -
src/leaf/state.py:AGENT_TURNS,agent_known,unnamed_agent_turnsandacting_agent_turn, the record of agent turns and the actor a command runs as. -
src/leaf/hooks.py:agent_commandandleaf_words, the marking, the denial while a turn awaits its agent and the refusal of a compound command; andsrc/leaf/hook_transport.py:carry_agent_turn, a fork's tool boundary. -
hooks/claude-code.ts:lineFork,workForkand theturn.completehook, which make the forks and report a working fork's end. -
notes/fork-replies.md: what Claude Code lets a hooks module fork, measured; how well the line fork judges; forks on real pages; and the build steps.