@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.
- package/CHANGELOG.md +37 -0
- package/dist/agents/agent-transcript-tool.d.ts +2 -2
- package/dist/agents/cascade.d.ts +2 -3
- package/dist/agents/repair-loop.d.ts +2 -2
- package/dist/agents/retain-ledger.d.ts +2 -3
- package/dist/agents/send-message-tool.d.ts +2 -2
- package/dist/agents/session-util.d.ts +2 -2
- package/dist/agents/subagent.d.ts +3 -4
- package/dist/agents/teacher.d.ts +2 -2
- package/dist/agents/team.d.ts +2 -2
- package/dist/agents/verify.d.ts +5 -6
- package/dist/core/agent-definition.d.ts +172 -0
- package/dist/core/agent-definition.js +1 -0
- package/dist/core/checkpoint-store.d.ts +8 -4
- package/dist/core/delegation-frames.d.ts +298 -0
- package/dist/core/delegation-frames.js +21 -0
- package/dist/core/engine-notice.d.ts +555 -0
- package/dist/core/engine-notice.js +55 -0
- package/dist/core/gate-fold.d.ts +12 -0
- package/dist/core/gate-fold.js +158 -0
- package/dist/core/gate-lanes.d.ts +93 -0
- package/dist/core/gate-lanes.js +626 -0
- package/dist/core/hands-band.d.ts +134 -0
- package/dist/core/hands-band.js +1 -0
- package/dist/core/hooks.d.ts +20 -101
- package/dist/core/hooks.js +53 -854
- package/dist/core/mcp-failure.d.ts +43 -5
- package/dist/core/mcp-failure.js +31 -14
- package/dist/core/mcp-server-spec.d.ts +217 -0
- package/dist/core/mcp-server-spec.js +1 -0
- package/dist/core/model-seat.d.ts +99 -0
- package/dist/core/model-seat.js +1 -0
- package/dist/core/reminder-mint.d.ts +10 -0
- package/dist/core/reminder-mint.js +3 -0
- package/dist/core/runner/contracts.d.ts +382 -6
- package/dist/core/runner/gate-exit.d.ts +177 -9
- package/dist/core/runner/gate-exit.js +70 -1
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +2 -7
- package/dist/core/runner/prepare-delegation-surface.d.ts +2 -7
- package/dist/core/runner/prepare-run-refs.d.ts +12 -0
- package/dist/core/runner/prepare-run-refs.js +5 -0
- package/dist/core/runner/prepare-task.d.ts +2 -2
- package/dist/core/runner/runtask.d.ts +4 -71
- package/dist/core/runner/runtask.js +18 -6
- package/dist/core/runner-deps.d.ts +1416 -0
- package/dist/core/runner-deps.js +1 -0
- package/dist/core/runtime-caps.d.ts +164 -0
- package/dist/core/runtime-caps.js +1 -0
- package/dist/core/task-event.d.ts +910 -0
- package/dist/core/task-event.js +1 -0
- package/dist/core/task-limits.d.ts +110 -0
- package/dist/core/task-limits.js +1 -0
- package/dist/core/task-result.d.ts +809 -0
- package/dist/core/task-result.js +1 -0
- package/dist/core/task-spec.d.ts +1370 -0
- package/dist/core/task-spec.js +1 -0
- package/dist/core/task-stream.d.ts +382 -0
- package/dist/core/task-stream.js +1 -0
- package/dist/core/tool-spec.d.ts +1174 -0
- package/dist/core/tool-spec.js +1 -0
- package/dist/core/types.d.ts +26 -7691
- package/dist/core/types.js +2 -76
- package/dist/core/warm-resume.d.ts +2 -2
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -1
- package/dist/orchestration/goal.d.ts +2 -2
- package/dist/orchestration/run-spec.d.ts +2 -2
- package/dist/orchestration/run-workflow-tool.d.ts +3 -3
- package/dist/orchestration/workflow.d.ts +4 -4
- package/dist/scenarios/scenario-registry.d.ts +3 -3
- package/dist/scenarios/teacher-quickstart.d.ts +2 -2
- package/dist/server/http.d.ts +2 -2
- package/dist/stores/file/fs-atomic.d.ts +88 -12
- package/dist/stores/file/fs-atomic.js +184 -55
- package/dist/stores/file/index.d.ts +1 -0
- package/dist/stores/file/index.js +1 -0
- package/package.json +1 -1
- 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
|
+
}
|