@sema-agent/core 7.17.7 → 7.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (201) hide show
  1. package/CHANGELOG.md +233 -13
  2. package/dist/agents/send-message-tool.js +1 -0
  3. package/dist/agents/subagent.d.ts +1 -0
  4. package/dist/agents/subagent.js +5 -0
  5. package/dist/agents/teacher.js +1 -0
  6. package/dist/agents/verify.js +3 -0
  7. package/dist/brain/anthropic.js +6 -3
  8. package/dist/brain/open-responses.js +10 -4
  9. package/dist/brain/openai.js +5 -2
  10. package/dist/brain/reasoning.d.ts +13 -13
  11. package/dist/brain/reasoning.js +2 -2
  12. package/dist/brain/route-adjudicator.d.ts +18 -0
  13. package/dist/brain/route-adjudicator.js +16 -0
  14. package/dist/brain/stream-engine.js +2 -1
  15. package/dist/brain/stream-shared.d.ts +3 -0
  16. package/dist/brain/stream-shared.js +1 -0
  17. package/dist/core/agent-definition.d.ts +10 -0
  18. package/dist/core/artifact-host.d.ts +41 -4
  19. package/dist/core/artifact-host.js +18 -2
  20. package/dist/core/ask-origin.d.ts +16 -0
  21. package/dist/core/ask-origin.js +8 -0
  22. package/dist/core/auto-mode-arming.d.ts +6 -0
  23. package/dist/core/auto-mode-arming.js +5 -5
  24. package/dist/core/auto-mode-defaults.d.ts +43 -15
  25. package/dist/core/auto-mode-defaults.js +4 -1
  26. package/dist/core/auto-mode.d.ts +184 -59
  27. package/dist/core/auto-mode.js +188 -13
  28. package/dist/core/checkpoint-store.d.ts +23 -2
  29. package/dist/core/checkpoint-store.js +2 -2
  30. package/dist/core/engine-notice.d.ts +77 -1
  31. package/dist/core/engine-notice.js +7 -0
  32. package/dist/core/gate-fold.js +5 -1
  33. package/dist/core/gate-lanes.js +9 -8
  34. package/dist/core/gate-outcome.d.ts +42 -2
  35. package/dist/core/gate-outcome.js +23 -0
  36. package/dist/core/governance-codes.d.ts +1 -1
  37. package/dist/core/governance-codes.js +14 -0
  38. package/dist/core/hook-seats.d.ts +66 -0
  39. package/dist/core/hook-seats.js +37 -0
  40. package/dist/core/hooks.d.ts +1 -1
  41. package/dist/core/read-root-candidate.d.ts +12 -0
  42. package/dist/core/read-root-candidate.js +24 -0
  43. package/dist/core/roles.d.ts +9 -1
  44. package/dist/core/roles.js +4 -0
  45. package/dist/core/runner/advertised-writable-dirs.d.ts +8 -3
  46. package/dist/core/runner/assemble-result.d.ts +2 -1
  47. package/dist/core/runner/assemble-result.js +1 -1
  48. package/dist/core/runner/classifier-ladder.d.ts +227 -0
  49. package/dist/core/runner/classifier-ladder.js +214 -0
  50. package/dist/core/runner/contracts.d.ts +71 -13
  51. package/dist/core/runner/decide-continuation.d.ts +1 -3
  52. package/dist/core/runner/decide-continuation.js +1 -7
  53. package/dist/core/runner/deferral-census.d.ts +52 -0
  54. package/dist/core/runner/deferral-census.js +44 -0
  55. package/dist/core/runner/denial-limit-arms.js +2 -2
  56. package/dist/core/runner/derived-route-fallback.d.ts +8 -4
  57. package/dist/core/runner/derived-route-fallback.js +8 -3
  58. package/dist/core/runner/gate-exit.d.ts +12 -0
  59. package/dist/core/runner/gate-exit.js +6 -3
  60. package/dist/core/runner/prepare-artifact.d.ts +10 -2
  61. package/dist/core/runner/prepare-artifact.js +5 -4
  62. package/dist/core/runner/prepare-ask-lane.js +1 -0
  63. package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -2
  64. package/dist/core/runner/prepare-caps-and-workflow.js +43 -55
  65. package/dist/core/runner/prepare-config-doors.d.ts +7 -1
  66. package/dist/core/runner/prepare-config-doors.js +13 -13
  67. package/dist/core/runner/prepare-defer-classify.d.ts +13 -19
  68. package/dist/core/runner/prepare-defer-classify.js +8 -3
  69. package/dist/core/runner/prepare-gate-stations.d.ts +26 -0
  70. package/dist/core/runner/prepare-gate-stations.js +1 -1
  71. package/dist/core/runner/prepare-hands-readface.d.ts +24 -7
  72. package/dist/core/runner/prepare-hands-readface.js +60 -24
  73. package/dist/core/runner/prepare-inherited-gate.d.ts +17 -4
  74. package/dist/core/runner/prepare-inherited-gate.js +10 -2
  75. package/dist/core/runner/prepare-lsp.d.ts +2 -2
  76. package/dist/core/runner/prepare-lsp.js +1 -1
  77. package/dist/core/runner/prepare-policy-chain.js +2 -0
  78. package/dist/core/runner/prepare-project-context.d.ts +5 -1
  79. package/dist/core/runner/prepare-project-context.js +7 -4
  80. package/dist/core/runner/prepare-prompt-inputs.d.ts +4 -2
  81. package/dist/core/runner/prepare-prompt-inputs.js +4 -1
  82. package/dist/core/runner/prepare-run-refs.d.ts +9 -1
  83. package/dist/core/runner/prepare-run-refs.js +3 -2
  84. package/dist/core/runner/prepare-suspend-saga.d.ts +5 -0
  85. package/dist/core/runner/prepare-suspend-saga.js +5 -2
  86. package/dist/core/runner/prepare-task.js +19 -14
  87. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +5 -0
  88. package/dist/core/runner/prepare-tool-disclosure-mount.js +10 -2
  89. package/dist/core/runner/prepare-turn-wiring.d.ts +8 -4
  90. package/dist/core/runner/prepare-turn-wiring.js +3 -2
  91. package/dist/core/runner/prepare-wiring-manifest.d.ts +5 -5
  92. package/dist/core/runner/prepare-wiring-manifest.js +5 -2
  93. package/dist/core/runner/resume-apply.js +3 -3
  94. package/dist/core/runner/resume-checkpoint-screen.js +7 -0
  95. package/dist/core/runner/run-harness-handlers.js +4 -1
  96. package/dist/core/runner/run-identity-wiring.js +6 -2
  97. package/dist/core/runner/run-leg.d.ts +2 -0
  98. package/dist/core/runner/run-leg.js +45 -45
  99. package/dist/core/runner/run-notification-lane.js +55 -27
  100. package/dist/core/runner/run-settle-and-teardown.js +1 -0
  101. package/dist/core/runner/run-terminal-adoption.js +16 -13
  102. package/dist/core/runner/run-turn-boundary.js +15 -5
  103. package/dist/core/runner/runtask.d.ts +6 -5
  104. package/dist/core/runner/runtask.js +26 -14
  105. package/dist/core/runner/skill-base-dir.d.ts +54 -0
  106. package/dist/core/runner/skill-base-dir.js +42 -0
  107. package/dist/core/runner/synthetic-tools.d.ts +83 -48
  108. package/dist/core/runner/synthetic-tools.js +69 -32
  109. package/dist/core/runner/tool-defer-gate.d.ts +22 -9
  110. package/dist/core/runner/tool-defer-gate.js +15 -5
  111. package/dist/core/runner/tool-disclosure.d.ts +5 -0
  112. package/dist/core/runner/tool-disclosure.js +2 -0
  113. package/dist/core/runner/tool-progress-lane.d.ts +42 -0
  114. package/dist/core/runner/tool-progress-lane.js +61 -0
  115. package/dist/core/runner/turn-attachments.d.ts +43 -12
  116. package/dist/core/runner/turn-attachments.js +58 -14
  117. package/dist/core/runner-deps.d.ts +48 -7
  118. package/dist/core/sensitive-path-policy.d.ts +58 -17
  119. package/dist/core/sensitive-path-policy.js +33 -5
  120. package/dist/core/skill-listing-budget.d.ts +112 -0
  121. package/dist/core/skill-listing-budget.js +58 -0
  122. package/dist/core/swappable-deps.d.ts +9 -7
  123. package/dist/core/swappable-deps.js +7 -3
  124. package/dist/core/task-event.d.ts +96 -29
  125. package/dist/core/task-notification.d.ts +142 -16
  126. package/dist/core/task-notification.js +106 -0
  127. package/dist/core/task-result.d.ts +9 -1
  128. package/dist/core/task-spec.d.ts +46 -3
  129. package/dist/core/tighten-task-spec.js +7 -5
  130. package/dist/core/tool-model-gate.d.ts +1 -1
  131. package/dist/core/tool-policy.d.ts +19 -4
  132. package/dist/core/tool-policy.js +6 -7
  133. package/dist/core/tool-search-setting.d.ts +78 -0
  134. package/dist/core/tool-search-setting.js +90 -0
  135. package/dist/core/tool-spec.d.ts +45 -4
  136. package/dist/core/types.d.ts +2 -2
  137. package/dist/core/types.js +1 -1
  138. package/dist/core/wiring-manifest.d.ts +46 -0
  139. package/dist/core/wiring-manifest.js +9 -1
  140. package/dist/core/write-protect.d.ts +8 -3
  141. package/dist/core/write-protect.js +8 -5
  142. package/dist/engine/execution-env/node-execution-env.js +10 -4
  143. package/dist/engine/harness/agent-harness.d.ts +42 -0
  144. package/dist/engine/harness/agent-harness.js +61 -29
  145. package/dist/engine/loop/agent-loop.d.ts +68 -3
  146. package/dist/engine/loop/agent-loop.js +109 -16
  147. package/dist/index.d.ts +13 -8
  148. package/dist/index.js +11 -7
  149. package/dist/internal/harness.d.ts +1 -1
  150. package/dist/orchestration/run-workflow-tool.d.ts +3 -0
  151. package/dist/orchestration/run-workflow-tool.js +6 -1
  152. package/dist/orchestration/workflow-governance.d.ts +7 -1
  153. package/dist/orchestration/workflow-governance.js +9 -5
  154. package/dist/orchestration/workflow-primitives.d.ts +3 -0
  155. package/dist/orchestration/workflow-primitives.js +6 -1
  156. package/dist/orchestration/workflow-types.d.ts +48 -13
  157. package/dist/orchestration/workflow-types.js +1 -1
  158. package/dist/orchestration/workflow.js +45 -17
  159. package/dist/prompt-assembly/epoch.js +2 -0
  160. package/dist/prompt-assembly/packs/sema-default.js +2 -2
  161. package/dist/prompt-assembly/section-usage.d.ts +26 -0
  162. package/dist/prompt-assembly/section-usage.js +10 -0
  163. package/dist/prompt-assembly/types.d.ts +8 -6
  164. package/dist/prompts/default.d.ts +13 -0
  165. package/dist/prompts/default.js +16 -8
  166. package/dist/tools/artifact/artifact-text.d.ts +10 -1
  167. package/dist/tools/artifact/artifact-text.js +6 -3
  168. package/dist/tools/artifact/artifact-tool.d.ts +12 -2
  169. package/dist/tools/artifact/artifact-tool.js +203 -70
  170. package/dist/tools/artifact/conformance.d.ts +56 -0
  171. package/dist/tools/artifact/conformance.js +349 -0
  172. package/dist/tools/artifact/local-stub.js +1 -1
  173. package/dist/tools/artifact/reject-breaker.d.ts +28 -0
  174. package/dist/tools/artifact/reject-breaker.js +42 -0
  175. package/dist/tools/fs/bash-lexer.d.ts +132 -0
  176. package/dist/tools/fs/bash-lexer.js +563 -0
  177. package/dist/tools/fs/bash-program-position.d.ts +142 -0
  178. package/dist/tools/fs/bash-program-position.js +125 -0
  179. package/dist/tools/fs/bash-readonly-classifier.d.ts +81 -16
  180. package/dist/tools/fs/bash-readonly-classifier.js +180 -558
  181. package/dist/tools/fs/containment-face.d.ts +89 -0
  182. package/dist/tools/fs/containment-face.js +81 -0
  183. package/dist/tools/fs/fs-bash.d.ts +46 -1
  184. package/dist/tools/fs/fs-bash.js +165 -25
  185. package/dist/tools/fs/fs-read.d.ts +1 -1
  186. package/dist/tools/fs/fs-search-tools.d.ts +3 -3
  187. package/dist/tools/fs/fs-write.d.ts +4 -3
  188. package/dist/tools/fs/fs-write.js +9 -9
  189. package/dist/tools/fs/index.d.ts +24 -5
  190. package/dist/tools/fs/index.js +18 -6
  191. package/dist/tools/fs/read-deny.d.ts +4 -3
  192. package/dist/tools/fs/repo-map.d.ts +2 -2
  193. package/dist/tools/fs/safety.d.ts +18 -3
  194. package/dist/tools/fs/safety.js +11 -4
  195. package/package.json +1 -1
  196. package/test/export-surface.snapshot.json +99 -13
  197. package/wire-changes/core@7.17.7.json +49 -0
  198. package/wire-changes/core@7.18.0.json +1273 -0
  199. package/wire-changes/core@7.19.0.json +457 -0
  200. package/dist/tools/fs/read-face.d.ts +0 -68
  201. package/dist/tools/fs/read-face.js +0 -47
