@sema-agent/core 2.12.0 → 3.0.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 (115) hide show
  1. package/dist/agents/agent-transcript-tool.d.ts +1 -0
  2. package/dist/agents/agent-transcript-tool.js +1 -1
  3. package/dist/agents/cascade.d.ts +4 -0
  4. package/dist/agents/cascade.js +54 -9
  5. package/dist/agents/repair-loop.d.ts +2 -0
  6. package/dist/agents/repair-loop.js +101 -2
  7. package/dist/agents/retain-ledger.d.ts +18 -2
  8. package/dist/agents/retain-ledger.js +37 -8
  9. package/dist/agents/roster-store.d.ts +4 -0
  10. package/dist/agents/roster-store.js +22 -1
  11. package/dist/agents/teacher.js +4 -2
  12. package/dist/agents/team.js +7 -4
  13. package/dist/agents/verify.d.ts +15 -2
  14. package/dist/agents/verify.js +36 -1
  15. package/dist/bin/sema-tb.js +2 -2
  16. package/dist/brain/circuit-breaker.d.ts +1 -0
  17. package/dist/brain/circuit-breaker.js +26 -9
  18. package/dist/brain/degrading.js +13 -1
  19. package/dist/brain/failover.js +2 -0
  20. package/dist/brain/repetition.js +10 -1
  21. package/dist/brain/routing.js +5 -1
  22. package/dist/brain/status-sink.js +5 -1
  23. package/dist/brain/stream-engine.js +4 -1
  24. package/dist/brain/tool-call-repair.js +32 -14
  25. package/dist/core/auto-compaction.js +17 -7
  26. package/dist/core/auto-promote.js +2 -1
  27. package/dist/core/checkpoint-store.d.ts +1 -0
  28. package/dist/core/checkpoint-store.js +28 -10
  29. package/dist/core/consolidate-scope.js +4 -3
  30. package/dist/core/context-edit.js +3 -0
  31. package/dist/core/context-guard.js +3 -0
  32. package/dist/core/git-worktree-env.d.ts +6 -1
  33. package/dist/core/git-worktree-env.js +17 -1
  34. package/dist/core/hooks.d.ts +1 -0
  35. package/dist/core/hooks.js +40 -2
  36. package/dist/core/lsp-diagnostics.js +13 -7
  37. package/dist/core/lsp-protocol.d.ts +1 -1
  38. package/dist/core/lsp-protocol.js +20 -8
  39. package/dist/core/lsp-session.d.ts +12 -2
  40. package/dist/core/lsp-session.js +114 -49
  41. package/dist/core/lsp.d.ts +13 -1
  42. package/dist/core/lsp.js +64 -9
  43. package/dist/core/mcp.d.ts +1 -0
  44. package/dist/core/mcp.js +4 -3
  45. package/dist/core/memory-engine/data-plane.js +4 -0
  46. package/dist/core/memory-engine/engine.d.ts +2 -1
  47. package/dist/core/memory-engine/engine.js +82 -7
  48. package/dist/core/memory-engine/file-backend.d.ts +1 -0
  49. package/dist/core/memory-engine/file-backend.js +13 -3
  50. package/dist/core/memory-engine/layout.js +12 -1
  51. package/dist/core/memory.js +3 -0
  52. package/dist/core/permission-rules.d.ts +2 -1
  53. package/dist/core/permission-rules.js +24 -3
  54. package/dist/core/runner/active-skill-scope.js +34 -6
  55. package/dist/core/runner/assemble-result.d.ts +1 -1
  56. package/dist/core/runner/assemble-result.js +0 -1
  57. package/dist/core/runner/prepare-task.js +23 -3
  58. package/dist/core/runner/runtask.js +48 -30
  59. package/dist/core/runner/tool-disclosure.d.ts +1 -0
  60. package/dist/core/runner/tool-disclosure.js +26 -7
  61. package/dist/core/runner/usage-accounting.d.ts +6 -0
  62. package/dist/core/runner/usage-accounting.js +9 -8
  63. package/dist/core/secret-env.js +5 -1
  64. package/dist/core/sensitive-path-policy.js +9 -3
  65. package/dist/core/session-store.js +15 -4
  66. package/dist/core/skill-tool-specifier.d.ts +8 -0
  67. package/dist/core/skill-tool-specifier.js +58 -0
  68. package/dist/core/skills-directory.d.ts +1 -1
  69. package/dist/core/skills-directory.js +16 -4
  70. package/dist/core/store-contracts/session-repo-contract.js +40 -0
  71. package/dist/core/task-registry-monitor.js +7 -1
  72. package/dist/core/tool-policy.js +65 -17
  73. package/dist/core/trace.d.ts +1 -0
  74. package/dist/core/types.d.ts +3 -5
  75. package/dist/core/with-retry.js +0 -1
  76. package/dist/engine/compaction/compaction.d.ts +1 -0
  77. package/dist/engine/compaction/compaction.js +14 -5
  78. package/dist/engine/lsp/frame-decoder.js +6 -3
  79. package/dist/engine/lsp/node-lsp-manager.d.ts +2 -0
  80. package/dist/engine/lsp/node-lsp-manager.js +103 -26
  81. package/dist/engine/lsp/stdio-lsp-transport.js +16 -7
  82. package/dist/index.d.ts +1 -1
  83. package/dist/internal/harness.d.ts +1 -1
  84. package/dist/internal/harness.js +1 -1
  85. package/dist/internal/llm.d.ts +1 -1
  86. package/dist/orchestration/workflow-governance.js +43 -9
  87. package/dist/orchestration/workflow-sandbox-conformance.js +85 -51
  88. package/dist/orchestration/workflow-script-store.js +34 -4
  89. package/dist/orchestration/workflow.js +10 -6
  90. package/dist/prompt-assembly/artifact-store.d.ts +2 -0
  91. package/dist/prompt-assembly/artifact-store.js +39 -24
  92. package/dist/prompt-assembly/assemble.js +38 -7
  93. package/dist/prompt-assembly/epoch.js +19 -2
  94. package/dist/prompt-assembly/event-registry.js +3 -3
  95. package/dist/prompt-assembly/explain.js +1 -1
  96. package/dist/prompt-assembly/tool-catalog.js +16 -1
  97. package/dist/prompt-assembly/turn-snapshot.js +4 -1
  98. package/dist/stores/file/fs-atomic.d.ts +3 -0
  99. package/dist/stores/file/fs-atomic.js +53 -8
  100. package/dist/stores/file/index.d.ts +1 -0
  101. package/dist/stores/file/index.js +7 -0
  102. package/dist/stores/file/session-store.d.ts +2 -0
  103. package/dist/stores/file/session-store.js +28 -1
  104. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -0
  105. package/dist/tools/fs/bash-readonly-classifier.js +57 -8
  106. package/dist/tools/fs/fs-bash.d.ts +1 -1
  107. package/dist/tools/fs/fs-bash.js +93 -5
  108. package/dist/tools/loop-tick.js +1 -1
  109. package/dist/tools/monitor.js +55 -9
  110. package/dist/tools/scheduler-tools.js +9 -3
  111. package/dist/tools/web.d.ts +8 -2
  112. package/dist/tools/web.js +46 -17
  113. package/dist/tools/worktree.d.ts +2 -0
  114. package/dist/tools/worktree.js +233 -143
  115. package/package.json +5 -2
