@sema-agent/core 2.13.0 → 4.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 (184) hide show
  1. package/CHANGELOG.md +2345 -0
  2. package/dist/agents/cascade.d.ts +6 -1
  3. package/dist/agents/cascade.js +76 -32
  4. package/dist/agents/cumulative-stats.d.ts +29 -0
  5. package/dist/agents/cumulative-stats.js +29 -0
  6. package/dist/agents/observer.d.ts +4 -0
  7. package/dist/agents/observer.js +11 -10
  8. package/dist/agents/repair-loop.d.ts +2 -0
  9. package/dist/agents/repair-loop.js +110 -3
  10. package/dist/agents/retain-ledger.d.ts +20 -2
  11. package/dist/agents/retain-ledger.js +38 -8
  12. package/dist/agents/roster-store.d.ts +4 -0
  13. package/dist/agents/roster-store.js +20 -1
  14. package/dist/agents/subagent.js +66 -64
  15. package/dist/agents/teacher.d.ts +1 -9
  16. package/dist/agents/teacher.js +23 -24
  17. package/dist/agents/team.js +7 -4
  18. package/dist/agents/verify.d.ts +15 -2
  19. package/dist/agents/verify.js +39 -3
  20. package/dist/bin/sema-tb.js +2 -2
  21. package/dist/brain/anthropic.js +24 -21
  22. package/dist/brain/circuit-breaker.d.ts +1 -0
  23. package/dist/brain/circuit-breaker.js +26 -9
  24. package/dist/brain/degrading.js +13 -1
  25. package/dist/brain/failover.js +2 -0
  26. package/dist/brain/openai.js +24 -21
  27. package/dist/brain/repetition.js +10 -1
  28. package/dist/brain/routing.js +5 -1
  29. package/dist/brain/status-sink.js +5 -1
  30. package/dist/brain/stream-engine.js +5 -2
  31. package/dist/brain/stream-shared.d.ts +1 -0
  32. package/dist/brain/stream-shared.js +14 -0
  33. package/dist/brain/terminal-cause.d.ts +4 -0
  34. package/dist/brain/terminal-cause.js +12 -0
  35. package/dist/brain/tool-call-id.d.ts +1 -0
  36. package/dist/brain/tool-call-id.js +3 -0
  37. package/dist/brain/tool-call-repair.js +34 -15
  38. package/dist/core/auto-compaction.js +33 -32
  39. package/dist/core/background-agent-store.d.ts +22 -22
  40. package/dist/core/background-agent-store.js +19 -23
  41. package/dist/core/checkpoint-store.d.ts +7 -13
  42. package/dist/core/checkpoint-store.js +39 -15
  43. package/dist/core/consolidate-scope.js +4 -3
  44. package/dist/core/context-edit.js +3 -0
  45. package/dist/core/context-guard.js +3 -0
  46. package/dist/core/file-snapshot-store.d.ts +8 -0
  47. package/dist/core/file-snapshot-store.js +4 -3
  48. package/dist/core/git-worktree-env.d.ts +6 -1
  49. package/dist/core/git-worktree-env.js +12 -1
  50. package/dist/core/hooks.d.ts +4 -1
  51. package/dist/core/hooks.js +35 -7
  52. package/dist/core/lsp-diagnostics.js +13 -7
  53. package/dist/core/lsp-protocol.d.ts +1 -1
  54. package/dist/core/lsp-protocol.js +20 -8
  55. package/dist/core/lsp-session.d.ts +12 -2
  56. package/dist/core/lsp-session.js +114 -49
  57. package/dist/core/lsp.d.ts +13 -1
  58. package/dist/core/lsp.js +65 -9
  59. package/dist/core/mailbox-store.d.ts +7 -10
  60. package/dist/core/mcp.d.ts +7 -10
  61. package/dist/core/mcp.js +7 -3
  62. package/dist/core/memory-engine/data-plane.js +4 -0
  63. package/dist/core/memory-engine/engine.d.ts +2 -2
  64. package/dist/core/memory-engine/engine.js +96 -31
  65. package/dist/core/memory-engine/file-backend.d.ts +2 -2
  66. package/dist/core/memory-engine/file-backend.js +38 -39
  67. package/dist/core/memory-engine/layout.d.ts +8 -0
  68. package/dist/core/memory-engine/layout.js +71 -4
  69. package/dist/core/memory-engine/scope-contract.js +9 -3
  70. package/dist/core/memory-engine/types.d.ts +1 -1
  71. package/dist/core/memory.js +3 -0
  72. package/dist/core/permission-rules.d.ts +2 -1
  73. package/dist/core/permission-rules.js +24 -3
  74. package/dist/core/runner/active-skill-scope.d.ts +0 -1
  75. package/dist/core/runner/active-skill-scope.js +1 -15
  76. package/dist/core/runner/assemble-result.d.ts +3 -8
  77. package/dist/core/runner/assemble-result.js +3 -5
  78. package/dist/core/runner/compaction-call-options.d.ts +7 -0
  79. package/dist/core/runner/compaction-call-options.js +30 -0
  80. package/dist/core/runner/prepare-task.d.ts +6 -23
  81. package/dist/core/runner/prepare-task.js +31 -53
  82. package/dist/core/runner/runtask.d.ts +1 -0
  83. package/dist/core/runner/runtask.js +86 -178
  84. package/dist/core/runner/session-rule-policy.js +4 -1
  85. package/dist/core/runner/teardown-bounded.d.ts +7 -0
  86. package/dist/core/runner/teardown-bounded.js +36 -0
  87. package/dist/core/runner/tool-disclosure.d.ts +2 -5
  88. package/dist/core/runner/turn-attachments.d.ts +5 -15
  89. package/dist/core/runner/turn-attachments.js +1 -1
  90. package/dist/core/runner/usage-accounting.d.ts +6 -0
  91. package/dist/core/runner/usage-accounting.js +9 -8
  92. package/dist/core/safe-notify.d.ts +17 -0
  93. package/dist/core/safe-notify.js +59 -0
  94. package/dist/core/secret-env.js +5 -1
  95. package/dist/core/sensitive-path-policy.js +11 -4
  96. package/dist/core/store-contracts/checkpoint-store-contract.js +1 -1
  97. package/dist/core/store-contracts/session-repo-contract.js +40 -0
  98. package/dist/core/stub-env.d.ts +2 -6
  99. package/dist/core/surrogate-safe-slice.d.ts +4 -0
  100. package/dist/core/surrogate-safe-slice.js +18 -0
  101. package/dist/core/task-registry-monitor.js +15 -14
  102. package/dist/core/task-registry-shared.d.ts +22 -0
  103. package/dist/core/task-registry-shared.js +16 -0
  104. package/dist/core/task-registry.d.ts +1 -0
  105. package/dist/core/task-registry.js +40 -57
  106. package/dist/core/tool-errors.js +5 -4
  107. package/dist/core/tool-name-aliases.js +2 -1
  108. package/dist/core/tool-policy.js +72 -19
  109. package/dist/core/tool-result-store.d.ts +1 -0
  110. package/dist/core/tool-result-store.js +17 -1
  111. package/dist/core/trace.d.ts +3 -9
  112. package/dist/core/types.d.ts +16 -12
  113. package/dist/core/untrusted-egress.js +7 -5
  114. package/dist/core/workflow-journal-store.d.ts +9 -20
  115. package/dist/core/workflow-run-store.d.ts +11 -10
  116. package/dist/core/workflow-run-store.js +22 -16
  117. package/dist/engine/compaction/compaction.d.ts +1 -0
  118. package/dist/engine/compaction/compaction.js +14 -5
  119. package/dist/engine/compaction/utils.js +4 -2
  120. package/dist/engine/execution-env/node-execution-env.d.ts +2 -24
  121. package/dist/engine/harness/agent-harness.d.ts +1 -0
  122. package/dist/engine/harness/agent-harness.js +13 -33
  123. package/dist/engine/harness/types.d.ts +29 -67
  124. package/dist/engine/llm/types.d.ts +1 -1
  125. package/dist/engine/loop/agent-loop.js +11 -4
  126. package/dist/engine/loop/types.d.ts +11 -9
  127. package/dist/engine/lsp/frame-decoder.js +6 -3
  128. package/dist/engine/lsp/node-lsp-manager.d.ts +2 -0
  129. package/dist/engine/lsp/node-lsp-manager.js +103 -26
  130. package/dist/engine/lsp/stdio-lsp-transport.js +16 -7
  131. package/dist/engine/session/memory-repo.d.ts +2 -6
  132. package/dist/engine/session/session.d.ts +5 -30
  133. package/dist/index.d.ts +3 -1
  134. package/dist/index.js +2 -1
  135. package/dist/internal/harness-types.d.ts +3 -1
  136. package/dist/internal/harness.d.ts +1 -1
  137. package/dist/internal/harness.js +1 -1
  138. package/dist/orchestration/run-spec.js +7 -4
  139. package/dist/orchestration/run-workflow-tool.js +3 -35
  140. package/dist/orchestration/workflow-governance.js +43 -9
  141. package/dist/orchestration/workflow-sandbox-conformance.js +85 -51
  142. package/dist/orchestration/workflow-script-store.js +34 -4
  143. package/dist/orchestration/workflow.d.ts +3 -11
  144. package/dist/orchestration/workflow.js +164 -204
  145. package/dist/prompt-assembly/artifact-store.d.ts +2 -0
  146. package/dist/prompt-assembly/artifact-store.js +39 -24
  147. package/dist/prompt-assembly/assemble.js +38 -7
  148. package/dist/prompt-assembly/epoch.js +19 -2
  149. package/dist/prompt-assembly/event-registry.js +3 -3
  150. package/dist/prompt-assembly/explain.js +1 -1
  151. package/dist/prompt-assembly/tool-catalog.d.ts +10 -9
  152. package/dist/prompt-assembly/tool-catalog.js +16 -1
  153. package/dist/prompt-assembly/turn-snapshot.d.ts +2 -5
  154. package/dist/prompt-assembly/turn-snapshot.js +4 -1
  155. package/dist/stores/file/background-agent-store.d.ts +3 -12
  156. package/dist/stores/file/background-agent-store.js +3 -41
  157. package/dist/stores/file/checkpoint-store.js +6 -6
  158. package/dist/stores/file/fs-atomic.d.ts +3 -0
  159. package/dist/stores/file/fs-atomic.js +53 -8
  160. package/dist/stores/file/index.d.ts +1 -0
  161. package/dist/stores/file/index.js +7 -0
  162. package/dist/stores/file/mailbox-store.d.ts +2 -6
  163. package/dist/stores/file/session-policy-store.js +15 -1
  164. package/dist/stores/file/session-store.d.ts +4 -6
  165. package/dist/stores/file/session-store.js +28 -1
  166. package/dist/stores/file/workflow-journal-store.d.ts +3 -11
  167. package/dist/stores/file/workflow-run-store.d.ts +3 -7
  168. package/dist/stores/file/workflow-run-store.js +4 -17
  169. package/dist/tools/fs/bash-readonly-classifier.d.ts +2 -0
  170. package/dist/tools/fs/bash-readonly-classifier.js +73 -21
  171. package/dist/tools/fs/fs-bash.d.ts +1 -1
  172. package/dist/tools/fs/fs-bash.js +407 -91
  173. package/dist/tools/fs/fs-pdf.d.ts +2 -12
  174. package/dist/tools/fs/fs-shared.js +2 -2
  175. package/dist/tools/fs/notebook.d.ts +2 -5
  176. package/dist/tools/fs/safety.d.ts +3 -0
  177. package/dist/tools/fs/safety.js +43 -14
  178. package/dist/tools/fs/search.js +5 -5
  179. package/dist/tools/loop-tick.js +1 -1
  180. package/dist/tools/monitor.d.ts +1 -0
  181. package/dist/tools/monitor.js +57 -9
  182. package/dist/tools/worktree.d.ts +4 -7
  183. package/dist/tools/worktree.js +235 -143
  184. package/package.json +9 -2
