@sema-agent/core 2.3.0 → 2.5.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 (83) hide show
  1. package/dist/agents/send-message-tool.d.ts +4 -0
  2. package/dist/agents/send-message-tool.js +37 -24
  3. package/dist/agents/subagent.js +279 -128
  4. package/dist/brain/errors.d.ts +1 -0
  5. package/dist/brain/errors.js +14 -0
  6. package/dist/brain/stream-engine.js +3 -3
  7. package/dist/core/auto-compaction.d.ts +2 -0
  8. package/dist/core/auto-compaction.js +2 -1
  9. package/dist/core/context-edit.js +2 -1
  10. package/dist/core/mcp.js +4 -1
  11. package/dist/core/runner/prepare-task.d.ts +5 -0
  12. package/dist/core/runner/prepare-task.js +41 -2
  13. package/dist/core/runner/runtask.js +39 -9
  14. package/dist/core/runner/tool-disclosure.d.ts +1 -0
  15. package/dist/core/runner/tool-disclosure.js +16 -5
  16. package/dist/core/runner/tool-output-projection.js +2 -1
  17. package/dist/core/session-reconcile.d.ts +1 -0
  18. package/dist/core/session-reconcile.js +40 -20
  19. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +37 -0
  20. package/dist/core/store-contracts/checkpoint-store-contract.js +195 -0
  21. package/dist/core/store-contracts/contract-harness.d.ts +6 -0
  22. package/dist/core/store-contracts/contract-harness.js +16 -0
  23. package/dist/core/store-contracts/contract-kit-version.d.ts +1 -0
  24. package/dist/core/store-contracts/contract-kit-version.js +2 -0
  25. package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +3 -0
  26. package/dist/core/store-contracts/file-snapshot-store-contract.js +126 -0
  27. package/dist/core/store-contracts/mailbox-store-contract.d.ts +6 -0
  28. package/dist/core/store-contracts/mailbox-store-contract.js +193 -0
  29. package/dist/core/store-contracts/session-repo-contract.d.ts +3 -0
  30. package/dist/core/store-contracts/session-repo-contract.js +36 -0
  31. package/dist/core/store-contracts/tool-result-store-contract.d.ts +3 -0
  32. package/dist/core/store-contracts/tool-result-store-contract.js +35 -0
  33. package/dist/core/task-notification.d.ts +2 -0
  34. package/dist/core/task-registry-agent.d.ts +6 -0
  35. package/dist/core/task-registry-agent.js +24 -1
  36. package/dist/core/task-registry-monitor.js +6 -6
  37. package/dist/core/task-registry-shared.d.ts +9 -2
  38. package/dist/core/task-registry-shared.js +1 -1
  39. package/dist/core/task-registry.d.ts +8 -0
  40. package/dist/core/task-registry.js +59 -4
  41. package/dist/core/tool-result-store.d.ts +3 -2
  42. package/dist/core/tool-result-store.js +12 -4
  43. package/dist/core/trace.d.ts +7 -0
  44. package/dist/core/types.d.ts +10 -3
  45. package/dist/engine/compaction/compaction.d.ts +5 -0
  46. package/dist/engine/compaction/compaction.js +68 -2
  47. package/dist/engine/compaction/utils.d.ts +6 -0
  48. package/dist/engine/compaction/utils.js +53 -3
  49. package/dist/engine/harness/messages.d.ts +1 -1
  50. package/dist/engine/harness/messages.js +11 -3
  51. package/dist/engine/loop/types.d.ts +2 -0
  52. package/dist/engine/lsp/node-lsp-manager.d.ts +2 -0
  53. package/dist/engine/lsp/node-lsp-manager.js +16 -0
  54. package/dist/engine/session/import-validate.js +30 -1
  55. package/dist/engine/session/session.js +7 -5
  56. package/dist/index.d.ts +8 -0
  57. package/dist/index.js +8 -0
  58. package/dist/internal/harness.d.ts +1 -1
  59. package/dist/internal/harness.js +1 -1
  60. package/dist/orchestration/builtin-workflows.d.ts +1 -1
  61. package/dist/orchestration/builtin-workflows.js +11 -2
  62. package/dist/orchestration/workflow-governance.d.ts +6 -1
  63. package/dist/orchestration/workflow-governance.js +24 -4
  64. package/dist/orchestration/workflow-primitives.js +7 -1
  65. package/dist/orchestration/workflow-types.d.ts +1 -0
  66. package/dist/orchestration/workflow.d.ts +1 -0
  67. package/dist/orchestration/workflow.js +41 -3
  68. package/dist/tools/fs/fs-bash.d.ts +7 -1
  69. package/dist/tools/fs/fs-bash.js +59 -22
  70. package/dist/tools/fs/fs-read.js +22 -11
  71. package/dist/tools/fs/fs-search-tools.js +3 -3
  72. package/dist/tools/fs/fs-shared.d.ts +20 -7
  73. package/dist/tools/fs/fs-shared.js +17 -3
  74. package/dist/tools/fs/fs-write.js +4 -4
  75. package/dist/tools/fs/index.d.ts +2 -0
  76. package/dist/tools/fs/index.js +7 -1
  77. package/dist/tools/fs/repo-map.js +2 -2
  78. package/dist/tools/fs/safety.d.ts +10 -0
  79. package/dist/tools/fs/safety.js +15 -1
  80. package/dist/tools/monitor.js +18 -4
  81. package/dist/tools/web.js +6 -2
  82. package/dist/tools/worktree.js +46 -25
  83. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import { createAssistantMessageEventStream, } from "../internal/llm.js";
