@sema-agent/core 7.17.6 → 7.18.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 (164) hide show
  1. package/CHANGELOG.md +165 -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/route-adjudicator.d.ts +18 -0
  8. package/dist/brain/route-adjudicator.js +16 -0
  9. package/dist/core/agent-definition.d.ts +10 -0
  10. package/dist/core/artifact-host.d.ts +41 -4
  11. package/dist/core/artifact-host.js +18 -2
  12. package/dist/core/auto-mode.d.ts +141 -45
  13. package/dist/core/auto-mode.js +109 -3
  14. package/dist/core/checkpoint-store.d.ts +16 -2
  15. package/dist/core/engine-notice.d.ts +47 -1
  16. package/dist/core/engine-notice.js +7 -0
  17. package/dist/core/gate-lanes.js +2 -1
  18. package/dist/core/gate-outcome.d.ts +17 -2
  19. package/dist/core/gate-outcome.js +16 -0
  20. package/dist/core/governance-codes.d.ts +1 -1
  21. package/dist/core/governance-codes.js +8 -0
  22. package/dist/core/hook-seats.d.ts +66 -0
  23. package/dist/core/hook-seats.js +37 -0
  24. package/dist/core/runner/advertised-writable-dirs.d.ts +8 -3
  25. package/dist/core/runner/assemble-result.d.ts +2 -1
  26. package/dist/core/runner/assemble-result.js +1 -1
  27. package/dist/core/runner/classifier-ladder.d.ts +89 -0
  28. package/dist/core/runner/classifier-ladder.js +65 -0
  29. package/dist/core/runner/contracts.d.ts +69 -13
  30. package/dist/core/runner/decide-continuation.d.ts +1 -3
  31. package/dist/core/runner/decide-continuation.js +1 -7
  32. package/dist/core/runner/deferral-census.d.ts +52 -0
  33. package/dist/core/runner/deferral-census.js +44 -0
  34. package/dist/core/runner/derived-route-fallback.d.ts +8 -4
  35. package/dist/core/runner/derived-route-fallback.js +8 -3
  36. package/dist/core/runner/gate-exit.d.ts +12 -0
  37. package/dist/core/runner/gate-exit.js +6 -3
  38. package/dist/core/runner/prepare-artifact.d.ts +10 -2
  39. package/dist/core/runner/prepare-artifact.js +5 -4
  40. package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -2
  41. package/dist/core/runner/prepare-caps-and-workflow.js +30 -31
  42. package/dist/core/runner/prepare-config-doors.d.ts +1 -1
  43. package/dist/core/runner/prepare-config-doors.js +5 -3
  44. package/dist/core/runner/prepare-defer-classify.d.ts +13 -19
  45. package/dist/core/runner/prepare-defer-classify.js +8 -3
  46. package/dist/core/runner/prepare-gate-stations.d.ts +26 -0
  47. package/dist/core/runner/prepare-gate-stations.js +1 -1
  48. package/dist/core/runner/prepare-hands-readface.d.ts +24 -7
  49. package/dist/core/runner/prepare-hands-readface.js +60 -24
  50. package/dist/core/runner/prepare-inherited-gate.d.ts +17 -4
  51. package/dist/core/runner/prepare-inherited-gate.js +10 -2
  52. package/dist/core/runner/prepare-lsp.d.ts +2 -2
  53. package/dist/core/runner/prepare-lsp.js +1 -1
  54. package/dist/core/runner/prepare-policy-chain.js +2 -0
  55. package/dist/core/runner/prepare-project-context.js +4 -2
  56. package/dist/core/runner/prepare-prompt-inputs.d.ts +4 -2
  57. package/dist/core/runner/prepare-prompt-inputs.js +4 -1
  58. package/dist/core/runner/prepare-run-refs.d.ts +9 -1
  59. package/dist/core/runner/prepare-run-refs.js +3 -2
  60. package/dist/core/runner/prepare-suspend-saga.d.ts +5 -0
  61. package/dist/core/runner/prepare-suspend-saga.js +5 -2
  62. package/dist/core/runner/prepare-task.js +18 -13
  63. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +5 -0
  64. package/dist/core/runner/prepare-tool-disclosure-mount.js +10 -2
  65. package/dist/core/runner/prepare-turn-wiring.d.ts +8 -4
  66. package/dist/core/runner/prepare-turn-wiring.js +3 -2
  67. package/dist/core/runner/prepare-wiring-manifest.d.ts +5 -5
  68. package/dist/core/runner/prepare-wiring-manifest.js +5 -2
  69. package/dist/core/runner/resume-checkpoint-screen.js +7 -0
  70. package/dist/core/runner/run-harness-handlers.js +4 -1
  71. package/dist/core/runner/run-identity-wiring.js +6 -2
  72. package/dist/core/runner/run-leg.d.ts +2 -0
  73. package/dist/core/runner/run-leg.js +35 -41
  74. package/dist/core/runner/run-notification-lane.js +55 -27
  75. package/dist/core/runner/run-settle-and-teardown.js +1 -0
  76. package/dist/core/runner/run-terminal-adoption.js +16 -13
  77. package/dist/core/runner/run-turn-boundary.js +3 -1
  78. package/dist/core/runner/runtask.d.ts +6 -5
  79. package/dist/core/runner/runtask.js +26 -14
  80. package/dist/core/runner/skill-base-dir.d.ts +54 -0
  81. package/dist/core/runner/skill-base-dir.js +42 -0
  82. package/dist/core/runner/synthetic-tools.d.ts +16 -19
  83. package/dist/core/runner/synthetic-tools.js +18 -10
  84. package/dist/core/runner/tool-defer-gate.d.ts +22 -9
  85. package/dist/core/runner/tool-defer-gate.js +15 -5
  86. package/dist/core/runner/tool-disclosure.d.ts +5 -0
  87. package/dist/core/runner/tool-disclosure.js +2 -0
  88. package/dist/core/runner/tool-progress-lane.d.ts +42 -0
  89. package/dist/core/runner/tool-progress-lane.js +61 -0
  90. package/dist/core/runner-deps.d.ts +48 -7
  91. package/dist/core/swappable-deps.d.ts +9 -7
  92. package/dist/core/swappable-deps.js +7 -3
  93. package/dist/core/task-event.d.ts +96 -29
  94. package/dist/core/task-notification.d.ts +142 -16
  95. package/dist/core/task-notification.js +106 -0
  96. package/dist/core/task-result.d.ts +9 -1
  97. package/dist/core/task-spec.d.ts +20 -3
  98. package/dist/core/tighten-task-spec.js +7 -5
  99. package/dist/core/tool-model-gate.d.ts +1 -1
  100. package/dist/core/tool-search-setting.d.ts +78 -0
  101. package/dist/core/tool-search-setting.js +90 -0
  102. package/dist/core/tool-spec.d.ts +8 -0
  103. package/dist/core/types.d.ts +1 -1
  104. package/dist/core/types.js +1 -1
  105. package/dist/core/wiring-manifest.d.ts +46 -0
  106. package/dist/core/wiring-manifest.js +9 -1
  107. package/dist/engine/execution-env/node-execution-env.js +10 -4
  108. package/dist/engine/harness/agent-harness.d.ts +42 -0
  109. package/dist/engine/harness/agent-harness.js +61 -29
  110. package/dist/engine/loop/agent-loop.d.ts +68 -3
  111. package/dist/engine/loop/agent-loop.js +109 -16
  112. package/dist/index.d.ts +10 -5
  113. package/dist/index.js +9 -5
  114. package/dist/internal/harness.d.ts +1 -1
  115. package/dist/orchestration/run-workflow-tool.d.ts +3 -0
  116. package/dist/orchestration/run-workflow-tool.js +6 -1
  117. package/dist/orchestration/workflow-governance.d.ts +7 -1
  118. package/dist/orchestration/workflow-governance.js +9 -5
  119. package/dist/orchestration/workflow-primitives.d.ts +3 -0
  120. package/dist/orchestration/workflow-primitives.js +6 -1
  121. package/dist/orchestration/workflow-types.d.ts +48 -13
  122. package/dist/orchestration/workflow-types.js +1 -1
  123. package/dist/orchestration/workflow.js +45 -17
  124. package/dist/prompt-assembly/epoch.js +2 -0
  125. package/dist/prompt-assembly/packs/sema-default.js +2 -2
  126. package/dist/prompt-assembly/section-usage.d.ts +26 -0
  127. package/dist/prompt-assembly/section-usage.js +10 -0
  128. package/dist/prompt-assembly/types.d.ts +8 -6
  129. package/dist/prompts/default.d.ts +13 -0
  130. package/dist/prompts/default.js +16 -8
  131. package/dist/tools/artifact/artifact-text.d.ts +10 -1
  132. package/dist/tools/artifact/artifact-text.js +6 -3
  133. package/dist/tools/artifact/artifact-tool.d.ts +12 -2
  134. package/dist/tools/artifact/artifact-tool.js +203 -70
  135. package/dist/tools/artifact/conformance.d.ts +56 -0
  136. package/dist/tools/artifact/conformance.js +349 -0
  137. package/dist/tools/artifact/local-stub.js +1 -1
  138. package/dist/tools/artifact/reject-breaker.d.ts +28 -0
  139. package/dist/tools/artifact/reject-breaker.js +42 -0
  140. package/dist/tools/fs/bash-lexer.d.ts +132 -0
  141. package/dist/tools/fs/bash-lexer.js +563 -0
  142. package/dist/tools/fs/bash-program-position.d.ts +142 -0
  143. package/dist/tools/fs/bash-program-position.js +125 -0
  144. package/dist/tools/fs/bash-readonly-classifier.d.ts +54 -16
  145. package/dist/tools/fs/bash-readonly-classifier.js +375 -530
  146. package/dist/tools/fs/containment-face.d.ts +89 -0
  147. package/dist/tools/fs/containment-face.js +81 -0
  148. package/dist/tools/fs/fs-bash.d.ts +46 -1
  149. package/dist/tools/fs/fs-bash.js +158 -22
  150. package/dist/tools/fs/fs-read.d.ts +1 -1
  151. package/dist/tools/fs/fs-search-tools.d.ts +3 -3
  152. package/dist/tools/fs/fs-write.d.ts +4 -3
  153. package/dist/tools/fs/fs-write.js +9 -9
  154. package/dist/tools/fs/index.d.ts +24 -5
  155. package/dist/tools/fs/index.js +18 -6
  156. package/dist/tools/fs/repo-map.d.ts +2 -2
  157. package/dist/tools/fs/safety.d.ts +3 -3
  158. package/dist/tools/fs/safety.js +4 -4
  159. package/package.json +1 -1
  160. package/test/export-surface.snapshot.json +85 -9
  161. package/wire-changes/core@7.17.6.json +81 -0
  162. package/wire-changes/core@7.17.7.json +49 -0
  163. package/dist/tools/fs/read-face.d.ts +0 -68
  164. package/dist/tools/fs/read-face.js +0 -47
