docs(agent): define coherent attachment work package

This commit is contained in:
Mario Zechner 2026-08-17 22:19:58 +02:00
parent b7277a566a
commit 9a8b75b1f8
2 changed files with 819 additions and 4 deletions

View file

@ -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<T>`, session-level ranked results, optional `searchEntries?: SessionSearch<TEntryHit>`, `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. |

View file

@ -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<TContext extends object | undefined = object | undefined>(
options: AgentHarnessOptions<TContext>,
context: Context,
): Promise<{ harness: AgentHarness<TContext>; 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<ResumeOutcome, { kind: "action_required" }>;
export type RunResult = Result<RunOperationOutcome, ExistingRunAdmissionErrors>;
export type CompactionResult = Result<CompactionOperationOutcome, ExistingCompactionAdmissionErrors>;
export type NavigationResult = Result<NavigationOperationOutcome, ExistingNavigationAdmissionErrors>;
export type ResumeResult = Result<ResumeOutcome, NothingToResume | Closed>;
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<ResumeResult>;
executeAction(context: Context): Promise<ResumeResult>;
runToCompletion(
context: Context,
): Promise<Result<SettledResumeOutcome, NothingToResume | Closed>>;
peekAction(context: Context): Promise<ActionInfo | undefined>;
}
```
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<unknown>;
}[];
};
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.