2
- import { BrainError, classifyHttp } from "./errors.js";
2
+ import { BrainError, classifyHttp, describeNetworkError } from "./errors.js";
3
3
  import { retryBackoffMs } from "./retry.js";
4
4
  import { emitBrainStatus, emitBrainTelemetry } from "./status-sink.js";
5
5
  import { createConnectController } from "./timeout.js";
@@ -139,7 +139,7 @@ export function runStreamingBrain(args) {
139
139
  continue;
140
140
  }
141
141
  if (netErr)
142
- throw new BrainError("network", netErr instanceof Error ? netErr.message : String(netErr));
142
+ throw new BrainError("network", describeNetworkError(netErr));
143
143
  const detail = r ? await r.text().catch(() => "") : "";
144
144
  const status = r?.status ?? 0;
145
145
  throw new BrainError(classifyHttp(status), `${httpLabel} HTTP ${status || "ERR"}: ${detail.slice(0, 500)}`, status);
@@ -250,7 +250,7 @@ export function runStreamingBrain(args) {
250
250
  throw e;
251
251
  failure = {
252
252
  kind: "connection",
253
- err: new BrainError("network", `mid-stream read failed: ${e instanceof Error ? e.message : String(e)}`),
253
+ err: new BrainError("network", `mid-stream read failed: ${describeNetworkError(e)}`),
254
254
  };
255
255
  break readLoop;
256
256
  }
@@ -96,11 +96,13 @@ export interface MaybeCompactOptions {
96
96
  droppedChars: number;
97
97
  keptChars: number;
98
98
  }) => void;
99
+ activeTools?: ReadonlyArray<string>;
99
100
  }
100
101
  export declare function maybeCompact(opts: MaybeCompactOptions): Promise<{
101
102
  compacted: boolean;
102
103
  blocked?: boolean;
103
104
  disabled?: boolean;
105
+ noop?: true;
104
106
  tokensBefore?: number;
105
107
  freedTokens?: number;
106
108
  triggerTokens?: number;
@@ -86,7 +86,7 @@ export async function maybeCompact(opts) {
86
86
  throw prep.error;
87
87
  }
88
88
  if (!prep.value) {
89
- return { contextUsage, compacted: false };
89
+ return { contextUsage, compacted: false, noop: true };
90
90
  }
91
91
  const trigger = opts.trigger ?? "auto";
92
92
  let hookInstructions;
@@ -454,6 +454,7 @@ export async function maybeCompact(opts) {
454
454
  ...(details ?? {}),
455
455
  promptEpoch: restatedEpoch,
456
456
  ...(restatedListings !== undefined ? { announcedListings: restatedListings } : {}),
457
+ ...(opts.activeTools !== undefined && opts.activeTools.length > 0 ? { activeTools: [...opts.activeTools] } : {}),
457
458
  }, false);
