@sema-agent/core 7.5.1 → 7.6.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.
Files changed (132) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/dist/agents/cascade.d.ts +2 -2
  3. package/dist/agents/cascade.js +12 -10
  4. package/dist/agents/repair-loop.d.ts +5 -3
  5. package/dist/agents/repair-loop.js +13 -15
  6. package/dist/agents/subagent.d.ts +24 -42
  7. package/dist/agents/subagent.js +119 -105
  8. package/dist/agents/suspend-guard.d.ts +31 -19
  9. package/dist/agents/suspend-guard.js +14 -8
  10. package/dist/agents/teacher.js +9 -9
  11. package/dist/agents/team.d.ts +4 -3
  12. package/dist/agents/team.js +10 -8
  13. package/dist/agents/verify.d.ts +3 -3
  14. package/dist/agents/verify.js +17 -17
  15. package/dist/core/a2a.js +2 -1
  16. package/dist/core/ask-origin.d.ts +60 -7
  17. package/dist/core/ask-origin.js +26 -1
  18. package/dist/core/checkpoint-store.d.ts +78 -76
  19. package/dist/core/checkpoint-store.js +17 -1
  20. package/dist/core/gate-outcome.d.ts +189 -0
  21. package/dist/core/gate-outcome.js +70 -0
  22. package/dist/core/hooks.d.ts +18 -92
  23. package/dist/core/hooks.js +88 -85
  24. package/dist/core/mcp-failure.d.ts +104 -0
  25. package/dist/core/mcp-failure.js +128 -0
  26. package/dist/core/mcp.d.ts +21 -77
  27. package/dist/core/mcp.js +76 -150
  28. package/dist/core/pause-registry.d.ts +131 -0
  29. package/dist/core/pause-registry.js +27 -0
  30. package/dist/core/protocol-table.d.ts +5 -0
  31. package/dist/core/protocol-table.js +1 -0
  32. package/dist/core/runner/abort-race.d.ts +41 -0
  33. package/dist/core/runner/abort-race.js +38 -0
  34. package/dist/core/runner/assemble-result.d.ts +32 -41
  35. package/dist/core/runner/assemble-result.js +55 -74
  36. package/dist/core/runner/checkpoint-scope.d.ts +15 -3
  37. package/dist/core/runner/checkpoint-scope.js +3 -0
  38. package/dist/core/runner/compaction-call-options.d.ts +1 -1
  39. package/dist/core/runner/content-ask-bindings.d.ts +27 -0
  40. package/dist/core/runner/content-ask-bindings.js +1 -0
  41. package/dist/core/runner/contracts.d.ts +144 -50
  42. package/dist/core/runner/denial-limit-arms.d.ts +24 -1
  43. package/dist/core/runner/denial-limit-arms.js +24 -3
  44. package/dist/core/runner/gate-exit.d.ts +74 -0
  45. package/dist/core/runner/gate-exit.js +55 -0
  46. package/dist/core/runner/inherited-ask-grants.d.ts +46 -0
  47. package/dist/core/runner/inherited-ask-grants.js +29 -0
  48. package/dist/core/runner/park-commit.d.ts +102 -0
  49. package/dist/core/runner/park-commit.js +31 -0
  50. package/dist/core/runner/{prepare-permission-rules.d.ts → permission-rule-lanes.d.ts} +109 -3
  51. package/dist/core/runner/{prepare-permission-rules.js → permission-rule-lanes.js} +47 -1
  52. package/dist/core/runner/prepare-ask-lane.d.ts +107 -0
  53. package/dist/core/runner/prepare-ask-lane.js +131 -0
  54. package/dist/core/runner/prepare-boundary-parks.d.ts +102 -0
  55. package/dist/core/runner/prepare-boundary-parks.js +169 -0
  56. package/dist/core/runner/prepare-caps-and-workflow.js +1 -1
  57. package/dist/core/runner/prepare-context-lane.d.ts +119 -0
  58. package/dist/core/runner/prepare-context-lane.js +230 -0
  59. package/dist/core/runner/prepare-gate-stations.d.ts +174 -0
  60. package/dist/core/runner/prepare-gate-stations.js +265 -0
  61. package/dist/core/runner/prepare-hands-readface.d.ts +4 -4
  62. package/dist/core/runner/prepare-inherited-gate.d.ts +4 -4
  63. package/dist/core/runner/prepare-inherited-gate.js +1 -1
  64. package/dist/core/runner/prepare-memory-engine-session.d.ts +84 -0
  65. package/dist/core/runner/prepare-memory-engine-session.js +233 -0
  66. package/dist/core/runner/prepare-memory.d.ts +44 -26
  67. package/dist/core/runner/prepare-park-ask.d.ts +162 -0
  68. package/dist/core/runner/prepare-park-ask.js +377 -0
  69. package/dist/core/runner/prepare-policy-chain.d.ts +208 -0
  70. package/dist/core/runner/prepare-policy-chain.js +584 -0
  71. package/dist/core/runner/prepare-project-context.d.ts +1 -13
  72. package/dist/core/runner/prepare-project-context.js +1 -3
  73. package/dist/core/runner/prepare-prompt-assembly.d.ts +95 -0
  74. package/dist/core/runner/prepare-prompt-assembly.js +162 -0
  75. package/dist/core/runner/prepare-prompt-inputs.d.ts +1 -20
  76. package/dist/core/runner/prepare-protocol-tools.d.ts +3 -3
  77. package/dist/core/runner/prepare-protocol-tools.js +0 -3
  78. package/dist/core/runner/prepare-question-face.d.ts +3 -21
  79. package/dist/core/runner/prepare-question-face.js +2 -1
  80. package/dist/core/runner/prepare-safety-scan.d.ts +0 -5
  81. package/dist/core/runner/prepare-safety-scan.js +1 -2
  82. package/dist/core/runner/prepare-suspend-saga.d.ts +170 -0
  83. package/dist/core/runner/prepare-suspend-saga.js +308 -0
  84. package/dist/core/runner/prepare-task.d.ts +9 -136
  85. package/dist/core/runner/prepare-task.js +47 -2745
  86. package/dist/core/runner/prepare-turn-wiring.d.ts +154 -0
  87. package/dist/core/runner/prepare-turn-wiring.js +201 -0
  88. package/dist/core/runner/prepare-wiring-manifest.d.ts +17 -17
  89. package/dist/core/runner/prepare-wiring-manifest.js +16 -10
  90. package/dist/core/runner/prepare-workspace-restore.d.ts +2 -29
  91. package/dist/core/runner/prepare-workspace-restore.js +3 -16
  92. package/dist/core/runner/prompt-hash-salt.d.ts +1 -0
  93. package/dist/core/runner/prompt-hash-salt.js +2 -0
  94. package/dist/core/runner/remote-env-retry.d.ts +29 -0
  95. package/dist/core/runner/remote-env-retry.js +16 -0
  96. package/dist/core/runner/runtask.d.ts +16 -31
  97. package/dist/core/runner/runtask.js +110 -121
  98. package/dist/core/runner/terminal-projection.d.ts +22 -0
  99. package/dist/core/runner/terminal-projection.js +28 -0
  100. package/dist/core/session.d.ts +12 -0
  101. package/dist/core/session.js +3 -0
  102. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +4 -1
  103. package/dist/core/store-contracts/checkpoint-store-contract.js +8 -2
  104. package/dist/core/terminal-cause.d.ts +137 -0
  105. package/dist/core/terminal-cause.js +9 -0
  106. package/dist/core/tool-policy.d.ts +43 -139
  107. package/dist/core/tool-policy.js +79 -112
  108. package/dist/core/types.d.ts +67 -164
  109. package/dist/core/wiring-manifest.d.ts +6 -3
  110. package/dist/core/workflow-journal-store.js +3 -4
  111. package/dist/engine/harness/agent-harness.d.ts +1 -1
  112. package/dist/index.d.ts +10 -7
  113. package/dist/index.js +8 -5
  114. package/dist/orchestration/builtin-workflows.d.ts +2 -2
  115. package/dist/orchestration/builtin-workflows.js +1 -1
  116. package/dist/orchestration/goal.js +8 -7
  117. package/dist/orchestration/run-spec.js +5 -3
  118. package/dist/orchestration/run-workflow-tool.d.ts +1 -1
  119. package/dist/orchestration/run-workflow-tool.js +4 -4
  120. package/dist/orchestration/workflow-governance.d.ts +4 -4
  121. package/dist/orchestration/workflow-governance.js +4 -2
  122. package/dist/orchestration/workflow-primitives.d.ts +1 -1
  123. package/dist/orchestration/workflow-primitives.js +1 -1
  124. package/dist/orchestration/workflow.d.ts +11 -0
  125. package/dist/orchestration/workflow.js +64 -39
  126. package/dist/prompts/supervisor.d.ts +1 -1
  127. package/dist/prompts/supervisor.js +3 -3
  128. package/dist/scenarios/scenario-registry.js +1 -1
  129. package/package.json +3 -1
  130. package/test/export-surface.snapshot.json +74 -22
  131. /package/dist/core/runner/{prepare-announce-once.d.ts → announce-once-ledger.d.ts} +0 -0
  132. /package/dist/core/runner/{prepare-announce-once.js → announce-once-ledger.js} +0 -0
