@sema-agent/core 7.37.1 → 7.38.1

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 (155) hide show
  1. package/CHANGELOG.md +168 -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/brain/tool-call-repair.js +1 -1
  20. package/dist/core/a2a-task-state.js +2 -2
  21. package/dist/core/artifact-host.js +6 -6
  22. package/dist/core/ask-origin.js +6 -6
  23. package/dist/core/ask-unresolvable-notice.js +1 -1
  24. package/dist/core/auto-mode.js +4 -4
  25. package/dist/core/background-agent-store.js +2 -2
  26. package/dist/core/checkpoint-execution-record.js +1 -1
  27. package/dist/core/checkpoint-store.d.ts +2 -2
  28. package/dist/core/checkpoint-store.js +2 -2
  29. package/dist/core/child-clamp-seats.js +3 -3
  30. package/dist/core/compliance.js +1 -1
  31. package/dist/core/context-edit.js +1 -1
  32. package/dist/core/context-guard.d.ts +0 -19
  33. package/dist/core/context-guard.js +0 -38
  34. package/dist/core/decision-receipt.d.ts +27 -1
  35. package/dist/core/decision-receipt.js +176 -1
  36. package/dist/core/engine-notice.d.ts +60 -8
  37. package/dist/core/engine-notice.js +2 -1
  38. package/dist/core/gate-fold.js +3 -2
  39. package/dist/core/gate-outcome.js +4 -4
  40. package/dist/core/governance-codes.d.ts +221 -13
  41. package/dist/core/governance-codes.js +104 -4
  42. package/dist/core/hook-seats.js +2 -2
  43. package/dist/core/hooks.d.ts +21 -2
  44. package/dist/core/hooks.js +4 -1
  45. package/dist/core/mailbox-store.js +3 -3
  46. package/dist/core/mcp-failure.js +4 -4
  47. package/dist/core/mcp-injection-drop.js +1 -1
  48. package/dist/core/memory-engine/consolidation.js +1 -1
  49. package/dist/core/memory-engine/delegation-settlement.d.ts +8 -0
  50. package/dist/core/memory-engine/delegation-settlement.js +7 -0
  51. package/dist/core/memory-engine/engine.d.ts +12 -2
  52. package/dist/core/memory-engine/engine.js +10 -6
  53. package/dist/core/memory-engine/export-bundle.js +1 -1
  54. package/dist/core/memory-engine/layout.d.ts +6 -6
  55. package/dist/core/memory-engine/layout.js +6 -8
  56. package/dist/core/memory-engine/session-write-home.d.ts +30 -0
  57. package/dist/core/memory-engine/session-write-home.js +34 -0
  58. package/dist/core/memory-engine/tools.js +1 -1
  59. package/dist/core/memory-engine/types.js +1 -1
  60. package/dist/core/memory.js +1 -1
  61. package/dist/core/mode-policy.d.ts +10 -1
  62. package/dist/core/mode-policy.js +15 -7
  63. package/dist/core/model-seat.js +1 -1
  64. package/dist/core/permission-rule-model.js +3 -3
  65. package/dist/core/permission-rule-store.js +2 -2
  66. package/dist/core/read-only-shell-table.js +8 -8
  67. package/dist/core/retention.js +2 -2
  68. package/dist/core/runner/assemble-result.js +15 -5
  69. package/dist/core/runner/classifier-ladder.js +1 -1
  70. package/dist/core/runner/denial-limit-arms.d.ts +2 -0
  71. package/dist/core/runner/denial-limit-arms.js +6 -2
  72. package/dist/core/runner/inherited-ask-grants.d.ts +6 -5
  73. package/dist/core/runner/inherited-ask-grants.js +3 -2
  74. package/dist/core/runner/prepare-artifact.d.ts +3 -2
  75. package/dist/core/runner/prepare-context-lane.d.ts +19 -15
  76. package/dist/core/runner/prepare-context-lane.js +11 -15
  77. package/dist/core/runner/prepare-gate-stations.js +5 -5
  78. package/dist/core/runner/prepare-hands-readface.d.ts +4 -3
  79. package/dist/core/runner/prepare-hands-readface.js +1 -0
  80. package/dist/core/runner/prepare-inherited-gate.js +1 -1
  81. package/dist/core/runner/prepare-policy-chain.js +10 -10
  82. package/dist/core/runner/prepare-turn-wiring.d.ts +2 -2
  83. package/dist/core/runner/run-harness-handlers.js +3 -0
  84. package/dist/core/runner/run-identity-wiring.d.ts +1 -1
  85. package/dist/core/runner/run-identity-wiring.js +0 -2
  86. package/dist/core/runner/run-leg.js +2 -2
  87. package/dist/core/runner/run-stop-and-final-verify.js +1 -0
  88. package/dist/core/runner/run-turn-boundary.js +1 -1
  89. package/dist/core/runner/session-mode-seats.d.ts +11 -3
  90. package/dist/core/runner/session-mode-seats.js +6 -0
  91. package/dist/core/runner/session-rule-policy.js +2 -3
  92. package/dist/core/runner/stream-steer-verb.js +1 -1
  93. package/dist/core/runner/tool-call-id-fold-seat.js +16 -9
  94. package/dist/core/runner/tool-turn-repair-seat.d.ts +11 -1
  95. package/dist/core/runner/tool-turn-repair-seat.js +35 -0
  96. package/dist/core/runner/turn-attachments.js +1 -1
  97. package/dist/core/runtime-caps.js +1 -1
  98. package/dist/core/safety-axis-vocab.d.ts +5 -0
  99. package/dist/core/safety-axis-vocab.js +5 -0
  100. package/dist/core/session-reconcile.d.ts +9 -7
  101. package/dist/core/session-reconcile.js +21 -1
  102. package/dist/core/shared-memory/types.js +1 -1
  103. package/dist/core/side-query.d.ts +6 -1
  104. package/dist/core/store-contracts/read-back-shape-contract.js +1 -1
  105. package/dist/core/swappable-deps.js +1 -1
  106. package/dist/core/task-notification.js +4 -4
  107. package/dist/core/task-registry-shared.js +1 -1
  108. package/dist/core/terminal-cause.d.ts +19 -0
  109. package/dist/core/tool-face.js +6 -6
  110. package/dist/core/tool-policy.d.ts +17 -11
  111. package/dist/core/tool-policy.js +38 -72
  112. package/dist/core/tool-roster.js +1 -1
  113. package/dist/core/tool-search-setting.js +1 -1
  114. package/dist/core/trace.d.ts +5 -4
  115. package/dist/core/types.d.ts +2 -2
  116. package/dist/core/types.js +1 -1
  117. package/dist/core/wiring-manifest.js +2 -2
  118. package/dist/core/write-protect.js +1 -1
  119. package/dist/engine/harness/agent-harness.js +10 -6
  120. package/dist/engine/llm/tool-turns.d.ts +93 -4
  121. package/dist/engine/llm/tool-turns.js +63 -9
  122. package/dist/engine/llm/types.d.ts +11 -0
  123. package/dist/index.d.ts +5 -3
  124. package/dist/index.js +4 -2
  125. package/dist/internal/llm.d.ts +2 -2
  126. package/dist/internal/llm.js +1 -1
  127. package/dist/orchestration/workflow-governance.js +2 -2
  128. package/dist/orchestration/workflow-types.js +2 -2
  129. package/dist/shell/lex.js +7 -7
  130. package/dist/tools/fs/bash-program-position.js +2 -2
  131. package/dist/tools/fs/fs-shared.d.ts +2 -1
  132. package/dist/tools/fs/fs-shared.js +1 -1
  133. package/dist/tools/fs/index.d.ts +13 -6
  134. package/dist/tools/fs/index.js +12 -4
  135. package/dist/tools/fs/read-deny-home.d.ts +98 -45
  136. package/dist/tools/fs/read-deny-home.js +294 -117
  137. package/dist/tools/fs/read-deny.d.ts +2 -2
  138. package/dist/tools/fs/read-deny.js +2 -2
  139. package/dist/tools/fs/safety.d.ts +2 -0
  140. package/dist/tools/fs/safety.js +2 -2
  141. package/dist/tools/fs/search.d.ts +4 -3
  142. package/dist/tools/fs/search.js +3 -3
  143. package/dist/tools/fs/shell-read-check.d.ts +9 -0
  144. package/dist/tools/fs/shell-read-check.js +3 -2
  145. package/dist/tools/fs/shell-read-establish.d.ts +3 -0
  146. package/dist/tools/fs/shell-read-establish.js +1 -1
  147. package/dist/tools/fs/shell-read-identity.d.ts +4 -3
  148. package/dist/tools/fs/shell-read-identity.js +2 -2
  149. package/dist/tools/fs/shell-read-judge.js +2 -2
  150. package/dist/tools/fs/shell-read-plan.js +2 -2
  151. package/dist/tools/fs/shell-read-write-targets.js +1 -1
  152. package/package.json +1 -1
  153. package/test/export-surface.snapshot.json +17 -1
  154. package/wire-changes/core@7.38.0.json +664 -0
  155. package/wire-changes/core@7.38.1.json +56 -0
