@sema-agent/core 1.452.0 → 2.0.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 (162) hide show
  1. package/dist/agents/agent-transcript-tool.d.ts +15 -0
  2. package/dist/agents/agent-transcript-tool.js +122 -0
  3. package/dist/agents/cascade.js +5 -8
  4. package/dist/agents/repair-loop.d.ts +1 -1
  5. package/dist/agents/repair-loop.js +1 -1
  6. package/dist/agents/retain-ledger.d.ts +60 -0
  7. package/dist/agents/retain-ledger.js +225 -0
  8. package/dist/agents/roster-store.d.ts +1 -0
  9. package/dist/agents/roster-store.js +9 -0
  10. package/dist/agents/send-message-tool.d.ts +41 -0
  11. package/dist/agents/send-message-tool.js +469 -0
  12. package/dist/agents/subagent.d.ts +33 -108
  13. package/dist/agents/subagent.js +22 -834
  14. package/dist/agents/teacher.d.ts +0 -1
  15. package/dist/agents/teacher.js +0 -9
  16. package/dist/bin/sema-tb.js +3 -3
  17. package/dist/brain/anthropic.js +15 -40
  18. package/dist/brain/openai.js +15 -40
  19. package/dist/brain/reasoning.d.ts +1 -1
  20. package/dist/brain/stream-engine.js +2 -2
  21. package/dist/brain/stream-shared.d.ts +17 -0
  22. package/dist/brain/stream-shared.js +50 -0
  23. package/dist/brain/timeout.d.ts +1 -1
  24. package/dist/brain/timeout.js +1 -1
  25. package/dist/config/catalog.js +1 -3
  26. package/dist/config/defaults.d.ts +6 -0
  27. package/dist/config/defaults.js +6 -0
  28. package/dist/core/ask-question.d.ts +1 -1
  29. package/dist/core/background-shell.d.ts +1 -1
  30. package/dist/core/checkpoint-store.d.ts +0 -1
  31. package/dist/core/exec-gate.d.ts +1 -1
  32. package/dist/core/exec-output-tail.d.ts +1 -0
  33. package/dist/core/exec-output-tail.js +6 -0
  34. package/dist/core/file-snapshot-store.d.ts +1 -1
  35. package/dist/core/fs-write-gate-policy.d.ts +1 -1
  36. package/dist/core/git-worktree-env.d.ts +1 -1
  37. package/dist/core/hooks.d.ts +1 -1
  38. package/dist/core/image-downsample.d.ts +44 -0
  39. package/dist/core/image-downsample.js +97 -0
  40. package/dist/core/lsp.d.ts +1 -1
  41. package/dist/core/mcp.d.ts +3 -45
  42. package/dist/core/mcp.js +2 -97
  43. package/dist/core/media-byte-cap.d.ts +1 -1
  44. package/dist/core/memory-recall.d.ts +0 -12
  45. package/dist/core/memory-recall.js +0 -222
  46. package/dist/core/memory.d.ts +0 -6
  47. package/dist/core/memory.js +0 -88
  48. package/dist/core/message-utils.d.ts +1 -1
  49. package/dist/core/oracle-isolation.d.ts +1 -1
  50. package/dist/core/remote-env.d.ts +1 -1
  51. package/dist/core/roles.d.ts +1 -1
  52. package/dist/core/runner/active-skill-scope.d.ts +1 -1
  53. package/dist/core/runner/assemble-result.d.ts +0 -2
  54. package/dist/core/runner/memory-consolidation.d.ts +1 -1
  55. package/dist/core/runner/prepare-memory.d.ts +16 -0
  56. package/dist/core/runner/prepare-memory.js +191 -0
  57. package/dist/core/runner/prepare-task.d.ts +4 -28
  58. package/dist/core/runner/prepare-task.js +19 -207
  59. package/dist/core/runner/prompt-suggestions.d.ts +1 -1
  60. package/dist/core/runner/runtask.d.ts +2 -3
  61. package/dist/core/runner/runtask.js +337 -529
  62. package/dist/core/runner/session-rule-policy.d.ts +1 -1
  63. package/dist/core/runner/synthetic-tools.d.ts +5 -5
  64. package/dist/core/runner/synthetic-tools.js +4 -4
  65. package/dist/core/runner/tool-disclosure.d.ts +3 -3
  66. package/dist/core/runner/tool-disclosure.js +2 -2
  67. package/dist/core/runner/tool-output-projection.d.ts +6 -0
  68. package/dist/core/runner/tool-output-projection.js +81 -0
  69. package/dist/core/runtime.d.ts +1 -1
  70. package/dist/core/scheduler.d.ts +1 -1
  71. package/dist/core/sensitive-path-policy.d.ts +1 -1
  72. package/dist/core/session-reconcile.d.ts +1 -1
  73. package/dist/core/session-store.d.ts +2 -1
  74. package/dist/core/session-store.js +2 -1
  75. package/dist/core/session.d.ts +0 -1
  76. package/dist/core/task-registry-agent.d.ts +115 -0
  77. package/dist/core/task-registry-agent.js +1163 -0
  78. package/dist/core/task-registry-monitor.d.ts +13 -0
  79. package/dist/core/task-registry-monitor.js +347 -0
  80. package/dist/core/task-registry-shared.d.ts +294 -0
  81. package/dist/core/task-registry-shared.js +271 -0
  82. package/dist/core/task-registry-workflow.d.ts +5 -0
  83. package/dist/core/task-registry-workflow.js +143 -0
  84. package/dist/core/task-registry.d.ts +51 -216
  85. package/dist/core/task-registry.js +127 -1940
  86. package/dist/core/task-tool-shape.d.ts +32 -0
  87. package/dist/core/task-tool-shape.js +83 -0
  88. package/dist/core/tool-policy.d.ts +1 -2
  89. package/dist/core/tool-result-budget.d.ts +1 -1
  90. package/dist/core/tool-result-store.d.ts +2 -2
  91. package/dist/core/tool-result-store.js +1 -1
  92. package/dist/core/tools.d.ts +2 -3
  93. package/dist/core/tools.js +0 -8
  94. package/dist/core/trace.d.ts +2 -2
  95. package/dist/core/types.d.ts +0 -29
  96. package/dist/core/workflow-run-store-contract.d.ts +1 -1
  97. package/dist/core/workflow-run-store-contract.js +19 -19
  98. package/dist/engine/execution-env/node-execution-env.d.ts +13 -0
  99. package/dist/engine/execution-env/node-execution-env.js +55 -25
  100. package/dist/engine/lsp/frame-decoder.d.ts +1 -1
  101. package/dist/engine/lsp/frame-decoder.js +1 -1
  102. package/dist/engine/lsp/node-lsp-manager.d.ts +1 -1
  103. package/dist/engine/lsp/stdio-lsp-transport.js +2 -2
  104. package/dist/index.d.ts +16 -14
  105. package/dist/index.js +13 -11
  106. package/dist/internal/harness-types.d.ts +7 -0
  107. package/dist/internal/harness-types.js +1 -0
  108. package/dist/internal/harness.d.ts +1 -7
  109. package/dist/internal/harness.js +1 -0
  110. package/dist/orchestration/goal.js +2 -2
  111. package/dist/orchestration/run-workflow-tool.d.ts +1 -1
  112. package/dist/orchestration/workflow-types.d.ts +193 -0
  113. package/dist/orchestration/workflow-types.js +54 -0
  114. package/dist/orchestration/workflow.d.ts +3 -193
  115. package/dist/orchestration/workflow.js +7 -59
  116. package/dist/prompt-assembly/assemble.d.ts +3 -3
  117. package/dist/prompt-assembly/assemble.js +0 -16
  118. package/dist/prompt-assembly/explain.d.ts +1 -1
  119. package/dist/prompt-assembly/tool-catalog.d.ts +1 -1
  120. package/dist/prompts/default.d.ts +1 -6
  121. package/dist/prompts/default.js +3 -11
  122. package/dist/scenarios/env.d.ts +1 -1
  123. package/dist/scenarios/full-body.d.ts +0 -3
  124. package/dist/scenarios/full-body.js +0 -2
  125. package/dist/stores/file/background-agent-store.d.ts +2 -15
  126. package/dist/stores/file/background-agent-store.js +30 -100
  127. package/dist/stores/file/checkpoint-store.d.ts +1 -14
  128. package/dist/stores/file/checkpoint-store.js +47 -119
  129. package/dist/stores/file/file-snapshot-store.d.ts +1 -1
  130. package/dist/stores/file/shared-ledger.d.ts +35 -0
  131. package/dist/stores/file/shared-ledger.js +94 -0
  132. package/dist/stores/file/workflow-run-store.d.ts +1 -14
  133. package/dist/stores/file/workflow-run-store.js +24 -93
  134. package/dist/tools/fs/bash-readonly-classifier.d.ts +8 -0
  135. package/dist/tools/fs/bash-readonly-classifier.js +166 -0
  136. package/dist/tools/fs/fs-bash.d.ts +31 -0
  137. package/dist/tools/fs/fs-bash.js +672 -0
  138. package/dist/tools/fs/fs-pdf.d.ts +22 -0
  139. package/dist/tools/fs/fs-pdf.js +236 -0
  140. package/dist/tools/fs/fs-read.d.ts +8 -0
  141. package/dist/tools/fs/fs-read.js +286 -0
  142. package/dist/tools/fs/fs-search-tools.d.ts +5 -0
  143. package/dist/tools/fs/fs-search-tools.js +173 -0
  144. package/dist/tools/fs/fs-shared.d.ts +55 -0
  145. package/dist/tools/fs/fs-shared.js +144 -0
  146. package/dist/tools/fs/fs-write.d.ts +8 -0
  147. package/dist/tools/fs/fs-write.js +423 -0
  148. package/dist/tools/fs/index.d.ts +12 -65
  149. package/dist/tools/fs/index.js +26 -2090
  150. package/dist/tools/fs/pdf.d.ts +1 -1
  151. package/dist/tools/fs/repo-map.d.ts +3 -3
  152. package/dist/tools/fs/repo-map.js +1 -1
  153. package/dist/tools/fs/safety.d.ts +1 -1
  154. package/dist/tools/fs/search.d.ts +1 -1
  155. package/dist/tools/monitor.d.ts +1 -1
  156. package/dist/tools/scheduler-tools.d.ts +1 -1
  157. package/dist/tools/web.d.ts +2 -2
  158. package/dist/tools/web.js +1 -1
  159. package/dist/tools/worktree.d.ts +1 -1
  160. package/package.json +1 -1
  161. package/dist/core/dynamic-recall.d.ts +0 -32
  162. package/dist/core/dynamic-recall.js +0 -76
@@ -1,94 +1,29 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { Type } from "typebox";
3
- import { uuidv7 } from "../internal/harness.js";
4
2
  import { defineTool, errorResult } from "./tools.js";
5
3
  import { hasBackgroundShell } from "./background-shell.js";
6
4
  import { shutdownDebug } from "./shutdown-debug.js";
7
- import { summarizeWorkflowRun } from "./workflow-run-store.js";
8
- import { canAccessAgentRecord, BackgroundAgentStoreError, } from "./background-agent-store.js";
5
+ import { canAccessAgentRecord, } from "./background-agent-store.js";
9
6
  import { delimitUntrusted } from "./untrusted-text.js";
10
- import { boundedRedactedSummary } from "./untrusted-egress.js";
11
- import { clipWithFilePointer } from "./tool-errors.js";
12
- function mintCompletionId(target) {
13
- if (target.completionId === undefined)
14
- target.completionId = uuidv7();
15
- return target.completionId;
16
- }
17
- function commitCompletionIdIfEmpty(target, value) {
18
- if (target.completionId === undefined)
19
- target.completionId = value;
20
- return target.completionId;
21
- }
22
- const defaultMonitorTimers = {
23
- setInterval: (fn, ms) => {
24
- const h = setInterval(() => void fn(), ms);
25
- h.unref?.();
26
- return h;
27
- },
28
- clearInterval: (h) => clearInterval(h),
29
- now: () => Date.now(),
30
- };
7
+ import { registerMonitorLane, pollMonitorLane, stopMonitorLane } from "./task-registry-monitor.js";
8
+ import { registerWorkflowLane, pollWorkflowLane, stopWorkflowLane } from "./task-registry-workflow.js";
9
+ import { mintCompletionId, canAccessWorkflowRun, formatWorkflowRun, clipTaskOutput, assertOwnership, sleepPollStep, statusFromBackground, rollSpoolText, accountDroppedBytes, droppedGapNote, alreadyTerminalStopNote, TASK_OUTPUT_MAX_CHARS, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE, canAccess, DURABLE_AGENT_HANDLE_RE, } from "./task-registry-shared.js";
10
+ export { normalizeAgentName, DURABLE_AGENT_HEARTBEAT_MS, DURABLE_AGENT_HANDLE_RE, BG_AGENT_REAP_STOP_ERROR } from "./task-registry-shared.js";
11
+ import { TASK_OUTPUT_TOOL_NAME, TASK_STOP_TOOL_NAME, TASK_OUTPUT_ALIASES, TASK_STOP_ALIASES, TASK_OUTPUT_CONTRACT, TASK_STOP_CONTRACT, TASK_OUTPUT_MISSING_ID_MESSAGE, TASK_STOP_MISSING_ID_MESSAGE, TASK_STOP_PARAMS, resolveTaskIdArg, REGISTRY_TASK_TOOL_CAPS, composeTaskOutputDescription, composeTaskOutputParams, composeTaskStopDescription, } from "./task-tool-shape.js";
12
+ import { durableAgentArmedLane, durableAgentRowProbeLane, beginDurableClaimLane, endDurableClaimLane, reapDurableAgentsLane, releaseDurableTranscriptAnchorLane, bindBackgroundAgentSessionLane, registerBackgroundAgentLane, parkBackgroundAgentLane, reconcileParkedAgentsLane, claimParkedAgentLane, rollbackParkedClaimLane, consumeParkedFlipLane, finalizeParkedResumeLane, settleBackgroundAgentLane, abortBackgroundAgentsForOwnerLane, serveDurableAgentRowLane, resolveBackgroundAgentByNameLane, markRetainedContinuationLane, reviveBackgroundAgentLane, settleRevivedAgentLane, unmarkRetainedContinuationLane, attachAgentNotifyLane, deliverToRunningAgentLane, runningBackgroundAgentLabelsLane, pollBackgroundAgentLane, stopBackgroundAgentLane, } from "./task-registry-agent.js";
13
+ export { canAccessWorkflowRun, clipTaskOutput, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE };
31
14
  const BLOCK_DEFAULT_TIMEOUT_MS = 30_000;