@@ -0,0 +1,22 @@
1
+ import type { CheckpointGate, CheckpointToken } from "../checkpoint-store.js";
2
+ import type { TaskResult, TaskStatus, TerminalCause } from "../types.js";
3
+ /** The plane words a {@link TerminalCause} reads as — one derivation, called by the faces that speak
4
+ * them. The paused arm's `status` is its gate kind's registry row (`suspended` for an approval,
5
+ * a resource slice or a platform pause; `needs_review` for a plan or dry-run review, which also
6
+ * carries the `review.pending` code the review consumers key on). */
7
+ export declare function terminalProjection(terminal: TerminalCause): {
8
+ status: TaskStatus;
9
+ errorCode?: string;
10
+ errorMessage?: string;
11
+ blockedReason?: string;
12
+ checkpointToken?: CheckpointToken;
13
+ checkpointId?: string;
14
+ checkpointGate?: CheckpointGate;
15
+ workspaceRestoreMode?: "snapshot" | "park_only";
16
+ };
17
+ /**
18
+ * A compensation that changes WHY a settled result ended (a resume leg's approval reopened / could not
19
+ * be reopened) amends the CAUSE — the one record there is. The code fills only an empty seat (the leg's
20
+ * own terminal code, when it had one, still names the ending); the message is extended, not replaced.
21
+ */
22
+ export declare function amendTerminal(result: TaskResult, code: string, message: (prior: string | undefined) => string): void;
@@ -0,0 +1,28 @@
1
+ import { PAUSE_REGISTRY } from "../pause-registry.js";
2
+ export function terminalProjection(terminal) {
3
+ switch (terminal.kind) {
4
+ case "completed":
5
+ return { status: "completed" };
6
+ case "failed":
7
+ return { status: "failed", errorCode: terminal.code, errorMessage: terminal.message };
8
+ case "blocked":
9
+ return { status: "blocked", blockedReason: terminal.reason };
10
+ case "paused": {
11
+ const status = PAUSE_REGISTRY[terminal.gate.kind].taskStatus;
12
+ return {
13
+ status,
14
+ ...(status === "needs_review" ? { errorCode: "review.pending" } : {}),
15
+ checkpointToken: terminal.token,
16
+ checkpointId: terminal.checkpointId,
17
+ checkpointGate: terminal.gate,
18
+ workspaceRestoreMode: terminal.restoreMode,
19
+ };
20
+ }
21
+ }
22
+ }
23
+ export function amendTerminal(result, code, message) {
24
+ const t = result.terminal;
25
+ if (t.kind !== "failed")
26
+ return;
27
+ result.terminal = { ...t, code: t.code ?? code, message: message(t.message) };
28
+ }
@@ -312,3 +312,15 @@ export declare function boundedTail(pathEntries: readonly SessionTreeEntry[]): {
312
312
  tail: SessionTreeEntry[];
313
313
  floorEntryId: string | null;
314
314
  };