@@ -35,25 +35,6 @@ export declare const CONTEXT_TRIM_NOTICE: string;
35
35
  * (fixed timestamp keeps it fully deterministic).
36
36
  */
37
37
  export declare function insertTrimNotice(trimmed: AgentMessage[]): AgentMessage[];
38
- /**
39
- * design/123 D3 belt (invariant-level, live-caught 16k 400s): the FINAL request view must never
40
- * contain a toolResult whose tool-call assistant is absent — strict gateways hard-400 on it
41
- * ("Messages with role 'tool' must be a response to a preceding message with 'tool_calls'"). Every
42
- * individual producer is turn-aligned (findCutPoint never cuts on a toolResult; trimToBudget
43
- * turn-aligns its floor), but NO pipeline combination may ship an orphan, so the prepare-task
44
- * context hook sweeps its output through this last. Deterministic ⇒ byte-stable per request ⇒
45
- * prefix-cache safe. Returns the SAME array when clean (zero-cost fast path); `dropped` carries a
46
- * diagnostic per removed orphan (toolCallId + neighbor roles) for onError attribution.
47
- */
48
- export declare function dropOrphanToolResults(messages: AgentMessage[]): {
49
- messages: AgentMessage[];
50
- dropped: Array<{
51
- toolCallId: string;
52
- index: number;
53
- prevRole: string;
54
- nextRole: string;
55
- }>;
56
- };
57
38
  /** R2-3 (falsification round 2) — does the message's ENGINE-AUTHORED region cover [start, start+len)?
58
39
  * The carrier scan must never crown a counterfeit: a tool result or a user paste can reproduce the
59
40
  * wrapped frame bytes verbatim, but only ENGINE metadata (engineMinted whole-message, the
@@ -77,44 +77,6 @@ export function insertTrimNotice(trimmed) {
77
77
  at++;
78
78
  return [...trimmed.slice(0, at), notice, ...trimmed.slice(at)];
79
79
  }
80
- export function dropOrphanToolResults(messages) {
81
- const seenCallIds = new Set();
82
- let orphanAt = -1;
83
- for (let i = 0; i < messages.length; i++) {
84
- const m = messages[i];
85
- if (m.role === "assistant" && Array.isArray(m.content)) {
86
- for (const c of m.content) {
87
- if (c?.type === "toolCall" && typeof c.id === "string")
88
- seenCallIds.add(c.id);
89
- }
90
- }
91
- else if (m.role === "toolResult" && !seenCallIds.has(m.toolCallId ?? "")) {
92
- orphanAt = i;
93
- break;
94
- }
95
- }
96
- if (orphanAt === -1) {
97
- return { messages, dropped: [] };
98
- }
99
- const dropped = [];
100
- const out = messages.slice(0, orphanAt);
101
- const roleOf = (i) => i < 0 || i >= messages.length ? "(none)" : (messages[i].role ?? "(unknown)");
102
- for (let i = orphanAt; i < messages.length; i++) {
103
- const m = messages[i];
104
- if (m.role === "assistant" && Array.isArray(m.content)) {
105
- for (const c of m.content) {
106
- if (c?.type === "toolCall" && typeof c.id === "string")
107
- seenCallIds.add(c.id);
108
- }
109
- }
110
- else if (m.role === "toolResult" && !seenCallIds.has(m.toolCallId ?? "")) {
111
- dropped.push({ toolCallId: m.toolCallId ?? "(missing)", index: i, prevRole: roleOf(i - 1), nextRole: roleOf(i + 1) });
112
- continue;
113
- }
114
- out.push(messages[i]);
115
- }
116
- return { messages: out, dropped };
117
- }
118
80
  export function engineRegionCovers(m, start, len) {
119
81
  const meta = m;
120
82
  if (meta.engineMinted === true)
@@ -1,6 +1,31 @@
1
1
  import type { PermissionResult } from "./tool-policy.js";
2
- /** The screen's one refusal — every arm's deny, so an out-of-contract decision is one code whichever arm caught it. */
3
2
  export declare const refusedOutOfContract: (message: string) => PermissionResult;