32
15
  const BLOCK_MAX_TIMEOUT_MS = 600_000;
33
- const BLOCK_POLL_INTERVAL_MS = 400;
34
16
  function blockDeadline(timeoutMs, now) {
35
17
  const t = Math.min(BLOCK_MAX_TIMEOUT_MS, Math.max(1, Math.floor(timeoutMs ?? BLOCK_DEFAULT_TIMEOUT_MS)));
36
18
  return now + t;
37
19
  }
38
- const sleepPollStep = (deadline, signal) => sleepMs(deadline === undefined ? BLOCK_POLL_INTERVAL_MS : Math.max(1, Math.min(BLOCK_POLL_INTERVAL_MS, deadline - Date.now())), signal);
39
- const sleepMs = (ms, signal) => new Promise((resolve) => {
40
- const t = setTimeout(done, ms);
41
- function done() {
42
- clearTimeout(t);
43
- signal?.removeEventListener("abort", done);
44
- resolve();
45
- }
46
- signal?.addEventListener("abort", done, { once: true });
47
- });
48
20
  const BLOCK_WAIT_WRITEOUT_RESERVE_MS = 5_000;
49
- export const DURABLE_AGENT_HEARTBEAT_MS = 60_000;
50
- export const DURABLE_AGENT_HANDLE_RE = /^a[0-9a-f]{6,32}$/;
51
21
  const TASK_PREFIX = {
52
22
  background_bash: "b",
53
23
  workflow: "w",
54
24
  background_agent: "a",
55
25
  monitor: "m",
56
26
  };
