@sema-agent/core 1.450.0 → 1.452.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 (57) hide show
  1. package/dist/agents/observer.js +3 -0
  2. package/dist/agents/subagent.js +51 -12
  3. package/dist/core/ask-question.js +5 -5
  4. package/dist/core/background-agent-store.d.ts +2 -0
  5. package/dist/core/background-agent-store.js +5 -1
  6. package/dist/core/exec-output-tail.d.ts +18 -1
  7. package/dist/core/exec-output-tail.js +38 -5
  8. package/dist/core/lsp-session.d.ts +1 -0
  9. package/dist/core/lsp-session.js +24 -6
  10. package/dist/core/lsp.d.ts +10 -0
  11. package/dist/core/lsp.js +63 -6
  12. package/dist/core/mailbox-store.d.ts +3 -2
  13. package/dist/core/mailbox-store.js +19 -4
  14. package/dist/core/memory.js +1 -1
  15. package/dist/core/runner/prepare-task.js +11 -1
  16. package/dist/core/runner/runtask.js +1 -1
  17. package/dist/core/session-reconcile.js +5 -2
  18. package/dist/core/task-notification.d.ts +1 -0
  19. package/dist/core/task-registry.d.ts +15 -9
  20. package/dist/core/task-registry.js +290 -53
  21. package/dist/core/tool-result-store.js +2 -2
  22. package/dist/core/tools.d.ts +5 -0
  23. package/dist/core/tools.js +3 -0
  24. package/dist/core/types.d.ts +2 -0
  25. package/dist/core/workflow-journal-store.d.ts +2 -0
  26. package/dist/core/workflow-journal-store.js +14 -0
  27. package/dist/engine/execution-env/node-execution-env.d.ts +1 -0
  28. package/dist/engine/execution-env/node-execution-env.js +130 -20
  29. package/dist/engine/lsp/node-lsp-manager.d.ts +3 -1
  30. package/dist/engine/lsp/node-lsp-manager.js +22 -5
  31. package/dist/engine/lsp/stdio-lsp-transport.d.ts +1 -1
  32. package/dist/engine/lsp/stdio-lsp-transport.js +17 -6
  33. package/dist/index.d.ts +2 -2
  34. package/dist/index.js +2 -2
  35. package/dist/orchestration/run-workflow-tool.d.ts +2 -0
  36. package/dist/orchestration/run-workflow-tool.js +35 -6
  37. package/dist/orchestration/workflow.d.ts +9 -0
  38. package/dist/orchestration/workflow.js +80 -5
  39. package/dist/stores/cc/mailbox-store.js +6 -1
  40. package/dist/stores/file/background-agent-store.js +3 -2
  41. package/dist/stores/file/mailbox-store.d.ts +1 -1
  42. package/dist/stores/file/mailbox-store.js +9 -7
  43. package/dist/tools/fs/encoding.d.ts +5 -0
  44. package/dist/tools/fs/encoding.js +6 -0
  45. package/dist/tools/fs/index.js +184 -120
  46. package/dist/tools/fs/notebook.d.ts +43 -0
  47. package/dist/tools/fs/notebook.js +141 -0
  48. package/dist/tools/fs/repo-map.js +2 -2
  49. package/dist/tools/fs/search.js +141 -12
  50. package/dist/tools/gitea-issue.js +4 -2
  51. package/dist/tools/monitor.js +12 -8
  52. package/dist/tools/scheduler-tools.js +16 -16
  53. package/dist/tools/task-list.js +34 -12
  54. package/dist/tools/web.d.ts +2 -0
  55. package/dist/tools/web.js +105 -19
  56. package/dist/tools/worktree.js +14 -14
  57. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  import { randomBytes } from "node:crypto";
2
2
  import { Type } from "typebox";
3
- import { defineTool } from "./tools.js";
3
+ import { uuidv7 } from "../internal/harness.js";
4
+ import { defineTool, errorResult } from "./tools.js";
4
5
  import { hasBackgroundShell } from "./background-shell.js";
5
6
  import { shutdownDebug } from "./shutdown-debug.js";
6
7
  import { summarizeWorkflowRun } from "./workflow-run-store.js";
@@ -8,6 +9,16 @@ import { canAccessAgentRecord, BackgroundAgentStoreError, } from "./background-a
8
9
  import { delimitUntrusted } from "./untrusted-text.js";
9
10
  import { boundedRedactedSummary } from "./untrusted-egress.js";
10
11
  import { clipWithFilePointer } from "./tool-errors.js";