3
+ export declare const isEngineRefusal: (d: unknown) => boolean;
4
+ /** A decision's `decisionReason` — the closed set; the type is derived FROM this list, so a word added to either side is a
5
+ * compile error rather than a drift between the type and the screen. Module-private (the package publishes the type). */
6
+ declare const DECISION_REASONS: readonly ["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable", "read_only"];
7
+ export type DecisionReason = (typeof DECISION_REASONS)[number];
8
+ export declare const RETIRED_WINDOW_MEMBER_DENY_MESSAGE: string;
9
+ export declare const carriesRetiredWindow: (fallback: unknown) => boolean;
10
+ /**
11
+ * The VALUE screen over the engine's one-read record of a decision (never a second read of the caller's object): every
12
+ * closed-set member holds a member of its set or is absent (`action` present), and the retired window is absent. The refusal
13
+ * names the member and the set, one code for every arm. `undefined` is the receipt's spelling of absence.
14
+ */
15
+ export declare function screenContractValues(contract: Record<string, unknown>, opts?: {
16
+ skip?: ReadonlySet<string>;
17
+ optional?: ReadonlySet<string>;
18
+ }): PermissionResult | undefined;
19
+ /**
20
+ * The receipt of the records a decision nests, into the one-read record itself: each record that is PRESENT is received by
21
+ * {@link receiveContract} (the decision's own function — an ordinary object, own enumerable data members, a prototype member
22
+ * refused by name) and must hold every required member; a present value that is not an object, or a record missing a required
23
+ * member, is refused by path. The engine's copy replaces the caller's record in `contract`, so no station reads the caller's
24
+ * again. Returns the refusal, or `undefined` when every record is in contract.
25
+ */
26
+ export declare function receiveNestedRecords(contract: Record<string, unknown>): PermissionResult | undefined;
27
+ /** The nested records of an engine receipt, by path — frozen with the receipt, so a receipt handed back unread is the receipt. */
28
+ export declare function nestedRecordsOf(receipt: object): object[];
4
29
  /** One level of the receipt, the same for the decision and each record it nests. The carrier is an ORDINARY object — it and
5
30
  * every object on its prototype chain are looked at for being a Proxy before any descriptor is asked for (a Proxy's traps are
6
31
  * the caller's code; the look invokes none of them) — so the receipt runs none of the carrier's code: a contract member is
@@ -19,3 +44,4 @@ export type Received = {
19
44
  refused: PermissionResult;
20
45
  };
21
46
  export declare function receiveContract(carrier: object, members: readonly string[], path: string): Received;
47
+ export {};
@@ -1,6 +1,181 @@
1
1
  import { types } from "node:util";
2
+ import { ASK_CLASSES, ASK_ORIGINS, isAskClass, isAskOrigin, isPersistedRuleMandate, PERSISTED_RULE_MANDATES } from "./ask-origin.js";
2
3
  import { thrownText } from "./thrown-value.js";
3
- export const refusedOutOfContract = (message) => ({ action: "deny", message, decisionReason: "rule" });
4
+ const ENGINE_REFUSALS = new WeakSet();
5
+ export const refusedOutOfContract = (message) => {
6
+ const refused = { action: "deny", message, decisionReason: "rule" };
7
+ ENGINE_REFUSALS.add(refused);
8
+ return refused;
9
+ };
10
+ export const isEngineRefusal = (d) => typeof d === "object" && d !== null && ENGINE_REFUSALS.has(d);
11
+ const DECISION_REASONS = Object.freeze(["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable", "read_only"]);
12
+ const DECISION_ACTIONS = Object.keys({ allow: 0, deny: 0, ask: 0 });
13
+ const GUARDS = Object.keys({ unverifiable_delete: 0, transcript_write: 0, artifact_shared_read: 0 });
14
+ const RULE_UNREADABLE_WORDS = Object.keys({ content_deny: 0, content_ask: 0, persisted_call: 0 });
15
+ const AUTHORED_BY_WORDS = Object.keys({ deployment: 0 });
16
+ const memberOf = (words) => {
17
+ const set = new Set(words);
18
+ return (v) => set.has(v);
19
+ };
20
+ const CLOSED_MEMBERS = [
21
+ { member: "action", words: DECISION_ACTIONS, has: memberOf(DECISION_ACTIONS), required: true },
22
+ { member: "decisionReason", words: DECISION_REASONS, has: memberOf(DECISION_REASONS) },
23
+ { member: "origin", words: ASK_ORIGINS, has: isAskOrigin },
24
+ { member: "askClass", words: ASK_CLASSES, has: isAskClass },
25
+ { member: "classes", words: ASK_CLASSES, has: isAskClass, list: true },
26
+ { member: "foldedAskClasses", words: ASK_CLASSES, has: isAskClass, list: true },
27
+ { member: "mandate", words: PERSISTED_RULE_MANDATES, has: isPersistedRuleMandate },
28
+ { member: "guard", words: GUARDS, has: memberOf(GUARDS) },
29
+ { member: "ruleUnreadable", words: RULE_UNREADABLE_WORDS, has: memberOf(RULE_UNREADABLE_WORDS) },
30
+ { member: "authoredBy", words: AUTHORED_BY_WORDS, has: memberOf(AUTHORED_BY_WORDS) },
31
+ { member: "requiresRealApproval", words: [true, false], has: (v) => v === true || v === false },
32
+ { member: "probeMandated", words: [true], has: (v) => v === true },
33
+ { member: "mandated", words: [true], has: (v) => v === true },
34
+ ];
35
+ const RETIRED_WINDOW_MEMBER = "autoDenyAfterMs";
36
+ export const RETIRED_WINDOW_MEMBER_DENY_MESSAGE = `a permission decision's "denialLimitFallback" carries "${RETIRED_WINDOW_MEMBER}" — the decision names the counts only; ` +
37
+ `the auto-deny window is decided by the ask's route (a live approver arms it, every other route runs none) and cannot be ` +
38
+ `declared by a policy; denied fail-closed rather than silently replacing the declared number`;
39
+ export const carriesRetiredWindow = (fallback) => typeof fallback === "object" && fallback !== null && fallback[RETIRED_WINDOW_MEMBER] !== undefined;
40
+ const OPERAND_MEMBERS = Object.keys({ shown: 0, total: 0 });
41
+ const NESTED_CONTRACTS = [
42
+ {
43
+ holder: "",
44
+ member: "denialLimitFallback",
45
+ members: Object.keys({ consecutive: 0, total: 0, limit: 0, autoDenyAfterMs: 0 }),
46
+ required: Object.keys({ consecutive: 0, total: 0, limit: 0 }),
47
+ },
48
+ { holder: "", member: "probeCause", members: Object.keys({ code: 0, roots: 0, further: 0 }), required: Object.keys({ code: 0, roots: 0 }) },
49
+ { holder: "probeCause.", member: "roots", members: OPERAND_MEMBERS, required: OPERAND_MEMBERS },
50
+ { holder: "probeCause.", member: "further", members: OPERAND_MEMBERS, required: OPERAND_MEMBERS },
51
+ ];
52
+ const nonNegativeInteger = (v) => typeof v === "number" && Number.isInteger(v) && v >= 0;
53
+ const NESTED_VALUE_FENCES = {
54
+ denialLimitFallback: { consecutive: nonNegativeInteger, total: nonNegativeInteger, limit: (v) => v === "consecutive" || v === "total" },
55
+ probeCause: { code: (v) => typeof v === "string" && v.length > 0 },
56
+ "probeCause.roots": { total: nonNegativeInteger },
57
+ "probeCause.further": { total: nonNegativeInteger },
58
+ };
59
+ const NESTED_LIST_MEMBERS = {
60
+ "probeCause.roots": { shown: { words: ["a string"], has: (x) => typeof x === "string" } },
61
+ "probeCause.further": { shown: { words: ["a string"], has: (x) => typeof x === "string" } },
62
+ };
63
+ function renderForeignValue(v) {
64
+ try {
65
+ return JSON.stringify(String(v).slice(0, 64));
66
+ }
67
+ catch {
68
+ return `[unprintable ${typeof v}]`;
69
+ }
70
+ }
71
+ export function screenContractValues(contract, opts) {
72
+ for (const { member, words, has, list, required } of CLOSED_MEMBERS) {
73
+ if (opts?.skip?.has(member) === true)
74
+ continue;
75
+ const v = contract[member];
76
+ if (v === undefined) {
77
+ if (required === true && opts?.optional?.has(member) !== true)
78
+ return refusedOutOfContract(`a permission decision's "${member}" is absent — the closed set is ${words.join("/")}; denied fail-closed rather than reading a word outside it as an allow`);
79
+ continue;
80
+ }
81
+ if (list === true) {
82
+ const received = receiveList(v, member, words, has);
83
+ if (received.kind === "refused")
84
+ return received.refused;
85
+ contract[member] = received.held;
86
+ continue;
87
+ }
88
+ if (has(v))
89
+ continue;
90
+ return unrecognized(member, v, words);
91
+ }
92
+ if (carriesRetiredWindow(contract.denialLimitFallback))
93
+ return refusedOutOfContract(RETIRED_WINDOW_MEMBER_DENY_MESSAGE);
94
+ return undefined;
95
+ }
96
+ const unrecognized = (member, v, words) => refusedOutOfContract(`a permission decision carries an unrecognized "${member}" value (${renderForeignValue(v)}) — ` +
97
+ `the closed set is ${words.join("/")}; denied fail-closed rather than letting consumers branch on a value this layer cannot classify`);
98
+ function receiveList(v, member, words, has) {
99
+ if (typeof v !== "object" || v === null)
100
+ return { kind: "refused", refused: unrecognized(member, v, words) };
101
+ const ordinary = ordinaryChain(v);
102
+ if (ordinary.kind === "not")
103
+ return { kind: "refused", refused: refusedOutOfContract(`a permission decision's "${member}" ${ordinary.why} — the engine reads a list off an ordinary array's own property descriptors and runs none of the carrier's code; denied fail-closed`) };
104
+ if (!Array.isArray(v))
105
+ return { kind: "refused", refused: unrecognized(member, v, words) };
106
+ let own;
107
+ try {
108
+ own = Object.getOwnPropertyDescriptors(v);
109
+ }
110
+ catch (e) {
111
+ return { kind: "refused", refused: refusedOutOfContract(`a permission decision's "${member}" threw while its property descriptors were looked at (${thrownText(e)}) — not an ordinary array; denied fail-closed`) };
112
+ }
113
+ const length = Object.hasOwn(own, "length") ? own.length : undefined;
114
+ if (length === undefined || !Object.hasOwn(length, "value") || typeof length.value !== "number")
115
+ return { kind: "refused", refused: refusedOutOfContract(`a permission decision's "${member}" has no plain length — not an ordinary array; denied fail-closed`) };
116
+ const held = [];
117
+ for (let i = 0; i < length.value; i++) {
118
+ const d = Object.hasOwn(own, String(i)) ? own[String(i)] : undefined;
119
+ if (d === undefined)
120
+ return { kind: "refused", refused: refusedOutOfContract(`a permission decision's "${member}" has no element at index ${i} (a hole) — the closed set is ${words.join("/")}; denied fail-closed rather than reading a hole as a word`) };
121
+ if (!Object.hasOwn(d, "value"))
122
+ return { kind: "refused", refused: refusedOutOfContract(`a permission decision's "${member}" element ${i} is held by an accessor — list elements must be own data properties, not values supplied through an accessor; denied fail-closed`) };
123
+ if (!has(d.value))
124
+ return { kind: "refused", refused: unrecognized(member, d.value, words) };
125
+ held.push(d.value);
126
+ }
127
+ return { kind: "held", held };
128
+ }
129
+ export function receiveNestedRecords(contract) {
130
+ const records = Object.assign(Object.create(null), { "": contract });
131
+ for (const { holder, member, members, required } of NESTED_CONTRACTS) {
132
+ const held = records[holder];
133
+ if (held === undefined)
134
+ continue;
135
+ const v = held[member];
136
+ if (v === undefined)
137
+ continue;
138
+ const path = `${holder}${member}`;
139
+ const shape = `a nested contract record is a plain object holding ${required.map((r) => `"${r}"`).join(", ")}`;
140
+ if (typeof v !== "object" || v === null) {
141
+ return refusedOutOfContract(`a permission decision's "${path}" is ${renderForeignValue(v)} (not an object) — ${shape}; denied fail-closed rather than reading a value outside the contract as a record`);
142
+ }
143
+ const nested = receiveContract(v, members, `${path}.`);
144
+ if (nested.kind === "refused")
145
+ return nested.refused;
146
+ if (Array.isArray(v))
147
+ return refusedOutOfContract(`a permission decision's "${path}" is an array — ${shape}; denied fail-closed rather than reading a list's properties as a record`);
148
+ const missing = required.find((r) => nested.held[r] === undefined);
149
+ if (missing !== undefined)
150
+ return refusedOutOfContract(`a permission decision's "${path}" record is missing its required member "${missing}" — ${shape}; denied fail-closed rather than reading a partial record`);
151
+ for (const [m, { words, has }] of Object.entries(NESTED_LIST_MEMBERS[path] ?? {})) {
152
+ const value = nested.held[m];
153
+ if (value === undefined)
154
+ continue;
155
+ const received = receiveList(value, `${path}.${m}`, words, has);
156
+ if (received.kind === "refused")
157
+ return received.refused;
158
+ nested.held[m] = received.held;
159
+ }
160
+ for (const [m, ok] of Object.entries(NESTED_VALUE_FENCES[path] ?? {})) {
161
+ const value = nested.held[m];
162
+ if (value !== undefined && !ok(value))
163
+ return refusedOutOfContract(`a permission decision's "${path}.${m}" is ${renderForeignValue(value)} — outside what the record's type admits; denied fail-closed rather than reading a value this layer cannot classify`);
164
+ }
165
+ held[member] = records[`${path}.`] = nested.held;
166
+ }
167
+ return undefined;
168
+ }
169
+ export function nestedRecordsOf(receipt) {
170
+ const ownRecord = (o, k) => (typeof o === "object" && o !== null && Object.hasOwn(o, k) ? o[k] : undefined);
171
+ const out = [];
172
+ for (const { holder, member } of NESTED_CONTRACTS) {
173
+ const record = ownRecord(holder.split(".").filter(Boolean).reduce(ownRecord, receipt), member);
174
+ if (typeof record === "object" && record !== null)
175
+ out.push(record);
176
+ }
177
+ return out;
178
+ }
4
179
  const spellsAbsence = (d) => Object.hasOwn(d, "value") && d.value === undefined;
5
180
  function ordinaryChain(carrier) {
6
181
  const chain = [];
@@ -254,19 +254,37 @@ export interface EngineNotice {
254
254
  * - `"route.tool_call_id_folded"` (#1207) — the provider named a tool call with an id outside the shape the
255
255
  * engine keys calls under (visible ASCII, 1–128, no `~`), so the brain keyed it under a deterministic fold
256
256
  * (`ToolCall.id`) and keeps the provider's spelling beside it (`ToolCall.wireId`), handed back only to the
257
- * provider. Advisory: the run is unchanged. ONCE PER RUN, on the first folded call — a gateway fact, not a
258
- * call fact. Audience `operator`; `detail: { toolCallId, rawLength, reason, sessionId, runId }` —
259
- * `toolCallId` the engine id, `reason` `"charset"` (a control byte, a space, a non-ASCII unit or `~`) or
260
- * `"length"` (admitted characters, over 128); never the raw bytes. Absent ⇒ no call of the run was folded.
257
+ * provider; or (#1234) it gave two calls in ONE response the same id, so the brain renamed the later ones on
258
+ * both faces (every provider requires a request's call ids to be unique). Advisory: every call runs. ONCE PER
259
+ * RUN, on the first folded call — a gateway fact, not a call fact. Audience `operator`;
260
+ * `detail: { toolCallId, rawLength?, reason, sessionId, runId }` — `toolCallId` the engine id, `reason`
261
+ * `"charset"` (a control byte, a space, a non-ASCII unit or `~`), `"length"` (admitted characters, over 128)
262
+ * or `"duplicate"` (a renamed later call); `rawLength` present for the two shape reasons only; never the raw
263
+ * bytes. Absent ⇒ no call of the run was folded.
261
264
  * - `"context.tool_turn_repaired"` (#1224) — a provider request was built from a transcript that breaks the
262
265
  * tool-turn law, and the request view restored it (`engine/llm/tool-turns.ts`; the transcript is never
263
266
  * rewritten): a block after an assistant's tool calls moved ahead of them (`trailing_text`), a result recorded
264
267
  * later moved up behind its call (`late_result`), a call with no result closed with the reconcile family's
265
268
  * interrupted result (`missing_result`), a second result recorded for one call folded into the first
266
- * (`duplicate_result`; an interrupted close beside a recorded result is left out, no recorded result is). Advisory:
269
+ * (`duplicate_result`; an interrupted close beside a recorded result is left out, no recorded result is), a second
270
+ * call with an id its message already used left out — the message keeps the first, whose result carries every
271
+ * result recorded for the id (`duplicate_call`, #1241), a result no earlier call answers left out (`orphan_result`,
272
+ * #1242: its call is gone or it was recorded ahead of it; the one repair of the view as a whole, its ids once
273
+ * each in transcript order). Advisory:
267
274
  * the run proceeds on the repaired request. Audience `operator`; `detail: { repairs: [{ form, toolCallIds }],
268
275
  * sessionId, runId }`. ONCE PER RUN PER (form, calls) — the same broken turn is repaired on every request of
269
276
  * the run and announced on the first. Absent ⇒ every request of the run already had the structure.
277
+ * - `"task.tool_turn_incomplete"` (#1230) — the person's line for a tool turn the model was handed incomplete, beside
278
+ * the note the model itself reads. `detail.cause` (closed `TOOL_TURN_INCOMPLETE_CAUSES`): `truncated` — calls whose
279
+ * arguments were cut before they parsed, dropped (they did not run; the model is asked to re-issue them);
280
+ * `unnamed` — calls that arrived with no tool name, dropped the same way; `bad_frame` — streamed response frames
281
+ * that could not be read, dropped (the response may be missing content); `missing_result_synthesized` — an
282
+ * earlier call with no recorded result, which the request view closed as interrupted, outcome unknown (the
283
+ * `missing_result` repair above). Audience `"user"`; `detail: { cause, count, toolCallIds?, sessionId, runId }` —
284
+ * `count` the calls (or frames) of that cause in the turn or request that first showed it; `toolCallIds` on
285
+ * `missing_result_synthesized` only, each id in the provider echo shape (a stored id outside it is not shown;
286
+ * `count` still counts it). Closed-set words, a count and call ids only: never a tool name, an argument or the
287
+ * note's text. ONCE PER RUN PER CAUSE. Absent ⇒ nothing of that cause reached the model.
270
288
  * - `"config.read_face_deployment_clamped"` (#237) — a deployment-wide `readFace: "open"` is not
271
289
  * in force beside a read-only (verifier) mount: it clamps to "roots" without throwing
272
290
  * (stricter-wins; the clamp verdict stands, only its occurrence was undisclosed). Announced
@@ -347,7 +365,7 @@ export interface EngineNotice {
347
365
  * `shellGate` `"off"` / `"classify"` are dropped, `"always"` kept) and the next suspend writes the new member.
348
366
  * `detail: { migrated: { permissionMode?, droppedKeys }, sessionId, runId }`. Audience `operator`. A row the
349
367
  * migration cannot read refuses the resume instead (`resume.legacy_mode_seed`, the error-code row above). The
350
- * migration is kept through 7.37.x and removed in 7.38.0, which refuses a row still carrying these keys.
368
+ * migration is kept through 7.38.x and removed in 7.39.0, which refuses a row still carrying these keys.
351
369
  * - `"config.durable_gate_unavailable"` — a durable-approval MANDATE is in force on a leg with NO
352
370
  * checkpoint store: the per-principal `forceDurableGate` entitlement, or the task's own
353
371
  * `durableApproval` opt-in with no live approver seat (`durableMandateSourceOf`, the predicate the
@@ -374,8 +392,12 @@ export interface EngineNotice {
374
392
  * frame's two count keys). They are NOT redelivered (a steer aimed at a finished run must not
375
393
  * fire at the next one — unlike ENGINE notes, which pend per session); the loud half of the
376
394
  * #257 contract's "accepted = enqueued, not consumed" sentence;
377
- * `detail: { steer, taskId?, sessionId?, runId? }` / `{ followUp, taskId?, sessionId?, runId? }`
378
- * (`sessionId` = #433's routing half for these two `"user"`-audience rows; `runId` = #499's
395
+ * `detail: { steer, cause, taskId?, sessionId?, runId? }` / `{ followUp, taskId?, sessionId?, runId? }`
396
+ * (`cause` on the steer row only, closed `USER_STEER_UNDRAINED_CAUSES`: `"leg_end"` — this engine's arm, the
397
+ * leg ended with it still owed; `"crashed"` — minted by the SERVING LAYER, not this engine, before the
398
+ * session's next run, for steers it had accepted whose consumption was not confirmed before the process died: their queue was the
399
+ * serving layer's, so only it can count them. One code for both, so a consumer de-duplicating on `code` keeps
400
+ * one row; `sessionId` = #433's routing half for these two `"user"`-audience rows; `runId` = #499's
379
401
  * per-run identity, the key this account is actually about — see
380
402
  * {@link undrainedUserInputNotices}; all omitted when the caller has none, never fabricated).
381
403
  * Per-run, at most once per family (the terminal sweep is a single site) — which is why
@@ -786,6 +808,25 @@ export interface EngineNotice {
786
808
  * throw rendered twice keeps one id, two refusals sharing a code/scope/message get two. Omitted
787
809
  * (never fabricated) when the caller composes the line with no error in hand.
788
810
  *
811
+ * The four SERVER-minted rows ({@link NOTICE_MINTER} `"server"`) — catalogued so their audience and detail shape
812
+ * are read here like every other row; this engine never mints them. A server forwards them through the same sink.
813
+ * - `"instructions.source_changed"` — the project instruction file this run injects is not the one the same
814
+ * session's previous run injected (a file added, removed or renamed). Audience `"user"`; `detail: { previous,
815
+ * current, sessionId }` — `previous` / `current` are chain-member relative paths, `null` = that run had none.
816
+ * No `runId`. Once per run.
817
+ * - `"session.records_skipped"` — the session's store skipped records it could not read when it opened the
818
+ * session; the run continues on the history that remains. Audience `"user"`; `detail: { dropped, entriesKept,
819
+ * sessionId }` — two observed counts, not to be combined. No `runId`. Once per open that skipped.
820
+ * - `"memory.layer_locked_legacy"` — a memory layer an older engine left without its owner-write bit was found at
821
+ * boot; deletes in it will fail until the operator restores the bit. Audience `"operator"`; `detail: { layers,
822
+ * remedy }` — layer directory names and the one command. No session. Once per boot.
823
+ * - `"server.model_unreachable"` — a model call of this leg ended in error and its last request failed to
824
+ * connect. Audience `"user"`; `detail: { code, targetHost, remedy, sessionId, proxyInUse? }` — `code` one of
825
+ * `UND_ERR_CONNECT_TIMEOUT` / `ECONNREFUSED` / `ENOTFOUND` / `EAI_AGAIN` / `EHOSTUNREACH` / `ENETUNREACH`;
826
+ * `targetHost` the model endpoint's `host:port`; `remedy` the constant `"check VPN / proxy; restart if the
827
+ * proxy changed since boot"`; `proxyInUse` present only when that request went through a proxy, its value the
828
+ * variable's name (`HTTP_PROXY` or `HTTPS_PROXY`), never the proxy address. No `runId`. Once per leg per target.
829
+ *
789
830
  * Deliberately NOT a notice family: brain retry/reconnect liveness (a rate limit, a 5xx, a
790
831
  * transient network failure being retried). Those are per-attempt liveness frames with their own
791
832
  * frequency semantics and ride the wire `status` channel ({@link BrainStatus}), whose sink the
@@ -910,6 +951,17 @@ export declare function settleOwedUserInput(account: OwedUserInputAccount, rowRe
910
951
  sessionId?: string;
911
952
  runId?: string;
912
953
  }, onNotice: ((notice: EngineNotice) => void) | undefined): void;
954
+ /**
955
+ * Why a person's steer is reported as not consumed — `task.user_steer_undrained`'s `detail.cause`. Closed:
956
+ * · `leg_end` — the leg ended with the steer still owed (this engine mints it, at the leg's settlement);
957
+ * · `crashed` — the engine process ended before the leg settled, and a steer the serving layer had accepted for it was
958
+ * not confirmed consumed before the process died (a live leg gives the serving layer no evidence that a steer reached
959
+ * 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
960
+ * mints this arm, before the next run of the session starts — the one member-level exception to
961
+ * {@link import("./governance-codes.js").NOTICE_MINTER}'s row for the code, which stays `"engine"`.
962
+ */
963
+ export declare const USER_STEER_UNDRAINED_CAUSES: readonly ["leg_end", "crashed"];
964
+ export type UserSteerUndrainedCause = (typeof USER_STEER_UNDRAINED_CAUSES)[number];
913
965
  export declare function undrainedUserInputNotices(counts: {
914
966
  steer: number;
915
967
  followUp: number;
@@ -51,6 +51,7 @@ export function settleOwedUserInput(account, rowReturned, ids, onNotice) {
51
51
  }
52
52
  }
53
53
  const SETTLED_OWED_ACCOUNTS = new WeakSet();
54
+ export const USER_STEER_UNDRAINED_CAUSES = Object.freeze(["leg_end", "crashed"]);
54
55
  export function undrainedUserInputNotices(counts, taskId, sessionId, runId) {
55
56
  const tid = taskId !== undefined ? { taskId } : {};
56
57
  const sid = sessionId !== undefined ? { sessionId } : {};
@@ -61,7 +62,7 @@ export function undrainedUserInputNotices(counts, taskId, sessionId, runId) {
61
62
  out.push({
62
63
  code: "task.user_steer_undrained",
63
64
  message: `${counts.steer} user steer(s) ${tail}`,
64
- detail: { steer: counts.steer, ...tid, ...sid, ...rid },
65
+ detail: { steer: counts.steer, cause: "leg_end", ...tid, ...sid, ...rid },
65
66
  });
66
67
  }
67
68
  if (counts.followUp > 0) {
@@ -1,4 +1,5 @@
1
1
  import { inlineUntrusted } from "./untrusted-text.js";
2
+ import { markTightenMessage } from "./safety-axis-vocab.js";
2
3
  import { PROBE_REASON_MAX, normalizeProbeCause } from "./checkpoint-store.js";
3
4
  import { normalizeReadRootCandidate } from "./read-root-candidate.js";
4
5
  import {} from "./runner/gate-exit.js";
@@ -70,7 +71,7 @@ export async function runGateFold(pass) {
70
71
  if (input.egress && pass.decision.action === "allow") {
71
72
  pass.decision = {
72
73
  action: "ask",
73
- message: `tool "${toolName}" performs an external write (egress) — explicit approval required`,
74
+ message: markTightenMessage(toolName, "egress"),
74
75
  decisionReason: "safety",
75
76
  };
76
77
  pass.tightenedBy = "safety";
@@ -122,7 +123,7 @@ export async function runGateFold(pass) {
122
123
  if (tighten) {
123
124
  pass.decision = {
124
125
  action: "ask",
125
- message: `tool "${toolName}" performs an irreversible action — explicit approval required`,
126
+ message: markTightenMessage(toolName, "irreversible"),
126
127
  decisionReason: "safety",
127
128
  ...(probeReason !== undefined ? { probeReason } : {}),
128
129
  ...(probeCause !== undefined ? { probeCause } : {}),
@@ -1,6 +1,6 @@
1
1
  import { isAskClass, isAskOrigin, isModeReleaseReason, isPermissionMode, RELEASABLE_ASK_CLASSES } from "./ask-origin.js";
2
2
  import { isAutoModeFallbackCause } from "./auto-mode.js";
3
- export const SETTLEMENT_KINDS = [
3
+ export const SETTLEMENT_KINDS = Object.freeze([
4
4
  "human_allowed",
5
5
  "human_refused",
6
6
  "approval_window_expired",
@@ -14,7 +14,7 @@ export const SETTLEMENT_KINDS = [
14
14
  "blanket_allow_refused",
15
15
  "policy_refused",
16
16
  "task_aborted",
17
- ];
17
+ ]);
18
18
  const SETTLEMENT_KIND_SET = new Set(SETTLEMENT_KINDS);
19
19
  export function isSettlementKind(v) {
20
20
  return SETTLEMENT_KIND_SET.has(v);
@@ -56,7 +56,7 @@ export const SETTLEMENT_NOBODY_JUDGED = {
56
56
  policy_refused: true,
57
57
  task_aborted: false,
58
58
  };
59
- export const DENIED_BY_VALUES = ["policy", "hook", "org", "persisted_rule", "classifier", "compliance", "write_protection", "ask_resolution", "read_boundary", "mode"];
59
+ export const DENIED_BY_VALUES = Object.freeze(["policy", "hook", "org", "persisted_rule", "classifier", "compliance", "write_protection", "ask_resolution", "read_boundary", "mode"]);
60
60
  const DENIED_BY_SET = new Set(DENIED_BY_VALUES);
61
61
  export function isDeniedBy(v) {
62
62
  return DENIED_BY_SET.has(v);
@@ -73,7 +73,7 @@ export const DENIED_BY_MAY_VETO = {
73
73
  read_boundary: true,
74
74
  mode: false,
75
75
  };
76
- export const CLASSIFIER_DENY_CAUSES = ["unavailable", "parse_error"];
76
+ export const CLASSIFIER_DENY_CAUSES = Object.freeze(["unavailable", "parse_error"]);
77
77
  const CLASSIFIER_DENY_CAUSE_SET = new Set(CLASSIFIER_DENY_CAUSES);
78
78
  export function isClassifierDenyCause(v) {
79
79
  return CLASSIFIER_DENY_CAUSE_SET.has(v);