@sema-agent/core 2.2.0 → 2.4.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 (70) 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 +275 -127
  4. package/dist/agents/teacher.js +51 -23
  5. package/dist/brain/errors.d.ts +1 -0
  6. package/dist/brain/errors.js +14 -0
  7. package/dist/brain/stream-engine.js +3 -3
  8. package/dist/core/context-edit.js +2 -1
  9. package/dist/core/runner/assemble-result.d.ts +1 -0
  10. package/dist/core/runner/assemble-result.js +12 -8
  11. package/dist/core/runner/prepare-task.js +32 -7
  12. package/dist/core/runner/runtask.js +18 -5
  13. package/dist/core/runner/tool-output-projection.js +2 -1
  14. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +37 -0
  15. package/dist/core/store-contracts/checkpoint-store-contract.js +195 -0
  16. package/dist/core/store-contracts/contract-harness.d.ts +6 -0
  17. package/dist/core/store-contracts/contract-harness.js +16 -0
  18. package/dist/core/store-contracts/contract-kit-version.d.ts +1 -0
  19. package/dist/core/store-contracts/contract-kit-version.js +2 -0
  20. package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +3 -0
  21. package/dist/core/store-contracts/file-snapshot-store-contract.js +126 -0
  22. package/dist/core/store-contracts/mailbox-store-contract.d.ts +6 -0
  23. package/dist/core/store-contracts/mailbox-store-contract.js +193 -0
  24. package/dist/core/store-contracts/session-repo-contract.d.ts +3 -0
  25. package/dist/core/store-contracts/session-repo-contract.js +36 -0
  26. package/dist/core/store-contracts/tool-result-store-contract.d.ts +3 -0
  27. package/dist/core/store-contracts/tool-result-store-contract.js +35 -0
  28. package/dist/core/task-notification.d.ts +2 -0
  29. package/dist/core/task-registry-agent.d.ts +7 -1
  30. package/dist/core/task-registry-agent.js +31 -2
  31. package/dist/core/task-registry-monitor.js +71 -6
  32. package/dist/core/task-registry-shared.d.ts +18 -1
  33. package/dist/core/task-registry-shared.js +5 -2
  34. package/dist/core/task-registry.d.ts +9 -0
  35. package/dist/core/task-registry.js +52 -7
  36. package/dist/core/tool-result-store.d.ts +3 -2
  37. package/dist/core/tool-result-store.js +12 -4
  38. package/dist/core/tools.d.ts +2 -0
  39. package/dist/core/tools.js +9 -0
  40. package/dist/core/trace.d.ts +7 -0
  41. package/dist/core/workflow-journal-store.d.ts +16 -0
  42. package/dist/core/workflow-journal-store.js +28 -0
  43. package/dist/engine/lsp/node-lsp-manager.d.ts +2 -0
  44. package/dist/engine/lsp/node-lsp-manager.js +16 -0
  45. package/dist/index.d.ts +1 -0
  46. package/dist/index.js +1 -0
  47. package/dist/orchestration/builtin-workflows.d.ts +1 -1
  48. package/dist/orchestration/builtin-workflows.js +11 -2
  49. package/dist/orchestration/workflow-governance.d.ts +6 -1
  50. package/dist/orchestration/workflow-governance.js +24 -4
  51. package/dist/orchestration/workflow-primitives.js +7 -1
  52. package/dist/orchestration/workflow.d.ts +16 -0
  53. package/dist/orchestration/workflow.js +94 -21
  54. package/dist/tools/fs/fs-bash.d.ts +7 -1
  55. package/dist/tools/fs/fs-bash.js +51 -20
  56. package/dist/tools/fs/fs-read.js +22 -11
  57. package/dist/tools/fs/fs-search-tools.js +3 -3
  58. package/dist/tools/fs/fs-shared.d.ts +20 -7
  59. package/dist/tools/fs/fs-shared.js +17 -3
  60. package/dist/tools/fs/fs-write.js +4 -4
  61. package/dist/tools/fs/index.d.ts +2 -0
  62. package/dist/tools/fs/index.js +7 -1
  63. package/dist/tools/fs/repo-map.js +2 -2
  64. package/dist/tools/fs/safety.d.ts +10 -0
  65. package/dist/tools/fs/safety.js +15 -1
  66. package/dist/tools/monitor.d.ts +2 -0
  67. package/dist/tools/monitor.js +20 -4
  68. package/dist/tools/web.js +6 -2
  69. package/dist/tools/worktree.js +46 -25
  70. package/package.json +1 -1