12
+ function mintCompletionId(target) {
13
+ if (target.completionId === undefined)
14
+ target.completionId = uuidv7();
15
+ return target.completionId;
16
+ }
17
+ function commitCompletionIdIfEmpty(target, value) {
18
+ if (target.completionId === undefined)
19
+ target.completionId = value;
20
+ return target.completionId;
21
+ }
11
22
  const defaultMonitorTimers = {
12
23
  setInterval: (fn, ms) => {
13
24
  const h = setInterval(() => void fn(), ms);
@@ -49,6 +60,9 @@ export const MONITOR_DEFAULT_TIMEOUT_MS = 300_000;
49
60
  export const MONITOR_MAX_TIMEOUT_MS = 3_600_000;
50
61
  export const MONITOR_MAX_BATCHES_PER_MINUTE = 50;
51
62
  const MONITOR_EVENT_RESULT_CAP = 2_000;
63
+ function clipMonitorEvent(s) {
64
+ return s.length <= MONITOR_EVENT_RESULT_CAP ? s : `${s.slice(0, MONITOR_EVENT_RESULT_CAP)}\n…[+${s.length - MONITOR_EVENT_RESULT_CAP} chars truncated]`;
65
+ }
52
66
  const TASK_OUTPUT_DEFAULT_CHARS = 32_000;
53
67
  const TASK_OUTPUT_MAX_CHARS = 160_000;
54
68
  const TASK_OUTPUT_ALIASES = ["BashOutput", "AgentOutputTool", "BashOutputTool", "AgentOutput", "WorkflowStatus"];
@@ -79,6 +93,7 @@ function noTask(taskId) {
79
93
  return {
80
94
  content: JSON.stringify({ error: "task not found" }),
81
95
  details: { task_id: taskId, type: "unknown", status: "not_found", retrieval_status: "not_ready", error: "not_found" },
96
+ isError: true,
82
97
  };
83
98
  }
84
99
  export function normalizeAgentName(s) {
@@ -133,6 +148,7 @@ function noTaskForStop(taskId, enrich) {
133
148
  return {
134
149
  content,
135
150
  details: { task_id: taskId, type: "unknown", status: "not_found", retrieval_status: "not_ready", error: "not_found" },
151
+ isError: true,
136
152
  };
137
153
  }
138
154
  function bashTerminalResult(spool) {
@@ -163,6 +179,31 @@ function bashMirrorGapNote(handle) {
163
179
  ? " [!] The output file is INCOMPLETE (one or more writes to it failed) — trust the result text / TaskOutput over the file."
164
180
  : "";
165
181
  }
182
+ function accountDroppedBytes(spool, poll) {
183
+ const d = poll.bytesDroppedBeforeCursor ?? 0;
184
+ if (d > 0)
185
+ spool.droppedBytes = (spool.droppedBytes ?? 0) + d;
186
+ else if (poll.truncated === true)
187
+ spool.dropUnknown = true;
188
+ }
189
+ function droppedGapNote(spool) {
190
+ if ((spool.droppedBytes ?? 0) > 0)
191
+ return ` [!] ${spool.droppedBytes} earlier byte(s) of output were permanently dropped before this window (execution-environment tail buffer evicted) — the event stream and this spool are INCOMPLETE.`;
192
+ if (spool.dropUnknown === true)
193
+ return " [!] An unknown amount of earlier output was permanently dropped before this window (execution-environment tail buffer evicted) — the event stream and this spool are INCOMPLETE.";
194
+ return "";
195
+ }
196
+ function firstDropNote(spool) {
197
+ if (spool.dropNotified === true)
198
+ return "";
199
+ const note = droppedGapNote(spool);
200
+ if (note !== "")
201
+ spool.dropNotified = true;
202
+ return note;
203
+ }
204
+ function alreadyTerminalStopNote(id, status) {
205
+ return `${id} already ${status} — nothing to stop; poll TaskOutput for its final state.`;
206
+ }
166
207
  function monitorInTimeoutWindow(handle) {
167
208
  return handle.status === "running" && handle.deadlineAt !== undefined && handle.timers.now() < handle.deadlineAt;
168
209
  }
@@ -238,6 +279,19 @@ function formatWorkflowRun(run) {
238
279
  ...(overCap ? { agent_runs_note: `showing ${agentRows.length} of ${run.agents.length} agent rows (every non-completed row kept; completed rows trimmed).` } : {}),
239
280
  startedAt: run.startedAt,
240
281
  ...(run.endedAt !== undefined ? { endedAt: run.endedAt, elapsedMs: run.endedAt - run.startedAt } : {}),
282
+ ...(run.resume !== undefined
283
+ ? {
284
+ resume: {
285
+ from_run_id: run.resume.fromRunId,
286
+ journal_entries: run.resume.journalEntries,
287
+ replayed: run.resume.replayed,
288
+ ...(run.resume.divergedAtOrdinal !== undefined
289
+ ? { replay_stopped_at: run.resume.divergedAtOrdinal, replay_stopped_reason: run.resume.divergedReason }
290
+ : {}),
291
+ },
292
+ }
293
+ : {}),
294
+ ...(run.journalSkips !== undefined && run.journalSkips > 0 ? { journal_skipped: run.journalSkips } : {}),
241
295
  ...(run.status === "failed" && run.error ? { error: delimitUntrusted("workflow error", boundedRedactedSummary(run.error, 500)) } : {}),
242
296
  ...(run.status === "completed" && run.result !== undefined
243
297
  ? { result: delimitUntrusted("workflow result", run.resultFull ?? run.result) }
@@ -268,6 +322,7 @@ function formatWorkflowRun(run) {
268
322
  type: "workflow",
269
323
  status: run.status,
270
324
  retrieval_status: "success",
325
+ ...(run.completionId !== undefined ? { completionId: run.completionId } : {}),
271
326
  details: summary,
272
327
  },
273
328
  };
@@ -762,6 +817,9 @@ export class TaskRegistry {
762
817
  return undefined;
763
818
  return handle.stoppedBy ?? handle.stopSource ?? "system";
764
819
  }
820
+ getCompletionId(id) {
821
+ return this.handles.get(id)?.completionId;
822
+ }
765
823
  parkBackgroundAgent(id, park) {
766
824
  const handle = this.handles.get(id);
767
825
  if (!handle || handle.type !== "background_agent")
@@ -819,8 +877,9 @@ export class TaskRegistry {
819
877
  handle.parkedCheckpointToken = undefined;
820
878
  handle.resolveParkedStop = undefined;
821
879
  handle.updatedAt = now;
880
+ mintCompletionId(handle);
822
881
  this.pokeBgQuiescence(handle.owner);
823
- this.durableAgentWrite(handle, { status: "failed", error: handle.error, settledAt: now, stoppedBy: "system" }, ["parkedCheckpointToken", "parkClaimId", "parkedAt"]);
882
+ this.durableAgentWrite(handle, { status: "failed", error: handle.error, settledAt: now, stoppedBy: "system", completionId: handle.completionId }, ["parkedCheckpointToken", "parkClaimId", "parkedAt"]);
824
883
  liveFailed++;
825
884
  }
826
885
  const { reconcileParkedAgents: storeReconcile } = await import("./background-agent-store.js");
@@ -895,6 +954,7 @@ export class TaskRegistry {
895
954
  return "retry";
896
955
  }
897
956
  const now = Date.now();
957
+ const rollbackCompletionIdCandidate = uuidv7();
898
958
  const settleLocalParkedHandle = (error) => {
899
959
  const h = this.handles.get(ticket.handle);
900
960
  if (h !== undefined && h.type === "background_agent" && h.status === "parked") {
@@ -903,6 +963,7 @@ export class TaskRegistry {
903
963
  h.parkedCheckpointToken = undefined;
904
964
  h.resolveParkedStop = undefined;
905
965
  h.updatedAt = now;
966
+ commitCompletionIdIfEmpty(h, rollbackCompletionIdCandidate);
906
967
  this.pokeBgQuiescence(h.owner);
907
968
  }
908
969
  };
@@ -948,6 +1009,7 @@ export class TaskRegistry {
948
1009
  term.stoppedBy = "system";
949
1010
  term.settledAt = now;
950
1011
  term.updatedAt = now;
1012
+ term.completionId = rollbackCompletionIdCandidate;
951
1013
  delete term.parkedCheckpointToken;
952
1014
  delete term.parkClaimId;
953
1015
  delete term.parkedAt;
@@ -977,6 +1039,7 @@ export class TaskRegistry {
977
1039
  failed.stoppedBy = "system";
978
1040
  failed.settledAt = now;
979
1041
  failed.updatedAt = now;
1042
+ failed.completionId = rollbackCompletionIdCandidate;
980
1043
  delete failed.parkedCheckpointToken;
981
1044
  delete failed.parkClaimId;
982
1045
  delete failed.parkedAt;
@@ -1053,6 +1116,7 @@ export class TaskRegistry {
1053
1116
  handle.parkedCheckpointToken = undefined;
1054
1117
  handle.resolveParkedStop = undefined;
1055
1118
  handle.updatedAt = Date.now();
1119
+ handle.completionId = undefined;
1056
1120
  if (flipped.seq !== undefined)
1057
1121
  handle.cycleSeq = flipped.seq;
1058
1122
  return true;
@@ -1069,6 +1133,7 @@ export class TaskRegistry {
1069
1133
  return undefined;
1070
1134
  if (handle.status !== "running") {
1071
1135
  if (handle.status === "killed") {
1136
+ mintCompletionId(handle);
1072
1137
  let backfilled = false;
1073
1138
  if (handle.result === undefined && outcome.result !== undefined && outcome.result !== "") {
1074
1139
  handle.result = outcome.result;
@@ -1088,12 +1153,14 @@ export class TaskRegistry {
1088
1153
  ...(handle.resultFull !== undefined ? { finalOutputFull: handle.resultFull } : {}),
1089
1154
  ...(handle.resultIsPartial ? { resultIsPartial: true } : {}),
1090
1155
  ...(handle.error !== undefined ? { error: handle.error } : {}),
1156
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
1091
1157
  });
1092
1158
  }
1093
1159
  }
1094
1160
  return handle.status === "completed" || handle.status === "failed" || handle.status === "killed" ? handle.status : undefined;
1095
1161
  }
1096
1162
  handle.status = outcome.status;
1163
+ mintCompletionId(handle);
1097
1164
  handle.notify = undefined;
1098
1165
  for (const [, , qResolve] of handle.preAttachQueue ?? [])
1099
1166
  qResolve?.({ ok: false, reason: "not_running" });
@@ -1123,6 +1190,7 @@ export class TaskRegistry {
1123
1190
  ...(outcome.seq !== undefined ? { seq: outcome.seq } : {}),
1124
1191
  settledAt: Date.now(),
1125
1192
  ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
1193
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
1126
1194
  ...(handle.result !== undefined ? { finalOutput: handle.result } : {}),
1127
1195
  ...(handle.resultFull !== undefined ? { finalOutputFull: handle.resultFull } : {}),
1128
1196
  ...(handle.resultIsPartial ? { resultIsPartial: true } : {}),
@@ -1195,6 +1263,7 @@ export class TaskRegistry {
1195
1263
  handle.status = "killed";
1196
1264
  handle.stoppedBy = handle.stopSource ?? source;
1197
1265
  handle.updatedAt = Date.now();
1266
+ mintCompletionId(handle);
1198
1267
  if (handle.outputFile !== undefined) {
1199
1268
  try {
1200
1269
  const drained = await handle.env.pollBackground(handle.shellId);
@@ -1221,6 +1290,7 @@ export class TaskRegistry {
1221
1290
  stoppedBy: handle.stoppedBy,
1222
1291
  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
1292
  ...(resultText.length > 0 ? { result: resultText, partial: true } : {}),
1293
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
1224
1294
  }));
1225
1295
  }
1226
1296
  else {
@@ -1235,6 +1305,7 @@ export class TaskRegistry {
1235
1305
  handle.status = "killed";
1236
1306
  handle.stoppedBy = handle.stopSource ?? source;
1237
1307
  handle.updatedAt = Date.now();
1308
+ mintCompletionId(handle);
1238
1309
  const lastLines = handle.lineBuf !== "" ? [handle.lineBuf] : [];
1239
1310
  handle.lineBuf = "";
1240
1311
  this.notifyTerminalOnce(handle, () => handle.onEvent?.({
@@ -1245,6 +1316,7 @@ export class TaskRegistry {
1245
1316
  stoppedBy: handle.stoppedBy,
1246
1317
  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
1318
  ...(lastLines.length > 0 ? { lines: lastLines, result: lastLines.join("\n"), partial: true } : {}),
1319
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
1248
1320
  }));
1249
1321
  }
1250
1322
  settled++;
@@ -1270,6 +1342,7 @@ export class TaskRegistry {
1270
1342
  handle.status = "killed";
1271
1343
  handle.stoppedBy = handle.stopSource ?? "system";
1272
1344
  handle.updatedAt = Date.now();
1345
+ mintCompletionId(handle);
1273
1346
  reaped++;
1274
1347
  continue;
1275
1348
  }
@@ -1286,6 +1359,7 @@ export class TaskRegistry {
1286
1359
  handle.status = "killed";
1287
1360
  handle.stoppedBy = handle.stopSource ?? "system";
1288
1361
  handle.updatedAt = Date.now();
1362
+ mintCompletionId(handle);
1289
1363
  reaped++;
1290
1364
  continue;
1291
1365
  }
@@ -1442,6 +1516,7 @@ export class TaskRegistry {
1442
1516
  return;
1443
1517
  handle.status = "failed";
1444
1518
  handle.updatedAt = Date.now();
1519
+ mintCompletionId(handle);
1445
1520
  stop();
1446
1521
  const lostResult = bashTerminalResult(handle.spool);
1447
1522
  this.notifyTerminalOnce(handle, () => onTerminal({
@@ -1452,11 +1527,13 @@ export class TaskRegistry {
1452
1527
  status: "failed",
1453
1528
  summary: `${(handle.description ?? "background command").slice(0, 200)} — lost its background process (${r.error.message}); no further output will arrive.${bashMirrorGapNote(handle)}`,
1454
1529
  ...(lostResult.length > 0 ? { result: lostResult } : {}),
1530
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
1455
1531
  }));
1456
1532
  return;
1457
1533
  }
1458
1534
  pollFailures = 0;
1459
1535
  const spool = handle.spool;
1536
+ accountDroppedBytes(spool, r.value);
1460
1537
  spool.stdout = roll(spool.stdout + r.value.stdout);
1461
1538
  spool.stderr = roll(spool.stderr + r.value.stderr);
1462
1539
  if (handle.outputFile !== undefined && (r.value.stdout.length > 0 || r.value.stderr.length > 0)) {
@@ -1470,6 +1547,7 @@ export class TaskRegistry {
1470
1547
  }
1471
1548
  handle.updatedAt = Date.now();
1472
1549
  if (r.value.status !== "running") {
1550
+ mintCompletionId(handle);
1473
1551
  spool.exitCode = r.value.exitCode;
1474
1552
  if (r.value.timedOut === true) {
1475
1553
  spool.timedOut = true;
@@ -1496,9 +1574,12 @@ export class TaskRegistry {
1496
1574
  ? r.value.timeoutSec !== undefined
1497
1575
  ? `${desc} — timed out after ${r.value.timeoutSec}s (its own background time budget); the process was killed`
1498
1576
  : `${desc} — timed out (its own background time budget); the process was killed`
1499
- : `${desc} — ${r.value.status}`) + bashMirrorGapNote(handle),
1577
+ : `${desc} — ${r.value.status}`) +
1578
+ bashMirrorGapNote(handle) +
1579
+ droppedGapNote(spool),
1500
1580
  ...(resultText.length > 0 ? { result: resultText } : {}),
1501
1581
  ...(handle.status === "killed" && resultText.length > 0 ? { partial: true } : {}),
1582
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
1502
1583
  }));
1503
1584
  }
1504
1585
  }
@@ -1509,6 +1590,7 @@ export class TaskRegistry {
1509
1590
  return;
1510
1591
  handle.status = "failed";
1511
1592
  handle.updatedAt = Date.now();
1593
+ mintCompletionId(handle);
1512
1594
  stop();
1513
1595
  const crashedResult = bashTerminalResult(handle.spool);
1514
1596
  this.notifyTerminalOnce(handle, () => onTerminal({
@@ -1519,6 +1601,7 @@ export class TaskRegistry {
1519
1601
  status: "failed",
1520
1602
  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
1603
  ...(crashedResult.length > 0 ? { result: crashedResult } : {}),
1604
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
1522
1605
  }));
1523
1606
  }
1524
1607
  finally {
@@ -1592,19 +1675,29 @@ export class TaskRegistry {
1592
1675
  this.startMonitorWatcher(id);
1593
1676
  return id;
1594
1677
  }
1678
+ absorbMonitorPoll(handle, v) {
1679
+ const spool = handle.spool;
1680
+ accountDroppedBytes(spool, v);
1681
+ const ROLL_CAP = 2 * TASK_OUTPUT_MAX_CHARS;
1682
+ spool.stdout = rollSpoolText(spool, spool.stdout + v.stdout, ROLL_CAP);
1683
+ spool.stderr = rollSpoolText(spool, spool.stderr + v.stderr, ROLL_CAP);
1684
+ handle.lineBuf += v.stdout;
1685
+ const parts = handle.lineBuf.split(/\r?\n/);
1686
+ handle.lineBuf = parts.pop() ?? "";
1687
+ return parts.filter((l) => l !== "");
1688
+ }
1689
+ emitMonitorEvent(handle, n) {
1690
+ try {
1691
+ handle.onEvent?.(n);
1692
+ }
1693
+ catch {
1694
+ }
1695
+ }
1595
1696
  startMonitorWatcher(id) {
1596
1697
  const handle = this.handles.get(id);
1597
1698
  if (!handle || handle.type !== "monitor")
1598
1699
  return;
1599
1700
  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
1701
  let ticking = false;
1609
1702
  const MAX_RETRYABLE_POLL_FAILURES = 3;
1610
1703
  let pollFailures = 0;
@@ -1614,14 +1707,7 @@ export class TaskRegistry {
1614
1707
  handle.watcher = undefined;
1615
1708
  }
1616
1709
  };
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]`;
1710
+ const emit = (n) => this.emitMonitorEvent(handle, n);
1625
1711
  const pendingFinalLines = (lines) => {
1626
1712
  const out = handle.lineBuf !== "" ? [...lines, handle.lineBuf] : lines;
1627
1713
  handle.lineBuf = "";
@@ -1629,6 +1715,7 @@ export class TaskRegistry {
1629
1715
  };
1630
1716
  const terminal = (status, summary, finalLines) => {
1631
1717
  handle.updatedAt = Date.now();
1718
+ mintCompletionId(handle);
1632
1719
  stop();
1633
1720
  this.notifyTerminalOnce(handle, () => emit({
1634
1721
  task_id: id,
@@ -1636,14 +1723,16 @@ export class TaskRegistry {
1636
1723
  ...(handle.toolUseId !== undefined ? { toolUseId: handle.toolUseId } : {}),
1637
1724
  status,
1638
1725
  ...(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 } : {}) } : {}),
1726
+ summary: `${(handle.description ?? "monitor").slice(0, 200)} — ${summary}${droppedGapNote(handle.spool)}`,
1727
+ ...(finalLines.length > 0 ? { lines: finalLines, result: clipMonitorEvent(finalLines.join("\n")), ...(status === "killed" ? { partial: true } : {}) } : {}),
1728
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
1641
1729
  }));
1642
1730
  };
1643
1731
  const tick = async () => {
1644
1732
  if (ticking)
1645
1733
  return;
1646
1734
  ticking = true;
1735
+ handle.tickInFlight = true;
1647
1736
  try {
1648
1737
  if (this.handles.get(id) !== handle)
1649
1738
  return stop();
@@ -1660,20 +1749,22 @@ export class TaskRegistry {
1660
1749
  }
1661
1750
  pollFailures = 0;
1662
1751
  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 !== "");
1752
+ const lines = this.absorbMonitorPoll(handle, r.value);
1669
1753
  handle.status = statusFromBackground(r.value.status, r.value.exitCode);
1670
1754
  if (r.value.status !== "running") {
1671
1755
  spool.exitCode = r.value.exitCode;
1672
1756
  if (handle.status === "killed" && handle.stoppedBy === undefined)
1673
- handle.stoppedBy = handle.stopSource ?? "system";
1757
+ handle.stoppedBy = handle.stopSource ?? (r.value.timedOut === true ? "timeout" : "system");
1758
+ if (r.value.timedOut === true) {
1759
+ spool.timedOut = true;
1760
+ if (r.value.timeoutSec !== undefined)
1761
+ spool.timeoutSec = r.value.timeoutSec;
1762
+ }
1674
1763
  const label = r.value.status === "exited"
1675
1764
  ? `exited(code ${r.value.exitCode}); watch ended.`
1676
- : `${r.value.status}; watch ended.`;
1765
+ : r.value.timedOut === true
1766
+ ? `timed out after ${r.value.timeoutSec !== undefined ? `${r.value.timeoutSec}s` : "its background time budget"} (the execution environment's background time budget); the process was killed and the watch ended.`
1767
+ : `${r.value.status}; watch ended.`;
1677
1768
  return terminal(handle.status === "completed" ? "completed" : handle.status === "killed" ? "killed" : "failed", label, pendingFinalLines(lines));
1678
1769
  }
1679
1770
  const now = timers.now();
@@ -1721,8 +1812,8 @@ export class TaskRegistry {
1721
1812
  status: "event",
1722
1813
  seq: handle.seq,
1723
1814
  lines,
1724
- summary: `${(handle.description ?? "monitor").slice(0, 200)} — ${lines.length} new output line(s)`,
1725
- result: clipEvent(lines.join("\n")),
1815
+ summary: `${(handle.description ?? "monitor").slice(0, 200)} — ${lines.length} new output line(s)${firstDropNote(spool)}`,
1816
+ result: clipMonitorEvent(lines.join("\n")),
1726
1817
  });
1727
1818
  }
1728
1819
  catch (e) {
@@ -1735,6 +1826,7 @@ export class TaskRegistry {
1735
1826
  }
1736
1827
  finally {
1737
1828
  ticking = false;
1829
+ handle.tickInFlight = undefined;
1738
1830
  }
1739
1831
  };
1740
1832
  handle.watcher = timers.setInterval(tick, handle.batchWindowMs);
@@ -1769,11 +1861,18 @@ export class TaskRegistry {
1769
1861
  const r = run?.resultFull ?? run?.result;
1770
1862
  if (typeof r === "string")
1771
1863
  handle.result = r;
1864
+ if (run?.completionId !== undefined)
1865
+ handle.completionId = run.completionId;
1772
1866
  handle.updatedAt = Date.now();
1773
1867
  }, (err) => {
1774
1868
  if (handle.status !== "cancelled")
1775
1869
  handle.status = "failed";
1776
1870
  handle.error = boundedRedactedSummary(err instanceof Error ? err.message : String(err), 500);
1871
+ const rejectedRun = err !== null && (typeof err === "object" || typeof err === "function")
1872
+ ? err.workflowRun
1873
+ : undefined;
1874
+ if (rejectedRun?.completionId !== undefined)
1875
+ handle.completionId = rejectedRun.completionId;
1777
1876
  handle.updatedAt = Date.now();
1778
1877
  });
1779
1878
  return id;
@@ -1825,6 +1924,7 @@ export class TaskRegistry {
1825
1924
  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
1925
  return {
1827
1926
  content: delimitUntrusted(`TaskOutput ${row.handle}`, body),
1927
+ isError: true,
1828
1928
  details: {
1829
1929
  task_id: row.handle,
1830
1930
  type: "background_agent",
@@ -1867,6 +1967,7 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
1867
1967
  ...(row.status === "killed" && row.stoppedBy !== undefined ? { stoppedBy: row.stoppedBy } : {}),
1868
1968
  ...(row.seq !== undefined ? { seq: row.seq } : {}),
1869
1969
  ...(row.resultIsPartial ? { partial_result: true } : {}),
1970
+ ...(row.completionId !== undefined ? { completionId: row.completionId } : {}),
1870
1971
  },
1871
1972
  };
1872
1973
  }
@@ -1917,7 +2018,7 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
1917
2018
  ? `${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
2019
  : row.status === "parked"
1919
2020
  ? `${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.`,
2021
+ : alreadyTerminalStopNote(row.handle, row.status),
1921
2022
  details: {
1922
2023
  task_id: row.handle,
1923
2024
  type: "background_agent",
@@ -1929,13 +2030,14 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
1929
2030
  };
1930
2031
  }
1931
2032
  }
1932
- const byName = this.resolveBackgroundAgentByName(taskId, access);
2033
+ const byName = this.resolveBackgroundAgentByName(taskId, access, { preferRunning: true });
1933
2034
  if (byName.status === "found")
1934
2035
  return this.stopBackgroundAgent(byName.handle);
1935
2036
  if (byName.status === "ambiguous") {
1936
2037
  return {
1937
2038
  content: byName.message,
1938
2039
  details: { task_id: taskId, retrieval_status: "not_ready", error: "ambiguous" },
2040
+ isError: true,
1939
2041
  };
1940
2042
  }
1941
2043
  return noTaskForStop(taskId, {
@@ -1943,7 +2045,7 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
1943
2045
  runningAgents: this.runningBackgroundAgentLabels(access),
1944
2046
  });
1945
2047
  }
1946
- resolveBackgroundAgentByName(name, access) {
2048
+ resolveBackgroundAgentByName(name, access, opts) {
1947
2049
  const key = normalizeAgentName(name);
1948
2050
  if (key === "")
1949
2051
  return { status: "not_found" };
@@ -1961,6 +2063,17 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
1961
2063
  if (h.name === name || normalizeAgentName(h.name) === key)
1962
2064
  named.push(h);
1963
2065
  }
2066
+ if (opts?.preferRunning === true) {
2067
+ const namedRunning = named.filter((h) => h.status === "running");
2068
+ if (namedRunning.length > 1) {
2069
+ return {
2070
+ status: "ambiguous",
2071
+ message: `Multiple RUNNING background agents match "${name}": ${namedRunning.map((h) => `${h.id} (${h.name ?? h.description ?? "unnamed"})`).join(", ")}. Use the task ID.`,
2072
+ };
2073
+ }
2074
+ if (namedRunning.length === 1)
2075
+ return { status: "found", handle: namedRunning[0] };
2076
+ }
1964
2077
  const namedWinner = latestOf(named);
1965
2078
  if (namedWinner !== undefined)
1966
2079
  return { status: "found", handle: namedWinner };
@@ -2038,10 +2151,11 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
2038
2151
  handle.error = undefined;
2039
2152
  handle.resultIsPartial = undefined;
2040
2153
  handle.stopSource = undefined;
2154
+ handle.completionId = undefined;
2041
2155
  handle.reviveCycle = (handle.reviveCycle ?? 0) + 1;
2042
2156
  handle.cycleSeq = (handle.cycleSeq ?? 1) + 1;
2043
2157
  handle.updatedAt = Date.now();
2044
- this.durableAgentWrite(handle, { status: "running" }, ["settledAt", "stoppedBy", "finalOutput", "finalOutputFull", "error", "resultIsPartial", "summary", "recentSteps", "editedFiles", "usage"]);
2158
+ this.durableAgentWrite(handle, { status: "running" }, ["settledAt", "stoppedBy", "finalOutput", "finalOutputFull", "error", "resultIsPartial", "completionId", "summary", "recentSteps", "editedFiles", "usage"]);
2045
2159
  return { ok: true, cycle: handle.reviveCycle };
2046
2160
  }
2047
2161
  settleRevivedAgent(id, cycle, outcome) {
@@ -2277,6 +2391,7 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
2277
2391
  return {
2278
2392
  content: `Error (TaskOutput): invalid filter regex: ${filter}`,
2279
2393
  details: { task_id: handle.id, type: "background_bash", status: handle.status, retrieval_status: "not_ready" },
2394
+ isError: true,
2280
2395
  };
2281
2396
  }
2282
2397
  }
@@ -2293,7 +2408,13 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
2293
2408
  const timedOutClause = spool.timedOut === true ? ` (timed out after ${spool.timeoutSec !== undefined ? `${spool.timeoutSec}s` : "its background time budget"})` : "";
2294
2409
  const displayStatus = (spool.exitCode !== undefined ? `exited(code ${spool.exitCode})` : running ? "running" : handle.status) + timedOutClause;
2295
2410
  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` +
2411
+ const droppedNote = (spool.droppedBytes ?? 0) > 0
2412
+ ? `[!] ${spool.droppedBytes} earlier byte(s) permanently dropped before this window (8MB buffer evicted)\n`
2413
+ : spool.dropUnknown === true
2414
+ ? "[!] an unknown amount of earlier output was permanently dropped before this window (buffer evicted)\n"
2415
+ : "";
2416
+ const stdoutHeading = droppedNote !== "" ? "stdout (re-readable spool — INCOMPLETE, see note above)" : "stdout (full, re-readable)";
2417
+ const body = `status: ${displayStatus}\n${rolledNote}${droppedNote}--- ${stdoutHeading} ---\n${shape(spool.stdout) || "(none)"}\n` +
2297
2418
  `--- stderr ---\n${shape(spool.stderr) || "(none)"}`;
2298
2419
  return {
2299
2420
  content: delimitUntrusted(`TaskOutput ${handle.id}`, body),
@@ -2303,9 +2424,11 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
2303
2424
  status: handle.status,
2304
2425
  retrieval_status: retrieval,
2305
2426
  ...(handle.status === "killed" && handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
2427
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
2306
2428
  details: {
2307
2429
  status: running ? "running" : handle.status,
2308
2430
  exitCode: spool.exitCode,
2431
+ ...(spool.droppedBytes !== undefined ? { bytesDroppedBeforeCursor: spool.droppedBytes } : {}),
2309
2432
  ...(spool.timedOut === true ? { timedOut: true, ...(spool.timeoutSec !== undefined ? { timeoutSec: spool.timeoutSec } : {}) } : {}),
2310
2433
  },
2311
2434
  },
@@ -2335,6 +2458,7 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
2335
2458
  return {
2336
2459
  content: `Error (TaskOutput): ${r.error.message}${salvaged}`,
2337
2460
  details: { task_id: handle.id, type: "background_bash", status: handle.status, retrieval_status, error: r.error.code },
2461
+ isError: true,
2338
2462
  };
2339
2463
  }
2340
2464
  stdout = roll(stdout + r.value.stdout);
@@ -2345,6 +2469,8 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
2345
2469
  handle.stoppedBy = handle.stopSource ?? (r.value.timedOut === true ? "timeout" : "system");
2346
2470
  handle.updatedAt = Date.now();
2347
2471
  const running = r.value.status === "running";
2472
+ if (!running)
2473
+ mintCompletionId(handle);
2348
2474
  if (!running || deadline === undefined || Date.now() >= deadline || signal?.aborted) {
2349
2475
  const retrieval = running && deadline !== undefined && Date.now() >= deadline ? "timeout" : "success";
2350
2476
  const shape = (text) => clipTaskOutput(rx ? text.split(/\r?\n/).filter((l) => rx.test(l)).join("\n") : text, handle.outputFile);
@@ -2362,6 +2488,7 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
2362
2488
  status: handle.status,
2363
2489
  retrieval_status: retrieval,
2364
2490
  ...(handle.status === "killed" && handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
2491
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
2365
2492
  details: {
2366
2493
  status: r.value.status,
2367
2494
  exitCode: r.value.exitCode,
@@ -2435,10 +2562,12 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
2435
2562
  type: "workflow",
2436
2563
  status: handle.status,
2437
2564
  retrieval_status: handle.status === "running" && deadline !== undefined && Date.now() >= deadline ? "timeout" : "success",
2565
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
2438
2566
  },
2439
2567
  };
2440
2568
  }
2441
2569
  async stopBackgroundBash(handle) {
2570
+ const wasRunning = handle.status === "running";
2442
2571
  if (handle.watcher !== undefined) {
2443
2572
  clearInterval(handle.watcher);
2444
2573
  handle.watcher = undefined;
@@ -2447,8 +2576,15 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
2447
2576
  const r = await handle.env.killBackground(handle.shellId);
2448
2577
  if (handle.status !== "running") {
2449
2578
  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 } : {}) },
2579
+ content: wasRunning ? `Terminated ${handle.id}.` : alreadyTerminalStopNote(handle.id, handle.status),
2580
+ details: {
2581
+ task_id: handle.id,
2582
+ type: "background_bash",
2583
+ status: handle.status,
2584
+ retrieval_status: "success",
2585
+ ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
2586
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
2587
+ },
2452
2588
  };
2453
2589
  }
2454
2590
  if (!r.ok) {
@@ -2457,10 +2593,18 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
2457
2593
  handle.stoppedBy = handle.stopSource ?? "parent";
2458
2594
  handle.status = "killed";
2459
2595
  handle.updatedAt = Date.now();
2596
+ mintCompletionId(handle);
2460
2597
  this.pokeBgQuiescence(handle.owner);
2461
2598
  return {
2462
2599
  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 },
2600
+ details: {
2601
+ task_id: handle.id,
2602
+ type: "background_bash",
2603
+ status: "killed",
2604
+ retrieval_status: "success",
2605
+ stoppedBy: handle.stoppedBy,
2606
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
2607
+ },
2464
2608
  };
2465
2609
  }
2466
2610
  const restored = handle.onTerminal !== undefined;
@@ -2471,16 +2615,25 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
2471
2615
  content: `Error (TaskStop): ${r.error.message} — the kill did not land; the process may still be running.` +
2472
2616
  (restored ? " Its watcher keeps running: TaskOutput still works and the completion notification will still fire." : " TaskOutput still reflects its live status."),
2473
2617
  details: { task_id: handle.id, type: "background_bash", status: handle.status, retrieval_status, error: r.error.code },
2618
+ isError: true,
2474
2619
  };
2475
2620
  }
2476
2621
  if (handle.stoppedBy === undefined)
2477
2622
  handle.stoppedBy = handle.stopSource ?? "parent";
2478
2623
  handle.status = "killed";
2479
2624
  handle.updatedAt = Date.now();
2625
+ mintCompletionId(handle);
2480
2626
  this.pokeBgQuiescence(handle.owner);
2481
2627
  return {
2482
2628
  content: `Terminated ${handle.id}.`,
2483
- details: { task_id: handle.id, type: "background_bash", status: "killed", retrieval_status: "success", stoppedBy: handle.stoppedBy },
2629
+ details: {
2630
+ task_id: handle.id,
2631
+ type: "background_bash",
2632
+ status: "killed",
2633
+ retrieval_status: "success",
2634
+ stoppedBy: handle.stoppedBy,
2635
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
2636
+ },
2484
2637
  };
2485
2638
  }
2486
2639
  async pollMonitor(handle, filter, deadline, signal) {
@@ -2493,6 +2646,7 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
2493
2646
  return {
2494
2647
  content: `Error (TaskOutput): invalid filter regex: ${filter}`,
2495
2648
  details: { task_id: handle.id, type: "monitor", status: handle.status, retrieval_status: "not_ready" },
2649
+ isError: true,
2496
2650
  };
2497
2651
  }
2498
2652
  }
@@ -2503,9 +2657,17 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
2503
2657
  const shape = (text) => clipTaskOutput(rx ? text.split(/\r?\n/).filter((l) => rx.test(l)).join("\n") : text, handle.outputFile);
2504
2658
  const running = handle.status === "running";
2505
2659
  const retrieval = running && deadline !== undefined && Date.now() >= deadline ? "timeout" : "success";
2506
- const displayStatus = spool.exitCode !== undefined ? `exited(code ${spool.exitCode})` : running ? "running" : handle.status;
2660
+ const timedOutClause = spool.timedOut === true ? ` (timed out after ${spool.timeoutSec !== undefined ? `${spool.timeoutSec}s` : "its background time budget"})` : "";
2661
+ const displayStatus = (spool.exitCode !== undefined ? `exited(code ${spool.exitCode})` : running ? "running" : handle.status) + timedOutClause;
2507
2662
  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` +
2663
+ const droppedNote = (spool.droppedBytes ?? 0) > 0
2664
+ ? `[!] ${spool.droppedBytes} earlier byte(s) permanently dropped before this window (8MB buffer evicted)\n`
2665
+ : spool.dropUnknown === true
2666
+ ? "[!] an unknown amount of earlier output was permanently dropped before this window (buffer evicted)\n"
2667
+ : "";
2668
+ const drainNote = spool.drainFailed === true ? "[!] the final output drain at stop failed — the tail of the output may be missing\n" : "";
2669
+ const stdoutHeading = droppedNote !== "" || drainNote !== "" ? "stdout (re-readable spool — INCOMPLETE, see note above)" : "stdout (full, re-readable)";
2670
+ const body = `status: ${displayStatus}\n${rolledNote}${droppedNote}${drainNote}--- ${stdoutHeading} ---\n${shape(spool.stdout) || "(none)"}\n` +
2509
2671
  `--- stderr ---\n${shape(spool.stderr) || "(none)"}`;
2510
2672
  return {
2511
2673
  content: delimitUntrusted(`TaskOutput ${handle.id}`, body),
@@ -2515,21 +2677,66 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
2515
2677
  status: handle.status,
2516
2678
  retrieval_status: retrieval,
2517
2679
  ...(handle.status === "killed" && handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
2518
- details: { status: running ? "running" : handle.status, exitCode: spool.exitCode },
2680
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
2681
+ details: {
2682
+ status: running ? "running" : handle.status,
2683
+ exitCode: spool.exitCode,
2684
+ ...(spool.droppedBytes !== undefined ? { bytesDroppedBeforeCursor: spool.droppedBytes } : {}),
2685
+ ...(spool.timedOut === true ? { timedOut: true, ...(spool.timeoutSec !== undefined ? { timeoutSec: spool.timeoutSec } : {}) } : {}),
2686
+ },
2519
2687
  },
2520
2688
  };
2521
2689
  }
2522
2690
  async stopMonitor(handle) {
2691
+ const wasRunning = handle.status === "running";
2523
2692
  if (handle.watcher !== undefined) {
2524
2693
  handle.timers.clearInterval(handle.watcher);
2525
2694
  handle.watcher = undefined;
2526
2695
  }
2696
+ if (wasRunning && handle.tickInFlight !== true) {
2697
+ try {
2698
+ const drained = await handle.env.pollBackground(handle.shellId);
2699
+ if (drained.ok) {
2700
+ const lines = this.absorbMonitorPoll(handle, drained.value);
2701
+ const finalLines = handle.lineBuf !== "" ? [...lines, handle.lineBuf] : lines;
2702
+ handle.lineBuf = "";
2703
+ if (finalLines.length > 0 && handle.status === "running") {
2704
+ handle.seq += 1;
2705
+ handle.updatedAt = Date.now();
2706
+ this.emitMonitorEvent(handle, {
2707
+ task_id: handle.id,
2708
+ task_type: "monitor",
2709
+ ...(handle.toolUseId !== undefined ? { toolUseId: handle.toolUseId } : {}),
2710
+ status: "event",
2711
+ seq: handle.seq,
2712
+ lines: finalLines,
2713
+ summary: `${(handle.description ?? "monitor").slice(0, 200)} — ${finalLines.length} final output line(s) drained at stop${firstDropNote(handle.spool)}`,
2714
+ result: clipMonitorEvent(finalLines.join("\n")),
2715
+ });
2716
+ }
2717
+ }
2718
+ else {
2719
+ handle.spool.drainFailed = true;
2720
+ }
2721
+ }
2722
+ catch {
2723
+ handle.spool.drainFailed = true;
2724
+ }
2725
+ }
2726
+ const drainNote = handle.spool.drainFailed === true ? " (the final output drain failed — TaskOutput may be missing the last window)" : "";
2527
2727
  this.markStopSource(handle.id, "parent");
2528
2728
  const r = await handle.env.killBackground(handle.shellId);
2529
2729
  if (handle.status !== "running") {
2530
2730
  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 } : {}) },
2731
+ content: wasRunning ? `Terminated ${handle.id}.${drainNote}` : alreadyTerminalStopNote(handle.id, handle.status),
2732
+ details: {
2733
+ task_id: handle.id,
2734
+ type: "monitor",
2735
+ status: handle.status,
2736
+ retrieval_status: "success",
2737
+ ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
2738
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
2739
+ },
2533
2740
  };
2534
2741
  }
2535
2742
  if (!r.ok) {
@@ -2538,10 +2745,18 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
2538
2745
  handle.stoppedBy = handle.stopSource ?? "parent";
2539
2746
  handle.status = "killed";
2540
2747
  handle.updatedAt = Date.now();
2748
+ mintCompletionId(handle);
2541
2749
  this.pokeBgQuiescence(handle.owner);
2542
2750
  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 },
2751
+ content: `Terminated ${handle.id} (the process was already gone).${drainNote}`,
2752
+ details: {
2753
+ task_id: handle.id,
2754
+ type: "monitor",
2755
+ status: "killed",
2756
+ retrieval_status: "success",
2757
+ stoppedBy: handle.stoppedBy,
2758
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
2759
+ },
2545
2760
  };
2546
2761
  }
2547
2762
  if (handle.watcher === undefined)
@@ -2549,6 +2764,7 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
2549
2764
  const retrieval_status = r.error.code === "timeout" ? "timeout" : "not_ready";
2550
2765
  return {
2551
2766
  content: `Error (TaskStop): ${r.error.message} — the kill did not land; the process may still be running. The watch keeps running: events and the terminal notification still fire.`,
2767
+ isError: true,
2552
2768
  details: { task_id: handle.id, type: "monitor", status: handle.status, retrieval_status, error: r.error.code },
2553
2769
  };
2554
2770
  }
@@ -2557,16 +2773,18 @@ ${clipTaskOutput(row.finalOutputFull ?? row.finalOutput)}` : "(no result text)"}
2557
2773
  handle.stoppedBy = handle.stopSource ?? "parent";
2558
2774
  handle.status = "killed";
2559
2775
  handle.updatedAt = Date.now();
2776
+ mintCompletionId(handle);
2560
2777
  this.pokeBgQuiescence(handle.owner);
2561
2778
  }
2562
2779
  return {
2563
- content: `Terminated ${handle.id}.`,
2780
+ content: `Terminated ${handle.id}.${drainNote}`,
2564
2781
  details: {
2565
2782
  task_id: handle.id,
2566
2783
  type: "monitor",
2567
2784
  status: handle.status,
2568
2785
  retrieval_status: "success",
2569
2786
  ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
2787
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
2570
2788
  },
2571
2789
  };
2572
2790
  }
