@sema-agent/core 1.451.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 (191) 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/observer.js +3 -0
  5. package/dist/agents/repair-loop.d.ts +1 -1
  6. package/dist/agents/repair-loop.js +1 -1
  7. package/dist/agents/retain-ledger.d.ts +60 -0
  8. package/dist/agents/retain-ledger.js +225 -0
  9. package/dist/agents/roster-store.d.ts +1 -0
  10. package/dist/agents/roster-store.js +9 -0
  11. package/dist/agents/send-message-tool.d.ts +41 -0
  12. package/dist/agents/send-message-tool.js +469 -0
  13. package/dist/agents/subagent.d.ts +33 -108
  14. package/dist/agents/subagent.js +40 -813
  15. package/dist/agents/teacher.d.ts +0 -1
  16. package/dist/agents/teacher.js +0 -9
  17. package/dist/bin/sema-tb.js +3 -3
  18. package/dist/brain/anthropic.js +15 -40
  19. package/dist/brain/openai.js +15 -40
  20. package/dist/brain/reasoning.d.ts +1 -1
  21. package/dist/brain/stream-engine.js +2 -2
  22. package/dist/brain/stream-shared.d.ts +17 -0
  23. package/dist/brain/stream-shared.js +50 -0
  24. package/dist/brain/timeout.d.ts +1 -1
  25. package/dist/brain/timeout.js +1 -1
  26. package/dist/config/catalog.js +1 -3
  27. package/dist/config/defaults.d.ts +6 -0
  28. package/dist/config/defaults.js +6 -0
  29. package/dist/core/ask-question.d.ts +1 -1
  30. package/dist/core/ask-question.js +5 -5
  31. package/dist/core/background-agent-store.d.ts +2 -0
  32. package/dist/core/background-agent-store.js +5 -1
  33. package/dist/core/background-shell.d.ts +1 -1
  34. package/dist/core/checkpoint-store.d.ts +0 -1
  35. package/dist/core/exec-gate.d.ts +1 -1
  36. package/dist/core/exec-output-tail.d.ts +19 -1
  37. package/dist/core/exec-output-tail.js +44 -5
  38. package/dist/core/file-snapshot-store.d.ts +1 -1
  39. package/dist/core/fs-write-gate-policy.d.ts +1 -1
  40. package/dist/core/git-worktree-env.d.ts +1 -1
  41. package/dist/core/hooks.d.ts +1 -1
  42. package/dist/core/image-downsample.d.ts +44 -0
  43. package/dist/core/image-downsample.js +97 -0
  44. package/dist/core/lsp-session.d.ts +1 -0
  45. package/dist/core/lsp-session.js +24 -6
  46. package/dist/core/lsp.d.ts +11 -1
  47. package/dist/core/lsp.js +63 -6
  48. package/dist/core/mailbox-store.d.ts +3 -2
  49. package/dist/core/mailbox-store.js +19 -4
  50. package/dist/core/mcp.d.ts +3 -45
  51. package/dist/core/mcp.js +2 -97
  52. package/dist/core/media-byte-cap.d.ts +1 -1
  53. package/dist/core/memory-recall.d.ts +0 -12
  54. package/dist/core/memory-recall.js +0 -222
  55. package/dist/core/memory.d.ts +0 -6
  56. package/dist/core/memory.js +0 -88
  57. package/dist/core/message-utils.d.ts +1 -1
  58. package/dist/core/oracle-isolation.d.ts +1 -1
  59. package/dist/core/remote-env.d.ts +1 -1
  60. package/dist/core/roles.d.ts +1 -1
  61. package/dist/core/runner/active-skill-scope.d.ts +1 -1
  62. package/dist/core/runner/assemble-result.d.ts +0 -2
  63. package/dist/core/runner/memory-consolidation.d.ts +1 -1
  64. package/dist/core/runner/prepare-memory.d.ts +16 -0
  65. package/dist/core/runner/prepare-memory.js +191 -0
  66. package/dist/core/runner/prepare-task.d.ts +4 -28
  67. package/dist/core/runner/prepare-task.js +29 -208
  68. package/dist/core/runner/prompt-suggestions.d.ts +1 -1
  69. package/dist/core/runner/runtask.d.ts +2 -3
  70. package/dist/core/runner/runtask.js +337 -529
  71. package/dist/core/runner/session-rule-policy.d.ts +1 -1
  72. package/dist/core/runner/synthetic-tools.d.ts +5 -5
  73. package/dist/core/runner/synthetic-tools.js +4 -4
  74. package/dist/core/runner/tool-disclosure.d.ts +3 -3
  75. package/dist/core/runner/tool-disclosure.js +2 -2
  76. package/dist/core/runner/tool-output-projection.d.ts +6 -0
  77. package/dist/core/runner/tool-output-projection.js +81 -0
  78. package/dist/core/runtime.d.ts +1 -1
  79. package/dist/core/scheduler.d.ts +1 -1
  80. package/dist/core/sensitive-path-policy.d.ts +1 -1
  81. package/dist/core/session-reconcile.d.ts +1 -1
  82. package/dist/core/session-reconcile.js +5 -2
  83. package/dist/core/session-store.d.ts +2 -1
  84. package/dist/core/session-store.js +2 -1
  85. package/dist/core/session.d.ts +0 -1
  86. package/dist/core/task-notification.d.ts +1 -0
  87. package/dist/core/task-registry-agent.d.ts +115 -0
  88. package/dist/core/task-registry-agent.js +1163 -0
  89. package/dist/core/task-registry-monitor.d.ts +13 -0
  90. package/dist/core/task-registry-monitor.js +347 -0
  91. package/dist/core/task-registry-shared.d.ts +294 -0
  92. package/dist/core/task-registry-shared.js +271 -0
  93. package/dist/core/task-registry-workflow.d.ts +5 -0
  94. package/dist/core/task-registry-workflow.js +143 -0
  95. package/dist/core/task-registry.d.ts +54 -213
  96. package/dist/core/task-registry.js +190 -1766
  97. package/dist/core/task-tool-shape.d.ts +32 -0
  98. package/dist/core/task-tool-shape.js +83 -0
  99. package/dist/core/tool-policy.d.ts +1 -2
  100. package/dist/core/tool-result-budget.d.ts +1 -1
  101. package/dist/core/tool-result-store.d.ts +2 -2
  102. package/dist/core/tool-result-store.js +3 -3
  103. package/dist/core/tools.d.ts +7 -3
  104. package/dist/core/tools.js +3 -8
  105. package/dist/core/trace.d.ts +2 -2
  106. package/dist/core/types.d.ts +1 -29
  107. package/dist/core/workflow-journal-store.d.ts +2 -0
  108. package/dist/core/workflow-journal-store.js +14 -0
  109. package/dist/core/workflow-run-store-contract.d.ts +1 -1
  110. package/dist/core/workflow-run-store-contract.js +19 -19
  111. package/dist/engine/execution-env/node-execution-env.d.ts +14 -0
  112. package/dist/engine/execution-env/node-execution-env.js +184 -44
  113. package/dist/engine/lsp/frame-decoder.d.ts +1 -1
  114. package/dist/engine/lsp/frame-decoder.js +1 -1
  115. package/dist/engine/lsp/node-lsp-manager.d.ts +4 -2
  116. package/dist/engine/lsp/node-lsp-manager.js +22 -5
  117. package/dist/engine/lsp/stdio-lsp-transport.d.ts +1 -1
  118. package/dist/engine/lsp/stdio-lsp-transport.js +19 -8
  119. package/dist/index.d.ts +18 -16
  120. package/dist/index.js +15 -13
  121. package/dist/internal/harness-types.d.ts +7 -0
  122. package/dist/internal/harness-types.js +1 -0
  123. package/dist/internal/harness.d.ts +1 -7
  124. package/dist/internal/harness.js +1 -0
  125. package/dist/orchestration/goal.js +2 -2
  126. package/dist/orchestration/run-workflow-tool.d.ts +2 -1
  127. package/dist/orchestration/run-workflow-tool.js +29 -6
  128. package/dist/orchestration/workflow-types.d.ts +193 -0
  129. package/dist/orchestration/workflow-types.js +54 -0
  130. package/dist/orchestration/workflow.d.ts +3 -184
  131. package/dist/orchestration/workflow.js +87 -64
  132. package/dist/prompt-assembly/assemble.d.ts +3 -3
  133. package/dist/prompt-assembly/assemble.js +0 -16
  134. package/dist/prompt-assembly/explain.d.ts +1 -1
  135. package/dist/prompt-assembly/tool-catalog.d.ts +1 -1
  136. package/dist/prompts/default.d.ts +1 -6
  137. package/dist/prompts/default.js +3 -11
  138. package/dist/scenarios/env.d.ts +1 -1
  139. package/dist/scenarios/full-body.d.ts +0 -3
  140. package/dist/scenarios/full-body.js +0 -2
  141. package/dist/stores/cc/mailbox-store.js +6 -1
  142. package/dist/stores/file/background-agent-store.d.ts +2 -15
  143. package/dist/stores/file/background-agent-store.js +33 -102
  144. package/dist/stores/file/checkpoint-store.d.ts +1 -14
  145. package/dist/stores/file/checkpoint-store.js +47 -119
  146. package/dist/stores/file/file-snapshot-store.d.ts +1 -1
  147. package/dist/stores/file/mailbox-store.d.ts +1 -1
  148. package/dist/stores/file/mailbox-store.js +9 -7
  149. package/dist/stores/file/shared-ledger.d.ts +35 -0
  150. package/dist/stores/file/shared-ledger.js +94 -0
  151. package/dist/stores/file/workflow-run-store.d.ts +1 -14
  152. package/dist/stores/file/workflow-run-store.js +24 -93
  153. package/dist/tools/fs/bash-readonly-classifier.d.ts +8 -0
  154. package/dist/tools/fs/bash-readonly-classifier.js +166 -0
  155. package/dist/tools/fs/encoding.d.ts +5 -0
  156. package/dist/tools/fs/encoding.js +6 -0
  157. package/dist/tools/fs/fs-bash.d.ts +31 -0
  158. package/dist/tools/fs/fs-bash.js +672 -0
  159. package/dist/tools/fs/fs-pdf.d.ts +22 -0
  160. package/dist/tools/fs/fs-pdf.js +236 -0
  161. package/dist/tools/fs/fs-read.d.ts +8 -0
  162. package/dist/tools/fs/fs-read.js +286 -0
  163. package/dist/tools/fs/fs-search-tools.d.ts +5 -0
  164. package/dist/tools/fs/fs-search-tools.js +173 -0
  165. package/dist/tools/fs/fs-shared.d.ts +55 -0
  166. package/dist/tools/fs/fs-shared.js +144 -0
  167. package/dist/tools/fs/fs-write.d.ts +8 -0
  168. package/dist/tools/fs/fs-write.js +423 -0
  169. package/dist/tools/fs/index.d.ts +12 -65
  170. package/dist/tools/fs/index.js +26 -2026
  171. package/dist/tools/fs/notebook.d.ts +43 -0
  172. package/dist/tools/fs/notebook.js +141 -0
  173. package/dist/tools/fs/pdf.d.ts +1 -1
  174. package/dist/tools/fs/repo-map.d.ts +3 -3
  175. package/dist/tools/fs/repo-map.js +3 -3
  176. package/dist/tools/fs/safety.d.ts +1 -1
  177. package/dist/tools/fs/search.d.ts +1 -1
  178. package/dist/tools/fs/search.js +141 -12
  179. package/dist/tools/gitea-issue.js +4 -2
  180. package/dist/tools/monitor.d.ts +1 -1
  181. package/dist/tools/monitor.js +12 -8
  182. package/dist/tools/scheduler-tools.d.ts +1 -1
  183. package/dist/tools/scheduler-tools.js +16 -16
  184. package/dist/tools/task-list.js +34 -12
  185. package/dist/tools/web.d.ts +4 -2
  186. package/dist/tools/web.js +106 -20
  187. package/dist/tools/worktree.d.ts +1 -1
  188. package/dist/tools/worktree.js +14 -14
  189. package/package.json +1 -1
  190. package/dist/core/dynamic-recall.d.ts +0 -32
  191. package/dist/core/dynamic-recall.js +0 -76
@@ -1,129 +1,36 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { Type } from "typebox";
3
- import { defineTool } from "./tools.js";
2
+ import { defineTool, errorResult } from "./tools.js";
4
3
  import { hasBackgroundShell } from "./background-shell.js";
5
4
  import { shutdownDebug } from "./shutdown-debug.js";
6
- import { summarizeWorkflowRun } from "./workflow-run-store.js";
7
- import { canAccessAgentRecord, BackgroundAgentStoreError, } from "./background-agent-store.js";
5
+ import { canAccessAgentRecord, } from "./background-agent-store.js";
8
6
  import { delimitUntrusted } from "./untrusted-text.js";
9
- import { boundedRedactedSummary } from "./untrusted-egress.js";
10
- import { clipWithFilePointer } from "./tool-errors.js";
11
- const defaultMonitorTimers = {
12
- setInterval: (fn, ms) => {
13
- const h = setInterval(() => void fn(), ms);
14
- h.unref?.();
15
- return h;
16
- },
17
- clearInterval: (h) => clearInterval(h),
18
- now: () => Date.now(),
19
- };
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 };
20
14
  const BLOCK_DEFAULT_TIMEOUT_MS = 30_000;
21
15
  const BLOCK_MAX_TIMEOUT_MS = 600_000;
22
- const BLOCK_POLL_INTERVAL_MS = 400;
23
16
  function blockDeadline(timeoutMs, now) {
24
17
  const t = Math.min(BLOCK_MAX_TIMEOUT_MS, Math.max(1, Math.floor(timeoutMs ?? BLOCK_DEFAULT_TIMEOUT_MS)));
25
18
  return now + t;
26
19
  }
27
- const sleepPollStep = (deadline, signal) => sleepMs(deadline === undefined ? BLOCK_POLL_INTERVAL_MS : Math.max(1, Math.min(BLOCK_POLL_INTERVAL_MS, deadline - Date.now())), signal);
28
- const sleepMs = (ms, signal) => new Promise((resolve) => {
29
- const t = setTimeout(done, ms);
30
- function done() {
31
- clearTimeout(t);
32
- signal?.removeEventListener("abort", done);
33
- resolve();
34
- }
35
- signal?.addEventListener("abort", done, { once: true });
36
- });
37
20
  const BLOCK_WAIT_WRITEOUT_RESERVE_MS = 5_000;
