ADR-0057 Architecture decision record
A Turn is its Frame, its Dials, and whether it continues
A live task run (2026-09-20, fa45e88f, seven tool calls, eight minutes, zero bytes of the target read) exposed the seam: the prompt said Work in /work/target/summarize while every Working tool resolved relative paths at /work, because cwd lived on the Agent definition and a definition is written before any run — before any worktree — exists. The package compensated with prose, and a weak model ignored prose: it wrote SUMMARY.md outside the worktree, called finish, and the run parked at review claiming done. The audit that followed found the invoke input carrying four ways to name one conversation (session: "continue", scope, conversation, and an undeclared instanceId), one of them (scope) with two unrelated author intents, none with a stated precedence, and every @kind fixture bypassing all of them. ADR-0028 already said the reviewing state hands a per-round worktree path as the reviewer's "cwd and prompt"; ADR-0018 and GLOSSARY.md said cwd is identity, never per Turn. The decided thing had no field to land in.
Decision
A Turn's input is its Frame, its Dials, and whether it continues — nothing else.
ts{ prompt, cwd?, continue?, model?, thinkingLevel? }Identity —
instructions,workspace— stays on the definition, and the definition is exactly identity plus one default:agent({ model, instructions, workspace?, thinkingLevel?, description? }).The Frame is
promptandcwd: what this Turn is about and where it works. Neither identity nor a Dial. A worktree path is per run by nature —/work/<slot>/<branch>exists only once a run has a branch — so no definition can name it, and a directory is not a property of a persona any more than a prompt is.cwdleavesAgentDefinition. The Harness roots the Working tools at the Frame'scwd, which rides the admit body beside the prompt and the Dials.The Briefing is not the Frame. jr2's Turn part (ADR-0062) follows the prompt — the resolved working directory, the Allowed picks and any notices — in a block of its own. The prompt stays the Machine state's words alone; jr2 never writes into it.
Under a Workspace, an absent
cwdresolves to the only Repo Slot's Worktree, and is refused when there is more than one. With one slot there is no choice, so no convention is being smuggled in (ADR-0051: the kit gives no slot a meaning). With two or more, the state must say which —cwd: context.workspace.repos.target— and the actor refuses the Turn before admission, naming the slots and the line to add. The trap is accepted with open eyes: a composer who adds a second slot on thecustomizeline can make a Machine that ran yesterday refuse today, in a file the Machine's author never sees. The refusal is explicit, at the first Turn, before any pod is spent;jr2 upcannot see it because the invoke's input is a function. Aworkspace: "none"Agent has no Working tools and nocwd(ADR-0028); a stub-Harness run with no Workspace keeps/work.taskalways passes itscwd, because "the first slot is the one the coder edits" is its convention and a convention is stated, not defaulted.continue: trueis the whole continuation surface. Absent, every invocation is a fresh conversation — jr's lossy handoff, ADR-0016's default, unchanged. Present, the Turn lands on this Agent's one conversation in this Machine instance, whichever state of the Machine invokes it. The Instance ID is structural —<runId>/<machine actor path>/<agent>— so the author writes a boolean, not a key: in a state machine the identity is already on the page (the run, the Machine instance, the Agent slot), and a caller-chosen string would only restate it, or contradict it. Every surveyed orchestration framework makes the caller hold a key (LangGraphthread_id, Temporal Workflow Id, MastrathreadId) because nothing else can; here something else can.- Fan-out is safe by construction. A Pool spawns each worker with the item id as its actor id (pool.ts), so three
taskchildren each mint their own…/backlog.worker.<item>/coder. The packaged Machine never learns it is under a Pool. - States of one Machine share by saying
continue. States differ by Menu — a coder inimplementoffersfinish, the same coder infixoffersfinishanddispute— and the same conversation is wanted across them. A state that wants a clean context leavescontinueoff. A Turn joins only a conversation that an earlier Turn started withcontinue: true, so a retry loop sayscontinueon each Turn, the first too: a first attempt without it is not in the conversation that attempt 2 continues. Thus the docs showcontinue: trueon each Turn of a retry loop, and a conditional spread (...(clean ? {} : { continue: true as const })) only for a state that must start a clean conversation on some Turns only (a state that always starts clean leavescontinueoff). A clean Turn does not start the continued conversation again: the next Turn withcontinue: truejoins the earlier one, so attempt 4 after a clean attempt 3 continues attempts 1 and 2. The alternative, a spread onattempts > 0, keeps attempt 1 out of the conversation, so attempt 2 does not see it. The cost ofcontinue: trueon attempt 1 is the Runaway reroll (ADR-0035): a continued Turn gets none, so a Runaway on attempt 1 sendsagent.faultat once, and burns the conversation. The docs accept this cost, so that attempt 2 sees attempt 1. - Two Agents never share a conversation. The agent name is in the id, and it always was; a second persona under one transcript would swap the system prompt mid-conversation. The memory-Agent shape ("read the coder's conversation, store what matters") is served by handing the transcript as prompt data to an Agent with its own conversation — Letta's sleep-time agents — not by joining; how a transcript reaches a Machine as data is a follow-on decision.
- A faulted conversation is never continued. No fault leaves a conversation worth continuing: a runaway either had its reroll or has a poisoned context; infra and no-signal mean the Harness that held it is unreachable (ADR-0035). jr2 keeps an epoch per continued conversation in the ledger beside the snapshot; the terminal
agent.faultbumps it, and the nextcontinueon that Agent mints<id>/<epoch>— a virgin conversation. The author deletes their generation counter. What stays with the author is re-briefing: the fault event is the signal that the next prompt must carry the whole task again, and jr2 cannot write that prompt. The one fault that is not the conversation's is a Harness restart that cut the Turn (Turn lost, ormemory limitbehind it): the rebuilt Harness holds the conversation (ADR-0031), so the epoch stays and the nextcontinuelands on it; a rebuild that could not read it answers that Turn'sholdscheck, which gives aconversation-newnotice (ADR-0062). - The reroll stays closed to continued conversations (ADR-0035): the one recovery jr2 knows is the identical prompt on a fresh conversation, and a continued Turn's prompt ("Continue.") is meaningless fresh. Fault route plus re-brief is the recovery.
- Two live invocations of one continued id — parallel states — are refused at the registration table, one live surface per address; a Submission that arrives behind a live one on the Harness queues (ADR-0027). Sequential states never race, because ADR-0024 ends the previous Turn before the next state invokes.
- Fan-out is safe by construction. A Pool spawns each worker with the item id as its actor id (pool.ts), so three
session,scope,conversation, andinstanceIdretire.session: "continue"was already this boolean under a longer name — its id was the invoking Machine's actor path plusscope— so what retires there is the name and the suffix;conversationwas the cross-Machine pin (a name replacing the path, so two Machine instances — two Pool children — collided by construction, the hazard ADR-0049 recorded and could not refuse);scopesuffixed all of them and carried two intents (a subject undercontinue, a generation counter intask);instanceIdbypassed minting for tests. One boolean replaces all four because the id is deterministic when continued, so nothing needs a handle to address it.The author type carries no mechanism.
endpointandsandbox— set only by stub-tier fixtures that have noworkspace()to resolve from — move to a mechanism type the tests import.tools, the Menu override, retires: a hand-written Menu can only agree with the invoking state's transitions or lie about them, and the lie is a pick that moves nothing (ADR-0029). The one shape it hid — picks written on the CHILD STATES of the invoking state — is derived: those child states are the state's modes (ADR-0015). A state that allows no picks stays legal, because a state a Gate or a timer moves asks its Agent for text and nothing else. A real run never sets any of them: a Sandbox Agent resolves ambiently (ADR-0016), aworkspace: "none"Agent lands on the Instance Harness (ADR-0031).
Considered options
- Prompt-only — tell the model that relative paths resolve at
/workand demand absolute ones. Rejected by the incident: ADR-0024 already established that the same prompt produces compliance on one run and not the next, and every other Machine would repeat the sentence. cwdstays on the definition, overridable per Turn. Rejected: a definition-levelcwdcan only ever say/work, the parent of every checkout, where grep and glob hit the pristinedefault/clone beside the worktree. A default that is wrong under everyworkspace()is not a default.cwdas a third Dial. Rejected on the word: Dials say how hard to run. A directory says where, which is the same kind of fact as the prompt — hence Frame.- Default
cwdto the first Repo Slot. Rejected: reverses ADR-0051 ("there is noworkdir"). The single-slot default is not the same thing — with one slot there is nothing to privilege. - Refuse an absent
cwdunder every Workspace. The safer twin of the single-slot default: one line per invoke, no composition trap. Not taken: the trap's failure is explicit and early, and the line is noise for the common single-repo Machine. - A caller-chosen conversation key (
conversation: "coder"). Rejected: it either restates the structure or contradicts it. Path-scoping the key was proposed and made the key redundant — once the id is<run>/<machine path>/<agent>, the author has nothing left to name. - Continuation scoped to the invoking state. Rejected: splits one conversation across the states that differ only by Menu, which is the case continuation exists for.
- The cross-Machine pin kept as an opt-in. Rejected: zero users since
examples/codingwent (ADR-0054), never sound — the two states it joined sat on different Harnesses, and a conversation lives on one server (ADR-0031) — and an unused opt-in is the fourth mechanism back under a new name. If a sound case appears it is a new decision. - Continue a faulted conversation, author's choice. Rejected: there is no case. Every fault class leaves either a poisoned context or no server — except a Harness restart, which cuts the Turn and keeps the conversation (above).
Consequences
AgentTurnInputis{ prompt, cwd?, continue?, model?, thinkingLevel? };AgentDefinitionlosescwd. The Harness admit body gains the Frame'scwd;ResolvedDefinition.cwdresolves from it, not the definition; the Harness stops validatingdescription, which the Orchestrator strips before admission (it is Console material).- A Frame's
cwdis absolute or the admission is refused, naming the Frame. Absent is the one silent case, and it says/work— a stated default. A relative path has no such answer: it would resolve against whatever directory the Harness process sits in, which is the silent wrong directory this ADR was written for, one level further from the author. The refusal is a 400 at admission, before a pod is spent. taskpassescwd: worktreeOf(context).pathandcontinue: true, and dropsgeneration(and itsscope). It keepsturns, reset onagent.fault, because re-briefing is prompt policy.- The ledger record for a continued conversation carries its epoch; the input mapper reads it at mint time, so the minted id persists with the input and restore re-attaches the same conversation (ADR-0016's mapper rule holds — the fresh suffix is still the random component).
- The
@kindfixtures droptools:andinstanceId:; the steps compute the continued id from the actor path, as they already computed today's pin. - A regression scenario for the silent success: a
taskwhose model uses relative paths must produce its file inside the worktree, and a multi-slot Machine with nocwdmust be refused with the slots named. - GLOSSARY.md: Frame enters; Dials loses
cwd; Instance ID states thecontinuescope and that a faulted conversation is not continued. ADR-0016 (continuation), ADR-0018 (identity line), ADR-0031 (placement scenario), ADR-0035 (reroll gate wording), ADR-0049 (persona hazard), and ADR-0054 (task's continuation) are edited to state this truth.