@sema-agent/core 2.4.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 (34) hide show
  1. package/dist/agents/subagent.js +17 -14
  2. package/dist/core/auto-compaction.d.ts +2 -0
  3. package/dist/core/auto-compaction.js +2 -1
  4. package/dist/core/mcp.js +4 -1
  5. package/dist/core/runner/prepare-task.d.ts +5 -0
  6. package/dist/core/runner/prepare-task.js +21 -1
  7. package/dist/core/runner/runtask.js +39 -9
  8. package/dist/core/runner/tool-disclosure.d.ts +1 -0
  9. package/dist/core/runner/tool-disclosure.js +16 -5
  10. package/dist/core/session-reconcile.d.ts +1 -0
  11. package/dist/core/session-reconcile.js +40 -20
  12. package/dist/core/task-registry-agent.d.ts +2 -0
  13. package/dist/core/task-registry-agent.js +11 -1
  14. package/dist/core/task-registry-shared.d.ts +1 -0
  15. package/dist/core/task-registry.d.ts +2 -0
  16. package/dist/core/task-registry.js +11 -0
  17. package/dist/core/types.d.ts +10 -3
  18. package/dist/engine/compaction/compaction.d.ts +5 -0
  19. package/dist/engine/compaction/compaction.js +68 -2
  20. package/dist/engine/compaction/utils.d.ts +6 -0
  21. package/dist/engine/compaction/utils.js +53 -3
  22. package/dist/engine/harness/messages.d.ts +1 -1
  23. package/dist/engine/harness/messages.js +11 -3
  24. package/dist/engine/loop/types.d.ts +2 -0
  25. package/dist/engine/session/import-validate.js +30 -1
  26. package/dist/engine/session/session.js +7 -5
  27. package/dist/index.d.ts +7 -0
  28. package/dist/index.js +7 -0
  29. package/dist/internal/harness.d.ts +1 -1
  30. package/dist/internal/harness.js +1 -1
  31. package/dist/orchestration/workflow-types.d.ts +1 -0
  32. package/dist/orchestration/workflow.js +10 -1
  33. package/dist/tools/fs/fs-bash.js +11 -5
  34. package/package.json +1 -1
@@ -221,6 +221,9 @@ export function classifySubagentError(child) {
221
221
  : "logic";
222
222
  return { errorKind, retryable: errorKind !== "logic" };
223
223
  }
224
+ function errorKindClause(c) {
225
+ return c !== undefined ? ` (error_kind: ${c.errorKind}, retryable: ${c.retryable})` : "";
226
+ }
224
227
  const FAILED_SESSION_RETAIN_TTL_MS = 15 * 60 * 1000;
225
228
  const PARTIAL_FINDINGS_MAX_CHARS = 1200;
226
229
  const BG_NOTIFY_DRAIN_WINDOW_MS = 2_000;
@@ -427,7 +430,7 @@ export function createSubagentResume(deps) {
427
430
  ...resultSettleFields(child.result),
428
431
  ...(status !== "completed" ? { error: (child.errorMessage ?? `resumed run ${status}`).slice(0, 500) } : {}),
429
432
  ...(errCodeRevive !== undefined ? { errorCode: errCodeRevive } : {}),
430
- ...(errClassRevive !== undefined ? { retryable: errClassRevive.retryable } : {}),
433
+ ...(errClassRevive !== undefined ? { retryable: errClassRevive.retryable, errorKind: errClassRevive.errorKind } : {}),
431
434
  });
432
435
  if (winner !== undefined)
433
436
  status = winner;
@@ -440,7 +443,7 @@ export function createSubagentResume(deps) {
440
443
  const reviveName = deps.rowDescription ?? `sub-agent ${marker}`;
441
444
  const failReasonRevive = status === "failed" ? child.errorMessage : undefined;
442
445
  const reviveTerminalSummary = failReasonRevive !== undefined
443
- ? `Agent "${reviveName}" (resumed) failed: ${failReasonRevive}${ccElapsedTag(Date.now() - reviveStartedAt)}`.slice(0, 300)
446
+ ? `Agent "${reviveName}" (resumed) failed: ${failReasonRevive}${ccElapsedTag(Date.now() - reviveStartedAt)}`.slice(0, 300) + errorKindClause(errClassRevive)
444
447
  : `Agent "${reviveName}" (resumed) ${status === "killed" ? "stopped" : child.status === "completed" ? "finished" : String(child.status)}${ccElapsedTag(Date.now() - reviveStartedAt)}`;
445
448
  const ownsTerminalFacesRevive = deps.registry !== undefined && deps.taskId !== undefined ? deps.registry.claimAgentTerminalNotify(deps.taskId) : true;
446
449
  if (ownsTerminalFacesRevive)
@@ -509,7 +512,7 @@ export function createSubagentResume(deps) {
509
512
  status: abort.signal.aborted ? "killed" : "failed",
510
513
  error: msgRevive.slice(0, 500),
511
514
  ...(errCodeReviveReject !== undefined ? { errorCode: errCodeReviveReject } : {}),
512
- ...(errClassReviveReject !== undefined ? { retryable: errClassReviveReject.retryable } : {}),
515
+ ...(errClassReviveReject !== undefined ? { retryable: errClassReviveReject.retryable, errorKind: errClassReviveReject.errorKind } : {}),
513
516
  });