38
- export const DURABLE_AGENT_HEARTBEAT_MS = 60_000;
39
- export const DURABLE_AGENT_HANDLE_RE = /^a[0-9a-f]{6,32}$/;
40
21
  const TASK_PREFIX = {
41
22
  background_bash: "b",
42
23
  workflow: "w",
43
24
  background_agent: "a",
44
25
  monitor: "m",
45
26
  };
46
- export const BG_AGENT_REAP_STOP_ERROR = "stopped (parent task ended or TaskStop)";
47
- export const MONITOR_BATCH_WINDOW_MS = 200;
48
- export const MONITOR_DEFAULT_TIMEOUT_MS = 300_000;
49
- export const MONITOR_MAX_TIMEOUT_MS = 3_600_000;
50
- export const MONITOR_MAX_BATCHES_PER_MINUTE = 50;
51
- const MONITOR_EVENT_RESULT_CAP = 2_000;
52
- const TASK_OUTPUT_DEFAULT_CHARS = 32_000;
53
- const TASK_OUTPUT_MAX_CHARS = 160_000;
54
- const TASK_OUTPUT_ALIASES = ["BashOutput", "AgentOutputTool", "BashOutputTool", "AgentOutput", "WorkflowStatus"];
55
- const TASK_STOP_ALIASES = ["KillShell", "KillBash"];
56
- const TASK_OUTPUT_MIN_CHARS = 512;
57
- function taskMaxOutputChars() {
58
- const raw = process.env.TASK_MAX_OUTPUT_LENGTH;
59
- if (!raw)
60
- return TASK_OUTPUT_DEFAULT_CHARS;
61
- if (!/^\d+$/.test(raw.trim()))
62
- return TASK_OUTPUT_DEFAULT_CHARS;
63
- const n = parseInt(raw.trim(), 10);
64
- if (!Number.isFinite(n) || n <= 0)
65
- return TASK_OUTPUT_DEFAULT_CHARS;
66
- return Math.min(Math.max(n, TASK_OUTPUT_MIN_CHARS), TASK_OUTPUT_MAX_CHARS);
67
- }
68
- export function clipTaskOutput(s, fullOutputPath) {
69
- return clipWithFilePointer(s, taskMaxOutputChars(), fullOutputPath);
70
- }
71
- function firstString(...values) {
72
- for (const value of values) {
73
- if (typeof value === "string" && value.trim() !== "")
74
- return value.trim();
75
- }
76
- return undefined;
77
- }
78
27
  function noTask(taskId) {
79
28
  return {
80
29
  content: JSON.stringify({ error: "task not found" }),
81
30
  details: { task_id: taskId, type: "unknown", status: "not_found", retrieval_status: "not_ready", error: "not_found" },
31
+ isError: true,
82
32
  };
83
33
  }
