@sema-agent/core 7.6.0 → 7.6.2

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 (78) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/agents/agent-transcript-tool.d.ts +2 -2
  3. package/dist/agents/cascade.d.ts +2 -3
  4. package/dist/agents/repair-loop.d.ts +2 -2
  5. package/dist/agents/retain-ledger.d.ts +2 -3
  6. package/dist/agents/send-message-tool.d.ts +2 -2
  7. package/dist/agents/session-util.d.ts +2 -2
  8. package/dist/agents/subagent.d.ts +3 -4
  9. package/dist/agents/teacher.d.ts +2 -2
  10. package/dist/agents/team.d.ts +2 -2
  11. package/dist/agents/verify.d.ts +5 -6
  12. package/dist/core/agent-definition.d.ts +172 -0
  13. package/dist/core/agent-definition.js +1 -0
  14. package/dist/core/checkpoint-store.d.ts +8 -4
  15. package/dist/core/delegation-frames.d.ts +298 -0
  16. package/dist/core/delegation-frames.js +21 -0
  17. package/dist/core/engine-notice.d.ts +555 -0
  18. package/dist/core/engine-notice.js +55 -0
  19. package/dist/core/gate-fold.d.ts +12 -0
  20. package/dist/core/gate-fold.js +158 -0
  21. package/dist/core/gate-lanes.d.ts +93 -0
  22. package/dist/core/gate-lanes.js +626 -0
  23. package/dist/core/hands-band.d.ts +134 -0
  24. package/dist/core/hands-band.js +1 -0
  25. package/dist/core/hooks.d.ts +20 -101
  26. package/dist/core/hooks.js +53 -854
  27. package/dist/core/mcp-failure.d.ts +43 -5
  28. package/dist/core/mcp-failure.js +31 -14
  29. package/dist/core/mcp-server-spec.d.ts +217 -0
  30. package/dist/core/mcp-server-spec.js +1 -0
  31. package/dist/core/model-seat.d.ts +99 -0
  32. package/dist/core/model-seat.js +1 -0
  33. package/dist/core/reminder-mint.d.ts +10 -0
  34. package/dist/core/reminder-mint.js +3 -0
  35. package/dist/core/runner/contracts.d.ts +382 -6
  36. package/dist/core/runner/gate-exit.d.ts +177 -9
  37. package/dist/core/runner/gate-exit.js +70 -1
  38. package/dist/core/runner/prepare-caps-and-workflow.d.ts +2 -7
  39. package/dist/core/runner/prepare-delegation-surface.d.ts +2 -7
  40. package/dist/core/runner/prepare-run-refs.d.ts +12 -0
  41. package/dist/core/runner/prepare-run-refs.js +5 -0
  42. package/dist/core/runner/prepare-task.d.ts +2 -2
  43. package/dist/core/runner/runtask.d.ts +4 -71
  44. package/dist/core/runner/runtask.js +18 -6
  45. package/dist/core/runner-deps.d.ts +1416 -0
  46. package/dist/core/runner-deps.js +1 -0
  47. package/dist/core/runtime-caps.d.ts +164 -0
  48. package/dist/core/runtime-caps.js +1 -0
  49. package/dist/core/task-event.d.ts +910 -0
  50. package/dist/core/task-event.js +1 -0
  51. package/dist/core/task-limits.d.ts +110 -0
  52. package/dist/core/task-limits.js +1 -0
  53. package/dist/core/task-result.d.ts +809 -0
  54. package/dist/core/task-result.js +1 -0
  55. package/dist/core/task-spec.d.ts +1370 -0
  56. package/dist/core/task-spec.js +1 -0
  57. package/dist/core/task-stream.d.ts +382 -0
  58. package/dist/core/task-stream.js +1 -0
  59. package/dist/core/tool-spec.d.ts +1174 -0
  60. package/dist/core/tool-spec.js +1 -0
  61. package/dist/core/types.d.ts +26 -7691
  62. package/dist/core/types.js +2 -76
  63. package/dist/core/warm-resume.d.ts +2 -2
  64. package/dist/index.d.ts +2 -1
  65. package/dist/index.js +1 -1
  66. package/dist/orchestration/goal.d.ts +2 -2
  67. package/dist/orchestration/run-spec.d.ts +2 -2
  68. package/dist/orchestration/run-workflow-tool.d.ts +3 -3
  69. package/dist/orchestration/workflow.d.ts +4 -4
  70. package/dist/scenarios/scenario-registry.d.ts +3 -3
  71. package/dist/scenarios/teacher-quickstart.d.ts +2 -2
  72. package/dist/server/http.d.ts +2 -2
  73. package/dist/stores/file/fs-atomic.d.ts +88 -12
  74. package/dist/stores/file/fs-atomic.js +184 -55
  75. package/dist/stores/file/index.d.ts +1 -0
  76. package/dist/stores/file/index.js +1 -0
  77. package/package.json +1 -1
  78. package/test/export-surface.snapshot.json +9 -1
