@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
@@ -340,18 +340,21 @@ export function createObserverReportToolSpec(opts) {
340
340
  return {
341
341
  content: "ObserverReport is only available to an observer agent; the main session does not have an observed pairing.",
342
342
  details: { success: false },
343
+ isError: true,
343
344
  };
344
345
  }
345
346
  if (pairing.state !== "armed") {
346
347
  return {
347
348
  content: "Your observer pairing is not armed (stopped, retired, or never installed). The report was not delivered.",
348
349
  details: { success: false },
350
+ isError: true,
349
351
  };
350
352
  }
351
353
  if (!pairing.observedRunning) {
352
354
  return {
353
355
  content: `The observed agent (${pairing.observedEnvelopeName}) is not running. The report was not delivered.`,
354
356
  details: { success: false },
357
+ isError: true,
355
358
  };
356
359
  }
357
360
  await opts.queueReport(frameObserverReport(pairing.observerAgentName, report));
@@ -650,6 +650,7 @@ function makeSubagentResume(deps) {
650
650
  }
651
651
  }
652
652
  const stoppedByRevive = status === "killed" ? deps.registry?.getStopAttribution(deps.taskId ?? "") ?? "system" : undefined;
653
+ const completionIdRevive = deps.registry?.getCompletionId(deps.taskId ?? "");
653
654
  const reviveName = deps.rowDescription ?? `sub-agent ${marker}`;
