@sema-agent/core 7.35.1 → 7.36.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 (71) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/dist/brain/anthropic.js +6 -6
  3. package/dist/brain/errors.d.ts +1 -14
  4. package/dist/brain/errors.js +2 -6
  5. package/dist/brain/open-responses.js +7 -7
  6. package/dist/brain/openai.js +7 -7
  7. package/dist/brain/provider-echo-shape.d.ts +26 -0
  8. package/dist/brain/provider-echo-shape.js +12 -0
  9. package/dist/brain/stream-engine.d.ts +2 -1
  10. package/dist/brain/stream-engine.js +28 -14
  11. package/dist/brain/tool-call-id.d.ts +27 -15
  12. package/dist/brain/tool-call-id.js +43 -0
  13. package/dist/core/ask-question.d.ts +0 -30
  14. package/dist/core/ask-question.js +3 -2
  15. package/dist/core/checkpoint-store.d.ts +5 -3
  16. package/dist/core/checkpoint-store.js +2 -1
  17. package/dist/core/context-edit.d.ts +4 -0
  18. package/dist/core/context-edit.js +5 -2
  19. package/dist/core/engine-notice.d.ts +7 -0
  20. package/dist/core/gate-lanes.js +12 -10
  21. package/dist/core/governance-codes.d.ts +1 -1
  22. package/dist/core/governance-codes.js +2 -0
  23. package/dist/core/hooks.d.ts +3 -0
  24. package/dist/core/hooks.js +4 -2
  25. package/dist/core/mode-policy.js +2 -1
  26. package/dist/core/permission-rule-model.d.ts +2 -2
  27. package/dist/core/permission-rule-model.js +2 -2
  28. package/dist/core/permission-rules.js +2 -5
  29. package/dist/core/runner/active-skill-scope.js +5 -5
  30. package/dist/core/runner/decide-continuation.js +2 -1
  31. package/dist/core/runner/permission-rule-lanes.d.ts +3 -0
  32. package/dist/core/runner/permission-rule-lanes.js +7 -6
  33. package/dist/core/runner/prepare-context-lane.d.ts +4 -0
  34. package/dist/core/runner/prepare-context-lane.js +7 -5
  35. package/dist/core/runner/prepare-gate-stations.js +17 -12
  36. package/dist/core/runner/prepare-park-ask.d.ts +2 -0
  37. package/dist/core/runner/prepare-park-ask.js +7 -6
  38. package/dist/core/runner/prepare-policy-chain.d.ts +2 -0
  39. package/dist/core/runner/prepare-policy-chain.js +10 -9
  40. package/dist/core/runner/prepare-safety-scan.js +9 -2
  41. package/dist/core/runner/prepare-task.js +3 -3
  42. package/dist/core/runner/resume-apply.js +2 -1
  43. package/dist/core/runner/run-attachment-seats.js +3 -1
  44. package/dist/core/runner/run-harness-handlers.js +6 -1
  45. package/dist/core/runner/run-leg.js +11 -9
  46. package/dist/core/runner/run-recovery-lanes.js +2 -0
  47. package/dist/core/runner/session-rule-policy.js +5 -4
  48. package/dist/core/runner/tool-call-id-fold-seat.d.ts +13 -0
  49. package/dist/core/runner/tool-call-id-fold-seat.js +31 -0
  50. package/dist/core/sensitive-path-policy.js +2 -2
  51. package/dist/core/side-query.d.ts +5 -1
  52. package/dist/core/side-query.js +5 -1
  53. package/dist/core/task-event.d.ts +2 -1
  54. package/dist/core/tool-name-readings.d.ts +366 -0
  55. package/dist/core/tool-name-readings.js +136 -0
  56. package/dist/core/tool-policy.d.ts +5 -5
  57. package/dist/core/tool-policy.js +15 -13
  58. package/dist/core/tool-registry.d.ts +1 -31
  59. package/dist/core/tool-registry.js +2 -22
  60. package/dist/core/tool-result-budget.d.ts +3 -0
  61. package/dist/core/tool-result-budget.js +5 -3
  62. package/dist/core/tool-roster.d.ts +30 -9
  63. package/dist/core/tool-roster.js +45 -5
  64. package/dist/engine/llm/types.d.ts +4 -0
  65. package/dist/tools/fs/file-read-check.d.ts +1 -1
  66. package/dist/tools/fs/file-read-check.js +4 -3
  67. package/dist/tools/fs/shell-read-check.d.ts +15 -1
  68. package/dist/tools/fs/shell-read-check.js +5 -2
  69. package/package.json +2 -1
  70. package/wire-changes/core@7.35.2.json +61 -0
  71. package/wire-changes/core@7.36.0.json +288 -0
@@ -5,7 +5,7 @@ import { defineTool, errorResult } from "./tools.js";
5
5
  import { delimitUntrusted, inlineUntrusted } from "./untrusted-text.js";
6
6
  import { toolFace } from "./tool-catalog-entries.js";
7
7
  import { thrownText } from "./thrown-value.js";
8
- import { callAnswersTo } from "./tool-roster.js";
8
+ import { heldBy } from "./tool-name-readings.js";
9
9
  export const ASK_USER_QUESTION_TOOL_NAME = "AskUserQuestion";
10
10
  export function classifyQuestionOutcome(outcome) {
11
11
  let unavailable;
@@ -132,10 +132,11 @@ export function validateAskQuestions(questions) {
132
132
  }
133
133
  return undefined;
134
134
  }
