@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
@@ -10,6 +10,8 @@ export interface TaskNotificationPayload {
10
10
  source?: string;
11
11
  stoppedBy?: "user" | "parent" | "system" | (string & {});
12
12
  summary: string;
13
+ error?: string;
14
+ errorCode?: string;
13
15
  exitCode?: number;
14
16
  result?: string;
15
17
  partial?: boolean;
@@ -66,6 +66,9 @@ export declare function settleBackgroundAgentLane(core: DurableAgentCore, id: st
66
66
  result?: string;
67
67
  resultFull?: string;
68
68
  error?: string;
69
+ errorCode?: string;
70
+ retryable?: boolean;
71
+ errorKind?: string;
69
72
  stoppedBy?: StopSource;
70
73
  seq?: number;
71
74
  }): "completed" | "failed" | "killed" | undefined;
@@ -98,6 +101,9 @@ export declare function settleRevivedAgentLane(core: DurableAgentCore, id: strin
98
101
  result?: string;
99
102
  resultFull?: string;
100
103
  error?: string;
104
+ errorCode?: string;
105
+ retryable?: boolean;
106
+ errorKind?: string;
101
107
  }): "completed" | "failed" | "killed" | undefined;
102
108
  export declare function unmarkRetainedContinuationLane(core: DurableAgentCore, id: string): void;
103
109
  export declare function attachAgentNotifyLane(core: DurableAgentCore, id: string, notify: NonNullable<BackgroundAgentTaskHandle["notify"]>, cycle?: number): void;
@@ -3,6 +3,7 @@ import { uuidv7 } from "../internal/harness.js";
3
3
  import { canAccessAgentRecord, BackgroundAgentStoreError, } from "./background-agent-store.js";
4
4
  import { shutdownDebug } from "./shutdown-debug.js";
5
5
  import { delimitUntrusted } from "./untrusted-text.js";
6
+ import { boundedRedactedSummary } from "./untrusted-egress.js";
6
7
  import { mintCompletionId, commitCompletionIdIfEmpty, clipTaskOutput, assertOwnership, sleepPollStep, alreadyTerminalStopNote, canAccess, normalizeAgentName, closestName, DURABLE_AGENT_HEARTBEAT_MS, DURABLE_AGENT_HANDLE_RE, BG_AGENT_REAP_STOP_ERROR, } from "./task-registry-shared.js";
7
8
  import { buildToolResultRef, OFFLOAD_TOOL_NAME } from "./tool-result-store.js";