654
655
  reviveEmit?.({
655
656
  kind: "terminal",
@@ -664,6 +665,7 @@ function makeSubagentResume(deps) {
664
665
  summary: `Agent "${reviveName}" (resumed) ${status === "killed" ? "stopped" : child.status === "completed" ? "finished" : String(child.status)}${ccElapsedTag(Date.now() - reviveStartedAt)}`,
665
666
  resumable: status !== "killed",
666
667
  usage: { tokens: child.stats.tokens, turns: child.stats.turns, costMicroUsd: child.stats.costMicroUsd },
668
+ ...(completionIdRevive !== undefined ? { completionId: completionIdRevive } : {}),
667
669
  });
668
670
  const resumeFrame = {
669
671
  task_id: deps.taskId ?? entry.childSessionId,
@@ -679,6 +681,7 @@ function makeSubagentResume(deps) {
679
681
  ...resumeResidual(),
680
682
  resumable: status !== "killed",
681
683
  usage: { tokens: child.stats.tokens, turns: child.stats.turns, costMicroUsd: child.stats.costMicroUsd },
684
+ ...(completionIdRevive !== undefined ? { completionId: completionIdRevive } : {}),
682
685
  };
683
686
  const gateOwner = status === "completed" && deps.notify !== undefined && deps.registry !== undefined && ledger.get(deps.parentToolCallId) === entry
684
687
  ? entry.childSessionId
@@ -714,6 +717,7 @@ function makeSubagentResume(deps) {
714
717
  }
715
718
  }
716
719
  const stoppedByReject = abort.signal.aborted ? deps.registry?.getStopAttribution(deps.taskId ?? "") ?? "system" : undefined;
720
+ const completionIdRejectRevive = deps.registry?.getCompletionId(deps.taskId ?? "");
717
721
  const rejectName = deps.rowDescription ?? `sub-agent ${marker}`;
718
722
  reviveEmit?.({
719
723
  kind: "terminal",
@@ -726,6 +730,7 @@ function makeSubagentResume(deps) {
726
730
  seq: entry.cycleSeq,
727
731
  ...(stoppedByReject !== undefined ? { stoppedBy: stoppedByReject } : {}),
728
732
  summary: `Agent "${rejectName}" (resumed) ${abort.signal.aborted ? "stopped" : "failed"}${ccElapsedTag(Date.now() - reviveStartedAt)}`.slice(0, 300),
733
+ ...(completionIdRejectRevive !== undefined ? { completionId: completionIdRejectRevive } : {}),
729
734
  });
730
735
  try {
731
736
  deps.notify?.({
@@ -737,6 +742,7 @@ function makeSubagentResume(deps) {
737
742
  seq: entry.cycleSeq,
738
743
  ...(stoppedByReject !== undefined ? { stoppedBy: stoppedByReject } : {}),
739
744
  summary: `Agent "${rejectName}" (resumed) ${abort.signal.aborted ? "stopped" : `failed: ${e instanceof Error ? e.message : String(e)}`}${ccElapsedTag(Date.now() - reviveStartedAt)}`.slice(0, 300),
745
+ ...(completionIdRejectRevive !== undefined ? { completionId: completionIdRejectRevive } : {}),
740
746
  }, { priority: "later" });
741
747
  }
742
748
  catch {
@@ -1805,6 +1811,7 @@ function makeSubagentTool(opts, depth, excluded, extraToolsBudget) {
1805
1811
  ...(!okBg ? { error: reapedBg ? BG_AGENT_REAP_STOP_ERROR : child.errorMessage ?? String(child.status) } : {}),
1806
1812
  }) ?? (abort.signal.aborted ? "killed" : okBg ? "completed" : "failed");
1807
1813
  const stoppedByBg = settledBg === "killed" ? bg.registry.getStopAttribution(taskId) ?? "system" : undefined;
1814
+ const completionIdFork = bg.registry.getCompletionId(taskId);
1808
1815
  const stillbornFork = child.errorCode === "resume.session_not_found";
1809
1816
  if (stillbornFork) {
1810
1817
  try {
@@ -1829,6 +1836,7 @@ function makeSubagentTool(opts, depth, excluded, extraToolsBudget) {
1829
1836
  ...residualFork,
1830
1837
  resumable: resumableFork,
1831
1838
  usage: { tokens: child.stats.tokens, turns: child.stats.turns, costMicroUsd: (child.stats.costMicroUsd ?? 0) + (child.stats.nested?.costMicroUsd ?? 0) },
1839
+ ...(completionIdFork !== undefined ? { completionId: completionIdFork } : {}),
1832
1840
  });
1833
1841
  try {
1834
1842
  notify?.({
@@ -1844,6 +1852,7 @@ function makeSubagentTool(opts, depth, excluded, extraToolsBudget) {
1844
1852
  ...residualFork,
1845
1853
  resumable: resumableFork,
1846
1854
  usage: { tokens: child.stats.tokens, turns: child.stats.turns, costMicroUsd: (child.stats.costMicroUsd ?? 0) + (child.stats.nested?.costMicroUsd ?? 0) },
1855
+ ...(completionIdFork !== undefined ? { completionId: completionIdFork } : {}),
1847
1856
  }, { priority: "later" });
1848
1857
  }
1849
1858
  catch {
@@ -1858,6 +1867,7 @@ function makeSubagentTool(opts, depth, excluded, extraToolsBudget) {
1858
1867
  error: killed ? BG_AGENT_REAP_STOP_ERROR : e instanceof Error ? e.message : String(e),
1859
1868
  }) ?? (killed ? "killed" : "failed");
1860
1869
  const stoppedByBg = settledBg === "killed" ? bg.registry.getStopAttribution(taskId) ?? "system" : undefined;
1870
+ const completionIdForkReject = bg.registry.getCompletionId(taskId);
1861
1871
  const summaryBg = `${shortDesc} — ${settledBg === "failed" ? `failed: ${e instanceof Error ? e.message : String(e)}` : settledBg}`.slice(0, 300);
1862
1872
  sinkEmit({
1863
1873
  kind: "terminal",
@@ -1869,6 +1879,7 @@ function makeSubagentTool(opts, depth, excluded, extraToolsBudget) {
1869
1879
  status: settledBg,
1870
1880
  ...(stoppedByBg !== undefined ? { stoppedBy: stoppedByBg } : {}),
1871
1881
  summary: summaryBg,
1882
+ ...(completionIdForkReject !== undefined ? { completionId: completionIdForkReject } : {}),
1872
1883
  });
1873
1884
  try {
1874
1885
  notify?.({
@@ -1879,6 +1890,7 @@ function makeSubagentTool(opts, depth, excluded, extraToolsBudget) {
1879
1890
  sessionId: forkedId,
1880
1891
  ...(stoppedByBg !== undefined ? { stoppedBy: stoppedByBg } : {}),
1881
1892
  summary: summaryBg,
1893
+ ...(completionIdForkReject !== undefined ? { completionId: completionIdForkReject } : {}),
1882
1894
  }, { priority: "later" });
1883
1895
  }
1884
1896
  catch {
@@ -2390,6 +2402,7 @@ task_id: ${taskId}
2390
2402
  }
2391
2403
  }
2392
2404
  const stoppedBy = settled === "killed" ? bg.registry.getStopAttribution(taskId) ?? "system" : undefined;
2405
+ const completionIdBg = bg.registry.getCompletionId(taskId);
2393
2406
  const observerNote = observerNoteFor(await startBoundedObserverDrain());
2394
2407
  const residual = residualFields();
2395
2408
  const resumableBg = bgRetain !== undefined && settled !== "killed";
@@ -2406,6 +2419,7 @@ task_id: ${taskId}
2406
2419
  ...residual,
2407
2420
  resumable: resumableBg,
2408
2421
  usage: { tokens: child.stats.tokens, turns: child.stats.turns, costMicroUsd: (child.stats.costMicroUsd ?? 0) + (child.stats.nested?.costMicroUsd ?? 0) },
2422
+ ...(completionIdBg !== undefined ? { completionId: completionIdBg } : {}),
2409
2423
  });
2410
2424
  const completionFrame = {
2411
2425
  task_id: taskId,
@@ -2421,6 +2435,7 @@ task_id: ${taskId}
2421
2435
  ...residual,
2422
2436
  resumable: resumableBg,
2423
2437
  usage: { tokens: child.stats.tokens, turns: child.stats.turns, costMicroUsd: (child.stats.costMicroUsd ?? 0) + (child.stats.nested?.costMicroUsd ?? 0) },
2438
+ ...(completionIdBg !== undefined ? { completionId: completionIdBg } : {}),
2424
2439
  };
2425
2440
  const gateOwner = settled === "completed" && notify !== undefined ? bgRetain?.childSessionId : undefined;
2426
2441
  const aliveBgCount = gateOwner !== undefined ? bg.registry.countRunningBackgroundForOwner(gateOwner) : 0;
@@ -2465,6 +2480,7 @@ task_id: ${taskId}
2465
2480
  }
2466
2481
  }
2467
2482
  const stoppedBy = settled === "killed" ? bg.registry.getStopAttribution(taskId) ?? "system" : undefined;
2483
+ const completionIdBgReject = bg.registry.getCompletionId(taskId);
2468
2484
  const observerNote = observerNoteFor(await startBoundedObserverDrain());
2469
2485
  sinkEmit({
2470
2486
  kind: "terminal",
@@ -2477,6 +2493,7 @@ task_id: ${taskId}
2477
2493
  ...(seqAtSettle !== undefined ? { seq: seqAtSettle } : {}),
2478
2494
  ...(stoppedBy !== undefined ? { stoppedBy } : {}),
2479
2495
  summary: `${settled === "failed" ? `Agent "${shortDesc}" failed: ${msg}${ccElapsedTag(Date.now() - bgStartedAt)}` : ccCompletionText(shortDesc, settled, settled, Date.now() - bgStartedAt)}${observerNote}`.slice(0, 300),
2496
+ ...(completionIdBgReject !== undefined ? { completionId: completionIdBgReject } : {}),
2480
2497
  });
2481
2498
  try {
2482
2499
  notify?.({
@@ -2488,6 +2505,7 @@ task_id: ${taskId}
2488
2505
  ...(seqAtSettle !== undefined ? { seq: seqAtSettle } : {}),
2489
2506
  ...(stoppedBy !== undefined ? { stoppedBy } : {}),
2490
2507
  summary: `${settled === "failed" ? `Agent "${shortDesc}" failed: ${msg}${ccElapsedTag(Date.now() - bgStartedAt)}` : ccCompletionText(shortDesc, settled, settled, Date.now() - bgStartedAt)}${observerNote}`.slice(0, 300),
2508
+ ...(completionIdBgReject !== undefined ? { completionId: completionIdBgReject } : {}),
2491
2509
  }, { priority: "later" });
2492
2510
  }
2493
2511
  catch {
@@ -2736,15 +2754,15 @@ export function createSendMessageTool(opts) {
2736
2754
  const to = String(a.to ?? "").trim();
2737
2755
  const message = String(a.message ?? "").trim();
2738
2756
  if (!to)
2739
- return { content: "Message not sent: 'to' was empty. Pass the agent's task_id (a…).", details: { error: "empty to" } };
2757
+ return { content: "Message not sent: 'to' was empty. Pass the agent's task_id (a…).", details: { error: "empty to" }, isError: true };
2740
2758
  if (!message)
2741
- return { content: "Message not sent: 'message' was empty.", details: { error: "empty message" } };
2759
+ return { content: "Message not sent: 'message' was empty.", details: { error: "empty message" }, isError: true };
2742
2760
  const senderId = ctx.taskId ?? opts.owner;
2743
2761
  if (senderId !== undefined && isObserverTaskId(senderId)) {
2744
- return { content: OBSERVER_SENDMESSAGE_SENDER_REFUSAL, details: { error: "observer_sender" } };
2762
+ return { content: OBSERVER_SENDMESSAGE_SENDER_REFUSAL, details: { error: "observer_sender" }, isError: true };
2745
2763
  }
2746
2764
  if (isObserverTaskId(to)) {
2747
- return { content: OBSERVER_SENDMESSAGE_TARGET_REFUSAL, details: { error: "observer_target", to } };
2765
+ return { content: OBSERVER_SENDMESSAGE_TARGET_REFUSAL, details: { error: "observer_target", to }, isError: true };
2748
2766
  }
2749
2767
  if (normalizeAgentName(to) === "main") {
2750
2768
  if (opts.uplink && senderId !== undefined) {
@@ -2761,7 +2779,7 @@ export function createSendMessageTool(opts) {
2761
2779
  }, { priority: "next" });
2762
2780
  }
2763
2781
  catch (e) {
2764
- return { content: `Message not sent: the parent's notification lane rejected it (${e instanceof Error ? e.message : String(e)}).`, details: { error: "uplink_failed", to } };
2782
+ return { content: `Message not sent: the parent's notification lane rejected it (${e instanceof Error ? e.message : String(e)}).`, details: { error: "uplink_failed", to }, isError: true };
2765
2783
  }
2766
2784
  return {
2767
2785
  content: `Message sent to main — it will reach the spawning conversation at its next turn boundary. Continue with your task; do not wait for a reply.`,
@@ -2772,6 +2790,7 @@ export function createSendMessageTool(opts) {
2772
2790
  content: `Message not sent: "main" (the spawning conversation) is not a deliverable target here. ` +
2773
2791
  `Your completion is reported to it automatically — finish your task and your final report will be relayed.`,
2774
2792
  details: { error: "main_not_deliverable", to },
2793
+ isError: true,
2775
2794
  };
2776
2795
  }
2777
2796
  const access = {
@@ -2808,12 +2827,14 @@ export function createSendMessageTool(opts) {
2808
2827
  return {
2809
2828
  content: `Message not sent: ${whoT3} was stopped${row.stoppedBy !== undefined ? ` (by ${row.stoppedBy})` : ""} — a stopped agent is not revived. Launch a new agent with the needed context instead.`,
2810
2829
  details: { error: "killed", to, ...(row.stoppedBy !== undefined ? { stoppedBy: row.stoppedBy } : {}) },
2830
+ isError: true,
2811
2831
  };
2812
2832
  }
2813
2833
  if (row.status === "parked") {
2814
2834
  return {
2815
2835
  content: `Message not sent: ${whoT3} is parked on a pending approval — it resumes when the approval is decided (durable approval inbox), not by message delivery. Send again after it resumes.`,
2816
2836
  details: { error: "parked_pending_approval", to },
2837
+ isError: true,
2817
2838
  };
2818
2839
  }
2819
2840
  if (row.status === "running") {
@@ -2821,17 +2842,20 @@ export function createSendMessageTool(opts) {
2821
2842
  return {
2822
2843
  content: `Message not sent: ${whoT3} is currently running in this process but its mid-run delivery channel is not reachable from here. Wait for its completion notification, then send again to continue it.`,
2823
2844
  details: { error: "still_running", to },
2845
+ isError: true,
2824
2846
  };
2825
2847
  }
2826
2848
  if (Date.now() - row.updatedAt > 10 * DURABLE_AGENT_HEARTBEAT_MS) {
2827
2849
  return {
2828
2850
  content: `Message not sent: ${whoT3} last ran on another host that has stopped renewing its record (no heartbeat). Its row will be settled by the stale-row sweep — send again after that to revive it.`,
2829
2851
  details: { error: "host_gone", to },
2852
+ isError: true,
2830
2853
  };
2831
2854
  }
2832
2855
  return {
2833
2856
  content: `Message not sent: ${whoT3} is running on another host — this process has no delivery channel to it. Wait for its completion notification, then send again to continue it.`,
2834
2857
  details: { error: "running_elsewhere", to },
2858
+ isError: true,
2835
2859
  };
2836
2860
  }
2837
2861
  if (row.sessionId === undefined)
@@ -2840,6 +2864,7 @@ export function createSendMessageTool(opts) {
2840
2864
  return {
2841
2865
  content: `Message not sent: ${whoT3} is being revived or recycled right now — send again in a moment.`,
2842
2866
  details: { error: "claim_contended", to },
2867
+ isError: true,
2843
2868
  };
2844
2869
  }
2845
2870
  try {
@@ -2873,11 +2898,13 @@ export function createSendMessageTool(opts) {
2873
2898
  return {
2874
2899
  content: `Message not sent: ${whoT3} was just revived by another delivery — it is running now. Send again to reach the running agent.`,
2875
2900
  details: { error: "claim_lost", to },
2901
+ isError: true,
2876
2902
  };
2877
2903
  }
2878
2904
  return {
2879
2905
  content: `Message not sent: ${whoT3}'s durable record changed underneath this delivery — send again.`,
2880
2906
  details: { error: "claim_lost", to },
2907
+ isError: true,
2881
2908
  };
2882
2909
  }
2883
2910
  const claimedRev = row.rev + 1;
@@ -2897,11 +2924,13 @@ export function createSendMessageTool(opts) {
2897
2924
  return {
2898
2925
  content: `Message not sent: ${whoT3}'s transcript session no longer exists (evicted or reaped) — it cannot be revived. Launch a new agent with the needed context instead.`,
2899
2926
  details: { error: "resume.session_not_found", to },
2927
+ isError: true,
2900
2928
  };
2901
2929
  }
2902
2930
  return {
2903
2931
  content: `Message not sent: ${whoT3}'s transcript store did not answer (${e instanceof Error ? e.message : String(e)}) — nothing was parked; send again.`,
2904
2932
  details: { error: "session_store_unavailable", to },
2933
+ isError: true,
2905
2934
  };
2906
2935
  }
2907
2936
  const clipped = message.length > UPLINK_RESULT_MAX ? `${message.slice(0, UPLINK_RESULT_MAX)}\n[message truncated: ${message.length} chars total]` : message;
@@ -2915,6 +2944,7 @@ export function createSendMessageTool(opts) {
2915
2944
  return {
2916
2945
  content: `Message not sent: the durable mailbox refused the message (${e instanceof Error ? e.message : String(e)}) — nothing was parked and the agent was not revived.`,
2917
2946
  details: { error: "mailbox_failed", to },
2947
+ isError: true,
2918
2948
  };
2919
2949
  }
2920
2950
  const leaseOwner = `${opts.registry.writerId}:${ctx.toolCallId}`;
@@ -2954,7 +2984,7 @@ export function createSendMessageTool(opts) {
2954
2984
  };
2955
2985
  }
2956
2986
  try {
2957
- await opts.mailbox.ack(scope, handle, lease.maxSeq);
2987
+ await opts.mailbox.ack(scope, handle, leaseOwner, lease.maxSeq);
2958
2988
  }
2959
2989
  catch {
2960
2990
  }
@@ -2996,7 +3026,7 @@ export function createSendMessageTool(opts) {
2996
3026
  }
2997
3027
  }
2998
3028
  if (byName.status === "ambiguous") {
2999
- return { content: `Message not sent: ${byName.message}`, details: { error: "ambiguous", to } };
3029
+ return { content: `Message not sent: ${byName.message}`, details: { error: "ambiguous", to }, isError: true };
3000
3030
  }
3001
3031
  if (byName.status === "found") {
3002
3032
  const h = byName.handle;
@@ -3019,6 +3049,7 @@ export function createSendMessageTool(opts) {
3019
3049
  content: `Message not sent: "${to}" is on the durable roster (agent ${rosterHit.agentId}) but not reachable in this run — its session belongs to an earlier process. ` +
3020
3050
  `Launch a new agent with the needed context instead.`,
3021
3051
  details: { error: "roster_only", to, agentId: rosterHit.agentId },
3052
+ isError: true,
3022
3053
  };
3023
3054
  }
3024
3055
  const labels = opts.registry.runningBackgroundAgentLabels(access);
@@ -3028,16 +3059,17 @@ export function createSendMessageTool(opts) {
3028
3059
  (labels.length > 0 ? ` Running background agents: ${labels.join(", ")}.` : "") +
3029
3060
  ` Note: a completed foreground agent is not resumable (its transcript is not retained) — spawn with run_in_background to keep an agent addressable, or launch a new agent.`,
3030
3061
  details: { error: "not_found", to, ...(byName.suggestion !== undefined ? { suggestion: byName.suggestion } : {}) },
3062
+ isError: true,
3031
3063
  };
3032
3064
  }
3033
3065
  }
