@sema-agent/core 5.8.0 → 5.10.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 (68) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/dist/agents/cascade.js +24 -0
  3. package/dist/agents/roster-store.d.ts +1 -0
  4. package/dist/agents/send-message-tool.js +6 -0
  5. package/dist/agents/subagent.d.ts +33 -0
  6. package/dist/agents/subagent.js +125 -33
  7. package/dist/agents/teacher.js +15 -3
  8. package/dist/agents/team.js +10 -0
  9. package/dist/agents/verify.js +7 -0
  10. package/dist/brain/anthropic.js +27 -10
  11. package/dist/brain/open-responses.d.ts +11 -0
  12. package/dist/brain/open-responses.js +736 -0
  13. package/dist/brain/openai.js +32 -5
  14. package/dist/brain/request-params.d.ts +1 -0
  15. package/dist/brain/request-params.js +16 -0
  16. package/dist/core/a2a.js +1 -1
  17. package/dist/core/fs-write-gate-policy.js +2 -2
  18. package/dist/core/lsp-diagnostics.d.ts +3 -2
  19. package/dist/core/lsp-diagnostics.js +20 -7
  20. package/dist/core/memory-recall.js +8 -3
  21. package/dist/core/memory.d.ts +5 -0
  22. package/dist/core/memory.js +6 -4
  23. package/dist/core/runner/assemble-result.d.ts +1 -0
  24. package/dist/core/runner/assemble-result.js +14 -7
  25. package/dist/core/runner/prepare-task.d.ts +9 -1
  26. package/dist/core/runner/prepare-task.js +51 -14
  27. package/dist/core/runner/runtask.d.ts +12 -0
  28. package/dist/core/runner/runtask.js +153 -42
  29. package/dist/core/runner/session-file-state-replay.d.ts +7 -0
  30. package/dist/core/runner/session-file-state-replay.js +56 -0
  31. package/dist/core/runner/session-rule-policy.d.ts +1 -0
  32. package/dist/core/runner/session-rule-policy.js +4 -3
  33. package/dist/core/runner/synthetic-tools.js +1 -1
  34. package/dist/core/runner/tool-output-projection.js +5 -4
  35. package/dist/core/session-reconcile.d.ts +7 -3
  36. package/dist/core/session-reconcile.js +3 -2
  37. package/dist/core/strategy-store.d.ts +1 -1
  38. package/dist/core/strategy-store.js +27 -4
  39. package/dist/core/task-registry-shared.d.ts +0 -1
  40. package/dist/core/tool-policy.d.ts +8 -0
  41. package/dist/core/tool-policy.js +11 -0
  42. package/dist/core/tools.js +9 -1
  43. package/dist/core/trace.d.ts +0 -2
  44. package/dist/core/types.d.ts +8 -1
  45. package/dist/engine/harness/agent-harness.d.ts +1 -0
  46. package/dist/engine/harness/agent-harness.js +3 -0
  47. package/dist/engine/harness/types.d.ts +1 -0
  48. package/dist/engine/llm/types.d.ts +2 -73
  49. package/dist/engine/loop/agent-loop.js +168 -22
  50. package/dist/engine/loop/types.d.ts +1 -0
  51. package/dist/engine/session/repo-utils.d.ts +1 -2
  52. package/dist/engine/session/repo-utils.js +0 -7
  53. package/dist/index.d.ts +3 -2
  54. package/dist/index.js +2 -1
  55. package/dist/internal/llm.d.ts +1 -1
  56. package/dist/orchestration/run-workflow-tool.js +1 -1
  57. package/dist/orchestration/workflow-governance.js +19 -0
  58. package/dist/orchestration/workflow-primitives.d.ts +1 -1
  59. package/dist/orchestration/workflow-primitives.js +4 -1
  60. package/dist/orchestration/workflow.js +15 -6
  61. package/dist/prompts/coordinator.d.ts +1 -1
  62. package/dist/prompts/coordinator.js +1 -1
  63. package/dist/stores/file/memory-store.js +3 -7
  64. package/dist/tools/fs/fs-bash.js +3 -3
  65. package/dist/tools/fs/fs-shared.d.ts +1 -0
  66. package/dist/tools/fs/fs-shared.js +4 -0
  67. package/dist/tools/web.js +20 -20
  68. package/package.json +5 -3
@@ -4,6 +4,7 @@ import type { RunInternals } from "./prepare-task.js";
4
4
  import { type TaskOutcome } from "../task-outcome.js";
5
5
  import { type SideQuerySpec, type SideQueryResult } from "../side-query.js";
6
6
  import type { SessionStore } from "../session.js";
7
+ import { type RecoveredOrphan } from "../session-reconcile.js";
7
8
  import type { AgentDefinition, RunnerDeps, TaskEvent, TaskResult, TaskSpec, TaskStream } from "../types.js";
8
9
  export type ResumeTaskConfig = Omit<TaskSpec, "objective" | "sessionId">;