84
- export function normalizeAgentName(s) {
85
- return s
86
- .normalize("NFKC")
87
- .replace(/[\p{Cc}\p{Cf}]/gu, (t) => (/\s/.test(t) ? t : ""))
88
- .trim()
89
- .toLowerCase()
90
- .replace(/\s+/g, "-");
91
- }
92
- function editDistance(a, b) {
93
- if (a === b)
94
- return 0;
95
- const m = a.length;
96
- const n = b.length;
97
- const d = Array.from({ length: m + 1 }, (_, i) => Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)));
98
- for (let i = 1; i <= m; i++) {
99
- for (let j = 1; j <= n; j++) {
100
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
101
- d[i][j] = Math.min(d[i - 1][j] + 1, d[i][j - 1] + 1, d[i - 1][j - 1] + cost);
102
- if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
103
- d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
104
- }
105
- }
106
- }
107
- return d[m][n];
108
- }
109
- function closestName(query, candidates) {
110
- let best;
111
- let bestDist = 3;
112
- const seen = new Set();
113
- for (const c of candidates) {
114
- if (seen.has(c))
115
- continue;
116
- seen.add(c);
117
- if (Math.abs(c.length - query.length) > 2)
118
- continue;
119
- const dist = editDistance(query, c);
120
- if (dist <= 2 && dist < bestDist) {
121
- bestDist = dist;
122
- best = c;
123
- }
124
- }
125
- return best;
126
- }
127
34
  function noTaskForStop(taskId, enrich) {
128
35
  let content = `No task found with ID: ${taskId}`;
129
36
  if (enrich?.suggestion !== undefined)
@@ -133,6 +40,7 @@ function noTaskForStop(taskId, enrich) {
133
40
  return {
134
41
  content,
135
42
  details: { task_id: taskId, type: "unknown", status: "not_found", retrieval_status: "not_ready", error: "not_found" },
43
+ isError: true,
136
44
  };
137
45
  }
138
46
  function bashTerminalResult(spool) {
@@ -140,24 +48,6 @@ function bashTerminalResult(spool) {
140
48
  const err = spool.stderr.slice(-2_000);
141
49
  return err.length > 0 ? `${out}${out.length > 0 ? "\n" : ""}--- stderr (tail) ---\n${err}` : out;
142
50
  }
143
- function statusFromBackground(status, exitCode) {
144
- if (status === "running")
145
- return "running";
146
- if (status === "killed")
147
- return "killed";
148
- if (status === "failed")
149
- return "failed";
150
- if (status === "exited")
151
- return exitCode === 0 ? "completed" : "failed";
152
- return "failed";
153
- }
154
- function rollSpoolText(spool, s, cap) {
155
- if (s.length <= cap)
156
- return s;
157
- const half = Math.floor(cap / 2);
158
- spool.rolledChars += s.length - 2 * half;
159
- return s.slice(0, half) + s.slice(s.length - half);
160
- }
161
51
  function bashMirrorGapNote(handle) {
162
52
  return handle.outputFile !== undefined && handle.spool?.mirrorFailed === true
163
53
  ? " [!] The output file is INCOMPLETE (one or more writes to it failed) — trust the result text / TaskOutput over the file."
@@ -166,362 +56,112 @@ function bashMirrorGapNote(handle) {
166
56
  function monitorInTimeoutWindow(handle) {
167
57
  return handle.status === "running" && handle.deadlineAt !== undefined && handle.timers.now() < handle.deadlineAt;
168
58
  }
169
- function canAccess(handle, access) {
170
- if (handle.scope === undefined || access.scope === undefined || access.scope !== handle.scope)
171
- return false;
172
- if (handle.owner === undefined)
173
- return false;
174
- if (access.owner === handle.owner)
175
- return true;
176
- if (handle.sessionScoped && access.sessionId !== undefined && access.sessionId === handle.owner)
177
- return true;
178
- if (handle.type === "workflow" && access.sessionId !== undefined && handle.originatingSessionId === access.sessionId)
179
- return true;
180
- return false;
181
- }
182
- function assertOwnership(input, site) {
183
- if (input.owner === undefined || input.scope === undefined) {
184
- 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)`);
185
- }
186
- }
187
- export function canAccessWorkflowRun(run, access) {
188
- if (access.scope === undefined || run.scope !== access.scope)
189
- return false;
190
- if (access.owner !== undefined && run.sourceTaskId !== undefined && run.sourceTaskId === access.owner)
191
- return true;
192
- if (access.sessionId !== undefined && run.originatingSessionId !== undefined && run.originatingSessionId === access.sessionId)
193
- return true;
194
- return false;
195
- }
196
- const WORKFLOW_PARTIAL_RESULTS_MAX = 5;
197
- const WORKFLOW_AGENT_ROWS_MAX = 60;
198
- function workflowAgentRow(a, ordinal) {
199
- return {
200
- ordinal,
201
- label: a.label,
202
- ...(a.phase !== undefined ? { phase: a.phase } : {}),
203
- status: a.status,
204
- ...(a.errorCode !== undefined ? { errorCode: a.errorCode } : {}),
205
- ...(a.errorMessage !== undefined
206
- ? { error: delimitUntrusted(`workflow agent ${a.label} error`, boundedRedactedSummary(a.errorMessage, 200)) }
207
- : {}),
208
- ...(a.startedAt !== undefined && a.endedAt !== undefined ? { elapsedMs: a.endedAt - a.startedAt } : {}),
209
- ...(a.replayed === true ? { replayed: true } : {}),
210
- };
211
- }
212
- function formatWorkflowRun(run) {
213
- const summary = summarizeWorkflowRun(run);
214
- const done = run.agents.filter((a) => a.status === "completed" || a.status === "failed").length;
215
- const failed = run.agents.filter((a) => a.status === "failed").length;
216
- const allRows = run.agents.map((a, i) => ({ a, i }));
217
- const overCap = allRows.length > WORKFLOW_AGENT_ROWS_MAX;
218
- const keptRows = overCap
219
- ? (() => {
220
- const urgent = allRows.filter((r) => r.a.status !== "completed");
221
- const budget = Math.max(0, WORKFLOW_AGENT_ROWS_MAX - urgent.length);
222
- const fill = allRows.filter((r) => r.a.status === "completed").slice(0, budget);
223
- return [...urgent, ...fill].sort((x, y) => x.i - y.i);
224
- })()
225
- : allRows;
226
- const agentRows = keptRows.map((r) => workflowAgentRow(r.a, r.i));
227
- const finishedAgents = run.status === "failed" ? run.agents.filter((a) => a.status === "completed" && a.output !== undefined && a.output !== "") : [];
228
- return {
229
- content: JSON.stringify({
230
- task_id: run.id,
231
- runId: run.id,
232
- type: "workflow",
233
- status: run.status,
234
- ...(summary.name ? { name: summary.name } : {}),
235
- ...(summary.currentPhase ? { currentPhase: summary.currentPhase } : {}),
236
- agents: { done, total: run.agents.length, failed },
237
- agent_runs: agentRows,
238
- ...(overCap ? { agent_runs_note: `showing ${agentRows.length} of ${run.agents.length} agent rows (every non-completed row kept; completed rows trimmed).` } : {}),
239
- startedAt: run.startedAt,
240
- ...(run.endedAt !== undefined ? { endedAt: run.endedAt, elapsedMs: run.endedAt - run.startedAt } : {}),
241
- ...(run.status === "failed" && run.error ? { error: delimitUntrusted("workflow error", boundedRedactedSummary(run.error, 500)) } : {}),
242
- ...(run.status === "completed" && run.result !== undefined
243
- ? { result: delimitUntrusted("workflow result", run.resultFull ?? run.result) }
244
- : {}),
245
- ...(finishedAgents.length > 0
246
- ? {
247
- partial_results: finishedAgents.slice(0, WORKFLOW_PARTIAL_RESULTS_MAX).map((a) => ({
248
- agent: a.label,
249
- output: delimitUntrusted(`workflow agent ${a.label} output`, a.output),
250
- })),
251
- ...(finishedAgents.length > WORKFLOW_PARTIAL_RESULTS_MAX
252
- ? { 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).` }
253
- : {}),
254
- }
255
- : {}),
256
- note: run.status === "running"
257
- ? "still running — poll again shortly."
258
- : run.status === "completed"
259
- ? run.result !== undefined
260
- ? "completed — `result` above is the workflow's return value."
261
- : "completed — this run predates result persistence; its result was delivered on the completion notification."
262
- : finishedAgents.length > 0
263
- ? `failed — see error. ${finishedAgents.length} agent(s) had already completed before the run ended; their outputs are inlined under \`partial_results\`.`
264
- : "failed — see error.",
265
- }),
266
- details: {
267
- task_id: run.id,
268
- type: "workflow",
269
- status: run.status,
270
- retrieval_status: "success",
271
- details: summary,
272
- },
273
- };
274
- }
275
59
  export class TaskRegistry {
276
60
  handles = new Map();
277
61
  sessionReapHooks = new Set();
278
62
  bgQuiescenceWatchers = new Map();
279
63
  legacyToTaskId = new Map();
280
- writerId = randomBytes(8).toString("hex");
281
- reapingHandles = new Set();
282
- claimingHandles = new Set();
283
- durableHeartbeatTimer;
284
- ensureDurableHeartbeat() {
285
- if (this.durableHeartbeatTimer !== undefined)
286
- return;
287
- const t = setInterval(() => {
288
- let liveLanes = 0;
289
- for (const h of this.handles.values()) {
290
- if (h.type !== "background_agent")
291
- continue;
292
- const bh = h;
293
- if (!bh.durable || bh.durable.poisoned)
294
- continue;
295
- if (bh.status === "running") {
296
- liveLanes++;
297
- this.durableAgentWrite(bh, {});
298
- }
299
- else if (bh.durable.flushFailed) {
300
- if ((bh.durable.redrives = (bh.durable.redrives ?? 0) + 1) > 60) {
301
- bh.durable.poisoned = true;
302
- 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`);
303
- continue;
304
- }
305
- liveLanes++;
306
- this.durableAgentWrite(bh, {});
307
- }
308
- }
309
- if (liveLanes === 0) {
310
- clearInterval(t);
311
- this.durableHeartbeatTimer = undefined;
312
- }
313
- }, DURABLE_AGENT_HEARTBEAT_MS);
314
- t.unref?.();
315
- this.durableHeartbeatTimer = t;
316
- }
317
- durableAgentWrite(handle, patch, clear) {
318
- const lane = handle.durable;
319
- if (!lane || lane.poisoned)
320
- return;
321
- lane.chain = lane.chain.then(async () => {
322
- if (lane.poisoned)
323
- return;
324
- Object.assign(lane.record, patch);
325
- if (clear !== undefined)
326
- for (const k of clear)
327
- delete lane.record[k];
328
- lane.record.updatedAt = Date.now();
329
- const poison = (why) => {
330
- lane.poisoned = true;
331
- process.emitWarning(`sema durable-agents: ${why} for ${lane.record.handle} — durable writes for this row stop here`);
332
- };
333
- const adoptOwnRow = async () => {
334
- try {
335
- const live = await lane.store.get(lane.record.handle, lane.record.scope);
336
- if (!live)
337
- return "gone";
338
- if (live.writerId !== lane.record.writerId)
339
- return "foreign";
340
- if (live.writerEpoch !== lane.record.writerEpoch)
341
- return "foreign";
342
- lane.record.rev = live.rev;
343
- lane.written = true;
344
- return "adopted";
345
- }
346
- catch {
347
- return "unreadable";
348
- }
349
- };
350
- for (let attempt = 0; attempt < 3; attempt++) {
351
- if (lane.poisoned)
352
- return;
353
- if (attempt > 0) {
354
- await new Promise((r) => {
355
- const t = setTimeout(r, 50 * attempt);
356
- t.unref?.();
357
- });
358
- }
359
- try {
360
- if (!lane.written) {
361
- await lane.store.put(structuredClone(lane.record));
362
- lane.written = true;
363
- lane.flushFailed = false;
364
- lane.redrives = 0;
365
- return;
366
- }
367
- const ok = await lane.store.update(lane.record.handle, lane.record.scope, structuredClone(lane.record), {
368
- rev: lane.record.rev,
369
- });
370
- if (ok) {
371
- lane.record.rev += 1;
372
- lane.flushFailed = false;
373
- lane.redrives = 0;
374
- return;
375
- }
376
- const verdict = await adoptOwnRow();
377
- if (lane.poisoned)
378
- return;
379
- if (verdict === "adopted")
380
- continue;
381
- if (verdict === "unreadable")
382
- continue;
383
- poison(verdict === "gone" ? "row reaped mid-write" : "CAS lost to a foreign writer");
384
- return;
385
- }
386
- catch (e) {
387
- if (e instanceof BackgroundAgentStoreError && e.code === "agent_record.already_exists") {
388
- const verdict = await adoptOwnRow();
389
- if (lane.poisoned)
390
- return;
391
- if (verdict === "adopted")
392
- continue;
393
- if (verdict === "unreadable")
394
- continue;
395
- if (verdict === "gone")
396
- continue;
397
- poison("handle collision (foreign durable row left untouched)");
398
- return;
399
- }
400
- shutdownDebug(`durable-agents write failed for ${lane.record.handle} (attempt ${attempt + 1}/3)`, { error: String(e) });
401
- }
402
- }
403
- lane.flushFailed = true;
404
- });
405
- }
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
+ })();
406
80
  durableAgentArmed(id) {
407
- const h = this.handles.get(id);
408
- if (!h || h.type !== "background_agent")
409
- return false;
410
- const lane = h.durable;
411
- return lane !== undefined && !lane.poisoned;
81
+ return durableAgentArmedLane(this.core, id);
412
82
  }
413
83
  durableAgentRowProbe(id) {
414
- const h = this.handles.get(id);
415
- if (!h || h.type !== "background_agent")
416
- return undefined;
417
- const lane = h.durable;
418
- if (!lane)
419
- return undefined;
420
- return async () => {
421
- await lane.chain.catch(() => undefined);
422
- return lane.written && !lane.poisoned && !lane.flushFailed;
423
- };
84
+ return durableAgentRowProbeLane(this.core, id);
424
85
  }
425
86
  beginDurableClaim(id) {
426
- const live = this.handles.get(id);
427
- if (live !== undefined && (live.status === "running" || live.status === "pending"))
428
- return false;
429
- if (this.reapingHandles.has(id) || this.claimingHandles.has(id))
430
- return false;
431
- this.claimingHandles.add(id);
432
- return true;
87
+ return beginDurableClaimLane(this.core, id);
433
88
  }
434
89
  endDurableClaim(id) {
435
- this.claimingHandles.delete(id);
90
+ return endDurableClaimLane(this.core, id);
436
91
  }
437
92
  async reapDurableAgents(scope, deps, policy) {
438
- const now = policy.now ?? Date.now();
439
- if (policy.staleRunningMaxAgeMs !== undefined) {
440
- await deps.store.reap(scope, now, { staleRunningMaxAgeMs: policy.staleRunningMaxAgeMs });
441
- }
442
- if (policy.maxAgeMs === undefined && policy.keep === undefined)
443
- return { rowsReaped: 0, sessionsReleased: 0, skippedNoSessions: 0 };
444
- const terminal = (await deps.store.listByScope(scope)).filter((r) => r.status !== "running" && r.status !== "parked");
445
- terminal.sort((a, b) => b.spawnedAt - a.spawnedAt);
446
- const doomed = new Map();
447
- if (policy.maxAgeMs !== undefined) {
448
- for (const r of terminal) {
449
- if ((r.settledAt ?? r.spawnedAt) < now - policy.maxAgeMs)
450
- doomed.set(r.handle, r);
451
- }
452
- }
453
- if (policy.keep !== undefined) {
454
- for (const r of terminal.slice(Math.max(0, policy.keep)))
455
- doomed.set(r.handle, r);
456
- }
457
- let rowsReaped = 0;
458
- let sessionsReleased = 0;
459
- let skippedNoSessions = 0;
460
- for (const r of doomed.values()) {
461
- if (this.claimingHandles.has(r.handle))
462
- continue;
463
- this.reapingHandles.add(r.handle);
464
- try {
465
- let live;
466
- try {
467
- live = await deps.store.get(r.handle, scope);
468
- }
469
- catch {
470
- continue;
471
- }
472
- if (!live || live.status === "running" || live.status === "parked")
473
- continue;
474
- const inProc = this.handles.get(r.handle);
475
- if (inProc !== undefined && (inProc.status === "running" || inProc.status === "pending" || inProc.status === "parked"))
476
- continue;
477
- if (live.sessionId !== undefined && deps.sessions === undefined) {
478
- skippedNoSessions++;
479
- continue;
480
- }
481
- try {
482
- if (!(await deps.store.delete(r.handle, scope, { rev: live.rev })))
483
- continue;
484
- }
485
- catch {
486
- continue;
487
- }
488
- rowsReaped++;
489
- if (deps.mailbox !== undefined) {
490
- try {
491
- await deps.mailbox.drop(scope, r.handle);
492
- }
493
- catch {
494
- }
495
- }
496
- if (live.sessionId !== undefined && deps.sessions !== undefined) {
497
- try {
498
- await deps.sessions.unpin?.(live.sessionId);
499
- await deps.sessions.release(live.sessionId);
500
- sessionsReleased++;
501
- }
502
- catch {
503
- }
504
- }
505
- }
506
- finally {
507
- this.reapingHandles.delete(r.handle);
508
- }
509
- }
510
- return { rowsReaped, sessionsReleased, skippedNoSessions };
93
+ return reapDurableAgentsLane(this.core, scope, deps, policy);
511
94
  }
512
95
  releaseDurableTranscriptAnchor(id) {
513
- const handle = this.handles.get(id);
514
- if (!handle || handle.type !== "background_agent")
515
- return;
516
- this.durableAgentWrite(handle, {}, ["sessionId"]);
96
+ return releaseDurableTranscriptAnchorLane(this.core, id);
517
97
  }
518
98
  bindBackgroundAgentSession(id, sessionId) {
519
- const handle = this.handles.get(id);
520
- if (!handle || handle.type !== "background_agent")
521
- return;
522
- handle.sessionId = sessionId;
523
- 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);
524
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);
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;
525
165
  mintTaskId(type) {
526
166
  const prefix = TASK_PREFIX[type];
527
167
  for (let i = 0; i < 32; i++) {
@@ -571,99 +211,6 @@ export class TaskRegistry {
571
211
  }
572
212
  return false;
573
213
  }
574
- registerBackgroundAgent(input) {
575
- assertOwnership(input, "registerBackgroundAgent");
576
- if (input.id !== undefined && !DURABLE_AGENT_HANDLE_RE.test(input.id)) {
577
- throw new Error(`TaskRegistry: background-agent id ${JSON.stringify(input.id)} does not match the a<hex> handle form`);
578
- }
579
- const id = input.id ?? this.mintTaskId("background_agent");
580
- if (input.durableSeed !== undefined) {
581
- const prior = this.handles.get(id);
582
- if (prior !== undefined) {
583
- const parkedHandoff = prior.status === "parked" && input.initialStatus === "parked";
584
- if (prior.type !== "background_agent" || prior.status === "running" || prior.status === "pending" || (prior.status === "parked" && !parkedHandoff)) {
585
- throw new Error(`TaskRegistry: task id already registered: ${id}`);
586
- }
587
- const priorLane = prior.durable;
588
- if (priorLane !== undefined)
589
- priorLane.poisoned = true;
590
- this.handles.delete(id);
591
- }
592
- }
593
- if (this.handles.has(id))
594
- throw new Error(`TaskRegistry: task id already registered: ${id}`);
595
- const now = input.now ?? Date.now();
596
- this.handles.set(id, {
597
- id,
598
- type: "background_agent",
599
- status: input.initialStatus ?? "running",
600
- ...(input.parkedCheckpointToken !== undefined ? { parkedCheckpointToken: input.parkedCheckpointToken } : {}),
601
- ...(input.resolveParkedStop !== undefined ? { resolveParkedStop: input.resolveParkedStop } : {}),
602
- ...(input.sessionScoped ? { sessionScoped: true } : {}),
603
- ...(input.description !== undefined ? { description: input.description } : {}),
604
- ...(input.name !== undefined ? { name: input.name } : {}),
605
- ...(input.agentType !== undefined ? { agentType: input.agentType } : {}),
606
- ...(input.parentTaskId !== undefined ? { parentTaskId: input.parentTaskId } : {}),
607
- ...(input.parentSessionId !== undefined ? { parentSessionId: input.parentSessionId } : {}),
608
- ...(input.rootSessionId !== undefined ? { rootSessionId: input.rootSessionId } : {}),
609
- ...(input.deliveryChannel !== undefined ? { channelState: input.deliveryChannel } : {}),
610
- ...(input.retainedContinuation !== undefined ? { retainedContinuation: input.retainedContinuation } : {}),
611
- ...(input.owner !== undefined ? { owner: input.owner } : {}),
612
- ...(input.scope !== undefined ? { scope: input.scope } : {}),
613
- ...(input.toolUseId !== undefined ? { toolUseId: input.toolUseId } : {}),
614
- cycleSeq: input.durableSeed !== undefined ? (input.durableSeed.record.seq ?? 1) : 1,
615
- createdAt: now,
616
- updatedAt: now,
617
- abort: input.abort,
618
- });
619
- if (input.store) {
620
- const handle = this.handles.get(id);
621
- if (input.durableSeed !== undefined) {
622
- const seeded = structuredClone(input.durableSeed.record);
623
- seeded.rev = input.durableSeed.rev;
624
- handle.durable = {
625
- store: input.store,
626
- record: seeded,
627
- chain: Promise.resolve(),
628
- written: true,
629
- poisoned: false,
630
- flushFailed: false,
631
- };
632
- this.ensureDurableHeartbeat();
633
- return id;
634
- }
635
- handle.durable = {
636
- store: input.store,
637
- record: {
638
- handle: id,
639
- scope: input.scope,
640
- owner: input.owner,
641
- sessionScoped: input.sessionScoped === true,
642
- ...(input.parentSessionId !== undefined ? { parentSessionId: input.parentSessionId } : {}),
643
- ...(input.rootSessionId !== undefined ? { rootSessionId: input.rootSessionId } : {}),
644
- ...(input.parentTaskId !== undefined ? { parentTaskId: input.parentTaskId } : {}),
645
- ...(input.name !== undefined ? { name: input.name } : {}),
646
- ...(input.agentType !== undefined ? { agentType: input.agentType } : {}),
647
- ...(input.description !== undefined ? { description: input.description } : {}),
648
- ...(input.model !== undefined ? { model: input.model } : {}),
649
- ...(input.teamName !== undefined ? { teamName: input.teamName } : {}),
650
- writerId: this.writerId,
651
- writerEpoch: 1,
652
- spawnedAt: now,
653
- updatedAt: now,
654
- status: "running",
655
- rev: 0,
656
- },
657
- chain: Promise.resolve(),
658
- written: false,
659
- poisoned: false,
660
- flushFailed: false,
661
- };
662
- this.durableAgentWrite(handle, {});
663
- this.ensureDurableHeartbeat();
664
- }
665
- return id;
666
- }
667
214
  getAccessibleTask(taskId, access) {
668
215
  const h = this.handles.get(taskId);
669
216
  if (!h || !canAccess(h, access))
@@ -762,388 +309,8 @@ export class TaskRegistry {
762
309
  return undefined;
763
310
  return handle.stoppedBy ?? handle.stopSource ?? "system";
764
311
  }
765
- parkBackgroundAgent(id, park) {
766
- const handle = this.handles.get(id);
767
- if (!handle || handle.type !== "background_agent")
768
- return undefined;
769
- if (handle.status !== "running")
770
- return undefined;
771
- handle.status = "parked";
772
- handle.parkedCheckpointToken = park.checkpointToken;
773
- handle.resolveParkedStop = park.resolveStop;
774
- handle.notify = undefined;
775
- for (const [, , qResolve] of handle.preAttachQueue ?? [])
776
- qResolve?.({ ok: false, reason: "not_running" });
777
- handle.preAttachQueue = undefined;
778
- for (const r of [...(handle.inFlightDirect ?? [])])
779
- r({ ok: false, reason: "not_running" });
780
- handle.inFlightDirect = undefined;
781
- handle.updatedAt = Date.now();
782
- if (park.seq !== undefined)
783
- handle.cycleSeq = park.seq;
784
- this.pokeBgQuiescence(handle.owner);
785
- this.durableAgentWrite(handle, {
786
- status: "parked",
787
- parkedCheckpointToken: park.checkpointToken,
788
- parkedAt: Date.now(),
789
- ...(park.seq !== undefined ? { seq: park.seq } : {}),
790
- }, ["parkClaimId"]);
791
- return "parked";
792
- }
793
- async reconcileParkedAgents(stores, scope, now, opts) {
794
- let liveFailed = 0;
795
- for (const handle of this.handles.values()) {
796
- if (handle.type !== "background_agent" || handle.status !== "parked")
797
- continue;
798
- if (handle.scope !== undefined && handle.scope !== scope)
799
- continue;
800
- const token = handle.parkedCheckpointToken;
801
- let state;
802
- if (token === undefined)
803
- state = "missing";
804
- else {
805
- try {
806
- const cp = await stores.checkpointStore.get(token);
807
- state = cp === null ? "missing" : cp.status;
808
- }
809
- catch {
810
- state = "unreadable";
811
- }
812
- }
813
- if (state !== "expired" && state !== "missing")
814
- continue;
815
- if (handle.status !== "parked")
816
- continue;
817
- handle.status = "failed";
818
- handle.error = `approval checkpoint ${state} while parked (reconciled)`;
819
- handle.parkedCheckpointToken = undefined;
820
- handle.resolveParkedStop = undefined;
821
- handle.updatedAt = now;
822
- this.pokeBgQuiescence(handle.owner);
823
- this.durableAgentWrite(handle, { status: "failed", error: handle.error, settledAt: now, stoppedBy: "system" }, ["parkedCheckpointToken", "parkClaimId", "parkedAt"]);
824
- liveFailed++;
825
- }
826
- const { reconcileParkedAgents: storeReconcile } = await import("./background-agent-store.js");
827
- const liveHandles = new Set();
828
- for (const h of this.handles.values())
829
- if (h.type === "background_agent")
830
- liveHandles.add(h.id);
831
- const storeHalf = await storeReconcile(stores, scope, now, {
832
- ...(opts?.staleClaimMaxAgeMs !== undefined ? { staleClaimMaxAgeMs: opts.staleClaimMaxAgeMs } : {}),
833
- excludeHandles: liveHandles,
834
- });
835
- return { failed: storeHalf.failed + liveFailed, rolledBack: storeHalf.rolledBack };
836
- }
837
- async claimParkedAgent(stores, handle, access) {
838
- if (access.scope === undefined)
839
- return { ok: false, reason: "not_found" };
840
- let row;
841
- try {
842
- row = await stores.agentStore.get(handle, access.scope);
843
- }
844
- catch {
845
- return { ok: false, reason: "store_unreachable" };
846
- }
847
- if (!row || !canAccessAgentRecord(row, access))
848
- return { ok: false, reason: "not_found" };
849
- if (row.status !== "parked")
850
- return { ok: false, reason: "not_parked" };
851
- const token = row.parkedCheckpointToken;
852
- if (token === undefined || row.sessionId === undefined)
853
- return { ok: false, reason: "binding_broken" };
854
- let cp;
855
- try {
856
- cp = (await stores.checkpointStore.get(token));
857
- }
858
- catch {
859
- return { ok: false, reason: "store_unreachable" };
860
- }
861
- if (cp === null || cp.sessionId !== row.sessionId)
862
- return { ok: false, reason: "binding_broken" };
863
- if (cp.status !== "pending")
864
- return { ok: false, reason: "checkpoint_not_pending" };
865
- const claimId = randomBytes(8).toString("hex");
866
- const reserved = structuredClone(row);
867
- reserved.parkClaimId = claimId;
868
- reserved.updatedAt = Date.now();
869
- let won;
870
- try {
871
- won = await stores.agentStore.updateIf(handle, access.scope, reserved, {
872
- rev: row.rev,
873
- status: "parked",
874
- parkedCheckpointToken: token,
875
- parkClaimId: null,
876
- });
877
- }
878
- catch {
879
- return { ok: false, reason: "store_unreachable" };
880
- }
881
- if (!won)
882
- return { ok: false, reason: "claim_lost" };
883
- return {
884
- ok: true,
885
- ticket: { handle, scope: access.scope, token, claimId, reservedRev: row.rev + 1, row: structuredClone(row) },
886
- };
887
- }
888
- async rollbackParkedClaim(stores, ticket) {
889
- let cpStatus;
890
- try {
891
- const cp = (await stores.checkpointStore.get(ticket.token));
892
- cpStatus = cp === null ? "missing" : cp.status;
893
- }
894
- catch {
895
- return "retry";
896
- }
897
- const now = Date.now();
898
- const settleLocalParkedHandle = (error) => {
899
- const h = this.handles.get(ticket.handle);
900
- if (h !== undefined && h.type === "background_agent" && h.status === "parked") {
901
- h.status = "failed";
902
- h.error = error;
903
- h.parkedCheckpointToken = undefined;
904
- h.resolveParkedStop = undefined;
905
- h.updatedAt = now;
906
- this.pokeBgQuiescence(h.owner);
907
- }
908
- };
909
- let liveRow;
910
- try {
911
- liveRow = await stores.agentStore.get(ticket.handle, ticket.scope);
912
- }
913
- catch {
914
- return "retry";
915
- }
916
- if (!liveRow || liveRow.parkClaimId !== ticket.claimId)
917
- return "lost";
918
- if (cpStatus === "pending") {
919
- const restored = structuredClone(liveRow);
920
- delete restored.parkClaimId;
921
- restored.updatedAt = now;
922
- restored.writerEpoch = (liveRow.writerEpoch ?? 0) + 1;
923
- const liveHandle = this.handles.get(ticket.handle);
924
- if (liveHandle?.type === "background_agent" &&
925
- liveHandle.durable !== undefined &&
926
- liveHandle.durable.record.writerId === liveRow.writerId &&
927
- (liveHandle.durable.record.writerEpoch ?? 0) === (liveRow.writerEpoch ?? 0)) {
928
- liveHandle.durable.record.writerEpoch = restored.writerEpoch;
929
- }
930
- try {
931
- const won = await stores.agentStore.updateIf(ticket.handle, ticket.scope, restored, {
932
- rev: liveRow.rev,
933
- status: "parked",
934
- parkedCheckpointToken: ticket.token,
935
- parkClaimId: ticket.claimId,
936
- });
937
- if (!won)
938
- return "lost";
939
- try {
940
- const cp2 = (await stores.checkpointStore.get(ticket.token));
941
- if (cp2 !== null && cp2.status === "pending")
942
- return "rolled_back";
943
- const post = await stores.agentStore.get(ticket.handle, ticket.scope);
944
- if (post && post.status === "parked") {
945
- const term = structuredClone(post);
946
- term.status = "failed";
947
- term.error = `approval checkpoint ${cp2 === null ? "missing" : cp2.status} under the resume claim (claim rollback)`;
948
- term.stoppedBy = "system";
949
- term.settledAt = now;
950
- term.updatedAt = now;
951
- delete term.parkedCheckpointToken;
952
- delete term.parkClaimId;
953
- delete term.parkedAt;
954
- if (await stores.agentStore.updateIf(ticket.handle, ticket.scope, term, { rev: post.rev, status: "parked" })) {
955
- settleLocalParkedHandle(term.error);
956
- return "failed";
957
- }
958
- }
959
- return "lost";
960
- }
961
- catch {
962
- return "rolled_back";
963
- }
964
- }
965
- catch {
966
- return "retry";
967
- }
968
- }
969
- const error = cpStatus === "resolved"
970
- ? "approval consumed but the resume could not proceed — outcome unknown (claim rollback)"
971
- : `approval checkpoint ${cpStatus} under the resume claim (claim rollback)`;
972
- if (liveRow.status !== "parked")
973
- return "lost";
974
- const failed = structuredClone(liveRow);
975
- failed.status = "failed";
976
- failed.error = error;
977
- failed.stoppedBy = "system";
978
- failed.settledAt = now;
979
- failed.updatedAt = now;
980
- delete failed.parkedCheckpointToken;
981
- delete failed.parkClaimId;
982
- delete failed.parkedAt;
983
- try {
984
- const won = await stores.agentStore.updateIf(ticket.handle, ticket.scope, failed, {
985
- rev: liveRow.rev,
986
- status: "parked",
987
- parkClaimId: ticket.claimId,
988
- });
989
- if (won)
990
- settleLocalParkedHandle(error);
991
- return won ? "failed" : "lost";
992
- }
993
- catch {
994
- return "retry";
995
- }
996
- }
997
- async consumeParkedFlip(id, stores, ticket) {
998
- const handle = this.handles.get(id);
999
- if (!handle || handle.type !== "background_agent" || handle.status !== "parked")
1000
- return false;
1001
- const lane = handle.durable;
1002
- const doFlip = async () => {
1003
- let live;
1004
- try {
1005
- live = await stores.agentStore.get(ticket.handle, ticket.scope);
1006
- }
1007
- catch {
1008
- return undefined;
1009
- }
1010
- if (!live || live.status !== "parked" || live.parkClaimId !== ticket.claimId || live.parkedCheckpointToken !== ticket.token)
1011
- return undefined;
1012
- const flipped = structuredClone(live);
1013
- flipped.status = "running";
1014
- flipped.writerId = this.writerId;
1015
- flipped.writerEpoch = (live.writerEpoch ?? 0) + 1;
1016
- flipped.updatedAt = Date.now();
1017
- flipped.seq = (live.seq ?? 1) + 1;
1018
- delete flipped.parkedCheckpointToken;
1019
- delete flipped.parkedAt;
1020
- try {
1021
- const won = await stores.agentStore.updateIf(ticket.handle, ticket.scope, flipped, {
1022
- rev: live.rev,
1023
- status: "parked",
1024
- parkedCheckpointToken: ticket.token,
1025
- parkClaimId: ticket.claimId,
1026
- });
1027
- if (!won)
1028
- return undefined;
1029
- }
1030
- catch {
1031
- return undefined;
1032
- }
1033
- flipped.rev = live.rev + 1;
1034
- return flipped;
1035
- };
1036
- let flipped;
1037
- if (lane !== undefined && !lane.poisoned) {
1038
- const p = lane.chain.then(doFlip, doFlip);
1039
- lane.chain = p.then((f) => {
1040
- if (f !== undefined) {
1041
- lane.record = structuredClone(f);
1042
- lane.written = true;
1043
- }
1044
- });
1045
- flipped = await p;
1046
- }
1047
- else {
1048
- flipped = await doFlip();
1049
- }
1050
- if (flipped === undefined)
1051
- return false;
1052
- handle.status = "running";
1053
- handle.parkedCheckpointToken = undefined;
1054
- handle.resolveParkedStop = undefined;
1055
- handle.updatedAt = Date.now();
1056
- if (flipped.seq !== undefined)
1057
- handle.cycleSeq = flipped.seq;
1058
- return true;
1059
- }
1060
- finalizeParkedResume(id) {
1061
- const handle = this.handles.get(id);
1062
- if (!handle || handle.type !== "background_agent")
1063
- return;
1064
- this.durableAgentWrite(handle, {}, ["parkClaimId"]);
1065
- }
1066
- settleBackgroundAgent(id, outcome) {
1067
- const handle = this.handles.get(id);
1068
- if (!handle || handle.type !== "background_agent")
1069
- return undefined;
1070
- if (handle.status !== "running") {
1071
- if (handle.status === "killed") {
1072
- let backfilled = false;
1073
- if (handle.result === undefined && outcome.result !== undefined && outcome.result !== "") {
1074
- handle.result = outcome.result;
1075
- handle.resultIsPartial = true;
1076
- backfilled = true;
1077
- if (outcome.resultFull !== undefined)
1078
- handle.resultFull = outcome.resultFull;
1079
- }
1080
- if (handle.error === undefined && outcome.error !== undefined && outcome.error !== BG_AGENT_REAP_STOP_ERROR) {
1081
- handle.error = outcome.error;
1082
- backfilled = true;
1083
- }
1084
- if (backfilled) {
1085
- handle.updatedAt = Date.now();
1086
- this.durableAgentWrite(handle, {
1087
- ...(handle.result !== undefined ? { finalOutput: handle.result } : {}),
1088
- ...(handle.resultFull !== undefined ? { finalOutputFull: handle.resultFull } : {}),
1089
- ...(handle.resultIsPartial ? { resultIsPartial: true } : {}),
1090
- ...(handle.error !== undefined ? { error: handle.error } : {}),
1091
- });
1092
- }
1093
- }
1094
- return handle.status === "completed" || handle.status === "failed" || handle.status === "killed" ? handle.status : undefined;
1095
- }
1096
- handle.status = outcome.status;
1097
- handle.notify = undefined;
1098
- for (const [, , qResolve] of handle.preAttachQueue ?? [])
1099
- qResolve?.({ ok: false, reason: "not_running" });
1100
- handle.preAttachQueue = undefined;
1101
- for (const r of [...(handle.inFlightDirect ?? [])])
1102
- r({ ok: false, reason: "not_running" });
1103
- handle.inFlightDirect = undefined;
1104
- if (outcome.status === "killed" && handle.stoppedBy === undefined) {
1105
- handle.stoppedBy = outcome.stoppedBy ?? handle.stopSource ?? "system";
1106
- }
1107
- if (outcome.result !== undefined)
1108
- handle.result = outcome.result;
1109
- if (outcome.resultFull !== undefined)
1110
- handle.resultFull = outcome.resultFull;
1111
- if (outcome.status === "killed" && outcome.result !== undefined && outcome.result !== "") {
1112
- handle.resultIsPartial = true;
1113
- }
1114
- if (outcome.error !== undefined && !(outcome.status === "killed" && outcome.error === BG_AGENT_REAP_STOP_ERROR)) {
1115
- handle.error = outcome.error;
1116
- }
1117
- handle.updatedAt = Date.now();
1118
- if (outcome.seq !== undefined)
1119
- handle.cycleSeq = outcome.seq;
1120
- this.pokeBgQuiescence(handle.owner);
1121
- this.durableAgentWrite(handle, {
1122
- status: outcome.status,
1123
- ...(outcome.seq !== undefined ? { seq: outcome.seq } : {}),
1124
- settledAt: Date.now(),
1125
- ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
1126
- ...(handle.result !== undefined ? { finalOutput: handle.result } : {}),
1127
- ...(handle.resultFull !== undefined ? { finalOutputFull: handle.resultFull } : {}),
1128
- ...(handle.resultIsPartial ? { resultIsPartial: true } : {}),
1129
- ...(handle.error !== undefined ? { error: handle.error } : {}),
1130
- }, ["parkedCheckpointToken", "parkClaimId", "parkedAt"]);
1131
- return outcome.status;
1132
- }
1133
- abortBackgroundAgentsForOwner(access, opts) {
1134
- let aborted = 0;
1135
- for (const handle of this.handles.values()) {
1136
- if (handle.type !== "background_agent" || handle.status !== "running" || !canAccess(handle, access))
1137
- continue;
1138
- if (opts?.skipSessionScoped && handle.sessionScoped)
1139
- continue;
1140
- if (opts?.sessionScopedOnly && !handle.sessionScoped)
1141
- continue;
1142
- this.markStopSource(handle.id, "parent");
1143
- handle.abort.abort();
1144
- aborted++;
1145
- }
1146
- return aborted;
312
+ getCompletionId(id) {
313
+ return this.handles.get(id)?.completionId;
1147
314
  }
1148
315
  markStopSourceForOwner(access, source, opts) {
1149
316
  let marked = 0;
@@ -1195,6 +362,7 @@ export class TaskRegistry {
1195
362
  handle.status = "killed";
1196
363
  handle.stoppedBy = handle.stopSource ?? source;
1197
364
  handle.updatedAt = Date.now();
365
+ mintCompletionId(handle);
1198
366
  if (handle.outputFile !== undefined) {
1199
367
  try {
1200
368
  const drained = await handle.env.pollBackground(handle.shellId);
@@ -1221,6 +389,7 @@ export class TaskRegistry {
1221
389
  stoppedBy: handle.stoppedBy,
1222
390
  summary: `${(handle.description ?? "background command").slice(0, 200)} — killed before completion (${clause(handle.stoppedBy)})${killFailed ? ` — kill attempt reported ${kill.ok ? "" : kill.error.code}: the process may still be terminating; the environment teardown retries` : ""}${bashMirrorGapNote(handle)}`,
1223
391
  ...(resultText.length > 0 ? { result: resultText, partial: true } : {}),
392
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
1224
393
  }));
1225
394
  }
1226
395
  else {
@@ -1235,6 +404,7 @@ export class TaskRegistry {
1235
404
  handle.status = "killed";
1236
405
  handle.stoppedBy = handle.stopSource ?? source;
1237
406
  handle.updatedAt = Date.now();
407
+ mintCompletionId(handle);
1238
408
  const lastLines = handle.lineBuf !== "" ? [handle.lineBuf] : [];
1239
409
  handle.lineBuf = "";
1240
410
  this.notifyTerminalOnce(handle, () => handle.onEvent?.({
@@ -1245,6 +415,7 @@ export class TaskRegistry {
1245
415
  stoppedBy: handle.stoppedBy,
1246
416
  summary: `${(handle.description ?? "monitor").slice(0, 200)} — killed before completion (${clause(handle.stoppedBy)}); watch ended.${killFailed ? ` Kill attempt reported ${kill.ok ? "" : kill.error.code}: the process may still be terminating; the environment teardown retries.` : ""}`,
1247
417
  ...(lastLines.length > 0 ? { lines: lastLines, result: lastLines.join("\n"), partial: true } : {}),
418
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
1248
419
  }));
1249
420
  }
1250
421
  settled++;
@@ -1270,6 +441,7 @@ export class TaskRegistry {
1270
441
  handle.status = "killed";
1271
442
  handle.stoppedBy = handle.stopSource ?? "system";
1272
443
  handle.updatedAt = Date.now();
444
+ mintCompletionId(handle);
1273
445
  reaped++;
1274
446
  continue;
1275
447
  }
@@ -1286,6 +458,7 @@ export class TaskRegistry {
1286
458
  handle.status = "killed";
1287
459
  handle.stoppedBy = handle.stopSource ?? "system";
1288
460
  handle.updatedAt = Date.now();
461
+ mintCompletionId(handle);
1289
462
  reaped++;
1290
463
  continue;
1291
464
  }
@@ -1442,6 +615,7 @@ export class TaskRegistry {
1442
615
  return;
1443
616
  handle.status = "failed";
1444
617
  handle.updatedAt = Date.now();
618
+ mintCompletionId(handle);
1445
619
  stop();
1446
620
  const lostResult = bashTerminalResult(handle.spool);
1447
621
  this.notifyTerminalOnce(handle, () => onTerminal({
@@ -1452,11 +626,13 @@ export class TaskRegistry {
1452
626
  status: "failed",
1453
627
  summary: `${(handle.description ?? "background command").slice(0, 200)} — lost its background process (${r.error.message}); no further output will arrive.${bashMirrorGapNote(handle)}`,
1454
628
  ...(lostResult.length > 0 ? { result: lostResult } : {}),
629
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
1455
630
  }));
1456
631
  return;
1457
632
  }
1458
633
  pollFailures = 0;
1459
634
  const spool = handle.spool;
635
+ accountDroppedBytes(spool, r.value);
1460
636
  spool.stdout = roll(spool.stdout + r.value.stdout);
1461
637
  spool.stderr = roll(spool.stderr + r.value.stderr);
1462
638
  if (handle.outputFile !== undefined && (r.value.stdout.length > 0 || r.value.stderr.length > 0)) {
@@ -1470,6 +646,7 @@ export class TaskRegistry {
1470
646
  }
1471
647
  handle.updatedAt = Date.now();
1472
648
  if (r.value.status !== "running") {
649
+ mintCompletionId(handle);
1473
650
  spool.exitCode = r.value.exitCode;
1474
651
  if (r.value.timedOut === true) {
1475
652
  spool.timedOut = true;
@@ -1496,9 +673,12 @@ export class TaskRegistry {
1496
673
  ? r.value.timeoutSec !== undefined
1497
674
  ? `${desc} — timed out after ${r.value.timeoutSec}s (its own background time budget); the process was killed`
1498
675
  : `${desc} — timed out (its own background time budget); the process was killed`
1499
- : `${desc} — ${r.value.status}`) + bashMirrorGapNote(handle),
676
+ : `${desc} — ${r.value.status}`) +
677
+ bashMirrorGapNote(handle) +
678
+ droppedGapNote(spool),
1500
679
  ...(resultText.length > 0 ? { result: resultText } : {}),
1501
680
  ...(handle.status === "killed" && resultText.length > 0 ? { partial: true } : {}),
681
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
1502
682
  }));
1503
683
  }
1504
684
  }
@@ -1509,6 +689,7 @@ export class TaskRegistry {
1509
689
  return;
1510
690
  handle.status = "failed";
1511
691
  handle.updatedAt = Date.now();
692
+ mintCompletionId(handle);
1512
693
  stop();
1513
694
  const crashedResult = bashTerminalResult(handle.spool);
1514
695
  this.notifyTerminalOnce(handle, () => onTerminal({
@@ -1519,6 +700,7 @@ export class TaskRegistry {
1519
700
  status: "failed",
1520
701
  summary: `${(handle.description ?? "background command").slice(0, 200)} — lost its background process (poll failed: ${e instanceof Error ? e.message : String(e)}); no further output will arrive.${bashMirrorGapNote(handle)}`,
1521
702
  ...(crashedResult.length > 0 ? { result: crashedResult } : {}),
703
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
1522
704
  }));
1523
705
  }
1524
706
  finally {
@@ -1555,229 +737,12 @@ export class TaskRegistry {
1555
737
  }
1556
738
  return out;
1557
739
  }
1558
- registerMonitor(input) {
1559
- assertOwnership(input, "registerMonitor");
1560
- const id = this.mintTaskId("monitor");
1561
- const now = input.now ?? Date.now();
1562
- const timers = input.timers ?? defaultMonitorTimers;
1563
- const persistent = input.persistent === true;
1564
- const timeoutMs = persistent
1565
- ? undefined
1566
- : Math.min(MONITOR_MAX_TIMEOUT_MS, Math.max(1, Math.floor(input.timeoutMs ?? MONITOR_DEFAULT_TIMEOUT_MS)));
1567
- const handle = {
1568
- id,
1569
- type: "monitor",
1570
- status: "running",
1571
- ...(input.description !== undefined ? { description: input.description } : {}),
1572
- ...(input.owner !== undefined ? { owner: input.owner } : {}),
1573
- ...(input.scope !== undefined ? { scope: input.scope } : {}),
1574
- ...(input.toolUseId !== undefined ? { toolUseId: input.toolUseId } : {}),
1575
- ...(persistent ? { persistent: true } : {}),
1576
- ...(input.sessionScoped ? { sessionScoped: true } : {}),
1577
- createdAt: now,
1578
- updatedAt: now,
1579
- env: input.env,
1580
- shellId: input.shellId,
1581
- spool: { stdout: "", stderr: "", rolledChars: 0 },
1582
- lineBuf: "",
1583
- seq: 0,
1584
- batchTimes: [],
1585
- batchWindowMs: Math.max(1, Math.floor(input.batchWindowMs ?? MONITOR_BATCH_WINDOW_MS)),
1586
- maxBatchesPerMinute: Math.max(1, Math.floor(input.maxBatchesPerMinute ?? MONITOR_MAX_BATCHES_PER_MINUTE)),
1587
- ...(timeoutMs !== undefined ? { timeoutMs, deadlineAt: timers.now() + timeoutMs } : {}),
1588
- timers,
1589
- ...(input.onEvent !== undefined ? { onEvent: input.onEvent } : {}),
1590
- };
1591
- this.handles.set(id, handle);
1592
- this.startMonitorWatcher(id);
1593
- return id;
1594
- }
1595
- startMonitorWatcher(id) {
1596
- const handle = this.handles.get(id);
1597
- if (!handle || handle.type !== "monitor")
1598
- return;
1599
- const timers = handle.timers;
1600
- const ROLL_CAP = 2 * TASK_OUTPUT_MAX_CHARS;
1601
- const roll = (s) => {
1602
- if (s.length <= ROLL_CAP)
1603
- return s;
1604
- const half = Math.floor(ROLL_CAP / 2);
1605
- handle.spool.rolledChars += s.length - 2 * half;
1606
- return s.slice(0, half) + s.slice(s.length - half);
1607
- };
1608
- let ticking = false;
1609
- const MAX_RETRYABLE_POLL_FAILURES = 3;
1610
- let pollFailures = 0;
1611
- const stop = () => {
1612
- if (handle.watcher !== undefined) {
1613
- timers.clearInterval(handle.watcher);
1614
- handle.watcher = undefined;
1615
- }
1616
- };
1617
- const emit = (n) => {
1618
- try {
1619
- handle.onEvent?.(n);
1620
- }
1621
- catch {
1622
- }
1623
- };
1624
- const clipEvent = (s) => s.length <= MONITOR_EVENT_RESULT_CAP ? s : `${s.slice(0, MONITOR_EVENT_RESULT_CAP)}\n…[+${s.length - MONITOR_EVENT_RESULT_CAP} chars truncated]`;
1625
- const pendingFinalLines = (lines) => {
1626
- const out = handle.lineBuf !== "" ? [...lines, handle.lineBuf] : lines;
1627
- handle.lineBuf = "";
1628
- return out;
1629
- };
1630
- const terminal = (status, summary, finalLines) => {
1631
- handle.updatedAt = Date.now();
1632
- stop();
1633
- this.notifyTerminalOnce(handle, () => emit({
1634
- task_id: id,
1635
- task_type: "monitor",
1636
- ...(handle.toolUseId !== undefined ? { toolUseId: handle.toolUseId } : {}),
1637
- status,
1638
- ...(status === "killed" && handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
1639
- summary: `${(handle.description ?? "monitor").slice(0, 200)} — ${summary}`,
1640
- ...(finalLines.length > 0 ? { lines: finalLines, result: clipEvent(finalLines.join("\n")), ...(status === "killed" ? { partial: true } : {}) } : {}),
1641
- }));
1642
- };
1643
- const tick = async () => {
1644
- if (ticking)
1645
- return;
1646
- ticking = true;
1647
- try {
1648
- if (this.handles.get(id) !== handle)
1649
- return stop();
1650
- if (handle.status !== "running")
1651
- return stop();
1652
- const r = await handle.env.pollBackground(handle.shellId);
1653
- if (this.handles.get(id) !== handle || handle.status !== "running")
1654
- return stop();
1655
- if (!r.ok) {
1656
- if ((r.error.code === "timeout" || r.error.code === "io") && ++pollFailures <= MAX_RETRYABLE_POLL_FAILURES)
1657
- return;
1658
- handle.status = "failed";
1659
- return terminal("failed", `monitor lost its process (${r.error.message}); watch ended.`, pendingFinalLines([]));
1660
- }
1661
- pollFailures = 0;
1662
- const spool = handle.spool;
1663
- spool.stdout = roll(spool.stdout + r.value.stdout);
1664
- spool.stderr = roll(spool.stderr + r.value.stderr);
1665
- handle.lineBuf += r.value.stdout;
1666
- const parts = handle.lineBuf.split(/\r?\n/);
1667
- handle.lineBuf = parts.pop() ?? "";
1668
- const lines = parts.filter((l) => l !== "");
1669
- handle.status = statusFromBackground(r.value.status, r.value.exitCode);
1670
- if (r.value.status !== "running") {
1671
- spool.exitCode = r.value.exitCode;
1672
- if (handle.status === "killed" && handle.stoppedBy === undefined)
1673
- handle.stoppedBy = handle.stopSource ?? "system";
1674
- const label = r.value.status === "exited"
1675
- ? `exited(code ${r.value.exitCode}); watch ended.`
1676
- : `${r.value.status}; watch ended.`;
1677
- return terminal(handle.status === "completed" ? "completed" : handle.status === "killed" ? "killed" : "failed", label, pendingFinalLines(lines));
1678
- }
1679
- const now = timers.now();
1680
- if (handle.deadlineAt !== undefined && now >= handle.deadlineAt) {
1681
- this.markStopSource(id, "timeout");
1682
- const k = await handle.env.killBackground(handle.shellId);
1683
- if (this.handles.get(id) !== handle || handle.status !== "running")
1684
- return stop();
1685
- if (!k.ok && k.error.code !== "not_found") {
1686
- handle.status = "failed";
1687
- 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));
1688
- }
1689
- handle.status = "killed";
1690
- handle.stoppedBy = handle.stopSource ?? "timeout";
1691
- return terminal("killed", `timed out after ${handle.timeoutMs}ms; the process was killed and the watch ended.`, pendingFinalLines(lines));
1692
- }
1693
- if (lines.length === 0) {
1694
- if (r.value.stdout.length > 0 || r.value.stderr.length > 0)
1695
- handle.updatedAt = Date.now();
1696
- return;
1697
- }
1698
- const cutoff = now - 60_000;
1699
- handle.batchTimes = handle.batchTimes.filter((t) => t > cutoff);
1700
- handle.batchTimes.push(now);
1701
- if (handle.batchTimes.length > handle.maxBatchesPerMinute) {
1702
- this.markStopSource(id, "system");
1703
- const k = await handle.env.killBackground(handle.shellId);
1704
- if (this.handles.get(id) !== handle || handle.status !== "running")
1705
- return stop();
1706
- if (!k.ok && k.error.code !== "not_found") {
1707
- handle.status = "failed";
1708
- 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));
1709
- }
1710
- handle.status = "killed";
1711
- handle.stoppedBy = handle.stopSource ?? "system";
1712
- return terminal("killed", `auto-stopped: it produced too many events (more than ${handle.maxBatchesPerMinute} notification batches within a minute). ` +
1713
- `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));
1714
- }
1715
- handle.seq += 1;
1716
- handle.updatedAt = Date.now();
1717
- emit({
1718
- task_id: id,
1719
- task_type: "monitor",
1720
- ...(handle.toolUseId !== undefined ? { toolUseId: handle.toolUseId } : {}),
1721
- status: "event",
1722
- seq: handle.seq,
1723
- lines,
1724
- summary: `${(handle.description ?? "monitor").slice(0, 200)} — ${lines.length} new output line(s)`,
1725
- result: clipEvent(lines.join("\n")),
1726
- });
1727
- }
1728
- catch (e) {
1729
- if (this.handles.get(id) !== handle || handle.status !== "running")
1730
- return stop();
1731
- if (++pollFailures <= MAX_RETRYABLE_POLL_FAILURES)
1732
- return;
1733
- handle.status = "failed";
1734
- terminal("failed", `monitor watcher crashed (${e instanceof Error ? e.message : String(e)}); watch ended.`, pendingFinalLines([]));
1735
- }
1736
- finally {
1737
- ticking = false;
1738
- }
1739
- };
1740
- handle.watcher = timers.setInterval(tick, handle.batchWindowMs);
1741
- }
1742
- registerWorkflow(input) {
1743
- assertOwnership(input, "registerWorkflow");
1744
- const id = input.runId;
1745
- if (this.handles.has(id))
1746
- throw new Error(`TaskRegistry: task id already registered: ${id}`);
1747
- const now = input.now ?? Date.now();
1748
- const originatingSessionId = input.originatingSessionId;
1749
- const handle = {
1750
- id,
1751
- type: "workflow",
1752
- status: "running",
1753
- ...(input.description !== undefined ? { description: input.description } : {}),
1754
- ...(input.owner !== undefined ? { owner: input.owner } : {}),
1755
- ...(input.scope !== undefined ? { scope: input.scope } : {}),
1756
- ...(input.toolUseId !== undefined ? { toolUseId: input.toolUseId } : {}),
1757
- createdAt: now,
1758
- updatedAt: now,
1759
- runId: id,
1760
- handle: input.handle,
1761
- ...(input.store !== undefined ? { store: input.store } : {}),
1762
- ...(originatingSessionId !== undefined ? { originatingSessionId, sessionScoped: true } : {}),
1763
- ...(input.onServedTerminal !== undefined ? { onServedTerminal: input.onServedTerminal } : {}),
1764
- };
1765
- this.handles.set(id, handle);
1766
- void input.handle.done.then((res) => {
1767
- handle.status = "completed";
1768
- const run = res?.run;
1769
- const r = run?.resultFull ?? run?.result;
1770
- if (typeof r === "string")
1771
- handle.result = r;
1772
- handle.updatedAt = Date.now();
1773
- }, (err) => {
1774
- if (handle.status !== "cancelled")
1775
- handle.status = "failed";
1776
- handle.error = boundedRedactedSummary(err instanceof Error ? err.message : String(err), 500);
1777
- handle.updatedAt = Date.now();
1778
- });
1779
- return id;
1780
- }
740
+ registerMonitor(input) {
741
+ return registerMonitorLane(this.core, input);
742
+ }
743
+ registerWorkflow(input) {
744
+ return registerWorkflowLane(this.core, input);
745
+ }
1781
746
  async pollTask(id, access, opts = {}) {
1782
747
  const taskId = id.trim();
1783
748
  const deadline = opts.block ? blockDeadline(opts.timeoutMs, Date.now()) : undefined;
@@ -1825,6 +790,7 @@ export class TaskRegistry {
1825
790
  The durable record is still marked running (last update ${new Date(row.updatedAt).toISOString()}), but this engine instance is not executing it — the outcome is unknown here. If its host process is gone, a retention sweep (staleRunningMaxAgeMs) settles the record as interrupted.`;
1826
791
  return {
1827
792
  content: delimitUntrusted(`TaskOutput ${row.handle}`, body),
793
+ isError: true,
1828
794
  details: {
1829
795
  task_id: row.handle,
1830
796
  type: "background_agent",
@@ -1839,37 +805,6 @@ The durable record is still marked running (last update ${new Date(row.updatedAt
1839
805
  }
1840
806
  return noTask(taskId);
1841
807
  }
1842
- serveDurableAgentRow(row) {
1843
- if (row.status === "parked") {
1844
- return {
1845
- content: delimitUntrusted(`TaskOutput ${row.handle}`, `status: parked
1846
- 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.`),
1847
- details: {
1848
- task_id: row.handle,
1849
- type: "background_agent",
1850
- status: "parked",
1851
- retrieval_status: "success",
1852
- ...(row.seq !== undefined ? { seq: row.seq } : {}),
1853
- },
1854
- };
1855
- }
1856
- const body = `status: ${row.status}
1857
- ${row.error ? `error: ${row.error}
1858
- ` : ""}${row.finalOutput ? `--- result${row.resultIsPartial ? " (partial — produced before the task was stopped)" : ""} ---
1859
- ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}`;
1860
- return {
1861
- content: delimitUntrusted(`TaskOutput ${row.handle}`, body),
1862
- details: {
1863
- task_id: row.handle,
1864
- type: "background_agent",
1865
- status: row.status,
1866
- retrieval_status: "success",
1867
- ...(row.status === "killed" && row.stoppedBy !== undefined ? { stoppedBy: row.stoppedBy } : {}),
1868
- ...(row.seq !== undefined ? { seq: row.seq } : {}),
1869
- ...(row.resultIsPartial ? { partial_result: true } : {}),
1870
- },
1871
- };
1872
- }
1873
808
  async stopTask(id, access, opts = {}) {
1874
809
  const taskId = id.trim();
1875
810
  const handle = this.resolveInternal(taskId, access);
@@ -1917,7 +852,7 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
1917
852
  ? `${row.handle} is not attached to this engine instance; cannot stop it here. If its host process is gone, a retention sweep (staleRunningMaxAgeMs) settles it as interrupted.`
1918
853
  : row.status === "parked"
1919
854
  ? `${row.handle} is parked on a pending approval — stop it by deciding (deny/expire) the pending approval in the durable approval inbox.`
1920
- : `${row.handle} already ${row.status} — nothing to stop; poll TaskOutput for its final state.`,
855
+ : alreadyTerminalStopNote(row.handle, row.status),
1921
856
  details: {
1922
857
  task_id: row.handle,
1923
858
  type: "background_agent",
@@ -1929,13 +864,14 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
1929
864
  };
1930
865
  }
1931
866
  }
1932
- const byName = this.resolveBackgroundAgentByName(taskId, access);
867
+ const byName = this.resolveBackgroundAgentByName(taskId, access, { preferRunning: true });
1933
868
  if (byName.status === "found")
1934
869
  return this.stopBackgroundAgent(byName.handle);
1935
870
  if (byName.status === "ambiguous") {
1936
871
  return {
1937
872
  content: byName.message,
1938
873
  details: { task_id: taskId, retrieval_status: "not_ready", error: "ambiguous" },
874
+ isError: true,
1939
875
  };
1940
876
  }
1941
877
  return noTaskForStop(taskId, {
@@ -1943,235 +879,6 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
1943
879
  runningAgents: this.runningBackgroundAgentLabels(access),
1944
880
  });
1945
881
  }
1946
- resolveBackgroundAgentByName(name, access) {
1947
- const key = normalizeAgentName(name);
1948
- if (key === "")
1949
- return { status: "not_found" };
1950
- const latestOf = (pool) => {
1951
- let best;
1952
- for (const h of pool)
1953
- if (best === undefined || h.createdAt >= best.createdAt)
1954
- best = h;
1955
- return best;
1956
- };
1957
- const named = [];
1958
- for (const h of this.handles.values()) {
1959
- if (h.type !== "background_agent" || h.name === undefined || !canAccess(h, access))
1960
- continue;
1961
- if (h.name === name || normalizeAgentName(h.name) === key)
1962
- named.push(h);
1963
- }
1964
- const namedWinner = latestOf(named);
1965
- if (namedWinner !== undefined)
1966
- return { status: "found", handle: namedWinner };
1967
- const exact = [];
1968
- for (const h of this.handles.values()) {
1969
- if (h.type !== "background_agent" || !canAccess(h, access))
1970
- continue;
1971
- if (h.description === name)
1972
- exact.push(h);
1973
- }
1974
- const exactRunning = exact.filter((h) => h.status === "running");
1975
- const exactPool = exactRunning.length > 0 ? exactRunning : exact;
1976
- if (exactPool.length === 1)
1977
- return { status: "found", handle: exactPool[0] };
1978
- if (exactPool.length > 1) {
1979
- return {
1980
- status: "ambiguous",
1981
- message: `Multiple background agents match "${name}": ${exactPool.map((h) => (h.description ? `${h.id} (${h.description})` : h.id)).join(", ")}. Use the task ID.`,
1982
- };
1983
- }
1984
- const matches = [];
1985
- for (const h of this.handles.values()) {
1986
- if (h.type !== "background_agent" || !canAccess(h, access))
1987
- continue;
1988
- if (h.description !== undefined && normalizeAgentName(h.description) === key)
1989
- matches.push(h);
1990
- }
1991
- const running = matches.filter((h) => h.status === "running");
1992
- const pool = running.length > 0 ? running : matches;
1993
- if (pool.length === 1)
1994
- return { status: "found", handle: pool[0] };
1995
- if (pool.length > 1) {
1996
- return {
1997
- status: "ambiguous",
1998
- message: `Multiple background agents match "${name}": ${pool.map((h) => (h.description ? `${h.id} (${h.description})` : h.id)).join(", ")}. Use the task ID.`,
1999
- };
2000
- }
2001
- const byKey = new Map();
2002
- for (const h of this.handles.values()) {
2003
- if (h.type !== "background_agent" || h.status !== "running" || !canAccess(h, access))
2004
- continue;
2005
- for (const label of [h.name, h.description]) {
2006
- if (label === undefined)
2007
- continue;
2008
- const k = normalizeAgentName(label);
2009
- if (k !== "" && !byKey.has(k))
2010
- byKey.set(k, label);
2011
- }
2012
- }
2013
- const closest = closestName(key, byKey.keys());
2014
- return { status: "not_found", ...(closest !== undefined ? { suggestion: byKey.get(closest) } : {}) };
2015
- }
2016
- markRetainedContinuation(id) {
2017
- const handle = this.handles.get(id);
2018
- if (handle && handle.type === "background_agent")
2019
- handle.retainedContinuation = true;
2020
- }
2021
- reviveBackgroundAgent(id, access, abort) {
2022
- if (this.reapingHandles.has(id) || this.claimingHandles.has(id))
2023
- return { ok: false, reason: "not_found" };
2024
- const handle = this.handles.get(id);
2025
- if (!handle || handle.type !== "background_agent" || !canAccess(handle, access))
2026
- return { ok: false, reason: "not_found" };
2027
- if (handle.status === "running" || handle.status === "pending")
2028
- return { ok: false, reason: "still_running" };
2029
- if (handle.status === "parked")
2030
- return { ok: false, reason: "still_running" };
2031
- handle.status = "running";
2032
- handle.channelState = "attaching";
2033
- handle.notify = undefined;
2034
- if (abort !== undefined)
2035
- handle.abort = abort;
2036
- handle.result = undefined;
2037
- handle.resultFull = undefined;
2038
- handle.error = undefined;
2039
- handle.resultIsPartial = undefined;
2040
- handle.stopSource = undefined;
2041
- handle.reviveCycle = (handle.reviveCycle ?? 0) + 1;
2042
- handle.cycleSeq = (handle.cycleSeq ?? 1) + 1;
2043
- handle.updatedAt = Date.now();
2044
- this.durableAgentWrite(handle, { status: "running" }, ["settledAt", "stoppedBy", "finalOutput", "finalOutputFull", "error", "resultIsPartial", "summary", "recentSteps", "editedFiles", "usage"]);
2045
- return { ok: true, cycle: handle.reviveCycle };
2046
- }
2047
- settleRevivedAgent(id, cycle, outcome) {
2048
- const handle = this.handles.get(id);
2049
- if (!handle || handle.type !== "background_agent")
2050
- return undefined;
2051
- if ((handle.reviveCycle ?? 0) !== cycle)
2052
- return undefined;
2053
- return this.settleBackgroundAgent(id, outcome);
2054
- }
2055
- unmarkRetainedContinuation(id) {
2056
- const handle = this.handles.get(id);
2057
- if (handle && handle.type === "background_agent")
2058
- handle.retainedContinuation = false;
2059
- }
2060
- attachAgentNotify(id, notify, cycle) {
2061
- const handle = this.handles.get(id);
2062
- if (!handle || handle.type !== "background_agent" || handle.status !== "running")
2063
- return;
2064
- if (cycle !== undefined && (handle.reviveCycle ?? 0) !== cycle)
2065
- return;
2066
- handle.notify = notify;
2067
- handle.channelState = "attaching";
2068
- const live = handle.preAttachQueue;
2069
- if (live !== undefined && live.length > 0) {
2070
- void (async () => {
2071
- while (handle.preAttachQueue === live && live.length > 0) {
2072
- const [qPayload, qOpts, qResolve] = live[0];
2073
- let result;
2074
- try {
2075
- const d = await notify(qPayload, qOpts);
2076
- result =
2077
- d === "parked" && handle.retainedContinuation !== true
2078
- ? { ok: false, reason: "not_running" }
2079
- : d === "parked"
2080
- ? { ok: true, disposition: "parked" }
2081
- : { ok: true, disposition: "queued" };
2082
- }
2083
- catch {
2084
- result = { ok: false, reason: "not_running" };
2085
- }
2086
- if (handle.preAttachQueue !== live)
2087
- return;
2088
- live.shift();
2089
- qResolve?.(result);
2090
- }
2091
- if (handle.preAttachQueue === live) {
2092
- handle.preAttachQueue = undefined;
2093
- handle.channelState = "attached";
2094
- }
2095
- })();
2096
- }
2097
- else {
2098
- handle.preAttachQueue = undefined;
2099
- handle.channelState = "attached";
2100
- }
2101
- }
2102
- async deliverToRunningAgent(id, access, notification, opts) {
2103
- const handle = this.handles.get(id);
2104
- if (!handle || handle.type !== "background_agent" || !canAccess(handle, access))
2105
- return { ok: false, reason: "not_found" };
2106
- if (handle.status !== "running")
2107
- return { ok: false, reason: "not_running" };
2108
- const bounded = (p, fallback, onTimeout) => new Promise((resolve) => {
2109
- const t = setTimeout(() => {
2110
- try {
2111
- onTimeout?.();
2112
- }
2113
- catch {
2114
- }
2115
- resolve(fallback);
2116
- }, this.deliverConfirmTimeoutMs);
2117
- t.unref?.();
2118
- void p.then((v) => {
2119
- clearTimeout(t);
2120
- resolve(v);
2121
- }, () => {
2122
- clearTimeout(t);
2123
- resolve(fallback);
2124
- });
2125
- });
2126
- if (handle.notify === undefined || handle.channelState === "attaching") {
2127
- if (handle.channelState !== "attaching")
2128
- return { ok: false, reason: "no_channel" };
2129
- const q = (handle.preAttachQueue ??= []);
2130
- if (q.length >= 8)
2131
- return { ok: false, reason: "no_channel" };
2132
- return bounded(new Promise((resolve) => {
2133
- q.push([notification, opts, resolve]);
2134
- }), { ok: true, disposition: "pending" });
2135
- }
2136
- let detachOnTimeout;
2137
- return await bounded(new Promise((resolve) => {
2138
- const inflight = (handle.inFlightDirect ??= new Set());
2139
- const settleOnce = (r) => {
2140
- if (!inflight.has(settleOnce))
2141
- return;
2142
- inflight.delete(settleOnce);
2143
- resolve(r);
2144
- };
2145
- inflight.add(settleOnce);
2146
- detachOnTimeout = () => inflight.delete(settleOnce);
2147
- void (async () => {
2148
- let result;
2149
- try {
2150
- const d = await handle.notify?.(notification, opts);
2151
- if (d === "parked") {
2152
- result = handle.retainedContinuation === true ? { ok: true, disposition: "parked" } : { ok: false, reason: "not_running" };
2153
- }
2154
- else {
2155
- result = { ok: true, disposition: "queued" };
2156
- }
2157
- }
2158
- catch {
2159
- result = { ok: false, reason: "not_running" };
2160
- }
2161
- settleOnce(result);
2162
- })();
2163
- }), { ok: true, disposition: "pending" }, () => detachOnTimeout?.());
2164
- }
2165
- runningBackgroundAgentLabels(access) {
2166
- const out = [];
2167
- for (const h of this.handles.values()) {
2168
- if (h.type !== "background_agent" || h.status !== "running" || !canAccess(h, access))
2169
- continue;
2170
- const label = h.name ?? h.description;
2171
- out.push(label ? `${h.id} (${label})` : h.id);
2172
- }
2173
- return out;
2174
- }
2175
882
  deliverConfirmTimeoutMs = 10_000;
2176
883
  lastGcAt = 0;
2177
884
  list(access, opts = {}) {
@@ -2277,6 +984,7 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
2277
984
  return {
2278
985
  content: `Error (TaskOutput): invalid filter regex: ${filter}`,
2279
986
  details: { task_id: handle.id, type: "background_bash", status: handle.status, retrieval_status: "not_ready" },
987
+ isError: true,
2280
988
  };
2281
989
  }
2282
990
  }
