From cfc6dd2101f77cc42e205d6ae19a99ff103d08b6 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 19 Aug 2026 09:21:49 +0200 Subject: [PATCH] feat(agent): remove drive deadlines --- packages/agent/docs/harness.md | 62 +- packages/agent/docs/runtime2.md | 634 ------------------ .../docs/work-packages/00-runtime1-removal.md | 4 +- .../work-packages/02-atomic-run-acceptance.md | 4 +- .../03-remove-drive-deadlines.md | 183 +++++ packages/agent/src/harness/agent-harness.ts | 2 - packages/agent/test/harness/types.test.ts | 10 + 7 files changed, 224 insertions(+), 675 deletions(-) delete mode 100644 packages/agent/docs/runtime2.md create mode 100644 packages/agent/docs/work-packages/03-remove-drive-deadlines.md diff --git a/packages/agent/docs/harness.md b/packages/agent/docs/harness.md index 46c017306..695f05ae3 100644 --- a/packages/agent/docs/harness.md +++ b/packages/agent/docs/harness.md @@ -229,7 +229,7 @@ Had the tool declared `replay: "safe"` (a read, a query), the harness would have - **Exactly-once external effects.** See above. Hooks with their own side effects must be idempotent, keyed by operation id. - **Provider stream resumption.** The harness never reattaches to a provider stream. Committed assistant frames (§3.7) preserve the latest durable partial for recovery and reconnect display, but a settled response is persisted *completely* before anything classifies it. - **Multiple writers.** One process per session. The serving layer routes accordingly, and the SQLite backend enforces it with a fenced lease (§1.7). Lanes cover the workload that looks like multi-writer. -- **Work scheduling.** The harness never creates platform alarms, scans repositories for abandoned sessions, leases hosted submissions, or promises an HTTP receipt. It reports durable waits and safe yields through `drive`; the serving layer decides when and where to call it again. +- **Work scheduling.** The harness never creates platform alarms, scans repositories for abandoned sessions, leases hosted submissions, or promises an HTTP receipt. It reports durable waits through `drive`; the serving layer decides when and where to call it again. - **Replication.** A session lives in one place. - **Durable write history.** Replaceable values retain only current state; lists retain elements only until whole-list deletion. No API or table exposes replaced values or deleted elements. Order-of-write assertions in tests use an instrumented storage decorator around `commit()` (Part 9); production auditing belongs to the telemetry layer (§5.8). - **Deletion as a runtime feature.** Entries and usage rows are never deleted: compaction changes provider context, not storage, and terminal cleanup deletes only values/lists. Note that `retainedTail` copies old messages forward into newer compaction entries and summaries derive from old content, so compaction is not erasure either. Compliance-grade "erase this" is the administrative precise rewrite (§2.9), the sole sanctioned exception. @@ -1543,7 +1543,7 @@ A declined summarized navigation moves nothing: the leaf stays at the source, an Captured `nextRun` items already have their payloads in `pendingEntry(id)` values; the command inserts their entries before request prompt entries, deletes their values, and removes their ids from `pendingNextRun` — the placement half of queued-content staging (§1.8). A late item keeps its enqueue-minted id and remains queued for the next run. The run's immutable `promptEntryIds` name only the request's normalized prompt entries; hook-injected messages are not acceptance metadata. -A run is accepted in payload-free `starting`. An accepted-but-undriven operation is simply open; its immediate `before_run` consumption and checkpoint commit are identity-free durable progress. Its first real drive, after cancellation/deadline checks and `before_drive`, invokes `before_run` outside the lane line. The consuming command appends returned messages and atomically replaces `starting` with `checkpoint{need_assistant(false)}`, using the newest injected entry as trigger or otherwise the newest accepted entry. It preserves concurrent inbox and control changes. A crash before that commit may rerun the hook; a crash after it cannot. Under `cancel_requested`, reconciliation invokes neither prerequisite. +A run is accepted in payload-free `starting`. An accepted-but-undriven operation is simply open; its immediate `before_run` consumption and checkpoint commit are identity-free durable progress. Its first real drive, after the cancellation check and `before_drive`, invokes `before_run` outside the lane line. The consuming command appends returned messages and atomically replaces `starting` with `checkpoint{need_assistant(false)}`, using the newest injected entry as trigger or otherwise the newest accepted entry. It preserves concurrent inbox and control changes. A crash before that commit may rerun the hook; a crash after it cannot. Under `cancel_requested`, reconciliation invokes neither prerequisite. Structural preparation may be computed outside the lane line against an observed source leaf and settings snapshot, but it grants no ownership and blocks no lane work. Its final command must revalidate that the lane is idle and that every state-dependent source still matches; stale preparation is discarded and recomputed. A concurrent accepted operation returns `LaneBusy`. Unsummarized navigation performs validation in its one command. @@ -1889,7 +1889,7 @@ Public operation status is a small process-relative observation, not a predictio - `aborting`: durable control is `cancel_requested` and cancellation reconciliation is next; - idle: `current: null`; it is not an open-operation status. -There is at most one process-local record per lane. It owns one `drive` pass, not the durable operation's whole lifetime: a pass may settle, reach a durable wait, yield safely for its caller's budget, or return `action_required` at a manual breakpoint. Durable operation phase and control remain the recovery authority. Model/tool availability is checked only when a procedure reaches the corresponding effect-admission boundary; absence becomes an in-band durable error/result rather than a public operation status. +There is at most one process-local record per lane. It owns one `drive` pass, not the durable operation's whole lifetime: a pass may settle, reach a durable wait, or return `action_required` at a manual breakpoint. Durable operation phase and control remain the recovery authority. Model/tool availability is checked only when a procedure reaches the corresponding effect-admission boundary; absence becomes an in-band durable error/result rather than a public operation status. ```ts interface ActiveOperation { @@ -1937,9 +1937,6 @@ async function executeDrivePass(operationId: string, if (!current) return settledFromLastResult(operationId); if (current.state.control.status === "cancel_requested") return reconcileCancellation(current); - if (atSafeBoundary(current) && deadlineReached(options)) - return { kind: "yielded", operationId }; - await runBeforeDrive(current); while (true) { @@ -1950,8 +1947,6 @@ async function executeDrivePass(operationId: string, if (mustWaitForRetry(current, options)) return retryWait(current); if (isDeferredSuspended(current) && !options.pollDeferred) return deferredWait(current); - if (atSafeBoundary(current) && deadlineReached(options)) - return { kind: "yielded", operationId }; if (current.state.phase.kind === "starting") await settleBeforeRun(current); else if (hasOrphanedEffect(current)) @@ -1975,7 +1970,7 @@ async function executeDrivePass(operationId: string, } ``` -The pseudo-code shows ownership and catch boundaries, not required helper names. A deadline is cooperative: it prevents the next transition or external effect only at an effect-free safe boundary; an admitted provider, tool, or hook may finish after it. `waitForRetry: false` returns the durable `notBefore`; `true` starts the existing abort-aware retry timer only when the deadline does not require an earlier return. `pollDeferred: true` grants at most the existing one-poll permit. `AbortRequested` is the one internal control-flow signal used when cancellation interrupts a parked breakpoint or wins the effect gate. A scope holding live local results catches it before the outer boundary when those values affect reconciliation. In particular, the parallel-tool procedure retains its promise map, stages each finalized started call as it settles, and materializes only the source-ready prefix; an assistant procedure parked after receiving a response retains that response long enough to settle it under cancelled control. +The pseudo-code shows ownership and catch boundaries, not required helper names. `waitForRetry: false` returns the durable `notBefore`; `true` starts the existing abort-aware retry timer. `pollDeferred: true` grants at most the existing one-poll permit. `AbortRequested` is the one internal control-flow signal used when cancellation interrupts a parked breakpoint or wins the effect gate. A scope holding live local results catches it before the outer boundary when those values affect reconciliation. In particular, the parallel-tool procedure retains its promise map, stages each finalized started call as it settles, and materializes only the source-ready prefix; an assistant procedure parked after receiving a response retains that response long enough to settle it under cancelled control. Ordinary procedures are straight-line across their own durable restart points. Assistant generation is representative: @@ -1999,13 +1994,13 @@ Normal execution handles checkpoints, ready/retry states, planned or locally own - the same operation already has `ActiveOperation`: join that pass's `completion`; - the same operation has no task: install one deferred completion promise and fresh gate before releasing the lane line, then start the pass after `Session.mutate` returns. -The first caller to install a pass supplies that pass's deadline/wait policy. Same-operation joiners receive the same `DriveOutcome`; a convenience caller whose policy requires more progress may call `drive` again after that pass returns. This arbitration lets `prompt` literally compose `accept` and `drive` even if an alarm reaches the accepted operation first, while preserving one process-local driver. Every drive is fenced by its expected operation id, so a stale wake for A never drives later operation B. +The first caller to install a pass supplies that pass's retry-wait and deferred-poll policy. Same-operation joiners receive the same `DriveOutcome`; a convenience caller whose policy requires more progress may call `drive` again after that pass returns. This arbitration lets `prompt` literally compose `accept` and `drive` even if an alarm reaches the accepted operation first, while preserving one process-local driver. Every drive is fenced by its expected operation id, so a stale wake for A never drives later operation B. -A newly installed real pass checks cancellation and its deadline, then invokes `before_drive` once before recovery or ordinary work. Joiners do not rerun it. A later pass after yield, retry wait, deferred suspension, close/reopen, or process restart invokes it again. Durable phase and local ownership alone determine dispatch: `starting` requires `before_run`; an unowned `effect_pending` phase is orphaned; every other phase follows its ordinary procedure. No fresh/continue/restore marker exists. +A newly installed real pass checks cancellation, then invokes `before_drive` once before recovery or ordinary work. Joiners do not rerun it. A later pass after retry wait, deferred suspension, close/reopen, or process restart invokes it again. Durable phase and local ownership alone determine dispatch: `starting` requires `before_run`; an unowned `effect_pending` phase is orphaned; every other phase follows its ordinary procedure. No fresh/continue/restore marker exists. -Every task has one outer `finally` path: after its last durable commit, wait/yield decision, or terminal observation and after publishing its final local events, it performs a no-write lane job that verifies it still owns the slot and removes its `ActiveOperation`; only then does `completion` resolve/reject. Close/fault removes records under the harness admission barrier. This keeps a lane locally busy until the old continuation and observations are actually done, even when its terminal transaction already made the durable lane idle. External finalization uses the same eventual `finally` removal after signalling the task (§4.9). +Every task has one outer `finally` path: after its last durable commit, wait decision, or terminal observation and after publishing its final local events, it performs a no-write lane job that verifies it still owns the slot and removes its `ActiveOperation`; only then does `completion` resolve/reject. Close/fault removes records under the harness admission barrier. This keeps a lane locally busy until the old continuation and observations are actually done, even when its terminal transaction already made the durable lane idle. External finalization uses the same eventual `finally` removal after signalling the task (§4.9). -Convenience methods add policy, not another execution path. `prompt`/`skill`/`compact`/`navigateTree` call `accept` and then drive the returned id; they wait locally through retry waits and return on terminal, deferred suspension, or `action_required`. `resume(context)` inspects the current id and drives it with one deferred-poll permit. `abort(context)` calls `requestAbort`, then ensures cancellation reconciliation has a drive pass; it still returns once cancellation is durable. A scheduler calls the expected-id primitives directly and may persist the admission, set an alarm from `waiting.notBefore`, or resume immediately after `yielded`. Normal applications do not manage operation ids when resuming. +Convenience methods add policy, not another execution path. `prompt`/`skill`/`compact`/`navigateTree` call `accept` and then drive the returned id; they wait locally through retry waits and return on terminal, deferred suspension, or `action_required`. `resume(context)` inspects the current id and drives it with one deferred-poll permit. `abort(context)` calls `requestAbort`, then ensures cancellation reconciliation has a drive pass; it still returns once cancellation is durable. A scheduler calls the expected-id primitives directly and may persist the admission or set an alarm from `waiting.notBefore`. Normal applications do not manage operation ids when resuming. `AgentHarness.create(options, context)` restores the minimal projection and reports open operations but installs no tasks and starts no hooks, providers, tools, timers, breakpoints, or application callbacks. @@ -2259,7 +2254,7 @@ The lane's configured model identity comes from `laneConfig`. A current operatio Recovery begins only when an open operation has no `ActiveOperation` and a matching `drive({ operationId }, context)` installs a real pass owner. `AgentHarness.create(options, context)` never drives. `resume(context)` inspects and drives the current operation without exposing its id and grants the same pass one deferred-poll permit. `requestAbort(operationId, context)` with no task commits cancellation but installs nothing; the next drive enters cancellation reconciliation directly. -The pass first reloads durable control. If cancellation is requested, it invokes neither `before_drive` nor `before_run` and enters §4.6. At an effect-free deadline boundary it yields before invoking either prerequisite. Otherwise it gates and invokes `before_drive`; failure rejects this pass without faulting the harness or writing durable progress. Model/tool implementations are resolved only at the actual operation boundary that needs them. An unavailable provider/model or configured request tool becomes a non-retryable configuration failure before request intent; an unavailable requested tool becomes a synthetic error result. Neither condition suspends the operation. Durable phase then decides the work: `starting` runs and settles `before_run` as §3.6 specifies; an unowned pending effect is orphaned by construction and follows the table below; all other phases continue ordinarily. In manual mode, reaching a breakpoint returns `action_required` with the parked `ActionInfo`; automatic mode crosses it. A later `executeAction(context)` releases one barrier, while `runToCompletion(context)` continues across action branches until it reaches a non-action result. +The pass first reloads durable control. If cancellation is requested, it invokes neither `before_drive` nor `before_run` and enters §4.6. Otherwise it gates and invokes `before_drive`; failure rejects this pass without faulting the harness or writing durable progress. Model/tool implementations are resolved only at the actual operation boundary that needs them. An unavailable provider/model or configured request tool becomes a non-retryable configuration failure before request intent; an unavailable requested tool becomes a synthetic error result. Neither condition suspends the operation. Durable phase then decides the work: `starting` runs and settles `before_run` as §3.6 specifies; an unowned pending effect is orphaned by construction and follows the table below; all other phases continue ordinarily. In manual mode, reaching a breakpoint returns `action_required` with the parked `ActionInfo`; automatic mode crosses it. A later `executeAction(context)` releases one barrier, while `runToCompletion(context)` continues across action branches until it reaches a non-action result. | Orphaned restart point | Activation recovery | |---|---| @@ -2291,12 +2286,12 @@ retry_wait retry_wait → drive({ waitForRetry: true }) reaches the retry breakpoint → assert EffectGate open and immediately start retry timer -→ deadline: Session.mutate verifies the same wait and commits ready +→ timer reaches notBefore: Session.mutate verifies the same wait and commits ready → requestAbort: timer wakes after durable cancellation and reconciliation runs → close: local task rejects; no durable write ``` -If a drive deadline precedes `notBefore`, it returns `waiting` even when `waitForRetry` is true. At or after `notBefore`, either policy verifies the same durable wait and commits `ready` without an unnecessary timer. +At or after `notBefore`, either policy verifies the same durable wait and commits `ready` without an unnecessary timer. ## 4.6 Abort and cancellation reconciliation @@ -2333,7 +2328,7 @@ With queued steer/follow-up, successful abort removes their ids from the inbox, ### No live task -After reopen, safe yield, or suspension there may be no gate to close or work to signal. `requestAbort(operationId, context)` then writes or observes `cancel_requested` and returns without installing `ActiveOperation`. A scheduler sets a wake and later calls `drive`. The `abort(context)` convenience ensures a same-id drive pass is claimed after the marker; in manual mode that pass parks at its first cancellation breakpoint, and in automatic mode it proceeds immediately. A crash between the marker and that claim is harmless because the next drive observes durable cancellation first. +After reopen or suspension there may be no gate to close or work to signal. `requestAbort(operationId, context)` then writes or observes `cancel_requested` and returns without installing `ActiveOperation`. A scheduler sets a wake and later calls `drive`. The `abort(context)` convenience ensures a same-id drive pass is claimed after the marker; in manual mode that pass parks at its first cancellation breakpoint, and in automatic mode it proceeds immediately. A crash between the marker and that claim is harmless because the next drive observes durable cancellation first. ### Reconciliation @@ -2394,7 +2389,7 @@ At most one terminal transaction commits because every candidate rereads ownersh ## 5.1 The lane surface -Expected rejection returns `Result.err`. `accept` resolves only after durable operation acceptance; `drive` resolves after one owned pass settles, waits, yields, or reports a manual action. Convenience operations return `Result.ok` for completed, failed, aborted, deferred suspension, and `action_required` outcomes. Storage faults, close during active work, and invariant defects reject the promise. +Expected rejection returns `Result.err`. `accept` resolves only after durable operation acceptance; `drive` resolves after one owned pass settles, waits, or reports a manual action. Convenience operations return `Result.ok` for completed, failed, aborted, deferred suspension, and `action_required` outcomes. Storage faults, close during active work, and invariant defects reject the promise. ```ts interface NavigateOptions { summarize?: boolean; label?: string; customInstructions?: string } @@ -2419,8 +2414,6 @@ interface OperationAdmission { interface DriveOptions { /** Expected durable owner; a stale call must never drive another operation. */ operationId: string; - /** Epoch-ms budget checked only at effect-free safe boundaries. */ - deadline?: number; /** Wait locally through retry_wait; false returns waiting/notBefore. Default false. */ waitForRetry?: boolean; /** Permit at most one poll of a suspended deferred response. Default false. */ @@ -2515,7 +2508,7 @@ interface WatchHandle { snapshot: T; start(listener: EventListener): void; un `accept(request, context)` starts no driver and does not consult process-local model/tool registries. Omitted operation ids are minted before the acceptance command; supplied ids follow §1.2. A supplied id is correlation, not an unbounded idempotency index: a scheduler first calls `inspectExecution(context)`, drives when that id is current, settles its own receipt when that id is latest-terminal, and calls `accept` only when neither is true. Skill/template expansion precedes storage. Prompt intent names only normalized request prompt messages, excluding captured `nextRun` and hook injections. -`drive(options, context)` is fenced by `operationId`. If that id is current and unowned, it claims one pass; if the same pass is already active, it joins; if it is the latest terminal id, it returns the complete settled outcome; otherwise it returns `OperationMismatch`. A deadline can prevent the next effect but cannot preempt one already admitted. Same-operation joiners receive the installed pass's outcome or current manual action and may call `drive` again if their policy requires more progress. Each invocation retains its own Context; a joiner never reparents execution or combines its abort signal into another caller's execution signal. +`drive(options, context)` is fenced by `operationId`. If that id is current and unowned, it claims one pass; if the same pass is already active, it joins; if it is the latest terminal id, it returns the complete settled outcome; otherwise it returns `OperationMismatch`. Same-operation joiners receive the installed pass's outcome or current manual action and may call `drive` again if their policy requires more progress. Each invocation retains its own Context; a joiner never reparents execution or combines its abort signal into another caller's execution signal. The convenience surface adds only invocation-lifetime policy: `prompt`/`skill`/`compact`/`navigateTree` call `accept` then drive the returned id with local retry waiting; `resume(context)` inspects and drives the current id with one deferred-poll permit; `abort(context)` requests durable cancellation and ensures reconciliation is driven. In manual mode these methods return `action_required`; applications continue with `executeAction(context)` or `runToCompletion(context)`. A serving layer may instead store `OperationAdmission`, return to its client, schedule from `waiting.notBefore`, and invoke the expected-id primitives later. @@ -2608,7 +2601,6 @@ type DriveOutcome = | { kind: "waiting"; operationId: string; reason: "retry"; notBefore: number } | { kind: "waiting"; operationId: string; reason: "deferred"; deferred: DeferredHandle } - | { kind: "yielded"; operationId: string } | { kind: "action_required"; operationId: string; action: ActionInfo }; type DriveResult = Result; @@ -3219,7 +3211,7 @@ Hook durability has three classes: | Hook | When | Event | Result | Durability | |---|---|---|---|---| -| `before_drive` | once per newly installed real drive pass, after cancellation/deadline checks and before recovery or ordinary work | `{ operation }` | `void`; failure rejects the pass with no durable progress | pass-local | +| `before_drive` | once per newly installed real drive pass, after the cancellation check and before recovery or ordinary work | `{ operation }` | `void`; failure rejects the pass with no durable progress | pass-local | | `before_run` | while a run is durably `starting`, after `before_drive` | `{ prompt, resources }` | `{ messages? }`; output commits with `starting → checkpoint` | transition-consumed: injected messages and the checkpoint commit together | | `before_run_end` | at a normal finish boundary | `{ runId, messages }` | `{ followUp? }` | transition-consumed: a follow-up and continuation commit together, or the terminal transaction consumes the no-follow-up decision | | `transform_context` | per request, `AgentMessage` level, before `toProviderMessages` | `{ messages, systemPrompt }` | `{ messages?, systemPrompt? }` | request-local: neither the transformed messages nor system prompt is persisted | @@ -3237,7 +3229,7 @@ Replay and repetition: | Hook | Repetition contract | |---|---| -| `before_drive` | once per installed pass; repeats after every yield/wait/suspension or process loss; joiners do not rerun it | +| `before_drive` | once per installed pass; repeats after every wait/suspension or process loss; joiners do not rerun it | | `before_run` | may rerun while `starting` until its consuming commit succeeds; never runs after that transition | | `transform_context`, `before_request`, `before_payload` | once per request attempt, including retry and replay | | `after_response` | per settled response unless abort wins before it starts | @@ -3561,7 +3553,7 @@ This part is a rolling plan for the repository as it exists now, not a replay of 4. Move any newly discovered normative behavior into Parts 0–7 or Part 9. 5. Only then replace the detailed row with a concise overview link. -Do not scaffold handoffs for distant work. This intentionally leaves Part 8 hybrid: linked rows for concrete packages and detailed rows for future candidates. `runtime2.md` is implementation history/status, not a competing package plan. +Do not scaffold handoffs for distant work. This intentionally leaves Part 8 hybrid: linked rows for concrete packages and detailed rows for future candidates. Every package implements its named concern end to end and tests its normal path, introduced states, owned crash boundaries, and both orders of owned races. Consumption-time dereference checks, implementation resolution, hooks, events, and breakpoint descriptors land with the package that first needs them; earlier packages do not build generic future machinery. If implementation exposes a contradiction or materially simpler boundary, stop for review rather than silently extending the package. @@ -3571,11 +3563,12 @@ Every package implements its named concern end to end and tests its normal path, |---|---|---|---| | WP00 (complete) | Reconciled acceptance/hooks, harvested runtime1 scenarios, switched the public factory, and deleted runtime1 without adding execution behavior. | Approved redesign and durability handoffs were present. | [Runtime1 removal](work-packages/00-runtime1-removal.md) | | WP01 (complete) | Replaced retained register/custom-state APIs with bound values/lists across Session, Memory, JSONL, SQLite, instrumentation, conformance, and public application access. | WP00 | [Bound values and lists](work-packages/01-bound-values-lists.md) | -| WP02 (in progress) | Implement atomic prompt/skill/template acceptance, minimal open-operation attachment, lane-line execution inspection, and gap-free ad-hoc lane watch capture without starting execution. | WP01 | [Atomic acceptance and coherent attachment](work-packages/02-atomic-run-acceptance.md) | +| WP02 (complete) | Implemented atomic prompt/skill/template acceptance, minimal open-operation attachment, lane-line execution inspection, and gap-free ad-hoc lane watch capture without starting execution. | WP01 | [Atomic acceptance and coherent attachment](work-packages/02-atomic-run-acceptance.md) | +| WP03 (complete) | Removed the unused wall-clock drive deadline and non-durable yielded outcome before redesigning execution manual-first. | WP02 | [Remove drive deadlines](work-packages/03-remove-drive-deadlines.md) | The old Types, Session/Memory, JSONL, and SQLite rows described work already present in the repository. Their remaining cross-backend delta was WP01; they are not separate future packages. -The former R1 row is likewise no longer a separate candidate. Its public types, lane/config shell, restore inventory, hook/event primitives, breakpoint/effect gate, neutral assistant/tool blocks, and direct primitive coverage are already present. WP02 takes atomic acceptance. The remaining lane-owned drive pass, expected-id fencing, same-id joining, deadline yield, and task lifecycle move into R2, where they first own real work; do not build an owner-only intermediate package. The harness-specific checkpoint update callback shape and its compatibility test remain R4 work. +The former R1 row is likewise no longer a separate candidate. Its public types, lane/config shell, restore inventory, hook/event primitives, breakpoint/effect gate, neutral assistant/tool blocks, and direct primitive coverage are already present. WP02 took atomic acceptance. WP03 removes deadline/yield policy before the remaining lane-owned drive pass, expected-id fencing, same-id joining, manual transition control, and task lifecycle are reassessed as one manual-first execution package. The detailed R2/R3 rows below preserve requirements but are not an approved handoff. The harness-specific checkpoint update callback shape and its compatibility test remain R4 work. ## Future package candidates @@ -3585,8 +3578,8 @@ These rows preserve requirements, not frozen package boundaries. Create their ha |---|---|---|---| | S3 | **Search** | The standalone `SessionSearchService` plus repo catch-up utilities (§2.8): core entry search as `SessionSearch`, session-level ranked results, optional `searchEntries?: SessionSearch`, `remove()`, the `SessionSearchSyncTarget` cursor/index-batch contract, sync enumeration and catch-up outside the service contract, debounced notify as a utility, `(sessionId, storeGeneration)` cursor keys, and the reference SQLite FTS5 implementation working over any backend's repository through the sync utility. | Cursor catch-up from empty against existing sessions, idempotent re-index after crash mid-batch, notify-utility/sweep equivalence, sessions-vs-entries queries and ranking, removal and reconciliation, shared-index multi-process discipline. | | S4 | **Dev TUI and Client** | A minimal `AgentClient` over one lane — `LaneSnapshot` plus `watch()` events, `prompt`/`steer`/`followUp`/`abort`/`resume`/`cancelQueued`, `pi.lane.lastResult` read — and a throwaway alt-screen TUI on `packages/tui`: transcript from snapshot and events, input box, status/queue display, abort key. Built first against a scripted fake client on the shared types; binds to the real harness as runtime packages land. Not final. | Compiles; fake-client smoke test. No durability obligations. | -| R2 | **Minimal no-tool drive** | Starting from WP02's durable `starting` operation, add the first real lane-owned `ActiveOperation` pass with expected-id fencing, same-id joining, latest-result lookup, safe deadline yields, stable `ActionInfo` breakpoints, real `action_required` pass behavior, `executeAction`, `runToCompletion`, and `EffectGate`; run driver-owned `before_drive`/`before_run`, make `prompt`/`skill`/`promptFromTemplate` compose `accept`+`drive`, and execute one assistant generation through captured context/options, payload/response hooks, intent/effect/settlement, synchronous per-event frame appends with latest-promise fencing, atomic frame-list deletion, usage, terminal cleanup plus `pi.lane.lastResult`, results, events, and telemetry. Unavailable captured model or configured active-tool definitions fail in-band before intent with configuration provenance; no response/usage is fabricated. | Accepted `starting` close/reopen/drive; prompt convenience and explicit accept+drive produce byte-identical writes/events; one same-id pass installed/joined and stale ids isolated; safe yields and stable descriptors; successful run with final assistant fields; one frame append per convertible event with no per-frame storage await and only the latest promise retained; settlement awaits the latest frame write and deletes the list atomically; frame-append failure faults before `after_response`; caller rejection and adapter-reported failures; exact ordering; supplied operation id propagates through `run_end` and `pi.lane.lastResult`; terminal cleanup; close at every boundary; invalid references rejected at consumption; re-enable coding-agent's one-shot and worker-owned Harness prompt tests. | -| R3 | **Generation recovery and retry** | Activation-only unknown-effect recovery that reduces committed frames into the synthetic zero-usage partial error under the reserved ids and then classifies ordinarily (retry or failure drain), non-retryable configuration failure when a later ready/retry attempt cannot resolve its captured model/tools, ordinary stop/error/deferred classification, provider-compliant `aborted`, failure-drain foundation, and both retry-wait policies: explicit drive returns `waiting/notBefore`, while convenience drive may own the abort-aware timer. Define stable breakpoint descriptors for retry timers and recovery commits. Overflow classification remains explicitly unimplemented until R9. | Every generation state before/after reopen; accept/close/reopen/drive; recovery with no frames, partial frames, and authoritative end-frame content; synthetic settlement always commits under reserved ids with the interruption warning and deletes the frame list; interrupted partial tool calls never execute; `after_response` never runs for synthetic settlement; caps/backoff; no timer under `waitForRetry:false`; due and future `notBefore`; deadline before retry; convenience/explicit-drive durable equivalence; timer abort-first/start-first; stable descriptors; stop/error/aborted/deferred classification; non-retryable configuration unavailability. | +| R2 | **Minimal no-tool drive** | Starting from WP02's durable `starting` operation, add the first real lane-owned `ActiveOperation` pass with expected-id fencing, same-id joining, latest-result lookup, stable `ActionInfo` breakpoints, real `action_required` pass behavior, `executeAction`, `runToCompletion`, and `EffectGate`; run driver-owned `before_drive`/`before_run`, make `prompt`/`skill`/`promptFromTemplate` compose `accept`+`drive`, and execute one assistant generation through captured context/options, payload/response hooks, intent/effect/settlement, synchronous per-event frame appends with latest-promise fencing, atomic frame-list deletion, usage, terminal cleanup plus `pi.lane.lastResult`, results, events, and telemetry. Unavailable captured model or configured active-tool definitions fail in-band before intent with configuration provenance; no response/usage is fabricated. | Accepted `starting` close/reopen/drive; prompt convenience and explicit accept+drive produce byte-identical writes/events; one same-id pass installed/joined and stale ids isolated; stable descriptors; successful run with final assistant fields; one frame append per convertible event with no per-frame storage await and only the latest promise retained; settlement awaits the latest frame write and deletes the list atomically; frame-append failure faults before `after_response`; caller rejection and adapter-reported failures; exact ordering; supplied operation id propagates through `run_end` and `pi.lane.lastResult`; terminal cleanup; close at every boundary; invalid references rejected at consumption. | +| R3 | **Generation recovery and retry** | Activation-only unknown-effect recovery that reduces committed frames into the synthetic zero-usage partial error under the reserved ids and then classifies ordinarily (retry or failure drain), non-retryable configuration failure when a later ready/retry attempt cannot resolve its captured model/tools, ordinary stop/error/deferred classification, provider-compliant `aborted`, failure-drain foundation, and both retry-wait policies: explicit drive returns `waiting/notBefore`, while convenience drive may own the abort-aware timer. Define stable breakpoint descriptors for retry timers and recovery commits. Overflow classification remains explicitly unimplemented until R9. | Every generation state before/after reopen; accept/close/reopen/drive; recovery with no frames, partial frames, and authoritative end-frame content; synthetic settlement always commits under reserved ids with the interruption warning and deletes the frame list; interrupted partial tool calls never execute; `after_response` never runs for synthetic settlement; caps/backoff; no timer under `waitForRetry:false`; due and future `notBefore`; convenience/explicit-drive durable equivalence; timer abort-first/start-first; stable descriptors; stop/error/aborted/deferred classification; non-retryable configuration unavailability. | | R4 | **Tools** | Add the harness-specific tool-update checkpoint options/callback while leaving pi-ai's `AgentToolUpdateCallback` unchanged, then compose §5.7's neutral tool phases with bound context, invocation-scoped memos, `pi.op.tool_args`, scalar `pendingToolOutput(operationId, invocationId)` checkpoints, `outcome_ready` staging in `pi.pending.entry`, completion-order outcome durability, source-ordered materialization, orphan recovery/replay, sequential/parallel promises, hook-blocked/genuine-length outcomes, hooks/events/usage, and stable descriptors. Missing requested tools stage direct `ToolResultMessage` errors with no `details`; missing safe-replay implementations synthesize interruption rather than suspending. The built-in bash tool emits live updates at 100 ms and requests distinct checkpoints at most every two seconds. | Stable invocation id/memos across safe replay; every planned/effect-pending/outcome-ready/completed state; parallel out-of-order staging without replay; synchronous update callback with the latest delivery and latest checkpoint write awaited before `after_tool`; `tool_end` before staging; every requested checkpoint enqueued synchronously with only the latest promise reference retained and no write dropped or coalesced; tool-selected cadence/duplicate suppression; late-write fencing after settlement; JSONL checkpoint growth/compaction; safe replay deletes old checkpoint; unsafe recovery with/without checkpoint; missing planned tools become source-ordered error results; absent or no-longer-safe replay implementations synthesize interruption without invented typed details; source-ordered materialization; termination and abort-ready states. | | R5 | **Inbox, configuration, and writes** | `nextRun`/steer/follow-up via `pendingEntry(id)` values, pending-payload consumption-time dereference, `cancelQueued` triage (`not_found`), durable drain markers, checkpoint consumption with pending-value deletion, immediate total config setters, deferred tree writes, adjustments, and stable breakpoint descriptors for the checkpoint boundaries introduced here. | Capture/cancel/consume races, repeated cancellation answering `not_found`, invalid queue payload kinds and missing pending values rejected at consumption, one-at-a-time crash after one drain, pending-value/entry exclusivity at every boundary, custom-write continuation, config-step race, writes surviving reopen. | | R6 | **Abort, close, and failure drain** | Add expected-id `requestAbort` as commit/signalling only. Compose `abort()` from inspection, marker, and same-id drive. Implement orthogonal control, per-phase reconciliation including planned/effect-pending/outcome-ready tools, best-effort deferred cancellation, waiters/run-when-idle, controlled-crash close, complete terminal deletion of memos/checkpoints/staged outcomes, external-finalization stop, and stable descriptors. | Cancellation with/without live pass; idempotency/fencing; abort at every tool state; restored interruption incorporates latest checkpoint without replay; restored cancellation synthesizes aborted responses preserving frame-reduced partial content and deleting the list; outcome-ready values survive and materialize; checkpoint/memo/frame cleanup; parked barriers; deferred cancellation; close races including a non-cooperative started tool that cannot block close and parallel retained promises that are all observed without later starts or unhandled rejection; external finalization fences late scalar/list writes; failure drain. | @@ -3595,7 +3588,7 @@ These rows preserve requirements, not frozen package boundaries. Create their ha | R9 | **Threshold and overflow compaction** | In-run structural decision, durable once-per-trigger threshold marker, continuation preservation, all overflow predicates, atomic response/preparation publication, specified normalization/projection, one overflow recovery flag, bounded second failure, and stable breakpoint descriptors for added boundaries. | Threshold decline/empty across reopen, all overflow classifier/preparation inputs, no overflow tool plan, genuine length, crash/reopen at every transition. | | R10 | **Navigation** | Add navigation `OperationRequest` acceptance without task installation and compose `navigateTree()` from accept+drive. Implement navigation source/target/summary consumption-time dereference, validation, summarized decision/generation, one final transaction combining move/summary/leaf/label with terminal writes, summary-only navigation hook, and stable descriptors. | Explicit accept+drive versus convenience equivalence; root/current/unknown rejection; invalid durable source/target/summary combinations; accept/close/reopen/drive; summarized/unsummarized paths; final leaf at summary; abort race; exact atomic publication including cleanup. | | R11 | **Schema version and migrations** | Chained migrate-on-open under the writer lease, with total mappings for open `pi.op.state` including `outcome_ready`, staged `pi.pending.entry` results, invocation memos, optional progress checkpoints, and assistant frame lists (paged in sequence order, mapped preserving `seq` or deleted whole-key); address namespace/key/kind migrations; JSONL lenient old-shape replay, mandatory post-migration compaction, refuse-newer. | Version gate; chained crash-idempotent migration; planned/effect-pending/outcome-ready tool states mapped safely; staged result/checkpoint/memo mappings; every legacy frame mapped or the whole list explicitly discarded with no completion inference; resumed operation correctness; old bytes retired. | -| R12 | **Surface completion** | Complete full-session watch, remaining live execution snapshot updates and outcome-ready visibility, event catalog/order/filtering, telemetry, public exports, backend parity, hosted-control/convenience documentation, remove scaffold code including the S4 fake client, rename the sole implementation from `runtime2/` to `runtime/`, update imports/tests, and delete `runtime2.md`. | Snapshot/event gap; attach during active effect with live/durable divergence for both frames and checkpoints; restored effect-pending exposing reduced `streamingMessage`; live partial precedence over durable reduction; outcome-ready, waiting/yielded/terminal states; no replay of old message or tool lifecycle; recovered turn brackets balanced by turn id across deadline yields and reopened on the next pass; close before tool-effect admission creates no raw `pi.harness.tool` span; stale-wake fencing; sensitive-event/content-free telemetry; full race/crash matrix on all backends. | +| R12 | **Surface completion** | Complete full-session watch, remaining live execution snapshot updates and outcome-ready visibility, event catalog/order/filtering, telemetry, public exports, backend parity, hosted-control/convenience documentation, remove scaffold code including the S4 fake client, rename the sole implementation from `runtime2/` to `runtime/`, and update imports/tests. | Snapshot/event gap; attach during active effect with live/durable divergence for both frames and checkpoints; restored effect-pending exposing reduced `streamingMessage`; live partial precedence over durable reduction; outcome-ready, waiting/terminal states; no replay of old message or tool lifecycle; recovered turn brackets balanced by turn id across durable waits and reopened on the next pass; close before tool-effect admission creates no raw `pi.harness.tool` span; stale-wake fencing; sensitive-event/content-free telemetry; full race/crash matrix on all backends. | | P1 | **Protocol schemas** | Future work after the internal harness is complete: define shared TypeBox schemas for serializable pi-ai and harness protocol data, derive the corresponding TypeScript types from those schemas, and reuse them across client/server protocol boundaries. Validation runs only on untrusted wire input and never inside Session, Storage, operation procedures, or in-process extensions. | Schema/type parity, accepted and rejected client/server payloads, protocol round trips, and no validators or schema construction on internal storage paths. | # Part 9 — Invariants and tests @@ -3631,10 +3624,10 @@ Operations: 19. **The settlement transaction that commits a response with `stopReason: "aborted"` must, in that same transaction, write an operation state with `control.status === "cancel_requested"`.** The invariant is scoped to the committing transaction — later terminal cleanup or forks may remove the state without violating it. Providers must comply with the harness-owned signal contract; violation is corruption. 20. Attachment restores and validates only the small lane/operation projection (§3.3, §4.4). Detailed presentation references are validated by `watch(context)` under the lane line; drive references are validated by their consuming procedure. Missing or contradictory required data faults that consumer, while optional frame/checkpoint absence is legal. Live phase/effect-identity checks remain concurrency fences. `pi.lane.lastResult` never determines an open operation's next procedure. 21. At most one terminal transaction ever commits per operation. A task mutation that finds its operation values absent raises internal `OperationEnded`; the task stops without writing and resolves from `pi.lane.lastResult` (§4.9). -22. At most one `ActiveOperation` drive pass exists per lane. Acceptance and taskless `requestAbort` never install one. A matching `drive` installs it before releasing the lane mutation line; another matching drive joins that pass, and a stale id starts nothing. Every pass's outer `finally` removes it in a no-write lane job only after final local observation; external finalization retains and signals it until that path runs. Each newly installed pass invokes `before_drive` once after cancellation/deadline checks; joiners do not. `starting + cancel_requested` invokes neither `before_drive` nor `before_run`. +22. At most one `ActiveOperation` drive pass exists per lane. Acceptance and taskless `requestAbort` never install one. A matching `drive` installs it before releasing the lane mutation line; another matching drive joins that pass, and a stale id starts nothing. Every pass's outer `finally` removes it in a no-write lane job only after final local observation; external finalization retains and signals it until that path runs. Each newly installed pass invokes `before_drive` once after the cancellation check; joiners do not. `starting + cancel_requested` invokes neither `before_drive` nor `before_run`. 23. The §4.2 `assertOpen()` catalog is complete. Every listed hook/provider/tool/timer integration checks immediately before invocation with no yield between them; no unlisted code checks the operation gate. Preparation stays before the check, and admitted asynchronous provider setup/delegation are continuations of the same operation owning `EffectGate.signal`. 24. `drive` and `requestAbort` are fenced by expected operation id. They may affect only that current operation; `drive` may also return its matching latest terminal result. A stale wake for A cannot drive or cancel B. -25. `drive` returns `yielded` only at an effect-free safe boundary. It starts no following transition, hook, provider, tool, or timer after deciding to yield. An already-admitted effect may outlive the caller's deadline and must settle normally or be recovered after task loss. +25. No public drive option encodes a wall-clock budget or partial-progress return. An admitted effect settles normally or is recovered from durable state after task loss; host scheduling and process termination remain outside the harness contract. 26. Convenience operations and their explicit primitive compositions produce the same durable writes, events, results, and recovery behavior. Their only difference is process-local waiting/scheduling policy. 27. Each logical tool call's public `invocationId` is its reserved `resultEntryId`: unique within the session and unchanged across safe replay. Tools must await invocation-memo writes. Such writes synchronously enqueue, verify effect-pending ownership on the lane line, and are deleted with outcome staging. 28. Completed tool calls form a source-ordered prefix. A sequential suffix permits at most one effect-pending or outcome-ready call before planned calls; a parallel suffix may mix `planned`, `effect_pending`, and `outcome_ready`. Completion-order outcome staging never extends the prefix; source-ordered materialization does. @@ -3677,7 +3670,6 @@ Each durable mutation race has exactly two durable histories. Process-local owne | `requestAbort` vs `before_drive`/`before_run` admission | admission first → the complete hook pipeline runs and its consuming command observes cancellation; cancellation first → reconciliation runs and neither hook starts | | `requestAbort` vs ordinary operation admission | admission first → operation is invoked with the signal; cancellation first → gate refuses invocation | | `requestAbort` vs parked ordinary breakpoint | cancellation rejects the interruptible barrier with `AbortRequested`; the ordinary statement after it does not run, and reconciliation proceeds after the lane mutation | -| drive deadline vs next ordinary effect | yield decision first → no effect starts; effect admission first → it may finish after the deadline before the pass can return | | `close` vs parked manual action | action rejected unexecuted; durable state is the committed prefix | | attachment vs concurrent resume | attachment owns the session before publication; after return, resume rereads durable state and stale `open` remains harmless | | watcher registration vs state publication | watcher first → old snapshot plus the complete buffered event batch; publication/enqueue first → new snapshot without that old batch | @@ -3689,7 +3681,7 @@ Each durable mutation race has exactly two durable histories. Process-local owne ## 9.3 Test tiers -**Tier A — state and drive.** For every state in Part 3, construct it durably, close, reopen, drive its expected operation id, and assert the next breakpoint, wait, yield, or terminal result. Coverage must include: accepted and restored `starting` with hook output applied once by its consuming transaction; minimal projection restore; ad-hoc watch capture with one compaction-bounded transcript scan and state-directed payload reads, with required presentation references faulting watch and optional frame/checkpoint absence remaining legal; drive references are detected at consumption and captured step configuration is not re-resolved; assistant intent with no settlement — with no frames, partial frames, and authoritative end-frame content — recovered into synthetic partial settlements below and at the retry cap; settlement followed by each classification branch; every settled stop reason surviving except the two deliberate normalizations; a self-contained deferred step with copied configuration, consecutive polls, repeated equal-handle pending responses, ready and terminal responses, and handle-mismatch normalization into durable failure; every tool state including planned, effect-pending safe/unsafe with and without a checkpoint, outcome-ready, and completed; parallel B/C outcomes staged before A and never replayed after reopen; source-ordered materialization; invocation memos surviving safe replay and dying at outcome staging; a batch where every call terminates; genuine-length synthetic outcomes; every overflow crash position; every navigation state; abort at every position; unavailable model/configured tools as non-retryable configuration failure; missing requested tools as detail-free error results; every terminal transaction proving deletion of tool args, memos, checkpoints, frame lists, staged outcomes, and pending payloads; `pi.lane.lastResult`; preserved `pendingNextRun`; representation exclusivity for every reserved id; and every half-completed recovery prefix. +**Tier A — state and drive.** For every state in Part 3, construct it durably, close, reopen, drive its expected operation id, and assert the next breakpoint, wait, or terminal result. Coverage must include: accepted and restored `starting` with hook output applied once by its consuming transaction; minimal projection restore; ad-hoc watch capture with one compaction-bounded transcript scan and state-directed payload reads, with required presentation references faulting watch and optional frame/checkpoint absence remaining legal; drive references are detected at consumption and captured step configuration is not re-resolved; assistant intent with no settlement — with no frames, partial frames, and authoritative end-frame content — recovered into synthetic partial settlements below and at the retry cap; settlement followed by each classification branch; every settled stop reason surviving except the two deliberate normalizations; a self-contained deferred step with copied configuration, consecutive polls, repeated equal-handle pending responses, ready and terminal responses, and handle-mismatch normalization into durable failure; every tool state including planned, effect-pending safe/unsafe with and without a checkpoint, outcome-ready, and completed; parallel B/C outcomes staged before A and never replayed after reopen; source-ordered materialization; invocation memos surviving safe replay and dying at outcome staging; a batch where every call terminates; genuine-length synthetic outcomes; every overflow crash position; every navigation state; abort at every position; unavailable model/configured tools as non-retryable configuration failure; missing requested tools as detail-free error results; every terminal transaction proving deletion of tool args, memos, checkpoints, frame lists, staged outcomes, and pending payloads; `pi.lane.lastResult`; preserved `pendingNextRun`; representation exclusivity for every reserved id; and every half-completed recovery prefix. For each recovery prefix: close, reopen, drive, and compare against uninterrupted recovery. Invoking recovery twice from the initial prefix is **not** sufficient. Every operation kind also covers accept → close before first drive → reopen → drive. @@ -3732,7 +3724,7 @@ One corruption assertion constructs an `aborted` response with running control d | **Attachment** | Minimal lane/operation projection restore plus open inventory; it starts no execution. | | **Invocation Context** | Immutable explicit telemetry/cancellation lineage for one call. It is never durable state or a shared receiver default. | | **Acceptance** | The atomic creation of one durable operation by `accept`; it starts no driver. | -| **Drive pass** | One process-local ownership span created or joined by `drive`, ending at settlement, a durable wait, safe yield, or a reported manual action whose continuation remains owned until released. | +| **Drive pass** | One process-local ownership span created or joined by `drive`, ending at settlement, a durable wait, or a reported manual action whose continuation remains owned until released. | | **Effect** | Anything not pure computation: commit, provider request, tool, hook, timer. | | **Repeat-sensitive effect** | One whose repetition is observable outside the harness. | | **Operation state** | The complete durable state of one open operation — the `operationState(operationId)` value and restart point after task loss. | diff --git a/packages/agent/docs/runtime2.md b/packages/agent/docs/runtime2.md deleted file mode 100644 index be77b0c20..000000000 --- a/packages/agent/docs/runtime2.md +++ /dev/null @@ -1,634 +0,0 @@ -# AgentHarness `runtime2/` historical implementation notes - -> **Archived after WP00.** Do not maintain this file as status, contract, or roadmap. `harness.md` is normative and `work-packages/*.md` are the only executable plans. These notes preserve implementation history and rejected-design lessons until R12 renames `runtime2/` to `runtime/` and deletes this file. - -## Original goal - -The runtime2 experiment built a replacement AgentHarness under `packages/agent/src/harness/runtime2/` to test whether a lane-owned in-memory model could make the complete runtime materially smaller and easier to trace than runtime1. WP00 selected it as the public factory and deleted runtime1 before execution behavior was complete. - -## Retained architecture lessons - -- one restore constructs authoritative owned `LaneState`; -- all lane-control writes route through `Lane.command()` and `Session.mutate()`; -- command planners make state-dependent decisions under the lane line and return explicit `commit`, `return`, or `reject` outcomes; -- commit commands publish memory after commit and synchronously materialize from `CommitResult`; -- committed values and owned state are immutable snapshots updated only by replacement; -- providers, tools, hooks, timers, events, and task waits stay outside the lane line; -- the Lane-backed `sessionTree` routes leaf/branch reads and idle/run appends through owned state; -- close/fault seal lanes before queued work can proceed, while admitted successful commits still publish; -- harness creation never writes durable suspension; `suspension` is process-local inspection metadata; -- durable control `running` means open/permitted; public `running` requires a real process-local drive owning an actual continuation; -- do not install a process owner, join promise, or `running` status around an effect-free classification shell; -- missing identity payloads are `{ tools: string[]; model?: string }`; one lane configuration references only one model; -- no separate runtime/controller abstraction is necessary; -- the rejected acceptance/drive attempt recreated runtime1-style orchestration through provisional ownership, `OperationHost`, `OperationLocalState`, and a fake owner pass; it was reverted completely. - -## Historical slice completeness rule - -A slice must close the entire named concern at parity, not only its immediately callable methods. Before editing, inventory all related constructor inputs, owned state, defaults, validation, public accessors, events, lifecycle behavior, recovery implications, and tests. Do not defer adjacent behavior merely because no current procedure consumes it yet. If scope must be deferred, name that boundary before implementation and get explicit agreement. - -## Historical reading policy - -`packages/agent/docs/harness.md` is the normative source. Until their designs are folded into it, `values.md`, `tool-durability.md`, and `assistant-durability.md` are binding handoffs for bound typed value/list addresses, tool outcomes/checkpoints/memos, and assistant frames. Runtime1 is implementation inspiration only: do not preserve its abstractions, internal bookkeeping, or accidental behavior unless the normative contract independently requires them. For each slice, read its listed sections completely. If any handoff or `harness.md` changed since the previous slice, inspect that diff and reread affected sections. - -Read a source or test file completely before editing that file. For dependencies that will not be edited, begin with the relevant exported types or functions and expand only when a concrete question requires it. - -Do **not** pre-read later runtime phases. In particular, R2.0 must not load assistant procedures, tool procedures, or the complete R2–R4 behavioral suites. The purpose of the slices is to keep both implementation and reasoning local. - -This archived plan is not source authority. If it disagrees with `harness.md`, a work-package handoff, or committed behavior, ignore this file; do not update it. - -### R2.0 — Restore one lane - -Read: - -- `harness.md`: §§0.2–0.3, 0.7–0.8, 1.3–1.4, 3.1–3.3, and 4.4; -- the lane/configuration/operation definitions in `packages/agent/src/harness/session/types.ts` and built-in address constructors in `session/values.ts`; -- repository/session helpers needed by the focused restore test; -- only the exported shape of the existing `restoreLane()` when comparing terminology. - -Do not read runtime1 procedures or R2–R4 behavior tests. Runtime2 intentionally trusts committed internal values and does not copy runtime1's semantic restore audit. - -### R2.1 — Restore a configured session - -Read: - -- `harness.md`: §§2.3, 2.8, and the lane inventory portions of 4.4; -- completed `runtime2/types.ts`, `runtime2/lane.ts`, and `runtime2/restore.ts`; -- address-based `Session.getValue()`, `Session.scanValues()`, `Session.readList()`, and `Session.mutate()` signatures in `session/types.ts`; -- focused session tests only when a repository helper is needed. - -Do not read runtime1 initialization, provider, assistant, tool, or full harness-facade implementations. - -### R2.1a — Lane owner and transition kernel - -Read: - -- `harness.md`: §§1.4, 3.4, 4.3, 4.7–4.8, and invariants 2, 21–23; -- completed runtime2 restore/session-loader files; -- `packages/agent/src/harness/session/lane-mutations.ts`; -- `SessionMutator`, `Write`, and `CommitResult` definitions in `session/types.ts`; -- focused mutation-line and instrumented-storage tests only when needed for test helpers. - -Do not read provider, assistant, tool, or full harness-facade implementations. - -### R2.2 — Main seeding and runtime shell - -Read: - -- `harness.md`: §§2.3, 2.8, 4.4–4.5, and 5.2; -- completed runtime2 restore/session-loader files; -- main-lane creation and configuration-seeding portions of `session/session.ts`; -- only constructor/seeding cases in `agent-harness-runtime.test.ts`. - -Do not read live assistant or tool procedures, and do not reintroduce runtime1's semantic restore audit. - -### R2.3 — In-memory reads, config, lanes, and close - -Read: - -- `harness.md`: §§2.3–2.4, 4.3, 4.7–4.8, 5.1–5.5; -- config, lane registry, facade delegation, close, and fault portions of `runtime/agent-harness-runtime.ts`; -- query/configuration/public-session-view portions of `runtime/lane-runtime.ts`; -- `packages/agent/src/harness/events.ts` and `hooks.ts` only for surfaces this slice instantiates; -- corresponding shell/configuration/close tests in `agent-harness-runtime.test.ts` and session tests. - -Do not read operation execution procedures. - -### R2.4 — Contract reconciliation and atomic run acceptance - -Read completely before editing: - -- `harness.md` §§3.1–3.6, 4.1–4.5, and 5.5–5.6 plus this document's historical R2.4 notes; -- the current diff of `harness.md`, then §§3.1–3.6, 4.1–4.4, 5.1–5.3, 5.5–5.6, build rows R1–R2, and matching invariants/races; those sections contained the superseded hook semantics that WP00 later reconciled; -- current `runtime2/harness.ts`, `lane.ts`, `types.ts`, `restore.ts`, and all focused runtime2 tests; -- public request/result/event/hook types in `agent-harness.ts`; -- neutral `hooks.ts`, `skills.ts`, `prompt-templates.ts`, bound value/list addresses and transaction helpers, and pending-entry reads; -- runtime1 acceptance only to inventory production/public-contract consequences, never as architecture; -- acceptance and restore cases in `agent-harness-r2.test.ts` and `restore.test.ts`. - -Do not read provider/tool procedures or implement ownership in a no-work shell. Neutral harness imports remain allowed; `runtime2/` must never import `harness/runtime/*`. - -### R2.5 — Minimal no-tool run parity - -Read: - -- `harness.md`: §§3.2, 3.7, 3.12–3.13, 4.1–4.2, 5.5–5.8; -- `values.md` assistant-frame consumer and `assistant-durability.md` fresh-stream/settlement sections; -- pi-ai's exported assistant-message frame converter/reducer; -- `execution/assistant.ts`; -- checkpoint, ordinary-ready/intent/settlement, and terminal paths in: - - `runtime/run-driver.ts`; - - `runtime/checkpoint-procedure.ts`; - - `runtime/assistant-procedure.ts`; - - `runtime/transitions.ts`; -- `execution-assistant.test.ts`; -- `agent-harness-r2.test.ts`; -- only R2-relevant restore cases in `restore.test.ts`. - -Do not read R3 recovery/retry or R4 tool cases except where they share a type already established in R2.0. - -### R2.6 — Generation recovery and retry parity - -Read: - -- `harness.md`: generation/deferred portions of §§3.2, 3.7, 4.4–4.6, and R3 rows in Parts 8–9; -- `assistant-durability.md` recovery, cancellation, deferred, snapshot, and race sections; -- assistant recovery, retry-wait, missing-identity, and deferred-suspension paths in `runtime/assistant-procedure.ts` and `runtime/run-driver.ts`; -- retry/deferred classifiers in `runtime/transitions.ts`; -- `agent-harness-r3.test.ts`; -- R3-relevant cases in `restore.test.ts`. - -Do not read tool-batch implementation or R4 tests. - -### R2.7 — Tool parity - -Read: - -- `harness.md`: §§3.8, 4.5–4.6 tool portions, 5.6–5.8 tool portions, and the R4 rows in Parts 8–9; -- `tool-durability.md` completely; -- `execution/tools.ts`; -- `runtime/tool-batch-procedure.ts`; -- the tool transition helpers used by that procedure; -- `execution-tools.test.ts`; -- `agent-harness-r4.test.ts`; -- tool-batch cases in `restore.test.ts`. - -Read assistant code only in the slice where assistant settlement creates a tool plan. - -### Expanding a slice's reads - -If implementation exposes a concrete dependency not listed above: - -1. state the specific unresolved question; -2. locate the smallest defining source or test with `rg`; -3. read that file or relevant section; -4. do not recursively read its entire dependency graph; -5. add it to this plan's slice list if future agents will need it. - -## Value/list and durability prerequisites - -Before further assistant/tool runtime slices, implement `values.md` repo-wide: - -- replace global namespace/value maps and register tokens with `value(namespace, key?)` and `list(namespace, key?)`; -- put direct built-in address constructors in `session/values.ts`, using the universal `value()`/`list()` constructors and the `pi.*` namespaces/key grammars documented by `values.md`; -- expose `getValue`/`setValue`/`deleteValue`/`scanValues` and `readList`/`appendList`/`deleteList` without a second key argument; core lane inventory and cleanup use only the five exported `*Prefix` constructors as `scanValues` inputs; -- add backend-conformant append/read/whole-list-delete storage; -- add `pendingAssistantFrames(operationId, responseEntryId)` as a `ValueList` constructor. - -Namespace `pi` and every `pi.*` namespace are reserved for built-ins by contract. Core and applications use the same `value()`/`list()` constructors; application misuse is a trusted-programming defect, not a runtime rejection. Runtime2 imports built-in addresses directly; do not add a private constructor, privilege token, bundle, catalog, runtime registry, `ValuePrefix` type, or runtime-specific storage abstraction. Every call receives an already-bound address. Stable physical namespace/key pairs preserve existing durable data; the new list table/records are a storage schema addition. - -R2.5 owns fresh assistant frame append and cleanup. R2.6 owns frame-based generation/deferred recovery and snapshots. R2.7 owns `outcome_ready`, invocation memos, and tool checkpoint values exactly as specified by `tool-durability.md`. - -## Non-goals until parity - -Do not implement or mix in: - -- R5 inbox/configuration/write behavior (`pendingNextRun` placement during R2.4 acceptance is the explicit exception; enqueue/cancel/consume behavior remains R5); -- checkpoint hook; -- caller-reserved queue IDs; -- explicit owner-requested failure; -- application-state semantics beyond migration to application-defined bound addresses; -- durable state-shape normalization unrelated to the specified tool lifecycle; -- summary/navigation slices beyond currently implemented R1–R4 behavior; -- structural-summary partial-frame persistence; -- compatibility shims between runtime1 and runtime2 internals. - -## Core principle - -Each lane has one in-memory owner while the harness instance exists. - -```text -create harness -→ restore each lane once -→ lane owns current leaf/config/queues/operation/last-result in memory -→ all lane-affecting commands serialize through that owner -→ compute private next state + exact transaction -→ commit -→ replace in-memory state -→ publish observations -``` - -External effects never hold the lane command line: - -```text -lane command: commit intent and publish pending state -→ provider/tool/hook/timer effect outside lane owner -→ lane command: settle against latest in-memory state -``` - -A crash discards memory. The next `createAgentHarness()` restores the committed state again. - -### Commit ordering - -Never mutate the owned state before storage commit succeeds. - -Correct: - -```text -current S0 -→ private candidate S1 -→ commit durable writes for S1 -→ lane.current = S1 -``` - -Incorrect: - -```text -lane.current = S1 -→ commit -→ attempt rollback on failure -``` - -The lane command remains serialized through commit and the memory replacement. A queued command sees either S0 before the transition or S1 after it, never an uncommitted state. - -## Target directory rule - -Runtime2 source must never import `../runtime/*.ts`. Copy the small primitive it needs into `runtime2/`, or move genuinely shared code to a neutral harness module. Runtime2 owns `Config`; do not reintroduce `RuntimeSettings`, a Runtime controller, or settings-line terminology. - -Create files only as a slice needs them. Do not scaffold the final directory with empty modules. - -Likely eventual shape—not a mandate: - -```text -runtime2/ - index.ts eventual stable runtime2 export - types.ts lane/config types - state.ts exhaustive refinements/invariants - lane.ts owner + commit-then-publish kernel - restore.ts one-time durable restore into lane memory - harness.ts facade, registries, config, close - operation.ts drive ownership/fencing - run.ts direct phase dispatch - assistant.ts intent/effect/settlement/recovery - tools.ts batch intent/effect/outcome/recovery - checkpoint.ts checkpoint/terminal procedure -``` - -Merge modules when that is easier to read. Do not create helper files solely to reduce line counts. - -## Historical switch outcome - -WP00 added `runtime2/index.ts`, switched the one public factory import, proved constructor selection directly, and deleted runtime1. No mutable runtime selector or dual-runtime parity harness was retained. - -## Size and readability gates - -Measure after every slice: - -```bash -find packages/agent/src/harness/runtime2 -name '*.ts' -print0 | xargs -0 wc -l -``` - -Targets are diagnostic, not an excuse for compressed code: - -- lane transition kernel: target ≤ 250 lines; -- restore projection: target ≤ 250 lines; -- facade/settings/close at R1 parity: target ≤ 450 lines; -- no procedure file above 700 lines without explicit review; -- R2 minimal no-tool parity: target total ≤ 2,000 lines; -- R3 recovery/retry parity: target total ≤ 2,800 lines; -- R4 tool parity: target total ≤ 3,400 lines. - -Stop and review if: - -- runtime2 exceeds a gate by more than 20%; -- `restoreLane()` appears in an ordinary live transition; -- the same ownership/phase check appears in three procedures; -- a generic context object accumulates unrelated runtime methods; -- a module cannot explain its protocol in one sentence; -- code size is moving toward runtime1 without additional behavior. - -Do not reduce lines by hiding transitions behind a generic reducer or callback framework. - -# Historical runtime2 checkpoints - -These sections explain how the current runtime2 shell was produced and preserve later design discoveries. They are not the active sequence. Execute only a concrete work-package handoff linked from `harness.md` Part 8. - -## R2.0 — Restore one lane - -### Goal - -Load the current committed control values for one configured lane into a small process-local `Lane` without starting work or interpreting the state. - -### Files - -Create only: - -```text -runtime2/types.ts -runtime2/lane.ts -runtime2/restore.ts -packages/agent/test/harness/runtime2/restore.test.ts -``` - -### Contract - -`LaneState` is the direct in-memory projection of: - -```ts -laneLeaf(lane) -laneConfig(lane) -laneState(lane) -laneLastResult(lane) // when present -operationMeta(operationId) // when currentOperationId is non-null -operationState(operationId) // when currentOperationId is non-null -``` - -`Lane` owns its name and one `LaneState`. It does not implement `AgentLane` yet. - -Restore trusts committed internal values. It checks only that values required to construct the projection exist. It does not: - -- validate referenced entries or semantic phase relationships; -- hydrate entries, pending payloads, preparations, or tool arguments; -- infer state from the transcript; -- enumerate lanes or seed main configuration; -- create hooks, events, effects, timers, drives, or public facade stubs. - -Exact durable payload reads remain available to the later procedure that needs them. Live operation/phase/effect checks remain required when asynchronous work settles; they are concurrency fences, not restore validation. - -### Tests - -- idle configured lane; -- optional latest result; -- open operation metadata and state; -- no interpretation of referenced payload ids; -- missing required lane value; -- missing current operation metadata/state. - -### Stop condition - -Report source and test line counts, focused test result, TypeScript result, and whether any additional runtime2 abstraction was required. Do not add a lane mutation kernel or session-wide loader yet. - -## R2.1 — Restore a configured session - -### Goal - -Load every configured lane in one existing session into memory without starting work. - -### Contract - -Add one small session loader: - -```ts -restoreSession(session: Session): Promise> -``` - -It calls `scanValues(laneLeafInventoryPrefix())` for the trusted lane inventory, requires `main`, and calls runtime2's one-lane restore exactly once for each listed lane under that lane's session mutation line. It does not seed configuration, scan other namespaces for orphaned values/lists, validate semantic state, or construct a public harness. - -### Required tests - -- configured main-only session; -- multiple configured lanes; -- open operations remain loaded and inert; -- missing `main` rejects; -- no hooks, providers, tools, timers, or writes; -- each lane is restored exactly once. - -### Stop condition - -Report code size and focused tests. Do not add main seeding, a public factory, or lane mutation behavior. - -## R2.1a — Lane owner and transition kernel - -### Goal - -Prove commit-then-publish on the real session mutation line without AgentHarness, providers, tools, hooks, events, or lifecycle machinery. - -### Kernel contract - -`Lane.transition()` accepts a synchronous planner. Inside `Session.mutate()` it: - -1. plans a transaction, candidate state, and result from current memory; -2. commits the transaction; -3. replaces lane memory only after commit succeeds; -4. returns the planned result before releasing the lane line. - -External async effects cannot be passed through the planner. Storage-assigned entry metadata and observation publication are added only when a concrete transition requires them. - -### Required tests - -1. Memory remains unchanged while commit is pending. -2. Commit failure leaves memory and storage unchanged. -3. A queued transition is not planned until the prior commit publishes memory, then sees that memory. - -### Stop condition - -Report line count and focused tests. Do not add lifecycle faulting, close, observations, or execution behavior. - -## R2.2 — Main seeding and runtime shell - -### Goal - -Seed an unconfigured `main`, call `restoreSession()`, and place the resulting lane map under one runtime2 harness shell without starting work. - -### Behavior - -- validate only constructor options needed to build the immutable lane seed; -- commit the seed when `main` has no configuration; -- call runtime2's configured-session loader; -- create no drive task/provider/tool/hook/timer; -- expose the exact `createAgentHarness()` signature only when the shell can satisfy it without casts or a speculative abstraction framework. - -### Required tests - -- fresh session/main seeding; -- configured lanes are never overwritten by the seed; -- idle and open multi-lane restore; -- last result loaded once; -- no effects during create; -- no full restore after construction. - -### Stop condition - -The runtime2 shell owns the restored lane map. Do not implement operation acceptance or execution. - -## R2.3 — In-memory reads, config, lanes, and close - -### Goal - -Make non-execution surfaces use owned memory. - -### Completed - -- `Harness` and `Lane` are the concrete `AgentHarness` and `AgentLane` implementations; there is no separate runtime/controller layer. -- `Lane.command()` is the central effect-free serialized primitive with explicit `commit`, `return`, and `reject` outcomes. -- Commit commands support bounded reads, one durable commit, memory publication, and synchronous `CommitResult` materialization. -- Lane configuration getters/setters derive queued updates from current owned memory. -- Lane lookup/listing and leaf/last-result reads use owned memory. -- The `sessionTree` facade routes leaf/branch reads and idle/run appends through `Lane`; application values and lists remain Session-owned. -- Close and fault synchronously seal every lane; close drains admitted session work without durable cancellation. -- `Harness.createLane()` calls `Session.createLane()` directly with the captured seed, publishes a known idle candidate after commit, maps expected Session errors, and emits `lane_created` after map publication. -- Harness-global config is one replace-only process-local `Config` snapshot. It captures mutable registries/policies plus immutable execution inputs (`systemPrompt`, `toolContext`, provider conversion, projectors, telemetry, drive mode, and tool-execution mode). Setters validate, publish synchronously, then emit `config_update`; no second mutation line exists. -- Shared config defaults, validators, and missing-identity classification live in `packages/agent/src/harness/config.ts` and are used by both implementations. -- Restored suspension descriptors hydrate run prompts and deferred handles once, classify captured missing identities, and stay owned by their lanes. -- `Lane.inspectExecution()` and `Harness.lanes()` read the same owned state without storage access. -- Application-defined values and lists pass directly through the Session facade while lanes are idle or active. -- Focused tests cover ordering, commit failure, fault publication, close, no-commit promise boxing, expected rejection, appends, restore, lane creation, config, inspection, suspension classification, and application values and lists. - -### Remaining R2.3 work - -None. Watch surfaces have no runtime1 implementation and remain a later separate concern. - -### Session boundary decision - -Harness-owned lane/tree/operation writes route through `Lane.command()`. Application value/list reads and writes remain direct Session operations because they do not mutate lane control state. - -## Accepted hook and execution design - -The rejected acceptance/drive attempt put hooks inside acceptance and recreated runtime1-style orchestration through provisional ownership and wrapper abstractions. It was reverted completely. The accepted replacement is now normative in `harness.md` §§3.1–3.6, 4.1–4.5, 5.5–5.6, and Parts 8–9: hook-free atomic acceptance into durable `starting`, real drive ownership, `before_drive`, driver-owned `before_run`, phase-based orphan recovery, and request-local context/system-prompt transformation. This archived note retains only that implementation lesson; it does not restate the contract. - -## R2.4 — Contract reconciliation and atomic run acceptance - -### Required preparation - -The normative contract has been reconciled in `harness.md`. WP00 removes the obsolete shared members and runtime1 before this historical implementation concern becomes actionable. Do not preserve compatibility shims. - -### Goal - -Implement the smallest complete concern: the revised public/durable contract plus one-command prompt/skill/template acceptance into `RunPhase.starting`. Do not install drive ownership unless this same slice also implements real `before_drive`/`before_run` work and the `starting → checkpoint` transition. - -### Required behavior - -- prompt, skill, and prompt-template normalization; -- write-nothing `Closed`, `LaneBusy`, missing-identity, pending-assistant, unknown-resource, and empty-input results; -- captured `pendingNextRun` placement/deletion before caller entries; -- exact acceptance transaction above; -- owned memory publication after commit; -- `run_start`, message lifecycle, `entry_added`, and trailing `queue_update` after publication; -- accepted-undriven and accept→reopen `starting` inspection/restore; -- no hook, breakpoint, process reservation, process owner, provider, tool, timer, or runtime1 import. - -### Required tests - -- exact transaction and durable-owned equality; -- concurrent accepts serialize and only one commits; -- append-vs-accept has only the natural two orders: append first becomes an earlier tree entry; accept first routes the append into the run inbox; -- config-vs-accept snapshots one complete order; -- all expected rejections write nothing; -- commit failure leaves memory idle and faults; -- admitted commit wins close and publishes; -- close-first returns `Closed`; -- pendingNextRun ordering and events; -- reopen restores `starting` with caller prompt and starts no work. - -Stop after this concern unless the user explicitly approves combining it with the real starting-phase drive slice. - -## R2.5 — Minimal no-tool run parity - -### Goal - -Implement the first real drive owner together with all work it owns; never land an owner-only shell. - -Implement direct flow: - -```text -drive arbitration installs one Lane-owned promise under the lane line -→ same-id drives join; stale ids fence -→ cancellation/deadline precheck -→ before_drive prerequisite outside the line -→ starting: before_run outside the line -→ commit injected messages + starting → checkpoint -→ generation.ready commit -→ request-local context/system-prompt transform -→ before_request/input preparation -→ effect_pending intent commit -→ provider effect; synchronously enqueue each converted assistant frame without awaiting storage -→ await latest frame-write promise before after_response -→ assistant settlement commit atomically deleting the frame list -→ finish terminal commit -→ remove owner only after final publication -``` - -The owner is one ordinary Lane field, not a runtime/controller/context abstraction. Close/fault rejects it; effects and hooks remain outside `Lane.command()`. Reuse execution-only assistant mechanics where useful, never runtime1 orchestration. Before provider work, settle the request-reconstruction decision described above. - -### Required tests - -Run the complete R2 suite against runtime2, including exact writes, events, telemetry, breakpoints, close boundaries, convenience/primitive equivalence, one frame append per convertible event, no per-frame storage await, latest-promise settlement fencing, and atomic frame-list deletion. - -### Size review - -Target total runtime2 implementation ≤ 2,000 lines. If larger, stop before R3 and identify why rather than introducing context/framework abstractions. - -## R2.6 — Generation recovery and retry parity - -### Goal - -Pass committed R3 behavior. - -Implement: - -- orphaned assistant `effect_pending` recovery when no live owner exists, reducing committed frames into the synthetic partial error before ordinary retry/failure classification; -- repeated `before_drive` prerequisites on every newly installed pass; -- synthetic cap settlement; -- retry wait/wake/deadline policy; -- deferred suspension/poll frame persistence, replacement cleanup, and snapshot foundation; -- missing-identity suspension; -- activation-only unknown-outcome policy. - -Recovery reads only the already-restored in-memory state. A same-process safe yield/hosted wake must not pretend a crash occurred. - -Run the complete R3 suite and scratch example against runtime2. - -### Size review - -Target total ≤ 2,200 lines. - -## R2.7 — Tool parity - -### Goal - -Pass the complete target R4 contract, including `tool-durability.md`. - -Implement: - -- complete durable tool plans and stable invocation ID = result entry ID; -- bound typed tool-argument, invocation-memo, progress-checkpoint, and staged-result addresses; -- planned → effect_pending → outcome_ready → completed; -- invocation-scoped `getMemo`/`setMemo` and Flue-style memoization support; -- sequential and parallel execution with completion-order outcome staging; -- source-ordered result materialization; -- immediate/synthetic outcomes; -- safe replay and unsafe interruption using the latest checkpoint value; -- every selected checkpoint synchronously enqueued, latest promise retained, and latest awaited before `after_tool`; -- hooks/events/usage/telemetry/breakpoints; -- termination and cancellation handling at every tool state. - -Parallel effects remain ordinary local promises. Intent, checkpoint, outcome-staging, and materialization commands queue through the lane owner. Current call state is read from memory, never via full restore. Do not reintroduce one-active/one-waiting checkpoint coalescing. - -Run the complete R4 suite and scratch example against runtime2. - -### Size review - -Target total ≤ 3,000 lines; stretch ≤ 2,500. If runtime2 approaches runtime1 size, do not continue to parity cleanup without architectural review. - -## Superseded parity/switch roadmap - -The former R2.8 parity-first switch plan was superseded by [WP00 — Runtime1 removal](work-packages/00-runtime1-removal.md). Active work proceeds only from `harness.md` Part 8 and its linked handoffs; do not execute work from this archive or recreate a dual-runtime parity gate. - -# Comparison discipline - -The target contract and transaction tables in `harness.md` are the authority. The historical comparison dimensions were: - -- transaction write arrays and durable value/entry state; -- events and ordering; -- telemetry spans/attributes; -- breakpoints/details; -- public results/errors and suspended descriptors; -- storage read counts and source line counts. - -Do not require parity with deleted runtime1 on redesigned acceptance, hooks, FSM, restore validation, bound value/list storage, assistant partial durability, or tool durability. Use the harvested scenario inventory and ordinary git history for archaeology; do not recreate a dual-runtime harness. - -# Coding rules - -- No `any`. -- No dynamic/inline imports. -- Erasable TypeScript syntax only. -- Keep phase/ID checks beside the transition they protect. -- No generic reducer/action framework. -- No external effect inside the lane command line. -- No mutation of published memory before commit. -- No full restore in live procedures. -- Inline one-use helpers. -- Check external API types from source/node_modules. -- Do not recreate runtime1 or change the target contract to preserve it. -- Do not touch unrelated coding-agent worktree files. -- Do not commit unless the user asks. diff --git a/packages/agent/docs/work-packages/00-runtime1-removal.md b/packages/agent/docs/work-packages/00-runtime1-removal.md index 3b707410f..a1f3819b7 100644 --- a/packages/agent/docs/work-packages/00-runtime1-removal.md +++ b/packages/agent/docs/work-packages/00-runtime1-removal.md @@ -10,12 +10,12 @@ Make runtime2 the sole public harness implementation, delete runtime1 and its ob ## Prerequisites -- The approved acceptance/hook redesign and durability handoffs are present in `runtime2.md`, `values.md`, `assistant-durability.md`, and `tool-durability.md`. +- The approved acceptance/hook redesign and durability handoffs are present in `harness.md`, `values.md`, `assistant-durability.md`, and `tool-durability.md`. - Existing tests are evidence, not authority. ## Work, in order -1. **Reconcile the contract.** Fold the approved acceptance/hook redesign into `harness.md`, including durable `starting`, atomic hook-free acceptance, driver-owned `before_run`, `before_drive`, request-local system-prompt transformation, trusted restore, and removal of process-origin activation semantics. Audit §§0.4, 1.2, 3.1–3.6, 4.1–4.2, 4.5, 5.1–5.2, 5.5–5.6, and Parts 8–9; remove every stale `BeforeResumePrepared`, `before_resume`, `resumeData`, `systemPromptOverride`, stable-ID routing, reservation, and `fresh | continue | resume` activation reference. Then remove the temporary supersession notice and duplicated redesign from `runtime2.md`. +1. **Reconcile the contract.** Fold the approved acceptance/hook redesign into `harness.md`, including durable `starting`, atomic hook-free acceptance, driver-owned `before_run`, `before_drive`, request-local system-prompt transformation, trusted restore, and removal of process-origin activation semantics. Audit §§0.4, 1.2, 3.1–3.6, 4.1–4.2, 4.5, 5.1–5.2, 5.5–5.6, and Parts 8–9; remove every stale `BeforeResumePrepared`, `before_resume`, `resumeData`, `systemPromptOverride`, stable-ID routing, reservation, and `fresh | continue | resume` activation reference. Delete obsolete runtime planning documents once `harness.md` and linked handoffs own the active contract. 2. **Harvest before deletion.** Inspect `agent-harness-runtime.test.ts`, `agent-harness-r2/r3/r4.test.ts`, and old `restore.test.ts`. Preserve unique scenarios in the detailed future rows or a temporary categorized inventory; explicitly discard old reservation, `before_resume`, `resumeData`, persisted hook prompt override, semantic restore audit, and pre-`outcome_ready` tool-crash behavior. 3. **Remove runtime1-only public members.** Delete `before_resume`, `BeforeResumePrepared`, `resumeData`, `systemPromptOverride`, and stable hook-ID routing. Add the approved `before_drive` and `transform_context` shapes. Update telemetry schema source and regenerate its document. Do not implement `starting` or acceptance behavior here. 4. **Switch the factory.** Add `packages/agent/src/harness/runtime2/index.ts`, point `agent-harness.ts` at it, and add a constructor-selection regression. Verify the experimental coding-agent worker still creates the harness, subscribes to events, and closes it. diff --git a/packages/agent/docs/work-packages/02-atomic-run-acceptance.md b/packages/agent/docs/work-packages/02-atomic-run-acceptance.md index 3bd798b9d..028a15972 100644 --- a/packages/agent/docs/work-packages/02-atomic-run-acceptance.md +++ b/packages/agent/docs/work-packages/02-atomic-run-acceptance.md @@ -2,9 +2,9 @@ ## Status -Phase A redesign in progress. Implementation planning showed that eager attachment hydration and predictive missing-identity status duplicated future drive logic and created unnecessary shadow state. `harness.md` is normative. Runtime implementation has not started. +Complete. Phase A established minimal attachment, open-operation inventory, lane-line inspection/watch capture, enqueue-time recipient binding, and in-band identity-failure vocabulary. Phase B landed in `beac75ecc` with focused, monorepo-check, full-suite, and final Fable review passing. -The implementation baseline is the current `origin/dev` runtime2. Preserve unrelated work, especially concurrent `packages/agent/docs/plugins.md` changes and experimental directories. +Implementation also updated downstream protocol/coding-agent wire projections required by the changed public contract. No execution owner, provider/tool effect, timer, retry, polling, cancellation procedure, manual action, or terminal settlement was added. ## Goal diff --git a/packages/agent/docs/work-packages/03-remove-drive-deadlines.md b/packages/agent/docs/work-packages/03-remove-drive-deadlines.md new file mode 100644 index 000000000..76787b2ef --- /dev/null +++ b/packages/agent/docs/work-packages/03-remove-drive-deadlines.md @@ -0,0 +1,183 @@ +# WP03 — Remove drive deadlines + +## Status + +Complete. `DriveOptions.deadline` and the `DriveOutcome` `yielded` branch are removed from public types, active documentation, future package requirements, invariants, races, and tests. Obsolete `runtime2.md` is deleted. Focused tests, `npm run check`, and `./test.sh` pass. + +WP02 is complete at `beac75ecc`. Preserve unrelated concurrent source work, especially current `packages/agent/src/harness/runtime2/lane.ts` changes, JSONL/fork work, plugins, RPC, and experimental directories. + +## Problem + +`DriveOptions.deadline` and the corresponding `DriveOutcome { kind: "yielded" }` do not provide a correctness boundary. + +A deadline is checked only before starting another transition or effect. An admitted provider/tool/hook may run beyond it and the host may terminate the process anyway: + +```text +check deadline +→ admit provider or tool +→ host limit expires while the effect is running +→ process dies with durable effect_pending +``` + +Unknown-outcome recovery remains mandatory. The deadline therefore does not prevent process loss, bound admitted work, make effects exactly once, or simplify recovery. Flue-style tool memoization depends on stable invocation ids and durable memos, not drive deadlines. + +Deadline handling instead adds wall-clock policy to the durable core: + +- a `yielded` public outcome unrelated to durable state; +- safe-boundary checks before hooks/effects/transitions; +- deadline-versus-retry-timer arbitration; +- deadline-versus-effect-admission races; +- convenience loops and event-bracket behavior for yields. + +The host already owns scheduling and termination. Process loss is a controlled crash boundary recovered from durable operation state. + +## Decision + +Remove drive deadlines completely: + +```ts +interface DriveOptions { + operationId: string; + waitForRetry?: boolean; + pollDeferred?: boolean; +} + +type DriveOutcome = + | { kind: "settled"; operationId: string; outcome: TerminalOperationOutcome } + | { kind: "waiting"; operationId: string; reason: "retry"; notBefore: number } + | { kind: "waiting"; operationId: string; reason: "deferred"; deferred: DeferredHandle } + | { kind: "action_required"; operationId: string; action: ActionInfo }; +``` + +There is no deprecated alias, ignored `deadline` field, compatibility overload, alternate timestamp option, or replacement pause flag in WP03. + +The next drive package will be redesigned manual-first. Procedure code reaches one shared `BreakpointBarrier`: manual mode parks and reports `action_required`; automatic mode crosses the same boundary immediately. Manual mode exists to inspect and test transitions, and automatic mode is the same transition path with automatic barrier release. That design is evaluated only after this cleanup lands. + +## Host behavior + +Hosts own execution budgets: + +```text +invoke drive +→ terminal or durable waiting result: schedule normally +→ planned shutdown: stop routing/releasing work and close session processes +→ forced termination: replacement attaches and recovers durable open operations +``` + +A process-per-session host may attempt graceful manual checkpoint parking before exit and use process termination as the hard fence for non-cooperative providers, tools, hooks, storage, or event listeners. This operational policy does not require a wall-clock field in `DriveOptions`. + +WP03 does not add `stopAfterCheckpoint`, `pause`, `quiesce`, or in-process crash simulation. Those ideas belong to the manual-first drive design review. In-process process-loss simulation is not a public core primitive: an old continuation requires fencing, for which process isolation is the reliable mechanism. + +## Work + +### Public types + +In `packages/agent/src/harness/agent-harness.ts`: + +- delete `DriveOptions.deadline`; +- delete the `DriveOutcome` `yielded` branch; +- preserve expected-id fencing, retry waiting, deferred waiting, and manual action branches unchanged. + +No current runtime2 drive implementation exists, so this package adds no execution behavior or owner. + +### Normative documentation + +Update `packages/agent/docs/harness.md` completely: + +- remove safe-yield scheduling language from non-goals/orientation; +- remove only the deadline half of cancellation/deadline prerequisite wording in §3.6, §5.6's `before_drive` row, and invariant 22; the cancellation prerequisite remains; +- remove deadline checks and yielded returns from the drive-pass pseudocode; +- remove deadline policy from pass joining, retry waiting, convenience composition, recovery, and public method prose; +- remove `deadline` from `DriveOptions` and `yielded` from `DriveOutcome`; +- remove deadline-specific event/turn requirements; +- remove deadline language from `before_drive` hook timing; +- remove deadline/yield requirements from future work rows; +- replace invariant 25 with the explicit no-wall-clock-policy invariant while retaining the rule that an admitted effect settles normally or is recovered after task loss; +- remove deadline races from the race catalog; +- update the drive-pass glossary. + +Do not alter generic RPC timeout/deadline documentation or unrelated process/model-catalog timeouts. Those are invocation/transport policies, not `AgentLane.drive`. + +Delete obsolete `packages/agent/docs/runtime2.md`; `harness.md` plus linked handoffs are the sole implementation plan and history needed for active work. + +Mark WP02 complete in its handoff and Part 8. Add WP03 as the concrete cleanup package. Leave the former R2/R3 drive rows as future candidates, minus deadline/yield requirements, until the manual-first redesign replaces them with a reviewed handoff. + +### Delete + +- `packages/agent/docs/runtime2.md` + +### Type tests + +Extend `packages/agent/test/harness/types.test.ts`: + +- assert `keyof DriveOptions` is exactly `"operationId" | "waitForRetry" | "pollDeferred"`; +- assert `DriveOutcome["kind"]` excludes `"yielded"`; +- add `@ts-expect-error` coverage proving callers cannot supply `deadline`; +- retain existing drive/result signatures. + +Run the focused type test after editing it. + +### Downstream compatibility + +Search protocol, server, coding-agent, examples, and tests for structural mirrors or exhaustive `DriveOutcome` switches. Update only compile-time/type compatibility forced by the public removal. Coding-agent experimental worker/remote-runtime behavior and tests are out of scope. + +The current protocol harness schemas expose prompt/run/watch DTOs, not `DriveOptions` or `DriveOutcome`; no protocol edit is expected unless the final search proves otherwise. + +## Non-goals + +WP03 does not implement or redesign: + +- `drive`, `resume`, prompt conveniences, operation ownership, or latest-result lookup; +- manual actions or automatic barrier release; +- provider/tool execution, recovery, retry timers, deferred polling, abort, or terminal settlement; +- checkpoint pause/quiesce; +- close admission changes; +- worker/RPC cancellation or experimental remote prompting; +- storage/schema/migration behavior. + +## Required checks + +```bash +# No drive deadline/yield contract remains in active harness docs or source. +rg -n 'deadline|yield' \ + packages/agent/src/harness \ + packages/agent/test/harness \ + packages/agent/docs/harness.md \ + packages/agent/docs/work-packages + +cd packages/agent +node "$(git rev-parse --show-toplevel)/node_modules/vitest/dist/cli.js" --run \ + test/harness/types.test.ts + +cd "$(git rev-parse --show-toplevel)" +git diff --check +npm run check +./test.sh +``` + +The removal grep may still match this handoff's historical problem explanation. Every remaining match must be reviewed; no active API, normative behavior, future acceptance criterion, or test expectation may retain the removed contract. + +## Review + +Before implementation: + +1. Fable reviews this handoff against complete docs/source. +2. As explicitly requested by the user, `openai-codex/gpt-5.6-sol` reviews it with thinking level high. +3. Resolve all findings and repeat until no findings. + +After implementation, repeat both reviews over the final documentation/type diff. + +## Stop condition + +Stop when: + +- `DriveOptions` has no wall-clock budget; +- `DriveOutcome` has no `yielded` branch; +- active normative docs contain no deadline/yield behavior; +- future drive rows contain no hidden deadline requirements; +- type tests prove removal; +- unrelated timeout/deadline APIs remain untouched; +- focused tests, `npm run check`, and `./test.sh` pass; +- final Fable and Codex reviews have no findings. + +Do not begin the manual-first drive redesign in this package. diff --git a/packages/agent/src/harness/agent-harness.ts b/packages/agent/src/harness/agent-harness.ts index 8a56f6abe..b8b4164f7 100644 --- a/packages/agent/src/harness/agent-harness.ts +++ b/packages/agent/src/harness/agent-harness.ts @@ -193,7 +193,6 @@ export type OperationAdmissionResult = Result; diff --git a/packages/agent/test/harness/types.test.ts b/packages/agent/test/harness/types.test.ts index ea832a410..f4c06938a 100644 --- a/packages/agent/test/harness/types.test.ts +++ b/packages/agent/test/harness/types.test.ts @@ -17,6 +17,8 @@ import type { Control, CustomEntry, Deferred, + DriveOptions, + DriveOutcome, DriveResult, Entry, EntryProjector, @@ -461,6 +463,8 @@ it("covers Part 5 results, events, hooks, snapshots, tools, and stream options", expectTypeOf().returns.toEqualTypeOf>(); expectTypeOf().returns.toEqualTypeOf>(); expectTypeOf().returns.toEqualTypeOf>(); + expectTypeOf().toEqualTypeOf<"operationId" | "waitForRetry" | "pollDeferred">(); + expectTypeOf().toEqualTypeOf<"settled" | "waiting" | "action_required">(); expectTypeOf().returns.toEqualTypeOf>(); expectTypeOf().toEqualTypeOf< "prompt" | "skill" | "prompt_template" | "compaction" | "navigation" @@ -479,6 +483,11 @@ it("covers Part 5 results, events, hooks, snapshots, tools, and stream options", const compileTimeFailures = () => { // @ts-expect-error callers cannot supply the harness-owned abort signal const invalidOptions: AgentHarnessStreamOptions = { signal: new AbortController().signal }; + const invalidDriveOptions: DriveOptions = { + operationId: "run", + // @ts-expect-error drive has no wall-clock deadline + deadline: Date.now(), + }; const invalidValueEvent: Extract = { type: "value_update", value: "session_name", @@ -487,6 +496,7 @@ it("covers Part 5 results, events, hooks, snapshots, tools, and stream options", lane: "main", }; void invalidOptions; + void invalidDriveOptions; void invalidValueEvent; }; expectTypeOf(compileTimeFailures).toBeFunction();