3034
3066
  const row = target;
3035
3067
  if (row === undefined)
3036
- return { content: `Message not sent: no agent named "${to}" is reachable.`, details: { error: "not_found", to } };
3068
+ return { content: `Message not sent: no agent named "${to}" is reachable.`, details: { error: "not_found", to }, isError: true };
3037
3069
  const targetId = row.task_id;
3038
3070
  const who = targetId === to ? `agent ${targetId}` : `agent "${to}" (${targetId})`;
3039
3071
  if (row.type !== "background_agent") {
3040
- return { content: `Message not sent: ${targetId} is a ${row.type} task, not a background agent.`, details: { error: "wrong_type", to } };
3072
+ return { content: `Message not sent: ${targetId} is a ${row.type} task, not a background agent.`, details: { error: "wrong_type", to }, isError: true };
3041
3073
  }
3042
3074
  if (row.status === "running" || row.status === "pending") {
3043
3075
  const s2Summary = typeof a.summary === "string" && a.summary.trim() !== "" ? a.summary.trim().slice(0, 200) : undefined;
@@ -3066,11 +3098,13 @@ export function createSendMessageTool(opts) {
3066
3098
  return {
3067
3099
  content: `Message not sent: ${who} is still running and this deployment has no mid-run delivery channel for it. Wait for its completion notification, then SendMessage to continue it.`,
3068
3100
  details: { error: "still_running", to },
3101
+ isError: true,
3069
3102
  };
3070
3103
  }
3071
3104
  return {
3072
3105
  content: `Message not sent: ${who} just finished (delivery raced its completion). Send again to continue it from its transcript.`,
3073
3106
  details: { error: "settle_race", to },
3107
+ isError: true,
3074
3108
  };
3075
3109
  }
