15  Errors, Retries, Recovery, and Durable State

Part IV · Portability in production

Deterministic recovery, transcript, replay, and Northstar composition tests · historical defects remain revision-scoped

The measurement operation times out. The agent has already announced that it is reading evidence, and the executor may already have processed the request. A retry can restore availability—or duplicate a consequential operation. Recovery starts by asking what is known about the previous attempt, not by choosing a backoff interval.

Recovery is a policy-controlled transition from a failed or interrupted operation to a trustworthy next state. Retry repeats an operation. Reconciliation determines what happened to an operation whose outcome is uncertain. Cancellation stops requested work where cooperation permits; it is not proof that its effects were undone.

15.1 The recovery problem

Chapter 14 admitted a deployment under an explicit task contract. Execution now introduces time and uncertainty. A model request can fail before transmission, after provider acceptance, after a tool executes, or after the client observes partial output. Those cases share an exception mechanism but not a safe next action.

The Northstar comparison is read-oriented, so a repeated measurement read may appear acceptable. Yet it can still consume quota, reveal a query, return a newer representation, or invalidate comparability. If the same tool boundary later publishes an artifact or updates a record, repeating the entire agent run becomes more dangerous. Reliability code must not depend on every tool remaining harmless forever.

The central principle is that recovery preserves commitments. We cannot claim an earlier answer was never shown, assume a timed-out effect did not occur, or grant broader authority because the original provider failed. A safe error category tells a client what class of failure occurred. It does not settle the operation’s history.

15.2 Recovery constraints

Availability competes with load. Retrying may mask transient failure while worsening overload. Multiple independent retry layers multiply calls rather than merely adding them.

Observation is incomplete. A network timeout reports lack of a timely response, not absence of a remote commit. The client and server can disagree temporarily about what happened.

Public errors need less detail than recovery diagnostics. Credentials and payloads must not leak, but discarding every correlation fact also prevents operational diagnosis.

Time belongs to the run. Each model step, tool call, queue wait, and backoff consumes one task deadline. Resetting a timeout on every retry can turn a finite request into an unbounded run.

Reading rule. Before retrying, identify the failure phase, visible commitments, possible effects, remaining budget, and the owner authorized to make the next decision.

15.3 Two commitments: visibility and effects

A run crosses two distinct boundaries. Publishing an output fragment commits the service to a visible attempt history. Executing a side effect commits the system to an external operation, even if its receipt has not arrived.

Admitted → request sent → provisional output → tool proposal
                                              │ policy/approval
                                              ▼
                                      effect submitted
                                              │
                         outcome known ←───────┴──────→ outcome unknown
                              │                              │
                         continue/finish                reconcile first

This is a reasoning diagram, not the reference’s complete state machine. Model and tool phases can interleave, and several read-only calls may be pending together. Its purpose is to prevent an exception from flattening materially different histories into “try again.”

A replacement model attempt should receive a new response identity. It can consume authorized, verified prior results without pretending to be the original stream. If partial output is retained, label it incomplete or abandoned. If the interface permits replacement, disclose that replacement and establish which terminal answer is authoritative. Chapter 7 already defines local terminal consistency; recovery must respect it rather than concatenate two attempts into one plausible answer.

15.4 Safe errors and protected diagnostics

The tagged AdapterError stores provider, code, retryability hint, and optional HTTP status. Its public message is constructed from controlled fields, not the upstream error text. The common loop in src/agent.ts recognizes API call errors with a status, recognizes aborts, and otherwise maps failures to a protocol error. A non-API exception is not automatically classified as transient transport failure.

Tagged mapping Category Consequence
401/403 Authentication, non-retryable Repair authorization/configuration; do not blindly repeat
400/404/422 Invalid request, non-retryable Inspect protocol/model/request binding
408/409/429 Rate limit, retryable hint Coarse grouping, not equivalent HTTP meanings
Other statuses Upstream; retryable for statuses at least 500 Still requires phase/effect policy
Abort signal Aborted, non-retryable Do not restart work merely because it stopped
Other exceptions Protocol, non-retryable Bounded public failure; limited diagnosis

The 408/409/429 grouping is a reference limitation. HTTP 409 denotes conflict with the resource’s current state, not a general rate limit. Status interpretation also depends on the provider and operation (Fielding et al. 2022). The hint must not be treated as a semantic recovery plan.

The adapter error fixture proves that a synthetic sensitive error message does not reach the collected public exception. It does not prove a protected diagnostic service exists. The reference does not retain a rich private failure envelope with selected headers, safe request identity, phase, and structured upstream cause.

A proposed diagnostic split exposes a bounded category and application incident reference to the caller, while a protected store retains only reviewed diagnostic fields. Access, retention, redaction, and audit policy apply to that store. A hash of a sensitive payload is not automatically harmless; predictable values can be guessed. Do not use an unrestricted cause serialization as a shortcut for observability.

15.5 Provider-specific variation: successful HTTP is not successful execution

Anthropic’s “HTTP errors” and “Request ID” sections document SSE error events after HTTP 200, overload as HTTP 529, and support correlation IDs (Anthropic, n.d.-a). Its official SDK documentation describes retries, but this reference uses the AI SDK provider package rather than the official Anthropic SDK client. Do not transfer another client’s retry defaults to this implementation.