@@ -2293,7 +1001,13 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
2293
1001
  const timedOutClause = spool.timedOut === true ? ` (timed out after ${spool.timeoutSec !== undefined ? `${spool.timeoutSec}s` : "its background time budget"})` : "";
2294
1002
  const displayStatus = (spool.exitCode !== undefined ? `exited(code ${spool.exitCode})` : running ? "running" : handle.status) + timedOutClause;
2295
1003
  const rolledNote = spool.rolledChars > 0 ? `[!] ${spool.rolledChars} earlier char(s) dropped from the spool (memory bound)\n` : "";
2296
- const body = `status: ${displayStatus}\n${rolledNote}--- stdout (full, re-readable) ---\n${shape(spool.stdout) || "(none)"}\n` +
1004
+ const droppedNote = (spool.droppedBytes ?? 0) > 0
1005
+ ? `[!] ${spool.droppedBytes} earlier byte(s) permanently dropped before this window (8MB buffer evicted)\n`
1006
+ : spool.dropUnknown === true
1007
+ ? "[!] an unknown amount of earlier output was permanently dropped before this window (buffer evicted)\n"
1008
+ : "";
1009
+ const stdoutHeading = droppedNote !== "" ? "stdout (re-readable spool — INCOMPLETE, see note above)" : "stdout (full, re-readable)";
1010
+ const body = `status: ${displayStatus}\n${rolledNote}${droppedNote}--- ${stdoutHeading} ---\n${shape(spool.stdout) || "(none)"}\n` +
2297
1011
  `--- stderr ---\n${shape(spool.stderr) || "(none)"}`;