458
459
  try {
459
460
  opts.onApplied?.(attachedComplete, excludedReadStatePreserveKeys);
@@ -1,7 +1,8 @@
1
1
  import { DEFAULT_CHARS_PER_TOKEN, estimateContextTokens, estimateTokens } from "../internal/harness.js";
2
2
  import { isToolResult } from "./message-utils.js";
3
+ import { offloadPagebackHint } from "./tool-result-store.js";
3
4
  const CLEARED_MARKER = "[tool result cleared to save context]";
4
- const refNote = (ref) => `full text persisted; call ReadToolResult with ref "${ref}" to read it back`;
5
+ const refNote = (ref) => offloadPagebackHint(ref, "cleared");
5
6
  const mediaNote = (blocks) => {
6
7
  const byType = new Map();
7
8
  for (const b of blocks) {
package/dist/core/mcp.js CHANGED
@@ -1039,13 +1039,16 @@ function intakeListedTools(listed, spec, client, health, imageResizer) {
1039
1039
  if (axis)
1040
1040
  serverAxes.push(axis);
1041
1041
  const writeEffect = (axis?.effect ?? "write") === "write";
1042
- const mcpMaxResultSizeChars = resolveMcpDeclaredResultSize(t._meta);
1042
+ const mcpToolMeta = t._meta;
1043
+ const mcpMaxResultSizeChars = resolveMcpDeclaredResultSize(mcpToolMeta);
1044
+ const mcpAlwaysLoad = mcpToolMeta?.["anthropic/alwaysLoad"] === true;
1043
1045
  serverTools.push({
1044
1046
  name: namespacedName,
1045
1047
  description: effectiveDescription ?? `MCP tool ${remoteName} from ${spec.name}`,
1046
1048
  label: `${spec.name}:${remoteName}`,
1047
1049
  parameters: (effectiveInputSchema ?? { type: "object" }),
1048
1050
  ...(mcpMaxResultSizeChars !== undefined ? { mcpMaxResultSizeChars } : {}),
1051
+ ...(mcpAlwaysLoad ? { mcpAlwaysLoad: true } : {}),
1049
1052
  execute: async (_toolCallId, params, signal) => {
1050
1053
  const what = `The call to tool ${inlineUntrusted(remoteName)}`;
1051
1054
  if (health.dead)
@@ -327,6 +327,11 @@ export interface RunInternals {
327
327
  requestedCwd?: string;
328
328
  onSubagentSpawn?: (handle: import("../../agents/subagent.js").SubagentSteerHandle) => void;
329
329
  onActivity?: (activity: ToolActivity) => void;
330
+ onWorkspaceResolved?: (workspace: ResolvedWorkspace) => void;
331
+ }
332
+ export interface ResolvedWorkspace {
333
+ cwd: string;
334
+ isolated: boolean;
330
335
  }
331
336
  export declare function batchContextAt(messages: AgentMessage[], currentId: string): {
332
337
  batchToolCallIds: string[];
@@ -1,7 +1,7 @@
1
1
  import { createHash, randomBytes } from "node:crypto";
2
2
  import { realpathSync } from "node:fs";
3
3
  import { resolve as resolveFsPath } from "node:path";
4
- import { AgentHarness, DEFAULT_CHARS_PER_TOKEN, DEFAULT_CLAMP_TOLERANCE, DEFAULT_COMPACTION_SETTINGS, estimateContextTokens, summaryOutputBudgetTokens } from "../../internal/harness.js";
4
+ import { AgentHarness, DEFAULT_CHARS_PER_TOKEN, DEFAULT_CLAMP_TOLERANCE, DEFAULT_COMPACTION_SETTINGS, estimateContextTokens, readCompactionActiveTools, summaryOutputBudgetTokens } from "../../internal/harness.js";
5
5
  const PROMPT_HASH_SALT = randomBytes(16);
6
6
  import { sanitizeCompactionSettings } from "../auto-compaction.js";
7
7
  import { createAutoModeDecider } from "../auto-mode.js";
@@ -177,6 +177,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
177
177
  const toolFaceSnapshot = {
178
178
  exclude: spec.excludeTools ? Object.freeze([...spec.excludeTools]) : undefined,
179
179
  defer: spec.deferTools ? Object.freeze([...spec.deferTools]) : undefined,
180
+ alwaysLoad: spec.alwaysLoadTools ? Object.freeze([...spec.alwaysLoadTools]) : undefined,
180
181
  };
181
182
  const promptProfile = resolveModelPromptTraits({ id: "" }, spec, internals).promptProfile;
182
183
  if (spec.resumeAt !== undefined) {
@@ -435,10 +436,11 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
435
436
  const offloadStore = rawOffloadStore instanceof RunnerSharedToolResultStore
436
437
  ? new ScopedToolResultStore(rawOffloadStore, taskScope)
437
438
  : rawOffloadStore;
439
+ const offloadReachableToolsRef = {};
438
440
  const maybeOffload = (tool, perTool) => {
439
441
  if (!offloadStore || perTool?.offload === false)
440
442
  return tool;
441
- return withToolResultOffload(tool, offloadStore, perTool?.offloadThresholdChars ?? offloadThreshold, sessionId);
443
+ return withToolResultOffload(tool, offloadStore, perTool?.offloadThresholdChars ?? offloadThreshold, sessionId, () => offloadReachableToolsRef.current?.());
442
444
  };
443
445
  const firstPartyOffload = (tool) => {
444
446
  const policy = firstPartyOffloadPolicy(tool.name);
@@ -607,6 +609,13 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
607
609
  }
608
610
  }
609
611
  }
612
+ if (internals?.onWorkspaceResolved !== undefined) {
613
+ try {
614
+ internals.onWorkspaceResolved({ cwd: taskRootPath, isolated: internals.isolation === "worktree" });
615
+ }
616
+ catch {
617
+ }
618
+ }
610
619
  let rewindTarget = spec.resumeAt !== undefined ? (spec.rewindFiles ? spec.resumeAt : undefined) : spec.rewindFilesTo;
611
620
  const rewindBefore = rewindTarget !== undefined && spec.resumeAt !== undefined && spec.resumeAtMode === "before";
612
621
  if (rewindTarget !== undefined && deps.fileSnapshotStore && handsEnabled) {
@@ -1123,6 +1132,14 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1123
1132
  phase: "config",
1124
1133
  sessionId,
1125
1134
  });
1135
+ emitTrace(deps.tracer, () => ({
1136
+ kind: "config.additional_directory_skipped",
1137
+ version: 1,
1138
+ taskId: hostTaskId,
1139
+ entry: dir,
1140
+ reason: `${c.error.code}: ${c.error.message}`,
1141
+ ts: Date.now(),
1142
+ }));
1126
1143
  }
1127
1144
  }
1128
1145
  if (spec.envFacts?.scratchpadDir) {
@@ -1255,6 +1272,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1255
1272
  ...(internals?.parentNotify !== undefined ? { uplink: internals.parentNotify } : {}),
1256
1273
  ...(internals?.explicitAgentName !== undefined ? { senderName: internals.explicitAgentName } : {}),
1257
1274
  ...(internals?.parentRetainLedger !== undefined ? { siblingRetain: internals.parentRetainLedger } : {}),
1275
+ ...(internals?.parentTaskId !== undefined ? { parentTaskId: internals.parentTaskId } : {}),
1276
+ ...(internals?.parentSessionId !== undefined ? { parentSessionId: internals.parentSessionId } : {}),
1258
1277
  ...(deps.rosterStore !== undefined ? { roster: deps.rosterStore } : {}),
1259
1278
  ...(deps.onBackgroundChildEvent ? { onBackgroundChildEvent: deps.onBackgroundChildEvent } : {}),
1260
1279
  ...(deps.backgroundAgentStore !== undefined ? { agentStore: deps.backgroundAgentStore } : {}),
@@ -1770,6 +1789,10 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1770
1789
  deferMode: deps.deferMode,
1771
1790
  model,
1772
1791
  deferNames: (toolFaceSnapshot.defer ?? []).filter((n) => tools.some((t) => t.name === n)),
1792
+ alwaysLoadNames: [
1793
+ ...(toolFaceSnapshot.alwaysLoad ?? []),
1794
+ ...mcp.tools.filter((t) => t.mcpAlwaysLoad === true).map((t) => t.name),
1795
+ ],
1773
1796
  });
1774
1797
  for (const n of [...deferred]) {
1775
1798
  if (!tools.some((t) => t.name === n))
@@ -1809,11 +1832,27 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1809
1832
  if (deferred.has(n))
1810
1833
  activeTools.add(n);
1811
1834
  }
1835
+ for (const entry of await session.getBranch()) {
1836
+ if (entry.type !== "compaction" || entry.fromHook === true)
1837
+ continue;
1838
+ for (const n of readCompactionActiveTools(entry.details)) {
1839
+ if (deferred.has(n))
1840
+ activeTools.add(n);
1841
+ }
1842
+ }
1812
1843
  if (resume) {
1813
1844
  for (const n of resume.seed.activeTools)
1814
1845
  if (deferred.has(n))
1815
1846
  activeTools.add(n);
1816
1847
  }
1848
+ offloadReachableToolsRef.current = () => {
1849
+ const s = new Set(tools.map((t) => t.name));
1850
+ for (const n of deferred)
1851
+ if (!activeTools.has(n))
1852
+ s.delete(n);
1853
+ s.add(TOOL_SEARCH_NAME);
1854
+ return s;
1855
+ };
1817
1856
  let toolSearch;
1818
1857
  const buildToolList = (active) => {
1819
1858
  const list = tools.map((t) => (deferred.has(t.name) && !active.has(t.name) ? placeholders.get(t.name) : t));
@@ -538,6 +538,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
538
538
  prepared.reviewRef.token !== undefined) {
539
539
  if (forceManual) {
540
540
  emitTrace(rs.telemetry.tracer, () => ({ kind: "compaction.mooted", version: 1, taskId: rs.telemetry.taskId, reason: "task_ending", ts: Date.now() }));
541
+ queue.push({ type: "compaction_outcome", outcome: "mooted", trigger: "manual", reason: "task_ending", ...ident() });
541
542
  drainManualCompact("mooted");
542
543
  }
543
544
  return undefined;
@@ -552,6 +553,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
552
553
  reason: "compaction circuit breaker open (3 consecutive failures this task) — manual request drained without an attempt",
553
554
  ts: Date.now(),
554
555
  }));
556
+ queue.push({ type: "compaction_outcome", outcome: "failed", trigger: "manual", reason: "compaction circuit breaker open (3 consecutive failures this task) — manual request drained without an attempt", ...ident() });
555
557
  drainManualCompact("failed");
556
558
  }