In the common reference loop, SDK maxRetries is set to zero. That identifies one retry owner that has been disabled. It does not prove the gateway, HTTP client, application, or user will never retry. Nor does receiving HTTP 200 prove that the stream reached a successful terminal event.

The post-release live HTTP bridge is a separate boundary. Its current completion mapping can represent a length finish or a normal end without an explicit model finish as completed. That limitation must not authorize treating partial generation as a successful recoverable checkpoint. Reducer validation, adapter projection, and HTTP mapping need separate evidence; a sound reducer does not repair an upstream mapper that mislabeled the outcome.

15.6 Cancellation is a cooperation protocol

DOM §3 defines abort signals; §3.3 specifies immediate rejection for an already-aborted signal in promise-returning APIs that use this integration contract (WHATWG, n.d.). The cited commit snapshot fixes the text rechecked on 2026-10-09. This is an API integration obligation, not a claim that arbitrary callbacks obey it. It does not grant JavaScript the ability to roll back arbitrary remote work. A tool that ignores its signal can keep running after the caller stops waiting.

Cancellation has several owners: the UI can stop consuming, the server can abort provider work, and a tool executor can cease cooperative activity. A transport disconnect may require its own server plumbing. A test cancelling a response reader proves that path, not every socket-close path. Keep those proofs distinct.

The tagged ToolPolicyRuntime combines an outer signal with a timeout controller, passes the signal in trusted execution context, and checks outer cancellation before policy hooks. Its fixture verifies pre-aborted cancellation and a timed-out execution with an outcome-unknown classification.

There is an important gap: the runtime does not recheck cancellation after awaiting authorization and approval, before calling execute. The retained book-owned probe aborts the outer signal while a controlled authorization hook waits, then releases that hook. It separately repeats the scenario at approval. Each path still executes once with an already-aborted signal; the machine-readable report labels both assertions known-defect. Reproduction requires an authorized exact-tag checkout, not a provider credential. This is not a provider problem. It is a missing cancellation checkpoint at a local policy boundary.

The timeout is also not a proved deadline over the entire policy pipeline. The timer is created before the hooks, but its promise is raced only against execution after the hooks. A hung authorization hook is not bounded by that execution race; timer rejection during that wait is not cleanly handled by a whole-pipeline race. Avoid inferring a total authorization/approval/execution deadline from the execution-timeout fixture.

A stronger proposed implementation checks cancellation before and after every awaited policy stage, gives each stage a bounded budget, handles every timer rejection, and removes listeners/timers on all terminal paths. Before an effect starts, cancellation can reject admission. After it starts, record what is known about the effect separately from the caller’s cancellation. An abort reason can itself contain untrusted or sensitive data and should not be copied into public errors.

15.7 One deadline, one retry owner

Use an end-to-end deadline and recompute remaining time before every queue wait, backoff, model call, and tool execution. A monotonic clock is appropriate for elapsed local budgets; wall-clock timestamps are useful for persisted expiry and diagnostics. Cross-process deadlines need an explicit conversion/skew policy rather than blindly comparing clocks from different machines.

Connection, first-byte, idle-stream, per-tool, and overall run timeouts solve different problems. Progress can justify an idle-timeout reset, but it must not reset the overall deadline. A long stream can remain active while making no useful progress toward the task.

AWS’s Builders’ Library, “Retries and backoff” and “Jitter,” explains layered retry amplification and spreading synchronized retries. Its introduction stresses that timeout does not imply absence of effects (Amazon Web Services, n.d.). These specific passages are recorded in the claim register. These are operational practices, not a normative model API contract.

A bounded policy needs both a total attempt limit and a time budget. Include the initial attempt in the count. Full jitter samples a delay within a bounded exponential window; randomization changes scheduling, not retry safety. Honor a validated upstream minimum wait when appropriate, but stop if it cannot fit the task deadline. RFC 9110 §10.2.3 permits Retry-After as an HTTP date or nonnegative delay in seconds (Fielding et al. 2022). Parsing and clock conversion are separate from the decision below.

15.8 A tested recovery decision, not a hidden retry loop

This implemented and deterministically tested book design takes trusted, normalized phase/effect information and produces a next-action decision. Its complete types are in code/recovery-policy.ts. attempts counts attempts already begun. Times use one caller-defined millisecond clock. retryAfterMs is a validated duration, not a raw HTTP header, and jitter is an injected sample in [0, 1).

