@sema-agent/core 2.8.0 → 2.9.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 (35) hide show
  1. package/dist/agents/send-message-tool.js +37 -29
  2. package/dist/agents/subagent.js +15 -3
  3. package/dist/brain/circuit-breaker.js +18 -8
  4. package/dist/brain/retry.d.ts +1 -0
  5. package/dist/brain/retry.js +29 -7
  6. package/dist/brain/stream-engine.d.ts +1 -0
  7. package/dist/brain/stream-engine.js +74 -12
  8. package/dist/core/auto-compaction.js +9 -1
  9. package/dist/core/background-agent-store.d.ts +2 -0
  10. package/dist/core/background-agent-store.js +20 -0
  11. package/dist/core/mcp.js +8 -5
  12. package/dist/core/runner/prepare-task.js +24 -8
  13. package/dist/core/runner/runtask.js +20 -7
  14. package/dist/core/runner/tool-disclosure.d.ts +8 -3
  15. package/dist/core/runner/tool-disclosure.js +22 -8
  16. package/dist/core/skills-directory.d.ts +1 -1
  17. package/dist/core/skills-directory.js +257 -28
  18. package/dist/core/task-registry-agent.d.ts +2 -1
  19. package/dist/core/task-registry-agent.js +47 -54
  20. package/dist/core/task-registry.d.ts +1 -0
  21. package/dist/core/task-registry.js +1 -1
  22. package/dist/core/types.d.ts +5 -1
  23. package/dist/engine/compaction/compaction.js +71 -20
  24. package/dist/internal/harness-types.d.ts +1 -1
  25. package/dist/internal/harness.d.ts +1 -1
  26. package/dist/internal/harness.js +1 -1
  27. package/dist/orchestration/run-workflow-tool.d.ts +1 -0
  28. package/dist/orchestration/run-workflow-tool.js +4 -1
  29. package/dist/orchestration/workflow.d.ts +1 -1
  30. package/dist/orchestration/workflow.js +2 -2
  31. package/dist/tools/fs/bash-readonly-classifier.js +83 -17
  32. package/dist/tools/fs/fs-bash.js +17 -11
  33. package/dist/tools/fs/fs-shared.d.ts +1 -0
  34. package/dist/tools/fs/fs-shared.js +44 -2
  35. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  import { Type } from "typebox";
2
2
  import { defineTool } from "../core/tools.js";
3
3
  import { normalizeAgentName, DURABLE_AGENT_HANDLE_RE, DURABLE_AGENT_HEARTBEAT_MS } from "../core/task-registry.js";
4
- import { canAccessAgentRecord } from "../core/background-agent-store.js";
4
+ import { canAccessAgentRecord, clearRevivedRowTerminalPayload } from "../core/background-agent-store.js";
5
5
  import { escapeAttributeValue, escapeEnvelopeTag, isObserverTaskId, OBSERVER_SENDMESSAGE_SENDER_REFUSAL, OBSERVER_SENDMESSAGE_TARGET_REFUSAL, } from "./observer.js";
6
6
  import { SUBAGENT_RESUME_CAP, SubagentRetainLedger, getSessionRetainLedger } from "./retain-ledger.js";
7
7
  import { createSubagentResume } from "./subagent.js";
@@ -91,8 +91,9 @@ export function createSendMessageTool(opts) {
91
91
  }
92
92
  const senderId = ctx.taskId ?? opts.owner;
93
93
  const [parentTaskId, parentSessionId] = ctx.parentTaskId !== undefined ? [ctx.parentTaskId, ctx.parentSessionId] : [opts.parentTaskId, opts.parentSessionId];
94
- const senderIsChild = parentTaskId !== undefined || opts.uplink !== undefined || opts.senderName !== undefined || opts.siblingRetain !== undefined;
95
- const senderLabel = opts.senderName ?? (senderIsChild ? senderId ?? "main" : "main");
94
+ const senderName = ctx.spawnedAgentName ?? opts.senderName;
95
+ const senderIsChild = parentTaskId !== undefined || opts.uplink !== undefined || senderName !== undefined || opts.siblingRetain !== undefined;
96
+ const senderLabel = senderName ?? (senderIsChild ? senderId ?? "main" : "main");
96
97
  if (senderId !== undefined && isObserverTaskId(senderId)) {
97
98
  return { content: OBSERVER_SENDMESSAGE_SENDER_REFUSAL, details: { error: "observer_sender" }, isError: true };
98
99
  }
