@sema-agent/core 1.451.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 +10 -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 +1 -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 +1 -0
  36. package/dist/orchestration/run-workflow-tool.js +29 -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,4 +1,5 @@
1
1
  import { uriToPath, buildRequest, callHierarchyMethod, parseResult, pathToUri } from "./lsp-protocol.js";
2
+ import { lspFailureOf } from "./lsp.js";
2
3
  export class TransportLspSession {
3
4
  transport;
4
5
  languageId;
@@ -37,7 +38,7 @@ export class TransportLspSession {
37
38
  async request(op, params, signal) {
38
39
  const built = buildRequest(op, params);
39
40
  if (!built)
40
- return { kind: "none" };
41
+ return { kind: "none", reason: "unsupported_operation" };
41
42
  await this.syncOpenedFiles(params.filePath, signal);
42
43
  if (!this.warmupBarrierDone && this.warmup.windowMs > 0) {
43
44
  this.warmupBarrierDone = true;
@@ -49,18 +50,23 @@ export class TransportLspSession {
49
50
  }
50
51
  for (let attempt = 0;; attempt++) {
51
52
  let raw;
53
+ let noHierarchyItem = false;
52
54
  try {
53
55
  raw = await this.transport.request(built.method, built.params, signal);
54
56
  if (op === "incomingCalls" || op === "outgoingCalls") {
55
57
  const items = Array.isArray(raw) ? raw : raw != null ? [raw] : [];
56
- raw = items.length === 0 ? null : await this.transport.request(callHierarchyMethod(op), { item: items[0] }, signal);
58
+ noHierarchyItem = items.length === 0;
59
+ raw = noHierarchyItem ? null : await this.transport.request(callHierarchyMethod(op), { item: items[0] }, signal);
57
60
  }
58
61
  }
59
62
  catch (e) {
60
- this.log("lsp_op_degraded", { op, file: params.filePath, err: e instanceof Error ? e.message : String(e) });
61
- return { kind: "none" };
63
+ const f = lspFailureOf(e) ?? TransportLspSession.classifyUnbranded(signal, this.transport);
64
+ this.log("lsp_op_degraded", { op, file: params.filePath, reason: f.reason, err: e instanceof Error ? e.message : String(e) });
65
+ return { kind: "none", reason: f.reason, ...(f.detail !== undefined ? { detail: f.detail } : {}) };
62
66
  }
63
- const result = parseResult(op, raw);
67
+ const result = noHierarchyItem
68
+ ? { kind: "none", reason: "no_call_hierarchy_item" }
69
+ : parseResult(op, raw);
64
70
  if (result.kind === "none" && this.warmup.windowMs > 0 && Date.now() - this.createdAt < this.warmup.windowMs && !signal?.aborted) {
65
71
  await new Promise((r) => setTimeout(r, this.warmup.retryMs));
66
72
  continue;
@@ -92,7 +98,12 @@ export class TransportLspSession {
92
98
  try {
93
99
  text = await this.readText(filePath, signal);
94
100
  }
95
- catch {
101
+ catch (e) {
102
+ this.log("lsp_sync_skipped", {
103
+ file: filePath,
104
+ error: e instanceof Error ? e.message : String(e),
105
+ opened: this.opened.has(filePath),
106
+ });
96
107
  return;
97
108
  }
98
109
  const cur = this.opened.get(filePath);
@@ -115,6 +126,13 @@ export class TransportLspSession {
115
126
  this.syncing.set(filePath, job);
116
127
  return job.finally(() => this.syncing.delete(filePath));
117
128
  }
129
+ static classifyUnbranded(signal, transport) {
130
+ if (signal?.aborted)
131
+ return { reason: "cancelled" };
132
+ if (transport.closed === true)
133
+ return { reason: "server_terminated" };
134
+ return { reason: "server_error" };
135
+ }
118
136
  get closed() {
119
137
  return this.transport.closed === true;
120
138
  }
@@ -14,6 +14,7 @@ export interface LspSymbolInfo {
14
14
  line: number;
15
15
  character: number;
16
16
  }
17
+ export type LspNoneReason = "unsupported_operation" | "server_error" | "timeout" | "cancelled" | "server_terminated" | "no_call_hierarchy_item";
17
18
  export type LspResult = {
18
19
  kind: "locations";
19
20
  locations: LspLocation[];
@@ -25,7 +26,15 @@ export type LspResult = {
25
26
  contents: string;
26
27
  } | {
27
28
  kind: "none";
29
+ reason?: LspNoneReason;
30
+ detail?: string;
28
31
  };
32
+ export declare const LSP_FAILURE_BRAND = "lspFailureReason";
33
+ export declare function brandLspFailure<E extends Error>(e: E, reason: LspNoneReason, detail?: string): E;
34
+ export declare function lspFailureOf(e: unknown): {
35
+ reason: LspNoneReason;
36
+ detail?: string;
37
+ } | undefined;
29
38
  export interface LspRequestParams {
30
39
  filePath: string;
31
40
  line?: number;
@@ -43,6 +52,7 @@ export interface LspTransport {
43
52
  readonly closed?: boolean;
44
53
  }
45
54
  export type LspReadText = (filePath: string, signal?: AbortSignal) => Promise<string>;
55
+ export declare const MAX_LSP_FILE_BYTES: number;
46
56
  export interface LspServerManager {
47
57
  sessionFor(filePath: string, signal?: AbortSignal, env?: ExecutionEnv): Promise<LspSession | undefined>;
48
58
  readonly diagnostics?: import("./lsp-diagnostics.js").LspDiagnosticsRegistry;
package/dist/core/lsp.js CHANGED
@@ -1,11 +1,30 @@
1
1
  import { uriToPath } from "./lsp-protocol.js";
2
2
  import { Type } from "typebox";
3
- import { defineTool } from "./tools.js";
3
+ import { defineTool, errorResult } from "./tools.js";
4
4
  export const LSP_OPERATIONS = [
5
5
  "goToDefinition", "findReferences", "hover", "documentSymbol", "workspaceSymbol",
6
6
  "goToImplementation", "prepareCallHierarchy", "incomingCalls", "outgoingCalls",
7
7
  ];
8
- function fmt(result) {
8
+ export const LSP_FAILURE_BRAND = "lspFailureReason";
9
+ const LSP_NONE_REASONS = new Set([
10
+ "unsupported_operation", "server_error", "timeout", "cancelled", "server_terminated", "no_call_hierarchy_item",
11
+ ]);
12
+ export function brandLspFailure(e, reason, detail) {
13
+ const carrier = e;
14
+ carrier[LSP_FAILURE_BRAND] = reason;
15
+ if (detail !== undefined)
16
+ carrier.lspFailureDetail = detail;
17
+ return e;
18
+ }
19
+ export function lspFailureOf(e) {
20
+ const r = e?.[LSP_FAILURE_BRAND];
21
+ if (typeof r !== "string" || !LSP_NONE_REASONS.has(r))
22
+ return undefined;
23
+ const d = e.lspFailureDetail;
24
+ return { reason: r, ...(typeof d === "string" ? { detail: d } : {}) };
25
+ }
26
+ export const MAX_LSP_FILE_BYTES = 10 * 1000 * 1000;
27
+ function fmt(result, ctx) {
9
28
  switch (result.kind) {
10
29
  case "hover":
11
30
  return result.contents.trim() || "(no hover information)";
@@ -20,9 +39,30 @@ function fmt(result) {
20
39
  : `Found ${result.symbols.length} symbol(s):\n` +
21
40
  result.symbols.map((s) => `- ${s.name}${s.kind ? ` (${s.kind})` : ""} — ${s.uri}:${s.line}:${s.character}`).join("\n");
22
41
  case "none":
42
+ return fmtNone(result, ctx);
43
+ }
44
+ }
45
+ function fmtNone(result, ctx) {
46
+ const { operation, filePath } = ctx;
47
+ const detail = result.detail;
48
+ switch (result.reason) {
49
+ case undefined:
23
50
  return "No results.";
51
+ case "no_call_hierarchy_item":
52
+ return "No call hierarchy item found at this position";
53
+ case "unsupported_operation":
54
+ return `The language server for "${filePath}" does not support ${operation}${detail ? ` (${detail})` : ""}. Fall back to Grep/Read for this lookup.`;
55
+ case "timeout":
56
+ return `Error performing ${operation}: the language server did not respond in time. This is NOT an empty result — do not conclude the symbol has no ${operation === "findReferences" ? "references" : "results"}; retry or fall back to Grep/Read.`;
57
+ case "server_terminated":
58
+ return `Error performing ${operation}: the language server exited before answering. This is NOT an empty result — retry (a fresh server is spawned on the next call) or fall back to Grep/Read.`;
59
+ case "cancelled":
60
+ return `Error performing ${operation}: the request was cancelled before the language server answered. This is NOT an empty result.`;
61
+ case "server_error":
62
+ return `Error performing ${operation}: ${detail ?? "the language server returned an error"}. This is NOT an empty result — fall back to Grep/Read.`;
24
63
  }
25
64
  }
65
+ const LSP_ERROR_REASONS = new Set(["server_error", "timeout", "cancelled", "server_terminated"]);
26
66
  async function filterIgnored(result, isIgnored) {
27
67
  if (!isIgnored)
28
68
  return result;
@@ -80,14 +120,29 @@ export function createLspTool(manager, opts = {}) {
80
120
  execute: async (args, ctx) => {
81
121
  const { operation, filePath, line, character, query, symbol } = args;
82
122
  if (POSITION_OPS.has(operation) && (line === undefined || character === undefined)) {
83
- return `Error (LSP): the "${operation}" operation needs both line and character.`;
123
+ return errorResult(`Error (LSP): the "${operation}" operation needs both line and character.`);
84
124
  }
85
125
  if (POSITION_OPS.has(operation) && (!Number.isInteger(line) || line < 1 || !Number.isInteger(character) || character < 1)) {
86
- return `Error (LSP): line and character are 1-based integers (as shown in editors); got line=${line}, character=${character}.`;
126
+ return errorResult(`Error (LSP): line and character are 1-based integers (as shown in editors); got line=${line}, character=${character}.`);
87
127
  }
88
128
  const wsQuery = query ?? symbol;
89
129
  if (operation === "workspaceSymbol" && wsQuery === undefined) {
90
- return `Error (LSP): the "workspaceSymbol" operation needs a query.`;
130
+ return errorResult(`Error (LSP): the "workspaceSymbol" operation needs a query.`);
131
+ }
132
+ if (opts.env) {
133
+ const isAbsForm = filePath.startsWith("/") || /^[A-Za-z]:[\\/]/.test(filePath);
134
+ const cwd = opts.env.cwd;
135
+ const probe = isAbsForm || !cwd ? filePath : cwd.replace(/[\\/]+$/, "") + (cwd.includes("\\") ? "\\" : "/") + filePath;
136
+ const info = await opts.env.fileInfo(probe, ctx.signal);
137
+ if (!info.ok && info.error.code === "not_found")
138
+ return errorResult(`Error (LSP): File does not exist: ${filePath}`);
139
+ if (!info.ok && info.error.code === "invalid")
140
+ return errorResult(`Error (LSP): Path is not a file: ${filePath}`);
141
+ if (info.ok && info.value.kind === "directory")
142
+ return errorResult(`Error (LSP): Path is not a file: ${filePath}`);
143
+ if (info.ok && info.value.kind === "file" && info.value.size > MAX_LSP_FILE_BYTES) {
144
+ return `File too large for LSP analysis (${Math.ceil(info.value.size / 1e6)}MB exceeds 10MB limit)`;
145
+ }
91
146
  }
92
147
  const session = await manager.sessionFor(filePath, ctx.signal, opts.env);
93
148
  if (!session) {
@@ -95,7 +150,9 @@ export function createLspTool(manager, opts = {}) {
95
150
  }
96
151
  const seamCharacter = character !== undefined ? character - 1 : undefined;
97
152
  const result = await session.request(operation, { filePath, line, character: seamCharacter, symbol: wsQuery }, ctx.signal);
98
- return fmt(toModelBase(await filterIgnored(result, opts.isPathIgnored)));
153
+ const final = toModelBase(await filterIgnored(result, opts.isPathIgnored));
154
+ const text = fmt(final, { operation, filePath });
155
+ return final.kind === "none" && final.reason !== undefined && LSP_ERROR_REASONS.has(final.reason) ? errorResult(text) : text;
99
156
  },
100
157
  });
101
158
  }
@@ -15,7 +15,7 @@ export interface MailboxStore {
15
15
  sentAt: number;
16
16
  }): Promise<number>;
17
17
  claimLease(scope: string, handle: string, owner: string, ttlMs: number, now?: number): Promise<MailboxLease | null>;
18
- ack(scope: string, handle: string, upToSeq: number): Promise<void>;
18
+ ack(scope: string, handle: string, owner: string, upToSeq: number): Promise<void>;
19
19
  releaseLease(scope: string, handle: string, owner: string): Promise<void>;
20
20
  peekCount(scope: string, handle: string): Promise<number>;
21
21
  drop(scope: string, handle: string): Promise<void>;
@@ -23,6 +23,7 @@ export interface MailboxStore {
23
23
  maxAgeMs?: number;
24
24
  }): Promise<number>;
25
25
  }
26
+ export declare function newestSentAt(messages: readonly MailboxMessage[]): number | undefined;
26
27
  export declare class InMemoryMailboxStore implements MailboxStore {
27
28
  private boxes;
28
29
  private key;
@@ -33,7 +34,7 @@ export declare class InMemoryMailboxStore implements MailboxStore {
33
34
  sentAt: number;
34
35
  }): Promise<number>;
35
36
  claimLease(scope: string, handle: string, owner: string, ttlMs: number, now?: number): Promise<MailboxLease | null>;
36
- ack(scope: string, handle: string, upToSeq: number): Promise<void>;
37
+ ack(scope: string, handle: string, owner: string, upToSeq: number): Promise<void>;
37
38
  releaseLease(scope: string, handle: string, owner: string): Promise<void>;
38
39
  peekCount(scope: string, handle: string): Promise<number>;
39
40
  drop(scope: string, handle: string): Promise<void>;
@@ -1,6 +1,19 @@
1
+ export function newestSentAt(messages) {
2
+ let newest;
3
+ for (const m of messages) {
4
+ if (!Number.isFinite(m.sentAt))
5
+ return undefined;
6
+ if (newest === undefined || m.sentAt > newest)
7
+ newest = m.sentAt;
8
+ }
9
+ return newest;
10
+ }
1
11
  export class InMemoryMailboxStore {
2
12
  boxes = new Map();
3
13
  key(scope, handle) {
14
+ if (handle.includes("\u0000")) {
15
+ throw new Error(`MailboxStore: unsafe handle ${JSON.stringify(handle)} — a NUL byte makes the composite key ambiguous`);
16
+ }
4
17
  return `${scope}\u0000${handle.toLowerCase()}`;
5
18
  }
6
19
  box(scope, handle) {
@@ -30,12 +43,14 @@ export class InMemoryMailboxStore {
30
43
  b.lease = { owner, expiresAt: now + ttlMs, maxSeq };
31
44
  return { messages: b.messages.map((m) => ({ ...m })), maxSeq };
32
45
  }
33
- async ack(scope, handle, upToSeq) {
46
+ async ack(scope, handle, owner, upToSeq) {
34
47
  const b = this.boxes.get(this.key(scope, handle));
35
48
  if (!b)
36
49
  return;
50
+ if (b.lease === undefined || b.lease.owner !== owner)
51
+ return;
37
52
  b.messages = b.messages.filter((m) => m.seq > upToSeq);
38
- if (b.lease !== undefined && b.lease.maxSeq <= upToSeq)
53
+ if (b.lease.maxSeq <= upToSeq)
39
54
  b.lease = undefined;
40
55
  }
41
56
  async releaseLease(scope, handle, owner) {
@@ -56,8 +71,8 @@ export class InMemoryMailboxStore {
56
71
  for (const b of this.boxes.values()) {
57
72
  if (b.scope !== scope)
58
73
  continue;
59
- const newest = b.messages[b.messages.length - 1];
60
- if (newest !== undefined && newest.sentAt < now - opts.maxAgeMs) {
74
+ const newest = newestSentAt(b.messages);
75
+ if (newest !== undefined && newest < now - opts.maxAgeMs) {
61
76
  b.messages = [];
62
77
  b.lease = undefined;
63
78
  dropped++;
@@ -486,7 +486,7 @@ export function createRememberTool(store, scope, onRemembered) {
486
486
  const a = args;
487
487
  const note = String(a.note ?? "").trim();
488
488
  if (!note) {
489
- return { content: "Nothing remembered (empty note).", details: { ok: false } };
489
+ return { content: "Nothing remembered (empty note).", details: { ok: false }, isError: true };
490
490
  }
491
491
  const oneLine = (v, cap) => {
492
492
  if (typeof v !== "string")
@@ -999,9 +999,12 @@ export async function prepareTask(spec, deps, sessions, resume, _memorySelector,
999
999
  const results = await mcp.refresh(typeof serverArg === "string" && serverArg.length > 0 ? serverArg : undefined);
1000
1000
  const lines = [];
1001
1001
  let changed = false;
1002
+ let anyActiveFailure = false;
1002
1003
  for (const r of results) {
1003
1004
  if (r.status !== "refreshed" || r.tools === undefined) {
1004
1005
  lines.push(`${r.server}: ${r.status}${r.error !== undefined ? ` (${r.error})` : ""}`);
1006
+ if (r.status === "failed")
1007
+ anyActiveFailure = true;
1005
1008
  continue;
1006
1009
  }
1007
1010
  const excludedSet = new Set(toolFaceSnapshot.exclude ?? []);
@@ -1012,6 +1015,7 @@ export async function prepareTask(spec, deps, sessions, resume, _memorySelector,
1012
1015
  }
1013
1016
  catch (foldErr) {
1014
1017
  lines.push(`${r.server}: failed (${foldErr instanceof Error ? foldErr.message : String(foldErr)})`);
1018
+ anyActiveFailure = true;
1015
1019
  continue;
1016
1020
  }
1017
1021
  for (let i = tools.length - 1; i >= 0; i--) {
@@ -1041,7 +1045,12 @@ export async function prepareTask(spec, deps, sessions, resume, _memorySelector,
1041
1045
  lines.push("WARNING: refreshed tools declare irreversible/egress safety hints, but this task started with no approval gate registered (no policy/hooks and no gated tools at start) — these hints cannot arm a gate mid-task (RB-46); the refreshed tools run ungated on this deployment shape.");
1042
1046
  }
1043
1047
  const text = lines.length === 0 ? "No connected MCP servers to refresh." : lines.join("\n");
1044
- return { content: [{ type: "text", text }], details: { results }, terminate: false };
1048
+ return {
1049
+ content: [{ type: "text", text }],
1050
+ details: { results },
1051
+ terminate: false,
1052
+ ...(anyActiveFailure && !changed ? { isError: true } : {}),
1053
+ };
1045
1054
  },
1046
1055
  });
1047
1056
  toolEffects.set("RefreshMcpTools", "read");
@@ -1243,7 +1243,7 @@ export class Runner {
1243
1243
  return { output: raw, truncated: true, totalChars };
1244
1244
  };
1245
1245
  const CC_DETAIL_TYPES = new Set([
1246
- "edit", "multiedit", "create", "update", "bash", "notebook-edit", "text", "grep", "glob", "mcp",
1246
+ "edit", "multiedit", "create", "update", "bash", "notebook-edit", "notebook", "text", "grep", "glob", "mcp",
1247
1247
  "agent", "task", "task-list", "task-output", "memory-saved", "workflow-run",
1248
1248
  "web-fetch", "web-search", "todo", "cron-create", "cron-delete", "cron-list", "image",
1249
1249
  "task-stop", "tool-search", "memory-recall", "repo-map", "fork", "enter-plan-mode", "exit-plan-mode",
@@ -6,6 +6,9 @@ const INTERRUPTED_UNKNOWN = "[INTERRUPTED] The previous run ended before this to
6
6
  const INTERRUPTED_SAFE = "[INTERRUPTED] The previous run ended before this tool call's result was recorded. This tool is " +
7
7
  "read-only/idempotent, so its outcome carries no risk: simply call it again if you still need " +
8
8
  "the result.";
9
+ const INTERRUPTED_IDEMPOTENT = "[INTERRUPTED] The previous run ended before this tool call's result was recorded. The call may or " +
10
+ "may not have taken effect. It is safe to REPLAY: re-issuing the same call converges the state to " +
11
+ "what you intended. If you decide not to re-issue it, verify the current state first.";
9
12
  const INTERRUPTED_NEVER_STARTED = "[INTERRUPTED] The run was aborted before this tool call started. It was never executed and had " +
10
13
  "no side effects — it is safe to re-issue this call if you still need it.";
11
14
  export function findOrphanToolCalls(messages, suspendedBatch) {
@@ -50,9 +53,9 @@ export async function reconcileInterruptedSession(session, toolEffects, suspende
50
53
  const recovered = [];
51
54
  for (const orphan of orphans) {
52
55
  const effect = toolEffects?.get(canonicalToolName(orphan.toolName)) ?? "write";
53
- const safe = effect === "read" || effect === "idempotent";
56
+ const effectText = effect === "read" ? INTERRUPTED_SAFE : effect === "idempotent" ? INTERRUPTED_IDEMPOTENT : INTERRUPTED_UNKNOWN;
54
57
  const neverStarted = startedToolCallIds !== undefined && !startedToolCallIds.has(orphan.toolCallId);
55
- const text = neverStarted ? INTERRUPTED_NEVER_STARTED : safe ? INTERRUPTED_SAFE : INTERRUPTED_UNKNOWN;
58
+ const text = neverStarted ? INTERRUPTED_NEVER_STARTED : effectText;
56
59
  const entryId = await session.appendMessage({
57
60
  role: "toolResult",
58
61
  toolCallId: orphan.toolCallId,
@@ -20,6 +20,7 @@ export interface TaskNotificationPayload {
20
20
  recentSteps?: import("../agents/subagent-steps.js").SubagentStep[];
21
21
  editedFiles?: import("../agents/subagent-steps.js").SubagentEditedFile[];
22
22
  resumable?: boolean;
23
+ completionId?: string;
23
24
  }
24
25
  export interface ExternalNotificationInput {
25
26
  task_id: string;
@@ -27,8 +27,14 @@ export interface UnifiedTaskOutput {
27
27
  stoppedBy?: StopSource;
28
28
  seq?: number;
29
29
  partial_result?: boolean;
30
+ completionId?: string;
30
31
  details?: unknown;
31
32
  }
33
+ type UnifiedTaskResult = {
34
+ content: string;
35
+ details: UnifiedTaskOutput;
36
+ isError?: boolean;
37
+ };
32
38
  export interface SemaTaskHandle {
33
39
  id: string;
34
40
  type: SemaTaskType;
@@ -41,6 +47,7 @@ export interface SemaTaskHandle {
41
47
  updatedAt: number;
42
48
  outputFile?: string;
43
49
  outputOffset?: number;
50
+ completionId?: string;
44
51
  }
45
52
  export interface TaskKind {
46
53
  type: SemaTaskType;
@@ -257,6 +264,7 @@ export declare class TaskRegistry {
257
264
  markStopSourceByShellId(shellId: string, source: StopSource, env?: unknown): void;
258
265
  clearPendingStopSourceByShellId(shellId: string, source: StopSource, env?: unknown): void;
259
266
  getStopAttribution(id: string): StopSource | undefined;
267
+ getCompletionId(id: string): string | undefined;
260
268
  parkBackgroundAgent(id: string, park: {
261
269
  checkpointToken: string;
262
270
  seq?: number;
@@ -323,18 +331,16 @@ export declare class TaskRegistry {
323
331
  timeoutResidentShellIds(env: unknown): BackgroundShellId[];
324
332
  retainedShellIds(env: unknown): BackgroundShellId[];
325
333
  registerMonitor(input: RegisterMonitorInput): string;
334
+ private absorbMonitorPoll;
335
+ private emitMonitorEvent;
326
336
  private startMonitorWatcher;
327
337
  registerWorkflow(input: RegisterWorkflowInput): string;
328
- pollTask(id: string, access: TaskAccess, opts?: TaskPollOptions): Promise<{
329
- content: string;
330
- details: UnifiedTaskOutput;
331
- }>;
338
+ pollTask(id: string, access: TaskAccess, opts?: TaskPollOptions): Promise<UnifiedTaskResult>;
332
339
  private serveDurableAgentRow;
333
- stopTask(id: string, access: TaskAccess, opts?: TaskStopOptions): Promise<{
334
- content: string;
335
- details: UnifiedTaskOutput;
336
- }>;
337
- resolveBackgroundAgentByName(name: string, access: TaskAccess): {
340
+ stopTask(id: string, access: TaskAccess, opts?: TaskStopOptions): Promise<UnifiedTaskResult>;
341
+ resolveBackgroundAgentByName(name: string, access: TaskAccess, opts?: {
342
+ preferRunning?: boolean;
343
+ }): {
338
344
  status: "found";
339
345
  handle: BackgroundAgentTaskHandle;
340
346
  } | {