557
559
  return undefined;
@@ -590,6 +592,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
590
592
  if (trimPressureArmed)
591
593
  prepared.trimPressureRef.droppedMessages = false;
592
594
  const trimPressure = trimPressureArmed && !rs.counters.trimForceBackoff;
595
+ const passTrigger = trimPressure ? "forced" : forceManual ? "manual" : "auto";
593
596
  let manualOutcome = "failed";
594
597
  if (rs.counters.finalizeInjected && forceManual) {
595
598
  emitTrace(rs.telemetry.tracer, () => ({ kind: "compaction.manual_in_finalize", version: 1, taskId: rs.telemetry.taskId, ts: Date.now() }));
@@ -617,6 +620,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
617
620
  onInputTruncated: (info) => emitTrace(rs.telemetry.tracer, () => ({ kind: "compaction.input_truncated", version: 1, taskId: rs.telemetry.taskId, label: info.label, droppedChars: info.droppedChars, keptChars: info.keptChars, ts: Date.now() })),
618
621
  forceUnderThreshold: trimPressure,
619
622
  overheadTokens: prepared.promptOverheadTokens,
623
+ ...(prepared.activeTools.size > 0 ? { activeTools: [...prepared.activeTools] } : {}),
620
624
  workingFileAttachments: spec.compaction?.attachWorkingFiles !== false && prepared.readTaskFile
621
625
  ? {
622
626
  readFile: prepared.readTaskFile,
@@ -632,7 +636,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
632
636
  ...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
633
637
  ...runnerHooks.seamCCompactionOptions(prepared),
634
638
  ...windowSafetyOptions(event.model),
635
- ...runnerHooks.compactionHookOptions(spec, prepared.sessionId, trimPressure ? "forced" : forceManual ? "manual" : "auto"),
639
+ ...runnerHooks.compactionHookOptions(spec, prepared.sessionId, passTrigger),
636
640
  });