@@ -2606,10 +2824,12 @@ ${clipTaskOutput(handle.resultFull ?? handle.result, handle.outputFile)}` : "(no
2606
2824
  ...(handle.cycleSeq !== undefined ? { seq: handle.cycleSeq } : {}),
2607
2825
  ...(handle.status === "killed" && handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
2608
2826
  ...(handle.resultIsPartial ? { partial_result: true } : {}),
2827
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
2609
2828
  },
2610
2829
  };
2611
2830
  }
2612
2831
  async stopBackgroundAgent(handle) {
2832
+ const entryStatus = handle.status;
2613
2833
  if (handle.status === "parked") {
2614
2834
  const arbiter = handle.resolveParkedStop;
2615
2835
  if (arbiter === undefined) {
@@ -2632,6 +2852,7 @@ ${clipTaskOutput(handle.resultFull ?? handle.result, handle.outputFile)}` : "(no
2632
2852
  handle.status = "killed";
2633
2853
  handle.stoppedBy = handle.stopSource ?? "parent";
2634
2854
  handle.updatedAt = Date.now();
2855
+ mintCompletionId(handle);
2635
2856
  handle.parkedCheckpointToken = undefined;
2636
2857
  handle.resolveParkedStop = undefined;
2637
2858
  this.pokeBgQuiescence(handle.owner);