@@ -16,18 +16,20 @@ import { deliverEngineNotice } from "../types.js";
16
16
  import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY, sanitizeUntrustedText } from "../untrusted-text.js";
17
17
  import { errorCodeOf } from "./assemble-result.js";
18
18
  import { DEFAULT_PRECALL_OUTPUT_TOKENS, platformLimitTerminal } from "./clock-and-limits.js";
19
- import { deepJsonEqual, DEFERRED_REISSUE, resumeContinuation } from "./decide-continuation.js";
19
+ import { deepJsonEqual, resumeContinuation } from "./decide-continuation.js";
20
20
  import { settleExecutionRecord } from "./execution-record.js";
21
21
  import { mintGateOutcome } from "./gate-exit.js";
22
22
  import { resolveGitLegDelivery, wrapGitFrame } from "./git-leg-delivery.js";
23
23
  import { GIT_STATUS_ECHO_PREVIEW } from "./git-status-frame.js";
24
24
  import { toImageContent } from "./image.js";
25
+ import { maybeHumanRejectionHalt } from "./prepare-gate-stations.js";
25
26
  import { nextHumanInputSeq } from "./steer-admission.js";
26
27
  import { settleTeardownLeg } from "./teardown-bounded.js";
27
28
  import { toolEndBodyFrom, toolResultMsg } from "./tool-end-body.js";
