@sema-agent/core 7.5.2 → 7.6.1

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 (138) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/dist/agents/agent-transcript-tool.d.ts +2 -2
  3. package/dist/agents/cascade.d.ts +4 -5
  4. package/dist/agents/cascade.js +12 -10
  5. package/dist/agents/repair-loop.d.ts +7 -5
  6. package/dist/agents/repair-loop.js +13 -15
  7. package/dist/agents/retain-ledger.d.ts +2 -3
  8. package/dist/agents/send-message-tool.d.ts +2 -2
  9. package/dist/agents/session-util.d.ts +2 -2
  10. package/dist/agents/subagent.d.ts +27 -46
  11. package/dist/agents/subagent.js +119 -105
  12. package/dist/agents/suspend-guard.d.ts +31 -19
  13. package/dist/agents/suspend-guard.js +14 -8
  14. package/dist/agents/teacher.d.ts +2 -2
  15. package/dist/agents/teacher.js +9 -9
  16. package/dist/agents/team.d.ts +6 -5
  17. package/dist/agents/team.js +10 -8
  18. package/dist/agents/verify.d.ts +8 -9
  19. package/dist/agents/verify.js +17 -17
  20. package/dist/core/a2a.js +2 -1
  21. package/dist/core/agent-definition.d.ts +172 -0
  22. package/dist/core/agent-definition.js +1 -0
  23. package/dist/core/ask-origin.d.ts +60 -7
  24. package/dist/core/ask-origin.js +26 -1
  25. package/dist/core/checkpoint-store.d.ts +78 -76
  26. package/dist/core/checkpoint-store.js +17 -1
  27. package/dist/core/delegation-frames.d.ts +298 -0
  28. package/dist/core/delegation-frames.js +21 -0
  29. package/dist/core/engine-notice.d.ts +555 -0
  30. package/dist/core/engine-notice.js +55 -0
  31. package/dist/core/gate-fold.d.ts +12 -0
  32. package/dist/core/gate-fold.js +158 -0
  33. package/dist/core/gate-lanes.d.ts +93 -0
  34. package/dist/core/gate-lanes.js +626 -0
  35. package/dist/core/gate-outcome.d.ts +189 -0
  36. package/dist/core/gate-outcome.js +70 -0
  37. package/dist/core/hands-band.d.ts +134 -0
  38. package/dist/core/hands-band.js +1 -0
  39. package/dist/core/hooks.d.ts +22 -177
  40. package/dist/core/hooks.js +53 -851
  41. package/dist/core/mcp-failure.d.ts +142 -0
  42. package/dist/core/mcp-failure.js +145 -0
  43. package/dist/core/mcp-server-spec.d.ts +217 -0
  44. package/dist/core/mcp-server-spec.js +1 -0
  45. package/dist/core/mcp.d.ts +21 -77
  46. package/dist/core/mcp.js +76 -150
  47. package/dist/core/model-seat.d.ts +99 -0
  48. package/dist/core/model-seat.js +1 -0
  49. package/dist/core/pause-registry.d.ts +131 -0
  50. package/dist/core/pause-registry.js +27 -0
  51. package/dist/core/reminder-mint.d.ts +10 -0
  52. package/dist/core/reminder-mint.js +3 -0
  53. package/dist/core/runner/assemble-result.d.ts +32 -41
  54. package/dist/core/runner/assemble-result.js +55 -74
  55. package/dist/core/runner/contracts.d.ts +427 -69
  56. package/dist/core/runner/denial-limit-arms.d.ts +1 -1
  57. package/dist/core/runner/denial-limit-arms.js +3 -3
  58. package/dist/core/runner/gate-exit.d.ts +242 -0
  59. package/dist/core/runner/gate-exit.js +124 -0
  60. package/dist/core/runner/park-commit.d.ts +17 -23
  61. package/dist/core/runner/park-commit.js +14 -15
  62. package/dist/core/runner/prepare-ask-lane.d.ts +0 -3
  63. package/dist/core/runner/prepare-ask-lane.js +3 -5
  64. package/dist/core/runner/prepare-boundary-parks.d.ts +3 -6
  65. package/dist/core/runner/prepare-boundary-parks.js +3 -3
  66. package/dist/core/runner/prepare-caps-and-workflow.d.ts +2 -7
  67. package/dist/core/runner/prepare-caps-and-workflow.js +1 -1
  68. package/dist/core/runner/prepare-delegation-surface.d.ts +2 -7
  69. package/dist/core/runner/prepare-gate-stations.d.ts +4 -7
  70. package/dist/core/runner/prepare-gate-stations.js +29 -54
  71. package/dist/core/runner/prepare-inherited-gate.js +1 -1
  72. package/dist/core/runner/prepare-memory.d.ts +44 -26
  73. package/dist/core/runner/prepare-park-ask.d.ts +2 -4
  74. package/dist/core/runner/prepare-park-ask.js +5 -5
  75. package/dist/core/runner/prepare-task.d.ts +2 -2
  76. package/dist/core/runner/prepare-task.js +8 -9
  77. package/dist/core/runner/prepare-wiring-manifest.d.ts +7 -15
  78. package/dist/core/runner/prepare-wiring-manifest.js +9 -10
  79. package/dist/core/runner/runtask.d.ts +20 -102
  80. package/dist/core/runner/runtask.js +119 -121
  81. package/dist/core/runner/terminal-projection.d.ts +22 -0
  82. package/dist/core/runner/terminal-projection.js +28 -0
  83. package/dist/core/runner-deps.d.ts +1416 -0
  84. package/dist/core/runner-deps.js +1 -0
  85. package/dist/core/runtime-caps.d.ts +164 -0
  86. package/dist/core/runtime-caps.js +1 -0
  87. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +4 -1
  88. package/dist/core/store-contracts/checkpoint-store-contract.js +8 -2
  89. package/dist/core/task-event.d.ts +910 -0
  90. package/dist/core/task-event.js +1 -0
  91. package/dist/core/task-limits.d.ts +110 -0
  92. package/dist/core/task-limits.js +1 -0
  93. package/dist/core/task-result.d.ts +809 -0
  94. package/dist/core/task-result.js +1 -0
  95. package/dist/core/task-spec.d.ts +1370 -0
  96. package/dist/core/task-spec.js +1 -0
  97. package/dist/core/task-stream.d.ts +382 -0
  98. package/dist/core/task-stream.js +1 -0
  99. package/dist/core/terminal-cause.d.ts +137 -0
  100. package/dist/core/terminal-cause.js +9 -0
  101. package/dist/core/tool-policy.d.ts +43 -139
  102. package/dist/core/tool-policy.js +79 -112
  103. package/dist/core/tool-spec.d.ts +1174 -0
  104. package/dist/core/tool-spec.js +1 -0
  105. package/dist/core/types.d.ts +27 -7789
  106. package/dist/core/types.js +2 -76
  107. package/dist/core/warm-resume.d.ts +2 -2
  108. package/dist/core/wiring-manifest.d.ts +6 -3
  109. package/dist/core/workflow-journal-store.js +3 -4
  110. package/dist/engine/harness/agent-harness.d.ts +1 -1
  111. package/dist/index.d.ts +12 -8
  112. package/dist/index.js +9 -6
  113. package/dist/orchestration/builtin-workflows.d.ts +2 -2
  114. package/dist/orchestration/builtin-workflows.js +1 -1
  115. package/dist/orchestration/goal.d.ts +2 -2
  116. package/dist/orchestration/goal.js +8 -7
  117. package/dist/orchestration/run-spec.d.ts +2 -2
  118. package/dist/orchestration/run-spec.js +5 -3
  119. package/dist/orchestration/run-workflow-tool.d.ts +4 -4
  120. package/dist/orchestration/run-workflow-tool.js +4 -4
  121. package/dist/orchestration/workflow-governance.d.ts +4 -4
  122. package/dist/orchestration/workflow-governance.js +4 -2
  123. package/dist/orchestration/workflow-primitives.d.ts +1 -1
  124. package/dist/orchestration/workflow-primitives.js +1 -1
  125. package/dist/orchestration/workflow.d.ts +15 -4
  126. package/dist/orchestration/workflow.js +64 -39
  127. package/dist/prompts/supervisor.d.ts +1 -1
  128. package/dist/prompts/supervisor.js +3 -3
  129. package/dist/scenarios/scenario-registry.d.ts +3 -3
  130. package/dist/scenarios/scenario-registry.js +1 -1
  131. package/dist/scenarios/teacher-quickstart.d.ts +2 -2
  132. package/dist/server/http.d.ts +2 -2
  133. package/dist/stores/file/fs-atomic.d.ts +88 -12
  134. package/dist/stores/file/fs-atomic.js +184 -55
  135. package/dist/stores/file/index.d.ts +1 -0
  136. package/dist/stores/file/index.js +1 -0
  137. package/package.json +3 -1
  138. package/test/export-surface.snapshot.json +82 -22