3076
3110
  if (row.status === "killed") {
@@ -3078,6 +3112,7 @@ export function createSendMessageTool(opts) {
3078
3112
  return {
3079
3113
  content: `Message not sent: ${who} was stopped${by ? ` (by ${by})` : ""} — a stopped agent is not resumable. Launch a new agent with the needed context instead.`,
3080
3114
  details: { error: "killed", to, ...(by !== undefined ? { stoppedBy: by } : {}) },
3115
+ isError: true,
3081
3116
  };
3082
3117
  }
3083
3118
  const runLedger = ctx.subagentRetain ?? opts.retain;
@@ -3096,6 +3131,7 @@ export function createSendMessageTool(opts) {
3096
3131
  return {
3097
3132
  content: `Message not sent: ${who}'s session was not retained (this run did not enable retainSubagentSessions), so it cannot be continued — relaunch a new agent with the needed context instead.`,
3098
3133
  details: { error: "not_retained", to },
3134
+ isError: true,
3099
3135
  };
3100
3136
  }
3101
3137
  const resume = makeSubagentResume({
@@ -3144,7 +3180,7 @@ export function createSendMessageTool(opts) {
3144
3180
  : code === "resume.row_gone"
3145
3181
  ? `${who}'s registry row no longer exists (terminal GC) — relaunch a new agent instead.`
3146
3182
  : `${e instanceof Error ? e.message : String(e)}`;
3147
- return { content: `Message not sent: ${text}`, details: { error: code ?? "resume_failed", to } };
3183
+ return { content: `Message not sent: ${text}`, details: { error: code ?? "resume_failed", to }, isError: true };
3148
3184
  }
3149
3185
  },
3150
3186
  });