514
517
  }
515
518
  catch {
@@ -530,7 +533,7 @@ export function createSubagentResume(deps) {
530
533
  status: abort.signal.aborted ? "killed" : "failed",
531
534
  seq: entry.cycleSeq,
532
535
  ...(stoppedByReject !== undefined ? { stoppedBy: stoppedByReject } : {}),
533
- summary: `Agent "${rejectName}" (resumed) ${abort.signal.aborted ? "stopped" : "failed"}${ccElapsedTag(Date.now() - reviveStartedAt)}`.slice(0, 300),
536
+ summary: `Agent "${rejectName}" (resumed) ${abort.signal.aborted ? "stopped" : "failed"}${ccElapsedTag(Date.now() - reviveStartedAt)}`.slice(0, 300) + (abort.signal.aborted ? "" : errorKindClause(errClassReviveReject)),
534
537
  ...(completionIdRejectRevive !== undefined ? { completionId: completionIdRejectRevive } : {}),
535
538
  });
536
539
  try {
@@ -543,7 +546,7 @@ export function createSubagentResume(deps) {
543
546
  status: abort.signal.aborted ? "killed" : "failed",
544
547
  seq: entry.cycleSeq,
545
548
  ...(stoppedByReject !== undefined ? { stoppedBy: stoppedByReject } : {}),
546
- summary: `Agent "${rejectName}" (resumed) ${abort.signal.aborted ? "stopped" : `failed: ${msgRevive}`}${ccElapsedTag(Date.now() - reviveStartedAt)}`.slice(0, 300),
549
+ summary: `Agent "${rejectName}" (resumed) ${abort.signal.aborted ? "stopped" : `failed: ${msgRevive}`}${ccElapsedTag(Date.now() - reviveStartedAt)}`.slice(0, 300) + (abort.signal.aborted ? "" : errorKindClause(errClassReviveReject)),
547
550
  ...(abort.signal.aborted ? {} : { error: msgRevive.slice(0, REPORT_FIELD_MAX) }),
548
551
  ...(!abort.signal.aborted && errCodeReviveReject !== undefined ? { errorCode: errCodeReviveReject } : {}),
549
552
  ...(completionIdRejectRevive !== undefined ? { completionId: completionIdRejectRevive } : {}),
@@ -1753,7 +1756,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1753
1756
  ...resultSettleFields(child.result),
1754
1757
  ...(!okBg ? { error: reapedBg ? (collateralBg ? BG_AGENT_COLLATERAL_REAP_REASON : BG_AGENT_REAP_STOP_ERROR) : child.errorMessage ?? String(child.status) } : {}),
1755
1758
  ...(errCodeFork !== undefined ? { errorCode: errCodeFork } : {}),
1756
- ...(errClassFork !== undefined ? { retryable: errClassFork.retryable } : {}),
1759
+ ...(errClassFork !== undefined ? { retryable: errClassFork.retryable, errorKind: errClassFork.errorKind } : {}),
1757
1760
  }) ?? (abort.signal.aborted ? "killed" : okBg ? "completed" : "failed");
1758
1761
  const stoppedByBg = settledBg === "killed" ? bg.registry.getStopAttribution(taskId) ?? "system" : undefined;
1759
1762
  const completionIdFork = bg.registry.getCompletionId(taskId);
@@ -1770,7 +1773,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1770
1773
  const forkTranscriptId = child.sessionId ?? forkedId;
1771
1774
  const failReasonFork = settledBg === "failed" ? child.errorMessage : undefined;
1772
1775
  const forkTerminalSummary = failReasonFork !== undefined
1773
- ? `Agent "${shortDesc}" failed: ${failReasonFork}${ccElapsedTag(Date.now() - forkBgStartedAt)}`.slice(0, 300)
1776
+ ? `Agent "${shortDesc}" failed: ${failReasonFork}${ccElapsedTag(Date.now() - forkBgStartedAt)}`.slice(0, 300) + errorKindClause(errClassFork)
1774
1777
  : `${ccCompletionText(shortDesc, settledBg, String(child.status), Date.now() - forkBgStartedAt)}`;