315
+ /**
316
+ * The ONE "has this session seen conversation?" predicate — the freshness question is asked three
317
+ * times in one prepare (project-memory phase, design/148 center adoption, the epoch-pin
318
+ * discriminator), and an entry-COUNT proxy at ANY of them re-creates the same lie: a host may write
319
+ * session_info / model_change / … rows before the first turn (naming a new chat, recording a model
320
+ * pick), and none of that is evidence of a prior session (5.29 merge-rescan: the count proxy
321
+ * survived at the adoption arm after the pin arm was fixed, silently costing a pre-named session
322
+ * its published center prompt for life). Conversation content — and only that — is the evidence.
323
+ */
324
+ export declare function hasConversationContent(branch: ReadonlyArray<{
325
+ type: string;
326
+ }>): boolean;
@@ -24,3 +24,6 @@ export function boundedTail(pathEntries) {
24
24
  }
25
25
  return { tail: pathEntries.slice(floorIdx), floorEntryId };
26
26
  }
27
+ export function hasConversationContent(branch) {
28
+ return branch.some((e) => e.type === "message" || e.type === "custom_message" || e.type === "compaction");
29
+ }
@@ -1,4 +1,5 @@
1
1
  import { type Checkpoint, type CheckpointStore, type ResumeOutcome } from "../checkpoint-store.js";
2
+ import { type CheckpointFields } from "../pause-registry.js";
2
3
  import { type ContractAssertionRunner } from "./contract-harness.js";
3
4
  /**
4
5
  * design/159 S1 — the cross-backend {@link CheckpointStore} contract, extracted VERBATIM from
@@ -14,7 +15,7 @@ import { type ContractAssertionRunner } from "./contract-harness.js";
14
15
  */
15
16
  /** Build a {@link Checkpoint} fixture. `createdAt` is FIXED (not wall-clock) so summary projections
16
17
  * are deterministic and cross-backend byte-comparable; override via `over` for ordering tests. */
