@sema-agent/core
Version:
Stateless, task-oriented AI agent core
3,189 lines • 256 kB
TypeScript
import type { GateOutcome, HostDecisionFacts } from "./gate-outcome.js";
import { type ExecutionOutcomeRecordWord } from "./checkpoint-execution-record.js";
import type { CheckpointPause } from "./pause-registry.js";
import type { SystemInjectionPriority } from "./task-notification.js";
import { type SteerReplayFacet } from "./steer-replay.js";
import type { QuestionAnswer } from "./ask-question.js";
import type { ReadEntry } from "../tools/fs/safety.js";
import type { RepairBundle } from "../agents/repair-loop.js";
import type { WorkspaceHandle } from "./remote-env.js";
import type { SessionPermissionRules } from "./session-policy-store.js";
import type { NestedUsageAccum, PermissionMode, ShellGateDoctrine, TaskResult } from "./types.js";
import type { ActiveWorktreeSession } from "../internal/harness-types.js";
import type { ReopenReason } from "./resume-startup-cause.js";
/**
* design/45 — the **durable-checkpoint** primitive: cross-process / resumable `suspend`/`resume`.
*
* A {@link CheckpointStore} is the third pluggable durable seam (alongside `ToolResultStore` and
* `MemoryStore`): a token-addressable store of "enough state to resume a suspended task", with an
* **atomic CAS `resolve`** so a token is acted on **exactly once** even across replicas / double
* approvals. Two profiles share this one seam (design/43 原语 4, proven by two service instances):
*
* - **F4** (`gate.kind === "human"`) — a running task hit a tool-policy `ask` that no in-process
* approver can answer (headless / long / cross-replica), so it persists a checkpoint and returns
* `status:"suspended"`. Later `runner.resume(token, {gate:"policy_ask", decision})` continues it.
* This is the cross-process version of the synchronous `onAsk` gate (design/37).
* - **1C** (`gate.kind === "task_done"`) — design/38 Path A persists a background sub-task handle here
* so a caller can `get` its outcome across replicas. v1 stores the handle only; the parent does NOT
* mid-task suspend (Path A unchanged) — `runner.resume()` does NOT serve this gate in v1.
*
* **opt-in, default-off**: with no `CheckpointStore` wired (and no durable approval mode requested), a
* policy `ask` still resolves the 1.63 way (synchronous `onAsk` / headless auto-deny) — behavior
* unchanged. Default {@link InMemoryCheckpointStore} is process-only (single instance / tests); a
* durable backend (service's `tidb-approval-store` / `tidb-run-store`) makes it cross-replica.
*
* See `design/45-durable-checkpoint原语-suspend-resume.md`.
*/
/**
* A high-entropy, single-use checkpoint token. Branded so it can't be confused with a `sessionId` /
* `taskId` (council Question #2): the token doubles as the resume capability (token-as-auth, §6), so a
* mix-up would be a security bug, not just a type slip. Mint with {@link mintCheckpointToken}.
*/
export type CheckpointToken = string & {
readonly __brand: "CheckpointToken";
};
/** Mint a CSPRNG 128-bit checkpoint token (token-as-auth, §6/Q5): unguessable, never logged/in-URL. */
export declare function mintCheckpointToken(): CheckpointToken;
/**
* Mint the NON-SECRET stable checkpoint identity — identification and capability are separate
* axes (settledBy/approvalId precedent): `checkpointId` is safe to log, render, and correlate across
* frames (tool_approval cards, replay, resume, snapshots); {@link Checkpoint.token} stays the secret
* resume capability and never rides a display surface. Minted independently (NOT a token digest —
* a digest would make the id derivable only where the token is held, and the projection surfaces
* that need it most are exactly the token-less ones). Legacy rows have no id (absent, additive).
*/
export declare function mintCheckpointId(): string;
/**
* Who resumes a checkpoint, and how the resume `outcome` is interpreted. The `kind` is the discriminant
* that {@link ResumeOutcome} must match at the resume entry (council #3: prevents a `task_done` gate
* being resumed with a `policy_ask` outcome → type confusion / corruption).
*/
/**
* Why a `resource_limit` suspend was minted. design/164 split the single `budget` member: a token
* window and a $ window are different allowances with different owners, and a scheduler reading a
* checkpoint inbox to decide "grant more of what?" could not tell them apart. The reasons match the
* terminal codes the non-suspending tier reports (`limits.max_*_exceeded`), so one vocabulary covers
* both tiers.
*
* **Consumers pinning the old closed set**: `"budget"` is gone — a switch on it now falls through.
*
* design/164 件四/件五 added two more members, and they are a DIFFERENT CLASS from the task axes above:
* `"env_lifetime"` (the execution environment's platform lifetime is about to expire) and
* `"usage_window"` (a deployment-level usage-governance window is exhausted) are EXTERNAL causes — the
* task did not choose them and cannot avoid them by asking for less. They therefore suspend on the
* INFRASTRUCTURE eligibility alone (checkpoint store + non-volatile offload + remote/no owned env),
* without the `TaskSpec.resourceSuspend` opt-in the task axes require. When several causes land on one
* turn boundary the attribution order is
* `preempt > env_lifetime > usage_window > budget_tokens > budget_cost > turns > walltime`
* (external causes above self-chosen allowances; preempt keeps the top seat it always had).
* `"preempt"` is a scheduler's yield (`spec.preemptSignal`), not a limit, riding the same suspend: its resume continues the work.
*
* **Consumers pinning the closed set again**: a `switch` that was exhaustive over the five 5.x members
* now falls through on these two.
*/
export type ResourceLimitReason = "budget_tokens" | "budget_cost" | "walltime" | "turns" | "preempt" | "env_lifetime" | "usage_window";
/**
* design/164 件四/件五 — the EXTERNAL subset of {@link ResourceLimitReason}: causes that come from the
* platform or the operator rather than from the task's own declared allowances. Written as an `Extract`
* of the parent union so the two can never drift into disagreeing about what "external" means, and so a
* seam that may only be reached by an external cause (the infrastructure-only suspend lane) cannot be
* handed a task-axis reason by mistake.
*/
export type PlatformLimitReason = Extract<ResourceLimitReason, "env_lifetime" | "usage_window">;
/**
* design/80 D-2: which SAFETY axis of THIS call caused an `ask` to mint an {@link CheckpointGate}
* `irreversible_ask`. Derived by the gate (`safetyAxisOf`) from the tool's static marks plus the engine's own
* per-call facts — never from a decision member a policy composes (forgeable). Persisted on the gate so a
* network budget/escalation resolver reads WHY this is a safety ask FROM THE DURABLE RECORD. Both can be true.
*/
export interface SafetyAxis {
/** design/70: the tool is egress-marked (`ToolSpec.egress` — an external write: push, open PR, send). */
egress?: boolean;
/** design/77 §4: an irreversibility MANDATE **of the TOOL's own** (its `always`/`maybe` tier); a SHELL SEAT raise is NOT this axis, see {@link shell}. */
irreversible?: boolean;
/** The engine's SHELL SEAT raised this ask. Its question is about TEXT — not about undoing an act — so
* the two axes are mutually exclusive. */
shell?: boolean;
}
/**
* design/80 §D-E: a DETERMINISTIC, REDACTED risk summary attached AT MINT to an escalation
* {@link CheckpointGate} (`human` / `irreversible_ask` — the tool-call approval escalations) so a supervisor
* INBOX can sort/triage by TRUE severity without re-deriving risk. **Display/triage metadata ONLY** — core
* NEVER reads it to gate / budget / suppress anything (structural ask strictly wins via `combinePolicies`;
* no mint suppression reads it). The profile/inbox READS it; core only ATTACHES it. Pure function of the
* call (no clock/random) — same call ⇒ identical descriptor.
*/
export interface RiskDescriptor {
/** ToolEmu-style severity tier 1..5 (5 = most severe). The inbox sorts DESC by this. Deterministic — the
* pure {@link riskSeverity} of {@link axes}. The ORDERING is what matters (the inbox's triage key). */
severity: 1 | 2 | 3 | 4 | 5;
/** Which safety axes tripped — for `irreversible_ask`, derived from the D-2 {@link SafetyAxis} (+ the shell
* seat). `shell` marks a tighten by the engine's shell seat (read-boundary judge and/or `shellGate` doctrine,
* NO explicit per-tool egress/irreversible mark). Empty `{}` for a plain budgetable `human` ask. */
axes: {
egress?: boolean;
irreversible?: boolean;
shell?: boolean;
};
/** The tool whose call is gated (mirrors the gate's `toolName`). */
toolName: string;
/** The operator's shell floor (`"always"`), present ONLY when this ask was minted through the engine's shell
* seat AND a floor was in force. Absent for a shape-tier ask with no floor, and for per-tool safety-axis /
* plain policy asks. Answers "was an operator floor live when this gate fired" from the persisted row alone. */
shellGateDoctrine?: ShellGateDoctrine;
/** A REDACTED, length-capped ONE-LINE summary of the call (the command for a shell gate; a brief key-arg
* digest otherwise) for the inbox preview. NEUTRALIZED via
* {@link import("./permission-rule-model.js").renderUntrustedCommandText} + length-capped (it is
* persisted + shown to a human inbox, so a crafted tool arg carrying a `</system-reminder>` variant /
* newline / huge string must NOT inject into the render or bloat storage — and, since #438, a
* directional/zero-width `\p{Cf}` marker cannot ride into the render either: the display baseline
* STRIPS the format class, so what the line shows is what the args carry, in the order they carry it).
* GENERATIONAL, like every mint-time seat: the summary is minted ONCE at park mint and persisted, so
* rows minted before the #438 render change keep their old bytes (which may carry `\p{Cf}`) forever —
* a consumer must not assume one byte-form across row generations, and the display obligation
* (render through the baseline, or screen) is the same for both. One benign visible consequence on
* new rows: an emoji ZWJ sequence in a command displays decomposed here (raw bytes stay in
* `pendingAction.args`/`toolInput` — this seat is DISPLAY, not identity).
* NEVER raw secrets / full args / env. Deterministic. Optional (omitted when nothing safe to summarize). */
summary?: string;
/** Best-effort file paths the action touches (fs-tool `path` args; shell parsing is deliberately NOT
* attempted — over-reaching a shell parse risks a wrong/forgeable path). Each path rendered through the
* same {@link import("./permission-rule-model.js").renderUntrustedCommandText} baseline as
* {@link summary} (a path name can carry the same reordering hazard as a command) + capped.
* Omitted when none derivable. */
touchedPaths?: string[];
/** The allow-layer members that MATCHED this call and could not clear the ask — one record per member (the member and
* the entries it matched), threaded to the mint so the durable-park route carries the same disclosure the synchronous
* card does: the inbox renders "their rule is alive, just outranked" instead of the person concluding it silently broke.
* Each entry is rendered through the same display baseline as {@link summary} and capped (the bytes may be a person's
* stored text, a skill's or a deployment's list — model-authored or a custom provider's — and can carry `\p{Cf}`; this
* durable seat strips what the synchronous card keeps, the safe direction, and carries no cross-face same-value
* contract, so a byte-pinning consumer reads per face). */
allowRuleShadowed?: readonly import("./allow-rule-shadow.js").AllowRuleShadow[];
/** WHY the reversibility probe did not clear this call, in the probe's own
* words — the cause the tighten acted on, which until now existed only inside the probe and left the
* card showing WHAT is gated with no account of WHY. Present only for a `"maybe"`-tier tighten whose
* probe supplied a cause (an `"always"`-tier tighten runs no probe; a timeout/throw yields no verdict).
* Neutralized + capped at {@link PROBE_REASON_MAX} — it is deployment-authored text on a human trust
* boundary, so it is DISPLAY/TRIAGE metadata only, never adjudication input.
*
* The UNSTRUCTURED face: a deployment's own probe writes prose here and this engine cannot say what it
* means. An engine-minted cause uses {@link probeCause} instead — see that field for why.
*
* RESIDUAL, disclosed (#438): unlike the descriptor-owned display family above, this seat does
* NOT strip the `\p{Cf}` display-reordering class — a directional/zero-width marker in the
* probe's prose survives here byte-for-byte, because the seat carries a documented SAME-VALUE
* contract with its synchronous `AskRequest.probeReason` twin (one string, two faces) and a
* one-face re-render would break it. A consumer rendering this seat owes its own screen (or the
* descriptor family's display baseline). */
probeReason?: string;
/** The STRUCTURED cause, for a probe whose verdict this engine understands —
* the built-in shell probe is the first. Carries a machine-readable {@link ProbeCause.code} and the
* operand families as ARRAYS with honest totals, so a consumer renders its own sentence in its own
* language instead of receiving one this engine had to write in advance.
*
* The prose sibling above is what this replaces for engine-minted causes, and the reason is not
* aesthetic: a sentence asserting what a command DOES is false for the demotion's documented
* over-fire, a sentence asserting what its OPERANDS are is false when the check synthesizes one, and
* serializing a variable-length multi-part disclosure into one length-capped string silently deletes
* whichever part sorts last. A code cannot be false and an array cannot be truncated into a lie. */
probeCause?: ProbeCause;
}
/** One operand family inside a {@link ProbeCause} — the entries a card may NAME, plus how
* many the family really has. `shown.length` ≤ {@link total}, and a consumer that renders only `shown`
* is still obliged to tell the person when `total` exceeds it: the count is the part that cannot be
* cut, and "there is a second unreviewed operand" is exactly what a reader cannot recover from the
* command text. */
export interface ProbeCauseOperands {
/** The named entries. Each `inlineUntrusted`-neutralized and capped at {@link PROBE_CAUSE_PATH_MAX}
* INDIVIDUALLY, so one pathological entry costs only itself. Bounded in count by the minting probe.
* RESIDUAL, disclosed (#438): `inlineUntrusted` does NOT strip the `\p{Cf}` display-reordering
* class the descriptor's own display family strips — a directional/zero-width marker in a path
* survives on this seat (cross-face byte parity with the synchronous `AskRequest.probeCause`
* twin; see that same-value contract for why a one-face re-render is off the table). A consumer
* rendering these entries owes its own screen (or the descriptor family's display baseline). */
readonly shown: readonly string[];
/** How many entries the family has in total — `shown.length` when nothing was elided. A non-negative
* integer; never a lie by omission. */
readonly total: number;
}
/**
* An ENGINE-MINTED, structured account of why a reversibility probe did not clear a call.
*
* DISPLAY/TRIAGE metadata, exactly like the rest of {@link RiskDescriptor} — nothing reads it to gate,
* budget or suppress anything, and it deliberately never reaches the ask's `message` (that string is
* handed to the auto-mode classifier, which may answer an ask with `allow`; a probe must not own an
* input to the decider that clears it).
*
* STRUCTURE IS THE CONTRACT. The entries are model-influenced text, so a consumer must render them as
* DATA — never re-derive structure by splitting or joining them. Structural characters are
* percent-encoded at the mint precisely so that a path cannot spell whatever grammar it is rendered
* into, but the array is the boundary that makes that guarantee cheap to keep.
*/
export interface ProbeCause {
/** Machine-readable cause id, `<domain>.<snake_case>` like this codebase's other notice codes. The
* consumer maps it to its own prose. A code is the one part of a cause that cannot become false as
* the check's edge cases are discovered. */
readonly code: string;
/** The paths the check TOOK as traversal roots — deduplicated and resolved, which is why they are
* described by what the check did with them rather than by what the caller wrote: a lexical operand
* walk has no option-value table (`du -d 2 sub` takes `2`), and it synthesizes the working directory
* when no operand is written at all (`du --help`). */
readonly roots: ProbeCauseOperands;
/** Operands the SAME verdict left unreviewed for a different reason (an unexpanded glob). Present only
* when non-empty. Named separately so a card can say what else this one approval clears — a partial
* account of an approval's reach is worse than none. */
readonly further?: ProbeCauseOperands;
}
/** The bloat ceiling on {@link RiskDescriptor.probeReason} — re-exported from the leaf neutralizer module
* for the same reason its `PROBE_CAUSE_PATH_MAX` sibling is homed there (a value import from tools/fs
* back into this module closes an ESM cycle), so mint, gate and persist read ONE cap. */
export { PROBE_REASON_MAX } from "./untrusted-text.js";
/**
* The ONE validator for a {@link ProbeCause} arriving from a probe.
*
* The value crosses a trust boundary twice — once at the gate (where a deployment-supplied probe hands
* it over) and once at the persist boundary (where {@link buildRiskDescriptor} is an exported function a
* caller may reach directly) — so both call it rather than each writing its own idea of "valid". A
* malformed cause yields `undefined`: the ASK still stands (the verdict was never this member's to
* decide), the card simply carries no account. Fail-closed on the disclosure, never on the gate.
*
* Every field is re-derived rather than trusted: strings are neutralized and capped individually, the
* shown list is clamped, and `total` is coerced to a non-negative integer that cannot understate what is
* shown (a `total` below `shown.length` would turn an honest count into a false reassurance).
*/
export declare function normalizeProbeCause(value: unknown): ProbeCause | undefined;
/**
* design/80 §D-E: the DETERMINISTIC severity tier (1..5) for an escalation checkpoint, a PURE function of the
* tripped {@link RiskDescriptor.axes} (NO LLM, NO clock/random) so the inbox's sort order is stable and
* unit-testable in isolation. The ORDERING is the contract; the absolute numbers map to ToolEmu's 5 tiers:
* - irreversible AND egress → **5** (the most severe — an external, irreversible write)
* - irreversible only → **4**
* - egress only → **3**
* - shell-gated tighten → **3** (a coarse shellGate ask with no explicit egress/irreversible mark — the
* command MAY be benign, so it is not auto-graded as high as a marked tool)
* - plain `human` ask → **2** (no safety axis tripped — a budgetable approval)
* - (1 reserved — least-severe / informational; not expected from these gates.)
* `egress` dominates the shell coarse-grade (an explicitly egress-marked shell tighten is still ≥3). A future
* arm may refine this — but only with a stated reason, and the ORDERING must stay monotone in risk.
*/
export declare function riskSeverity(axes: {
egress?: boolean;
irreversible?: boolean;
shell?: boolean;
}): 1 | 2 | 3 | 4 | 5;
/** design S1e: the char cap for the {@link CheckpointSummary.toolInput} BOUNDED raw preview of a
* `tool_approval` pendingAction's `args` (`JSON.stringify`-ed). Caps the `listByScope` payload size; over-cap
* is truncated with a trailing `…`. Bounded raw (NOT neutralized) — redaction is the consumer's job (echo-only). */
export declare const MAX_TOOL_INPUT_PREVIEW_CHARS = 512;
/**
* HRD-APV-4: the char cap {@link validatePendingSteer} enforces on a parked operator steer — the last
* persisted, model-facing string on a checkpoint row that had none.
*
* Sized as an INSTRUCTION budget, not a preview budget: the siblings above cap echo-only projections at
* 200/512 chars, while a steer is prose a human wrote for the model to act on and is re-delivered on
* every resume of the checkpoint. 16k chars (~4k tokens) holds any realistic operator instruction and
* still bounds the durable row and the per-resume context cost. Exported so a BFF can validate before it
* calls {@link CheckpointStore.setPendingSteer} rather than discovering the reject at persist time.
*/
export declare const MAX_PENDING_STEER_CHARS = 16000;
/**
* design/171 §6.3 — the BYTE budget the whole parked-steer queue must fit inside, measured on its JSON
* serialization (the form every backend actually persists).
*
* 48,000 bytes sits under a MySQL `TEXT` column's 65,535-BYTE ceiling with room for UTF-8 expansion: the
* per-entry cap above counts CHARACTERS, and 16,000 CJK characters serialize to ~48,000 bytes, so a
* char-only cap does not bound the column at all. Both limits are enforced and either one can bind.
*/
export declare const PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES = 48000;
/**
* design/171 §6.3 — the queue's entry-count cap, DERIVED from the byte budget and the per-entry char cap
* (how many maximum-size entries the budget can hold). It bounds the count independently of the byte
* measure so a flood of tiny steers cannot grow the row unboundedly through per-entry overhead alone.
* Exceeding either limit is a fail-loud `steering.queue_full` — never a silent drop of the oldest entry
* (a dropped operator instruction is exactly the failure this queue exists to remove).
*/
export declare const MAX_PENDING_STEER_ENTRIES: number;
/**
* design/171 §6.3 — the FROZEN field set of a persisted {@link PendingSteerEntry}. `validatePendingSteer`
* copies by this set and REFUSES an unknown key, so a producer one release ahead of this worker gets a
* loud rejection instead of having its new field silently dropped on the parked leg. Consumers (other
* repos' backends) read it to assert they round-trip every field rather than hand-listing their own copy.
*/
export declare const PENDING_STEER_FROZEN_FIELDS: readonly ["text", "trusted", "actor", "seq", "inputId", "priority"];
/**
* design/171 §6.1 — WHO a piece of human input came from, as an attribution label. ONE definition:
* the shape lives at its layering-legal home (`engine/llm/types.ts`, where `UserMessage.actor`
* carries it and the engine cannot import core) and this module re-exports it — the live message
* seat and the parked entry can never drift apart. Normative clauses (never authority; ingress-only
* `hostAsserted`) are on the definition.
*/
import type { ActorAssertion } from "../internal/llm.js";
export type { ActorAssertion } from "../internal/llm.js";
/**
* design/171 §5.3/§6.3 — one entry of the parked-steer QUEUE. Replaces the former single seat, whose
* last-writer-wins overwrite silently destroyed an undelivered operator instruction whenever two people
* steered the same suspended run.
*/
export interface PendingSteerEntry {
/** The operator's instruction text (validated exactly like the pre-queue single seat). */
text: string;
/** Frozen at append time from the SERVICE's verified-principal check; never recomputed on resume. */
trusted: boolean;
/** Attribution only (see {@link ActorAssertion}) — absent = the caller supplied none (anonymous). */
actor?: ActorAssertion;
/** Queue-monotonic ordering key, minted by the store on append (first entry = 1). Drain order. */
seq: number;
/** Caller-supplied correlation/idempotency id (a service passes the message id it already minted);
* a uuidv7 is minted when absent. Re-appending the SAME `inputId` is a no-op, which is what keeps
* `setPendingSteer` retry-safe now that it appends instead of overwriting. Because the id lives on
* the PERSISTED entry and every replayed drain of this row carries it verbatim, `(notice code,
* sessionId, detail.inputId)` is the parked family's cross-replay OCCURRENCE key — consumers dedup
* on it directly; no per-delivery key exists or is needed. (Write-path guarantee: a row written by
* a non-conforming store may lack it, and read points do not re-validate.) */
inputId: string;
/** Carried VERBATIM for the serving layer; it does NOT reorder the drain (drain is `seq` order). */
priority?: SystemInjectionPriority;
}
/** design/171 §6.3 — the caller-supplied half of a {@link PendingSteerEntry}: everything except `seq`,
* which only the store may mint. `inputId` is optional on the way in (minted when absent). */
export interface PendingSteerInput {
text: string;
trusted: boolean;
actor?: ActorAssertion;
inputId?: string;
priority?: SystemInjectionPriority;
}
/** True ONLY for a PLAIN data record (not null / array / class-instance / proxy-with-odd-prototype) — the only
* shape we safely digest. Adversarial args (a proxy, a getter that throws) must never crash the mint, so this +
* {@link safeDataValue}/{@link safeKeys} make {@link buildRiskDescriptor} fully TOTAL. */
export declare function isPlainRecord(x: unknown): x is Record<string, unknown>;
/** Read `obj[key]` as a DATA property only — NEVER invokes a getter/accessor (so a throwing getter can't escape
* AND a non-deterministic getter can't vary the digest); a trapped descriptor read ⇒ undefined. */
export declare function safeDataValue(obj: Record<string, unknown>, key: string): unknown;
/**
* design/80 §D-E: build the DETERMINISTIC, REDACTED {@link RiskDescriptor} for an escalation checkpoint at
* MINT. Pure — a function ONLY of (`toolName`, `args`, the D-2 `safety` axis, the `shellGated` flag); it reads
* NO clock/random, so the SAME call ⇒ an IDENTICAL descriptor (pinned by a test).
*
* **Determinism contract = plain-DATA args** (the real flow: model-JSON / hook-rewritten plain objects). A
* Proxy whose `ownKeys` trap returns a DIFFERENT key set per call is OUT OF CONTRACT — JS cannot detect a
* Proxy, so its (display-only) digest may vary. This NEVER affects a security/budget/mint
* decision: `riskDescriptor` is INERT (no core path reads it to gate), so an out-of-contract input can at worst
* degrade an inbox preview, never a permission outcome.
*
* **Redaction is load-bearing** (the `summary`/`touchedPaths` are PERSISTED + surfaced to a human inbox):
* every model-controlled value in the DESCRIPTOR-OWNED display family (`summary` both arms,
* `touchedPaths`, `allowRuleShadowed`) goes through
* {@link import("./permission-rule-model.js").renderUntrustedCommandText} — which STRIPS the `\p{Cf}`
* format class (directional controls / zero-width formatters, the display-reordering hazard the
* `hasBidiControls` bit warns about; #438) and then applies the shared inline form (folds CR/LF/Unicode
* separators to one space, defuses `</system-reminder>` variants + `<<<`/`>>>` fence sentinels) — AND a
* length cap, so a crafted arg carrying a break-out tag / newline / huge string / RLO can neither inject
* into the inbox render nor display as something other than what runs nor bloat the durable row.
* TWO SEATS ARE DELIBERATELY NOT IN THE FAMILY: `probeCause` and `probeReason` carry a documented
* SAME-VALUE contract with their synchronous `AskRequest` twins (their docs say so verbatim), so
* changing their bytes here alone would break that cross-face contract — they keep the shared
* `inlineUntrusted`/`normalizeProbeCause` treatment (a residual their docs disclose). NEVER dumps full
* args / env / secrets — only a bash command string or a short `name=value` digest of the SHOWN args.
*
* `summary` for a shell gate = the `command` string (capped 200 cp); otherwise a `name=value` digest of the
* call's top-level string/number/boolean args (each value capped), omitted when nothing safe to summarize.
* `touchedPaths` reads ONLY the obvious fs `path` arg (read_file/edit_file/write_file) — shell parsing is
* deliberately NOT attempted (over-reaching a shell parse risks a wrong/forgeable path, so OMIT for bash).
*/
export declare function buildRiskDescriptor(input: {
toolName: string;
args: unknown;
/** The D-2 {@link SafetyAxis} threaded to the mint (egress/irreversible), or `undefined` for a plain ask. */
safety?: SafetyAxis;
/** True ONLY when this is a shell call whose tier is the engine's shell seat's (read-boundary judge and/or
* `shellGate` doctrine, no explicit per-tool egress/irreversible mark) — drives the `shell` axis + severity 3. */
shellGated?: boolean;
/** The resolved doctrine to persist when `shellGated` (see {@link RiskDescriptor.shellGateDoctrine}). */
shellGateDoctrine?: ShellGateDoctrine;
/** The matched-but-not-cleared allow-layer members to persist (see {@link RiskDescriptor.allowRuleShadowed}). */
allowRuleShadowed?: readonly import("./allow-rule-shadow.js").AllowRuleShadow[];
/** The reversibility probe's cause for not clearing this call (see
* {@link RiskDescriptor.probeReason}). Re-neutralized here like its shadowed-rule sibling: this
* function is exported, so a caller reaching it directly must get the same redaction the gate applies. */
probeReason?: string;
/** The STRUCTURED cause (see {@link RiskDescriptor.probeCause}). Re-validated here for
* the same reason the prose sibling is re-neutralized — this function is exported, so a caller
* reaching it directly must not be able to persist a shape the gate would have rejected. */
probeCause?: unknown;
}): RiskDescriptor;
/**
* The durable record of an ask's `requiresRealApproval` bit, which used
* to DIE at the park: the mint keyed the gate kind on the static tool marks only, so an org-governed
* ask on an unmarked tool minted a plain `{kind:"human"}` — the one kind a network budget resolver may
* auto-approve — and the org's "only judgment clears this" demand was silently budgetable after the
* durability boundary. Present ⇒ the mint escalates to `irreversible_ask` (non-budgetable) and stamps
* {@link REAL_APPROVAL_CHECKPOINT_VERSION}. `origin` additionally records WHY, for the resume belts:
* · `"org_rule"` — an org ASK rule fired (governance was READABLE at mint). Resume keeps the strict
* posture: org unavailable at resume still refuses (newly-blind ⇒ fail-closed).
* · `"org_unavailable"` — governance could NOT be read at mint and the gate's own message promised
* "every allow tightens to a real approval until it can". A person approving THIS park IS that real
* approval — so a resume that finds org still unavailable executes instead of refusing and burning
* the approval (the park-only livelock #131 closed). The durable row itself — this origin plus the
* recorded human outcome — is the audit record of that passage; the resume emits no extra notice.
* · `"policy"` — a policy/hook minted the bit (e.g. the always-on classifier-parity rule); no org
* semantics, the resume belts treat it as a plain non-budgetable approval.
* Both org origins double as the #120 governed mark: a resuming worker with NO org adjudication wiring
* refuses to redeem such a row (the governed boundary must not vanish with a deployment's wiring).
*/
export interface RealApprovalGateBit {
/** The parked ask's ORIGIN word — a projection of the same closed set the card, the observer and the
* `tool_end` frame carry ({@link import("./ask-origin.js").AskOrigin}), never a second vocabulary. It
* is the ask's ROUTING word: a marked call reads `ancestor_marked` even when the org governs it, because
* the ladder routes a marked call to the park before anything else. Rows minted before the projection
* carried a three-word subset of this set (`org_rule` / `org_unavailable` / `policy`), so every stored
* value is still a member. */
origin: import("./ask-origin.js").AskOrigin;
/** The ORG FACT the ask was minted under (`"rule"` = an org rule asked, `"unavailable"` = org adjudication
* unreachable), kept beside the routing word because routing can outrank the word (a marked call reads
* `ancestor_marked`) while the governance stays. The #120 governed mark and the resume belts read THIS
* member through {@link realApprovalOrgFact} (absent on rows minted before it: the org words imply it). */
org?: "rule" | "unavailable";
}
/** The ONE read of a bit's governed fact (the member; the two org origin words for rows minted before it). */
export declare function realApprovalOrgFact(bit: RealApprovalGateBit | undefined): "rule" | "unavailable" | undefined;
export type CheckpointGate =
/** F4: a human (or any external authority) must allow/deny a pending tool call. design/80 §D-E:
* carries an OPTIONAL display-only {@link RiskDescriptor} (severity/axes/summary) for the supervisor
* inbox to triage by — INERT (core never reads it to gate/budget).
* Both approval arms carry `approvalCard`: which card the pending call renders under (the call's face's card,
* copied at the park). It is also the row's question mark — `"question"` is the reserved question tool, whose
* deadline voids it instead of denying it (`facetOf`). Absent on a row parked before the member (read as no
* card, never as a question) and on a call that carried no face. */
{
kind: "human";
reason: string;
toolName: string;
approvalCard?: import("./tool-face.js").ToolApprovalCard;
riskDescriptor?: RiskDescriptor;
}
/** design/77 §4 (Gate 4) + design/80 D-2: a PRE-ACTION human approval before a SAFETY-tightened tool runs —
* an IRREVERSIBLE tool (send money/email, file a return) OR an EGRESS tool (push, open PR, send). Same
* approval family as `human` (a pending tool call to allow/deny → suspendRef → `status:"suspended"`, resumed
* with a `policy_ask` outcome). **design/80 D-2 (load-bearing):** minted whenever an `ask` is for a tool
* carrying the egress / irreversibility (`always`/`maybe`) SAFETY MARKS — regardless of how the ask arose
* (a gate tighten OR a policy/hook that already asked; round-1 council fix: keying on the per-call
* `decisionReason` missed the policy-ask case AND was forgeable) — **EVEN WHEN `durableApproval` is wired**
* (`durableApproval` supplies scope/ttl, NOT the kind). The DISJOINT-from-`human` kind is what a
* network budget resolver keys on to NEVER auto-approve a safety ask (a budget may only down-budget a plain
* `human` ask). {@link SafetyAxis} records which axis(es) tightened. NOT a dry-run review. */
| {
kind: "irreversible_ask";
reason: string;
toolName: string;
approvalCard?: import("./tool-face.js").ToolApprovalCard;
safetyAxis?: SafetyAxis;
realApproval?: RealApprovalGateBit;
riskDescriptor?: RiskDescriptor;
}
/** design/74: a resource slice limit (budget/walltime/turns) was reached — suspend (resumable) instead of
* fail. There is NO pending tool to adjudicate; resume just continues the run with the next slice's
* allowance (sized from {@link Checkpoint.resourceLedger}). The matching {@link ResumeOutcome} arm is
* `{ gate: "resource_limit"; decision: "continue" }`.
*
* design/164 件五: `resumeAfterMs` is the RESUME-TIMING hint that pairs with {@link Checkpoint.deadline}
* — how long the driver should wait before resuming, because resuming sooner would only re-suspend.
* **In-scope condition**: present ONLY for `reason: "usage_window"` (the one cause that knows when it
* clears); every other reason omits it, so a consumer must not read its presence as "this is
* resumable". Additive on the existing checkpoint version — a reader that ignores it is unchanged. */
| {
kind: "resource_limit";
reason: ResourceLimitReason;
resumeAfterMs?: number;
}
/** design/76 §2.5 (dry-run / shadow): a POST-PREDICTION REVIEW pause. A profile's dry-run interception ran a
* predicted action and produced a buffered state-diff a human (or judge) must REVIEW before it is applied —
* so the task suspends to the durable `needs_review` TERMINAL (`TaskStatus:"needs_review"`) instead of
* finishing. **DISJOINT from the approval family** (`human`/`irreversible_ask`): those are PRE-ACTION
* *approvals* (suspendRef → `status:"suspended"`, resumed with a `policy_ask` outcome); this is a
* POST-prediction *review* (reviewRef → `status:"needs_review"`, resumed with a `dry_run_review` outcome).
* The split is load-bearing — reusing the approval family would make assemble-result report `"suspended"`
* and the `needs_review`/`review.pending` branch dead code (v4 MAJOR-A). It is ALSO disjoint from the
* `RepairTerminal.needs_human_oracle` (design/78) — a different type space; never cross-use the two.
* The state-diff itself is a PROFILE concern (a REF, not stored here — design/76 §9 / §2.5); core only
* owns the gate/status/discriminant plumbing. NOT a pre-action approval. */
| {
kind: "needs_review";
reason: string;
}
/** design/80 D-B (plan-gate): a PRE-ACTION human PLAN REVIEW. Before an agent acts on a high-blast-radius
* plan (a profile's plan-gate fires on shell⇒gate + an irreversible/egress/finance mark present + a
* blast-radius trigger — NOT every task), the run pauses so a human can **approve / edit / reject the
* proposed PLAN** before any step runs. It is its OWN gate kind (r2 ruling — do NOT reuse `needs_review`,
* whose gateMatch arm forces a `dry_run_review` outcome; reusing it would make a plan resume a
* type-confusion mismatch), but it shares the **review-pause** machinery: it routes to `reviewRef` +
* `status:"needs_review"` (a human-review pause), resumed with a `plan_review` outcome.
*
* **THREE-WAY SPLIT (load-bearing):**
* - the APPROVAL family (`human`/`irreversible_ask`) → PRE-ACTION *approvals* of a pending TOOL CALL
* (suspendRef → `status:"suspended"`, resumed with a `policy_ask` outcome bound to a tool call);
* - `needs_review` → a POST-prediction *review* of a buffered state-diff (reviewRef →
* `status:"needs_review"`, resumed with a `dry_run_review` outcome);
* - `plan_review` (this) → a PRE-ACTION *review* of a PLAN (reviewRef → `status:"needs_review"`, resumed
* with a `plan_review` outcome). It binds NO tool call — the human reviews the PLAN, not a pending arg —
* so it carries no `tool_approval` pendingAction (its pendingAction is `{kind:"plan_review"}`, with no
* tool fields) and the `policy_ask` decision-action binding machinery never touches it. The plan/diff
* artifact is a PROFILE concern (a REF, not stored here — design/76 §9); core owns the
* gate/status/discriminant plumbing only. NOT a pre-action approval, NOT a dry-run review. */
| {
kind: "plan_review";
reason: string;
}
/** 1C: the checkpoint tracks a background sub-task's completion (design/38 Path A). v1 = handle only. */
| {
kind: "task_done";
};
/**
* The outcome a caller supplies to `runner.resume(token, outcome)` — a **discriminated union** keyed by
* `gate`, validated against `checkpoint.gate.kind` at the entry (council #3). **v1 implements only the
* `policy_ask` arm**: `task_done` is a dead branch under 1C Path A (the caller orchestrates, never
* mid-task `resume()`), typed here for completeness and the v2 join-suspend option.
*/
export type ResumeOutcome = {
gate: "policy_ask";
/**
* design/80 D-1 (decision-action binding): the `toolCallId` the human actually saw/approved. The
* resume is REJECTED (`checkpoint.invalid_outcome`, fail-closed, pre-CAS) unless it matches the
* checkpoint's pending tool call (`pendingAction.toolCallId`) — this closes the TOCTOU re-suspend
* wrong-apply: a stale `yes` minted against pending call X must not resolve a DIFFERENT pending call
* Y (approve vendor-A $5k applied to vendor-B). The decision must name the action it bound to; the
* runner never trusts that the caller matched the correct pending call (design/77 doctrine —
* deterministic structural backstop, not a model self-report).
*
* The correct value is the resumed checkpoint's `pendingAction.toolCallId`; the caller has it via the
* `suspendRef` / pending record it is answering.
*/
boundCallId: string;
/**
* design/80 D-1 §2 (slice 1a.2): the **server-minted opaque** hash of the pending tool call's input
* that the human actually saw/approved (`PendingAction.tool_approval.boundInputHash`, computed by the
* engine at suspend-mint via {@link import("./canonical-json.js").boundInputHashOf} over the post-hook
* `args`). The caller echoes it VERBATIM — it is opaque; the SDK/service NEVER re-canonicalize args
* (a second runtime's serialization could diverge → false mismatch → fail-closed a
* legitimate approval). The resume verifies it by **string equality** against the checkpoint's
* persisted value (`checkpoint.invalid_outcome`, fail-closed, pre-CAS) — closing the TOCTOU "same call
* id, different input" variant that `boundCallId` alone misses (a re-mint that swapped the input under
* the same tool-call id). It binds the SHOWN input, NOT `updatedInput`: an `allow` edit is the same
* operator's authorized rewrite, applied AFTER this binding check (design/37,末位应用), so the echoed
* hash is always the pending record's value regardless of any edit. (Legacy pre-1a.2 checkpoints have
* no persisted hash → the resume skips this check, binding on `boundCallId` alone; new mints enforce.)
*/
boundInputHash: string;
/** `allow` → execute the pending tool call; `deny` → inject a denial result and continue. */
decision: "allow" | "deny";
/** A re-written arg payload (design/37 policy `allow` rewrite); re-validated on execute. */
updatedInput?: unknown;
/** Model-readable reason attached to a `deny` (else a default is used). */
reason?: string;
/**
* The ANSWER to a CONTENT-ASK (the reserved AskUserQuestion tool), carried by the decide itself
* (ruled 2026-08-04). A content-ask is a question TO the user: `allow` alone says "let the tool
* run" but supplies nothing to run it AGAINST, and the only pre-existing answer seat was the
* resumed leg's `onQuestion` config closure — a seat an OFFLINE background child has no operator
* on, because its resume config is rebuilt from the parked row rather than handed in. With the
* answer on the decision payload, the same `/decide`-shaped call serves both arms: the resume
* binds it as the resumed leg's answering face, so the pending call executes against the SAME
* tool, the SAME `selected ⊆ options` fence and the SAME untrusted free-text framing (design/80
* D-F) as a live answer — one support surface, not two.
*
* TRUST: untrusted wire data. It is NEVER read as instructions — the tool splits it into the
* trusted selections (which must be the model's own option labels) and a fenced free-text
* remainder. Off-list `selected` values and `note` are exactly the "Other" free-text path.
*
* ARM-SCOPED, like `updatedInput`/`reason` on this same arm: only a `policy_ask` `allow` whose
* pending call IS the reserved tool may carry one. Supplying it anywhere else is rejected
* pre-CAS (`checkpoint.invalid_outcome`, `detail.field: "answer"`) rather than silently dropped,
* and OMITTING it on a content-ask allow with no live answering face is rejected the same way —
* consuming the approval to hand the model a fabricated "no human available" default is the
* silent-continue this seat exists to end.
*/
answer?: QuestionAnswer;
/**
* The host's DECISION FACTS — who ended this wait: a person (`"person"`, the decide an operator made
* through the deployment's approval channel, with the attribution that channel reports), the
* host's own SLA sweep at the row's deadline (`"sla_timeout"` — FACET A of the deadline contract; only
* a sweep may say it), or the deployment's OWN POLICY (`"policy"` — a machine decided, either way: an
* unattended seat answering by class, no interactive approval surface for this row, a standing posture;
* the durable spelling of the synchronous leg's `AskOutcome.settledBy: "policy"`, so ONE decision reads
* the same on both faces). A person and a policy may each `allow` or `deny`; `"sla_timeout"` names a wait
* that ended with nobody answering and cannot `allow`.
* REQUIRED: a decide that names nobody is not a record of anything that happened, and core does not guess.
*
* The host supplies FACTS, never the settlement WORD: core mints the record
* ({@link import("./gate-outcome.js").GateOutcome} — `human_allowed` / `human_refused` / `policy_allowed`
* / `policy_refused` / `park_sla_expired`, joined with the row's persisted `origin`) at the resume entry, writes it onto the resolved row
* (`resolvedOutcome.gateOutcome`) and projects it onto the resolved call's `tool_end` frame. A caller
* that supplies `gateOutcome` itself is refused pre-CAS.
*
* `approver` — the attribution the approval channel reports for the party that ended the wait. Core
* AUTHENTICATES NOTHING: the identity work is the deployment's (its card, its signature check, its
* console). What core owns is that the claim is well-formed: a plain string, bounded, free of control
* characters, refused pre-CAS (`checkpoint.invalid_outcome`) otherwise — refused rather than dropped,
* because a settlement that silently lost its attributed party reads as unattributed.
*
* SCOPE — the facts ride the settlement, not the persisted winner: `winnerFromOutcome` records what an
* `env_failed` replay must reproduce exactly, and who reported a settlement is not part of that
* authorization; a replay states its own facts.
*/
hostDecision: HostDecisionFacts;
/**
* CORE-MINTED, on the persisted twin only: the settlement record this decide produced. Present on
* the row's `resolvedOutcome` after the resolve, and on the `tool_end` frame of the resolved call.
* Never accepted from a caller (refused pre-CAS) — the host does not mint words.
*/
gateOutcome?: import("./gate-outcome.js").GateOutcome;
}
/** design/74: continue a resource-suspended run with the next slice's allowance. NO decision payload and
* NO budget figure — the allowance is computed from {@link Checkpoint.resourceLedger}, so money never
* reaches the caller or the model. Matches `CheckpointGate.kind === "resource_limit"`. */
| {
gate: "resource_limit";
decision: "continue";
}
/** design/76 §2.5 (dry-run / shadow): resolve a `needs_review` suspend after a human/judge reviewed the
* buffered predicted state-diff. `decision:"approve"` → the profile applies the buffered diff (atomically,
* invalidating `readFileState` on touched paths — design/76 §2.5; the apply itself is PROFILE, not core)
* then continues; `decision:"reject"` → the prediction is discarded and the run continues without it. Kept
* DISJOINT from `policy_ask` (the approval family) so the resume-side discriminant never confuses a
* post-prediction review with a pre-action approval (the third `needs_review`/`dry_run_review` gateMatch arm).
* No money/diff payload reaches the caller here — the diff lives in the profile's REF store (design/76 §9). */
| {
gate: "dry_run_review";
decision: "approve" | "reject";
reason?: string;
}
/** design/80 D-B (plan-gate): resolve a `plan_review` PRE-ACTION plan pause after a human reviewed the
* proposed PLAN. `decision:"approve"` → proceed with the plan as-is; `decision:"edit"` → proceed with the
* human's `editedPlan` (a TYPED sibling of the plan — NEVER a raw tool `updatedInput`; a plan is not a tool
* arg, so this binds NO action and never enters the `policy_ask` boundCallId/boundInputHash machinery);
* `decision:"reject"` → the plan is discarded; a PERSON's refusal stops the model to wait for the user (a `reason` is their direction, fenced; with none the wait stands), a deployment POLICY's is attributed and the model re-plans.
* Kept DISJOINT from `policy_ask` (approval) AND `dry_run_review` (post-prediction review) so the
* resume-side discriminant never confuses the three (the `plan_review`/`plan_review` gateMatch arm). It
* binds no TOOL CALL, but since RB-64 it does record a winner: `winnerFromOutcome` returns the decision
* (plus `editedPlan` as `updatedInput` and the reviewer `reason`) under the synthetic `gate:plan_review`
* marker, so an `env_failed` reopen can replay it. The `boundCallId`/`boundInputHash` TOOL-CALL binding
* is what stays `policy_ask`-only. No plan/diff payload (beyond the human's
* `editedPlan` text) reaches core — the plan artifact lives in the PROFILE's REF store (design/76 §9). */
| {
gate: "plan_review";
decision: "approve" | "edit" | "reject";
editedPlan?: string;
reason?: string;
/** Who ended this wait — the tool-approval arm's member, same shape, closed set, validation and refusals; REQUIRED.
* `reject` is the refusal direction; `"sla_timeout"` is refused here (a plan pause expires, a sweep never resolves it). */
hostDecision: HostDecisionFacts;
/** CORE-MINTED on the persisted twin only (origin `"plan_review"`; a plan pause has no `tool_end`, so the row is its only face); a supplied one is refused pre-CAS. */
gateOutcome?: import("./gate-outcome.js").GateOutcome;
}
/** design/144 §3 (message-wake): wake a PARKED (non-gate) checkpoint and drive it to continue — a pure
* un-park verb, **NOT a gate decision**: it never allows/denies/approves anything. Governance-gate
* purity is enforced at the resume entry (fail-closed, pre-CAS): a checkpoint awaiting ANY pending
* gate decision (`gate.kind` ∈ {human, irreversible_ask, needs_review, plan_review, resource_limit} —
* i.e. the tool_approval / dry_run_review / plan_review / resource_limit decision families) REJECTS a
* wake with `wake.gate_pending`, so wake can never become a side door around an approval. Only a
* non-gate park (e.g. a deployment-minted approval-timeout park / pure suspend) is wakeable.
*
* `message` (optional): operator content delivered to the resumed run — the ATOMIC equivalent of
* `setPendingSteer` + resume in one verb (same `validatePendingSteer` persist-time guards, same
* trusted/untrusted delivery framing as {@link CheckpointState.pendingSteerQueue}). It rides the resume
* continuation's steer tail as its OWN segment, appended AFTER any already-parked queue entry
* (X5: both are delivered — the wake message never displaces a parked, undelivered supervisor steer;
* each segment is framed under its own `trusted` semantics, never one merged trust domain). WITHOUT a
* `message`, the checkpoint must already carry a parked steer — a wake with nothing to
* deliver is rejected (`wake.nothing_to_deliver`, anti spin-wake). Idempotency rides the existing
* resolve CAS: a second wake of the same token loses the CAS (`checkpoint.already_resolved`). */
| {
gate: "wake";
message?: PendingSteerInput;
} | {
gate: "task_done";
result: TaskResult;
};
/**
* The pending action a checkpoint suspends *before* — a **discriminated union** keyed by `kind`. Every
* consumer that reads the tool fields MUST branch on `kind` first: the `resource_limit` arm (design/74)
* has none (a resource slice suspend has no pending tool to resolve).
*
* `tool_approval` (§4): a tool call adjudicated `ask` in durable mode, captured with its **post-hook args**
* (the design/37 rewrite already applied) so resume executes the exact same call without re-running
* PreToolUse hooks (§11 M2). `batchToolCallIds` / `completedCallIds` capture the **mid-batch** position
* (§4.ter): one assistant turn can emit a batch of tool calls; if call #k hits `ask`, calls #1..k-1 already
* executed (in `completedCallIds`) and #k+1..N are still pending. ID-based (a `Set<toolCallId>`), not
* positional — immune to reordering / off-by-one (council Question #1), and it doubles as the reconcile
* suspended-batch discriminant (§15.2 net-add #7) so wake-reconcile never closes a suspended call.
*/
export type PendingAction = {
kind: "tool_approval";
toolCallId: string;
toolName: string;
/** Post-hook (design/37-rewritten) args to execute on `allow`. */
args: unknown;
/** (additive) — the tool's approval display projection of `args`, minted alongside the
* row (see AgentTool.approvalPreview; control-char-sanitized + 16KiB-clamped at mint).
* **UNTRUSTED, ADVISORY**: inboxes must escape contextually and surface `args` alongside.
* NEVER read by the resume path (args + boundInputHash stay the sole execution contract). */
preview?: unknown;
/** #457 ② (additive; no checkpoint-version bump — the `hasBidiControls` precedent below: an
* optional field an older reader ignores and the resume path never reads) — the PARK twin of
* `AskRequest.previewWithheld`: present ⇔ this tool declares an approval preview and the mint
* could not present a usable one, `"oversize"` (produced but past the 16KiB display bound — the
* stub still rides {@link preview}) or `"unavailable"` (threw / no JSON serialization). ABSENT
* covers "no preview declared", "the projection declined" and "it worked" — read presence, never
* absence. A DISCLOSURE for the approval inbox's card, never a verdict: {@link args} and
* {@link boundInputHash} are carried either way and the resume contract is untouched. Judged over
* THIS row's fidelity-projected args snapshot, so it can honestly differ from the synchronous
* face on a shape-changing backend — exactly as {@link hasBidiControls} can. */
previewWithheld?: "oversize" | "unavailable";
/** design/179 §4, re-formed by design/375 §3.1/§3.3 (BREAKING — replaces the retired
* `ruleSuggestions` seat; a pre-375 inbox reading a new row simply finds no offers and renders
* no "don't ask again" option, fail toward asking) — the persistable allow-rule OFFERS this
* parked call could be covered by, minted from the SAME post-hook args the row executes.
* Advisory display metadata for the approval inbox's "stop asking me this" option; never read
* by the resume path, and never a rule by itself (minting one goes through the approval-record
* protocol). Absent when no rule lane is armed, the command is one the lane cannot speak for,
* or the parked ask is one a persisted rule could not clear (mandated marks / a real-approval
* demand / a shadowed matching rule).
* CONTRACT (same as the synchronous `AskRequest.ruleOffers` — see
* {@link import("./permission-rule-model.js").RuleOffer}): at most 2 offers, the whole-string
* EXACT single always index 0 when present, the BATCH offer (choosing it is one yes to ALL of
* its 1..5 per-segment rules at once) always last; selection indices are keyed against this
* order (redemption TICKETS are keyed on the consent record's flat CANDIDATE list instead —
* a chosen offer is redeemed via `redeemRuleBatch`), per-element parse with single-row
* degrade on an unknown `kind` and
* original-index preservation. design/382 B3: a batch offer's members are the
* {@link import("./permission-rule-model.js").RuleOfferBatchMember} discriminated union
* (`command` | `directoryRead`), and the member-level degrade is the union's own normative
* arm — an unknown MEMBER kind drops the WHOLE batch offer (never one member: a conjunction
* silently one member short renders "yes to N" as "yes to N−1"), while the sibling single
* offers stay rendered. The park→resume redemption chain walks the same consent
* protocol as the synchronous card — there is no second form. */
ruleOffers?: readonly import("./permission-rule-model.js").RuleOffer[];
/** #490 修② (additive; no checkpoint-version bump — the `previewWithheld`/`hasBidiControls`
* precedents above: an optional field an older reader ignores and the resume path never reads)
* — the PARK twin of `AskRequest.ruleOffersAbsence`, minted by the SAME one factory as
* {@link ruleOffers} so the parked card and the live card cannot answer differently. Present ⇔
* the rule-offer lane is in play for this call and has nothing to give, naming which door
* closed, one word of {@link import("./ask-origin.js").RULE_OFFERS_ABSENCES} per reason (`"mandated"`:
* an inbox must NOT point at rule-writing nor at a directory grant on this arm — confirming the call
* is the whole of what a person can do about it). Mutually exclusive with {@link ruleOffers}.
* ABSENCE is not a claim — it also covers "there are offers" and the structural doors (no lane
* wired, another tool, a task that cannot hold a rule) — so read presence. See the synchronous
* seat's doc for the full contract. */
ruleOffersAbsence?: import("./ask-origin.js").RuleOffersAbsence;
/** design/382 §2.4 (adversarial-review r3; additive — no checkpoint-version bump, the
* `previewWithheld` precedent: an optional field an older reader ignores and the resume
* path never reads) — the PARK twin of `AskRequest.execCwd`, minted by the SAME factory as
* {@link ruleOffers}: the relative-cd resolution base those offers were minted with (the
* live tracked cwd at park time). An approval inbox reconstructing the authoritative
* consent record threads it as `prepareCardApproval`'s `execCwd`. Present only beside
* {@link ruleOffers} when the run had a tracked cwd; echo-only, never a control input. */
execCwd?: string;
/** #557 (additive; no checkpoint-version bump — the `previewWithheld` precedent: an optional
* field an older reader ignores and the resume path never reads) — the PARK twin of
* `AskRequest.requiresRealApproval`: present (`true`) ⇔ this parked ask demanded real human
* judgment (the gate's `realApproval` bit was minted — the SAME fact, restated where a card
* reader looks for it, so an inbox needs no gate-kind decoding to say "only a person can clear
* this"). Absent otherwise (never `false`). Display metadata: the resume belts keep reading the
* gate's own bit. */
requiresRealApproval?: true;
/** #1079 (additive, no checkpoint-version bump — the {@link requiresRealApproval} precedent) — the PARK
* twin of `AskRequest.mandated`: present (`true`) ⇔ no persisted allow rule and no standing answer may
* retire this parked question; absent otherwise (never `false`; a pre-#1079 row reads absent = unreported).
* Minted from the SAME facts object the row's rule-offer factory reads, through
* {@link import("./ask-origin.js").askMandateMember} (the live card's derivation). Three independent
* facts: {@link requiresRealApproval} (only a person — implies this one), this one (no rule), and
* {@link ruleOffersAbsence} (what the offer lane has to give: silent off its tool and wiring, `"ask_rule_matched"`
* when a person's rule also speaks — never a substitute for this one). Display metadata: no gate, CAS or
* resume path reads it. */
mandated?: true;
/** The PARK twin of `AskRequest.authoredBy`: the deployment's own governance layer minted this parked question — a
* person's under every mode, whatever grant or rule the redeeming host holds. Display / seat metadata. */
authoredBy?: "deployment";
/** The PARK twin of `AskRequest.mandate` (same derivation, same precedent): the table word the bit stands on,
* present only when it is the whole reason. Written only as a member of the closed set. */
mandate?: import("./ask-origin.js").PersistedRuleMandate;
/** The PARK twin of `AskRequest.denialLimitFallback`: present
* ⇔ this parked ask is the classifier DENIAL-LIMIT fallback, carrying the counts that tripped
* the bound. Its `autoDenyAfterMs` is ALWAYS `0` on a row: the window is a fact of the ask's
* route, armed only at a hand-out to a live approver, and nothing counts down on the parked lane
* — a park's expiry is the row's own `deadline`/ttl, the engine arms NO timer off this row, and
* its settlement never reads as the fallback's auto-deny (see `tool_end.autoDenied`). A durable
* card renders the counts and the limit; it has no countdown to render. Echo-only. */
denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
/** (additive) The PARK twin of `AskRequest.classifierNoRuling`, written only as a member of its closed set. Display metadata. */
classifierNoRuling?: import("./auto-mode.js").AutoModeFallbackCause;
/** (additive, same precedent) — the PARK twin of `AskRequest.origin`: WHICH AUTHORITY raised the
* parked ask, one word from the closed {@link import("./ask-origin.js").AskOrigin} set, the same
* word the synchronous card is rendered by (engine-stamped at the gate, never a policy's claim).
* Written only when it is a member of the set; a pre-existing row reads absent (unreported).
* Display metadata — the resume belts keep reading the gate's own bit. */
origin?: import("./ask-origin.js").AskOrigin;
/** (additive, same precedent) — the PARK twin of `AskRequest.askClass`: the parked ask's class, the first member of
* {@link classes}, from the same classification the gate arbitrated it by — so a durable approval seat can answer by
* class exactly as a synchronous `onAsk` does. Written by the park mint only as a well-formed pair with
* {@link classes} (both or neither). A row parked by an engine before this member existed reads ABSENT
* (unclassified), and a resume does not fill it in: a seat answering by class treats absence as a class it will
* not answer. Display / seat metadata — no gate, CAS or resume path reads it. */
askClass?: import("./ask-origin.js").AskClass;
/** The PARK twin of `AskRequest.classes`: every class of the parked ask, in `ASK_CLASSES` order
* (`classes[0] === askClass`). Present exactly when {@link askClass} is. */
classes?: readonly import("./ask-origin.js").AskClass[];
/**
* design/80 D-1 §2 (slice 1a.2): the server-minted **opaque** boundInputHash of {@link args} — a
* SHA-256 (hex) via {@link import("./canonical-json.js").boundInputHashOf}, computed ONCE here at
* suspend-mint and persisted on the row. The operator sees it (surfaced on the pending record) and
* echoes it back as {@link ResumeOutcome} `boundInputHash`; the resume verifies opaque string equality
* against THIS value (never re-serializing the args — see the field's doc). Absent only on a legacy
* pre-1a.2 checkpoint (deserialized without it), in which case the resume skips the hash check.
*/
boundInputHash: string;
/** A parked `Skill` call's binding to its card — the skill's name and its two lists ({@link import("./skill-grant.js").skillGrantDigest}, judged by {@link import("./skill-grant.js").parkedSkillApprovalRefusal}); only that row carries it. */
boundGrantDigest?: string;
/** The value {@link boundGrantDigest} hashes — the card's twin of `AskRequest.boundGrants`, minted by the same factory; written exactly when the digest is. Display metadata (additive, no version bump): the resume judges the digest alone. */
boundGrants?: import("./skill-grant-binding.js").BoundSkillGrants;
/** The PARK twin of `AskRequest.boundGrantAbsence`, minted by the same factory: written exactly when the digest is not. Display metadata. */
boundGrantAbsence?: import("./skill-grant-binding.js").BoundGrantAbsence;
/**
* design/388 §2.5 boundary ② (B18; additive — no checkpoint-version bump, the `previewWithheld`
* precedent: an optional group an older reader ignores): the IDENTITY of the row the park minted
* this action against, read off the leg's tool roster at the suspend station — `capabilityId`
* (engine-minted over {source, peer, name, contractId}), `contractId`, `shapeDigest`,
* `implementationRevision` — plus the roster `digest` of the leg that parked. The resume finds the
* same-name row on ITS leg and compares the FOUR identity members (`judgeParkedToolIdentity`): any
* mismatch is a replacement wearing the name and the resume refuses it
* (`resume.tool_contract_mismatch`, the `tool_unavailable` reopen family). All four ABSENT ⇒ a row
* minted before the members existed ⇒ the name-only path, never widened by their absence.
*/
capabilityId?: string;
contractId?: string;
shapeDigest?: string;
implementationRevision?: string;
rosterDigest?: string;
/**
* design/388 B6 × B18 (additive; the identity group's precedent): the FACE the parked call was judged under —
* display `family` + declared `pathTarget` — off the same roster row at the same mint. The pre-CAS resume-edit
* recheck has no roster yet; it rebuilds the call face from these (`parkedCallFaceOf`) and judges the edit as
* the live gate judged the original (merge-seam H1: without them a caller slot fell to the catalog's slot for
* the NAME). Any member absent ⇒ pre-388 row ⇒ the recheck falls to the catalog, never a widened grant — including #635's resolution members, which a row written before them simply lacks (they then read as their documented defaults).
*/
family?: import("./tool-face.js").ToolFamily;
pathTarget?: import("./tool-face.js").ToolRosterPathTarget;
/** The names the parked call answered to, copied from the face the gate judged it under (`ToolCallFace.names`: the
* model's spelling first, then the wire name and the other aliases that resolve to it; a row minted before the
* spelling led carries the wire name first) — the pre-CAS recheck judges an edit against rules written on any of
* them, as the live gate judged the original, and the resume re-mints its face under the spelling while it still
* resolves to the row's tool. Absent on a row minted before it ⇒ the row's `toolName` alone. */
names?: readonly string[];
/**
* #438 (additive; no checkpoint-version bump — the `reminderMark` precedent: an optional field an
* older reader ignores. Unlike that precedent seat — which resume DOES read, through its
* verify-then-adopt port — nothing reads THIS bit at all, per the echo-only clause below):
* PRESENCE ONLY — the row's execution
* payload carries at least one DIRECTIONAL format control. The durable twin of the synchronous
* {@link import("./tool-policy.js").AskRequest.hasBidiControls} (ONE contract, two faces — that
* field's doc owns the character class, the hazard and the warning-not-verdict posture; everything
* there applies here unchanged). What is stated per face:
*
* - JUDGED AT THE PARK MINT over THIS ROW's materialized payload: the fidelity-projected `args`
* snapshot (the bytes a human approves and a resume executes) plus the row's own `preview`
* projection. The row's bit describes the row's bytes — on a shape-narrowing backend (a Map key
* under a JSON codec) a marker the synchronous face detected can be GONE from the row, and the
* honest bit for the row is then ABSENT: the inbox renders the row's snapshot, and the bit
* agrees with what it renders. The two faces coincide whenever the projection is shape-preserving.
* - `true` or ABSENT, never `false` (absence = "not detected": clean, or the bounded scan did not
* reach it — a `false` would claim a proof the scan does not offer).
* - A DEPLOYMENT-WRITTEN park row (the design/144 convention's deployment half) may carry `true`
* (taken at its word — over-warning is the safe direction, and a writer with row access holds
* the args themselves already) or omit it (the summary projection then runs its own bounded
* backfill scan — see {@link CheckpointSummary.hasBidiControls}). A written `false` is
* out-of-contract and is read as ABSENT everywhere; it never reaches a projection.
* - ECHO-ONLY, like {@link CheckpointSummary.toolInput}: NO resume / gate / CAS /
* `winnerFromOutcome` path reads it — it is a display warning, never a control input.
*/
hasBidiControls?: true;
/** All tool-call ids in the suspending assistant message, in emission order. */
batchToolCallIds: string[];
/** Ids of calls already executed (results in the session) when the suspend fired — #1..k-1. */
completedCallIds: string[];
}
/** design/74: suspended at a resource slice boundary — there is NO pending tool to resolve. Consumers
* that read the tool-approval fields above MUST branch on `kind` first (this arm has none). */
| {
kind: "resource_limit";
reason: ResourceLimitReason;
}
/** design/76 §2.5: paused at a POST-PREDICTION REVIEW (`CheckpointGate.kind === "needs_review"`). There is NO
* pending tool to resolve — a human/judge reviews the buffered predicted state-diff (a PROFILE-held REF, never
* stored here) — so this arm has no fields. Its OWN member: before the pause registry a `needs_review` row
* borrowed another arm's seat, and the gate × pendingAction pairing lived in a comment. */
| {
kind: "review";
}
/** design/80 D-B (r3 — an EXPLICIT arm, NOT a reused `resource_limit` placeholder): paused at a PRE-ACTION
* PLAN REVIEW (`CheckpointGate.kind === "plan_review"`). There is NO pending tool to resolve — the human
* reviews the PLAN, not a tool call — so this arm has no tool fields. Every consumer that reads the
* `tool_approval` fields above MUST branch on `kind` FIRST: a `plan_review` checkpoint must never flow into
* a `resource_limit`- or `tool_approval`-shaped continuation (a contract test pins this). The plan artifact
* is a PROFILE concern (a REF — design/76 §9); core stores no plan here. */
| {
kind: "plan_review";
}
/** design/144 §3 (X2) — the NON-GATE arm: nothing pends a decision. The shape of a deployment-minted
* PARK checkpoint (approval-timeout park / pure suspend; paired with `gate.kind === "task_done"`, the
* non-gate gate arm) and of a 1C `task_done` handle row. It is the ONLY pendingAction kind the `wake`
* resume entry accepts — every other kind above is a pending GATE decision, and the wake gate rejects
* it (`wake.gate_pending`) via an EXHAUSTIVE switch (a future kind added here without classifying it
* there is a compile error, and an unknown kind on a deserialized row fails closed at runtime). Core
* never mints this arm itself (the park convention is the deployment half); the pre-existing doc
* references to "a task_done pendingAction" (`getCheckpointSummary` et al.) name exactly this shape. */
| {
kind: "task_done";
};
/**
* The per-task correctness state a checkpoint must carry so a resumed task runs in the **same state
* space** it suspended in (§4.bis, the jury head must-fix). An **explicit whitelist** of serializable
* correctness fields — never a blind `JSON.stringify(Prepared)`, which would silently corrupt the 9+
* non-serializable runtime objects (`harness`/`session`/`abortController`…) it holds (round-2 BUG#1).
*
* Round-trip fidelity of every field is covered by tests; runtime objects are **forbidden** here.
* `cacheFingerprint` (design/31) is deliberately **excluded** — it is observation-only, so resume just
* suppresses cache-break detection on the first turn rather than persisting it as correctness state.
*/
export interface CheckpointState {
/** design/36: deferred-tool activation set — else resume re-discloses / diverges from history. */
activeTools: string[];
/** 1.41 submit_output: the validated structured output set pre-suspend, else it's lost on resume. */
outputRef?: {
value?: unknown;
set?: boolean;
};
/** design/38: nested sub-agent cumulative cost — else pre-suspend child cost evaporates (§4.bis/Q7). */
nestedStats: NestedUsageAccum;
/** design/44: the hand's read-file state (content hashes), serialized from the hands-toolkit closure
* (NOT part of Prepared — §15.2 net-add #8). Without it a resumed `edit_file` is rejected "not read". */
readFileState?: Array<[string, ReadEntry]>;
/**
* design/199 — the READ-face containment state at suspend (v9,
* {@link FACE_CHECKPOINT_VERSION}): the RESOLVED face plus the NORMALIZED deny-set ADDITIONS in
* force (deployment ∪ task layers; the built-in table is engine code under the RESUMING deployment's
* own tier selection — empty unless it made one — never frozen here). Written ONLY when the posture
* is non-default (face "open", or additions present) — an absent section IS the default posture and
* resumes as roots plus whatever built-in tiers the resuming worker selected. Resume folds STRICTER-WINS: face = roots unless the
* section says open AND the live resolution says open; deny additions = frozen ∪ live (add-only,
* monotone — a resume can never widen what the cut leg could read).
*
* The design's r3/r4 rounds carried a third key (the workspace domain at cut) to evaluate
* domain-scoped deny entries under their frozen domain; the D-4 ruling (2026-08-13) removed the
* scoped tier entirely, leaving that key without a consumer — it is deliberately NOT persisted
* (dead fields in a durable format are the drift surface the liveness gate exists to catch).
*/
readFace?: {
face: "open" | "roots";
denyEntries?: Array<{
pattern: string;
caseSensitive: boolean;
}>;
/** REDUNDANT governance marker: `true` iff the row's gate was minted WITH a
* {@link RealApprovalGateBit}. v8's bit-required invariant was version-keyed and v9 (face)
* outranks v8 in the stamp, so without this a v9 GOVERNED row whose `gate.realApproval` member
* was dropped in storage would pass pre-CAS and be redeemable on an unwired worker — the exact
* #130/#120 corruption/downgrade hole, reopened one version up. The resume pre-CAS ladder
* refuses a v9+ row whose section says `realApproval: true` while the gate carries no
* well-formed bit (same protection level as v8: a coherent rewrite of BOTH sites remains out of
* scope for any in-row invariant). Absent = the row was minted ungoverned. */
realApproval?: true;
};
/**
* The WRITE-face containment state at suspend: the RESOLVED write face, written ONLY when it is
* non-default ("open"). An absent section IS "roots" for every reader — which is why it carries NO
* version stamp: losing it can only make a resume STRICTER. Resume folds STRICTER-WINS, the read seat's
* law: roots unless the section says open AND the live resolution says open. No deny half and no
* governance marker.
*/
writeFace?: {
face: "open" | "roots";
};
/** The grant rows the run grew ({@link import("./skill-grant.js").frozenSkillGrants}); absent = none, damaged = refused pre-CAS. */
skillGrants?: import("./skill-grant.js").SkillGrantRecord[];
/**
* design/319 (A ticket) — the session's reminder provenance mark
* ({@link import("./reminder-mint.js").mintReminderMark}), persisted so a durable resume keeps
* minting the SAME mark its transcript's engine reminders and system-prompt declaration carry
* ("resume reuses" — one declaration, one mark). Additive on the existing checkpoint version —
* a reader that ignores it is unchanged (the :646 precedent; no version bump). Resume goes
* through the VERIFY port ({@link import("./reminder-mint.js").isValidReminderMark}): a missing
* field (pre-319 row) or an unrecognized value-shape (corrupt / foreign / old-scheme after a
* scheme change) RE-MINTS instead of adopting — the declaration stays strict, and the historic
* legs' reminders simply read as data (fail-safe downgrade, design/319 a-2).
*/
reminderMark?: string;
/**
* design/78 Slice-1: the SAFE-tier self-repair loop's durable state (`failureTrace`/`diagnostics`/
* `rejectedHypotheses`/`attemptCount`/`oracleTier` — all JSON/`structuredClone`-safe, no fn/Date). Set ONLY
* when an orthogonal durable suspend (resource/HITL) interleaves a `runRepairLoop` run — the happy path is
* in-memory only. On resume it re-seeds `RepairLoopConfig.resumeBundle` so `attemptCount` advances
* MONOTONICALLY (never reset). `baselinePassTests` is deliberately NOT carried here (grader-computed
* out-of-process — a worker must not be able to shrink the ratchet). Absent for a run with no repair loop.
*/
repairBundle?: RepairBundle;
/**
* design/49 v1.5: the remote workspace's serializable identity (E2B sandbox id / provider / mount path /
* snapshot id). Set ONLY when the suspended task ran with a remote, suspendable {@link WorkspaceHandle}
* (a per-task `executionEnvFactory` env that was `suspendVM()`-paused rather than destroyed). Resume
* rebuilds the env via the factory and `resumeVM(snapshotId)` so the restored workspace matches the
* `readFileState` above. All-string fields → JSON/`structuredClone` round-trips safely (consistent with
* this whitelist's "no runtime objects" rule). Absent for a process-local (non-remote) suspend.
*/
workspaceHandle?: WorkspaceHandle;
/**
* design/171 §5.3 — the BOUNDED, ORDERED queue of parked operator steers for a DURABLY-SUSPENDED task,
* drained in `seq` order on resume with every entry framed under its own trust semantics. Live
* `TaskStream.steer` (runtask.ts) is unreachable while the harness is idle, so a supervisor's guidance is
* parked HERE via {@link CheckpointStore.setPendingSteer} and injected on resume (after the
* resume-continuation prompt). It is GUIDANCE ONLY — never an approval channel (§3 inv #4) and never
* parsed into control state (§3 inv #5); a budget/autonomy/gate-threshold is CONFIG, not steer text.
*
* `trusted` is FROZEN at `setPendingSteer` from the SERVICE's verified-principal check (an operator-role
* check, NOT a client header) and NEVER recomputed on resume (§3 inv #1). A `trusted:false` entry reaches
* the model as a PLAIN user message with NO `<system-reminder>` wrapper (§3 inv #3); a `trusted:true`
* entry MAY ride the reminder. `text` containing `</system-reminder>` is REJECTED at `setPendingSteer`
* (`steering.invalid_content`) so a dirty steer never enters this state (§3 inv #2).
*
* Delivery is BEST-EFFORT, at-most-meaningfully-once: an entry rides EVERY resume of THIS checkpoint
* that runs a turn (a faithful reopen→re-resume re-shows it), is NOT carried onto a re-suspend
* checkpoint (serializeCheckpointState stamps `undefined`), and is DROPPED on a resume that runs no
* turn. A supervisor re-issues `setPendingSteer` if a steer did not land.
*
* Bounds are fail-loud, never lossy: {@link MAX_PENDING_STEER_ENTRIES} entries and
* {@link PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES} serialized bytes, whichever binds first, reject the
* APPEND with `steering.queue_full` rather than evicting an already-accepted instruction.
*
* Absent when nothing is parked. {@link readPendingSteerQueue} is the single read point; a row that
* still carries the retired pre-queue single seat (`pendingSteer`) is refused there
* (`checkpoint.unsupported_version`, `seat_retired`), never folded in.
*/
pendingSteerQueue?: PendingSteerEntry[];
/**
* SR-7 (CC 2.1.198 orphaned-background-task notice: F6c pretty.js:698391-698398, resume leg
* :707384-707399 under the `CLAUDE_CODE_RESUME_INTERRUPTED_TURN` gate — the cloud-worker restart
* leg, exactly sema's durable-resume shape): the background tasks (pending/running, this run's
* owner triple) still ALIVE at suspend. Background processes never survive a suspend (design/103
* §3.7 unconditional dispose + killed receipts), so on resume any snapshot entry NOT
* alive in the resume leg's registry is an ORPHAN — aggregated into ONE CC-verbatim "The container
* was restarted…" reminder appended to the resume continuation (single message: header +
* `- description (task id)` list + re-create instruction; never a per-task barrage). A survivor
* (session-resident shell / in-window monitor) is excluded — still reachable via TaskOutput/
* TaskStop, it needs no obituary. CC's per-orphan `Ku(task_id,"stopped")` status write (:707399)
* is N/A here: the suspend teardown already settled them killed with receipts. Absent for a run
* with no live background tasks at suspend, and for pre-1.262 checkpoints (deserialized without
* the field) — both resume silently, exactly like CC with an empty list.
*/
runningBackgroundTasks?: Array<{
id: string;
description?: string;
}>;
/**
* the hands band's LOGICAL working directory at suspend (`handsCwdRef.current` —
* moved by `cd` and by EnterWorktree). Without it a resume silently reset the task cwd to the task root:
* relative Read/Edit/Write paths and Bash commands then operated somewhere else than the model believes.
* Absent when the task has no tracked cwd (no real shell / read-only hands). The directory itself is
* plain on-disk state that survives a process-local suspend (worktrees included — the tree stays on disk).
*/
handsCwd?: string;
/**
* The hands band's ROOT at suspend (the canonical root its tools resolve a root-relative spelling against — the
* `root` every adjudicated request of the leg carries). Read by ONE seat: the pre-claim recheck of an edited parked
* call, which has no environment yet and judges the edit at the row's coordinates (`handsRoot` + {@link handsCwd}).
* After the claim the resumed leg uses its own live root. Absent on a row written before this field (that recheck then
* reads the root as unknown, as before) and when the task has no hands. An ignorable optional field: an older reader
* that does not know it resumes the row as it always did, so it raises no row format.
*/
handsRoot?: string;
/**
* The home the leg's caller policies installed their `~/` words at (the execution environment's declared home, absolute),
* beside {@link handsRoot}: the pre-claim recheck installs the edit-base policy at the row's root and this home, the way
* the leg did. Absent on a row written before this field, on a hands-less task and on an environment that declares no
* absolute home — a `~/` row then reads unanchored there (never the engine process's home). Ignorable, like `handsRoot`.
*/
handsHome?: string;
/**
* the ACTIVE EnterWorktree session at suspend (worktree.ts keeps it in a shared
* serializable ref, not a closure-only var, precisely so it lands here). Without it a resume LOST the
* session: ExitWorktree became a no-op (the unchanged worktree could never be removed), a second
* EnterWorktree was wrongly accepted, and `handsCwd` pointed into a worktree the tooling no longer
* owned. All-string fields; the worktree directory survives the suspend on disk (managed under the
* task root, `git worktree` metadata intact).
*/
activeWorktree?: ActiveWorktreeSession;
/**
* Parent effective-policy inheritance (tighten-only): the durable projection of the run's
* {@link import("./runner/contracts.js").InheritedGate}. Only the serializable DATA half persists —
* `ancestorRules` (each ancestor's session-rule snapshot: sessionId/principal/rev/rules, re-compiled on
* resume against the resumed leg's env), `shellGate` (the chain's operator floor) and `permissionMode`. The OPAQUE
* half (`parentConstraints` — live ToolPolicy/onAsk closures) CANNOT persist; `requiresParentConstraint`
* records only its PRESENCE, so the resume pre-CAS ladder can fail-loud (`resume.parent_constraint_missing`,
* checkpoint stays `pending`) when the resuming caller did not re-supply the closures via
* `resumeStream(..., internals)`. Absent for a task that inherited nothing (pre-1.294 checkpoints included)
* — distinguishable from `requiresParentConstraint: false` with data-only inheritance, which resumes
* without any re-supplied internals. All-data fields → JSON/`structuredClone` round-trips safely.
*/
inheritedGate?: {
ancestorRules?: ReadonlyArray<{
sessionId: string;
principal?: string;
rev: number;
gen?: number;
rules: SessionPermissionRules;
}>;
shellGate?: ShellGateDoctrine;
/** The chain's DECLARED permission mode at suspend (data half, same law as `shellGate`): the suspended leg's
* own effective declaration — its request clamped under its parent's ceiling — before the deployment's fold.
* A resume leg reads it as the CEILING its re-passed spec is clamped under (tighten-only, the shell floor's
* law) and re-judges the deny bits and the classifier face for its own leg: a `bypassPermissions` row does not
* revive the mode where the redeeming deployment denies it, and an `auto` row arms only where a face is
* wired. Absent on a task that declared nothing and inherited nothing (`default`). Rows written before this
* member carried `autoModeRequested` / `approverPosture` / a doctrine `shellGate`; no engine since 7.39.0 reads
* them — such a row is refused before the claim ({@link refuseRetiredModeSeed}). */
permissionMode?: PermissionMode;
/** Beside a recorded `plan`: the declared word the leg had when a steer or the model's `EnterPlanMode` moved it into
* plan — the mode an approved or edited `plan_review` resumes under ({@link import("./mode-policy.js").modeRequestAfterResume}). */
prePlanMode?: PermissionMode;
/** Org-memory admission freeze (ruled 2026-08-05): the chain's admitted org-scope set at
* suspend (data half, plain strings). The resume leg folds it seed ∩ live (tighten-only) and
* re-runs admission under it — a resume must never widen the delegation freeze. Absent on
* older checkpoints ⇒ the live channel (or, for a delegated child, the fail-closed ∅) stands.
* Cross-VERSION enforcement (an older worker resuming a newer checkpoint ignores fields it
* does not know) is the deployment's server/core version-gate concern, not a per-field fence. */
admittedOrgScopes?: readonly string[];
/** THIS task's OWN admitted org verdict at suspend (adversarial-review adoption, 2026-08-05):
* distinct from {@link admittedOrgScopes} (the incoming PARENT constraint). The resume leg
* applies it as the SESSION freeze on the final admitted projection — regardless of origin
* (deployment-origin scopes circumvent the chain freeze, never the session freeze) — so a session's
* verdict can only NARROW across suspend/resume: re-adjudication still runs every leg (a
* revoked grant refuses the resume), but a policy widened mid-session cannot widen THIS
* session's mount or the chain its children inherit. Present (possibly EMPTY — an adjudicated
* empty verdict is not "org-less") iff the suspend leg adjudicated an org plane; absent on
* older checkpoints / org-less tasks. Checkpoints carrying this state are stamped
* {@link ORG_ADMISSION_CHECKPOINT_VERSION} so a pre-admission worker rejects them pre-CAS. */
ownAdmittedOrgScopes?: readonly string[];
/** The WRITE half of the session freeze: the org write grant this session held at suspend —
* `null` = the org write face was read-only (a resume must not gain write authority the
* suspend leg did not hold); a scope string = the explicit grant. Present iff
* {@link ownAdmittedOrgScopes} is (same adjudication record). */
ownAdmittedOrgWriteScope?: string | null;
/** The MONOTONIC org-governance provenance bit (falsification-style round 5): `true` when any leg of
* the suspended tree ran with an org-admission surface (or inherited the bit). A resume worker
* missing the surface still treats the leg as governed — org-shaped scopes respelled outside
* the v2 contract are refused, even when every admitted set en route was EMPTY. Absent on
* trees that never had a governance surface. */
orgAdmissionGoverned?: true;
/** The chain's top leg's root and home at suspend (data half): the anchors of the person's global / session path rows
* and of every `~/` row on this chain (a project row anchors at its own scope root). Absent on older checkpoints and on
* a top leg ⇒ the resumed leg is its own top. Rebased with the workspace on a restore elsewhere; a value that is not
* an absolute path anchors nothing (the rows it would anchor read `unreadable`). */
pathRuleAnchors?: {
readonly root: string;
readonly home?: string;
};
/** Where each parent constraint the suspended leg ran under was installed, position for position (data half; `null`: an
* entry with no path rules). A resume that re-supplies the chain uninstalled installs each entry there. Rebased with the
* workspace; a value that is not an absolute path installs nothing (the entry is installed at the chain top). */
pathRuleInstallations?: ReadonlyArray<{
readonly root: string;
readonly home?: string;
} | null>;
requiresParentConstraint: boolean;
/** How many opaque parent constraints the suspended leg ran under (present iff `requiresParentConstraint`).
* The resume pre-CAS ladder rejects a re-supply whose `parentConstraints.length` differs
* (`resume.parent_constraint_mismatch`) — a partial re-supply is a typed error, never a silently
* thinner chain. Absent on older checkpoints ⇒ only the non-empty check applies. */
parentConstraintCount?: number;
/**
* F-012 L1 — the persisted CONSTRAINT PROJECTIONS of the inherited chain, one ordered entry per
* `parentConstraints` layer: the layer's deterministic deny data
* ({@link import("./tool-policy.js").ToolPolicyProjection} content) or an explicit `{opaque:true}`
* placeholder for a layer that exports none. The resume leg EXECUTES these persisted projections
* (extra deny-narrowing layers + the pre-CAS edit re-adjudication) rather than trusting a
* re-supplied policy's execution half for the frozen deny data — a re-supplied closure that
* matches the digest but behaves differently cannot un-deny what the suspend froze. Present iff
* `requiresParentConstraint` on new mints; absent on PRE-F-012 checkpoints (⇒ no frozen deny data —
* the count-only contract governs, and an EDIT resume is refused for want of a validatable chain).
* "Older" is version-decidable, not guessed: chain, digest and the {@link F012_CHECKPOINT_VERSION}
* stamp are one write, so on a v7+ row the pair is REQUIRED — its absence there is a damaged row and
* the resume ladder refuses it pre-CAS rather than falling back to the count-only contract. */
constraintChain?: ReadonlyArray<import("./tool-policy.js").ConstraintChainEntry>;
/**
* F-012 L1 — the content digest over `constraintChain` ({@link import("./tool-policy.js").constraintChainDigest}):
* binds the ordered chain topology (layer boundaries + opaque placeholders) and each projection's
* content. The resume pre-CAS ladder recomputes the digest over the RE-SUPPLIED chain's
* projections and rejects a mismatch (`resume.parent_constraint_mismatch`) — replacing the
* count-only shape check with a content contract (a same-length chain of different frozen deny
* data no longer passes). Present iff `constraintChain` is — and on a v7+ row both are required
* (see {@link constraintChain}: the pair and the version stamp are minted together). */
constraintDigest?: string;
};
/**
* [c209-C] Q5 — the listing frames' announced NAME-SETS at suspend (agent types / skills the model
* has actually been shown this run, committed on first-frame delivery or intact boundary survival).
* A resume leg seeds its producers from these names and DIFFS the current roster/skill set against
* them, so cross-leg drift (web-admin roster change between suspend and resume) is announced as
* a delta frame at the first boundary instead of silently swallowed — the exact event the listing
* eviction from the cached prefix exists to serve. Names only, never contents (a description change
* on a surviving name is deliberately not re-announced — name-keyed diff, agent_listing parity).
* Schema ADDITION (backward-compatible, B-5): an older checkpoint without the field resumes via the
* MED-3② transcript probe (delivered ⇒ seeded as the CURRENT entries, drift undetectable there;
* not found ⇒ full re-announce at the first boundary — conservative direction preserved).
*
* [c209-C] R2 C7 — `models`: the model CATALOG names announced (the agent_listing initial frame's
* "Models available for the 'model' parameter:" tail, Q4). The resume seed feeds it to the
* producer; a differing current catalog ⇒ the first boundary's agent_listing frame re-carries the
* full line — without this a catalog swap between legs was never re-disclosed anywhere (the
* catalog left the tool description with Q4). Absent (older checkpoint / never announced) ⇒ drift
* undetectable, silent (conservative, same B-5 direction).
*/
announcedListings?: import("../internal/harness.js").AnnouncedListingSnapshot;
/**
* The git-status frame's announced `(kind, hash)` state at suspend (env-tail migration: the
* run-loop-maintained mirror, advanced at every frame receipt). The LOWEST rung of the resume
* read ladder — the session branch walk is the authority (a nearest PENDING mirror there, or an
* announced mirror whose frame entry is off the active branch, forces a re-announce regardless of
* this field); the checkpoint seeds only when the branch carries no mirror at all, and its
* `entryId` must itself sit on the active branch to count as announced. Schema ADDITION
* (additive, old readers ignore it — the Q5 posture): an older checkpoint without the field re-announces
* conservatively — the frame is a tail append, so a duplicate costs bytes, never a prefix break.
*/
gitAnnouncement?: {
kind: "full" | "degraded" | "unavailable" | "non-repo";
hash: string;
entryId?: string;
pending?: true;
};
/**
* design/180 R-4 (R-2 cross-process variant) — the armed delegation child's MONOTONIC runtime-provenance
* aggregate at suspend ({@link import("./memory-engine/delegation-provenance.js").DelegationProvenanceAggregate}:
* version + coverageStartTurn + the two never-cleared bits sawExternal/incomplete). Persisted so a
* resume — a cross-process one especially, which lost the in-memory ref — CONTINUES accumulating from
* the recorded bits instead of restarting a fresh aggregate that could prove "clean" on the post-resume
* segment alone. Plain data → JSON/structuredClone round-trips. Present ONLY when the suspended leg ran
* with a recorder armed; ABSENT on an unarmed leg AND on an older checkpoint — the resume side reads
* absence-on-an-armed-child fail-closed as `incomplete`. */
delegationProvenance?: import("./memory-engine/delegation-provenance.js").DelegationProvenanceAggregate;
/**
* #281 件B follow-up — `true` iff the suspended leg ran as a DELEGATED CHILD (the trusted
* `RunInternals.isDelegatedChild` fact, read off the leg's own minted
* {@link import("./runner/contracts.js").Prepared.hookIdentity} so a re-suspend chain carries it
* forward). Without it a deps-only resume (`resume(token, outcome, config)` — no trusted internals
* re-supplied) re-minted the resumed leg's identity with `isDelegatedChild: false`, so the
* delegation-lifecycle observer's emission gate stayed shut and the resumed leg emitted NEITHER
* frame — a deployment watching only that seat saw a human-approved delegation frozen at its
* park-leg `"suspended"` terminal forever. Live internals still win when re-supplied (the in-engine
* resume lanes all do); the checkpoint key is the fallback for the deps-only leg. Only this ONE
* axis persists — `insideFork`/`agentName`/`parentToolCallId` stay internals-only, honestly absent
* on a deps-only resume rather than guessed; the accepted residue of the absent `insideFork` is
* that a FORK child resumed deps-only reads as a plain delegated child on the non-fork consumer
* facet (subagent persona/context trim) — documented here rather than fixed with a new
* persistence axis. Schema ADDITION (additive, old readers ignore it —
* the Q5 posture; no version movement): an older worker resuming a newer row keeps the pre-key
* behavior, and an older row without the key resumes frame-less by documented honest absence
* (see {@link import("./types.js").DelegationLifecycleEvent}). Absent = the leg was not a
* delegated child (never `false` — presence-coded like the other monotonic bits here).
*/
isDelegatedChild?: true;
/**
* design/380 O1③ — the run TREE's placement fixed point at suspend: the SAME three-segment fold the
* env-factory ctx mint uses (explicit `RunInternals.placementRoot` > the delegation lanes'
* `rootSessionId` > the leg's own resolved session), stamped from TRUSTED internals so a durable
* resume can restore it instead of falling back to the parked leg's own session (a target-bound
* factory keys its placement lookup on the fixed point — the self fallback read a resumed leg as a
* NEW placement). Restore is the `isDelegatedChild` posture replayed on the placement axis: an
* EXPLICIT live `internals.placementRoot` wins when re-supplied; otherwise THIS key restores into
* `internals.placementRoot` at prepare so descendants and the NEXT re-suspend mint inherit it too
* — outranking a supplied `rootSessionId`, which is DELEGATION IDENTITY (the peer/registry axis a
* revive leg re-supplies from its row), never a placement claim (reading it as one
* permanently stuck every parked agent spawned under an explicit placement root). The resume entry
* ALSO reads this key as an identity record: a resume whose supplied `internals.placementRoot`
* CONTRADICTS it is refused pre-CAS (`resume.placement_mismatch`); either side absent skips the
* check (an older row / a claim-less resume — the honest degradation arm), and `""` reads as
* absence (the identity fields' discipline). Schema ADDITION (additive, old readers ignore it — the Q5 posture;
* deliberately NOT version-gated: same rolling-deployment account as {@link Checkpoint.principal},
* whose doc records version-gating as a design candidate — any new stamp is a lattice-wide window
* rewrite, owned by that ticket, not ad-hoc here). Absent ⇒ minted before the field, or the fold
* resolved empty on a factory-less run (never persisted as garbage).
*/
placementRootSessionId?: string;
/**
* design/380 O9a — MONOTONIC presence bit: this run's hand-tool content fold was EXTERNAL-TARGET
* armed (the env declared `ExecutionEnv.externalContentTarget`, or an earlier leg of this suspend
* chain did). Stamped at the suspend mint from the run's RESOLVED fold state — never from first
* tool use, so a park landing BEFORE the first hand call is covered too. Presence-coded like
* {@link isDelegatedChild} (absent = never armed; never `false`). The resume side folds
* **persisted OR live declaration** (a monotonic join, never a downgrade): a run minted under a
* declaring adapter keeps its memory fold and its prompt disclosure when resumed on an
* undeclared/older adapter — the downgrade is closed in the derivation itself, deliberately
* WITHOUT a new refusal arm (one fewer failure path, same safety). Feeds the memory face and the
* prompt disclosure only — no gate/policy/roster behavior reads it (the design/378 red line).
* Schema ADDITION (additive, Q5 posture; the version-gate residual is shared with
* {@link placementRootSessionId} above and owned by the same version-lattice ticket).
*/
externalContentTarget?: true;
/** The scope this LEG's rows land in (its prepare's one `checkpointScopeOf` reading), stamped at every mint: a resume
* that hands no live seat restores it, so the resume config cannot move the leg's next row. Not {@link Checkpoint.scope}
* (where THIS row is filed — a resource slice's own declared scope). Absent ⇒ minted before the member: derived as before. */
legScope?: string;
/** The tool-selector layers this LEG was spawned under (`RunInternals.toolCeiling`), stamped at every mint: a resume that hands
* no live ceiling restores it, so a resumed child can never use a tool its spawning run could not. Absent ⇒ the leg had no
* ceiling (a root run) or the row was minted before the member. A reader that lacks the word would run the leg unbounded.
* Each entry is a tool name in its canonical spelling (`persistedToolNameList`, `src/agents/tool-filter.ts`): a row restored with
* any other entry is refused before the claim (`checkpoint.config_refused`, `detail.code: config.tool_face_control`, `detail.field: inheritedGate`, the row pending). */
toolCeiling?: ReadonlyArray<{
readonly allow?: readonly string[];
readonly deny: readonly string[];
}>;
/** The evidence that the run that spawned this LEG judged its own tool lists (`RunInternals.toolListsJudged`: the digest of exactly
* those lists), stamped at every mint while it holds: a resume that hands no internals and the same lists keeps the judgment, one
* that hands different lists is judged again. Absent ⇒ the leg judged its own lists (or the row was minted before the member). A
* reader that lacks the word would judge again a list judged against a wider universe, and refuse a leg that was valid. */
toolListsJudged?: string;
/** Whose lists this LEG's own are (`RunInternals.ownToolLists`: the delegation tool that spawned it, by its declared name, the
* agent definition it was spawned as — absent ⇒ the tool's tool-level list — and `lists`, the digest of exactly the lists it wrote),
* stamped at every mint while it holds: a resume that hands no internals restores it while its own lists digest to `lists`, so the
* resumed leg asks its own lists of the same universe (which carries that tool's name — a name of its lineage, mounted or not at its
* depth) and a refusal names the same list; a resume that hands another table reads it as the host's own `TaskSpec` seats. Absent ⇒ the
* leg's lists were not written by a delegation tool (or the row was minted before the member): the leg judges them against its own
* roster as before. A reader that lacks the word would refuse a list the leg's lineage wrote, and refuse a leg that was valid. */
ownToolLists?: {
readonly toolName: string;
readonly definition?: string;
readonly lists: string;
};
}
/**
* {@link CheckpointState} with **every** field made required-PRESENT, while each value keeps its original
* type (so an absent optional field is still passed explicitly as `undefined`). The suspend-side
* serialization builds `state` as this type (design/45 P3 / design/51 §P3): adding a new per-task
* correctness field to {@link CheckpointState} is then a **compile error** at the serialization site until
* it is explicitly handled — it can never be silently omitted and lost on resume. (Keying off
* `keyof Required<…>` makes the mapped type non-homomorphic, so it forces presence of every key yet leaves
* each value's `| undefined` intact — `workspaceHandle: undefined` for a process-local suspend still
* type-checks.)
*/
export type SerializedCheckpointState = {
[K in keyof Required<CheckpointState>]: CheckpointState[K];
};
/**
* The checkpoint FEATURE TABLE — one row per feature that ever raised the row format: its schema version, and
* what a mint owed when the feature was present (`writes`, prose). The per-feature constants below ARE the rows'
* versions. The table is CLOSED at `requirements` (v10): every live mint stamps 10 and lists the row's
* reader-requirement words beside it (`Checkpoint.requires`, checkpoint-requirements.ts) — a later segment adds a
* word there, never a row here. Each feature key below (but `baseline` / `requirements`) is itself one of those
* words, and whether a v10 row carries the feature is read off the row's content by the requirement tables.
*
* A v≤9 row was stamped the HIGHEST feature it carried, so the number records only the top feature; the resume
* rules that read a v≤9 row by number are exact to the version at which their feature was the top
* (`realApproval` is version-recorded at v8 and marker-recorded — `readFace.realApproval` — at v9).
*
* v4 is a GAP, deliberately: the retired cross-slice wall-clock allocation. This worker still reads v4
* rows; a ledger carrying the retired axis is refused by FIELD (`walltime_axis_retired`) at any version.
*/
export declare const CHECKPOINT_FEATURES: {
/** The baseline shape: a plain human-approval suspend before any feature raised the format. No live
* mint stamps it today; it is the floor of the ladder and the reading of a row written before any feature. */
readonly baseline: {
readonly version: 1;
readonly introduced: "design/49";
readonly writes: "the row itself";
};
/** A `resource_limit` suspend: `resourceLedger` plus a `resource_limit` gate/pendingAction an older worker
* cannot handle (it rejects pre-CAS; the row stays `pending`, retryable on a newer worker). */
readonly resource: {
readonly version: 2;
readonly introduced: "design/74";
readonly writes: "resourceLedger; gate/pendingAction kind resource_limit";
};
/** A HUMAN-DECISION park (tool approval or plan_review) of the binding era: the decision-action binding
* (`boundCallId` + `boundInputHash` on a tool_approval) is enforced ENTIRELY by the resuming worker, so a
* pre-binding worker must reject the row rather than execute the pending tool unverified. Like `resource`,
* this row is a park-kind FLOOR: a plan_review park carries it with no binding (it was introduced above it);
* whether a tool_approval row carries its hash is the resume binding rung's own conjunction. */
readonly binding: {
readonly version: 3;
readonly introduced: "design/80";
readonly writes: "pendingAction.boundCallId + boundInputHash on a tool_approval; a plan_review carries the stamp only";
};
/** A ledger carrying a cross-slice TOKEN allocation: the window clamp and the exhausted-resume fail-fast
* live in the resuming worker. A ledger with no token total keeps the historic stamps. */
readonly token: {
readonly version: 5;
readonly introduced: "design/164";
readonly writes: "resourceLedger.totalTokens";
};
/** Org-memory admission freeze state (the adjudicated-empty record included): the session/delegation
* freeze on resume and the tenant-admission door live in the resuming worker. */
readonly orgAdmission: {
readonly version: 6;
readonly introduced: "org-admission";
readonly writes: "inheritedGate.admittedOrgScopes / ownAdmittedOrgScopes / ownAdmittedOrgWriteScope / orgAdmissionGoverned";
};
/** The F-012 durable enforcement fields: the frozen ancestor projection chain + its digest (one write, the
* resume-edit re-adjudication executes it) and/or the delegation-provenance aggregate a resume re-seeds. */
readonly f012: {
readonly version: 7;
readonly introduced: "F-012";
readonly writes: "inheritedGate.constraintChain + constraintDigest together; delegationProvenance";
};
/** A gate carrying {@link RealApprovalGateBit}: the no-org-wiring refusal and the org_unavailable resume
* belt live in the resuming worker. Minted at ONE site (the approval park); the two always-mounted
* integrity policies mint it on any durable deployment, org-governed or not. */
readonly realApproval: {
readonly version: 8;
readonly introduced: "#130/#120";
readonly writes: "gate.realApproval (a well-formed bit) + pendingAction.requiresRealApproval; from v9: readFace.realApproval = true beside it";
};
/** The READ-face section ({@link CheckpointState.readFace}): resolved face + normalized deny-set additions,
* written only for a non-default posture; the stricter-wins fold lives in the resuming worker. Carries the
* redundant governance marker for `realApproval` (see the table note above). Absent ⇒ default posture. */
readonly face: {
readonly version: 9;
readonly introduced: "design/199";
readonly writes: "state.readFace { face, denyEntries? } for a non-default posture; realApproval: true only at the mint whose gate carries the bit (a hands-less re-park never carries the seed's marker forward)";
};
/** The last row: the reader-requirement words ({@link CheckpointRow.requires}). Every live mint stamps it, so a
* worker of v7.24.2–7.48.x (ceiling 9) refuses every row minted from here on, `version_newer`, pre-CAS. */
readonly requirements: {
readonly version: 10;
readonly introduced: "#1320";
readonly writes: "requires (the row's words) + requirementsDigest (the row digest), both derived from its content (checkpoint-requirements.ts)";
};
};
/** One row of {@link CHECKPOINT_FEATURES}. */
export interface CheckpointFeatureRow {
readonly version: number;
/** Where the feature came from — a design or ticket tag, documentation only. */
readonly introduced: string;
/** What a mint wrote when the feature was present (prose; the mint sites are the code). */
readonly writes: string;
}
/** The feature words — the row keys of {@link CHECKPOINT_FEATURES}. */
export type CheckpointFeature = keyof typeof CHECKPOINT_FEATURES;
/** The BASELINE {@link Checkpoint} schema version — the `baseline` row. NOT the highest a resume accepts (that
* is {@link MAX_SUPPORTED_CHECKPOINT_VERSION}); resume stays forward-compatible by `<=` against MAX, never `===`. */
export declare const CURRENT_CHECKPOINT_VERSION: 1;
/** The `resource` row's version. */
export declare const RESOURCE_CHECKPOINT_VERSION: 2;
/** The `binding` row's version. */
export declare const BINDING_CHECKPOINT_VERSION: 3;
/** The `token` row's version. v4 (the retired wall-clock total) is deliberately not reused. */
export declare const TOKEN_CHECKPOINT_VERSION: 5;
/** The `orgAdmission` row's version. */
export declare const ORG_ADMISSION_CHECKPOINT_VERSION: 6;
/** The `f012` row's version. */
export declare const F012_CHECKPOINT_VERSION: 7;
/** The `realApproval` row's version. */
export declare const REAL_APPROVAL_CHECKPOINT_VERSION: 8;
/** The `face` row's version. */
export declare const FACE_CHECKPOINT_VERSION: 9;
/** The highest {@link Checkpoint.version} `runner.resume` will act on — the top row of the table, and the stamp
* of every live mint (the table is closed there); a higher one is rejected pre-CAS with {@link CheckpointError}
* `unsupported_version` (the row stays `pending`, retryable on a newer worker). */
export declare const MAX_SUPPORTED_CHECKPOINT_VERSION: 10;
/**
* Read a checkpoint's schema version, defaulting an absent field to **legacy `0`** (a 1.67-era checkpoint
* written before the field existed — it carries no `workspaceHandle`, so resuming it the v1 way is safe).
* Compare with `<= MAX_SUPPORTED_CHECKPOINT_VERSION`, never `=== CURRENT_CHECKPOINT_VERSION` (design/49 §3).
*/
export declare function checkpointVersionOf(cp: Pick<Checkpoint, "version">): number;
/**
* design/74 R3-B: cross-slice resource accounting carried by a `resource_limit` {@link Checkpoint}. The
* total budget is the human's allocation; `spent*` accumulate across the resume→re-suspend chain (debited
* at each suspend's `put`). A resumed slice's effective `maxCostUsd` = `min(window, totalBudgetMicroUsd -
* spentMicroUsd)`; the final {@link TaskResult} stats aggregate `spent* + this slice's stats` so a
* multi-slice run reports the whole, not just the last segment. Lives ON the checkpoint row so one
* `resolve`/`put` CAS covers status + ledger atomically (no cross-store split).
*/
export interface ResourceLedger {
/** The human's total $ allocation in micro-USD; `undefined` = no $ ceiling (only window/walltime bound). */
totalBudgetMicroUsd?: number;
/** design/164 — the human's total TOKEN allocation across the whole resume→re-suspend chain;
* `undefined` = no token ceiling (only the per-slice window binds). Replaces the retired wall-clock
* total: tokens are additive across processes, wall-clock across a suspend chain is not. */
totalTokens?: number;
/** Cumulative cost across all slices so far (micro-USD), debited at each suspend's `put`. */
spentMicroUsd: number;
/** Cumulative tokens across all slices so far. */
spentTokens: number;
/** Cumulative turns across all slices so far. */
spentTurns: number;
/** Cumulative wall-clock across all slices so far (ms; each slice measured on the monotonic clock).
* design/164: PURE ACCOUNTING — it is no longer a gate on anything (the wall-clock resource axis
* retired). Kept because "how long did this chain actually run" is a real observability question.
* Absent on checkpoints written before it existed; readers treat missing as 0. */
spentWalltimeMs?: number;
/** How many slices have run so far (the resource chain's suspend count). */
sliceCount: number;
}
export declare function debitLedger(prior: ResourceLedger | undefined, slice: {
costMicroUsd: number;
tokens: number;
turns: number;
walltimeMs?: number;
}, total?: {
totalBudgetMicroUsd?: number;
totalTokens?: number;
}, opts?: {
countSlice?: boolean;
}): ResourceLedger;
/** design/74 Slice 4: the $ budget (micro-USD) the NEXT slice may still spend = `totalBudgetMicroUsd -
* spentMicroUsd`, never negative. `undefined` when no total $ ceiling is set (the run is bounded only by the
* per-slice window / walltime). A resumed slice's effective `maxCostUsd` is `min(window, this)`. NB: `0` is a
* VALID, exhausted ceiling — NOT "unlimited" (only `undefined` is unlimited). Any spend then immediately trips
* `overBudget`, and the run loop fails an exhausted resume fast (runtask). Never falsy-test this value (a
* `if (remaining)` would skip a legitimate 0 ceiling and silently circumvent the budget). */
export declare function remainingBudgetMicroUsd(ledger: ResourceLedger | undefined): number | undefined;
/** design/164 (the TOKEN sibling of {@link remainingBudgetMicroUsd}): tokens the NEXT slice may still
* spend = `totalTokens − spentTokens`, never negative. `undefined` when no token total is set (the run is
* bounded only by the per-slice window / the $ axis). A resumed slice's effective `limits.maxTokens` is
* `min(window, this)` — a resume can't refresh the allocation. NB: `0` is a VALID, exhausted ceiling
* (only `undefined` is unlimited); the run loop fails an exhausted resume fast — never falsy-test it. */
export declare function remainingTokens(ledger: ResourceLedger | undefined): number | undefined;
/**
* design/80 D-1 (reopen-by-reason + persist-winner): the WINNING resume binding, recorded ON the
* checkpoint row the moment a {@link CheckpointStore.resolve} CAS wins (pending → resolved). It is the
* durable record of "which decision was approved against which pending action" so a later reopen→re-resume
* can be validated against it. A `policy_ask` resolve and BOTH review-gate resolves record a winner: only
* `policy_ask` is bound to a specific pending tool call, while `plan_review` / `dry_run_review` record
* their decision under the synthetic `gate:<kind>` marker instead (RB-64). A `resource_limit` / `wake` /
* `task_done` resolve records none (there is no decision to bind). `boundInputHash` (binding the exact executed bytes) is a SEPARATE
* later D-1 slice (1a.2) and is deliberately NOT part of this winner.
*/
export interface ResolvedOutcome {
/** The pending tool call this decision was bound to (design/80 D-1 slice 1a). A REVIEW gate binds
* no tool call, so it records the synthetic `gate:<kind>` marker instead — the replay guard compares the
* DECISION for those, never a call id. */
boundCallId: string;
/** The adjudication the operator made. RB-64 widens this to the review verdicts (`approve`/`reject`/
* `edit`) so a `plan_review`/`dry_run_review` winner can be persisted and replayed by the same
* env_failed reopen guard the approval gate has — previously review gates recorded NO winner, which is
* what left their reopen path with nothing to check a re-vote against. */
decision: "allow" | "deny" | "approve" | "reject" | "edit";
/** A re-written arg payload, if the `allow` rewrote the call (design/37). Carried so an `env_failed`
* re-resume (a system retry of the SAME approved action) must replay the identical `updatedInput`. */
updatedInput?: unknown;
/** The content-ask ANSWER the `allow` carried, if any (ruled 2026-08-04). Recorded for the same reason
* `updatedInput` is: the answer is the SUBSTANCE of a content-ask decision, not a decoration on it, so
* an `env_failed` re-resume — a system retry of the already-approved action — must replay the identical
* answer. Without it the replay could keep the verdict and swap the answer text going into the model's
* context (the shape the plan-edit replay guard closed for `editedPlan`). Omitted when absent so the
* replay equality check does not falsely diverge on `undefined`. */
answer?: QuestionAnswer;
/** The operator's NOTE, if the decision carried one — the approval lane's deny `reason` and the review
* lanes' reject `reason` alike. Recorded for the same reason `updatedInput` and `answer` are: the note
* is the substance of the decision, not a decoration on it. It is the text the resume continuation
* fences into the model-facing reminder that carries the refusal, so a replay keeping the verdict and
* swapping the note delivers a different instruction under the same word. Omitted when absent.
*
* ADDITIVE and OPTIONAL by contract: rows persisted before this field existed carry no `reason` key,
* and the key's absence is read as "this dimension was never recorded" — the replay guard skips it —
* NEVER as "recorded as empty", which would refuse every legitimate retry of an existing row. Absence
* is therefore indistinguishable from a genuinely note-less decision, which is the accepted price of
* the additive shape: the binding starts with the rows that record it.
*
* RECORDED VERBATIM, and independently of the verdict: any supplied note is kept exactly as given —
* including `""`, and including a note on a verdict that never relays one (an `allow`, an `approve`, an
* `edit`). The winner is a record of the decision that was made, not of the subset of it the
* continuation happens to render, and `updatedInput` has carried the same property since D-1 (a rewrite
* on a `deny` is recorded and replay-compared although nothing executes it). Normalizing here would
* mean an empty note collapsed into the "never recorded" hole above and a note on a non-relaying
* verdict became free to change between the resolve and its replay — both weaken "the retry presents
* the same decision" for no delivery benefit, since a replay is produced by re-presenting the same
* outcome, not by reconstructing an equivalent one. */
reason?: string;
/** The settlement record core minted for this decide ({@link import("./gate-outcome.js").GateOutcome}):
* the row's own copy of what the resolved call's `tool_end` frame carries, written with the winner so
* an audit reading the row reads the same record the stream carried. NOT part of the replay equality
* (`when` differs per replay; the record is a consequence of the decision, not its substance). */
gateOutcome?: import("./gate-outcome.js").GateOutcome;
}
/** Why a consumed checkpoint was reopened — declared beside the one table that picks it. */
export { REOPEN_REASONS, type ReopenReason } from "./resume-startup-cause.js";
/**
* design/80 D-1 (atomicity fix): the optimistic-concurrency precondition a {@link CheckpointStore.resolve}
* caller passes so its CAS is atomic with the validation it did against an earlier `get()` snapshot. The
* resume-side decision-action guards (boundCallId/boundInputHash) bind to per-token-IMMUTABLE fields, but the
* **reopen-by-reason** guard reads `reopenReason` + `resolvedOutcome` — which a concurrent
* {@link CheckpointStore.reopen}/{@link CheckpointStore.resolve} cycle mutates. Passing the monotonic
* {@link Checkpoint.rev} the caller observed makes `resolve` additionally require the LIVE row's `rev` to still
* equal it; ANY intervening resolve/reopen bumps `rev`, so a cycle in the get→resolve window loses the CAS
* (fail-closed), forcing a re-`get` + re-validate.
*
* **Why a counter, not the reopenReason value (round-2 BLOCKER fix):** `reopenReason` is a 2-valued enum, so a
* full `env_failed`→`tool_unavailable`→`env_failed` cycle (the `tool_unavailable` arm permits a fresh P-7
* re-decision) returns `reopenReason` to the SAME value while `resolvedOutcome` silently changed to a different
* (e.g. RETRACTED) winner — a classic ABA on the OCC key. A monotonic `rev` bumped on every resolve/reopen is
* ABA-proof: a returned-to-the-same-value `reopenReason` still has a strictly higher `rev`. Omit `expect`
* (legacy callers / direct store tests) → no OCC.
*/
export interface ResolveExpectation {
/** The monotonic {@link Checkpoint.rev} the caller observed at `get()` (absent rev ⇒ legacy `0`). */
rev: number;
}
/** design/384 slice 2 / S-25-R1 — the terminal intents a claim can carry. Deliberately the
* TERMINAL pair only: `resolve` and `expire` are the two verbs that race one pending row to
* its end (the design/51 fence law). `reopen` (resolved→pending) is not terminal and `reap`
* is a bulk sweep — both stay on their own verbs. */
export type TerminalClaimIntent = {
kind: "resolve";
outcome: ResumeOutcome;
expect?: ResolveExpectation;
} | {
kind: "expire";
};
/** design/384 slice 2 / S-25-R1 — the single-round-trip answer: EITHER the claim won, OR it lost and
* the SAME atomic operation reports what the row is now. `current` reuses the row's own vocabulary
* (status / rev / resolvedOutcome) — no new value domain. `status:"pending"` on a loss is the OCC
* arm only (`expect.rev` mismatch): the row is still open — re-get + re-validate, zero backoff (a
* cleanly KNOWN state is never "unknown"). */
export type TerminalClaimOutcome = {
claimed: true;
} | {
claimed: false;
current: {
status: "pending" | "resolved" | "expired";
/** `(cp.rev ?? 0)` — the absent-rev-reads-as-0 rule the OCC key already uses. */
rev: number;
/** Row-field PASSTHROUGH on every present status: the row PRESERVES it across `reopen`
* (resolved→pending rows carry it), `expire` never clears it, and a winnerless resolve
* (resource/wake/task_done) does not overwrite an older value — so even
* `(status:"resolved", outcome present)` is only "the row is resolved AND this is its
* LAST RECORDED decision winner", never a store-level guarantee that THIS resolve minted
* it. Interpreting the pair as a current decision is the CALLER's discipline (the
* runner's gate-kind matching; a single-decision row schema like a deployment's ask
* rows). See {@link CheckpointStore.claimTerminal} law 6. */
resolvedOutcome?: ResolvedOutcome;
}
/** Row missing — OR wrong scope: the CAS predicate is the shared REF-A4 row match
* ({@link checkpointRowMatches}), and a wrong-scope claim must lose WITHOUT reporting the
* row's truth (multi-tenant isolation: cross-scope truth in a loss answer would be a read
* bypass of the scope WHERE). Absent is a KNOWN state (zero backoff): expire intent ⇒ moot;
* resolve intent ⇒ the caller's typed not-found path, no retry. */
| {
status: "absent";
};
};
/** A persisted suspension point: enough to resume a task on any replica. `status` drives the 3-state
* machine (pending → resolved | expired) that makes resume idempotent (§5). */
/** A durable checkpoint: the row's fields ({@link CheckpointRow}) plus its registry-bound pause pair. */
export type Checkpoint = CheckpointRow & CheckpointPause;
export interface CheckpointRow {
token: CheckpointToken;
/** Non-secret stable identity ({@link mintCheckpointId}) — the display/correlation twin of
* the secret `token` (identification vs capability, separate axes). Present on every row minted
* since its introduction; absent ⇒ legacy row (read compatibly, no backfill). */
checkpointId?: string;
/**
* Schema version of this checkpoint (design/49 v1.5), FROZEN at the `requirements` row's 10: every live mint
* stamps 10 beside {@link requires}, and a segment added later adds a word there, never a number here. A v≤9 row
* (stamped the highest feature it carried) is read by the feature table's numbers; absent ⇒ legacy `0`. Resume
* rejects `> MAX_SUPPORTED_CHECKPOINT_VERSION` pre-CAS. Read via {@link checkpointVersionOf}.
*/
version?: number;
/** The reader-requirement words of a v10 row: what a resuming worker must know to resume it right, derived from
* the row's own content at the mint (`checkpointRequirementsFor`, checkpoint-requirements.ts) — present on every
* v10 row (possibly empty), absent below. Typed as plain strings: a newer release's word is a word this one lacks. */
requires?: readonly string[];
/** The row digest of a v10 row: sha256 over the canonical JSON of every member a decision reads (`requirementsDigestOf`,
* checkpoint-requirements.ts) — a lost member, element or changed value is refused `requirements_damaged`. */
requirementsDigest?: string;
/** Multi-tenant isolation key (forced through `resolve`/`reap` WHERE, §2.1 service [4]). */
scope: string;
/** The session to resume (via the SessionStore). */
sessionId: string;
/** The session leaf the suspend happened at — the resume point (CAS write base, §5). */
leafId: string;
state: CheckpointState;
/** design/74 R3-B: cross-slice resource accounting. Present for a `resource_limit` gate (debited at the
* suspend's `put`; read on resume to size the next slice + aggregate the final stats). **design/80 D-E-core
* (A3):** ALSO attached to a human/irreversible_ask APPROVAL suspend — debited with that leg's spend — so a
* STATELESS policy can read durable cumulative spend across the resume chain via {@link ToolCallRequest.budget}
* (a fresh in-memory counter would reset every leg). On the approval path it is READ-CONTEXT ONLY: the final
* -stats aggregate fold (runtask) is gated to the `resource_limit` gate kind, so an approval resume is never
* double-counted. Absent for `task_done`. See {@link ResourceLedger}. */
resourceLedger?: ResourceLedger;
/**
* Absolute epoch-ms deadline for an awaiting-human checkpoint; past it, `reap` expires it (§6).
*
* **design/80 §D-D semantic contract (the DURABLE FIELD a reaper-loop consumes — read it on
* {@link CheckpointStore} too):** for an APPROVAL gate (`gate.kind` ∈ {`human`, `irreversible_ask`}) the
* deployment's reaper-loop treats this as an **SLA resolve-deny** time (FACET A — its fast path: at deadline
* it `resolve`s the checkpoint as a `deny` so the run ends cleanly), with a LATER `terminalAt` abandonment
* backstop the loop ALSO honors (crash-safe if the SLA timer never fired). For an UNATTENDED-TTL gate
* (`gate.kind` ∈ {`resource_limit`, `needs_review`, `plan_review`} — the resource + review-pause family) this
* is an **abandonment-TTL** → `expire`/{@link reap} ONLY, NEVER a resolve-deny (FACET B — those kinds resume
* only with their OWN outcome, so a `policy_ask` resolve-deny hits no `gateMatch` arm → `gate_mismatch`). The
* reaper-loop + the gate.kind policy + minting `terminalAt` are the DEPLOYMENT's job
* (its store impl + loop — owned service-side); CORE only guarantees this `deadline`
* field (for `human`/`irreversible_ask`) + the persisted {@link gate}.kind survive the round-trip so any
* reaper consumer (the service's TiDB loop OR a non-service Pg deployment) implements the SAME contract.
*/
deadline?: number;
status: "pending" | "resolved" | "expired";
createdAt: number;
/**
* How many times THIS task has suspended, counting this checkpoint (1 = first suspend; design/72
* §2.2 (B)). Carried forward across resume→re-suspend (each new checkpoint = the prior count + 1), so
* a restart-prone model that keeps re-issuing the gated/egress call — re-suspending every resume —
* is capped: past `maxSuspends` the run fails (`suspend.loop`) instead of minting another checkpoint
* and looping forever. Absent ⇒ legacy `0` (pre-§2.2 checkpoints; treated as "no prior suspends").
*/
suspendCount?: number;
/**
* design/91 — the wall-clock instant (epoch ms, from {@link import("./types.js").RunnerDeps.now}) this task
* suspended at a HUMAN-REVIEW gate. Resume derives the human-review latency for this leg as
* `now() − suspendedAt` and folds it into `TaskResult.stats.humanReview` (the design/89 §2.4 C2 burden axis).
* The human-review family is the APPROVAL gates (`gate.kind` ∈ {`human`, `irreversible_ask`}) — core stamps
* this automatically on the approval-suspend mint — AND the REVIEW-PAUSE gates (`needs_review` = a dry-run
* diff review, `plan_review` = a pre-action PLAN review). Review-pause checkpoints are minted by a PROFILE
* (core owns only the routing/resume discriminants), so a profile that wants the C2 wait counted MUST stamp
* `suspendedAt = deps.now()` at mint — the same injectable clock the resume reads, so a test pins the latency.
* Absent on a `resource_limit` suspend (a machine backstop, not a human wait), on a review-pause checkpoint
* whose profile did not opt in, and on a legacy pre-design/91 checkpoint (⇒ resume skips the durable latency
* for that leg — no undercount surprise, just no entry).
*/
suspendedAt?: number;
/**
* design/91 — the accumulated {@link import("./types.js").TaskResult.stats}`.humanReview` from all PRIOR legs,
* carried forward across resume→re-suspend (like {@link suspendCount} / {@link resourceLedger}) so a
* multi-leg suspend/resume chain reports the WHOLE human-review burden, not just the last leg's. It holds
* the gates resolved up to and including the suspend BEFORE this one — the latency for THIS suspend is added
* by the resume that wakes it (`now() − suspendedAt`). Absent ⇒ no prior human-review time (a first suspend
* with no earlier synchronous asks). Budget-EXCLUDED side observable — never a cost/gate input.
*/
humanReview?: {
count: number;
totalWaitMs: number;
gates: import("./task-result.js").HumanReviewGateRow[];
};
/**
* design/80 D-1 (persist-winner): the winning resume binding, recorded by {@link CheckpointStore.resolve}
* when its CAS wins (pending → resolved) — present after a `policy_ask` resolve and after either REVIEW-gate
* resolve (`plan_review` / `dry_run_review`, RB-64); absent on a freshly minted pending checkpoint and on
* `resource_limit` / `wake` / `task_done` resolves. PRESERVED (never cleared) across a
* {@link CheckpointStore.reopen} so an `env_failed` re-resume can be validated against it. See
* {@link ResolvedOutcome}.
*/
resolvedOutcome?: ResolvedOutcome;
/**
* design/80 D-1 (reopen-by-reason): why this checkpoint was last reopened (resolved → pending), recorded
* by {@link CheckpointStore.reopen}. Drives re-resume validation (§3 invariant #1): `env_failed` ⇒ the
* re-resume must replay the persisted {@link resolvedOutcome} winner (a system retry of the approved
* action); `tool_unavailable` ⇒ a fresh operator decision is allowed (P-7 re-decide with the tool
* present). Absent on a never-reopened checkpoint (a first resume is unconstrained by reason).
*/
reopenReason?: ReopenReason;
/**
* design/80 D-1 (atomicity fix, round-2 BLOCKER): a **monotonic revision counter** bumped by EVERY
* {@link CheckpointStore.resolve} (won) and {@link CheckpointStore.reopen}. It is the optimistic-concurrency
* key ({@link ResolveExpectation}): a resume reads it at `get()` and `resolve` requires the live `rev` to
* still equal it, so any resolve/reopen cycle landing in the get→resolve window loses the CAS (fail-closed).
* Unlike the 2-valued `reopenReason`, it cannot ABA back to a prior value. Absent ⇒ legacy `0` (a checkpoint
* minted before this field; treated as never mutated).
*/
rev?: number;
/**
* design S1d (source-tag persistence): the issuing task's session id, stamped at the
* APPROVAL suspend mint so a supervisor inbox can attribute a paused/awaiting checkpoint to the worker that
* raised it (the durable analog of {@link import("./tool-policy.js").AskRequest}.sourceTaskId — the durable
* suspend path does NOT invoke `onAsk`, so the synchronous source identity is captured here instead). It is
* the SAME value the synchronous ask carries: `= sessionId` at the mint.
*
* **Security invariant (carried verbatim from the 1.113.0 ask-bubbling contract):** for a Runner-created
* DELEGATED subagent this is **worker-unforgeable** (a child never sets its own sessionId — the Runner mints
* it); a top-level caller MAY continue its own session id via {@link import("./types.js").TaskSpec.sessionId},
* so this is NOT a global "never forgeable" run id — aggregation only aggregates delegated workers, where it
* holds. **ECHO-ONLY triage metadata, NEVER a gate input:** no gate / CAS / winner / resume-validation path
* reads it (a contract-test pins this). Absent ⇒ no source task id (a non-ask gate, or a legacy checkpoint).
*/
sourceTaskId?: string;
/**
* design S1d (source-tag persistence): the issuing task's authenticated end-user {@link
* import("./types.js").TaskSpec.principal} (design/62), stamped at the APPROVAL suspend mint — and, since
* #449 G2, at the `resource_limit` mint too — so an aggregating inbox can attribute a paused checkpoint
* per-user — the durable analog of {@link import("./tool-policy.js").AskRequest}.principal. Caller-set
* (never a tool/worker arg), and like {@link sourceTaskId} it is **NEVER a gate input**: no gate / CAS /
* winner path reads it. The RESUME ENTRY however does read it as the row's identity record (#449 G2):
* a resume config that omits `principal` inherits this value for the resumed leg (usage-ledger bucket,
* later mints' scope derivation, ask attribution — a dropped field must not silently move the leg onto
* the SHARED usage bucket), and a resume config carrying a DIFFERENT non-empty principal is refused
* pre-CAS (`resume.principal_mismatch`). Absent ⇒ no principal (a legacy checkpoint, an unauthenticated
* task, or a gate kind that predates the stamp). Rolling-deployment note (#449 G2 residual): the field
* is NOT version-gated — an older worker accepts a principal-bearing row and keeps its historic
* behavior (no fallback, no mismatch refusal); the identity-continuity guarantee holds once the fleet
* is on enforcing workers. Version-gating it is a recorded design candidate: the lattice's newest rung
* (design/199) encodes "v>=9 ⟺ face section present", so ANY new version above it must rewrite that
* guard's window form and add its own presence invariant — a lattice-wide change, not a stamp.
*/
principal?: string;
/**
* design/153 §7.4 (1.389.0, RB-42②③): the VERBATIM `TaskSpec.durableApproval` opt-in in force at this
* APPROVAL suspend's mint — the engine-recorded provenance of "this task was durable-approval armed, with
* this scope and this reaper TTL". Consumed by the parked-resume drive's REVIVE-CONTINUITY re-derivation
* (subagent settle path): the drive spec carries no forwarding seat, so without this record a resumed leg's
* SECOND plain ask would headless-deny instead of re-parking. Reading it explicitly (instead of inferring
* the opt-in from `gate.kind === "human"`) covers a SAFETY-TIER first park (irreversible_ask — the kind
* alone never proved the spawn opted in) and carries `ttlMs` across the re-park so the re-minted
* checkpoint re-arms its reaper `deadline` (previously unbounded). Absent ⇒ the mint ran with NO
* `durableApproval` (an unattended safety suspend, a non-ask gate, or a legacy pre-1.389 checkpoint — the
* consumer then falls back to the human-kind scope-only inference for legacy rows and injects NOTHING for
* unattended parks, so a spawn that never opted in can never gain the opt-in through a resume). Engine-
* minted from the task's own spec — a worker/tool cannot self-declare it (`durableApproval` never rides tool args).
*/
durableApproval?: {
scope: string;
ttlMs?: number;
};
/**
* @contract checkpoint.execution_outcome — the settlement record of how the DECIDED action was
* finally disposed on the resumed leg: the {@link GateOutcome} the resumed call's `tool_end` frame
* carried (an executed allow, a person's refusal delivered, or a post-CAS belt's veto of a person's
* yes), written by {@link CheckpointStore.recordExecutionOutcome} exactly once per row, on every
* resolution (an `allowed` disposition is recorded too — the column's absence has ONE meaning:
* the execution outcome is not known, never "it was allowed"). Independent of
* {@link resolvedOutcome}, which stays the immutable record of the DECISION: a row can read
* `resolvedOutcome.gateOutcome.disposition.kind === "allowed"` beside
* `executionOutcome.disposition = { kind: "denied", deniedBy: "policy" }` — the person approved,
* the deployment's belt refused the edited call — and a reader rebuilding the run from the row
* sees both facts instead of the pre-execution one alone. The record is physically immutable:
* a second write of a different record is a conflict, never an overwrite, and a row carrying one
* cannot be {@link CheckpointStore.reopen | reopened} (the action is settled; a reopen would
* re-run or re-decide work whose outcome is already on file). Read through
* `executionVerdict` (checkpoint-execution-record.ts). Present together with {@link executionAtMs}.
*/
executionOutcome?: GateOutcome;
/** Epoch ms the store stamped {@link executionOutcome} at. Present iff `executionOutcome` is. */
executionAtMs?: number;
}
/**
* design/80 assistant-scheduler seam #1: a LIGHTWEIGHT projection of one PENDING
* checkpoint, returned in bulk by {@link CheckpointStore.listByScope} so a supervisor scheduler can list a
* scope's suspended/awaiting tasks in ONE call (no N+1 `get`s, no full-{@link Checkpoint} payloads). It is a
* read-only DISPLAY/triage summary — every field is DERIVED from the persisted {@link Checkpoint}; nothing
* here is a new source of truth or a gate input.
*/
export interface CheckpointSummary {
/** The resume capability token ({@link Checkpoint.token}). */
token: CheckpointToken;
/** Non-secret stable identity ({@link Checkpoint.checkpointId}) — absent on legacy rows. */
checkpointId?: string;
/** The suspended task / session id ({@link Checkpoint.sessionId}). */
sessionId: string;
/** The multi-tenant scope this checkpoint lives in ({@link Checkpoint.scope}) — always equal to the query's. */
scope: string;
/** The gate's discriminant ({@link CheckpointGate}.kind): which kind of pause this is. */
gateKind: CheckpointGate["kind"];
/** The deterministic risk tier (1..5) when the gate carries a {@link RiskDescriptor} (the `human` /
* `irreversible_ask` approval escalations), derived via {@link riskSeverity} from the descriptor's axes —
* the SAME value as `gate.riskDescriptor.severity`, recomputed from the single source helper rather than
* re-stored. `undefined` for a gate with no descriptor (resource_limit / needs_review / plan_review / a
* legacy human ask minted before D-E). The inbox sorts DESC by this. */
severity?: 1 | 2 | 3 | 4 | 5;
/** Cumulative spend debited to this suspend chain ({@link ResourceLedger.spentMicroUsd}) in micro-USD —
* present whenever the checkpoint carries a {@link Checkpoint.resourceLedger} (every resource_limit suspend,
* and every approval suspend on the D-E-core budget-read path); `undefined` when no ledger is attached. */
spentMicroUsd?: number;
/** The awaiting-human SLA / abandonment deadline ({@link Checkpoint.deadline}, epoch ms) when present. */
deadline?: number;
/** design S1d (source-tag): the issuing worker's session id ({@link Checkpoint.sourceTaskId}) — projected so
* a supervisor inbox attributes each pending checkpoint to its worker in ONE `listByScope` call (no N+1
* `getCheckpoint`). ECHO-ONLY display/triage; absent on a non-ask gate / legacy checkpoint. */
sourceTaskId?: string;
/** design S1d (source-tag): the issuing task's end-user principal ({@link Checkpoint.principal}) for per-user
* inbox attribution. ECHO-ONLY display/triage; absent when the checkpoint carries no principal. */
principal?: string;
/** The pending tool call's id ({@link PendingAction} `tool_approval.toolCallId`) when this is an approval
* suspend — projected so the inbox shows WHICH call awaits a decision without an N+1 `getCheckpoint`.
* `undefined` for a non-`tool_approval` pendingAction (resource_limit / plan_review / task_done).
*
* **ABSENCE SHAPE (contract)** — shared by every optional field on this projection: a
* tool-less park (`resource_limit` / `plan_review` / `task_done` — none of which binds a tool call)
* gets the key **OMITTED**, never `null` and never a present-but-`undefined` property. The
* `PendingAction` arms for those kinds declare no tool/binding fields at all, so there is nothing to
* project; `summarizeCheckpoint` spreads them conditionally for exactly this reason. A consumer
* therefore tests presence (`"toolCallId" in summary`), and a re-projecting service (BFF / SDK wire
* face) must not materialize the keys as `null` on the way out. Pinned in
* `test/checkpoint-store.test.ts` ("tool-less park …"). */
toolCallId?: string;
/** The pending tool's name ({@link PendingAction} `tool_approval.toolName`) when this is an approval suspend.
* OMITTED (not `null`) for a non-`tool_approval` pendingAction — see {@link toolCallId}'s absence-shape note. */
toolName?: string;
/** Which approval card the pending call renders under — the gate's {@link CheckpointGate} `approvalCard` echoed when
* it is a member of the closed set `TOOL_APPROVAL_CARDS` (a stored word outside the set is no card). The one typed
* discriminant a shell routes on: `"question"` is a question TO the user (the reserved AskUserQuestion tool, whose
* typed questions ride the drilled-in Checkpoint's `pendingAction.args`, kept off this lightweight projection), not a
* side-effecting tool; the other words pick the card family. Present on an approval suspend (`human` /
* `irreversible_ask`) that carries one; OMITTED for a row parked before the member and for every non-approval park —
* see {@link toolCallId}'s absence-shape note. */
approvalCard?: import("./tool-face.js").ToolApprovalCard;
/** design S1e: when the checkpoint was created ({@link Checkpoint.createdAt}, epoch ms) —
* projected so a supervisor inbox can sort/age pending entries (oldest-first triage) in ONE `listByScope`
* call, no N+1 `getCheckpoint`. ECHO-ONLY display/triage; always present (a Checkpoint always has a
* `createdAt`). */
createdAt?: number;
/**
* design S1e: a **BOUNDED, UNREDACTED** raw preview of the pending tool call's input
* ({@link PendingAction} `tool_approval.args` `JSON.stringify`-ed, truncated to {@link
* MAX_TOOL_INPUT_PREVIEW_CHARS} chars with a `…` marker when over) — projected so a supervisor inbox shows
* WHAT a paused tool call will do without an N+1 `getCheckpoint`. `undefined` for a non-`tool_approval`
* pendingAction (resource_limit / plan_review / task_done — no args to preview).
*
* **Contract (load-bearing):**
* - **bounded raw preview** — length-capped to {@link MAX_TOOL_INPUT_PREVIEW_CHARS} so it cannot bloat the
* `listByScope` payload, but the chars within the cap are the RAW serialized args (no folding/escaping).
* - **redaction is the CONSUMER's responsibility** — core gives the inbox the unredacted bounded raw; the
* service/inbox decides what (if anything) to mask before showing a human. (Contrast {@link
* RiskDescriptor.summary}, which is the NEUTRALIZED+capped descriptor that DOES ride into a render —
* `toolInput` is the raw-data sibling for a consumer that wants the actual args.)
* - **same data as the full {@link Checkpoint}** — it is a projection of `pendingAction.args`, which the
* caller could already read via `get`; this adds NO new exposure, it only saves the round-trip.
* - **ECHO-ONLY, like {@link sourceTaskId}** — NO gate / CAS / resume / {@link winnerFromOutcome} path reads
* it (a contract test pins this); it is pure display/triage metadata, never a control input.
*/
toolInput?: string;
/**
* HOW this pause's remote workspace comes back, projected from
* `state.workspaceHandle.restoreMode` so a supervisor inbox can tell the two apart in ONE `listByScope`
* call. `"snapshot"` = the VM was paused into a snapshot (billing typically stops, memory state
* captured); `"park_only"` = a non-suspendable target (SSH host / ADB device) was never paused — the
* machine is still running and still costing, and resume just reconnects. OMITTED (see the
* {@link toolCallId} absence-shape note) for a process-local suspend, which captured no remote
* workspace at all. ECHO-ONLY display/triage — nothing gates on it. Mirrors `TaskResult.workspaceRestoreMode`.
*/
restoreMode?: "snapshot" | "park_only";
/** the tool's approval display projection ({@link PendingAction} `preview`),
* projected so the ONE-CALL inbox (`listByScope`) can render a rich confirmation without an N+1
* `get`. Same trust posture as `toolInput`: **UNTRUSTED, advisory display metadata** (strings are
* sanitized of control chars at mint but may still carry model-authored markdown — renderers must
* contextually escape), ECHO-ONLY (no gate/CAS/resume reads it), and surfaces SHOULD show the
* bound raw args (`toolInput`/`pendingAction.args`) alongside — the preview never replaces them. */
preview?: unknown;
/** #457 ② — the row's `PendingAction.tool_approval.previewWithheld`, projected BESIDE {@link preview}
* for the same reason that one is: without it the ONE-CALL inbox cannot tell "this tool declares no
* preview" from "the projection failed", which is the exact collapse the determinant exists to
* remove — and re-opening the row with an N+1 `get` just to answer it defeats `listByScope`.
* `"oversize"` (produced but past the 16KiB mint bound — the truncation stub still rides
* {@link preview}) or `"unavailable"` (threw / no JSON serialization). The {@link toolCallId}
* ABSENCE-SHAPE contract applies: OMITTED otherwise, never `null`/`false`/present-but-`undefined`.
* A pure echo of the row's own persisted value — unlike {@link hasBidiControls}, there is no
* projection-time backfill, because whether a tool DECLARED a preview is not knowable from the
* projection's own materials (a pre-#457 row therefore reads absent, which is honest: unreported).
* ECHO-ONLY display metadata; nothing gates on it. */
previewWithheld?: "oversize" | "unavailable";
/** The row's `PendingAction.tool_approval.requiresRealApproval`, projected so the ONE-CALL
* inbox can label "only a person can clear this" without an N+1 `get`. `true` or OMITTED (the
* {@link toolCallId} absence-shape contract); pure echo of the persisted bit — a pre-#557 row reads
* absent, which is honest (unreported), never a claim that budget may clear it. */
requiresRealApproval?: true;
/** The row's `PendingAction.tool_approval.mandated` (#1079), for the same one-call reason: `true` or
* OMITTED (the {@link toolCallId} absence-shape contract), screened to the literal `true` — a pre-#1079 row
* reads absent (unreported). ECHO-ONLY; nothing gates on it. */
mandated?: true;
/** The row's `PendingAction.tool_approval.authoredBy`, echoed as the one literal. ECHO-ONLY. */
authoredBy?: "deployment";
/** The row's `PendingAction.tool_approval.mandate`, echoed ONLY as a member of the closed set. ECHO-ONLY. */
mandate?: import("./ask-origin.js").PersistedRuleMandate;
/** The row's `PendingAction.tool_approval.denialLimitFallback`, projected BESIDE the preview
* for the same one-call reason: a card that shows the fallback's counts and limit must not re-open
* the row to learn them (`autoDenyAfterMs` is `0` on every row — see the row member). Pure echo;
* OMITTED when the row carries none. */
denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
/** The row's `PendingAction.tool_approval.classifierNoRuling`, echoed ONLY as a member of its closed set (else omitted). */
classifierNoRuling?: import("./auto-mode.js").AutoModeFallbackCause;
/** The row's `boundGrantDigest`, echoed (a string; else omitted): the hash of {@link boundGrants}. ECHO-ONLY. */
boundGrantDigest?: string;
/** The row's `boundGrants` — a fresh copy of the skill and its two lists, published only when it hashes to {@link boundGrantDigest} (never a list beside a digest that binds another). Deployment text: render with contextual escaping. ECHO-ONLY. */
boundGrants?: import("./skill-grant-binding.js").BoundSkillGrants;
/** Every `tool_approval` summary carries {@link boundGrantDigest} or this, never both: the row's word when it is a member and the row has no digest, else `unreadable` (the row does not say what it binds). ECHO-ONLY. */
boundGrantAbsence?: import("./skill-grant-binding.js").BoundGrantAbsence;
/** The row's `PendingAction.tool_approval.origin`, projected for the same one-call reason: a card
* rendered by the ask's origin word must not re-open the row to learn it. Echoed ONLY when the stored
* value is a member of the closed set ({@link import("./ask-origin.js").isAskOrigin}) — a bad value
* reads as absent, never as a word. OMITTED when the row carries none (a pre-existing row). */
origin?: import("./ask-origin.js").AskOrigin;
/**
* PRESENCE ONLY — this pending `tool_approval` row's payload carries at least one DIRECTIONAL
* format control (the {@link import("./tool-policy.js").AskRequest.hasBidiControls} character class;
* that doc owns the hazard). Only ever for a `tool_approval` pendingAction; OMITTED for every other
* kind (the {@link toolCallId} ABSENCE-SHAPE contract applies: never `null`, never present-but-
* `undefined`, never `false` — a re-projecting BFF must not materialize any of those on the way out).
*
* WHERE THE ANSWER COMES FROM: the row's own persisted bit when present (the park mint is the
* authority — one look, one answer, never re-derived); else a bounded BACKFILL scan run AT PROJECTION
* TIME over this projection's OWN bounded materials — the truncated {@link toolInput} string just
* derived plus the row's `preview`, under a small per-row budget — so a pre-#438 row and a
* deployment-written row still get a bit exactly where the inbox reads. The backfill judges THE BYTES
* THIS PROJECTION HANDS THE INBOX: a marker buried past the `toolInput` truncation point (or past the
* per-row scan budget) is honestly reported as absent — new-engine-minted rows carry the mint-time bit
* (judged over the full parked args) for exactly that reason. The backfill NEVER writes back to the
* row: `summarizeCheckpoint` stays a pure projection.
*
* HOW TO READ IT: `true` = core detected the class in this row's payload (or took a deployment row's
* own `true` at its word — over-warning is the safe direction); render a visible "the text below can
* display differently from what it runs" annotation. ABSENT = NOT DETECTED, never "clean" — it covers
* clean, budget-exhausted, and a `false`-writing out-of-contract producer (read as absent, never
* projected). The bit changes NO rendering obligation a surface already had: with or without it,
* `toolInput` / `toolName` / a summary string go through
* {@link import("./permission-rule-model.js").renderUntrustedCommandText} (or the surface's own
* screen) before a human reads them; for the structured `args`/`preview` the FIRST-CHOICE display
* material is the bounded projections core already minted (`toolInput`, the descriptor `summary`) —
* do not re-serialize the whole args for a list render, and a drill-in that must show everything owes
* itself a TOTAL, bounded traversal (cycle-safe, per-type output forms, throwing getters to a
* placeholder, depth/node/char budgets with a truncation marker — never "full `JSON.stringify` then
* truncate", which does all the work and takes all the throws before the cap). ECHO-ONLY, like
* {@link sourceTaskId}: a warning to a person, never a control input — resume/gate/CAS never read it.
*/
hasBidiControls?: true;
}
/**
* design/80 assistant-scheduler seam #1: project a single PENDING {@link Checkpoint} to its lightweight
* {@link CheckpointSummary}. Shared by every {@link CheckpointStore} impl so the projection is IDENTICAL
* across the in-memory, file, and durable backends (the anti-drift guard) — `severity` always derives from the
* gate's {@link RiskDescriptor} via the single {@link riskSeverity} helper, `spentMicroUsd` always reads
* {@link ResourceLedger.spentMicroUsd}. Pure; reads no clock/random.
*/
export declare function summarizeCheckpoint(cp: Checkpoint): CheckpointSummary;
/**
* WHICH arm of a multi-arm {@link CheckpointError} `code` fired (requested 2026-08-10: a deployment retry
* policy needs to tell "a newer worker can redeem this row" from "this row is damaged/caller-declared and
* no worker ever will" — blanket-retriable and gate-shape heuristics were both refuted downstream, so the
* throw site carries the fact it always knew).
*
* A CLOSED SET with one home, registered in `docs/CLOSED-SETS.md`. It is carried in an OPTIONAL member, so
* the compiler is silent when a throw site omits it, and a site that omits it hands the host a refusal it
* cannot classify — the registry's mint-obligation leg is what makes the omission loud (every
* `checkpoint.unsupported_version` construction site must spell one of these words AT the site).
*
* · `unsupported_version` arms: `"version_newer"` (the row's format is newer than this worker's, or it lists a
* reader-requirement word this worker lacks — `detail.requires` names those words — retryable on a newer
* worker), `"env_factory_missing"` (retryable on a factory-wired worker),
* `"governed_unwired"` (retryable on an org-wired worker), `"origin_missing"` (a pending approval row
* minted before the ask's origin was persisted on the park — terminal for the row's bytes; the host
* decides the orphaned card itself, no worker version redeems it), `"ledger_event_unknown"` (a durable
* LEDGER carries an event kind this binary does not know, so the replay is refused rather than served
* incomplete — the one arm that is not row-scoped: NO row in that data directory is readable here, so
* the host rolls the binary forward instead of routing one token elsewhere), `"seat_retired"` (the row
* carries a seat this engine no longer reads — terminal for the row's bytes: the retired pre-queue single
* steer seat, which no worker version reads and the host decides itself, or a retired mode-seed member of
* `inheritedGate`, which an engine at 7.38.x or earlier still reads once).
* · `invalid_outcome` pre-CAS row-integrity arms: `"real_approval_damaged"`, `"real_approval_forged"`,
* `"read_face_damaged"`, `"read_face_forged"`, `"write_face_damaged"`, `"skill_grants_damaged"`, `"constraint_chain_missing"`, `"hands_coordinates_damaged"`, `"requirements_damaged"` (all terminal for the row's
* current bytes — no worker version redeems a damaged row).
*/
export type CheckpointErrorReason = "version_newer" | "env_factory_missing" | "governed_unwired" | "origin_missing" | "ledger_event_unknown" | "seat_retired" | "real_approval_damaged" | "real_approval_forged" | "read_face_damaged" | "read_face_forged"
/** `state.writeFace` is present but not a well-formed section. No `_forged` twin: the section is
* legal-and-absent at every version, so there is no version to judge a fabrication against — and a
* fabricated "open" still needs the LIVE resolution to say open too. */
| "write_face_damaged" | "skill_grants_damaged" | "constraint_chain_missing"
/** `state.handsRoot` / `state.handsHome` / `state.handsCwd` is present but not an absolute path (the row's hands' coordinates). */
| "hands_coordinates_damaged"
/** The row's reader-requirement list ({@link CheckpointRow.requires}) is missing on a v10 row, is not a list of words,
* is present below v10, or does not equal the words the row's content carries (a segment dropped, or a word not
* declared) — terminal for the row's bytes. */
| "requirements_damaged"
/** On `resume.parent_constraint_mismatch`: a re-supplied chain entry whose path rules depend on the leg that installed it has
* no installation record (a chain written before the record existed) / a record that does not cover the chain entry for
* entry / a record member naming no absolute root. */
| "installation_record_absent" | "installation_record_misaligned" | "installation_record_invalid"
/** On `checkpoint.config_refused` with `detail.code` `config.tool_face_control` — the arm prepare's own refusal carries as its `detail.reason`:
* a tool-name list the row recorded is not a list / has a member that is not a string / has a spelling no tool name can have / is not in
* the one spelling a row records (`"not_a_list"`, `"not_a_string"`, `"unknown_name"`, `"unsupported_pattern"`; the re-supplied config's own four lists —
* `allowTools`, `excludeTools`, `deferTools`, `alwaysLoadTools` — are refused as `"not_a_list"` / `"not_a_string"`); the record of the delegation tool that
* wrote the leg's lists is not one (`"invalid_record"`); the roster the resume hands mounts a delegation tool under another name than it declares
* (`"renamed_mount"`); the process-wide table of declared names is occupied by a value that is not the engine's (`"registry_occupied"`). */
| "not_a_list" | "not_a_string" | "unknown_name" | "unsupported_pattern" | "invalid_record" | "renamed_mount" | "registry_occupied"
/** On `checkpoint.config_refused` with `detail.code` `config.unknown_role` — a role seat or a tier binding the deployment wrote that the engine cannot read:
* a word the model catalog does not hold (`"unknown_model_ref"`), or a value that is no model reference at all — a number, a boolean, a list
* (`"not_a_model_ref"`). The sentence names the seat (`roles.consolidate`, `tiers.pro`). */
| "unknown_model_ref" | "not_a_model_ref";
/**
* The codes a RESUME's pre-claim configuration screen carries as `detail.code` of a `checkpoint.config_refused` — the words prepare's
* doors throw, held unchanged so one refusal reads the same on a fresh run (`TaskResult.errorCode`) and on a resume: the mode, output-style,
* approval-reach, invoked-skill, attachment, recorded-mode and tool-face doors (`config.spec_retired_key` … `config.tool_face_control`, the first
* nine — `detail.reason` names the tool-face arm: {@link CheckpointErrorReason}), and every value door of the config-door phase over the
* re-supplied config and the deployment's deps (`configValueDoors`, the rest): the retired deps seats and caller-policy members
* (`config.deps_retired_key`), the peer presence, the restore selector, the microCompact knob, the prompt selectors, `resumeAt` on a resume
* (`resume_at.conflicts_resume`, `resume_at.mode_without_target` — a resume carrying `resumeAt` is refused as a conflict before its mode is
* read, so `resume_at.invalid_mode` is a fresh run's word only), the containment faces, the add-only seats and the MCP / A2A `toolAxes` (`config.invalid_argument`), the three memory seats,
* the auto-consolidation arming (its role key too, and the role seats and tier bindings it reads: `config.unknown_role`, `detail.reason` naming the arm), the materialize strategy and persistence flag, the lock layer (`config.locked_registry`,
* `config.locked_key`), an MCP server's content class, the skills-listing seats (`config.skill_listing_budget_invalid`), the limits and the
* resource-suspend knobs, the attachment modes, the governance windows, the brain-call guardrail and the retention declaration over the stores the leg
* would run on (`config.retention_policy`, `config.retention_capability`), and the checkpoint store seat itself (`config.invalid_checkpoint_store`: the
* retired `null` spelling, a store that declares no execution outcome), and the skills seat and the hands band (`config.invalid_argument`;
* a skill's `baseDir` by its own reader, `config.skill_base_dir_invalid`). A door not entered here (the leg's model, its rebuilt
* roster, its answer face, the approval seat's value) still refuses after the claim, each for a stated reason (INTEGRATION-CORE, "A resume's two doors").
*/
export declare const CHECKPOINT_CONFIG_REFUSAL_CODES: readonly ["config.spec_retired_key", "config.output_style_invalid", "config.permission_mode_invalid", "config.shell_gate_value_retired", "config.approval_reach_invalid", "config.user_invoked_skill_unknown", "config.skill_deny_unreadable", "config.user_attachment_invalid", "config.tool_face_control", "config.deps_retired_key", "config.peer_presence_invalid", "config.tool_model_gate_invalid", "config.microcompact_invalid", "config.prompt_selector_invalid", "resume_at.conflicts_resume", "resume_at.mode_without_target", "config.read_face_invalid", "config.write_face_invalid", "config.invalid_argument", "config.memory_delegation_evidence", "config.memory_provenance", "config.memory_capture_policy", "config.auto_consolidation", "config.unknown_role", "config.tool_materialize_invalid", "config.memory_persistence_invalid", "config.tool_materialize_unreachable", "config.locked_registry", "config.locked_key", "config.mcp_content_class", "config.skill_listing_budget_invalid", "config.limit_unknown_key", "config.limit_invalid", "config.attachment_invalid", "config.usage_window_invalid", "config.brain_call_guardrail_invalid", "config.retention_policy", "config.retention_capability", "config.invalid_checkpoint_store", "config.skill_base_dir_invalid"];
export type CheckpointConfigRefusalCode = (typeof CHECKPOINT_CONFIG_REFUSAL_CODES)[number];
export declare const isCheckpointConfigRefusalCode: (word: unknown) => word is CheckpointConfigRefusalCode;
/** A typed checkpoint-store error so callers branch on `code` (mirrors `SessionError`). */
export declare class CheckpointError extends Error {
readonly code: "checkpoint.already_exists" | "checkpoint.already_resolved" | "checkpoint.not_found"
/** `runner.resume` was handed an {@link ResumeOutcome} whose `gate` arm does not match the
* persisted {@link CheckpointGate} `kind` (council #3 — type confusion guard), or a gate v1
* resume does not serve (`task_done`). */
| "checkpoint.gate_mismatch"
/** The resume's `TaskSpec.signal` was aborted at the entrance — handed in ALREADY-ABORTED, or aborted
* while the wake screen / `resumePreflight` / pre-claim re-adjudication still ran: ONE code for a
* cancellation there (the seats' own codes keep what the hook itself did). Refused BEFORE the CAS, so
* the checkpoint stays `pending`; consuming the decision on a leg that aborts at once would lose it. */
| "checkpoint.resume_aborted"
/** `runner.resume` was handed an outcome whose payload is unsafe to apply — e.g. a deny `reason`
* carrying a `</system-reminder>` tag that would escape the model-facing wrapper (round-2 #1). */
| "checkpoint.invalid_outcome"
/** The checkpoint's {@link Checkpoint.version} is newer than this worker supports
* (`> MAX_SUPPORTED_CHECKPOINT_VERSION`), or it carries a remote `workspaceHandle` but no
* `executionEnvFactory` is wired to rebuild the env — either way this worker cannot safely resume it.
* Rejected pre-CAS so the checkpoint stays `pending` and a capable worker can still resume it
* (design/49 §2/§3, code-ready council round-2). */
| "checkpoint.unsupported_version"
/** The resume's re-supplied config, or a mode the checkpoint row recorded, is one prepare would refuse
* (a retired spec key, a malformed output style, a mode word outside the closed set, a retired shell-floor
* word). Refused PRE-CAS, so the checkpoint stays `pending` and byte-identical. `message` is prepare's own
* sentence; `detail.code` is prepare's own code ({@link CheckpointConfigRefusalCode}) and `detail.field`
* says which input carries it: `"taskConfig"` (correct the config and resume the SAME token) or
* `"inheritedGate"` (the recorded row — no config reaches it; expire the row). A fresh run of the same
* bad config reports the same `detail.code` as its `TaskResult.errorCode`. */
| "checkpoint.config_refused"
/** `resume`/`resumeStream` was handed `internals.requestedCwd` for a checkpoint that
* carries a remote `workspaceHandle`. The restored workspace's mountPath is authoritative for the
* task root, so the cwd would be ignored at best — and verifying it would touch the env BEFORE the
* VM restore (a lazy-connect adapter then connects a fresh empty sandbox, the design/49 disease).
* Rejected pre-CAS so the checkpoint stays `pending`; re-resume without `requestedCwd`. */
| "checkpoint.cwd_conflicts_restore"
/** design/164: the checkpoint was written by a pre-164 worker AND its ledger carries the RETIRED
* cross-slice wall-clock allocation (`resourceLedger.totalWalltimeSec`). The axis no longer exists,
* so resuming would run the leg with the operator's time ceiling silently unenforced. Refused
* PRE-CAS (the row stays `pending`, so a worker of the previous release can still finish it) rather
* than migrated: there is no honest conversion from a wall-clock allocation to a token one. */
| "checkpoint.walltime_axis_retired"
/** design/80 D-1 (reopen-by-reason): a re-resume of an `env_failed`-reopened checkpoint supplied an
* outcome that does NOT equal the persisted {@link ResolvedOutcome} winner. An `env_failed` reopen is
* a SYSTEM RETRY of the already-approved action (the infra failed, not the decision) — it must replay
* the exact winning binding, never a new vote. Rejected pre-CAS, fail-closed, so the checkpoint stays
* `pending` for the correctly-replayed retry. (A `tool_unavailable` reopen — P-7 — is exempt: it lets
* a human re-decide with the tool present, so a fresh decision is permitted there.) */
| "checkpoint.reopen_revote"
/** design/80 D-1 (atomicity fix): the resume validated its decision-action / reopen-by-reason guards
* against a `get()` snapshot, but a concurrent {@link CheckpointStore.resolve}/{@link CheckpointStore.reopen}
* cycle advanced the monotonic {@link Checkpoint.rev} in the get→resolve window, so the
* {@link CheckpointStore.resolve} CAS's optimistic-concurrency check lost (the validation is stale). The
* row is still `pending` — fail-closed; the caller must re-`get` and re-validate against the CURRENT state
* (a fresh resume), never blindly retry. */
| "checkpoint.reopened_concurrently"
/** A post-CAS compensation {@link CheckpointStore.reopen} returned `false` — the consumed row could not
* be flipped back to `pending` (missing, expired, or changed under a concurrent cycle), so no retryable
* checkpoint exists. Surfaced instead of silently treating the failed reopen as committed, so the
* caller's compensation path settles the token as terminally consumed. */
| "checkpoint.reopen_failed"
/** {@link CheckpointStore.recordExecutionOutcome} met a row that already carries a DIFFERENT
* execution record — see `ExecutionOutcomeConflictError` (checkpoint-execution-record.ts), the
* in-tree form of this code. Never a retry condition. */
| "checkpoint.execution_outcome_conflict"
/** design/80 D-A: {@link CheckpointStore.setPendingSteer} was handed `text` containing a
* `</system-reminder>` close tag — it would escape the model-facing `<system-reminder>` wrapper a
* trusted steer rides on resume. Rejected fail-closed so a dirty steer NEVER enters {@link CheckpointState}
* (§3 inv #2). Mirrors the trusted-steer reject at runtask.ts (`steering.invalid_content`). */
| "steering.invalid_content"
/** design/171 §6.3: the append would cross the parked-steer queue's entry-count or byte bound
* ({@link MAX_PENDING_STEER_ENTRIES} / {@link PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES}). Fail-loud
* rather than evicting: an already-accepted operator instruction is never dropped to make room —
* the caller delivers or removes a parked steer first. Nothing is written on this path. */
| "steering.queue_full"
/** design/171 §6.3 (falsification review adoption): the append reused an `inputId` already parked on
* this checkpoint, but with DIFFERENT content. Re-appending an IDENTICAL steer stays the idempotent
* no-op a retry needs; a differing one is refused, because swallowing it would lose the second
* operator's instruction — the exact silent loss the queue replaced. `detail.differs` names the facets
* that differ: with `text` among them it is another instruction (retry it under a fresh id); without,
* it is the parked one re-sent at another tier / trust / attribution (a fresh id would park it twice —
* replay it with its original values). Nothing is written on this path. */
| "steering.duplicate_input_id"
/** design/373 §4.3 (D2): the deployment's `userPromptSubmit` hook refused a wake resume's
* `message` at the entrance screen (block verdict, or a fail-closed timeout/crash — the
* message discriminates). Pre-CAS: the checkpoint stays `pending` and byte-identical, so the
* SAME token is redeemable once the screen clears — a refusal burns nothing. The live steer
* verb refuses under the same code word (its own Error face); one vocabulary, two entrances. */
| "steering.blocked_by_hook"
/** design/144 §3 (governance-gate purity, fail-closed pre-CAS): a `wake` outcome was supplied for a
* checkpoint that awaits a PENDING GATE DECISION (tool approval / dry-run review / plan review /
* resource-limit continue). Wake is a pure un-park verb and must never circumvent an approval — resume
* the checkpoint through its own decide entry instead (the error message names it). */
| "wake.gate_pending"
/** design/144 §3 (anti spin-wake): a `wake` outcome carried no `message` and the checkpoint has no
* parked steer either — there is nothing to deliver, so the wake is rejected pre-CAS
* (the checkpoint stays pending) instead of burning the token on an empty continuation. */
| "wake.nothing_to_deliver"
/** Parent effective-policy inheritance: the checkpoint records `state.inheritedGate.requiresParentConstraint`
* (the suspended task ran under inherited OPAQUE parent-policy constraints — live closures that cannot
* persist), but the resume supplied no `RunInternals.inheritedGate.parentConstraints` to re-instate them.
* Resuming anyway would silently DROP the ancestors' gate (a widening), so this rejects pre-CAS — the
* checkpoint stays `pending`; re-resume via `resumeStream(token, outcome, config, internals)` with the
* parent chain re-supplied. */
| "resume.parent_constraint_missing"
/** Parent effective-policy inheritance (shape check): the resume DID re-supply
* `internals.inheritedGate.parentConstraints`, but its length differs from the checkpoint's recorded
* `parentConstraintCount` — or (F-012 L1) its projection CONTENT digests differently from the
* recorded `constraintDigest` — or a screening entry names a `tool.check` guard beside a policy the
* constraint mint did not make (no chain to raise it on) — a partial/mismatched re-supply would silently run
* the child under a thinner (or different) ancestor chain than it suspended with. Rejected pre-CAS (the
* checkpoint stays `pending`); re-resume with the full original chain. */
| "resume.parent_constraint_mismatch"
/** #449 G1 (codex r2, pre-CAS entrance): a deployment usage-governance window for this resume's
* ledger key (the config's principal, else the row's recorded one, else the shared bucket) is
* EXHAUSTED, and the row still OWES a delivery a re-mint cannot carry (an approval/review
* decision, a wake, parked steers, a background-task obituary snapshot). Refused BEFORE the CAS,
* so nothing is consumed and nothing is unpinned — the SAME token with the SAME decision is
* redeemable once the window frees (`detail.retryAfterMs` carries the wait). A bare
* `resource_limit` continue with nothing parked does NOT take this refusal: it proceeds and
* re-suspends at turn 0 into a FRESH checkpoint carrying the new wait hint. */
| "resume.usage_window_exhausted"
/** #449 G2 (identity continuity): the resume config carries a NON-EMPTY `principal` that differs from
* the {@link Checkpoint.principal} recorded at the suspend mint. `ResumeTaskConfig`'s contract is
* "the same TaskSpec the run suspended with", and the principal keys real lanes on the resumed leg
* (the usage-governance ledger bucket, later mints' scope derivation, session-rule reads, ask
* attribution) — silently running the leg under a different identity is the bad-value direction this
* refusal exists to close. Rejected PRE-CAS (the checkpoint stays `pending`); re-resume with the
* original principal, or omit the field to inherit the recorded one. */
| "resume.principal_mismatch"
/** design/380 O1③ (placement identity continuity — the `resume.principal_mismatch` rung's twin
* on the placement axis): the resume supplied trusted internals whose EXPLICIT
* `placementRoot` differs from the {@link CheckpointState.placementRootSessionId} recorded at
* the suspend mint. A target-bound env factory keys its placement lookup on that fixed point,
* so silently resuming under a different one would re-place the leg — and every descendant
* the child-internals chain hands it to — on another target than the parked workspace lives
* on. A supplied `rootSessionId` deliberately does NOT take this refusal: it is
* delegation identity, re-supplied by every revive leg, and legitimately differs from the
* recorded root whenever the spawner carried an explicit placement — the recorded root simply
* outranks it in prepare's restore fold. Rejected PRE-CAS (the checkpoint stays `pending`);
* re-resume with the recorded root, or omit `internals.placementRoot` to inherit it. Either
* side absent (an older row / a claim-less resume) skips the check, and `""` reads as absence
* — the honest degradation arm. */
| "resume.placement_mismatch"
/** design/380 O2: the deployment's `RunnerDeps.resumePreflight` refused this resume — an
* explicit refusal verdict, a throw, a timeout past its bounded deadline, or an unreadable
* answer (all fail-closed). The LAST rung before the CAS, so nothing was consumed. Two arms,
* discriminated by the message: the default `retry_later` arm leaves the checkpoint `pending`
* and the SAME token redeemable once the obstacle clears (`detail.retryAfterMs` carries the
* deployment's wait hint when it gave one — the #449 G1 carrier); the explicit `terminal` arm
* has ALREADY settled the row out of `pending` via the single-shot `expire` CAS (reaper
* parity) before this throw — the token is NOT redeemable, by the deployment's own permanent
* verdict (a dead binding must not be redialed forever). */
| "resume.preflight_rejected"
/** F-012 L3: the resume carried an approver EDIT (`updatedInput`) and a FROZEN inherited ancestor
* constraint (the persisted projection chain) — or the deployment's resume-edit policy — DENIES the
* edited args. Refused PRE-CAS with fresh-redecision semantics: the checkpoint stays `pending` and
* the same token remains decidable (approve as shown, deny, or edit differently) — the rejection
* consumed the decision's INPUT, never the ask itself. */
| "resume.constraint_rejected"
/** F-012 L3: the resume carried an approver EDIT but the inherited ancestor chain cannot be
* re-adjudicated statically — an OPAQUE layer is present, a layer declares a live remainder beyond
* its projection, or the checkpoint predates the projection chain. "Would have been allowed" is not
* computable from a self-reported closure, so the edited resume is refused typed pre-CAS (the
* checkpoint stays `pending`, still decidable); an UNEDITED approval is untouched. */
| "resume.constraint_unprojectable";
/** Structured discrimination a caller can branch on where the code alone is ambiguous. `field`
* names WHICH part of the decision payload was rejected on a
* `checkpoint.invalid_outcome` — `"boundCallId"` (the action you decided on has been replaced:
* re-fetch the pending list), `"boundInputHash"` (the input you reviewed has changed under the same
* action: re-review), `"boundGrantDigest"` (a parked `Skill` approval does not cover the skill the decision selects — another skill, or the card's with other lists: deny, re-activate), or `"answer"` (the content-ask answer is missing, or was attached to a
* decision that cannot carry one: re-decide with a well-formed answer — ruled 2026-08-04), or
* `"hostDecision"` (a tool approval's or a plan pause's decision facts are missing, name a decider outside
* `person`/`sla_timeout`/`policy`, pair the sweep's `sla_timeout` with an allow or put it on a row a sweep does not
* resolve, or try to supply the minted record), or — design/252 G-7 — `"approver"` (the attribution is not a
* well-formed identifier: re-decide with a plain, bounded, control-character-free string, or omit
* it). On a `checkpoint.config_refused` `field` names which INPUT carries the refusal — `"taskConfig"`
* (the re-supplied config: correct it and resume the same token), `"deps"` (the deployment's own seat — and the policies the
* caller re-supplies as inherited constraints: correct them and resume the same token) or `"inheritedGate"` (the row's record: no config reaches it; expire the row). The throw
* sites always knew which arm fired; this carries that fact instead of dropping it (ruled
* 2026-08-03, cross-repo HITL discussion). Additive and optional — `code` remains the only REQUIRED
* discriminant. */
readonly detail?: {
field?: "boundCallId" | "boundInputHash" | "boundGrantDigest" | "answer" | "hostDecision" | "approver" | "taskConfig" | "deps" | "inheritedGate";
/** On `checkpoint.config_refused` — prepare's own code for the refusal ({@link CheckpointConfigRefusalCode}), the
* word a fresh run of the same bad config reports as its `errorCode`. Absent on every other code. */
code?: CheckpointConfigRefusalCode;
/** #449 G1 — on `resume.usage_window_exhausted`: how long until the binding window frees
* (the same wait a suspended row carries as `gate.resumeAfterMs`). design/380 O2 — on a
* retry-later `resume.preflight_rejected`: the deployment preflight's own wait hint, when it
* gave one (same carrier, same semantics: retry the SAME token after the wait). Absent on
* every other code. */
retryAfterMs?: number;
/** WHICH arm fired, where one `code` covers several — the word list and what each word means to a
* host live at {@link CheckpointErrorReason}, its one home. Additive and optional like `field`
* (`code` remains the only REQUIRED discriminant), which is exactly why the obligation to SPELL it
* is a gate rather than a type: see the closed set's own doc. */
reason?: CheckpointErrorReason;
/** On a `version_newer` refusal of a v10 row: the reader-requirement words the row lists that this worker lacks
* (sorted, display only). Absent on every other arm. */
requires?: readonly string[];
/** On `steering.duplicate_input_id` — the facets the repeat differs from the parked steer in (closed
* {@link SteerReplayFacet}, in that set's order; always present on that code, never empty). Absent on
* every other code. The live verb's refusal carries the same value. */
differs?: readonly SteerReplayFacet[];
} | undefined;
constructor(code: "checkpoint.already_exists" | "checkpoint.already_resolved" | "checkpoint.not_found"
/** `runner.resume` was handed an {@link ResumeOutcome} whose `gate` arm does not match the
* persisted {@link CheckpointGate} `kind` (council #3 — type confusion guard), or a gate v1
* resume does not serve (`task_done`). */
| "checkpoint.gate_mismatch"
/** The resume's `TaskSpec.signal` was aborted at the entrance — handed in ALREADY-ABORTED, or aborted
* while the wake screen / `resumePreflight` / pre-claim re-adjudication still ran: ONE code for a
* cancellation there (the seats' own codes keep what the hook itself did). Refused BEFORE the CAS, so
* the checkpoint stays `pending`; consuming the decision on a leg that aborts at once would lose it. */
| "checkpoint.resume_aborted"
/** `runner.resume` was handed an outcome whose payload is unsafe to apply — e.g. a deny `reason`
* carrying a `</system-reminder>` tag that would escape the model-facing wrapper (round-2 #1). */
| "checkpoint.invalid_outcome"
/** The checkpoint's {@link Checkpoint.version} is newer than this worker supports
* (`> MAX_SUPPORTED_CHECKPOINT_VERSION`), or it carries a remote `workspaceHandle` but no
* `executionEnvFactory` is wired to rebuild the env — either way this worker cannot safely resume it.
* Rejected pre-CAS so the checkpoint stays `pending` and a capable worker can still resume it
* (design/49 §2/§3, code-ready council round-2). */
| "checkpoint.unsupported_version"
/** The resume's re-supplied config, or a mode the checkpoint row recorded, is one prepare would refuse
* (a retired spec key, a malformed output style, a mode word outside the closed set, a retired shell-floor
* word). Refused PRE-CAS, so the checkpoint stays `pending` and byte-identical. `message` is prepare's own
* sentence; `detail.code` is prepare's own code ({@link CheckpointConfigRefusalCode}) and `detail.field`
* says which input carries it: `"taskConfig"` (correct the config and resume the SAME token) or
* `"inheritedGate"` (the recorded row — no config reaches it; expire the row). A fresh run of the same
* bad config reports the same `detail.code` as its `TaskResult.errorCode`. */
| "checkpoint.config_refused"
/** `resume`/`resumeStream` was handed `internals.requestedCwd` for a checkpoint that
* carries a remote `workspaceHandle`. The restored workspace's mountPath is authoritative for the
* task root, so the cwd would be ignored at best — and verifying it would touch the env BEFORE the
* VM restore (a lazy-connect adapter then connects a fresh empty sandbox, the design/49 disease).
* Rejected pre-CAS so the checkpoint stays `pending`; re-resume without `requestedCwd`. */
| "checkpoint.cwd_conflicts_restore"
/** design/164: the checkpoint was written by a pre-164 worker AND its ledger carries the RETIRED
* cross-slice wall-clock allocation (`resourceLedger.totalWalltimeSec`). The axis no longer exists,
* so resuming would run the leg with the operator's time ceiling silently unenforced. Refused
* PRE-CAS (the row stays `pending`, so a worker of the previous release can still finish it) rather
* than migrated: there is no honest conversion from a wall-clock allocation to a token one. */
| "checkpoint.walltime_axis_retired"
/** design/80 D-1 (reopen-by-reason): a re-resume of an `env_failed`-reopened checkpoint supplied an
* outcome that does NOT equal the persisted {@link ResolvedOutcome} winner. An `env_failed` reopen is
* a SYSTEM RETRY of the already-approved action (the infra failed, not the decision) — it must replay
* the exact winning binding, never a new vote. Rejected pre-CAS, fail-closed, so the checkpoint stays
* `pending` for the correctly-replayed retry. (A `tool_unavailable` reopen — P-7 — is exempt: it lets
* a human re-decide with the tool present, so a fresh decision is permitted there.) */
| "checkpoint.reopen_revote"
/** design/80 D-1 (atomicity fix): the resume validated its decision-action / reopen-by-reason guards
* against a `get()` snapshot, but a concurrent {@link CheckpointStore.resolve}/{@link CheckpointStore.reopen}
* cycle advanced the monotonic {@link Checkpoint.rev} in the get→resolve window, so the
* {@link CheckpointStore.resolve} CAS's optimistic-concurrency check lost (the validation is stale). The
* row is still `pending` — fail-closed; the caller must re-`get` and re-validate against the CURRENT state
* (a fresh resume), never blindly retry. */
| "checkpoint.reopened_concurrently"
/** A post-CAS compensation {@link CheckpointStore.reopen} returned `false` — the consumed row could not
* be flipped back to `pending` (missing, expired, or changed under a concurrent cycle), so no retryable
* checkpoint exists. Surfaced instead of silently treating the failed reopen as committed, so the
* caller's compensation path settles the token as terminally consumed. */
| "checkpoint.reopen_failed"
/** {@link CheckpointStore.recordExecutionOutcome} met a row that already carries a DIFFERENT
* execution record — see `ExecutionOutcomeConflictError` (checkpoint-execution-record.ts), the
* in-tree form of this code. Never a retry condition. */
| "checkpoint.execution_outcome_conflict"
/** design/80 D-A: {@link CheckpointStore.setPendingSteer} was handed `text` containing a
* `</system-reminder>` close tag — it would escape the model-facing `<system-reminder>` wrapper a
* trusted steer rides on resume. Rejected fail-closed so a dirty steer NEVER enters {@link CheckpointState}
* (§3 inv #2). Mirrors the trusted-steer reject at runtask.ts (`steering.invalid_content`). */
| "steering.invalid_content"
/** design/171 §6.3: the append would cross the parked-steer queue's entry-count or byte bound
* ({@link MAX_PENDING_STEER_ENTRIES} / {@link PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES}). Fail-loud
* rather than evicting: an already-accepted operator instruction is never dropped to make room —
* the caller delivers or removes a parked steer first. Nothing is written on this path. */
| "steering.queue_full"
/** design/171 §6.3 (falsification review adoption): the append reused an `inputId` already parked on
* this checkpoint, but with DIFFERENT content. Re-appending an IDENTICAL steer stays the idempotent
* no-op a retry needs; a differing one is refused, because swallowing it would lose the second
* operator's instruction — the exact silent loss the queue replaced. `detail.differs` names the facets
* that differ: with `text` among them it is another instruction (retry it under a fresh id); without,
* it is the parked one re-sent at another tier / trust / attribution (a fresh id would park it twice —
* replay it with its original values). Nothing is written on this path. */
| "steering.duplicate_input_id"
/** design/373 §4.3 (D2): the deployment's `userPromptSubmit` hook refused a wake resume's
* `message` at the entrance screen (block verdict, or a fail-closed timeout/crash — the
* message discriminates). Pre-CAS: the checkpoint stays `pending` and byte-identical, so the
* SAME token is redeemable once the screen clears — a refusal burns nothing. The live steer
* verb refuses under the same code word (its own Error face); one vocabulary, two entrances. */
| "steering.blocked_by_hook"
/** design/144 §3 (governance-gate purity, fail-closed pre-CAS): a `wake` outcome was supplied for a
* checkpoint that awaits a PENDING GATE DECISION (tool approval / dry-run review / plan review /
* resource-limit continue). Wake is a pure un-park verb and must never circumvent an approval — resume
* the checkpoint through its own decide entry instead (the error message names it). */
| "wake.gate_pending"
/** design/144 §3 (anti spin-wake): a `wake` outcome carried no `message` and the checkpoint has no
* parked steer either — there is nothing to deliver, so the wake is rejected pre-CAS
* (the checkpoint stays pending) instead of burning the token on an empty continuation. */
| "wake.nothing_to_deliver"
/** Parent effective-policy inheritance: the checkpoint records `state.inheritedGate.requiresParentConstraint`
* (the suspended task ran under inherited OPAQUE parent-policy constraints — live closures that cannot
* persist), but the resume supplied no `RunInternals.inheritedGate.parentConstraints` to re-instate them.
* Resuming anyway would silently DROP the ancestors' gate (a widening), so this rejects pre-CAS — the
* checkpoint stays `pending`; re-resume via `resumeStream(token, outcome, config, internals)` with the
* parent chain re-supplied. */
| "resume.parent_constraint_missing"
/** Parent effective-policy inheritance (shape check): the resume DID re-supply
* `internals.inheritedGate.parentConstraints`, but its length differs from the checkpoint's recorded
* `parentConstraintCount` — or (F-012 L1) its projection CONTENT digests differently from the
* recorded `constraintDigest` — or a screening entry names a `tool.check` guard beside a policy the
* constraint mint did not make (no chain to raise it on) — a partial/mismatched re-supply would silently run
* the child under a thinner (or different) ancestor chain than it suspended with. Rejected pre-CAS (the
* checkpoint stays `pending`); re-resume with the full original chain. */
| "resume.parent_constraint_mismatch"
/** #449 G1 (codex r2, pre-CAS entrance): a deployment usage-governance window for this resume's
* ledger key (the config's principal, else the row's recorded one, else the shared bucket) is
* EXHAUSTED, and the row still OWES a delivery a re-mint cannot carry (an approval/review
* decision, a wake, parked steers, a background-task obituary snapshot). Refused BEFORE the CAS,
* so nothing is consumed and nothing is unpinned — the SAME token with the SAME decision is
* redeemable once the window frees (`detail.retryAfterMs` carries the wait). A bare
* `resource_limit` continue with nothing parked does NOT take this refusal: it proceeds and
* re-suspends at turn 0 into a FRESH checkpoint carrying the new wait hint. */
| "resume.usage_window_exhausted"
/** #449 G2 (identity continuity): the resume config carries a NON-EMPTY `principal` that differs from
* the {@link Checkpoint.principal} recorded at the suspend mint. `ResumeTaskConfig`'s contract is
* "the same TaskSpec the run suspended with", and the principal keys real lanes on the resumed leg
* (the usage-governance ledger bucket, later mints' scope derivation, session-rule reads, ask
* attribution) — silently running the leg under a different identity is the bad-value direction this
* refusal exists to close. Rejected PRE-CAS (the checkpoint stays `pending`); re-resume with the
* original principal, or omit the field to inherit the recorded one. */
| "resume.principal_mismatch"
/** design/380 O1③ (placement identity continuity — the `resume.principal_mismatch` rung's twin
* on the placement axis): the resume supplied trusted internals whose EXPLICIT
* `placementRoot` differs from the {@link CheckpointState.placementRootSessionId} recorded at
* the suspend mint. A target-bound env factory keys its placement lookup on that fixed point,
* so silently resuming under a different one would re-place the leg — and every descendant
* the child-internals chain hands it to — on another target than the parked workspace lives
* on. A supplied `rootSessionId` deliberately does NOT take this refusal: it is
* delegation identity, re-supplied by every revive leg, and legitimately differs from the
* recorded root whenever the spawner carried an explicit placement — the recorded root simply
* outranks it in prepare's restore fold. Rejected PRE-CAS (the checkpoint stays `pending`);
* re-resume with the recorded root, or omit `internals.placementRoot` to inherit it. Either
* side absent (an older row / a claim-less resume) skips the check, and `""` reads as absence
* — the honest degradation arm. */
| "resume.placement_mismatch"
/** design/380 O2: the deployment's `RunnerDeps.resumePreflight` refused this resume — an
* explicit refusal verdict, a throw, a timeout past its bounded deadline, or an unreadable
* answer (all fail-closed). The LAST rung before the CAS, so nothing was consumed. Two arms,
* discriminated by the message: the default `retry_later` arm leaves the checkpoint `pending`
* and the SAME token redeemable once the obstacle clears (`detail.retryAfterMs` carries the
* deployment's wait hint when it gave one — the #449 G1 carrier); the explicit `terminal` arm
* has ALREADY settled the row out of `pending` via the single-shot `expire` CAS (reaper
* parity) before this throw — the token is NOT redeemable, by the deployment's own permanent
* verdict (a dead binding must not be redialed forever). */
| "resume.preflight_rejected"
/** F-012 L3: the resume carried an approver EDIT (`updatedInput`) and a FROZEN inherited ancestor
* constraint (the persisted projection chain) — or the deployment's resume-edit policy — DENIES the
* edited args. Refused PRE-CAS with fresh-redecision semantics: the checkpoint stays `pending` and
* the same token remains decidable (approve as shown, deny, or edit differently) — the rejection
* consumed the decision's INPUT, never the ask itself. */
| "resume.constraint_rejected"
/** F-012 L3: the resume carried an approver EDIT but the inherited ancestor chain cannot be
* re-adjudicated statically — an OPAQUE layer is present, a layer declares a live remainder beyond
* its projection, or the checkpoint predates the projection chain. "Would have been allowed" is not
* computable from a self-reported closure, so the edited resume is refused typed pre-CAS (the
* checkpoint stays `pending`, still decidable); an UNEDITED approval is untouched. */
| "resume.constraint_unprojectable", message: string,
/** Structured discrimination a caller can branch on where the code alone is ambiguous. `field`
* names WHICH part of the decision payload was rejected on a
* `checkpoint.invalid_outcome` — `"boundCallId"` (the action you decided on has been replaced:
* re-fetch the pending list), `"boundInputHash"` (the input you reviewed has changed under the same
* action: re-review), `"boundGrantDigest"` (a parked `Skill` approval does not cover the skill the decision selects — another skill, or the card's with other lists: deny, re-activate), or `"answer"` (the content-ask answer is missing, or was attached to a
* decision that cannot carry one: re-decide with a well-formed answer — ruled 2026-08-04), or
* `"hostDecision"` (a tool approval's or a plan pause's decision facts are missing, name a decider outside
* `person`/`sla_timeout`/`policy`, pair the sweep's `sla_timeout` with an allow or put it on a row a sweep does not
* resolve, or try to supply the minted record), or — design/252 G-7 — `"approver"` (the attribution is not a
* well-formed identifier: re-decide with a plain, bounded, control-character-free string, or omit
* it). On a `checkpoint.config_refused` `field` names which INPUT carries the refusal — `"taskConfig"`
* (the re-supplied config: correct it and resume the same token), `"deps"` (the deployment's own seat — and the policies the
* caller re-supplies as inherited constraints: correct them and resume the same token) or `"inheritedGate"` (the row's record: no config reaches it; expire the row). The throw
* sites always knew which arm fired; this carries that fact instead of dropping it (ruled
* 2026-08-03, cross-repo HITL discussion). Additive and optional — `code` remains the only REQUIRED
* discriminant. */
detail?: {
field?: "boundCallId" | "boundInputHash" | "boundGrantDigest" | "answer" | "hostDecision" | "approver" | "taskConfig" | "deps" | "inheritedGate";
/** On `checkpoint.config_refused` — prepare's own code for the refusal ({@link CheckpointConfigRefusalCode}), the
* word a fresh run of the same bad config reports as its `errorCode`. Absent on every other code. */
code?: CheckpointConfigRefusalCode;
/** #449 G1 — on `resume.usage_window_exhausted`: how long until the binding window frees
* (the same wait a suspended row carries as `gate.resumeAfterMs`). design/380 O2 — on a
* retry-later `resume.preflight_rejected`: the deployment preflight's own wait hint, when it
* gave one (same carrier, same semantics: retry the SAME token after the wait). Absent on
* every other code. */
retryAfterMs?: number;
/** WHICH arm fired, where one `code` covers several — the word list and what each word means to a
* host live at {@link CheckpointErrorReason}, its one home. Additive and optional like `field`
* (`code` remains the only REQUIRED discriminant), which is exactly why the obligation to SPELL it
* is a gate rather than a type: see the closed set's own doc. */
reason?: CheckpointErrorReason;
/** On a `version_newer` refusal of a v10 row: the reader-requirement words the row lists that this worker lacks
* (sorted, display only). Absent on every other arm. */
requires?: readonly string[];
/** On `steering.duplicate_input_id` — the facets the repeat differs from the parked steer in (closed
* {@link SteerReplayFacet}, in that set's order; always present on that code, never empty). Absent on
* every other code. The live verb's refusal carries the same value. */
differs?: readonly SteerReplayFacet[];
} | undefined);
}
/**
* The pluggable durable seam (design/45 §2.1). Symmetric with `ToolResultStore`/`MemoryStore`:
* create-once `put`, `get`, an **atomic CAS `resolve`** (the once-only foundation), and a `reap` for TTL
* expiry. A durable backend (TiDB) makes resume cross-replica; the default in-memory impl is single
* instance / tests only.
*
* **Load-bearing contract (§2.1, service-side report):** `resolve` is the once-only gate. The store guarantees
* *exactly one* `resolve(token, ...)` wins the CAS (pending → resolved); the **runner** must treat
* "won the CAS" as the *sole* trigger to execute the pending action — that is what makes a pending tool
* call run exactly once across retries / double approvals / multiple replicas.
*
* **design/80 §D-D expiry semantic contract (gate.kind-aware reap + `terminalAt` backstop):** the
* reaper-LOOP that calls {@link reap}/{@link expire} is the DEPLOYMENT's (its store impl + loop — OWNED
* service-side); core's D-D job is ONLY to keep writing the durable FIELDS the loop
* reads ({@link Checkpoint.deadline} for an approval gate + the persisted {@link Checkpoint.gate}.kind) and
* to DOCUMENT the contract here so ANY reaper consumer (the service's TiDB loop, or a non-service Pg
* deployment) implements it IDENTICALLY:
* - **FACET A — APPROVAL gates** (`gate.kind` ∈ {`human`, `irreversible_ask`}): the loop's FAST PATH at
* `deadline` is an SLA **resolve-deny** (`resolve(token, {gate:"policy_ask", decision:"deny"}, …)` so the
* suspended run ends cleanly), with a LATER `terminalAt` **abandonment backstop** the loop ALSO honors
* (crash-safe if the SLA timer never fired — then {@link reap}/{@link expire} flips it `expired`).
* - **FACET B — UNATTENDED-TTL gates** (`gate.kind` ∈ {`resource_limit`, `needs_review`, `plan_review`} — the
* resource slice + the review-pause family): `deadline` is an abandonment-TTL → {@link expire}/{@link reap}
* ONLY, **NEVER** a resolve-deny (a resolve-deny on these kinds hits NO `gateMatch` arm → `gate_mismatch`;
* their resume outcome is `resource_limit`/`dry_run_review`/`plan_review` respectively, not `policy_ask`).
* NO core-minted `terminalAt`, NO core reaper, NO change to {@link reap} logic — minting `terminalAt` and
* running the gate.kind-aware loop are the deployment's, by design.
*/
/**
* design/173 §2.3 — the store DURABILITY declaration vocabulary (declaration, never duck-typing —
* the exact posture of {@link CheckpointStore.retention}): `"durable"` promises the rows SURVIVE A
* PROCESS RESTART of the deployment that owns the store's data domain (a durable park can be
* resumed by a later process); `"process-local"` states they die with the process. **This axis
* claims restart survival ONLY — it does NOT claim multi-replica coordination** (whether a SECOND
* concurrent replica can serve the same rows is a separate property of the backend: the file
* backend is single-instance-per-data-dir by design, Pg/TiDB backends coordinate — both honestly
* declare `"durable"` on THIS axis; review fold round 1, finding 2). ABSENT is read FAIL-CLOSED as
* `"process-local"` (the wiring manifest may under-promise durability, never over-promise it); an
* unparseable value is refused loudly (`config.store_durability_invalid`) rather than folded to
* either arm. Adapter contract with a conformance obligation: a backend that persists outside the
* process (file/Pg/TiDB) SHOULD declare `"durable"` — until it does, restart-surviving resume
* capability is reported as absent.
*/
export type StoreDurability = "durable" | "process-local";
/**
* The store ENCODING-FIDELITY declaration vocabulary — the third honest-declaration axis, alongside
* {@link StoreDurability} and {@link CheckpointStore.retention}, and the one that says WHICH VALUES a
* row can hold unchanged.
*
* `"structured-clone"` promises the structured-clone value domain survives a round trip through the
* backend (a `Date` comes back a `Date`, a `Map` a `Map`, `NaN` stays `NaN`); `"json"` states the
* backend serialises through JSON, so the row that comes back after a restart carries the JSON
* PROJECTION of what was filed (`Date` → string, `Map`/`Set` → `{}`, `NaN`/`Infinity` → `null`, `-0`
* → `0`, an `undefined`-valued key gone).
*
* Why this has to be declared rather than inferred: the mint decides what a durable approval row may
* carry, and it decides it with `structuredClone` — which is WIDER than a JSON codec. A value can pass
* that check, be filed, degrade on disk, and come back a different value, while the binding hash minted
* over the pre-serialisation value still matches the one the operator echoes (the resume compares the
* persisted string opaquely and never re-hashes). The approval is then accepted for arguments that are
* not the arguments it bound. Only the backend knows its own codec, so only the backend can say.
*
* ABSENT is read FAIL-CLOSED as `"json"` — the narrower domain, the same "may under-promise, never
* over-promise" posture {@link StoreDurability} takes with `"process-local"`: an undeclared backend gets
* a row it can certainly hold, and a backend that can hold more earns the wider domain by saying so. An
* unparseable value is refused loudly (`config.store_fidelity_invalid`) rather than folded to either arm.
*/
export type StoreFidelity = "structured-clone" | "json";
/**
* Resolve a store's declared {@link StoreFidelity} (the read half of the declaration — the exact shape
* `resolveDeclaredFidelity`'s durability twin has): ABSENT → the fail-closed `"json"`, a recognized
* value verbatim, anything else refused loudly. Never duck-types the backend.
*/
export declare function resolveDeclaredFidelity(store: {
readonly fidelity?: StoreFidelity;
} | undefined, storeName: string): StoreFidelity;
/**
* Project an already-captured value onto what a backend of the given {@link StoreFidelity} can hold, so
* everything minted from it — the row's args, the approval preview, the binding hash, the risk
* descriptor — describes the value that comes BACK, not the one that went in.
*
* `"structured-clone"` is the identity: the caller's snapshot already is what such a backend stores.
* `"json"` runs the backend's own codec (`JSON.stringify` → `JSON.parse`, `toJSON` hooks included, since
* that is exactly what the backend will do to it). The input is expected to be an accessor-free snapshot
* already, so no property is read a second time here.
*
* A value JSON cannot ENCODE AT ALL (a `BigInt`, a cycle) fails instead of degrading — those would throw
* inside the backend's own write anyway, and failing here means the caller can say why while nothing has
* been committed. The thrown value rides out on `cause` so the caller decides how to word the refusal.
*/
export declare function encodeAtFidelity(fidelity: StoreFidelity, value: unknown): {
ok: true;
value: unknown;
} | {
ok: false;
cause: unknown;
};
/**
* CONSERVATIVE "these two are provably the same value" for deciding whether a {@link encodeAtFidelity}
* projection moved anything, and whether a policy's rewrite names the very value about to be filed.
*
* Deliberately NOT the binding hash, which is the wrong instrument for this question in two directions:
* it folds `-0` onto `0` (a real projection change the policy may care about, invisible to the hash), and
* it is a digest, so a collision would read as sameness. `Object.is` at the leaves keeps the `-0`
* distinction; comparing own-key SETS keeps the dropped `undefined`-valued key visible.
*
* Conservative on purpose — the answer it must never give wrongly is TRUE, because both callers read
* TRUE as "nothing moved / the policy named this very value" and act by filing. So sameness has to be
* PROVED, not merely un-disproved:
* - the PROTOTYPES must match and be the plain-object or array one. `[object Object]` is not proof of
* a plain object — a class instance and a null-prototype object both report it, and a tool executing
* the arguments can observe inherited behaviour. (Neither input to the projection comparison can be
* such a value: `structuredClone` and `JSON.parse` both produce plain data. A policy's rewrite can.)
* - own keys are compared with `Reflect.ownKeys` and POSITIONALLY, so a SYMBOL key or a non-enumerable
* property on one side is a difference rather than an invisible extra, and so is a different order
* (which `Object.keys`, `JSON.stringify` and any tool iterating its arguments can see).
* - properties are compared through their DESCRIPTORS — every flag, so a frozen object is not the
* mutable value a JSON decode produces — and an accessor on either side answers FALSE without being
* read: an approval decision must not be made by running deployment code, and what a getter returns
* need not be the same twice.
* - reference TOPOLOGY counts: two properties that share one object are not two properties holding
* equal objects, and a JSON round trip is precisely what turns the first into the second.
* - what it CANNOT prove on its own is that the value it inspected answers ordinary reads the way its
* descriptors did — a Proxy reports the target's descriptors and can return anything from a `get`.
* A caller comparing a value it did not construct must first establish that it is an inert data
* graph (the capture clone does exactly that, and refuses a Proxy outright).
* - any brand outside plain object/array that is not already reference-identical answers FALSE (a Date
* is never the string a JSON codec leaves behind, a Map is never the `{}`), and so does any
* inspection that throws.
* An uncertain answer therefore costs a policy consultation or a park, never a silently filed value.
*/
export declare function samePlainValue(a: unknown, b: unknown): boolean;
export interface CheckpointStore {
/**
* Managed-retention declaration (ruled 2026-08-05, retention.ts): `"managed"` promises the
* `ManagedRetentionCapability` contract — including the reap/unpin gap this interface documents on
* {@link reap} (a managed store's scheduled expiry DOES release the session pins). `"none"` (or
* ABSENT — read fail-closed as `"none"`) states the store cannot delete on schedule; a deployment
* with a LOCKED retention policy refuses to start over it. Declaration, never duck-typing.
*/
readonly retention?: import("./retention.js").RetentionDeclaration;
/**
* design/173 §2.3 — durability declaration (see {@link StoreDurability}): whether checkpoints
* this store accepts survive a process restart. Same "declaration, never duck-typing" posture as
* {@link retention}; the wiring manifest relays it verbatim and reads ABSENT fail-closed as
* `"process-local"`.
*/
readonly durability?: StoreDurability;
/**
* Encoding-fidelity declaration (see {@link StoreFidelity}): which value domain a row keeps INTACT
* through this backend. Same "declaration, never duck-typing" posture as {@link durability}; the park
* mint reads it to decide the width of the snapshot it files, and reads ABSENT fail-closed as
* `"json"`. A backend that serialises through JSON MUST say so (or leave it absent, which says the
* same) — a `"structured-clone"` claim it cannot keep is what lets a row degrade under a binding hash
* that still matches.
*/
readonly fidelity?: StoreFidelity;
/**
* F-012 L2/L3 — the RE-DECISION capability declaration: whether this store's `reopen` (and, when
* present, a durable validating-lease discipline) actually works — a promise, not a method-presence
* sniff (`reopen` is optional and a stub that returns `false` still satisfies the type). The
* sandbox-admission mode's pre-flight REQUIRES `redecision.reopen === true` (its boundary asks park
* to a human and must be re-decidable); absent reads fail-closed as "no declared re-decision face"
* and the mode refuses to arm, loudly. Same "declaration, never duck-typing" posture as
* {@link retention}/{@link durability}/{@link fidelity}.
*/
readonly redecision?: {
reopen: boolean;
validatingLease?: boolean;
};
/**
* The EXECUTION-OUTCOME declaration — REQUIRED, and required to be exactly `{ outcome: true }`: the
* store promises that {@link recordExecutionOutcome} is the real CAS this contract describes (the
* predicate, the four words, the conflict throw) and that {@link reopen} honours the record. Same
* "declaration, never duck-typing" posture as the three optional axes above, but not optional: the
* durable resume lane records how every decided action was disposed, and a store that cannot keep
* that record would leave every resolved row it holds silently unreadable on that axis. A store
* without the declaration is refused at the FIRST read of the store seat
* ({@link resolveCheckpointStore}, code `config.invalid_checkpoint_store`) — a JavaScript caller that
* satisfies no type is told at prepare what the type would have told it.
*/
readonly execution: {
readonly outcome: true;
};
/** Create-once. Throws {@link CheckpointError} `already_exists` on a token collision (never a silent
* overwrite — a reused token would clobber a live suspension). */
put(token: CheckpointToken, cp: Checkpoint): Promise<void>;
get(token: CheckpointToken): Promise<Checkpoint | null>;
/**
* Atomic CAS: `UPDATE … SET status='resolved' WHERE token=? AND scope=? AND status='pending'`.
* Returns `true` for the single winner (was pending → now resolved), `false` if already resolved/
* expired (`AlreadyResolved` — the caller treats it as a no-op, never re-executes). `scope` is in the
* WHERE for multi-tenant isolation (a wrong-scope resolve must not win).
*
* `outcome` is supplied so a **durable** backend persists it atomically with the status flip (design/45
* v2 resumable-resume hook). **design/80 D-1 (persist-winner):** when the CAS wins, the store ALSO records
* the winning binding as {@link Checkpoint.resolvedOutcome} on the row — so a later {@link reopen} →
* re-resume can be validated against the approved decision (an `env_failed` reopen must replay the same
* winner; design/80 §3 inv #1). A `policy_ask` outcome records the decision bound to its pending tool
* call; the two REVIEW outcomes (`plan_review` / `dry_run_review`) record theirs under a synthetic
* `gate:` marker; `resource_limit` / `wake` / `task_done` bind nothing and record no winner.
*
* **Implementers: derive the winner with {@link winnerFromOutcome} and persist the object WHOLE.** Its
* field set GROWS (`updatedInput`, then `answer`, then `reason` — each additive and optional), and the
* replay guard reads an absent optional field as "this backend never recorded it" so that rows written
* before the field existed still replay. A backend that projects a hand-listed subset of columns is
* therefore indistinguishable from an old row and silently leaves that dimension UNBOUND for every
* decision it ever writes — the replay guard cannot tell the two apart, and there is no capability
* negotiation on this interface to tell it. Round-trip the whole record (opaque JSON is what every
* in-tree backend does) rather than enumerating its keys.
*
* (The crash-recovery semantics of *acting* on a won CAS are runner contract — see
* {@link Runner.resume}; the store only owns the atomic once-only flip + the durable winner.)
*/
resolve(token: CheckpointToken, scope: string, outcome: ResumeOutcome, expect?: ResolveExpectation): Promise<boolean>;
/**
* Inverse of {@link resolve}: CAS `resolved → pending` (`UPDATE … SET status='pending' WHERE token=?
* AND scope=? AND status='resolved'`). Returns the CAS winner (`true` = was resolved → now pending again).
*
* **Compensation for a post-CAS env-restore failure (design/45/49).** `Runner.resumeStream` wins the
* `resolve` CAS *before* the workspace `resumeVM` runs; if that restore then fails (`resume.env_failed`)
* the pending action never executed yet the checkpoint is consumed — without this the suspended work is
* lost to a forced "re-initiate" (fine for a short approval, ruinous for a long unattended task).
* Reopening lets a retry re-resume the SAME work. Optional: a store that omits it keeps the prior (lossy)
* behavior, and the runner degrades gracefully. Safe by the `status='resolved'` guard — only the run that
* consumed the checkpoint (and then failed to restore) can reopen it, and only while it is still `resolved`.
*
* **design/80 D-1 (reopen-by-reason):** the `reason` distinguishes the two reopen triggers (both shipped at
* runtask.ts) so re-resume can validate per reason (§3 inv #1): `env_failed` (the action is still valid,
* only infra failed → re-resume MUST replay the persisted {@link Checkpoint.resolvedOutcome} winner) vs
* `tool_unavailable` (P-7: the approved tool vanished, the action may now be invalid → a human re-decides,
* so a FRESH decision is allowed). The store RECORDS the reason on the row as {@link Checkpoint.reopenReason}
* and PRESERVES the persisted winner across the reopen (never clears it).
*
* **A row that carries an execution record is NOT reopenable** (`… AND execution_outcome IS NULL` in the
* WHERE; `checkpointExecutionRecorded` is the shared predicate): the decided action's disposition is on
* file — see {@link CheckpointRow.executionOutcome} — and reopening it would either re-run an action that
* already ran or re-decide one already refused. Answers `false` like every other predicate miss; the
* reasons and the winner are untouched.
*/
reopen?(token: CheckpointToken, scope: string, reason: ReopenReason): Promise<boolean>;
/**
* Record how the DECIDED action was finally disposed on the resumed leg — the {@link GateOutcome} its
* `tool_end` frame carried (see {@link CheckpointRow.executionOutcome} for what the record means and why
* it is a column of its own beside the immutable decision). REQUIRED (declared by {@link execution}).
*
* The CAS: `UPDATE … SET execution_outcome=?, execution_at_ms=? WHERE token=? AND scope=? AND
* status='resolved' AND execution_outcome IS NULL`. The answer is a word of the closed
* `ExecutionOutcomeRecordWord` set — `recorded` (the CAS won), `already_recorded` (the row already
* carries an EQUAL record: a retry, idempotent), `not_resolved` (pending/expired: no decision to be the
* outcome of), `absent` (no such row in this scope). A row carrying a DIFFERENT record throws
* `ExecutionOutcomeConflictError` (`checkpoint.execution_outcome_conflict`) — the record is immutable
* and two accounts of one execution are an engine defect, never a store overwrite. Equality is
* structural over the record's JSON (key order and freezing are not part of the value —
* `sameExecutionRecord`); every backend takes its disposition from `executionRecordDisposition`
* (checkpoint-execution-record.ts) and keeps only the commit half itself.
*
* Never touches `status`, `rev`, `resolvedOutcome` or `reopenReason`: the record is a fact about the
* execution, not a transition of the decision. Its one effect on the row's other verbs is that
* {@link reopen} refuses a recorded row.
*/
recordExecutionOutcome(token: CheckpointToken, scope: string, gate: GateOutcome): Promise<ExecutionOutcomeRecordWord>;
/**
* design/80 D-A: park a durable mid-task STEER on a DURABLY-SUSPENDED task's checkpoint. CAS-style update
* `SET state.pendingSteerQueue=? WHERE token=? AND scope=? AND status='pending'`: returns `true` iff the
* checkpoint is still `pending` (a resolved/expired checkpoint can't be steered → `false`, a no-op). Scope
* is in the WHERE for multi-tenant isolation, exactly like {@link resolve}/{@link expire}.
*
* **design/171 §5.3 — this APPENDS to {@link CheckpointState.pendingSteerQueue}; it no longer overwrites a
* single seat.** Two people steering one suspended run used to silently destroy the first instruction,
* with no record that anything was lost. Idempotency moved from last-writer-wins to the `inputId` key:
* re-appending an `inputId` already queued (a service retrying with the message id it already minted)
* is a no-op that still returns `true`. Crossing {@link MAX_PENDING_STEER_ENTRIES} or
* {@link PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES} throws `steering.queue_full` — bounded fail-loud, never
* by evicting an accepted instruction.
*
* A backend must round-trip the queue field verbatim and must build its new queue with
* {@link appendPendingSteer} so the bounds and the `seq` mint are one implementation rather than one
* per store.
*
* **Trust is FROZEN here** — `steer.trusted` is whatever the SERVICE computed from the verified principal
* at this call (an operator-role check, NOT a client header — the service's job); the core stores it
* VERBATIM and NEVER recomputes it on resume (§3 inv #1).
*
* **Validation (load-bearing, §3 inv #2):** `steer.text` containing a `</system-reminder>` close tag is
* REJECTED with a typed {@link CheckpointError} `steering.invalid_content` (mirroring runtask.ts's trusted-
* steer reject) — a dirty steer NEVER enters {@link CheckpointState}, so the persisted state stays clean
* regardless of `trusted`. (The untrusted RESUME-injection path ALSO sanitizes the text as untrusted data;
* this persist-time reject is the belt that keeps a caller-declared close tag out of the durable record on BOTH
* paths.) Text over {@link MAX_PENDING_STEER_CHARS} is rejected with the same code (HRD-APV-4 — the
* steer is re-delivered into the model context on every resume, so it is capped like every other
* persisted model-facing field on this row; rejected rather than truncated because cutting an
* instruction can invert it). NOT an approval channel: this only writes the parked queue, never
* touches status / the resolve path / any decision (§3 inv #4).
*/
setPendingSteer(token: CheckpointToken, scope: string, steer: PendingSteerInput): Promise<boolean>;
/**
* CAS-expire a **single** `pending` checkpoint by token: `UPDATE … SET status='expired' WHERE token=?
* AND scope=? AND status='pending'`. Returns the CAS winner (`true` = was pending → now expired; `false`
* = already resolved/expired). The same UPDATE as {@link reap} but keyed by **token**, not by deadline.
*
* **Load-bearing contract (design/51 §2/§4):** `expire` and {@link resolve} race the **same `pending`
* row**, so the store serializes them — exactly one wins. This is what makes {@link TaskStream.destroy}
* a correct *fence-then-reap*: `destroy` calls `expire` FIRST to fence any concurrent `resume` (which
* goes through `resolve`); only the `expire` winner then destroys the paused env, so the same checkpoint
* is **never both reaped and resumed**. `scope` is in the WHERE for multi-tenant isolation, like
* `resolve`/`reap`.
*/
expire(token: CheckpointToken, scope: string): Promise<boolean>;
/**
* design/384 slice 2 / S-25-R1 — the ATOMIC terminal claim: "win the terminal race" and "read the
* truth on a loss" as ONE store operation. OPTIONAL, probed by presence (the `listByScope`
* precedent); the runner's resume chain adopts it when present (the loser triages off `current`
* with zero second read) and keeps the two-step resolve+get path otherwise. This JSDoc is the
* single normative statement of the claim law — `put`'s create-once/read-back three-state and the
* suspend saga's commit discipline are the same single-winner law's PUT variant and cross-reference
* here rather than restate.
*
* The laws:
* 1. **Atomicity** — the win/lose decision and the loser's `current` reading MUST come from the
* same atomic unit (one `UPDATE … RETURNING` / one transaction / one single-threaded map
* operation). A settle is self-sufficient: a caller holding `claimed:false` NEVER needs a
* second read to act.
* 2. **CAS-predicate identity** — `kind:"resolve"`'s win IS {@link resolve}'s win (same WHERE:
* token+scope+pending[+rev]; a win records the winner via {@link winnerFromOutcome} and bumps
* `rev` identically); `kind:"expire"` IS {@link expire}. ONE-MACHINE OBLIGATION: a store that
* provides `claimTerminal` must route all three verbs through one protected write path
* (resolve/expire implemented over claimTerminal, or all three over one shared CAS kernel) —
* two predicates would be a drift window, and this law forbids it.
* 3. **Timeout posture** — no timeout parameter here: transport deadlines belong to the backend,
* wall-clock backstops to the caller. The contract states exactly two halves: a SETTLE carries
* the truth; a REJECT proves NOTHING (the row may already be terminal — the caller must not
* proclaim a terminal state from its own intent; fail-closed projection of "still pending"
* stays legal). A LATE settle is as self-sufficient as a prompt one — an answer arriving after
* the caller's own deadline still carries the true decision, so the caller settles on truth
* with zero follow-up reads.
* 4. **Recovery discipline — truth is comparable, ownership is not.** A bounded retry after a
* reject re-sends the SAME claim (the CAS is idempotent). When the retry answers
* `claimed:false`, `current` tells the caller what the row IS — never WHOSE claim made it so:
* two claimants writing byte-identical outcomes cannot be told apart by content, and
* {@link ResolvedOutcome} carries no claimant identity. Two consumption classes follow:
* IDEMPOTENT consumption (settle on the recorded truth — who wrote it is irrelevant) may use
* `claimed:false` + the recorded decision directly; RESPONSIBILITY-bearing consumption (only
* the winner may execute the pending action — the runner's resume) must NEVER self-attribute
* after ambiguity: treat it as a loss (the `already_resolved` shape, nothing executed) — the
* same law as the existing crash-after-CAS pin ("retry sees resolved and does NOT re-execute"),
* which this claim does not weaken. There is NO replay arm: a burned approval takes the
* existing reopen/re-ask compensation route, and this contract opens no "assume approved and
* replay" door. (A claimant-identity field would be a schema change; not here.) Expire-class
* claims are naturally idempotent (expire-by-me ≡ expire-by-reaper; no attribution needed).
* 5. **A loss reading is a consistent snapshot at the claim's linearization point, not an eternal
* truth** — `resolved` can be reopened back to pending, so "settle is self-sufficient" scopes
* to the disposition of THIS loss (settle on truth / re-validate on OCC / close on absent),
* never to the row's future; a caller needing current state later still `get`s. `current` is a
* projection, not the row (no state / pendingAction payloads).
* 6. **`resolvedOutcome` reads as the row field it is, passed through verbatim** — the persisted
* winner record: written only by decision-bearing resolves ({@link winnerFromOutcome} answers
* `undefined` for resource/wake/task_done), PRESERVED by `reopen`, not cleared by later
* winnerless resolves. In a loss answer it means "the row's last recorded decision winner",
* NOT "the payload of the claim that beat you"; absence means "no decision winner recorded",
* not "the winner had no content". For has-a-human-decided consumption this is exactly the
* needed semantics; "who beat me / with what" is claimant-identity territory this contract
* does not mint.
* 7. **No declaration bit** — presence of the method IS the contract (an implementation
* obligation at the same level as `resolve`'s CAS promise); no `redecision`-style declaration,
* because that family guards against a stub satisfying a type, and a `claimTerminal` stub has
* no such shape — the return form itself carries the obligation.
*
* Deployment correspondence (an ask-row store implementing the same law maps its own vocabulary):
* token+scope ↔ the ask id (+ its run binding); pending/resolved/expired ↔ the ask row's
* open/decided/terminal states; intent `resolve(outcome)` ↔ the human-decision verb; intent
* `expire` ↔ the expire/cancel verbs; the `claimed:false → (status, resolvedOutcome)` PAIRED read
* ↔ the decided row's recorded decision (reading the pair as the CURRENT decision is the CALLER's
* discipline here — gate-kind matching — while a single-decision row schema satisfies it
* structurally). Two shape notes carried from that correspondence: a store whose rows have NO
* OCC/rev axis simply never produces the `status:"pending"` loss arm (an honestly absent arm is
* correct — do not fabricate a rev); a store whose domain has NO reopen verb enjoys stronger
* monotonicity than law 5 assumes — consumption written against law 5's weaker snapshot reading
* stays correct there unchanged.
*/
claimTerminal?(token: CheckpointToken, scope: string, intent: TerminalClaimIntent): Promise<TerminalClaimOutcome>;
/**
* CAS-expire `pending` checkpoints in `scope` whose `deadline` has passed (`deadline <= cutoff`):
* `pending → expired`. Returns the count expired (for metrics). Idempotent across replicas (DB
* serializes; only the first wins each row) — no leader election needed (§6).
*
* **Does NOT unpin the sessions of the checkpoints it expires (council finding #2).** A suspend pinned
* its session against idle eviction; reaping the checkpoint here leaves that pin in place. The reaper
* that calls `reap` owns releasing the pin. For a **durable** session store this is a no-op anyway
* (`pin`/`unpin` are no-ops; it never idle-GCs), so reap-without-unpin is harmless — the only store with
* a real pin is the in-memory `TtlSessionStore` (single-instance / tests), where an un-released pin
* keeps an abandoned suspended session in the cache until process exit. v1 leaves the runner without an
* auto-reaper (the service owns the TTL worker); a reaper over the in-memory store should track and
* `unpin` the expired sessions itself if that leak matters for its deployment. This no-unpin posture
* is LOCAL to this live-path `reap`: the managed-retention contract's `expireCheckpoints`
* ({@link import("./retention.js").ManagedRetentionCapability}) deliberately closes the reap/unpin
* split — a retention-terminal row has its pin released BY that method, so pin release stays
* exactly-once per row (each row is terminated by exactly one of the two lanes).
*/
reap(scope: string, cutoff: number): Promise<number>;
/**
* design/80 assistant-scheduler seam #1: list a lightweight {@link CheckpointSummary}
* for EVERY **pending** checkpoint in `scope` (the suspended / awaiting-human tasks — NOT resolved/expired) in
* ONE call, so a supervisor scheduler can enumerate a scope's open suspensions without N+1 `get`s or hauling
* full {@link Checkpoint} payloads. Read-only — it never mutates a row, takes no CAS, and has ZERO effect on
* the once-only resolve. The PENDING predicate is byte-for-byte the {@link resolve} CAS's (`scope === scope &&
* status === 'pending'`), so the list reflects exactly the rows that are still resumable. Order is unspecified
* (the inbox sorts by `severity`/`deadline` itself). An empty scope returns `[]`.
*
* **OPTIONAL** for backward-compat with external {@link CheckpointStore} impls (a caller probes
* `store.listByScope?.(scope) ?? []`); ALL THREE first-party impls (in-memory, file, Pg) provide it.
*/
listByScope?(scope: string): Promise<CheckpointSummary[]>;
/**
* Deployment seam: the DISTINCT scopes that currently have at least one PENDING checkpoint — the
* enumeration face `listByScope` lacks. A host keeping its own scope registry (e.g. a sidecar file for a
* cross-scope inbox) can rebuild it from the data itself when the registry is lost/corrupt, instead of the
* enumeration silently going empty and pending approvals turning invisible. Scoped to PENDING deliberately:
* it matches `listByScope`'s visible surface, so `listScopes() × listByScope()` is exactly the full inbox.
* **OPTIONAL** like {@link listByScope}; the in-memory and file first-party impls provide it.
*/
listScopes?(): Promise<string[]>;
}
/**
* The SINGLE resolution of "which CheckpointStore does this run use" (ruled 2026-08-04; re-homed
* from prepare-task with the design/173 wiring manifest so the static self-check half shares it
* without a runtime cycle). The spec seat is three-valued — a store wins, `undefined` falls through
* to the deployment's `RunnerDeps.checkpointStore`, and the explicit word `"disabled"` means this run
* has NO store at all (see `TaskSpec.checkpointStore`: the per-run off switch for the durable
* machine, so a machine-started run cannot mint a checkpoint nobody will ever resolve).
*
* The off switch is a WORD, not `null`, on purpose: `??` treats `null` exactly like `undefined`, so
* the old null sentinel silently fell through a bare coalesce to the very deployment store the
* caller asked not to have — re-arming every suspend leg while the run looked perfectly normal right
* up until it parked forever — and every read of the pair had to remember a special `=== null` test.
* A string is never nullish, so the ordinary coalesce is the whole rule. The retired spelling is
* REFUSED loudly here (the one read every site goes through) rather than read as either value: a
* caller still writing `null` is told the word to write, at the first prepare, instead of being
* handed a store it asked not to have or a silent off switch it did not spell.
*/
export declare function resolveCheckpointStore(spec: {
checkpointStore?: CheckpointStore | "disabled";
}, deps: {
checkpointStore?: CheckpointStore;
}): CheckpointStore | undefined;
/** The ONE refusal of a store seat whose {@link CheckpointStore} does not declare `execution = { outcome: true }`
* (named by seat and by the store's constructor when it has one). Same door, same code, as the retired-`null`
* refusal above: a configuration the run cannot honour is refused at the first read, never carried. */
export declare function checkpointStoreExecutionUndeclared(seat: "TaskSpec.checkpointStore" | "RunnerDeps.checkpointStore", store: object): Error & {
code: "config.invalid_checkpoint_store";
};
/** The ONE refusal of the retired `checkpointStore: null` spelling (the resolver and the workflow baseline overlay both throw it). */
export declare function retiredCheckpointStoreNull(): Error & {
code: "config.invalid_checkpoint_store";
};
/**
* design/80 D-1 (persist-winner): derive the durable {@link ResolvedOutcome} winner from a resume
* `outcome`, or `undefined` when the outcome binds to no pending action. A `policy_ask` resume names a
* specific tool call (`boundCallId`) + decision; since RB-64 the REVIEW gates (`plan_review` /
* `dry_run_review`) record their decision under a `gate:` sentinel too — the env_failed replay guard can
* only compare what is recorded here. Only `resource_limit` / `task_done` bind nothing and record no
* winner. Shared by every {@link CheckpointStore} impl so the persisted winner is identical across the
* in-memory and durable backends.
*/
export declare function winnerFromOutcome(outcome: ResumeOutcome): ResolvedOutcome | undefined;
/**
* design/80 D-A: the persist-time validation every {@link CheckpointStore.setPendingSteer} impl runs so the
* reject is IDENTICAL across the in-memory and durable backends. A `steer.text` carrying a `</system-reminder>`
* close tag would escape the model-facing `<system-reminder>` wrapper a trusted steer rides on resume — reject
* fail-closed (`steering.invalid_content`) so a dirty steer NEVER enters {@link CheckpointState} (§3 inv #2).
* Applied for BOTH `trusted` values: the durable record must be clean on either path (the untrusted resume
* path ALSO sanitizes as untrusted data, but the close tag must never reach the persisted state at all).
* Returns a copy built from the FROZEN field set ({@link PENDING_STEER_FROZEN_FIELDS} minus the
* store-minted `seq`), so no extra caller field leaks into the row — and, since design/171 §6.3, an
* unknown key is a loud rejection rather than a silent drop: a producer one release ahead of this worker
* must learn that its new field would not survive the parked leg.
*/
export declare function validatePendingSteer(steer: PendingSteerInput): Omit<PendingSteerEntry, "seq">;
/** design/171 §6.1 — the `actor` id/issuer bound. Attribution labels ride the durable row and the model
* context, so they are capped like every other persisted model-facing string on this row. */
export declare const MAX_ACTOR_FIELD_CHARS = 256;
/** design/171 §6.3 — bound on the caller-supplied correlation id (a service's own message id). */
export declare const MAX_STEER_INPUT_ID_CHARS = 128;
/** design/171 §6.1 — the frozen field set of an {@link ActorAssertion} (same fail-loud posture as the
* entry's own set: an unknown key means a producer's provenance field would vanish silently). */
export declare const ACTOR_ASSERTION_FROZEN_FIELDS: readonly ["id", "hostAsserted", "issuer"];
/**
* design/171 §5.3 — THE single read point for a checkpoint's parked steers, in delivery (`seq`) order.
* Every consumer — the resume drain, the wake's nothing-to-deliver guard, a serving layer — goes through
* here, so no site can grow its own idea of what "the parked steer" is.
*
* A row that still carries the RETIRED pre-queue single seat (`state.pendingSteer`, written before the
* queue existed) is refused here rather than read: the seat has no `inputId`, `seq` or actor, so folding
* it in would mint a synthetic identity nothing else can name. The refusal is `checkpoint.unsupported_version`
* with `reason: "seat_retired"` — terminal for the row's bytes; the host decides the parked instruction
* on its own surface and parks it again through `setPendingSteer` if it still applies.
*/
export declare function readPendingSteerQueue(state: Pick<CheckpointState, "pendingSteerQueue">): PendingSteerEntry[];
/**
* design/171 §5.3/§6.3 — THE append, shared by every backend so the bounds and the ordering are one
* implementation rather than one per store.
*
* Returns the NEW queue (the caller persists it); throws `steering.queue_full` when either bound would
* be crossed. Re-appending an entry whose `inputId` is already queued is a NO-OP returning the queue
* unchanged — that is what keeps `setPendingSteer` retry-safe now that it appends instead of overwriting.
*/
export declare function appendPendingSteer(state: Pick<CheckpointState, "pendingSteerQueue">, entry: Omit<PendingSteerEntry, "seq">): PendingSteerEntry[];
/**
* REF-A4 — THE row half of every guarded checkpoint transition's CAS predicate, shared by every backend:
* the row must EXIST, its `scope` must match (multi-tenant isolation, §2.1 — a wrong-scope caller never
* wins), and it must currently be in `status`. `resolve` requires `"pending"`, `reopen` requires
* `"resolved"` (the inverse gate), `setPendingSteer`/`expire` require `"pending"`.
*
* PURE: it decides nothing about persistence. Each backend keeps its own commit half — the in-memory one
* flips fields in place, the file one appends+fsyncs the event BEFORE the flip (RB-150: commit ordering is
* that backend's own correctness core and must never be absorbed into a shared abstraction).
*/
export declare function checkpointRowMatches(cp: Checkpoint | undefined, scope: string, status: Checkpoint["status"]): cp is Checkpoint;
/**
* REF-A4 — THE optimistic-concurrency half of `resolve`'s CAS predicate (design/80 D-1), shared by every
* backend. The caller validated its reopen-by-reason guard against a `get()` snapshot; if a concurrent
* resolve/reopen cycle moved the monotonic `rev` in the get→resolve window, that validation is stale ⇒
* lose the CAS (fail-closed), forcing a re-get + re-validate. Keyed on `rev`, NOT on `reopenReason` — a
* 2-valued enum can ABA back to a prior value while the winner changed (round-2 BLOCKER fix). An absent
* expectation opts out of the guard entirely.
*/
export declare function checkpointOccMatches(cp: Checkpoint, expect?: ResolveExpectation): boolean;
/** Fault-injection mode for {@link InMemoryCheckpointStore.testInjectFault} (council #5). One-shot. */
export type CheckpointFaultMode =
/** `resolve` commits the CAS (status → resolved) then throws — simulates a crash *after* the commit
* but before the caller is acked, so a retry must see `resolved` and NOT re-execute (idempotency). */
"resolve-after-commit"
/** `resolve` throws *before* the CAS — simulates a crash before the commit; the row stays `pending`
* so a retry can still win it. */
| "resolve-before-commit"
/** design/384 slice 2 — `claimTerminal` wins its claim (the commit lands) then throws before the
* caller is acked: the claim-law recovery scenario (law 3/4 — a reject proves nothing; the retry's
* loss answer carries the truth and a responsibility-bearing consumer must NOT self-attribute). */
| "claim-after-commit"
/** design/384 slice 2 — `claimTerminal` throws *before* its CAS: the row is untouched (still
* pending), so a retry can still win the same claim. */
| "claim-before-commit";
/**
* Default in-process {@link CheckpointStore}. Single-instance / tests only — it does NOT survive a
* restart or span replicas, so it cannot deliver the cross-process guarantee a durable backend does.
* Single-threaded JS already serializes `resolve`, so the CAS is trivially atomic here;
* {@link testInjectFault} simulates the crash-recovery races a real backend must survive.
*/
export declare class InMemoryCheckpointStore implements CheckpointStore {
/** In-memory rows die with the process, but there is no scheduled tenant-scoped deletion contract
* here — honestly `"none"` (a locked retention policy must refuse this store). */
readonly retention: "none";
/** design/173 §2.3 — honest declaration: in-memory rows die with the process. */
readonly durability: "process-local";
/** Honest declaration on the fidelity axis: `put`/`get` are `structuredClone`, so a row keeps the
* whole structured-clone domain — a `Date` filed here is a `Date` when it comes back. */
readonly fidelity: "structured-clone";
/** F-012 L2: this store's `reopen` is a real resolved→pending CAS (below) — declared, not sniffed. */
readonly redecision: {
readonly reopen: true;
};
/** The execution-outcome record verb below is the contract's CAS, and `reopen` honours the record. */
readonly execution: {
readonly outcome: true;
};
private cps;
private fault;
put(token: CheckpointToken, cp: Checkpoint): Promise<void>;
get(token: CheckpointToken): Promise<Checkpoint | null>;
/** The loser's single-round-trip reading (claim law 1): what the row IS, in its own vocabulary —
* or `absent` for a missing row AND a wrong-scope row alike (the isolation rule: cross-scope truth
* in a loss answer would be a read bypass of the scope WHERE). Cloned projection, never a live ref. */
protected claimLossFrom(cp: Checkpoint | undefined, scope: string): TerminalClaimOutcome;
/**
* The ONE protected write core for the terminal-resolve claim — `resolve` and
* `claimTerminal({kind:"resolve"})` both run exactly this (claim law 2's one-machine obligation:
* one predicate, one commit, no drift window). Single-threaded JS makes the check + flip atomic; a
* durable backend folds the same predicate into its CAS WHERE clause.
*
* ORDER (design/384 slice 2, healing the pre-existing resolve shape): the winner is derived and
* CLONED — the point where an uncloneable `updatedInput` says so — BEFORE any field is written, and
* the commit is then one uninterrupted write group (status+rev+winner+reopenReason). The old order
* cloned AFTER the status/rev flip, so an uncloneable winner threw with the row already resolved
* and NO winner recorded; now the same throw leaves the row byte-identical (still pending) — the
* claim REJECTS (a reject proves nothing, law 3) instead of half-committing.
*/
protected claimResolveCore(token: CheckpointToken, scope: string, outcome: ResumeOutcome, expect?: ResolveExpectation): TerminalClaimOutcome;
/** The one protected write core for the terminal-expire claim — `expire` and
* `claimTerminal({kind:"expire"})` both run exactly this (claim law 2, same as the resolve core). */
protected claimExpireCore(token: CheckpointToken, scope: string): TerminalClaimOutcome;
resolve(token: CheckpointToken, scope: string, outcome: ResumeOutcome, expect?: ResolveExpectation): Promise<boolean>;
/** design/384 slice 2 — the atomic terminal claim (the interface JSDoc is the law's single
* normative statement). Implemented OVER the same protected cores `resolve`/`expire` run, which is
* the one-machine obligation discharged rather than restated. ONE fault plane with the one
* machine: a fault armed under the `resolve-*` names fires on the claim route of the SAME CAS too
* (`kind:"resolve"` only — expire never consumed resolve faults), so a crash simulation pinned
* against the two-step verb keeps firing byte-identically when the runner adopts the claim form.
*
* INHERITANCE OBLIGATION (a subclass provides `claimTerminal` whether it means to or not): a
* subclass that overrides `resolve`/`expire` to wrap the CAS (audit hooks, fault injection,
* interleave fixtures) must override THIS method consistently. Overriding the `protected` cores
* (`claimResolveCore`/`claimExpireCore`) instead covers every COMMIT on every route — the cores
* are the one write core all three verbs share — but NOT every ATTEMPT: `resolve`/`expire`
* evaluate the shared row predicate themselves and return `false` without entering a core when
* the row is not pending (missing, another scope, already terminal), while this claim route
* enters the core unconditionally and takes its loss reading from it. A core override therefore
* sees every winning claim and every OCC loss, but a row-predicate loss only when it arrived via
* the claim route; a hook that must record EVERY attempt, including those losses, on EVERY route
* wraps the three public verbs consistently. The same applies to a DELEGATING WRAPPER (Proxy / hand-built object)
* that forwards `claimTerminal` while hooking only `resolve`: forwarding IS providing, and the
* claim route is then the one that actually runs. A resolve-only wrap goes dark the moment a
* consumer prefers the claim route (the runner's resume does), which is a one-machine-law
* violation the SUBCLASS created — the base class cannot route through the public verbs
* instead, because an `await` between the CAS and the loss reading would break the law-1
* atomic unit this method exists to provide. (A wrapper that OMITS the member opts out
* cleanly: the consumer's presence probe then keeps the two-step path, hooks intact.) */
claimTerminal(token: CheckpointToken, scope: string, intent: TerminalClaimIntent): Promise<TerminalClaimOutcome>;
recordExecutionOutcome(token: CheckpointToken, scope: string, gate: GateOutcome): Promise<ExecutionOutcomeRecordWord>;
reopen(token: CheckpointToken, scope: string, reason: ReopenReason): Promise<boolean>;
setPendingSteer(token: CheckpointToken, scope: string, steer: PendingSteerInput): Promise<boolean>;
expire(token: CheckpointToken, scope: string): Promise<boolean>;
reap(scope: string, cutoff: number): Promise<number>;
listByScope(scope: string): Promise<CheckpointSummary[]>;
listScopes(): Promise<string[]>;
/** Arm a one-shot fault on the next `resolve` (council #5: makes crash-recovery unit-testable). */
testInjectFault(mode: CheckpointFaultMode | null): void;
/** Test/inspection helper: number of stored checkpoints. */
get size(): number;
}
/**
* The ONE refusal of a retired mode seed, read PRE-CAS on every resume kind (resume-admission): a row whose `inheritedGate`
* still carries a retired member is refused as the engine refuses every retired row seat (`readPendingSteerQueue`) —
* `checkpoint.unsupported_version` with `reason: "seat_retired"` — terminal for the row's bytes on this engine, and
* nothing consumed: the row stays byte-identical and `pending`. No engine reads the retired keys into a mode any more; an
* engine at 7.38.x or earlier still does, so the row can be resumed there once (its next suspend writes `permissionMode`), or
* expired (`CheckpointStore.expire`).
*/
export declare function refuseRetiredModeSeed(state: Pick<CheckpointState, "inheritedGate">): void;
/**
* The mode seed a row records (the 7.34.0 shape), read at prepare after {@link refuseRetiredModeSeed} has screened the row:
* `permissionMode` must be a member of the closed set and `prePlanMode` a mode a plan can return to (else
* `config.permission_mode_invalid`, the row cannot be resumed under a word this engine does not know); the shell floor is
* `"always"` or absent. Nothing here migrates or guesses.
*/
export declare function recordedModeSeed(gate: Pick<NonNullable<CheckpointState["inheritedGate"]>, "permissionMode" | "prePlanMode" | "shellGate"> | undefined): {
permissionMode?: PermissionMode;
prePlanMode?: PermissionMode;
shellGate?: ShellGateDoctrine;
};