packages/agent/docs/work-packages/05-direct-durable-drive.md
Status: WP05 complete through M10: lane-owned inbox, immutable result records, 13 family-neutral leaves, atomic boundary planning, total cancellation/dispatch, public and replicated lane surfaces, documentation reconciliation, and lane-safe provider cache identity. Remaining assistant-output work is owned by the mobile assistant-output handoff, outside the public-drive gate.
WP06's Session/Branch/Lane separation is part of the foundation. Public drive is enabled; watchSession is the sole deferred Harness method.
Format 4 remains work in progress. Every durable type replacement in this package requires no migration and no compatibility decoder for the pre-redesign shapes.
The standalone-compaction inbox/promotion design previously in this document is withdrawn (§5). R1–R3 and M7/M8 replace it. M9 reconciled docs/harness.md, which is normative on its own again.
Read completely before implementation work:
packages/agent/docs/harness.md (normative; reconciled by M9)packages/agent/docs/runtime-simplification.md (its 22-leaf list is replaced by R3)packages/agent/src/harness/session/types.tspackages/agent/src/harness/session/values.tspackages/agent/src/harness/runtime/lane.tspackages/agent/src/harness/runtime/types.tspackages/agent/src/harness/runtime/drive/*.tspackages/agent/src/harness/runtime/progress.tspackages/agent/src/harness/runtime/restore.tspackages/agent/src/harness/execution/{effect-gate,assistant,tools}.tsDo not inspect Git history or removed runtime implementations. The current source and these documents are the only implementation inputs.
Lane.state is authoritative while a Harness owns its Session. It contains all orchestration state required to dispatch:
Every supported mutation commits on the one Session mutation line and publishes the matching Lane.state before releasing the line. Drive procedures never reread laneState, operationMeta, operationState, branchTip, laneConfig, or operationResult from storage.
SessionReader is used only to dereference content named by in-memory state and to enumerate operation-owned cleanup addresses:
Queued input is lane-owned, never operation-owned. LaneState (durable and process-local) carries one ordered inbox of { entryId, kind } items with kind: "steer" | "followUp" | "nextRun" | "write". Payload staging is unchanged: enqueue mints the entry id and writes pendingEntry(id); the inbox holds ids only. Item lifecycle is admission → consumption | cancellation; terminal cleanup never touches queue payloads.
Tags are consumption-eligibility markers, not ownership. Enqueue always succeeds — during runs, structural operations, cancellation, and idle. Every drain point selects eligible items per tag, applying queue modes during selection, but places all selected items in the inbox's single global admission order. Tag grouping must never reorder user input. At acceptance, request prompt entries follow the selected inbox items because the request is the newest admission.
| Drain point | Eligibility and decision order |
|---|---|
| acceptance (idle lane) | write + nextRun (all), steer (steeringMode: all or oldest), and followUp (followUpMode) are eligible. Place selected items in global admission order, then request prompt entries. The acceptance transaction places them and removes their ids; starting drains nothing. followUp is eligible because an idle lane vacuously satisfies its "after current work" condition. |
| turn-end boundary pass (run) | write + steer are eligible before threshold/continuation planning and are placed in global admission order. followUp becomes eligible only at may_finish, before before_run_end and finish. nextRun is never eligible mid-run and never blocks finish. |
| idle direct append | queued write items are placed in admission order, then the new entry, in one commit |
| abort (M7) | steer + followUp are removed from the inbox, payload values deleted, and payloads returned; nextRun and write items stay |
One decision, at most one commit (R1b). A boundary pass may enter the mutation line several times for bounded reads and for before_run_end mediation, but it performs at most one commit, and that commit always lands in a state that neither drains nor rechecks (assistant.ready, summary.deciding, placed entries + assistant.ready, or a terminal transaction). No boundary decision ever commits back into checkpoint. A crash before the commit re-runs the whole decision with nothing consumed; a crash after it lands past the decision point. Consequently skipInboxOnce and thresholdCheckedTriggerEntryId are deleted: drains cannot re-fire because their target states do not drain, and the threshold marker is replaced by a guard derived from the branch itself — threshold fires only when shouldCompact holds and the branch's newest compaction entry is older than the trigger entry, so a committed threshold compaction is its own durable marker. Mode remainders stay queued and are consumed at later boundaries, giving the per-turn steer cadence and per-run-end followUp cadence.
There is no terminal drain. cancelQueued triage is unchanged and single-location. Queued input survives operation termination (including abort, for nextRun/write) until consumed or cancelled; this is a deliberate product decision.
OperationState has one at discriminator with 13 direct leaves:
startingcheckpointassistant.readyassistant.effect_pendingassistant.retry_waittoolsdeferred.suspendeddeferred.effect_pendingsummary.decidingsummary.readysummary.effect_pendingsummary.retry_waitnavigation.ready_to_commitEvery leaf carries one uniform scope { control, settings, latestAssistantEntryId }. There are no run./compaction./navigation. leaf prefixes, no per-family scope types, and no intersection zoo. The four summary.* leaves carry a SummaryTask whose boundary datum (a closed three-arm union, §8) decides what happens at the result boundary. starting and navigation.ready_to_commit are intent-locked entry leaves. ToolBatch/ToolCall remain the nested child collection state machine. Control stays orthogonal and loses its drained fields when M7 installs drain-and-return abort.
Every terminal transaction writes one small immutable result record at operationResult(operationId) (namespace pi.result, operation id key). The record is lane-lived; keeping it outside pi.op.* keeps that namespace's "deleted no later than the terminal transaction" grammar total and exception-free. LaneState.lastOperationId points at the newest record. laneLastResult (value, address constructor, and type union) is deleted.
drive(id) becomes total for settled operations: current id → install/join; record exists → return the record itself; neither → OperationMismatch.cancel_requested always records status: "aborted"; equivalently, any other status implies running terminal control. (Every non-aborted terminal path goes through continueOperation, which diverts under cancellation, and the mutation line serializes the marker against the terminal commit.)getResult(operationId) and the drive settled arm are the entire read surface; both return the record with no entry dereference, so observation can never fault on missing entries.Unchanged: one process-local Drive pass per lane; first matching caller installs, later matching callers observe the same completion; no caller owns it after installation; invocation cancellation rejects only that caller's observation; an installed Drive is never abandoned or replaced in-process; requestAbort(operationId) is the only durable operation cancellation; a stale operation id returns OperationMismatch and affects nothing. Drive.context removes the installing invocation signal.
Unchanged: close is not abort. It seals mutation admission, rejects local observations with HarnessClosed, observes detached pass failures, drains mutations admitted before the seal, and closes the Session. It writes no cancellation marker or synthetic terminal state and never replaces a Drive.
Unchanged: no live-process external-finalization path, OperationEnded, finalizedOutcome, ownership-loss result, or exact-Drive ABA fence.
Live procedures are ordinary straight-line async functions:
prepare
→ commit intent
→ admit and await effect
→ commit settlement
A procedure is the sole writer that changes its top-level at leaf. requestAbort changes only control (and the lane inbox); inbox admission changes only the lane inbox. Operation state has exactly two supported writers: the procedure and requestAbort.
The Lane supplies continueOperation and settleOperation as today. A terminal decision appends the universal suffix:
setValue(operationResult(operationId), record);laneState with currentOperationId: null, lastOperationId: operationId, and the preserved inbox;The state argument is a type capability established by dispatcher control flow, never compared with current state at runtime.
Keep checks only where another supported writer can change the relevant fact:
requestAbort versus effect admission and settlement;Do not reintroduce operation identity, expected-at, exact-Drive, or ownership-loss checks into ordinary transitions.
requestAbort keeps the two-step gate order: beginAbort before the cancellation mutation, commit cancel_requested, signalAbort after the commit.
Complete. Breakpoints, manual drive, and drive deadlines are absent.
Complete. Includes the split effect gate, deterministic gated storage, authoritative process-local Lane projection, commit-result usage totals, progress channels, terminal cleanup, and immutable operation-result observation.
Complete. Includes run.starting → run.checkpoint, assistant ready/intent/effect/settlement, frame persistence and cleanup, configuration failure, unknown-outcome assistant recovery, and response/usage/state atomicity.
Complete. Includes durable retry waits, local wait policy, one deferred poll permit per pass, unknown-poll replacement under fresh ids, and event-stream-preserving Models.streamDeferred.
Complete. Includes planned/effect-pending/outcome-ready/completed calls, safe replay, unsafe interruption, invocation memos, bounded checkpoints, completion-order staging, source-order placement, sequential and parallel modes, and tool-reported usage.
Complete. Canonical flat at types, continueOperation/settleOperation, removal of the installer-owned Drive model, and conversion of M3–M5 procedures.
Committed (9b23c6583): runtime/drive/structural.ts, test/harness/runtime/drive-structural.test.ts, the one-provider-request seams in the compaction modules, threshold routing, overflow preparation, per-request structural intent/usage settlement, hook decisions, structural retry/recovery, and unsummarized navigation commit. Its family cross-product layer (triplicated quadruples, three-armed publishers) is rebuilt by R3; its effect seam, preparation plumbing, threshold/overflow logic, and navigation commit are retained.
The design that gave compaction.* leaves a RunScope inbox and a one-way cross-family move into run.* at the result boundary is withdrawn. Its problems: a relaxed intent/state invariant with bespoke restore rules, compact() resolving with a run outcome that does not fit CompactionResult, suppressed run_start producing unbalanced event brackets, fused-consumption rules to avoid cancelQueued stranding, and promotion-specific arms in reconciliation, cleanup, watch, and admission.
Replacement, spread across this package:
No code implements promotion. M9 removed the withdrawn design from harness.md; this section remains only as the historical decision record.
Move all queued input to one lane-owned tagged inbox first (R1a, implemented), then — after R3 — replace the transitional stepwise checkpoint drains with one shared atomic boundary planner (R1b). Control.drained* remains until M7 introduces drain-and-return abort; it is not part of inbox ownership.
src/harness/session/types.ts — durable LaneState { currentOperationId, inbox } with InboxItem { entryId, kind } replaces pendingNextRun; delete Inbox and RunScope.inbox. Keep Control.drained*, skipInboxOnce, and thresholdCheckedTriggerEntryId temporarily.src/harness/runtime/types.ts — process-local LaneState.inbox replaces pendingNextRun; LanePatch.inbox pairs durable lane-state replacement with process-local publication.src/harness/runtime/lane.ts — acceptance selects per tag and mode but places selected items in global admission order, then request prompt entries; it deletes selected pending values and removes their ids atomically with the LaneBusy check. A lone queued write never validates acceptance. append during any operation enqueues a write-tagged lane item without touching operation state; idle append places queued writes first, then the new entry, in one commit. watch derives queues and pending writes from the lane inbox.src/harness/runtime/drive/checkpoint.ts — transitional stepwise drains read and replace the lane inbox rather than operation state. skipInboxOnce and thresholdCheckedTriggerEntryId continue to protect those temporary multi-commit boundaries.src/harness/runtime/drive/terminal.ts — operation cleanup never deletes lane-inbox payloads. Staged outcome_ready results, drained abort payloads, and other operation-owned addresses are unchanged.src/harness/runtime/drive/{generation,response,deferred,tools,structural}.ts — runScopeOf and scope copies drop inbox; ordinary operation-state transitions cannot clobber concurrent input.src/harness/runtime/restore.ts, fork/legacy normalization, and focused tests use the tagged lane inbox.Lands after R3, because pre-R3 the structural result boundary is spread across the triplicated publishers: fusing the planner into three arms and then collapsing them in R3 would be double work, while post-R3 the single ResultBoundary switch is exactly one planner call site. The two CheckpointData patch fields ride through R3's mechanical rename as inert baggage.
Core invariant: no boundary decision ever commits back into checkpoint. checkpoint is the durable "boundary pending" resting leaf, entered only from settlements, run-start consumption, and deferred redemption. Every exit of its pass is assistant.ready (with any placed entries), summary.deciding, a placed follow-up + assistant.ready, or the terminal transaction.
One shared boundary planner with a process-local threshold: "check" | "skip" parameter has exactly two callers:
checkpoint pass (threshold: "check"): select write + steer (modes, global admission order) → threshold guard → continuation → followUp (at may_finish) → before_run_end → finish; one commit;resume_checkpoint (threshold: "skip"): the same planner runs inside the publication commit — compaction entry (on success) first, then selected write/steer items, then the routed continuation, all in one transaction. Any placed conversational item routes to assistant.ready with that item as trigger, overriding a may_finish resume continuation (steer semantics). With nothing queued: need_assistant resumes commit assistant.ready directly; may_finish resumes commit checkpoint{may_finish} as the resting leaf and the live pass continues into the finish phases.Threshold without the durable marker. The guard is derived from the branch: threshold fires only when shouldCompact holds and the newest compaction entry is older than the trigger entry. A committed threshold compaction is therefore its own marker — any crash re-entry sees the newer compaction and skips. A declined threshold compaction publishes no entry and needs no marker: decline never lands on checkpoint. The decline decision stays process-local at summary.deciding until its single continuation commit (assistant.ready, follow-up + ready, or the terminal after before_run_end); a crash in that window restores deciding and re-runs before_compaction under its documented repetition contract. A live decline/recheck loop is structurally impossible because no decline path re-enters the threshold-checking pass.
before_run_end without hooks on the mutation line. The finish arm is the one multi-phase case: (1) on-line verdict, no commit — reached only at may_finish with no eligible write/steer/followUp; (2) hook runs off the line; (3) on-line replan from current state — if the inbox gained eligible items or control changed, the stale hook result is dropped and the planner takes the new decision; otherwise the single commit is the follow-up injection (entry + assistant.ready) or the terminal transaction. Stale-dropping is replanning, not a version check.
Concrete control trace (post-R3 names; threshold compaction with may_finish resume and a steer arriving mid-compaction):
TX1 settlement: response n7 + usage + tip, S=checkpoint{may_finish, trigger n7}
pass (check): shouldCompact && newestCompaction < n7 → prepare
TX2: preparation, S=summary.deciding{boundary: resume_checkpoint{may_finish, n7}}
TX3 (any time): steer s1 → pendingEntry(s1), laneState.inbox += s1
TX4..n: deciding → ready → effect_pending → nested request/usage settlements
TX-pub (skip): insert compaction c1, tip=c1, insert s1 entry (parent c1),
delete pendingEntry(s1), laneState.inbox -= s1,
S=assistant.ready{trigger s1} ← one commit, checkpoint never re-entered
…turn answers s1; next settlement → checkpoint{may_finish, trigger n9}
pass (check): newestCompaction c1 > … tokens small → no threshold; inbox empty; verdict finish
→ before_run_end off-line → replan: still empty, still running
TX-final: record + cleanup + laneState{current: null, last: op}
Crash between TX4..n and TX-pub → structural recovery (attempt unknown, per existing rules); crash during the hook window → checkpoint{may_finish} restored, pass re-runs (threshold skipped by the recency guard), hook re-runs per contract.
Slices, in order (the order is load-bearing — deleting the marker while decline still re-entered checkpoint would live-loop):
resume_checkpoint boundary (threshold: "skip"); structural success/decline route directly except for a may_finish resting checkpoint, which remains safe for either outcome while the durable marker still exists. Keep the route inline until R1b-2 creates its second caller. Both patch fields are still written and checked by the old checkpoint pass — harmless.checkpoint pass to single-commit exits with the recency guard, implement the three-phase finish arm, delete skipInboxOnce and thresholdCheckedTriggerEntryId.Enqueue during idle/run/structural/cancelled states always admits; acceptance preserves global order with modes applied and remainders retained; one-at-a-time cadence gives each remaining steer its own later boundary; followUp is captured at idle acceptance and only at may_finish during a run; a boundary crash before its commit consumes nothing; threshold is checked at most once per boundary by construction; idle append places queued writes first; items survive every terminal status and process loss; cancelQueued works at every boundary; watch groups by tag without changing order; no terminal transaction deletes a lane-inbox payload; a lone queued write never validates acceptance.
Replace the three per-family outcome unions and laneLastResult with one immutable per-operation result record. The record is the public settled outcome; there is no separate outcome type, no embedded entries, and no hydration.
type TerminalStatus = "completed" | "declined" | "aborted" | "failed";
/** Stored at operationResult(operationId) — namespace "pi.result" — by the terminal transaction. Immutable, lane-lived. */
interface OperationResultRecord {
operationId: string;
kind: "run" | "compaction" | "navigation"; // meta.intent.kind; matches OperationAdmission.kind
status: TerminalStatus;
error?: OperationError; // status "failed"
fromTipId: string | null; // meta.sourceTipId — start of the transcript segment
tipId: string | null; // lane tip at terminal — end of the segment
startedAt: number; // Unix ms, from meta
endedAt: number; // Unix ms, Date.now() at terminal planning
}
/** Convenience-only suspension observation for prompt()/resume() (M8). Never stored. */
interface SuspendedRun { operationId: string; status: "suspended"; deferred: DeferredHandle }
The record is a disposition plus a pointer to the transcript segment (fromTipId, tipId]. It never lists intermediate work (compactions, turns, tools) and never embeds entries. There is no tipEntry and no OperationOutcome union: a caller that wants the payload dereferences tipId with the already-public getEntry/findEntry (for runs, the final message was additionally delivered by message_end/entry_added). This buys three things: terminal planners perform zero outcome reads, observation cannot fault on a missing entry (a getEntries throw inside hydration was a harness-fault path for a read-only convenience), and the future protocol never serializes Entry inside results — a result frame is eight flat fields. SuspendedRun is the one non-terminal observation, exists only on convenience returns, and carries nothing derivable elsewhere.
src/harness/session/types.ts — add the record; delete the LaneLastResult union; LaneState gains lastOperationId: string | null; delete failure_drain, RunFailureDrainOperation, and FailureProvenance.src/harness/session/values.ts — operationResult(operationId) = value<OperationResultRecord>("pi.result", operationId); delete laneLastResult. The pi.op.* lifetime grammar stays total: no operation-lived namespace survives its terminal transaction.src/harness/agent-harness.ts — delete RunOutcome, CompactionOutcome, NavigationOutcome, OptionalFinalAssistant, TerminalOperationOutcome, ResumeOutcome, and the transitional OperationOutcome union; DriveOutcome.settled carries OperationResultRecord directly (no duplicate operationId field); result aliases become
RunResult = Result<OperationResultRecord | SuspendedRun, …>,
CompactionResult = Result<{ compaction: OperationResultRecord; run?: OperationResultRecord | SuspendedRun }, …>,
NavigationResult = Result<{ navigation: OperationResultRecord; run?: OperationResultRecord | SuspendedRun }, …>,
ResumeResult = Result<OperationResultRecord | SuspendedRun, …>;
QueueResult loses NoActiveRun and absorbs NextRunResult. SuspendedRun is declared here but constructed only by M8 convenience paths.run_end and navigation_end carry { runId, status, error?, fromTipId, tipId }. compaction_end is a segment event, not a terminal event — it also closes in-run threshold/overflow brackets — and always carries { runId, reason, status: "completed" | "declined" | "failed" | "aborted", error?, entryId? }, where entryId names the compaction entry on success. Embedded entry/summaryEntry/final-assistant fields are dropped everywhere (entry_added/message_end already delivered payloads).src/harness/runtime/types.ts — FinishDecision carries the record instead of lastResult.src/harness/runtime/lane.ts — terminal suffix per §2; getLastResult is replaced by getResult(operationId, context): Promise<OperationResultRecord | undefined> — one getValue, nothing else; inspectExecution reports lastOperationId only.src/harness/runtime/drive/terminal.ts — hydrateTerminalOutcome/hydrateOperationOutcome are deleted outright; the file keeps only operationCleanupWrites and the operationResultRecord constructor. Finish decisions materialize { kind: "settled", outcome: record } from the record they already built — no reader dereference inside any terminal planner.src/harness/runtime/drive/{checkpoint,response,structural,recovery,deferred,tools}.ts — every finish decision constructs the record; runCompletion, per-family last-result construction, and final-assistant plumbing in results are deleted. Every former failure_drain producer instead commits terminal failure directly: response publication and cleanup stay in the same transaction, in-run structural failure closes compaction_end then run_end, and queued lane input remains untouched for a later ordinary run. (latestAssistantEntryId stays in scope for cancellation classification and checkpoint logic; it just no longer feeds results.)src/harness/runtime/restore.ts — restore lastOperationId; attachment never reads records.cancel_requested records aborted; no path commits any other status under cancellation. M7 adds the enforcing test; M9 adds the Part 9 invariant.pi.result values.drive(id) arms: current → install/join; record → returned directly; neither → OperationMismatch.Record written for every terminal path of every operation kind and status; drive(id) returns records for arbitrarily old ids across reopen; getResult is a plain value read returning the record or undefined; no terminal planner or observation path reads entries for the result; lastOperationId restore; suspension never stored; fork exclusion; segment pointers correct (fromTipId = pre-acceptance tip, including navigation); compaction_end closes both standalone and in-run brackets, including aborted via reconciliation; each former failure-drain producer finishes in one transaction and preserves every lane-inbox item for a later run.
Collapse 22 leaves to the 13 in §1; rebuild structural.ts's state-shape layer on one summary quadruple with an explicit result-boundary datum.
interface OperationScope { control: Control; settings: RunSettings; latestAssistantEntryId: string | null }
type ResultBoundary = // closed; do not extend
| { kind: "resume_checkpoint"; resumeAfter: CheckpointData } // in-run threshold/overflow
| { kind: "finish" } // standalone compaction
| { kind: "commit_navigation"; targetId: string; label?: string };
interface SummaryTask {
taskId: string;
reason?: "manual" | "threshold" | "overflow";
customInstructions?: string;
boundary: ResultBoundary;
}
The summary algorithm kind is derived from the boundary (resume_checkpoint/finish → compaction; commit_navigation → branch summary); it is not stored, so no contradictory kind/boundary combination is representable. summary.ready/effect_pending/retry_wait additionally carry the generation snapshot (result entry id, configuration, stream options, retry policy, attempt counters); the snapshot drops the taskId/kind/reason fields it previously duplicated. Canonical declarations land in session/types.ts. Uniform scope means compaction/navigation acceptance captures settings exactly like run acceptance; latestAssistantEntryId stays null for structural intents.
| Intent | Admissible leaves |
|---|---|
| run | starting, checkpoint, assistant.*, tools, deferred.*, summary.* with boundary resume_checkpoint |
| compaction | summary.* with boundary finish |
| navigation | navigation.ready_to_commit; summary.* with boundary commit_navigation |
Forbidden and unreachable by construction (assert in tests, never implement): any edge into starting or navigation.ready_to_commit other than acceptance; summary.* → tools/assistant.* directly; navigation.ready_to_commit → summary.*; terminal → anything.
Every summary boundary (hook decline, hook-supplied result, generated success, terminal generation failure, model unavailability) plans on the mutation line and switches on boundary in one visible place:
resume_checkpoint → publish result and restore the marked checkpoint on success or threshold decline; overflow decline or structural failure closes the compaction bracket and terminal-fails the run in the same commit;finish → publish the compaction entry and terminal-complete (or terminal declined/failed) in one commit;commit_navigation → the single move/summary/label/terminal commit (or terminal declined/failed).A cancel_requested boundary defers to reconciliation and never takes its continuation.
src/harness/session/types.ts — the 13-leaf union, OperationScope, SummaryTask, ResultBoundary; delete RunScope, CompactionScope, NavigationScope, NavigationSummaryScope, RunCompactionScope, StructuralTask, per-family Extract aliases, isRunOperationState.src/harness/runtime/drive/structural.ts — rebuild: keep durableCompactionPreparation/readers, the nested request seam, performStructuralAttempt, runCompactionThreshold, prepareOverflowCompaction, commitNavigation; delete the seven union aliases, the startsWith guards, and the three-armed effectPendingFromReady/retryWaitFromEffect/readyFromRetryWait/publishStructuralReady/publishStructuralDecline/publishStructuralFailure/publishCompactionResult-vs-publishNavigationSummary split; one quadruple of converters plus one boundary switch replace them.src/harness/runtime/drive/{checkpoint,generation,response,recovery,deferred,tools,tool-placement,terminal}.ts — leaf literal renames (run.checkpoint → checkpoint, …); response.ts overflow arm constructs summary.deciding with resume_checkpoint; no other behavior change.src/harness/runtime/progress.ts — frame/checkpoint fences currently match the leaf literals "run.assistant.effect_pending", "run.deferred.effect_pending", and "run.tools"; retarget them to the neutral leaves.src/harness/runtime/restore.ts — reachability predicate above.src/harness/runtime/lane.ts — capturedModel and watch switch over the neutral leaves; acceptance writes neutral initial leaves.docs/runtime-simplification.md — replace the 22-leaf list and status.test/harness/runtime/drive-structural.test.ts and other focused tests — retarget; behavioral assertions survive.Everything the M6 foundation covered, re-expressed over neutral leaves, plus: reachability accept/reject matrix (including corrupted boundary/intent combinations faulting restore); each boundary arm × {success, hook result, decline, failure, model absence} × {running, cancelled}; a post-terminal leak scan asserting every pi.op.* address is gone for every leaf while the pi.result record is present; crash/reopen at every leaf under every intent that can reach it.
Implemented and reviewed. Make the internal graph total without public wiring.
Create src/harness/runtime/drive/reconcile.ts, src/harness/runtime/drive.ts, and focused reconciliation and switch tests.
requestOperationAbortPackage-private expected-id primitive:
OperationMismatch when the expected id is not the current durable operation, including when that id already has a settled result record;beginAbort on a matching live Drive;control = cancel_requested (with no drained fields) plus removal of steer/followUp-tagged ids from the lane inbox and deletion of their pendingEntry values; payloads are read in the same mutation and returned; nextRun/write items stay;signalAbort after the commit;{ type: "operation_abort", operationId, steer, followUp } and return once cancellation is durable. This family-neutral event replaces run_abort/runId; the lane snapshot already identifies the operation kind. Repeat calls against the same current cancelled operation publish nothing, drain nothing further, and report newlyRequested: false.With no Drive but a matching current durable operation, it commits the same marker and starts no pass. There is no control.drained*; the drained payloads exist only in the returned result (accepted loss: a crash inside the abort window loses their content).
Checked before before_drive and ordinary dispatch; a single switch over the 13 leaves; never starts new ordinary work. It must handle:
aborted without taking their ResultBoundary continuation;status: "aborted");Queued lane-inbox items are not applied or deleted by reconciliation; nextRun/write items simply remain queued.
Each Drive owns a private close controller and exposes its signal to mandatory cleanup. closeGate() aborts that controller on harness close or fault; operation abort does not. Deferred-provider cancellation uses this close-only signal, so the cleanup request can start after durable operation cancellation but cannot outlive harness shutdown. Do not add a harness-global cancellation controller.
runtime/drive.ts owns one direct state.at switch over 13 leaves. It imports only complete procedure modules. No graph table, action interpreter, ownership-loss arm, external-finalization arm, or storage-state reload. A continue result must correspond to a replaced Lane.state projection or to currently observable cancel_requested control, which the switch routes to reconciliation before ordinary dispatch; any other unchanged continuation is an invariant defect.
Public methods remain guarded through M7.
Implemented. Execution guards were removed only after every leaf and reconciliation path became total.
Order:
settings; writes the R3 entry leaves);drive install/join/record lookup (three arms, §7);requestAbort;watchSession as the sole SliceNotImplemented method.steer/followUp/nextRun are tag sugar over one enqueue and always admit. queue_update and LaneSnapshot.queues expose the same tagged ordered inbox, including pending writes; clients group by tag without reordering it.
Findings from a working RPC-shaped client replica (mini TUI exercise) fold in here. The goal is one testable contract: a remote client renders exclusively from a replicated LaneSnapshot and the event stream, with no side-channel getters.
reduceLaneSnapshot(snapshot, event): LaneSnapshot | { rebase: true } from packages/agent. It is the single supported event fold: in-run compaction_start/compaction_end are segments and must not clear operation (the fold knows the open operation's kind, so no event field is needed); run_suspend keeps operation non-null with deferred set; only run_end, navigation_end, and compaction_end under a compaction-kind operation are operation-terminal. navigation_end returns { rebase: true } because the moved tip may lie outside the replica. Conformance assertion: for every non-navigation flow, folding a watch() snapshot over its own event stream equals a later watch() snapshot — this equivalence test is what keeps the event vocabulary complete.LaneSnapshot gains configuration: LaneConfiguration, lastResult?: OperationResultRecord (replacing the bare lastOperationId field), a real faulted flag (the current hardcoded false is a defect), and session stats: SessionStats as the usage baseline (usage events already carry totals; the snapshot supplies the value before the first event).config_update variant whose value is data carries value/previous (streamOptions, retryPolicy, compactionSettings, steeringMode, followUpMode, plus the existing lane variants); tools/resources remain notification-only because registries are code — clients re-fetch names.setModel. setModel accepts ModelIdentity ({ provider, modelId }); a live Model object is a process-local registry concern, and an unregistered identity fails in-band at generation exactly like any registry absence.WatchHandle.resnapshot(context): Promise<LaneSnapshot> captures a fresh snapshot on the mutation line, serialized against the same stream — the recovery path after { rebase: true } with no teardown/re-subscribe choreography.run_start/compaction_start/navigation_start carry startedAt (operation starts: meta.startedAt; in-run segment starts: the commit timestamp), so folds never invent times.prompt/skill/promptFromTemplate: accept + drive; return Result<OperationResultRecord | SuspendedRun, …> — the record for terminal outcomes, SuspendedRun when the run defers.compact/navigateTree: accept A + drive A. If A settled with status completed, declined, or failed and eligible steer/followUp/nextRun items exist, accept a continuation run B with the public empty-prompt request ({ kind: "prompt", prompt: "" }): acceptance places no request messages and is legal exactly when its capture places at least one conversational item; OperationMeta.intent is an ordinary run intent with empty promptEntryIds. Then drive B and return { compaction|navigation: A, run?: B }. Never after aborted: abort already drained steer/followUp, and the terminal-control invariant (§7) makes status ∈ {completed, declined, failed} imply the operation was never durably cancelled, so the rule is decidable from the record alone. B is an ordinary run — it emits run_start, runs before_run (with prompt: [], the existing captured-only acceptance shape), and owns the full run graph with no continuation-aware special case.InvalidMessage) or LaneBusy, and the convenience returns A without run. Both histories are valid. A crash between A's terminal and B's acceptance leaves the items queued; no auto-start on reopen.resume: inspect + drive current id, one deferred-poll permit; returns OperationResultRecord | SuspendedRun.abort: inspect + requestAbort + ensure a reconciliation pass; returns the drained steer/followUp payloads.getResult(operationId): public.compact() with continuation ≡ accept(A); drive(A); accept({ kind: "prompt", prompt: "" }); drive(B)), byte-identical writes and events — externally reproducible by any scheduler.One install and same-id joins; stale-id isolation; record lookup for old ids; caller cancellation before/after installation; close/fault during cooperative and non-cooperative effects; accept/drive vs convenience equivalence including continuation via the public empty-prompt request; steer-only, followUp-only, and nextRun-only continuation each start run B; both continuation race orders; abort drain-and-return including the both-orders abort/settlement race; enqueue during every state through public surfaces; full crash matrix across all 13 leaves; no unhandled detached rejection; no SliceNotImplemented except watchSession; reducer fold-equivalence across every non-navigation flow (including in-run compaction segments, suspend/resume, retry waits, and reattach mid-stream); navigation_end fold returns rebase and resnapshot restores equivalence; snapshot configuration/lastResult/faulted/stats replicate through events alone.
Implemented. docs/harness.md is reconciled to the runtime and normative on its own. Updated sections include:
pi.result namespace, laneLastResult removed, LaneState.inbox/lastOperationId; pi.op.* stays strictly operation-lived;pi.lane.lastResult; state the bounded growth tradeoff — one small immutable record per operation, retained forever, carried through JSONL snapshot compaction;tipId the rewrite removes (retain-dangling or delete);OperationScope, SummaryTask/ResultBoundary (kind derived from boundary), Control without drained fields, CheckpointData without skipInboxOnce/thresholdCheckedTriggerEntryId;waitForIdle-then-append escape hatch;drive(id) total, getResult);queue_update and end-event payloads (compaction_end as a segment event with aborted);cancelQueued, and continuation;run_end, navigation_end, compaction-kind compaction_end) versus segment brackets (in-run compaction_start/compaction_end) versus non-terminal lifecycle (run_suspend leaves the operation durably open), and name reduceLaneSnapshot as the normative fold;stopAtType applies after ordering, so oldestFirst + stopAtType: "compaction" returns the oldest segment; document the canonical context read as newestFirst + reverse;AgentLane, while whole-tree browsing, forks, label inventory, and session listing live beside it in Session/repository services composed by an RPC facade.Also update docs/runtime-simplification.md status and remove every remaining promotion reference in the repository's docs. Grep gate:
rg -i 'promotion|drainedSteer|drainedFollowUp|laneLastResult|lastResult|skipInboxOnce|thresholdChecked' packages/agent/src packages/agent/docs
(matches must be zero in src; docs may retain historical notes only inside completed-milestone records.)
Implemented and reviewed. Core Session identity alone is not a valid provider cache lineage: several lanes may issue concurrent requests over divergent transcripts. Ordinary assistant requests therefore derive identity from Session metadata id plus lane name; structural requests remain isolated.
SimpleStreamOptions.sessionId, including prompt-cache keys, affinity headers, WebSocket/session-resource caches, deferred requests, and structural summary requests. Ordinary harness generation now forwards the derived lane identity; legacy src/agent.ts keeps its independent conversation id; structural summary requests mint fresh ids.Session metadata id + ":" + lane name; do not store another durable identifier. It remains stable for the lane's lifetime and differs across lanes in one Session.cacheRetention: "none" and fresh request identity per nested request remain the baseline unless the review proves a safe, useful alternative.sessionId means only KV caching; some adapters also use it for headers, WebSocket reuse, fallback state, or resource cleanup.Ordinary assistant requests use the derived lane identity, structural requests keep fresh identities with cacheRetention: "none", and deferred handle polling sends no cache identity. The policy is documented in docs/harness.md and independently reviewed before WP05 completion.
Tracked follow-up; not part of the M8/M10 public-drive gate. A trivial mini coding-agent Session produced an approximately 300 KB JSONL file because live assistant streaming persists many pi.pending.assistant_frame list appends. Logical frame cleanup does not reclaim bytes already appended to the JSONL history, so short conversations can have disproportionate durable storage and replay cost.
The authoritative follow-up is the mobile assistant-output handoff and its numbered prerequisites in the mobile handoff README. It addresses the complete path rather than only batching frames: Chord op tracking, scoped pending-output durability, tool/assistant output reduction, and message_update replication amplification. Preserve the existing crash contract: an admitted assistant effect remains reconstructible, progress observation remains useful, and settlement retires operation-owned pending state.
Exit checks:
session/** durable storage; imports no runtime module
execution/** neutral provider/tool/gate mechanics
runtime/types.ts LaneState, Drive, command decisions
runtime/progress.ts frame/tool progress channels
runtime/drive/*.ts direct procedures
runtime/drive.ts total flat switch, created only in M7
runtime/lane.ts Lane actor and public surfaces
runtime/harness.ts Harness lifecycle and Lane composition
Procedure modules import concrete Lane<TContext> type-only. TContext remains object | undefined invariant. No any, Lane<any>, as unknown as, @ts-expect-error, inline imports, parameter properties, enums, or other non-erasable TypeScript syntax.
Do not introduce:
ResultBoundary stays a closed three-arm data union switched in one visible place;OptionalFinalAssistant, or embedded result entries in stored records;drive);at runtime checks for the sole top-level writer;getValues batching;phase/segment discriminator field on structural events — the reducer derives segment-vs-terminal from the open operation's kind;AgentLane; branch-relative reads/appends remain part of the lane facade, while broader administration lives beside it.Procedure-specific writes, effect admission, settlement classification, and event construction remain visible at their call sites.
After every code stage:
npm run check
Run each modified focused test file from the package root. Do not invoke the full Vitest suite directly. Run ./test.sh only for final package validation or when explicitly requested.
Review checkpoints are mandatory at the end of R3, R1b, M7, M10, and final completion. Delegated reviews use provider anthropic and model claude-fable-5.
Final greps:
rg 'lost_ownership|LostOwnership|DriveAbandoned|commandDriveOwned|installerSignal|finalizedOutcome|OperationEnded' \
packages/agent/src/harness packages/agent/test/harness
rg 'drainedSteer|drainedFollowUp|laneLastResult|RunOutcome|CompactionOutcome|NavigationOutcome|TerminalOperationOutcome|skipInboxOnce|thresholdCheckedTriggerEntryId' \
packages/agent/src/harness
rg 'SliceNotImplemented' packages/agent/src/harness/runtime
rg ': any\b|<any>|as unknown as|@ts-expect-error' packages/agent/src/harness/runtime
Final exit conditions:
watchSession is the only deferred public method;drive(id) answers every settled operation id in the session;