@sema-agent/core 2.9.0 → 2.10.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.
@@ -17,7 +17,7 @@ import { addWorktree } from "../core/git-worktree-env.js";
17
17
  import { shellQuote } from "../tools/fs/search.js";
18
18
  import { BG_AGENT_REAP_STOP_ERROR } from "../core/task-registry.js";
19
19
  import { extractErrorCode } from "../brain/errors.js";
20
- import { FORK_DEFAULT_MAX_TURNS, SESSION_BG_DEFAULT_TIMEOUT_SEC, RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX } from "../config/defaults.js";
20
+ import { FORK_DEFAULT_MAX_TURNS, SESSION_BG_DEFAULT_TIMEOUT_SEC, RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX, RUNNING_AGENT_OBSERVE_EVERY_BEATS } from "../config/defaults.js";
21
21
  export { FORK_DEFAULT_MAX_TURNS, SESSION_BG_DEFAULT_TIMEOUT_SEC, RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX };
22
22
  import { SUBAGENT_RESUME_CAP, SubagentRetainLedger, getOrCreateSessionRetainLedger, ensureSessionReapHook, createResumePrompt, } from "./retain-ledger.js";
23
23
  import { recordRosterSpawn } from "./roster-store.js";
@@ -227,6 +227,21 @@ function errorKindClause(c) {
227
227
  const FAILED_SESSION_RETAIN_TTL_MS = 15 * 60 * 1000;
228
228
  const PARTIAL_FINDINGS_MAX_CHARS = 1200;
229
229
  const BG_NOTIFY_DRAIN_WINDOW_MS = 2_000;
230
+ function createBgActivityBeat(parentToolCallId, emitTick) {
231
+ let beats = 0;
232
+ let starts = 0;
233
+ return (e) => {
234
+ if (e.type !== "tool_start" && e.type !== "tool_end")
235
+ return;
236
+ if (parentToolCallId === undefined || e.parentToolCallId !== parentToolCallId)
237
+ return;
238
+ if (e.type === "tool_start")
239
+ starts += 1;
240
+ beats += 1;
241
+ if (beats === 1 || beats % RUNNING_AGENT_OBSERVE_EVERY_BEATS === 0)
242
+ emitTick(starts);
243
+ };
244
+ }
230
245
  function markerFragment() {
231
246
  return uuidv7().replace(/-/g, "").slice(-12);
232
247
  }
@@ -351,6 +366,25 @@ export function createSubagentResume(deps) {
351
366
  }
352
367
  }
353
368
  reviveStartedAt = Date.now();