9
10
  interface ResumeRun {
@@ -19,6 +20,17 @@ interface ResumeRun {
19
20
  onEnvRestoreFailed?: (reason: ReopenReason) => Promise<void>;
20
21
  decisionDelivered?: boolean;
21
22
  }
23
+ declare function toolEndBodyFrom(result: unknown, isError: boolean): {
24
+ output?: unknown;
25
+ truncated?: boolean;
26
+ totalChars?: number;
27
+ structured?: unknown;
28
+ errorCode?: string;
29
+ };
30
+ export declare function reconciledToolEndBody(orphan: Pick<RecoveredOrphan, "text" | "errorKind">): ReturnType<typeof toolEndBodyFrom>;
31
+ export declare const GOVERNANCE_READ_STALLED: unique symbol;
32
+ export declare function awaitChargeWithSlowDisclosure<T>(charge: Promise<T>, onSlow: () => void, discloseAfterMs?: number): Promise<T>;
33
+ export declare function raceUntilDeadline<T>(p: Promise<T>, deadline: number): Promise<T | typeof GOVERNANCE_READ_STALLED>;
22
34
  export declare class Runner {
23
35
  private deps;
24
36
  readonly sessions: SessionStore;
@@ -24,7 +24,7 @@ import { cacheFamilyOf, usageCostMicroUsd } from "./usage-accounting.js";
24
24
  import { assembleResult, errorCodeOf } from "./assemble-result.js";
25
25
  import { ATTACHMENT_BYTE_CAP, CHANGED_FILES_MAX, AGENT_LISTING_REMOVED_HEADER, SKILLS_LISTING_DELTA_HEADER, SKILLS_LISTING_REMOVED_HEADER, advanceCadenceClock, agentListingDeltaHeader, agentListingInitialHeader, replayAnnouncedListing, replayAnnouncedModels, clipToBytes, collectDateChange, collectDueAttachments, collectInstructionsChange, commitAgentListing, commitInstructionsChange, commitSkillsListing, createAttachmentState, rebaseCadenceWindows, reduceToolEnd, renderAgentListingDelta, renderMcpDroppedTools, renderMcpInstructionsDelta, renderOrphanedBackgroundTasks, selectMcpDroppedBatch, renderSkillsListingDelta, renderToolsDelta, stampWriteAnchor } from "./turn-attachments.js";
26
26
  import { buildWorkingFileAttachments, centerAdoptionOption, emitInputTruncated } from "./compaction-call-options.js";
27
- import { prepareTask } from "./prepare-task.js";
27
+ import { prepareTask, resolveCheckpointStore } from "./prepare-task.js";
28
28
  import { settleTeardownLeg } from "./teardown-bounded.js";
29
29
  import { TOOL_SEARCH_NAME } from "./tool-disclosure.js";
30
30
  import { hasVerifiableStructureSignal } from "./grounding-signal.js";
@@ -113,6 +113,19 @@ function resumeDecisionWasNegative(resume) {
113
113
  }
114
114
  const DEFERRED_REISSUE = "[DEFERRED] This tool call shared a batch with a call that suspended for durable approval, so it was " +
115
115
  "NOT executed on resume. If you still need it, issue it again now.";
116
+ function toolEndBodyFrom(result, isError) {
117
+ const o = toolOutputFrom(result);
118
+ const st = structuredFrom(result);
119
+ const code = isError ? result?.details?.code : undefined;
120
+ return {
121
+ ...(o !== undefined ? { output: o.output, ...(o.truncated ? { truncated: true } : {}), ...(o.totalChars !== undefined ? { totalChars: o.totalChars } : {}) } : {}),
122
+ ...(st !== undefined ? { structured: st } : {}),
123
+ ...(typeof code === "string" ? { errorCode: code } : {}),
124
+ };
125
+ }
126
+ export function reconciledToolEndBody(orphan) {
127
+ return toolEndBodyFrom({ content: orphan.text, details: { code: orphan.errorKind } }, true);
128
+ }
116
129
  function deepJsonEqual(a, b) {
117
130
  if (a === b)
118
131
  return true;
@@ -230,6 +243,42 @@ function resumeContinuation(resume) {
230
243
  `conversation above. Do NOT restart the task or re-run any tool you already ran; continue from this ` +
231
244
  `exact point, building on the existing results, and finish the remaining work.`);
232
245
  }
246
+ const ENV_DUE_GOVERNANCE_READ_BUDGET_MS = 5_000;
247
+ const MAX_TIMER_DELAY_MS = 2_147_483_647;
248
+ const DEADLINE_TICK = Symbol("deadline-tick");
249
+ export const GOVERNANCE_READ_STALLED = Symbol("governance-read-stalled");
250
+ const CHARGE_SETTLE_DISCLOSE_MS = 10_000;
251
+ export async function awaitChargeWithSlowDisclosure(charge, onSlow, discloseAfterMs = CHARGE_SETTLE_DISCLOSE_MS) {
252
+ let timer = setTimeout(() => {
253
+ timer = undefined;
254
+ try {
255
+ onSlow();
256
+ }
257
+ catch {
258
+ }
259
+ }, discloseAfterMs);
260
+ try {
261
+ return await charge;
262
+ }
263
+ finally {
264
+ if (timer !== undefined)
265
+ clearTimeout(timer);
266
+ }
267
+ }
268
+ export async function raceUntilDeadline(p, deadline) {
269
+ for (let firstPass = true;; firstPass = false) {
270
+ const remaining = deadline - Date.now();
271
+ if (remaining <= 0 && !firstPass)
272
+ return GOVERNANCE_READ_STALLED;
273
+ let timer;
274
+ const tick = new Promise((res) => {
275
+ timer = setTimeout(() => res(DEADLINE_TICK), Math.max(0, Math.min(remaining, MAX_TIMER_DELAY_MS)));
276
+ });
277
+ const out = await Promise.race([p, tick]).finally(() => clearTimeout(timer));
278
+ if (out !== DEADLINE_TICK)
279
+ return out;
280
+ }
281
+ }
233
282
  function platformLimitTerminal(reason, retryAfterMs, moment = "turn_boundary") {
234
283
  const message = moment === "entry"
235
284
  ? `a deployment usage window is exhausted (RunnerDeps.usageWindows), so the task was refused before its first model call — nothing ran and nothing was spent. There is no checkpoint to suspend into at this point, so the caller re-submits after the window frees, in ${String(retryAfterMs ?? 0)}ms.`
@@ -257,17 +306,39 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
257
306
  let platformRetryAfterMs;
258
307
  const preemptWillSuspend = preemptHit && prepared.suspendForResource !== undefined;
259
308
  if (!preemptWillSuspend && prepared.suspendRef.token === undefined && !prepared.abortController.signal.aborted) {
260
- if (prepared.envLifetimeSuspendAt !== undefined && Date.now() >= prepared.envLifetimeSuspendAt) {
261
- platformCause = "env_lifetime";
262
- }
263
- else if (prepared.usageGovernance !== undefined) {
309
+ const ledgerReadDeadline = prepared.envLifetimeSuspendAt !== undefined ? prepared.envLifetimeSuspendAt + ENV_DUE_GOVERNANCE_READ_BUDGET_MS : undefined;
310
+ if (prepared.usageGovernance !== undefined) {
264
311
  const governance = prepared.usageGovernance;
265
312
  try {
266
313
  const at = Date.now();
267
- await governance.commit(stats.tokens, at);
268
- platformRetryAfterMs = await governance.check(at);
269
- if (platformRetryAfterMs !== undefined)
270
- platformCause = "usage_window";
314
+ await awaitChargeWithSlowDisclosure(governance.commit(stats.tokens, at), () => runnerHooks.onError?.(new Error("the usage ledger CHARGE has not settled after 10s — still waiting (a charge is never abandoned: walking away would double-charge on the end-of-task flush). A wedged ledger store wedges this boundary, now visibly."), { phase: "config", sessionId: prepared.sessionId }));
315
+ const windowRead = governance.check(at);
316
+ let answer;
317
+ if (ledgerReadDeadline !== undefined) {
318
+ answer = await raceUntilDeadline(windowRead, ledgerReadDeadline);
319
+ if (answer === GOVERNANCE_READ_STALLED) {
320
+ void windowRead.catch((lateErr) => {
321
+ try {
322
+ runnerHooks.onError?.(lateErr instanceof Error ? lateErr : new Error(String(lateErr)), { phase: "config", sessionId: prepared.sessionId });
323
+ }
324
+ catch {
325
+ }
326
+ });
327
+ try {
328
+ runnerHooks.onError?.(new Error("the usage ledger did not answer before the execution environment's suspend deadline — stopping for the environment, the cause this run can still prove. The window state for this boundary is UNKNOWN and was not enforced."), { phase: "config", sessionId: prepared.sessionId });
329
+ }
330
+ catch {
331
+ }
332
+ }
333
+ }
334
+ else {
335
+ answer = await windowRead;
336
+ }
337
+ if (answer !== GOVERNANCE_READ_STALLED) {
338
+ platformRetryAfterMs = answer;
339
+ if (platformRetryAfterMs !== undefined)
340
+ platformCause = "usage_window";
341
+ }
271
342
  }
272
343
  catch (govErr) {
273
344
  rs.limits.platformTerminal = govErr instanceof Error ? govErr : new Error(String(govErr));
@@ -276,6 +347,9 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
276
347
  return undefined;
277
348
  }
278
349
  }
350
+ if (platformCause === undefined && prepared.envLifetimeSuspendAt !== undefined && Date.now() >= prepared.envLifetimeSuspendAt) {
351
+ platformCause = "env_lifetime";
352
+ }
279
353
  }
280
354
  if (platformCause !== undefined) {
281
355
  const platformSpend = { costMicroUsd: stats.costMicroUsd, tokens: stats.tokens, turns: stats.turns, walltimeMs: Math.round(performance.now() - rs.telemetry.taskStartMonotonic) };
@@ -380,7 +454,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
380
454
  emitTrace(rs.telemetry.tracer, () => ({ kind: "review.dropped", version: 1, taskId: rs.telemetry.taskId, ts: Date.now() }));
381
455
  }
382
456
  if (prepared.lspDiagnostics && !prepared.lspDiagnostics.registry.isEmpty() && !prepared.abortController.signal.aborted) {
383
- const files = prepared.lspDiagnostics.registry.drain();
457
+ const files = prepared.lspDiagnostics.registry.drain(prepared.lspDiagnostics.runIdent);
384
458
  if (files.length > 0) {
385
459
  queue.push({ type: "diagnostics", files, isNew: true, ...ident() });
386
460
  const block = formatDiagnosticsBlock(files);
@@ -913,7 +987,8 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
913
987
  : {}),
914
988
  ts: callEndAt,
915
989
  }));
916
- if (prepared.cacheBreakDetector && prepared.cacheFingerprint) {
990
+ const cacheRowUnknown = m.usageMissing === true || m.stopReason === "aborted";
991
+ if (prepared.cacheBreakDetector && prepared.cacheFingerprint && !cacheRowUnknown) {
917
992
  const finding = prepared.cacheBreakDetector.observe({
918
993
  turn: stats.turns + 1,
919
994
  systemPrompt: prepared.cacheFingerprint.systemPrompt,
@@ -1049,16 +1124,7 @@ function makeHarnessHandlers(prepared, stats, rs, deps) {
1049
1124
  toolName: event.toolName,
1050
1125
  ...(toolLabels.get(event.toolName) !== undefined ? { label: toolLabels.get(event.toolName) } : {}),
1051
1126
  isError: event.isError,
1052
- ...(() => {
1053
- const o = toolOutputFrom(event.result);
1054
- return o !== undefined
1055
- ? { output: o.output, ...(o.truncated ? { truncated: true } : {}), ...(o.totalChars !== undefined ? { totalChars: o.totalChars } : {}) }
1056
- : {};
1057
- })(),
1058
- ...(() => {
1059
- const st = structuredFrom(event.result);
1060
- return st !== undefined ? { structured: st } : {};
1061
- })(),
1127
+ ...toolEndBodyFrom(event.result, event.isError),
1062
1128
  ...ident(),
1063
1129
  });
1064
1130
  announceWorkspaceMove();
@@ -1312,6 +1378,10 @@ export class Runner {
1312
1378
  finally {
1313
1379
  releaseLock?.();
1314
1380
  publishReady(handle);
1381
+ manualCompactRef.closed = true;
1382
+ if (manualCompactRef.waiters.length > 0)
1383
+ manualCompactRef.emitMooted?.("task_ending");
1384
+ manualCompactRef.emitMooted = undefined;
1315
1385
  drainManualCompactWaiters("mooted");
1316
1386
  }
1317
1387
  })();
@@ -1500,6 +1570,8 @@ export class Runner {
1500
1570
  throw steeringError("the task is not running");
1501
1571
  if (opts?.signal?.aborted)
1502
1572
  return "mooted";
1573
+ if (manualCompactRef.closed)
1574
+ return "mooted";
1503
1575
  return new Promise((resolve) => {
1504
1576
  const signal = opts?.signal;
1505
1577
  const entry = {
@@ -1515,6 +1587,7 @@ export class Runner {
1515
1587
  if (manualCompactRef.waiters.length === 0) {
1516
1588
  manualCompactRef.requested = false;
1517
1589
  }
1590
+ manualCompactRef.emitMooted?.("cancelled");
1518
1591
  entry.resolve("mooted");
1519
1592
  }
1520
1593
  };
@@ -1648,6 +1721,13 @@ export class Runner {
1648
1721
  }, this);
1649
1722
  notificationHarness = prepared.harness;
1650
1723
  notificationSessionId = prepared.sessionId;
1724
+ const runSourceTaskId = spec.taskId ?? prepared.sessionId;
1725
+ const parentToolCallId = internals?.parentToolCallId;
1726
+ const ident = () => parentToolCallId !== undefined ? { eventId: uuidv7(), parentToolCallId, sourceTaskId: runSourceTaskId } : { eventId: uuidv7() };
1727
+ notificationIdent = ident;
1728
+ manualCompactRef.emitMooted = (reason) => {
1729
+ queue.push({ type: "compaction_outcome", outcome: "mooted", trigger: "manual", reason, ...ident() });
1730
+ };
1651
1731
  prepared.harness.onUndrainedEngineNotes = (payloads) => {
1652
1732
  if (notificationSessionId === undefined)
1653
1733
  return;
@@ -1666,7 +1746,7 @@ export class Runner {
1666
1746
  if (pendingIdle !== undefined) {
1667
1747
  for (const payload of discloseDroppedPending(pendingIdle)) {
1668
1748
  deliveredAtTurnOpen.add(taskNotificationDedupKey(payload));
1669
- queue.push({ type: "task_notification", notification: payload });
1749
+ queue.push({ type: "task_notification", notification: payload, ...ident() });
1670
1750
  void prepared.harness.nextTurn(renderTaskNotificationXml(payload), { provenance: "engine-note", enginePayload: payload }).catch(() => {
1671
1751
  this.pendingSessionNotifications.pend(prepared.sessionId, payload);
1672
1752
  });
@@ -1715,7 +1795,7 @@ export class Runner {
1715
1795
  rs.limits.outputRetryCap = resolveOutputRetries(spec.outputRetries);
1716
1796
  rs.limits.effectiveMaxTurns = resolveMaxTurns(spec.limits);
1717
1797
  rs.telemetry.tracer = spec.tracer ?? this.deps.tracer;
1718
- rs.telemetry.taskId = spec.taskId ?? prepared.sessionId;
1798
+ rs.telemetry.taskId = runSourceTaskId;
1719
1799
  if (taskIdRef) {
1720
1800
  taskIdRef.current = rs.telemetry.taskId;
1721
1801
  taskIdRef.sessionId = prepared.sessionId;
@@ -2063,9 +2143,6 @@ export class Runner {
2063
2143
  const walltimeMonotonicDeadline = effectiveTimeoutMs !== undefined ? rs.telemetry.taskStartMonotonic + effectiveTimeoutMs : undefined;
2064
2144
  rs.counters.walltimeSyncBackstopFired = false;
2065
2145
  const timeout = startTimeout(prepared.harness, prepared.abortController, walltimeMonotonicDeadline !== undefined ? walltimeMonotonicDeadline - performance.now() : undefined, prepared.suspendForResource !== undefined);
2066
- const parentToolCallId = internals?.parentToolCallId;
2067
- const ident = () => parentToolCallId !== undefined ? { eventId: uuidv7(), parentToolCallId, sourceTaskId: rs.telemetry.taskId } : { eventId: uuidv7() };
2068
- notificationIdent = ident;
2069
2146
  const pushContent = (e) => {
2070
2147
  queue.push(e);
2071
2148
  if (parentToolCallId !== undefined && internals?.onForwardEvent) {
@@ -2113,6 +2190,10 @@ export class Runner {
2113
2190
  };
2114
2191
  const withBrainSinks = (fn) => runWithStatusSink(statusEmit, () => runWithBrainTelemetry(telemetryEmit, fn));
2115
2192
  const toolLabels = new Map(prepared.tools.flatMap((t) => (t.label !== undefined && t.label !== t.name ? [[t.name, t.label]] : [])));
2193
+ for (const orphan of prepared.wakeRecovered) {
2194
+ queue.push({ type: "tool_end", toolCallId: orphan.toolCallId, toolName: orphan.toolName, ...(toolLabels.has(orphan.toolName) ? { label: toolLabels.get(orphan.toolName) } : {}), isError: true, ...reconciledToolEndBody(orphan), ...ident() });
2195
+ emitCommitted(orphan.entryId, "toolResult", orphan.toolCallId);
2196
+ }
2116
2197
  const postToolBatchHook = (spec.hooks ?? this.deps.hooks)?.postToolBatch;
2117
2198
  const batchArgs = postToolBatchHook ? new Map() : undefined;
2118
2199
  rs.turn.toolBatch = [];
@@ -2446,7 +2527,7 @@ export class Runner {
2446
2527
  if (resume) {
2447
2528
  const walltimeExhaustedResume = effectiveTimeoutMs !== undefined && effectiveTimeoutMs <= 0;
2448
2529
  if (!walltimeExhaustedResume && resume.outcome.gate !== "wake") {
2449
- await this.applyResumeDecision(prepared, resume, (e) => queue.push(e), emitCommitted, (toolName, details) => {
2530
+ await this.applyResumeDecision(prepared, resume, (e) => pushContent({ ...e, ...ident() }), emitCommitted, (toolName, details) => {
2450
2531
  if (rs.attach.attachState === undefined)
2451
2532
  return;
2452
2533
  const family = writeFamilyOf(toolName);
@@ -2638,6 +2719,7 @@ export class Runner {
2638
2719
  }
2639
2720
  finally {
2640
2721
  timeout.clear();
2722
+ prepared.lspDiagnostics?.registry.releaseRun(prepared.lspDiagnostics.runIdent);
2641
2723
  prepared.abortController.abort();
2642
2724
  prepared.releaseSignal();
2643
2725
  try {
@@ -2658,7 +2740,7 @@ export class Runner {
2658
2740
  }
2659
2741
  if (prepared.usageGovernance !== undefined) {
2660
2742
  try {
2661
- await prepared.usageGovernance.commit(stats.tokens, Date.now());
2743
+ await awaitChargeWithSlowDisclosure(prepared.usageGovernance.commit(stats.tokens, Date.now()), () => this.deps.onError?.(new Error("the usage ledger's FINAL charge has not settled after 10s — still waiting (a charge is never abandoned: walking away would double-charge on a later flush). A wedged ledger store wedges this teardown, now visibly."), { phase: "config", sessionId: prepared.sessionId }));
2662
2744
  }
2663
2745
  catch (flushErr) {
2664
2746
  this.deps.onError?.(flushErr, { phase: "config", sessionId: prepared.sessionId });
@@ -2673,7 +2755,7 @@ export class Runner {
2673
2755
  try {
2674
2756
  const report = await reconcileInterruptedSession(prepared.session, prepared.toolEffects, undefined, startedToolCallIds);
2675
2757
  for (const orphan of report.recovered) {
2676
- queue.push({ type: "tool_end", toolCallId: orphan.toolCallId, toolName: orphan.toolName, ...(toolLabels.has(orphan.toolName) ? { label: toolLabels.get(orphan.toolName) } : {}), isError: true, ...ident() });
2758
+ queue.push({ type: "tool_end", toolCallId: orphan.toolCallId, toolName: orphan.toolName, ...(toolLabels.has(orphan.toolName) ? { label: toolLabels.get(orphan.toolName) } : {}), isError: true, ...reconciledToolEndBody(orphan), ...ident() });
2677
2759
  emitCommitted(orphan.entryId, "toolResult", orphan.toolCallId);
2678
2760
  }
2679
2761
  }
@@ -2779,6 +2861,7 @@ export class Runner {
2779
2861
  unpricedSpend: rs.telemetry.unpricedSpend,
2780
2862
  rewindNotes: prepared.rewindNotes,
2781
2863
  remoteEnvFailures: prepared.remoteEnvFailures,
2864
+ retryAfterMs: rs.limits.platformTerminal?.retryAfterMs,
2782
2865
  abortedForTimeout: timeout.fired,
2783
2866
  abortedForTurns: rs.limits.turnsExceeded,
2784
2867
  abortedLive,
@@ -2870,7 +2953,7 @@ export class Runner {
2870
2953
  const committedToken = prepared.suspendRef.token ?? prepared.reviewRef.token;
2871
2954
  const committedScope = prepared.suspendRef.scope ?? prepared.reviewRef.scope;
2872
2955
  if (committedToken !== undefined) {
2873
- const store = spec.checkpointStore ?? this.deps.checkpointStore;
2956
+ const store = resolveCheckpointStore(spec, this.deps);
2874
2957
  if (store && committedScope !== undefined) {
2875
2958
  onSuspend({
2876
2959
  env: prepared.ownedEnv,
@@ -2926,6 +3009,11 @@ export class Runner {
2926
3009
  catch {
2927
3010
  }
2928
3011
  }
3012
+ if (manualCompactRef.waiters.length > 0) {
3013
+ queue.push({ type: "compaction_outcome", outcome: "mooted", trigger: "manual", reason: "task_ending", ...ident() });
3014
+ drainManualCompact("mooted");
3015
+ }
3016
+ manualCompactRef.emitMooted = undefined;
2929
3017
  queue.push({ type: "done", result });
2930
3018
  queue.close();
2931
3019
  if (spec.rewindFiles && result.status === "completed") {
@@ -3114,7 +3202,7 @@ export class Runner {
3114
3202
  return stream.result();
3115
3203
  }
3116
3204
  async resumeStream(token, outcome, taskConfig, internals) {
3117
- const store = taskConfig.checkpointStore ?? this.deps.checkpointStore;
3205
+ const store = resolveCheckpointStore(taskConfig, this.deps);
3118
3206
  if (!store) {
3119
3207
  throw new CheckpointError("checkpoint.not_found", "no CheckpointStore wired — cannot resume (set RunnerDeps.checkpointStore or taskConfig.checkpointStore)");
3120
3208
  }
@@ -3349,13 +3437,16 @@ export class Runner {
3349
3437
  const completed = new Set(pendingAction.completedCallIds);
3350
3438
  const deferredIds = pendingAction.batchToolCallIds.filter((id) => id !== pendingAction.toolCallId && !completed.has(id));
3351
3439
  const names = new Map();
3440
+ const deferredArgs = new Map();
3352
3441
  if (deferredIds.length > 0) {
3353
3442
  const { messages } = await prepared.session.buildContext();
3354
3443
  for (const m of messages) {
3355
3444
  if (m.role === "assistant") {
3356
3445
  for (const c of m.content) {
3357
- if (c.type === "toolCall")
3446
+ if (c.type === "toolCall") {
3358
3447
  names.set(c.id, c.name);
3448
+ deferredArgs.set(c.id, c.arguments);
3449
+ }
3359
3450
  }
3360
3451
  }
3361
3452
  }
@@ -3366,7 +3457,12 @@ export class Runner {
3366
3457
  }
3367
3458
  else if (!completed.has(id)) {
3368
3459
  const name = names.get(id) ?? "unknown";
3369
- emit({ type: "tool_end", toolCallId: id, toolName: name, ...((() => { const l = prepared.tools.find((t) => t.name === name)?.label; return l !== undefined && l !== name ? { label: l } : {}; })()), isError: true });
3460
+ const displayLabel = (() => {
3461
+ const l = prepared.tools.find((t) => t.name === name)?.label;
3462
+ return l !== undefined && l !== name ? { label: l } : {};
3463
+ })();
3464
+ emit({ type: "tool_start", toolCallId: id, toolName: name, ...displayLabel, args: deferredArgs.get(id) ?? {} });
3465
+ emit({ type: "tool_end", toolCallId: id, toolName: name, ...displayLabel, isError: true, ...toolEndBodyFrom({ content: formatHookFeedback(DEFERRED_REISSUE) }, true) });
3370
3466
  const eid = await prepared.session.appendMessage(toolResultMsg(id, name, formatHookFeedback(DEFERRED_REISSUE), true));
3371
3467
  emitCommitted(eid, "toolResult", id);
3372
3468
  }
@@ -3380,10 +3476,10 @@ export class Runner {
3380
3476
  const resolvedArgs = pendingAction.toolName === ASK_USER_QUESTION_TOOL_NAME ? pendingAction.args : (outcome.updatedInput ?? pendingAction.args);
3381
3477
  const pendingLabel = (() => { const l = prepared.tools.find((t) => t.name === pendingAction.toolName)?.label; return l !== undefined && l !== pendingAction.toolName ? { label: l } : {}; })();
3382
3478
  emit({ type: "tool_start", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, args: resolvedArgs });
3383
- const emitEnd = (isError) => emit({ type: "tool_end", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, isError });
3479
+ const emitEnd = (isError, result) => emit({ type: "tool_end", toolCallId: pendingAction.toolCallId, toolName: pendingAction.toolName, ...pendingLabel, isError, ...toolEndBodyFrom(result, isError) });
3384
3480
  if (outcome.decision === "deny") {
3385
- emitEnd(true);
3386
3481
  const reason = outcome.reason ? delimitUntrusted("reviewer note", outcome.reason) : `The pending tool call "${pendingAction.toolName}" was denied by an approver.`;
3482
+ emitEnd(true, { content: formatHookFeedback(reason) });
3387
3483
  const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, formatHookFeedback(reason), true));
3388
3484
  emitCommitted(eid, "toolResult", pendingAction.toolCallId);
3389
3485
  return;
@@ -3391,8 +3487,9 @@ export class Runner {
3391
3487
  if (outcome.decision === "allow" && outcome.updatedInput !== undefined && prepared.basePolicyForResumeEdit) {
3392
3488
  const rechecked = refuseOutOfContractDecision(await prepared.basePolicyForResumeEdit.check({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId }, prepared.abortController.signal));
3393
3489
  if (rechecked.action === "deny") {
3394
- emitEnd(true);
3395
- const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, formatHookFeedback(`The approver EDITED this call's input; the edited call is denied by the deployment's tool policy and was not executed${rechecked.message ? `: ${rechecked.message}` : ""}.`), true));
3490
+ const editedDenial = formatHookFeedback(`The approver EDITED this call's input; the edited call is denied by the deployment's tool policy and was not executed${rechecked.message ? `: ${rechecked.message}` : ""}.`);
3491
+ emitEnd(true, { content: editedDenial });
3492
+ const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, editedDenial, true));
3396
3493
  emitCommitted(eid, "toolResult", pendingAction.toolCallId);
3397
3494
  return;
3398
3495
  }
@@ -3400,8 +3497,9 @@ export class Runner {
3400
3497
  if (prepared.denyNarrowingPolicy) {
3401
3498
  const narrowed = refuseOutOfContractDecision(await prepared.denyNarrowingPolicy.check({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId }, prepared.abortController.signal));
3402
3499
  if (narrowed.action === "deny") {
3403
- emitEnd(true);
3404
- const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, formatHookFeedback(`The approved tool call "${pendingAction.toolName}" is now denied by a session rule and was not executed${narrowed.message ? `: ${narrowed.message}` : ""}.`), true));
3500
+ const narrowedDenial = formatHookFeedback(`The approved tool call "${pendingAction.toolName}" is now denied by a session rule and was not executed${narrowed.message ? `: ${narrowed.message}` : ""}.`);
3501
+ emitEnd(true, { content: narrowedDenial });
3502
+ const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, narrowedDenial, true));
3405
3503
  emitCommitted(eid, "toolResult", pendingAction.toolCallId);
3406
3504
  return;
3407
3505
  }
@@ -3419,13 +3517,26 @@ export class Runner {
3419
3517
  res = await tool.execute(pendingAction.toolCallId, args, prepared.abortController.signal);
3420
3518
  }
3421
3519
  catch (err) {
3422
- emitEnd(true);
3423
- const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, `Error: ${err instanceof Error ? err.message : String(err)}`, true));
3520
+ const execError = `Error: ${err instanceof Error ? err.message : String(err)}`;
3521
+ const marks = (() => {
3522
+ if (err === null || typeof err !== "object")
3523
+ return undefined;
3524
+ const src = err;
3525
+ let d;
3526
+ if (src.details !== null && typeof src.details === "object" && !Array.isArray(src.details)) {
3527
+ d = { ...src.details };
3528
+ }
3529
+ if (typeof src.errorKind === "string")
3530
+ d = { ...(d ?? {}), errorKind: src.errorKind };
3531
+ return d;
3532
+ })();
3533
+ emitEnd(true, { content: execError, ...(marks !== undefined ? { details: marks } : {}) });
3534
+ const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, execError, true));
3424
3535
  emitCommitted(eid, "toolResult", pendingAction.toolCallId);
3425
3536
  return;
3426
3537
  }
3427
3538
  const executedIsError = res.isError === true;
3428
- emitEnd(executedIsError);
3539
+ emitEnd(executedIsError, res);
3429
3540
  onResolvedToolSuccess?.(pendingAction.toolName, executedIsError ? undefined : res.details);
3430
3541
  const eid = await prepared.session.appendMessage({
3431
3542
  role: "toolResult",
@@ -0,0 +1,7 @@
1
+ import type { AgentMessage } from "../../internal/harness.js";
2
+ export interface TranscriptFileRecord {
3
+ path: string;
4
+ content: string;
5
+ at: number;
6
+ }
7
+ export declare function wholeFileRecordsFromTranscript(messages: readonly AgentMessage[]): TranscriptFileRecord[];
@@ -0,0 +1,56 @@
1
+ import { isAbsolutePathForm } from "../../tools/fs/safety.js";
2
+ const READ_TOOL = "Read";
3
+ const WRITE_TOOL = "Write";
4
+ const RETRACTING_RESULTS = [
5
+ { toolName: "Edit", type: "edit", pathField: "filePath" },
6
+ { toolName: "NotebookEdit", type: "notebook-edit", pathField: "notebookPath" },
7
+ ];
8
+ function cardOf(details) {
9
+ if (typeof details !== "object" || details === null)
10
+ return undefined;
11
+ const rest = details;
12
+ return typeof rest.type === "string" ? { type: rest.type, rest } : undefined;
13
+ }
14
+ function wholeFileFromReadCard(rest) {
15
+ const file = rest.file;
16
+ if (typeof file !== "object" || file === null)
17
+ return undefined;
18
+ const f = file;
19
+ if (typeof f.content !== "string" || f.truncatedByTokenCap === true)
20
+ return undefined;
21
+ if (f.startLine !== 1 || typeof f.numLines !== "number" || f.numLines !== f.totalLines)
22
+ return undefined;
23
+ return f.content;
24
+ }
25
+ export function wholeFileRecordsFromTranscript(messages) {
26
+ const byPath = new Map();
27
+ for (const m of messages) {
28
+ if (m.role !== "toolResult" || m.isError)
29
+ continue;
30
+ const card = cardOf(m.details);
31
+ if (card === undefined)
32
+ continue;
33
+ const { type, rest } = card;
34
+ const retracts = RETRACTING_RESULTS.find((r) => r.toolName === m.toolName && r.type === type);
35
+ if (retracts !== undefined) {
36
+ const changed = rest[retracts.pathField];
37
+ if (typeof changed === "string")
38
+ byPath.delete(changed);
39
+ continue;
40
+ }
41
+ const isRead = m.toolName === READ_TOOL && type === "text";
42
+ const isWrite = m.toolName === WRITE_TOOL && (type === "create" || type === "update");
43
+ if (!isRead && !isWrite)
44
+ continue;
45
+ const nested = rest.file;
46
+ const holder = isRead ? (typeof nested === "object" && nested !== null ? nested : undefined) : rest;
47
+ const filePath = holder?.filePath;
48
+ if (typeof filePath !== "string" || !isAbsolutePathForm(filePath))
49
+ continue;
50
+ const content = isRead ? wholeFileFromReadCard(rest) : typeof rest.content === "string" ? rest.content : undefined;
51
+ if (content === undefined)
52
+ continue;
53
+ byPath.set(filePath, { path: filePath, content, at: m.timestamp });
54
+ }
55
+ return [...byPath.values()];
56
+ }
@@ -3,6 +3,7 @@ import type { ToolEffect } from "../types.js";
3
3
  import { type NamedToolPolicy } from "../tool-policy.js";
4
4
  import type { SessionPermissionRules } from "../session-policy-store.js";
5
5
  export declare const PATH_WRITE_TOOLS: ReadonlySet<string>;
6
+ export declare const PATH_CONFINABLE_WRITE_TOOLS: ReadonlySet<string>;
6
7
  export declare function isWithin(root: string, p: string): boolean;
7
8
  export declare function createSessionRulePolicy(rules: SessionPermissionRules, opts: {
8
9
  env: ExecutionEnv;
@@ -1,7 +1,8 @@
1
- import { canonicalizeTarget, fileArgPath } from "../../tools/fs/safety.js";
1
+ import { canonicalizeTarget, writeTargetPath } from "../../tools/fs/safety.js";
2
2
  import { isWinFormPath } from "../../tools/fs/safety.js";
3
3
  import { createCoarseCommandNamePolicy } from "../tool-policy.js";
4
4
  export const PATH_WRITE_TOOLS = new Set(["Write", "Edit", "MultiEdit"]);
5
+ export const PATH_CONFINABLE_WRITE_TOOLS = new Set([...PATH_WRITE_TOOLS, "NotebookEdit"]);
5
6
  export function isWithin(root, p) {
6
7
  if (!root)
7
8
  return false;
@@ -46,14 +47,14 @@ export function createSessionRulePolicy(rules, opts) {
46
47
  return d;
47
48
  }
48
49
  if (allowDirs) {
49
- if (!PATH_WRITE_TOOLS.has(toolName)) {
50
+ if (!PATH_CONFINABLE_WRITE_TOOLS.has(toolName)) {
50
51
  const eff = toolEffects?.get(toolName) ?? "write";
51
52
  if (eff !== "read") {
52
53
  return deny(`write-capable tool "${req.toolName}" denied: session rule confines writes to allowDirs but this tool cannot be path-confined`);
53
54
  }
54
55
  }
55
56
  else {
56
- const path = fileArgPath(req.args);
57
+ const path = writeTargetPath(toolName, req.args);
57
58
  if (typeof path !== "string" || path.length === 0) {
58
59
  return deny(`write tool "${req.toolName}" denied: session rule confines writes to allowDirs but the call has no resolvable path`);
59
60
  }
@@ -78,7 +78,7 @@ export function createReportFindingsTool() {
78
78
  const count = findings.length;
79
79
  return {
80
80
  content: count === 0 ? "No findings reported." : `${count} finding${count === 1 ? "" : "s"} reported.`,
81
- details: { count, ...(level !== undefined ? { level } : {}), findings },
81
+ details: { type: "report-findings", count, ...(level !== undefined ? { level } : {}), findings },
82
82
  };
83
83
  },
84
84
  });
@@ -57,11 +57,12 @@ 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", "file_unchanged", "worktree", "text", "grep", "glob", "mcp",
61
- "agent", "task", "task-list", "task-output", "memory-saved", "workflow-run",
60
+ "edit", "create", "update", "bash", "notebook-edit", "notebook", "file_unchanged", "worktree", "text", "grep", "glob", "mcp",
61
+ "agent", "task", "task-list", "task-output", "workflow-run",
62
62
  "web-fetch", "web-search", "todo", "cron-create", "cron-delete", "cron-list", "image",
63
- "task-stop", "tool-search", "memory-recall", "repo-map", "fork", "enter-plan-mode", "exit-plan-mode",
64
- "monitor-start", "path_not_in_root",
63
+ "task-stop", "tool-search", "repo-map", "fork", "enter-plan-mode", "exit-plan-mode",
64
+ "monitor-start", "path_not_in_root", "readonly_out_of_root",
65
+ "report-findings", "schedule-wakeup", "send-message", "agent-transcript", "a2a", "document",
65
66
  ]);
66
67
  export const structuredFrom = (result) => {
67
68
  const details = result !== null && typeof result === "object" ? result.details : undefined;
@@ -6,10 +6,14 @@ export interface OrphanToolCall {
6
6
  toolName: string;
7
7
  kind?: "result";
8
8
  }
9
+ export type ReconciledErrorKind = "interrupted_never_started" | "interrupted_outcome_unknown";
10
+ export type RecoveredOrphan = OrphanToolCall & {
11
+ entryId: string;
12
+ text: string;
13
+ errorKind: ReconciledErrorKind;
14
+ };
9
15
  export interface ReconcileReport {
10
- recovered: Array<OrphanToolCall & {
11
- entryId: string;
12
- }>;
16
+ recovered: RecoveredOrphan[];
13
17
  }
14
18
  export declare function findOrphanToolCalls(messages: AgentMessage[], suspendedBatch?: ReadonlySet<string>): OrphanToolCall[];
15
19
  export declare function reconcileInterruptedSession(session: Session, toolEffects?: Map<string, ToolEffect>, suspendedBatch?: ReadonlySet<string>, startedToolCallIds?: ReadonlySet<string>): Promise<ReconcileReport>;
@@ -75,16 +75,17 @@ export async function reconcileInterruptedSession(session, toolEffects, suspende
75
75
  const effectText = effect === "read" ? INTERRUPTED_SAFE : effect === "idempotent" ? INTERRUPTED_IDEMPOTENT : INTERRUPTED_UNKNOWN;
76
76
  const neverStarted = startedToolCallIds !== undefined && !startedToolCallIds.has(orphan.toolCallId);
77
77
  const text = neverStarted ? INTERRUPTED_NEVER_STARTED : effectText;
78
+ const errorKind = neverStarted ? "interrupted_never_started" : "interrupted_outcome_unknown";
78
79
  const entryId = await session.appendMessage({
79
80
  role: "toolResult",
80
81
  toolCallId: orphan.toolCallId,
81
82
  toolName: orphan.toolName,
82
83
  content: [{ type: "text", text }],
83
- details: { errorKind: neverStarted ? "interrupted_never_started" : "interrupted_outcome_unknown" },
84
+ details: { errorKind },
84
85
  isError: true,
85
86
  timestamp: Date.now(),
86
87
  });
87
- recovered.push({ ...orphan, entryId });
88
+ recovered.push({ ...orphan, entryId, text, errorKind });
88
89
  }
89
90
  return { recovered };
90
91
  }
@@ -14,8 +14,8 @@ export interface StrategyStore {
14
14
  prune?(scope: string, maxSize: number): Promise<void> | void;
15
15
  }
16
16
  export declare class InMemoryStrategyStore implements StrategyStore {
17
- private readonly maxPerScope;
18
17
  private byScope;
18
+ private readonly maxPerScope;
19
19
  constructor(maxPerScope?: number);
20
20
  save(s: StoredStrategy): void;
21
21
  find(scope: string, query: string, limit: number): StoredStrategy[];