@sema-agent/core 2.10.0 → 2.12.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.
@@ -8,6 +8,9 @@ import { createSubagentResume } from "./subagent.js";
8
8
  export const SEND_MESSAGE_TOOL_NAME = "SendMessage";
9
9
  let uplinkSeqGlobal = Date.now();
10
10
  const UPLINK_RESULT_MAX = 8000;
11
+ function clipCarrierMessage(text) {
12
+ return text.length > UPLINK_RESULT_MAX ? `${text.slice(0, UPLINK_RESULT_MAX)}\n[message truncated: ${text.length} chars total]` : text;
13
+ }
11
14
  export const SEND_MESSAGE_SUMMARY_MAX = 200;
12
15
  export function clipSendMessageSummary(raw) {
13
16
  return raw.length > SEND_MESSAGE_SUMMARY_MAX
@@ -89,6 +92,7 @@ export function createSendMessageTool(opts) {
89
92
  isError: true,
90
93
  };
91
94
  }
95
+ const summary = clipSendMessageSummary(summaryArg);
92
96
  const senderId = ctx.taskId ?? opts.owner;
93
97
  const [parentTaskId, parentSessionId] = ctx.parentTaskId !== undefined ? [ctx.parentTaskId, ctx.parentSessionId] : [opts.parentTaskId, opts.parentSessionId];
94
98
  const senderName = ctx.spawnedAgentName ?? opts.senderName;
@@ -102,14 +106,12 @@ export function createSendMessageTool(opts) {
102
106
  }