@@ -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({
@@ -0,0 +1,54 @@
1
+ /**
2
+ * A skill's BASE DIRECTORY: the placeholder grammar its material may use to name its own root, the bounded
3
+ * substitution that resolves it, and the delivered-length measure the load gate budgets on.
4
+ *
5
+ * Split out of the skill tool because it is a different concern: the tool decides what a skill INVOKE
6
+ * returns, this decides how the caller's material names files on the machine the tools run on.
7
+ */
8
+ /** The typed refusal a `SkillSpec.baseDir` outside its domain raises — one code, one sentence. */
9
+ export declare const SKILL_BASE_DIR_INVALID_CODE = "config.skill_base_dir_invalid";
10
+ /**
11
+ * The ONE screen of `SkillSpec.baseDir`: absent ⇒ no directory (nothing is substituted and no opening line is
12
+ * added); a NON-EMPTY string ⇒ itself; anything else ⇒ a loud refusal carrying {@link SKILL_BASE_DIR_INVALID_CODE}.
13
+ *
14
+ * Both rejected shapes were SILENT before, and each was silent in its own direction. `""` is not "no directory":
15
+ * it substitutes to nothing, so a body's `${SKILL_ROOT}/ref.md` is delivered as the filesystem-root path
16
+ * `/ref.md` and the opening line names no directory at all — a wrong answer where absence would have given the
17
+ * visible unresolved placeholder the seat's contract promises. A non-string makes the delivered LENGTH `NaN`,
18
+ * and `NaN <= limit` is false, so the load gate dropped the skill and reported "content is NaN chars" —
19
+ * a configuration typo reading as an over-limit body. Core still neither resolves nor reads the directory
20
+ * (the brain-outside rule); this screens the VALUE, not the path.
21
+ *
22
+ * It runs at the load gate ({@link substitutedSkillLength}, which spec assembly calls for every skill), so the
23
+ * substitution faces below are reached only with a value that already passed it.
24
+ */
25
+ export declare function skillBaseDirOf(value: unknown): string | undefined;
26
+ /**
27
+ * Substitute a skill's base directory for the root placeholders in ITS OWN MATERIAL.
28
+ *
29
+ * @contract skill.base_dir — with `SkillSpec.baseDir` set, a `Skill` result opens with the line
30
+ * `Base directory for this skill: <baseDir>` and every `${SKILL_ROOT}` / `${CLAUDE_PLUGIN_ROOT}` in the
31
+ * body AND in every attachment's content is replaced by that value; `files[].path` is then read relative to
32
+ * it. The substitution covers the CALLER'S material only — never the model-supplied `args` section, which is
33
+ * live input and is delivered as written. Core neither resolves nor reads the directory (the brain-outside
34
+ * rule that keeps `SkillSpec.files` inline); with `baseDir` absent no line is added, nothing is substituted,
35
+ * and the result is byte-identical to a pre-baseDir invoke.
36
+ */
37
+ export declare function substituteSkillRoot(text: string, baseDir: string): string;
38
+ /**
39
+ * Substitute under a CEILING, and report the size the full substitution would have had.
40
+ *
41
+ * Substitution is the one step here that can make text grow without bound: a placeholder is 13 or 21 chars and a real directory can be any length, so an
42
+ * attachment that is nothing but placeholders expands by a factor of the path's length. Building that string in order to measure it against the attachments
43
+ * cap — or worse, to discover it does not FIT and drop it — spends exactly the memory the cap exists to bound before the cap is consulted, and a big enough
44
+ * one exceeds the runtime's own string limit. So the length is counted as the loop walks, nothing is materialized past `maxChars`, and the caller budgets on
45
+ * `totalChars` while rendering only what fits.
46
+ */
47
+ /** The DELIVERED length of a body once its root placeholders resolve — counted, never materialized, so the load gate measures what the model will actually
48
+ * receive rather than what the caller typed. Equal to `text.length` when no directory is stated, and the one place the seat's value is SCREENED
49
+ * ({@link skillBaseDirOf}) — the gate that budgets on this number is the first reader of every skill on every leg. */
50
+ export declare function substitutedSkillLength(text: string, baseDir: unknown): number;
51
+ export declare function substituteSkillRootBounded(text: string, baseDir: string, maxChars: number): {
52
+ text: string;
53
+ totalChars: number;
54
+ };
@@ -0,0 +1,42 @@
1
+ const SKILL_ROOT_PLACEHOLDERS = /\$\{(?:SKILL_ROOT|CLAUDE_PLUGIN_ROOT)\}/g;
2
+ export const SKILL_BASE_DIR_INVALID_CODE = "config.skill_base_dir_invalid";
3
+ export function skillBaseDirOf(value) {
4
+ if (value === undefined)
5
+ return undefined;
6
+ if (typeof value === "string" && value !== "")
7
+ return value;
8
+ const e = new Error(`SkillSpec.baseDir must be a non-empty string naming its own directory (got ${value === "" ? '""' : typeof value}) — ` +
9
+ `omit the key where no directory is stated; an empty one resolves the body's \${SKILL_ROOT} placeholders to nothing.`);
10
+ e.code = SKILL_BASE_DIR_INVALID_CODE;
11
+ throw e;
12
+ }
13
+ export function substituteSkillRoot(text, baseDir) {
14
+ return text.replace(SKILL_ROOT_PLACEHOLDERS, () => baseDir);
15
+ }
16
+ export function substitutedSkillLength(text, baseDir) {
17
+ const dir = skillBaseDirOf(baseDir);
18
+ if (dir === undefined)
19
+ return text.length;
20
+ return substituteSkillRootBounded(text, dir, 0).totalChars;
21
+ }
22
+ export function substituteSkillRootBounded(text, baseDir, maxChars) {
23
+ const parts = [];
24
+ let totalChars = 0;
25
+ let built = 0;
26
+ const push = (piece) => {
27
+ totalChars += piece.length;
28
+ if (built >= maxChars)
29
+ return;
30
+ const kept = piece.length > maxChars - built ? piece.slice(0, maxChars - built) : piece;
31
+ parts.push(kept);
32
+ built += kept.length;
33
+ };
34
+ let last = 0;
35
+ for (const m of text.matchAll(SKILL_ROOT_PLACEHOLDERS)) {
36
+ push(text.slice(last, m.index));
37
+ push(baseDir);
38
+ last = m.index + m[0].length;
39
+ }
40
+ push(text.slice(last));
41
+ return { text: parts.join(""), totalChars };
42
+ }
@@ -52,12 +52,10 @@ export interface ReportedFinding {
52
52
  export declare function createReportFindingsTool(): AgentTool;
53
53
  /** Reserved name of the injected skill-loading tool (present only when `spec.skills` is non-empty). */
54
54
  export declare const SKILL_TOOL_NAME = "Skill";
55
- /** Per-skill LOAD gate on `SkillSpec.content` (CC 2.1.207 parity: CC's only size door at invoke
56
- * time is a 1MB file-load gate — an over-limit skill is REJECTED WHOLE, never truncated; the
57
- * invoked body itself enters context untruncated). Sema skills are spec-supplied content, so the
58
- * equivalent gate sits at spec assembly (prepare-task): an oversized skill is excluded from both
59
- * the disclosure block and the tool, with an operator-visible config error. The compaction-time
60
- * retention caps live in the engine's compaction module — a different door, deliberately. */
55
+ /** Per-skill LOAD gate on `SkillSpec.content` (CC 2.1.207 parity: CC's only invoke-time size door is a 1MB file-load gate — an over-limit skill is REJECTED
56
+ * WHOLE, never truncated, and the invoked body enters context untruncated). Sema skills are spec-supplied, so the equivalent gate sits at spec assembly:
57
+ * an oversized skill is excluded from the disclosure block and the tool, with an operator-visible config error. The compaction-time retention caps are a
58
+ * different door, deliberately. */
61
59
  export declare const SKILL_CONTENT_MAX_CHARS: number;
62
60
  /** Total listing-block cap in UTF-8 BYTES — [c209-C] R2 C4①: the old 8000-CHAR cap measured a
63
61
  * different coordinate than the bundle's `ATTACHMENT_BYTE_CAP` (8192 UTF-8 bytes) and counted only
@@ -68,28 +66,24 @@ export declare const SKILL_CONTENT_MAX_CHARS: number;
68
66
  * bundle cap solo and an intact commit is always reachable. MUST stay ≤ ATTACHMENT_BYTE_CAP
69
67
  * (turn-attachments.ts; not imported here — that module imports this one — pinned by test instead). */
70
68
  export declare const SKILLS_BLOCK_MAX_BYTES = 8000;
71
- /** Total per-invoke cap for a skill's inlined attachments (`SkillSpec.files`) — an ATTACHMENT-face
72
- * budget, distinct from the (gone) body truncation door: the body itself is delivered untruncated
73
- * at invoke time (CC parity), while attachments are reference material riding along — bounded and
74
- * truncation-disclosed. */
69
+ /** Total per-invoke cap for a skill's inlined attachments (`SkillSpec.files`) — an ATTACHMENT-face budget, distinct from the (gone) body truncation door:
70
+ * the body is delivered untruncated at invoke time (CC parity), while attachments ride along bounded and truncation-disclosed. */
75
71
  export declare const SKILL_ATTACHMENTS_MAX_CHARS = 50000;
76
- /** Per-attachment cap on the PATH rendered in its `###` header — a single pathological path must not
77
- * eat the attachments budget on its own. Overflow is truncated with an ellipsis. */
72
+ /** Per-attachment cap on the PATH rendered in its `###` header — one pathological path must not eat the budget. Overflow truncates with an ellipsis. */
78
73
  export declare const SKILL_ATTACHMENT_PATH_MAX_CHARS = 512;
79
74
  interface SkillLike {
80
75
  name: string;
81
76
  description: string;
82
77
  content: string;
83
- /** Optional Gate-3 runtime capability manifest — carried through so a successful
84
- * `skill` load can push its scope frame. Skills without one stay unscoped (backward-compatible). */
78
+ /** Optional Gate-3 runtime capability manifest — carried through so a successful `skill` load can push its scope frame; skills without one stay unscoped. */
85
79
  manifest?: SkillManifest;
86
- /** Optional attachments (SkillSpec.files): disclosed as paths in the block, delivered inline on
87
- * invoke under {@link SKILL_ATTACHMENTS_MAX_CHARS}. Same caller-trusted contract as `content`
88
- * (see the module note above) — core never writes them to disk (deployment's half). */
80
+ /** Optional attachments (SkillSpec.files): paths in the block, delivered inline on invoke under {@link SKILL_ATTACHMENTS_MAX_CHARS}. Same caller-trusted
81
+ * contract as `content` — core never writes them to disk (deployment's half). */
89
82
  files?: Array<{
90
83
  path: string;
91
84
  content: string;
92
85
  }>;
86
+ /** Optional base directory (SkillSpec.baseDir) — see `@contract skill.base_dir`. Absent ⇒ byte-identical result. */ baseDir?: string;
93
87
  }
94
88
  /** [c209-C] The metadata slice of a skill the LISTING faces consume (disclosure block + the
95
89
  * skills_listing turn attachment): name, description, attachment paths — never the body. The
@@ -116,8 +110,11 @@ export interface SkillListingEntry {
116
110
  * keep their descriptions"). Absent ⇒ the entry's array index is used, i.e. the prior behavior. */
117
111
  declaredRank?: number;
118
112
  }
119
- /** Dedupe by name (first occurrence wins) and sort by name — one deterministic list shared by the
120
- * disclosure block and the tool, so what the prompt promises is exactly what the tool serves. */
113
+ /** Dedupe by name (first occurrence wins) and sort by name — one deterministic list shared by the disclosure block
114
+ * and the tool, so what the prompt promises is exactly what the tool serves. Each field is READ ONCE, BY NAME, and
115
+ * both consumers see the copy: the gate screens this list while the invoke substitutes from it, so a `get baseDir()`
116
+ * answering those two reads differently passed a screen that measured a value the model never received — and a rest
117
+ * copy would silently drop an INHERITED or non-enumerable `manifest`, i.e. widen a skill's capability restriction. */
121
118
  export declare function normalizeSkills(skills: SkillLike[] | undefined): SkillLike[];
122
119
  /** [c209-C] The skills listing's lead line — single-sourced so the run loop's durable-resume
123
120
  * transcript probe (MED-3② skills arm, runtask) matches EXACTLY what the initial frame carries
@@ -2,6 +2,7 @@ import { Type } from "typebox";
2
2
  import { defineTool } from "../tools.js";
3
3
  import { sanitizeUntrustedText, SHELLED_BODY_ENVELOPE_TAGS } from "../untrusted-text.js";
4
4
  import { toolFace } from "../tool-catalog-entries.js";
5
+ import { substituteSkillRoot, substituteSkillRootBounded } from "./skill-base-dir.js";
5
6
  export const OUTPUT_TOOL_NAME = "StructuredOutput";
6
7
  export const REPORT_BLOCKED_TOOL_NAME = "ReportBlocked";
7
8
  export const REPORT_FINDINGS_TOOL_NAME = "ReportFindings";
@@ -112,7 +113,9 @@ export function normalizeSkills(skills) {
112
113
  for (const s of skills ?? []) {
113
114
  if (!seen.has(s.name)) {
114
115
  seen.add(s.name);
115
- out.push(s);
116
+ const { name, description, content, manifest, files, baseDir, ...rest } = s;
117
+ void rest;
118
+ out.push({ name, description, content, ...(manifest !== undefined ? { manifest } : {}), ...(files !== undefined ? { files } : {}), ...(baseDir !== undefined ? { baseDir } : {}) });
116
119
  }
117
120
  }
118
121
  return out.toSorted((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
@@ -202,7 +205,8 @@ export function createSkillTool(skills, scope) {
202
205
  if (scope && skill.manifest) {
203
206
  scope.push({ kind: "manifest", manifest: skill.manifest });
204
207
  }
205
- let body = skill.content;
208
+ const baseDir = skill.baseDir;
209
+ let body = baseDir !== undefined ? substituteSkillRoot(skill.content, baseDir) : skill.content;
206
210
  if (skill.files !== undefined && skill.files.length > 0) {
207
211
  const SEPARATOR_CHARS = 2;
208
212
  const SECTION_HEADER = "## Attachments";
@@ -217,27 +221,30 @@ export function createSkillTool(skills, scope) {
217
221
  : f.path;
218
222
  const header = `### ${path}\n`;
219
223
  const renderedOverhead = header.length + SEPARATOR_CHARS;
224
+ const expanded = baseDir !== undefined ? substituteSkillRootBounded(f.content, baseDir, Math.max(0, budget)) : { text: f.content, totalChars: f.content.length };
225
+ const content = expanded.text;
226
+ const contentChars = expanded.totalChars;
220
227
  if (budget <= renderedOverhead) {
221
228
  omittedCount += 1;
222
- omittedChars += header.length + f.content.length;
229
+ omittedChars += header.length + contentChars;
223
230
  continue;
224
231
  }
225
232
  const contentBudget = budget - renderedOverhead;
226
- if (f.content.length > contentBudget) {
227
- const markerFor = (shown) => `\n[attachment truncated: showing the first ${shown} of ${f.content.length} chars — attachments cap reached]`;
233
+ if (contentChars > contentBudget) {
234
+ const markerFor = (shown) => `\n[attachment truncated: showing the first ${shown} of ${contentChars} chars — attachments cap reached]`;
228
235
  const shown = contentBudget - markerFor(contentBudget).length;
229
236
  if (shown <= 0) {
230
237
  omittedCount += 1;
231
- omittedChars += header.length + f.content.length;
238
+ omittedChars += header.length + contentChars;
232
239
  budget = 0;
233
240
  continue;
234
241
  }
235
- sections.push(`${header}${f.content.slice(0, shown)}${markerFor(shown)}`);
242
+ sections.push(`${header}${content.slice(0, shown)}${markerFor(shown)}`);
236
243
  budget = 0;
237
244
  continue;
238
245
  }
239
- sections.push(`${header}${f.content}`);
240
- budget -= renderedOverhead + f.content.length;
246
+ sections.push(`${header}${content}`);
247
+ budget -= renderedOverhead + contentChars;
241
248
  }
242
249
  if (omittedCount > 0) {
243
250
  sections.push(`[+${omittedCount} more attachment(s) omitted (${omittedChars} chars) — the ${SKILL_ATTACHMENTS_MAX_CHARS}-char attachments cap is exhausted]`);
@@ -245,7 +252,8 @@ export function createSkillTool(skills, scope) {
245
252
  body = `${body}\n\n${sections.join("\n\n")}`;
246
253
  }
247
254
  const skillArgs = typeof a.args === "string" && a.args.length > 0 ? a.args.slice(0, 10_000) : undefined;
248
- return skillArgs ? `${body}\n\n## Input\n${skillArgs}` : body;
255
+ const delivered = skillArgs ? `${body}\n\n## Input\n${skillArgs}` : body;
256
+ return baseDir !== undefined ? `Base directory for this skill: ${baseDir}\n\n${delivered}` : delivered;
249
257
  },
250
258
  });
251
259
  }
@@ -1,15 +1,16 @@
1
1
  import type { Model } from "../../internal/llm.js";
2
2
  import type { ToolSpec } from "../types.js";
3
3
  import type { ToolFingerprintInput } from "../cache-break-detector.js";
4
+ import { type ResolvedToolSearch, type ToolSearchSetting } from "../tool-search-setting.js";
4
5
  /**
5
6
  * The deferral GATE: which mounted tool names ship as placeholders on a request. Pure
6
7
  * registry logic over names and sizes — no harness, no I/O. `tool-disclosure.ts` owns the placeholder
7
8
  * and ToolSearch mechanics that act on the set this module decides; `prepare-defer-classify.ts` is the
8
9
  * phase that assembles the inputs from the mounted face.
9
10
  */
10
- /** The deferral gate's threshold: the engine's candidate set is deferred when its inlined size reaches
11
- * this fraction of the model's context window (that set measured, never the rest of the face). */
12
- export declare const DEFER_THRESHOLD_FRACTION = 0.1;
11
+ /** The deferral gate's DEFAULT threshold: with no `toolSearch` seat, the candidate set defers when its inlined size reaches this fraction of the model's
12
+ * context window (that set measured, never the rest of the face). Derived from the seat's own default percent — one default, two units. */
13
+ export declare const DEFER_THRESHOLD_FRACTION: number;
13
14
  /** The typed refusal a `deferMode` outside its declared domain (`"auto"` or absent) raises at the classifier —
14
15
  * a bad value never selects an arm silently. */
15
16
  export declare const DEFER_MODE_INVALID_CODE = "config.defer_mode_invalid";
@@ -24,10 +25,13 @@ export interface DeferralGate {
24
25
  gated: Set<string>;
25
26
  /** Σ inlined chars (name + description + serialized schema) over `gated` — the bytes the gate could reclaim. */
26
27
  gatedChars: number;
27
- /** `floor(contextTokens × fraction) × CHARS_PER_TOKEN`; the char reading of the token threshold. */
28
+ /** `floor(contextTokens × fraction) × CHARS_PER_TOKEN`; the char reading of the token threshold. `0` under `toolSearch: true` and `Infinity` under
29
+ * `false` — the two boolean arms ARE this number. */
28
30
  thresholdChars: number;
29
31
  /** Whether `gated` is deferred on this request: `gatedChars ≥ thresholdChars`. */
30
32
  open: boolean;
33
+ /** The lowered seat this verdict was measured under ({@link ToolSearchSetting}); always present, since an absent seat resolves to the default `auto`. */
34
+ toolSearch: ResolvedToolSearch;
31
35
  }
32
36
  /**
33
37
  * Deferral has TWO rules, and only two.
@@ -44,11 +48,12 @@ export interface DeferralGate {
44
48
  * "auto"` WIDENS the candidate set to every caller spec — undeclared caller tools included — and nothing
45
49
  * else: it is not a second gate, and engine built-ins the deployment did not name are candidates under no
46
50
  * setting. The gate decides the set all or nothing: deferred iff `gatedChars ≥ thresholdChars`, where the
47
- * threshold is {@link DEFER_THRESHOLD_FRACTION} of the model's context window (`contextTokens ??
48
- * contextWindow`, in chars at {@link CHARS_PER_TOKEN}). The measure is the candidate set ONLY — the bytes
49
- * the gate could reclaim — never the caller's list, the pins, or non-candidates: a request whose weight
50
- * sits elsewhere gains nothing from a ToolSearch round-trip, so nothing about it argues for one. A window
51
- * of zero reads as a zero threshold (every candidate byte is over budget), so the candidates defer.
51
+ * threshold is the DEPLOYMENT'S FRACTION of the model's context window (`contextTokens ?? contextWindow`,
52
+ * in chars at {@link CHARS_PER_TOKEN}) — {@link ToolSearchSetting} lowered by {@link resolveToolSearch},
53
+ * which is {@link DEFER_THRESHOLD_FRACTION} when no seat is wired. The measure is the candidate set ONLY —
54
+ * the bytes the gate could reclaim — never the caller's list, the pins, or non-candidates: a request whose
55
+ * weight sits elsewhere gains nothing from a ToolSearch round-trip, so nothing about it argues for one. A
56
+ * window of zero reads as a zero threshold (every candidate byte is over budget), so the candidates defer.
52
57
  *
53
58
  * Invariants: `explicit ∩ gated = ∅`; `deferred = explicit ∪ (open ? gated : ∅)`; a pinned name is in no
54
59
  * list; the same inputs always yield the same verdict (a pure function of the request).
@@ -65,6 +70,9 @@ export declare function deferralGate(opts: {
65
70
  fullTools: ReadonlyArray<ToolFingerprintInput>;
66
71
  /** `"auto"` widens the candidate set to every caller spec (see above). Any other value refuses. */
67
72
  deferMode?: "auto";
73
+ /** The deployment's gate policy for rule ② ({@link ToolSearchSetting}); absent ⇒ the default `auto` at
74
+ * {@link TOOL_SEARCH_DEFAULT_THRESHOLD_PERCENT}, byte-for-byte the prior behavior. */
75
+ toolSearch?: ToolSearchSetting;
68
76
  model: Model;
69
77
  /** Rule ① — `TaskSpec.deferTools`: ALREADY-MOUNTED wire names (built-ins included; `ToolSpec.defer`
70
78
  * only reaches caller specs) the caller defers on every request. Caller pre-filters to mounted names. */
@@ -84,3 +92,8 @@ export declare function deferralGate(opts: {
84
92
  * gate; the `TaskSpec` / `RunnerDeps` JSDoc points here.
85
93
  */
86
94
  export declare function classifyDeferred(opts: Parameters<typeof deferralGate>[0]): Set<string>;
95
+ /** The SAME fold, with the verdict it folded — a disclosure that reports the policy must read the one the gate measured under, never a later re-resolution. */
96
+ export declare function classifyDeferredWithGate(opts: Parameters<typeof deferralGate>[0]): {
97
+ deferred: Set<string>;
98
+ gate: DeferralGate;
99
+ };
@@ -1,5 +1,6 @@
1
1
  import { DEFAULT_CHARS_PER_TOKEN } from "../../internal/harness.js";
2
- export const DEFER_THRESHOLD_FRACTION = 0.1;
2
+ import { TOOL_SEARCH_DEFAULT_THRESHOLD_PERCENT, describeRejectedValue, resolveToolSearch } from "../tool-search-setting.js";
3
+ export const DEFER_THRESHOLD_FRACTION = TOOL_SEARCH_DEFAULT_THRESHOLD_PERCENT / 100;
3
4
  const CHARS_PER_TOKEN = DEFAULT_CHARS_PER_TOKEN;
4
5
  function inlinedChars(t) {
5
6
  let schema = "";
@@ -14,10 +15,11 @@ function inlinedChars(t) {
14
15
  export const DEFER_MODE_INVALID_CODE = "config.defer_mode_invalid";
15
16
  export function deferralGate(opts) {
16
17
  if (opts.deferMode !== undefined && opts.deferMode !== "auto") {
17
- const e = new Error(`RunnerDeps.deferMode: invalid value ${JSON.stringify(opts.deferMode)} — expected "auto" or omitted; a bad value refuses loudly, nothing falls back to a default.`);
18
+ const e = new Error(`RunnerDeps.deferMode: invalid value ${describeRejectedValue(opts.deferMode)} — expected "auto" or omitted; a bad value refuses loudly, nothing falls back to a default.`);
18
19
  e.code = DEFER_MODE_INVALID_CODE;
19
20
  throw e;
20
21
  }
22
+ const toolSearch = resolveToolSearch(opts.toolSearch);
21
23
  const pinned = new Set(opts.alwaysLoadNames ?? []);
22
24
  for (const s of opts.specs) {
23
25
  if (s.alwaysLoad === true)
@@ -44,14 +46,22 @@ export function deferralGate(opts) {
44
46
  if (gated.has(t.name))
45
47
  gatedChars += inlinedChars(t);
46
48
  const windowTokens = Math.max(0, opts.model.contextTokens ?? opts.model.contextWindow ?? 0);
47
- const thresholdChars = Math.floor(windowTokens * DEFER_THRESHOLD_FRACTION) * CHARS_PER_TOKEN;
48
- return { explicit, gated, gatedChars, thresholdChars, open: gatedChars >= thresholdChars };
49
+ const thresholdTokens = windowTokens * toolSearch.fraction;
50
+ const thresholdChars = Number.isFinite(thresholdTokens)
51
+ ? Math.floor(thresholdTokens) * CHARS_PER_TOKEN
52
+ : toolSearch.fraction === 0
53
+ ? 0
54
+ : Number.POSITIVE_INFINITY;
55
+ return { explicit, gated, gatedChars, thresholdChars, open: gatedChars >= thresholdChars, toolSearch };
49
56
  }
50
57
  export function classifyDeferred(opts) {
58
+ return classifyDeferredWithGate(opts).deferred;
59
+ }
60
+ export function classifyDeferredWithGate(opts) {
51
61
  const gate = deferralGate(opts);
52
62
  const deferred = new Set(gate.explicit);
53
63
  if (gate.open)
54
64
  for (const n of gate.gated)
55
65
  deferred.add(n);
56
- return deferred;
66
+ return { deferred, gate };
57
67
  }
@@ -247,6 +247,11 @@ export declare function createToolSearchTool(opts: {
247
247
  */
248
248
  isMounted?: (name: string) => boolean;
249
249
  serializeActivation?: <T>(section: () => Promise<T>) => Promise<T>;
250
+ /** Called at the TAIL of this lane's activation section, after the rollback window has closed — the one point at which the shared active set is committed
251
+ * for this call. The disclosure census rides it, because a publisher reading that set from OUTSIDE the section (an MCP refresh's rematerialize, which runs
252
+ * unserialized and after an await) can observe a name a racing activation staged and then rolled back, and announce an activation that never happened.
253
+ * Absent ⇒ nothing is published. */
254
+ onActivationCommitted?: () => void;
250
255
  /** Resolve a deferred name's REAL declared
251
256
  * parameter schema, read from the live roster at call time. Presence switches this face to the
252
257
  * static posture: activation never swaps the placeholder in the tools block, so the result text
@@ -336,6 +336,8 @@ export function createToolSearchTool(opts) {
336
336
  throw e;
337
337
  }
338
338
  }
339
+ if (newly.length > 0)
340
+ opts.onActivationCommitted?.();
339
341
  return { newly, ride: newly.length > 0 ? listingRide?.(newly) : undefined };
340
342
  });
341
343
  const { newly, ride } = await section;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * The run's TOOL-PROGRESS lane — the single mint of the `tool_progress` frame: its clock, its cadence,
3
+ * and the heartbeat that keeps a silent long-running tool call visible.
4
+ *
5
+ * TWO PRODUCERS, ONE MINT. A tool that observes its own work ticks fast and carries what it produced: the
6
+ * shell's execute leg watches its command's output and calls `emit` about once a second. Every OTHER call is
7
+ * opaque here — the runner knows only that it started and has not ended — so this lane runs a slow beat for
8
+ * any in-flight call silent for {@link TOOL_HEARTBEAT_MS}.
9
+ *
10
+ * THE LANE OWNS TIME, the producer owns FACTS. A producer reports what it has seen (the tail, the totals),
11
+ * never how long it has run: `elapsedTimeSeconds` is computed HERE from one origin per call, because the two
12
+ * producers do not start their clocks together — the station registers a call when it ENTERS EXECUTION, a
13
+ * shell command's own clock could only start at its spawn. With two origins a 30 s beat could be followed
14
+ * moments later by a shell tick reporting 1 s, breaking the monotonicity the frame promises.
15
+ *
16
+ * THE CADENCE HAS ONE ENFORCEMENT POINT, same reason: a producer's timer is a SAMPLING clock, not the
17
+ * contract. `emit` drops a frame landing inside {@link TOOL_PROGRESS_MIN_INTERVAL_MS} of that call's last.
18
+ *
19
+ * ONE RULE FOR THE BEAT: a call is beaten when nothing has been emitted FOR IT for a whole interval,
20
+ * counting its own start as the first such event — covering both "it only just started" and "the tool
21
+ * already ticks faster than this". The interval is the RUN's, so a first beat lands anywhere in
22
+ * [TOOL_HEARTBEAT_MS, 2 × TOOL_HEARTBEAT_MS): the price of one timer per run, stated rather than hidden.
23
+ *
24
+ * THE CLOCK IS MONOTONIC: every predicate here is a DIFFERENCE of two readings (is this call due a beat, is
25
+ * this offer inside the floor, how long has it run), and off the wall clock a backward step — NTP, a
26
+ * container's clock settling after a resume — makes each difference negative and silences BOTH producers
27
+ * until it catches up: the exact failure this lane exists to prevent.
28
+ *
29
+ * LIVENESS ONLY — see the `tool_progress` member's contract in `task-event.ts`. The timer is `unref`'d and
30
+ * every emission is fault-contained. */
31
+ import type { TaskEvent, TaskEventIdentity } from "../types.js";
32
+ import type { ToolProgressLane } from "./contracts.js";
33
+ /** How long a tool call may stay silent before this lane beats for it, in ms. ONE SOURCE, and CC
34
+ * 2.1.259's own per-tool heartbeat interval (`Cct`) — this engine keeps the cadence and folds the beat
35
+ * into the one progress frame instead of minting a second shape (registered divergence). */
36
+ export declare const TOOL_HEARTBEAT_MS = 30000;
37
+ /** The floor between two frames of ONE call — the `tool_progress.cadence` contract, enforced here and
38
+ * nowhere else so it holds across producers rather than inside each one. */
39
+ export declare const TOOL_PROGRESS_MIN_INTERVAL_MS = 1000;
40
+ /** Build the lane over the run's content pusher (`pushContent` — the ONE seat that puts a frame on this
41
+ * run's queue and, for a sub-agent, forwards it to the parent's sink) and its identity mint. */
42
+ export declare function createToolProgressLane(push: (e: TaskEvent) => void, ident: () => TaskEventIdentity): ToolProgressLane;
@@ -0,0 +1,61 @@
1
+ export const TOOL_HEARTBEAT_MS = 30_000;
2
+ export const TOOL_PROGRESS_MIN_INTERVAL_MS = 1_000;
3
+ const monotonicNow = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
4
+ export function createToolProgressLane(push, ident) {
5
+ const inFlight = new Map();
6
+ let timer;
7
+ const arm = () => {
8
+ if (timer !== undefined)
9
+ return;
10
+ timer = setInterval(beat, TOOL_HEARTBEAT_MS);
11
+ timer.unref?.();
12
+ };
13
+ const emit = (body) => {
14
+ const now = monotonicNow();
15
+ let live = inFlight.get(body.toolCallId);
16
+ if (live === undefined) {
17
+ live = { toolName: body.toolName, startedAt: now, lastAt: now - TOOL_PROGRESS_MIN_INTERVAL_MS };
18
+ inFlight.set(body.toolCallId, live);
19
+ arm();
20
+ }
21
+ if (now - live.lastAt < TOOL_PROGRESS_MIN_INTERVAL_MS)
22
+ return;
23
+ live.lastAt = now;
24
+ try {
25
+ push({ type: "tool_progress", ...body, elapsedTimeSeconds: Math.floor((now - live.startedAt) / 1000), ...ident() });
26
+ }
27
+ catch {
28
+ }
29
+ };
30
+ function beat() {
31
+ const now = monotonicNow();
32
+ for (const [toolCallId, live] of inFlight) {
33
+ if (now - live.lastAt < TOOL_HEARTBEAT_MS)
34
+ continue;
35
+ emit({ toolCallId, toolName: live.toolName });
36
+ }
37
+ }
38
+ const disarm = () => {
39
+ if (timer === undefined)
40
+ return;
41
+ clearInterval(timer);
42
+ timer = undefined;
43
+ };
44
+ return {
45
+ emit,
46
+ noteStart(toolCallId, toolName) {
47
+ const startedAt = monotonicNow();
48
+ inFlight.set(toolCallId, { toolName, startedAt, lastAt: startedAt });
49
+ arm();
50
+ },
51
+ noteEnd(toolCallId) {
52
+ inFlight.delete(toolCallId);
53
+ if (inFlight.size === 0)
54
+ disarm();
55
+ },
56
+ clear() {
57
+ inFlight.clear();
58
+ disarm();
59
+ },
60
+ };
61
+ }