1775
1778
  const ownsTerminalFacesFork = bg.registry.claimAgentTerminalNotify(taskId);
1776
1779
  if (ownsTerminalFacesFork)
@@ -1823,11 +1826,11 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1823
1826
  status: killed ? "killed" : "failed",
1824
1827
  error: killed ? BG_AGENT_REAP_STOP_ERROR : msgFork,
1825
1828
  ...(errCodeForkReject !== undefined ? { errorCode: errCodeForkReject } : {}),
1826
- ...(errClassForkReject !== undefined ? { retryable: errClassForkReject.retryable } : {}),
1829
+ ...(errClassForkReject !== undefined ? { retryable: errClassForkReject.retryable, errorKind: errClassForkReject.errorKind } : {}),
1827
1830
  }) ?? (killed ? "killed" : "failed");
1828
1831
  const stoppedByBg = settledBg === "killed" ? bg.registry.getStopAttribution(taskId) ?? "system" : undefined;
1829
1832
  const completionIdForkReject = bg.registry.getCompletionId(taskId);
1830
- const summaryBg = `${shortDesc} — ${settledBg === "failed" ? `failed: ${msgFork}` : settledBg}`.slice(0, 300);
1833
+ const summaryBg = `${shortDesc} — ${settledBg === "failed" ? `failed: ${msgFork}` : settledBg}`.slice(0, 300) + (settledBg === "failed" ? errorKindClause(errClassForkReject) : "");
1831
1834
  const ownsTerminalFacesForkReject = bg.registry.claimAgentTerminalNotify(taskId);
1832
1835
  if (ownsTerminalFacesForkReject)
1833
1836
  sinkEmit({
@@ -2390,7 +2393,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2390
2393
  ...resultSettleFields(child.result),
2391
2394
  ...(!ok ? { error: reaped ? (collateral ? BG_AGENT_COLLATERAL_REAP_REASON : BG_AGENT_REAP_STOP_ERROR) : child.errorMessage ?? String(child.status) } : {}),
2392
2395
  ...(errCodeBg !== undefined ? { errorCode: errCodeBg } : {}),
2393
- ...(errClassBg !== undefined ? { retryable: errClassBg.retryable } : {}),
2396
+ ...(errClassBg !== undefined ? { retryable: errClassBg.retryable, errorKind: errClassBg.errorKind } : {}),
2394
2397
  }) ??
2395
2398
  (abort.signal.aborted ? "killed" : ok ? "completed" : "failed");
2396
2399
  if (!bgRetain && reviveRow === undefined && !(await bgRowConfirmed())) {
@@ -2408,7 +2411,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2408
2411
  const resumableBg = bgRetain !== undefined && settled !== "killed";
2409
2412
  const failReasonBg = settled === "failed" ? child.errorMessage : undefined;
2410
2413
  const bgTerminalSummary = failReasonBg !== undefined
2411
- ? `Agent "${shortDesc}" failed: ${failReasonBg}${ccElapsedTag(Date.now() - bgStartedAt)}${observerNote}`.slice(0, 300)
2414
+ ? `Agent "${shortDesc}" failed: ${failReasonBg}${ccElapsedTag(Date.now() - bgStartedAt)}${observerNote}`.slice(0, 300) + errorKindClause(errClassBg)
2412
2415
  : `${ccCompletionText(shortDesc, settled, String(child.status), Date.now() - bgStartedAt)}${observerNote}`;
2413
2416
  const ownsTerminalFaces = bg.registry.claimAgentTerminalNotify(taskId);
2414
2417
  if (ownsTerminalFaces)
@@ -2485,7 +2488,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2485
2488
  error: msg,
2486
2489
  seq: seqAtSettle ?? 1,
2487
2490
  ...(errCodeBgReject !== undefined ? { errorCode: errCodeBgReject } : {}),
2488
- ...(errClassBgReject !== undefined ? { retryable: errClassBgReject.retryable } : {}),
2491
+ ...(errClassBgReject !== undefined ? { retryable: errClassBgReject.retryable, errorKind: errClassBgReject.errorKind } : {}),
2489
2492
  }) ?? (killed ? "killed" : "failed");