@@ -2657,7 +2878,12 @@ ${clipTaskOutput(handle.resultFull ?? handle.result, handle.outputFile)}` : "(no
2657
2878
  }
2658
2879
  }
2659
2880
  }
2660
- this.durableAgentWrite(handle, { status: "killed", settledAt: Date.now(), ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}) }, ["parkedCheckpointToken", "parkClaimId"]);
2881
+ this.durableAgentWrite(handle, {
2882
+ status: "killed",
2883
+ settledAt: Date.now(),
2884
+ ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
2885
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
2886
+ }, ["parkedCheckpointToken", "parkClaimId"]);
2661
2887
  return {
2662
2888
  content: `Terminated ${handle.id} (pending approval expired).`,
2663
2889
  details: {
@@ -2667,6 +2893,7 @@ ${clipTaskOutput(handle.resultFull ?? handle.result, handle.outputFile)}` : "(no
2667
2893
  retrieval_status: "success",
2668
2894
  ...(handle.cycleSeq !== undefined ? { seq: handle.cycleSeq } : {}),
2669
2895
  ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
2896
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
2670
2897
  },
2671
2898
  };
2672
2899
  }
@@ -2674,11 +2901,13 @@ ${clipTaskOutput(handle.resultFull ?? handle.result, handle.outputFile)}` : "(no
2674
2901
  return {
2675
2902
  content: `${handle.id} was not stopped: the approval arbitration store was unreachable — the row stays parked; retry the stop.`,
2676
2903
  details: { task_id: handle.id, type: "background_agent", status: handle.status, retrieval_status: "not_ready", error: "park_arbiter_unreachable" },
2904
+ isError: true,
2677
2905
  };
2678
2906
  }
2679
2907
  return {
2680
2908
  content: `${handle.id} was not stopped: its pending approval was already consumed (a resume won the arbitration).`,
2681
2909
  details: { task_id: handle.id, type: "background_agent", status: handle.status, retrieval_status: "not_ready", error: "park_resume_won" },
2910
+ isError: true,
2682
2911
  };
2683
2912
  }
2684
2913
  if (handle.status === "running") {
@@ -2687,6 +2916,7 @@ ${clipTaskOutput(handle.resultFull ?? handle.result, handle.outputFile)}` : "(no
2687
2916
  handle.status = "killed";