@@ -1,3 +1,4 @@
1
+ import type { ImageContent } from "../../internal/llm.js";
1
2
  export declare function isNotebookPath(p: string): boolean;
2
3
  export declare const NOTEBOOK_CELL_OUTPUT_POINTER_CHARS = 10000;
3
4
  export declare const NOTEBOOK_OUTPUT_TEXT_CLIP = 10000;
@@ -31,11 +32,7 @@ export interface NotebookRender {
31
32
  blocks: Array<{
32
33
  type: "text";
33
34
  text: string;
34
- } | {
35
- type: "image";
36
- data: string;
37
- mimeType: string;
38
- }>;
35
+ } | ImageContent>;
39
36
  textChars: number;
40
37
  imageChars: number;
41
38
  }
@@ -22,8 +22,11 @@ export interface FsViolation {
22
22
  roots?: readonly string[];
23
23
  message: string;
24
24
  }
25
+ export declare function isAbsolutePathForm(p: string): boolean;
26
+ export declare function isWinFormPath(p: string): boolean;
25
27
  export declare function isBlockedDevicePath(key: string): boolean;
26
28
  export declare function normalizeAbsPathLexically(p: string): string;
29
+ export declare function foldRedundantPathSegments(p: string): string;
27
30
  export declare function hasBinaryExtension(path: string): boolean;