2298
1012
  return {
2299
1013
  content: delimitUntrusted(`TaskOutput ${handle.id}`, body),
@@ -2303,9 +1017,11 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
2303
1017
  status: handle.status,
2304
1018
  retrieval_status: retrieval,
2305
1019
  ...(handle.status === "killed" && handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
1020
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
2306
1021
  details: {
2307
1022
  status: running ? "running" : handle.status,
2308
1023
  exitCode: spool.exitCode,
1024
+ ...(spool.droppedBytes !== undefined ? { bytesDroppedBeforeCursor: spool.droppedBytes } : {}),
2309
1025
  ...(spool.timedOut === true ? { timedOut: true, ...(spool.timeoutSec !== undefined ? { timeoutSec: spool.timeoutSec } : {}) } : {}),
2310
1026
  },
2311
1027
  },
@@ -2335,6 +1051,7 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
2335
1051
  return {
2336
1052
  content: `Error (TaskOutput): ${r.error.message}${salvaged}`,
2337
1053
  details: { task_id: handle.id, type: "background_bash", status: handle.status, retrieval_status, error: r.error.code },
1054
+ isError: true,
2338
1055
  };
2339
1056
  }
2340
1057
  stdout = roll(stdout + r.value.stdout);
@@ -2345,6 +1062,8 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
2345
1062
  handle.stoppedBy = handle.stopSource ?? (r.value.timedOut === true ? "timeout" : "system");