export function decideRecovery(context: RecoveryContext): RecoveryDecision {
  for (const value of [
    context.attempts,
    context.maximumAttempts,
    context.minimumAttemptMs,
    context.baseDelayMs,
    context.maximumDelayMs,
  ]) {
    if (!Number.isSafeInteger(value) || value < 1) throw new TypeError("Invalid recovery budget.");
  }
  for (const value of [context.nowMs, context.deadlineMs, context.retryAfterMs ?? 0]) {
    if (!Number.isSafeInteger(value) || value < 0) throw new TypeError("Invalid recovery time.");
  }
  if (!Number.isFinite(context.jitter) || context.jitter < 0 || context.jitter >= 1) {
    throw new TypeError("Invalid jitter sample.");
  }
  // Reconciliation means determine the outcome, not repeat the effect.
  if (context.effects === "unknown") return { action: "reconcile" };
  if (context.cause !== "transient") return { action: "stop", reason: "cause" };
  if (context.outputVisible) return { action: "stop", reason: "visible" };
  if (context.effects === "committed") return { action: "stop", reason: "effects" };
  if (context.attempts >= context.maximumAttempts) return { action: "stop", reason: "budget" };
  const ceiling = Math.min(
    context.maximumDelayMs,
    context.baseDelayMs * 2 ** Math.min(context.attempts - 1, 30),
  );
  const delayMs = Math.max(Math.floor(context.jitter * ceiling), context.retryAfterMs ?? 0);
  if (context.deadlineMs - context.nowMs < delayMs + context.minimumAttemptMs) {
    return { action: "stop", reason: "budget" };
  }
  return { action: "retry", delayMs };
}

Unknown effects take precedence: reconcile means determine the outcome, never repeat it. Even after cancellation, the system may need a separately authorized outcome lookup for an already-submitted operation. This decision does not launch that lookup or authorize background work.

Committed effects prevent restarting the operation. The caller can reuse a verified receipt and continue a later stage under a new plan, but this helper does not implement that continuation. Visible output also prevents an invisible repeat of the same attempt. Only transient failures with no effects or trustworthy evidence of non-application can reach the retry branch. Here, effect knowledge concerns the domain/tool operation, not the absence of provider billing, access logs, or quota consumption. Even an admitted repeat can spend resources; the surrounding run must debit its cost and attempt budgets independently.

The local backoff cap does not shorten the upstream minimum wait. A 600-millisecond upstream wait can exceed a 500-millisecond local window; the deadline decides whether it fits. The minimumAttemptMs reservation is an admission estimate, not an enforced timeout on actual execution. The runner must enforce remaining time, debit attempts, sleep cancellably, and recheck policy after waiting. A pure policy function cannot provide those runtime guarantees by itself.

The scripted Northstar composition uses this decision without pretending to supply a runner. A transient failure before output or tool work permits response-2, explicitly abandoning response-1; already-visible output stops replacement. If a tool has started and its receipt is lost, the result is reconcile. Reopening the transcript leaves that call inert and the execution count at one. Cancellation in this unknown-effect scenario changes the requested work, not the missing receipt.

Recover from evidence about the attempt, not from a status-code lookup alone. Schedule only work that still fits the run’s authority, effect history, visibility contract, and budget.

15.9 Reconciliation examples

Reconciliation is operation-specific. It cannot be implemented as “retry once, then ask the user.” The lookup must use identity owned by the effect system and must itself be authorized.

15.9.1 Lost Northstar measurement receipt

Suppose measurement-a was submitted, the executor count advanced, and the process lost the result before appending it. Reopening the transcript shows a pending call, not proof that no measurement occurred. The next action is:

  1. retain the original operation identity and outcome-unknown state;
  2. perform a separately authorized read-only receipt/snapshot lookup if the measurement service supports one;
  3. append the verified original result without dispatching the measurement again; or
  4. if no lookup exists, report that the run cannot establish a comparable pair and require an explicit new research attempt.

Repeating the read is not automatically equivalent. It can return a newer observation, spend quota, disclose data again, and break the five-second comparability rule. Cancellation after the receipt was lost does not change step 1 into permission to call the measurement tool again.

15.9.2 Presentation artifact handoff interrupted

If presentation_builder completed and returned an artifact ID, but Pi disconnected before saving, the client may fetch that exact unexpired opaque ID and verify its declared byte count and SHA-256. It must not rebuild the deck merely because no local file appeared. If the lease expired or server restart destroyed the store, the system can report that the exact artifact is unavailable. A rebuild is a new authorized operation with a new identity and potentially different bytes.

If the verified target filename already exists, reconciliation also checks local state: equal digest can be treated as already saved; a different digest requires explicit replacement confirmation. “The prior save timed out” is not permission to overwrite.

15.9.3 Consequential publication with unknown remote commit

For an operation that publishes a brief, the effect service should own an idempotency key bound to tenant, principal, normalized intent digest, and artifact digest. After a timeout, query publication status by that key. A verified committed receipt ends the operation; a verified non-application may permit a new attempt under current authorization; an unresolved status remains unresolved. Compensation, such as withdrawing a publication, is a new effect with separate approval.

These examples share one rule: cancellation can stop waiting or prevent work that has not crossed the effect boundary. It cannot convert uncertain history into evidence of non-execution.

15.10 Idempotency is an operation contract

RFC 9110 §9.2.2 advises against automatically retrying non-idempotent methods unless the client knows the semantics are idempotent or knows the original operation was not applied (Fielding et al. 2022). Most generation APIs use POST; the method name alone does not decide whether replaying a particular application operation is safe.

A tool call ID correlates a proposal and result. It is not automatically a business idempotency key. The tagged runtime rejects repeated call IDs within one runtime instance; a fresh request constructs a fresh runtime and seen-ID set. It does not provide cross-request deduplication, crash recovery, or a durable exactly-once effect guarantee.

