@sema-agent/core 7.17.7 → 7.19.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 (201) hide show
  1. package/CHANGELOG.md +233 -13
  2. package/dist/agents/send-message-tool.js +1 -0
  3. package/dist/agents/subagent.d.ts +1 -0
  4. package/dist/agents/subagent.js +5 -0
  5. package/dist/agents/teacher.js +1 -0
  6. package/dist/agents/verify.js +3 -0
  7. package/dist/brain/anthropic.js +6 -3
  8. package/dist/brain/open-responses.js +10 -4
  9. package/dist/brain/openai.js +5 -2
  10. package/dist/brain/reasoning.d.ts +13 -13
  11. package/dist/brain/reasoning.js +2 -2
  12. package/dist/brain/route-adjudicator.d.ts +18 -0
  13. package/dist/brain/route-adjudicator.js +16 -0
  14. package/dist/brain/stream-engine.js +2 -1
  15. package/dist/brain/stream-shared.d.ts +3 -0
  16. package/dist/brain/stream-shared.js +1 -0
  17. package/dist/core/agent-definition.d.ts +10 -0
  18. package/dist/core/artifact-host.d.ts +41 -4
  19. package/dist/core/artifact-host.js +18 -2
  20. package/dist/core/ask-origin.d.ts +16 -0
  21. package/dist/core/ask-origin.js +8 -0
  22. package/dist/core/auto-mode-arming.d.ts +6 -0
  23. package/dist/core/auto-mode-arming.js +5 -5
  24. package/dist/core/auto-mode-defaults.d.ts +43 -15
  25. package/dist/core/auto-mode-defaults.js +4 -1
  26. package/dist/core/auto-mode.d.ts +184 -59
  27. package/dist/core/auto-mode.js +188 -13
  28. package/dist/core/checkpoint-store.d.ts +23 -2
  29. package/dist/core/checkpoint-store.js +2 -2
  30. package/dist/core/engine-notice.d.ts +77 -1
  31. package/dist/core/engine-notice.js +7 -0
  32. package/dist/core/gate-fold.js +5 -1
  33. package/dist/core/gate-lanes.js +9 -8
  34. package/dist/core/gate-outcome.d.ts +42 -2
  35. package/dist/core/gate-outcome.js +23 -0
  36. package/dist/core/governance-codes.d.ts +1 -1
  37. package/dist/core/governance-codes.js +14 -0
  38. package/dist/core/hook-seats.d.ts +66 -0
  39. package/dist/core/hook-seats.js +37 -0
  40. package/dist/core/hooks.d.ts +1 -1
  41. package/dist/core/read-root-candidate.d.ts +12 -0
  42. package/dist/core/read-root-candidate.js +24 -0
  43. package/dist/core/roles.d.ts +9 -1
  44. package/dist/core/roles.js +4 -0
  45. package/dist/core/runner/advertised-writable-dirs.d.ts +8 -3
  46. package/dist/core/runner/assemble-result.d.ts +2 -1
  47. package/dist/core/runner/assemble-result.js +1 -1
  48. package/dist/core/runner/classifier-ladder.d.ts +227 -0
  49. package/dist/core/runner/classifier-ladder.js +214 -0
  50. package/dist/core/runner/contracts.d.ts +71 -13
  51. package/dist/core/runner/decide-continuation.d.ts +1 -3
  52. package/dist/core/runner/decide-continuation.js +1 -7
  53. package/dist/core/runner/deferral-census.d.ts +52 -0
  54. package/dist/core/runner/deferral-census.js +44 -0
  55. package/dist/core/runner/denial-limit-arms.js +2 -2
  56. package/dist/core/runner/derived-route-fallback.d.ts +8 -4
  57. package/dist/core/runner/derived-route-fallback.js +8 -3
  58. package/dist/core/runner/gate-exit.d.ts +12 -0
  59. package/dist/core/runner/gate-exit.js +6 -3
  60. package/dist/core/runner/prepare-artifact.d.ts +10 -2
  61. package/dist/core/runner/prepare-artifact.js +5 -4
  62. package/dist/core/runner/prepare-ask-lane.js +1 -0
  63. package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -2
  64. package/dist/core/runner/prepare-caps-and-workflow.js +43 -55
  65. package/dist/core/runner/prepare-config-doors.d.ts +7 -1
  66. package/dist/core/runner/prepare-config-doors.js +13 -13
  67. package/dist/core/runner/prepare-defer-classify.d.ts +13 -19
  68. package/dist/core/runner/prepare-defer-classify.js +8 -3
  69. package/dist/core/runner/prepare-gate-stations.d.ts +26 -0
  70. package/dist/core/runner/prepare-gate-stations.js +1 -1
  71. package/dist/core/runner/prepare-hands-readface.d.ts +24 -7
  72. package/dist/core/runner/prepare-hands-readface.js +60 -24
  73. package/dist/core/runner/prepare-inherited-gate.d.ts +17 -4
  74. package/dist/core/runner/prepare-inherited-gate.js +10 -2
  75. package/dist/core/runner/prepare-lsp.d.ts +2 -2
  76. package/dist/core/runner/prepare-lsp.js +1 -1
  77. package/dist/core/runner/prepare-policy-chain.js +2 -0
  78. package/dist/core/runner/prepare-project-context.d.ts +5 -1
  79. package/dist/core/runner/prepare-project-context.js +7 -4
  80. package/dist/core/runner/prepare-prompt-inputs.d.ts +4 -2
  81. package/dist/core/runner/prepare-prompt-inputs.js +4 -1
  82. package/dist/core/runner/prepare-run-refs.d.ts +9 -1
  83. package/dist/core/runner/prepare-run-refs.js +3 -2
  84. package/dist/core/runner/prepare-suspend-saga.d.ts +5 -0
  85. package/dist/core/runner/prepare-suspend-saga.js +5 -2
  86. package/dist/core/runner/prepare-task.js +19 -14
  87. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +5 -0
  88. package/dist/core/runner/prepare-tool-disclosure-mount.js +10 -2
  89. package/dist/core/runner/prepare-turn-wiring.d.ts +8 -4
  90. package/dist/core/runner/prepare-turn-wiring.js +3 -2
  91. package/dist/core/runner/prepare-wiring-manifest.d.ts +5 -5
  92. package/dist/core/runner/prepare-wiring-manifest.js +5 -2
  93. package/dist/core/runner/resume-apply.js +3 -3
  94. package/dist/core/runner/resume-checkpoint-screen.js +7 -0
  95. package/dist/core/runner/run-harness-handlers.js +4 -1
  96. package/dist/core/runner/run-identity-wiring.js +6 -2
  97. package/dist/core/runner/run-leg.d.ts +2 -0
  98. package/dist/core/runner/run-leg.js +45 -45
  99. package/dist/core/runner/run-notification-lane.js +55 -27
  100. package/dist/core/runner/run-settle-and-teardown.js +1 -0
  101. package/dist/core/runner/run-terminal-adoption.js +16 -13
  102. package/dist/core/runner/run-turn-boundary.js +15 -5
  103. package/dist/core/runner/runtask.d.ts +6 -5
  104. package/dist/core/runner/runtask.js +26 -14
  105. package/dist/core/runner/skill-base-dir.d.ts +54 -0
  106. package/dist/core/runner/skill-base-dir.js +42 -0
  107. package/dist/core/runner/synthetic-tools.d.ts +83 -48
  108. package/dist/core/runner/synthetic-tools.js +69 -32
  109. package/dist/core/runner/tool-defer-gate.d.ts +22 -9
  110. package/dist/core/runner/tool-defer-gate.js +15 -5
  111. package/dist/core/runner/tool-disclosure.d.ts +5 -0
  112. package/dist/core/runner/tool-disclosure.js +2 -0
  113. package/dist/core/runner/tool-progress-lane.d.ts +42 -0
  114. package/dist/core/runner/tool-progress-lane.js +61 -0
  115. package/dist/core/runner/turn-attachments.d.ts +43 -12
  116. package/dist/core/runner/turn-attachments.js +58 -14
  117. package/dist/core/runner-deps.d.ts +48 -7
  118. package/dist/core/sensitive-path-policy.d.ts +58 -17
  119. package/dist/core/sensitive-path-policy.js +33 -5
  120. package/dist/core/skill-listing-budget.d.ts +112 -0
  121. package/dist/core/skill-listing-budget.js +58 -0
  122. package/dist/core/swappable-deps.d.ts +9 -7
  123. package/dist/core/swappable-deps.js +7 -3
  124. package/dist/core/task-event.d.ts +96 -29
  125. package/dist/core/task-notification.d.ts +142 -16
  126. package/dist/core/task-notification.js +106 -0
  127. package/dist/core/task-result.d.ts +9 -1
  128. package/dist/core/task-spec.d.ts +46 -3
  129. package/dist/core/tighten-task-spec.js +7 -5
  130. package/dist/core/tool-model-gate.d.ts +1 -1
  131. package/dist/core/tool-policy.d.ts +19 -4
  132. package/dist/core/tool-policy.js +6 -7
  133. package/dist/core/tool-search-setting.d.ts +78 -0
  134. package/dist/core/tool-search-setting.js +90 -0
  135. package/dist/core/tool-spec.d.ts +45 -4
  136. package/dist/core/types.d.ts +2 -2
  137. package/dist/core/types.js +1 -1
  138. package/dist/core/wiring-manifest.d.ts +46 -0
  139. package/dist/core/wiring-manifest.js +9 -1
  140. package/dist/core/write-protect.d.ts +8 -3
  141. package/dist/core/write-protect.js +8 -5
  142. package/dist/engine/execution-env/node-execution-env.js +10 -4
  143. package/dist/engine/harness/agent-harness.d.ts +42 -0
  144. package/dist/engine/harness/agent-harness.js +61 -29
  145. package/dist/engine/loop/agent-loop.d.ts +68 -3
  146. package/dist/engine/loop/agent-loop.js +109 -16
  147. package/dist/index.d.ts +13 -8
  148. package/dist/index.js +11 -7
  149. package/dist/internal/harness.d.ts +1 -1
  150. package/dist/orchestration/run-workflow-tool.d.ts +3 -0
  151. package/dist/orchestration/run-workflow-tool.js +6 -1
  152. package/dist/orchestration/workflow-governance.d.ts +7 -1
  153. package/dist/orchestration/workflow-governance.js +9 -5
  154. package/dist/orchestration/workflow-primitives.d.ts +3 -0
  155. package/dist/orchestration/workflow-primitives.js +6 -1
  156. package/dist/orchestration/workflow-types.d.ts +48 -13
  157. package/dist/orchestration/workflow-types.js +1 -1
  158. package/dist/orchestration/workflow.js +45 -17
  159. package/dist/prompt-assembly/epoch.js +2 -0
  160. package/dist/prompt-assembly/packs/sema-default.js +2 -2
  161. package/dist/prompt-assembly/section-usage.d.ts +26 -0
  162. package/dist/prompt-assembly/section-usage.js +10 -0
  163. package/dist/prompt-assembly/types.d.ts +8 -6
  164. package/dist/prompts/default.d.ts +13 -0
  165. package/dist/prompts/default.js +16 -8
  166. package/dist/tools/artifact/artifact-text.d.ts +10 -1
  167. package/dist/tools/artifact/artifact-text.js +6 -3
  168. package/dist/tools/artifact/artifact-tool.d.ts +12 -2
  169. package/dist/tools/artifact/artifact-tool.js +203 -70
  170. package/dist/tools/artifact/conformance.d.ts +56 -0
  171. package/dist/tools/artifact/conformance.js +349 -0
  172. package/dist/tools/artifact/local-stub.js +1 -1
  173. package/dist/tools/artifact/reject-breaker.d.ts +28 -0
  174. package/dist/tools/artifact/reject-breaker.js +42 -0
  175. package/dist/tools/fs/bash-lexer.d.ts +132 -0
  176. package/dist/tools/fs/bash-lexer.js +563 -0
  177. package/dist/tools/fs/bash-program-position.d.ts +142 -0
  178. package/dist/tools/fs/bash-program-position.js +125 -0
  179. package/dist/tools/fs/bash-readonly-classifier.d.ts +81 -16
  180. package/dist/tools/fs/bash-readonly-classifier.js +180 -558
  181. package/dist/tools/fs/containment-face.d.ts +89 -0
  182. package/dist/tools/fs/containment-face.js +81 -0
  183. package/dist/tools/fs/fs-bash.d.ts +46 -1
  184. package/dist/tools/fs/fs-bash.js +165 -25
  185. package/dist/tools/fs/fs-read.d.ts +1 -1
  186. package/dist/tools/fs/fs-search-tools.d.ts +3 -3
  187. package/dist/tools/fs/fs-write.d.ts +4 -3
  188. package/dist/tools/fs/fs-write.js +9 -9
  189. package/dist/tools/fs/index.d.ts +24 -5
  190. package/dist/tools/fs/index.js +18 -6
  191. package/dist/tools/fs/read-deny.d.ts +4 -3
  192. package/dist/tools/fs/repo-map.d.ts +2 -2
  193. package/dist/tools/fs/safety.d.ts +18 -3
  194. package/dist/tools/fs/safety.js +11 -4
  195. package/package.json +1 -1
  196. package/test/export-surface.snapshot.json +99 -13
  197. package/wire-changes/core@7.17.7.json +49 -0
  198. package/wire-changes/core@7.18.0.json +1273 -0
  199. package/wire-changes/core@7.19.0.json +457 -0
  200. package/dist/tools/fs/read-face.d.ts +0 -68
  201. package/dist/tools/fs/read-face.js +0 -47
