@sema-agent/core 5.58.0 → 5.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/dist/brain/anthropic.js +15 -5
  3. package/dist/brain/errors.d.ts +18 -1
  4. package/dist/brain/errors.js +7 -1
  5. package/dist/brain/input-too-long.d.ts +57 -0
  6. package/dist/brain/input-too-long.js +35 -0
  7. package/dist/brain/route-adjudicator.d.ts +8 -1
  8. package/dist/brain/route-adjudicator.js +8 -1
  9. package/dist/brain/stream-engine.js +9 -1
  10. package/dist/core/auto-compaction.js +2 -2
  11. package/dist/core/checkpoint-store.d.ts +116 -19
  12. package/dist/core/checkpoint-store.js +15 -8
  13. package/dist/core/context-edit.d.ts +243 -41
  14. package/dist/core/context-edit.js +247 -32
  15. package/dist/core/governance-codes.d.ts +37 -10
  16. package/dist/core/governance-codes.js +57 -1
  17. package/dist/core/locked-config.d.ts +36 -4
  18. package/dist/core/locked-config.js +34 -1
  19. package/dist/core/mcp.js +10 -6
  20. package/dist/core/memory-engine/consolidation-driver.d.ts +6 -2
  21. package/dist/core/memory-engine/consolidation-driver.js +54 -5
  22. package/dist/core/memory-engine/consolidation.d.ts +73 -1
  23. package/dist/core/memory-engine/consolidation.js +21 -1
  24. package/dist/core/memory-engine/content-origin.d.ts +24 -2
  25. package/dist/core/memory-engine/content-origin.js +6 -1
  26. package/dist/core/memory-engine/engine.d.ts +97 -8
  27. package/dist/core/memory-engine/engine.js +112 -20
  28. package/dist/core/memory-engine/file-backend.d.ts +13 -1
  29. package/dist/core/memory-engine/file-backend.js +3 -0
  30. package/dist/core/memory-engine/index.d.ts +4 -3
  31. package/dist/core/memory-engine/layout.js +20 -6
  32. package/dist/core/memory-engine/types.d.ts +17 -0
  33. package/dist/core/memory.d.ts +10 -0
  34. package/dist/core/park-selfcheck.js +1 -0
  35. package/dist/core/permission-rule-consent.js +9 -5
  36. package/dist/core/permission-rule-model.d.ts +42 -1
  37. package/dist/core/permission-rule-model.js +12 -0
  38. package/dist/core/runner/prepare-config-doors.d.ts +22 -1
  39. package/dist/core/runner/prepare-config-doors.js +36 -0
  40. package/dist/core/runner/prepare-task.d.ts +28 -1
  41. package/dist/core/runner/prepare-task.js +109 -11
  42. package/dist/core/runner/runtask.js +45 -8
  43. package/dist/core/store-contracts/checkpoint-store-contract.js +32 -0
  44. package/dist/core/tool-policy.d.ts +74 -0
  45. package/dist/core/tool-policy.js +80 -1
  46. package/dist/core/tools.js +1 -1
  47. package/dist/core/trace.d.ts +36 -0
  48. package/dist/core/types.d.ts +172 -22
  49. package/dist/core/types.js +4 -3
  50. package/dist/core/untrusted-text.d.ts +11 -0
  51. package/dist/core/untrusted-text.js +1 -0
  52. package/dist/engine/llm/types.d.ts +21 -2
  53. package/dist/engine/loop/agent-loop.js +7 -1
  54. package/dist/engine/loop/types.d.ts +4 -1
  55. package/dist/index.d.ts +3 -3
  56. package/dist/index.js +2 -2
  57. package/dist/tools/fs/fs-bash.js +1 -2
  58. package/package.json +1 -1
  59. package/test/export-surface.snapshot.json +1771 -1
@@ -900,6 +900,78 @@ export function coreMintedResolutionOf(d, call) {
900
900
  return undefined;
901
901
  return isAskDenyResolution(v.resolution) ? v.resolution : undefined;
902
902
  }
903
+ const BIDI_CONTROL_RE = /[\u061C\u200E\u200F\u202A-\u202E\u2066-\u2069]/u;
904
+ const BIDI_SCAN_MAX_NODES = 5_000;
905
+ const BIDI_SCAN_MAX_CHARS = 1_000_000;
906
+ export function carriesBidiControls(value, limits) {
907
+ let budget = limits?.maxNodes ?? BIDI_SCAN_MAX_NODES;
908
+ let charBudget = limits?.maxChars ?? BIDI_SCAN_MAX_CHARS;
909
+ const seen = new WeakSet();
910
+ const scan = (s) => {
911
+ if (charBudget <= 0)
912
+ return false;
913
+ const window = s.length <= charBudget ? s : s.slice(0, charBudget);
914
+ charBudget -= window.length;
915
+ return BIDI_CONTROL_RE.test(window);
916
+ };
917
+ const walk = (v) => {
918
+ if (budget-- <= 0)
919
+ return false;
920
+ if (typeof v === "string")
921
+ return scan(v);
922
+ if (typeof v !== "object" || v === null)
923
+ return false;
924
+ if (seen.has(v))
925
+ return false;
926
+ seen.add(v);
927
+ if (Array.isArray(v)) {
928
+ for (const el of v) {
929
+ if (walk(el))
930
+ return true;
931
+ if (budget <= 0)
932
+ return false;
933
+ }
934
+ return false;
935
+ }
936
+ if (v instanceof Map) {
937
+ for (const [k, val] of v) {
938
+ if (walk(k) || walk(val))
939
+ return true;
940
+ if (budget <= 0)
941
+ return false;
942
+ }
943
+ return false;
944
+ }
945
+ if (v instanceof Set) {
946
+ for (const el of v) {
947
+ if (walk(el))
948
+ return true;
949
+ if (budget <= 0)
950
+ return false;
951
+ }
952
+ return false;
953
+ }
954
+ for (const k in v) {
955
+ if (budget-- <= 0)
956
+ return false;
957
+ if (!Object.prototype.hasOwnProperty.call(v, k))
958
+ continue;
959
+ if (scan(k))
960
+ return true;
961
+ if (walk(v[k]))
962
+ return true;
963
+ if (budget <= 0 || charBudget <= 0)
964
+ return false;
965
+ }
966
+ return false;
967
+ };
968
+ try {
969
+ return walk(value);
970
+ }
971
+ catch {
972
+ return false;
973
+ }
974
+ }
903
975
  export async function resolveAsk(req, onAsk, signal) {
904
976
  const r = await resolveAskArms(req, onAsk, signal);
905
977
  if (r.action === "deny" && isAskDenyResolution(r.resolution))
@@ -954,7 +1026,14 @@ async function resolveAskArms(req, onAsk, signal) {
954
1026
  settledBy: "aborted",
955
1027
  };
956
1028
  }