For a consequential operation, a proposed ledger binds an operation ID to principal/tenant, tool version, normalized intent digest, and recorded status. Reusing a key with a different intent must reject. Claiming a pending operation must be atomic. A verified terminal receipt can be reused; an unknown outcome triggers a lookup against the effect system, not a second blind invocation.

Even a durable local ledger cannot atomically commit a remote effect unless the remote system participates in an appropriate protocol or supports its own idempotency contract. There remains a crash interval between remote commit and local receipt persistence. Design reconciliation for that interval. Compensation is a new authorized operation, not proof that history never happened; cancelling an alert may not retract an alert already observed by a user.

A measurement read is usually less consequential than an update, but two reads at different times can yield different representations. Reuse a snapshot only when the task’s freshness policy permits it. Idempotent external effects and identical model answers are different notions.

15.11 Failure modes

Retry storms. Client, router, SDK, and gateway each retry three times, increasing demand precisely when the service is overloaded. Choose one owner and expose the remaining attempt budget to adjacent layers.

Timeout interpreted as rollback. The caller receives no receipt and marks the operation failed. Preserve outcome-unknown and reconcile rather than repeating the effect.

Cancellation only at entry. A request passes the initial check and waits on policy; later cancellation does not prevent execution. Place checkpoints at each effect-admission boundary and test the intervening awaits.

Partial output laundering. A second model completes a sentence started by another attempt. Preserve distinct identities and do not manufacture a continuous authoritative answer.

Diagnostics as exfiltration. A safe category is followed by a raw payload in an ordinary log. Apply the same data-minimization boundary to every diagnostic path.

15.12 Security considerations

Recovery can amplify both cost and privilege. A retry must not bypass approval, extend a grant beyond expiry, or reuse an authorization decision after its context changed. A reconciliation call must be read-only or separately authorized for its own effects. Persisted approvals need binding to the operation and intent, not just a remembered “yes.”

Do not allow untrusted provider messages to set retry count, deadline, fallback destination, or operator instructions. Provider response headers can influence waiting only after parsing and bounding; they cannot create an unlimited budget. Rate limiting protects the shared service as well as the individual run.

Diagnostic references must not expose another tenant’s incident. Protect support-facing provider request IDs according to policy. Store outcomes and receipts with retention appropriate to their sensitivity; absence of a receipt is not evidence that the effect did not occur.

15.13 Telemetry contract

Proposed fields distinguish failure phase, normalized cause, effect knowledge, output visibility, attempt index, delay class, deadline exhaustion, and reconciliation outcome. Record initial work separately from retry amplification. Unknown outcomes deserve their own counter instead of being buried in generic errors.

A cancellation metric should distinguish requested, locally observed, provider work aborted, and effect outcome resolved. Those are different measurements. Avoid public raw abort reasons and uncontrolled high-cardinality labels. The pure decision exposes neither diagnostic persistence nor emitted metrics.

15.14 How we test portability

Six book tests exercise exponential/jittered delay, local caps, upstream minimum waits, attempt and deadline boundaries, refusal/cancellation/visibility stops, committed effects, unknown-effect reconciliation, and malformed policy inputs. Injected clocks and samples make these tests deterministic without sleeping or calling a provider.

The tagged tool tests prove pre-aborted cancellation, duplicate rejection within one runtime, timeout classification, and selected output/policy failures. The authorization and approval probes establish the missing checkpoints described above; they do not prove all transport cancellation paths or exercise a hung-hook timer. The composition fixture checks new-attempt identity, visible-output stop, and unknown-effect/no-repeat behavior. The reference source remains unchanged; a future repaired release needs different regression assertions and a new immutable identity.

bun test book/test/recovery-policy.test.ts
bun run check

Before stronger production claims, add fault-injection tests around remote-commit/local-receipt gaps, authorization cancellation, timer cleanup, slow readers, hidden gateway retries, and idempotency-key/intent conflicts. These are proposed coverage, not successful runs reported here.

15.15 Portability and evidence ledger

Dimension Evidence label and scope Limitation
Transport Specified: HTTP repeat/wait semantics; DOM abort integration No remote rollback or complete disconnect proof
Provider Implemented: safe categories and SDK retry suppression; synthetic success/failure decoding probed Coarse status mapping; no complete diagnostic envelope
Protocol Inspected limitation: separately pinned bridge completion mapping length/missing finish can be mislabeled; no conformance claim
Feature Tested: book recovery decision; tag timeout/pre-abort; retained post-hook counterexamples No whole-policy deadline, sleeping runner, or repaired cancellation claim
Semantic Locally tested: explicit replacement identity and inert unknown-effect history No repeat authorization or identical regenerated answers implied

Additional recovery concern. A durable receipt/effect ledger and separately authorized reconciliation are proposed, not supplied by the transcript or next-action function.

15.16 Durable state, memory, and replay

Recovery decisions depend on causal history that survives process restart. The former standalone state chapter is consolidated here so the error taxonomy, retry decision, unresolved effects, transcripts, replay, and compaction form one policy rather than two adjacent introductions.

15.16.1 Why recovery needs durable state

