Run history
Code:
src/core/runLedger/sourceEvidence.tsTests:
src/core/runLedger/sourceEvidence.test.tsTests:
src/core/runLedger/inboxDelivery.test.ts,src/core/runLedger/sourceRelevance.test.ts
Opted-in coding runs also obey the first coding baseline admission and durable receipt contract. Every run becomes a durable record — identity, timing, terminal status, the redacted event stream, and the friction diagnosis — kept for a retention window instead of evicted 60 s after finish; the record is tombstone-first (item 27), so even a run cut down before finish leaves an interrupted trace. The record is built the moment a run finishes and written after the reply, so persistence never delays the user; /runs/:id serves a finished run from the store through the same run page the live view uses (the web shell + a history seed, live-view.md), and the runs.* commands read every run — live or persisted — through one RunsService. This file covers the node-free record contract both the bot and the state Worker import (shape, validator, the one retention function both sides run, the byte budget), the RunStore seam with its three implementations (in-memory, host-disk file directory, and the RunHistoryDO on the state Worker), the runHistory config section, the dispatcher's write path, the RunsService read merge, and the friction ledger served from the store. The commands over this service are command-registry.md; the page is live-view.md.
Code:
src/core/dispatch/operatorTail.ts;src/core/checkInspection.ts,src/core/storedJson.ts(retained JSON decoder privacy),src/core/storageFailureDiagnostic.ts(shared request-bound storage exception telemetry),src/core/coordinator/pullOwnerQualification.ts(private observational locator and canonical snapshot projection);src/core/coordinator/pullOwnership.tsCode:
src/core/branchIdentityHistory.ts(refusal-only canonical history before identity capture),src/core/branchIdentityBaseline.ts(immutable first-attachment identity proof),src/core/branchStartState.ts(node-free identity evidence types),src/core/runIdentity.ts(shared unchanged ID patterns),src/core/reviewPublication.ts(original review target/hash receipt),src/core/branchPublication.ts(typed producer publication facts and private intent),src/core/workspaceSettlement.ts(compact owner-bound terminal workspace versions and exact acknowledgments),src/core/runStoreConstants.ts(the fixed Worker store key and timeout),src/core/storeFailure.ts(typed unavailable, refused and uncertain outcomes),src/core/storeResponse.ts(frozen request witnesses and response-byte/JSON decoding),src/core/runStateWriteDiagnostic.ts(closed exact-wire-bound state-write failure evidence),src/core/answerOutcome.ts,src/core/audienceDecision.ts,src/core/testing/audienceVerifier.ts,src/core/runsService.ts(item 58:listUnitRuns,listChildren,searchSession,listFindings,ListRunsOptions.parentRunId,ListRunsOptions.pr,SessionSearchView) andsrc/core/unitRuns.ts(item 58:unitRunsOf,roundBoundaries,UnitRunsView),src/core/runUsage.ts(item 56:usageOfEvents,addUsage,aggregateUsage,reportOfUsageRows,billedTo,UsageRun/RunUsageRows/UsageRow/RunUsageReport/ShipRunUsage— the store's rows, allocation cells and Ship child points),src/core/coordinator/contract.ts(items 47–49: the coordinator's node-free contract —sendRunFinished, therun-finished-<runId>event,CoordinatorTagandcoordinatorFields,CoordinatorInstanceandisCoordinatorInstance, the id, step and key patterns) andsrc/core/coordinator/instanceStore.ts(item 49:CoordinatorInstanceStore— the Worker client, the in-memory double, the null store,buildCoordinatorInstanceStore);src/core/runRecord.ts(RunRecord,RunListItem,StoredRunEvent,RunStatus,RunSeed/RUN_SEEDS(item 52),RUN_ID_PATTERN,isRunRecord,isRunListItem,isStoredDiagnosis/normalizeDiagnosis,storedEventSeqs,RetentionPolicy,DEFAULT_RETENTION_POLICY,RETENTION_BOUNDS,clampRetentionPolicy,applyRetention,fitRecordToBudget,MAX_RECORD_BYTES,MAX_EVENT_BYTES, theRUN_LIST_*/RUN_EVENTS_*paging limits);src/core/runStore.ts(RunStore,PutResult,InMemoryRunStore,FileRunStore,RunHistoryConfig,retentionPolicyOf,buildRunStore);src/core/runStoreWorker.ts(WorkerRunStore,RouteMissingError,TransientStoreError,PermanentStoreError);deploy/cloudflare-memory/worker.ts(RunHistoryDO,POST /runs/put|get|list|events|delete; the live-run ledger tables and routes of items 28–34,RunTranscriptDO,SessionLogDOand the/runs/session/*routes of item 53, thesessionsregistry and the sweep's drop) anddeploy/cloudflare-memory/wrangler.template.jsonc(RUNSbinding, migrationv4;RUN_TRANSCRIPTSbinding, migrationv6;SESSION_LOGSbinding, migrationv9); the ledger's node-free contract and decisions undersrc/core/runLedger/(types.ts,inboxDelivery.ts— keyed delivery snapshots and receipt validation,decisions.ts,transcript.ts,flusher.ts,ledger.ts,inMemory.ts,sessionLog.ts— the session log's pure rules, item 53;writeThrough.ts—createLedgerWriteThrough,mintGeneration,LedgerRun,adopt;resume.ts—planResume,settlementFor,transcriptSource,loopEndingOf,reviewPostedBefore) and its Worker clientsrc/core/runLedgerWorker.ts(WorkerRunLedger,buildRunLedger); the harness's step report and re-entry (src/core/runLedger/stepReport.ts,StepReport;src/core/harness/pi/mirror.ts, the mirror that writes them;src/core/harness/pi/harness.ts,PiHarnessResume,settlementResults); the registry'screateoptions (src/core/runRegistry.ts,CreateOptions); the card handle onsrc/core/types.ts(StatusHandle.handle, set by the Slack adapter, passed through bycoalesceStatus); thegenerationfield of/healthz(src/channels/health.ts); the boot reclaim, the reclaim sweep and the closer (src/core/boot.ts,reclaimRuns,startReclaimSweep,closeReclaimed,ResumableRun); the resume launcher (src/core/resumeLaunch.ts,launchResumes,knownToolsFor,resumeMessage,repoContextOf,inputTextOf); the resumed run's channels (src/channels/slack.tsresumeSlackIOand theexistingCardoption;src/channels/slack/statusCard.ts— the live-card record the sweep asks and the reclaim's card closes;src/core/nullChannelIo.ts); the dispatcher's resume path (DispatchOptions;src/core/dispatch/admission.ts:ResumeContext,RestartContext, the row a resume adopts or a restart re-reserves inadoptCarriedRun, the durable inboxfoldCarriedInboxfolds in, a superseded row closed bycloseResumedRow/closeRestartRow;src/core/dispatch/reattach.ts:carriedWorkspaceBinding,abandonLostWorkspace,lostWorkspaceNote,prepareRestartTurn, item 54;src/execution/factory.ts:WorkspaceBinding,workspaceBindingOf,workspaceBindingFor,WorkspaceReattachRefusedError,ExecutorContext.reattach);src/config.ts(runHistoryinAppConfig),src/config/validate.ts(validateRunHistory);src/core/runHistoryWriter.ts(createRunHistoryWriter,RUN_HISTORY_RETRY_DELAYS_MS); the record build at finish and its post-reply write (src/core/dispatch/record.ts:writeTombstone,registerFinishRecord), the ledger claim (src/core/dispatch/run.ts:claimRun), the step/state mirroring in the loop (src/core/dispatch/runLoop.ts),finishingbefore the reply (src/core/dispatch/reply.ts:deliverAnswer), and the same on the ship path (src/core/dispatcher.ts,CoreDeps.runStore/RecordDeps.runHistoryWriter/AdmissionDeps.runLedger); the run's row and reservation before the attach, item 42 (src/core/dispatch/provision.ts:registerRun,reserveRun; the attach and the re-attach of item 54:attachWorkspace,reattachWorkspace); the one record assembly and the drain's and the reclaim's records (src/core/dispatch/record.ts:assembleRunRecord,interruptedRunRecord,reclaimedRunRecord,writeAbandonedRunRecords, thechannelVisibilityOfstamp);src/core/runRegistry.ts(markPersisted, the token-freegetById/snapshotById/requestStopById) andsrc/core/runRegistry/projections.ts(RunSummary,RunSnapshotwith itsstartedAt/eventCountfields,SealResult, and the projections that build them);src/core/runsService.ts(createRunsService,RunsService,RunView,Result,LiveRunAccess);RunActor/sanitizeActorand theactorfield onstop_requestednotes (src/core/runEvents.ts);src/core/frictionLedger.ts(RunStoreFrictionLedger,selectFrictionLedger); history mode insrc/channels/liveView.ts(withOmittedMarkers); wiring, boot probe and drain insrc/index.tsandsrc/cli.ts; config inconfig/config.example.yaml(runHistory);src/core/requestTrace.ts(item 42:RequestTrace.runId, the bound run the second net reads).;src/core/runLedger/allocationAck.ts(canonical original allocation claim ACK);src/core/coordinator/pullOwnership.tsTests:
src/channels/githubActivity.test.ts,src/core/dispatch/operatorTail.test.ts;src/core/runLedger/checkInspection.test.ts,src/core/commands/runChecks.test.ts,src/core/storageFailureDiagnostic.test.ts,src/core/reviewPublication.test.ts,src/core/audienceDecision.test.ts,src/core/runsService.test.tsandsrc/core/unitRuns.test.ts(item 58),src/core/runUsage.test.ts(item 56),src/core/runRecord.test.ts,src/core/coordinator/contract.test.tsandsrc/core/coordinator/instanceStore.test.ts(items 47–49),src/core/runStore.test.ts,src/core/runStoreWorker.test.ts,deploy/cloudflare-memory/runs.test.ts(inside workerd against the real SQLite DO),deploy/cloudflare-memory/runLedger.test.ts,deploy/cloudflare-memory/runTranscript.test.tsanddeploy/cloudflare-memory/sessionLog.test.ts(the ledger, transcript and session log objects, inside workerd),src/core/runLedger/sessionLog.test.ts(item 53's pure rules),src/core/runLedger/decisions.test.ts,src/core/runLedger/transcript.test.ts,src/core/runLedger/flusher.test.ts,src/core/runLedger/inMemory.test.ts,src/core/runLedger/writeThrough.test.ts,src/core/runLedgerWorker.test.ts,src/core/runLedger/resume.test.ts,src/core/boot.test.ts,src/core/resumeLaunch.test.ts,src/channels/slack.test.ts(SlackIO.status on a resumed run (existing card)),src/channels/slack/statusCard.test.ts(live cards),src/core/harness/pi/mirror.test.ts(the step records) andsrc/core/harness/pi/harness.test.ts(the way back after a restart),src/core/dispatcher.test.ts(run ledger write-through (docs/reference/specs/run-history.md item 35), the ship claim test underagent:ship (the hand-off to the plan runner)),src/core/runHistoryWriter.test.ts(via),src/config.test.ts(runHistory config),src/core/runHistoryWriter.test.ts,src/core/dispatcher.test.ts(run history write path …),src/core/dispatch/record.test.ts(the drain deadline's records; the one assembly's fields),src/core/dispatch/runLoop.test.ts(the failure by name, item 57),src/core/dispatch/admission.test.ts(a carried run's row and durable inbox;followUpFromInbox),src/core/runRegistry.test.ts(RunRegistry.markPersisted,RunRegistry — token-free operator reads …),src/core/runRegistry/projections.test.ts(RunRegistry.snapshot — record inputs …, the summary's terminal fields),src/core/runsService.test.ts,src/core/frictionLedger.test.ts(RunStoreFrictionLedger),src/channels/liveView.test.ts(live view on RunsService: history pages + index toggle …),src/core/dispatch/reattach.test.tsandsrc/execution/factory.test.ts(the workspace binding on the row,on a resume (ctx.reattach)…) (item 54).;src/core/runLedger/allocationAck.test.ts;src/core/coordinator/managedReviewOwnership.test.tsDocs: AGENTS.md invariants 2, 4, 6, run-friction.md (the diagnosis a record carries), live-view.md (the in-memory registry this history outlives; the history page), command-registry.md (the
runs.*commands), self-improvement.md (the ledger served from the store).
Behavior
The record
- Node-free contract.
src/core/runRecord.tshas no Node built-in imports and no I/O or clock: bytes are measured withTextEncoder, and callers passnowMs. The Cloudflare state Worker imports it by relative path, the same way it importsfrictionProposals.ts, so the bot and the Worker validate, trim and page with ONE implementation. - Record shape.
RunRecord = { id, label?, agent?, model?, channelId, userId, relayedBy?, authenticatedAs?, threadKey, channelVisibility, repo?, startedAt, finishedAt, receivedAt?, sealedAt?, replyOk?, replyNote?, stepCount?, schema?, status, eventCount, storedEventCount, truncated, events, diagnosis, record?, recordTaskKey?, headSha?, handoff?, verdict?, reviewHead?, reviewPost?, dispositions?, profile?, parentRunId?, seed?, pr?, pushed?, lease?, route?, provisional?, instanceId?, pipeline?, hosted? }.recordis the runner-reserved decision-record number andrecordTaskKeyis the stable key for a direct coding task; a direct re-issue pages through that thread's retained coding records until it finds the key or exhausts history, never stopping at the newest page. (pipelinethe standing fold's summary over the run's ownship_round/ship_unitevents —PipelineSummary, record 0065, agent-ship.md item 19 — folded at the one record assembly so the stored row reads the standing without the events;hostedthe ship parent's marker,RunMeta.hostedcarried onto the record at the seal, the literaltrueor absent likeprovisional; both absent on every other run and on records written before them, which read as today) (profileis item 45,parentRunIditem 46,seeditem 52,leasethe run's lease as its harness started it —{ startedAt, endsAt, loopEndsAt }off the firstleaseevent, folded at the one record assembly, present only on a run whose harness started a loop (harness-pi.md item 15, decision 0046);pushedthe heads the run pushed —{ ref, sha, by? }[], one per branch with the lastpushed_headevent's sha and source winning, folded at the one record assembly (byis"push"for an ordinary coding push or"salvage"for a mechanical checkpoint, and is absent only on records from before the source survived the fold); the event is published by the accepted-write recorder when an ordinary coding push is durably accepted (by: "push"), not by the observing coding post-step, and by the mechanical checkpoints — every abnormal coding-child ending (including budget end) and the compaction checkpoint (harness-pi.md item 7), allby: "salvage"— whether or not a pull request follows, so a renewal reads progress off the row without a model (decision 0046);prthe pull request the coding post-step opened or edited —{ number, url, head? }off the lastpr_openedevent, folded at the one record assembly (the event itself may also carryrewritten— how many of the run's commits the identity rewrite re-authored before the open, record 0062, agent-coding.md item 2; absent when none were),headthe branch the PR is opened from when the event names it — the fact a follow-up in the thread continues from, resident-repos.md item 29, and the branch the run's release hands the resident to remember, resident-repos.md item 16a;verdictthe verdict a review run submitted throughsubmit_verdict— agent-review.md — with its findings,reviewHeadthe head it reviewed and posted against after the settle, 7 to 40 lowercase hex,reviewPosthow its post-step ended —{ posted: true, target: { repo, number }, head, verdict? }or{ posted: false, reason }, agent-review.md item 18 — anddispositionsthe set a coding run submitted throughsubmit_dispositions, as submitted, the plan runner matching it to its round's findings when it reads the record — agent-ship.md item 6 — each redacted at every string leaf in the one record assembly and present only on a run that produced it, so a coordinator'sread-recordanswers a review child's verdict, whether it was posted, and a coding run's dispositions from the record — throughRunsService, which supplies them from the store the moment it holds the record, a finished row still inside the registry's window included, item 21) (the five optional stamps per tracing.md:stepCountis the registry's content-event count, span records excluded;schemathe stream schema, 2 once spans are emitted;headShathe final branch head independently observed after every tail step and mechanical salvage, or the identity rewrite's published tip if it moved the remote branch afterward, 7 to 40 lowercase hex, persisted at the one finish assembly and returned by the coordinator's productionread-recordbesidehandoffandpushed; absent when the workspace had no readable Git head and on older records;handoffthe typed handoff a coding child submitted — agent-ship.md item 14 — redacted like every stored string and present only on a run that submitted one, an affirmed empty handoff being three empty lists, distinguishable from none) (instanceIdthe plan runner instance a ship run's hand-off created — record 0051 R2 — folded at the one record assembly from the lastship_handoff { instanceId, at }event the ship branch publishes after a successful hand-off and never after a refusal, projected like the coordinator tag, exposed onRunView.instanceId, and how the thread's owner rule finds the instance, thread-admission.md item 9; the consuming run'sinputevent may carrymodeandconsumed— the thread events it folded, record 0051's mode-as-receipt rule; a HOSTED ship parent's own record (record 0060) carries TWOrun_metaevents — the dispatch's, then the hand-off's naming the runner instance inrun_meta.instanceId— plus theship_unitfacts the runner's routes published (agent-ship.md item 17): every reader resolves such a run's instance from the LASTrun_metacarrying one, and both event kinds pass the validator like any other) withstatus ∈ completed | stopped_soft | stopped_hard | failed | interruptedandchannelVisibility ∈ public | private | dm | machine | unknown— the channel's visibility as theChannelDirectoryreported it at dispatch (authorization.md item 7), whatmember-ofreads; a record written before the stamp existed is accepted and reads asunknown(normalizeStored), never public —interruptedmarks a run cut down before finish (container replaced or crashed; the provisional tombstone and the drain-deadline write of item 27 are its only writers), andprovisional?: trueis those two writers' stamp on the record itself (item 27): absent on every final write, so a store-only reader tells "no finish recorded yet" from a real interrupt that owns no live row.eventCountis what the run published;storedEventCountisevents.length;truncatedsays the two differ (the registry backlog or the byte budget dropped events). Every stored event keeps the registry'sseq(StoredRunEvent;storedEventSeqsfalls back to positions only for a record whose events arrived without strictly increasing stamps).RunListItemis the record minuseventsplus an optionalbytes(the stored JSON size) —diagnosisstays on the list item so the friction ledger can be served from a listing without loading events. How the run's preset was chosen rides the stream (routing-and-config.md item 21):run_meta.agentSource—directive,sticky,user,channel,defaultorroute— on every run since the router landed (absent on older records and on a command run), and aroute { preset, reason, model, parts?, collapsed?, command?, input?, receipt?, outcome?, handBackRunId? }event right afterrun_metaon a run the router answered (head material, a side fact likepr_opened, never a step) — on a command run the router bound from prose (routing-and-config.md item 21's command menu)presetiscommand,commandthe id the model called,inputthe bound{ args, options }with every string redacted and cut atROUTE_COMMAND_VALUE_CAP,receiptthe chat form the reply led with, redacted and cut atROUTE_RECEIPT_CAP; how the invoke ended is the run's ownstatusandanswer; and, on a door decision about a state change (record 0044),outcome:hand_backon the record of a routed write the door handed back — a command run that invoked nothing,completed, itsanswerthe line to paste, written byrecordRoutedDecisionwith no run signalled to the channel —offeredon the record of the same decision on a channel that can show a confirmation (routing-and-config.md item 25) — a command run that invoked nothing,completed, itsanswerthe offer as the channel shows it (the full line and the risk line), the row minted in the config object —confirmedon the record of the stored input run at the click, as the requester, through the typed line's path (reason: confirmed after offer,source: confirmon the audit line; its status andanswerthe command's own, as a routed read's are), andpastedon the record of the typed line that followed a hand-back in the same thread with the same capped receipt, whatever the command,handBackRunIdnaming the hand-back's record (routing-and-config.md item 21). A sixth outcome,refused, on the record of a routed decision a gate refused after the bind (record 0054): written byrecordRoutedDecisionfrom the confirm path when the store's refusal still names the row (routing-and-config.md item 25:expired,foreign) — a command run that invoked nothing,completed,reason: refused after offer, itsanswerthe refusal's named line, the event carrying the row's command, input and receipt andrefusalCode(a code fromsrc/core/refusal.ts) — and the door report counts such records per day, cause and code (load-harness.md item 19). Every other refusal is a run record too (record 0054, as amended): a gate refusal before any command is bound and before admission, a silent refusal and the catch-all'suncaughtalike leave one record with agentdoor— written byrecordRefusalbeside the rendered sentence, the refusal's code leading the label, statuscompleted, no surface told of the run and no thread claimed — whose events are the redacted request asinputand exactly onerefusal { code, cause, text }, the sentence redacted and capped atROUTE_RECEIPT_CAP, the cause the one table's for the code; a message without a thread of its own records with the channel as its thread key, a refusal that already recorded throughrecordRoutedDecisionrecords once, and a refused click on a question's stored proposal records the proposal as its request; ausedclick and an unreadable store name no row and leave no record — there is no message to record — and stay countable from the trace'srefusal/causeroot attributes (tracing.md item 3), which every refusal still carries unchanged. A run a question's Yes redispatched (record 0054; routing-and-config.md item 21) names the question on its record: arun_note { kind: redispatch, summary: "confirmed after question <code>" }published before the first turn,<code>the refusal code the stored row carried, so the Yes-run is traceable to the question whose proposal it ran. Which command runs are recorded: the inline-run commands (isInlineRunCommand, the ones that do work) and every routed decision whoseroutecarries anoutcome; a typed no-work command outside a paste, and a routed read of one, leave no record, as before:partson a compound routed to the conductor — one{ preset, text }per child the brief told it to spawn, the text redacted and capped as the child's whole prompt — orcollapsed: { presets }on the write run a compound answer collapsed onto (routing-and-config.md item 21: a write ask is never a part, so the answer became one route to that preset), the preset each part named in answer order and noparts, and, for a compound the parse refused, the event on the run that fell to the default,presetbeingdefaults.agentandreasonreadingcompound_rejected: <why>whileagentSourcestaysdefault(routing-and-config.md item 21); the replay (load route, load-harness.md item 17) reads both to tell a requester's own choice from a fallback and to leave the router's own answers out of its labels. The decision itself also rides the record asroute? { preset, reason, model, parts?, collapsed? }(routing-and-config.md item 21): the run's own route — folded at the one record assembly from the dispatcher's decision, or from therouteevent whenrun_meta.agentSourceisroute(routeOfEvents) — or, on a sticky-by-transcript follow-up that continued a routed thread, the thread's decision carried forward with no event; on the record (and itsRunListItemandRunView) so the next follow-up's card can keep its route note without reading the events. - Structural validator.
isRunRecord(x)accepts only an object whoseidmatchesRUN_ID_PATTERN(^[A-Za-z0-9_-]{1,64}$), whose identity/timing/count fields have the right types, whosestatusis one of the five, whoseeventsis an array of objects each with a stringtype(the union grows over time — an older reader must still accept a newer record, and the run-page fieldscallId/exitCode/output/sourceride through verbatim), and whosediagnosisis structurally a diagnosis (isStoredDiagnosis: abyCategorymap of{ count, durationMs }totals — NOT the current category list), and whoseheadSha, when present, is 7 to 40 lowercase hex, and whoseverdict,reviewHeadanddispositions, when present, have their stored shapes (isReviewVerdictShape, the head pattern,isFindingDispositionsShape— shape only, no bounds, like the handoff), and whosehandoff, when present, has the handoff's shape (isHandoffShape, agent-ship.md item 14: three lists of string-field entries — shape only, no bounds, because redaction may lengthen a stored string), and whoseprovisionalandhosted, when present, are each the literaltrue(item 27's persisted stamp and record 0065's hosted marker; any other value is a malformed record), and whosepipeline, when present, has the summary's shape (isPipelineSummaryShape— shape only, so an older reader accepts a newer record's field). Readers zero-fill categories a stored diagnosis lacks and drop unknown ones (normalizeDiagnosis), so adding a friction category never invalidates a stored record. Anything else —null, a bad id, an unknown status, an event without atype— is rejected.isRunListItemis the same check minus events, plus an optional numericbytes. - Retention policy.
RetentionPolicy = { retentionDays, maxRuns, maxBytes }, default{ 30, 5000, 2 GiB }.clampRetentionPolicy(partial)fills missing fields from the defaults, replaces non-finite values with the defaults, floors fractions, and clamps intoRETENTION_BOUNDS:retentionDays [1, 365],maxRuns [1, 20000],maxBytes [16 MiB, 8 GiB]. - One retention function.
applyRetention(items, policy, nowMs, context?)drops items withfinishedAt < nowMs − retentionDays, keeps the newestmaxRunsbyfinishedAtdesc (tie-breakiddesc — a total order, so bot and Worker cut identical rows), then drops the oldest while the cumulativebytesof the kept set exceedsmaxBytes(missingbytescounts as 0). The optional context suppliesreferencesandliveHolderIds; otherwise references come from each record'schildHandoff. A live holder or a record kept by ordinary retention also keeps its manifest's existing source records. Retention does not recursively promote a pinned source into another holder, and it cannot recreate a missing source. The manifest explicitly lists required ancestors. Once the last live or ordinarily retained holder goes away, the source follows ordinary retention again. Returns the kept items newest-first and never mutates its input. - Byte budget.
fitRecordToBudget(record, maxBytes = MAX_RECORD_BYTES = 1.5 MiB)first caps each event toMAX_EVENT_BYTES(64 KiB) by truncating itstext(input/context/assistant/answer events) orsummary(tool/note events) with a trailing…, measured in UTF-8 bytes. Review-artifact payloads are exempt from this text cap and retain their producer bounds; the whole-record budget can still omit an artifact and mark the record truncated. If the record is still over budget, span records go first (tracing.md — spans displace no content): pair by pair (both records of onespanId) from the middle of the stream outward, never from the protected head (the leading run of head material), until the record fits or no span outside the head remains; a droppedtool.*/mcp.*twin is re-synthesized bynormalizeSpansfrom its content pair. Only if that was not enough are content events dropped from the middle: a head and a tail are grown alternately from the two ends (head first) until the next event would not fit, so the request/context/first tool steps and the terminal notes survive. The result hastruncated: true,eventCountunchanged,storedEventCount = events.length; an under-budget record comes back with its events intact andtruncateduntouched. The source record is never mutated.
The store
RunStoreseam.RunStore = { put, get, getSummary, list, events, delete }withput → { ok, retained, stored, rewritten }(stored: falsewhen the record fell outside policy in its own write;rewritten: truewhen an existing record with that id was replaced by a different one). Every implementation rejects an id failingRUN_ID_PATTERNbefore touching storage — a bad id is not-found (get → null,getSummary → null,events → null,deleteno-op), never a path or a request.getSummary(id)is the record minus its events — the listing row,bytesincluded — for callers that need identity, status, or the diagnosis but not the event set; it hides exactly whatlisthides.listis newest-first (finishedAtdesc,iddesc), default 50 rows (RUN_LIST_DEFAULT_LIMIT), capped at 200 (RUN_LIST_MAX_LIMIT), with the compound cursor{ before, beforeId }(finished_at < ? OR (finished_at = ? AND run_id < ?)— same-millisecond siblings across a page boundary are never skipped) plussinceMs/agent/channelfilters and the caller'svisibleTo— the authorization predicate in its wire form (RunVisibilityFilter:all | none | channels-in | user-is | repos-in | visibility-in | or | and, authorization.md item 6), ANDed with the other filters and answered by every store with the one truth tablematchesVisibility(nonematches nothing, a row without the stamp isunknown) — and never carries events; a full page returnsnextBefore.events(id, { afterSeq, limit })pages a record's events by their storedseq(strictlyseq > afterSeq), default 1000 per page, capped at 5000, returningnextAfterSeqwhile more follow; an unknown or expired id isnull, a run with nothing past the cursor is{ events: [] }.- In-memory and file stores.
InMemoryRunStore(tests/dev).FileRunStoreis a directory store and an explicit opt-in (store: "file"):data/runs/<id>.jsonper run, written temp-then-rename with mode 0600 in a 0700 directory, plusdata/runs/index.jsonlofRunListItems withbytes—listandgetSummaryread only the index andgetopens exactly one file. Retention is applied on every read (expired rows hidden) and on every write (files unlinked, index compacted to the kept rows);sweep()does the write-side work without a new record andbuildRunStoreruns it on start and every 6 h from an unref'd timer. A torn record file (size ≠ indexedbytes, or unparsable) is skipped byget, absent fromlistand null fromgetSummary(the two index readers apply the size check;getopens the one file instead of stat'ing them all — an unparsable file is the same not-found); an index line whose file is gone is hidden and dropped at the nextput. Shrinking then growingretentionDaysnever resurrects a deleted record — the file is gone. - Worker client.
WorkerRunStorespeaksPOST /runs/put|get|summary|list|events|deleteto the state Worker with{ storeKey: "runs:default", … }, a STRING JSON body (the runtime derives the numericContent-Lengththe Worker requires; the client never sets it by hand — the one header the memory/friction/schedule clients omit, and hand-setting it makes every request from the container end infetch failed),authorization: Bearer <MEMORY_TOKEN>, and a 10 sAbortSignal.timeout. Onlyputcarriespolicy+policyUpdatedAt(the bot's config load time);get/summary/list/events/deletenever do. Responses are re-validated (isRunRecord/isRunListItem, diagnosis normalized). Reads classify interrupted response bodies, network failures and HTTP 5xx/408/429 asTransientStoreError; complete malformed read bodies arePermanentStoreError. Mutations freeze the complete encoded request and digest before transport: network loss, HTTP 5xx/408/429, interrupted or non-JSON bodies, invalid response objects and missing or malformed acknowledgements areUncertainStoreError, never automatic retry or rejection credit. The original private witness remains held for exact reconciliation. HTTP 404 isRouteMissingError(missing route, never retried); other definite HTTP 4xx refusals arePermanentStoreError. - Config and selection.
runHistory: { retentionDays?, maxRuns?, maxBytes?, includeContext?, store?: "worker" | "file", worker?: { baseUrl, tokenEnv? } }(includeContext, default true, read as!== false: publish the thread-context turns fed to the model into the run stream — live page and persisted record alike; run-visibility.md item 6).validateConfigrejectsretentionDays/maxRuns< 1, a non-httpsworker.baseUrl, and an unknownstore.buildRunStore(cfg, env, { dataDir, warn, now?, setInterval? }): no section →null(history OFF: runs stay live-only);store: "file"→FileRunStoreunder<dataDir>/runs;workerwith its bearer (tokenEnv, defaultMEMORY_TOKEN) →WorkerRunStore;workerwithout the bearer →nullplus a warning naming the env var (history off rather than a silent host-disk fallback).retentionPolicyOf(cfg)is the clamped policy the bot proposes and the live view's retention sentence reads.
The state Worker
RunHistoryDO(state Worker). One SQLite Durable Object per store key: tablesruns(listing columns includingchannel_visibility TEXT NOT NULL DEFAULT 'unknown',bytes,stored_at,diagnosis_json,summary_json= the record minus events),run_events(run_id, seq, json, PRIMARY KEY(run_id, seq)),meta(key, value); indexes onfinished_atand, for the visibility predicate's leaves,(channel_id, finished_at DESC, run_id DESC),(channel_visibility, finished_at DESC, run_id DESC),(user_id, finished_at DESC, run_id DESC). The constructor runs the DO's one migration: arunstable created before the stamp gainschannel_visibilitywithunknownfor every existing row.putis ONEtransactionSync: clampfinishedAtto now + 24 h and recordstored_at; upsertruns; skip the event rewrite when the stored version is unchanged (event_count,finished_at,bytesequal →rewritten: false), elseDELETE run_events WHERE run_idthen batchedINSERT(33 rows per statement); trim by the PERSISTED policy from both tables, oldest first, at most 500 rows per put (deletion fence, logged) — the just-written record is deleted regardless when it is outside policy (stored: false); orphan sweep;retained= rows the policy keeps. A failure anywhere inside rolls the whole put back (norunsrow, no events).alarm()still handles live-run deadlines and re-asks. Its scheduled physical retention work runs only withRUN_HISTORY_MAINTENANCE=enabled; the deployment defaults topausedwhile bounded maintenance is designed. Reads (get,summary,list,events) apply the persisted policy first, so rows still on disk behind the fence are invisible; a corruptrun_eventsrow is skipped.summaryreads therunsrow alone (norun_events).listdecides its path from ONE aggregate over the in-cutoff rows (COUNT(*),SUM(bytes)wherefinished_at >= cutoff): withinmaxRuns/maxBytesevery in-cutoff row is kept, so the page is ONE indexed query — age cutoff,agent/channelfilters, thevisibleTopredicate compiled to SQL (visibilitySql:channel_id IN (…),channel_visibility IN (…),user_id = ?,repo IN (…),or/andas parenthesised groups;noneis an empty page without a query,alladds nothing; a malformed filter or one binding more ids than the DO's 100 bound parameters allow is 400), the cursor predicate,ORDER BY finished_at DESC, run_id DESC,LIMIT— and no table scan; when a bound is exceeded, or a context-bearing page cannot be filled from in-policy rows, the kept set is computed withapplyRetention, including live holders and reference edges. Referenced older sources can create gaps in ordinary retention order, so the list query filters by the kept ids before applying its page limit; it never assumes the kept set is a prefix. An explicitsinceMsfilter still applies.deleteremoves the run and all its events.- Policy ownership.
metaholds{ retentionDays, maxRuns, maxBytes, policyUpdatedAt }, clamped withclampRetentionPolicy. Only/runs/putmay carry a proposal, accepted whenmin(policyUpdatedAt, DO now)is strictly newer than the stored stamp — so a proposal dated a year ahead is stored with the DO clock and a later, correctly dated one still wins. A proposal withretentionDays/maxRuns/maxBytes< 1 is 400.get/listignore any policy in the body. Cutoffs use the DO clock. - Routes and fences.
/runs/put|get|summary|list|events|deleterequire the bearer; unknown route 404, non-POST 405, missing/non-numericContent-Length411 (unchanged global rule). The body cap is per route, decided after routing and beforerequest.json(): 2 MiB for/runs/put, 512 KB for everything else, measured in bytes (a 1.9 MB multibyte body is accepted; cap + 1 byte is 413). Unknown or expired id →{ record: null }/{ summary: null }/{ events: null }with 200 (a 404 always means the route is missing).listaccepts anylimitbut returns ≤ 200 rows plusnextBefore; a malformedbeforeIdis 400.GET /healthz→{ ok: true, features: ["memory", "friction", "schedules", "runs"] }. wrangler: migrationv4(new_sqlite_classes: ["RunHistoryDO"]), bindingRUNS. The Worker must be deployed before the bot version that writes to it — the bot's boot probe (item 18) andRouteMissingError(item 17) make the wrong order loud and non-destructive. - Cloudflare DO SQLite limits (pinned in
worker.tsfrom the platform limits page). 100 bound parameters per query → 33 event rows perINSERT(3 parameters each) and 100 ids perDELETE … IN (…); 100 KB per SQL statement; 2 MB per string/BLOB/row (ordinary appended events are bounded to 64 KiB; review artifacts to 1.5 MiB); 100 columns per table; 10 GB storage per object (Workers Paid) —maxBytesis clamped to 8 GiB, under that ceiling.
The write path
- Record built at finish, inside the run's try/catch. The dispatcher computes the terminal status first and hands it to
registry.finish(id, status)— the registry stores it and everyRunSummaryprojectsstatus+finishedAt, so no consumer re-derives them (the one status the registry cannot know is a reply that throws after the loop: the record saysfailed, the registry row keepscompletedfor its TTL). Every record of a finished run is written by the dispatch's drain AFTER the run's seal (src/core/runEnding.ts; live-view.md item 4):assembleRunRecordappends the events the seal returned (the span records published between finish and seal), takes the larger published total, stampssealedAtandreplyOk, then fits the budget — so the record's duration and delivery caption equal the live page's, and a command run's record carries the stamps too. A reply that throws sealsreplyOk: falseand flips acompletedrecord tofailed; a fenced run (another generation's) writes no record here but is still sealed by the backstop; a command run's status is its command'sokand never flips. Right afterfinish, the dispatcher readsregistry.snapshot(id, token)ONCE — the same read feeds the friction diagnosis — and builds theRunRecordsynchronously: identity (channelId,userId,threadKey,agent,model= the resolved<provider>/<model>, the registry's redactedlabel,repowhen resolved),startedAt(the registry's create-time clock) andfinishedAt, terminalstatus(failedwhen the runner or the reply threw; elsestopped_hard/stopped_softfrom the run's stop control; elsecompleted),eventCount= the registry's monotonic published total,storedEventCount= the snapshot length,truncatedtrue when the bounded backlog dropped events, thenfitRecordToBudget. A failed run has a record too (the outer catch writes it after the error reply). A reply slower than the registry TTL cannot lose the record: it was built before the reply.snapshotreturnsstartedAtandeventCountalongside the events (still token-gated). - Write after the reply, fire-and-forget, drain-counted. The record is handed to
CoreDeps.runHistoryWriter.write()only aftersendAnswer(success) or the error reply (failure), so persistence never delays the user;writenever throws.pending()increments before the outerfinally'sactiveRuns--and is part of the shutdown drain insrc/index.ts(with the existing 15 min deadline), which also logsfailures()at exit. Without a writer (history off, most tests) nothing is written and the run is unchanged.src/cli.tsbuilds the same store/writer and awaitssettled()before exiting. - Retry policy.
createRunHistoryWriter({ store, warn, onPersisted?, sleep?, random? })handlesUncertainStoreErrorbefore rejection or retry: it retains the exact original request and record, reports the unknown obligation to its original sink, and grants no retry, fallback replay, abandonment or persisted credit. This includes an uncertain separate fallback; the original landing remains held until that fallback has a known result. OtherTransientStoreErroror unclassified failures get two retries with jittered backoff (base 1 s then 4 s, each ±50%); definitePermanentStoreErrorrefusals (for example HTTP 413) are not retried and produce one warning plusfailures() + 1; aRouteMissingError(404 — the deployed Worker predates the run-history routes) logs[run-history] state Worker has no /runs/put — deploy the state Worker with run-history routes before this bot versionONCE per process, setsdegraded(), never retries, and counts every loss. Retries exhausted → one warn naming the run id and attempt count,failures() + 1. On successonPersisted(id)runs (exception-isolated) — the dispatcher wires it toregistry.markPersisted, which setspersisted: trueon the run'sRunSummary(the key is absent until then) and emits one indexupsert; a no-op for an unknown or already-evicted run. A 404/413 on/runs/putloses that run for the friction ledger too — it reads run history and has no other source. - Startup.
src/index.tsbuilds the store withbuildRunStore(cfg.runHistory, env, { dataDir: "./data", warn }), logs which store was selected (host-disk file (data/runs),durable Worker (<baseUrl>), oroff … live-only), serves the friction ledger throughselectFrictionLedger(store)(none without a store), and — for the Worker store — probesGET <baseUrl>/healthzbest-effort at boot: afeatureslist withoutrunslogs anORDERING ERRORline; the probe never blocks or fails startup.
Reading runs: RunsService
- One service, token-free views.
createRunsService({ registry, store, analyze? })is the one async service behind everyruns.*command and the live view;store: nullmeans history is off (live-only). Every result is aResult<T> = { ok: true, value } | { ok: false, error: "not_found" | "conflict" | "hosted" | "unavailable" }and NO output carries a run's capability token: live registry rows are projected field-by-field intoRunView(id, label?, agent?, model?, channelId?, userId?, threadKey?, repo?— theRunMetathe dispatcher passes toregistry.create(label, meta)— plusstartedAt, finished, eventCount, stop?, persisted?, and for a finished registry run thefinishedAt/statusgiven tofinish()) and a persistedRunListItembecomes the sameRunViewshape withfinished: true, persisted: true, so a run in both sources projects identically except for the finish-only fields (finishedAt,status,storedEventCount/truncated/bytes,diagnosis). A hosted parent's view also carriespipeline— the standing fold's summary (record 0065; agent-ship.md item 19): on a live registry row from the summary (folded at publish, so a fresh publish and the re-host replay agree), on a ledger row folded from its mirrored events, on a persisted row from the record — one fold (pipelineStandingOf), so a run in any source reads one standing; and the seal keeps it so: the record'spipelineis the registry summary's own — folded over the whole ship-event list (RunState.pipelineEvents) the backlog's trim never touches — handed toassembleRunRecord, never refolded from the trimmed snapshot (the fold over the record's own events stands in only where no summary exists, a reclaim over the ledger's mirror); absent on a run without ship facts and on records written before the field. Callers are authorized one layer up (command scopes/chat gates + Access); the service only assumes it. - Read merge.
listRuns({ status, visibleTo, agent?, channel?, threadKey?, parentRunId?, sinceMs?, limit?, before?, beforeId? }):limitdefault 50, cap 200 (the sharedRUN_LIST_*limits).visibleTois REQUIRED — the caller's authorization predicate (predicateFor(actor, "runs:read", "run"), authorization.md item 6;{ kind: "all" }is an explicit choice, never a default): live rows are filtered by the reference evaluatormatchesPredicate, the store receives its wire form asvisibleTo(omitted forall), andnonereturns an empty page without touching either.active= registry runs not finished, after checking their shared deletion markers when history is configured. This check fetches no history records or events.finished= finished registry runs ∪store.list;all= every registry run ∪store.list. Persisted rows are fetched with the sameagent/channel/sinceMsfilters and a bound ofmin(200, limit + liveCount).threadKeynarrows both sources to one thread's runs, newest first, ANDed withvisibleTo— live rows by theirRunMeta, the store by the filter it is handed — solimit: 1is the thread's newest run: the one read behind the dispatcher's thread read (routing-and-config.md item 3), a child's thread-aware rows (agent-conductor.md item 10) and stage A's paste check (item 2'soutcome; routing-and-config.md item 21);parentRunIdis item 58's. Every view carries the run'schannelVisibility(from theRunMetalive, the record persisted; absent on a hand-built live row) so theruns.*commands authorize a point read on the view itself. A run in both sources is ONE row. An UNFINISHED live row wins whole, and its store row is suppressed from every listing (finishedincluded): that row is the run's provisionalinterruptedtombstone (item 27) — the truth only once the run is dead — so a live run always lists as live, never as interrupted. A FINISHED live row is merged{ ...persisted, ...live }: the live row wins every field it carries (the finalstopstate, the registry'spersistedflag) except the finish fields — the record'sfinishedAt/statuswin (it is the source of truth: a reply that threw after the loop isfailedthere while the registry row keepscompleted) — and the store row contributes what only the record knows (diagnosis,bytes, the typed artifacts of item 2). The record'sfinishedAtIS the registry's finish clock (RunSnapshot.finishedAt), so the two agree whenever both are present. Rows sort by the store's own key — unfinished rows first (newest started first), thenfinishedAtdesc, then id desc — so the page is a true top-N of the union and a finished registry run sits exactly where its record will;limitapplies after the merge. A full page ending on a persisted row returnsnextBefore: { finishedAt, id }; passing it back asbefore/beforeIdpages the store with the compound cursor and omits live rows (they all sort ahead of any cursor and were on the first page). Live rows carry theirRunMeta, soagent/channelfilters apply to them exactly as to persisted rows (a live row created without meta is excluded by those filters);sinceMscomparesfinishedAt ?? startedAt. After deletion checks succeed, a history-list failure degrades to{ runs: <live rows>, storeUnavailable: true }. A deletion-marker failure rejects the read through the caller’s failure response and returns no cached rows. - Single-run reads.
getRun(id, { include? })checks shared deletion markers before returning a cached registry row (token-freegetById/snapshotById), then reads history when needed —store.getSummarywithoutinclude(metadata only, the event set is never loaded),store.getwithinclude: "messages"(events present). A FINISHED row the registry still holds (inside its 60 s TTL) is read with the store beside it: the row's identity,stopstate,status/finishedAtand events are the registry's, and the typed artifacts of item 2 —verdict,reviewHead,reviewPost,dispositions,handoff— and the record'susagewith its price (cost, costs.md item 4c; a persisted row carries both too) are overlaid fromstore.getSummarythe moment the store holds the run's record; the finish record lands in the store before the finish event that wakes a coordinator (item 47) is sent, so a reader woken by it never meets a row without them. A row whose record has not landed carries none — the start tombstone (item 27) lends nothing, not itsinterrupted— an unfinished row reads only its deletion marker when history is configured. A deletion-marker failure rejects the read and serves no cached data. Other artifact-read failures produce onewarnline and the registry row.getRunEvents(id, { afterSeq?, limit? })returns events withseq > afterSeq(strict), at most 500 per page and ≤ 256 KiB of event JSON (always ≥ 1 event), withnextAfterSeq= the last returnedseqwhile more follow; live from the registry backlog, persisted viastore.events(never a full-record fetch).seqis the registry's stamp on BOTH sides, so anafterSeqcursor taken from the live stream addresses the same events after eviction.store.events → nullisnot_found; an existing run with nothing pastafterSeq— a zero-event record included — isokwith{ events: [] }.getRunFriction(id)→{ id, finished, diagnosis }: live runs are analyzed withanalyze(events, { finished, truncated })(truncated= the snapshot's flag, item 5 of run-visibility.md), persisted runs return the stored diagnosis fromstore.getSummary— never the events.stopRunon a run outside the registry decides 409-vs-404 fromstore.getSummarytoo. After deletion checks succeed, a history-list failure insidelistRunsdegrades to live rows withstoreUnavailable: trueand ONEwarnline carrying the error message (never a token) per failing call (createRunsService({ …, warn? }), defaultconsole.warn). Unknown, malformed, expired (retention-hidden) ids arenot_foundeverywhere. - Stop with actor.
stopRun(id, mode, actor)calls the token-freeregistry.requestStopById(id, mode, actor); a live run →{ id, mode, state: "stopping" }and astop_requestedrun_note carryingactor: { kind: access | mcp | cli | chat, id }withidstripped to^[A-Za-z0-9:@._-]{1,128}$(unknownwhen nothing survives); a finished-but-unevicted run and a persisted run →conflict; anything else →not_found. The token-gatedrequestStop(the HTML button) publishes no actor. - Registry capability model.
RunRegistryenforces capability OR operator: the token-gatedhas/subscribe/snapshot/requestStopstay for the live HTML/SSE routes (defense in depth), and the token-freegetById/snapshotById/requestStopByIdserve the service.authorizeLive(id, token)is synchronous and returns aLiveRunAccess(subscribe,snapshot,requestStop) bound to the registry when the token is right for a non-evicted run, elsenull— the live SSE path stays byte-identical through it.
Consumers
- The friction ledger is the store.
RunStoreFrictionLedger(store)implementsFrictionLedger:recent()pagesstore.list(neverget) down tolimit ?? 500rows, projects only{ runId, label, agent, finishedAt, diagnosis }, and orders oldest-first with the samerunIdtie-break as the in-memory test double; the caller's predicate (LedgerReadOptions.visibleTo) reachesstore.list({ visibleTo })in its wire form andnonereads nothing. It never writes: the dispatcher's history write path is the one place a diagnosis is stored. See self-improvement.md. - Live view on the service.
src/channels/liveView.tstakescreateLiveViewHandler({ shell, service, index: { listActive, subscribeIndex }, retention, audit? })—shellis the bound web-shell renderer and the handler emits page seeds (see live-view.md's Rendering paragraph): every per-run read and the tokenless stop go throughRunsService(a valid token → the synchronousauthorizeLivelive path; the page's Stop/Kill stay token-gated throughLiveRunAccess.requestStop), the default/runsview is served from the registry's index face alone, and?all=1merges one index page oflistRuns({ status: "all", limit: INDEX_PAGE_SIZE, before?, beforeId? }), attaching the registry token only to unfinished rows in the seed (anOlder runs →link carries the cursor on). History-mode page, stored replay +end(from the same singlegetRunread), 409 stop, the omission marker, the retention tooltip, feed semantics and the audit line are live-view.md items 15–16.src/index.tspassesretentionPolicyOf(cfg.runHistory).retentionDays(null with no store), the viewer'sActor(accessActor: the browser sessionaccess:<sub>— a service token,access:svc:<common_name>, can only ever reach/api/*, see command-registry.md item 12) that the index's predicate and every tokenless run read decide on (authorization.md items 5–7), and a dev-bypass gate that is loopback-only on a localhostPUBLIC_BASE_URL(isLocalhostBase).
Deploy race
Deploy race (design gap, partially mitigated). A run that starts in the gap between the bot deploy preflight's "no runs in flight" check and the container's SIGTERM is unprotected: it gets only the drain window (15 min; a coding run's budget is 90 min), and while the old container drains it holds the slot with its Slack socket closed, so followers wait up to the drain deadline (mentions are recovered late by the reconnect catch-up). Preflight cannot lock that window; only a resumable run can close it. The durable run record is the vehicle: it must carry enough for the NEXT container to either resume the run or fail it explicitly with a card edit — never a frozen card. Phase 1 delivers: the record shape (identity, thread, agent/model, full event stream with
seq, status) sufficient to reconstruct the conversation, and a record written for every run that reachesfinish(including a drained one that finishes or is killed inside the window, asfailed). Phase 2 delivers (item 27): the provisional record at run START — written as terminalinterrupted, notrunning, so a run cut down beforefinishleaves a durable trace with NO store-side fixup by the next container. The resume step (the next container picking a cut-down run back up) is items 36–38 (0019); the store's half of it is the live-run ledger, items 28–34 below — the earlier reading that the store needed nothing for it was wrong: a resume needs a lease, the transcript the model actually saw, and a record of what was in flight. Interim mitigations: the deploy preflight, the visible drain and the orphaned-card sweep.Tombstone-first records. A run's record is written twice. (1) At START — when the run loop takes the run, after the workspace attach; the registry row, the stream and the initial
input/run_meta/contextevents date from the reservation (item 42), and a setup exit before the run loop finishes the advertised run under the same id (item 63) — when the loop begins, the dispatcher writes a provisional record from the run's current snapshot withstatus: "interrupted"andfinishedAt = startedAt(provisional on purpose: after a crash nobody knows the real death time, so the tombstone keeps the start time). Because this record is already TERMINAL, a crash or a drain-abandonment needs no next-container reconciliation — the tombstone is already the truth, andruns list --status finishedshows the lost run once the registry no longer holds it. The write goes through the samerunHistoryWriterfire-and-forget (retries + drain accounting), never delays the first model call, and isprovisional: it must NOT triggeronPersisted/markPersisted— the index's persisted flag means "finished and durably stored". (2) At FINISH, the item-15/16 write replaces the tombstone whole (same-id upsert;sameStoredVersiondiffers, so the Worker rewrites the event set). Final beats provisional: the writer stands a provisional write down the moment a final (non-provisional) write for the same id is enqueued — before every attempt, retry backoff included — and drops outright a provisional write enqueued after the final one; a stood-down write is not counted a loss. Without this, a start tombstone sitting in retry backoff when a fast run finishes, or a drain write racing a run that finishes inside the drain's write budget, could land after the finish record and overwrite it withinterrupted. The store holds the same line on its own floor (run-metrics.md item 3):upsertInTransactionanswers aputof a provisional orrestartingrecord over an existing terminal row{ ok, stored: true, retained, rewritten: false, emits: false }and writes nothing — the row, its events and the sessions table stay exactly as the final write left them — so a provisional write the in-process stand-down could not see (another generation's drain racing this one's finish) still cannot overwrite a finished record. Between the two writes, the drain insrc/index.tsupgrades what it abandons: when the drain deadline passes with runs still in flight,writeAbandonedRunRecordsgives each still-active registry run aninterruptedrecord built from its FULL snapshot (interruptedRunRecord: every event published so far,finishedAt= now), logged as[drain] wrote interrupted record for <id> (<n> events)and given a bounded ~10 s budget beforeprocess.exit— so the common abandonment carries a full transcript, not just the start-of-run tombstone. The drain writes areprovisionallike the start tombstone (the persisted flag would be a lie on a run that never finished, and the flag lives in a registry the process is about to drop) and therefore also stand down for a racing finish. The flag is persisted too: both provisional writers stampprovisional: trueon the record itself (RunRecord.provisional, items 2–3 —writeTombstoneandinterruptedRunRecordset it) and every final writer — the finish, the reclaim's close, the ship close — omits it, so the finish upsert replaces it whole;RunsServicecarries it onto the view of a persisted row (RunView.provisional,persistedView), and a store-only reader — the CLI'sruns list/runs get, the web index and run page served without this process's registry — renders a flagged record as the third stateunfinished — no finish recorded(PROVISIONAL_LABELinrunRecord.ts, an amber dot) rather than asinterrupted, because the run may still be live in a registry that reader cannot see (live-view.md item 17). While the run is alive its tombstone never surfaces: theRunsServicemerge suppresses a store row whose run is unfinished in the registry (item 20) — and, with the run ledger on, a store row whose run the ledger holds live (item 41), so a run live under ANOTHER generation is never shown asinterruptedhere. The tombstone itself stays with the ledger on: it covers the window before the claim (registry.createto the claim after the workspace attach is seconds) and the no-ledger mode, and the reclaim's finished record (item 36) replaces it whole like any finish. Inline command runs skip the tombstone: they are sub-second and their loss window is negligible. NOTE deploy ordering: the state Worker validatesstatusthrough the sharedrunRecord.ts, so it must be redeployed (to acceptinterrupted) before a bot that writes tombstones — the usual Worker-before-bot rule (item 13).The live-run ledger — one seam, two implementations (0001, 0019). A run that must outlive the bot container leaves its live state on the state Worker:
RunLedger(src/core/runLedger/ledger.ts) is what the bot calls —claim,seed,step,heartbeat,append,setState,pushInbox,requestStop,handoff,finishing,finish,reclaim,listLive,readTranscript— withWorkerRunLedger(HTTPS to the/runs/*ledger routes, the same bearer and error classes asWorkerRunStore, a409as a RESULT never a retry) andInMemoryRunLedger(the reference the bot's tests run against). Every decision is a pure function insrc/core/runLedger/decisions.ts, imported by the Durable Object and the in-memory ledger alike, so the two agree by construction. Every owner write carries the bot generation as a fencing token (gen: process start plus a random suffix):checkFencerefuses a write from a generation that does not hold the lease with409 {ok:false, reason:"fenced"}(an unknown run answersunknown-run), so a zombie old container can never overwrite a run the next one resumed. Off-state: norunHistoryconfig → no ledger (this item is inert; the bot behaves as before).One live run per thread, enforced by the store.
POST /runs/claim {storeKey, run}inserts alive_runsrow (run_id,thread_key UNIQUE,owner_gen,lease_until,started_at,phase,stop,meta_json,card_json,system_text,tools_json,state_json) on theRunHistoryDO; a second live run on the thread is409 {ok:false, reason:"thread-live", live:{runId, agent, startedAt}}— what the steer message needs — and the owner's re-claim of the same run is idempotent (decideClaim). Live runs never enter therunstable: that table'sfinished_atdrives retention and listing, so a live row there would list as finished and be trimmed first;POST /runs/live {storeKey}is how they are read, and the finished-runs routes (get,summary,events,list) answer for a live run as for an unknown id. The row carries what a resume needs and the dispatcher otherwise keeps in closures: the card{channel, ts}, the composed system prompt and the serialized tool definitions verbatim (a resume is a valid continuation on what the model saw, not a byte-identical request across a code deploy), the executor selection and worktree path, ref and head sha,state_jsonfor the dispatcher-local callbacks (verdict, PR description, checklist, pushed branch, review head, infra counters;POST /runs/state), the router's decision when it chose the run's preset (meta.route: the preset, the reason, the router's model, a compound's parts or its collapse — routing-and-config item 21; absent for a preset a person, a scope or the default chose), so a resume repaints the card as it was and a reclaim's close knows the run was routed without reading its events, and — for a run with a conversation of its own —meta.session, the run's place in its session log (item 53; session-log.md item 2): the log's key, the index its seed began at, its request row and the range it appends to. A claim naming a session registers it in the object'ssessionstable for the sweep (session-log item 7). The host key (record 0060): a ship pipeline's parent run is claimed under the thread key plus a#hostsuffix while the row'smeta.threadKeynames the thread itself, so the key column'sUNIQUEnever sees the parent — a child's claim on the thread lands beside it, and a second host-key claim in the thread is the onethread-liveit refuses (one pipeline per thread, agent-ship.md item 16). One module mints and recognises the suffix (src/core/runLedger/hostKey.ts:hostKeyOf,threadOf,isHostKey); it refuses a key that already carries the suffix and one the 256-character cap could not hold once suffixed. The row's meta carrieshosted: trueand the run'slabel, so every listing reads the marker and the label off the metadata; a hosted claim carries no seed, so no session is registered for it.The event stream appends as it happens (
POST /runs/append {storeKey, runId, gen, events}): rows into the existingrun_events(run_id, seq)under the registry's ownseq(INSERT OR REPLACE, so an identical retry is a no-op), ordinary events underMAX_EVENT_BYTES(64 KiB), review artifacts underMAX_RECORD_BYTES(1.5 MiB), or the batch is400. Artifacts retain their exact bytes and truncation flag. The HTTP client partitions large batches by encoded byte size before sending; it stops at the first refused or unknown result without replaying prior chunks. A partial failure logs acknowledged and unconfirmed event counts without event content; the write-through warning reports incomplete acknowledgement rather than claiming the entire batch was lost. The bot batches throughcreateAppendFlusher(src/core/runLedger/flusher.ts): everyAPPEND_FLUSH_MS(500 ms) orAPPEND_FLUSH_EVENTS(32), whichever first; a push never blocks or throws; a failed batch is reported and dropped, because the transcript (item 32), not the event stream, is what a resume is rebuilt from, and the finish record still carries every event. The events appended since the last flush are the only loss a hard kill can cause.The step record is written before a step's tools run (
POST /runs/step {storeKey, runId, gen, record};run_steps(run_id, step)keyed by step number, replaced on retry):{step, seq, turnIndex, inFlight:[{callId, tool}], inboxConsumedSeq, remainingMs, turn, iteration}. The client's order is part of the contract: transcript turns (item 32) land first, the step record second. Before an ancillary prompt starts, a zero-row step records the selected substantive report and its current cursor; the dispatcher acknowledges that checkpoint before new rows may land. This protects even the first ancillary split write. ThustranscriptCompletenessdecides what a resume does from the turn count alone: the count equals the last record'sturnIndex(or the seed's turn count with no record yet) → resume there and settle itsinFlightcalls; two more → an ordinary substantive step's turns landed but its record did not, so nothing of it was dispatched and its tools simply run; a prior ancillary selection instead refuses this unknown phase; anything else is a partial write and the run closesinterruptedwith every event held. The turns counted are the run's rows in its session log fromseedFrom(item 53), or its own transcript object for a row withoutsession(transcriptSource); a compaction row (session-log item 6) counts as one turn.heartbeat(POST /runs/heartbeat, everyHEARTBEAT_MS= 10 s) extendslease_untilbyleaseMsiff the caller owns the run and answers thestopany generation requested (POST /runs/stop {runId, mode}sets it from anywhere and says whether the owner's lease is live) and the row'sphase. The heartbeat body carries the run's facts (record 0064;HeartbeatFacts): the round index (the step counter), the in-flight call with its declared bound (itssinceAtthe call's owntool_callstamp, the stream's last move only until that event lands; the matchingtool_resultclears the fact, so a finished call never reads as still running), the last event's time and the newest pushed head with itscleanfact — assembled by the write-through from the step reports and events it already sees, sent only for a coding run with a known start (the row's meta and start rideadopttoo, so a resumed coding run keeps beating with facts), and judged by the plane's decider for the checkpoint steer inside the heartbeat's own transaction; a facts-less or malformed body still extends the lease. Reclaim (POST /runs/reclaim {storeKey, gen, now, leaseMs}, called once per boot BEFORE the Slack socket opens) atomically takes every row whose lease is past (lease_until ≤ now) or whose phase ishandoff, sets the new owner and lease andphase: live, and answers each with its last step record, the inbox items pastinboxConsumedSeq, its state and its jobs; a live lease is left alone.The transcript is stored whole, one row per content part, and never edited. The model's
thinkingblocks are verified by signature and a conversation whose earlier turns changed is rejected, and the persisted events cap tool outputs and omit tool inputs, so a resume needs the rawChatMessage[]the runner had. A run's rows live in its session log (item 53; session-log.md), one object per thread and agent, at the indices its range names. The per-run object described here,RunTranscriptDO— ONE object per live run named by the run id (RUN_TRANSCRIPTSbinding, migrationv6), kept apart from the history object so transcript bytes never queue behind an admission decision — is where a row claimed before the log existed keeps its rows; no new run writes to it, its routes stay until no live row lackssession, and its row shape is the log's:run_messages(idx, part, json)(one{role, part}row per content part;turnRowsrefuses a part overTRANSCRIPT_PART_BYTES= 1.5 MB by name rather than truncating),attachments(ref, media_type, data)for base64 image/document data overATTACHMENT_REF_BYTES= 1 MB (stored once, the row carriesdataRef), and anowner(gen)row — the object's own fence, set at claim and replaced by reclaim, because this object and the history object commit independently and a zombie whose history write is about to be refused must not land transcript rows either (POST /runs/transcript/owner|write|read|clear;writebefore an owner is set isunknown-run). The client writes the seed once at start (seed) and, with each step, the previous step's results turn and this step's assistant turn (chunkRowskeeps each request under the 2 MiB fence; attachments travel one per request).assembleTranscriptrebuilds the exact array in(idx, part)order, re-inflating references, and names the first gap (turn N is missing,turn N is missing part M,attachment R is missing) while returning the turns before it, so aninterruptedrecord can still carry what was stored. The Memory write transport may encode oversized parts as versioned bounded chunks in the same transcript/session store. Every chunk is acknowledged under the same original owner before its logical row, with full ordered hash/length verification; successful READ returns the ordinary legacy part/attachment DTO. Actual serialized writes stay under 2 MiB and physical values under 1.5 MiB. Unsupported Memory, unknown acknowledgement or required corrupt content holds the original input; no replacement run or silent truncation is admitted. Required read errors do not use ordinary channel/agent-log fallback. Reader-first activation requires compatible Memory maintenance and bot failure guards; rollback below that minimum remains unsupported while internal rows or orphans exist.Phases and finish.
phaseis a compare-and-swap table (phaseTransition):live → handoff(SIGTERM:POST /runs/handoff {gen, runIds}marks this generation's live runs for the next one, never another generation's and never afinishingrun),live → finishing(POST /runs/finishing, taken BEFORE the reply so a fenced old generation never answers a thread twice; a secondfinishingon the same run is409), and back tolivefrom either by a reclaim.POST /runs/finish {storeKey, runId, gen, record}writes the finished record exactly asputdoes (the same upsert, retention and policy proposal;record.idmust equalrunId) and deletes the run's execution rows (live_runs,run_stepsandrun_jobs) in ONEtransactionSync, while originalrun_inboxbytes remain held until verified native consumption — the reason the live tables live inRunHistoryDOand not a second object. A finish that is the run's terminal close on an unlatched run (neither provisional norrestarting; record 0097) also writes the record's metrics point — sent beside the record aspoint— to the Analytics Engine dataset AFTER the commit (run-metrics.md item 2). The finish clears no transcript row: the run's rows stay in its session log for the thread's next run (item 53), the record carries the run's closed range, and the client releases the log's owner best-effort so a zombie of a finished run is refused; therunsrow is stamped with the session's key and thesessionsregistry with the finish time and the object's bytes, for the sweep. A row claimed before the log existed finishes on its own transcript object, which is then orphaned and harmless. The finish also records the ending's cause on the plane (record 0064, "Endings and the watches"):ended { kind, cause }intoplane_endingsin the same transaction, one row per closed run and the first cause standing — with one keyed exception: a standingresident_replacedwas arestartingclose, the run carrying on under its own id (item 42's restart), so that run's own later finish replaces it and a restarted run that completes readscompletedhere as its record does — the record's own status word as the kind, the cause from the closed set (completed,failed,stopped,withdrawn,refused,lease_lapsed,resident_replaced,runner_gone;causeOfCloseinsrc/core/plane/decide.ts):completed,failedand the stops map to themselves, aninterruptedrecord carryingrestarting: true(the reattach path restarted the run, item 47a) toresident_replaced, and any other close tolease_lapsed, which is true and blames nobody. The bot renders the cause (endingCauseWords, the one rendering every surface shares); the object never renders. Afterfinishthe thread is free to claim again and the record lists as finished. A HOSTED ship parent's finish is the coordinator'sfinishroute (record 0060; agent-ship.md item 17): it publishes the plan's summary as the run'sanswer, finishes the registry row and writes the one record — the run's own events, in seq order — through this same transaction via the ledger sink, filed under the METADATA's thread while the row sits under the host key (item 29), so the close releases the key and the nextagent:shipin the thread claims it;parentRunRecord, the row-assembled record the recordless finish wrote before the parent stayed live, is gone. On a run another generation hosts the route answers409 not_hostand writes nothing; an untracked pipeline's finish writes no record — the parent's was written at the hand-off.The durable inbox and jobs.
POST /runs/inbox {storeKey, runId, message}appends a steered follow-up from any generation with an increasingseq(refused as{ok:false}for a run that is not live); the row is theIncomingMessageminus attachment bytes plus what rebuilds a channel handle, so the fresh-turn settle of unconsumed follow-ups works after a resume, and the step record'sinboxConsumedSeqsays which ones the run has read.run_jobs(run_id, kind)holds post-run work a handoff leaves for the next generation (one reflection per run, hence the key); jobs are deleted with execution atfinish; inbox custody is independent and remains held without a native consumption receipt. Bodies: the ledger routes are validated before any object call (400names the field), share/runs/put's 2 MiB fence where they carry a record, a transcript chunk or an event batch (/runs/finish,/runs/transcript/write,/runs/append) and the 512 KiB fence otherwise; observability lines carry ids and counts, never text.The bot's write-through (
createLedgerWriteThrough,src/core/runLedger/writeThrough.ts; wired insrc/index.tsbeside the run store asCoreDeps.runLedger, Worker-backed history only — astore: filehistory has no ledger). The process mints ONE generation at boot (mintGeneration: an ISO stamp then eight random hex, e.g.20260907T231512Z-3fa9c1d2, reported asgenerationon/healthz) and writes everything under it. Per run: claim once the prompt exists — after the workspace attach and the system composition, not at the in-process admission, because a row without its prompt could not be resumed (the registry row itself dates from the reservation, item 42) — with the composed system prompt and the tool definitions (mergeToolsof the agent's toolset and the run's MCP tools, definitions only) verbatim, the card's message handle (StatusHandle.handle, which the Slack adapter sets to{channel, ts}and the coalescer passes through), the run meta (agent, model, effort, repo/ref/PR/head,readonly,selection,workspace), then the seed: the exactmessagesthe first model call carries, followed by the seed record — step0, nothing in flight,turnIndex= the seed's length,remainingMs= the agent's whole budget (budgetMs) — so a reclaim always has a record to judge the transcript against and a row with no record at all is known to have died before its conversation was stored. The claim is awaited (one round trip) so no step record precedes it; the whole claim is retried on a transient failure (200 ms, 800 ms) and a run is untracked — it runs and replies exactly as before, one warning — when the thread's row belongs to another run (a stale row; reclaim is item 31's), the routes are missing (warned once per process), or the claim kept failing. Each step: the harness's mirror reports each step (StepReport: the turns appended since the previous report — the previous step's results turn and this step's assistant turn — their first index, the calls in flight, turn, iteration, remaining wall clock) as pi's assistant turn lands, before the tools' results (harness-pi.md item 8), and the write-through turns it into item 31's step write (turns first, then{step, seq: the registry seq last seen, turnIndex, inFlight, inboxConsumedSeq: 0 until the durable inbox, …}); a transient failure is retried once after a 200 ms backoff. Events: the registry subscription feeds item 30's flusher with the registry's ownseq. State: the checklist, thesubmit_verdict/submit_pr_descriptionpayloads and the pushed branch are merged intostate_jsonas they happen (coalesced, newest wins; a transient failure is retried once after the same backoff, and one that fails twice leaves the state dirty so the next patch carries it — the last state of a run is never lost to one blip). Heartbeat everyHEARTBEAT_MS, and astopanother generation set is relayed to the run's control once per mode. Finishing is taken before the card close and the reply (item 39: its answer gates the reply); finish goes through the writer aswrite(record, { via: run.sink })— the ledger's one-transaction finish, with the writer's retries and drain accounting, falling back to the plain store when the ledger refuses (fenced,unknown-run) or lacks the route, so a record is never dropped and never written twice. Background mirrors and delivery gates: a run detached after a refused or failed background write continues best-effort (one warning… detached: <reason> — this run is not resumable; every later write a no-op, the heartbeat stopped). An ownership fence remains a hard stop under item 39 and suppresses delivery. Before takingfinishing, ordinary answer delivery commitsanswerOutcomeandfinalStatus: if the run was tracked when this commit began, anunavailableresult suppresses the reply; afencedresult also suppresses it. A run already untracked when the commit begins may reply best-effort, without claiming that its outcome was durably saved. A ship pipeline is claimed for the live index and the finish only (system: "", no seed, no steps: each child round is a run of its own), so it closesinterruptedat a reclaim; inline command runs are not claimed. Finishing also records the status the record will carry (state.finalStatus) before the CAS, and the finish write goes out right after the reply, BEFORE the workspace release — the record does not depend on it, and on the ledger the finish is what frees the thread, which must not wait on a sandbox teardown (measured live at ~90 s).Reclaim at boot (
reclaimRuns,src/core/boot.ts; awaited bysrc/index.tsbeforeapp.start()and repeated every lease interval — item 38). The new generation takes every row whose lease is past or whose phase ishandoff(item 31's reclaim) — never a row it owns itself, whatever its lease or phase: the reclaim also runs on the sweep inside the live process, and a lapsed lease on our own row is a heartbeat that could not land (a state Worker blip), not a dead owner; taking it would launch the run a second time in the same process, and the fence, which compares generations, would never stop the first — and, unless item 38 can resume it, closes each with a record built from the ledger's own copy of the run —reclaimedRunRecord: the row's identity and meta, the events read back withPOST /runs/live-events {storeKey, runId}(RunHistoryDO.liveEvents; the finished-runs reads never see a live run),eventCount= the lastseq,finishedAt= the reclaim's clock — thenfinish(item 33), so the row, its steps and its transcript go and the record lists as finished. The status: a row taken fromfinishinghad fenced replay before replying and closes with thefinalStatusit recorded,completedwhen none; delivery remains unknown without a final receipt; an ended row also closes from its terminal event and saved result without replay; every other row closesinterrupted— when item 38 cannot resume it — and its reason names what the completeness rule found (transcriptCompletenessagainst the last step record:resumable: the transcript's N turns match the last step record,resumable (next step's turns landed, record did not), a partial write, an incomplete transcript, orno step recordfor a row killed before its seed). A hosted row is classified after reclaimed children and before the transcript rule (record 0060): a row whose state carrieshosting(hostingOf— the instance the ship branch recorded and the deadline the runner renews) has no transcript to judge, the plan runner drives the pipeline. One reclaim admission first classifies its non-hosted rows: a child taken fromfinishingor refused by the transcript rule closes and does not keep its parent alive; a child handed to the launcher, or a child row another generation still owns, does. The hosted parent is then a rehost candidate while itsuntilis future, one of those children remains live, orReclaimOptions.hostedInstanceLivesays its Workflow still runs or cannot determine its status. Past itsuntilwith no live child and a confirmed absent or terminal Workflow, it closesinterruptedthrough the same record assembly (the reason names the deadline; filed under the metadata's thread, so the close releases the host key and the nextagent:shipin the thread claims it). For a rehost candidate with no child, a plain-store record whose status iscompletedorfailed(ReclaimOptions.storedStatus, wired to the store'sget: the pipeline'sfinishlanded in the plain store because the ledger refused its write) instead makes the row abandoned — the live rows go, no record is written; a live child is authoritative over that stale projection. Otherwise the candidate isrehost, handed to item 38's launcher with the row, the hosting fact, its actually-live children and the events. The provisionalinterruptedrecord the ship branch writes at a run's start is the normal state of a live hosted run and never abandons it — the guard reads the record's STATUS, not its existence. A store read that fails is one warning and rehosts, and an unknown Workflow status retains the hosted owner for later reconciliation. Arehostrow puts nothing in the elsewhere map (threadsElsewhereOf, what the dispatcher's steer map is fed): the hosted parent occupies no thread, and a FOREIGN hosted row's entry is keyed by the ledger's key column, whose#hostsuffix no message's thread can ever match. One run's failure is reported and does not stop the others; its row stays this generation's and the next boot takes it again. A ledger without the routes or out of reach is one warning and the bot boots as before. Rows another generation still holds a current lease on (a rollout overlap) are left alone and handed back asliveElsewhere: the Slack adapter marks their cards (markForeignLiveCards) so the reconnect catch-up's orphan sweep (slack-channel.md item 8) asksisLiveCard— driven here, or live on the ledger elsewhere — and never closes a running run's card; the set is refreshed from the ledger before every catch-up scan (refreshForeignLiveCards, rows under a CURRENT lease held by another generation), so a generation that dies later loses its hold and its cards are swept like any orphan, and a failed refresh keeps the previous set. The cards of the runs closed here: a run taken fromfinishinggets a card that reports unconfirmed delivery —⚠️ <agent> · ended,⏹/⛔for a stop,❌for failed — before the socket opens (closeReclaimedCards); a saved budget outcome also reports absent or unknown authored output; an interrupted run's card is closed there too —❌ <agent> · interruptedwith a note that says what to do next (closureNote), a ship pipeline's names the PR it had opened, if any, and the re-issue that continues it (agent:shipwith the PR URL, or with the task when no PR existed), every other run's says to re-send the request — and a ship pipeline's note is also posted as a reply in its thread, because its work stands on GitHub with nobody driving it. The orphan sweep is the backstop for a card the reclaim could not close, no longer the only closer (it only looks two hours back, less than a pipeline's ceiling).POST /admin/crash(src/channels/adminCrash.ts) is the kill injection for testing a crash live: aSWITCHBOARD_INGRESS_TOKENSbearer withdeploy:write— the same authorization asdeploy restart— gets202 {ok, generation, pid}and the process exits hard 50 ms later (process.exit(137): no drain, no handoff, no finish writes; the platform restarts the container). Not a self-SIGKILL: the bot is PID 1 in its container and the kernel drops a SIGKILL that init sends itself, so a self-SIGKILL would answer 202 and leave the run finishing on the same generation. No bearer 401, no scope 403, no token map 503, only POST. The Worker shim forwards it like any other path. Records file by the metadata's thread, never the ledger's key column (record 0060):reclaimedRunRecordand the closure the reclaim hands back (ReclaimedClosure.threadKey, what the interrupted notice posts by) both readrow.meta.threadKey, so a hosted row — claimed under the host key (item 29) — closes under its conversation. The reclaim's outcome is reported to the object (record 0064): after the classification,reclaimRunsposts every taken row's word —resume,restart,rehostorclosed— toPOST /plane/reclaimed(RunLedger.planeReclaimed); onlyclosedrecords an ending (lease_lapsed), a row that carries on records nothing, and the standing causes come back so the interrupted note RENDERS the plane's word (closureNotewithendingCauseWords) and never composes one; an older state Worker without the route is one warning and today's words stand. The object's alarm helps the sweep (record 0064): it is set to the earliest hosting deadline, lease end or re-ask across the rows and re-armed only when that earliest changes (the owner's heartbeat, moving the lease end, moves it); at a lease end it only OFFERS the row — the row stays live and unclosed, claimable by any generation but its owner, the open effects re-pushed — and never ends a run: a lapsed lease on the owner's own row is a heartbeat that could not land, and the owner's next heartbeat refreshes an unreclaimed row.Resuming a run — the pieces (
src/core/runLedger/resume.ts,src/core/harness/pi/harness.ts,src/core/runRegistry.ts,src/core/runLedger/writeThrough.ts; the boot step that puts them together is the next item). The plan (planResume({transcript, lastStep, tools})) is pure: no step record →interrupted(killed before the seed); an incomplete transcript →interruptednaming the gap; then item 31'stranscriptCompletenessdecides.resume— the last step's record landed: itsinFlightcalls were dispatched and their results died with the process, so everytool_useof the transcript's last assistant turn (which must be exactly the recorded calls, elseinterrupted) is settled (settlementFor): a tool that no longer exists → a synthetic result saying so; a call's action isreruniff its tool issideEffectFree(reads, GitHub reads, web, skills) or on the closed rerun-safe list (RERUN_SAFE_TOOLS:update_status; thesubmit_*recorders); every other known tool —bash, pi'swriteandedit, the GitHub writes, a bridged MCP tool that mutates — gets the synthetic result "restarted while this call was in flight; its effects are unknown — re-check them before re-running it" (isError: truein the conversation,ok: falseon the stream). The default is the synthetic result: re-running is earned, never assumed — and on the harness nothing is re-run at all: the action decides the words of the restart note the rebuilt session ends on,its result was lost — re-check its effects before re-running itfor areruncall andits effects are unknownfor the rest (harness-pi.md item 8). The turn's calls and the recorded calls must be the same set — a call on either side the other does not carry isinterrupted. With nothing in flight, a transcript ending on a user turn (the seed, or a results turn) simply continues; the record'sturn,iterationandremainingMscarry over and the step isstepRecorded. A transcript ending on a text-only assistant turn with nothing in flight is the model's final answer, and the plan isfinish: the loop had ended, only the post-steps and the reply are owed, and the plan carries that turn's text asanswerwith the record's counters (no model call, no settlement). An assistant turn carrying tool calls the record does not know isinterrupted.run-step-fresh— the next step's turns landed but its record did not: nothing of it was dispatched, so all of its calls run fresh, the step is unrecorded (step + 1,iteration + 1,turn + 1unless the calls are allupdate_status); a fresh step whose assistant turn has no calls is the final answer whose record never landed, the samefinishwith the counters advanced. The run loop's finish entry (runLoop, on afinishplan): no model loop runs; how the loop had ended is read back from the notes the previous generation published (loopEndingOf, the last ending note wins: a softstoppednote is restored onto this generation'sRunControl, so the status isstopped_softand the answer wears the soft stop's label as if this process had taken the stop; atime_budget_exhausted,turn_budget_exhausted,stuck_looporsandbox_deadnote puts the harness's write-up label on it (harness-pi.md item 15; the last two are the deleted native loop's kinds, on rows from before it); no note is a plain answer; a hard stop is no ending here, it leaves no answer turn), aresumednote says the model had already answered, and the post-steps run with the answer in hand exactly as after a live loop: the reviewed-head settle, the workspace observation, the description turn, the coding PR post-step (open-or-edit, idempotent by itself), the review post-step. A verdict a previous generation already posted (reviewPostedBefore: the lastreview_postedevent among the replayed events) is the post-step's outcome on the record; neither the settle nor the post runs again and the reviewed head is the event's. The leftover pi is ended at the pid and root the row's facts name (harness-pi.md item 8), and a post-turn the run still owed is not run — there is no session to prompt (harness-pi.md item 14). The harness's entry (PiHarnessResume): the transcript is the conversation; the deadline isnow + remainingMsinstead of the agent's budget; aresumedrun note says how many calls were in flight and how each was settled and whether pi was found alive; the settlements are answered, never re-run — one errortool_resultper call in flight carrying the restart note, the user turn the rebuilt session ends on when pi is restarted from the transcript, or the answer the relay gives a surviving pi's re-ask for the same call (harness-pi.md item 8) — and the mirror's next step report carries that results turn. The registry (create(label, meta, {id, replay})) creates the resumed run under the ledger's run id with the events published before the restart replayed under their originalseqs (bounded like any backlog) and the counter continuing past the highest, so the finish record and the ledger's event stream stay one contiguous record. The write-through (adopt({runId, threadKey, state, lastStep, lastSeq})) takes up a reclaimed row with no claim and no seed: the heartbeat starts at once, steps number on from the last record, appends continue past the last seq, patches merge into the state the previous generation recorded. The run tools (agent-conductor.md item 8) settle by the same rule:await_runsis side-effect free and runs again — the wait picks up from the children's current ends, a child another generation finished meanwhile read from the store;send_to_runandspawn_runare mutations and get the restart result — never a second push into a child's inbox, never a second child.Resuming a run — the launch (
src/core/boot.ts,src/core/resumeLaunch.ts,src/core/dispatcher.ts,src/channels/slack.ts,src/channels/slack/statusCard.ts,src/core/nullChannelIo.ts; wired insrc/index.ts). The reclaim hands resumable rows back instead of closing them: a nonterminal row fromliveorhandoffwhose transcript is whole and whose last step record item 31's rule accepts (resumeorrun-step-fresh) comes out ofreclaimRunsasresumable— the row (ours now), the record, the transcript and the events — while everything else closes as item 36 says. The reclaim repeats (startReclaimSweep, everyLEASE_MS): a kill lands at an arbitrary point in the lease, and the next boot arrives seconds later, so at boot the dead generation's lease is usually still current and the rule rightly leaves the row alone — without the sweep a killed run would sit orphaned until the following restart; the sweep takes it within two intervals of the lease expiring, refreshes the cards other generations hold (item 36) and launches the resumes. One pass at a time; a failing pass is a warning. The launcher (launchResumes, afterapp.start()for the boot's rows and from every sweep pass) plans each run (item 37'splanResume, with the agent's static tools as the known tools — a bridged MCP tool is unknown and gets the not-available result) and dispatches it through the ordinarydispatch()with aResumeContext(the row, the last step record, the plan, the events published before the restart, their highestseq, the repo context from the row's meta); a run whose plan isinterrupted, whose agent this build does not know, or whose channel cannot be resumed on is closed with that reason (closeReclaimed); the launcher tells its caller once per run when the resume is over (onDone: the dispatch settled, or the run closed here), which is how the harness door stops holding for that run's bearer (harness-pi.md item 7). The resumed dispatch runs through the same admission, workspace attach, tools, post-steps and finish as a fresh run, with these differences: the ledger row is adopted at the admission point — before the card, the repo resolution and the attach, so the heartbeat keeps the lease through a slow resident attach — never claimed or seeded; a resume is never a follow-up: a thread that already has a run in flight (the user re-mentioned the bot after the kill) closes the reclaimed rowinterruptedwith no reply and no steer, and a resumed dispatch that ends before its run starts (an unknown provider, a refusal, a gate) closes its adopted row the same way, so the sweep never relaunches a run that cannot start; the repo context is the row's, never re-resolved from the message; the attach-head guard does not run (the stored head is the run's; the settle after the loop still reconciles a moved head); the conversation is the transcript and the system prompt is the row's, verbatim; the run is created in the registry under its own id, with its original start (the row'sstartedAt, so the record, the card's elapsed time and the admission slot's — the steer ack's "N in" — span the whole run) and the earlier events replayed under their seqs — the card repainted as the first generation painted it, the row'smeta.route(item 35) putting theroute reason: <reason>note back on the label (adebugnote, routing-and-config.md item 28 — painted only when the request's level shows it, and no footer on the close), the router never asked again (the resumed message pins the preset by directive) and no secondrouteevent published — so the run page URL and the record are the same run and the record is one contiguous stream; no secondinput/run_meta/contextevent and no tombstone; only this generation's events are appended to the ledger (the subscription starts after the highest replayed seq); the dispatcher's state — checklist, verdict, PR description, pushed branch — comes back from the row, the verdict and description re-validated through the tools' own parsers; the runner re-enters from the plan (item 37), or, on afinishplan, the loop skips the model and runs the post-steps with the answer the transcript holds. The message the dispatch runs under pinsagent:/model:/effort:from the row's meta with the original request text from itsinputevent as the label — the model sees the transcript, not the message. The channel: a Slack run resumes on its thread (resumeSlackIO: the thread and requester from the row's METADATA —resumeIoTargetandresumeMessagereadmeta.threadKey, never the ledger's key column, which on a hosted row carries the#hostsuffix no channel's thread can match; record 0060) and keeps the card the previous generation posted —status()edits it in place instead of posting a second one, so the thread shows one card whose frames carry on (a card deleted since falls back to a fresh one); an HTTP or MCP run, whose caller is gone, resumes on a null channel (nullChannelIO: the reply is logged, never sent — its deliverable is the record and, for a coding run, the PR — and the run's seal saysreplyOk: falsewithreplyNote: "no channel to deliver to", because a reply nobody could receive was not delivered); any other channel closesinterrupted. The reconnect catch-up never re-dispatches the original mention: it wore the previous generation's 👀 and is handled. Ship pipelines never resume through a dispatch (item 35): a hosted parent comes back from the reclaim asrehost(item 36) and the launcher re-hosts it instead — no plan, no channel handle, no agent of this build needed — the registry row is recreated under the run's id (create(label, meta, {id, startedAt, replay}): the row's original start, the label the row's meta kept, a fresh live token, the METADATA's thread on the identity — never the ledger's#hostkey — and the ledger's events replayed under their seqs), the ledger row is adopted (adoptwith the row's state and the highest replayed seq, itsonStop/onFencedwired to the recreated run's control, so the heartbeat keeps the host key from the moment of the rehost), and the write-through is subscribed after the highest replayed seq, so only what this generation publishes — the runner's later writes — appends to the ledger; the row's card, when it has one, is claimed as this process's own (keepCardLive, wired to the adapter'sadoptLiveCard) — the rehost dispatches nothing, so nostatus()would register it, and an unclaimed card would be closed as interrupted by the reconnect catch-up's orphan sweep (slack-channel.md item 8) seconds after boot while the runner and its children keep running; the runner's later redraws edit the same message and its terminal frame closes it — andonDonefires at once, there being no dispatch to settle.The drain is a handoff, and a fence is a stop (
src/index.ts,src/core/runLedger/writeThrough.ts,src/core/drain.ts). SIGTERM: after the Slack socket closes, the drain marks every run a resume can continue —LedgerRun.resumable: its seed and seed record landed, it was adopted from a resume, or it is a hosted ship parent (record 0060: a row with no process of its own, which the next generation re-hosts); never a detached run or a run whose seed failed —handoffon the ledger in one call (LedgerWriteThrough.handoff,POST /runs/handoff), so the next generation takes them at once whatever their lease (item 31's reclaim, item 38's launcher). Those runs are not waited for: they keep running here until the exit, and their writes are fenced the moment the next generation reclaims them. Only the runs a resume cannot continue hold the drain, up toDRAIN_DEADLINE_MSas before; with none, the drain waitsHANDOFF_BUDGET_MS(6 s) for pending history writes and reflections and exits. The handoff is the recovery for a roll that happens — a hard kill, a forced deploy — not a licence to roll: the deploy preflight still refuses while runs are in flight and fails by name past its budget (slack-channel item 8, release-and-deploy item 13), because a resume the next generation fails (a harness refusing the rebuilt session) ends the run it was meant to save. A handoff the ledger refuses (unreachable, no routes) is a warning and the old wait. The drain-deadline abandonment writer skips the handed-off runs: their record is the ledger's. A handoff closes no row, so the plane records no ending for it (record 0064): the cause is assigned only when a row closes — by the next generation's finish or reclaim close — never by the drain that handed it on. The finishing gate:finishing()answersok(reply),fenced(another generation owns the run — thelive → finishingCAS was refused because the row was reclaimed, or the row is gone because the other generation already finished it) orunavailable(the ledger could not be asked, or the run is untracked or was detached for a reason other than a fence: the run is this process's, reply as before) — a run detached BY a fence (a heartbeat, step, append or state write refused asfenced) answersfencedfrom then on, so the reply and the record stay the other generation's. A handed-off run that completes before the next generation reclaims it replies itself:handoff → finishingis allowed for the owner (the reclaim then finds no row), so the handoff never discards a finished answer. Onfencednothing more reaches the thread from this process — no reply, no card close, and no record: the run is the other generation's now and its record is theirs to write; a partial record from here could race the real finish. A fenced write stops the run: a heartbeat, step, append or state write refused asfenceddetaches the run (item 35) and, once, tells the dispatcher (onFenced), which hard-stops it — its next tool call would act on a run someone else is driving.LedgerWriteThrough.liveRuns()names the runs this generation drives (opened or adopted, not finished, not detached);/healthzis unchanged.The durable inbox (thread-admission item 5 has the admission side).
run_inboxholds the follow-ups steered into a live run, one row per push with a per-runseq(the message with its attachments when it fits the route's 512 KiB body — thread-admission item 5 has the cap and the drop note; a staged file's reference — name, size, type,url_private, message id, execution.md item 20 — is metadata and rides the base row, so it survives the restart even when the row drops its inline attachments, and reads back as the same reference);pushInboxis never fenced (a steer arrives on whichever container is up). It returnsnot-livefor an absent live row andinvalid-messagewhen custody cannot be verified;readInbox(runId, afterSeq)(POST /runs/inbox/read, any generation) is the same slice on demand — the resume's re-read at adopt time, so a push that landed after the reclaim's snapshot is found; the write-through answers it empty with a warning when the ledger cannot be asked. Each step record carriesinboxConsumedSeq, the highest inbox seq among the follow-ups the run had folded in when the record was written (the runner keeps the counter; a resume starts it at the last record's), andinboxDeferredSeqs, the seqs at or below it the run was handed and its model has not read — pi's harness names every follow-up still queued or steered-but-unechoed there, since one drain can hand pi an ordinary steer and the plane's provider-up row and pi reads the reissue prompt first; the reclaim hands the resume exactly the items past the cursor plus the deferred ones (unreadInbox, shared by both ledgers; the Worker refuses a deferred list that is not positive integers) —ReclaimedRun.inbox, carried throughResumableRunandResumeContext— and finish preserves original inbox rows rather than crediting delivery or cursor advancement as native consumption. The write-through'spushInbox(runId, message)answers the sequence after a valid append, or undefined with a warning only for explicit target absence. Other rejections and storage errors propagate without delivery or fresh-routing fallback. A null ledger still represents disabled durability. The plane writes rows here too (record 0064): the checkpoint steer — one fixed sentence for a coding run whose in-flight call is past its declared bound (or the no-bound line) or whose round pushed no head pastnoPushMinutes(default 15;PLANE.noPushMinutes), at most once per run per round per cause and the same sentence at most once per round — and the reissue steer for a run parked on its provider. A provider-up decision writes the typed sender-planemessage as arun_inboxrow and offers asteereffect carrying that row's per-run sequence inside the same transaction (the dedupe rows rideplane_reservations, kindsteer/park, and the park plus any open live steer go at the run's seal). The state Worker pushes the committed effect through/plane/effects; before the local registry can accept it, the push path atomically verifies the open effect and authoritative owner generation on the ledger and renews that owner's lease, so reclaim cannot cross the synchronous inbox put and a stale registry row left during handoff or reclaim overlap defers. The owning generation puts the already-durable message into its in-memory inbox, where the sequence deduplicates push, heartbeat and reclaim delivery. It acknowledgesdoneafter that put,skippedwhen the sequence was already accepted, anddeferredwhile draining or when another generation owns the run;doneandskippedclose a steer only under the same authoritative owner fence. A deferred or stale-acknowledged offer rides later owner heartbeats, and the durable row remains the reclaim fallback./runs/liverows are what the admission map is built from (ThreadsElsewhere), replaced whole on every reclaim pass;LiveElsewherenames the thread, start and agent for it.One registry on every read surface (live-view item 17). With the ledger on,
RunsServicetakes it as a read source: the ledger's live rows whose run is NOT in this process's registry — live under another generation (a rollout overlap), or reclaimed here and not yet launched; never a run this process is attaching, whose registry row dates from its reservation (item 42) — are live rows on every surface, built from the row's meta and start and the events the ledger holds (readEvents: count and activity), withownerGennaming the generation driving them and never a capability token (the page token is the other generation's).listRunslists them underallandactive(neverfinished, never on a cursor page — live rows sort ahead of any cursor) and suppresses their store rows whatever was asked (item 27);liveElsewhere(visibleTo)is that slice alone, for the default index.getRun(with the events on a messages read),getRunEvents(a seq page) andgetRunFriction(a live diagnosis) answer for such a row after the registry and before the store;stopRunasks the ledger (requestStop), which the owning generation reads on its next heartbeat, and answersstopping. A run in both the registry and the ledger is the registry's (once). The viewer's predicate applies to ledger rows as to any row. A ledger that cannot be read is one warning per call and the registry (and the store) alone — never a failed read. A failed live-row listing setsledgerUnavailable: trueonlistRunsfor every status and cursor page: a short fallback listing is not proof of cross-generation completeness, including for tombstone suppression. This is separate fromstoreUnavailable, since readable history remains available. An empty successful ledger listing, a later successful retry, or a deployment without a ledger carries no failure marker. The web surface admits such a run tokenless (live-view item 16): its page token is the other generation's, so the viewer's attribute decision is its only gate; the page renders live with a running server clock and time allocation, and its explicit EventSource follow tracks the ledger by sequence until a terminal receipt; default tokenless replay reads remain finite snapshots for work folds, the friction route answers the live diagnosis, and the tokenless stop route stops it through the ledger for a viewer holdingruns:write(the tokenless view route remains operator-only; the command surface adds requester cancellation under authorization item 5a). One ledger listing serves every service read withinLEDGER_LIST_TTL_MS(2 s) — a page view's three reads, or a history read of a finished run, cost one ledger call — and the rows' events are read in parallel; a failed listing is not kept. The remote event stream refreshes without overlapping reads; unavailable or denied reads close transport without reporting a terminal outcome. The owner and sweep retain all execution authority. A deferred checkpoint event marks a ledger view asrecoveryPending: truewith activitypaused for storage recovery, including when its pause handoff was not confirmed. A later recorded resume clears this projected hold. This read-only fact does not resume the producer or change its ownership.A run is on the ledger from before its workspace attach, and a kill there restarts it (thread-admission item 5). Until this item the claim (item 35) came after the attach, so a run was carded and in flight for the whole attach — a minute on a resident's mirror mutex, minutes on a cold clone — with nothing on the ledger: a kill there lost it (no resume, no record, the card left for the orphan sweep; seen live on a review whose resident attach waited out the mirror mutex). Now the dispatcher reserves the row right before the attach (
LedgerWriteThrough.reserve,POST /runs/claimwithphase: attaching): the run's id (minted from the registry at admission,RunRegistry.mintId, and handed back tocreate), thread, start, card, the run meta, and the request inmeta.request— the message in the durable inbox row's shape (durableInboxMessage: text with its directives, sender, link, thread, arrival time, the attachments when they fitDURABLE_INBOX_MAX_BYTES, elseattachmentsDropped) — with an empty prompt; the heartbeat runs from here, so a long attach keeps the lease. **The registry row is created immediately after the reserve (registry.createwith the minted id, the card's start and the composed label), so the run is one row on every surface from admission on: the runs index lists it with its label and capability link, the run page serves it, the card carries the live link through the attach, the HTTP ingress answers with its id, and a stop relayed during the attach latches in its control for the run loop's first step. (Before this the row came after the attach, and the index showed the reservation meanwhile as a labelless item-41 ledger row with a tokenless link that 404'd once the run was promoted.) Its stream is live from registration too:bindRunafter the reservation (the spans before it — the root, the ack card, the repo resolution — backfilled; the reservation, the attach and the resident's grafted steps streamed as they happen), theninput,run_metaandcontextpublished right after, before the attach — so the run page shows the request and the setup steps while the workspace is still being attached, and the index's event count moves. The request stays the record's first content event; the setup spans ahead of it are head material (isHeadMaterial:request,slack.receive,dispatch.*), so the protected head runs unbroken from the first event through the request. The tombstone (item 27) still waits for the run loop. Every advertised run keeps its identity through setup: a shared finalizer closes a pre-model exit under that same id, through the ledger's atomic finish when tracked or the history store when untracked (item 63). The claim once the prompt exists is the same request as before and promotes the row in place (decideClaimWrite:insertfor a fresh thread,promotefor the owner's attaching row — prompt, tools, card and state land, phaselive, identity and start untouched —refreshfor an owner's re-reserve,keepfor any re-claim on a row past attaching), through the same tracked run (OpenRunRequest.reservation), so the seed and every later write are exactly item 35's.phaseTransitionallowsattaching → liveandattaching → finishing(a dispatch that fails before its prompt); neverattaching → handoff— an attaching run has nothing to resume from, so the drain waits for it as it always did. A ship run is never reserved: it claims its own row without a seed (item 35) and closesinterruptedat a reclaim as before. Reclaim (item 36) leaves an expiredattachingrow in that phase (reclaimPhase) and hands it to the launcher as aRestartRun— row, unconsumed inbox, no transcript. The launcher (launchResumes) reads the request back (messageFromInbox) and dispatches it again as the message it was — directives included, so the same agent and model resolve, and for a routed row the row'smeta.route(item 35) carried into the route stage, so the same preset resolves with no second router call and the fresh record gets itsrouteevent (routing-and-config item 21) — with aRestartContext {row, inbox}: the dispatcher claims the thread in-process with the row's original start, reserves the row again (the owner's idempotent re-claim, for the heartbeat), folds the carried inbox plus a re-read of anything later into the run's inbox (item 40's rule, no second ack), keeps the row's card (resumeSlackIOwith the row'scard.ts) and id (registry.createwithidandstartedAt), and runs the request from scratch — attach, prompt, promotion, steps, finish — so the record carries the original start and both the request and the follow-ups asinputevents. A restart onto a thread that has a newer run in flight (the user re-mentioned after the kill) closes the reserved rowinterruptedwith no reply. A reservation that meets the row of a run this process is still closing — a restart from that run's request, or the fresh turn for its unconsumed follow-ups, both dispatched from its finally — waits for that finish and claims again, rather than running untracked (item 54 has the rule and its bound). A fenced reservation — the lease lapsed during the attach and another generation reclaimed the row — is told throughonFenced; the dispatch stops at the attach's end, releases the workspace and says nothing: the run is theirs to restart, and a promotion that finds its own run under another generation answers the same way (claim→fenced, nothing seeded). An ordinary run's promotion whose claim goes untracked — the retries spent or the route missing while the reserved row stood — abandons the row fromopenitself (neverclose, which only stops the heartbeat and leaves the attaching row for the reclaim sweep to restart while the untracked original still runs), says why throughOpenRunRequest.onUntracked, and the dispatcher publishes theledger_untrackednote and marks the carduntracked by the ledgerfromclaimRun, as the reserve-time path does (item 54); the abandon owns the write-through's dead-reservation bookkeeping, so an abandon that itself fails leaves the run known there and the thread's next claim abandons the row again (item 54's safety net). Every fresh run that reachesregistry.creategets one setup finalizer before its link is exposed. The finalizer stays active through promotion, seed and working-state commit. An ask-once refusal, moved-head gate refusal, failed attach or other pre-model exit after registration finishes the same run ID through its tracked reservation or untracked history store, retains its finished row and record, and releases the thread before a queued follow-up claims. A stop keeps its stop status. A fence leaves the row to its new owner without a model call, reply or finish from the old generation. Only a reservation that never reachedregistry.createhas no setup finalizer or advertised link; the outer finally abandons that reservation before settling the thread. A fenced or untracked reservation has no owned row to abandon. The in-process steer's durable copy (item 40) lands from the reserve on; only a steer in the moment between admission and the reservation (the ack card, the repo resolution) rides in memory alone. The same finally is a second net for a registry run a branch of the dispatch opened itself and left unfinished —runShipBranchopens its own row and finishes it on every non-hosted exit (agent-ship.md item 16), but a row a branch leftrunninghas no runner behind it: a stop is a request to the runner, so it can never take, and the shutdown drain waits on the row to its deadline. Once the branch has returned or thrown, any run bound to this dispatch — the trace's bound run (RequestTrace.runId, set atbindRun), the thread slot's (LiveThread.runId) — that is still unfinished is finishedfailed, with a log line naming the run and the branch, before the thread is settled. The one deliberate survivor is the hosted parent of a completed hand-off (record 0060): the branch names it (ShipBranchEnd.hostedLive) and the plan runner'sfinishends it, so the net leaves it live.A narrow lookup: the submitted PR description for a head (reading-diff.md item 7).
findSubmittedPrDescription(store, { repo, pr, headSha }, limit = SUBMITTED_LOOKUP_LIMIT = 10)(src/core/reviewDescription.ts) is the one read of the store the dispatcher makes for a running run:store.list({ limit, visibleTo: { kind: "repos-in", repos: [repo] } })— the repo the review is authorized for, newest first — thenstore.geton each row, scanning its events newest first for areview_artifactpr_descriptionwithorigin: "submitted"whoserepo,prandheadShaall equal the key; the first match wins, itsfromRunIdkept (a review run's copy names the coding run). At mostlimitrecords are read; no new store method, filter or column — the record'srepois the only index used, and a store that has no such record answers "none" afterlimitreads.RunDeps.runStoreis the seam (NullRunStorein a process without run history: the lookup finds nothing and the review parses the body). A store failure surfaces to the caller, which degrades to the parsed body.A finished record gains events by exactly one path — a whole-record rewrite through
putthat only areview_artifactuses (reading-diff.md item 8). Records are otherwise immutable once finished (the finish write is the last one, item 16). The abridged reading diff is produced AFTER the review, soappendReviewArtifact(src/core/reviewAbridge.ts) re-reads the record, drops a previous meat artifact (a--forcereplaces, never accumulates), stamps the new event with the nextseqafter every event the record ever held (the replaced one included, so anafterSeqcursor that saw the old one sees the new), bumpseventCountfor a new artifact (not for a replacement), refits the byte budget withfitRecordToBudget— whose per-event cap leaves areview_artifactalone: itsdiffis capped by its producer aboveMAX_EVENT_BYTESby design and itssummaryis one line — andputs the whole record: the same-id upsert every store already implements (rewritten: true, the DO rewrites the event rows in one transaction). It refuses by name when the record is gone (the run's record is gone — nothing to append to), when the artifact would not survive the budget, or when the store answersstored: false(the record fell outside retention in its own write); nothing is ever created by the rewrite. No other writer touches a finished record. The rewrite emits no metrics point: it lands on a run whose emission latch is already set, soemitsanswers false (run-metrics.md item 2).The
profilefield (record 0026; routing-and-config.md items 2 and 4).RunRecord.profile?: { preset, machine, identity, minutes, boundedBy? }is the effective profile the run was admitted with — the preset's name, the machine class its tools executed on, the identity it acted as, the wall-clock minutes it ran on and, when a boundary clipped that budget below the preset's own, the scope whose boundary did (defaults | channel | user | directive) — so a reader can tell a clipped budget from a declared one and a reclaimed run keeps the budget it was admitted with. Written by the one record assembly (assembleRunRecord,profileunder the preset's name) at the tombstone and the finish, and by the reclaim from the ledger row'smeta.profile(LiveRunMeta.profile, the same shape without the preset — the row carriesagent); the drain deadline's tombstone of a run whose registry row predates profiles carries none.isRunRecordchecks it structurally when present — a known class and identity, a positive numeric budget, a known clip scope, a string preset — and a record without one (written before the field existed) is accepted as before. The seed a fresh run claims with (item 35) carries the effective budget, so the step record'sremainingMsa resume runs on (item 37) is the clipped one, never re-derived from a preset.The
parentRunIdfield (agent-conductor.md; routing-and-config.md item 20).RunRecord.parentRunId?: stringnames the run that spawned this one or its thread — set by the dispatcher on a child started throughspawnChild()(DispatchOptions.parent.runId), on a run a person's reply starts in a thread that spawn opened (the lineage stage,src/core/dispatch/lineage.ts; agent-conductor.md item 10: the same parent at depth 1, no clock), and on nothing else — on every surface a run has: the registry row'sRunMeta.parentRunId(soRunSummary, the index feed andRunsService's views carry it, live and persisted alike), the ledger row'sLiveRunMeta.parentRunId(so a reclaimed child's record still names its parent), the tombstone, the finish record and the drain's interrupted record.isRunRecordaccepts a value of the run-id shape (RUN_ID_PATTERN) and refuses anything else; a record with no parent carries no key, and every run a person or a schedule started has none. It is what a listing draws a tree from and what the fan-out cap counts (a parent's live children in the registry); the index and the label are unchanged.run-finished-<runId>rides the state Worker's finish (http-ingress.md item 9; record 0029). Every terminal record commits throughRunHistoryDO.finish— the finish after the reply (item 33), the reclaim's close of an unresumable run (item 36), the admission's close of a superseded resume or restart (items 38 and 42) — so the handler is the one place a coordinator's child reports its end: after the transaction commits, and only for a record carryingparentInstanceId(item 48), the object sends that Workflow instance one event of typerun-finished-<runId>— the run id under a prefix in the platform's alphabet (letters, digits,-,_; a space, a colon or a dot is refused asworkflow.invalid_event_type, a refused send being a lost event the parent recovers from only at its nextread-record) — through itsSHIP_COORDINATORbinding (sendRunFinished,src/core/coordinator/contract.ts) with the payload{ runId, status, finishedAt, parentInstanceId }. The type carries the run id so eachwaitForEventmatches its own child and a duplicate is buffered harmlessly; the parent confirms every event throughread-recordbefore it acts, so the payload carries ids and a status, nothing more. The send never touches the commit: a fenced finish sends nothing; an engine that refuses (the instance ended, or is unknown) is swallowed, named in the answer'sevent(sent | none | no-binding | failed) and in one warn line; a Worker without the binding commits as before and answersno-binding. The binding is optional on the state Worker on purpose: it is a cross-script binding to the bot shim'sShipCoordinator, the state Worker deploys before the bot, and a binding to a class that has never deployed fails the state Worker's own deploy — so the class ships first (the shim's own same-script binding,deploy/cloudflare/coordinator.ts) and the state Worker's binding followed one release later — its template names the bot'sShipCoordinatoracross scripts by the bot's script name ({{bot.script}}, the Workflow named<bot script>-ship-coordinatoras the shim names it), so a state Worker deployed without the binding still commits and answersno-binding. Records that reach the object throughputalone also send the event when they are a child's END outside the ledger's finish — the run loop'sinterruptedclose of a superseded or reclaimed run, the pi harness's typed restart closing the old run, a resume abandoning a lost workspace — so the parent's wait settles at once instead of walking out the child's budget: the send fires for a stored record carryingparentInstanceIdwhosefinishedAtis past itsstartedAt, which excludes the start tombstone (finishedAt=startedAtthere), and the parent still confirms byread-record, so a duplicate send is harmless. A tombstone at a run's start sends nothing, and a lost send still falls back toread-recordat the wait's next chunk (http-ingress.md item 9). The object also speaks at a claim (record 0064): a claim whose meta namesrestartOfunder a coordinator (LiveRunMeta.restartOf, set by the restart-from-request dispatch) sends the parent instance onechild-resumed-<runId>through the same binding, best effort — the plane's twin beside the bot's own announcement (item 47a); a duplicate is consumed and re-armed by the driver's wait, harmless — and a Worker without the binding claims as before.
47a. A deploy roll under a coordinator child is said twice: a typed event on the child's record, and its Workflow twin beside run-finished-<runId> (agent-ship.md item 15; issue 1364 part 2). A child interrupted or resumed by a roll — the bot's own restart resuming the run, or the resident's container replaced under it — leaves the fact on its own record and tells its waiting parent, so the plan runner ends the round with the child's own reason at once instead of walking out the child's budget, and never reads a survived roll as a lost round. The record events. child_interrupted ({ parentInstanceId, reason }) is published straight to the registry when a coordinator child closes interrupted for a restart from its request: by abandonLostWorkspace when a resumed child's workspace cannot be re-attached (item 54, appended to the closing row's record past the highest replayed seq beside the resumed note), and by the run loop's interruption (a replaced container whose relaunch was refused, another harness's row — item 54's mid-run outcome), the reason the note's own words. child_resumed ({ parentInstanceId, summary }) is published by the dispatcher when a resumed coordinator child's workspace re-attaches and the run carries on under its record, tag and budget — and by the run loop when a coordinator child's relaunch succeeds in the replacement container (item 54's mid-run resume, the resident roll survived): the typed event beside the harness's sandbox_restarted note, with no Workflow twin from there — a resumed child settles nothing, the parent's wait keeps waiting on the finish. Both ride only runs whose dispatch carries the coordinator tag (item 48); every other run publishes neither. The Workflow twin. The reattach path sends the parent instance child-interrupted-<runId> or child-resumed-<runId> (sendChildSignal, the same alphabet and best-effort rule as sendRunFinished: a refusal is answered, never thrown — a lost send costs the wait a chunk, not the round) through the process's WorkflowSender (the shim's event relay, http-ingress.md item 12); the run loop's interruption sends nothing itself — its terminal record's put already sends run-finished-<runId> (item 47). The driver's wait (agent-ship.md item 15): each wait chunk arms three waitForEvents under durable names — the finish under the step's own name, the interruption under <step>/interrupted, the resume under <step>/resumed[/n] — the finish or the interruption settles the wait as the event outcome (confirmed by read-record before the machine acts, as every wait is), a resumed event is consumed and re-armed under the next name and settles nothing (a resumed child keeps the wait), and the chunk times out only once the finish and interruption waits both have. The restarting ending (record 0064): the close a restart follows carries restarting: true and restartUntil on its record (RunRecord.restarting / restartUntil, stamped from the injected close clock by abandonLostWorkspace through closeResumedRow), the deadline one central RESTART_CLAIM_GRACE_MS after finishedAt — one whole coordinator wait chunk. read-record searches for the same instance-and-key successor first, so a live or finished successor claimed before or after the deadline always wins as restartedAs; only with none does the restarting close decide the answer: one millisecond before the deadline it is still running, at or after it is the original interrupted ending with restarting suppressed from the projection and the cause read from its existing events, never wall_clock_cap. A process that survives still takes the eager path: recordRestartDeath immediately rewrites the close without restarting or restartUntil and appends restart_died. A legacy restarting record without restartUntil retains the old still-running behavior; normalizeStored drops the restarting projection for a deadline before its close or beyond the central grace, failing closed without making a successor invisible. The plane's child-resumed-<runId> from the successor's restartOf claim (item 47) lands beside the bot's own, and a runner that hears it twice re-arms its wait and loses nothing.
- The coordinator's tag:
parentInstanceIdandidempotencyKey(http-ingress.md item 9). A run a coordinator spawned carries two required identity fields on every row it has, plus the admitted cost cap when one exists, set by the dispatcher fromDispatchOptions.coordinatorand by nothing else:parentInstanceId, the Workflow instance it is a child of (the platform's id alphabet, at most 100 characters,INSTANCE_ID_PATTERN), andidempotencyKey, the key its spawn carried (<parentInstanceId>:<step>, the step any name without a colon —<unit>/<round>/<kind>for the plan runner;IDEMPOTENCY_KEY_PATTERN). The tag (CoordinatorTag) also carriesbase, an optional field that is NOT a record field, the branch the child's pull request targets — the parent record'sbase(item 49), set by the spawn when the instance knows one — read by the child's PR post-step alone (pr-description.md item 5), an instruction to the run likeDispatchOptions.contract— but one the claim publishes as a typed event so it survives the spawning process (item 48a);coordinatorFieldsspreads the two identity fields and optionalcostCapUsd, leavingbaseout; the cap survives on rows and records. The identity and optional cap ride whereparentRunId(item 46) rides: the registry row'sRunMeta(soRunSummary, the index feed andRunsService's views carry them — live here, live on another generation's ledger row, and persisted), the ledger row'sLiveRunMeta— checked at the claim (a malformed key, instance id or cap is 400, and so is either identity field alone) and stored there, so a retried spawn finds its child before any record exists and a reclaimed child's record still names its instance — the tombstone, the finish record, the drain's interrupted record and the reclaim's close (assembleRunRecord'scoordinator, the one assembly). A refused claim on a thread names the live run's key beside its id, agent and start (decideClaim; the in-memory ledger and the object agree), so a coordinator's spawn can tell its own child from a busy thread without a second read. The tag is both fields or neither:isRunRecordaccepts the two by shape together, refuses a malformed one and refuses either alone, and the claim's parser holds the same line; a run no coordinator spawned carries neither key.
48a. The tag survives a bot roll: admission persists it, restart and resume read it back, and the coordinator store is the second guard. DispatchOptions.coordinator lives only in the spawning process's memory, so a coordinator's child re-admitted after a bot death would reach its PR post-step with no tag and treat the unit branch as its own base. Three layers close that hole. (a) Admission of a fresh coordinator child persists one coordinator_tag event before registration acknowledgement — { parentInstanceId, unit?, base?, issuedFindingIds? }, with the exact issued IDs bound only for a findings child, the unit read off the idempotency key's step (unitOfIdempotencyKey, the head of the step before its first /) — mirrored onto the ledger row like every event; a resume republishes nothing, an attaching restart replays the original stream without duplicating its tag. (b) A resume without a tag in its options rebuilds one (carriedCoordinatorTag): the two record fields from the row's meta, the base and issued IDs from the row's coordinator_tag event; a row written before the event existed yields the tag with no base or IDs and restores its admitted cost cap from row meta, and a run no coordinator spawned yields none. (c) The run loop resolves the base once, before the harness session opens — the tag's, else instance.base read from the coordinator instance store by parentInstanceId (item 49), for a coding PR run only — and both the gate's push rules (harness-pi.md item 7: the recovered base is the protected branch, the unit branch the run's own) and the PR post-step read that one answer; when a coding child still has no base — no event, no instance — the PR post-step opens nothing: with a description in hand it publishes pr_not_opened saying the plan's base was lost across a roll and replies with the compare URL, and it never falls through to the binding ref or the repository's default branch (agent-ship.md item 4).
The parent ship record (
CoordinatorInstance,src/core/coordinator/contract.ts; the seamCoordinatorInstanceStore,src/core/coordinator/instanceStore.ts). What the bot writes at a coordinator instance's creation and the spawn route reads the requester, channel and thread from, so no step ever takes an actor from its caller:{ id, kind: "ship", userId, userName?, authenticatedAs?, postedBy?, channelId, channelName?, threadKey, sourceUrl?, generatedTask?, generatedTaskSource?, repo, branch, base?, createdAt, plan?, caps?, card?, runId?, label?, attempt? }— the instance's identity and surfaces plus a first-generated-unit admission checkpoint:generatedTaskbacks up the admitted task when the first instance write precedes its unit row;generatedTaskSourceretains the original admission run and source URL across re-issues, while the unit row (item 50) owns subsequent unit state;planthe plan the instance runs (its id, the file's name, and its path in the repository),capsthe pipeline's caps as the profile gate clipped them (the rounds cap and the wall clock per unit),cardthe status card in the requesting thread the bot redraws from the coordinator's round events ({ channel, ts },StatusHandle.handle),runIdthe id the bot writes the parent's record under when the instance ends andlabelthe card's label — the instance's own two surfaces;attempt(an integer from 2) which re-issue of the plan the instance runs (agent-ship.md item 16: a later attempt lives underplan-<plan-id>-<attempt>and reruns the units not merged), absent for the first; beyond that first admission backup, unit state is the unit's row (item 50). One seam, two implementations (record 0001): the state Worker'scoordinator_instancestable on the run-history object —POST /runs/coordinator/put {storeKey, instance}→{ ok: true }, idempotent for the same record and409 { ok: false, reason: "exists" }for a different one under a taken id,400for a record the shared validator (isCoordinatorInstance) refuses;POST /runs/coordinator/replace {storeKey, instance}→{ ok: true }replaces only valid draft unit rows with no execution or publication facts, in the same transaction. A retained task claim, recovery journal, legacy ending, wake decision or unknown unit field refuses as{ ok: false, reason: "exists" }without changing the saved owner. Replacement is for an unstarted attempt whose Workflow creation was confirmed absent by the shim;POST /runs/coordinator/get {storeKey, id}→{ instance | null },400for an id outside the platform's alphabet; the same bearer and size fence as every ledger route — behindWorkerCoordinatorInstanceStore, which throws on an answer it cannot read rather than guessing (a spawn on a guess would be a spawn nobody asked for); and the in-memory double.NullCoordinatorInstanceStoreis the store of a process without a Worker-backed run history (no history, a file store, a missing bearer —buildCoordinatorInstanceStorefollowsbuildRunLedger's rule): it knows no instance and refuses a put asunavailable, so every coordinator route then refuses by name.The coordinator's unit rows (
CoordinatorUnit,src/core/coordinator/contract.ts; the same seam and store). One row per unit of the plan an instance runs — a task string is a plan of one unit,task— keyed by(instanceId, unit):{ instanceId, unit, slug, title?, branch, dependsOn, threadKey?, sourceUrl?, reviewThread?, issue?, pr?, resume?, record?, lastPush?, publication?, segments?, idle?, wakes?, rounds, ending?, startedAt? }— the unit's branch (plan/<plan-id>/<unit-slug>, or the task's ship branch) and the units it waits on, written at the instance's creation; its thread once the runner opens it (a task's is the requesting thread from the start) and, on a row a bot wrote before record 0055, its review thread (reviewThread: { threadKey, sourceUrl? }, retired: a new row never gets one and every child runs in the unit's thread, agent-ship.md item 5; a row that carries one keeps its review rounds there), its board issue when one titled by the unit id exists, its pull request, aresume(the open pull request of ship's own the requester named, its head and url — written by the hand-off on a task's row, read by the driver so the unit opens at its review round; agent-ship.md item 10), the renewals it spent (segments: { index, from?, runId?, at }[], one per segment the grant opened after the first, written byunit-endon acontinuedending before that segment runs and never twice for one index, so a reclaimed runner finds the row; decision 0046), the round boundaries the coordinator reported (theship_roundvocabulary, oldest first) and how it ended (the kind, the thread's report, when) as the runner reaches them — or that it idles (record 0051; agent-ship.md item 8):idle { why, at, renewalsLeft, from?, runId?, spendUsd, handoff?, wakes }, written byunit-endon anidleending in place ofendingso the unit stays unfinished —whythe old kind (at mostIDLE_WHY_MAXcharacters, refused longer at the route), the continuation facts beside it (an idledreview_pendingnames the pending head asfromand the body'sheadShastill lands aslastPush),wakeszero at the write; every futuremerge_readyending likewise writes its final reviewed full head tolastPush; a normal reply may repair a legacymerge_readyrow only from one full head jointly attested by that same unit's completed coding record (its final head or an exact-unit-branch durablepushed[]head) and posted approving review record, with exact parent/idempotency/requester/repository/pull-request binding — a same-thread standalone run is not unit evidence — after fresh exact requester, open same-repository pull request, head/base ref, live head and sole-owner verification, by one conditional atomic transition that requires the exact unchanged legacy row, reserves sole ownership, and replaces it with bothlastPushand the complete existing-PRpublicationbinding; invalid, ambiguous or stale evidence leaves the row unchanged; indexed answers live beside it aswakes: Record<waitId, UnitWakeAnswer>so replay reads the samesegment,answered,stoppedorexpireddecision, and the state Worker writes that answer, its segment row and consumed event marks atomically; a later realendingdrops the idle from the row — with the report still posted to the unit's thread and the parent card's line readingidle · <why>— so a unit's thread, branch, pull request and ending are one row a person can read, and a runner instance resumes at the first unit without an ending by reading the rows, not its memory. On the object:coordinator_unitsbeside the instance table;POST /runs/coordinator/units/put {storeKey, units}upserts each row whole (a replace keeps the row's place; an empty list, more than 200 rows, or a rowisCoordinatorUnitrefuses is400);POST /runs/coordinator/units/claim-legacy-continuation {storeKey, expected, recovered}compares and replaces one unit row in a single transaction (409 stalewhen the exact expected JSON no longer stands);POST /runs/coordinator/units/list {storeKey, instanceId}→{ units }in the order first written (the plan's),400for an id outside the platform's alphabet,[]for an unknown instance; the same bearer as every ledger route. The in-memory double keeps insertion order the same way; the null store refuses a unit read rather than certifying an empty list, and refuses a put asunavailable. The state Worker's test pool binds the bot's script as a stub Worker carrying aShipCoordinator, so the cross-script binding resolves in workerd and a test that needs a Worker without the binding installs its absence on the live object.A run on the pi harness is one more row of the same shape (harness-pi.md item 8). The harness's mirror writes the ledger's step records as the native loop did — the assistant turn with its calls in flight before the results, the results and any steer's text as the next step's user turn, the echoed prompt skipped as the seed — so a pi run's transcript is
ChatMessages andplanResumereads its row unchanged; pi's compaction entry is one more row of the run's range (item 53; session-log.md item 6), written as its own step with nothing in flight. The row'sstate.harnesscarries the facts a resume needs beyond the transcript —{ pid, logOffset, root, sessionFile, bearerHash, container }, written when pi starts, the offset moved to the boundary after each row the mirror writes (harness-pi.md item 8 says what it names and why never a turn's end) — and the resume launch hands the plan to the harness like any run's, which either re-attaches to the container's pi at the recorded offset (the calls in flight settled on the relay first, so the extension's re-ask for one reads the restart result; the continue queued as a steer, since a pi inside a call refuses a plain prompt) or restarts pi on a session file rebuilt from the transcript (the same result written as those calls' tool results) — on either path every call in flight is answered with the restart result of item 37 and never re-run. Threerun_notekinds are pi's alone (compacted,harness_error,tool_refused; run-visibility.md item 1); no event type is.The
seedfield (routing-and-config.md item 20; agent-conductor.md item 3).RunRecord.seed?: RunSeednames where the run's conversation started —channel, its own thread's history, as for every run a person, a schedule or a coordinator started;parent, a spawned child seeded from admitted structured evidence at the spawn (DispatchOptions.parentContext); orsession, a follow-up on the pi harness seeded from the tail of its own session's log (session-log.md item 9), whosesession.seedFromnames the log index that tail began at — set by the dispatcher from the seed source it resolved and by nothing else, on every surface a run has: the registry row'sRunMeta.seed(soRunSummaryand the index feed carry it, and the drain deadline's interrupted record reads it off the row), the ledger row'sLiveRunMeta.seed(so a reclaimed child's record still says so), the tombstone and the finish record — the one assembly (assembleRunRecord) carries it. A child'scontextevents are its seed turns when it has them, so the record shows what the model saw.isRunRecordacceptschannel,parentandsessionand refuses every other value; a record written before the field existed carries no key and reads as it always did.A run is a range of its session's log (session-log.md; record 0035). The transcript of a run with a conversation of its own lives in
SessionLogDO, one object per working session (SESSION_LOGSbinding, migrationv9), which holds every run of that session's rows in order and is kept when a run finishes. Ordinary work uses<threadKey>:<agent>; a unit lane uses<instance>:<unit>:codingor<instance>:<unit>:reviewacross rounds. The shared<threadKey>:@threadcontrol log carries connector and report turns separately from those fenced execution rows (session-log.md item 13). The write-through reads the log's tail before the claim and the row'smeta.sessionnames the range the run occupies —{ key, threadSession?, seedFrom, request, range: { from } }, where a fresh claim'sthreadSessionnames the exact shared thread key and an adopted legacy claim retains its absence: the seed's rows are appended fromseedFrom(a thread's first run of an agent starts at 0; the next starts where the last left off), the request is the seed's last user turn, and every step's turns land atseedFromplus their index in the run's own conversation, under the same generation fence and(idx, part)upsert as item 32 and before the tools run as item 35. The finished record carriesRunRecord.sessionwith the range closed at the last turn written, orrange: "broken"when a refused or twice-failed write detached the run (item 35) — the log then ends short of what the model saw, and the record says so; a record withoutsession(written before the log, or a run without a conversation) validates and reads as before. A reclaimed row resumes from its log (transcriptSource; item 31) and keeps appending there; a row claimed before the log existed resumes from and finishes on its ownRunTranscriptDO. Retention:runs.session_keynames the session a finished record was a range of and the object'ssessionstable lists every session a claim or a record named; the enabled 6-hourly sweep, after it trims the runs, drops each session object no remaining run row or retained child context reference names and whose thread has no live run — owner row first, then the rows — and its registry row (session-log item 7). Storage per object is bounded byRetentionPolicy.sessionLogMaxBytes(session-log item 5). The tool events name their rows. Atool_callcarrieslogIndex, the log row of the assistant turn it rode in, and atool_resultthe row of the user turn its batch's results make: the harness asks the mirror which of the run's own rows each is (PiMirror.rowOfCalls— the last assistant turn the ledger holds, written here or met again on a re-attach;rowOfResults— the next row, where pending results and a steer's text go first) and the ledger run for that row's place in the log (LedgerRun.logIndexOf:seedFromplus the index, the one arithmetic every seed and step write uses), so a stamp and the row the step wrote cannot disagree. A run without a session, a record from before the field and a call the mirror cannot place (a re-attach whose transcript ended on a compaction) carry none, and every reader takes such a record as before. Theassistantevent is not stamped: the bridge emits it as the turn ends, before the mirror writes the row, and its prose shares that row with the turn's calls, which place it. This is how a search hit's turn finds its step (session-log.md item 11).The row records where the run's workspace is, and a resume re-attaches there; a legacy lost workspace restarts the run, while a pilot writer pauses on its original binding (resident-repos.md items 24 and 66; harness-pi.md item 8). Until this item a resumed dispatch (item 38) attached its workspace exactly as a fresh run does: the resident's attach treated the run's own dirty tree as disposable and recreated it under the run's still-living pi, or raced the run's in-flight install and failed, and the factory's fallback then moved the run onto a cold sandbox with a
cold_sandboxnote, where the pi probe looked in the wrong container and read a living pi as dead. The binding. The claim (item 35) writesstate.bindingbeside the harness facts:{ backend, workspace?, user?, container? }(WorkspaceBinding, fromworkspaceBindingFor(selection, machine): the selection's backend, and the known checkout from either a resident attach or sandbox seed, plus a resident pool user and container; nothing for thenoneclass, which has no workspace). The re-attach. A resume reads it back (carriedWorkspaceBinding: the state's binding, or on a row written before it a binding derived frommeta.selectionandmeta.workspace) and hands it to the attach asExecutorContext.reattach, and the factory then provisions nothing: a recorded resident is probed and asked to keep the thread's tree as it stands (reuse: true, resident-repos item 66), and its answer must name the run's own worktree and pool user; a recorded per-thread backend re-makes the per-thread executor for the thread with no resident probe, so a run that started cold is never moved onto the resident; the recorded backend is the only one consulted. A successful re-attach writes the binding back complete (a row from before the container was recorded learns it). ExplicitresidentIdandplacementIdmetadata also stays with that binding. Outside pilot preservation, a failed reattachment with any explicit resident address, including malformed metadata or a contradictory reply, fails setup without restarting on an address-free binding (exact resident address). The restart for ordinary legacy runs. A resident foundrestoringis first waited through, never refused at once (execution.md item 27's held/await-restorerequest, on the re-attach too — issue 1364 part 1): the run's worktree lives on that resident or nowhere, and its restore ends on its own, so the hold — bounded by the restore ceiling clipped to the run's lease — re-attaches the same worktree and the run carries on under its id, record, tag and budget instead of restarting from its request. Past that, anything short of the run's own tree (a refusal, an unreachable or unserviceable resident, a needs-ref, another tree) is aWorkspaceReattachRefusedError; for an opted-in pilot writerattachWorkspaceholds the same ledger row and recorded binding for later recovery, while an ordinary legacy binding without resident address metadata answersreattach_refused, and the dispatcher (abandonLostWorkspace,refuse("workspace_lost")) publishes aresumednote naming the refusal, closes the card, closes the adopted rowinterruptedwith that note appended to its record past the highest replayed seq (an operator's stop that ended the re-attach's wait is not a refusal: the request endsstoppedand the row closes with the stop's status,stopped_hardorstopped_soft, neverinterrupted— execution.md item 9), and, once the outer finally has freed the thread, dispatches the row's request again in the thread under the SAME run id (prepareRestartTurn: the request asmeta.requestcarried it with a fresh receipt and its own root, the follow-ups the resumed run never consumed appended as a fresh turn appends them), provisioned as a fresh run is, on a fresh card, and naming the run it restarts (DispatchOptions.restartOf) so thread admission never steers the request into the row it closed. The restart keeps the run's identity (issue 1880; one identity, as a renewed lease opens a new segment under record 0046): the closing run's registry row is read before the wind-down discards or seals it (carriedRunIdentity: its retained events, its capability token, its original start) and rides the dispatch asDispatchOptions.restartCarried;registerRunthen reusesrestartOfas the run's id, replays the carried events under their seqs, keeps the token (CreateOptions.token) and the start — so the page a person opened for the run, every posted capability link (the card's, a unit thread's) and the ledger row stay valid across the replacement, the runs index and the plane's tables count one run (the registry re-creates under the same id, replacing the finished row; the ledger's live row and the stored record share the id and every list dedupes on it; the run store's put is an upsert, so the run's final record replaces the interiminterruptedone), and the record reads as one run whose transcript carries the replacement as ONEresumednote in user words ("container replaced, resumed from the request" for a replaced container; "workspace lost, resumed from the request" for a lost workspace), published between the replayed segment and the restart's own turn. Only when the closing run's registry row is already gone does the restart run under a fresh id — a page that moved, never a request that vanished. The reservation and the finish in flight. The restart — and the fresh turn for a closed run's unconsumed follow-ups (thread-admission item 4), dispatched from the same finally — is dispatched right after the closed run's finish was handed to the history writer, so its reservation (item 42) can reach the ledger ahead of that write — for a restart that keeps the run's id, as the owner's idempotent re-claim of the very row the landing finish is about to delete; for the fresh turn, answeredthread-livefor the row it is about to release. A claim under a run id whose own finish is named inlandingtherefore awaits that finish FIRST, before its first ledger call, and then claims — the row gone, a fresh row for the run's new segment is inserted; a finish that failed for good leaves the run's own row standing and the idempotent re-claim keeps it, the run riding its row tracked (noledger_untrackednote: the row is its own). The write-through keeps every finish this process is landing named by run id (landing, from the sink's firstputto the outcome: landed, refused, or failed for good), and a claim answeredthread-livefor a run named there claims once more after awaiting that finish — its own promise, an awaited event, no timer of the write-through's own; what bounds it is the finish's attempt sequence, every ledger call of an attempt aborting at the store client'sAbortSignal.timeout(RUN_STORE_TIMEOUT_MS; a finish is the state flush, the append flush and thefinish, so two or three calls), the writer's fixed backoff between its attempts, and the caller's final word: a failedputsettles nothing by itself — the sink never re-derives a retry policy — and the name settles as failed when the caller saysRecordSink.abandoned(the history writer at its give-up: a missing route, a permanent failure, the attempts spent; a one-shot closer,closeResumedRow/closeRestartRow, right after its own failed put), so no waiter outlives its caller — the word is required on every sink (a store, keeping nothing in flight, answers no-op), so a sink that keeps something in flight cannot forget it and park its waiters; the writer says it from its defensive catch too (a rejection outside its stop rules — the injected sleep, a hook — is still its give-up), and a one-shot closer says it throughputOnce, which puts a record once and says the word on a failure before rethrowing. A dead reservation of this generation. Athread-livenaming a run this generation reserved and never promoted, with no tracked run driving it and nothing landing for it, is this process's own reservation that never started and whose abandon did not reach the ledger (item 42's one unretried call): the claim abandons it again from there and claims once more, so no dispatch ordering in the finally has to be right; an abandon that keeps failing leaves the run untracked with that why.reserveanswers the outcome itself (ReserveOutcome:trackedwith the run,untrackedwith the why,fenced, orofffor a process without a ledger), and the reservation stage puts the why onReservation.untrackedWhy. Athread-livenaming a run whose finish landed in this process most recently (landed, the newestLANDED_MAX= 256 finishes, oldest out — an entry covers only the moment after its finish, and a resident roll finishes every live run within seconds, so the newest few hundred hold far more than that window needs; an older one is a stale row) is the moment between the answer and the row's release, and is claimed once more at once; a row named neither there nor inlandingis another run's for real and is untracked in one claim, as item 35 has always had it. The second answer is the last: a finish that failed or was refused leaves the row standing, and the run goes on untracked, by name, never refused (an untracked run still runs and replies, item 35; a refused request would be lost) — and says so on its own record, whichever way the reservation ended untracked: the write-through hands the why back on every untracked exit — the row of a run this process closed still stood after its finish settled (did not land (<error>),the ledger refused its finish, or another run's row holding the thread now), the row is another run's whose finish is not in flight here, the state Worker has no run-ledger routes, the claim kept failing — and the dispatcher publishes it from one place as aledger_untrackednote (head material, likecold_sandbox) right after the reservation, the card's label carryinguntracked by the ledger— the bot log's warning is not the only witness. The wait sits inside the reservation, so the in-memory-only steer window (item 42: from admission until the reserve resolves) spans it too; nothing is lost unless the process dies inside it. Before this rule the reservation was refused and the restart ran untracked — no row to hand off, resume or reclaim, and the store holding only its start tombstone, which any reader without this process's registry (the CLI'sruns get) shows as a finishedinterruptedrun while the run turns on — and carrying the coordinator tag the interrupted run had (DispatchOptions.coordinator, item 48a: the run loop's interruption hands over the tag it ran under, a refused re-attach rebuilds it from the row's meta and itscoordinator_tagevent), so a coordinator's child restarts as the same instance's child — its unit branch stays its own push target and the plan's base stays the branch its pull request targets, where a restart without the tag read the unit branch as the base, refused the child's push and could not open its pull request (thread-admission.md item 5); a row whose request cannot be read ends there, the note and the card saying so. The transcript is not continued on a fresh tree: the work it describes is gone with the tree, and a restart from the request is the honest continuation. Never a silent migration: a resumed run reaches a sandbox only if its row says it ran on one. The same step mid-run. The re-attach is one step the dispatch-time attach and a run in flight share (reattachWorkspace: the binding handed to the factory under thedispatch.workspace.attachspan, the factory's refusal read by name), so a process relaunched in a replacement container can re-attach the run's binding without the dispatch-time gate — the ask-once question, the card and the reply stay the dispatch-time attach's (attachWorkspace). Outside a pilot writer's recorded preservation policy, an interruption mid-run is the run loop's own outcome, never an exception the dispatcher catches: a container replaced under the run (harness-pi.md item 16), a row another harness wrote (harness.md item 7) — the loop finishes the runinterrupted, ends the process, releases the workspace, closes the card🔁, and answersRunInterrupted(kind: "interrupted"; the card'sreason; the request'srefusalby name; the record'snote; andrestart, the request to run again with the run it restarts), and the dispatcher reads the refusal as the request's outcome and dispatches the restart exactly as it dispatches this item's, once the thread is free, once. Under a living bot a container replaced under the run is first a relaunch, not an interruption (harness.md item 6): the loop re-attaches the run's binding through this item'sreattachWorkspace— the same round back, the resident asked to keep the tree as it stands — before the process is started again in the replacement, and only an ordinary run's refusal by name (the refusalworkspace_lost) or the relaunch ceiling closes the run this way; a pilot writer's re-attach refusal leaves its row and workspace binding for the next generation.A quoted conversation is on the record (record 0037). Right after
input, the provision stage publishes onereferenceevent per conversation the request pointed at and the references step quoted: the block as the model saw it (text, redacted, under the per-event byte budget by the step's own 32 KB cap), the classifier's freshchannelName, thechannelId, the message count and the permalink (url). The finished record derivesreferences: { url, channelId, messages }[]from those events at assembly (referencesOfEvents, the wayprcomes frompr_opened), absent when the run quoted nothing, so a pull request a steered run opened traces back to the text that steered it;isRunRecordaccepts a record with or without it. The event is head material (isHeadMaterial): published contiguously afterinput, it survives the backlog trim and the record budget that drop the oldest events first, so the audit trail is never the first thing to go; a captured events file replayed through the analyzer keeps it too (isRunEvent). The friction analyzer counts the event as a side fact; the card's activity line names itreferenced thread quoted; theinputevent stays the request text alone.The record carries what the run cost (costs.md, cost by user).
usage—{ turns, byModel: { <provider>/<model>: { turns, inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens } } }— is summed at the one assembly (assembleRunRecord→usageOfEvents) from everymodel.turnspan end the run published, BEFORE the byte budget can cut a middle event, so every record written since carries it, zero turns included; a turn without a model attr counts underunknown. The Worker store keeps it in its own column (usage_json, NULL for a record written before the field) and answersPOST /runs/usage {storeKey, sinceMs, untilMs}→{ runs: [{ id, userId, userName?, parentRunId?, threadKey, channelId, agent?, startedAt, finishedAt, usage? }], parents: { <run id>: { userId, userName? } }, pending, earliestFinishedAt?, retentionDays }: one row per kept run that finished in the range, oldest finish first, plus the identity of every parent a child names that is outside the batch and known to the store. The arithmetic is the bot's (aggregateUsage,reportOfUsageRows,RunStore.usage): a child billed to its parent's requester (a parent in the batch, inparents, or the child's own id when no parent is known), the runs summed into usage cells — one per (billed requester, thread, channel, agent, UTC day of finish),wallMsthe summed finish − start, a record naming no agent underunknown— the cube every cost dimension is a sum over (costs.md items 10–10a), so a snapshot holds cells for allocation and individual coordinator-child points for the Ship spend trend, each with its durable unit key, finish time, recorded usage and admitted cap when known. Other runs have no per-run point. A row withoutusage_jsonis priced there and then from its storedmodel.turnevents and written back — at most 200 a call, the rest answered withoutusageand counted inpending— so the history heals as it is read, with no operator step; a run without turns is written back as the zero usage so it is never re-read. The range may span at most 366 days and must be non-empty; a local store (InMemoryRunStore,FileRunStore) answers the same cells over its records, pricing a usage-less record from its events, so it never reports pending; the null store answers an empty report.The failure by name (harness-pi.md item 6; session-log.md item 9).
RunRecord.failure?: { kind: "policy_refusal" | "provider_transient" | "model_stream_incomplete" | "sandbox_fleet_busy" | "restart_unclaimed" | "harness_ending_unconfirmed" | "runtime_shutdown_unconfirmed" }(RunFailure,RUN_FAILURE_KINDS) names a failure or an unconfirmed runtime ending when the failure has a name a reader acts on:policy_refusal— the model provider refused the run's call under its usage policy, read from the stop reason its wire names and never from the explanation's words;provider_transient— verified provider transport failure spent the lease-backed retry window (a gateway 5xx or HTML page, overload or reset);model_stream_incomplete— pi ended an answer stream without a complete answer and spent its local retry lease, with no provider failure established (execution.md item 29 and harness-pi.md item 6). Ship reads either model retryable kind to re-run a round-0 child only when its workspace had nothing to checkpoint; a pre-model sandbox fleet refusal may recheck the original PR head and retry review inside the same unit (agent-ship.md items 8 and 9).harness_ending_unconfirmedretains an original producer whose end did not acknowledge exit.runtime_shutdown_unconfirmedretains a killed run whose producer or required cleanup did not confirm shutdown. These records refuse destructive removal; a finished stream is not a shutdown receipt. The run loop sets model failures from the harness's typed errors (ModelPolicyRefusedError,ModelTransientFailureError,ModelStreamIncompleteError) and hands it to the one assembly (assembleRunRecord) through the finish record; the shared setup finalizer sets sandbox_fleet_busy only from the executor's typed spent-wait facts before any model turn;restart_unclaimedis the run store's own: its sweep closes arestartingrecord whose claim deadline passed with no successor holding the id asinterruptedwith that kind (run-metrics.md item 3a); the tombstone and the drain deadline's interrupted record carry none — a run that has not failed has no failure. The runs service carries it on the view of a persisted run (RunView.failure), which is how the thread read finds the request rows the next seed leaves out.isRunRecordaccepts the named kinds and refuses any other shape; a record without the field — a run that did not fail, one that failed for a reason without a name, one written before the field existed — validates and reads as before.Listings across runs: a unit's, a parent's, a session's, a pull request's (agent-ship.md items 17 and 18, agent-conductor.md item 11, session-log.md item 11;
src/core/runsService.ts,src/core/unitRuns.ts,src/core/findingsLedger.ts).RunListOptions.parentRunIdnarrows a listing to the runs naming that parent (item 46) on every store —selectListItemsfor the in-memory and file stores; aparent_run_idcolumn on the Worker'srunstable, indexed in the page order and written beside every row atput, a table from before the column gaining it at construction filled once from each record's ownsummary_jsonso a child stored earlier still lists under its parent (RunHistoryDO.migrateRunsTable, idempotent, the one method every column migration lives in) — and the/runs/listbody validates it as a run id.RunListOptions.pr({ repo, number }) narrows a listing to the runs that name one pull request the same way (namesPullRequestinsrc/core/runRecord.ts: the row'srepois the pull request's andpullRequestNumberOf(row)— the coding post-step'spr.number, else a posted review'sreviewPost.target.number, nothing for a skipped post — is its number):selectListItemsfor the in-memory and file stores; apr_numbercolumn on the Worker'srunstable, indexed withrepoin the page order (runs_pr), written beside every row atputand filled once at construction fromsummary_jsonby the same rule for a table from before the column (migrateRunsTable) — and the/runs/listbody validates it as anowner/nameslug and a positive integer, its two bound parameters taken from the same headroomvisibleTofits under only when asked.RunsServiceadds four reads over its one listing, each undervisibleTo(run-visibility.md item 9):listUnitRuns(unitKey, visibleTo)— the unit's row from the coordinator's store (item 50;RunsServiceDeps.units, the instance for the requester's identity when the reader sees no run), both threads' runs through thethreadKeylisting, cut byunitRunsOf— answeringUnitRunsView = { unit, instanceId, threads, rounds, runs: (RunView & { round, thread })[] }ornot_found;listChildren(parentRunId, visibleTo)— theparentRunIdlisting in start order;searchSession(key, query, limit, visibleTo)— the ledger's session search (RunsServiceDeps.sessions: the same ledger object asledgerin the bot; the CLI builds it and the coordinator's store from the run-history config beside its run store,buildCoreCommands) answeringSessionSearchView = { session, hits: [{ turn, role?, snippet, runId?, gap? }], gaps }, each hit'srunIdthe finished or admitted active run of the session whose exact recorded or live range (item 53; session-log.md item 11) holds the turn andgapthe newest gap marker the hits straddle below it.listFindings({ repo, number }, visibleTo)— theprlisting joined with the unit rows that name the pull request, handed toledgerOf(agent-ship.md item 18) — answeringFindingsLedgerViewornot_found.runs unit,runs children,runs search,runs findingsandruns list --parentare their commands (command-registry.md).The intake receipt store (record 0058; routing-and-config.md item 27).
RunLedger.recordIntake(key, receipt)writes one row tointake_receiptson theRunHistoryDO, keyed by the message (<channel>:<ts>), as insert-if-absent insidetransactionSync— the first writer's row stands, this and every later write answers{inserted, stored}with the stored row (decideIntakeInsert, the shared decision), so one unmentioned thread reply is decided once across processes. The row (IntakeReceipt,src/core/runLedger/types.ts— the shape the intake seam re-exports) holdsverdict,reason,source, optional typedproviderFailure,mode,model,gen,threadKey,decidedAt.readIntake(key)answers the row or none;listIntake({threadKey?, since?})answers a thread's rows or the rows since an instant, oldest first — what the live false-silence ratio reads. The routesPOST /runs/intake {storeKey, key, receipt, windowMs?},POST /runs/intake/read {storeKey, key}andPOST /runs/intake/list {storeKey, threadKey?, since?}carrystoreKeylike every run route, refuse a malformed key, receipt (isIntakeReceipt) or filter as 400 and no bearer as 401. Provider-failure delivery is a second durable row:POST /runs/intake/delivery/claim {storeKey, key, poster, claimedAt}atomically grants one poster the receipt-keyed right to publish unless delivery already succeeded or an unexpired claimant stands;POST /runs/intake/delivery/finish {storeKey, key, poster, delivered}closes the owner's success forever or releases its rejected post.INTAKE_DELIVERY_CLAIM_MSbounds recovery when a process dies while owning an unfinished claim, and a stale/non-owner finish changes nothing. When scheduled cleanup is enabled, rows are pruned on the object's alarm after the larger of 24 hours and the writer's reconnect catch-up window plus the drain deadline (intakeReceiptRetentionMsinsrc/core/budgets.ts;prune_afterstamped at the insert from the write'swindowMs, the client'scatchUpWindowMs), so a catch-up that runs after the longest allowed drain still reads the verdict instead of deciding again. While cleanup is paused, expired receipts remain readable and the same keys continue to deduplicate. The write-through retries a lost response as the claim does (RETRY_MS): the retry READS the same row — the insert may have landed — and a row carrying this write'sgenanddecidedAtanswersinserted: true, another writer's answers what it stored, none at all is inserted once more; a missing route, a permanent refusal or a failing retry answers undefined with one warning — the caller degrades rather than falling silent (record 0058). The in-memory ledger implements the receipt reads/writes and the same atomic delivery claim with a failure toggle (intakeFailure) so tests can make a write or a read throw on demand. The Slack gate writes and reads these receipts on both the live and catch-up paths; a provider-failure receipt preserves the cause so neither path reconstructs it from the rendered reason.The operator's shadow decision beside the routed request (record 0057; routing-and-config.md item 29). Under
routing.operator: shadoworonevery admitted chat event's decision is oneoperatorevent on the run that carries the request — the agent run's record beside itsrouteevent, the inline command run a typed line becomes, or the live run a reply was folded into — published like therouteevent (OperatorEventFields) and projected onto the record asRunRecord.operator(operatorOfEvents, therouteOfEventspattern). The event holds the mode, the outcome (binds,question,refusalornon_decision— the structured seam's floor, record 0067), the reason, the seam'sattempts(what each answer violated, or that it was accepted), the bound lines redacted and cut like the receipt (ROUTE_RECEIPT_CAP) — never the message text — a question's proposed line (proposal, cut the same way: what the next turn's "yes" binds, routing-and-config.md item 29), the intake gate's verdict when the gate was present, and the turn's latency and output tokens for the replay's medians (load-harness.md item 17). Underona decision that starts no run records on a door record of its own (recordOperatorDecision: agentdoor, statusfailedfornon_decisionandcompletedfor a question or refusal, the redacted request asinputand the decision as its oneoperatorevent); a bind that runs carries the event on its command run. It follows the record's retention like every event; a record without the field validates and reads as before; nothing runs from it undershadow.A child's durable context manifest.
RunRecord.childHandoffcarries the boundedChildHandoffmanifest from the parent: source identities, session ranges, typed source receipts, tool-result references, notes and attachment references.isRunRecorddelegates its validation toisChildHandoff; malformed manifests are rejected on persisted reads. The manifest is separate from the coding outcome's existinghandoff. A new session claim recordsRunSession.threadSession, the canonical<threadKey>:@threadmigration marker, including when a work unit uses its own session key. The marker must be a valid session key for that record's exact thread; legacy records may omit it. The operator's repository provenance includescontextfor a canonical repository inferred from notes, repository briefs or other admitted context.Original source evidence survives finish.
RunRecord.sourceReadsarchives the canonical source-action state, including original response bodies and pending or unknown actions, under that run's exact id, requester, agent, channel and thread.sourceReadStatevalidates this owner against the record rather than trusting the archive's owner field.contextDependenciescarries the bounded canonical dependency closure; an origin naming this run must match its identity. Finish and reclaim retain these fields andchildHandoffbefore the live row disappears. Full internal record reads retain source bodies; listing rows excludesourceReads, and every outwardRunsServiceprojection removes the archive, raw child context and dependency envelope, including full-record reads that include events. A malformed archive or dependency envelope remains invalid, never a source-free legacy record. Retention uses dependency references without exposing bodies in its summaries.Exact owner evidence for resident cleanup. The bearer-protected
POST /runs/preservation-owner {storeKey, runId}reads one run ID. A live owner takes precedence over a stored summary, including a provisional summary. Only a non-provisional terminal record is returned as terminal; an unknown or malformed record never authorizes resident deletion. This read does not use the retention-filtered list or summary route. The resident uses the fixed store key and the existing state Worker credential.An advertised run survives setup failure. A fresh run reserves before its registry id or Live run link is published. A pre-model exit finishes that same id, through the tracked ledger reservation or the history store when untracked; it never creates a second door record. An untracked reservation omits the Live run link because its history write is not yet proven. A stop keeps its stop status. A capacity ending has a typed failure and zero turns, so a review with no verdict or GitHub post is distinguishable from one that reached the PR.
Validation criteria
| Criterion | Proof |
|---|---|
| An optional HTTP read-scope preview returns the exact requested channel, requester and repository with permission from the same compiled list predicate and the channel-directory machine stamp, even when history is empty. It creates no run or ending fact, grants no access, and requires the paired requester/repository options and an HTTP channel, refusing partial or unsupported identifiers. Lists without this option retain their existing shape; revoked command grants cannot obtain a preview. | [unit] src/core/commands/runs.test.ts::runs.list::proves exact HTTP read scope from the existing policy even when history is empty; ::refuses incomplete or unsupported HTTP scope requests; ::a revoked command grant cannot obtain a scope proof |
| Hard stop fences the exact original and terminates only proven owned work. A stable native replacement epoch may acknowledge runtime retirement while retaining unknown historic effects and original journal bytes; UID reuse by a successor never authorizes a signal. Without original native review target receipts, missing bindings may request positive discovery only from the recorded Resident backend, never infer an absent workspace or guess a target. Unknown runtime or terminal acknowledgments retain the fence and cannot claim shutdown. | [unit] src/core/runLedger/originalPromotionHold.test.ts::original promotion hold diagnosis::hard stop cancels an unconfirmed original without confirming its source or retaining an active run; deploy/cloudflare-memory/runLedger.test.ts::original workspace durability in real SQLite::hard stop cancellation closes a real SQLite unconfirmed original without releasing its source; ::a lost cancellation terminal acknowledgment reconciles only a committed record…; src/execution/runStop.test.ts::run runtime stop::*; src/execution/sandboxLifecycleReader.test.ts::sandbox run cancellation::*; deploy/cloudflare-resident/workspaceSettlement.test.ts::resident run cancellation::*; src/core/runsService.test.ts::RunsService.stopRun::keeps a refused cancellation fenced and seals the same ticket after an acknowledged retirement; src/core/commands/runs.test.ts::runs.stop::names a known shutdown refusal without claiming the stop was unrecorded or suggesting replay; deploy/cloudflare-resident/workspaceSettlement.test.ts::resident cancellation admission::*; src/core/runLedger/originalPromotionHold.test.ts::original promotion hold diagnosis::unsupported local fallback retains ordinary hard stop and canonical finish; ::cancelled resident work retains exact terminal settlement ownership; deploy/cloudflare-memory/runLedger.test.ts::original workspace durability in real SQLite::cancelled SQLite resident ownership remains readable and can be acknowledged without a VM; src/channels/commandHttp.test.ts::createCommandHttpHandler — hard stop after restart::* |
Hard cancellation of a deferred review checkpoint preserves both possible outcomes of a lost state-write reply, retains the original ticket and deferred event, and requires an independent runtime acknowledgment before sealing stopped_hard. It retains a verdict already saved by a committed checkpoint without creating a new verdict or publication receipt, and rejects late original writes after the seal. | [unit] src/core/runsService.test.ts::RunsService.stopRun::hard stop preserves deferred review checkpoint outcomes without posting: committed=%s |
| Large authored Unicode text and PDF data use bounded same-owner write chunks and project one exact legacy logical part; missing, changed, reordered, duplicate or malformed internal content remains a typed required hold | [unit] src/core/runLedger/transcript.test.ts::internal bounded Memory transcript::*; deploy/cloudflare-memory/runLedger.test.ts::original ingress SQLite owner::stores bounded Unicode text and PDF chunks on the original owner and returns an exact legacy transcript | | Oversized canonical compaction holds before any upload; an ordinary summary keeps the original compaction DTO | [unit] src/core/runLedgerWorker.test.ts::WorkerRunLedger::holds oversized canonical compaction before uploading any content | | Unknown chunk acknowledgement keeps its exact write witness and publishes no logical row; an older Memory codec is refused before content upload | [unit] src/core/runLedgerWorker.test.ts::WorkerRunLedger::keeps a lost chunk acknowledgement uncertain and never publishes its logical row | | Required transcript failures cannot become channel or alternate-agent history, while ordinary legacy missing-log behavior remains unchanged | [unit] src/core/dispatch/seed.test.ts::sessionSeedFor — the seed read from the ledger, with the notepad::holds unreadable required transcript bytes instead of falling back to channel history; src/core/dispatch/operator.test.ts::operatorThreadTail carries each turn's actor off the assembled transcript::holds a required transcript failure instead of falling back to another agent log; src/core/dispatch/operatorTail.test.ts::operator source-aware tail::holds required transcript failure before substituting source-aware empty history | | A checkpoint write ACK must match its original run/generation/key/before-hash/revision/inputs. An optional closed local session boundary is checked without changing wire bytes; InMemory rejects a mismatch before recovery effects. | [unit] src/core/runLedgerWorker.test.ts::checkpoint acknowledgment original binding::*; src/core/runLedger/inMemory.test.ts::local original checkpoint boundary before effects::* | | A HERE hosted hard stop requires confirmed pipeline admission-stop, including an exact committed readback after a lost reply, and preserves the human stop intent but closes a tracked canonical run only through its own writer's reconciliation, sink and known terminal ledger ACK. Missing, ambiguous, foreign, handed-off, unstarted, off or fallback evidence does not grant closure. Explicit registry-only and remote stop paths retain their existing behavior. | [unit] src/core/runsService.test.ts::RunsService.stopRun — a hosted parent: soft refused, hard seals (record 0060)::* |
| Criterion | Proof |
|---|---|
| Malformed possibly-committed live-state and context-checkpoint write replies retain the exact original wire request as an uncertain effect. Non-JSON replies remain uncertain; known canonical refusal codes keep their precise negative result. No ambiguous reply authorizes success or mutation replay. | [unit] src/core/runLedgerWorker.test.ts::status and checkpoint mutation acknowledgement custody::* |
| Criterion | Proof |
|---|---|
| The first original harness facts may carry a closed launch policy. Canonical policy survives omitted harness/policy updates and legitimate lifecycle changes; malformed or changed policy refuses. A legacy original without the marker remains unknown and generic state writes cannot backfill it. One policy-only helper shapes caller snapshots and both owning receivers without granting unit-seed or checkpoint authority. | [unit] src/core/runLedger/checkpointState.test.ts::original session policy custody::*; ::original policy owning state transaction::*; ::matches omitted-policy caller bytes to actual InMemory readback after accepted reply loss; deploy/cloudflare-memory/runLedger.test.ts::original policy owning SQLite state transaction::*; ::matches omitted-policy caller bytes to actual SQLite readback after accepted HTTP reply loss |
| Criterion | Proof |
|---|---|
| An explicit versioned original workspace allocation is validated against canonical admission identity and retained through promotion, reclaim, finish and abandon. Missing legacy authority cannot be retrofitted; caller mutation, generic state/record writes and replacement metadata cannot create disposal authority. | [unit] src/core/runLedger/workspaceDurability.test.ts::original workspace allocation::* |
| New original cold allocation data uses a closed version 2 contract binding exact original run/requester/thread/repository/ref/initial head and positive PR number to the recorded registered purpose, retained-resident placement and exclusive-cold custody clause. Names, read-only labels and raw flags cannot construct it; missing, contradictory or mismatched data refuses construction. Legacy version 1 remains readable and is not recognized as a newly declared contract; current policy cannot upgrade an old first admission. Constructor data supplies no ACK, backend proof, closure or physical permission. | [unit] src/core/runLedger/workspaceDurability.test.ts::declared original cold allocation::*; [gap] The supported allocation-closing consumer is not wired. |
| A fresh registered PR review with an existing trusted exact publication target puts its entire declared allocation on the first reserve after existing admission gates and accepts only a matching canonical version 2 ACK before exposing a handle or heartbeat. Accepted claims with unavailable ACKs remain unknown without retry or deletion; proven ownership conflicts and disabled tracking remain distinct refusals. Unrelated, no-PR, no-workspace and recovery paths gain no authority. A resident binding remains retained. | [unit] src/core/dispatch/provision.test.ts::original allocation before first reservation::* |
| Private promotion preparation stores the one complete original claim wire body and SHA in the existing allocation envelope, without claiming, seeding, upgrading canonical-null or granting model/disposal permission. Closed receiver receipts bind actual original run/actor/namespace/gen/start; lost prepare replies remain independently readable. Pending or corrupt witness bytes block mutation, handoff, finish, abandon and reclaim; older unsupported receivers stay held before effect. The existing 512KiB wire and 1.5MiB private budgets count full UTF-8/escape overhead without truncation. | [unit] src/core/runLedger/promotion.test.ts::original promotion private prepare::*; deploy/cloudflare-memory/runLedger.test.ts::original workspace durability in real SQLite::prepares exact original promotion bytes in SQLite and holds actual lifecycle mutations without claiming or upgrading null; ::reads a real committed prepare after lost HTTP acknowledgment through a new client without repeating prepare; [unit] src/core/runLedger/writeThrough.test.ts::original reservation promotion custody::*; src/core/runLedger/seedVerification.test.ts::receiver confirmation and authenticated source release::*. |
| Prepared promotion commits only the byte-identical original claim body through the existing claim transaction, retaining its canonical allocation/null, immutable preparation and receiver-produced unconfirmed receipt. Exact duplicates read that committed receipt without repeating effects; owner, target, body, generation and private revision conflicts hold. Lost commit replies remain independently readable after restart. A committed receipt permits neither model execution nor cleanup; unconfirmed finish, abandon and reclaim stay held. | [unit] src/core/runLedger/promotion.test.ts::original promotion receiver commit::*; [unit] src/core/runLedger/writeThrough.test.ts::original reservation promotion custody::* |
| The pure expected-seed builder authenticates raw reused rows independently of the supported deterministic session projection (orphan results removed, thinking omitted, typed policy-refusal indices replaced), binds both raw-source and resolved-model hashes, omits a reused witness for zero reused turns, and captures actual resolved messages, canonical rows, every attachment and actor, context, notes, system, initial budget, session range and verified reused input before effects. A closed ASCII/base64 private header seals that compact manifest beside the full original claim body and binds its SHA in prepare/commit receipts. The full 512KiB original-body and 1.5MiB UTF-8/escape-inclusive private fences remain; duplicate, malformed, foreign and inconsistent data holds without mutation. Absent legacy evidence remains unconfirmed and cannot be upgraded. | [unit] src/core/runLedger/seedManifest.test.ts::immutable expected seed before effects::*; ::expected seed complete content and private envelope::*; deploy/cloudflare-memory/runLedger.test.ts::original workspace durability in real SQLite::stores expected seed with full512KiB original body and reads actual lost prepare reply without reissuing; ::refuses invalid expected seed header before actualSQLite mutation: %s; [gap] No-session positive protocol remains held pending equivalent source proof. |
| Source verification authenticates references through the actual canonical committed Runs preparation and exact session identity, hashes only actual untrimmed source rows/attachments/actors/context/notes/range, rereads after awaits, and atomically stores an immutable receipt plus persistent guarded holder. Pending source mutations, owner replacement, append, checkpoint, drop and even valid newer-revision pruning preserve the original or return the actual pending precondition. Source receipts do not prove Runs system, budget or step0 and grant no model permission. | [unit] src/core/runLedger/seedVerification.test.ts::source-own expected seed verification::*; ::pending source writer barrier controls::*; deploy/cloudflare-memory/runLedger.test.ts::original workspace durability in real SQLite::commits actual source verification receipt and pending holder through HTTP, refusing source mutations and newer-revision removal; [gap] No-session positive protocol remains held pending equivalent source proof. |
| Runs confirmation reads the actual guarded source receipt through its bound source store, hashes its own admitted system, profile budget and canonical step0 with no later step or in-flight calls, then rereads live owner and immutable prepare/commit plus local seed facts after awaits in its owning transaction. Source release reads that actual immutable confirmation through its bound Runs store and rereads its own owner/data/holder before releasing only the temporary write hold. Immutable source/confirmation/release receipts and guarded custody pins remain; duplicate and lost replies reconcile by exact read, never mutation replay. Missing, foreign, gapped, trimmed, unsupported legacy and no-session inputs remain held. | [unit] src/core/runLedger/seedVerification.test.ts::receiver confirmation and authenticated source release::*; deploy/cloudflare-memory/runLedger.test.ts::original workspace durability in real SQLite::real HTTP confirmation and release retain exact immutable receipts across lost replies and new clients; ::Runs owning SQLite transaction rereads its actual seed boundary after source acknowledgment and hashing; ::source owning release transaction rereads its owner and data after canonical Runs confirmation; [gap] No-session positive protocol remains held pending equivalent source proof. |
| Confirmation compares the original semantic row, immutable preparation/archive and complete canonical seed steps after source reads and hashing. An authenticated same-owner heartbeat may renew only its liveness lease without invalidating this comparison or renewing its admitted budget. Owner, generation, stop, state, metadata, system, archive, seed and step changes still hold confirmation; guarded source remains unreleased. | [unit] src/core/runLedger/seedVerification.test.ts::receiver confirmation and authenticated source release::confirms the original seed across an authenticated heartbeat without renewing its budget or changing custody; ::keeps concurrent*; deploy/cloudflare-memory/runLedger.test.ts::original workspace durability in real SQLite::confirms actual SQLite seed custody while the original owner heartbeat renews only liveness; ::keeps actual concurrent SQLite* |
| The original producer seals one actual body and expected input before effects, prepares and claims once, persists actual seed/step0, and starts execution only after canonical source verification, Runs confirmation and authenticated release/readback. Unknown replies reconcile by original references without blind mutation replay; final unknown retains row/session/request/workspace across cleanup and restart; hard stop may settle only through the separate fenced cancellation outcome. Distinct logical originals, including same-ID supported restarts, keep separate immutable body/gen fingerprints and evidence in existing carriers; historical release proves the predecessor only. First allocation, actor and start remain unchanged. | [unit] src/core/runLedger/writeThrough.test.ts::original reservation promotion custody::*; src/core/runLedger/seedVerification.test.ts::receiver confirmation and authenticated source release::*; src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::starts no provider and preserves original custody when final promotion is %s; deploy/cloudflare-memory/runLedger.test.ts::original workspace durability in real SQLite::actual producer same-session %s successor has separate immutable receipt and independent source/model readiness; [gap] Physical retirement remains outside source confirmation. |
| A state-write promotion hold discovered after HTTP preflight crosses Durable Object RPC as the existing closed held response before any write. Ordinary identity-none writes still acknowledge; an unexpected error after a committed write stays unknown even if its name resembles a promotion exception. No owner, source-release, input or deadline guard is relaxed. | [unit] deploy/cloudflare-memory/runLedger.test.ts::state-write holds across Durable Object RPC::* |
| Original promotion holds report the bounded failed phase, read or write, and typed reason without printing private causes or original request bytes. Exact confirmation readback resolves lost or malformed replies once; source uncertainty, foreign source, known refusal and unreadable confirmation stay held without replay, release or abandon. Diagnosis never grants recovery or changes the original deadline or ownership. | [unit] src/core/runLedger/originalPromotionHold.test.ts::original promotion hold diagnosis::* |
| Promotion preserves the original admitted start, actor, target and ownership rather than inventing a clock identity. The original capability and any snapshot must agree; an absent snapshot can use a validated receiver ACK or a current-generation carried row. Canonical-null ACKs remain allocation-free and require the original request's actor binding. They preserve supported verified current-head observation, including a re-reserved attaching standalone review with an actual canonical-null ACK and a matching verified attached checkout; unknown ACKs and allocated heads remain fenced; an actual allocated head remains immutable. Unknown acknowledged promotion retains original custody after one claim, before seeding or model admission; known fences remain distinct and no-ACK legacy retries remain supported. Recovery never upgrades legacy or null into a new contract. | [unit] src/core/dispatch/run.test.ts::original admission identity at promotion::*; src/core/runLedger/writeThrough.test.ts::original reservation promotion custody::*; deploy/cloudflare-memory/runLedger.test.ts::original workspace durability in real SQLite::promotes actual %s originals through HTTP at their first start and preserves unknown promotion ACKs; src/core/dispatcher.test.ts::closed-card checklist and review verdict run link::appends the run link to the review verdict reply — and ONLY to the projection: the answer event and the PR post stay link-free |
| A successful claim returns a receiver-produced versioned original allocation ACK from the private archive in the owning claim transaction, bound to actual run/thread/generation/start. Keep, refresh and promotion preserve the original contract or null marker. Clients validate and clone the complete ACK; older, malformed or foreign replies carry no acknowledged allocation. An unknown allocation-bearing remote claim throws before reserve exposes a handle, with no blind retry; ordinary legacy claim behavior remains. The ACK is not prototype-v1 disposal eligibility or a replacement for original purpose/consumer proof. | [unit] src/core/runLedger/allocationAck.test.ts::original allocation claim acknowledgment::*; src/core/runLedger/workspaceDurability.test.ts::original workspace allocation::acknowledges the canonical original allocation across keep refresh and promotion without exposing caller aliases; deploy/cloudflare-memory/runLedger.test.ts::original workspace durability in real SQLite::returns the original allocation ACK from the successful private claim transaction and canonical null stays null; ::preserves a committed allocation after a lost HTTP ACK and refuses reserve without a retry or handle |
| An unknown native claim without an allocation keeps the original run ID, request and actual transport cause in the writer. It throws before a handle or heartbeat and cannot become untracked execution or a repeated claim. Setup retains an explicit unknown outcome and held thread. Coordinator spawn keeps its original effect uncertain. Follow-ups receive a hold and unsaved response at every verbosity, including messages queued before the hold. Setup does not attach a workspace, start a model, close the original run or start queued work. Known missing routes and legacy fallback remain distinct. | [unit] src/core/runLedger/writeThrough.test.ts::unknown native initial claim::*; src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::holds an unknown native claim through setup and follow-up*; src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::coordinator producer identity::keeps an unknown initial claim uncertain at the actual spawn endpoint* |
Persisted unknown native claim observations survive process replacement through the existing ledger seam without creating a live or terminal run. runs claims lists only observations visible under the caller's existing run-read predicate and is not exposed in chat. Every page says recorded-only coverage: a missing row proves neither absence of an unknown claim nor a current hold or commit result. The original run, generation and namespace remain fixed; only closed original transport facts are retained. Diagnostic failure preserves the original unknown error and never retries its claim, releases it or settles custody. | [unit] src/core/runLedger/claimObservation.test.ts::unknown native claim observations::*; src/core/commands/runs.test.ts::runs claim observations::*; src/core/commandChat.test.ts::runs list on chat::keeps private claim observation metadata off a shared chat audience; deploy/cloudflare-memory/runLedger.test.ts::durable unknown native claim observations::* |
Failed observation reads keep a fixed unavailable response and log only closed transport facts. Ordinary and direct queries retain unreadable rows for safe validation, including malformed JSON and missing or null thread keys, while excluding known workers before paging. They log a bounded validated key or invalid-key, without stored JSON or exception text, and leave the row unchanged. | [unit] src/core/commands/runs.test.ts::runs claim observations::logs only closed read failure facts…; deploy/cloudflare-memory/runLedger.test.ts::claim observation authorization before SQL paging::identifies an unreadable observation with a bounded key… |
| Scratch custody requires two independent receipts: the complete protected owned loop range and the exact immutable original run/thread report row, each rechecked by range, hash and receiver-owned guarded pin revision alongside the answer event and terminal record. Normalized reports and original parent/maintenance identity do not imply pending work. Actual jobs, unread inbox, paused work, unknown tools/publication and every unresolved run-bound external effect remain held; a capped delivery response cannot prove their absence. Completed nonzero or not-started outcomes require matching original typed execution receipts, never display exit/status. | [unit] src/core/runLedger/workspaceDurability.test.ts::canonical scratch custody disposition::*; deploy/cloudflare-memory/runLedger.test.ts::original workspace durability in real SQLite::*; [gap] Original Review Bash typed outcome production, cold Worker propagation and physical retirement are not activated by this store-only change. |
| Allocation-only private obligation envelopes neither alter resident settlement ownership nor hide malformed resident evidence. Async custody inputs are rechecked inside finish, including recorded execution mutations during the session read. | [unit] deploy/cloudflare-memory/runLedger.test.ts::original workspace durability in real SQLite::allocation-only envelopes preserve unrelated ownership and cannot hide malformed resident rows; ::refuses mutation of canonical execution custody during the awaited session boundary |
| Criterion | Evidence |
|---|---|
| A setup held by an actual unknown terminal commitment preserves that original private request and digest through reservation, promotion and dispatcher finalization. It performs no model turn, terminal sink, abandon or workspace release; the typed original error stays internal and no synthetic witness or completed result replaces it. Coordinator and valid child-handoff reservations propagate the held error before ordinary non-tracked refusal. | [unit] src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::keeps the actual unknown terminal witness through setup finalization*; src/core/dispatch/provision.test.ts::reserveRun — the ledger reservation before the attach::preserves actual held error* |
A hosted parent's completed registry and unit projections never settle its unknown canonical #host terminal commitment. A later Ship request preserves the actual original held error through both inner and outer finalizers, including a claim already awaiting the finish; it starts no replacement hand-off, Workflow, unit or model, writes no terminal record, and performs no abandon, release or completion. Directly thrown typed errors retain the same private request and digest. | [unit] src/core/dispatcher.test.ts::agent:ship (the hand-off to the plan runner)::keeps the actual hosted terminal witness beyond both Ship finalizers* |
| 63: pre-model setup failure closes the advertised run, direct review capacity carries typed failure, and an untracked reservation keeps its note but omits the unproven link | [unit] src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::a setup failure finishes the advertised run before a queued follow-up claims the thread, ::keeps a direct PR review's advertised run when fleet capacity prevents setup, ::keeps an untracked reserve reason on the advertised run while omitting its unproven link |
| 62: archives remain bound to their actual run owner and malformed dependency envelopes are invalid | [unit] src/core/runRecord.test.ts::retained source archives remain bound to the original run owner, src/core/runRecord.test.ts::retained context dependencies validate their canonical shape and local origin binding |
| 62: assembly and reclaim retain original source state, unknown outcomes and child context | [unit] src/core/dispatch/record.test.ts::durable source archive assembly::* |
| 62: internal full reads retain archives; listing and summary reads omit original bodies | [unit] src/core/runStore.test.ts::InMemoryRunStore — RunStore contract::archives source bodies only in full records and preserves the dependency references in summaries, src/core/runStore.test.ts::FileRunStore — RunStore contract::archives source bodies only in full records and preserves the dependency references in summaries |
| 62: local stores apply the canonical validator before saving an archive and never persist a source-free fallback for invalid evidence | [unit] src/core/runStore.test.ts::InMemoryRunStore — RunStore contract::refuses malformed source archives without storing a source-free fallback, src/core/runStore.test.ts::FileRunStore — RunStore contract::refuses malformed source archives without storing a source-free fallback |
| 62: public summary, full-event and final-record projections strip all internal context fields | [unit] src/core/runsService.test.ts::RunsService.getRun::never exposes internal source archives or handoff context through any persisted projection |
| 62: finish archives source state before deleting the live row, merges earlier session dependencies into a later run, and captures a final revoked session even when the run state was last flushed as known | [unit] src/core/runLedger/writeThrough.test.ts::open — claim and seed::archives original source state and dependencies before deleting the live row, ::merges retained session dependencies before a later run writes its own context, ::archives the final session dependency status even when a row write tainted it after the last state flush |
| 62: inherited context is persisted before child seed bytes; only acknowledged rows form a handoff checkpoint | [unit] src/core/runLedger/writeThrough.test.ts::open — claim and seed::persists inherited context before writing the child seed, ::checkpoints only acknowledged source rows under the current owner |
2: an identity rewrite after the model's push publishes the rewritten ref tip as the last pushed_head; the completed coding record's headSha and folded pushed name that same final remote tip | [unit] src/core/dispatch/runLoop.test.ts::a resume with the answer in hand (the \finish` plan)::records the identity rewrite's final pushed head for the coordinator's exact-head read` |
2, 19: the standing fold's three carriers (record 0065; agent-ship.md item 19) — one event list yields one pipeline on the registry summary at publish and on the re-host replay, on the ledger view of a row live under another generation, and on the assembled record (hosted beside it), the stored row projecting both onto the persisted view; a record without the fields projects neither; and the seal carries the registry's whole-list standing even when the backlog trimmed ship events out of the snapshot — the runner's finish and the hard stop's seal alike | [unit] src/core/runsService.test.ts::the registry summary, the ledger view and the record carry one standing::*, src/core/runsService.test.ts::RunsService.stopRun — a hosted parent: soft refused, hard seals (record 0060)::a hard stop's sealed record carries the registry's whole-list standing even when the backlog trimmed ship events out of the snapshot, src/channels/adminCoordinator.test.ts::the plan runner's steps — plan, unit-start, branch, round, unit-end, finish (item 9)::finish seals the record with the registry's whole-list standing even when the backlog trimmed ship events out of the snapshot |
36: a handoff row with state.hosting, a future deadline and the ship branch's provisional interrupted store record is rehost with the row (ours now), the hosting fact and the events — nothing closed, no record written; one whose store record is completed or failed is abandoned (the live row gone, no record) and a store that cannot be asked is one warning and a rehost; one past its deadline rehosts while its Workflow, a foreign/resumable child, or a child whose failed classification left its row live remains live. An unknown or unreadable Workflow status keeps the owner hosted for reconciliation; only confirmed absence or terminal state permits the deadline to close it when no child remains. A finishing or non-resumable child reclaimed beside it closes first and does not keep the parent alive; a host-keyed row with no state.hosting falls to the transcript rule; two generations reclaiming the same sweep leave the loser listing the row live elsewhere under the winner's generation | [unit] src/core/boot.test.ts::reclaimRuns — the hosted parent's classification (record 0060)::* |
36: a rehost row puts nothing in the elsewhere map, and a foreign hosted row's entry is keyed …#host, which no message's thread can match | [unit] src/core/boot.test.ts::reclaimRuns — the hosted parent's classification (record 0060)::a rehost row puts nothing in the elsewhere map… |
38: the launcher's rehost branch recreates the registry row under the run's id — the original start, the label, a fresh live token, the events replayed under their seqs — adopts the ledger row with the row's state and the highest replayed seq, mirrors only this generation's publishes, dispatches nothing and fires onDone at once; the recreated identity is the metadata's thread, never the ledger's #host key | [unit] src/core/resumeLaunch.test.ts::launchResumes — the rehost branch (record 0060)::* |
| 38: rehosting a row that has a card claims it in the adapter's live-card set, so the orphan sweep leaves the still-running pipeline's card untouched while an unowned live card in the same thread is still swept | [unit] src/core/resumeLaunch.test.ts::launchResumes — the rehost branch (record 0060)::rehosting a row that has a card claims it in the live-card set, so the orphan sweep leaves it untouched |
Item 55: every reference event folds into references in publish order, the record validates with and without the field, and a run that quoted nothing has no key | [unit] src/core/dispatch/record.test.ts::assembleRunRecord — the pull request on the record (docs/reference/specs/run-history.md item 2)::folds every reference event into \references` in publish order, and the record still validates; without one, no key` |
46: isRunRecord accepts a parentRunId of the run-id shape — also after a JSON round-trip — a record without one carrying no key, and refuses a non-string, an empty string and a path; parent is accepted as a profile's clip scope | [unit] src/core/runRecord.test.ts::isRunRecord — the parentRunId field::*, src/core/runRecord.test.ts::isRunRecord — the profile field::accepts \parent` as a clip scope…` |
46: the registry projects RunMeta.parentRunId onto the summary and the index feed, and omits it when absent | [unit] src/core/runRegistry/projections.test.ts::RunRegistry.snapshot — record inputs::carries parentRunId from the RunMeta… |
46: RunsService views carry it live and persisted, and a listing shows the parent of every child | [unit] src/core/runsService.test.ts::RunsService.getRun::carries parentRunId from a live child's RunMeta… |
2: a direct decision-record task's re-issue pages through retained coding records until its recordTaskKey is found beyond the newest 200 rows, then reuses that record | [unit] src/core/dispatcher.test.ts::coding PR post-step (docs/reference/specs/pr-description.md)::a re-issued decision-record task finds its reservation beyond the newest 200 coding records |
2: the final headSha is the independently observed workspace head unless an identity rewrite changes the published branch tip; then it records that rewritten remote tip while the workspace remains on the model's commit. It validates and is absent without a readable Git head; production read-record returns it beside the handoff and pushed heads, so a completed same-head salvage opens the pull request and enters review while a differing returned head remains a checkpoint | [unit] src/core/dispatch/record.test.ts::assembleRunRecord — the handoff on the record::carries the independently observed final workspace head, validates it, and omits it when the workspace had no head; src/core/dispatch/runLoop.test.ts::a resume with the answer in hand (the \finish` plan)::records the identity rewrite's final pushed head for the coordinator's exact-head read; src/core/dispatch/runLoop.test.ts::runLoop — the model turn and everything that rides on it::a coding child's final description turn checkpoints dirty work before release; src/channels/adminCoordinator.test.ts::POST /admin/coordinator/read-record — the renewal's facts off the record (decision 0046)::a completed child's production read-record view carries its observed final head, so same-head salvage opens the pull request and enters review, ::a completed child whose salvage differs from the head returned by production read-record still aborts at the checkpoint` |
2: the pushed heads — one per branch, the last sha winning — fold onto the record off pushed_head events and validate, none → no key; the event parses with a ref, a sha and who pushed it, and the run timeline and the card carry it | [unit] src/core/runRecord.test.ts::*::pushedHeadsOf lists the run's pushed heads…, src/core/dispatch/record.test.ts::*::carries the run's pushed heads off its pushed_head events…, src/core/runEventLines.test.ts::parseRunEventLines::accepts \pushed_head`…, src/channels/runTimeline.test.ts::createRunTimeline — grouping::passes input / answer / run_note through…, src/core/dispatch/reply.test.ts:😗::every other event is its activityLine as a line part` |
2: the compaction checkpoint's push is a fact of the run — a pushed_head event (by: "salvage") on the record beside its compaction_salvage note, so a renewal reads the checkpointed head off the row | [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… |
2: a run a question's Yes redispatched carries the redispatch note — confirmed after question <code>, the stored row's refusal code — published before the first turn | [unit] src/core/dispatcher.test.ts::the question's Yes: the guess is stored and one click redispatches it (record 0054)::Yes consumes the row and hands the proposal to dispatch() as the requester… |
2: every refusal is a run record (record 0054, as amended) — a gate refusal leaves one door record, completed, its refusal event carrying the code, the cause and the reply's sentence, with no thread claim, no run signalled and the reply unchanged; the catch-all's uncaught records with cause system; a refused click leaves exactly one record | [unit] src/core/dispatcher.test.ts::every refusal is a run record (record 0054, as amended)::*, src/core/dispatcher.test.ts::the question's Yes: the guess is stored and one click redispatches it (record 0054)::a stranger's Yes is refused with the requester line… |
2: recordRefusal writes the door record beside the rendered sentence — agent door, the code leading the label, input + refusal as its content events, the sentence redacted and capped at ROUTE_RECEIPT_CAP, neither runStarted nor runFinished, and a message without a thread of its own recorded under the channel's key | [unit] src/core/dispatch/commandRun.test.ts::recordRefusal — every refusal is a run record (record 0054, as amended)::* |
2: the one record assembly carries the lease off the first lease event and validates; no lease event → no key | [unit] src/core/dispatch/record.test.ts::*::carries the run's lease off its lease event… |
| 46: the one record assembly carries it and validates, no parent → no key; the drain deadline's interrupted record carries the parent the registry row names | [unit] src/core/dispatch/record.test.ts::assembleRunRecord — the handoff on the record::carries parentRunId for a spawned child…, ::the drain deadline's interrupted record carries the parent… |
| 46: end to end — a spawned child's finish record and live summary carry the parent's id, and the parent's record carries no key | [unit] src/core/dispatcher.test.ts::agent:conductor — a run that spawns child runs through dispatch()::spawns a research child as the requester in a thread of its own… |
46: a run a person's reply starts in a spawned thread carries the same parent's id on its record and live summary, with seed: channel and no parent-bounded profile | [unit] src/core/dispatcher.test.ts::agent:conductor — a run that spawns child runs through dispatch()::a person's reply in a spawned thread after the child ended… |
47: sendRunFinished sends exactly one run-finished-<runId> to the record's instance with { runId, status, finishedAt, parentInstanceId }; a record without parentInstanceId sends nothing; no binding is no-binding; an engine that refuses is swallowed and reported as failed with the reason, never thrown | [unit] src/core/coordinator/contract.test.ts::sendRunFinished — the event a terminal record sends::* |
47: the event type is in the platform's alphabet — run-finished-<runId> for a UUID run id matches ^[a-zA-Z0-9_][a-zA-Z0-9-_]*$, the pattern Workflows accepts, and the old run finished: shape does not | [unit] src/core/coordinator/contract.test.ts::the coordinator's names::the event type is run-finished-<runId>, the run id under a prefix in the platform's alphabet… |
47: on the object — the owner's finish, the reclaim's close of an expired live row and the admission's close of a superseded reservation each send exactly one event after the commit and answer event: sent; a record without the field answers none and a fenced finish sends nothing; a refused send leaves the commit standing (the row gone, the record listed) and answers failed; a Worker without the binding commits as before and answers no-binding | [unit] deploy/cloudflare-memory/runLedger.test.ts::run ledger — the coordinator's event and the key (items 47–48)::a record carrying parentInstanceId committed by the owner's finish sends exactly one…, ::the reclaim's close (an expired live row taken by the next generation) and the admission's close (a reserved row it supersedes) each send exactly one event, ::a record without parentInstanceId sends nothing; a fenced finish sends nothing, ::a send the engine refuses — the instance ended — is swallowed…, ::a Worker without the coordinator binding (the release before it bound the bot's class) commits as before and answers no-binding |
33, 36 (record 0064): the owner's finish records ended { kind, cause } exactly when the row closes — completed to completed, an interrupted record with restarting to resident_replaced, a bare interrupted to lease_lapsed — and the first cause stands, except that a same-id successor's finish replaces a standing resident_replaced (a restarting close is the run continuing, not its end); /plane/reclaimed records lease_lapsed for a closed row and nothing for resume, restart or rehost; the alarm at a lease end offers the row — nothing closed — and the owner's heartbeat re-arms it | [unit] deploy/cloudflare-memory/runLedger.test.ts::the plane's endings and the alarm — the cause on close, /plane/reclaimed, the lease-end offer (record 0064)::* |
| 36: the reclaim reports every taken row's word and the interrupted note renders the recorded cause word; an older Worker without the route is one warning and today's words | [unit] src/core/boot.test.ts::reclaimRuns — the reclaim's outcome reported to the plane::* |
47 (record 0064): a claim whose meta names restartOf under a coordinator sends the parent one child-resumed-<runId>; a claim without it sends nothing; a Worker without the binding claims as before | [unit] deploy/cloudflare-memory/runLedger.test.ts::the plane's endings and the alarm — the cause on close, /plane/reclaimed, the lease-end offer (record 0064)::a claim whose meta carries restartOf under a coordinator sends the parent one child-resumed-<runId>… |
47a (record 0064): a close a restart follows carries restarting: true on its record and one no restart follows does not; read-record on a restarting record with no successor row yet answers the child as still running, so the runner keeps waiting for child_resumed | [unit] src/core/dispatch/reattach.test.ts::the restarting ending::*, src/channels/adminCoordinator.test.ts::POST /admin/coordinator/read-record — an interrupted child (issues 1903/1876)::an interrupted record carrying \restarting: true` with no successor row yet answers the child as still running…` |
33, 36 (record 0064): causeOfClose, causeOfReclaim and endingCauseWords — the closed set, the reclaim's one recording word and the one rendering every surface shares | [unit] src/core/plane/decide.test.ts::endings and their causes::* |
48: isRunRecord accepts parentInstanceId in the platform's alphabet and idempotencyKey as <instance>:<step> together — also after a JSON round-trip — and refuses a colon or over-long id, a key without its step, a non-string, and either field alone; a record without them carries neither key | [unit] src/core/runRecord.test.ts::isRunRecord — the coordinator fields (parentInstanceId, idempotencyKey)::*, src/core/coordinator/contract.test.ts::the coordinator's names::an instance id is the platform's alphabet… |
48: the claim stores the tag on the row and a second claim on the thread is refused naming the live run's key — decideClaim, the in-memory ledger and the object agree; a malformed key, instance id or admitted cap in the meta, or either identity field alone, is 400 | [unit] src/core/runLedger/decisions.test.ts::decideClaim — one live run per thread::a refused claim carries the live run's idempotency key…, src/core/runLedger/inMemory.test.ts::InMemoryRunLedger::a claim whose meta carries a coordinator's key stores it on the row…, deploy/cloudflare-memory/runLedger.test.ts::run ledger — the coordinator's event and the key (items 47–48)::the claim stores the key on the row and a second claim on the thread is refused naming it… |
48: the registry projects the identity and admitted cap onto the summary and the index feed; RunsService views carry it live here, live on another generation's ledger row and persisted, and omit both keys without one | [unit] src/core/runRegistry/projections.test.ts::RunRegistry.snapshot — record inputs::carries coordinator identity and cap from RunMeta onto the summary and index feed…, src/core/runsService.test.ts::RunsService.getRun::carries parentInstanceId and idempotencyKey from a live child's RunMeta, a ledger row's meta and a persisted record… |
| 48: the one record assembly carries the tag and validates, none → no key; the drain deadline's interrupted record carries the identity and cap the registry summary names; the reclaim's close carries the identity and cap the ledger row's meta names | [unit] src/core/dispatch/record.test.ts::assembleRunRecord — the handoff on the record::carries the coordinator tag (parentInstanceId, idempotencyKey) for a coordinator's child…, ::the drain deadline's interrupted record carries the coordinator tag…, ::the reclaim's close carries the coordinator tag the ledger row's meta names… |
48: end to end — a coordinator's child carries the tag on the reserved row, the claimed row, the live summary and the finish record, and a second coordinator dispatch onto the live thread is refused coordinator_thread_live with nothing said | [unit] src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::a coordinator's child (DispatchOptions.coordinator) carries parentInstanceId and idempotencyKey on the reserved row… |
48: coordinatorFields spreads the two identity fields and optional admitted cost cap into the child record, and leaves the tag's base out | [unit] src/core/coordinator/contract.test.ts::the coordinator's names::carries an admitted unit cost cap into its child record, ::the tag's base is an instruction to the child's post-step, not a record field… |
48a: a fresh coordinator child's admission persists one coordinator_tag event before registration acknowledgement with the instance, unit, base and findings child's exact issued IDs, mirrored onto the row; a resume restores the issued IDs and republishes nothing; unitOfIdempotencyKey answers nothing for a malformed key | [unit] src/core/dispatch/provision.test.ts::registerRun — the run's row on every surface before the attach::publishes the coordinator admission tag before attachment and never duplicates it on restart, src/core/dispatch/reattach.test.ts::carriedCoordinatorTag: the tag a resumed run carries forward::restored finding IDs still reject a near-match…, src/core/coordinator/contract.test.ts::a unit's key — the instance and the unit, the prefix of every child's idempotency key::unitOfIdempotencyKey reads the unit off a child's key… |
| 48a: a resume rebuilds the tag from the row — meta fields plus the event's base; a pre-event row yields the tag with no base and restores the admitted cost cap from row meta; a run no coordinator spawned yields none | [unit] src/core/dispatch/reattach.test.ts::carriedCoordinatorTag: the tag a resumed run carries forward::* |
48a: a resumed coordinator child without a base reads instance.base from the coordinator store by parentInstanceId and its PR opens against it, never against the unit branch; the store-recovered base reaches the gate's push rules before the session opens — the unit branch is the one push target and the recovered base is protected; base survived nowhere → no PR, pr_not_opened saying the plan's base was lost across a roll, the compare URL in the reply, no default-branch fallback; without a description, silence | [unit] src/core/dispatch/runLoop.test.ts::a resume with the answer in hand …::keeps one successful unit PR and its base when private chat refusal is…, ::a coordinator child whose plan base survived nowhere…, src/core/dispatch/runLoop.test.ts::the pi harness — every preset's runs, in the run's container::the gate's push rules follow the thread…, src/core/codingPrPostStep.test.ts::runCodingPrPostStep (callable with explicit inputs)::planBaseLost with a description and a pushed branch…, ::planBaseLost without a description… |
49: the store contract holds on both implementations — put stores and get reads back, an identical put is idempotent, a different record under a taken id is exists, an unknown id is null; an unreconciled instance is confirmed created only by exact-record CAS, leaving its unit rows untouched and refusing a replaced owner; the Worker client posts the store key and the record with the bearer and throws on an answer it cannot read; the null store knows no instance and refuses a put as unavailable; the builder follows the run ledger's rule | [unit] src/core/coordinator/instanceStore.test.ts::*, src/core/coordinator/contract.test.ts::isCoordinatorInstance — the parent ship record::* |
49: on the object — put and get round-trip the record, an identical put is idempotent, a different record under a taken id is 409 exists, an unknown id is null; the admission confirm route atomically marks only the exact unreconciled record and preserves its units; a malformed record or id is 400 and no bearer is 401 | [unit] deploy/cloudflare-memory/runLedger.test.ts::run ledger — the coordinator instance record (item 49)::* |
| 49: the record's own surfaces — the plan, the caps, the card, the run id and the label are accepted when shaped, also after a JSON round-trip, and a malformed one is refused | [unit] src/core/coordinator/contract.test.ts::isCoordinatorInstance — the parent ship record::accepts the plan, the caps, the card, the run id and the label when present… |
| Pull-owner lookup and admission use versioned, validated terminal candidate keys for the exact repository, PR and normalized branch, then read canonical candidates plus all live/unit/effect/workspace and audit dependencies in the owning transaction. Source inserts, updates and deletions invalidate the SQLite index atomically; the in-memory store compares current source facts. Bounded backfill makes idempotent derived progress without proving absence until all sources are covered. Missing/dirty/incomplete coverage, candidate content that differs from its validated footprint and unreadable global or legacy producer facts remain held. A derived-cache mismatch invalidates only that cache; a later bounded lookup must reread canonical source. Candidate rows retain the existing row/byte bounds; an index key grants no owner, transfer, source confirmation or execution authority. Canonical history, unknown writes, budgets and source holds remain unchanged. | [unit] src/core/coordinator/pullOwnerIndex.test.ts::bounded terminal pull-owner candidates::*; deploy/cloudflare-memory/runLedger.test.ts::bounded SQLite terminal pull-owner candidates::*; src/core/coordinator/pullOwnership.test.ts::complete canonical pull ownership::*; deploy/cloudflare-memory/runLedger.test.ts::unit effect owner transaction::* |
| 50: one bounded current effect lives on the existing unit; admission reads exact whole unit, caller Workflow/action, stop state and complete canonical target owners in the same owner transaction. Pending is persisted before a call; stop-first denies new calls, a begun call retains its accepted outcome after stop, and unknown/lost-response work cannot replay or retire. Exact branch-ref observations may resolve pending/unknown branch creation to its frozen commit without another POST. Missing, moved or foreign observations cannot resolve it. Merge observations match the complete frozen PR target and native commit; accepted queue effects cannot settle generically and retain ownership until exact terminal merge or known-entry absence. Unknown enqueue cannot resolve from absence. Operation-specific receipt shapes are enforced by the decoder, owner planner and HTTP matcher. Cancellation records only a never-started call; ordinary CAS and batches cannot introduce or erase effect authority. Admission and ordinary active-cell writes reserve maximum receipt bytes alongside the existing recovery settlement allowance | [unit] src/core/coordinator/unitEffect.test.ts::unit current effect::*, deploy/cloudflare-memory/runLedger.test.ts::unit effect owner transaction::*, src/core/coordinator/recoveryHistory.test.ts::recovery history store::effect admission retains recovery settlement space… |
50a: uncertain child dispatch resolves only from an exact durable run claim or terminal record and one retained coordinator tag, read with the existing unit transaction; requester, credential, unit, attempt, spawn key, thread, repository, branch and Workflow must agree. A finished review uses its first durable run_meta admission target rather than the final workspace head or opened-PR summary. Missing or contradictory private evidence retains the obligation; stop permits resolution of an admitted child but never dispatch replay | [unit] src/core/coordinator/unitEffect.test.ts::spawn effect durable receipt::*, [unit] deploy/cloudflare-memory/runLedger.test.ts::spawn effect durable receipt transaction::*, [unit] src/channels/adminCoordinator.test.ts::spawn durable effect admission::* |
50: a unit row's idle — why, at, renewalsLeft, wakes and the optional continuation facts accepted; a missing why, a negative count, a malformed spendUsd or handoff refused; unit-end on an idle ending writes it with wakes: 0 and no ending, the report still reaches the thread and the card line reads idle · <why>; a why past IDLE_WHY_MAX is the route's 400, the body's headSha lands as lastPush, and a later real ending drops the idle; the unit's readable facts carry {why, at, renewalsLeft, wakes} and a row without one carries none | [unit] src/core/coordinator/contract.test.ts::isCoordinatorUnit — one unit's row::the idle on a row (record 0051, run-history item 50)…, src/channels/adminCoordinator.test.ts::the plan runner's steps — plan, unit-start, branch, round, unit-end, finish (item 9)::unit-end with an idle ending writes idle {why, at, renewalsLeft, from, runId, spendUsd, handoff, wakes: 0} and no ending…, src/core/unitRuns.test.ts::unitFactsOf — the idle on the facts::* |
50: a merge-ready row retains its reviewed full head in lastPush. Continuation keeps the original row, task, branch, requester and instance; the existing recovery controller owns evidence-backed binding repair and its journal. Dispatcher callback repair and ownership transfer are removed. Exact unit CAS never overwrites a newer row; original-unit evidence, requester, publication and budget checks refuse incomplete or conflicting authority | [unit] src/channels/adminCoordinator.test.ts::the plan runner's steps — plan, unit-start, branch, round, unit-end, finish (item 9)::unit-end writes the ending and the pull request on the row…; src/core/coordinator/driver.test.ts::the plan runner's driver — the Workflow body over the step runner (item 9)::a review that requests changes is followed by findings and re-review at the moved head…; src/core/dispatcher.test.ts::a unit-owned thread (record 0051's reply-as-event and gone-instance rules)::plain continuation delegates to exact original-unit recovery without replacing its owner, ; src/core/coordinator/instanceStore.test.ts::InMemoryCoordinatorInstanceStore::claimLegacyContinuation replaces only the exact expected row, so one caller wins and stale state is never overwritten, ::WorkerCoordinatorInstanceStore (over a state Worker double)::claimLegacyContinuation replaces only the exact expected row, so one caller wins and stale state is never overwritten; deploy/cloudflare-memory/runLedger.test.ts::run ledger — the coordinator's unit rows (item 50)::claim-legacy-continuation atomically replaces only the exact expected row, so a concurrent or stale caller cannot erase newer state |
50: an original-unit recovery claim replaces one terminal ending and is mutually exclusive with idle and ending. It retains the stage, round, unchanged full head, exact remaining milliseconds and absolute deadline, claim time, original-unit step, exact authorizing review key/run and findings, byte-for-byte prior ending, and the separate Workflow id. Terminal settlement removes the claim by full-row CAS and writes a receipt that prevents the same evidence from being consumed twice | [unit] src/core/coordinator/contract.test.ts::isCoordinatorUnit — one unit's row::an original-unit recovery claim retains its exact lease, evidence, previous ending and Workflow id, mutually exclusive with idle or ending; src/channels/adminCoordinator.test.ts::POST /admin/coordinator/recover-unit — unchanged-head original-unit recovery::* |
50: indexed wake answers retain every segment continuation fact and reject malformed keys or answers; the wake route stores one answer with its event marks, replays it without another write, and no-event answers do not increment the idle count; reopening segment one leaves the renewal-only segments list unchanged and passes the production Worker's row validator | [unit] src/core/coordinator/contract.test.ts::isCoordinatorUnit — one unit's row::wake answers are keyed by the indexed wait…, src/channels/adminCoordinator.test.ts::the plan runner's steps — plan, unit-start, branch, round, unit-end, finish (item 9)::unit-wake*, deploy/cloudflare-memory/runLedger.test.ts::run ledger — the coordinator's unit rows (item 50)::the validating wake boundary accepts a first-segment resume… |
| 50: a unit row's segments — an index from two up with an optional sha and run id and a time accepted, a first-segment index, a missing time, a malformed sha or a non-array refused | [unit] src/core/coordinator/contract.test.ts::isCoordinatorUnit — one unit's row::segments are the renewals the unit spent… |
50: a unit row's shape — a full row, its round-trip and a bare one accepted; record accepts exactly four digits and rejects another shape; a review thread with its key and an optional link accepted, one without its key, with a malformed link or as a bare string refused; a bad instance id, a missing unit, slug or branch, a non-array dependency list, a malformed pull request, round, ending or issue refused | [unit] src/core/coordinator/contract.test.ts::isCoordinatorUnit — one unit's row::accepts a full row, its JSON round-trip and a bare one (the branch, the dependencies and no rounds) |
50: the store contract on both implementations — put writes the rows and list reads an instance's back in first-written order, a row is replaced whole and keeps its place, another instance's rows never appear; the null store refuses a unit read rather than certifying an empty list, and refuses a put as unavailable | [unit] src/core/coordinator/instanceStore.test.ts::*::putUnits writes the rows and listUnits reads an instance's back in first-written order…, ::NullCoordinatorInstanceStore and the builder::the null store refuses unit reads instead of reporting an empty durable owner |
| 50: on the object — put and list round-trip the rows in first-written order with a replace keeping its place, another instance's rows never listed, an unknown instance listing none; an empty list, a malformed row or instance id is 400 and no bearer is 401; the atomic decision-record reservation includes persisted unit and run rows and keeps a task key stable across process restarts | [unit] deploy/cloudflare-memory/runLedger.test.ts::run ledger — the coordinator's unit rows (item 50)::*, deploy/cloudflare-memory/runLedger.test.ts::run ledger — durable decision-record reservations (agent-ship item 16)::advances past reservations persisted on unit and run rows, and reuses a task key after a process restart |
| 50: one thread event row under the cap — attachments dropped whole and counted when the row is over it, a text alone over it cut from the end deterministically with the cut counted, attachments going first; a stored row accepted; a malformed or negative counter and an oversized id, sender or name refused | [unit] src/core/coordinator/contract.test.ts::capThreadEvent and isThreadEvent — one event row of a unit's list::* |
50: the events on the object — append assigns sequences and applies the cap (attachments dropped, a text alone cut and counted), a caller's consumedBy never stored, list filters unconsumed, mark-consumed is idempotent, a put of the unit row leaves the events untouched | [unit] deploy/cloudflare-memory/runLedger.test.ts::run ledger — the coordinator's unit events (record 0051's reply-as-event rule)::* |
49, 50: the rows as the runner fills them — unit-start writes the thread, source, issue and start, pr-check the pull request, round the boundaries, unit-end the ending; finish writes the parent's record from them under the instance's run id with its label, the requester's identity and visibility, one ship_round per boundary and the plan's summary as the answer, a valid record | [unit] src/channels/adminCoordinator.test.ts::the plan runner's steps — plan, unit-start, branch, round, unit-end, finish (item 9)::unit-start opens a plan unit's thread through the requesting thread's channel and no review thread…, ::pr-check for a unit looks up the unit's branch and remembers the pull request on the row, ::round appends the boundary to the unit's row…, ::unit-end writes the ending and the pull request on the row… |
47: on the object — a terminal record committed through put, outside the ledger's finish (the run loop's or a reclaim's interrupted close, the pi harness's restart, a resume abandoning a lost workspace), sends exactly one run-finished-<runId> after the commit; the start tombstone (finishedAt equal to startedAt) sends nothing; a record without the tag sends nothing | [unit] deploy/cloudflare-memory/runLedger.test.ts::run ledger — the coordinator's event and the key (items 47–48)::a terminal record committed through put, outside the ledger's finish… |
47: the state Worker's template binds the bot's ShipCoordinator across scripts by the bot's script name, the Workflow named as the shim names its own; the bot's template keeps its same-script binding under the same class | [unit] src/deploy/wranglerTemplate.test.ts::templateView::the committed state Worker template binds the bot's ShipCoordinator Workflow by the bot's script name, deploy/cloudflare/coordinator.test.ts::the state Worker's template binds this class across scripts::* |
47a: a coordinator child abandoning a lost workspace whose request RESTARTS publishes child_resumed (issues 1903/1876: the resume succeeded, the parent keeps waiting) and one whose request cannot restart publishes child_interrupted — either with the note's reason, on its stream and into the closing row's record past the highest replayed seq, its Workflow twin sent to the instance; a run no coordinator spawned publishes neither; a resumed child's re-attach publishes child_resumed and a refusing engine is swallowed, never thrown | [unit] src/core/dispatch/reattach.test.ts::abandonLostWorkspace: the resumed run closes saying why, and hands its request back::a coordinator's child whose request RESTARTS publishes child_resumed…, ::a coordinator's child whose request CANNOT restart publishes child_interrupted…, ::a run no coordinator spawned publishes no child event and sends nothing, ::announceChildRoll for a resumed child publishes child_resumed and sends child-resumed-<runId>… |
47a: a coordinator child relaunched in the replacement container publishes child_resumed on its own record — the mid-run resume, the roll survived under the run's id, tag and budget — and a run no coordinator spawned publishes no child event on a roll it survived | [unit] src/core/dispatch/runLoop.test.ts::the pi harness — the container replaced under a living bot: the relaunch ceiling::a coordinator's child relaunched in the replacement container publishes child_resumed…, ::a living bot, the typed error from the log read… |
47a: sendChildSignal sends child-interrupted-<runId> / child-resumed-<runId> with { runId, kind, reason, at, parentInstanceId }, both types in the platform's alphabet; no binding is no-binding and a refusal is answered failed, never thrown | [unit] src/core/coordinator/contract.test.ts::sendChildSignal — the deploy-roll signal a child sends its parent::*, src/core/coordinator/contract.test.ts::the coordinator's names::the deploy-roll signals ride the same alphabet… |
47a: a restarting close stamps finishedAt and restartUntil = finishedAt + RESTART_CLAIM_GRACE_MS from one injected clock; the eager restart_died correction drops both restart fields; record normalization preserves legacy rows without a deadline and valid bounded deadlines, but fails closed for a deadline before the close or beyond the grace | [unit] src/core/dispatch/admission.test.ts::closeResumedRow / closeRestartRow — one attempt, the final word said (item 54)::a restarting close stamps its finish and one central claim deadline from the injected clock; src/core/dispatch/reattach.test.ts::the restarting ending::a close a restart follows carries \restarting: true` on its record…, src/core/dispatch/reattach.test.ts::recordRestartDeath: the restart died between the restarting close and the successor's claim (issue 2081)::ends the record for real…; src/core/runRecord.test.ts::isRunRecord::normalizes a restart deadline only inside the close's central grace…` |
47a: after a whole-process death, fresh read-side dependencies with only the restarting record answer running one millisecond before its deadline and the original interruption at equality and after, never wall_clock_cap; a successor wins before or after expiry, an overlong deadline cannot hide it, a legacy row remains compatible, and a normal same-id restart stays live then completes | [unit] src/channels/adminCoordinator.test.ts::POST /admin/coordinator/read-record — an interrupted child (issues 1903/1876)::a whole-process death leaves only the restarting record…, ::a successor wins before the restart deadline and when it appears before a later read after expiry…, ::an interrupted record carrying \restarting: true` with no successor row yet answers the child as still running…, ::a normal same-id restart is the live registry row and can complete…` |
43: the lookup reads the repo's newest records first and each record's events newest first, matches only submitted + same repo, PR and head, keeps a copy's fromRunId, and stops after limit records | [unit] src/core/reviewDescription.test.ts::findSubmittedPrDescription::* |
44: the appended artifact takes the next seq after every existing event, a replacement keeps eventCount, the event cap leaves a review_artifact's summary alone, and a missing record / a stored: false put are named failures with nothing created | [unit] src/core/reviewAbridge.test.ts::ReviewAbridger.abridge — the one path::absent → running…, ::is idempotent…, src/core/reviewAbridge.test.ts::ReviewAbridger — failures and the append::a record deleted…, ::a rewrite the store answers…, src/core/runRecord.test.ts::capEvent on a review_artifact::does not shrink the summary… |
A 4 MB event list fits under 1.5 MiB; first and last events kept; head/tail balanced; truncated: true; eventCount preserved; storedEventCount = events.length; source untouched | [unit] src/core/runRecord.test.ts::fitRecordToBudget::fits a 4 MB event list under 1.5 MB, keeping the first and last events, marking truncated, preserving eventCount |
An under-budget record is returned with its events intact and truncated untouched | [unit] src/core/runRecord.test.ts::fitRecordToBudget::returns an under-budget record unchanged (same events, truncated stays false) |
| 30: Live review artifacts preserve their exact bytes and neighboring events across the Worker boundary; large bursts fit the request limit, while oversized artifacts and foreign writers remain refused | [unit] deploy/cloudflare-memory/runLedger.test.ts::durable review artifact appends::*; src/core/runLedgerWorker.test.ts::WorkerRunLedger::stops partitioned appends at the first refused or unknown acknowledgement |
A single 200 KB event is capped to ≤ 64 KiB with an ellipsis, on summary and on text alike | [unit] src/core/runRecord.test.ts::fitRecordToBudget::caps a single 200 KB event to at most 64 KiB, ending its summary with an ellipsis, ::caps a \text` field the same way when an event carries text instead of summary` |
| Per-event cap measures UTF-8 bytes, not chars | [unit] src/core/runRecord.test.ts::fitRecordToBudget::measures bytes, not chars: multi-byte text is capped by its UTF-8 size |
A 31-day-old record is hidden at retentionDays: 30; a 29-day-old one kept | [unit] src/core/runRecord.test.ts::applyRetention::hides a record finished 31 days ago under retentionDays 30 and keeps a 29-day-old one |
Newest 3 of 4 kept at maxRuns: 3, newest first; ties broken by id desc | [unit] src/core/runRecord.test.ts::applyRetention::keeps the newest 3 of 4 under maxRuns 3, newest first, ::breaks a finishedAt tie by id descending |
maxBytes trims the oldest large runs even when maxRuns would keep them | [unit] src/core/runRecord.test.ts::applyRetention::maxBytes trims the oldest large runs even when maxRuns would keep them |
Missing bytes counts as 0; input never mutated | [unit] src/core/runRecord.test.ts::applyRetention::treats a missing bytes field as 0 and never mutates the input |
| 5, 61: live and retained child holders keep existing source records; the last holder's removal releases them without recursive retention or resurrection | [unit] src/core/runRecord.test.ts::applyRetention — child context sources::* |
| 61: persisted child manifests use the canonical validator; malformed manifests and foreign or malformed thread markers are rejected, while legacy sessions still read | [unit] src/core/runRecord.test.ts::isRunRecord — the child context manifest::*, src/core/runRecord.test.ts::isRunRecord — the session field::* |
| 61: inferred repository provenance survives event projection and JSON storage | [unit] src/core/runRecord.test.ts::an inferred repository keeps context provenance through event projection and JSON storage |
clampRetentionPolicy fills defaults, clamps out-of-range values to the bounds, replaces non-finite values, floors fractions | [unit] src/core/runRecord.test.ts::clampRetentionPolicy::* |
isRunRecord accepts the tracing stamps (receivedAt, sealedAt, replyOk, stepCount, schema; tracing.md) when typed and refuses them otherwise; the registry projects receivedAt from the meta onto the summary and the snapshot; the Worker clamps receivedAt/sealedAt like finishedAt; the history seed's duration is runDurationMs | [unit] src/core/runRecord.test.ts::isRunRecord::accepts the tracing stamps when typed (docs/reference/specs/tracing.md) and refuses them otherwise, src/core/runRegistry/projections.test.ts::*::carries receivedAt from the RunMeta onto the summary and the snapshot, and omits it when absent (docs/reference/specs/tracing.md), deploy/cloudflare-memory/runs.test.ts::*::receivedAt and sealedAt a year ahead are clamped like finishedAt (docs/reference/specs/tracing.md), src/channels/liveView.test.ts::*::a record carrying receivedAt seeds a duration that opens there — the one definition (docs/reference/specs/tracing.md) |
2: isRunRecord accepts the run's pull request as { number, url, head? } — a positive integer, a URL and, when present, a non-empty branch, also after a JSON round-trip — and refuses any other shape; prOfEvents reads the last pr_opened and nothing without one, carrying the head branch when the event names it; assembleRunRecord folds it into pr and writes no key without one; a persisted view and a finished registry row carry it from the store | [unit] src/core/runRecord.test.ts::the pull request on the record (docs/reference/specs/run-history.md item 2)::*, src/core/dispatch/record.test.ts::assembleRunRecord — the pull request on the record (docs/reference/specs/run-history.md item 2)::*, src/core/runsService.test.ts::RunsService — the pull request on the record (run-history item 2)::* |
2: authenticatedAs — the bound credential behind the person (authorization.md item 15) — is written from the message, validated as an optional string, survives a JSON round-trip and is absent for a sender who is their own credential | [unit] src/core/dispatch/record.test.ts::*::carries authenticatedAs when a bound credential sent the request, and the record still validates; a sender who is their own credential → no key, src/core/runRecord.test.ts::isRunRecord::rejects missing identity/timing fields and a non-boolean truncated |
| 2: a door decision that starts no ordinary run is still recorded; an offered operator write and its confirmed click leave the operator and command outcomes legible | [unit] src/core/dispatcher.test.ts::the operator behind routing.operator (record 0057; routing-and-config item 29)::on: a config-set-me bind on a channel with offer mints one confirmation row…, ::on: the offered bind's Yes runs it as the requester…, src/core/dispatch/commandRun.test.ts::recordRoutedDecision — a door decision as a run record no surface is told about (record 0044)::* |
isRunRecord accepts a well-formed record (also after a JSON round-trip) and keeps the run-page event fields verbatim | [unit] src/core/runRecord.test.ts::isRunRecord::accepts a well-formed record, ::round-trips the run-page fields on tool, input and model.turn span events verbatim … |
45: isRunRecord accepts a profile of the typed shape — with or without a clip, also after a JSON round-trip — a record without one carrying no key, and refuses an unknown class or identity, a non-positive or non-numeric budget, an unknown clip scope, a non-string preset, a non-object; the one record assembly carries the profile under its preset's name and omits the key when the caller has none; the tombstone at the loop's start and the finish record carry the run's effective profile; a reclaimed row's record takes the profile the row carries | [unit] src/core/runRecord.test.ts::isRunRecord — the profile field::*, src/core/dispatch/record.test.ts::assembleRunRecord — the handoff on the record::carries the run's effective profile with its preset, and the record still validates, ::no handoff → no key (the record's JSON is exactly what the store measures), src/core/dispatch/record.test.ts::writeTombstone — the provisional interrupted record at the loop's start::a fresh run writes one provisional record: interrupted, finishedAt = startedAt, the events so far, src/core/dispatch/record.test.ts::registerFinishRecord — the finish record, written by the drain after the reply::the handoff the run loop captured rides the finish record |
45: a resume runs on the step record's remainingMs — the seed carries the run's effective budget, a clipped one included — never a budget re-derived from a preset | [unit] src/core/runLedger/resume.test.ts::planResume::the budget a resume runs on is the step record's remainingMs — the seed carries the run's EFFECTIVE budget (a clipped one included), so nothing is ever re-derived from a preset |
2, 3: isRunRecord accepts a verdict, a 7-to-40-hex reviewed head, a review post (posted at a pinned head to a named pull request, or skipped with a reason) and a disposition set — also after a JSON round-trip — a record without them carrying no key, and refuses a malformed verdict, a head outside the pattern, a malformed post and a malformed set; the stored post shape refuses a posted outcome without its target or head and a skip without a reason, and redaction walks the skip's reason; the stored shapes accept a parsed verdict and set and refuse an unknown kind, a missing finding field and a non-string note; redaction walks the summary, every finding's file and title and every disposition's note; the one record assembly carries the three redacted — and the review post, its skip's reason redacted — and omits the keys when the run has none; a review run's finish record carries the verdict it submitted and the head it reviewed; a findings child records only the exact issued IDs accepted by submit_dispositions (including a correction after an invalid call), and a coding run that submitted none records none | [unit] src/core/runRecord.test.ts::isRunRecord — the review's verdict and head, the fix round's dispositions::*, src/core/reviewVerdict.test.ts::the stored shapes — isReviewVerdictShape, isFindingDispositionsShape, and their redaction::*, src/core/dispatch/record.test.ts::assembleRunRecord — the handoff on the record::carries the verdict, the reviewed head and the dispositions…, ::carries the review post — posted at the pinned head, or skipped with its reason redacted…, src/core/reviewVerdict.test.ts::the stored review post — isReviewPostShape and its redaction::*, src/core/dispatcher.test.ts::review post-step::head-moved note (item 10)::the review run's record carries the submitted verdict…, src/core/dispatcher.test.ts::coding PR post-step (docs/reference/specs/pr-description.md)::a findings child refuses unknown IDs… |
isRunRecord accepts a handoff of the typed shape (agent-ship.md item 14), also after a JSON round-trip, and refuses a malformed one, a record without one carrying no key; the one record assembly carries a submitted handoff redacted and omits the key when none was submitted; the finish record carries the handoff the run loop captured | [unit] src/core/runRecord.test.ts::isRunRecord::accepts a handoff of the typed shape…, src/core/dispatch/record.test.ts::assembleRunRecord — the handoff on the record::*, src/core/dispatch/record.test.ts::registerFinishRecord — the finish record, written by the drain after the reply::the handoff the run loop captured rides the finish record |
A stored diagnosis missing a current category (or carrying an unknown one) is accepted; normalizeDiagnosis zero-fills and drops; the store and Worker reads normalize the same way | [unit] src/core/runRecord.test.ts::isRunRecord::accepts a record whose diagnosis lacks a current category or carries an unknown one …, src/core/runStore.test.ts::* — RunStore contract::a stored record missing a diagnosis category still loads and lists …, src/core/runStoreWorker.test.ts::WorkerRunStore::get and list normalize a stored diagnosis missing a current category …, deploy/cloudflare-memory/runs.test.ts::run history routes::a stored record missing a diagnosis category still reads via get and list … |
isRunRecord rejects a bad id (space, empty, > 64 chars) | [unit] src/core/runRecord.test.ts::isRunRecord::rejects a bad id |
isRunRecord rejects an unknown status, non-array events, an event without a string type, non-object input, wrong-typed identity/timing fields | [unit] src/core/runRecord.test.ts::isRunRecord::rejects an unknown status, a non-array events, an event without a string type, and non-object input, ::rejects missing identity/timing fields and a non-boolean truncated |
isRunListItem accepts a record minus events (with/without numeric bytes), rejects a bad bytes or a bad row | [unit] src/core/runRecord.test.ts::isRunRecord::isRunListItem accepts a record minus events (with or without numeric bytes) and rejects a bad bytes or a bad row |
storedEventSeqs keeps registry stamps when strictly increasing, else positions for every event | [unit] src/core/runRecord.test.ts::isRunRecord::storedEventSeqs keeps the registry stamps when strictly increasing, else positions for every event |
| The module has no Node built-in imports (Worker-importable) | [agent] grep -n "node:" src/core/runRecord.ts prints nothing, and cd deploy/cloudflare-memory && npm run typecheck (which compiles the import) is clean. |
A 2 MB record round-trips in the in-memory and file stores; list carries no events and reports bytes | [unit] src/core/runStore.test.ts::* — RunStore contract::round-trips a 2 MB record and lists without events |
A malformed id (../../etc/x, spaces) is not-found on every method, never thrown, never touches the filesystem or the network | [unit] src/core/runStore.test.ts::* — RunStore contract::rejects ids failing RUN_ID_PATTERN without throwing, ::FileRunStore::get("../../etc/x") is not-found and touches nothing outside data/runs, src/core/runStoreWorker.test.ts::WorkerRunStore::rejects a bad id locally without a request |
list: newest-first, default 50, cap 200, sinceMs/agent/channel filters; the { before, beforeId } cursor shows every same-finishedAt sibling across a page boundary | [unit] src/core/runStore.test.ts::* — RunStore contract::list is newest-first, capped at 200 (default 50) …, ::list cursor {before, beforeId}: two runs with identical finishedAt straddling a page boundary both appear |
events(id, {afterSeq: 10, limit: 5}) returns five events with seq > 10 and nextAfterSeq; the last page has no cursor; unknown id → null; a run with no events → { events: [] }; events stamped 2001..7000 page from afterSeq: 6500 by their stored seq | [unit] src/core/runStore.test.ts::* — RunStore contract::events(id, {afterSeq: 10, limit: 5}) returns five events with seq … and a cursor …, ::events keep the registry seq they were published with … |
Expired record absent from get/list before any write; put of an already-expired record is stored: false; rewritten only on a changed record; delete removes run + events | [unit] src/core/runStore.test.ts::* — RunStore contract::an expired record is absent from get/list before any write, ::put reports stored:false for a record already outside policy, ::rewritten is true only when an existing record changed, ::delete removes the run and its events |
| File mode 0600, dir 0700, no temp files left after rename | [unit] src/core/runStore.test.ts::FileRunStore::writes <id>.json with mode 0600 inside a 0700 directory, temp-then-rename |
Torn <id>.json skipped by get, absent from list, others still read; stale index line hidden then healed on the next put; expired record unlinked and the index compacted on the next put; shrink-then-grow does not resurrect; persists across instances; sweep() unlinks expired files with no writes | [unit] src/core/runStore.test.ts::FileRunStore::* |
buildRunStore: unconfigured → null (no timer); store: file → FileRunStore with a 6 h unref'd sweep timer that unlinks expired files; worker without bearer → null + warning naming the env var; worker with bearer → WorkerRunStore | [unit] src/core/runStore.test.ts::buildRunStore::* |
WorkerRunStore.put of a 1.9 MB record sends a string body with NO hand-set Content-Length (runtime-derived), bearer, policy + policyUpdatedAt; get/list/events/delete send no policy; get re-validates and returns null for {record: null}; 404 → RouteMissingError, 503/408/429/network → TransientStoreError, 400 → PermanentStoreError with the Worker's error text | [unit] src/core/runStoreWorker.test.ts::WorkerRunStore::* |
WorkerRunStore.put under a span is an http.client child with route /runs/put and the trace context for the configured host; the history writer hands its span option through to the sink (tracing.md item 24) | [unit] src/core/runStoreWorker.test.ts::WorkerRunStore trace context::*, src/core/runHistoryWriter.test.ts::createRunHistoryWriter::write hands its span to the store's put… |
Config: accepts a well-formed runHistory; rejects retentionDays: 0, maxRuns: 0, http:// worker.baseUrl, unknown store | [unit] src/config.test.ts::runHistory config::* |
DO: put→get round-trip with events in seq order; unknown id {record: null} 200; 5000 events in one transaction, paged by seq; registry seq preserved (2001..7000 pages from 6500); two puts one id → one coherent set; identical repeat rewritten: false; failed mid-put leaves no rows | [unit] deploy/cloudflare-memory/runs.test.ts::run history routes::put → get round-trips the record with events in seq order …, ::inserts 5000 events inside one transaction and pages them by seq, ::events keep the registry seq …, ::two puts for one id leave exactly one coherent event set …, ::a put that fails mid-transaction leaves no runs row and no events |
DO policy: newer policyUpdatedAt wins, older ignored, retentionDays: 0 → 400, future stamp clamped to the DO clock so a later correct one wins; get/list cannot resurrect with a body policy | [unit] deploy/cloudflare-memory/runs.test.ts::run history routes::policy: the newer policyUpdatedAt wins …, ::get/list never accept a policy … |
DO trim: a shrink dropping >25% deletes ≤500 per put while list/get/events hide the rest (age, then newest maxRuns with id tie-break, then maxBytes); the next put finishes; evicted run's events gone; re-put with 3 events leaves 3 rows; outside-policy put stored: false | [unit] deploy/cloudflare-memory/runs.test.ts::run history routes::a shrink dropping >25% deletes at most 500 rows per put while list already hides them, ::get/events hide exactly what list hides for rows still on disk …, ::after a maxRuns trim the evicted run's events are gone … (the get hiding assertion goes red when the read-side retention check is removed) |
DO clock: finishedAt a year ahead clamped to now + 24 h, stored_at recorded; an alarm with no writes deletes expired rows when scheduled cleanup is enabled and re-arms | [unit] deploy/cloudflare-memory/runs.test.ts::run history routes::a finishedAt one year ahead is clamped to now + 24 h and stored_at is recorded, ::the alarm deletes expired rows with no writes, and get is then not-found |
When enabled, the alarm's orphan sweep deletes an event row only when neither runs nor live_runs owns its run; policy trimming is a separate phase | [unit] deploy/cloudflare-memory/runLedger.test.ts::run ledger — alarm retention of live events::keeps an unfinished run's events while pruning orphaned event rows |
| With scheduled cleanup paused, the real alarm skips trim, pin reconciliation, session drop and orphan/receipt pruning; live reads and keyed append still work, and the alarm still probes a waiting resident and offers an expired lease | [unit] deploy/cloudflare-memory/runLedger.test.ts::run ledger — alarm retention of live events::keeps scheduled cleanup paused while the alarm serves live work, deploy/cloudflare-memory/runLedger.test.ts::the plane's resident stage — /plane/level, /plane/observe, the re-ask alarm (orchestration-plane item 9; record 0064)::a paused-maintenance alarm still probes a waiting resident and offers an expired lease |
The alarm is a state.alarm root in the state Worker's log, ending with swept (zero while cleanup is paused; tracing.md item 25) | [agent] live: wrangler tail <state-worker> across one alarm. |
Fences: 2 MiB at cap ok, +1 byte 413, 1.9 MB multibyte measured in bytes, other routes 512 KB, missing Content-Length 411; 400 on bad key/record/id/limit/beforeId; unknown route 404; no bearer 401; GET 405 | [unit] deploy/cloudflare-memory/runs.test.ts::run history routes::body fence: 2 MiB cap on /runs/put …, ::validates: bad store key, malformed record, bad id, bad limit → 400 …, ::list cursor {before, beforeId}: … a bad beforeId is 400 |
Corrupt event row skipped; /healthz lists runs; list limit 1000 → 200 + cursor with filters and no events on the wire; delete removes all events | [unit] deploy/cloudflare-memory/runs.test.ts::run history routes::a corrupt event row is skipped …, ::/healthz lists runs, ::list: newest-first, limit 1000 → at most 200 rows plus a cursor …, ::delete removes the run and all its events … |
State Worker (migration v4, run-history routes) deployed before the bot that writes to it; /healthz lists runs | [agent] Deploy deploy/cloudflare-memory (npm run deploy) first; curl https://<state-worker>/healthz → features includes runs; the bot's startup log then has no ORDERING ERROR line. |
Completed run → one put with status: completed, eventCount = the registry's published count = events.length, truncated: false, events include the input and answer events, identity/model/label fields set, diagnosis = analyzeRunFriction(events), persisted flagged on the index row, pending() back to 0 | [unit] src/core/dispatcher.test.ts::run history write path …::a completed run ends as one stored record: status completed, eventCount = published count, events include the user and assistant messages, identity fields set |
Soft-stopped run → stopped_soft; a review stopped before substantive review activity records no verdict, reviewed head or review-post outcome and replies with only the stop sentence, while one stopped after checkout and a real verdict retains its findings-so-far; a reply that throws after the loop completed → failed while a stop keeps its stopped_* status | [unit] src/core/dispatcher.test.ts::run history write path …::a soft-stopped run is stored as stopped_soft…, src/core/dispatcher.test.ts::review post-step::a soft stop before review activity replies only that it stopped and runs no review tail: pending completion…, src/core/dispatcher.test.ts::review post-step::a soft stop after checkout and real findings keeps the findings-so-far review behavior, ::a reply that throws after the run loop completed yields status failed (not completed) and seals the run once… |
Provider throw → status: failed, the record (with the input event) is still written, the failure reply still sent, activeRunCount() back to 0 | [unit] src/core/dispatcher.test.ts::run history write path …::a provider throw yields status failed, the record is still written, and the failure reply is still sent |
| The record and the friction row carry the registry's redacted label — a pasted token in the request reaches neither | [unit] src/core/dispatcher.test.ts::run history write path …::the record and the friction row carry the registry's redacted label … |
A reply slower than the registry TTL (ttlMs: 10, 40 ms reply) still yields a full record; the record's events equal the finish-time snapshot even though it is assembled after the reply | [unit] src/core/dispatcher.test.ts::run history write path …::a reply slower than the registry TTL: the finished run stays unsealed…, ::the record's events equal the registry snapshot taken at finish … |
More published events than the backlog holds (backlogLimit: 16, 12 context turns) → eventCount = published total, storedEventCount: 16, truncated: true, and what survives is the protected head (the input, the context, the meta) plus the newest events | [unit] src/core/dispatcher.test.ts::run history write path …::more published events than the backlog holds: eventCount is the published total, storedEventCount the backlog length, truncated true — and the protected head… (scaled stand-in; the 8000-count bound itself is covered in runRegistry.test.ts) |
| Byte budget: over budget, span records go first, pair by pair from the middle outward, never from the head; every content event survives when that suffices; a budget below the content alone still drops content from both ends | [unit] src/core/runRecord.test.ts::fitRecordToBudget::over budget, span records go first… |
The FINISH put is not called until after io.reply ran — when the reply runs, the store holds only the start-of-run tombstone (goes red when the finish write is moved before sendAnswer) | [unit] src/core/dispatcher.test.ts::run history write path …::the finish write happens after the reply: only the provisional tombstone has been put when io.reply runs |
| Without a writer nothing is written and the run behaves as before | [unit] src/core/dispatcher.test.ts::run history write path …::without a writer nothing is written and the run behaves as before |
503 twice then 200 → exactly one stored record, three attempts, backoff 1 s then 4 s (jittered ±50%), pending() 0, failures() 0 | [unit] src/core/runHistoryWriter.test.ts::createRunHistoryWriter::503 twice then 200: three puts of the same record, backoff 1 s then 4 s (jittered), one success, pending 0, no failure counted, ::the jitter is bounded: a delay is within ±50% of its base value …, src/core/dispatcher.test.ts::run history write path …::put 503 twice then 200: exactly one record, pending back to 0, no failure counted |
pending() counts a write sitting in retry backoff; concurrent writes tracked independently; settled() waits for all | [unit] src/core/runHistoryWriter.test.ts::createRunHistoryWriter::pending() counts a write that is sitting in retry backoff, ::concurrent writes are tracked independently: pending() is the number in flight, settled() waits for all |
| Retries exhausted → counted once, warned once with run id + attempt count | [unit] src/core/runHistoryWriter.test.ts::createRunHistoryWriter::three transient failures: the write is lost, counted once, warned once with the run id and attempt count |
413 (PermanentStoreError) → one attempt per record (tombstone and finish write alike), no sleep, one warn each, failures() counted per loss; the reply unaffected and never mentions the store | [unit] src/core/runHistoryWriter.test.ts::createRunHistoryWriter::a PermanentStoreError (413) is never retried: one put, one warn, failures +1, no sleep, src/core/dispatcher.test.ts::run history write path …::a 413 (PermanentStoreError) is not retried: one attempt, one warn, failures +1; the reply is unaffected |
404 (RouteMissingError) → the deploy-ordering line exactly once per process across two runs, no retry, degraded() true, every loss counted; a lost record is also a run friction report never sees | [unit] src/core/runHistoryWriter.test.ts::createRunHistoryWriter::a RouteMissingError (404) logs the deploy-ordering message ONCE per writer, sets degraded, never retries, counts every loss, src/core/dispatcher.test.ts::run history write path …::a 404 (RouteMissingError) logs once, is not retried, sets degraded; the runs are still counted |
An unclassified error is retried; a throwing onPersisted hook is isolated | [unit] src/core/runHistoryWriter.test.ts::createRunHistoryWriter::an unclassified error (a file store's fs failure) is treated as transient: retried, then counted, ::a throwing onPersisted hook is isolated: still counted as a success and pending drains |
markPersisted sets the flag and emits one upsert; no-op for unknown/evicted runs; the key is absent until then; snapshot carries startedAt + monotonic eventCount | [unit] src/core/runRegistry.test.ts::RunRegistry.markPersisted::*, src/core/runRegistry/projections.test.ts::RunRegistry.snapshot — record inputs …::* |
Live run via getRun → finished: false, no token property, no events unless include: "messages"; persisted run → same shape with finished: true, persisted: true (goes red when liveView becomes a spread of RunSummary) | [unit] src/core/runsService.test.ts::RunsService.getRun::returns a live run as finished:false with no token and no events unless asked, ::returns a persisted run in the same shape (finished:true, persisted:true), events only on include |
A finished run still in the registry carries verdict, reviewHead, reviewPost, dispositions and handoff from the store's summary row the moment the store holds its record — identity, status, finishedAt and events the registry's, store.get never called, the list saying the same; a row whose record has not landed carries none and is not persisted (the tombstone lends nothing, not its status), a live row never asks the store, a store that throws is one warning naming the run and the registry row | [unit] src/core/runsService.test.ts::RunsService.getRun::a finished run still in the registry carries verdict, reviewHead, reviewPost, dispositions and handoff from the store the moment the store holds its record…, ::a finished registry row whose record has not landed carries no artifacts and is not persisted… |
Unknown, malformed (../etc), and expired (31-day-old) ids → not_found; store null → live reads work, persisted are not_found | [unit] src/core/runsService.test.ts::RunsService.getRun::is not_found for an unknown id, a malformed id, and an expired record, ::works with history off (store null): live runs read, persisted ones not_found |
Run in both registry and store listed once with the live stop state and the record's finish-only fields; same eventCount before and after TTL eviction, persisted: true both times; the live and persisted projections differ only in the finish-only fields | [unit] src/core/runsService.test.ts::RunsService.listRuns — read merge::lists a run in both the registry and the store once, live stop state winning, same eventCount before and after eviction, ::a run in both sources projects identically from the live row and the persisted row, except for the finish-only fields |
active = unfinished registry runs and never calls store.list (spy); finished = finished registry ∪ store; all = union | [unit] src/core/runsService.test.ts::RunsService.listRuns — read merge::active = unfinished registry runs only and never calls the store; finished = finished registry ∪ store; all = union |
Sort by the store's key: live rows first, then finishedAt desc, id desc — a true top-N even when an old run started late; nextBefore pages the store without skipping same-ms siblings, omits live rows past the first page, and stays present when a full store fetch becomes a short visible page after tombstone filtering | [unit] src/core/runsService.test.ts::RunsService.listRuns — read merge::orders by the store's key — live rows first, then finishedAt desc, id desc …, ::pages with nextBefore = the last persisted row's {finishedAt, id} …, ::keeps a store cursor when filtering a provisional tombstone makes a fetched page short |
threadKey narrows live and persisted rows to one thread's runs, newest first, so the thread's newest run is one read with limit: 1 | [unit] src/core/runsService.test.ts::RunsService.listRuns — read merge::\threadKey` narrows the listing to one thread's runs…` |
agent/channel/sinceMs filters apply to live rows through their RunMeta as to persisted rows | [unit] src/core/runsService.test.ts::RunsService.listRuns — read merge::filters by agent, channel and sinceMs on live rows too — a live run carries the RunMeta given at create() |
limit after the merge (10 → 10 rows, live rows newest), default 50, cap 200; store.list asked for limit + liveCount, never above 200; a 5000-run store fetches one bounded page | [unit] src/core/runsService.test.ts::RunsService.listRuns — read merge::applies limit after the merge (default 50, cap 200) and fetches at most limit + activeCount persisted rows, ::listRuns({limit:10}) against a 5000-run store fetches a bounded page |
Store throwing → all/finished return live rows + storeUnavailable: true; active unaffected and never calls the store; history off never reports storeUnavailable | [unit] src/core/runsService.test.ts::RunsService.listRuns — read merge::degrades to live rows + storeUnavailable when the store throws…, ::with history off (store null) lists live rows and never reports storeUnavailable |
A transient history read failure on runs.get is unavailable (503), distinct from the not_found mask for a run outside the actor's view; the error reveals no run identity or store detail | [unit] src/core/commands/runs.test.ts::channel visibility (authorization.md items 5–7)::runs.get reports a transient history outage as unavailable, distinct from a hidden run |
getRunEvents strict seq > afterSeq live and persisted; a 5000-event persisted run → 500 events + nextAfterSeq: 500 read via store.events with limit: 500; limit: 100000 still 500; a live-taken cursor addresses the same events once persisted | [unit] src/core/runsService.test.ts::RunsService.getRunEvents::live: seq … afterSeq strictly, honoring limit, with nextAfterSeq while more follow, ::persisted: seq … afterSeq strictly; a 5000-event run returns a bounded page (≤ 500 events) with nextAfterSeq, read via store.events, ::RunsService.listRuns — read merge::a live→persisted afterSeq cursor addresses the same events … |
A page stops under 256 KiB of event JSON (4 × 60 KiB events) and resumes from nextAfterSeq; unknown/expired → not_found; a zero-event persisted run → ok with an empty page | [unit] src/core/runsService.test.ts::RunsService.getRunEvents::caps a page at 256 KiB of event JSON and resumes from the cut, ::is not_found for an unknown or expired run; a persisted run with zero events is ok with an empty page (getRun ok too) |
getRunFriction: live → analyze(events, { finished }) (spy sees the stamped events + flag), persisted → the stored diagnosis, unknown → not_found | [unit] src/core/runsService.test.ts::RunsService.getRunFriction::* |
stopRun live → { mode, state: "stopping" }, control driven, stop_requested note with actor; actor id stripped to the charset and capped at 128 (unknown when empty); finished/persisted → conflict; unknown → not_found | [unit] src/core/runsService.test.ts::RunsService.stopRun::*, src/core/runRegistry.test.ts::RunRegistry — token-free operator reads …::* |
authorizeLive → null for a wrong token or unknown run; a working subscribe/snapshot for the right token (events flow, onFinish fires) | [unit] src/core/runsService.test.ts::RunsService.authorizeLive::* |
JSON.stringify of every method's output lacks the fixture token | [unit] src/core/runsService.test.ts::RunsService — no output carries the capability token::* (plus expectNoToken inside most other cases) |
The friction ledger is the store: differential ordering vs the in-memory double, field-explicit projection, the predicate in wire form, list-only reads bounded to 500, compound-cursor paging, an unreadable store rejects with its error, no store → no ledger | [unit] src/core/frictionLedger.test.ts::RunStoreFrictionLedger::* |
LiveRunAccess.requestStop drives the registry's token-gated stop; a store row merged into its live twin keeps the live stop/persisted and gains finishedAt/status | [unit] src/channels/liveView.test.ts::live view on RunsService: history pages + index toggle …::persisted run events, friction and stop::a valid token still stops a live run (200) and 409s a finished one …, ::index: active by default, everything with ?all=1::a persisted row's flag rides the seed; the store spy sees exactly one list call for ?all=1 |
| History page, stored replay, 409 stop, 404 shapes, index toggle, row mirror | [unit] the live view on RunsService rows in live-view.md |
SIGTERM during retry backoff: the drain waits on pending() and exits within the deadline | [agent] With runHistory.store: file and data/runs made read-only, run npx tsx src/cli.ts ask "hi" in one shell and kill -TERM the bot mid-backoff: the [drain] line reports 1 history write(s) in flight, then exits ≤ 15 min later reporting the write as lost. (No unit test: the drain loop lives in main().) |
Locally with runHistory.store: file, a CLI run creates data/runs/<id>.json and an index.jsonl row | [agent] npx tsx src/cli.ts ask "what is 2+2" with runHistory: { store: file } in the config, then ls data/runs/ shows one <uuid>.json whose status is completed. |
getSummary (every store): the listing row with bytes, no events; null for unknown/expired/malformed ids; the file store hides a torn file from it like list does | [unit] src/core/runStore.test.ts::* — RunStore contract::getSummary returns the listing row …, ::FileRunStore::a torn <id>.json is skipped by get and absent from list … |
WorkerRunStore.getSummary posts /runs/summary, re-validates (isRunListItem), {summary: null} → null, malformed → PermanentStoreError, bad id → no request | [unit] src/core/runStoreWorker.test.ts::WorkerRunStore::getSummary posts /runs/summary … |
/runs/summary returns the row without events and reads no run_events (statement spy); {summary: null} for unknown; bad id 400 | [unit] deploy/cloudflare-memory/runs.test.ts::run history routes::/runs/summary returns the listing row … |
RunsService.getRun (no include), getRunFriction and stopRun read store.getSummary and never store.get; include: "messages" is the one full read | [unit] src/core/runsService.test.ts::RunsService — summary-only persisted reads::* |
RunHistoryDO.list fast path: within maxRuns/maxBytes the page is ONE SELECT … LIMIT ? with the agent/channel filters in SQL and no retention scan (statement spy); over a bound the kept set is computed and rows, order, cursor and filters are identical; get hides the same rows | [unit] deploy/cloudflare-memory/runs.test.ts::run history routes::list fast path …, ::get/events hide exactly what list hides … |
| A full page of recent runs stays bounded when active context pins exist: it neither scans all history nor materializes the reference graph, and the next page can still find a protected older source. Unpinned ordinary context rows do not trigger a full retention scan on a sparse filtered page. | [unit] deploy/cloudflare-memory/runs.test.ts::run history routes::a full recent page stays bounded when an older source has a retention pin, and the next page still finds that source, ::a sparse filtered page with only unpinned context rows avoids the full retention scan |
Context retention during put, finish, and a sparse history page reads coordinator-unit rows in linear work, preserving sources held by valid unit contexts while rejecting malformed or context-free holders; point reads and existing-unit writes use the unit's composite key, including a colon in the unit name | [unit] deploy/cloudflare-memory/runs.test.ts::unit context retention work::put, finish and sparse list read unit roots in linear work…, ::a protected point read looks up one unit by its composite key…, ::an unchanged unit write looks up its existing context… |
channelVisibility on the record: required on write, unknown when a stored record or listing row lacks it (normalizeStored), an unknown value rejected by isRunRecord; RunVisibilityFilter is the predicate's wire form (toVisibilityFilter sorts the sets), isRunVisibilityFilter bounds depth and width and rejects unknown kinds/visibilities, matchesVisibility is the one truth table (an empty and/or and a missing stamp match nothing but unknown) | [unit] src/core/runRecord.test.ts::isRunRecord::channelVisibility defaults to unknown …, src/core/runRecord.test.ts::run visibility filter — the wire form of an authz Predicate…::* |
list({ visibleTo }) on every store: the in-memory/file stores apply matchesVisibility ANDed with the other filters; the DO compiles it into the one page query (channel_id IN, channel_visibility IN, user_id =, or/and), returns an empty page for none without a query, answers a malformed or too-wide filter 400 (never all); a pre-stamp table gains the channel_visibility column with unknown and a put without the stamp stores unknown | [unit] src/core/runStore.test.ts::* — RunStore contract::list applies \visibleTo` …, deploy/cloudflare-memory/runs.test.ts::run history routes::list with `visibleTo` (authorization.md item 6) …, ::a table created before the visibility stamp gains the column …` |
RunsService.listRuns requires visibleTo: live rows filtered by matchesPredicate, the store asked with the wire form (none for all), none touches neither; every view carries channelVisibility; RunStoreFrictionLedger.recent({ visibleTo }) pushes it to the store in wire form | [unit] src/core/runsService.test.ts::RunsService.listRuns — read merge::\visibleTo` is pushed down …, src/core/frictionLedger.test.ts::RunStoreFrictionLedger::recent() takes the actor's predicate …` |
Dispatch stamps every run's RunMeta and record with the channel directory's visibility (static default: http:/mcp: machine, slack:D… dm, slack:G… private, else unknown); an injected directory is asked once per run; a failing directory stamps unknown | [unit] src/core/dispatcher.test.ts::run history write path …::channel visibility stamp … |
| Seal after reply: delivery success/failure, slow replies, fenced backstops and ship final replies each seal exactly once; the ending remains idempotent | [unit] src/core/dispatcher.test.ts::run history write path …::a delivered run is sealed after its reply…, ::a reply that throws after the run loop completed…, ::a reply slower than the registry TTL…, src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::a fenced finishing means another generation owns the run…, src/core/runEnding.test.ts::createRunEnding — seal after the reply, records after the seal::* |
registry.finish(id, status) stores the status; the summary and the index upsert carry status + finishedAt, and the seal's upsert adds sealedAt/replyOk; a completed / soft-stopped dispatch leaves the registry row saying so | [unit] src/core/runRegistry/projections.test.ts::RunRegistry — terminal status + finishedAt …::*, src/core/dispatcher.test.ts::run history write path …::a soft-stopped run is stored as stopped_soft, and the registry summary carries the same status …, ::a completed run's registry summary says \completed` …` |
A coding run's refused PR descriptions are on the record: each submit_pr_description refusal is a description_refused run_note carrying the object as submitted (every string leaf redacted) and the issues with their cuts, published by the tool before it answers, so the record says what changed between one submit and the next (pr-description.md item 5); the friction reader accepts the kind | [unit] src/tools/submit.test.ts::submit_pr_description tool::a refused submit is on the record…, src/core/runEventLines.test.ts::parseRunEventLines::accepts every declared run_note kind… |
A run whose loop threw carries its reason on the record: the run loop's catch publishes a run_failed run_note — the error's message, redacted and capped — before the finish, so the run page says why a failed run failed even when the reply is never delivered | [unit] src/core/dispatch/runLoop.test.ts::runLoop — the model turn and everything that rides on it::a failed run leaves its reason on the record: the loop's throw is published as a \run_failed` run_note before the finish, so the run page says why` |
A reply with no channel to deliver to (the resumed ingress run's null channel) is not a delivered reply: the seal says replyOk: false with replyNote: "no channel to deliver to" — without flipping the completed record to failed — and the record carries both | [unit] src/core/dispatch/reply.test.ts::*::a run replying on a null channel (no channel to deliver to) is sealed replyOk false with the reason, and the record carries it, src/core/runEnding.test.ts::createRunEnding — seal after the reply, records after the seal::a reply with no channel to deliver to seals replyOk false with the reason, without flipping the record to failed |
RunsService projects a finished registry row's status/finishedAt from the summary; unfinished rows sort first, finished rows by finishedAt desc | [unit] src/core/runsService.test.ts::RunsService.listRuns — read merge::a finished registry row carries the status and finishedAt …, ::orders by the store's key … |
Store failure: listRuns warns once per failing call with the error message, never a token; storeUnavailable: true | [unit] src/core/runsService.test.ts::RunsService.listRuns — read merge::degrades to live rows + storeUnavailable when the store throws, warning once … |
A head-truncated backlog (RunSnapshot.truncated) stamps the diagnosis truncatedInput: true; formatFrictionReport and the friction/proposals report say so; a full backlog leaves the field absent; normalizeDiagnosis keeps it | [unit] src/core/runFriction.test.ts::analyzeRunFriction — truncated input …::*, src/core/selfImprovement.test.ts::formatSelfImprovementReport::counts runs diagnosed on a truncated event stream …, src/core/dispatcher.test.ts::run history write path …::a completed run's registry summary says \completed`; a truncated backlog stamps the diagnosis `truncatedInput` …` |
| An inline command run (`friction report | propose, cron firings) is created with RunMeta(agentcommand, the caller's channel/user/thread) and persisted through the same writer: status from ok, events [input, answer]`, so it outlives the 60 s TTL |
| Run in flight across a container replacement → resumed, or failed visibly (its card edited, never frozen), within 60 s of the new container's Slack socket connecting; the run's record exists afterwards with a terminal status | [agent] Start a long coding run, then trigger a bot deploy so the run is in flight when the old container receives SIGTERM; after the new container logs its Slack connected, watch the run's status card and runs get id=<id>: within 60 s the card must show a terminal state (resumed-and-finished, or failed with a reason) and the record must be retrievable. Until the resume step ships, the expected outcome is the failed-visibly branch via the orphaned-card sweep. |
| A finished run is readable long after eviction in production | [agent] Behind Access, open a run's page more than 60 s after it finished: request, context, tool steps, reply and the grey finished · completed header render; /runs?all=1 lists it tokenless with duration and finished-at; the default /runs omits it; runs list --json from the CLI and GET /api/runs.list?status=finished (service token) both include its id. |
Tombstone written at start: the run's first put is a provisional interrupted record — finishedAt = startedAt, the request/context events, isRunRecord-valid — and the finish write replaces it (one stored row, final status/events win) | [unit] src/core/dispatcher.test.ts::run history write path …::tombstone-first provisional records …::a provisional interrupted record is written at run start — finishedAt = startedAt, the request/context events — and the finish write replaces it (goes red when the provisional write is removed) |
| The initial provisional record carries only the session already admitted for its own request. Without a ledger session it adds none; a resume writes no new initial record. Its ordinary checkpoint archive keeps the exact canonical requester, receipt owner and range, and refusal preserves the original seed, state and unrelated live owners. | [unit] src/core/dispatch/record.test.ts::writeTombstone — the provisional interrupted record at the loop's start::preserves the public review start session only when supplied: %s; src/core/dispatcher.test.ts::run history write path::tombstone-first provisional records::the initial record carries this request's admitted ledger session only: %s; deploy/cloudflare-memory/sessionUnification.test.ts::initial ordinary tombstone archive::* |
The provisional write never marks the run persisted: a run whose finish write is permanently lost keeps persisted unset; the writer's provisional writes skip onPersisted but keep retries and drain accounting | [unit] src/core/dispatcher.test.ts::…::the provisional write never marks the run persisted: a run whose finish write is lost keeps \persisted` unset, src/core/runHistoryWriter.test.ts::createRunHistoryWriter::a provisional write … skips onPersisted but is stored, retried and drain-counted like any write` |
A live run never lists as interrupted: its tombstone is suppressed under all AND finished while the run is unfinished in the registry, and getRun serves the live row | [unit] src/core/runsService.test.ts::RunsService.listRuns — read merge::a live run's provisional interrupted tombstone … never surfaces … (goes red when the merge fix is reverted) |
Crash case (no drain): with the registry empty, the terminal tombstone is the record — visible in runs list --status finished as interrupted | [unit] src/core/runsService.test.ts::RunsService.listRuns — read merge::a crash leaves the tombstone as the record … |
The drain-deadline write carries the FULL snapshot: interruptedRunRecord(summary, snap, now) yields every event published so far with status: interrupted, finishedAt = the drain clock, the run's identity/label | [unit] src/core/dispatch/record.test.ts::…::interruptedRunRecord (the drain deadline's seam) builds a full-snapshot interrupted record from a live run's summary + snapshot |
The drain's abandonment pass writes one PROVISIONAL record per unfinished registry run (never a finished one), from the full snapshot, and reports the count so main() knows whether to await the write budget | [unit] src/core/dispatch/record.test.ts::…::writeAbandonedRunRecords (the drain deadline's pass) writes one PROVISIONAL full-snapshot interrupted record per unfinished run, skips finished ones, returns the count (only the Promise.race budget still lives in main() — see the [agent] row below) |
| Final beats provisional: a provisional write in retry backoff stands down when the run's final write is enqueued (two puts, the final record last, no loss counted); the stand-down is per id; a provisional write enqueued after the final one never attempts | [unit] src/core/runHistoryWriter.test.ts::createRunHistoryWriter::a final write for the same id stands down a provisional write sitting in retry backoff …, …::the stand-down is per id …, …::a provisional write enqueued after the run's final write is dropped before its first attempt (backoff test goes red when the stand-down check is removed) |
The DO accepts interrupted through the SHARED validator (no worker code change; redeploy required) and the finish put replaces the provisional record whole (rewritten: true, final events) | [unit] src/core/runRecord.test.ts::isRunRecord::accepts every terminal status — \interrupted` … included …, deploy/cloudflare-memory/runs.test.ts::run history routes::tombstone-first …: the DO accepts status `interrupted` (shared validator), and the finish put replaces the provisional record whole` |
interrupted renders everywhere a status shows: red dot/chip on the index and run page, the word passes through statusLabel unchanged; runs list filters stay finished-ness (`active | finished |
The persisted flag: isRunRecord accepts provisional: true, refuses any other value, and a record without the key validates; the start tombstone and the drain-deadline record both carry it on the record itself | [unit] src/core/runRecord.test.ts::isRunRecord::provisional: true on an interrupted tombstone is valid; any other value is rejected; absent on a final record is valid, src/core/dispatch/record.test.ts::writeTombstone — the provisional interrupted record at the loop's start::a fresh run writes one provisional record…, ::interruptedRunRecord (the drain deadline's seam) builds a full-snapshot interrupted record… |
The flag reaches every store-only view: RunsService lists and gets a stored provisional tombstone with provisional: true and a final record's view carries no key | [unit] src/core/runsService.test.ts::RunsService.listRuns — read merge::a stored provisional tombstone's flag rides the view: listRuns and getRun carry \provisional: true`, and a final record's view carries no key` |
The CLI's store-only readers render the third state: runs list (text and chat) and runs get's status line say unfinished — no finish recorded for a flagged record, never interrupted; an unflagged interrupted record still says interrupted | [unit] src/core/commandRegistry.test.ts::*::renderCompact renders a provisional tombstone row as the third state…, src/core/commands/runs.test.ts::*::runs.get renders a provisional tombstone's status line as 'unfinished — no finish recorded'… |
Deploy-race re-run: a run SIGTERM'd mid-flight and abandoned at the drain deadline has a runs get-retrievable record with a terminal status within 60 s of the next container's connect, and the [drain] wrote interrupted record for <id> (<n> events) line appears | [agent] Re-run the deploy-race exercise (the container-replacement row above) after deploying the state Worker THEN the bot; check runs.list?status=finished for the probe's thread and the drain log line. |
28: the fence — the owner passes, another generation is fenced, an unknown run is named; the claim decision (no live run, a different live run refused with its identity, the owner's re-claim idempotent) | [unit] src/core/runLedger/decisions.test.ts::decideClaim — one live run per thread::*, ::checkFence — the owner generation on every write::* |
28: the Worker client — routes and bodies, the bearer, the step write's transcript-first order and its stop on a fenced transcript write, fenced answers as results, RouteMissingError/TransientStoreError/PermanentStoreError, a malformed id never leaves the process | [unit] src/core/runLedgerWorker.test.ts::WorkerRunLedger::* |
| Observational ledger reads retry transient transport, body-read, HTTP 408/429 and 5xx failures at most twice with the shared store backoff and one original HTTP deadline. Each failure retains its diagnostic. Current access denials, missing routes, malformed replies and uncertain writes are not retried. Recovered publication validation still requires the current source, exact receipt and saved bytes. | [unit] src/core/runLedger/readRecovery.test.ts::read-only ledger recovery::*; src/core/dispatch/contextAccess.test.ts::message-bound context access::checks source access, receipts, session and bytes… |
| 28: the reference ledger runs the whole protocol — claim → seed → steps → finishing → finish, thread refusal, every owner write fenced, heartbeat/stop, reclaim with the last step and unconsumed inbox, handoff | [unit] src/core/runLedger/inMemory.test.ts::InMemoryRunLedger::* |
31, 40: the heartbeat body — a coding run's beat carries the round, the in-flight call with its bound, the last event's time and the newest pushed head with its clean fact; a read run beats facts-less; on the object a bound crossed writes ONE inbox row (the fixed sentence, sender plane) in the heartbeat's transaction, once per round, again on a new round, and a second cause repeats no sentence | [unit] src/core/runLedger/writeThrough.test.ts::events, state, heartbeat::a coding run's heartbeat carries the facts body…, src/core/runLedger/writeThrough.test.ts::events, state, heartbeat::the in-flight fact tracks the call itself…, src/core/runLedger/writeThrough.test.ts::events, state, heartbeat::an adopted coding run keeps the backpressure contract…, deploy/cloudflare-memory/runLedger.test.ts::the plane's checkpoint steers and the provider condition — the heartbeat body, /plane/park, /plane/level provider (record 0064)::*, src/core/plane/decide.test.ts::decide — the checkpoint steers and the provider condition (record 0064)::* |
29: claim, thread-live 409 with the live run, idempotent re-claim, /runs/live lists the row, a live run is absent from the finished listing; validation 400s and 401 | [unit] deploy/cloudflare-memory/runLedger.test.ts::run ledger — claim and admission (item 29)::* |
29: the host key — hostKeyOf suffixes and threadOf inverts, isHostKey tells the two apart, a suffixed key and one over the 256-character cap refused by name; the ledger accepts a host-key claim beside a child's claim on the thread and refuses a second host-key claim thread-live | [unit] src/core/runLedger/hostKey.test.ts::the host key (run-history item 29; record 0060)::*, ::the ledger under a host key (run-history item 29)::* |
29, 41: a ledger row claimed under the host key lists under its conversation's thread with hosted and its label (never under the key column); a registry row created with meta.hosted lists the same | [unit] src/core/runsService.test.ts::RunsService with the run ledger — one registry across generations (run-history item 41)::lists a ledger row claimed under the host key under its conversation's threadKey, with hosted and its label; a registry row created with meta.hosted lists the same |
16 (agent-ship), 29: the ship branch claims under the host key with the thread, hosted and the label in the metadata and registers no session; a live host-key row refuses a second ship by name with nothing handed to the runner; an untracked answer that is not thread-live hands off as before | [unit] src/core/dispatch/ship.test.ts::runShipBranch — the host key and the hosted marker (record 0060)::* |
36: a host-keyed row closes under the metadata's thread — the reclaim's closed outcome and reclaimedRunRecord both carry the conversation, never the #host key | [unit] src/core/boot.test.ts::reclaimRuns::closes a host-keyed row under the metadata's thread…, src/core/dispatch/record.test.ts::assembleRunRecord — the handoff on the record::the reclaim's close files a host-keyed row under the metadata's thread, never the ledger's key column |
| 38: the resume message and the launcher's handle are built from the metadata's thread, never the ledger's key column | [unit] src/core/resumeLaunch.test.ts::the pure pieces::resumeMessage and resumeIoTarget name the metadata's thread for a host-keyed row, never the ledger's key column |
| 28: the object fences heartbeat, append, step, state, finishing and finish from another generation; heartbeat extends the lease and reports a stop; stop says whether the owner is live | [unit] deploy/cloudflare-memory/runLedger.test.ts::run ledger — the fence (item 28)::* |
30: the flusher — one timer per window, maxEvents sends at once, flush()/pending(), a failed send reported and dropped, close() refuses later pushes | [unit] src/core/runLedger/flusher.test.ts::createAppendFlusher::* |
30–31: append lands run_events rows by seq (the finished-runs routes do not see a live run), an over-cap event is 400, step records replace by step; the inbox increments seq from any generation; state replaces | [unit] deploy/cloudflare-memory/runLedger.test.ts::run ledger — steps, events, inbox, state (items 30–31)::* |
| 31: reclaim selection (expired and handoff, never a live lease, equal-now is expired); the phase CAS table; the transcript-completeness rule (resume, run-step-fresh, interrupted with the reason) | [unit] src/core/runLedger/decisions.test.ts::selectReclaim — expired leases and handed-off runs::*, ::phaseTransition — the CAS table::*, ::transcriptCompleteness — what a resume does with what it finds::* |
| 31, 33: finishing is a CAS taken once; finish writes the record and removes every live row in one step, the record lists as finished, the thread is free again, a mismatched record id is 400; handoff marks this generation's live runs; reclaim takes expired and handed-off rows with the last step and unconsumed inbox, re-owns them, leaves a live lease alone | [unit] deploy/cloudflare-memory/runLedger.test.ts::run ledger — finishing, finish, handoff, reclaim (items 31, 33)::* |
| 32: part rows carry idx/part and the part verbatim, attachments over the threshold by reference, small ones inline, an over-budget part refused by name; request chunking under the fence; assembly in (idx, part) order re-inflating references, naming a missing turn, part or attachment | [unit] src/core/runLedger/transcript.test.ts::* |
| 32: the transcript object — unknown-run before an owner, the owner writes and another generation is fenced, read in (idx, part) order with attachments, owner replaced by reclaim, clear empties everything, a 1.4 MB part row under the fence, malformed rows/attachments/ids 400 | [unit] deploy/cloudflare-memory/runTranscript.test.ts::run transcript object::* |
| 35: the mirror reports each step as pi's assistant turn lands, before the tools' results — the turns since the last report (seed + reports = the model's conversation), their first index, the calls in flight; the echoed seed is skipped | [unit] src/core/harness/pi/mirror.test.ts::PiMirror — step records as the runner writes them::* |
| 35: the write-through — the generation id; claim with prompt/tools/card/meta and the seed in the session log; a thread with another run's row, missing routes (warned once), a claim that kept failing → untracked; an older Memory codec refuses a large seed and detaches it while its finish still lands; a supported large seed retains the original stored part and owner; a ship claim without a seed | [unit] src/core/runLedger/writeThrough.test.ts::mintGeneration::*, ::open — claim and seed::*, ::open — claim and seed::a seed an older Memory codec refuses detaches the run — it stays claimed, so its finish still clears the row, ::open — claim and seed::a supported large seed stays tracked and stores the exact original part |
53: a run's rows land in its session log at seedFrom + their index, the row and the record carry session, the finish closes the range and clears nothing, a detach marks the record broken, an older row keeps its own object | [unit] src/core/runLedger/writeThrough.test.ts::the session log — a run is a range of it::*, src/core/runRecord.test.ts::isRunRecord — the session field::* |
53: a tool event's logIndex is the row the mirrored step wrote — logIndexOf is seedFrom plus the local index (the seed's rows, a step's, the row after a reused tail) and reads back the turn written there, undefined without a session and on the null run; the mirror names the calls' row and the results' row, a compaction moving the latter past its entry, a turn met again on a re-attach placed at the transcript's last row; the harness stamps every tool_call and tool_result through logIndexOf — two calls of one turn share a row, each result the next row — and none without it | [unit] src/core/runLedger/writeThrough.test.ts::the session log — a run is a range of it::logIndexOf names the log row a local index lands on…, src/core/harness/pi/mirror.test.ts::PiMirror — step records as the runner writes them::names the row the calls under way ride in…, src/core/harness/pi/harness.test.ts::runPiHarness — a run on pi from the first file to the answer::stamps every tool_call with the log row… |
53: the sessions registry and the enabled sweep — a claim registers the session, a finish stamps session_key and the finish time; when enabled, the sweep drops an emptied session's object owner-first and leaves a kept session and a live thread's sessions | [unit] deploy/cloudflare-memory/runLedger.test.ts::the sessions registry and the sweep's drop of a session log::*, src/core/runLedger/sessionLog.test.ts::sessionsToDrop — the sweep's decision, per candidate on what it re-reads at the drop::* |
53: a reclaimed row resumes from its session log from seedFrom, a row without session from its own object | [unit] src/core/runLedger/resume.test.ts::planResume::transcriptSource: a row with a session reads the log from its seedFrom… |
| 33: terminal commit retains at most 20 pending versions per exact physical workspace owner in the same ledger; capacity or a different terminal thread leaves live ownership and history unchanged; each obligation stays in the existing ledger independently of immediate history trimming or explicit record deletion; an unknown version changes nothing, duplicate exact ACK never removes a later ending, unacknowledged earlier versions survive later endings, and live ownership or incomplete binding/publication/proof refuses ACK | [unit] deploy/cloudflare-memory/runLedger.test.ts::exact owner evidence for resident preservation::retains the exact terminal workspace obligation through …, ::retains incomplete publication and refuses workspace acknowledgment under a live or mismatched owner; src/core/runLedger/inMemory.test.ts::resident workspace settlement::*, src/core/runLedgerWorker.test.ts::WorkerRunLedger::reads and acknowledges only an exact retained workspace version and rejects ambiguous responses, deploy/cloudflare-memory/runLedger.test.ts::exact owner evidence for resident preservation::keeps corrupt retained evidence under acknowledgment and refuses invalid owner inputs, ::retains invalid binding and saved proof as unverified and checks the owner stored under an acknowledgment key, ::keeps each unverified terminal version when a later segment completes and refuses a different terminal thread |
| 54: saved resident workspace bindings carry only the acknowledged successful attach generation and fence; a different response fence refuses without cold fallback, every successful reattach serializes and awaits its owner-fenced binding write before commands or publication, failed writes and rejected successful responses latch admission, the initial publication base survives automatic reattachment before promotion, and missing new physical identity never borrows an earlier resident acknowledgment | [unit] src/execution/resident.test.ts::ResidentExecutor.open (attach-on-open)::retains the acknowledged attachment fence and refuses a different fence before commands, ::does not invent an acknowledged attachment fence from the request, ::awaits the current successful attachment binding and replaces an earlier fence on a later attach; src/execution/factory.test.ts::the workspace binding on the row::carries the actual attachment fence through save and resume without borrowing a later claim, src/execution/factory.test.ts::makeExecutor resident selection::a mismatched acknowledged attachment fence never becomes a cold fallback; src/core/dispatch/provision.test.ts::reattachWorkspace — the run's recorded workspace re-attached without the gate::persists each acknowledged resident attachment before continuation and removes an absent physical fence; src/core/dispatch/readyBinding.test.ts::resumed pilot binding commit::does not borrow an earlier resident incarnation or attachment fence when the new response omits them; deploy/cloudflare-resident/runRegistration.test.ts::exact-thread owner reconciliation::repairs the exact-thread ledger, attaches a new owner without touching private bytes, then fences detach, src/execution/resident.test.ts::ResidentExecutor.open (attach-on-open)::holds concurrent commands behind attachment acknowledgment and latches a failed write, ::holds publication while an operation's wake reattachment awaits acknowledgment, ::latches a server registration refusal before later commands, ::blocks commands after a successful physical attach whose binding acknowledgment failed, ::latches an unacknowledged successful reattachment response: …, src/execution/factory.test.ts::the workspace binding on the row::uses the latest physical attachment while preserving the opening publication base through promotion |
| 54: the binding for a selection names the backend and one known checkout path from either a resident attach or a sandbox seed, plus a resident pool user and container; a seeded sandbox also records its repository and ref, and a re-attach reads the actual origin, ref and full head before restoring seed facts; foreign, missing or unreadable checkouts refuse; a run without a backend records nothing, and the row's binding is read back only in this build's shape | [unit] src/execution/factory.test.ts::the workspace binding on the row::*; src/execution/factory.test.ts::makeExecutor resident selection::on a resume (ctx.reattach): re-attach, never re-provision::a seeded sandbox re-attach* |
| 54: a resume reads the state's binding, a row from before it its meta's word (resident with the worktree, per-thread bare), and a run without a workspace nothing | [unit] src/core/dispatch/reattach.test.ts::carriedWorkspaceBinding…::* |
| 54: a lost workspace publishes the resumed note, closes the card, closes the adopted row interrupted with the note in its record, and hands back the row's request; an unreadable request hands nothing back and the note says the run ends | [unit] src/core/dispatch/reattach.test.ts::abandonLostWorkspace…::* |
| 54: the restart turn keeps the request as the row carried it with a fresh receipt and its own root, the unconsumed follow-ups appended and their attachments merged after the request's own | [unit] src/core/dispatch/reattach.test.ts::prepareRestartTurn…::* |
| 54: the factory re-attaches a recorded resident in reuse-only mode and provisions nothing on any refusal; a recorded per-thread backend makes zero resident calls | [unit] src/execution/factory.test.ts::makeExecutor resident selection::on a resume (ctx.reattach): re-attach, never re-provision::* |
54: end to end, the claim records the binding on the row's state; a resume re-attaches its recorded resident worktree with reuse: true, continues the transcript and completes the binding; a refused re-attach of a legacy binding without resident address metadata provisions no sandbox, closes the resumed row interrupted with the resumed note, and the request runs again under the same run id and completes on the resident — one row on the index, the one record ending completed with the replacement's user-words note beside the refusal's | [unit] src/core/dispatcher.test.ts::run ledger write-through …::the claim records the workspace binding on the row's state…, ::a resume re-attaches its recorded resident worktree in reuse-only mode…, ::a resume whose recorded resident refuses to reuse the worktree provisions no sandbox…, src/core/dispatch/reattach.test.ts::prepareRestartTurn: the request runs again as its own dispatch::carries the coordinator tag the interrupted run had…, src/core/dispatch/runLoop.test.ts::the pi harness — the container replaced under a living bot: the relaunch ceiling::a coordinator's child that restarts from its request hands the dispatcher its coordinator tag… |
| 54: an explicit resident address cannot be dropped into an automatic restart after reattachment fails; malformed saved metadata or a changed/missing expected reply fails setup and closes the ordinary adopted row interrupted without fresh provisioning. Pilot preservation remains first. | [unit] src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::an ordinary resume with a %s resident address fails setup without restarting on the primary |
54: a restart keeps its predecessor's id, token and replayed events only when the identity rode the dispatch (restartCarried); restartOf alone — the registry row already gone — mints a fresh id, so an eventless restart never upserts over the predecessor's record | [unit] src/core/dispatch/provision.test.ts::registerRun — the run's row on every surface before the attach::a restart keeps the run's id, token and events only with restartCarried; restartOf alone mints a fresh id |
54: the re-attach of a recorded binding is callable mid-run without the gate — no message, no card, no refusal wrap — and hands back the round the dispatch-time attach hands back for the same binding, the factory handed the same round input under the attach span (less the card's setup-note sink, which only the gate path carries — it has the card); an address-free legacy re-attach refusal is reattach_refused with the factory's why on both paths, no card closed and no reply | [unit] src/core/dispatch/provision.test.ts::reattachWorkspace — the run's recorded workspace re-attached without the gate::* |
| 54: the dispatch-time attach still runs the gate: no ref bound and none named is the ask-once refusal on the card and in the thread, no model turn | [unit] src/core/dispatch/provision.test.ts::attachWorkspace — the workspace attach and the ask-once refusal::no ref bound and none named… |
54: outside a pilot writer's recorded preservation policy, an interruption mid-run is the loop's outcome — a relaunch refused by name (a legacy worktree, the ceiling) answers interrupted with the reason, the refusal, the note and the request to run again, the run finished and its card closed by the loop; the dispatcher reads it and dispatches the request again under the same run id exactly once; a container replaced under a living bot is a relaunch first, through this item's re-attach, the roll order included (a foreign row never reaches the loop: the row's own word picks its harness, harness.md item 8, and the seam's door refuses one for any other caller) | [unit] src/core/dispatch/runLoop.test.ts::the pi harness — the container replaced under a living bot: the relaunch ceiling::*, 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…, ::a relaunch whose re-attach the resident refuses…, ::the roll order every deploy has… |
| 54: a pilot writer whose live relaunch cannot re-attach its original checkout pauses with the same run id and durable binding, does not finish/release/restart, and a later generation holds the row again even if pilot config changed | [unit] src/core/dispatch/runLoop.test.ts::the pi harness — the container replaced under a living bot: the relaunch ceiling::pauses a coding run on mid-run reattach refusal without finishing or releasing its recorded worktree; src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::a pilot writer paused by mid-run reattach refusal keeps its original ledger row and checkout |
| 54: a stamped pilot writer that reaches the relaunch ceiling or another named relaunch refusal pauses the original run without finishing, releasing its checkout or restarting from the request; ordinary runs still interrupt and restart | [unit] src/core/dispatch/relaunch.test.ts::prepareRelaunch — the relaunch decided and prepared::pauses a pilot at the relaunch ceiling without releasing or replacing its dirty checkout, ::pauses a pilot on other relaunch refusals instead of restarting its dirty checkout; src/core/dispatch/runLoop.test.ts::the pi harness — the container replaced under a living bot: the relaunch ceiling::pauses a pilot at the relaunch ceiling without finishing or releasing the run, ::the third finding closes the run interrupted naming the bound… |
| 54: a stamped pilot resume with a missing or invalid saved workspace binding pauses the original ledger run before any fresh attach, model turn or replacement workspace; the next generation can reclaim that same run for operator repair | [unit] src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::a resumed pilot with no recoverable workspace binding pauses its original run before provisioning; src/core/dispatch/reattach.test.ts::hasPilotWorkspaceBinding::* |
| 54: after a successful reuse-only reattach, a later setup failure still pauses the stamped pilot's original row and dirty checkout; it does not release, finish or start a replacement, and the next generation can reclaim it | [unit] src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::a resumed pilot keeps its dirty checkout when setup fails after successful reattach |
| 54: a restart whose reservation would meet the closed run's own row still live — its finish in flight from the same process, under the SAME run id — awaits that finish before claiming, then claims the id fresh: the order is the predecessor's reserve and promotion, its interrupted finish, the restart's reserve and promotion, the run's own finish, all under the one id, with the record completed and no untracked note; the fresh turn for a closed run's unconsumed follow-ups takes the thread-live wait and is tracked | [unit] src/core/dispatcher.test.ts::run ledger write-through …::a restart whose reservation would meet the closed run's own row still live…, ::the fresh turn for a closed run's unconsumed follow-ups… |
54: the write-through's wait is the named run's in-flight finish promise alone, no timer scheduled — landed, the reservation claims again and is tracked with no warning and no note; the finish fails for good (the history writer's last attempt), the claim is made once more the moment the writer gives up, the run goes on untracked by name and onUntracked carries why; the finish landed in the moment between the thread-live answer and the read (landed), the re-claim still happens and tracks the run; the name stays through the history writer's retry backoff and a claim arriving then waits for the retry that lands; a one-shot closer's failed put keeps the name until its abandoned, and a row whose finish is neither in flight nor landed here is untracked in one claim | [unit] src/core/runLedger/writeThrough.test.ts::reserve — the row before the prompt (item 42)::a reservation that meets the row of a run whose finish is in flight…, ::a reservation whose awaited finish fails…, ::a reservation whose awaited finish lands in the moment between…, ::the name stays through the history writer's retry backoff… |
54: the writer's final word settles the name — abandoned once at each give-up (a missing route, a permanent failure, the attempts spent), never after a put that landed; a finish whose route is missing on both the ledger and the store settles the moment the writer gives up, so the claim awaiting it is not parked and the run goes on untracked with the why; the word is required on every sink — a wordless sink does not compile (@ts-expect-error, a compile-time proof) and a store answers it no-op | [unit] src/core/runHistoryWriter.test.ts::createRunHistoryWriter::tells a sink \abandoned` once at each give-up…, ::a sink without the final word does not compile…, src/core/runLedger/writeThrough.test.ts::reserve — the row before the prompt (item 42)::a finish the writer abandons settles the name…` |
54: landed remembers the newest LANDED_MAX finishes, oldest out — a thread-live naming an older one is untracked in one claim, one naming the newest is claimed once more, and the note then says the finish landed here and the row stands past it | [unit] src/core/runLedger/writeThrough.test.ts::reserve — the row before the prompt (item 42)::\landed` remembers the newest LANDED_MAX finishes…` |
54: a rejection that escapes the writer's stop rules (the injected sleep throwing) still says abandoned from write()'s defensive catch; a one-shot closer whose put fails says the word once through putOnce, and a record that cannot be assembled is caught too — the closer stays best-effort | [unit] src/core/runHistoryWriter.test.ts::createRunHistoryWriter::a rejection that escapes the give-up rules…, src/core/dispatch/admission.test.ts::closeResumedRow / closeRestartRow — one attempt, the final word said (item 54)::* |
| 54: a claim that meets this generation's own dead reservation — its abandon failed transiently — abandons it again and claims once more, tracked; an abandon that keeps failing leaves the run untracked with that why; a transient setup finish is retried before the next turn claims | [unit] src/core/runLedger/writeThrough.test.ts::reserve — the row before the prompt (item 42)::a claim that meets this generation's own dead reservation…, src/core/dispatcher.test.ts::run ledger write-through …::a setup finish retry releases the original row before a queued follow-up claims |
54: every untracked exit of a reservation says why through onUntracked — no run-ledger routes, a claim that kept failing, another run's row whose finish is not in flight here, and the waited-for finish that did not land | [unit] src/core/runLedger/writeThrough.test.ts::reserve — the row before the prompt (item 42)::every untracked exit says why…, ::a reservation whose awaited finish fails… |
54: end to end, a restart whose predecessor's finish keeps failing awaits its settling and then keeps the run's own standing row — the same id, the owner's idempotent re-claim — tracked with no ledger_untracked note, its record reaching the plain store through the writer's give-up fallback when the ledger finish fails again; a failed setup finishes its own row before the next turn claims | [unit] src/core/dispatcher.test.ts::run ledger write-through …::a restart whose predecessor's finish keeps failing…, ::a restart whose reservation would meet the closed run's own row still live…, ::a setup failure finishes the advertised run before a queued follow-up claims the thread |
54 live (human-gated): with a coding run on pi mid-edit on a resident, deploy restart; the new generation's [reclaim] line, the resident log's attach for the thread with reuse and no worktree-clean, the run's resumed note ("pi still runs in the container") and the edit still present in the worktree and in the run's eventual PR. Then, with another run in flight, remove its worktree on the resident between the drain and the reclaim: the resident log shows the 409 reuse-refused, the old run's record ends interrupted with the resumed note naming the refusal and no cold_sandbox note, and a new run in the thread carries the request through | [agent] post-deploy, on a test repo's resident. |
| 35: steps — turns first then a record numbered from 1 with the registry seq, the turn index after the write and the calls in flight; a fenced step detaches without throwing; one retry after a backoff on a transient failure, none on a permanent one | [unit] src/core/runLedger/writeThrough.test.ts::step — turns first, then the record::* |
| Completed model context is acknowledged before any normal producer teardown. A failed checkpoint retains the original producer for recovery; confirmed endings survive restart and cannot resurrect or authorize a different producer. | [unit] src/core/dispatch/runLoop.test.ts::the pi harness — every preset's runs, in the run's container::retains a completed producer while its final context checkpoint is unavailable; src/core/runLedger/checkpointState.test.ts::original session policy custody::keeps a confirmed ending bound to its producer and refuses stale resurrection; src/core/harness/contract.test.ts::durable launch intent::starts a successor only after the exact… |
35: a final answer or review verdict retries one unavailable checkpoint state acknowledgment on the same owned run. If the answer's transcript is durable but the state acknowledgment remains unavailable, the run retains its live row for a later generation to finish from the saved turns; no answer or review is published before recovery. The adopted run rebuilds a missing cursor from the verified whole transcript. The domain reader restores only the accepted result of the latest review submission. A later rejected or uncertain submission leaves earlier accepted facts in the transcript, without a recoverable current verdict. The run loop still retains a stored verdict when no accepted transcript verdict exists; [gap] finalization must distinguish that historical value from an incomplete current review before publication. A missing or broken cursor, ownership fence, permanent state refusal or unknown state error still fails closed. The checkpoint_deferred event is written when the event ledger is available; the write-through warning carries a run-scoped category without transcript text. | [unit] src/core/dispatch/runLoop.test.ts::a resume with the answer in hand (the \finish` plan)::keeps a %s answer behind its final context checkpoint (persistent failure: %s); src/core/reviewVerdict.test.ts::recordedVerdictFromTranscript::restores only an accepted tool result, never a bare or failed verdict call, ::a later rejected verdict leaves recovery incomplete; src/core/runLedger/writeThrough.test.ts::open — claim and seed::logs a sanitized checkpoint refusal category for state timeouts and detached runs, ::keeps a %s state refusal out of the recoverable timeout category` |
| 35: events batched with the registry seq and nothing after the finish; state merged and coalesced, a transient state failure retried after a backoff and left dirty for the next patch when the retry fails too; the heartbeat extends the lease and relays a stop once per mode; a refused heartbeat detaches and stops the timer | [unit] src/core/runLedger/writeThrough.test.ts::events, state, heartbeat::* |
| A state commit succeeds only after an acknowledged snapshot contains its patch, even if a later unrelated patch is dirty or fails; coalesced commits can share one acknowledgment. Failed writes never acknowledge a later patch, and ownership fences, detach and finish still refuse the commit. | [unit] src/core/runLedger/writeThrough.test.ts::events, state, heartbeat::a later setState cannot revoke an acknowledged state commit, ::two concurrent state commits resolve against the snapshot that contains each patch, ::coalesced state commits share one acknowledged snapshot, ::an acknowledged state commit survives a later failed write without crediting it, ::a state commit waits for acknowledgment and cannot outlive a fence or finish |
| 35: finishing is taken once; the sink's finish replaces the live rows with the record (events flushed first) and never touches the fallback; a refused finish or missing route goes to the fallback store; a transient finish failure propagates for the writer's retry, which is idempotent | [unit] src/core/runLedger/writeThrough.test.ts::finishing and finish::* |
35: the writer routes one record through via with the same retries and accounting, the store untouched for it and used again for the next write | [unit] src/core/runHistoryWriter.test.ts::createRunHistoryWriter::\via` routes one record to another sink with the same retries and accounting; the store never sees it, and the next write uses the store again` |
35: end to end in the dispatcher — the claim carries the system prompt, tool definitions only, the card handle and the meta; the seed equals the first model call's conversation; the step record precedes the tools and names both calls; the checklist lands in state; finishing is set when the reply goes out; the finish empties the live rows, lands the record in the ledger with every event in seq order, and the fallback is never used | [unit] src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::claims the run once its prompt exists (system, tools, card, meta, seed), records each step before its tools, appends events, takes finishing before the reply and finishes through the ledger |
| 38: a resumed run whose row carries the route repaints the route note (at debug, routing-and-config item 28) from the first frame; the router is not asked again; no second route event | [unit] src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::a resumed run whose row carries the route repaints the route note at debug from the first frame (items 38 and routing-and-config 21); the router is not asked again and no second route event is published |
21/42: a restart of a historical routed row reuses its stored preset and repaints the reason without any model call or new route event | [unit] src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::a restarted historical route reuses the row's preset and repaints its reason without a model call or new route event |
| 35: an untracked run (another run's row on the thread) runs and replies as before, its record reaches the store, one warning; a review's verdict lands in the ledger state | [unit] src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::a thread whose ledger row belongs to another run leaves this run untracked: it runs and replies as before, its record goes to the store, one warning, ::a review's verdict lands in the run's ledger state as it is submitted |
35: a ship request is claimed without a seed and a taken hand-off keeps the row live with state.hosting, nothing finished through the ledger | [unit] src/core/dispatcher.test.ts::agent:ship (the hand-off to the plan runner)::the ship request is claimed on the run ledger without a seed (item 35) and the hand-off keeps the run live… |
36: reclaim at boot — a run the completeness rule refuses closes interrupted with a record from the ledger's events, meta and identity (row, steps, transcript gone; the verdict is the reason); a finishing row closes with its recorded finalStatus or completed; no step record and a handoff are named; a partial step write is named; rows another generation holds are left alone and returned as liveElsewhere; a complete listing returns every hosted instance for ownership recovery, including current-generation rows whose classification failed; a row this generation owns is never taken however stale its lease; a closure carries the note its card and (for a pipeline) its thread get, naming the PR the events recorded; missing routes, an unreachable ledger or a failed live listing are warned and marked incomplete so recovery guards remain fenced, and a later complete empty listing is still reported; one run's failure is reported and does not stop the others | [unit] src/core/boot.test.ts::reclaimRuns::*, src/core/runLedger/decisions.test.ts::selectReclaim — expired leases and handed-off runs::*, src/core/runLedger/inMemory.test.ts::InMemoryRunLedger::reclaim takes the expired and handed-off runs, gives them to the new generation with the last step, the unconsumed inbox and the jobs, and re-fences the transcript, deploy/cloudflare-memory/runLedger.test.ts::run ledger — finishing, finish, handoff, reclaim (items 31, 33)::handoff marks this generation's live runs; reclaim takes expired and handed-off rows with the last step, the unconsumed inbox and jobs, and re-owns them; a live lease is left alone |
| 36/38: one reclaim admission owns a hosted ship parent and every child row together: a live Workflow, a child another generation still owns or a child this admission will resume keeps the parent resumable after its old deadline, so no interrupted or re-issue notice is emitted while work is live; a child that closes in this admission does not; the ship branch durably records the resolved display level even though its production input event strips the verbosity directive, and the launcher rehosts the parent beside the resumed child and rewrites the existing card once from ledger facts, naming the unit, pull request, round step and child at that quiet, verbose or debug level | [unit] src/core/boot.test.ts::reclaimRuns — the hosted parent's classification (record 0060)::*, src/core/dispatch/ship.test.ts::runShipBranch — the host key and the hosted marker (record 0060)::durably records a request-level verbosity override after the production input event strips its directive, src/core/resumeLaunch.test.ts::launchResumes — the rehost branch (record 0060)::rehosts the restart card at the durably recorded $level display level |
| 36: the record a reclaim closes with carries the row's identity, meta and every appended event; the live-events read answers a live run's rows in seq order, an unknown run empty, a bad id 400; a reclaim says which phase each row came from | [unit] src/core/boot.test.ts::reclaimRuns::closes a run the rule refuses (a partial step write) \interrupted` with a record built from the ledger's events, meta and identity; the row, steps and transcript go; the reason is the verdict, deploy/cloudflare-memory/runLedger.test.ts::run ledger — steps, events, inbox, state (items 30–31)::, src/core/runLedger/inMemory.test.ts::InMemoryRunLedger::` |
36: the finish reaches the ledger before the workspace release completes, and finalStatus is on the row when finishing is taken | [unit] src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::the finish reaches the ledger BEFORE the workspace release completes (item 36), and the final status rides on the row before finishing: a slow sandbox teardown never keeps the thread's row live after the reply |
| 36: ordinary answered-run release waits for the exact run's confirmed terminal ledger write after the reply, including transient retries; a refused or failed finish retains the workspace even when the plain-store fallback succeeds; a successor owner remains intact; hard stops tear down immediately, including when requested during the terminal-write wait | [unit] src/core/dispatch/runLoop.test.ts::runLoop — the model turn and everything that rides on it::ordinary review release waits for its exact terminal ledger write… |
| 36: the release wait observes only that run's existing finish outcome, retained after settlement; an unstarted finish confirms nothing, and a disabled ledger remains explicitly off | [unit] src/core/runLedger/writeThrough.test.ts::finishing and finish::waits only for this run's finish and retains its outcome after settlement |
36: POST /admin/crash — a deploy:write bearer gets 202 with the generation and the hard exit is deferred past the response; 401/403/503 exit nothing; only POST | [unit] src/channels/adminCrash.test.ts::POST /admin/crash::* |
36: the sweep's isLive predicate — a card driven here or marked live on the ledger is never an orphan; the foreign set is refreshed from its source on every scan (a dead generation loses its hold, a failed refresh keeps the set); a replied run's card is closed with how it ended, an interrupted run's card is closed with its closure note, a failed edit is isolated | [unit] src/channels/slackCatchUp.test.ts::findOrphanedCards (pure selection of the bot's own frozen live cards)::*, src/channels/slack/statusCard.test.ts::live cards::* |
37: the settlement rule — bash, pi's write and GitHub writes get the restart result, reads/update_status/submit_* are rerun, an unknown tool gets the not-available result; the plan for resume (settlements from the last turn, counters carried, nothing in flight → continue from a user turn), run-step-fresh (all calls fresh, counters advanced, update_status-only keeps the turn), and every interrupted reason | [unit] src/core/runLedger/resume.test.ts::settlementFor — D4::*, ::planResume::* |
37: the harness's entry — the calls in flight at the kill are answered with the restart note and never re-run, on the rebuilt session a restarted pi continues from and on the relay a surviving pi's re-ask reads; the resumed note names what happened | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — after a bot restart::the explicit replacement protocol rebuilds the transcript and settles calls in flight before continuing, ::re-attaches to a pi still 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, so…, src/core/harness/pi/harness.test.ts::the small pure pieces::settlementResults answers every call in flight with the restart note and nothing for none |
| 37: the registry creates a run under a given id with its original start and earlier events replayed under their seqs and the counter continuing past the highest; the write-through adopts a reclaimed row without claim or seed — heartbeat at once, steps and seqs continuing, state merging | [unit] src/core/runRegistry.test.ts::RunRegistry.create::a resume creates the run under a given id, with its original start and its earlier events replayed under their seqs; new events continue past the highest, src/core/runLedger/writeThrough.test.ts::adopt — a reclaimed run continues under this generation (item 37)::* |
| 39: finishing is a tri-state gate — ok once, a refusal is fenced (and detaches), a run detached by a fence keeps answering fenced, one detached otherwise or an unreachable ledger is unavailable; a fenced write tells the run once (onFenced); liveRuns names the runs driven here; handoff marks the resumable ones (a hosted ship parent included — record 0060; not a seedless claim without the marker, not a detached run), remembers it, and reports a ledger failure instead of throwing | [unit] src/core/runLedger/writeThrough.test.ts::finishing and finish::* |
| 39: a fenced finishing means no reply, no card close and no record from this generation | [unit] src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::a fenced finishing means another generation owns the run: nothing more reaches the thread and no record is written from here — the run is theirs (D9) |
39: live — a forced deploy restart (or a forced rollout) with a resumable run in flight: the old container logs [drain] handed N run(s) to the next generation and exits within seconds; the new one resumes the run (item 38's receipt) | [agent] With a slow run in flight, switchboard deploy restart --force (the unforced preflight refuses for runs in flight and fails by name past its budget — slack-channel item 8); time from SIGTERM to the new generation on /healthz, then the item 38 checks. |
| 38: the reclaim hands a resumable row back untouched (row ours, record, transcript, events kept) and closes the rest; a handed-off row with a whole transcript is resumable; the sweep repeats every lease interval, reports only non-empty outcomes, runs one pass at a time, and a failing pass is a warning | [unit] src/core/boot.test.ts::reclaimRuns::* |
38: the launcher — the agent's static tools as known tools, the request text from the input event, the message pinning the preset and identity while model and effort resolve from the configuration in force for this fresh segment, the repo context from the meta; a resumable run is dispatched with the full ResumeContext; interrupted plans, unknown agents and unresumable channels are closed with their reason; a throwing dispatch is logged and the others still launch; onDone is told once per run when its resume is over — a dispatch that resolved or threw, or a run closed instead — never before | [unit] src/core/resumeLaunch.test.ts::the pure pieces::*, ::launchResumes::* |
| 38: end to end — a reclaimed run resumes under its own id on the current configured model: the row adopted, the transcript the conversation, the calls in flight settled before the first model call (a known tool re-runs and refreshes the row's state, an unknown one gets the not-available result), the earlier events replayed under their seqs with no second input event, only this generation's events appended, the finish closing the same row | [unit] src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::a reclaimed run resumes under its own id (item 38): the row is adopted at admission, the transcript is the conversation, the calls in flight are settled before the first model call, the earlier events are replayed under their seqs, and the finish closes the same row |
| 38: a resume onto a thread with a newer run closes the reclaimed row interrupted with no reply; a resumed dispatch that ends before its run starts closes its adopted row, except an opted-in pilot writer whose failed readiness or reuse-only reattach check hands off the same row and recorded workspace binding for the next generation | [unit] src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::a resume onto a thread that has a newer run in flight is never steered or refused as a follow-up: the reclaimed row is closed interrupted with no reply and no run, ::a resumed dispatch that ends before its run starts (a repo refusal) closes the adopted row interrupted instead of leaving it for the sweep to relaunch forever |
38: the Slack IO of a resumed run edits the existing card instead of posting one (a deleted card falls back to a fresh one), and resumeSlackIO builds the thread IO from the row's parts | [unit] src/channels/slack.test.ts::SlackIO.status on a resumed run (existing card)::* |
| 38/42: a rebuilt Slack handle for a continuing run or child keeps the parent turn and its attachment; a request restarted during workspace attach skips its own message from history because dispatch appends it as the current turn. The launcher passes the restored request to the channel handle. | [unit] src/channels/slack.test.ts::SlackIO.history — thread reuse and concurrent attachment downloads::a rebuilt handle keeps the parent message and its plan attachment for a coding child, ::a rebuilt handle for a restarted request skips that request but keeps the earlier plan, src/core/resumeLaunch.test.ts::launchResumes::a restart (item 42) dispatches the row's own request… |
37: the run tools settle by the same rule — await_runs runs again, send_to_run and spawn_run get the restart result | [unit] src/core/runLedger/resume.test.ts::settlementFor — D4::the run tools settle by the same rule… |
37: the finish plan: a transcript ending on a text-only assistant turn with nothing in flight is the model's answer, whether the final turn's record landed or only its turns did; an assistant turn with calls the record does not know stays interrupted; the launcher dispatches it and says so | [unit] src/core/runLedger/resume.test.ts::planResume::finish: a transcript ending on the model's final answer…, ::planResume::interrupted: no step record…, src/core/resumeLaunch.test.ts::launchResumes::a run whose transcript ends on its final answer with nothing in flight is dispatched with a … |
37: how the loop ended is read from the notes: a soft stopped note, the write-up notes with their summary, the last one wins, a hard stop or no note is a plain answer; a posted verdict is read off review_posted | [unit] src/core/runLedger/resume.test.ts::planResume::loopEndingOf reads how the model's loop ended from the run's notes…, ::planResume::reviewPostedBefore: the last … |
37: the run loop's finish entry: no model call, the transcript's answer published under the ending's status and label (⏹ and stopped_soft after a soft stop, the ⚠️ budget label after a budget), a resumed note, the review post-steps with the answer in hand, never a second post after exact original canonical acceptance; display events alone provide no publication credit, the leftover pi ended at its recorded pid and root | [unit] src/core/dispatch/runLoop.test.ts::a resume with the answer in hand …::* |
37: end to end: a reclaimed run whose transcript ends on its answer finishes through dispatch() with the answer in the thread and the row closed, never interrupted | [unit] src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::a reclaimed run whose transcript ends on the model's final answer finishes instead of closing interrupted… |
| 40: the step record carries the inbox sequence the run consumed. A valid append returns its sequence. Only explicit target absence returns undefined; storage failure, invalid messages and unqualified rejections throw. A null ledger represents disabled durability | [unit] src/core/runLedger/writeThrough.test.ts::finishing and finish::the step record carries the inbox seq the run has consumed; pushInbox distinguishes delivery, target absence and storage failure |
| 40: provider-up writes the durable row and offered steer atomically; failed transaction writes neither, failed push preserves both; a pushed steer atomically fences and renews authoritative generation ownership before registry delivery, and only an owner-fenced ack closes it; stale push then owner-heartbeat delivery and duplicate push/reclaim are sequence-idempotent | [unit] deploy/cloudflare-memory/runLedger.test.ts::the plane's checkpoint steers and the provider condition — the heartbeat body, /plane/park, /plane/level provider (record 0064)::a provider down and parked live run recover atomically as one durable row and one pushed offered steer; repeat up writes nothing, ::fences a pushed steer and renews its owner's lease atomically before registry delivery, ::a stale generation cannot close a steer offer after reclaim; the durable owner heartbeat receives and closes it, ::a failed provider-up transaction writes neither the durable row nor its steer effect, ::a failed live push leaves both the durable row and steer offer for an owner heartbeat; sealing first removes the park and produces no steer, src/channels/planeEffects.test.ts::POST /plane/effects — the push transport (record 0064)::the stale generation defers a pushed steer, then the durable owner heartbeat delivers and acks it, ::renews the durable owner fence before registry delivery, so reclaim cannot cross the local wake, ::rechecks drain after the durable fence returns, so handoff cannot wake stale registry state, src/core/runLedgerWorker.test.ts::WorkerRunLedger::planeFenceSteer asks the object to atomically renew ownership before local delivery, ::planeAck carries the authoritative owner fence for a steer, src/core/runLedger/writeThrough.test.ts::events, state, heartbeat::an owner heartbeat delivers a live steer once; duplicate heartbeat and reclaim copies of its durable sequence are skipped |
40: readInbox is the inbox past a seq — the same slice the reclaim hands over, on demand; empty for an unknown run; the Worker route validates afterSeq; the client posts it | [unit] src/core/runLedger/inMemory.test.ts::* (readInbox), src/core/runLedgerWorker.test.ts::WorkerRunLedger::readInbox posts the run id and the seq to read past, and returns the Worker's items (item 40); [workerd] deploy/cloudflare-memory/runLedger.test.ts::* (/runs/inbox/read) |
| 40: a follow-up the cursor passed unread survives a process restart — one drain hands pi an ordinary steer and the plane's provider-up row, the reissue prompt's echo moves the cursor past the steer, the step names it deferred, the reclaim offers it and not the consumed provider-up row, and the next generation steers it into pi; both ledgers reclaim past-cursor plus deferred rows and nothing else the cursor passed; the Worker refuses a malformed deferred list | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — a run on pi from the first file to the answer::an ordinary follow-up the cursor passed beside a consumed provider-up row survives a process restart: the reclaim offers it and the next generation steers it into pi, src/core/runLedger/inMemory.test.ts::* (reclaim offers the deferred rows at or below the cursor with every row past it, and nothing else the cursor passed); [workerd] deploy/cloudflare-memory/runLedger.test.ts::* (reclaim offers the deferred rows at or below the cursor with every row past it; a malformed deferred list is refused) |
| 40: provider-up rows buffered during a post-loop follow-up's winning local retry or successful reissue are checkpointed alongside the completed post-turn transcript; after reclaim every provider-up row stays consumed while an ordinary follow-up beside one remains deferred and is offered | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — a run on pi from the first file to the answer::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 temporary failure during a follow-up turn parks and resumes on provider-up without ending the run |
| 37: ancillary description and verdict steps acknowledge the selected substantive report before their transcript rows can land. A selection-bearing step whose next transcript lacks its record is refused; ordinary substantive fresh-step recovery keeps its existing order. Substantive re-review and answer correction select their own completed text. Recovery carries an ancillary selection through in-flight calls; an uncommitted selection is refused rather than guessed. | [unit] src/core/runLedger/resume.test.ts::planResume::*, src/core/runLedgerWorker.test.ts::WorkerRunLedger::an acknowledged first ancillary marker prevents publishing a split-write acknowledgement…, src/core/harness/pi/harness.test.ts::runPiHarnessOpen — the session stays open for one more turn::holds the first ancillary prompt until its selection is acknowledged, src/core/harness/opencode/harness.test.ts::hosted Review originating lifecycle::holds the first ancillary prompt until its selection is acknowledged, 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… |
| 40: the resume plan carries the last record's inbox seq | [unit] src/core/runLedger/resume.test.ts::* (the inboxConsumedSeq: 7 assertions) |
| 40: a staged file's reference rides the durable row and reads back as the same reference; it survives when the inline attachments are dropped over the cap; a malformed entry is dropped, never fatal | [unit] src/core/runLedger/inboxMessage.test.ts::durable inbox — staged references (record 0033)::* |
| 40: the reclaim hands a resumable run its inbox past the last record; a row live elsewhere names its thread | [unit] src/core/boot.test.ts::* (r.inbox, liveElsewhere shape) |
| 40: the launcher passes the inbox through to the ResumeContext | [unit] src/core/resumeLaunch.test.ts::launchResumes::plans and dispatches a resumable run with the full ResumeContext — the plan, the row, the last step, the events, the highest seq, the repo context — under the row's identity |
| 41: a checkpoint-held ledger run reports its pending recovery, with or without a confirmed pause handoff; its named steer retains the original inbox owner and reports the hold until a recorded resume | [unit] src/core/dispatch/admission.test.ts::createSteerSender — the wired sender behind \steer.run` (the one-door plan's admission unit)::reports a checkpoint hold after saving a named steer (handoff confirmed: %s)` |
41: a ledger row not in the registry lists as a live row under all/active, never finished, with its meta, events, activity and ownerGen; its tombstone never surfaces; a row also in the registry lists once (registry); the predicate applies; getRun/getRunEvents/getRunFriction/stopRun answer for it; a ledger-list failure is one warning and the registry/store fallback with ledgerUnavailable on every status and cursor page (never a completeness proof); a later successful retry clears the marker; liveElsewhere is the slice for the index | [unit] src/core/runsService.test.ts::RunsService with the run ledger — one registry across generations (run-history item 41)::* |
41a: a hard stop of this bot generation's paused, handed-off child seals its ledger row and record as stopped_hard promptly, frees its thread/session owner without touching the workspace or branch, and is idempotent with a parent stop; reclaim cannot replay its paused publication. The hard stop stays durable when it races the retry handoff while the child remains in the registry, including through the token-gated run page. The durable stop write precedes the local hard-abort signal; a failed write returns unavailable without aborting that control or acknowledging a lost stop. A pause already sealed by a concurrent stop does not remain a thread owner, including when the pause-side seal is fenced or its first read fails. An ordinary live row and a foreign generation's row retain their stop-signalling behavior; a failed guarded seal retains durable stop intent. | [unit] src/core/runsService.test.ts::RunsService with the run ledger — one registry across generations (run-history item 41)::a hard stop seals a same-generation paused handoff without replaying publication, ::persists a local hard stop before pause and seals a pause still in the registry, ::reports an unavailable local hard stop until its ledger write succeeds; [unit] src/core/runLedger/writeThrough.test.ts::seals a hard stop that lands before the retry pause hands off, ::does not retain a retry pause already sealed by a concurrent stop, ::confirms row ownership after a concurrent finish fences the pause-side seal, ::does not infer a retained pause when its seal read fails after another stop closed the row |
| 41: one ledger listing serves every read within the TTL; the rows' events are read in parallel; a failed listing is not kept | [unit] src/core/runsService.test.ts::RunsService with the run ledger — one registry across generations (run-history item 41)::one ledger listing serves every read within the TTL — a page view's run, events and friction reads cost one listLive; the events of several rows are read in parallel; a failed listing is not kept |
| 41: a run live elsewhere opens tokenless in live mode with a server clock, an explicit EventSource follow that tracks ledger events until a terminal receipt, finite default snapshot replays for work folds, friction from the live diagnosis and stop controls through the ledger | [unit] src/channels/liveView.test.ts::live view on RunsService: history pages + index toggle …::index: active by default, everything with ?all=1::a run live on the ledger under another generation stays live tokenless with a clock, an updating stream and ledger stop controls |
41: the default /runs seeds the rows live elsewhere after this process's rows, tokenless, with the live count, and still never reads the store | [unit] src/channels/liveView.test.ts::live view on RunsService: history pages + index toggle …::index: active by default, everything with ?all=1::the default view also seeds the runs live on the ledger under another generation (run-history item 41): tokenless, live, after this process's rows; still never a store call |
41: live — during a rollout (or after a kill, before the resume), /runs on the new container shows the run the old one is still driving (or the row awaiting launch) as live with its generation; the run page opens; runs stop on it stops it | [agent] Start a slow run, deploy restart --force; while both containers exist, open /runs on the new one and the run's page; then switchboard runs stop <id>. |
38: live — kill the bot mid-run with POST /admin/crash while a step is in flight; within two lease intervals of the boot the run continues on the new generation: the card's frames carry on, the run page shows the resumed note after the pre-kill events, the reply lands in the thread, and runs get <id> is completed with one contiguous event stream | [agent] Same procedure as item 36 on a slower run (agent:research with a strong-tier model:<provider>/<model> directive); check /healthz for the new generation, then the thread and the record. |
36: live — kill the bot mid-run with POST /admin/crash; the next container's boot log shows [reclaim] <id> <thread> closed interrupted (from live; …), runs get <id> returns an interrupted record whose events are the ones published before the kill, the card is closed ❌ by the sweep, and POST /runs/live is empty | [agent] With a deploy:write token: start <@bot> agent:general … on a quiet thread, then curl -s -X POST -H "authorization: Bearer $TOKEN" https://<bot>/admin/crash; watch /healthz for a new generation; check the run and the ledger. |
35: live — a run's rows are visible on the ledger while it runs: POST /runs/live on the state Worker lists the row with ownerGen equal to the bot's /healthz generation, and the row is gone once the run finishes | [agent] With the state Worker and then the bot deployed: start any run; curl -s -X POST -H "authorization: Bearer $MEMORY_TOKEN" -H 'content-type: application/json' -d '{"storeKey":"runs:default"}' https://<state-worker>/runs/live shows the run; compare ownerGen with curl -s https://<bot>/healthz | jq .generation; re-run after the reply → the run is absent and runs get <id> returns its record. |
42: the CAS table admits attaching → live and attaching → finishing, never attaching → handoff or anything back to attaching; decideClaimWrite is insert / promote / refresh / keep; reclaimPhase keeps attaching and makes every other phase live | [unit] src/core/runLedger/decisions.test.ts::phaseTransition — the CAS table::an \attaching` row (reserved at admission, item 42) goes live when its prompt lands or finishing when the dispatch fails before that; nothing goes back to attaching and an attaching row is never handed off (it is not resumable — a restart re-dispatches it), ::decideClaimWrite — what a claim does to the thread's row (item 42)::, ::reclaimPhase — the phase a reclaimed row lands in::` |
42: the reference ledger and the Durable Object agree — a claim with phase: attaching reserves the thread with the request and no prompt (another run refused meanwhile); the owner's claim with the prompt promotes in place (identity and start kept); a re-claim on a live row changes nothing; a bad phase is 400; reclaim keeps an expired attaching row's phase and hands the request and inbox back; abandon drops the live rows with no record, fenced, unknown afterwards | [unit] src/core/runLedger/inMemory.test.ts::InMemoryRunLedger::a run reserved \attaching` at admission (item 42) holds the thread with its request and no prompt; the owner's claim with the prompt promotes it to live in place (card, prompt, tools, state land; identity and start stay); another run on the thread is refused meanwhile; a reclaim of an expired attaching row keeps the phase and hands the request back, deploy/cloudflare-memory/runLedger.test.ts::run ledger — claim and admission (item 29)::a claim with `phase: attaching` reserves the thread before the prompt exists (item 42): the row lists as attaching with its request and an empty prompt; the owner's later claim with the prompt promotes it to live in place; reclaim keeps an expired attaching row's phase` |
| 42: promotion merges the claim's state into the attaching row's durable state: an assigned live-state projection and sequence coexist with a later workspace binding, and the next sequenced transition succeeds on the same run | [unit] src/core/runLedger/inMemory.test.ts::InMemoryRunLedger::promotion preserves live state assigned while the run was attaching and accepts its next transition, deploy/cloudflare-memory/runLedger.test.ts::run ledger — claim and admission (item 29)::promotion preserves live state assigned while the run was attaching and accepts its next transition |
42: the write-through — reserve claims an attaching row with the request and card, starts the heartbeat, is tracked and live but not resumable and never handed off; open with the reservation promotes the same tracked run (one heartbeat, seed written, resumable from there); a reservation another generation took is fenced through the heartbeat or the promotion (onFenced once, undefined, nothing seeded); a refused reservation is undefined with one warning; abandon drops the row with no record and is a no-op on a fenced run | [unit] src/core/runLedger/writeThrough.test.ts::reserve — the row before the prompt (item 42)::* |
42: an ordinary run’s promotion whose claim goes untracked abandons the reserved row — gone from the ledger, not left attaching for the reclaim sweep to restart — and says why through onUntracked; claimRun publishes the ledger_untracked note and marks the card; an abandon that itself fails keeps the run known as this generation's dead reservation, so the thread's next claim abandons the row again and is tracked | [unit] src/core/runLedger/writeThrough.test.ts::reserve — the row before the prompt (item 42)::a reserved run whose promotion claim fails three times is abandoned…, ::a promotion gone untracked whose abandon itself fails…, src/core/dispatch/run.test.ts::claimRun — the ledger claim once the prompt exists::a promotion gone untracked publishes the ledger_untracked note… |
| 42: the reclaim hands an expired attaching row to the launcher as a restart with its request and inbox, and closes one without its request interrupted naming why | [unit] src/core/boot.test.ts::reclaimRuns::a row reserved at admission (item 42) whose owner died is handed to the launcher as a restart — the row still attaching, its request and unconsumed inbox in hand, nothing closed; one reserved without its request is closed interrupted naming why |
42: reclaim closes a hard-stopped ordinary run as stopped_hard, even when its attaching request or live transcript could restart; it never launches that work again | [unit] src/core/boot.test.ts::reclaimRuns::a hard stop recorded before reclaim closes an ordinary attaching or live row without restarting it |
| 42: the launcher dispatches a restart's own request — directives, sender, link, attachments — under the row's identity with a RestartContext, and closes a row whose request it cannot read | [unit] src/core/resumeLaunch.test.ts::launchResumes::a restart (item 42) dispatches the row's own request — text with its directives, sender, link, attachments — under the row's identity with a RestartContext carrying the row and the inbox; a row whose request cannot be read is closed with the reason |
52: isRunRecord accepts seed: channel, seed: parent and seed: session — also after a JSON round-trip — a record without one carrying no key, and refuses another word, an empty string and a non-string | [unit] src/core/runRecord.test.ts::isRunRecord — the seed field::* |
| 52: the one record assembly carries the seed and validates, a caller without one leaves no key; the drain deadline's interrupted record and the reclaim's close carry the seed the registry row and the ledger row name | [unit] src/core/dispatch/record.test.ts::assembleRunRecord — the handoff on the record::carries the seed…, ::the drain deadline's interrupted record and the reclaim's close carry the seed… |
52: the registry projects RunMeta.seed onto the summary and the index feed, and omits it when absent | [unit] src/core/runRegistry/projections.test.ts::RunRegistry.snapshot — record inputs::carries seed from the RunMeta… |
52: end to end — a spawned child's finish record reads seed: parent with its focused parent brief as context events; original omitted exchanges remain in authorized frozen parent recall, the parent's record seed: channel; a child redispatched through the boot gap keeps seed: parent on its row and record | [unit] src/core/dispatcher.test.ts::agent:conductor — a run that spawns child runs through dispatch()::a %s child's model receives a focused parent brief and recalls omitted evidence within the frozen source range; both records retain seed provenance, ::run ledger write-through (docs/reference/specs/run-history.md item 35)::a spawned child whose thread the boot-gap map names… |
42: RunRegistry.discard drops a live run with no finished frame — the index feed's removed, the end frame to a live subscriber, the id unknown afterwards — and is a no-op for an unknown or a finished run | [unit] src/core/runRegistry.test.ts::RunRegistry.discard::* |
| 42: end to end in the dispatcher — the row is reserved BEFORE the attach with the request, card and no prompt under the run's id, the registry row (label, token, the same start) exists from that moment and is the index's only row for the run, and the claim promotes the ledger row in place; a pre-model refusal finishes the advertised run under that same id, with no separate door record and an index feed that keeps its finished row; a restart runs the request again under the row's id and card, folds the carried follow-ups in with no second ack, promotes the row and finishes it with the original start; a restart onto a thread with a newer run closes the row interrupted with no reply | [unit] src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::the run is reserved on the ledger BEFORE the workspace attach (item 42): an attaching row with the request (text, sender, link, attachments), the card and no prompt, under the id the run will have, and the registry row — label, token, the same start — exists from that moment too; the claim once the prompt exists promotes that row in place — one row, one id — and the finish clears it, ::an attach refusal finishes the same advertised run without a second door record, ::a run reserved at admission whose owner died is restarted under its own id (item 42): the request is dispatched again from the row, the card is the row's, the follow-ups steered in meanwhile are folded in with no second ack, this generation promotes the row and the finish closes it — the record keeps the original start, ::a restart onto a thread that has a newer run in flight closes the reserved row interrupted with no reply and no run (item 42) |
| 42: a ledger stop delivered before registry registration is held until the run control exists; a hard stop aborts setup and ends the request stopped without a model call or a stranded ledger row | [unit] src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::a hard stop relayed before the registry row exists aborts setup and closes the reservation |
| 42: a hard stop during cold sandbox credential cleanup, snapshot seeding or the missing-seed refresh aborts setup, closes the reservation and never starts the model | [unit] src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::a hard stop during cold %s aborts the request and closes the reservation without starting the model |
42: live — kill the bot (POST /admin/crash) while a run is still attaching its workspace (the card reads attaching the workspace…, POST /runs/live shows its row with phase: attaching and no system); the next generation's boot log shows [reclaim] <id> <thread> restartable (from attaching …) and [resume] <id> <thread>: restarting from its request, the same card carries on, and the run finishes under the same id with startedAt the original | [agent] On a quiet thread, mention a review or research run against a repo whose resident is busy (or a cold sandbox clone), crash within the attach, watch /healthz for the new generation, then the thread, runs get <id> and POST /runs/live. Pending live receipts. |
56: a run's usage is summed per model from its model.turn spans (a model-less turn under unknown, non-numeric attrs as 0, no turns → the zero usage), merged with addUsage, validated by isRunUsage; the aggregate bills a child to its parent's requester (in the batch, by lookup, or its own id when unknown) and sums the runs into one cell per requester, thread, channel, agent and UTC day of finish, oldest day first — the child's cell in the child's own thread and agent, a record naming no agent under unknown, two runs of one requester in one thread on one agent one day sharing a cell — counting a usage-less run as pending; the report also retains individual coordinator-child points for the Ship spend trend; reportOfUsageRows folds the store's per-run answer with its parents; isRunUsageRows and isRunUsageReport validate the two wire shapes, a cell of the previous shape refused | [unit] src/core/runUsage.test.ts::usageOfEvents::*, src/core/runUsage.test.ts::aggregateUsage::* |
| 56: the assembly carries the usage summed from the run's spans, per model, and the zero usage for a run without turns; the record still validates | [unit] src/core/dispatch/record.test.ts::assembleRunRecord — the handoff on the record::carries the run's usage summed from its model.turn spans, per model; a run without turns carries the zero usage |
56: a local store's usage sums per requester, thread, channel, agent and UTC day within the range, bills a child to its parent's requester in the child's own thread and agent, prices a usage-less record from its events (never pending) and reports the oldest finish it holds | [unit] src/core/runStore.test.ts::* — RunStore contract::usage sums per requester, thread, channel, agent and UTC day within the range, bills a child to its parent's requester, and prices a usage-less record from its events |
56: the Worker client posts /runs/usage with the range, no policy, re-validates the per-run rows and folds them into the cells, a child billed to the parent the Worker named; a malformed answer is a PermanentStoreError | [unit] src/core/runStoreWorker.test.ts::WorkerRunStore::usage posts /runs/usage with the range, re-validates the per-run rows and folds them into the cells — a child billed to the parent the Worker named; a malformed answer is a PermanentStoreError |
57: isRunRecord accepts policy refusal, provider transient, incomplete model stream and pre-model sandbox fleet failures — also after a JSON round-trip — a record without one carrying no key, and refuses another kind, a bare word and an empty object | [unit] src/core/runRecord.test.ts::isRunRecord — the failure field::* |
57: a coordinator child that spends the typed sandbox fleet wait during setup records failure: sandbox_fleet_busy on the original failed run before its model starts. The same typed executor facts select the bot's paired ending log; a message or a non-fleet capacity error cannot name this failure. The durable read-record carries the fact to Ship. | [unit] src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::coordinator producer identity::records typed fleet capacity only when a review child failed before its model started, src/execution/sandboxErrors.test.ts::the fleet-busy ending log lines::matches the error by name across an import boundary — a name-and-facts shape logs like the class itself, src/core/runRecord.test.ts::isRunRecord — the failure field::* |
| 57: the one record assembly carries the failure by name and validates; a caller without one leaves no key | [unit] src/core/dispatch/record.test.ts::assembleRunRecord — the handoff on the record::carries the failure by name… |
57: through the run loop — a run whose model call the provider refused under its usage policy ends with the harness's typed error, the registry finished failed, the record saying failure: policy_refusal beside status: failed with the policy_refusal note carrying only the permanent cause's rendered sentence; a run failed for an unclassified reason carries no failure key | [unit] src/core/dispatch/runLoop.test.ts::runLoop — the model turn and everything that rides on it::a run whose model call the provider refused under its usage policy fails by name… |
57: an exhausted pi-local stream reaches the record as failure: model_stream_incomplete without a provider-down claim | [unit] src/core/dispatch/runLoop.test.ts::runLoop — the model turn and everything that rides on it::an exhausted local stream records model_stream_incomplete without claiming a provider failure; src/core/coordinator/driver.test.ts::*::an incomplete local stream failure survives read-record and earns Ship's one empty-branch retry |
56: inside workerd — put stores the record's usage beside the row; /runs/usage answers one row per run, oldest finish first, with its requester, thread, channel and agent, names a child's parent outside the batch (and none when every parent is inside), backfills a usage-less record from its stored events and writes it back (pending 0 after), reports retentionDays and the oldest finish held, honours the range; a malformed range is a 400 by name and an empty range empty rows | [unit] deploy/cloudflare-memory/runs.test.ts::run usage::* |
58: the in-memory and file stores filter by parentRunId, newest first under the cap; a run with no children is empty | [unit] src/core/runStore.test.ts::InMemoryRunStore — RunStore contract::list filters by \parentRunId`…, src/core/runStore.test.ts::FileRunStore — RunStore contract::list filters by `parentRunId`…` |
58: the Worker client posts parentRunId beside the listing's other filters | [unit] src/core/runStoreWorker.test.ts::WorkerRunStore::get/list/events/delete send no policy… |
58: inside workerd — /runs/list filters by parentRunId, newest first, empty for a parent with none, 400 for a malformed one; a table from before the column gains it filled from each record's own parent, idempotently, and a later put writes it beside the row | [unit] deploy/cloudflare-memory/runs.test.ts::run history routes::list: newest-first, limit 1000 → at most 200 rows plus a cursor; before/sinceMs/agent/channel/threadKey/parentRunId/pr filters; no events on the wire, deploy/cloudflare-memory/runs.test.ts::run history routes::a table created before the parent column gains it… |
58: the in-memory and file stores filter by pr — the coding runs whose record names the pull request and the reviews posted to it, on that repository, newest first under the cap — never a review that posted nothing, another repository's run or a run without a repository | [unit] src/core/runStore.test.ts::InMemoryRunStore — RunStore contract::list filters by \pr`…, src/core/runStore.test.ts::FileRunStore — RunStore contract::list filters by `pr`…` |
58: inside workerd — /runs/list filters by pr on repo and pr_number, newest first, empty for a number no run names, 400 for a zero, a non-slug or a string; a table from before the column gains it filled from each record's own pr or posted review target — a skipped post and a run naming none stay null — idempotently, and a later put writes it beside the row | [unit] deploy/cloudflare-memory/runs.test.ts::run history routes::list: newest-first, limit 1000 → at most 200 rows plus a cursor; before/sinceMs/agent/channel/threadKey/parentRunId/pr filters; no events on the wire, deploy/cloudflare-memory/runs.test.ts::run history routes::a table created before the pull request column gains it… |
58: RunsService.listRuns narrows live and persisted rows by pr and hands the store the filter; listFindings joins the pull request's records with its unit's runs under visibleTo and answers the ledger or not_found | [unit] src/core/runsService.test.ts::RunsService.listFindings — a pull request's findings ledger from the records (agent-ship item 18)::* |
58: RunsService.listRuns narrows live and persisted rows by parentRunId and hands the store the filter; listChildren orders them by start; listUnitRuns and searchSession answer their views or not_found | [unit] src/core/runsService.test.ts::RunsService.listChildren — a conductor's children in start order::*, src/core/runsService.test.ts::RunsService.listUnitRuns — a unit's runs in round order::*, src/core/runsService.test.ts::RunsService.searchSession — a person's search over one session's log::* |
| 59: insert-if-absent on the reference ledger — the first write on a key answers inserted with the row, a second answers the first stored row; a read before any write answers none; list filters by thread and by since, oldest first; one failure-delivery claim wins atomically, rejection releases it, success closes it, and an unfinished claim recovers only after its bound | [unit] src/core/runLedger/inMemory.test.ts::intake receipts (run-history item 59)::* |
| 59: the in-memory failure toggle makes a write and a read throw on demand and clears back to working | [unit] src/core/runLedger/inMemory.test.ts::intake receipts (run-history item 59)::the failure toggle makes a write and a read throw on demand, and clears back to working |
| 59: inside workerd — the insert is if-absent inside the transaction, one failure-delivery claim wins atomically, rejection releases it and success closes it; read answers the row or null, list filters by thread and since, a malformed key, receipt or windowMs is 400 and no bearer 401 | [unit] deploy/cloudflare-memory/runLedger.test.ts::run ledger — intake receipts (item 59)::* |
| 59: when scheduled cleanup is enabled, the alarm prunes by both arms of the bound — a 30 minute window keeps a row 24 hours, a two day window keeps it the window plus the drain deadline | [unit] deploy/cloudflare-memory/runLedger.test.ts::run ledger — intake receipts (item 59)::the alarm prunes by both arms of the bound… |
| 59: the Worker client posts the key and receipt under the store key with windowMs only when configured, carries the delivery claim/finish bodies, reads the row or none, and lists with only the filters given | [unit] src/core/runLedgerWorker.test.ts::intake receipts — the routes and the retry (run-history item 59)::* |
| 59: the write-through's retry after a lost response reads the same row — this write's own landed row answers inserted, another writer's answers what it stored, none inserts once more; a missing route, a permanent refusal or a failing retry answers undefined with one warning | [unit] src/core/runLedger/writeThrough.test.ts::intake receipts — the write-through's retry (run-history item 59)::*, src/core/runLedgerWorker.test.ts::intake receipts — the routes and the retry (run-history item 59)::the write-through's retry after a lost response reads the same row and answers the stored insert |
The stall signal's pace facts (live-view item 32) are the registry summary's, LIVE rows only: eventsLast5m counts the content events of the last window (span records excluded, present from create), lastToolCallAt stamps the newest tool call, inFlight carries the open call with its declared bound; a finished summary carries none | [unit] src/core/runRegistry.test.ts::RunRegistry — the stall signal's pace facts (live-view item 32)::* |
The pace facts ride RunView for this process's live rows only — never a persisted row, so a store-only reader shows no signal rather than a false stall | [unit] src/core/runsService.test.ts::RunsService.getRun::a live view carries the stall signal's pace facts — eventsLast5m, lastToolCallAt, inFlight with its bound; a persisted row none |
60: the record carries the operator's shadow decision off its operator event with the intake verdict and validates; none → no key | [unit] src/core/dispatch/record.test.ts::assembleRunRecord — the handoff on the record::carries the operator's shadow decision off its operator event with the intake verdict, and the record still validates; none → no key |
60: an exhausted or unavailable operator writes a failed door record with non_decision and its attempts; no default run starts | [unit] src/core/dispatcher.test.ts::the operator behind routing.operator (record 0057; routing-and-config item 29)::on: a turn ending with no tool call is re-asked once, then stops at the door, ::on: an unavailable operator records failure and starts no default run |
60: a refusal after an operator choice keeps input, operator, and refusal on one door record; the operator event snapshots organization, inherited repository facts and authorized candidate-list status/count, and its request root logs the door run ID for correlation | [unit] src/core/dispatch/commandRun.test.ts::recordRefusal — every refusal is a run record (record 0054, as amended)::keeps a preceding operator choice and the refusal on one trace-linked door record, src/core/dispatcher.test.ts::the operator behind routing.operator (record 0057; routing-and-config item 29)::keeps the operator choice on the same door record when the repository gate refuses |
Coordinator producer identity
Canonical spawn requires the original unit, executing Workflow and recovery action, exact step/preset and monotone effect ordinal. It persists pending before dispatch and acknowledges only the exact durable child receipt. Retry uses listings to locate candidates; listings do not authorize adoption or prove absence. An unknown dispatch remains retryable for observation and cannot execute twice. Branch reuse leaves the ordinal unchanged; creating a branch and dispatching a child each consume one ordinal.
| Criterion | Proof |
|---|---|
| A child registration notification follows the committed admission identity and setup stream, including exactly one coordinator tag. | src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::coordinator registration waits for the durable identity and exact setup tag before notifying its owner: … |
| An attaching restart retains the original durable stream, canonical publication, admitted profile and absolute allowance. Resident wait bounds cannot exceed that allowance. Missing or conflicting publication evidence cannot become a new review target. | src/core/boot.test.ts::reclaimRuns::a row reserved at admission…, src/core/resumeLaunch.test.ts::the pure pieces::repoContextOf restores the coordinator review base from its durable admission tag, src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::coordinator producer identity::a restarted coordinator review retains its original publication before resolving a moved head, ::a canonical child retains its original profile and absolute budget through… |
| Exact finished child evidence resolves a lost dispatch acknowledgement without dispatch replay; unproven registry/history candidates and uncertain outcomes leave queued attachments unconsumed. | src/channels/adminCoordinator.test.ts::spawn durable effect admission::a lost dispatch reply resolves from the exact finished review without dispatching it again, src/channels/adminCoordinator.test.ts::the fold — a unit's thread events reach the pipeline's next step (record 0051's fold rule)::an uncertain coding dispatch leaves its attachment unconsumed and never replays on retry, ::a registry-only spawn candidate cannot authorize adoption or consume queued thread events |
| 61: exact live ownership wins over retained history, terminal evidence excludes provisional records, and malformed IDs fail closed | [unit] deploy/cloudflare-memory/runLedger.test.ts::exact owner evidence for resident preservation::* |
| A coordinator child reserves its same ID before registry creation, then installs the shared setup finalizer immediately after creation and before its link or acknowledgement exposes the ID. A reservation failure returns a typed non-200 without an id. Post-ack pre-model failure retains the original parent, key, thread, repository and ref with a typed nonempty reason; replay creates no child or budget. Foreign and missing reads remain indistinguishable. | [unit] src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::coordinator producer identity::* |
| The shared setup finalizer remains active until durable promotion and pre-loop setup succeed. Ordinary pre-effect setup failures retain the existing failed-child and workspace-release behavior. Unknown accepted original promotion or session ownership retains the original row, binding and held context without finalizing, abandoning, releasing, seeding or admitting the model. A fenced promotion neither runs nor finishes the new owner's row. | [unit] src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::coordinator producer identity::keeps the same-id finalizer through a…, ::a promotion fenced by another generation never runs or finalizes that generation's child, src/core/dispatcher.test.ts::executor provisioning by agent resources::releases a review workspace when setup fails after attach, src/core/runLedger/writeThrough.test.ts::reserve — the row before the prompt (item 42)::a coordinator promotion failing from…, src/core/dispatch/run.test.ts::claimRun — the ledger claim once the prompt exists::a coordinator promotion ending…; src/core/dispatcher.test.ts::run ledger write-through …::holds unknown original promotion beyond every setup finalizer and release: %s |
| Legacy refusal decoding retains its exact text, including empty text; missing or non-string fields remain rejected, never synthesized from a neighbouring record. | [unit] src/core/runEventLines.test.ts::parseRunEventLines::keeps legacy refusal text exact and rejects missing or non-string refusal fields |
Historical recovery evidence
The internal recoveryEvidence listing is an admission audit, not a UI page. It selects the union of the original parent instance, an exact same-unit idempotency-key prefix or bare <instance>:<unit> key (even with a contradictory parent), and the original coding/review threads before applying the unchanged 200-row bound. No user, repository, thread-only or cursor filter may narrow this query. Ordinary run-list paging remains unchanged. The query proves completeness within retained history only; it does not reconstruct missing records, prices, caps, or lease origins.
| Criterion | Proof |
|---|---|
| The local stores and SQLite query select either identity claim and original threads before the cap; the bare same-unit key is retained for rejection by the child audit, while adjacent units do not match. More than 200 unrelated retained runs do not consume the evidence bound. SQL attests completeness only for a short valid result; full relevant sets, malformed candidate JSON and narrowed/cursor queries cannot attest it. | [unit] src/core/runStore.test.ts::* — RunStore contract::recovery evidence selects either identity claim and original threads before the cap; deploy/cloudflare-memory/runs.test.ts::run history routes::recovery evidence filters before the cap and attests only a complete valid set |
| Recovery attestation validates every retained summary with the record parser before trusting SQL identity selection. Valid JSON that is not a run record (scalars, arrays, missing required fields or malformed optional fields) cannot hide a foreign-thread child; any unreadable retained summary refuses completeness, including a pinned row beyond a maxRuns gap or an older pinned source outside the age cutoff. Unreadable existing records remain protected beyond ordinary retention limits and refuse completeness. Validation streams over retained evidence without changing the relevant-row cap. | [unit] deploy/cloudflare-memory/runs.test.ts::run history routes::recovery evidence refuses a retained foreign-thread summary that is*, ::recovery evidence refuses unreadable protected bytes beyond ordinary retention limits, ::recovery evidence refuses a malformed context-pinned row beyond a retention gap, ::recovery evidence refuses a malformed pinned source older than the age cutoff |
| The Worker client requires the explicit completeness attestation, no cursor, a short bounded set of valid relevant rows and no duplicate ids. An old Worker without this contract cannot accidentally authorize recovery. | [unit] src/core/runStoreWorker.test.ts::WorkerRunStore::recovery evidence requires an explicit complete bounded response without malformed or cursor rows |
| Admission bypasses the UI ledger cache and cap, reads live evidence before history, and preserves conflicting source identities. Missing stores and narrowed queries cannot prove completeness; a matching finished registry projection uses persisted artifacts. | [unit] src/core/runsService.test.ts::RunsService recovery evidence::*; src/channels/adminCoordinator.test.ts::POST /admin/coordinator/recover-unit — unchanged-head original-unit recovery::historical admission::reads uncached foreign live evidence beyond a full unrelated live and persisted page |
| A malformed or paginated live-ledger response is unavailability, never an empty complete ledger. | [unit] src/core/runLedgerWorker.test.ts::WorkerRunLedger::listLive refuses malformed or paginated responses instead of proving an empty ledger |
| Audience diagnostics contain a version, closed cause and stage fields, an optional closed context-check name, and optional closed GitHub read failure facts with the original call hash and observed response facts. Unknown codes, checks, stages or extra private fields are unavailable, never an allow receipt. The first failed check and its diagnostic survive later refusal observations, serialization and terminal cleanup. | [unit] src/core/audienceDecision.test.ts::audience refusal diagnostics::accepts only the closed structural receipt and never private extra fields; ::retains a closed context check without accepting source content; ::retains the first GitHub read failure across serialization and rejects private extras; src/core/runRecord.test.ts::stored audience diagnostics retain safe GitHub failure facts after normalization; src/core/dispatch/runLoop.test.ts::*::retains the failed context check and traces it when review publication is withheld |
| A refusal recorded after registry finish is read at reply seal without replacing the completed work facts. | [unit] src/core/dispatch/record.test.ts::registerFinishRecord — the finish record, written by the drain after the reply::reads a late audience refusal at seal without replacing completed work facts |
| The current owner can commit a bounded refusal while finishing. A lost owner cannot overwrite it or reply; storage unavailability remains distinct from an ownership fence. | [unit] src/core/runLedger/writeThrough.test.ts::finishing and finish::commits a refusal after finishing and distinguishes ownership loss from unavailable storage |
| Explicit audience reads return only committed receipts from local, foreign-live or stored runs; default point and list projections omit them. | [unit] src/core/runsService.test.ts::RunsService.getRun::exposes only committed audience diagnostics from… |
| Ledger ownership loss comes from the typed write result, never words in a storage error. | [unit] src/core/runLedger/writeThrough.test.ts::finishing and finish::does not turn a storage error mentioning a fence into ownership loss |
Ordinary answer outcomes
The dispatcher records AnswerOutcome before fallback text and Markdown rendering. Its typed ending and authored-output presence are independent of verification, audience refusal, and replyOk. A tracked run commits those facts before entering finishing; an unavailable write prevents delivery and an ownership fence prevents both delivery and record replacement. Recovered state uses the same strict decoder as RunRecord. Missing or malformed legacy facts cannot become present output from prose. A completed execution status is not proof of requested-work completion.
finishing fences replay before channel delivery. Reclaim preserves the saved outcome but leaves delivery unknown; its card stays amber and says delivery was not confirmed. A general or research budget close with absent output explicitly says the answer was not written, ahead of the advisory checklist so adapter clipping cannot hide it. Continuation and authorized run readers consume the same saved outcome. A nonempty partial write-up establishes authored text only.
| Criterion | Proof |
|---|---|
| The producer captures empty and present raw write-ups independently of their claims; legacy, malformed and future data cannot supply facts | [unit] src/core/answerOutcome.test.ts::ordinary answer outcome::* |
| Delivery waits for the saved result; an unavailable write and an ownership fence suppress publication; failed or withheld delivery never rewrites authored output | [unit] src/core/dispatch/reply.test.ts::deliverAnswer — the answer reaches the thread::* |
| Reclaim before the final seal retains the saved missing answer with delivery unknown | [unit] src/core/boot.test.ts::reclaimRuns::a restart before the final seal retains the missing write-up without claiming delivery |
| Stored and recovered outcomes share validation and remain independent of delivery | [unit] src/core/dispatch/record.test.ts::assembleRunRecord — the handoff on the record::validates producer outcome separately from fallback text and delivery |
| A recovered card exposes the saved missing answer and unknown delivery | [unit] src/channels/slack/statusCard.test.ts::live cards::a recovered budget card exposes the saved missing answer and unknown delivery |
| Saved absence survives nonempty rendered text across restart | [unit] src/core/dispatch/runLoop.test.ts::a resume with the answer in hand (the \finish` plan)::a recovered*` |
| Continuation cannot derive authority from a legacy fallback sentence | [unit] src/core/dispatcher.test.ts::a follow-up seeds from its session (docs/reference/specs/session-log.md item 9)::a legacy fallback cannot authorize continuation through its wording |
Durable publication projection
| Criterion | Proof |
|---|---|
| Producer branch publication remains typed and bounded independently of display events. Missing or malformed historical facts never prove completeness; pending and over-cap facts cannot authorize ordinary release. Metadata-only edits retain their accepted PR target for canonical live ownership lookup while never gaining pushed-branch ownership; target capacity refuses before the API call. | src/core/runRecord.test.ts::durable branch publication contract::validates complete and pending producer projections while rejecting unknown or over-cap facts; src/core/dispatch/runLoop.test.ts::a resume with the answer in hand (the \finish` plan)::durable branch publication through the real run loop…` |
| Criterion | Proof |
|---|---|
| Both ledgers fold only saved producer publication at terminal commit, independently of display events and speculative process state; summaries omit its intent. Missing or malformed producer state cannot prove branch ownership, but terminal outcome and accounting still survive. | deploy/cloudflare-memory/runLedger.test.ts::run ledger — durable branch publication::*; src/core/runLedger/inMemory.test.ts::InMemoryRunLedger::retains the terminal outcome using only saved producer publication…; src/core/runLedger/inMemory.test.ts::InMemoryRunLedger::folds the fenced producer projection at finish independently of events; src/core/runsService.test.ts::RunsService.getRun::never exposes internal source archives or handoff context through any persisted projection |
| Criterion | Proof |
|---|---|
| Automatic history retention keeps an incomplete, pending or unreadable producer publication and its existing context sources beyond ordinary age, count and byte limits. Direct runs need no coordinator unit or resident binding. Full records retain typed private intent, and unreadable producer facts remain in the existing private evidence column; summaries and listings omit it. Retention alone neither settles nor replays the mutation. | src/core/runStore.test.ts::* — RunStore contract::retains unresolved publication and its context beyond ordinary history limits…; src/core/runRecord.test.ts::applyRetention::protects unresolved evidence and existing context beyond byte limits without resurrecting absent records; src/core/runStore.test.ts::FileRunStore::retains private pending intent after reopening and preserves unreadable evidence during sweep; deploy/cloudflare-memory/runLedger.test.ts::run ledger — durable branch publication::retains unresolved direct publication through finish and physical history trimming |
| Criterion | Proof |
|---|---|
| Malformed private evidence remains stored through retention and refuses replacement. An unreadable listing row consumes its cursor position so older readable producers remain discoverable. | deploy/cloudflare-memory/runLedger.test.ts::run ledger — durable branch publication::preserves unreadable private publication evidence and refuses replacement…; deploy/cloudflare-memory/runLedger.test.ts::run ledger — durable branch publication::advances listing past unreadable protected bytes to an older unresolved producer |
| Criterion | Proof |
|---|---|
| Replacement after a failed Workflow create can discard only valid units with no execution or publication evidence. Existing PR, resume, pushed head, legacy ending, wake decisions and unknown progress facts retain their original instance and unit atomically, even without a typed outcome or recovery history. | src/core/coordinator/instanceStore.test.ts::*::replacement retains existing publication or execution evidence…; deploy/cloudflare-memory/runLedger.test.ts::run ledger — the coordinator instance record (item 49)::replacement retains existing publication or execution evidence… |
| Criterion | Proof |
|---|---|
| Unreadable unit bytes refuse instance replacement without deleting or rewriting the saved owner, and a refused replacement performs no retention synchronization. | deploy/cloudflare-memory/runLedger.test.ts::run ledger — the coordinator instance record (item 49)::refuses replacement of unreadable unit bytes without deleting the original owner |
Canonical pull ownership lookup
Ownership admission diagnostics identify the failing validation check, row source and index, and bounded scan counts. The state Worker logs these facts with the attempted instance and unit. It never logs row contents, publication text or private evidence. Diagnostics leave the ownership result and retained bytes unchanged.
| Criterion | Proof |
|---|---|
| An admitted unit retains its original start fence across ordinary, CAS and wake writes. Delayed draft snapshots cannot erase admission and release its branch. Ordinary batches serialize all rows before committing any one, so malformed payloads leave no partial state. | src/core/coordinator/pullOwnership.test.ts::complete canonical pull ownership::keeps the original start fence through delayed whole-row and wake writes, ::serializes the whole batch before committing any ordinary draft |
| Main task claim and recovery claim reserve targets under the canonical owner before first execution. Competing authority refuses without inserting the Main link/instance, recovery journal, receipt or changed unit. Initial Main serialization failure also leaves its instance, unit and link unchanged. | src/core/coordinator/pullOwnership.test.ts::complete canonical pull ownership::refuses a conflicting first Main task claim without recording any link or instance; src/core/coordinator/recoveryHistory.test.ts::recovery history store::refuses recovery ownership conflict before journaling any action or receipt |
| Ordinary batch and wake writes that establish or change a PR binding enforce complete canonical ownership in their existing transaction. A conflict, unavailable authority or stale target refuses the entire batch, keeps original rows and leaves wake events unconsumed. | src/core/coordinator/pullOwnership.test.ts::complete canonical pull ownership::fences ordinary batch and wake binding writes before any mutation |
| A new PR binding admitted by exact unit compare-and-replace reads complete canonical owners and reserves its targets in the same existing-owner transaction. Competing unhosted units, direct producers, unresolved legacy endings and incomplete/unavailable evidence refuse before unit or context mutation; previous targets cannot be silently dropped. This guard does not grant external effect authority. | src/core/coordinator/pullOwnership.test.ts::complete canonical pull ownership::reserves a new PR binding atomically against every canonical owner; deploy/cloudflare-memory/runLedger.test.ts::complete canonical pull ownership::refuses competing PR reservations inside the unit compare-and-replace transaction |
| Complete pull ownership reads canonical units and direct producer facts, including unhosted Main work and terminal uncertain publication. Legacy endings retain their owner. Missing paired authority, incomplete or unreadable evidence refuses; public listing absence cannot prove ownership absence. Pending workspace obligations include accepted targets after historical history removal; their JSON owner and revision must match durable columns. Scans bound rows and bytes; normal exact outbox-bound plane admission is recognized without treating request text as PR authority. Lookup alone does not authorize a later mutation. | src/core/coordinator/pullOwnership.test.ts::complete canonical pull ownership::*; deploy/cloudflare-memory/runLedger.test.ts::complete canonical pull ownership::* |
| Terminal ownership scans read canonical run identity and publication facts directly from strictly valid stored JSON in the admission transaction. Historical display text and diagnostics cannot exhaust the ownership byte budget. Unknown identity, publication or private evidence still refuses; relevant source bytes remain bounded before parsing. SQLite JSON5 normalization cannot turn unreadable evidence into a cleared publication. No history is deleted to establish ownership absence. | deploy/cloudflare-memory/runLedger.test.ts::complete canonical pull ownership::ignores historical display bytes while retaining canonical terminal publication; ::bounds canonical producer bytes before parsing; ::retains unreadable canonical identity and publication after excluding display fields |
| History deletion refuses while producer publication is pending or unreadable; unknown private envelopes retain existing authority. Unresolved Door intents retain their context beyond ordinary history limits. The terminal transaction folds saved Door facts; malformed bytes stay private and the producer's null clear sentinel creates no obligation. Deleting an active or unknown ID changes no events or pins. | src/core/runStore.test.ts::InMemoryRunStore — RunStore contract::retains unresolved publication…; src/core/runStore.test.ts::FileRunStore — RunStore contract::retains unresolved publication…; deploy/cloudflare-memory/runLedger.test.ts::complete canonical pull ownership::* |
Terminal PR reads propagate refused durable writes before reporting merged or closed success. Proof: [unit] src/channels/adminCoordinator.test.ts::pr-check keeps the machine's adopted pull request authoritative before branch discovery (agent-ship items 9, 10 and 12)::refuses terminal PR success when its durable ownership write is rejected.
| Criterion | Proof |
|---|---|
| Canonical attachment restart requires the original valid admitted deadline and saved profile. Missing facts retain the original row, and an expired allowance admits no attachment or model execution. | [unit] src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::coordinator producer identity::a canonical attachment restart with a… |
| Canonical model admission carries the absolute allowance through both harness implementations; attachment downtime cannot renew the model or provider lease. | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — a run on pi from the first file to the answer::a carried absolute deadline bounds…, src/core/harness/opencode/harness.test.ts::an absolute admitted deadline bounds… |
| Canonical post-loop model turns and their finale consume only the absolute remainder; expiry opens no further prompt. | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — a run on pi from the first file to the answer::a canonical post-loop turn… |
| The first resident status request and retry requests consume the clipped allowance; an expired bound cannot provision a fallback. | [unit] src/execution/factory.test.ts::makeExecutor resident selection::a restoring resident — the one held /await-restore request (item 27)::the first resident probe cannot outlast… |
| Incomplete original profile or coordinator ownership cannot turn a saved child into an ordinary restart. Missing evidence refuses before executor effects and retains the original row. | [unit] src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::coordinator producer identity::a canonical attachment restart with a… |
| Explicit source deletion revokes ordinary checkpoint aliases after retention expiry, while a live owner retains events and context pins even beside a provisional record. | [unit] deploy/cloudflare-memory/sessionUnification.test.ts::unified sessions — durable owner and retention lifecycle::bounds 384 ordinary continuations across retention and reconstruction while frozen units keep their originals, ::explicit deletion keeps a live owner's events and context pins with a provisional record… |
| Criterion | Proof |
|---|---|
| A workspace ACK refuses an unknown run, generation or fence with no retained revision. Duplicate ACKs require a known retained version; live ownership still refuses. ACK success never authorizes removal of private files. | [unit] src/core/workspaceSettlement.test.ts::exact retained workspace acknowledgment::*; deploy/cloudflare-memory/runLedger.test.ts::durable coordinator Workflow reconciliation::* |
| Resident retained-owner ACK consumers use one exact owner and positive safe revision validator. Live, absent, unknown, malformed and foreign observations cannot become ACKs. Workspace removal and predecessor settlement still require their saved exact revision and full binding/publication checks. | [unit] src/core/workspaceSettlement.test.ts::acknowledged workspace owner evidence::*; deploy/cloudflare-resident/workspaceSettlement.test.ts::*; deploy/cloudflare-resident/dependencyInspection.test.ts::* |
| Proposed exact-original negative admission resolution preserves unconfirmed source custody and distinguishes cancellation, actual ending and explicitly authorized loss. It cannot create confirmation/release receipts, reset an allowance or affect a successor. Existing hard-stop protection remains until the complete protocol is qualified. | [gap] Run owner resolution plan: cancellation, every-operation fencing and physical-disposition producers are proposed, not implemented. |
| Criterion | Proof |
|---|---|
| A pending original report retains its canonical unit target; its envelope cannot assign repository authority or become a competing owner. | [unit] src/core/coordinator/pullOwnership.test.ts::complete canonical pull ownership::retains the canonical target for a pending original report* |
| Criterion | Proof |
|---|---|
| Original review publication survives terminal assembly, event trimming and restart with exact run/repository/PR/full commit/body hash and accepted/refused/pending/uncertain state. Malformed saved evidence remains invalid instead of becoming an empty legacy record. | [unit] src/core/dispatch/record.test.ts::terminal review publication receipt::*; src/core/runRecord.test.ts::strict terminal review publication::* |
| Terminal publication comes only from canonical fenced producer state, never caller-provided acceptance; missing state cannot manufacture a write, malformed state stays retained as unreadable evidence. Generic summaries expose no review receipt; pending/uncertain/malformed receipts protect retention. | [unit] src/core/runLedger/inMemory.test.ts::canonical terminal review publication::*; src/core/reviewPublication.test.ts::review publication terminal retention::*; src/core/runStore.test.ts::terminal review publication store privacy::*; deploy/cloudflare-memory/runLedger.test.ts::*::finishes with only canonical review publication and retains unknown evidence: … |
| Terminal records project original review and native push receipts only from canonical saved producer state. A caller cannot replace pending or malformed source evidence with acceptance; unknown private bytes survive history trimming, and public summaries omit these private receipts. | [unit] deploy/cloudflare-memory/runLedger.test.ts::run ledger — durable branch publication::finishes with only canonical review publication and retains unknown evidence: %s; deploy/cloudflare-memory/runLedger.test.ts::run ledger — durable branch publication::folds only saved native push receipts at finish: %s; src/core/dispatch/record.test.ts::original native branch receipt on the terminal record::projects latest exact producer receipts independently of trimmed display events; src/core/runLedger/inMemory.test.ts::canonical terminal native branch receipt::folds latest original native ref evidence and retains malformed canonical history without granting publication credit |
| Criterion | Proof |
|---|---|
| Malformed restored native push history remains protected and blocks later native publication. A later accepted push cannot overwrite missing original evidence or enable ordinary workspace release. | [unit] src/core/dispatch/runLoop.test.ts::a resume with the answer in hand …::malformed restored push history refuses a later native push and preserves the original evidence |
| Original frozen maintenance plans survive byte pressure while uncertain calls remain owned; transient tool context can trim without deleting the effect payload or permitting replay. | [unit] src/core/coordinator/sweepEffectJournal.test.ts::durable sweep journal::keeps the original frozen plan under byte pressure while an uncertain call remains owned |
| An initial coding child with one exact native accepted branch push remains owned by its original canonical unit before a PR exists. Exact terminal identity, checkpoint, native receipt and accepted unit admission must agree. A clean completion checkpoint with no additional publication may qualify the already accepted model push for an original unit without a stored adoption audit only when its head and the canonical run head equal the sole native receipt and its original base head remains bound; this does not invent checkpoint publication, preservation, release or native ending. Matching retained workspace settlement also requires its exact original generation/ref, any recorded publication base and a unique unchanged publication snapshot; unknown intents and foreign or additional publications still refuse. Delegated ownership keeps the same recovery or adoption action identity as its canonical unit, yielding one owner. Ownership lookup preserves the unit’s existing holding policy and never rewrites publication completeness, workspace retention or release receipts. | [unit] src/core/coordinator/pullOwnership.test.ts::complete canonical pull ownership::keeps an accepted pre-PR * publication under its original unit; deploy/cloudflare-memory/runLedger.test.ts::complete canonical pull ownership::admits the original pre-PR * child and retains its workspace obligation through PR binding | | Standing historical audits consume global ownership capacity only for canonical identity, typed publication/causal tool fields and sequence completeness. Model prose, stdout and diagnostics stay outside that budget. The proposed in-memory audited unit is included before accounting. Initial audit and every audited CAS still verify the complete bounded original run snapshot/digest and recheck it atomically before rival admission; the global scan derives its compact evidence from those same canonical rows, with no additional stored truth. Several verbose histories cannot poison a fourth valid claim, standing lookup, posting/binding or ordinary admission. Missing or changed authority, malformed typed fields and actual rivals still refuse. | [unit] src/channels/adminCoordinator.test.ts::original committed head adoption — create-only draft PR::keeps verbose audited histories out of initial and standing global ownership capacity; deploy/cloudflare-memory/runLedger.test.ts::historical original native adoption owner CAS::admits a fourth verbose history and preserves standing, posting and ordinary ownership capacity; src/channels/adminCoordinator.test.ts::original committed head adoption — create-only draft PR::rechecks canonical bytes after the asynchronous audit digest before owner CAS; ::refuses standing ownership when authoritative history changes at the same count and heads |
Durable branch identity baseline
| A source-context save on an attached run with a session reports a caught failure or refused source/state write through the existing ledger warning channel, with a bounded stage and typed failure but no private exception or request bytes. Inactive or sessionless guards remain silent. Diagnosis does not change retry policy or grant acknowledgement, review-history, recovery or completion credit. | [unit] src/core/runLedger/writeThrough.test.ts::source context write diagnostics::* |
| A checkpoint uses the same versioned durable codec as external follow-ups and provider controls. Its original run and channel/thread are canonical; an unknown version or malformed credential/relay/parent principal remains unreadable and cannot downgrade to a person. | [unit] src/core/runLedger/inboxMessage.test.ts::durable inbox continuity::* | | A skipped row remains an owed sequence without entering model input. Later consumption cannot hide that hole, and terminal finish or abandon preserves unconsumed raw inbox bytes. A storage/delivery ACK is not a native consumption receipt. | [unit] src/core/threadAdmission.test.ts::durable inbox held rows::*; src/core/runLedger/inMemory.test.ts::durable inbox terminal custody::*; deploy/cloudflare-memory/runLedger.test.ts::durable inbox terminal custody::* | | Response-byte interruption is distinct from complete malformed JSON. Reads retain the transport cause; a write without a valid positive result remains an unknown mutation. HTTP success alone cannot acknowledge a fenced operation, and an unknown inbox append is never blindly repeated. | [unit] src/core/runLedgerWorker.test.ts::durable ledger acknowledgement continuity::*; src/core/runStoreWorker.test.ts::durable store acknowledgement continuity::* | | An uncertain primary or fallback history commit retains the exact original record and request witness. The uncertain mutation is neither retried nor counted as permanent loss (a definite primary refusal remains counted); it grants no persisted or terminal-owner release credit without positive canonical reconciliation. | [unit] src/core/runHistoryWriter.test.ts::unconfirmed history commitment::* | | A lost state or step ACK keeps the immutable expected boundary, run and original generation. Only an exact fenced canonical read may reconcile commitment; absent or different data stays held without replaying the write. This reconciliation proves a store boundary, not native inbox consumption or safe further execution by an unwired harness. | [unit] src/core/runLedger/writeThrough.test.ts::uncertain canonical boundaries::* | | An uncertain canonical write retains the first observed transport, HTTP, response-decoding or acknowledgement classification with its original immutable request. A bounded diagnostic names a known operation, failure class and observed HTTP status; an exact-wire-bound state-write failure may also name a closed server stage and exception/reply category. Exception prose, credentials and request/response bodies never reach it. The existing readable failure note keeps its original reason and appends only the original run/current-generation diagnostic. Later held patches do not replace that cause, retry the uncertain write or grant acknowledgement. Legacy unclassified errors remain unclassified; known fence refusals stay definite. | [unit] src/core/runLedger/writeFailureDiagnostics.test.ts::original uncertain write diagnostics::*; src/core/dispatch/runLoop.test.ts::*::a failed run exposes only its original owner-bound write diagnosis on the readable failure event | | An authenticated state-write failure distinguishes promotion preflight, state RPC and invalid reply mapping through a closed diagnostic header bound to the complete original wire digest. Operator telemetry retains redacted exception messages, stacks, causes and Cloudflare error flags for every failed attempt, including recovered attempts. The Worker retries a Cloudflare-marked retryable RPC twice with the existing store backoff, but never overloads or ordinary application errors. State retries use the original owner and a captured canonical state guard: an already committed identical snapshot is acknowledged without another mutation, an unchanged prior state permits the write, and a moved state or owner refuses it. The shared client telemetry boundary retains validated diagnostic headers on HTTP400+ only when the digest matches the exact request, including the legacy state-header alias on /runs/state. State-specific uncertain-write diagnostics remain limited to /runs/state HTTP5xx. Telemetry never acknowledges a mutation or permits replay of an uncertain HTTP request. | [unit] src/core/runStateWriteDiagnostic.test.ts::closed state-write failure evidence::*; src/core/runLedger/writeFailureDiagnostics.test.ts::original uncertain write diagnostics::*; deploy/cloudflare-memory/runLedger.test.ts::state-write holds across Durable Object RPC::* | | Canonical archive callback errors do not reset the shared Runs object. Carry every callback error, including typed validation refusals and unexpected source or storage faults, out of the input gate as data, then reject the caller with the original error after the gate closes. Pending receipts, corrupt evidence and unrelated original ownership remain intact. A failed archive does not grant history or cleanup credit. | [unit] deploy/cloudflare-memory/runLedger.test.ts::keeps one rejected archive from resetting unrelated live ownership; deploy/cloudflare-memory/sessionUnification.test.ts::checkpoint input gate failures::keeps a real pending-source refusal outside the %s gate and preserves recovery; ::rejects an unexpected storage fault outside the gate and keeps the original checkpoint recoverable | | Native inbox consumption requires an original-run, current-owner, codec-version and exact-row witness plus a verified native input or control-transition receipt. A prompt submission, queue delivery, high-water cursor or unverified stream cannot dispose of raw custody or establish safe further execution. | [gap] Native receipt integration across the supported harnesses remains unproved; raw rows and uncertain commitments stay held until that consumer capability is verified. |
| Malformed native success flags cannot authorize the initial historical audit or create a PR. Compact canonical events pass runtime typed-field validation before standing attribution. | [unit] src/channels/adminCoordinator.test.ts::original committed head adoption — create-only draft PR::refuses malformed native success before the initial audit; deploy/cloudflare-memory/runLedger.test.ts::historical original native adoption owner CAS::admits a fourth verbose history and preserves standing, posting and ordinary ownership capacity |
| Criterion | Proof |
|---|---|
| An identity baseline binds the original run, requester, conversation, repository, branch, base, first full head and paired coordinator instance/step. Exact commit fingerprints remain bounded to 300 commits and 64 KiB. Restart matching permits an advanced attachment head but refuses a changed identity or target. Known, boundary and unknown states remain distinct; restoring unknown evidence cannot authorize publication, and a lost boundary refuses the identity check. | [unit] src/core/branchIdentityBaseline.test.ts::durable branch identity baseline::retains a typed*; ::retains exact old commit fingerprints and permits an empty commit message; ::refuses malformed baseline evidence*; ::refuses oversized fingerprints and commit ranges rather than dropping evidence; ::restores the original baseline after the current attachment head advances; ::refuses baseline substitution across its admitted binding*; ::keeps an inherited author only for its original fingerprint after restart; ::retains a boundary through restart and refuses a range that lost it; ::restores unknown evidence without authorizing an identity read or publication |
| The first valid identity baseline is immutable in the existing ledger state. Omitted patches restore its bytes; a changed head/fingerprint, unknown-to-known replacement or malformed prior/incoming evidence refuses the write. Its retained bytes share the existing aggregate work-evidence budget without weakening source checkpoint or seed guards. | [unit] src/core/branchIdentityBaseline.test.ts::identity baseline retention in checkpoint state::accepts the first valid baseline and restores it across omitted state patches; ::refuses a changed first head, changed fingerprint or unknown-to-known replacement; ::refuses malformed incoming or retained evidence without silently clearing it; ::charges the retained baseline against the existing aggregate evidence budget; ::retains the existing source checkpoint and unit seed guards |
| Both ledgers retain the acknowledged first identity baseline through native pushes, owner handoff and terminal archive. A foreign owner or replacement refuses the write; stale generations remain fenced. Full authorized records preserve exact fingerprints in private work evidence; summaries and ordinary run projections exclude them. Corrupt retained identity evidence refuses a full-record read. | [unit] src/core/runLedger/inMemory.test.ts::durable branch identity baseline::retains the first acknowledged baseline through push, restart and terminal archive while refusing replacement and foreign owners; deploy/cloudflare-memory/runs.test.ts::durable branch identity baseline::keeps the acknowledged baseline through SQLite owner handoff and terminal storage without exposing commit bytes in summaries; src/core/runStore.test.ts::* — RunStore contract::retains branch identity fingerprints privately across store reloads; src/core/runRecord.test.ts::binds private branch identity baselines to the exact run, requester, thread, repository and coordinator step |
Restart identity capture paths
| Criterion | Proof |
|---|---|
| Refused ownership admission reports its failing check and bounded row location without exposing private contents or changing the refusal or retained rows. Incomplete unit-write replies carry the existing closed read-diagnostic shape through the authenticated Worker client. Unknown or private extra fields are omitted; reason-only replies remain compatible. | [unit] src/core/coordinator/pullOwnership.test.ts::pull ownership refusal diagnostics::*, deploy/cloudflare-memory/runLedger.test.ts::coordinator admission diagnostics::*; src/core/coordinator/instanceStore.test.ts::pull-owner diagnostic reader::preserves only a closed diagnostic on an incomplete unit write |
| Resident, repository-matched seeded and verified cold coding checkouts capture the ref and full SHA from the same selected checkout and await the durable first baseline before writable execution. A seed from another repository refuses; an unbound model-cloned checkout remains unknown without reading a mutable ref. Legacy pushed seeded/cold runs without evidence remain held. | [unit] src/core/dispatch/runLoop.test.ts::a resume with the answer in hand (the \finish` plan)::restores an acknowledged*; ::awaits the frozen attachment read and its durable acknowledgment before opening writable tools*; ::keeps a legacy pushed*; ::refuses a seeded checkout from another repository before capturing evidence or opening writable tools; ::keeps an unbound model-cloned checkout unknown without comparing a mutable branch` |
| A standalone coding run persists its resolved nonprivate PR base in admitted metadata and restores that same base before matching identity evidence or publishing. Coordinator fallback applies only when admitted base metadata is absent; a conflicting resolved base does not replace the original receipt. Same-ID request restarts consume carried original identity evidence before writable tools rather than recapturing the advanced branch. | [unit] src/core/dispatch/runLoop.test.ts::a resume with the answer in hand (the \finish` plan)::restores a standalone non-main PR base and original fingerprints after restart; ::refuses a changed resolved PR base rather than replacing the saved identity binding; ::uses carried original evidence on a same-ID request restart before opening writable tools` |
| Same-ID reservation, promotion and repeated claim restore the first archived identity baseline before model tools. Replacements, corrupt evidence, foreign original targets and a substituted live baseline refuse at state/finish writes in both stores. Carried original context remains separate from the fingerprint receipt; truncated or uncertain history is refusal evidence, never publication authority. | [unit] src/core/runLedger/inMemory.test.ts::durable branch identity baseline::restores the archived first baseline*; ::refuses a foreign or corrupt archived baseline*; deploy/cloudflare-memory/runs.test.ts::durable branch identity baseline::restores private archived baseline*; ::fails closed on corrupt or foreign archived baseline*; src/core/dispatch/reattach.test.ts::*::carries raw original baseline and target*; ::*::keeps unknown prior history*; ::*::restores the original same-ID repository context*; src/core/dispatch/run.test.ts::*::promotes a same-ID segment with its raw original baseline* |
| Canonical saved branch receipts, PR publication receipts and checkpoint/publication settlements are checked by attachment ref before any initial capture. Missing scalar/display projections or an auxiliary last push cannot erase earlier owned writes; malformed/incomplete or unresolved producer evidence holds capture. Original acknowledged baseline restoration precedes this refusal-only history check. | [unit] src/core/branchIdentityHistory.test.ts::identity capture after retained publication::*; src/core/dispatch/runLoop.test.ts::a resume with the answer in hand (the \finish` plan)::refuses a receipt-only legacy*; ::does not read an advanced attachment from canonical history*; ::restores an acknowledged*| | A boot-reclaimed attaching same-ID successor restores its archived baseline and independently admitted repo/ref/base through the same admission fields as a request restart. Raw malformed evidence, missing targets and gapped history stay refusal-only; no advanced-head capture repairs missing original proof. |[unit]src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::a boot restart of an attaching same-ID successor restores the archived baseline and original PR base before model admission; src/core/dispatch/reattach.test.ts::::carries raw malformed baselines and independently admitted target without deriving it from the receipt; ::::keeps an owned push despite a later auxiliary push and holds recapture on missing or malformed history` |
| Canonical identity-capture refusal survives every same-ID request restart: boot attaching rows, workspace loss before loop entry, and a live harness interruption. It is computed before closing the predecessor; missing original acknowledged proof on an interrupted coding run is never a new first attachment. Contiguous display events or an auxiliary last ref cannot erase canonical writes or unknown producer/Door effects. A valid original baseline restores before refusal hints. | [unit] src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::workspace-loss request restart retains canonical owned-branch refusal before advanced-head capture; src/core/dispatch/reattach.test.ts::*::carries canonical publication refusal with contiguous events and no owned display projection; src/core/dispatch/runLoop.test.ts::a resume with the answer in hand (the \finish` plan)::carries identity refusal through a live request interruption*; ::uses carried original evidence on a same-ID request restart before opening writable tools` |
Final admitted profile
A finished run retained in the live registry exposes its admitted profile from the persisted record, just as the cold stored read does. A missing record or a legacy record without a profile remains unknown; this adds metadata behind the existing authorized run read, not an authority route.
| Criterion | Proof |
|---|---|
| Fresh and cold final-run reads carry the same persisted admitted profile | [unit] src/core/runsService.test.ts::RunsService.getRun::finished registry projection exposes the persisted admitted profile |
| Exact private qualification is opt-in with both diagnostic and qualifyRecord. A terminal initial-coding ownership refusal may expose only its canonical run key, closed failed predicate and target/owner/dependency digests. Canonical decoded wrapper, merged owner projection and matching unit/instance plus already-stored relevant audit events are snapshotted in the owner transaction before hashing; deterministic code-unit order allows exact named-read verification. Malformed owner identity yields an unbound dependency observation, never invented empty ownership. Unsafe/live source keys, other refusal stages, malformed/foreign payloads and hash failures yield no locator. Default and anonymous results, authentication, scan bounds, source bytes, owner/admission/Review gates remain unchanged. A stale/missing locator gives no absence, retry, release, ending, continuation or ACK authority; named readbacks must match the snapshot. Existing unreadable full-record/audit routes remain a proof gap; no new audit is prepared. | [unit] src/core/coordinator/pullOwnerQualification.test.ts::private initial publication owner qualification::*; deploy/cloudflare-memory/runLedger.test.ts::private initial publication owner qualification in SQLite::* | | The authenticated pull-owner read optionally returns only closed scan stage/source/cause enums, ordinal position and bounded counters. JSON, shape, validation, storage and original row/byte-budget failures remain incomplete before target filtering; diagnostics neither expose identifiers/private bodies nor change owner decisions, projections, limits or canonical stored rows. A malformed optional diagnostic is omitted without clearing an incomplete lookup. | [unit] deploy/cloudflare-memory/runLedger.test.ts::read-only pull-owner scan diagnostics::*; deploy/cloudflare-memory/runLedger.test.ts::pull-owner diagnostic validation and read stages::*; src/core/coordinator/instanceStore.test.ts::pull-owner diagnostic reader::*; src/core/coordinator/instanceStore.test.ts::pull-owner diagnostic default compatibility::*; src/core/coordinator/pullOwnership.test.ts::closed pull-owner read diagnostic data::* |
| Ownership validation on merged live/terminal run rows reports the original physical live_runs/runs source and its actual source-local ordinal. Ephemeral diagnostic provenance contains only the closed source enum and ordinal; it does not change queries, owner decisions, scan limits, defaults, privacy or retained rows. Pre-merge inventory failures keep their original physical positions, successful and default reads omit diagnostics. | [unit] src/core/coordinator/pullOwnership.test.ts::source-local run diagnostic provenance::*; deploy/cloudflare-memory/runLedger.test.ts::pull-owner source-local diagnostic provenance::* |
| Heartbeat and pushed provider steers use the same closed durable decoder before queue delivery; malformed target, version, principal or sequence stays deferred. Valid provider controls render canonical text and preserve durable-sequence idempotence. | [unit] src/core/runLedger/writeThrough.test.ts::events, state, heartbeat::heartbeat provider control renders canonical text instead of persisted display bytes; src/core/runLedger/writeThrough.test.ts::events, state, heartbeat::heartbeat steers reject malformed version, target, principal and sequence before queue delivery; src/core/runLedger/writeThrough.test.ts::events, state, heartbeat::an owner heartbeat delivers a live steer once; duplicate heartbeat and reclaim copies of its durable sequence are skipped |
| Reclaim returns the full original inbox payload with its exact store-owned destination and producer generation; it changes the live owner without replacing custody. Abandoning an attaching row preserves opaque bytes, and an incremental inbox read retains the same custody stamp. | [unit] src/core/boot.test.ts::reclaimRuns::a run whose transcript and last step record the completeness rule accepts is handed to the launcher untouched — row, steps, transcript and events all still there; deploy/cloudflare-memory/runLedger.test.ts::run ledger — claim and admission (item 29)::a claim with*; deploy/cloudflare-memory/runLedger.test.ts::run ledger — steps, events, inbox, state (items 30–31)::inbox appends with increasing seq from any generation; state replaces; both refused for an unknown run |
| Provider recovery fixtures cross the actual Memory park/up transaction, canonical row and outbox, owner fence and effect acknowledgement before local fake-pi delivery. Original release timing, prompt counts, positive cursor and deferred ordinary rows remain proven; raw rows survive local cursor filtering and owner closure. External plane rows and unversioned pushed effects refuse. | [unit] src/core/harness/pi/harness.test.ts::runPiHarness — a run on pi from the first file to the answer::a temporary failure during a follow-up turn parks and resumes on provider-up without ending the run; ::a provider-up buffered during a successful follow-up*; ::an ordinary follow-up the cursor*; ::a provider-up buffered during a successful main-loop*; ::provider-up rows buffered*; ::a provider-up effect pushed* | | A real Memory transaction followed by a fake-pi echo and local cursor checkpoint does not establish native durable consumption or an irreversible-work guard. | [gap] Both native persistent-consumption protocols remain unverified; original raw custody is retained. |
| A resumed run delivers the readable snapshot and late follow-up once, preserves original input provenance and sends no second admitting acknowledgement. An unreadable carried row is reported pending and retains its full original bytes and producer custody after execution closes. | [unit] src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::a resumed run inherits the follow-ups the durable inbox holds* |
| Every concurrently uncertain state or step request keeps its own immutable witness and expected boundary. Same-owner exact reconciliation removes only the matching obligation; an unknown arriving during a read or hash remains held. Status, pause, abandonment and terminal landing wait for already-started state/step replies and reconcile original unknowns before changing or closing canonical readback. An unresolved terminal keeps the original request in the existing admission hold and leaves its owner unfinished. A closing attempt admits no new state/step mutation. Heartbeat and phase-only transitions retain their ordinary behavior. No command replay or completion credit comes from an unknown check intent or result. | [unit] src/core/runLedger/writeThrough.test.ts::uncertain canonical boundaries::retains overlapping originals*; ::does not erase an already-started unknown write*; ::keeps an unknown step held*; ::continues ordinary state and step writes*; ::terminal landing*; ::* preserves an original unknown state*; src/core/checkExecution.test.ts::recorded checks with uncertain canonical ACK::* | | Terminal cleanup retains a private closed-segment delivery boundary in the existing step carrier, separate from public steps and consumption receipts. A same-ID zero seed cannot re-admit prior ordinary, provider or opaque rows; raw bytes remain original. Deferred handoff and positively undelivered attaching inputs preserve eligibility. | [unit] src/core/runLedger/inMemory.test.ts::closed inbox segment admission::*; deploy/cloudflare-memory/runLedger.test.ts::closed SQLite inbox segment admission::* | | Live-state assignments and context normalization serialize their canonical state mutation with state writes and join terminal settlement for their whole call. An uncertain reply retains its actual immutable request in the existing boundary carrier. Live-state reconciliation requires the original closed projection, known sequence and encoded state patch; an unknown server-chosen sequence remains held. Normalization accepts only a committed same-owner receipt matching the captured original session range, before hash, revision and all input hashes. Pending or foreign receipts grant no credit; readback never replays normalization or invokes checkpoint recovery. | [unit] src/core/runLedger/checkpointMutations.test.ts::adjacent canonical mutation closure::* | | An unknown terminal finish or fallback retains its complete original request and record in the current process. Same-ID and already-waiting claims resolve held, never untracked permission; separate fallbacks retain the original landing until their known outcome. A one-shot unknown close propagates its hold and yields no lost-workspace replacement request. | [unit] src/core/runLedger/writeThrough.test.ts::unknown terminal admission hold::*; src/core/dispatch/admission.test.ts::closeResumedRow / closeRestartRow — one attempt, the final word said (item 54)::propagates an unknown one-shot close*; src/core/dispatch/reattach.test.ts::abandonLostWorkspace: the resumed run closes saying why, and hands its request back::does not return the original restart request*; src/core/runHistoryWriter.test.ts::unconfirmed history commitment::* | | An authorized named steer preserves its source route and credential separately from canonical target custody. With persistence enabled, its durable sequence precedes local delivery; a refused or unknown push delivers no local-only input. Explicit NullLedger persistence-off permits only the exact live local target, with no durable sequence or elsewhere claim. Cross-destination private audience authority, foreign targets and external plane controls remain refused. | [unit] src/core/runLedger/inboxMessage.test.ts::durable inbox continuity::binds the canonical destination*; ::does not carry private audience authority*; src/core/dispatch/admission.test.ts::steerRun — a run steers a live run through the inbox a thread reply takes::does not deliver a named steer only in memory*; ::preserves explicit ledger-off*; ::does not invent local or remote delivery*; ::does not turn an unknown inbox push*; src/core/dispatch/admission.test.ts::*::durably delivers an authorized cross-channel*; deploy/cloudflare-memory/runLedger.test.ts::cross-channel named steer custody::* |
Terminal completion and recovery
An acknowledged ended assignment commits its event, terminal result and finishing phase in the same ledger transaction. Execution cannot resume after this boundary. Channel delivery and workspace settlement remain separate obligations; completion does not prove either one. A same-owner finishing call confirms this existing boundary, while a former owner stays fenced. Recovery settles older ended rows without starting another model run, preserves their terminal status and completion time, and leaves delivery unknown without a receipt.
| Criterion | Proof |
|---|---|
| Ended state and the no-replay phase commit together; a rejected or failed write changes neither | [unit] src/core/runLedger/inMemory.test.ts::InMemoryRunLedger::terminal completion atomically fences replay and preserves the result through reclaim; deploy/cloudflare-memory/runLedger.test.ts::run ledger — steps, events, inbox, state (items 30–31)::terminal completion atomically fences replay and preserves the result through reclaim |
| The model loop commits its terminal result before returning to channel delivery | [unit] src/core/dispatch/runLoop.test.ts::runLoop — the model turn and everything that rides on it::commits the execution result with ended before channel delivery |
| Recovery closes an ended run without replay and retains its result, time and unknown delivery, including rows written before the atomic completion boundary | [unit] src/core/boot.test.ts::reclaimRuns::settles terminal completion without resuming execution or claiming delivery; src/core/boot.test.ts::reclaimRuns::settles legacy ended rows from their recorded cause instead of resuming them |
| An ended row with an unreadable terminal event stays owned and starts no replacement | [unit] src/core/boot.test.ts::reclaimRuns::retains an ended row when its terminal event cannot be verified |
| Reclaimed terminal records retain submitted review, findings, handoff, research and final-head artifacts; review publication comes from canonical evidence and research requires the matching canonical submission | [unit] src/core/dispatch/record.test.ts::reclaimed terminal artifacts::* |
| A legacy local soft stop without a ledger stop flag keeps its actual stopped outcome from the terminal stop event | [unit] src/core/boot.test.ts::reclaimRuns::recovers a legacy local soft stop from its actual stopped event |
| A stop request alone cannot prove its executed mode; a stopped boundary without mode evidence remains held | [unit] src/core/boot.test.ts::reclaimRuns::does not infer an executed stop from a request without its stopped event |
| Typed outputs survive legacy live, handoff and finishing recovery; a hosted terminal parent keeps its outcome and summary without rehosting | [unit] src/core/boot.test.ts::reclaimRuns::preserves typed terminal outputs when recovering the …; src/core/boot.test.ts::reclaimRuns::settles an ended hosted parent as … |
| Real SQLite recovery archives accepted or uncertain reviews, findings, empty handoffs and canonical research with their original result and context facts | [unit] deploy/cloudflare-memory/runLedger.test.ts::run ledger — steps, events, inbox, state (items 30–31)::archives original … |
The run's setup result
A run that asked for a workspace records its setup result (RunRecord.setup, record 0097): what its repository and profile requested (resident, cold or blank), and, when it got something else, a fallback word (a sandbox stood in for a resident, with how the sandbox was filled) or a lost word (no workspace). The words are in src/core/runSetup.ts. The factory decides a fresh selection's result from its own branch and the resident's typed refusal code, never from the note (ExecutorSelection.setup). The claim writes it on the row beside the binding, so it is durable before the first model turn; a same-id restart's claim marks it reallocated. A failed attach names a lost word from the error's type (lostWordOf in src/core/dispatch/provision.ts) on the setup failure record. A resumed run keeps the result its row was claimed with, and a reclaimed row closes with it. One strict decoder (runSetupOf) reads the row, the reclaimed state and the stored record; a shape whose fields disagree is dropped, never stored. A run that asked for no workspace has no setup result. On a local or E2B deployment the result also names its backend, so it never reads as a Cloudflare sandbox. Beside it, RunRecord.targetPr carries the pull request number the run was admitted to act on (the repository context's, before the model starts; the reclaimed row's meta.pr), distinct from pr, the one a coding post-step opened.
| Criterion | Proof |
|---|---|
| The setup result's fields agree with each other: a fallback word only for a requested resident, seeding only with a fallback word, a restore wait only without a reason; unknown words and extra keys are refused | [unit] src/core/runSetup.test.ts::runSetupOf::round-trips every shape the appendix lists; src/core/runSetup.test.ts::runSetupOf::refuses shapes whose fields disagree, unknown words and extra keys |
| A run's request is read the way the factory provisions it: a resident only with a repository and a resident backend | [unit] src/core/runSetup.test.ts::requestedWorkspaceOf::reads the machine class the way the factory provisions it |
| Every fresh selection names its result: a warm or restored resident, each fallback with its seeding, a non-serviceable, unreachable or unregistered resident, the cold and blank classes, a local or E2B backend; an agent with no workspace has none | [unit] src/execution/factory.test.ts::the setup result on the selection (record 0097)::* |
| The claim persists the result beside the binding, reallocated on a same-id restart | [unit] src/core/dispatch/run.test.ts::claimRun — the ledger claim once the prompt exists::claims the selection's setup result beside the binding (record 0097), reallocated when the claim restarts a run |
| A failed attach names its lost word from the error's type, carried on the error and on the ask-once and stop outcomes | [unit] src/core/dispatch/provision.test.ts::a lost workspace's setup result::* |
| The finished record and the setup failure record carry the result; the record validates it | [unit] src/core/dispatcher.test.ts::run history write path::the record carries the setup result its attach claimed, and a failed attach's lost word (record 0097); src/core/dispatch/record.test.ts::assembleRunRecord — the handoff on the record::carries the setup result and refuses one whose fields disagree (record 0097) |
| The record carries its target pull request number and refuses one that is not a positive integer | [unit] src/core/dispatch/record.test.ts::assembleRunRecord — the handoff on the record::carries the pull request the run was admitted to act on, and refuses one that is not a positive integer (record 0097) |
| A reclaimed row closes with its result and target pull request; a malformed result is dropped | [unit] src/core/dispatch/record.test.ts::private context refusal receipt on the finished run::closes a reclaimed run with its row's setup result and target pull request, and drops a malformed setup result (record 0097) |
| Criterion | Proof |
|---|---|
A tool recovery ending records the closed failure kind tool_recovery_exhausted. Its retained guard survives reconstruction through the existing state field; a terminal reason or unavailable checkpoint buys no further model call. | [unit] src/core/runRecord.test.ts::isRunRecord — the failure field::accepts named provider and local stream failures after a JSON round-trip; a record without one carries no key; src/core/harness/toolRecovery.test.ts::run-owned tool recovery::retains the allowance and settled call identities across reconstruction; ::fails closed on refused checkpoints and malformed retained state |
| An operator event keeps the accepted preset identity separately from its redacted display line. A typed saved choice retains its selected preset, safe task and accepted settings. Display changes do not change the accepted action; raw registry arguments remain private execution data. | [unit] src/core/dispatch/operator.test.ts::publication of an operator decision::executes the accepted typed preset when display text names another preset, ::typed action limits and saved choices::a saved typed question keeps its action, task and settings when the display line changes |
| Criterion | Proof |
|---|---|
| Run-record reconstruction retains the typed preset, task and saved-question identity. A rejected or uncertain final review records failure with no current verdict, retaining the authorized report and safe incomplete reason; raw submission arguments remain private state. | [unit] src/core/runRecord.test.ts::saved operator choices keep their typed identity and task through run-record reconstruction, src/core/dispatch/runLoop.test.ts::a resume with the answer in hand (the \finish` plan)::a latest …` |
Storage failure telemetry
One shared contract owns storage failure evidence: the digest-bound header and closed server failure validator, storageFailureNotice, the storage_failure run event, and the existing privacy and retention rules. Exception and validation-rejection producers use the same Worker log emitter. The state-only header is a compatibility alias; state RPC guards and recovery remain operation-specific and never broaden replay authority.
Storage diagnostics describe observed failures. They never acknowledge a mutation, permit replay, or grant recovery or completion credit. The shared ledger request boundary captures reads and writes before an action-specific handler can swallow the exception. Request identity and digests come from the frozen wire bytes. Redacted exceptions carry bounded stacks, causes and platform flags; request and response bodies remain excluded. Decoder exceptions that can quote response bytes are excluded. Operator logs remain available when event storage is unavailable or the run has already ended. Failed diagnostic-only appends log without generating another diagnostic event.
Client failures also retain elapsed time, the configured HTTP deadline and whether that deadline signal fired. A fresh transport request ID distinguishes identical wire payloads. State, heartbeat and append RPC progress logs join that ID to the exact digest, original run, owner generation, serving build, stage and attempt. Each call logs before awaiting RPC and after completion or failure. A completed RPC is an observed return, not a mutation receipt. Missing completion logs leave the outcome unknown; retention or sampling gaps do not prove a call never reached the Worker. These observations do not change retry policy.
The base-config and runtime-override clients use the same failure notice for /config/get and /config/put. Their request start time is distinct from the time a later report is saved. Elapsed time and deadline state are frozen at the failure boundary before asynchronous diagnostic work. Response metadata uses closed content-type and observed-body-format values; an unreadable or unread body is explicit. HTML or JSON alone does not prove whether an edge service or the Worker answered. Bodies, arbitrary headers, document keys and credentials stay out of notices. A diagnostic sink or request-ID failure cannot change the original result or cause a retry.
The shared response decoder retains only the closed body-format tag against the original response. Diagnostics use that tag without reading the body again. An externally consumed response without decoder evidence is unknown, not not-read. Runtime overrides also report an unreadable successful response while keeping their existing empty-document result.
| Criterion | Proof |
|---|---|
| Config denials retain request time, wire identity, Ray ID and response format without response bytes; unreadable bodies preserve available HTTP evidence, and telemetry failures leave writes unknown without replay | [unit] src/configStorageDiagnostics.test.ts::config storage failure evidence::* |
| Interrupted and decoded ledger responses keep their observed format; an externally consumed response without original decoder evidence is unknown rather than unread | [unit] src/core/runLedger/writeFailureDiagnostics.test.ts::original uncertain write diagnostics::retains body evidence from the original decoder without reading a ledger response twice; src/core/storageFailureDiagnostic.test.ts::storage diagnostic captures::does not label an externally consumed body as unread when no decoder evidence was retained |
| A caught checkpoint state failure retains its remote exception, request identity, server stage and Ray ID without credentials or request/response bodies | [unit] src/core/runLedger/writeFailureDiagnostics.test.ts::original uncertain write diagnostics::reports a caught checkpoint exception with its original request and redacted remote cause |
| Transport and response-body timeouts retain elapsed time, the configured deadline, its observed signal state and the transport request ID through exported run captures, without replay | [unit] src/core/runLedger/writeFailureDiagnostics.test.ts::original uncertain write diagnostics::retains elapsed time… |
| Route-specific malformed acknowledgments retain the original transport ID and timing even when identical payloads share a digest; their write outcomes remain unknown with no replay | [unit] src/core/runLedger/writeFailureDiagnostics.test.ts::original uncertain write diagnostics::distinguishes identical requests with malformed acknowledgments by their original transport observations |
| Pending state, heartbeat and append RPCs are visible before they return; completion joins the same transport ID and exact digest without payload bytes | [unit] deploy/cloudflare-memory/runLedger.test.ts::state-write holds across Durable Object RPC::exposes the pending… |
| The shared diagnostic boundary retains a recovered read transport failure and a refused malformed write acknowledgment | [unit] src/core/runLedger/writeFailureDiagnostics.test.ts::original uncertain write diagnostics::captures read transport failures and malformed write acknowledgments at the shared boundary |
| A failed diagnostic append or throwing telemetry sink neither creates a write loop nor replaces the storage error | [unit] src/core/runLedger/writeFailureDiagnostics.test.ts::original uncertain write diagnostics::does not turn failed diagnostic appends or a throwing telemetry sink into another write |
| Storage evidence survives bounded live backlog and final record trimming and counts as infrastructure friction | [unit] src/core/runRegistry/backlog.test.ts::storage diagnostic retention::retains a storage exception through backlog trimming and final record assembly |
| Authenticated non-state run routes return the same digest-bound redacted remote exception without retrying an uncertain mutation; legacy state headers remain compatible | [unit] deploy/cloudflare-memory/runLedger.test.ts::state-write holds across Durable Object RPC::captures non-state RPC exceptions from… |
| The timeline shows the redacted exception; exported diagnostics retain bounded causal evidence and reject unknown fields without discarding legacy notes | [unit] src/core/storageFailureDiagnostic.test.ts::storage diagnostic captures::* |
| Source saves, confirmations, allocations, cancellation readbacks and batch handoffs correlate only explicit wire run identities; key-only requests remain unbound | [unit] src/core/storageFailureDiagnostic.test.ts::storage diagnostic captures::correlates every explicit wire run identity without guessing a key-only request owner; src/core/runLedger/writeFailureDiagnostics.test.ts::original uncertain write diagnostics::keeps the source-save owner visible when the session write fails |
| Private main diagnostics retain their structural failure classification and correlation facts without exception prose; failed diagnostic appends cannot generate another append | [unit] src/core/runLedger/writeFailureDiagnostics.test.ts::original uncertain write diagnostics::keeps private diagnostic appends non-recursive after registry projection without exposing exception prose | | Retained JSON failures use a fixed SyntaxError before RPC; authenticated HTTP diagnostics and operator logs exclude malformed stored bytes while the original rows remain intact | [unit] deploy/cloudflare-memory/runLedger.test.ts::state-write holds across Durable Object RPC::keeps corrupt retained inbox bytes out of HTTP diagnostics and preserves the original SQLite row; ::does not disclose corrupt retained session metadata through real RPC | | Capture and header validation exclude direct and nested decoder messages and stacks while retaining error identity and platform flags | [unit] src/core/runStateWriteDiagnostic.test.ts::closed state-write failure evidence::excludes direct and nested decoder prose from captures and remote headers while retaining error identity | | Machine history recursively fences local and remote exception messages, stacks and causes without changing typed facts | [unit] src/core/commands/runs.test.ts::runs.get / runs.events / runs.friction::machine history fences local and remote storage exception prose recursively without changing typed facts |
| Machine fencing preserves bounded local, remote and nested exception details, including repeated fence markers, when a capture is read back; oversized source text remains invalid | [unit] src/core/commands/runs.test.ts::runs.get / runs.events / runs.friction::keeps bounded exception evidence when a fenced machine capture is read back |
| Authenticated append rejections for the events array, sequence or individual event size carry a closed reason and the exact wire digest through Worker, client and retained run history; rejected payloads stay excluded and no replay occurs | [unit] deploy/cloudflare-memory/runLedger.test.ts::append rejection diagnostics::* | | Closed append rejection evidence survives private serialization, live trimming and final record assembly | [unit] src/core/runLedger/writeFailureDiagnostics.test.ts::original uncertain write diagnostics::retains a closed append rejection through private backlog trimming and final record assembly |
| Failed digest, diagnostic-header or operator-log capture emits one safe fallback stage and error class without exception prose; a failed fallback sink preserves the original rejection | [unit] deploy/cloudflare-memory/runLedger.test.ts::append rejection diagnostics::reports a safe fallback…; ::preserves the original rejection when the fallback log sink also fails | | Legacy cancellation permits incomplete target discovery only when Resident selection was recorded. A new Cloudflare native read-only Resident review records its Resident namespace and dedicated fallback sandbox key at first admission. The original client declares that isolation contract; older clients cannot claim or reclaim it. Before binding, one cancellation ticket closes both targets and finishes only after both acknowledge. The completed ticket remains in the original archive after history removal and refuses any claim that reuses that run ID. Old archives cannot gain these targets from later metadata. A foreign target or unknown reply retains the fence. | [unit] deploy/cloudflare-memory/runLedger.test.ts::original workspace durability in real SQLite::prepares only recorded Resident discovery and never treats an unknown target as absent; ::hard stop prepares the original native review targets before selection and fences late attachment; src/core/runLedger/originalPromotionHold.test.ts::original promotion hold diagnosis::hard stop closes both original native review targets before backend selection: refusal=%s; src/core/runLedger/workspaceDurability.test.ts::native execution first admission::retains original review stop targets without retrofitting an older admission; src/core/dispatch/provision.test.ts::original allocation before first reservation::records early stop targets only for the original Cloudflare backend: %s; src/core/dispatch/admission.test.ts::admit — the thread admission claim::holds an original isolated review instead of resuming it on another backend |
| Routing timeout and provider refusals seal failed door records; policy/request refusals remain completed decisions. Historical timeouts produce a deadline finding even without phase telemetry. | [unit] src/core/dispatch/commandRun.test.ts::recordRefusal — every refusal is a run record (record 0054, as amended)::persists routing timeouts and provider refusals as failed while policy refusals complete | | Record reconstruction and JSONL parsing retain routing phase receipts, timeout identity and output-count estimate provenance. | [unit] src/core/runRecord.test.ts::retains routing phases and estimate provenance through record and JSONL reconstruction; src/core/runEventLines.test.ts::parseRunEventLines::keeps non-decision routing receipts and their phase telemetry | | Machine history fences routing question/proposal/rejection text as untrusted evidence while preserving timing, validation codes and request IDs. Stored receipts stay intact. | [unit] src/core/commands/runs.test.ts::provider diagnostics in machine history::fences routing question and rejection evidence while retaining typed timing |
Private recorded check custody
Final close, hard-stop completion and unclaimed restart closure retain bounded canonical checks and the full authorization resource from the fenced live row in private evidence. Submitted history cannot mint or replace it. Restart snapshots remain unsealed, the next owner preserves original call identities, and final close captures its latest state. Malformed checks and combined record/evidence byte overflow refuse before deleting live evidence. Public run projections do not gain check output. Existing history retention applies; missing source remains unavailable.
| Criterion | Proof |
|---|---|
| Final capture uses canonical private authority, refuses caller archives and malformed source, and replaces an unsealed restart snapshot with the final owner state. | [unit] src/core/runLedger/checkInspection.test.ts::canonical check terminal custody::*; src/core/runLedger/inMemory.test.ts::cancelled check terminal custody::*; [unit] deploy/cloudflare-memory/runLedger.test.ts::recorded check terminal custody in real SQLite::*; deploy/cloudflare-memory/runLedger.test.ts::cancelled check terminal custody in real SQLite::* |
| Provisional puts never freeze an early check snapshot. Actual UTF-8 private evidence is counted with the record; overflow refuses without finishing or deleting original unknown evidence. | [unit] deploy/cloudflare-memory/runLedger.test.ts::recorded check terminal custody in real SQLite::*; deploy/cloudflare-memory/runLedger.test.ts::cancelled check terminal custody in real SQLite::* |
| Abandon only removes an unstarted reservation. Any recorded command, preflight or malformed check evidence refuses before deleting the original live row; absent or strictly empty check state remains allowed. | [unit] src/core/runLedger/inMemory.test.ts::recorded check abandon custody::*; deploy/cloudflare-memory/runLedger.test.ts::recorded check abandon custody in real SQLite::* |
PR identity belongs to work
A conversation can contain several work items and PRs, including PRs in different repositories. The existing unit row retains its PR relation after a run ends. Active publication ownership can end separately. Each unit binds at most one PR number across its PR, resume, publication and adoption fields. A write cannot change that identity by retaining the old number in a different field. Legacy rows remain readable; contradictory target changes refuse. A fresh unit can bind its first PR through the existing accepted native publication path.
| Criterion | Proof |
|---|---|
| One work item cannot substitute its PR through whole-row or compare-and-replace writes; distinct work items in one conversation can retain distinct PRs, including the same number in separate repositories. | [unit] src/core/coordinator/pullOwnership.test.ts::complete canonical pull ownership::keeps one PR per work item while another work item can bind another PR; deploy/cloudflare-memory/runLedger.test.ts::complete canonical pull ownership::refuses PR substitution without binding the whole conversation |
| Settled work retains its PR relation while releasing active ownership; a new admitted work item can own that PR. Head-specific publication facts can advance without changing PR identity. | [unit] src/core/coordinator/pullOwnership.test.ts::complete canonical pull ownership::retains the PR relation after settlement and permits a new authorized work item; src/core/coordinator/maintenanceAdmission.test.ts::* |
| An explicit original work key or exact watch identity selects that canonical work among settled histories for the same PR. Unnamed ambiguity, active rivals and another unit's unknown review publication still refuse; exact replay reuses the selected effect without changing sibling history. | [unit] src/core/coordinator/maintenanceAdmission.test.ts::durable maintenance admission::selects the explicit … work among settled PR history and preserves rival authority; ::does not let explicit work selection bypass another unit's unknown review publication; deploy/cloudflare-memory/runLedger.test.ts::durable maintenance admission::selects an explicit … work item through settled SQLite PR history |
| A managed child derives its exact PR identity from the parent publication tag before prompt, operator or history resolution. A foreign parent or unit refuses. Restart retains that identity while fresh head verification can refuse a moved head. | [unit] src/core/dispatcher.test.ts::*::a managed review inherits its parent target before reading misleading child text; ::*::a managed child refuses a foreign …; ::*::a restarted coordinator review retains its original publication before resolving a moved head |
| Parent target inheritance retains the existing resource admission path; it cannot opt a historical execution into a new prospective allocation protocol. | [unit] src/core/dispatch/provision.test.ts::original allocation before first reservation::retains a parent PR target without changing its resource admission protocol; src/core/dispatcher.test.ts::*::a canonical child retains its original profile and absolute budget through relaunch fallback |
| A legacy PR-bound coding child promotes under its original saved operation target while resolving the parent PR. Parent-derived resolution cannot add PR fields to its reservation. | [unit] src/core/dispatcher.test.ts::*::a legacy PR-bound coding child promotes with its original reservation and parent PR |
Original managed review publication custody
The canonical review journal records the original native POST target. A pending or uncertain managed review remains attributable to its original run and unit after that unit ends. A new head, terminal status or another request does not settle the publication. Owner reads retain this original obligation before a new managed effect or sibling work can be admitted. Observation and settlement of the original unit effect still use its existing fence. Accepted, refused and absent journals release this publication obligation; other active unit, effect and workspace obligations remain independent.
The terminal candidate index routes a review by repository and PR. A conservative repository candidate also loads it for ref-only requests; the canonical original unit determines whether that exact ref is held. The index version changes with the projection, and old coverage is rebuilt from stored journals within the existing limits. Invalid managed receipts remain incomplete before target filtering. No private transcript or review body enters this projection.
| Criterion | Proof |
|---|---|
| An original uncertain review remains held after unit settlement and head movement. | [unit] src/core/coordinator/pullOwnership.test.ts::complete canonical pull ownership::retains an original managed review's uncertain publication after its unit settles |
| Original effect settlement works while a new managed review, sibling unit or distinct-thread maintenance request refuses. Restart retains the original run and unit. | [unit] src/core/coordinator/managedReviewOwnership.test.ts::managed review publication ownership::retains … publication without blocking the original effect's observation; deploy/cloudflare-memory/runLedger.test.ts::complete canonical pull ownership::retains the original managed review's … receipt across settlement and index upgrade |
| A historical carried-head receipt can name a different commit from the original checkout. New publication cannot reinterpret or resend it. Its known accepted/refused outcome remains readable, releases the original obligation, and allows a new authorized intent to reuse that unit | [unit] src/core/coordinator/managedReviewOwnership.test.ts::managed review publication ownership::retains a known historical carried-head … receipt without republishing it |
| Independent manual reviews and display-only failure do not acquire a shared PR claim. Changed canonical sources invalidate cached absence; malformed managed sources remain incomplete. | [unit] src/core/coordinator/managedReviewOwnership.test.ts::managed review publication ownership::does not turn an independent manual review or display-only failure into a shared PR claim; ::invalidates cached absence when the retained canonical review source changes; ::keeps malformed managed receipts unreadable before target filtering |
| Exact original maintenance replay can settle its accepted push/spawn while the child review publication remains unknown. The normal complete-empty branch projection does not block this replay; an independent pending branch write still does. It cannot repeat native calls or grant a different action. Live and terminal SQLite replay retain the receipt through old index backfill. | [unit] src/core/coordinator/maintenanceSweep.test.ts::durable maintenance sweep integration::replays only the original accepted maintenance child with a … unknown review receipt; deploy/cloudflare-memory/runLedger.test.ts::durable maintenance admission::reattaches only the original maintenance action to its … accepted review child | | A legacy PR/resume relation can retain its original review obligation without creating publication write authority. Conflicting original actor, unit or present publication repository remains unreadable. | [unit] src/core/coordinator/managedReviewOwnership.test.ts::managed review publication ownership::retains a legacy unit's review obligation without granting publication authority; ::refuses a retained review attributed to a different original … | | Acknowledged orchestrator selections survive normal finish and terminal recovery outside the bounded event log. Full records validate their original owner. Invalid-present history rejects terminal recovery and runtime snapshots instead of becoming an empty history. Terminal recovery retains its original live row. Run summaries and public run views omit this internal history. Retained source bodies and target names are not part of worker decision metadata. | [unit] src/core/dispatcher.test.ts::dispatch::uses the configured pilot repository for a later main-run fix…, ::records commands and linked-worker changes in one original conversation history; src/core/dispatch/record.test.ts::reclaimed terminal artifacts::retains acknowledged orchestrator decisions after terminal recovery without an event backlog; src/core/boot.test.ts::reclaimRuns::keeps invalid-present orchestrator history on its live row…; src/core/dispatch/orchestratorDecisions.test.ts::orchestrator decision records::holds malformed or unsupported history…; src/core/runStore.test.ts::*::archives source bodies only in full records and preserves the dependency references in summaries; src/core/runsService.test.ts::*::never exposes internal source archives or handoff context through any persisted projection; [worker] deploy/cloudflare-memory/runs.test.ts::retained source archive storage::retains orchestrator decisions in full records without exposing them in summaries | | The decision writer captures the full supplied decision before entering its asynchronous queue. Later caller changes cannot replace source, target or context metadata, including behind an earlier save. It retains the original save callback and receiver; an uncloneable proposal is refused with a closed diagnostic that contains no proposal or exception data. A failed diagnostic sink does not block a later valid decision. This does not validate the caller's canonical source or task. | [unit] src/core/dispatch/orchestratorSourceFold.test.ts::orchestrator source fold decisions::captures the complete decision before its first queued turn; ::keeps a waiting source decision fixed behind an earlier save; ::retains its original save callback while the write is queued; ::rejects an uncloneable proposal without blocking a later valid decision | | Work-decision construction captures supplied source, call identity, action, input and context before its first validation or hash await. The dispatcher uses this writer. Current validation remains before and after the owner-fenced save, and a later refusal does not erase the saved selection. Uncloneable input stays refused without blocking later valid work. Stable proposal data alone does not prove canonical source/task state or effect authority. | [unit] src/core/dispatch/orchestratorDecisions.test.ts::work decision construction::captures the complete proposal before asynchronous context validation; ::keeps later fields fixed while an earlier field hash awaits; ::keeps validation before and after the owner-fenced save; ::refuses an uncloneable work proposal without blocking a later valid decision; src/core/dispatcher.test.ts::dispatch::uses the configured pilot repository for a later main-run fix…; ::records commands and linked-worker changes in one original conversation history |
| Criterion | Proof |
|---|---|
Inbox append responses separate not-live from invalid-message. The shared sequence reader returns a sequence for a valid append and absence only for not-live; storage failure, invalid messages and legacy unqualified rejections throw. The CLI and bot use this same reader. Unknown writes retain their original error and request witness without a second append. | [unit] src/core/dispatch/admission.test.ts::admit — the thread admission claim::holds a…; src/core/dispatcher.test.ts::run ledger write-through (docs/reference/specs/run-history.md item 35)::an unavailable inbox keeps the remote work owner without starting another agent; src/core/runLedger/writeThrough.test.ts::*::the step record carries the inbox seq the run has consumed; pushInbox distinguishes delivery, target absence and storage failure; src/core/runLedgerWorker.test.ts::durable ledger acknowledgement continuity::retains an inbox rejection's target-state reason…, ::keeps a committed inbox append with a lost body unknown without a second append; [worker] deploy/cloudflare-memory/runLedger.test.ts::run ledger — steps, events, inbox, state (items 30–31)::inbox appends with increasing seq from any generation; state replaces; both refused for an unknown run |
| Relay attribution retains the person, bound credential and relay app as distinct fields in registry, remote ledger, saved run views and provisional drain records; absence stays absent | [unit] src/core/runsService.test.ts::RunsService.getRun::preserves relay attribution beside the person and credential in live, ledger and saved views; src/core/dispatch/record.test.ts::the drain deadline's records::preserves the relay app separately from the requester and credential in an abandoned run; src/core/dispatch/provision.test.ts::registerRun — the run's row on every surface before the attach::publishes a newly admitted run under its original ingress owner's identity |
Shared source-event reception
The existing ledger accepts bounded source data before a producer can be enabled. An event has separate source, provider delivery, native object and change IDs. Its kind is a request, source evidence or a typed source fact. The event contains no trusted worker destination or requester grant. Receipt sequence numbers describe local reception order, not upstream completeness.
The state worker saves the event, its first pending job and the next shared alarm inside one asynchronous SQLite storage transaction. The reference ledger provides the same reception and replay results. A receipt confirms reception only. It does not confirm relevance, routing, internal delivery, consumption or an external effect.
Repeated delivery returns the original receipt before checking queue capacity or decoding the job. It cannot reset a completed or unknown future job. Different content under the same source and delivery ID is refused; a content hash does not determine event order. An unreadable stored event or job is held, not treated as absent.
Reception permits at most 400 KiB of canonical event JSON, 1,024 retained events and 32 MiB of retained event JSON per existing ledger partition. These are provisional limits for an inactive producer. A due job contributes a 30-second recovery wake to the existing shared alarm; an earlier shared deadline remains earlier. Source rows are independent of run ending and run cleanup. This cut has no source producer, job claim, execution loop or event-retention policy. Those must be qualified before enablement.
| Criterion | Proof |
|---|---|
| Reception retains the original event and a pending job. Repeated delivery returns the first receipt; changed bytes under that identity are refused. Distinct source IDs and A to B to A observations remain distinct. | [unit] src/core/runLedger/inMemory.test.ts::saved source-event reception::*; [worker] deploy/cloudflare-memory/runLedger.test.ts::saved source-event reception in SQLite::* |
| Failed alarm writes before or after saving the alarm roll back the event, job, reception sequence and alarm. An earlier shared wake survives; the shared alarm re-arms with no live agents and physical maintenance paused. | [worker] deploy/cloudflare-memory/runLedger.test.ts::saved source-event reception in SQLite::rolls back the event, job and sequence…; ::preserves an earlier shared deadline before and after an alarm with no live agents |
| Invalid or oversized data cannot consume a sequence. Count and byte limits refuse only new deliveries; known receipts remain readable at capacity. Concurrent duplicate deliveries retain one event. | [unit] src/core/runLedger/inMemory.test.ts::saved source-event reception::refuses untrusted destination fields…; ::returns old receipts at the count limit and refuses a new event; ::bounds retained event bytes independently of row count; ::retains exactly one event under simultaneous duplicate deliveries |
| Unknown acknowledgments remain uncertain. A client accepts only a receipt bound to its source and body. Missing, changed or unknown-schema read data cannot claim source absence. | [unit] src/core/runLedgerWorker.test.ts::saved source-event client::* |
| New receptions, run claims, heartbeats and alarm finalization cannot postpone an earlier armed source wake. A consumed alarm can still re-arm a bounded next wake. | [worker] deploy/cloudflare-memory/runLedger.test.ts::saved source-event reception in SQLite::later…; ::keeps the pending source wake across external discovery and the alarm finally path; ::preserves an earlier shared deadline before and after an alarm with no live agents |
| Job execution, internal delivery, retention, access checks and source enablement use the common orchestrator path. | [gap] Remaining shared intake steps. Reception is inactive until those contracts are complete. |
Source job claims and retry
A compatible consumer can claim at most 16 saved source jobs in one request. A claim grants a 60-second queue lease with a server-generated token. It grants no task, tool, source-read or publication authority and cannot renew an agent's execution budget. A stopped claimant leaves the input saved; after expiry another process can claim it with a new token and attempt number.
Each claim request scans at most 64 eligible records. The saved scan position advances past unsupported records without changing them, so those records cannot permanently hide later work. An incompatible reader cannot claim or change a job. The version 2 job reader also accepts original version 1 received jobs. Reader version 3 also understands saved relevance records. Version 2 callers hold version 3 jobs without consuming attempts; no reader downgrades retained records.
Retry and hold are separate outcomes. Temporary source, storage, provider or context unavailability can request a bounded retry delay. A permanent permission refusal, unsupported schema or invalid source can only hold the job as failed. Eight attempts exhaust automatic recovery. Failure retains the original source and a closed failure reason; it proves neither delivery nor absence of an external effect.
A release receipt is retained under its claim token. Repeating the same release returns its original result before checking the current lease, even if a later process has claimed the job. Changed release metadata under that token is refused. Stale or expired tokens cannot change another claim. Claim/retry state and the scheduled wake commit in the same storage transaction. Unknown HTTP acknowledgments remain uncertain and have no automatic retry or fresh-work fallback.
No source processor, producer or job completion path is enabled by this capability. Completion still requires the later internal-delivery contract. A lost claim response can leave a lease held until expiry; it cannot erase input. The client does not retry that claim implicitly. Structured HTTP outcome logs contain counts, opaque event keys and failure reasons, without source text or lease tokens.
| Criterion | Proof |
|---|---|
| Bounded claims require no live agent. Concurrent claimants cannot receive the same active lease. Expiry permits a new token and fences an old owner. | [unit] src/core/runLedger/inMemory.test.ts::saved source-event jobs::*; [worker] deploy/cloudflare-memory/runLedger.test.ts::saved source-event jobs in SQLite::claims through the private HTTP route without a live agent; ::reclaims an expired lease and rejects an old owner's release; ::gives one concurrent claimant the lease for a single queued event |
| A saved release replays before the current lease check. Changed metadata cannot release newer work. | [unit] src/core/runLedger/inMemory.test.ts::saved source-event jobs::returns the original retry receipt…; [worker] deploy/cloudflare-memory/runLedger.test.ts::saved source-event jobs in SQLite::replays a saved retry after a new claim without changing the new owner |
| Failed wake writes before and after storage roll back claims and retry releases, including their ownership, attempts and saved receipts. | [worker] deploy/cloudflare-memory/runLedger.test.ts::saved source-event jobs in SQLite::rolls back claim ownership…; ::rolls back retry release… |
| An incompatible reader cannot consume an attempt. A page of unknown schemas cannot starve known work or downgrade unknown jobs. | [unit] src/core/runLedger/inMemory.test.ts::saved source-event jobs::refuses an incapable reader without consuming an attempt; [worker] deploy/cloudflare-memory/runLedger.test.ts::saved source-event jobs in SQLite::does not let a page of future job schemas starve known work |
| Committed scans identify held records by event key and fixed reason without source text or lease tokens. A rejected transaction emits no held-record log. | [worker] deploy/cloudflare-memory/runLedger.test.ts::saved source-event jobs in SQLite::identifies an unreadable retained job only after its claim transaction commits… |
| Exhaustion and permanent refusal retain failed input and stop scheduling automatic attempts. Repeated source reception cannot reopen it. A lease that contradicts a recorded failure remains unreadable. | [worker] deploy/cloudflare-memory/runLedger.test.ts::saved source-event jobs in SQLite::retains exhausted input…; ::holds permanent refusal…; [unit] src/core/runLedger/inMemory.test.ts::saved source-event jobs::holds a stored lease that contradicts an earlier permanent failure |
| Clients reject foreign, contradictory, unbounded or malformed acknowledgments. Retry receipts require the exact delay for their attempt. Retained jobs with an invalid delay remain held without rewriting or replay. Permanent refusal cannot be requested as a temporary retry. | [unit] src/core/runLedgerWorker.test.ts::saved source-event job client::*; src/core/runLedger/inMemory.test.ts::saved source-event jobs::holds a retained retry outside its bounded delay without rewriting or replaying it |
| A source processor resumes saved handling stages and completes only after recorded internal delivery. | [gap] Remaining shared intake routing and delivery work. These storage capabilities remain inactive. |
| Criterion | Proof |
|---|---|
| Source adapters can obtain a detached canonical JSON snapshot without evaluating accessors or changing the existing reception codec; normalizing key order preserves revision and receipt identity | [unit] src/channels/githubActivity.test.ts::GitHub comment normalization::holds unsafe input without evaluating an accessor; ::separates delivery identity…; ::uses the existing source receipt… |
Keyed inbox delivery receipts
The existing ledger has an inactive deliverInbox protocol for an internal caller that already has delivery authority. A request names a version, stable delivery key and frozen message. It does not grant permissions, select a task or start a fresh run. The caller must save its decision and exact message before it uses this protocol.
One SQLite transaction writes the existing inbox row and a receipt with the run ID, delivery key, body hash and sequence number. A retry of the same run/key/body returns the original receipt, even if the target has ended. Changed input under the same key is refused. Receipt lookup precedes live-target and capacity checks. Receipt rows survive run closure and history deletion; there is no expiry that silently allows another append. Their retirement still needs a separate lifetime and consumption contract before activation.
A run in finishing refuses a new keyed delivery with not-live, including when it entered that phase while the input was being hashed. The SQLite check and append share the delivery transaction. This refusal means the run no longer accepts new keyed input; it does not prove that its live row has disappeared. An exact saved delivery still replays its receipt, and changed input under that key still conflicts. Handoff rows remain eligible for queued input. The ordinary requester inbox path keeps its existing settlement behavior.
Each run retains at most 256 keyed receipts. New keys at that limit are held; existing receipts can still be read by retry. Each message uses the existing 400 KiB inbox bound. The store captures canonical JSON before hashing, rejects supplied custody and stamps the existing destination custody on append. Unsupported receipt versions remain held. A failed receipt insert rolls back the inbox row in the same transaction.
The HTTP client accepts only a receipt for its exact run, key and body hash. Missing routes, unreadable acknowledgments and ambiguous transport results do not fall back to the ordinary inbox route. Deploy compatible readers before any caller enables the protocol. Current dispatch and steer writers still use their existing path. This receipt proves saved delivery, not model consumption; exact consumption and source-job settlement remain pending.
| Criterion | Proof |
|---|---|
| Concurrent retries append once; changed input is held; a saved receipt survives target closure | [unit] src/core/runLedger/inMemory.test.ts::keyed inbox delivery receipts::* |
| Finishing prevents a new keyed append while exact replays, conflicts, handoff input and ordinary requester follow-ups retain their contracts | [unit] src/core/runLedger/inMemory.test.ts::keyed inbox delivery receipts::*; [integration] deploy/cloudflare-memory/runLedger.test.ts::keyed SQLite inbox delivery receipts::* |
| Captured bodies resist mutation; capacity never evicts an earlier key; unsafe or unsupported input appends nothing | [unit] src/core/runLedger/inMemory.test.ts::keyed inbox delivery bounds::* |
| A real SQLite transaction commits one input and receipt; failed receipt storage rolls back the input and allows a later first append | [integration] deploy/cloudflare-memory/runLedger.test.ts::keyed SQLite inbox delivery receipts::*; deploy/cloudflare-memory/runLedger.test.ts::keyed SQLite inbox retention::* |
| Only an exact receipt is accepted; malformed acknowledgments stay uncertain; an old server causes no ordinary inbox fallback | [unit] src/core/runLedgerWorker.test.ts::keyed inbox delivery client::*; src/core/runLedgerWorker.test.ts::keyed inbox protocol compatibility::* |
| Refused keyed delivery logs the run ID and reason without the private message or delivery key; successful and duplicate deliveries emit no refusal log | [integration] deploy/cloudflare-memory/runLedger.test.ts::keyed inbox refusal diagnostics::* |
| Retained receipt decode diagnostics distinguish invalid JSON from an unsupported receipt schema; no stored bytes, message or delivery key enter the log | [integration] deploy/cloudflare-memory/runLedger.test.ts::keyed receipt decode diagnostics::* |
Link delivery to an observed row
The delivery hash covers canonical message data before the store stamps destination custody. The inbox row witness hashes the complete stored message, including custody. The hashes therefore cover different data. inboxDeliveryWitness checks both hashes, run and sequence IDs, the known custody schema, and the expected channel, thread and requester. It removes only the store-owned target field when reconstructing the delivery hash. Captured row data keeps its JSON property order through asynchronous hashing.
The caller must obtain the delivery receipt through the ledger's verified response. This helper correlates supplied data; it does not authenticate the delivery key, certify first-claim identity or grant current execution authority. An observed witness is still not native consumption. No delivery caller, consumption ACK, retirement path or source producer is enabled by this helper.
| Criterion | Proof |
|---|---|
| A delivery receipt links to its exact custody-stamped row despite distinct hashes; reclaim preserves original custody and creates no consumption claim | [unit] src/core/runLedger/inboxDelivery.test.ts::keyed delivery row witness::links distinct payload…; deploy/cloudflare-memory/runLedger.test.ts::keyed SQLite delivery row witness::* |
| Changed message bytes cannot qualify even with a recomputed row witness; foreign destination/row identities and unknown receipt, witness or custody schemas stay held | [unit] src/core/runLedger/inboxDelivery.test.ts::keyed delivery row witness::refuses changed input…; ::holds foreign destination…; ::holds unknown receipt… |
| The check captures receipts and row bytes before hashing and refuses accessors without invoking them | [unit] src/core/runLedger/inboxDelivery.test.ts::keyed delivery row witness::captures both receipts…; ::holds unsafe row accessors… |
Bounded original-ingress metadata
A valid completed ingress owner retires after its original receipt/origin horizon only when no live, provisional, restarting, unresolved custody or retained dependency requires it. Ordinary messages, target answers and saved confirmations use the same rule. Retirement removes original attribution, the run lookup index and its deletion marker together; an expired original request cannot create a new owner. Missing horizons, invalid records and unproved custody remain protected. This removes no run, transcript, model, workspace or canonical answer. A canonical terminal machine:none record may release only this metadata when its exact owner and first-claim identity match, its answer ending is known, and no live, provisional, restarting, promotion, physical-allocation contradiction or pending custody remains. A null allocation alone supplies no ending or physical cleanup authority. A retained native media consumer keeps its exact A/B source owners until that consumer leaves policy; missing or unknown ending/profile and corrupt custody retain them.
Deletion-before-consume stores a finite deadline from protected receipt/config metadata on the existing clear operation. Known origin deadlines and existing marker deadlines never move on retries or configuration changes. Legacy key-only markers receive one baseline on upgraded access; corrupt associated ownership remains protected. Older internal callers use the declared receipt default. The existing alarm and ingress operations retire bounded rotating batches; no separate controller is introduced.
| Criterion | Proof |
|---|---|
| Completed ordinary, target-answer, confirmation and claimed unread-continuation metadata expires together with its run index; deleted owners and standalone markers retire without admitting their expired original input | [unit] src/core/runLedger/ingress.test.ts::original ingress owner::retires completed * owner metadata after its receipt horizon, ::retires an expired completed deleted owner and its marker without permitting its expired request, ::bounds a deletion-before-consume marker and still rejects an expired original request, ::bounds unread continuation metadata after its original horizon: %s; deploy/cloudflare-memory/runLedger.test.ts::original ingress SQLite owner::retires expired completed ingress classes and bounded deletion fences in real SQLite, ::bounds unread continuation metadata after its original horizon in SQLite: %s |
| Active or uncertain ownership and exact retained native A/B/C dependencies remain protected; corrupt dependency or ownership cannot authorize pruning | [unit] src/core/runLedger/ingress.test.ts::original ingress owner::keeps expired * original ownership, ::preserves an expired source owner required by an exact retained native A/B/C dependency; deploy/cloudflare-memory/runLedger.test.ts::original ingress SQLite owner::keeps exact native media dependency pins and fails closed on a corrupt retained root, ::stamps one protected deletion deadline and preserves legacy or corrupt associated owners |
| Deletion protection uses the configured immutable original horizon, never payload fields or later retries; SDK and runtime forward only protected receipt/config metadata | [unit] src/core/runLedger/ingress.test.ts::original ingress owner::never resets a deletion deadline from retry, policy or request-body changes; deploy/cloudflare-memory/runLedger.test.ts::original ingress SQLite owner::uses the configured original deletion horizon through the existing Worker client; src/core/organization/runtime.test.ts::organization runtime::persists deleted original-owner cleanup across an unavailable store and process replacement |
| Terminal machine:none metadata retires only with a known ending and matching canonical owner; missing/unknown endings, changed profile, live/corrupt/pending custody and retained consumers remain protected | [unit] src/core/runLedger/ingress.test.ts::original ingress owner::retires terminal metadata only for canonical * evidence; deploy/cloudflare-memory/runLedger.test.ts::original ingress SQLite owner::keeps exact native media dependency pins and fails closed on a corrupt retained root |
| A retained or corrupt owner backlog cannot starve expired standalone deletion markers or the one-time legacy baseline; bounded scans advance without deleting the protected owners | [unit] src/core/runLedger/ingress.test.ts::original ingress owner::retires a standalone deletion marker behind a bounded retained-owner backlog; deploy/cloudflare-memory/runLedger.test.ts::original ingress SQLite owner::advances legacy deletion markers past a corrupt retained-owner backlog |
| Bounded rotating retirement schedules remaining work on the existing alarm | [unit] deploy/cloudflare-memory/runLedger.test.ts::original ingress SQLite owner::sweeps expired owner indexes in bounded rotating batches and schedules more work on the existing alarm |
| Native dependency pins retain the exact original and execution sources while their root remains, without recursively promoting unrelated rows | [unit] src/core/runLedger/contextRetention.test.ts::context retention — bounded references follow their live holders::pins the exact original and execution sources of a retained native media dependency |
Legacy live and finished run context is backfilled once through a fixed run-ID snapshot and durable cursor in the existing metadata table. Each pass selects at most 100 distinct run IDs, with one small ID lookahead. A run can have both live and finished rows, so it reads at most 200 table bodies, one at a time. No original-ingress owner retires until that census completes. Corrupt census addresses or required context hold retirement; later passes and a reconstructed Worker continue the same snapshot. Current accepted claim/state/finish and thread-session writes maintain the same exact A/B pins, including run IDs behind the migration cursor. Session roots use their existing indexed registration; unit roots retain their existing handoff/ancestor indexes, without a cross-store history reader.
| Criterion | Proof |
|---|---|
| More than 100 retained native consumers are indexed through bounded durable passes; incomplete or corrupt census preserves the expired source, restart continues the cursor, an older-ID writer still pins its source, and the source retires only after its closed consumers leave policy | [worker] deploy/cloudflare-memory/runLedger.test.ts::original ingress SQLite owner::backfills native dependency roots in bounded durable passes before retiring an expired source |
| Accepted claim, state, finish and thread-session writers maintain exact native A/B pins after census completion | [worker] deploy/cloudflare-memory/runLedger.test.ts::original ingress SQLite owner::keeps native source pins on accepted live state finish and session writers after the census |
The additive clear-protection fields require compatible Memory handling for bounded retirement. Older Memory still protects deletion, but cannot prove the new metadata expiry or native leaf accounting. Compatible Memory must precede native v3 writer activation; Memory rollback below that floor is unsafe once those leaves exist. Existing bot context readers must retain v3 support. Legacy session context cannot be reconstructed from the run-store metadata table. Publication, deployment and this minimum-reader qualification remain separate owner-controlled gates.
Typed source evidence message
The shared inbox codec has a versioned source-evidence message. It retains source, delivery, object and change IDs; a nullable native author; the original source receipt and timestamps; source text; and an orchestrator run/decision reference. Source text and author fields carry no requester, credential, private-audience, approval or control authority. Even evidence authored by the requester remains evidence. Empty text and unknown authors remain explicit source data.
The decoder checks the known schema, source receipt key and decision-ID shape. A stored row also needs the expected run, channel, thread and requester custody. Its producer generation remains original through reclaim. Wire messages cannot supply custody. The message uses the existing 400 KiB inbox bound and is never truncated. Unknown schemas and unsafe JSON remain held.
This validates data consistency. It does not authenticate the source receipt or decision. A consumer must read their canonical records, verify source metadata and content, and check current destination authority before delivery. Current runtimes hold this kind instead of interpreting it as a requester message. A present non-message tag also remains held when its version is missing; it cannot use the legacy requester decoder.
Version 2 retains the event's common content and distinguishes a first notice from an internal fold. Both name the original orchestrator owner. A notice has no prior fold decision. A fold requires a decision reference from that owner. Text state, exact text (including empty or null), metadata and typed facts remain data. Stored notices must match the expected root's four custody coordinates; this is consistency proof, not proof that the stored run is an orchestrator.
sourceEvidenceMessageFromRecord projects only version 2 evidence/fact records whose complete event matches their source receipt. It never parses an opaque provider payload or upgrades a version 1 receipt. It captures the record and routing before asynchronous validation. The caller still must load canonical storage and check the actual root, decision, task context and current source access. Neither the codec nor this projection authenticates a caller-supplied record or route.
No producer or runtime reader is enabled. Canonical decision admission, source-job handling stages, typed delivery through both runtimes and unit events, exact native admission and checkpoint acknowledgment, and reader migration remain [gap]. Original source records and inbox bytes stay retained.
| Criterion | Proof |
|---|---|
| Human, bot and unknown authors remain evidence without requester identity; empty text stays intact | [unit] src/core/runLedger/sourceEvidence.test.ts::typed source evidence message::preserves author…; ::keeps empty source text… |
| Source metadata and a decision reference survive delivery/reclaim through both stores, and the ordinary requester decoder refuses them | [unit] src/core/runLedger/sourceEvidence.test.ts::typed source evidence message::keeps a source receipt…; deploy/cloudflare-memory/runLedger.test.ts::typed SQLite source evidence::* |
| Requester/control fields, inconsistent references, foreign custody, unknown schemas, unsafe accessors and oversized input remain held | [unit] src/core/runLedger/sourceEvidence.test.ts::typed source evidence message::rejects requester…; ::holds mismatched source receipts…; ::requires matching stored custody…; ::holds unsafe or oversized input… |
| The message captures source and decision data before hashing; a missing-version evidence tag cannot adopt shadow requester fields, while valid legacy messages still decode | [unit] src/core/runLedger/sourceEvidence.test.ts::typed source evidence message::captures source and decision data…; src/core/runLedger/inboxMessage.test.ts::durable inbox continuity::holds an evidence tag… |
| A tagged hold reports its closed reason and the caller's expected run ID when available; logs contain no source text or raw kind/version values and never coerce a raw tag to text | [unit] src/core/runLedger/inboxMessage.test.ts::durable inbox continuity::reports tagged holds… |
| A first notice needs no prior fold decision; a fold names the original owner decision. Common facts and text lifecycle/null values remain typed source data. | [unit] src/core/runLedger/sourceEvidence.test.ts::source notice and fold messages::retains a first notice without inventing a prior fold decision; ::requires a fold to name its original orchestrator decision; ::keeps typed facts and text lifecycle values without requester authority |
| The source projection checks the complete event receipt, rejects legacy or requester events and captures record/routing before asynchronous checks. Unknown authority fields and mixed schemas stay held. | [unit] src/core/runLedger/sourceEvidence.test.ts::source notice and fold messages::projects common content from the consistent source record and never parses its native payload; ::captures both stored source and route before asynchronous validation; ::refuses source requests, mixed schemas and authority fields while retaining a valid notice |
| A stored first notice retains its original producer custody after reclaim and remains unread as requester input. A notice under another destination is held. SQLite retains notice and fold data through the same operations. | [unit] src/core/runLedger/sourceEvidence.test.ts::source notice and fold messages::preserves first notices through keyed delivery and reclaim only under their root custody; [worker] deploy/cloudflare-memory/runLedger.test.ts::typed SQLite source evidence::retains common source %s data and custody through reclaim |
| Both stores check a version 2 source row's current owner at the existing append boundary. A notice requires the actual original orchestrator role and exact owner coordinates. A direct child fold requires the current original root, matching saved handoff/consumer and its exact saved source-fold decision and receipt. Finishing or unavailable roots, unproved nested/unit relationships and changed ownership stay held. Checks precede both keyed and ordinary inbox append; a keyed replay still returns its original stored receipt. Legacy data storage remains unchanged. These are necessary owner checks only: canonical source/task identity, destination/input/context hashes, current access, retention and native acknowledgment still require qualification; no producer or consumer is enabled. | [unit] src/core/runLedger/sourceEvidence.test.ts::source notice and fold messages::refuses a common notice outside an original orchestrator: %j; ::checks the current notice owner after asynchronous delivery preparation; ::requires the actual root and its saved source-fold decision for a direct child; [worker] deploy/cloudflare-memory/runLedger.test.ts::typed SQLite source evidence::* |
| Both inbox stores require a version 2 notice or fold to match the stored event's complete receipt, source identity, author and common content. They validate the event hash, keep the provider payload opaque, and compare the same stored source bytes at append. Missing, corrupt or changed sources refuse a new row and keyed receipt together. Source-proof refusal logs name the prepare or append phase with fixed metadata and no source text. An exact keyed replay still returns the old receipt without proving current access or consumption. Legacy messages are unchanged. Task/destination hashes, access, retention and native acknowledgment remain separate gates; producers and consumers stay disabled. | [unit] src/core/runLedger/sourceEvidence.test.ts::source notice and fold messages::refuses substituted source content and receipt fields without saving an inbox row; ::checks stored source bytes after asynchronous preparation and preserves the original delivery; [worker] deploy/cloudflare-memory/runLedger.test.ts::typed SQLite source evidence::* |
Saved source relevance
The existing source job can retain a relevance recommendation for an orchestrator conversation and task-context hash. The record carries the original conversation coordinates, exact classifier-input hash, result source, verdict, reason and store-assigned time. The source receipt binds the save to the original event. A context hash must cover the complete task snapshot; the input hash must cover the actual classifier input and policy. These are caller-supplied references, not a proof of source access, current context or an authorized decision.
Only a current queue owner can save a new result. A repeated result for the same context returns the original record before checking the current lease or capacity. Changed input or result under that context is refused. Different task snapshots and conversations have separate records. Each event holds at most 16 records; reasons have a 1,024-byte UTF-8 limit and each identity has a 512-character limit. Each result also has a 4-KiB JSON byte limit. These provisional limits bound inactive storage; enablement still requires measured capacity and retention.
Saving a result upgrades the existing job to version 3. Claim, retry, expiry and permanent failure preserve every saved result. Incompatible readers hold the job. The save does not change its queue deadline, alarm, execution lease or completion state. A failed transaction restores the prior job. An unknown acknowledgment remains uncertain; it cannot justify another model call or a routing fallback.
No source processor calls this operation. Recovery must read and match the saved source, context and input before reusing a result. A result cannot select a specialist, ignore evidence by itself, grant permission, prove internal delivery, settle a job or retire input. Those integration steps remain pending.
| Criterion | Proof |
|---|---|
| An exact save survives reclaim; incapable readers hold the new schema, and stale or conflicting results cannot change newer work. | [unit] src/core/runLedger/sourceRelevance.test.ts::saved source relevance::retains a context-bound result and replays it after expiry without changing the job; ::fences an unsaved late result and keeps the saved result through retry and failure; [worker] deploy/cloudflare-memory/runLedger.test.ts::source relevance in SQLite::saves through the private HTTP route and holds incompatible readers |
| Different contexts remain separate. Changed classifier input conflicts; malformed, contradictory or foreign-source saves are refused. Known records remain readable at capacity. | [unit] src/core/runLedger/sourceRelevance.test.ts::saved source relevance::separates task contexts and refuses changed inputs under the same context; ::binds reception and rejects unsafe or contradictory results without losing valid work; ::bounds retained contexts and returns known results at capacity; ::captures immutable input and bounds UTF-8 metadata without evaluating accessors |
| Concurrent saves retain one result. Unknown saved result schemas remain held without rewriting. | [worker] deploy/cloudflare-memory/runLedger.test.ts::source relevance races in SQLite::* |
| A failed SQLite write rolls back the saved recommendation without changing the original queue lease or alarm. Retry and reclaim preserve a successful save. | [worker] deploy/cloudflare-memory/runLedger.test.ts::source relevance in SQLite::rolls back a failed save and keeps the original queue lease and alarm |
| Unreadable saved jobs log their event key and a fixed invalid-json/invalid-schema cause. Stored bytes and exceptions stay out of logs; refusal retains the original job. | [worker] deploy/cloudflare-memory/runLedger.test.ts::source relevance stored-job diagnostics::* |
| Successful HTTP saves and exact replays log the event key, fixed verdict/source and original save time. Refusals log the validated request's event key and fixed reason; malformed requests expose no raw key. Logs exclude private text, context coordinates and claim tokens. | [worker] deploy/cloudflare-memory/runLedger.test.ts::source relevance diagnostics in SQLite::* |
| HTTP acknowledgments bind the exact source receipt and frozen result. Refusals stay explicit; missing or malformed responses remain unknown with no fallback operation. | [unit] src/core/runLedgerWorker.test.ts::source relevance client::* |
| The source processor reuses a matching saved result and checks current context/access before an authorized route or fold. | [gap] No processor or source producer is enabled. This is storage support only. |
Terminal native retirement authority
| Criterion | Proof |
|---|---|
| Memory prepares a terminal-native-scope permit only from an exact canonical terminal record with a finite seal, no raw provisional/restart marker, and no live row. Its durable run-ID fence orders cleanup against direct claims, queued live-row creation and reclaim. History remains unchanged. | [unit] deploy/cloudflare-memory/runLedger.test.ts::terminal native retirement authority::* |
| The terminal preparation route logs its prepared or held outcome and a closed reason when held. The log contains no target identity or original content, and the response retains the same status and body. | [unit] deploy/cloudflare-memory/runLedger.test.ts::terminal native retirement authority::logs only the preparation outcome while preserving held and prepared responses |
Common content on saved source events
Version 2 of the existing SourceEvent adds provider-neutral content. Requests and source evidence carry text with a common present, deleted or withdrawn state; source facts carry a JSON facts object. A dismissed review maps to withdrawn evidence. Optional message metadata retains source labels and native locations as data, not requester credentials or execution targets. Common handling can read that lifecycle and display metadata without parsing GitHub fields. Null text is unknown text, not an empty message or permission to infer a body from the provider payload. The native payload remains opaque in common handling code. The adapter still owns source authentication and projection.
The existing canonical JSON, size bound and body hash cover the content. The delivery key is unchanged. An exact repeat returns the original receipt; changed content under the same delivery identity is refused. An older stored event cannot be rewritten merely to add version 2 content. Version 1 remains readable without invented common content, and a version 1 envelope cannot carry a version 2 field. Unsupported versions, mismatched text/fact kinds and extra authority fields in the content envelope are refused. Facts can describe controls or permissions as untrusted data; they cannot execute a control or grant a permission.
The inactive GitHub converters emit version 2. Human and bot comments, inline replies, submitted reviews and commit comments share text content. PR, repository, thread, check/status, ref and installation changes share fact content. Common content holds the normalized text or facts once; the opaque payload retains native data and source-specific relationships. No receiver calls these converters, and no source processor or producer is activated. A future producer must verify compatible readers. A future handler must hold legacy or unsupported content rather than parse a provider payload or silently downgrade it. Event content versions are separate from queue job/relevance versions.
| Criterion | Proof |
|---|---|
| The source receipt covers normalized text independently of opaque provider payload; text changes preserve the delivery key but change the body hash | [unit] src/core/runLedger/sourceContent.test.ts::common source event content::retains normalized text independently of the provider payload and binds it to the source receipt |
| Null text and fact data keep their type without becoming a control; legacy source remains readable without inferred content | [unit] src/core/runLedger/sourceContent.test.ts::common source event content::preserves unknown text and typed facts as source data without control authority; ::keeps legacy records readable without inferring normalized content from their payload |
| Invalid, downgraded and unsupported content is held; text cannot supply credentials in its content envelope | [unit] src/core/runLedger/sourceContent.test.ts::common source event content::holds unsupported, downgraded, malformed and authority-bearing content |
| Exact content survives existing event storage; changed content or a changed canonical record cannot inherit the old receipt; schema upgrades cannot overwrite an older delivery | [unit] src/core/runLedger/sourceContent.test.ts::common source event content::saves and replays the exact common content through the existing event store; ::does not replace an older stored event when a new schema reuses its delivery identity |
| Message metadata stays data and is covered by the receipt; inline file/commit/line metadata is available without parsing the native GitHub payload | [unit] src/core/runLedger/sourceContent.test.ts::common source event content::keeps message metadata as data and covers it with the receipt; src/channels/githubActivity.test.ts::GitHub comment normalization::separates delivery identity from stable object revisions and preserves native inline metadata |
| SQLite retains both common text and facts, replays exact duplicates and refuses changed content | [worker] deploy/cloudflare-memory/runLedger.test.ts::saved source-event reception in SQLite::retains common text and facts and refuses changed content under the original delivery |
Record readers accept a typed native GitHub task identity only when it matches targetPr. Malformed or conflicting data cannot become legacy absence. Full reads retain it; summaries and public run views omit it. Legacy records stay absent. Production writers remain unchanged until compatible readers are qualified. Stored identity supplies no current access or delivery permission. | [unit] src/core/runRecord.test.ts::accepts a typed archived GitHub identity only for its original PR and keeps legacy absence; src/core/runStore.test.ts::*::keeps the native GitHub task identity in full records without listing it; src/core/runsService.test.ts::*::never exposes internal source archives or handoff context through any persisted projection; [worker] deploy/cloudflare-memory/runs.test.ts::retained source archive storage::retains native GitHub task identity only in full records and refuses corrupt identity |
| Human and bot PR comment surfaces supply the same common text. Native comment/ref/access facts, revisions and source metadata retain their existing proofs after the normalized fields move. | [unit] src/channels/githubActivity.test.ts::GitHub comment normalization::supplies common text for human and bot comments across every PR comment surface; src/channels/githubActivity.test.ts::*::*; src/channels/githubInstallationActivity.test.ts::GitHub installation activity normalization::* |
| Repository lifecycle activity uses common facts and retains native identity, unknown state, actor data and original snapshots | [unit] src/channels/githubActivity.test.ts::GitHub repository activity normalization::* |
| The inactive GitHub task matcher validates a complete source record and the existing typed native PR context. Direct PR comments, reviews, inline threads and PR lifecycle activity are candidates only under the same host, App, installation, numeric repository and PR number; a supplied native PR ID must agree. Names, author type, text and selected-head changes do not supply task identity or authority. Conflicting identities and legacy content stay held. Unsupported commit/ref/repository/check/installation relationships stay held within the matching source and repository scope. Inputs are captured before asynchronous receipt validation. Candidate matching proves neither canonical storage origin, relevance, current access, root ownership nor a delivery target. | [unit] src/channels/githubActivity.test.ts::GitHub activity task candidates::* |
| Handler capability gates and common relevance consume the new projection before delivery is enabled | [gap] The converters remain inactive. Source ingress, source processing and delivery are not enabled. |
Destructive run removal
Kill requires confirmation from the original producer and any required workspace cleanup. A sealed stream, a saved terminal record or the absence of a workspace does not prove shutdown. An unconfirmed producer ending retains its diagnostic record and refuses deletion. A retained legacy cancellation with only a no-workspace disposition also remains unconfirmed and cannot authorize deletion.
Ordinary and setup records retain runtimeShutdownConfirmed: true only after the original terminal write and original cleanup acknowledge. An absent field means unknown, including on legacy and boot-reclaimed records. A later positive acknowledgement updates that same record and clears a pending runtime_shutdown_unconfirmed marker. An uncertain terminal write does not start that update. With history enabled, Kill requires the current saved proof or an acknowledged prepared cancellation. With history off, it checks the original local control. Losing a control, or retaining an old control for a newer saved segment, does not grant permission to delete.
The cleanup update compares the original terminal identity and events, then changes only its shutdown metadata. It cannot replace a newer segment under the same run ID, erase newer metadata or restore deleted history. The Worker checks the comparison and the absence of a live owner in one SQLite transaction through /runs/shutdown. Unsupported stores retain an unknown outcome; no full-record fallback or retry follows an uncertain acknowledgement.
Initial fitting reserves room for the later shutdown flag. The acknowledgement leaves the saved events unchanged and stays within MAX_RECORD_BYTES.
runs stop <id> ends the current turn and forces runtime cleanup. It retains the run, who requested the stop, and the final stopped result. runs kill <id> skips the graceful turn interrupt and removes the run from live and saved run views after runtime shutdown is confirmed. Kill also permits explicit removal of a finished run. Existing runs.stop calls with an explicit mode: soft|hard keep their original non-destructive behavior and response mode. The mode is optional for the new Stop behavior. Deploy the history Worker before the bot and bundled dashboard so deletion intent, fences and reads are available.
The history store keeps a separate run-ID deletion fence. It rejects later record writes and same-ID claims. Kill removes the run row, its saved events and the existing history references. It retains source deduplication, ownership and shutdown evidence that prevent replay. Shared session logs, other runs, memory, metrics, external output and backups are outside this row-removal contract.
| Criterion | Evidence |
|---|---|
| A near-limit terminal record can save original shutdown proof without replacing events or exceeding the record byte limit. | [worker] deploy/cloudflare-memory/runs.test.ts::unit context retention work::a near-limit terminal record saves shutdown proof without replacing its events |
| Cancellation finish shares the existing 2 MiB whole-record request bound. A valid record above 512 KiB preserves complete terminal history and closes its prepared live row. Requests above 2 MiB still return 413; ordinary controls keep their 512 KiB bound. Unauthorized calls and mismatched tickets, owners or records cannot end the live row. | [worker] deploy/cloudflare-memory/runLedger.test.ts::cancelled check terminal custody in real SQLite::saves complete large cancellation history through the actual HTTP route; ::keeps the cancellation bulk body limit and the ordinary control body limit; ::holds large cancellation history when its %s does not match |
| Late cleanup preserves newer terminal segments and metadata; the SQLite update also refuses a current live owner or deletion fence. | [unit] src/core/dispatch/record.test.ts::registerFinishRecord — the finish record, written by the drain after the reply::old cleanup cannot replace a newer terminal record for the same run; src/core/runStore.test.ts::* — RunStore contract::acknowledges only the original terminal segment and never restores deleted history; [worker] deploy/cloudflare-memory/runs.test.ts::unit context retention work::shutdown acknowledgement preserves newer records and the deletion fence; ::shutdown acknowledgement refuses a run with a current live owner |
| Saved terminal rows without original shutdown proof retain their events; retained positive proof permits explicit deletion. | [unit] src/core/runsService.test.ts::RunsService.stopRun::Kill retains saved %s history without original shutdown proof; ::Kill accepts saved original shutdown proof after the local control is gone |
| The original cleanup can update a saved outcome and permit a late Kill retry. | [unit] src/core/dispatch/record.test.ts::registerFinishRecord — the finish record, written by the drain after the reply::retains unknown cleanup, then saves the original positive shutdown acknowledgement; src/core/runsService.test.ts::RunsService.stopRun::Kill can retry after the original cleanup confirms a previously unknown ending |
| Unknown or refused original terminal writes start no cleanup-proof update, including after local detachment; shutdown alone cannot replace a terminal record acknowledgement. | [unit] src/core/dispatch/record.test.ts::registerFinishRecord — the finish record, written by the drain after the reply::keeps an uncertain original terminal write unknown after cleanup confirms; ::a refused original ledger finish cannot qualify its saved fallback as shutdown proof: tracked=%s; src/core/runsService.test.ts::RunsService.stopRun::Kill waits for the terminal record as well as original shutdown |
| In-memory and file stores reject a late put after Kill. | [unit] src/core/runStore.test.ts::kill deletes the run and prevents a delayed put from restoring it |
| The SQLite history service removes the run and events and keeps a deletion fence that rejects the delayed put. | [unit] deploy/cloudflare-memory/runs.test.ts::unit context retention work::kill deletes run history and rejects a delayed record write |
| Kill also fences a run whose model has finished but whose final reply or record is still pending. | [unit] src/core/runsService.test.ts::RunsService.stopRun::Kill fences a finished run whose final reply has not been sealed |
| Kill intent survives an owner’s full state save and cannot be changed by that owner. | [unit] src/core/runLedger/inMemory.test.ts::InMemoryRunLedger::heartbeat extends the lease and reports a stop another generation requested; requestStop says whether the owner is live, deploy/cloudflare-memory/runLedger.test.ts::run ledger — the fence (item 28)::the owner's heartbeat extends the lease and reports a stop any generation requested; stop says whether the owner is live |
| Kill suppresses replies pending card closure, publication checks, current-work checks or refusal persistence, including a confirmed Stop notice. | [unit] src/core/dispatch/reply.test.ts::deliverAnswer…::Kill suppresses… |
| Stop waits for cancellation confirmation before its one terminal notice; unconfirmed shutdown remains silent. | [unit] src/core/dispatch/reply.test.ts::deliverAnswer…::Stop waits for the cancellation result before its notice… |
| Kill deletes a hosted parent after its original writer confirms terminal closure. | [unit] src/core/runsService.test.ts::RunsService.stopRun — a hosted parent…::Kill deletes a hosted parent after its original writer confirms closure |
| Shared deletion fences remove cached reads and listings in another generation. | [unit] src/core/runsService.test.ts::RunsService.getRun::Kill in another generation removes registry-backed reads and listings, src/channels/liveView.test.ts::shared Kill hides a cached run from capability route… |
| Open completed-run feeds remove saved rows killed through another generation, without exposing private rows. | [unit] src/channels/liveView.test.ts::shared Kill removes saved feed rows after registry eviction without exposing private rows |
| Setup errors and refusals cannot send a pending final reply after Kill. | [unit] src/core/dispatcher.test.ts::executor provisioning by agent resources::Kill suppresses a setup… |
| A Stop accepted after normal delivery starts replaces the pending answer with one stopped result. | [unit] src/core/dispatch/reply.test.ts::deliverAnswer…::a late Stop replaces the pending… |
| A shared deletion-marker failure refuses cached reads and logs once per outage; successful revalidation restores a normal live read. | [unit] src/core/runsService.test.ts::RunsService.getRun::refuses cached reads when shared deletion state is unavailable |
| Kill waits for the original producer and cleanup, rather than stream sealing; an unknown producer ending remains saved and cannot be deleted. | [unit] src/core/runsService.test.ts::RunsService.stopRun::Kill keeps a sealed row until the original producer and cleanup acknowledge shutdown; ::Kill retains a terminal record with an unconfirmed producer ending; ::Kill retains a legacy no-workspace record without a producer exit receipt; src/core/dispatch/runLoop.test.ts::runLoop…::an unconfirmed producer ending retains its original workspace…; src/execution/runStop.test.ts::run runtime stop::does not treat machine=none as a receipt that the bot-host producer exited |
| Shared deletion checks fence foreign ledger caches. The feed tracks foreign active rows and starts with an authorized snapshot so a deletion between page and feed cannot strand a row. | [unit] src/core/runsService.test.ts::RunsService.getRun::shared Kill fences cached foreign ledger reads and listings; src/channels/liveView.test.ts::shared Kill removes a foreign row seeded by the active index; ::an index feed reconciles a row killed between the page and the feed request; web/src/pages/runsIndex.test.ts::RunsIndexPage — the live feed::reconciles the actual seeded rows when Kill falls before feed attachment |