103
107
  if (normalizeAgentName(to) === "main") {
104
108
  if (opts.uplink && senderId !== undefined) {
105
- const uplinkSummaryRaw = typeof a.summary === "string" && a.summary.trim() !== "" ? clipSendMessageSummary(a.summary.trim()) : message.slice(0, 80);
106
- const fromLabel = senderName ?? senderId;
107
109
  try {
108
110
  opts.uplink({
109
111
  task_id: senderId,
110
112
  task_type: "background_agent",
111
113
  status: "event",
112
- summary: `message from ${fromLabel}: ${uplinkSummaryRaw}`,
114
+ summary: `message from ${senderLabel}: ${summary}`,
113
115
  result: message.length > UPLINK_RESULT_MAX ? `${message.slice(0, UPLINK_RESULT_MAX)}\n[uplink truncated: ${message.length} chars total — read the agent's transcript for the rest]` : message,
114
116
  seq: ++uplinkSeqGlobal,
115
117
  }, { priority: "next" });
@@ -270,11 +272,9 @@ export function createSendMessageTool(opts) {
270
272
  isError: true,
271
273
  };
272
274
  }
273
- const clipped = message.length > UPLINK_RESULT_MAX ? `${message.slice(0, UPLINK_RESULT_MAX)}\n[message truncated: ${message.length} chars total]` : message;
274
- const t3Summary = typeof a.summary === "string" && a.summary.trim() !== "" ? clipSendMessageSummary(a.summary.trim()) : undefined;
275
- const t3From = senderLabel;
275
+ const clipped = clipCarrierMessage(message);
276
276
  try {
277
- await opts.mailbox.append(scope, handle, { from: t3From, content: t3Summary !== undefined ? `[${t3Summary}] ${clipped}` : clipped, sentAt: now });
277
+ await opts.mailbox.append(scope, handle, { from: senderLabel, content: `[${summary}] ${clipped}`, sentAt: now });
278
278
  }
279
279
  catch (e) {
280
280
  await rollback();
@@ -418,17 +418,13 @@ export function createSendMessageTool(opts) {
418
418
  }
419
419
  if (row.status === "running" || row.status === "pending") {
420
420
  return await withTargetLane(targetLaneKey(access.scope, targetId), async () => {
421
- const s2Summary = typeof a.summary === "string" && a.summary.trim() !== "" ? clipSendMessageSummary(a.summary.trim()) : undefined;
422
- const fromLabel = senderLabel;
423
- const s2Clipped = message.length > UPLINK_RESULT_MAX
424
- ? `${message.slice(0, UPLINK_RESULT_MAX)}\n[message truncated: ${message.length} chars total]`
425
- : message;
426
- const teammateXml = frameTeammateMessage({ from: fromLabel, ...(s2Summary !== undefined ? { summary: s2Summary } : {}), text: s2Clipped });
421
+ const s2Clipped = clipCarrierMessage(message);
422
+ const teammateXml = frameTeammateMessage({ from: senderLabel, summary, text: s2Clipped });
427
423
  const delivered = await opts.registry.deliverToRunningAgent(targetId, resolvedAccess, {
428
424
  task_id: senderLabel,
429
425
  task_type: "background_agent",
430
426
  status: "event",
431
- summary: `message from ${fromLabel}${s2Summary !== undefined ? `: ${s2Summary}` : ""}`,
427
+ summary: `message from ${senderLabel}: ${summary}`,
432
428
  result: teammateXml,
433
429
  seq: ++uplinkSeqGlobal,
434
430
  }, { priority: "next" });
@@ -440,7 +436,7 @@ export function createSendMessageTool(opts) {
440
436
  : `Message queued for delivery to ${who} at its next turn. If it finishes before reading it, the message may not survive — you will be notified of its completion either way; resend then if it went unanswered. Continue with other work; do not poll.`;
441
437
  return {
442
438
  content: receiptText,
443
- details: { type: "send-message", status: "delivered_running", disposition: delivered.disposition, to, task_id: targetId, ...(s2Summary !== undefined ? { summary: s2Summary } : {}) },
439
+ details: { type: "send-message", status: "delivered_running", disposition: delivered.disposition, to, task_id: targetId, summary },
444
440
  };
445
441
  }
446
442
  if (delivered.reason === "no_channel") {
@@ -531,16 +527,15 @@ export function createSendMessageTool(opts) {
531
527
  ...(row.rootSessionId !== undefined ? { rowRootSessionId: row.rootSessionId } : {}),
532
528
  ...(opts.notify ? { currentParentNotify: opts.notify } : {}),
533
529
  });
534
- const summary = typeof a.summary === "string" && a.summary.trim() !== "" ? clipSendMessageSummary(a.summary.trim()) : undefined;
535
- const fromPrefix = parentTaskId !== undefined ? `(message from teammate "${senderName ?? senderId ?? "unknown"}")\n` : "";
530
+ const fromPrefix = senderIsChild ? `(message from teammate "${senderLabel}")\n` : "";
536
531
  try {
537
- const safeSummary = summary !== undefined ? escapeEnvelopeTag(TEAMMATE_MESSAGE_TAG, summary) : undefined;
532
+ const safeSummary = escapeEnvelopeTag(TEAMMATE_MESSAGE_TAG, summary);
538
533
  const safeMessage = escapeEnvelopeTag(TEAMMATE_MESSAGE_TAG, message);
539
- const marker = await resume(`${fromPrefix}${safeSummary ? `[${safeSummary}] ${safeMessage}` : safeMessage}`);
534
+ const marker = await resume(`${fromPrefix}[${safeSummary}] ${safeMessage}`);
540
535
  return {
541
536
  content: `Message sent — ${who} resumed in the background with its prior context intact (correlation marker [${marker}]).\n` +
542
537
  `You will be notified automatically when it completes; its reply will carry [${marker}]. Continue with other work — do not poll.`,
543
- details: { type: "send-message", status: "resumed", to, task_id: targetId, marker, ...(summary !== undefined ? { summary } : {}) },
538
+ details: { type: "send-message", status: "resumed", to, task_id: targetId, marker, summary },
544
539
  };
545
540
  }
546
541
  catch (e) {
@@ -1372,8 +1372,10 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1372
1372
  const editedFiles = stepRecorder.editedFiles();
1373
1373
  return { ...(recentSteps ? { recentSteps } : {}), ...(editedFiles ? { editedFiles } : {}) };
1374
1374
  };
1375
+ const treeScope = reviveClaim?.row.scope ?? ctx.principal ?? opts.background?.scope;
1375
1376
  const childInternals = {
1376
1377
  ...(inheritedManifestScope ? { inheritedManifestScope } : {}),
1378
+ ...(treeScope !== undefined ? { registryScope: treeScope } : {}),
1377
1379
  ...(ctx.inheritedGateForChildren ? { inheritedGate: ctx.inheritedGateForChildren() } : {}),
1378
1380
  ...(childDefaultPersona !== undefined ? { defaultSystemPrompt: childDefaultPersona } : {}),
1379
1381
  isDelegatedChild: true,
@@ -1632,7 +1634,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1632
1634
  }
1633
1635
  const shortDesc = `fork: ${(typeof a.description === "string" && a.description.trim() ? a.description.trim() : prompt).slice(0, 180)}`;
1634
1636
  const bgOwner = sessionScopedBg ? ctx.sessionId : ctx.taskId ?? bg.owner;
1635
- const bgScope = ctx.principal ?? bg.scope;
1637
+ const bgScope = treeScope;
1636
1638
  let taskId;
1637
1639
  try {
1638
1640
  taskId = bg.registry.registerBackgroundAgent({
@@ -2037,7 +2039,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2037
2039
  }
2038
2040
  const shortDesc = reviveRow?.description ?? String(a.description ?? "sub-agent").slice(0, 200);
2039
2041
  const bgOwner = reviveRow !== undefined ? reviveRow.owner : sessionScopedBg ? ctx.sessionId : ctx.taskId ?? bg.owner;
2040
- const bgScope = reviveRow !== undefined ? reviveRow.scope : ctx.principal ?? bg.scope;
2042
+ const bgScope = treeScope;
2041
2043
  let taskId;
2042
2044
  try {
2043
2045
  taskId = bg.registry.registerBackgroundAgent(reviveRow !== undefined
@@ -242,6 +242,7 @@ export interface CheckpointSummary {
242
242
  contentKind?: "content_ask";
243
243
  createdAt?: number;
244
244
  toolInput?: string;
245
+ restoreMode?: "snapshot" | "park_only";
245
246
  preview?: unknown;
246
247
  }
247
248
  export declare function summarizeCheckpoint(cp: Checkpoint): CheckpointSummary;
@@ -181,6 +181,9 @@ export function summarizeCheckpoint(cp) {
181
181
  ...(tool !== undefined ? { toolCallId: tool.toolCallId, toolName: tool.toolName } : {}),
182
182
  ...(tool?.toolName === ASK_USER_QUESTION_TOOL_NAME ? { contentKind: "content_ask" } : {}),
183
183
  ...(toolInput !== undefined ? { toolInput } : {}),
184
+ ...(cp.state.workspaceHandle !== undefined
185
+ ? { restoreMode: cp.state.workspaceHandle.restoreMode === "park_only" ? "park_only" : "snapshot" }
186
+ : {}),
184
187
  ...(tool?.preview !== undefined ? { preview: tool.preview } : {}),
185
188
  };
186
189
  }
@@ -23,8 +23,10 @@ export async function consolidateScope(scope, deps, opts = {}) {
23
23
  "(getConsolidationCursor/setConsolidationCursor) — skipped (no-op), NOT degraded to a full re-consolidation"));
24
24
  return undefined;
25
25
  }
26
- if (!supportsConsolidation(store) || typeof store.listStructuredNotes !== "function") {
27
- onWarn?.(new Error("consolidateScope: store is not id-addressable / has no manifest — skipped (no-op)"));
26
+ if (!supportsConsolidation(store) ||
27
+ typeof store.listStructuredNotes !== "function" ||
28
+ typeof store.getByIds !== "function") {
29
+ onWarn?.(new Error("consolidateScope: store is not id-addressable / has no manifest read pair — skipped (no-op)"));
28
30
  return undefined;
29
31
  }
30
32
  const release = deps.acquire ? await deps.acquire(scope) : () => { };
@@ -76,29 +76,28 @@ export function clearStaleToolResults(messages, opts) {
76
76
  }
77
77
  const keep = opts.keepRecentToolResults ?? 3;
78
78
  const compactable = opts.compactableTools ?? COMPACTABLE_TOOLS;
79
- const toolResultIdx = messages.flatMap((m, i) => isToolResult(m) && !isCleared(m) && compactable.has(m.toolName ?? "") ? [i] : []);
80
- const clearable = toolResultIdx.slice(0, Math.max(0, toolResultIdx.length - keep));
79
+ const toolResultCandidates = messages.flatMap((m, i) => isToolResult(m) && !isCleared(m) && compactable.has(m.toolName) ? [{ idx: i, target: m }] : []);
80
+ const clearable = toolResultCandidates.slice(0, Math.max(0, toolResultCandidates.length - keep));
81
81
  if (clearable.length === 0) {
82
82
  return messages;
83
83
  }
84
84
  const out = messages.slice();
85
85
  let current = total;
86
- for (const idx of clearable) {
86
+ for (const { idx, target } of clearable) {
87
87
  if (current <= opts.budgetTokens) {
88
88
  break;
89
89
  }
90
- const before = estimateTokens(out[idx], cpt);
91
- const msg = out[idx];
92
- const rawContent = Array.isArray(msg.content) ? msg.content : [];
90
+ const before = estimateTokens(target, cpt);
91
+ const rawContent = Array.isArray(target.content) ? target.content : [];
93
92
  let ref;
94
93
  if (opts.offload) {
95
94
  const fullText = rawContent
96
95
  .filter((c) => c?.type === "text" && typeof c.text === "string")
97
96
  .map((c) => c.text)
98
97
  .join("\n");
99
- if (msg.toolCallId && fullText.trim().length > 0) {
98
+ if (target.toolCallId && fullText.trim().length > 0) {
100
99
  try {
101
- ref = refNote(opts.offload.persist(msg.toolCallId, fullText));
100
+ ref = refNote(opts.offload.persist(target.toolCallId, fullText));
102
101
  }
103
102
  catch {
104
103
  ref = undefined;
@@ -107,10 +106,7 @@ export function clearStaleToolResults(messages, opts) {
107
106
  }
108
107
  const mediaBlocks = rawContent.filter((c) => c?.type !== "text");
109
108
  const marker = clearedMarker([ref, mediaBlocks.length > 0 ? mediaNote(mediaBlocks) : undefined]);
110
- const cleared = {
111
- ...out[idx],
112
- content: [{ type: "text", text: marker }],
113
- };
109
+ const cleared = { ...target, content: [{ type: "text", text: marker }] };
114
110
  out[idx] = cleared;
115
111
  current -= idx > anchorIdx ? before - estimateTokens(cleared, cpt) : 0;
116
112
  }
@@ -57,6 +57,7 @@ export interface McpServerStatus {
57
57
  };
58
58
  toolNames?: string[];
59
59
  error?: string;
60
+ transportClosed?: boolean;
60
61
  }
61
62
  export declare const MCP_PREFIX = "mcp__";
62
63
  export declare function resolveMcpDeclaredResultSize(meta: Record<string, unknown> | undefined): number | undefined;
@@ -69,6 +70,7 @@ export declare const MCP_IDLE_TIMEOUT_STDIO_DEFAULT_MS: number;
69
70
  export declare const MCP_IDLE_TIMEOUT_HTTP_DEFAULT_MS: number;
70
71
  export declare function mcpIdleTimeoutMs(kind: "stdio" | "http"): number;
71
72
  export declare function describeMcpSpecErrorCode(code: unknown): string | undefined;
73
+ export declare function collapseMcpErrorPrefix(message: string): string;
72
74
  export declare function normalizeMcpName(name: string): string;
73
75
  export declare function clampNameSegment(seg: string, max?: number): string;
74
76
  export * from "./image-downsample.js";
package/dist/core/mcp.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
3
  import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
4
- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4
+ import { StreamableHTTPClientTransport, StreamableHTTPError } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
5
5
  import { lstat, mkdir, writeFile } from "node:fs/promises";
6
6
  import { tmpdir } from "node:os";
7
7
  import { join } from "node:path";
@@ -145,8 +145,24 @@ const MCP_SPEC_ERROR_CODE_NAMES = new Map([
145
145
  export function describeMcpSpecErrorCode(code) {
146
146
  return typeof code === "number" ? MCP_SPEC_ERROR_CODE_NAMES.get(code) : undefined;
147
147
  }
148
+ export function collapseMcpErrorPrefix(message) {
149
+ let out = message;
150
+ for (;;) {
151
+ const m = /^MCP error (-?\d+): (?=MCP error \1: )/.exec(out);
152
+ if (m === null)
153
+ return out;
154
+ out = out.slice(m[0].length);
155
+ }
156
+ }
157
+ function collapseMcpErrorStampInPlace(err) {
158
+ if (!(err instanceof McpError))
159
+ return;
160
+ const collapsed = collapseMcpErrorPrefix(err.message);
161
+ if (collapsed !== err.message)
162
+ err.message = collapsed;
163
+ }
148
164
  function namedMcpFailureText(err) {
149
- const detail = err instanceof Error ? err.message : String(err);
165
+ const detail = err instanceof Error ? collapseMcpErrorPrefix(err.message) : String(err);
150
166
  const condition = err instanceof McpError ? describeMcpSpecErrorCode(err.code) : undefined;
151
167
  return condition !== undefined ? `${condition}: ${detail}` : detail;
152
168
  }
@@ -155,6 +171,53 @@ function isTransportLost(err) {
155
171
  return true;
156
172
  return err instanceof Error && /not connected|connection closed/i.test(err.message);
157
173
  }
174
+ const NETWORK_CODES_NEVER_DELIVERED = new Set([
175
+ "ECONNREFUSED",
176
+ "ENOTFOUND",
177
+ "EAI_AGAIN",
178
+ "EHOSTUNREACH",
179
+ "ENETUNREACH",
180
+ "UND_ERR_CONNECT_TIMEOUT",
181
+ ]);
182
+ function networkErrorCode(err, depth = 0) {
183
+ if (depth > 5 || !(err instanceof Error))
184
+ return undefined;
185
+ const code = err.code;
186
+ if (typeof code === "string" && /^(?:E[A-Z]+|UND_ERR_[A-Z_]+)$/.test(code))
187
+ return code;
188
+ if (err instanceof AggregateError) {
189
+ for (const inner of err.errors) {
190
+ const found = networkErrorCode(inner, depth + 1);
191
+ if (found !== undefined)
192
+ return found;
193
+ }
194
+ }
195
+ return networkErrorCode(err.cause, depth + 1);
196
+ }
197
+ function describeHttpTransportFailure(err) {
198
+ if (err instanceof McpError)
199
+ return undefined;
200
+ if (err instanceof StreamableHTTPError) {
201
+ const status = typeof err.code === "number" && err.code > 0 ? err.code : undefined;
202
+ return {
203
+ condition: status !== undefined
204
+ ? `its HTTP endpoint answered ${status} instead of an MCP response`
205
+ : "its HTTP endpoint answered something that is not an MCP response",
206
+ delivered: "unknown",
207
+ ...(status !== undefined ? { httpStatus: status } : {}),
208
+ };
209
+ }
210
+ const code = networkErrorCode(err);
211
+ if (code !== undefined) {
212
+ return NETWORK_CODES_NEVER_DELIVERED.has(code)
213
+ ? { condition: `its HTTP endpoint could not be reached (${code})`, delivered: "no" }
214
+ : { condition: `the connection to its HTTP endpoint failed (${code})`, delivered: "unknown" };
215
+ }
216
+ if (err instanceof TypeError && /fetch failed|terminated|network/i.test(err.message)) {
217
+ return { condition: "the HTTP request to its endpoint failed at the network layer", delivered: "unknown" };
218
+ }
219
+ return undefined;
220
+ }
158
221
  function writeEffectWarning(writeEffect) {
159
222
  return writeEffect
160
223
  ? " This tool is write-capable: treat its side effects as POSSIBLY APPLIED and verify the actual state before retrying."
@@ -173,6 +236,7 @@ function rethrowHonestMcpError(err, ctx) {
173
236
  }
174
237
  if (ctx.signal?.aborted)
175
238
  throw err;
239
+ collapseMcpErrorStampInPlace(err);
176
240
  const serverLabel = inlineUntrusted(ctx.server);
177
241
  if (err instanceof McpError && err.code === ErrorCode.RequestTimeout) {
178
242
  const data = err.data;
@@ -193,6 +257,21 @@ function rethrowHonestMcpError(err, ctx) {
193
257
  e.details = { transportLost: true, server: ctx.server };
194
258
  throw e;
195
259
  }
260
+ const httpFailure = describeHttpTransportFailure(err);
261
+ if (httpFailure !== undefined) {
262
+ const detail = err instanceof Error ? err.message : String(err);
263
+ const fenced = `\nThe transport error follows as external/untrusted data:\n${delimitUntrusted(`${ctx.server} transport error`, truncateMcpErrorText(detail))}`;
264
+ const e = new Error(httpFailure.delivered === "no"
265
+ ? `${ctx.what} could not reach MCP server "${serverLabel}": ${httpFailure.condition}. The request was not delivered, so the server did not execute it. This server's tools and resources will keep failing until its endpoint is reachable again — do not retry them; use an alternative if one exists.${fenced}`
266
+ : `${ctx.what} failed at the transport layer of MCP server "${serverLabel}": ${httpFailure.condition}. The request may or may not have executed on the server — the outcome is unknown.${writeEffectWarning(ctx.writeEffect)}${fenced}`, { cause: err });
267
+ e.errorKind = httpFailure.delivered === "no" ? "server_disconnected" : "transport_lost";
268
+ e.details = {
269
+ server: ctx.server,
270
+ ...(httpFailure.delivered === "no" ? {} : { transportLost: true }),
271
+ ...(httpFailure.httpStatus !== undefined ? { httpStatus: httpFailure.httpStatus } : {}),
272
+ };
273
+ throw e;
274
+ }
196
275
  if (err instanceof McpError) {
197
276
  const condition = describeMcpSpecErrorCode(err.code);
198
277
  if (condition !== undefined) {
@@ -546,12 +625,16 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
546
625
  serverInstructions.push({ server: spec.name, text: s.instructions });
547
626
  }
548
627
  const hasInstructions = s.instructions !== undefined;
549
- s.announce.fn = () => {
550
- if (!disposing && hasInstructions)
628
+ const announceClose = () => {
629
+ if (disposing)
630
+ return;
631
+ s.status.transportClosed = true;
632
+ if (hasInstructions)
551
633
  instructionsDelta.pendingRemovals.push(spec.name);
552
634
  };
553
- if (s.health.dead && hasInstructions)
554
- instructionsDelta.pendingRemovals.push(spec.name);
635
+ s.announce.fn = announceClose;
636
+ if (s.health.dead)
637
+ announceClose();
555
638
  if (s.resourceServer)
556
639
  resourceServers.push(s.resourceServer);
557
640
  for (const d of s.dropped)
@@ -47,8 +47,10 @@ export interface MemoryNoteHeader {
47
47
  export interface MemoryNoteRecord extends MemoryNoteHeader {
48
48
  text: string;
49
49
  }
50
- export declare function supportsConsolidation(store: MemoryStore): boolean;
51
- export declare function supportsPeriodicConsolidation(store: MemoryStore): boolean;
50
+ export type ConsolidationCapableStore = MemoryStore & Required<Pick<MemoryStore, "searchScored" | "update" | "delete">>;
51
+ export declare function supportsConsolidation(store: MemoryStore): store is ConsolidationCapableStore;
52
+ export type PeriodicConsolidationCapableStore = MemoryStore & Required<Pick<MemoryStore, "getConsolidationCursor" | "setConsolidationCursor">>;
53
+ export declare function supportsPeriodicConsolidation(store: MemoryStore): store is PeriodicConsolidationCapableStore;
52
54
  export declare const CALLER_AUTHORED_TYPES: ReadonlySet<string>;
53
55
  export declare const DEFAULT_CALLER_AUTHORED_TYPE: MemoryNoteType;
54
56
  export type UtilityGate = (stats: unknown) => boolean;
@@ -1,3 +1,4 @@
1
1
  import type { AgentMessage } from "../internal/harness-types.js";
2
+ import type { ToolResultMessage } from "../internal/llm.js";
2
3
  export declare function messageRole(m: AgentMessage): string | undefined;
3
- export declare function isToolResult(m: AgentMessage): boolean;
4
+ export declare function isToolResult(m: AgentMessage): m is ToolResultMessage;
@@ -44,6 +44,8 @@ export interface ExecStreamOptions {
44
44
  maxOutputBytes?: number;
45
45
  }
46
46
  export type RemoteExecutionErrorCode = "suspended" | "command_in_flight" | "connect_failed" | "post_resume_failed" | "aborted" | "timeout" | "unsupported" | "auth_transient" | "auth_failed" | "transport_lost" | "unknown";
47
+ export declare const RETRYABLE_REMOTE_ERROR_CODES: readonly RemoteExecutionErrorCode[];
48
+ export declare function isRetryableRemoteErrorCode(code: RemoteExecutionErrorCode): boolean;
47
49
  export declare class RemoteExecutionError extends Error {
48
50
  readonly code: RemoteExecutionErrorCode;
49
51
  constructor(code: RemoteExecutionErrorCode, message: string, cause?: Error);
@@ -101,5 +103,6 @@ export interface ExecutionEnvFactoryContext {
101
103
  export type ExecutionEnvFactory = (ctx: ExecutionEnvFactoryContext) => ExecutionEnv | Promise<ExecutionEnv>;
102
104
  export declare function hasDestroy(env: ExecutionEnv): env is ExecutionEnv & Pick<RemoteExecutionEnv, "destroy">;
103
105
  export declare function isRemoteExecutionEnv(env: ExecutionEnv): env is RemoteExecutionEnv;
106
+ export declare function missingRestoreSurface(env: ExecutionEnv): readonly ("resumeVM" | "postResumeInit")[];
104
107
  export declare function isSuspendable(env: ExecutionEnv): env is RemoteExecutionEnv;
105
108
  export declare function isIsolated(env: ExecutionEnv): boolean;
@@ -1,3 +1,12 @@
1
+ export const RETRYABLE_REMOTE_ERROR_CODES = [
2
+ "auth_transient",
3
+ "connect_failed",
4
+ "timeout",
5
+ "transport_lost",
6
+ ];
7
+ export function isRetryableRemoteErrorCode(code) {
8
+ return RETRYABLE_REMOTE_ERROR_CODES.includes(code);
9
+ }
1
10
  export class RemoteExecutionError extends Error {
2
11
  code;
3
12
  constructor(code, message, cause) {
@@ -18,8 +27,17 @@ export function isRemoteExecutionEnv(env) {
18
27
  typeof e.capabilities === "object" &&
19
28
  e.capabilities !== null);
20
29
  }
30
+ export function missingRestoreSurface(env) {
31
+ const e = env;
32
+ const missing = [];
33
+ if (typeof e.resumeVM !== "function")
34
+ missing.push("resumeVM");
35
+ if (typeof e.postResumeInit !== "function")
36
+ missing.push("postResumeInit");
37
+ return missing;
38
+ }
21
39
  export function isSuspendable(env) {
22
- return isRemoteExecutionEnv(env) && env.capabilities.suspendable;
40
+ return isRemoteExecutionEnv(env) && env.capabilities.suspendable && missingRestoreSurface(env).length === 0;
23
41
  }
24
42
  export function isIsolated(env) {
25
43
  return isRemoteExecutionEnv(env) ? env.capabilities.isolation : false;
@@ -70,6 +70,7 @@ export interface ResultFlags {
70
70
  model?: string;
71
71
  unpricedSpend?: boolean;
72
72
  rewindNotes?: TaskResult["rewindNotes"];
73
+ remoteEnvFailures?: TaskResult["remoteEnvFailures"];
73
74
  abortedForTimeout: boolean;
74
75
  abortedForTurns: boolean;
75
76
  abortedLive?: boolean;
@@ -81,10 +82,12 @@ export interface ResultFlags {
81
82
  suspendRef?: {
82
83
  token: import("../checkpoint-store.js").CheckpointToken;
83
84
  gate: import("../checkpoint-store.js").CheckpointGate;
85
+ restoreMode?: "snapshot" | "park_only";
84
86
  };
85
87
  reviewRef?: {
86
88
  token: import("../checkpoint-store.js").CheckpointToken;
87
89
  gate: import("../checkpoint-store.js").CheckpointGate;
90
+ restoreMode?: "snapshot" | "park_only";
88
91
  };
89
92
  }
90
93
  export declare function errorCodeOf(err: unknown): string | undefined;
@@ -54,6 +54,7 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
54
54
  let salvagedOutput;
55
55
  let checkpointToken;
56
56
  let checkpointGate;
57
+ let workspaceRestoreMode;
57
58
  const isDegenerate = final?.stopReason === "error" && final.errorMessage === DEGENERATE_MESSAGE;
58
59
  const isWalltimeCutoff = final?.stopReason === "error" && final.errorMessage === WALLTIME_CUTOFF_MESSAGE;
59
60
  if (flags.outputInvalid) {
@@ -105,12 +106,14 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
105
106
  status = "suspended";
106
107
  checkpointToken = flags.suspendRef.token;
107
108
  checkpointGate = flags.suspendRef.gate;
109
+ workspaceRestoreMode = flags.suspendRef.restoreMode;
108
110
  }
109
111
  else if (flags.reviewRef) {
110
112
  status = "needs_review";
111
113
  errorCode = "review.pending";
112
114
  checkpointToken = flags.reviewRef.token;
113
115
  checkpointGate = flags.reviewRef.gate;
116
+ workspaceRestoreMode = flags.reviewRef.restoreMode;
114
117
  }
115
118
  else if (flags.abortedLive || final?.stopReason === "aborted") {
116
119
  status = flags.abortedForTimeout ? "timeout" : "failed";
@@ -143,5 +146,5 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
143
146
  void _internalCompaction;
144
147
  if (flags.unpricedSpend)
145
148
  delete publicStats.costMicroUsd;
146
- return { taskId, sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, checkpointToken, checkpointGate, ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), stats: publicStats };
149
+ return { taskId, sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, checkpointToken, checkpointGate, ...(workspaceRestoreMode !== undefined ? { workspaceRestoreMode } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: flags.remoteEnvFailures } : {}), stats: publicStats };
147
150
  }
@@ -127,12 +127,15 @@ export interface Prepared {
127
127
  token?: CheckpointToken;
128
128
  gate?: CheckpointGate;
129
129
  scope?: string;
130
+ restoreMode?: "snapshot" | "park_only";
130
131
  };
131
132
  reviewRef: {
132
133
  token?: CheckpointToken;
133
134
  gate?: CheckpointGate;
134
135
  scope?: string;
136
+ restoreMode?: "snapshot" | "park_only";
135
137
  };
138
+ remoteEnvFailures: NonNullable<TaskResult["remoteEnvFailures"]>;
136
139
  suspendLoopRef: {
137
140
  hit: boolean;
138
141
  };
@@ -308,6 +311,7 @@ export interface RunInternals {
308
311
  parentTaskId?: string;
309
312
  parentSessionId?: string;
310
313
  rootSessionId?: string;
314
+ registryScope?: string;
311
315
  parentCenterArtifactDigest?: string;
312
316
  parentCenterSourceRevision?: string;
313
317
  promptProfile?: "simple" | "classic";
@@ -339,4 +343,5 @@ export declare function batchContextAt(messages: AgentMessage[], currentId: stri
339
343
  completedCallIds: string[];
340
344
  };
341
345
  export declare function rebaseWorkspacePath(p: string, fromRaw: string, toRaw: string): string;
346
+ export declare function rebaseWorkspacePathAcross(p: string, froms: readonly string[], to: string): string;
342
347
  export declare function prepareTask(spec: TaskSpec, deps: RunnerDeps, sessions: SessionStore, resume?: PrepareResume, internals?: RunInternals, runnerSelf?: Runner): Promise<Prepared>;