@@ -3169,7 +3205,7 @@ export function createAgentTranscriptTool(opts) {
3169
3205
  const a = args;
3170
3206
  const id = String(a.id ?? "").trim();
3171
3207
  if (!id)
3172
- return { content: "No transcript read: 'id' was empty. Pass the agent's task_id (a…).", details: { error: "empty id" } };
3208
+ return { content: "No transcript read: 'id' was empty. Pass the agent's task_id (a…).", details: { error: "empty id" }, isError: true };
3173
3209
  const lastN = typeof a.lastN === "number" && Number.isFinite(a.lastN) ? Math.max(1, Math.min(AGENT_TRANSCRIPT_MAX_N, Math.floor(a.lastN))) : AGENT_TRANSCRIPT_DEFAULT_N;
3174
3210
  const access = {
3175
3211
  owner: ctx.taskId ?? opts.owner,
@@ -3214,12 +3250,13 @@ export function createAgentTranscriptTool(opts) {
3214
3250
  return {
3215
3251
  content: `No transcript: "${id}" is a WORKFLOW run id — AgentTranscript reads sub-agent transcripts only. For the run's per-agent rows (which agent failed, why, elapsed) call TaskOutput({ task_id: "${id}" }); to continue a fixed script, re-invoke the workflow with resumeFromRunId: "${id}".`,
3216
3252
  details: { error: "workflow_id", id },
3253
+ isError: true,
3217
3254
  };
3218
3255
  }
3219
3256
  const durable = await readDurableSteps();
3220
3257
  if (durable)
3221
3258
  return durable;
3222
- return { content: `No transcript: no background agent task "${id}" (unknown id, not yours, or expired).`, details: { error: "not_found", id } };
3259
+ return { content: `No transcript: no background agent task "${id}" (unknown id, not yours, or expired).`, details: { error: "not_found", id }, isError: true };
3223
3260
  }
3224
3261
  const runLedger = ctx.subagentRetain ?? undefined;
3225
3262
  const atLedgerSessionId = ctx.sessionId ?? opts.sessionId;
@@ -3232,6 +3269,7 @@ export function createAgentTranscriptTool(opts) {
3232
3269
  return {
3233
3270
  content: `No transcript: agent ${id}'s session was not retained (this run did not enable retainSubagentSessions), so its history is unavailable. Its completion notification carried the recent steps.`,
3234
3271
  details: { error: "not_retained", id },
3272
+ isError: true,
3235
3273
  };
3236
3274
  }
3237
3275
  let steps;
@@ -3246,6 +3284,7 @@ export function createAgentTranscriptTool(opts) {
3246
3284
  return {
3247
3285
  content: `No transcript: agent ${id}'s session could not be read (${code ?? (e instanceof Error ? e.message : String(e))}).`,
3248
3286
  details: { error: code ?? "read_failed", id },
3287
+ isError: true,
3249
3288
  };
3250
3289
  }
3251
3290
  if (steps.length === 0) {
@@ -1,5 +1,5 @@
1
1
  import { Type } from "typebox";
2
- import { defineTool } from "./tools.js";
2
+ import { defineTool, errorResult } from "./tools.js";
3
3
  import { delimitUntrusted, inlineUntrusted } from "./untrusted-text.js";
4
4
  export const ASK_USER_QUESTION_TOOL_NAME = "AskUserQuestion";
5
5
  const NO_HUMAN = "No human is available to answer right now. Proceed with your best judgment: pick the most reasonable " +
@@ -66,23 +66,23 @@ export function createAskUserQuestionTool(onQuestion, source) {
66
66
  execute: async (args, ctx) => {
67
67
  const { questions } = args;
68
68
  if (!Array.isArray(questions) || questions.length < 1 || questions.length > 4) {
69
- return "Error (AskUserQuestion): provide between 1 and 4 questions.";
69
+ return errorResult("Error (AskUserQuestion): provide between 1 and 4 questions.");
70
70
  }
71
71
  const seenHeaders = new Set();
72
72
  for (const q of questions) {
73
73
  if (!Array.isArray(q.options) || q.options.length < 2 || q.options.length > 4) {
74
- return `Error (AskUserQuestion): the question "${q.header || q.question}" must have between 2 and 4 options.`;
74
+ return errorResult(`Error (AskUserQuestion): the question "${q.header || q.question}" must have between 2 and 4 options.`);
75
75
  }
76
76
  const key = typeof q.header === "string" ? q.header.trim() : "";
77
77
  if (key === "" || seenHeaders.has(key)) {
78
- return `Error (AskUserQuestion): each question needs a distinct, non-empty header (got a duplicate or empty one).`;
78
+ return errorResult(`Error (AskUserQuestion): each question needs a distinct, non-empty header (got a duplicate or empty one).`);
79
79
  }
80
80
  seenHeaders.add(key);
81
81
  const seenLabels = new Set();
82
82
  for (const o of q.options) {
83
83
  const labelKey = typeof o?.label === "string" ? o.label.trim() : "";
84
84
  if (seenLabels.has(labelKey)) {
85
- return `Error (AskUserQuestion): the question "${q.header || q.question}" has duplicate option labels — option labels must be unique within each question.`;
85
+ return errorResult(`Error (AskUserQuestion): the question "${q.header || q.question}" has duplicate option labels — option labels must be unique within each question.`);
86
86
  }
87
87
  seenLabels.add(labelKey);
88
88
  }
@@ -28,6 +28,7 @@ export interface BackgroundAgentRecord {
28
28
  settledAt?: number;
29
29
  status: "running" | "parked" | "completed" | "failed" | "killed";
30
30
  stoppedBy?: string;
31
+ completionId?: string;
31
32
  seq?: number;
32
33
  parkedCheckpointToken?: string;
33
34
  parkedAt?: number;
@@ -66,6 +67,7 @@ export declare function canAccessAgentRecord(record: Pick<BackgroundAgentRecord,
66
67
  scope?: string;
67
68
  sessionId?: string;
68
69
  }): boolean;
70
+ export declare const STALE_RUNNING_REAP_ATTRIBUTION = "the host process was interrupted while this agent was running \u2014 its in-process state was lost. Check its worktree / output file for partial work before assuming the task landed (stale running row reaped).";
69
71
  export interface BackgroundAgentStore {
70
72
  put(record: BackgroundAgentRecord): Promise<void>;
71
73
  get(handle: string, scope: string): Promise<BackgroundAgentRecord | null>;
@@ -1,3 +1,4 @@
1
+ import { uuidv7 } from "../internal/harness.js";
1
2
  export class BackgroundAgentStoreError extends Error {
2
3
  code;
3
4
  constructor(code, message) {
@@ -20,6 +21,7 @@ export function canAccessAgentRecord(record, access) {
20
21
  }
21
22
  return false;
22
23
  }
24
+ export const STALE_RUNNING_REAP_ATTRIBUTION = "the host process was interrupted while this agent was running — its in-process state was lost. Check its worktree / output file for partial work before assuming the task landed (stale running row reaped).";
23
25
  export async function reconcileParkedAgents(stores, scope, now, opts) {
24
26
  const out = { failed: 0, rolledBack: 0 };
25
27
  const excluded = (row) => (opts?.excludeHandles?.has(row.handle) ?? false) ||
@@ -31,6 +33,7 @@ export async function reconcileParkedAgents(stores, scope, now, opts) {
31
33
  next.stoppedBy = "system";
32
34
  next.settledAt = now;
33
35
  next.updatedAt = now;
36
+ next.completionId ??= uuidv7();
34
37
  delete next.parkedCheckpointToken;
35
38
  delete next.parkClaimId;
36
39
  delete next.parkedAt;
@@ -248,7 +251,8 @@ export class InMemoryBackgroundAgentStore {
248
251
  const flipped = structuredClone(r);
249
252
  flipped.status = "failed";
250
253
  flipped.stoppedBy = "system";
251
- flipped.summary = flipped.summary ?? "host process interrupted (stale running row reaped)";
254
+ flipped.summary = flipped.summary ?? STALE_RUNNING_REAP_ATTRIBUTION;
255
+ flipped.error = flipped.error ?? STALE_RUNNING_REAP_ATTRIBUTION;
252
256
  flipped.settledAt = now;
253
257
  flipped.updatedAt = now;
254
258
  flipped.rev = r.rev + 1;
@@ -1,9 +1,11 @@
1
+ import { StringDecoder } from "node:string_decoder";
1
2
  export declare const MAX_EXEC_OUTPUT_BYTES: number;
2
3
  export declare class RollingTailBuffer {
3
4
  private readonly maxBytes;
4
5
  private readonly chunks;
5
6
  private size;
6
- private dropped;
7
+ private headDropped;
8
+ private sourceSkipped;
7
9
  constructor(maxBytes?: number);
8
10
  push(chunk: Buffer): void;
9
11
  recordSkippedBytes(n: number): void;
@@ -11,5 +13,20 @@ export declare class RollingTailBuffer {
11
13
  text: string;
12
14
  droppedBytes: number;
13
15
  };
16
+ bytes(): Buffer;
17
+ headDroppedBytes(): number;
18
+ sourceSkippedBytes(): number;
14
19
  }
20
+ export interface StreamCursorState {
21
+ tail: RollingTailBuffer;
22
+ cursorBytes: number;
23
+ acceptedBytes: number;
24
+ disclosedSkippedBytes: number;
25
+ decoder: StringDecoder;
26
+ }
27
+ export declare function newStreamCursorState(tail?: RollingTailBuffer): StreamCursorState;
28
+ export declare function sliceStreamIncrement(s: StreamCursorState, terminal: boolean): {
29
+ inc: string;
30
+ droppedBeforeCursor: number;
31
+ };
15
32
  export declare function markTruncated(text: string, droppedBytes: number): string;
@@ -1,9 +1,11 @@
1
+ import { StringDecoder } from "node:string_decoder";
1
2
  export const MAX_EXEC_OUTPUT_BYTES = 8 * 1024 * 1024;
2
3
  export class RollingTailBuffer {
3
4
  maxBytes;
4
5
  chunks = [];
5
6
  size = 0;
6
- dropped = 0;
7
+ headDropped = 0;
8
+ sourceSkipped = 0;
7
9
  constructor(maxBytes = MAX_EXEC_OUTPUT_BYTES) {
8
10
  this.maxBytes = maxBytes;
9
11
  }
@@ -18,22 +20,53 @@ export class RollingTailBuffer {
18
20
  if (over >= head.length) {
19
21
  this.chunks.shift();
20
22
  this.size -= head.length;
21
- this.dropped += head.length;
23
+ this.headDropped += head.length;
22
24
  }
23
25
  else {
24
26
  this.chunks[0] = head.subarray(over);
25
27
  this.size -= over;
26
- this.dropped += over;
28
+ this.headDropped += over;
27
29
  }
28
30
  }
29
31
  }
30
32
  recordSkippedBytes(n) {
31
33
  if (n > 0)
32
- this.dropped += n;
34
+ this.sourceSkipped += n;
33
35
  }
34
36
  result() {
35
- return { text: Buffer.concat(this.chunks).toString("utf8"), droppedBytes: this.dropped };
37
+ return { text: Buffer.concat(this.chunks).toString("utf8"), droppedBytes: this.headDropped + this.sourceSkipped };
36
38
  }
39
+ bytes() {
40
+ return Buffer.concat(this.chunks);
41
+ }
42
+ headDroppedBytes() {
43
+ return this.headDropped;
44
+ }
45
+ sourceSkippedBytes() {
46
+ return this.sourceSkipped;
47
+ }
48
+ }
49
+ export function newStreamCursorState(tail = new RollingTailBuffer()) {
50
+ return { tail, cursorBytes: 0, acceptedBytes: 0, disclosedSkippedBytes: 0, decoder: new StringDecoder("utf8") };
51
+ }
52
+ export function sliceStreamIncrement(s, terminal) {
53
+ const headDropped = s.tail.headDroppedBytes();
54
+ const sourceSkipped = s.tail.sourceSkippedBytes();
55
+ const newSkipped = Math.max(0, sourceSkipped - s.disclosedSkippedBytes);
56
+ s.disclosedSkippedBytes = sourceSkipped;
57
+ const droppedBeforeCursor = Math.max(0, headDropped - s.cursorBytes) + newSkipped;
58
+ const buf = s.tail.bytes();
59
+ const startInTail = Math.min(Math.max(s.cursorBytes, headDropped) - headDropped, buf.length);
60
+ const incBuf = startInTail <= 0 ? buf : buf.subarray(startInTail);
61
+ if (droppedBeforeCursor > 0)
62
+ s.decoder = new StringDecoder("utf8");
63
+ let inc = s.decoder.write(incBuf);
64
+ if (terminal) {
65
+ inc += s.decoder.end();
66
+ s.decoder = new StringDecoder("utf8");
67
+ }
68
+ s.cursorBytes = s.acceptedBytes;
69
+ return { inc, droppedBeforeCursor };
37
70
  }
38
71
  export function markTruncated(text, droppedBytes) {
39
72
  return droppedBytes > 0
@@ -18,6 +18,7 @@ export declare class TransportLspSession implements LspSession {
18
18
  request(op: LspOperation, params: LspRequestParams, signal?: AbortSignal): Promise<LspResult>;
19
19
  private syncOpenedFiles;
20
20
  private syncOne;
21
+ private static classifyUnbranded;
21
22
  get closed(): boolean;
22
23
  openedFiles(): string[];
23
24
  warmOpen(filePaths: string[], signal?: AbortSignal): Promise<void>;