@@ -29,6 +29,16 @@ const MONITOR_DESCRIPTION = "Run a shell command in the background and watch its
29
29
  '- Wrong: `until grep -q "BUILD SUCCEEDED" build.log; do sleep 5; done` — if the build fails, this never exits and you wait until the watch times out.\n' +
30
30
  '- Right: `until grep -qE "BUILD (SUCCEEDED|FAILED)" build.log; do sleep 5; done` — every terminal state exits.\n' +
31
31
  "Before arming, ask: *if this process crashed right now, would my filter emit anything?* If not, widen it.";
32
+ function envBackgroundWallSec(caps, requestedSec) {
33
+ const positive = (v) => (typeof v === "number" && Number.isFinite(v) && v > 0 ? v : undefined);
34
+ const cap = positive(caps.maxBgTimeoutSec);
35
+ const base = requestedSec ?? positive(caps.defaultBgTimeoutSec);
36
+ if (base === undefined)
37
+ return cap;
38
+ if (cap === undefined)
39
+ return base;
40
+ return Math.min(base, cap);
41
+ }
32
42
  export function createMonitorTool(env, opts) {
33
43
  return defineTool({
34
44
  name: "Monitor",
@@ -56,9 +66,10 @@ export function createMonitorTool(env, opts) {
56
66
  }
57
67
  const isPersistent = persistent === true;
58
68
  const timeoutMs = Math.min(MONITOR_MAX_TIMEOUT_MS, Math.max(1, Math.floor(timeout_ms ?? MONITOR_DEFAULT_TIMEOUT_MS)));
69
+ const spawnTimeoutSec = isPersistent ? undefined : Math.ceil(timeoutMs / 1000) + 5;
59
70
  const spawn = await env.spawnBackground(command, {
60
71
  ...(opts.cwdRef !== undefined ? { cwd: opts.cwdRef.current } : {}),
61
- ...(isPersistent ? {} : { timeout: Math.ceil(timeoutMs / 1000) + 5 }),
72
+ ...(spawnTimeoutSec !== undefined ? { timeout: spawnTimeoutSec } : {}),
62
73
  });
63
74
  if (!spawn.ok)
64
75
  return errorResult(`Error (Monitor): ${spawn.error.message}`);
@@ -87,18 +98,50 @@ export function createMonitorTool(env, opts) {
87
98
  });
88
99
  }
89
100
  catch (e) {
90
- await env.killBackground(spawn.value.shellId).catch(() => undefined);
91
- return errorResult(`Error (Monitor): the watch process was started but could not be registered (${e instanceof Error ? e.message : String(e)}); it has been terminated.`);
101
+ const why = e instanceof Error ? e.message : String(e);
102
+ let killDetail;
103
+ try {
104
+ const kill = await env.killBackground(spawn.value.shellId);
105
+ if (!kill.ok)
106
+ killDetail = `${kill.error.code}: ${kill.error.message}`;
107
+ }
108
+ catch (killErr) {
109
+ killDetail = killErr instanceof Error ? killErr.message : String(killErr);
110
+ }
111
+ return errorResult(killDetail === undefined
112
+ ? `Error (Monitor): the watch process was started but could not be registered (${why}); it has been terminated.`
113
+ : `Error (Monitor): the watch process was started but could not be registered (${why}), and it could NOT be terminated either (${killDetail}). ` +
114
+ `It may still be running as background shell ${spawn.value.shellId}; no task id was minted, so TaskOutput/TaskStop cannot reach it — ` +
115
+ "stop it out of band (the environment's own background time budget and its teardown sweep are the remaining backstops).");
92
116
  }
93
- const bgTimeout = env.backgroundCapabilities.defaultBgTimeoutSec;
117
+ const bgTimeout = envBackgroundWallSec(env.backgroundCapabilities, spawnTimeoutSec);
94
118
  const wasClamped = timeout_ms !== undefined && timeout_ms !== timeoutMs;
95
119
  const clampNote = !isPersistent && wasClamped ? ` (clamped to ${timeoutMs}ms from your requested ${timeout_ms}ms)` : "";
96
120
  const timeoutIgnoredNote = isPersistent && timeout_ms !== undefined ? ` (the timeout_ms you passed is ignored because persistent is true)` : "";
121
+ const envWallMs = bgTimeout !== undefined ? bgTimeout * 1000 : undefined;
122
+ const envWallWins = !isPersistent && envWallMs !== undefined && envWallMs < timeoutMs;
123
+ const persistentBudget = bgTimeout !== undefined
124
+ ? `the execution environment kills background processes after ${bgTimeout}s`
125
+ : `the execution environment's background time budget still applies`;
126
+ const retainedByEnv = env.backgroundCapabilities.retainBackgroundProcesses === true;
127
+ const persistentAnchor = sessionScoped
128
+ ? retainedByEnv
129
+ ? ". This environment declares retained background processes, so at the run teardown the watch is marked retained and " +
130
+ "survives the session's release: only your TaskStop, the command's own exit, or that budget end it."
131
+ : "; it runs until then, until you stop it, or until the session ends."
132
+ : retainedByEnv
133
+ ? ". This mount carries no session identity, so the watch is NOT session-resident despite persistent: true; and because this " +
134
+ "environment declares retained background processes it survives the session's release rather than being settled with the run — " +
135
+ "nothing but your TaskStop, the command's own exit, or that budget will end it, and no later turn can reach it " +
136
+ "(stop it explicitly with TaskStop before you finish)."
137
+ : ". This mount carries no session identity, so the watch is NOT session-resident despite persistent: true — " +
138
+ "it belongs to this task alone and the run teardown settles it, so do not count on it outliving this turn " +
139
+ "(stop it explicitly with TaskStop when you are done).";
97
140
  const lifetime = isPersistent
98
- ? typeof bgTimeout === "number"
99
- ? `It is persistent: no watch timeout of its own${timeoutIgnoredNote}, but the execution environment kills background processes after ${bgTimeout}s; it runs until then, until you stop it, or until the session ends.`
100
- : `It is persistent: no watch timeout of its own${timeoutIgnoredNote}, but the execution environment's background time budget still applies; it runs until then, until you stop it, or until the session ends.`
101
- : `It will be killed after ${timeoutMs}ms${clampNote} if still running.`;
141
+ ? `It is persistent: no watch timeout of its own${timeoutIgnoredNote}, but ${persistentBudget}${persistentAnchor}`
142
+ : envWallWins
143
+ ? `It will be killed after ${envWallMs}ms if still running — the execution environment's background time budget (${bgTimeout}s) is shorter than the ${timeoutMs}ms watch timeout${clampNote}, and the shorter of the two wins.`
144
+ : `It will be killed after ${timeoutMs}ms${clampNote} if still running.`;
102
145
  const content = onNotify !== undefined
103
146
  ? `Monitoring in background; task_id=${taskId}. Each stdout line becomes a notification event (lines within ` +
104
147
  `~200ms are batched); you'll get a final notification with the exit code when it ends — do not poll. ` +
@@ -111,8 +154,11 @@ export function createMonitorTool(env, opts) {
111
154
  type: "monitor-start",
112
155
  taskId,
113
156
  persistent: isPersistent,
157
+ ...(bgTimeout !== undefined ? { envBudgetSec: bgTimeout } : {}),
158
+ ...(envWallWins && envWallMs !== undefined ? { effectiveTimeoutMs: envWallMs } : {}),
114
159
  ...(isPersistent
115
- ? { timeoutIgnored: timeout_ms !== undefined }
160
+ ?
161
+ { timeoutIgnored: timeout_ms !== undefined, sessionScoped, ...(retainedByEnv ? { retainedByEnv: true } : {}) }
116
162
  : { timeoutMs, ...(timeout_ms !== undefined ? { requestedTimeoutMs: timeout_ms } : {}), wasClamped }),
117
163
  },
118
164
  };