2688
2917
  handle.stoppedBy = handle.stopSource ?? "parent";
2689
2918
  handle.updatedAt = Date.now();
2919
+ mintCompletionId(handle);
2690
2920
  handle.notify = undefined;
2691
2921
  for (const [, , qResolve] of handle.preAttachQueue ?? [])
2692
2922
  qResolve?.({ ok: false, reason: "not_running" });
@@ -2699,10 +2929,11 @@ ${clipTaskOutput(handle.resultFull ?? handle.result, handle.outputFile)}` : "(no
2699
2929
  status: "killed",
2700
2930
  settledAt: Date.now(),
2701
2931
  ...(handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
2932
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
2702
2933
  });
2703
2934
  }
2704
2935
  return {
2705
- content: `Terminated ${handle.id}.`,
2936
+ content: entryStatus === "running" ? `Terminated ${handle.id}.` : alreadyTerminalStopNote(handle.id, handle.status),
2706
2937
  details: {
2707
2938
  task_id: handle.id,
2708
2939
  type: "background_agent",
@@ -2710,6 +2941,7 @@ ${clipTaskOutput(handle.resultFull ?? handle.result, handle.outputFile)}` : "(no
2710
2941
  retrieval_status: "success",
2711
2942
  ...(handle.cycleSeq !== undefined ? { seq: handle.cycleSeq } : {}),
