@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
@@ -9,8 +9,10 @@ import { askUnresolvableNotice } from "../ask-unresolvable-notice.js";
9
9
  import { askReachesPerson } from "../wiring-manifest.js";
10
10
  import { classifierUnavailableSeat } from "./classifier-ladder.js";
11
11
  import { PATH_CONFINABLE_WRITE_TOOLS } from "./session-rule-policy.js";
12
- import { anyNamed, callAnswersTo, callFaceSeat, firstNamed, rosterToolName, ruleNamesOf, toolCallFaceOf } from "../tool-roster.js";
12
+ import { callFaceSeat, rosterToolName, toolCallFaceOf } from "../tool-roster.js";
13
+ import { heldBy, heldValues, seatOf, wireOf } from "../tool-name-readings.js";
13
14
  import { noteHostRewrite } from "../frame-settlement.js";
15
+ const PEER_MESSAGE_NAMES = new Set([SEND_MESSAGE_TOOL_NAME]);
14
16
  function screenGateResult(result) {
15
17
  if (result.suspend !== undefined)
16
18
  return { defects: [] };
@@ -88,12 +90,12 @@ export function prepareGateStations(input) {
88
90
  harness.on("tool_call", async (called) => {
89
91
  const e = { ...called, toolName: rosterToolName(toolRosterDeltas.current, called.toolName) };
90
92
  const call = { toolName: e.toolName, ...callFaceSeat(toolCallFaceOf(toolRosterDeltas.current, called.toolName)) };
91
- const names = ruleNamesOf(call);
92
- const tierSeat = firstNamed(irreversibilityTier, names);
93
+ const tierSeat = seatOf("gate.shellSeat", irreversibilityTier, call);
94
+ const markSources = new Set(heldValues("gate.markSources", toolMarkSources, call).flatMap((s) => [...s]));
93
95
  blockedToolCalls.delete(e.toolCallId);
94
96
  inheritedAskGrants.delete(e.toolCallId);
95
97
  {
96
- const complianceDeny = names.map((n) => complianceCallDenial(complianceDenies, n)).find((d) => d !== undefined);
98
+ const complianceDeny = heldValues("gate.compliance", { get: (n) => complianceCallDenial(complianceDenies, n) }, call)[0];
97
99
  if (complianceDeny !== undefined) {
98
100
  if (blockedTracked)
99
101
  blockedToolCalls.add(e.toolCallId);
@@ -122,19 +124,19 @@ export function prepareGateStations(input) {
122
124
  resolveAsk: resolveAskBound,
123
125
  suspendAsk,
124
126
  resolveContentAsk,
125
- egress: anyNamed(egressTools, names),
126
- peerMessage: callAnswersTo(call, SEND_MESSAGE_TOOL_NAME),
127
+ egress: heldBy("gate.egress", egressTools, call),
128
+ peerMessage: heldBy("gate.peerMessage", PEER_MESSAGE_NAMES, call),
127
129
  ...(writeProtectionCheck !== undefined ? { writeProtectionCheck } : {}),
128
130
  ...(shellReadCheck !== undefined ? { shellReadCheck } : {}),
129
131
  irreversibility: tierSeat?.value,
130
- reversibilityProbe: tierSeat !== undefined ? reversibilityProbes.get(tierSeat.name) : undefined,
132
+ reversibilityProbe: tierSeat?.read(reversibilityProbes),
131
133
  abortSignal: abortController.signal,
132
134
  ...(notifyPermissionDenied ? { permissionDenied: notifyPermissionDenied } : {}),
133
135
  onHookError: notifyHookError,
134
- shellGated: tierSeat !== undefined && shellGatedTools.has(tierSeat.name),
136
+ shellGated: tierSeat?.shellGated(shellGatedTools, toolMarkSources) === true,
135
137
  ...(autoModeDecider.current !== undefined ? { autoMode: { decider: autoModeDecider.current, ...(autoModeDenialTracking.current !== undefined ? { denialTracking: autoModeDenialTracking.current } : {}), unavailable: classifierUnavailableSeat({ station: "leg", ...(autoModeDecider.current.timeoutMs !== undefined ? { budgetMs: autoModeDecider.current.timeoutMs } : {}), approvalSeat: askReachesPerson(askRoute), onNotice: deps.onNotice, sessionId, runId }) } } : {}),
136
138
  permissionMode: () => permissionMode.current,
137
- ...(() => { const s = firstNamed(toolMarkSources, names)?.value; return s !== undefined ? { toolMarkSources: s } : {}; })(),
139
+ ...(markSources.size > 0 ? { toolMarkSources: markSources } : {}),
138
140
  ...(a2aIsolatePeers === true ? { a2aIsolatePeers: true } : {}),
139
141
  onModeReleased,
140
142
  ancestorArbitrationOf: (toolCallId) => ancestorArbitrations.get(toolCallId),
@@ -243,6 +245,7 @@ export function prepareGateStations(input) {
243
245
  }
244
246
  let content = e.content;
245
247
  let changed = false;
248
+ const post = { toolName: rosterToolName(toolRosterDeltas.current, e.toolName) };
246
249
  if (e.isError) {
247
250
  if (hooks?.postToolUseFailure) {
248
251
  let clonedDetails = e.details;
@@ -251,7 +254,8 @@ export function prepareGateStations(input) {
251
254
  }
252
255
  catch {
253
256
  }
254
- const failureSeat = await runHookSeat("postToolUseFailure", { timeoutMs: hookTimeoutMs, signal: abortController.signal }, (sig) => hooks.postToolUseFailure(e.toolName, e.input, {
257
+ const failedName = wireOf("hook.postToolUseFailure", post);
258
+ const failureSeat = await runHookSeat("postToolUseFailure", { timeoutMs: hookTimeoutMs, signal: abortController.signal }, (sig) => hooks.postToolUseFailure(failedName, e.input, {
255
259
  error: e.content
256
260
  .filter((c) => c.type === "text")
257
261
  .map((c) => c.text)
@@ -259,7 +263,7 @@ export function prepareGateStations(input) {
259
263
  isInterrupt: abortController.signal.aborted || spec.signal?.aborted === true,
260
264
  content: e.content.map((c) => ({ ...c })),
261
265
  details: clonedDetails,
262
- }, { toolCallId: e.toolCallId, toolName: e.toolName, signal: sig, ...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}), ...(hookEnvFace !== undefined ? { env: hookEnvFace } : {}), identity: hookIdentity }));
266
+ }, { toolCallId: e.toolCallId, toolName: failedName, signal: sig, ...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}), ...(hookEnvFace !== undefined ? { env: hookEnvFace } : {}), identity: hookIdentity }));
263
267
  if (failureSeat.expired) {
264
268
  notifyOwnHookCrash(hookSeatExpiredError("postToolUseFailure", hookTimeoutMs, failureSeat.cause, "the observation was abandoned and its additionalContext dropped; the failure result itself is unchanged"));
265
269
  }
@@ -271,7 +275,8 @@ export function prepareGateStations(input) {
271
275
  }
272
276
  }
273
277
  else if (hooks?.postToolUse) {
274
- const successSeat = await runHookSeat("postToolUse", { timeoutMs: hookTimeoutMs, signal: abortController.signal }, (sig) => hooks.postToolUse(e.toolName, e.input, { content: e.content, details: e.details, isError: e.isError }, { toolCallId: e.toolCallId, toolName: e.toolName, signal: sig, ...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}), ...(hookEnvFace !== undefined ? { env: hookEnvFace } : {}), identity: hookIdentity }));
278
+ const ranName = wireOf("hook.postToolUse", post);
279
+ const successSeat = await runHookSeat("postToolUse", { timeoutMs: hookTimeoutMs, signal: abortController.signal }, (sig) => hooks.postToolUse(ranName, e.input, { content: e.content, details: e.details, isError: e.isError }, { toolCallId: e.toolCallId, toolName: ranName, signal: sig, ...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}), ...(hookEnvFace !== undefined ? { env: hookEnvFace } : {}), identity: hookIdentity }));
275
280
  if (successSeat.expired) {
276
281
  notifyOwnHookCrash(hookSeatExpiredError("postToolUse", hookTimeoutMs, successSeat.cause, "the seat was abandoned; the tool result is delivered to the model UNMODIFIED (no updatedOutput, no additionalContext)"));
277
282
  }
@@ -116,6 +116,8 @@ export interface PrepareParkAskInput {
116
116
  shellGatedTools: ReadonlySet<string>;
117
117
  /** borrowed-readonly — the per-tool tier, read only for the name the call's tier seat is read under (as at the gate). */
118
118
  irreversibilityTier: IrreversibilityTier;
119
+ /** borrowed-readonly — who marked each name's seat (the descriptor's `shellGated` reads it as the gate does). */
120
+ toolMarkSources: ReadonlyMap<string, ReadonlySet<import("../ask-origin.js").ToolMarkSource>>;
119
121
  /** borrowed-readonly — the operator's shell floor (`"always"`), persisted on a shell-gated descriptor; undefined = no floor. */
120
122
  effectiveShellGate: ShellGateDoctrine | undefined;
121
123
  /** borrowed-readonly — the EFFECTIVE durable-approval opt-in (the caller's, or the synthesized mandate scope): picks the
@@ -15,7 +15,8 @@ import { checkpointScopeOf } from "./checkpoint-scope.js";
15
15
  import { CONTENT_ASK_BINDING_CAP } from "./content-ask-bindings.js";
16
16
  import { DEFAULT_UNATTENDED_APPROVAL_TTL_MS, publishCommittedSuspend, sanitizedTtlMs } from "./park-commit.js";
17
17
  import { remoteEnvFailureNote } from "./remote-env-retry.js";
18
- import { callFaceSeat, firstNamed, parkedToolIdentity, parkedToolFace, ruleNamesOf } from "../tool-roster.js";
18
+ import { callFaceSeat, parkedToolIdentity, parkedToolFace } from "../tool-roster.js";
19
+ import { seatOf, wireOf } from "../tool-name-readings.js";
19
20
  class ParkRefusal extends Error {
20
21
  constructor(message, options) {
21
22
  super(message, options);
@@ -26,7 +27,7 @@ const wordlessDemand = (toolName) => {
26
27
  throw new Error(`a real-approval demand for "${toolName}" reached the park with no origin word — the origin stamp precedes every park`);
27
28
  };
28
29
  export function prepareParkAsk(input) {
29
- const { askLane, saga, spec, deps, sessionId, checkpointStore, toolRosterDeltas, parkLaneArmed, contentAskRoutable, liveQuestionFace, mountedQuestionTool, contentAskBindings, lateStrandedAnswers, discloseStrandedAnswers, onAsk, runtimeCaps, inheritedUnavailableAsks, basePolicyForResumeEdit, budgetSnapshot, handsCwdRef, advertisedWritableDirs, offloadStore, ownedEnv, incompleteSuspendAdapter, session, sessions, suspendChainBase, maxSuspends, remoteEnvFailures, shellGatedTools, irreversibilityTier, effectiveShellGate, durableApproval, priorLedger, liveSpendRef, resourceTotal, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, ruleOffersOf, now, humanReviewRef, abortController, harness, pausedRef } = input;
30
+ const { askLane, saga, spec, deps, sessionId, checkpointStore, toolRosterDeltas, parkLaneArmed, contentAskRoutable, liveQuestionFace, mountedQuestionTool, contentAskBindings, lateStrandedAnswers, discloseStrandedAnswers, onAsk, runtimeCaps, inheritedUnavailableAsks, basePolicyForResumeEdit, budgetSnapshot, handsCwdRef, advertisedWritableDirs, offloadStore, ownedEnv, incompleteSuspendAdapter, session, sessions, suspendChainBase, maxSuspends, remoteEnvFailures, shellGatedTools, irreversibilityTier, toolMarkSources, effectiveShellGate, durableApproval, priorLedger, liveSpendRef, resourceTotal, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, ruleOffersOf, now, humanReviewRef, abortController, harness, pausedRef } = input;
30
31
  if (askLane === undefined || saga === undefined)
31
32
  return { parkAsk: undefined };
32
33
  const { composedCallSignal, approvalPreviewOf } = askLane;
@@ -114,7 +115,7 @@ export function prepareParkAsk(input) {
114
115
  };
115
116
  const suspendAsk = parkLaneArmed && checkpointStore !== undefined
116
117
  ? async (req, postHookArgs, safety, liveFaceUnavailable, realApproval, shadowedRule, askDecisionReason, probeReason, probeCause, segmentCoverage, matchedAskRule, probeMandated, carry) => {
117
- const syncFirstEligible = req.toolName === ASK_USER_QUESTION_TOOL_NAME ? contentAskRoutable(req.toolCallId) : isLiveApproverSeat(onAsk);
118
+ const syncFirstEligible = wireOf("park.syncFirstRoute", req) === ASK_USER_QUESTION_TOOL_NAME ? contentAskRoutable(req.toolCallId) : isLiveApproverSeat(onAsk);
118
119
  if (syncFirstEligible &&
119
120
  runtimeCaps?.forceDurableGate !== true &&
120
121
  liveFaceUnavailable !== true &&
@@ -234,7 +235,7 @@ export function prepareParkAsk(input) {
234
235
  remoteHandle = parkOnlyHandle(parkOnlyRemoteEnv);
235
236
  }
236
237
  token = mintCheckpointToken();
237
- const tierSeat = firstNamed(irreversibilityTier, ruleNamesOf(req));
238
+ const tierSeat = seatOf("park.riskDescriptor", irreversibilityTier, req);
238
239
  const riskDescriptor = buildRiskDescriptor({
239
240
  toolName: req.toolName,
240
241
  args: parkedArgs,
@@ -242,7 +243,7 @@ export function prepareParkAsk(input) {
242
243
  ...(shadowedRule !== undefined ? { shadowedRule } : {}),
243
244
  ...(probeReason !== undefined ? { probeReason } : {}),
244
245
  ...(probeCause !== undefined ? { probeCause } : {}),
245
- shellGated: tierSeat !== undefined && shellGatedTools.has(tierSeat.name),
246
+ shellGated: tierSeat?.shellGated(shellGatedTools, toolMarkSources) === true,
246
247
  ...(effectiveShellGate !== undefined ? { shellGateDoctrine: effectiveShellGate } : {}),
247
248
  });
248
249
  gate =
@@ -303,7 +304,7 @@ export function prepareParkAsk(input) {
303
304
  kind: "tool_approval",
304
305
  toolCallId: req.toolCallId,
305
306
  toolName: req.toolName,
306
- ...parkedToolIdentity(toolRosterDeltas.current, req.toolName), ...parkedToolFace(toolRosterDeltas.current, req.toolName),
307
+ ...parkedToolIdentity(toolRosterDeltas.current, req.toolName), ...parkedToolFace(toolRosterDeltas.current, req),
307
308
  args: parkedArgs,
308
309
  ...(() => {
309
310
  const p = approvalPreviewOf(req.toolName, parkedArgs);
@@ -85,6 +85,8 @@ export interface PreparePolicyChainInput {
85
85
  envHandToolNames: ReadonlySet<string>;
86
86
  /** borrowed-readonly — the shell-seat provenance set (the offer factory's `shellGated` mandate term, read per call). */
87
87
  shellGatedTools: ReadonlySet<string>;
88
+ /** borrowed-readonly — who marked each name's seat (the offer factory's `shellGated` term reads it as the gate does). */
89
+ toolMarkSources: ReadonlyMap<string, ReadonlySet<import("../ask-origin.js").ToolMarkSource>>;
88
90
  /** borrowed-readonly — the tracked-cwd seat, read per call by the lanes and the offer factory (never written here). */
89
91
  handsCwdRef: CwdRef | undefined;
90
92
  /** borrowed-readonly — the compiled read-deny judge, or undefined (the offer factory's directory negative control). */
@@ -20,9 +20,9 @@ import { askGrantShapeOf } from "./inherited-ask-grants.js";
20
20
  import { createPermissionRuleLanes, inheritedAskRuleEvidence, createRuleOffersOf } from "./permission-rule-lanes.js";
21
21
  import { createLiveSessionRules, createSessionRulePolicy } from "./session-rule-policy.js";
22
22
  import { thrownText } from "../thrown-value.js";
23
- import { anyNamed, firstNamed, ruleNamesOf } from "../tool-roster.js";
23
+ import { heldBy, seatOf, wireOf } from "../tool-name-readings.js";
24
24
  export async function preparePolicyChain(input) {
25
- const { spec, deps, internals, sessionId, runId, hostTaskId, taskRootFinal, additionalRootsCanonical, executionEnv, lockedPreflight, tools, harnessTools, toolEffects, egressTools, irreversibilityTier, axisExplicitNegatives, shellGatedTools, envHandToolNames, handsCwdRef, readDenyMatcher, announceRuleStoreSkipped, questionToolMounted, runtimeCaps, inheritedAncestorRules, ancestorSessionRulesRef, inheritedParentConstraints, seedInheritedGate, hasSkillManifest, skillScope, frozenOnAsk, hookEnvSource, notifyOwnHookCrash, autoModeDecider, permissionMode, scratchpadDir, toolRosterDeltas, stopForDenialLimit, delegation, abortController, now, ownSessionRulesRef } = input;
25
+ const { spec, deps, internals, sessionId, runId, hostTaskId, taskRootFinal, additionalRootsCanonical, executionEnv, lockedPreflight, tools, harnessTools, toolEffects, egressTools, irreversibilityTier, axisExplicitNegatives, shellGatedTools, toolMarkSources, envHandToolNames, handsCwdRef, readDenyMatcher, announceRuleStoreSkipped, questionToolMounted, runtimeCaps, inheritedAncestorRules, ancestorSessionRulesRef, inheritedParentConstraints, seedInheritedGate, hasSkillManifest, skillScope, frozenOnAsk, hookEnvSource, notifyOwnHookCrash, autoModeDecider, permissionMode, scratchpadDir, toolRosterDeltas, stopForDenialLimit, delegation, abortController, now, ownSessionRulesRef } = input;
26
26
  const ancestorArbitrations = new Map();
27
27
  const arbitrateInherited = (ask, toolCallId) => {
28
28
  const verdict = arbitrateByMode(permissionMode.current, classifyAsk(ask, INHERITED_STATION_FACTS), { ...(ask.origin !== undefined ? { origin: ask.origin } : {}), ...(ask.mandate !== undefined ? { mandate: ask.mandate } : {}) });
@@ -233,7 +233,8 @@ export async function preparePolicyChain(input) {
233
233
  list.push({ cls, layer });
234
234
  foldAskClasses.set(toolCallId, list);
235
235
  };
236
- const sandboxBoundaryCapable = (call) => anyNamed(egressTools, ruleNamesOf(call)) || !envHandToolNames.has(call.toolName);
236
+ const isDelegatedQuestion = (creq) => wireOf("inherited.questionFloat", creq) === ASK_USER_QUESTION_TOOL_NAME;
237
+ const sandboxBoundaryCapable = (call) => heldBy("gate.sandboxBoundary.egress", egressTools, call) || !envHandToolNames.has(wireOf("gate.sandboxBoundary.hand", call));
237
238
  const emitSandboxAdmitted = (info) => {
238
239
  emitTrace(deps.tracer, () => ({
239
240
  kind: "permission.sandbox_admitted",
@@ -278,11 +279,10 @@ export async function preparePolicyChain(input) {
278
279
  ...(delegation.isDelegatedChild ? { isDelegatedChild: true } : {}),
279
280
  });
280
281
  const riskAxesOf = (call) => {
281
- const names = ruleNamesOf(call);
282
- const tier = firstNamed(irreversibilityTier, names)?.value;
283
- const negatives = firstNamed(axisExplicitNegatives, names)?.value;
282
+ const tier = seatOf("report.riskAxes", irreversibilityTier, call)?.value;
283
+ const negatives = seatOf("report.riskAxes.negatives", axisExplicitNegatives, call)?.value;
284
284
  const irreversible = tier === "always" || tier === "maybe" ? true : tier === "never" ? false : negatives?.irreversible;
285
- const egress = anyNamed(egressTools, names) ? true : negatives?.egress;
285
+ const egress = heldBy("report.riskAxes.egress", egressTools, call) ? true : negatives?.egress;
286
286
  if (irreversible === undefined && egress === undefined)
287
287
  return {};
288
288
  return { riskAxes: { ...(irreversible !== undefined ? { irreversible } : {}), ...(egress !== undefined ? { egress } : {}) } };
@@ -310,6 +310,7 @@ export async function preparePolicyChain(input) {
310
310
  egressTools,
311
311
  irreversibilityTier,
312
312
  shellGatedTools,
313
+ toolMarkSources,
313
314
  taskRoot: taskRootFinal,
314
315
  cwdRef: handsCwdRef,
315
316
  deniesDirectoryRead: readDenyMatcher !== undefined ? (d) => readDenyMatcher.matchPath(d) !== null : undefined,
@@ -420,7 +421,7 @@ export async function preparePolicyChain(input) {
420
421
  return first;
421
422
  if (first.action === "allow")
422
423
  return { action: "allow" };
423
- if (creq.toolName === ASK_USER_QUESTION_TOOL_NAME) {
424
+ if (isDelegatedQuestion(creq)) {
424
425
  if (pc.contentMandate !== true || markInheritedUnavailable(creq.toolCallId))
425
426
  return first;
426
427
  return {
@@ -493,7 +494,7 @@ export async function preparePolicyChain(input) {
493
494
  }
494
495
  if (decision.action !== "ask")
495
496
  return decision;
496
- if (creq.toolName === ASK_USER_QUESTION_TOOL_NAME) {
497
+ if (isDelegatedQuestion(creq)) {
497
498
  if (pc.contentMandate !== true || markInheritedUnavailable(creq.toolCallId))
498
499
  return decision;
499
500
  return {
@@ -4,6 +4,8 @@ import { callerCardIdOf, offendingResultCard, rosterMemberBoundProblem, schemaKe
4
4
  import { toolFaceProblem } from "./tool-face-overlay.js";
5
5
  import { OFFLOAD_TOOL_NAME } from "../tool-result-store.js";
6
6
  import { PRESENT_PLAN_TOOL_NAME, ENTER_PLAN_MODE_TOOL_NAME } from "../present-plan-tool.js";
7
+ const REPEAT_RISK = { read: 0, idempotent: 1, write: 2 };
8
+ const leastRepeatSafe = (a, b) => (REPEAT_RISK[a] >= REPEAT_RISK[b] ? a : b);
7
9
  export function prepareSafetyScan(input) {
8
10
  const { spec, deps } = input;
9
11
  const toolEffects = new Map();
@@ -120,11 +122,16 @@ export function prepareSafetyScan(input) {
120
122
  }
121
123
  }
122
124
  toolEffects.set(OFFLOAD_TOOL_NAME, "read");
125
+ const claimerEffects = new Map([...claimedAliases].map((a) => [a, toolEffects.get(a) ?? "write"]));
126
+ const recordHandEffect = (name, effect) => {
127
+ const claimed = claimerEffects.get(name);
128
+ toolEffects.set(name, claimed === undefined ? effect : leastRepeatSafe(claimed, effect));
129
+ };
123
130
  if (deps.executionEnvFactory || deps.executionEnv) {
124
131
  for (const [name, effect] of handBandEffects())
125
- toolEffects.set(name, effect);
132
+ recordHandEffect(name, effect);
126
133
  if (spec.handsReadOnly === true)
127
- toolEffects.set("Bash", "read");
134
+ recordHandEffect("Bash", "read");
128
135
  }
129
136
  if (spec.enablePlanMode === true) {
130
137
  toolEffects.set(PRESENT_PLAN_TOOL_NAME, "read");
@@ -740,7 +740,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
740
740
  releaseSignal = () => sig.removeEventListener("abort", onExternalAbort);
741
741
  }
742
742
  }
743
- const { hooks, hookTimeoutMs, preToolUseObservational, ownGatePreToolUse, basePolicyForResumeEdit, denyNarrowingPolicy, policyLayers, effectivePolicy, inheritedUnavailableAsks, inheritedAskGrants, askSourceIdentity, riskAxesOf, ruleOffersOf, permissionRuleLane, permissionRuleOrgLane, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, foldAskClasses, ancestorSandboxAdmissions, ancestorArbitrations } = await preparePolicyChain({ spec, deps, internals, sessionId, runId, hostTaskId, taskRootFinal, additionalRootsCanonical, executionEnv, lockedPreflight, tools, harnessTools, toolEffects, egressTools, irreversibilityTier, axisExplicitNegatives, shellGatedTools, envHandToolNames, handsCwdRef, readDenyMatcher, announceRuleStoreSkipped, questionToolMounted, runtimeCaps, inheritedAncestorRules, ancestorSessionRulesRef, inheritedParentConstraints, seedInheritedGate, hasSkillManifest, skillScope, frozenOnAsk, hookEnvSource, notifyOwnHookCrash, autoModeDecider: () => autoModeDeciderRef.current, permissionMode, scratchpadDir: spec.envFacts?.scratchpadDir, toolRosterDeltas: rosterSeat, stopForDenialLimit, delegation, abortController, now, ownSessionRulesRef });
743
+ const { hooks, hookTimeoutMs, preToolUseObservational, ownGatePreToolUse, basePolicyForResumeEdit, denyNarrowingPolicy, policyLayers, effectivePolicy, inheritedUnavailableAsks, inheritedAskGrants, askSourceIdentity, riskAxesOf, ruleOffersOf, permissionRuleLane, permissionRuleOrgLane, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, foldAskClasses, ancestorSandboxAdmissions, ancestorArbitrations } = await preparePolicyChain({ spec, deps, internals, sessionId, runId, hostTaskId, taskRootFinal, additionalRootsCanonical, executionEnv, lockedPreflight, tools, harnessTools, toolEffects, egressTools, irreversibilityTier, axisExplicitNegatives, shellGatedTools, toolMarkSources, envHandToolNames, handsCwdRef, readDenyMatcher, announceRuleStoreSkipped, questionToolMounted, runtimeCaps, inheritedAncestorRules, ancestorSessionRulesRef, inheritedParentConstraints, seedInheritedGate, hasSkillManifest, skillScope, frozenOnAsk, hookEnvSource, notifyOwnHookCrash, autoModeDecider: () => autoModeDeciderRef.current, permissionMode, scratchpadDir: spec.envFacts?.scratchpadDir, toolRosterDeltas: rosterSeat, stopForDenialLimit, delegation, abortController, now, ownSessionRulesRef });
744
744
  const { onAsk, preToolContexts, blockedToolCalls, gateOutcomes, batchHaltRef, blockedTracked, incompleteSuspendAdapter, durableSuspendInfraReady, resourceSuspendEligible, usageGovernance, durableApproval, wiringManifest, writeProtection, parkLaneArmed, gateMachineryActive, hookIdentity, hookEnvFace, sessionBoxBound } = prepareWiringManifest({ spec, deps, internals, resume, sessionId, runId, hostTaskId, taskScope, hooks, preToolUseObservational, policyLayers, effectivePolicy, handsEnabled, taskRootFinal, mcp, irreversibleTools, egressTools, shellReadStationArmed: handsReadFace.shellReadStationArmed, toolEffects, ownedEnv, executionEnv, offloadStore, onceLedger, usageWindows, envLifetimeSuspendAt, runtimeCaps, resolvedInteractionPosture, durableQuestionFace, liveQuestionFace, checkpointStore, sessions, modelGateManifest, autoModeArmReason, permissionMode, lockedPreflight, peerLaneRefs, frameSettlement, harness, pausedRef, frozenOnAsk, liveInheritedGate, delegation, toolRoster, readDenyBuiltinTiersActive: handsReadFace.readDenyBuiltinTiersActive, lspManagerWired, lspDiagnosticsLive: lspDiagnostics !== undefined, fileHistoryFact, sessionBox });
745
745
  const writeProtectionCheck = writeProtection?.check;
746
746
  writeProtectionJudgeRef.current = writeProtection?.judgeTarget;
@@ -753,10 +753,10 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
753
753
  const { askLane } = prepareAskLane({ gateMachineryActive, abortController, effectivePolicy, budgetSnapshot, handsCwdRef, advertisedWritableDirs, tools, inheritedUnavailableAsks, inheritedAskGrants, onAsk, humanReviewRef, now, ruleOffersOf, askSourceIdentity, riskAxesOf, autoModeDenialTracking: autoModeDenialTrackingRef, spec, deps, sessionId, runId, hooks, hookTimeoutMs, notifyOwnHookCrash, grantReadRoot: handsReadFace.grantReadRoot });
754
754
  const { saga } = prepareSuspendSaga({ gateMachineryActive, abortController, harness, checkpointStore, deps, sessionId, hostTaskId, taskScope, liveSpendRef, resume, nestedStats, internals, activeTools, outputRef, readFileStateForCheckpoint, faceCheckpointSection, writeFaceCheckpointSection, reminderMark, handsCwdRef, worktreeSessionRef, undeliveredContinuationRef, inheritedParentConstraints, seedInheritedGate, ancestorRulesNow, inheritedShellGate, permissionMode, inheritedAdmittedOrgScopes, ownOrgVerdictRef, orgGovernedProvenance, announcedListingsRef, gitStatusRef, hookIdentity, placementRootResolved, externalContentTargetActive, remoteEnvFailures, memoryEngineSession, suspendLoopRef, ownedEnv, incompleteSuspendAdapter });
755
755
  const { suspendForResource, suspendForPlatformLimit, suspendForReview } = prepareBoundaryParks({ saga, spec, checkpointStore, abortController, harness, session, sessions, sessionId, deps, priorLedger, maxSlices, maxSuspends, resourceTotal, priorSuspendCount, suspendChainBase, humanReviewRef, liveSpendRef, now, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, pausedRef, remoteEnvFailures, resourceSuspendEligible, durableSuspendInfraReady, incompleteSuspendAdapter });
756
- const { parkAsk } = prepareParkAsk({ askLane, saga, spec, deps, sessionId, checkpointStore, toolRosterDeltas: rosterSeat, parkLaneArmed, contentAskRoutable, liveQuestionFace, mountedQuestionTool, contentAskBindings, lateStrandedAnswers, discloseStrandedAnswers, onAsk, runtimeCaps, inheritedUnavailableAsks, basePolicyForResumeEdit, budgetSnapshot, handsCwdRef, advertisedWritableDirs, offloadStore, ownedEnv, incompleteSuspendAdapter, session, sessions, suspendChainBase, maxSuspends, remoteEnvFailures, shellGatedTools, irreversibilityTier, effectiveShellGate, durableApproval, priorLedger, liveSpendRef, resourceTotal, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, ruleOffersOf, now, humanReviewRef, abortController, harness, pausedRef });
756
+ const { parkAsk } = prepareParkAsk({ askLane, saga, spec, deps, sessionId, checkpointStore, toolRosterDeltas: rosterSeat, parkLaneArmed, contentAskRoutable, liveQuestionFace, mountedQuestionTool, contentAskBindings, lateStrandedAnswers, discloseStrandedAnswers, onAsk, runtimeCaps, inheritedUnavailableAsks, basePolicyForResumeEdit, budgetSnapshot, handsCwdRef, advertisedWritableDirs, offloadStore, ownedEnv, incompleteSuspendAdapter, session, sessions, suspendChainBase, maxSuspends, remoteEnvFailures, shellGatedTools, irreversibilityTier, toolMarkSources, effectiveShellGate, durableApproval, priorLedger, liveSpendRef, resourceTotal, faceCheckpointState, f012CheckpointState, orgAdmissionCheckpointState, ruleOffersOf, now, humanReviewRef, abortController, harness, pausedRef });
757
757
  prepareGateStations({ askLane, parkAsk, tools, toolRosterDeltas: rosterSeat, toolEffects, deps, toolCallGateArmedRef, effectivePolicy, hooks, egressTools, irreversibleTools, spec, complianceDenies, harness, blockedToolCalls, inheritedAskGrants, inheritedUnavailableAsks, foldAskClasses, ancestorSandboxAdmissions, preToolContexts, gateOutcomes, batchHaltRef, blockedTracked, hookIdentity, reminderMark, hostTaskId, sessionId, ownGatePreToolUse, hookTimeoutMs, handsCwdRef, readDenyInForce: () => ({ tiers: handsReadFace.readDenyBuiltinTiersActive, rows: [...readDenyAdditionsNormalized.map((e) => e.pattern), ...handsReadFaceInput.pathDenyRules.filter((r) => r.faces.includes("read")).map((r) => r.rule)] }), hookEnvFace, writeProtectionCheck, shellReadCheck, irreversibilityTier, reversibilityProbes, abortController, shellGatedTools, autoModeDecider: autoModeDeciderRef, autoModeDenialTracking: autoModeDenialTrackingRef, askRoute: wiringManifest.ask, permissionMode, toolMarkSources, ...(deps.a2a?.isolatePeers === true ? { a2aIsolatePeers: true } : {}), ancestorArbitrations, runId, stopForDenialLimit, permissionRuleLane, permissionRuleOrgLane, questionToolMounted, sandboxAdmissionArmed, sandboxBoundaryCapable, emitSandboxAdmitted, delegation, notifyOwnHookCrash, frameSettlement });
758
758
  const postureFrameRef = {};
759
- const { microCompact, charsPerToken } = prepareContextLane({ model, deps, sessionId, runId, hostTaskId, offloadStore, microCompactKnob, harness, batchHaltRef, gitStatusRef, postureFrameRef, requestLossyRef, trimPressureRef, contextOverlay: legContextOverlay, ...(readFileStateForCheckpoint !== undefined ? { readFileState: readFileStateForCheckpoint } : {}) });
759
+ const { microCompact, charsPerToken } = prepareContextLane({ model, deps, sessionId, runId, hostTaskId, offloadStore, toolRosterDeltas: rosterSeat, microCompactKnob, harness, batchHaltRef, gitStatusRef, postureFrameRef, requestLossyRef, trimPressureRef, contextOverlay: legContextOverlay, ...(readFileStateForCheckpoint !== undefined ? { readFileState: readFileStateForCheckpoint } : {}) });
760
760
  const preparedHolder = {};
761
761
  const { cacheBreakDetector, cacheFingerprint, promptOverheadTokens, readTaskFile, normalizeAttachmentPath, isDedupStubResult, recentlyReadFiles, onCompactionApplied, detectExternalChanges, listBackgroundTasks, centerCompactionCandidate, effectiveReadFaceObserved, effectiveWriteFaceObserved, effectiveReadDenyObserved } = prepareTurnWiring({ deps, systemPromptSeat, harnessTools, fpRef, model, epochArtifactDigestForSnapshot, promptProfile, fableMitigations, systemBlocks, thinking, spec, sessionId, turnSnapshotRef, promptManifest, charsPerToken, handsEnabled, executionEnv, attachmentRootCanonical, additionalRootsCanonical, additionalReadRootsCanonical, readDenyMatcher, resolvedReadFace, handsCwdRef, readFileStateForCheckpoint, denyNarrowingPolicy, abortController, hostTaskId, taskScope, buildAssembleInputs, centerAdoptionRef, harnessRef, epochDeclaredSections, carrierReadFace, carrierWriteFace, readDenyAdditionsNormalized, preparedHolder });
762
762
  await onceLedger.settle(session, announcedListingsRef);
@@ -10,6 +10,7 @@ import { inlineUntrusted } from "../untrusted-text.js";
10
10
  import { deepJsonEqual, describeSuppliedValue } from "./decide-continuation.js";
11
11
  import { mintGateOutcome } from "./gate-exit.js";
12
12
  import { asThrownError, thrownText } from "../thrown-value.js";
13
+ import { wireOf } from "../tool-name-readings.js";
13
14
  export async function resumeApply(input) {
14
15
  const { cp, config, internals, outcomeGate, wakeMessage, resumeSignal, recheckGovernanceWindow, plainReviewOutcome, plainParkOutcome, runner, effectiveDelegationFacts } = input;
15
16
  let plainPolicyOutcome = input.plainPolicyOutcome;
@@ -78,7 +79,7 @@ export async function resumeApply(input) {
78
79
  plainPolicyOutcome.decision === "allow" &&
79
80
  plainPolicyOutcome.updatedInput !== undefined &&
80
81
  cp.pendingAction.kind === "tool_approval" &&
81
- cp.pendingAction.toolName !== ASK_USER_QUESTION_TOOL_NAME) {
82
+ wireOf("resume.editRecheckQuestionSkip", cp.pendingAction) !== ASK_USER_QUESTION_TOOL_NAME) {
82
83
  const editedArgs = plainPolicyOutcome.updatedInput;
83
84
  const editBudget = {
84
85
  resourceRemainingMicroUsd: remainingBudgetMicroUsd(cp.resourceLedger),
@@ -2,6 +2,8 @@ import { eventDefaultOn } from "../../prompt-assembly/event-registry.js";
2
2
  import { stripGitStatusUnits } from "./git-status-frame.js";
3
3
  import { SKILLS_LISTING_PROBE_HEADER } from "./synthetic-tools.js";
4
4
  import { AGENT_LISTING_REMOVED_HEADER, SKILLS_LISTING_DELTA_HEADER, SKILLS_LISTING_REMOVED_HEADER, agentListingDeltaHeader, agentListingInitialHeader, createAttachmentState, replayAnnouncedListing, replayAnnouncedModels } from "./turn-attachments.js";
5
+ import { wireOf } from "../tool-name-readings.js";
6
+ import { rosterToolName } from "../tool-roster.js";
5
7
  export async function runAttachmentSeats(input) {
6
8
  const { spec, prepared, resume, rs, runner, next } = input;
7
9
  rs.counters.wroteThisRun = false;
@@ -174,7 +176,7 @@ export async function runAttachmentSeats(input) {
174
176
  if ((fvMarked || text.includes("<system-reminder>[final verification]")) && rs.counters.finalVerifyInjections < 2)
175
177
  rs.counters.finalVerifyInjections += 1;
176
178
  }
177
- else if (m.role === "toolResult" && !rs.counters.wroteThisRun && (prepared.toolEffects.get(m.toolName) ?? "write") !== "read") {
179
+ else if (m.role === "toolResult" && !rs.counters.wroteThisRun && (prepared.toolEffects.get(wireOf("verify.wroteThisRunSeed", { toolName: rosterToolName(prepared.toolRosterDeltas.current, m.toolName) })) ?? "write") !== "read") {
178
180
  rs.counters.wroteThisRun = true;
179
181
  }
180
182
  }
@@ -11,9 +11,13 @@ import { gatedCallIdOf } from "./park-commit.js";
11
11
  import { hasVerifiableStructureSignal } from "./grounding-signal.js";
12
12
  import { batchResponseDigest, toolEndBodyFrom } from "./tool-end-body.js";
13
13
  import { createResponseModelSeat } from "./response-model-seat.js";
14
+ import { createToolCallIdFoldSeat } from "./tool-call-id-fold-seat.js";
15
+ import { wireOf } from "../tool-name-readings.js";
16
+ import { rosterToolName } from "../tool-roster.js";
14
17
  export function createHarnessHandlers(input) {
15
18
  const { prepared, stats, rs, deps } = input;
16
19
  const responseModelSeat = createResponseModelSeat(deps.runnerHooks.onNotice, { sessionId: prepared.sessionId, runId: prepared.runId });
20
+ const toolCallIdFoldSeat = createToolCallIdFoldSeat(deps.runnerHooks.onNotice, { sessionId: prepared.sessionId, runId: prepared.runId });
17
21
  const { spec, queue, internals, ident, parentToolCallId, subagentName, pushContent, emitCommitted, startedToolCallIds, toolStartAt, toolProgress, writeFamilyOf, toolLabels, postToolBatchHook, batchArgs, internalsNotifier } = deps;
18
22
  let lastWorkspaceCwd = prepared.cwdRef?.current;
19
23
  const announceWorkspaceMove = () => {
@@ -147,6 +151,7 @@ export function createHarnessHandlers(input) {
147
151
  }
148
152
  if (m.role === "assistant") {
149
153
  rs.turn.turnResponseModel = responseModelSeat.observe(m);
154
+ toolCallIdFoldSeat.observe(m);
150
155
  }
151
156
  if (m.role === "assistant") {
152
157
  const rep = m.repetition;
@@ -219,7 +224,7 @@ export function createHarnessHandlers(input) {
219
224
  toolStartAt.delete(event.toolCallId);
220
225
  toolProgress.noteEnd(event.toolCallId);
221
226
  const toolNow = Date.now();
222
- if (!rs.counters.wroteThisRun && (prepared.toolEffects.get(event.toolName) ?? "write") !== "read") {
227
+ if (!rs.counters.wroteThisRun && (prepared.toolEffects.get(wireOf("verify.wroteThisRun", { toolName: rosterToolName(prepared.toolRosterDeltas.current, event.toolName) })) ?? "write") !== "read") {
223
228
  rs.counters.wroteThisRun = true;
224
229
  }
225
230
  if (rs.attach.attachState !== undefined && !event.isError) {
@@ -13,7 +13,7 @@ import { composeApprovalPostureDirective } from "../auto-mode-directive.js";
13
13
  import { mintSystemReminder, statedReminderBodiesOf } from "../reminder-mint.js";
14
14
  import { hasDestroy } from "../remote-env.js";
15
15
  import { advertisedWritableRootsSeat, policyRefusalMessage, refuseOutOfContractDecision } from "../tool-policy.js";
16
- import { callFaceSeat, judgeParkedToolIdentity, rosterEntryNamed, toolCallFaceOf } from "../tool-roster.js";
16
+ import { callFaceSeat, judgeParkedToolIdentity, parkedRowLiveFace, rosterEntryNamed } from "../tool-roster.js";
17
17
  import { deliverEngineNotice } from "../types.js";
18
18
  import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY, sanitizeUntrustedText } from "../untrusted-text.js";
19
19
  import { errorCodeOf } from "./assemble-result.js";
@@ -34,6 +34,7 @@ import { attachmentEnvelopeTags, collectDateChange, commitAgentListing, commitSk
34
34
  import { flushOwedAnnouncedSnapshot } from "./announce-once-ledger.js";
35
35
  import { asThrownError, thrownMarksOf, thrownText } from "../thrown-value.js";
36
36
  import { ruleSourcePhrase } from "../rule-denial-sentence.js";
37
+ import { wireOf } from "../tool-name-readings.js";
37
38
  export async function runLeg(input) {
38
39
  const { spec, queue, prepared, resume, rs, isDelegatedChild, entryActor, effectiveTimeoutMs, timeout, pushContent, ident, emitCommitted, writeFamilyOf, startedToolCallIds, announceWorkspaceMove, withBrainSinks, flushGitMirror, loopLatch, notificationLane, unsubscribeTaskNotifications, unsubGitRetry, unsubBoundary, unsub, runner, next } = input;
39
40
  let final;
@@ -563,12 +564,13 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
563
564
  if (decidedGate === undefined) {
564
565
  throw new CheckpointError("checkpoint.invalid_outcome", "pending action reached the resolver with no settlement record on its decision (gateOutcome) — refusing to execute or settle a decision that carries no record");
565
566
  }
566
- const resolvedArgs = pendingAction.toolName === ASK_USER_QUESTION_TOOL_NAME ? pendingAction.args : (outcome.updatedInput !== undefined ? outcome.updatedInput : pendingAction.args);
567
+ const resolvedArgs = wireOf("resume.questionArgsShown", pendingAction) === ASK_USER_QUESTION_TOOL_NAME ? pendingAction.args : (outcome.updatedInput !== undefined ? outcome.updatedInput : pendingAction.args);
567
568
  const pendingLabel = (() => { const l = prepared.tools.find((t) => t.name === pendingAction.toolName)?.label; return l !== undefined && l !== pendingAction.toolName ? { label: l } : {}; })();
568
- emit({ type: "tool_start", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, args: resolvedArgs });
569
+ const spelledName = pendingAction.names?.[0] ?? pendingAction.toolName;
570
+ emit({ type: "tool_start", toolCallId: pendingAction.toolCallId, toolName: spelledName, ...pendingLabel, args: resolvedArgs });
569
571
  const settleEnd = async (isError, result, gate = decidedGate) => {
570
572
  prepared.toolProgressRef.current?.noteEnd(pendingAction.toolCallId);
571
- emit({ type: "tool_end", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, isError, ...toolEndBodyFrom(result, isError, prepared.structuredProjector, gate) });
573
+ emit({ type: "tool_end", toolCallId: pendingAction.toolCallId, toolName: spelledName, ...pendingLabel, isError, ...toolEndBodyFrom(result, isError, prepared.structuredProjector, gate) });
572
574
  const word = await resume.recordExecutionOutcome(gate);
573
575
  settleExecutionRecord(runner.deps.onNotice, word, { toolName: pendingAction.toolName, gate, sessionId: prepared.sessionId, runId: prepared.runId, scope: resume.cp.scope, ...(resume.cp.checkpointId !== undefined ? { checkpointId: resume.cp.checkpointId } : {}) });
574
576
  };
@@ -611,7 +613,7 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
611
613
  budget: beltBudget,
612
614
  ...(prepared.cwdRef !== undefined ? { cwd: prepared.cwdRef.current } : {}),
613
615
  ...advertisedWritableRootsSeat(prepared.advertisedWritableDirs),
614
- ...callFaceSeat(toolCallFaceOf(prepared.toolRosterDeltas.current, pendingAction.toolName)),
616
+ ...callFaceSeat(parkedRowLiveFace(prepared.toolRosterDeltas.current, pendingAction)),
615
617
  }, prepared.abortController.signal));
616
618
  const beltUnsafe = rechecked.action === "deny" ||
617
619
  (rechecked.updatedInput !== undefined && !deepJsonEqual(rechecked.updatedInput, resolvedArgs));
@@ -634,7 +636,7 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
634
636
  }
635
637
  if (prepared.permissionRuleOrg !== undefined) {
636
638
  const orgVerdict = prepared.permissionRuleOrg
637
- .adjudicate({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId, ...callFaceSeat(toolCallFaceOf(prepared.toolRosterDeltas.current, pendingAction.toolName)) })
639
+ .adjudicate({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId, ...callFaceSeat(parkedRowLiveFace(prepared.toolRosterDeltas.current, pendingAction)) })
638
640
  .catch(() => ({ status: "unavailable", disclosures: ["the org adjudication face threw on resume"] }));
639
641
  let orgWaitCancelled = false;
640
642
  const org = await settleOrgVerdictWithin(orgVerdict, { status: "unavailable", disclosures: [`the org adjudication face did not answer within ${ORG_ADJUDICATION_TIMEOUT_MS}ms (or the task ended first)`] }, {
@@ -669,7 +671,7 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
669
671
  toolName: pendingAction.toolName,
670
672
  args: resolvedArgs,
671
673
  toolCallId: pendingAction.toolCallId,
672
- ...callFaceSeat(toolCallFaceOf(prepared.toolRosterDeltas.current, pendingAction.toolName)),
674
+ ...callFaceSeat(parkedRowLiveFace(prepared.toolRosterDeltas.current, pendingAction)),
673
675
  budget: beltBudget,
674
676
  ...(prepared.cwdRef !== undefined ? { cwd: prepared.cwdRef.current } : {}),
675
677
  ...advertisedWritableRootsSeat(prepared.advertisedWritableDirs),
@@ -691,7 +693,7 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
691
693
  toolName: pendingAction.toolName,
692
694
  args: resolvedArgs,
693
695
  toolCallId: pendingAction.toolCallId,
694
- ...callFaceSeat(toolCallFaceOf(prepared.toolRosterDeltas.current, pendingAction.toolName)),
696
+ ...callFaceSeat(parkedRowLiveFace(prepared.toolRosterDeltas.current, pendingAction)),
695
697
  budget: beltBudget,
696
698
  ...(prepared.cwdRef !== undefined ? { cwd: prepared.cwdRef.current } : {}),
697
699
  ...advertisedWritableRootsSeat(prepared.advertisedWritableDirs),
@@ -717,7 +719,7 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
717
719
  }
718
720
  if (prepared.shellReadCheck !== undefined) {
719
721
  const edited = outcome.updatedInput !== undefined && !deepJsonEqual(outcome.updatedInput, pendingAction.args);
720
- const readCall = { toolName: pendingAction.toolName, ...callFaceSeat(toolCallFaceOf(prepared.toolRosterDeltas.current, pendingAction.toolName)) };
722
+ const readCall = { toolName: pendingAction.toolName, ...callFaceSeat(parkedRowLiveFace(prepared.toolRosterDeltas.current, pendingAction)) };
721
723
  const judged = prepared.shellReadCheck.judge(readCall, resolvedArgs, prepared.cwdRef?.current, prepared.abortController.signal, {
722
724
  toolCallId: pendingAction.toolCallId,
723
725
  ...(edited ? { seat: "rejudge" } : {}),
@@ -1,5 +1,6 @@
1
1
  import { compactionKindOf } from "../task-notification.js";
2
2
  import { planRejectionClears } from "../context-edit.js";
3
+ import { rosterToolName } from "../tool-roster.js";
3
4
  import { landStaleResultClears } from "./prepare-context-lane.js";
4
5
  import { DEFAULT_COMPACTION_INSTRUCTIONS, maybeCompact, mintCompactedEvent } from "../auto-compaction.js";
5
6
  import { emitTrace } from "../trace.js";
@@ -122,6 +123,7 @@ export function runRecoveryLanes(input) {
122
123
  ? { declined: "no_candidates" }
123
124
  : planRejectionClears(proj.messages, {
124
125
  keyOf: proj.keyOf,
126
+ toolNameOf: (recorded) => rosterToolName(prepared.toolRosterDeltas.current, recorded),
125
127
  ...(prepared.microCompact.offloadPersist ? { offload: { persist: prepared.microCompact.offloadPersist } } : {}),
126
128
  });
127
129
  if ("declined" in plan) {
@@ -2,7 +2,7 @@ import { canonicalizeTarget, isWinFormPath, pathFamilyOf, writeTargetPath } from
2
2
  import { createCoarseCommandNamePolicy, namespacedCoveringEntries, namespacedCoveringHit } from "../tool-policy.js";
3
3
  import { continuesLineage } from "../session-policy-store.js";
4
4
  import { thrownText } from "../thrown-value.js";
5
- import { toolNameSetHas } from "../tool-roster.js";
5
+ import { heldBy, wireOf } from "../tool-name-readings.js";
6
6
  import { ruleDenialSentence } from "../rule-denial-sentence.js";
7
7
  import { DEFAULT_GATE_AWAIT_TIMEOUT_MS, gateWaitFailureOf, withGateTimeout } from "../gate-bounded-await.js";
8
8
  import { declaredPathTargetOf, isProtectedWrite, pathConfinableWriteToolNames, skillScopeWriteToolNames } from "../tool-registry.js";
@@ -137,9 +137,10 @@ export function createSessionRulePolicy(rules, opts) {
137
137
  async check(req, signal) {
138
138
  const { toolDeny, toolAllow, toolDenyCovering, toolAllowCovering, cmdPolicy, allowDirs } = compile(await readRules(req.toolCallId, signal));
139
139
  const toolName = req.toolName;
140
- if (toolNameSetHas(toolDeny, req) || namespacedCoveringHit(toolDenyCovering, toolName))
140
+ const listed = (set) => heldBy("policy.sessionRules", set, req);
141
+ if (listed(toolDeny) || namespacedCoveringHit(toolDenyCovering, toolName))
141
142
  return deny(req.toolName);
142
- if (toolAllow && !toolNameSetHas(toolAllow, req) && !namespacedCoveringHit(toolAllowCovering, toolName)) {
143
+ if (toolAllow && !listed(toolAllow) && !namespacedCoveringHit(toolAllowCovering, toolName)) {
143
144
  return deny(req.toolName, "it is not in the session-rule allowlist");
144
145
  }
145
146
  if (cmdPolicy) {
@@ -151,7 +152,7 @@ export function createSessionRulePolicy(rules, opts) {
151
152
  }
152
153
  if (allowDirs) {
153
154
  if (!isProtectedWrite(req)) {
154
- const eff = toolEffects?.get(toolName) ?? "write";
155
+ const eff = toolEffects?.get(wireOf("policy.sessionRules.effect", req)) ?? "write";
155
156
  if (eff !== "read") {
156
157
  return deny(req.toolName, "it is write-capable and cannot be path-confined, while the rule confines writes to allowDirs");
157
158
  }
@@ -0,0 +1,13 @@
1
+ import type { EngineNotice } from "../engine-notice.js";
2
+ export interface ToolCallIdFoldSeat {
3
+ /** Observe one assistant message; announces the run's first folded call. */
4
+ observe(message: {
5
+ readonly content?: unknown;
6
+ }): void;
7
+ }
8
+ /** The notice's attribution ids — the same pair, for the same reason, as {@link ResponseModelSeatIds}. */
9
+ export interface ToolCallIdFoldSeatIds {
10
+ sessionId: string;
11
+ runId: string;
12
+ }
13
+ export declare function createToolCallIdFoldSeat(onNotice: ((notice: EngineNotice) => void) | undefined, ids: ToolCallIdFoldSeatIds): ToolCallIdFoldSeat;
@@ -0,0 +1,31 @@
1
+ import { deliverEngineNotice } from "../engine-notice.js";
2
+ import { providerToolCallId, toolCallIdFoldReason } from "../../brain/tool-call-id.js";
3
+ function isCall(block) {
4
+ return typeof block === "object" && block !== null && block.type === "toolCall" && typeof block.id === "string";
5
+ }
6
+ export function createToolCallIdFoldSeat(onNotice, ids) {
7
+ let announced = false;
8
+ return {
9
+ observe(message) {
10
+ if (announced || !Array.isArray(message.content))
11
+ return;
12
+ for (const call of message.content.filter(isCall)) {
13
+ const wireId = providerToolCallId(call);
14
+ const reason = wireId === call.id ? undefined : toolCallIdFoldReason(wireId);
15
+ if (reason === undefined)
16
+ continue;
17
+ announced = true;
18
+ deliverEngineNotice(onNotice, {
19
+ code: "route.tool_call_id_folded",
20
+ message: `The provider named a tool call with an id outside the shape the engine keys calls under ` +
21
+ `(${reason === "length" ? `${wireId.length} characters, over 128` : "a control byte, a space, a non-ASCII character or a `~`"}). ` +
22
+ `The engine keys it as "${call.id}" on every face — frames, cards, transcript, park rows — and hands the ` +
23
+ `provider its own id back for the call and its result, so the run is unchanged. Once per run; later calls ` +
24
+ `with such ids are folded the same way without another notice.`,
25
+ detail: { toolCallId: call.id, rawLength: wireId.length, reason, sessionId: ids.sessionId, runId: ids.runId },
26
+ });
27
+ return;
28
+ }
29
+ },
30
+ };
31
+ }
@@ -5,7 +5,7 @@ import { compileSegmentPattern, matchSegmentPatterns, READ_FACE_BUILTIN_DENY_TAB
5
5
  import { isProtectedWrite, protectivePathTargetOf } from "./tool-registry.js";
6
6
  import { pathTargetBaseOf } from "./effective-path-target.js";
7
7
  import { symlinkChainHops } from "./path-identity.js";
8
- import { toolNameSetHas } from "./tool-roster.js";
8
+ import { heldBy } from "./tool-name-readings.js";
9
9
  export function agentConfigDenyPatterns() {
10
10
  return READ_FACE_BUILTIN_DENY_TABLE.filter((row) => row.tier === "agent-config").map((row) => readDenyPatternRun(row.pattern).run);
11
11
  }
@@ -147,7 +147,7 @@ export function createSensitivePathPolicy(opts) {
147
147
  async check(req, signal) {
148
148
  if (compiled.length === 0)
149
149
  return { action: "allow" };
150
- if (guardedByName !== undefined ? !toolNameSetHas(guardedByName, req) : !isProtectedWrite(req))
150
+ if (guardedByName !== undefined ? !heldBy("policy.sensitivePathGuardedByName", guardedByName, req) : !isProtectedWrite(req))
151
151
  return { action: "allow" };
152
152
  const protective = protectivePathTargetOf(req);
153
153
  const path = writeTargetPath(req, protective);
@@ -76,9 +76,13 @@ export interface SideQuerySpec {
76
76
  export interface SideQueryResult {
77
77
  /** Concatenated assistant text (may be empty when the reply is tool-calls-only). */
78
78
  text: string;
79
- /** Tool calls the model made against the DEFINITIONS (never executed by the engine). */
79
+ /** Tool calls the model made against the DEFINITIONS (never executed by the engine). `id` is the engine id;
80
+ * `wireId` is the provider's own spelling, present only when the two differ (`ToolCall.wireId`, the same
81
+ * source). A caller replaying a call carries both on its `toolCall` block and keys the result by `id`; one that
82
+ * talks to the provider itself hands it `wireId ?? id`. */
80
83
  toolCalls: Array<{
81
84
  id: string;
85
+ wireId?: string;
82
86
  name: string;
83
87
  args: unknown;
84
88
  }>;
@@ -2,6 +2,7 @@ import { resolveTaskModel } from "./roles.js";
2
2
  import { readDegradation } from "../brain/degrading.js";
3
3
  import { cacheFamilyOf, uncachedInputTokensOf } from "./runner/usage-accounting.js";
4
4
  import { providerReportedModel } from "./message-utils.js";
5
+ import { providerToolCallId } from "../brain/tool-call-id.js";
5
6
  function normalizeSideQueryUsage(model, u) {
6
7
  const input = uncachedInputTokensOf(cacheFamilyOf(model), u);
7
8
  return input === u.input ? u : { ...u, input };
@@ -61,7 +62,10 @@ export async function runSideQuery(spec, deps) {
61
62
  .join("");
62
63
  const toolCalls = msg.content
63
64
  .filter((c) => c.type === "toolCall")
64
- .map((c) => ({ id: c.id, name: c.name, args: c.arguments }));
65
+ .map((c) => {
66
+ const wireId = providerToolCallId(c);
67
+ return { id: c.id, ...(wireId !== c.id ? { wireId } : {}), name: c.name, args: c.arguments };
68
+ });
65
69
  const degraded = readDegradation(msg);
66
70
  const servedModel = typeof msg.model === "string" && msg.model.length > 0 ? msg.model : resolved.model.id;
67
71
  const reportedModel = providerReportedModel(msg);