@sema-agent/core
Version:
Stateless, task-oriented AI agent core
1,002 lines • 94.3 kB
TypeScript
/**
* A structured operator-facing notice ({@link RunnerDeps.onNotice}) — a fact the engine announces that
* is neither an error nor part of the task result: today, a configured value that was discarded in
* favor of another (the loud-bad-value discipline's announcement dialect). Structured so a host can
* FORWARD it to its own user surface instead of losing it in process stderr.
*/
export interface EngineNotice {
/** Stable machine-readable family, dot-namespaced. Current families:
* - `"config.autocompact_window_clamped"` (design/374 slice 1b) — a model declared
* `autoCompactTokens` ABOVE its physical window; the trigger-side geometry was clamped to the
* physical window (an autocompact window only ever lowers the trigger) and the bad value is
* announced once per prepared task; `detail: { modelId, declaredAutoCompactTokens,
* physicalWindow, sessionId, runId }` — `runId` is what makes the "once per prepared task" claim
* checkable, the session id beside it being shared by every run of the session.
* - `"config.env_timeout_discarded"` — a Bash timeout knob (option or env) held a value that is not
* the value in force; `detail: { knob, raw, usedMs }`.
* - `"config.materialize_env_discarded"` — `SEMA_TOOL_MATERIALIZE_STRATEGY` held a value outside the
* closed set in a seat where it is not in force; `detail: { raw, specStrategy? }`.
* - `"config.task_root_not_canonical"` (#759) — the task root this run was handed is not its own
* canonical path (a symlinked directory; `/var` → `/private/var` on macOS). Containment is judged
* on the CANONICAL spelling, so the deployment and the engine reason about two different
* directories: a target the host considers inside the root can be judged outside it. The run is
* unchanged — announce, never narrow, never repair — and the fix is to hand the canonical path in.
* Audience `operator` (a deployment wiring fact). `detail: { given, canonical, sessionId, runId }`
* — both spellings, so a consumer can diff them without guessing which one it holds. Once per
* prepared leg, and never for a root that already is its own canonical form (its absence is the
* claim that the two agree).
* - `"config.write_protection_target_view_absent"` — this leg's write-protection judge was compiled
* with NO execution environment while at least one path-confinable write tool is mounted, so it
* compares SPELLINGS against its table and nothing else: a write reaching a protected path only
* through an alias is cleared. Announce, never narrow — no verdict moves (an environment-less judge
* has no filesystem to resolve in). Audience `operator`. `detail: { tools, sessionId, runId }`,
* `tools` = the path-confinable write tools on the LEG-START roster, ascending. EXACTLY ONCE per
* prepared leg, never per judgment and never re-fired for a mid-run roster delta; absent ⇒ the judge
* has its target view, or no covered write tool was mounted AT PREPARE, or there is no judge —
* `wiringManifest.writeProtection` reports the judge's reading either way.
* - `"config.write_protection_unresolved"` (#869) — this leg's write-protection judge HAS a target view
* and the environment behind it could not answer: it refused a spelling, could not follow a link, or
* threw. That view then does not vote and the write is judged by its SPELLING alone (the judge's
* absence law), so the disclosure is the only place a deployment learns that its write floor is
* running on half a judgment. Announce, never narrow — no verdict moves because of this notice; the
* verdict already moved when the environment failed to answer. Audience `operator`.
* `detail: { reason, cause, sessionId, runId }` — `reason` is `"unresolved-symlink"` (a link whose
* real target is unknowable) or `"unresolvable-target"` (everything else: a refused spelling, an
* errored or throwing probe), and `cause` is the environment's own first sentence for that reason.
* ONCE PER LEG PER `reason` — the fact is about the ENVIRONMENT, not about a write, so it carries no
* call identity and a second failing write of the same shape adds nothing. Absent ⇒ every judgment
* this leg made had the views it was compiled for, OR the judge never finished at all (a judgment the
* gate's bounded wait gave up on is reported on the hook-error lane instead, `onHookError`).
* - `"config.tool_card_undeclared"` (design/388 L4) — a tool minted a `details.type` card its definition
* (or its caller declaration's `renderHints.resultCards`) never declared; the card was dropped from
* `tool_end.structured`; once per leg per type; `detail: { type, sessionId, runId }`.
* - `"config.tool_face_undeclared"` / `"config.tool_face_invalid"` (design/388 §2.7) — an MCP server's
* `toolFaces` overlay names a tool the listing does not carry / does not fit the tool's materialized
* schema (the tool mounts bare); once per leg per tool; `detail: { server, tool, problem?, sessionId, runId }`.
* - `"agents.listing_truncated"` — the agent-type roster frame did not fit the same listing budget:
* at least one type is listed name-only (`nameOnly`) or not at all (`dropped`). Audience `operator`.
* `detail: { budgetBytes, total, kept, nameOnly: [name], dropped: [name], sessionId }`; the same
* firing rule as its skills sibling below (once per delivered frame, on the carrying message's receipt).
* - `"permission.allowed_tool_not_granted"` — an allow list holds entries that grant (or refuse) nothing: a loaded skill's
* `allowed-tools` / `disallowed-tools`, the request's `TaskSpec.allowedTools`, the deployment's `RunnerDeps.allowedTools`.
* `*` (it voids an allow list; on a deny list it reads as nothing and the rest still refuses), `mcp__*` (no server
* named), a write-path or domain entry, a shape the rule grammar does not read. The run goes on and asks for those tools
* as usual. Audience `user`; once per prepared task for the skill and request lists (`detail.sessionId` present), once
* per Runner for the deployment's (at construction, no session). `detail: { sessionId?, entries: [{ source, entry,
* reason, skill?, list? }] }` (`source` ∈ `skill` / `request` / `deployment`; `skill` and `list` only on a skill's;
* `reason` ∈ `wildcard_all` / `namespace_wildcard` / `path_pattern` / `domain` / `content_unsupported` / `grammar`).
* - `"skills.listing_truncated"` — the `<skills>` disclosure block did not fit its byte budget, so
* at least one skill is listed without its description (`nameOnly`) or, in the pathological case,
* not listed at all (`dropped`); the `Skill` tool still serves every one of them. Audience
* `operator` — raising `TaskSpec.skillListingBudgetFraction`, lowering `skillListingMaxDescChars`
* or declaring fewer skills are all deployment acts. `detail: { budgetBytes, descMaxChars, total,
* kept, nameOnly: [name], dropped: [name], sessionId }` — `budgetBytes` IS the budget in force;
* no ceiling stands above the declared fraction, so no second number rides beside it.
* Announced on the RECEIPT of the message that carried the frame — the leg's first user message, or
* the boundary steer that a drift (or a resume leg's initial listing) rides — once per delivered
* frame the budget actually degraded, and never for one that fit. Every count is read off that frame, so
* `total` is the whole roster on an initial listing and the newly announced set on a drift frame.
* A run whose deployment turned the listing off (`attachments.skillsListing: false`) emits nothing,
* since nothing is rendered. There is NO dedup key: each notice is one delivered frame`s cost, and
* a frame is delivered (and committed) once.
* - `"tool_result.offload_put_failed"` (#167) — a clear-with-offload persist's fire-and-forget put
* failed; THIS attempt stored nothing (the failure arm reports, it never re-inserts under the
* ref) — an earlier attempt of the same idempotent re-put may already have stored the row, so
* the notice claims a failed write, not an empty ref;
* `detail: { ref, sessionId, cause, occurrenceId }`. Per-occurrence, not
* per-process-deduplicated: each failed write is a distinct fact, and `ref` names the ROW rather
* than the attempt, so the uuidv7 `occurrenceId` minted at the failure is what tells two genuine
* losses of one ref apart. Dedup key: (code, occurrenceId).
* - `"workflow.governance_key_stripped"` (#235) — the fields of an LLM-authored workflow `agent(spec)`
* that did NOT cross the governed default-deny whitelist: an unrecognized/control-plane field, or a
* `readFace` value that is not the containment-tightening one. The spawn PROCEEDS on the deployment
* baseline (the strip is not a refusal in this window), which is why the drop is announced;
* `detail: { total, stripped: [{ key, reason }], omitted?, occurrenceId }`, the rendered key list
* bounded in count and length because the names come from the untrusted script. One aggregated
* notice per governed child build, not de-duplicated across builds: each spec is a distinct fact,
* and the uuidv7 `occurrenceId` minted at the announcement is what carries that claim to a consumer
* that would otherwise read two builds of one spec as one repeated notice. Dedup key: (code, occurrenceId).
* - `"workflow.agent_option_ignored"` (#505 ①) — the OPTIONS-object twin of the row above: keys of
* `agent(spec, opts)`'s SECOND argument that were not applied (unrecognized, or a known option
* whose value its type check rejected — `label: 42`). The spawn PROCEEDS without them, which is
* why the drop is announced. A separate code because the two membranes accept different key
* sets and the message names its own. `detail: { total, ignored: [key], omitted?, occurrenceId }`,
* same bounding and same per-call (never de-duplicated) semantics as the spec-side row; dedup
* key: (code, occurrenceId). NOT the whole family: a SPEC field written on the options object
* (`model`, `modelName`, `thinking`, …) is REFUSED at the membrane instead — a silently ignored
* `model` runs a different agent than the author asked for.
* - `"config.forward_sink_failed"` — the deployment's opt-in subagent display sink
* (`RunInternals.onForwardEvent`) failed and the frame it was handed was dropped: it threw, or —
* since an `async` function satisfies that `=> void` seat — it returned a promise that rejected.
* The run is untouched (forwarding is a render channel, and delivery is never awaited, so a sink
* that never settles is not a failure and announces nothing). `detail: { site, cause }` — `site` is
* the engine-owned site name the failures are counted under, `cause` the thrown value's message.
* Audience `operator`: the sink is deployment wiring and nobody else can repair it. Dedup unit:
* once per RUN (the counters ride `RunInternals.forwardScope`, which every copy of a run's
* internals carries), later failures of the same run counted only — a sink that fails on every
* frame therefore costs ONE line, not one per frame.
* - `"memory.restore_withheld"` — a memory containment DID NOT write a committed baseline back: another
* writer held the path when the restore reached it, and those newer bytes were left standing.
* Containment is unaffected and the committed copy is still in the store, so reconciling the two is
* the host's act (audience `operator`). `detail: { paths: [{ path, entryId?, rev? }], sessionId? }`,
* `rev` = the declined generation. Dedup unit: once per containment PASS, never per path.
* - `"memory.project_scope_absent"` — the assembly point DECLARED that it could derive no project
* identity for this session's working root (`TaskSpec.memory.projectKey: { absent }`), so the
* layering it sent is used exactly as written: entries this session writes go to whatever the
* spec named, typically a layer shared with every other project. Audience `operator` — the
* reading is about how the deployment resolves working roots, and only an operator can change
* it; nothing retries and nothing is refused. `detail: { reason, taskRoot, sessionId,
* writeScope? }`, `reason` the deployment's own word (not_git | bare | unreadable),
* `writeScope` absent ⇔ a read-only layering. Minted at the DECLARATION rather than at the
* mount (the declaration is already the fact, and a mount that later fails must not swallow
* it). Dedup unit: once per prepared leg.
* - `"mcp.revocation_probe_failed"` (design/338) — the deployment's `mcpRevocations.isRevoked`
* probe threw; MCP dispatch FAILS OPEN (revocation is a tightening face) and this announces
* once per materialization (a resume re-materializes and may announce again).
* `detail: { message, runId }` — still no sessionId (a deployment wiring fact, not
* session-attributed); `runId` (#499) makes the once-per-materialization claim CHECKABLE without
* attributing the line to a conversation, an opaque invocation id saying which materialization
* rather than whose. `"operator"` audience by
* its explicit {@link NOTICE_AUDIENCE} row (#433 made the registry total over the catalog —
* no engine-minted code is audience-defaulted any more). The refusal itself
* (`mcp.server_revoked`) is a tool RESULT code, not a notice.
* - `"mcp.server_redialed"` — a host asked this run to RE-DIAL one MCP server (the run
* handle's `reconnectMcpServer` verb) and this is what came of it. `detail: { server, outcome,
* added, removed, reason?, sessionId, runId }` — `outcome` is the result's own three-word answer
* (`reconnected` / `refused` / `not_declared`) and `added`/`removed` are COUNTS of namespaced
* tool names, never the names themselves (a server's tool names are server-authored text and
* this is an operator line, not a disclosure channel). ONE code rather than a success code and a
* failure code: the outcome is data about one event, not two kinds of event, and both would
* carry the same presentation tier — a consumer alerting on failures reads one field.
* `"operator"` audience: re-dialing is deployment plumbing and a failed one leaves the server
* DOWN, which only the operator can act on. Dedup unit: once per ATTEMPT — never folded per run,
* because two attempts on one server are two different answers and folding the second would hide
* the one that says the server is still down.
* - `"mcp.injection_dropped"` (L-167) — an entry of the user's own request-lane MCP injection
* was NOT mounted for the leg. The HOST owns the drop sites and supplies the facts; CORE composes
* the notice ({@link import("./mcp-injection-drop.js").mcpInjectionDroppedNotice}, the one mint).
* `detail: { sessionId, server, reason, field? }` — `reason` is the closed set
* `malformed_entry` / `name_reserved_by_deployment` / `gate_closed` / `over_cap`
* (`MCP_INJECTION_DROP_REASONS`, CLOSED-SETS), `field` names the bad key for `malformed_entry`
* when known. `"user"` audience; `sessionId` REQUIRED at the mint (a user row with no session
* routes nowhere — refused, never fabricated). Dedup unit: per leg per server name, the host's.
* - `"config.models_swapped"` — `Runner.swapDeps` replaced the model catalog generation
* (zero-restart model switching; the `models` / `tiers` seats of the swappable set). `detail:
* { models, tiers, occurrenceId }` — key COUNTS only, never the catalog itself. In-flight tasks
* finish on the models they resolved at prepare (natural snapshot); every later prepare resolves
* against the new generation. A failed swap (illegal tier binding, a refused candidate) throws
* atomically and mints nothing. The verb carries no run and no session and the payload is a pair
* of counts, so the uuidv7 `occurrenceId` minted at the announcement is what tells two swaps of the
* same shape apart. Dedup key: (code, occurrenceId).
* - `"config.read_face_swapped"` — `Runner.swapDeps` replaced the deployment's read-face
* declaration (`RunnerDeps.readFace`, the `readFace` seat of the swappable set) without a
* restart. `detail: { from, to, occurrenceId }` — the seat's previous and new value, each
* `"open"` | `"roots"` | `null` (`null` = no declaration; the engine default `roots` applies).
* The SEAT is announced, not a leg's resolved face: resolution (task seat, governance, the
* read-only mount) still runs per prepare. In-flight legs keep the face they prepared under;
* every later prepare resolves against the new declaration. A candidate outside the value gate
* (`assertContainmentFaceValue`) throws atomically and mints nothing. Own `occurrenceId` for the same
* reason as the catalog line (no run, no session, a two-word payload). Dedup key: (code, occurrenceId).
* - `"config.write_face_swapped"` — the same line for the WRITE seat (`RunnerDeps.writeFace`),
* same payload, same laws. A SEPARATE code rather than a face field on the line above: the audience
* table and every consumer table are keyed by code.
* - `"config.context_settings_swapped"` — `Runner.swapDeps` replaced the deployment's per-model
* context-window overlay (`RunnerDeps.contextSettings`, the `contextSettings` seat of the swappable
* set) without a restart. Unlike every other swappable seat this one is read LIVE: a running leg
* measures its next boundary (the compaction trigger, the sanitize clamp, the request guard, the
* disclosed `contextUsage`) against the new windows, while its routing stays on the generation it
* prepared under. `detail: { words, declared, previousDeclared, occurrenceId }` — the declared model
* WORDS (the first eight) and the two counts, never the numbers: an overlay's values are deployment
* configuration and this line is an operator breadcrumb, not a config dump. A candidate outside the
* value gate (`assertContextSettingsValue`) throws atomically and mints nothing. Own `occurrenceId`
* for the same reason as the catalog line. Dedup key: (code, occurrenceId).
* - `"route.fallback_to_primary"` (key↔URL pairing, `src/brain/route-adjudicator.ts`) — a
* DERIVED-leg model (role/tier/system-default resolution, never a caller-explicit one) failed
* the pairing pre-flight and the seat fell back to the primary model instead of sinking the
* task; the notice is the loud half of that swap.
* `detail: { seat, from, to, cause, fixHint, sessionId?, runId? }` — `cause` is the refusal code
* (`route.credential_mismatch` / `route.credential_missing`); `sessionId` (#433, additive)
* rides when the adjudicating seat knows its session, and `runId` (#499, additive) names the
* INVOCATION whose seats were being resolved — both correlation keys only, the audience stays
* `"operator"` (see the registry's entitlement-not-routability note), and `runId` is what tells one
* task falling back twice apart from two tasks falling back once each.
* Explicitly-named models never mint this: they refuse at the brain's request gate instead.
* - `"route.classifier_unroutable"` (same pre-flight, the other POSTURE) — the derived classifier seat
* failed the same pairing pre-flight while the deployment had declared NO fallback
* (`RunnerDeps.autoMode.classifierFallback: "none"`), so the seat was KEPT and its calls are refused at
* the request gate rather than served by the primary model. The line is why "no swap" is still not "no
* word". `detail: { seat, from, cause, fixHint, action: "refused", sessionId?, runId? }` — the twin
* above's keys MINUS `to` (nothing moved, and a `to` naming a model that was not used is the one lie a
* consumer cannot detect); `action` is the discriminator, so a reader never infers the posture from that
* absence. Audience `operator`, same entitlement reading as its twin. Dedup key: (code, seat) per run.
* - `"classifier.fallback"` (the classifier's candidate ladder, `src/core/runner/classifier-ladder.ts`)
* — a gated call's classification did NOT rule on the classifier seat and was re-asked on the model
* behind it. Sibling of `route.fallback_to_primary` one layer later: that one fires when the derived
* model is not ROUTABLE at all, this one when it is routable and still does not answer.
* `detail: { from, to, cause, sessionId?, runId? }` — `cause` is the `AutoModeFallbackCause` word, the
* two ids are the rungs. Dedup key: (code, from, cause) PER RUN; a second CAUSE on the same rung is
* its own line.
* - `"classifier.role_defaulted"` (`src/core/runner/classifier-ladder.ts`, the seat resolver) — no
* `classifier` role names a model, so another model STANDS IN for the seat. `detail: { model, reason,
* thinkingOffSpelled, sessionId?, runId? }` — `reason` ∈ `tier_below_main` (the nearest directly-bound
* tier below the run's main model) / `tier_table` (the static `flash` row, a main bound to no tier) /
* `role_fallback` (the `summarize → default` chain, no tiers) / `main_only` (the main model is the only
* model there is) / `role_unresolvable` (the role named a model the catalog cannot resolve; the main
* stands in); `thinkingOffSpelled` = whether that model's wire can carry a disable key. Minted
* AFTER the route pre-flight, so it names the seat actually used. Once per run.
* - `"classifier.contract_broken"` (same file, the round observer) — ONE model answered
* `AUTO_MODE_CLASSIFIER_CONTRACT_BROKEN_AFTER` CONSECUTIVE classifications without a verdict (each already
* re-asked once by the leg). `detail: { model, rung, consecutive, sessionId?, runId? }` — `rung` is the
* ladder position (0 the seat, 1 the model behind it), since two rungs may share an id. A ruling from
* that rung resets its count; an `unavailable` round neither counts nor resets. Once per run per rung,
* and by construction before the denial limit's third block hands the user its card.
* - `"classifier.unavailable"` (`src/core/runner/classifier-ladder.ts`, `classifierUnavailableNotice`; minted by
* both classifier stations — the leg's gate and an ancestor's frozen classifier in the policy fold) — a
* classification could NOT RULE (the classifier leg failed, or its per-level budget
* `RunnerDeps.autoMode.timeoutMs` ran out) and says where the call went: `disposition: "asked"` = on down the
* ask chain to the leg's approval seat (a live approver or a durable park decides; the handed-on ask carries
* `requiresRealApproval`, so no automatic lane answers it; a declared departure from CC, which refuses either
* way), `"denied"` = refused with the classifier's own sentence (no approval seat).
* `detail: { toolName, toolCallId, cause, disposition, sessionId?, runId? }` — `cause` is the
* `AutoModeUnavailableCause` word. Once per classification that could not answer (a call judged by an ancestor's
* classifier and the leg's own can carry two); not minted while the call is being aborted (a halt, or the run's end). Audience
* `user`: on a lane with no approval face this line is the only place the user hears why the call did not
* run. It is not a block — the denial limit does not count it.
* - `"route.base_url_changed_key_unchanged"` (key↔URL pairing) — `Runner.swapDeps` moved a
* same-name entry's `baseUrl` while its Model-visible credential half (auth-bearing headers)
* did not change: legal (a provider changing domains), but worth one loud line — if the
* PROVIDER changed, the credential reference needs the same update. Advisory only, never a
* refusal; per-model-hook credentials are invisible to the catalog swap, so the notice is
* deliberately conservative (it may fire when a hook-side credential DID change in lockstep).
* One aggregated notice per swap; `detail: { entries: [{ modelId, from, to }], total,
* occurrenceId }` (rendered list bounded, total always exact); the `occurrenceId` is minted at this
* announcement, independently of the `config.models_swapped` line's own, because the bounded render
* makes two swaps moving the same first entries identical. Dedup key: (code, occurrenceId).
* - `"route.response_model_mismatch"` — the provider answered a turn naming a model that is NOT the id
* the request was sent with (an alias, a pool or a gateway-side substitution looks exactly like this),
* so it is advisory, never a refusal: the engine keeps asking for the catalog's id and reports the
* served name beside it (`turn_end.responseModel` / `TaskResult.responseModel`). ONCE PER LEG PER
* (requested, reported) PAIR — a catalog fact, not a turn fact. Audience `operator`;
* `detail: { requested, reported, sessionId, runId }`. Absent ⇒ the provider echoed the requested id
* or named nothing — one reading, not two.
* - `"route.tool_call_id_folded"` (#1207) — the provider named a tool call with an id outside the shape the
* engine keys calls under (visible ASCII, 1–128, no `~`), so the brain keyed it under a deterministic fold
* (`ToolCall.id`) and keeps the provider's spelling beside it (`ToolCall.wireId`), handed back only to the
* provider; or (#1234) it gave two calls in ONE response the same id, so the brain renamed the later ones on
* both faces (every provider requires a request's call ids to be unique). Advisory: every call runs. ONCE PER
* RUN, on the first folded call — a gateway fact, not a call fact. Audience `operator`;
* `detail: { toolCallId, rawLength?, reason, sessionId, runId }` — `toolCallId` the engine id, `reason`
* `"charset"` (a control byte, a space, a non-ASCII unit or `~`), `"length"` (admitted characters, over 128)
* or `"duplicate"` (a renamed later call); `rawLength` present for the two shape reasons only; never the raw
* bytes. Absent ⇒ no call of the run was folded.
* - `"context.tool_turn_repaired"` (#1224) — a provider request was built from a transcript that breaks the
* tool-turn law, and the request view restored it (`engine/llm/tool-turns.ts`; the transcript is never
* rewritten): a block after an assistant's tool calls moved ahead of them (`trailing_text`), a result recorded
* later moved up behind its call (`late_result`), a call with no result closed with the reconcile family's
* interrupted result (`missing_result`), a second result recorded for one call folded into the first
* (`duplicate_result`; an interrupted close beside a recorded result is left out, no recorded result is), a second
* call with an id its message already used left out — the message keeps the first, whose result carries every
* result recorded for the id (`duplicate_call`, #1241), a result no earlier call answers left out (`orphan_result`,
* #1242: its call is gone or it was recorded ahead of it; the one repair of the view as a whole, its ids once
* each in transcript order). Advisory:
* the run proceeds on the repaired request. Audience `operator`; `detail: { repairs: [{ form, toolCallIds }],
* sessionId, runId }`. ONCE PER RUN PER (form, calls) — the same broken turn is repaired on every request of
* the run and announced on the first. Absent ⇒ every request of the run already had the structure.
* - `"task.tool_turn_incomplete"` (#1230) — the person's line for a tool turn the model was handed incomplete, beside
* the note the model itself reads. `detail.cause` (closed `TOOL_TURN_INCOMPLETE_CAUSES`): `truncated` — calls whose
* arguments were cut before they parsed, dropped (they did not run; the model is asked to re-issue them);
* `unnamed` — calls that arrived with no tool name, dropped the same way; `bad_frame` — streamed response frames
* that could not be read, dropped (the response may be missing content); `missing_result_synthesized` — a
* call of this run with no recorded result, which the request view closed as interrupted, outcome unknown (the
* `missing_result` repair above; a call that went unanswered before the run began is the transcript's history and
* is on that operator line only). Audience `"user"`; `detail: { cause, count, toolCallIds?, sessionId, runId }` —
* `count` the calls (or frames) of that cause in the turn or request that first showed it; `toolCallIds` on
* `missing_result_synthesized` only, each id in the provider echo shape (a stored id outside it is not shown;
* `count` still counts it). Closed-set words, a count and call ids only: never a tool name, an argument or the
* note's text. ONCE PER RUN PER CAUSE. Absent ⇒ nothing of that cause reached the model.
* - `"config.read_face_deployment_clamped"` (#237) — a deployment-wide `readFace: "open"` is not
* in force beside a read-only (verifier) mount: it clamps to "roots" without throwing
* (stricter-wins; the clamp verdict stands, only its occurrence was undisclosed). Announced
* once per `onNotice` SINK at the prepare station (the verifier lane would otherwise repeat it
* every spawn; the sink — not the deps object — is the unit because the Runner rebuilds its
* deps and `runTask` builds a Runner per call), unwired console arm once per process;
* a library-direct `createHandsToolkit` mount announces at toolkit creation (one per mount,
* through the band-local `HandsToolkitOptions.onNotice` seat, absent ⇒ `console.warn`);
* `detail: { seat, declared, inForce, cause }`.
* - `"config.write_face_deployment_clamped"` — the write seat's twin of the line above, with the
* same stations, the same once-per-sink ledger (its OWN ledger row) and the same detail shape. The
* cause differs in prose only: that mount carries no write faces at all.
* - `"config.tool_model_gate_removed"` (design/277) — the model gate trimmed default-mounted
* scaffold entries from a task's roster. One notice per (model, class), de-duplicated once per
* `onNotice` SINK on the (modelId, class, canonical sorted removed-name set) line (same unit
* as `config.read_face_deployment_clamped`: two deployments hosted in one process each hear
* their own trim; unwired console arm once per process) — same class with a different removal
* shape is a distinct fact and announces again;
* `detail: { modelId, gateClass, removed, restore }` (`restore` names the three valves).
* - `"config.tool_model_gate_unknown_class"` (design/277) — a `ToolSpec.modelGate` tag names a
* class the merged gate table has no row for: the tag is inert (fail-open — the tool stays
* mounted) and this is its loud half (a tag typo must not silently become "never gated" with
* nobody told). Once per `onNotice` sink per class (console arm once per process);
* `detail: { gateClass }`.
* - `"config.tool_model_gate_env_invalid"` (design/277, the NOTICE dialect of the same fact the
* refusal code carries) — `SEMA_TOOL_MODEL_GATE` holds a value outside `on|1|true|off|0|false`
* in a seat where it is NOT in force (nothing this prepare would gate): announced once per
* `onNotice` sink per value (console arm once per process) instead of lying in wait; where the
* value IS in force the prepare refuses with the same code as `TaskResult.errorCode` (one
* fact, one code, two loudness dialects); `detail: { raw }`.
* - `"config.artifact_host_invalid"` — `RunnerDeps.artifactHost` is wired but its `capabilities()`
* declaration fails one of the seat's invariants (see `ArtifactHostCapabilities`), or its `onMount`
* threw: the `Artifact` tool is not mounted for the task and this is the loud half (an absent seat
* is silent; a broken one must not look the same). Once per prepared leg;
* `detail: { problem, sessionId, runId }`.
* - `"config.execution_env_capability_invalid"` — a remote env DECLARES a boolean capability key
* (`isolation`, `suspendable`, `canonicalPathAuthoritative`) with a non-boolean value; ABSENCE is a
* supported reading of each and stays silent. Once per key per run; `detail: { key, valueType }`.
* - `"config.secret_env_scrubbed"` (#1081) — the D1 fail-closed `scrub` policy withheld one or more
* secret-shaped keys from a spawned child's environment (a tool's shell, or a language server).
* Once per PROCESS, shared by every scrub site ({@link import("./secret-env.js").announceSecretEnvScrub});
* `detail: { names }` — every withheld key's name, never a value.
* - `"config.permission_mode_unavailable"` — the permission mode a task requested is not the mode the leg runs
* under: the deployment denies it (`by: "operator_deny"` — `RuntimeCaps.bypassPermissions` / `autoMode` is
* `false`), a requested `auto` has no classifier face (`by: "face_absent"`), or a child's declaration was folded
* under its parent's ceiling (`by: "inherited_ceiling"`, minted on the PARENT's stream with the child's
* `sessionId` beside the parent's — a child's own manifest is not shown to the person). The leg runs in
* `effective` (always `default` for the first two arms), announced once per leg and once per steer that folds;
* `detail: { requested, effective, by, sessionId, runId }`. Audience `user`.
* - `"config.permission_mode_invalid"`, `"config.spec_retired_key"` and `"config.shell_gate_value_retired"`
* are ERROR codes, not notices (named here only to keep the mode family's spellings
* in one place): a `permissionMode` word outside the closed set (on the spec or on a checkpoint row), a spec
* carrying a retired mode key (`autoModeRequested`, `approverPosture`) and a `shellGate` value other than
* `"always"`. Each is a door refusal: the
* code rides the thrown error (the run fails, or the resume is refused) and never crosses
* {@link deliverEngineNotice}, so all three are deliberately absent from {@link ENGINE_NOTICE_CODES} /
* {@link NOTICE_AUDIENCE}, like `config.deps_retired_key`. A consumer diffing its own table against the
* catalog must not add a row for them; it reads them on the error's `code`.
* - `"permission.ask_released_by_mode"` — the `bypassPermissions` mode released a surviving ask instead of
* putting it to anyone; once per (mode, askClass) per run, `detail: { mode, askClass, origin?, mandate?, count,
* firstToolCallId, sessionId, runId }` — `count` is how many releases of that pair the run had made when the
* notice was minted (the first). The per-call record is `tool_end.gate.disposition.released`. Audience `user`.
* - `"permission.read_deny_unapplied"` — the mode released a call whose reads the shell read station could not
* establish (`unestablishable` / `declined`) while a read-deny table is in force: no row of the table was applied
* to what it reads (C-R124, "released and disclosed"). Every such call, `detail: { mode, toolCallId, tiers, rows,
* rowCount, sessionId, runId }` (`rows` = the first eight non-built-in rows). Audience `user`.
* - `"permission.rule_store_unavailable"` — the person's persisted permission rules could not be read (a read
* failure, a corrupt or unreadable store, a read that did not answer in time), so that read's rows were SKIPPED:
* none of their allow, ask or deny rows took part in the decisions it served — a deny row among them protected
* nothing meanwhile — and no card is raised for it. `detail.at` names the read that found it first: `"prepare"`
* (the path rows the run's file and shell checks take at run start — the next run reads them again) or `"call"`
* (one call's own rule check — every later call reads again, so the rows apply as soon as a read succeeds). Once
* per run, whichever read fails first; the trace keeps every read's line (`permission.rule_store_unreadable`).
* `detail: { at, sessionId, runId }`. Audience `user`.
* - `"config.durable_gate_unavailable"` — a durable-approval MANDATE is in force on a leg with NO
* checkpoint store: the per-principal `forceDurableGate` entitlement, or the task's own
* `durableApproval` opt-in with no live approver seat (`durableMandateSourceOf`, the predicate the
* leg's children inherit). A mandate with no park facility cannot be honored: no ask of the leg can suspend, so each one resolves on the LIVE
* chain — a live approver / question face answers in-stream, and with none the ask is denied
* fail-closed — and NO durable approval record is written. The ask routing itself is not
* changed by the notice (a store-less leg never could park); the notice is its loud half,
* disclosed before the first ask of the leg. Audience
* `"user"` (the person whose asks will not be recorded is entitled to that; the operator hears
* it through the sink as always). Once per prepared task leg — a resume or a delegated child
* is its own leg and says so again; `detail: { sessionId, runId, principal?, cause, source,
* liveApprover, liveQuestionFace }` — `source` is `"force_durable_gate"` or `"durable_approval"`
* (which mandate is unhonored; the recovery differs); `cause` is `"no_deployment_store"` (the deployment wired none) or
* `"task_store_disabled"` (`TaskSpec.checkpointStore: "disabled"`, the per-run off switch, on a leg
* whose deployment may well have a store: the fix is the task's); `liveApprover` /
* `liveQuestionFace`: whether a live seat will EFFECTIVELY answer the leg's permission asks /
* questions in-stream (a delegated child under an inherited content mandate reads `false`
* even with a face wired — the marker withholds the question), or the fail-closed deny will.
*
* - `"task.user_steer_undrained"` / `"task.user_followup_undrained"` (#259) — user steers /
* follow-ups whose receipts said "queued" were still in their queue at agent_end: the run
* ended before any turn could drain them. One notice PER family (a consumer routing on `code`
* alone must not mistake a stranded follow-up for a stranded steer — same split as the settled
* frame's two count keys). They are NOT redelivered (a steer aimed at a finished run must not
* fire at the next one — unlike ENGINE notes, which pend per session); the loud half of the
* #257 contract's "accepted = enqueued, not consumed" sentence;
* `detail: { steer, cause, taskId?, sessionId?, runId? }` / `{ followUp, taskId?, sessionId?, runId? }`
* (`cause` on the steer row only, closed `USER_STEER_UNDRAINED_CAUSES`: `"leg_end"` — this engine's arm, the
* leg ended with it still owed; `"crashed"` — minted by the SERVING LAYER, not this engine, before the
* session's next run, for steers it had accepted whose consumption was not confirmed before the process died: their queue was the
* serving layer's, so only it can count them. One code for both, so a consumer de-duplicating on `code` keeps
* one row; `sessionId` = #433's routing half for these two `"user"`-audience rows; `runId` = #499's
* per-run identity, the key this account is actually about — see
* {@link undrainedUserInputNotices}; all omitted when the caller has none, never fabricated). A delegated child
* leg is announced on its PARENT's session as well — the direct parent only — as a second row per family,
* `detail: { steer | followUp, cause?, taskId?, sessionId: <the parent's>, childSessionId }` (no `runId`: nothing
* carries the parent's), message tail `(a child leg, <childSessionId>)`; `childSessionId` present = a child reported it.
* Per-run, at most once per family (the terminal sweep is a single site) — which is why
* `taskId`/`sessionId` cannot key it: both answer the same value for two runs of one session.
* ① The family fires on the INTERRUPT path too, `abort()` being barred from emptying either queue
* before agent_end can count it. ② On a DURABLE PARK the verdict is
* settled at the run tail instead of at agent_end, because the tail first carries the still-
* undelivered input onto the committed checkpoint's parked-steer queue (the resume delivers it)
* — what is announced is the remainder that could NOT be carried, so a fully-migrated park says
* nothing at all and a queue-full / no-longer-pending row still says exactly what was lost.
*
* - `"task.turn_interrupted"` (design/373) — a running turn was ACTUALLY CUT at a manufactured
* boundary: the in-flight provider stream / tool batch was aborted (finished tool calls keep
* their real results, never-started ones settle as paired interrupted results, the interruption
* marker lands). **TWO mint lanes, one code** — the CUT is lane-independent, so it is one fact
* with one spelling, and `detail.cause` is the arm discriminator (a consumer that must tell
* them apart reads that key; one that only cares "a turn was cut" reads neither):
* · the STEER-NOW lane (design/373) — a caller-provenance steer with `priority: "now"` cut
* the turn and the run CONTINUES with the steer at the queue head.
* `detail: { inputId, sessionId, runId, actorId?, taskId? }`, NO `cause` key — `inputId` is
* the steer's own correlation key (the `human_input` frame's id) and `actorId` the caller's
* asserted identity when one rode the steer.
* · the BARE-HALT lane (#504, {@link TaskStream.halt}) — the person's Esc-form stop cut the
* turn and the run does NOT continue: it collects to a clean, resumable
* `status:"completed"` ending carrying {@link TaskResult.haltedByUser}.
* `detail: { cause: "user_halt", sessionId, runId, taskId? }` — no `inputId` and no
* `actorId`, because no text entered the model and the verb carries no caller identity. A
* consumer keying `detail.inputId` off this row must treat it as ABSENT on this lane.
* Fence tension: this notice asserts THE CUT only — when the cut
* raced an approval park whose commit was already in flight, the gate's durable leg may
* still collect to `suspended` (park wins, row redeemable, the park's abort owns the
* ending, no `haltedByUser`, plus a `task.halt_unconsumed`); the notice stands beside that
* terminal without contradiction, because a seat really was cut — `haltedByUser` answers
* the other question (was the halt CONSUMED), see that seat.
* In both lanes: one notice per REAL cut (a `now` that found nothing in flight or whose frame
* already rode the imminent boundary, and a halt with nothing in flight, announce nothing — no
* false interrupt claims); `runId` is the INVOCATION that was cut; every cut turn counts toward
* `maxTurns`, so an interrupt storm's cost is bounded and each of its cuts is on the record.
* Audience `"user"`: the person who forced the boundary is the one entitled to see that it landed.
* RETIRED here (BREAKING, named): `"task.injection_priority_unimplemented"` — the ladder is
* implemented, so the unhonored-knob disclosure it carried has no referent; consumers must
* judge ladder support by VERSION, never by that code's absence.
*
* - `"task.halt_unconsumed"` — the user's stop verb ({@link TaskStream.halt}) was ANSWERED (not
* refused) but the run's ending was not its doing: the run's own ending stands and the result will
* NOT carry `haltedByUser` for it (the seat signs consumption, never acceptance). TWO mint points,
* one fact, one spelling, disjoint: the VERB arm — the halt arrived while the run's ending was already
* owned by its own abort (nothing cut, nothing stopped; once per such verb call, and only while no halt
* has been ACCEPTED on the run — after one was, the settle arm alone speaks for the run);
* the SETTLE arm — the halt was ACCEPTED (it may even have cut a live seat: `{turnCut:true}` +
* `task.turn_interrupted`) but the run ended for its own reason before the machinery consumed it —
* no interrupt settlement took the halt's cut as its cause and no boundary consult stopped the loop
* for it (a brain minting its own `aborted` final beside a late halt, a natural completion the halt
* arrived too late to prevent, an approval park whose commit won the race and whose abort owned the
* ending); once per such run, at the result's settlement. Either way the notice is the halt's trace
* beside the verb receipt. `detail: { sessionId, runId, taskId? }` — `runId` is the run whose ending
* the halt failed to claim. Audience `"user"`. A REFUSED halt (typed `steering.not_running` throw)
* mints nothing: the refusal itself is the loud answer.
*
* - `"task.late_approval"` (design/384 slice 1) — a synchronous ask approval was NOT CONSUMED:
* a run or turn interrupt released the wait (the `resolveAsk` race arm detached the approver's
* promise; an unconsumed resolve is the approver releasing its wait, never a verdict), so the
* tool did NOT run and the approval was not honored. The notice asserts non-consumption ONLY —
* never an arrival order: an approval and an abort settling close together read as the abort
* (the resolver's documented one-directional residual — the engine never claims a person
* decided something it cannot show they did, and it equally never claims which came first).
* Minted only for a value that reads as an APPROVAL — an unconsumed
* deny/refusal is an ordinary release and leaves no trace, and an unconsumed REJECTION goes to
* `onError(phase:"hook")` instead (a failing callback, not an answer). One notice per
* unconsumed approval; `detail: { toolName, toolCallId, sessionId, runId, taskId? }`. Audience `"user"`
* (whoever answered the card is the one entitled to hear the answer ran nothing); a host may
* forward it on its own wire.
*
* - `"task.interrupt_unconsumed"` — a HARD stop ({@link TaskStream.interrupt}, the task's `signal`,
* the walltime or the turns limit) landed AFTER the run's last turn had already given its answer
* (`stopReason:"stop"`, not partial, and no steer / follow-up of the person's still pending — a
* queued continuation makes that "stop" work in progress, and a stop that cuts it stays the
* ordinary abort with its undrained-input account) and before the loop's end — what it aborted was the run's
* tail (a final-boundary compaction is mooted, a stop hook's wait is cut), never its work. The
* answer stands: the run ends `completed` with it as `result`, no `[Request interrupted by user]`
* marker is written and no `limits.*` terminal is minted for the axis that fired; this notice is
* the stop's only trace. Minted once per such run, after the terminal arbitration (so it never
* claims a completed ending the terminal did not give — a budget ceiling crossed by the last
* turn's own spend still fails the run and mints nothing here); `detail: { sessionId, runId,
* taskId?, origin }` with `origin` ∈ `"interrupt"` (the verb) | `"walltime"` | `"turns"` |
* `"signal"` (every other run-level abort: the task's own `signal`, a parent's cancel, a
* `destroy()`, a refused preempt — the leg names the three it can tell apart and files the rest
* here). Audience `"user"`. A stop that arrives
* AFTER the loop's end is refused typed (`steering.not_running`, every verb alike) and mints
* nothing: the refusal itself is the loud answer.
*
* - `"approval.read_root_granted"` / `"approval.read_root_grant_rejected"` — an approver's `readRootGrant`
* (`AskOutcome`). GRANTED: it equalled the card's `readRootCandidate.dir` beside `allow: true`; this run's READ
* directories now include it (never the write roots; this run only); `detail: { dir, covers?, root, toolName,
* toolCallId, sessionId, runId, taskId? }`, `root` = the canonical spelling held. REJECTED: nothing changed,
* the verdict stands; `detail: { reason, offered?, requested?, …same ids }`, `reason` ∈ `on_refusal` |
* `not_a_string` | `no_candidate` | `mismatch` | `unresolvable` | `resolves_elsewhere` | `admits_artifact_store`
* (the directory contains the wired local artifact store, lies inside it or is it) | `no_read_roots`. Once per
* answered ask. Audience `"user"`.
*
* - `"checkpoint.execution_outcome_unrecorded"` — the resumed leg's pending-call resolver disposed
* the decided action (executed it, delivered the refusal, or vetoed the person's yes) and filed
* the frame's record through `CheckpointStore.recordExecutionOutcome`, and the store answered
* `not_resolved` or `absent` — the row was reopened, expired or removed under the leg by another
* actor, so the row now reads `unknown` on its execution axis (`executionVerdict`). The
* execution result is NOT changed by the answer (the record is an account of what ran, never a
* gate on it); the notice is the loud half. Once per such resolution;
* `detail: { sessionId, runId, scope, checkpointId?, word }` (`word` = the store's answer; the
* secret token never rides a notice). A DIFFERENT record already on file is not this notice — it
* is the `checkpoint.execution_outcome_conflict` throw (an engine defect, propagated).
*
* - `"classifier.denial_limit"` (#548, CC 2.1.250 `FO`/"too many classifier denials in headless
* mode") — the auto-mode classifier's DENIAL LIMIT was reached (3 consecutive blocks, or 20 in the
* run; `RunnerDeps.autoMode.denialLimit`) and the fallback ask it turns into was never JUDGED by
* anyone. Two lanes reach it. SYNCHRONOUS: no approver to go to — none wired, a blanket
* `onAsk:"allow"` (refused for a real-approval ask), or an approver that reported unavailable with
* no durable park to take it. DURABLE: the fallback PARKED, and the decide that resolved the row
* reported a settlement the nobody-judged table reads as such (`hostDecision.decidedBy:"policy"` ⇒
* `policy_refused`) — a machine answered where the bound had asked for a person, so the last resort
* is the same one. A person's refusal (`"person"` ⇒ `human_refused`) is NOT this notice on either
* lane: someone looked. The run was STOPPED — a `failed` terminal, `code` the same code, `message`
* the limit sentence — rather than kept spending on a model the classifier denies without end; the
* triggering call's own result is the deny that stood. The two lanes' sentences differ in ONE
* clause (which of the two ways nobody answered), and the durable one states the counts the PARKED
* row carried, so a resume in a fresh process reports the streak that actually happened. Once per
* run leg (the first such fallback owns the terminal);
* `detail: { sessionId, runId, toolName, toolCallId, consecutive, total, limit }` — `limit` is
* `"consecutive"` | `"total"`, the bound that tripped. Audience `"user"` (the person whose run
* ended is the one entitled to hear why, and to review the transcript the sentence points at).
* With an approver wired the same bound mints NO notice — the fallback ask reaches the person
* instead (`AskRequest.denialLimitFallback`).
*
* - `"steering.parked_input_blocked"` (design/373 §4.3) — a PARKED steer entry was withheld when
* a resume redelivered it (any resume kind that drains parked steers — wake included) by the
* deployment's `userPromptSubmit` screen (block verdict, or a fail-closed non-answer/crash):
* the row was consumed with the checkpoint, the frame never reached the model, and the resume
* itself proceeds (a blocked instruction must never wedge a wake). The lifecycle half of the
* same fact is the entry's own `human_input` account with `delivery: "blocked"` — the two share
* the inputId, so "accepted at park, screened at redelivery" is auditable end to end. Audience
* `"user"` (the person whose instruction was withheld is the one entitled to re-issue it);
* `detail: { inputId?, sessionId, runId, taskId? }` — `inputId` is the parked entry's stored
* correlation key (OPTIONAL: a row parked without one exists), and `runId` names the resume leg
* that withheld it, so a session resumed twice does not report two withholdings as one.
*
* - `"memory.session_polluted"` (design/178 §3, #324a; message mode-aware since design/336) —
* this session's memory crossed into the one-way externally-exposed state (a tool classified
* as an external content source was invoked, directly or through a delegated child). Under
* `memoryProvenance: "carry"` (default) the message states the tag-admit semantics (ordinary
* writes commit with an origin marker; instruction-form files are withheld); under `"off"` it
* keeps the pre-336 sentence — writes no longer eligible for the long-term library. The
* notice states that VERDICT plus what a harvest does when it collects — it does
* not promise quarantine, because a `writeScope`-null layering and a declared-unavailable
* session both reach the zero-admission harvest arm (nothing collected ⇒ nothing captured), and
* a mark landing after the pre-commit pollution read leaves already-committed rows to the
* challenge sweep; what a harvest actually contained is the other code below. The challenge
* clause is a policy statement, not a receipt — a sweep that cannot run announces
* `"memory.challenge_sweep_failed"` on the engine's incident seat. Announced ONCE PER
* SESSION, at the mark seat: a repeat mark inside the process and a resumed session whose
* durable marker already exists stay quiet (the state is one-way, so a second line would carry
* no new fact); a pollution state that cannot be read announces rather than assuming it was
* already said. `detail: { reason, sessionId? }` — `reason` names the invoked tool and is
* neutralized/length-bounded (the name comes from the host/protocol roster).
* - `"memory.harvest_quarantined"` (design/178 §3, #324a) — a polluted session's harvest ran its
* containment: `count` ENTRY files written or changed in the session were withheld from the
* library, `moved` of them were physically moved into the control-plane quarantine directory,
* and `escalated` of them carry a `HarvestReport.quarantineFailures` row (a failed capture, a
* failed removal, or a file tombstoned in place — which counts as moved AND escalated, so the
* two numbers are read off the report's rows, never subtracted from each other). One notice PER
* HARVEST that withheld at least one entry file (a checkpoint harvest and the terminal harvest
* are distinct facts), never minted for a clean session; `detail: { count, moved, escalated,
* reportId?, reason?, sessionId? }` (`reason` absent ⇔ the pollution marker could not be re-read
* at report time — the withheld count stays true either way). `reportId` (#479) is the minting
* harvest's own occurrence identity ({@link import("./memory-engine/types.js").HarvestReport}
* `.reportId`, uuidv7): two harvests whose counts and paths coincide carry DISTINCT ids, a
* durable replay of the same notice carries the SAME id, and the hold family below shares this
* harvest's id — dedup on `(code, detail.reportId)`, never on `reportId` alone (absent only for
* a hand-built report; every engine-minted report carries it). The formerly registered index-only gap is
* CLOSED under `memoryProvenance: "carry"` (design/336 §6.3, #331): the mint condition reads
* `HarvestReport.containment`, so a containment whose only act was the derived-index rollback
* announces with `count: 0` and `detail.indexRolledBack: true`; under `"off"` the pre-336
* count>0 condition (and its silence on index-only containment) is kept byte-level.
* - `"memory.hold_opened"` / `"memory.hold_released"` / `"memory.hold_disposed"` (design/336
* §4/§6.3) — the instruction-hold lifecycle, derived from the same structured
* `HarvestReport.containment` signal (hold seats are only ever filled under
* `memoryProvenance: "carry"`): a PENDING session's instruction-form entry files were captured
* off the model-visible plane (`hold_opened`, `detail: { count, paths, reportId?, sessionId? }`
* — paths neutralized/length-bounded); previously held entries re-walked the full gate set and
* committed after their writer session settled clean or a host valve released them
* (`hold_released`, same detail shape); held entries moved to control-plane quarantine
* (`hold_disposed`, `detail.disposed: [{ path, terminal }]` with the closed terminal set
* dirty/expired/conflict/capture_lost/discarded — an `expired` terminal is explicitly a
* TIMEOUT, not a conviction, and the message names `resolveHold` as the recovery valve). At
* most one notice per family per harvest report; wording is factual, never threat-flavored
* (the design/336 §13-2 model-psyche guardrail). `detail.reportId` (#479) is shared by all
* three families AND `"memory.harvest_quarantined"` when they derive from the same harvest
* report — that sharing is the contract (one report, several distinct facts): dedup on
* `(code, detail.reportId)`; a repeat with the same pair is a replay, a new pair is a new fact.
* - `"memory.delegation_static_mark_waived"` (design/324, #324 ruling ①) — the deployment set
* {@link RunnerDeps.memoryDelegationEvidence} to `"attested-only"` and a delegation call whose
* STATIC tool-face verdict would have marked this session's memory polluted (attestation
* missing/unknown + face can reach external content) was not marked: the waiver is announced —
* the explicit dual of `"memory.session_polluted"` for this arm, so the arm has a voice where a
* mark used to land. MINT-side guarantee only: delivery rides the shared guarded form
* ({@link deliverEngineNotice} — a wired sink that throws owns that loss, exactly as at every
* de-duplicating station), and the leg latch is consumed at mint. Minted only when a pollution
* face is mounted (a
* recorder-only child's mark is a no-op — nothing is waived there), at most ONCE PER PREPARED
* TASK LEG (a resume leg may announce again — a new leg's audit stream is a new fact;
* deliberately no durable once-per-session state), never on the `"static-face"` default, and
* never for a delivered `"external"` attestation (that mark still lands);
* `detail: { reason, subagentType?, sessionId?, runId }` — `runId` (#499) is what makes the
* ONCE-PER-LEG guarantee above checkable, since a resume leg may legitimately announce again and
* the session id cannot tell the two legs apart; `reason` is the same sentence the waived
* mark would have carried, neutralized/length-bounded (tool and agent-type names are
* host/model-controlled inputs).
* - `"memory.content_class_declared"` (design/378) — an `McpServerSpec` entry carries an explicit
* {@link McpServerSpec.contentOrigin}: the deployment declared this server's whole tool set to
* be inside (or explicitly outside) its trust boundary, so the protocol namespace's structural
* `"external"` no longer decides. The AUDIT line for a trust boundary an operator drew by
* configuration — which is why it announces on the explicit `"external"` value too: pinning is
* not a no-op (a declaration beats the `trustedTools` allowlist, so it forecloses the per-name
* exemption for this server's tools). One line per DECLARED ENTRY per prepared task leg, minted
* once at preparation and never per call (the declaration's whole effect is that calls stop
* marking; narrating each call would trade the saved mark for equal noise). Armed on the SAME
* condition as the classification it talks about — a leg with no engine-memory session and no
* provenance recorder classifies nothing, so there is no posture to report and no line is
* minted. `detail: { server,
* contentOrigin, toolCount, execIsExternalContent?, sessionId?, runId }` — `runId` (#499) names
* the LEG, which is what "one line per declared entry per prepared task leg" is a claim about:
* the session id beside it is shared by every leg of the session, so two legs reading one
* declaration used to mint byte-identical rows. `server` is the
* host-authored entry name, neutralized/length-bounded; `toolCount` is what this entry actually
* mounted (0 for a server that failed to connect — the declaration still stands and is still
* disclosed); `execIsExternalContent` rides the `"execution"` value only and reports the strict
* knob AS RESOLVED FOR THIS RUN, because that arm's posture depends on it. The message's closing
* clause is written per value so it can never describe a posture the run does not have. The
* entry's TRANSPORT is deliberately absent: it is a claim about a mutable host-owned object made
* long after the dial, and D-10 already rules topology is not evidence of the lineage this line
* audits — connection facts live on `MaterializedMcp.statuses` instead.
*
* - `"memory.capture_opted_out"` (design/383 §2.8) — memory capture is OFF for this session and
* the switch provably TOOK: nothing the session produces will be committed to the long-term
* store and its contributions no longer enter consolidation. The user must be able to SEE the
* switch land rather than infer it from silence, which is why it is announced rather than left
* implicit. States three facts and an exit, and deliberately overclaims none of them: entries
* committed BEFORE the opt-out remain in the library and stay retrievable, entries already
* folded into consolidation products leave those products unaffected, and deleting committed
* content is a separate host action (the erase valve), never a side effect of this switch.
* `detail: { sessionId, origin }` — `origin` is the closed set
* `"declaration" | "flip-verb" | "standing-record" | "delegation-floor" | "fork-origin"`, and
* the message is written per value (a delegated child's inherited floor and a fork's inherited
* one-way record say so, rather than claiming the session declared it). Audience `"user"`.
* Dedup unit: once per SESSION — key `(code, detail.sessionId)`, so the standing-record resume
* arm folds into the standing entry instead of double-announcing.
* - `"memory.capture_optout_unpersisted"` (design/383 §2.8) — the OPPOSITE receipt, and a
* separate code on purpose ("it is off for you" and "it could not be turned off" must never
* share a spelling): the opt-out could NOT be durably recorded (the control-plane write
* failed), so it is REFUSED rather than honored in-process under a switch that would silently
* stop holding at the next resume. Minted only beside that refusal — the throw carrying the
* same code — never beside a success, and no success notice is minted on this arm.
* `detail: { sessionId }`. Audience `"user"` (the operator repairs the store, but the
* entitlement to the fact belongs to whoever asked for the privacy). **TWO ingresses, and they
* settle differently — a consumer must not read one as the other:**
* · PREPARE-TIME (a declared/inherited opt-out, before the run starts) — the refusal is
* TERMINAL: the task never runs and nothing is captured. Dedup unit: once per task, since
* the refusal it rides settles the task.
* · the LIVE FLIP VERB ({@link TaskStream.optOutMemoryCapture} mid-run) — the refusal is
* SCOPED TO THAT CALL: it rejects the verb's promise and nothing else. The run keeps going
* and capture is still ENABLED (the switch did not take), which is exactly why the notice
* is owed. Because no record was retained, a retry can fail the same way, so this ingress
* can announce ONCE PER ATTEMPT — a consumer de-duplicating per task would hide later
* failures of a switch the person is still trying to throw.
* Read the run's own outcome (or the verb's rejection) to tell the two apart; this code alone
* does not say whether anything stopped.
* - `"memory.consolidation_incomplete"` (design/376, LLM consolidation driver) — a driver run
* settled without reaching the fixpoint: `detail` names the stop reason (the closed
* `ConsolidationRunStopReason` set), cycles done, and the residue (write-failure or
* fuse-refused groups by name). Advisory: committed cycles stand (add-only, never rolled
* back); the recovery verb is re-running the host driver, which resumes the same pending run.
* - `"memory.consolidation_withheld"` (design/383 §1.4) — the exposure partition engaged on a
* consolidation run, so the CLEAN arm's authoring prompts were composed WITHOUT the marked
* rows: anything this run's clean products say was written without them, and their silence on a
* topic therefore does not mean the library is silent on it (the withheld rows still stand and
* are still readable). OPERATOR audience (#692 C-a): the run happens outside any session, so
* there is no `sessionId` to route a user row to — the receipt's `withheldFromCleanArm` /
* `withheldNeverSent` are the reader-facing half. TWO counts, deliberately separate sentences and separate keys —
* `withheld` rows were kept out of the clean arm's prompts, and `neverSent` of those were not
* sent to the consolidation model AT ALL (a lone marked row has no group to fold, so it is
* withheld from every call). "Withheld from the clean arm" and "never left this machine" are
* different facts about a person's own data and must not be merged into one comforting number,
* which is why `sentToProvider` is carried as its own member rather than left to subtraction.
* `detail: { scope, runId, withheld, neverSent, sentToProvider }` — `scope` is
* neutralized/length-bounded. Audience `"user"`: the ONE consolidation row that is a reader's
* fact rather than an operator's (the other five report what a host-driven maintenance run DID;
* this one reports what the products a person will READ were written without). Minted once per
* run, beside the receipt — the dedup unit.
* - `"memory.consolidation_driver_superseded"` is an ERROR code, not a notice (named here only
* to keep the family's spellings in one place): a concurrent driver invocation took over this
* scope's run row (attempt fencing) and the losing worker's call THROWS with this `code` —
* it never crosses {@link deliverEngineNotice}, is deliberately absent from
* {@link ENGINE_NOTICE_CODES}/{@link NOTICE_AUDIENCE}, and sits in the non-governance
* disposition table with the error dispositions. The loser MUST NOT retry into the winner's
* account. A consumer diffing its own table against the catalog must not add a row for it.
*
* - `"config.peer_admission_out_of_range"` (#551) — a `RunnerDeps.peerLane` field the
* resolver could not use: outside its legal range, or not a finite number at all (a string off
* an untyped host's wiring, `NaN`, `Infinity`). THAT FIELD falls back to its own default (never
* a clamp to the nearest edge, never a whole-config reject) and every other field is unaffected.
* One notice PER FIELD — two bad fields in one config are two notices — deduped per SINK on
* `(field, given)` rather than per call (the resolver runs on every SendMessage and every drain
* round; the fact is about the wiring, not about the call). Per SINK, not per process: two
* Runners in one process each own an `onNotice`, and a process-global ledger would deliver the
* fact to whichever resolved first and leave the other silently without its own disclosure. The
* unwired arm (no `onNotice`: the shared `console.warn` throat) latches per process instead —
* the console IS one process-wide channel. `detail: { field, given, default,
* range: [lo, hi], reason }`, where `reason` is `"out_of_range" | "not_a_finite_number"`.
* Audience `"operator"` (a configuration fact; the fix is the deployment's).
* - `"config.peer_lane_unmounted"` (design/385 §1.2⑥) — `RunnerDeps.peerDirectory` is wired but
* the cross-session lane could not mount on this leg: no `mailboxStore`, or one that does not
* declare `crossProcessSafe: true` (several terminals would share one session box on luck). The
* seat is inert for the run (no ListAgents, peer addresses refuse with the same reason). Once per
* prepared leg; `detail: { reason, code, mailboxWired, sessionId, runId }`. Audience `"operator"`
* (a wiring fact; the fix is the deployment's).
* - `"peer.inbound_disposition"` (design/385 §1.2④ / §4.6 / §4.4) — a message parked in THIS
* session's own box was settled at the drain WITHOUT reaching the model. `detail.disposition` is
* the arm (closed set `PeerInboundDisposition`): `"refused"` (this session's `crossSessionInbound`
* setting, or a record whose typed fields cannot be rendered canonically), `"admission_refused"`
* (the drain-stage admission re-check: duplicate / hop loop / runaway — `cause` names it),
* `"notice_unrouted"` (a notice-kind record the notice face cannot render — no typed `notice`, or
* a state that does not fit its kind — settled without delivery, never ridden through the
* peer-message envelope), `"notice_user_only"` (a delivery receipt / idle notice whose sender this
* session's parity would HOLD: surfaced here for the user, not read by the model — CC's
* `modelVisible:false` arm; `cause` carries the rendered notice text), or `"held"` — the parity
* judgment held it: the message ENTERED this session's process-level held queue and was acked
* from the box (later messages are not blocked behind it), where it is re-judged at every
* boundary, released or refused with a receipt, and expires WITH a receipt under `dialogExpiry`
* (see `peer.held_settled`); `cause` is a `PeerInboundHoldCause` — the parity causes, or the
* drain gate's TRANSIENT `rate_limited` (that one does NOT enter the queue: the message stays in
* the box and the drain stops at its seq until the bucket refills). Disclosed once per seq per leg,
* and again when a re-judgment moves a held entry's cause. `detail: { disposition, cause, seq, box,
* fromSession?, sessionId, runId }`. Audience `"user"`: the session's user is the one who did
* not receive it.
* - `"peer.held_settled"` (design/385 §4.4, slice 4) — a message left THIS session's held queue.
* `detail.settlement` names how: `"delivered"` (a re-judgment or an approval released it into
* this session's model context), `"refused"` (the setting moved to refuse, or an approval met a
* refusing policy), `"expired"` (the `dialogExpiry` deadline, an eviction when the 100-entry queue
* was full, a cancelled review, a graceful shutdown, or a record another lease holder already settled),
* `"denied"` (the review face), `"dropped"` (the injection lane refused the released frame).
* `detail.reason` is the trigger (`PeerHeldSettleReason`: `deadline | evicted | shutdown | rejudge |
* approved | denied | cancelled | inject_failed | superseded`); the sender was sent the matching receipt
* whenever the record carried a reply address — except on `superseded`, where the lease holder that
* settled the record sent it.
* `detail: { settlement, reason, seq, box, fromSession?, heldCount, sessionId, runId }`. Audience
* `"user"`: it is this session's user who held (or was holding) the message.
* - `"peer.idle_subscription"` (design/385 §5.2, slice 4) — a peer session asked to be told when
* THIS session next goes idle (`SendMessage … notify_when_idle`). `detail.outcome`: `"recorded"`
* / `"refreshed"` (a same-requester re-ask; the subscription is one-shot and expires unfired after
* 12 h), `"full"` (the 32-entry table is full) or `"refused"` (this session's `crossSessionInbound`
* is `refuse`) — the last two answered the requester with an `unavailable` notice. `detail:
* { outcome, fromSession, live, seq, box, sessionId, runId }`. Audience `"user"`: the person whose
* session is being watched is the one entitled to know (CC announces it in the session UI).
*
* - `"delegation.transcript_integrity"` (subagent transcript persistence) — a durable agent row
* with a BOUND transcript sessionId met a session store that attests `not_found` for it: the
* deployment's declared transcript durability is being contradicted by reality. Announced at
* most once per (scope, handle, process) — `detail: { handle, scope? }`, scope = the resolved
* access scope of the read that found the gap — from the continuation read faces (SendMessage preflight /
* AgentTranscript's durable leg); the per-call honest refusals are unchanged, and the declared
* tier is NOT auto-downgraded (declaration-制 — observation reports, it never re-adjudicates).
* - `"delegation.ask_unresolvable"` — an `ask` reached its FINAL deny with nobody having judged it:
* the call's approval could not be RESOLVED. `detail.cause` (closed set `ASK_UNRESOLVABLE_CAUSES`):
* `approver_unavailable` — the approver consulted (the run's own seat, or an ancestor's frozen seat
* on a delegated child) answered `unavailable` and no park took the call (`tool_end.gate.settlement
* .kind:"approver_unavailable"`); `no_approver_wired` — no approver / question face is wired, or the
* seat is the deny posture (the headless auto-deny, `settlement.kind:"no_approver"`);
* `mandate_unreconstructible` — an inherited durable mandate binds the ask and this leg has no park
* facility (an in-fold deny, no settlement on the frame). The deny itself is unchanged (the
* fail-closed sentence as the tool result); this is its person-facing half, minted at the gate's ONE
* deny exit, once per tool call, by `ask-unresolvable-notice.ts`. `detail: { sessionId, toolName,
* toolCallId, cause, parkLaneExisted, remedy? }` — `parkLaneExisted` says whether a park lane was armed and
* declined/failed (`true`) or none existed (`false`, the usual shape); `remedy` (the two causes that say nobody
* could be asked — `approver_unavailable` and `no_approver_wired`; never `mandate_unreconstructible`)
* is the one sentence naming the caller-side change that would have let the call run, as the tool text carries it. Audience `"user"`: the refused
* work is the session user's own; the operator hears it through the sink like every code.
*
* - `"memory.consolidation_recommended"` (design/339 §2.2/§6.2) — the engine-minted per-scope
* session count crossed the consolidation thresholds (time gate open ∧ enough distinct
* sessions); minted at most once per crossing (a completed run re-arms the edge), NEVER when
* the deployment leaves consolidation off. ADVISORY: the host owns the verbs, nothing runs
* automatically; `detail: { scope, sessionsSince, reportId?, sessionId? }` — `reportId` (#479)
* is the minting harvest report's occurrence identity, shared with the harvest/hold family's
* notices of the same report (dedup on `(code, detail.reportId)`).
* - `"memory.consolidation_committed"` (design/339 §6.2) — a consolidation plan reached
* `completed`: products landed, superseded targets left the default read face (retained as
* evidence), intents settled; `detail: { planId, scope, products, superseded, intents }` —
* counts and engine-minted ids only, zero content.
* - `"memory.consolidation_conflict"` (design/339 §6.2) — a plan parked CONFLICT: at least one
* target did not reach its planned state (a concurrent write, a patch the store declined, or a
* target the re-judgment could no longer act on — the plan's audit rows carry the per-target
* reason). The REFUSED targets were not overwritten; patches the plan had already applied
* stand (discard rolls nothing back — the receipt's applied-vs-conflict counts say how many).
* Affected intents stay pending and wait for the host valve
* (`resolveConsolidationPlan` retry/discard); `detail: { planId, scope }`.
* - `"memory.consolidation_refused"` (design/339 §6.2) — the notice dialect of a consolidation
* verb's coded structured refusal (the verb itself throws
* {@link import("../core/memory-engine/consolidation.js").ConsolidationRefusedError});
* `detail: { refusalCode, scope?, occurrenceId? }`. `occurrenceId` (#495) is the THROWN ERROR's
* own id, minted once where the refusal happens and read off it by the notice factory — one
* throw rendered twice keeps one id, two refusals sharing a code/scope/message get two. Omitted
* (never fabricated) when the caller composes the line with no error in hand.
*
* The four SERVER-minted rows ({@link NOTICE_MINTER} `"server"`) — catalogued so their audience and detail shape
* are read here like every other row; this engine never mints them. A server forwards them through the same sink.
* - `"instructions.source_changed"` — the project instruction file this run injects is not the one the same
* session's previous run injected (a file added, removed or renamed). Audience `"user"`; `detail: { previous,
* current, sessionId }` — `previous` / `current` are chain-member relative paths, `null` = that run had none.
* No `runId`. Once per run.
* - `"session.records_skipped"` — the session's store skipped records it could not read when it opened the
* session; the run continues on the history that remains. Audience `"user"`; `detail: { dropped, entriesKept,
* sessionId }` — two observed counts, not to be combined. No `runId`. Once per open that skipped.
* - `"memory.layer_locked_legacy"` — a memory layer an older engine left without its owner-write bit was found at
* boot; deletes in it will fail until the operator restores the bit. Audience `"operator"`; `detail: { layers,
* remedy }` — layer directory names and the one command. No session. Once per boot.
* - `"server.model_unreachable"` — a model call of this leg ended in error and its last request failed to
* connect. Audience `"user"`; `detail: { code, targetHost, remedy, sessionId, proxyInUse? }` — `code` one of
* `UND_ERR_CONNECT_TIMEOUT` / `ECONNREFUSED` / `ENOTFOUND` / `EAI_AGAIN` / `EHOSTUNREACH` / `ENETUNREACH`;
* `targetHost` the model endpoint's `host:port`; `remedy` the constant `"check VPN / proxy; restart if the
* proxy changed since boot"`; `proxyInUse` present only when that request went through a proxy, its value the
* variable's name (`HTTP_PROXY` or `HTTPS_PROXY`), never the proxy address. No `runId`. Once per leg per target.
*
* Deliberately NOT a notice family: brain retry/reconnect liveness (a rate limit, a 5xx, a
* transient network failure being retried). Those are per-attempt liveness frames with their own
* frequency semantics and ride the wire `status` channel ({@link BrainStatus}), whose sink the
* Runner establishes unconditionally around every brain-driving call — no host seat to wire, no
* console fallback to flood. A reader looking for retry disclosure should look there, not here. */
code: string;
/** The exact human-readable line the unwired build prints via `console.warn` — same words, one text. */
message: string;
/** Machine-readable facts of the notice (knob names, arriving values, values in force). */
detail?: Record<string, unknown>;
/** The owning session, when the notice HAS one — an ATTRIBUTION/correlation key, never a
* projection permission: whether a notice may be surfaced onto that session's user-facing
* stream is decided by the code's {@link NOTICE_AUDIENCE} row alone ("audience is entitlement,
* not routability" — several `"operator"` rows carry a sessionId purely for correlation, and a
* projector keying on presence would push deployment facts at end users). Presence only makes
* routing structurally POSSIBLE; a session-less code structurally cannot be routed at all.
* Mint sites do not set this: {@link deliverEngineNotice} — the ONE delivery throat — lifts a
* string `detail.sessionId` here, so the typed key and the detail carriage can never disagree.
* Optional and absent for process/config-scoped codes. */
sessionId?: string;
}
/** Test seam (mirrors `__resetBashTimeoutAnnouncements`): never called by production code. */
export declare function __resetMalformedNoticeSeatAnnouncement(): void;
/**
* The ONE delivery form behind every {@link RunnerDeps.onNotice} emission point (#170: the form was
* triplicated across the emission stations, and every copy judged the seat with `!== undefined` — so a
* PRESENT non-function seat (null, a config typo, an untyped host's JSON wiring) entered the wired arm,
* threw `TypeError` on the call, and the swallow guard silenced BOTH channels at once). Contract:
* · a FUNCTION seat REPLACES the console line (a host forwarding notices to its own surface must not
* show every fact twice), swallow-guarded against both failure shapes the void-typed seat admits —
* a synchronous throw and an async sink's rejected promise (unhandled, that rejection is a
* process-level fault): a notice sink must never turn an announcement into a failure. Deliberately
* NO console fallback on sink failure: loudness ownership transfers with the wiring, and a console
* echo of a transient sink failure would double-send the fact. Corollary kept as-is: stations that
* de-duplicate ledger the line BEFORE this call, so a true-function sink that throws can in
* principle lose a ledgered line for the process lifetime — un-ledgering after the fact would race
* an async sink's late rejection and re-announce (double-send) on transient failures.
* · a PRESENT NON-function seat is a bad deployment value, and #123 (loud-bad-value law) forbids
* folding it to silence: the notice itself falls back to `console.warn` (no line is lost), and the
* seat defect — the one fact every fallback would otherwise repeat — is announced once per process.
* · an ABSENT seat prints the historic `console.warn` line verbatim (byte-compat loudness).
*/
export declare function deliverEngineNotice(onNotice: ((notice: EngineNotice) => void) | undefined, notice: EngineNotice): void;
/**
* #259 loud half, one notice PER FAMILY (`task.user_steer_undrained` / `task.user_followup_undrained`):
* a consumer routing on `code` alone must never mistake a stranded follow-up for a stranded steer, so
* the code carries exactly the semantics its name claims — the same two-key split the settled frame
* uses. Pure (the terminal sweep race window is not constructible deterministically; this seam is).
*
* `sessionId` (#433, additive and optional): these two codes are the `"user"` audience rows whose
* subject is the END USER's own lost input, and a disclosure that cannot say WHICH session lost it
* has nowhere to be delivered — the audience registry and the routing key are the two halves of one
* answer. Carried in `detail` like every other session-attributed code, so {@link deliverEngineNotice}
* — the one throat — lifts it to the typed top-level key and the two spellings cannot disagree.
* Omitted when the caller has none (never fabricated: the message is what the run lost, and a made-up
* routing key would deliver it to the wrong stream).
*
* `runId` (#499, additive and optional): THE key for this account. The two ids above name the task and
* the conversation, and both answer the same value for two runs of one session whenever the host
* supplies no task id — so two runs each stranding one steer produced two byte-identical rows, read as
* one repeated disclosure about one run. The account is per-RUN by its own contract sentence ("at most
* once per family, per run"), so it must carry the run's own id. Same omit-never-fabricate rule.
*/
/** What a settled leg still owes a person BEFORE the compensation lane has had its say: the facts the
* terminal-adoption lane holds, carried to the settle lane that closes the account
* ({@link settleOwedUserInput}). */
export interface OwedUserInputAccount {
/** The harness's at-agent_end snapshot, held because the tail still had something to say about it. */
readonly held: {
steer: number;
followUp: number;
} | undefined;
/** What a durable park's migration carried onto the new checkpoint, by family. */
readonly migrated: {
steer: number;
followUp: number;
};
/** The resumed leg's undelivered continuation frames, and how many of them the migration carried. */
readonly continuation: {
frames: ReadonlyArray<{
seq?: number;
}>;
carried: number;
};
}
/**
* What this leg still OWES a person, one count per family — the input to {@link undrainedUserInputNotices}.
* TWO lanes accept caller input and either can be holding some when a run ends: the harness's live queue
* (`held`, the at-agent_end snapshot, minus what a durable park migrated onto the new checkpoint) and a resumed
* leg's undelivered continuation frames (`owedContinuation` — the checkpoint's parked steers and the caller's
* wake message, minus what the same migration carried). They are ONE book: a frame is owed for the same reason
* on either lane — accepted, and never in the transcript — and counting the live queue alone is what let a leg
* that committed no pause drop a person's instruction in silence (the original row is already spent by then).
*/
export declare function owedUserInputCounts(held: {
steer: number;
followUp: number;
} | undefined, migrated: {
steer: number;
followUp: number;
}, continuation: {
frames: ReadonlyArray<{
seq?: number;
}>;
carried: number;
rowReturned: boolean;
}): {
steer: number;
followUp: number;
};
/** Close the account — the ONE settlement point, called AFTER the reopen compensation with what that
* compensation actually did. Ordering is the whole rule: the counts are arithmetic, and the only fact they
* cannot be computed without is one the lane before this one does not have yet. EXACTLY ONCE per account,
* which is what lets the teardown call it again as a backstop for a settle lane that threw on its way here
* (an account nobody closes is the silence this seat exists to end; a second row would be the same loss
* counted twice). Keyed on the account OBJECT — one per run, minted by the lane that assembles it. NEVER
* THROWS: a station calls it from a teardown path where the run is already failing, and a disclosure that
* takes the cleanup down with it is worse than the silence it was fixing (the absent-sink arm of
* {@link deliverEngineNotice} ends at a bare `console.warn`, which a host-replaced console can throw from). */
export declare function settleOwedUserInput(account: OwedUserInputAccount, rowReturned: boolean, ids: {
taskId?: string;
sessionId?: string;
runId?: string;
parent?: ParentLegIds | undefined;
}, onNotice: ((notice: EngineNotice) => void) | undefined): void;
/**
* Why a person's steer is reported as not consumed — `task.user_steer_undrained`'s `detail.cause`. Closed:
* · `leg_end` — the leg ended with the steer still owed (this engine mints it, at the leg's settlement);
* · `crashed` — the engine process ended before the leg settled, and a steer the serving layer had accepted for it was
* not confirmed consumed before the process died (a live leg gives the serving layer no evidence that a steer reached
* the model, so such a steer MAY have been consumed). Only the serving layer can count these (the queue was its own), so the serving layer
* mints this arm, before the next run of the session starts — the one member-level exception to
* {@link import("./governance-codes.js").NOTICE_MINTER}'s row for the code, which stays `"engine"`.
*/
export declare const USER_STEER_UNDRAINED_CAUSES: readonly ["leg_end", "crashed"];
export type UserSteerUndrainedCause = (typeof USER_STEER_UNDRAINED_CAUSES)[number];
/** Where a child leg's account is ALSO announced: its spawner's session, and that run's task id when the spawner carried one
* (it never carries the run id, and the row never invents one). */
interface ParentLegIds {
readonly sessionId: string;
readonly taskId?: string;
}
/**
* The parent a leg reports to, read off the trusted run-scoped channel (`RunInternals.parentSessionId` / `parentTaskId`,
* threaded by the delegation lanes from the spawner's own ids). `undefined` for a leg with no parent SESSION — a row
* with no session to address cannot be projected anywhere, so a bare `parentTaskId` names no one.
*/
export declare function parentLegIds(internals: {
readonly parentSessionId?: string;
readonly parentTaskId?: string;
} | undefined): ParentLegIds | undefined;
/**
* The rows for what a leg still owes a person. One session's view is the leg's own rows (`taskId` / `sessionId` /
* `runId` — each omitted when the caller has none, never fabricated). A leg that has a PARENT is announced to it as well,
* the way a child's folded permission mode is: after the leg's own rows come one copy per row, `detail.sessionId` the
* parent's (and `taskId` the parent run's, when carried), `childSessionId` the leg's own session — the key a
* consumer reads "a child reported this" from — and the message tail `(a child leg, <childSessionId>)`. The copy carries
* no `runId`: the account is per run and nothing carries the parent's. Only the DIRECT parent hears: a grandchild's
* row reaches its parent's session (the middle leg), not the root's. A parent that is no addressee — null, no or an empty session, or the leg's own session (which already
* hears the row) — gets no copy, and the call never throws on it.
*/
export declare function undrainedUserInputNotices(counts: {
steer: number;
followUp: number;
}, taskId?: string, sessionId?: string, runId?: string, parent?: ParentLegIds): EngineNotice[];
export {};