The first half of this chapter made recovery depend on the history of visible output and effects. That history must outlive the generator if the system is to survive restart. But “save the conversation” is underspecified. A display transcript can omit an unresolved operation. A provider cursor can become unusable after a deployment change. A model-generated summary can omit the very constraint that made the previous execution safe.

The Northstar task makes the distinction concrete. We may have a user request, a proposed Prototype A lookup, a verified measurement, an unfinished Prototype B lookup, and provisional comparison text. Reloading only the visible messages cannot tell us whether the second lookup was submitted. Reusing only an upstream response ID cannot establish who owns the task or whether the measurements are still fresh.

A portable design separates state by purpose. It can reconstruct a user view without calling tools, build authorized model context without importing provider-private state into another provider, and resume work only after resolving uncertain operations. Those are three different activities, not three names for reading a JSON file.

15.16.2 State constraints

Persistence without authority persistence. Yesterday’s transcript may contain an approved action, but today’s user, policy, or grant can differ. Retention of a record must not silently extend its authority.

History without complete reproducibility. Replaying deterministic application events can reproduce a snapshot. Asking a stochastic model again with the same messages can produce different output, usage, and tool proposals.

Compact context versus faithful evidence. Context windows are finite. Summaries help generation but can lose qualifiers, temporal facts, citations, and causal relations.

Local transactions versus remote effects. A database can atomically append history and advance a revision. It cannot thereby atomically commit an unrelated external tool operation.

Reading rule. For each state object, name its authority domain, retention owner, compatibility key, and the action that consuming it is allowed to trigger.

15.16.3 Five state planes

Plane Purpose Typical owner Must not be mistaken for
Interaction transcript Messages, proposals, results, visible outcomes Application or client session Authorization or remote effect ledger
Run journal/checkpoint Accepted events, pending operations, policy/budgets Orchestrator A complete model conversation
Retrieval memory Selected facts/documents for future context Retrieval service under application policy Instructions or timeless truth
Provider continuation Response lineage or opaque compatible items Provider boundary Cross-provider portable memory
Derived view UI snapshot, summary, cached model context Projection/compaction layer The authoritative retained history

A session is an application identity and ownership boundary connecting some of these planes. It is not identical to a provider response ID, HTTP connection, or current model. A task can span several responses; a session can contain several tasks. Routing changes must not change session ownership.

The portable spine is the application’s record of intent, accepted observations, causal outcomes, and policy-relevant progress. Provider-specific continuation can accompany that spine as an extension. It should not be the only evidence that the application performed its own work.

15.16.4 What the reference actually retains and converts

The tagged ModelInputItem admits user/assistant text, tool calls, and correlated tool results with an optional error flag. The shared modelMessage conversion constructs SDK assistant/tool messages. Non-string successful tool output is JSON-stringified into SDK text; error output uses string conversion. The original typed item and the SDK encoding are therefore distinct representations.

This small contract is sufficient for the tagged common-loop fixture. It is not a complete transcript format for every provider item. It lacks provider-private reasoning content, native citation annotations, assistant phase metadata, media, and a private continuation envelope. Preserving visible text alone cannot preserve fields absent from the contract.

The tagged response reducer and its tests demonstrate deterministic projection: reducing the same valid event fixture again yields the same snapshot; missing terminal events and inconsistent text reject. This is a local event-replay property. It does not establish durable retention, authorized event loading, replay across schema migrations, or re-execution of an external operation.

Pi owns its client sessions. The separately pinned post-release acceptance script exercises synthetic transcript continuation and persistent client session recovery, among other paths. That evidence is not in the basic tagged Pi text acceptance and is not a server-side durable transcript store. Keeping UI sessions in the client is a valid boundary, but a shared service still needs its own authenticated state ownership if it retains runs.

15.16.5 Three meanings of replay

Projection replay consumes accepted events to reconstruct a view. Its transition function should not execute tools or contact the provider. Determinism is relative to the event schema and reducer revision.

Context replay selects recorded messages/items for a new model request. It is an input-construction decision. It may require sanitization, loss accounting, current policy, and provider compatibility checks.

Effect replay reissues an operation. It requires the idempotency/reconciliation contract established earlier in this chapter. A historical tool call is evidence of a proposal, not permission to perform it again.

Recorded history
    ├── deterministic reducer ──→ display snapshot
    ├── authorized context selection ──→ new model request
    └── operation ledger + current policy ──→ continue/reconcile work

No automatic arrow from "read transcript" to "execute tool"

These distinctions matter after failures. A pending call in the transcript could mean proposal-only, submitted-without-receipt, or an incomplete local record. Unless a journal or effect ledger resolves that ambiguity, reloading it must not choose execution as the default. Read-only reconstruction is a safer initial operation than automatic resumption.

15.16.6 Provider-specific variation: lineage and opaque state

OpenAI’s conversation-state guide, rechecked in the 2026-10-08–09 review, distinguishes manual history from “Using the Conversations API” and “Passing context from the previous response” (OpenAI, n.d.-a). The claim register records these sections as mutable documentation, not tests of pinned SDK continuation. These are different ownership and retention modes, not interchangeable identifiers. A response chain’s cursor is not a portable application session, and stateless request construction does not imply that every participant retains no data.

