diff --git a/packages/agent/docs/harness.md b/packages/agent/docs/harness.md index bffd25f7d..554f9ad6a 100644 --- a/packages/agent/docs/harness.md +++ b/packages/agent/docs/harness.md @@ -3465,8 +3465,11 @@ 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 | Implement atomic prompt/skill/template acceptance plus coherent attachment, phase-aware open-operation status, fully hydrated owned inspection state, and gap-free lane watch snapshots without starting execution. | WP01 | [Atomic acceptance and coherent attachment](work-packages/02-atomic-run-acceptance.md) | -The old Types, Session/Memory, JSONL, and SQLite rows described work already present in the repository. Their remaining cross-backend delta is WP01; they are not separate future packages. +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. ## Future package candidates @@ -3476,10 +3479,9 @@ 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. | -| R1 | **Runtime shell and execution primitives** | Extend the public types with `OperationRequest`, admission/drive/cancellation/inspection results, `OperationMismatch`, `AgentHarnessToolInvocation`, and harness-specific tool-update checkpoint options. Implement lane/settings mutation lines; one drive-pass `ActiveOperation` slot per lane; expected-id fencing, same-operation drive joining, coherent `inspectExecution`, safe deadline-yield plumbing, `ActionInfo` breakpoint barrier, `EffectGate` and its internal abort control flow, projection-existence restore checks, hook/event primitives, restore inventory, registry lookup plumbing, and fault/close plumbing. Also implement the independently testable behavior-only blocks under `packages/agent/src/harness/execution/` from §5.7; `agent-loop.ts` remains unchanged. | Type surface clean; coherent inspection; expected-id mismatch/latest-result lookup; one same-id pass installed/joined; stale-id drive isolation; safe yield; breakpoint and effect-gate coverage; task lifecycle; hook aggregation; ordered passive event buffering; restore without history reads, list reads, or reference audits; direct assistant/tool block tests; old `AgentToolUpdateCallback` remains unchanged while the harness callback carries `checkpoint:true`. | -| R2 | **Minimal no-tool run** | Compose R1's assistant block with generic hook-free prompt `accept`, optional caller-supplied operation id, prompt expansion, atomic `starting` acceptance without task installation, pending-capture placement, driver-owned `before_drive`/`before_run` settlement, and `prompt` as `accept`+`drive`; add projection-only restore with consumption-time dereference, captured request options/thinking and model preflight, payload/response hooks, one generation intent/effect/settlement with per-event frame appends (synchronous enqueue, latest-promise settlement fencing) and atomic frame-list deletion, usage, terminal cleanup plus `pi.lane.lastResult`, results, basic events/telemetry, and stable breakpoint descriptors. | Acceptance returns before any drive work; crash between accept and drive resumes the initial state; prompt convenience and explicit accept+drive produce byte-identical writes/events; supplied operation id reaches `pi.op.meta`/events/result; 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 storage failure faults before `after_response`; caller rejection and adapter-reported failures; exact ordering; terminal cleanup; same-id drive join during the accept/drive gap; close at every boundary; invalid references rejected at consumption; re-enable coding-agent's one-shot and worker-owned Harness prompt tests. | +| 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, 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. | 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), 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; missing identities. | -| R4 | **Tools** | Compose R1's 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, blocked/genuine-length outcomes, hooks/events/usage, and stable descriptors. 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; batch-wide identity preflight starts no effect when any captured tool is missing; identity-free unsafe orphan settlement precedes later context resolution; a missing first safe orphan starts/resolves nothing later; current sequential override serializes recovered calls before later identity suspension; source-ordered materialization; termination and abort-ready states. | +| 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, blocked/genuine-length outcomes, hooks/events/usage, and stable descriptors. 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; batch-wide identity preflight starts no effect when any captured tool is missing; identity-free unsafe orphan settlement precedes later context resolution; a missing first safe orphan starts/resolves nothing later; current sequential override serializes recovered calls before later identity suspension; 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. | | R7 | **Deferred provider redemption** | One poll per drive pass carrying `pollDeferred:true`; `resume()` composes inspection and that drive option. Add deferred consumption-time dereference and provider identity preflight. Preserve copied configuration/options, per-poll hooks, exact source lineage/equality, poll frame persistence with fresh intent after unknown poll deleting the abandoned frame list, mismatch-to-error, ready tools, advancement of R6 cancellation, and stable poll descriptors. | `pollDeferred:false` returns waiting without work; one permit performs at most one poll; invalid/missing/mismatched durable handles; repeated pending, ready/error/aborted/mismatch, crash positions, poll frame cleanup on every settlement and replacement, unknown-poll snapshot without permit, convenience/primitive equivalence, no cap/backoff/loop, newest-handle cancellation. | diff --git a/packages/agent/docs/work-packages/02-atomic-run-acceptance.md b/packages/agent/docs/work-packages/02-atomic-run-acceptance.md new file mode 100644 index 000000000..0a88438b6 --- /dev/null +++ b/packages/agent/docs/work-packages/02-atomic-run-acceptance.md @@ -0,0 +1,813 @@ +# WP02 — Atomic acceptance and coherent attachment + +## Status + +Implementation-ready; Phase A not started. `harness.md` remains normative, but its current attachment, suspension, snapshot, and manual-progress sections must be revised by Phase A of this package before implementation begins. + +This handoff expects a clean implementation baseline: all discarded pre-handoff WP02 source and test changes have been explicitly removed. If a future session finds that implementation still present, stop before Phase A and restore these exact tracked paths to `HEAD`: + +- `packages/agent/src/harness/runtime2/harness.ts` +- `packages/agent/src/harness/runtime2/lane.ts` +- `packages/agent/src/harness/runtime2/types.ts` +- `packages/agent/test/harness/runtime2/harness.test.ts` +- `packages/agent/test/harness/runtime2/lane.test.ts` + +Delete the untracked `packages/agent/test/harness/runtime2/accept.test.ts`. Leave `packages/agent/docs/harness.md` and this handoff in place. Use only explicit per-path operations permitted by repository git-safety rules. Do not preserve, patch forward, or mine the discarded implementation. + +## Goal + +Deliver one effect-free lifecycle boundary: + +```text +idle lane +→ atomic prompt/skill/template acceptance +→ durable open operation +→ close or process loss +→ coherent attachment +→ immediately complete, gap-free watch snapshot +``` + +After `AgentHarness.create(options, context)` succeeds: + +- every configured lane has one coherent process-local projection; +- every open operation is reported as `resumable`, `blocked`, or `aborting`; +- `inspectExecution(context)` and the first `watch(context)` read owned memory only; +- the first watch snapshot can render committed transcript, queues, pending writes, unfinished assistant output, running-tool checkpoints, retry/deferred state, manual action, control, leaf, and last result without replaying old lifecycle events; +- attachment starts no hook, provider, tool, timer, breakpoint, drive owner, or application callback; +- future drive passes still reread and fence durable state and never use attachment display hydration as transition authority. + +WP02 also implements atomic prompt/skill/template acceptance into payload-free `starting`. It does not implement `drive()`, provider generation, hooks, tool effects, retries, deferred polling, cancellation procedures, or manual action execution. + +## Decision trace + +### 1. The old suspension type mixed unrelated concepts + +Problem: + +```ts +reason: "crash" | "deferred" | "missing_identities" +``` + +`crash` described how process ownership disappeared. `deferred` described durable provider state. `missing_identities` described a current process registry blocker. The `crash` branch could also contain `deferred` and `missing`, so application code could not make an exhaustive decision from `reason`. + +Concrete trace: + +```text +run is deferred +→ process closes +→ reopen reports reason:"crash" with optional deferred +→ application must inspect both reason and optional fields +``` + +Decision: delete `SuspendedOperation`. Report open operations with one process-relative status that directly controls application behavior: + +- `resumable`: no process owner, and the next real drive can make durable progress; +- `blocked`: no process owner, and the immediate next durable transition requires a currently missing model/tool identity; +- `aborting`: durable `cancel_requested`; cancellation reconciliation is next; +- `running`: a process-local drive owner exists; used by live inspection, never returned by attachment. + +Idle is `current: null` and is not an open-operation status. + +### 2. Attachment and drive have different authority + +Problem: treating attachment hydration as transition authority creates stale parallel state. Treating attachment as projection-only produces incomplete public inspection and forces an artificial classification drive. + +Decision: + +- attachment fully reconstructs and validates the exact state-directed data required by `open`, `inspectExecution(context)`, and an immediate `watch(context)` snapshot; +- owned memory is authoritative for process-local inspection and event publication; +- every future drive pass independently enters the lane mutation line, rereads current durable ownership/control/phase, fences the expected operation id, dereferences its own transition inputs, checks current identities, commits intent before effects, and rereads before settlement; +- attachment data is never passed into a durable transition. + +### 3. The first watch must be complete before resume + +Required application order: + +```ts +const { harness, open } = await AgentHarness.create(options, context); +const watch = await harness.watch(context); +render(watch.snapshot); +watch.start((event, eventContext) => renderEvent(event, eventContext)); +// Only now may the application call resume(context) or manual progression methods. +``` + +Problem: if watch lazily reads durable partials after resume starts, snapshot and events can race. If it omits pre-attachment partials, the UI cannot render interrupted provider/tool work. + +Decision: `create(options, context)` performs the bounded hydration. `watch(context)` atomically captures owned memory and begins buffering before it returns. It performs no storage read. The initial snapshot contains durable partial fallback; live process partials replace it after a future drive owns work. Old message/tool lifecycle events are never replayed. + +### 4. Missing identities are phase-aware and advisory + +Problem: comparing every lane-configured identity against registries at attachment can label identity-free orphan recovery or cancellation as blocked. + +Decision: one pure phase classifier derives the identity requirement of the immediate next durable transition. `blocked` means: + +> Calling `resume(context)` now cannot commit any durable progress because that next transition requires a missing current-process identity. + +Inspection computes this advisory status from owned durable phase plus current in-process registries. A future drive recomputes it at the actual transition boundary. Registry replacement can change `blocked` to `resumable` without a durable write or automatic start. + +### 5. Malformed required attachment state faults attachment + +Problem: returning a harness whose first watch later discovers a missing required queue/deferred/tool reference violates the coherent-memory invariant. + +Decision: + +- missing/mismatched required projection or public-snapshot references reject `create()` with one `HarnessFault`; +- no partially attached harness is returned; +- absence of optional assistant frame lists and optional tool checkpoints is valid and merely omits the partial field; +- drive-only references not represented in attachment inspection remain consumption-time validations. + +### 6. Normal users do not manage operation ids + +Low-level `drive({ operationId })` and `requestAbort(operationId)` retain expected-id fencing so a stale hosted wake for operation A cannot act on later operation B. + +Normal applications use `prompt()`, `skill()`, `compact()`, `navigateTree()`, `resume()`, and `abort()` without ids and pass the trailing invocation `Context` required by the current API. After attachment, the application calls `resume(context)` on a lane reported `resumable` or `aborting`; it repairs registries before resuming a `blocked` lane. + +### 7. Manual mode returns progress instead of parking public promises + +Problem: a manual-mode convenience promise that remains unresolved behind a breakpoint requires polling, a waiter API, or a second controller object. + +Decision: add `action_required` to the existing operation outcome families. In manual mode: + +- `resume(context)` and operation convenience calls return at the first parked breakpoint; +- `executeAction(context)` releases exactly one parked action and returns the next `ResumeResult` action or non-action outcome; +- `runToCompletion(context)` repeatedly releases actions and cannot return `action_required`; +- `peekAction(context)` remains immediate nonblocking inspection; +- automatic mode never returns `action_required`. + +WP02 lands these public types and stubs. The first real drive package implements their behavior. + +### 8. Invocation context makes telemetry and RPC boundaries explicit + +Problem: one harness or lane can serve concurrent local callers and RPC requests. A receiver-level or ambient context would let one invocation overwrite another's telemetry parent or cancellation authority. RPC also needs a place to reconstruct request-local authority without adding transport fields to every business method. + +Concrete trace: + +```text +client invocation context +→ rpc.client injects a trace carrier and maps abort to request cancellation +→ rpc.server creates a request AbortController and extracts a local telemetry parent +→ adapter constructs a fresh server Context +→ harness call, Session commit, and emitted events receive that Context +``` + +Decision: + +- every current public harness/lane operation receives one explicit trailing `context: Context`; +- `context.telemetryContext` supplies the invocation parent for harness/session spans, and derived child contexts preserve parentage across concurrent asynchronous branches without ambient state; +- `context.abortSignal` is process-local invocation cancellation; it never implies durable `cancel_requested`; +- shared harness, lane, Session, and SessionTree receivers never retain a caller context; +- acceptance passes its invocation context to Session reads/mutation/commit, faults, and emitted events; buffered events retain `{ event, context }` so delayed local handlers and future RPC event frames preserve the source telemetry lineage; +- RPC adapters remove Context from serialized business arguments, carry required trace/cancellation data as control-plane metadata, and reconstruct a fresh local Context at the receiving boundary; the Context object, `AbortSignal`, and `TelemetryContext` are never serialized as business arguments or stored durably; +- whether specific adapter-managed typed values may cross as control-plane metadata remains an open RPC design decision; +- WP02 documents and preserves that boundary but does not implement RPC transport, carrier encoding, distributed cancellation, new telemetry spans, or final RPC parameter-position policy. + +### 9. Rejected alternatives + +Do not implement: + +- `resumeManual()` or wrong-mode throws; +- a separate execution/controller object; +- polling `peekAction()`; +- `waitForAction()` ceremony; +- `reason: "ready"`; +- redundant `status: "suspended"` inside a suspended descriptor; +- `crash` as a user-action classification; +- attach-time broad history/value scans; +- attachment data as drive transition input; +- compatibility aliases for `SuspendedOperation` or the old convenience result shapes. + +## End-state public contract + +Phase A must first place these normative shapes in `harness.md`. Phase B then updates source types. + +### Operation status and attachment result + +```ts +export type OperationStatus = "running" | "resumable" | "blocked" | "aborting"; + +export type OpenOperation = { + lane: string; + operationId: string; + kind: "run" | "compaction" | "navigation"; + startedAt: number; +} & ( + | { status: "resumable" | "aborting"; missing?: never } + | { status: "blocked"; missing: MissingIdentityInfo } +); + +export type CurrentOperationInfo = { + id: string; + kind: "run" | "compaction" | "navigation"; + startedAt: number; +} & ( + | { status: "running" | "resumable" | "aborting"; missing?: never } + | { status: "blocked"; missing: MissingIdentityInfo } +); + +export interface LaneInfo { + name: string; + leafId: string | null; + operation: CurrentOperationInfo | null; +} + +export interface SessionSnapshot { + lanes: LaneInfo[]; + faulted: boolean; +} + +export interface AgentHarnessConstructor { + create( + options: AgentHarnessOptions, + context: Context, + ): Promise<{ harness: AgentHarness; open: OpenOperation[] }>; +} +``` + +Rules: + +- `open` contains exactly one item per lane with a durable current operation; +- idle lanes are omitted; +- attachment never returns `running`; +- `missing` is present exactly when status is `blocked`; +- `open` is an observation and may become stale after return; +- `inspectExecution(context)` and snapshots derive current status again from owned state and current registries. + +### Phase-aware identity requirement + +Add one exhaustive classifier over `Operation` plus the current time, defined for the behavior of normal `resume()` (which always owns one deferred-poll permit): + +```ts +export type IdentityRequirement = + | { kind: "model"; provider: string; modelId: string } + | { kind: "tools"; names: string[] } + | null; + +export function nextResumeIdentityRequirement( + operation: Operation, + now: number, +): IdentityRequirement; +``` + +The classifier is pure for fixed arguments. Low-level drive options do not alter attachment status; hosted `drive()` performs its own options-aware preflight. + +Minimum classification table: + +| Current durable state | Immediate requirement | +|---|---| +| `control.cancel_requested` | none; status is `aborting` | +| run `starting` | none | +| checkpoint with steer/follow-up/writes to apply | none | +| checkpoint `need_assistant` after inbox is empty | current lane model | +| checkpoint `may_finish` after inbox is empty | none | +| assistant/summary generation `ready` | captured model | +| retry wait with `notBefore > now` | none; `resume()` may own the timer and status remains `resumable` | +| retry wait with `notBefore <= now` | captured model | +| unowned assistant/summary `effect_pending` | none; unknown-effect recovery is first | +| deferred suspended or deferred `effect_pending` | captured model; `resume()` owns one poll/replacement-fetch permit | +| planned tool batch before any call starts | captured active tools | +| safe tool replay immediately requiring one tool | that captured tool | +| unsafe orphan synthesis, outcome materialization, failure drain | none | +| structural deciding and navigation ready-to-commit | none | + +The implementation must exhaust every current `OperationState` variant. Do not infer requirements from the lane's full active-tool list when the next transition uses a captured batch subset or no identity. + +### Manual progress outcomes + +```ts +export interface ActionRequired { + kind: "action_required"; + action: ActionInfo; +} + +export type RunOutcome = ExistingRunOutcome | ActionRequired; +export type CompactionOutcome = ExistingCompactionOutcome | ActionRequired; +export type NavigationOutcome = ExistingNavigationOutcome | ActionRequired; + +export type RunOperationOutcome = { operation: "run"; runId: string } & RunOutcome; +export type CompactionOperationOutcome = { + operation: "compaction"; + runId: string; +} & CompactionOutcome; +export type NavigationOperationOutcome = { + operation: "navigation"; + runId: string; +} & NavigationOutcome; + +export type ResumeOutcome = + | RunOperationOutcome + | CompactionOperationOutcome + | NavigationOperationOutcome; + +export type SettledResumeOutcome = Exclude; + +export type RunResult = Result; +export type CompactionResult = Result; +export type NavigationResult = Result; +export type ResumeResult = Result; + +export type DriveOutcome = + | ExistingDriveOutcome + | { kind: "action_required"; operationId: string; action: ActionInfo }; +``` + +Post-acceptance missing identities use the existing suspended/waiting outcome branch, not both that branch and `Err(MissingIdentities)`. `MissingIdentities` remains an acceptance error. + +```ts +interface AgentLane { + resume(context: Context): Promise; + executeAction(context: Context): Promise; + runToCompletion( + context: Context, + ): Promise>; + peekAction(context: Context): Promise; +} +``` + +WP02 changes types and keeps these execution methods as `SliceNotImplemented`. The first drive package implements the behavior and updates convenience tests. + +### Complete lane snapshot + +```ts +export interface LaneSnapshot { + lane: string; + transcript: Entry[]; + leafId: string | null; + operation: null | { + id: string; + kind: "run" | "compaction" | "navigation"; + startedAt: number; + status: OperationStatus; + missing?: MissingIdentityInfo; + action?: ActionInfo; + retry?: { attempt: number; maxAttempts: number; nextAttemptAt: number }; + deferred?: { handle: DeferredHandle; poll: number }; + drained?: { steer: QueuedItem[]; followUp: QueuedItem[] }; + streamingMessage?: AssistantMessage; + runningTools: { + toolCallId: string; + toolName: string; + args: unknown; + partialResult?: AgentToolResult; + }[]; + }; + queues: { steer: QueuedItem[]; followUp: QueuedItem[]; nextRun: QueuedItem[] }; + pendingWrites: { + entryId: string; + type: EntryType; + customType?: string; + message?: AgentMessage; + data?: JsonValue; + }[]; + faulted: boolean; +} +``` + +Snapshot rules: + +- `transcript` contains committed entries only; +- `streamingMessage` is a display projection, not a committed transcript entry; +- `runningTools[].partialResult` is a display projection from live state or durable checkpoint; +- live partials take precedence over attachment fallback; +- outcome-ready/materialized tools are not reported as running; +- `action` exists only while a live manual breakpoint is parked; +- `retry`, `deferred`, and `drained` are descriptive details, not scheduling instructions; +- attachment and watch replay no pre-attachment lifecycle events. + +## End-state attachment hydration + +Attachment hydrates each lane into owned immutable process-local memory under the `Context` supplied to `AgentHarness.create(options, context)`. Projection restore, public-state hydration, and any missing-main-config seed commit pass that same context to every Session operation. Reads are exact or bounded by current state. WP02 adds no attachment-specific cancellation semantics beyond the behavior already provided by Session operations receiving `context.abortSignal`; `nextResumeIdentityRequirement(operation, now)` remains context-free and pure. + +### Required reads — missing/mismatched faults `create()` + +| Data | Read | +|---|---| +| lane projection | `laneLeaf`, `laneConfig`, `laneState`, optional `laneLastResult` | +| open operation projection | `operationMeta`, `operationState` | +| transcript | one compaction-bounded branch scan from current leaf | +| pending queues | every `pendingEntry(id)` named by `pendingNextRun` and run inbox steer/follow-up | +| pending writes | every `pendingEntry(id)` named by run inbox writes | +| abort drain | every `pendingEntry(id)` named by drained steer/follow-up | +| deferred display | exact deferred source message carrying its handle | +| running tool display | batch assistant entry and exact persisted arguments for each `effect_pending` call represented in `runningTools`; planned calls have not started, and outcome-ready/completed calls are not running | + +Required message positions must contain messages of the required role/state. Required pending values must have the expected pending kind. Duplicate/missing lineage and impossible typed relationships fault attachment. + +### Optional reads — absence omits display fallback + +| Data | Read | +|---|---| +| interrupted assistant partial | bounded pages from exact `pendingAssistantFrames(operationId, responseEntryId)` | +| running tool checkpoint | exact `pendingToolOutput(operationId, invocationId)` | + +A storage read failure still faults. A valid empty frame list or absent checkpoint is not corruption. + +### Not hydrated for public inspection + +Do not read preparation values, invocation memos, staged outcomes, or other drive-only references unless a snapshot field introduced by the normative spec directly represents them. Their consuming procedure validates them during drive. + +### Startup complexity + +Per lane: + +- fixed projection point reads; +- one compaction-bounded transcript scan; +- O(ids named by the current lane/operation) exact value/entry reads; +- bounded assistant frame pages and exact optional checkpoints; +- no history-wide value scans, usage ledger reads, provider context construction, effects, hooks, tools, timers, or callbacks. + +## Phase A — Amend `harness.md` before code + +This phase is mandatory and has its own review stop. Do not edit runtime source until it passes review. + +### Part 0, terminology, and invocation context + +- Replace `SuspendedOperation` glossary/provenance entries with `OperationStatus` and `OpenOperation`. +- Define `running`, `resumable`, `blocked`, `aborting`, and idle once. +- State that status is process-relative observation, while durable operation phase/control remains recovery truth. +- Remove language equating every unowned operation with `status: "suspended"`. +- Motivate explicit Context with the two accepted requirements: correct telemetry parentage for concurrent asynchronous work, and a transport-agnostic RPC boundary for reconstructed request telemetry/cancellation. +- Document the existing source contract: every public harness/lane operation takes a trailing `context: Context`; hooks and event listeners receive the invocation context as their final argument; Session reads/writes receive that context. +- State that shared harness/lane/Session/SessionTree receivers retain no caller context. Context is invocation-scoped authority and propagation, not durable operation data: acceptance must not persist a `Context` or add it to the write set. +- Specify the RPC boundary principle: an adapter maps a request trace carrier and request cancellation/disconnect into a fresh local Context, strips/inserts Context outside serialized business arguments, and never serializes the Context object, `AbortSignal`, or `TelemetryContext` as business arguments. Whether specific adapter-managed typed values cross as control-plane metadata remains open in `rpc.md`. +- Distinguish `context.abortSignal` from durable abort: invocation or RPC cancellation must not write `cancel_requested`. +- Keep the existing `AgentHarnessOptions.telemetryContext` option unchanged in WP02, but do not describe it as the invocation span parent; its removal belongs to the pending context/telemetry design. +- Keep trailing parameter position and propagation behavior aligned with current source for WP02. Broader RPC transport, carrier encoding, and any future context-position migration remain open in non-normative `rpc.md` and are not decided by WP02. + +### §3.3 — Restore validation + +Replace the blanket "nothing else" language with two explicit attachment layers: + +1. projection restore: exact lane/operation point values; +2. public-state hydration: bounded state-directed reads required for coherent `open`, inspection, and initial watch. + +Amend the trusted-values doctrine explicitly: attachment does not schema-validate trusted stored objects, but it does validate existence, address/kind discriminants, and entry type/role relationships required to construct public state. Specify required-reference fault behavior and legal optional absence. Preserve consumption-time validation for drive-only references. + +### §3.6 — Acceptance + +Retain the atomic run acceptance algorithm and exact transaction. Replace references to caller entries with request prompt entries. State that accepted-undriven `starting` is `resumable`: its immediate `before_run` consumption and checkpoint commit are identity-free durable progress. + +### §3.11 and §4.6 — Queues and abort + +- Remove `SuspendedOperation.aborting`. +- Put drained payloads in `LaneSnapshot.operation.drained` and `AbortResult`. +- Require attachment hydration of every queued, pending-write, and abort-drained `pendingEntry(id)` represented by the snapshot. + +### §4.1 — Ownership and manual progress + +- Replace suspended-owner language with four-way `OperationStatus`. +- Add the rule that a manual breakpoint returns `action_required` instead of leaving the public drive/convenience promise unresolved. +- Same-operation callers observing a parked pass return the same current action without releasing it. +- Automatic mode crosses barriers and never returns `action_required`. + +### §4.4 — Attachment and missing identities + +Rewrite attachment as: + +```text +inventory +→ projection restore +→ bounded public-state hydration and validation +→ phase-aware status classification +→ publish complete harness + open[] +``` + +Enumerate exact required and optional reads from the End-state attachment hydration section above. Fix the stale "five point lookups" count to include optional `laneLastResult` without calling it a recovery input. Remove `reason: "crash"` reconstruction. + +Add the exhaustive identity requirement table and state that attachment classification is advisory while drive preflight is authoritative. + +### §4.5 — Drive and recovery + +- Keep expected-id fencing and durable rereads. +- State that `resume(context)` inspects and drives the current operation without exposing ids. +- Remove `MissingIdentities` from `ResumeResult` errors; use the existing suspended/waiting outcome. +- Preserve one deferred poll permit for `resume(context)`. +- Add `action_required` pass behavior, while leaving implementation to the next package. + +### §5.1 — Public result surface + +Apply the exact operation-outcome and action-required types above. Ensure convenience operation results are branches of `ResumeOutcome`, so manual progression moves from `prompt()`/`compact()` to `executeAction()` without conversion helpers. Add trailing `context: Context` to every normative method signature, and align the lane tree view with the current `readonly sessionTree: SessionTree` property rather than the stale `readonly session` name. + +### §5.2 — Harness creation + +- Specify `AgentHarness.create(options, context)` with trailing `context: Context` and require projection reads, hydration reads, and any initial configuration seed commit to receive it. +- Change `{ harness, suspended }` to `{ harness, open }`. +- Delete `SuspendedOperation`. +- Add `OpenOperation` and the status rules. +- State that `open` is an attachment observation, not a scheduler reservation. + +### §5.4 — Snapshots and watches + +- Apply the complete snapshot shape above. +- Require `watch(context)` to capture owned memory and start buffering atomically, with no storage read. +- Specify one lane-local observation gate ordering state publication plus synchronous event-batch enqueue against watcher registration plus synchronous memory capture. Event-bus enqueue binds each event's invocation context together with its recipient set immediately rather than consulting later listeners at delivery time. Therefore a watcher is either registered before publication and receives the `(event, context)` batch after an old snapshot, or registers after publication/enqueue and receives a new snapshot without that old batch. +- Explain why the pair is required: local event handlers derive telemetry from the emitting invocation, while an RPC subscription injects that same source lineage into each event frame and reconstructs a fresh client-side delivery context. Subscription-establishment context must not replace event-source context. +- Specify transcript, partial-message, checkpoint, queue, drained, retry, deferred, action, and live-over-durable precedence. +- Remove restored crash descriptors and lifecycle replay implications. + +### Part 8 roadmap + +- Mark WP02 in progress and rename its outcome to atomic acceptance plus coherent attachment/watch. +- Move real `action_required`, `executeAction`, `runToCompletion`, and convenience-drive behavior into the first no-tool drive package. +- Keep provider generation, retry ownership, deferred polling, tools, cancellation procedures, and full session watch in their owning packages. + +### Part 9 invariants and races + +Amend or add: + +- projection restore versus public-state hydration versus drive consumption; +- successful attachment publishes no incomplete lane; +- required attachment reference corruption faults once; optional frame/checkpoint absence is valid; +- `blocked` is phase-aware and never prevents identity-free progress; +- `open`/snapshot status is advisory and drive rereads durable state; +- initial watch snapshot plus buffered events has no gap; +- no pre-attachment lifecycle replay; +- automatic mode never returns `action_required`; +- no public execution promise remains parked invisibly behind a manual barrier; +- expected-id primitive fencing remains unchanged; +- shared receivers never retain invocation Context, and concurrent calls preserve independent telemetry/cancellation lineage; +- Context and its values are neither durable data nor serialized business arguments; +- buffered events retain `{ event, context }`, not only event payloads; +- invocation/RPC cancellation does not become durable cancellation; +- attachment/resume, registration/resume, close/attach, snapshot/resume, and external-finalization races. + +### Phase A review stop + +After editing only `harness.md` and this handoff: + +1. Run a Terra librarian contradiction audit. +2. Run a Fable work-product review. +3. Resolve every finding. +4. Re-read all changed normative sections together. +5. Do not begin Phase B until the user approves the normative diff. + +## Phase B — Implementation + +### Public types + +Modify `packages/agent/src/harness/agent-harness.ts`: + +- remove `SuspendedOperation` and exports; +- add `OperationStatus`, `OpenOperation`, `IdentityRequirement`, and `ActionRequired`; +- change `CurrentOperationInfo`, `LaneInfo`, `LaneSnapshot`, and `SessionSnapshot`; +- change `AgentHarness.create(options, context)` result to `open`; +- make convenience operation values operation-tagged branches of `ResumeOutcome`; +- remove post-acceptance `MissingIdentities` from `ResumeResult` errors; +- add `DriveOutcome.action_required`; +- change `executeAction(context)` and `runToCompletion(context)` result signatures; +- preserve every existing trailing `context: Context` parameter, including expected-id primitives, and do not persist `Context`. + +Update public type tests before runtime code. + +### Runtime source layout + +Add focused modules rather than growing `lane.ts` into a generic reducer: + +- `packages/agent/src/harness/runtime2/classify.ts` + - exhaustive `nextResumeIdentityRequirement(operation, now)`; + - status derivation from operation, local owner presence, and current registries. +- `packages/agent/src/harness/runtime2/hydrate.ts` + - bounded attachment reads under the `create(options, context)` context; + - required-reference validation; + - immutable presentation snapshot construction; + - optional frame/checkpoint fallback. +- `packages/agent/src/harness/runtime2/accept.ts` only if extracting the run acceptance planner materially clarifies `Lane`; do not introduce a future operation context. + +Modify: + +- `runtime2/types.ts` to hold the complete owned hydrated lane projection; +- `runtime2/restore.ts` to keep projection restoration direct and separate from public hydration; +- `runtime2/harness.ts` to hydrate all lanes before constructing/publishing the harness and return `open`; +- `runtime2/lane.ts` to implement acceptance, memory-only inspection, and memory-only `watch(context)`; +- `runtime2/index.ts` exports only as needed. + +### Atomic run acceptance + +Implement atomic acceptance under the approved Phase A contract. + +`accept(request, context)` performs state-independent normalization of the `OperationRequest` before `Lane.command(plan, context)`: + +- prompt strings/images; +- supplied message or message array; +- explicit skill formatting; +- prompt-template formatting; +- pending assistant rejection; +- unknown skill/template errors; +- supplied or minted operation id and request prompt entry ids. + +Keep the public prompt overloads exactly `[text, images | undefined, context]` and `[messageOrMessages, context]`, but leave both convenience implementations as `SliceNotImplemented`; WP02 must not turn `prompt()` into acceptance-only behavior. + +The accepting context is process-local call authority, not durable operation data. Pass it through `Lane.command(plan, context)`, `session.mutate(lane, callback, context)`, every Session read/commit, and fault handling as `onFault(cause, context)`. Do not add it to operation metadata or any write. + +Inside that one lane command: + +1. reject busy; +2. capture all current `pendingNextRun` ids; +3. phase-independent admission identity preflight against the captured config/registries; +4. read and validate captured pending messages; +5. reject zero placed messages; +6. parent captured next-run entries before request prompt entries; +7. commit exactly once; +8. publish owned hydrated state from `CommitResult`. + +Exact writes: + +```text +insert captured nextRun message entries +insert request prompt entries +delete captured pendingEntry values +set laneLeaf +set operationMeta +set operationState(run starting) +set laneState(current operation, pendingNextRun=[]) +``` + +Post-commit events carry the exact accepting invocation context through `onEvent(event, context)` while the bus remains open: + +```text +run_start +for each placed message: + message_start + message_end + entry_added +queue_update if nextRun was captured +accept resolves +``` + +Acceptance updates the owned transcript, leaf, operation projection, queues, and snapshot fields in the same publication. It starts no drive or effect. + +### Memory-only inspection and watch + +`inspectExecution(context)` derives current status from owned operation/control, current local owner state, the pure identity classifier, and current registries. It performs no storage read. + +`watch(context)` uses the Phase A observation protocol, not the current asynchronous `HarnessEventBus.watchFromSnapshot()` ordering by itself. One lane-local observation gate serializes synchronous owned-state publication plus event-batch enqueue against synchronous watcher registration plus memory capture. Event enqueue snapshots each event's invocation context together with recipients immediately; `WatchHandle.start(listener)` later delivers `(event, context)`. Event handlers still run later and outside lane/storage mutation lines. Since capture is memory-only, watch returns a coherent clone without a gap or duplicate. Event payload mutation cannot alter owned state. + +The main harness inherits lane `watch(context)`. `watchSession(context)` may remain later work, but all public session-snapshot type changes land now. + +### Out-of-scope execution methods + +`drive`, `resume`, `prompt`, `skill`, `promptFromTemplate`, `compact`, `navigateTree`, abort/queues, `executeAction`, and `runToCompletion` keep throwing `SliceNotImplemented` where WP02 does not already implement their non-executing primitive. Their updated signatures compile against future behavior. + +Do not add `ActiveOperation`, effect gates, breakpoint execution, hooks, providers, tools, retry timers, deferred polls, terminal settlement, or usage. + +## Required tests + +All runtime2 tests use `BACKGROUND_CONTEXT` by default and pass it explicitly to public harness/lane and Session calls. Public type tests cover hook/listener context signatures. Runtime context-propagation cases use a distinct derived `Context` and assert object-identical delivery to acceptance event and fault boundaries without durable serialization. + +### Public type contract + +- `SuspendedOperation` is absent. +- `OpenOperation` and status unions narrow exhaustively. +- convenience operation values are assignable to `ResumeOutcome`. +- `action_required` narrows across run/compaction/navigation and drive outcomes. +- `executeAction(context)`/`runToCompletion(context)` signatures match the normative contract. +- prompt overloads have the exact trailing-Context tuple shapes; `Context` is not assignable to the images or message positions. + +### Atomic acceptance + +Implement coverage for: + +- text-only, images-only, text-plus-images, supplied message arrays; +- pending assistant rejection; +- skill with/without instructions and prompt-template arguments; +- unknown resources and empty input write nothing; +- supplied/minted operation ids; +- exact transaction, parent chain, metadata, payload-free `starting`, settings snapshot, `CommitResult` materialization; +- pending-next-run capture and deletion; +- missing model/tools, busy structural/run operations; +- one concurrent accept winner; +- commit publication/failure and close races; +- exact post-commit events carrying the accepting `Context`; +- no callback/effect invocation. + +### Attachment statuses + +Construct every durable phase directly and assert: + +- idle omitted from `open`; +- starting and identity-free phases are resumable; +- model boundary with missing model is blocked; +- planned captured tool boundary with missing tool is blocked in captured order; +- unowned effect-pending recovery is resumable despite missing identities; +- safe replay requiring a missing tool is blocked; +- unsafe recovery/outcome materialization is resumable; +- deferred poll boundary is blocked only when its required model is missing; +- cancel requested is aborting and never blocked; +- replacing registries changes later inspection blocked/resumable without a durable write; +- attachment never reports running. + +### Required hydration and corruption + +For each required reference category: + +- valid data appears in owned memory and first snapshot; +- missing value, wrong pending kind, wrong entry type/role, or impossible relationship rejects create with one `HarnessFault`; +- no partial harness is returned; +- attachment commits nothing for an already configured session; +- no hook/provider/tool/timer/application callback runs. + +### Optional partial recovery + +- empty/missing assistant frame list omits streaming fallback; +- one and many frame pages reduce in sequence order; +- frame read failure faults attachment; +- absent tool checkpoint omits partial result; +- present checkpoint populates the represented running tool; +- live partial later overrides durable fallback without changing transcript. + +### Watch snapshots and races + +- first snapshot includes complete transcript, queues, pending writes, drained payloads, retry/deferred details, streaming message, running tools, leaf/result/control/status; +- unfinished assistant/tool partials are not committed transcript entries; +- pre-attachment message/tool lifecycle events are not replayed; +- watcher registered before concurrent acceptance publication receives either snapshot-before-plus-events or snapshot-after without a gap/duplicate; +- a watcher started after emission receives each buffered event with the object-identical emitting Context, never the watch/start invocation context; +- caller mutation of snapshot/event payloads does not mutate owned memory; +- watch performs no storage read; +- close/fault behavior matches event-bus lifecycle. + +### Drive authority boundary + +Without implementing drive, add direct classifier/hydration tests proving: + +- attachment status is advisory; +- later durable ownership changes make stale `open` harmless; +- no hydrated display object is stored in a durable operation value; +- expected-id primitive types remain mandatory. + +## Files + +### Add + +- `packages/agent/src/harness/runtime2/classify.ts` +- `packages/agent/src/harness/runtime2/hydrate.ts` +- `packages/agent/test/harness/runtime2/accept.test.ts` +- focused hydration/watch tests if `harness.test.ts` would become oversized. + +### Modify + +- `packages/agent/docs/harness.md` — Phase A first +- `packages/agent/docs/work-packages/02-atomic-run-acceptance.md` +- `packages/agent/src/harness/agent-harness.ts` +- `packages/agent/src/harness/runtime2/harness.ts` +- `packages/agent/src/harness/runtime2/lane.ts` +- `packages/agent/src/harness/runtime2/restore.ts` +- `packages/agent/src/harness/runtime2/types.ts` +- `packages/agent/test/harness/runtime2/harness.test.ts` +- `packages/agent/test/harness/runtime2/lane.test.ts` +- `packages/agent/test/harness/runtime2/restore.test.ts` +- `packages/agent/test/harness/types.test.ts` +- test utilities only where controlled/instrumented reads require them. + +No backend, storage schema, telemetry schema, coding-agent, or changelog change is expected. Stop for boundary review if one becomes necessary. On `dev`, defer the changelog entry. + +## Validation + +After Phase A docs: + +```bash +git diff --check +``` + +After Phase B code: + +```bash +cd packages/agent +node "$(git rev-parse --show-toplevel)/node_modules/vitest/dist/cli.js" --run \ + test/harness/runtime2/accept.test.ts \ + test/harness/runtime2/harness.test.ts \ + test/harness/runtime2/lane.test.ts \ + test/harness/runtime2/restore.test.ts \ + test/harness/types.test.ts + +cd "$(git rev-parse --show-toplevel)" +git diff --check +npm run check +./test.sh +``` + +If subprocess tests fail only because ignored workspace `dist` artifacts are stale, obtain explicit user authorization before `npm run build`, then rerun `./test.sh`. + +Report runtime2 source line counts. The rebased clean pre-WP02 baseline is 970 lines; treat growth above 1,900 runtime2 source lines as a design review trigger, not a compression target. + +## Review checkpoints + +1. **Phase A:** Terra contradiction audit, then Fable normative/handoff review. Resolve all findings and obtain user approval. +2. **Phase B midpoint:** review public types, classifier table, and hydration matrix before acceptance/watch implementation. +3. **Final:** Fable implementation review after focused tests and `npm run check`; resolve findings before full validation. + +Keep the same Fable subagent alive through the Phase A discussion so it retains the decision context. + +## Stop condition + +Stop when: + +- `harness.md` contains the approved status, attachment, watch, and manual-progress contract; +- prompt/skill/template acceptance commits exactly once into payload-free `starting`; +- successful create publishes only fully hydrated coherent lane memory and accurate `open[]` statuses; +- required attachment corruption faults and optional partial absence remains legal; +- immediate watch is read-free, gap-free, and UI-complete; +- no execution effect or owner is introduced; +- focused tests, `npm run check`, and full tests pass; +- runtime2 line count and deferred changelog are reported. + +Do not begin the first real drive package. `drive`, convenience execution, action progression, hooks, providers, tools, retries, deferred polling, cancellation procedures, and terminal settlement remain the next reviewed package.