28
- import { attachmentEnvelopeTags, commitAgentListing, commitSkillsListing, reduceToolEnd, renderAgentListingDelta, renderOrphanedBackgroundTasks, renderSkillsListingDelta, stampWriteAnchor } from "./turn-attachments.js";
29
+ import { announceSkillListingTruncation } from "../skill-listing-budget.js";
30
+ import { attachmentEnvelopeTags, commitAgentListing, commitSkillsListing, reduceToolEnd, renderAgentListingDelta, renderOrphanedBackgroundTasks, renderSkillsListingFrame, stampWriteAnchor } from "./turn-attachments.js";
29
31
  export async function runLeg(input) {
30
- const { spec, queue, prepared, resume, rs, entryActor, effectiveTimeoutMs, timeout, pushContent, ident, emitCommitted, writeFamilyOf, startedToolCallIds, announceWorkspaceMove, withBrainSinks, flushGitMirror, loopLatch, notificationLane, unsubscribeTaskNotifications, unsubGitRetry, unsubBoundary, unsub, runner, next } = input;
32
+ 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;
31
33
  let final;
32
34
  let threw;
33
35
  let abortedLive = false;
@@ -101,11 +103,12 @@ export async function runLeg(input) {
101
103
  else {
102
104
  let continuation = resumeContinuation(resume, prepared.reminderMark);
103
105
  const bgSnapshot = resume.cp.state.runningBackgroundTasks;
106
+ let orphanTasks = [];
104
107
  if (bgSnapshot !== undefined && bgSnapshot.length > 0) {
105
108
  const alive = new Set(prepared.listBackgroundTasks().map((t) => t.id));
106
- const orphans = bgSnapshot.filter((t) => !alive.has(t.id));
107
- if (orphans.length > 0) {
108
- continuation += "\n\n" + formatHookFeedback(renderOrphanedBackgroundTasks(orphans), prepared.reminderMark);
109
+ orphanTasks = bgSnapshot.filter((t) => !alive.has(t.id));
110
+ if (orphanTasks.length > 0) {
111
+ continuation += "\n\n" + formatHookFeedback(renderOrphanedBackgroundTasks(orphanTasks), prepared.reminderMark);
109
112
  }
110
113
  }
111
114
  let gitResumeDelivered;
@@ -128,10 +131,13 @@ export async function runLeg(input) {
128
131
  }
129
132
  }
130
133
  const engineSegments = continuation.length > 0 ? [{ start: 0, end: continuation.length }] : [];
134
+ const parkedSteers = readPendingSteerQueue(resume.cp.state);
131
135
  const resumeFrames = [
132
- ...readPendingSteerQueue(resume.cp.state).map((entry) => ({ entry, source: "steer" })),
136
+ ...parkedSteers.map((entry) => ({ entry, source: "steer" })),
133
137
  ...(resume.wakeMessage !== undefined ? [{ entry: resume.wakeMessage, source: "wake" }] : []),
134
138
  ];
139
+ const appliedEvents = [];
140
+ const withheldSteerIds = new Set();
135
141
  for (const { entry: steer, source } of resumeFrames) {
136
142
  if (source === "wake" && resume.wakeMessageHookContext !== undefined) {
137
143
  const hookStart = continuation.length;
@@ -166,6 +172,8 @@ export async function runLeg(input) {
166
172
  outcome = { blocked: `the screen crashed (${inlineUntrusted(err.message)}; fail-closed)` };
167
173
  }
168
174
  if ("blocked" in outcome) {
175
+ if (steer.inputId !== undefined)
176
+ withheldSteerIds.add(steer.inputId);
169
177
  deliverEngineNotice(runner.deps.onNotice, {
170
178
  code: "steering.parked_input_blocked",
171
179
  message: "a parked steering input was withheld by the deployment's userPromptSubmit screen when this resume redelivered it: " +
@@ -207,7 +215,7 @@ export async function runLeg(input) {
207
215
  (steer.trusted ? formatHookFeedback(projected, prepared.reminderMark) : delimitUntrusted("supervisor steering message", projected));
208
216
  if (steer.trusted)
209
217
  engineSegments.push({ start, end: continuation.length });
210
- queue.push({
218
+ appliedEvents.push({
211
219
  ...buildHumanInputEvent({
212
220
  carrier: source,
213
221
  source,
@@ -236,7 +244,22 @@ export async function runLeg(input) {
236
244
  },
237
245
  };
238
246
  }
239
- final = await withBrainSinks(() => prepared.harness.prompt(continuation, { engineSegments }));
247
+ const pending = resume.cp.pendingAction;
248
+ const rejectionHalt = pending.kind === "tool_approval" && resume.outcome.gate === "policy_ask"
249
+ ? maybeHumanRejectionHalt({ gate: resume.outcome.gateOutcome, isDelegatedChild, toolName: pending.toolName, toolCallId: pending.toolCallId })
250
+ : undefined;
251
+ if (rejectionHalt !== undefined && pending.kind === "tool_approval")
252
+ prepared.batchHaltRef.current = { rejectedToolCallId: pending.toolCallId };
253
+ prepared.undeliveredContinuationRef.current = { parkedSteers: resumeFrames.map(({ entry }) => entry).filter((e) => !withheldSteerIds.has(e.inputId)), orphanTasks };
254
+ final = await withBrainSinks(() => prepared.harness.prompt(continuation, {
255
+ engineSegments,
256
+ settleUnansweredToolCalls: rejectionHalt !== undefined ? { halt: rejectionHalt } : {},
257
+ onDelivered: () => {
258
+ prepared.undeliveredContinuationRef.current = { parkedSteers: [], orphanTasks: [] };
259
+ for (const e of appliedEvents)
260
+ queue.push(e);
261
+ },
262
+ }));
240
263
  await flushGitMirror();
241
264
  if (prepared.gitStatusRef.announced?.pending === true)
242
265
  await prepared.gitStatusRef.reassert?.();
@@ -309,6 +332,7 @@ export async function runLeg(input) {
309
332
  }
310
333
  }
311
334
  const firstFrames = [];
335
+ let skillsListingCost;
312
336
  if (rs.attach.attachState !== undefined) {
313
337
  const attach = rs.attach.attachState;
314
338
  if (rs.attach.agentListingOn && prepared.agentListing !== undefined) {
@@ -329,14 +353,15 @@ export async function runLeg(input) {
329
353
  }
330
354
  if (rs.attach.skillsListingOn && prepared.skillsListing !== undefined) {
331
355
  const listing = prepared.skillsListing;
332
- const body = renderSkillsListingDelta(attach, listing.entries);
333
- if (body !== undefined) {
356
+ const frame = renderSkillsListingFrame(attach, listing.entries, listing.budget);
357
+ if (frame !== undefined) {
334
358
  firstFrames.push({
335
359
  source: "skills_listing",
336
- body,
360
+ body: frame.text,
337
361
  commit: () => {
338
362
  commitSkillsListing(attach, listing.entries);
339
363
  prepared.announcedListingsRef.skills = listing.entries.map((e) => e.name);
364
+ skillsListingCost = { total: frame.listed, nameOnly: frame.nameOnly, dropped: frame.dropped, budget: listing.budget };
340
365
  },
341
366
  });
342
367
  }
@@ -410,6 +435,9 @@ export async function runLeg(input) {
410
435
  rs.attach.attachmentsInjected += firstFrames.length;
411
436
  if (firstFrames.length > 0 && prepared.announcedSnapshotRecovered)
412
437
  await prepared.session.appendAnnouncedListing(prepared.announcedListingsRef).catch(() => undefined);
438
+ if (skillsListingCost !== undefined) {
439
+ announceSkillListingTruncation(runner.deps.onNotice, { ...skillsListingCost, sessionId: prepared.sessionId });
440
+ }
413
441
  const enginePrefixChars = effectiveObjective.length - projectedObjective.length;
414
442
  queue.push({
415
443
  ...buildHumanInputEvent({
@@ -452,6 +480,7 @@ export async function runLeg(input) {
452
480
  finally {
453
481
  timeout.clear();
454
482
  prepared.lspDiagnostics?.registry.releaseRun(prepared.lspDiagnostics.runIdent);
483
+ prepared.toolProgressRef.current?.clear();
455
484
  prepared.abortController.abort();
456
485
  prepared.releaseSignal();
457
486
  strandedHumanAnswers = prepared.settleContentAskBindings();
@@ -463,6 +492,7 @@ export async function runLeg(input) {
463
492
  notificationLane.live = false;
464
493
  unsubscribeTaskNotifications();
465
494
  prepared.toolRosterDeltas.unsubscribe();
495
+ prepared.toolDisclosure?.unsubscribe();
466
496
  unsubGitRetry();
467
497
  unsubBoundary();
468
498
  unsub();
@@ -491,39 +521,7 @@ async function applyResumeDecision(runner, prepared, resume, emit, emitCommitted
491
521
  e.code = "resume.tool_contract_mismatch";
492
522
  throw e;
493
523
  }
494
- const completed = new Set(pendingAction.completedCallIds);
495
- const deferredIds = pendingAction.batchToolCallIds.filter((id) => id !== pendingAction.toolCallId && !completed.has(id));
496
- const names = new Map();
497
- const deferredArgs = new Map();
498
- if (deferredIds.length > 0) {
499
- const { messages } = await prepared.session.buildContext();
500
- for (const m of messages) {
501
- if (m.role === "assistant") {
502
- for (const c of m.content) {
503
- if (c.type === "toolCall") {
504
- names.set(c.id, c.name);
505
- deferredArgs.set(c.id, c.arguments);
506
- }
507
- }
508
- }
509
- }
510
- }
511
- for (const id of pendingAction.batchToolCallIds) {
512
- if (id === pendingAction.toolCallId) {
513
- await resolvePendingCall(runner, prepared, resume, emit, emitCommitted, onResolvedToolSuccess, onExecuteStart);
514
- }
515
- else if (!completed.has(id)) {
516
- const name = names.get(id) ?? "unknown";
517
- const displayLabel = (() => {
518
- const l = prepared.tools.find((t) => t.name === name)?.label;
519
- return l !== undefined && l !== name ? { label: l } : {};
520
- })();
521
- emit({ type: "tool_start", toolCallId: id, toolName: name, ...displayLabel, args: deferredArgs.get(id) ?? {} });
522
- emit({ type: "tool_end", toolCallId: id, toolName: name, ...displayLabel, isError: true, ...toolEndBodyFrom({ content: formatHookFeedback(DEFERRED_REISSUE, prepared.reminderMark) }, true, prepared.structuredProjector) });
523
- const eid = await prepared.session.appendMessage(toolResultMsg(id, name, formatHookFeedback(DEFERRED_REISSUE, prepared.reminderMark), true));
524
- emitCommitted(eid, "toolResult", id);
525
- }
526
- }
524
+ await resolvePendingCall(runner, prepared, resume, emit, emitCommitted, onResolvedToolSuccess, onExecuteStart);
527
525
  }
528
526
  async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted, onResolvedToolSuccess, onExecuteStart) {
529
527
  const { pendingAction } = resume.cp;
@@ -538,6 +536,7 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
538
536
  const pendingLabel = (() => { const l = prepared.tools.find((t) => t.name === pendingAction.toolName)?.label; return l !== undefined && l !== pendingAction.toolName ? { label: l } : {}; })();
539
537
  emit({ type: "tool_start", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, args: resolvedArgs });
540
538
  const settleEnd = async (isError, result, gate = decidedGate) => {
539
+ prepared.toolProgressRef.current?.noteEnd(pendingAction.toolCallId);
541
540
  emit({ type: "tool_end", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, isError, ...toolEndBodyFrom(result, isError, prepared.structuredProjector, gate) });
542
541
  const word = await resume.recordExecutionOutcome(gate);
543
542
  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 } : {}) });
@@ -676,6 +675,7 @@ async function resolvePendingCall(runner, prepared, resume, emit, emitCommitted,
676
675
  }
677
676
  const args = resolvedArgs;
678
677
  onExecuteStart?.(pendingAction.toolCallId);
678
+ prepared.toolProgressRef.current?.noteStart(pendingAction.toolCallId, pendingAction.toolName);
679
679
  prepared.suspendProgressRef.executedApproved = true;
680
680
  let res;
681
681
  try {
@@ -1,6 +1,6 @@
1
1
  import { createPeerInboundChainRef, createPeerSelfRef } from "../../agents/peer-admission.js";
2
2
  import { boundInputHashOf } from "../canonical-json.js";
3
- import { isDelegatedAgentTerminal, isTerminalTaskNotification, renderTaskNotificationXml, SystemInjectionQueue, taskNotificationDedupKey } from "../task-notification.js";
3
+ import { createLiveNotificationLane, isDelegatedAgentTerminal, isTerminalTaskNotification, renderTaskNotificationXml, routeOrParkNotification, SystemInjectionQueue } from "../task-notification.js";
4
4
  import { ToolDetachHub } from "../tool-detach.js";
5
5
  import { pendingContentAskCallId } from "./decide-continuation.js";
6
6
  export async function runNotificationLane(input) {
@@ -32,9 +32,13 @@ export async function runNotificationLane(input) {
32
32
  let notificationLaneLive = true;
33
33
  let notificationSessionId;
34
34
  let descendantAnchorDisclosed = false;
35
- const parkTaskNotification = (payload, priority) => {
36
- if (notificationSessionId !== undefined)
37
- pendingSessionNotifications.pend(notificationSessionId, payload, priority);
35
+ const parkTaskNotification = (payload, priority, onDisposition) => {
36
+ laneSeat.handOff(payload);
37
+ const ownSession = notificationSessionId;
38
+ if (ownSession !== undefined)
39
+ routeOrParkNotification(pendingSessionNotifications, ownSession, payload, priority, onDisposition);
40
+ else
41
+ onDisposition?.("parked");
38
42
  if (!isDelegatedAgentTerminal(payload))
39
43
  return;
40
44
  const uplink = internals?.parentNotify;
@@ -49,12 +53,12 @@ export async function runNotificationLane(input) {
49
53
  }
50
54
  const rootAnchor = internals?.rootSessionId;
51
55
  if (rootAnchor !== undefined && rootAnchor !== notificationSessionId) {
52
- pendingSessionNotifications.pend(rootAnchor, payload, priority);
56
+ routeOrParkNotification(pendingSessionNotifications, rootAnchor, payload, priority);
53
57
  return;
54
58
  }
55
59
  if (internals?.parentSessionId !== undefined && !descendantAnchorDisclosed) {
56
60
  descendantAnchorDisclosed = true;
57
- runner.deps.onError?.(new Error(`background-agent terminal ${payload.task_id} parked on this run's own session — the delegation tree's root session is not reachable from this run (no uplink, no root anchor)`), { phase: "degraded", sessionId: notificationSessionId ?? "", classification: "descendant-terminal-unrouted" });
61
+ runner.deps.onError?.(new Error(`background-agent terminal ${payload.task_id} stayed on this run's own session — the delegation tree's root session is not reachable from this run (no uplink, no root anchor)`), { phase: "degraded", sessionId: notificationSessionId ?? "", classification: "descendant-terminal-unrouted" });
58
62
  }
59
63
  };
60
64
  const unsubscribeTaskNotifications = taskNotificationQueue.subscribe((item) => {
@@ -70,34 +74,33 @@ export async function runNotificationLane(input) {
70
74
  ? notificationHarness.followUp(xml, noteOptions)
71
75
  : notificationHarness.steer(xml, { ...noteOptions, ...(item.priority === "now" ? { immediate: true } : {}) });
72
76
  void deliver.then(() => item.onDisposition?.("queued"), () => {
73
- parkTaskNotification(item.payload, item.priority);
74
- item.onDisposition?.("parked");
77
+ parkTaskNotification(item.payload, item.priority, item.onDisposition);
75
78
  });
76
79
  }
77
80
  else {
78
- parkTaskNotification(item.payload, item.priority);
79
- item.onDisposition?.("parked");
81
+ parkTaskNotification(item.payload, item.priority, item.onDisposition);
80
82
  }
81
83
  });
82
84
  const upstreamTaskNotification = internals?.onTaskNotification;
83
85
  const deliveredAtTurnOpen = new Set();
86
+ const laneSeat = createLiveNotificationLane({
87
+ queue: taskNotificationQueue,
88
+ canTakeNow: () => notificationLaneLive && notificationHarness?.acceptsInjections === true,
89
+ isLive: () => notificationLaneLive,
90
+ alreadyDelivered: (key) => deliveredAtTurnOpen.has(key),
91
+ });
92
+ const takeOnThisLane = laneSeat.take;
93
+ const takeAsDestination = laneSeat.takeAsDestination;
84
94
  const injectTaskNotification = (notification, opts) => {
85
- if (deliveredAtTurnOpen.has(taskNotificationDedupKey(notification)))
86
- return Promise.resolve("dropped_duplicate");
87
- if (!notificationLaneLive) {
88
- parkTaskNotification(notification, opts?.priority ?? "later");
89
- return Promise.resolve("parked");
90
- }
95
+ const priority = opts?.priority ?? "later";
91
96
  return new Promise((resolve) => {
92
- const accepted = taskNotificationQueue.enqueue({
93
- kind: "task_notification",
94
- priority: opts?.priority ?? "later",
95
- dedupKey: taskNotificationDedupKey(notification),
96
- payload: notification,
97
- onDisposition: resolve,
98
- });
99
- if (!accepted)
97
+ const own = takeOnThisLane(notification, priority, resolve);
98
+ if (own === "duplicate")
99
+ resolve("dropped_duplicate");
100
+ else if (own === "pending")
100
101
  resolve("queued");
102
+ else if (own === "unavailable")
103
+ parkTaskNotification(notification, priority, resolve);
101
104
  });
102
105
  };
103
106
  const peerSelfRef = internals?.peerSelfRef ?? createPeerSelfRef(internals?.registryScope ?? spec.principal ?? "default");
@@ -120,9 +123,34 @@ export async function runNotificationLane(input) {
120
123
  }, self, taskIdRef);
121
124
  const notificationLane = {
122
125
  get harness() { return notificationHarness; }, set harness(h) { notificationHarness = h; },
123
- get sessionId() { return notificationSessionId; }, set sessionId(s) { notificationSessionId = s; },
126
+ get sessionId() { return notificationSessionId; },
127
+ set sessionId(s) {
128
+ if (notificationSessionId !== undefined)
129
+ pendingSessionNotifications.liveLanes.unregister(notificationSessionId, takeAsDestination);
130
+ notificationSessionId = s;
131
+ if (s !== undefined && notificationLaneLive)
132
+ pendingSessionNotifications.liveLanes.register(s, takeAsDestination);
133
+ },
124
134
  get ident() { return notificationIdent; }, set ident(f) { notificationIdent = f; },
125
- get live() { return notificationLaneLive; }, set live(v) { notificationLaneLive = v; },
135
+ get live() { return notificationLaneLive; },
136
+ set live(v) {
137
+ notificationLaneLive = v;
138
+ if (notificationSessionId === undefined)
139
+ return;
140
+ if (v) {
141
+ pendingSessionNotifications.liveLanes.register(notificationSessionId, takeAsDestination);
142
+ }
143
+ else {
144
+ pendingSessionNotifications.liveLanes.unregister(notificationSessionId, takeAsDestination);
145
+ laneSeat.release();
146
+ }
147
+ },
126
148
  };
127
- return await next({ prepared, injectTaskNotification, deliveredAtTurnOpen, peerInboundChainRef, modelCatalog, notificationLane, unsubscribeTaskNotifications });
149
+ try {
150
+ return await next({ prepared, injectTaskNotification, deliveredAtTurnOpen, peerInboundChainRef, modelCatalog, notificationLane, unsubscribeTaskNotifications });
151
+ }
152
+ finally {
153
+ if (notificationSessionId !== undefined)
154
+ pendingSessionNotifications.liveLanes.unregister(notificationSessionId, takeAsDestination);
155
+ }
128
156
  }
@@ -107,6 +107,7 @@ export async function runSettleAndTeardown(input) {
107
107
  strandedHumanAnswers,
108
108
  remoteEnvFailures: prepared.remoteEnvFailures,
109
109
  effectiveReadFace: prepared.effectiveReadFace,
110
+ effectiveWriteFace: prepared.effectiveWriteFace,
110
111
  effectiveReadDenyPatterns: prepared.effectiveReadDenyPatterns,
111
112
  effectiveMemoryScopes: prepared.effectiveMemoryScopes,
112
113
  effectiveReasoning: reasoningResolution.current,
@@ -3,7 +3,7 @@ import { DEFAULT_COMPACTION_SETTINGS } from "../../internal/harness.js";
3
3
  import { eventDefaultOn } from "../../prompt-assembly/event-registry.js";
4
4
  import { appendInterruptionMarker, reconcileInterruptedSession } from "../session-reconcile.js";
5
5
  import { emitTrace } from "../trace.js";
6
- import { deliverEngineNotice, undrainedUserInputNotices } from "../types.js";
6
+ import { deliverEngineNotice, owedUserInputCounts, undrainedUserInputNotices } from "../types.js";
7
7
  import { answerSettled, errorCodeOf } from "./assemble-result.js";
8
8
  import { awaitChargeWithSlowDisclosure, discloseUnevaluableWindow } from "./clock-and-limits.js";
9
9
  import { buildWorkingFileAttachments, centerAdoptionOption, contextInstructionFilesOption, emitInputTruncated, forkContextOption } from "./compaction-call-options.js";
@@ -14,7 +14,7 @@ import { reconciledToolEndBody } from "./tool-end-body.js";
14
14
  export async function runTerminalAdoption(input) {
15
15
  const { spec, queue, prepared, rs, stats, ident, emitCommitted, startedToolCallIds, toolLabels, timeout, compactionBrain, compactionBreaker, windowSafetyOptions, undrainedUserAtEnd, userInputLostAtEnd, final, abortedLive, userInterruptedLive, runner, sessions, next } = input;
16
16
  let { threw } = input;
17
- const settled = answerSettled(final, userInputLostAtEnd.current || prepared.workCutRef.continuation || prepared.workCutRef.abortedBeforeTurnEnd);
17
+ const settled = answerSettled(final, userInputLostAtEnd.current || prepared.undeliveredContinuationRef.current.parkedSteers.length > 0 || prepared.workCutRef.continuation || prepared.workCutRef.abortedBeforeTurnEnd);
18
18
  if (threw === undefined && prepared.brainCallGuardrailRef.timedOut !== undefined) {
19
19
  threw = prepared.brainCallGuardrailRef.timedOut;
20
20
  }
@@ -77,16 +77,24 @@ export async function runTerminalAdoption(input) {
77
77
  }
78
78
  }
79
79
  let migratedParked = { steer: 0, followUp: 0 };
80
+ let carriedContinuation = 0;
80
81
  if (committedPause !== undefined && runner.deps.checkpointStore !== undefined) {
81
82
  const parkToken = committedPause.cause.token;
82
83
  const parkScope = committedPause.scope;
83
84
  const store = runner.deps.checkpointStore;
84
85
  const carried = [];
85
- for (const record of prepared.harness.readParkableUserInputs()) {
86
+ const owed = [
87
+ ...prepared.undeliveredContinuationRef.current.parkedSteers.map(({ seq: _seq, ...input }) => ({ input })),
88
+ ...prepared.harness.readParkableUserInputs().map((record) => ({ input: record, live: record })),
89
+ ];
90
+ for (const { input, live } of owed) {
86
91
  try {
87
- if (!(await store.setPendingSteer(parkToken, parkScope, record)))
92
+ if (!(await store.setPendingSteer(parkToken, parkScope, input)))
88
93
  break;
89
- carried.push(record);
94
+ if (live !== undefined)
95
+ carried.push(live);
96
+ else
97
+ carriedContinuation += 1;
90
98
  }
91
99
  catch (parkErr) {
92
100
  runner.deps.onError?.(parkErr instanceof Error ? parkErr : new Error(String(parkErr)), {
@@ -101,14 +109,9 @@ export async function runTerminalAdoption(input) {
101
109
  }
102
110
  migratedParked = prepared.harness.dropParkedUserInputs(carried);
103
111
  }
104
- if (undrainedUserAtEnd.current !== undefined) {
105
- const remaining = {
106
- steer: Math.max(0, undrainedUserAtEnd.current.steer - migratedParked.steer),
107
- followUp: Math.max(0, undrainedUserAtEnd.current.followUp - migratedParked.followUp),
108
- };
109
- for (const notice of undrainedUserInputNotices(remaining, spec.taskId ?? prepared.sessionId, prepared.sessionId, prepared.runId)) {
110
- deliverEngineNotice(runner.deps.onNotice, notice);
111
- }
112
+ const owed = owedUserInputCounts(undrainedUserAtEnd.current, migratedParked, { frames: prepared.undeliveredContinuationRef.current.parkedSteers, carried: carriedContinuation, rowMayReturn: committedPause === undefined && !prepared.suspendProgressRef.executedApproved });
113
+ for (const notice of undrainedUserInputNotices(owed, spec.taskId ?? prepared.sessionId, prepared.sessionId, prepared.runId)) {
114
+ deliverEngineNotice(runner.deps.onNotice, notice);
112
115
  }
113
116
  if (rs.attach.attachState?.postCompactPending === true && (rs.attach.attachmentsCfg?.backgroundTasks ?? eventDefaultOn("background_tasks")) === true) {
114
117
  emitTrace(rs.telemetry.tracer, () => ({ kind: "compaction.announce_dropped", version: 1, taskId: rs.telemetry.taskId, ts: Date.now() }));
@@ -4,7 +4,8 @@ import { eventDefaultOn } from "../../prompt-assembly/event-registry.js";
4
4
  import { DEFAULT_COMPACTION_INSTRUCTIONS, isCompactionManualCancel, maybeCompact, nextTrimForceBackoff, recordCompactionAndCheckRapidRefill } from "../auto-compaction.js";
5
5
  import { emitTrace } from "../trace.js";
6
6
  import { redactThenCut } from "../../agents/subagent-steps.js";
7
- import { ATTACHMENT_BYTE_CAP, CHANGED_FILES_MAX, TOTAL_TOKENS_REMINDER_DEFAULT_MODE, attachmentEnvelopeTags, clipToBytes, collectDateChange, collectDueAttachments, collectInstructionsChange, commitAgentListing, commitInstructionsChange, commitSkillsListing, rebaseCadenceWindows, renderAgentListingDelta, renderMcpDroppedTools, renderMcpInstructionsDelta, selectMcpDroppedBatch, renderSkillsListingDelta, renderToolsDelta } from "./turn-attachments.js";
7
+ import { ATTACHMENT_BYTE_CAP, CHANGED_FILES_MAX, TOTAL_TOKENS_REMINDER_DEFAULT_MODE, attachmentEnvelopeTags, clipToBytes, collectDateChange, collectDueAttachments, collectInstructionsChange, commitAgentListing, commitInstructionsChange, commitSkillsListing, rebaseCadenceWindows, renderAgentListingDelta, renderMcpDroppedTools, renderMcpInstructionsDelta, selectMcpDroppedBatch, renderSkillsListingFrame, renderToolsDelta } from "./turn-attachments.js";
8
+ import { announceSkillListingTruncation } from "../skill-listing-budget.js";
8
9
  import { buildWorkingFileAttachments, centerAdoptionOption, contextInstructionFilesOption, emitInputTruncated, forkContextOption } from "./compaction-call-options.js";
9
10
  import { hookSeatExpiredError, runHookSeat } from "../hooks.js";
10
11
  import { sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
@@ -15,6 +16,7 @@ import { awaitChargeWithSlowDisclosure, discloseUnevaluableWindow, ENV_DUE_GOVER
15
16
  import { gitRestateOption } from "./git-leg-delivery.js";
16
17
  import { COMPACTION_FREED_EPSILON, COMPACTION_REGROWTH_FACTOR, MAX_CONSECUTIVE_COMPACTION_FAILURES } from "./compaction-knobs.js";
17
18
  import { compactionHookOptions, recordCompactionReuse, seamCCompactionOptions } from "./compaction-seams.js";
19
+ import { contextUsageSections } from "../../prompt-assembly/section-usage.js";
18
20
  const BATCH_CONTEXT_MIN_KEEP_BYTES = 160;
19
21
  const BATCH_TRUNCATION_MARKER = "\n…[truncated]";
20
22
  export function createTurnBoundary(input) {
@@ -201,6 +203,7 @@ export function createTurnBoundary(input) {
201
203
  }
202
204
  let boundaryAttachmentBytes = 0;
203
205
  let attachmentsPayload;
206
+ let skillsListingCost;
204
207
  if ((rs.attach.dateState !== undefined ||
205
208
  rs.attach.instrState !== undefined ||
206
209
  rs.attach.sizeGuidelineState !== undefined ||
@@ -312,7 +315,9 @@ export function createTurnBoundary(input) {
312
315
  ...(prepared.agentListing.models !== undefined ? { agentModels: prepared.agentListing.models } : {}),
313
316
  }
314
317
  : {}),
315
- ...(rs.attach.skillsListingOn && prepared.skillsListing !== undefined ? { skillsListing: prepared.skillsListing.entries } : {}),
318
+ ...(rs.attach.skillsListingOn && prepared.skillsListing !== undefined
319
+ ? { skillsListing: prepared.skillsListing.entries, skillsListingBudget: prepared.skillsListing.budget }
320
+ : {}),
316
321
  ...(mcpAdds !== undefined || mcpRemovals !== undefined
317
322
  ? { mcpInstructions: { added: mcpAdds ?? [], removed: mcpRemovals ?? [] } }
318
323
  : {}),
@@ -376,9 +381,10 @@ export function createTurnBoundary(input) {
376
381
  }
377
382
  if (rs.attach.skillsListingOn && prepared.skillsListing !== undefined && due.some((a) => a.source === "skills_listing")) {
378
383
  const listing = prepared.skillsListing;
379
- const intactBody = renderSkillsListingDelta(rs.attach.attachState, listing.entries);
380
- if (intactBody !== undefined && due.some((a) => a.source === "skills_listing" && a.body === intactBody)) {
384
+ const intact = renderSkillsListingFrame(rs.attach.attachState, listing.entries, listing.budget);
385
+ if (intact !== undefined && due.some((a) => a.source === "skills_listing" && a.body === intact.text)) {
381
386
  commitSkillsListing(rs.attach.attachState, listing.entries);
387
+ skillsListingCost = { total: intact.listed, nameOnly: intact.nameOnly, dropped: intact.dropped, budget: listing.budget };
382
388
  prepared.announcedListingsRef.skills = listing.entries.map((e) => e.name);
383
389
  if (prepared.announcedSnapshotRecovered)
384
390
  await prepared.session.appendAnnouncedListing(prepared.announcedListingsRef).catch(() => undefined);
@@ -473,6 +479,9 @@ export function createTurnBoundary(input) {
473
479
  if ((attachmentsPayload !== undefined || batchContextBlock !== undefined) && prepared.batchHaltRef.current === undefined) {
474
480
  const payload = attachmentsPayload !== undefined && batchContextBlock !== undefined ? `${attachmentsPayload}\n${batchContextBlock}` : (attachmentsPayload ?? batchContextBlock);
475
481
  void prepared.harness.steer(payload, { engineMinted: true }).catch(() => { });
482
+ if (skillsListingCost !== undefined) {
483
+ announceSkillListingTruncation(deps.runner.deps.onNotice, { ...skillsListingCost, sessionId: prepared.sessionId });
484
+ }
476
485
  }
477
486
  const forceManual = manualCompactRef.requested;
478
487
  if ((!withinTaskCompaction && !forceManual) ||
@@ -580,7 +589,8 @@ export function createTurnBoundary(input) {
580
589
  }
581
590
  rs.counters.trimForceBackoff = nextTrimForceBackoff(rs.counters.trimForceBackoff, comp, trimPressure);
582
591
  if (comp.contextUsage) {
583
- queue.push({ type: "context_usage", ...comp.contextUsage, ...ident() });
592
+ const sections = contextUsageSections(prepared.promptManifest.sections);
593
+ queue.push({ type: "context_usage", ...comp.contextUsage, ...(sections !== undefined ? { sections } : {}), ...ident() });
584
594
  }
585
595
  if (comp.unevaluableWindow) {
586
596
  discloseUnevaluableWindow(comp.unevaluableWindow, prepared, rs.telemetry.tracer, rs.telemetry.taskId, runnerHooks.onError);
@@ -91,14 +91,14 @@ export declare class Runner implements RunnerSelfSeat {
91
91
  /**
92
92
  * Hot-swap the deployment seats a running Runner may change without a restart — the ONE door for every
93
93
  * hot-swappable seat ({@link SWAPPABLE_DEP_SEATS}: the model catalog generation `models` + its `tiers`
94
- * bindings, and the deployment's read-face declaration `readFace`), and the ONLY sanctioned way to
94
+ * bindings, and the deployment's two containment-face declarations `readFace` / `writeFace`), and the ONLY sanctioned way to
95
95
  * change any of them after construction. It replaces the former `swapModels` verb (retired, no alias):
96
96
  * one verb per seat restated the same laws with a list of verbs as the only record of what is swappable.
97
97
  *
98
98
  * The laws, stated once (see the vocabulary module for the seat-by-seat meaning):
99
99
  * - **Screened first, written once — atomic.** The candidate is screened whole
100
100
  * ({@link screenSwappableDeps}: the key set is the closed one, at least one seat, `tiers` rides with
101
- * `models`, each seat's own value gate — `readFace` through the very `assertReadFaceValue` every
101
+ * `models`, each seat's own value gate — both face seats through the very `assertContainmentFaceValue` every
102
102
  * prepare door applies) and the catalog is tier-expanded on the candidate (an illegal binding throws
103
103
  * exactly like the constructor's boot error) BEFORE the deps object is replaced. On any throw the
104
104
  * current deps stay in force, byte-identical.
@@ -108,14 +108,15 @@ export declare class Runner implements RunnerSelfSeat {
108
108
  * redirects never re-route or re-price a task mid-run; a read-face change never re-fences a mounted
109
109
  * band; the divergence window is exactly the in-flight legs' lifetime, by design.
110
110
  * - **Presence is the instruction.** A key present on the candidate is replaced (an explicit
111
- * `undefined` clears an optional seat: `tiers: undefined` ⇒ no bindings, `readFace: undefined` ⇒ the
111
+ * `undefined` clears an optional seat: `tiers: undefined` ⇒ no bindings, a face seat `undefined` ⇒ the
112
112
  * engine default); a key absent is kept. `tiers` omitted beside `models` ⇒ the current bindings are
113
113
  * re-applied over the new catalog.
114
114
  * - **Announced per seat.** `config.models_swapped` (models/tiers counts — key material only, never the
115
115
  * catalog itself; plus the advisory `route.base_url_changed_key_unchanged` for same-name entries
116
116
  * whose URL moved while the Model-visible credential fingerprint did not) and
117
- * `config.read_face_swapped` (`{ from, to, occurrenceId }`, the seat's previous and new value, `null`
118
- * = no declaration) — the table {@link SWAP_SEAT_NOTICE} binds every seat to its line.
117
+ * `config.read_face_swapped` / `config.write_face_swapped` (`{ from, to, occurrenceId }`, the seat's
118
+ * previous and new value, `null` = no declaration) — the table {@link SWAP_SEAT_NOTICE} binds every
119
+ * seat to its line.
119
120
  */
120
121
  swapDeps(next: SwappableDeps): void;
121
122
  /** one-shot brain-routed utility query (see {@link runSideQuery} for the full contract):
@@ -49,6 +49,7 @@ import { runAttachmentSeats } from "./run-attachment-seats.js";
49
49
  import { runToolMountFacts } from "./run-tool-mount-facts.js";
50
50
  import { runReasoningSeat } from "./run-reasoning-seat.js";
51
51
  import { runClockAndContent } from "./run-clock-and-content.js";
52
+ import { createToolProgressLane } from "./tool-progress-lane.js";
52
53
  import { runBrainSinks } from "./run-brain-sinks.js";
53
54
  import { runCompactionMachinery } from "./run-compaction-machinery.js";
54
55
  import { runStopAndFinalVerify } from "./run-stop-and-final-verify.js";
@@ -212,18 +213,25 @@ export class Runner {
212
213
  }
213
214
  }
214
215
  }
215
- const faceSwap = seats.includes("readFace");
216
- const priorFace = this.deps.readFace;
216
+ const faceSeats = ["readFace", "writeFace"].filter((seat) => seats.includes(seat));
217
+ const priorFaces = {
218
+ readFace: this.deps.readFace,
219
+ writeFace: this.deps.writeFace,
220
+ };
217
221
  const written = {
218
222
  ...this.deps,
219
223
  ...(expanded !== undefined ? { models: expanded } : {}),
220
224
  ...(catalogSwap && tiers !== undefined ? { tiers } : {}),
221
- ...(faceSwap && candidate.readFace !== undefined ? { readFace: candidate.readFace } : {}),
222
225
  };
223
226
  if (catalogSwap && tiers === undefined)
224
227
  delete written.tiers;
225
- if (faceSwap && candidate.readFace === undefined)
226
- delete written.readFace;
228
+ const writtenFaceSeats = written;
229
+ for (const seat of faceSeats) {
230
+ if (candidate[seat] !== undefined)
231
+ writtenFaceSeats[seat] = candidate[seat];
232
+ else
233
+ delete writtenFaceSeats[seat];
234
+ }
227
235
  this.deps = written;
228
236
  if (catalogSwap) {
229
237
  const modelCount = Object.keys(candidate.models).length;
@@ -245,13 +253,15 @@ export class Runner {
245
253
  });
246
254
  }
247
255
  }
248
- if (faceSwap) {
249
- const to = candidate.readFace ?? null;
250
- deliverEngineNotice(this.deps.onNotice, {
251
- code: "config.read_face_swapped",
252
- message: `deployment read face swapped: ${priorFace ?? "(no declaration)"} → ${to ?? "(no declaration)"}; in-flight legs keep the face they prepared under, new legs resolve against the new declaration`,
253
- detail: { from: priorFace ?? null, to, occurrenceId: uuidv7() },
254
- });
256
+ for (const seat of faceSeats) {
257
+ const to = candidate[seat] ?? null;
258
+ const from = priorFaces[seat] ?? null;
259
+ const message = `deployment ${seat === "readFace" ? "read" : "write"} face swapped: ${from ?? "(no declaration)"} → ${to ?? "(no declaration)"}; in-flight legs keep the face they prepared under, new legs resolve against the new declaration`;
260
+ const detail = { from, to, occurrenceId: uuidv7() };
261
+ if (seat === "readFace")
262
+ deliverEngineNotice(this.deps.onNotice, { code: "config.read_face_swapped", message, detail });
263
+ else
264
+ deliverEngineNotice(this.deps.onNotice, { code: "config.write_face_swapped", message, detail });
255
265
  }
256
266
  }
257
267
  sideQuery(spec) {
@@ -413,6 +423,8 @@ export class Runner {
413
423
  const { todoToolMounted, taskToolsMounted, writeFamilyOf, toolStartAt, startedToolCallIds } = runToolMountFacts({ spec, prepared, rs });
414
424
  const { observeReasoningWireFacts, reasoningResolution } = runReasoningSeat({ prepared, rs, taskIdRef });
415
425
  const { effectiveTimeoutMs, walltimeMonotonicDeadline, timeout, pushContent, ownCommittedTailRef, emitCommitted } = runClockAndContent({ spec, prepared, queue, internals, rs, ident, parentToolCallId, startedToolCallIds });
426
+ const toolProgress = createToolProgressLane(pushContent, ident);
427
+ prepared.toolProgressRef.current = toolProgress;
416
428
  const { subagentName, withBrainSinks } = runBrainSinks({ queue, internals, rs, ident, parentToolCallId, observeReasoningWireFacts });
417
429
  const toolLabels = new Map(prepared.tools.flatMap((t) => (t.label !== undefined && t.label !== t.name ? [[t.name, t.label]] : [])));
418
430
  for (const orphan of prepared.wakeRecovered) {
@@ -426,7 +438,7 @@ export class Runner {
426
438
  prepared, stats, rs,
427
439
  deps: {
428
440
  spec, queue, internals, ident, parentToolCallId, subagentName,
429
- pushContent, emitCommitted, startedToolCallIds, toolStartAt, writeFamilyOf,
441
+ pushContent, emitCommitted, startedToolCallIds, toolStartAt, toolProgress, writeFamilyOf,
430
442
  toolLabels, postToolBatchHook, batchArgs,
431
443
  runnerHooks: { onError: (e, info) => this.deps.onError?.(e, info) },
432
444
  },
@@ -453,7 +465,7 @@ export class Runner {
453
465
  });
454
466
  const { flushGitMirror, unsubGitRetry, unsubBoundary, unsub } = runGitLane({ prepared, queue, rs, ident, onMessageUpdate, onMessageEnd, onToolStart, onToolEnd, onTurnEnd, onTurnBoundary, runner: this.depsSeat });
455
467
  return runLeg({
456
- spec, queue, prepared, resume, rs, entryActor, effectiveTimeoutMs, timeout, pushContent, ident, emitCommitted, writeFamilyOf, startedToolCallIds,
468
+ spec, queue, prepared, resume, rs, isDelegatedChild: internals?.isDelegatedChild === true, entryActor, effectiveTimeoutMs, timeout, pushContent, ident, emitCommitted, writeFamilyOf, startedToolCallIds,
457
469
  announceWorkspaceMove, withBrainSinks, flushGitMirror, loopLatch, notificationLane, unsubscribeTaskNotifications, unsubGitRetry, unsubBoundary, unsub,
458
470
  runner: this.depsSeat,
459
471
  next: ({ final, threw, abortedLive, userInterruptedLive, strandedHumanAnswers }) => runTerminalAdoption({