The pi harness: a preset's runs driven by pi in the run's own container, on the run's own record
Record 0032 decided that pi (@earendil-works/pi-coding-agent) replaces the native turn loop one preset at a time; the series is complete, and the loop, both provider adapters and the native tool table are deleted (the source scan in src/core/harness/oneHarness.test.ts keeps them gone, item 1). This is the one harness every run is driven by: pi in --mode rpc inside the run's execution container, started as the thread's user (or, for a preset without a workspace, as a child of the bot itself, item 12) with the run's model-proxy bearer as its only key, driven by the bot over pi's line protocol, every event it emits bridged onto the run's own stream in the vocabulary the native loop wrote, our tools and the coding preset's tool rules as its extension, the transcript mirrored onto the run ledger so a bot restart continues the run, the run's budgets and wind-downs kept by the harness itself (items 6 and 15). A deployment has nothing to set: the harness: block that moved the presets one at a time is a retired key (item 1).
- Code:
src/core/redact.ts(run bearer redaction, item 6),src/core/harness/toolRecovery.ts,src/channels/modelProxyResponses.tsandsrc/core/modelProxy/providerFailureAuth.tsandsrc/core/modelProxy/terminalEvidence.ts(signed structural unknown-terminal evidence, item 6),src/core/budgets.ts(ASKS,FLOORS,ALLOWANCES,POST_STEP_MINUTES,RUNAWAY_TURNS_PER_MINUTE,runawayTurnCap,reserveMinutes,carve,fit: the one table every wall clock derives from, item 15),src/agents/registry.ts(loopBudget: a preset'smaxMinutesandmaxTurnsread from the module, item 15),src/config/profile.ts(budgetedAgent: the guard re-derived for a clipped budget),src/config.ts(AppConfig.harness, the word per preset),src/config/validate.ts(validateHarness: the words the key accepts and what it refuses, item 1),src/core/harness/oneHarness.test.ts(the source scan, item 1),src/core/harness/windDown.ts(the wind-down wording:wrapUpNote,wrapUpInstruction,WRITE_UP_REQUEST,timeBudgetInstruction,turnGuardInstruction,SOFT_STOP_INSTRUCTION,turnGuardPace, the*Noteand*Answerfunctions,HARD_STOP_MESSAGE— items 6 and 15),src/tools/toolsets.ts(TOOLSETS,mergeTools: the relayed tools, item 7),src/core/runLedger/stepReport.ts(StepReport, the mirror's report to the ledger, item 8),src/core/modelProxy/usage.ts(usageFromAnthropic,usageFromOpenAI: the proxy's meter, item 13),src/core/harness/contract.ts(pi's optional output-scratch launch capability),src/core/harness/pi/protocol.ts(the framing and thePiTransportseam),src/core/harness/pi/toolRules.ts(judgeToolCall, the coding preset's tool rules),src/core/publicationEffect.ts(the runner-owned typed Git Door push),src/core/harness/pi/process.ts(piRunPaths,piLaunchArgs,piLaunchEnv,piLaunchFiles,piModelsJson,takesAdaptiveThinking,piSystemPrompt,piThinkingLevel),src/core/harness/container.ts(theHarnessContainerseam with itsidentity,ExecHarnessContainerand its scripts,RUNTIME_WORDandHarnessContainerRuntimeReplacedError: the executors' word for a replaced runtime as the seam raises it;replacedVerdict,ReplacedCondition,identityChangedConditionandreplacedBecause: the one more command before the crash judgement;sleepUnlessStopped: the one pause every wait under the run makes, raced against the hard-stop signal — the one more command's and the pi harness's wait for a write in flight to settle, item 16),src/core/harness/botHostContainer.ts(BotHostHarnessContainer,harnessContainerFor: pi as a child of the bot for a preset without a workspace),src/core/harness/pi/transport.ts(PiRpcTransport),src/core/harness/pi/bridge.ts(PiBridge,PI_EVENT_DISPOSITION),src/core/harness/pi/mirror.ts(PiMirror,chatMessageOf,piSessionFile),src/core/harness/pi/extensionSource.ts(piExtensionSource, the extension pi loads),src/core/harness/pi/relay.ts(HarnessRegistrywithreplaceandforget,RelayedCallswithawaitInFlightandanswered,relayToolCall,authorizeToolCall,answerCompaction,runRelayedTool,relayedToolDefinitions,stillRunningNote),src/core/harness/pi/piHarness.ts(PiHarness: pi as the contract's object, harness.md item 7),src/core/harness/pi/harness.ts(runPiHarness,runPiHarnessOpen,locatePi,relayedTools,splitSeed,promptOf,settlementText,settlementResults,settlementAnswer,compactionSteer;PiContainerReplacedError,replacedCallNote: the container replaced under a live run, item 16),src/core/harness/reattach.ts(classifyLoopFailure,resolveControlResetWrite,reattachTransport,MAX_INPLACE_REATTACHES,PROMPT_ECHO_WAIT_MS,WORD_ALIVE_REATTACH_NOTE,CONTROL_RESET_RESUMED_NOTE,reattachBoundMessage,ReattachKind,HeldSends: the in-place re-attach machinery both harnesses share on a control reset or a refuted replaced word, and the one gate pi's writes take while a prompt awaits its echo, item 16),src/core/descriptionTurn.tsandsrc/core/reviewRound.ts(followUpon their turn specs: the two post-turns on the run's session, item 14),src/core/harness/testing/fakeContainer.ts(the in-memory container);src/channels/harnessRoutes.ts(GET /harness/tools,POST /harness/authorize,POST /harness/tool,POST /harness/compaction;admitHarnessRequest,DOOR_HOLD_MS— the door and its hold during a generation's boot),src/core/harness/pi/compactionFallback.ts(CompactionAsk,CompactionAnswer,compactionAskOf,pointerSummary,POINTER_SUMMARY_PREFIX— the compaction the bot writes when pi's own summary failed for good, item 7),src/core/runLedger/takeover.ts(LedgerTakeover,TakeoverFacts: the ledger's live runs this generation has not finished resuming, what the door holds on),src/core/resumeLaunch.ts(onDone: the launcher's word that a resume is over);src/core/dispatch/runLoop.ts(the run stage: theHarnessobject'sopenfor every run, itsfindandendon afinishplan, the two post-turns, item 14; the relaunch on the typed replaced-container verdict, item 16),src/core/dispatch/relaunch.ts(prepareRelaunch: the ceiling's steps, harness.md item 6),src/core/dispatch/run.ts(RunDeps.harness,HarnessProcessDeps.harnessand.loopbackUrl),src/core/dispatcher.ts(the bearer handed to the loop; the restart from the request after a replaced container, item 16),src/core/dispatch/resolve.ts(resolveTarget: the model ref's provider checked against the config before any card, item 13),src/index.ts(the registry, the routes and the loopback URL),src/cli.ts(askLoopbackServer: the one-shot process's own proxy and routes, item 2),src/core/modelProxy/runBearers.ts(reparent),src/core/runEvents.ts(thecompacted,harness_errorandtool_refusednote kinds; the native loop's kinds kept for older records, item 5),src/execution/executor.ts,resident.ts,cloudflareSandbox.ts(ExecOptions.env),deploy/cloudflare-resident/worker.ts(/execcarriesenv),Dockerfile,deploy/cloudflare-resident/Dockerfile,deploy/cloudflare-sandbox/Dockerfile(pi at the pin),src/core/trace/workerTrace.ts(shimRoute: theharnessroute word),scripts/load.tsandsrc/load/piProcess.ts(--through-proxy);src/core/harness/piAi.ts(item 13:PiAiProviders,PiAiProvider,ProviderTable,piApiFor,toPiContext,piStreamOptions,fromPiMessage— pi's model library as the bot's provider layer for the router's and reflection's calls),src/core/dispatch/route.tsandsrc/core/dispatch/reply.ts(RouteDeps.completions, the table those two calls read),src/index.tsandsrc/cli.ts(that table built for the process),scripts/load.ts(the replay's router on the same table),package.json(pi's model library as a dependency, at pi's pin).;src/core/harness/sessionPolicy.ts(OriginalSessionPolicy,originalSessionPolicyOf,sameOriginalSessionPolicy) - Tests:
src/core/redact.test.ts(run bearer redaction, item 6),src/core/harness/toolRecovery.test.ts,src/core/harness/launchTools.test.ts,src/channels/modelProxyResponses.test.ts(real pi adapter carries signed structural reasons, item 6),src/core/modelProxy/providerFailureAuth.test.ts(legacy compatibility and diagnostic authentication, item 6),src/core/harness/oneHarness.test.ts(the source scan, item 1),src/core/harness/windDown.test.ts(the finale answer over what the ending established, item 6),src/config.test.ts(theharnessblock),src/core/budgets.check.test.ts(the table, the reserve, the carve, the fit and the stack, item 15),src/agents/registry.test.tsandsrc/config/profile.test.ts(the budgets, item 15),src/deploy/imagePiHarness.test.ts,src/execution/executor.test.ts,src/execution/resident.test.ts,src/execution/cloudflareSandbox.test.ts,src/execution/residentExecEnv.test.ts(the env channel),src/core/harness/pi/protocol.test.ts,src/core/harness/pi/toolRules.test.ts,src/core/publicationEffect.test.ts,src/core/harness/pi/process.test.ts,src/core/harness/container.test.ts(the seam's scripts;replacedVerdict, item 16),src/core/harness/conformance.test.tsandsrc/core/harness/opencode/bridge.test.ts(the one more command before the crash judgement on every harness, item 16),src/core/harness/botHostContainer.test.ts,src/core/harness/pi/transport.test.ts,src/core/harness/pi/bridge.test.ts,src/core/harness/pi/mirror.test.ts,src/core/harness/pi/extensionSource.test.ts,src/core/harness/pi/compactionFallback.test.ts,src/core/harness/pi/relay.test.ts,src/core/harness/pi/harness.test.ts,src/channels/harnessRoutes.test.ts,src/core/runLedger/takeover.test.tsandsrc/core/resumeLaunch.test.ts(the door's hold during a generation's boot, item 7),src/core/dispatch/runLoop.test.ts(every run through the harness; the post-turns and thefinishplan),src/core/descriptionTurn.test.tsandsrc/core/reviewRound.test.ts(the post-turns on the session and thefinishplan, item 14),src/core/dispatcher.test.ts(the stops' consequences past the loop, item 6; every end-to-end suite drives a scripted pi through the real bridge, mirror, relay and gate —src/core/harness/pi/testing/providerPi.ts),src/core/modelProxy/runBearers.test.ts(reparent),src/core/trace/workerTrace.test.ts(the route word),src/load/piProcess.test.ts(the through-proxy models file),src/core/harness/piAi.test.ts(item 13),src/core/dispatch/operator.test.tsandsrc/core/dispatcher.test.ts(the two callers oncompletions),src/config.test.ts(the OpenRouter block on pi's library),src/channels/modelProxy.test.ts(the attr set a pi run'smodel.turnspans carry, item 5),src/core/ship/codingChild.test.ts(the unit contract as its own text part of the request turn, item 9);src/core/harness/sessionPolicy.test.ts - Docs: record 0032 (the design and its series), model-proxy.md (the bearer and the meter the harness is the first client of), tracing.md item 17 (the emitters), run-history.md (the ledger the mirror writes), How-to: turn features on and off (the
harnessknob), routing-and-config.md item 21 and memory.md items 11 and 12 (the two calls item 13 moves onto pi's library)
Behavior
One loop per preset; the
harness:key names which harness, pi by default. Every preset's runs are driven by one harness object off the roster (harness.md item 8): pi, unless a scope names another word for the preset — the requester's ownusers.<id>.harness, the channel'schannels.<id>.harness, or the deployment's top-levelharness:block (harness: { coding: opencode }), user > channel > defaults, the word a scope setting like the model's (config set me --harness.coding opencodemoves one person's coding runs and nobody else's; a resumed row keeps the harness it started on); there is no other loop in the tree,AgentDefhas no harness field and the registry exports no roster. Every layer's words are validated at load against the roster's, a stored overrides document included: a mapping of registered presets topioropencodeloads;nativenames a loop that no longer exists and fails the load by name, as does any other word (codex, a case slip, a non-string), each refusal naming the words that exist (harness.<preset>: native is not a harness; the harnesses are pi and opencode), a preset the registry does not know (harness.<preset> is not a known agent) or a non-mapping (harness must be a mapping of preset to a harness name (pi or opencode)) — a setting that read as "this preset runs on X" while nothing did would be the worst kind of quiet. The source scan (src/core/harness/oneHarness.test.ts) fails the build the day the deleted loop comes back or the roster leaves the wiring:src/runner.ts,src/providers/,src/tools/workspace.tsorsrc/core/harness/select.tspresent; a module undersrc/importing one of them or callingrunAgent,ProviderRegistry, an adapter,ExecHealthTrackeroreffectiveHarnessby name; arunAgent,effectiveHarnessorrunPiHarnessOpenreference, a harness comparison or a harness class in the run stage (the loop opens every run through the seam's door on an object it picked off the roster, harness.md items 7 and 8); a preset declaring a harness or the registry exporting a table of them, the roster's names or its pick;src/index.tsorsrc/cli.tsno longer constructing both harnesses; a workspace tool of pi's own in the toolset table;run-loop.mdback in the specs index. A preset with a workspace runs its harness process in the run's container; a preset without one runs it as a child of the bot (item 12).The run stage hands every run to the harness, and everything around it is shared.
runLoophands the run to theHarnessobject it was given —PiHarness.open, which isrunPiHarnessOpen, item 14's open form; the loop names no harness and compares no word (harness.md item 7) — with the tool context, the event and progress sinks, the control, the inbox and the ledger step hook, therun.agentparent span and the resume plan — and the rules the gate (item 7) judges pi's own tools by, which follow the thread the way the post-step's pull-request target does: when the thread came from a pull request or a coordinator's spawn, the thread is bound at the run's own branch (a fix round repushes the PR's head; a unit child is dispatched at its unit branch), so that branch is the one push target and the base its pull request targets is protected; otherwise the binding is that base, protected, and the run pushes a branch of its own making. Admission, resolution, the gates, the workspace attach, the bearer mint and the prompt before it, and the reviewed-head settle, the workspace observation, the description turn, the coding PR post-step, the answer's publish, the finish and the release after it are one code path for every preset — the settle's re-review and the description turn reaching their one more model turn through item 14's seam: the post-step sees the same branch and pull-request facts because it reads them off the same stream (trackPushedBranchover the bridge'stool_call.command) and the same workspace. No run starts without pi: a process without the harness deps, withoutPUBLIC_BASE_URL(the container reaches the proxy and the routes through it; for a preset without a workspace the missing name isPORT, item 12) or without a minted bearer fails the run naming exactly what is missing. The CLI's one-shotaskis such a process: it opens its own loopback HTTP server for the length of the run — the model proxy and the four harness routes, nothing else (askLoopbackServer,src/cli.ts) — so the run's pi reaches it over127.0.0.1whether it runs as a child of the process or in the local workspace, and closes it once the run's record has settled.pi lives in every image, pinned and proven. Each execution image installs
@earendil-works/pi-coding-agentglobally with its own Node 24 at one exact version (1.0.4), read by the pin parser like pnpm's and playwright's; the layer proves at build time that thepion PATH answers the pin and that its help names RPC mode, and the resident image proves it once more asworker1, the way a run's pi is started. The bot image installs the same layer in its runtime stage, as root before the switch to the bot's user, on the Node it already builds from, for the presets without a workspace, whose pi is a child of the bot (item 12), and the pin is held equal across the three images and the root and published package's@earendil-works/pi-aidependencies (item 13), so pi's library in the process and the pi in the images move together. Outside the images pi comes from the process's PATH — the CLI'saskon a developer's machine, the test that spawns it (src/cli.ask.test.ts) on a CI runner — so@earendil-works/pi-coding-agentis a devDependency at the same exact pin, andnpm test,npm runandnpxput itsnode_modules/.bin/pifirst; a machine without npm's PATH needs pi installed at the pin. Nothing in an image starts pi; the harness does, once per run.The process: pi as the thread's user, the bearer as its only key, through the container seam. A run's pi has one directory of its own outside the checkout, directly under
/var/tmp:/var/tmp/switchboard-pi-<runId>(piRunPaths, the constantPI_RUN_ROOT_PREFIX; the same layout under any root ispiRunPathsAt, how a re-attach reaches the root a row recorded, item 8), made at mode 700 by whichever OS user runs the run's commands (the umask before the mkdir in every script that creates it: themkdtempshape)./var/tmpis sticky (mode 1777) like/tmp, so every run's root is a sibling under a parent any user may create in and only the owner may remove from, and nothing about the root depends on knowing which user that is. It is/var/tmpand not/tmpbecause the model's shell runs in the same container as the same user, and the shared temp directory (/tmp,$TMPDIR) is exactly what a checkout's test suite or cleanup empties — taking the harness's own FIFO with it, which lost a live run — while nothing has a reason to touch/var/tmp. There is no parent between/var/tmpand the root in any case, shared or per user: the resident runs each thread's commands as its own pool user andmkdir -pgives a parent it creates to its caller, so a parent shared by every run would belong to whichever user ran first and every other user's run would fail at its own mkdir before a model turn, a shared parent made 1777 would not repair a container where the owned one already stands, and a parent per user would only move that ownership to the runs that know their user. The directory holds pi's config directory withsettings.jsonthat never trusts the checkout, carries the shell command prefix that makes the bash tool's timeout final (PI_SHELL_COMMAND_PREFIX, item 15), and sets pi'shttpIdleTimeoutMsto the run lease remaining when this process starts — pi applies that to undici's between-byte bound and provider requests, so a fixed transport default cannot cut a reasoning pause before the harness's own lease does — and carries the deployment's compaction thresholds when the config'spi.compactionblock sets them (reserveTokens,keepRecentTokens, pi's own names in tokens: pi compacts when the context passes the model's window less the reserve and keeps about the recent tokens named; unset, pi's defaults stand and the file is byte-identical to before the block existed; a receipt or a test of the compaction path sets a reserve near the window so a short run compacts, production leaves it unset; positive integers only, any other shape or key refused at load by name) —models.json,SYSTEM.mdandsessions/, the extension, the FIFO, the log, the error log and the pid file.models.jsonnames one provider — the bot's model proxy on the wire shape the run's provider speaks (anthropic-messagesat the bot's base,openai-completionsunder<base>/v1) — withapiKey: "$SWITCHBOARD_RUN_BEARER", a zero rate card, because pi's own cost is never what a page shows: the proxy meters. The model entry is the run's card (model-proxy.md item 11), never one pi invents: the resolved level map asthinkingLevelMap(piThinkingLevelMap) — each tier the card's own wire word, a fallback's word included so pi clamps nothing the card did not, a refused tiernull, and the identity map when no layer names the levels, so an unknown tier goes out as its own word, unvouched, never pi's silent clamp — the card's window ascontextWindow(200,000 stands where no card came: a hand-built spec), its inputs asinput(["text"]when the card says no images), and — on the completions shape — the card's cap field ascompat.maxTokensFieldwhen it is one of the two spellings pi's compat takes, and, for a block whose biller a harness speaks natively (billerHarnessProvider, record 0052's amendment), the biller's own compat words beside the card's — so pi through the proxy behaves as pi does against the biller directly, since through the proxy pi sees the bot's URL and its own detection would take the block for a generic endpoint:compat.cacheControlFormat: "anthropic"when the card's cache rule is markers (an Anthropic vendor through the aggregator; a markers card on a biller no harness speaks natively carries no marker word — its cache row is already degraded),thinkingFormat: "openrouter"(the tier goes out asreasoning: { effort }, never the flatreasoning_effort),sessionAffinityFormat: "openrouter", andsupportsDeveloperRolesaid either way from the biller's id prefixes (its Anthropic and OpenAI ids carry the role, any other id is told not to claim it) — the four words copied once from pi's own completions detection as the biller table's row, never inferred from a URL at run time, each forwarded by the proxy untouched — and — on the Anthropic shape — the thinking payload the model takes as pi'scompat.forceAdaptiveThinking(takesAdaptiveThinking): a Claude model from the 4.6 generation on takes adaptive thinking (thinking.type: "adaptive"plusoutput_config.effort) and the 5 family refuses the legacy budget (thinking.type: "enabled"plusbudget_tokens) with a 400; a model before 4.6 refuses adaptive and keeps the budget. pi's built-in Anthropic catalog says this per model and sends the budget for a model it does not know; the proxy's model is not in that catalog, so the harness says it — the generation is the id's first version number (claude-fable-5is 5,claude-sonnet-4-5-202509294.5: a date is never a minor), and an id with none is taken for the current generation. The completions shape carries no such switch. pi starts detached (setsid -f) with--mode rpc, every discovery off (--no-extensions --no-skills --no-prompt-templates --no-themes), the extension by explicit path,--toolsallowlisting pi's own workspace tools (read,bash,edit,write,grep,find,ls) plus the relayed ones, the proxy's provider and the run's model with the effort tier as pi's thinking level (the five tiers are pi's levels by name; no tier leaves pi's default), and a session directory — or--session <file>when pi has turns to start from: the thread's earlier turns (item 9) or a resume's mirrored transcript (item 8). Its stdin is a FIFO the wrapper holds open read-write on a spare descriptor, so pi never sees an end of file while it lives; its stdout goes through a line-buffered filter that dropsmessage_updatedeltas into a log the harness polls; its stderr into its own file; the wrapper's pid, the group leader, is recorded. The environment is the channel for the bearer: the run bearer, the bot's URL, the run id,PI_CODING_AGENT_DIRand pi's offline switches rideExecOptions.env— the exec's own env option on every executor (the local child gets the public environment plus these and never the host's secrets; the resident and the sandbox send them as the body'senv, the resident Worker reading the map through the one validated reader and handing it to the exec's env option under its own injected variables — the same body map the run's four commit identity variables ride, resolved per exec and winning a clash, execution.md item 5) — never a command line, never a file, never a model key, Slack token or GitHub App credential. Every operation is one command over the run's ownExecutoras the thread's user — a file written inprintfchunks underumask 077, a line fed to the FIFO (a long one through a file), the log read from a byte offset as one-line base64 so the bytes survive the text channel exactly,kill -0for liveness, the container's identity read (identity: the kernel's boot id from/proc/sys/kernel/random/boot_id, one word or nothing; a known original placement without a current identity retains custody before any probe; legacy unbound placement remains observational compatibility only by it), the group then the leader killed, the run's directory removed as one tree — and a command the executor reports failed is a namedHarnessContainerError, never a silent empty. A control file of the run's own that vanished under a live run — the send into the FIFO failing with the shell's no-such-file while the container itself is alive and answering — is the typedHarnessControlFileLostErrornaming the file and the root: the run fails fast with aharness_errornote saying which file under which root is gone, never a replaced-container verdict (isContainerGoneandsaysContainerReplaceddo not match it). Once the run is over — settled, stopped or failed — the harness ends pi and then removes the run's directory, best-effort like the kill: nothing reads its log, session or FIFO again (a later run in the thread seeds from the record, item 9; a resume that finds pi alive, item 8, belongs to a generation whose run never ended).makeRootis the seam's answer to where a fresh run's files go, from the root pi proposes (piRunPaths): the exec container answers the proposal itself, which its scripts make; the bot host makes a root of its own beside it (item 12); the harness lays pi's files out under whatever root comes back (piRunPathsAt), files nothing before it has the answer and records it on the row (item 8), so the way back never has to guess it. The seam itself names no harness (harness.md item 9): the program (pi), its arguments, the filter on its stdout (PI_STDOUT_FILTER, themessage_updatemark) and pi's layout (PiRunPaths, pi's files over the seam'sHarnessPaths) are the start's inputs, and the script the exec container runs for them is byte for byte the one it ran before they were. The in-memoryFakeHarnessContaineris the seam's second implementation (it answers the root proposed too); the bot host'sBotHostHarnessContainer(item 12) the third.The bridge: pi's events become the run's, in the vocabulary the native loop wrote, and the proxy meters the turns. Every event kind pi's protocol documents has a disposition (
PI_EVENT_DISPOSITION, held to the same set of kinds as the spike's home table):tool_execution_startopens atool.<name>span underrun.agentand announces thetool_callinside it (the same one-line summary the loop writes, bash's full command beside it, capped and redacted);tool_execution_endemits thetool_resultwith the loop'sok— pi'sisError, or, for a relayed tool that declaresfailsInText(RunnableTool; the harness hands the bridge the declaring names astextFailing), a result text openingerror:read as the deleted native runner read it (toolTextFailed, run-visibility.md item 5), while a relay of someone else's bytes (read_file,web_fetch, the skill and GitHub reads) is never judged by its content — bash'sexitCoderead from pi'sCommand exited with code Ntrailer (or the executors'exit N:prefix), the summary and the capped output, and ends the span withcallId,ok,exitCode; the model's text beside its tool calls isassistant, a text-only turn is the answer candidate, a turn whose every call isupdate_statusis bookkeeping — not counted against the guard, its text held until the next turn decides (the guard, item 15, is not spent by it);💭 thought for …is said at each assistant message's end;compaction_endis acompactednote with the token counts and any summarization retries folded in;extension_error, a dialog request (answeredcancelled, since no one is watching), an event kind this build does not know and a kind the harness turns off at start (auto_retry_*,bash_execution_update) areharness_errornotes naming what happened, each said once per kind for the run (harness.md item 4: the first arrival is the finding, later arrivals of the kind write nothing);message_updateandtool_execution_updateare folded into the final records;response, the agent and turn boundaries andqueue_updateare structure. The model turns are not the bridge's: every call pi makes goes through the model proxy, which spends the run's turn guard and closes onemodel.turnspan with the runner's attrs as the proxy knows them (model-proxy.md item 6:model,stopReason, the four token counts,ttftMs) and never the stream timings only the deleted native runner's observer wrote (blocks,thinkingMs,textMs; live-view.md item 15), so a record from before the deletion is told from one since by their presence — and the harness re-parents the run's grant from the request root to itsrun.agent(RunBearerStore.reparent), so a proxied turn lands where the loop's would. The vocabulary is held as a literal: thetool_call,tool_resultandassistantevents the native loop emitted for one turn — compared equal against a scripted provider while both loops stood — are what the bridge must still emit for the equivalent pi stream (src/core/harness/pi/bridge.test.ts). What the native loop did around a turn that pi does its own way, and what went with it. pi runs a turn's tool calls as it chooses (the loop ran consecutive side-effect-free calls concurrently, mutating ones alone in order); pi's own adapters place the prompt-cache markers and replay thinking (the loop's Anthropic adapter set fourcache_controlbreakpoints, a per-agent TTL and clampedeffortper model —AgentDef.cacheTtlstays on the def for the router's and reflection's requests and is not read by the harness); the loop's fail-fast on a wedged sandbox (ExecHealthTracker: two consecutiveExecInfraErrors with no success between ended the run with a diagnostic) and itsfleet_busyretry text are gone — the harness's container operations and the relayed tools read anExecInfraErroras the failure of that one operation, named, aHarnessContainerErrorwhen it is pi's own file or process, and the container is provisioned before pi starts; its stuck-loop guard is the gap below, and its rolling-container settlement is item 16's floor — the run ends for a restart from its request instead of going on. The note kindssandbox_dead,fleet_busyandstuck_loopstay onRunNoteKindso a record from before the deletion still reads, and no loop writes them;sandbox_restartedis the harness's own again, said of a container replaced under a live run (item 16).The wind-downs, in the loop's words (
src/core/harness/windDown.ts). The harness counts pi's turns for the guard and keeps the lease's clocks (loopClock, the budgets, item 15): the lease ends at its deadline and the loop ends earlier, atloopEnd, with the write-up's and the post-step's allowances held back so both run inside the lease; it steers the wrap-up warning once as the loop's end nears (wrapUpInstruction, awrap_upnote), steers the session's notepad after everycompaction_endthat carried a summary (compactionSteer, session-log.md item 10: the notes as they stand, or that they are empty and how to keep them, and thatrecallreaches every earlier turn — never during the write-up, after a hard stop or while a re-attach catches up on history; a notepad read that fails there costs the steer its notes and never the run), and at the loop's end or the turn guard it steers the loop's forced write-up instruction (timeBudgetInstruction,turnGuardInstructionwith the pace) with every tool refused meanwhile through the gate (item 7) — the model reads the refusal's reason — and answers with the loop's label (⚠️ _Hit the N-minute budget…_,⚠️ _Stopped after N model turns in M — that pace looks like a loop…_) around the text pi settled on, or the loop's reason alone when none came — and hands the run loop the ending that labelled it (HarnessSession.ending, aWindDownEnding: the kind, the text, the failed call), since the harness answers before the loop's budget salvage, description turn and PR post-step have run and cannot know what the tree held. The finale answer reads what the ending established: the run loop composes the thread's answer again after those steps, through the one composer (windDownAnsweroverEndingFacts), in this precedence — the tree, then the description, then the write-up's text. A tree the salvage pushed names the branch and the head as where a follow-up starts; a tree measured clean with nothing unpushed names its head on the remote; work measured and pushed nowhere is said with its counts and its fate under the release the record decides — kept for the thread by a cold workspace'sif-idlerelease, discarded at the run's end by a resident's (a run starts from a clean tree), torn down when a command may still be running in it (harness.md item 13) — so a discarded or torn-down tree is never said to hold work; a workspace that could not be measured says so; a run with no workspace names none; and only where nothing was measured (a run that observes no tree, a tail skipped by a hard stop or a relaunch) do the harness's own words stand, that partial work may exist. Whether a PR description was submitted follows the tree's sentence; the write-up's text follows the label, which carries the facts beforeFindings so far:. The harness's own answer is the same composer with no facts, so the words never fork, and an answer a post-turn replaced (a review's re-review at a moved head) is left as it is. The same shape on OpenCode: the conformance rowsbudget-ending-*hold every harness to it; the write-up is bounded by its allowance (ALLOWANCES.writeUp, three minutes,loopClock.finaleMs; thenabort); a tool call in flight at the loop's end is cut (decision 0046, unit seven): the abort follows the write-up's steer on the transport's chain, so pi has the instruction first and reads it as its next turn once the cut call's result landsaborted, and the write-up keeps its whole allowance instead of the finale being spent waiting the command out — atool_cutnote names the tools beside the budget note, and the aborted model call pi reports for the cut turn is the cut's own failure, never the write-up's; a model call in flight is left to answer, the steer landing at its turn boundary; the steer also marks the loop's end on the run's bearer entry, so the model proxy sends the checkpoint turn upstream withtool_choice: none(model-proxy.md item 6). A soft stop steersSOFT_STOP_INSTRUCTIONthe same way and answers⏹ _Stopped early by an operator (soft stop)…_; Stop attempts the current-turn interrupt, ends pi and forces cleanup, then sends one neutral⏹ Stopped by user.reply with no write-up. Kill skips the turn interrupt, suppresses final output and removes the row only after confirmed runtime shutdown; past the loop the run stage releases the workspace withalwayswhatever the preset (a resident/detach force), runs no memory reflection and posts no review — there are no findings to distill or post (live-view.md item 10 has the route). The notes are the loop's kinds (wrap_up,time_budget_exhausted,turn_budget_exhausted,stoppedwith its mode), so the friction analyzer reads one vocabulary. A thread follow-up (thread-admission.md item 2) is steered into pi as the loop's follow-up prompt, its images along, recorded as the sameinputevent andfollow_upnote, its ledger seq folded into the next step record. A follow-up that carries staged files (execution.md item 20) is staged before its steer is sent — the hook the run loop binds only when the deployment configures a store: each file is copied into the store and pulled into the container's workspace over the run's executor, theinevent recorded, and the steer's text ends with the attachments line, so pi never reads a name it cannot open; steers go out in the order their follow-ups were drained, one batch's staging awaited before the next is sent; a hook that throws still steers the person's words with a line saying the files could not be staged. Without a store the steer is sent as it always was. Auto-retry is turned off at start. Pi reports a typed terminal result: a proven provider failure or refusal, a pi-local abort or incomplete stream, or an unknown ending. Catch-up after a restart (item 8) and wind-down (item 15) retain their endings. A transient or rate-limitedProviderFailurecause is held for at most two retries within one minute and the loop lease; billing/credit/hard-quota failures end immediately; its cause comes from the one classifier insrc/core/provider.ts, never a harness prose list. A pi-local abort or a stream ending before pi receivesmessage_stoporfinish_reasonuses the same lease-bounded backoff but does not claim the provider went down and cannot be released by provider-up. An unknown ending fails without retry or a provider-refusal claim. An intentional tool-cut abort is booked first and a wind-down abort remains the wind-down's; a ledger-tracked run's plane reissue steer may release only the transient or rate-limited hold, within the same attempt budget and never before a rate-limit wait ends (execution.md item 29, model-proxy.md item 12a). A call the provider refused under its usage policy is the failure by name: pi maps the wire's refusal to an error and keeps the wire's own stop reason beside it (rawStopReason— the Messages API'srefusalandsensitive, the Chat Completionscontent_filter;POLICY_REFUSAL_STOP_REASONS, a closed set of words, the explanation's text never read), the bridge recordsprovider_refusalas the terminal result, and the harness never retries it (the same words are refused again), publishes apolicy_refusalnote carrying onlyrenderProviderFailure(permanent), and ends the run withModelPolicyRefusedError— whose message is that same one sentence,The model provider refused the call; the request ended without exposing the provider's response., with the run link and none of the provider's words — so the record saysfailure: policy_refusal(run-history.md item 57) and the session's next seed leaves the refused request out (session-log.md item 9); a follow-up turn on the session (item 14) refused the same way ends the same way. pi's own death before settling fails the run with the tail of its stderr; a prompt pi refuses fails it by reason.The extension and the gate: our tools registered into pi, every tool call decided in the bot. Before pi starts, the harness writes its extension into the run's directory (
piExtensionSource, plain JavaScript using Node builtins and pi's pinned public SDK: pi's own loader resolves the SDK outside the checkout). At load the extension registers the tool declarations frozen into its source by the existing runner-owned launch input, each as a pi tool with the bot's JSON Schema, soupdate_status,submit_pr_description,submit_handoff,submit_dispositions, the run-boundpublish_brancheffect on a coding run with a Git Door and bearer store,attach_file,diff_digest,web_fetch, the skill tools, the GitHub tools, the session toolsrecallandnotes(session-log.md item 10) and any bridged MCP tool exist under their own names — the preset's toolset less the workspace tools pi has of its own (relayedTools) — and the composed system prompt gains one last note mapping the native workspace tool names onto pi's. A relayed tool's call posts toPOST /harness/tooland runs in the bot with the run's own tool context — the sameRunnableTool, the executor, the requester's GitHub gate, the dispatcher's recorders — under thetool.<name>span the bridge opened, its result in pi's content shape (images as image blocks, a document as its descriptor), a failure as an error result the model reads. Itstool_callhook asksPOST /harness/authorizebefore every tool executes, pi's own included: a relayed tool is allowed under the bot's own gates (the publication effect also requires an exact active tool name and input observed by the bridge, not just a run bearer); pi's own tools are judged by the coding preset's tool rules (judgeToolCall: on a write run with a Git Door, no model shell push obtains publication authority, including a literal owned push; the runner-ownedpublish_branchtool instead checks the checked-out source, clean tracked tree, bound origin, destination, old head and immutable source commit before its per-effect credential opens one Door slot; without the typed tool a publish fails closed; never a merge or an approval; never the executor's credential store, an environment dump or a credential-named variable; paths inside the checkout; a tool outside the preset's reach; a bash call whose explicittimeout— pi's, in seconds, unbounded when absent — reaches past the loop's end,loopEndon the harness's clock (item 15; a follow-up turn's own deadline while a turn runs, item 14), refused before it runs with the seconds asked, the seconds left and the two ways forward, re-issue inside what is left or push and write up, in the wind-down's words (commandPastLoopEndRefusal) — a call naming no timeout is never refused by that rule, agit pushin the last minute must run, and the loop's end cuts it as before (item 6)); during a write-up every tool is refused. A refusal is atool_refusednote on the run and the reason the model reads. A compaction pi's own summary cannot write carries the bot's pointer summary (record 0035: a compaction is a pointer, never a loss). pi compacts by asking the model for a summary of the turns it drops, and that call can fail for good — the provider's classifier refusing the serialized turns under its usage policy, a summary over its cap, a summary that called a tool — and pi tries again at every turn boundary while the window stays over the threshold, each try paying the summary calls before the failing one and none succeeding, until the context overflows. The extension'ssession_before_compacthook asksPOST /harness/compactionhow the compaction is written — once, with the preparation's facts (CompactionAsk: pi's trigger,tokensBefore, the summary the compaction before it left, pi's own file lists) — and hands pi the bot's summary as the extension's compaction under pi's ownfirstKeptEntryIdand size, the file lists as itsdetails; a bot that says nothing, cannot be reached or refuses leaves the compaction to pi. The bot's word (answerCompaction,src/core/harness/pi/compactionFallback.ts) is nothing while pi's own summary stands, and the pointer summary once the run's last compaction failed for good: the bridge hands a failed, unabortedcompaction_end's words to the harness (BridgeObservation.compactionFailed), the harness keeps them unless the typedProviderFailure.causeis one the provider-down set says pi's next try would ride out, the relay takes them once (LiveHarness.takeCompactionFailure) and a compaction that lands clears them, so pi's own summary is tried again at the compaction after. A compaction that failed for good is also a checkpoint signal: the run's work must not sit unpushed while the window grows, so the harness awaits the run'sonCompactionFailedhook (HarnessRun; the run loop wires it for a coding run bound at a branch of its own) — which commits the tracked changes and pushes them to that branch in the push-before-abort's shape (apushed_headevent,by: "salvage"; run-history.md item 2) and publishes acompaction_salvagenote naming the failure, a clean tree getting the note alone — and the loop goes on while the context still fits, a context that no longer fits ending the round with the push already made; the hook is best-effort (a throw is aharness_errornote, never the run's end) and is not called on a re-attach's catch-up, whose failure is history. The pointer (pointerSummary, opening withPOINTER_SUMMARY_PREFIX) says why pi's summary could not be written in the failure's redacted, capped words, that every dropped turn is in the session log behindrecalland the notes are where to keep what is still needed, and carries the summary the compaction before it left and the file lists in pi's own tags, each only when there is one. Its compaction is a row like any other (session-log.md item 6), itscompactednote says the window carries the bot's summary, pi's own having failed, and the notepad steer follows it (item 6). The gate's coverage is checked from the stream. The authorize route tells the harness the gate saw each call (LiveHarness.gateSaw, whatever the answer; the bridge keeps the ids), and pi'stool_callhook fires only after pi's own argument validation (pi-agent-coreprepareToolCall), so atool_execution_endthe gate never saw is one of two things. A call pi answered by itself — its arguments failed the tool's schema, its tool is not on pi's list, or its assistant message was cut by the output token limit; pi announces it, ends it with an error result in its own words and never fires the hook (piAnsweredWithoutRunningreads those words) — never ran: it is aharness_errornote naming pi's reason and refuses nothing. So did a call the extension blocked by itself, without a verdict — refused at the door by a 4xx before the route reached the gate, or a bot that did not answer for the wait — whose end carries the extension's own reason (authorization refused at the door: …,authorization unavailable: …): aharness_errornote naming it. Every other such end is a tool that ran unvetted: aharness_errornote names the call, pi is aborted and the run fails closed on it (the gate was bypassed: pi ran <tool> (call <id>) without asking the bot), itsrun.agentspan ending in error. Calls that started while the harness was catching up on a re-attach (item 8) are not judged — the generation that died vetted them. The hook blocks at once on a refusal at the door — a 4xx: the bearer wrong, expired or revoked, the run not on the harness — since nothing later changes it, and keeps asking a bot it cannot reach or that is failing (a network error, a 5xx) every two seconds, blocking the call by itself after 90 seconds, naming why. The four routes are the model proxy's door: the run bearer names its run and verifies against the store before a byte of the body is read (no bearer or a malformed one 401, an unknown run 404, a wrong secret 401, an expired or revoked one 403), a run not on the harness is 404, the body is capped and must be JSON, and a bearer buys nothing else. The shim forwards the paths blind under the one route wordharness. A relayed call may outlive one request. The conductor'sawait_runswaits for minutes andsend_to_runmay wait on a child's admission, soPOST /harness/toolholds a request for at most the relay's window (RELAY_POLL_WINDOW_MS, 30 s) and then answers202 { pending: true, toolCallId }; the extension asks again with the same call id, at once after a window and no sooner than a second after a bot that answered early, until the answer lands. The bot runs each call once: the run's relayed calls are kept by call id beside its registration (RelayedCalls), a request for a call already running joins it, an answered call keeps its answer until the run ends so an ask retried after a lost response reads the same one, and aspawn_runasked twice spawns once. A call the run's row says was in flight when the previous bot generation died is settled on the registration, before anything is awaited (RelayedCalls.settle, item 8): the re-attached pi's extension, whose first request died with that generation, asks again with the same call id and reads the restart note (isError, the words the rebuilt session file carries for the same call) — the tool never runs a second time; a call the row does not know runs as before, and a call already running keeps its own answer. The window sits under every timeout on the path: the extension gives up on one request after 60 s of its own (AbortSignal.timeoutbeside pi's abort signal), pi's fetch on the headers after 300 s, and the bot's server bounds only a request's arrival (its header timeouts, 60 s and 300 s, which a body that arrives whole never nears), never a response. A bot the extension cannot reach or that fails mid-wait (a network error, a timed-out request, a 5xx) is asked again every two seconds and the call fails after 90 s without an answer, counted from the bot's last one, naming the tool; a 4xx at the door fails it at once; pi's abort ends the asking. When the run ends, a relayed call still running is told to stop through its context's signal and the calls go with the registration. A tool that answers within the window (every relayed tool but the conductor's waits) is answered in one request exactly as before. The door during a generation's boot. The routes answer from the moment the bot's HTTP server listens, which comes before the ledger reclaim, the Slack handshake and the resumes (src/index.ts), and a run is on the registry only once its resume has reached the harness (HarnessRegistry.registerinrunPiHarnessOpen, after the resume's admission, workspace re-attach and bearer mint), its previous bearer adopted right after (item 8). A request from a surviving pi that lands in between names a run this process does not know yet —unknown_runbefore this generation minted the run's entry,unknown_bearerbetween the mint and the adoption,run_not_on_harnessbefore the registration — and the extension takes a 4xx as final, so the door does not answer one. It reads this generation's takeover of the ledger's live runs (LedgerTakeover,src/core/runLedger/takeover.ts: fed by every reclaim outcome, at boot and on each sweep pass, and by the launcher when a resume's dispatch is over or the run was closed instead). A request with one of those three refusals while the boot reclaim has not yet listed the ledger is held — the response waits for the listing, up toDOOR_HOLD_MS(HANDOFF_BUDGET_MS+ 4 s = 10 s: the budget the previous generation had to hand its runs off, as long again to take them, under the extension's 60 s on one request) — and judged again once it lands. Then a run the ledger lists as live and not yet resumed here (handed to the launcher and not yet ended, or held by another generation until the sweep takes it), or any such bearer while the listing has still not arrived, is answered retryably:POST /harness/toolgets202 { pending: true, reason }— a pending answer, which the relay asks again with the same call id and which refreshes its 90 s wait, so a resume longer than the wait still lands — and the other two routes503 { error: reason }withRetry-After: 2(the hook's own cadence; its 90 s from the first ask stand),reasonbeingrun_resumingorreclaim_pending; the body is never read for a held answer, as for a refusal, and the log line saysheld (<reason>) run=<id>. The re-ask then lands on the re-registered run and reads the answer settled for it (item 8). A bearer no resume can make good — malformed, expired, revoked — is final whatever the boot is doing; once the reclaim has listed the ledger a bearer no live row names is401/404as before, and a run's resume ending (its dispatch settled, or the launcher closed it) returns the store's verdict on its bearer. The publication effect issues a distinct one-use run bearer only into its executor call's environment; the Door also requires that credential's hash, the source commit, destination and old head before receive-pack, including its advertisement. The ordinary model bearer cannot borrow a concurrent effect's slot. An accepted effect still needs the Door's durable outcome and a matching remote head; an existing-PR receipt pairs the observed typed call, runner admission and result on the ledger. Runner checkpoints use a separate effect credential too. The shell guard's recognizer is defense in depth, not a proof of what arbitrary executables do. The policy table's own tool-level rows remain the authorization spec's open gap. Workspace runs also receive a separatescratchdirectory under their owned root. Use its absolute path with file tools and$TMPDIRwith commands. It is outside the checkout and runtime configuration. Reattachment restores this access only when the original launch facts record it; cleanup follows the existing producer-custody and owned-root rules.The mirror and the way back. pi's finished messages become the ledger's transcript in the runner's vocabulary: an assistant turn (its text and tool calls; thinking dropped) is one step record with its calls in flight, written before the tools' results land, exactly as
runAgent's step report; ordinary results since, and any steer's text, are the one user turn the next step carries. A controller-recorded source result instead checkpoints its observed nativemessage_end(toolResult)through the same step acknowledgment before the next model call. The checkpoint waits for all declared parallel results, so the existing resume record keeps complete call custody. It preserves indices and reattach deduplication. The existing native drain waits for this saved result before model capacity is reserved; a native tool ending alone is insufficient; the prompt pi echoes on a fresh start is the seed already on the ledger and is not written twice. The row's state gains the harness facts (harness: { harness: "pi", pid, logOffset, root, sessionFile, bearerHash, wire, container, relaunches }—harness: "pi"names the harness that wrote the row, so the row is read as pi's and refused by any other (harness.md item 7; a row from before the field is read as pi's, the one harness that ever wrote one), andrelaunchesis the run loop's count of the process's relaunches, 0 on a fresh start and carried unchanged through every save and every restart of pi within the run, so the bound the loop keeps survives a bot generation): the pid, the run's root (the directory pi was filed under, item 4) and the container's identity (HarnessContainer.identity, item 4) from the first save, right after pi starts; the session file onceget_statenames it; and the log offset, the byte the next generation reads pi's log from. What the saved offset names: the boundary after the last record whose effect the ledger holds — the assistant turn the mirror wrote as a step, the compaction it wrote as a row — saved right after each such write. Never where the transport's chunked reads had reached (the transport keeps the two apart:offsetis the read position,consumedOffsetthe boundary after the record last handed out), and never at a turn's end: pi writes a call'stoolResultbefore the turn'sturn_end, so an offset taken there already sits past a result the mirror still holds in memory for the next step's user turn, and a bot that died during the model call after it took the result with it — the ledger then showed an assistant turn with a call followed by the next assistant turn, no user turn between them. Whatever pi wrote after the saved boundary died with the bot's memory and is read again. A bot restart hands a pi run off like a native one (run-history.md item 39 reads no harness): pi keeps running in the container, and the ledger's resume plan reaches the harness like any run's. Before any pid is probed, the container this run was handed names itself and the row's word is compared: a row naming another container, or naming a container when the current identity is unavailable, refuses continuation with the original producer and workspace held: no PID probe, kill, removal, replacement or new facts. A legacy row with unbound placement may continue through its existing PID observation when its root, wire and bearer are compatible; that observation does not establish birth identity, descendant exclusion or physical custody acceptance. (The dispatcher's resume never changes backend, run-history.md item 54, so the case is a container replaced under the same thread key: a recycled sandbox, a pid reused.) When the container's pi still lives (its pid answers) and the row names its root, the harness re-attaches at that root: it reads the log again from the recorded offset and feeds the FIFO there, whatever root this build would file a fresh run under, so a build that moves the directory cannot lose the runs in flight across its deploy. The re-read is idempotent. The results and steers the dead generation read but never wrote land as the next step's user turn — a call still running at the death ends on the restart note the relay answers the extension's re-ask with (below), and that note is the result the mirror writes. The last row the dead generation did write — an assistant turn, or the compaction that closed the transcript — read again because the row's offset is saved after the ledger's write, and a death between the two leaves it one row behind, is known to the mirror by sight (ledgerTailOf: the compaction whosebeforeis the message count, else the last message when an assistant's) and spends no second index: the first row the mirror is about to write is compared with it, once (a message with no parts is still no turn). The index a resumed mirror starts at counts every row the transcript holds, compaction rows included, since the ledger writes a step's rows from that index. The answers pi gave the dead generation's own commands are not taken for answers to this generation's: every generation's command ids carry the moment it began and a nonce (prompt:<ms>-<nonce>, so two generations begun in one millisecond share none), and the catch-up ends at this generation's prompt answer and no earlier. A steer's text read again is not mistaken for the seed's echo: only a fresh start's prompt is the seed, and its echo is the first user message after this generation's prompt is answered; a continue prompt's echo — this generation's or a dead one's — is a user turn like a steer's text, since the model was told it. The stream's tool events for a call read again are said again: they narrate, where the ledger's rows are the transcript and are written once. The harness treats a model call that failed and pi's settling on it while the bot was away as facts of the death (aharness_errornote, not a failure — and not a turn: the errored message writes no row and takes no index, so the log stays whole for the next reclaim; session-log.md item 2), and asks pi to continue on the same session with apromptqueued as a steer (streamingBehavior: "steer"): the pi found alive may be inside a tool call — its extension waiting on the relay for the answer the dead generation never sent — and pi refuses a plain prompt while its loop runs (Agent is already processing), a refusal that failed the run and ended pi with the call still open; queued, the continue lands after the call as the next user turn, and an idle pi (a model call failed while the bot was away, the loop ended) takes the same command as the prompt it is. The row cannot tell the two shapes apart — the last step's calls in flight name both, since their results ride the next step's user turn — so pi decides; the calls in flight are settled on the relay before anything is awaited (item 7), so the extension's re-ask reads the restart note and runs nothing. All of this having adopted the bearer that pi holds onto this generation's proxy by the hash its facts carry (bearerHash; model-proxy.md item 2); a row whose facts carry no adoptable hash refuses continuation and retains its original producer, root and workspace, without ending or replacing it. The facts also carry the immutable modelwire: a present wire different from the run provider's wire holds the incompatible original producer without kill, removal or replacement; an absent wire from a release before the field existed is compatible when every other survival fact matches, re-attaches to the live pi, and gains the run wire on the next save; a matching wire re-attaches unchanged. A live pi with no recorded root, an incompatible wire or no adoptable bearer retains original custody and refuses continuation before any kill, removal or new launch. Sampled absence and foreign placement are unconfirmed. On the explicit typed replacement protocol, or a first attachment with no recorded original producer, the harness writes a session file rebuilt from the mirrored transcript (pi's version-3 format, one linear branch, every call in flight at the kill answered with the restart note and never re-run — its effects are the container's) and starts pi on--session <file>under this build's own root. The replacement protocol probes, kills and removes nothing by the old producer's PID or root in the replacement container. The session file's header names the directory this pi runs in as the container answers it for the root just made (HarnessContainer.cwd): the thread's checkout on an execution container, where the executor runs every command; the run's own root on the bot host (item 12). It is never a constant: pi exits at once on a stored working directory that does not exist where it runs, and the bot host has no checkout path and a new root in each generation. The seed session (item 9) names its directory the same way. Supported in-place continuation and explicit replacement are recorded separately; sampled absence or a bot-only restart grants no replacement authority. pi's compaction is a row of that transcript too (session-log.md item 6): acompaction_endwhose result carries the summary hands the bridge the entry — the summary,tokensBefore, pi'sfirstKeptEntryId— and the mirror writes it as its own step, the results pending since the last assistant turn as the user turn they would have been and the entry as the row after them, nothing in flight; the rebuilt session file renders each such row as pi'scompactionentry where it sat, so the restarted pi's window is the summary and the turns after it rather than the raw turns compacted again. A compaction that failed, was aborted or carried no summary writes no row — thecompactedorharness_errornote alone. A pi run handed off after its final answer (the mirror wrote the text-only turn as a step with nothing in flight) is afinishplan (run-history.md item 37): no pi starts. A compatible original producer may be ended through the existing finish path before post-steps run with the answer. A foreign, sampled-absent or unverified original producer retains the workspace and is neither ended nor replaced. The completed saved answer still runs its post-steps; that answer grants no process or workspace retirement authority. A re-attach adopts, a relaunch rotates. A process relaunched under the same run while the bot lives — the survival clause's ceiling over item 16's floor (harness.md item 6; the run loop'sprepareRelaunch,src/core/dispatch/relaunch.ts) — cannot be handed the bearer a live process holds, so the bearer store and the relay give a relaunch two pieces. The run's bearer is rotated on its own meter (model-proxy.md item 2: the new hash on the row before the old are dropped; the turns, the expiry and the span kept), and the relaunched process takes the run's registration over (HarnessRegistry.replace) with the run's relayed calls kept: a call in flight keeps running in the bot under the run's own end signal, every call still running is awaited together up to the relay window (RelayedCalls.awaitInFlight, one window as onePOST /harness/toolwaits), a call that answers inside it is the result the rebuilt session carries, and one still running after it is not lost — it keeps running for an ask by the same call id, and the rebuilt session carries the still-running note in the result's place (stillRunningNote). The earlier registration's forget is a no-op from the replacement on; a run not registered is refused by name. The relaunch on pi. The replaced-container verdict (item 16) carries the record as pi holds it (HarnessRecordonPiContainerReplacedError): the transcript pi's session started on (the seed with its request, or the rebuilt transcript with its settlement turn) followed by the rows the mirror wrote (PiMirror.written, kept wired or not — this generation's copy of the ledger's rows, the same rows a bot death's resume reads back), each compaction where it sat, every call of the last assistant turn settled with the replaced note (replacedCallNote; a result the mirror still held in memory is lost as it is on a bot death), the turn count, the inbox seq and the run's deadline. Anopenhanded that record back as its resume withrelaunchset — the loop's, after the re-attach and the rotation, the row's facts carrying the new hash and the count one higher — locates nothing: the row's pi is known gone with its container, so the pid is neither probed nor ended and the recorded root is not removed, whatever this container answers for its name; the run's relay registration is taken over (HarnessRegistry.replace) instead of made anew; the relayed calls the bot still runs are awaited up to the relay window (awaitInFlight, paced by the harness's own sleep) — an answer inside it, or one that landed before the dead pi could read it (RelayedCalls.answered; never a settlement placed from the record), is the result the rebuilt session carries with the relay's own verdict, a straggler the still-running note — while pi's own calls carry the replaced note; the session file is rebuilt from the record with that settlement turn and pi starts on it under this build's root, its first facts carrying the count the loop wrote; the same settlement turn is primed into the mirror as the results pending for the first step (PiMirror.prime; on every rebuild, a bot death's as much as a relaunch's, never on a re-attach, whose log carries the results), so the ledger's next rows are[user(settlements + the continue's echo), assistant]from the seed index — the ledger, pi's session and the record pi holds (recordBase, the resumed transcript without the settlement turn, which the mirror's rows then carry once) agree, and a rebuild from the ledger at the next death has atool_resultfor everytool_use; the wait on the relay window is one progress line on the card (waiting up to 30 s for N relayed call(s) still running in the bot before pi restarts); and oneresumednote saysrelaunched after the container was replaced (<from> → <to>): the row's pi (pid N) went with the old container and was neither probed nor ended here; pi restarted in the container the run holds on the mirrored transcript — K call(s) were in flight: <answered on the relay / still running there / lost with the container>; M min of budget left. The registration a replaced pi leaves standing is the loop's to forget when it does not relaunch (HarnessRegistry.forget).The seed rule: the session is the thread so far, the prompt is the one new turn. The dispatcher's seed is the thread's history first and the request as the last user turn (
buildMessagesinsrc/core/dispatch/messages.ts, merged into the alternation providers require, so the request is one user turn).splitSeedreads it in that shape: the last user turn is the prompt pi is sent (promptOf: its text parts joined with a blank line into pi's one prompt string — so a request turn built of more than one text part reaches pi whole, the ship pipeline's unit contract among them, which enters the coding child's first user turn as its own text part after the request's text (withContractInFirstUserTurn,src/core/ship/codingChild.ts; agent-ship.md item 13) — its images as pi's image blocks, a document named in the text since pi's prompt carries none), and every turn before it is the session pi starts on — written into the container as a session file by the samepiSessionFilethe way back (item 8) uses, so an earlier assistant turn's tool calls and their results are pi's owntoolCallblocks andtoolResultentries, and an earlier document is named in a text block — and pi starts on--session <file>as a resume does. A seed of one turn — a fresh mention, a coordinator's unit thread whose lead is the bot's — writes no session file and starts pi on the bare session directory, byte for byte as before the rule; the mirror's index still counts from the whole seed, already on the ledger, and noresumednote is said. Without the rule a run in a thread with earlier user turns prompted pi with the thread's oldest turn and never showed it the request. The seed the dispatcher hands over is not always the thread's history: a follow-up whose thread has a session log of its agent starts from the log's tail within the seed budget, the lines a person wrote since the previous run ended and the request (session-log.md item 9) — the same shape, sosplitSeedreads it unchanged: the request is still the last user turn and everything before it the session file, tool calls and results from the log included.The review preset on the harness: the read identity's allowlist, framing and gate, and a verdict that reaches GitHub exactly as the native loop's did. A review runs where it always ran — the resident's read-only worktree attached by identity
read, no write token in the tree, the mirror as its origin — and the harness reads that identity once (AgentDef.identity) for three things. The allowlist: for a bound tracked Review, the runner selectshosted-review: pi's--toolscarriesread,grep,find,lsand the relayed ones; nativebash,editandwriteare absent, with an independent bot-side denial of native bash.run_checkis supplied only with the ready original owner, read policy, typed executor and fenced store capability; an unavailable or cached Review remains held without a shell fallback. Other read-identity runs retain the existing read-tool policy, and coding retains its native bash/edit/write positives. The Review's relayed tools are — thereadonlytoolset less the workspace tools:update_status,submit_verdict,web_fetch,diff_digest, the skill tools, the GitHub reads — so pi never has a write tool to call, before the gate ever sees a call. The framing: the composed system prompt is the dispatcher's — the registry's review prompt (the resident variant, the REVIEW TARGET block, the contract, the skills) asmakeSystemComposerrenders it, never a copy — and the harness note names the actual selected native tools and hosted command route, says the run has noeditand nowriteand is read-only, and mapsread_fileontoreadalone. The gate: the rules the gate judges pi's own tools by carry the run's identity, folded in by the harness beside the thread's branch facts, and the reach is the identity's (reachFor): a write run's is the coding preset's; a run without an identity's is empty (item 12); a read run's holds nowrite-files,github-writeorprbundle, soedit,writeand asubmit_pr_descriptionareoutside-profile; its shell never pushes —git pushin any form is refusedread-only— and never writes to GitHub by CLI or API (gh pr comment|edit|…,gh apiwith a write method or a field, curl atapi.github.comwith a write method or a body), while every read of both stays allowed; a merge or an approval is refused before that as it is for every run. Each refusal is atool_refusednote. The verdict path:submit_verdictis a relayed tool, so pi's call is the native tool's own call (tool.run(input, ctx)in the bot with the run's tool context,onVerdictincluded): the verdict the run holds, the acknowledgement the model reads and the parser's refusal of a malformed one are the tool's own, and everything after the loop is the same code — the reviewed-head settle,runReviewPostStepbehind the reviewed-head guard, theLGTM:first line built by code for an approve andChanges requested:otherwise, a comment and never an approval or a merge, thereview_postedevent or thereview_not_postednote, thereviewPostfact on the record. The head-move re-review is on the harness too: a substantive head move mid-review (item 12 of agent-review.md) uses the existing pi session when the executor advances in place; checkout replacement uses one successor after the original's acknowledged ending, as item 14 describes.A preset without a workspace runs pi as a child of the bot. The general, research and conductor presets run on machine class
none: no container is provisioned and the run's executor is the null one, so there is nothing to exec a container command through. Such a preset runs on the harness all the same. The run loop reads the run's machine class once and, fornone, drives pi through the seam's third implementation,BotHostHarnessContainer(harnessContainerForpicks it; every other class keepsExecHarnessContainerover the run's executor), which spawns the program the start names —pihere — with the start's arguments and filter, and reaches a loopback server a process listens on withfetch(harness.md item 9). Its operations arenode:fsandnode:child_processon the bot host: the run's root made by the container itself (makeRoot:mkdtempbeside the predictable root, on its name as the prefix, exclusive and 700 in one call, with a suffix nobody can guess, recorded on the row's facts by item 8 so the way back finds it), its files created 600 and never replaced (a path already present is refused, never followed or truncated), pi spawned in that directory as its own group leader with--mode rpcand the same arguments, its stdin a pipe from the bot, its stdout through the samemessage_updatefilter into the same log the transport polls, its stderr into its own file, the pid recorded; a kill is TERM to the group, a second's grace, then KILL while the leader still stands (once it has gone the group id could name a reused pid, so nothing more is sent);removetakes the root down as one tree. The two files are the bot host's disk: a write that fails there is handled, never an unhandled stream error in the bot process, and it ends pi, so the harness finds it dead and fails the run with the error log's tail rather than waiting on a log nothing lands in; a pi spawned but not tracked (the pid file could not be written) is ended beforestartfails by name. The child's environment is PATH, HOME and what the launch names (the bearer, the URL, the run id, pi's config directory and the offline switches), and never another variable of the bot process, which holds every secret the deployment has. The bot is what pi reaches:SWITCHBOARD_HARNESS_URLand models.json's provider point at this process's own HTTP server over loopback (http://127.0.0.1:<PORT>,HarnessProcessDeps.loopbackUrl), where the model proxy and the four harness routes already listen, and never atPUBLIC_BASE_URLthrough the shim; a process withoutPORTfails such a run by name, as one withoutPUBLIC_BASE_URLfails a container preset's. No shell and no files: a run whose identity isnoneholds none of pi's own tools:piBuiltinToolsForgives it nothing, so--toolsis the relayed tools alone and pi never has aread,bash,grep,find,ls,editorwriteto call; its reach for pi's own tools is empty (reachFor:NONE_REACH), so a built-in tool such a pi asks the gate for all the same (a pi that grew a tool, a stale allowlist) is refused by name (<tool> is the <bundle> bundle: a run without a workspace (identity none) has none of pi's own tools) with atool_refusednote, before any rule about what the call does is consulted; the harness note in SYSTEM.md says the run has no workspace and none of pi's own tools, names the relayed ones and maps no native name onto one. Every tool the run has is a relayed one (for general, theassistanttoolset'sweb_fetch,update_status, the GitHub reads, issue writes,recalland own-sessionnotes), running in the bot as the requesting user under the bot's own gates, as on every run (item 7). The bridge, the mirror, the wind-downs and the record are unchanged: a general run replays like any run's. A bot-generation restart retains unverified original custody. A bot-host pi belongs to oneBotHostHarnessContainer's process registry and stdin pipe. A new generation cannot observe or reach that predecessor through its new registry:aliveanswers false for a PID it did not start, and the host names no container identity. With recorded original facts and no explicit replacement protocol, that sampled absence raises typed unconfirmed custody; the harness retains the original producer, root, session and workspace custody without kill, removal, replacement launch or new facts. It does not automatically rebuild pi from the transcript. Stdin EOF may cause the predecessor to exit, but that expectation is not observed termination, descendant exclusion or replacement authority. The current ordinary bot-generation continuation path is held; a supported restart remains a gap. A fresh run's session uses the root made by this generation as its working directory (cwd), never a nonexistent checkout. The existing host-cwd fixture suppliesresume.relaunchexplicitly and proves that replacement transcript's rendering and retained prior root; it does not prove ordinary bot-generation resume or native restoration. A bot-host command failure supplies no executor verdict that the runtime was replaced: the host names no container and itsnode:fsandnode:child_processoperations carry no such word. Mid-run sampled process loss therefore remains the named same-container failure of item 16, without an inferred replacement. The bot image carries pi at the pin for this (item 3). Research and the conductor take the same path: the container asked for by thenoneclass, the loopback URL, none of pi's own tools, the preset's toolset relayed and nothing else: for research thewebtoolset'ssubmit_research,web_fetch,web_search,update_status, the GitHub reads,recalland own-sessionnotes, each the native tool run in the bot under its own gates (aweb_fetchof an internal address is refused by the tool's URL guard without a fetch, and that refusal is what pi reads); for the conductor theconductortoolset'sspawn_run,send_to_run,await_runs,list_runsandget_run_statusbesideweb_fetch,update_status, the GitHub reads,recalland own-sessionnotes, its waits carried by the relay's pending answer (item 7) and its spawn seeding the child from the run's session log (agent-conductor.md item 3).A verdict names its head because the served definition requires it; the relay neither refuses nor supplies one. The
submit_verdicta review's pi is served is the native tool's definition field for field — the description that says to rungit rev-parse HEADin the checkout reviewed and pass its output, the schema whoserequirednamesheadbesideverdictandsummary— and pi validates every call against the schema it was served before the extension runs it, so a verdict naming no head is a schema error the model reads and answers, and never reaches the relay. The relay adds nothing to a verdict and removes nothing from it: a relayed call is the native tool's own (item 10), so a verdict without a head is accepted exactly as the native call accepts it — the run'sonVerdictreceives what the parser yields, head absent — and the reviewed-head guard decides afterwards. The relay never stamps the run's pinned head into a verdict: the settle reads the worktree's ownHEADfirst (observed), and the model'sheadis the guard's fallback for a workspace whoseHEADcould not be read — a head written from the run's expectation would pass exactly the check that exists to catch a review of another commit. A driver with no bot that serves an extension of its own registers the same two definitions verbatim (load-harness.md item 14); one that serves asubmit_verdictwithoutheadgets verdicts naming none, since a model follows the schema it is given.pi’s model library is the bot’s provider layer for structured calls outside a run loop. The one door, intake and memory reflection use
PiAiProvidersand the sharedproviderStructuredModelseam. The table is built from config provider blocks;defaults.models.general,intake.modelandmemory.modelresolve through it. The door’s no-call repair stays on the same operator model; a second no-call ends with anon_decisiondoor record without a classifier fallback or a run bind. Each request keeps the completion vocabulary, provider-specific forced-tool spelling, cache controls, abort signal and usage conversion. Before calling a direct pi API, the adapter normalizes the context: the system prompt and shaped tool declarations travel in a leading system message, not as top-level fields. Replayed tool arguments are JSON objects, serialized without mutating the caller's input. These calls do not publish a run’smodel.turnspan or usage record; the run’s own pi still reaches its model through the proxy.The session outlives the loop for post-turns; checkout replacement first ends its producer. Three turns can run after a loop returns and before the answer lands — the coding preset's description turn (pr-description.md item 5), the review's verdict turn (agent-review.md item 5) and the review's head-move re-review (agent-review.md item 12) — each continues the run's saved conversation. In-place movement uses one more
prompton the existing session. Checkout replacement acknowledges the completed transcript and original ending, advances the checkout, then launches one successor with the same permissions, original deadline and persisted post-turn clock. The run loop takes the harness's open form,runPiHarnessOpen, which hands the loop's answer back with pi alive and idle on its session — the transport still polling, the run still registered on the relay, the root in place — together with a follow-up entry (FollowUpTurn) and the session's end — the contract'sHarnessSession(harness.md item 7);runPiHarness, the closed form, is that loop with the end at once. The loop hands the entry torunDescriptionTurn,runVerdictTurnandsettleReviewedHeadasfollowUpon their turn specs, and ends pi after each has had its say — before the coding post-step and before the workspace pi runs in is released; on a throw, before the release too. A follow-up turn sends the caller's user turn —descriptionFollowUp,verdictFollowUporrereviewFollowUp— as apromptunder a command id of its own, reads pi's records off the same transport through the same bridge, and answers with pi's reply once it settles, labelled as the loop labels a write-up. The relayed tools read the turn's tool context for its duration — the registry entry'stoolContextis the turn's, then the run's again — so the hook the caller overrides (the description's capture, the verdict turn's and the re-review's own verdict capture) is the one fed, andremainingMsis the turn's. Its budget is carved from the run's lease (decision 0046): the lesser of its ask — the description turn's, the coding post-step's 5 minutes, the verdict turn's, the review's 3 (postStepLease, each read off what the session says the lease still holds,HarnessSession.remainingMs), or the re-review turn's preset ask — and the lease's remainder, never under a minute (turnLeaseMs; the bearer's grace covers that floor); the turn holds nothing back for a write-up of its own, its deliverable being a tool call, so its loop ends at its deadline (loopClock(…, "turn")); the tools the turn may call (FollowUpTurnInput.tools: the description turn's read-and-submit set, the verdict turn's) are marked on the run's bearer entry for the turn's duration, so the proxy trims each request's tool list to them and lifts the checkpoint'snone(model-proxy.md item 6); past either, the same write-up steer with every tool refused (toolsBlocked), the sametime_budget_exhaustedorturn_budget_exhaustednote and the same label on the answer; the loop's stops with them — a soft stop steers the soft-stop write-up with every tool refused, saysstopped(soft) and labels the answer ⏹, exactly as the loop does, a hard stop isabort,HARD_STOP_MESSAGEand thestoppednote, and the finale bound closes a write-up that overruns. Its spans: arun.agentopened under the caller's span (run.description_turn,run.verdict_turn,run.settle_reviewed_head), the proxy's turns re-parented to it and the bridge'stool.<name>spans under it, ended when the turn ends —errorwhen the turn threw, was hard-stopped or bypassed the gate,okotherwise, and the loop's ownrun.agentlikewise endserrorwhen the loop threw; the loop'srun.agentends when the loop settles, before any follow-up. Its messages and compactions are mirrored with the initial loop's durable indices. The saved offset advances only after the corresponding write, so a restart preserves a completed re-review or description instead of recovering its superseded answer; the inbox is not drained during a follow-up turn and no compaction steer follows one. Failure handling is the caller's: a prompt pi refuses, a model call that fails, a pi that exits mid-turn or a gate bypass (itsharness_errornote andabortas in the loop) throws — the description turn logs it and reports nothing submitted, the re-review fails the run; a follow-up asked of a session already ended throws naming it and sends nothing. pi's system prompt is the session's — pi readsSYSTEM.mdonce, at load, and its RPC has no command to change it — so the re-review does not recompose the REVIEW TARGET block: the follow-up text alone names the new head, the moved worktree and the freshsubmit_verdictit wants. Afinishplan has no session (item 8: the previous generation's loop had answered and its pi is ended here), so a post-turn a resumed run still owes is not run, and neither caller starts a second pi (a fresh session would carry none of the run's context, and the record already says what the run did). The description turn publishes itsdescription_turnnote saying there is no session to ask on — the loop answered before a restart — and submits nothing, so the post-step's note says the description was not resubmitted (pr-description.md item 5's warning); a substantive head move the settle finds is not re-reviewed: no worktree move, ahead_movednote saying the session is gone and that the verdict stands for the head it reviewed, and the post gate pins it there (agent-review.md item 10).Budgets: the wall clock is the budget, the turn cap the runaway guard derived from it. Each preset's
maxMinutesis its ask, a row ofsrc/core/budgets.tsthe registry reads (loopBudget), the hard deadline the harness keeps (item 6); the module also holds every round's floor (the least lease in which it does useful work), the allowances a lease holds back (provisioning, the write-up, the post-step turn, the command reserve, the exec margin, the bearer's grace), the lease minimum derived from them (leaseMinimum: the write-up, the post-step and one turn — the least lease the profile gate admits, routing-and-config.md item 2) andcarve, the one computation a round's minutes come from — the parent's remainder minus a reserve derived over the rounds that must follow — which the ship coordinator, the config validator and the fork adopt in the plan's unit two; until then the coordinator carves with its own reserves (agent-ship.md item 8) and only the verify-time check reads the module's (decision 0046; the ship loop's use of it is agent-ship.md item 8). ItsmaxTurnsis not set by hand: every preset that runs the loop derives it asrunawayTurnCap(maxMinutes)=maxMinutes × RUNAWAY_TURNS_PER_MINUTE, withRUNAWAY_TURNS_PER_MINUTE = 6(src/core/budgets.ts) — a model turn every ten seconds, sustained for the whole budget, is a loop, not work (a busy run takes 20–40 s a turn), so the cap is never a number a working run reaches, and a run that reaches it first was pacing like a loop. Derived caps: coding 540 in 90 min, review 150 in 25, research 48 in 8, general 360 in 60, explore 720 in 120, conductor 8640 in 1440, orchestrator 60 in 10;shipkeepsmaxTurns: 1because its def never runs the loop. When a boundary or abudget:directive clips a run's minutes below the preset's (routing-and-config.md item 2),budgetedAgentre-derivesmaxTurnsfrom the clipped minutes with the same helper, so the six-per-minute guard holds for the budget the run actually has (coding clipped to 10 minutes runs with 60 turns). The guard is spent where the calls are made — the model proxy counts a turn before it forwards the call and refuses the one past the cap (model-proxy.md item 5) — and the harness reads the same count off pi's turns (item 6); anupdate_status-only turn does not count against it (item 5's bookkeeping rule). The lease's clocks (loopClock,src/core/budgets.ts): the lease ends at itsdeadline; the LOOP ends atloopEnd, the write-up allowance and the preset's post-step allowance (postStepMinutes: coding 5, review 3, none for the rest) earlier, so the write-up and the post-step run inside the lease — a coding lease of 90 runs its loop for 82; a lease shorter than its hold-back has no loop time and writes up at once. The harness publishes aleaseevent when it first starts the loop (the start, the end, the loop's cut; run-history.md item 2) and moves the run bearer's expiry to the lease's end plus the grace (model-proxy.md item 2); a resume continues the lease the record holds and publishes no second event. A command that could never finish is refused, not cut: pi's bashtimeoutis optional seconds with no default, so a call naming none runs until it exits or the loop's end cuts it (item 6, decision 0046 unit seven); a call that names one reaching past the loop's end is refused at the gate before it runs (item 7,commandPastLoopEndRefusal) — the seconds left to the LOOP's end, not the lease's — so the minutes a full verification would have spent before the cut are the model's to use, and the coding contract asks for a timeout on any command expected to run longer than a minute (agent-coding.md item 13). OpenCode's gate cannot: its permission ask carries the command alone, so the conformance table declares the rowcannotthere and the loop's end cuts as today. A timeout the call names is final, whatever the command does with its pipes: pi kills the command's process group at the deadline, but its post-exit wait re-arms on every output chunk, so a descendant outside the group that kept the tool's pipe open and kept writing held a call open far past its bound while the card only showed a growing count. The harness closes that hole in pi's settings (item 4):shellCommandPrefix(PI_SHELL_COMMAND_PREFIX, process.ts) routes every bash command's stdout and stderr through one forwarder inside the group (exec > >(exec cat) 2>&1— bash process substitution, and pi's shell is bash by name), so descendants inherit the pipe into the forwarder and never the tool's own, the deadline's group kill closes the tool's read side, and the call returns pi's own timeout result (Command timed out after <N> seconds) with the output captured; a backgrounded subprocess holding the pipe extends nothing, and a command that exits in time is unchanged in output and exit code. Wrap-up warning: once, as the loop's end nears (≤ 3 min or 25 % of the loop left,WRAP_UP_WARNING), the model is steered how long the loop has left and to consolidate rather than explore (wrapUpInstruction, awrap_upnote). Forced write-up: at the loop's end or the turn guard the model gets one more turn with every tool refused (item 7) to report findings so far and what a follow-up should do, told which of the two ended the run (timeBudgetInstruction,turnGuardInstructionwith the pace). The answer is prefixed⚠️ Hit the <N>-minute budget before finishing — findings so far:at the deadline, and⚠️ Stopped after <N> model turns in <M> minutes — that pace looks like a loop; findings so far:when the guard fires — the turns counted and the minutes elapsed, never the word budget, because the cap is not one (timeBudgetAnswer,turnGuardAnswer); an empty write-up still produces a user-facing message that says the same reason and what the ending established (item 6: the pushed head, the salvage, the work left and its fate — never a guess at a tree the salvage measured); therun_notekinds aretime_budget_exhaustedandturn_budget_exhausted(the friction analyzer and the model proxy share them); the write-up is bounded by its allowance (loopClock.finaleMs, thewriteUpallowance of three minutes). Thetime_budget_exhaustednote says the loop's time is up and what the run was doing — the open tool calls by name, or that a model call was in flight when pi had opened a turn (turn_start) it had not yet answered, else the budget alone (timeBudgetNote(doing),PiBridge.doingNow, the wordsMODEL_CALL_IN_FLIGHTshared with OpenCode's bridge) — so the card explains the ending and never claims a call that is not there. The wind-down owns the ending: a model call that fails once the run is winding down — the finale bound's ownabortof a call in flight included — never turns the run into a failed model call; the failure goes on the record as aharness_errornote and the run (or a follow-up turn, item 14) still closes with the wind-down's own answer, so a budgeted run always ends by its budget's words, never bythe model call failed: This operation was aborted; when that failure is why no write-up came, the answer's reason-alone line names it (timeBudgetAnswer,turnGuardAnswerandsoftStopAnswertake thewriteUpFailedclause; the note iswindDownFailureNote), so the thread reads what failed without the run ending as a failure — and an operator's stop still wins: a soft-stopped run whose in-flight call then fails answers under the ⏹ label naming the failure, its unread follow-ups dropped as after any stop. The rule is the contract's, not pi's: harness.md item 5 holds the harness-neutral row. A resumed run whose loop had ended under one of these before the restart wears the same label, read back from its notes (answerUnderEndingin the run loop; run-history.md item 37). Current wall clocks: general 60 min · research 8 · orchestrator 10 · review 25 · coding 90 · explore 120 · conductor 1440 · ship 1440 (the pipeline's, agent-ship.md); each turn guard is six times the minutes. Coding's 90 is sized on the ledger's ship children, not its standalone runs: children clipped to 45 were cut one run in twelve while running the repo's gate after their push, so the ask is double their 90th percentile with the gate inside it; the findings round's floor of 15 is the least that runs that gate; ship's 1440 holds 48 rounds at their floors. Changing a wall clock is a feature change — update this item and the registry together; the guard follows.A container replaced under a live run is the harness's typed verdict: under a living bot the run loop relaunches pi in the replacement (the ceiling), and an ordinary run ends
interruptedwith its request run again when the relaunch is refused by name or bounded; a stamped pilot writer instead pauses its original run and workspace for recovery — the floor of record 0038's survival clause. A FIFOsendthat reaches its command timeout before pi produces its first line is instead a named start failure: once that one write settles the run ends immediately, without the replacement probe, restore-window wait or relaunch path. The resident's rolling-container pause (resident-repos.md item 65) replaces the container under a live run — every image-changing release does it twice, at our deploy's restart and at the platform's own roll a few minutes later — and the harness drives pi inside that container, so pi and the tool it was running are gone with the old container's disk while the run's record is whole on the ledger. The condition is the executor's word, on any container command under the loop — the log read, thealiveprobe, a line into the FIFO (a failed write surfaces at the next read, the transport's order): the command failed with the executors' word for a replaced runtime (saysContainerReplaced), typed first: the resident client'sExecSandboxRestartedError, its one word for every answer that says the container under the thread is gone — the container exited inside a rollout and the executor re-attached to the replacement before the command ran (resident-repos.md item 65); a deploy swapped the runtime under the command in flight (runtime-replaced, item 43); the preflight found the container disk recycled since the last attach (worktree-missing, item 27) — the last two thrown at once, before any recovery, because pi's own tools run inside the container and never through the bot, so the roll usually lands between two of the harness's polls and reaches the seam only as the next poll'sworktree-missing, and because the recovery the client used to make first — a re-attach that blocks through the restore, or is refusedimage-stale/not-serviceablewhile the replacement reconciles — stood in for the verdict on 1.230.0 (run5b7e3b50, the floor's failed live receipt: the harness readpi exited before the run settledor the attach's refusal, said nothing, and the run was not dispatched again); and the seam'sHarnessContainerRuntimeReplacedError, an executor that handed the word back as a command's text (stdoutOfraises it rather than letaliveread it asdeador the log read decode it as bytes). Then the words themselves,runtime-replacedorruntime-unreachable(the sandbox's word for a runtime nothing answered, execution.md item 9), anywhere in a failure's text — behind the executors'exit N:prefix and newline, the seam's wrap, or the resident client's own sentence — since the seam's commands print a pid, a boot id,alive/dead, base64 or nothing and never the word, so a failure carrying it is the executor's; a failure without any of that is the failure it was. The container's name is corroboration while the word is there to be had: the container this run was handed names itself again (identity, item 4) and both words go on the note beside the recorded one (facts.container, item 8) — the word is the kernel's boot id, which a container replaced on the same kernel keeps, so a match proves nothing; a change decides only where no word can come (the one more command, below). Then, in order: every call still open on the bridge is settled on the record with the restart note said of the container (replacedCallNote: the tool ran in the container and its result died with it, so it is never re-run from here; itstool.<name>span ends in error), arun_noteof kindsandbox_restartedsaysthe container running pi was replaced (<old identity> → <new identity>; the executor said: <its words>), and the loop throwsPiContainerReplacedError(said,was,now, andrecord, the record as pi holds it, item 8) with the run left registered on the relay for the relaunch to take over. Nothing is killed and nothing removed: the executor reaches the replacement now, where a pid is a stranger's and the run's root never was, whatever the container answers for its name. The run loop reads the error by type (HarnessContainerReplacedError, harness.md item 6) and, the bot being alive, relaunches pi from the record in the container the run holds (prepareRelaunch: the bound read off the row'srelaunches, the workspace re-attached or refused by name, the bearer rotated with the row written inside the rotation, then pi'sopenwith the record, item 8), and the run goes on as the one run it was. The run's hard stop rides into the relaunch's re-attach (RelaunchContext.stopSignal): a stop while the replacement is being re-attached — a selection probe or an attach waited through a blip the Worker typed transient (execution.md item 9) — ends the wait at once and the run ends as a hard-stopped run does, the stop's answer andstopped_hard, astoppednote (mode: "hard") saying pi was not relaunched — never a secondsandbox_restarted, which every reader counts as a replaced container's verdict — nothing rotated, nothing relaunched, nothing restarted from the request. For an ordinary run, the floor is what a refused relaunch leaves — the ceiling of two reached, the worktree refused by name, the bearer not rotatable, a row without facts: the loop forgets the run on the relay, publishes a secondsandbox_restartednote saying why after pi's verdict (starting at the why — the verdict is the first note's — and neverresumed, since the run is not), and finishes that runinterrupted— the workspace is released, the card closes🔁 <label> · <the refusal's reason> · <elapsed>(relaunch ceiling: 2 relaunches already; restarting from the request;workspace lost with the replaced container; restarting from the request) and never❌, and the dispatcher, in place of the failure reply, hands the request to the restart a refused workspace re-attach takes (run-history.md item 54:prepareRestartTurn, the follow-ups the run never consumed appended, dispatched again under the same run id once the thread is free — run-history item 54: the restart keeps the run's identity); the request's own outcome is the refusal's shape,container_replacedfor the ceiling andworkspace_lostfor the worktree, as item 54's. A container replaced with the run inside its write-up reserve is neither: the re-attach is not opened, or the lease ran out under its waits (lease_spent, execution.md item 9), nothing is rotated or relaunched, and the run ends on its budget — thetime_budget_exhaustednote carrying the bound's own sentence (how much of the lease was left and which bound refused — the reserve, or the attach floor past it) andno write-up ran, the budget's plain answer, the statuscompleted, and none of the tail against the replaced container (a coding run's observation, salvage, description turn, work-left-behind note and PR post-step; a review's head settle, verdict turn and review post — nothing drives the replaced container's executor, and the budget's answer is never posted to the pull request as a verdict) — neverworkspace_lostand never a new run from its request with a fresh lease. A bot that died with the container is not this item's: the next generation resumes the run through the ledger (item 8). A pi found dead without the executor's word takes one more command before the judgement (harness.md item 6;replacedVerdicton the container seam): the platform's rollout kills the container's processes first whileexecstill answers, so thealiveprobe finds pi gone before any command could fail with the word — the loop's transport ends unsettled with nothing said. Oneidentitycommand then: failing with the word, it is the executor's word and the verdict above follows unchanged (saidthe word); answering another word thanfacts.container, the changed identity is the condition — a renamed container with a dead pi is a replaced one — and the loop throwsPiContainerReplacedErrorwithsaidnothing andconditiontaggedidentity(wordon the verdict above — the tag is what a reader compares, andsaidis present exactly when it isword), the note readingthe container running pi was replaced (<was> → <now>; the changed identity was the condition: …), the call in flight settled and nothing killed or removed as above. Only past both did pi die where it ran: the failure it always was —pi exited before the run settledwith the error log's tail, pi ended and its root removed — with nothing named by the container, since a name that matched, or was missing on either side, judged nothing. The ceiling above this floor: the relaunch of pi in the replacement container on the mirrored transcript with a rotated bearer keeps the run and costs it at most one model call and never a tool's effects (harness.md item 6; record 0038's survival clause) — this item is the verdict beneath it and the settle-and-close the record names, taken when the relaunch is refused by name, and an ordinary run that meets a roll starts its request over only then. A container command that fails on its transport with no word takes the same one more command (harness.md item 6, the third failure shape;saysTransportLoston the container seam): the platform's replacement kills the container under pi's command and the read's WebSocket closes before any word — the resident client'sresident /exec: Peer closed WebSocket: 1006 WebSocket disconnected without sending Close frame., the binding'sThe container is not running, consider calling start(), a connection reset — the words name the shape, never the executor's infra type alone, so an infra failure that says nothing of the container (the Worker unreachable, an attach refused) stays the plain failure it was — so the loop, which used to throw that as the run's plain failure, ends for the judgement instead: the word on the one moreidentitycommand is the verdict above unchanged (saidthe word), another identity thanfacts.containeris the verdict by the changed identity, and the recorded identity triggers the PID probe, not a replacement guess: a live PID resumes from the last consumed log record; a proven dead PID leaves the transport error standing with aharness_errornote and nosandbox_restarted, and an unknown PID fails by name on either backend. A resident binding does not prove replacement: a live recorded PID re-attaches from the consumed output, while a proven dead PID with the same identity fails by name on either backend, rather than launching another process. An ambiguous liveness probe fails explicitly; only the executor's replacement word or a changed identity can authorize relaunch. The one more command waits through the restore window: the resident rebuilt a replaced container in about a minute, and until thenidentityitself meetsThe container is not running—HarnessContainerDownError, a container down under the question with no word — whichreplacedVerdictre-sends after the executor's backoff (PROBE_WAIT_BACKOFF_MS, the start gate's ownSANDBOX_START_BACKOFF_MS: 5 s, then 10 s, then 15 s between every later re-send) withdeps.sleepuntil the container answers or the executor's start bound runs out (PROBE_WAIT_MAX_MS, five minutes — the seam's own bound, shorter than the start gate's ten-minuteSANDBOX_START_WAIT_MAX_MSfor a fresh run's first container, since here a run's work is in flight; the backoff is the start gate's; execution.md item 23), twoharness_errornotes saying the wait began and how long the container took to answer; only a container that answers, or a wait that runs out, decides. The wait is the run's:run.control.hardSignalends it at once and the run ends as the hard stop (the abort line, onestoppednote in mode hard, nothing judged), and the lease'sdeadlineends it before a re-send that would pass it; the bound is read on the harness's clock, so a probe that hangs to its timeout is counted. The wind-down owns the ending (item 15): the word or a transport loss met while the finale was being aborted is said in aharness_errornote and never judged — no probe, no verdict, no thrown transport error — and the budget answer stands. A follow-up turn (item 14) meets the three shapes as the loop does: the word on the turn's read is the verdict, thrown from the turn with the record for the caller to read by type; a transport loss takes the same one more command with the same wait, and a hard stop that ends that wait ends the turn as the stop (the abort line as the turn's answer, onestoppednote, never the transport error) — the HARD stop alone is read after the wait, as the loop reads it, and it aborts pi's turn in flight through the ONE hard-stop sequence every tick and every end of a wait runs (hardStop: the loop'scheck, a turn'sturnCheck, the read after the loop's wait and after a turn's — the flag once, one abort): the one more command may have found the container alive with pi mid-turn; the abort is delivered now, on a tick as at the end of a wait — through the gate's exit that never holds it (PASSES_HOLD; the tick that sends it also breaks the loop on the flag, so a held abort would never be released) onto the transport's own step (write's abort step, below) — and never left for a re-attach to replay: a soft stop or the turn's deadline landing during the wait neither notes a stop nor steers a write-up into the transport already known lost, and the turn fails with the transport error named; the recorded identity leaves the turn failing with the transport error named and the note; a turn that meets none of them fails as it always did, a control file lost among them noted exactly once. The word does not outrank a live pi, and a control reset is not the word. The executor'sruntime-replacedis the resident's word for a container gone; the resident's own Durable Object resetting under a live pi — a Worker-code deploy superseding this DO's isolate,isDurableObjectCodeUpdateReset— is a distinct word,control-reset, the container and every process the run holds in it exactly as they were and the command's outcome unknown (resident-repos.md item 43); it is neverswapIncarnationand never the replaced verdict, so a DO reset over a live pi never orphans it by relaunching a second pi in the same container (the incident this item names). The seam re-sends an idempotent op once on a control reset (ExecHarnessContainer.execIdempotent: read the log, alive, identity, tail, a GET request — the container is unchanged, a re-read is safe) and never a write (writeLine, writeFile, start, a POST — a duplicated prompt or steer is a corruption), raising the write it did not re-send as the seam's ownHarnessContainerControlResetError; the loop, meeting a control reset, re-attaches in place — a fresh transport from the last record boundary,relaunchesuntouched — and resolves the write whose outcome is unknown by pi's echo (resolveControlResetWrite, ONE rule the control-reset word and the refuted replaced word both take, in the loop and in every follow-up turn alike —recoverInPlace): a command whose id pi has ALREADY echoed landed and needs nothing — the loop records every echoed id at the consume (noteEcho), since the transport surfaces a send failure only at the top of its next read iteration, after the chunk holding the echo was handed out; apromptwith an id awaits that id's echo and is re-sent steer-delivered only if the echo does not come withinPROMPT_ECHO_WAIT_MSon the harness's clock, read on every iteration of the loop on which the feed is QUIET — the transport has handed out every record it read and its last read was short (PiRpcTransport.caughtUp,HeldSends.quiet), the awaited head's clock starting at the first such read after it became the head — never a count of ticks or events (a streaming pi that answers every poll starves the loop's tick, and each of its records leaves the reader caught up, so a held prompt's clock runs under it) and never while a catch-up burst is still being handed out (the records pi wrote during the reset window, read in one chunk and consumed one ledger write at a time, are no time under any clock: the echo late in that burst lands the prompt before the bound is judged) — pi takes a steer-delivered prompt mid-turn, so a prompt that did land is never delivered twice, and every prompt the loop sends carries an id (the seed's, a turn's, the provider retry's<seed id>:retry), an id-less prompt failing the run by name since its echo could not be told from any other; asteerleft to its echo and the loop's requeue; an id-carrying control command and the gate reply (extension_ui_response) re-sent as they were, since a duplicate response for a settled id pi ignores (RESEND_AS_IS); anabortneeding nothing, since it is never the write in doubt — written as the transport's own step (write's abort step), its failed write neverpendingSendand its sender's to ask again (below); and only a write with no echo to resolve its outcome by fails the run by name. A failed send stops the transport's chain: the writes queued behind it, and any sent after it, never land on the old transport (a steer must not land ahead of the prompt's re-send) and are never lost. The re-attach first waits for every write in flight to settle (flushed(): the poll's read and a write are two commands to the same Durable Object, one reset fails both a few ms apart with the read's error first, and until the write settles it is neitherpendingSendnor queued — so a prompt in flight is never abandoned unresolved; the wait is bounded by the write's own command timeout and observes the run as the one more command's wait does, with the same pause (sleepUnlessStopped): the run's hard-stop signal is raced, so a stop ends the wait the moment it fires, the run ending as the stop with pi aborted, and the end the wait was handed fails it by name — the lease's end from the loop, whose write-up runs inside the lease, and a turn's END from a follow-up turn, its deadline plus its finale allowance, since a turn's write-up runs past its deadline — judged on the clock only after the write was raced once, so a reset met with the deadline already past and the write settling a moment later still re-attaches), then resolvespendingSend, then takes the never-landed writes (PiRpcTransport.takeUnsent,reattachTransport) and sends them on the fresh transport in the loop's order behind the resolved write — a gate reply among them, the one the reset left unresolved or one queued behind the failed write, passing the hold as the gate'ssendlets every reply (below), since held behind the prompt in doubt it would stall the very tool call pi waits on. Every write to pi takes ONE gate (HeldSends), the seed's included — the source scan holds the file's one directtransport.sendto the gate's own deliver: while a prompt in doubt awaits its echo, every later write of turn content — a follow-up's steer, the wrap-up, a turn's prompt — is held behind it in the order it was sent, and a second prompt in doubt is appended behind the first, never overwriting it, its own wait starting the moment it becomes the head (at the first's echo or the first's bound alike); the gate moves on by the echo (landed, nothing to send) or the bound on the clock (re-sent steer-delivered once) — the clock read on every iteration AFTER the event just consumed is observed, so an echo in hand lands its prompt before the bound is judged — so pi sees the order the loop sent. The gate has exactly two exits, and they differ — each a mechanism where the write is made, never call-site discipline. A gate reply (extension_ui_response) passes the HOLD but not the chain, by its type insideHeldSends.senditself: it answers an ask pi already made, so held behind a prompt in doubt it would stall the very tool call pi waits on, yet it is delivered through the transport's chain like every write, so one lost to a failing write ispendingSendand re-sent as it was, and one queued on a spent chain is the re-attach's to take — never written past the chain, never lost, and never held on the re-send either, since the re-send takes the samesend. An abort never waits at the gate either — the tick that sends it also breaks the loop on the flag, so a held abort would never be released (the drop at the loop's end would discard it and pi would run on unwatched), and a streaming pi starves the tick besides — soHeldSends.sendholds no abort, by its type as it holds no reply (PASSES_HOLD: the two exits, both in the gate's ownsend), and the transport is never a second writer either: the abort is ONE step on the transport's chain (write's own abort step — every abort the loop sends, the hard stop's included, goes this one way) that a spent chain does not stop. Nothing in flight (an idle chain, or one a failure already spent) and the step runs at once — that is the abort's own step — so a hard stop, the finale's bound or a gate bypass reaches pi whatever the gate holds and whatever the chain's state (the reset that fails a prompt a moment after the abort was asked for cannot swallow the stop); a write in flight or queued on a live chain, and the abort follows it, so pi has the prompt first and the abort after, never an idle session aborted and then handed the prompt to run unwatched. No two writes to the FIFO are ever in flight at once: a line is a separate exec into the FIFO and a line over PIPE_BUF is severalwrite(2)s, so a second write beside an in-flight one could land inside pi's line and cost both (the fake container's two-phaseslowWritesis how a test sees an overlap the real pipe would). The step skips only on a transport abandoned to a re-attach, whose fresh transport is the one writer then, and is never queued for a re-attach to replay (takeUnsentnever holds one). The finale's clock starts when the wrap-up steer has LANDED — the transport's write settled as landed (PiRpcTransport.writeanswers aLanding:landed,failed,heldfor a write kept intakeUnsent,droppedfor one nothing kept — a write to a closed transport, an abort's step on an abandoned one), which the gate'sonLandedwaits on — never when the loop asked for it and never at the hand-off to a chain that only holds it for the re-attach: a steer held behind a prompt in doubt, or left intakeUnsentfor the fresh transport, cannot arrive with its allowance already spent, the callback riding the very object through the re-send (a bound re-send's steer-delivered copy included), and adroppedwrite's callback told so, since no re-send will come: a steer is dropped only after the run has ended (end()closed the transport), so its sender ends nothing further, and an abort whose write FAILED (chained behind a steer the same reset failed, or alone on a live chain) is answeredfailedwith nothing kept — neverpendingSend, neverqueued, which is one-to-one with the chain's steps; the transport re-sends no stop — owed: the loop's next tick asks again throughabortPi(idempotent, past the gate's hold, its own landing heard; any stop asked for while one is owed is the re-ask, a hard stop's included — the tick reads the hard stop before the debt, so a tick that stops the run sends one write, not two), on every tick until the stop lands and with no cap on the re-asks (a cap could only lose the stop, never land it sooner; the loop's life — its deadline, the lease, a hard stop, pi settling — bounds them), and the record gets two lines for the series, never one per tick:abortWriteFailedNoteonce at the first failure, naming the run or the turn that owes the stop as its sibling notes do, the re-asks that fail after it writing nothing, andabortReaskedNoteonce with the count — astop_landednote when a stop lands (any sender's: pi has it, and the debt is settled), aharness_errorwhen the loop or turn ends with the stop still unheard and none in flight; on the loop's clock rather than from the landing itself — a re-ask fired there would run inside the recovery's wait when the abort failed chained, the transport about to be abandoned under it, and would retry at the seam's speed while the reset window still fails writes — so the stop reaches pi whether the read survived the reset (alone: no re-attach comes, and the re-ask lands on the same live chain, an abort's failure spending nothing) or failed with it (chained: the re-ask goes out after the re-attach, on the fresh transport, ahead of the prompt in doubt); the debt is the loop's — the series is an object per loop or turn (StopSeries: owed, re-asks, stops in flight, live) that the abort's callback closes over, so a landing after the loop ended settles the loop's series and never a later turn's, and no stale stop reaches a follow-up turn; the series is ended once, at the first drop — a turn'sfinallyafter its loop, or the loop's catch after its drop, drops again and says nothing more — and the gate keeps a callback its sender marked as outliving the drop (send(command, onLanded, { outlivesDrop }): the sender states the lifetime, the gate keeps one rule; the harness marks its abort's, for the record — a swallowed stop is seen, never assumed), so a stop sent while the loop was live whose write fails after the loop ended but before the session's end — the hard stop's or a gate bypass's, which the same tick sends and breaks on — is oneharness_errorline (abortFailedAfterEndNote, the series' closing line with its count of re-asks when it was the last re-ask still in flight at the settle) and owed by nobody, and one that lands closes the series (stop_landed, the count on it); the session's end (end()) races the transport'sflushed()— every unsettled write is on the current transport, a re-attach having awaited the old one's chain before abandoning it — against its own kill, never a timer, and speaks for every series with a stop still out (the harness keeps that set; the loop's and each follow-up turn's, since a turn's series does not retire the loop's); the kill is bounded by the executor's command timeout (OP_TIMEOUT_MS), and on the bot host by TERM, the grace, KILL — and when the kill completes first writes the record's last word on each BEFORE the run loop marks the run finished (abortUnheardAtEndNote: the stop's landing is unheard, the kill ended pi), since the registry drops content on a finished run and a line from the landing itself would depend on that race; on a container replaced under the run no kill runs and the run loop relaunches pi, so the line says the container was replaced with the stop still out and the relaunch carries on; a landing after the end is not recorded (the recovery's deadline abort on a run its throw ends — the loop's catch drops as a turn'sfinallydoes — is that shape: the end says it was in flight, its later failure adds nothing); in one rule, a stop that failed after its owner ended is said once, by whichever end is still alive to say it — a turn's by the landing (abortFailedAfterEndNote, once per series), the run's by the landing if it beats the kill, else by the session's end (abortUnheardAtEndNote, which speaks the same way for a turn's stop still out as the session ends) — never by both: the landing strikes its series from the stops out before it looks, says nothing once the end has spoken, and the end speaks only for the series still out; one stop is in flight per series at a time — a stop asked for while one is out, by the finale's bound, a tool cut or a hard stop, is covered by it, so a tick sends one write whatever asks; a stop sent after the drop — the hard stop read after the one more command's wait, a turn's stop after its drop — gets no callback: the transport is lost or the run is being torn down, andend()'s kill is the stop; a dropped one — nothing kept it — is notedABORT_DROPPED_NOTEonce per series, as a failed one is, and, while the loop lives, owed like a failed one, a path nothing reaches today. And a landing acts on nothing once the loop or turn that sent the write has ended:dropHeldforgets every pending callback its sender did not mark as outliving the drop (a write in flight included; the harness so marks its abort's alone), so a loop's steer still in flight when the loop ends, settling after — before the next follow-up turn, or inside one that has its own write-up — is nobody's by construction: never the turn's clock, never a failure noted after the answer, never a re-ask of the loop's instruction into the turn. No abort is chained while a re-attach abandons — every abort's sender is the loop or the turn, suspended in the recovery's one raced wait (an owed re-ask is the tick's, and no tick runs inside that wait), and nothing is awaited a second time before the abandon, since a second, un-raced wait would hold a person's stop for as long as a follow-up steer's hung write takes; a step that meets a closed transport is teardown, wherekillends the process. A wrap-up steer whose write FAILED with the reset starts no clock and keeps no label on its own: a steer is never resolved by a re-send, so pi may never have got the instruction — the loop asks again through the gate (the fresh transport carries it once re-attached), awrap_upnote says so (wrapUpWriteFailedNote), and the clock starts when that instruction lands; if pi settles before it does, the drop at the loop's end unlabels the answer as for any wrap-up pi never saw. What the gate still holds when its loop ends — settled, stopped or thrown — and when a follow-up turn ends is DROPPED (HeldSends.dropHeld), delivering nothing, after the unechoed steers went back to the inbox: a held follow-up steer was just requeued and would otherwise run twice, a held wind-down steer belongs to the loop that ended and would otherwise be steered into the next turn, and a prompt still in doubt when the loop settled is nothing a later turn should start — so the transport's unsent writes are dropped with the gate's held ones (takeUnsent("loop-end"), taken to be discarded, by identity — the re-attach'stakeUnsent("reattach")is the other taking, and who took an entry decides a later step's landing, not the transport's state since: an entry the re-attach took answersheld, whatever order the microtasks land in, since the fresh transport re-sends the same object, and an entry the loop's end discarded answersdroppedeven on a transport a later recovery abandoned — so a pending step whose entry is gone answersdroppedand never lands, and the queue stays one-to-one with the chain's steps, so a follow-up turn's write is landed by its own step and never by the dead loop's — and a wrap-up steer among the transport's dropped clears the label and is said in thewrap_upnote as one among the gate's is), a wrap-up steer queued behind a failed write is not carried into the turn bytakeUnsent, a follow-up turn's first prompt is never held behind the dead loop's bound, and nothing of the dead loop's reaches pi inside the turn. A follow-up whose staging is still in flight when the loop ends (loopEnded) is requeued when the staging completes and sends nothing, exactly as one pi settled before. And a wrap-up steer among the dropped means pi finished on its own and never saw the instruction: the write-up is cleared, so the answer wears no time-budget, turn-guard or soft-stop label for a wrap-up that never went, and awrap_upnote says so (wrapUpUndeliveredNote); a failure the wind-down saw before the loop settled is kept, not dropped with the label: the answer is the model's own text with the failure after it, or the failure alone when pi wrote nothing (unlabelledAnswer, the same words OpenCode's unposted write-up uses). A plainclose()(teardown) delivers what was queued unless an earlier write failed — those writes are the re-attach's to take, and teardown ends the process bykill, never by the abort it sent. A runaway guard (MAX_INPLACE_REATTACHES) bounds re-attaches that read no record between them, and past it the run fails by name (reattachBoundMessage) on both harnesses whichever word it met — a control plane that keeps resetting, or the replaced word that keeps coming while the pid still answers — never turned into the verdict at the bound, which would relaunch a second pi beside the live one: the process's answer alone decidesword-alivefromword-gone, never the count. A follow-up turn (item 14) meets the same two failures as the loop does: a control reset or the refuted word on the turn's read or on its prompt's send re-attaches in place on the same pi — the turn goes on writing to the fresh transport, never a copy of the old — instead of the replaced verdict (a relaunch beside a live pi) or the turn's failure (which wouldend()a healthy pi). The re-attach machinery is lifted into one module (src/core/harness/reattach.ts:classifyLoopFailure,resolveControlResetWrite,reattachTransport, the notes and the bound) that pi and the OpenCode bridge both drive, so the rule lands in both at once. The OpenCode bridge's feed is read-only, so its control-reset re-attach covers the feed reads and the same bound; a control reset on the bridge's HTTP writes (the prompt POST, a permission reply) is resolved from the server's own state — the store and the pending asks tell whether the write landed, each re-issued once only where it did not, a steer the reset cuts never re-sent (harness.md item 13 and the row below). And on theruntime-replacedword itself, before any relaunch, the loop takes onealive(pid)for the row's pi in the container the run holds: the identity is the kernel's boot id, which a same-kernel replacement keeps, so the name never refutes the word — only the process does. Alive here, the run re-attaches in place,relaunchesuntouched, oneresumednote in exactly this wording,the executor said replaced; the row's pi answers alive in this container; re-attached; gone, the verdict stands by the word as above. A pid-reuse guard would compare the process's start time or command line to the row's facts where the driver has them; the container seam has neither (itsaliveis a barekill -0), so the pid alone answers here and the guard is a gap noted below.One run owns tool recovery. The shared relay classifies declared text failures with
toolTextFailedbefore returning them. The bridges feed native tool exceptions into the sameToolRecovery; normal command exit codes remain task evidence. Only failures with the same tool name and the same complete arguments share a correction allowance. Argument key order does not change a request. Matching error text and varied unresolved failures do not end a run early; the normal run budgets still apply. A repeated-failure notice counts attempts of that exact request. The retained checkpoint stays bounded, and a full checkpoint blocks further admission without losing its prior receipts. Unrelated reads and status updates do not clear failures. A successful corrected deliverable clears its own failures. Only hashes and closed stop facts reach the existing ledger state, acknowledged before further model admission. Saved call IDs deduplicate reconstruction. Model admission joins the native calls already tracked in each bridge before charging the next turn; unavailable original outcomes remain held. Initial and post-loop turns share the guard; a stopped run ends through the existing failure and producer-settlement path without buying a model write-up. Known pre-execution check refusals allow correction or the existing permitted fallback; repeated task refusals still stop. The existing gate marks lifecycle denials on their call identity, and typed no-dispatch budget or stop responses retain the normal ending without consuming task recovery. Uncertain outcomes stop immediately. A refused checkpoint uses the existing checkpoint-deferred hold path; malformed retained state blocks model admission. The guard grants no effect retry, provider-recovery or publication authority. In the pi extension, a missing or non-directoryfindorlsroot returns a path-unavailable result with layout guidance. The tool does not silently search a different root. Other filesystem failures and cancellation remain errors.
Workspace pi launches with workspaceScratch: true use the run-owned scratch directory as $TMPDIR. Identity-appropriate file tools can use its declared path; the ordinary review write restrictions still apply. The read wrapper checks the opened regular, single-link file against that directory and reads the same descriptor: Linux uses its descriptor path; macOS accepts only the system-owned /var and /tmp spellings and checks file identity. Runtime configuration and other declared run paths remain outside the grant. Reattachment restores the directory only from the original launch facts; cleanup follows the existing producer-custody and owned-root rules. Without the workspace capability, Linux outputScratch mode retains the older output-only behavior: bash spills into a fresh output/process-* child and only guarded read consumes it. That older mode grants no scratch read on hosts without Linux descriptor paths, and a fresh process chooses another child. Both modes retain pinned path normalization, offset/limit rendering, ordinary checkout images and a blocking authorization hook when initialization fails. These capabilities do not alter terminal classification, retry policy or saved-context validation.
Continuation after task settlement. After a successful settled task turn, the harness requests a native compact operation when the working view exceeds the history byte budget and useful time remains. The extension uses pi's shared summary generator under the native compaction signal and selected model registry, including quoted prior system context as evidence. It accepts only a nonempty complete summary under one quarter of the history budget and returns a self-retaining continuation compaction. The bridge mirrors its compaction_end through the existing fenced step write before returning the turn. It retains the authored answer and starts no task turn. Failed, truncated or aborted summaries leave no partial checkpoint. Hard stops still stop the run; soft stops and the existing deadline abort the checkpoint wait. The live model window remains unchanged.
Validation criteria
Unknown terminal diagnostics retain bounded, redacted pi and provider stop words, a closed structural reason when signed by the proxy, whether the proxy authenticated the unknown ending, whether pi supplied error text, and the number of partial content blocks. Unverified Pi error fields retain credential-redacted type, code, parameter, message, title, detail, body status and request ID, with strings capped at 400 characters and a truncation marker; valid response IDs permit correlation. The initial and follow-up failure notes carry these fields in a structured modelFailure object. Broad telemetry records closed observations, body status, a stable error fingerprint, and the run ID, trace ID, agent and model. Error prose and upstream IDs stay in the protected run record; progress logs and status cards use a fixed message. A log failure leaves a closed failure note in the existing run-event lane without calling the failed sink again or recording exception text. It cannot replace the original ending. Run tools mark every string from Pi as untrusted evidence for the investigating agent. Request/response bodies, URLs, proxy authentication markers and partial content never enter this diagnostic. SDK parse errors omit their payload and parser snippet. Run bearer patterns remain redacted after reattachment. Oversized or undecodable diagnostic input cannot replace the original failure. These error fields are evidence only and never select recovery. These facts do not change the unknown outcome or authorize a retry.
| Criterion | Proof |
|---|---|
| 7: quoted search arguments are data only in static pipelines of known source-read commands; both complete recorded source searches work, while executable consumers, rg preprocessors and whole-environment reads retain their refusals | [unit] src/core/harness/pi/toolRules.test.ts::judgeToolCall — bash::allows quoted search patterns without treating their operators as environment commands, ::allows the complete recorded static source searches for …, ::interpreter pipelines retain environment dump refusals for …, ::an environment dump is the whole environment, however it is asked for — not a prefix or one variable |
| 7: multiline, expansion, heredoc, comment-bearing and other complex shell text remains raw for environment-dump checks; nested substitution quotes and heredoc data cannot erase executable dumps | [unit] src/core/harness/pi/toolRules.test.ts::judgeToolCall — bash::quoted command substitutions cannot hide an executable environment dump for …, ::heredoc quotes cannot hide an executable environment dump for …, ::comment quotes cannot hide an executable environment dump |
7: workspace file tools accept this run's separate temporary directory; other runs and runtime configuration stay outside the grant. Pi supplies $TMPDIR, reads the same open regular file with aliases refused, and restores the grant only from recorded launch facts. | [unit] src/core/harness/pi/toolRules.test.ts::judgeToolCall — run temporary files::*, src/core/harness/pi/extensionSource.test.ts::the harness extension::reads a run-owned temporary file while keeping runtime files and aliases outside the grant:…, src/core/harness/pi/harness.test.ts::runPiHarness — a run on pi from the first file to the answer::restores output reads only for a recorded scratch-capable pi:… |
| 6: authenticated local model-call budget refusal ends initial and follow-up calls with the original used/maximum counts, without retry, provider-down wording, replenishment or unknown-result wording; an actual unknown result remains unknown | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — a run on pi from the first file to the answer::an authenticated local model-call cap stops initial and follow-up calls without retry or provider classification |
| 6: authenticated local admission capacity is named plainly and grants no provider retry or replay | [unit] src/core/harness/pi/harness.test.ts::*::names authenticated local admission capacity without provider recovery or replay |
| 6: unknown endings retain structural diagnostics without partial content, retry or provider-failure authority; only a verified proxy marker identifies a proxy-authenticated unknown ending or structural reason. Unverified Pi errors also retain bounded credential-redacted error fields and a valid response ID, excluding request/response bodies, URLs and authentication markers | [unit] src/core/modelProxy/providerFailureAuth.test.ts::unknown terminal authentication::preserves legacy markers and signs only closed structural reasons, src/channels/modelProxyResponses.test.ts::Responses unknown terminal diagnostics::carries structural reasons through the real pi adapter without provider prose or replay, src/core/harness/pi/bridge.test.ts::turns, narration and the answer — the loop's rules::unknown terminal diagnostics distinguish authenticated proxy evidence without exposing request bodies or content, src/core/harness/pi/harness.test.ts::runPiHarness — a run on pi from the first file to the answer::an unknown terminal model result fails honestly without retrying or claiming a provider refusal, ::unknown terminal diagnostics reach the run record on initial and follow-up calls without replay or private text, src/core/harness/pi/bridge.test.ts::*::retains bounded redacted Pi error details and correlation without granting recovery, src/core/harness/pi/bridge.test.ts::*::withholds proxy authentication text and malformed correlation from Pi diagnostics, src/core/harness/pi/harness.test.ts::*::unverified Pi error diagnostics reach initial and follow-up run notes without replay or bearer exposure, src/core/dispatch/runLoop.test.ts::*::saves redacted unverified Pi error fields on the original failed run |
16: a FIFO send timeout before pi's first line ends the run immediately as PiStartFailureError, with one harness_error, no replacement probe and no relaunch verdict | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — the container replaced under a live run::a send timeout before pi's first line is a start failure at once — no replacement probe or relaunch verdict |
1: one loop — the deleted modules are absent, no module imports them or calls the deleted code by name, the run stage has the harness's open form, no harness class and no selection between two loops, no preset declares a harness and the registry exports no table of them, no roster names and no pick, the roster is constructed in src/index.ts and src/cli.ts, the toolset table holds none of pi's own workspace tools, run-loop.md is gone from the tree and the specs index | [unit] src/core/harness/oneHarness.test.ts::one harness — the native loop, both provider adapters and the native tool table are gone (harness-pi.md item 1)::* |
1: the harness block parses as a mapping of presets to the roster's words, an absent block leaves the field unset (every preset on pi), and an unknown preset, native (the deleted loop), any other word and a non-mapping fail the load by name, naming the two harnesses; the same words under a channel's or a user's scope resolve user > channel > the block and are refused by name at load with their path, a stored overrides document included | [unit] src/config.test.ts::harness block (harness.<preset>: pi or opencode)::*, src/config.test.ts::harness as a scope setting (users.<id>.harness, channels.<id>.harness)::* |
2: every run is on the harness — pi's answer is the run's, its tool events are on the stream and in the record, the bearer reaches pi, the provider is never called in-process, pi is ended; a process without the harness deps, a public URL or a bearer fails the run naming what is missing; the gate's rules follow the thread — a pull-request thread's or a unit child's bound branch is the run's own push target with its base protected, a plain thread's binding is the protected base; the run stage around the loop — a completed run's publish, finish and record, a failed run's release and ❌ card, attach_file through the channel or the artifact store, a steered follow-up's staged file — is one code path over a scripted pi | [unit] src/core/dispatch/runLoop.test.ts::the pi harness — every preset's runs, in the run's container::*, src/core/dispatch/runLoop.test.ts::runLoop — the model turn and everything that rides on it::* |
| 3: each image installs pi once, globally, at exactly the pin; proves the pin and RPC mode at build; drops npm's cache; the execution images install it after the Node swap; the three images, root and published pi-ai libraries and development pi CLI name one exact version; internal pi dependencies are pinned to that release so fresh installs cannot mix minor-version APIs; the resident proves it as worker1; the bot image installs it in the runtime stage before the switch to its user | [unit] src/deploy/imagePiHarness.test.ts::* |
| 3, live: the built images answer the pin as a thread user | [agent] npm run check:image builds both; docker run --rm --entrypoint /bin/bash <resident image> -c 'su -s /bin/bash worker3 -c "pi --version"' → 1.0.4. |
4: the paths derive from the run id under one directory outside the checkout; the arguments pin RPC mode with every discovery off, the extension by path, pi's tools plus the relayed ones, the proxy provider and the model with its thinking level, a session directory or --session; the tiers are pi's levels by name; the env carries the bearer, the URL, the run id, the config directory and the offline switches and nothing else; models.json names the proxy on the run's shape with the bearer's variable, a zero rate card and the run's card — the resolved level map (the identity map when levels are unknown, null for a refused tier), the card's window as contextWindow, its inputs, the cap field and the marker rule as compat; the files are the never-trust settings with httpIdleTimeoutMs equal to the run lease remaining when pi starts (so no shorter library default cuts a reasoning pause), the models file, the prompt with the harness note last, the extension — none a secret | [unit] src/core/harness/pi/process.test.ts::* |
4: the biller's compat words — an aggregator block's entry carries the four beside the card's own, the developer role following the biller's id prefixes; an Anthropic block none of the four; a generic block's markers card none, its cache row degraded; the real binary honours the file: the tier as reasoning: { effort } with the markers on the prompt, the last tool and the last message, and the generic contrast carries neither; the aggregator × pi cells for cache and the effort shape; the proxy forwards each word byte-for-byte outside the two pinned fields | [unit] src/core/harness/pi/process.test.ts::piModelsJson::an aggregator block's entry carries the biller's four compat words beside the card's own, src/core/harness/pi/process.test.ts::piModelsJson::the developer role follows the aggregator's id…, src/core/harness/pi/process.test.ts::piModelsJson::an Anthropic block carries none of the four aggregator words…, src/core/harness/pi/process.test.ts::piModelsJson::a generic block's markers card carries none of the four…, src/core/harness/pi/testing/realCompat.test.ts::the real pi binary honours the biller's compat words::*, src/core/providerConformance.test.ts::provider conformance — pi::effort: a variant tier on the aggregator goes out as the aggregator's own reasoning object…, src/core/providerConformance.test.ts::provider conformance — pi::cache: a markers vendor on a biller with no harness-side provider degrades…, src/channels/modelProxy.test.ts::pinning and pass-through — the OpenAI shape::forwards pi's aggregator words byte-for-byte outside the two pinned fields… |
4: the pi block parses both compaction thresholds or either alone, refuses a non-integer, zero, a negative, a string, a non-mapping and an unknown key at either level by name, and an absent block leaves the field unset; the settings file carries the thresholds under pi's compaction key when the launch spec names them and is byte-identical without; the harness writes them from its deps for every run, and the dispatcher hands the config's block to the harness deps | [unit] src/config.test.ts::pi block (pi.compaction.reserveTokens, pi.compaction.keepRecentTokens)::*, src/core/harness/pi/process.test.ts::piLaunchFiles::carries the deployment's compaction thresholds…, src/core/harness/pi/harness.test.ts::runPiHarness — the deployment's compaction thresholds in pi's settings::*, src/core/dispatcher.test.ts::a follow-up seeds from its session (docs/reference/specs/session-log.md item 9)::the deployment's compaction thresholds ride from the config into the pi harness's deps… |
4: the per-run root: every path hangs off /var/tmp/switchboard-pi-<runId>, a sibling of every other run's directly under /var/tmp — outside the shared temp directory a checkout's suite or cleanup empties — with no parent between them, shared or per user; the write script and the start script make the run's directories at 700 with the umask before the mkdir, the start script's in a subshell; remove takes the directory down as one tree; on one container two thread users both write their files, start their pi and remove their root, each under a root of its run's own, where one shared parent refuses the second user's mkdir; the harness files a run under its own root and removes it once the run is over, settled or failed; the run loop files a resident run under the run's root whatever pool user the binding names | [unit] src/core/harness/pi/process.test.ts::piRunPaths::derives every path from the run id under one directory of the run's own directly under /var/tmp…, src/core/harness/container.test.ts::the container scripts::writes a file with printf in chunks a command can carry: exact bytes, the first creating its directories at 700…, src/core/harness/container.test.ts::the container scripts::starts the process detached behind a FIFO held open for writing…, src/core/harness/container.test.ts::ExecHarnessContainer — each operation is one command over the executor::remove takes the run's directory down as one tree, and nothing else, src/core/harness/container.test.ts::ExecHarnessContainer on a resident, two thread users on one container::*, src/core/harness/pi/harness.test.ts::runPiHarness…::writes every file and starts pi under the run's own root directly under /var/tmp…, src/core/harness/pi/harness.test.ts::runPiHarness…::a pi that dies before settling fails the run naming its last stderr…, src/core/dispatch/runLoop.test.ts::the pi harness — every preset's runs, in the run's container::a run on a resident files its pi under the run's own root directly under /var/tmp… |
4: a control file that vanished under a live run is the typed control-file-lost failure by name — the file and the root on the error, a harness_error note saying which file under which root is gone — never the replaced-container verdict: the container is alive and answering, and any other send failure stays what it was | [unit] src/core/harness/container.test.ts::ExecHarnessContainer — each operation is one command over the executor::a send that fails because the FIFO is gone is the control-file-lost failure by name…, src/core/harness/pi/harness.test.ts::runPiHarness — the container replaced under a live run::a control file that vanished under a live run fails it by name… |
4, 8: the layout under any root: piRunPathsAt lays the run's files out under a given root, and piRunPaths is that layout under this build's own | [unit] src/core/harness/pi/process.test.ts::piRunPaths::piRunPathsAt lays a run's files out under a given root, the one a row recorded, and piRunPaths is that layout under this build's own root |
| 4: the model's generation decides its thinking payload — Fable 5 and 5.1, Opus 5, Sonnet 5, Opus 4.6 to 4.8 and Sonnet 4.6 adaptive; Haiku 4.5, Sonnet 4.5, Opus 4.5 and older the budget; a dated alias read by its version and never its date; an id without a version the current generation — and the run's Anthropic-shape entry carries the flag beside every field it had, while the completions shape carries none | [unit] src/core/harness/pi/process.test.ts::takesAdaptiveThinking::*, src/core/harness/pi/process.test.ts::piModelsJson::the run's model entry asks a Claude 5 model for adaptive thinking…, src/core/harness/pi/process.test.ts::piModelsJson::a Claude model before 4.6 keeps the budget payload…, src/core/harness/pi/process.test.ts::piModelsJson::an OpenAI-compatible provider's entry carries no Anthropic compat… |
| 4: the container scripts — printf chunks under umask 077, the FIFO held open and the pid recorded, the filter, the line and the long line, the offset read as base64, liveness, the group kill; a failed command is a named error; each operation is one exec with the env on the start alone | [unit] src/core/harness/container.test.ts::* |
4: ExecOptions.env — the local child sees the caller's variables beside the public environment and never a host secret, and inherits everything without one; the resident sends env in the body only when there is one — a caller's variables with the run's four commit identity variables over them, resolved per exec (execution.md item 5); the sandbox merges it under the resolved credential; the resident Worker reads the body through envFromRequest and threads it under its own injected variables | [unit] src/execution/executor.test.ts::LocalExecutor exec env::*, src/execution/resident.test.ts::ResidentExecutor.exec::sends a caller's env in the /exec body…, src/execution/cloudflareSandbox.test.ts::CloudflareSandboxExecutor credential freshness::a caller's env rides beside the resolved credential…, src/execution/residentExecEnv.test.ts::resident Worker /exec env::*, src/execution/residentExecEnv.test.ts::ResidentExecutor — the commit identity rides each /exec body's env::* |
| 4: the transport sends in order, reads whole records however the bytes fall and advances by exact bytes, waits for a partial record, ends when pi is found dead after a last read, surfaces a failed write, re-attaches at an offset, and keeps the boundary after the record last handed out apart from the read position | [unit] src/core/harness/pi/transport.test.ts::PiRpcTransport::* |
| 4: the framing and the record parser | [unit] src/core/harness/pi/protocol.test.ts::* |
5: every documented kind has a disposition and the set equals the spike's home table; the tool and narration events for one turn are the ones the native loop emitted for it, held as a literal; a nonzero bash exit and a non-bash failure read as the loop's; one tool.<name> span per call under run.agent ended with the call's outcome, its id on both events, open spans closed on a stop; bookkeeping turns and the held answer follow the loop's rules; 💭 thought for …; a provider error is reported; the compaction, harness-error and dialog notes and the structural silences | [unit] src/core/harness/pi/bridge.test.ts::* |
5: a run's model.turn spans are the proxy's — model, the tools offered (tools, toolNames, toolChoice; model-proxy.md item 6), stopReason, the four token counts and ttftMs when known, and nothing else: no blocks, thinkingMs or textMs, the stream timings only the deleted native runner's observer wrote | [unit] src/channels/modelProxy.test.ts::the meter — one model.turn span per proxied call, the runner's attrs::a streamed Anthropic reply is forwarded chunk for chunk in order with its headers, and the span ends only after the last chunk with model, stop reason, the four token counts and the time to first token, ::a streamed OpenAI reply: finish_reason tool_calls is tool_use, the usage frame's cached_tokens is the cache read; without a usage frame the span still ends ok with no token counts |
5: a relayed tool that declares failsInText and answers a text opening error: is ok:false on the record without pi's isError; a relay that does not declare (read_file) stays ok:true whatever its text says | [unit] src/core/harness/pi/bridge.test.ts::the gate's coverage — every call that ran was vetted::a relayed tool whose text opens error: is recorded ok:false without pi's isError; ordinary text stays ok:true |
5: the grant re-parented to run.agent, so proxied turns hang under the harness's span; nothing for an unknown or ended run | [unit] src/core/modelProxy/runBearers.test.ts::RunBearerStore — turns and the operator's extra bearer::reparent hangs the run's proxied turns under another span… |
| 6, 8: the harness end to end — the files and the process with the bearer in the env and no key, the protocol, the bridged events, the mirrored steps, the facts, pi ended and the run off the registry, the spans; the run on the registry while pi runs; a follow-up steered with the loop's prompt and recorded; the wrap-up once; the deadline's write-up with tools refused and the budget label; the turn guard with the pace; the soft stop; the hard stop; the bounded write-up; pi's death, a refused prompt, a model error; a dialog cancelled and an unknown kind noted; the re-attach with the death's failure tolerated; the restart on the mirrored session | [unit] src/core/harness/pi/harness.test.ts::* |
6: transient and rate-limited provider causes hold and retry at most twice inside one minute and the loop lease; a pi-local abort or stream EOF (including Anthropic without a stop reason) also retries locally but never waits for provider-up; intentional tool-cut and wind-down aborts keep their endings; unknown results fail without retry or provider-refusal wording; the provider retry window ends as ModelTransientFailureError with failure: provider_transient, while a pi-local stream EOF that exhausts its retry lease ends as ModelStreamIncompleteError with failure: model_stream_incomplete; billing, key, model, request-rejected and permanent causes are never retried | [unit] src/core/harness/pi/bridge.test.ts::turns, narration and the answer — the loop's rules::keeps local aborts and unknown terminal results outside the provider-failure types, ::classifies incomplete OpenAI Responses streams as local retries without provider-down evidence, src/core/harness/pi/harness.test.ts::runPiHarness — a run on pi from the first file to the answer::an unknown terminal model result fails honestly without retrying or claiming a provider refusal, src/core/harness/pi/harness.test.ts::runPiHarness — a run on pi from the first file to the answer::a typed billing failure ends the run without another model prompt, ::a 5xx then success: the transient failure is retried after the ladder's first backoff — pi is re-prompted and the retry's answer is the run's, ::a provider-up buffered during a successful follow-up retry stays consumed across a process restart while an ordinary follow-up beside it survives…, ::a provider-up buffered during a successful main-loop retry stays consumed across a process restart, ::provider-up rows buffered during a successful main-loop reissue stay consumed across a process restart, ::an OpenAI stream ending without finish_reason is retried and the child survives, ::an incomplete OpenAI Responses stream retries locally and completes the original turn, ::an abort while a model stream is open is retried as the same turn, ::transport failures stay held on backoff inside the lease; exhausting that retry budget ends by type without exposing a gateway page, while a non-transient error is never retried, ::a parked provider failure on a park-capable run holds the turn and retries on backoff without waiting for another run; a plane reissue can still release it early, ::the ProviderFailure seam classifies real transient answers without promoting embedded tokens, ::the harness's park decision is the typed cause set, not a second prose classifier; src/core/dispatch/runLoop.test.ts::runLoop — the model turn and everything that rides on it::a run whose model call spent the transport retry budget fails by name: the record says failure: provider_transient, ::an exhausted local stream records model_stream_incomplete without claiming a provider failure; src/core/harness/pi/harness.test.ts::runPiHarness — a run on pi from the first file to the answer::an incomplete local stream that spends the retry lease ends with its own typed failure |
6: an errored assistant whose raw stop reason is one of the provider's refusal words has a typed provider_refusal terminal result; another word, or none, is not a provider refusal | [unit] src/core/harness/pi/bridge.test.ts::turns, narration and the answer — the loop's rules::an errored assistant whose raw stop reason is the provider's refusal word is a policy refusal… |
6: a call refused under the provider's usage policy fails the run by name at once — one prompt, no retry, one permanent-cause sentence on the policy_refusal note and in the thread, no provider words | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — a run on pi from the first file to the answer::a call refused under the provider's usage policy fails the run by name at once… |
6: through dispatch() a refused run's thread reply is the permanent cause's one sentence with the run link, and its run-page note carries that same sentence — neither renderer receives provider words; any other provider error keeps the harness's safe typed sentence | [unit] src/core/dispatcher.test.ts::closed-card checklist and review verdict run link::a run the provider refused under its usage policy replies the permanent cause's one sentence…, ::a failed run's error reply carries the run link…, src/core/harness/pi/harness.test.ts::runPiHarness — a run on pi from the first file to the answer::a call refused under the provider's usage policy fails the run by name at once… |
| 6, live: a thread whose earlier request the provider refused continues on the next plainly worded follow-up | [agent] (human-gated.) In a coding thread on pi, ask the model to quote its notes for the thread verbatim; when the run ends with the refusal sentence, reply in plain words asking what the notes record. The second run completes: the refused run's record says failure: policy_refusal, the second run's record carries the seed note counting one request left out, its reply is the model's answer, and recall still finds the refused words in the log. |
6: a steered follow-up's staged file is copied into the store and pulled into the container over the run's executor before pi reads the steer, whose text ends with the attachments line; the record carries the in event; without a store the steer is sent as before — no copy, no pull, no line | [unit] src/core/dispatch/runLoop.test.ts::the pi harness — every preset's runs, in the run's container::a steered follow-up's staged file is copied and pulled before pi reads the steer…, …::without a store the pi steer is sent as before… |
6: past the loop a hard stop releases the workspace with always whatever the preset and the card ends ⏹, a soft stop keeps the preset's release policy (if-idle for coding), posts the write-up under the ⏹ label and ends the card ⏹; a hard-stopped review classifies, moves and re-reviews nothing and posts nothing to the pull request; a hard-stopped run never reflects into memory, even when the gate would qualify it | [unit] src/core/dispatcher.test.ts::executor provisioning by agent resources::a hard stop from /runs releases the executor with 'always' and reports the abort, ::a soft stop keeps the normal release policy (if-idle for coding) and marks the card stopped, src/core/dispatcher.test.ts::review post-step::a hard-stopped review posts nothing to the PR, src/core/dispatcher.test.ts::review post-step::head moved during the run (item 12)::a hard-stopped review never classifies, moves or re-reviews, src/core/dispatcher.test.ts::cross-session memory WRITE path::a hard-stopped run does NOT reflect, even when the gate would qualify it |
| 6, live: Stop interrupts the turn and forces cleanup while retaining a stopped result. Kill skips the turn interrupt and removes the row after runtime shutdown. | [gap] Requires two disposable runs on a deployed build with these controls. Verify the terminal Stop result, the absent Kill row, and the exact runtime shutdown receipts. |
6: after every compaction that carried a summary pi is steered with the session's notepad as it stands — or that it is empty and how to keep one — and the reach recall gives; a failed compaction steers nothing; a notepad read that fails costs the steer its notes and never the run | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — a run on pi from the first file to the answer::after every compaction pi is steered with the notepad…, ::a notepad read that fails at compaction time costs the steer its notes, never the run… |
| 7: the extension imports only Node builtins and the pinned pi SDK, retains a blocking gate when settings or initialization fail, registers every served tool as a relay with the bot's schema asking with the bearer alone, relays a call and throws on an error answer, asks the bot before every tool call and blocks with its reason, blocks at once on a 4xx at the door while a 5xx is asked again, and blocks by itself after 90 s of an unreachable bot | [unit] src/core/harness/pi/extensionSource.test.ts::the harness extension::* |
| 7: new pi and OpenCode launches register the runner-owned tool declarations without a discovery HTTP request. The same frozen declarations supply names, schemas, prompts and allowlists; registration grants no authority. Retained processes keep their original code, and the legacy discovery route remains available. | [unit] src/core/harness/launchTools.test.ts::runner-owned tool registration::%s registers launch definitions without discovery and still asks the bot for authority; src/core/harness/pi/extensionSource.test.ts::the harness extension::retains a blocking gate when extension initialization fails, ::names scratch setup failure without exposing paths or opening the gate; src/core/harness/opencode/pluginSource.test.ts::the OpenCode relay plugin::rejects setup when registration fails after adding one tool |
| 7: the extension restores the declared run_check root union when the pinned non-strict Anthropic adapter omits it. It only changes an already-offered matching tool, preserves unrelated schemas and payload metadata, and leaves strict schemas, retained unions and other wires unchanged. | [unit] src/core/harness/pi/extensionSource.test.ts::run_check provider schema::retains the checkpoint rule in the serialized Anthropic payload for %s without restoring removed tools |
7: the registry; the definitions served as declared; the gate allowing relayed tools and ordinary commands, refusing a push off the run's branch and a tool outside the reach with a tool_refused note, refusing everything during the write-up; a relayed tool run under the call's span with the run's context, unstamped without one, a throw and an unknown tool as error answers, images as blocks | [unit] src/core/harness/pi/relay.test.ts::* |
7: the gate's coverage — pi's three pre-hook answers read by name and nothing else; the authorize route tells the harness the gate saw every call, allowed or refused; a call the gate saw ends quietly, pi's own answer and the extension's own block are harness_error notes naming the reason, an unvetted call that ran is reported as a bypass, calls started while not judging are not; end to end, a bypass fails the run closed with the note, the abort and the span in error, pi's own answer is noted and the run goes on, a re-attach judges nothing the log already held | [unit] src/core/harness/pi/protocol.test.ts::piAnsweredWithoutRunning…::*; src/core/harness/pi/relay.test.ts::authorizeToolCall — the gate::tells the harness the gate saw the call…; src/core/harness/pi/bridge.test.ts::the gate's coverage…::*; src/core/harness/pi/harness.test.ts::runPiHarness — a run on pi from the first file to the answer::a tool call pi ran without asking the gate fails the run closed…, ::a call pi answered itself…; src/core/harness/pi/harness.test.ts::runPiHarness — after a bot restart::re-attaches without judging the calls the log already held… |
7: the coding preset's tool rules — bundles and the reach, bash pushes (the pure preview still checks literal source, destination and lease shape, but a live write-harness run with a Git Door refuses even a well-formed shell push; only its runner-owned publish_branch tool publishes); legacy write-identity shell calls with active expansion, escaped words, malformed quotes, nested shell execution or a helper passing the Git executable and push verb separately, or a direct programmable interpreter even with no Git words in the shell text, are refused before execution; the typed effect pins the source commit and a separate credential before the Door permits one matching receive-pack; existing-PR shell command receipt recognition remains only for historical records; Git options that change its repository or configured refspec fail closed), merge/approve, credentials, environment dumps, paths, malformed inputs | [unit] src/core/harness/pi/toolRules.test.ts::*, src/core/harness/pi/toolRules.test.ts::judgeToolCall — bash::allows only a terminal stderr redirect, never a redirect that hides later arguments |
| 7: on Linux without the workspace-scratch launch capability, pi's pinned bash spills success and failure output into an isolated per-process scratch under the run's output-only directory, outside the checkout and agent configs; only read may consume it, with offset/limit intact, open-file identity checked against that scratch, and symlinks, hardlinks, cross-run/process paths and ordinary outside paths refused; a live reattach keeps its scratch only when its launch facts attest the output guard, legacy processes gain no new read, and a new pi gets fresh scratch | [unit] src/core/harness/pi/extensionSource.test.ts::the harness extension::reads pinned pi bash spills only from this process's output scratch for …, src/core/harness/pi/process.test.ts::piLaunchEnv::binds an output-only scratch root for workspace identities, never the agent config or checkout, src/core/harness/pi/toolRules.test.ts::judgeToolCall — output scratch::allows only read within the bound output root for both workspace identities, src/core/harness/pi/extensionSource.test.ts::the harness extension::loads the output read override through the pinned pi loader outside node_modules, src/core/harness/pi/harness.test.ts::runPiHarness — a run on pi from the first file to the answer::restores output reads only for a recorded scratch-capable pi:… |
7: typed-publication mode allows harmless wildcard reads, interpreters and ordinary composition without a publication grant; literal shell pushes direct recovery to publish_branch, while credentials, merges, the publication fence and legacy source binding remain guarded; indirect interpreter pushes with the model bearer fail at the actual Git Door without writes or borrowing a pending effect credential | [unit] src/core/harness/pi/toolRules.test.ts::judgeToolCall — typed publication::allows harmless wildcards, interpreters and shell composition without minting publication, src/core/harness/pi/toolRules.test.ts::judgeToolCall — typed publication::refuses literal shell publication with typed-tool recovery guidance and preserves credential and merge guards, src/channels/githubDoorIntegration.test.ts::GitHub door against a real Git smart HTTP peer::accepts a large new branch push after an empty probe without spending its one-use authority, ::accepts a large existing PR push after an empty probe without spending its one-use authority |
| 7: reading a pull request's reviews through the GitHub API is allowed evidence; posting a review or merge through the CLI or API remains reserved for the runner's typed post-step | [unit] src/core/harness/pi/toolRules.test.ts::judgeToolCall — bash::refuses merging or approving a pull request, by CLI or by API — never the agent's to do |
7: in legacy literal-source mode, a push is refused when its shell call also runs a status command, another push or any sequence, background, conditional, pipeline, subshell or redirection (except terminal 2>&1); escaped-newline continuations are refused before an existing-PR push can execute because the leased receipt parser cannot recognize them, while operative metacharacters fail closed and quoted metacharacters remain data unless the leased parser cannot attest the quoted push; a helper passing Git and push as separate words or constructing both words in a direct interpreter is refused before execution, even while a typed effect is pending; the owned destination, explicit expected-head lease, single destination and branch source checks remain in the pure preview and in the typed Door effect, with HEAD refused for its source rather than invented extra refspecs | [unit] src/core/harness/pi/toolRules.test.ts::judgeToolCall — multiline pushes::*, src/core/harness/pi/toolRules.test.ts::judgeToolCall — multiline pushes::refuses a helper program passing Git's push verb as a separate argument before shell execution, src/core/harness/pi/relay.test.ts::authorizeToolCall — the gate::refuses a compound leased push before execution or publication attribution |
7: the pi harness refuses model-shell pushes; a relayed publish_branch requires the bridge to observe its exact active tool name and input before the runner admits an immutable source and effect-only credential at the Git Door; the shell's legacy admission hook, if used without this credential, cannot open a scoped Door slot; a forged authorization request for another command earns no Door slot, refusal leaves no receipt-eligible event, retry can succeed, and a duplicate authorize ask for one call produces one authorization | [unit] src/core/harness/pi/relay.test.ts::authorizeToolCall — the gate::waits for source-bound Door admission before emitting a receipt-eligible push and recovers from refusal, src/core/harness/pi/bridge.test.ts::the gate's coverage — every call that ran was vetted::binds a publication ask to the actual bash call and exact command pi announced, src/core/dispatch/runLoop.test.ts::runLoop — the model turn and everything that rides on it::refuses model-shell publication without opening a Door slot, including a literal owned push, ::serves the runner-owned publication effect only with both typed capabilities:…, src/core/publicationEffect.test.ts::publish_branch — a runner-owned Git Door effect::*, src/core/harness/pi/relay.test.ts::HarnessRegistry::does not run a forged publication effect from a bearer without an observed matching Pi tool call, src/core/harness/pi/bridge.test.ts::the gate's coverage — every call that ran was vetted::binds a typed publication effect to its observed tool name and exact input |
7: a runner-owned publish_branch accepts only a checked-out, clean, non-protected, bound source and origin, pushes the immutable source SHA with the expected-head lease on an existing PR, verifies the Door's durable accepted transition and remote head before crediting the result, refuses foreign or opaque model-shell pushes and mismatched tool-call replays, and leaves work local on a refusal; the first-branch path also carries the distinct effect credential | [unit] src/core/publicationEffect.test.ts::publish_branch — a runner-owned Git Door effect::*, src/core/modelProxy/gitBindings.test.ts::GitBindings::does not let a model shell bearer consume the runner's execution-bound slot, src/channels/githubDoor.test.ts::GitHub run-bearer door::refuses a concurrent model bearer even when a typed effect owns the same one-use ref/source slot, src/core/publicationPush.test.ts::leased publication push evidence::pairs a runner-owned effect only with its trusted admission, result and Git Door output, src/core/codingPrPostStep.test.ts::pushedBranchOf (the branch a run's own git push named)::trackPushedBranch (paired with the command that produced the output)::credits a typed publication only with its own successful Git result |
| 7: runner-owned publication reads structured command and transport outcomes, never rendered output: empty successful status is clean; failed or truncated inspection refuses before grant, failed or truncated transport cannot report success, unsupported backends fail closed, and tracing forwards facts without logging them | [unit] src/core/publicationEffect.test.ts::publish_branch — a runner-owned Git Door effect::*, src/execution/resident.test.ts::ResidentExecutor.exec::keeps command facts separate from the human empty-output sentinel, src/execution/tracingExecutor.test.ts::TracingExecutor::forwards typed command and publication outcomes without putting their data in spans |
| 7: a runner-owned publication uses an operator-only typed transport whose effect bearer runs under the resident service UID, never the model shell UID; it uses fixed Git argv against the root-owned mirror, reads only object bytes from the model worktree, disables hooks and host config, and refuses unsupported execution backends without falling back to a shell push | [unit] src/execution/residentPublication.test.ts::resident runner-owned Git publication::*, src/execution/resident.test.ts::ResidentExecutor.exec::keeps model env from replacing the door bearer and sends publication only through the typed route, src/core/publicationEffect.test.ts::publish_branch — a runner-owned Git Door effect::* |
7, 15: a bash call whose explicit timeout reaches past the loop's end is refused in the exact sentence naming the seconds asked, the seconds left and the two ways forward; a timeout inside the end runs; a call naming no timeout is never refused by it, ten seconds from the end included; a non-numeric, non-finite or non-positive timeout and a context with no loop clock are not its business; past the end it names 0 s left and the other rules speak first; at the gate the refusal is the tool's result and a tool_refused note on the record | [unit] src/core/harness/pi/toolRules.test.ts::judgeToolCall — bash: an explicit timeout against the loop's end::*, src/core/harness/pi/relay.test.ts::authorizeToolCall — the gate::refuses a bash call whose explicit timeout reaches past the loop's end… |
| 15, 4: the timeout ends the command whatever it does with its pipes — through pi's real bash tool with the harness's shell command prefix, a child that keeps stdout open past the deadline is ended with pi's own timeout result and the captured output, a backgrounded subprocess outside the group holding the pipe and writing does not extend the call, and a command that exits in time is unchanged in output and exit code; the settings file the harness writes carries the prefix for every run | [unit] src/core/harness/pi/shellPrefix.test.ts::the bash tool's timeout ends the command whatever it does with its pipes (harness-pi.md item 15)::*, src/core/harness/pi/process.test.ts::piLaunchFiles::writes settings that never trust the checkout…, src/core/harness/pi/harness.test.ts::runPiHarness — the deployment's compaction thresholds in pi's settings::* |
| 7: the routes — the door from the headers by reason, the tools served, the verdict, the relayed result, method and body refusals; the node adapter refuses before the body, caps and parses it, answers one document | [unit] src/channels/harnessRoutes.test.ts::* |
| 7: the shim names the four paths one route word | [unit] src/core/trace/workerTrace.test.ts::shimRoute::names the pi harness's four paths as one route word… |
7: the pointer summary opens with its fixed prefix, says why pi's summary could not be written in the failure's redacted and capped words, points at recall and the notes, and carries the previous summary and pi's file lists in pi's own tags, each only when there is one; the route's body is read by shape and refused by name | [unit] src/core/harness/pi/compactionFallback.test.ts::* |
| 7: the bot's word on a compaction — nothing while pi's own summary stands, the pointer once the run's last compaction failed for good, taken once — through the relay and over the route (a malformed ask 400, a GET 405); the bridge hands a failed, unaborted compaction's words to the harness and nothing for an aborted one, and notes a compaction carrying the pointer as the bot's; end to end, a summary refused under the provider's policy arms the pointer through the live registration, a transient failure arms nothing, a compaction that landed clears it, and the pointer's compaction steers the notes like any other | [unit] src/core/harness/pi/relay.test.ts::answerCompaction — the bot's word on how pi's compaction is written::*, src/channels/harnessRoutes.test.ts::handleHarnessRequest::POST /harness/compaction answers nothing while pi's own summary stands…, src/core/harness/pi/bridge.test.ts::the notes — compaction, harness errors, dialogs, the unknown::a failed compaction rides the observation with pi's words…, src/core/harness/pi/harness.test.ts::runPiHarness — a run on pi from the first file to the answer::a compaction that failed for good… |
7: a compaction that failed for good is a checkpoint signal: the run loop's hook commits a tracked worktree and pushes it to the unit's branch with a pushed_head event (by: "salvage") and a compaction_salvage note naming the failure, and the run continues; a clean tree yields the note alone; a context that no longer fits ends the round with the push already made; the harness awaits the hook with the failure's words — never for a transient or aborted compaction — and a hook that throws is a harness_error note, never the run's end | [unit] src/core/dispatch/runLoop.test.ts::the pi harness — every preset's runs, in the run's container::a coding child whose compaction fails for good checkpoints its tree…, src/core/harness/pi/harness.test.ts::runPiHarness — a run on pi from the first file to the answer::a compaction that failed for good is a checkpoint signal… |
7: the extension's session_before_compact hook asks the bot once with the preparation's facts and hands pi the bot's compaction under pi's kept entry and size with the file lists as its details; a bot that leaves the summary to pi, cannot be reached or refuses answers nothing and is not asked again | [unit] src/core/harness/pi/extensionSource.test.ts::the harness extension::the session_before_compact hook asks the bot once… |
7: a relayed call that outlives one request: a tool that answers within the window answers in one request as before; one still running after the window is pending, every later ask with the same call id joins the one run and never starts it twice, and the ask after the answer reads it, as does a retry; an await_runs that ends after seven minutes of the run's clock is asked after every thirty-second window and answers once, the tool having run once; send_to_run through the relay steers the child as the requester; a run's end stops a call still running through its context's signal and the registry's calls go with the registration | [unit] src/core/harness/pi/relay.test.ts::relayToolCall: a relayed call that outlives one request::* |
7: the route answers a call still running after the window 202 { pending, toolCallId }, and the same id asked again joins the one run and reads the answer, through the pure handler and over HTTP through the node adapter, the response held for the window and never for the tool | [unit] src/channels/harnessRoutes.test.ts::handleHarnessRequest::POST /harness/tool for a call still running after the window is 202 pending…, src/channels/harnessRoutes.test.ts::createHarnessRoutesHandler…::a call that outlives the window is answered 202 pending over HTTP… |
| 7: the extension asks again with the same call id while the bot says the call is still running and hands pi the one result; a bot that stops answering mid-wait is asked every two seconds and the call fails after 90 s naming the tool; a 4xx at the door fails it at once; pi's abort ends the asking | [unit] src/core/harness/pi/extensionSource.test.ts::the harness extension::a relayed tool whose bot says the call is still running…, src/core/harness/pi/extensionSource.test.ts::the harness extension::a bot that stops answering mid-wait… |
7: the door during a generation's boot — an unknown bearer before the reclaim settles is held for up to DOOR_HOLD_MS, not answered, and judged again once the listing lands (a reclaim naming no such run makes it the store's 404); a run the reclaim handed to the launcher is answered 202 pending on /harness/tool and 503 with Retry-After on authorize and tools, never a 4xx; the bound elapsing unsettled answers retryable (reclaim_pending); a surviving pi's bearer is retryable through the whole resume — unknown run before this generation's mint, unknown bearer between the mint and the adoption — and the same ask succeeds once the run registers and adopts it, the store's verdict final again after the resume ends; a malformed, revoked, expired or missing bearer is final whatever the takeover says; over HTTP the held answer carries the header, the log names the hold and the body is not read | [unit] src/channels/harnessRoutes.test.ts::the door during a generation's boot…::*, src/channels/harnessRoutes.test.ts::createHarnessRoutesHandler…::over HTTP a run still resuming… |
7: the takeover fact — unsettled until a reclaim outcome is taken or there is no ledger, whenSettled true then and false at its bound with no timer left behind; a run handed to the launcher is pending until done, one held by another generation while the latest listing names it; the launcher tells onDone once per run when its resume is over — a dispatch that resolved or threw, or a run closed instead — never before | [unit] src/core/runLedger/takeover.test.ts::*, src/core/resumeLaunch.test.ts::launchResumes::onDone names each run… |
7: the extension against a bot whose door opens late — 503 reclaim_pending, then 202 run_resuming, then the settled answer — asks again with the same call id until it answers and hands pi the settled answer; the hook waits through the same 503 for its verdict | [unit] src/core/harness/pi/extensionSource.test.ts::the harness extension::a bot whose door opens late… |
| 7, live: a surviving pi's re-ask across a restart lands on the re-registered run | [agent] A coding run on a resident or sandbox repository mid-bash (a sleep 120 in its command) → deploy restart → the new generation's log carries [harness] 202 tool — held (run_resuming) run=<id> (or 503 authorize — held (run_resuming) run=<id>) lines between its [reclaim] … took N run(s) line and the run's own [harness] run=<id> … lines, and never a 404 tool — unknown_run or 401 tool — unknown_bearer for that run; the run continues on the same pi (the record shows one tool.bash for the call, the re-attach note, no second run of the command) and ends with its reply. |
| 8: the saved offset names the last record the ledger holds and is saved after the write, never at a turn's end: a re-attach reads the log again from there, so the tool result the dead generation read but never wrote is the next step's user turn; an assistant turn read again spends no second index and the steps continue from the transcript; a compaction row read again spends none either, and the step after it lands after every row the transcript holds, the compaction row counted; the tail is compared with the first row alone; the mirror answers whether the ledger holds everything up to a message; the transport keeps the consumed boundary apart from the read position | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — after a bot restart::a re-attach reads pi's log again from the last turn the ledger holds…, ::an assistant turn the ledger already holds, read again…, ::a compaction row the ledger already holds, read again…, src/core/harness/pi/mirror.test.ts::PiMirror — step records as the runner writes them::answers whether the ledger holds everything up to the message…, ::on a re-attach the transcript's last assistant turn, met again…, ::a compaction row the ledger already holds, met again as the first row…, ::the transcript's tail is compared with the first row the mirror meets alone…, src/core/harness/pi/transport.test.ts::PiRpcTransport::the consumed offset is the boundary after the record last handed out… |
| 8: the answers a dead generation's commands got are not taken for this generation's, so the catch-up lasts until its own prompt is answered and the death's model failure stays a note; the ids carry a nonce beside the moment, so two generations begun on one clock reading share none; a steer's text read again is not mistaken for the seed's echo, and a continue prompt's echo is a turn the model was told | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — after a bot restart::the answers pi gave a dead generation's commands, read again…, ::this generation's command ids carry the moment it began and a nonce…, ::a steer's text the dead generation read but never wrote… |
| 8: the failed call a re-attach catches up on is a note and never a turn — the first step this generation mirrors lands right after the transcript it resumed from, no step carries a turn without parts, and the bridge yields no message for an errored assistant | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — after a bot restart::re-attaches to a pi still running…, src/core/harness/pi/bridge.test.ts::turns, narration and the answer — the loop's rules::a model call that failed is a provider error and no message… |
| 8: a re-attach to a pi inside a relayed call: the continue is a prompt queued as a steer, never the plain prompt pi refuses while processing; the calls in flight are settled on the relay before anything is awaited, the extension's re-ask reads the restart note and the tool never runs; the run settles when pi ends its turn; an idle pi is sent the same queued prompt, a fresh pi the plain one; over the relay a settled call id is answered from the record asked once or again, a call the record does not know runs, a call already running keeps its own answer | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — after a bot restart::re-attaches to a pi still inside a relayed call…, ::re-attaches to a pi still running…, ::the explicit replacement protocol rebuilds the transcript…, src/core/harness/pi/relay.test.ts::RelayedCalls.settle — a call in flight when the previous generation died::an ask for a settled call id is answered at once… |
| 7: the bridge knows which calls are open, from a call's start to its end, so a relayed request can wait for the bridge to have seen its call | [unit] src/core/harness/pi/bridge.test.ts::the bridge's spans…::knows which calls are open… |
| 8: a native source result is checkpointed only after its finished native message, before the next model source check. The native drain waits through canonical persistence; a missing or failed write cannot reach the provider. Parallel calls and reattach retain original custody without duplicate results. Source validators and the late model fence remain unchanged | [unit] src/core/harness/pi/mirror.test.ts::*; src/core/harness/pi/harness.test.ts::native source result checkpoint::*; src/core/runLedger/resume.test.ts::planResume::keeps a complete native source-result checkpoint resumable without replaying calls; src/core/dispatch/contextAccess.test.ts::message-bound context access::checks the next model call against a native GitHub… |
| 8: pi's messages in the runner's vocabulary; the step records as the runner writes them; the session file a restarted pi continues from | [unit] src/core/harness/pi/mirror.test.ts::* |
| 8: a compaction's entry rides the bridge's observation, the mirror writes it as its own step after the pending results, the session file renders it where it sat, and a restarted pi starts on a session carrying it | [unit] src/core/harness/pi/bridge.test.ts::the notes — compaction, harness errors, dialogs, the unknown::a compaction is a \compacted` note…, src/core/harness/pi/mirror.test.ts::PiMirror — step records as the runner writes them::a compaction is its own step report…, src/core/harness/pi/mirror.test.ts::piSessionFile — a compaction entry where pi wrote it:😗, src/core/harness/pi/harness.test.ts::runPiHarness — after a bot restart::the explicit replacement protocol preserves compaction entries at their original positions` |
| 8: the way back across a deploy that moved the directory: the first facts name the root beside the pid; a re-attach reads pi's log and feeds its FIFO at the root the row recorded, a previous build's shape, never under this build's own, and removes that root when the run ends; a live pi whose row names no root retains original custody and refuses continuation before any ending or fresh start; the facts reader keeps a row without a root as facts without one and answers no facts for another shape | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — a run on pi from the first file to the answer::writes every file and starts pi under the run's own root directly under /var/tmp…, src/core/harness/pi/harness.test.ts::runPiHarness — after a bot restart::re-attaches at the root the row recorded, a previous build's shape…, src/core/harness/pi/harness.test.ts::runPiHarness — after a bot restart::a row with no recorded root retains original custody…, src/core/harness/contract.test.ts::harnessFactsOf — a row's facts, read by the harness that wrote them::a row naming pi, and a row from before the discriminator, are pi's… |
| 8: the way back across a deploy that restarted the bot while pi lived: the first facts carry the bearer's hash beside the root; a re-attach adopts it so the previous generation's bearer verifies on this generation's proxy beside its own; a row without an adoptable hash retains original custody and refuses continuation before any ending or fresh start | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — after a bot restart::re-attaches only after adopting the bearer pi holds…, ::a row without an adoptable bearer retains original pi custody |
| 8: a live pi with no recorded model wire survives an upgrade and records the run wire on its next save; a present differing wire holds the original without replacement, while a matching wire re-attaches | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — after a bot restart::uses the recorded model wire compatibly… |
8: a pi run handed off after its final answer resumes as a finish plan: no pi starts, the leftover pi is ended at the pid and root the facts name, the answer runs the post-steps | [unit] src/core/dispatch/runLoop.test.ts::a resume with the answer in hand …::on the pi harness no pi is started and the one the previous generation left is ended at its recorded pid and root… |
| 8: the identity script reads the boot id and never fails; the exec container answers one word or nothing (an empty, malformed or failed answer is none); the bot host names no container | [unit] src/core/harness/container.test.ts::the container scripts::reads the log from a byte offset…, src/core/harness/container.test.ts::ExecHarnessContainer — each operation is one command over the executor::identity reads the boot id…, src/core/harness/botHostContainer.test.ts::BotHostHarnessContainer: pi as a child of the bot::names no container… |
| 8: the first facts carry the container beside the pid, root and bearer hash; the facts parser reads it | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — a run on pi from the first file to the answer::writes every file and starts pi under the run's own root…, src/core/harness/contract.test.ts::harnessFactsOf — a row's facts, read by the harness that wrote them::a row naming pi, and a row from before the discriminator, are pi's… |
| 8: a row naming another container is "pi is elsewhere": the pid is neither probed nor ended and the original root is retained; typed held custody prevents any fresh pi or pointer replacement; a row naming this container, or none, is judged by the pid as before and the re-attach records the container; a known original placement without a current identity retains custody before any probe; legacy unbound placement remains observational compatibility only | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — after a bot restart::foreign original pi custody is held…, ::a row whose facts name this very container re-attaches as before…, ::a known original container requires current identity… |
8: a finish plan ends the leftover pi only in the container its facts name; a leftover elsewhere retains typed held custody without ending or workspace release | [unit] src/core/dispatch/runLoop.test.ts::a resume with the answer in hand …::on the pi harness a foreign original finish… |
| 8: a relaunched process takes the run's registration over with its relayed calls kept — a call held across the replacement runs once, the earlier registration's forget is a no-op, its answer reaches an ask through the new registration, the new forget ends the calls; a run not registered is refused by name | [unit] src/core/harness/pi/relay.test.ts::HarnessRegistry.replace and RelayedCalls.awaitInFlight — a relaunched process keeps the run's relayed calls::replace… |
| 8: the calls still running are awaited together up to one window — one that answers inside it is done with its answer, one still running after it is not done, keeps running for an ask by the same id and never ends, and the rebuilt session's words for it are the still-running note; nothing in flight answers at once | [unit] src/core/harness/pi/relay.test.ts::HarnessRegistry.replace and RelayedCalls.awaitInFlight — a relaunched process keeps the run's relayed calls::awaitInFlight… |
| 8: a re-attach adopts and a relaunch rotates — the rotation keeps the run's turns, expiry and span and hands the row the new hash before the old are dropped | [unit] src/core/modelProxy/runBearers.test.ts::RunBearerStore — rotate: a relaunch's new bearer on the run's own meter::* |
| 8: the replaced-container verdict carries the record as pi mirrored it — the seed and the rows the steps carried, every call of the last turn settled with the replaced note, the turn, the inbox seq and the deadline — and leaves the run registered for the relaunch | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — the relaunch in the replacement container::the verdict carries the record as pi mirrored it… |
8: an open told the container was replaced probes, ends and removes nothing at the row's pid and root whatever the container answers for its name, starts pi on the rebuilt session with the relaunch count carried and the container it runs in recorded, and says relaunched in one resumed note | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — the relaunch in the replacement container::an open told the container was replaced probes, ends and removes nothing… |
| 8: a relaunch takes the run's registration over with its relayed calls kept — an answer that landed before pi could read it and one inside the window are the results the rebuilt session carries with the relay's verdict, a straggler gets the still-running note and keeps running for an ask by the same id, the note counts each | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — the relaunch in the replacement container::a relaunch takes the run's registration over with its relayed calls kept… |
| 8: the mirror keeps every row it wrote and each compaction's position among them, wired or unwired, and not a row met again on a re-attach | [unit] src/core/harness/pi/mirror.test.ts::PiMirror — the rows it wrote, kept for a relaunch::* |
8: forget(runId) ends whichever registration holds the run and its calls and is a no-op for a run not registered; answered(callId) hands a relaunch the answer of a call that landed before the dead process could read it, a rejected start as the error answer, and nothing for a call still running, never asked or settled from the record | [unit] src/core/harness/pi/relay.test.ts::HarnessRegistry::forget(runId) ends whichever registration holds the run…, src/core/harness/pi/relay.test.ts::HarnessRegistry.replace and RelayedCalls.awaitInFlight — a relaunched process keeps the run's relayed calls::answered(callId) hands a relaunch the answer of a call… |
| 8: a rebuilt session's settlement turn reaches the ledger — on a bot death's rebuild the first step's user turn is the settlement results with the continue's echo from the seed index; across a relaunch the record a second replacement carries equals the resumed transcript plus the ledger's rows, held once, with a result for every call, and the session's tool results are the ledger's; the mirror primed with the turn writes it before the next step's user content and a compaction after it sits one row later | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — after a bot restart::the explicit replacement protocol rebuilds the transcript…, src/core/harness/pi/harness.test.ts::runPiHarness — the relaunch in the replacement container::the record and the ledger agree across a relaunch…, src/core/harness/pi/mirror.test.ts::PiMirror — the rows it wrote, kept for a relaunch::prime puts a rebuilt session's settlement turn before the next step's user content… |
| 9: the seed rule as a pure split — the last user turn is the prompt and every turn before it the session; a one-turn seed has no session; an empty seed has neither | [unit] src/core/harness/pi/harness.test.ts::the small pure pieces::splitSeed: the last user turn is the prompt and every turn before it is the session; a one-turn seed has no session; an empty seed has neither |
| 9: the prompt is the last user turn rendered — its text parts joined with a blank line into one string, images carried, a document named — and none of the earlier turns; an empty seed is an empty prompt; the ship pipeline's unit contract is such a second text part of the request turn, appended after the request's text and never a turn of its own | [unit] src/core/harness/pi/harness.test.ts::the small pure pieces::promptOf is the last user turn as pi's prompt …, src/core/ship/codingChild.test.ts::withContractInFirstUserTurn — the unit's contract in the child's first turn (item 13)::the first USER turn takes the block as its last text part after the request's text… |
9: a seed with earlier turns starts pi on --session <file> holding them in order as pi's own entries (an assistant's tool call and its result, a document as its note), prompts it with the request alone with its image, counts the mirror from the whole seed and says no resumed note | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — a run on pi from the first file to the answer::a seed with the thread's earlier turns starts pi on a session holding them in order … |
| 9: a seed of one turn starts pi on the bare session directory, writes no session file, and prompts it with that turn entire — the fresh mention and the unit thread, byte for byte as before | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — a run on pi from the first file to the answer::writes pi's files, starts it with the bearer in the env and no key, … |
| 5, 6, live: the five spike tasks through the model proxy with a run bearer as pi's only key, every call metered on the borrowed run's page | [agent] The through-proxy row of load-harness.md. |
| 2 to 8, live: a coding run on the harness end to end | [agent] @switchboard agent:coding in <onboarded repo>: <a small change> → the run page shows the run's tool.bash steps with pi's read/edit/write beside them and model.turn spans with token counts, the card ticks through 💭 thought for …, the thread's reply carries the pull request the post-step opened, the bot's log carries [model-proxy] run=<id> … and [harness] run=<id> authorize … lines and never a bearer; git status in the worktree shows no .pi/ or /var/tmp/switchboard-pi file; a second run in the same thread after deploy restart mid-run continues (a resumed note, then the pull request). |
10: the reach is the identity's — write is the coding preset's, read holds no write bundle, none holds no bundle of pi's own tools at all; a read run's edit, write and submit_pr_description are outside the reach, the reads and the verdict in it; its shell never pushes and never writes to GitHub by CLI or API while every read is allowed; a write run's pushes and GitHub writes are judged by the coding rules alone | [unit] src/core/harness/pi/toolRules.test.ts::judgeToolCall — bundles::the reach is the identity's: write is the coding preset's, read holds no write bundle, none holds no bundle of pi's own tools at all, src/core/harness/pi/toolRules.test.ts::judgeToolCall — a read-identity run::* |
10: outside the bound tracked hosted Review route, a read identity retains the five native read tools and omits edit/write; coding keeps its native tools. Hosted Review instead omits native bash, names run_check in its framing and retains the direct hosted command and read tools. | [unit] src/core/harness/pi/process.test.ts::piLaunchArgs::a read-identity run's allowlist is pi's tools less edit and write, then the relayed ones — a write run's is unchanged, src/core/harness/pi/process.test.ts::piLaunchFiles::a read-identity run's note names the five read tools, says the run has no edit or write, and never maps write_file onto one, src/core/harness/pi/process.test.ts::hosted Review launch policy::* |
| 10: the harness reads the preset's identity once — the allowlist, the note in SYSTEM.md and the rules the gate judges by; a write preset's rules carry the write identity | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — a read-identity preset::* |
10: the gate under a read identity refuses edit, write and a push with a tool_refused note each and allows read, an ordinary command and the relayed verdict | [unit] src/core/harness/pi/relay.test.ts::authorizeToolCall — a read-identity run::* |
10: a relayed submit_verdict is the native tool's own call — the run's onVerdict receives the verdict the native call yields, the model reads the same acknowledgement, the post-step's body is the same with LGTM: first; a malformed verdict is the same error and reaches no run | [unit] src/core/harness/pi/relay.test.ts::runRelayedTool — the verdict path::* |
10: a tracked, bound review runs on pi through the hosted command policy — no native bash, edit or write on its allowlist, the direct recorded command and readonly tools relayed, the matching note in the framing, a write refused with a tool_refused note, and the relayed verdict posted as LGTM: behind the reviewed-head guard with review_posted and reviewPost on the record | [unit] src/core/dispatch/runLoop.test.ts::the pi harness — the review preset::* |
| 10, live: the review task set on pi through the model proxy — verdicts recorded, no write calls | [agent] The review suite row of load-harness.md, through --through-proxy: every task settled, every verdict in the house shape naming the reviewed head, every call vetted, no write tool ran, the checkout untouched, verdict PASS. |
| 10, live: a review run on the harness end to end | [agent] @switchboard agent:review <a pull request of an onboarded repository> → the run page shows pi's read and hosted run_check steps for a ready tracked review and no edit/write, model.turn spans from the proxy, the submit_verdict tool call and its result; the pull request carries the comment with the verdict line first (LGTM: or Changes requested:) and no approval; the record carries review_posted and reviewPost; the bot's log carries [harness] run=<id> authorize … lines and never a bearer. |
| 10, 14: a substantive head move mid-review preserves the saved conversation: an in-place move uses one process and two prompts; checkout replacement confirms the original ending and starts one successor with a persisted post-turn clock; the worktree moved first, the second verdict posted pinned to the new head, the second answer the run's, pi ended after the settle | [unit] src/core/dispatch/runLoop.test.ts::the pi harness — the review preset::a substantive head move mid-review on pi preserves… |
14: the open form hands the loop's answer back with pi alive and registered on the relay; a follow-up prompts the same session with the caller's text, its tool call is on the stream under a run.agent of the caller's span, the relayed tools read the turn's context and the run's again after, its steps are mirrored, and the end kills pi and removes the root once | [unit] src/core/harness/pi/harness.test.ts::runPiHarnessOpen — the session stays open for one more turn::hands back the loop's answer with pi alive… |
| 14: a follow-up's budget is its own, not the run's — past its turn cap the turn-guard write-up is steered with every tool refused, the note is on the stream and the answer wears the guard's label | [unit] src/core/harness/pi/harness.test.ts::runPiHarnessOpen — the session stays open for one more turn::a follow-up is bounded by its own budget… |
14: the failure modes — a prompt pi refuses throws, the turn's run.agent ends error and the session still ends; a hard stop mid-turn aborts pi and answers the hard-stop message; pi dying mid-turn throws naming it; a session already ended takes no follow-up and sends nothing; a loop that throws ends its own run.agent error | [unit] src/core/harness/pi/harness.test.ts::runPiHarnessOpen — the session stays open for one more turn::the failure modes…, ::a session that has ended takes no follow-up…, src/core/harness/pi/harness.test.ts::runPiHarness — a run on pi from the first file to the answer::a pi that dies before settling fails the run naming its last stderr… |
14: a soft stop mid-follow-up is honoured as the loop honours it — the soft-stop write-up steered with every tool refused, the stopped note saying soft, no abort, the answer under the ⏹ label | [unit] src/core/harness/pi/harness.test.ts::runPiHarnessOpen — the session stays open for one more turn::a soft stop mid-follow-up is honoured as the loop honours it… |
14, pr-description 5: the description turn is a prompt on the run's session — the follow-up text, the clipped budget (never the shared def's), the turn's tool context with the hook under the caller's span; the description the relayed tool submits reaches the hook and the return value; a turn that submits nothing reports nothing, a refused prompt is logged and reports nothing; without a session (a finish plan) the turn asks nothing — one note saying no session, nothing submitted, no throw | [unit] src/core/descriptionTurn.test.ts::runDescriptionTurn — one clipped prompt on the run's own pi session (harness-pi item 14)::* |
14, agent-review 5: the verdict turn is a prompt on the run's session in the same shape — the follow-up text, the clipped budget, the turn's tool context with the onVerdict hook under the caller's span; the verdict the relayed tool submits reaches the hook and the return value; nothing submitted or a refused prompt reports nothing; without a session the turn asks nothing | [unit] src/core/verdictTurn.test.ts::runVerdictTurn — one clipped prompt on the run's own pi session (harness-pi item 14)::* |
14, agent-review 12: the head-move re-review is a prompt on the run's session — the worktree moved first, the follow-up text as the appended user turn, the round's budget, the turn's own verdict capture, the head re-probed after; a refused prompt propagates and fails the run; without a session (a finish plan) a substantive move is not re-reviewed — no worktree move, a head_moved note saying the session is gone, the verdict and the reviewed head kept, the observed head re-read | [unit] src/core/reviewRound.test.ts::settleReviewedHead — the head-move re-review::* |
14, end to end: a coding run whose push landed on an open pull request without a description gets its description turn as a prompt on the same session — one process, two prompts, the relayed submit_pr_description lands, the pull request is edited, the note and the description are on the record, pi ended once after the turn | [unit] src/core/dispatch/runLoop.test.ts::the pi harness — every preset's runs, in the run's container::a coding run on pi whose push landed on an open PR without a description… |
| 14, live: the two post-turns on a production pi session | [agent] On a deployment at this release: (a) a coding run that pushes onto a branch heading an open pull request and ends without submit_pr_description → the run page shows the description_turn note, then a second prompt's tool calls (github_pull_get, submit_pr_description) with no second pi process in the container (pgrep -f pi during the turn names the loop's pid), pr_description and pr_opened { created: false } on the record and the pull request's body re-rendered at the pushed head; (b) a review during which the pull request gets a new commit → the 🔀 note in the thread, head moved → on the card, the head_moved note and one more run.agent under run.settle_reviewed_head on the run page, one pi process for an in-place move, or a confirmed original ending followed by one owned successor for checkout replacement, and a single GitHub review pinned to the new head with the second verdict. Receipts on the tracker. |
11: a review run's pi is served the readonly toolset's submit_verdict as the native tool declares it — the description naming git rev-parse HEAD, the schema requiring head beside the verdict and the summary | [unit] src/core/harness/pi/relay.test.ts::relayedToolDefinitions::a review run's pi is served the readonly toolset's submit_verdict as the native tool declares it … |
11: a relayed verdict naming no head is neither refused nor stamped — the run's onVerdict receives what the native call yields, head absent; the guard fails closed on it when nothing was observed and the observed HEAD alone rescues it | [unit] src/core/harness/pi/relay.test.ts::runRelayedTool — the verdict path::a relayed verdict naming no head is neither refused nor stamped … |
11: the load driver's extension registers submit_verdict and submit_pr_description as the native definitions field for field, head required | [unit] src/load/piExtension.test.ts::piExtension::registers the two terminal tools as the native definitions field for field … |
| 12: the bot-host container: the run's root made with mkdtemp beside the predictable one, exclusive and 700, the usual layout, two calls two roots; a file written under it with parents at 700 and the file at 600, exact bytes, a path already present refused and never replaced; pi started in the run's directory with the arguments as given, PATH and HOME and the start's env and no other host variable, the directories at 700, the logs emptied, the pid recorded, alive until ended; a line fed to pi's stdin and its answers read from the log as exact bytes from an offset, the streaming deltas never landing, a long line whole, stderr what tail reads; a pi that exits is found dead with its last records in the log and a later line a named failure; kill ends pi and its group, TERM then KILL, idempotent for an ended pid and for one never started here; alive is no for a pid this container did not start; remove takes the root down as one tree, idempotent; every path under the run's own root directly under /var/tmp; a pi that cannot be started and a line for a run with no pi are named failures; a log the host cannot write ends pi rather than the bot, with the error log still readable and the root removable; a pi spawned but not tracked is ended before start fails | [unit] src/core/harness/botHostContainer.test.ts::BotHostHarnessContainer: pi as a child of the bot::* |
| 4, 8, 12: the harness files a fresh run under the root the container makes, records it on the facts and removes it when the run ends; the fake and the exec container answer the predictable root, the exec container without running a command | [unit] src/core/harness/pi/harness.test.ts::runPiHarness: the root the container makes::*, src/core/harness/container.test.ts::ExecHarnessContainer.makeRoot::answers the root the harness proposed, without running a command |
8, 12: the session file's working directory is the container's answer for the root pi is filed under: the bot host answers the run's own root, the directory start spawns pi in and one present on the host, whatever checkout it is handed, and a second root as its own; the exec container answers the checkout without running a command; an explicit replacement-protocol session names the current host root without deleting the prior root, a seed session on the bot host names that root too, and on the exec container the session names the checkout as before | [unit] src/core/harness/botHostContainer.test.ts::BotHostHarnessContainer: pi as a child of the bot::cwd answers the run's root, whatever checkout the harness names: the directory start spawns pi in, present on this host, so a session written for that root names a directory pi finds when it resumes, src/core/harness/container.test.ts::ExecHarnessContainer.cwd::answers the checkout the harness names, where the executor runs every command and so the process, without running a command, src/core/harness/pi/harness.test.ts::runPiHarness: the root the container makes::the explicit replacement protocol uses the current host root as session cwd and retains the prior root, ::a fresh run on the bot host with the thread's earlier turns starts on a seed session whose working directory is that root too, ::on the exec container the session's working directory is the checkout the harness names, where the executor runs pi, as before |
12: the container follows the run's machine class: over the executor for a class with a workspace, the bot host for none | [unit] src/core/harness/botHostContainer.test.ts::harnessContainerFor: the container follows the run's machine class::a class with a workspace gets the container over the run's executor; \none` gets the bot host` |
| 12: a run without a workspace holds none of pi's own tools: the allowlist is the relayed tools alone; its note says it has no workspace and none of pi's own tools, that a call to any is refused, names the relayed tools and maps no native name onto one; models.json points at the loopback URL it was given | [unit] src/core/harness/pi/process.test.ts::piLaunchArgs::a run without a workspace (identity none) holds none of pi's own tools: the allowlist is the relayed tools alone, src/core/harness/pi/process.test.ts::piLaunchFiles::a run without a workspace is told it has none of pi's own tools and that a call to any is refused, names its relayed tools, and maps no native name onto one; models.json points at the loopback URL it was given |
| 12: the rules refuse every one of pi's own tools by name for a run without a workspace, whatever the call asks, and the load driver's terminal tools and an unknown tool with them | [unit] src/core/harness/pi/toolRules.test.ts::judgeToolCall: a run without a workspace (identity none)::* |
12: the gate under identity none refuses pi's own tools by name with a tool_refused note each and allows the relayed tools | [unit] src/core/harness/pi/relay.test.ts::authorizeToolCall: a run without a workspace (identity none)::* |
12: a general ask runs on pi on the bot host: the container asked for by the none machine class, pi reaching the bot over loopback, the allowlist the assistant toolset's relays and none of pi's own tools, the note saying so, a shell pi asks for refused by name on the record, the relayed update_status run in the bot, pi's answer the run's, pi ended and its root removed; a process without the loopback URL fails such a run naming PORT | [unit] src/core/dispatch/runLoop.test.ts::the pi harness — a preset without a workspace, as a child of the bot::* |
| 12: the bot image installs pi in the runtime stage as root before the switch to the bot's user, and the three images carry one pi | [unit] src/deploy/imagePiHarness.test.ts::the bot image::*, src/deploy/imagePiHarness.test.ts::the three images carry one pi::* |
12: the research preset on pi: the served definitions are exactly the web toolset's (submit_research, web_fetch, web_search, update_status, the GitHub reads, recall and notes) and none of pi's own; the gate allows web_fetch and web_search by name and refuses bash, edit and a tool outside the toolset with a tool_refused note each; a relayed web_fetch is the native tool run in the bot under its own URL guard, an internal address refused without a fetch as the content pi reads; a tool outside the toolset asked of the relay is named unknown | [unit] src/core/harness/pi/relay.test.ts::the research preset on pi: the web toolset relayed, run in the bot as the requesting user::* |
12: a research ask runs on pi on the bot host: the container asked for by the none class, the loopback URL, the allowlist the web toolset's relays and none of pi's own tools, a shell refused by name, the relayed web_fetch run in the bot under its own URL guard, pi's answer the run's, pi ended | [unit] src/core/dispatch/runLoop.test.ts::the pi harness — a preset without a workspace, as a child of the bot::\harness: { research: pi }` runs a research ask on pi on the bot host…` |
12: the conductor runs on pi on the bot host with the conductor toolset relayed: a coding spawn refused spawn_identity in the tool result and starting nothing, a research spawn reaching spawnChild with the text turns of the conversation the session log holds so the child is dispatched with seed set, pi's answer the run's; and end to end through dispatch() with the ledger's session log, the child receives a focused parent brief with the original request and retained source dependencies, then recalls the exact omitted assistant/tool exchange through its actual parent session capability; a pending spawn result outside the frozen range remains unknown; the child's record says seed: parent, the relayed await_runs returns its reply, and the parent's answer carries all three | [unit] src/core/dispatch/runLoop.test.ts::the pi harness — a preset without a workspace, as a child of the bot::\harness: { conductor: pi }` runs the conductor on pi on the bot host…, src/core/dispatcher.test.ts::agent:conductor…::`harness: { conductor: pi }` runs the conductor on pi as a child of the bot…` |
| 12, live: a research run and a conductor run on pi as children of the bot end to end | [agent] @switchboard agent:research <a question the web answers> → the run page shows relayed web_search or web_fetch steps and model.turn spans from the proxy, no read or bash step, and the answer in the thread; then @switchboard agent:conductor <a request with two read parts and one that needs a code change> → two child threads open, each child's record says seed: parent with the conductor's text turns as its context, the coding part's spawn_run step shows spawn refused (spawn_identity) in its result and no third thread opens, the await_runs step lasts as long as the slower child and the bot's log carries [harness] run=<id> tool await_runs → pending lines under it, and the conductor's answer compiles both children's replies. |
| 12, live: a general run on pi as a child of the bot end to end | [agent] @switchboard agent:general <a question the org's GitHub answers> → the run page shows the relayed github_* or web_fetch steps and model.turn spans from the proxy, no read or bash step, and the answer in the thread; the bot's log carries [model-proxy] run=<id> … and [harness] run=<id> authorize … lines and never a bearer; ls /var/tmp in the bot container shows no switchboard-pi-<id> once the run is over; ordinary bot-generation continuation with recorded original facts is held, not proven by this fresh-run procedure; no restart or native-restoration acceptance is claimed. |
| 13: the table — one provider per block in config order, an unknown name and a baseUrl-less compatible block refused in the registry's words; each block one API at its base with the trailing slash stripped; the model named after the block with the request's cap and a zero rate card | [unit] src/core/harness/piAi.test.ts::PiAiProviders — the provider table config.yaml names, built on pi's model library::* |
13: on the wire through pi's own adapters — Anthropic: the system prompt, the one user turn, the tool with its schema, tool_choice: {type: "tool", name}, max_tokens, the key as x-api-key; OpenRouter: a system message, tools as functions, the function choice, max_completion_tokens, the key as a bearer; a keyless block sends no authorization header; a text answer as text and end_turn, a cut answer as max_tokens; an unset variable fails by name before any request and an anthropic block without one reads ANTHROPIC_API_KEY; a provider error names the provider and the model; a local abort remains an AbortError; a scripted API receives the model, the normalized transcript (system prompt and tools in its leading system message) and the options and its message comes back mapped | [unit] src/core/harness/piAi.test.ts::complete — one request through pi's own adapter, on the wire::*, ::the adapter seam — a scripted pi API stands in for the wire::* |
| 13: an automatic tool choice disables parallel calls on Anthropic, Chat Completions and Responses while allowing a text-only answer. Requests without tools retain their original payload. | [unit] src/core/harness/piAi.test.ts::complete — one request through pi's own adapter, on the wire::automatic tool choice disables parallel calls on Anthropic and still permits a text answer, ::automatic tool choice disables parallel calls on Chat Completions without forcing a call; src/core/dispatch/operatorResponses.test.ts::the direct operator on the Responses wire::repairs the specific severity evidence failure without changing the review:* |
13: the vocabulary both ways — the context (system, text and images, the document note, assistant text, calls with JSON-only object arguments and thinking, tool results named after their calls, the clock's timestamps, tools as parameters, nothing for an absent system prompt or tool list), the options per dialect (the key or the keyless convention, the cap, the signal, the retention, the forced tool), the result (content in order, the stop reasons, the four counters; provider error thrown by typed cause, local abort thrown as AbortError without a provider-health claim) | [unit] src/core/harness/piAi.test.ts::toPiContext — the completion vocabulary in pi's shape::*, ::piStreamOptions — what rides beside the context, per dialect::*, ::fromPiMessage — pi's assistant message as the completion result::* |
13: the example config's OpenRouter block, live as shipped, is one openai-completions provider on pi's library, same base, trailing slash stripped; @earendil-works/pi-ai is pinned in both root and published package manifests at exactly the pi pin | [unit] src/config.test.ts::the example config's provider blocks::the OpenRouter block ships live: the example loads with its configured review and work providers, and pi's library builds one openai-completions provider from it…, src/deploy/imagePiHarness.test.ts::the three images carry one pi::* |
13: the proxy's meter reads each wire shape's usage — Anthropic's input_tokens/output_tokens with both cache counters, Chat Completions' prompt_tokens/completion_tokens with the cached-prompt detail — and reads nothing from an absent or malformed usage | [unit] src/core/modelProxy/usage.test.ts::* |
15: RUNAWAY_TURNS_PER_MINUTE is 6 and runawayTurnCap multiplies; every preset but ship has maxTurns === runawayTurnCap(maxMinutes) (coding 540, review 150, research 48, general 360, explore 720 and conductor 8640); ship keeps 1 | [unit] src/agents/registry.test.ts::the turn cap is a runaway guard derived from the wall clock…::* |
15: the asks are one table — coding 90, review 25, research 8, general 60, orchestrator 10, explore 120, conductor 1440, ship 1440 — and the floors coding and findings 15, review 7, the orchestrator 4, the merge wait 10; every loop-running preset's maxMinutes and maxTurns are read from it, and every child floor holds its write-up, post-step and a model minute while remaining at most its ask | [unit] src/core/budgets.check.test.ts::the budgets module — one table every wall clock derives from…::*; src/core/budgets.check.test.ts::the derivations the registry reads from the module::every loop-running preset's maxMinutes is its ask…; src/core/budgets.check.test.ts::the lease minimum — the least lease whose loop has a minute after the write-up and the post-step::every child and Ship review floor holds its write-up… |
| 15: the reserve before a round is the floors plus provisioning of every later review and findings round and the merge floor — 76 before the coding round at three review rounds (104 at four), 66, 48, 38, 20, 10, 0 before the rounds after — and a floor that moves moves the reserve | [unit] src/core/budgets.check.test.ts::the reserve — derived over the rounds that must follow, never tabled::* |
15: carve computes a round's minutes — the remainder minus the reserve, capped at the ask, refused under the floor reporting what was left and never a negative number, the merge wait and a conductor's child included | [unit] src/core/budgets.check.test.ts::carve — a round's minutes from the parent's remainder, refused under the floor::* |
15: the stack fits inside every ask — writeUp + postStep(preset) + execCall ≤ ask(preset), a post-step of 0 where the preset runs none, and a command is refused before the write-up begins | [unit] src/core/budgets.check.test.ts::the stack — the write-up, the post-step and the exec margin fit inside every loop-running preset's ask::* |
15: a clipped budget re-derives the guard — budgetedAgent on a 90-minute preset clipped to 10 minutes yields maxTurns 60 | [unit] src/config/profile.test.ts::a 90-minute preset clipped to 10 minutes gets 60 turns — the runaway guard follows the clipped budget… |
15: the wrap-up warning is steered once as the deadline nears; at the deadline the write-up is steered in the loop's words with every tool refused and the answer wears the N-minute label; the turn guard fires at the cap with the pace in the note, the instruction and the answer, never the word budget; a write-up that never comes is bounded and the answer is the reason alone; a preset's budget is the one the harness keeps (a clipped run's clip reaches the harness's def) | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — a run on pi from the first file to the answer::the wrap-up warning is steered once as the loop's end nears — the lease's end less the write-up and the post-step it holds back, ::at the loop's end the write-up is steered with the loop's words, every tool is refused meanwhile, the answer carries the budget label, the lease event names the start, the end and the cut, and the bearer expires a minute past the lease, ::the turn guard fires at the cap with the pace in the note, the instruction and the answer, ::a write-up that never comes is bounded by its allowance: pi is aborted three minutes after the steer and the answer is the reason alone, src/core/dispatcher.test.ts::boundaries: the profile gate, before any executor exists::a budget above the cap is clipped, never refused… |
| 15: the loop's clocks — a coding lease of 90 ends its loop at 82 with the warning at 79; a preset without a post-step holds back the write-up alone; a lease shorter than its hold-back has no loop time; a follow-up turn holds nothing back; the post-step's minutes are carved from the remainder with a one-minute floor; a turn's lease is the lesser of its ask and the remainder; the bearer outlives the lease by one minute and the mint's expiry is provisional | [unit] src/core/budgets.check.test.ts::the loop's clocks — the loop ends inside the lease, so the write-up and the post-step fit::* |
6: a tool call in flight at the loop's end is cut on pi — the budget note names it, one tool_cut note says why, the call's result lands as a failure marked cut (the workspace's release reads it as a command that may run on, harness.md item 13; a tool that exited clean before pi handled the abort settles unmarked), the cut's abort closes the turn the tool was in as an aborted model call, which the harness books as the cut's own (no harness_error, no failed write-up), the write-up is the model call that follows and the run answers under the budget's label before the lease ends; the same row on OpenCode, whose loop interrupts the hung step and queues the write-up, the cut tool's own late outcome landing marked cut (harness.md item 13) | [unit] src/core/harness/conformance.test.ts::conformance — pi::conversation: a tool call in flight at the loop's end is cut…, src/core/harness/conformance.test.ts::conformance — opencode (fake serve, gate and record)::conversation: a tool call in flight at the loop's end is cut…, src/core/harness/pi/bridge.test.ts::the bridge's spans — one tool.<name> per call under run.agent, ended with the call's outcome::a call an abort was sent for ends marked…; src/core/harness/conformance.test.ts::the table::the matrix prints every harness × row… |
| 15: on both harnesses the loop ends inside the lease: the lease event names the start, the end and the cut, the write-up is steered at the cut with the in-flight call named, and every event of the run lands before the lease ends | [unit] src/core/harness/conformance.test.ts::conformance — pi::conversation: the loop ends inside the lease…, src/core/harness/conformance.test.ts::conformance — opencode::conversation: the loop ends inside the lease… |
15, 7: on pi a scripted run with its clock short of the loop's end issues a bash whose timeout reaches past the end and reads the refusal as the call's result — one tool_refused note in the wind-down's sentence with the seconds left, no budget note, no cut — and its next bash, timeout inside the end, runs; OpenCode declares the row cannot, its ask carrying no timeout, and the table asserts that failure | [unit] src/core/harness/conformance.test.ts::conformance — pi::gate: a bash call whose explicit timeout reaches past the loop's end…, src/core/harness/conformance.test.ts::conformance — opencode::gate: a bash call whose explicit timeout reaches past the loop's end… — declared cannot… |
| 14: a follow-up turn marks the tools it may call on the run's bearer entry for its duration and clears them after; the description and verdict turns name their read-and-submit tools | [unit] src/core/harness/pi/harness.test.ts::runPiHarnessOpen — the session stays open for one more turn::a follow-up turn marks the tools it may call…, src/core/descriptionTurn.test.ts::runDescriptionTurn — one clipped prompt on the run's own pi session (harness-pi item 14)::publishes the description_turn note and prompts the session once…, src/core/verdictTurn.test.ts::runVerdictTurn — one clipped prompt on the run's own pi session (harness-pi item 14)::publishes the verdict_turn note and prompts the session once… |
| 14: the description turn and the verdict turn carve their minutes from the lease's remainder — twenty minutes left give 5 and 3, one minute left gives one — and the allowance stands when no remainder is handed | [unit] src/core/descriptionTurn.test.ts::runDescriptionTurn — one clipped prompt on the run's own pi session (harness-pi item 14)::the turn's minutes are carved from the lease's remainder…, src/core/verdictTurn.test.ts::runVerdictTurn — one clipped prompt on the run's own pi session (harness-pi item 14)::the turn's minutes are carved from the lease's remainder… |
15: the wind-down owns the ending — a model call in flight at the budget that dies aborted ends the run by the budget answer naming the failed call, with a harness_error note, never as a failed model call, and the time_budget_exhausted note says what the run was doing; the bridge says the model call only while pi has a turn open, the tools by name while they run, and nothing between turns; an operator's soft stop whose in-flight call then fails still counts as stopped — the answer names the failure and the follow-up is not run | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — a run on pi from the first file to the answer::a model call in flight at the budget ends by the wind-down, not an aborted call: the budget answer stands, the failure is a note, and the budget note says what the run was doing, src/core/harness/pi/bridge.test.ts::turns, narration and the answer — the loop's rules::doingNow: nothing before a turn, the model call once pi opens a turn, the open tools by name while they run, nothing again between turns, src/core/dispatcher.test.ts::thread admission (docs/reference/specs/thread-admission.md)::a run that THREW after an operator stop was requested still counts as stopped: its follow-up is not run |
| 15: a resumed run whose loop had ended under a budget or a stop before the restart wears that ending's label, read back from its notes | [unit] src/core/dispatch/runLoop.test.ts::a resume with the answer in hand (the \finish` plan)::the ending is read from the run's notes, not from this generation's control…` |
| 6: the finale answer reads what the ending established — a clean tree with a pushed head names the head and the submitted description and drops the partial-work clause and the advice; a salvaged tree names the branch and the head as where a follow-up starts; work left names its counts and its fate (kept for the thread, discarded at the run's end, torn down) and is never said to exist in a discarded or torn-down tree; an unmeasured workspace and a run with no workspace say so; only an unread tree keeps the harness's guess; the failed write-up's clause stays beside the facts; the turn guard, the soft stop and the unlabelled endings read the same facts with their own reason and advice; the harness's own words stand with no facts | [unit] src/core/harness/windDown.test.ts::windDownAnswer — the finale answer reads what the ending established (harness-pi item 6)::* |
| 6: through the run loop the answer is composed after the salvage, the description turn and the PR post-step — a coding child at its budget marks even a clean, already-pushed tree with a WIP checkpoint and an empty write-up names that checkpoint and the submitted description, the record's answer event carrying the same words; a salvaged tree names the salvage's branch and head; work the salvage could not push names its counts and its discard | [unit] src/core/dispatch/runLoop.test.ts::the pi harness — every preset's runs, in the run's container::a coding child at its time budget marks a clean, already-pushed tree as WIP and names the checkpoint and submitted description, src/core/dispatch/runLoop.test.ts::the pi harness — every preset's runs, in the run's container::a coordinator's coding child at its time budget commits and pushes its tree to the unit's branch and says so; with the plan's base unknown the salvage is skipped, nothing is pushed, and the note says why |
| 6: every harness hands the run loop the budget's ending as a structured ending — the kind, the text, the failed call — and the composer says the same words over it on pi and OpenCode for every branch: clean and pushed, salvaged, unpushed and kept, unpushed and torn down, and the write-up's text under a label that carries the facts | [unit] src/core/harness/conformance.test.ts::conformance — pi::conversation: the budget's ending is handed to the run loop as a structured ending…, src/core/harness/conformance.test.ts::conformance — opencode::conversation: the budget's ending is handed to the run loop as a structured ending…, src/core/harness/conformance.test.ts::conformance — pi::conversation: the budget's ending carries the write-up's text to the run loop…, src/core/harness/conformance.test.ts::conformance — opencode::conversation: the budget's ending carries the write-up's text to the run loop… |
16: the resident's ExecSandboxRestartedError from the alive probe is a replaced container — the call in flight is settled on the record with the restart note and its span ends in error, the sandbox_restarted note names both containers and the executor's words, the loop throws PiContainerReplacedError and never pi exited before the run settled, nothing is killed or removed in the container the executor reaches now, the run stays registered on the relay for the relaunch and the facts still name pi's container | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — the container replaced under a live run::the resident's ExecSandboxRestartedError from the alive probe is a replaced container… |
16: the executor's word is the condition whatever the container answers for its name — runtime-unreachable: from the log read in a container that names itself as before, runtime-replaced on a line into the FIFO surfacing at the next read — with nothing killed or removed; any other failed read is the failure it was; saysContainerReplaced rejects structured command failures as infrastructure evidence, then reads the typed word first (ExecSandboxRestartedError, HarnessContainerRuntimeReplacedError) and then runtime-replaced or runtime-unreachable anywhere in a failure's text, behind the exit prefix, the seam's wrap or the resident client's own sentence, and never a failure without it nor a bare string; the seam raises an answer carrying the word as the typed HarnessContainerRuntimeReplacedError, never as dead or log bytes, wherever the word sits in the answer, while a failure without the word is the plain failure it was | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — the container replaced under a live run::the executor's word is the condition whatever the container answers for its name…, ::saysContainerReplaced reads the executors' typed word first…, src/core/harness/container.test.ts::stdoutOf — an executor's answer as the operation's stdout::runtime words in stdout are data; typed replacement errors remain the replacement condition |
16: the resident's answers after a roll as the executor really hands them to the seam — the real ResidentExecutor over a resident Worker that answers as the real one does, through ExecHarnessContainer and the harness: the container replaced between two polls (pi's own bash inside it, nothing of the bot's in flight) reaches the seam as the preflight's worktree-missing and is the typed restart at once with no re-attach, the run ending by the redispatch path with the call settled and the sandbox_restarted note naming both containers and never as a dead pi in the replacement; a log read in flight when the runtime is swapped (runtime-replaced) is the typed restart at once, the recovery attach the client used to make — refused image-stale while the replacement reconciles — no longer standing in for the verdict; nothing is killed or removed in the replacement | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — the resident's answers after a roll, as the executor hands them to the seam::* |
| 16: a probe or a read that throws ends the transport's stream with that error for the harness to judge, and pi is not read as dead | [unit] src/core/harness/pi/transport.test.ts::PiRpcTransport::a probe or a read that throws ends the stream with that error, and pi is not read as dead |
16: a pi found dead without the executor's word, in a container that answers the same word on the one more command, keeps the failure it always was — pi exited before the run settled with the error log's tail, no sandbox_restarted note, pi ended and its root removed — and a name missing on either side, or a probe that failed for a reason of its own, judges nothing | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — the container replaced under a live run::a pi found dead without the executor's word, in a container that answers the same word… |
16, one more command before the judgement: a pi found dead before any command returned the executor's word takes one identity command — failing with the word, the replaced verdict with the record, the call in flight settled with the restart note and its span ended, one sandbox_restarted note, nothing killed or removed, the run left on the relay; answering another identity than the one recorded when pi started, the verdict by the changed identity with said nothing and condition tagged identity, the note carrying the identity's sentence; answering the recorded identity, the crash judgement standing on every harness — no verdict, no note, the process ended and its root removed; the seam's helper alone judges the word, the changed identity and no verdict for the rest; both harnesses through the conformance rows, and OpenCode end to end against the fake serve with the same-word death keeping its crash | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — the container replaced under a live run::a pi found dead before any command returned the executor's word takes one more container command…, ::the one more command answering another identity than the one recorded when pi started…, src/core/harness/container.test.ts::replacedVerdict — one more container command before a dead process is judged to have died where it ran::*, src/core/harness/conformance.test.ts::conformance — pi::survival: the process is found dead with a tool call in flight and no command has returned the executor's word…, ::survival: the process is found dead with a tool call in flight, no command has returned the executor's word, and the one more container command answers another identity…, ::survival: the process is found dead with a tool call in flight, no command has returned the executor's word, and the one more container command answers the identity recorded…, src/core/harness/conformance.test.ts::conformance — opencode::survival: the process is found dead with a tool call in flight and no command has returned the executor's word…, ::survival: the process is found dead with a tool call in flight, no command has returned the executor's word, and the one more container command answers another identity…, ::survival: the process is found dead with a tool call in flight, no command has returned the executor's word, and the one more container command answers the identity recorded…, src/core/harness/opencode/bridge.test.ts::an OpenCode found dead before any command returned the executor's word — one more container command before the crash judgement::* |
16, the third failure shape: a container command that fails on its transport with no word (the read's WebSocket closed with 1006 as the platform killed the container, the executor's infra failure) takes the one more identity command — failing with the word, the replaced verdict with the record, the call in flight settled and its span ended, one sandbox_restarted note, nothing killed or removed, the run left on the relay; answering another identity, the verdict by the changed identity; answering the recorded identity with a proven dead PID, on any backend, leaves the failure standing NAMED as the transport error it was — never pi exited before the run settled — with a harness_error note saying the one more command named no replacement, no sandbox_restarted note, pi ended and its root removed, exactly one more command asked — while a still-live recorded PID on any backend re-attaches in place and an unanswerable one fails closed; and the one more command waiting through a container that is down — The container is not running from the probe re-sent with the harness's sleep after the first pauses of PROBE_WAIT_BACKOFF_MS (two down answers, so 5 s then 10 s of its 5 s, 10 s, 15 s), the notes saying the wait began and how long the container took to answer, the same identity then leaving the failure standing and the word then the verdict — both harnesses through the conformance rows; a hard stop requested while the container is down ending the wait at once and the run as the stop (the abort line, one stopped note in mode hard, exactly one probe, no verdict); the word or a transport loss met while the finale was being aborted noted and never judged, the budget answer standing; and a follow-up turn meeting the three shapes as the loop does — the verdict thrown from the turn, the sandbox-backed failure standing named with the note and the resident-backed turn also failing when the PID is proven dead, a hard stop ending the turn's wait aborting pi's turn in flight | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — the container replaced under a live run::the wait observes the run…, ::the wind-down owns the ending…, ::a follow-up turn meets the three shapes as the loop does…, ::a follow-up turn's wait observes the run as the loop's does…, ::a hard stop that ends a follow-up turn's wait also aborts pi's turn in flight…, ::the abort reaches pi even when the turn's own failed write spent the transport's chain…, ::the loop's wait ended by a hard stop aborts pi too…, ::after a follow-up turn's wait only the hard stop is read…, ::a control file that vanished under a follow-up turn is noted exactly once…, src/core/harness/pi/transport.test.ts::PiRpcTransport::an abort is one step on the chain…, src/core/harness/pi/harness.test.ts::runPiHarness — the container replaced under a live run::a container command that fails on its transport with no word…, ::a transport loss whose one more command answers another identity…, ::a transport loss whose one more command answers the identity recorded, on a sandbox-backed run, leaves the failure standing…, ::a resident-backed run also refuses to relaunch after /exec loss…, ::the one more command waits through a container that is down…, src/core/harness/conformance.test.ts::conformance — pi::survival: the in-flight container command fails on its transport with no word (the platform kills the container under it and the WebSocket closes with 1006 before any word) and the one more command fails with the word…, ::survival: the in-flight container command fails on its transport with no word and the one more command answers another identity…, ::survival: the in-flight container command fails on its transport with no word and the one more command answers the identity recorded…, ::survival: the in-flight container command fails on its transport with no word and the one more command finds the container not running…, src/core/harness/container.test.ts::saysTransportLost — the third failure shape: a container command failed on its transport with no word::*, src/core/harness/container.test.ts::replacedVerdict — the one more command waits through a container that is down (the restore window), bounded like the start gate::* |
16, cold sandbox /exec loss: a recorded PID that answers alive after the lost command keeps the same pi and its consumed log boundary only when the container's recorded identity was confirmed unchanged; a missing recorded identity, a missing answer or a failed identity probe leaves liveness unresolved even when a new container reuses the recorded PID, so no re-attach or relaunch is authorized; a prompt in flight is held until its echo, and a missing echo after the bound fails instead of sending the request twice; an un-echoed steer, gate reply or control command after lost /exec fails rather than risking replay (control-reset keeps its existing id-deduplicated reply); OpenCode's server store and pending asks similarly resolve lost HTTP writes; repeated read losses without progress stop at the in-place bound, while down/unknown PID probes stop by the deadline or hard stop without manufacturing a replacement | [unit] src/core/harness/conformance.test.ts::conformance — pi::survival: a cold sandbox loses /exec with Network connection lost while the recorded pid stays alive…, src/core/harness/pi/harness.test.ts::runPiHarness — the resident's control plane reset under a live pi::a cold sandbox loses /exec while a prompt write is in flight…, src/core/harness/reattach.test.ts::classifyTransportLoss — a cold sandbox's lost exec is not a dead process::does not reattach to a reused pid when a recorded container identity cannot be confirmed, src/core/harness/conformance.test.ts::conformance — opencode::survival: a cold sandbox loses /exec and the identity probe returns no name…, src/core/harness/reattach.test.ts::HeldSends — the one gate every write to pi takes (harness-pi item 16)::a prompt unresolved after /exec transport loss waits for its echo and fails closed instead of resending on silence, src/core/harness/reattach.test.ts::resolveControlResetWrite — an in-flight write after /exec loss::fails closed on an un-echoed steer, gate reply or control command after transport loss; a matching id echo proves delivery |
16: the run stage reads the verdict by type and relaunches — a living bot, the typed error from the log read: pi relaunched in the replacement from the record, nothing killed or removed for the old process, the workspace re-attached and the row learning its binding, the bearer rotated with its turns preserved and the row's hash the new secret's, one resumed note, relaunches = 1, the run completed with the model continuing from the rebuilt transcript; for an ordinary run, the worktree refused by name closes it interrupted with workspace_lost, a second sandbox_restarted note saying why after pi's verdict, the relay forgotten, the workspace released, the card 🔁 and the request to run again; for an ordinary run the third finding closes it naming the bound with no fourth start and the count on the row never going back; a stamped pilot writer pauses at that bound without releasing its checkout or restarting the request; a row write that throws inside the rotation fails the run with nothing relaunched and both secrets verifying; two deaths in one run — a relaunch, then a bot death with pi inside its next call — leave a ledger planResume reads back with a result for every call, and the settlement projection contains those results; an unverified third container is held, with no restarted-model uptake claimed | [unit] src/core/dispatch/runLoop.test.ts::the pi harness — the container replaced under a living bot: the relaunch ceiling::* |
16: a relaunch that finds the run inside its write-up reserve, or under the attach floor past it, ends the run on its budget with none of the tail against the replaced container: the time_budget_exhausted note carries the bound's own sentence, the answer is the budget's plain form, the status completed; on a coding run no observation, salvage, description turn, work-left-behind note or PR post-step (a description already submitted posts nothing); on a review no exec after the end (no head settle), no verdict turn, nothing posted to the pull request, no review_posted and no post outcome on the record | [unit] src/core/dispatch/runLoop.test.ts::the run control's lease clock …::a container replaced with the run inside its write-up reserve …; src/core/dispatch/runLoop.test.ts::the run control's lease clock …::a container replaced with a REVIEW run inside its write-up reserve … |
16: end to end through dispatch() with the ledger: a living bot's run is relaunched — the resident asked once to keep the run's own worktree (reuse), the bearer rotated on the same meter, pi restarted on the mirrored transcript with the call settled — and the one record ends completed with the verdict and one resumed note, no ❌ reaching the thread and no second run started | [unit] src/core/dispatcher.test.ts::run ledger write-through …::a live pi run whose resident container is replaced under it is relaunched in the replacement… |
16: outside a stamped pilot writer, a relaunch whose re-attach the resident refuses — another tree for the thread — closes the run interrupted with workspace_lost and restarts from the request, never steering into the row it closed: with the boot-gap map naming the replaced run and the row's finish landing only after the restart began, the request still runs under the same run id, nothing is pushed into the closed row and the map forgets it; and the roll order every deploy has: the run handed to the next generation and resumed there with pi alive, the map forgetting the thread at the adopt, the container then replaced under the living generation — pi relaunched there with the adopted bearer rotated away, one record completed carrying the resume, the verdict and the relaunch, no second run | [unit] src/core/dispatcher.test.ts::run ledger write-through …::a relaunch whose re-attach the resident refuses…, ::the roll order every deploy has… |
| 16, live: a pi coding run on a resident across a resident-only deploy on a living bot | [agent] Start a coding run of a few minutes on a resident (@switchboard agent:coding in <owner/name>: <the task>), then deploy the resident Worker alone so its container rolls under the run → the run keeps its card and finishes the task on it; its run page shows the sandbox_restarted note naming both boot ids, the in-flight call settled with the restart note and one resumed note reading relaunched after the container was replaced …; the row's facts read relaunches: 1; the record reads completed, never interrupted; the thread sees no ❌ and no second card |
| 16, live: a resident roll under a live SHIP child — the binary proof that a coordinator child survives the roll (issue 1364 part 1; run-history.md item 47a) | [agent] Issue a small one-unit plan (or agent:ship a small task) and wait for its coding child to be mid-implementation on the resident, then deploy the resident Worker alone so its container rolls under the child → EITHER the child resumes in place — its record reads completed with the sandbox_restarted and resumed notes, the row's facts read relaunches: 1, and the unit's round continues to review on the same run (no child_interrupted event, no second coding spawn) — OR, for a child without the stamped pilot preservation policy, where the relaunch is refused by name, the child's record reads interrupted and carries the child_interrupted event with the reason, and the plan runner ends the round within one wait chunk with that reason (the recover pr-check follows; never the budget clip 20–35 minutes later) |
16, the replaced word never outranks a live pi: before any relaunch the harness probes the row's pid in the container the run holds — alive here, the run re-attaches in place with relaunches untouched, one resumed note in exactly the wording the executor said replaced; the row's pi answers alive in this container; re-attached, no verdict and no second process; gone, the verdict by the word as before (the fake answers alive false while a replacement is armed) — held on both drivers by the conformance row | [unit] src/core/harness/conformance.test.ts::conformance — pi::survival: the executor says replaced while the recorded pid still answers alive…, ::conformance — opencode::survival: the executor says replaced while the recorded pid still answers alive…, src/core/harness/pi/harness.test.ts::runPiHarness — the container replaced under a live run::the executor's word is the condition whatever the container answers for its name… |
16, a control reset is not the word: a resident Durable Object reset over a live container answers control-reset, distinct from runtime-replaced and never the verdict; the seam re-sends an idempotent op once and never a write; the loop re-attaches in place and resolves the write by pi's echo (resolveControlResetWrite, one rule for both words) — a prompt with an id awaits that id's echo (one already read before the failure was seen counts: the echoed ids are recorded at the consume) and is re-sent steer-delivered only if it does not come within the bound on the harness's clock (never a count of ticks or events — a streaming pi cannot stall it), an abort passing the gate's hold by type (PASSES_HOLD) onto the transport's one step (write's abort step: at once when nothing is in flight, behind a write in flight or queued otherwise, so no two writes to the FIFO are ever in flight and the abort follows the prompt it was asked for during; not queued for a re-attach when it lands, none chained while one abandons — its sender is the suspended loop, and the tick that re-asks never runs inside the wait — a failed one answered failed with nothing kept and asked again by the loop's every tick until it lands (abortPi, no cap), alone on a live chain or after the re-attach on the fresh one, one stop in flight per series whatever asks, the series ended with the loop at dropHeld (a turn starts its own) and every series' stop still in flight said by the session's end before the finish (the kill ended pi; or, on a replaced container where no kill runs, the relaunch carries on), the record carrying two lines for the series — the first failure's, and one closing with the count of re-asks when the stop lands or the loop ends unheard — and a dropped one noted as the harness's error), an id-less prompt failing by name, a steer left to the echo/requeue, an id-carrying control command and the gate reply re-sent as they were, an abort never in doubt, and only a write with no echo to resolve it by failing the run by name; the transport records the failed command as pendingSend, stops its chain at the first failure and hands every write whose turn never came to the re-attach (takeUnsent), which waits for every write in flight to settle (flushed(), the hard-stop signal raced with the one more command's own pause, the write raced once before the clock is judged, giving up at the lease's end from the loop and at a turn's end — deadline plus finale allowance — from a follow-up turn) before it resolves and then sends them on the fresh transport in order behind the resolved write, through the one gate (HeldSends) that holds every later write of turn content behind a prompt awaiting its echo, appends a second prompt in doubt behind the first, passes a gate reply and an abort through the hold by their types inside send (PASSES_HOLD: the loop's reply sites, a reply the reset left unresolved or queued behind the failed write, and any abort handed to the gate alike), reads its clock (quiet) only on an iteration on which the feed is quiet — the transport caught up, a catch-up burst still being handed out being no time — after the event just consumed is observed, the awaited head's clock starting at the first quiet read after it became the head, runs a write's onLanded when the transport's write has settled rather than at the hand-off, told landed or failed (the finale's clock starts on the landing; a wrap-up whose write failed with the reset starts no clock and is asked again, a wrap_up note saying so; a dropped write — to a closed transport, or an abort's step on an abandoned one — is told so, no re-send coming: a steer dropped after the run ended ends nothing further, a failed abort noted harness_error once and asked again on every tick until a stop lands, the series closed by one line with the count — stop_landed when a stop lands, harness_error when the loop ends with it unheard — a dropped one noted harness_error, and no landing sets a debt for a loop or turn that has ended — dropHeld forgets every pending callback its sender did not mark as outliving the drop (the harness's abort's alone), a write in flight included, ends the series once (closing it there when no stop is in flight — a second drop says nothing more; a stop in flight closes it by its landing, or is one harness_error for a write that failed after the end, and an abort sent after the drop gets no callback), and drops the transport's unsent (takeUnsent, taken to be discarded, by identity, so a pending step never lands a dropped write nor takes a later turn's entry) so nothing of the dead loop's rides takeUnsent or lands into the turn, a wrap-up among either list clearing the label), and drops whatever it still holds when the loop or a turn ends (dropHeld, after the unechoed steers are requeued: nothing of a dead loop's is delivered into a follow-up turn; a follow-up still staging is requeued when its staging completes; a dropped wrap-up steer clears the write-up's label and is said in a wrap_up note) (a plain close() flushes unless an earlier write failed); a re-attach that reads no record between them does not reset the runaway bound (a line that parses to no record is not progress), and past the bound the run fails by name on both harnesses whichever word it met — never the verdict for a word the pid still refutes; the re-attach machinery is one shared module both drivers run | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — the resident's control plane reset under a live pi::*, src/core/harness/pi/harness.test.ts::runPiHarness — the container replaced under a live run::the hard stop that ends the loop's wait sends its abort after the loop dropped what it held…, src/core/harness/pi/harness.test.ts::runPiHarness — the container replaced under a live run::a container replaced with a stop still in flight gets its own last word…, ::the control-reset write-resolution rules (harness-pi item 16)::*, src/core/harness/container.test.ts::ExecHarnessContainer — a Durable Object control reset over a live container::*, src/core/harness/container.test.ts::replacedVerdict — the one more command waits through a container that is down (the restore window), bounded like the start gate::*, src/core/harness/opencode/harness.test.ts::OpenCodeHarness — the resident's control plane keeps resetting the feed::*, src/core/harness/pi/transport.test.ts::PiRpcTransport::a failed send records the command it carried as…, src/core/harness/pi/transport.test.ts::PiRpcTransport::abandon() holds every write still queued…, src/core/harness/pi/transport.test.ts::PiRpcTransport::a failed send stops the chain…, src/core/harness/pi/transport.test.ts::PiRpcTransport::the contracts after a failed send…, src/core/harness/pi/transport.test.ts::PiRpcTransport::an abort is one step on the chain…, src/core/harness/pi/transport.test.ts::PiRpcTransport::a failed abort's write is its sender's to ask again…, src/core/harness/pi/transport.test.ts::PiRpcTransport::the ended loop's takeUnsent drops by identity…, src/core/harness/pi/transport.test.ts::PiRpcTransport::takeUnsent at the loop's end takes the unsent writes to discard them…, src/core/harness/pi/transport.test.ts::PiRpcTransport::no two writes to the FIFO are ever in flight at once…, src/core/harness/pi/transport.test.ts::PiRpcTransport::write answers with the landing…, src/core/harness/pi/transport.test.ts::PiRpcTransport::caughtUp says whether every record read has been handed out…, src/core/harness/pi/transport.test.ts::PiRpcTransport::a gate reply rides the chain like every other write…, src/core/harness/reattach.test.ts::HeldSends — the one gate every write to pi takes (harness-pi item 16)::*, src/core/harness/reattach.test.ts::reattachBoundMessage — the bound names the harness's own process::* |
16, a follow-up turn meets the control plane's reset and the refuted word as the loop does: the executor's word on a turn's read while the row's pid answers alive re-attaches the turn in place on the same pi — one resumed note in the exact wording, no sandbox_restarted, one process, nothing killed before end(); a control reset on the turn's prompt send re-attaches and re-sends the prompt steer-delivered once when its echo does not come; a reset that raced the provider-retry prompt is resolved by that prompt's own id; the turn writes to the re-attached transport, never a copy of the old | [unit] src/core/harness/pi/harness.test.ts::runPiHarnessOpen — a follow-up turn meets the control plane's reset and the refuted word as the loop does::* |
16, a control reset on the OpenCode bridge's HTTP writes — the prompt POST, a permission reply — is resolved from the server's own state (harness.md item 13): the store read whole page by page tells whether a queue prompt landed and the pending asks whether a reply did, each re-issued once only where it did not, the feed re-attached in place under a resumed note; a steer the reset cuts is never re-sent (a write-up steer noted as not reaching the server, a follow-up handed back to the inbox); a state that cannot be read, or a re-issue that meets the reset again, fails the run by name | [unit] src/core/harness/opencode/harness.test.ts::OpenCodeHarness — the resident's control plane resets under a write::* |
16, [gap] a pid-reuse guard on the re-attach — comparing the process's start time or command line to the row's facts — is not built: the container seam's alive is a bare kill -0 <pid>, so it exposes no process facts to compare, and the pid alone answers the ask-2 probe; a driver that gains process facts closes this | [gap] the container seam exposes no process start time or command line |
| 16, live: a control reset over a live pi across a resident WORKER-only deploy | [agent] Start a coding run of a few minutes on a resident, then deploy the resident Worker code alone (no image change) so its Durable Object resets under the live pi → the run keeps its card and finishes on the SAME pi; its run page shows one resumed note reading the resident's control plane reset under the run …, no sandbox_restarted note, the row's facts read relaunches: 0, and no second pi is orphaned in the container (ps on the resident shows one pi for the run); owed after the next release |
| 13, live: the operator and a memory reflection both run through pi’s provider library; a no-call repair makes no classifier call | [agent] On a deployment with memory enabled, send a plain request whose first and second operator turns both end without a tool call; the second operator request is on the same configured model, no run starts, the failed door record carries non_decision, and logs contain no dispatch.route span; a tool-using run still writes reflection without a failure line |
New bound/tracked Review selects a hosted command policy: pi omits native bash, registers the existing direct run_check relay and independently rejects stale native shell asks. Ordinary coding and explore tool policies remain unchanged. Compatible original sessions resume under their acknowledged launch policy; current definitions never upgrade that policy. Missing, malformed or foreign hosted policy remains held before producer effects. The first tracked policy-bearing facts require a known canonical ACK before model entry; false or thrown saves retain the actual started producer and authorize no replacement. | [unit] src/core/harness/pi/process.test.ts::hosted Review launch policy::*; src/core/harness/pi/extensionSource.test.ts::hosted Review extension relay::*; src/core/harness/pi/relay.test.ts::hosted Review native admission::*; src/core/harness/contract.test.ts::hosted Review retained session policy::* |
Continuation checkpoints
| Criterion | Proof |
|---|---|
| 6: the installed Responses adapter retains redacted problem title, detail, status and request ID; initial and follow-up notes retain structured unknown evidence; telemetry joins closed observations and an error fingerprint to the original run and trace without private diagnostic prose; a throwing telemetry observer preserves the original failure | [unit] src/core/harness/pi/bridge.test.ts::*::retains problem details from the installed Responses adapter without treating them as recovery authority, src/core/harness/pi/harness.test.ts::*::unknown terminal diagnostics reach the run record on initial and follow-up calls without replay or private text, src/core/dispatch/runLoop.test.ts::*::persists structured Pi problem details and logs the same evidence with run correlation (%s) |
| A long completed view produces a bounded continuation checkpoint after task settlement without another task turn; the bridge mirrors it without changing the answer | [unit] src/core/harness/pi/extensionSource.test.ts::the harness extension::summarizes a continuation through native compaction and rejects partial or aborted summaries, src/core/harness/pi/harness.test.ts::runPiHarness — a run on pi from the first file to the answer::compacts a long settled run through the native command and persists its full checkpoint before closing, ::keeps the answer on a failed checkpoint and honours hard stops and the existing deadline while compaction waits |
| An optional checkpoint with uncertain delivery never replays its summary or loses the completed answer; cancellation waits for native idle before another prompt, within the existing deadline. Both the initial task and post-loop turns keep this contract. | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — a run on pi from the first file to the answer::keeps the completed %s answer when a reset leaves compact delivery unknown without replaying it, ::waits for native checkpoint cancellation after the %s task before the next post-turn, ::refuses another post-turn when checkpoint cancellation reaches its existing deadline without native idle |
| Optional checkpoint recovery retains replacement evidence through initial and post-loop answer return. Word/dead, word/word and changed-identity outcomes never kill an old PID or remove its root in the replacement; unverified placement retains original custody and returns an unconfirmed ending. | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — a run on pi from the first file to the answer::retains checkpoint cleanup custody for the %s task after %s |
| A known replacement that ends a completed optional checkpoint releases its relay registration and cached answers on session end. Unconfirmed custody stays held, and actual relaunches keep their handoff registration. | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — a run on pi from the first file to the answer::retains checkpoint cleanup custody for the %s task after %s; src/core/harness/pi/harness.test.ts::runPiHarness — the relaunch in the replacement container::* |
| A missing FIFO during optional compact delivery preserves the completed initial or post-loop answer and refuses further prompts on that session. | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — a run on pi from the first file to the answer::keeps the completed %s answer when the optional compact FIFO is missing |
| Each refused continuation summary uses the native RPC error-notification API to emit a retained receipt with its run ID and a closed reason. Aborted, truncated, empty, over-budget and provider-error receipts contain no summary or exception prose. | [unit] src/core/harness/pi/extensionSource.test.ts::the harness extension::summarizes a continuation through native compaction and rejects partial or aborted summaries, ::retains a cancelled checkpoint receipt through the pinned RPC mode and bridge |
| A continuation summary maps the configured effort through the pinned provider adapter. | [unit] src/core/harness/pi/extensionSource.test.ts::the harness extension::sends the selected high effort through the pinned provider adapter during a continuation summary |
Canonical child allowance
A canonical child uses the absolute bound committed at its original admission. Setup, downtime, resume and container recovery consume that allowance. Post-loop turns and finales cannot extend it; their one-minute minimum is capped by the actual remainder. Expiry admits no model prompt. Ordinary runs retain their existing lease policy.
| Criterion | Proof |
|---|---|
| Both harnesses preserve the absolute bound when model admission follows attachment downtime. | [unit] src/core/harness/pi/harness.test.ts::a carried absolute deadline bounds…, src/core/harness/opencode/harness.test.ts::an absolute admitted deadline bounds… |
| Post-loop turns consume only the absolute remainder; expiry prevents further prompts and a hung turn cannot admit a finale afterward. | [unit] src/core/harness/pi/harness.test.ts::a canonical post-loop turn…, ::an absolute deadline ends a hung post-loop turn… |
| An original maintenance action supplies an absolute deadline before dispatch. Authorization, reservation, durable admission and workspace setup consume that same allowance; expired or subminimum starts refuse before paid work. The saved admission and both harnesses retain the original bound through downtime and relaunch, never a later sampled remainder. | [unit] src/core/dispatcher.test.ts::trusted original action remaining clock::pre-admission delay cannot renew the original absolute action deadline; ::an original action with %s remaining starts no model or attachment; ::expiry during durable admission starts no attachment or model; ::attachment consuming the original deadline starts no model; src/core/harness/pi/harness.test.ts::a carried absolute deadline bounds the model lease after attachment downtime; src/core/harness/opencode/harness.test.ts::an absolute admitted deadline bounds the OpenCode lease after attachment downtime |
The provider-recovery fixtures use the actual local Memory worker and SQLite producer, row, outbox, fence and acknowledgement path. Their scripted pi echo and cursor/reclaim assertions prove local protocol handling. They do not prove native durable consumption: exact raw inbox rows remain retained after local filtering and owner closure; that receipt remains a [gap] in run history.
| Hosted Review checks bind the observed native call's cancellation and originating turn budget at relay admission. The observation allowance excludes durable step saves, bounded log reads and main-owned same-producer recovery in progress, including work that starts during the last observation poll. Read-error classification stays with the main loop; a recovered original call can execute once. A changed or unproved producer, hard stop, deadline or session end before native observation grants no execution. A cut before dequeue or after pending ACK starts no command; an executing cut reaches the executor and remains unknown. Owner/read-policy/ACK ordering and fresh narrowed follow-ups remain independent. | [unit] src/core/harness/pi/relay.test.ts::hosted pi check lifecycle::*, ::hosted opencode check lifecycle::*; src/core/harness/pi/harness.test.ts::hosted Review originating lifecycle::*; src/core/harness/opencode/harness.test.ts::hosted Review originating lifecycle::* |
| 12: an ordinary bot-generation resume cannot infer replacement from an unobserved predecessor PID. The current source holds sampled-absent original custody; an explicit host-cwd replacement fixture proves rendering only. | [unit] src/core/harness/contract.test.ts::PiHarness — pi as the contract's object::find holds foreign or sampled-absent original custody and preserves alive-here observations, src/core/harness/pi/harness.test.ts::runPiHarness — after a bot restart::*, src/core/harness/pi/harness.test.ts::*::the explicit replacement protocol uses the current host root as session cwd and retains the prior root | | 12: ordinary bot-generation continuation preserves and restores a recorded bot-host producer through a supported native path. | [gap] A new host registry cannot establish predecessor liveness or reach its stdin. Existing cwd and settlement projections grant no restart, retirement or native-uptake proof. |
| Original logical policy is a closed native/hosted route plus identity captured by the trusted original launcher; compatible resumes preserve it, incompatible/unknown hosted originals hold, and first canonical facts ACK precedes model entry. | [unit] src/core/harness/sessionPolicy.test.ts::original session policy closed contract::*; src/core/harness/contract.test.ts::original hosted policy compatible continuation::*; ::original hosted policy uncertainty remains held::*; ::original session policy canonical first-fact ACK::*; src/core/dispatch/runLoop.test.ts::tracked Review command receipt binding::uses the original native session route without upgrading it from current Review eligibility |
| Quote characters inside shell comments cannot hide executable environment dumps. Read and write identities still refuse them; literal hashes and quoted search patterns remain data. | [unit] src/core/harness/pi/toolRules.test.ts::judgeToolCall — bash::comment quotes cannot hide an executable environment dump; ::allows quoted search patterns without treating their operators as environment commands |
| Criterion | Proof |
|---|---|
| 17: declared text refusals are errors on the model wire; ordinary source text remains successful. | [unit] src/core/harness/pi/relay.test.ts::relayed tool failure outcomes::reports a declared text refusal as an error while preserving ordinary source text |
| 17: both harnesses stop repeated tool failures after two model calls without a model write-up or operator stop. | [unit] src/core/harness/conformance.test.ts::tool recovery — *::ends repeated bot-tool failures without a model write-up or operator stop |
| 17: corrected submissions succeed, unrelated reads/status cannot replenish recovery, reconstruction retains attempts, and uncertain writes remain held. | [unit] src/core/harness/toolRecovery.test.ts::run-owned tool recovery::* |
| 17: both native tool bridges retain nonzero command exits as task evidence and stop repeated native exceptions. | [unit] src/core/harness/toolRecovery.test.ts::native tool recovery — *::keeps nonzero command exits as evidence and stops repeated native tool exceptions |
| 17: terminal recovery is retained in the existing ledger and prevents verdict repair and review publication. | [unit] src/core/dispatch/runLoop.test.ts::runLoop — the model turn and everything that rides on it::persists tool recovery failure in %s and blocks review publication |
| 17: an unconfirmed recovery checkpoint holds the original run without releasing its workspace or starting another model call. | [unit] src/core/dispatch/runLoop.test.ts::runLoop — the model turn and everything that rides on it::holds the original run when its %s tool recovery checkpoint is unconfirmed |
| 17: a failed terminal recovery save holds the original owner regardless of its earlier stop reason. Checkpoint traces name the tool and a closed cause without exception prose. A confirmed hard stop retains its stopped ending after tool refusals. | [unit] src/core/dispatch/runLoop.test.ts::runLoop — the model turn and everything that rides on it::holds the original run when its %s tool recovery checkpoint is unconfirmed; ::preserves a hard stop already honored after repeated tool refusals |
| 17: the run owner joins queued recovery writes and judges the current guard before finalization, including native failures and swallowed repair-turn errors. An unconfirmed native save holds its original owner; a confirmed terminal repair blocks publication even if a verdict was recorded before the stop. | [unit] src/core/dispatch/runLoop.test.ts::runLoop — the model turn and everything that rides on it::holds the original run when its %s tool recovery checkpoint is unconfirmed; ::persists tool recovery failure in %s and blocks review publication |
| 17: known lifecycle denials stay wire errors but do not spend or reset task recovery. Both native bridges retain the gate's per-call classification, including a typed command-budget refusal; the soft-stop write-up remains available. | [unit] src/core/harness/toolRecovery.test.ts::native tool recovery — *::preserves %s lifecycle refusals without clearing earlier task failures; src/core/harness/toolRecovery.test.ts::run-owned tool recovery::keeps definitive command-budget refusals out of task failure recovery; src/core/harness/conformance.test.ts::tool recovery — *::keeps a soft stop and its write-up after late relayed calls |
| 17: reconstruction retains the saved allowance without reclassifying historical Pi lifecycle refusals as new task failures. A call observed live or handled by this generation's gate still counts; a current gate can run while historical replay finishes. | [unit] src/core/harness/toolRecovery.test.ts::native tool recovery — pi::does not reclassify replayed lifecycle refusals after reconstruction |
| Background completion observers receive the provider request ID from response headers before stream consumption; a diagnostic observer failure cannot change the completion. | [unit] src/core/harness/piAi.test.ts::PiAiProviders — the provider table config.yaml names, built on pi's model library::observes the provider request ID at response headers without changing the completion |
| 6: Anthropic without a stop reason uses the same bounded local-stream retry in initial and follow-up turns. Local stream and abort retries emit model_retry notices, separate from harness failures. | [unit] src/core/harness/pi/harness.test.ts::*::an Anthropic stream without a stop reason recovers in the same initial or follow-up turn, ::*::an abort while a model stream is open is retried as the same turn, src/core/harness/pi/bridge.test.ts::*::recognizes the installed Anthropic adapter's missing-stop error as a local stream failure |
| 6: error diagnostics exclude SDK parser payloads and previous-generation run bearers; oversized or deeply nested error data cannot replace the original failure | [unit] src/core/harness/pi/bridge.test.ts::*::recognizes the installed Anthropic adapter's missing-stop error as a local stream failure, src/core/harness/pi/bridge.test.ts::*::keeps the original unknown failure when Pi error diagnostics exceed size or nesting limits, src/core/harness/pi/harness.test.ts::*::redacts the original Pi bearer after reattachment in initial and follow-up failures, src/core/redact.test.ts::oneLine::redacts run bearers from earlier generations before truncating the text |
| 17: changed arguments with the same error can reach another model turn across reconstruction, while an exact repeated request still stops; missing directory probes can select a valid root. | [unit] src/core/harness/toolRecovery.test.ts::run-owned tool recovery::allows changed arguments after the same error and retains them across reconstruction; ::allows a changed verdict with the same refusal before accepting its correction; src/core/harness/pi/extensionSource.test.ts::the harness extension::keeps a missing %s directory recoverable and lists the corrected location |
User stop controls
The public Stop control uses the existing immediate stop path: attempt to interrupt the active turn, make no summary turn, and force cleanup. Its answer is ⏹ Stopped by user. and its run span ends successfully. Kill bypasses that turn interrupt and all checkpoint preservation. The runtime must confirm shutdown before history removal. Legacy explicit soft/hard calls remain supported and retain history. The dashboard offers Stop and Kill.
| Criterion | Evidence |
|---|---|
| Kill ends pi without a graceful abort or a summary turn. | [unit] src/core/harness/pi/harness.test.ts::runPiHarness…::kill ends pi without a graceful abort or a summary turn |