The reasoning guide, “Preserve reasoning across calls,” distinguishes visible history from opaque reasoning items and describes compatible replay in supported contexts (OpenAI, n.d.-b). That is provider-specific continuity. An application must not decode opaque content to invent a chain of thought, convert it into ordinary instructions, or assume that another provider can consume it.

Anthropic’s Messages reference, “Create a Message” and “Body parameters / messages,” describes stateless multi-turn conversations built from supplied messages (Anthropic, n.d.-b). A generic transcript can support that request construction, but content-block and model-specific requirements still belong to its boundary. Do not infer identical continuation behavior from both providers accepting message arrays.

A proposed opaque-state envelope binds payload to issuer/provider, protocol and adapter revisions, deployment/account scope, compatible model class, session/task identity, producing response, and the exact transcript prefix or digest. It records expiry and security classification separately from the payload. A matching compatibility key is necessary but not sufficient: authoritative contracts or reviewed execution tests must establish that this exact state can be consumed. Equal provider/model labels cannot prove validity.

The book does not implement this envelope or replay opaque reasoning. The reference port cannot represent it. On incompatibility, an application may restart from approved neutral context if the task permits that loss. If the task requires continuity that cannot be reconstructed, reject or request an explicit restart; do not silently erase private state and claim equivalent continuation.

15.16.7 A bounded, transactional transcript experiment

A small independently authored, implemented and tested book design demonstrates a narrower property: trusted tenant/principal comparison and atomic append of a limited JSON transcript. It uses Bun 1.3.9’s built-in bun:sqlite, with no new dependency (Bun, n.d.). It is not installed into the reference server.

The complete code/transcript-store.ts defines user/assistant messages and correlated tool calls/results, including an optional result error flag. It rejects system roles and unknown top-level fields, validates causal call/result ordering, caps a session at 1,000 records, and caps a serialized record at 16,384 UTF-8 bytes. Its input must be ordinary JSON-decoded values rather than objects with serialization hooks. Persisted-byte checks do not replace request-body bounds before decoding.

The database contains session ownership/revision and ordered JSON records. revision counts accepted records, so a batch advances it by its length. The following excerpt contains the public read and append operations; database creation, decoder, ownership lookup, and causal validation are in the full tested file.

export function readTranscript(db: Database, id: string, owner: SessionOwner): TranscriptSnapshot {
  return db.transaction(() => ownedSnapshot(db, id, owner))();
}

export function appendTranscript(
  db: Database,
  id: string,
  owner: SessionOwner,
  expectedRevision: number,
  batch: readonly unknown[],
): number {
  if (
    !Number.isSafeInteger(expectedRevision) ||
    expectedRevision < 0 ||
    batch.length === 0 ||
    batch.length > maximumRecords
  )
    return rejectHistory();
  // Input must be ordinary JSON-decoded data, not objects with serialization hooks.
  const payloads = batch.map(encodeItem);
  return db
    .transaction(() => {
      const current = ownedSnapshot(db, id, owner);
      if (current.revision !== expectedRevision) throw new TranscriptError("conflict");
      if (current.revision + batch.length > maximumRecords) return rejectHistory();
      const additions = payloads.map((payload) => decodeItem(JSON.parse(payload)));
      validateHistory([...current.items, ...additions]);
      const insert = db.query("INSERT INTO history VALUES (?, ?, ?)");
      payloads.forEach((payload, index) => {
        insert.run(id, current.revision + index + 1, payload);
      });
      const revision = current.revision + payloads.length;
      db.query("UPDATE sessions SET revision = ? WHERE id = ?").run(revision, id);
      return revision;
    })
    .immediate();
}

Reads use one transaction to obtain ownership, revision, and records from a consistent snapshot. Appends use an immediate transaction: ownership, expected revision, history validity, inserted records, and the new revision are checked or changed in one transaction. SQLite permits only one simultaneous writer; BEGIN IMMEDIATE can return busy when another writer holds the database (SQLite, n.d.-b, n.d.-a). The example does not implement contention retries or a distributed lock service.

An expected-revision mismatch rejects rather than overwriting another writer’s history. The caller must reread and decide whether a new append still belongs to the task; mechanically reapplying a stale batch can duplicate intent. Revision conflict is not a transient failure to be retried without semantic review.

The history validator uses a deliberately conservative profile: multiple pending calls are permitted, but ordinary messages cannot cross an unresolved call; each result must match a pending call’s ID and name, and repeated call IDs reject. This is book policy, not a universal provider protocol rule. An unresolved final call may be persisted as checkpoint data. Reading that checkpoint does not resolve or dispatch it.

In the composition trace, a completed comparison reopens revision 5: one user request plus two calls and two results. The tool execution count remains two. The answer is then computed from the restored results, not an independent copy of fixture data. An unknown-effect branch reopens revision 2—request plus pending call—with the execution count still one. Neither read path infers a receipt or performs resubmission. The trace checks ordinary restart behavior; it does not simulate a power-loss crash.

Ownership comes from a trusted SessionOwner supplied by the caller. The helper compares tenant and principal, but does not authenticate them. Passing user-controlled claims would defeat its authorization boundary. Missing sessions and ownership mismatches share a bounded access error on reads and appends; this is not a claim that every timing or creation path prevents membership inference.