@@ -1,5 +1,6 @@
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, 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
+ import { buildToolResultRef, OFFLOAD_TOOL_NAME } from "./tool-result-store.js";
3
4
  export function registerMonitorLane(core, input) {
4
5
  assertOwnership(input, "registerMonitor");
5
6
  const id = core.mintTaskId("monitor");
@@ -37,17 +38,66 @@ export function registerMonitorLane(core, input) {
37
38
  ...(timeoutMs !== undefined ? { timeoutMs, deadlineAt: timers.now() + timeoutMs } : {}),
38
39
  timers,
39
40
  ...(input.onEvent !== undefined ? { onEvent: input.onEvent } : {}),
41
+ ...(input.toolResultStore !== undefined ? { toolResultStore: input.toolResultStore } : {}),
42
+ ...(input.sessionId !== undefined ? { spillSessionId: input.sessionId } : {}),
40
43
  };
41
44
  core.handles.set(id, handle);
42
45
  startMonitorWatcherLane(core, id);
43
46
  return id;
44
47
  }
48
+ function spillRolledMonitorChunk(handle, stream, dropped) {
49
+ const store = handle.toolResultStore;
50
+ if (store === undefined)
51
+ return;
52
+ const used = handle.spillCharsUsed ?? 0;
53
+ if (used >= MONITOR_SPILL_CAP_CHARS) {
54
+ handle.spillCapped = true;
55
+ return;
56
+ }
57
+ const n = stream === "out" ? (handle.spillSegCount ?? 0) : (handle.spillErrSegCount ?? 0);
58
+ const ref = buildToolResultRef(handle.spillSessionId ?? "no-session", `${handle.id}_${stream}_seg${n}`);
59
+ handle.spillCharsUsed = used + dropped.length;
60
+ if (stream === "out")
61
+ handle.spillSegCount = n + 1;
62
+ else
63
+ handle.spillErrSegCount = n + 1;
64
+ try {
65
+ void Promise.resolve(store.put(ref, dropped)).catch(() => {
66
+ handle.spillFailed = true;
67
+ });
68
+ }
69
+ catch {
70
+ handle.spillFailed = true;
71
+ }
72
+ }
73
+ function monitorSpillNote(handle) {
74
+ const outN = handle.spillSegCount ?? 0;
75
+ const errN = handle.spillErrSegCount ?? 0;
76
+ if (outN === 0 && errN === 0)
77
+ return "";
78
+ const sid = handle.spillSessionId ?? "no-session";
79
+ const segLabel = (stream, n) => {
80
+ const first = buildToolResultRef(sid, `${handle.id}_${stream}_seg0`);
81
+ return n <= 1 ? `ref "${first}"` : `refs "${first}" .. "${buildToolResultRef(sid, `${handle.id}_${stream}_seg${n - 1}`)}"`;
82
+ };
83
+ const clauses = [];
84
+ if (outN > 0)
85
+ clauses.push(`stdout ${segLabel("out", outN)}`);
86
+ if (errN > 0)
87
+ clauses.push(`stderr ${segLabel("err", errN)}`);
88
+ const coverage = handle.spillFailed === true
89
+ ? " — a write failed partway through; the ref chain may be INCOMPLETE, read what is there"
90
+ : handle.spillCapped === true
91
+ ? ` — spill cap (${MONITOR_SPILL_CAP_CHARS} chars) reached; earlier segments retained, later rolls were not spilled`
92
+ : "";
93
+ return `; spilled to ${clauses.join(", ")} (read back via ${OFFLOAD_TOOL_NAME})${coverage}`;
94
+ }
45
95
  export function absorbMonitorPollLane(handle, v) {
46
96
  const spool = handle.spool;
47
97
  accountDroppedBytes(spool, v);
48
98
  const ROLL_CAP = 2 * TASK_OUTPUT_MAX_CHARS;
49
- spool.stdout = rollSpoolText(spool, spool.stdout + v.stdout, ROLL_CAP);
50
- spool.stderr = rollSpoolText(spool, spool.stderr + v.stderr, ROLL_CAP);
99
+ spool.stdout = rollSpoolText(spool, spool.stdout + v.stdout, ROLL_CAP, (dropped) => spillRolledMonitorChunk(handle, "out", dropped));
100
+ spool.stderr = rollSpoolText(spool, spool.stderr + v.stderr, ROLL_CAP, (dropped) => spillRolledMonitorChunk(handle, "err", dropped));
51
101
  handle.lineBuf += v.stdout;
52
102
  if (handle.lineBuf.length > MONITOR_LINE_BUF_CAP)
53
103
  handle.lineBuf = handle.lineBuf.slice(-MONITOR_LINE_BUF_CAP);
@@ -264,7 +314,7 @@ export async function pollMonitorLane(handle, filter, deadline, signal) {
264
314
  const retrieval = running && deadline !== undefined && Date.now() >= deadline ? "timeout" : "success";
265
315
  const timedOutClause = spool.timedOut === true ? ` (timed out after ${spool.timeoutSec !== undefined ? `${spool.timeoutSec}s` : "its background time budget"})` : "";
266
316
  const displayStatus = (spool.exitCode !== undefined ? `exited(code ${spool.exitCode})` : running ? "running" : handle.status) + timedOutClause;
267
- const rolledNote = spool.rolledChars > 0 ? `[!] ${spool.rolledChars} earlier char(s) dropped from the spool (memory bound)\n` : "";
317
+ const rolledNote = spool.rolledChars > 0 ? `[!] ${spool.rolledChars} earlier char(s) dropped from the spool (memory bound)${monitorSpillNote(handle)}\n` : "";
268
318
  const droppedNote = (spool.droppedBytes ?? 0) > 0
269
319
  ? `[!] ${spool.droppedBytes} earlier byte(s) permanently dropped before this window (8MB buffer evicted)\n`
270
320
  : spool.dropUnknown === true
@@ -299,6 +349,21 @@ function consumePendingSuppressionNote(handle) {
299
349
  handle.suppressedBatches = 0;
300
350
  return ` [${suppressed} event batch(es) suppressed before the watch ended — output rate was too high; the suppressed output is still readable via TaskOutput.]`;
301
351
  }
352
+ function explicitStopTerminalNote(handle, alreadyGone) {
353
+ const label = (handle.description ?? "monitor").slice(0, 200);
354
+ const detail = alreadyGone
355
+ ? "killed before completion (stopped via TaskStop; the process was already gone); watch ended."
356
+ : "killed before completion (stopped via TaskStop); watch ended.";
357
+ emitMonitorEventLane(handle, {
358
+ task_id: handle.id,
359
+ task_type: "monitor",
360
+ ...(handle.toolUseId !== undefined ? { toolUseId: handle.toolUseId } : {}),
361
+ status: "killed",
362
+ ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
363
+ summary: `${terminalTaskSummary("monitor", label, "killed")} — ${detail}${droppedGapNote(handle.spool)}`,
364
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
365
+ });
366
+ }
302
367
  export async function stopMonitorLane(core, handle) {
303
368
  const wasRunning = handle.status === "running";
304
369
  if (handle.watcher !== undefined) {
@@ -358,7 +423,7 @@ export async function stopMonitorLane(core, handle) {
358
423
  handle.status = "killed";
359
424
  handle.updatedAt = Date.now();
360
425
  mintCompletionId(handle);
361
- core.pokeBgQuiescence(handle.owner);
426
+ core.notifyTerminalOnce(handle, () => explicitStopTerminalNote(handle, true));
362
427
  return {
363
428
  content: `Terminated ${handle.id} (the process was already gone).${drainNote}${consumePendingSuppressionNote(handle)}`,
364
429
  details: {
@@ -386,7 +451,7 @@ export async function stopMonitorLane(core, handle) {
386
451
  handle.status = "killed";
387
452
  handle.updatedAt = Date.now();
388
453
  mintCompletionId(handle);
389
- core.pokeBgQuiescence(handle.owner);
454
+ core.notifyTerminalOnce(handle, () => explicitStopTerminalNote(handle, false));
390
455
  }
391
456
  return {
392
457
  content: `Terminated ${handle.id}.${drainNote}${consumePendingSuppressionNote(handle)}`,
@@ -4,6 +4,7 @@ import { type WorkflowRunStore } from "./workflow-run-store.js";
4
4
  import type { BackgroundAgentRecord, BackgroundAgentStore } from "./background-agent-store.js";
5
5
  import type { WorkflowHandle, WorkflowRun } from "../orchestration/workflow.js";
6
6
  import type { TaskNotificationPayload } from "./task-notification.js";
7
+ import type { ToolResultStore } from "./tool-result-store.js";
7
8
  export type SemaTaskType = "background_bash" | "workflow" | "background_agent" | "monitor";
8
9
  export type StopSource = "user" | "parent" | "system" | (string & {});
9
10
  export type SemaTaskStatus = "pending" | "running" | "parked" | "completed" | "failed" | "killed" | "cancelled";
@@ -15,6 +16,8 @@ export interface UnifiedTaskOutput {
15
16
  retrieval_status: TaskRetrievalStatus;
16
17
  content?: string;
17
18
  error?: string;
19
+ errorCode?: string;
20
+ retryable?: boolean;
18
21
  stoppedBy?: StopSource;
19
22
  seq?: number;
20
23
  partial_result?: boolean;
@@ -132,10 +135,15 @@ export interface BackgroundAgentTaskHandle extends SemaTaskHandle {
132
135
  sessionScoped?: true;
133
136
  result?: string;
134
137
  resultFull?: string;
138
+ spillRef?: string;
135
139
  error?: string;
140
+ errorCode?: string;
141
+ errorRetryable?: boolean;
136
142
  resultIsPartial?: boolean;
137
143
  stopSource?: StopSource;
138
144
  stoppedBy?: StopSource;
145
+ onReapTerminal?: () => void;
146
+ terminalNotified?: true;
139
147
  }
140
148
  export interface MonitorTimers {
141
149
  setInterval: (fn: () => unknown, ms: number) => unknown;
@@ -182,6 +190,13 @@ export interface MonitorTaskHandle extends SemaTaskHandle {
182
190
  stopSource?: StopSource;
183
191
  stoppedBy?: StopSource;
184
192
  terminalNotified?: true;
193
+ toolResultStore?: ToolResultStore;
194
+ spillSessionId?: string;
195
+ spillSegCount?: number;
196
+ spillErrSegCount?: number;
197
+ spillCharsUsed?: number;
198
+ spillCapped?: true;
199
+ spillFailed?: true;
185
200
  }
186
201
  export type RegisteredTaskHandle = BackgroundBashTaskHandle | WorkflowTaskHandle | BackgroundAgentTaskHandle | MonitorTaskHandle;
187
202
  export interface RegisterMonitorInput extends TaskAccess {
@@ -198,6 +213,7 @@ export interface RegisterMonitorInput extends TaskAccess {
198
213
  maxBatchesPerMinute?: number;
199
214
  stormBurst?: number;
200
215
  now?: number;
216
+ toolResultStore?: ToolResultStore;
201
217
  }
202
218
  export interface RegisterWorkflowInput extends TaskAccess {
203
219
  runId: string;
@@ -226,7 +242,8 @@ export declare function clipTaskOutput(s: string, fullOutputPath?: string): stri
226
242
  export declare function statusFromBackground(status: string, exitCode?: number): SemaTaskStatus;
227
243
  export declare function rollSpoolText(spool: {
228
244
  rolledChars: number;
229
- }, s: string, cap: number): string;
245
+ }, s: string, cap: number, onDrop?: (dropped: string) => void): string;
246
+ export declare const MONITOR_SPILL_CAP_CHARS: number;
230
247
  export declare function accountDroppedBytes(spool: {
231
248
  droppedBytes?: number;
232
249
  dropUnknown?: true;
@@ -85,13 +85,16 @@ export function statusFromBackground(status, exitCode) {
85
85
  return exitCode === 0 ? "completed" : "failed";
86
86
  return "failed";
87
87
  }
88
- export function rollSpoolText(spool, s, cap) {
88
+ export function rollSpoolText(spool, s, cap, onDrop) {
89
89
  if (s.length <= cap)
90
90
  return s;
91
91
  const half = Math.floor(cap / 2);
92
- spool.rolledChars += s.length - 2 * half;
92
+ const dropped = s.slice(half, s.length - half);
93
+ spool.rolledChars += dropped.length;
94
+ onDrop?.(dropped);
93
95
  return s.slice(0, half) + s.slice(s.length - half);
94
96
  }
97
+ export const MONITOR_SPILL_CAP_CHARS = 64 * 1024 * 1024;
95
98
  export function accountDroppedBytes(spool, poll) {
96
99
  const d = poll.bytesDroppedBeforeCursor ?? 0;
97
100
  if (d > 0)
@@ -4,6 +4,7 @@ import { type BackgroundShellCapability, type BackgroundShellId } from "./backgr
4
4
  import type { WorkflowRunStore } from "./workflow-run-store.js";
5
5
  import { type BackgroundAgentStore } from "./background-agent-store.js";
6
6
  import type { TaskNotificationPayload } from "./task-notification.js";
7
+ import type { ToolResultStore } from "./tool-result-store.js";
7
8
  import { type SemaTaskType, type StopSource, type SemaTaskStatus, type TaskRetrievalStatus, type UnifiedTaskOutput, type UnifiedTaskResult, type SemaTaskHandle, type TaskAccess, type BackgroundAgentTaskHandle, type MonitorTimers, type RegisterMonitorInput, type RegisterWorkflowInput, canAccessWorkflowRun, clipTaskOutput, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE } from "./task-registry-shared.js";
8
9
  export { normalizeAgentName, DURABLE_AGENT_HEARTBEAT_MS, DURABLE_AGENT_HANDLE_RE, BG_AGENT_REAP_STOP_ERROR } from "./task-registry-shared.js";
9
10
  export type { ParkedClaimTicket, RegisterBackgroundAgentInput } from "./task-registry-shared.js";
@@ -33,6 +34,7 @@ export interface TaskPollOptions {
33
34
  timeoutMs?: number;
34
35
  signal?: AbortSignal;
35
36
  oneShot?: boolean;
37
+ toolResultStore?: ToolResultStore;
36
38
  }
37
39
  export interface TaskStopOptions {
38
40
  workflowStore?: WorkflowRunStore;
@@ -45,6 +47,7 @@ export interface TaskToolOptions extends TaskAccess {
45
47
  deadlineMs?: () => number | undefined;
46
48
  notificationWired?: boolean;
47
49
  oneShot?: boolean;
50
+ toolResultStore?: ToolResultStore;
48
51
  }
49
52
  export interface AccessibleTaskRow {
50
53
  task_id: string;
@@ -130,6 +133,8 @@ export declare class TaskRegistry {
130
133
  result?: string;
131
134
  resultFull?: string;
132
135
  error?: string;
136
+ errorCode?: string;
137
+ retryable?: boolean;
133
138
  stoppedBy?: StopSource;
134
139
  seq?: number;
135
140
  }): "completed" | "failed" | "killed" | undefined;
@@ -163,9 +168,13 @@ export declare class TaskRegistry {
163
168
  result?: string;
164
169
  resultFull?: string;
165
170
  error?: string;
171
+ errorCode?: string;
172
+ retryable?: boolean;
166
173
  }): "completed" | "failed" | "killed" | undefined;
167
174
  unmarkRetainedContinuation(id: string): void;
168
175
  attachAgentNotify(id: string, notify: NonNullable<BackgroundAgentTaskHandle["notify"]>, cycle?: number): void;
176
+ attachAgentTerminalNotifier(id: string, fn: () => void): void;
177
+ claimAgentTerminalNotify(id: string): boolean;
169
178
  deliverToRunningAgent(id: string, access: TaskAccess, notification: import("./task-notification.js").TaskNotificationPayload, opts?: {
170
179
  priority?: import("./task-notification.js").SystemInjectionPriority;
171
180
  }): 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,14 +162,31 @@ 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
  }
151
185
  runningBackgroundAgentLabels(access) {
152
186
  return runningBackgroundAgentLabelsLane(this.core, access);
153
187
  }
154
- async pollBackgroundAgent(handle, deadline, signal, oneShot) {
155
- return pollBackgroundAgentLane(handle, deadline, signal, oneShot);
188
+ async pollBackgroundAgent(handle, deadline, signal, oneShot, toolResultStore, sessionId) {
189
+ return pollBackgroundAgentLane(handle, deadline, signal, oneShot, toolResultStore, sessionId);
156
190
  }
157
191
  async stopBackgroundAgent(handle) {
158
192
  return stopBackgroundAgentLane(this.core, handle);
@@ -471,8 +505,17 @@ export class TaskRegistry {
471
505
  if (!canAccess(handle, access))
472
506
  continue;
473
507
  this.markStopSource(handle.id, "system");
508
+ const reapTerminalNote = handle.onReapTerminal;
474
509
  handle.abort.abort();
475
510
  this.settleBackgroundAgent(handle.id, { status: "killed", error: "session released" });
511
+ if (reapTerminalNote !== undefined && handle.terminalNotified !== true) {
512
+ handle.terminalNotified = true;
513
+ try {
514
+ reapTerminalNote();
515
+ }
516
+ catch {
517
+ }
518
+ }
476
519
  reaped++;
477
520
  }
478
521
  this.pokeBgQuiescence(sessionId);
@@ -748,7 +791,7 @@ export class TaskRegistry {
748
791
  if (handle.type === "background_bash")
749
792
  return this.pollBackgroundBash(handle, taskId, access, opts.filter, deadline, opts.signal);
750
793
  if (handle.type === "background_agent")
751
- return this.pollBackgroundAgent(handle, deadline, opts.signal, opts.oneShot);
794
+ return this.pollBackgroundAgent(handle, deadline, opts.signal, opts.oneShot, opts.toolResultStore, access.sessionId);
752
795
  if (handle.type === "monitor")
753
796
  return this.pollMonitor(handle, opts.filter, deadline, opts.signal);
754
797
  return this.pollWorkflow(handle, access, deadline, opts.signal);
@@ -1122,7 +1165,7 @@ The durable record is still marked running (last update ${new Date(row.updatedAt
1122
1165
  handle.status = "killed";
1123
1166
  handle.updatedAt = Date.now();
1124
1167
  mintCompletionId(handle);
1125
- this.pokeBgQuiescence(handle.owner);
1168
+ this.notifyTerminalOnce(handle, () => explicitStopBashTerminalNote(handle, true));
1126
1169
  return {
1127
1170
  content: `Terminated ${handle.id} (the process was already gone).`,
1128
1171
  details: {
@@ -1151,7 +1194,7 @@ The durable record is still marked running (last update ${new Date(row.updatedAt
1151
1194
  handle.status = "killed";
1152
1195
  handle.updatedAt = Date.now();
1153
1196
  mintCompletionId(handle);
1154
- this.pokeBgQuiescence(handle.owner);
1197
+ this.notifyTerminalOnce(handle, () => explicitStopBashTerminalNote(handle, false));
1155
1198
  return {
1156
1199
  content: `Terminated ${handle.id}.`,
1157
1200
  details: {
@@ -1193,9 +1236,10 @@ export function createTaskOutputTool(opts) {
1193
1236
  let effectiveTimeoutMs = requestedMs;
1194
1237
  let waitClampNote = "";
1195
1238
  const wall = opts.deadlineMs?.();
1239
+ const requestedWaitMs = Math.max(1, Math.floor(requestedMs ?? BLOCK_DEFAULT_TIMEOUT_MS));
1196
1240
  if (args.block !== false && wall !== undefined) {
1197
1241
  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)));
1242
+ const wantMs = Math.min(BLOCK_MAX_TIMEOUT_MS, requestedWaitMs);
1199
1243
  if (remainMs <= 0) {
1200
1244
  effectiveTimeoutMs = 1;
1201
1245
  waitClampNote =
@@ -1203,7 +1247,7 @@ export function createTaskOutputTool(opts) {
1203
1247
  }
1204
1248
  else if (remainMs < wantMs) {
1205
1249
  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.`;
1250
+ 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
1251
  }
1208
1252
  }
1209
1253
  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 } : {}) }, {
@@ -1214,6 +1258,7 @@ export function createTaskOutputTool(opts) {
1214
1258
  timeoutMs: effectiveTimeoutMs,
1215
1259
  signal: ctx.signal,
1216
1260
  oneShot: opts.oneShot,
1261
+ toolResultStore: opts.toolResultStore,
1217
1262
  });
1218
1263
  const { type: taskType, ...rest } = r.details;
1219
1264
  return {
@@ -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
  }
@@ -6,4 +6,6 @@ export declare function errorResult(text: string, details?: unknown): {
6
6
  isError: true;
7
7
  details?: unknown;
8
8
  };
9
+ export declare function isDefineToolProduct(x: unknown): x is AgentTool;
10
+ export declare function stampDefineToolBrand<T extends object>(tool: T): T;
9
11
  export declare function defineTool<TParams extends TSchema = TSchema>(spec: ToolSpec<TParams>): AgentTool<TParams>;
@@ -25,6 +25,14 @@ function isEmptyToolContent(content) {
25
25
  export function errorResult(text, details) {
26
26
  return details === undefined ? { content: text, isError: true } : { content: text, isError: true, details };
27
27
  }
28
+ const DEFINE_TOOL_BRAND = Symbol("sema.core.defineTool.product");
29
+ export function isDefineToolProduct(x) {
30
+ return typeof x === "object" && x !== null && x[DEFINE_TOOL_BRAND] === true;
31
+ }
32
+ export function stampDefineToolBrand(tool) {
33
+ Object.defineProperty(tool, DEFINE_TOOL_BRAND, { value: true, enumerable: false });
34
+ return tool;
35
+ }
28
36
  export function defineTool(spec) {
29
37
  const executionMode = spec.executionMode ?? (spec.effect === "read" ? "parallel" : "sequential");
30
38
  const tool = {
@@ -72,5 +80,6 @@ export function defineTool(spec) {
72
80
  ...(spec.aliases && spec.aliases.length > 0 ? { aliases: spec.aliases } : {}),
73
81
  });
74
82
  }
83
+ stampDefineToolBrand(tool);
75
84
  return tool;
76
85
  }
@@ -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;
@@ -26,8 +26,24 @@ export declare function oversizeJournalResult(serialized: string): boolean;
26
26
  export declare function callKeyOrdinal(callKey: string): number;
27
27
  export declare const JOURNAL_OVERSIZE_ERROR_CODE = "workflow.journal_oversize";
28
28
  export declare function journalOversizeTombstone(result: TaskResult, bytes: number): TaskResult;
29
+ export declare const IN_MEMORY_RESUME_CLAIM_TTL_MS: number;
29
30
  export declare class InMemoryWorkflowJournalStore implements WorkflowJournalStore {
30
31
  private readonly runs;
32
+ private readonly resumeClaims;
33
+ private claimsForScope;
34
+ resumeClaim(input: {
35
+ sourceRunId: string;
36
+ newRunId: string;
37
+ scope: string;
38
+ }): Promise<{
39
+ granted: boolean;
40
+ holder?: string;
41
+ }>;
42
+ releaseResumeClaim(input: {
43
+ sourceRunId: string;
44
+ newRunId: string;
45
+ scope: string;
46
+ }): Promise<void>;
31
47
  load(runId: string, scope: string): Promise<WorkflowJournalEntry[]>;
32
48
  append(runId: string, scope: string, entry: WorkflowJournalEntry): Promise<void>;
33
49
  }
@@ -29,8 +29,36 @@ function snapshot(entry) {
29
29
  return JSON.parse(JSON.stringify(entry));
30
30
  }
31
31
  }
32
+ export const IN_MEMORY_RESUME_CLAIM_TTL_MS = 60 * 60 * 1000;
32
33
  export class InMemoryWorkflowJournalStore {
33
34
  runs = new Map();
35
+ resumeClaims = new Map();
36
+ claimsForScope(scope) {
37
+ let m = this.resumeClaims.get(scope);
38
+ if (m === undefined) {
39
+ m = new Map();
40
+ this.resumeClaims.set(scope, m);
41
+ }
42
+ return m;
43
+ }
44
+ async resumeClaim(input) {
45
+ const table = this.claimsForScope(input.scope);
46
+ const now = Date.now();
47
+ const existing = table.get(input.sourceRunId);
48
+ if (existing !== undefined && existing.expiresAt > now && existing.holder !== input.newRunId) {
49
+ return { granted: false, holder: existing.holder };
50
+ }
51
+ table.set(input.sourceRunId, { holder: input.newRunId, expiresAt: now + IN_MEMORY_RESUME_CLAIM_TTL_MS });
52
+ return { granted: true };
53
+ }
54
+ async releaseResumeClaim(input) {
55
+ const table = this.resumeClaims.get(input.scope);
56
+ if (table === undefined)
57
+ return;
58
+ const existing = table.get(input.sourceRunId);
59
+ if (existing !== undefined && existing.holder === input.newRunId)
60
+ table.delete(input.sourceRunId);
61
+ }
34
62
  async load(runId, scope) {
35
63
  const rec = this.runs.get(runId);
36
64
  if (!rec || rec.scope !== scope)
@@ -28,6 +28,7 @@ export declare class NodeLspManager implements LspServerManager {
28
28
  private sweepTimer?;
29
29
  private readonly cache;
30
30
  private readonly opening;
31
+ private readonly failed;
31
32
  private disposed;
32
33
  constructor(opts?: NodeLspManagerOptions);
33
34
  private sweepIdle;
@@ -35,6 +36,7 @@ export declare class NodeLspManager implements LspServerManager {
35
36
  private open;
36
37
  private evictOverCap;
37
38
  evict(root: string): Promise<void>;
39
+ clearFailed(root?: string): void;
38
40
  private openSession;
39
41
  private serverCmd;
40
42
  dispose(): Promise<void>;
@@ -59,6 +59,7 @@ export class NodeLspManager {
59
59
  sweepTimer;
60
60
  cache = new Map();
61
61
  opening = new Map();
62
+ failed = new Map();
62
63
  disposed = false;
63
64
  constructor(opts = {}) {
64
65
  this.servers = { ...DEFAULT_LSP_SERVERS, ...opts.servers };
@@ -96,6 +97,11 @@ export class NodeLspManager {
96
97
  cached.lastUsed = Date.now();
97
98
  return cached.session;
98
99
  }
100
+ const neg = this.failed.get(key);
101
+ if (neg !== undefined) {
102
+ neg.attempts += 1;
103
+ return undefined;
104
+ }
99
105
  if (cached) {
100
106
  this.cache.delete(key);
101
107
  const carry = cached.session.openedFiles();
@@ -163,6 +169,14 @@ export class NodeLspManager {
163
169
  }
164
170
  await Promise.all(closing);
165
171
  }
172
+ clearFailed(root) {
173
+ for (const [key, e] of [...this.failed]) {
174
+ if (root !== undefined && e.root !== root)
175
+ continue;
176
+ this.failed.delete(key);
177
+ this.log("lsp_negative_cache_cleared", { key, attempts: e.attempts });
178
+ }
179
+ }
166
180
  async openSession(language, root, signal, _env) {
167
181
  const cmd = this.serverCmd(language);
168
182
  if (!cmd)
@@ -171,6 +185,8 @@ export class NodeLspManager {
171
185
  const [command, ...args] = tokens.length > 0 ? tokens : [""];
172
186
  const child = await this.spawnFn(command, args, root, signal).catch(() => undefined);
173
187
  if (!child) {
188
+ if (signal?.aborted !== true)
189
+ this.failed.set(`${language} ${root}`, { root, attempts: 1 });
174
190
  this.log("lsp_spawn_degraded", { language, root, command });
175
191
  return undefined;
176
192
  }
package/dist/index.d.ts CHANGED
@@ -66,6 +66,7 @@ export { sanitizeUntrustedText, delimitUntrusted, inlineUntrusted } from "./core
66
66
  export { deriveInvariants, checkInvariants } from "./core/property-harness.js";
67
67
  export type { InvariantKind, FunctionContract, Invariant, InvariantViolation, CheckResult, } from "./core/property-harness.js";
68
68
  export { HAND_TOOL_EFFECTS, bashReversibilityProbe, BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName, classifyCompoundReadonly, MAX_EDIT_BYTES } from "./tools/fs/index.js";
69
+ export { resolveBashTimeoutCaps } from "./tools/fs/index.js";
69
70
  export { InMemoryToolResultStore, OFFLOAD_TOOL_NAME, DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, type ToolResultStore, type ToolResultSlice, } from "./core/tool-result-store.js";
70
71
  export { InMemoryCheckpointStore, CheckpointError, mintCheckpointToken, checkpointVersionOf, CURRENT_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, WALLTIME_CHECKPOINT_VERSION, debitLedger, remainingBudgetMicroUsd, remainingWalltimeMs, winnerFromOutcome, validatePendingSteer, riskSeverity, buildRiskDescriptor, summarizeCheckpoint, type RiskDescriptor, type CheckpointStore, type CheckpointSummary, type Checkpoint, type CheckpointToken, type CheckpointGate, type CheckpointState, type SerializedCheckpointState, type CheckpointFaultMode, type PendingAction, type ResumeOutcome, type ResolvedOutcome, type ReopenReason, type ResolveExpectation, type SafetyAxis, type ResourceLedger, type ResourceLimitReason, } from "./core/checkpoint-store.js";
71
72
  export { InMemoryFileSnapshotStore, DEFAULT_SNAPSHOT_BOUNDS } from "./core/file-snapshot-store.js";
package/dist/index.js CHANGED
@@ -57,6 +57,7 @@ export { runExecGate } from "./core/exec-gate.js";
57
57
  export { sanitizeUntrustedText, delimitUntrusted, inlineUntrusted } from "./core/untrusted-text.js";
58
58
  export { deriveInvariants, checkInvariants } from "./core/property-harness.js";
59
59
  export { HAND_TOOL_EFFECTS, bashReversibilityProbe, BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName, classifyCompoundReadonly, MAX_EDIT_BYTES } from "./tools/fs/index.js";
60
+ export { resolveBashTimeoutCaps } from "./tools/fs/index.js";
60
61
  export { InMemoryToolResultStore, OFFLOAD_TOOL_NAME, DEFAULT_TOOL_RESULT_THRESHOLD_CHARS, } from "./core/tool-result-store.js";
61
62
  export { InMemoryCheckpointStore, CheckpointError, mintCheckpointToken, checkpointVersionOf, CURRENT_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, WALLTIME_CHECKPOINT_VERSION, debitLedger, remainingBudgetMicroUsd, remainingWalltimeMs, winnerFromOutcome, validatePendingSteer, riskSeverity, buildRiskDescriptor, summarizeCheckpoint, } from "./core/checkpoint-store.js";
62
63
  export { InMemoryFileSnapshotStore, DEFAULT_SNAPSHOT_BOUNDS } from "./core/file-snapshot-store.js";