957
- ok = await onAsk({ ...req, boundInputHash: boundInputHashOf(presented.value), args: approverView.value }, signal);
1029
+ const bidi = carriesBidiControls(presented.value) || carriesBidiControls(req.preview);
1030
+ const { hasBidiControls: _carried, ...bare } = req;
1031
+ ok = await onAsk({
1032
+ ...bare,
1033
+ boundInputHash: boundInputHashOf(presented.value),
1034
+ args: approverView.value,
1035
+ ...(bidi ? { hasBidiControls: true } : {}),
1036
+ }, signal);
958
1037
  }
959
1038
  catch (err) {
960
1039
  return {
@@ -45,7 +45,7 @@ export function defineTool(spec, options) {
45
45
  executionMode,
46
46
  ...(spec.isConcurrencySafe ? { isConcurrencySafe: spec.isConcurrencySafe } : {}),
47
47
  ...(spec.effect ? { effect: spec.effect } : {}),
48
- ...(spec.contentOrigin ? { contentOrigin: spec.contentOrigin } : {}),
48
+ ...(spec.contentOrigin !== undefined ? { contentOrigin: spec.contentOrigin } : {}),
49
49
  ...(spec.egress ? { egress: true } : {}),
50
50
  ...(spec.irreversibility !== undefined ? { irreversibility: spec.irreversibility } : {}),
51
51
  ...(spec.reversibilityProbe ? { reversibilityProbe: spec.reversibilityProbe } : {}),
@@ -729,6 +729,42 @@ export type TraceEvent = {
729
729
  /** Messages dropped from THIS request view. */
730
730
  dropped: number;
731
731
  ts: number;
732
+ } | {
733
+ /** design/374 (G11) — the microCompact clearing machine FIRED: `clearedCount` tool-result
734
+ * occurrences were replaced with markers in one pass. `trigger` names the arm: `"frontier"`
735
+ * = the proactive request-build pass (anchored estimate crossed the edit budget),
736
+ * `"refusal"` = the MC-R rejection-recovery arm (provider said input-too-long), `"blocking"`
737
+ * = the slice-3 pre-guard arm (reserved; not emitted before slice 3). SCOPE (X1): emitted
738
+ * only when the design/374 machinery is enabled — the `"frontier"` arm requires
739
+ * `microCompact.machine: "cc"`, the `"refusal"` arm requires `microCompact.clearOnRejection`;
740
+ * the legacy DEFAULT machine clears silently exactly as pre-374 (its clears are ledger-less
741
+ * and re-fire per request, so a frame there would re-count the same occurrences and break
742
+ * this frame's cardinality clause). Cardinality: exactly ONE frame per firing pass — a retry
743
+ * chain re-sending an already-cleared view emits none. A `"refusal"` frame also marks one
744
+ * attempt of the shared prompt-too-long recovery budget spent (the MC-R arm's retry rides
745
+ * the same per-chain account the forced-compaction arm draws on — the knob doc on
746
+ * `RunnerDeps.microCompact` carries the full accounting). */
747
+ kind: "context.mc_clear";
748
+ version: 1;
749
+ taskId: string;
750
+ clearedCount: number;
751
+ /** The pass's structural savings estimate (the ≥20k gate's own number). */
752
+ tokensSavedEstimate: number;
753
+ trigger: "frontier" | "refusal" | "blocking";
754
+ ts: number;
755
+ } | {
756
+ /** design/374 (G11) — the MC-R rejection arm ran and DECLINED to clear: `reason` says why
757
+ * (`"no_candidates"` = nothing clearable beyond the keep window on the rejected projection;
758
+ * `"below_min_savings"` = clearable but under the 20k gate), `nextArm` names where the
759
+ * recovery chain goes instead (`"forced_compaction"` when the compaction arm is available,
760
+ * `"none"` when it is disabled/tripped — the turn will surface the provider error). Emitted
761
+ * only on the refusal arm; the frontier pass declines silently every request by design. */
762
+ kind: "context.mc_null";
763
+ version: 1;
764
+ taskId: string;
765
+ reason: "no_candidates" | "below_min_savings";
766
+ nextArm: "forced_compaction" | "none";
767
+ ts: number;
732
768
  } | {
733
769
  /** C8 — the per-turn aggregate tool-result budget capped a result (offloaded to the store, or
734
770
  * degraded to a self-contained truncation preview when the store failed/absent). */
@@ -353,7 +353,14 @@ export interface ToolSpec<TParams extends TSchema = TSchema> {
353
353
  * - `"local"` — purely local reads/computation.
354
354
  * Classification is single-sourced with the tool definition: core built-ins declare theirs here;
355
355
  * a HOST tool that omits it is classified fail-closed as `"external"` (unknown = external) unless
356
- * the deployment exempts the name via `TaskSpec.memory.trustedTools`.
356
+ * the deployment exempts the name via `TaskSpec.memory.trustedTools`. A declaration BEATS that
357
+ * allowlist (the allowlist is the channel for UNDECLARED tools, never a lever to re-grade a tool
358
+ * that classified itself), and only a member of the closed vocabulary counts as one — an unreadable
359
+ * value classifies `"external"` rather than exempting anything.
360
+ *
361
+ * Tools core mints on the deployment's behalf get the same seat one level up: an MCP server's whole
362
+ * tool set is declared at its entry ({@link McpServerSpec.contentOrigin}, design/378), because the
363
+ * host never touches those tool objects.
357
364
  */
358
365
  contentOrigin?: ToolContentOrigin;
359
366
  /**
@@ -1299,7 +1306,11 @@ export interface McpServerSpec {
1299
1306
  */
1300
1307
  principalHeader?: string;
1301
1308
  };
1302
- /** Optional allowlist of tool names to expose (others are dropped). */
1309
+ /** Optional allowlist of tool names to expose (others are dropped). ORTHOGONAL to
1310
+ * {@link contentOrigin} and usefully paired with it (design/378 §2): the class declaration follows
1311
+ * the server's roster, so a high-assurance deployment that wants a CLOSED tool set writes both —
1312
+ * a tool a later refresh adds then lands outside this list and is simply not mounted. Existing
1313
+ * semantics, no new mechanism; the refresh receipt still names every added tool either way. */
1303
1314
  allowTools?: string[];
1304
1315
  /**
1305
1316
  * design/99 §E23 — opt in to INBOUND elicitation for THIS server: when `true` AND a {@link RunnerDeps.onElicit}
@@ -1316,12 +1327,66 @@ export interface McpServerSpec {
1316
1327
  * override is AUTHORITATIVE and may LOWER an effect (vouch a tool is `read`/`idempotent`) as well as raise it
1317
1328
  * (`egress` / irreversible). This is the ONLY trusted way to drop an MCP tool below the fail-closed `write`
1318
1329
  * default (design F). Folds over the server hints in prepare-task (caller > server hint > fail-closed write).
1330
+ *
1331
+ * A DIFFERENT AXIS from {@link contentOrigin}: this one is about what a call DOES (repeat-safety,
1332
+ * blast radius, reversibility — it feeds the approval gate); the content class is about what a call
1333
+ * BRINGS BACK (memory-write governance, no gate/policy/roster effect). Neither implies the other —
1334
+ * a read-only tool can return third-party text, and a deployment's own writer brings back nothing
1335
+ * external — so vouching on one axis never quietly vouches on the other.
1319
1336
  */
1320
1337
  toolAxes?: Record<string, {
1321
1338
  effect?: ToolEffect;
1322
1339
  egress?: boolean;
1323
1340
  irreversibility?: "always" | "never";
1324
1341
  }>;
1342
+ /**
1343
+ * design/378 — declare {@link ToolSpec.contentOrigin} on behalf of THIS SERVER'S ENTIRE TOOL SET
1344
+ * (tools a mid-task refresh adds included), with the same authority and the same responsibility a
1345
+ * directly-mounted host tool's own declaration carries.
1346
+ *
1347
+ * WHY THE SEAT EXISTS. Without it the class keys on the MOUNT SHAPE rather than on lineage: a tool
1348
+ * the deployment wrote and runs itself is structurally `"external"` the moment it arrives over the
1349
+ * MCP protocol namespace, so every call marks the session's memory externally exposed. The
1350
+ * per-name channels cannot express the fact either — {@link import("./memory.js").MemorySpecInput.trustedTools}
1351
+ * is a per-REQUEST allowlist keyed on the MINTED name (the host would have to predict the charset
1352
+ * normalization) whose own definition is "the exception channel for UNDECLARED tools", and it says
1353
+ * nothing about tools the server adds later.
1354
+ *
1355
+ * THIS IS A TRUST DECLARATION, not a routing hint. Use it only for servers inside the deployment's
1356
+ * trust boundary — a process, socket or service the deployment itself runs. Declaring a THIRD-PARTY
1357
+ * server means treating its output as content the deployment wrote: the class names are
1358
+ * BOUNDARY-relative, never topological, so neither the transport kind nor the address is evidence
1359
+ * of lineage (a stdio child can be an untrusted package; a loopback URL can be your own service) and
1360
+ * core deliberately does not gate on either. The declaration covers THE PEER THIS ENTRY CONNECTS TO
1361
+ * — authenticating that peer (socket permissions, credentials) is the host's mounting duty.
1362
+ *
1363
+ * SEMANTICS. `"local"` ⇒ invocations no longer mark this session's memory; `"execution"` ⇒ the same,
1364
+ * except that {@link import("./memory.js").MemorySpecInput.execIsExternalContent} can still upgrade
1365
+ * the class for a strict deployment (which is why this seat takes the three-value vocabulary and not
1366
+ * a single "mine" flag — an execution-shaped tool mounted over MCP must stay inside that knob's
1367
+ * reach); `"external"` is an explicit PIN, and pins are not no-ops — a declaration beats the
1368
+ * `trustedTools` allowlist, so writing it forecloses the per-name exemption for this server's tools.
1369
+ * ABSENT ⇒ the pre-378 behavior byte for byte: the protocol namespace classifies the tools
1370
+ * `"external"` (fail-closed). A value outside the vocabulary is refused at the preparation door
1371
+ * (`config.mcp_content_class`), never folded to a class.
1372
+ *
1373
+ * COVERAGE, stated honestly: the class rides this server's own mounted tools. The cross-server
1374
+ * resource faces (ListMcpResourcesTool / ReadMcpResourceTool / ReadMcpResourceDirTool) aggregate
1375
+ * over every connected server in one call, so they stay `"external"` and still mark — over-marking,
1376
+ * the safe direction. A delegated child's pool is a separate static declaration surface
1377
+ * ({@link ToolSpec.agentToolPool} entries carry their own `contentOrigin`): a deployment handing
1378
+ * this server's tools to children mirrors the value there, and not mirroring it over-marks.
1379
+ *
1380
+ * TRUST SOURCE — a DEPLOYMENT-plane key. It redefines where the trust boundary runs, which puts it
1381
+ * on the same authority plane as {@link RunnerDeps} wiring, not on the request plane. Core sees one
1382
+ * `TaskSpec` and cannot tell a deployment-baseline entry from one a request supplied, so any
1383
+ * assembly layer that accepts REQUEST-side MCP entries must reject or strip this key from them:
1384
+ * "allowed to mount a server" is not "allowed to redefine the deployment's trust boundary", and a
1385
+ * caller-supplied `"local"` would otherwise be self-authorization around the session mark. A
1386
+ * single-tenant superuser surface (a host reading its own `--mcp-config` file) IS the deployment
1387
+ * plane and needs no such gate.
1388
+ */
1389
+ contentOrigin?: ToolContentOrigin;
1325
1390
  }
1326
1391
  /**
1327
1392
  * Definition of one A2A (agent-to-agent protocol) PEER to talk to for the duration of one task.
@@ -4830,6 +4895,11 @@ export interface ProjectMemoryLoad {
4830
4895
  */
4831
4896
  export interface EngineNotice {
4832
4897
  /** Stable machine-readable family, dot-namespaced. Current families:
4898
+ * - `"config.autocompact_window_clamped"` (design/374 slice 1b) — a model declared
4899
+ * `autoCompactTokens` ABOVE its physical window; the trigger-side geometry was clamped to the
4900
+ * physical window (an autocompact window only ever lowers the trigger) and the bad value is
4901
+ * announced once per prepared task; `detail: { modelId, declaredAutoCompactTokens,
4902
+ * physicalWindow, sessionId }`.
4833
4903
  * - `"config.env_timeout_discarded"` — a Bash timeout knob (option or env) held a value that is not
4834
4904
  * the value in force; `detail: { knob, raw, usedMs }`.
4835
4905
  * - `"config.materialize_env_discarded"` — `SEMA_TOOL_MATERIALIZE_STRATEGY` held a value outside the
@@ -4850,8 +4920,9 @@ export interface EngineNotice {
4850
4920
  * probe threw; MCP dispatch FAILS OPEN (revocation is a tightening face) and this announces
4851
4921
  * once per materialization (a resume re-materializes and may announce again). `detail: { message }`
4852
4922
  * — no sessionId (a deployment wiring fact, not session-attributed), `"operator"` audience by
4853
- * the {@link NOTICE_AUDIENCE} default. The refusal itself (`mcp.server_revoked`) is a tool
4854
- * RESULT code, not a notice.
4923
+ * its explicit {@link NOTICE_AUDIENCE} row (#433 made the registry total over the catalog —
4924
+ * no engine-minted code is audience-defaulted any more). The refusal itself
4925
+ * (`mcp.server_revoked`) is a tool RESULT code, not a notice.
4855
4926
  * - `"config.models_swapped"` — `Runner.swapModels` replaced the model catalog generation
4856
4927
  * (zero-restart model switching). `detail: { models, tiers }` — key COUNTS only, never the
4857
4928
  * catalog itself. In-flight tasks finish on the models they resolved at prepare (natural
@@ -4860,8 +4931,11 @@ export interface EngineNotice {
4860
4931
  * - `"route.fallback_to_primary"` (key↔URL pairing, `src/brain/route-adjudicator.ts`) — a
4861
4932
  * DERIVED-leg model (role/tier/system-default resolution, never a caller-explicit one) failed
4862
4933
  * the pairing pre-flight and the seat fell back to the primary model instead of sinking the
4863
- * task; the notice is the loud half of that swap. `detail: { seat, from, to, cause, fixHint }`
4864
- * — `cause` is the refusal code (`route.credential_mismatch` / `route.credential_missing`).
4934
+ * task; the notice is the loud half of that swap.
4935
+ * `detail: { seat, from, to, cause, fixHint, sessionId? }` — `cause` is the refusal code
4936
+ * (`route.credential_mismatch` / `route.credential_missing`); `sessionId` (#433, additive)
4937
+ * rides when the adjudicating seat knows its session — a correlation key only, the audience
4938
+ * stays `"operator"` (see the registry's entitlement-not-routability note).
4865
4939
  * Explicitly-named models never mint this: they refuse at the brain's request gate instead.
4866
4940
  * - `"route.base_url_changed_key_unchanged"` (key↔URL pairing) — `Runner.swapModels` moved a
4867
4941
  * same-name entry's `baseUrl` while its Model-visible credential half (auth-bearing headers)
@@ -4906,8 +4980,10 @@ export interface EngineNotice {
4906
4980
  * frame's two count keys). They are NOT redelivered (a steer aimed at a finished run must not
4907
4981
  * fire at the next one — unlike ENGINE notes, which pend per session); the loud half of the
4908
4982
  * #257 contract's "accepted = enqueued, not consumed" sentence;
4909
- * `detail: { steer, taskId? }` / `{ followUp, taskId? }`. Per-run, at most once per family
4910
- * (the terminal sweep is a single site).
4983
+ * `detail: { steer, taskId?, sessionId? }` / `{ followUp, taskId?, sessionId? }` (`sessionId`
4984
+ * = #433's routing half for these two `"user"`-audience rows — see
4985
+ * {@link undrainedUserInputNotices}; omitted when the caller has none, never fabricated).
4986
+ * Per-run, at most once per family (the terminal sweep is a single site).
4911
4987
  * **#389 (two corrections).** ① The family now fires on the INTERRUPT path too: `abort()` used
4912
4988
  * to empty both queues before agent_end could count them, so the one loss path an operator most
4913
4989
  * needs to hear about was the one path that stayed silent. ② On a DURABLE PARK the verdict is
@@ -4924,7 +5000,10 @@ export interface EngineNotice {
4924
5000
  * notification would be the worse error); what is announced is that the knob's promise is not
4925
5001
  * honored, so an operator can stop building on it. Once per run (the injection funnel is a
4926
5002
  * single site, and a busy lane must not narrate the same gap once per frame);
4927
- * `detail: { priority, taskId? }`. An UNKNOWN priority value is a different fact with a
5003
+ * `detail: { priority, taskId?, sessionId }` `sessionId` (#433, ALWAYS present: the funnel
5004
+ * sits past prepare, which owns a session unconditionally) is a correlation key only, the
5005
+ * audience stays `"operator"` (the party building on the unhonored knob is whoever called
5006
+ * notify, not the end user). An UNKNOWN priority value is a different fact with a
4928
5007
  * different posture — `TaskStream.notify` refuses it typed (`notify.invalid_payload`).
4929
5008
  *
4930
5009
  * - `"memory.session_polluted"` (design/178 §3, #324a; message mode-aware since design/336) —
@@ -4988,16 +5067,40 @@ export interface EngineNotice {
4988
5067
  * `detail: { reason, subagentType?, sessionId? }` — `reason` is the same sentence the waived
4989
5068
  * mark would have carried, neutralized/length-bounded (tool and agent-type names are
4990
5069
  * host/model-controlled inputs).
5070
+ * - `"memory.content_class_declared"` (design/378) — an `McpServerSpec` entry carries an explicit
5071
+ * {@link McpServerSpec.contentOrigin}: the deployment declared this server's whole tool set to
5072
+ * be inside (or explicitly outside) its trust boundary, so the protocol namespace's structural
5073
+ * `"external"` no longer decides. The AUDIT line for a trust boundary an operator drew by
5074
+ * configuration — which is why it announces on the explicit `"external"` value too: pinning is
5075
+ * not a no-op (a declaration beats the `trustedTools` allowlist, so it forecloses the per-name
5076
+ * exemption for this server's tools). One line per DECLARED ENTRY per prepared task leg, minted
5077
+ * once at preparation and never per call (the declaration's whole effect is that calls stop
5078
+ * marking; narrating each call would trade the saved mark for equal noise). Armed on the SAME
5079
+ * condition as the classification it talks about — a leg with no engine-memory session and no
5080
+ * provenance recorder classifies nothing, so there is no posture to report and no line is
5081
+ * minted. `detail: { server,
5082
+ * contentOrigin, toolCount, execIsExternalContent?, sessionId? }` — `server` is the
5083
+ * host-authored entry name, neutralized/length-bounded; `toolCount` is what this entry actually
5084
+ * mounted (0 for a server that failed to connect — the declaration still stands and is still
5085
+ * disclosed); `execIsExternalContent` rides the `"execution"` value only and reports the strict
5086
+ * knob AS RESOLVED FOR THIS RUN, because that arm's posture depends on it. The message's closing
5087
+ * clause is written per value so it can never describe a posture the run does not have. The
5088
+ * entry's TRANSPORT is deliberately absent: it is a claim about a mutable host-owned object made
5089
+ * long after the dial, and D-10 already rules topology is not evidence of the lineage this line
5090
+ * audits — connection facts live on `MaterializedMcp.statuses` instead.
4991
5091
  *
4992
5092
  * - `"memory.consolidation_incomplete"` (design/376, LLM consolidation driver) — a driver run
4993
5093
  * settled without reaching the fixpoint: `detail` names the stop reason (the closed
4994
5094
  * `ConsolidationRunStopReason` set), cycles done, and the residue (write-failure or
4995
5095
  * fuse-refused groups by name). Advisory: committed cycles stand (add-only, never rolled
4996
5096
  * back); the recovery verb is re-running the host driver, which resumes the same pending run.
4997
- * - `"memory.consolidation_driver_superseded"` (same family) a concurrent driver invocation
4998
- * took over this scope's run row (attempt fencing): the losing worker stops with this code
4999
- * and MUST NOT retry into the winner's account. Coordination fact between two host calls,
5000
- * not a principal ruling listed in the non-governance disposition table.
5097
+ * - `"memory.consolidation_driver_superseded"` is an ERROR code, not a notice (named here only
5098
+ * to keep the family's spellings in one place): a concurrent driver invocation took over this
5099
+ * scope's run row (attempt fencing) and the losing worker's call THROWS with this `code`
5100
+ * it never crosses {@link deliverEngineNotice}, is deliberately absent from
5101
+ * {@link ENGINE_NOTICE_CODES}/{@link NOTICE_AUDIENCE}, and sits in the non-governance
5102
+ * disposition table with the error dispositions. The loser MUST NOT retry into the winner's
5103
+ * account. A consumer diffing its own table against the catalog must not add a row for it.
5001
5104
  *
5002
5105
  * - `"delegation.transcript_integrity"` (subagent transcript persistence) — a durable agent row
5003
5106
  * with a BOUND transcript sessionId met a session store that attests `not_found` for it: the
@@ -5005,8 +5108,7 @@ export interface EngineNotice {
5005
5108
  * most once per (scope, handle, process) — `detail: { handle, scope? }`, scope = the resolved
5006
5109
  * access scope of the read that found the gap — from the continuation read faces (SendMessage preflight /
5007
5110
  * AgentTranscript's durable leg); the per-call honest refusals are unchanged, and the declared
5008
- * tier is NOT auto-downgraded (declaration-制 — observation reports, it never re-adjudicates);
5009
- * `detail: { handle }`.
5111
+ * tier is NOT auto-downgraded (declaration-制 — observation reports, it never re-adjudicates).
5010
5112
  *
5011
5113
  * - `"memory.consolidation_recommended"` (design/339 §2.2/§6.2) — the engine-minted per-scope
5012
5114
  * session count crossed the consolidation thresholds (time gate open ∧ enough distinct
@@ -5039,11 +5141,15 @@ export interface EngineNotice {
5039
5141
  message: string;
5040
5142
  /** Machine-readable facts of the notice (knob names, arriving values, values in force). */
5041
5143
  detail?: Record<string, unknown>;
5042
- /** The owning session, when the notice HAS one — the wire-routing attribution key (a
5043
- * session-scoped notice can be projected onto that session's event stream; a session-less code
5044
- * structurally cannot). Mint sites do not set this: {@link deliverEngineNotice} the ONE
5045
- * delivery throatlifts a string `detail.sessionId` here, so the typed key and the detail
5046
- * carriage can never disagree. Optional and absent for process/config-scoped codes. */
5144
+ /** The owning session, when the notice HAS one — an ATTRIBUTION/correlation key, never a
5145
+ * projection permission: whether a notice may be surfaced onto that session's user-facing
5146
+ * stream is decided by the code's {@link NOTICE_AUDIENCE} row alone ("audience is entitlement,
5147
+ * not routability"several `"operator"` rows carry a sessionId purely for correlation, and a
5148
+ * projector keying on presence would push deployment facts at end users). Presence only makes
5149
+ * routing structurally POSSIBLE; a session-less code structurally cannot be routed at all.
5150
+ * Mint sites do not set this: {@link deliverEngineNotice} — the ONE delivery throat — lifts a
5151
+ * string `detail.sessionId` here, so the typed key and the detail carriage can never disagree.
5152
+ * Optional and absent for process/config-scoped codes. */
5047
5153
  sessionId?: string;
5048
5154
  }
5049
5155
  /** Test seam (mirrors `__resetBashTimeoutAnnouncements`): never called by production code. */
@@ -5073,11 +5179,19 @@ export declare function deliverEngineNotice(onNotice: ((notice: EngineNotice) =>
5073
5179
  * a consumer routing on `code` alone must never mistake a stranded follow-up for a stranded steer, so
5074
5180
  * the code carries exactly the semantics its name claims — the same two-key split the settled frame
5075
5181
  * uses. Pure (the terminal sweep race window is not constructible deterministically; this seam is).
5182
+ *
5183
+ * `sessionId` (#433, additive and optional): these two codes are the `"user"` audience rows whose
5184
+ * subject is the END USER's own lost input, and a disclosure that cannot say WHICH session lost it
5185
+ * has nowhere to be delivered — the audience registry and the routing key are the two halves of one
5186
+ * answer. Carried in `detail` like every other session-attributed code, so {@link deliverEngineNotice}
5187
+ * — the one throat — lifts it to the typed top-level key and the two spellings cannot disagree.
5188
+ * Omitted when the caller has none (never fabricated: the message is what the run lost, and a made-up
5189
+ * routing key would deliver it to the wrong stream).
5076
5190
  */
5077
5191
  export declare function undrainedUserInputNotices(counts: {
5078
5192
  steer: number;
5079
5193
  followUp: number;
5080
- }, taskId?: string): EngineNotice[];
5194
+ }, taskId?: string, sessionId?: string): EngineNotice[];
5081
5195
  /** Runtime dependencies shared across tasks. */
5082
5196
  export interface RunnerDeps {
5083
5197
  brain: Brain;
@@ -5094,7 +5208,9 @@ export interface RunnerDeps {
5094
5208
  * probe must not brick every MCP call) with a once-per-MATERIALIZATION `mcp.revocation_probe_failed`
5095
5209
  * notice (a resume re-materializes and may announce again — the standing condition is re-news at
5096
5210
  * each fresh mount, never per-call). The notice is a deployment wiring fact: `detail: { message }`
5097
- * only, no session attribution, `"operator"` audience by the {@link NOTICE_AUDIENCE} default — a
5211
+ * only, no session attribution, `"operator"` audience by its explicit {@link NOTICE_AUDIENCE}
5212
+ * row (#433 made the registry total over the catalog — no engine-minted code is
5213
+ * audience-defaulted any more) — a
5098
5214
  * wire projector forwards it operator-tier and needs no per-session de-duplication of its own.
5099
5215
  */
5100
5216
  mcpRevocations?: {
@@ -6045,6 +6161,40 @@ export interface RunnerDeps {
6045
6161
  * ~20000; set `0` or `Infinity` to disable offloading. Per-tool override via `ToolSpec.offloadThresholdChars`.
6046
6162
  */
6047
6163
  toolResultThresholdChars?: number;
6164
+ /**
6165
+ * design/374 — microCompact machine-alignment knobs (EXPERIMENTAL until the slice-3 default
6166
+ * flip; both default OFF so a deployment that never touches this bag runs the pre-374 machine
6167
+ * byte for byte).
6168
+ *
6169
+ * - `machine`: which stale-tool-result clearing machine the request pipeline runs —
6170
+ * `"legacy"` (default; the historical keep-3 / clear-to-budget machine) or `"cc"` (the CC
6171
+ * 2.1.223 rejection-leg form: keep 5, ≥20k minimum-savings gate, one deep clear beyond the
6172
+ * keep window, CC marker bytes). ⚠️ Read the P-form warning on
6173
+ * {@link import("./context-edit.js").ContextEditMachine} before selecting `"cc"`: until the
6174
+ * slice-3 fallback re-ordering ships, the 20k gate sits in front of the only reduction while
6175
+ * the message-dropping guard trim still backstops — opting in is accepting that trade.
6176
+ * - `clearOnRejection` (MC-R, slice 2): on a provider input-too-long rejection, run ONE cheap
6177
+ * deterministic clear over the rejected projection (same cc machine, savings ≥20k or nothing)
6178
+ * and retry inside the turn BEFORE the forced-compaction recovery. Default false (X2: the
6179
+ * machinery lands dark; the default flips together with the machine in slice 3). Independent
6180
+ * of `machine` — an enabled MC-R always clears in the cc form (the rejection arm has no
6181
+ * budget coordinate for the legacy incremental form to stop at). BUDGET ACCOUNTING (design/374
6182
+ * §3.2.1, stated here because it is otherwise invisible to a deployment): a successful MC-R
6183
+ * clear-and-retry SPENDS one attempt of the shared prompt-too-long recovery budget (default 2
6184
+ * attempts per chain), so a chain that clears and is rejected AGAIN has one forced-compaction
6185
+ * attempt left where the knob-off chain nominally had two — the trade costs no effective
6186
+ * compaction pass, because the second forced-compaction call of the off chain is structurally
6187
+ * a no-op whenever the first one landed (the branch leaf is already a compaction entry).
6188
+ *
6189
+ * A declaration outside the closed vocabulary (a `machine` string not in the union, a
6190
+ * non-boolean `clearOnRejection` — JSON/env-derived config the type cannot guard) refuses the
6191
+ * whole prepare loudly (`code: "config.microcompact_invalid"`, no silent re-default): folding it
6192
+ * would run the pre-374 machine while the deployment believes it opted in.
6193
+ */
6194
+ microCompact?: {
6195
+ machine?: "legacy" | "cc";
6196
+ clearOnRejection?: boolean;
6197
+ };
6048
6198
  /**
6049
6199
  * Two-phase prefix-cache-break detection (design/31): per turn, fingerprint the prefix and, on a
6050
6200
  * confirmed `cacheRead` drop, emit a root-cause finding via `onError(phase:"prompt-cache")`. Cheap
@@ -52,22 +52,23 @@ export function deliverEngineNotice(onNotice, notice) {
52
52
  }
53
53
  console.warn(notice.message);
54
54
  }
55
- export function undrainedUserInputNotices(counts, taskId) {
55
+ export function undrainedUserInputNotices(counts, taskId, sessionId) {
56
56
  const tid = taskId !== undefined ? { taskId } : {};
57
+ const sid = sessionId !== undefined ? { sessionId } : {};
57
58
  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.`;
58
59
  const out = [];
59
60
  if (counts.steer > 0) {
60
61
  out.push({
61
62
  code: "task.user_steer_undrained",
62
63
  message: `${counts.steer} user steer(s) ${tail}`,
63
- detail: { steer: counts.steer, ...tid },
64
+ detail: { steer: counts.steer, ...tid, ...sid },
64
65
  });
65
66
  }
66
67
  if (counts.followUp > 0) {
67
68
  out.push({
68
69
  code: "task.user_followup_undrained",
69
70
  message: `${counts.followUp} user follow-up(s) ${tail}`,
70
- detail: { followUp: counts.followUp, ...tid },
71
+ detail: { followUp: counts.followUp, ...tid, ...sid },
71
72
  });
72
73
  }
73
74
  return out;
@@ -393,6 +393,17 @@ export declare function defuseControlChars(text: string): string;
393
393
  * Defense-in-depth, NOT a guarantee (same posture as the rest of this module).
394
394
  */
395
395
  export declare function inlineUntrusted(text: string, maxLen?: number): string;
396
+ /** backlog #239: the per-ENTRY ceiling inside a `ProbeCause` operand family (checkpoint-store's
397
+ * structured probe account), sized like the descriptor's own `touchedPaths` entries (the same thing
398
+ * on the same card). Per entry, not per cause: that is the whole point of the structured shape —
399
+ * one pathological path costs only itself, where a single joined string let it consume every other
400
+ * entry's room. HOMED HERE (the leaf neutralizer module) rather than with the `ProbeCause` types:
401
+ * its two consumers — the shell probe that sanitizes at the entry boundary (tools/fs) and the
402
+ * descriptor builder that re-sanitizes at the persist boundary (core/checkpoint-store) — sit on
403
+ * opposite sides of a value-import boundary, and a value import from the shell tool back into
404
+ * checkpoint-store closed an ESM evaluation cycle (checkpoint-store → tool-policy → tools/fs →
405
+ * checkpoint-store, a latent TDZ). Both already import this module; one cap, no cycle. */
406
+ export declare const PROBE_CAUSE_PATH_MAX = 200;
396
407
  /** The one body bound every "reviewer note" relay passes to {@link delimitUntrusted} — the decider's
397
408
  * free text attached to a deny (sync `AskOutcome.reason` and the durable `ResumeOutcome` `reason`
398
409
  * legs alike). A note is steering, not payload: unbounded it can flood the transcript/context the
@@ -568,6 +568,7 @@ export function inlineUntrusted(text, maxLen = LABEL_MAX) {
568
568
  function sanitizeLabel(label) {
569
569
  return inlineUntrusted(label, LABEL_MAX);
570
570
  }
571
+ export const PROBE_CAUSE_PATH_MAX = 200;
571
572
  export const REVIEWER_NOTE_MAX_BODY = 2048;
572
573
  export function delimitUntrusted(label, text, maxBody) {
573
574
  return delimitUntrustedWithClip(label, text, maxBody).text;
@@ -413,8 +413,27 @@ export interface AssistantMessage {
413
413
  * ⚠️ Brain OBLIGATION (RB-482 #12, 5.1.0): stamping this field on every cut turn is part of the
414
414
  * Brain contract — the sentinel-prose fallback readers are RETIRED, so a custom Brain that stamps
415
415
  * only `errorMessage` prose is no longer recognized as a cut (terminal-cause.ts reads this field
416
- * and nothing else). Widening note: exhaustive switches on this union gain arms as it grows. */
417
- errorKind?: "length_empty" | "degenerate";
416
+ * and nothing else). Widening note: exhaustive switches on this union gain arms as it grows.
417
+ *
418
+ * `"input_too_long"` (design/374 slice 2): the provider refused because the request INPUT alone
419
+ * exceeds its context limit — classified at the brain boundary from provenance-checked signals
420
+ * only (`brain/input-too-long.ts`: the diagnostic field of a JSON error envelope, anchored
421
+ * provider openings, the OpenAI `context_length_exceeded` structural code, HTTP 413, and the
422
+ * Anthropic in-band `model_context_window_exceeded` stop / streamed `invalid_request_error`
423
+ * twin). The loop's prompt-too-long recovery reads THIS seat first; the historic prose regex
424
+ * over `errorMessage` remains as the fallback for brains that do not stamp it (additive — the
425
+ * stamping obligation above extends to this member for cuts a Brain can classify structurally,
426
+ * but unlike the two cut kinds there is no retired fallback: prose detection still works). */
427
+ errorKind?: "length_empty" | "degenerate" | "input_too_long";
428
+ /** design/374 slice 2, the AUTHORITATIVE-NEGATIVE twin of `errorKind: "input_too_long"`: the
429
+ * brain's provenance-checked classifier RAN and ruled this failure NOT input-too-long (wrong
430
+ * status family, or the provider's own diagnostic envelope states a different error). The
431
+ * loop's prompt-too-long recovery treats it as a hard negative and SKIPS its prose fallback —
432
+ * a "prompt is too long" sentence reflected inside an unrelated error body must never drive a
433
+ * context-shrinking recovery (adversarial-review r1). Absent = classification could not
434
+ * establish provenance (prose-only gateway, truncated body, non-classifying custom Brain), and
435
+ * the historic prose fallback keeps its reach. Mutually exclusive with the positive stamp. */
436
+ inputTooLongRuledOut?: true;
418
437
  /**
419
438
  * design/124 tier A: this final message was PARTIALLY FINALIZED after a mid-stream connection
420
439
  * loss/stall — the substantive streamed prefix (text and/or completed tool calls) was promoted to
@@ -39,7 +39,13 @@ function isBlankFailureContent(message) {
39
39
  return message.content.every((c) => c.type === "text" ? c.text.trim() === "" : c.type === "thinking" ? c.thinking.trim() === "" : false);
40
40
  }
41
41
  function defaultDetectPromptTooLong(message) {
42
- return message.stopReason === "error" && PROMPT_TOO_LONG_RE.test(message.errorMessage ?? "");
42
+ if (message.stopReason !== "error")
43
+ return false;
44
+ if (message.errorKind === "input_too_long")
45
+ return true;
46
+ if (message.inputTooLongRuledOut === true)
47
+ return false;
48
+ return PROMPT_TOO_LONG_RE.test(message.errorMessage ?? "");
43
49
  }
44
50
  function createPtlWithholdBuffer(emit, detect) {
45
51
  const held = [];
@@ -161,7 +161,10 @@ export interface LoopPromptTooLongRecovery {
161
161
  * Contract: must not throw or reject.
162
162
  */
163
163
  recover: (messages: AgentMessage[], attempt: number) => Promise<AgentMessage[] | undefined>;
164
- /** Override the prompt-too-long classifier. Default: a conservative provider-message pattern. */
164
+ /** Override the prompt-too-long classifier. Default (design/374 slice 2): the TYPED cause first
165
+ * (`errorKind: "input_too_long"`, stamped by the brains from provenance-checked provider
166
+ * signals), then the conservative provider-message prose pattern as the fallback for brains
167
+ * that do not stamp it. */
165
168
  detect?: (message: AssistantMessage) => boolean;
166
169
  /** Max recovery retries per turn. Default: 2. */
167
170
  maxRetries?: number;