The experiment proves atomic local recording and ordinary close/reopen persistence. It does not prove power-loss durability under every storage configuration, encryption, tamper resistance, backup restoration, multi-process fault tolerance, schema upgrades, or remote effect atomicity. The database is a trusted local store, not a signed evidentiary archive. Public API error sanitization remains an outer responsibility for raw SQLite failures.

15.16.8 Checkpoints are not receipts

Suppose the journal contains a call proposal and the process crashes before a result is appended. There are at least three possibilities: no effect was submitted; it was submitted but not committed; it committed remotely but its receipt was not persisted. A transcript schema cannot distinguish them unless it records additional facts and coordinates with the effect system.

A production checkpoint therefore needs an operation-status plane with intent identity, submitted/known/unknown state, and verified receipts. The transcript can refer to that plane without pretending to replace it. A local transaction spanning two database tables still cannot cover an HTTP effect outside the transaction.

Persist facts once, derive views many times, and resume effects only from explicit operation knowledge. A transcript revision is a consistency boundary, not an authorization token or a remote commit receipt.

After a restart, load owned state, validate schema/reducer compatibility, resolve uncertain operations, re-evaluate freshness and authorization, and only then construct the next model request. These are proposed orchestration stages, not an implemented automatic resumer. New provider output can vary even if neutral context and policy remain the same.

15.16.9 Memory and compaction are provenance-bearing transformations

A memory item can be an observation, extracted source passage, preference, or model-generated summary. Each needs source identity, time, scope, and a policy for reuse. “The model remembered it” does not establish truth, freshness, or permission to use it for another tenant or purpose.

Compaction trades context size for retained meaning. Alternatives include a selected raw window, a structured fact table, a summary, or externally indexed evidence. Each has different loss modes. A summary can omit that Prototype B’s measurement was stale; a last-N-message window can orphan a tool result; a fact table can remove disagreement and source qualifiers.

Preserve the authoritative retained record according to retention policy, and mark summaries as derived. Record input-prefix identity, summarizer/configuration revision, intended scope, and relevant omissions. Test whether hard constraints, unresolved operations, dissenting evidence, and citations survive. Do not elevate a generated summary into system authority merely because it is shorter.

Retrieval memory has its own access control. A globally useful-looking fact can still come from a private document. Query-time ownership checks must precede its insertion into model context, not only its insertion into the index. Chapter 13 deepens this boundary by giving each source and evidence locator a concrete identity.

15.16.10 Failure modes

A cursor treated as a session. The provider ID restores model lineage but not authenticated application ownership or tool receipts.

Replay runs the past. Deserializing a tool-call record invokes the tool again. Keep projection/context construction free of dispatch and require a separate operation plan.

Lost updates. Two clients append to the same transcript prefix and one overwrites the other’s work. Use expected revisions and atomic append; surface conflicts rather than silently merge incompatible actions.

Orphaned causal history. Compaction retains a result without its call or a pending proposal without its unresolved status. Preserve coherent units and test compaction invariants.

A summary becomes a grant. Generated memory says an action was approved and later execution trusts that sentence. Keep authorization in trusted policy/approval records with current validity.

15.16.11 Security considerations

A session identifier is a lookup key, not proof of access. Derive ownership from authenticated application context. Encrypt sensitive retained data when required, restrict file/database access, and prevent cross-tenant retrieval. The local example provides none of that infrastructure; it only demonstrates an ownership comparison given trusted claims.

Opaque provider state can be sensitive even when unreadable. Restrict destinations, retention, logs, and export. Treat unverified state as data, not executable deserialization instructions. Do not migrate state to another account merely because the receiving model has a similar name.

Deletion must account for authoritative history, snapshots, summaries, retrieval indexes, provider-held state, and backups under their respective policies. A derived summary can retain information removed from a transcript. Content digests aid identity but are not anonymization; predictable content can be guessed.

15.16.12 Telemetry contract

Proposed fields distinguish load/read, projection replay, context selection, state incompatibility, revision conflict, unresolved operation, compaction, and fresh execution. Record a safe reducer/schema revision and journal cursor without logging transcript content.

Do not call a successful file read a successful resumed task. Separate “state loaded,” “operation reconciled,” and “task completed.” Track context omissions and compatibility rejection rather than silently discarding state. The local reopen test measures consistency, not an operational resume metric.

15.16.13 How we test portability

Six book tests verify ordinary file close/reopen, detached reads, tenant/principal rejection, stale revision rejection across two connections, causal ordering and pending-call retention, SQL-failure rollback of both records and revision, JSON-profile rejection, and parameterized identifiers. The stale-writer scenario is deterministic sequencing, not a stress test of concurrent processes. A trigger injects failure during the second insert to prove the batch rolls back.

The reference reducer tests separately verify repeated projection equality and terminal consistency. Post-release Pi session acceptance is a client-boundary proof, not this database’s server integration. The composition exercises owned reopen and reconstruction from retained observations, with no dispatch at read. No provider-private continuation or compaction experiment is claimed. Reader-only checks are also available in the archive.

bun test book/test/transcript-store.test.ts
bun run check

