ADR-0062 Architecture decision record
jr2 briefs the Agent on its seat, never on its task
The model saw two sources of text: the Agent's instructions (identity, ADR-0018) and the Frame's prompt (the Machine state's, ADR-0057). jr2 itself spoke only after a failure — the no-signal nudge — so an Agent had no way to learn facts only the mechanism knows. ADR-0057 opens with that failure: a weak model never learned its working directory, wrote outside the worktree and still called finish. ADR-0060 adds more such facts: os.cpus() reports the node, not the Sandbox's CPUs. ADR-0061 adds events: a kernel memory kill ends a conversation, and the next Agent inherits a Workspace whose processes are gone, with no way to know why.
Decision
jr2 has its own text to the model, the Briefing. It says a fact only if a correct Agent would act differently knowing it and cannot find it out for itself: how it acts on the workflow, the Sandbox's CPUs and Size, the working directory, that a Menu-only Agent has no Working tools, and what happened to the last Turn. Nothing about the task — that is the Frame's — and nothing about style or care — that is identity's. Stand-ins are left out: the only change that knowing could cause is an attempt to go around the Custodian.
Two parts, placed by how often they change. The standing part is the same on every Turn of a seat; it follows
instructionsin the system prompt, in its own delimited section, and is byte-stable (no timestamps, no counters, no Menu names). The Turn part follows the Frame's prompt in a delimited block: the working directory, the notices, a pick's briefing for each Allowed pick that has one (jr2's text on the pick's parameter, which the Orchestrator's surface marks on the pick: the Chart briefing of ADR-0070, said only on a Turn that may make that pick), and the Turn's Allowed picks (ADR-0029). So the Briefing never breaks the provider's prompt cache — the Turn part sits in the newest message, which is uncached anyway, and stays unchanged in the history after.How the Agent acts on the workflow is one standing statement, true whether a pick ends the Turn or keeps it open (ADR-0024): "You act on the workflow by picking from your Menu: a call to one of the workflow's tools that the newest "Allowed now" lists. Each pick's answer says if your Turn is over. If it is over, stop and call no other tool. If it is not, continue. If nothing is allowed, stop: the workflow prompts you again when something is." The last sentence is safe because the wake asks a model that stopped with nothing allowed when a pick opens (ADR-0016). A line that says a Turn ends with exactly one pick would be false for a pick that keeps the Turn open.
The Turn part is written once per Submission, and restated when its Allowed picks change. When something other than the Agent's own pick changes them inside the Turn, the Harness appends the Allowed picks again at the next step boundary, in a
<jr2-turn>block of their own: "The workflow's state changed since you last read the Allowed picks. Allowed now: …" (ADR-0029). It is appended, never a rewrite of the first, so the cache argument above holds for it too. It carries no notice: notices are for the next Turn in their scope. It carries a pick's briefing when the restatement opens that pick.The Turn part comes last, because a model obeys what it read last. Measured on the home-lab vLLM model: with a prompt that argued for a pick the Turn did not allow, the Allowed picks placed ahead of the prompt were obeyed 0/10 (thinking off) and 5/10 (thinking on); placed after it, 10/10 both. Wording made no difference. The author's task still reads first, as it would with no jr2.
Always on. No author can turn it off: every fact in it passes the rule above, so removing one can only make the Agent act worse. An
instructionsthat repeats a fact is harmless.A notice is delivered once, on the next admission in its scope that takes it, then cleared. That is the next Turn, or, for a guard kill that the Agent of an open Turn was not told, that Turn's nudge or wake (below).
- Conversation scope ("this conversation is new; earlier context is gone") goes to that Agent's next Turn. So does a pick's answer that never reached its call (ADR-0024): the route left the Agent's state, and the next Turn of the conversation the picking Turn continued hears "the workflow left the state of your last turn while your pick was outstanding", with the answer — or, when something other than the route left the state while the pick's work ran, "your pick did not finish: the workflow left the state that asked for your last turn while it ran, and that stopped it" (
stopped), with an earlier link's reply when one came. No pick-answer notice says the work ran to its end. A fresh picking Turn has no such conversation, and its answer is no notice. - Workspace scope ("the Sandbox's processes were killed at its memory limit during
coder's Turn;/dev/shmwas cleared. Anything that was running in the Sandbox at that time is gone.") goes to the next admission of any Agent in that Workspace that takes it. One kill is one notice, however many Turns saw it. Only a kill read on a stream while its wait is live names the Turn whose Submission ran. Both forms end with the whole Sandbox, because every process in it died, a shared Workspace's too, and in the past tense, because the Agent may hear it after processes started again. A kill read from the pod's list names no Turn and says nothing of when or whose: "the Sandbox's processes were killed at its memory limit (peak X of Y); /dev/shm was cleared. Anything that was running in the Sandbox at that time is gone." The list cannot say whose Turn a kill ended: the kill may land after a state exits, or end an attach, a sync or a command. A kernel kill read from the Sandbox's last end (below) says the same, with the limit and no peak: "the Sandbox's processes were killed at its memory limit of Y; …". A Menu-only Agent outside a Workspace receives conversation notices only. - A notice never replaces a fault. The Machine still receives
agent.faultand decides (ADR-0016); the notice makes sure the next Agent is not working blind.
- Conversation scope ("this conversation is new; earlier context is gone") goes to that Agent's next Turn. So does a pick's answer that never reached its call (ADR-0024): the route left the Agent's state, and the next Turn of the conversation the picking Turn continued hears "the workflow left the state of your last turn while your pick was outstanding", with the answer — or, when something other than the route left the state while the pick's work ran, "your pick did not finish: the workflow left the state that asked for your last turn while it ran, and that stopped it" (
The Orchestrator is the only keeper of notices: pending notices are plain data in the run's persisted state (ADR-0007). It sees kernel kills and fresh conversations itself. A kernel kill (ADR-0061, layer 5) has two paths, under one source, the Workspace and the time the Harness container ended. A Turn that the kill ended faults on it, and the fault raises the notice with the Turn's Agent. A kill while no Turn runs faults nothing, so before each admission into a Workspace the Agent actor also reads the container's last end from the Sandbox watch, which the operator publishes: a last end
OOMKilledraises the notice, which names no Turn. The ledger'sseenmakes the two paths one notice. An actor whose own watch saw the restart reads the last end only when it re-admits the Turn after the cut. When the Turn faults, the fault's notice names the Turn's Agent. A watch that has not listed yet says nothing, and the next admission reads again: the last end stays until the container ends again. A nudge and a wake take no kernel kill; it stays for the next Turn. The re-admission after a cut takes it, as it takes every notice in scope that is not a guard kill. The Harness reports a guard kill (ADR-0061) on two paths, under one id that the Harness gives the kill:- On a stream. While a Submission runs, the kill is an event on that conversation's updates stream. The Agent actor reads a stream while it waits on the Submission. It stops when the Agent's state exits, before its abort reaches the Harness. So a stream does not always reach a reader: a kill in that window is on a stream that no one reads. A stopped actor raises nothing from its stream.
- In the pod's list. The Harness keeps every kill, with
told: the conversations whosebashcall returned the kill's verdict to the model. A conversation goes into it when the answer returns, not when the guard fires: a call cut short (the container's end, a restart) told no one. A model that writes text, or waits, is not told. The Harness keeps the list in a file beside its conversations and answers it onGET /memory-kills. A kill while no Submission runs is on this path alone: for example, a dev server killed while the workflow waits at a Gate. Before each admission into a Workspace, the Agent actor reads the list. The actor sets the read's limit of 5 seconds, and it is the only limit. A read that never left the Orchestrator, or a draining Harness's 503, is sent again in that time. A read that fails or times out is none, and the next admission reads again: the list keeps the kill. The Orchestrator logs one line when the reads from a Harness start to fail. It logs no line when a read works again, and it logs the failure again only when a read fails after a read that worked. The Agent's uid can write the file, as it can the conversation records: it can change only what its own Workspace is told. So apeakor alimitthat is not a size in the guard's form, or is long, is left out and logged, on the Harness's read of the file and on the Orchestrator's read of the list and of a stream. The kill is kept, so a real kill is never lost, and its notice says no sizes: "the Sandbox's processes were killed at its memory limit; /dev/shm was cleared. …". Only a kill with no id or no time, or one outside the bounds below, is dropped. A stream event from an older Harness has no id; its source is then its place on the stream, exactly as the Orchestrator raised it before.
Both paths raise the notice with the same source, the Workspace and the kill's id. The ledger keeps every source it raised (
seen) in the run's persisted state. So a kill read on both paths, or on two streams, is one notice. Each read of the list raises one notice at most: the newest kill's, unless it is raised already. The read marks the older kills in it seen, and they give no notice, so a burst of kills in one read tells the Agent one thing. Kills read on a stream are not folded: each one with different sizes gives its own notice. Two notices with the same words keep two entries in the ledger, and an admission that takes both says them once. The run is saved when a raise changes the ledger, and only then. A read removes nothing, so a crash between the read and the save that keeps it loses no kill. A restart of the Orchestrator raises no kill twice. A restart of the Harness container keeps the file, which is on the pod's emptyDir with its conversations (ADR-0031). A lost pod takes the file with it, but then the Workspace is lost (ADR-0021). The list keeps the newest 16 kills, each with at most 32 conversations intold, and the Orchestrator reads at most 256 KiB of it. The Agent's uid can write the file, so the two sides hold the same bounds: anidand anatof at most 64, anagentand aninstanceIdof at most 128, and anatthat parses as a time. A kill outside them is dropped, and atoldentry outside them is left out. The Harness counts the bytes of the JSON text, and the Orchestrator counts characters, which are never more, so the Harness keeps no kill that the Orchestrator drops. The Harness reads at most the last 256 KiB of the file. With these bounds the largest list is 149,259 bytes. When more come between two admissions, the oldest is lost, and a newer kill tells the same thing. When more than 16 come in one Turn, the Turn's mark leaves the list too, and every kill in the list then counts as after the mark; a nudge or a wake of that Turn may then carry a kill from before it. The Instance Harness keeps no kill, because it holds no Workspace.A notice counts as delivered when its admission is ledgered, so a restart before that point delivers it again and a restart after it does not. Delivered is not yet heard: if the Orchestrator ends the Turn before the Harness reads its surface, the Harness never prompts it (ADR-0026), so the Orchestrator raises that admission's notices again. This holds for a nudge and a wake too: each one must have its surface read again before its notices count as heard. An admission that fails leaves the earlier admission's notices heard. A nudge and a wake re-prompt the Turn within itself, so they take no notice of the kind a Turn takes. They take only the guard kills of the list that come after the newest kill whose
toldnames their conversation. Thatbashanswer told the Agent that all its processes died, so it covers every kill up to it. They also take no kill from before the Turn started. Such a kill would tell the Agent that processes it started in this Turn died. The Turn's first read of the list sets its mark: the id of the newest kill it saw, or "none". The mark is ledgered with every admission of the Turn, and a restore reads it back, so a nudge or a wake takes only kills after it. Only when the first read failed is the Turn's start the mark, ledgered the same way, with one second allowed for a pod clock that runs behind. A read that failed is "could not tell", never an empty list, so it sets no mark. The kills they do not take stay pending for the next Turn. The re-admission after a Harness restart cut the Turn continues the same Turn, so it takes every notice in scope except the guard kills that this rule leaves out. When its read could not tell, it takes no guard kill at all, as a nudge then takes none: it cannot know which kills its ownbashreported. The notices that the Turn's admissions carried since the Harness last read the Turn's surface are pending again first, each with the source it was raised with, and this re-admission carries them whatever else its scope leaves out. A notice with no source merges only into a pending one with no source, so an equal notice of another kill never takes its place. When such an unread admission carried the Turn's task, the task follows the re-admit text. When the rebuilt Harness lost the conversation, nothing of the Turn is left: every notice that the Turn's admissions carried is pending again whatever the surface reads said, the re-admission takes every notice in scope, as a Turn's admission does, and the Turn's task follows the re-admit text. It says once that the conversation is new. A restore drops the prompt from a Turn's input, so the task rides the ledgered admission too; a lost conversation with no task to send faults the Turn instead. Whether the Harness still holds the conversation is asked again for up to 30 seconds while the new container does not listen yet or drains, and only then is the answer taken as "held". The continue path asks the same way, so a continued Turn after a restart can wait up to 30 seconds before its admission. A Harness in CrashLoopBackOff can take longer than that: the answer is then "held", and it can be wrong. So every admission to a conversation that the Harness was taken to keep says so withkept: true: the re-admission after a cut, a continued Turn, and a nudge or a wake. A Harness that keeps no such conversation refuses it with 404 andkept: falsein the body, and seats none. The Orchestrator takes only that 404 as the refusal; another 404 is a failed admission. Without the refusal the Harness would seat a new conversation, and a model on an empty context would read "continue your task" with no task. On the refusal the Turn takes the path of a lost conversation. The re-admission is admitted as a Turn, with the task after the re-admit text and the notice that the conversation is new, or faults when it has no task to send. A continued Turn is told that its conversation is new, and is admitted again. A nudge or a wake is admitted again as a Turn: its prompt is the task alone, because nothing was cut, with the notice that the conversation is new and every notice in scope. Every notice that the Turn's admissions carried is pending again first, as for a lost conversation after a cut. With no task to send, the Turn faults. The Instance Harness's restart is on no watch, so this refusal is the only way a nudge or a wake learns of it. What the Turn's admissions carried, and whether the Harness read their surface, is kept in the Orchestrator's memory alone: after a restore of the Orchestrator before a surface read, nothing is raised again. A fresh Turn's "this conversation is new", which rides an admission alone, is carried again by the next re-admission when the Harness has not read the one before.A continued Turn of the same conversation is told, as a notice, a kill that its own
bashcall answered in an earlier Turn. The Agent reads that kill twice, which is true and costs little. Thetoldrule could later extend to the first admission of a continued Turn too; it does not now, because a fresh Turn's conversation holds no earlier answer, and the rule would then need the conversation in the ledger's scope.The Harness writes every word. The admit body carries typed notices (
notices: [{ kind: "memory-limit", scope, agent?, limit?, peak? } | { kind: "conversation-new", reason } | { kind: "pick-answer", pick, cut, answer?, stopped? } …], wherepeakcomes only withlimit), and the Harness renders both parts. An outstanding pick's answer is data too — the last reply since the pick, a party's event with its payload or a child's output (ADR-0024) — and its words ("Its answer:approvefrom Gatereview, with {…}", "No reply came back for it.") are the Harness's, in the receipt and in the notice alike; "it moved the workflow, and nothing ran" is a guard straight back's receipt alone. The standing part needs no wire: the Harness already holds its limits (cgroup and Downward API env), the definition'sworkspaceaccess, and the Menu. The wording lives in one place, and Harness conformance checks it (ADR-0027).
Considered options
- Put the facts in the Frame. Rejected: the Frame is the Machine state's (ADR-0057), and a kit that writes into it makes the author's prompt say things the author did not write.
- Leave it to authors'
instructions. Rejected: a definition is written before any run, so it cannot know a Size a composer retuned, a working directory, or what happened to the last Turn — and packaged Agents would each word it differently or not at all. - An author switch (
agent({ briefing: false }), or notices only). Rejected: under the rule, each fact is one a correct Agent needs; the switch only lets the ADR-0057 failure back in. - Everything in the system prompt. Rejected: per-Turn content there breaks the prompt cache on every Turn.
- Finished text from the Orchestrator. Rejected: wording split across two processes, and untestable where the turn loop is tested.
- Report a guard kill on the stream alone. Rejected: a kill while no Submission runs, or after the wait stopped, reaches no reader. A dev server killed while the workflow waits at a Gate is then never told to the next Turn, which works blind.
- Put that kill on every conversation's stream. Rejected: the next Admission reads its conversation's stream from its own offset, which is after the event.
- Keep in the list only the kills that no stream carried. Rejected: a kill on a stream whose wait stopped is then in no place that is read. And two streams that carry one kill would give two notices.
- The Harness pushes the kill to the Orchestrator. Rejected: the Harness container is the Agent's, and ADR-0013 closes a route from it to the Orchestrator. A push also needs a retry and an acknowledgement, and an idempotent read does not.
- Read the kill on the Sandbox watch. Rejected: the operator publishes what the kubelet says about the container, and a guard kill does not end the container. The Harness has no Kubernetes credentials to publish it.
- The Harness words the kill into the next Turn itself. Rejected: a second keeper of notices, outside the run's persisted state, which cannot know when an admission is ledgered or heard.
Consequences
- ADR-0060's CPU fact and ADR-0061's memory-limit notice are the Briefing's first users.
- Each admission into a Workspace costs one more request to its Harness,
GET /memory-kills. The first read from a Harness that fails is said in one line of the Orchestrator's log. - The no-signal nudge stays a re-prompt within a Turn (ADR-0016); it is not a notice.
- The Briefing is how a Turn learns its Allowed picks: the tools block is the whole Menu, fixed for a conversation so it does not break the cache (ADR-0029).
- GLOSSARY.md gains Briefing.