2490
2493
  if (!bgRetain && reviveRow === undefined && !(await bgRowConfirmed())) {
2491
2494
  try {
@@ -2510,7 +2513,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2510
2513
  status: settled,
2511
2514
  ...(seqAtSettle !== undefined ? { seq: seqAtSettle } : {}),
2512
2515
  ...(stoppedBy !== undefined ? { stoppedBy } : {}),
2513
- summary: `${settled === "failed" ? `Agent "${shortDesc}" failed: ${msg}${ccElapsedTag(Date.now() - bgStartedAt)}` : ccCompletionText(shortDesc, settled, settled, Date.now() - bgStartedAt)}${observerNote}`.slice(0, 300),
2516
+ summary: `${settled === "failed" ? `Agent "${shortDesc}" failed: ${msg}${ccElapsedTag(Date.now() - bgStartedAt)}` : ccCompletionText(shortDesc, settled, settled, Date.now() - bgStartedAt)}${observerNote}`.slice(0, 300) + (settled === "failed" ? errorKindClause(errClassBgReject) : ""),
2514
2517
  ...(completionIdBgReject !== undefined ? { completionId: completionIdBgReject } : {}),
2515
2518
  });
2516
2519
  try {
@@ -2523,7 +2526,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2523
2526
  sessionId: bgChildSessionId,
2524
2527
  ...(seqAtSettle !== undefined ? { seq: seqAtSettle } : {}),
2525
2528
  ...(stoppedBy !== undefined ? { stoppedBy } : {}),
2526
- summary: `${settled === "failed" ? `Agent "${shortDesc}" failed: ${msg}${ccElapsedTag(Date.now() - bgStartedAt)}` : ccCompletionText(shortDesc, settled, settled, Date.now() - bgStartedAt)}${observerNote}`.slice(0, 300),
2529
+ summary: `${settled === "failed" ? `Agent "${shortDesc}" failed: ${msg}${ccElapsedTag(Date.now() - bgStartedAt)}` : ccCompletionText(shortDesc, settled, settled, Date.now() - bgStartedAt)}${observerNote}`.slice(0, 300) + (settled === "failed" ? errorKindClause(errClassBgReject) : ""),
2527
2530
  ...(settled === "failed" ? { error: msg.slice(0, REPORT_FIELD_MAX) } : {}),
2528
2531
  ...(settled === "failed" && errCodeBgReject !== undefined ? { errorCode: errCodeBgReject } : {}),
2529
2532
  ...(completionIdBgReject !== undefined ? { completionId: completionIdBgReject } : {}),
@@ -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);
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) {
@@ -608,6 +609,13 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
608
609
  }
609
610
  }
610
611
  }
612
+ if (internals?.onWorkspaceResolved !== undefined) {
613
+ try {
614
+ internals.onWorkspaceResolved({ cwd: taskRootPath, isolated: internals.isolation === "worktree" });
615
+ }
616
+ catch {
617
+ }
618
+ }
611
619
  let rewindTarget = spec.resumeAt !== undefined ? (spec.rewindFiles ? spec.resumeAt : undefined) : spec.rewindFilesTo;
612
620
  const rewindBefore = rewindTarget !== undefined && spec.resumeAt !== undefined && spec.resumeAtMode === "before";
613
621
  if (rewindTarget !== undefined && deps.fileSnapshotStore && handsEnabled) {
@@ -1781,6 +1789,10 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1781
1789
  deferMode: deps.deferMode,
1782
1790
  model,
1783
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
+ ],
1784
1796
  });
1785
1797
  for (const n of [...deferred]) {
1786
1798
  if (!tools.some((t) => t.name === n))
@@ -1820,6 +1832,14 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1820
1832
  if (deferred.has(n))
1821
1833
  activeTools.add(n);
1822
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
+ }
1823
1843
  if (resume) {
1824
1844
  for (const n of resume.seed.activeTools)
1825
1845
  if (deferred.has(n))
@@ -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({
@@ -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";
@@ -68,6 +68,7 @@ export declare function settleBackgroundAgentLane(core: DurableAgentCore, id: st
68
68
  error?: string;
69
69
  errorCode?: string;
70
70
  retryable?: boolean;
71
+ errorKind?: string;
71
72
  stoppedBy?: StopSource;
72
73
  seq?: number;
73
74
  }): "completed" | "failed" | "killed" | undefined;
@@ -102,6 +103,7 @@ export declare function settleRevivedAgentLane(core: DurableAgentCore, id: strin
102
103
  error?: string;
103
104
  errorCode?: string;
104
105
  retryable?: boolean;
106
+ errorKind?: string;
105
107
  }): "completed" | "failed" | "killed" | undefined;
106
108
  export declare function unmarkRetainedContinuationLane(core: DurableAgentCore, id: string): void;
107
109
  export declare function attachAgentNotifyLane(core: DurableAgentCore, id: string, notify: NonNullable<BackgroundAgentTaskHandle["notify"]>, cycle?: number): void;