57
- export const BG_AGENT_REAP_STOP_ERROR = "stopped (parent task ended or TaskStop)";
58
- export const MONITOR_BATCH_WINDOW_MS = 200;
59
- export const MONITOR_DEFAULT_TIMEOUT_MS = 300_000;
60
- export const MONITOR_MAX_TIMEOUT_MS = 3_600_000;
61
- export const MONITOR_MAX_BATCHES_PER_MINUTE = 50;
62
- const MONITOR_EVENT_RESULT_CAP = 2_000;
63
- function clipMonitorEvent(s) {
64
- return s.length <= MONITOR_EVENT_RESULT_CAP ? s : `${s.slice(0, MONITOR_EVENT_RESULT_CAP)}\n…[+${s.length - MONITOR_EVENT_RESULT_CAP} chars truncated]`;
65
- }
66
- const TASK_OUTPUT_DEFAULT_CHARS = 32_000;
67
- const TASK_OUTPUT_MAX_CHARS = 160_000;
68
- const TASK_OUTPUT_ALIASES = ["BashOutput", "AgentOutputTool", "BashOutputTool", "AgentOutput", "WorkflowStatus"];
69
- const TASK_STOP_ALIASES = ["KillShell", "KillBash"];
70
- const TASK_OUTPUT_MIN_CHARS = 512;
71
- function taskMaxOutputChars() {
72
- const raw = process.env.TASK_MAX_OUTPUT_LENGTH;
73
- if (!raw)
74
- return TASK_OUTPUT_DEFAULT_CHARS;
75
- if (!/^\d+$/.test(raw.trim()))
76
- return TASK_OUTPUT_DEFAULT_CHARS;
77
- const n = parseInt(raw.trim(), 10);
78
- if (!Number.isFinite(n) || n <= 0)
79
- return TASK_OUTPUT_DEFAULT_CHARS;
80
- return Math.min(Math.max(n, TASK_OUTPUT_MIN_CHARS), TASK_OUTPUT_MAX_CHARS);
81
- }
82
- export function clipTaskOutput(s, fullOutputPath) {
83
- return clipWithFilePointer(s, taskMaxOutputChars(), fullOutputPath);
84
- }
85
- function firstString(...values) {
86
- for (const value of values) {
87
- if (typeof value === "string" && value.trim() !== "")
88
- return value.trim();
89
- }
90
- return undefined;
91
- }
92
27
  function noTask(taskId) {
93
28
  return {
94
29
  content: JSON.stringify({ error: "task not found" }),
@@ -96,49 +31,6 @@ function noTask(taskId) {
96
31
  isError: true,
97
32
  };
98
33
  }
99
- export function normalizeAgentName(s) {
100
- return s
101
- .normalize("NFKC")
102
- .replace(/[\p{Cc}\p{Cf}]/gu, (t) => (/\s/.test(t) ? t : ""))
103
- .trim()
104
- .toLowerCase()
105
- .replace(/\s+/g, "-");
106
- }
107
- function editDistance(a, b) {
108
- if (a === b)
109
- return 0;
110
- const m = a.length;
111
- const n = b.length;
112
- const d = Array.from({ length: m + 1 }, (_, i) => Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)));
113
- for (let i = 1; i <= m; i++) {
114
- for (let j = 1; j <= n; j++) {
115
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
116
- d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
117
- if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
118
- d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
119
- }
120
- }
121
- }
122
- return d[m][n];
123
- }
124
- function closestName(query, candidates) {
125
- let best;
126
- let bestDist = 3;
127
- const seen = new Set();
128
- for (const c of candidates) {
129
- if (seen.has(c))
130
- continue;
131
- seen.add(c);
132
- if (Math.abs(c.length - query.length) > 2)
133
- continue;
134
- const dist = editDistance(query, c);
135
- if (dist <= 2 && dist < bestDist) {
136
- bestDist = dist;
137
- best = c;
138
- }
139
- }
140
- return best;
141
- }
142
34
  function noTaskForStop(taskId, enrich) {
143
35
  let content = `No task found with ID: ${taskId}`;
144
36
  if (enrich?.suggestion !== undefined)
@@ -156,427 +48,120 @@ function bashTerminalResult(spool) {
156
48
  const err = spool.stderr.slice(-2_000);
157
49
  return err.length > 0 ? `${out}${out.length > 0 ? "\n" : ""}--- stderr (tail) ---\n${err}` : out;
158
50
  }
159
- function statusFromBackground(status, exitCode) {
160
- if (status === "running")
161
- return "running";
162
- if (status === "killed")
163
- return "killed";
164
- if (status === "failed")
165
- return "failed";
166
- if (status === "exited")
167
- return exitCode === 0 ? "completed" : "failed";
168
- return "failed";
169
- }
170
- function rollSpoolText(spool, s, cap) {
171
- if (s.length <= cap)
172
- return s;
173
- const half = Math.floor(cap / 2);
174
- spool.rolledChars += s.length - 2 * half;
175
- return s.slice(0, half) + s.slice(s.length - half);
176
- }
177
51
  function bashMirrorGapNote(handle) {
178
52
  return handle.outputFile !== undefined && handle.spool?.mirrorFailed === true
179
53
  ? " [!] The output file is INCOMPLETE (one or more writes to it failed) — trust the result text / TaskOutput over the file."
180
54
  : "";
181
55
  }
182
- function accountDroppedBytes(spool, poll) {
183
- const d = poll.bytesDroppedBeforeCursor ?? 0;
184
- if (d > 0)
185
- spool.droppedBytes = (spool.droppedBytes ?? 0) + d;
186
- else if (poll.truncated === true)
187
- spool.dropUnknown = true;
188
- }
189
- function droppedGapNote(spool) {
190
- if ((spool.droppedBytes ?? 0) > 0)
191
- return ` [!] ${spool.droppedBytes} earlier byte(s) of output were permanently dropped before this window (execution-environment tail buffer evicted) — the event stream and this spool are INCOMPLETE.`;
192
- if (spool.dropUnknown === true)
193
- return " [!] An unknown amount of earlier output was permanently dropped before this window (execution-environment tail buffer evicted) — the event stream and this spool are INCOMPLETE.";
194
- return "";
195
- }
196
- function firstDropNote(spool) {
197
- if (spool.dropNotified === true)
198
- return "";
199
- const note = droppedGapNote(spool);
200
- if (note !== "")
201
- spool.dropNotified = true;
202
- return note;
203
- }
204
- function alreadyTerminalStopNote(id, status) {
205
- return `${id} already ${status} — nothing to stop; poll TaskOutput for its final state.`;
206
- }
207
56
  function monitorInTimeoutWindow(handle) {
208
57
  return handle.status === "running" && handle.deadlineAt !== undefined && handle.timers.now() < handle.deadlineAt;
209
58
  }
210
- function canAccess(handle, access) {
211
- if (handle.scope === undefined || access.scope === undefined || access.scope !== handle.scope)
212
- return false;
213
- if (handle.owner === undefined)
214
- return false;
215
- if (access.owner === handle.owner)
216
- return true;
217
- if (handle.sessionScoped && access.sessionId !== undefined && access.sessionId === handle.owner)
218
- return true;
219
- if (handle.type === "workflow" && access.sessionId !== undefined && handle.originatingSessionId === access.sessionId)
220
- return true;
221
- return false;
222
- }
223
- function assertOwnership(input, site) {
224
- if (input.owner === undefined || input.scope === undefined) {
225
- throw new Error(`TaskRegistry.${site}: owner AND scope are both required (access is default-deny on both axes; use scope "default" for a single-tenant deployment)`);
226
- }
227
- }
228
- export function canAccessWorkflowRun(run, access) {
229
- if (access.scope === undefined || run.scope !== access.scope)
230
- return false;
231
- if (access.owner !== undefined && run.sourceTaskId !== undefined && run.sourceTaskId === access.owner)
232
- return true;
233
- if (access.sessionId !== undefined && run.originatingSessionId !== undefined && run.originatingSessionId === access.sessionId)
234
- return true;
235
- return false;
236
- }
237
- const WORKFLOW_PARTIAL_RESULTS_MAX = 5;
238
- const WORKFLOW_AGENT_ROWS_MAX = 60;
239
- function workflowAgentRow(a, ordinal) {
240
- return {
241
- ordinal,
242
- label: a.label,
243
- ...(a.phase !== undefined ? { phase: a.phase } : {}),
244
- status: a.status,
245
- ...(a.errorCode !== undefined ? { errorCode: a.errorCode } : {}),
246
- ...(a.errorMessage !== undefined
247
- ? { error: delimitUntrusted(`workflow agent ${a.label} error`, boundedRedactedSummary(a.errorMessage, 200)) }
248
- : {}),
249
- ...(a.startedAt !== undefined && a.endedAt !== undefined ? { elapsedMs: a.endedAt - a.startedAt } : {}),
250
- ...(a.replayed === true ? { replayed: true } : {}),
251
- };
252
- }
253
- function formatWorkflowRun(run) {
254
- const summary = summarizeWorkflowRun(run);
255
- const done = run.agents.filter((a) => a.status === "completed" || a.status === "failed").length;
256
- const failed = run.agents.filter((a) => a.status === "failed").length;
257
- const allRows = run.agents.map((a, i) => ({ a, i }));
258
- const overCap = allRows.length > WORKFLOW_AGENT_ROWS_MAX;
259
- const keptRows = overCap
260
- ? (() => {
261
- const urgent = allRows.filter((r) => r.a.status !== "completed");
262
- const budget = Math.max(0, WORKFLOW_AGENT_ROWS_MAX - urgent.length);
263
- const fill = allRows.filter((r) => r.a.status === "completed").slice(0, budget);
264
- return [...urgent, ...fill].sort((x, y) => x.i - y.i);
265
- })()
266
- : allRows;
267
- const agentRows = keptRows.map((r) => workflowAgentRow(r.a, r.i));
268
- const finishedAgents = run.status === "failed" ? run.agents.filter((a) => a.status === "completed" && a.output !== undefined && a.output !== "") : [];
269
- return {
270
- content: JSON.stringify({
271
- task_id: run.id,
272
- runId: run.id,
273
- type: "workflow",
274
- status: run.status,
275
- ...(summary.name ? { name: summary.name } : {}),
276
- ...(summary.currentPhase ? { currentPhase: summary.currentPhase } : {}),
277
- agents: { done, total: run.agents.length, failed },
278
- agent_runs: agentRows,
279
- ...(overCap ? { agent_runs_note: `showing ${agentRows.length} of ${run.agents.length} agent rows (every non-completed row kept; completed rows trimmed).` } : {}),
280
- startedAt: run.startedAt,
281
- ...(run.endedAt !== undefined ? { endedAt: run.endedAt, elapsedMs: run.endedAt - run.startedAt } : {}),
282
- ...(run.resume !== undefined
283
- ? {
284
- resume: {
285
- from_run_id: run.resume.fromRunId,
286
- journal_entries: run.resume.journalEntries,
287
- replayed: run.resume.replayed,
288
- ...(run.resume.divergedAtOrdinal !== undefined
289
- ? { replay_stopped_at: run.resume.divergedAtOrdinal, replay_stopped_reason: run.resume.divergedReason }
290
- : {}),
291
- },
292
- }
293
- : {}),
294
- ...(run.journalSkips !== undefined && run.journalSkips > 0 ? { journal_skipped: run.journalSkips } : {}),
295
- ...(run.status === "failed" && run.error ? { error: delimitUntrusted("workflow error", boundedRedactedSummary(run.error, 500)) } : {}),
296
- ...(run.status === "completed" && run.result !== undefined
297
- ? { result: delimitUntrusted("workflow result", run.resultFull ?? run.result) }
298
- : {}),
299
- ...(finishedAgents.length > 0
300
- ? {
301
- partial_results: finishedAgents.slice(0, WORKFLOW_PARTIAL_RESULTS_MAX).map((a) => ({
302
- agent: a.label,
303
- output: delimitUntrusted(`workflow agent ${a.label} output`, a.output),
304
- })),
305
- ...(finishedAgents.length > WORKFLOW_PARTIAL_RESULTS_MAX
306
- ? { partial_results_note: `showing ${WORKFLOW_PARTIAL_RESULTS_MAX} of ${finishedAgents.length} completed agent outputs; the full set remains retrievable from the workflow run record (getWorkflowRun / the workflow journal).` }
307
- : {}),
308
- }
309
- : {}),
310
- note: run.status === "running"
311
- ? "still running — poll again shortly."
312
- : run.status === "completed"
313
- ? run.result !== undefined
314
- ? "completed — `result` above is the workflow's return value."
315
- : "completed — this run predates result persistence; its result was delivered on the completion notification."
316
- : finishedAgents.length > 0
317
- ? `failed — see error. ${finishedAgents.length} agent(s) had already completed before the run ended; their outputs are inlined under \`partial_results\`.`
318
- : "failed — see error.",
319
- }),
320
- details: {
321
- task_id: run.id,
322
- type: "workflow",
323
- status: run.status,
324
- retrieval_status: "success",
325
- ...(run.completionId !== undefined ? { completionId: run.completionId } : {}),
326
- details: summary,
327
- },
328
- };
329
- }
330
59
  export class TaskRegistry {
331
60
  handles = new Map();
332
61
  sessionReapHooks = new Set();
333
62
  bgQuiescenceWatchers = new Map();
334
63
  legacyToTaskId = new Map();
335
- writerId = randomBytes(8).toString("hex");
336
- reapingHandles = new Set();
337
- claimingHandles = new Set();
338
- durableHeartbeatTimer;
339
- ensureDurableHeartbeat() {
340
- if (this.durableHeartbeatTimer !== undefined)
341
- return;
342
- const t = setInterval(() => {
343
- let liveLanes = 0;
344
- for (const h of this.handles.values()) {
345
- if (h.type !== "background_agent")
346
- continue;
347
- const bh = h;
348
- if (!bh.durable || bh.durable.poisoned)
349
- continue;
350
- if (bh.status === "running") {
351
- liveLanes++;
352
- this.durableAgentWrite(bh, {});
353
- }
354
- else if (bh.durable.flushFailed) {
355
- if ((bh.durable.redrives = (bh.durable.redrives ?? 0) + 1) > 60) {
356
- bh.durable.poisoned = true;
357
- process.emitWarning(`sema durable-agents: terminal flush for ${bh.durable.record.handle} unrecoverable after 60 heartbeat re-drives — durable writes for this row stop here`);
358
- continue;
359
- }
360
- liveLanes++;
361
- this.durableAgentWrite(bh, {});
362
- }
363
- }
364
- if (liveLanes === 0) {
365
- clearInterval(t);
366
- this.durableHeartbeatTimer = undefined;
367
- }
368
- }, DURABLE_AGENT_HEARTBEAT_MS);
369
- t.unref?.();
370
- this.durableHeartbeatTimer = t;
371
- }
372
- durableAgentWrite(handle, patch, clear) {
373
- const lane = handle.durable;
374
- if (!lane || lane.poisoned)
375
- return;
376
- lane.chain = lane.chain.then(async () => {
377
- if (lane.poisoned)
378
- return;
379
- Object.assign(lane.record, patch);
380
- if (clear !== undefined)
381
- for (const k of clear)
382
- delete lane.record[k];
383
- lane.record.updatedAt = Date.now();
384
- const poison = (why) => {
385
- lane.poisoned = true;
386
- process.emitWarning(`sema durable-agents: ${why} for ${lane.record.handle} — durable writes for this row stop here`);
387
- };
388
- const adoptOwnRow = async () => {
389
- try {
390
- const live = await lane.store.get(lane.record.handle, lane.record.scope);
391
- if (!live)
392
- return "gone";
393
- if (live.writerId !== lane.record.writerId)
394
- return "foreign";
395
- if (live.writerEpoch !== lane.record.writerEpoch)
396
- return "foreign";
397
- lane.record.rev = live.rev;
398
- lane.written = true;
399
- return "adopted";
400
- }
401
- catch {
402
- return "unreadable";
403
- }
404
- };
405
- for (let attempt = 0; attempt < 3; attempt++) {
406
- if (lane.poisoned)
407
- return;
408
- if (attempt > 0) {
409
- await new Promise((r) => {
410
- const t = setTimeout(r, 50 * attempt);
411
- t.unref?.();
412
- });
413
- }
414
- try {
415
- if (!lane.written) {
416
- await lane.store.put(structuredClone(lane.record));
417
- lane.written = true;
418
- lane.flushFailed = false;
419
- lane.redrives = 0;
420
- return;
421
- }
422
- const ok = await lane.store.update(lane.record.handle, lane.record.scope, structuredClone(lane.record), {
423
- rev: lane.record.rev,
424
- });
425
- if (ok) {
426
- lane.record.rev += 1;
427
- lane.flushFailed = false;
428
- lane.redrives = 0;
429
- return;
430
- }
431
- const verdict = await adoptOwnRow();
432
- if (lane.poisoned)
433
- return;
434
- if (verdict === "adopted")
435
- continue;
436
- if (verdict === "unreadable")
437
- continue;
438
- poison(verdict === "gone" ? "row reaped mid-write" : "CAS lost to a foreign writer");
439
- return;
440
- }
441
- catch (e) {
442
- if (e instanceof BackgroundAgentStoreError && e.code === "agent_record.already_exists") {
443
- const verdict = await adoptOwnRow();
444
- if (lane.poisoned)
445
- return;
446
- if (verdict === "adopted")
447
- continue;
448
- if (verdict === "unreadable")
449
- continue;
450
- if (verdict === "gone")
451
- continue;
452
- poison("handle collision (foreign durable row left untouched)");
453
- return;
454
- }
455
- shutdownDebug(`durable-agents write failed for ${lane.record.handle} (attempt ${attempt + 1}/3)`, { error: String(e) });
456
- }
457
- }
458
- lane.flushFailed = true;
459
- });
460
- }
64
+ core = (() => {
65
+ const self = this;
66
+ return {
67
+ handles: self.handles,
68
+ mintTaskId: (type) => self.mintTaskId(type),
69
+ markStopSource: (id, source) => self.markStopSource(id, source),
70
+ notifyTerminalOnce: (handle, send) => self.notifyTerminalOnce(handle, send),
71
+ pokeBgQuiescence: (owner) => self.pokeBgQuiescence(owner),
72
+ get writerId() { return self.writerId; },
73
+ get reapingHandles() { return self.reapingHandles; },
74
+ get claimingHandles() { return self.claimingHandles; },
75
+ get durableHeartbeatTimer() { return self.durableHeartbeatTimer; },
76
+ set durableHeartbeatTimer(v) { self.durableHeartbeatTimer = v; },
77
+ get deliverConfirmTimeoutMs() { return self.deliverConfirmTimeoutMs; },
78
+ };
79
+ })();
461
80
  durableAgentArmed(id) {
462
- const h = this.handles.get(id);
463
- if (!h || h.type !== "background_agent")
464
- return false;
465
- const lane = h.durable;
466
- return lane !== undefined && !lane.poisoned;
81
+ return durableAgentArmedLane(this.core, id);
467
82
  }
468
83
  durableAgentRowProbe(id) {
469
- const h = this.handles.get(id);
470
- if (!h || h.type !== "background_agent")
471
- return undefined;
472
- const lane = h.durable;
473
- if (!lane)
474
- return undefined;
475
- return async () => {
476
- await lane.chain.catch(() => undefined);
477
- return lane.written && !lane.poisoned && !lane.flushFailed;
478
- };
84
+ return durableAgentRowProbeLane(this.core, id);
479
85
  }
480
86
  beginDurableClaim(id) {
481
- const live = this.handles.get(id);
482
- if (live !== undefined && (live.status === "running" || live.status === "pending"))
483
- return false;
484
- if (this.reapingHandles.has(id) || this.claimingHandles.has(id))
485
- return false;
486
- this.claimingHandles.add(id);
487
- return true;
87
+ return beginDurableClaimLane(this.core, id);
488
88
  }
489
89
  endDurableClaim(id) {
490
- this.claimingHandles.delete(id);
90
+ return endDurableClaimLane(this.core, id);
491
91
  }
492
92
  async reapDurableAgents(scope, deps, policy) {
493
- const now = policy.now ?? Date.now();
494
- if (policy.staleRunningMaxAgeMs !== undefined) {
495
- await deps.store.reap(scope, now, { staleRunningMaxAgeMs: policy.staleRunningMaxAgeMs });
496
- }
497
- if (policy.maxAgeMs === undefined && policy.keep === undefined)
498
- return { rowsReaped: 0, sessionsReleased: 0, skippedNoSessions: 0 };
499
- const terminal = (await deps.store.listByScope(scope)).filter((r) => r.status !== "running" && r.status !== "parked");
500
- terminal.sort((a, b) => b.spawnedAt - a.spawnedAt);
501
- const doomed = new Map();
502
- if (policy.maxAgeMs !== undefined) {
503
- for (const r of terminal) {
504
- if ((r.settledAt ?? r.spawnedAt) < now - policy.maxAgeMs)
505
- doomed.set(r.handle, r);
506
- }
507
- }
508
- if (policy.keep !== undefined) {
509
- for (const r of terminal.slice(Math.max(0, policy.keep)))
510
- doomed.set(r.handle, r);
511
- }
512
- let rowsReaped = 0;
513
- let sessionsReleased = 0;
514
- let skippedNoSessions = 0;
515
- for (const r of doomed.values()) {
516
- if (this.claimingHandles.has(r.handle))
517
- continue;
518
- this.reapingHandles.add(r.handle);
519
- try {
520
- let live;
521
- try {
522
- live = await deps.store.get(r.handle, scope);
523
- }
524
- catch {
525
- continue;
526
- }
527
- if (!live || live.status === "running" || live.status === "parked")
528
- continue;
529
- const inProc = this.handles.get(r.handle);
530
- if (inProc !== undefined && (inProc.status === "running" || inProc.status === "pending" || inProc.status === "parked"))
531
- continue;
532
- if (live.sessionId !== undefined && deps.sessions === undefined) {
533
- skippedNoSessions++;
534
- continue;
535
- }
536
- try {
537
- if (!(await deps.store.delete(r.handle, scope, { rev: live.rev })))
538
- continue;
539
- }
540
- catch {
541
- continue;
542
- }
543
- rowsReaped++;
544
- if (deps.mailbox !== undefined) {
545
- try {
546
- await deps.mailbox.drop(scope, r.handle);
547
- }
548
- catch {
549
- }
550
- }
551
- if (live.sessionId !== undefined && deps.sessions !== undefined) {
552
- try {
553
- await deps.sessions.unpin?.(live.sessionId);
554
- await deps.sessions.release(live.sessionId);
555
- sessionsReleased++;
556
- }
557
- catch {
558
- }
559
- }
560
- }
561
- finally {
562
- this.reapingHandles.delete(r.handle);
563
- }
564
- }
565
- return { rowsReaped, sessionsReleased, skippedNoSessions };
93
+ return reapDurableAgentsLane(this.core, scope, deps, policy);
566
94
  }
567
95
  releaseDurableTranscriptAnchor(id) {
568
- const handle = this.handles.get(id);
569
- if (!handle || handle.type !== "background_agent")
570
- return;
571
- this.durableAgentWrite(handle, {}, ["sessionId"]);
96
+ return releaseDurableTranscriptAnchorLane(this.core, id);
572
97
  }
573
98
  bindBackgroundAgentSession(id, sessionId) {
574
- const handle = this.handles.get(id);
575
- if (!handle || handle.type !== "background_agent")
576
- return;
577
- handle.sessionId = sessionId;
578
- this.durableAgentWrite(handle, { sessionId });
99
+ return bindBackgroundAgentSessionLane(this.core, id, sessionId);
100
+ }
101
+ registerBackgroundAgent(input) {
102
+ return registerBackgroundAgentLane(this.core, input);
103
+ }
104
+ parkBackgroundAgent(id, park) {
105
+ return parkBackgroundAgentLane(this.core, id, park);
106
+ }
107
+ async reconcileParkedAgents(stores, scope, now, opts) {
108
+ return reconcileParkedAgentsLane(this.core, stores, scope, now, opts);
109
+ }
110
+ async claimParkedAgent(stores, handle, access) {
111
+ return claimParkedAgentLane(stores, handle, access);
112
+ }
113
+ async rollbackParkedClaim(stores, ticket) {
114
+ return rollbackParkedClaimLane(this.core, stores, ticket);
115
+ }
116
+ async consumeParkedFlip(id, stores, ticket) {
117
+ return consumeParkedFlipLane(this.core, id, stores, ticket);
118
+ }
119
+ finalizeParkedResume(id) {
120
+ return finalizeParkedResumeLane(this.core, id);
121
+ }
122
+ settleBackgroundAgent(id, outcome) {
123
+ return settleBackgroundAgentLane(this.core, id, outcome);
124
+ }
125
+ abortBackgroundAgentsForOwner(access, opts) {
126
+ return abortBackgroundAgentsForOwnerLane(this.core, access, opts);
127
+ }
128
+ serveDurableAgentRow(row) {
129
+ return serveDurableAgentRowLane(row);
130
+ }
131
+ resolveBackgroundAgentByName(name, access, opts) {
132
+ return resolveBackgroundAgentByNameLane(this.core, name, access, opts);
133
+ }
134
+ markRetainedContinuation(id) {
135
+ return markRetainedContinuationLane(this.core, id);
579
136
  }
137
+ reviveBackgroundAgent(id, access, abort) {
138
+ return reviveBackgroundAgentLane(this.core, id, access, abort);
139
+ }
140
+ settleRevivedAgent(id, cycle, outcome) {
141
+ return settleRevivedAgentLane(this.core, id, cycle, outcome);
142
+ }
143
+ unmarkRetainedContinuation(id) {
144
+ return unmarkRetainedContinuationLane(this.core, id);
145
+ }
146
+ attachAgentNotify(id, notify, cycle) {
147
+ return attachAgentNotifyLane(this.core, id, notify, cycle);
148
+ }
149
+ async deliverToRunningAgent(id, access, notification, opts) {
150
+ return deliverToRunningAgentLane(this.core, id, access, notification, opts);
151
+ }
152
+ runningBackgroundAgentLabels(access) {
153
+ return runningBackgroundAgentLabelsLane(this.core, access);
154
+ }
155
+ async pollBackgroundAgent(handle, deadline, signal) {
156
+ return pollBackgroundAgentLane(handle, deadline, signal);
157
+ }
158
+ async stopBackgroundAgent(handle) {
159
+ return stopBackgroundAgentLane(this.core, handle);
160
+ }
161
+ writerId = randomBytes(8).toString("hex");
162
+ reapingHandles = new Set();
163
+ claimingHandles = new Set();
164
+ durableHeartbeatTimer;
580
165
  mintTaskId(type) {
581
166
  const prefix = TASK_PREFIX[type];
582
167
  for (let i = 0; i < 32; i++) {
@@ -626,99 +211,6 @@ export class TaskRegistry {
626
211
  }
627
212
  return false;
628
213
  }
629
- registerBackgroundAgent(input) {
630
- assertOwnership(input, "registerBackgroundAgent");
631
- if (input.id !== undefined && !DURABLE_AGENT_HANDLE_RE.test(input.id)) {
632
- throw new Error(`TaskRegistry: background-agent id ${JSON.stringify(input.id)} does not match the a<hex> handle form`);
633
- }
634
- const id = input.id ?? this.mintTaskId("background_agent");
635
- if (input.durableSeed !== undefined) {
636
- const prior = this.handles.get(id);
637
- if (prior !== undefined) {
638
- const parkedHandoff = prior.status === "parked" && input.initialStatus === "parked";
639
- if (prior.type !== "background_agent" || prior.status === "running" || prior.status === "pending" || (prior.status === "parked" && !parkedHandoff)) {
640
- throw new Error(`TaskRegistry: task id already registered: ${id}`);
641
- }
642
- const priorLane = prior.durable;
643
- if (priorLane !== undefined)
644
- priorLane.poisoned = true;
645
- this.handles.delete(id);
646
- }
647
- }
648
- if (this.handles.has(id))
649
- throw new Error(`TaskRegistry: task id already registered: ${id}`);
650
- const now = input.now ?? Date.now();
651
- this.handles.set(id, {
652
- id,
653
- type: "background_agent",
654
- status: input.initialStatus ?? "running",
655
- ...(input.parkedCheckpointToken !== undefined ? { parkedCheckpointToken: input.parkedCheckpointToken } : {}),
656
- ...(input.resolveParkedStop !== undefined ? { resolveParkedStop: input.resolveParkedStop } : {}),
657
- ...(input.sessionScoped ? { sessionScoped: true } : {}),
658
- ...(input.description !== undefined ? { description: input.description } : {}),
659
- ...(input.name !== undefined ? { name: input.name } : {}),
660
- ...(input.agentType !== undefined ? { agentType: input.agentType } : {}),
661
- ...(input.parentTaskId !== undefined ? { parentTaskId: input.parentTaskId } : {}),
662
- ...(input.parentSessionId !== undefined ? { parentSessionId: input.parentSessionId } : {}),
663
- ...(input.rootSessionId !== undefined ? { rootSessionId: input.rootSessionId } : {}),
664
- ...(input.deliveryChannel !== undefined ? { channelState: input.deliveryChannel } : {}),
665
- ...(input.retainedContinuation !== undefined ? { retainedContinuation: input.retainedContinuation } : {}),
666
- ...(input.owner !== undefined ? { owner: input.owner } : {}),
667
- ...(input.scope !== undefined ? { scope: input.scope } : {}),
668
- ...(input.toolUseId !== undefined ? { toolUseId: input.toolUseId } : {}),
669
- cycleSeq: input.durableSeed !== undefined ? (input.durableSeed.record.seq ?? 1) : 1,
670
- createdAt: now,
671
- updatedAt: now,
672
- abort: input.abort,
673
- });
674
- if (input.store) {
675
- const handle = this.handles.get(id);
676
- if (input.durableSeed !== undefined) {
677
- const seeded = structuredClone(input.durableSeed.record);
678
- seeded.rev = input.durableSeed.rev;
679
- handle.durable = {
680
- store: input.store,
681
- record: seeded,
682
- chain: Promise.resolve(),
683
- written: true,
684
- poisoned: false,
685
- flushFailed: false,
686
- };
687
- this.ensureDurableHeartbeat();
688
- return id;
689
- }
690
- handle.durable = {
691
- store: input.store,
692
- record: {
693
- handle: id,
694
- scope: input.scope,
695
- owner: input.owner,
696
- sessionScoped: input.sessionScoped === true,
697
- ...(input.parentSessionId !== undefined ? { parentSessionId: input.parentSessionId } : {}),
698
- ...(input.rootSessionId !== undefined ? { rootSessionId: input.rootSessionId } : {}),
699
- ...(input.parentTaskId !== undefined ? { parentTaskId: input.parentTaskId } : {}),
700
- ...(input.name !== undefined ? { name: input.name } : {}),
701
- ...(input.agentType !== undefined ? { agentType: input.agentType } : {}),
702
- ...(input.description !== undefined ? { description: input.description } : {}),
703
- ...(input.model !== undefined ? { model: input.model } : {}),
704
- ...(input.teamName !== undefined ? { teamName: input.teamName } : {}),
705
- writerId: this.writerId,
706
- writerEpoch: 1,
707
- spawnedAt: now,
708
- updatedAt: now,
709
- status: "running",
710
- rev: 0,
711
- },
712
- chain: Promise.resolve(),
713
- written: false,
714
- poisoned: false,
715
- flushFailed: false,
716
- };
717
- this.durableAgentWrite(handle, {});
718
- this.ensureDurableHeartbeat();
719
- }
720
- return id;
721
- }
722
214
  getAccessibleTask(taskId, access) {
723
215
  const h = this.handles.get(taskId);
724
216
  if (!h || !canAccess(h, access))
@@ -820,399 +312,6 @@ export class TaskRegistry {
820
312
  getCompletionId(id) {
821
313
  return this.handles.get(id)?.completionId;
822
314
  }
823
- parkBackgroundAgent(id, park) {
824
- const handle = this.handles.get(id);
825
- if (!handle || handle.type !== "background_agent")
826
- return undefined;
827
- if (handle.status !== "running")
828
- return undefined;
829
- handle.status = "parked";
830
- handle.parkedCheckpointToken = park.checkpointToken;
831
- handle.resolveParkedStop = park.resolveStop;
832
- handle.notify = undefined;
833
- for (const [, , qResolve] of handle.preAttachQueue ?? [])
834
- qResolve?.({ ok: false, reason: "not_running" });
835
- handle.preAttachQueue = undefined;
836
- for (const r of [...(handle.inFlightDirect ?? [])])
837
- r({ ok: false, reason: "not_running" });
838
- handle.inFlightDirect = undefined;
839
- handle.updatedAt = Date.now();
840
- if (park.seq !== undefined)
841
- handle.cycleSeq = park.seq;
842
- this.pokeBgQuiescence(handle.owner);
843
- this.durableAgentWrite(handle, {
844
- status: "parked",
845
- parkedCheckpointToken: park.checkpointToken,
846
- parkedAt: Date.now(),
847
- ...(park.seq !== undefined ? { seq: park.seq } : {}),
848
- }, ["parkClaimId"]);
849
- return "parked";
850
- }
851
- async reconcileParkedAgents(stores, scope, now, opts) {
852
- let liveFailed = 0;
853
- for (const handle of this.handles.values()) {
854
- if (handle.type !== "background_agent" || handle.status !== "parked")
855
- continue;
856
- if (handle.scope !== undefined && handle.scope !== scope)
857
- continue;
858
- const token = handle.parkedCheckpointToken;
859
- let state;
860
- if (token === undefined)
861
- state = "missing";
862
- else {
863
- try {
864
- const cp = await stores.checkpointStore.get(token);
865
- state = cp === null ? "missing" : cp.status;
866
- }
867
- catch {
868
- state = "unreadable";
869
- }
870
- }
871
- if (state !== "expired" && state !== "missing")
872
- continue;
873
- if (handle.status !== "parked")
874
- continue;
875
- handle.status = "failed";
876
- handle.error = `approval checkpoint ${state} while parked (reconciled)`;
877
- handle.parkedCheckpointToken = undefined;
878
- handle.resolveParkedStop = undefined;
879
- handle.updatedAt = now;
880
- mintCompletionId(handle);
881
- this.pokeBgQuiescence(handle.owner);
882
- this.durableAgentWrite(handle, { status: "failed", error: handle.error, settledAt: now, stoppedBy: "system", completionId: handle.completionId }, ["parkedCheckpointToken", "parkClaimId", "parkedAt"]);
883
- liveFailed++;
884
- }
885
- const { reconcileParkedAgents: storeReconcile } = await import("./background-agent-store.js");
886
- const liveHandles = new Set();
887
- for (const h of this.handles.values())
888
- if (h.type === "background_agent")
889
- liveHandles.add(h.id);
890
- const storeHalf = await storeReconcile(stores, scope, now, {
891
- ...(opts?.staleClaimMaxAgeMs !== undefined ? { staleClaimMaxAgeMs: opts.staleClaimMaxAgeMs } : {}),
892
- excludeHandles: liveHandles,
893
- });
894
- return { failed: storeHalf.failed + liveFailed, rolledBack: storeHalf.rolledBack };
895
- }
896
- async claimParkedAgent(stores, handle, access) {
897
- if (access.scope === undefined)
898
- return { ok: false, reason: "not_found" };
899
- let row;
900
- try {
901
- row = await stores.agentStore.get(handle, access.scope);
902
- }
903
- catch {
904
- return { ok: false, reason: "store_unreachable" };
905
- }
906
- if (!row || !canAccessAgentRecord(row, access))
907
- return { ok: false, reason: "not_found" };
908
- if (row.status !== "parked")
909
- return { ok: false, reason: "not_parked" };
910
- const token = row.parkedCheckpointToken;
911
- if (token === undefined || row.sessionId === undefined)
912
- return { ok: false, reason: "binding_broken" };
913
- let cp;
914
- try {
915
- cp = (await stores.checkpointStore.get(token));
916
- }
917
- catch {
918
- return { ok: false, reason: "store_unreachable" };
919
- }
920
- if (cp === null || cp.sessionId !== row.sessionId)
921
- return { ok: false, reason: "binding_broken" };
922
- if (cp.status !== "pending")
923
- return { ok: false, reason: "checkpoint_not_pending" };
924
- const claimId = randomBytes(8).toString("hex");
925
- const reserved = structuredClone(row);
926
- reserved.parkClaimId = claimId;
927
- reserved.updatedAt = Date.now();
928
- let won;
929
- try {
930
- won = await stores.agentStore.updateIf(handle, access.scope, reserved, {
931
- rev: row.rev,
932
- status: "parked",
933
- parkedCheckpointToken: token,
934
- parkClaimId: null,
935
- });
936
- }
937
- catch {
938
- return { ok: false, reason: "store_unreachable" };
939
- }
940
- if (!won)
941
- return { ok: false, reason: "claim_lost" };
942
- return {
943
- ok: true,
944
- ticket: { handle, scope: access.scope, token, claimId, reservedRev: row.rev + 1, row: structuredClone(row) },
945
- };
946
- }
947
- async rollbackParkedClaim(stores, ticket) {
948
- let cpStatus;
949
- try {
950
- const cp = (await stores.checkpointStore.get(ticket.token));
951
- cpStatus = cp === null ? "missing" : cp.status;
952
- }
953
- catch {
954
- return "retry";
955
- }
956
- const now = Date.now();
957
- const rollbackCompletionIdCandidate = uuidv7();
958
- const settleLocalParkedHandle = (error) => {
959
- const h = this.handles.get(ticket.handle);
960
- if (h !== undefined && h.type === "background_agent" && h.status === "parked") {
961
- h.status = "failed";
962
- h.error = error;
963
- h.parkedCheckpointToken = undefined;
964
- h.resolveParkedStop = undefined;
965
- h.updatedAt = now;
966
- commitCompletionIdIfEmpty(h, rollbackCompletionIdCandidate);
967
- this.pokeBgQuiescence(h.owner);
968
- }
969
- };
970
- let liveRow;
971
- try {
972
- liveRow = await stores.agentStore.get(ticket.handle, ticket.scope);
973
- }
974
- catch {
975
- return "retry";
976
- }
977
- if (!liveRow || liveRow.parkClaimId !== ticket.claimId)
978
- return "lost";
979
- if (cpStatus === "pending") {
980
- const restored = structuredClone(liveRow);
981
- delete restored.parkClaimId;
982
- restored.updatedAt = now;
983
- restored.writerEpoch = (liveRow.writerEpoch ?? 0) + 1;
984
- const liveHandle = this.handles.get(ticket.handle);
985
- if (liveHandle?.type === "background_agent" &&
986
- liveHandle.durable !== undefined &&
987
- liveHandle.durable.record.writerId === liveRow.writerId &&
988
- (liveHandle.durable.record.writerEpoch ?? 0) === (liveRow.writerEpoch ?? 0)) {
989
- liveHandle.durable.record.writerEpoch = restored.writerEpoch;
990
- }
991
- try {
992
- const won = await stores.agentStore.updateIf(ticket.handle, ticket.scope, restored, {
993
- rev: liveRow.rev,
994
- status: "parked",
995
- parkedCheckpointToken: ticket.token,
996
- parkClaimId: ticket.claimId,
997
- });
998
- if (!won)
999
- return "lost";
1000
- try {
1001
- const cp2 = (await stores.checkpointStore.get(ticket.token));
1002
- if (cp2 !== null && cp2.status === "pending")
1003
- return "rolled_back";
1004
- const post = await stores.agentStore.get(ticket.handle, ticket.scope);
1005
- if (post && post.status === "parked") {
1006
- const term = structuredClone(post);
1007
- term.status = "failed";
1008
- term.error = `approval checkpoint ${cp2 === null ? "missing" : cp2.status} under the resume claim (claim rollback)`;
1009
- term.stoppedBy = "system";
1010
- term.settledAt = now;
1011
- term.updatedAt = now;
1012
- term.completionId = rollbackCompletionIdCandidate;
1013
- delete term.parkedCheckpointToken;
1014
- delete term.parkClaimId;
1015
- delete term.parkedAt;
1016
- if (await stores.agentStore.updateIf(ticket.handle, ticket.scope, term, { rev: post.rev, status: "parked" })) {
1017
- settleLocalParkedHandle(term.error);
1018
- return "failed";
1019
- }
1020
- }
1021
- return "lost";
1022
- }
1023
- catch {
1024
- return "rolled_back";
1025
- }
1026
- }
1027
- catch {
1028
- return "retry";
1029
- }
1030
- }
1031
- const error = cpStatus === "resolved"
1032
- ? "approval consumed but the resume could not proceed — outcome unknown (claim rollback)"
1033
- : `approval checkpoint ${cpStatus} under the resume claim (claim rollback)`;
1034
- if (liveRow.status !== "parked")
1035
- return "lost";
1036
- const failed = structuredClone(liveRow);
1037
- failed.status = "failed";
1038
- failed.error = error;
1039
- failed.stoppedBy = "system";
1040
- failed.settledAt = now;
1041
- failed.updatedAt = now;
1042
- failed.completionId = rollbackCompletionIdCandidate;
1043
- delete failed.parkedCheckpointToken;
1044
- delete failed.parkClaimId;
1045
- delete failed.parkedAt;
1046
- try {
1047
- const won = await stores.agentStore.updateIf(ticket.handle, ticket.scope, failed, {
1048
- rev: liveRow.rev,
1049
- status: "parked",
1050
- parkClaimId: ticket.claimId,
1051
- });
1052
- if (won)
1053
- settleLocalParkedHandle(error);
1054
- return won ? "failed" : "lost";
1055
- }
1056
- catch {
1057
- return "retry";
1058
- }
1059
- }
1060
- async consumeParkedFlip(id, stores, ticket) {
1061
- const handle = this.handles.get(id);
1062
- if (!handle || handle.type !== "background_agent" || handle.status !== "parked")
1063
- return false;
1064
- const lane = handle.durable;
1065
- const doFlip = async () => {
1066
- let live;
1067
- try {
1068
- live = await stores.agentStore.get(ticket.handle, ticket.scope);
1069
- }
1070
- catch {
1071
- return undefined;
1072
- }
1073
- if (!live || live.status !== "parked" || live.parkClaimId !== ticket.claimId || live.parkedCheckpointToken !== ticket.token)
1074
- return undefined;
1075
- const flipped = structuredClone(live);
1076
- flipped.status = "running";
1077
- flipped.writerId = this.writerId;
1078
- flipped.writerEpoch = (live.writerEpoch ?? 0) + 1;
1079
- flipped.updatedAt = Date.now();
1080
- flipped.seq = (live.seq ?? 1) + 1;
1081
- delete flipped.parkedCheckpointToken;
1082
- delete flipped.parkedAt;
1083
- try {
1084
- const won = await stores.agentStore.updateIf(ticket.handle, ticket.scope, flipped, {
1085
- rev: live.rev,
1086
- status: "parked",
1087
- parkedCheckpointToken: ticket.token,
1088
- parkClaimId: ticket.claimId,
1089
- });
1090
- if (!won)
1091
- return undefined;
1092
- }
1093
- catch {
1094
- return undefined;
1095
- }
1096
- flipped.rev = live.rev + 1;
1097
- return flipped;
1098
- };
1099
- let flipped;
1100
- if (lane !== undefined && !lane.poisoned) {
1101
- const p = lane.chain.then(doFlip, doFlip);
1102
- lane.chain = p.then((f) => {
1103
- if (f !== undefined) {
1104
- lane.record = structuredClone(f);
1105
- lane.written = true;
1106
- }
1107
- });
1108
- flipped = await p;
1109
- }
1110
- else {
1111
- flipped = await doFlip();
1112
- }
1113
- if (flipped === undefined)
1114
- return false;
1115
- handle.status = "running";
1116
- handle.parkedCheckpointToken = undefined;
1117
- handle.resolveParkedStop = undefined;
1118
- handle.updatedAt = Date.now();
1119
- handle.completionId = undefined;
1120
- if (flipped.seq !== undefined)
1121
- handle.cycleSeq = flipped.seq;
1122
- return true;
1123
- }
1124
- finalizeParkedResume(id) {
1125
- const handle = this.handles.get(id);
1126
- if (!handle || handle.type !== "background_agent")
1127
- return;
1128
- this.durableAgentWrite(handle, {}, ["parkClaimId"]);
1129
- }
1130
- settleBackgroundAgent(id, outcome) {
1131
- const handle = this.handles.get(id);
1132
- if (!handle || handle.type !== "background_agent")
1133
- return undefined;
1134
- if (handle.status !== "running") {
1135
- if (handle.status === "killed") {
1136
- mintCompletionId(handle);
1137
- let backfilled = false;
1138
- if (handle.result === undefined && outcome.result !== undefined && outcome.result !== "") {
1139
- handle.result = outcome.result;
1140
- handle.resultIsPartial = true;
1141
- backfilled = true;
1142
- if (outcome.resultFull !== undefined)
1143
- handle.resultFull = outcome.resultFull;
1144
- }
1145
- if (handle.error === undefined && outcome.error !== undefined && outcome.error !== BG_AGENT_REAP_STOP_ERROR) {
1146
- handle.error = outcome.error;
1147
- backfilled = true;
1148
- }
1149
- if (backfilled) {
1150
- handle.updatedAt = Date.now();
1151
- this.durableAgentWrite(handle, {
1152
- ...(handle.result !== undefined ? { finalOutput: handle.result } : {}),
1153
- ...(handle.resultFull !== undefined ? { finalOutputFull: handle.resultFull } : {}),
1154
- ...(handle.resultIsPartial ? { resultIsPartial: true } : {}),
1155
- ...(handle.error !== undefined ? { error: handle.error } : {}),
1156
- ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
1157
- });
1158
- }
1159
- }
1160
- return handle.status === "completed" || handle.status === "failed" || handle.status === "killed" ? handle.status : undefined;
1161
- }
1162
- handle.status = outcome.status;
1163
- mintCompletionId(handle);
1164
- handle.notify = undefined;
1165
- for (const [, , qResolve] of handle.preAttachQueue ?? [])
1166
- qResolve?.({ ok: false, reason: "not_running" });
1167
- handle.preAttachQueue = undefined;
1168
- for (const r of [...(handle.inFlightDirect ?? [])])
1169
- r({ ok: false, reason: "not_running" });
1170
- handle.inFlightDirect = undefined;
1171
- if (outcome.status === "killed" && handle.stoppedBy === undefined) {
1172
- handle.stoppedBy = outcome.stoppedBy ?? handle.stopSource ?? "system";
1173
- }
1174
- if (outcome.result !== undefined)
1175
- handle.result = outcome.result;
1176
- if (outcome.resultFull !== undefined)
1177
- handle.resultFull = outcome.resultFull;
1178
- if (outcome.status === "killed" && outcome.result !== undefined && outcome.result !== "") {
1179
- handle.resultIsPartial = true;
1180
- }
1181
- if (outcome.error !== undefined && !(outcome.status === "killed" && outcome.error === BG_AGENT_REAP_STOP_ERROR)) {
1182
- handle.error = outcome.error;
1183
- }
1184
- handle.updatedAt = Date.now();
1185
- if (outcome.seq !== undefined)
1186
- handle.cycleSeq = outcome.seq;
1187
- this.pokeBgQuiescence(handle.owner);
1188
- this.durableAgentWrite(handle, {
1189
- status: outcome.status,
1190
- ...(outcome.seq !== undefined ? { seq: outcome.seq } : {}),
1191
- settledAt: Date.now(),
1192
- ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
1193
- ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
1194
- ...(handle.result !== undefined ? { finalOutput: handle.result } : {}),
1195
- ...(handle.resultFull !== undefined ? { finalOutputFull: handle.resultFull } : {}),
1196
- ...(handle.resultIsPartial ? { resultIsPartial: true } : {}),
1197
- ...(handle.error !== undefined ? { error: handle.error } : {}),
1198
- }, ["parkedCheckpointToken", "parkClaimId", "parkedAt"]);
1199
- return outcome.status;
1200
- }
1201
- abortBackgroundAgentsForOwner(access, opts) {
1202
- let aborted = 0;
1203
- for (const handle of this.handles.values()) {
1204
- if (handle.type !== "background_agent" || handle.status !== "running" || !canAccess(handle, access))
1205
- continue;
1206
- if (opts?.skipSessionScoped && handle.sessionScoped)
1207
- continue;
1208
- if (opts?.sessionScopedOnly && !handle.sessionScoped)
1209
- continue;
1210
- this.markStopSource(handle.id, "parent");
1211
- handle.abort.abort();
1212
- aborted++;
1213
- }
1214
- return aborted;
1215
- }
1216
315
  markStopSourceForOwner(access, source, opts) {
1217
316
  let marked = 0;
1218
317
  for (const handle of this.handles.values()) {
@@ -1639,243 +738,10 @@ export class TaskRegistry {
1639
738
  return out;
1640
739
  }
1641
740
  registerMonitor(input) {
1642
- assertOwnership(input, "registerMonitor");
1643
- const id = this.mintTaskId("monitor");
1644
- const now = input.now ?? Date.now();
1645
- const timers = input.timers ?? defaultMonitorTimers;
1646
- const persistent = input.persistent === true;
1647
- const timeoutMs = persistent
1648
- ? undefined
1649
- : Math.min(MONITOR_MAX_TIMEOUT_MS, Math.max(1, Math.floor(input.timeoutMs ?? MONITOR_DEFAULT_TIMEOUT_MS)));
1650
- const handle = {
1651
- id,
1652
- type: "monitor",
1653
- status: "running",
1654
- ...(input.description !== undefined ? { description: input.description } : {}),
1655
- ...(input.owner !== undefined ? { owner: input.owner } : {}),
1656
- ...(input.scope !== undefined ? { scope: input.scope } : {}),
1657
- ...(input.toolUseId !== undefined ? { toolUseId: input.toolUseId } : {}),
1658
- ...(persistent ? { persistent: true } : {}),
1659
- ...(input.sessionScoped ? { sessionScoped: true } : {}),
1660
- createdAt: now,
1661
- updatedAt: now,
1662
- env: input.env,
1663
- shellId: input.shellId,
1664
- spool: { stdout: "", stderr: "", rolledChars: 0 },
1665
- lineBuf: "",
1666
- seq: 0,
1667
- batchTimes: [],
1668
- batchWindowMs: Math.max(1, Math.floor(input.batchWindowMs ?? MONITOR_BATCH_WINDOW_MS)),
1669
- maxBatchesPerMinute: Math.max(1, Math.floor(input.maxBatchesPerMinute ?? MONITOR_MAX_BATCHES_PER_MINUTE)),
1670
- ...(timeoutMs !== undefined ? { timeoutMs, deadlineAt: timers.now() + timeoutMs } : {}),
1671
- timers,
1672
- ...(input.onEvent !== undefined ? { onEvent: input.onEvent } : {}),
1673
- };
1674
- this.handles.set(id, handle);
1675
- this.startMonitorWatcher(id);
1676
- return id;
1677
- }
1678
- absorbMonitorPoll(handle, v) {
1679
- const spool = handle.spool;
1680
- accountDroppedBytes(spool, v);
1681
- const ROLL_CAP = 2 * TASK_OUTPUT_MAX_CHARS;
1682
- spool.stdout = rollSpoolText(spool, spool.stdout + v.stdout, ROLL_CAP);
1683
- spool.stderr = rollSpoolText(spool, spool.stderr + v.stderr, ROLL_CAP);
1684
- handle.lineBuf += v.stdout;
1685
- const parts = handle.lineBuf.split(/\r?\n/);
1686
- handle.lineBuf = parts.pop() ?? "";
1687
- return parts.filter((l) => l !== "");
1688
- }
1689
- emitMonitorEvent(handle, n) {
1690
- try {
1691
- handle.onEvent?.(n);
1692
- }
1693
- catch {
1694
- }
1695
- }
1696
- startMonitorWatcher(id) {
1697
- const handle = this.handles.get(id);
1698
- if (!handle || handle.type !== "monitor")
1699
- return;
1700
- const timers = handle.timers;
1701
- let ticking = false;
1702
- const MAX_RETRYABLE_POLL_FAILURES = 3;
1703
- let pollFailures = 0;
1704
- const stop = () => {
1705
- if (handle.watcher !== undefined) {
1706
- timers.clearInterval(handle.watcher);
1707
- handle.watcher = undefined;
1708
- }
1709
- };
1710
- const emit = (n) => this.emitMonitorEvent(handle, n);
1711
- const pendingFinalLines = (lines) => {
1712
- const out = handle.lineBuf !== "" ? [...lines, handle.lineBuf] : lines;
1713
- handle.lineBuf = "";
1714
- return out;
1715
- };
1716
- const terminal = (status, summary, finalLines) => {
1717
- handle.updatedAt = Date.now();
1718
- mintCompletionId(handle);
1719
- stop();
1720
- this.notifyTerminalOnce(handle, () => emit({
1721
- task_id: id,
1722
- task_type: "monitor",
1723
- ...(handle.toolUseId !== undefined ? { toolUseId: handle.toolUseId } : {}),
1724
- status,
1725
- ...(status === "killed" && handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
1726
- summary: `${(handle.description ?? "monitor").slice(0, 200)} — ${summary}${droppedGapNote(handle.spool)}`,
1727
- ...(finalLines.length > 0 ? { lines: finalLines, result: clipMonitorEvent(finalLines.join("\n")), ...(status === "killed" ? { partial: true } : {}) } : {}),
1728
- ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
1729
- }));
1730
- };
1731
- const tick = async () => {
1732
- if (ticking)
1733
- return;
1734
- ticking = true;
1735
- handle.tickInFlight = true;
1736
- try {
1737
- if (this.handles.get(id) !== handle)
1738
- return stop();
1739
- if (handle.status !== "running")
1740
- return stop();
1741
- const r = await handle.env.pollBackground(handle.shellId);
1742
- if (this.handles.get(id) !== handle || handle.status !== "running")
1743
- return stop();
1744
- if (!r.ok) {
1745
- if ((r.error.code === "timeout" || r.error.code === "io") && ++pollFailures <= MAX_RETRYABLE_POLL_FAILURES)
1746
- return;
1747
- handle.status = "failed";
1748
- return terminal("failed", `monitor lost its process (${r.error.message}); watch ended.`, pendingFinalLines([]));
1749
- }
1750
- pollFailures = 0;
1751
- const spool = handle.spool;
1752
- const lines = this.absorbMonitorPoll(handle, r.value);
1753
- handle.status = statusFromBackground(r.value.status, r.value.exitCode);
1754
- if (r.value.status !== "running") {
1755
- spool.exitCode = r.value.exitCode;
1756
- if (handle.status === "killed" && handle.stoppedBy === undefined)
1757
- handle.stoppedBy = handle.stopSource ?? (r.value.timedOut === true ? "timeout" : "system");
1758
- if (r.value.timedOut === true) {
1759
- spool.timedOut = true;
1760
- if (r.value.timeoutSec !== undefined)
1761
- spool.timeoutSec = r.value.timeoutSec;
1762
- }
1763
- const label = r.value.status === "exited"
1764
- ? `exited(code ${r.value.exitCode}); watch ended.`
1765
- : r.value.timedOut === true
1766
- ? `timed out after ${r.value.timeoutSec !== undefined ? `${r.value.timeoutSec}s` : "its background time budget"} (the execution environment's background time budget); the process was killed and the watch ended.`
1767
- : `${r.value.status}; watch ended.`;
1768
- return terminal(handle.status === "completed" ? "completed" : handle.status === "killed" ? "killed" : "failed", label, pendingFinalLines(lines));
1769
- }
1770
- const now = timers.now();
1771
- if (handle.deadlineAt !== undefined && now >= handle.deadlineAt) {
1772
- this.markStopSource(id, "timeout");
1773
- const k = await handle.env.killBackground(handle.shellId);
1774
- if (this.handles.get(id) !== handle || handle.status !== "running")
1775
- return stop();
1776
- if (!k.ok && k.error.code !== "not_found") {
1777
- handle.status = "failed";
1778
- return terminal("failed", `timed out after ${handle.timeoutMs}ms, but the kill failed (${k.error.message}); the process may still be running. Watch ended.`, pendingFinalLines(lines));
1779
- }
1780
- handle.status = "killed";
1781
- handle.stoppedBy = handle.stopSource ?? "timeout";
1782
- return terminal("killed", `timed out after ${handle.timeoutMs}ms; the process was killed and the watch ended.`, pendingFinalLines(lines));
1783
- }
1784
- if (lines.length === 0) {
1785
- if (r.value.stdout.length > 0 || r.value.stderr.length > 0)
1786
- handle.updatedAt = Date.now();
1787
- return;
1788
- }
1789
- const cutoff = now - 60_000;
1790
- handle.batchTimes = handle.batchTimes.filter((t) => t > cutoff);
1791
- handle.batchTimes.push(now);
1792
- if (handle.batchTimes.length > handle.maxBatchesPerMinute) {
1793
- this.markStopSource(id, "system");
1794
- const k = await handle.env.killBackground(handle.shellId);
1795
- if (this.handles.get(id) !== handle || handle.status !== "running")
1796
- return stop();
1797
- if (!k.ok && k.error.code !== "not_found") {
1798
- handle.status = "failed";
1799
- return terminal("failed", `auto-stop triggered (more than ${handle.maxBatchesPerMinute} notification batches within a minute), but the kill failed (${k.error.message}); the process may still be running. Watch ended.`, pendingFinalLines(lines));
1800
- }
1801
- handle.status = "killed";
1802
- handle.stoppedBy = handle.stopSource ?? "system";
1803
- return terminal("killed", `auto-stopped: it produced too many events (more than ${handle.maxBatchesPerMinute} notification batches within a minute). ` +
1804
- `The process was killed; its full output remains readable via TaskOutput. Restart it with a quieter command (filter its output) if you still need the watch.`, pendingFinalLines(lines));
1805
- }
1806
- handle.seq += 1;
1807
- handle.updatedAt = Date.now();
1808
- emit({
1809
- task_id: id,
1810
- task_type: "monitor",
1811
- ...(handle.toolUseId !== undefined ? { toolUseId: handle.toolUseId } : {}),
1812
- status: "event",
1813
- seq: handle.seq,
1814
- lines,
1815
- summary: `${(handle.description ?? "monitor").slice(0, 200)} — ${lines.length} new output line(s)${firstDropNote(spool)}`,
1816
- result: clipMonitorEvent(lines.join("\n")),
1817
- });
1818
- }
1819
- catch (e) {
1820
- if (this.handles.get(id) !== handle || handle.status !== "running")
1821
- return stop();
1822
- if (++pollFailures <= MAX_RETRYABLE_POLL_FAILURES)
1823
- return;
1824
- handle.status = "failed";
1825
- terminal("failed", `monitor watcher crashed (${e instanceof Error ? e.message : String(e)}); watch ended.`, pendingFinalLines([]));
1826
- }
1827
- finally {
1828
- ticking = false;
1829
- handle.tickInFlight = undefined;
1830
- }
1831
- };
1832
- handle.watcher = timers.setInterval(tick, handle.batchWindowMs);
741
+ return registerMonitorLane(this.core, input);
1833
742
  }
1834
743
  registerWorkflow(input) {
1835
- assertOwnership(input, "registerWorkflow");
1836
- const id = input.runId;
1837
- if (this.handles.has(id))
1838
- throw new Error(`TaskRegistry: task id already registered: ${id}`);
1839
- const now = input.now ?? Date.now();
1840
- const originatingSessionId = input.originatingSessionId;
1841
- const handle = {
1842
- id,
1843
- type: "workflow",
1844
- status: "running",
1845
- ...(input.description !== undefined ? { description: input.description } : {}),
1846
- ...(input.owner !== undefined ? { owner: input.owner } : {}),
1847
- ...(input.scope !== undefined ? { scope: input.scope } : {}),
1848
- ...(input.toolUseId !== undefined ? { toolUseId: input.toolUseId } : {}),
1849
- createdAt: now,
1850
- updatedAt: now,
1851
- runId: id,
1852
- handle: input.handle,
1853
- ...(input.store !== undefined ? { store: input.store } : {}),
1854
- ...(originatingSessionId !== undefined ? { originatingSessionId, sessionScoped: true } : {}),
1855
- ...(input.onServedTerminal !== undefined ? { onServedTerminal: input.onServedTerminal } : {}),
1856
- };
1857
- this.handles.set(id, handle);
1858
- void input.handle.done.then((res) => {
1859
- handle.status = "completed";
1860
- const run = res?.run;
1861
- const r = run?.resultFull ?? run?.result;
1862
- if (typeof r === "string")
1863
- handle.result = r;
1864
- if (run?.completionId !== undefined)
1865
- handle.completionId = run.completionId;
1866
- handle.updatedAt = Date.now();
1867
- }, (err) => {
1868
- if (handle.status !== "cancelled")
1869
- handle.status = "failed";
1870
- handle.error = boundedRedactedSummary(err instanceof Error ? err.message : String(err), 500);
1871
- const rejectedRun = err !== null && (typeof err === "object" || typeof err === "function")
1872
- ? err.workflowRun
1873
- : undefined;
1874
- if (rejectedRun?.completionId !== undefined)
1875
- handle.completionId = rejectedRun.completionId;
1876
- handle.updatedAt = Date.now();
1877
- });
1878
- return id;
744
+ return registerWorkflowLane(this.core, input);
1879
745
  }
1880
746
  async pollTask(id, access, opts = {}) {
1881
747
  const taskId = id.trim();
@@ -1939,38 +805,6 @@ The durable record is still marked running (last update ${new Date(row.updatedAt
1939
805
  }
1940
806
  return noTask(taskId);
1941
807
  }
1942
- serveDurableAgentRow(row) {
1943
- if (row.status === "parked") {
1944
- return {
1945
- content: delimitUntrusted(`TaskOutput ${row.handle}`, `status: parked
1946
- The agent is durably suspended, waiting for an approval decision. It resumes when the pending approval is decided (durable approval inbox), or lands failed if the approval expires.`),
1947
- details: {
1948
- task_id: row.handle,
1949
- type: "background_agent",
1950
- status: "parked",
1951
- retrieval_status: "success",
1952
- ...(row.seq !== undefined ? { seq: row.seq } : {}),
1953
- },
1954
- };
1955
- }
1956
- const body = `status: ${row.status}
1957
- ${row.error ? `error: ${row.error}
1958
- ` : ""}${row.finalOutput ? `--- result${row.resultIsPartial ? " (partial — produced before the task was stopped)" : ""} ---
1959
- ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}`;
1960
- return {
1961
- content: delimitUntrusted(`TaskOutput ${row.handle}`, body),
1962
- details: {
1963
- task_id: row.handle,
1964
- type: "background_agent",
1965
- status: row.status,
1966
- retrieval_status: "success",
1967
- ...(row.status === "killed" && row.stoppedBy !== undefined ? { stoppedBy: row.stoppedBy } : {}),
1968
- ...(row.seq !== undefined ? { seq: row.seq } : {}),
1969
- ...(row.resultIsPartial ? { partial_result: true } : {}),
1970
- ...(row.completionId !== undefined ? { completionId: row.completionId } : {}),
1971
- },
1972
- };
1973
- }
1974
808
  async stopTask(id, access, opts = {}) {
1975
809
  const taskId = id.trim();
1976
810
  const handle = this.resolveInternal(taskId, access);
@@ -2026,265 +860,24 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
2026
860
  retrieval_status: row.status === "parked" ? "not_ready" : "success",
2027
861
  ...(row.status === "running" ? { error: "not_local" } : {}),
2028
862
  ...(row.status === "parked" ? { error: "parked_pending_approval" } : {}),
2029
- },
2030
- };
2031
- }
2032
- }
2033
- const byName = this.resolveBackgroundAgentByName(taskId, access, { preferRunning: true });
2034
- if (byName.status === "found")
2035
- return this.stopBackgroundAgent(byName.handle);
2036
- if (byName.status === "ambiguous") {
2037
- return {
2038
- content: byName.message,
2039
- details: { task_id: taskId, retrieval_status: "not_ready", error: "ambiguous" },
2040
- isError: true,
2041
- };
2042
- }
2043
- return noTaskForStop(taskId, {
2044
- ...(byName.suggestion !== undefined ? { suggestion: byName.suggestion } : {}),
2045
- runningAgents: this.runningBackgroundAgentLabels(access),
2046
- });
2047
- }
2048
- resolveBackgroundAgentByName(name, access, opts) {
2049
- const key = normalizeAgentName(name);
2050
- if (key === "")
2051
- return { status: "not_found" };
2052
- const latestOf = (pool) => {
2053
- let best;
2054
- for (const h of pool)
2055
- if (best === undefined || h.createdAt >= best.createdAt)
2056
- best = h;
2057
- return best;
2058
- };
2059
- const named = [];
2060
- for (const h of this.handles.values()) {
2061
- if (h.type !== "background_agent" || h.name === undefined || !canAccess(h, access))
2062
- continue;
2063
- if (h.name === name || normalizeAgentName(h.name) === key)
2064
- named.push(h);
2065
- }
2066
- if (opts?.preferRunning === true) {
2067
- const namedRunning = named.filter((h) => h.status === "running");
2068
- if (namedRunning.length > 1) {
2069
- return {
2070
- status: "ambiguous",
2071
- message: `Multiple RUNNING background agents match "${name}": ${namedRunning.map((h) => `${h.id} (${h.name ?? h.description ?? "unnamed"})`).join(", ")}. Use the task ID.`,
2072
- };
2073
- }
2074
- if (namedRunning.length === 1)
2075
- return { status: "found", handle: namedRunning[0] };
2076
- }
2077
- const namedWinner = latestOf(named);
2078
- if (namedWinner !== undefined)
2079
- return { status: "found", handle: namedWinner };
2080
- const exact = [];
2081
- for (const h of this.handles.values()) {
2082
- if (h.type !== "background_agent" || !canAccess(h, access))
2083
- continue;
2084
- if (h.description === name)
2085
- exact.push(h);
2086
- }
2087
- const exactRunning = exact.filter((h) => h.status === "running");
2088
- const exactPool = exactRunning.length > 0 ? exactRunning : exact;
2089
- if (exactPool.length === 1)
2090
- return { status: "found", handle: exactPool[0] };
2091
- if (exactPool.length > 1) {
2092
- return {
2093
- status: "ambiguous",
2094
- message: `Multiple background agents match "${name}": ${exactPool.map((h) => (h.description ? `${h.id} (${h.description})` : h.id)).join(", ")}. Use the task ID.`,
2095
- };
2096
- }
2097
- const matches = [];
2098
- for (const h of this.handles.values()) {
2099
- if (h.type !== "background_agent" || !canAccess(h, access))
2100
- continue;
2101
- if (h.description !== undefined && normalizeAgentName(h.description) === key)
2102
- matches.push(h);
863
+ },
864
+ };
865
+ }
2103
866
  }
2104
- const running = matches.filter((h) => h.status === "running");
2105
- const pool = running.length > 0 ? running : matches;
2106
- if (pool.length === 1)
2107
- return { status: "found", handle: pool[0] };
2108
- if (pool.length > 1) {
867
+ const byName = this.resolveBackgroundAgentByName(taskId, access, { preferRunning: true });
868
+ if (byName.status === "found")
869
+ return this.stopBackgroundAgent(byName.handle);
870
+ if (byName.status === "ambiguous") {
2109
871
  return {
2110
- status: "ambiguous",
2111
- message: `Multiple background agents match "${name}": ${pool.map((h) => (h.description ? `${h.id} (${h.description})` : h.id)).join(", ")}. Use the task ID.`,
872
+ content: byName.message,
873
+ details: { task_id: taskId, retrieval_status: "not_ready", error: "ambiguous" },
874
+ isError: true,
2112
875
  };
2113
876
  }
2114
- const byKey = new Map();
2115
- for (const h of this.handles.values()) {
2116
- if (h.type !== "background_agent" || h.status !== "running" || !canAccess(h, access))
2117
- continue;
2118
- for (const label of [h.name, h.description]) {
2119
- if (label === undefined)
2120
- continue;
2121
- const k = normalizeAgentName(label);
2122
- if (k !== "" && !byKey.has(k))
2123
- byKey.set(k, label);
2124
- }
2125
- }
2126
- const closest = closestName(key, byKey.keys());
2127
- return { status: "not_found", ...(closest !== undefined ? { suggestion: byKey.get(closest) } : {}) };
2128
- }
2129
- markRetainedContinuation(id) {
2130
- const handle = this.handles.get(id);
2131
- if (handle && handle.type === "background_agent")
2132
- handle.retainedContinuation = true;
2133
- }
2134
- reviveBackgroundAgent(id, access, abort) {
2135
- if (this.reapingHandles.has(id) || this.claimingHandles.has(id))
2136
- return { ok: false, reason: "not_found" };
2137
- const handle = this.handles.get(id);
2138
- if (!handle || handle.type !== "background_agent" || !canAccess(handle, access))
2139
- return { ok: false, reason: "not_found" };
2140
- if (handle.status === "running" || handle.status === "pending")
2141
- return { ok: false, reason: "still_running" };
2142
- if (handle.status === "parked")
2143
- return { ok: false, reason: "still_running" };
2144
- handle.status = "running";
2145
- handle.channelState = "attaching";
2146
- handle.notify = undefined;
2147
- if (abort !== undefined)
2148
- handle.abort = abort;
2149
- handle.result = undefined;
2150
- handle.resultFull = undefined;
2151
- handle.error = undefined;
2152
- handle.resultIsPartial = undefined;
2153
- handle.stopSource = undefined;
2154
- handle.completionId = undefined;
2155
- handle.reviveCycle = (handle.reviveCycle ?? 0) + 1;
2156
- handle.cycleSeq = (handle.cycleSeq ?? 1) + 1;
2157
- handle.updatedAt = Date.now();
2158
- this.durableAgentWrite(handle, { status: "running" }, ["settledAt", "stoppedBy", "finalOutput", "finalOutputFull", "error", "resultIsPartial", "completionId", "summary", "recentSteps", "editedFiles", "usage"]);
2159
- return { ok: true, cycle: handle.reviveCycle };
2160
- }
2161
- settleRevivedAgent(id, cycle, outcome) {
2162
- const handle = this.handles.get(id);
2163
- if (!handle || handle.type !== "background_agent")
2164
- return undefined;
2165
- if ((handle.reviveCycle ?? 0) !== cycle)
2166
- return undefined;
2167
- return this.settleBackgroundAgent(id, outcome);
2168
- }
2169
- unmarkRetainedContinuation(id) {
2170
- const handle = this.handles.get(id);
2171
- if (handle && handle.type === "background_agent")
2172
- handle.retainedContinuation = false;
2173
- }
2174
- attachAgentNotify(id, notify, cycle) {
2175
- const handle = this.handles.get(id);
2176
- if (!handle || handle.type !== "background_agent" || handle.status !== "running")
2177
- return;
2178
- if (cycle !== undefined && (handle.reviveCycle ?? 0) !== cycle)
2179
- return;
2180
- handle.notify = notify;
2181
- handle.channelState = "attaching";
2182
- const live = handle.preAttachQueue;
2183
- if (live !== undefined && live.length > 0) {
2184
- void (async () => {
2185
- while (handle.preAttachQueue === live && live.length > 0) {
2186
- const [qPayload, qOpts, qResolve] = live[0];
2187
- let result;
2188
- try {
2189
- const d = await notify(qPayload, qOpts);
2190
- result =
2191
- d === "parked" && handle.retainedContinuation !== true
2192
- ? { ok: false, reason: "not_running" }
2193
- : d === "parked"
2194
- ? { ok: true, disposition: "parked" }
2195
- : { ok: true, disposition: "queued" };
2196
- }
2197
- catch {
2198
- result = { ok: false, reason: "not_running" };
2199
- }
2200
- if (handle.preAttachQueue !== live)
2201
- return;
2202
- live.shift();
2203
- qResolve?.(result);
2204
- }
2205
- if (handle.preAttachQueue === live) {
2206
- handle.preAttachQueue = undefined;
2207
- handle.channelState = "attached";
2208
- }
2209
- })();
2210
- }
2211
- else {
2212
- handle.preAttachQueue = undefined;
2213
- handle.channelState = "attached";
2214
- }
2215
- }
2216
- async deliverToRunningAgent(id, access, notification, opts) {
2217
- const handle = this.handles.get(id);
2218
- if (!handle || handle.type !== "background_agent" || !canAccess(handle, access))
2219
- return { ok: false, reason: "not_found" };
2220
- if (handle.status !== "running")
2221
- return { ok: false, reason: "not_running" };
2222
- const bounded = (p, fallback, onTimeout) => new Promise((resolve) => {
2223
- const t = setTimeout(() => {
2224
- try {
2225
- onTimeout?.();
2226
- }
2227
- catch {
2228
- }
2229
- resolve(fallback);
2230
- }, this.deliverConfirmTimeoutMs);
2231
- t.unref?.();
2232
- void p.then((v) => {
2233
- clearTimeout(t);
2234
- resolve(v);
2235
- }, () => {
2236
- clearTimeout(t);
2237
- resolve(fallback);
2238
- });
877
+ return noTaskForStop(taskId, {
878
+ ...(byName.suggestion !== undefined ? { suggestion: byName.suggestion } : {}),
879
+ runningAgents: this.runningBackgroundAgentLabels(access),
2239
880
  });
2240
- if (handle.notify === undefined || handle.channelState === "attaching") {
2241
- if (handle.channelState !== "attaching")
2242
- return { ok: false, reason: "no_channel" };
2243
- const q = (handle.preAttachQueue ??= []);
2244
- if (q.length >= 8)
2245
- return { ok: false, reason: "no_channel" };
2246
- return bounded(new Promise((resolve) => {
2247
- q.push([notification, opts, resolve]);
2248
- }), { ok: true, disposition: "pending" });
2249
- }
2250
- let detachOnTimeout;
2251
- return await bounded(new Promise((resolve) => {
2252
- const inflight = (handle.inFlightDirect ??= new Set());
2253
- const settleOnce = (r) => {
2254
- if (!inflight.has(settleOnce))
2255
- return;
2256
- inflight.delete(settleOnce);
2257
- resolve(r);
2258
- };
2259
- inflight.add(settleOnce);
2260
- detachOnTimeout = () => inflight.delete(settleOnce);
2261
- void (async () => {
2262
- let result;
2263
- try {
2264
- const d = await handle.notify?.(notification, opts);
2265
- if (d === "parked") {
2266
- result = handle.retainedContinuation === true ? { ok: true, disposition: "parked" } : { ok: false, reason: "not_running" };
2267
- }
2268
- else {
2269
- result = { ok: true, disposition: "queued" };
2270
- }
2271
- }
2272
- catch {
2273
- result = { ok: false, reason: "not_running" };
2274
- }
2275
- settleOnce(result);
2276
- })();
2277
- }), { ok: true, disposition: "pending" }, () => detachOnTimeout?.());
2278
- }
2279
- runningBackgroundAgentLabels(access) {
2280
- const out = [];
2281
- for (const h of this.handles.values()) {
2282
- if (h.type !== "background_agent" || h.status !== "running" || !canAccess(h, access))
2283
- continue;
2284
- const label = h.name ?? h.description;
2285
- out.push(label ? `${h.id} (${label})` : h.id);
2286
- }
2287
- return out;
2288
881
  }
2289
882
  deliverConfirmTimeoutMs = 10_000;
2290
883
  lastGcAt = 0;
@@ -2501,70 +1094,8 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
2501
1094
  await sleepPollStep(deadline, signal);
2502
1095
  }
2503
1096
  }
2504
- fireServedTerminal(handle) {
2505
- const cb = handle.onServedTerminal;
2506
- if (!cb)
2507
- return;
2508
- handle.onServedTerminal = undefined;
2509
- try {
2510
- cb();
2511
- }
2512
- catch {
2513
- }
2514
- }
2515
1097
  async pollWorkflow(handle, access, deadline, signal) {
2516
- const store = handle.store;
2517
- if (store) {
2518
- for (;;) {
2519
- const run = await store.get(handle.runId);
2520
- if (!run || !canAccessWorkflowRun(run, access))
2521
- break;
2522
- if (run.status !== "running" || deadline === undefined || Date.now() >= deadline || signal?.aborted) {
2523
- const out = formatWorkflowRun(run);
2524
- if (run.status === "running" && deadline !== undefined && Date.now() >= deadline)
2525
- out.details.retrieval_status = "timeout";
2526
- else if (run.status !== "running")
2527
- this.fireServedTerminal(handle);
2528
- return out;
2529
- }
2530
- await sleepPollStep(deadline, signal);
2531
- }
2532
- }
2533
- while (handle.status === "running" && deadline !== undefined && Date.now() < deadline && !signal?.aborted) {
2534
- await sleepPollStep(deadline, signal);
2535
- }
2536
- if (handle.status !== "running")
2537
- this.fireServedTerminal(handle);
2538
- return {
2539
- content: JSON.stringify({
2540
- task_id: handle.id,
2541
- runId: handle.id,
2542
- type: "workflow",
2543
- status: handle.status,
2544
- ...(handle.status === "completed" && handle.result !== undefined
2545
- ? { result: delimitUntrusted("workflow result", handle.result) }
2546
- : {}),
2547
- ...(handle.status === "failed" && handle.error !== undefined
2548
- ? { error: delimitUntrusted("workflow error", handle.error) }
2549
- : {}),
2550
- note: handle.status === "running"
2551
- ? "still running — poll again shortly."
2552
- : handle.status === "completed"
2553
- ? handle.result !== undefined
2554
- ? "completed — `result` above is the workflow's return value."
2555
- : "completed — its result was delivered on the completion notification."
2556
- : handle.status === "failed" && handle.error !== undefined
2557
- ? "failed — see error."
2558
- : "workflow reached a terminal state.",
2559
- }),
2560
- details: {
2561
- task_id: handle.id,
2562
- type: "workflow",
2563
- status: handle.status,
2564
- retrieval_status: handle.status === "running" && deadline !== undefined && Date.now() >= deadline ? "timeout" : "success",
2565
- ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
2566
- },
2567
- };
1098
+ return pollWorkflowLane(handle, access, deadline, signal);
2568
1099
  }
2569
1100
  async stopBackgroundBash(handle) {
2570
1101
  const wasRunning = handle.status === "running";
@@ -2637,367 +1168,30 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
2637
1168
  };
2638
1169
  }
2639
1170
  async pollMonitor(handle, filter, deadline, signal) {
2640
- let rx;
2641
- if (filter !== undefined) {
2642
- try {
2643
- rx = new RegExp(filter);
2644
- }
2645
- catch {
2646
- return {
2647
- content: `Error (TaskOutput): invalid filter regex: ${filter}`,
2648
- details: { task_id: handle.id, type: "monitor", status: handle.status, retrieval_status: "not_ready" },
2649
- isError: true,
2650
- };
2651
- }
2652
- }
2653
- while (handle.status === "running" && deadline !== undefined && Date.now() < deadline && !signal?.aborted) {
2654
- await sleepPollStep(deadline, signal);
2655
- }
2656
- const spool = handle.spool;
2657
- const shape = (text) => clipTaskOutput(rx ? text.split(/\r?\n/).filter((l) => rx.test(l)).join("\n") : text, handle.outputFile);
2658
- const running = handle.status === "running";
2659
- const retrieval = running && deadline !== undefined && Date.now() >= deadline ? "timeout" : "success";
2660
- const timedOutClause = spool.timedOut === true ? ` (timed out after ${spool.timeoutSec !== undefined ? `${spool.timeoutSec}s` : "its background time budget"})` : "";
2661
- const displayStatus = (spool.exitCode !== undefined ? `exited(code ${spool.exitCode})` : running ? "running" : handle.status) + timedOutClause;
2662
- const rolledNote = spool.rolledChars > 0 ? `[!] ${spool.rolledChars} earlier char(s) dropped from the spool (memory bound)\n` : "";
2663
- const droppedNote = (spool.droppedBytes ?? 0) > 0
2664
- ? `[!] ${spool.droppedBytes} earlier byte(s) permanently dropped before this window (8MB buffer evicted)\n`
2665
- : spool.dropUnknown === true
2666
- ? "[!] an unknown amount of earlier output was permanently dropped before this window (buffer evicted)\n"
2667
- : "";
2668
- const drainNote = spool.drainFailed === true ? "[!] the final output drain at stop failed — the tail of the output may be missing\n" : "";
2669
- const stdoutHeading = droppedNote !== "" || drainNote !== "" ? "stdout (re-readable spool — INCOMPLETE, see note above)" : "stdout (full, re-readable)";
2670
- const body = `status: ${displayStatus}\n${rolledNote}${droppedNote}${drainNote}--- ${stdoutHeading} ---\n${shape(spool.stdout) || "(none)"}\n` +
2671
- `--- stderr ---\n${shape(spool.stderr) || "(none)"}`;
2672
- return {
2673
- content: delimitUntrusted(`TaskOutput ${handle.id}`, body),
2674
- details: {
2675
- task_id: handle.id,
2676
- type: "monitor",
2677
- status: handle.status,
2678
- retrieval_status: retrieval,
2679
- ...(handle.status === "killed" && handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
2680
- ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
2681
- details: {
2682
- status: running ? "running" : handle.status,
2683
- exitCode: spool.exitCode,
2684
- ...(spool.droppedBytes !== undefined ? { bytesDroppedBeforeCursor: spool.droppedBytes } : {}),
2685
- ...(spool.timedOut === true ? { timedOut: true, ...(spool.timeoutSec !== undefined ? { timeoutSec: spool.timeoutSec } : {}) } : {}),
2686
- },
2687
- },
2688
- };
1171
+ return pollMonitorLane(handle, filter, deadline, signal);
2689
1172
  }
2690
1173
  async stopMonitor(handle) {
2691
- const wasRunning = handle.status === "running";
2692
- if (handle.watcher !== undefined) {
2693
- handle.timers.clearInterval(handle.watcher);
2694
- handle.watcher = undefined;
2695
- }
2696
- if (wasRunning && handle.tickInFlight !== true) {
2697
- try {
2698
- const drained = await handle.env.pollBackground(handle.shellId);
2699
- if (drained.ok) {
2700
- const lines = this.absorbMonitorPoll(handle, drained.value);
2701
- const finalLines = handle.lineBuf !== "" ? [...lines, handle.lineBuf] : lines;
2702
- handle.lineBuf = "";
2703
- if (finalLines.length > 0 && handle.status === "running") {
2704
- handle.seq += 1;
2705
- handle.updatedAt = Date.now();
2706
- this.emitMonitorEvent(handle, {
2707
- task_id: handle.id,
2708
- task_type: "monitor",
2709
- ...(handle.toolUseId !== undefined ? { toolUseId: handle.toolUseId } : {}),
2710
- status: "event",
2711
- seq: handle.seq,
2712
- lines: finalLines,
2713
- summary: `${(handle.description ?? "monitor").slice(0, 200)} — ${finalLines.length} final output line(s) drained at stop${firstDropNote(handle.spool)}`,
2714
- result: clipMonitorEvent(finalLines.join("\n")),
2715
- });
2716
- }
2717
- }
2718
- else {
2719
- handle.spool.drainFailed = true;
2720
- }
2721
- }
2722
- catch {
2723
- handle.spool.drainFailed = true;
2724
- }
2725
- }
2726
- const drainNote = handle.spool.drainFailed === true ? " (the final output drain failed — TaskOutput may be missing the last window)" : "";
2727
- this.markStopSource(handle.id, "parent");
2728
- const r = await handle.env.killBackground(handle.shellId);
2729
- if (handle.status !== "running") {
2730
- return {
2731
- content: wasRunning ? `Terminated ${handle.id}.${drainNote}` : alreadyTerminalStopNote(handle.id, handle.status),
2732
- details: {
2733
- task_id: handle.id,
2734
- type: "monitor",
2735
- status: handle.status,
2736
- retrieval_status: "success",
2737
- ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
2738
- ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
2739
- },
2740
- };
2741
- }
2742
- if (!r.ok) {
2743
- if (r.error.code === "not_found") {
2744
- if (handle.stoppedBy === undefined)
2745
- handle.stoppedBy = handle.stopSource ?? "parent";
2746
- handle.status = "killed";
2747
- handle.updatedAt = Date.now();
2748
- mintCompletionId(handle);
2749
- this.pokeBgQuiescence(handle.owner);
2750
- return {
2751
- content: `Terminated ${handle.id} (the process was already gone).${drainNote}`,
2752
- details: {
2753
- task_id: handle.id,
2754
- type: "monitor",
2755
- status: "killed",
2756
- retrieval_status: "success",
2757
- stoppedBy: handle.stoppedBy,
2758
- ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
2759
- },
2760
- };
2761
- }
2762
- if (handle.watcher === undefined)
2763
- this.startMonitorWatcher(handle.id);
2764
- const retrieval_status = r.error.code === "timeout" ? "timeout" : "not_ready";
2765
- return {
2766
- content: `Error (TaskStop): ${r.error.message} — the kill did not land; the process may still be running. The watch keeps running: events and the terminal notification still fire.`,
2767
- isError: true,
2768
- details: { task_id: handle.id, type: "monitor", status: handle.status, retrieval_status, error: r.error.code },
2769
- };
2770
- }
2771
- if (handle.status === "running") {
2772
- if (handle.stoppedBy === undefined)
2773
- handle.stoppedBy = handle.stopSource ?? "parent";
2774
- handle.status = "killed";
2775
- handle.updatedAt = Date.now();
2776
- mintCompletionId(handle);
2777
- this.pokeBgQuiescence(handle.owner);
2778
- }
2779
- return {
2780
- content: `Terminated ${handle.id}.${drainNote}`,
2781
- details: {
2782
- task_id: handle.id,
2783
- type: "monitor",
2784
- status: handle.status,
2785
- retrieval_status: "success",
2786
- ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
2787
- ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
2788
- },
2789
- };
2790
- }
2791
- async pollBackgroundAgent(handle, deadline, signal) {
2792
- while (handle.status === "running" && deadline !== undefined && Date.now() < deadline && !signal?.aborted) {
2793
- await sleepPollStep(deadline, signal);
2794
- }
2795
- const running = handle.status === "running";
2796
- const retrieval = running && deadline !== undefined && Date.now() >= deadline ? "timeout" : "success";
2797
- if (handle.status === "parked") {
2798
- return {
2799
- content: delimitUntrusted(`TaskOutput ${handle.id}`, `status: parked
2800
- The agent is durably suspended, waiting for an approval decision. It resumes when the pending approval is decided (durable approval inbox), or lands failed if the approval expires.`),
2801
- details: {
2802
- task_id: handle.id,
2803
- type: "background_agent",
2804
- status: "parked",
2805
- retrieval_status: "success",
2806
- ...(handle.cycleSeq !== undefined ? { seq: handle.cycleSeq } : {}),
2807
- },
2808
- };
2809
- }
2810
- const body = running
2811
- ? `status: running
2812
- The agent is still working — you will be notified when it completes.`
2813
- : `status: ${handle.status}
2814
- ${handle.error ? `error: ${handle.error}
2815
- ` : ""}${handle.result ? `--- result${handle.resultIsPartial ? " (partial — produced before the task was stopped)" : ""} ---
2816
- ${clipTaskOutput(handle.resultFull ?? handle.result, handle.outputFile)}` : "(no result text)"}`;
2817
- return {
2818
- content: delimitUntrusted(`TaskOutput ${handle.id}`, body),
2819
- details: {
2820
- task_id: handle.id,
2821
- type: "background_agent",
2822
- status: handle.status,
2823
- retrieval_status: retrieval,
2824
- ...(handle.cycleSeq !== undefined ? { seq: handle.cycleSeq } : {}),
2825
- ...(handle.status === "killed" && handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
2826
- ...(handle.resultIsPartial ? { partial_result: true } : {}),
2827
- ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
2828
- },
2829
- };
2830
- }
2831
- async stopBackgroundAgent(handle) {
2832
- const entryStatus = handle.status;
2833
- if (handle.status === "parked") {
2834
- const arbiter = handle.resolveParkedStop;
2835
- if (arbiter === undefined) {
2836
- return {
2837
- content: `${handle.id} is parked on a pending approval — stop it by deciding (deny/expire) the pending approval in the durable approval inbox.`,
2838
- details: { task_id: handle.id, type: "background_agent", status: "parked", retrieval_status: "not_ready", error: "parked_pending_approval" },
2839
- };
2840
- }
2841
- let stopWon = false;
2842
- let arbiterUnreachable = false;
2843
- try {
2844
- stopWon = await arbiter();
2845
- }
2846
- catch {
2847
- stopWon = false;
2848
- arbiterUnreachable = true;
2849
- }
2850
- if (stopWon && handle.status === "parked") {
2851
- this.markStopSource(handle.id, "parent");
2852
- handle.status = "killed";
2853
- handle.stoppedBy = handle.stopSource ?? "parent";
2854
- handle.updatedAt = Date.now();
2855
- mintCompletionId(handle);
2856
- handle.parkedCheckpointToken = undefined;
2857
- handle.resolveParkedStop = undefined;
2858
- this.pokeBgQuiescence(handle.owner);
2859
- {
2860
- const lane = handle.durable;
2861
- if (lane !== undefined) {
2862
- try {
2863
- const live = await lane.store.get(handle.id, lane.record.scope);
2864
- if (live && (live.status === "parked" || live.status === "running")) {
2865
- const term = structuredClone(live);
2866
- term.status = "killed";
2867
- if (handle.stoppedBy !== undefined)
2868
- term.stoppedBy = handle.stoppedBy;
2869
- term.settledAt = Date.now();
2870
- term.updatedAt = Date.now();
2871
- delete term.parkedCheckpointToken;
2872
- delete term.parkClaimId;
2873
- delete term.parkedAt;
2874
- await lane.store.update(handle.id, lane.record.scope, term, { rev: live.rev });
2875
- }
2876
- }
2877
- catch {
2878
- }
2879
- }
2880
- }
2881
- this.durableAgentWrite(handle, {
2882
- status: "killed",
2883
- settledAt: Date.now(),
2884
- ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
2885
- ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
2886
- }, ["parkedCheckpointToken", "parkClaimId"]);
2887
- return {
2888
- content: `Terminated ${handle.id} (pending approval expired).`,
2889
- details: {
2890
- task_id: handle.id,
2891
- type: "background_agent",
2892
- status: "killed",
2893
- retrieval_status: "success",
2894
- ...(handle.cycleSeq !== undefined ? { seq: handle.cycleSeq } : {}),
2895
- ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
2896
- ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
2897
- },
2898
- };
2899
- }
2900
- if (arbiterUnreachable) {
2901
- return {
2902
- content: `${handle.id} was not stopped: the approval arbitration store was unreachable — the row stays parked; retry the stop.`,
2903
- details: { task_id: handle.id, type: "background_agent", status: handle.status, retrieval_status: "not_ready", error: "park_arbiter_unreachable" },
2904
- isError: true,
2905
- };
2906
- }
2907
- return {
2908
- content: `${handle.id} was not stopped: its pending approval was already consumed (a resume won the arbitration).`,
2909
- details: { task_id: handle.id, type: "background_agent", status: handle.status, retrieval_status: "not_ready", error: "park_resume_won" },
2910
- isError: true,
2911
- };
2912
- }
2913
- if (handle.status === "running") {
2914
- this.markStopSource(handle.id, "parent");
2915
- handle.abort.abort();
2916
- handle.status = "killed";
2917
- handle.stoppedBy = handle.stopSource ?? "parent";
2918
- handle.updatedAt = Date.now();
2919
- mintCompletionId(handle);
2920
- handle.notify = undefined;
2921
- for (const [, , qResolve] of handle.preAttachQueue ?? [])
2922
- qResolve?.({ ok: false, reason: "not_running" });
2923
- handle.preAttachQueue = undefined;
2924
- for (const r of [...(handle.inFlightDirect ?? [])])
2925
- r({ ok: false, reason: "not_running" });
2926
- handle.inFlightDirect = undefined;
2927
- this.pokeBgQuiescence(handle.owner);
2928
- this.durableAgentWrite(handle, {
2929
- status: "killed",
2930
- settledAt: Date.now(),
2931
- ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
2932
- ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
2933
- });
2934
- }
2935
- return {
2936
- content: entryStatus === "running" ? `Terminated ${handle.id}.` : alreadyTerminalStopNote(handle.id, handle.status),
2937
- details: {
2938
- task_id: handle.id,
2939
- type: "background_agent",
2940
- status: handle.status,
2941
- retrieval_status: "success",
2942
- ...(handle.cycleSeq !== undefined ? { seq: handle.cycleSeq } : {}),
2943
- ...(handle.status === "killed" && handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
2944
- ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
2945
- },
2946
- };
1174
+ return stopMonitorLane(this.core, handle);
2947
1175
  }
2948
1176
  async stopWorkflow(handle) {
2949
- if (handle.status !== "running") {
2950
- return {
2951
- content: JSON.stringify({
2952
- task_id: handle.id,
2953
- runId: handle.id,
2954
- type: "workflow",
2955
- status: handle.status,
2956
- stopped: false,
2957
- note: `workflow already ${handle.status} — nothing to stop; poll TaskOutput for its final state.`,
2958
- }),
2959
- details: { task_id: handle.id, type: "workflow", status: handle.status, retrieval_status: "success" },
2960
- };
2961
- }
2962
- handle.status = "cancelled";
2963
- handle.updatedAt = Date.now();
2964
- handle.handle.cancel("TaskStop requested");
2965
- return {
2966
- content: JSON.stringify({
2967
- task_id: handle.id,
2968
- runId: handle.id,
2969
- type: "workflow",
2970
- status: "cancelled",
2971
- stopped: true,
2972
- note: "workflow cancellation requested; durable run status maps cancellation to failed when it finalizes.",
2973
- }),
2974
- details: { task_id: handle.id, type: "workflow", status: "cancelled", retrieval_status: "success" },
2975
- };
1177
+ return stopWorkflowLane(handle);
2976
1178
  }
2977
1179
  }
2978
1180
  export const defaultTaskRegistry = new TaskRegistry();
2979
1181
  export function createTaskOutputTool(opts) {
1182
+ const caps = { ...REGISTRY_TASK_TOOL_CAPS, notification: opts.notificationWired !== false };
2980
1183
  return defineTool({
2981
- name: "TaskOutput",
1184
+ name: TASK_OUTPUT_TOOL_NAME,
2982
1185
  aliases: [...TASK_OUTPUT_ALIASES],
2983
- contract: { contractId: "core.task_output@1", implementationRevision: "1" },
2984
- description: "[Deprecated] — for background Bash tasks, prefer Read on the output file path; for agent tasks, use the Agent tool result directly. " +
2985
- "DEPRECATED: Background tasks return their output file path in the tool result, and you receive a <task-notification> with the same path when the task completes. " +
2986
- "Retrieve output or status for a background task by task_id. Supports background Bash tasks and workflow " +
2987
- "tasks returned by RunWorkflow. Returns only the caller's own task scope; unknown and out-of-scope ids are " +
2988
- "reported the same way. Legacy bash_id and runId arguments are accepted for compatibility.",
2989
- parameters: Type.Object({
2990
- task_id: Type.Optional(Type.String({ description: "The task_id returned by Bash(run_in_background) or RunWorkflow." })),
2991
- block: Type.Optional(Type.Boolean({ description: "Whether to wait for completion (default true — the call waits until the task finishes or `timeout`, saving re-poll turns). Pass false for an instantaneous snapshot." })),
2992
- timeout: Type.Optional(Type.Number({ description: "Optional timeout in milliseconds when block is true (default 30000, max 600000)." })),
2993
- filter: Type.Optional(Type.String({ description: "Optional regex filter for background Bash output (sema extension)." })),
2994
- }),
1186
+ contract: TASK_OUTPUT_CONTRACT,
1187
+ description: composeTaskOutputDescription(caps),
1188
+ parameters: composeTaskOutputParams(caps),
2995
1189
  effect: "read",
2996
1190
  execute: async (rawArgs, ctx) => {
2997
1191
  const args = rawArgs;
2998
- const id = firstString(args.task_id, args.bash_id, args.runId);
1192
+ const id = resolveTaskIdArg(args.task_id, args.bash_id, args.runId);
2999
1193
  if (!id)
3000
- return errorResult("Error (TaskOutput): Missing required parameter: task_id");
1194
+ return errorResult(TASK_OUTPUT_MISSING_ID_MESSAGE);
3001
1195
  const requestedMs = typeof args.timeout === "number" ? args.timeout : undefined;
3002
1196
  let effectiveTimeoutMs = requestedMs;
3003
1197
  let waitClampNote = "";
@@ -3034,24 +1228,17 @@ export function createTaskOutputTool(opts) {
3034
1228
  }
3035
1229
  export function createTaskStopTool(opts) {
3036
1230
  return defineTool({
3037
- name: "TaskStop",
1231
+ name: TASK_STOP_TOOL_NAME,
3038
1232
  aliases: [...TASK_STOP_ALIASES],
3039
- contract: { contractId: "core.task_stop@1", implementationRevision: "1" },
3040
- description: "- Stops a running background task by its ID\n" +
3041
- "- Takes a task_id parameter identifying the task to stop\n" +
3042
- "- To stop a background agent spawned with a name, pass that name as task_id\n" +
3043
- "- Returns a success or failure status\n" +
3044
- "- Use this tool when you need to terminate a long-running task",
3045
- parameters: Type.Object({
3046
- task_id: Type.Optional(Type.String({ description: "The ID of the background task to stop" })),
3047
- shell_id: Type.Optional(Type.String({ description: "Deprecated: use task_id instead" })),
3048
- }),
1233
+ contract: TASK_STOP_CONTRACT,
1234
+ description: composeTaskStopDescription(REGISTRY_TASK_TOOL_CAPS),
1235
+ parameters: TASK_STOP_PARAMS,
3049
1236
  effect: "write",
3050
1237
  execute: async (rawArgs, ctx) => {
3051
1238
  const args = rawArgs;
3052
- const id = firstString(args.task_id, args.shell_id, args.runId);
1239
+ const id = resolveTaskIdArg(args.task_id, args.shell_id, args.runId);
3053
1240
  if (!id)
3054
- return errorResult("Missing required parameter: task_id");
1241
+ return errorResult(TASK_STOP_MISSING_ID_MESSAGE);
3055
1242
  const r = await opts.registry.stopTask(id, { owner: ctx.taskId ?? opts.owner, scope: ctx.principal ?? opts.scope, ...((ctx.sessionId ?? opts.sessionId) !== undefined ? { sessionId: ctx.sessionId ?? opts.sessionId } : {}) }, { workflowStore: opts.workflowStore, agentStore: opts.agentStore });
3056
1243
  const { type: taskType, ...rest } = r.details;
3057
1244
  return {