17
- export declare function createCheckpointFixture(over?: Partial<Checkpoint>): Checkpoint;
18
+ export declare function createCheckpointFixture(over?: Partial<CheckpointFields>): Checkpoint;
18
19
  /** The canonical `policy_ask allow` outcome the contract resolves with. Declared as the NARROW
19
20
  * `policy_ask` arm — not the whole union — so field access and spread call sites need no
20
21
  * re-assertion (RB-396-d: the wide declaration forced an `as Extract<…>` + a same-value
@@ -45,6 +46,7 @@ export declare const EXPECTED_LISTBYSCOPE_SUMMARIES: ({
45
46
  toolCallId: string;
46
47
  toolName: string;
47
48
  toolInput: string;
49
+ origin: string;
48
50
  spentMicroUsd?: undefined;
49
51
  } | {
50
52
  token: string;
@@ -60,4 +62,5 @@ export declare const EXPECTED_LISTBYSCOPE_SUMMARIES: ({
60
62
  toolCallId?: undefined;
61
63
  toolName?: undefined;
62
64
  toolInput?: undefined;
65
+ origin?: undefined;
63
66
  })[];
@@ -1,9 +1,10 @@
1
1
  import { strict as assert } from "node:assert";
2
2
  import { MAX_PENDING_STEER_ENTRIES, mintCheckpointToken, readPendingSteerQueue, } from "../checkpoint-store.js";
3
+ import { checkpointFrom } from "../pause-registry.js";
3
4
  import { beginContract } from "./contract-harness.js";
4
5
  export function createCheckpointFixture(over = {}) {
5
6
  const token = over.token ?? mintCheckpointToken();
6
- return {
7
+ return checkpointFrom({
7
8
  token,
8
9
  scope: "tenant-a",
9
10
  sessionId: "sess-1",
@@ -11,6 +12,7 @@ export function createCheckpointFixture(over = {}) {
11
12
  gate: { kind: "human", reason: "approve", toolName: "Write" },
12
13
  pendingAction: {
13
14
  kind: "tool_approval",
15
+ origin: "policy",
14
16
  toolCallId: "call-3",
15
17
  toolName: "Write",
16
18
  args: { path: "/x", content: "y" },
@@ -25,13 +27,14 @@ export function createCheckpointFixture(over = {}) {
25
27
  status: "pending",
26
28
  createdAt: 1_700_000_000_000,
27
29
  ...over,
28
- };
30
+ });
29
31
  }
30
32
  export const ALLOW = {
31
33
  gate: "policy_ask",
32
34
  decision: "allow",
33
35
  boundCallId: "call-3",
34
36
  boundInputHash: "h0",
37
+ hostDecision: { decidedBy: "person" },
35
38
  };
36
39
  export async function checkpointStoreContract(make, runAssertion) {
37
40
  const { run, settle } = beginContract(runAssertion);
@@ -193,6 +196,7 @@ export async function checkpointStoreContract(make, runAssertion) {
193
196
  sessionId: "bidi-bitless",
194
197
  pendingAction: {
195
198
  kind: "tool_approval",
199
+ origin: "policy",
196
200
  toolCallId: "call-3",
197
201
  toolName: "Write",
198
202
  args: { path: `/x/${RLO}txt.exe` },
@@ -254,6 +258,7 @@ export async function checkpointListByScopeSummaries(store) {
254
258
  },
255
259
  pendingAction: {
256
260
  kind: "tool_approval",
261
+ origin: "policy",
257
262
  toolCallId: "call-pr",
258
263
  toolName: "open_pr",
259
264
  args: { repo: "x" },
@@ -293,6 +298,7 @@ export const EXPECTED_LISTBYSCOPE_SUMMARIES = [
293
298
  toolCallId: "call-pr",
294
299
  toolName: "open_pr",
295
300
  toolInput: '{"repo":"x"}',
301
+ origin: "policy",
296
302
  },
297
303
  {
298
304
  token: "tok-resource",
@@ -0,0 +1,137 @@
1
+ /**
2
+ * The terminal cause — the ONE record of how a run ended (`TaskResult.terminal`), its closed kind set,
3
+ * the one disposition table over that set (which cause a resume journal may replay) and the admission
4
+ * read a journal entry's cause passes through. Layer 0 vocabulary, beside `gate-outcome.ts` (the record
5
+ * of one gate pass) and `pause-registry.ts` (the pause gate × pending-action pairing a paused cause
6
+ * points at): no runtime import above this layer, so any module may read the words.
7
+ *
8
+ * The plane-word projection a face that still speaks `status` needs lives in
9
+ * `runner/terminal-projection.ts` — the one cause→plane derivation.
10
+ */
11
+ import type { AssertAllKeysHandled } from "./ask-origin.js";
12
+ /**
13
+ * WHY a run ended — ONE tagged cause, and the ONLY terminal record a {@link TaskResult} carries. The run
14
+ * loop's terminal arbitration used to decide a `status` word plus seven optional plane fields one by one
15
+ * across an if/else-if ladder over competing optional flags, and the legal combinations
16
+ * ("`status:"suspended"` ⇔ a token is present", "a review pause and an approval pause are never both set")
17
+ * lived only in that ladder's order. Here they are the SHAPE: a paused run carries its token; a completed
18
+ * run cannot. Branch on `kind`; the five-word {@link TaskStatus} a cause reads as (a `paused` cause reads
19
+ * its gate kind's registry row) is derived by {@link import("./runner/terminal-projection.js").terminalProjection},
20
+ * the one cause→plane derivation, for the faces that still speak the word.
21
+ * - `completed` — the run finished (a person's clean halt included — see `TaskResult.haltedByUser`).
22
+ * - `failed` — a limit, a provider failure, an abort, an invalid output…
23
+ * - `blocked` — the agent could not finish and said why.
24
+ * - `paused` — a durable pause committed a checkpoint and the run is resumable.
25
+ * A `GateOutcome` is the record of ONE gate pass (per call); a `TerminalCause` is the record of ONE run
26
+ * (per leg); a paused run's `gate` and the park row's `gate.kind` are the same word, joined by the token.
27
+ */
28
+ export type TerminalCause = {
29
+ kind: "completed";
30
+ } | {
31
+ kind: "failed";
32
+ /**
33
+ * Machine-readable failure code when the failure carried one — e.g. a `SessionError.code` such as
34
+ * `"conflict"` (a cross-instance write lost the optimistic lock), or a Node error code. Lets a
35
+ * caller branch programmatically (e.g. evict a stale cache + retry on `"conflict"`) instead of
36
+ * matching `message` strings. Absent only for the failures that never had one (an abort with no
37
+ * limit axis, no assistant message produced).
38
+ *
39
+ * Terminal codes use a **dotted namespace** so a caller can prefix-match a whole class:
40
+ * `"limits.max_tokens_exceeded"` / `"limits.max_cost_exceeded"` / `"limits.max_turns_exceeded"` /
41
+ * `"limits.max_walltime_exceeded"` / `"config.limit_invalid"` / `"config.limit_unknown_key"` /
42
+ * `"config.attachment_invalid"` (a mode-valued `TaskSpec.attachments` member outside its closed
43
+ * set, refused at the same door as the limits) (e.g. `code.startsWith("limits.")`). Brain codes
44
+ * (`auth`/`network`/`rate_limit`/…) and `"conflict"` remain flat.
45
+ *
46
+ * Two codes name the EXTERNAL stop causes — neither is a `limits.` code, because neither is an
47
+ * allowance the task chose, and a caller that retries on `limits.*` should NOT treat these the same:
48
+ * - `"env.lifetime_expired"` — the execution environment's platform lifetime ran out and the run
49
+ * could not be suspended durably. Retrying needs a NEW environment, not a smaller budget.
50
+ * - `"usage.window_exhausted"` — a deployment usage-governance window is full (see
51
+ * `RunnerDeps.usageWindows`). Reported at task ENTRY (nothing ran) or after a suspend was
52
+ * impossible; the wait hint rides the thrown error's `retryAfterMs` (delivered through
53
+ * `RunnerDeps.onError`) and the message text. Retrying before the window frees is refused again.
54
+ * Their config-time siblings are `"config.env_lifetime_invalid"` / `"config.usage_window_invalid"`.
55
+ * A governance window with a MONEY ceiling adds two more, both of which say "the ceiling could not
56
+ * be evaluated" rather than "the ceiling was reached" — neither is retryable without a config change:
57
+ * - `"config.usage_window_unpriced"` — a `UsageWindow.maxCostUsd` over a run with no cost figure
58
+ * (no `RunnerDeps.pricing` entry and no `Model.cost`).
59
+ * - `"usage_window.store_cost_unanswered"` — the wired ledger does not carry the money arm.
60
+ * A NESTED orchestrator that met a durable pause it cannot drive stamps `"unexpected.suspended"` /
61
+ * `"unexpected.needs_review"` and carries the pause on {@link nestedPause}.
62
+ */
63
+ code?: string;
64
+ /** Human-readable failure text. */
65
+ message?: string;
66
+ /**
67
+ * The durable pause this failure STANDS IN FOR — set only by the nested hard boundary
68
+ * (`mapNestedSuspend`): an orchestrator's nested leg paused on a durable gate, the orchestrator cannot
69
+ * drive a resume from the inside, so at ITS boundary the pause is a failure — but the pause's own
70
+ * cause travels here verbatim so the top-level caller keeps the recovery capability
71
+ * (`nestedPause.token` → `runner.resume`). Absent on every failure the engine itself assembles.
72
+ */
73
+ nestedPause?: PausedCause;
74
+ } | {
75
+ kind: "blocked";
76
+ /** Why the agent could not finish. */
77
+ reason: string;
78
+ } | {
79
+ kind: "paused";
80
+ /**
81
+ * WHICH pause — who/what must resume (e.g. `{kind:"human", reason, toolName}`), so the caller knows
82
+ * what decision the checkpoint awaits and which resume outcome it takes (a `policy_ask` for an
83
+ * approval gate; a `dry_run_review` on a `needs_review` gate; a `plan_review` on a `plan_review`
84
+ * gate). Its kind's {@link import("./pause-registry.js").PAUSE_REGISTRY} row is what
85
+ * {@link import("./runner/terminal-projection.js").terminalProjection} reads the status word from
86
+ * (`"suspended"` for an approval / a resource slice / a platform pause, `"needs_review"` for a review).
87
+ */
88
+ gate: import("./checkpoint-store.js").CheckpointGate;
89
+ /**
90
+ * The durable-checkpoint token to resume this task with via `runner.resume(token, outcome)`.
91
+ * Branded `CheckpointToken`; **never log it or put it in a URL** (it is the resume capability, §6).
92
+ */
93
+ token: import("./checkpoint-store.js").CheckpointToken;
94
+ /**
95
+ * The pause's NON-SECRET stable identity ({@link import("./checkpoint-store.js").Checkpoint.checkpointId})
96
+ * — the display/correlation key a consumer may log or render where the token must not travel.
97
+ * Absent on pre-identity checkpoints.
98
+ */
99
+ checkpointId?: string;
100
+ /**
101
+ * HOW the paused task's remote workspace will come back, present whenever the pause captured a
102
+ * remote workspace. The two modes bill and behave very differently:
103
+ * - `"snapshot"` — the workspace VM was `suspendVM`-paused into a snapshot. Provider billing
104
+ * typically stops; in-memory process state is captured; resume restores it.
105
+ * - `"park_only"` — the env declared itself non-suspendable (an SSH host / an ADB device), so
106
+ * NOTHING was paused: the machine keeps running (and keeps costing), any in-memory process state
107
+ * is at the target's mercy, and resume simply reconnects to the still-present workspace.
108
+ * Deliberate and honest (core does not fabricate a snapshot) — but a scheduler that assumes
109
+ * "paused ⇒ idle and free" must be able to see it.
110
+ * ABSENT for a process-local pause (a static caller-owned env: no remote workspace was captured).
111
+ * The same discriminant rides {@link import("./checkpoint-store.js").CheckpointSummary} for the
112
+ * inbox/`listByScope` face.
113
+ */
114
+ restoreMode?: "snapshot" | "park_only";
115
+ };
116
+ /** The paused arm alone — the shape the run's committed-pause holder carries. */
117
+ /**
118
+ * The one disposition table over `TerminalCause["kind"]`: which causes a resume journal may REPLAY
119
+ * instead of re-running. Only a completed cause is a finished leg; every other cause means the leg has
120
+ * work left (or failed) and a resume runs it live. The table is also the admission list for a journal
121
+ * entry's cause at load: a kind absent here (a journal written before `TaskResult.terminal`, a corrupted
122
+ * or version-skewed entry) is refused before anything is dispatched — it is never read as "not
123
+ * completed, so run it again", which would replay the side effects of finished work.
124
+ */
125
+ export declare const TERMINAL_CAUSE_IS_REPLAYABLE: {
126
+ readonly completed: true;
127
+ readonly failed: false;
128
+ readonly blocked: false;
129
+ readonly paused: false;
130
+ };
131
+ /** Compile-time fence: every `TerminalCause` kind has a row in the replay table (and, via `satisfies`, only those). */
132
+ export type ReplayTableCoversEveryTerminalCause = AssertAllKeysHandled<Exclude<TerminalCause["kind"], keyof typeof TERMINAL_CAUSE_IS_REPLAYABLE>>;
133
+ /** The admission read for a journaled cause: a closed-set member, by table row. */
134
+ export declare function isTerminalCauseKind(kind: unknown): kind is TerminalCause["kind"];
135
+ export type PausedCause = Extract<TerminalCause, {
136
+ kind: "paused";
137
+ }>;
@@ -0,0 +1,9 @@
1
+ export const TERMINAL_CAUSE_IS_REPLAYABLE = {
2
+ completed: true,
3
+ failed: false,
4
+ blocked: false,
5
+ paused: false,
6
+ };
7
+ export function isTerminalCauseKind(kind) {
8
+ return typeof kind === "string" && Object.hasOwn(TERMINAL_CAUSE_IS_REPLAYABLE, kind);
9
+ }
@@ -1,3 +1,5 @@
1
+ import type { AskOrigin } from "./ask-origin.js";
2
+ import type { Settlement } from "./gate-outcome.js";
1
3
  /** A tool call presented to a policy before it executes. */