@@ -102,7 +103,7 @@ export function createSendMessageTool(opts) {
102
103
  if (normalizeAgentName(to) === "main") {
103
104
  if (opts.uplink && senderId !== undefined) {
104
105
  const uplinkSummaryRaw = typeof a.summary === "string" && a.summary.trim() !== "" ? clipSendMessageSummary(a.summary.trim()) : message.slice(0, 80);
105
- const fromLabel = opts.senderName !== undefined ? opts.senderName : senderId;
106
+ const fromLabel = senderName ?? senderId;
106
107
  try {
107
108
  opts.uplink({
108
109
  task_id: senderId,
@@ -140,6 +141,9 @@ export function createSendMessageTool(opts) {
140
141
  scope: ctx.principal ?? opts.scope,
141
142
  ...((ctx.sessionId ?? opts.sessionId) !== undefined ? { sessionId: ctx.sessionId ?? opts.sessionId } : {}),
142
143
  };
144
+ const parentAccess = parentTaskId !== undefined
145
+ ? { ...access, owner: parentTaskId, ...(parentSessionId !== undefined ? { sessionId: parentSessionId } : {}) }
146
+ : undefined;
143
147
  const tier3Revive = async (handle) => {
144
148
  if (opts.agentStore === undefined || opts.mailbox === undefined || opts.reviveSpawn === undefined)
145
149
  return undefined;
@@ -156,10 +160,9 @@ export function createSendMessageTool(opts) {
156
160
  if (!row)
157
161
  return undefined;
158
162
  if (!canAccessAgentRecord(row, access)) {
159
- if (parentTaskId === undefined)
163
+ if (parentAccess === undefined)
160
164
  return undefined;
161
- const parentView = { ...access, owner: parentTaskId, ...(parentSessionId !== undefined ? { sessionId: parentSessionId } : {}) };
162
- if (!canAccessAgentRecord(row, parentView))
165
+ if (!canAccessAgentRecord(row, parentAccess))
163
166
  return undefined;
164
167
  }
165
168
  if (row.name === undefined || row.agentType === "fork")
@@ -213,15 +216,7 @@ export function createSendMessageTool(opts) {
213
216
  const now = Date.now();
214
217
  const nextSeq = (row.seq ?? 1) + 1;
215
218
  const claimed = { ...row, status: "running", writerId: opts.registry.writerId, writerEpoch: (row.writerEpoch ?? 0) + 1, updatedAt: now, seq: nextSeq };
216
- delete claimed.settledAt;
217
- delete claimed.stoppedBy;
218
- delete claimed.finalOutput;
219
- delete claimed.error;
220
- delete claimed.resultIsPartial;
221
- delete claimed.summary;
222
- delete claimed.recentSteps;
223
- delete claimed.editedFiles;
224
- delete claimed.usage;
219
+ clearRevivedRowTerminalPayload(claimed);
225
220
  let won = false;
226
221
  try {
227
222
  won = await opts.agentStore.update(handle, scope, claimed, { rev: row.rev });
@@ -345,12 +340,11 @@ export function createSendMessageTool(opts) {
345
340
  };
346
341
  let idRow = opts.registry.getAccessibleTask(to, access);
347
342
  let resolvedAccess = access;
348
- if (!idRow && parentTaskId !== undefined) {
349
- const parentView = { ...access, owner: parentTaskId, ...(parentSessionId !== undefined ? { sessionId: parentSessionId } : {}) };
350
- const siblingById = opts.registry.getAccessibleTask(to, parentView);
343
+ if (!idRow && parentAccess !== undefined) {
344
+ const siblingById = opts.registry.getAccessibleTask(to, parentAccess);
351
345
  if (siblingById) {
352
346
  idRow = siblingById;
353
- resolvedAccess = parentView;
347
+ resolvedAccess = parentAccess;
354
348
  }
355
349
  }
356
350
  if (!idRow && DURABLE_AGENT_HANDLE_RE.test(to)) {
@@ -361,12 +355,13 @@ export function createSendMessageTool(opts) {
361
355
  let target = idRow;
362
356
  if (!target) {
363
357
  let byName = opts.registry.resolveBackgroundAgentByName(to, access);
364
- if (byName.status === "not_found" && parentTaskId !== undefined) {
365
- const parentView = { ...access, owner: parentTaskId, ...(parentSessionId !== undefined ? { sessionId: parentSessionId } : {}) };
366
- const sibling = opts.registry.resolveBackgroundAgentByName(to, parentView);
358
+ let parentByName;
359
+ if (byName.status === "not_found" && parentAccess !== undefined) {
360
+ const sibling = opts.registry.resolveBackgroundAgentByName(to, parentAccess);
361
+ parentByName = sibling;
367
362
  if (sibling.status === "found" || sibling.status === "ambiguous") {
368
363
  byName = sibling;
369
- resolvedAccess = parentView;
364
+ resolvedAccess = parentAccess;
370
365
  }
371
366
  }
372
367
  if (byName.status === "ambiguous") {
@@ -381,6 +376,8 @@ export function createSendMessageTool(opts) {
381
376
  if (opts.roster) {
382
377
  try {
383
378
  rosterHit = await opts.roster.resolve(to, access);
379
+ if (rosterHit === undefined && parentAccess !== undefined)
380
+ rosterHit = await opts.roster.resolve(to, parentAccess);
384
381
  }
385
382
  catch {
386
383
  }
@@ -396,13 +393,17 @@ export function createSendMessageTool(opts) {
396
393
  isError: true,
397
394
  };
398
395
  }
399
- const labels = opts.registry.runningBackgroundAgentLabels(access);
396
+ const suggestion = byName.suggestion ?? (parentByName?.status === "not_found" ? parentByName.suggestion : undefined);
397
+ const ownLabels = opts.registry.runningBackgroundAgentLabels(access);
398
+ const labels = parentAccess !== undefined
399
+ ? [...ownLabels, ...opts.registry.runningBackgroundAgentLabels(parentAccess).filter((l) => !ownLabels.includes(l))]
400
+ : ownLabels;
400
401
  return {
401
402
  content: `Message not sent: no agent matches "${to}" (unknown id or name, not yours, or expired).` +
402
- (byName.suggestion !== undefined ? ` Did you mean: ${byName.suggestion}?` : "") +
403
+ (suggestion !== undefined ? ` Did you mean: ${suggestion}?` : "") +
403
404
  (labels.length > 0 ? ` Running background agents: ${labels.join(", ")}.` : "") +
404
405
  ` 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.`,
405
- details: { error: "not_found", to, ...(byName.suggestion !== undefined ? { suggestion: byName.suggestion } : {}) },
406
+ details: { error: "not_found", to, ...(suggestion !== undefined ? { suggestion } : {}) },
406
407
  isError: true,
407
408
  };
408
409
  }
@@ -424,7 +425,7 @@ export function createSendMessageTool(opts) {
424
425
  : message;
425
426
  const teammateXml = frameTeammateMessage({ from: fromLabel, ...(s2Summary !== undefined ? { summary: s2Summary } : {}), text: s2Clipped });
426
427
  const delivered = await opts.registry.deliverToRunningAgent(targetId, resolvedAccess, {
427
- task_id: senderId ?? "main",
428
+ task_id: senderLabel,
428
429
  task_type: "background_agent",
429
430
  status: "event",
430
431
  summary: `message from ${fromLabel}${s2Summary !== undefined ? `: ${s2Summary}` : ""}`,
@@ -450,6 +451,13 @@ export function createSendMessageTool(opts) {
450
451
  };
451
452
  }
452
453
  const nowRow = opts.registry.getAccessibleTask(targetId, resolvedAccess);
454
+ if (nowRow?.status === "parked") {
455
+ return {
456
+ content: `Message not sent: ${who} is parked on a pending approval — it resumes when the approval is decided, not by message delivery. Send again after it resumes.`,
457
+ details: { error: "parked_pending_approval", to },
458
+ isError: true,
459
+ };
460
+ }
453
461
  const stillLive = nowRow !== undefined && (nowRow.status === "running" || nowRow.status === "pending");
454
462
  return stillLive
455
463
  ? {
@@ -524,7 +532,7 @@ export function createSendMessageTool(opts) {
524
532
  ...(opts.notify ? { currentParentNotify: opts.notify } : {}),
525
533
  });
526
534
  const summary = typeof a.summary === "string" && a.summary.trim() !== "" ? clipSendMessageSummary(a.summary.trim()) : undefined;
527
- const fromPrefix = parentTaskId !== undefined ? `(message from teammate "${opts.senderName ?? senderId ?? "unknown"}")\n` : "";
535
+ const fromPrefix = parentTaskId !== undefined ? `(message from teammate "${senderName ?? senderId ?? "unknown"}")\n` : "";
528
536
  try {
529
537
  const safeSummary = summary !== undefined ? escapeEnvelopeTag(TEAMMATE_MESSAGE_TAG, summary) : undefined;
530
538
  const safeMessage = escapeEnvelopeTag(TEAMMATE_MESSAGE_TAG, message);
@@ -1201,6 +1201,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1201
1201
  tools: [reportTool],
1202
1202
  ...(ctx.excludeTools !== undefined ? { excludeTools: [...ctx.excludeTools] } : {}),
1203
1203
  ...(ctx.deferTools !== undefined ? { deferTools: [...ctx.deferTools] } : {}),
1204
+ ...(ctx.alwaysLoadTools !== undefined ? { alwaysLoadTools: [...ctx.alwaysLoadTools] } : {}),
1204
1205
  ...(ctx.promptProfile !== undefined ? { promptProfile: ctx.promptProfile } : {}),
1205
1206
  enableBlockedReport: false,
1206
1207
  limits: { maxTurns: observerDef.maxTurns ?? 8, timeoutSec: 120 },
@@ -1344,9 +1345,17 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1344
1345
  parentToolCallId: ctx.toolCallId,
1345
1346
  ...(childAgentName ? { agentName: childAgentName } : {}),
1346
1347
  ...(agentName !== undefined ? { explicitAgentName: agentName } : {}),
1347
- ...(ctx.taskId !== undefined ? { parentTaskId: ctx.taskId } : {}),
1348
- ...(ctx.sessionId !== undefined ? { parentSessionId: ctx.sessionId } : {}),
1349
- ...((ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}),
1348
+ ...(reviveClaim !== undefined
1349
+ ? {
1350
+ ...(reviveClaim.row.parentTaskId !== undefined ? { parentTaskId: reviveClaim.row.parentTaskId } : {}),
1351
+ ...(reviveClaim.row.parentSessionId !== undefined ? { parentSessionId: reviveClaim.row.parentSessionId } : {}),
1352
+ ...(reviveClaim.row.rootSessionId !== undefined ? { rootSessionId: reviveClaim.row.rootSessionId } : {}),
1353
+ }
1354
+ : {
1355
+ ...(ctx.taskId !== undefined ? { parentTaskId: ctx.taskId } : {}),
1356
+ ...(ctx.sessionId !== undefined ? { parentSessionId: ctx.sessionId } : {}),
1357
+ ...((ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}),
1358
+ }),
1350
1359
  ...(ctx.centerArtifactDigest !== undefined ? { parentCenterArtifactDigest: ctx.centerArtifactDigest } : {}),
1351
1360
  ...(ctx.centerSourceRevision !== undefined ? { parentCenterSourceRevision: ctx.centerSourceRevision } : {}),
1352
1361
  ...(ctx.onTaskNotification !== undefined ? { parentNotify: ctx.onTaskNotification } : {}),
@@ -1375,6 +1384,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1375
1384
  ...(ctx.clientContext !== undefined ? { clientContext: ctx.clientContext } : {}),
1376
1385
  ...(ctx.excludeTools !== undefined ? { excludeTools: [...ctx.excludeTools] } : {}),
1377
1386
  ...(ctx.deferTools !== undefined ? { deferTools: [...ctx.deferTools] } : {}),
1387
+ ...(ctx.alwaysLoadTools !== undefined ? { alwaysLoadTools: [...ctx.alwaysLoadTools] } : {}),
1378
1388
  ...(ctx.promptProfile !== undefined ? { promptProfile: ctx.promptProfile } : {}),
1379
1389
  ...(ctx.additionalDirectories !== undefined ? { additionalDirectories: [...ctx.additionalDirectories] } : {}),
1380
1390
  ...(ctx.envFacts !== undefined ? { envFacts: { ...ctx.envFacts } } : {}),
@@ -1402,6 +1412,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1402
1412
  ...(plainSpec.clientContext !== undefined ? { clientContext: { ...plainSpec.clientContext } } : {}),
1403
1413
  ...(plainSpec.excludeTools !== undefined ? { excludeTools: [...plainSpec.excludeTools] } : {}),
1404
1414
  ...(plainSpec.deferTools !== undefined ? { deferTools: [...plainSpec.deferTools] } : {}),
1415
+ ...(plainSpec.alwaysLoadTools !== undefined ? { alwaysLoadTools: [...plainSpec.alwaysLoadTools] } : {}),
1405
1416
  }),
1406
1417
  internalsSnapshot: { ...childInternals },
1407
1418
  release: async () => {
@@ -1482,6 +1493,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1482
1493
  ...(plainSpec.clientContext !== undefined ? { clientContext: { ...plainSpec.clientContext } } : {}),
1483
1494
  ...(plainSpec.excludeTools !== undefined ? { excludeTools: [...plainSpec.excludeTools] } : {}),
1484
1495
  ...(plainSpec.deferTools !== undefined ? { deferTools: [...plainSpec.deferTools] } : {}),
1496
+ ...(plainSpec.alwaysLoadTools !== undefined ? { alwaysLoadTools: [...plainSpec.alwaysLoadTools] } : {}),
1485
1497
  }),
1486
1498
  internalsSnapshot: { ...childInternals },
1487
1499
  release: releaseChild,
@@ -42,20 +42,23 @@ export function createCircuitBreakerBrain(inner, opts = {}) {
42
42
  const tryPass = (key) => {
43
43
  const s = snap(key);
44
44
  if (s.phase === "closed")
45
- return true;
45
+ return { pass: true };
46
46
  if (s.phase === "open") {
47
- if (s.openedAt !== undefined && now() - s.openedAt >= cooldownMs) {
47
+ if (s.openedAt === undefined)
48
+ return { pass: false };
49
+ const elapsed = now() - s.openedAt;
50
+ if (elapsed >= cooldownMs) {
48
51
  setState(key, s.phase, { ...s, phase: "half-open", halfOpenInFlight: 1 });
49
- return true;
52
+ return { pass: true };
50
53
  }
51
- return false;
54
+ return { pass: false, retryInMs: cooldownMs - elapsed };
52
55
  }
53
56
  const inFlight = s.halfOpenInFlight ?? 0;
54
57
  if (inFlight < halfOpenProbes) {
55
58
  state.set(key, { ...s, halfOpenInFlight: inFlight + 1 });
56
- return true;
59
+ return { pass: true };
57
60
  }
58
- return false;
61
+ return { pass: false };
59
62
  };
60
63
  const record = (key, failedCode, viaProbe = true) => {
61
64
  const s = snap(key);
@@ -81,8 +84,15 @@ export function createCircuitBreakerBrain(inner, opts = {}) {
81
84
  const key = keyOf(model);
82
85
  void (async () => {
83
86
  const bypass = options?.resilience?.bypassBreaker === true;
84
- if (!bypass && !tryPass(key)) {
85
- emitBrainStatus({ phase: "circuit_open", detail: "repeated errors, pausing briefly" });
87
+ const decision = bypass ? { pass: true } : tryPass(key);
88
+ if (!decision.pass) {
89
+ emitBrainStatus({
90
+ phase: "circuit_open",
91
+ detail: "repeated errors, pausing briefly",
92
+ ...(decision.retryInMs !== undefined
93
+ ? { retryInMs: decision.retryInMs, retryInSec: Math.ceil(decision.retryInMs / 1000) }
94
+ : {}),
95
+ });
86
96
  out.push({ type: "error", reason: "error", error: errorAssistantMessage(model, "network", `${CIRCUIT_OPEN_MARKER}${key}`) });
87
97
  return;
88
98
  }
@@ -1,2 +1,3 @@
1
1
  export declare function parseRetryAfter(res: Response | undefined): number | undefined;
2
+ export declare function parseRateLimitReset(res: Response | undefined): number | undefined;
2
3
  export declare function retryBackoffMs(baseDelayMs: number, attempt: number, res?: Response, rand?: () => number): number;
@@ -1,5 +1,7 @@
1
- const MAX_BACKOFF_MS = 30_000;
2
- const MAX_RETRY_AFTER_MS = 120_000;
1
+ const MAX_BACKOFF_MS = 32_000;
2
+ const JITTER_FRACTION = 0.25;
3
+ const MAX_HEADER_WAIT_MS = 60_000;
4
+ const RATE_LIMIT_STATUS = 429;
3
5
  export function parseRetryAfter(res) {
4
6
  const h = res?.headers?.get?.("retry-after");
5
7
  if (!h)
@@ -12,11 +14,31 @@ export function parseRetryAfter(res) {
12
14
  return Math.max(0, date - Date.now());
13
15
  return undefined;
14
16
  }
17
+ export function parseRateLimitReset(res) {
18
+ const h = res?.headers?.get?.("anthropic-ratelimit-unified-reset");
19
+ if (!h)
20
+ return undefined;
21
+ const epochSecs = Number(h);
22
+ if (!Number.isFinite(epochSecs))
23
+ return undefined;
24
+ const ms = Math.round(epochSecs * 1000 - Date.now());
25
+ return ms > 0 ? ms : undefined;
26
+ }
27
+ function providerWaitHintMs(res) {
28
+ const hints = [];
29
+ const retryAfter = parseRetryAfter(res);
30
+ if (retryAfter !== undefined)
31
+ hints.push(Math.min(MAX_HEADER_WAIT_MS, retryAfter));
32
+ if (res?.status === RATE_LIMIT_STATUS) {
33
+ const reset = parseRateLimitReset(res);
34
+ if (reset !== undefined)
35
+ hints.push(Math.min(MAX_HEADER_WAIT_MS, reset));
36
+ }
37
+ return hints.length === 0 ? undefined : Math.max(...hints);
38
+ }
15
39
  export function retryBackoffMs(baseDelayMs, attempt, res, rand = Math.random) {
16
40
  const exp = Math.min(MAX_BACKOFF_MS, baseDelayMs * 2 ** attempt);
17
- const jittered = rand() * exp;
18
- const retryAfter = parseRetryAfter(res);
19
- if (retryAfter === undefined)
20
- return jittered;
21
- return Math.max(Math.min(MAX_RETRY_AFTER_MS, retryAfter), jittered);
41
+ const computed = Math.round(exp + rand() * JITTER_FRACTION * exp);
42
+ const hint = providerWaitHintMs(res);
43
+ return hint === undefined ? computed : Math.max(hint, computed);
22
44
  }
@@ -4,6 +4,7 @@ export interface StreamEngineConfig extends BrainTimeoutConfig {
4
4
  maxRetries?: number;
5
5
  retryDelayMs?: number;
6
6
  }
7
+ export declare function resolveMaxRetries(configured: number | undefined): number;
7
8
  export interface SSERequest {
8
9
  url: string;
9
10
  headers: Record<string, string>;
@@ -4,6 +4,22 @@ import { retryBackoffMs } from "./retry.js";
4
4
  import { emitBrainStatus, emitBrainTelemetry } from "./status-sink.js";
5
5
  import { createConnectController } from "./timeout.js";
6
6
  import { WALLTIME_CUTOFF_MESSAGE } from "./walltime.js";
7
+ const DEFAULT_MAX_RETRIES = 10;
8
+ const MAX_RETRIES_ENV_CEILING = 15;
9
+ const MAX_RETRIES_ENV = "SEMA_MAX_RETRIES";
10
+ export function resolveMaxRetries(configured) {
11
+ if (configured !== undefined)
12
+ return configured;
13
+ const raw = process.env[MAX_RETRIES_ENV];
14
+ if (raw !== undefined && raw.trim() !== "") {
15
+ const parsed = Number(raw);
16
+ if (Number.isFinite(parsed) && parsed >= 0)
17
+ return Math.min(Math.floor(parsed), MAX_RETRIES_ENV_CEILING);
18
+ }
19
+ return DEFAULT_MAX_RETRIES;
20
+ }
21
+ const RETRY_STATUS_SLICE_MS = 30_000;
22
+ const THINKING_RETRY_BUDGET = 2;
7
23
  const EMPTY_USAGE = {
8
24
  input: 0,
9
25
  output: 0,
@@ -43,25 +59,54 @@ function sleep(ms, signal) {
43
59
  signal?.addEventListener("abort", onAbort, { once: true });
44
60
  });
45
61
  }
62
+ async function sleepAnnouncingRetry(totalMs, signal, frame) {
63
+ let remaining = Math.max(0, totalMs);
64
+ for (;;) {
65
+ if (signal?.aborted)
66
+ return;
67
+ emitBrainStatus(frame(remaining));
68
+ if (remaining <= 0)
69
+ return;
70
+ const slice = Math.min(remaining, RETRY_STATUS_SLICE_MS);
71
+ await sleep(slice, signal);
72
+ remaining -= slice;
73
+ if (remaining <= 0)
74
+ return;
75
+ }
76
+ }
46
77
  export function runStreamingBrain(args) {
47
78
  const { model, doFetch, signal, config, httpLabel, buildRequest, makeParser, callDeadlineMs, stallTimeouts } = args;
48
79
  const out = createAssistantMessageEventStream();
49
80
  let cleanup;
81
+ let announcedRetry = false;
82
+ let terminalRetryPhase = "recovered";
83
+ let terminalRetryDetail = "recovered after retrying";
50
84
  void run()
51
85
  .catch((err) => {
52
86
  const aborted = signal?.aborted === true || isAbortError(err);
87
+ terminalRetryPhase = "gave_up";
88
+ terminalRetryDetail = aborted ? "cancelled while retrying" : "retries exhausted";
53
89
  const errorMsg = emptyAssistant(model);
54
90
  errorMsg.stopReason = aborted ? "aborted" : "error";
55
91
  errorMsg.errorMessage = err instanceof Error ? err.message : String(err);
56
92
  errorMsg.usageMissing = true;
57
93
  out.push({ type: "error", reason: aborted ? "aborted" : "error", error: errorMsg });
58
94
  })
59
- .finally(() => cleanup?.());
95
+ .finally(() => {
96
+ cleanup?.();
97
+ if (!announcedRetry)
98
+ return;
99
+ try {
100
+ emitBrainStatus({ phase: terminalRetryPhase, detail: terminalRetryDetail });
101
+ }
102
+ catch {
103
+ }
104
+ });
60
105
  return out;
61
106
  async function run() {
62
107
  const req = buildRequest();
63
- const maxRetries = config.maxRetries ?? 2;
64
- const baseDelay = config.retryDelayMs ?? 400;
108
+ const maxRetries = resolveMaxRetries(config.maxRetries);
109
+ const baseDelay = config.retryDelayMs ?? 500;
65
110
  const firstTokenTimeoutMs = config.firstTokenTimeoutMs ?? stallTimeouts?.firstTokenMs;
66
111
  const idleTimeoutMs = config.idleTimeoutMs ?? stallTimeouts?.idleMs;
67
112
  const connectTimeoutMs = config.connectTimeoutMs ?? stallTimeouts?.connectMs;
@@ -126,16 +171,19 @@ export function runStreamingBrain(args) {
126
171
  }
127
172
  const statusPhase = r?.status === 429 ? "rate_limited" : netErr !== undefined ? "reconnecting" : "retrying";
128
173
  emitBrainTelemetry({ kind: "retry", attempt: attempt + 1, phase: "connect" });
129
- emitBrainStatus({
174
+ announcedRetry = true;
175
+ await sleepAnnouncingRetry(delayMs, signal, (remainingMs) => ({
130
176
  phase: statusPhase,
131
177
  detail: statusPhase === "rate_limited"
132
178
  ? "rate limited, backing off"
133
179
  : statusPhase === "reconnecting"
134
180
  ? "connection lost, reconnecting"
135
181
  : "transient error, retrying",
136
- retryInSec: Math.ceil(delayMs / 1000),
137
- });
138
- await sleep(delayMs, signal);
182
+ retryInSec: Math.ceil(remainingMs / 1000),
183
+ retryInMs: remainingMs,
184
+ attempt: attempt + 1,
185
+ maxRetries,
186
+ }));
139
187
  continue;
140
188
  }
141
189
  if (netErr)
@@ -311,14 +359,21 @@ export function runStreamingBrain(args) {
311
359
  cleanup?.();
312
360
  cleanup = undefined;
313
361
  if (snap.hasOnlyThinking) {
314
- if (thinkingRetries >= 2)
362
+ if (thinkingRetries >= THINKING_RETRY_BUDGET)
315
363
  throw failure.err;
316
364
  thinkingRetries++;
317
365
  parser.sealForRetry();
318
366
  const delayMs = 100 * thinkingRetries;
319
367
  emitBrainTelemetry({ kind: "retry", attempt: thinkingRetries, phase: "midstream" });
320
- emitBrainStatus({ phase: "reconnecting", detail: "connection lost, reconnecting", retryInSec: Math.ceil(delayMs / 1000) });
321
- await sleep(delayMs, signal);
368
+ announcedRetry = true;
369
+ await sleepAnnouncingRetry(delayMs, signal, (remainingMs) => ({
370
+ phase: "reconnecting",
371
+ detail: "connection lost, reconnecting",
372
+ retryInSec: Math.ceil(remainingMs / 1000),
373
+ retryInMs: remainingMs,
374
+ attempt: thinkingRetries,
375
+ maxRetries: THINKING_RETRY_BUDGET,
376
+ }));
322
377
  continue;
323
378
  }
324
379
  if (attempt >= maxRetries)
@@ -326,8 +381,15 @@ export function runStreamingBrain(args) {
326
381
  const delayMs = retryBackoffMs(baseDelay, attempt);
327
382
  attempt++;
328
383
  emitBrainTelemetry({ kind: "retry", attempt, phase: "midstream" });
329
- emitBrainStatus({ phase: "reconnecting", detail: "connection lost, reconnecting", retryInSec: Math.ceil(delayMs / 1000) });
330
- await sleep(delayMs, signal);
384
+ announcedRetry = true;
385
+ await sleepAnnouncingRetry(delayMs, signal, (remainingMs) => ({
386
+ phase: "reconnecting",
387
+ detail: "connection lost, reconnecting",
388
+ retryInSec: Math.ceil(remainingMs / 1000),
389
+ retryInMs: remainingMs,
390
+ attempt,
391
+ maxRetries,
392
+ }));
331
393
  }
332
394
  }
333
395
  }
@@ -1,4 +1,4 @@
1
- import { DEFAULT_CHARS_PER_TOKEN, DEFAULT_CLAMP_TOLERANCE, DEFAULT_COMPACTION_SETTINGS, CompactionError, compact, computeFileLists, dryRunSummarizationClamp, estimateContextTokens, estimateTokens, prepareCompaction, shouldCompact, } from "../internal/harness.js";
1
+ import { DEFAULT_CHARS_PER_TOKEN, DEFAULT_CLAMP_TOLERANCE, DEFAULT_COMPACTION_SETTINGS, CompactionError, compact, computeFileLists, dryRunSummarizationClamp, estimateContextTokens, estimateTokens, formatPersistedOutputRefs, prepareCompaction, shouldCompact, } from "../internal/harness.js";
2
2
  import { fileArgPath } from "../tools/fs/safety.js";
3
3
  import { contextEditFrontier } from "./context-edit.js";
4
4
  import { selectCompactionEpoch } from "../prompt-assembly/epoch.js";
@@ -155,6 +155,14 @@ export async function maybeCompact(opts) {
155
155
  if (prep.value.invokedSkills.length > 0) {
156
156
  details.invokedSkills = prep.value.invokedSkills;
157
157
  }
158
+ const reusedRefs = prep.value.persistedOutputRefs ?? [];
159
+ if (reusedRefs.length > 0) {
160
+ details.persistedOutputRefs = reusedRefs;
161
+ summary += formatPersistedOutputRefs(reusedRefs);
162
+ }
163
+ if (prep.value.elidedMessages !== undefined && prep.value.elidedMessages > 0) {
164
+ details.elidedMessages = prep.value.elidedMessages;
165
+ }
158
166
  }
159
167
  else {
160
168
  let summaryModel = opts.compactionModel ?? opts.model;
@@ -46,6 +46,8 @@ export interface BackgroundAgentRecord {
46
46
  usage?: BackgroundAgentUsage;
47
47
  rev: number;
48
48
  }
49
+ export declare const REVIVED_ROW_CLEARED_FIELDS: readonly ["settledAt", "stoppedBy", "completionId", "finalOutput", "finalOutputFull", "error", "errorCode", "errorRetryable", "errorKind", "resultIsPartial", "summary", "recentSteps", "editedFiles", "usage"];
50
+ export declare function clearRevivedRowTerminalPayload(record: BackgroundAgentRecord): void;
49
51
  export interface BackgroundAgentRowSummary {
50
52
  handle: string;
51
53
  owner: string;
@@ -1,4 +1,24 @@
1
1
  import { uuidv7 } from "../internal/harness.js";
2
+ export const REVIVED_ROW_CLEARED_FIELDS = [
3
+ "settledAt",
4
+ "stoppedBy",
5
+ "completionId",
6
+ "finalOutput",
7
+ "finalOutputFull",
8
+ "error",
9
+ "errorCode",
10
+ "errorRetryable",
11
+ "errorKind",
12
+ "resultIsPartial",
13
+ "summary",
14
+ "recentSteps",
15
+ "editedFiles",
16
+ "usage",
17
+ ];
18
+ export function clearRevivedRowTerminalPayload(record) {
19
+ for (const field of REVIVED_ROW_CLEARED_FIELDS)
20
+ delete record[field];
21
+ }
2
22
  export class BackgroundAgentStoreError extends Error {
3
23
  code;
4
24
  constructor(code, message) {
package/dist/core/mcp.js CHANGED
@@ -145,6 +145,11 @@ 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
+ function namedMcpFailureText(err) {
149
+ const detail = err instanceof Error ? err.message : String(err);
150
+ const condition = err instanceof McpError ? describeMcpSpecErrorCode(err.code) : undefined;
151
+ return condition !== undefined ? `${condition}: ${detail}` : detail;
152
+ }
148
153
  function isTransportLost(err) {
149
154
  if (err instanceof McpError && err.code === ErrorCode.ConnectionClosed)
150
155
  return true;
@@ -555,7 +560,7 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
555
560
  }
556
561
  else {
557
562
  warnings.push(asServerWarning(spec, r.reason));
558
- statuses.push({ name: spec.name, status: "failed", error: r.reason instanceof Error ? r.reason.message : String(r.reason) });
563
+ statuses.push({ name: spec.name, status: "failed", error: namedMcpFailureText(r.reason) });
559
564
  }
560
565
  }
561
566
  const resourceTools = buildResourceTools(resourceServers);
@@ -614,7 +619,7 @@ export async function materializeMcpTools(specs, principal, onElicit, imageResiz
614
619
  toolCount: h.toolNames.length,
615
620
  added: [],
616
621
  removed: [],
617
- error: inlineUntrusted(err instanceof Error ? err.message : String(err), 240),
622
+ error: inlineUntrusted(namedMcpFailureText(err), 240),
618
623
  });
619
624
  }
620
625
  }
@@ -1155,9 +1160,7 @@ function intakeListedTools(listed, spec, client, health, imageResizer) {
1155
1160
  return { serverTools, serverAxes, dropped };
1156
1161
  }
1157
1162
  function asServerWarning(spec, err) {
1158
- const detail = err instanceof Error ? err.message : String(err);
1159
- const condition = err instanceof McpError ? describeMcpSpecErrorCode(err.code) : undefined;
1160
- const warning = new Error(`mcp: server "${spec.name}" failed to connect — skipped (${condition !== undefined ? `${condition}: ` : ""}${detail})`, { cause: err });
1163
+ const warning = new Error(`mcp: server "${spec.name}" failed to connect — skipped (${namedMcpFailureText(err)})`, { cause: err });
1161
1164
  warning.code = "mcp.server_unavailable";
1162
1165
  return warning;
1163
1166
  }
@@ -758,6 +758,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
758
758
  clientContext: spec.clientContext,
759
759
  excludeTools: toolFaceSnapshot.exclude,
760
760
  deferTools: toolFaceSnapshot.defer,
761
+ alwaysLoadTools: toolFaceSnapshot.alwaysLoad,
761
762
  promptProfile,
762
763
  ...(spec.additionalDirectories !== undefined ? { additionalDirectories: Object.freeze([...spec.additionalDirectories]) } : {}),
763
764
  ...(spec.envFacts !== undefined ? { envFacts: { ...spec.envFacts } } : {}),
@@ -885,6 +886,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
885
886
  governanceBaseline: deps.workflowGovernanceBaseline,
886
887
  parentExcludeTools: toolFaceSnapshot.exclude,
887
888
  parentDeferTools: toolFaceSnapshot.defer,
889
+ parentAlwaysLoadTools: toolFaceSnapshot.alwaysLoad,
888
890
  parentPromptProfile: promptProfile,
889
891
  models: deps.models,
890
892
  agents: deps.agents,
@@ -1796,7 +1798,9 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1796
1798
  deferNames: (toolFaceSnapshot.defer ?? []).filter((n) => tools.some((t) => t.name === n)),
1797
1799
  alwaysLoadNames: [
1798
1800
  ...(toolFaceSnapshot.alwaysLoad ?? []),
1799
- ...mcp.tools.filter((t) => t.mcpAlwaysLoad === true).map((t) => t.name),
1801
+ ...mcp.tools
1802
+ .filter((t) => t.mcpAlwaysLoad === true && !(toolFaceSnapshot.defer ?? []).includes(t.name))
1803
+ .map((t) => t.name),
1800
1804
  ],
1801
1805
  });
1802
1806
  for (const n of [...deferred]) {
@@ -1831,21 +1835,32 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1831
1835
  throw e;
1832
1836
  }
1833
1837
  const registry = buildDeferredRegistry(deferred, tools);
1834
- const realByName = new Map(tools.map((t) => [t.name, t]));
1835
1838
  const directCallFor = (name) => {
1836
1839
  if (spec.deferSelfResolve === false)
1837
1840
  return undefined;
1838
- const real = realByName.get(name);
1839
- if (real === undefined)
1840
- return undefined;
1841
+ const executionMode = tools.find((t) => t.name === name)?.executionMode;
1841
1842
  return {
1842
- parameters: real.parameters,
1843
- invoke: (toolCallId, params, signal) => real.execute(toolCallId, params, signal),
1843
+ resolveReal: () => {
1844
+ const real = tools.find((t) => t.name === name);
1845
+ if (real === undefined)
1846
+ return undefined;
1847
+ return {
1848
+ parameters: real.parameters,
1849
+ invoke: (toolCallId, params, signal, onUpdate) => real.execute(toolCallId, params, signal, onUpdate),
1850
+ };
1851
+ },
1852
+ ...(executionMode !== undefined ? { executionMode } : {}),
1844
1853
  activate: async () => {
1845
1854
  if (activeTools.has(name))
1846
1855
  return;
1847
1856
  activeTools.add(name);
1848
- await rematerialize(activeTools);
1857
+ try {
1858
+ await rematerialize(activeTools);
1859
+ }
1860
+ catch (e) {
1861
+ activeTools.delete(name);
1862
+ throw e;
1863
+ }
1849
1864
  },
1850
1865
  };
1851
1866
  };
@@ -1918,6 +1933,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1918
1933
  rematerialize,
1919
1934
  listingRide: (newly) => listingRideRef.current?.(newly),
1920
1935
  mountedNames: () => new Set(buildToolList(activeTools).map((t) => t.name).filter((n) => !registry.has(n) || activeTools.has(n))),
1936
+ directCallEnabled: spec.deferSelfResolve !== false,
1921
1937
  });
1922
1938
  harnessTools = buildToolList(activeTools);
1923
1939
  }