8
9
  export function ensureDurableHeartbeatLane(core) {
@@ -636,6 +637,7 @@ export async function consumeParkedFlipLane(core, id, stores, ticket) {
636
637
  handle.resolveParkedStop = undefined;
637
638
  handle.updatedAt = Date.now();
638
639
  handle.completionId = undefined;
640
+ handle.terminalNotified = undefined;
639
641
  if (flipped.seq !== undefined)
640
642
  handle.cycleSeq = flipped.seq;
641
643
  return true;
@@ -681,6 +683,7 @@ export function settleBackgroundAgentLane(core, id, outcome) {
681
683
  handle.status = outcome.status;
682
684
  mintCompletionId(handle);
683
685
  handle.notify = undefined;
686
+ handle.onReapTerminal = undefined;
684
687
  for (const [, , qResolve] of handle.preAttachQueue ?? [])
685
688
  qResolve?.({ ok: false, reason: "not_running" });
686
689
  handle.preAttachQueue = undefined;
@@ -699,6 +702,12 @@ export function settleBackgroundAgentLane(core, id, outcome) {
699
702
  }
700
703
  if (outcome.error !== undefined && !(outcome.status === "killed" && outcome.error === BG_AGENT_REAP_STOP_ERROR)) {
701
704
  handle.error = outcome.error;
705
+ if (outcome.errorCode !== undefined)
706
+ handle.errorCode = outcome.errorCode;
707
+ if (outcome.retryable !== undefined)
708
+ handle.errorRetryable = outcome.retryable;
709
+ if (outcome.errorKind !== undefined)
710
+ handle.errorKind = outcome.errorKind;
702
711
  }
703
712
  handle.updatedAt = Date.now();
704
713
  if (outcome.seq !== undefined)
@@ -837,9 +846,13 @@ export function reviveBackgroundAgentLane(core, id, access, abort) {
837
846
  handle.resultFull = undefined;
838
847
  handle.spillRef = undefined;
839
848
  handle.error = undefined;
849
+ handle.errorCode = undefined;
850
+ handle.errorRetryable = undefined;
851
+ handle.errorKind = undefined;
840
852
  handle.resultIsPartial = undefined;
841
853
  handle.stopSource = undefined;
842
854
  handle.completionId = undefined;
855
+ handle.terminalNotified = undefined;
843
856
  handle.reviveCycle = (handle.reviveCycle ?? 0) + 1;
844
857
  handle.cycleSeq = (handle.cycleSeq ?? 1) + 1;
845
858
  handle.updatedAt = Date.now();
@@ -1022,6 +1035,7 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
1022
1035
  ...(row.resultIsPartial ? { partial_result: true } : {}),
1023
1036
  ...(row.completionId !== undefined ? { completionId: row.completionId } : {}),
1024
1037
  },
1038
+ ...(row.status === "failed" ? { isError: true } : {}),
1025
1039
  };
1026
1040
  }
1027
1041
  export async function spillClippedAgentResult(handle, full, clipped, store, sessionId) {
@@ -1057,6 +1071,9 @@ The agent is durably suspended, waiting for an approval decision. It resumes whe
1057
1071
  }
1058
1072
  const fullResult = handle.result ? (handle.resultFull ?? handle.result) : undefined;
1059
1073
  const resultText = fullResult !== undefined ? await spillClippedAgentResult(handle, fullResult, clipTaskOutput(fullResult, handle.outputFile), store, sessionId) : undefined;
1074
+ const kindClause = handle.status === "failed" && handle.errorKind !== undefined && handle.errorRetryable !== undefined
1075
+ ? ` (error_kind: ${handle.errorKind}, retryable: ${handle.errorRetryable})`
1076
+ : "";
1060
1077
  const body = running
1061
1078
  ? oneShot === true
1062
1079
  ? `status: running
@@ -1064,7 +1081,7 @@ This is a ONE-SHOT submission — there is no later turn for a background notifi
1064
1081
  : `status: running
1065
1082
  The agent is still working — you will be notified when it completes.`
1066
1083
  : `status: ${handle.status}
1067
- ${handle.error ? `error: ${handle.error}
1084
+ ${handle.error ? `error: ${handle.error}${kindClause}
1068
1085
  ` : ""}${handle.result ? `--- result${handle.resultIsPartial ? " (partial — produced before the task was stopped)" : ""} ---
1069
1086
  ${resultText}` : "(no result text)"}`;
1070
1087
  return {
@@ -1076,9 +1093,15 @@ ${resultText}` : "(no result text)"}`;
1076
1093
  retrieval_status: retrieval,
1077
1094
  ...(handle.cycleSeq !== undefined ? { seq: handle.cycleSeq } : {}),
1078
1095
  ...(handle.status === "killed" && handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
1096
+ ...(handle.status === "failed" && handle.error !== undefined
1097
+ ? { error: delimitUntrusted("agent error", boundedRedactedSummary(handle.error, 300)) }
1098
+ : {}),
1099
+ ...(handle.status === "failed" && handle.errorCode !== undefined ? { errorCode: handle.errorCode } : {}),
1100
+ ...(handle.status === "failed" && handle.errorRetryable !== undefined ? { retryable: handle.errorRetryable } : {}),
1079
1101
  ...(handle.resultIsPartial ? { partial_result: true } : {}),
1080
1102
  ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
1081
1103
  },
1104
+ ...(handle.status === "failed" ? { isError: true } : {}),
1082
1105
  };
1083
1106
  }
1084
1107
  export async function stopBackgroundAgentLane(core, handle) {
@@ -1,5 +1,5 @@
1
1
  import { delimitUntrusted } from "./untrusted-text.js";
2
- import { assertOwnership, defaultMonitorTimers, MONITOR_MAX_TIMEOUT_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_BATCH_WINDOW_MS, MONITOR_MAX_BATCHES_PER_MINUTE, MONITOR_STORM_BURST, MONITOR_STORM_KILL_AFTER_MS, MONITOR_LINE_BUF_CAP, MONITOR_SPILL_CAP_BYTES, TASK_OUTPUT_MAX_CHARS, mintCompletionId, clipMonitorEvent, clipMonitorLine, terminalTaskSummary, accountDroppedBytes, rollSpoolText, statusFromBackground, droppedGapNote, firstDropNote, alreadyTerminalStopNote, clipTaskOutput, sleepPollStep, } from "./task-registry-shared.js";
2
+ import { assertOwnership, defaultMonitorTimers, MONITOR_MAX_TIMEOUT_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_BATCH_WINDOW_MS, MONITOR_MAX_BATCHES_PER_MINUTE, MONITOR_STORM_BURST, MONITOR_STORM_KILL_AFTER_MS, MONITOR_LINE_BUF_CAP, MONITOR_SPILL_CAP_CHARS, TASK_OUTPUT_MAX_CHARS, mintCompletionId, clipMonitorEvent, clipMonitorLine, terminalTaskSummary, accountDroppedBytes, rollSpoolText, statusFromBackground, droppedGapNote, firstDropNote, alreadyTerminalStopNote, clipTaskOutput, sleepPollStep, } from "./task-registry-shared.js";
3
3
  import { buildToolResultRef, OFFLOAD_TOOL_NAME } from "./tool-result-store.js";
4
4
  export function registerMonitorLane(core, input) {
5
5
  assertOwnership(input, "registerMonitor");
@@ -49,14 +49,14 @@ function spillRolledMonitorChunk(handle, stream, dropped) {
49
49
  const store = handle.toolResultStore;
50
50
  if (store === undefined)
51
51
  return;
52
- const used = handle.spillBytesUsed ?? 0;
53
- if (used >= MONITOR_SPILL_CAP_BYTES) {
52
+ const used = handle.spillCharsUsed ?? 0;
53
+ if (used >= MONITOR_SPILL_CAP_CHARS) {
54
54
  handle.spillCapped = true;
55
55
  return;
56
56
  }
57
57
  const n = stream === "out" ? (handle.spillSegCount ?? 0) : (handle.spillErrSegCount ?? 0);
58
58
  const ref = buildToolResultRef(handle.spillSessionId ?? "no-session", `${handle.id}_${stream}_seg${n}`);
59
- handle.spillBytesUsed = used + dropped.length;
59
+ handle.spillCharsUsed = used + dropped.length;
60
60
  if (stream === "out")
61
61
  handle.spillSegCount = n + 1;
62
62
  else
@@ -88,7 +88,7 @@ function monitorSpillNote(handle) {
88
88
  const coverage = handle.spillFailed === true
89
89
  ? " — a write failed partway through; the ref chain may be INCOMPLETE, read what is there"
90
90
  : handle.spillCapped === true
91
- ? ` — spill cap (${MONITOR_SPILL_CAP_BYTES} bytes) reached; earlier segments retained, later rolls were not spilled`
91
+ ? ` — spill cap (${MONITOR_SPILL_CAP_CHARS} chars) reached; earlier segments retained, later rolls were not spilled`
92
92
  : "";
93
93
  return `; spilled to ${clauses.join(", ")} (read back via ${OFFLOAD_TOOL_NAME})${coverage}`;
94
94
  }
@@ -359,7 +359,7 @@ function explicitStopTerminalNote(handle, alreadyGone) {
359
359
  task_type: "monitor",
360
360
  ...(handle.toolUseId !== undefined ? { toolUseId: handle.toolUseId } : {}),
361
361
  status: "killed",
362
- stoppedBy: handle.stoppedBy,
362
+ ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
363
363
  summary: `${terminalTaskSummary("monitor", label, "killed")} — ${detail}${droppedGapNote(handle.spool)}`,
364
364
  ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
365
365
  });
@@ -16,6 +16,8 @@ export interface UnifiedTaskOutput {
16
16
  retrieval_status: TaskRetrievalStatus;
17
17
  content?: string;
18
18
  error?: string;
19
+ errorCode?: string;
20
+ retryable?: boolean;
19
21
  stoppedBy?: StopSource;
20
22
  seq?: number;
21
23
  partial_result?: boolean;
@@ -135,9 +137,14 @@ export interface BackgroundAgentTaskHandle extends SemaTaskHandle {
135
137
  resultFull?: string;
136
138
  spillRef?: string;
137
139
  error?: string;
140
+ errorCode?: string;
141
+ errorRetryable?: boolean;
142
+ errorKind?: string;
138
143
  resultIsPartial?: boolean;
139
144
  stopSource?: StopSource;
140
145
  stoppedBy?: StopSource;
146
+ onReapTerminal?: () => void;
147
+ terminalNotified?: true;
141
148
  }
142
149
  export interface MonitorTimers {
143
150
  setInterval: (fn: () => unknown, ms: number) => unknown;
@@ -188,7 +195,7 @@ export interface MonitorTaskHandle extends SemaTaskHandle {
188
195
  spillSessionId?: string;
189
196
  spillSegCount?: number;
190
197
  spillErrSegCount?: number;
191
- spillBytesUsed?: number;
198
+ spillCharsUsed?: number;
192
199
  spillCapped?: true;
193
200
  spillFailed?: true;
194
201
  }
@@ -237,7 +244,7 @@ export declare function statusFromBackground(status: string, exitCode?: number):
237
244
  export declare function rollSpoolText(spool: {
238
245
  rolledChars: number;
239
246
  }, s: string, cap: number, onDrop?: (dropped: string) => void): string;
240
- export declare const MONITOR_SPILL_CAP_BYTES: number;
247
+ export declare const MONITOR_SPILL_CAP_CHARS: number;
241
248
  export declare function accountDroppedBytes(spool: {
242
249
  droppedBytes?: number;
243
250
  dropUnknown?: true;
@@ -94,7 +94,7 @@ export function rollSpoolText(spool, s, cap, onDrop) {
94
94
  onDrop?.(dropped);
95
95
  return s.slice(0, half) + s.slice(s.length - half);
96
96
  }
97
- export const MONITOR_SPILL_CAP_BYTES = 64 * 1024 * 1024;
97
+ export const MONITOR_SPILL_CAP_CHARS = 64 * 1024 * 1024;
98
98
  export function accountDroppedBytes(spool, poll) {
99
99
  const d = poll.bytesDroppedBeforeCursor ?? 0;
100
100
  if (d > 0)
@@ -133,6 +133,9 @@ export declare class TaskRegistry {
133
133
  result?: string;
134
134
  resultFull?: string;
135
135
  error?: string;
136
+ errorCode?: string;
137
+ retryable?: boolean;
138
+ errorKind?: string;
136
139
  stoppedBy?: StopSource;
137
140
  seq?: number;
138
141
  }): "completed" | "failed" | "killed" | undefined;
@@ -166,9 +169,14 @@ export declare class TaskRegistry {
166
169
  result?: string;
167
170
  resultFull?: string;
168
171
  error?: string;
172
+ errorCode?: string;
173
+ retryable?: boolean;
174
+ errorKind?: string;
169
175
  }): "completed" | "failed" | "killed" | undefined;
170
176
  unmarkRetainedContinuation(id: string): void;
171
177
  attachAgentNotify(id: string, notify: NonNullable<BackgroundAgentTaskHandle["notify"]>, cycle?: number): void;
178
+ attachAgentTerminalNotifier(id: string, fn: () => void): void;
179
+ claimAgentTerminalNotify(id: string): boolean;
172
180
  deliverToRunningAgent(id: string, access: TaskAccess, notification: import("./task-notification.js").TaskNotificationPayload, opts?: {
173
181
  priority?: import("./task-notification.js").SystemInjectionPriority;
174
182
  }): Promise<{
@@ -52,6 +52,23 @@ function bashMirrorGapNote(handle) {
52
52
  ? " [!] The output file is INCOMPLETE (one or more writes to it failed) — trust the result text / TaskOutput over the file."
53
53
  : "";
54
54
  }
55
+ function explicitStopBashTerminalNote(handle, alreadyGone) {
56
+ const resultText = handle.spool !== undefined ? bashTerminalResult(handle.spool) : "";
57
+ const detail = alreadyGone
58
+ ? "killed before completion (stopped via TaskStop; the process was already gone)"
59
+ : "killed before completion (stopped via TaskStop)";
60
+ handle.onTerminal?.({
61
+ task_id: handle.id,
62
+ task_type: "background_bash",
63
+ ...(handle.toolUseId !== undefined ? { toolUseId: handle.toolUseId } : {}),
64
+ ...(handle.outputFile !== undefined ? { output_file: handle.outputFile } : {}),
65
+ status: "killed",
66
+ ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
67
+ summary: `${terminalTaskSummary("bash", (handle.description ?? "background command").slice(0, 200), "killed")} — ${detail}${bashMirrorGapNote(handle)}${handle.spool !== undefined ? droppedGapNote(handle.spool) : ""}`,
68
+ ...(resultText.length > 0 ? { result: resultText, partial: true } : {}),
69
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
70
+ });
71
+ }
55
72
  function monitorInTimeoutWindow(handle) {
56
73
  return handle.status === "running" && handle.deadlineAt !== undefined && handle.timers.now() < handle.deadlineAt;
57
74
  }
@@ -145,6 +162,23 @@ export class TaskRegistry {
145
162
  attachAgentNotify(id, notify, cycle) {
146
163
  return attachAgentNotifyLane(this.core, id, notify, cycle);
147
164
  }
165
+ attachAgentTerminalNotifier(id, fn) {
166
+ const handle = this.handles.get(id);
167
+ if (!handle || handle.type !== "background_agent")
168
+ return;
169
+ if (handle.status !== "running" && handle.status !== "parked" && handle.status !== "pending")
170
+ return;
171
+ handle.onReapTerminal = fn;
172
+ }
173
+ claimAgentTerminalNotify(id) {
174
+ const handle = this.handles.get(id);
175
+ if (!handle || handle.type !== "background_agent")
176
+ return true;
177
+ if (handle.terminalNotified === true)
178
+ return false;
179
+ handle.terminalNotified = true;
180
+ return true;
181
+ }
148
182
  async deliverToRunningAgent(id, access, notification, opts) {
149
183
  return deliverToRunningAgentLane(this.core, id, access, notification, opts);
150
184
  }
@@ -334,6 +368,17 @@ export class TaskRegistry {
334
368
  async settleKilledForOwner(access, opts) {
335
369
  const source = opts?.source ?? "parent";
336
370
  const clause = (by) => by === "user" ? "stopped by user" : by === "parent" ? "its parent run ended" : `stopped by ${by}`;
371
+ if (source === "user") {
372
+ for (const handle of this.handles.values()) {
373
+ if (handle.type !== "background_agent" || handle.status !== "running")
374
+ continue;
375
+ if (!canAccess(handle, { owner: access.owner, scope: access.scope }))
376
+ continue;
377
+ if (opts?.skipSessionScoped && handle.sessionScoped)
378
+ continue;
379
+ this.markStopSource(handle.id, source);
380
+ }
381
+ }
337
382
  let settled = 0;
338
383
  for (const handle of this.handles.values()) {
339
384
  if (handle.type !== "background_bash" && handle.type !== "monitor")
@@ -471,8 +516,17 @@ export class TaskRegistry {
471
516
  if (!canAccess(handle, access))
472
517
  continue;
473
518
  this.markStopSource(handle.id, "system");
519
+ const reapTerminalNote = handle.onReapTerminal;
474
520
  handle.abort.abort();
475
521
  this.settleBackgroundAgent(handle.id, { status: "killed", error: "session released" });
522
+ if (reapTerminalNote !== undefined && handle.terminalNotified !== true) {
523
+ handle.terminalNotified = true;
524
+ try {
525
+ reapTerminalNote();
526
+ }
527
+ catch {
528
+ }
529
+ }
476
530
  reaped++;
477
531
  }
478
532
  this.pokeBgQuiescence(sessionId);
@@ -1122,7 +1176,7 @@ The durable record is still marked running (last update ${new Date(row.updatedAt
1122
1176
  handle.status = "killed";
1123
1177
  handle.updatedAt = Date.now();
1124
1178
  mintCompletionId(handle);
1125
- this.pokeBgQuiescence(handle.owner);
1179
+ this.notifyTerminalOnce(handle, () => explicitStopBashTerminalNote(handle, true));
1126
1180
  return {
1127
1181
  content: `Terminated ${handle.id} (the process was already gone).`,
1128
1182
  details: {
@@ -1151,7 +1205,7 @@ The durable record is still marked running (last update ${new Date(row.updatedAt
1151
1205
  handle.status = "killed";
1152
1206
  handle.updatedAt = Date.now();
1153
1207
  mintCompletionId(handle);
1154
- this.pokeBgQuiescence(handle.owner);
1208
+ this.notifyTerminalOnce(handle, () => explicitStopBashTerminalNote(handle, false));
1155
1209
  return {
1156
1210
  content: `Terminated ${handle.id}.`,
1157
1211
  details: {
@@ -1193,9 +1247,10 @@ export function createTaskOutputTool(opts) {
1193
1247
  let effectiveTimeoutMs = requestedMs;
1194
1248
  let waitClampNote = "";
1195
1249
  const wall = opts.deadlineMs?.();
1250
+ const requestedWaitMs = Math.max(1, Math.floor(requestedMs ?? BLOCK_DEFAULT_TIMEOUT_MS));
1196
1251
  if (args.block !== false && wall !== undefined) {
1197
1252
  const remainMs = wall - Date.now() - BLOCK_WAIT_WRITEOUT_RESERVE_MS;
1198
- const wantMs = Math.min(BLOCK_MAX_TIMEOUT_MS, Math.max(1, Math.floor(requestedMs ?? BLOCK_DEFAULT_TIMEOUT_MS)));
1253
+ const wantMs = Math.min(BLOCK_MAX_TIMEOUT_MS, requestedWaitMs);
1199
1254
  if (remainMs <= 0) {
1200
1255
  effectiveTimeoutMs = 1;
1201
1256
  waitClampNote =
@@ -1203,7 +1258,7 @@ export function createTaskOutputTool(opts) {
1203
1258
  }
1204
1259
  else if (remainMs < wantMs) {
1205
1260
  effectiveTimeoutMs = remainMs;
1206
- waitClampNote = `\n\nNOTE: wait clamped to ${Math.round(remainMs / 1000)}s (requested ${Math.round(wantMs / 1000)}s) — the task's wall-clock deadline is near. If the task is still running after this wait, do NOT re-wait: proceed with other work or write out your results now.`;
1261
+ waitClampNote = `\n\nNOTE: wait clamped to ${Math.round(remainMs / 1000)}s (requested ${Math.round(requestedWaitMs / 1000)}s) — the task's wall-clock deadline is near. If the task is still running after this wait, do NOT re-wait: proceed with other work or write out your results now.`;
1207
1262
  }
1208
1263
  }
1209
1264
  const r = await opts.registry.pollTask(id, { owner: ctx.taskId ?? opts.owner, scope: ctx.principal ?? opts.scope, ...((ctx.sessionId ?? opts.sessionId) !== undefined ? { sessionId: ctx.sessionId ?? opts.sessionId } : {}) }, {
@@ -45,6 +45,7 @@ export declare class ScopedToolResultStore implements ToolResultStore {
45
45
  }
46
46
  export declare function isVolatileOffloadStore(store: ToolResultStore): boolean;
47
47
  export declare const OFFLOAD_TOOL_NAME = "ReadToolResult";
48
+ export declare function offloadPagebackHint(ref: string, form: "preview" | "cleared", reachableTools?: ReadonlySet<string>): string;
48
49
  export declare const PERSISTED_OUTPUT_PREFIX = "<persisted-output ref=";
49
50
  export declare const DEFAULT_TOOL_RESULT_THRESHOLD_CHARS = 20000;
50
51
  export declare function firstPartyOffloadPolicy(toolName: string): {
@@ -54,6 +55,6 @@ export declare function firstPartyOffloadPolicy(toolName: string): {
54
55
  export declare function buildPreview(full: string, ref: string, sizes?: {
55
56
  head: number;
56
57
  tail: number;
57
- }): string;
58
- export declare function withToolResultOffload(tool: AgentTool, store: ToolResultStore, thresholdChars: number, sessionId: string): AgentTool;
58
+ }, reachableTools?: ReadonlySet<string>): string;
59
+ export declare function withToolResultOffload(tool: AgentTool, store: ToolResultStore, thresholdChars: number, sessionId: string, reachableTools?: () => ReadonlySet<string> | undefined): AgentTool;
59
60
  export declare function createReadToolResultTool(store: ToolResultStore): AgentTool;
@@ -1,6 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { Type } from "typebox";
3
3
  import { defineTool, errorResult } from "./tools.js";
4
+ import { TOOL_SEARCH_NAME } from "./runner/tool-disclosure.js";
4
5
  export function assertSafeToolResultRef(ref) {
5
6
  const bad = ref === "" ||
6
7
  ref === "." ||
@@ -92,6 +93,13 @@ export function isVolatileOffloadStore(store) {
92
93
  return store instanceof InMemoryToolResultStore;
93
94
  }
94
95
  export const OFFLOAD_TOOL_NAME = "ReadToolResult";
96
+ export function offloadPagebackHint(ref, form, reachableTools) {
97
+ const activate = reachableTools !== undefined && !reachableTools.has(OFFLOAD_TOOL_NAME)
98
+ ? `${OFFLOAD_TOOL_NAME} is not active yet — call ${TOOL_SEARCH_NAME} with {"query":"select:${OFFLOAD_TOOL_NAME}"} to activate it, then `
99
+ : "";
100
+ const core = `${activate === "" && form === "preview" ? "Call" : `${activate}call`} ${OFFLOAD_TOOL_NAME} with ref`;
101
+ return form === "preview" ? `${core}="${ref}" (offset, limit) to read more.` : `full text persisted; ${core} "${ref}" to read it back`;
102
+ }
95
103
  export const PERSISTED_OUTPUT_PREFIX = "<persisted-output ref=";
96
104
  export const DEFAULT_TOOL_RESULT_THRESHOLD_CHARS = 20_000;
97
105
  export function firstPartyOffloadPolicy(toolName) {
@@ -122,7 +130,7 @@ function totalTextChars(content) {
122
130
  n += b.text.length;
123
131
  return n;
124
132
  }
125
- export function buildPreview(full, ref, sizes) {
133
+ export function buildPreview(full, ref, sizes, reachableTools) {
126
134
  const total = full.length;
127
135
  const headChars = sizes?.head ?? PREVIEW_HEAD_CHARS;
128
136
  const tailChars = sizes?.tail ?? PREVIEW_TAIL_CHARS;
@@ -131,11 +139,11 @@ export function buildPreview(full, ref, sizes) {
131
139
  const tail = full.slice(tailStart);
132
140
  return (`${PERSISTED_OUTPUT_PREFIX}"${ref}" chars="${total}">\n` +
133
141
  `${head}\n` +
134
- `…[truncated — ${total} chars total. Call ${OFFLOAD_TOOL_NAME} with ref="${ref}" (offset, limit) to read more.]\n` +
142
+ `…[truncated — ${total} chars total. ${offloadPagebackHint(ref, "preview", reachableTools)}]\n` +
135
143
  `${tail}\n` +
136
144
  `</persisted-output>`);
137
145
  }
138
- export function withToolResultOffload(tool, store, thresholdChars, sessionId) {
146
+ export function withToolResultOffload(tool, store, thresholdChars, sessionId, reachableTools) {
139
147
  const wrappedExecute = async (toolCallId, params, signal, onUpdate) => {
140
148
  const res = await tool.execute(toolCallId, params, signal, onUpdate);
141
149
  if (totalTextChars(res.content) <= thresholdChars)
@@ -149,7 +157,7 @@ export function withToolResultOffload(tool, store, thresholdChars, sessionId) {
149
157
  const ref = buildToolResultRef(sessionId, toolCallId);
150
158
  await store.put(ref, full);
151
159
  const images = res.content.filter((b) => b.type !== "text");
152
- return { ...res, content: [{ type: "text", text: buildPreview(full, ref) }, ...images] };
160
+ return { ...res, content: [{ type: "text", text: buildPreview(full, ref, undefined, reachableTools?.()) }, ...images] };
153
161
  };
154
162
  return { ...tool, execute: wrappedExecute };
155
163
  }
@@ -104,6 +104,13 @@ export type TraceEvent = {
104
104
  }>;
105
105
  overrideReasons?: Record<string, string>;
106
106
  ts: number;
107
+ } | {
108
+ kind: "config.additional_directory_skipped";
109
+ version: 1;
110
+ taskId: string;
111
+ entry: string;
112
+ reason: string;
113
+ ts: number;
107
114
  } | {
108
115
  kind: "task.end";
109
116
  version: 1;
@@ -47,6 +47,7 @@ export interface ToolSpec<TParams extends TSchema = TSchema> {
47
47
  offload?: boolean;
48
48
  offloadThresholdChars?: number;
49
49
  defer?: boolean;
50
+ alwaysLoad?: boolean;
50
51
  contract?: {
51
52
  contractId: string;
52
53
  implementationRevision: string;
@@ -256,6 +257,7 @@ export interface TaskSpec {
256
257
  tools?: ToolSpec[];
257
258
  excludeTools?: string[];
258
259
  deferTools?: string[];
260
+ alwaysLoadTools?: string[];
259
261
  promptProfile?: "simple" | "classic";
260
262
  agents?: AgentDefinition[];
261
263
  toolPolicy?: import("./tool-policy.js").ToolPolicy;
@@ -514,9 +516,9 @@ export type TaskEvent = ({
514
516
  };
515
517
  usageMissing?: true;
516
518
  stopReason?: string;
517
- } & TaskEventIdentity) | {
519
+ } & TaskEventIdentity) | ({
518
520
  type: "compacted";
519
- trigger: "auto" | "manual";
521
+ trigger: "auto" | "manual" | "forced";
520
522
  tokensBefore: number;
521
523
  tokensAfter?: number;
522
524
  triggerTokensBefore?: number;
@@ -534,7 +536,12 @@ export type TaskEvent = ({
534
536
  clampedRatio?: number;
535
537
  clampReason?: "budget" | "walltime" | "tolerance";
536
538
  phaseDurations?: import("./auto-compaction.js").CompactionPhaseDurations;
537
- } | ({
539
+ } & TaskEventIdentity) | ({
540
+ type: "compaction_outcome";
541
+ outcome: Exclude<CompactOutcome, "compacted"> | "suppressed";
542
+ trigger: "auto" | "manual" | "forced";
543
+ reason?: string;
544
+ } & TaskEventIdentity) | ({
538
545
  type: "steering_injected";
539
546
  source: "deadline_nudge" | "finalize" | "todo_reminder" | "task_reminder" | "tool_search_usage_reminder" | "changed_files" | "plan_mode" | "date_change" | "instructions_change" | "budget_usd" | "background_tasks" | "tools_delta" | "agent_listing" | "skills_listing" | "mcp_instructions" | "mcp_dropped_tools";
540
547
  preview: string;
@@ -8,6 +8,9 @@ export interface CompactionDetails {
8
8
  modifiedFiles: string[];
9
9
  modifiedFilesByRecency?: string[];
10
10
  invokedSkills?: InvokedSkillRetention[];
11
+ elidedMessages?: number;
12
+ persistedOutputRefs?: string[];
13
+ activeTools?: string[];
11
14
  }
12
15
  export interface CompactionResult<T = unknown> {
13
16
  summary: string;
@@ -68,6 +71,8 @@ export interface CompactionPreparation {
68
71
  previousSummary?: string;
69
72
  fileOps: FileOperations;
70
73
  invokedSkills: InvokedSkillRetention[];
74
+ persistedOutputRefs?: string[];
75
+ elidedMessages?: number;
71
76
  settings: CompactionSettings;
72
77
  }
73
78
  export declare function prepareCompaction(pathEntries: SessionTreeEntry[], settings: CompactionSettings, charsPerToken?: number, windowTokens?: number): Result<CompactionPreparation | undefined, CompactionError>;
@@ -2,7 +2,7 @@ import { resolveAgentCoreCompleteFn, } from "../loop/runtime-deps.js";
2
2
  import { asAgentMessage, convertToLlm, createCompactionSummaryMessage, createCustomMessage, } from "../harness/messages.js";
3
3
  import { buildSessionContext } from "../session/session.js";
4
4
  import { CompactionError, err, ok, } from "../harness/types.js";
5
- import { budgetInvokedSkillsRetention, computeFileLists, createFileOps, extractFileOpsFromMessage, extractInvokedSkills, formatFileOperations, readRetainedInvokedSkills, replaceInvokedSkillBodiesForSummary, serializeConversation, stripFileOperationsFooter, } from "./utils.js";
5
+ import { budgetInvokedSkillsRetention, computeFileLists, createFileOps, extractFileOpsFromMessage, extractInvokedSkills, extractPersistedOutputRefs, formatFileOperations, formatPersistedOutputRefs, PERSISTED_OUTPUT_REFS_MAX_ENTRIES, readElidedMessages, readPersistedOutputRefs, readRetainedInvokedSkills, replaceInvokedSkillBodiesForSummary, serializeConversation, stripFileOperationsFooter, } from "./utils.js";
6
6
  function safeJsonStringify(value) {
7
7
  try {
8
8
  return JSON.stringify(value) ?? "undefined";
@@ -256,6 +256,51 @@ export function findTurnStartIndex(entries, entryIndex, startIndex) {
256
256
  }
257
257
  return -1;
258
258
  }
259
+ function enforceToolPairContainment(entries, startIndex, endIndex, cutIndex) {
260
+ const callSites = new Map();
261
+ for (let i = startIndex; i < endIndex; i++) {
262
+ const entry = entries[i];
263
+ if (entry.type !== "message")
264
+ continue;
265
+ const msg = entry.message;
266
+ if (msg.role !== "assistant" || !Array.isArray(msg.content))
267
+ continue;
268
+ for (const block of msg.content) {
269
+ if (block.type === "toolCall" && typeof block.id === "string") {
270
+ const at = callSites.get(block.id);
271
+ if (at === undefined)
272
+ callSites.set(block.id, [i]);
273
+ else
274
+ at.push(i);
275
+ }
276
+ }
277
+ }
278
+ if (callSites.size === 0)
279
+ return cutIndex;
280
+ for (let i = cutIndex; i < endIndex; i++) {
281
+ const entry = entries[i];
282
+ if (entry.type !== "message")
283
+ continue;
284
+ const msg = entry.message;
285
+ if (msg.role !== "toolResult")
286
+ continue;
287
+ const sites = callSites.get(msg.toolCallId);
288
+ if (sites === undefined)
289
+ continue;
290
+ let site = -1;
291
+ for (const s of sites) {
292
+ if (s < i)
293
+ site = s;
294
+ else
295
+ break;
296
+ }
297
+ if (site === -1 || site >= cutIndex)
298
+ continue;
299
+ cutIndex = site;
300
+ i = site;
301
+ }
302
+ return cutIndex;
303
+ }
259
304
  export function findCutPoint(entries, startIndex, endIndex, keepRecentTokens, charsPerToken = DEFAULT_CHARS_PER_TOKEN) {
260
305
  const cutPoints = findValidCutPoints(entries, startIndex, endIndex);
261
306
  if (cutPoints.length === 0) {
@@ -290,6 +335,7 @@ export function findCutPoint(entries, startIndex, endIndex, keepRecentTokens, ch
290
335
  break;
291
336
  }
292
337
  }
338
+ cutIndex = enforceToolPairContainment(entries, startIndex, endIndex, cutIndex);
293
339
  const cutEntry = entries[cutIndex];
294
340
  const isUserMessage = cutEntry.type === "message" && cutEntry.message.role === "user";
295
341
  const turnStartIndex = isUserMessage ? -1 : findTurnStartIndex(entries, cutIndex, startIndex);
@@ -721,6 +767,21 @@ export function prepareCompaction(pathEntries, settings, charsPerToken = DEFAULT
721
767
  ? Math.max(0, windowTokens - settings.reserveTokens - settings.keepRecentTokens) * charsPerToken
722
768
  : undefined;
723
769
  const invokedSkills = budgetInvokedSkillsRetention(extractInvokedSkills([...messagesToSummarize, ...turnPrefixMessages], prevRetainedSkills), retentionBudgetChars);
770
+ const refSet = new Set();
771
+ if (prevCompactionIndex >= 0 && !pathEntries[prevCompactionIndex].fromHook) {
772
+ for (const r of readPersistedOutputRefs(pathEntries[prevCompactionIndex].details)) {
773
+ refSet.add(r);
774
+ }
775
+ }
776
+ extractPersistedOutputRefs(messagesToSummarize, refSet);
777
+ if (cutPoint.isSplitTurn) {
778
+ extractPersistedOutputRefs(turnPrefixMessages, refSet);
779
+ }
780
+ const persistedOutputRefs = [...refSet].slice(-PERSISTED_OUTPUT_REFS_MAX_ENTRIES);
781
+ const prevElided = prevCompactionIndex >= 0 && !pathEntries[prevCompactionIndex].fromHook
782
+ ? readElidedMessages(pathEntries[prevCompactionIndex].details)
783
+ : undefined;
784
+ const elidedMessages = (prevElided ?? 0) + messagesToSummarize.length + turnPrefixMessages.length;
724
785
  const [summarizeReady, prefixReady] = replaceInvokedSkillBodiesForSummary([messagesToSummarize, turnPrefixMessages], new Set(invokedSkills.map((s) => s.name)));
725
786
  return ok({
726
787
  firstKeptEntryId,
@@ -731,6 +792,8 @@ export function prepareCompaction(pathEntries, settings, charsPerToken = DEFAULT
731
792
  previousSummary,
732
793
  fileOps,
733
794
  invokedSkills,
795
+ persistedOutputRefs,
796
+ elidedMessages,
734
797
  settings,
735
798
  });
736
799
  }
@@ -750,7 +813,7 @@ Summarize the prefix to provide context for the retained suffix:
750
813
  Be concise. Focus on what's needed to understand the kept suffix.`;
751
814
  export { computeFileLists, serializeConversation } from "./utils.js";
752
815
  export async function compact(preparation, model, apiKey, headers, customInstructions, signal, thinkingLevel, streamFn, runtime, charsPerToken, onInputTruncated, onPtlRetry) {
753
- const { firstKeptEntryId, messagesToSummarize, turnPrefixMessages, isSplitTurn, tokensBefore, previousSummary, fileOps, invokedSkills, settings, } = preparation;
816
+ const { firstKeptEntryId, messagesToSummarize, turnPrefixMessages, isSplitTurn, tokensBefore, previousSummary, fileOps, invokedSkills, persistedOutputRefs, elidedMessages, settings, } = preparation;
754
817
  if (!firstKeptEntryId) {
755
818
  return err(new CompactionError("invalid_session", "First kept entry has no UUID - session may need migration"));
756
819
  }
@@ -784,6 +847,7 @@ export async function compact(preparation, model, apiKey, headers, customInstruc
784
847
  }
785
848
  const { readFiles, modifiedFiles, modifiedFilesByRecency } = computeFileLists(fileOps);
786
849
  summary += formatFileOperations(readFiles, modifiedFiles);
850
+ summary += formatPersistedOutputRefs(persistedOutputRefs ?? []);
787
851
  return ok({
788
852
  summary,
789
853
  firstKeptEntryId,
@@ -793,6 +857,8 @@ export async function compact(preparation, model, apiKey, headers, customInstruc
793
857
  modifiedFiles,
794
858
  modifiedFilesByRecency,
795
859
  ...(invokedSkills !== undefined && invokedSkills.length > 0 ? { invokedSkills } : {}),
860
+ ...(persistedOutputRefs !== undefined && persistedOutputRefs.length > 0 ? { persistedOutputRefs } : {}),
861
+ ...(elidedMessages !== undefined && elidedMessages > 0 ? { elidedMessages } : {}),
796
862
  },
797
863
  });
798
864
  }
@@ -21,6 +21,12 @@ export declare function budgetInvokedSkillsRetention(newestFirst: InvokedSkillRe
21
21
  export declare function replaceInvokedSkillBodiesForSummary(groups: AgentMessage[][], retainedNames: ReadonlySet<string>): AgentMessage[][];
22
22
  export declare function readRetainedInvokedSkills(details: unknown): InvokedSkillRetention[];
23
23
  export declare function renderInvokedSkillsRetention(skills: InvokedSkillRetention[]): string;
24
+ export declare const PERSISTED_OUTPUT_REFS_MAX_ENTRIES = 50;
25
+ export declare function extractPersistedOutputRefs(messages: AgentMessage[], into: Set<string>): void;
26
+ export declare function readElidedMessages(details: unknown): number | undefined;
27
+ export declare function readCompactionActiveTools(details: unknown): string[];
28
+ export declare function readPersistedOutputRefs(details: unknown): string[];
29
+ export declare function formatPersistedOutputRefs(refs: string[]): string;
24
30
  export declare function computeFileLists(fileOps: FileOperations): {
25
31
  readFiles: string[];
26
32
  modifiedFiles: string[];