@sema-agent/core 7.37.1 → 7.38.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 (133) hide show
  1. package/CHANGELOG.md +135 -0
  2. package/dist/agents/observer.js +1 -1
  3. package/dist/agents/peer-notices.js +4 -4
  4. package/dist/agents/subagent.js +1 -1
  5. package/dist/agents/team-role.js +2 -2
  6. package/dist/brain/anthropic.js +10 -9
  7. package/dist/brain/errors.d.ts +26 -0
  8. package/dist/brain/errors.js +34 -0
  9. package/dist/brain/input-too-long.d.ts +0 -31
  10. package/dist/brain/input-too-long.js +1 -18
  11. package/dist/brain/open-responses.js +10 -9
  12. package/dist/brain/openai.js +10 -9
  13. package/dist/brain/status-sink.d.ts +1 -1
  14. package/dist/brain/stream-engine.js +10 -2
  15. package/dist/brain/stream-shared.d.ts +11 -3
  16. package/dist/brain/stream-shared.js +4 -2
  17. package/dist/brain/tool-call-id.d.ts +29 -8
  18. package/dist/brain/tool-call-id.js +36 -6
  19. package/dist/core/a2a-task-state.js +2 -2
  20. package/dist/core/artifact-host.js +6 -6
  21. package/dist/core/ask-origin.js +6 -6
  22. package/dist/core/ask-unresolvable-notice.js +1 -1
  23. package/dist/core/auto-mode.js +4 -4
  24. package/dist/core/background-agent-store.js +2 -2
  25. package/dist/core/checkpoint-execution-record.js +1 -1
  26. package/dist/core/checkpoint-store.d.ts +2 -2
  27. package/dist/core/checkpoint-store.js +2 -2
  28. package/dist/core/child-clamp-seats.js +3 -3
  29. package/dist/core/compliance.js +1 -1
  30. package/dist/core/context-edit.js +1 -1
  31. package/dist/core/context-guard.d.ts +0 -19
  32. package/dist/core/context-guard.js +0 -38
  33. package/dist/core/decision-receipt.d.ts +27 -1
  34. package/dist/core/decision-receipt.js +176 -1
  35. package/dist/core/engine-notice.d.ts +59 -8
  36. package/dist/core/engine-notice.js +2 -1
  37. package/dist/core/gate-fold.js +3 -2
  38. package/dist/core/gate-outcome.js +4 -4
  39. package/dist/core/governance-codes.d.ts +221 -13
  40. package/dist/core/governance-codes.js +104 -4
  41. package/dist/core/hook-seats.js +2 -2
  42. package/dist/core/hooks.d.ts +21 -2
  43. package/dist/core/hooks.js +4 -1
  44. package/dist/core/mailbox-store.js +3 -3
  45. package/dist/core/mcp-failure.js +4 -4
  46. package/dist/core/mcp-injection-drop.js +1 -1
  47. package/dist/core/memory-engine/consolidation.js +1 -1
  48. package/dist/core/memory-engine/delegation-settlement.d.ts +8 -0
  49. package/dist/core/memory-engine/delegation-settlement.js +7 -0
  50. package/dist/core/memory-engine/engine.d.ts +12 -2
  51. package/dist/core/memory-engine/engine.js +10 -6
  52. package/dist/core/memory-engine/export-bundle.js +1 -1
  53. package/dist/core/memory-engine/layout.d.ts +6 -6
  54. package/dist/core/memory-engine/layout.js +6 -8
  55. package/dist/core/memory-engine/session-write-home.d.ts +30 -0
  56. package/dist/core/memory-engine/session-write-home.js +34 -0
  57. package/dist/core/memory-engine/tools.js +1 -1
  58. package/dist/core/memory-engine/types.js +1 -1
  59. package/dist/core/memory.js +1 -1
  60. package/dist/core/mode-policy.d.ts +10 -1
  61. package/dist/core/mode-policy.js +15 -7
  62. package/dist/core/model-seat.js +1 -1
  63. package/dist/core/permission-rule-model.js +3 -3
  64. package/dist/core/permission-rule-store.js +2 -2
  65. package/dist/core/read-only-shell-table.js +8 -8
  66. package/dist/core/retention.js +2 -2
  67. package/dist/core/runner/assemble-result.js +15 -5
  68. package/dist/core/runner/classifier-ladder.js +1 -1
  69. package/dist/core/runner/denial-limit-arms.d.ts +2 -0
  70. package/dist/core/runner/denial-limit-arms.js +6 -2
  71. package/dist/core/runner/inherited-ask-grants.d.ts +6 -5
  72. package/dist/core/runner/inherited-ask-grants.js +3 -2
  73. package/dist/core/runner/prepare-context-lane.d.ts +19 -15
  74. package/dist/core/runner/prepare-context-lane.js +11 -15
  75. package/dist/core/runner/prepare-gate-stations.js +5 -5
  76. package/dist/core/runner/prepare-inherited-gate.js +1 -1
  77. package/dist/core/runner/prepare-policy-chain.js +10 -10
  78. package/dist/core/runner/run-harness-handlers.js +3 -0
  79. package/dist/core/runner/run-identity-wiring.d.ts +1 -1
  80. package/dist/core/runner/run-identity-wiring.js +0 -2
  81. package/dist/core/runner/run-leg.js +2 -2
  82. package/dist/core/runner/run-stop-and-final-verify.js +1 -0
  83. package/dist/core/runner/run-turn-boundary.js +1 -1
  84. package/dist/core/runner/session-mode-seats.d.ts +11 -3
  85. package/dist/core/runner/session-mode-seats.js +6 -0
  86. package/dist/core/runner/session-rule-policy.js +2 -3
  87. package/dist/core/runner/stream-steer-verb.js +1 -1
  88. package/dist/core/runner/tool-call-id-fold-seat.js +16 -9
  89. package/dist/core/runner/tool-turn-repair-seat.d.ts +11 -1
  90. package/dist/core/runner/tool-turn-repair-seat.js +35 -0
  91. package/dist/core/runner/turn-attachments.js +1 -1
  92. package/dist/core/runtime-caps.js +1 -1
  93. package/dist/core/safety-axis-vocab.d.ts +5 -0
  94. package/dist/core/safety-axis-vocab.js +5 -0
  95. package/dist/core/session-reconcile.d.ts +9 -7
  96. package/dist/core/session-reconcile.js +21 -1
  97. package/dist/core/shared-memory/types.js +1 -1
  98. package/dist/core/side-query.d.ts +6 -1
  99. package/dist/core/store-contracts/read-back-shape-contract.js +1 -1
  100. package/dist/core/swappable-deps.js +1 -1
  101. package/dist/core/task-notification.js +4 -4
  102. package/dist/core/task-registry-shared.js +1 -1
  103. package/dist/core/terminal-cause.d.ts +19 -0
  104. package/dist/core/tool-face.js +6 -6
  105. package/dist/core/tool-policy.d.ts +17 -11
  106. package/dist/core/tool-policy.js +38 -72
  107. package/dist/core/tool-roster.js +1 -1
  108. package/dist/core/tool-search-setting.js +1 -1
  109. package/dist/core/trace.d.ts +5 -4
  110. package/dist/core/types.d.ts +2 -2
  111. package/dist/core/types.js +1 -1
  112. package/dist/core/wiring-manifest.js +2 -2
  113. package/dist/core/write-protect.js +1 -1
  114. package/dist/engine/harness/agent-harness.js +10 -6
  115. package/dist/engine/llm/tool-turns.d.ts +93 -4
  116. package/dist/engine/llm/tool-turns.js +63 -9
  117. package/dist/engine/llm/types.d.ts +11 -0
  118. package/dist/index.d.ts +5 -3
  119. package/dist/index.js +4 -2
  120. package/dist/internal/llm.d.ts +2 -2
  121. package/dist/internal/llm.js +1 -1
  122. package/dist/orchestration/workflow-governance.js +2 -2
  123. package/dist/orchestration/workflow-types.js +2 -2
  124. package/dist/shell/lex.js +7 -7
  125. package/dist/tools/fs/bash-program-position.js +2 -2
  126. package/dist/tools/fs/read-deny.js +2 -2
  127. package/dist/tools/fs/safety.js +1 -1
  128. package/dist/tools/fs/shell-read-judge.js +2 -2
  129. package/dist/tools/fs/shell-read-plan.js +2 -2
  130. package/dist/tools/fs/shell-read-write-targets.js +1 -1
  131. package/package.json +1 -1
  132. package/test/export-surface.snapshot.json +17 -1
  133. package/wire-changes/core@7.38.0.json +664 -0