28
31
  export declare function imageMimeForRead(path: string): string | undefined;
29
32
  export declare function imageMagicMatches(bytes: Uint8Array, mimeType: string): boolean;
@@ -18,20 +18,20 @@ export function sha256(content) {
18
18
  return createHash("sha256").update(content, "utf8").digest("hex");
19
19
  }
20
20
  const SEP = "/";
21
- function isAbsolutePathForm(p) {
22
- return p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p);
21
+ export function isAbsolutePathForm(p) {
22
+ return p.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p) || p.startsWith("\\\\");
23
23
  }
24
24
  function normalizeForCompare(p) {
25
- let n = p.replace(/\\/g, "/");
25
+ let n = isWinFormPath(p) ? p.replace(/\\/g, "/") : p;
26
26
  if (/^[A-Za-z]:(\/|$)/.test(n))
27
27
  n = n[0].toLowerCase() + n.slice(1);
28
28
  return n;
29
29
  }
30
30
  function nativeSepOf(p) {
31
- return p.includes("\\") ? "\\" : "/";
31
+ return isWinFormPath(p) && p.includes("\\") ? "\\" : "/";
32
32
  }
33
33
  function lastSepIndex(p) {
34
- return Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\"));
34
+ return isWinFormPath(p) ? Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\")) : p.lastIndexOf("/");
35
35
  }
