diff --git a/packages/chord/docs/next/slice-6b.md b/packages/chord/docs/next/slice-6b.md
index 02f685ac0..52d9cd21e 100644
--- a/packages/chord/docs/next/slice-6b.md
+++ b/packages/chord/docs/next/slice-6b.md
@@ -1,245 +1,316 @@
# Slice 6b: Durable and jobs on the store (spec)
-Status: **decided, ready for the spec reviews** (2026-10-27; D1-D9 decided by the owner), written by the redesign session after reading
-Durable's `Storage` interface (`packages/durable/src/types.ts`), its SQLite storage, schema and migrations, the storage
-conformance suite and benchmark, `session/session.ts`, `session/transaction.ts`, `session/forks.ts`, and Chord v2's
-store engine, applier, schema, commit step and jobs, in full. Durable's memory, JSONL and IndexedDB storages were not
-read (they stay as they are, S15).
+Status: **revised after the first spec review** (2026-10-27): D1-D9 and R1-R4 decided by the owner (decision log,
+entries "Slice 6b …" and "owner names and claims"); the four reviews (`/tmp/s6b-review-{astra,fable,opus,sol}.md`)
+kept every decision and found the gaps this revision closes. Written by the redesign session after reading, in full,
+Durable's `types.ts`, its SQLite storage, schema and migrations, the storage conformance suite and benchmark,
+`session/session.ts`, `session/transaction.ts`, `session/forks.ts`, the scheduler's restarts, `writeAtClose` and `abort`,
+and Chord v2's store, engine, applier, commit step and jobs. Durable's memory, JSONL and IndexedDB storages stay as they
+are (S15).
-The single source for building 6b, together with: `design.md` read in full; the decision log
-(`packages/chord/docs/next/decisions.md`, later entries win), in particular "The store", S1, S2, S4, S10, S11, S13, S15,
-W1, W12, W13, "Slice 6 first decisions", "Durable observers", "Durable's synchronous observers", "Text is plain UTF-8",
-"Failures are watch calls", the "One vocabulary" entries, "The performance and memory bar"; `slice-6a.md` (what 6a
-built); the feature map and the carry-over of `slices.md` (slice 6b); the root `AGENTS.md` "Chord v2 Code Style".
+The single source for building 6b, together with `design.md` read in full; the decision log `decisions.md` (later
+entries win), in particular "The store", S1, S2, S4, S10, S11, S13, S15, W1, W12, "Slice 6 first decisions", "Durable's
+synchronous observers", "Text is plain UTF-8", "Failures are watch calls", the "One vocabulary" entries, "The performance
+and memory bar", and every "Slice 6b" entry; `slice-6a.md` (what 6a built); the feature map and the carry-over of
+`slices.md` (6b); the root `AGENTS.md` "Chord v2 Code Style".
6b puts Durable on the store (`StoreStorage`), follows Durable's views as Chord sources (`self.follow`), persists jobs,
-and extends the applier to every write kind. After 6b a worker has one store: Chord's objects and Durable's Session
-commit through one queue, and a commit of either is one seq in one outbox.
+and extends the applier to every write kind. A worker then has one store: Chord's objects and Durable's Session commit
+through one queue, and a commit of either is one seq in one outbox.
## What an app can do after this slice
```ts
-// the start code of a session's worker (p3 base code)
+// p3 worker base code
const store = await openStore(sqliteBackend(db), ctx);
-const worker = createHost
({ address: Session({ id }), principal, store, onError });
+const host = createHost
({ address: Session({ id }), principal, store, onError });
+await host.load([{ name: "@p3/core", facet: coreWorkerFacet({ init, store }) }, ...plugins], ctx);
-// the core facet: the harness object opens Durable on the same store
-host.serve(Harness, (_, self) => {
+// the core facet: the harness object opens Durable on the same store, its data under the harness's own type
+host.serve(Harness, (_, self) => { // local, empty params (P3)
const harness = self.open(async (context) =>
Harness.open(await StoreStorage.open(store, Harness.name, context), { models, registry, … }, context),
(h, _reason, context) => h.close(context));
- const view = self.follow((observer, ctx) => harness.value.observeConversation(id, observer, ctx)); // section 2
- return { … };
+ const view = self.follow((observer, ctx) => harness.value.observeConversation(rootId, observer, ctx, { min: 50 }));
+ return { view };
});
-// a persisted job: survives a crash
+// a persisted job: survives a crash, a clean close and its object's retry
const ImportJob = defineJob("notes.import", {
version: 1, args: obj({ path: str() }), result: obj({ count: int() }),
- state: ImportState, persist: { resume: "rerun" },
+ state: ImportState /* persist: true, checkpointWhen */, persist: { resume: "rerun" },
});
const imports = self.job(ImportJob, { run, finish });
-await imports.abort(id, ctx); // stored; Decision needed (D7): the result
+await imports.abort(id, ctx); // "marked" | "terminal", stored
+await ctx.state.change((d) => { d.output = text; }, ctx, { persistence: { kind: "deferred" } });
```
-## 1. `StoreStorage`: Durable's `Storage` on the store
+## 1. Changes to 6a's store
-Durable's `Storage` (types.ts, "Atomic persistence boundary for Session records") implemented over a `ChordStore`, so
-Durable's Session runs unchanged on it and passes Durable's storage conformance suite
-(`createStorageConformance`), across reopen too.
+Every item below changes 6a's code; nothing is deployed, so the schema and the archive format change in place.
-**Where it lives (D1, owner, 2026-10-27: "they are only used if durable is used"):** `StoreStorage` belongs to
-Durable (D9 makes `durableView` a generic Chord primitive, `self.follow`), in its own `src` (Chord v2 is in pi upstream since `93029a0f1`), importing
-`@earendil-works/chord/next` (Chord's `./next` package export comes with them). Durable already depends on Chord and
-Chord imports nothing of Durable (S13); Durable's conformance suite runs in Durable's tests and CI.
+### 1.1 Names and claims (decisions "R2", "owner names and claims")
-### 1.1 Records
+- Tables are `":"` (`"notes.store:edits"`), document owners `""`; no host kind. The applier and
+ import check two name parts.
+- Every owner name is claimed once per store (in memory, for the store's lifetime): an object claims its type when it
+ first declares persisted data; `StoreStorage.open` claims its name; a second claim is refused `invalid/args`; a
+ claim is released by its owner's end (`StoreStorage.close`, the object's end), so a new Session can reopen over the
+ same store.
-Durable's four record tables (conversations, entries, tasks, submissions) and its ID namespace.
+### 1.2 One id allocator (D3)
-**Layout (D2, owner, 2026-10-27: "the whole purpose of this slice"):** declared tables of the store, written as
-ordinary `put` writes: `defineTable`s that `StoreStorage` declares for itself under its owner name (`StoreStorage.open(store, owner, ctx)`,
-the name claimed once per store; SQL names `":"`, e.g. `"p3.harness:entries"`; decision "owner names
-and claims"), each row the record's indexed columns (`id`, `conversation_id`, `kind`, `status`, …) plus
-the whole record in a `json(...)` column. The outbox, export, import and the applier handle them as every table;
-Durable's layout changes later through `evolveTable`. The partial head index becomes an index on `(conversation_id,
-has_head, id)` (index columns are non-`opt`); indexed strings that must stay lossless (`kind`, `requestId`, document
-kinds and keys) keep Durable's JSON encoding in their columns.
+- The store owns one synchronous allocator: `mintId()` takes the next id; the commit step takes its documents' ids from
+ the same allocator when a creation applies (no copy in the `Step`, no overwrite after `COMMIT`); explicit ids in
+ staged writes raise it at staging (`max`); a refused step leaves its ids unused (gaps, S10). Each commit persists
+ the allocator's current value.
+- It starts at 2 (id 1 is Durable's root conversation). It saturates at `Number.MAX_SAFE_INTEGER`: an explicit write of
+ that id is accepted, the persisted value stays `MAX_SAFE_INTEGER`, every later `mintId` rejects "ID space is
+ exhausted", and a Chord creation at the saturated allocator refuses its own commit (1.4).
+- Outbox commits carry the allocator's value after them (`{ seq, nextId, writes }`); the archive manifest's
+ `nextDocumentId` is `nextId`; import and batch apply restore it, so a replica mints above every id it holds.
-- **Writes:** every `StorageWrite` of a table record becomes a `put` of its row (Durable replaces complete records;
- conversations and entries are immutable and written once). `record_ids` is a declared table too (`id`,
- `record_type`), one `put` per new id (decision "Slice 6 first decisions" (3)).
-- **Reads:** `conversation`, `entry` (with its `commitSeq`: a column), `findLatestHeadMarker`, `scanEntries` (both
- orders, the fork chain walked as `SqliteStorage` does), `task`, `scanTasks`, `submission`, `scanSubmissions`,
- `submissionByRequest`, `scanConversations`: queries over the declared tables, Durable's cursors (`{ after, order }`,
- `scan.ts`) kept as they are (built from the last id, not from Chord's table cursors).
-- **Validation in the step** (D4): what `SqliteStorage` checks inside its transaction (`checkGlobalIds`,
- `prepareDocumentActions`, `checkDocumentActions`) runs inside the store's step, which rolls back a refused commit.
+### 1.3 The commit step
-### 1.2 IDs
+- **A staged write returns the writes it recorded:** `StagedWrite.apply(step): Promise`.
+ `StoreStorage` stages one composite write per Durable commit (1.3 of section 2).
+- **Order:** within the step, writes apply in the order staged, except that a staged write that frees a live address
+ (a retirement of another incarnation) is applied before the creation at that address (rule 9's reason, the partial
+ unique index); Chord's own staging keeps 6a's order. The outbox records the writes in applied order; the applier
+ replays them in that order (no re-sort).
+- **Stamps:** a staged write can take values only the step knows (`step.seq`): Durable's `entries.commit_seq`. The
+ outbox records the resolved values.
+- **Adopt in the owner's batch:** the host gives the store its batch when it attaches (`attach({ failed, batch })`);
+ an adapter's fence runs its `adopt` in `batch.within(…)` (joins a running batch, opens one otherwise); Chord's own
+ transactions keep their `batch.commit`. Without a host, `adopt` runs plain.
-**One counter per store (D3, owner, 2026-10-27):** the store's counter (`next_document_id` renamed `next_id`, the
-manifest's `nextDocumentId` renamed `nextId`) numbers every id: `StoreStorage.mintId()` takes from it in memory,
-explicit ids in Durable's writes raise it, Chord's commit step takes its documents' ids from it, and every commit
-persists it as a high-water mark. `record_ids` stays for Durable's "ID n already belongs to entry" check. Durable's ids
-rise in its creation order (its scans and windows rely on it); Chord's ids are gaps between them. Spec reviewers check
-that nothing relies on Chord's document ids rising with seq.
+### 1.4 Errors: Durable's Storage semantics (D4)
-### 1.3 Documents
+- The step records the error its own code threw inside the transaction callback. A rejection of the backend
+ transaction that is that very error is a **refusal**: the facade rolled back (a failed `ROLLBACK` reaches us as
+ another error, an `AggregateError` on Node), the commit rejects, nothing is adopted, the allocator keeps its raised
+ value, the store goes on. Any other rejection (`BEGIN`, `COMMIT`, `ROLLBACK`, a throwing `adopt`) is an unknown
+ outcome: the store stops (`storage_failed`). The Cloudflare facade is checked to keep the same identity; a test
+ injects failures at `BEGIN`, in the body, at `COMMIT` and at `ROLLBACK` on both drivers' facades.
+- **The refusal's error reaches the owner unchanged** (Durable's suite matches its messages); the store classifies it
+ for Chord's owners: an `int` `CHECK` failure (SQLite's extended code `SQLITE_CONSTRAINT_CHECK`) is `invalid/args` and
+ fails only that transaction; every other refusal of a Chord commit (the engine's counted `UPDATE`/`DELETE`, the
+ live-address index, a primary key, the saturated allocator, a backend error in a statement) fails the object live
+ (`object_failed`, `broken`: memory and disk disagree).
+- **6a's owners:** a refused `self.tx` rejects with the error above and the object fails as above; a refused
+ migration commit (a chunk, (A) or (Z)) fails the activation `broken`, the table stays `migrating`, committed chunks
+ stay; a refused job start rejects the start; a refused outcome or finish commit fails the object (records and
+ memory disagree); a refused commit that carried held changes (D8) fails the object.
+- **Reads:** a read's callback returns data, never throws for a request: an adapter's request checks (an unknown
+ conversation, a cursor, history a document does not keep) happen after the guarded read returned, from tagged
+ results, so only a backend error fails the store. The store offers a read transaction for reads of several queries
+ that must see one state (S4).
-Durable's documents on the documents engine (S13): address `(namespace "durable", owner, kind, key?)`, owner the
-scope as a string (`session`, `conversation:7`, `task:31`), kind and key JSON-encoded as Durable's indexed strings.
-6a built `latest`; 6b adds what Durable needs:
+### 1.5 The documents engine
-- **`rewindable` records** keep every revision and are not deleted at retirement: `retired_at` is set, revisions
- stay (decision "Writes carry their versions" already defined the archive fields).
-- **Reads at a point:** `findDocument(address, at)`, `document(id, at)` (the base at or below `at` plus the deltas up
- to it, in one read transaction, S4), `scanDocuments(scope, at, kind?)`, with Durable's half-open lifetime
- (`createdAt <= at < retiredAt`); a historical read of a `latest` (or session or task) record is a
- `StorageRequestError` ("does not retain historical content").
-- **`document.copy`** (S11): `StoreStorage` reads the source at its point before the step and enters the step with a
- plain create carrying the base; the conformance rules (source not changed in the copy batch, a matching record) are
- checked there.
-- **Stored version and `deltasSinceBase`** as `StoredDocument` reports them; Durable's records carry no fingerprint:
- the engine's fingerprint column takes `""` for Durable records.
-- **Export** carries every revision of a `rewindable` record, retired ones included, with its seq (format 1's
- fields); import and the applier take them.
+- **Addresses:** `(owner, namespace, kind, key?)`: `owner` the claimed name; `namespace` the adapter's partition
+ (Chord: `chord` for documents, `chord.jobs` and `chord.job-states` for persisted runs; Durable: its scope,
+ `session`, `conversation:`, `task:`); `kind` and `key` as the adapter writes them (Durable JSON-encodes both,
+ as its indexed strings). One live record per address (the partial unique index).
+- **`history` per record (R1):** `none` (Chord: older revisions dropped at a new base; row and revisions deleted at
+ retirement), `latest` (Durable's session, task and latest conversation documents: older revisions dropped; the row
+ kept with `retired_at` at retirement), `rewindable` (nothing dropped; the row kept). `retireDocument` reads the record
+ and applies its rule.
+- **Explicit ids:** a creation may carry its id (Durable mints ahead); otherwise the allocator gives one.
+- **Changes without a known revision:** an adapter whose writes carry no revision (Durable) stages a change that reads
+ the record in the step and uses its revision; the counted conditional `UPDATE` stays the guard.
+- **`meta`:** Durable's `fork` policy and `history` details live in the record's `meta`.
+- **Reads at a point:** `find(address, at)`, `load(id, at)` (the base at or below `at` plus the deltas up to it, its
+ version the base's), `list(owner, namespace, at, kind?, limit, after?)` by id, with the half-open lifetime
+ (`createdAt <= at < retiredAt`); records carry `retiredAt`. New indexes (store schema version 2):
+ `(owner, namespace, kind, key, created_at)`, `(owner, namespace, id)`, revisions `(document_id, kind, seq)`; their
+ cost on Chord's commit gate is measured.
-### 1.4 Commits
+### 1.6 The archive and batch apply
-- `StoreStorage.commit(writes, adopt, ctx)`: validates (1.1), resolves copies (1.3), stages the writes (table `put`s,
- document creates, changes and retirements) and calls `store.commit(staged, { admit: () => true, adopt: (seq) =>
- adopt(seq) }, ctx)`. The store's step runs `adopt` inside `host.batch.commit(...)` when the store has a host, so
- Durable's observers mark Chord sources in the step (W1, "Slice 6 first decisions" (1)); `adopt` never throws
- (Durable's rule).
-- **Durable's seq is the store's seq** (gaps allowed: Chord's commits take seqs too).
-- **Errors:**
+- **Format 1 gains** (nothing deployed): `document.create` carries `history`, `retiredAt?` and `meta`; a
+ `document.revision { id, seq, kind, version, content }` part per kept revision, in seq order after its create; for
+ `none` and `latest` records only the current base and its deltas are exported (as 6a), for `rewindable` every
+ revision, retired records included. Import checks: revisions in seq order, a base first, `seq <= lastSeq`, lifetimes
+ at one address never overlapping (a second live record is still refused), versions per revision; the `end` count
+ includes revisions.
+- **Batch apply** (`applyBatch(store, commits, ctx)`, internal): only on a store with no attached host (no owner's
+ memory to update); `commits[0].seq` follows `lastSeq` (S5); one backend transaction for the batch, every write applied
+ with its commit's seq, `lastSeq` and `nextId` set from the last commit; the applier handles every `StoreWrite` kind
+ (`alter`, `dropIndex`, `update`/`updateWhere`/`delete`/`deleteWhere` with their recorded counts and versions,
+ `document.change`, `document.retire`, `document.revision`), checked against the store's records (a trust boundary);
+ a count that differs from the recorded one stops applying and marks the store diverged (S17): every later
+ `applyBatch` rejects. A test replays a store's outbox into an empty store and compares the exports, manifest
+ included.
-**Errors (D4, owner, 2026-10-27):** the store takes Durable's `Storage` semantics (design §10 "Store errors"): a
-step that fails before `COMMIT` rolls back and rejects that commit only; the store stops only on an unknown outcome
-(`COMMIT` or `ROLLBACK` failed), a throwing `adopt`, or a read's backend error. Owners react as Durable's Session:
-Durable's Session fails (`GuardedStorage`, unchanged); a Chord object whose commit the step refused fails live
-(`object_failed`, `broken`); an `int` `CHECK` failure fails only its transaction (`invalid/args`). `StoreStorage` runs
-Durable's commit checks in the step, as `SqliteStorage` does, so the conformance suite's refused commits leave the
-store usable. 6b changes 6a's step and object failure accordingly.
+## 2. `StoreStorage`: Durable's `Storage` on the store (D1, D2)
-- `mintId`, `close(context)` (closes nothing of the store: the app owns it; later calls reject "closed"), reads after
- a store failure reject as Durable's `GuardedStorage` expects.
+In Durable's `src` (an ordinary pi module), importing `@earendil-works/chord/next` (Chord's `./next` export comes
+with it: the store and the adapter surface below, marked internal for adapters). It passes Durable's storage
+conformance suite on SQLite `:memory:` and across reopen (a wrapper that closes and reopens the store file after
+every commit, Durable's reopen harness), and Durable's Session and harness test files run on it.
-### 1.5 Statements per commit
+### 2.1 Opening and the adapter surface
-A target to measure, as 6a's: an entry append is the `record_ids` put, the entry put, `lastSeq` (plus the outbox); a
-document delta is 6a's three statements. `SqliteStorage` does the same work today (`record_ids`, the row, metadata).
+`StoreStorage.open(store, owner, ctx)`: claims `owner` (1.1); declares its tables through the store's table
+preparation (the path `self.table` takes: created at the newest version or migrated); raises the allocator to
+`max(record_ids.id) + 1` (one index read; covers stores built by import or batch apply); attaches to the store's host
+batch if there is one. Chord exports for it: `StoreOwnerTables` (declare and prepare tables under an owner),
+staged table writes and engine writes, the step reader, the read transaction, `mintId`, the commit with a fence. Its
+`close(context)` releases the claim and refuses every later call ("StoreStorage is closed"); the store stays open (the
+app owns it).
-## 2. `self.follow`: Durable's views as Chord sources (D9, owner, 2026-10-27)
+### 2.2 Tables
-Durable's synchronous observers (`observeConversation`, `observeTaskGraph`, `observeDoc`; decision "Durable's
-synchronous observers") have Chord's watch shapes (`advance(call, ctx)` with a `WatchCall`, `end(end)` with a
-`WatchEnd`, pi `5f312dac4`), so Chord follows them with one generic primitive and Durable needs no adapter:
+Every row carries the record's indexed columns and the whole record as `json(unknown(), { version: 1 })`; indexed
+strings that must stay lossless (`kind`, `request_id`) hold Durable's JSON encoding; index columns are non-`opt`, so
+absent values take sentinels that no encoded value can be (`0` for an absent owner id, ids start at 2; `""` for an
+absent request id, JSON-encoded strings start with `"`).
+
+| Table | Key | Columns | Indexes |
+| --- | --- | --- | --- |
+| `record_ids` | `id` | `record_type str` | |
+| `conversations` | `id` | `owner_conversation_id int`, `owner_task_id int`, `record` | `(owner_conversation_id, id)`, `(owner_task_id, id)` |
+| `entries` | `id` | `conversation_id int`, `has_head int` (0/1), `commit_seq int` (stamped), `record` | `(conversation_id, id)`, `(conversation_id, has_head, id)` |
+| `tasks` | `id` | `conversation_id int`, `kind str`, `status str`, `abort_requested int`, `background int`, `record` | `(status, id)`, `(conversation_id, id)`, `(kind, id)`, `(abort_requested, id)`, `(background, id)` |
+| `submissions` | `id` | `conversation_id int`, `request_id str`, `status str`, `record` | `(conversation_id, request_id)`, `(conversation_id, id)`, `(status, id)` |
+
+### 2.3 Commits
+
+`commit(writes, adopt, ctx)` stages one composite write and calls the store's commit with `{ admit: () => true,
+adopt: (seq) => adopt(seq) }` (adopted in the host's batch, 1.3). Inside the step the composite write does what
+`SqliteStorage` does inside its transaction, with the step's reader: `checkGlobalIds` (an id already claimed by
+another record type, a conversation, entry or document written twice: Durable's messages), the per-document actions
+of `prepareDocumentActions`/`checkDocumentActions` (one content command per document, a delta on its version, one live
+incarnation per address: "already has a current incarnation"), then the table `put`s (`record_ids` only for ids not yet
+claimed by the same type), then per document: its creation (explicit id) or content, then its retirement; a
+create-plus-retire is an empty lifetime (`createdAt = retiredAt = seq`). Copies (`document.copy`) are resolved before
+the step (S11): the source read at its point, checked against the copy's record ("changed in the copy batch",
+kind, key, history, fork), entered as a creation carrying the base. A refused check rejects with Durable's message (a
+refusal, 1.4): the store goes on, Durable's Session fails (`GuardedStorage`).
+
+### 2.4 Reads
+
+`conversation`, `entry` (both overloads, `commitSeq` from its column), `findLatestHeadMarker`, `scanEntries` (both
+orders, the fork chain walked as `SqliteStorage` does), `task`, `scanTasks`, `submission`, `scanSubmissions`,
+`submissionByRequest`, `scanConversations`: queries over the tables, Durable's cursors (`scan.ts`) as they are;
+`findDocument`, `document` (one read transaction), `scanDocuments`: the engine's reads at a point (1.5). Request errors
+(`StorageRequestError`) are thrown after the guarded read (1.4).
+
+### 2.5 Cost
+
+Statements per commit, as the gate measures them: an entry append, a `record_ids` check read, the `record_ids` put,
+the entry put, the meta update (4; `SqliteStorage` 5); a document delta, the record read, the revision insert, the
+counted update, the meta update (about 4-5; `SqliteStorage` about 7). Per `put` one compiled row check and the JSON
+encoding of indexed strings.
+
+## 3. `self.follow`: Durable's views as Chord sources (D9, R4)
+
+`self.follow(attach)` in a factory returns a `Watchable` (a state member, a derive source, an `on` source);
+`attach(observer, ctx)` returns `Promise<{ value, detach } | undefined>` (Durable's `Observation`; `undefined` for an
+absent `observeDoc` address). In 6b it is used inside the object that opened Durable (a native's value); reaching
+Durable from other objects is slice 8's (the harness in a registry entry, carry-over).
+
+- **Activation:** after the natives opened, before the members are checked and derives compute: Chord makes the
+ observer and calls `attach`; calls arriving before it resolves are queued and applied in order on `value`; a rejected
+ or `undefined` attach fails the activation like a failed dependency (`transient`); a failed activation detaches
+ what attached. The attempt's end detaches.
+- **Calls:** a call inside a running batch (Durable's `adopt` on the store) applies the queued calls first, then marks
+ the source in that batch; a call outside any batch (Durable's deferred commits, `#hold`, and the task graph's
+ `blocked` refresh, even on the store; any Durable storage that is not the store) is queued and one batch on the next
+ microtask applies everything queued. A derive over two views of one commit sees both changes in one computation.
+- **Ends (R4):** `failed` fails the object `transient` (its retry reopens Durable and attaches again); `ended` is final:
+ a pending retirement call reaches the source's watches first (Durable's `endAfterDelivery`), then they end `ended`;
+ the member keeps its last value; a new `watch` on an ended follow source opens at that value and ends `ended` at once.
+
+## 4. Persisted jobs (D5-D8, R3)
+
+`persist: { resume: "rerun" | "abandon" }` only in `local` types with empty params (P3, at `self.job`); a persisted job
+needs a state definition with `persist: true` and `checkpointWhen` if it has state, and a JSON-only result (no `bytes()`,
+refused at declaration).
+
+### 4.1 Records
+
+Each unsettled run is a keyed persisted document in namespace `chord.jobs` (kind the job name, key the run id,
+`history: none`); its state is a keyed persisted document in namespace `chord.job-states` (same kind and key), so two
+jobs sharing a state definition never meet. The record:
```ts
-host.serve(ChatView, ({ conversation }, self) => {
- const harness = self.get(Harness, {});
- const view = self.follow((observer, ctx) =>
- harness.value.observeConversation(conversation, observer, ctx, { min: 50 }));
- return { view, title: self.derive([view], ([v]) => v.title) };
-});
+{ version, fingerprint, args, key?, trace?,
+ phase: "pending" | "running" | "stopped" | "finishing" | "blocked",
+ restarts, finishFailures, abort?: true, blocked?: "finish_failed" | "version",
+ outcome?: { kind: "done", result } | { kind: "failed", error: StoredJobError } }
+// StoredJobError: { kind: "threw", error: WireError } | { kind: "aborted", reason } | { kind: "restart_limit", restarts }
```
-- `self.follow(attach)` returns a `Watchable`: a state member, a derive source, an `on` source. `attach(observer,
- ctx)` returns `Promise<{ value, detach }>` (Durable's `Observation`).
-- **Attaching** is that one call during activation: Chord makes the observer, calls `attach`, takes `value` as the
- source's value (calls arriving before the promise resolves are held and applied after it), and the object goes live.
- The attempt's end calls `detach()`. An attach that rejects, or resolves `undefined` (`observeDoc` of an absent
- address), fails the activation like a failed dependency.
-- **Calls:** `advance` records the call. Inside a batch (on the store, Durable's `adopt` runs in the host's batch) the
- source is marked in the step and its frame goes out in that flush; outside a batch (a Durable storage that is not
- the store) the source collects the calls and opens one batch on the next microtask. A derive over two views of one
- Durable commit sees both changes in one computation.
-- **End:** Durable's failures are final: `end({ kind: "failed" })` ends the watches `failed`, `ended` ends them
- `ended`.
+Errors are stored in the wire error format (a `PluginError`'s code, message and details survive); a stored outcome is
+rebuilt as the in-memory one.
-## 3. Persisted jobs
+### 4.2 Transitions
-Design §15 "Persistence": `persist: { resume: "rerun" | "abandon" }`, only in `local` objects with empty params (P3,
-at declaration); a run's record and state document in the owner's store; a recorded outcome resumes only `finish`; an
-abort request is persisted; a restart limit; the job fingerprint stored with every record; the start trace carrier.
+| Event | Writes (one commit unless noted) | After |
+| --- | --- | --- |
+| start | record `pending`, state `initial()` | invoked on the live attempt |
+| reservation (before `run`) | record `running` | `run` invoked (a crash after it counts) |
+| `ctx.state.change` | state; the record's `restarts: 0` only when it was not 0 | |
+| abort (D7) | record `abort: true` (on the line); the signal fires after the commit, outside the batch; the line is released, then `abort` joins `run` | `"marked"` |
+| `run` settles | held state flushed (D8), record `finishing` with the outcome | `finish` on the live attempt |
+| `finish` fails | record `finishFailures + 1`; at 5 `blocked: "finish_failed"` | retried after the backoff, a blocked run on reload |
+| `finish` succeeds | record deleted, state retired (Q9), with what `finish` wrote | the run settles |
+| clean stop (host close, a native restart, R3's seal) | the stopped runs written `stopped`, held state flushed, by the close writer | |
+| activation (once per object, not per retry) | each `running` record: `restarts + 1` (or, at 5, `failed { restart_limit }` and `finishing`, `job_interrupted` reported); `stopped` and `pending`: back to `pending` | maps, keys, active count, `running` rebuilt before anything runs; `rerun` invokes, `abandon` records `failed { aborted, restart }`; an abort-marked run finishes `aborted` without `run`; `finishing` resumes `finish`; `blocked` stays |
-- **Where records live:**
+- **The close writer:** at the object's end, after the drain and before the documents drop, one commit past the seal
+ (Durable's `writeAtClose`): the runs stopped by the end written `stopped`, held state flushed; a run whose `run`
+ outlived the drain is fenced and written `stopped` too; blocked persisted runs keep their records (dropped only by
+ `abort`). In-memory runs keep 6a's rules.
+- **Pinning:** records and state documents of unsettled runs stay in memory (never evicted): `ctx.state.value` and
+ `running` read them synchronously.
+- **Fingerprint:** the job's fingerprint (args, result, state) is stored; a record of the job's version with another
+ fingerprint is `blocked: "version"` (resolved by slice 7).
-**Where records live (D5, owner, 2026-10-27):** each unsettled run's record is a keyed persisted document of its
-object in the engine's namespace `chord.jobs` (owner the object's owner string, kind the job name, key the run id:
-`{ version, fingerprint, args, key?, phase, outcome?, abort?, restarts, trace? }`); Chord sets that namespace, a plugin
-never does (its documents are always `chord`), so nothing can collide. Its state document stays an ordinary document
-of the run. Both change in the transactions that change them today (start, state change, outcome, finish, block,
-drop); `finish` deletes the record and retires the state in its commit (Q9), so only unsettled runs are stored. At the
-object's first activation the records are loaded through the engine's key listing and a load per run (no new read).
+### 4.3 Deferred state (D8)
-- **Resume at activation:** a run recorded `finishing` resumes `finish` only; a run recorded `running` was cut off by
- a crash: `rerun` invokes `run` again with the stored args and state, `abandon` records `failed { aborted, reason:
- "restart" }` and finishes.
-- **Restart limit** (aligned with Durable's `restarts`):
-
-**The same rule as Durable (D6, owner, 2026-10-27: "yes, same rule for chord"):** at activation a run recorded
-`running` was cut off by a crash: its `restarts` rises by one (stored), then it resumes (`rerun` invokes `run` again,
-`abandon` finishes with `failed { aborted, reason: "restart" }`); a clean close (`host.close`, a native restart on
-reload) writes its stopped runs back so the next activation counts nothing (Durable's `writeAtClose`); a
-`ctx.state.change` resets the count (progress); at five in a row the run ends with a new `JobError` `{ kind:
-"restart_limit", restarts }`, its `finish` runs, and `job_interrupted` is reported (Durable's `restart_limit`,
-`task_interrupted`). No `blocked { restart_limit }`: supersedes design §15's parking, as Durable dropped parking in
-#11139.
-
-- **Abort** (owner, 2026-10-27: async and stored in 6b):
-
-**`job.abort(id, ctx): Promise<"marked" | "terminal">` (D7, owner, 2026-10-27), Durable's words and behaviour:**
-a running run gets its abort stored (a rerun after a crash then finishes `aborted` instead of running again) and its
-signal fired, and `abort` resolves `"marked"` once `run` settled (as `abortTask` joins the run); a finishing run is only
-marked (`"marked"`, as Durable's `completing`); a blocked run is dropped at once (`"marked"`, as Durable's orphan); an id
-with no unsettled run resolves `"terminal"` (Chord keeps no settled runs, Q9, so a settled and an unknown id are the
-same; Durable throws for an unknown one).
-
-- **Deferred state persistence:**
-
-**Per change, Durable's shape (D8, owner, 2026-10-27):** `ctx.state.change(fn, ctx, { persistence: { kind:
-"deferred", withinMs? } })` (default 1,000 ms, as Durable's `CommitPersistence`): adopted and published at once,
-stored with the next immediate change of the document, before the outcome is recorded (so `outcome.state` has it), at
-close, or after `withinMs`; a crash loses what is not stored. Only run state documents; other persisted documents get
-it when a real case needs it.
-
-- **Fingerprint:** the job's fingerprint (args, result and state schemas) stored with every record; a record of the
- same version with another fingerprint is `blocked { version }` (slice 7's family; the carry-over's "a job
- fingerprint over args, result and state").
-- **Trace carrier:** the start's carrier (design §29) stored with the record; used in slice 9.
-
-## 4. The applier for batch apply
-
-6a's applier applies an archive's writes; 6b extends it to every `StoreWrite` kind (`alter`, `dropIndex`, `update`,
-`updateWhere`, `delete`, `deleteWhere` with their recorded counts and versions, `document.change`, `document.retire`),
-checked against the store's records (a trust boundary), one code path with import. A test replays a store's outbox
-into an empty store and compares the two stores' exports. Applying a batch with a different count than recorded stops
-applying (S17).
+As Durable's holds: the slot adopts and publishes the change at once (`valueVersion` ahead of what is stored), keeps
+the held ops in a `PendingOps` (a base flag when `checkpointWhen` selected one), and the run's documents stay pinned;
+the next immediate change of the state document stores the held ops and its own as one delta on the stored revision
+(or a base); one foreground timer per object for the earliest deadline (`withinMs`, default 1,000 ms, `Infinity` waits
+for the others; a negative or non-number `withinMs` is `invalid/args`); the outcome's commit and the close writer
+store what is held; a held change of a fenced invocation is still stored; a crash loses it. A deferred change that
+would reset `restarts` is stored at once (the reset must be durable). On an in-memory job the option is accepted and
+does nothing.
## 5. Benchmarks and tests
-- **The gate** (decisions "The commit-step benchmark", "The performance and memory bar"): Durable's own storage
- benchmark (`test/storage.bench.ts`: the write and read cases of `testing/storage-benchmark.ts`) on `StoreStorage`
- against `SqliteStorage`, and Durable's `test/conversation-view-bench.ts` on both: each hot path within about 5%,
- memory (allocation per op, retained after GC) no worse beyond noise; persisted job starts and state changes against
- in-memory ones.
-- **Tests:** Durable's storage conformance suite on `StoreStorage` (memory `:memory:` and across reopen); Durable's
- Session and harness test files that take a storage, run on `StoreStorage`; `self.follow` (one frame per commit, a
- derive over two views, outside the store, early calls held, failure and end, an absent `observeDoc`); persisted jobs (rerun, abandon, a recorded outcome
- resumes `finish`, restart counting and reset, the limit, abort stored, deferred state, fingerprints); the applier
- (every kind, the outbox replay, refused counts); export and import with Durable records and `rewindable` history;
- GC (observers detached with their attempt).
-- **Examples:** `26-durable-on-the-store.ts`, `27-durable-views.ts`, `28-persisted-jobs.ts`.
+- **The gate** ("The performance and memory bar"): Durable's storage benchmark (`test/storage.bench.ts`, every read and
+ write case, 1k and 10k) and `test/conversation-view-bench.ts` on `StoreStorage` against `SqliteStorage`, outbox off
+ (its cost reported separately): each hot path within about 5%, allocation per op and retained memory no worse beyond
+ noise; Chord's 6a commit gate again with the new indexes and the allocator; persisted job start and state change
+ reported against in-memory ones (a disk commit is not held to the in-memory number).
+- **Tests:** the conformance suite (both fixtures); the D4 matrix (`BEGIN`, body, `COMMIT`, `ROLLBACK` failures; a
+ refused Durable commit keeps the store; a refused Chord commit fails its object; a `CHECK` failure only its
+ transaction; a refused migration chunk; a request error inside a multi-query read keeps the store); the allocator (a
+ mint during a running Chord step, saturation across reopen, a refused step's gap, replay restoring `nextId`);
+ create-plus-retire and change-plus-retire through the outbox and the applier; reads at a point on retired `latest`
+ and `rewindable` records; export and import of rewindable history and mixed Chord and Durable data; `applyBatch`
+ (every kind, the replay equality, a diverged count); `self.follow` (one frame per commit, a derive over two views,
+ calls outside a batch on the store, early calls, an absent `observeDoc`, `failed` and `ended`, a retirement before
+ the end); persisted jobs (every transition of 4.2, a crash between reservation and `run`, a crash during the close
+ writer, a seal and retry, restart counting and its reset, the limit, the finish failure count across a crash, abort
+ joined and stored, abort racing an outcome, deferred state merged, flushed at the outcome and at close, pinned past
+ the retention); GC (closed `StoreStorage`, ended follow sources, cancelled abort waiters, deferred timers, settled and
+ dropped runs).
+- **Examples:** `26-durable-on-the-store.ts` (a worker with Durable's Session and a Chord store object on one store,
+ exporting the archive), `27-durable-views.ts`, `28-persisted-jobs.ts`.
## Carry-over taken
-From `slices.md` "Slice 6b": persisted jobs (all of it), `StoreStorage` (ids, fork copies, `adopt` in the batch),
-the Chord source over Durable's views (`self.follow`, the batch on and off the store), the applier for batch apply; `job.abort` async and
-stored (decision of 2026-10-27).
+From `slices.md` "Slice 6b": persisted jobs (all of it, the finish failure count included), `StoreStorage` (ids, fork
+copies, `adopt` in the batch), the Chord source over Durable's views (`self.follow`), the applier for batch apply;
+`job.abort` async and stored.
## Carry-over left
-Slice 7: a job fingerprint mismatch's `blocked { version }` resolution on reload; runs of jobs the code no longer
-declares (with D5's records). Slice 9: the trace carrier's use.
+Slice 7: `blocked: "version"` resolved on reload; runs of jobs the code no longer declares; blocked runs retried on
+reload. Slice 8: Durable's views from other objects (the harness in a registry entry). Slice 9: the trace carrier's
+use.