@@ -158,6 +158,12 @@ export function createSchedulerTools(env, ctx) {
158
158
  return [];
159
159
  const sched = env;
160
160
  const schedCtx = toSchedulerContext(ctx);
161
+ let cronCreateChain = Promise.resolve();
162
+ const serializedCronCreateOp = (fn) => {
163
+ const next = cronCreateChain.then(fn, fn);
164
+ cronCreateChain = next.then(() => undefined, () => undefined);
165
+ return next;
166
+ };
161
167
  const cronCreateDescription = `Schedule a prompt to be enqueued at a future time. Use for both recurring schedules and one-shot reminders. The task runs UNATTENDED when it fires, so write a fully self-contained \`prompt\` (it will not see this conversation). Use CronList to see what you've scheduled, CronDelete to remove one.
162
168
 
163
169
  ## One-shot tasks (schedule kind "delay" or "at")
@@ -210,7 +216,7 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
210
216
  label: Type.Optional(Type.String({ description: "Short label (also a dedup key with the schedule)." })),
211
217
  }),
212
218
  effect: "write",
213
- execute: async (args) => {
219
+ execute: async (args) => serializedCronCreateOp(async () => {
214
220
  const a = args;
215
221
  if (a.schedule !== undefined && a.cron !== undefined) {
216
222
  return errorResult("Error (CronCreate): pass exactly one of `schedule` (object form) or `cron` (string form), not both.");
@@ -224,7 +230,7 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
224
230
  const when = a.schedule ?? { kind: "cron", expr: a.cron };
225
231
  if (when.kind === "cron") {
226
232
  if (!isValidCronExpr(when.expr)) {
227
- return errorResult(`Error (CronCreate): invalid cron expression "${when.expr}" — only 5/6 space-separated cron fields (digits and * / , -) are allowed.`);
233
+ return errorResult(`Error (CronCreate): invalid cron expression "${when.expr}" — only 5 space-separated cron fields (digits and * / , -) are allowed (a 6th leading seconds field is parsed but always rejected next — this scheduler has no seconds hand).`);
228
234
  }
229
235
  const deepErr = cronScheduleError(when.expr);
230
236
  if (deepErr)
@@ -277,7 +283,7 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
277
283
  ...(replaced !== undefined ? { replaced } : {}),
278
284
  },
279
285
  };
280
- },
286
+ }),
281
287
  });
282
288
  const cronCancel = defineTool({
283
289
  name: "CronDelete",
@@ -6,7 +6,10 @@ export interface WebFetchConfig {
6
6
  fetchImpl?: typeof fetch;
7
7
  maxBytes?: number;
8
8
  timeoutMs?: number;
9
- summarize?: (content: string, prompt: string, signal?: AbortSignal) => Promise<string>;
9
+ summarize?: (content: string, prompt: string, signal?: AbortSignal) => Promise<string | {
10
+ text: string;
11
+ truncated?: boolean;
12
+ }>;
10
13
  userAgent?: string;
11
14
  }
12
15
  export declare function htmlToText(html: string): string;
@@ -14,7 +17,10 @@ export declare function webFetchToolSpec(config?: WebFetchConfig): ToolSpec;
14
17
  export declare function createWebFetchTool(config?: WebFetchConfig): AgentTool;
15
18
  export declare const WEBFETCH_SUMMARY_MAX_CONTENT = 100000;
16
19
  export declare const WEBFETCH_SUMMARY_GUIDELINES: string;
17
- export declare function createWebFetchSummarizer(brain: Brain, model: Model): (content: string, prompt: string, signal?: AbortSignal) => Promise<string>;
20
+ export declare function createWebFetchSummarizer(brain: Brain, model: Model): (content: string, prompt: string, signal?: AbortSignal) => Promise<string | {
21
+ text: string;
22
+ truncated?: boolean;
23
+ }>;
18
24
  export interface WebSearchConfig {
19
25
  search: (query: string, signal?: AbortSignal, opts?: {
20
26
  allowedDomains?: string[];
package/dist/tools/web.js CHANGED
@@ -495,6 +495,7 @@ export function webFetchToolSpec(config = {}) {
495
495
  }
496
496
  const text = /html/i.test(contentType) || /^\s*</.test(raw) ? htmlToText(raw) : raw;
497
497
  let out = text;
498
+ let summaryTruncated = false;
498
499
  let note;
499
500
  if (prompt && bodyCut) {
500
501
  note =
@@ -503,7 +504,17 @@ export function webFetchToolSpec(config = {}) {
503
504
  }
504
505
  else if (prompt && config.summarize) {
505
506
  try {
506
- out = await config.summarize(text, prompt, ctx.signal);
507
+ const summarized = await config.summarize(text, prompt, ctx.signal);
508
+ if (typeof summarized === "string") {
509
+ out = summarized;
510
+ }
511
+ else {
512
+ out = summarized.text;
513
+ if (summarized.truncated) {
514
+ summaryTruncated = true;
515
+ note = `[note: the summary below is INCOMPLETE — the summarizer hit its output limit before finishing]`;
516
+ }
517
+ }
507
518
  }
508
519
  catch (e) {
509
520
  out = text;
@@ -534,7 +545,7 @@ export function webFetchToolSpec(config = {}) {
534
545
  bytes: bodyBytes ? bodyBytes.length : raw.length,
535
546
  result: out.length > RESULT_PREVIEW_CHARS ? `${out.slice(0, RESULT_PREVIEW_CHARS)}\n…[${out.length - RESULT_PREVIEW_CHARS} chars truncated — full text in the tool output]` : out,
536
547
  durationMs: Date.now() - startedAt,
537
- ...(truncationNote ? { truncated: true } : {}),
548
+ ...(truncationNote || summaryTruncated ? { truncated: true } : {}),
538
549
  ...transferStateDetails,
539
550
  },
540
551
  };
@@ -560,18 +571,17 @@ export function createWebFetchSummarizer(brain, model) {
560
571
  const msg = brain.complete
561
572
  ? await brain.complete(model, context, { signal })
562
573
  : await (await Promise.resolve(brain.stream(model, context, { signal }))).result();
563
- const am = msg;
564
- if (am?.stopReason === "error" || am?.stopReason === "aborted") {
565
- throw new Error(am.errorMessage ?? `summarizer stopped with ${am.stopReason}`);
574
+ if (msg.stopReason === "error" || msg.stopReason === "aborted") {
575
+ throw new Error(msg.errorMessage ?? `summarizer stopped with ${msg.stopReason}`);
566
576
  }
567
- const text = (am?.content ?? [])
568
- .filter((b) => b?.type === "text" && typeof b.text === "string")
577
+ const text = msg.content
578
+ .filter((b) => b.type === "text" && typeof b.text === "string")
569
579
  .map((b) => b.text)
570
580
  .join("\n")
571
581
  .trim();
572
582
  if (!text)
573
583
  throw new Error("summarizer returned no text");
574
- return text;
584
+ return msg.stopReason === "length" || msg.partialFinalized === true ? { text, truncated: true } : text;
575
585
  };
576
586
  }
577
587
  const DEFAULT_SEARCH_TIMEOUT_MS = 30_000;
@@ -801,25 +811,27 @@ export function createWebSearchTool(config) {
801
811
  export function createSearxngSearchBackend(baseUrl, options = {}) {
802
812
  const doFetch = options.fetchImpl ?? globalThis.fetch;
803
813
  const timeoutMs = options.timeoutMs ?? 10_000;
804
- const base = baseUrl.replace(/\/+$/, "");
814
+ const parsedBase = new URL(baseUrl);
815
+ parsedBase.hash = "";
816
+ parsedBase.pathname = `${parsedBase.pathname.replace(/\/+$/, "")}/search`;
805
817
  return async (query, signal, opts) => {
806
818
  const q = opts?.allowedDomains && opts.allowedDomains.length > 0
807
819
  ? `${opts.allowedDomains.map((d) => `site:${d}`).join(" OR ")} ${query}`
808
820
  : query;
809
- const url = new URL(`${base}/search`);
810
- url.searchParams.set("q", q);
811
- url.searchParams.set("format", "json");
821
+ const url = new URL(parsedBase);
812
822
  for (const [k, v] of Object.entries(options.extraParams ?? {}))
813
823
  url.searchParams.set(k, v);
824
+ url.searchParams.set("q", q);
825
+ url.searchParams.set("format", "json");
814
826
  const timeout = AbortSignal.timeout(timeoutMs);
815
827
  const res = await doFetch(url, { signal: signal ? AbortSignal.any([signal, timeout]) : timeout, headers: { accept: "application/json" } });
816
828
  if (!res.ok)
817
- throw new Error(`SearXNG ${res.status} ${res.statusText} from ${base}/search`);
829
+ throw new Error(`SearXNG ${res.status} ${res.statusText} from ${url.origin}${url.pathname}`);
818
830
  const body = (await res.json());
819
831
  if (!Array.isArray(body.results))
820
- throw new Error(`SearXNG returned no results array from ${base}/search — is format=json enabled on this instance?`);
832
+ throw new Error(`SearXNG returned no results array from ${url.origin}${url.pathname} — is format=json enabled on this instance?`);
821
833
  return body.results
822
- .filter((r) => typeof r.url === "string" && r.url !== "")
834
+ .filter((r) => typeof r === "object" && r !== null && typeof r.url === "string" && r.url !== "")
823
835
  .map((r) => ({
824
836
  title: typeof r.title === "string" && r.title !== "" ? r.title : r.url,
825
837
  url: r.url,
@@ -829,14 +841,31 @@ export function createSearxngSearchBackend(baseUrl, options = {}) {
829
841
  }
830
842
  export async function probeSearchBackend(search, options) {
831
843
  const budget = options?.timeoutMs ?? 15_000;
844
+ let timer;
832
845
  try {
833
846
  const results = await Promise.race([
834
847
  search("connectivity probe", AbortSignal.timeout(budget)),
835
- new Promise((_, reject) => setTimeout(() => reject(new Error(`probe timed out after ${budget}ms`)), budget)),
848
+ new Promise((_, reject) => {
849
+ timer = setTimeout(() => reject(new Error(`probe timed out after ${budget}ms`)), budget);
850
+ }),
836
851
  ]);
837
852
  return { ok: true, results: results.length };
838
853
  }
839
854
  catch (e) {
840
- return { ok: false, error: e instanceof Error ? e.message : String(e) };
855
+ let text;
856
+ if (e instanceof Error)
857
+ text = e.message;
858
+ else {
859
+ try {
860
+ text = String(e);
861
+ }
862
+ catch {
863
+ text = Object.prototype.toString.call(e);
864
+ }
865
+ }
866
+ return { ok: false, error: text };
867
+ }
868
+ finally {
869
+ clearTimeout(timer);
841
870
  }
842
871
  }
@@ -8,6 +8,8 @@ export interface ActiveWorktreeSession {
8
8
  }
9
9
  export interface WorktreeSessionRef {
10
10
  current?: ActiveWorktreeSession;
11
+ exiting?: boolean;
12
+ entering?: boolean;
11
13
  }
12
14
  export interface WorktreeToolsOptions {
13
15
  repoRoot: string;