2712
2943
  ...(handle.status === "killed" && handle.stoppedBy !== undefined ? { stoppedBy: handle.stoppedBy } : {}),
2944
+ ...(handle.completionId !== undefined ? { completionId: handle.completionId } : {}),
2713
2945
  },
2714
2946
  };
2715
2947
  }
@@ -2765,7 +2997,7 @@ export function createTaskOutputTool(opts) {
2765
2997
  const args = rawArgs;
2766
2998
  const id = firstString(args.task_id, args.bash_id, args.runId);
2767
2999
  if (!id)
2768
- return "Error (TaskOutput): Missing required parameter: task_id";
3000
+ return errorResult("Error (TaskOutput): Missing required parameter: task_id");
2769
3001
  const requestedMs = typeof args.timeout === "number" ? args.timeout : undefined;
2770
3002
  let effectiveTimeoutMs = requestedMs;
2771
3003
  let waitClampNote = "";
@@ -2792,7 +3024,11 @@ export function createTaskOutputTool(opts) {
2792
3024
  signal: ctx.signal,
2793
3025
  });
2794
3026
  const { type: taskType, ...rest } = r.details;
2795
- return { content: r.content + waitClampNote, details: { type: "task-output", ...(taskType !== undefined ? { taskType } : {}), ...(waitClampNote !== "" ? { waitClamped: true } : {}), ...rest } };
3027
+ return {
3028
+ content: r.content + waitClampNote,
3029
+ details: { type: "task-output", ...(taskType !== undefined ? { taskType } : {}), ...(waitClampNote !== "" ? { waitClamped: true } : {}), ...rest },
3030
+ ...(r.isError ? { isError: true } : {}),
3031
+ };
2796
3032
  },
2797
3033
  });
2798
3034
  }
@@ -2815,12 +3051,13 @@ export function createTaskStopTool(opts) {
2815
3051
  const args = rawArgs;
2816
3052
  const id = firstString(args.task_id, args.shell_id, args.runId);
2817
3053
  if (!id)
2818
- return "Missing required parameter: task_id";
3054
+ return errorResult("Missing required parameter: task_id");
2819
3055
  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
3056
  const { type: taskType, ...rest } = r.details;
2821
3057
  return {
2822
3058
  content: r.content,
2823
3059
  details: { type: "task-stop", message: r.content, task_type: taskType ?? "unknown", ...rest },
3060
+ ...(r.isError ? { isError: true } : {}),
2824
3061
  };
2825
3062
  },
2826
3063
  });