2346
1063
  handle.updatedAt = Date.now();
2347
1064
  const running = r.value.status === "running";
1065
+ if (!running)
1066
+ mintCompletionId(handle);
2348
1067
  if (!running || deadline === undefined || Date.now() >= deadline || signal?.aborted) {
2349
1068
  const retrieval = running && deadline !== undefined && Date.now() >= deadline ? "timeout" : "success";
2350
1069
  const shape = (text) => clipTaskOutput(rx ? text.split(/\r?\n/).filter((l) => rx.test(l)).join("\n") : text, handle.outputFile);
@@ -2362,6 +1081,7 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
2362
1081
  status: handle.status,
2363
1082
  retrieval_status: retrieval,
2364
1083
  ...(handle.status === "killed" && handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
1084
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
2365
1085
  details: {
2366
1086
  status: r.value.status,
2367
1087
  exitCode: r.value.exitCode,
@@ -2374,71 +1094,11 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
2374
1094
  await sleepPollStep(deadline, signal);
2375
1095
  }
2376
1096
  }
2377
- fireServedTerminal(handle) {
2378
- const cb = handle.onServedTerminal;
2379
- if (!cb)
2380
- return;
2381
- handle.onServedTerminal = undefined;
2382
- try {
2383
- cb();
2384
- }
2385
- catch {
2386
- }
2387
- }
2388
1097
  async pollWorkflow(handle, access, deadline, signal) {
2389
- const store = handle.store;
2390
- if (store) {
2391
- for (;;) {
2392
- const run = await store.get(handle.runId);
2393
- if (!run || !canAccessWorkflowRun(run, access))
2394
- break;
2395
- if (run.status !== "running" || deadline === undefined || Date.now() >= deadline || signal?.aborted) {
2396
- const out = formatWorkflowRun(run);
2397
- if (run.status === "running" && deadline !== undefined && Date.now() >= deadline)
2398
- out.details.retrieval_status = "timeout";
2399
- else if (run.status !== "running")
2400
- this.fireServedTerminal(handle);
2401
- return out;
2402
- }
2403
- await sleepPollStep(deadline, signal);
2404
- }
2405
- }
2406
- while (handle.status === "running" && deadline !== undefined && Date.now() < deadline && !signal?.aborted) {
2407
- await sleepPollStep(deadline, signal);
2408
- }
2409
- if (handle.status !== "running")
2410
- this.fireServedTerminal(handle);
2411
- return {
2412
- content: JSON.stringify({
2413
- task_id: handle.id,
2414
- runId: handle.id,
2415
- type: "workflow",
2416
- status: handle.status,
2417
- ...(handle.status === "completed" && handle.result !== undefined
2418
- ? { result: delimitUntrusted("workflow result", handle.result) }
2419
- : {}),
2420
- ...(handle.status === "failed" && handle.error !== undefined
2421
- ? { error: delimitUntrusted("workflow error", handle.error) }
2422
- : {}),
2423
- note: handle.status === "running"
2424
- ? "still running — poll again shortly."
2425
- : handle.status === "completed"
2426
- ? handle.result !== undefined
2427
- ? "completed — `result` above is the workflow's return value."
2428
- : "completed — its result was delivered on the completion notification."
2429
- : handle.status === "failed" && handle.error !== undefined
2430
- ? "failed — see error."
2431
- : "workflow reached a terminal state.",
2432
- }),
2433
- details: {
2434
- task_id: handle.id,
2435
- type: "workflow",
2436
- status: handle.status,
2437
- retrieval_status: handle.status === "running" && deadline !== undefined && Date.now() >= deadline ? "timeout" : "success",
2438
- },
2439
- };
1098
+ return pollWorkflowLane(handle, access, deadline, signal);
2440
1099
  }
2441
1100
  async stopBackgroundBash(handle) {
1101
+ const wasRunning = handle.status === "running";
2442
1102
  if (handle.watcher !== undefined) {
2443
1103
  clearInterval(handle.watcher);
2444
1104
  handle.watcher = undefined;
@@ -2447,8 +1107,15 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
2447
1107
  const r = await handle.env.killBackground(handle.shellId);
2448
1108
  if (handle.status !== "running") {
2449
1109
  return {
2450
- content: `Terminated ${handle.id}.`,
2451
- details: { task_id: handle.id, type: "background_bash", status: handle.status, retrieval_status: "success", ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}) },
1110
+ content: wasRunning ? `Terminated ${handle.id}.` : alreadyTerminalStopNote(handle.id, handle.status),
1111
+ details: {
1112
+ task_id: handle.id,
1113
+ type: "background_bash",
1114
+ status: handle.status,
1115
+ retrieval_status: "success",
1116
+ ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
1117
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
1118
+ },
2452
1119
  };
2453
1120
  }
2454
1121
  if (!r.ok) {
@@ -2457,10 +1124,18 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
2457
1124
  handle.stoppedBy = handle.stopSource ?? "parent";
2458
1125
  handle.status = "killed";
2459
1126
  handle.updatedAt = Date.now();
1127
+ mintCompletionId(handle);
2460
1128
  this.pokeBgQuiescence(handle.owner);
2461
1129
  return {
2462
1130
  content: `Terminated ${handle.id} (the process was already gone).`,
2463
- details: { task_id: handle.id, type: "background_bash", status: "killed", retrieval_status: "success", stoppedBy: handle.stoppedBy },
1131
+ details: {
1132
+ task_id: handle.id,
1133
+ type: "background_bash",
1134
+ status: "killed",
1135
+ retrieval_status: "success",
1136
+ stoppedBy: handle.stoppedBy,
1137
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
1138
+ },
2464
1139
  };
2465
1140
  }
2466
1141
  const restored = handle.onTerminal !== undefined;
@@ -2471,301 +1146,52 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
2471
1146
  content: `Error (TaskStop): ${r.error.message} — the kill did not land; the process may still be running.` +
2472
1147
  (restored ? " Its watcher keeps running: TaskOutput still works and the completion notification will still fire." : " TaskOutput still reflects its live status."),
2473
1148
  details: { task_id: handle.id, type: "background_bash", status: handle.status, retrieval_status, error: r.error.code },
1149
+ isError: true,
2474
1150
  };