@@ -0,0 +1,214 @@
1
+ import { sameRouteIdentity } from "../../brain/route-adjudicator.js";
2
+ import { thinkingOffSpelled } from "../../brain/reasoning.js";
3
+ import { AUTO_MODE_CLASSIFIER_CONTRACT_BROKEN_AFTER, AUTO_MODE_CLASSIFIER_LOW_REASONING_BUDGET_TOKENS, AUTO_MODE_CLASSIFIER_MAX_TOKENS, AUTO_MODE_CLASSIFIER_THINK_BUDGET_TOKENS } from "../auto-mode-defaults.js";
4
+ import { autoModeClassifierFallbackOf, parseAutoModeResponse } from "../auto-mode.js";
5
+ import { DEFAULT_TIER_ORDER, resolveTaskModel, roleSpecNamesModel } from "../roles.js";
6
+ import { emitTrace } from "../trace.js";
7
+ import { deliverEngineNotice } from "../types.js";
8
+ export function classifierLadderRungs(knob, classifier, main) {
9
+ if (autoModeClassifierFallbackOf(knob) === "none")
10
+ return [classifier];
11
+ return sameRouteQuietly(classifier, main) ? [classifier] : [classifier, main];
12
+ }
13
+ function sameRouteQuietly(a, b) {
14
+ try {
15
+ return sameRouteIdentity(a, b);
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ export const CLASSIFIER_ROLE_DEFAULT_REASONS = ["tier_below_main", "tier_table", "role_fallback", "main_only", "role_unresolvable"];
22
+ export function classifierSeatModel(args) {
23
+ const { deps, main } = args;
24
+ if (roleSpecNamesModel(deps.roles?.classifier)) {
25
+ try {
26
+ return { model: resolveTaskModel({ modelRole: "classifier" }, deps).model };
27
+ }
28
+ catch {
29
+ return { model: main, defaulted: "role_unresolvable" };
30
+ }
31
+ }
32
+ const below = tierBelowMain(deps, main);
33
+ if (below !== undefined)
34
+ return below;
35
+ try {
36
+ const model = resolveTaskModel({ modelRole: "classifier" }, deps).model;
37
+ if (sameRouteQuietly(model, main))
38
+ return { model, defaulted: "main_only" };
39
+ return { model, defaulted: deps.tiers !== undefined && Object.keys(deps.tiers).length > 0 ? "tier_table" : "role_fallback" };
40
+ }
41
+ catch {
42
+ return { model: main, defaulted: "main_only" };
43
+ }
44
+ }
45
+ function tierBelowMain(deps, main) {
46
+ if (deps.tiers === undefined || Object.keys(deps.tiers).length === 0 || deps.models === undefined)
47
+ return undefined;
48
+ const bound = DEFAULT_TIER_ORDER.map((tier) => deps.models?.[tier]);
49
+ let mainTier = -1;
50
+ for (let i = bound.length - 1; i >= 0; i--) {
51
+ const m = bound[i];
52
+ if (m !== undefined && sameRouteQuietly(m, main)) {
53
+ mainTier = i;
54
+ break;
55
+ }
56
+ }
57
+ if (mainTier < 0)
58
+ return undefined;
59
+ for (let i = mainTier + 1; i < bound.length; i++) {
60
+ const m = bound[i];
61
+ if (m !== undefined && !sameRouteQuietly(m, main))
62
+ return { model: m, defaulted: "tier_below_main" };
63
+ }
64
+ return { model: main, defaulted: "main_only" };
65
+ }
66
+ export function announceClassifierRoleDefaulted(args) {
67
+ const { seat } = args;
68
+ if (seat.defaulted === undefined)
69
+ return;
70
+ const spelled = thinkingOffSpelled(seat.model);
71
+ deliverEngineNotice(args.onNotice, {
72
+ code: "classifier.role_defaulted",
73
+ message: `no classifier role names a model — "${seat.model.id}" classifies this run (${seat.defaulted}); ` +
74
+ (spelled ? "its wire can turn thinking off for the verdict. " : "its wire cannot turn thinking off, so each classification carries a thinking allowance. ") +
75
+ "Name a `classifier` role, or declare the model's thinking format (`compat.thinkingFormat`) so the seat can spell off.",
76
+ detail: {
77
+ model: seat.model.id,
78
+ reason: seat.defaulted,
79
+ thinkingOffSpelled: spelled,
80
+ ...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}),
81
+ ...(args.runId !== undefined ? { runId: args.runId } : {}),
82
+ },
83
+ ...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}),
84
+ });
85
+ }
86
+ function classifierFallbackAnnouncer(args) {
87
+ const announced = new Set();
88
+ return (attribution) => {
89
+ const fallback = attribution?.fallback;
90
+ if (fallback === undefined)
91
+ return;
92
+ const key = `${fallback.from}\u0000${fallback.cause}`;
93
+ if (announced.has(key))
94
+ return;
95
+ announced.add(key);
96
+ deliverEngineNotice(args.onNotice, {
97
+ code: "classifier.fallback",
98
+ message: `the auto-mode classifier model "${fallback.from}" did not rule (${fallback.cause}) — this run re-asks each gated call on "${fallback.to}". ` +
99
+ 'Check that the classifier role names a model this deployment\'s gateway serves; set RunnerDeps.autoMode.classifierFallback to "none" to deny instead of falling back.',
100
+ detail: {
101
+ from: fallback.from,
102
+ to: fallback.to,
103
+ cause: fallback.cause,
104
+ ...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}),
105
+ ...(args.runId !== undefined ? { runId: args.runId } : {}),
106
+ },
107
+ ...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}),
108
+ });
109
+ };
110
+ }
111
+ function classifierContractAnnouncer(args) {
112
+ const streak = new Map();
113
+ const announced = new Set();
114
+ const noVerdict = (rung, model) => {
115
+ const key = `${rung}:${model}`;
116
+ const n = (streak.get(key) ?? 0) + 1;
117
+ streak.set(key, n);
118
+ if (n < AUTO_MODE_CLASSIFIER_CONTRACT_BROKEN_AFTER || announced.has(key))
119
+ return;
120
+ announced.add(key);
121
+ deliverEngineNotice(args.onNotice, {
122
+ code: "classifier.contract_broken",
123
+ message: `the auto-mode classifier model "${model}" has answered ${n} consecutive classifications without a verdict (each asked twice) — ` +
124
+ "every gated call it judges is denied. Declare the model's thinking format (`compat.thinkingFormat`) so the seat can turn thinking off, " +
125
+ "or name a `classifier` role whose model follows the <block> contract.",
126
+ detail: {
127
+ model,
128
+ rung,
129
+ consecutive: n,
130
+ ...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}),
131
+ ...(args.runId !== undefined ? { runId: args.runId } : {}),
132
+ },
133
+ ...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}),
134
+ });
135
+ };
136
+ return (info) => {
137
+ const record = info.classifier;
138
+ if (record === undefined)
139
+ return;
140
+ if (record.fallback?.cause === "parse_error")
141
+ noVerdict(0, record.fallback.from);
142
+ const landed = record.fallback !== undefined ? 1 : 0;
143
+ if (info.verdict === "parse_error")
144
+ noVerdict(landed, record.modelUsed);
145
+ else if (info.verdict === "allow" || info.verdict === "block")
146
+ streak.delete(`${landed}:${record.modelUsed}`);
147
+ };
148
+ }
149
+ export const CLASSIFIER_OFF_SEAT = { reasoning: "off", maxTokens: AUTO_MODE_CLASSIFIER_MAX_TOKENS };
150
+ export const CLASSIFIER_LOW_SEAT = { reasoning: "low", maxTokens: AUTO_MODE_CLASSIFIER_MAX_TOKENS + AUTO_MODE_CLASSIFIER_LOW_REASONING_BUDGET_TOKENS };
151
+ export const CLASSIFIER_THINK_SEAT = { reasoning: "low", maxTokens: AUTO_MODE_CLASSIFIER_MAX_TOKENS + AUTO_MODE_CLASSIFIER_THINK_BUDGET_TOKENS };
152
+ export function classifierRungSeat(model) {
153
+ return thinkingOffSpelled(model) ? CLASSIFIER_OFF_SEAT : CLASSIFIER_LOW_SEAT;
154
+ }
155
+ export const CLASSIFIER_ATTEMPT_SUFFIX = {
156
+ first: '\nErr on the side of blocking. Your ENTIRE response MUST begin with <block>. Do NOT output any analysis, reasoning, or commentary before <block>. No "Looking at..." or similar preamble.',
157
+ reask: "\nReview the classification process and follow it carefully, making sure you deny actions that should be blocked. As a reminder, explicit (not suggestive or implicit) user confirmation is required to override blocks. Use <thinking> before responding with <block>. Think longer on ambiguous or borderline actions; keep reasoning brief for clear-cut ones.",
158
+ };
159
+ export function classifierReplyText(reply) {
160
+ return reply.content
161
+ .filter((c) => c.type === "text")
162
+ .map((c) => c.text)
163
+ .join("");
164
+ }
165
+ export function classifierReplyEmptyAtCap(reply) {
166
+ if (classifierReplyText(reply) !== "")
167
+ return false;
168
+ return reply.stopReason === "length" || (reply.stopReason === "error" && reply.errorKind === "length_empty");
169
+ }
170
+ export function classifierReplyTagOnly(reply) {
171
+ return reply.stopReason === "length" || reply.stopReason === "toolUse" || reply.partialFinalized === true || reply.content.some((c) => c.type === "toolCall");
172
+ }
173
+ export function classifierReplyVerdictText(reply) {
174
+ const text = classifierReplyText(reply);
175
+ if (!classifierReplyTagOnly(reply))
176
+ return text;
177
+ const tagged = parseAutoModeResponse(text, { bareAnswer: false });
178
+ return tagged.kind === "parse_error" && tagged.form === "missing" ? "" : text;
179
+ }
180
+ export function classifierReplyAsksAgain(reply) {
181
+ if (classifierReplyEmptyAtCap(reply))
182
+ return true;
183
+ const text = classifierReplyVerdictText(reply);
184
+ if (text === "")
185
+ return classifierReplyTagOnly(reply) && classifierReplyText(reply) !== "";
186
+ const verdict = parseAutoModeResponse(text);
187
+ return verdict.kind === "parse_error" && verdict.form === "missing";
188
+ }
189
+ export function classifierReplyFailure(reply) {
190
+ if (reply.stopReason === "aborted")
191
+ return new Error("the auto-mode classifier request was aborted");
192
+ if (reply.stopReason !== "error" || reply.errorKind === "length_empty")
193
+ return undefined;
194
+ return new Error(`the auto-mode classifier request failed: ${reply.errorMessage ?? "the model leg reported an error with no message"}`);
195
+ }
196
+ export function classifierRoundObserver(args) {
197
+ const announceFallback = classifierFallbackAnnouncer(args);
198
+ const countContract = classifierContractAnnouncer(args);
199
+ return (info) => {
200
+ emitTrace(args.tracer, () => ({
201
+ kind: "auto_mode.classified",
202
+ version: 1,
203
+ taskId: args.taskId,
204
+ toolCallId: info.toolCallId,
205
+ model: info.classifier?.modelUsed ?? args.seatModelId,
206
+ ms: info.ms,
207
+ verdict: info.verdict,
208
+ ...(info.cause !== undefined ? { cause: info.cause } : {}),
209
+ ts: Date.now(),
210
+ }));
211
+ announceFallback(info.classifier);
212
+ countContract(info);
213
+ };
214
+ }
@@ -20,7 +20,7 @@ import type { ActiveWorktreeSession, AgentHarness, AgentMessage, AgentTool, Anno
20
20
  import type { Model } from "../../internal/llm.js";
21
21
  import type { WorkflowSizeGuideline } from "../../orchestration/workflow-size-guideline.js";
22
22
  import type { ToolManifestRow } from "../../prompt-assembly/tool-catalog.js";
23
- import type { CwdRef, ReadFace } from "../../tools/fs/index.js";
23
+ import type { CwdRef, ContainmentFace } from "../../tools/fs/index.js";
24
24
  import type { MaterializedA2a } from "../a2a.js";
25
25
  import type { CompactionForkContext, CompactionPhaseDurations, MaybeCompactOptions, RapidRefillState } from "../auto-compaction.js";
26
26
  import type { AutoModeArmingRecipe } from "../auto-mode-arming.js";
@@ -32,6 +32,7 @@ import type { ClearedProjectionLedger, MicroCompactMachine, OccurrenceIndex } fr
32
32
  import type { HookInvocationIdentity, Hooks, OrgGateVerdict, PermissionDeniedPayload, PostToolBatchCall, PostToolBatchResult, ToolGateInput } from "../hooks.js";
33
33
  import type { MaterializedMcp } from "../mcp.js";
34
34
  import type { MemoryEngine } from "../memory-engine/engine.js";
35
+ import type { ResolvedSkillListingBudget } from "../skill-listing-budget.js";
35
36
  import type { HarvestReport, MemorySessionHandle } from "../memory-engine/types.js";
36
37
  import type { SessionPermissionRules } from "../session-policy-store.js";
37
38
  import type { RecoveredOrphan } from "../session-reconcile.js";
@@ -43,7 +44,7 @@ import type { SystemInjectionPriority, TaskNotificationPayload } from "../task-n
43
44
  import type { OnAsk, PermissionResult, ResolvedAsk, ToolCallRequest, ToolPolicy } from "../tool-policy.js";
44
45
  import type { RemoteExecutionEnv, SnapshotId, WorkspaceHandle } from "../remote-env.js";
45
46
  import type { ToolDisclosureManifest } from "../trace.js";
46
- import type { AgentDefinition, Brain, CompactOutcome, ModelRoles, NestedUsageAccum, RunnerDeps, ShellGateDoctrine, TaskEvent, TaskResult, TaskSpec, TaskStream, ToolActivity, ToolEffect } from "../types.js";
47
+ import type { AgentDefinition, Brain, CompactOutcome, ModelRoles, NestedUsageAccum, RunnerDeps, ShellGateDoctrine, TaskEvent, TaskEventIdentity, TaskResult, TaskSpec, TaskStream, ToolActivity, ToolEffect } from "../types.js";
47
48
  import type { WiringManifest } from "../wiring-manifest.js";
48
49
  import type { ActiveSkillFrame } from "./active-skill-scope.js";
49
50
  import type { BudgetAxis } from "./assemble-result.js";
@@ -157,6 +158,16 @@ export interface FileHistoryBoundarySeat {
157
158
  * may no longer match), and the id of the tool call the park is holding (only a `tool_approval` pending
158
159
  * action parks ON a call; absent, never guessed, for the kinds that hold none).
159
160
  */
161
+ /** See {@link Prepared.undeliveredContinuationRef} — `parkedSteers` is in ISSUE order, and the wake message rides it with no `seq` (no store minted one). */
162
+ export interface UndeliveredContinuation {
163
+ parkedSteers: Array<Omit<PendingSteerEntry, "seq"> & {
164
+ seq?: number;
165
+ }>;
166
+ orphanTasks: Array<{
167
+ id: string;
168
+ description?: string;
169
+ }>;
170
+ }
160
171
  export interface CommittedPause {
161
172
  cause: import("../types.js").PausedCause;
162
173
  scope: string;
@@ -278,7 +289,9 @@ export interface Prepared {
278
289
  * block's single resolution, or the hands-less legs' resolver run (live spec-time facts, with the
279
290
  * checkpoint seed folded stricter-wins where one exists). Every leg that completes prepare has a
280
291
  * read posture now — this is what its delegation subtree is clamped by even where no faces mount. */
281
- effectiveReadFace?: ReadFace;
292
+ effectiveReadFace?: ContainmentFace;
293
+ /** owned — the same observation on the WRITE seat (`TaskResult.effectiveWriteFace`). */
294
+ effectiveWriteFace?: ContainmentFace;
282
295
  /** The normalized deny ADDITIONS in force (deployment ∪ task ∪ checkpoint seed; built-ins
283
296
  * excluded), echoed on `TaskResult.effectiveReadDenyPatterns`. Present iff non-empty; a defensive
284
297
  * copy (the wide-scope working array stays the engine's own). */
@@ -291,6 +304,13 @@ export interface Prepared {
291
304
  /** The per-task logical cwd ref when a real shell is mounted (else undefined). The Runner
292
305
  * reads `cwdRef.current` after each tool to detect a `cd` move and emit `workspace_changed`. */
293
306
  cwdRef?: CwdRef;
307
+ /** The run's TOOL-PROGRESS lane seat, late-bound for the same reason `harnessRef` is (the tool band is
308
+ * built during prepare; the lane needs `pushContent` and `ident`, which exist only after it returns).
309
+ * The driver binds `current` right after `pushContent` exists. Always present on a completed prepare —
310
+ * the CELL, not the lane: unbound means "no run body has claimed this band", and emissions no-op. */
311
+ toolProgressRef: {
312
+ current?: ToolProgressLane;
313
+ };
294
314
  /** The run's advertised-writable-directory seat (present with hands): `roots` is what the prompt advertised as writable and the fence admitted. Read live wherever a policy is adjudicated off a resumed leg, so the write gate's exemption domain is that same list. */
295
315
  advertisedWritableDirs?: Pick<import("./advertised-writable-dirs.js").AdvertisedWritableDirs, "roots">;
296
316
  /** The ACTIVE EnterWorktree session ref (mounted with the worktree tools; undefined without
@@ -462,6 +482,7 @@ export interface Prepared {
462
482
  * "undiscovered" census the tool_search_usage_reminder lane enumerates. Frozen at prepare time: a
463
483
  * name never leaves the deferred set, it only moves into `activeTools`. */
464
484
  deferredToolNames?: ReadonlySet<string>;
485
+ /** The deferral census seat, present iff {@link deferredToolNames} is. */ toolDisclosure?: import("./deferral-census.js").ToolDisclosureSeat;
465
486
  /** The RESOLVED materialization strategy this run runs under (`true` = "static":
466
487
  * activation never swaps the placeholder; the real schema's only in-context carrier is the
467
488
  * ToolSearch result text). Consumed by the tools_delta boundary renderer, whose "full schemas are
@@ -587,6 +608,13 @@ export interface Prepared {
587
608
  pausedRef: {
588
609
  current?: CommittedPause;
589
610
  };
611
+ /** A resumed leg's continuation content that has NOT reached the transcript yet: EVERY input frame the
612
+ * prompt carries (parked steers, the caller's wake message) and the orphaned background tasks whose obituary
613
+ * rides it. Set before the prompt, emptied at delivery; a re-park carries it forward (the saga folds the
614
+ * orphans in, the adoption re-parks the frames), and what no re-park carries the undrained account announces. */
615
+ undeliveredContinuationRef: {
616
+ current: UndeliveredContinuation;
617
+ };
590
618
  /** Ruled 2026-08-05 (matrix ruling arm A): set true by the resume engine when this leg EXECUTES the
591
619
  * approved pending call — the restart-loop cap then counts from a fresh base (consecutive
592
620
  * no-progress suspends only). See `suspendChainBase`. */
@@ -997,6 +1025,7 @@ export interface Prepared {
997
1025
  skillsListing?: {
998
1026
  entries: ReadonlyArray<SkillListingEntry>;
999
1027
  seedAnnounced: boolean;
1028
+ budget: ResolvedSkillListingBudget;
1000
1029
  };
1001
1030
  /** [c209-C] Q5 — the run loop's live mirror of the listing frames' announced NAME-SETS (updated at
1002
1031
  * every commit point: first-frame delivery, intact boundary survival, resume seed). Read by the
@@ -1345,6 +1374,7 @@ export interface PromptFeatureFlags {
1345
1374
  isolationEnabled: boolean;
1346
1375
  reminderMark: string;
1347
1376
  readFaceOpen: boolean;
1377
+ writeFaceOpen: boolean;
1348
1378
  orchestrationEnabled: boolean;
1349
1379
  orchestrationDeferred: boolean;
1350
1380
  promptProfile: "simple" | "classic";
@@ -1782,10 +1812,6 @@ export interface RunInternals {
1782
1812
  onStatusEvent?: (event: Extract<TaskEvent, {
1783
1813
  type: "status";
1784
1814
  }>) => void;
1785
- /**
1786
- * Trusted run-local system-injection sink. `Runner.runLocked` wires this to the
1787
- * live TaskStream queue plus the current harness follow-up lane; it is not a public TaskSpec field.
1788
- */
1789
1815
  /** The run-local per-tool-call detach hub. runtask creates it and exposes
1790
1816
  * `TaskStream.detach(toolCallId)`; the hands Bash tool threads `signalFor(toolCallId)` into env.exec. */
1791
1817
  detachHub?: import("../tool-detach.js").ToolDetachHub;
@@ -1797,6 +1823,11 @@ export interface RunInternals {
1797
1823
  * channel, same posture as every other field here.
1798
1824
  */
1799
1825
  sessionReadStates?: SessionReadFileStates;
1826
+ /** Trusted system-injection sink (`Runner.runLocked` wires it to the live TaskStream queue plus the
1827
+ * current harness lanes; never a public TaskSpec field), ADDRESSED TO THE SESSION rather than to this
1828
+ * run: a producer wired here (a background shell, a watch, a delegation settle) keeps the closure long
1829
+ * after the run that mounted it ended, and its frame is then routed to whatever run the SESSION has
1830
+ * live — at that run's next turn boundary, the seat a steer lands on; only a session with NO live run parks it. */
1800
1831
  onTaskNotification?: (notification: TaskNotificationPayload,
1801
1832
  /** Injection tier (the ladder is LIVE): "next" = the running turn's next boundary
1802
1833
  * (arrival order, consecutive frames batch); "later" = the run's would-otherwise-stop seat
@@ -1812,9 +1843,9 @@ export interface RunInternals {
1812
1843
  * `injectTaskNotification`), threaded into a CHILD's internals at spawn time so the child's
1813
1844
  * SendMessage("main") uplink lands in the parent's queue at a turn boundary (the CC
1814
1845
  * "delivered automatically" parent half). DELIBERATELY a separate field from
1815
- * {@link RunInternals.onTaskNotification}: that one is "inject into THIS run" (runtask wraps it
1816
- * as upstream-observer + own-queue), and reusing it for the child would tee every internal child
1817
- * notification (grandchild completions, monitor events) into the parent — double delivery.
1846
+ * {@link RunInternals.onTaskNotification}: that one is "inject into the run's own SESSION" (runtask
1847
+ * wraps it as upstream-observer + session route), and reusing it for the child would tee every
1848
+ * internal child notification (grandchild completions, monitor events) into the parent — double delivery.
1818
1849
  */
1819
1850
  parentNotify?: (notification: TaskNotificationPayload, opts?: {
1820
1851
  priority?: import("../task-notification.js").SystemInjectionPriority;
@@ -1831,9 +1862,9 @@ export interface RunInternals {
1831
1862
  * live (runtask calls it with its wrapped `injectTaskNotification`). The spawner stores it on the
1832
1863
  * child's registry handle so a parent/sibling SendMessage can deliver TO the RUNNING child at its
1833
1864
  * next turn boundary (CC's in-memory `pendingMessages` pedestal, anchors/2.1.212
1834
- * messaging-runtime.md §2.5). The injector is RUN-SCOPED but fail-safe after the run: the lane's
1835
- * teardown branch parks late payloads per session (PendingSessionNotifications), which is exactly
1836
- * CC's durable-mailbox posture for an idle teammate (§2.4) — no separate file mailbox needed.
1865
+ * messaging-runtime.md §2.5). The injector outlives its run safely: once this lane is over the frame
1866
+ * is routed to whatever run the SESSION has live, and only a session with no live run parks it
1867
+ * (PendingSessionNotifications) — exactly CC's durable-mailbox posture for an idle teammate (§2.4).
1837
1868
  * TRUSTED chain (core spawner only); never reachable from TaskSpec.
1838
1869
  */
1839
1870
  onNotifyInjectorReady?: (inject: (notification: TaskNotificationPayload, opts?: {
@@ -2360,6 +2391,30 @@ export interface TurnBoundaryDeps {
2360
2391
  /** R5(harness 事件处理器族)的依赖包。全部为 runLocked 内声明顺序早于
2361
2392
  * 工厂调用点的 const 稳定引用 + 三个 runLocked 形参;this 面(deps.onError)经 runnerHooks 打包
2362
2393
  * (B13 #coreView / R9 TurnBoundaryDeps 同款)。*/
2394
+ /** What a PRODUCER of a `tool_progress` frame knows: the FACTS it observed. The frame minus everything only
2395
+ * the run supplies — its discriminator, its {@link TaskEventIdentity} stamp, and the elapsed time, which the
2396
+ * lane computes from its own per-call origin. Derived from the union member, so seat and frame cannot drift. */
2397
+ export type ToolProgressBody = Omit<Extract<TaskEvent, {
2398
+ type: "tool_progress";
2399
+ }>, "type" | "elapsedTimeSeconds" | keyof TaskEventIdentity>;
2400
+ /** The run's TOOL-PROGRESS lane: the ONE mint of `tool_progress` plus the heartbeat clock over the calls
2401
+ * in flight. Declared on the FLOOR because both registry-side readers sit above its implementation (the
2402
+ * driver builds it, the harness-handler lane drives it) and the Prepared seat that carries it to the tool
2403
+ * band is here too. `createToolProgressLane` (tool-progress-lane.ts, layer 1) is its one implementation. */
2404
+ export interface ToolProgressLane {
2405
+ /** Offer ONE frame — from a tool that observes its own progress (the shell's per-second tick) or from
2406
+ * the lane's own heartbeat. The lane stamps the elapsed time, DROPS the offer when it would land inside
2407
+ * the cadence floor, and resets that call's beat clock when it does not. NEVER THROWS: a tool calls this
2408
+ * from inside an environment output callback, where a throw kills the command it is describing. */
2409
+ emit(body: ToolProgressBody): void;
2410
+ /** A tool call entered execution: it joins the heartbeat's in-flight set and its clock starts, stamped
2411
+ * by the LANE (one clock owns one number — see its module header on why that clock is monotonic). */
2412
+ noteStart(toolCallId: string, toolName: string): void;
2413
+ /** A tool call settled: it leaves the in-flight set. */
2414
+ noteEnd(toolCallId: string): void;
2415
+ /** Drop every in-flight call and disarm the timer (turn end / teardown): no beating into the next turn. */
2416
+ clear(): void;
2417
+ }
2363
2418
  export interface HarnessHandlersDeps {
2364
2419
  spec: TaskSpec;
2365
2420
  queue: PushQueue<TaskEvent>;
@@ -2375,6 +2430,9 @@ export interface HarnessHandlersDeps {
2375
2430
  emitCommitted: (entryId: string, role: "user" | "assistant" | "toolResult", toolCallId?: string) => void;
2376
2431
  startedToolCallIds: Set<string>;
2377
2432
  toolStartAt: Map<string, number>;
2433
+ /** The run's tool-progress lane: the handlers tell it when a call enters and leaves execution, and
2434
+ * clear it with the rest of the per-call bookkeeping at turn end. */
2435
+ toolProgress: ToolProgressLane;
2378
2436
  writeFamilyOf: (name: string) => WriteFamily | undefined;
2379
2437
  toolLabels: Map<string, string>;
2380
2438
  postToolBatchHook: ((batch: PostToolBatchCall[], meta?: {
@@ -7,7 +7,7 @@
7
7
  * · which call a CONTENT-ASK checkpoint parks on, and the one-shot answering face a decide's answer is
8
8
  * bound to for the redeemed call;
9
9
  * · the capture-time domain checks on a decide's TEXT payloads, and the label a rejected value gets;
10
- * · the model-facing text for a deferred batch sibling, and the continuation prompt itself.
10
+ * · the continuation prompt itself.
11
11
  *
12
12
  * Pure over its arguments (the answering face is the one closure, and it closes over its own arguments
13
13
  * only). Run-loop machinery (layer 1), named without the `resume-` family prefix: that prefix marks a
@@ -25,8 +25,6 @@ import type { ResumeRun } from "./contracts.js";
25
25
  * reported the same decidePath locked no matter how many times the human decided).
26
26
  */
27
27
  export declare function resumeDecisionWasNegative(resume: ResumeRun): boolean;
28
- /** Model-facing result for a batch sibling of a suspended call that v1 does not blind-run (§4.ter). */
29
- export declare const DEFERRED_REISSUE: string;
30
28
  /**
31
29
  * Structural deep equality of two JSON-serializable values (order-independent over object keys). Used ONLY
32
30
  * by the Runner's `resumeStream` reopen-by-reason guard to confirm an `env_failed` re-resume
@@ -7,8 +7,6 @@ export function resumeDecisionWasNegative(resume) {
7
7
  const o = resume.outcome;
8
8
  return o.decision === "reject" || o.decision === "deny";
9
9
  }
10
- export const DEFERRED_REISSUE = "[DEFERRED] This tool call shared a batch with a call that suspended for durable approval, so it was " +
11
- "NOT executed on resume. If you still need it, issue it again now.";
12
10
  export function deepJsonEqual(a, b) {
13
11
  if (a === b)
14
12
  return true;
@@ -122,11 +120,7 @@ export function resumeContinuation(resume, mark) {
122
120
  const decided = resume.outcome.decision === "allow"
123
121
  ? "was APPROVED and has now been executed — its result is in the tool results above"
124
122
  : `was DENIED${resume.outcome.reason ? `: ${delimitUntrusted("reviewer note", resume.outcome.reason, REVIEWER_NOTE_MAX_BODY)}` : ""}`;
125
- const remaining = pendingAction.batchToolCallIds.filter((id) => id !== pendingAction.toolCallId && !pendingAction.completedCallIds.includes(id)).length;
126
- const batchNote = remaining > 0
127
- ? ` ${remaining} other tool call(s) from that batch were returned as deferred and not run — re-issue any you still need.`
128
- : "";
129
- return formatHookFeedback(`The pending tool call "${pendingAction.toolName}" ${decided}.${batchNote} ` +
123
+ return formatHookFeedback(`The pending tool call "${pendingAction.toolName}" ${decided}. ` +
130
124
  `This is a RESUMED task — every step before this point is already done and its results are in the ` +
131
125
  `conversation above. Do NOT restart the task or re-run any tool you already ran; continue from this ` +
132
126
  `exact point, building on the existing results, and finish the remaining work.`, mark);
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The deferral CENSUS: what a leg discloses about which of its tools ship as name-only placeholders, and the
3
+ * seat that carries it to the stream. Split out of the disclosure mount because it is a different concern —
4
+ * the mount decides the request FACE, this decides what is SAID about it.
5
+ */
6
+ import type { ToolSearchPolicy, ResolvedToolSearch } from "../tool-search-setting.js";
7
+ /**
8
+ * The HOST-facing census of this leg's deferral: which tool names ship as name-only placeholders, which the model has activated, and the policy that decided it.
9
+ * @contract tool_disclosure.snapshot — the `tool_disclosure` frame is minted ONLY here ({@link ToolDisclosureSeat.publish}), exists ONLY on a leg that defers
10
+ * at least one tool, and carries the WHOLE census each time (a consumer replaces its copy; the "what just happened" delta is this snapshot's `activated` minus
11
+ * the previous one's). `deferred` is frozen at prepare (a name never leaves it, it only appears in `activated`); both lists are sorted, so two snapshots
12
+ * compare by value. A leg that defers nothing emits no frame at all — absence is "nothing was deferred", never "the disclosure is off".
13
+ */
14
+ export interface ToolDisclosureSnapshot {
15
+ /** Which arm of `RunnerDeps.toolSearch` decided rule ② on this leg. */
16
+ policy: ToolSearchPolicy;
17
+ /** Present iff `policy === "auto"`: the percent of the window the candidate set had to reach. */
18
+ thresholdPercent?: number;
19
+ /** Every deferred name on this leg, sorted. Non-empty (the frame does not exist otherwise). */
20
+ deferred: readonly string[];
21
+ /** The deferred names ACTIVE on this leg right now, sorted — the engine's state, not a record of commits. Empty on the leg-start census only when nothing
22
+ * seeded it; a RESUMED leg carries whatever the engine reseeded (transcript replay, compaction snapshot, checkpoint) before anything executed. */
23
+ activated: readonly string[];
24
+ }
25
+ /**
26
+ * The run-time seat the frame rides: prepare publishes the leg-start census, every activation that changes it publishes the next, and the run's event wiring
27
+ * subscribes once. Snapshots published before the sink is installed are buffered (bounded) and flushed in order, so the leg-start census can be minted at
28
+ * prepare and still arrive after the manifest frame it belongs beside. Deliberately the same shape as `ToolRosterDeltaSeat` — the two are one problem.
29
+ */
30
+ export declare class ToolDisclosureSeat {
31
+ #private;
32
+ /** Publish a census. A snapshot equal to the last published one is DROPPED: `rematerialize` also runs on an MCP refresh, and a frame that says nothing
33
+ * changed reads as an activation that did not happen. */
34
+ publish(snapshot: ToolDisclosureSnapshot): void;
35
+ /** Install the stream sink; anything published before is flushed in order. */
36
+ subscribe(sink: (snapshot: ToolDisclosureSnapshot) => void): void;
37
+ /** The leg ended: drop the sink so a late publish never reaches a closed stream (it buffers, unread). */
38
+ unsubscribe(): void;
39
+ }
40
+ /**
41
+ * Mint a leg's census producer: the frozen deferred list, the policy it was classified under, and a seat.
42
+ * One place holds both halves, so a census can never name a policy the classification did not run under.
43
+ *
44
+ * `deferred` is frozen because every census shares the one array — a host that mutated the list it was handed
45
+ * would be editing every later frame of the leg. `activated` is filtered to the DEFERRED names: the live
46
+ * activation set is seeded from the transcript and a resume checkpoint, both of which can name an undeferred
47
+ * tool.
48
+ */
49
+ export declare function createDeferralCensus(deferred: ReadonlySet<string>, policy: ResolvedToolSearch): {
50
+ seat: ToolDisclosureSeat;
51
+ publish: (active: ReadonlySet<string>) => void;
52
+ };
@@ -0,0 +1,44 @@
1
+ export class ToolDisclosureSeat {
2
+ #pending = [];
3
+ #sink;
4
+ #last;
5
+ publish(snapshot) {
6
+ if (this.#last !== undefined && sameDisclosure(this.#last, snapshot))
7
+ return;
8
+ this.#last = snapshot;
9
+ if (this.#sink !== undefined)
10
+ this.#sink(snapshot);
11
+ else {
12
+ this.#pending.push(snapshot);
13
+ if (this.#pending.length > 64)
14
+ this.#pending.shift();
15
+ }
16
+ }
17
+ subscribe(sink) {
18
+ this.#sink = sink;
19
+ const pending = this.#pending;
20
+ this.#pending = [];
21
+ for (const s of pending)
22
+ sink(s);
23
+ }
24
+ unsubscribe() {
25
+ this.#sink = undefined;
26
+ }
27
+ }
28
+ function sameDisclosure(a, b) {
29
+ const sameList = (x, y) => x.length === y.length && x.every((n, i) => n === y[i]);
30
+ return a.policy === b.policy && a.thresholdPercent === b.thresholdPercent && sameList(a.deferred, b.deferred) && sameList(a.activated, b.activated);
31
+ }
32
+ export function createDeferralCensus(deferred, policy) {
33
+ const seat = new ToolDisclosureSeat();
34
+ const names = Object.freeze([...deferred].sort());
35
+ return {
36
+ seat,
37
+ publish: (active) => seat.publish({
38
+ policy: policy.policy,
39
+ ...(policy.thresholdPercent !== undefined ? { thresholdPercent: policy.thresholdPercent } : {}),
40
+ deferred: names,
41
+ activated: [...active].filter((n) => deferred.has(n)).sort(),
42
+ }),
43
+ };
44
+ }
@@ -1,4 +1,4 @@
1
- import { CLASSIFIER_PARSE_FAILURE_DENY_MESSAGE, classifierUnavailableDenyMessage, createAutoModeDenialTracker, denialLimitFallbackMessage, denialLimitSentence, unarmedWindow } from "../auto-mode.js";
1
+ import { classifierParseFailureDenyMessage, classifierUnavailableDenyMessage, createAutoModeDenialTracker, denialLimitFallbackMessage, denialLimitSentence, unarmedWindow } from "../auto-mode.js";
2
2
  import { sanitizeAutoModeArmingRecipe, tightenDenialLimit } from "../auto-mode-arming.js";
3
3
  import { attestClassifierDenyCause } from "../tool-policy.js";
4
4
  import { deliverEngineNotice } from "../types.js";
@@ -92,7 +92,7 @@ export async function judgeInheritedClassifier(opts) {
92
92
  result: attestClassifierDenyCause({ action: "deny", message: classifierUnavailableDenyMessage(req.toolName, verdict.cause), decisionReason: "classifier" }, "unavailable", req),
93
93
  };
94
94
  }
95
- const reason = verdict.kind === "block" ? (verdict.reason ? inlineUntrusted(verdict.reason) : "") : CLASSIFIER_PARSE_FAILURE_DENY_MESSAGE;
95
+ const reason = verdict.kind === "block" ? (verdict.reason ? inlineUntrusted(verdict.reason) : "") : classifierParseFailureDenyMessage(verdict.classifier);
96
96
  const category = verdict.kind === "block" && verdict.category ? inlineUntrusted(verdict.category) : "";
97
97
  const tracked = autoMode.denialTracking?.recordBlock();
98
98
  if (tracked?.limitReached !== true) {
@@ -2,15 +2,15 @@
2
2
  * The derived-leg route pre-flight shared by prepareTask's two derived seats: the compaction-summary model (the
3
3
  * driver's own seat, resolved before the offload prelude) and the auto-mode classifier model (the caps-and-workflow
4
4
  * phase's arming). Moved whole out of the orchestrator so the phase reaches DOWN for it instead of the two callers
5
- * spelling it twice. The judgment law itself lives in the brain's route adjudicator; this is the one guarded seat
6
- * around it.
5
+ * spelling it twice. The judgment law and both announcements live in the brain's route adjudicator; this is the one
6
+ * guarded seat around them.
7
7
  */
8
8
  import type { Model } from "../../internal/llm.js";
9
9
  import type { RunnerDeps, TaskSpec } from "../types.js";
10
10
  /**
11
11
  * The derived-leg pairing pre-flight as ONE guarded seat: judge the derived model where its route
12
- * differs from the primary's, announce the fallback on a broken pairing, and ABSTAIN (keep the
13
- * derived model) on ANY throw. The pre-flight is advisory — the brain's request gate re-runs the
12
+ * differs from the primary's, announce what a broken pairing means for THIS seat (`onBrokenPairing` —
13
+ * one judgment, and the POSTURE is the deployment's), and ABSTAIN (keep the derived model) on ANY throw. The pre-flight is advisory — the brain's request gate re-runs the
14
14
  * same law — so neither a throwing judge face nor a garbage catalog value (the route-identity
15
15
  * normalization throws a TypeError on a non-string baseUrl) may widen into "task preparation
16
16
  * failed": at the compaction seat the session is already acquired and the throw-cleanup contract
@@ -31,4 +31,8 @@ export declare function derivedRouteFallsBack(args: {
31
31
  /** The INVOCATION this seat resolution belongs to. The session cannot stand in for it: two
32
32
  * runs of one session that both fall back on the same seat mint otherwise byte-identical lines. */
33
33
  runId?: string;
34
+ /** What this seat's DEPLOYMENT asked for on a broken pairing. Absent / `"fall_back"`: swap and announce `route.fallback_to_primary`
35
+ * (the only posture a seat with no knob of its own has). `"refuse"`: keep the seat and announce `route.classifier_unroutable` — never
36
+ * quietly granting a fallback the deployment declined (`autoMode.classifierFallback: "none"`); announcing is why no swap is not no word. */
37
+ onBrokenPairing?: "fall_back" | "refuse";
34
38
  }): Promise<boolean>;
@@ -1,4 +1,4 @@
1
- import { adjudicateDerivedRoute, fallbackToPrimaryNotice, sameRouteIdentity } from "../../brain/route-adjudicator.js";
1
+ import { adjudicateDerivedRoute, fallbackToPrimaryNotice, sameRouteIdentity, unroutableSeatNotice } from "../../brain/route-adjudicator.js";
2
2
  import { deliverEngineNotice } from "../types.js";
3
3
  export async function derivedRouteFallsBack(args) {
4
4
  try {
@@ -7,8 +7,13 @@ export async function derivedRouteFallsBack(args) {
7
7
  const verdict = await adjudicateDerivedRoute({ brain: args.brain, model: args.derived, getApiKeyAndHeaders: args.getApiKeyAndHeaders });
8
8
  if (verdict === undefined || verdict.ok)
9
9
  return false;
10
- deliverEngineNotice(args.onNotice, fallbackToPrimaryNotice({ seat: args.seat, from: args.derived.id, to: args.primary.id, verdict, ...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}), ...(args.runId !== undefined ? { runId: args.runId } : {}) }));
11
- return true;
10
+ const shared = { seat: args.seat, from: args.derived.id, verdict, ...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}), ...(args.runId !== undefined ? { runId: args.runId } : {}) };
11
+ const refuse = args.onBrokenPairing === "refuse";
12
+ if (refuse)
13
+ deliverEngineNotice(args.onNotice, unroutableSeatNotice({ ...shared, primary: args.primary.id }));
14
+ else
15
+ deliverEngineNotice(args.onNotice, fallbackToPrimaryNotice({ ...shared, to: args.primary.id }));
16
+ return !refuse;
12
17
  }
13
18
  catch {
14
19
  return false;