@@ -16,9 +16,9 @@ import type { ToolEffect } from "./types.js";
16
16
  * Contract enforced here:
17
17
  * - **Never auto-replay a tool.** Reconstruction (`buildSessionContext`) already does not re-run
18
18
  * tools; this guarantees we also never synthesize a "success" that hides an unknown outcome.
19
- * - For each orphan, append a synthetic `toolResult` (isError) that tells the model the call was
20
- * interrupted and its outcome is unknown — making the transcript API-valid AND handing the
21
- * decision to the model under its verification discipline.
19
+ * - For each orphan an append can close (an id's last call site), append a synthetic `toolResult`
20
+ * (isError) that tells the model the call was interrupted and its outcome is unknown — making the
21
+ * transcript API-valid AND handing the decision to the model under its verification discipline.
22
22
  * - Use the tool's {@link ToolEffect} to tune the message: `read` carries no risk at all; `idempotent`
23
23
  * is replayable (re-issuing converges the state, but the interrupted attempt may have partially
24
24
  * landed — RB-221); `write` (and unknown tools, conservatively) must be verified/confirmed
@@ -38,9 +38,9 @@ export interface OrphanToolCall {
38
38
  * with a synthetic interrupted result).
39
39
  * - `"result"` = a **result-orphan**: a `toolResult` with no matching assistant `toolCall` — a
40
40
  * protocol-invalid context every mainstream chat API rejects. The oracle used to walk only
41
- * assistant→result, so it reported a context in exactly this state as clean while the
42
- * request-time sweeper (`context-guard`'s `dropOrphanToolResults`) was silently deleting the
43
- * result. NOT reconcile-actionable (appending another result would not repair it); consumers
41
+ * assistant→result, so it reported a context in exactly this state as clean while the request
42
+ * builder dropped the result (the request view's tool-turn law leaves it out and announces it,
43
+ * `orphan_result`). NOT reconcile-actionable (appending another result would not repair it); consumers
44
44
  * deciding on repair must filter on this field.
45
45
  * Additive: absent = call-orphan, so pre-existing consumers and persisted expectations are
46
46
  * byte-identical for the historical direction.
@@ -153,7 +153,9 @@ export declare function interruptedResultMessage(result: InterruptedResult, time
153
153
  }>;
154
154
  /**
155
155
  * Reconcile a resumed session's active branch: close any orphan tool calls with a synthetic
156
- * interrupted `toolResult` (never re-running the tool). Returns what was recovered.
156
+ * interrupted `toolResult` (never re-running the tool) — the ones an append can close
157
+ * ({@link closableByAppend}), so a second reconcile of the same branch appends nothing. Returns what
158
+ * was recovered.
157
159
  *
158
160
  * @param toolEffects optional name→effect map (from the task's tools). Unknown names are treated as
159
161
  * `write` — the conservative default, so an unrecognized interrupted call is never said to be safe.
@@ -124,9 +124,29 @@ export function interruptedResultMessage(result, timestamp) {
124
124
  timestamp,
125
125
  };
126
126
  }
127
+ function closableByAppend(messages, suspendedBatch) {
128
+ const lastSite = new Map();
129
+ const lastResult = new Map();
130
+ let order = 0;
131
+ messages.forEach((m, i) => {
132
+ if (m.role === "assistant") {
133
+ for (const part of m.content) {
134
+ if (part.type === "toolCall" && !suspendedBatch?.has(part.id) && lastSite.get(part.id)?.at !== i)
135
+ lastSite.set(part.id, { at: i, order: order++, toolName: part.name });
136
+ }
137
+ }
138
+ else if (m.role === "toolResult") {
139
+ lastResult.set(m.toolCallId, i);
140
+ }
141
+ });
142
+ return [...lastSite]
143
+ .filter(([id, site]) => (lastResult.get(id) ?? -1) < site.at)
144
+ .sort((a, b) => a[1].order - b[1].order)
145
+ .map(([toolCallId, site]) => ({ toolCallId, toolName: site.toolName }));
146
+ }
127
147
  export async function reconcileInterruptedSession(session, toolEffects, suspendedBatch, startedToolCallIds) {
128
148
  const { messages } = await session.buildContext();
129
- const orphans = findOrphanToolCalls(messages, suspendedBatch).filter((o) => o.kind !== "result");
149
+ const orphans = closableByAppend(messages, suspendedBatch);
130
150
  const recovered = [];
131
151
  for (const result of interruptedResults(orphans, toolEffects, startedToolCallIds)) {
132
152
  const entryId = await session.appendMessage(interruptedResultMessage(result, Date.now()));
@@ -1,6 +1,6 @@
1
1
  export const SHARED_MEMORY_LIST_TOOL_NAME = "memory_list";
2
2
  export const SHARED_MEMORY_READ_TOOL_NAME = "memory_read";
3
- export const SHARED_MEMORY_TOOL_NAMES = [SHARED_MEMORY_LIST_TOOL_NAME, SHARED_MEMORY_READ_TOOL_NAME];
3
+ export const SHARED_MEMORY_TOOL_NAMES = Object.freeze([SHARED_MEMORY_LIST_TOOL_NAME, SHARED_MEMORY_READ_TOOL_NAME]);
4
4
  export const SHARED_MEMORY_READ_CAP_BYTES = 102400;
5
5
  export const SHARED_MEMORY_LIST_PAGE_SIZE = 50;
6
6
  export const SHARED_MEMORY_MAX_STORES = 64;
@@ -47,7 +47,12 @@ export interface SideQuerySpec {
47
47
  modelRole?: ModelRole;
48
48
  thinking?: ThinkingLevel;
49
49
  systemPrompt?: string;
50
- /** Full multi-turn message list (user/assistant/toolResult), passed to the brain UNFLATTENED. */
50
+ /** Full multi-turn message list (user/assistant/toolResult), passed to the brain UNFLATTENED — through the same
51
+ * tool-turn law the harness applies to a session transcript (`ensureToolTurnInvariants`): an assistant's tool calls
52
+ * become its trailing blocks, each call is followed by exactly one result, and a call this list leaves unanswered
53
+ * (a trailing tool call, with the query's own prompt appended after it) is closed with a synthesized
54
+ * "interrupted, outcome unknown" result and an operator notice. A caller that does not want that close answers or
55
+ * drops the open call before handing the list over. The caller's array is not modified. */
51
56
  messages: SideQueryMessage[];
52
57
  /** Tool definitions the model may call in its reply. The engine NEVER executes them — any calls
53
58
  * come back in `SideQueryResult.toolCalls` for the caller to interpret. */
@@ -1,7 +1,7 @@
1
1
  import assert from "node:assert";
2
2
  import { thrownText } from "../thrown-value.js";
3
3
  import { beginContract } from "./contract-harness.js";
4
- export const CORRUPT_BODY_FORMS = ["not-an-object", "missing-required", "wrong-type", "torn-json"];
4
+ export const CORRUPT_BODY_FORMS = Object.freeze(["not-an-object", "missing-required", "wrong-type", "torn-json"]);
5
5
  async function observe(subject) {
6
6
  try {
7
7
  const value = await subject.read();
@@ -1,6 +1,6 @@
1
1
  import { assertContainmentFaceValue } from "../tools/fs/containment-face.js";
2
2
  import { assertContextSettingsValue } from "./context-settings.js";
3
- export const SWAPPABLE_DEP_SEATS = ["models", "tiers", "readFace", "writeFace", "contextSettings"];
3
+ export const SWAPPABLE_DEP_SEATS = Object.freeze(["models", "tiers", "readFace", "writeFace", "contextSettings"]);
4
4
  const SWAPPABLE_DEP_SEAT_SET = new Set(SWAPPABLE_DEP_SEATS);
5
5
  export function isSwappableDepSeat(k) {
6
6
  return SWAPPABLE_DEP_SEAT_SET.has(k);
@@ -2,7 +2,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { escapeEnvelopeTag, inlineUntrusted, sanitizeUntrustedText } from "./untrusted-text.js";
3
3
  import { PEER_MESSAGE_NOTICE } from "../agents/peer-admission.js";
4
4
  import { buildCrossSessionEnvelope, CROSS_SESSION_MESSAGE_NOTICE, neutralizePeerBody } from "../agents/cross-session-envelope.js";
5
- export const NOTIFICATION_INJECTION_OUTCOMES = ["queued", "parked", "parked_durably", "dropped_duplicate"];
5
+ export const NOTIFICATION_INJECTION_OUTCOMES = Object.freeze(["queued", "parked", "parked_durably", "dropped_duplicate"]);
6
6
  export function isNotificationInjectionOutcome(value) {
7
7
  return typeof value === "string" && NOTIFICATION_INJECTION_OUTCOMES.includes(value);
8
8
  }
@@ -143,13 +143,13 @@ export const MAX_PENDING_EVENTS_PER_TASK = 50;
143
143
  export const MAX_PENDING_EVENTS_PER_SESSION = 200;
144
144
  export const MAX_PENDING_SESSIONS = 100;
145
145
  export const MAX_SESSION_BOX_BINDINGS = 4096;
146
- export const HANDBACK_STATES = ["send", "flagged", "withheld"];
146
+ export const HANDBACK_STATES = Object.freeze(["send", "flagged", "withheld"]);
147
147
  export function handbackStateOf(facts) {
148
148
  if (facts.warning !== undefined)
149
149
  return "flagged";
150
150
  return facts.result || facts.partialText ? "send" : "withheld";
151
151
  }
152
- export const COMPACTION_KINDS = ["manual", "auto", "reactive"];
152
+ export const COMPACTION_KINDS = Object.freeze(["manual", "auto", "reactive"]);
153
153
  export function compactionKindOf(trigger) {
154
154
  return trigger === "forced" ? "reactive" : trigger;
155
155
  }
@@ -161,7 +161,7 @@ export function terminalFrameFacets(facts) {
161
161
  };
162
162
  }
163
163
  export const ENGINE_FRAME_CODEC_VERSION = 1;
164
- export const ENGINE_FRAME_TASK_TYPES = ["background_agent", "monitor", "workflow"];
164
+ export const ENGINE_FRAME_TASK_TYPES = Object.freeze(["background_agent", "monitor", "workflow"]);
165
165
  const SHELL_LESS_CARRIERS = ["agentMessage", "crossSessionMessage", "crossSessionNotice", "inboxNotice", "_sema_provenance"];
166
166
  export function isEngineFrameParkable(payload) {
167
167
  return ENGINE_FRAME_TASK_TYPES.includes(payload.task_type) && SHELL_LESS_CARRIERS.every((k) => payload[k] === undefined);
@@ -101,7 +101,7 @@ export function spoolDropNote(acct) {
101
101
  return "[!] an unknown amount of earlier output was permanently dropped before this window (buffer evicted)\n";
102
102
  return "";
103
103
  }
104
- export const TASK_OUTPUT_REPLAY_KINDS = ["full", "cursor"];
104
+ export const TASK_OUTPUT_REPLAY_KINDS = Object.freeze(["full", "cursor"]);
105
105
  export const REPLAY_FACE = {
106
106
  full: { stdout: "stdout (full, re-readable)", stdoutWhenGapped: "stdout (re-readable spool — INCOMPLETE, see note above)", stderr: "stderr" },
107
107
  cursor: { stdout: "new stdout", stdoutWhenGapped: undefined, stderr: "new stderr" },
@@ -66,6 +66,17 @@ export type TerminalCause = {
66
66
  code?: string;
67
67
  /** Human-readable failure text. */
68
68
  message?: string;
69
+ /**
70
+ * A machine-readable account of the failure beside `code`, for the one shape a consumer offers its own way out
71
+ * of: `provider_rejected` — the provider REFUSED the request as sent (a 4xx answer) and that refusal named this
72
+ * result. `status` is the refusal's HTTP status (`TaskResult.apiFailure.status`, the same value);
73
+ * `code` the provider's own word for it from its error envelope (`AssistantMessage.apiErrorCode`), when it
74
+ * stated one. PRESENCE: only on that terminal — a run whose higher-ranked cause named the result, a 5xx, a
75
+ * connect / stream failure, an in-band error inside a 200 and every non-provider failure leave it absent.
76
+ * A 401 / 403 (credentials) and a 429 (rate limit) are refusals too: branch on `status` (or on `code` above)
77
+ * to tell a request a consumer may change and resend from one it may not.
78
+ */
79
+ detail?: TerminalFailureDetail;
69
80
  /**
70
81
  * The durable pause this failure STANDS IN FOR — set only by the nested hard boundary
71
82
  * (`mapNestedSuspend`): an orchestrator's nested leg paused on a durable gate, the orchestrator cannot
@@ -117,6 +128,14 @@ export type TerminalCause = {
117
128
  */
118
129
  restoreMode?: "snapshot" | "park_only";
119
130
  };
131
+ /** The machine-readable account a `failed` cause may carry ({@link TerminalCause}). `kind` is closed; one member. */
132
+ export interface TerminalFailureDetail {
133
+ kind: "provider_rejected";
134
+ /** The refusal's HTTP status, 400–499. */
135
+ status: number;
136
+ /** The provider's own word for the refusal (its error envelope's `error.code`, else `error.type`), when stated. */
137
+ code?: string;
138
+ }
120
139
  /** The paused arm alone — the shape the run's committed-pause holder carries. */
121
140
  /**
122
141
  * The one disposition table over `TerminalCause["kind"]`: which causes a resume journal may REPLAY
@@ -1,9 +1,9 @@
1
- export const TOOL_FAMILIES = ["shell", "file-read", "file-write", "search", "web", "delegate", "plan", "scaffold", "memory", "protocol", "other"];
2
- export const TOOL_MOUNT_TAGS = ["hands", "backgroundTasks", "workflow", "delegation", "hostInternals", "peerLane", "scheduler", "memory", "scenario", "always", "optional"];
3
- export const TOOL_APPROVAL_CARDS = ["file-edit", "file-write", "notebook-edit", "shell", "fetch", "plan", "question", "generic"];
4
- export const TOOL_PATH_ACCESSES = ["read", "create", "edit"];
5
- export const TOOL_PATH_BASES = ["cwd", "root"];
6
- export const TOOL_PATH_ABSENCES = ["none", "base"];
1
+ export const TOOL_FAMILIES = Object.freeze(["shell", "file-read", "file-write", "search", "web", "delegate", "plan", "scaffold", "memory", "protocol", "other"]);
2
+ export const TOOL_MOUNT_TAGS = Object.freeze(["hands", "backgroundTasks", "workflow", "delegation", "hostInternals", "peerLane", "scheduler", "memory", "scenario", "always", "optional"]);
3
+ export const TOOL_APPROVAL_CARDS = Object.freeze(["file-edit", "file-write", "notebook-edit", "shell", "fetch", "plan", "question", "generic"]);
4
+ export const TOOL_PATH_ACCESSES = Object.freeze(["read", "create", "edit"]);
5
+ export const TOOL_PATH_BASES = Object.freeze(["cwd", "root"]);
6
+ export const TOOL_PATH_ABSENCES = Object.freeze(["none", "base"]);
7
7
  export function pathTargetSlotOnly(pt) {
8
8
  return {
9
9
  param: pt.param,
@@ -1,6 +1,8 @@
1
1
  import { type AskOrigin, type AssertAllKeysHandled } from "./ask-origin.js";
2
2
  import { type ReadRootGrantScreen } from "./read-root-grant-screen.js";
3
3
  import { type Settlement } from "./gate-outcome.js";
4
+ import { type DecisionReason } from "./decision-receipt.js";
5
+ export { isEngineRefusal } from "./decision-receipt.js";
4
6
  /** A tool call presented to a policy before it executes. */
5
7
  /** design/388 B6 — the per-call face (see {@link ToolCallRequest.face}). */
6
8
  export interface ToolCallFace {
@@ -153,8 +155,9 @@ export declare function advertisedWritableRootsSeat(seat: {
153
155
  } | undefined): {
154
156
  advertisedWritableRoots?: readonly string[];
155
157
  };
156
- declare const DECISION_REASONS: readonly ["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable", "read_only"];
157
- export type DecisionReason = (typeof DECISION_REASONS)[number];
158
+ /** Declared beside the receipt's list (decision-receipt.ts) — a new member added to either side is a compile error rather
159
+ * than a silent drift between the type and the screen; re-exported here, the module every consumer of the decision reads. */
160
+ export type { DecisionReason };
158
161
  /** Carry every engine attestation from a decision onto a reconstruction of it — the
159
162
  * seam for the engine's OWN clone sites outside this module (the inherited chain's final-form re-check carries the
160
163
  * post-rewrite args on a spread of an ancestor's deny). Not re-exported from `src/index.ts`. */
@@ -621,8 +624,10 @@ export interface ToolPolicyProjection {
621
624
  * self-checks with those.
622
625
  */
623
626
  export declare function namespacedCoveringEntries(entries: readonly string[] | undefined): readonly string[];
624
- /** Does any covering entry reach `toolName`? See {@link namespacedCoveringEntries}. */
625
- export declare function namespacedCoveringHit(covering: readonly string[], toolName: string): boolean;
627
+ /** Does any covering entry reach the call — under ANY name it answers to, the same reading the exact lists take through
628
+ * `nameListHolds` (a covering entry and an exact entry in one list are one rule: it reaches the call iff its name is on the
629
+ * list)? See {@link namespacedCoveringEntries}. */
630
+ export declare function namespacedCoveringHit(covering: readonly string[], call: Pick<ToolCallRequest, "toolName" | "face">): boolean;
626
631
  /**
627
632
  * Execute a persisted {@link ToolPolicyProjection} against a call (F-012 L1): returns the first
628
633
  * component's deny, or `undefined` when the projection has no opinion (it is deny-only by
@@ -754,7 +759,7 @@ export declare const DECISION_MEMBERS: readonly DecisionMember[];
754
759
  * mode arbitration, the approval request, the rewrite that executes) reads the copy; a copy handed to the screen again
755
760
  * comes back as it is, unread. In order: (1) the CARRIER's type — a decision that is not an object (a string, a function
756
761
  * holding the members) has no `action`; (2) the ONE READ — ONE function ({@link receiveContract}) for the decision and for the
757
- * records it nests ({@link NESTED_CONTRACTS}), on an ORDINARY object (a Proxy, as the carrier or anywhere on its prototype
762
+ * records it nests (`receiveNestedRecords`, decision-receipt.ts), on an ORDINARY object (a Proxy, as the carrier or anywhere on its prototype
758
763
  * chain, is refused before a trap of its could run): each contract member absent or an own enumerable DATA property, its value
759
764
  * taken off the carrier's own descriptor (a member outside the contract is neither read nor carried — a policy's self-declared
760
765
  * record on another name finds no reader); any other spelling refused by name — an accessor (never invoked), a member the
@@ -762,7 +767,7 @@ export declare const DECISION_MEMBERS: readonly DecisionMember[];
762
767
  * descriptor, never read; (3) the retired `reason` field as an OWN property, judged without a read (authorship of the old
763
768
  * spelling: a prototype exposing it never wrote it; ignoring it silently dropped the policy's text); (4) the VALUES, off the engine's
764
769
  * one-read record, never a second read of the caller's object — the retired window absent, `action` in
765
- * {@link DECISION_ACTIONS} (a consumer ranks a decision by testing two of its words, so any other value used to rank as an
770
+ * its closed set (`screenContractValues`, decision-receipt.ts — every closed-set member is held to its set there; a consumer ranks a decision by testing two of its words, so any other value used to rank as an
766
771
  * allow; a `canUseTool` result outside its set is a failure in CC) and `decisionReason` in {@link DECISION_REASONS} when
767
772
  * present (absence stays legal); (5) the DETACH —
768
773
  * the contract members' values structured-cloned ({@link tryCloneArgs}; one that cannot be, a function inside a rewrite,
@@ -778,10 +783,12 @@ export declare const DECISION_MEMBERS: readonly DecisionMember[];
778
783
  * there is what is cloned and what runs — one read, consistent) and by a refusal's rendering of a foreign word (`String(v)` on an
779
784
  * out-of-set `action`, after the deny is decided). Same-process JavaScript has no trust boundary a function can enforce.
780
785
  *
781
- * `reasonIsNonInput` (the HOOK seam): a hook's `decisionReason` is DISCARDED there — every mint point downstream re-stamps
782
- * `"hook"`, and the seam maps a hook's deny and ask and reads anything else as no objection, the policy still adjudicating
783
- * the call — so that seam takes the two retired-field arms alone and its object back unchanged (`undefined` = no
784
- * opinion): refusing a value that cannot travel would fail-close a call over a field with no consumer.
786
+ * `reasonIsNonInput` (the HOOK seam): the same receipt, with the two differences the seam's consumers imply — a hook's
787
+ * `decisionReason` is DISCARDED there (every mint point downstream re-stamps `"hook"`) and so is not held to its set, and a
788
+ * hook result may leave `action` out (no opinion; the policy still adjudicates). `undefined`, `null` and `false` are no
789
+ * opinion (what a hook body yields when it has nothing to say). A refusal the receipt mints at this seam is the ENGINE's
790
+ * decision ({@link isEngineRefusal}) and exits through the engine's fail-closed arm (the deny observer), never as the hook's
791
+ * own deny.
785
792
  */
786
793
  export declare function refuseOutOfContractDecision(d: PermissionResult, opts?: {
787
794
  reasonIsNonInput?: boolean;
@@ -1780,4 +1787,3 @@ onLateSettlement?: (late: {
1780
1787
  kind: "error";
1781
1788
  error: unknown;
1782
1789
  }) => void): Promise<ResolvedAsk>;
1783
- export {};
@@ -13,7 +13,8 @@ import { boundInputHashOf } from "./canonical-json.js";
13
13
  import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY } from "./untrusted-text.js";
14
14
  import { describeRejectedValue } from "./tool-search-setting.js";
15
15
  import { thrownText } from "./thrown-value.js";
16
- import { receiveContract, refusedOutOfContract } from "./decision-receipt.js";
16
+ import { nestedRecordsOf, receiveContract, receiveNestedRecords, refusedOutOfContract, screenContractValues } from "./decision-receipt.js";
17
+ export { isEngineRefusal } from "./decision-receipt.js";
17
18
  import { isNamespacedCoveringRuleName, namespacedRuleNameCovers, parsePermissionRule } from "./permission-rules.js";
18
19
  import { protocolOf } from "./protocol-table.js";
19
20
  import { isAbsoluteForFamily, isAbsolutePathForm, joinForFamily, normalizeAbsPathLexically, pathFamilyOf, writeTargetPath } from "../tools/fs/safety.js";
@@ -23,8 +24,6 @@ const shellToolsListHolds = (set, req) => heldBy("policy.shellToolsList", set, r
23
24
  export function advertisedWritableRootsSeat(seat) {
24
25
  return seat === undefined ? {} : { advertisedWritableRoots: Object.freeze([...seat.roots]) };
25
26
  }
26
- const DECISION_REASONS = ["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable", "read_only"];
27
- const DECISION_REASON_SET = new Set(DECISION_REASONS);
28
27
  function settledByNobody(kind) {
29
28
  return { kind, who: { party: "none" }, when: Date.now() };
30
29
  }
@@ -164,20 +163,20 @@ export function toolPolicyWriteTargetGuard(policies) {
164
163
  export function namespacedCoveringEntries(entries) {
165
164
  return (entries ?? []).filter(isNamespacedCoveringRuleName);
166
165
  }
167
- export function namespacedCoveringHit(covering, toolName) {
168
- return covering.length > 0 && covering.some((e) => namespacedRuleNameCovers(e, toolName));
166
+ export function namespacedCoveringHit(covering, call) {
167
+ return covering.length > 0 && nameListHolds({ has: (name) => covering.some((e) => namespacedRuleNameCovers(e, name)) }, call);
169
168
  }
170
169
  export function checkToolPolicyProjection(projection, req) {
171
170
  const listed = (list) => heldBy("policy.projectionReplay", { has: (n) => list.includes(n) }, req);
172
171
  for (const c of projection.components) {
173
172
  if (c.kind === "tool_deny") {
174
- if (listed(c.names) || namespacedCoveringHit(namespacedCoveringEntries(c.names), req.toolName)) {
173
+ if (listed(c.names) || namespacedCoveringHit(namespacedCoveringEntries(c.names), req)) {
175
174
  return { action: "deny", message: `tool "${req.toolName}" is denied by a frozen inherited policy projection` };
176
175
  }
177
176
  continue;
178
177
  }
179
178
  if (c.kind === "tool_allowlist") {
180
- if (!listed(c.names) && !namespacedCoveringHit(namespacedCoveringEntries(c.names), req.toolName)) {
179
+ if (!listed(c.names) && !namespacedCoveringHit(namespacedCoveringEntries(c.names), req)) {
181
180
  return { action: "deny", message: `tool "${req.toolName}" is not in a frozen inherited policy projection's allowlist` };
182
181
  }
183
182
  continue;
@@ -247,87 +246,56 @@ export function constraintChainDigest(chain) {
247
246
  }
248
247
  const ALLOW = Object.freeze({ action: "allow" });
249
248
  const RETIRED_TEXT_FIELD = "reason";
250
- const RETIRED_WINDOW_MEMBER = "autoDenyAfterMs";
251
- const RETIRED_WINDOW_MEMBER_DENY_MESSAGE = `a permission decision's "denialLimitFallback" carries "${RETIRED_WINDOW_MEMBER}" — the decision names the counts only; ` +
252
- `the auto-deny window is decided by the ask's route (a live approver arms it, every other route runs none) and cannot be ` +
253
- `declared by a policy; denied fail-closed rather than silently replacing the declared number`;
254
- const carriesRetiredWindow = (fallback) => typeof fallback === "object" && fallback !== null && fallback[RETIRED_WINDOW_MEMBER] !== undefined;
255
- const DECISION_ACTIONS = Object.keys({ allow: 0, deny: 0, ask: 0 });
256
- const DECISION_ACTION_SET = new Set(DECISION_ACTIONS);
257
249
  export const DECISION_MEMBERS = Object.keys({
258
250
  action: 0, updatedInput: 0, message: 0, decisionReason: 0, requiresRealApproval: 0, denialLimitFallback: 0, persistedRuleShadowed: 0, probeReason: 0,
259
251
  probeCause: 0, probeMandated: 0, mandated: 0, mandate: 0, probeReadRootCandidate: 0, matchedAskRule: 0, ruleEvidence: 0, segmentCoverage: 0,
260
252
  origin: 0, askClass: 0, classes: 0, guard: 0, ruleUnreadable: 0, authoredBy: 0, foldedAskClasses: 0,
261
253
  });
262
- const OPERAND_MEMBERS = Object.keys({ shown: 0, total: 0 });
263
- const NESTED_CONTRACTS = [
264
- ["", "denialLimitFallback", Object.keys({ consecutive: 0, total: 0, limit: 0, autoDenyAfterMs: 0 })],
265
- ["", "probeCause", Object.keys({ code: 0, roots: 0, further: 0 })],
266
- ["probeCause.", "roots", OPERAND_MEMBERS],
267
- ["probeCause.", "further", OPERAND_MEMBERS],
268
- ];
269
- const refusedAction = (what) => refusedOutOfContract(`a permission decision's "action" is ${what} — the closed set is ${DECISION_ACTIONS.join("/")}; denied fail-closed rather than reading a word outside it as an allow`);
270
- function renderForeignValue(v) {
271
- try {
272
- return JSON.stringify(String(v).slice(0, 64));
273
- }
274
- catch {
275
- return `[unprintable ${typeof v}]`;
276
- }
277
- }
254
+ const HOOK_RESULT_MEMBERS = [...DECISION_MEMBERS, "additionalContext"];
255
+ const HOOK_SEAM_UNREAD = new Set(["decisionReason"]);
256
+ const HOOK_SEAM_OPTIONAL = new Set(["action"]);
257
+ const HOOK_RECEIPTS = new WeakSet();
278
258
  const RETIRED_TEXT_FIELD_DENY_MESSAGE = `a permission decision carries the retired "${RETIRED_TEXT_FIELD}" field — rename it to "message" (the one text field ` +
279
259
  `a decision carries); denied fail-closed rather than executing a decision whose text this layer cannot read`;
280
260
  const ENGINE_RECEIPTS = new WeakSet();
281
261
  export function refuseOutOfContractDecision(d, opts) {
282
- if (opts?.reasonIsNonInput === true) {
283
- if (d === null || d === undefined)
284
- return d;
285
- if (Object.prototype.hasOwnProperty.call(d, RETIRED_TEXT_FIELD))
286
- return refusedOutOfContract(RETIRED_TEXT_FIELD_DENY_MESSAGE);
287
- return carriesRetiredWindow(d.denialLimitFallback) ? refusedOutOfContract(RETIRED_WINDOW_MEMBER_DENY_MESSAGE) : d;
262
+ const hookSeam = opts?.reasonIsNonInput === true;
263
+ if (hookSeam && (d === null || d === undefined || Object.is(d, false)))
264
+ return d;
265
+ if (typeof d !== "object" || d === null) {
266
+ return refusedOutOfContract(`a permission decision's "action" is absent (the decision is ${d === null || d === undefined ? String(d) : `a ${typeof d}`}, not an object) — the closed set is allow/deny/ask; denied fail-closed rather than reading a word outside it as an allow`);
288
267
  }
289
- if (typeof d !== "object" || d === null)
290
- return refusedAction(`absent (the decision is ${d === null || d === undefined ? String(d) : `a ${typeof d}`}, not an object)`);
291
- if (ENGINE_RECEIPTS.has(d))
268
+ if ((hookSeam ? HOOK_RECEIPTS : ENGINE_RECEIPTS).has(d))
292
269
  return d;
293
- const decision = receiveContract(d, DECISION_MEMBERS, "");
270
+ const decision = receiveContract(d, hookSeam ? HOOK_RESULT_MEMBERS : DECISION_MEMBERS, "");
294
271
  if (decision.kind === "refused")
295
272
  return decision.refused;
296
273
  if (Object.prototype.hasOwnProperty.call(d, RETIRED_TEXT_FIELD))
297
274
  return refusedOutOfContract(RETIRED_TEXT_FIELD_DENY_MESSAGE);
298
275
  const contract = decision.held;
299
- const records = Object.assign(Object.create(null), { "": contract });
300
- for (const [holder, member, members] of NESTED_CONTRACTS) {
301
- const held = records[holder];
302
- if (held === undefined || typeof held[member] !== "object" || held[member] === null)
303
- continue;
304
- const nested = receiveContract(held[member], members, `${holder}${member}.`);
305
- if (nested.kind === "refused")
306
- return nested.refused;
307
- held[member] = records[`${holder}${member}.`] = nested.held;
308
- }
309
- if (carriesRetiredWindow(contract.denialLimitFallback))
310
- return refusedOutOfContract(RETIRED_WINDOW_MEMBER_DENY_MESSAGE);
311
- if (!DECISION_ACTION_SET.has(contract.action))
312
- return refusedAction(contract.action === undefined ? "absent" : renderForeignValue(contract.action));
313
- if (contract.decisionReason !== undefined && !DECISION_REASON_SET.has(contract.decisionReason)) {
314
- return refusedOutOfContract(`a permission decision carries an unrecognized "decisionReason" value (${renderForeignValue(contract.decisionReason)}) — ` +
315
- `the closed set is ${DECISION_REASONS.join("/")}; denied fail-closed rather than letting consumers branch on a value this layer cannot classify`);
276
+ const refused = receiveNestedRecords(contract) ?? screenContractValues(contract, hookSeam ? { skip: HOOK_SEAM_UNREAD, optional: HOOK_SEAM_OPTIONAL } : undefined);
277
+ if (refused !== undefined)
278
+ return refused;
279
+ if (hookSeam && contract.additionalContext !== undefined && typeof contract.additionalContext !== "string") {
280
+ return refusedOutOfContract(`a PreToolUse hook result's "additionalContext" is not a string — the member is the text appended for the model; denied fail-closed rather than rendering a value this layer cannot classify`);
316
281
  }
317
282
  const detached = tryCloneArgs(contract);
318
283
  if (!detached.ok) {
319
- return refusedOutOfContract(`a permission decision's members could not be detached from the policy that returned them (${detached.reason}) — ` +
284
+ return refusedOutOfContract(`a permission decision's members could not be detached from the layer that returned them (${detached.reason}) — ` +
320
285
  `the engine keeps its own copy of what it read; denied fail-closed`);
321
286
  }
322
287
  const receipt = Object.freeze(detached.value);
323
- const ownRecord = (o, k) => (typeof o === "object" && o !== null && Object.hasOwn(o, k) ? o[k] : undefined);
324
- for (const [holder, member] of NESTED_CONTRACTS) {
325
- const record = ownRecord(holder.split(".").filter(Boolean).reduce(ownRecord, receipt), member);
326
- if (typeof record === "object" && record !== null)
327
- Object.freeze(record);
288
+ for (const k of ["classes", "foldedAskClasses"])
289
+ if (Object.hasOwn(receipt, k) && Array.isArray(receipt[k]))
290
+ Object.freeze(receipt[k]);
291
+ for (const record of nestedRecordsOf(receipt)) {
292
+ for (const v of Object.values(record))
293
+ if (Array.isArray(v))
294
+ Object.freeze(v);
295
+ Object.freeze(record);
328
296
  }
329
297
  carryEngineAttestations(d, receipt);
330
- ENGINE_RECEIPTS.add(receipt);
298
+ (hookSeam ? HOOK_RECEIPTS : ENGINE_RECEIPTS).add(receipt);
331
299
  return receipt;
332
300
  }
333
301
  const DEADLINE_ELAPSED = Symbol("approval.deadline_elapsed");
@@ -424,11 +392,10 @@ export function createAllowDenyPolicy(opts) {
424
392
  },
425
393
  nameSets: [{ ...(opts.allow ? { allow: [...opts.allow] } : {}), ...(opts.deny ? { deny: [...opts.deny] } : {}) }],
426
394
  check(req) {
427
- const toolName = req.toolName;
428
- if (nameListHolds(deny, req) || namespacedCoveringHit(denyCovering, toolName)) {
395
+ if (nameListHolds(deny, req) || namespacedCoveringHit(denyCovering, req)) {
429
396
  return { action: "deny", message: `tool "${req.toolName}" is denied by policy` };
430
397
  }
431
- if (allow && !nameListHolds(allow, req) && !namespacedCoveringHit(allowCovering, toolName)) {
398
+ if (allow && !nameListHolds(allow, req) && !namespacedCoveringHit(allowCovering, req)) {
432
399
  return { action: "deny", message: `tool "${req.toolName}" is not in the allowlist` };
433
400
  }
434
401
  return ALLOW;
@@ -464,11 +431,10 @@ export function createApprovalPolicy(opts) {
464
431
  },
465
432
  ],
466
433
  async check(req, signal) {
467
- const toolName = req.toolName;
468
- if (nameListHolds(deny, req) || namespacedCoveringHit(denyCovering, toolName)) {
434
+ if (nameListHolds(deny, req) || namespacedCoveringHit(denyCovering, req)) {
469
435
  return { action: "deny", message: `tool "${req.toolName}" is denied by policy` };
470
436
  }
471
- if (nameListHolds(need, req) || namespacedCoveringHit(needCovering, toolName)) {
437
+ if (nameListHolds(need, req) || namespacedCoveringHit(needCovering, req)) {
472
438
  const attest = (d, settlement) => attestEngineSettlement(d, { settlement, origin: "policy" }, req);
473
439
  if (signal?.aborted) {
474
440
  return attest({ action: "deny", message: `approval aborted for "${req.toolName}" (run or turn interrupted)` }, settledByNobody("task_aborted"));
@@ -518,7 +484,7 @@ export function createApprovalPolicy(opts) {
518
484
  }
519
485
  return attest({ action: "deny", message: `approval denied for "${req.toolName}"` }, settledByPerson("human_refused", undefined));
520
486
  }
521
- if (opts.denyByDefault && !nameListHolds(auto, req) && !namespacedCoveringHit(autoCovering, toolName)) {
487
+ if (opts.denyByDefault && !nameListHolds(auto, req) && !namespacedCoveringHit(autoCovering, req)) {
522
488
  return { action: "deny", message: `tool "${req.toolName}" requires explicit allow` };
523
489
  }
524
490
  return ALLOW;
@@ -1051,7 +1017,7 @@ export function askApproverIdentity(onAsk) {
1051
1017
  export function isLiveApproverSeat(onAsk) {
1052
1018
  return typeof onAsk === "function";
1053
1019
  }
1054
- export const DURABLE_MANDATE_SOURCES = ["force_durable_gate", "durable_approval"];
1020
+ export const DURABLE_MANDATE_SOURCES = Object.freeze(["force_durable_gate", "durable_approval"]);
1055
1021
  Object.freeze(DURABLE_MANDATE_SOURCES);
1056
1022
  export function durableMandateSourceOf(args) {
1057
1023
  if (args.forceDurableGate)
@@ -11,7 +11,7 @@ const Name = Type.String({ minLength: 1, maxLength: TOOL_WIRE_NAME_MAX_CHARS });
11
11
  const Short = Type.String({ maxLength: TOOL_CONTRACT_MAX_CHARS });
12
12
  const Key = Type.String({ maxLength: TOOL_KEY_MAX_CHARS });
13
13
  const Digest16 = Type.String({ minLength: 16, maxLength: 16 });
14
- export const ROSTER_SOURCES = ["builtin", "caller", "host", "mcp", "a2a", "external", "synthetic"];
14
+ export const ROSTER_SOURCES = Object.freeze(["builtin", "caller", "host", "mcp", "a2a", "external", "synthetic"]);
15
15
  export const ToolRosterRenderHints = Type.Object({
16
16
  userFacingName: Type.Optional(Type.String({ maxLength: RENDER_HINT_MAX_CHARS })),
17
17
  activity: Type.Optional(Type.String({ maxLength: RENDER_HINT_ACTIVITY_MAX_CHARS })),
@@ -1,6 +1,6 @@
1
1
  export const TOOL_SEARCH_DEFAULT_THRESHOLD_PERCENT = 10;
2
2
  export const TOOL_SEARCH_INVALID_CODE = "config.tool_search_invalid";
3
- export const TOOL_SEARCH_POLICIES = ["always", "never", "auto"];
3
+ export const TOOL_SEARCH_POLICIES = Object.freeze(["always", "never", "auto"]);
4
4
  export function resolveToolSearch(setting) {
5
5
  if (setting === undefined)
6
6
  return { policy: "auto", thresholdPercent: TOOL_SEARCH_DEFAULT_THRESHOLD_PERCENT, fraction: TOOL_SEARCH_DEFAULT_THRESHOLD_PERCENT / 100 };
@@ -860,13 +860,14 @@ export type TraceEvent = {
860
860
  } | {
861
861
  /** A provider echo field was refused by {@link import("../brain/errors.js").PROVIDER_ECHO_SHAPE} (visible ASCII,
862
862
  * 1–128 characters) and dropped: the response's `request-id` / `x-request-id` header values (`field:
863
- * "requestId"`, read at connect — the id a failure would carry as `apiFailure.requestId`), or the model name
864
- * its frames report (`field: "responseModel"`). One frame per provider response per field; the value itself
865
- * is never carried (metadata-only). A count of these frames is this engine's drop count. */
863
+ * "requestId"`, read at connect — the id a failure would carry as `apiFailure.requestId`), the model name
864
+ * its frames report (`field: "responseModel"`), or the word a refusal's error envelope states (`field:
865
+ * "apiErrorCode"`). One frame per provider response per field; the value itself is never carried
866
+ * (metadata-only). A count of these frames is this engine's drop count. */
866
867
  kind: "brain.echo_dropped";
867
868
  version: 1;
868
869
  taskId: string;
869
- field: "requestId" | "responseModel";
870
+ field: "requestId" | "responseModel" | "apiErrorCode";
870
871
  ts: number;
871
872
  } | {
872
873
  /** C10 — a `preemptSignal` fired on a task that is NOT resource-suspend-eligible: the preempt is a
@@ -29,6 +29,6 @@ export { AUTO_MODE_DENY_SOURCES, isAutoModeDenySource } from "./runtime-caps.js"
29
29
  export type { BackgroundChildEvent, DelegationLifecycleEvent } from "./delegation-frames.js";
30
30
  export { __resetMalformedDelegationSeatAnnouncement, deliverDelegationLifecycle } from "./delegation-frames.js";
31
31
  export type { ProjectMemoryLoad, ResumePreflightInfo, ResumePreflightVerdict, RunnerDeps } from "./runner-deps.js";
32
- export type { EngineNotice, OwedUserInputAccount } from "./engine-notice.js";
33
- export { __resetMalformedNoticeSeatAnnouncement, deliverEngineNotice, owedUserInputCounts, settleOwedUserInput, undrainedUserInputNotices } from "./engine-notice.js";
32
+ export type { EngineNotice, OwedUserInputAccount, UserSteerUndrainedCause } from "./engine-notice.js";
33
+ export { __resetMalformedNoticeSeatAnnouncement, deliverEngineNotice, owedUserInputCounts, settleOwedUserInput, undrainedUserInputNotices, USER_STEER_UNDRAINED_CAUSES } from "./engine-notice.js";
34
34
  export type { AgentTool, Model, StreamFn, ThinkingLevel, ToolInputValidationContext, ToolInputVerdict };
@@ -1,4 +1,4 @@
1
1
  export { MODEL_ROLES } from "./model-seat.js";
2
2
  export { AUTO_MODE_DENY_SOURCES, isAutoModeDenySource } from "./runtime-caps.js";
3
3
  export { __resetMalformedDelegationSeatAnnouncement, deliverDelegationLifecycle } from "./delegation-frames.js";
4
- export { __resetMalformedNoticeSeatAnnouncement, deliverEngineNotice, owedUserInputCounts, settleOwedUserInput, undrainedUserInputNotices } from "./engine-notice.js";
4
+ export { __resetMalformedNoticeSeatAnnouncement, deliverEngineNotice, owedUserInputCounts, settleOwedUserInput, undrainedUserInputNotices, USER_STEER_UNDRAINED_CAUSES } from "./engine-notice.js";
@@ -5,8 +5,8 @@ import { canonicalize } from "./canonical-json.js";
5
5
  import { resolveCheckpointStore } from "./checkpoint-store.js";
6
6
  import { isLiveQuestionFace } from "./ask-question.js";
7
7
  import { AUTO_MODE_DENY_SOURCES, isAutoModeDenySource } from "./runtime-caps.js";
8
- export const AUTO_MODE_ARM_REASONS = ["armed", "no_intent", "no_face", "denied", "resolver_fault"];
9
- export const LSP_LANES = ["tool", "diagnostics", "tool_and_diagnostics"];
8
+ export const AUTO_MODE_ARM_REASONS = Object.freeze(["armed", "no_intent", "no_face", "denied", "resolver_fault"]);
9
+ export const LSP_LANES = Object.freeze(["tool", "diagnostics", "tool_and_diagnostics"]);
10
10
  export function deriveHandsSeam(handsMounted) {
11
11
  return handsMounted ? { mounted: true } : { mounted: false, reason: "no_execution_env" };
12
12
  }
@@ -183,7 +183,7 @@ function relativeUnderDataRoot(path, roots) {
183
183
  }
184
184
  return null;
185
185
  }
186
- export const WRITE_PROTECTION_TARGET_VIEWS = ["spelling-only", "target"];
186
+ export const WRITE_PROTECTION_TARGET_VIEWS = Object.freeze(["spelling-only", "target"]);
187
187
  export function compileWriteProtection(entries, opts) {
188
188
  const rows = resolveWriteProtectedTable(entries);
189
189
  if (rows.length === 0)