135
+ const QUESTION_TOOL_NAMES = new Set([ASK_USER_QUESTION_TOOL_NAME]);
135
136
  export function createDurableQuestionPolicy() {
136
137
  return {
137
138
  check(req) {
138
- if (!callAnswersTo(req, ASK_USER_QUESTION_TOOL_NAME)) {
139
+ if (!heldBy("policy.durableQuestion", QUESTION_TOOL_NAMES, req)) {
139
140
  return { action: "allow" };
140
141
  }
141
142
  return { action: "ask", message: "AskUserQuestion: awaiting a human answer (durable)" };
@@ -868,9 +868,11 @@ export type PendingAction = {
868
868
  */
869
869
  family?: import("./tool-face.js").ToolFamily;
870
870
  pathTarget?: import("./tool-face.js").ToolRosterPathTarget;
871
- /** The names a rule may spell the parked tool by (`ToolCallFace.names`: the wire name, then the aliases that resolve
872
- * to it) off the same roster row at the same mint — the pre-CAS recheck judges an edit against rules written on
873
- * either name, as the live gate judged the original. Absent on a row minted before it ⇒ the row's `toolName` alone. */
871
+ /** The names the parked call answered to, copied from the face the gate judged it under (`ToolCallFace.names`: the
872
+ * model's spelling first, then the wire name and the other aliases that resolve to it; a row minted before the
873
+ * spelling led carries the wire name first) — the pre-CAS recheck judges an edit against rules written on any of
874
+ * them, as the live gate judged the original, and the resume re-mints its face under the spelling while it still
875
+ * resolves to the row's tool. Absent on a row minted before it ⇒ the row's `toolName` alone. */
874
876
  names?: readonly string[];
875
877
  /**
876
878
  * #438 (additive; no checkpoint-version bump — the `reminderMark` precedent: an optional field an
@@ -9,6 +9,7 @@ import { renderUntrustedCommandText, stripFormatCharacters } from "./permission-
9
9
  import { redactSecrets } from "./untrusted-egress.js";
10
10
  import { ASK_USER_QUESTION_TOOL_NAME } from "./ask-question.js";
11
11
  import { isPermissionMode } from "./ask-origin.js";
12
+ import { wireOf } from "./tool-name-readings.js";
12
13
  export function mintCheckpointToken() {
13
14
  return randomBytes(16).toString("hex");
14
15
  }
@@ -290,7 +291,7 @@ export function summarizeCheckpoint(cp) {
290
291
  ...(cp.sourceTaskId !== undefined ? { sourceTaskId: cp.sourceTaskId } : {}),
291
292
  ...(cp.principal !== undefined ? { principal: cp.principal } : {}),
292
293
  ...(tool !== undefined ? { toolCallId: tool.toolCallId, toolName: tool.toolName } : {}),
293
- ...(tool?.toolName === ASK_USER_QUESTION_TOOL_NAME ? { contentKind: "content_ask" } : {}),
294
+ ...(tool !== undefined && wireOf("park.contentKindProjection", tool) === ASK_USER_QUESTION_TOOL_NAME ? { contentKind: "content_ask" } : {}),
294
295
  ...(toolInput !== undefined ? { toolInput } : {}),
295
296
  ...(cp.state.workspaceHandle !== undefined
296
297
  ? { restoreMode: cp.state.workspaceHandle.restoreMode === "park_only" ? "park_only" : "snapshot" }
@@ -171,6 +171,9 @@ export interface ContextEditOptions {
171
171
  * registry state are compaction's job, not a blanking marker's.
172
172
  */
173
173
  compactableTools?: ReadonlySet<string>;
174
+ /** The wire name a result's recorded tool name resolves to on the leg's roster (a result is clearable by the tool the
175
+ * call ran, not the spelling the model used). Absent ⇒ the recorded name. */
176
+ toolNameOf?: (recorded: string) => string;
174
177
  /**
175
178
  * design/374 (RB-212 durable half): the PRE-CAP counterpart array of `messages`, position-aligned
176
179
  * (the pipeline's aggregate caps replace in place, never drop). When present, the machine
@@ -316,6 +319,7 @@ export declare function planRejectionClears(messages: AgentMessage[], opts: {
316
319
  keyOf: OccurrenceIndex["keyOf"];
317
320
  offload?: ContextEditOptions["offload"];
318
321
  compactableTools?: ReadonlySet<string>;
322
+ toolNameOf?: (recorded: string) => string;
319
323
  keepRecentToolResults?: number;
320
324
  }): RejectionClearPlan;
321
325
  /**
@@ -5,6 +5,7 @@ import { physicalWindowOf } from "./context-settings.js";
5
5
  import { PERSISTED_OUTPUT_PREFIX, offloadPagebackHint } from "./tool-result-store.js";
6
6
  import { assertNoRetiredKeys } from "./retired-keys.js";
7
7
  import { compactableToolNames } from "./tool-registry.js";
8
+ import { wireOf } from "./tool-name-readings.js";
8
9
  const CC_CLEARED_MARKER = "[Old tool result content cleared]";
9
10
  const CC_CLEARED_EXTENDED_PREFIX = "[Old tool result content cleared —";
10
11
  const refNote = (ref) => offloadPagebackHint(ref, "cleared");
@@ -47,6 +48,7 @@ function resolveKeepRecentToolResults(value, defaultValue) {
47
48
  return Math.max(MIN_KEEP_RECENT_TOOL_RESULTS, value);
48
49
  }
49
50
  const COMPACTABLE_TOOLS = compactableToolNames();
51
+ const compactableResult = (compactable, recorded, toolNameOf) => compactable.has(wireOf("compaction.compactableResult", { toolName: toolNameOf !== undefined ? toolNameOf(recorded) : recorded }));
50
52
  function soleMarkerText(m) {
51
53
  const content = m.content;
52
54
  if (!Array.isArray(content) || content.length !== 1)
@@ -127,6 +129,7 @@ const CONTEXT_EDIT_OPTION_KEYS = new Set([
127
129
  "anchoredTotalTokens",
128
130
  "charsPerToken",
129
131
  "compactableTools",
132
+ "toolNameOf",
130
133
  "clearSource",
131
134
  ]);
132
135
  const RETIRED_CONTEXT_EDIT_OPTION_KEYS = {
@@ -159,7 +162,7 @@ export function clearStaleToolResults(messages, opts) {
159
162
  }
160
163
  const keep = resolveKeepRecentToolResults(opts.keepRecentToolResults, CC_DEFAULT_KEEP_RECENT_TOOL_RESULTS);
161
164
  const compactable = opts.compactableTools ?? COMPACTABLE_TOOLS;
162
- const candidates = messages.flatMap((m, i) => isToolResult(m) && compactable.has(m.toolName) ? [{ idx: i, target: m }] : []);
165
+ const candidates = messages.flatMap((m, i) => isToolResult(m) && compactableResult(compactable, m.toolName, opts.toolNameOf) ? [{ idx: i, target: m }] : []);
163
166
  const beyondKeep = candidates.slice(0, Math.max(0, candidates.length - keep));
164
167
  const clearable = beyondKeep.filter(({ target }) => !isCleared(target));
165
168
  if (clearable.length === 0) {
@@ -223,7 +226,7 @@ function occurrenceGroupId(m) {
223
226
  export function planRejectionClears(messages, opts) {
224
227
  const keep = resolveKeepRecentToolResults(opts.keepRecentToolResults, CC_DEFAULT_KEEP_RECENT_TOOL_RESULTS);
225
228
  const compactable = opts.compactableTools ?? COMPACTABLE_TOOLS;
226
- const candidates = messages.flatMap((m) => (isToolResult(m) && compactable.has(m.toolName) ? [m] : []));
229
+ const candidates = messages.flatMap((m) => (isToolResult(m) && compactableResult(compactable, m.toolName, opts.toolNameOf) ? [m] : []));
227
230
  const beyondKeep = candidates.slice(0, Math.max(0, candidates.length - keep));
228
231
  const clearable = beyondKeep.flatMap((m) => {
229
232
  if (isCleared(m))
@@ -251,6 +251,13 @@ export interface EngineNotice {
251
251
  * (requested, reported) PAIR — a catalog fact, not a turn fact. Audience `operator`;
252
252
  * `detail: { requested, reported, sessionId, runId }`. Absent ⇒ the provider echoed the requested id
253
253
  * or named nothing — one reading, not two.
254
+ * - `"route.tool_call_id_folded"` (#1207) — the provider named a tool call with an id outside the shape the
255
+ * engine keys calls under (visible ASCII, 1–128, no `~`), so the brain keyed it under a deterministic fold
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.
254
261
  * - `"config.read_face_deployment_clamped"` (#237) — a deployment-wide `readFace: "open"` is not
255
262
  * in force beside a read-only (verifier) mount: it clamps to "roots" without throwing
256
263
  * (stricter-wins; the clamp verdict stands, only its occurrence was undisclosed). Announced
@@ -11,13 +11,15 @@ import { CD_GIT_COMPOUND_SENTENCE, changesDirectoryBeforeGit, readOnlyShellVerdi
11
11
  import { readShellCommand } from "./shell-lexer.js";
12
12
  import { COMMAND_RULE_TOOL } from "./permission-rule-model.js";
13
13
  import { catalogRuleFaceOf } from "./tool-registry.js";
14
- import { callAnswersTo, callFaceSeat } from "./tool-roster.js";
14
+ import { callFaceSeat } from "./tool-roster.js";
15
+ import { heldBy, wireOf } from "./tool-name-readings.js";
15
16
  import { modeAcceptsFileCommand } from "./mode-policy.js";
16
17
  import { ORG_ADJUDICATION_TIMEOUT_MS, ORG_RULE_DECISION_REASON, ORG_UNAVAILABLE_DECISION_REASON, settleOrgVerdictWithin } from "./permission-rule-org.js";
17
18
  import { exitGate } from "./runner/gate-exit.js";
18
19
  import { traceHookCrash } from "./gate-crash-trace.js";
19
20
  import { createGateDeadline, awaitWriteProtectionJudgment, withGateTimeout } from "./gate-bounded-await.js";
20
21
  import { thrownText } from "./thrown-value.js";
22
+ const QUESTION_TOOL_NAMES = new Set([ASK_USER_QUESTION_TOOL_NAME]);
21
23
  function isPlainOwnRecord(x) {
22
24
  try {
23
25
  if (x === null || typeof x !== "object" || Array.isArray(x))
@@ -170,7 +172,7 @@ export async function runGateLanes(pass) {
170
172
  writeProtectionUnresolved: pass.writeProtectionUnresolved === true,
171
173
  });
172
174
  const askOriginFacts = (org, ruleCallUnreadable) => ({
173
- contentQuestion: callAnswersTo(pass.req, ASK_USER_QUESTION_TOOL_NAME),
175
+ contentQuestion: heldBy("lanes.contentQuestionClass", QUESTION_TOOL_NAMES, pass.req),
174
176
  markedUnresolvable: markedUnresolvable(),
175
177
  org,
176
178
  ...(ruleCallUnreadable ? { ruleCallUnreadable } : {}),
@@ -195,7 +197,7 @@ export async function runGateLanes(pass) {
195
197
  const applyOrgLayer = async (current, args) => {
196
198
  if (input.orgRules === undefined)
197
199
  return { decision: current, org: undefined };
198
- if (toolName === ASK_USER_QUESTION_TOOL_NAME && input.orgRules.contentAskToolMounted === true)
200
+ if (wireOf("lanes.orgContentAskExemption", pass.req) === ASK_USER_QUESTION_TOOL_NAME && input.orgRules.contentAskToolMounted === true)
199
201
  return { decision: current, org: undefined };
200
202
  let decided = current;
201
203
  let pending;
@@ -271,7 +273,7 @@ export async function runGateLanes(pass) {
271
273
  return await settleOrgVerdictWithin(pendingHit.then(normalizePersistedRuleHit).catch(() => ({ unreadable: true })), { unreadable: true }, { ...(input.abortSignal !== undefined ? { signal: input.abortSignal } : {}), timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS });
272
274
  };
273
275
  const laneReq = { ...pass.req, ...(pass.policyRewrite !== undefined ? { args: pass.policyRewrite } : {}) };
274
- const laneAnswer = input.persistedRules !== undefined && pass.req.toolName !== ASK_USER_QUESTION_TOOL_NAME ? await readPersistedRules(laneReq) : undefined;
276
+ const laneAnswer = input.persistedRules !== undefined && wireOf("lanes.persistedLaneSkip", pass.req) !== ASK_USER_QUESTION_TOOL_NAME ? await readPersistedRules(laneReq) : undefined;
275
277
  {
276
278
  const tightened = applyPersistedTightening(pass.decision, laneAnswer);
277
279
  pass.decision = tightened.decision;
@@ -299,7 +301,7 @@ export async function runGateLanes(pass) {
299
301
  pass.decision.action === "ask" &&
300
302
  pass.decision.requiresRealApproval !== true &&
301
303
  pass.decision.decisionReason !== "hook" &&
302
- pass.req.toolName !== ASK_USER_QUESTION_TOOL_NAME &&
304
+ wireOf("lanes.allowLayerQuestionSkip", pass.req) !== ASK_USER_QUESTION_TOOL_NAME &&
303
305
  !markedUnresolvable();
304
306
  if (input.persistedRules && allowLayerMayClear()) {
305
307
  const answer = laneAnswer ?? {};
@@ -356,7 +358,7 @@ export async function runGateLanes(pass) {
356
358
  classifyStanding(stamped, originFacts);
357
359
  pass.decision = stampAskOrigin(stamped, originFacts);
358
360
  }
359
- if (allowLayerMayClear() && pass.decision.action === "ask" && ruleMayClear() && pass.probeBoundaryDeclined !== true && pass.req.toolName === COMMAND_RULE_TOOL) {
361
+ if (allowLayerMayClear() && pass.decision.action === "ask" && ruleMayClear() && pass.probeBoundaryDeclined !== true && wireOf("lanes.readOnlyShellAllow", pass.req) === COMMAND_RULE_TOOL) {
360
362
  const commandParam = catalogRuleFaceOf(COMMAND_RULE_TOOL)?.primaryParams[0] ?? "command";
361
363
  const args = pass.req.args;
362
364
  const command = typeof args === "object" && args !== null && !Array.isArray(args) ? args[commandParam] : undefined;
@@ -435,7 +437,7 @@ export async function runGateLanes(pass) {
435
437
  }
436
438
  if (input.sandboxAdmission !== undefined &&
437
439
  pass.decision.action === "ask" &&
438
- pass.req.toolName !== ASK_USER_QUESTION_TOOL_NAME &&
440
+ wireOf("lanes.sandboxQuestionSkip", pass.req) !== ASK_USER_QUESTION_TOOL_NAME &&
439
441
  input.isMarkedUnresolvable?.(input.event.toolCallId) !== true &&
440
442
  pass.decision.requiresRealApproval !== true &&
441
443
  pass.decision.decisionReason !== "hook" &&
@@ -463,7 +465,7 @@ export async function runGateLanes(pass) {
463
465
  if (pass.decision.action === "ask" && input.permissionMode !== undefined && pass.askClassification !== undefined) {
464
466
  const standing = pass.decision;
465
467
  const mode = input.permissionMode();
466
- const fileCommandByWord = modeAcceptsFileCommand(mode, toolName, pass.policyRewrite ?? pass.currentInput);
468
+ const fileCommandByWord = modeAcceptsFileCommand(mode, wireOf("lanes.modeFileCommand", pass.req), pass.policyRewrite ?? pass.currentInput);
467
469
  const verdict = arbitrateByMode(mode, pass.askClassification, { ...(standing.origin !== undefined ? { origin: standing.origin } : {}), ...(standing.mandate !== undefined ? { mandate: standing.mandate } : {}) }, { fileCommand: pass.shellWritesPlaced === true && fileCommandByWord });
468
470
  if (verdict.kind === "ask" && fileCommandByWord && pass.shellWritesPlaced !== true && pass.shellUnplacedOptions !== undefined && pass.askClassification.classes.size === 1 && pass.askClassification.classes.has("ordinary")) {
469
471
  pass.decision = { ...standing, message: fileCommandOptionsSentence(pass.shellUnplacedOptions.program) };
@@ -528,7 +530,7 @@ export async function runGateLanes(pass) {
528
530
  return { suspend: suspended, preToolContext: pass.preToolContext };
529
531
  }
530
532
  }
531
- if (pass.decision.action === "ask" && pass.req.toolName === ASK_USER_QUESTION_TOOL_NAME) {
533
+ if (pass.decision.action === "ask" && wireOf("lanes.contentAskRoute", pass.req) === ASK_USER_QUESTION_TOOL_NAME) {
532
534
  const outcome = input.resolveContentAsk !== undefined ? await (callSignal !== undefined ? input.resolveContentAsk(pass.req, callSignal) : input.resolveContentAsk(pass.req)) : { kind: "unavailable", parkDeclined: false };
533
535
  if (outcome.kind === "answered") {
534
536
  pass.decision = { action: "allow", updatedInput: outcome.presentedInput };
@@ -692,7 +694,7 @@ export async function runGateLanes(pass) {
692
694
  break;
693
695
  }
694
696
  let editRuleCallUnreadable = false;
695
- if (input.persistedRules !== undefined && toolName !== ASK_USER_QUESTION_TOOL_NAME) {
697
+ if (input.persistedRules !== undefined && wireOf("lanes.persistedEditSkip", pass.req) !== ASK_USER_QUESTION_TOOL_NAME) {
696
698
  const editLane = await readPersistedRules({ toolName, args: editArgs, toolCallId, ...(pass.req.face !== undefined ? { face: pass.req.face } : {}) });
697
699
  editRuleCallUnreadable = ruleCallUnreadableOf(editLane);
698
700
  const editTightened = applyPersistedTightening(recheck, editLane);
@@ -135,7 +135,7 @@ export type NoticeAudience = "user" | "operator";
135
135
  * the union's catalog ({@link EngineNotice.code}). The groups below say only which family a row joins,
136
136
  * so the catalog is not written a second time here and the two cannot drift.
137
137
  */
138
- export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.context_settings_swapped", "config.read_face_swapped", "config.read_face_deployment_clamped", "config.write_face_swapped", "config.write_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.artifact_host_invalid", "config.task_root_not_canonical", "config.write_protection_target_view_absent", "config.write_protection_unresolved", "config.execution_env_capability_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.forward_sink_failed", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "config.secret_env_scrubbed", "config.permission_mode_unavailable", "permission.ask_released_by_mode", "permission.read_deny_unapplied", "permission.rule_store_unavailable", "resume.legacy_mode_seed_migrated", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "classifier.fallback", "classifier.role_defaulted", "classifier.contract_broken", "classifier.unavailable", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "delegation.ask_unresolvable", "mcp.injection_dropped", "mcp.revocation_probe_failed", "mcp.server_redialed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "memory.restore_withheld", "memory.project_scope_absent", "route.fallback_to_primary", "route.classifier_unroutable", "route.base_url_changed_key_unchanged", "route.response_model_mismatch", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "task.interrupt_unconsumed", "approval.read_root_granted", "approval.read_root_grant_rejected", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "skills.listing_truncated", "agents.listing_truncated", "tool_result.offload_put_failed"];
138
+ export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.context_settings_swapped", "config.read_face_swapped", "config.read_face_deployment_clamped", "config.write_face_swapped", "config.write_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.artifact_host_invalid", "config.task_root_not_canonical", "config.write_protection_target_view_absent", "config.write_protection_unresolved", "config.execution_env_capability_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.forward_sink_failed", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "config.secret_env_scrubbed", "config.permission_mode_unavailable", "permission.ask_released_by_mode", "permission.read_deny_unapplied", "permission.rule_store_unavailable", "resume.legacy_mode_seed_migrated", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "classifier.fallback", "classifier.role_defaulted", "classifier.contract_broken", "classifier.unavailable", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "delegation.ask_unresolvable", "mcp.injection_dropped", "mcp.revocation_probe_failed", "mcp.server_redialed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "memory.restore_withheld", "memory.project_scope_absent", "route.fallback_to_primary", "route.classifier_unroutable", "route.base_url_changed_key_unchanged", "route.response_model_mismatch", "route.tool_call_id_folded", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "task.interrupt_unconsumed", "approval.read_root_granted", "approval.read_root_grant_rejected", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "skills.listing_truncated", "agents.listing_truncated", "tool_result.offload_put_failed"];
139
139
  /** A code this engine mints (see {@link ENGINE_NOTICE_CODES}). NOT the type of
140
140
  * `EngineNotice.code`, which stays `string` — a host forwarding its own notices through the same
141
141
  * sink is a supported shape, and narrowing that field would break it. */
@@ -178,6 +178,7 @@ export const ENGINE_NOTICE_CODES = [
178
178
  "route.classifier_unroutable",
179
179
  "route.base_url_changed_key_unchanged",
180
180
  "route.response_model_mismatch",
181
+ "route.tool_call_id_folded",
181
182
  "task.user_steer_undrained",
182
183
  "task.user_followup_undrained",
183
184
  "steering.parked_input_blocked",
@@ -269,6 +270,7 @@ const NOTICE_AUDIENCE_TABLE = {
269
270
  "route.classifier_unroutable": "operator",
270
271
  "route.base_url_changed_key_unchanged": "operator",
271
272
  "route.response_model_mismatch": "operator",
273
+ "route.tool_call_id_folded": "operator",
272
274
  "skills.listing_truncated": "operator",
273
275
  "agents.listing_truncated": "operator",
274
276
  "tool_result.offload_put_failed": "operator",
@@ -474,6 +474,9 @@ export interface PostToolUseFailureResult {
474
474
  /** One resolved call in a postToolBatch payload (CC PostToolBatchToolCall shape, thin projection —
475
475
  * `response` is a bounded text digest of the result, never the full details object). */
476
476
  export interface PostToolBatchCall {
477
+ /** The name the model spelled on the tool_use block (an alias stays an alias) — the reference's PostToolBatch payload
478
+ * names each block the same way, while the per-call post hooks receive the resolved tool's wire name. Correlate
479
+ * the two faces by `toolCallId`. */
477
480
  toolName: string;
478
481
  input: unknown;
479
482
  toolCallId: string;
@@ -11,6 +11,7 @@ import { isAskOrigin } from "./ask-origin.js";
11
11
  import { inlineUntrusted } from "./untrusted-text.js";
12
12
  export { formatHookFeedback } from "./reminder-mint.js";
13
13
  import { createSafeNotifier } from "./safe-notify.js";
14
+ import { wireOf } from "./tool-name-readings.js";
14
15
  export const DEFAULT_HOOK_TIMEOUT_MS = 600_000;
15
16
  export const MAX_HOOK_TIMEOUT_MS = 2_147_483_647;
16
17
  const POST_ABORT_GRACE_MS = 1_000;
@@ -247,9 +248,10 @@ export async function runToolGate(input) {
247
248
  const { event, preToolUse, adjudicate, resolveAsk, suspendAsk } = input;
248
249
  const { toolCallId, toolName } = event;
249
250
  const callSignal = input.callSignal;
251
+ const screenedName = wireOf("gate.preToolUse", event);
250
252
  const hookCtx = (seatSignal) => ({
251
253
  toolCallId,
252
- toolName,
254
+ toolName: screenedName,
253
255
  ...(seatSignal !== undefined ? { signal: seatSignal } : {}),
254
256
  ...(() => {
255
257
  const c = input.trackedCwd?.();
@@ -284,7 +286,7 @@ export async function runToolGate(input) {
284
286
  };
285
287
  const screening = preToolUse !== undefined
286
288
  ? {
287
- screen: (args) => runHookSeat("preToolUse", seatBound(true), (sig) => preToolUse(toolName, args, hookCtx(sig))),
289
+ screen: (args) => runHookSeat("preToolUse", seatBound(true), (sig) => preToolUse(screenedName, args, hookCtx(sig))),
288
290
  screened: screenPreToolUseResult,
289
291
  crashReason: preToolUseCrashReason,
290
292
  unansweredReason: preToolUseUnansweredBlock,
@@ -1,6 +1,7 @@
1
1
  import { createFsWriteGatePolicy } from "./fs-write-gate-policy.js";
2
2
  import { isProtectedWrite } from "./tool-registry.js";
3
3
  import { rosterEntryNamed } from "./tool-roster.js";
4
+ import { wireOf } from "./tool-name-readings.js";
4
5
  import { clampPermissionMode } from "./ask-origin.js";
5
6
  import { argvOf, lexShellText } from "../shell/lex.js";
6
7
  import { COMMAND_RULE_TOOL } from "./permission-rule-model.js";
@@ -42,7 +43,7 @@ const ENGINE_ROSTER_SOURCES = new Set(["builtin", "synthetic"]);
42
43
  export function planRefusesWrite(req, roster, readOnlyDeclared) {
43
44
  if (isProtectedWrite(req))
44
45
  return true;
45
- const row = roster === undefined ? undefined : rosterEntryNamed(roster, req.toolName);
46
+ const row = roster === undefined ? undefined : rosterEntryNamed(roster, wireOf("mode.planRosterRow", req));
46
47
  return row !== undefined && !ENGINE_ROSTER_SOURCES.has(row.source) && row.effect !== "read" && !(row.effectProvenance === "default" && readOnlyDeclared?.(row.name) === true);
47
48
  }
48
49
  const planAsk = (message) => ({ action: "ask", message, decisionReason: "mode" });
@@ -141,8 +141,8 @@ export declare const READ_RULE_TOOL = "Read";
141
141
  * catalog's declarations, exactly as the protective fences' covered set does.
142
142
  */
143
143
  export declare function ruleToolGrammarOf(toolName: string): "command" | "path" | undefined;
144
- /** The names the persisted grammar judges a call under: every one of the call's rule names (`ruleNamesOf` — the wire
145
- * name, then the aliases that resolve to it) the grammar speaks for, so a stored `Bash(…)` or `Edit(…)` row reaches a
144
+ /** The names the persisted grammar judges a call under: every name the call answers to (the model's spelling, the wire
145
+ * name, the other aliases that resolve to it) the grammar speaks for, so a stored `Bash(…)` or `Edit(…)` row reaches a
146
146
  * tool that answers to that name under another wire name, as a policy's rule does. Empty ⇒ the grammar speaks for none. */
147
147
  export declare function laneToolNamesOf(req: Pick<import("./tool-policy.js").ToolCallRequest, "toolName" | "face">): readonly string[];
148
148
  /**
@@ -4,7 +4,7 @@ import { compileReadDeny } from "../tools/fs/read-deny.js";
4
4
  import { carriesShellRedirection, parseLeadingCommandName, splitShellCompoundSegments } from "../tools/fs/bash-readonly-classifier.js";
5
5
  import { inlineUntrusted } from "./untrusted-text.js";
6
6
  import { pathTargetOf } from "./tool-registry.js";
7
- import { ruleNamesOf } from "./tool-roster.js";
7
+ import { heldValues } from "./tool-name-readings.js";
8
8
  import { carriesDotSegment, isAbsoluteForFamily, isIdentityDir, isPathIdentity, isShellRootedSpellingUnmapped, isUncIdentity, pathFamilyOf, pathIdentityOf } from "../tools/fs/safety.js";
9
9
  import { readShellCommand } from "./shell-lexer.js";
10
10
  import { readOnlyLineObjection, readOnlyShellVerdict } from "./read-only-shell.js";
@@ -32,7 +32,7 @@ export function ruleToolGrammarOf(toolName) {
32
32
  return pathTargetOf(toolName) !== undefined ? "path" : undefined;
33
33
  }
34
34
  export function laneToolNamesOf(req) {
35
- return ruleNamesOf(req).filter((n) => ruleToolGrammarOf(n) !== undefined);
35
+ return heldValues("policy.persistedGrammar", { get: (n) => (ruleToolGrammarOf(n) !== undefined ? n : undefined) }, req);
36
36
  }
37
37
  export const MAX_RULE_TEXT_CHARS = 512;
38
38
  export const BARE_INTERPRETER_NAMES = new Set([
@@ -6,7 +6,7 @@ import { pathFamilyOf, pathIdentityOf, spellingOfIdentity } from "../tools/fs/sa
6
6
  import { effectivePathTargetOf } from "./effective-path-target.js";
7
7
  import { protectivePathTargetOf } from "./tool-registry.js";
8
8
  import { ruleDenialSentence } from "./rule-denial-sentence.js";
9
- import { ruleNamesOf } from "./tool-roster.js";
9
+ import { heldValues } from "./tool-name-readings.js";
10
10
  export { parsePermissionRule };
11
11
  export const BASH_GENERIC_PARAMS = new Set(catalogRuleFaceOf("Bash")?.params ?? []);
12
12
  const DEFAULT_CAPS = {
@@ -381,10 +381,7 @@ export function createPermissionRulePolicy(rules, opts) {
381
381
  requiresRealApproval: true,
382
382
  });
383
383
  const entryFor = (req) => {
384
- const entries = ruleNamesOf(req).flatMap((n) => {
385
- const e = byTool.get(n);
386
- return e !== undefined ? [e] : [];
387
- });
384
+ const entries = heldValues("policy.permissionRules", byTool, req);
388
385
  if (entries.length <= 1)
389
386
  return entries[0];
390
387
  const bare = {};
@@ -3,7 +3,7 @@ import { pathTargetValue } from "../tool-registry.js";
3
3
  import { pathTargetBaseOf } from "../effective-path-target.js";
4
4
  import { PATH_WRITE_TOOLS, isWithin } from "./session-rule-policy.js";
5
5
  import { parseSkillToolEntry, skillSpecifierRejection } from "../skill-tool-specifier.js";
6
- import { ruleNamesOf } from "../tool-roster.js";
6
+ import { heldValues, wireOf } from "../tool-name-readings.js";
7
7
  export class ActiveSkillScope {
8
8
  frames = [];
9
9
  push(frame) {
@@ -23,7 +23,7 @@ export function createActiveSkillScopePolicy(opts) {
23
23
  const { scope, env, rootPath, toolEffects } = opts;
24
24
  return {
25
25
  async check(req, signal) {
26
- const names = ruleNamesOf(req);
26
+ const entriesFor = (byName) => heldValues("policy.skillScope", byName, req).flat();
27
27
  const frames = scope.active();
28
28
  if (frames.length === 0)
29
29
  return { action: "allow" };
@@ -48,7 +48,7 @@ export function createActiveSkillScopePolicy(opts) {
48
48
  }
49
49
  return { manifest: m, byName };
50
50
  });
51
- if (perFrame.length === 0 || perFrame.some((f) => !names.some((n) => f.byName.has(n)))) {
51
+ if (perFrame.length === 0 || perFrame.some((f) => entriesFor(f.byName).length === 0)) {
52
52
  const ids = manifests.map((m) => m.lineageId).join(", ");
53
53
  return {
54
54
  action: "deny",
@@ -57,7 +57,7 @@ export function createActiveSkillScopePolicy(opts) {
57
57
  };
58
58
  }
59
59
  for (const frame of perFrame) {
60
- const alternatives = names.flatMap((n) => frame.byName.get(n) ?? []);
60
+ const alternatives = entriesFor(frame.byName);
61
61
  const rejections = [];
62
62
  let admitted = false;
63
63
  for (const entry of alternatives) {
@@ -80,7 +80,7 @@ export function createActiveSkillScopePolicy(opts) {
80
80
  const declaredTarget = req.face?.pathTarget;
81
81
  const skillScopeEligible = declaredTarget !== undefined && declaredTarget.skillScopeEligible === true;
82
82
  if (pathConstrainingActive && !skillScopeEligible) {
83
- const eff = toolEffects?.get(req.toolName) ?? "write";
83
+ const eff = toolEffects?.get(wireOf("policy.skillScope.effect", req)) ?? "write";
84
84
  if (eff !== "read") {
85
85
  const ids = manifests.filter((m) => m.allowPaths?.length).map((m) => m.lineageId).join(", ");
86
86
  return {
@@ -3,6 +3,7 @@ import { boundInputHashOf } from "../canonical-json.js";
3
3
  import { CheckpointError } from "../checkpoint-store.js";
4
4
  import { formatHookFeedback } from "../hooks.js";
5
5
  import { delimitUntrusted, REVIEWER_NOTE_MAX_BODY } from "../untrusted-text.js";
6
+ import { wireOf } from "../tool-name-readings.js";
6
7
  export function resumeDecisionWasNegative(resume) {
7
8
  const o = resume.outcome;
8
9
  return o.decision === "reject" || o.decision === "deny";
@@ -49,7 +50,7 @@ export function sameWinner(incoming, persisted) {
49
50
  (persisted.reason === undefined || incoming.reason === persisted.reason));
50
51
  }
51
52
  export function pendingContentAskCallId(cp) {
52
- return cp.pendingAction.kind === "tool_approval" && cp.pendingAction.toolName === ASK_USER_QUESTION_TOOL_NAME
53
+ return cp.pendingAction.kind === "tool_approval" && wireOf("resume.contentAskRow", cp.pendingAction) === ASK_USER_QUESTION_TOOL_NAME
53
54
  ? cp.pendingAction.toolCallId
54
55
  : undefined;
55
56
  }
@@ -241,6 +241,9 @@ export declare function createRuleOffersOf(cfg: {
241
241
  shellGatedTools: {
242
242
  has(name: string): boolean;
243
243
  };
244
+ toolMarkSources: {
245
+ get(name: string): ReadonlySet<string> | undefined;
246
+ };
244
247
  taskRoot: string | undefined;
245
248
  /** The live tracked-cwd ref (read per call — the relative-cd resolution base). */
246
249
  cwdRef: {
@@ -3,7 +3,8 @@ import {} from "../hooks.js";
3
3
  import { adjudicatePersistedPathRules, adjudicatePersistedRules, laneToolNamesOf, ruleToolGrammarOf, segmentCoverageOf, suggestRulesForCommand } from "../permission-rule-model.js";
4
4
  import { isAbsolutePathForm } from "../../tools/fs/safety.js";
5
5
  import { effectivePathTargetOf } from "../effective-path-target.js";
6
- import { anyNamed, declaredPathTargetOf, firstNamed, protectivePathTargetOf, ruleNamesOf } from "../tool-registry.js";
6
+ import { declaredPathTargetOf, protectivePathTargetOf } from "../tool-registry.js";
7
+ import { heldBy, seatOf, wireOf } from "../tool-name-readings.js";
7
8
  import { orgRuleVerdictFor } from "../permission-rule-org.js";
8
9
  import { ownerStoreOf } from "../permission-rule-provider.js";
9
10
  import { thrownText } from "../thrown-value.js";
@@ -92,7 +93,7 @@ export function createPermissionRuleLanes(cfg) {
92
93
  const org = provider.partitions.org
93
94
  ? {
94
95
  adjudicate: async (req) => {
95
- if (req.toolName === cfg.questionToolName && cfg.questionToolMounted)
96
+ if (wireOf("offer.orgQuestionExemption", req) === cfg.questionToolName && cfg.questionToolMounted)
96
97
  return { status: "available" };
97
98
  let resolution;
98
99
  try {
@@ -158,16 +159,16 @@ export function createPermissionRuleLanes(cfg) {
158
159
  }
159
160
  export function createRuleOffersOf(cfg) {
160
161
  return (call, args, ask) => {
161
- const toolName = call.toolName;
162
+ const toolName = wireOf("offer.laneTool", call);
162
163
  if (!cfg.laneArmed || toolName !== PERSISTED_RULE_TOOL)
163
164
  return {};
164
165
  if ((cfg.principal === undefined || cfg.principal === "") && !cfg.localOwnerDeclared)
165
166
  return {};
166
- const tierSeat = firstNamed(cfg.irreversibilityTier, ruleNamesOf(call));
167
+ const tierSeat = seatOf("offer.marks", cfg.irreversibilityTier, call);
167
168
  const closedDoor = ruleClearanceOf(ask, {
168
- egress: anyNamed(cfg.egressTools, ruleNamesOf(call)),
169
+ egress: heldBy("offer.marks.egress", cfg.egressTools, call),
169
170
  irreversibility: tierSeat?.value,
170
- shellGated: tierSeat !== undefined && cfg.shellGatedTools.has(tierSeat.name),
171
+ shellGated: tierSeat?.shellGated(cfg.shellGatedTools, cfg.toolMarkSources) === true,
171
172
  }).word;
172
173
  if (closedDoor !== undefined)
173
174
  return { ruleOffersAbsence: closedDoor };
@@ -24,6 +24,7 @@ import { type RunnerDeps } from "../types.js";
24
24
  import type { Prepared, PreparedMicroCompact } from "./contracts.js";
25
25
  import { type GitStatusLaneRef } from "./git-status-frame.js";
26
26
  import type { ReadFileState } from "../../tools/fs/safety.js";
27
+ import { type ToolRosterDeltaSeat } from "../tool-roster.js";
27
28
  export interface PrepareContextLaneInput {
28
29
  /** borrowed-readonly — the task model: the budgets, the trigger-window geometry and `charsPerToken` are read off it. */
29
30
  model: Model;
@@ -39,6 +40,8 @@ export interface PrepareContextLaneInput {
39
40
  hostTaskId: string;
40
41
  /** borrowed-readonly — the run's offload store, or undefined (the caps' and the machine's persist seat). */
41
42
  offloadStore: ToolResultStore | undefined;
43
+ /** borrowed-readonly — the leg's live roster: a recorded tool name is judged as the wire name it resolves to. */
44
+ toolRosterDeltas: ToolRosterDeltaSeat;
42
45
  /** borrowed-readonly — the doors' RESOLVED microCompact knob (immutable values, never the deps bag). */
43
46
  microCompactKnob: {
44
47
  machine: MicroCompactMachine;
@@ -135,6 +138,7 @@ export interface GuardChainArgs {
135
138
  offloadStore: import("../tool-result-store.js").ToolResultStore | undefined;
136
139
  sessionId: string;
137
140
  taskId: string;
141
+ toolNameOf?: (recorded: string) => string;
138
142
  deps: RunnerDeps;
139
143
  trimPressureRef: {
140
144
  droppedMessages: boolean;
@@ -8,8 +8,10 @@ import { emitTrace } from "../trace.js";
8
8
  import { deliverEngineNotice } from "../types.js";
9
9
  import { applyGitFrameGuard } from "./git-status-frame.js";
10
10
  import { applyContextShrinkToReadFileState } from "../../tools/fs/index.js";
11
+ import { rosterToolName } from "../tool-roster.js";
11
12
  export function prepareContextLane(input) {
12
- const { model, deps, sessionId, runId, hostTaskId, offloadStore, microCompactKnob, harness, batchHaltRef, gitStatusRef, postureFrameRef, requestLossyRef, trimPressureRef, readFileState } = input;
13
+ const { model, deps, sessionId, runId, hostTaskId, offloadStore, toolRosterDeltas, microCompactKnob, harness, batchHaltRef, gitStatusRef, postureFrameRef, requestLossyRef, trimPressureRef, readFileState } = input;
14
+ const toolNameOf = (recorded) => rosterToolName(toolRosterDeltas.current, recorded);
13
15
  const contextOverlay = input.contextOverlay ?? (() => undefined);
14
16
  const requestGeometry = () => {
15
17
  const current = harness.getModel();
@@ -27,7 +29,7 @@ export function prepareContextLane(input) {
27
29
  const replay = replayClearedProjection(healed, microCompact.ledger);
28
30
  const replayed = replay.messages;
29
31
  const capped = await capAggregateToolResults(replayed, {
30
- store: offloadStore,
32
+ store: offloadStore, toolNameOf,
31
33
  sessionId,
32
34
  onCapped: (info) => pendingProjectionObservations.push(() => emitTrace(deps.tracer, () => ({
33
35
  kind: "tool_result.capped",
@@ -49,7 +51,7 @@ export function prepareContextLane(input) {
49
51
  });
50
52
  let ccPass;
51
53
  const edited = microCompact.machine === "off" ? mediaCapped : clearStaleToolResults(mediaCapped, {
52
- budgetTokens: editAt,
54
+ budgetTokens: editAt, toolNameOf,
53
55
  clearSource: replayed,
54
56
  onCleared: (pass) => { ccPass = pass; },
55
57
  anchoredTotalTokens: estimateContextTokens(mediaCapped, cpt).tokens,
@@ -67,7 +69,7 @@ export function prepareContextLane(input) {
67
69
  }
68
70
  const chain = await runGuardChain({
69
71
  edited, replayed, index: replay.index, microCompact, guardAt, charsPerToken: cpt, offloadStore,
70
- sessionId, taskId: hostTaskId, deps, trimPressureRef, recheck, signal,
72
+ sessionId, taskId: hostTaskId, deps, trimPressureRef, recheck, signal, toolNameOf,
71
73
  pendingProjectionObservations,
72
74
  ...(readFileState !== undefined ? { readFileState } : {}),
73
75
  });
@@ -221,7 +223,7 @@ export async function runGuardChain(args) {
221
223
  if (microCompact.machine === "off" && anchoredWorking > guardAt) {
222
224
  let blockingPass;
223
225
  const blockingEdited = clearStaleToolResults(working, {
224
- budgetTokens: guardAt,
226
+ budgetTokens: guardAt, toolNameOf: args.toolNameOf,
225
227
  clearSource: args.replayed,
226
228
  onCleared: (pass) => { blockingPass = pass; },
227
229
  anchoredTotalTokens: anchoredWorking,