43 KiB
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.tspackages/agent/src/harness/runtime2/lane.tspackages/agent/src/harness/runtime2/types.tspackages/agent/test/harness/runtime2/harness.test.tspackages/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:
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, oraborting; inspectExecution(context)and the firstwatch(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:
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:
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: durablecancel_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 immediatewatch(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:
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 oneHarnessFault; - 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 nextResumeResultaction or non-action outcome;runToCompletion(context)repeatedly releases actions and cannot returnaction_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 carries telemetry parentage and RPC request cancellation
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 specifically needs an explicit channel for the server-side abort signal belonging to one request without adding transport fields to every business method.
Concrete telemetry trace:
client invocation Context
→ rpc.client injects a trace carrier
→ rpc.server extracts a local telemetry parent
→ adapter derives a fresh server Context with withTelemetryContext(...)
→ harness call, Session commit, and emitted events preserve that lineage
Concrete RPC-abort trace:
server allocates one request AbortController
→ adapter derives the server Context with withAbortSignal(...) before invocation
→ harness begins with that Context
→ client context.abortSignal aborts and client sends cancel(request ID)
→ server aborts the matching request AbortController
→ the invocation's existing context.abortSignal fires
A connection disconnect aborts every active request controller and subscription owned by that connection. A pre-aborted client request is rejected by the adapter before server work starts.
Decision:
- every current public harness/lane operation receives one explicit trailing
context: Context; context.telemetryContextsupplies the invocation parent for harness/session spans, and derived child contexts preserve parentage across concurrent asynchronous branches without ambient state;context.abortSignalis the explicit process-local invocation-cancellation channel used by local callers and reconstructed by RPC adapters from per-request cancel/disconnect control; it never implies durablecancel_requestedand must not callrequestAbort()implicitly;- each RPC request has an independent server
AbortController; canceling one request or drive joiner must not abort unrelated callers or shared execution owned under a different policy; - 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, andTelemetryContextare 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; crashas a user-action classification;- attach-time broad history/value scans;
- attachment data as drive transition input;
- compatibility aliases for
SuspendedOperationor 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
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:
opencontains exactly one item per lane with a durable current operation;- idle lanes are omitted;
- attachment never returns
running; missingis present exactly when status isblocked;openis 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):
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
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.
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
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:
transcriptcontains committed entries only;streamingMessageis a display projection, not a committed transcript entry;runningTools[].partialResultis 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;
actionexists only while a live manual breakpoint is parked;retry,deferred, anddrainedare 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
SuspendedOperationglossary/provenance entries withOperationStatusandOpenOperation. - 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 carriage of the invocation
AbortSignalthat an RPC adapter reconstructs from one request's cancel/disconnect control. - 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
Contextor add it to the write set. - Specify the RPC boundary principle: the client maps
context.abortSignaltocancel(requestId); the server owns oneAbortControllerper request, aborts it on matching cancellation or connection loss, and useswithAbortSignalplus extracted telemetry to construct a fresh local Context. The adapter strips/inserts Context outside serialized business arguments and never serializes the Context object,AbortSignal, orTelemetryContextas business arguments. Whether specific adapter-managed typed values cross as control-plane metadata remains open inrpc.md. - Require pre-aborted RPC calls to start no server work, while leaving that rejection in the adapter rather than the core harness.
- Distinguish
context.abortSignalfrom durable abort: invocation or RPC cancellation must not callrequestAbort()or writecancel_requested; one canceled request/joiner must not cancel unrelated callers. - Keep the existing
AgentHarnessOptions.telemetryContextoption 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.mdand are not decided by WP02.
§3.3 — Restore validation
Replace the blanket "nothing else" language with two explicit attachment layers:
- projection restore: exact lane/operation point values;
- 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.drainedandAbortResult. - 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_requiredinstead 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:
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
MissingIdentitiesfromResumeResulterrors; use the existing suspended/waiting outcome. - Preserve one deferred poll permit for
resume(context). - Add
action_requiredpass 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 trailingcontext: Contextand require projection reads, hydration reads, and any initial configuration seed commit to receive it. - Change
{ harness, suspended }to{ harness, open }. - Delete
SuspendedOperation. - Add
OpenOperationand the status rules. - State that
openis 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;
blockedis 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; - RPC cancellation/disconnect reaches the matching invocation through
context.abortSignal, does not become durable cancellation, and does not cancel unrelated request contexts; - attachment/resume, registration/resume, close/attach, snapshot/resume, and external-finalization races.
Phase A review stop
After editing only harness.md and this handoff:
- Run a Terra librarian contradiction audit.
- Run a Fable work-product review.
- Resolve every finding.
- Re-read all changed normative sections together.
- 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
SuspendedOperationand exports; - add
OperationStatus,OpenOperation,IdentityRequirement, andActionRequired; - change
CurrentOperationInfo,LaneInfo,LaneSnapshot, andSessionSnapshot; - change
AgentHarness.create(options, context)result toopen; - make convenience operation values operation-tagged branches of
ResumeOutcome; - remove post-acceptance
MissingIdentitiesfromResumeResulterrors; - add
DriveOutcome.action_required; - change
executeAction(context)andrunToCompletion(context)result signatures; - preserve every existing trailing
context: Contextparameter, including expected-id primitives, and do not persistContext.
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.
- exhaustive
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.
- bounded attachment reads under the
packages/agent/src/harness/runtime2/accept.tsonly if extracting the run acceptance planner materially clarifiesLane; do not introduce a future operation context.
Modify:
runtime2/types.tsto hold the complete owned hydrated lane projection;runtime2/restore.tsto keep projection restoration direct and separate from public hydration;runtime2/harness.tsto hydrate all lanes before constructing/publishing the harness and returnopen;runtime2/lane.tsto implement acceptance, memory-only inspection, and memory-onlywatch(context);runtime2/index.tsexports 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:
- reject busy;
- capture all current
pendingNextRunids; - phase-independent admission identity preflight against the captured config/registries;
- read and validate captured pending messages;
- reject zero placed messages;
- parent captured next-run entries before request prompt entries;
- commit exactly once;
- publish owned hydrated state from
CommitResult.
Exact writes:
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:
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
SuspendedOperationis absent.OpenOperationand status unions narrow exhaustively.- convenience operation values are assignable to
ResumeOutcome. action_requirednarrows 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;
Contextis 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,CommitResultmaterialization; - 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
openharmless; - 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.tspackages/agent/src/harness/runtime2/hydrate.tspackages/agent/test/harness/runtime2/accept.test.ts- focused hydration/watch tests if
harness.test.tswould become oversized.
Modify
packages/agent/docs/harness.md— Phase A firstpackages/agent/docs/work-packages/02-atomic-run-acceptance.mdpackages/agent/src/harness/agent-harness.tspackages/agent/src/harness/runtime2/harness.tspackages/agent/src/harness/runtime2/lane.tspackages/agent/src/harness/runtime2/restore.tspackages/agent/src/harness/runtime2/types.tspackages/agent/test/harness/runtime2/harness.test.tspackages/agent/test/harness/runtime2/lane.test.tspackages/agent/test/harness/runtime2/restore.test.tspackages/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:
git diff --check
After Phase B code:
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
- Phase A: Terra contradiction audit, then Fable normative/handoff review. Resolve all findings and obtain user approval.
- Phase B midpoint: review public types, classifier table, and hydration matrix before acceptance/watch implementation.
- 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.mdcontains 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.