369
+ const reviveActivityBeat = createBgActivityBeat(deps.parentToolCallId, (toolStarts) => {
370
+ if (reviveEmit === undefined)
371
+ return;
372
+ const currentAction = resumeStepRecorder.currentAction();
373
+ const currentTool = resumeStepRecorder.currentActionStructured();
374
+ reviveEmit({
375
+ kind: "tick",
376
+ taskId: deps.taskId,
377
+ sessionScoped: deps.sessionScoped === true,
378
+ ...(deps.rowAgentType !== undefined ? { agentType: deps.rowAgentType } : {}),
379
+ transcriptId: entry.childSessionId,
380
+ sessionId: entry.childSessionId,
381
+ ...(deps.parentToolCallId !== undefined ? { parentToolCallId: deps.parentToolCallId } : {}),
382
+ progressTaskId: entry.childSessionId,
383
+ ...(currentAction !== undefined ? { currentAction } : {}),
384
+ ...(currentTool !== undefined ? { currentTool } : {}),
385
+ usage: { toolUses: toolStarts },
386
+ });
387
+ });
354
388
  stream = childRunner.runTaskStream(resumeSpec, undefined, {
355
389
  ...entry.internalsSnapshot,
356
390
  ...(true
@@ -362,6 +396,7 @@ export function createSubagentResume(deps) {
362
396
  }
363
397
  catch {
364
398
  }
399
+ reviveActivityBeat(e);
365
400
  if (reviveEmit !== undefined && e.type === "task_progress") {
366
401
  reviveEmit({
367
402
  kind: "tick",
@@ -1698,6 +1733,23 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1698
1733
  const s2ForkNotifyReady = (inject) => {
1699
1734
  bg.registry.attachAgentNotify(taskId, inject);
1700
1735
  };
1736
+ const bgForkActivityBeat = createBgActivityBeat(ctx.toolCallId, (toolStarts) => {
1737
+ const currentAction = stepRecorder.currentAction();
1738
+ const currentTool = stepRecorder.currentActionStructured();
1739
+ sinkEmit({
1740
+ kind: "tick",
1741
+ taskId,
1742
+ sessionScoped: sessionScopedBg === true,
1743
+ transcriptId: forkedId,
1744
+ sessionId: forkedId,
1745
+ parentToolCallId: ctx.toolCallId,
1746
+ progressTaskId: forkedId,
1747
+ agentType: spawnAgentType,
1748
+ ...(currentAction !== undefined ? { currentAction } : {}),
1749
+ ...(currentTool !== undefined ? { currentTool } : {}),
1750
+ usage: { toolUses: toolStarts },
1751
+ });
1752
+ });
1701
1753
  const bgForkInternals = bgSink
1702
1754
  ? {
1703
1755
  ...forkInternals,
@@ -1708,6 +1760,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1708
1760
  }
1709
1761
  catch {
1710
1762
  }
1763
+ bgForkActivityBeat(e);
1711
1764
  if (e.type === "task_progress") {
1712
1765
  const currentAction = stepRecorder.currentAction();
1713
1766
  const currentTool = stepRecorder.currentActionStructured();
@@ -1764,6 +1817,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1764
1817
  ? classifySubagentError({ status: "failed", ...(child.errorCode !== undefined ? { errorCode: child.errorCode } : {}), ...(child.errorMessage !== undefined ? { errorMessage: child.errorMessage } : {}) })
1765
1818
  : undefined;
1766
1819
  const settledBg = bg.registry.settleBackgroundAgent(taskId, {
1820
+ cycle: 0,
1767
1821
  status: okBg ? "completed" : reapedBg ? "killed" : "failed",
1768
1822
  ...resultSettleFields(child.result),
1769
1823
  ...(!okBg ? { error: reapedBg ? (collateralBg ? BG_AGENT_COLLATERAL_REAP_REASON : BG_AGENT_REAP_STOP_ERROR) : child.errorMessage ?? String(child.status) } : {}),
@@ -1835,6 +1889,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1835
1889
  const errCodeForkReject = killed ? undefined : extractErrorCode(msgFork);
1836
1890
  const errClassForkReject = killed ? undefined : classifySubagentError({ status: "failed", errorMessage: msgFork });
1837
1891
  const settledBg = bg.registry.settleBackgroundAgent(taskId, {
1892
+ cycle: 0,
1838
1893
  status: killed ? "killed" : "failed",
1839
1894
  error: killed ? BG_AGENT_REAP_STOP_ERROR : msgFork,
1840
1895
  ...(errCodeForkReject !== undefined ? { errorCode: errCodeForkReject } : {}),
@@ -2157,6 +2212,23 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2157
2212
  bg.registry.finalizeParkedResume(taskId);
2158
2213
  reviveAttachedResolve?.();
2159
2214
  };
2215
+ const bgActivityBeat = createBgActivityBeat(ctx.toolCallId, (toolStarts) => {
2216
+ const currentAction = stepRecorder.currentAction();
2217
+ const currentTool = stepRecorder.currentActionStructured();
2218
+ sinkEmit({
2219
+ kind: "tick",
2220
+ taskId,
2221
+ sessionScoped: sessionScopedBg === true,
2222
+ transcriptId: bgChildSessionId,
2223
+ sessionId: bgChildSessionId,
2224
+ parentToolCallId: ctx.toolCallId,
2225
+ progressTaskId: bgChildSessionId,
2226
+ agentType: spawnAgentType,
2227
+ ...(currentAction !== undefined ? { currentAction } : {}),
2228
+ ...(currentTool !== undefined ? { currentTool } : {}),
2229
+ usage: { toolUses: toolStarts },
2230
+ });
2231
+ });
2160
2232
  const bgInternals = bgSink
2161
2233
  ? {
2162
2234
  ...childInternals,
@@ -2167,6 +2239,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2167
2239
  }
2168
2240
  catch {
2169
2241
  }
2242
+ bgActivityBeat(e);
2170
2243
  if (e.type === "task_progress") {
2171
2244
  const currentAction = stepRecorder.currentAction();
2172
2245
  const currentTool = stepRecorder.currentActionStructured();
@@ -2400,6 +2473,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2400
2473
  ? classifySubagentError({ status: "failed", ...(child.errorCode !== undefined ? { errorCode: child.errorCode } : {}), ...(child.errorMessage !== undefined ? { errorMessage: child.errorMessage } : {}) })
2401
2474
  : undefined;
2402
2475
  const settled = bg.registry.settleBackgroundAgent(taskId, {
2476
+ cycle: 0,
2403
2477
  status: ok ? "completed" : reaped ? "killed" : "failed",
2404
2478
  seq: seqAtSettle ?? 1,
2405
2479
  ...resultSettleFields(child.result),
@@ -2496,6 +2570,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2496
2570
  const errCodeBgReject = killed ? undefined : extractErrorCode(msg);
2497
2571
  const errClassBgReject = killed ? undefined : classifySubagentError({ status: "failed", errorMessage: msg });
2498
2572
  const settled = bg.registry.settleBackgroundAgent(taskId, {
2573
+ cycle: 0,
2499
2574
  status: killed ? "killed" : "failed",
2500
2575
  error: msg,
2501
2576
  seq: seqAtSettle ?? 1,
@@ -4,3 +4,4 @@ export declare const SESSION_BG_DEFAULT_TIMEOUT_SEC: number;
4
4
  export declare const RETAIN_DEFAULT_TTL_MS: number;
5
5
  export declare const RETAIN_DEFAULT_MAX = 16;
6
6
  export declare const SESSION_DEFAULT_TTL_DAYS = 7;
7
+ export declare const RUNNING_AGENT_OBSERVE_EVERY_BEATS = 4;
@@ -4,3 +4,4 @@ export const SESSION_BG_DEFAULT_TIMEOUT_SEC = 30 * 60;
4
4
  export const RETAIN_DEFAULT_TTL_MS = 30 * 60 * 1000;
5
5
  export const RETAIN_DEFAULT_MAX = 16;
6
6
  export const SESSION_DEFAULT_TTL_DAYS = 7;
7
+ export const RUNNING_AGENT_OBSERVE_EVERY_BEATS = 4;
@@ -69,6 +69,7 @@ export interface ResultFlags {
69
69
  threw: unknown;
70
70
  model?: string;
71
71
  unpricedSpend?: boolean;
72
+ rewindNotes?: TaskResult["rewindNotes"];
72
73
  abortedForTimeout: boolean;
73
74
  abortedForTurns: boolean;
74
75
  abortedLive?: boolean;
@@ -143,5 +143,5 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
143
143
  void _internalCompaction;
144
144
  if (flags.unpricedSpend)
145
145
  delete publicStats.costMicroUsd;
146
- return { taskId, sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, checkpointToken, checkpointGate, stats: publicStats };
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 };
147
147
  }
@@ -17,7 +17,7 @@ import { type CwdRef } from "../../tools/fs/index.js";
17
17
  import type { Runner } from "./runtask.js";
18
18
  import { type CheckpointGate, type CheckpointState, type CheckpointToken, type ResourceLedger, type ResourceLimitReason } from "../checkpoint-store.js";
19
19
  import type { AgentMessage, AgentTool, ExecutionEnv } from "../../internal/harness.js";
20
- import type { RunnerDeps, TaskEvent, TaskSpec, ToolActivity, ToolEffect } from "../types.js";
20
+ import type { RunnerDeps, TaskEvent, TaskResult, TaskSpec, ToolActivity, ToolEffect } from "../types.js";
21
21
  import type { RepairBundle } from "../../agents/repair-loop.js";
22
22
  export declare const DEFAULT_IRREVERSIBLE_SCOPE = "irreversible";
23
23
  export declare function checkpointScopeOf(spec: {
@@ -48,6 +48,7 @@ export interface Prepared {
48
48
  tasks: number;
49
49
  costMicroUsd: number;
50
50
  };
51
+ rewindNotes?: NonNullable<TaskResult["rewindNotes"]>;
51
52
  cwdRef?: CwdRef;
52
53
  worktreeSessionRef?: {
53
54
  current?: {
@@ -616,8 +616,37 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
616
616
  catch {
617
617
  }
618
618
  }
619
- let rewindTarget = spec.resumeAt !== undefined ? (spec.rewindFiles ? spec.resumeAt : undefined) : spec.rewindFilesTo;
619
+ const rewindNotes = [];
620
+ const rewindCaptureRequested = spec.rewindFiles === true;
621
+ let rewindTarget = spec.resumeAt !== undefined ? (rewindCaptureRequested ? spec.resumeAt : undefined) : spec.rewindFilesTo;
620
622
  const rewindBefore = rewindTarget !== undefined && spec.resumeAt !== undefined && spec.resumeAtMode === "before";
623
+ if (spec.resumeAt !== undefined && !rewindCaptureRequested) {
624
+ rewindNotes.push({
625
+ code: "conversation_only",
626
+ message: `the conversation was branched at entry "${spec.resumeAt}" but the working tree was NOT rewound — this task set resumeAt without rewindFiles, so files remain at their current state`,
627
+ });
628
+ }
629
+ if (!deps.fileSnapshotStore) {
630
+ if (rewindTarget !== undefined) {
631
+ const e = new Error(`rewind-files: restoring the working tree to entry "${rewindTarget}" requires a snapshot backend, but this deployment wired no RunnerDeps.fileSnapshotStore — no snapshot was ever captured, so the files were NOT rewound`);
632
+ e.code = "rewind.store_unconfigured";
633
+ throw e;
634
+ }
635
+ if (rewindCaptureRequested) {
636
+ rewindNotes.push({
637
+ code: "snapshot_store_unconfigured",
638
+ message: "rewindFiles was requested but no RunnerDeps.fileSnapshotStore is wired — no working-tree snapshot was captured for this turn, so it cannot be rewound to later",
639
+ });
640
+ }
641
+ }
642
+ else if (!handsEnabled) {
643
+ if (rewindTarget !== undefined || rewindCaptureRequested) {
644
+ rewindNotes.push({
645
+ code: "files_env_unsupported",
646
+ message: "the file side of rewind was inert: this deployment mounts no filesystem-capable ExecutionEnv, so no working-tree snapshot was captured or restored",
647
+ });
648
+ }
649
+ }
621
650
  if (rewindTarget !== undefined && deps.fileSnapshotStore && handsEnabled) {
622
651
  if (rewindBefore) {
623
652
  let anchor;
@@ -642,18 +671,13 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
642
671
  const restoreSignal = spec.signal ? AbortSignal.any([abortController.signal, spec.signal]) : abortController.signal;
643
672
  const restored = await deps.fileSnapshotStore.restore(sessionId, rewindTarget, executionEnv, restoreRoot, restoreSignal);
644
673
  if (!restored.ok) {
645
- if (restored.error.code === "not_found" && rewindBefore) {
646
- const e = new Error(`rewind-files: the resolved "before" snapshot anchor "${rewindTarget}" disappeared before restore — files were NOT rewound`);
674
+ if (restored.error.code === "not_found") {
675
+ const e = new Error(rewindBefore
676
+ ? `rewind-files: the resolved "before" snapshot anchor "${rewindTarget}" disappeared before restore — files were NOT rewound`
677
+ : `rewind-files: no file snapshot exists for entry "${rewindTarget}" on session "${sessionId}" — the working tree was NOT rewound (only a COMPLETED turn that ran with rewindFiles is snapshotted, and snapshots are keyed by that turn's END leaf; earlier turns that ran without rewindFiles, mid-turn entries, and reaped snapshots have none)`);
647
678
  e.code = "rewind_snapshot.unresolvable";
648
679
  throw e;
649
680
  }
650
- if (restored.error.code === "not_found") {
651
- try {
652
- deps.onError?.(new Error(`rewind-files: no snapshot for entry "${rewindTarget}" — files left unchanged`), { phase: "rewind", sessionId });
653
- }
654
- catch {
655
- }
656
- }
657
681
  else {
658
682
  const e = new Error(`rewind-files restore failed (${restored.error.code}): ${restored.error.message}`);
659
683
  e.code = "rewind.restore_failed";
@@ -1852,7 +1876,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1852
1876
  ...(executionMode !== undefined ? { executionMode } : {}),
1853
1877
  activate: async () => {
1854
1878
  if (activeTools.has(name))
1855
- return;
1879
+ return undefined;
1856
1880
  activeTools.add(name);
1857
1881
  try {
1858
1882
  await rematerialize(activeTools);
@@ -1861,6 +1885,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1861
1885
  activeTools.delete(name);
1862
1886
  throw e;
1863
1887
  }
1888
+ return listingRideRef.current?.([name]);
1864
1889
  },
1865
1890
  };
1866
1891
  };
@@ -3357,7 +3382,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3357
3382
  : undefined;
3358
3383
  overheadState.promptChars = systemPrompt.length;
3359
3384
  const preparedHolder = {};
3360
- const buildPrepared = () => ({ harness, session, sessionId, taskRootPath, model, thinking, compModel, mcp: mcp, blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, nestedStats, cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), releaseSignal, cacheBreakDetector, cacheFingerprint, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), ownedEnv, suspendRef, reviewRef, reviewRequestRef, suspendLoopRef, suspendForResource, ...(callCapRef ? { callCapRef } : {}), ...(cutKills ? { cutKills } : {}), ...(callCapRef ? { callIssuedAtRef } : {}), suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, promptOverheadTokens, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
3385
+ const buildPrepared = () => ({ harness, session, sessionId, taskRootPath, model, thinking, compModel, mcp: mcp, blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), releaseSignal, cacheBreakDetector, cacheFingerprint, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), ownedEnv, suspendRef, reviewRef, reviewRequestRef, suspendLoopRef, suspendForResource, ...(callCapRef ? { callCapRef } : {}), ...(cutKills ? { cutKills } : {}), ...(callCapRef ? { callIssuedAtRef } : {}), suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, promptOverheadTokens, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
3361
3386
  const prepared = buildPrepared();
3362
3387
  preparedHolder.current = prepared;
3363
3388
  return prepared;
@@ -2764,6 +2764,7 @@ export class Runner {
2764
2764
  threw,
2765
2765
  model: prepared.model.id,
2766
2766
  unpricedSpend: rs.telemetry.unpricedSpend,
2767
+ rewindNotes: prepared.rewindNotes,
2767
2768
  abortedForTimeout: timeout.fired,
2768
2769
  abortedForTurns: rs.limits.turnsExceeded,
2769
2770
  abortedLive,
@@ -3045,10 +3046,19 @@ export class Runner {
3045
3046
  if (!leafId)
3046
3047
  return;
3047
3048
  const root = prepared.taskRootPath;
3048
- const refusedAt = this.snapshotTooLargeRoots.get(root);
3049
- if (refusedAt !== undefined) {
3050
- if (Date.now() - refusedAt < SNAPSHOT_TOO_LARGE_TTL_MS)
3049
+ const refusal = this.snapshotTooLargeRoots.get(root);
3050
+ if (refusal !== undefined) {
3051
+ if (Date.now() - refusal.refusedAt < SNAPSHOT_TOO_LARGE_TTL_MS) {
3052
+ if (!refusal.skipAnnounced) {
3053
+ refusal.skipAnnounced = true;
3054
+ try {
3055
+ this.deps.onError?.(new Error(`rewind-files snapshot skipped for this working-tree root: a too_large refusal is still inside its ${SNAPSHOT_TOO_LARGE_TTL_MS / 60_000}-minute cooldown (re-probed at ${new Date(refusal.refusedAt + SNAPSHOT_TOO_LARGE_TTL_MS).toISOString()}) — turns completed during the cooldown capture no snapshot and cannot be rewound to. Announced once per cooldown window.`), { phase: "rewind", sessionId: prepared.sessionId, classification: "too_large" });
3056
+ }
3057
+ catch {
3058
+ }
3059
+ }
3051
3060
  return;
3061
+ }
3052
3062
  this.snapshotTooLargeRoots.delete(root);
3053
3063
  }
3054
3064
  const ac = new AbortController();
@@ -3071,11 +3081,14 @@ export class Runner {
3071
3081
  }
3072
3082
  if (!r.ok) {
3073
3083
  const tooLarge = r.error.code === "too_large";
3084
+ const refusedAt = Date.now();
3074
3085
  if (tooLarge)
3075
- this.snapshotTooLargeRoots.set(root, Date.now());
3086
+ this.snapshotTooLargeRoots.set(root, { refusedAt, skipAnnounced: false });
3076
3087
  try {
3077
3088
  this.deps.onError?.(new Error(`rewind-files snapshot failed (${r.error.code}): ${r.error.message}` +
3078
- (tooLarge ? " — skipping further snapshots for this root (this process); raise the store's snapshotBounds or shrink the tree to re-enable rewind" : "")), { phase: "rewind", sessionId: prepared.sessionId, ...(tooLarge ? { classification: "too_large" } : {}) });
3089
+ (tooLarge
3090
+ ? ` — skipping further snapshots for this root (this process) during a ${SNAPSHOT_TOO_LARGE_TTL_MS / 60_000}-minute cooldown, until ${new Date(refusedAt + SNAPSHOT_TOO_LARGE_TTL_MS).toISOString()}, when the tree is re-probed automatically. Raise the store's snapshotBounds to accept a tree this size; shrinking the tree also re-enables rewind, but only at that re-probe — not immediately`
3091
+ : "")), { phase: "rewind", sessionId: prepared.sessionId, ...(tooLarge ? { classification: "too_large" } : {}) });
3079
3092
  }
3080
3093
  catch {
3081
3094
  }
@@ -31,7 +31,7 @@ export declare function buildDeferredRegistry(deferred: ReadonlySet<string>, too
31
31
  export interface PlaceholderDirectCall {
32
32
  resolveReal: () => PlaceholderDirectTarget | undefined;
33
33
  executionMode?: ToolExecutionMode;
34
- activate: () => Promise<void>;
34
+ activate: () => Promise<string | undefined>;
35
35
  }
36
36
  export interface PlaceholderDirectTarget {
37
37
  parameters: TSchema;
@@ -79,8 +79,11 @@ export function createPlaceholderTool(info, direct) {
79
79
  execute: async (toolCallId, params, signal, onUpdate) => {
80
80
  const real = direct.resolveReal();
81
81
  if (real !== undefined && Value.Check(real.parameters, params)) {
82
- await direct.activate();
83
- return real.invoke(toolCallId, params, signal, onUpdate);
82
+ const ride = await direct.activate();
83
+ const result = await real.invoke(toolCallId, params, signal, onUpdate);
84
+ if (ride === undefined || ride === "")
85
+ return result;
86
+ return { ...result, content: [...result.content, { type: "text", text: ride }] };
84
87
  }
85
88
  return teachingRejection();
86
89
  },
@@ -168,15 +171,27 @@ export function resolveToolSearch(args, registry) {
168
171
  }
169
172
  export function extractDiscoveredToolNames(messages, registry) {
170
173
  const names = new Set();
174
+ const pendingDirect = new Map();
171
175
  for (const m of messages) {
176
+ if (m.role === "toolResult") {
177
+ const name = pendingDirect.get(m.toolCallId);
178
+ if (name !== undefined && m.isError !== true)
179
+ names.add(name);
180
+ continue;
181
+ }
172
182
  if (m.role !== "assistant")
173
183
  continue;
174
184
  for (const part of m.content) {
175
- if (part.type === "toolCall" && part.name === TOOL_SEARCH_NAME) {
185
+ if (part.type !== "toolCall")
186
+ continue;
187
+ if (part.name === TOOL_SEARCH_NAME) {
176
188
  for (const n of resolveToolSearch(part.arguments, registry)) {
177
189
  names.add(n);
178
190
  }
179
191
  }
192
+ else if (registry.has(part.name)) {
193
+ pendingDirect.set(part.id, part.name);
194
+ }
180
195
  }
181
196
  }
182
197
  return [...names];
@@ -652,6 +652,9 @@ export function settleBackgroundAgentLane(core, id, outcome) {
652
652
  const handle = core.handles.get(id);
653
653
  if (!handle || handle.type !== "background_agent")
654
654
  return undefined;
655
+ if (outcome.cycle === undefined && (handle.reviveCycle ?? 0) !== 0) {
656
+ throw new Error(`settleBackgroundAgent("${id}"): the row has been revived (cycle ${handle.reviveCycle}) — pass the cycle this settle speaks for, or use settleRevivedAgent`);
657
+ }
655
658
  if ((outcome.cycle ?? 0) !== (handle.reviveCycle ?? 0))
656
659
  return undefined;
657
660
  if (handle.status !== "running") {
@@ -390,6 +390,10 @@ export interface TaskResult {
390
390
  atTurn: number;
391
391
  };
392
392
  structuredOutput?: unknown;
393
+ rewindNotes?: Array<{
394
+ code: "conversation_only" | "files_env_unsupported" | "snapshot_store_unconfigured";
395
+ message: string;
396
+ }>;
393
397
  stats: {
394
398
  turns: number;
395
399
  tokens: number;
package/dist/index.d.ts CHANGED
@@ -7,7 +7,7 @@ export { createSkillsFromDirectory, type SkillsDirectoryOptions, type SkillsDire
7
7
  export { REPORT_FINDINGS_TOOL_NAME, type ReportedFinding } from "./core/runner/synthetic-tools.js";
8
8
  export { formatToolError, formatZodValidationError, formatValidationPath, truncateError, errorClassOf } from "./core/tool-errors.js";
9
9
  export type { WorkerErrorClass } from "./core/tool-errors.js";
10
- export { createWebFetchTool, webFetchToolSpec, htmlToText, type WebFetchConfig, createWebSearchTool, type WebSearchConfig, createWebFetchSummarizer, WEBFETCH_SUMMARY_MAX_CONTENT, WEBFETCH_SUMMARY_GUIDELINES, } from "./tools/web.js";
10
+ export { createWebFetchTool, webFetchToolSpec, htmlToText, type WebFetchConfig, createWebSearchTool, type WebSearchConfig, createSearxngSearchBackend, probeSearchBackend, type SearxngBackendOptions, createWebFetchSummarizer, WEBFETCH_SUMMARY_MAX_CONTENT, WEBFETCH_SUMMARY_GUIDELINES, } from "./tools/web.js";
11
11
  export { createTodoWriteTool } from "./tools/todo.js";
12
12
  export { createTaskListTools, createMemoryTaskListStore, assertJsonMetadata, type TaskListItem, type TaskListStore } from "./tools/task-list.js";
13
13
  export { assembleCodeTools, type CodeToolsConfig, CODE_ROLE } from "./scenarios/full-body.js";
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ export { SKILL_TOOL_NAME } from "./core/runner/synthetic-tools.js";
5
5
  export { createSkillsFromDirectory, } from "./core/skills-directory.js";
6
6
  export { REPORT_FINDINGS_TOOL_NAME } from "./core/runner/synthetic-tools.js";
7
7
  export { formatToolError, formatZodValidationError, formatValidationPath, truncateError, errorClassOf } from "./core/tool-errors.js";
8
- export { createWebFetchTool, webFetchToolSpec, htmlToText, createWebSearchTool, createWebFetchSummarizer, WEBFETCH_SUMMARY_MAX_CONTENT, WEBFETCH_SUMMARY_GUIDELINES, } from "./tools/web.js";
8
+ export { createWebFetchTool, webFetchToolSpec, htmlToText, createWebSearchTool, createSearxngSearchBackend, probeSearchBackend, createWebFetchSummarizer, WEBFETCH_SUMMARY_MAX_CONTENT, WEBFETCH_SUMMARY_GUIDELINES, } from "./tools/web.js";
9
9
  export { createTodoWriteTool } from "./tools/todo.js";
10
10
  export { createTaskListTools, createMemoryTaskListStore, assertJsonMetadata } from "./tools/task-list.js";
11
11
  export { assembleCodeTools, CODE_ROLE } from "./scenarios/full-body.js";
@@ -9,6 +9,7 @@ import { combinePolicies, createAllowDenyPolicy } from "../core/tool-policy.js";
9
9
  import { callKeyOrdinal, oversizeJournalResult, journalOversizeTombstone, JOURNAL_OVERSIZE_ERROR_CODE, MAX_JOURNAL_RESULT_BYTES } from "../core/workflow-journal-store.js";
10
10
  import { isWorkflowRunActive, closeWorkflowChannel, markWorkflowActive, publishWorkflowEvent } from "./workflow-observe.js";
11
11
  import { isDurablePause, mapNestedSuspend } from "../agents/suspend-guard.js";
12
+ import { RUNNING_AGENT_OBSERVE_EVERY_BEATS } from "../config/defaults.js";
12
13
  import { boundInputHashOf } from "../core/canonical-json.js";
13
14
  import { boundedRedactedSummary } from "../core/untrusted-egress.js";
14
15
  import { delimitUntrusted } from "../core/untrusted-text.js";
@@ -20,7 +21,6 @@ const MAX_TRANSCRIPT_CHARS = 4000;
20
21
  const WORKFLOW_RESULT_MAX = 4000;
21
22
  const WORKFLOW_RESULT_FULL_MAX = 200_000;
22
23
  const MAX_ACTIVITY = 30;
23
- const RUNNING_AGENT_PERSIST_EVERY_BEATS = 4;
24
24
  function workflowModelLabel(spec) {
25
25
  const model = spec.model;
26
26
  if (model === undefined)
@@ -453,16 +453,17 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
453
453
  };
454
454
  const waIdOf = (callKey) => `wa${createHash("sha256").update(`${runId}:${callKey}`).digest("hex").slice(0, 16)}`;
455
455
  const bceLive = new Map();
456
- const bceSpawn = (callKey, label, agentType, replayed) => {
456
+ const bceSpawn = (callKey, label, agentType, replayed, sessionId) => {
457
457
  if (!bceSink)
458
458
  return;
459
459
  const id = waIdOf(callKey);
460
- bceLive.set(id, { callKey, label, ...(agentType !== undefined ? { agentType } : {}) });
460
+ bceLive.set(id, { callKey, label, ...(agentType !== undefined ? { agentType } : {}), ...(sessionId !== undefined ? { sessionId } : {}) });
461
461
  bceEmit({
462
462
  kind: "spawn",
463
463
  taskId: id,
464
464
  sessionScoped: false,
465
465
  owner: runId,
466
+ ...(sessionId !== undefined ? { sessionId, transcriptId: sessionId } : {}),
466
467
  ...(scope !== undefined ? { scope } : {}),
467
468
  description: replayed ? `${label} (replayed)` : label,
468
469
  agentType: agentType ?? "workflow-agent",
@@ -474,6 +475,11 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
474
475
  startedAt: Date.now(),
475
476
  });
476
477
  };
478
+ const bceBindSession = (callKey, sessionId) => {
479
+ const row = bceLive.get(waIdOf(callKey));
480
+ if (row !== undefined)
481
+ row.sessionId = sessionId;
482
+ };
477
483
  const bceTick = (callKey, e) => {
478
484
  if (!bceSink)
479
485
  return;
@@ -489,6 +495,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
489
495
  workflowRunId: runId,
490
496
  ...(scope !== undefined ? { scope } : {}),
491
497
  ...(row.agentType !== undefined ? { agentType: row.agentType } : { agentType: "workflow-agent" }),
498
+ ...(row.sessionId !== undefined ? { sessionId: row.sessionId, transcriptId: row.sessionId } : {}),
492
499
  name: e.name ?? row.label,
493
500
  progressTaskId: e.taskId,
494
501
  ...(e.parentTaskId !== undefined ? { progressParentTaskId: e.parentTaskId } : {}),
@@ -635,7 +642,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
635
642
  rec.toolCalls = (rec.toolCalls ?? 0) + 1;
636
643
  rec.activity = tail;
637
644
  beatCount += 1;
638
- if (beatCount === 1 || beatCount % RUNNING_AGENT_PERSIST_EVERY_BEATS === 0) {
645
+ if (beatCount === 1 || beatCount % RUNNING_AGENT_OBSERVE_EVERY_BEATS === 0) {
639
646
  void persist("update");
640
647
  }
641
648
  };
@@ -768,7 +775,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
768
775
  agentPhaseOf.set(replayRec, phaseInstance);
769
776
  emit({ type: "agent_start", runId, label, phase, ...(groupId !== undefined ? { groupId } : {}), callKey, prompt, ...(model !== undefined ? { model } : {}), replayed: true, ts: at });
770
777
  emit({ type: "agent_end", runId, label, phase, ...(groupId !== undefined ? { groupId } : {}), status: replayRec.status, output: cachedOutput, ...(rs.toolCalls !== undefined ? { toolCalls: rs.toolCalls } : {}), replayed: true, ts: at });
771
- bceSpawn(callKey, label, agentOpts.agentType, true);
778
+ bceSpawn(callKey, label, agentOpts.agentType, true, r.sessionId || undefined);
772
779
  bceTerminal(callKey, replayRec.status === "completed" ? "completed" : "failed", cachedOutput, r.sessionId || undefined, replayRec.stats);
773
780
  accumulateStats(r, false);
774
781
  void persist("update");
@@ -810,7 +817,8 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
810
817
  if (finalized)
811
818
  throw new Error("workflow run already finalized — ctx.agent cannot spawn after the run ended");
812
819
  rec.startedAt = now();
813
- bceSpawn(callKey, label, agentOpts.agentType, false);
820
+ const bornChildSessionId = resolveChildSessionIdAtSpawn(spec);
821
+ bceSpawn(callKey, label, agentOpts.agentType, false, bornChildSessionId);
814
822
  const typedSpec0 = applyWorkflowAgentType(spec, agentOpts.agentType, agentRegistry);
815
823
  const typedSpec = typedSpec0.model === undefined && inheritedModelSnap !== undefined ? { ...typedSpec0, model: inheritedModelSnap } : typedSpec0;
816
824
  const framedSpec = withWorkflowChildPersona(typedSpec, agentOpts.schema ?? typedSpec.outputSchema);
@@ -852,10 +860,11 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
852
860
  const onCallerAbort = () => attemptCtl.abort(new Error("workflow aborted"));
853
861
  effectiveSignal?.addEventListener("abort", onCallerAbort, { once: true });
854
862
  armWatchdog();
855
- const attemptSessionId = resolveChildSessionIdAtSpawn(runSpec);
863
+ const attemptSessionId = attempts === 1 ? bornChildSessionId : resolveChildSessionIdAtSpawn(runSpec);
856
864
  const attemptSpec = attemptSessionId !== undefined ? { ...runSpec, sessionId: attemptSessionId, signal: attemptCtl.signal } : { ...runSpec, signal: attemptCtl.signal };
857
865
  if (attemptSessionId !== undefined && !finalized) {
858
866
  rec.sessionId = attemptSessionId;
867
+ bceBindSession(callKey, attemptSessionId);
859
868
  void persist("update");
860
869
  }
861
870
  const attemptInternals = {
@@ -1143,9 +1152,9 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1143
1152
  throw new Error(finalized ? "workflow run already finalized — ctx.agentStream cannot spawn after the run ended" : "workflow aborted");
1144
1153
  }
1145
1154
  rec.startedAt = now();
1146
- bceSpawn(callKey, label, agentOpts.agentType, false);
1155
+ const childSessionId = resolveChildSessionIdAtSpawn(spec);
1156
+ bceSpawn(callKey, label, agentOpts.agentType, false, childSessionId);
1147
1157
  let stream;
1148
- let childSessionId;
1149
1158
  try {
1150
1159
  const typedSpec0 = applyWorkflowAgentType(spec, agentOpts.agentType, agentRegistry);
1151
1160
  const typedSpec = typedSpec0.model === undefined && inheritedModelSnap !== undefined ? { ...typedSpec0, model: inheritedModelSnap } : typedSpec0;
@@ -1154,7 +1163,6 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
1154
1163
  const baseRunSpec = agentOpts.schema
1155
1164
  ? { ...framedSpec, ...authInherit, signal: effectiveSignal, outputSchema: agentOpts.schema }
1156
1165
  : { ...framedSpec, ...authInherit, signal: effectiveSignal };
1157
- childSessionId = resolveChildSessionIdAtSpawn(baseRunSpec);
1158
1166
  const runSpec = childSessionId !== undefined ? { ...baseRunSpec, sessionId: childSessionId } : baseRunSpec;
1159
1167
  const enrichedForwardS = opts.onForwardEvent !== undefined
1160
1168
  ? (e) => {
@@ -29,3 +29,18 @@ export interface WebSearchConfig {
29
29
  }
30
30
  export declare function clipCodePoints(s: string, max: number): string;
31
31
  export declare function createWebSearchTool(config: WebSearchConfig): ToolSpec;
32
+ export interface SearxngBackendOptions {
33
+ fetchImpl?: typeof fetch;
34
+ timeoutMs?: number;
35
+ extraParams?: Record<string, string>;
36
+ }
37
+ export declare function createSearxngSearchBackend(baseUrl: string, options?: SearxngBackendOptions): WebSearchConfig["search"];
38
+ export declare function probeSearchBackend(search: WebSearchConfig["search"], options?: {
39
+ timeoutMs?: number;
40
+ }): Promise<{
41
+ ok: true;
42
+ results: number;
43
+ } | {
44
+ ok: false;
45
+ error: string;
46
+ }>;
package/dist/tools/web.js CHANGED
@@ -798,3 +798,45 @@ export function createWebSearchTool(config) {
798
798
  },
799
799
  };
800
800
  }
801
+ export function createSearxngSearchBackend(baseUrl, options = {}) {
802
+ const doFetch = options.fetchImpl ?? globalThis.fetch;
803
+ const timeoutMs = options.timeoutMs ?? 10_000;
804
+ const base = baseUrl.replace(/\/+$/, "");
805
+ return async (query, signal, opts) => {
806
+ const q = opts?.allowedDomains && opts.allowedDomains.length > 0
807
+ ? `${opts.allowedDomains.map((d) => `site:${d}`).join(" OR ")} ${query}`
808
+ : query;
809
+ const url = new URL(`${base}/search`);
810
+ url.searchParams.set("q", q);
811
+ url.searchParams.set("format", "json");
812
+ for (const [k, v] of Object.entries(options.extraParams ?? {}))
813
+ url.searchParams.set(k, v);
814
+ const timeout = AbortSignal.timeout(timeoutMs);
815
+ const res = await doFetch(url, { signal: signal ? AbortSignal.any([signal, timeout]) : timeout, headers: { accept: "application/json" } });
816
+ if (!res.ok)
817
+ throw new Error(`SearXNG ${res.status} ${res.statusText} from ${base}/search`);
818
+ const body = (await res.json());
819
+ if (!Array.isArray(body.results))
820
+ throw new Error(`SearXNG returned no results array from ${base}/search — is format=json enabled on this instance?`);
821
+ return body.results
822
+ .filter((r) => typeof r.url === "string" && r.url !== "")
823
+ .map((r) => ({
824
+ title: typeof r.title === "string" && r.title !== "" ? r.title : r.url,
825
+ url: r.url,
826
+ snippet: typeof r.content === "string" ? r.content : "",
827
+ }));
828
+ };
829
+ }
830
+ export async function probeSearchBackend(search, options) {
831
+ const budget = options?.timeoutMs ?? 15_000;
832
+ try {
833
+ const results = await Promise.race([
834
+ search("connectivity probe", AbortSignal.timeout(budget)),
835
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`probe timed out after ${budget}ms`)), budget)),
836
+ ]);
837
+ return { ok: true, results: results.length };
838
+ }
839
+ catch (e) {
840
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
841
+ }
842
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "2.9.0",
3
+ "version": "2.10.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",