A run whose configuration names a session host (engine.Config.Session: the host, the step bus, the run's owner thread) runs every
agent call as a session call (CRW-1316). The owner thread - the child (project session, relay role parent, the level table) - runs the call's turns on role subagents it
spawns, and the engine starts no thread and no turn of its own: it publishes a step, binds the subagent the owner reports, reads
its answer from the host and records it in the rows relay acceptance reads. Everything around a turn is the engine's as for any
call: the prepared row and its allocation, the gate, the schema validation and its one repair, the terminal rows, the pending
tree and the effect proofs.
The run's started row records backend: session (app-server for a run on the App Server; a run that records none is an App
Server run). A resume runs a journal on the backend it records: a resume on the other backend is refused with the reason before
any row is written. crw flow shadow replays either from the trace and names the recorded backend in its report (backend).
-
Every role's call runs in a directory P of its own, made by the placer after the
preparedrow as for any call: the attempt worktree of an executor, a call-private checkout at theheadoption for every other role, the scratch worktree of a QA capture call. P is the call's cwd for every purpose (pending_tree,p_held,qa_source); the cwd the host reports for a subagent is its parent's, and is recorded only ashost_cwd. -
A turn is a step. Its intent row
step_published(k,op<call>/t<k>,marker<call>/t<k>-a<n>,attemptn,action,input,input_digest,packetandpacket_digest,role,agent_type,model,effort,workspaceP,agent_idof asend_input, thenoteabout earlier attempts,previous_answer, the answer a repair turn repairs (a later attempt of the turn on a new subagent names it in its packet, after a restart too), for asend_inputtokens_seenandcall_tokens, what the turns before it counted, and the turn's place in the call's conversation:conversation_turn, the conversation turn it is (the call's task turn 1, its repair 2, on whatever subagent, attempt and k it runs), andreplaces_turn, the turn it replaces when it is a turn run again; for a spawn step of a gated executor role in a run with an evidence cwd,evidence_assignment, the flow evidence record of the step,evidence_cwds, the directories it was written under before the row, andevidence_ignored) is committed before the packetsteps/<call>/packet-t<k>-a<n>.mdand the stepsteps/<call>/t<k>-a<n>.jsonare written under<state>/flow/<run>/(engine.FileStepBus). A step isspawn(a first turn, the attempt after a lost one, a continuation, a repair whose subagent is closed) orsend_input(the repair turn on the open subagent);closesteps (steps/<call>/close-a<n>-<agent>.json, one file per subagent, so two closes of one attempt never overwrite each other) name a subagent to close. A step whose packet or file cannot be written (a full disk) was sent to nobody: the run writes the flow warningstep_unwritten(marker,k,attempt,action,input_state: not_delivered,error) and writes the step again at the poll until it can or the turn ends; the call is never ended unknown for it. A failure that does not pass shows how long in the record: the warning is written at the 1st, 10th, 100th, ... failure withfailuresand thefirst_atandlast_attimes, and a closing one whenever a warning was written:written: falsewhen the turn ends unwritten,written: truewhen the step is written, after any number of failures. -
The spawn message is short; the packet holds the rest:
crw-flow-op: <call>/t<k>-a<n> CRW-ROLE: <role> Your shell starts in a directory that is NOT your workspace. Your workspace is <P>: run every command with workdir <P> and edit only by absolute paths under <P>. Read the task packet <packet> in full and follow it exactly. You are a leaf subagent: never spawn agents. Your final message is exactly the JSON object the packet names, and nothing else.A
send_inputmessage is the marker line and the turn's input. The packet is self-contained: where the subagent works (only under the absolute path P; a role that writes nothing changes nothing; nobody runsgit commit,add,branch,resetorcheckout), the repository tools when the run has them (CRW-1488: aRepository toolssection with the go ofverifier.go_root, the file that sets it, and the prefixPATH=<go_root>/bin:"$PATH" GOROOT=<go_root> GOTOOLCHAIN=localthat every command running go takes, because the subagent's shell is the owner session's and spawn_agent takes no environment; CRW-1570: the run's recorded TMPDIR,env.TMPDIRof its run.json and never the TMPDIR of the shell that relaunched the run, is a tool of its own, first: a fact that names the directory and run.json,TMPDIR=<dir>first in the prefix, which then goes in front of every command the subagent runs, andTMPDIR=<dir>first in the entries; a configuration with no go gives the TMPDIR alone, a spec without env the tools of before; on the session backend the packet is its only carrier), the role's overlay ofinternal/flow/roleswithout its native-spawn section, the earlier attempts, for a continuation the packet of the call it continues and a file with that call's answer, the task, and the output contract (the schema the final message satisfies). An App Server call's turn text ends with the same section (after the task; the journaled input is the script's), and its thread also gets the entries in its shell: the thread/start and thread/fork carry them as the leaf config keysshell_environment_policy.set.<NAME>, which Codex applies key by key over the server's own overrides, so the policy the server set stays. A thread resumed after an App Server restart, which the host rebuilds from its own configuration, gets them again in its shell: the backend's thread/resume for the call's turn (or for following a turn an earlier process started) carries the same leaf keys, taken from that call's run and kept nowhere (CRW-1559), and the turn text still names them. -
The owner reports what it spawned with a dispatch report in
<state>/flow/<run>/inbox/and reads the run's answer fromreplies/(the commands are the CLI's). The run binds a subagent only when the host shows it is the owner thread's (parentThreadId), carries the marker in a turn's first user message and is configured with the role's pair. The answers:Reply When boundthe subagent is the step's; its rows are written pair_mismatchthe subagent runs another pair: close it and spawn again with the role's pair (named); the step stays retrythe host or the marker turn cannot be read yet refusedanother run, a report without a subagent for a step the run did not publish, or not a subagent of the owner thread closethe report names an attempt that ended (answered, lost), a step the run did not publish (an early report), or a second subagent for a bound attempt A role whose pair the role table leaves to the host is bound without the pair check (
pair_unchecked); anagentRoleother than the expected agent type is recorded (session_agent_role_mismatch) and bound. A role that names a model and leaves its effort undecided is bound on its model alone:thread_startedrecords the effort the host applied, witheffort_unchecked, as the call's pair. A continuation is judged by the same rule: only the decided parts of the role's pair are compared with the pair the call's subagent was bound with, so a call boundpair_uncheckedoreffort_uncheckedcontinues on a new subagent whose bind records the pair it runs, and a decided part that differs is refused (pair_refused); an explicitefforton a continuation of a role whose effort is undecided is compared with the effort the call's subagent ran. While a step waits, the run also looks the marker up on the host - the subagent that carries it for a spawn, the turn that carries it on the subagent for asend_input- so a subagent spawned, or a repair sent, and never reported is bound all the same. -
A bind writes
thread_started(the subagent'sthread_id, the host-readmodelandeffort,cwdP,host_cwd, for a step with a flow evidence recordevidence_cwd, the cwd the stop hook claims its receipt under,parent_thread_id,agent_role,attempt,via: session,rollout_path,marker, andprior_pairs, the pairs earlier attempts of the call were bound with when they differ) andturn_started(k,op,turn_id,attempt,marker, and theconversation_turnandreplaces_turnof its step) in one batch; asend_inputturn writesturn_startedonly. The relay reads the call's pair from its newestthread_startedrow and an author call's earlier pairs fromprior_pairs. A bind whose host cwd no row of the call's newest generation names writesevidence_placedfor it before it writes the step's flow evidence record there and beforethread_started; a record that cannot be placed there leaves the subagent unbound, and the call is heldevidence_assignment_failed.
- The run reads each bound subagent until its marker turn ends. The answer is the turn's last
final_answermessage, or from a provider that sets no phase the last message without one when no commentary came before it; a commentary message is no answer. When prose precedes the JSON, the object that ends the message is taken out of it: the completed row keeps the canonicalresult, itsresult_digest, and the message as it was (raw_text,extracted). The rest is the engine's: schema validation, one repair,null_model,completed. - A turn that ended
failedorinterruptedwith an error isTurnOutcome.Failedwith itscodexErrorInfo: a usage or rate limit pauses the run, an exceeded context window is anull_model, a host failure holds the call, as for any call. - Tokens are the rollout's last
token_count(thread/readpath), less what the subagent's earlier turns counted, in all its parts: the input and its cached part, the output and its reasoning part, the total (tokens:input,cached_input,output,reasoning_output,total; a part that is 0 is left out of the row, and a row written before the parts reads them as 0). A recoveredsend_inputturn takes that and the call's tokens, in the same parts, from itsstep_publishedrow (tokens_seen,call_tokens), so it counts only what it adds. The call's tokens it takes are in no terminal row (the process that counted them died before it ended the call), so the resumed process counts them against the run's figure when it takes them, and the call's terminal row carries them:RunSpentand the process'sspent()count them once. The App Server backend records the same parts fromthread/tokenUsage/updated. A rollout that cannot be read leaves the turn unmeasured: thetokens_unmeasuredrow sayspauses: false, and a session run's token ceiling counts the calls it measured. - A pause, a cancellation or a deadline interrupts the bound subagent's running turns and reads the thread until it is no longer
active; only then is the call ended
held(orsuperseded). A call that waited for a dispatch retracts its step. A step whose subagent is known (asend_input, an adopted one) and was not bound yet has the turn that subagent started for it bound first (turn_started), so the rows name the turn the interrupt reaches; a spawn step no subagent was bound to has no turn to interrupt. The rows before an interrupt, thatturn_startedamong them, are stored before it is sent: a journal that batches its rows commits them first, so a process that dies once the interrupt is sent leaves the turn theinterrupt_requestedrow decides. - An interrupt goes with the turn it was asked for. In the process that asked for it, that is the turn in flight. For a process
that resumes the run it is the turn the rows bound to the step and attempt of the
interrupt_requestedrow (itsop, the step published last before it): that turn, if the resumed call goes on with it, endsheldfor the row's reason whatever it came to. Every other turn starts with no intent: a new attempt after a lost one, a step the resumed process publishes again, a turn the host started for a step after the interrupt (asend_inputthe owner had not taken). The intent is compared where the outcome is read; it is never moved onto another turn. - Write isolation is recorded, never enforced (the host gives a subagent its parent's sandbox). With
SessionConfig.Tracethe run takes digests of P, of the owner's checkout and of the run's shared state when it binds and when it harvests, and writes a flow warning when they differ:read_role_wrote(a role that changes nothing),executor_wrote_outside,shared_state_changed. - An executor's
HEADis compared with the commit its worktree is to be at (the node's lastcrw.commitafter the allocation the worktree was made or restored for, else that allocation's start). A subagent that committed moved it: the commit is kept onrefs/crw/flow/<run>/stray/<node>/a<attempt>-<call>(the executor calls of one allocation, a build and its fix rounds, each keep their own; the ref is created by a compare-and-swap against no ref, and moved only by a fast-forward to a descendant, the same call's laterHEAD, with the commit it holds as the compare-and-swap's old value) and the completed row recordsexecutor_head_moved(start,head,ref);crw.commitcommits the pending tree onstartwith the observedHEADas the compare-and-swap's old value, and the proof takes the recorded strayHEADwhose tree is the pending tree as the call's state (journal.md). The subagent is closed when the call has its terminal row.
-
An attempt is lost only when its marker turn is not completed, its thread is
notLoaded,systemErroror unknown to the host, and the turn ended interrupted or failed without an error, or has not changed forSessionConfig.LostAfter(ten minutes); the thread is read once more before. A thread the host cannot be asked about is not judged, and an active one is waited for.session_attempt_lost(attempt,agent_id,k,turn_id,reason,host_status,turn_status) is written, the old subagent is closed, and the next attempt spawns in the same allocation; its packet says the previous attempt was cut off, and an executor's says to readgit -C <P> statusand the diff first. -
One allocation serves every attempt of a call: the App Server reconcile, the live resend and the retirement of a worktree are not used for session calls. A resumed run goes on with an open session call in place, from its last row:
Last row The resumed call preparedpublishes the first step (the directory an earlier process made is the call's) step_publishedanswers first a dispatch report the owner wrote that no process answered (it waits in the inbox), then looks the step up on the host - the subagent that carries the marker for a spawn, the marker turn on the subagent for a send_input(a repair is never sent twice) - and binds what it finds; else publishes the same step againthread_startedadopts the subagent the row names and writes only its turn_started(nothing is spawned or published again; one the host does not know, or showsnotLoadedorsystemError, forLostAfterloses the attempt, and the gate decides the next; a read the host cannot answer judges nothing and leaves that time as it is, as in the harvest), then reads it as afterturn_startedturn_startedretracts the step file the dead process left and reads the bound subagent: harvests a completed turn, waits for a running one, loses a lost one session_attempt_lostcloses the old subagent and spawns the next attempt -
A held session call goes on in place after the subagents of its held attempts are shown not running (a running turn is interrupted and the thread read until it is not active): a delivered turn is taken again by a new attempt whose packet says why the previous one stopped (a call held in its repair turn goes on with that repair: the packet names the task once, the answer repaired and the repair instruction, and the call has no second repair, as on the App Server path); a step that was published and not bound is looked up by its marker first.
-
A new attempt of a node supersedes the open session calls of the previous process only after every subagent they were bound to, or that took a step they published, runs nothing.
-
A continuation (
resume: handle) runs on a new subagent and writes its ownthread_startedrow, so the handle chain and the pair the relay reads name it. Inside a resume's suffix it never forks: it takes the checkpoint branch (checkpoint_started), and its packet carries the journal's record of the handle's turns, each with the input of the step or request it took: for a session subagent the conversation it goes on from, the turns of the calls its handle continues before its own and the call's conversation before the turn its subagent started at. A session call's conversation is its turns at the conversation turn each records (conversation_turnof itsturn_startedrow), each conversation turn once: a turn run again (the next attempt of a lost turn; the turn a held call goes on with in place on a new subagent, at the next k) is recorded at the conversation turn of the turn it replaces and names that turn (replaces_turn), and supersedes it, and the packet is built from that list alone. The place is recorded when the step is published and the turn bound, never read back from held rows or k; aturn_startedrow of a journal written before the place was recorded (noconversation_turn) takes it from therepairmember of thestep_publishedrow at its k (the task turn 1, the repair 2), and a later such turn at that place replaces the earlier one. The continuation's input digest keeps the handle's own thread chain: a position replays only after every one before it.
| Row | In a session call |
|---|---|
prepared |
as for any call, with backend: session and the allocation every attempt shares |
step_published |
the intent of a step, committed before the step file and the packet; conversation_turn and replaces_turn record the turn's place in the call's conversation |
thread_started |
a subagent bound to an attempt (one row per attempt and per continuation) |
turn_started |
the turn of a step the subagent took, with the place its step records (conversation_turn, replaces_turn) |
interrupt_requested |
the engine's interrupt of the turn in flight (reason, op); a resumed process applies it only to the turn bound to that step and attempt |
session_attempt_lost |
an attempt whose subagent will not finish its turn |
evidence_placed |
the intent to write a step's flow evidence record under a directory no row of the call's newest generation (its rows from the newest prepared on) names yet: evidence_assignment, evidence_cwd, attempt, marker; committed before the record is written, so the release of a superseded or prepared-again call and the restore of a resumed one find every copy from the rows; it changes no reading of the call's state |
| terminal rows | as for any call |
session_wake (flow row) |
a wake of the owner thread the run staged: batch, wake_id, renotify, ops, owner, owner_status, waiting_seconds, staged (and error, or reason when the relay already held the message and staged nothing) |
session_host_warning (flow row) |
what the session host adapter's marker search could not tie as it should: op, warnings (no_ledger_line, ledger_op_differs, several_spawns, window_ended) |
role_store_changed (flow row) |
written by no command: the run's process is the one party that records the pairs a run runs on and their change (its started row, policy_repinned); the journal of an older run may hold one (pinned_digest, store_digest) |
escalation (flow row) |
reason: session_unresponsive: the steps waited UnresponsiveAfter without a dispatched report (owner, ops, batch, waiting_seconds); the run goes on |
step_unwritten (flow row) |
a step whose packet or file could not be written: nothing was sent (input_state: not_delivered, marker, error, failures, first_at, last_at, and written on the closing row); the run writes it again |
A session run is started, stepped and resumed from its owner thread with the commands of internal/flow/cli; every one reads the
run's directory <state>/flow/<run>/ and none writes the journal, which only the run process writes.
crw flow start WORKFLOW [--backend session|app-server]starts a session run by default;--backend app-serveropens a thread per call, for the tests and shadow runs. The spec records thebackend, and the run's temporary location:env.TMPDIRinrun.json(crw-flow-run/1; an optional member, absent from a spec written before it, which reads as before) is what the one temp function (internal/tmploc,DirFrom) answers for the starting environment, as written, also when that is its default. It is asked once, at the start, and no other variable of the caller's environment is recorded (noPATH,GOFLAGSorGOTOOLCHAIN); a TMPDIR that is not absolute, or a directory that is missing, not a directory or not writable, stops the start (exit 1, with tmploc's reason) before the spec is written and before any process starts or owner is bound. A session run's pool ismin(pool, agents.max_threads - 1)(the owner keeps a slot for a helper of its own;agents.max_threadsis read from<CODEX_HOME>/config.toml, default 6), the preview lists the spawn format of each of the six roles (spawn_agent(agent_type, message, model, reasoning_effort, fork_context=false)) and the six session facts below, and the answer names the command the steps are taken with (next). Before the process starts the owner thread is bound (below).crw flow next RUN [--wait S] [--json]answers the run'sstate(running,waiting_session,waiting_decision,paused,endedwith theendedstatus and state,startingbelow), itssteps, theslots(limit:agents.max_threads,open: the subagents of calls not ended and of close steps not taken,agents: their ids), thespawn_formatsand ahint; with--waitit waits up to S seconds for a step or a change of state. Each step carries the step file's members (call, k, attempt, op, role, action, agent_type, model, reasoning_effort, message, packet, workspace, agent_id) and the host call to make with the live argument names: a spawn step isspawn_agentand thencrw flow dispatched, asend_inputstep issend_input(target, message)and thencrw flow dispatched, a close step isclose_agent(target); the hint says to alternatewait_agent(targets, timeout_ms)withnext --wait(itstargetsare the open subagents' ids), which subagents to close first and when the slots are full (agent thread limit reached; the step stays). A close step's file is removed once next has handed it out. A run no process holds that is neither paused nor over is resumed ascrw flow resume RUN --detachdoes, and the hint says so (resumed_pid). next writes neither the owner's orchestrator line nor a row of the run (CRW-1372): while a process holds the run, that process is the one party that records the pairs it runs on (therole_pairsandpolicy_digestof its started row, which names its process lock asprocess) and writes the owner's line from them right after that row (below), and next reads both. The spawn formats are the pairs of the holder's started row: the newest started row when the process lock it names is held, or a newest row that names no process (one of a process of a crw before the process lock, which bound before it recorded its row) when no process lock of the run is held. A run whose holder has not recorded its started row yet (the newest row names a process lock that was let go, or names none while a process lock of the run is held: the holder is coming up, or a command that writes a row holds the run lock for a moment: an answer holds it from the end of the process it drained until its row is stored, and the resume it starts takes the lock after it), or whose start holds its start lock, isstarting: next reads it again, up to 15 seconds whatever--waitsays, never takes an earlier process's pins for the holder's and resumes nothing; still starting then, it answersstartingwith no spawn formats and a hint to ask again (at once without--wait, else when the state changes or--waitruns out). A holder that took its process lock less than 15 seconds ago, recorded its started row and has not bound the owner's line under that row's digest yet is read again the same way, and its line is then reported as it is. A run no process holds answers the role store's spawn formats (a resume reads the store). next warns, and records nothing, when the owner's last line is not the holder's (another live run's, another digest's, or none: the hook gives the owner's spawns no pair of the run until the run's process, or its next process when it is resumed, binds it), when the role store reads other pairs than the holder pinned (the process runs on its pins; a resume takes the new ones and records them in its started row and the change in apolicy_repinnedrow) and, for a run next resumed, when that process repinned (itspolicy_repinnedrow: it runs on the role store as it reads now, not on the pairs the earlier process pinned). A run that recorded no pairs answers the store's, as before.crw flow dispatched RUN CALL --agent ID --k K --attempt N [--wait S] [--json]writes the dispatch report into the inbox (a temporary name and a rename), waits up to S seconds (120) for the run's reply and prints it (boundexits 0, the other replies exit 1). A report of a run no process holds, or one the run has not answered in time, is answeredqueued: it stays in the inbox and the run answers it when it reaches the step (a resumed run included).- A resume (
crw flow resume RUN, the processnextstarts, and the engine'scrw flow resume RUN SCRIPTthroughjournal.ResumeEnv) goes on with the backend the run's started row records (engine.StartedBackend): a session run gets its session environment again (the session host adapter ofinternal/flow/sessionhost, the run'sFileStepBus, the owner of its record). A spec that names another backend than the started row is a mixed resume, refused with the reason as an input error (exit 2); a run whose first process stored no started row goes on with its spec's. Every one of them holds a session run alike (the foreground command throughengine.ResumeHold): at mostagents.max_threads - 1calls in flight whatever--poolsays, and the owner thread bound to the run while the resume runs and its line closed when the run ends. - Every detached launch of the run's process (the start,
crw flow answer,crw flow resume RUN --detachand the resumenextstarts) gives the process the caller's environment with the variables the spec records in place (env, todayTMPDIR; exactly one entry each), so a run relaunched from a shell with another TMPDIR, or none, runs below the TMPDIR its start recorded; a spec written before the record leaves the environment the caller's, as before. A recorded TMPDIR that is missing, not a directory or not writable is refused before anything of the run changes:crw flow answer(without--no-resume) refuses withrun_env_unusable(exit 1, nothing appended, nothing drained, no process started), and the same answer applies once and resumes the run once the directory is made again;crw flow resume RUN --detachexits 1 naming the directory;nextresumes nothing and warns "its resume did not start" with the reason.--no-resumestarts nothing and is not asked; the later relaunch refuses. The spec is never rewritten: the directory is made again. - A run that works in the process that reads its spec (the run process
crw flow _run, the foregroundcrw flow resume RUNand the engine'scrw flow resume RUN SCRIPT) puts the env its spec records into that process's environment before it takes a lock and puts back what was there when the run is done, so everything the run reads from its own environment (the land-head ci local, the integration batch's TMPDIR, the temporary files of its operations) is below the recorded TMPDIR. A recorded TMPDIR that is missing, not a directory or not writable refuses that process (exit 2, the reason naming the directory) before any lock or row. - The owner thread is bound to a session run by the
orchestratorline of its flow-bound file (internal/flow/boundBindOrchestrator:kind: orchestrator,run_id, thepolicyDigestof the role table and thehelperPairsof the six roles architect, executor, executor-t1, reviewer, explorer, critic;Line.Validatetakes no orchestrator line without the first five; the critic is optional, so the open line of a run an earlier crw bound stays readable and its critic spawns are undecided, and a pair an earlier crw wrote for the retired role key of thecrw.reviewerFinalop is read and never applied). A pair carries the decided parts of its role only: an undecided role has the empty pair, a role that names a model and leaves its effort undecided has the empty effort, and an effort without a model is no role's pair. The start writes it before any process of the run exists. While a process holds the run, that process is the one party that writes it (CRW-1372): right after its started row is in the journal it writes the line again with the table that row pinned when it is not the last line (engine.SessionConfig.Started; the foregroundcrw flow resume RUN SCRIPTthroughengine.ResumeHold), and it closes it when the run ends (CloseOrchestrator).crw flow nextonly reads it, so a line that is missing while a process holds the run (its bind failed, or a process of an older crw left it) is reported and stays missing until the run's next process, a resume, binds it after its own started row. A session run's process holds its process lock,<state>/flow/<run>/process.<token>.lock, from before it takes the run lock until after it lets the run lock go, and its started row names the token (process); it removes the file when it ends (the file of a process that died stays, held by nobody). A process lock that cannot be taken is warned about and the process runs with a started row that names no process. A live run is one whose process holds its run lock, or whose start holds its start lock (<state>/flow/<run>/start.lock, held from before the start binds until the run's process holds the run lock, since the start binds before its process exists): the open line of a run with neither is closed before the new one is written, and a thread holds the line of one live run, so a second start while the first lives, or is still starting, warns and does not bind. The read of the thread's last line and the append are one step under a lock of the flow-bound directory, so two near-simultaneous starts of one thread cannot both bind, and neither can their two run processes. A thread whose binding cannot be written is warned about; the run starts. - The spawn hook decides the owner thread's spawns by that line. It refuses two: a spawn by a subagent (a leaf) and a full-history
fork (
full_history_fork_refused: the fork keeps the parent's pair). Every other spawn is allowed withmodelandreasoning_effortoverwritten from the decided parts of the pair of its role (an undecided effort keeps thereasoning_effortthe spawn was sent, and an undecided role keeps the whole spawn, with the warningrole_undecided): the role the firstCRW-ROLE:line of the message header names when it is exactly one of the six (a parser of the flow-bound path; the ordinary path's oracle parser is unchanged), else the role of theagent_type(worker is executor), else explorer. The ledger line (decisions/<owner>.jsonl) records therole, theopof thecrw-flow-op:first line, and awarningwhere the table has one:role_agent_type_mismatch(the header role does not pair with the agent_type: executor-t1 with executor, critic with reviewer, the others with their own name),unrecorded_spawn(no step marker) orunrecorded_author(no marker, an executor's or architect's type). A ledger that cannot be written allows the spawn and says so on stderr, and a flow-bound directory or file that cannot be trusted is read as no binding, with a stderr warning: no spawn is refused for it. The run joins the ledger'sopwith the owner turn'scollabAgentToolCallitems to find the subagent of a published step after a restart (session-host.md). - The run wakes its owner. When it publishes a step and the owner thread is idle or notLoaded (the host's
thread/read), it stages asession_step_waitingmessage throughrun:<id>(journal.StageWake), which the supervisor channel delivers to the run's owner (flow_runs.actor_task_id): a new turn on an idle thread, a resume and a turn on a notLoaded one, after the turn on a busy one. The logical id is(run, process, batch), the batch being the set of waiting steps, so a wake of the same steps staged again is one message: the run's n-th process (its n-th started row) wakes under<batch>/e<n>(the first under<batch>), because the relay keeps a message id for good while a process counts its re-notifications from 0 again, so a resume stages new messages. A wake the relay already held stages nothing, and its row saysstaged: falsewith the relay'sreason. The message's delivery is amessage_deliveryrow the daemon's settle writes (reporting.md). While steps wait without a dispatched report the run notifies again afterWakeAfter(15 minutes), each interval twice the one before up toRenotifyMax(an hour), whatever the owner's status; afterUnresponsiveAfter(2 hours) it writes thesession_unresponsiveescalation once and goes on waiting. Each wake is asession_wakerow. A session run's started row pins its role pairs (role_pairs), and the session host adapter's warnings aresession_host_warningrows. - With
SessionConfig.Trace(the session backend'sTrace) the digests are: the workspace, P'sgit status --porcelainandHEAD; outside, the owner's working directory's when it is a git checkout other than P; shared, the refs underrefs/crw/of the run's repository other than the run's ownrefs/crw/flow/<run>/(the run moves those itself) and the binding files of the flow-bound directory (the hook's ledger and claims are left out: the owner's spawns write them while a subagent works). - The session facts of the preview are warnings, never a refusal (CRW-1318):
owner_bound(the owner can be bound: a thread id, a readable binding, no other live run, live as the binding reads it: its process holds the run lock or its start holds the start lock),role_models(the six pairs, a role without one or without an effort; same-thread review is warning-only when observed),owner_state_writable(the run directory, the flow-bound directory, the control socket and the repository),subagent_slots(agents.max_threadsleaves a slot for a call) andmulti_agent_v1(features.multi_agent_v2is off: under V2 the protocol stops);verifier_prerequisites, the sixth, is judged for every run (CRW-1319).