For deployment, add interrupted-write, schema-migration, corruption, concurrent-process/busy, backup/restore, revocation, and effect-reconciliation tests under a documented storage configuration. Add model-level tests only when the exact provider continuation contract is exposed and reviewed.

15.16.14 Portability and evidence ledger

Dimension Evidence label and scope Limitation
Transport Not implemented here: remote state transfer Local SQLite reopening proves no wire/storage migration contract
Provider Implemented: neutral reference history conversion Subset, not complete provider continuation
Protocol Documented: provider lineage/opaque-history distinctions No portable envelope or cross-provider state proof
Feature Implemented/tested: owned local append/read; reference projection equality is separate No migrator, compactor, server integration, or durability audit
Semantic Locally tested: restored observations reconstruct the comparison; pending history remains inert No identical model regeneration or automatic resume authority

Additional state/policy concern. Authentication, retention, fresh permission, and durable effect receipts remain outside this trusted-owner store.

15.17 Integrated pattern summary

Problem: A transient-looking failure can hide visible output, completed effects, or unresolved operations whose history must survive restart.

Pattern: Normalize errors, record phase and effect knowledge, propagate cancellation without treating it as rollback, give one owner a bounded retry policy, reconcile unknown outcomes, and retain a portable causal spine whose projection is effect-free.

Result: Recovery, restart, provider change, and compaction preserve commitments without silently repeating work or turning history into authority.

Cost: Durable receipts, operation-specific lookup, storage versions, retention, cancellation checkpoints, and loss-aware compaction are harder than a retry decorator or chat-log save.

15.18 Exercises

  1. Read the retained approval-cancellation counterexample. Specify the regression for a repaired release: after the gate resumes, executor count must remain zero. Explain why changing the historical assertion would falsify evidence.
  2. Specify an idempotency ledger for publishing a synthetic Northstar brief. Identify the remote-commit/local-receipt crash interval and the lookup needed to resolve it.
  3. Add a cancellable runner around the pure policy. Test cancellation during backoff and after effect submission; assert that neither path blindly repeats an uncertain effect.
  4. Extend the transcript experiment with a migration version and reject future schema rather than interpreting it as current history.
  5. Specify a compaction invariant that preserves unresolved calls, conflicting observation times, and current authority boundaries.
  6. Define the user-visible protocol for replacing an incomplete answer with a new attempt. Identify which response is authoritative.

15.19 Further reading

Read RFC 9110’s idempotent-method and Retry-After sections (Fielding et al. 2022), the DOM abort integration requirements (WHATWG, n.d.), and AWS’s retry/backoff analysis (Amazon Web Services, n.d.). Compare Anthropic’s error/request-ID guidance (Anthropic, n.d.-a), OpenAI conversation lineage and reasoning continuity (OpenAI, n.d.-a, n.d.-b), stateless Anthropic Messages construction (Anthropic, n.d.-b), and SQLite transaction/isolation behavior (SQLite, n.d.-b, n.d.-a; Bun, n.d.). Each source owns a different boundary; none grants replay authority.

15.20 Chapter contract

  • Scope: errors, retries, cancellation, reconciliation, durable transcripts, replay, and compaction.
  • Canonical claims: uncertain effects require reconciliation, and persisted history grants no execution authority.
  • Migration inputs: former recovery chapter plus the retired state-memory-and-replay chapter.
  • Evidence gates: crash-window, unknown-effect, ownership, schema-version, and replay tests.
  • Demonstration slots: deterministic Northstar interruption trace.
  • Source-claim ledger: local transaction evidence is not remote-effect evidence.
  • Exclusions: stream-local state belongs to Chapter 7; telemetry implementation belongs to Chapter 16.
Amazon Web Services. n.d. “Timeouts, Retries, and Backoff with Jitter.” Accessed October 8, 2026. https://aws.amazon.com/builders-library/timeouts-retries-and-backoff-with-jitter/.
Anthropic. n.d.-a. “API Errors.” Accessed October 9, 2026. https://platform.claude.com/docs/en/api/errors.
Anthropic. n.d.-b. “Messages API Reference.” Accessed October 9, 2026. https://platform.claude.com/docs/en/api/messages.
Bun. n.d. “SQLite.” Accessed October 8, 2026. https://bun.com/docs/runtime/sqlite.
Fielding, Roy T., Mark Nottingham, and Julian Reschke. 2022. “HTTP Semantics, RFC 9110.” June. https://www.rfc-editor.org/rfc/rfc9110.html.
OpenAI. n.d.-a. “Conversation State.” Accessed October 9, 2026. https://developers.openai.com/api/docs/guides/conversation-state.
OpenAI. n.d.-b. “Reasoning Models.” Accessed October 9, 2026. https://developers.openai.com/api/docs/guides/reasoning.
SQLite. n.d.-a. “Isolation in SQLite.” Accessed October 8, 2026. https://www.sqlite.org/isolation.html.
SQLite. n.d.-b. “Transaction.” Accessed October 8, 2026. https://www.sqlite.org/lang_transaction.html.
WHATWG. n.d. “DOM Standard: Aborting Ongoing Activities.” Accessed October 9, 2026. https://dom.spec.whatwg.org/commit-snapshots/b76da7af5fe35a2a368127cc9ec22449ac8fd559/#aborting-ongoing-activities.