@@ -0,0 +1,555 @@
1
+ /**
2
+ * A structured operator-facing notice ({@link RunnerDeps.onNotice}) — a fact the engine announces that
3
+ * is neither an error nor part of the task result: today, a configured value that was discarded in
4
+ * favor of another (the loud-bad-value discipline's announcement dialect). Structured so a host can
5
+ * FORWARD it to its own user surface instead of losing it in process stderr.
6
+ */
7
+ export interface EngineNotice {
8
+ /** Stable machine-readable family, dot-namespaced. Current families:
9
+ * - `"config.autocompact_window_clamped"` (design/374 slice 1b) — a model declared
10
+ * `autoCompactTokens` ABOVE its physical window; the trigger-side geometry was clamped to the
11
+ * physical window (an autocompact window only ever lowers the trigger) and the bad value is
12
+ * announced once per prepared task; `detail: { modelId, declaredAutoCompactTokens,
13
+ * physicalWindow, sessionId, runId }` — `runId` (#499) is what makes the "once per prepared
14
+ * task" claim checkable: the session id beside it is shared by every run of the session, so two
15
+ * runs clamping the same model used to mint byte-identical lines.
16
+ * - `"config.env_timeout_discarded"` — a Bash timeout knob (option or env) held a value that is not
17
+ * the value in force; `detail: { knob, raw, usedMs }`.
18
+ * - `"config.materialize_env_discarded"` — `SEMA_TOOL_MATERIALIZE_STRATEGY` held a value outside the
19
+ * closed set in a seat where it is not in force; `detail: { raw, specStrategy? }`.
20
+ * - `"tool_result.offload_put_failed"` (#167) — a clear-with-offload persist's fire-and-forget put
21
+ * failed; THIS attempt stored nothing (the failure arm reports, it never re-inserts under the
22
+ * ref) — an earlier attempt of the same idempotent re-put may already have stored the row, so
23
+ * the notice claims a failed write, not an empty ref;
24
+ * `detail: { ref, sessionId, cause, occurrenceId }`.
25
+ * Per-occurrence, not per-process-deduplicated: each failed write is a distinct fact — and
26
+ * `occurrenceId` (#495, uuidv7 minted at the failure) is what lets a consumer act on that:
27
+ * `ref` names the ROW, and the same ref is attempted again on every later re-clear, so two
28
+ * genuine losses of one ref were byte-identical notices. Dedup key: (code, occurrenceId).
29
+ * - `"workflow.governance_key_stripped"` (#235) — the fields of an LLM-authored workflow `agent(spec)`
30
+ * that did NOT cross the governed default-deny whitelist: an unrecognized/control-plane field, or a
31
+ * `readFace` value that is not the containment-tightening one. The spawn PROCEEDS on the deployment
32
+ * baseline (the strip is not a refusal in this window), which is why the drop is announced;
33
+ * `detail: { total, stripped: [{ key, reason }], omitted?, occurrenceId }`, the rendered key list
34
+ * bounded in count and length because the names come from the untrusted script. One aggregated
35
+ * notice per governed child build, not de-duplicated across builds: each spec is a distinct fact —
36
+ * `occurrenceId` (#495, uuidv7 minted at the announcement) carries that claim to the consumer,
37
+ * which otherwise saw two builds of the same spec as one repeated notice. Dedup key:
38
+ * (code, occurrenceId).
39
+ * - `"workflow.agent_option_ignored"` (#505 ①) — the OPTIONS-object twin of the row above: keys of
40
+ * `agent(spec, opts)`'s SECOND argument that were not applied (unrecognized, or a known option
41
+ * whose value its type check rejected — `label: 42`). The spawn PROCEEDS without them, which is
42
+ * why the drop is announced. A separate code because the two membranes accept different key
43
+ * sets and the message names its own. `detail: { total, ignored: [key], omitted?, occurrenceId }`,
44
+ * same bounding and same per-call (never de-duplicated) semantics as the spec-side row; dedup
45
+ * key: (code, occurrenceId). NOT the whole family: a SPEC field written on the options object
46
+ * (`model`, `modelName`, `thinking`, …) is REFUSED at the membrane instead — a silently ignored
47
+ * `model` runs a different agent than the author asked for.
48
+ * - `"mcp.revocation_probe_failed"` (design/338) — the deployment's `mcpRevocations.isRevoked`
49
+ * probe threw; MCP dispatch FAILS OPEN (revocation is a tightening face) and this announces
50
+ * once per materialization (a resume re-materializes and may announce again).
51
+ * `detail: { message, runId }` — still no sessionId (a deployment wiring fact, not
52
+ * session-attributed); `runId` (#499) makes the once-per-materialization claim CHECKABLE without
53
+ * attributing the line to a conversation, an opaque invocation id saying which materialization
54
+ * rather than whose. `"operator"` audience by
55
+ * its explicit {@link NOTICE_AUDIENCE} row (#433 made the registry total over the catalog —
56
+ * no engine-minted code is audience-defaulted any more). The refusal itself
57
+ * (`mcp.server_revoked`) is a tool RESULT code, not a notice.
58
+ * - `"config.models_swapped"` — `Runner.swapModels` replaced the model catalog generation
59
+ * (zero-restart model switching). `detail: { models, tiers, occurrenceId }` — key COUNTS only,
60
+ * never the
61
+ * catalog itself. In-flight tasks finish on the models they resolved at prepare (natural
62
+ * snapshot); every later prepare resolves against the new generation. A failed swap (illegal
63
+ * tier binding) throws atomically and mints nothing.
64
+ * `occurrenceId` (#495, uuidv7 minted at the announcement): the verb carries no run and no
65
+ * session, and the payload is a pair of counts, so two swaps of the same shape were byte-
66
+ * identical lines. Dedup key: (code, occurrenceId).
67
+ * - `"route.fallback_to_primary"` (key↔URL pairing, `src/brain/route-adjudicator.ts`) — a
68
+ * DERIVED-leg model (role/tier/system-default resolution, never a caller-explicit one) failed
69
+ * the pairing pre-flight and the seat fell back to the primary model instead of sinking the
70
+ * task; the notice is the loud half of that swap.
71
+ * `detail: { seat, from, to, cause, fixHint, sessionId?, runId? }` — `cause` is the refusal code
72
+ * (`route.credential_mismatch` / `route.credential_missing`); `sessionId` (#433, additive)
73
+ * rides when the adjudicating seat knows its session, and `runId` (#499, additive) names the
74
+ * INVOCATION whose seats were being resolved — both correlation keys only, the audience
75
+ * stays `"operator"` (see the registry's entitlement-not-routability note). Two runs of one
76
+ * session falling back on the same seat used to mint byte-identical lines; `runId` is what
77
+ * tells one task falling back twice apart from two tasks falling back once each.
78
+ * Explicitly-named models never mint this: they refuse at the brain's request gate instead.
79
+ * - `"route.base_url_changed_key_unchanged"` (key↔URL pairing) — `Runner.swapModels` moved a
80
+ * same-name entry's `baseUrl` while its Model-visible credential half (auth-bearing headers)
81
+ * did not change: legal (a provider changing domains), but worth one loud line — if the
82
+ * PROVIDER changed, the credential reference needs the same update. Advisory only, never a
83
+ * refusal; per-model-hook credentials are invisible to the catalog swap, so the notice is
84
+ * deliberately conservative (it may fire when a hook-side credential DID change in lockstep).
85
+ * One aggregated notice per swap; `detail: { entries: [{ modelId, from, to }], total,
86
+ * occurrenceId }` (rendered list bounded, total always exact). `occurrenceId` (#495, uuidv7
87
+ * minted at the announcement, independent of the `config.models_swapped` line's own): the
88
+ * bounded render makes two swaps moving the same first entries byte-identical.
89
+ * Dedup key: (code, occurrenceId).
90
+ * - `"config.read_face_deployment_clamped"` (#237) — a deployment-wide `readFace: "open"` is not
91
+ * in force beside a read-only (verifier) mount: it clamps to "roots" without throwing
92
+ * (stricter-wins; the clamp verdict stands, only its occurrence was undisclosed). Announced
93
+ * once per `onNotice` SINK at the prepare station (the verifier lane would otherwise repeat it
94
+ * every spawn; the sink — not the deps object — is the unit because the Runner rebuilds its
95
+ * deps and `runTask` builds a Runner per call), unwired console arm once per process;
96
+ * a library-direct `createHandsToolkit` mount announces at toolkit creation (one per mount,
97
+ * through the band-local `HandsToolkitOptions.onNotice` seat, absent ⇒ `console.warn`);
98
+ * `detail: { seat, declared, inForce, cause }`.
99
+ * - `"config.tool_model_gate_removed"` (design/277) — the model gate trimmed default-mounted
100
+ * scaffold entries from a task's roster. One notice per (model, class), de-duplicated once per
101
+ * `onNotice` SINK on the (modelId, class, canonical sorted removed-name set) line (same unit
102
+ * as `config.read_face_deployment_clamped`: two deployments hosted in one process each hear
103
+ * their own trim; unwired console arm once per process) — same class with a different removal
104
+ * shape is a distinct fact and announces again;
105
+ * `detail: { modelId, gateClass, removed, restore }` (`restore` names the three valves).
106
+ * - `"config.tool_model_gate_unknown_class"` (design/277) — a `ToolSpec.modelGate` tag names a
107
+ * class the merged gate table has no row for: the tag is inert (fail-open — the tool stays
108
+ * mounted) and this is its loud half (a tag typo must not silently become "never gated" with
109
+ * nobody told). Once per `onNotice` sink per class (console arm once per process);
110
+ * `detail: { gateClass }`.
111
+ * - `"config.tool_model_gate_env_invalid"` (design/277, the NOTICE dialect of the same fact the
112
+ * refusal code carries) — `SEMA_TOOL_MODEL_GATE` holds a value outside `on|1|true|off|0|false`
113
+ * in a seat where it is NOT in force (nothing this prepare would gate): announced once per
114
+ * `onNotice` sink per value (console arm once per process) instead of lying in wait; where the
115
+ * value IS in force the prepare refuses with the same code as `TaskResult.errorCode` (one
116
+ * fact, one code, two loudness dialects); `detail: { raw }`.
117
+ * - `"config.durable_gate_unavailable"` — the per-principal `forceDurableGate` entitlement is in
118
+ * force on a leg whose deployment wired NO checkpoint store. The entitlement is a mandate
119
+ * ("every interactive ask of this principal gates durably"), and a mandate with no park
120
+ * facility cannot be honored: no ask of the leg can suspend, so each one resolves on the LIVE
121
+ * chain — a live approver / question face answers in-stream, and with none the ask is denied
122
+ * fail-closed — and NO durable approval record is written. The ask routing itself is not
123
+ * changed by the notice (a store-less leg never could park); the notice is the loud half of a
124
+ * posture that used to degrade silently, disclosed before the first ask of the leg. Audience
125
+ * `"user"` (the person whose asks will not be recorded is entitled to that; the operator hears
126
+ * it through the sink as always). Once per prepared task leg — a resume or a delegated child
127
+ * is its own leg and says so again; `detail: { sessionId, runId, principal?, cause, liveApprover,
128
+ * liveQuestionFace }` — `cause` is `"no_deployment_store"` (the deployment wired none) or
129
+ * `"task_store_disabled"` (`TaskSpec.checkpointStore: "disabled"`, the per-run off switch, on a leg
130
+ * whose deployment may well have a store: the fix is the task's); `liveApprover` /
131
+ * `liveQuestionFace`: whether a live seat will EFFECTIVELY answer the leg's permission asks /
132
+ * questions in-stream (a delegated child under an inherited content mandate reads `false`
133
+ * even with a face wired — the marker withholds the question), or the fail-closed deny will.
134
+ *
135
+ * - `"task.user_steer_undrained"` / `"task.user_followup_undrained"` (#259) — user steers /
136
+ * follow-ups whose receipts said "queued" were still in their queue at agent_end: the run
137
+ * ended before any turn could drain them. One notice PER family (a consumer routing on `code`
138
+ * alone must not mistake a stranded follow-up for a stranded steer — same split as the settled
139
+ * frame's two count keys). They are NOT redelivered (a steer aimed at a finished run must not
140
+ * fire at the next one — unlike ENGINE notes, which pend per session); the loud half of the
141
+ * #257 contract's "accepted = enqueued, not consumed" sentence;
142
+ * `detail: { steer, taskId?, sessionId?, runId? }` / `{ followUp, taskId?, sessionId?, runId? }`
143
+ * (`sessionId` = #433's routing half for these two `"user"`-audience rows; `runId` = #499's
144
+ * per-run identity, the key this account is actually about — see
145
+ * {@link undrainedUserInputNotices}; all omitted when the caller has none, never fabricated).
146
+ * Per-run, at most once per family (the terminal sweep is a single site) — which is exactly why
147
+ * `taskId`/`sessionId` cannot key it: both answer the same value for two runs of one session,
148
+ * so two runs each stranding one steer folded into one indistinguishable row.
149
+ * **#389 (two corrections).** ① The family now fires on the INTERRUPT path too: `abort()` used
150
+ * to empty both queues before agent_end could count them, so the one loss path an operator most
151
+ * needs to hear about was the one path that stayed silent. ② On a DURABLE PARK the verdict is
152
+ * settled at the run tail instead of at agent_end, because the tail first carries the still-
153
+ * undelivered input onto the committed checkpoint's parked-steer queue (the resume delivers it)
154
+ * — what is announced is the remainder that could NOT be carried, so a fully-migrated park says
155
+ * nothing at all and a queue-full / no-longer-pending row still says exactly what was lost.
156
+ *
157
+ * - `"task.turn_interrupted"` (design/373) — a running turn was ACTUALLY CUT at a manufactured
158
+ * boundary: the in-flight provider stream / tool batch was aborted (finished tool calls keep
159
+ * their real results, never-started ones settle as paired interrupted results, the interruption
160
+ * marker lands). **TWO mint lanes, one code** — the CUT is lane-independent, so it is one fact
161
+ * with one spelling, and `detail.cause` is the arm discriminator (a consumer that must tell
162
+ * them apart reads that key; one that only cares "a turn was cut" reads neither):
163
+ * · the STEER-NOW lane (design/373) — a caller-provenance steer with `priority: "now"` cut
164
+ * the turn and the run CONTINUES with the steer at the queue head.
165
+ * `detail: { inputId, sessionId, runId, actorId?, taskId? }`, NO `cause` key — `inputId` is
166
+ * the steer's own correlation key (the `human_input` frame's id) and `actorId` the caller's
167
+ * asserted identity when one rode the steer.
168
+ * · the BARE-HALT lane (#504, {@link TaskStream.halt}) — the person's Esc-form stop cut the
169
+ * turn and the run does NOT continue: it collects to a clean, resumable
170
+ * `status:"completed"` ending carrying {@link TaskResult.haltedByUser}.
171
+ * `detail: { cause: "user_halt", sessionId, runId, taskId? }` — no `inputId` and no
172
+ * `actorId`, because no text entered the model and the verb carries no caller identity. A
173
+ * consumer keying `detail.inputId` off this row must treat it as ABSENT on this lane.
174
+ * Fence tension (design/384 slice 2): this notice asserts THE CUT only — when the cut
175
+ * raced a durable park whose commit was already in flight, the gate's durable leg may
176
+ * still collect to `suspended` (park wins, row redeemable, no `haltedByUser`); the notice
177
+ * stands beside that terminal without contradiction, because a seat really was cut.
178
+ * In both lanes: one notice per REAL cut (a `now` that found nothing in flight or whose frame
179
+ * already rode the imminent boundary, and a halt with nothing in flight, announce nothing — no
180
+ * false interrupt claims); `runId` (#499) is the INVOCATION that was cut (the other two ids are
181
+ * shared by every run of the session); every cut turn counts toward `maxTurns`, so an interrupt
182
+ * storm's cost is bounded and each of its cuts is on the record. Audience `"user"`: the person
183
+ * who forced the boundary is the one entitled to see that it landed.
184
+ * RETIRED here (BREAKING, named): `"task.injection_priority_unimplemented"` — the ladder is
185
+ * implemented, so the unhonored-knob disclosure it carried has no referent; consumers must
186
+ * judge ladder support by VERSION, never by that code's absence.
187
+ *
188
+ * - `"task.halt_unconsumed"` (design/384 slice 1) — the user's stop verb ({@link TaskStream.halt})
189
+ * was ANSWERED (not refused) while the run's ending was already owned by its own abort: nothing
190
+ * was cut and nothing was stopped by the halt, the run's own ending stands, and the result will
191
+ * NOT carry `haltedByUser` for it (the attribution seat never signs someone else's stop). This
192
+ * notice is the halt's only trace on that arm — without it the verb receipt (`{turnCut:false}`)
193
+ * was the sole record and the caller could not tell "my stop did nothing because the run beat
194
+ * me to ending" from "my stop is being honored at the next boundary". Minted once per such verb
195
+ * call (each arrival is a distinct fact); `detail: { sessionId, runId, taskId? }` — `runId` is
196
+ * the run whose ending the halt failed to claim. Audience `"user"` (the person who pressed
197
+ * stop). A REFUSED halt (typed `steering.not_running` throw) mints nothing: the refusal itself
198
+ * is the loud answer.
199
+ *
200
+ * - `"task.late_approval"` (design/384 slice 1) — a synchronous ask approval was NOT CONSUMED:
201
+ * a run or turn interrupt released the wait (the `resolveAsk` race arm detached the approver's
202
+ * promise; an unconsumed resolve is the approver releasing its wait, never a verdict), so the
203
+ * tool did NOT run and the approval was not honored. The notice asserts non-consumption ONLY —
204
+ * never an arrival order: an approval and an abort settling close together read as the abort
205
+ * (the resolver's documented one-directional residual — the engine never claims a person
206
+ * decided something it cannot show they did, and it equally never claims which came first).
207
+ * Minted only for a value that reads as an APPROVAL — an unconsumed
208
+ * deny/refusal is an ordinary release and leaves no trace, and an unconsumed REJECTION goes to
209
+ * `onError(phase:"hook")` instead (a failing callback, not an answer). One notice per
210
+ * unconsumed approval; `detail: { toolName, toolCallId, sessionId, runId, taskId? }`. Audience `"user"`
211
+ * (whoever answered the card is the one entitled to hear the answer ran nothing); a host may
212
+ * forward it on its own wire.
213
+ *
214
+ * - `"classifier.denial_limit"` (#548, CC 2.1.250 `FO`/"too many classifier denials in headless
215
+ * mode") — the auto-mode classifier's DENIAL LIMIT was reached (3 consecutive blocks, or 20 in the
216
+ * run; `RunnerDeps.autoMode.denialLimit`) and the fallback ask it turns into had NO approver to go
217
+ * to: none wired, a blanket `onAsk:"allow"` (refused for a real-approval ask), or an approver that
218
+ * reported unavailable with no durable park to take it. The run was STOPPED — `TaskResult.status`
219
+ * `"failed"`, `errorCode` the same code, `errorMessage` the limit sentence — rather than kept
220
+ * spending on a model the classifier denies without end; the triggering call's own result is the
221
+ * deny that stood. Once per run (the first headless fallback owns the terminal);
222
+ * `detail: { sessionId, runId, toolName, toolCallId, consecutive, total, limit }` — `limit` is
223
+ * `"consecutive"` | `"total"`, the bound that tripped. Audience `"user"` (the person whose run
224
+ * ended is the one entitled to hear why, and to review the transcript the sentence points at).
225
+ * With an approver wired the same bound mints NO notice — the fallback ask reaches the person
226
+ * instead (`AskRequest.denialLimitFallback`).
227
+ *
228
+ * - `"steering.parked_input_blocked"` (design/373 §4.3) — a PARKED steer entry was withheld when
229
+ * a resume redelivered it (any resume kind that drains parked steers — wake included) by the
230
+ * deployment's `userPromptSubmit` screen (block verdict, or a fail-closed non-answer/crash):
231
+ * the row was consumed with the checkpoint, the frame never reached the model, and the resume
232
+ * itself proceeds (a blocked instruction must never wedge a wake). The lifecycle half of the
233
+ * same fact is the entry's own `human_input` account with `delivery: "blocked"` — the two share
234
+ * the inputId, so "accepted at park, screened at redelivery" is auditable end to end. Audience
235
+ * `"user"` (the person whose instruction was withheld is the one entitled to re-issue it);
236
+ * `detail: { inputId?, sessionId, runId, taskId? }` — `inputId` is the parked entry's stored
237
+ * correlation key (OPTIONAL: a row parked without one exists), and `runId` (#499) names the
238
+ * resume leg that withheld it, so a session resumed twice does not report two withholdings as
239
+ * one.
240
+ *
241
+ * - `"memory.session_polluted"` (design/178 §3, #324a; message mode-aware since design/336) —
242
+ * this session's memory crossed into the one-way externally-exposed state (a tool classified
243
+ * as an external content source was invoked, directly or through a delegated child). Under
244
+ * `memoryProvenance: "carry"` (default) the message states the tag-admit semantics (ordinary
245
+ * writes commit with an origin marker; instruction-form files are withheld); under `"off"` it
246
+ * keeps the pre-336 sentence — writes no longer eligible for the long-term library. The
247
+ * notice states that VERDICT plus what a harvest does when it collects — it does
248
+ * not promise quarantine, because a `writeScope`-null layering and a declared-unavailable
249
+ * session both reach the zero-admission harvest arm (nothing collected ⇒ nothing captured), and
250
+ * a mark landing after the pre-commit pollution read leaves already-committed rows to the
251
+ * challenge sweep; what a harvest actually contained is the other code below. The challenge
252
+ * clause is a policy statement, not a receipt — a sweep that cannot run announces
253
+ * `"memory.challenge_sweep_failed"` on the engine's incident seat. Announced ONCE PER
254
+ * SESSION, at the mark seat: a repeat mark inside the process and a resumed session whose
255
+ * durable marker already exists stay quiet (the state is one-way, so a second line would carry
256
+ * no new fact); a pollution state that cannot be read announces rather than assuming it was
257
+ * already said. `detail: { reason, sessionId? }` — `reason` names the invoked tool and is
258
+ * neutralized/length-bounded (the name comes from the host/protocol roster).
259
+ * - `"memory.harvest_quarantined"` (design/178 §3, #324a) — a polluted session's harvest ran its
260
+ * containment: `count` ENTRY files written or changed in the session were withheld from the
261
+ * library, `moved` of them were physically moved into the control-plane quarantine directory,
262
+ * and `escalated` of them carry a `HarvestReport.quarantineFailures` row (a failed capture, a
263
+ * failed removal, or a file tombstoned in place — which counts as moved AND escalated, so the
264
+ * two numbers are read off the report's rows, never subtracted from each other). One notice PER
265
+ * HARVEST that withheld at least one entry file (a checkpoint harvest and the terminal harvest
266
+ * are distinct facts), never minted for a clean session; `detail: { count, moved, escalated,
267
+ * reportId?, reason?, sessionId? }` (`reason` absent ⇔ the pollution marker could not be re-read
268
+ * at report time — the withheld count stays true either way). `reportId` (#479) is the minting
269
+ * harvest's own occurrence identity ({@link import("./memory-engine/types.js").HarvestReport}
270
+ * `.reportId`, uuidv7): two harvests whose counts and paths coincide carry DISTINCT ids, a
271
+ * durable replay of the same notice carries the SAME id, and the hold family below shares this
272
+ * harvest's id — dedup on `(code, detail.reportId)`, never on `reportId` alone (absent only for
273
+ * a hand-built report; every engine-minted report carries it). The formerly registered index-only gap is
274
+ * CLOSED under `memoryProvenance: "carry"` (design/336 §6.3, #331): the mint condition reads
275
+ * `HarvestReport.containment`, so a containment whose only act was the derived-index rollback
276
+ * announces with `count: 0` and `detail.indexRolledBack: true`; under `"off"` the pre-336
277
+ * count>0 condition (and its silence on index-only containment) is kept byte-level.
278
+ * - `"memory.hold_opened"` / `"memory.hold_released"` / `"memory.hold_disposed"` (design/336
279
+ * §4/§6.3) — the instruction-hold lifecycle, derived from the same structured
280
+ * `HarvestReport.containment` signal (hold seats are only ever filled under
281
+ * `memoryProvenance: "carry"`): a PENDING session's instruction-form entry files were captured
282
+ * off the model-visible plane (`hold_opened`, `detail: { count, paths, reportId?, sessionId? }`
283
+ * — paths neutralized/length-bounded); previously held entries re-walked the full gate set and
284
+ * committed after their writer session settled clean or a host valve released them
285
+ * (`hold_released`, same detail shape); held entries moved to control-plane quarantine
286
+ * (`hold_disposed`, `detail.disposed: [{ path, terminal }]` with the closed terminal set
287
+ * dirty/expired/conflict/capture_lost/discarded — an `expired` terminal is explicitly a
288
+ * TIMEOUT, not a conviction, and the message names `resolveHold` as the recovery valve). At
289
+ * most one notice per family per harvest report; wording is factual, never threat-flavored
290
+ * (the design/336 §13-2 model-psyche guardrail). `detail.reportId` (#479) is shared by all
291
+ * three families AND `"memory.harvest_quarantined"` when they derive from the same harvest
292
+ * report — that sharing is the contract (one report, several distinct facts): dedup on
293
+ * `(code, detail.reportId)`; a repeat with the same pair is a replay, a new pair is a new fact.
294
+ * - `"memory.delegation_static_mark_waived"` (design/324, #324 ruling ①) — the deployment set
295
+ * {@link RunnerDeps.memoryDelegationEvidence} to `"attested-only"` and a delegation call whose
296
+ * STATIC tool-face verdict would have marked this session's memory polluted (attestation
297
+ * missing/unknown + face can reach external content) was not marked: the waiver is announced —
298
+ * the explicit dual of `"memory.session_polluted"` for this arm, so the arm has a voice where a
299
+ * mark used to land. MINT-side guarantee only: delivery rides the shared guarded form
300
+ * ({@link deliverEngineNotice} — a wired sink that throws owns that loss, exactly as at every
301
+ * de-duplicating station), and the leg latch is consumed at mint. Minted only when a pollution
302
+ * face is mounted (a
303
+ * recorder-only child's mark is a no-op — nothing is waived there), at most ONCE PER PREPARED
304
+ * TASK LEG (a resume leg may announce again — a new leg's audit stream is a new fact;
305
+ * deliberately no durable once-per-session state), never on the `"static-face"` default, and
306
+ * never for a delivered `"external"` attestation (that mark still lands);
307
+ * `detail: { reason, subagentType?, sessionId?, runId }` — `runId` (#499) is what makes the
308
+ * ONCE-PER-LEG guarantee above checkable, since a resume leg may legitimately announce again and
309
+ * the session id cannot tell the two legs apart; `reason` is the same sentence the waived
310
+ * mark would have carried, neutralized/length-bounded (tool and agent-type names are
311
+ * host/model-controlled inputs).
312
+ * - `"memory.content_class_declared"` (design/378) — an `McpServerSpec` entry carries an explicit
313
+ * {@link McpServerSpec.contentOrigin}: the deployment declared this server's whole tool set to
314
+ * be inside (or explicitly outside) its trust boundary, so the protocol namespace's structural
315
+ * `"external"` no longer decides. The AUDIT line for a trust boundary an operator drew by
316
+ * configuration — which is why it announces on the explicit `"external"` value too: pinning is
317
+ * not a no-op (a declaration beats the `trustedTools` allowlist, so it forecloses the per-name
318
+ * exemption for this server's tools). One line per DECLARED ENTRY per prepared task leg, minted
319
+ * once at preparation and never per call (the declaration's whole effect is that calls stop
320
+ * marking; narrating each call would trade the saved mark for equal noise). Armed on the SAME
321
+ * condition as the classification it talks about — a leg with no engine-memory session and no
322
+ * provenance recorder classifies nothing, so there is no posture to report and no line is
323
+ * minted. `detail: { server,
324
+ * contentOrigin, toolCount, execIsExternalContent?, sessionId?, runId }` — `runId` (#499) names
325
+ * the LEG, which is what "one line per declared entry per prepared task leg" is a claim about:
326
+ * the session id beside it is shared by every leg of the session, so two legs reading one
327
+ * declaration used to mint byte-identical rows. `server` is the
328
+ * host-authored entry name, neutralized/length-bounded; `toolCount` is what this entry actually
329
+ * mounted (0 for a server that failed to connect — the declaration still stands and is still
330
+ * disclosed); `execIsExternalContent` rides the `"execution"` value only and reports the strict
331
+ * knob AS RESOLVED FOR THIS RUN, because that arm's posture depends on it. The message's closing
332
+ * clause is written per value so it can never describe a posture the run does not have. The
333
+ * entry's TRANSPORT is deliberately absent: it is a claim about a mutable host-owned object made
334
+ * long after the dial, and D-10 already rules topology is not evidence of the lineage this line
335
+ * audits — connection facts live on `MaterializedMcp.statuses` instead.
336
+ *
337
+ * - `"memory.capture_opted_out"` (design/383 §2.8) — memory capture is OFF for this session and
338
+ * the switch provably TOOK: nothing the session produces will be committed to the long-term
339
+ * store and its contributions no longer enter consolidation. The user must be able to SEE the
340
+ * switch land rather than infer it from silence, which is why it is announced rather than left
341
+ * implicit. States three facts and an exit, and deliberately overclaims none of them: entries
342
+ * committed BEFORE the opt-out remain in the library and stay retrievable, entries already
343
+ * folded into consolidation products leave those products unaffected, and deleting committed
344
+ * content is a separate host action (the erase valve), never a side effect of this switch.
345
+ * `detail: { sessionId, origin }` — `origin` is the closed set
346
+ * `"declaration" | "flip-verb" | "standing-record" | "delegation-floor" | "fork-origin"`, and
347
+ * the message is written per value (a delegated child's inherited floor and a fork's inherited
348
+ * one-way record say so, rather than claiming the session declared it). Audience `"user"`.
349
+ * Dedup unit: once per SESSION — key `(code, detail.sessionId)`, so the standing-record resume
350
+ * arm folds into the standing entry instead of double-announcing.
351
+ * - `"memory.capture_optout_unpersisted"` (design/383 §2.8) — the OPPOSITE receipt, and a
352
+ * separate code on purpose ("it is off for you" and "it could not be turned off" must never
353
+ * share a spelling): the opt-out could NOT be durably recorded (the control-plane write
354
+ * failed), so it is REFUSED rather than honored in-process under a switch that would silently
355
+ * stop holding at the next resume. Minted only beside that refusal — the throw carrying the
356
+ * same code — never beside a success, and no success notice is minted on this arm.
357
+ * `detail: { sessionId }`. Audience `"user"` (the operator repairs the store, but the
358
+ * entitlement to the fact belongs to whoever asked for the privacy). **TWO ingresses, and they
359
+ * settle differently — a consumer must not read one as the other:**
360
+ * · PREPARE-TIME (a declared/inherited opt-out, before the run starts) — the refusal is
361
+ * TERMINAL: the task never runs and nothing is captured. Dedup unit: once per task, since
362
+ * the refusal it rides settles the task.
363
+ * · the LIVE FLIP VERB ({@link TaskStream.optOutMemoryCapture} mid-run) — the refusal is
364
+ * SCOPED TO THAT CALL: it rejects the verb's promise and nothing else. The run keeps going
365
+ * and capture is still ENABLED (the switch did not take), which is exactly why the notice
366
+ * is owed. Because no record was retained, a retry can fail the same way, so this ingress
367
+ * can announce ONCE PER ATTEMPT — a consumer de-duplicating per task would hide later
368
+ * failures of a switch the person is still trying to throw.
369
+ * Read the run's own outcome (or the verb's rejection) to tell the two apart; this code alone
370
+ * does not say whether anything stopped.
371
+ * - `"memory.consolidation_incomplete"` (design/376, LLM consolidation driver) — a driver run
372
+ * settled without reaching the fixpoint: `detail` names the stop reason (the closed
373
+ * `ConsolidationRunStopReason` set), cycles done, and the residue (write-failure or
374
+ * fuse-refused groups by name). Advisory: committed cycles stand (add-only, never rolled
375
+ * back); the recovery verb is re-running the host driver, which resumes the same pending run.
376
+ * - `"memory.consolidation_withheld"` (design/383 §1.4) — the exposure partition engaged on a
377
+ * consolidation run, so the CLEAN arm's authoring prompts were composed WITHOUT the marked
378
+ * rows: anything this run's clean products say was written without them, and their silence on a
379
+ * topic therefore does not mean the library is silent on it (the withheld rows still stand and
380
+ * are still readable). TWO counts, deliberately separate sentences and separate keys —
381
+ * `withheld` rows were kept out of the clean arm's prompts, and `neverSent` of those were not
382
+ * sent to the consolidation model AT ALL (a lone marked row has no group to fold, so it is
383
+ * withheld from every call). "Withheld from the clean arm" and "never left this machine" are
384
+ * different facts about a person's own data and must not be merged into one comforting number,
385
+ * which is why `sentToProvider` is carried as its own member rather than left to subtraction.
386
+ * `detail: { scope, runId, withheld, neverSent, sentToProvider }` — `scope` is
387
+ * neutralized/length-bounded. Audience `"user"`: the ONE consolidation row that is a reader's
388
+ * fact rather than an operator's (the other five report what a host-driven maintenance run DID;
389
+ * this one reports what the products a person will READ were written without). Minted once per
390
+ * run, beside the receipt — the dedup unit.
391
+ * - `"memory.consolidation_driver_superseded"` is an ERROR code, not a notice (named here only
392
+ * to keep the family's spellings in one place): a concurrent driver invocation took over this
393
+ * scope's run row (attempt fencing) and the losing worker's call THROWS with this `code` —
394
+ * it never crosses {@link deliverEngineNotice}, is deliberately absent from
395
+ * {@link ENGINE_NOTICE_CODES}/{@link NOTICE_AUDIENCE}, and sits in the non-governance
396
+ * disposition table with the error dispositions. The loser MUST NOT retry into the winner's
397
+ * account. A consumer diffing its own table against the catalog must not add a row for it.
398
+ *
399
+ * - `"config.peer_admission_out_of_range"` (#551) — a `RunnerDeps.peerAdmission` field the
400
+ * resolver could not use: outside its legal range, or not a finite number at all (a string off
401
+ * an untyped host's wiring, `NaN`, `Infinity`). THAT FIELD falls back to its own default (never
402
+ * a clamp to the nearest edge, never a whole-config reject) and every other field is unaffected.
403
+ * One notice PER FIELD — two bad fields in one config are two notices — deduped per SINK on
404
+ * `(field, given)` rather than per call (the resolver runs on every SendMessage and every drain
405
+ * round; the fact is about the wiring, not about the call). Per SINK, not per process: two
406
+ * Runners in one process each own an `onNotice`, and a process-global ledger would deliver the
407
+ * fact to whichever resolved first and leave the other silently without its own disclosure. The
408
+ * unwired arm (no `onNotice`: the shared `console.warn` throat) latches per process instead —
409
+ * the console IS one process-wide channel. `detail: { field, given, default,
410
+ * range: [lo, hi], reason }`, where `reason` is `"out_of_range" | "not_a_finite_number"`.
411
+ * Audience `"operator"` (a configuration fact; the fix is the deployment's).
412
+ * - `"config.peer_lane_unmounted"` (design/385 §1.2⑥) — `RunnerDeps.peerDirectory` is wired but
413
+ * the cross-session lane could not mount on this leg: no `mailboxStore`, or one that does not
414
+ * declare `crossProcessSafe: true` (several terminals would share one session box on luck). The
415
+ * seat is inert for the run (no ListAgents, peer addresses refuse with the same reason). Once per
416
+ * prepared leg; `detail: { reason, code, mailboxWired, sessionId, runId }`. Audience `"operator"`
417
+ * (a wiring fact; the fix is the deployment's).
418
+ * - `"peer.inbound_disposition"` (design/385 §1.2④ / §4.6 / §4.4) — a message parked in THIS
419
+ * session's own box was settled at the drain WITHOUT reaching the model. `detail.disposition` is
420
+ * the arm (closed set `PeerInboundDisposition`): `"refused"` (this session's `crossSessionInbound`
421
+ * setting, or a record whose typed fields cannot be rendered canonically), `"admission_refused"`
422
+ * (the drain-stage admission re-check: duplicate / hop loop / runaway — `cause` names it),
423
+ * `"notice_unrouted"` (a notice-kind record the notice face cannot render — no typed `notice`, or
424
+ * a state that does not fit its kind — settled without delivery, never ridden through the
425
+ * peer-message envelope), `"notice_user_only"` (a delivery receipt / idle notice whose sender this
426
+ * session's parity would HOLD: surfaced here for the user, not read by the model — CC's
427
+ * `modelVisible:false` arm; `cause` carries the rendered notice text), or `"held"` — the parity
428
+ * judgment held it: the message ENTERED this session's process-level held queue and was acked
429
+ * from the box (later messages are not blocked behind it), where it is re-judged at every
430
+ * boundary, released or refused with a receipt, and expires WITH a receipt under `dialogExpiry`
431
+ * (see `peer.held_settled`); `cause` is a `PeerInboundHoldCause` — the parity causes, or the
432
+ * drain gate's TRANSIENT `rate_limited` (that one does NOT enter the queue: the message stays in
433
+ * the box and the drain stops at its seq until the bucket refills). Disclosed once per seq per leg,
434
+ * and again when a re-judgment moves a held entry's cause. `detail: { disposition, cause, seq, box,
435
+ * fromSession?, sessionId, runId }`. Audience `"user"`: the session's user is the one who did
436
+ * not receive it.
437
+ * - `"peer.held_settled"` (design/385 §4.4, slice 4) — a message left THIS session's held queue.
438
+ * `detail.settlement` names how: `"delivered"` (a re-judgment or an approval released it into
439
+ * this session's model context), `"refused"` (the setting moved to refuse, or an approval met a
440
+ * refusing policy), `"expired"` (the `dialogExpiry` deadline, an eviction when the 100-entry queue
441
+ * was full, a cancelled review, or a graceful shutdown), `"denied"` (the review face), `"dropped"`
442
+ * (the injection lane refused the released frame). `detail.reason` is the trigger
443
+ * (`deadline | evicted | shutdown | rejudge | approved | denied | cancelled | inject_failed`); the
444
+ * sender was sent the matching receipt whenever the record carried a reply address.
445
+ * `detail: { settlement, reason, seq, box, fromSession?, heldCount, sessionId, runId }`. Audience
446
+ * `"user"`: it is this session's user who held (or was holding) the message.
447
+ * - `"peer.idle_subscription"` (design/385 §5.2, slice 4) — a peer session asked to be told when
448
+ * THIS session next goes idle (`SendMessage … notify_when_idle`). `detail.outcome`: `"recorded"`
449
+ * / `"refreshed"` (a same-requester re-ask; the subscription is one-shot and expires unfired after
450
+ * 12 h), `"full"` (the 32-entry table is full) or `"refused"` (this session's `crossSessionInbound`
451
+ * is `refuse`) — the last two answered the requester with an `unavailable` notice. `detail:
452
+ * { outcome, fromSession, live, seq, box, sessionId, runId }`. Audience `"user"`: the person whose
453
+ * session is being watched is the one entitled to know (CC announces it in the session UI).
454
+ *
455
+ * - `"delegation.transcript_integrity"` (subagent transcript persistence) — a durable agent row
456
+ * with a BOUND transcript sessionId met a session store that attests `not_found` for it: the
457
+ * deployment's declared transcript durability is being contradicted by reality. Announced at
458
+ * most once per (scope, handle, process) — `detail: { handle, scope? }`, scope = the resolved
459
+ * access scope of the read that found the gap — from the continuation read faces (SendMessage preflight /
460
+ * AgentTranscript's durable leg); the per-call honest refusals are unchanged, and the declared
461
+ * tier is NOT auto-downgraded (declaration-制 — observation reports, it never re-adjudicates).
462
+ *
463
+ * - `"memory.consolidation_recommended"` (design/339 §2.2/§6.2) — the engine-minted per-scope
464
+ * session count crossed the consolidation thresholds (time gate open ∧ enough distinct
465
+ * sessions); minted at most once per crossing (a completed run re-arms the edge), NEVER when
466
+ * the deployment leaves consolidation off. ADVISORY: the host owns the verbs, nothing runs
467
+ * automatically; `detail: { scope, sessionsSince, reportId?, sessionId? }` — `reportId` (#479)
468
+ * is the minting harvest report's occurrence identity, shared with the harvest/hold family's
469
+ * notices of the same report (dedup on `(code, detail.reportId)`).
470
+ * - `"memory.consolidation_committed"` (design/339 §6.2) — a consolidation plan reached
471
+ * `completed`: products landed, superseded targets left the default read face (retained as
472
+ * evidence), intents settled; `detail: { planId, scope, products, superseded, intents }` —
473
+ * counts and engine-minted ids only, zero content.
474
+ * - `"memory.consolidation_conflict"` (design/339 §6.2) — a plan parked CONFLICT: at least one
475
+ * target did not reach its planned state (a concurrent write, a patch the store declined, or a
476
+ * target the re-judgment could no longer act on — the plan's audit rows carry the per-target
477
+ * reason). The REFUSED targets were not overwritten; patches the plan had already applied
478
+ * stand (discard rolls nothing back — the receipt's applied-vs-conflict counts say how many).
479
+ * Affected intents stay pending and wait for the host valve
480
+ * (`resolveConsolidationPlan` retry/discard); `detail: { planId, scope }`.
481
+ * - `"memory.consolidation_refused"` (design/339 §6.2) — the notice dialect of a consolidation
482
+ * verb's coded structured refusal (the verb itself throws
483
+ * {@link import("../core/memory-engine/consolidation.js").ConsolidationRefusedError});
484
+ * `detail: { refusalCode, scope?, occurrenceId? }`. `occurrenceId` (#495) is the THROWN ERROR's
485
+ * own id, minted once where the refusal happens and read off it by the notice factory — one
486
+ * throw rendered twice keeps one id, two refusals sharing a code/scope/message get two. Omitted
487
+ * (never fabricated) when the caller composes the line with no error in hand.
488
+ *
489
+ * Deliberately NOT a notice family: brain retry/reconnect liveness (a rate limit, a 5xx, a
490
+ * transient network failure being retried). Those are per-attempt liveness frames with their own
491
+ * frequency semantics and ride the wire `status` channel ({@link BrainStatus}), whose sink the
492
+ * Runner establishes unconditionally around every brain-driving call — no host seat to wire, no
493
+ * console fallback to flood. A reader looking for retry disclosure should look there, not here. */
494
+ code: string;
495
+ /** The exact human-readable line the unwired build prints via `console.warn` — same words, one text. */
496
+ message: string;
497
+ /** Machine-readable facts of the notice (knob names, arriving values, values in force). */
498
+ detail?: Record<string, unknown>;
499
+ /** The owning session, when the notice HAS one — an ATTRIBUTION/correlation key, never a
500
+ * projection permission: whether a notice may be surfaced onto that session's user-facing
501
+ * stream is decided by the code's {@link NOTICE_AUDIENCE} row alone ("audience is entitlement,
502
+ * not routability" — several `"operator"` rows carry a sessionId purely for correlation, and a
503
+ * projector keying on presence would push deployment facts at end users). Presence only makes
504
+ * routing structurally POSSIBLE; a session-less code structurally cannot be routed at all.
505
+ * Mint sites do not set this: {@link deliverEngineNotice} — the ONE delivery throat — lifts a
506
+ * string `detail.sessionId` here, so the typed key and the detail carriage can never disagree.
507
+ * Optional and absent for process/config-scoped codes. */
508
+ sessionId?: string;
509
+ }
510
+ /** Test seam (mirrors `__resetBashTimeoutAnnouncements`): never called by production code. */
511
+ export declare function __resetMalformedNoticeSeatAnnouncement(): void;
512
+ /**
513
+ * The ONE delivery form behind every {@link RunnerDeps.onNotice} emission point (#170: the form was
514
+ * triplicated across the emission stations, and every copy judged the seat with `!== undefined` — so a
515
+ * PRESENT non-function seat (null, a config typo, an untyped host's JSON wiring) entered the wired arm,
516
+ * threw `TypeError` on the call, and the swallow guard silenced BOTH channels at once). Contract:
517
+ * · a FUNCTION seat REPLACES the console line (a host forwarding notices to its own surface must not
518
+ * show every fact twice), swallow-guarded against both failure shapes the void-typed seat admits —
519
+ * a synchronous throw and an async sink's rejected promise (unhandled, that rejection is a
520
+ * process-level fault): a notice sink must never turn an announcement into a failure. Deliberately
521
+ * NO console fallback on sink failure: loudness ownership transfers with the wiring, and a console
522
+ * echo of a transient sink failure would double-send the fact. Corollary kept as-is: stations that
523
+ * de-duplicate ledger the line BEFORE this call, so a true-function sink that throws can in
524
+ * principle lose a ledgered line for the process lifetime — un-ledgering after the fact would race
525
+ * an async sink's late rejection and re-announce (double-send) on transient failures.
526
+ * · a PRESENT NON-function seat is a bad deployment value, and #123 (loud-bad-value law) forbids
527
+ * folding it to silence: the notice itself falls back to `console.warn` (no line is lost), and the
528
+ * seat defect — the one fact every fallback would otherwise repeat — is announced once per process.
529
+ * · an ABSENT seat prints the historic `console.warn` line verbatim (byte-compat loudness).
530
+ */
531
+ export declare function deliverEngineNotice(onNotice: ((notice: EngineNotice) => void) | undefined, notice: EngineNotice): void;
532
+ /**
533
+ * #259 loud half, one notice PER FAMILY (`task.user_steer_undrained` / `task.user_followup_undrained`):
534
+ * a consumer routing on `code` alone must never mistake a stranded follow-up for a stranded steer, so
535
+ * the code carries exactly the semantics its name claims — the same two-key split the settled frame
536
+ * uses. Pure (the terminal sweep race window is not constructible deterministically; this seam is).
537
+ *
538
+ * `sessionId` (#433, additive and optional): these two codes are the `"user"` audience rows whose
539
+ * subject is the END USER's own lost input, and a disclosure that cannot say WHICH session lost it
540
+ * has nowhere to be delivered — the audience registry and the routing key are the two halves of one
541
+ * answer. Carried in `detail` like every other session-attributed code, so {@link deliverEngineNotice}
542
+ * — the one throat — lifts it to the typed top-level key and the two spellings cannot disagree.
543
+ * Omitted when the caller has none (never fabricated: the message is what the run lost, and a made-up
544
+ * routing key would deliver it to the wrong stream).
545
+ *
546
+ * `runId` (#499, additive and optional): THE key for this account. The two ids above name the task and
547
+ * the conversation, and both answer the same value for two runs of one session whenever the host
548
+ * supplies no task id — so two runs each stranding one steer produced two byte-identical rows, read as
549
+ * one repeated disclosure about one run. The account is per-RUN by its own contract sentence ("at most
550
+ * once per family, per run"), so it must carry the run's own id. Same omit-never-fabricate rule.
551
+ */
552
+ export declare function undrainedUserInputNotices(counts: {
553
+ steer: number;
554
+ followUp: number;
555
+ }, taskId?: string, sessionId?: string, runId?: string): EngineNotice[];
@@ -0,0 +1,55 @@
1
+ let malformedNoticeSeatAnnounced = false;
2
+ export function __resetMalformedNoticeSeatAnnouncement() {
3
+ malformedNoticeSeatAnnounced = false;
4
+ }
5
+ export function deliverEngineNotice(onNotice, notice) {
6
+ if (notice.sessionId === undefined && typeof notice.detail?.["sessionId"] === "string") {
7
+ notice = { ...notice, sessionId: notice.detail["sessionId"] };
8
+ }
9
+ if (typeof onNotice === "function") {
10
+ try {
11
+ const r = onNotice(notice);
12
+ if (typeof r?.then === "function") {
13
+ r.then(undefined, () => {
14
+ });
15
+ }
16
+ }
17
+ catch {
18
+ }
19
+ return;
20
+ }
21
+ if (onNotice !== undefined) {
22
+ if (!malformedNoticeSeatAnnounced) {
23
+ malformedNoticeSeatAnnounced = true;
24
+ try {
25
+ console.warn(`The structured notice sink (RunnerDeps.onNotice) holds ${onNotice === null ? "null" : typeof onNotice} — not a ` +
26
+ `function. Notices fall back to console.warn until the wiring is fixed (omit the key, or wire a function).`);
27
+ }
28
+ catch {
29
+ }
30
+ }
31
+ }
32
+ console.warn(notice.message);
33
+ }
34
+ export function undrainedUserInputNotices(counts, taskId, sessionId, runId) {
35
+ const tid = taskId !== undefined ? { taskId } : {};
36
+ const sid = sessionId !== undefined ? { sessionId } : {};
37
+ const rid = runId !== undefined ? { runId } : {};
38
+ const tail = `accepted as "queued" were never consumed — the run ended first. They are NOT redelivered; re-send against a live run if still wanted.`;
39
+ const out = [];
40
+ if (counts.steer > 0) {
41
+ out.push({
42
+ code: "task.user_steer_undrained",
43
+ message: `${counts.steer} user steer(s) ${tail}`,
44
+ detail: { steer: counts.steer, ...tid, ...sid, ...rid },
45
+ });
46
+ }
47
+ if (counts.followUp > 0) {
48
+ out.push({
49
+ code: "task.user_followup_undrained",
50
+ message: `${counts.followUp} user follow-up(s) ${tail}`,
51
+ detail: { followUp: counts.followUp, ...tid, ...sid, ...rid },
52
+ });
53
+ }
54
+ return out;
55
+ }