2475
1151
  }
2476
1152
  if (handle.stoppedBy === undefined)
2477
1153
  handle.stoppedBy = handle.stopSource ?? "parent";
2478
1154
  handle.status = "killed";
2479
1155
  handle.updatedAt = Date.now();
1156
+ mintCompletionId(handle);
2480
1157
  this.pokeBgQuiescence(handle.owner);
2481
- return {
2482
- content: `Terminated ${handle.id}.`,
2483
- details: { task_id: handle.id, type: "background_bash", status: "killed", retrieval_status: "success", stoppedBy: handle.stoppedBy },
2484
- };
2485
- }
2486
- async pollMonitor(handle, filter, deadline, signal) {
2487
- let rx;
2488
- if (filter !== undefined) {
2489
- try {
2490
- rx = new RegExp(filter);
2491
- }
2492
- catch {
2493
- return {
2494
- content: `Error (TaskOutput): invalid filter regex: ${filter}`,
2495
- details: { task_id: handle.id, type: "monitor", status: handle.status, retrieval_status: "not_ready" },
2496
- };
2497
- }
2498
- }
2499
- while (handle.status === "running" && deadline !== undefined && Date.now() < deadline && !signal?.aborted) {
2500
- await sleepPollStep(deadline, signal);
2501
- }
2502
- const spool = handle.spool;
2503
- const shape = (text) => clipTaskOutput(rx ? text.split(/\r?\n/).filter((l) => rx.test(l)).join("\n") : text, handle.outputFile);
2504
- const running = handle.status === "running";
2505
- const retrieval = running && deadline !== undefined && Date.now() >= deadline ? "timeout" : "success";
2506
- const displayStatus = spool.exitCode !== undefined ? `exited(code ${spool.exitCode})` : running ? "running" : handle.status;
2507
- const rolledNote = spool.rolledChars > 0 ? `[!] ${spool.rolledChars} earlier char(s) dropped from the spool (memory bound)\n` : "";
2508
- const body = `status: ${displayStatus}\n${rolledNote}--- stdout (full, re-readable) ---\n${shape(spool.stdout) || "(none)"}\n` +
2509
- `--- stderr ---\n${shape(spool.stderr) || "(none)"}`;
2510
- return {
2511
- content: delimitUntrusted(`TaskOutput ${handle.id}`, body),
2512
- details: {
2513
- task_id: handle.id,
2514
- type: "monitor",
2515
- status: handle.status,
2516
- retrieval_status: retrieval,
2517
- ...(handle.status === "killed" && handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
2518
- details: { status: running ? "running" : handle.status, exitCode: spool.exitCode },
2519
- },
2520
- };
2521
- }
2522
- async stopMonitor(handle) {
2523
- if (handle.watcher !== undefined) {
2524
- handle.timers.clearInterval(handle.watcher);
2525
- handle.watcher = undefined;
2526
- }
2527
- this.markStopSource(handle.id, "parent");
2528
- const r = await handle.env.killBackground(handle.shellId);
2529
- if (handle.status !== "running") {
2530
- return {
2531
- content: `Terminated ${handle.id}.`,
2532
- details: { task_id: handle.id, type: "monitor", status: handle.status, retrieval_status: "success", ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}) },
2533
- };
2534
- }
2535
- if (!r.ok) {
2536
- if (r.error.code === "not_found") {
2537
- if (handle.stoppedBy === undefined)
2538
- handle.stoppedBy = handle.stopSource ?? "parent";
2539
- handle.status = "killed";
2540
- handle.updatedAt = Date.now();
2541
- this.pokeBgQuiescence(handle.owner);
2542
- return {
2543
- content: `Terminated ${handle.id} (the process was already gone).`,
2544
- details: { task_id: handle.id, type: "monitor", status: "killed", retrieval_status: "success", stoppedBy: handle.stoppedBy },
2545
- };
2546
- }
2547
- if (handle.watcher === undefined)
2548
- this.startMonitorWatcher(handle.id);
2549
- const retrieval_status = r.error.code === "timeout" ? "timeout" : "not_ready";
2550
- return {
2551
- 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.`,
2552
- details: { task_id: handle.id, type: "monitor", status: handle.status, retrieval_status, error: r.error.code },
2553
- };
2554
- }
2555
- if (handle.status === "running") {
2556
- if (handle.stoppedBy === undefined)
2557
- handle.stoppedBy = handle.stopSource ?? "parent";
2558
- handle.status = "killed";
2559
- handle.updatedAt = Date.now();
2560
- this.pokeBgQuiescence(handle.owner);
2561
- }
2562
1158
  return {
2563
1159
  content: `Terminated ${handle.id}.`,
2564
1160
  details: {
2565
1161
  task_id: handle.id,
2566
- type: "monitor",
2567
- status: handle.status,
1162
+ type: "background_bash",
1163
+ status: "killed",
2568
1164
  retrieval_status: "success",
2569
- ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
1165
+ stoppedBy: handle.stoppedBy,
1166
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
2570
1167
  },
2571
1168
  };
2572
1169
  }
2573
- async pollBackgroundAgent(handle, deadline, signal) {
2574
- while (handle.status === "running" && deadline !== undefined && Date.now() < deadline && !signal?.aborted) {
2575
- await sleepPollStep(deadline, signal);
2576
- }
2577
- const running = handle.status === "running";
2578
- const retrieval = running && deadline !== undefined && Date.now() >= deadline ? "timeout" : "success";
2579
- if (handle.status === "parked") {
2580
- return {
2581
- content: delimitUntrusted(`TaskOutput ${handle.id}`, `status: parked
2582
- 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.`),
2583
- details: {
2584
- task_id: handle.id,
2585
- type: "background_agent",
2586
- status: "parked",
2587
- retrieval_status: "success",
2588
- ...(handle.cycleSeq !== undefined ? { seq: handle.cycleSeq } : {}),
2589
- },
2590
- };
2591
- }
2592
- const body = running
2593
- ? `status: running
2594
- The agent is still working — you will be notified when it completes.`
2595
- : `status: ${handle.status}
2596
- ${handle.error ? `error: ${handle.error}
2597
- ` : ""}${handle.result ? `--- result${handle.resultIsPartial ? " (partial — produced before the task was stopped)" : ""} ---
2598
- ${clipTaskOutput(handle.resultFull ?? handle.result, handle.outputFile)}` : "(no result text)"}`;
2599
- return {
2600
- content: delimitUntrusted(`TaskOutput ${handle.id}`, body),
2601
- details: {
2602
- task_id: handle.id,
2603
- type: "background_agent",
2604
- status: handle.status,
2605
- retrieval_status: retrieval,
2606
- ...(handle.cycleSeq !== undefined ? { seq: handle.cycleSeq } : {}),
2607
- ...(handle.status === "killed" && handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
2608
- ...(handle.resultIsPartial ? { partial_result: true } : {}),
2609
- },
2610
- };
1170
+ async pollMonitor(handle, filter, deadline, signal) {
1171
+ return pollMonitorLane(handle, filter, deadline, signal);
2611
1172
  }
2612
- async stopBackgroundAgent(handle) {
2613
- if (handle.status === "parked") {
2614
- const arbiter = handle.resolveParkedStop;
2615
- if (arbiter === undefined) {
2616
- return {
2617
- content: `${handle.id} is parked on a pending approval — stop it by deciding (deny/expire) the pending approval in the durable approval inbox.`,
2618
- details: { task_id: handle.id, type: "background_agent", status: "parked", retrieval_status: "not_ready", error: "parked_pending_approval" },
2619
- };
2620
- }
2621
- let stopWon = false;
2622
- let arbiterUnreachable = false;
2623
- try {
2624
- stopWon = await arbiter();
2625
- }
2626
- catch {
2627
- stopWon = false;
2628
- arbiterUnreachable = true;
2629
- }
2630
- if (stopWon && handle.status === "parked") {
2631
- this.markStopSource(handle.id, "parent");
2632
- handle.status = "killed";
2633
- handle.stoppedBy = handle.stopSource ?? "parent";
2634
- handle.updatedAt = Date.now();
2635
- handle.parkedCheckpointToken = undefined;
2636
- handle.resolveParkedStop = undefined;
2637
- this.pokeBgQuiescence(handle.owner);
2638
- {
2639
- const lane = handle.durable;
2640
- if (lane !== undefined) {
2641
- try {
2642
- const live = await lane.store.get(handle.id, lane.record.scope);
2643
- if (live && (live.status === "parked" || live.status === "running")) {
2644
- const term = structuredClone(live);
2645
- term.status = "killed";
2646
- if (handle.stoppedBy !== undefined)
2647
- term.stoppedBy = handle.stoppedBy;
2648
- term.settledAt = Date.now();
2649
- term.updatedAt = Date.now();
2650
- delete term.parkedCheckpointToken;
2651
- delete term.parkClaimId;
2652
- delete term.parkedAt;
2653
- await lane.store.update(handle.id, lane.record.scope, term, { rev: live.rev });
2654
- }
2655
- }
2656
- catch {
2657
- }
2658
- }
2659
- }
2660
- this.durableAgentWrite(handle, { status: "killed", settledAt: Date.now(), ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}) }, ["parkedCheckpointToken", "parkClaimId"]);
2661
- return {
2662
- content: `Terminated ${handle.id} (pending approval expired).`,
2663
- details: {
2664
- task_id: handle.id,
2665
- type: "background_agent",
2666
- status: "killed",
2667
- retrieval_status: "success",
2668
- ...(handle.cycleSeq !== undefined ? { seq: handle.cycleSeq } : {}),
2669
- ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
2670
- },
2671
- };
2672
- }
2673
- if (arbiterUnreachable) {
2674
- return {
2675
- content: `${handle.id} was not stopped: the approval arbitration store was unreachable — the row stays parked; retry the stop.`,
2676
- details: { task_id: handle.id, type: "background_agent", status: handle.status, retrieval_status: "not_ready", error: "park_arbiter_unreachable" },
2677
- };
2678
- }
2679
- return {
2680
- content: `${handle.id} was not stopped: its pending approval was already consumed (a resume won the arbitration).`,
2681
- details: { task_id: handle.id, type: "background_agent", status: handle.status, retrieval_status: "not_ready", error: "park_resume_won" },
2682
- };
2683
- }
2684
- if (handle.status === "running") {
2685
- this.markStopSource(handle.id, "parent");
2686
- handle.abort.abort();
2687
- handle.status = "killed";
2688
- handle.stoppedBy = handle.stopSource ?? "parent";
2689
- handle.updatedAt = Date.now();
2690
- handle.notify = undefined;
2691
- for (const [, , qResolve] of handle.preAttachQueue ?? [])
2692
- qResolve?.({ ok: false, reason: "not_running" });
2693
- handle.preAttachQueue = undefined;
2694
- for (const r of [...(handle.inFlightDirect ?? [])])
2695
- r({ ok: false, reason: "not_running" });
2696
- handle.inFlightDirect = undefined;
2697
- this.pokeBgQuiescence(handle.owner);
2698
- this.durableAgentWrite(handle, {
2699
- status: "killed",
2700
- settledAt: Date.now(),
2701
- ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
2702
- });
2703
- }
2704
- return {
2705
- content: `Terminated ${handle.id}.`,
2706
- details: {
2707
- task_id: handle.id,
2708
- type: "background_agent",
2709
- status: handle.status,
2710
- retrieval_status: "success",
2711
- ...(handle.cycleSeq !== undefined ? { seq: handle.cycleSeq } : {}),
2712
- ...(handle.status === "killed" && handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
2713
- },
2714
- };
1173
+ async stopMonitor(handle) {
1174
+ return stopMonitorLane(this.core, handle);
2715
1175
  }
2716
1176
  async stopWorkflow(handle) {
2717
- if (handle.status !== "running") {
2718
- return {
2719
- content: JSON.stringify({
2720
- task_id: handle.id,
2721
- runId: handle.id,
2722
- type: "workflow",
2723
- status: handle.status,
2724
- stopped: false,
2725
- note: `workflow already ${handle.status} — nothing to stop; poll TaskOutput for its final state.`,
2726
- }),
2727
- details: { task_id: handle.id, type: "workflow", status: handle.status, retrieval_status: "success" },
2728
- };
2729
- }
2730
- handle.status = "cancelled";
2731
- handle.updatedAt = Date.now();
2732
- handle.handle.cancel("TaskStop requested");
2733
- return {
2734
- content: JSON.stringify({
2735
- task_id: handle.id,
2736
- runId: handle.id,
2737
- type: "workflow",
2738
- status: "cancelled",
2739
- stopped: true,
2740
- note: "workflow cancellation requested; durable run status maps cancellation to failed when it finalizes.",
2741
- }),
2742
- details: { task_id: handle.id, type: "workflow", status: "cancelled", retrieval_status: "success" },
2743
- };
1177
+ return stopWorkflowLane(handle);
2744
1178
  }
2745
1179
  }
2746
1180
  export const defaultTaskRegistry = new TaskRegistry();
2747
1181
  export function createTaskOutputTool(opts) {
1182
+ const caps = { ...REGISTRY_TASK_TOOL_CAPS, notification: opts.notificationWired !== false };
2748
1183
  return defineTool({
2749
- name: "TaskOutput",
1184
+ name: TASK_OUTPUT_TOOL_NAME,
2750
1185
  aliases: [...TASK_OUTPUT_ALIASES],
2751
- contract: { contractId: "core.task_output@1", implementationRevision: "1" },
2752
- description: "[Deprecated] — for background Bash tasks, prefer Read on the output file path; for agent tasks, use the Agent tool result directly. " +
2753
- "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. " +
2754
- "Retrieve output or status for a background task by task_id. Supports background Bash tasks and workflow " +
2755
- "tasks returned by RunWorkflow. Returns only the caller's own task scope; unknown and out-of-scope ids are " +
2756
- "reported the same way. Legacy bash_id and runId arguments are accepted for compatibility.",
2757
- parameters: Type.Object({
2758
- task_id: Type.Optional(Type.String({ description: "The task_id returned by Bash(run_in_background) or RunWorkflow." })),
2759
- 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." })),
2760
- timeout: Type.Optional(Type.Number({ description: "Optional timeout in milliseconds when block is true (default 30000, max 600000)." })),
2761
- filter: Type.Optional(Type.String({ description: "Optional regex filter for background Bash output (sema extension)." })),
2762
- }),
1186
+ contract: TASK_OUTPUT_CONTRACT,
1187
+ description: composeTaskOutputDescription(caps),
1188
+ parameters: composeTaskOutputParams(caps),
2763
1189
  effect: "read",
2764
1190
  execute: async (rawArgs, ctx) => {
2765
1191
  const args = rawArgs;
2766
- const id = firstString(args.task_id, args.bash_id, args.runId);
1192
+ const id = resolveTaskIdArg(args.task_id, args.bash_id, args.runId);
2767
1193
  if (!id)
2768
- return "Error (TaskOutput): Missing required parameter: task_id";
1194
+ return errorResult(TASK_OUTPUT_MISSING_ID_MESSAGE);
2769
1195
  const requestedMs = typeof args.timeout === "number" ? args.timeout : undefined;
2770
1196
  let effectiveTimeoutMs = requestedMs;
2771
1197
  let waitClampNote = "";
@@ -2792,35 +1218,33 @@ export function createTaskOutputTool(opts) {
2792
1218
  signal: ctx.signal,
2793
1219
  });
2794
1220
  const { type: taskType, ...rest } = r.details;
2795
- return { content: r.content + waitClampNote, details: { type: "task-output", ...(taskType !== undefined ? { taskType } : {}), ...(waitClampNote !== "" ? { waitClamped: true } : {}), ...rest } };
1221
+ return {
1222
+ content: r.content + waitClampNote,
1223
+ details: { type: "task-output", ...(taskType !== undefined ? { taskType } : {}), ...(waitClampNote !== "" ? { waitClamped: true } : {}), ...rest },
1224
+ ...(r.isError ? { isError: true } : {}),
1225
+ };
2796
1226
  },
2797
1227
  });
2798
1228
  }
2799
1229
  export function createTaskStopTool(opts) {
2800
1230
  return defineTool({
2801
- name: "TaskStop",
1231
+ name: TASK_STOP_TOOL_NAME,
2802
1232
  aliases: [...TASK_STOP_ALIASES],
2803
- contract: { contractId: "core.task_stop@1", implementationRevision: "1" },
2804
- description: "- Stops a running background task by its ID\n" +
2805
- "- Takes a task_id parameter identifying the task to stop\n" +
2806
- "- To stop a background agent spawned with a name, pass that name as task_id\n" +
2807
- "- Returns a success or failure status\n" +
2808
- "- Use this tool when you need to terminate a long-running task",
2809
- parameters: Type.Object({
2810
- task_id: Type.Optional(Type.String({ description: "The ID of the background task to stop" })),
2811
- shell_id: Type.Optional(Type.String({ description: "Deprecated: use task_id instead" })),
2812
- }),
1233
+ contract: TASK_STOP_CONTRACT,
1234
+ description: composeTaskStopDescription(REGISTRY_TASK_TOOL_CAPS),
1235
+ parameters: TASK_STOP_PARAMS,
2813
1236
  effect: "write",
2814
1237
  execute: async (rawArgs, ctx) => {
2815
1238
  const args = rawArgs;
2816
- const id = firstString(args.task_id, args.shell_id, args.runId);
1239
+ const id = resolveTaskIdArg(args.task_id, args.shell_id, args.runId);
2817
1240
  if (!id)
2818
- return "Missing required parameter: task_id";
1241
+ return errorResult(TASK_STOP_MISSING_ID_MESSAGE);
2819
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 });
2820
1243
  const { type: taskType, ...rest } = r.details;
2821
1244
  return {
2822
1245
  content: r.content,
2823
1246
  details: { type: "task-stop", message: r.content, task_type: taskType ?? "unknown", ...rest },
1247
+ ...(r.isError ? { isError: true } : {}),
2824
1248
  };
2825
1249
  },
2826
1250
  });