36
36
  const BLOCKED_DEVICE_PATHS = new Set([
37
37
  "/dev/zero", "/dev/random", "/dev/urandom", "/dev/full",
@@ -46,7 +46,7 @@ function isProcSensitiveFile(key) {
46
46
  return key.startsWith("/proc/") && PROC_SENSITIVE_SUFFIXES.some((suf) => key.endsWith(suf));
47
47
  }
48
48
  const WIN_RESERVED_RE = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\.[^\\/]*)?$/i;
49
- function isWinFormPath(p) {
49
+ export function isWinFormPath(p) {
50
50
  return /^[A-Za-z]:[\\/]/.test(p) || p.startsWith("\\\\") || (!p.startsWith("/") && p.includes("\\"));
51
51
  }
52
52
  function isWinReservedDeviceKey(key) {
@@ -60,19 +60,48 @@ export function isBlockedDevicePath(key) {
60
60
  return BLOCKED_DEVICE_PATHS.has(key) || isProcStdioFd(key) || isProcSensitiveFile(key) || isWinReservedDeviceKey(key);
61
61
  }
62
62
  export function normalizeAbsPathLexically(p) {
63
- if (!p.startsWith("/"))
64
- return p;
65
- const out = [];
66
- for (const seg of p.split("/")) {
63
+ return foldSegmentsLexically(p, true);
64
+ }
65
+ export function foldRedundantPathSegments(p) {
66
+ return foldSegmentsLexically(p, false);
67
+ }
68
+ function foldSegmentsLexically(p, foldParents) {
69
+ const isUnc = p.startsWith("\\\\");
70
+ const drive = /^[A-Za-z]:/.exec(p)?.[0] ?? "";
71
+ const winForm = isUnc || drive !== "" || (!p.startsWith("/") && p.includes("\\"));
72
+ const sep = winForm && p.includes("\\") ? "\\" : "/";
73
+ const splitter = winForm ? /[\\/]+/ : /\/+/;
74
+ let prefix = drive;
75
+ let rest = p.slice(drive.length + (isUnc ? 2 : 0));
76
+ let abs = rest.startsWith("/") || (winForm && rest.startsWith("\\"));
77
+ if (isUnc) {
78
+ const segs = rest.split(splitter).filter((seg) => seg !== "");
79
+ const host = segs.shift() ?? "";
80
+ const share = segs.shift();
81
+ prefix = sep + sep + host + (share === undefined ? "" : sep + share);
82
+ rest = segs.join(sep);
83
+ abs = true;
84
+ }
85
+ const outSegs = [];
86
+ for (const seg of rest.split(splitter)) {
67
87
  if (seg === "" || seg === ".")
68
88
  continue;
69
89
  if (seg === "..") {
70
- out.pop();
90
+ if (!foldParents) {
91
+ outSegs.push(seg);
92
+ continue;
93
+ }
94
+ if (outSegs.length > 0 && outSegs[outSegs.length - 1] !== "..")
95
+ outSegs.pop();
96
+ else if (!abs)
97
+ outSegs.push("..");
71
98
  continue;
72
99
  }
73
- out.push(seg);
100
+ outSegs.push(seg);
74
101
  }
75
- return `/${out.join("/")}`;
102
+ if (outSegs.length === 0)
103
+ return abs ? prefix + sep : prefix === "" ? "." : prefix;
104
+ return prefix + (abs ? sep : "") + outSegs.join(sep);
76
105
  }
77
106
  const BINARY_EXTENSIONS = new Set([
78
107
  ".exe", ".dll", ".so", ".dylib", ".o", ".a", ".obj", ".lib", ".bin", ".class", ".pyc", ".pyo", ".wasm",
@@ -229,7 +258,7 @@ function parentDir(abs) {
229
258
  }
230
259
  async function canonicalizeNewPath(env, abs, signal) {
231
260
  const sep = nativeSepOf(abs);
232
- const parts = abs.split(/[\\/]/);
261
+ const parts = isWinFormPath(abs) ? abs.split(/[\\/]/) : abs.split("/");
233
262
  const tail = [];
234
263
  while (parts.length > 1) {
235
264
  const candidate = parts.join(sep) || sep;
@@ -1,5 +1,5 @@
1
1
  import { delimitUntrusted } from "../../core/untrusted-text.js";
2
- import { hasBinaryExtension } from "./safety.js";
2
+ import { hasBinaryExtension, isAbsolutePathForm } from "./safety.js";
3
3
  const WALK_MAX_FILES = 5000;
4
4
  const WALK_MAX_DEPTH = 32;
5
5
  const GREP_DEFAULT_CAP = 250;
@@ -51,7 +51,7 @@ export function splitGlobParam(glob) {
51
51
  return out;
52
52
  }
53
53
  export function splitAbsoluteGlobPattern(pattern) {
54
- if (!(pattern.startsWith("/") || /^[A-Za-z]:[\\/]/.test(pattern)))
54
+ if (!isAbsolutePathForm(pattern))
55
55
  return undefined;
56
56
  const m = /[*?[{]/.exec(pattern);
57
57
  if (!m) {
@@ -758,7 +758,7 @@ export async function jsGrep(env, root, p, signal, guards) {
758
758
  }
759
759
  const ignore = await buildIgnore(env, root, signal);
760
760
  const rootPrefix = root.replace(/[\\/]+$/, "") + (root.includes("\\") ? "\\" : "/");
761
- const start = p.path ? (p.path.startsWith("/") || /^[A-Za-z]:[\\/]/.test(p.path) ? p.path : `${rootPrefix}${p.path}`) : root;
761
+ const start = p.path ? (isAbsolutePathForm(p.path) ? p.path : `${rootPrefix}${p.path}`) : root;
762
762
  let walked;
763
763
  if (p.path) {
764
764
  const info = await env.fileInfo(start, signal);
@@ -992,7 +992,7 @@ async function sortRgFilesByMtime(env, root, stdout, signal) {
992
992
  const rootPrefix = root.replace(/[\\/]+$/, "") + (root.includes("\\") ? "\\" : "/");
993
993
  const toAbs = (p) => {
994
994
  const stripped = p.startsWith("./") ? p.slice(2) : p;
995
- return stripped.startsWith("/") || /^[A-Za-z]:[\\/]/.test(stripped) ? stripped : `${rootPrefix}${stripped}`;
995
+ return isAbsolutePathForm(stripped) ? stripped : `${rootPrefix}${stripped}`;
996
996
  };
997
997
  const infos = await Promise.all(paths.map((p) => env.fileInfo(toAbs(p), signal)));
998
998
  const withMtime = paths.map((p, i) => {
@@ -1104,7 +1104,7 @@ export async function runGlobDetailed(env, root, pattern, opts = {}, signal) {
1104
1104
  const t0 = Date.now();
1105
1105
  const baseIgnore = await buildIgnore(env, root, signal);
1106
1106
  const rootPrefix = root.replace(/[\\/]+$/, "") + (root.includes("\\") ? "\\" : "/");
1107
- const start = opts.path ? (opts.path.startsWith("/") || /^[A-Za-z]:[\\/]/.test(opts.path) ? opts.path : `${rootPrefix}${opts.path}`) : root;
1107
+ const start = opts.path ? (isAbsolutePathForm(opts.path) ? opts.path : `${rootPrefix}${opts.path}`) : root;
1108
1108
  if (opts.path !== undefined) {
1109
1109
  const probe = await env.fileInfo(start, signal);
1110
1110
  if (!probe.ok && probe.error.code === "not_found") {
@@ -49,7 +49,7 @@ Read and analyze freely — understanding the state of things has no blast radiu
49
49
  `;
50
50
  const DYNAMIC_APPENDIX = `
51
51
 
52
- If a Monitor is armed (check TaskList), keep \`delaySeconds\` at 1200–1800s — the Monitor is the wake signal and this is only the fallback heartbeat. If you were woken by a \`<task-notification>\`, handle the event before deciding whether to re-arm. To stop the loop, call ${SCHEDULE_WAKEUP_TOOL_NAME} with \`stop: true\` and TaskStop the monitor (use TaskList to find its task ID if no longer in context).`;
52
+ If a Monitor is armed (its task_id was reported in the receipt when you started it), keep \`delaySeconds\` at 1200–1800s — the Monitor is the wake signal and this is only the fallback heartbeat. If you were woken by a \`<task-notification>\`, handle the event before deciding whether to re-arm. To stop the loop, call ${SCHEDULE_WAKEUP_TOOL_NAME} with \`stop: true\` and TaskStop the monitor (its task id is in that start receipt, and in the \`<task-id>\` of every notification the watch sends).`;
53
53
  function pushNotificationAppendix(persistentVariant) {
54
54
  const r = persistentVariant
55
55
  ? "newly blocked on a decision you won't make alone, you're ending the loop"
@@ -17,5 +17,6 @@ export interface MonitorToolOptions {
17
17
  batchWindowMs?: number;
18
18
  maxBatchesPerMinute?: number;
19
19
  toolResultStore?: ToolResultStore;
20
+ retainBackgroundProcesses?: boolean;
20
21
  }
21
22
  export declare function createMonitorTool(env: ExecutionEnv, opts: MonitorToolOptions): AgentTool;
@@ -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,52 @@ 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 retainedBySpec = opts.retainBackgroundProcesses === true;
128
+ const retained = retainedByEnv || retainedBySpec;
129
+ const persistentAnchor = sessionScoped
130
+ ? retained
131
+ ? ". This run retains background processes (environment capability or task spec), so at the run teardown the watch is marked retained and " +
132
+ "survives the session's release: only your TaskStop, the command's own exit, or that budget end it."
133
+ : "; it runs until then, until you stop it, or until the session ends."
134
+ : retained
135
+ ? ". This mount carries no session identity, so the watch is NOT session-resident despite persistent: true; and because this " +
136
+ "run retains background processes (environment capability or task spec) it survives the session's release rather than being settled with the run — " +
137
+ "nothing but your TaskStop, the command's own exit, or that budget will end it, and no later turn can reach it " +
138
+ "(stop it explicitly with TaskStop before you finish)."
139
+ : ". This mount carries no session identity, so the watch is NOT session-resident despite persistent: true — " +
140
+ "it belongs to this task alone and the run teardown settles it, so do not count on it outliving this turn " +
141
+ "(stop it explicitly with TaskStop when you are done).";
97
142
  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.`;
143
+ ? `It is persistent: no watch timeout of its own${timeoutIgnoredNote}, but ${persistentBudget}${persistentAnchor}`
144
+ : envWallWins
145
+ ? `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.`
146
+ : `It will be killed after ${timeoutMs}ms${clampNote} if still running.`;
102
147
  const content = onNotify !== undefined
103
148
  ? `Monitoring in background; task_id=${taskId}. Each stdout line becomes a notification event (lines within ` +
104
149
  `~200ms are batched); you'll get a final notification with the exit code when it ends — do not poll. ` +
@@ -111,8 +156,11 @@ export function createMonitorTool(env, opts) {
111
156
  type: "monitor-start",
112
157
  taskId,
113
158
  persistent: isPersistent,
159
+ ...(bgTimeout !== undefined ? { envBudgetSec: bgTimeout } : {}),
160
+ ...(envWallWins && envWallMs !== undefined ? { effectiveTimeoutMs: envWallMs } : {}),
114
161
  ...(isPersistent
115
- ? { timeoutIgnored: timeout_ms !== undefined }
162
+ ?
163
+ { timeoutIgnored: timeout_ms !== undefined, sessionScoped, ...(retainedByEnv ? { retainedByEnv: true } : {}), ...(retainedBySpec ? { retainedBySpec: true } : {}) }
116
164
  : { timeoutMs, ...(timeout_ms !== undefined ? { requestedTimeoutMs: timeout_ms } : {}), wasClamped }),
117
165
  },
118
166
  };
@@ -1,13 +1,10 @@
1
- import type { AgentTool, ExecutionEnv } from "../internal/harness-types.js";
1
+ import type { ActiveWorktreeSession, AgentTool, ExecutionEnv } from "../internal/harness-types.js";
2
2
  import type { CwdRef } from "./fs/index.js";
3
- export interface ActiveWorktreeSession {
4
- worktreeDir: string;
5
- originalCwd: string;
6
- baseSha: string;
7
- entered?: boolean;
8
- }
3
+ export type { ActiveWorktreeSession };
9
4
  export interface WorktreeSessionRef {
10
5
  current?: ActiveWorktreeSession;
6
+ exiting?: boolean;
7
+ entering?: boolean;
11
8
  }
12
9
  export interface WorktreeToolsOptions {
13
10
  repoRoot: string;