637
641
  if (comp.blocked) {
638
642
  emitTrace(rs.telemetry.tracer, () => ({
@@ -670,17 +674,32 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
670
674
  emitTrace(rs.telemetry.tracer, () => ({ kind: "compaction.noop", version: 1, taskId: rs.telemetry.taskId, ts: Date.now() }));
671
675
  }
672
676
  }
677
+ if (!comp.compacted) {
678
+ const outcome = comp.blocked
679
+ ? "blocked"
680
+ : comp.disabled
681
+ ? (forceManual || trimPressure ? "disabled" : undefined)
682
+ : comp.suppressedByFloor
683
+ ? "suppressed"
684
+ : comp.noop || forceManual
685
+ ? "noop"
686
+ : undefined;
687
+ if (outcome !== undefined) {
688
+ queue.push({ type: "compaction_outcome", outcome, trigger: passTrigger, ...ident() });
689
+ }
690
+ }
673
691
  if (comp.compacted && !forceManual) {
674
692
  if (recordCompactionAndCheckRapidRefill(rapidRefill, stats.turns)) {
675
693
  compactionBreaker.failures = MAX_CONSECUTIVE_COMPACTION_FAILURES;
676
694
  runnerHooks.onError?.(new Error("compaction.rapid_refill: the context refilled within <3 turns of compaction 3 times in a row — compaction disabled for the rest of this task (thrash spiral; the transcript is dominated by incompressible content)"), { phase: "compaction", sessionId: prepared.sessionId });
695
+ queue.push({ type: "compaction_outcome", outcome: "disabled", trigger: passTrigger, reason: "rapid_refill: compaction disabled for the rest of this task", ...ident() });
677
696
  }
678
697
  }
679
698
  runnerHooks.recordCompactionReuse(prepared, comp);
680
699
  if (comp.compacted) {
681
700
  queue.push({
682
701
  type: "compacted",
683
- trigger: forceManual ? "manual" : "auto",
702
+ trigger: passTrigger,
684
703
  tokensBefore: comp.tokensBefore ?? 0,
685
704
  ...(comp.postTriggerTokens !== undefined ? { tokensAfter: comp.postTriggerTokens } : {}),
686
705
  ...(comp.triggerTokens !== undefined ? { triggerTokensBefore: comp.triggerTokens } : {}),
@@ -692,6 +711,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
692
711
  ...(comp.fallbackReason !== undefined ? { fallbackReason: comp.fallbackReason } : {}),
693
712
  ...(comp.clampedRatio !== undefined ? { clampedRatio: comp.clampedRatio } : {}),
694
713
  ...(comp.clampReason !== undefined ? { clampReason: comp.clampReason } : {}),
714
+ ...ident(),
695
715
  });
696
716
  if (comp.phaseDurations !== undefined && comp.durationMs !== undefined) {
697
717
  const pd = comp.phaseDurations;
@@ -713,6 +733,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
713
733
  const manualCancelAbort = !prepared.abortController.signal.aborted && isCompactionManualCancel(err);
714
734
  if (manualCancelAbort) {
715
735
  emitTrace(rs.telemetry.tracer, () => ({ kind: "compaction.mooted", version: 1, taskId: rs.telemetry.taskId, reason: "cancelled", ts: Date.now() }));
736
+ queue.push({ type: "compaction_outcome", outcome: "mooted", trigger: passTrigger, reason: "cancelled", ...ident() });
716
737
  manualOutcome = "mooted";
717
738
  }
718
739
  else if (!prepared.abortController.signal.aborted) {
@@ -721,18 +742,20 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
721
742
  compactionBreaker.failures += 1;
722
743
  runnerHooks.onError?.(err, { phase: "compaction", sessionId: prepared.sessionId });
723
744
  const msg = String(err instanceof Error ? err.message : err);
745
+ const failReason = walltimeAbort
746
+ ? "walltime soft-deadline abort (engine-initiated write-out protection; not counted toward the breaker)"
747
+ : msg.length > 512
748
+ ? `${msg.slice(0, 512)}…`
749
+ : msg;
724
750
  emitTrace(rs.telemetry.tracer, () => ({
725
751
  kind: "compaction.failed",
726
752
  version: 1,
727
753
  taskId: rs.telemetry.taskId,
728
- trigger: trimPressure ? "forced" : forceManual ? "manual" : "auto",
729
- reason: walltimeAbort
730
- ? "walltime soft-deadline abort (engine-initiated write-out protection; not counted toward the breaker)"
731
- : msg.length > 512
732
- ? `${msg.slice(0, 512)}…`
733
- : msg,
754
+ trigger: passTrigger,
755
+ reason: failReason,
734
756
  ts: Date.now(),
735
757
  }));
758
+ queue.push({ type: "compaction_outcome", outcome: "failed", trigger: passTrigger, reason: failReason, ...ident() });
736
759
  manualOutcome = "failed";
737
760
  }
738
761
  else {
@@ -2319,6 +2342,7 @@ export class Runner {
2319
2342
  minTokens: 0,
2320
2343
  force: true,
2321
2344
  overheadTokens: prepared.promptOverheadTokens,
2345
+ ...(prepared.activeTools.size > 0 ? { activeTools: [...prepared.activeTools] } : {}),
2322
2346
  onInputTruncated: (info) => emitTrace(rs.telemetry.tracer, () => ({ kind: "compaction.input_truncated", version: 1, taskId: rs.telemetry.taskId, label: info.label, droppedChars: info.droppedChars, keptChars: info.keptChars, ts: Date.now() })),
2323
2347
  workingFileAttachments: spec.compaction?.attachWorkingFiles !== false && prepared.readTaskFile
2324
2348
  ? {
@@ -2346,7 +2370,7 @@ export class Runner {
2346
2370
  }
2347
2371
  queue.push({
2348
2372
  type: "compacted",
2349
- trigger: "auto",
2373
+ trigger: "forced",
2350
2374
  tokensBefore: comp.tokensBefore ?? 0,
2351
2375
  ...(comp.postTriggerTokens !== undefined ? { tokensAfter: comp.postTriggerTokens } : {}),
2352
2376
  ...(comp.triggerTokens !== undefined ? { triggerTokensBefore: comp.triggerTokens } : {}),
@@ -2358,6 +2382,7 @@ export class Runner {
2358
2382
  ...(comp.fallbackReason !== undefined ? { fallbackReason: comp.fallbackReason } : {}),
2359
2383
  ...(comp.clampedRatio !== undefined ? { clampedRatio: comp.clampedRatio } : {}),
2360
2384
  ...(comp.clampReason !== undefined ? { clampReason: comp.clampReason } : {}),
2385
+ ...ident(),
2361
2386
  });
2362
2387
  if (comp.phaseDurations !== undefined && comp.durationMs !== undefined) {
2363
2388
  const pd = comp.phaseDurations;
@@ -2372,12 +2397,15 @@ export class Runner {
2372
2397
  }
2373
2398
  else {
2374
2399
  compactionBreaker.failures += 1;
2400
+ queue.push({ type: "compaction_outcome", outcome: comp.noop ? "noop" : "failed", trigger: "forced", reason: "prompt-too-long recovery pass did not land", ...ident() });
2375
2401
  }
2376
2402
  return comp.compacted === true;
2377
2403
  }
2378
2404
  catch (err) {
2379
2405
  if (!isCompactionWalltimeAbort(err))
2380
2406
  compactionBreaker.failures += 1;
2407
+ const msg = String(err instanceof Error ? err.message : err);
2408
+ queue.push({ type: "compaction_outcome", outcome: "failed", trigger: "forced", reason: msg.length > 512 ? `${msg.slice(0, 512)}…` : msg, ...ident() });
2381
2409
  return false;
2382
2410
  }
2383
2411
  },
@@ -2661,6 +2689,7 @@ export class Runner {
2661
2689
  ...(comp.fallbackReason !== undefined ? { fallbackReason: comp.fallbackReason } : {}),
2662
2690
  ...(comp.clampedRatio !== undefined ? { clampedRatio: comp.clampedRatio } : {}),
2663
2691
  ...(comp.clampReason !== undefined ? { clampReason: comp.clampReason } : {}),
2692
+ ...ident(),
2664
2693
  });
2665
2694
  if (comp.phaseDurations !== undefined && comp.durationMs !== undefined) {
2666
2695
  const pd = comp.phaseDurations;
@@ -3453,6 +3482,7 @@ export class Runner {
3453
3482
  customInstructions: spec.compaction?.instructions ?? DEFAULT_COMPACTION_INSTRUCTIONS,
3454
3483
  minTokens: opts?.minTokens,
3455
3484
  overheadTokens: prepared.promptOverheadTokens,
3485
+ ...(prepared.activeTools.size > 0 ? { activeTools: [...prepared.activeTools] } : {}),
3456
3486
  onInputTruncated: (info) => emitTrace(spec.tracer ?? this.deps.tracer, () => ({
3457
3487
  kind: "compaction.input_truncated",
3458
3488
  version: 1,
@@ -21,6 +21,7 @@ export declare function classifyDeferred(opts: {
21
21
  deferMode?: "auto";
22
22
  model: Model;
23
23
  deferNames?: ReadonlyArray<string>;
24
+ alwaysLoadNames?: ReadonlyArray<string>;
24
25
  }): Set<string>;
25
26
  export declare function buildDeferredRegistry(deferred: ReadonlySet<string>, tools: ReadonlyArray<{
26
27
  name: string;
@@ -24,17 +24,24 @@ function inlinedChars(t) {
24
24
  return t.description.length + schema.length;
25
25
  }
26
26
  export function classifyDeferred(opts) {
27
+ const pinned = new Set(opts.alwaysLoadNames ?? []);
28
+ for (const s of opts.specs) {
29
+ if (s.alwaysLoad === true)
30
+ pinned.add(s.name);
31
+ }
27
32
  const deferred = new Set();
28
33
  for (const s of opts.specs) {
29
- if (s.defer === true)
34
+ if (s.defer === true && !pinned.has(s.name))
30
35
  deferred.add(s.name);
31
36
  }
32
37
  for (const name of opts.mcpToolNames)
33
- deferred.add(name);
38
+ if (!pinned.has(name))
39
+ deferred.add(name);
34
40
  for (const name of opts.deferNames ?? [])
35
- deferred.add(name);
41
+ if (!pinned.has(name))
42
+ deferred.add(name);
36
43
  if (opts.deferMode === "auto") {
37
- const candidates = opts.fullTools.filter((t) => !deferred.has(t.name));
44
+ const candidates = opts.fullTools.filter((t) => !deferred.has(t.name) && !pinned.has(t.name));
38
45
  const total = candidates.reduce((n, t) => n + inlinedChars(t), 0);
39
46
  const window = (opts.model.contextTokens ?? opts.model.contextWindow ?? 0) * CHARS_PER_TOKEN;
40
47
  if (window > 0 && total > DEFER_AUTO_FRACTION * window) {
@@ -159,10 +166,14 @@ export function createToolSearchTool(opts) {
159
166
  name: TOOL_SEARCH_NAME,
160
167
  contract: { contractId: "core.tool_search@1", implementationRevision: "1" },
161
168
  description: "Discover and activate deferred tools. Most tools start as name-only placeholders to keep requests " +
162
- "small; to USE one you must activate it here first. Query forms: " +
169
+ "small; to USE one you must activate it here first. When any instruction, reminder, or another " +
170
+ 'tool\'s description names a deferred tool, activate it with query "select:<name>" before calling it. ' +
171
+ "Query forms: " +
163
172
  '"select:ToolA,ToolB" — activate these exact tools by name; ' +
164
173
  '"notebook jupyter" — keyword search, up to max_results best matches; ' +
165
174
  "a bare tool name — activates that tool directly. " +
175
+ "Activate every tool you expect to need in one call (select accepts a comma-separated list) " +
176
+ "rather than one at a time. " +
166
177
  "Activated tools become callable with their full parameters on your next turn.",
167
178
  parameters: Type.Object({
168
179
  query: Type.Optional(Type.String({
@@ -57,10 +57,11 @@ export const toolOutputFrom = (result) => {
57
57
  return { output: raw, truncated: true, totalChars };
58
58
  };
59
59
  const CC_DETAIL_TYPES = new Set([
60
- "edit", "multiedit", "create", "update", "bash", "notebook-edit", "notebook", "text", "grep", "glob", "mcp",
60
+ "edit", "multiedit", "create", "update", "bash", "notebook-edit", "notebook", "file_unchanged", "worktree", "text", "grep", "glob", "mcp",
61
61
  "agent", "task", "task-list", "task-output", "memory-saved", "workflow-run",
62
62
  "web-fetch", "web-search", "todo", "cron-create", "cron-delete", "cron-list", "image",
63
63
  "task-stop", "tool-search", "memory-recall", "repo-map", "fork", "enter-plan-mode", "exit-plan-mode",
64
+ "monitor-start", "path_not_in_root",
64
65
  ]);
65
66
  export const structuredFrom = (result) => {
66
67
  const details = result !== null && typeof result === "object" ? result.details : undefined;
@@ -4,6 +4,7 @@ import type { ToolEffect } from "./types.js";
4
4
  export interface OrphanToolCall {
5
5
  toolCallId: string;
6
6
  toolName: string;
7
+ kind?: "result";
7
8
  }
8
9
  export interface ReconcileReport {
9
10
  recovered: Array<OrphanToolCall & {
@@ -13,6 +13,7 @@ const INTERRUPTED_NEVER_STARTED = "[INTERRUPTED] The run was aborted before this
13
13
  "no side effects — it is safe to re-issue this call if you still need it.";
14
14
  export function findOrphanToolCalls(messages, suspendedBatch) {
15
15
  const resolvedAt = new Map();
16
+ const callSitesAt = new Map();
16
17
  messages.forEach((m, i) => {
17
18
  if (m.role === "toolResult") {
18
19
  const at = resolvedAt.get(m.toolCallId);
@@ -21,35 +22,54 @@ export function findOrphanToolCalls(messages, suspendedBatch) {
21
22
  else
22
23
  at.push(i);
23
24
  }
24
- });
25
- const nextReuseAfter = (id, from) => {
26
- for (let i = from + 1; i < messages.length; i++) {
27
- const m = messages[i];
28
- if (m?.role !== "assistant")
29
- continue;
30
- if (m.content.some((p) => p.type === "toolCall" && p.id === id))
31
- return i;
25
+ else if (m.role === "assistant") {
26
+ for (const part of m.content) {
27
+ if (part.type !== "toolCall")
28
+ continue;
29
+ const at = callSitesAt.get(part.id);
30
+ if (at === undefined)
31
+ callSitesAt.set(part.id, [i]);
32
+ else
33
+ at.push(i);
34
+ }
32
35
  }
33
- return Number.POSITIVE_INFINITY;
34
- };
36
+ });
37
+ const nextReuseAfter = (id, from) => callSitesAt.get(id)?.find((j) => j > from) ?? Number.POSITIVE_INFINITY;
35
38
  const orphans = [];
36
39
  messages.forEach((m, i) => {
37
- if (m.role !== "assistant")
38
- return;
39
- for (const part of m.content) {
40
- if (part.type !== "toolCall" || suspendedBatch?.has(part.id))
41
- continue;
42
- const bound = nextReuseAfter(part.id, i);
43
- const closed = (resolvedAt.get(part.id) ?? []).some((at) => at > i && at < bound);
44
- if (!closed)
45
- orphans.push({ toolCallId: part.id, toolName: part.name });
40
+ if (m.role === "assistant") {
41
+ for (const part of m.content) {
42
+ if (part.type !== "toolCall" || suspendedBatch?.has(part.id))
43
+ continue;
44
+ const bound = nextReuseAfter(part.id, i);
45
+ const closed = (resolvedAt.get(part.id) ?? []).some((at) => at > i && at < bound);
46
+ if (!closed)
47
+ orphans.push({ toolCallId: part.id, toolName: part.name });
48
+ }
49
+ }
50
+ else if (m.role === "toolResult") {
51
+ if (suspendedBatch?.has(m.toolCallId))
52
+ return;
53
+ const sites = callSitesAt.get(m.toolCallId) ?? [];
54
+ const results = resolvedAt.get(m.toolCallId) ?? [];
55
+ let paired = false;
56
+ for (const j of sites) {
57
+ if (j >= i)
58
+ break;
59
+ if (!results.some((at) => at > j && at < i)) {
60
+ paired = true;
61
+ break;
62
+ }
63
+ }
64
+ if (!paired)
65
+ orphans.push({ toolCallId: m.toolCallId, toolName: m.toolName, kind: "result" });
46
66
  }
47
67
  });
48
68
  return orphans;
49
69
  }
50
70
  export async function reconcileInterruptedSession(session, toolEffects, suspendedBatch, startedToolCallIds) {
51
71
  const { messages } = await session.buildContext();
52
- const orphans = findOrphanToolCalls(messages, suspendedBatch);
72
+ const orphans = findOrphanToolCalls(messages, suspendedBatch).filter((o) => o.kind !== "result");
53
73
  const recovered = [];
54
74
  for (const orphan of orphans) {
55
75
  const effect = toolEffects?.get(canonicalToolName(orphan.toolName)) ?? "write";
@@ -0,0 +1,37 @@
1
+ import { type Checkpoint, type CheckpointStore, type ResumeOutcome } from "../checkpoint-store.js";
2
+ import { type ContractAssertionRunner } from "./contract-harness.js";
3
+ export declare function createCheckpointFixture(over?: Partial<Checkpoint>): Checkpoint;
4
+ export declare const ALLOW: Extract<ResumeOutcome, {
5
+ gate: "policy_ask";
6
+ }>;
7
+ export declare function checkpointStoreContract(make: () => CheckpointStore, runAssertion?: ContractAssertionRunner): Promise<void>;
8
+ export declare function checkpointListByScopeSummaries(store: CheckpointStore): Promise<import("../checkpoint-store.js").CheckpointSummary[]>;
9
+ export declare const EXPECTED_LISTBYSCOPE_SUMMARIES: ({
10
+ token: string;
11
+ sessionId: string;
12
+ scope: string;
13
+ gateKind: string;
14
+ severity: number;
15
+ deadline: number;
16
+ createdAt: number;
17
+ sourceTaskId: string;
18
+ principal: string;
19
+ toolCallId: string;
20
+ toolName: string;
21
+ toolInput: string;
22
+ spentMicroUsd?: undefined;
23
+ } | {
24
+ token: string;
25
+ sessionId: string;
26
+ scope: string;
27
+ gateKind: string;
28
+ spentMicroUsd: number;
29
+ createdAt: number;
30
+ severity?: undefined;
31
+ deadline?: undefined;
32
+ sourceTaskId?: undefined;
33
+ principal?: undefined;
34
+ toolCallId?: undefined;
35
+ toolName?: undefined;
36
+ toolInput?: undefined;
37
+ })[];