2
4
  export interface ToolCallRequest {
3
5
  toolName: string;
@@ -80,55 +82,17 @@ export interface ToolCallRequest {
80
82
  * spelling this word is minted from). */
81
83
  declare const DECISION_REASONS: readonly ["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable"];
82
84
  export type DecisionReason = (typeof DECISION_REASONS)[number];
83
- /**
84
- * WHO (or what) ENDED an approval the machine-readable twin of a settlement's human-readable text,
85
- * so a consumer tells "a person decided this" from "nobody answered" without prose-matching a sentence.
86
- * The sibling axis of {@link DecisionReason}, which says which LAYER produced a verdict; this one says
87
- * what ended the WAIT.
88
- *
89
- * - `"human"` — a person, or the approver acting for one, returned a final verdict (allow or deny).
90
- * - `"timeout"` — an approval window elapsed with no answer.
91
- * - `"aborted"` — every other NON-HUMAN end: the task aborted, the approver threw or reported nobody
92
- * reachable, the decision arrived out of contract, a store or transport gave way, retries ran out.
93
- *
94
- * **Which windows `"timeout"` speaks for** (#114①, 2026-08-09 — the promise this note used to make was
95
- * wider than the code): the engine stamps it at the waits IT owns — `createApprovalPolicy`'s
96
- * `approvalTimeoutMs` window, the durable park's TTL, and (#548) the denial-limit fallback ask's
97
- * auto-deny window. The SYNCHRONOUS `onAsk` leg is otherwise not one of them: there the deployment owns
98
- * the window (the engine starts no timer for a callback it does not schedule), so an unanswered card and
99
- * a refused one arrive as the same `false` and the engine records
100
- * `"human"` rather than inventing a cause it did not observe. A host that DOES time its own card out
101
- * can say so — {@link AskOutcome}'s object arm carries an optional `settledBy` for exactly this — but a
102
- * host that does not is indistinguishable, by construction. Read an absent `"timeout"` as "no window
103
- * the engine owns elapsed", never as "nobody's window elapsed".
104
- *
105
- * **The one `onAsk` exception** (#548): a classifier DENIAL-LIMIT fallback ask — the ask that carries
106
- * `denialLimitFallback`, minted when the auto-mode classifier reaches its consecutive/total bound —
107
- * IS timed by the engine over the synchronous leg, because that ask exists to bound a classifier that
108
- * would otherwise deny without end, and an unbounded wait would only move the "without end" onto the
109
- * person. Its window elapsing produces an engine-stamped `settledBy: "timeout"` with
110
- * `resolution: "window_expired"` and `autoDenied: true` on the deny — that last bit is the
111
- * discriminator between core's window and a host self-report ({@link AskOutcome}'s object arm has no
112
- * `autoDenied` seat, so a host cannot claim the word).
113
- * `autoDenyAfterMs: 0` (or an ask with no fallback member) arms nothing, which is every other ask.
114
- *
115
- * The three words are exhaustive and mutually exclusive over the ways an approval can end, and the
116
- * minimum discrimination a consumer needs — someone refused vs nobody answered — is `"human"` vs the
117
- * other two.
118
- *
119
- * **Fill it AT the settlement site.** Each site knows its own cause and names it; deriving the value
120
- * afterwards from whatever outcome happens to be at hand is exactly how a window that elapsed comes to
121
- * be reported as a person's refusal.
122
- *
123
- * ABSENT means this settlement named no source: an older caller that predates the field, or a verdict
124
- * that settled no wait at all (a policy POSTURE — headless auto-deny, a blanket circumvention — where nobody
125
- * was ever asked). A consumer MUST NOT read a semantic out of the absence; render the text instead.
126
- */
127
- export type ApprovalSettledBy = "human" | "timeout" | "aborted";
128
- /** The closed set above, for runtime domain checks at the seams that accept a caller-supplied value. */
129
- export declare const APPROVAL_SETTLED_BY_VALUES: readonly ApprovalSettledBy[];
130
- /** True iff `v` is one of the three {@link ApprovalSettledBy} words. */
131
- export declare function isApprovalSettledBy(v: unknown): v is ApprovalSettledBy;
85
+ /** Read the engine-attested settlement off a funneled decision FOR the named call (the gate's exit is the
86
+ * one consumer): an attestation bound to a different call is a replayed object and answers absence.
87
+ * Exported for the gate module only deliberately NOT re-exported from `src/index.ts` (an internal seam
88
+ * between engine modules, not a facility deployments call). */
89
+ export declare function engineSettlementOf(d: unknown, call: {
90
+ toolCallId: string;
91
+ toolName: string;
92
+ }): {
93
+ settlement: Settlement;
94
+ origin: AskOrigin;
95
+ } | undefined;
132
96
  /**
133
97
  * design/252 G-7 — how long an approver-attribution identifier may be.
134
98
  *
@@ -176,10 +140,10 @@ export declare function screenApproverAttribution(v: unknown): {
176
140
  * - `deny` blocks the call.
177
141
  * Human-readable text is `message`; read it via {@link decisionText}.
178
142
  *
179
- * The two TERMINAL arms may also carry {@link ApprovalSettledBy} as `settledBy` what ended the wait,
180
- * when this verdict ended one. It is on `allow`/`deny` and deliberately NOT on `ask`: an `ask` has
181
- * settled nothing yet, so a source there would name a decider who has not decided. A layer that never
182
- * waited for anyone omits it (see the type's ABSENT clause).
143
+ * NO arm carries a settlement: what ended a wait is not a policy's to state. The engine's own settlement
144
+ * sites attest it through a module-private seat ({@link engineSettlementOf}) and the gate's exit mints the
145
+ * one settlement record ({@link import("./gate-outcome.js").GateOutcome}); a policy that puts a `settledBy`
146
+ * or `approver` member on its verdict is putting an unread property on it.
183
147
  * RB-479-B① (ruled 2026-08-02): the legacy `reason` field is REMOVED — one name for one thing.
184
148
  *
185
149
  * How far the TYPE carries that retirement (measured 2026-08-03, correcting this note's earlier claim
@@ -200,8 +164,6 @@ export type PermissionResult = {
200
164
  updatedInput?: unknown;
201
165
  message?: string;
202
166
  decisionReason?: DecisionReason;
203
- settledBy?: Extract<ApprovalSettledBy, "human">;
204
- approver?: string;
205
167
  } | {
206
168
  action: "ask";
207
169
  updatedInput?: unknown;
@@ -297,8 +259,6 @@ export type PermissionResult = {
297
259
  updatedInput?: unknown;
298
260
  message?: string;
299
261
  decisionReason?: DecisionReason;
300
- settledBy?: ApprovalSettledBy;
301
- approver?: string;
302
262
  };
303
263
  /**
304
264
  * design/252 G-2 — WHY a piece of rule-provenance evidence is not on an ask.
@@ -1204,7 +1164,7 @@ export interface AskRequest {
1204
1164
  * {@link PermissionResult} ask-arm member of the same name): the counts that tripped the bound and
1205
1165
  * this ask's own auto-deny window. Two readers: a card renders it as a countdown; {@link resolveAsk}
1206
1166
  * arms its deadline from `autoDenyAfterMs` (> 0 ⇒ an unanswered function approver auto-denies at
1207
- * that deadline — `settledBy:"timeout"`, `resolution:"window_expired"`, `autoDenied:true`). Filled
1167
+ * that deadline — the `denial_limit_window_expired` settlement on the gate record). Filled
1208
1168
  * by the gate from the decision, never a caller/worker-settable field. This is the ARMED type: the
1209
1169
  * decision's member carries the counts alone, and the only two ways to reach this one are the route
1210
1170
  * stations — `AutoModeDenialTracker.armTimedWindow` (a hand-out to a live function approver, the one
@@ -1349,9 +1309,8 @@ export type OnAsk = "deny" | "allow" | ((req: AskRequest, signal?: AbortSignal)
1349
1309
  * the fallback exists to bound a classifier that would otherwise deny without end. When it elapses
1350
1310
  * the approver's promise is DETACHED (design/384: the wait is released, not cancelled — an approver
1351
1311
  * that answers afterwards is not consulted, and an `allow` it returns late becomes the
1352
- * `task.late_approval` notice), and the engine's own deny carries `settledBy: "timeout"`,
1353
- * `resolution: "window_expired"` and `autoDenied: true`. See {@link ApprovalSettledBy} for how a
1354
- * consumer tells that window from the host's.
1312
+ * `task.late_approval` notice), and the engine's own deny settles as `denial_limit_window_expired`
1313
+ * (the host's own elapsed window settles as `approval_window_expired` with `who.party: "host"`).
1355
1314
  *
1356
1315
  * `reason` — the SYNCHRONOUS leg's seat for a model-readable reason attached to a deny, the exact
1357
1316
  * counterpart of the durable leg's `ResumeOutcome` `policy_ask` `reason` ("Model-readable reason
@@ -1387,7 +1346,7 @@ export type OnAsk = "deny" | "allow" | ((req: AskRequest, signal?: AbortSignal)
1387
1346
  export type AskOutcome = boolean | "unavailable" | {
1388
1347
  allow: boolean;
1389
1348
  updatedInput?: unknown;
1390
- settledBy?: Extract<ApprovalSettledBy, "human" | "timeout">;
1349
+ settledBy?: "human" | "timeout";
1391
1350
  reason?: string;
1392
1351
  approver?: string;
1393
1352
  };
@@ -1473,90 +1432,35 @@ export declare function tryCloneArgs<T>(v: T): {
1473
1432
  * text they let through. Not part of the package's public surface.
1474
1433
  */
1475
1434
  export declare function describeThrown(err: unknown): string;
1476
- /**
1477
- * The deny-arm classification a {@link resolveAsk} refusal carries — MINTED at the composing arm
1478
- * (the minter reports the fact; no consumer re-derives it from message text, which is exactly the
1479
- * inference this closed set exists to end). One word per family of arms:
1480
- * - `"human_refused"` — a person answered no (the boolean false fold and the object arm's
1481
- * allow-false, noted or bare — the note fact rides {@link ResolvedAsk.humanRefusalNote});
1482
- * - `"window_expired"` — the approver's own window elapsed (the timeout-settled deny);
1483
- * - `"no_approver"` — headless auto-deny (no approver wired, or the deny posture string);
1484
- * - `"blanket_allow_refused"` — a blanket allow posture met a `requiresRealApproval` ask;
1485
- * - `"approver_unavailable"` — the approver answered the ROUTING question "nobody reachable"
1486
- * (the G1 marker's fail-closed carry — the gate may re-route it to a durable park instead);
1487
- * - `"task_aborted"` — the wait's abort signal ended it (pre-wait, mid-wait and race arms). The
1488
- * signal is the run's own end AND, since design/384, any turn-level interrupt composed into the
1489
- * wait (a bare user halt, a steer-now boundary cut): one abort family, one word — a consumer
1490
- * that must tell the sources apart reads the run's own terminal facts, not this classification;
1491
- * - `"presentation_failed"` — the args/edit could not be safely presented or adopted (unclonable);
1492
- * - `"approver_error"` — the approver callback threw;
1493
- * - `"approver_contract"` — the approver returned something outside the contract (non-boolean
1494
- * allow, out-of-vocabulary settlement word, unreadable members, a timeout-settled allow, a
1495
- * non-string or unreadable reason, an out-of-contract truthy, a refused attribution).
1496
- */
1497
- export type AskDenyResolution = "human_refused" | "window_expired" | "no_approver" | "blanket_allow_refused" | "approver_unavailable" | "task_aborted" | "presentation_failed" | "approver_error" | "approver_contract";
1498
- /** The closed set above, for runtime domain checks at the seams that accept a caller-supplied value
1499
- * (the `APPROVAL_SETTLED_BY_VALUES` precedent: the word crosses process boundaries on `tool_end`,
1500
- * so a consumer enumerating or validating it must not hand-roll the vocabulary). */
1501
- export declare const ASK_DENY_RESOLUTION_VALUES: readonly AskDenyResolution[];
1502
- /** Closed-vocabulary guard for {@link AskDenyResolution} — the screen every carrier runs before it
1503
- * files or forwards the word (a policy layer could self-declare the member on its own deny; an
1504
- * out-of-vocabulary word is dropped by the carriers, never coerced or forwarded). */
1505
- export declare function isAskDenyResolution(v: unknown): v is AskDenyResolution;
1506
- /** #548: the engine-attested auto-deny marker off a funneled decision, bound to the named call like
1507
- * {@link coreMintedResolutionOf} (absent ⇒ not an attested auto-deny of THIS call). */
1508
- export declare function coreMintedAutoDeniedOf(d: unknown, call: {
1509
- toolCallId: string;
1510
- toolName: string;
1511
- }): boolean;
1512
- /** Read the engine-attested resolution off a funneled decision (the gate's single deny exit is the
1513
- * one consumer), FOR the named call: an attestation bound to a different toolCallId/toolName is a
1514
- * replayed object, not this call's settlement — the reader answers absence (the safe direction; the
1515
- * public `settledBy`/message on such an object were always the policy's own to state). A present
1516
- * word is an engine settlement site's own attestation for THIS object and THIS call — no foreign
1517
- * policy can reach the sidecar. The vocabulary screen is a belt (the typed stamp is the only
1518
- * writer). Exported for the gate module only — deliberately NOT re-exported from `src/index.ts`
1519
- * (the {@link refuseOutOfContractDecision} precedent: an internal seam between engine modules, not
1520
- * a facility deployments call). */
1521
- export declare function coreMintedResolutionOf(d: unknown, call: {
1522
- toolCallId: string;
1523
- toolName: string;
1524
- }): AskDenyResolution | undefined;
1525
1435
  /**
1526
1436
  * A {@link resolveAsk} result: always a TERMINAL `allow`/`deny` (never `ask`). `approverUnavailable`
1527
1437
  * is the out-of-band G1 three-value marker: the live approver returned `"unavailable"` for this ask —
1528
1438
  * the carried `deny` is the FAIL-CLOSED FALLBACK, and the gate may instead re-route the ask onto the
1529
1439
  * durable park leg (the only consumer; everywhere else the result reads as a plain deny).
1530
1440
  */
1531
- export type ResolvedAsk = PermissionResult & {
1532
- approverUnavailable?: true;
1533
- /** The EXACT data snapshot the approver was shown (the `structuredClone` of the
1534
- * presented args). On a plain approval (no edit) the gate EXECUTES this snapshot, not the original
1535
- * object: a stateful getter or an external alias must not be able to make the executed action
1536
- * differ from the approved one (shown == executed, by construction). Set only on the
1537
- * function-approver path (string modes present nothing). */
1441
+ export type ResolvedAsk = (Extract<PermissionResult, {
1442
+ action: "allow";
1443
+ }> & {
1444
+ /** Present exactly when a PERSON approved (`human_allowed`, with the channel's attribution when it
1445
+ * reported one); absent on the posture allows (a blanket `"allow"`, a reused grant) nobody was
1446
+ * asked, so nothing was settled. */
1447
+ settlement?: Settlement;
1448
+ /** The EXACT data snapshot the approver was shown (the `structuredClone` of the presented args). On
1449
+ * a plain approval (no edit) the gate EXECUTES this snapshot, not the original object: a stateful
1450
+ * getter or an external alias must not be able to make the executed action differ from the
1451
+ * approved one (shown == executed, by construction). Set only on the function-approver path
1452
+ * (string modes present nothing). */
1538
1453
  presentedInput?: unknown;
1539
- /** Present exactly when this deny is a PERSON's refusal (`settledBy: "human"`) that carried the
1540
- * decider's own note (the object arm's `reason`, screened and non-empty). Its ABSENCE on a human
1541
- * deny is the structural fact that the refusal was BARE — a "no" with no direction attached —
1542
- * which is the arm the runner treats as a control-flow boundary for the issuing batch (a bare
1543
- * "no" on the parent thread halts the remaining same-message tool calls; a refusal WITH a note
1544
- * gives the model direction to adapt to, so the turn continues). Never derived from message
1545
- * text; stamped only at the composing arm. Not stamped on the timeout deny (nobody answered)
1546
- * or on any engine-produced fail-closed refusal. */
1547
- humanRefusalNote?: true;
1548
- /** #548 — present exactly when this deny is the classifier denial-limit fallback's AUTO-DENY: the
1549
- * ask's own window (`AskRequest.denialLimitFallback.autoDenyAfterMs`, CC `AKe`) elapsed with no
1550
- * answer. Rides beside `settledBy:"timeout"` / `resolution:"window_expired"` (the established approval
1551
- * factory's deadline arm speaks the same two words, so a consumer classifying on them needs no new
1552
- * branch) and says WHOSE window it was: core's, not the host's. Stamped only at the composing arm. */
1553
- autoDenied?: true;
1554
- /** The deny-arm classification (see {@link AskDenyResolution}) — present on every deny this
1555
- * resolver composes, absent on every allow. Carried by the gate to its block exit, the
1556
- * permission-denied observer payload and the settlement sideband (thence the call's `tool_end`
1557
- * frame), so a consumer classifies a refusal by code instead of parsing its text. */
1558
- resolution?: AskDenyResolution;
1559
- };
1454
+ approverUnavailable?: never;
1455
+ }) | (Extract<PermissionResult, {
1456
+ action: "deny";
1457
+ }> & {
1458
+ /** What ended the wait composed at the arm that ended it, one of the refusal kinds. A deny this
1459
+ * resolver composes ALWAYS names its settlement: there is no unclassified refusal. */
1460
+ settlement: Settlement;
1461
+ presentedInput?: unknown;
1462
+ approverUnavailable?: true;
1463
+ });
1560
1464
  /**
1561
1465
  * Does any string reachable in `value` carry a {@link BIDI_CONTROL_RE} member? Bounded, cycle-safe,
1562
1466
  * and never throwing — every caller is on an approval/projection path, where a scan that failed must