docs(agent): add durable queue-item cancellation to harness v2 variants

Queued steer/followUp/nextRun items get a durable identity and retraction,
mirrored in both design variants so the diff stays mechanism-only:

- queue_enqueued acceptance returns the provisioned entryId; snapshots and
  queue_update events carry QueuedItem { entryId, message }.
- New queue_cancelled record: without it, recovery would resurrect a
  cancelled item (record-without-entry restores as pending). Validity rules
  reject cancels of unknown or already-consumed items.
- cancelQueued(entryId) is one conditional mutation-line job; race-catalog
  row 12 covers cancel-vs-consume with exactly two histories. Reduction now
  also excludes abort-killed items (baseline omission).
- Adds the LaneRecord/NewRecord unions that sections 12/13 already
  referenced but section 5 never defined.

Adopted from the effect-durable-agent comparison (pending-message identity
and cancellation), trimmed to the minimal slice.
This commit is contained in:
Mario Zechner 2026-08-02 23:09:59 +02:00
parent 88ed89dbab
commit 01eeafd14a
2 changed files with 104 additions and 18 deletions

View file

@ -305,6 +305,15 @@ interface QueueEnqueued extends RecordBase {
target: ProvisionedEntry;
}
// Durable retraction of a pending queue item, before consumption. Without
// this record a crash would resurrect the item: recovery treats a
// queue_enqueued without its entry as pending.
interface QueueCancelled extends RecordBase {
type: "queue_cancelled";
runId?: string; // matches the queue_enqueued it kills
entryId: string; // the enqueued target's provisioned id
}
// Deferred-write acceptance: an entry or configuration change requested
// while a step was in flight. Applied at the next checkpoint.
interface WriteDeferred extends RecordBase {
@ -312,6 +321,12 @@ interface WriteDeferred extends RecordBase {
runId: string;
target: ProvisionedEntry;
}
type LaneRecord = OperationStarted | AbortRequested | OperationFinished
| TaskAttempt | ToolStarted | QueueEnqueued | QueueCancelled | WriteDeferred;
type NewRecord<T extends LaneRecord = LaneRecord> =
T extends LaneRecord ? Omit<T, "seq" | "timestamp"> : never;
```
Blocked or invalid tool calls write no `tool_started`. No effect starts, so no intent is needed: the block is durable as a tool-result entry with `isError: true` and the block reason as content. A crash before that entry loses only the decision, and recovery makes it again — `before_tool` runs again for a call with no `tool_started` and no result.
@ -327,6 +342,7 @@ Recovery rejects a lane's log as corrupt when:
- attempt numbers are not consecutive within a task;
- `compactionReason` is absent from a compaction attempt or present on another task kind;
- a steer or follow-up `queue_enqueued` for a run follows its `abort_requested`;
- a `queue_cancelled` targets an id with no `queue_enqueued`, or one whose entry exists;
- two `tool_started` records share an invocation identity;
- a provisioned id exists with different content.
@ -396,6 +412,16 @@ R task_attempt next request sees the steering message
Crash before `queue_enqueued`: the steer never happened; the caller's promise never resolved. Crash after: recovery finds the record without its entry and appends it at the same point the checkpoint would have.
A queued item can be durably retracted before consumption:
```text
R queue_enqueued steer, full payload, provisioned id
cancelQueued(entryId) caller resolves here
R queue_cancelled the entry will never be appended
```
Crash between the two records: the item is still pending; the cancel promise never resolved. Cancellation and consumption are jobs on the lane mutation line, so `[cancel, consume]` and `[consume, cancel]` are the only histories (section 15).
### Input at the finish boundary
Same-lane decisions have one order: the lane mutation line (section 15). The final pending-work check and the terminal append are one `tryFinishRun` mutation, so a concurrent steer has exactly two histories:
@ -533,7 +559,7 @@ Deferred assistant messages carry a handle, not content; they project to nothing
Opening a session restores every lane independently. Restore reads; it never appends and never starts effects.
Per lane, one question: does the operation log hold an `operation_started` without a matching `operation_finished`? No: the lane is idle. Its only remaining state is pending next-run queue items. Next-run messages can be enqueued at any time; only the acceptance of a run consumes them — compaction and navigation pass over the queue. Pending items are therefore the `queue_enqueued` records after the lane's most recent run-kind `operation_started` whose provisioned entries do not exist; nothing older can still be pending. Items a run captured are listed in its intent's `initialMessages`, so a captured-but-unappended item is completed by that run's recovery and is never offered to the next run. Yes: the lane is suspended, and its state is reduced from two bounded reads:
Per lane, one question: does the operation log hold an `operation_started` without a matching `operation_finished`? No: the lane is idle. Its only remaining state is pending next-run queue items. Next-run messages can be enqueued at any time; only the acceptance of a run consumes them — compaction and navigation pass over the queue. Pending items are therefore the `queue_enqueued` records after the lane's most recent run-kind `operation_started` whose provisioned entries do not exist and that no `queue_cancelled` retracts; nothing older can still be pending. Items a run captured are listed in its intent's `initialMessages`, so a captured-but-unappended item is completed by that run's recovery and is never offered to the next run. Yes: the lane is suspended, and its state is reduced from two bounded reads:
1. **The lane's records** since that `operation_started`. Everything after the finish of the previous operation is irrelevant history.
2. **The lane's own entries**: the path from its leaf back to the operation's anchor (`sourceLeafId`). These are exactly the entries this operation appended.
@ -548,7 +574,7 @@ From those two reads, the lane's state:
- **attempts used** — `task_attempt` records newer than the lane's newest own entry. An entry landing ends its task; attempts before it belong to finished work.
- **tool batch** — the newest assistant entry with tool calls, each call matched against `tool_started` records and result entries (section 6, crash-site table). The assistant stop reason is retained: a `length` batch is truncated and never executes on recovery. Persisted `terminate` values on result entries decide whether the completed batch forces another step.
- **deferred handle** — the newest own entry is a deferred assistant message with no successor.
- **pending queue items** — `queue_enqueued` records whose provisioned entry does not exist.
- **pending queue items** — `queue_enqueued` records whose provisioned entry does not exist, excluding items retracted by `queue_cancelled` and steer/follow-up items killed by this run's `abort_requested`.
- **pending writes** — `write_deferred` records whose provisioned entry does not exist.
- **missing initial messages** — provisioned ids from the run intent without entries.
- **structural targets** — for compaction and navigation: does the provisioned result entry exist.
@ -595,14 +621,17 @@ interface AgentLane {
resume(): Promise<ResumeResult>; // continue this lane's open operation
abort(): Promise<AbortResult>; // durable on resolve; reconciliation runs in background
// Queues. Durable on resolve (queue_enqueued record). steer/followUp
// require an active run; nextRun works anytime.
// Queues. Durable on resolve (queue_enqueued record); the returned
// entryId identifies the item until consumption. steer/followUp require
// an active run; nextRun and cancelQueued work anytime.
steer(text: string, images?: ImageContent[]): Promise<QueueResult>;
steer(message: AgentMessage): Promise<QueueResult>;
followUp(text: string, images?: ImageContent[]): Promise<QueueResult>;
followUp(message: AgentMessage): Promise<QueueResult>;
nextRun(text: string, images?: ImageContent[]): Promise<QueueResult>;
nextRun(message: AgentMessage): Promise<QueueResult>;
/** Durably retract a pending queue item (queue_cancelled record). */
cancelQueued(entryId: string): Promise<CancelQueuedResult>;
waitForIdle(): Promise<void>;
runWhenIdle(callback: () => void | Promise<void>): Promise<void>; // runtime-only
@ -764,7 +793,12 @@ type NavigationResult =
| { ok: false; outcome: "failed"; runId: string; error: ErrorInfo }
| Failure;
type QueueResult = { ok: true } | Failure;
type QueueResult = { ok: true; entryId: string } | Failure;
type CancelQueuedResult =
| { ok: true } // entry will never be appended
| { ok: false; outcome: "already_consumed" | "already_cleared" }
| Failure; // unknown id: rejected
type ResumeResult =
| ({ kind: "run" } & RunResult)
@ -772,7 +806,9 @@ type ResumeResult =
| ({ kind: "navigation" } & NavigationResult);
```
Rejection codes: `busy` (this lane), `no_active_run`, `no_active_operation`, `nothing_to_resume`, `missing_identities`, `invalid_message`, `unknown_skill`, `unknown_template`, `unknown_target`, `unknown_lane`, `lane_exists`, `invalid_lane`, `nothing_to_compact`, `closed`, `faulted`.
Rejection codes: `busy` (this lane), `no_active_run`, `no_active_operation`, `nothing_to_resume`, `missing_identities`, `invalid_message`, `unknown_skill`, `unknown_template`, `unknown_target`, `unknown_queue_item`, `unknown_lane`, `lane_exists`, `invalid_lane`, `nothing_to_compact`, `closed`, `faulted`.
`cancelQueued` outcomes mirror the mutation-line histories: `already_consumed` means the entry exists (the model saw or will see it); `already_cleared` means abort drained the item or an earlier cancel won.
`finalMessage` is the run's newest entry that projects to an assistant message; `finalEntryId` is that entry's id. `leafId` is the lane's leaf when the operation finished — the race-free anchor for branch queries (`findEntriesOnBranch({ start: leafId })`). The two differ when a deferred write was applied after the final assistant message. Full transcripts are not duplicated into results; they are in the session and were delivered as events.
@ -843,6 +879,11 @@ start((event) => send(client, event)); // flush buffer in order,
`watch()` is lane-scoped: this lane's transcript, operation state, queues, pending writes, and only this lane's events. A Slack thread renderer sees its thread and nothing else. `watchSession()` is the session-wide observer: lane inventory, no transcripts, unfiltered event stream. A dashboard composes both: `watchSession()` for the overview, `lane.watch()` per opened thread.
```ts
interface QueuedItem {
entryId: string; // correlates with QueueResult and cancelQueued
message: AgentMessage;
}
interface LaneSnapshot {
lane: string;
/** This lane's branch, oldest first: the context window plus its
@ -870,7 +911,7 @@ interface LaneSnapshot {
retry?: { attempt: number; maxAttempts: number; nextAttemptAt: number };
};
queues: { steer: AgentMessage[]; followUp: AgentMessage[]; nextRun: AgentMessage[] };
queues: { steer: QueuedItem[]; followUp: QueuedItem[]; nextRun: QueuedItem[] };
pendingWrites: { id: string; entry: ProvisionedEntry }[];
faulted: boolean; // harness-wide, mirrored into every snapshot
@ -939,7 +980,7 @@ Guarantees:
{ type: "entry_added"; entry: Entry } // non-message entries
{ type: "write_pending"; runId; entryId; entry } // deferred write accepted; entry_added
// or message_end follows with the same id
{ type: "queue_update"; steer: AgentMessage[]; followUp: AgentMessage[]; nextRun: AgentMessage[] }
{ type: "queue_update"; steer: QueuedItem[]; followUp: QueuedItem[]; nextRun: QueuedItem[] }
{ type: "fact_update" } & (
| { fact: "name"; name: string }
| { fact: "label"; targetId: string; label: string | undefined })
@ -1608,6 +1649,7 @@ The jobs, by caller:
- **Lane surface** (ungated, enqueue directly):
- *Operation acceptance* — validate idle, capture the pending `nextRun` items into `initialMessages`, write `operation_started`, set `state.operation`. The second of two concurrent acceptances sees the first and rejects `busy` with no write. `before_run` ran before this job, outside the line, on the prompt only.
- *Queue acceptance* (`steer`, `followUp`) — validate an active, non-aborting run; write `queue_enqueued`. `nextRun` validates nothing and always accepts.
- *Queue cancellation* (`cancelQueued`) — target entry exists: `already_consumed`; not pending (abort-drained or already cancelled): `already_cleared`; else write `queue_cancelled` and remove the item from its pending set.
- *Deferred-write acceptance* (lane-view writes, config setters) — run open: write `write_deferred`; structural operation open: wait for it to end, then re-enter; idle: append the entry directly.
- *Abort* — write `abort_requested`, set `aborting`, drain `pendingSteer`/`pendingFollowUp` (payloads return to the abort caller and in the `run_abort` event), signal the active effect's `AbortController`.
- *Resume admission* — reserve the lane's single execution slot; no write.
@ -1649,6 +1691,7 @@ The complete list. Each row names the two legal histories and the jobs that forc
| 9 | config/tree write vs acceptance snapshot | committed before the run's first request · deferred write | both are line jobs; snapshots read after acceptance |
| 10 | abort vs in-flight provider/tool effect | effect settles · effect interrupted | irreducible: signal cancellation; only the procedure commits results (abort path owns synthetics) |
| 11 | cross-lane writes | any interleaving | storage `seq` linearization (section 13); lanes share no state |
| 12 | `cancelQueued` vs consumption | consumed first: `already_consumed` · cancelled first: consumption skips, the model never sees it | cancel job + `consumeQueueItem` |
Row 10 is the one race no ordering can remove: an external effect may have happened even though its result never arrived. The design's answer is the section 5 intent record plus the replay policy — the same answer as for a crash.
@ -2279,7 +2322,7 @@ expect(suspended).toHaveLength(1);
expect((await harness.resume()).ok).toBe(true);
```
Coverage: every X1–X5 tool state, replay safe/never/changed declarations, every source-order position in a batch, truncated (`length`) batches proving no execution, abort before and after each durable point, the terminal-failure marker with and without later consumed input, missing initial messages, pending and abort-killed queue items, deferred writes, deferred handles (pending, ready, terminal, rejected fetch, abort), unfinished tasks and attempt caps across restart including auto-compaction exhaustion, post-move navigation states from the section 6 table, section 5 validity rejections, and half-completed recovery (run the same prefix through recovery twice).
Coverage: every X1–X5 tool state, replay safe/never/changed declarations, every source-order position in a batch, truncated (`length`) batches proving no execution, abort before and after each durable point, the terminal-failure marker with and without later consumed input, missing initial messages, pending, cancelled, and abort-killed queue items, deferred writes, deferred handles (pending, ready, terminal, rejected fetch, abort), unfinished tasks and attempt caps across restart including auto-compaction exhaustion, post-move navigation states from the section 6 table, section 5 validity rejections, and half-completed recovery (run the same prefix through recovery twice).
The in-memory backend is the reference. The parity suite runs the same setups against memory, JSONL, and SQLite; one case runs concurrent writes on two lanes and asserts unique increasing `seq` and identical `getLog()` order; another asserts every backend rejects the same non-JSON payloads.

View file

@ -305,6 +305,15 @@ interface QueueEnqueued extends RecordBase {
target: ProvisionedEntry;
}
// Durable retraction of a pending queue item, before consumption. Without
// this record a crash would resurrect the item: recovery treats a
// queue_enqueued without its entry as pending.
interface QueueCancelled extends RecordBase {
type: "queue_cancelled";
runId?: string; // matches the queue_enqueued it kills
entryId: string; // the enqueued target's provisioned id
}
// Deferred-write acceptance: an entry or configuration change requested
// while a step was in flight. Applied at the next checkpoint.
interface WriteDeferred extends RecordBase {
@ -312,6 +321,12 @@ interface WriteDeferred extends RecordBase {
runId: string;
target: ProvisionedEntry;
}
type LaneRecord = OperationStarted | AbortRequested | OperationFinished
| TaskAttempt | ToolStarted | QueueEnqueued | QueueCancelled | WriteDeferred;
type NewRecord<T extends LaneRecord = LaneRecord> =
T extends LaneRecord ? Omit<T, "seq" | "timestamp"> : never;
```
Blocked or invalid tool calls write no `tool_started`. No effect starts, so no intent is needed: the block is durable as a tool-result entry with `isError: true` and the block reason as content. A crash before that entry loses only the decision, and recovery makes it again — `before_tool` runs again for a call with no `tool_started` and no result.
@ -327,6 +342,7 @@ Recovery rejects a lane's log as corrupt when:
- attempt numbers are not consecutive within a task;
- `compactionReason` is absent from a compaction attempt or present on another task kind;
- a steer or follow-up `queue_enqueued` for a run follows its `abort_requested`;
- a `queue_cancelled` targets an id with no `queue_enqueued`, or one whose entry exists;
- two `tool_started` records share an invocation identity;
- a provisioned id exists with different content.
@ -396,6 +412,16 @@ R task_attempt next request sees the steering message
Crash before `queue_enqueued`: the steer never happened; the caller's promise never resolved. Crash after: recovery finds the record without its entry and appends it at the same point the checkpoint would have.
A queued item can be durably retracted before consumption:
```text
R queue_enqueued steer, full payload, provisioned id
cancelQueued(entryId) caller resolves here
R queue_cancelled the entry will never be appended
```
Crash between the two records: the item is still pending; the cancel promise never resolved. Cancellation and consumption are jobs on the lane mutation line, so `[cancel, consume]` and `[consume, cancel]` are the only histories (section 15).
### Input at the finish boundary
Same-lane decisions have one order: the lane mutation line (section 15). The final pending-work check and the terminal append are one `tryFinishRun` mutation, so a concurrent steer has exactly two histories:
@ -533,7 +559,7 @@ Deferred assistant messages carry a handle, not content; they project to nothing
Opening a session restores every lane independently. Restore reads; it never appends and never starts effects.
Per lane, one question: does the operation log hold an `operation_started` without a matching `operation_finished`? No: the lane is idle. Its only remaining state is pending next-run queue items. Next-run messages can be enqueued at any time; only the acceptance of a run consumes them — compaction and navigation pass over the queue. Pending items are therefore the `queue_enqueued` records after the lane's most recent run-kind `operation_started` whose provisioned entries do not exist; nothing older can still be pending. Items a run captured are listed in its intent's `initialMessages`, so a captured-but-unappended item is completed by that run's recovery and is never offered to the next run. Yes: the lane is suspended, and its state is reduced from two bounded reads:
Per lane, one question: does the operation log hold an `operation_started` without a matching `operation_finished`? No: the lane is idle. Its only remaining state is pending next-run queue items. Next-run messages can be enqueued at any time; only the acceptance of a run consumes them — compaction and navigation pass over the queue. Pending items are therefore the `queue_enqueued` records after the lane's most recent run-kind `operation_started` whose provisioned entries do not exist and that no `queue_cancelled` retracts; nothing older can still be pending. Items a run captured are listed in its intent's `initialMessages`, so a captured-but-unappended item is completed by that run's recovery and is never offered to the next run. Yes: the lane is suspended, and its state is reduced from two bounded reads:
1. **The lane's records** since that `operation_started`. Everything after the finish of the previous operation is irrelevant history.
2. **The lane's own entries**: the path from its leaf back to the operation's anchor (`sourceLeafId`). These are exactly the entries this operation appended.
@ -548,7 +574,7 @@ From those two reads, the lane's state:
- **attempts used** — `task_attempt` records newer than the lane's newest own entry. An entry landing ends its task; attempts before it belong to finished work.
- **tool batch** — the newest assistant entry with tool calls, each call matched against `tool_started` records and result entries (section 6, crash-site table). The assistant stop reason is retained: a `length` batch is truncated and never executes on recovery. Persisted `terminate` values on result entries decide whether the completed batch forces another step.
- **deferred handle** — the newest own entry is a deferred assistant message with no successor.
- **pending queue items** — `queue_enqueued` records whose provisioned entry does not exist.
- **pending queue items** — `queue_enqueued` records whose provisioned entry does not exist, excluding items retracted by `queue_cancelled` and steer/follow-up items killed by this run's `abort_requested`.
- **pending writes** — `write_deferred` records whose provisioned entry does not exist.
- **missing initial messages** — provisioned ids from the run intent without entries.
- **structural targets** — for compaction and navigation: does the provisioned result entry exist.
@ -595,14 +621,17 @@ interface AgentLane {
resume(): Promise<ResumeResult>; // continue this lane's open operation
abort(): Promise<AbortResult>; // durable on resolve; reconciliation runs in background
// Queues. Durable on resolve (queue_enqueued record). steer/followUp
// require an active run; nextRun works anytime.
// Queues. Durable on resolve (queue_enqueued record); the returned
// entryId identifies the item until consumption. steer/followUp require
// an active run; nextRun and cancelQueued work anytime.
steer(text: string, images?: ImageContent[]): Promise<QueueResult>;
steer(message: AgentMessage): Promise<QueueResult>;
followUp(text: string, images?: ImageContent[]): Promise<QueueResult>;
followUp(message: AgentMessage): Promise<QueueResult>;
nextRun(text: string, images?: ImageContent[]): Promise<QueueResult>;
nextRun(message: AgentMessage): Promise<QueueResult>;
/** Durably retract a pending queue item (queue_cancelled record). */
cancelQueued(entryId: string): Promise<CancelQueuedResult>;
waitForIdle(): Promise<void>;
runWhenIdle(callback: () => void | Promise<void>): Promise<void>; // runtime-only
@ -764,7 +793,12 @@ type NavigationResult =
| { ok: false; outcome: "failed"; runId: string; error: ErrorInfo }
| Failure;
type QueueResult = { ok: true } | Failure;
type QueueResult = { ok: true; entryId: string } | Failure;
type CancelQueuedResult =
| { ok: true } // entry will never be appended
| { ok: false; outcome: "already_consumed" | "already_cleared" }
| Failure; // unknown id: rejected
type ResumeResult =
| ({ kind: "run" } & RunResult)
@ -772,7 +806,9 @@ type ResumeResult =
| ({ kind: "navigation" } & NavigationResult);
```
Rejection codes: `busy` (this lane), `no_active_run`, `no_active_operation`, `nothing_to_resume`, `missing_identities`, `invalid_message`, `unknown_skill`, `unknown_template`, `unknown_target`, `unknown_lane`, `lane_exists`, `invalid_lane`, `nothing_to_compact`, `closed`, `faulted`.
Rejection codes: `busy` (this lane), `no_active_run`, `no_active_operation`, `nothing_to_resume`, `missing_identities`, `invalid_message`, `unknown_skill`, `unknown_template`, `unknown_target`, `unknown_queue_item`, `unknown_lane`, `lane_exists`, `invalid_lane`, `nothing_to_compact`, `closed`, `faulted`.
`cancelQueued` outcomes mirror the mutation-line histories: `already_consumed` means the entry exists (the model saw or will see it); `already_cleared` means abort drained the item or an earlier cancel won.
`finalMessage` is the run's newest entry that projects to an assistant message; `finalEntryId` is that entry's id. `leafId` is the lane's leaf when the operation finished — the race-free anchor for branch queries (`findEntriesOnBranch({ start: leafId })`). The two differ when a deferred write was applied after the final assistant message. Full transcripts are not duplicated into results; they are in the session and were delivered as events.
@ -843,6 +879,11 @@ start((event) => send(client, event)); // flush buffer in order,
`watch()` is lane-scoped: this lane's transcript, operation state, queues, pending writes, and only this lane's events. A Slack thread renderer sees its thread and nothing else. `watchSession()` is the session-wide observer: lane inventory, no transcripts, unfiltered event stream. A dashboard composes both: `watchSession()` for the overview, `lane.watch()` per opened thread.
```ts
interface QueuedItem {
entryId: string; // correlates with QueueResult and cancelQueued
message: AgentMessage;
}
interface LaneSnapshot {
lane: string;
/** This lane's branch, oldest first: the context window plus its
@ -870,7 +911,7 @@ interface LaneSnapshot {
retry?: { attempt: number; maxAttempts: number; nextAttemptAt: number };
};
queues: { steer: AgentMessage[]; followUp: AgentMessage[]; nextRun: AgentMessage[] };
queues: { steer: QueuedItem[]; followUp: QueuedItem[]; nextRun: QueuedItem[] };
pendingWrites: { id: string; entry: ProvisionedEntry }[];
faulted: boolean; // harness-wide, mirrored into every snapshot
@ -939,7 +980,7 @@ Guarantees:
{ type: "entry_added"; entry: Entry } // non-message entries
{ type: "write_pending"; runId; entryId; entry } // deferred write accepted; entry_added
// or message_end follows with the same id
{ type: "queue_update"; steer: AgentMessage[]; followUp: AgentMessage[]; nextRun: AgentMessage[] }
{ type: "queue_update"; steer: QueuedItem[]; followUp: QueuedItem[]; nextRun: QueuedItem[] }
{ type: "fact_update" } & (
| { fact: "name"; name: string }
| { fact: "label"; targetId: string; label: string | undefined })
@ -1709,6 +1750,7 @@ The jobs, by caller:
- **Lane surface** (enqueue directly, never through the machine):
- *Operation acceptance* — validate idle, capture the pending `nextRun` items into `initialMessages`, write `operation_started`, set `state.operation`. The second of two concurrent acceptances sees the first and rejects `busy` with no write. `before_run` ran before this job, outside the line, on the prompt only.
- *Queue acceptance* (`steer`, `followUp`) — validate an active, non-aborting run; write `queue_enqueued`. `nextRun` validates nothing and always accepts.
- *Queue cancellation* (`cancelQueued`) — target entry exists: `already_consumed`; not pending (abort-drained or already cancelled): `already_cleared`; else write `queue_cancelled` and remove the item from its pending set.
- *Deferred-write acceptance* (lane-view writes, config setters) — run open: write `write_deferred`; structural operation open: wait for it to end, then re-enter; idle: append the entry directly.
- *Abort* — write `abort_requested`, set `aborting`, drain `pendingSteer`/`pendingFollowUp` (payloads return to the abort caller and in the `run_abort` event), signal the active effect's `AbortController`.
- *Resume admission* — reserve the lane's single execution slot; no write.
@ -1752,6 +1794,7 @@ The complete list. Each row names the two legal histories and the jobs that forc
| 9 | config/tree write vs acceptance snapshot | committed before the run's first request · deferred write | both are line jobs; snapshots read after acceptance |
| 10 | abort vs in-flight provider/tool effect | effect settles · effect interrupted | irreducible: signal cancellation; only the machine commits results (abort machine owns synthetics) |
| 11 | cross-lane writes | any interleaving | storage `seq` linearization (section 13); lanes share no state |
| 12 | `cancelQueued` vs consumption | consumed first: `already_consumed` · cancelled first: consumption skips, the model never sees it | cancel job + `consume_queue_item` |
Row 10 is the one race no ordering can remove: an external effect may have happened even though its result never arrived. The design's answer is the section 5 intent record plus the replay policy — the same answer as for a crash.
@ -2402,7 +2445,7 @@ expect(suspended).toHaveLength(1);
expect((await harness.resume()).ok).toBe(true);
```
Coverage: every X1–X5 tool state, replay safe/never/changed declarations, every source-order position in a batch, truncated (`length`) batches proving no execution, abort before and after each durable point, the terminal-failure marker with and without later consumed input, missing initial messages, pending and abort-killed queue items, deferred writes, deferred handles (pending, ready, terminal, rejected fetch, abort), unfinished tasks and attempt caps across restart including auto-compaction exhaustion, post-move navigation states from the section 6 table, section 5 validity rejections, and half-completed recovery (run the same prefix through recovery twice).
Coverage: every X1–X5 tool state, replay safe/never/changed declarations, every source-order position in a batch, truncated (`length`) batches proving no execution, abort before and after each durable point, the terminal-failure marker with and without later consumed input, missing initial messages, pending, cancelled, and abort-killed queue items, deferred writes, deferred handles (pending, ready, terminal, rejected fetch, abort), unfinished tasks and attempt caps across restart including auto-compaction exhaustion, post-move navigation states from the section 6 table, section 5 validity rejections, and half-completed recovery (run the same prefix through recovery twice).
The in-memory backend is the reference. The parity suite runs the same setups against memory, JSONL, and SQLite; one case runs concurrent writes on two lanes and asserts unique increasing `seq` and identical `getLog()` order; another asserts every backend rejects the same non-JSON payloads.