@sema-agent/core 5.20.0 → 5.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/dist/agents/agent-definition.d.ts +7 -0
- package/dist/agents/agent-transcript-tool.d.ts +30 -0
- package/dist/agents/builtin-agents.d.ts +60 -0
- package/dist/agents/cascade.d.ts +107 -0
- package/dist/agents/cumulative-stats.d.ts +56 -0
- package/dist/agents/observer.d.ts +162 -0
- package/dist/agents/peer-admission.d.ts +95 -0
- package/dist/agents/repair-loop.d.ts +226 -0
- package/dist/agents/retain-ledger.d.ts +141 -0
- package/dist/agents/roster-store.d.ts +105 -0
- package/dist/agents/send-message-tool.d.ts +92 -0
- package/dist/agents/session-util.d.ts +5 -0
- package/dist/agents/subagent-steps.d.ts +66 -0
- package/dist/agents/subagent.d.ts +600 -0
- package/dist/agents/suspend-guard.d.ts +29 -0
- package/dist/agents/teacher.d.ts +75 -0
- package/dist/agents/team.d.ts +120 -1
- package/dist/agents/tool-filter.d.ts +34 -0
- package/dist/agents/verify.d.ts +198 -0
- package/dist/bench/metrics.d.ts +455 -0
- package/dist/brain/anthropic.d.ts +30 -0
- package/dist/brain/circuit-breaker.d.ts +33 -0
- package/dist/brain/context-overflow.d.ts +60 -3
- package/dist/brain/degrading.d.ts +67 -0
- package/dist/brain/errors.d.ts +42 -0
- package/dist/brain/failover.d.ts +15 -0
- package/dist/brain/media-degrade.d.ts +39 -0
- package/dist/brain/model-presets.d.ts +31 -0
- package/dist/brain/open-responses.d.ts +19 -0
- package/dist/brain/openai.d.ts +46 -0
- package/dist/brain/reasoning.d.ts +106 -1
- package/dist/brain/repetition.d.ts +83 -0
- package/dist/brain/request-params.d.ts +56 -0
- package/dist/brain/retry.d.ts +46 -0
- package/dist/brain/routing.d.ts +12 -0
- package/dist/brain/status-sink.d.ts +30 -1
- package/dist/brain/stream-engine.d.ts +147 -0
- package/dist/brain/stream-shared.d.ts +34 -0
- package/dist/brain/terminal-cause.d.ts +31 -0
- package/dist/brain/timeout.d.ts +107 -0
- package/dist/brain/tool-call-id.d.ts +20 -0
- package/dist/brain/tool-call-repair.d.ts +13 -0
- package/dist/config/catalog.d.ts +47 -0
- package/dist/config/defaults.d.ts +33 -0
- package/dist/core/a2a-task-state.d.ts +53 -0
- package/dist/core/a2a.d.ts +51 -0
- package/dist/core/arg-summary.d.ts +62 -0
- package/dist/core/ask-question.d.ts +272 -2
- package/dist/core/auto-compaction.d.ts +467 -0
- package/dist/core/auto-compaction.js +20 -6
- package/dist/core/auto-mode-prompt-assets.d.ts +8 -0
- package/dist/core/auto-mode-prompt.d.ts +27 -0
- package/dist/core/auto-mode.d.ts +54 -3
- package/dist/core/auto-promote.d.ts +100 -0
- package/dist/core/background-agent-store.d.ts +292 -0
- package/dist/core/background-shell.d.ts +110 -0
- package/dist/core/cache-break-detector.d.ts +34 -0
- package/dist/core/canonical-json.d.ts +57 -0
- package/dist/core/checkpoint-store.d.ts +1574 -16
- package/dist/core/compliance.d.ts +30 -0
- package/dist/core/consolidate-scope.d.ts +75 -0
- package/dist/core/context-edit.d.ts +99 -0
- package/dist/core/context-guard.d.ts +46 -0
- package/dist/core/exec-gate.d.ts +44 -0
- package/dist/core/exec-output-tail.d.ts +61 -0
- package/dist/core/file-snapshot-store.d.ts +104 -0
- package/dist/core/fs-write-gate-policy.d.ts +36 -0
- package/dist/core/git-worktree-env.d.ts +31 -0
- package/dist/core/governance-codes.d.ts +19 -0
- package/dist/core/hooks.d.ts +608 -1
- package/dist/core/human-input-projection.d.ts +37 -0
- package/dist/core/human-input-projection.js +13 -0
- package/dist/core/image-downsample.d.ts +74 -0
- package/dist/core/locked-config.d.ts +37 -0
- package/dist/core/lsp-diagnostics.d.ts +77 -0
- package/dist/core/lsp-protocol.d.ts +29 -0
- package/dist/core/lsp-session.d.ts +60 -1
- package/dist/core/lsp.d.ts +150 -1
- package/dist/core/mailbox-store.d.ts +57 -0
- package/dist/core/mcp.d.ts +381 -0
- package/dist/core/media-byte-cap.d.ts +21 -0
- package/dist/core/memory-admission.d.ts +71 -0
- package/dist/core/memory-engine/content-origin.d.ts +64 -0
- package/dist/core/memory-engine/data-plane.d.ts +43 -0
- package/dist/core/memory-engine/dual-root.d.ts +24 -0
- package/dist/core/memory-engine/engine.d.ts +230 -0
- package/dist/core/memory-engine/engine.js +103 -35
- package/dist/core/memory-engine/file-backend.d.ts +155 -0
- package/dist/core/memory-engine/frontmatter.d.ts +16 -0
- package/dist/core/memory-engine/layout.d.ts +217 -0
- package/dist/core/memory-engine/memory-backend-contract.d.ts +12 -0
- package/dist/core/memory-engine/migrate.d.ts +9 -0
- package/dist/core/memory-engine/scan.d.ts +12 -0
- package/dist/core/memory-engine/scope-contract.d.ts +61 -1
- package/dist/core/memory-engine/sync-client.d.ts +87 -0
- package/dist/core/memory-engine/sync.d.ts +60 -0
- package/dist/core/memory-engine/tools.d.ts +41 -0
- package/dist/core/memory-engine/types.d.ts +188 -0
- package/dist/core/memory-recall.d.ts +141 -0
- package/dist/core/memory-vector.d.ts +20 -0
- package/dist/core/memory.d.ts +458 -0
- package/dist/core/message-utils.d.ts +6 -0
- package/dist/core/oracle-isolation.d.ts +69 -0
- package/dist/core/permission-rule-consent.d.ts +138 -0
- package/dist/core/permission-rule-model.d.ts +122 -0
- package/dist/core/permission-rule-store.d.ts +119 -3
- package/dist/core/permission-rules.d.ts +87 -1
- package/dist/core/present-plan-tool.d.ts +20 -0
- package/dist/core/pricing.d.ts +26 -0
- package/dist/core/property-harness.d.ts +86 -0
- package/dist/core/protocol-naming.d.ts +38 -0
- package/dist/core/protocol-table.d.ts +61 -0
- package/dist/core/push-queue.d.ts +1 -0
- package/dist/core/remote-env.d.ts +383 -1
- package/dist/core/retention-policy.d.ts +33 -0
- package/dist/core/retention.d.ts +51 -0
- package/dist/core/roles.d.ts +59 -0
- package/dist/core/runner/active-skill-scope.d.ts +34 -0
- package/dist/core/runner/assemble-result.d.ts +134 -0
- package/dist/core/runner/compaction-call-options.d.ts +97 -1
- package/dist/core/runner/grounding-signal.d.ts +10 -0
- package/dist/core/runner/image.d.ts +17 -0
- package/dist/core/runner/image.js +29 -15
- package/dist/core/runner/memory-consolidation.d.ts +60 -0
- package/dist/core/runner/prepare-memory.d.ts +59 -0
- package/dist/core/runner/prepare-task.d.ts +1011 -2
- package/dist/core/runner/prepare-task.js +12 -11
- package/dist/core/runner/prompt-suggestions.d.ts +7 -0
- package/dist/core/runner/runtask.d.ts +304 -3
- package/dist/core/runner/session-file-state-replay.d.ts +49 -0
- package/dist/core/runner/session-rule-policy.d.ts +58 -0
- package/dist/core/runner/strict-output-schema.d.ts +18 -3
- package/dist/core/runner/synthetic-tools.d.ts +113 -0
- package/dist/core/runner/teardown-bounded.d.ts +34 -0
- package/dist/core/runner/tool-disclosure.d.ts +227 -0
- package/dist/core/runner/tool-output-projection.d.ts +6 -0
- package/dist/core/runner/turn-attachments.d.ts +634 -0
- package/dist/core/runner/usage-accounting.d.ts +32 -0
- package/dist/core/runtime.d.ts +9 -0
- package/dist/core/safe-notify.d.ts +64 -0
- package/dist/core/safety-axis-vocab.d.ts +23 -0
- package/dist/core/safety-merge-corpus.d.ts +37 -0
- package/dist/core/scheduler.d.ts +121 -0
- package/dist/core/secret-env.d.ts +32 -0
- package/dist/core/select-model.d.ts +15 -0
- package/dist/core/sensitive-path-policy.d.ts +42 -0
- package/dist/core/session-policy-store.d.ts +94 -0
- package/dist/core/session-reconcile.d.ts +80 -0
- package/dist/core/session-store.d.ts +85 -0
- package/dist/core/session.d.ts +153 -0
- package/dist/core/shared-memory/contract.d.ts +22 -0
- package/dist/core/shared-memory/normalize.d.ts +123 -2
- package/dist/core/shared-memory/tools.d.ts +14 -0
- package/dist/core/shared-memory/types.d.ts +105 -0
- package/dist/core/shutdown-debug.d.ts +6 -0
- package/dist/core/side-query.d.ts +38 -0
- package/dist/core/side-query.js +6 -1
- package/dist/core/skill-tool-specifier.d.ts +72 -0
- package/dist/core/skills-directory.d.ts +100 -1
- package/dist/core/spec-contract.d.ts +89 -0
- package/dist/core/store-contracts/background-agent-store-contract.d.ts +31 -0
- package/dist/core/store-contracts/checkpoint-store-contract.d.ts +26 -0
- package/dist/core/store-contracts/contract-harness.d.ts +20 -0
- package/dist/core/store-contracts/contract-kit-version.d.ts +27 -0
- package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +8 -0
- package/dist/core/store-contracts/mailbox-store-contract.d.ts +42 -0
- package/dist/core/store-contracts/tool-result-store-contract.d.ts +13 -0
- package/dist/core/strategy-store.d.ts +37 -0
- package/dist/core/stub-env.d.ts +7 -0
- package/dist/core/surrogate-safe-slice.d.ts +35 -0
- package/dist/core/task-notification.d.ts +202 -0
- package/dist/core/task-outcome.d.ts +53 -0
- package/dist/core/task-registry-agent.d.ts +337 -1
- package/dist/core/task-registry-monitor.d.ts +12 -0
- package/dist/core/task-registry-shared.d.ts +540 -0
- package/dist/core/task-registry.d.ts +343 -0
- package/dist/core/task-registry.js +13 -2
- package/dist/core/task-tool-shape.d.ts +44 -0
- package/dist/core/tighten-task-spec.d.ts +21 -0
- package/dist/core/tool-detach.d.ts +21 -0
- package/dist/core/tool-errors.d.ts +131 -0
- package/dist/core/tool-errors.js +4 -0
- package/dist/core/tool-name-aliases.d.ts +27 -0
- package/dist/core/tool-policy.d.ts +555 -0
- package/dist/core/tool-result-budget.d.ts +32 -0
- package/dist/core/tool-result-store.d.ts +174 -1
- package/dist/core/tools.d.ts +45 -0
- package/dist/core/trace.d.ts +323 -0
- package/dist/core/types.d.ts +3859 -2
- package/dist/core/untrusted-egress.d.ts +8 -0
- package/dist/core/untrusted-text.d.ts +156 -0
- package/dist/core/usage-window-store.d.ts +95 -0
- package/dist/core/version.d.ts +1 -0
- package/dist/core/warm-resume.d.ts +17 -0
- package/dist/core/wiring-manifest.d.ts +169 -0
- package/dist/core/with-retry.d.ts +24 -0
- package/dist/core/workflow-journal-store.d.ts +160 -0
- package/dist/core/workflow-run-store-contract.d.ts +25 -0
- package/dist/core/workflow-run-store.d.ts +119 -0
- package/dist/engine/compaction/compaction.d.ts +256 -1
- package/dist/engine/compaction/utils.d.ts +94 -0
- package/dist/engine/execution-env/kill-tree.d.ts +29 -0
- package/dist/engine/execution-env/node-execution-env.d.ts +94 -0
- package/dist/engine/harness/agent-harness.d.ts +116 -0
- package/dist/engine/harness/agent-harness.js +3 -14
- package/dist/engine/harness/messages.d.ts +15 -0
- package/dist/engine/harness/types.d.ts +464 -2
- package/dist/engine/llm/diagnostics.d.ts +4 -0
- package/dist/engine/llm/event-stream.d.ts +3 -0
- package/dist/engine/llm/index.d.ts +7 -0
- package/dist/engine/llm/types.d.ts +500 -3
- package/dist/engine/llm/validation.d.ts +3 -0
- package/dist/engine/loop/agent-loop.d.ts +87 -2
- package/dist/engine/loop/runtime-deps.d.ts +7 -0
- package/dist/engine/loop/types.d.ts +424 -0
- package/dist/engine/lsp/frame-decoder.d.ts +13 -0
- package/dist/engine/lsp/node-lsp-manager.d.ts +107 -0
- package/dist/engine/lsp/stdio-lsp-transport.d.ts +7 -0
- package/dist/engine/session/import-validate.d.ts +27 -0
- package/dist/engine/session/log-digest.d.ts +93 -0
- package/dist/engine/session/memory-repo.d.ts +6 -0
- package/dist/engine/session/memory-storage.d.ts +2 -0
- package/dist/engine/session/session.d.ts +75 -0
- package/dist/engine/session/storage-base.d.ts +8 -0
- package/dist/fixtures/index.d.ts +36 -0
- package/dist/index.d.ts +16 -1
- package/dist/index.js +0 -1
- package/dist/internal/harness-types.d.ts +6 -0
- package/dist/internal/harness.d.ts +11 -0
- package/dist/internal/llm.d.ts +6 -0
- package/dist/orchestration/builtin-workflows.d.ts +53 -0
- package/dist/orchestration/dev-vm-script-runner.d.ts +19 -0
- package/dist/orchestration/goal.d.ts +57 -0
- package/dist/orchestration/goal.js +3 -0
- package/dist/orchestration/run-spec.d.ts +42 -0
- package/dist/orchestration/run-workflow-tool.d.ts +169 -0
- package/dist/orchestration/workflow-governance.d.ts +61 -0
- package/dist/orchestration/workflow-meta.d.ts +28 -0
- package/dist/orchestration/workflow-observe.d.ts +60 -0
- package/dist/orchestration/workflow-primitives.d.ts +23 -1
- package/dist/orchestration/workflow-sandbox-conformance.d.ts +38 -0
- package/dist/orchestration/workflow-script-runner.d.ts +88 -0
- package/dist/orchestration/workflow-script-store.d.ts +98 -0
- package/dist/orchestration/workflow-size-guideline.d.ts +67 -0
- package/dist/orchestration/workflow-types.d.ts +169 -2
- package/dist/orchestration/workflow.d.ts +358 -0
- package/dist/prompt-assembly/artifact-store.d.ts +33 -0
- package/dist/prompt-assembly/artifact.d.ts +25 -0
- package/dist/prompt-assembly/assemble.d.ts +20 -0
- package/dist/prompt-assembly/composer.d.ts +29 -0
- package/dist/prompt-assembly/epoch.d.ts +55 -1
- package/dist/prompt-assembly/event-registry.d.ts +35 -0
- package/dist/prompt-assembly/explain.d.ts +12 -0
- package/dist/prompt-assembly/tool-catalog.d.ts +38 -0
- package/dist/prompt-assembly/turn-snapshot.d.ts +40 -0
- package/dist/prompt-assembly/types.d.ts +115 -0
- package/dist/prompts/coordinator.d.ts +27 -0
- package/dist/prompts/default.d.ts +539 -0
- package/dist/prompts/simple-sections.d.ts +45 -0
- package/dist/prompts/supervisor.d.ts +66 -0
- package/dist/scenarios/env.d.ts +28 -0
- package/dist/scenarios/full-body.d.ts +50 -0
- package/dist/scenarios/scenario-registry.d.ts +60 -0
- package/dist/scenarios/teacher-quickstart.d.ts +27 -0
- package/dist/server/http.d.ts +17 -0
- package/dist/stores/cc/lockfile.d.ts +6 -0
- package/dist/stores/cc/mailbox-store.d.ts +8 -0
- package/dist/stores/cc/roster-adapter.d.ts +4 -0
- package/dist/stores/cc/sidecar-transcript.d.ts +66 -0
- package/dist/stores/file/background-agent-store.d.ts +24 -0
- package/dist/stores/file/checkpoint-store.d.ts +38 -0
- package/dist/stores/file/file-snapshot-store.d.ts +25 -0
- package/dist/stores/file/fs-atomic.d.ts +155 -0
- package/dist/stores/file/index.d.ts +89 -0
- package/dist/stores/file/mailbox-store.d.ts +36 -0
- package/dist/stores/file/memory-store.d.ts +82 -0
- package/dist/stores/file/permission-rule-store.d.ts +79 -0
- package/dist/stores/file/session-policy-store.d.ts +28 -0
- package/dist/stores/file/session-store.d.ts +40 -0
- package/dist/stores/file/shared-ledger.d.ts +83 -0
- package/dist/stores/file/tool-result-store.d.ts +11 -0
- package/dist/stores/file/usage-window-store.d.ts +18 -0
- package/dist/stores/file/workflow-journal-store.d.ts +100 -0
- package/dist/stores/file/workflow-run-store.d.ts +11 -0
- package/dist/tools/fs/bash-readonly-classifier.d.ts +282 -0
- package/dist/tools/fs/encoding.d.ts +60 -0
- package/dist/tools/fs/fs-bash.d.ts +133 -0
- package/dist/tools/fs/fs-pdf.d.ts +28 -0
- package/dist/tools/fs/fs-search-tools.d.ts +5 -0
- package/dist/tools/fs/fs-shared.d.ts +360 -0
- package/dist/tools/fs/fs-write.d.ts +16 -0
- package/dist/tools/fs/gh-rate-limit.d.ts +22 -0
- package/dist/tools/fs/index.d.ts +79 -0
- package/dist/tools/fs/notebook.d.ts +56 -0
- package/dist/tools/fs/pdf.d.ts +70 -0
- package/dist/tools/fs/pdf.js +8 -2
- package/dist/tools/fs/repo-map.d.ts +10 -0
- package/dist/tools/fs/safety.d.ts +376 -0
- package/dist/tools/fs/search.d.ts +131 -0
- package/dist/tools/loop-tick.d.ts +24 -0
- package/dist/tools/monitor.d.ts +55 -0
- package/dist/tools/scheduler-tools.d.ts +51 -0
- package/dist/tools/sql-adapters.d.ts +18 -0
- package/dist/tools/sql.d.ts +25 -0
- package/dist/tools/task-list.d.ts +77 -0
- package/dist/tools/todo.d.ts +8 -0
- package/dist/tools/web.d.ts +184 -0
- package/dist/tools/worktree.d.ts +81 -0
- package/package.json +2 -2
- package/dist/tools/gitea-issue.d.ts +0 -13
- package/dist/tools/gitea-issue.js +0 -75
|
@@ -7,48 +7,225 @@ import type { ConsolidationNote } from "./runner/memory-consolidation.js";
|
|
|
7
7
|
import type { SessionPermissionRules } from "./session-policy-store.js";
|
|
8
8
|
import type { NestedUsageAccum, TaskResult } from "./types.js";
|
|
9
9
|
import type { ActiveWorktreeSession } from "../internal/harness-types.js";
|
|
10
|
+
/**
|
|
11
|
+
* design/45 — the **durable-checkpoint** primitive: cross-process / resumable `suspend`/`resume`.
|
|
12
|
+
*
|
|
13
|
+
* A {@link CheckpointStore} is the third pluggable durable seam (alongside `ToolResultStore` and
|
|
14
|
+
* `MemoryStore`): a token-addressable store of "enough state to resume a suspended task", with an
|
|
15
|
+
* **atomic CAS `resolve`** so a token is acted on **exactly once** even across replicas / double
|
|
16
|
+
* approvals. Two profiles share this one seam (design/43 原语 4, proven by two service instances):
|
|
17
|
+
*
|
|
18
|
+
* - **F4** (`gate.kind === "human"`) — a running task hit a tool-policy `ask` that no in-process
|
|
19
|
+
* approver can answer (headless / long / cross-replica), so it persists a checkpoint and returns
|
|
20
|
+
* `status:"suspended"`. Later `runner.resume(token, {gate:"policy_ask", decision})` continues it.
|
|
21
|
+
* This is the cross-process version of the synchronous `onAsk` gate (design/37).
|
|
22
|
+
* - **1C** (`gate.kind === "task_done"`) — design/38 Path A persists a background sub-task handle here
|
|
23
|
+
* so a caller can `get` its outcome across replicas. v1 stores the handle only; the parent does NOT
|
|
24
|
+
* mid-task suspend (Path A unchanged) — `runner.resume()` does NOT serve this gate in v1.
|
|
25
|
+
*
|
|
26
|
+
* **opt-in, default-off**: with no `CheckpointStore` wired (and no durable approval mode requested), a
|
|
27
|
+
* policy `ask` still resolves the 1.63 way (synchronous `onAsk` / headless auto-deny) — behavior
|
|
28
|
+
* unchanged. Default {@link InMemoryCheckpointStore} is process-only (single instance / tests); a
|
|
29
|
+
* durable backend (service's `tidb-approval-store` / `tidb-run-store`) makes it cross-replica.
|
|
30
|
+
*
|
|
31
|
+
* See `design/45-durable-checkpoint原语-suspend-resume.md`.
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* A high-entropy, single-use checkpoint token. Branded so it can't be confused with a `sessionId` /
|
|
35
|
+
* `taskId` (council Question #2): the token doubles as the resume capability (token-as-auth, §6), so a
|
|
36
|
+
* mix-up would be a security bug, not just a type slip. Mint with {@link mintCheckpointToken}.
|
|
37
|
+
*/
|
|
10
38
|
export type CheckpointToken = string & {
|
|
11
39
|
readonly __brand: "CheckpointToken";
|
|
12
40
|
};
|
|
41
|
+
/** Mint a CSPRNG 128-bit checkpoint token (token-as-auth, §6/Q5): unguessable, never logged/in-URL. */
|
|
13
42
|
export declare function mintCheckpointToken(): CheckpointToken;
|
|
43
|
+
/**
|
|
44
|
+
* Who resumes a checkpoint, and how the resume `outcome` is interpreted. The `kind` is the discriminant
|
|
45
|
+
* that {@link ResumeOutcome} must match at the resume entry (council #3: prevents a `task_done` gate
|
|
46
|
+
* being resumed with a `policy_ask` outcome → type confusion / corruption).
|
|
47
|
+
*/
|
|
48
|
+
/** design/74: why a resource-slice suspend fired. `"budget"` covers cost OR tokens (both are checked by
|
|
49
|
+
* `overBudget`); `"walltime"` is the soft slice deadline; `"turns"` the slice turn cap.
|
|
50
|
+
* design/80 Seam #2: `"preempt"` is NOT a resource limit — it is an EXTERNAL scheduler yield (the scheduler
|
|
51
|
+
* raised `spec.preemptSignal` to free resources for a higher-priority task). It rides the SAME durable-suspend
|
|
52
|
+
* mechanism: the gate.kind stays `resource_limit` (the mechanism), so the resume path is byte-identical to a
|
|
53
|
+
* budget/turns/walltime resume (continue the work, no decision); `"preempt"` is only the CAUSE. */
|
|
54
|
+
/**
|
|
55
|
+
* Why a `resource_limit` suspend was minted. design/164 split the single `budget` member: a token
|
|
56
|
+
* window and a $ window are different allowances with different owners, and a scheduler reading a
|
|
57
|
+
* checkpoint inbox to decide "grant more of what?" could not tell them apart. The reasons match the
|
|
58
|
+
* terminal codes the non-suspending tier reports (`limits.max_*_exceeded`), so one vocabulary covers
|
|
59
|
+
* both tiers.
|
|
60
|
+
*
|
|
61
|
+
* **Consumers pinning the old closed set**: `"budget"` is gone — a switch on it now falls through.
|
|
62
|
+
*
|
|
63
|
+
* design/164 件四/件五 added two more members, and they are a DIFFERENT CLASS from the task axes above:
|
|
64
|
+
* `"env_lifetime"` (the execution environment's platform lifetime is about to expire) and
|
|
65
|
+
* `"usage_window"` (a deployment-level usage-governance window is exhausted) are EXTERNAL causes — the
|
|
66
|
+
* task did not choose them and cannot avoid them by asking for less. They therefore suspend on the
|
|
67
|
+
* INFRASTRUCTURE eligibility alone (checkpoint store + non-volatile offload + remote/no owned env),
|
|
68
|
+
* without the `TaskSpec.resourceSuspend` opt-in the task axes require. When several causes land on one
|
|
69
|
+
* turn boundary the attribution order is
|
|
70
|
+
* `preempt > env_lifetime > usage_window > budget_tokens > budget_cost > turns > walltime`
|
|
71
|
+
* (external causes above self-chosen allowances; preempt keeps the top seat it always had).
|
|
72
|
+
*
|
|
73
|
+
* **Consumers pinning the closed set again**: a `switch` that was exhaustive over the five 5.x members
|
|
74
|
+
* now falls through on these two.
|
|
75
|
+
*/
|
|
14
76
|
export type ResourceLimitReason = "budget_tokens" | "budget_cost" | "walltime" | "turns" | "preempt" | "env_lifetime" | "usage_window";
|
|
77
|
+
/**
|
|
78
|
+
* design/164 件四/件五 — the EXTERNAL subset of {@link ResourceLimitReason}: causes that come from the
|
|
79
|
+
* platform or the operator rather than from the task's own declared allowances. Written as an `Extract`
|
|
80
|
+
* of the parent union so the two can never drift into disagreeing about what "external" means, and so a
|
|
81
|
+
* seam that may only be reached by an external cause (the infrastructure-only suspend lane) cannot be
|
|
82
|
+
* handed a task-axis reason by mistake.
|
|
83
|
+
*/
|
|
15
84
|
export type PlatformLimitReason = Extract<ResourceLimitReason, "env_lifetime" | "usage_window">;
|
|
85
|
+
/**
|
|
86
|
+
* design/80 D-2: which SAFETY MARK(s) on the tool caused an `ask` to mint an {@link CheckpointGate}
|
|
87
|
+
* `irreversible_ask`. Derived from the tool's STATIC spec marks (`ToolSpec.egress` / `ToolSpec.irreversibility`,
|
|
88
|
+
* round-1 council fix — NOT the per-call `decisionReason`, which missed a policy/hook pre-ask and was
|
|
89
|
+
* forgeable). Persisted on the gate so a network budget/escalation resolver reads WHY this is a safety ask FROM
|
|
90
|
+
* THE DURABLE RECORD (never re-derives risk from a model-controlled value — the model self-reports nothing
|
|
91
|
+
* here). Both can be true (a tool marked BOTH egress AND irreversible).
|
|
92
|
+
*/
|
|
16
93
|
export interface SafetyAxis {
|
|
94
|
+
/** design/70: the tool is egress-marked (`ToolSpec.egress` — an external write: push, open PR, send). */
|
|
17
95
|
egress?: boolean;
|
|
96
|
+
/** design/77 §4: the tool's irreversibility tier is `always` or `maybe` (`ToolSpec.irreversibility`). */
|
|
18
97
|
irreversible?: boolean;
|
|
19
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* design/80 §D-E: a DETERMINISTIC, REDACTED risk summary attached AT MINT to an escalation
|
|
101
|
+
* {@link CheckpointGate} (`human` / `irreversible_ask` — the tool-call approval escalations) so a supervisor
|
|
102
|
+
* INBOX can sort/triage by TRUE severity without re-deriving risk. **Display/triage metadata ONLY** — core
|
|
103
|
+
* NEVER reads it to gate / budget / suppress anything (structural ask strictly wins via `combinePolicies`;
|
|
104
|
+
* no mint suppression reads it). The profile/inbox READS it; core only ATTACHES it. Pure function of the
|
|
105
|
+
* call (no clock/random) — same call ⇒ identical descriptor.
|
|
106
|
+
*/
|
|
20
107
|
export interface RiskDescriptor {
|
|
108
|
+
/** ToolEmu-style severity tier 1..5 (5 = most severe). The inbox sorts DESC by this. Deterministic — the
|
|
109
|
+
* pure {@link riskSeverity} of {@link axes}. The ORDERING is what matters (the inbox's triage key). */
|
|
21
110
|
severity: 1 | 2 | 3 | 4 | 5;
|
|
111
|
+
/** Which safety axes tripped — for `irreversible_ask`, derived from the D-2 {@link SafetyAxis} (+ the
|
|
112
|
+
* shell-gate). `shell` marks a coarse shellGate tighten (a bash command gated only because the deployment
|
|
113
|
+
* set `shellGate`, with NO explicit per-tool egress/irreversible mark). Empty `{}` for a plain budgetable
|
|
114
|
+
* `human` ask. Self-contained so the inbox needn't cross-ref `safetyAxis`. */
|
|
22
115
|
axes: {
|
|
23
116
|
egress?: boolean;
|
|
24
117
|
irreversible?: boolean;
|
|
25
118
|
shell?: boolean;
|
|
26
119
|
};
|
|
120
|
+
/** The tool whose call is gated (mirrors the gate's `toolName`). */
|
|
27
121
|
toolName: string;
|
|
122
|
+
/** Ruled 2026-08-05 (forensics): the run's resolved shellGate doctrine, present ONLY when this ask
|
|
123
|
+
* was minted through the shell gate (coarse `always`, or a `classify` probe that declined to
|
|
124
|
+
* auto-allow). Absent for per-tool safety-axis / plain policy asks. Answers "which doctrine was
|
|
125
|
+
* live when this gate fired" from the persisted row alone — three artifact layers could not
|
|
126
|
+
* answer that in the field. */
|
|
28
127
|
shellGateDoctrine?: "classify" | "always";
|
|
128
|
+
/** A REDACTED, length-capped ONE-LINE summary of the call (the command for a shell gate; a brief key-arg
|
|
129
|
+
* digest otherwise) for the inbox preview. NEUTRALIZED via {@link import("./untrusted-text.js").inlineUntrusted}
|
|
130
|
+
* + length-capped (it is persisted + shown to a human inbox, so a crafted tool arg carrying a
|
|
131
|
+
* `</system-reminder>` variant / newline / huge string must NOT inject into the render or bloat storage).
|
|
132
|
+
* NEVER raw secrets / full args / env. Deterministic. Optional (omitted when nothing safe to summarize). */
|
|
29
133
|
summary?: string;
|
|
134
|
+
/** Best-effort file paths the action touches (fs-tool `path` args; shell parsing is deliberately NOT
|
|
135
|
+
* attempted — over-reaching a shell parse risks a wrong/forgeable path). Each path `inlineUntrusted`-capped.
|
|
136
|
+
* Omitted when none derivable. */
|
|
30
137
|
touchedPaths?: string[];
|
|
31
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* design/80 §D-E: the DETERMINISTIC severity tier (1..5) for an escalation checkpoint, a PURE function of the
|
|
141
|
+
* tripped {@link RiskDescriptor.axes} (NO LLM, NO clock/random) so the inbox's sort order is stable and
|
|
142
|
+
* unit-testable in isolation. The ORDERING is the contract; the absolute numbers map to ToolEmu's 5 tiers:
|
|
143
|
+
* - irreversible AND egress → **5** (the most severe — an external, irreversible write)
|
|
144
|
+
* - irreversible only → **4**
|
|
145
|
+
* - egress only → **3**
|
|
146
|
+
* - shell-gated tighten → **3** (a coarse shellGate ask with no explicit egress/irreversible mark — the
|
|
147
|
+
* command MAY be benign, so it is not auto-graded as high as a marked tool)
|
|
148
|
+
* - plain `human` ask → **2** (no safety axis tripped — a budgetable approval)
|
|
149
|
+
* - (1 reserved — least-severe / informational; not expected from these gates.)
|
|
150
|
+
* `egress` dominates the shell coarse-grade (an explicitly egress-marked shell tighten is still ≥3). A future
|
|
151
|
+
* arm may refine this — but only with a stated reason, and the ORDERING must stay monotone in risk.
|
|
152
|
+
*/
|
|
32
153
|
export declare function riskSeverity(axes: {
|
|
33
154
|
egress?: boolean;
|
|
34
155
|
irreversible?: boolean;
|
|
35
156
|
shell?: boolean;
|
|
36
157
|
}): 1 | 2 | 3 | 4 | 5;
|
|
158
|
+
/** design S1e (service [204]): the char cap for the {@link CheckpointSummary.toolInput} BOUNDED raw preview of a
|
|
159
|
+
* `tool_approval` pendingAction's `args` (`JSON.stringify`-ed). Caps the `listByScope` payload size; over-cap
|
|
160
|
+
* is truncated with a trailing `…`. Bounded raw (NOT neutralized) — redaction is the consumer's job (echo-only). */
|
|
37
161
|
export declare const MAX_TOOL_INPUT_PREVIEW_CHARS = 512;
|
|
162
|
+
/**
|
|
163
|
+
* HRD-APV-4: the char cap {@link validatePendingSteer} enforces on a parked operator steer — the last
|
|
164
|
+
* persisted, model-facing string on a checkpoint row that had none.
|
|
165
|
+
*
|
|
166
|
+
* Sized as an INSTRUCTION budget, not a preview budget: the siblings above cap echo-only projections at
|
|
167
|
+
* 200/512 chars, while a steer is prose a human wrote for the model to act on and is re-delivered on
|
|
168
|
+
* every resume of the checkpoint. 16k chars (~4k tokens) holds any realistic operator instruction and
|
|
169
|
+
* still bounds the durable row and the per-resume context cost. Exported so a BFF can validate before it
|
|
170
|
+
* calls {@link CheckpointStore.setPendingSteer} rather than discovering the reject at persist time.
|
|
171
|
+
*/
|
|
38
172
|
export declare const MAX_PENDING_STEER_CHARS = 16000;
|
|
173
|
+
/**
|
|
174
|
+
* design/171 §6.3 — the BYTE budget the whole parked-steer queue must fit inside, measured on its JSON
|
|
175
|
+
* serialization (the form every backend actually persists).
|
|
176
|
+
*
|
|
177
|
+
* 48,000 bytes sits under a MySQL `TEXT` column's 65,535-BYTE ceiling with room for UTF-8 expansion: the
|
|
178
|
+
* per-entry cap above counts CHARACTERS, and 16,000 CJK characters serialize to ~48,000 bytes, so a
|
|
179
|
+
* char-only cap does not bound the column at all. Both limits are enforced and either one can bind.
|
|
180
|
+
*/
|
|
39
181
|
export declare const PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES = 48000;
|
|
182
|
+
/**
|
|
183
|
+
* design/171 §6.3 — the queue's entry-count cap, DERIVED from the byte budget and the per-entry char cap
|
|
184
|
+
* (how many maximum-size entries the budget can hold). It bounds the count independently of the byte
|
|
185
|
+
* measure so a flood of tiny steers cannot grow the row unboundedly through per-entry overhead alone.
|
|
186
|
+
* Exceeding either limit is a fail-loud `steering.queue_full` — never a silent drop of the oldest entry
|
|
187
|
+
* (a dropped operator instruction is exactly the failure this queue exists to remove).
|
|
188
|
+
*/
|
|
40
189
|
export declare const MAX_PENDING_STEER_ENTRIES: number;
|
|
190
|
+
/**
|
|
191
|
+
* design/171 §6.3 — the FROZEN field set of a persisted {@link PendingSteerEntry}. `validatePendingSteer`
|
|
192
|
+
* copies by this set and REFUSES an unknown key, so a producer one release ahead of this worker gets a
|
|
193
|
+
* loud rejection instead of having its new field silently dropped on the parked leg. Consumers (other
|
|
194
|
+
* repos' backends) read it to assert they round-trip every field rather than hand-listing their own copy.
|
|
195
|
+
*/
|
|
41
196
|
export declare const PENDING_STEER_FROZEN_FIELDS: readonly ["text", "trusted", "actor", "seq", "inputId", "priority"];
|
|
197
|
+
/**
|
|
198
|
+
* design/171 §6.1 — WHO a piece of human input came from, as an attribution label. ONE definition:
|
|
199
|
+
* the shape lives at its layering-legal home (`engine/llm/types.ts`, where `UserMessage.actor`
|
|
200
|
+
* carries it and the engine cannot import core) and this module re-exports it — the live message
|
|
201
|
+
* seat and the parked entry can never drift apart. Normative clauses (never authority; ingress-only
|
|
202
|
+
* `hostAsserted`) are on the definition.
|
|
203
|
+
*/
|
|
42
204
|
import type { ActorAssertion } from "../internal/llm.js";
|
|
43
205
|
export type { ActorAssertion } from "../internal/llm.js";
|
|
206
|
+
/**
|
|
207
|
+
* design/171 §5.3/§6.3 — one entry of the parked-steer QUEUE. Replaces the former single seat, whose
|
|
208
|
+
* last-writer-wins overwrite silently destroyed an undelivered operator instruction whenever two people
|
|
209
|
+
* steered the same suspended run.
|
|
210
|
+
*/
|
|
44
211
|
export interface PendingSteerEntry {
|
|
212
|
+
/** The operator's instruction text (validated exactly like the pre-queue single seat). */
|
|
45
213
|
text: string;
|
|
214
|
+
/** Frozen at append time from the SERVICE's verified-principal check; never recomputed on resume. */
|
|
46
215
|
trusted: boolean;
|
|
216
|
+
/** Attribution only (see {@link ActorAssertion}) — absent = the caller supplied none (anonymous). */
|
|
47
217
|
actor?: ActorAssertion;
|
|
218
|
+
/** Queue-monotonic ordering key, minted by the store on append (first entry = 1). Drain order. */
|
|
48
219
|
seq: number;
|
|
220
|
+
/** Caller-supplied correlation/idempotency id (a service passes the message id it already minted);
|
|
221
|
+
* a uuidv7 is minted when absent. Re-appending the SAME `inputId` is a no-op, which is what keeps
|
|
222
|
+
* `setPendingSteer` retry-safe now that it appends instead of overwriting. */
|
|
49
223
|
inputId: string;
|
|
224
|
+
/** Carried VERBATIM for the serving layer; it does NOT reorder the drain (drain is `seq` order). */
|
|
50
225
|
priority?: SystemInjectionPriority;
|
|
51
226
|
}
|
|
227
|
+
/** design/171 §6.3 — the caller-supplied half of a {@link PendingSteerEntry}: everything except `seq`,
|
|
228
|
+
* which only the store may mint. `inputId` is optional on the way in (minted when absent). */
|
|
52
229
|
export interface PendingSteerInput {
|
|
53
230
|
text: string;
|
|
54
231
|
trusted: boolean;
|
|
@@ -56,59 +233,281 @@ export interface PendingSteerInput {
|
|
|
56
233
|
inputId?: string;
|
|
57
234
|
priority?: SystemInjectionPriority;
|
|
58
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* design/80 §D-E: build the DETERMINISTIC, REDACTED {@link RiskDescriptor} for an escalation checkpoint at
|
|
238
|
+
* MINT. Pure — a function ONLY of (`toolName`, `args`, the D-2 `safety` axis, the `shellGated` flag); it reads
|
|
239
|
+
* NO clock/random, so the SAME call ⇒ an IDENTICAL descriptor (pinned by a test).
|
|
240
|
+
*
|
|
241
|
+
* **Determinism contract = plain-DATA args** (the real flow: model-JSON / hook-rewritten plain objects). A
|
|
242
|
+
* Proxy whose `ownKeys` trap returns a DIFFERENT key set per call is OUT OF CONTRACT — JS cannot detect a Proxy
|
|
243
|
+
* (codex review Item 2), so its (display-only) digest may vary. This NEVER affects a security/budget/mint
|
|
244
|
+
* decision: `riskDescriptor` is INERT (no core path reads it to gate), so an out-of-contract input can at worst
|
|
245
|
+
* degrade an inbox preview, never a permission outcome.
|
|
246
|
+
*
|
|
247
|
+
* **Redaction is load-bearing** (the `summary`/`touchedPaths` are PERSISTED + surfaced to a human inbox):
|
|
248
|
+
* every model-controlled value goes through {@link inlineUntrusted} (folds CR/LF/Unicode separators to one
|
|
249
|
+
* space, defuses `</system-reminder>` variants + `<<<`/`>>>` fence sentinels) AND a length cap, so a
|
|
250
|
+
* crafted arg carrying a break-out tag / newline / huge string can neither inject into the inbox render nor
|
|
251
|
+
* bloat the durable row. NEVER dumps full args / env / secrets — only a bash command string or a short
|
|
252
|
+
* `name=value` digest of the SHOWN args.
|
|
253
|
+
*
|
|
254
|
+
* `summary` for a shell gate = the `command` string (capped 200 cp); otherwise a `name=value` digest of the
|
|
255
|
+
* call's top-level string/number/boolean args (each value capped), omitted when nothing safe to summarize.
|
|
256
|
+
* `touchedPaths` reads ONLY the obvious fs `path` arg (read_file/edit_file/write_file) — shell parsing is
|
|
257
|
+
* deliberately NOT attempted (over-reaching a shell parse risks a wrong/forgeable path, so OMIT for bash).
|
|
258
|
+
*/
|
|
59
259
|
export declare function buildRiskDescriptor(input: {
|
|
60
260
|
toolName: string;
|
|
61
261
|
args: unknown;
|
|
262
|
+
/** The D-2 {@link SafetyAxis} threaded to the mint (egress/irreversible), or `undefined` for a plain ask. */
|
|
62
263
|
safety?: SafetyAxis;
|
|
264
|
+
/** True ONLY when this is a `bash` call gated coarsely by `shellGate` (no explicit per-tool egress/irreversible
|
|
265
|
+
* mark) — drives the `shell` axis + severity-3 coarse grade. */
|
|
63
266
|
shellGated?: boolean;
|
|
267
|
+
/** The resolved doctrine to persist when `shellGated` (see {@link RiskDescriptor.shellGateDoctrine}). */
|
|
64
268
|
shellGateDoctrine?: "classify" | "always";
|
|
65
269
|
}): RiskDescriptor;
|
|
66
|
-
export type CheckpointGate =
|
|
270
|
+
export type CheckpointGate =
|
|
271
|
+
/** F4: a human (or any external authority) must allow/deny a pending tool call. design/80 §D-E:
|
|
272
|
+
* carries an OPTIONAL display-only {@link RiskDescriptor} (severity/axes/summary) for the supervisor
|
|
273
|
+
* inbox to triage by — INERT (core never reads it to gate/budget). */
|
|
274
|
+
{
|
|
67
275
|
kind: "human";
|
|
68
276
|
reason: string;
|
|
69
277
|
toolName: string;
|
|
70
278
|
riskDescriptor?: RiskDescriptor;
|
|
71
|
-
}
|
|
279
|
+
}
|
|
280
|
+
/** design/77 §4 (Gate 4) + design/80 D-2: a PRE-ACTION human approval before a SAFETY-tightened tool runs —
|
|
281
|
+
* an IRREVERSIBLE tool (send money/email, file a return) OR an EGRESS tool (push, open PR, send). Same
|
|
282
|
+
* approval family as `human` (a pending tool call to allow/deny → suspendRef → `status:"suspended"`, resumed
|
|
283
|
+
* with a `policy_ask` outcome). **design/80 D-2 (load-bearing):** minted whenever an `ask` is for a tool
|
|
284
|
+
* carrying the egress / irreversibility (`always`/`maybe`) SAFETY MARKS — regardless of how the ask arose
|
|
285
|
+
* (a gate tighten OR a policy/hook that already asked; round-1 council fix: keying on the per-call
|
|
286
|
+
* `decisionReason` missed the policy-ask case AND was forgeable) — **EVEN WHEN `durableApproval` is wired**
|
|
287
|
+
* (`durableApproval` supplies scope/ttl, NOT the kind). The DISJOINT-from-`human` kind is what a
|
|
288
|
+
* network budget resolver keys on to NEVER auto-approve a safety ask (a budget may only down-budget a plain
|
|
289
|
+
* `human` ask). {@link SafetyAxis} records which axis(es) tightened. NOT a dry-run review. */
|
|
290
|
+
| {
|
|
72
291
|
kind: "irreversible_ask";
|
|
73
292
|
reason: string;
|
|
74
293
|
toolName: string;
|
|
75
294
|
safetyAxis?: SafetyAxis;
|
|
76
295
|
riskDescriptor?: RiskDescriptor;
|
|
77
|
-
}
|
|
296
|
+
}
|
|
297
|
+
/** design/74: a resource slice limit (budget/walltime/turns) was reached — suspend (resumable) instead of
|
|
298
|
+
* fail. There is NO pending tool to adjudicate; resume just continues the run with the next slice's
|
|
299
|
+
* allowance (sized from {@link Checkpoint.resourceLedger}). The matching {@link ResumeOutcome} arm is
|
|
300
|
+
* `{ gate: "resource_limit"; decision: "continue" }`.
|
|
301
|
+
*
|
|
302
|
+
* design/164 件五: `resumeAfterMs` is the RESUME-TIMING hint that pairs with {@link Checkpoint.deadline}
|
|
303
|
+
* — how long the driver should wait before resuming, because resuming sooner would only re-suspend.
|
|
304
|
+
* **In-scope condition**: present ONLY for `reason: "usage_window"` (the one cause that knows when it
|
|
305
|
+
* clears); every other reason omits it, so a consumer must not read its presence as "this is
|
|
306
|
+
* resumable". Additive on the existing checkpoint version — a reader that ignores it is unchanged. */
|
|
307
|
+
| {
|
|
78
308
|
kind: "resource_limit";
|
|
79
309
|
reason: ResourceLimitReason;
|
|
80
310
|
resumeAfterMs?: number;
|
|
81
|
-
}
|
|
311
|
+
}
|
|
312
|
+
/** design/76 §2.5 (dry-run / shadow): a POST-PREDICTION REVIEW pause. A profile's dry-run interception ran a
|
|
313
|
+
* predicted action and produced a buffered state-diff a human (or judge) must REVIEW before it is applied —
|
|
314
|
+
* so the task suspends to the durable `needs_review` TERMINAL (`TaskStatus:"needs_review"`) instead of
|
|
315
|
+
* finishing. **DISJOINT from the approval family** (`human`/`irreversible_ask`): those are PRE-ACTION
|
|
316
|
+
* *approvals* (suspendRef → `status:"suspended"`, resumed with a `policy_ask` outcome); this is a
|
|
317
|
+
* POST-prediction *review* (reviewRef → `status:"needs_review"`, resumed with a `dry_run_review` outcome).
|
|
318
|
+
* The split is load-bearing — reusing the approval family would make assemble-result report `"suspended"`
|
|
319
|
+
* and the `needs_review`/`review.pending` branch dead code (v4 MAJOR-A). It is ALSO disjoint from the
|
|
320
|
+
* `RepairTerminal.needs_human_oracle` (design/78) — a different type space; never cross-use the two.
|
|
321
|
+
* The state-diff itself is a PROFILE concern (a REF, not stored here — design/76 §9 / §2.5); core only
|
|
322
|
+
* owns the gate/status/discriminant plumbing. NOT a pre-action approval. */
|
|
323
|
+
| {
|
|
82
324
|
kind: "needs_review";
|
|
83
325
|
reason: string;
|
|
84
|
-
}
|
|
326
|
+
}
|
|
327
|
+
/** design/80 D-B (plan-gate): a PRE-ACTION human PLAN REVIEW. Before an agent acts on a high-blast-radius
|
|
328
|
+
* plan (a profile's plan-gate fires on shell⇒gate + an irreversible/egress/finance mark present + a
|
|
329
|
+
* blast-radius trigger — NOT every task), the run pauses so a human can **approve / edit / reject the
|
|
330
|
+
* proposed PLAN** before any step runs. It is its OWN gate kind (r2 ruling — do NOT reuse `needs_review`,
|
|
331
|
+
* whose gateMatch arm forces a `dry_run_review` outcome; reusing it would make a plan resume a
|
|
332
|
+
* type-confusion mismatch), but it shares the **review-pause** machinery: it routes to `reviewRef` +
|
|
333
|
+
* `status:"needs_review"` (a human-review pause), resumed with a `plan_review` outcome.
|
|
334
|
+
*
|
|
335
|
+
* **THREE-WAY SPLIT (load-bearing):**
|
|
336
|
+
* - the APPROVAL family (`human`/`irreversible_ask`) → PRE-ACTION *approvals* of a pending TOOL CALL
|
|
337
|
+
* (suspendRef → `status:"suspended"`, resumed with a `policy_ask` outcome bound to a tool call);
|
|
338
|
+
* - `needs_review` → a POST-prediction *review* of a buffered state-diff (reviewRef →
|
|
339
|
+
* `status:"needs_review"`, resumed with a `dry_run_review` outcome);
|
|
340
|
+
* - `plan_review` (this) → a PRE-ACTION *review* of a PLAN (reviewRef → `status:"needs_review"`, resumed
|
|
341
|
+
* with a `plan_review` outcome). It binds NO tool call — the human reviews the PLAN, not a pending arg —
|
|
342
|
+
* so it carries no `tool_approval` pendingAction (its pendingAction is `{kind:"plan_review"}`, with no
|
|
343
|
+
* tool fields) and the `policy_ask` decision-action binding machinery never touches it. The plan/diff
|
|
344
|
+
* artifact is a PROFILE concern (a REF, not stored here — design/76 §9); core owns the
|
|
345
|
+
* gate/status/discriminant plumbing only. NOT a pre-action approval, NOT a dry-run review. */
|
|
346
|
+
| {
|
|
85
347
|
kind: "plan_review";
|
|
86
348
|
reason: string;
|
|
87
|
-
}
|
|
349
|
+
}
|
|
350
|
+
/** 1C: the checkpoint tracks a background sub-task's completion (design/38 Path A). v1 = handle only. */
|
|
351
|
+
| {
|
|
88
352
|
kind: "task_done";
|
|
89
353
|
};
|
|
354
|
+
/**
|
|
355
|
+
* The outcome a caller supplies to `runner.resume(token, outcome)` — a **discriminated union** keyed by
|
|
356
|
+
* `gate`, validated against `checkpoint.gate.kind` at the entry (council #3). **v1 implements only the
|
|
357
|
+
* `policy_ask` arm**: `task_done` is a dead branch under 1C Path A (the caller orchestrates, never
|
|
358
|
+
* mid-task `resume()`), typed here for completeness and the v2 join-suspend option.
|
|
359
|
+
*/
|
|
90
360
|
export type ResumeOutcome = {
|
|
91
361
|
gate: "policy_ask";
|
|
362
|
+
/**
|
|
363
|
+
* design/80 D-1 (decision-action binding): the `toolCallId` the human actually saw/approved. The
|
|
364
|
+
* resume is REJECTED (`checkpoint.invalid_outcome`, fail-closed, pre-CAS) unless it matches the
|
|
365
|
+
* checkpoint's pending tool call (`pendingAction.toolCallId`) — this closes the TOCTOU re-suspend
|
|
366
|
+
* wrong-apply: a stale `yes` minted against pending call X must not resolve a DIFFERENT pending call
|
|
367
|
+
* Y (approve vendor-A $5k applied to vendor-B). The decision must name the action it bound to; the
|
|
368
|
+
* runner never trusts that the caller matched the correct pending call (design/77 doctrine —
|
|
369
|
+
* deterministic structural backstop, not a model self-report).
|
|
370
|
+
*
|
|
371
|
+
* The correct value is the resumed checkpoint's `pendingAction.toolCallId`; the caller has it via the
|
|
372
|
+
* `suspendRef` / pending record it is answering.
|
|
373
|
+
*/
|
|
92
374
|
boundCallId: string;
|
|
375
|
+
/**
|
|
376
|
+
* design/80 D-1 §2 (slice 1a.2): the **server-minted opaque** hash of the pending tool call's input
|
|
377
|
+
* that the human actually saw/approved (`PendingAction.tool_approval.boundInputHash`, computed by the
|
|
378
|
+
* engine at suspend-mint via {@link import("./canonical-json.js").boundInputHashOf} over the post-hook
|
|
379
|
+
* `args`). The caller echoes it VERBATIM — it is opaque; the SDK/service NEVER re-canonicalize args
|
|
380
|
+
* (review r3: a second runtime's serialization could diverge → false mismatch → fail-closed a
|
|
381
|
+
* legitimate approval). The resume verifies it by **string equality** against the checkpoint's
|
|
382
|
+
* persisted value (`checkpoint.invalid_outcome`, fail-closed, pre-CAS) — closing the TOCTOU "same call
|
|
383
|
+
* id, different input" variant that `boundCallId` alone misses (a re-mint that swapped the input under
|
|
384
|
+
* the same tool-call id). It binds the SHOWN input, NOT `updatedInput`: an `allow` edit is the same
|
|
385
|
+
* operator's authorized rewrite, applied AFTER this binding check (design/37,末位应用), so the echoed
|
|
386
|
+
* hash is always the pending record's value regardless of any edit. (Legacy pre-1a.2 checkpoints have
|
|
387
|
+
* no persisted hash → the resume skips this check, binding on `boundCallId` alone; new mints enforce.)
|
|
388
|
+
*/
|
|
93
389
|
boundInputHash: string;
|
|
390
|
+
/** `allow` → execute the pending tool call; `deny` → inject a denial result and continue. */
|
|
94
391
|
decision: "allow" | "deny";
|
|
392
|
+
/** A re-written arg payload (design/37 policy `allow` rewrite); re-validated on execute. */
|
|
95
393
|
updatedInput?: unknown;
|
|
394
|
+
/** Model-readable reason attached to a `deny` (else a default is used). */
|
|
96
395
|
reason?: string;
|
|
396
|
+
/**
|
|
397
|
+
* The ANSWER to a CONTENT-ASK (the reserved AskUserQuestion tool), carried by the decide itself
|
|
398
|
+
* (ruled 2026-08-04). A content-ask is a question TO the user: `allow` alone says "let the tool
|
|
399
|
+
* run" but supplies nothing to run it AGAINST, and the only pre-existing answer seat was the
|
|
400
|
+
* resumed leg's `onQuestion` config closure — a seat an OFFLINE background child has no operator
|
|
401
|
+
* on, because its resume config is rebuilt from the parked row rather than handed in. With the
|
|
402
|
+
* answer on the decision payload, the same `/decide`-shaped call serves both arms: the resume
|
|
403
|
+
* binds it as the resumed leg's answering face, so the pending call executes against the SAME
|
|
404
|
+
* tool, the SAME `selected ⊆ options` fence and the SAME untrusted free-text framing (design/80
|
|
405
|
+
* D-F) as a live answer — one support surface, not two.
|
|
406
|
+
*
|
|
407
|
+
* TRUST: untrusted wire data. It is NEVER read as instructions — the tool splits it into the
|
|
408
|
+
* trusted selections (which must be the model's own option labels) and a fenced free-text
|
|
409
|
+
* remainder. Off-list `selected` values and `note` are exactly the "Other" free-text path.
|
|
410
|
+
*
|
|
411
|
+
* ARM-SCOPED, like `updatedInput`/`reason` on this same arm: only a `policy_ask` `allow` whose
|
|
412
|
+
* pending call IS the reserved tool may carry one. Supplying it anywhere else is rejected
|
|
413
|
+
* pre-CAS (`checkpoint.invalid_outcome`, `detail.field: "answer"`) rather than silently dropped,
|
|
414
|
+
* and OMITTING it on a content-ask allow with no live answering face is rejected the same way —
|
|
415
|
+
* consuming the approval to hand the model a fabricated "no human available" default is the
|
|
416
|
+
* silent-continue this seat exists to end.
|
|
417
|
+
*/
|
|
97
418
|
answer?: QuestionAnswer;
|
|
419
|
+
/**
|
|
420
|
+
* WHAT ENDED THIS APPROVAL — `"human"` (a person gave the final verdict), `"timeout"` (the
|
|
421
|
+
* configured window elapsed with no answer), or `"aborted"` (every other non-human end: the run
|
|
422
|
+
* was cancelled, the connection dropped, the store gave way, retries ran out). See
|
|
423
|
+
* {@link import("./tool-policy.js").ApprovalSettledBy} for the vocabulary itself.
|
|
424
|
+
*
|
|
425
|
+
* Attribution travels WITH the decision because it is a property OF the decision, not something
|
|
426
|
+
* a later reader can reconstruct: by the time an approval reaches a consumer, "denied" and "the
|
|
427
|
+
* window closed" have already collapsed into the same `decision:"deny"`, and only the caller that
|
|
428
|
+
* settled it still knows which happened. Core does not derive it, does not default it, and never
|
|
429
|
+
* infers it from some other outcome — it carries what the settling caller wrote onto the
|
|
430
|
+
* `tool_end` frame of the call this resume resolves.
|
|
431
|
+
*
|
|
432
|
+
* **Fill it at every settlement site.** A deployment resolving an approval from several places
|
|
433
|
+
* (an operator decide, an SLA sweep at the deadline, a cancel path) names the cause at each of
|
|
434
|
+
* them. Deriving it once, centrally, from the last outcome at hand is precisely how a window that
|
|
435
|
+
* elapsed gets reported to a person as another person's refusal.
|
|
436
|
+
*
|
|
437
|
+
* OPTIONAL and additive: absent means the caller named no source (a caller written before this
|
|
438
|
+
* field existed), and a consumer must NOT read a semantic out of the absence — core neither
|
|
439
|
+
* substitutes a default nor guesses. A value outside the three words is refused pre-CAS
|
|
440
|
+
* (`checkpoint.invalid_outcome`), like every other out-of-domain field on this arm: a settlement
|
|
441
|
+
* source nobody can read is worse than none at all. So is one that contradicts its own decision:
|
|
442
|
+
* `decision:"allow"` accepts only `"human"` or omission, because "nobody answered, therefore it
|
|
443
|
+
* ran" is not a record of anything that happened (a deployment whose CONFIGURATION auto-allows an
|
|
444
|
+
* elapsed window omits the field — no one ended that wait).
|
|
445
|
+
*
|
|
446
|
+
* SCOPE — this rides the settlement, NOT the persisted winner: `winnerFromOutcome` records the
|
|
447
|
+
* decision (and its rewrite/answer/reason), which is what an `env_failed` replay must reproduce
|
|
448
|
+
* exactly, and the source is not part of that authorization. A replay therefore states its own
|
|
449
|
+
* source and is not held to the first one. Binding it into the persisted winner would also make a
|
|
450
|
+
* re-supply that merely OMITS the field fail closed on the recovery path, which is the wrong place
|
|
451
|
+
* to add a new refusal; carrying it into the durable row is a store-shape change owned jointly
|
|
452
|
+
* with the backends and belongs to its own change, not to this one.
|
|
453
|
+
*/
|
|
98
454
|
settledBy?: import("./tool-policy.js").ApprovalSettledBy;
|
|
99
|
-
}
|
|
455
|
+
}
|
|
456
|
+
/** design/74: continue a resource-suspended run with the next slice's allowance. NO decision payload and
|
|
457
|
+
* NO budget figure — the allowance is computed from {@link Checkpoint.resourceLedger}, so money never
|
|
458
|
+
* reaches the caller or the model. Matches `CheckpointGate.kind === "resource_limit"`. */
|
|
459
|
+
| {
|
|
100
460
|
gate: "resource_limit";
|
|
101
461
|
decision: "continue";
|
|
102
|
-
}
|
|
462
|
+
}
|
|
463
|
+
/** design/76 §2.5 (dry-run / shadow): resolve a `needs_review` suspend after a human/judge reviewed the
|
|
464
|
+
* buffered predicted state-diff. `decision:"approve"` → the profile applies the buffered diff (atomically,
|
|
465
|
+
* invalidating `readFileState` on touched paths — design/76 §2.5; the apply itself is PROFILE, not core)
|
|
466
|
+
* then continues; `decision:"reject"` → the prediction is discarded and the run continues without it. Kept
|
|
467
|
+
* DISJOINT from `policy_ask` (the approval family) so the resume-side discriminant never confuses a
|
|
468
|
+
* post-prediction review with a pre-action approval (the third `needs_review`/`dry_run_review` gateMatch arm).
|
|
469
|
+
* No money/diff payload reaches the caller here — the diff lives in the profile's REF store (design/76 §9). */
|
|
470
|
+
| {
|
|
103
471
|
gate: "dry_run_review";
|
|
104
472
|
decision: "approve" | "reject";
|
|
105
473
|
reason?: string;
|
|
106
|
-
}
|
|
474
|
+
}
|
|
475
|
+
/** design/80 D-B (plan-gate): resolve a `plan_review` PRE-ACTION plan pause after a human reviewed the
|
|
476
|
+
* proposed PLAN. `decision:"approve"` → proceed with the plan as-is; `decision:"edit"` → proceed with the
|
|
477
|
+
* human's `editedPlan` (a TYPED sibling of the plan — NEVER a raw tool `updatedInput`; a plan is not a tool
|
|
478
|
+
* arg, so this binds NO action and never enters the `policy_ask` boundCallId/boundInputHash machinery);
|
|
479
|
+
* `decision:"reject"` → the model RE-PLANS (the rejected plan is discarded; a `reason` may steer the
|
|
480
|
+
* re-plan). Kept DISJOINT from `policy_ask` (approval) AND `dry_run_review` (post-prediction review) so the
|
|
481
|
+
* resume-side discriminant never confuses the three (the `plan_review`/`plan_review` gateMatch arm). It
|
|
482
|
+
* binds no TOOL CALL, but since RB-64 it does record a winner: `winnerFromOutcome` returns the decision
|
|
483
|
+
* (plus `editedPlan` as `updatedInput` and the reviewer `reason`) under the synthetic `gate:plan_review`
|
|
484
|
+
* marker, so an `env_failed` reopen can replay it. The `boundCallId`/`boundInputHash` TOOL-CALL binding
|
|
485
|
+
* is what stays `policy_ask`-only. No plan/diff payload (beyond the human's
|
|
486
|
+
* `editedPlan` text) reaches core — the plan artifact lives in the PROFILE's REF store (design/76 §9). */
|
|
487
|
+
| {
|
|
107
488
|
gate: "plan_review";
|
|
108
489
|
decision: "approve" | "edit" | "reject";
|
|
109
490
|
editedPlan?: string;
|
|
110
491
|
reason?: string;
|
|
111
|
-
}
|
|
492
|
+
}
|
|
493
|
+
/** design/144 §3 (message-wake): wake a PARKED (non-gate) checkpoint and drive it to continue — a pure
|
|
494
|
+
* un-park verb, **NOT a gate decision**: it never allows/denies/approves anything. Governance-gate
|
|
495
|
+
* purity is enforced at the resume entry (fail-closed, pre-CAS): a checkpoint awaiting ANY pending
|
|
496
|
+
* gate decision (`gate.kind` ∈ {human, irreversible_ask, needs_review, plan_review, resource_limit} —
|
|
497
|
+
* i.e. the tool_approval / dry_run_review / plan_review / resource_limit decision families) REJECTS a
|
|
498
|
+
* wake with `wake.gate_pending`, so wake can never become a side door around an approval. Only a
|
|
499
|
+
* non-gate park (e.g. a deployment-minted approval-timeout park / pure suspend) is wakeable.
|
|
500
|
+
*
|
|
501
|
+
* `message` (optional): operator content delivered to the resumed run — the ATOMIC equivalent of
|
|
502
|
+
* `setPendingSteer` + resume in one verb (same `validatePendingSteer` persist-time guards, same
|
|
503
|
+
* trusted/untrusted delivery framing as {@link CheckpointState.pendingSteer}). It rides the resume
|
|
504
|
+
* continuation's steer tail as its OWN segment, appended AFTER any already-parked `pendingSteer`
|
|
505
|
+
* (X5: both are delivered — the wake message never displaces a parked, undelivered supervisor steer;
|
|
506
|
+
* each segment is framed under its own `trusted` semantics, never one merged trust domain). WITHOUT a
|
|
507
|
+
* `message`, the checkpoint must already carry a parked `pendingSteer` — a wake with nothing to
|
|
508
|
+
* deliver is rejected (`wake.nothing_to_deliver`, anti spin-wake). Idempotency rides the existing
|
|
509
|
+
* resolve CAS: a second wake of the same token loses the CAS (`checkpoint.already_resolved`). */
|
|
510
|
+
| {
|
|
112
511
|
gate: "wake";
|
|
113
512
|
message?: {
|
|
114
513
|
text: string;
|
|
@@ -118,46 +517,220 @@ export type ResumeOutcome = {
|
|
|
118
517
|
gate: "task_done";
|
|
119
518
|
result: TaskResult;
|
|
120
519
|
};
|
|
520
|
+
/**
|
|
521
|
+
* The pending action a checkpoint suspends *before* — a **discriminated union** keyed by `kind`. Every
|
|
522
|
+
* consumer that reads the tool fields MUST branch on `kind` first: the `resource_limit` arm (design/74)
|
|
523
|
+
* has none (a resource slice suspend has no pending tool to resolve).
|
|
524
|
+
*
|
|
525
|
+
* `tool_approval` (§4): a tool call adjudicated `ask` in durable mode, captured with its **post-hook args**
|
|
526
|
+
* (the design/37 rewrite already applied) so resume executes the exact same call without re-running
|
|
527
|
+
* PreToolUse hooks (§11 M2). `batchToolCallIds` / `completedCallIds` capture the **mid-batch** position
|
|
528
|
+
* (§4.ter): one assistant turn can emit a batch of tool calls; if call #k hits `ask`, calls #1..k-1 already
|
|
529
|
+
* executed (in `completedCallIds`) and #k+1..N are still pending. ID-based (a `Set<toolCallId>`), not
|
|
530
|
+
* positional — immune to reordering / off-by-one (council Question #1), and it doubles as the reconcile
|
|
531
|
+
* suspended-batch discriminant (§15.2 net-add #7) so wake-reconcile never closes a suspended call.
|
|
532
|
+
*/
|
|
121
533
|
export type PendingAction = {
|
|
122
534
|
kind: "tool_approval";
|
|
123
535
|
toolCallId: string;
|
|
124
536
|
toolName: string;
|
|
537
|
+
/** Post-hook (design/37-rewritten) args to execute on `allow`. */
|
|
125
538
|
args: unknown;
|
|
539
|
+
/** [1245] (additive) — the tool's approval display projection of `args`, minted alongside the
|
|
540
|
+
* row (see AgentTool.approvalPreview; control-char-sanitized + 16KiB-clamped at mint).
|
|
541
|
+
* **UNTRUSTED, ADVISORY**: inboxes must escape contextually and surface `args` alongside.
|
|
542
|
+
* NEVER read by the resume path (args + boundInputHash stay the sole execution contract). */
|
|
126
543
|
preview?: unknown;
|
|
544
|
+
/** design/179 §4 (additive) — the persistable allow-rule forms this parked call could be covered by,
|
|
545
|
+
* minted from the SAME post-hook args the row executes. Advisory display metadata for the approval
|
|
546
|
+
* inbox's "stop asking me this" option; never read by the resume path, and never a rule by itself
|
|
547
|
+
* (minting one goes through the approval-record protocol). Absent when no rule lane is armed or the
|
|
548
|
+
* command is one the lane cannot speak for. */
|
|
127
549
|
ruleSuggestions?: readonly import("./permission-rule-model.js").RuleSuggestion[];
|
|
550
|
+
/**
|
|
551
|
+
* design/80 D-1 §2 (slice 1a.2): the server-minted **opaque** boundInputHash of {@link args} — a
|
|
552
|
+
* SHA-256 (hex) via {@link import("./canonical-json.js").boundInputHashOf}, computed ONCE here at
|
|
553
|
+
* suspend-mint and persisted on the row. The operator sees it (surfaced on the pending record) and
|
|
554
|
+
* echoes it back as {@link ResumeOutcome} `boundInputHash`; the resume verifies opaque string equality
|
|
555
|
+
* against THIS value (never re-serializing the args — see the field's doc). Absent only on a legacy
|
|
556
|
+
* pre-1a.2 checkpoint (deserialized without it), in which case the resume skips the hash check.
|
|
557
|
+
*/
|
|
128
558
|
boundInputHash: string;
|
|
559
|
+
/** All tool-call ids in the suspending assistant message, in emission order. */
|
|
129
560
|
batchToolCallIds: string[];
|
|
561
|
+
/** Ids of calls already executed (results in the session) when the suspend fired — #1..k-1. */
|
|
130
562
|
completedCallIds: string[];
|
|
131
|
-
}
|
|
563
|
+
}
|
|
564
|
+
/** design/74: suspended at a resource slice boundary — there is NO pending tool to resolve. Consumers
|
|
565
|
+
* that read the tool-approval fields above MUST branch on `kind` first (this arm has none). */
|
|
566
|
+
| {
|
|
132
567
|
kind: "resource_limit";
|
|
133
568
|
reason: ResourceLimitReason;
|
|
134
|
-
}
|
|
569
|
+
}
|
|
570
|
+
/** design/80 D-B (r3 — an EXPLICIT arm, NOT a reused `resource_limit` placeholder): paused at a PRE-ACTION
|
|
571
|
+
* PLAN REVIEW (`CheckpointGate.kind === "plan_review"`). There is NO pending tool to resolve — the human
|
|
572
|
+
* reviews the PLAN, not a tool call — so this arm has no tool fields. Every consumer that reads the
|
|
573
|
+
* `tool_approval` fields above MUST branch on `kind` FIRST: a `plan_review` checkpoint must never flow into
|
|
574
|
+
* a `resource_limit`- or `tool_approval`-shaped continuation (a contract test pins this). The plan artifact
|
|
575
|
+
* is a PROFILE concern (a REF — design/76 §9); core stores no plan here. */
|
|
576
|
+
| {
|
|
135
577
|
kind: "plan_review";
|
|
136
|
-
}
|
|
578
|
+
}
|
|
579
|
+
/** design/144 §3 (X2) — the NON-GATE arm: nothing pends a decision. The shape of a deployment-minted
|
|
580
|
+
* PARK checkpoint (approval-timeout park / pure suspend; paired with `gate.kind === "task_done"`, the
|
|
581
|
+
* non-gate gate arm) and of a 1C `task_done` handle row. It is the ONLY pendingAction kind the `wake`
|
|
582
|
+
* resume entry accepts — every other kind above is a pending GATE decision, and the wake gate rejects
|
|
583
|
+
* it (`wake.gate_pending`) via an EXHAUSTIVE switch (a future kind added here without classifying it
|
|
584
|
+
* there is a compile error, and an unknown kind on a deserialized row fails closed at runtime). Core
|
|
585
|
+
* never mints this arm itself (the park convention is the deployment half); the pre-existing doc
|
|
586
|
+
* references to "a task_done pendingAction" (`getCheckpointSummary` et al.) name exactly this shape. */
|
|
587
|
+
| {
|
|
137
588
|
kind: "task_done";
|
|
138
589
|
};
|
|
590
|
+
/**
|
|
591
|
+
* The per-task correctness state a checkpoint must carry so a resumed task runs in the **same state
|
|
592
|
+
* space** it suspended in (§4.bis, the jury head must-fix). An **explicit whitelist** of serializable
|
|
593
|
+
* correctness fields — never a blind `JSON.stringify(Prepared)`, which would silently corrupt the 9+
|
|
594
|
+
* non-serializable runtime objects (`harness`/`session`/`abortController`…) it holds (round-2 BUG#1).
|
|
595
|
+
*
|
|
596
|
+
* Round-trip fidelity of every field is covered by tests; runtime objects are **forbidden** here.
|
|
597
|
+
* `cacheFingerprint` (design/31) is deliberately **excluded** — it is observation-only, so resume just
|
|
598
|
+
* suppresses cache-break detection on the first turn rather than persisting it as correctness state.
|
|
599
|
+
*/
|
|
139
600
|
export interface CheckpointState {
|
|
601
|
+
/** design/36: deferred-tool activation set — else resume re-discloses / diverges from history. */
|
|
140
602
|
activeTools: string[];
|
|
603
|
+
/** 1.41 submit_output: the validated structured output set pre-suspend, else it's lost on resume. */
|
|
141
604
|
outputRef?: {
|
|
142
605
|
value?: unknown;
|
|
143
606
|
set?: boolean;
|
|
144
607
|
};
|
|
608
|
+
/** design/38: nested sub-agent cumulative cost — else pre-suspend child cost evaporates (§4.bis/Q7). */
|
|
145
609
|
nestedStats: NestedUsageAccum;
|
|
610
|
+
/** design/41: consolidation notes collected pre-suspend — else the task-end pass loses them. */
|
|
146
611
|
consolidationNotes?: ConsolidationNote[];
|
|
612
|
+
/** design/44: the hand's read-file state (content hashes), serialized from the hands-toolkit closure
|
|
613
|
+
* (NOT part of Prepared — §15.2 net-add #8). Without it a resumed `edit_file` is rejected "not read". */
|
|
147
614
|
readFileState?: Array<[string, ReadEntry]>;
|
|
615
|
+
/**
|
|
616
|
+
* design/78 Slice-1: the SAFE-tier self-repair loop's durable state (`failureTrace`/`diagnostics`/
|
|
617
|
+
* `rejectedHypotheses`/`attemptCount`/`oracleTier` — all JSON/`structuredClone`-safe, no fn/Date). Set ONLY
|
|
618
|
+
* when an orthogonal durable suspend (resource/HITL) interleaves a `runRepairLoop` run — the happy path is
|
|
619
|
+
* in-memory only. On resume it re-seeds `RepairLoopConfig.resumeBundle` so `attemptCount` advances
|
|
620
|
+
* MONOTONICALLY (never reset). `baselinePassTests` is deliberately NOT carried here (grader-computed
|
|
621
|
+
* out-of-process — a worker must not be able to shrink the ratchet). Absent for a run with no repair loop.
|
|
622
|
+
*/
|
|
148
623
|
repairBundle?: RepairBundle;
|
|
624
|
+
/**
|
|
625
|
+
* design/49 v1.5: the remote workspace's serializable identity (E2B sandbox id / provider / mount path /
|
|
626
|
+
* snapshot id). Set ONLY when the suspended task ran with a remote, suspendable {@link WorkspaceHandle}
|
|
627
|
+
* (a per-task `executionEnvFactory` env that was `suspendVM()`-paused rather than destroyed). Resume
|
|
628
|
+
* rebuilds the env via the factory and `resumeVM(snapshotId)` so the restored workspace matches the
|
|
629
|
+
* `readFileState` above. All-string fields → JSON/`structuredClone` round-trips safely (consistent with
|
|
630
|
+
* this whitelist's "no runtime objects" rule). Absent for a process-local (non-remote) suspend.
|
|
631
|
+
*/
|
|
149
632
|
workspaceHandle?: WorkspaceHandle;
|
|
633
|
+
/**
|
|
634
|
+
* design/80 D-A: a durable mid-task STEER for a DURABLY-SUSPENDED task. Live `TaskStream.steer`
|
|
635
|
+
* (runtask.ts) is unreachable while the harness is idle (durably suspended), so a human supervisor's
|
|
636
|
+
* guidance is parked HERE via {@link CheckpointStore.setPendingSteer} and injected on resume (runtask.ts,
|
|
637
|
+
* after the resume-continuation prompt). It is GUIDANCE ONLY — never an approval channel (§3 inv #4) and
|
|
638
|
+
* never parsed into control state (§3 inv #5); a budget/autonomy/gate-threshold is CONFIG, not steer text.
|
|
639
|
+
*
|
|
640
|
+
* `trusted` is FROZEN at `setPendingSteer` from the SERVICE's verified-principal check (an operator-role
|
|
641
|
+
* check, NOT a client header — the service's job, out of scope here) and NEVER recomputed on resume
|
|
642
|
+
* (§3 inv #1). On resume a `trusted:false` steer reaches the model as a PLAIN user message with NO
|
|
643
|
+
* `<system-reminder>` wrapper (no authority laundering, §3 inv #3); a `trusted:true` steer MAY ride the
|
|
644
|
+
* reminder (`formatHookFeedback`, mirroring the live trusted branch). `text` containing `</system-reminder>`
|
|
645
|
+
* is REJECTED at `setPendingSteer` (typed `steering.invalid_content`) so a dirty steer never enters this
|
|
646
|
+
* state (§3 inv #2); the untrusted-injection path ALSO sanitizes the text as untrusted data, belt-and-braces.
|
|
647
|
+
* Absent when no steer is pending. All-string fields → JSON/`structuredClone` round-trips safely.
|
|
648
|
+
*
|
|
649
|
+
* **Delivery is BEST-EFFORT, at-most-meaningfully-once (review-council, by design):** a steer is GUIDANCE,
|
|
650
|
+
* not a correctness-critical message, so the delivery guarantee is intentionally loose:
|
|
651
|
+
* - It rides EVERY resume of THIS checkpoint that runs a turn — including a faithful `env_failed`/
|
|
652
|
+
* `tool_unavailable` reopen→re-resume, which re-shows the guidance (coherent with the reopen REPLAYING the
|
|
653
|
+
* leg; the model re-does the work, re-seeing the steer). It is NOT carried onto a NEW re-suspend checkpoint
|
|
654
|
+
* (serializeCheckpointState stamps `undefined`) — a supervisor steers the new checkpoint afresh.
|
|
655
|
+
* - It is DROPPED (never delivered) on a resume that runs no turn (an exhausted-budget resume) or that is
|
|
656
|
+
* set in the get→resolve race window of an in-flight resume — both rare; the run is ending or the steer
|
|
657
|
+
* just missed its train. A supervisor re-issues `setPendingSteer` if a steer didn't land.
|
|
658
|
+
* A precise exactly-once delivery would need clearing the steer from the persisted row on consume (a reopen
|
|
659
|
+
* clear + a get→resolve interlock); deferred as not worth the cross-backend complexity for guidance text.
|
|
660
|
+
*
|
|
661
|
+
* **Superseded by {@link CheckpointState.pendingSteerQueue} (design/171 §5.3).** New writes go to the
|
|
662
|
+
* queue; this seat remains ONLY so rows written before the queue existed keep delivering their parked
|
|
663
|
+
* steer. Never read it directly — {@link readPendingSteerQueue} is the single read point that folds
|
|
664
|
+
* this legacy seat in as queue member 0.
|
|
665
|
+
*/
|
|
150
666
|
pendingSteer?: {
|
|
151
667
|
text: string;
|
|
152
668
|
trusted: boolean;
|
|
153
669
|
};
|
|
670
|
+
/**
|
|
671
|
+
* design/171 §5.3 — the BOUNDED, ORDERED queue of parked operator steers, drained in `seq` order on
|
|
672
|
+
* resume with every entry framed under its own trust semantics.
|
|
673
|
+
*
|
|
674
|
+
* It replaces the single {@link CheckpointState.pendingSteer} seat, whose last-writer-wins overwrite
|
|
675
|
+
* meant two people steering one suspended run silently destroyed the first instruction — and destroyed
|
|
676
|
+
* it with no attribution, so nobody could tell afterwards that anything was lost.
|
|
677
|
+
*
|
|
678
|
+
* Bounds are fail-loud, never lossy: {@link MAX_PENDING_STEER_ENTRIES} entries and
|
|
679
|
+
* {@link PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES} serialized bytes, whichever binds first, reject the
|
|
680
|
+
* APPEND with `steering.queue_full` rather than evicting an already-accepted instruction.
|
|
681
|
+
*
|
|
682
|
+
* Absent when nothing is parked. A row may legally carry BOTH this and the legacy seat (a pre-queue
|
|
683
|
+
* suspend that was steered again after the upgrade); the read helper orders the legacy seat first.
|
|
684
|
+
*/
|
|
154
685
|
pendingSteerQueue?: PendingSteerEntry[];
|
|
686
|
+
/**
|
|
687
|
+
* SR-7 (CC 2.1.198 orphaned-background-task notice: F6c pretty.js:698391-698398, resume leg
|
|
688
|
+
* :707384-707399 under the `CLAUDE_CODE_RESUME_INTERRUPTED_TURN` gate — the cloud-worker restart
|
|
689
|
+
* leg, exactly sema's durable-resume shape): the background tasks (pending/running, this run's
|
|
690
|
+
* owner triple) still ALIVE at suspend. Background processes never survive a suspend (design/103
|
|
691
|
+
* §3.7 unconditional dispose + 飞轮 [506]③ killed receipts), so on resume any snapshot entry NOT
|
|
692
|
+
* alive in the resume leg's registry is an ORPHAN — aggregated into ONE CC-verbatim "The container
|
|
693
|
+
* was restarted…" reminder appended to the resume continuation (single message: header +
|
|
694
|
+
* `- description (task id)` list + re-create instruction; never a per-task barrage). A survivor
|
|
695
|
+
* (session-resident shell / in-window monitor) is excluded — still reachable via TaskOutput/
|
|
696
|
+
* TaskStop, it needs no obituary. CC's per-orphan `Ku(task_id,"stopped")` status write (:707399)
|
|
697
|
+
* is N/A here: the suspend teardown already settled them killed with receipts. Absent for a run
|
|
698
|
+
* with no live background tasks at suspend, and for pre-1.262 checkpoints (deserialized without
|
|
699
|
+
* the field) — both resume silently, exactly like CC with an empty list.
|
|
700
|
+
*/
|
|
155
701
|
runningBackgroundTasks?: Array<{
|
|
156
702
|
id: string;
|
|
157
703
|
description?: string;
|
|
158
704
|
}>;
|
|
705
|
+
/**
|
|
706
|
+
* codex 终审 1.255 F2: the hands band's LOGICAL working directory at suspend (`handsCwdRef.current` —
|
|
707
|
+
* moved by `cd` and by EnterWorktree). Without it a resume silently reset the task cwd to the task root:
|
|
708
|
+
* relative Read/Edit/Write paths and Bash commands then operated somewhere else than the model believes.
|
|
709
|
+
* Absent when the task has no tracked cwd (no real shell / read-only hands). The directory itself is
|
|
710
|
+
* plain on-disk state that survives a process-local suspend (worktrees included — the tree stays on disk).
|
|
711
|
+
*/
|
|
159
712
|
handsCwd?: string;
|
|
713
|
+
/**
|
|
714
|
+
* codex 终审 1.255 F2: the ACTIVE EnterWorktree session at suspend (worktree.ts keeps it in a shared
|
|
715
|
+
* serializable ref, not a closure-only var, precisely so it lands here). Without it a resume LOST the
|
|
716
|
+
* session: ExitWorktree became a no-op (the unchanged worktree could never be removed), a second
|
|
717
|
+
* EnterWorktree was wrongly accepted, and `handsCwd` pointed into a worktree the tooling no longer
|
|
718
|
+
* owned. All-string fields; the worktree directory survives the suspend on disk (managed under the
|
|
719
|
+
* task root, `git worktree` metadata intact).
|
|
720
|
+
*/
|
|
160
721
|
activeWorktree?: ActiveWorktreeSession;
|
|
722
|
+
/**
|
|
723
|
+
* Parent effective-policy inheritance (tighten-only): the durable projection of the run's
|
|
724
|
+
* {@link import("./runner/prepare-task.js").InheritedGate}. Only the serializable DATA half persists —
|
|
725
|
+
* `ancestorRules` (each ancestor's session-rule snapshot: sessionId/principal/rev/rules, re-compiled on
|
|
726
|
+
* resume against the resumed leg's env) and `shellGate` (the chain's max-rank shell doctrine). The OPAQUE
|
|
727
|
+
* half (`parentConstraints` — live ToolPolicy/onAsk closures) CANNOT persist; `requiresParentConstraint`
|
|
728
|
+
* records only its PRESENCE, so the resume pre-CAS ladder can fail-loud (`resume.parent_constraint_missing`,
|
|
729
|
+
* checkpoint stays `pending`) when the resuming caller did not re-supply the closures via
|
|
730
|
+
* `resumeStream(..., internals)`. Absent for a task that inherited nothing (pre-1.294 checkpoints included)
|
|
731
|
+
* — distinguishable from `requiresParentConstraint: false` with data-only inheritance, which resumes
|
|
732
|
+
* without any re-supplied internals. All-data fields → JSON/`structuredClone` round-trips safely.
|
|
733
|
+
*/
|
|
161
734
|
inheritedGate?: {
|
|
162
735
|
ancestorRules?: ReadonlyArray<{
|
|
163
736
|
sessionId: string;
|
|
@@ -166,36 +739,174 @@ export interface CheckpointState {
|
|
|
166
739
|
rules: SessionPermissionRules;
|
|
167
740
|
}>;
|
|
168
741
|
shellGate?: "off" | "always" | "classify";
|
|
742
|
+
/** Org-memory admission freeze (ruled 2026-08-05): the chain's admitted org-scope set at
|
|
743
|
+
* suspend (data half, plain strings). The resume leg folds it seed ∩ live (tighten-only) and
|
|
744
|
+
* re-runs admission under it — a resume must never widen the delegation freeze. Absent on
|
|
745
|
+
* older checkpoints ⇒ the live channel (or, for a delegated child, the fail-closed ∅) stands.
|
|
746
|
+
* Cross-VERSION enforcement (an older worker resuming a newer checkpoint ignores fields it
|
|
747
|
+
* does not know) is the deployment's server/core version-gate concern, not a per-field fence. */
|
|
169
748
|
admittedOrgScopes?: readonly string[];
|
|
749
|
+
/** THIS task's OWN admitted org verdict at suspend (adversarial-review adoption, 2026-08-05):
|
|
750
|
+
* distinct from {@link admittedOrgScopes} (the incoming PARENT constraint). The resume leg
|
|
751
|
+
* applies it as the SESSION freeze on the final admitted projection — regardless of origin
|
|
752
|
+
* (deployment-origin scopes bypass the chain freeze, never the session freeze) — so a session's
|
|
753
|
+
* verdict can only NARROW across suspend/resume: re-adjudication still runs every leg (a
|
|
754
|
+
* revoked grant refuses the resume), but a policy widened mid-session cannot widen THIS
|
|
755
|
+
* session's mount or the chain its children inherit. Present (possibly EMPTY — an adjudicated
|
|
756
|
+
* empty verdict is not "org-less") iff the suspend leg adjudicated an org plane; absent on
|
|
757
|
+
* older checkpoints / org-less tasks. Checkpoints carrying this state are stamped
|
|
758
|
+
* {@link ORG_ADMISSION_CHECKPOINT_VERSION} so a pre-admission worker rejects them pre-CAS. */
|
|
170
759
|
ownAdmittedOrgScopes?: readonly string[];
|
|
760
|
+
/** The WRITE half of the session freeze: the org write grant this session held at suspend —
|
|
761
|
+
* `null` = the org write face was read-only (a resume must not gain write authority the
|
|
762
|
+
* suspend leg did not hold); a scope string = the explicit grant. Present iff
|
|
763
|
+
* {@link ownAdmittedOrgScopes} is (same adjudication record). */
|
|
171
764
|
ownAdmittedOrgWriteScope?: string | null;
|
|
765
|
+
/** The MONOTONIC org-governance provenance bit (adversarial round 5): `true` when any leg of
|
|
766
|
+
* the suspended tree ran with an org-admission surface (or inherited the bit). A resume worker
|
|
767
|
+
* missing the surface still treats the leg as governed — org-shaped scopes respelled outside
|
|
768
|
+
* the v2 contract are refused, even when every admitted set en route was EMPTY. Absent on
|
|
769
|
+
* trees that never had a governance surface. */
|
|
172
770
|
orgAdmissionGoverned?: true;
|
|
173
771
|
requiresParentConstraint: boolean;
|
|
772
|
+
/** How many opaque parent constraints the suspended leg ran under (present iff `requiresParentConstraint`).
|
|
773
|
+
* The resume pre-CAS ladder rejects a re-supply whose `parentConstraints.length` differs
|
|
774
|
+
* (`resume.parent_constraint_mismatch`) — a partial re-supply is a typed error, never a silently
|
|
775
|
+
* thinner chain. Absent on older checkpoints ⇒ only the non-empty check applies. */
|
|
174
776
|
parentConstraintCount?: number;
|
|
175
777
|
};
|
|
778
|
+
/**
|
|
779
|
+
* [c209-C] Q5 — the listing frames' announced NAME-SETS at suspend (agent types / skills the model
|
|
780
|
+
* has actually been shown this run, committed on first-frame delivery or intact boundary survival).
|
|
781
|
+
* A resume leg seeds its producers from these names and DIFFS the current roster/skill set against
|
|
782
|
+
* them, so cross-leg drift (config-center roster change between suspend and resume) is announced as
|
|
783
|
+
* a delta frame at the first boundary instead of silently swallowed — the exact event the listing
|
|
784
|
+
* eviction from the cached prefix exists to serve. Names only, never contents (a description change
|
|
785
|
+
* on a surviving name is deliberately not re-announced — name-keyed diff, agent_listing parity).
|
|
786
|
+
* Schema ADDITION (backward-compatible, B-5): an older checkpoint without the field resumes via the
|
|
787
|
+
* MED-3② transcript probe (delivered ⇒ seeded as the CURRENT entries, drift undetectable there;
|
|
788
|
+
* not found ⇒ full re-announce at the first boundary — conservative direction preserved).
|
|
789
|
+
*
|
|
790
|
+
* [c209-C] R2 C7 — `models`: the model CATALOG names announced (the agent_listing initial frame's
|
|
791
|
+
* "Models available for the 'model' parameter:" tail, Q4). The resume seed feeds it to the
|
|
792
|
+
* producer; a differing current catalog ⇒ the first boundary's agent_listing frame re-carries the
|
|
793
|
+
* full line — without this a catalog swap between legs was never re-disclosed anywhere (the
|
|
794
|
+
* catalog left the tool description with Q4). Absent (older checkpoint / never announced) ⇒ drift
|
|
795
|
+
* undetectable, silent (conservative, same B-5 direction).
|
|
796
|
+
*/
|
|
176
797
|
announcedListings?: {
|
|
177
798
|
agents?: string[];
|
|
178
799
|
skills?: string[];
|
|
179
800
|
models?: string[];
|
|
180
801
|
};
|
|
181
802
|
}
|
|
803
|
+
/**
|
|
804
|
+
* {@link CheckpointState} with **every** field made required-PRESENT, while each value keeps its original
|
|
805
|
+
* type (so an absent optional field is still passed explicitly as `undefined`). The suspend-side
|
|
806
|
+
* serialization builds `state` as this type (design/45 P3 / design/51 §P3): adding a new per-task
|
|
807
|
+
* correctness field to {@link CheckpointState} is then a **compile error** at the serialization site until
|
|
808
|
+
* it is explicitly handled — it can never be silently omitted and lost on resume. (Keying off
|
|
809
|
+
* `keyof Required<…>` makes the mapped type non-homomorphic, so it forces presence of every key yet leaves
|
|
810
|
+
* each value's `| undefined` intact — `workspaceHandle: undefined` for a process-local suspend still
|
|
811
|
+
* type-checks.)
|
|
812
|
+
*/
|
|
182
813
|
export type SerializedCheckpointState = {
|
|
183
814
|
[K in keyof Required<CheckpointState>]: CheckpointState[K];
|
|
184
815
|
};
|
|
816
|
+
/**
|
|
817
|
+
* The BASELINE {@link Checkpoint} schema version (design/49 §3, code-ready council round-2): the
|
|
818
|
+
* shape a plain human-approval suspend carried before any feature raised the format. It is NOT the highest
|
|
819
|
+
* a resume accepts — that is {@link MAX_SUPPORTED_CHECKPOINT_VERSION}, which the per-feature stamps
|
|
820
|
+
* (RESOURCE / BINDING / TOKEN / ORG_ADMISSION) have each raised on their own, deliberately without moving
|
|
821
|
+
* this constant. Resume stays forward-compatible by `<=` against MAX, never `===`. Every live mint site
|
|
822
|
+
* stamps one of the feature versions, so nothing in-tree produces this value today; it is kept as the
|
|
823
|
+
* floor of the version ladder and as the reading for a row written before any feature raised the format.
|
|
824
|
+
*/
|
|
185
825
|
export declare const CURRENT_CHECKPOINT_VERSION = 1;
|
|
826
|
+
/** design/74 R3-B: the schema version a `resource_limit` suspend stamps (v2 — it adds `resourceLedger` and a
|
|
827
|
+
* `resource_limit` `pendingAction`/`gate` an old worker can't handle). An old worker (`MAX_SUPPORTED`=1)
|
|
828
|
+
* rejects it pre-CAS (stays `pending`, retryable on a new worker). */
|
|
186
829
|
export declare const RESOURCE_CHECKPOINT_VERSION = 2;
|
|
830
|
+
/**
|
|
831
|
+
* design/80 D-1 (version-skew downgrade fix): the schema version a **binding-bearing** human/irreversible_ask
|
|
832
|
+
* `tool_approval` suspend stamps. These checkpoints carry the decision-action binding (`boundCallId` +
|
|
833
|
+
* `boundInputHash`) whose enforcement lives ENTIRELY in the resuming worker's resume path. A pre-D-1 worker
|
|
834
|
+
* (released 1.100.0: `MAX_SUPPORTED`=2, and its resume code has ZERO binding logic) would otherwise resume a
|
|
835
|
+
* D-1-minted v1 checkpoint and execute the pending tool with NO decision-action verification — the exact
|
|
836
|
+
* "approve vendor-A $5 → execute vendor-B $5000" bypass the binding exists to prevent (council BLOCKER #1).
|
|
837
|
+
* Stamping these at **v3 (> the old worker's MAX of 2)** forces a pre-D-1 worker to reject them PRE-CAS
|
|
838
|
+
* (`unsupported_version`, stays `pending`, retried on a binding-enforcing worker) instead of silently voiding
|
|
839
|
+
* the binding. A resource_limit suspend keeps stamping v2; a pre-binding (legacy 1.100.0) checkpoint is v1.
|
|
840
|
+
*/
|
|
187
841
|
export declare const BINDING_CHECKPOINT_VERSION = 3;
|
|
842
|
+
/**
|
|
843
|
+
* design/164 (the BINDING_CHECKPOINT_VERSION precedent replayed a third time): the schema version a suspend
|
|
844
|
+
* stamps when its ledger CARRIES a cross-slice TOKEN allocation (`resourceLedger.totalTokens` set). The
|
|
845
|
+
* enforcement — window clamp against the remaining allocation, exhausted-resume fail-fast — lives ENTIRELY
|
|
846
|
+
* in the resuming worker, so a pre-164 worker (MAX_SUPPORTED=4) would accept the row, ignore
|
|
847
|
+
* `spentTokens` as a bound, and grant a fresh full window. Stamping v5 forces it to reject PRE-CAS
|
|
848
|
+
* (`unsupported_version`, stays `pending`, retried on an enforcing worker). A ledger with NO token total
|
|
849
|
+
* keeps the historic stamps (v2 resource / v3 binding) — deployments not using it see zero version movement.
|
|
850
|
+
*
|
|
851
|
+
* v4 (the retired wall-clock total) is deliberately NOT reused: this worker still READS v4 rows, and one
|
|
852
|
+
* whose ledger carries the retired axis is refused with its own typed code (see
|
|
853
|
+
* {@link CheckpointError} `checkpoint.walltime_axis_retired`) instead of silently resuming under a bound
|
|
854
|
+
* nobody enforces any more.
|
|
855
|
+
*/
|
|
188
856
|
export declare const TOKEN_CHECKPOINT_VERSION = 5;
|
|
857
|
+
/**
|
|
858
|
+
* Org-memory admission state (adversarial round 2, 2026-08-05 — the BINDING_CHECKPOINT_VERSION
|
|
859
|
+
* precedent replayed a fourth time): the schema version a suspend stamps when its state carries
|
|
860
|
+
* org-admission freeze fields (`inheritedGate.admittedOrgScopes` / `ownAdmittedOrgScopes` /
|
|
861
|
+
* `ownAdmittedOrgWriteScope` — the adjudicated-empty record included). The enforcement — the
|
|
862
|
+
* session/delegation freeze on resume, the tenant-admission door itself — lives ENTIRELY in the
|
|
863
|
+
* resuming worker, so a pre-admission worker (MAX_SUPPORTED=5) would accept the row, ignore the
|
|
864
|
+
* freeze fields, and remount the raw org scopes with no admission at all. Stamping v6 forces it to
|
|
865
|
+
* reject PRE-CAS (`unsupported_version`, stays `pending`, retried on an enforcing worker). A
|
|
866
|
+
* checkpoint with NO org-admission state keeps the historic stamps — deployments not using org
|
|
867
|
+
* memory see zero version movement.
|
|
868
|
+
*/
|
|
189
869
|
export declare const ORG_ADMISSION_CHECKPOINT_VERSION = 6;
|
|
870
|
+
/** The highest {@link Checkpoint.version} `runner.resume` will act on; a higher one is rejected pre-CAS with
|
|
871
|
+
* {@link CheckpointError} `unsupported_version` (the checkpoint stays `pending`, retryable on a newer worker).
|
|
872
|
+
* Raised to 6 for org-admission-bearing checkpoints — this worker reads v1 (legacy human), v2
|
|
873
|
+
* (resource), v3 (binding human/irreversible_ask), v4 (a pre-164 row, refused only when it carries the
|
|
874
|
+
* retired wall-clock allocation), v5 (token-allocation-bearing resource/approval) and v6
|
|
875
|
+
* (org-admission freeze state). */
|
|
190
876
|
export declare const MAX_SUPPORTED_CHECKPOINT_VERSION = 6;
|
|
877
|
+
/**
|
|
878
|
+
* Read a checkpoint's schema version, defaulting an absent field to **legacy `0`** (a 1.67-era checkpoint
|
|
879
|
+
* written before the field existed — it carries no `workspaceHandle`, so resuming it the v1 way is safe).
|
|
880
|
+
* Compare with `<= MAX_SUPPORTED_CHECKPOINT_VERSION`, never `=== CURRENT_CHECKPOINT_VERSION` (design/49 §3).
|
|
881
|
+
*/
|
|
191
882
|
export declare function checkpointVersionOf(cp: Pick<Checkpoint, "version">): number;
|
|
883
|
+
/**
|
|
884
|
+
* design/74 R3-B: cross-slice resource accounting carried by a `resource_limit` {@link Checkpoint}. The
|
|
885
|
+
* total budget is the human's allocation; `spent*` accumulate across the resume→re-suspend chain (debited
|
|
886
|
+
* at each suspend's `put`). A resumed slice's effective `maxCostUsd` = `min(window, totalBudgetMicroUsd -
|
|
887
|
+
* spentMicroUsd)`; the final {@link TaskResult} stats aggregate `spent* + this slice's stats` so a
|
|
888
|
+
* multi-slice run reports the whole, not just the last segment. Lives ON the checkpoint row so one
|
|
889
|
+
* `resolve`/`put` CAS covers status + ledger atomically (no cross-store split).
|
|
890
|
+
*/
|
|
192
891
|
export interface ResourceLedger {
|
|
892
|
+
/** The human's total $ allocation in micro-USD; `undefined` = no $ ceiling (only window/walltime bound). */
|
|
193
893
|
totalBudgetMicroUsd?: number;
|
|
894
|
+
/** design/164 — the human's total TOKEN allocation across the whole resume→re-suspend chain;
|
|
895
|
+
* `undefined` = no token ceiling (only the per-slice window binds). Replaces the retired wall-clock
|
|
896
|
+
* total: tokens are additive across processes, wall-clock across a suspend chain is not. */
|
|
194
897
|
totalTokens?: number;
|
|
898
|
+
/** Cumulative cost across all slices so far (micro-USD), debited at each suspend's `put`. */
|
|
195
899
|
spentMicroUsd: number;
|
|
900
|
+
/** Cumulative tokens across all slices so far. */
|
|
196
901
|
spentTokens: number;
|
|
902
|
+
/** Cumulative turns across all slices so far. */
|
|
197
903
|
spentTurns: number;
|
|
904
|
+
/** Cumulative wall-clock across all slices so far (ms; each slice measured on the monotonic clock).
|
|
905
|
+
* design/164: PURE ACCOUNTING — it is no longer a gate on anything (the wall-clock resource axis
|
|
906
|
+
* retired). Kept because "how long did this chain actually run" is a real observability question.
|
|
907
|
+
* Absent on checkpoints written before it existed; readers treat missing as 0. */
|
|
198
908
|
spentWalltimeMs?: number;
|
|
909
|
+
/** How many slices have run so far (the resource chain's suspend count). */
|
|
199
910
|
sliceCount: number;
|
|
200
911
|
}
|
|
201
912
|
export declare function debitLedger(prior: ResourceLedger | undefined, slice: {
|
|
@@ -209,34 +920,180 @@ export declare function debitLedger(prior: ResourceLedger | undefined, slice: {
|
|
|
209
920
|
}, opts?: {
|
|
210
921
|
countSlice?: boolean;
|
|
211
922
|
}): ResourceLedger;
|
|
923
|
+
/** design/74 Slice 4: the $ budget (micro-USD) the NEXT slice may still spend = `totalBudgetMicroUsd -
|
|
924
|
+
* spentMicroUsd`, never negative. `undefined` when no total $ ceiling is set (the run is bounded only by the
|
|
925
|
+
* per-slice window / walltime). A resumed slice's effective `maxCostUsd` is `min(window, this)`. NB: `0` is a
|
|
926
|
+
* VALID, exhausted ceiling — NOT "unlimited" (only `undefined` is unlimited). Any spend then immediately trips
|
|
927
|
+
* `overBudget`, and the run loop fails an exhausted resume fast (runtask). Never falsy-test this value (a
|
|
928
|
+
* `if (remaining)` would skip a legitimate 0 ceiling and silently bypass the budget). */
|
|
212
929
|
export declare function remainingBudgetMicroUsd(ledger: ResourceLedger | undefined): number | undefined;
|
|
930
|
+
/** design/164 (the TOKEN sibling of {@link remainingBudgetMicroUsd}): tokens the NEXT slice may still
|
|
931
|
+
* spend = `totalTokens − spentTokens`, never negative. `undefined` when no token total is set (the run is
|
|
932
|
+
* bounded only by the per-slice window / the $ axis). A resumed slice's effective `limits.maxTokens` is
|
|
933
|
+
* `min(window, this)` — a resume can't refresh the allocation. NB: `0` is a VALID, exhausted ceiling
|
|
934
|
+
* (only `undefined` is unlimited); the run loop fails an exhausted resume fast — never falsy-test it. */
|
|
213
935
|
export declare function remainingTokens(ledger: ResourceLedger | undefined): number | undefined;
|
|
936
|
+
/**
|
|
937
|
+
* design/80 D-1 (reopen-by-reason + persist-winner): the WINNING resume binding, recorded ON the
|
|
938
|
+
* checkpoint row the moment a {@link CheckpointStore.resolve} CAS wins (pending → resolved). It is the
|
|
939
|
+
* durable record of "which decision was approved against which pending action" so a later reopen→re-resume
|
|
940
|
+
* can be validated against it. A `policy_ask` resolve and BOTH review-gate resolves record a winner: only
|
|
941
|
+
* `policy_ask` is bound to a specific pending tool call, while `plan_review` / `dry_run_review` record
|
|
942
|
+
* their decision under the synthetic `gate:<kind>` marker instead (RB-64). A `resource_limit` / `wake` /
|
|
943
|
+
* `task_done` resolve records none (there is no decision to bind). `boundInputHash` (binding the exact executed bytes) is a SEPARATE
|
|
944
|
+
* later D-1 slice (1a.2) and is deliberately NOT part of this winner.
|
|
945
|
+
*/
|
|
214
946
|
export interface ResolvedOutcome {
|
|
947
|
+
/** The pending tool call this decision was bound to (design/80 D-1 slice 1a). RB-64: a REVIEW gate binds
|
|
948
|
+
* no tool call, so it records the synthetic `gate:<kind>` marker instead — the replay guard compares the
|
|
949
|
+
* DECISION for those, never a call id. */
|
|
215
950
|
boundCallId: string;
|
|
951
|
+
/** The adjudication the operator made. RB-64 widens this to the review verdicts (`approve`/`reject`/
|
|
952
|
+
* `edit`) so a `plan_review`/`dry_run_review` winner can be persisted and replayed by the same
|
|
953
|
+
* env_failed reopen guard the approval gate has — previously review gates recorded NO winner, which is
|
|
954
|
+
* what left their reopen path with nothing to check a re-vote against. */
|
|
216
955
|
decision: "allow" | "deny" | "approve" | "reject" | "edit";
|
|
956
|
+
/** A re-written arg payload, if the `allow` rewrote the call (design/37). Carried so an `env_failed`
|
|
957
|
+
* re-resume (a system retry of the SAME approved action) must replay the identical `updatedInput`. */
|
|
217
958
|
updatedInput?: unknown;
|
|
959
|
+
/** The content-ask ANSWER the `allow` carried, if any (ruled 2026-08-04). Recorded for the same reason
|
|
960
|
+
* `updatedInput` is: the answer is the SUBSTANCE of a content-ask decision, not a decoration on it, so
|
|
961
|
+
* an `env_failed` re-resume — a system retry of the already-approved action — must replay the identical
|
|
962
|
+
* answer. Without it the replay could keep the verdict and swap the answer text going into the model's
|
|
963
|
+
* context (the shape the plan-edit replay guard closed for `editedPlan`). Omitted when absent so the
|
|
964
|
+
* replay equality check does not falsely diverge on `undefined`. */
|
|
218
965
|
answer?: QuestionAnswer;
|
|
966
|
+
/** The operator's NOTE, if the decision carried one — the approval lane's deny `reason` and the review
|
|
967
|
+
* lanes' reject `reason` alike. Recorded for the same reason `updatedInput` and `answer` are: the note
|
|
968
|
+
* is the substance of the decision, not a decoration on it. It is the text the resume continuation
|
|
969
|
+
* fences into the model-facing reminder that steers the re-plan, so a replay keeping the verdict and
|
|
970
|
+
* swapping the note delivers a different instruction under the same word. Omitted when absent.
|
|
971
|
+
*
|
|
972
|
+
* ADDITIVE and OPTIONAL by contract: rows persisted before this field existed carry no `reason` key,
|
|
973
|
+
* and the key's absence is read as "this dimension was never recorded" — the replay guard skips it —
|
|
974
|
+
* NEVER as "recorded as empty", which would refuse every legitimate retry of an existing row. Absence
|
|
975
|
+
* is therefore indistinguishable from a genuinely note-less decision, which is the accepted price of
|
|
976
|
+
* the additive shape: the binding starts with the rows that record it.
|
|
977
|
+
*
|
|
978
|
+
* RECORDED VERBATIM, and independently of the verdict: any supplied note is kept exactly as given —
|
|
979
|
+
* including `""`, and including a note on a verdict that never relays one (an `allow`, an `approve`, an
|
|
980
|
+
* `edit`). The winner is a record of the decision that was made, not of the subset of it the
|
|
981
|
+
* continuation happens to render, and `updatedInput` has carried the same property since D-1 (a rewrite
|
|
982
|
+
* on a `deny` is recorded and replay-compared although nothing executes it). Normalizing here would
|
|
983
|
+
* mean an empty note collapsed into the "never recorded" hole above and a note on a non-relaying
|
|
984
|
+
* verdict became free to change between the resolve and its replay — both weaken "the retry presents
|
|
985
|
+
* the same decision" for no delivery benefit, since a replay is produced by re-presenting the same
|
|
986
|
+
* outcome, not by reconstructing an equivalent one. */
|
|
219
987
|
reason?: string;
|
|
220
988
|
}
|
|
989
|
+
/**
|
|
990
|
+
* design/80 D-1: why a consumed checkpoint was {@link CheckpointStore.reopen | reopened} (resolved →
|
|
991
|
+
* pending). The reason drives re-resume validation (§3 invariant #1): an `env_failed` reopen is a SYSTEM
|
|
992
|
+
* RETRY of the ALREADY-APPROVED action — the re-resume MUST replay the persisted {@link ResolvedOutcome}
|
|
993
|
+
* winner, never a new vote — while a `tool_unavailable` reopen (the action could not run, may now be
|
|
994
|
+
* invalid — P-7) lets a human RE-DECIDE with the tool present, so a fresh operator decision IS allowed.
|
|
995
|
+
*/
|
|
221
996
|
export type ReopenReason = "env_failed" | "tool_unavailable";
|
|
997
|
+
/**
|
|
998
|
+
* design/80 D-1 (atomicity fix): the optimistic-concurrency precondition a {@link CheckpointStore.resolve}
|
|
999
|
+
* caller passes so its CAS is atomic with the validation it did against an earlier `get()` snapshot. The
|
|
1000
|
+
* resume-side decision-action guards (boundCallId/boundInputHash) bind to per-token-IMMUTABLE fields, but the
|
|
1001
|
+
* **reopen-by-reason** guard reads `reopenReason` + `resolvedOutcome` — which a concurrent
|
|
1002
|
+
* {@link CheckpointStore.reopen}/{@link CheckpointStore.resolve} cycle mutates. Passing the monotonic
|
|
1003
|
+
* {@link Checkpoint.rev} the caller observed makes `resolve` additionally require the LIVE row's `rev` to still
|
|
1004
|
+
* equal it; ANY intervening resolve/reopen bumps `rev`, so a cycle in the get→resolve window loses the CAS
|
|
1005
|
+
* (fail-closed), forcing a re-`get` + re-validate.
|
|
1006
|
+
*
|
|
1007
|
+
* **Why a counter, not the reopenReason value (round-2 BLOCKER fix):** `reopenReason` is a 2-valued enum, so a
|
|
1008
|
+
* full `env_failed`→`tool_unavailable`→`env_failed` cycle (the `tool_unavailable` arm permits a fresh P-7
|
|
1009
|
+
* re-decision) returns `reopenReason` to the SAME value while `resolvedOutcome` silently changed to a different
|
|
1010
|
+
* (e.g. RETRACTED) winner — a classic ABA on the OCC key. A monotonic `rev` bumped on every resolve/reopen is
|
|
1011
|
+
* ABA-proof: a returned-to-the-same-value `reopenReason` still has a strictly higher `rev`. Omit `expect`
|
|
1012
|
+
* (legacy callers / direct store tests) → no OCC.
|
|
1013
|
+
*/
|
|
222
1014
|
export interface ResolveExpectation {
|
|
1015
|
+
/** The monotonic {@link Checkpoint.rev} the caller observed at `get()` (absent rev ⇒ legacy `0`). */
|
|
223
1016
|
rev: number;
|
|
224
1017
|
}
|
|
1018
|
+
/** A persisted suspension point: enough to resume a task on any replica. `status` drives the 3-state
|
|
1019
|
+
* machine (pending → resolved | expired) that makes resume idempotent (§5). */
|
|
225
1020
|
export interface Checkpoint {
|
|
226
1021
|
token: CheckpointToken;
|
|
1022
|
+
/**
|
|
1023
|
+
* Schema version of this checkpoint (design/49 v1.5). Every live suspend stamps the version of the
|
|
1024
|
+
* FEATURE it carries (RESOURCE / BINDING / TOKEN / ORG_ADMISSION); {@link CURRENT_CHECKPOINT_VERSION}
|
|
1025
|
+
* is the baseline at the bottom of that ladder, not the stamp of any current mint site.
|
|
1026
|
+
* absent ⇒ legacy `0` (1.67-era, no `state.workspaceHandle`). Resume rejects `> MAX_SUPPORTED_CHECKPOINT_VERSION`
|
|
1027
|
+
* pre-CAS so an old worker can't silently ignore fields a newer format relies on (forward-compatible by `<=`).
|
|
1028
|
+
* Read via {@link checkpointVersionOf}.
|
|
1029
|
+
*/
|
|
227
1030
|
version?: number;
|
|
1031
|
+
/** Multi-tenant isolation key (forced through `resolve`/`reap` WHERE, §2.1 service [4]). */
|
|
228
1032
|
scope: string;
|
|
1033
|
+
/** The session to resume (via the SessionStore). */
|
|
229
1034
|
sessionId: string;
|
|
1035
|
+
/** The session leaf the suspend happened at — the resume point (CAS write base, §5). */
|
|
230
1036
|
leafId: string;
|
|
231
1037
|
gate: CheckpointGate;
|
|
232
1038
|
pendingAction: PendingAction;
|
|
233
1039
|
state: CheckpointState;
|
|
1040
|
+
/** design/74 R3-B: cross-slice resource accounting. Present for a `resource_limit` gate (debited at the
|
|
1041
|
+
* suspend's `put`; read on resume to size the next slice + aggregate the final stats). **design/80 D-E-core
|
|
1042
|
+
* (A3):** ALSO attached to a human/irreversible_ask APPROVAL suspend — debited with that leg's spend — so a
|
|
1043
|
+
* STATELESS policy can read durable cumulative spend across the resume chain via {@link ToolCallRequest.budget}
|
|
1044
|
+
* (a fresh in-memory counter would reset every leg). On the approval path it is READ-CONTEXT ONLY: the final
|
|
1045
|
+
* -stats aggregate fold (runtask) is gated to the `resource_limit` gate kind, so an approval resume is never
|
|
1046
|
+
* double-counted. Absent for `task_done`. See {@link ResourceLedger}. */
|
|
234
1047
|
resourceLedger?: ResourceLedger;
|
|
1048
|
+
/**
|
|
1049
|
+
* Absolute epoch-ms deadline for an awaiting-human checkpoint; past it, `reap` expires it (§6).
|
|
1050
|
+
*
|
|
1051
|
+
* **design/80 §D-D semantic contract (the DURABLE FIELD a reaper-loop consumes — read it on
|
|
1052
|
+
* {@link CheckpointStore} too):** for an APPROVAL gate (`gate.kind` ∈ {`human`, `irreversible_ask`}) the
|
|
1053
|
+
* deployment's reaper-loop treats this as an **SLA resolve-deny** time (FACET A — its fast path: at deadline
|
|
1054
|
+
* it `resolve`s the checkpoint as a `deny` so the run ends cleanly), with a LATER `terminalAt` abandonment
|
|
1055
|
+
* backstop the loop ALSO honors (crash-safe if the SLA timer never fired). For an UNATTENDED-TTL gate
|
|
1056
|
+
* (`gate.kind` ∈ {`resource_limit`, `needs_review`, `plan_review`} — the resource + review-pause family) this
|
|
1057
|
+
* is an **abandonment-TTL** → `expire`/{@link reap} ONLY, NEVER a resolve-deny (FACET B — those kinds resume
|
|
1058
|
+
* only with their OWN outcome, so a `policy_ask` resolve-deny hits no `gateMatch` arm → `gate_mismatch`). The
|
|
1059
|
+
* reaper-loop + the gate.kind policy + minting `terminalAt` are the DEPLOYMENT's job
|
|
1060
|
+
* (its store impl + loop — owned service-side); CORE only guarantees this `deadline`
|
|
1061
|
+
* field (for `human`/`irreversible_ask`) + the persisted {@link gate}.kind survive the round-trip so any
|
|
1062
|
+
* reaper consumer (the service's TiDB loop OR a non-service Pg deployment) implements the SAME contract.
|
|
1063
|
+
*/
|
|
235
1064
|
deadline?: number;
|
|
236
1065
|
status: "pending" | "resolved" | "expired";
|
|
237
1066
|
createdAt: number;
|
|
1067
|
+
/**
|
|
1068
|
+
* How many times THIS task has suspended, counting this checkpoint (1 = first suspend; design/72
|
|
1069
|
+
* §2.2 (B)). Carried forward across resume→re-suspend (each new checkpoint = the prior count + 1), so
|
|
1070
|
+
* a restart-prone model that keeps re-issuing the gated/egress call — re-suspending every resume —
|
|
1071
|
+
* is capped: past `maxSuspends` the run fails (`suspend.loop`) instead of minting another checkpoint
|
|
1072
|
+
* and looping forever. Absent ⇒ legacy `0` (pre-§2.2 checkpoints; treated as "no prior suspends").
|
|
1073
|
+
*/
|
|
238
1074
|
suspendCount?: number;
|
|
1075
|
+
/**
|
|
1076
|
+
* design/91 — the wall-clock instant (epoch ms, from {@link import("./types.js").RunnerDeps.now}) this task
|
|
1077
|
+
* suspended at a HUMAN-REVIEW gate. Resume derives the human-review latency for this leg as
|
|
1078
|
+
* `now() − suspendedAt` and folds it into `TaskResult.stats.humanReview` (the design/89 §2.4 C2 burden axis).
|
|
1079
|
+
* The human-review family is the APPROVAL gates (`gate.kind` ∈ {`human`, `irreversible_ask`}) — core stamps
|
|
1080
|
+
* this automatically on the approval-suspend mint — AND the REVIEW-PAUSE gates (`needs_review` = a dry-run
|
|
1081
|
+
* diff review, `plan_review` = a pre-action PLAN review). Review-pause checkpoints are minted by a PROFILE
|
|
1082
|
+
* (core owns only the routing/resume discriminants), so a profile that wants the C2 wait counted MUST stamp
|
|
1083
|
+
* `suspendedAt = deps.now()` at mint — the same injectable clock the resume reads, so a test pins the latency.
|
|
1084
|
+
* Absent on a `resource_limit` suspend (a machine backstop, not a human wait), on a review-pause checkpoint
|
|
1085
|
+
* whose profile did not opt in, and on a legacy pre-design/91 checkpoint (⇒ resume skips the durable latency
|
|
1086
|
+
* for that leg — no undercount surprise, just no entry).
|
|
1087
|
+
*/
|
|
239
1088
|
suspendedAt?: number;
|
|
1089
|
+
/**
|
|
1090
|
+
* design/91 — the accumulated {@link import("./types.js").TaskResult.stats}`.humanReview` from all PRIOR legs,
|
|
1091
|
+
* carried forward across resume→re-suspend (like {@link suspendCount} / {@link resourceLedger}) so a
|
|
1092
|
+
* multi-leg suspend/resume chain reports the WHOLE human-review burden, not just the last leg's. It holds
|
|
1093
|
+
* the gates resolved up to and including the suspend BEFORE this one — the latency for THIS suspend is added
|
|
1094
|
+
* by the resume that wakes it (`now() − suspendedAt`). Absent ⇒ no prior human-review time (a first suspend
|
|
1095
|
+
* with no earlier synchronous asks). Budget-EXCLUDED side observable — never a cost/gate input.
|
|
1096
|
+
*/
|
|
240
1097
|
humanReview?: {
|
|
241
1098
|
count: number;
|
|
242
1099
|
totalWaitMs: number;
|
|
@@ -246,49 +1103,469 @@ export interface Checkpoint {
|
|
|
246
1103
|
decision?: string;
|
|
247
1104
|
}>;
|
|
248
1105
|
};
|
|
1106
|
+
/**
|
|
1107
|
+
* design/80 D-1 (persist-winner): the winning resume binding, recorded by {@link CheckpointStore.resolve}
|
|
1108
|
+
* when its CAS wins (pending → resolved) — present after a `policy_ask` resolve and after either REVIEW-gate
|
|
1109
|
+
* resolve (`plan_review` / `dry_run_review`, RB-64); absent on a freshly minted pending checkpoint and on
|
|
1110
|
+
* `resource_limit` / `wake` / `task_done` resolves. PRESERVED (never cleared) across a
|
|
1111
|
+
* {@link CheckpointStore.reopen} so an `env_failed` re-resume can be validated against it. See
|
|
1112
|
+
* {@link ResolvedOutcome}.
|
|
1113
|
+
*/
|
|
249
1114
|
resolvedOutcome?: ResolvedOutcome;
|
|
1115
|
+
/**
|
|
1116
|
+
* design/80 D-1 (reopen-by-reason): why this checkpoint was last reopened (resolved → pending), recorded
|
|
1117
|
+
* by {@link CheckpointStore.reopen}. Drives re-resume validation (§3 invariant #1): `env_failed` ⇒ the
|
|
1118
|
+
* re-resume must replay the persisted {@link resolvedOutcome} winner (a system retry of the approved
|
|
1119
|
+
* action); `tool_unavailable` ⇒ a fresh operator decision is allowed (P-7 re-decide with the tool
|
|
1120
|
+
* present). Absent on a never-reopened checkpoint (a first resume is unconstrained by reason).
|
|
1121
|
+
*/
|
|
250
1122
|
reopenReason?: ReopenReason;
|
|
1123
|
+
/**
|
|
1124
|
+
* design/80 D-1 (atomicity fix, round-2 BLOCKER): a **monotonic revision counter** bumped by EVERY
|
|
1125
|
+
* {@link CheckpointStore.resolve} (won) and {@link CheckpointStore.reopen}. It is the optimistic-concurrency
|
|
1126
|
+
* key ({@link ResolveExpectation}): a resume reads it at `get()` and `resolve` requires the live `rev` to
|
|
1127
|
+
* still equal it, so any resolve/reopen cycle landing in the get→resolve window loses the CAS (fail-closed).
|
|
1128
|
+
* Unlike the 2-valued `reopenReason`, it cannot ABA back to a prior value. Absent ⇒ legacy `0` (a checkpoint
|
|
1129
|
+
* minted before this field; treated as never mutated).
|
|
1130
|
+
*/
|
|
251
1131
|
rev?: number;
|
|
1132
|
+
/**
|
|
1133
|
+
* design S1d (source-tag persistence, service [198]/[199]): the issuing task's session id, stamped at the
|
|
1134
|
+
* APPROVAL suspend mint so a supervisor inbox can attribute a paused/awaiting checkpoint to the worker that
|
|
1135
|
+
* raised it (the durable analog of {@link import("./tool-policy.js").AskRequest}.sourceTaskId — the durable
|
|
1136
|
+
* suspend path does NOT invoke `onAsk`, so the synchronous source identity is captured here instead). It is
|
|
1137
|
+
* the SAME value the synchronous ask carries: `= sessionId` at the mint.
|
|
1138
|
+
*
|
|
1139
|
+
* **Security invariant (carried verbatim from the 1.113.0 ask-bubbling contract):** for a Runner-created
|
|
1140
|
+
* DELEGATED subagent this is **worker-unforgeable** (a child never sets its own sessionId — the Runner mints
|
|
1141
|
+
* it); a top-level caller MAY continue its own session id via {@link import("./types.js").TaskSpec.sessionId},
|
|
1142
|
+
* so this is NOT a global "never forgeable" run id — aggregation only aggregates delegated workers, where it
|
|
1143
|
+
* holds. **ECHO-ONLY triage metadata, NEVER a gate input:** no gate / CAS / winner / resume-validation path
|
|
1144
|
+
* reads it (a contract-test pins this). Absent ⇒ no source task id (a non-ask gate, or a legacy checkpoint).
|
|
1145
|
+
*/
|
|
252
1146
|
sourceTaskId?: string;
|
|
1147
|
+
/**
|
|
1148
|
+
* design S1d (source-tag persistence): the issuing task's authenticated end-user {@link
|
|
1149
|
+
* import("./types.js").TaskSpec.principal} (design/62), stamped at the APPROVAL suspend mint so an aggregating
|
|
1150
|
+
* inbox can attribute a paused checkpoint per-user — the durable analog of {@link
|
|
1151
|
+
* import("./tool-policy.js").AskRequest}.principal. Caller-set (never a tool/worker arg), and like
|
|
1152
|
+
* {@link sourceTaskId} it is **ECHO-ONLY triage metadata, NEVER a gate input** (no gate/CAS/winner/resume path
|
|
1153
|
+
* reads it). Absent ⇒ no principal (a non-ask gate, an unauthenticated task, or a legacy checkpoint).
|
|
1154
|
+
*/
|
|
253
1155
|
principal?: string;
|
|
1156
|
+
/**
|
|
1157
|
+
* design/153 §7.4 (1.389.0, RB-42②③): the VERBATIM `TaskSpec.durableApproval` opt-in in force at this
|
|
1158
|
+
* APPROVAL suspend's mint — the engine-recorded provenance of "this task was durable-approval armed, with
|
|
1159
|
+
* this scope and this reaper TTL". Consumed by the parked-resume drive's REVIVE-CONTINUITY re-derivation
|
|
1160
|
+
* (subagent settle path): the drive spec carries no forwarding seat, so without this record a resumed leg's
|
|
1161
|
+
* SECOND plain ask would headless-deny instead of re-parking. Reading it explicitly (instead of inferring
|
|
1162
|
+
* the opt-in from `gate.kind === "human"`) covers a SAFETY-TIER first park (irreversible_ask — the kind
|
|
1163
|
+
* alone never proved the spawn opted in) and carries `ttlMs` across the re-park so the re-minted
|
|
1164
|
+
* checkpoint re-arms its reaper `deadline` (previously unbounded). Absent ⇒ the mint ran with NO
|
|
1165
|
+
* `durableApproval` (an unattended safety suspend, a non-ask gate, or a legacy pre-1.389 checkpoint — the
|
|
1166
|
+
* consumer then falls back to the human-kind scope-only inference for legacy rows and injects NOTHING for
|
|
1167
|
+
* unattended parks, so a spawn that never opted in can never gain the opt-in through a resume). Engine-
|
|
1168
|
+
* minted from the task's own spec — a worker/tool cannot forge it (`durableApproval` never rides tool args).
|
|
1169
|
+
*/
|
|
254
1170
|
durableApproval?: {
|
|
255
1171
|
scope: string;
|
|
256
1172
|
ttlMs?: number;
|
|
257
1173
|
};
|
|
258
1174
|
}
|
|
1175
|
+
/**
|
|
1176
|
+
* design/80 assistant-scheduler seam #1: a LIGHTWEIGHT projection of one PENDING
|
|
1177
|
+
* checkpoint, returned in bulk by {@link CheckpointStore.listByScope} so a supervisor scheduler can list a
|
|
1178
|
+
* scope's suspended/awaiting tasks in ONE call (no N+1 `get`s, no full-{@link Checkpoint} payloads). It is a
|
|
1179
|
+
* read-only DISPLAY/triage summary — every field is DERIVED from the persisted {@link Checkpoint}; nothing
|
|
1180
|
+
* here is a new source of truth or a gate input.
|
|
1181
|
+
*/
|
|
259
1182
|
export interface CheckpointSummary {
|
|
1183
|
+
/** The resume capability token ({@link Checkpoint.token}). */
|
|
260
1184
|
token: CheckpointToken;
|
|
1185
|
+
/** The suspended task / session id ({@link Checkpoint.sessionId}). */
|
|
261
1186
|
sessionId: string;
|
|
1187
|
+
/** The multi-tenant scope this checkpoint lives in ({@link Checkpoint.scope}) — always equal to the query's. */
|
|
262
1188
|
scope: string;
|
|
1189
|
+
/** The gate's discriminant ({@link CheckpointGate}.kind): which kind of pause this is. */
|
|
263
1190
|
gateKind: CheckpointGate["kind"];
|
|
1191
|
+
/** The deterministic risk tier (1..5) when the gate carries a {@link RiskDescriptor} (the `human` /
|
|
1192
|
+
* `irreversible_ask` approval escalations), derived via {@link riskSeverity} from the descriptor's axes —
|
|
1193
|
+
* the SAME value as `gate.riskDescriptor.severity`, recomputed from the single source helper rather than
|
|
1194
|
+
* re-stored. `undefined` for a gate with no descriptor (resource_limit / needs_review / plan_review / a
|
|
1195
|
+
* legacy human ask minted before D-E). The inbox sorts DESC by this. */
|
|
264
1196
|
severity?: 1 | 2 | 3 | 4 | 5;
|
|
1197
|
+
/** Cumulative spend debited to this suspend chain ({@link ResourceLedger.spentMicroUsd}) in micro-USD —
|
|
1198
|
+
* present whenever the checkpoint carries a {@link Checkpoint.resourceLedger} (every resource_limit suspend,
|
|
1199
|
+
* and every approval suspend on the D-E-core budget-read path); `undefined` when no ledger is attached. */
|
|
265
1200
|
spentMicroUsd?: number;
|
|
1201
|
+
/** The awaiting-human SLA / abandonment deadline ({@link Checkpoint.deadline}, epoch ms) when present. */
|
|
266
1202
|
deadline?: number;
|
|
1203
|
+
/** design S1d (source-tag): the issuing worker's session id ({@link Checkpoint.sourceTaskId}) — projected so
|
|
1204
|
+
* a supervisor inbox attributes each pending checkpoint to its worker in ONE `listByScope` call (no N+1
|
|
1205
|
+
* `getCheckpoint`). ECHO-ONLY display/triage; absent on a non-ask gate / legacy checkpoint. */
|
|
267
1206
|
sourceTaskId?: string;
|
|
1207
|
+
/** design S1d (source-tag): the issuing task's end-user principal ({@link Checkpoint.principal}) for per-user
|
|
1208
|
+
* inbox attribution. ECHO-ONLY display/triage; absent when the checkpoint carries no principal. */
|
|
268
1209
|
principal?: string;
|
|
1210
|
+
/** The pending tool call's id ({@link PendingAction} `tool_approval.toolCallId`) when this is an approval
|
|
1211
|
+
* suspend — projected so the inbox shows WHICH call awaits a decision without an N+1 `getCheckpoint`.
|
|
1212
|
+
* `undefined` for a non-`tool_approval` pendingAction (resource_limit / plan_review / task_done).
|
|
1213
|
+
*
|
|
1214
|
+
* **ABSENCE SHAPE (contract, [1995]②)** — shared by every optional field on this projection: a
|
|
1215
|
+
* tool-less park (`resource_limit` / `plan_review` / `task_done` — none of which binds a tool call)
|
|
1216
|
+
* gets the key **OMITTED**, never `null` and never a present-but-`undefined` property. The
|
|
1217
|
+
* `PendingAction` arms for those kinds declare no tool/binding fields at all, so there is nothing to
|
|
1218
|
+
* project; `summarizeCheckpoint` spreads them conditionally for exactly this reason. A consumer
|
|
1219
|
+
* therefore tests presence (`"toolCallId" in summary`), and a re-projecting service (BFF / SDK wire
|
|
1220
|
+
* face) must not materialize the keys as `null` on the way out. Pinned in
|
|
1221
|
+
* `test/checkpoint-store.test.ts` ("[1995]②③ tool-less park …"). */
|
|
269
1222
|
toolCallId?: string;
|
|
1223
|
+
/** The pending tool's name ({@link PendingAction} `tool_approval.toolName`) when this is an approval suspend.
|
|
1224
|
+
* OMITTED (not `null`) for a non-`tool_approval` pendingAction — see {@link toolCallId}'s absence-shape note. */
|
|
270
1225
|
toolName?: string;
|
|
1226
|
+
/** design/99 MF-14 (design-review DoR): the DERIVED content-gate classification — `"content_ask"` when this
|
|
1227
|
+
* `tool_approval` gates the reserved AskUserQuestion tool (a question TO the user, not a side-effecting tool).
|
|
1228
|
+
* A TYPED discriminant so a shell renders the question UI without sniffing `toolName` or parsing the bounded
|
|
1229
|
+
* `toolInput` preview. The full typed questions ride the drilled-in Checkpoint's `pendingAction.args` (kept off
|
|
1230
|
+
* this lightweight `listByScope` projection). Deliberately NOT a new `content_ask` checkpoint gate kind (that
|
|
1231
|
+
* would cross the durable-checkpoint once-only-winner / reaper-deadline / batch invariants — the DoR). */
|
|
271
1232
|
contentKind?: "content_ask";
|
|
1233
|
+
/** design S1e (service [204]): when the checkpoint was created ({@link Checkpoint.createdAt}, epoch ms) —
|
|
1234
|
+
* projected so a supervisor inbox can sort/age pending entries (oldest-first triage) in ONE `listByScope`
|
|
1235
|
+
* call, no N+1 `getCheckpoint`. ECHO-ONLY display/triage; always present (a Checkpoint always has a
|
|
1236
|
+
* `createdAt`). */
|
|
272
1237
|
createdAt?: number;
|
|
1238
|
+
/**
|
|
1239
|
+
* design S1e (service [204]): a **BOUNDED, UNREDACTED** raw preview of the pending tool call's input
|
|
1240
|
+
* ({@link PendingAction} `tool_approval.args` `JSON.stringify`-ed, truncated to {@link
|
|
1241
|
+
* MAX_TOOL_INPUT_PREVIEW_CHARS} chars with a `…` marker when over) — projected so a supervisor inbox shows
|
|
1242
|
+
* WHAT a paused tool call will do without an N+1 `getCheckpoint`. `undefined` for a non-`tool_approval`
|
|
1243
|
+
* pendingAction (resource_limit / plan_review / task_done — no args to preview).
|
|
1244
|
+
*
|
|
1245
|
+
* **Contract (load-bearing):**
|
|
1246
|
+
* - **bounded raw preview** — length-capped to {@link MAX_TOOL_INPUT_PREVIEW_CHARS} so it cannot bloat the
|
|
1247
|
+
* `listByScope` payload, but the chars within the cap are the RAW serialized args (no folding/escaping).
|
|
1248
|
+
* - **redaction is the CONSUMER's responsibility** — core gives the inbox the unredacted bounded raw; the
|
|
1249
|
+
* service/inbox decides what (if anything) to mask before showing a human. (Contrast {@link
|
|
1250
|
+
* RiskDescriptor.summary}, which is the NEUTRALIZED+capped descriptor that DOES ride into a render —
|
|
1251
|
+
* `toolInput` is the raw-data sibling for a consumer that wants the actual args.)
|
|
1252
|
+
* - **same data as the full {@link Checkpoint}** — it is a projection of `pendingAction.args`, which the
|
|
1253
|
+
* caller could already read via `get`; this adds NO new exposure, it only saves the round-trip.
|
|
1254
|
+
* - **ECHO-ONLY, like {@link sourceTaskId}** — NO gate / CAS / resume / {@link winnerFromOutcome} path reads
|
|
1255
|
+
* it (a contract test pins this); it is pure display/triage metadata, never a control input.
|
|
1256
|
+
*/
|
|
273
1257
|
toolInput?: string;
|
|
1258
|
+
/**
|
|
1259
|
+
* RB-439-b — HOW this pause's remote workspace comes back, projected from
|
|
1260
|
+
* `state.workspaceHandle.restoreMode` so a supervisor inbox can tell the two apart in ONE `listByScope`
|
|
1261
|
+
* call. `"snapshot"` = the VM was paused into a snapshot (billing typically stops, memory state
|
|
1262
|
+
* captured); `"park_only"` = a non-suspendable target (SSH host / ADB device) was never paused — the
|
|
1263
|
+
* machine is still running and still costing, and resume just reconnects. OMITTED (see the
|
|
1264
|
+
* {@link toolCallId} absence-shape note) for a process-local suspend, which captured no remote
|
|
1265
|
+
* workspace at all. ECHO-ONLY display/triage — nothing gates on it. Mirrors `TaskResult.workspaceRestoreMode`.
|
|
1266
|
+
*/
|
|
274
1267
|
restoreMode?: "snapshot" | "park_only";
|
|
1268
|
+
/** [1245] codex F2 — the tool's approval display projection ({@link PendingAction} `preview`),
|
|
1269
|
+
* projected so the ONE-CALL inbox (`listByScope`) can render a rich confirmation without an N+1
|
|
1270
|
+
* `get`. Same trust posture as `toolInput`: **UNTRUSTED, advisory display metadata** (strings are
|
|
1271
|
+
* sanitized of control chars at mint but may still carry model-authored markdown — renderers must
|
|
1272
|
+
* contextually escape), ECHO-ONLY (no gate/CAS/resume reads it), and surfaces SHOULD show the
|
|
1273
|
+
* bound raw args (`toolInput`/`pendingAction.args`) alongside — the preview never replaces them. */
|
|
275
1274
|
preview?: unknown;
|
|
276
1275
|
}
|
|
1276
|
+
/**
|
|
1277
|
+
* design/80 assistant-scheduler seam #1: project a single PENDING {@link Checkpoint} to its lightweight
|
|
1278
|
+
* {@link CheckpointSummary}. Shared by every {@link CheckpointStore} impl so the projection is IDENTICAL
|
|
1279
|
+
* across the in-memory, file, and durable backends (the anti-drift guard) — `severity` always derives from the
|
|
1280
|
+
* gate's {@link RiskDescriptor} via the single {@link riskSeverity} helper, `spentMicroUsd` always reads
|
|
1281
|
+
* {@link ResourceLedger.spentMicroUsd}. Pure; reads no clock/random.
|
|
1282
|
+
*/
|
|
277
1283
|
export declare function summarizeCheckpoint(cp: Checkpoint): CheckpointSummary;
|
|
1284
|
+
/** A typed checkpoint-store error so callers branch on `code` (mirrors `SessionError`). */
|
|
278
1285
|
export declare class CheckpointError extends Error {
|
|
279
|
-
readonly code: "checkpoint.already_exists" | "checkpoint.already_resolved" | "checkpoint.not_found"
|
|
1286
|
+
readonly code: "checkpoint.already_exists" | "checkpoint.already_resolved" | "checkpoint.not_found"
|
|
1287
|
+
/** `runner.resume` was handed an {@link ResumeOutcome} whose `gate` arm does not match the
|
|
1288
|
+
* persisted {@link CheckpointGate} `kind` (council #3 — type confusion guard), or a gate v1
|
|
1289
|
+
* resume does not serve (`task_done`). */
|
|
1290
|
+
| "checkpoint.gate_mismatch"
|
|
1291
|
+
/** RB-77: `runner.resume` was handed an ALREADY-ABORTED `TaskSpec.signal`. Refused BEFORE the CAS, so
|
|
1292
|
+
* the checkpoint stays `pending` — this worker demonstrably cannot execute the approved action, and
|
|
1293
|
+
* consuming the human's decision on a leg that will abort immediately loses BOTH the decision and the
|
|
1294
|
+
* suspended work with nothing to reopen. Re-resume with a live signal. */
|
|
1295
|
+
| "checkpoint.resume_aborted"
|
|
1296
|
+
/** `runner.resume` was handed an outcome whose payload is unsafe to apply — e.g. a deny `reason`
|
|
1297
|
+
* carrying a `</system-reminder>` tag that would escape the model-facing wrapper (round-2 #1). */
|
|
1298
|
+
| "checkpoint.invalid_outcome"
|
|
1299
|
+
/** The checkpoint's {@link Checkpoint.version} is newer than this worker supports
|
|
1300
|
+
* (`> MAX_SUPPORTED_CHECKPOINT_VERSION`), or it carries a remote `workspaceHandle` but no
|
|
1301
|
+
* `executionEnvFactory` is wired to rebuild the env — either way this worker cannot safely resume it.
|
|
1302
|
+
* Rejected pre-CAS so the checkpoint stays `pending` and a capable worker can still resume it
|
|
1303
|
+
* (design/49 §2/§3, code-ready council round-2). */
|
|
1304
|
+
| "checkpoint.unsupported_version"
|
|
1305
|
+
/** design/164: the checkpoint was written by a pre-164 worker AND its ledger carries the RETIRED
|
|
1306
|
+
* cross-slice wall-clock allocation (`resourceLedger.totalWalltimeSec`). The axis no longer exists,
|
|
1307
|
+
* so resuming would run the leg with the operator's time ceiling silently unenforced. Refused
|
|
1308
|
+
* PRE-CAS (the row stays `pending`, so a worker of the previous release can still finish it) rather
|
|
1309
|
+
* than migrated: there is no honest conversion from a wall-clock allocation to a token one. */
|
|
1310
|
+
| "checkpoint.walltime_axis_retired"
|
|
1311
|
+
/** design/80 D-1 (reopen-by-reason): a re-resume of an `env_failed`-reopened checkpoint supplied an
|
|
1312
|
+
* outcome that does NOT equal the persisted {@link ResolvedOutcome} winner. An `env_failed` reopen is
|
|
1313
|
+
* a SYSTEM RETRY of the already-approved action (the infra failed, not the decision) — it must replay
|
|
1314
|
+
* the exact winning binding, never a new vote. Rejected pre-CAS, fail-closed, so the checkpoint stays
|
|
1315
|
+
* `pending` for the correctly-replayed retry. (A `tool_unavailable` reopen — P-7 — is exempt: it lets
|
|
1316
|
+
* a human re-decide with the tool present, so a fresh decision is permitted there.) */
|
|
1317
|
+
| "checkpoint.reopen_revote"
|
|
1318
|
+
/** design/80 D-1 (atomicity fix): the resume validated its decision-action / reopen-by-reason guards
|
|
1319
|
+
* against a `get()` snapshot, but a concurrent {@link CheckpointStore.resolve}/{@link CheckpointStore.reopen}
|
|
1320
|
+
* cycle advanced the monotonic {@link Checkpoint.rev} in the get→resolve window, so the
|
|
1321
|
+
* {@link CheckpointStore.resolve} CAS's optimistic-concurrency check lost (the validation is stale). The
|
|
1322
|
+
* row is still `pending` — fail-closed; the caller must re-`get` and re-validate against the CURRENT state
|
|
1323
|
+
* (a fresh resume), never blindly retry. */
|
|
1324
|
+
| "checkpoint.reopened_concurrently"
|
|
1325
|
+
/** A post-CAS compensation {@link CheckpointStore.reopen} returned `false` — the consumed row could not
|
|
1326
|
+
* be flipped back to `pending` (missing, expired, or changed under a concurrent cycle), so no retryable
|
|
1327
|
+
* checkpoint exists. Surfaced instead of silently treating the failed reopen as committed, so the
|
|
1328
|
+
* caller's compensation path settles the token as terminally consumed. */
|
|
1329
|
+
| "checkpoint.reopen_failed"
|
|
1330
|
+
/** design/80 D-A: {@link CheckpointStore.setPendingSteer} was handed `text` containing a
|
|
1331
|
+
* `</system-reminder>` close tag — it would escape the model-facing `<system-reminder>` wrapper a
|
|
1332
|
+
* trusted steer rides on resume. Rejected fail-closed so a dirty steer NEVER enters {@link CheckpointState}
|
|
1333
|
+
* (§3 inv #2). Mirrors the trusted-steer reject at runtask.ts (`steering.invalid_content`). */
|
|
1334
|
+
| "steering.invalid_content"
|
|
1335
|
+
/** design/171 §6.3: the append would cross the parked-steer queue's entry-count or byte bound
|
|
1336
|
+
* ({@link MAX_PENDING_STEER_ENTRIES} / {@link PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES}). Fail-loud
|
|
1337
|
+
* rather than evicting: an already-accepted operator instruction is never dropped to make room —
|
|
1338
|
+
* the caller delivers or removes a parked steer first. Nothing is written on this path. */
|
|
1339
|
+
| "steering.queue_full"
|
|
1340
|
+
/** design/171 §6.3 (adversarial review adoption): the append reused an `inputId` already parked on
|
|
1341
|
+
* this checkpoint, but with DIFFERENT content. Re-appending an IDENTICAL payload stays the
|
|
1342
|
+
* idempotent no-op a retry needs; a differing one is refused, because swallowing it would lose the
|
|
1343
|
+
* second operator's instruction — the exact silent loss the queue replaced. Retry with a fresh id.
|
|
1344
|
+
* Nothing is written on this path. */
|
|
1345
|
+
| "steering.duplicate_input_id"
|
|
1346
|
+
/** design/144 §3 (governance-gate purity, fail-closed pre-CAS): a `wake` outcome was supplied for a
|
|
1347
|
+
* checkpoint that awaits a PENDING GATE DECISION (tool approval / dry-run review / plan review /
|
|
1348
|
+
* resource-limit continue). Wake is a pure un-park verb and must never bypass an approval — resume
|
|
1349
|
+
* the checkpoint through its own decide entry instead (the error message names it). */
|
|
1350
|
+
| "wake.gate_pending"
|
|
1351
|
+
/** design/144 §3 (anti spin-wake): a `wake` outcome carried no `message` and the checkpoint has no
|
|
1352
|
+
* parked `pendingSteer` either — there is nothing to deliver, so the wake is rejected pre-CAS
|
|
1353
|
+
* (the checkpoint stays pending) instead of burning the token on an empty continuation. */
|
|
1354
|
+
| "wake.nothing_to_deliver"
|
|
1355
|
+
/** Parent effective-policy inheritance: the checkpoint records `state.inheritedGate.requiresParentConstraint`
|
|
1356
|
+
* (the suspended task ran under inherited OPAQUE parent-policy constraints — live closures that cannot
|
|
1357
|
+
* persist), but the resume supplied no `RunInternals.inheritedGate.parentConstraints` to re-instate them.
|
|
1358
|
+
* Resuming anyway would silently DROP the ancestors' gate (a widening), so this rejects pre-CAS — the
|
|
1359
|
+
* checkpoint stays `pending`; re-resume via `resumeStream(token, outcome, config, internals)` with the
|
|
1360
|
+
* parent chain re-supplied. */
|
|
1361
|
+
| "resume.parent_constraint_missing"
|
|
1362
|
+
/** Parent effective-policy inheritance (shape check): the resume DID re-supply
|
|
1363
|
+
* `internals.inheritedGate.parentConstraints`, but its length differs from the checkpoint's recorded
|
|
1364
|
+
* `parentConstraintCount` — a partial/mismatched re-supply would silently run the child under a
|
|
1365
|
+
* thinner (or different) ancestor chain than it suspended with. Rejected pre-CAS (the checkpoint
|
|
1366
|
+
* stays `pending`); re-resume with the full original chain. */
|
|
1367
|
+
| "resume.parent_constraint_mismatch";
|
|
1368
|
+
/** Structured discrimination a caller can branch on where the code alone is ambiguous. Today one
|
|
1369
|
+
* member: `field` names WHICH part of the decision payload was rejected on a
|
|
1370
|
+
* `checkpoint.invalid_outcome` — `"boundCallId"` (the action you decided on has been replaced:
|
|
1371
|
+
* re-fetch the pending list), `"boundInputHash"` (the input you reviewed has changed under the same
|
|
1372
|
+
* action: re-review), or `"answer"` (the content-ask answer is missing, or was attached to a
|
|
1373
|
+
* decision that cannot carry one: re-decide with a well-formed answer — ruled 2026-08-04), or
|
|
1374
|
+
* `"settledBy"` (the settlement source is outside the three-word vocabulary: re-decide naming one
|
|
1375
|
+
* of them, or omit the field). The throw
|
|
1376
|
+
* sites always knew which arm fired; this carries that fact instead of dropping it (ruled
|
|
1377
|
+
* 2026-08-03, cross-repo HITL discussion). Additive and optional — `code` remains the only REQUIRED
|
|
1378
|
+
* discriminant. */
|
|
280
1379
|
readonly detail?: {
|
|
281
1380
|
field?: "boundCallId" | "boundInputHash" | "answer" | "settledBy";
|
|
282
1381
|
} | undefined;
|
|
283
|
-
constructor(code: "checkpoint.already_exists" | "checkpoint.already_resolved" | "checkpoint.not_found"
|
|
1382
|
+
constructor(code: "checkpoint.already_exists" | "checkpoint.already_resolved" | "checkpoint.not_found"
|
|
1383
|
+
/** `runner.resume` was handed an {@link ResumeOutcome} whose `gate` arm does not match the
|
|
1384
|
+
* persisted {@link CheckpointGate} `kind` (council #3 — type confusion guard), or a gate v1
|
|
1385
|
+
* resume does not serve (`task_done`). */
|
|
1386
|
+
| "checkpoint.gate_mismatch"
|
|
1387
|
+
/** RB-77: `runner.resume` was handed an ALREADY-ABORTED `TaskSpec.signal`. Refused BEFORE the CAS, so
|
|
1388
|
+
* the checkpoint stays `pending` — this worker demonstrably cannot execute the approved action, and
|
|
1389
|
+
* consuming the human's decision on a leg that will abort immediately loses BOTH the decision and the
|
|
1390
|
+
* suspended work with nothing to reopen. Re-resume with a live signal. */
|
|
1391
|
+
| "checkpoint.resume_aborted"
|
|
1392
|
+
/** `runner.resume` was handed an outcome whose payload is unsafe to apply — e.g. a deny `reason`
|
|
1393
|
+
* carrying a `</system-reminder>` tag that would escape the model-facing wrapper (round-2 #1). */
|
|
1394
|
+
| "checkpoint.invalid_outcome"
|
|
1395
|
+
/** The checkpoint's {@link Checkpoint.version} is newer than this worker supports
|
|
1396
|
+
* (`> MAX_SUPPORTED_CHECKPOINT_VERSION`), or it carries a remote `workspaceHandle` but no
|
|
1397
|
+
* `executionEnvFactory` is wired to rebuild the env — either way this worker cannot safely resume it.
|
|
1398
|
+
* Rejected pre-CAS so the checkpoint stays `pending` and a capable worker can still resume it
|
|
1399
|
+
* (design/49 §2/§3, code-ready council round-2). */
|
|
1400
|
+
| "checkpoint.unsupported_version"
|
|
1401
|
+
/** design/164: the checkpoint was written by a pre-164 worker AND its ledger carries the RETIRED
|
|
1402
|
+
* cross-slice wall-clock allocation (`resourceLedger.totalWalltimeSec`). The axis no longer exists,
|
|
1403
|
+
* so resuming would run the leg with the operator's time ceiling silently unenforced. Refused
|
|
1404
|
+
* PRE-CAS (the row stays `pending`, so a worker of the previous release can still finish it) rather
|
|
1405
|
+
* than migrated: there is no honest conversion from a wall-clock allocation to a token one. */
|
|
1406
|
+
| "checkpoint.walltime_axis_retired"
|
|
1407
|
+
/** design/80 D-1 (reopen-by-reason): a re-resume of an `env_failed`-reopened checkpoint supplied an
|
|
1408
|
+
* outcome that does NOT equal the persisted {@link ResolvedOutcome} winner. An `env_failed` reopen is
|
|
1409
|
+
* a SYSTEM RETRY of the already-approved action (the infra failed, not the decision) — it must replay
|
|
1410
|
+
* the exact winning binding, never a new vote. Rejected pre-CAS, fail-closed, so the checkpoint stays
|
|
1411
|
+
* `pending` for the correctly-replayed retry. (A `tool_unavailable` reopen — P-7 — is exempt: it lets
|
|
1412
|
+
* a human re-decide with the tool present, so a fresh decision is permitted there.) */
|
|
1413
|
+
| "checkpoint.reopen_revote"
|
|
1414
|
+
/** design/80 D-1 (atomicity fix): the resume validated its decision-action / reopen-by-reason guards
|
|
1415
|
+
* against a `get()` snapshot, but a concurrent {@link CheckpointStore.resolve}/{@link CheckpointStore.reopen}
|
|
1416
|
+
* cycle advanced the monotonic {@link Checkpoint.rev} in the get→resolve window, so the
|
|
1417
|
+
* {@link CheckpointStore.resolve} CAS's optimistic-concurrency check lost (the validation is stale). The
|
|
1418
|
+
* row is still `pending` — fail-closed; the caller must re-`get` and re-validate against the CURRENT state
|
|
1419
|
+
* (a fresh resume), never blindly retry. */
|
|
1420
|
+
| "checkpoint.reopened_concurrently"
|
|
1421
|
+
/** A post-CAS compensation {@link CheckpointStore.reopen} returned `false` — the consumed row could not
|
|
1422
|
+
* be flipped back to `pending` (missing, expired, or changed under a concurrent cycle), so no retryable
|
|
1423
|
+
* checkpoint exists. Surfaced instead of silently treating the failed reopen as committed, so the
|
|
1424
|
+
* caller's compensation path settles the token as terminally consumed. */
|
|
1425
|
+
| "checkpoint.reopen_failed"
|
|
1426
|
+
/** design/80 D-A: {@link CheckpointStore.setPendingSteer} was handed `text` containing a
|
|
1427
|
+
* `</system-reminder>` close tag — it would escape the model-facing `<system-reminder>` wrapper a
|
|
1428
|
+
* trusted steer rides on resume. Rejected fail-closed so a dirty steer NEVER enters {@link CheckpointState}
|
|
1429
|
+
* (§3 inv #2). Mirrors the trusted-steer reject at runtask.ts (`steering.invalid_content`). */
|
|
1430
|
+
| "steering.invalid_content"
|
|
1431
|
+
/** design/171 §6.3: the append would cross the parked-steer queue's entry-count or byte bound
|
|
1432
|
+
* ({@link MAX_PENDING_STEER_ENTRIES} / {@link PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES}). Fail-loud
|
|
1433
|
+
* rather than evicting: an already-accepted operator instruction is never dropped to make room —
|
|
1434
|
+
* the caller delivers or removes a parked steer first. Nothing is written on this path. */
|
|
1435
|
+
| "steering.queue_full"
|
|
1436
|
+
/** design/171 §6.3 (adversarial review adoption): the append reused an `inputId` already parked on
|
|
1437
|
+
* this checkpoint, but with DIFFERENT content. Re-appending an IDENTICAL payload stays the
|
|
1438
|
+
* idempotent no-op a retry needs; a differing one is refused, because swallowing it would lose the
|
|
1439
|
+
* second operator's instruction — the exact silent loss the queue replaced. Retry with a fresh id.
|
|
1440
|
+
* Nothing is written on this path. */
|
|
1441
|
+
| "steering.duplicate_input_id"
|
|
1442
|
+
/** design/144 §3 (governance-gate purity, fail-closed pre-CAS): a `wake` outcome was supplied for a
|
|
1443
|
+
* checkpoint that awaits a PENDING GATE DECISION (tool approval / dry-run review / plan review /
|
|
1444
|
+
* resource-limit continue). Wake is a pure un-park verb and must never bypass an approval — resume
|
|
1445
|
+
* the checkpoint through its own decide entry instead (the error message names it). */
|
|
1446
|
+
| "wake.gate_pending"
|
|
1447
|
+
/** design/144 §3 (anti spin-wake): a `wake` outcome carried no `message` and the checkpoint has no
|
|
1448
|
+
* parked `pendingSteer` either — there is nothing to deliver, so the wake is rejected pre-CAS
|
|
1449
|
+
* (the checkpoint stays pending) instead of burning the token on an empty continuation. */
|
|
1450
|
+
| "wake.nothing_to_deliver"
|
|
1451
|
+
/** Parent effective-policy inheritance: the checkpoint records `state.inheritedGate.requiresParentConstraint`
|
|
1452
|
+
* (the suspended task ran under inherited OPAQUE parent-policy constraints — live closures that cannot
|
|
1453
|
+
* persist), but the resume supplied no `RunInternals.inheritedGate.parentConstraints` to re-instate them.
|
|
1454
|
+
* Resuming anyway would silently DROP the ancestors' gate (a widening), so this rejects pre-CAS — the
|
|
1455
|
+
* checkpoint stays `pending`; re-resume via `resumeStream(token, outcome, config, internals)` with the
|
|
1456
|
+
* parent chain re-supplied. */
|
|
1457
|
+
| "resume.parent_constraint_missing"
|
|
1458
|
+
/** Parent effective-policy inheritance (shape check): the resume DID re-supply
|
|
1459
|
+
* `internals.inheritedGate.parentConstraints`, but its length differs from the checkpoint's recorded
|
|
1460
|
+
* `parentConstraintCount` — a partial/mismatched re-supply would silently run the child under a
|
|
1461
|
+
* thinner (or different) ancestor chain than it suspended with. Rejected pre-CAS (the checkpoint
|
|
1462
|
+
* stays `pending`); re-resume with the full original chain. */
|
|
1463
|
+
| "resume.parent_constraint_mismatch", message: string,
|
|
1464
|
+
/** Structured discrimination a caller can branch on where the code alone is ambiguous. Today one
|
|
1465
|
+
* member: `field` names WHICH part of the decision payload was rejected on a
|
|
1466
|
+
* `checkpoint.invalid_outcome` — `"boundCallId"` (the action you decided on has been replaced:
|
|
1467
|
+
* re-fetch the pending list), `"boundInputHash"` (the input you reviewed has changed under the same
|
|
1468
|
+
* action: re-review), or `"answer"` (the content-ask answer is missing, or was attached to a
|
|
1469
|
+
* decision that cannot carry one: re-decide with a well-formed answer — ruled 2026-08-04), or
|
|
1470
|
+
* `"settledBy"` (the settlement source is outside the three-word vocabulary: re-decide naming one
|
|
1471
|
+
* of them, or omit the field). The throw
|
|
1472
|
+
* sites always knew which arm fired; this carries that fact instead of dropping it (ruled
|
|
1473
|
+
* 2026-08-03, cross-repo HITL discussion). Additive and optional — `code` remains the only REQUIRED
|
|
1474
|
+
* discriminant. */
|
|
1475
|
+
detail?: {
|
|
284
1476
|
field?: "boundCallId" | "boundInputHash" | "answer" | "settledBy";
|
|
285
1477
|
} | undefined);
|
|
286
1478
|
}
|
|
1479
|
+
/**
|
|
1480
|
+
* The pluggable durable seam (design/45 §2.1). Symmetric with `ToolResultStore`/`MemoryStore`:
|
|
1481
|
+
* create-once `put`, `get`, an **atomic CAS `resolve`** (the once-only foundation), and a `reap` for TTL
|
|
1482
|
+
* expiry. A durable backend (TiDB) makes resume cross-replica; the default in-memory impl is single
|
|
1483
|
+
* instance / tests only.
|
|
1484
|
+
*
|
|
1485
|
+
* **Load-bearing contract (§2.1, service-side report):** `resolve` is the once-only gate. The store guarantees
|
|
1486
|
+
* *exactly one* `resolve(token, ...)` wins the CAS (pending → resolved); the **runner** must treat
|
|
1487
|
+
* "won the CAS" as the *sole* trigger to execute the pending action — that is what makes a pending tool
|
|
1488
|
+
* call run exactly once across retries / double approvals / multiple replicas.
|
|
1489
|
+
*
|
|
1490
|
+
* **design/80 §D-D expiry semantic contract (gate.kind-aware reap + `terminalAt` backstop):** the
|
|
1491
|
+
* reaper-LOOP that calls {@link reap}/{@link expire} is the DEPLOYMENT's (its store impl + loop — OWNED
|
|
1492
|
+
* service-side); core's D-D job is ONLY to keep writing the durable FIELDS the loop
|
|
1493
|
+
* reads ({@link Checkpoint.deadline} for an approval gate + the persisted {@link Checkpoint.gate}.kind) and
|
|
1494
|
+
* to DOCUMENT the contract here so ANY reaper consumer (the service's TiDB loop, or a non-service Pg
|
|
1495
|
+
* deployment) implements it IDENTICALLY:
|
|
1496
|
+
* - **FACET A — APPROVAL gates** (`gate.kind` ∈ {`human`, `irreversible_ask`}): the loop's FAST PATH at
|
|
1497
|
+
* `deadline` is an SLA **resolve-deny** (`resolve(token, {gate:"policy_ask", decision:"deny"}, …)` so the
|
|
1498
|
+
* suspended run ends cleanly), with a LATER `terminalAt` **abandonment backstop** the loop ALSO honors
|
|
1499
|
+
* (crash-safe if the SLA timer never fired — then {@link reap}/{@link expire} flips it `expired`).
|
|
1500
|
+
* - **FACET B — UNATTENDED-TTL gates** (`gate.kind` ∈ {`resource_limit`, `needs_review`, `plan_review`} — the
|
|
1501
|
+
* resource slice + the review-pause family): `deadline` is an abandonment-TTL → {@link expire}/{@link reap}
|
|
1502
|
+
* ONLY, **NEVER** a resolve-deny (a resolve-deny on these kinds hits NO `gateMatch` arm → `gate_mismatch`;
|
|
1503
|
+
* their resume outcome is `resource_limit`/`dry_run_review`/`plan_review` respectively, not `policy_ask`).
|
|
1504
|
+
* NO core-minted `terminalAt`, NO core reaper, NO change to {@link reap} logic — minting `terminalAt` and
|
|
1505
|
+
* running the gate.kind-aware loop are the deployment's, by design.
|
|
1506
|
+
*/
|
|
1507
|
+
/**
|
|
1508
|
+
* design/173 §2.3 — the store DURABILITY declaration vocabulary (declaration, never duck-typing —
|
|
1509
|
+
* the exact posture of {@link CheckpointStore.retention}): `"durable"` promises the rows SURVIVE A
|
|
1510
|
+
* PROCESS RESTART of the deployment that owns the store's data domain (a durable park can be
|
|
1511
|
+
* resumed by a later process); `"process-local"` states they die with the process. **This axis
|
|
1512
|
+
* claims restart survival ONLY — it does NOT claim multi-replica coordination** (whether a SECOND
|
|
1513
|
+
* concurrent replica can serve the same rows is a separate property of the backend: the file
|
|
1514
|
+
* backend is single-instance-per-data-dir by design, Pg/TiDB backends coordinate — both honestly
|
|
1515
|
+
* declare `"durable"` on THIS axis; review fold round 1, finding 2). ABSENT is read FAIL-CLOSED as
|
|
1516
|
+
* `"process-local"` (the wiring manifest may under-promise durability, never over-promise it); an
|
|
1517
|
+
* unparseable value is refused loudly (`config.store_durability_invalid`) rather than folded to
|
|
1518
|
+
* either arm. Adapter contract with a conformance obligation: a backend that persists outside the
|
|
1519
|
+
* process (file/Pg/TiDB) SHOULD declare `"durable"` — until it does, restart-surviving resume
|
|
1520
|
+
* capability is reported as absent.
|
|
1521
|
+
*/
|
|
287
1522
|
export type StoreDurability = "durable" | "process-local";
|
|
1523
|
+
/**
|
|
1524
|
+
* The store ENCODING-FIDELITY declaration vocabulary — the third honest-declaration axis, alongside
|
|
1525
|
+
* {@link StoreDurability} and {@link CheckpointStore.retention}, and the one that says WHICH VALUES a
|
|
1526
|
+
* row can hold unchanged.
|
|
1527
|
+
*
|
|
1528
|
+
* `"structured-clone"` promises the structured-clone value domain survives a round trip through the
|
|
1529
|
+
* backend (a `Date` comes back a `Date`, a `Map` a `Map`, `NaN` stays `NaN`); `"json"` states the
|
|
1530
|
+
* backend serialises through JSON, so the row that comes back after a restart carries the JSON
|
|
1531
|
+
* PROJECTION of what was filed (`Date` → string, `Map`/`Set` → `{}`, `NaN`/`Infinity` → `null`, `-0`
|
|
1532
|
+
* → `0`, an `undefined`-valued key gone).
|
|
1533
|
+
*
|
|
1534
|
+
* Why this has to be declared rather than inferred: the mint decides what a durable approval row may
|
|
1535
|
+
* carry, and it decides it with `structuredClone` — which is WIDER than a JSON codec. A value can pass
|
|
1536
|
+
* that check, be filed, degrade on disk, and come back a different value, while the binding hash minted
|
|
1537
|
+
* over the pre-serialisation value still matches the one the operator echoes (the resume compares the
|
|
1538
|
+
* persisted string opaquely and never re-hashes). The approval is then accepted for arguments that are
|
|
1539
|
+
* not the arguments it bound. Only the backend knows its own codec, so only the backend can say.
|
|
1540
|
+
*
|
|
1541
|
+
* ABSENT is read FAIL-CLOSED as `"json"` — the narrower domain, the same "may under-promise, never
|
|
1542
|
+
* over-promise" posture {@link StoreDurability} takes with `"process-local"`: an undeclared backend gets
|
|
1543
|
+
* a row it can certainly hold, and a backend that can hold more earns the wider domain by saying so. An
|
|
1544
|
+
* unparseable value is refused loudly (`config.store_fidelity_invalid`) rather than folded to either arm.
|
|
1545
|
+
*/
|
|
288
1546
|
export type StoreFidelity = "structured-clone" | "json";
|
|
1547
|
+
/**
|
|
1548
|
+
* Resolve a store's declared {@link StoreFidelity} (the read half of the declaration — the exact shape
|
|
1549
|
+
* `resolveDeclaredFidelity`'s durability twin has): ABSENT → the fail-closed `"json"`, a recognized
|
|
1550
|
+
* value verbatim, anything else refused loudly. Never duck-types the backend.
|
|
1551
|
+
*/
|
|
289
1552
|
export declare function resolveDeclaredFidelity(store: {
|
|
290
1553
|
readonly fidelity?: StoreFidelity;
|
|
291
1554
|
} | undefined, storeName: string): StoreFidelity;
|
|
1555
|
+
/**
|
|
1556
|
+
* Project an already-captured value onto what a backend of the given {@link StoreFidelity} can hold, so
|
|
1557
|
+
* everything minted from it — the row's args, the approval preview, the binding hash, the risk
|
|
1558
|
+
* descriptor — describes the value that comes BACK, not the one that went in.
|
|
1559
|
+
*
|
|
1560
|
+
* `"structured-clone"` is the identity: the caller's snapshot already is what such a backend stores.
|
|
1561
|
+
* `"json"` runs the backend's own codec (`JSON.stringify` → `JSON.parse`, `toJSON` hooks included, since
|
|
1562
|
+
* that is exactly what the backend will do to it). The input is expected to be an accessor-free snapshot
|
|
1563
|
+
* already, so no property is read a second time here.
|
|
1564
|
+
*
|
|
1565
|
+
* A value JSON cannot ENCODE AT ALL (a `BigInt`, a cycle) fails instead of degrading — those would throw
|
|
1566
|
+
* inside the backend's own write anyway, and failing here means the caller can say why while nothing has
|
|
1567
|
+
* been committed. The thrown value rides out on `cause` so the caller decides how to word the refusal.
|
|
1568
|
+
*/
|
|
292
1569
|
export declare function encodeAtFidelity(fidelity: StoreFidelity, value: unknown): {
|
|
293
1570
|
ok: true;
|
|
294
1571
|
value: unknown;
|
|
@@ -296,40 +1573,319 @@ export declare function encodeAtFidelity(fidelity: StoreFidelity, value: unknown
|
|
|
296
1573
|
ok: false;
|
|
297
1574
|
cause: unknown;
|
|
298
1575
|
};
|
|
1576
|
+
/**
|
|
1577
|
+
* CONSERVATIVE "these two are provably the same value" for deciding whether a {@link encodeAtFidelity}
|
|
1578
|
+
* projection moved anything, and whether a policy's rewrite names the very value about to be filed.
|
|
1579
|
+
*
|
|
1580
|
+
* Deliberately NOT the binding hash, which is the wrong instrument for this question in two directions:
|
|
1581
|
+
* it folds `-0` onto `0` (a real projection change the policy may care about, invisible to the hash), and
|
|
1582
|
+
* it is a digest, so a collision would read as sameness. `Object.is` at the leaves keeps the `-0`
|
|
1583
|
+
* distinction; comparing own-key SETS keeps the dropped `undefined`-valued key visible.
|
|
1584
|
+
*
|
|
1585
|
+
* Conservative on purpose — the answer it must never give wrongly is TRUE, because both callers read
|
|
1586
|
+
* TRUE as "nothing moved / the policy named this very value" and act by filing. So sameness has to be
|
|
1587
|
+
* PROVED, not merely un-disproved:
|
|
1588
|
+
* - the PROTOTYPES must match and be the plain-object or array one. `[object Object]` is not proof of
|
|
1589
|
+
* a plain object — a class instance and a null-prototype object both report it, and a tool executing
|
|
1590
|
+
* the arguments can observe inherited behaviour. (Neither input to the projection comparison can be
|
|
1591
|
+
* such a value: `structuredClone` and `JSON.parse` both produce plain data. A policy's rewrite can.)
|
|
1592
|
+
* - own keys are compared with `Reflect.ownKeys` and POSITIONALLY, so a SYMBOL key or a non-enumerable
|
|
1593
|
+
* property on one side is a difference rather than an invisible extra, and so is a different order
|
|
1594
|
+
* (which `Object.keys`, `JSON.stringify` and any tool iterating its arguments can see).
|
|
1595
|
+
* - properties are compared through their DESCRIPTORS — every flag, so a frozen object is not the
|
|
1596
|
+
* mutable value a JSON decode produces — and an accessor on either side answers FALSE without being
|
|
1597
|
+
* read: an approval decision must not be made by running deployment code, and what a getter returns
|
|
1598
|
+
* need not be the same twice.
|
|
1599
|
+
* - reference TOPOLOGY counts: two properties that share one object are not two properties holding
|
|
1600
|
+
* equal objects, and a JSON round trip is precisely what turns the first into the second.
|
|
1601
|
+
* - what it CANNOT prove on its own is that the value it inspected answers ordinary reads the way its
|
|
1602
|
+
* descriptors did — a Proxy reports the target's descriptors and can return anything from a `get`.
|
|
1603
|
+
* A caller comparing a value it did not construct must first establish that it is an inert data
|
|
1604
|
+
* graph (the capture clone does exactly that, and refuses a Proxy outright).
|
|
1605
|
+
* - any brand outside plain object/array that is not already reference-identical answers FALSE (a Date
|
|
1606
|
+
* is never the string a JSON codec leaves behind, a Map is never the `{}`), and so does any
|
|
1607
|
+
* inspection that throws.
|
|
1608
|
+
* An uncertain answer therefore costs a policy consultation or a park, never a silently filed value.
|
|
1609
|
+
*/
|
|
299
1610
|
export declare function samePlainValue(a: unknown, b: unknown): boolean;
|
|
300
1611
|
export interface CheckpointStore {
|
|
1612
|
+
/**
|
|
1613
|
+
* Managed-retention declaration (ruled 2026-08-05, retention.ts): `"managed"` promises the
|
|
1614
|
+
* `ManagedRetentionCapability` contract — including the reap/unpin gap this interface documents on
|
|
1615
|
+
* {@link reap} (a managed store's scheduled expiry DOES release the session pins). `"none"` (or
|
|
1616
|
+
* ABSENT — read fail-closed as `"none"`) states the store cannot delete on schedule; a deployment
|
|
1617
|
+
* with a LOCKED retention policy refuses to start over it. Declaration, never duck-typing.
|
|
1618
|
+
*/
|
|
301
1619
|
readonly retention?: import("./retention.js").RetentionDeclaration;
|
|
1620
|
+
/**
|
|
1621
|
+
* design/173 §2.3 — durability declaration (see {@link StoreDurability}): whether checkpoints
|
|
1622
|
+
* this store accepts survive a process restart. Same "declaration, never duck-typing" posture as
|
|
1623
|
+
* {@link retention}; the wiring manifest relays it verbatim and reads ABSENT fail-closed as
|
|
1624
|
+
* `"process-local"`.
|
|
1625
|
+
*/
|
|
302
1626
|
readonly durability?: StoreDurability;
|
|
1627
|
+
/**
|
|
1628
|
+
* Encoding-fidelity declaration (see {@link StoreFidelity}): which value domain a row keeps INTACT
|
|
1629
|
+
* through this backend. Same "declaration, never duck-typing" posture as {@link durability}; the park
|
|
1630
|
+
* mint reads it to decide the width of the snapshot it files, and reads ABSENT fail-closed as
|
|
1631
|
+
* `"json"`. A backend that serialises through JSON MUST say so (or leave it absent, which says the
|
|
1632
|
+
* same) — a `"structured-clone"` claim it cannot keep is what lets a row degrade under a binding hash
|
|
1633
|
+
* that still matches.
|
|
1634
|
+
*/
|
|
303
1635
|
readonly fidelity?: StoreFidelity;
|
|
1636
|
+
/** Create-once. Throws {@link CheckpointError} `already_exists` on a token collision (never a silent
|
|
1637
|
+
* overwrite — a reused token would clobber a live suspension). */
|
|
304
1638
|
put(token: CheckpointToken, cp: Checkpoint): Promise<void>;
|
|
305
1639
|
get(token: CheckpointToken): Promise<Checkpoint | null>;
|
|
1640
|
+
/**
|
|
1641
|
+
* Atomic CAS: `UPDATE … SET status='resolved' WHERE token=? AND scope=? AND status='pending'`.
|
|
1642
|
+
* Returns `true` for the single winner (was pending → now resolved), `false` if already resolved/
|
|
1643
|
+
* expired (`AlreadyResolved` — the caller treats it as a no-op, never re-executes). `scope` is in the
|
|
1644
|
+
* WHERE for multi-tenant isolation (a wrong-scope resolve must not win).
|
|
1645
|
+
*
|
|
1646
|
+
* `outcome` is supplied so a **durable** backend persists it atomically with the status flip (design/45
|
|
1647
|
+
* v2 resumable-resume hook). **design/80 D-1 (persist-winner):** when the CAS wins, the store ALSO records
|
|
1648
|
+
* the winning binding as {@link Checkpoint.resolvedOutcome} on the row — so a later {@link reopen} →
|
|
1649
|
+
* re-resume can be validated against the approved decision (an `env_failed` reopen must replay the same
|
|
1650
|
+
* winner; design/80 §3 inv #1). A `policy_ask` outcome records the decision bound to its pending tool
|
|
1651
|
+
* call; the two REVIEW outcomes (`plan_review` / `dry_run_review`) record theirs under a synthetic
|
|
1652
|
+
* `gate:` marker; `resource_limit` / `wake` / `task_done` bind nothing and record no winner.
|
|
1653
|
+
*
|
|
1654
|
+
* **Implementers: derive the winner with {@link winnerFromOutcome} and persist the object WHOLE.** Its
|
|
1655
|
+
* field set GROWS (`updatedInput`, then `answer`, then `reason` — each additive and optional), and the
|
|
1656
|
+
* replay guard reads an absent optional field as "this backend never recorded it" so that rows written
|
|
1657
|
+
* before the field existed still replay. A backend that projects a hand-listed subset of columns is
|
|
1658
|
+
* therefore indistinguishable from an old row and silently leaves that dimension UNBOUND for every
|
|
1659
|
+
* decision it ever writes — the replay guard cannot tell the two apart, and there is no capability
|
|
1660
|
+
* negotiation on this interface to tell it. Round-trip the whole record (opaque JSON is what every
|
|
1661
|
+
* in-tree backend does) rather than enumerating its keys.
|
|
1662
|
+
*
|
|
1663
|
+
* (The crash-recovery semantics of *acting* on a won CAS are runner contract — see
|
|
1664
|
+
* {@link Runner.resume}; the store only owns the atomic once-only flip + the durable winner.)
|
|
1665
|
+
*/
|
|
306
1666
|
resolve(token: CheckpointToken, scope: string, outcome: ResumeOutcome, expect?: ResolveExpectation): Promise<boolean>;
|
|
1667
|
+
/**
|
|
1668
|
+
* Inverse of {@link resolve}: CAS `resolved → pending` (`UPDATE … SET status='pending' WHERE token=?
|
|
1669
|
+
* AND scope=? AND status='resolved'`). Returns the CAS winner (`true` = was resolved → now pending again).
|
|
1670
|
+
*
|
|
1671
|
+
* **Compensation for a post-CAS env-restore failure (design/45/49).** `Runner.resumeStream` wins the
|
|
1672
|
+
* `resolve` CAS *before* the workspace `resumeVM` runs; if that restore then fails (`resume.env_failed`)
|
|
1673
|
+
* the pending action never executed yet the checkpoint is consumed — without this the suspended work is
|
|
1674
|
+
* lost to a forced "re-initiate" (fine for a short approval, ruinous for a long unattended task).
|
|
1675
|
+
* Reopening lets a retry re-resume the SAME work. Optional: a store that omits it keeps the prior (lossy)
|
|
1676
|
+
* behavior, and the runner degrades gracefully. Safe by the `status='resolved'` guard — only the run that
|
|
1677
|
+
* consumed the checkpoint (and then failed to restore) can reopen it, and only while it is still `resolved`.
|
|
1678
|
+
*
|
|
1679
|
+
* **design/80 D-1 (reopen-by-reason):** the `reason` distinguishes the two reopen triggers (both shipped at
|
|
1680
|
+
* runtask.ts) so re-resume can validate per reason (§3 inv #1): `env_failed` (the action is still valid,
|
|
1681
|
+
* only infra failed → re-resume MUST replay the persisted {@link Checkpoint.resolvedOutcome} winner) vs
|
|
1682
|
+
* `tool_unavailable` (P-7: the approved tool vanished, the action may now be invalid → a human re-decides,
|
|
1683
|
+
* so a FRESH decision is allowed). The store RECORDS the reason on the row as {@link Checkpoint.reopenReason}
|
|
1684
|
+
* and PRESERVES the persisted winner across the reopen (never clears it).
|
|
1685
|
+
*/
|
|
307
1686
|
reopen?(token: CheckpointToken, scope: string, reason: ReopenReason): Promise<boolean>;
|
|
1687
|
+
/**
|
|
1688
|
+
* design/80 D-A: park a durable mid-task STEER on a DURABLY-SUSPENDED task's checkpoint. CAS-style update
|
|
1689
|
+
* `SET state.pendingSteerQueue=? WHERE token=? AND scope=? AND status='pending'`: returns `true` iff the
|
|
1690
|
+
* checkpoint is still `pending` (a resolved/expired checkpoint can't be steered → `false`, a no-op). Scope
|
|
1691
|
+
* is in the WHERE for multi-tenant isolation, exactly like {@link resolve}/{@link expire}.
|
|
1692
|
+
*
|
|
1693
|
+
* **design/171 §5.3 — this APPENDS to {@link CheckpointState.pendingSteerQueue}; it no longer overwrites a
|
|
1694
|
+
* single seat.** Two people steering one suspended run used to silently destroy the first instruction,
|
|
1695
|
+
* with no record that anything was lost. Idempotency moved from last-writer-wins to the `inputId` key:
|
|
1696
|
+
* re-appending an `inputId` already queued (a service retrying with the message id it already minted)
|
|
1697
|
+
* is a no-op that still returns `true`. Crossing {@link MAX_PENDING_STEER_ENTRIES} or
|
|
1698
|
+
* {@link PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES} throws `steering.queue_full` — bounded fail-loud, never
|
|
1699
|
+
* by evicting an accepted instruction.
|
|
1700
|
+
*
|
|
1701
|
+
* **Migration (additive read):** a row written before the queue existed keeps its legacy
|
|
1702
|
+
* {@link CheckpointState.pendingSteer} seat and is read back as queue member 0 by
|
|
1703
|
+
* {@link readPendingSteerQueue}; new writes only ever touch the queue. A backend must round-trip BOTH
|
|
1704
|
+
* fields, and must build its new queue with {@link appendPendingSteer} so the bounds and the `seq` mint
|
|
1705
|
+
* are one implementation rather than one per store.
|
|
1706
|
+
*
|
|
1707
|
+
* **Trust is FROZEN here** — `steer.trusted` is whatever the SERVICE computed from the verified principal
|
|
1708
|
+
* at this call (an operator-role check, NOT a client header — the service's job); the core stores it
|
|
1709
|
+
* VERBATIM and NEVER recomputes it on resume (§3 inv #1).
|
|
1710
|
+
*
|
|
1711
|
+
* **Validation (load-bearing, §3 inv #2):** `steer.text` containing a `</system-reminder>` close tag is
|
|
1712
|
+
* REJECTED with a typed {@link CheckpointError} `steering.invalid_content` (mirroring runtask.ts's trusted-
|
|
1713
|
+
* steer reject) — a dirty steer NEVER enters {@link CheckpointState}, so the persisted state stays clean
|
|
1714
|
+
* regardless of `trusted`. (The untrusted RESUME-injection path ALSO sanitizes the text as untrusted data;
|
|
1715
|
+
* this persist-time reject is the belt that keeps a forged close tag out of the durable record on BOTH
|
|
1716
|
+
* paths.) Text over {@link MAX_PENDING_STEER_CHARS} is rejected with the same code (HRD-APV-4 — the
|
|
1717
|
+
* steer is re-delivered into the model context on every resume, so it is capped like every other
|
|
1718
|
+
* persisted model-facing field on this row; rejected rather than truncated because cutting an
|
|
1719
|
+
* instruction can invert it). NOT an approval channel: this only writes the parked queue, never
|
|
1720
|
+
* touches status / the resolve path / any decision (§3 inv #4).
|
|
1721
|
+
*/
|
|
308
1722
|
setPendingSteer(token: CheckpointToken, scope: string, steer: PendingSteerInput): Promise<boolean>;
|
|
1723
|
+
/**
|
|
1724
|
+
* CAS-expire a **single** `pending` checkpoint by token: `UPDATE … SET status='expired' WHERE token=?
|
|
1725
|
+
* AND scope=? AND status='pending'`. Returns the CAS winner (`true` = was pending → now expired; `false`
|
|
1726
|
+
* = already resolved/expired). The same UPDATE as {@link reap} but keyed by **token**, not by deadline.
|
|
1727
|
+
*
|
|
1728
|
+
* **Load-bearing contract (design/51 §2/§4):** `expire` and {@link resolve} race the **same `pending`
|
|
1729
|
+
* row**, so the store serializes them — exactly one wins. This is what makes {@link TaskStream.destroy}
|
|
1730
|
+
* a correct *fence-then-reap*: `destroy` calls `expire` FIRST to fence any concurrent `resume` (which
|
|
1731
|
+
* goes through `resolve`); only the `expire` winner then destroys the paused env, so the same checkpoint
|
|
1732
|
+
* is **never both reaped and resumed**. `scope` is in the WHERE for multi-tenant isolation, like
|
|
1733
|
+
* `resolve`/`reap`.
|
|
1734
|
+
*/
|
|
309
1735
|
expire(token: CheckpointToken, scope: string): Promise<boolean>;
|
|
1736
|
+
/**
|
|
1737
|
+
* CAS-expire `pending` checkpoints in `scope` whose `deadline` has passed (`deadline <= cutoff`):
|
|
1738
|
+
* `pending → expired`. Returns the count expired (for metrics). Idempotent across replicas (DB
|
|
1739
|
+
* serializes; only the first wins each row) — no leader election needed (§6).
|
|
1740
|
+
*
|
|
1741
|
+
* **Does NOT unpin the sessions of the checkpoints it expires (council finding #2).** A suspend pinned
|
|
1742
|
+
* its session against idle eviction; reaping the checkpoint here leaves that pin in place. The reaper
|
|
1743
|
+
* that calls `reap` owns releasing the pin. For a **durable** session store this is a no-op anyway
|
|
1744
|
+
* (`pin`/`unpin` are no-ops; it never idle-GCs), so reap-without-unpin is harmless — the only store with
|
|
1745
|
+
* a real pin is the in-memory `TtlSessionStore` (single-instance / tests), where an un-released pin
|
|
1746
|
+
* keeps an abandoned suspended session in the cache until process exit. v1 leaves the runner without an
|
|
1747
|
+
* auto-reaper (the service owns the TTL worker); a reaper over the in-memory store should track and
|
|
1748
|
+
* `unpin` the expired sessions itself if that leak matters for its deployment.
|
|
1749
|
+
*/
|
|
310
1750
|
reap(scope: string, cutoff: number): Promise<number>;
|
|
1751
|
+
/**
|
|
1752
|
+
* design/80 assistant-scheduler seam #1: list a lightweight {@link CheckpointSummary}
|
|
1753
|
+
* for EVERY **pending** checkpoint in `scope` (the suspended / awaiting-human tasks — NOT resolved/expired) in
|
|
1754
|
+
* ONE call, so a supervisor scheduler can enumerate a scope's open suspensions without N+1 `get`s or hauling
|
|
1755
|
+
* full {@link Checkpoint} payloads. Read-only — it never mutates a row, takes no CAS, and has ZERO effect on
|
|
1756
|
+
* the once-only resolve. The PENDING predicate is byte-for-byte the {@link resolve} CAS's (`scope === scope &&
|
|
1757
|
+
* status === 'pending'`), so the list reflects exactly the rows that are still resumable. Order is unspecified
|
|
1758
|
+
* (the inbox sorts by `severity`/`deadline` itself). An empty scope returns `[]`.
|
|
1759
|
+
*
|
|
1760
|
+
* **OPTIONAL** for backward-compat with external {@link CheckpointStore} impls (a caller probes
|
|
1761
|
+
* `store.listByScope?.(scope) ?? []`); ALL THREE first-party impls (in-memory, file, Pg) provide it.
|
|
1762
|
+
*/
|
|
311
1763
|
listByScope?(scope: string): Promise<CheckpointSummary[]>;
|
|
1764
|
+
/**
|
|
1765
|
+
* server [1800] seam: the DISTINCT scopes that currently have at least one PENDING checkpoint — the
|
|
1766
|
+
* enumeration face `listByScope` lacks. A host keeping its own scope registry (e.g. a sidecar file for a
|
|
1767
|
+
* cross-scope inbox) can rebuild it from the data itself when the registry is lost/corrupt, instead of the
|
|
1768
|
+
* enumeration silently going empty and pending approvals turning invisible. Scoped to PENDING deliberately:
|
|
1769
|
+
* it matches `listByScope`'s visible surface, so `listScopes() × listByScope()` is exactly the full inbox.
|
|
1770
|
+
* **OPTIONAL** like {@link listByScope}; the in-memory and file first-party impls provide it.
|
|
1771
|
+
*/
|
|
312
1772
|
listScopes?(): Promise<string[]>;
|
|
313
1773
|
}
|
|
1774
|
+
/**
|
|
1775
|
+
* The SINGLE resolution of "which CheckpointStore does this run use" (ruled 2026-08-04; re-homed
|
|
1776
|
+
* from prepare-task with the design/173 wiring manifest so the static self-check half shares it
|
|
1777
|
+
* without a runtime cycle). The spec seat is three-valued — a store wins, `undefined` falls through
|
|
1778
|
+
* to the deployment's `RunnerDeps.checkpointStore`, and an explicit `null` means this run has NO
|
|
1779
|
+
* store at all (see `TaskSpec.checkpointStore`: the per-run off switch for the durable machine, so
|
|
1780
|
+
* a machine-started run cannot mint a checkpoint nobody will ever resolve).
|
|
1781
|
+
*
|
|
1782
|
+
* Written as an explicit `=== null` test rather than the obvious `spec.checkpointStore ??
|
|
1783
|
+
* deps.checkpointStore` BECAUSE `??` treats `null` exactly like `undefined`: under a bare nullish
|
|
1784
|
+
* coalesce the null falls through and the run is handed the very deployment store the caller asked
|
|
1785
|
+
* it not to have, re-arming every suspend leg — silently, since the resulting run looks perfectly
|
|
1786
|
+
* normal right up until it parks forever. Every read of the pair goes through here so that trap has
|
|
1787
|
+
* ONE place to be wrong instead of nine.
|
|
1788
|
+
*/
|
|
314
1789
|
export declare function resolveCheckpointStore(spec: {
|
|
315
1790
|
checkpointStore?: CheckpointStore | null;
|
|
316
1791
|
}, deps: {
|
|
317
1792
|
checkpointStore?: CheckpointStore;
|
|
318
1793
|
}): CheckpointStore | undefined;
|
|
1794
|
+
/**
|
|
1795
|
+
* design/80 D-1 (persist-winner): derive the durable {@link ResolvedOutcome} winner from a resume
|
|
1796
|
+
* `outcome`, or `undefined` when the outcome binds to no pending action. A `policy_ask` resume names a
|
|
1797
|
+
* specific tool call (`boundCallId`) + decision; since RB-64 the REVIEW gates (`plan_review` /
|
|
1798
|
+
* `dry_run_review`) record their decision under a `gate:` sentinel too — the env_failed replay guard can
|
|
1799
|
+
* only compare what is recorded here. Only `resource_limit` / `task_done` bind nothing and record no
|
|
1800
|
+
* winner. Shared by every {@link CheckpointStore} impl so the persisted winner is identical across the
|
|
1801
|
+
* in-memory and durable backends.
|
|
1802
|
+
*/
|
|
319
1803
|
export declare function winnerFromOutcome(outcome: ResumeOutcome): ResolvedOutcome | undefined;
|
|
1804
|
+
/**
|
|
1805
|
+
* design/80 D-A: the persist-time validation every {@link CheckpointStore.setPendingSteer} impl runs so the
|
|
1806
|
+
* reject is IDENTICAL across the in-memory and durable backends. A `steer.text` carrying a `</system-reminder>`
|
|
1807
|
+
* close tag would escape the model-facing `<system-reminder>` wrapper a trusted steer rides on resume — reject
|
|
1808
|
+
* fail-closed (`steering.invalid_content`) so a dirty steer NEVER enters {@link CheckpointState} (§3 inv #2).
|
|
1809
|
+
* Applied for BOTH `trusted` values: the durable record must be clean on either path (the untrusted resume
|
|
1810
|
+
* path ALSO sanitizes as untrusted data, but the close tag must never reach the persisted state at all).
|
|
1811
|
+
* Returns a copy built from the FROZEN field set ({@link PENDING_STEER_FROZEN_FIELDS} minus the
|
|
1812
|
+
* store-minted `seq`), so no extra caller field leaks into the row — and, since design/171 §6.3, an
|
|
1813
|
+
* unknown key is a loud rejection rather than a silent drop: a producer one release ahead of this worker
|
|
1814
|
+
* must learn that its new field would not survive the parked leg.
|
|
1815
|
+
*/
|
|
320
1816
|
export declare function validatePendingSteer(steer: PendingSteerInput): Omit<PendingSteerEntry, "seq">;
|
|
1817
|
+
/** design/171 §6.1 — the `actor` id/issuer bound. Attribution labels ride the durable row and the model
|
|
1818
|
+
* context, so they are capped like every other persisted model-facing string on this row. */
|
|
321
1819
|
export declare const MAX_ACTOR_FIELD_CHARS = 256;
|
|
1820
|
+
/** design/171 §6.3 — bound on the caller-supplied correlation id (a service's own message id). */
|
|
322
1821
|
export declare const MAX_STEER_INPUT_ID_CHARS = 128;
|
|
1822
|
+
/** design/171 §6.1 — the frozen field set of an {@link ActorAssertion} (same fail-loud posture as the
|
|
1823
|
+
* entry's own set: an unknown key means a producer's provenance field would vanish silently). */
|
|
323
1824
|
export declare const ACTOR_ASSERTION_FROZEN_FIELDS: readonly ["id", "hostAsserted", "issuer"];
|
|
1825
|
+
/**
|
|
1826
|
+
* design/171 §5.3 — THE single read point for a checkpoint's parked steers, in delivery (`seq`) order.
|
|
1827
|
+
*
|
|
1828
|
+
* Additive read: a row written before the queue existed carries its steer in the legacy
|
|
1829
|
+
* {@link CheckpointState.pendingSteer} seat, and it is folded in as the FIRST member (it was parked
|
|
1830
|
+
* before anything in the queue could have been). Every consumer — the resume drain, the wake's
|
|
1831
|
+
* nothing-to-deliver guard, a serving layer — goes through here, so no site can grow its own idea of
|
|
1832
|
+
* what "the parked steer" is.
|
|
1833
|
+
*/
|
|
324
1834
|
export declare function readPendingSteerQueue(state: Pick<CheckpointState, "pendingSteer" | "pendingSteerQueue">): PendingSteerEntry[];
|
|
1835
|
+
/** design/171 §5.3 — the synthetic `inputId` a pre-queue single-seat steer reads back as. Stable (never
|
|
1836
|
+
* minted per read) so dedupe and re-reads behave, and reserved so a caller cannot collide with it. */
|
|
325
1837
|
export declare const LEGACY_PENDING_STEER_INPUT_ID = "legacy-single-seat";
|
|
1838
|
+
/**
|
|
1839
|
+
* design/171 §5.3/§6.3 — THE append, shared by every backend so the bounds and the ordering are one
|
|
1840
|
+
* implementation rather than one per store.
|
|
1841
|
+
*
|
|
1842
|
+
* Returns the NEW queue (the caller persists it); throws `steering.queue_full` when either bound would
|
|
1843
|
+
* be crossed. Re-appending an entry whose `inputId` is already queued is a NO-OP returning the queue
|
|
1844
|
+
* unchanged — that is what keeps `setPendingSteer` retry-safe now that it appends instead of overwriting.
|
|
1845
|
+
*/
|
|
326
1846
|
export declare function appendPendingSteer(state: Pick<CheckpointState, "pendingSteer" | "pendingSteerQueue">, entry: Omit<PendingSteerEntry, "seq">): PendingSteerEntry[];
|
|
1847
|
+
/**
|
|
1848
|
+
* REF-A4 — THE row half of every guarded checkpoint transition's CAS predicate, shared by every backend:
|
|
1849
|
+
* the row must EXIST, its `scope` must match (multi-tenant isolation, §2.1 — a wrong-scope caller never
|
|
1850
|
+
* wins), and it must currently be in `status`. `resolve` requires `"pending"`, `reopen` requires
|
|
1851
|
+
* `"resolved"` (the inverse gate), `setPendingSteer`/`expire` require `"pending"`.
|
|
1852
|
+
*
|
|
1853
|
+
* PURE: it decides nothing about persistence. Each backend keeps its own commit half — the in-memory one
|
|
1854
|
+
* flips fields in place, the file one appends+fsyncs the event BEFORE the flip (RB-150: commit ordering is
|
|
1855
|
+
* that backend's own correctness core and must never be absorbed into a shared abstraction).
|
|
1856
|
+
*/
|
|
327
1857
|
export declare function checkpointRowMatches(cp: Checkpoint | undefined, scope: string, status: Checkpoint["status"]): cp is Checkpoint;
|
|
1858
|
+
/**
|
|
1859
|
+
* REF-A4 — THE optimistic-concurrency half of `resolve`'s CAS predicate (design/80 D-1), shared by every
|
|
1860
|
+
* backend. The caller validated its reopen-by-reason guard against a `get()` snapshot; if a concurrent
|
|
1861
|
+
* resolve/reopen cycle moved the monotonic `rev` in the get→resolve window, that validation is stale ⇒
|
|
1862
|
+
* lose the CAS (fail-closed), forcing a re-get + re-validate. Keyed on `rev`, NOT on `reopenReason` — a
|
|
1863
|
+
* 2-valued enum can ABA back to a prior value while the winner changed (round-2 BLOCKER fix). An absent
|
|
1864
|
+
* expectation opts out of the guard entirely.
|
|
1865
|
+
*/
|
|
328
1866
|
export declare function checkpointOccMatches(cp: Checkpoint, expect?: ResolveExpectation): boolean;
|
|
329
|
-
|
|
1867
|
+
/** Fault-injection mode for {@link InMemoryCheckpointStore.testInjectFault} (council #5). One-shot. */
|
|
1868
|
+
export type CheckpointFaultMode =
|
|
1869
|
+
/** `resolve` commits the CAS (status → resolved) then throws — simulates a crash *after* the commit
|
|
1870
|
+
* but before the caller is acked, so a retry must see `resolved` and NOT re-execute (idempotency). */
|
|
1871
|
+
"resolve-after-commit"
|
|
1872
|
+
/** `resolve` throws *before* the CAS — simulates a crash before the commit; the row stays `pending`
|
|
1873
|
+
* so a retry can still win it. */
|
|
1874
|
+
| "resolve-before-commit";
|
|
1875
|
+
/**
|
|
1876
|
+
* Default in-process {@link CheckpointStore}. Single-instance / tests only — it does NOT survive a
|
|
1877
|
+
* restart or span replicas, so it cannot deliver the cross-process guarantee a durable backend does.
|
|
1878
|
+
* Single-threaded JS already serializes `resolve`, so the CAS is trivially atomic here;
|
|
1879
|
+
* {@link testInjectFault} simulates the crash-recovery races a real backend must survive.
|
|
1880
|
+
*/
|
|
330
1881
|
export declare class InMemoryCheckpointStore implements CheckpointStore {
|
|
1882
|
+
/** In-memory rows die with the process, but there is no scheduled tenant-scoped deletion contract
|
|
1883
|
+
* here — honestly `"none"` (a locked retention policy must refuse this store). */
|
|
331
1884
|
readonly retention: "none";
|
|
1885
|
+
/** design/173 §2.3 — honest declaration: in-memory rows die with the process. */
|
|
332
1886
|
readonly durability: "process-local";
|
|
1887
|
+
/** Honest declaration on the fidelity axis: `put`/`get` are `structuredClone`, so a row keeps the
|
|
1888
|
+
* whole structured-clone domain — a `Date` filed here is a `Date` when it comes back. */
|
|
333
1889
|
readonly fidelity: "structured-clone";
|
|
334
1890
|
private cps;
|
|
335
1891
|
private fault;
|
|
@@ -342,6 +1898,8 @@ export declare class InMemoryCheckpointStore implements CheckpointStore {
|
|
|
342
1898
|
reap(scope: string, cutoff: number): Promise<number>;
|
|
343
1899
|
listByScope(scope: string): Promise<CheckpointSummary[]>;
|
|
344
1900
|
listScopes(): Promise<string[]>;
|
|
1901
|
+
/** Arm a one-shot fault on the next `resolve` (council #5: makes crash-recovery unit-testable). */
|
|
345
1902
|
testInjectFault(mode: CheckpointFaultMode | null): void;
|
|
1903
|
+
/** Test/inspection helper: number of stored checkpoints. */
|
|
346
1904
|
get size(): number;
|
|
347
1905
|
}
|