@@ -0,0 +1,809 @@
1
+ /**
2
+ * The TERMINAL record of one run: `TaskResult`, the plane word it still projects (`TaskStatus`), the
3
+ * memory-scope summary it carries (`EffectiveMemoryScopes`) and the remote-workspace failure note
4
+ * (`RemoteEnvFailureNote`). The cause itself lives one module over in `terminal-cause.ts`, which owns
5
+ * the closed set and its replay table. Layer 0 vocabulary; `types.ts` re-exports every name below, so
6
+ * no consumer's import changes.
7
+ */
8
+ import type { TerminalCause } from "./terminal-cause.js";
9
+ import type { NestedUsage } from "./tool-spec.js";
10
+ /**
11
+ * `"suspended"` (design/45) is a **non-terminal** outcome: the task hit a durable tool-policy gate
12
+ * (`ask` with a `CheckpointStore` wired) and persisted a checkpoint instead of finishing — resume it
13
+ * with `runner.resume(checkpointToken, outcome)`. v1 contract: it is only safe at the **top-level
14
+ * `runTask` boundary**; the orchestrators (cascade/teacher/verify/team) map an unexpected `suspended`
15
+ * to `failed` + `errorCode:"unexpected.suspended"` and pass the token up (§11 Q6, hard boundary).
16
+ *
17
+ * `"needs_review"` (design/76 §2.5, dry-run / shadow; design/80 D-B, plan-gate) is also a **non-terminal,
18
+ * resumable** outcome, but a DISTINCT one: it is the REVIEW-PAUSE terminal a human/judge must clear. Two
19
+ * disjoint gate kinds share it (carrying a `reviewRef`, errorCode `"review.pending"`): a `{kind:"needs_review"}`
20
+ * checkpoint — a profile's dry-run interception produced a predicted state-diff to REVIEW before it is applied,
21
+ * resumed with a `dry_run_review` outcome — and a `{kind:"plan_review"}` checkpoint (design/80 D-B) — a
22
+ * profile's plan-gate produced a proposed PLAN a human must approve/edit/reject BEFORE any step runs, resumed
23
+ * with a `plan_review` outcome. Both are DISJOINT from `"suspended"`: that is a PRE-ACTION approval of a TOOL
24
+ * CALL (resumed with `policy_ask`); these are human REVIEWS (the distinct gate.kind picks the resume outcome).
25
+ * Orchestrators that map an unexpected `suspended` should map an unexpected `needs_review` the same
26
+ * hard-boundary way (it is, like `suspended`, only safe at the top-level `runTask` boundary).
27
+ */
28
+ export type TaskStatus = "completed" | "blocked" | "failed" | "suspended" | "needs_review";
29
+ /**
30
+ * RB-439-a — one remote-workspace lifecycle failure, reported as data on {@link TaskResult.remoteEnvFailures}.
31
+ * See that field for why the eleven-code taxonomy needed a structured channel to reach a caller at all.
32
+ */
33
+ export interface RemoteEnvFailureNote {
34
+ /** Which VM-lifecycle call failed. */
35
+ op: "suspendVM" | "resumeVM" | "postResumeInit";
36
+ /** The adapter's verbatim code — the whole point of this note. */
37
+ code: import("./remote-env.js").RemoteExecutionErrorCode;
38
+ /** `code` is in the retryable family (`RETRYABLE_REMOTE_ERROR_CODES`) — a re-attempt can plausibly work. */
39
+ retryable: boolean;
40
+ /** How many times the engine called `op` before giving up (1 = it did not retry). */
41
+ attempts: number;
42
+ /** The adapter's message. Human-facing; branch on `code`, not on this. */
43
+ message: string;
44
+ }
45
+ /** Result returned when a task finishes or gets stuck. Designed to be machine-readable for an external AI. */
46
+ /**
47
+ * design/178 v2 §2.3 (件①) — the value of {@link TaskResult.effectiveMemoryScopes}: the memory
48
+ * visibility face a leg ACTUALLY ran under, as a DISCRIMINATED three-state union (deliberately no
49
+ * `"partial"` state — the engine session is built only after every plane materialized, so a
50
+ * half-mounted scene is a fail-open `memoryless` with residue, never a served half-face):
51
+ *
52
+ * - `"mounted"` — the memory engine session mounted. `scopes` is the EFFECTIVE VISIBILITY set in
53
+ * the effective serving order, both exactly as the engine mounted them: per plane, the
54
+ * admission-projected read layering PLUS a distinct write-only scope where one exists (the
55
+ * engine registers the write scope's dir and its entries ride the injected index — a write-only
56
+ * scope IS visible; the common writeScope∈scopes form dedups to the plain layering), project
57
+ * plane first on a dual root. Each row carries its admission ORIGIN (`"deployment"` = the
58
+ * operator's own declared set; `"request"` = the caller's spec — the two trust planes of the
59
+ * org admission door). `writeScope` is the effective write face as granted (an org writeScope
60
+ * not explicitly granted reads `null` here, exactly as it ran). `contract` names the
61
+ * scope-identity contract in force (`"v2"` typed keys / `"legacy"` opaque strings).
62
+ * SCOPE OF THE CLAIM (stated precisely): the rows are the MEMORY-ENGINE scope set this leg
63
+ * mounted — the dirs the engine registered and indexed. They deliberately do NOT cover the
64
+ * separate memory-adjacent faces (the shared-memory store pair, the project-context memory
65
+ * layer — each its own surface with its own disclosure), and two REGISTERED engine-plane
66
+ * channels can carry another scope's bytes without a row here: a root-owning scope's non-empty
67
+ * on-disk index is served verbatim to a later mount of that root ("live file wins"),
68
+ * and control-plane announcements carry no producing scope and drain plane-wide. Both are
69
+ * engine mount semantics under their own tickets, disclosed here so this face is never read as
70
+ * a complete cross-face visibility proof.
71
+ * - `"memoryless"` — memory was configured but did not mount. `reason: "mount-failed"` = the
72
+ * fail-open mount arm caught a fault anywhere in the mount phase (directory resolution through
73
+ * materialize — named for the whole captured span, not one step); `reason: "no-backend"` = the
74
+ * spec enables memory but no `RunnerDeps.memoryBackend` is configured. `materializedResidue`
75
+ * (mount-failed only) lists plane scopes whose PHYSICAL materialize had already completed when
76
+ * the fault hit — a LOWER bound (a plane that threw mid-materialize is not listed; on-disk
77
+ * residue is ≥ this list), and explicitly NOT a visibility face: an auditor must never read
78
+ * residue as mounted scopes, which is why it is a separate seat from the always-empty `scopes`.
79
+ * - `"none"` — memory was not in play at all: `"no-spec"` = the task carries no usable memory
80
+ * spec; `"disabled"` = a spec is present with `enabled: false`.
81
+ *
82
+ * Deliberate-refusal configurations (`config.memory_*` codes) fail the whole prepare and produce
83
+ * NO observation — this union never dresses a refusal as a state. Honesty note (design r7): the
84
+ * `?: never` members forbid non-`undefined` values at the type level; the repo does not compile
85
+ * with `exactOptionalPropertyTypes`, so explicit-`undefined` presence is a wire-validator concern,
86
+ * not a type-level one.
87
+ */
88
+ export type EffectiveMemoryScopes = {
89
+ state: "mounted";
90
+ reason?: never;
91
+ contract: "v2" | "legacy";
92
+ scopes: Array<{
93
+ scope: string;
94
+ origin: "deployment" | "request";
95
+ }>;
96
+ writeScope: string | null;
97
+ materializedResidue?: never;
98
+ } | {
99
+ state: "memoryless";
100
+ reason: "mount-failed";
101
+ contract?: "v2" | "legacy";
102
+ scopes: [];
103
+ writeScope: null;
104
+ materializedResidue?: string[];
105
+ } | {
106
+ state: "memoryless";
107
+ reason: "no-backend";
108
+ contract?: "v2" | "legacy";
109
+ scopes: [];
110
+ writeScope: null;
111
+ } | {
112
+ state: "none";
113
+ reason: "no-spec" | "disabled";
114
+ contract?: never;
115
+ scopes: [];
116
+ writeScope: null;
117
+ materializedResidue?: never;
118
+ };
119
+ export interface TaskResult {
120
+ taskId: string;
121
+ /**
122
+ * #499 — the ENGINE-minted identity of THIS run (uuidv7), minted once at the top of prepare and
123
+ * never rewritten. The third id of the trio, and the only one the engine owns:
124
+ * - {@link taskId} — the HOST's task identity, or (when the host named none) the session id: it is
125
+ * `spec.taskId ?? sessionId` and therefore answers the SAME value for every run of one session;
126
+ * - {@link sessionId} — the conversation, shared by every run that continues it;
127
+ * - `runId` — this call. Two runs of one session ALWAYS differ here; a host-supplied `taskId` does
128
+ * not change it (the host's handle and the engine's run are different questions).
129
+ *
130
+ * Use it to join a run's per-run disclosures (`task.user_steer_undrained` /
131
+ * `task.user_followup_undrained` / `route.fallback_to_primary` / `steering.parked_input_blocked` /
132
+ * `task.turn_interrupted` all carry `detail.runId`) and its `task.start`/`task.end` trace pair to
133
+ * the result they belong to. Do NOT use it as an aggregation key across runs — that is what
134
+ * `taskId`/`taskSignature` are for.
135
+ *
136
+ * **In-presence condition**: present on every terminal of a run that ENTERED prepare — a prepare-time
137
+ * refusal included. The id is minted and published at prepare's FIRST statement precisely so this
138
+ * holds: prepare itself emits run-scoped disclosures (`route.fallback_to_primary`), and a disclosure
139
+ * naming a run that no terminal ever names would be unjoinable on exactly the failure path it is
140
+ * about. It is absent only where no run was entered — a throw above the prepare call — and on
141
+ * stub/synthesized results a host assembles itself. Optional for that reason, never fabricated, and
142
+ * never the `"unknown"` sentinel `taskId`/`sessionId` fall back to on that path.
143
+ */
144
+ runId?: string;
145
+ /** Use this to continue the same conversation on the next call. */
146
+ sessionId: string;
147
+ /**
148
+ * WHY the run ended — the ONE tagged cause ({@link TerminalCause}), and the ONLY terminal record on this
149
+ * result: there is no `status` word and no `errorCode`/`errorMessage`/`blockedReason`/`checkpointToken`/
150
+ * `checkpointId`/`checkpointGate`/`workspaceRestoreMode` beside it (a second spelling of the same fact
151
+ * would be the one a reader reaches for, and the two would have to be kept equal forever). Branch on
152
+ * `terminal.kind`: `"paused"` carries the token and the gate together (a suspended run with no token
153
+ * cannot be constructed), `"failed"` carries the code and the message together, `"blocked"` its reason.
154
+ * A face that still speaks the five-word {@link TaskStatus} derives it with
155
+ * {@link import("./runner/terminal-projection.js").terminalProjection} — the one cause→word derivation.
156
+ */
157
+ terminal: TerminalCause;
158
+ /** design/99 MF-25: the EFFECTIVE model id that served this task — the RESOLVED `Model.id`, not the requested
159
+ * `TaskSpec.model` ref (which may be a role / name / `@mention`). Lets a UI echo "served by X" instead of the
160
+ * requested ref. A mid-run degradation is observed separately (see the degraded-model fields). */
161
+ model?: string;
162
+ /**
163
+ * #327 — the leg's effective REASONING resolution: the result-face twin of {@link model} for the thinking
164
+ * knob. How the requested tier resolved against the serving model's real capability
165
+ * (`requested`/`effective`/`graded`/`clamped`/`format`/`endpoint`, plus `dropped:true` when a
166
+ * non-reasoning model dropped the request entirely — field semantics on
167
+ * {@link import("../brain/reasoning.js").ResolvedReasoning}). It is the SAME resolver output the
168
+ * `reasoning.resolved` trace frame carries — the two faces cannot tell different stories; this seat
169
+ * serves consumers without a tracer (the trace frame is the deployment-observability face, this is the
170
+ * caller face).
171
+ *
172
+ * **In-presence condition** — mirrors the trace frame exactly: present on every terminal of a leg that ran
173
+ * with a REQUESTED thinking tier other than off/unset (the `spec > role > model.defaultThinking` chain);
174
+ * absent when thinking was off/unset for the leg, and on prepare failures (the resolution is minted after
175
+ * prepare). On a resumed task each leg re-resolves against the leg's own serving model.
176
+ *
177
+ * **Per-request correction** — the leg-entry mint runs before any request exists, so it can only
178
+ * describe the model's CAPABILITY. When the leg's FIRST committed request reports wire facts that change
179
+ * that answer, the seat is re-resolved against them by the same resolver and the trace twin is re-emitted
180
+ * with it. Today that is one arm: the anthropic budget path's cap-wins skip — a HARD per-request output
181
+ * cap too small to host a legal thinking budget deletes the thinking block, so a leg capped that way now
182
+ * reports `effective:"off"`/`dropped:true` instead of claiming the gradient the wire never carried. Only
183
+ * the FIRST committed request is consumed: the seat stays a leg-ENTRY snapshot (see the degradation law
184
+ * below), and the leg's nested internal brain calls (compaction summary, tool-invoked side query) carry
185
+ * their own caps and must never restate the leg's posture.
186
+ *
187
+ * **Degradation law** — same as {@link model}, whose resolution this is: the seat describes the leg's
188
+ * PRIMARY serving model at leg entry. A mid-run degradation (reactive fallback / near-budget switch)
189
+ * changes the serving model WITHOUT re-minting this seat or its trace twin — read {@link degraded} to see
190
+ * the switch; the fallback's own reasoning capability is deliberately NOT re-reported here (re-resolving
191
+ * one face would desync it from the task-start `reasoning.resolved` frame). A consumer needing the
192
+ * fallback's reasoning posture resolves `degraded.to` itself (`resolveReasoning` is exported).
193
+ */
194
+ effectiveReasoning?: import("../brain/reasoning.js").ResolvedReasoning;
195
+ /** Final assistant text. */
196
+ result: string;
197
+ /**
198
+ * Best-effort salvaged assistant text from a terminal FAILURE. Consumption recipe: on a non-`completed`
199
+ * status read `result` FIRST — most failure terminals (max-turns, budget, tool errors, …) still carry
200
+ * the final assistant text there — and fall back to `salvagedOutput` only when `result` is empty:
201
+ * `const out = r.result || r.salvagedOutput || ""`. Do NOT treat `salvagedOutput` as "the output on
202
+ * failure": it is populated on one CLOSED list of terminals, and on all of them the assembler fills it
203
+ * from the SAME final-message text that `result` carries, so a consumer that reads only `salvagedOutput`
204
+ * on failure discards real work on every other failure path.
205
+ *
206
+ * The populating terminals, and what the text means on each:
207
+ * - `errorCode === "output.degenerate"` (degenerate-repetition cutoff): the model's text up to the
208
+ * loop, with the degenerate tail TRIMMED at the brain stream layer (byte-identical repeats dropped,
209
+ * one instance of the repeating unit kept — see `trimDegenerateTail`). Declared LOSSY normalization:
210
+ * the trim never drops unique bytes, but the repetition COUNT is not preserved — a payload whose
211
+ * meaning lives in how many times a unit repeats (fixed-length padding, "output N copies") comes
212
+ * back as one instance; this is not the raw failure snapshot.
213
+ * - the `limits.max_*_exceeded` family (a per-slice window was exhausted on a task that did not opt
214
+ * into resource-suspend): whatever text the run had produced before the loud terminal.
215
+ * - `"env.lifetime_expired"` / `"usage.window_exhausted"` (ruled 2026-08-04): the same, for the two
216
+ * EXTERNAL stop causes — an expiring environment or a closed deployment window that could not be
217
+ * suspended into a checkpoint. Nothing about the run itself failed, so its work is handed back on the
218
+ * same seat the ceilings use. (An entry-time `usage.window_exhausted` refusal ran nothing, so there
219
+ * is nothing to salvage and the field is absent.)
220
+ *
221
+ * Undefined on every other terminal and when there was nothing to salvage.
222
+ */
223
+ salvagedOutput?: string;
224
+ /**
225
+ * RB-439-a — the remote-workspace lifecycle failures this run hit, as DATA. The seam declares eleven
226
+ * distinct `RemoteExecutionErrorCode`s, but every one of them used to reach the caller as the same
227
+ * thing: a `suspendVM` refusal was swallowed into the `onError` side channel and the result read
228
+ * `limits.max_turns_exceeded` (identical for a transient auth blip and a permanently-unsupported adapter), while
229
+ * a failed restore reported the single code `resume.env_failed` with the real code buried in prose. A
230
+ * deployment that wants to distinguish "refresh the credential and re-resume" from "this adapter can
231
+ * never do it" had nothing but a string to parse.
232
+ *
233
+ * - `op` — which lifecycle call failed.
234
+ * - `code` — the adapter's verbatim `RemoteExecutionErrorCode`.
235
+ * - `retryable` — whether `code` is in the retryable family (`RETRYABLE_REMOTE_ERROR_CODES`). For the
236
+ * IDEMPOTENT restore ops the engine has already spent its own bounded retries (see `attempts`); this
237
+ * flag is what a caller acts on for the ops the engine deliberately does not retry — a `suspendVM`
238
+ * takes a snapshot, so re-driving it is the caller's decision, not the engine's.
239
+ * - `attempts` — how many times the engine called it (1 = no retry was made or none was warranted).
240
+ * - `message` — the adapter's message, for humans.
241
+ *
242
+ * Present only when at least one lifecycle call failed (absent = nothing to report), so a consumer that
243
+ * ignores the field is unchanged. It is DIAGNOSTIC: {@link terminal} keeps its existing meaning —
244
+ * a failed suspend still ends the task the way it always did, it is just no longer anonymous.
245
+ */
246
+ remoteEnvFailures?: RemoteEnvFailureNote[];
247
+ /**
248
+ * Present when this run's terminal failure came from the PROVIDER BOUNDARY — the transport or the
249
+ * provider failed the model call, as opposed to this deployment refusing to send one, a limit being
250
+ * reached, or the model's own output being unusable. Its PRESENCE is the assertion; the members are
251
+ * whatever the failing attempt actually stated about itself:
252
+ * - `status` — the failing HTTP status, when the provider answered with one. Absent for a
253
+ * connect failure, a stall, a mid-stream tear, and an in-band error frame delivered inside a
254
+ * 200 — never zeroed, since 0 reads as a number a consumer may format.
255
+ * - `requestId` — the provider's own request identifier for that attempt, when it stated one in a
256
+ * shape worth carrying. This is the handle a provider's support channel asks for.
257
+ * Both may be absent together: that is a provider failure nobody could label further, which is a
258
+ * different statement from "not a provider failure" (the field itself absent).
259
+ *
260
+ * Nested rather than three loose keys so the assertion and its evidence cannot drift apart — there
261
+ * is no state in which a consumer sees the claim beside a status belonging to some other attempt.
262
+ * The same family rides the assistant message itself as `isApiErrorMessage` / `apiErrorStatus` /
263
+ * `requestId`; this seat is its RUN-LEVEL projection, lifted from that message at the one assembly
264
+ * point rather than re-derived, and the two spellings are named here so the mapping is written down
265
+ * instead of inferred.
266
+ *
267
+ * PRESENCE, deliberately narrow: filled only when the provider failure is the terminal that NAMED
268
+ * this result. A run that hit a provider failure and then a higher-ranked terminal (a budget
269
+ * ceiling, a walltime cut) reports that other cause and leaves this absent — the same rule
270
+ * {@link usageWindowRetryAfterMs} states for itself, and for the same reason: a diagnostic seat
271
+ * beside a cause it does not belong to is worse than a missing one.
272
+ */
273
+ apiFailure?: {
274
+ status?: number;
275
+ requestId?: string;
276
+ };
277
+ /**
278
+ * How long (milliseconds) until the deployment usage window that stopped this task frees up — the wait
279
+ * hint a scheduler needs to decide WHEN to re-submit, rather than polling.
280
+ *
281
+ * **In-presence condition:** set on exactly two terminals — `errorCode === "usage.window_exhausted"`
282
+ * (either moment: the entry refusal that ran nothing, or the running terminal that could not suspend)
283
+ * and `errorCode === "memory.admission_required"` (the prepare-throw path sets it there too — see
284
+ * assemble-result's accepted-code pair). Absent everywhere else, INCLUDING when a usage window did
285
+ * stop the run but a higher-ranked terminal
286
+ * named the result (a budget ceiling crossed on the same turn): the reported cause is then that other
287
+ * code, and a wait filed under it would describe something the code does not name.
288
+ *
289
+ * **Do not branch on its presence** to work out WHY a task stopped — read `errorCode` for that (the
290
+ * standing rule for every optional field on this type). A consumer treating "has `retryAfterMs`" as
291
+ * "was rate-limited" misreads both directions: the same code can arrive without one, and the field
292
+ * says nothing about which of the two governance moments produced it (`stats.turns`/`result` do).
293
+ *
294
+ * A DURABLE pause carries its own wait elsewhere: a `suspended` result's hint rides the checkpoint gate
295
+ * (`resumeAfterMs`), which the resume path consumes — the two are deliberately separate seats for the
296
+ * two different resumption stories (re-submit the task vs resume the checkpoint), never aliases.
297
+ *
298
+ * The same number is also delivered to `RunnerDeps.onError` on the typed error (unchanged) — this seat
299
+ * is for the caller that holds the result and never wired that sink.
300
+ */
301
+ retryAfterMs?: number;
302
+ /**
303
+ * Present when the task was served — at least once — by a **degraded** (cheaper/different) model
304
+ * because the primary was rate-limited, its circuit breaker was open, or it failed with a
305
+ * `server_error`/`last_resort` class error (design/126 fallback chain, opt-in via
306
+ * `createDegradingBrain`). Lets a caller know the result was produced at reduced quality and alert/
307
+ * track it. Records the **first** degradation only (degradation is one-way within a task). Set even
308
+ * when the fallback also failed — so `status:"failed"` + `degraded` means "primary down, fallback
309
+ * down too". `reason="budget"` is reserved for the v2 near-budget trigger.
310
+ */
311
+ degraded?: {
312
+ from: string;
313
+ to: string;
314
+ /** design/126 widens the union: `server_error` (5xx/529 escaped the retry budget) and `last_resort`
315
+ * (404/other non-retryable statuses) — the CC 批 β fallback-chain triggers. Additive on this
316
+ * OUTPUT field; consumers switching exhaustively should add the two arms. */
317
+ reason: "breaker_open" | "rate_limit" | "budget" | "server_error" | "last_resort";
318
+ /** design/126 — the fallback-chain model ids attempted after the primary, in order (absent on the
319
+ * single-hop legacy path's pre-126 markers and on the budget path). */
320
+ chain?: string[];
321
+ /** 1-based turn during which degradation was triggered. Reactive (rate_limit/breaker): the turn the
322
+ * cheaper model first served. Budget: the turn whose cumulative cost crossed the threshold (the
323
+ * switch takes effect from the next turn). */
324
+ atTurn: number;
325
+ };
326
+ /** The validated object the model submitted via `submit_output` when `TaskSpec.outputSchema` was set
327
+ * (1.41). Undefined if the model answered in prose instead, or the task didn't finish via the output
328
+ * tool. Cast it with `Static<typeof yourSchema>`. (Exhausting the output-retry cap → `failed` +
329
+ * `errorCode="output.invalid"`, and this stays undefined.) */
330
+ structuredOutput?: unknown;
331
+ /**
332
+ * design/101 §E19 + design/381 — NON-FATAL rewind disclosures for this run: the legs where the rewind
333
+ * machinery did something other than what the request's plain reading implies, reported as data instead
334
+ * of silence. Present only when at least one applies (absent = nothing to disclose), so a consumer that
335
+ * ignores the field is unchanged. A rewind that could NOT be delivered as asked is NOT a note — it is a
336
+ * terminal failure (`rewind_snapshot.unresolvable` / `rewind.store_unconfigured` /
337
+ * `rewind.restore_failed` / `rewind.invalid_spec` / `rewind.rewind_files_retired` /
338
+ * `rewind.conflicting_targets` / `rewind.child_scope_unsupported`).
339
+ *
340
+ * - `conversation_only` — {@link TaskSpec.resumeAt} branched the transcript WITHOUT
341
+ * {@link TaskSpec.restoreFiles}, so the working tree was deliberately left where it was (CC's
342
+ * "Restore conversation" mode). Legal and useful; the note is what makes the two axes' divergence
343
+ * visible.
344
+ * - `files_env_unsupported` — a file restore was requested but this deployment mounts no
345
+ * filesystem-capable `ExecutionEnv` (stub env), so the file axis was inert. Not an error: such a
346
+ * deployment has no working tree to rewind.
347
+ * - `restore_partial` — {@link TaskSpec.acceptPartialRestore} tolerated a partial restore: ≥1 tracked
348
+ * file was refused or failed while the others converged; the message carries the per-file ledger
349
+ * summary (applied / identical / refused / failed). Present ONLY under that explicit opt-in — the
350
+ * default posture is the loud terminal `rewind.restore_failed` (DV-15).
351
+ *
352
+ * (design/381 closed-set surgery: `snapshot_store_unconfigured` is RETIRED — its only producer was
353
+ * the per-task capture request, and capture is now always-on when a store is wired; a restore request
354
+ * with no store keeps the fail-loud terminal instead.)
355
+ */
356
+ rewindNotes?: Array<{
357
+ code: "conversation_only" | "files_env_unsupported" | "restore_partial";
358
+ /** Human-readable statement of what did NOT happen and why — safe to show a user verbatim. */
359
+ message: string;
360
+ }>;
361
+ /**
362
+ * The files THIS RUN's hands actually mutated, with how many times each — the same
363
+ * `{path, edits}` shape the delegated-child projection publishes as
364
+ * {@link import("../agents/subagent-steps.js").SubagentEditedFile}, so a host can read the two
365
+ * seats side by side. A run is one host-side user message, which is the granularity a
366
+ * "restore the code this message changed" affordance needs; the per-model-turn beat
367
+ * (`turn_end`) deliberately has no such seat.
368
+ *
369
+ * **Source**: the hands band's own mutation lane. One entry is bumped when a `Write`/`Edit`/
370
+ * `NotebookEdit` call's FINAL env write returns success — not when a tool call starts, not from a
371
+ * tool-name table over the event stream. The consequences are the contract:
372
+ * - a refused, gated or FAILED write is not counted (a write whose bytes never landed is not an
373
+ * edit — the same lane point that retracts its first-touch history record);
374
+ * - a `Bash` command that changed a file is not counted: it does not go through those lanes.
375
+ * This matches the reference implementation's own trigger set;
376
+ * - one tool CALL counts once, so a batch `Edit` (multiple `edits[]` against one file in one
377
+ * call) is one edit here, exactly as the child projection counts it.
378
+ *
379
+ * **`path` form**: the model-supplied path argument, verbatim — the same coordinate
380
+ * `SubagentEditedFile.path` uses, deliberately not the canonical containment key, so both seats
381
+ * spell a file the way the transcript does. IDENTITY, however, is the canonical FILE, not the
382
+ * spelling: a relative argument resolves against the run's LIVE cwd, so one spelling can name two
383
+ * different files across a `cd` (two entries, each showing that spelling) and two spellings can
384
+ * name one file (ONE entry, showing the first spelling that reached it). Insertion order = order
385
+ * of first edit.
386
+ *
387
+ * **Three-valued on the failure edge, upper bound never lower**: a write failure is three-valued.
388
+ * A failure whose error code's contract states nothing was written is a proven no-op and is NOT
389
+ * counted. An AMBIGUOUS failure or a thrown write IS counted: an `ExecutionEnv` whose write is not
390
+ * atomic can truncate a file and then report an error, that file may really have changed, and the
391
+ * first-touch history record for it is kept for exactly that reason — so this list names it too,
392
+ * and a `rewindFilesTo` will restore it. (The reference env replaces whole files by stage+rename —
393
+ * fully old or fully new — so this edge belongs to its in-place fallback arms and to third-party
394
+ * envs.) A host therefore reads this list as "files that may differ from before this run", never
395
+ * as "writes the model saw succeed"; the tool answers carry that.
396
+ *
397
+ * **In-presence condition**: present iff this run's hands landed ≥1 such write — zero edits means
398
+ * the key is ABSENT, never an empty array. It does NOT depend on whether a
399
+ * `RunnerDeps.fileHistoryStore` is wired: this is an observation of what this run did, not a
400
+ * durable history, and a deployment with no history store still gets it (what a store adds is the
401
+ * ability to REWIND, not the ability to say what changed). It covers this run's OWN band only — a
402
+ * delegated child's writes ride the child's own result and the delegation projection, not this
403
+ * key.
404
+ *
405
+ * **Bound**: at most 1000 distinct files. Past that, already-listed files keep counting and new
406
+ * ones are not added — a result seat cannot be unbounded, and a single user message reaching a
407
+ * thousand distinct files is already outside what this observation is for. A consumer that must
408
+ * know whether it is reading a saturated list can compare the length against that ceiling.
409
+ */
410
+ editedFiles?: import("../agents/subagent-steps.js").SubagentEditedFile[];
411
+ /**
412
+ * Present (`true`) exactly when the run's FINAL turn was halted by a person's BARE rejection of a
413
+ * tool call — the parent-thread control-flow boundary: the rejected call's same-message siblings
414
+ * that had not started were settled un-executed (each an error result coded `gate.batch_halted`),
415
+ * and the engine deliberately did NOT re-invoke the model, so the run ends awaiting the user's
416
+ * direction. On this path `status` is still `"completed"` (nothing failed, nothing is suspended,
417
+ * the session is continuable as ever) — this field is what tells such an end apart from a natural
418
+ * finish: `result` text is whatever the model had produced BEFORE the rejection (often empty), and
419
+ * a consumer surface should read the state as "stopped by the user, awaiting their direction",
420
+ * never as "the task finished its work". Absent everywhere else — including when queued user input
421
+ * (a steer/follow-up) continued the run past the rejection and it later ended naturally.
422
+ */
423
+ haltedOnUserRejection?: true;
424
+ /**
425
+ * design/373 (#504) — present (`true`) exactly when a {@link TaskStream.halt} was ACCEPTED while
426
+ * this run was live: the person issued the bare user interrupt (the CC Esc form — cut the
427
+ * in-flight turn, stop at that boundary, wait for their next input). The ruled terminal form is
428
+ * completed-with-marker, this seat being the marker: `status` is `"completed"` on the ordinary
429
+ * path (a clean, resumable ending — nothing failed, nothing is suspended, the session continues
430
+ * via the ordinary next-run front door), and this field is what tells a person-stopped ending
431
+ * apart from a natural finish — same reading discipline as {@link haltedOnUserRejection}, its
432
+ * sibling seat ("stopped by the user, awaiting their direction", never "the task finished its
433
+ * work"). `result` is whatever the model had produced before the halt (possibly empty — a halt
434
+ * can land before any output; the run then completes empty rather than failing). Pass-through on
435
+ * EVERY terminal the run still reaches (a halt ACCEPTED first that then raced a real
436
+ * failure/limit truthfully says a person also stopped it), and — the stated honest window — on
437
+ * a natural completion the halt arrived too late to prevent (past the loop's final commit
438
+ * point): the fact reported is the ACCEPTED HALT, and the transcript says how far the model
439
+ * got. First-writer-wins against the run's abort (the `interrupt()` attribution law, the same
440
+ * gate): a halt landing AFTER the abort signal already fired neither cut nor stopped anything —
441
+ * that ending belongs to the abort, and this seat stays ABSENT rather than signing someone
442
+ * else's stop with the halt caller's name. Absent everywhere else; never `false`.
443
+ *
444
+ * design/384 slice 2 (TRANSITIONAL narrowing): a `"suspended"` / `"needs_review"` terminal does
445
+ * NOT carry this seat even when a halt was accepted — those statuses mean a durable park WON its
446
+ * race with the halt (the row is committed and redeemable; the run is waiting to continue), and
447
+ * "stopped by the person" beside "waiting to resume" was a self-contradictory pair. The halt's
448
+ * own receipt (`{turnCut}`) and the `task.turn_interrupted` notice still stand — a seat WAS cut.
449
+ * Transitional: once halt-boundary source accounting lands (slice 3), the boundary-CONSUMED
450
+ * suspension arms flip to signing (a probe pinning today's suppressed shape goes red then, by
451
+ * design). The pass-through law is untouched for every OTHER terminal: a halt racing a real
452
+ * failure/limit — including one that outranks a committed park — still signs.
453
+ */
454
+ haltedByUser?: true;
455
+ /**
456
+ * The deliveries of AskUserQuestion calls a person ANSWERED but whose call never executed to collect
457
+ * the answer (the leg ended first — abort, batch teardown, or a loop failure). Rides EVERY terminal
458
+ * when non-empty, because the alternative is an answer a human produced vanishing with nothing to
459
+ * show for it: the answers were NOT delivered to the model and are gone with this leg — re-ask if
460
+ * the decision is still needed. `RunnerDeps.onError` (classification `"unconsumed-human-answer"`)
461
+ * carries the same fact as an additional alert; this field is the mandatory face.
462
+ */
463
+ strandedHumanAnswers?: ReadonlyArray<{
464
+ /** The engine-minted per-delivery identity (AskQuestionRequest.deliveryId) — the KEY: call ids can
465
+ * repeat across deliveries, so two lost answers on one call id are still two records here. */
466
+ deliveryId: string;
467
+ toolCallId: string;
468
+ }>;
469
+ /**
470
+ * #481 — HOST-SUPPLIED: the approval plane's report that this leg ended with human approvals still
471
+ * OUTSTANDING (the ask identities it is holding, and when the OLDEST of them was created).
472
+ *
473
+ * **Who writes it, and why not the engine.** Core mints no ask identity — an ask id belongs to the
474
+ * deployment's approval store, and `resolveAsk` only awaits the host's `onAsk` — and core starts no
475
+ * approval clock. So this seat is filled by the `Runner` a deployment hands to
476
+ * `runWorkflow`/`startWorkflow`: that call both RAN the leg and owns the pending-approval map, which
477
+ * is the only place the two facts meet without inventing an attribution. Core's own Runner never
478
+ * writes it. ABSENT is therefore the ordinary world — an older deployment, or one whose approvals are
479
+ * always answered inline — and every such leg behaves exactly as it did before this seat existed.
480
+ *
481
+ * **Three-state, deliberately.** Absent = "this deployment is not reporting outstanding approvals, or
482
+ * there are none"; present = "these asks are outstanding". An EMPTY `askIds` is neither and is
483
+ * refused: it would let a half-written record read as "waiting on nothing", which is precisely the
484
+ * misdiagnosis the seat exists to prevent. A present-but-malformed value is announced and then
485
+ * treated as no report — never silently folded into absence, and never granted the exemption below.
486
+ *
487
+ * **What core does with it** (`src/orchestration/workflow.ts`): a workflow agent leg whose progress
488
+ * watchdog fired while an approval was outstanding was NOT stalled — it was waiting on a person, by
489
+ * design — so that attempt is not charged against the stall-retry budget. The run's own
490
+ * `totalTimeoutMs` remains the backstop for a wait nobody ever answers.
491
+ */
492
+ pendingApproval?: {
493
+ askIds: readonly string[];
494
+ oldestCreatedAtMs: number;
495
+ };
496
+ /**
497
+ * #485 — HOST-SUPPLIED: how long this leg spent BLOCKED on human approvals, in ms, as the deployment's
498
+ * approval plane measured it. Same writer and same absence contract as {@link pendingApproval}: the
499
+ * approval clock belongs to whoever holds the ask, so the figure is SINGLE-SOURCED there and core neither
500
+ * estimates it nor accumulates it (two sides publishing two numbers for one wait is the drift this seat
501
+ * exists to avoid). ABSENT is the ordinary world.
502
+ *
503
+ * Already CUMULATIVE for the leg — a receipt states the leg's total wait so far, not that attempt's
504
+ * increment — so a later receipt SUPERSEDES an earlier one. Adding successive receipts double-counts.
505
+ *
506
+ * **Absence is not zero.** `0` is "nobody was waited on"; absent is "this deployment does not supply the
507
+ * fact". A consumer that defaults absence to `0` reports a deployment's silence as a measurement.
508
+ *
509
+ * What it is FOR: a workflow that hit its total timeout cannot otherwise say whether it was SLOW or
510
+ * WAITING. `WorkflowRun.timeoutInterruption.approvalWaitedMs` carries the largest leg's figure onto the
511
+ * terminal disclosure for exactly that reading — near `timeoutMs` ⇒ the window went on a person, not on
512
+ * work.
513
+ */
514
+ approvalWaitedMs?: number;
515
+ /**
516
+ * #240 (design/199 v1.1) — the READ-face this leg's read surfaces actually judged under, as an
517
+ * engine-filled OBSERVATION (never a knob: writing it on a spec does nothing). It is the run's ONE
518
+ * resolved face — the same value the hands toolkit enforced and the delegation carriers rode — with
519
+ * the hands-less resume pin folded in (a row resumed on a hands-less worker still reports the
520
+ * checkpoint-frozen `"roots"`).
521
+ *
522
+ * **In-presence condition (#242 widened):** present on every leg that completes prepare — hands
523
+ * mounted (any run with the built-in fs band) AND every hands-less leg (resume legs fold the
524
+ * checkpoint seed stricter-wins; non-resume legs report the live resolution). Absent only on
525
+ * prepare-failure terminals (nothing ran). Consumers that keyed on absence = "no posture" must
526
+ * re-key: absence now means only "prepare never completed". Rides every OTHER terminal, not just
527
+ * `completed` — the posture is a fact about the leg that ran, whatever ended it.
528
+ *
529
+ * Purpose: a POST-COMPLETION follow-on leg spawned OUTSIDE the delegation tree (the verify gate's
530
+ * verifier and fix legs are the canonical case) cannot see the checkpoint fold the completed leg ran
531
+ * under — its spawner holds only the resume-side config, which never contained the frozen posture.
532
+ * Folding this seat into the follow-on spec (stricter-wins: only `"roots"` ever tightens; an
533
+ * `"open"` observation is never forwarded) closes that escape for ANY follow-on, not just the
534
+ * verifier. See {@link effectiveReadDenyPatterns} for the deny-set half.
535
+ */
536
+ effectiveReadFace?: import("../tools/fs/read-face.js").ReadFace;
537
+ /**
538
+ * #240 (design/199 v1.1) — the sensitive-path deny ADDITIONS in force on this leg beyond the
539
+ * built-in table, normalized (deployment ∪ task ∪ checkpoint-frozen seed), as an engine-filled
540
+ * OBSERVATION. "In force" = judged by this leg's own read surfaces where they mounted, and carried
541
+ * to its delegation subtree either way (a hands-less resume leg reports the entries its children
542
+ * judge under).
543
+ * This seat is the only place a checkpoint-FROZEN deny entry becomes visible after the resumed leg
544
+ * completes: the resume-side caller's own config never contained it. A follow-on leg's spawner
545
+ * unions these into the child's `readDenyPatterns` (add-only; the child's compile folds exact
546
+ * duplicates, so re-supplying deployment-shared entries is idempotent) — that is how the frozen
547
+ * posture rides every leg that continues this work, not just the delegation subtree.
548
+ *
549
+ * **In-presence condition:** present iff non-empty. Built-in entries are never listed (they are in
550
+ * force on every run and re-compile locally). Same terminal coverage as
551
+ * {@link effectiveReadFace}: rides any terminal of a leg that ran; absent on prepare failures.
552
+ *
553
+ * On a DURABLE-PAUSE hand-back these seats are additionally the RE-SUPPLY hint: a checkpoint
554
+ * minted by a leg with no face resolution (a hands-less non-resume continuation) carries no
555
+ * frozen face section of its own, so a caller resuming that token must fold these entries back
556
+ * into the resume-side config (`readDenyPatterns` / `readFace: "roots"`). The resumed leg unions
557
+ * them into its own judgment and delegation carriers; the union is NOT serialized into any
558
+ * further checkpoint that leg may mint (a hands-less RESUME mint re-freezes only its original seed; a non-resume hands-less pause mints its live-resolved posture, #242), so
559
+ * fold them on EVERY resume — the standing resume contract. The seats describe the posture
560
+ * governing the WORK, never the contents of any checkpoint row.
561
+ */
562
+ effectiveReadDenyPatterns?: readonly import("../tools/fs/read-deny.js").NormalizedReadDenyEntry[];
563
+ /**
564
+ * design/178 v2 §2.3 (件①) — the memory VISIBILITY face this leg actually ran under, as an
565
+ * engine-filled OBSERVATION (never a knob: writing it on a spec does nothing). This is the
566
+ * EFFECTIVE face, not the requested one: a refused request never reaches a terminal at all (the
567
+ * whole prepare fails with its governance code), so what this seat answers is "what was actually
568
+ * given" — the delta against the request is directly readable (an org writeScope not explicitly
569
+ * granted collapses to `null` here as it did in the run; a fail-open mount failure reads
570
+ * `memoryless`, never a dressed-up mount).
571
+ *
572
+ * **In-presence condition** (same law as {@link effectiveReadFace}): present on every terminal of
573
+ * a leg that COMPLETED prepare — the memory-less states are answered as their own values
574
+ * (`none` / `memoryless`), so consumers must never read ABSENCE as "no memory"; absence means
575
+ * only "prepare never completed". Delegated children mint their own on their own legs (their
576
+ * request plane can only narrow the parent's frozen org verdict); a resume leg's value is the
577
+ * re-adjudication at resume time (admission runs in every prepare — the current-policy reading,
578
+ * same axis as the read-face seats).
579
+ *
580
+ * Minted AFTER the materialize outcome, not after the admission verdict — the fail-open mount
581
+ * arm sits between the two, and stamping earlier would report a mount that never happened.
582
+ * See {@link EffectiveMemoryScopes} for the per-state field law.
583
+ */
584
+ effectiveMemoryScopes?: EffectiveMemoryScopes;
585
+ /**
586
+ * `turns`/`tokens`/`costMicroUsd` are this task's OWN model usage. `nested` is the summed usage of any
587
+ * delegated sub-runs (sub-agents) it spawned — present only when it delegated. The true total
588
+ * cost is `tokens + (nested?.tokens ?? 0)`; a large `nested` is the multi-agent "~15×" made visible.
589
+ */
590
+ stats: {
591
+ turns: number;
592
+ tokens: number;
593
+ /** design/97 CORE-8 (②): total TOOL CALLS executed (one per `tool_start`) — distinct from `turns` (model
594
+ * rounds; a turn may issue several parallel tool calls). For a CC-style "N tool calls" per-agent row.
595
+ * **v1 fidelity (audit MINOR)**: counted in THIS run leg via the harness dispatch path, so on a DURABLE
596
+ * RESUME it under-reports — the resumed gated call (executed by the resume engine, not the harness) and any
597
+ * cross-slice resource-suspend legs are not folded (turns/tokens/cost ARE folded; toolCalls is per-leg, like
598
+ * the compaction subtotal). OBSERVE-ONLY (never gates budget). The workflow display is unaffected (its agents
599
+ * fail-on-suspend, never durably resume). A `spentToolCalls` ledger fold is a follow-on. */
600
+ toolCalls?: number;
601
+ /** Prompt (input) tokens that MISSED the prefix cache, summed across the task's turns and normalized
602
+ * across providers (Anthropic already reports `input_tokens` this way; OpenAI/vLLM include the cache
603
+ * in theirs, so the subsets are removed). Mutually exclusive with `cachedTokens`/`cacheWriteTokens*`;
604
+ * the three of them sum to `totalInputTokens`.
605
+ *
606
+ * **RB-457-a (BREAKING at 3.0.0)**: this used to carry the cache-INCLUSIVE total, which collides with
607
+ * the Anthropic protocol's identically-named quantity — a consumer computing
608
+ * `cachedTokens / (promptTokens + cachedTokens)` double-counted the cache subset and reported
609
+ * `h/(1+h)` (a 98% hit rate surfaced as 49.5%). Use `totalInputTokens` for the old value; use
610
+ * `cacheHitRate` rather than recomputing a ratio. */
611
+ promptTokens?: number;
612
+ /** Prompt tokens served from the model's prefix cache (a cache HIT), summed over the task's
613
+ * turns — when the gateway reports it (vLLM/OpenAI `prompt_tokens_details.cached_tokens`,
614
+ * DeepSeek `prompt_cache_hit_tokens`, Anthropic `cache_read_input_tokens`). */
615
+ cachedTokens?: number;
616
+ /** Prompt tokens written to the cache (Anthropic `cache_creation_input_tokens`); 0 elsewhere. */
617
+ cacheWriteTokens?: number;
618
+ /** `cachedTokens / totalInputTokens` ∈ [0,1] — the prefix-cache hit rate for this task. Cost-critical;
619
+ * a low value on a multi-turn task means the cacheable prefix is unstable (see `design/09`).
620
+ * Track it per task and trend it per session. Undefined when the gateway reports no usage.
621
+ * **Prefer this over recomputing a ratio** — it is the one place the denominator is defined. */
622
+ cacheHitRate?: number;
623
+ /** The cache-INCLUSIVE prompt total across the task's turns — `promptTokens + cachedTokens +
624
+ * cacheWriteTokens + cacheWriteTokensLong`, matching OTel `gen_ai.usage.input_tokens`. This is the
625
+ * quantity cost is computed from, the denominator of `cacheHitRate`, and the figure to use for
626
+ * "how much context did this task present" (window/budget views). RB-457-a: `promptTokens` carried
627
+ * this value up to 2.13.x; it now carries the cache MISS count and this field is its own
628
+ * accumulator rather than an alias. Optional BY DESIGN, like every usage field on this stats
629
+ * face: undefined means the gateway reported no usage (and pre-3.0.0 persisted rows never had
630
+ * it) — the event-face `turn_end.usage.totalInputTokens` is required because there the whole
631
+ * `usage` object is already conditional (intentional asymmetry, not a gap). */
632
+ totalInputTokens?: number;
633
+ /** Completion (output) tokens summed over the task's turns. */
634
+ outputTokens?: number;
635
+ /** Cache-write tokens at the 1-hour TTL (Anthropic); 0 unless 1h caching is in use. */
636
+ cacheWriteTokensLong?: number;
637
+ /** Authoritative (and only) cost figure: **integer micro-USD** (1e-6 USD), computed in core from
638
+ * injected pricing — integer to avoid float-accumulation error in billing. (design/157 B19: the
639
+ * float-USD twin was removed; divide by 1e6 at the display edge if you need USD.)
640
+ * **ABSENT when any spend was unpriced** (RB-368): the serving model had neither a
641
+ * `RunnerDeps.pricing` entry nor a `Model.cost` declaration — an unpriced run reports NO cost
642
+ * rather than a fabricated 0, so "no price table" stays distinguishable from "declared free"
643
+ * (an explicit all-zero `Model.cost` still reports 0). `costBreakdown` is omitted with it. */
644
+ costMicroUsd?: number;
645
+ nested?: NestedUsage;
646
+ /**
647
+ * Post-task memory-consolidation usage (design/41), when consolidation ran. Its own line item —
648
+ * **kept out of `tokens`/`costMicroUsd` and excluded from the budget gate** — so a completed task is
649
+ * never retroactively flipped to a budget terminal by reconcile cost. `tokens` is 0 when every note
650
+ * took a cheap (no-LLM) path; `applied` counts store mutations the pass made (cheap-path
651
+ * UPDATE/DELETE + applied LLM decisions) so callers can observe reconcile efficacy without re-reading
652
+ * the store. Absent when consolidation was disabled / no-op / produced no notes. Populated
653
+ * asynchronously by whatever engine-plane pass produced it (consolidation is fire-and-forget).
654
+ */
655
+ memory?: {
656
+ tokens: number;
657
+ costMicroUsd: number;
658
+ applied: number;
659
+ };
660
+ /**
661
+ * design/100 §E12 — the prompt-suggestion pass's usage, when it ran. Same budget-excluded side-observable
662
+ * treatment as {@link memory}: **kept out of `tokens`/`costMicroUsd` and excluded from the budget gate**, so
663
+ * the (fire-and-forget, post-completion) suggestion pass can never retroactively flip a done task to
664
+ * a budget terminal. Absent when `suggestNextPrompts` was off / produced nothing. Populated asynchronously —
665
+ * see {@link TaskStream.suggestions}.
666
+ */
667
+ suggestions?: {
668
+ tokens: number;
669
+ costMicroUsd: number;
670
+ };
671
+ /**
672
+ * TB telemetry B2: counters for the engine's wall-clock/deadline mechanisms, so a
673
+ * bench/monitoring consumer can judge from the RESULT whether they actually engaged (refute-130
674
+ * lesson: "did P1/P2 fire" was unanswerable from artifacts). Present only when at least one
675
+ * mechanism engaged this leg; absent = none did (byte-compatible).
676
+ */
677
+ mechanisms?: {
678
+ /** design/132: the natural-end final-verification reminder was injected this run. */
679
+ finalVerifyInjected?: true;
680
+ /** oracle-grounding gate (opus LOW 复审, additive): HOW MANY final-verification injections
681
+ * fired this run — 1 = the R9 one-shot only, 2 = R9 + the grounding re-entry (hard cap).
682
+ * Present iff `finalVerifyInjected` is (the boolean stays for compatibility). */
683
+ finalVerifyInjections?: number;
684
+ /** design/133: number of attachment blocks injected this run — turn-boundary bundles
685
+ * (todo/task reminders, changed-files notes, plan-mode re-reminders, listing drift frames;
686
+ * each bundled block counts once) PLUS the [c209-C] first-frame listing deliveries riding the
687
+ * first user message (agent_listing / skills_listing — counted only when the first prompt was
688
+ * actually sent, R2 C5: a hook-blocked / budget-rejected run counts zero). */
689
+ attachmentsInjected?: number;
690
+ /** design/164: how many LIMIT-APPROACH notices were injected this run (0..2 — the
691
+ * "start converging" frame and the "deliver now" frame, each one-shot). Absent when none fired,
692
+ * which is also the answer for a task that armed no limit axis at all. */
693
+ approachNoticesSent?: number;
694
+ /** Repetition telemetry (2026-07-10 observability): brain-side degenerate-repetition
695
+ * stream CUTS this run (each pairs with an `output.degenerate`-classified turn). */
696
+ repetitionCuts?: number;
697
+ /** Repetitions that landed in a detection window but were SPARED by a 2e1c161 structural
698
+ * allowance (code-line shape / divider run) — the raw material for judging the allowance. */
699
+ repetitionSpared?: number;
700
+ /** Per-event details (turn + rule + period/reps + a ≤120-char segment sample), capped at 20
701
+ * entries — `repetitionCuts`/`repetitionSpared` keep the true totals past the cap. */
702
+ repetitionEvents?: Array<{
703
+ turn: number;
704
+ action: "cut" | "spared";
705
+ rule: "char-run" | "unit-loop";
706
+ period: number;
707
+ reps: number;
708
+ segment: string;
709
+ }>;
710
+ /** design/319 (B ticket, G9② observation seat) — reminder-disclosure trigger counts for the
711
+ * leg, keyed `<outlet>.<form>`: outlets `read` / `notebook` / `pdf` / `mcp` / `webFetch` /
712
+ * `webSearch`; forms
713
+ * · `bare` (bare-form trailer appended — reminder-shaped text without this session's mark),
714
+ * · `marked` (marked-form trailer — never throttled),
715
+ * · `bare_throttled` (a bare trailer suppressed by the 60s per-key window),
716
+ * · `defused` (an MCP/web segment's exact-mark bytes were rewritten — the lane's one sanctioned
717
+ * byte change, always paired with a `marked` disclosure),
718
+ * · `envelope` (text shaped like one of the engine's OTHER authority envelopes — the DISCLOSED
719
+ * subset is `task-notification` / `new-diagnostics` / `user_memory` / `skills` /
720
+ * `total_tokens` / `instruction-files`; `scope` is
721
+ * fenced but not disclosed, since `<scope>…</scope>` is also an ordinary build-file element.
722
+ * That family carries no mark, so its sentence is positional rather than byte-testable. It
723
+ * rides ON the reminder copy when both families hit, so `envelope` can be bumped alongside
724
+ * `marked`/`bare` on one result),
725
+ * · `envelope_throttled` (an envelope sentence suppressed by its OWN 60s window — the two
726
+ * families throttle independently, so one may emit while the other reports suppression),
727
+ * · `mark_echo` (observation ONLY, never a disclosure: the session's mark VALUE appeared in
728
+ * external bytes with no reminder-shaped tag around it, so the tag-grammar detector cannot
729
+ * reach it. Nothing is rewritten and no trailer is appended; it is an upper bound on model
730
+ * exposure for the lanes that do not defuse).
731
+ * Present only when ≥1 key is non-zero. The map is OPEN by type — fold by key rather than
732
+ * switching on a closed set. This is the D-4/D-6 re-ruling data (defuse/trailer widening to
733
+ * Read/Bash/Grep): a reading, never a gate. */
734
+ reminderDisclosures?: Record<string, number>;
735
+ };
736
+ /**
737
+ * design/91 — **human-review burden** (design/89 §2.4 C2 axis). The wall-clock time a task spent waiting
738
+ * on a human at an approval gate, plus the count, bucketed by `gate.kind`. Both human-review paths are
739
+ * captured: the **synchronous** `onAsk`/`resolveAsk` await (`t_return − t_call`) and the **durable**
740
+ * suspend→resume wait (`resumedAt − checkpoint.suspendedAt`, including the human's offline time); a
741
+ * multi-leg suspend/resume chain **accumulates across legs**. All timestamps come from {@link RunnerDeps.now}.
742
+ *
743
+ * **NOT an LLM cost (load-bearing, design/91 §1):** human-review time is wall-clock latency, not token
744
+ * spend — it is **NEVER folded into `costMicroUsd`, `costBreakdown`, or the budget gate** (the same
745
+ * budget-excluded side-observable treatment as {@link memory}). Adding human seconds to token µUSD is the
746
+ * design/89 §2.1 "apples + oranges" trap; the supervisor Pareto frontier keeps `(token cost, human seconds,
747
+ * correctness)` as three SEPARATE axes. Absent (`undefined`) when the task hit no approval gate OR no clock
748
+ * is injectable to measure — byte-compatible with a task that never paused for a human.
749
+ */
750
+ humanReview?: {
751
+ /** Number of human decisions awaited (each synchronous `resolveAsk` return + each durable resume of an
752
+ * approval suspend). Equals `gates.length`. */
753
+ count: number;
754
+ /** Cumulative wall-clock the approval gates were alive (ms), summed across all legs. Equals `Σ gates[].waitMs`. */
755
+ totalWaitMs: number;
756
+ /** One entry per human decision, in order. `kind` = the gate's discriminant (`human` /
757
+ * `irreversible_ask` for durable; `human` for a synchronous ask). `decision` = the adjudication when
758
+ * known (`allow`/`deny`). design/99 MF-24: `toolName` = the gated tool; `toolArg` = a
759
+ * SHORT, SECRET-SCRUBBED one-line summary of its input (the same `primaryActivityArg` boundary as the MF-W
760
+ * activity arg) — the permission-denial ledger's "denied: Bash(rm …)" display. `toolArg` is set on the
761
+ * SYNCHRONOUS-ask path (the input is in scope); a durable-resume gate carries `toolName` only (its input is
762
+ * not threaded onto the persisted gate — a documented follow-on). */
763
+ gates: Array<{
764
+ kind: string;
765
+ waitMs: number;
766
+ decision?: string;
767
+ toolName?: string;
768
+ toolArg?: string;
769
+ }>;
770
+ };
771
+ /**
772
+ * design/80 D-E-core: a thin FINANCE TAXONOMY of the LLM-derived costs the engine actually prices,
773
+ * decomposing the task's spend into report categories (derived from existing cost sources — no new
774
+ * persisted structure). **CORE = LLM-token-derived ONLY** — the deployment SERVICE adds the infra axes it
775
+ * owns (tool-call / sandbox-walltime / egress; it has the k8s cost data) and composes them with this for
776
+ * the supervisor surface. Each category is ≥ 0 and the LLM parts reconcile to the independently-summed
777
+ * LLM total: `llmRootMicroUsd + compactionMicroUsd === costMicroUsd` and the fully-reconciled
778
+ * spend is `costMicroUsd + nested.costMicroUsd` — `costMicroUsd` deliberately EXCLUDES nested
779
+ * subagent cost (it lands in `nested`, see assemble-result.ts), so `nestedSubagentMicroUsd` sits
780
+ * OUTSIDE the costMicroUsd identity, not inside it (measured correction: the previous wording
781
+ * `llmRoot + nested + compaction === costMicroUsd` only held when nested was 0).
782
+ * (`memoryConsolidationMicroUsd` is a separate, budget-excluded line, permanently 0 since the
783
+ * runner-integrated consolidation pass was retired — design/157 B19 — and NOT part of any identity).
784
+ * Present whenever stats are assembled, EXCEPT when `costMicroUsd` is absent for unpriced spend
785
+ * (RB-368) — the taxonomy would partition a fabricated total, so it is omitted together with it.
786
+ */
787
+ costBreakdown?: {
788
+ /** Root-agent LLM cost (micro-USD) = `costMicroUsd − compactionMicroUsd` (the sub-category folded into
789
+ * it). NOT minus nested — nested cost is never folded into `costMicroUsd` (it lands in `nested`).
790
+ * ⚠️ APPROXIMATE on a multi-slice `resource_limit` RESUME (design/80 D-E-core): the
791
+ * durable resource ledger persists only the AGGREGATE prior spend, not its compaction subtotal, so
792
+ * prior slices' compaction folds in HERE rather than into `compactionMicroUsd`. The TOTAL (`costMicroUsd`)
793
+ * stays exact; only this llmRoot-vs-compaction split is per-leg-approximate across resource slices. */
794
+ llmRootMicroUsd: number;
795
+ /** Delegated sub-agent (nested) LLM cost (micro-USD) = `nested?.costMicroUsd ?? 0`. */
796
+ nestedSubagentMicroUsd: number;
797
+ /** Post-task memory-consolidation LLM cost (micro-USD). Permanently 0 since design/157 B19 retired
798
+ * the runner-integrated consolidation pass (kept: public stats contract; an engine-plane re-mount —
799
+ * design/138 §7 — would fill it). A separate, budget-excluded line. */
800
+ memoryConsolidationMicroUsd: number;
801
+ /** design/100 §E12 — prompt-suggestion pass LLM cost (micro-USD) = `suggestions?.costMicroUsd ?? 0`; filled
802
+ * asynchronously after the (post-completion) pass (0 until then). A separate, budget-excluded line. */
803
+ suggestionsMicroUsd?: number;
804
+ /** Within-task compaction LLM cost (micro-USD) — part of `costMicroUsd`, so subtracted from root.
805
+ * ⚠️ See {@link llmRootMicroUsd}: under-reports prior slices' compaction on a multi-slice resource resume. */
806
+ compactionMicroUsd: number;
807
+ };
808
+ };
809
+ }