@sema-agent/core 1.279.0 → 1.280.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 (76) hide show
  1. package/dist/agents/subagent.d.ts +12 -0
  2. package/dist/agents/subagent.d.ts.map +1 -1
  3. package/dist/agents/subagent.js +221 -19
  4. package/dist/agents/subagent.js.map +1 -1
  5. package/dist/bin/sema-tb.d.ts +10 -0
  6. package/dist/bin/sema-tb.d.ts.map +1 -1
  7. package/dist/bin/sema-tb.js +81 -10
  8. package/dist/bin/sema-tb.js.map +1 -1
  9. package/dist/core/background-shell.d.ts +2 -0
  10. package/dist/core/background-shell.d.ts.map +1 -1
  11. package/dist/core/background-shell.js.map +1 -1
  12. package/dist/core/context-guard.d.ts +2 -0
  13. package/dist/core/context-guard.d.ts.map +1 -1
  14. package/dist/core/context-guard.js +10 -0
  15. package/dist/core/context-guard.js.map +1 -1
  16. package/dist/core/mcp.d.ts +4 -0
  17. package/dist/core/mcp.d.ts.map +1 -1
  18. package/dist/core/mcp.js +189 -32
  19. package/dist/core/mcp.js.map +1 -1
  20. package/dist/core/runner/call-cap.d.ts +4 -0
  21. package/dist/core/runner/call-cap.d.ts.map +1 -1
  22. package/dist/core/runner/call-cap.js +15 -1
  23. package/dist/core/runner/call-cap.js.map +1 -1
  24. package/dist/core/runner/prepare-task.d.ts.map +1 -1
  25. package/dist/core/runner/prepare-task.js +5 -3
  26. package/dist/core/runner/prepare-task.js.map +1 -1
  27. package/dist/core/runner/runtask.d.ts.map +1 -1
  28. package/dist/core/runner/runtask.js +28 -20
  29. package/dist/core/runner/runtask.js.map +1 -1
  30. package/dist/core/session-reconcile.d.ts +1 -1
  31. package/dist/core/session-reconcile.d.ts.map +1 -1
  32. package/dist/core/session-reconcile.js +7 -2
  33. package/dist/core/session-reconcile.js.map +1 -1
  34. package/dist/core/task-notification.d.ts +2 -0
  35. package/dist/core/task-notification.d.ts.map +1 -1
  36. package/dist/core/task-notification.js.map +1 -1
  37. package/dist/core/task-registry.d.ts +1 -0
  38. package/dist/core/task-registry.d.ts.map +1 -1
  39. package/dist/core/task-registry.js +184 -55
  40. package/dist/core/task-registry.js.map +1 -1
  41. package/dist/core/types.d.ts.map +1 -1
  42. package/dist/engine/execution-env/node-execution-env.d.ts.map +1 -1
  43. package/dist/engine/execution-env/node-execution-env.js +17 -0
  44. package/dist/engine/execution-env/node-execution-env.js.map +1 -1
  45. package/dist/engine/harness/types.d.ts.map +1 -1
  46. package/dist/engine/harness/types.js.map +1 -1
  47. package/dist/engine/loop/agent-loop.js +15 -5
  48. package/dist/engine/loop/agent-loop.js.map +1 -1
  49. package/dist/index.d.ts +1 -1
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +1 -1
  52. package/dist/index.js.map +1 -1
  53. package/dist/orchestration/workflow.d.ts +19 -3
  54. package/dist/orchestration/workflow.d.ts.map +1 -1
  55. package/dist/orchestration/workflow.js +61 -11
  56. package/dist/orchestration/workflow.js.map +1 -1
  57. package/dist/tools/fs/index.d.ts +1 -0
  58. package/dist/tools/fs/index.d.ts.map +1 -1
  59. package/dist/tools/fs/index.js +137 -35
  60. package/dist/tools/fs/index.js.map +1 -1
  61. package/dist/tools/fs/pdf.d.ts +5 -0
  62. package/dist/tools/fs/pdf.d.ts.map +1 -1
  63. package/dist/tools/fs/pdf.js +58 -11
  64. package/dist/tools/fs/pdf.js.map +1 -1
  65. package/dist/tools/fs/safety.d.ts +1 -0
  66. package/dist/tools/fs/safety.d.ts.map +1 -1
  67. package/dist/tools/fs/safety.js +1 -0
  68. package/dist/tools/fs/safety.js.map +1 -1
  69. package/dist/tools/fs/search.d.ts +13 -0
  70. package/dist/tools/fs/search.d.ts.map +1 -1
  71. package/dist/tools/fs/search.js +58 -16
  72. package/dist/tools/fs/search.js.map +1 -1
  73. package/dist/tools/web.d.ts.map +1 -1
  74. package/dist/tools/web.js +141 -28
  75. package/dist/tools/web.js.map +1 -1
  76. package/package.json +1 -1
@@ -4,15 +4,21 @@ import { defineTool } from "../../core/tools.js";
4
4
  import { createTaskOutputTool, createTaskStopTool, defaultTaskRegistry } from "../../core/task-registry.js";
5
5
  import { hasBackgroundShell } from "../../core/background-shell.js";
6
6
  import { delimitUntrusted } from "../../core/untrusted-text.js";
7
- import { sha256, resolveKey, violationText, requireRead, checkStale, checkEditMatch, checkNoChange, hasBinaryExtension, isBinaryContent, fileArgPath, imageMimeForRead, imageMagicMatches, similarNameSuggestion, resolveQuoteMatch, adaptNewStringQuotes, deletionOldString, countOccurrences, } from "./safety.js";
7
+ import { sha256, resolveKey, violationText, requireRead, checkStale, checkEditMatch, checkNoChange, hasBinaryExtension, isBinaryContent, fileArgPath, imageMimeForRead, imageMagicMatches, similarNameSuggestion, resolveQuoteMatch, adaptNewStringQuotes, deletionOldString, countOccurrences, OVERSIZE_READ_ESCAPE_HINT, } from "./safety.js";
8
8
  import { decodeTextBytes, encodeTextForFile, normalizeEditText } from "./encoding.js";
9
- import { runGrep, runGlobDetailed, splitAbsoluteGlobPattern } from "./search.js";
9
+ import { runGrepDetailed, runGlobDetailed, splitAbsoluteGlobPattern } from "./search.js";
10
10
  import { makeRepoMapTool } from "./repo-map.js";
11
11
  import { ghRateLimitHint } from "./gh-rate-limit.js";
12
12
  import { MCP_IMAGE_MAX_BASE64, IMAGE_TARGET_RAW_SIZE, sharpImageDownsampler } from "../../core/mcp.js";
13
13
  import { PDF_FALLBACK_RENDER_PAGES, PDF_INLINE_PAGE_THRESHOLD, PDF_MAX_EXTRACT_SIZE, PDF_MAX_PAGES_PER_READ, PDF_TARGET_RAW_SIZE, extractPdfPagesAsImages, extractPdfTextLayer, getPdfPageCount, parsePdfPageRange, pdfMagicMatches, } from "./pdf.js";
14
14
  export { pdfModelCapabilitiesOf } from "./pdf.js";
15
15
  const MAX_READ_BYTES = 256 * 1024;
16
+ export const SLICED_READ_MAX_BYTES = 64 * 1024 * 1024;
17
+ async function notReadRefusalText(env, toolName, key, v, signal) {
18
+ const base = `Error (${toolName}): ${v.message}`;
19
+ const info = await env.fileInfo(key, signal);
20
+ return info.ok && info.value.kind !== "directory" && info.value.size > MAX_READ_BYTES ? `${base} ${OVERSIZE_READ_ESCAPE_HINT}` : base;
21
+ }
16
22
  const MAX_IMAGE_READ_BYTES = 5 * 1024 * 1024;
17
23
  const MAX_IMAGE_DOWNSAMPLE_INPUT_BYTES = 64 * 1024 * 1024;
18
24
  let autoDownsampler;
@@ -162,17 +168,20 @@ async function readPdfFile(env, path, key, pages, signal, downsamplerOpt, cwd, p
162
168
  const t = await extractPdfTextLayer(env, key, { first: range.first, last }, signal);
163
169
  if (t.ok) {
164
170
  const { body, truncated } = boundPdfExtractedText(t.text);
171
+ const timeoutNote = t.timedOutAfterSec !== undefined
172
+ ? `; PARTIAL — pdftotext timed out after ${t.timedOutAfterSec}s, so the text below covers only what was extracted before the timeout; use a narrower pages range for the rest`
173
+ : "";
165
174
  return {
166
175
  content: [
167
176
  {
168
177
  type: "text",
169
- text: `[PDF: ${path} — pages ${range.first}-${last}${pageCount !== undefined ? ` of ${pageCount}` : ""} extracted as text via pdftotext (the serving model does not support native PDF input)]` +
178
+ text: `[PDF: ${path} — pages ${range.first}-${last}${pageCount !== undefined ? ` of ${pageCount}` : ""} extracted as text via pdftotext (the serving model does not support native PDF input)${timeoutNote}]` +
170
179
  `\n${delimitUntrusted("PDF text layer", body)}${truncated ? `\n[extracted text truncated at ${MAX_READ_BYTES} bytes; narrow the pages range]` : ""}`,
171
180
  },
172
181
  ],
173
182
  details: {
174
183
  type: "document",
175
- file: { type: "application/pdf", originalSize: size, ...(pageCount !== undefined ? { pageCount } : {}), pages: `${range.first}-${last}` },
184
+ file: { type: "application/pdf", originalSize: size, ...(pageCount !== undefined ? { pageCount } : {}), pages: `${range.first}-${last}`, ...(t.timedOutAfterSec !== undefined ? { timedOut: true } : {}) },
176
185
  fallback: { level: 1, reason: "model lacks document input; text layer extracted via pdftotext" },
177
186
  },
178
187
  };
@@ -204,18 +213,29 @@ async function readPdfFile(env, path, key, pages, signal, downsamplerOpt, cwd, p
204
213
  const blocks = await pdfPagesToImageBlocks(extract.pages, path, downsamplerOpt);
205
214
  if (typeof blocks === "string")
206
215
  return blocks;
216
+ const requestedPages = last - range.first + 1;
217
+ const renderedAll = extract.pages.length >= requestedPages;
218
+ const header = extract.timedOutAfterSec !== undefined
219
+ ? renderedAll
220
+ ? `[PDF: ${path} — pages ${range.first}-${last}${pageCount !== undefined ? ` of ${pageCount}` : ""}: pdftoppm timed out after ${extract.timedOutAfterSec}s, but all ${requestedPages} requested page${requestedPages === 1 ? "" : "s"} were rendered before the timeout hit at teardown (100 dpi); nothing is missing.]`
221
+ : `[PDF: ${path} — pages ${range.first}-${last}${pageCount !== undefined ? ` of ${pageCount}` : ""}: pdftoppm timed out after ${extract.timedOutAfterSec}s; rendered ${extract.pages.length} of ${requestedPages} requested page${requestedPages === 1 ? "" : "s"} before the timeout (100 dpi). Read the remaining pages with a narrower range (e.g. pages: "${range.first + extract.pages.length}-${last}").]`
222
+ : `[PDF: ${path} — pages ${range.first}-${last}${pageCount !== undefined ? ` of ${pageCount}` : ""} rendered as ${extract.pages.length} image${extract.pages.length === 1 ? "" : "s"} (100 dpi)]`;
207
223
  const content = [
208
- {
209
- type: "text",
210
- text: `[PDF: ${path} — pages ${range.first}-${last}${pageCount !== undefined ? ` of ${pageCount}` : ""} rendered as ${extract.pages.length} image${extract.pages.length === 1 ? "" : "s"} (100 dpi)]`,
211
- },
224
+ { type: "text", text: header },
212
225
  ...blocks,
213
226
  ];
214
227
  return {
215
228
  content,
216
229
  details: {
217
230
  type: "document",
218
- file: { type: "application/pdf", originalSize: size, ...(pageCount !== undefined ? { pageCount } : {}), pages: `${range.first}-${last}`, renderedPages: extract.pages.length },
231
+ file: {
232
+ type: "application/pdf",
233
+ originalSize: size,
234
+ ...(pageCount !== undefined ? { pageCount } : {}),
235
+ pages: `${range.first}-${last}`,
236
+ renderedPages: extract.pages.length,
237
+ ...(extract.timedOutAfterSec !== undefined ? { requestedPages, timedOut: true } : {}),
238
+ },
219
239
  },
220
240
  };
221
241
  }
@@ -231,15 +251,22 @@ async function readPdfFile(env, path, key, pages, signal, downsamplerOpt, cwd, p
231
251
  const t = await extractPdfTextLayer(env, key, undefined, signal);
232
252
  if (t.ok) {
233
253
  const { body, truncated } = boundPdfExtractedText(t.text);
254
+ const timeoutNote = t.timedOutAfterSec !== undefined
255
+ ? `; PARTIAL — pdftotext timed out after ${t.timedOutAfterSec}s, so the text below covers only what was extracted before the timeout; use the pages parameter for the rest`
256
+ : "";
234
257
  return {
235
258
  content: [
236
259
  {
237
260
  type: "text",
238
- text: `[PDF: ${path} — extracted text layer via pdftotext${pageCount !== undefined ? `; ${pageCount} page${pageCount === 1 ? "" : "s"}` : ""} (the serving model does not support native PDF input)]` +
261
+ text: `[PDF: ${path} — extracted text layer via pdftotext${pageCount !== undefined ? `; ${pageCount} page${pageCount === 1 ? "" : "s"}` : ""} (the serving model does not support native PDF input)${timeoutNote}]` +
239
262
  `\n${delimitUntrusted("PDF text layer", body)}${truncated ? `\n[extracted text truncated at ${MAX_READ_BYTES} bytes; use the pages parameter to read specific ranges]` : ""}`,
240
263
  },
241
264
  ],
242
- details: { type: "document", file: fileCard, fallback: { level: 1, reason: "model lacks document input; text layer extracted via pdftotext" } },
265
+ details: {
266
+ type: "document",
267
+ file: { ...fileCard, ...(t.timedOutAfterSec !== undefined ? { timedOut: true } : {}) },
268
+ fallback: { level: 1, reason: "model lacks document input; text layer extracted via pdftotext" },
269
+ },
243
270
  };
244
271
  }
245
272
  const textWhy = t.toolMissing ? "`pdftotext` is not available in this environment" : `pdftotext failed: ${t.error}`;
@@ -250,18 +277,19 @@ async function readPdfFile(env, path, key, pages, signal, downsamplerOpt, cwd, p
250
277
  if (extract.ok) {
251
278
  const blocks = await pdfPagesToImageBlocks(extract.pages, path, downsamplerOpt);
252
279
  if (typeof blocks !== "string") {
280
+ const renderTimeoutNote = extract.timedOutAfterSec !== undefined ? ` pdftoppm timed out after ${extract.timedOutAfterSec}s — only the pages that finished rendering are included.` : "";
253
281
  return {
254
282
  content: [
255
283
  {
256
284
  type: "text",
257
- text: `[PDF: ${path} — first ${extract.pages.length}${pageCount !== undefined ? ` of ${pageCount}` : ""} page${extract.pages.length === 1 ? "" : "s"} rendered as image${extract.pages.length === 1 ? "" : "s"} (100 dpi; ${textWhy}). ` +
258
- `Use the pages parameter (e.g., pages: "${n + 1}-${n + PDF_FALLBACK_RENDER_PAGES}") to read more pages.]`,
285
+ text: `[PDF: ${path} — first ${extract.pages.length}${pageCount !== undefined ? ` of ${pageCount}` : ""} page${extract.pages.length === 1 ? "" : "s"} rendered as image${extract.pages.length === 1 ? "" : "s"} (100 dpi; ${textWhy}).${renderTimeoutNote} ` +
286
+ `Use the pages parameter (e.g., pages: "${extract.pages.length + 1}-${extract.pages.length + PDF_FALLBACK_RENDER_PAGES}") to read more pages.]`,
259
287
  },
260
288
  ...blocks,
261
289
  ],
262
290
  details: {
263
291
  type: "document",
264
- file: { ...fileCard, pages: `1-${extract.pages.length}`, renderedPages: extract.pages.length },
292
+ file: { ...fileCard, pages: `1-${extract.pages.length}`, renderedPages: extract.pages.length, ...(extract.timedOutAfterSec !== undefined ? { timedOut: true } : {}) },
265
293
  fallback: { level: 2, reason: `model lacks document input; ${textWhy}; rendered first ${extract.pages.length} page(s) via pdftoppm` },
266
294
  },
267
295
  };
@@ -309,7 +337,7 @@ export function makeReadFileTool(env, state, rootCanonical, cwdRef, additionalRo
309
337
  "- This tool can only read files, not directories. To read a directory, use Glob/Grep or an ls command via the Bash tool.\n" +
310
338
  "- You must read a file before editing it.\n" +
311
339
  "- Reads PDFs (.pdf): the document is provided to the model directly (text layer included). Use the `pages` parameter (e.g., \"1-5\") to read a page range; PDFs over 10 pages REQUIRE `pages`. Maximum 20 pages per request.\n" +
312
- "- Other binaries (archives, executables) and text files over 256 KB are refused; Grep a large file instead.\n" +
340
+ "- Other binaries (archives, executables) are refused. A whole-file read of a text file over 256 KB is refused read it in slices with explicit offset/limit, or Grep it instead.\n" +
313
341
  "- If you read a file that exists but has empty contents you will receive a warning in place of file contents.\n" +
314
342
  "- Do NOT re-read a file you just edited to verify — Edit/Write would have errored if the change failed, and the harness tracks file state for you.",
315
343
  parameters: Type.Object({
@@ -399,8 +427,13 @@ export function makeReadFileTool(env, state, rootCanonical, cwdRef, additionalRo
399
427
  if (info.value.kind === "directory") {
400
428
  return `Error (Read): "${path}" is a directory, not a file; use glob/grep or list it with the shell.`;
401
429
  }
402
- if (info.value.size > MAX_READ_BYTES) {
403
- return `Error (Read): "${path}" is too large to read (${info.value.size} bytes > ${MAX_READ_BYTES}-byte cap); use grep to search it instead.`;
430
+ if (info.value.size > SLICED_READ_MAX_BYTES) {
431
+ return (`Error (Read): "${path}" is too large to read with this tool even as an offset/limit slice ` +
432
+ `(${info.value.size} bytes > ${SLICED_READ_MAX_BYTES}-byte cap — the reader loads the whole file into memory before slicing). ` +
433
+ `Stream a portion with bash instead, e.g. \`sed -n '1,200p' <file>\` for a line range or \`head -c 65536 <file>\` for the leading bytes, or use grep to search it.`);
434
+ }
435
+ if (info.value.size > MAX_READ_BYTES && offset === undefined && limit === undefined) {
436
+ return `Error (Read): "${path}" is too large to read in full (${info.value.size} bytes > ${MAX_READ_BYTES}-byte cap); pass an explicit offset/limit to read a slice, or use grep to search it instead.`;
404
437
  }
405
438
  }
406
439
  else {
@@ -411,6 +444,15 @@ export function makeReadFileTool(env, state, rootCanonical, cwdRef, additionalRo
411
444
  const readBin = await env.readBinaryFile(r.key, ctx.signal);
412
445
  if (!readBin.ok)
413
446
  return `Error (Read): cannot read "${path}": ${readBin.error.message}`;
447
+ const readSize = readBin.value.byteLength;
448
+ if (readSize > SLICED_READ_MAX_BYTES) {
449
+ return (`Error (Read): "${path}" is too large to read with this tool even as an offset/limit slice ` +
450
+ `(${readSize} bytes > ${SLICED_READ_MAX_BYTES}-byte cap — the reader loads the whole file into memory before slicing). ` +
451
+ `Stream a portion with bash instead, e.g. \`sed -n '1,200p' <file>\` for a line range or \`head -c 65536 <file>\` for the leading bytes, or use grep to search it.`);
452
+ }
453
+ if (readSize > MAX_READ_BYTES && offset === undefined && limit === undefined) {
454
+ return `Error (Read): "${path}" is too large to read in full (${readSize} bytes > ${MAX_READ_BYTES}-byte cap); pass an explicit offset/limit to read a slice, or use grep to search it instead.`;
455
+ }
414
456
  if (pdfMagicMatches(readBin.value)) {
415
457
  return await readPdfFile(env, path, r.key, pages, ctx.signal, imageDownsampler, cwdRef?.current ?? rootCanonical, readBin.value, pdfCapabilities);
416
458
  }
@@ -617,7 +659,7 @@ export function makeEditFileTool(env, state, rootCanonical, cwdRef, additionalRo
617
659
  }
618
660
  const notRead = requireRead(state, r.key);
619
661
  if (notRead)
620
- return violationText("Edit", notRead);
662
+ return await notReadRefusalText(env, "Edit", r.key, notRead, ctx.signal);
621
663
  const readBin = await env.readBinaryFile(r.key, ctx.signal);
622
664
  if (!readBin.ok)
623
665
  return `Error (Edit): cannot read "${path}": ${readBin.error.message}`;
@@ -795,7 +837,7 @@ export function makeNotebookEditTool(env, state, rootCanonical, cwdRef, addition
795
837
  return violationText("NotebookEdit", r.violation);
796
838
  const notRead = requireRead(state, r.key);
797
839
  if (notRead)
798
- return violationText("NotebookEdit", notRead);
840
+ return await notReadRefusalText(env, "NotebookEdit", r.key, notRead, ctx.signal);
799
841
  const readBin = await env.readBinaryFile(r.key, ctx.signal);
800
842
  if (!readBin.ok)
801
843
  return `Error (NotebookEdit): cannot read "${notebook_path}": ${readBin.error.message}`;
@@ -936,7 +978,7 @@ export function makeGrepTool(env, rootCanonical, additionalRoots) {
936
978
  return violationText("Grep", r.violation);
937
979
  scoped = r.key;
938
980
  }
939
- const text = await runGrep(env, rootCanonical, {
981
+ const grepRun = await runGrepDetailed(env, rootCanonical, {
940
982
  ...a,
941
983
  path: scoped,
942
984
  head_limit: a.head_limit ?? a.max_results,
@@ -946,6 +988,7 @@ export function makeGrepTool(env, rootCanonical, additionalRoots) {
946
988
  ignore_case: a.ignore_case ?? a["-i"],
947
989
  only_matching: a.only_matching ?? a["-o"],
948
990
  }, ctx.signal);
991
+ const text = grepRun.text;
949
992
  const mode = a.output_mode ?? "content";
950
993
  const rows = text.startsWith("No matches.")
951
994
  ? []
@@ -984,7 +1027,7 @@ export function makeGrepTool(env, rootCanonical, additionalRoots) {
984
1027
  }
985
1028
  return {
986
1029
  content: text,
987
- details: { type: "grep", mode, ...detailFields },
1030
+ details: { type: "grep", mode, ...detailFields, ...(grepRun.degraded ?? {}) },
988
1031
  };
989
1032
  },
990
1033
  });
@@ -1206,7 +1249,7 @@ async function runShell(env, cwd, toolName, command, timeoutSec, signal, cwdRef,
1206
1249
  if (res.error.code === "transport_lost") {
1207
1250
  return `Error (${toolName}): connection to the execution environment was lost — the command's outcome is unknown and it may still be running. Verify its effects before retrying; retry only if the command is idempotent. (${res.error.message})`;
1208
1251
  }
1209
- if (res.error.code === "timeout" || res.error.code === "aborted") {
1252
+ if (res.error.code === "timeout" || res.error.code === "aborted" || res.error.code === "callback_error") {
1210
1253
  const clip = clipShellOutput;
1211
1254
  const pStdout = clip(res.error.partialStdout ?? "");
1212
1255
  const pStderr = clip(res.error.partialStderr ?? "");
@@ -1216,7 +1259,9 @@ async function runShell(env, cwd, toolName, command, timeoutSec, signal, cwdRef,
1216
1259
  ? `Command was cut off after ${timeout}s — NOT its requested ${requestedSec}s timeout: the task's wall-clock deadline has already been reached, so the command only got a ${timeout}s grace window. The process was killed; output produced before the cutoff is shown below. Do NOT retry this foreground command — there is no time left. Use run_in_background (exempt from this clamp) if the work must continue, or write out your results now.`
1217
1260
  : `Command was cut off after ${timeout}s — NOT its requested ${requestedSec}s timeout: the task is near its wall-clock deadline, so the foreground time budget was clamped to the ${timeout}s remaining. The process was killed; output produced before the cutoff is shown below. Do NOT retry this foreground command — the next attempt gets even less time. Use run_in_background (exempt from this clamp) if the work must continue, or write out your results now.`
1218
1261
  : `Command timed out after ${timeout}s (timeout limit: ${timeout}s). The process was killed; output produced before the cutoff is shown below. Re-running the same command will likely time out again — consider run_in_background for long commands, a narrower command, or resuming from the partial progress below.`
1219
- : `Command was interrupted (aborted) before completion. Output produced before the interrupt is shown below.`;
1262
+ : res.error.code === "aborted"
1263
+ ? `Command was interrupted (aborted) before completion. Output produced before the interrupt is shown below.`
1264
+ : `Command started but its completion could not be observed — a host-side output-stream callback failed mid-run and the process was terminated (${res.error.message}). Output captured before the cut is shown below; its effects up to that point may have landed. Verify before assuming it needs a full re-run.`;
1220
1265
  const body = (pStdout ? `\n--- partial stdout ---\n${pStdout}` : "") +
1221
1266
  (pStderr ? `\n--- partial stderr ---\n${pStderr}` : "") +
1222
1267
  (!pStdout && !pStderr ? `\n(no output was produced before the cutoff)` : "");
@@ -1233,14 +1278,16 @@ async function runShell(env, cwd, toolName, command, timeoutSec, signal, cwdRef,
1233
1278
  timeoutSec: timeout,
1234
1279
  ...(clampedByDeadline ? { requestedTimeoutSec: requestedSec, clampedByDeadline: true } : {}),
1235
1280
  }
1236
- : { aborted: true }),
1281
+ : res.error.code === "aborted"
1282
+ ? { aborted: true }
1283
+ : { callbackError: true }),
1237
1284
  },
1238
1285
  };
1239
1286
  }
1240
- return `Error (${toolName}): command did not run (${res.error.code}): ${res.error.message}`;
1287
+ return `Error (${toolName}): command execution failed (${res.error.code}): ${res.error.message}. Whether the command started or completed could not be determined — verify its effects before retrying.`;
1241
1288
  }
1242
1289
  if (res.value.detached && detach) {
1243
- return detach.onDetached(res.value.detached.shellId, res.value.stdout, res.value.stderr);
1290
+ return await detach.onDetached(res.value.detached.shellId, res.value.stdout, res.value.stderr);
1244
1291
  }
1245
1292
  let { stderr } = res.value;
1246
1293
  const { stdout, exitCode } = res.value;
@@ -1349,12 +1396,18 @@ export function makeBashTool(env, rootCanonical, coAuthor = DEFAULT_COMMIT_COAUT
1349
1396
  if (diagnosis !== undefined)
1350
1397
  throw new Error(diagnosis);
1351
1398
  }
1399
+ const requestedTimeoutSec = timeout !== undefined ? Math.max(1, Math.round(timeout / 1000)) : undefined;
1352
1400
  const r = await env.spawnBackground(command, {
1353
1401
  cwd: cwdRef.current,
1354
- ...(timeout !== undefined ? { timeout: Math.max(1, Math.round(timeout / 1000)) } : {}),
1402
+ ...(requestedTimeoutSec !== undefined ? { timeout: requestedTimeoutSec } : {}),
1355
1403
  });
1356
1404
  if (!r.ok)
1357
1405
  return `Error (Bash): ${r.error.message}`;
1406
+ const bgCaps = env.backgroundCapabilities;
1407
+ const appliedTimeoutSec = typeof bgCaps.defaultBgTimeoutSec === "number" && typeof bgCaps.maxBgTimeoutSec === "number"
1408
+ ? Math.min(requestedTimeoutSec ?? bgCaps.defaultBgTimeoutSec, bgCaps.maxBgTimeoutSec)
1409
+ : undefined;
1410
+ const budgetNote = appliedTimeoutSec !== undefined ? ` Time budget: auto-terminates if still running after ${appliedTimeoutSec}s (hard cap ${bgCaps.maxBgTimeoutSec}s).` : "";
1358
1411
  if (taskOpts.taskRegistry) {
1359
1412
  const sessionScoped = taskOpts.sessionId !== undefined;
1360
1413
  const owner = sessionScoped ? taskOpts.sessionId : (ctx.taskId ?? taskOpts.taskOwner);
@@ -1374,24 +1427,43 @@ export function makeBashTool(env, rootCanonical, coAuthor = DEFAULT_COMMIT_COAUT
1374
1427
  });
1375
1428
  }
1376
1429
  catch (e) {
1377
- await env.killBackground(r.value.shellId).catch(() => undefined);
1378
- return `Error (Bash): background process was started but could not be registered (${e instanceof Error ? e.message : String(e)}); it has been terminated.`;
1430
+ const reason = e instanceof Error ? e.message : String(e);
1431
+ let kill;
1432
+ try {
1433
+ kill = await env.killBackground(r.value.shellId);
1434
+ }
1435
+ catch {
1436
+ kill = undefined;
1437
+ }
1438
+ if (kill !== undefined && (kill.ok || kill.error.code === "not_found")) {
1439
+ return `Error (Bash): background process was started but could not be registered (${reason}); it has been terminated.`;
1440
+ }
1441
+ const killMsg = kill !== undefined && !kill.ok ? kill.error.message : "killBackground threw";
1442
+ return {
1443
+ content: `Error (Bash): background process was started but could not be registered (${reason}), and the cleanup kill failed (${killMsg}). ` +
1444
+ `The process could NOT be terminated — it may still be running, but it has no task_id, so TaskOutput/TaskStop cannot reach it. ` +
1445
+ `If it must be stopped, kill it manually (e.g. \`pkill -f\` on its command line).`,
1446
+ details: { type: "bash", registerFailed: true, killFailed: true },
1447
+ };
1379
1448
  }
1380
1449
  return onNotify !== undefined
1381
1450
  ? `Command running in background; task_id=${taskId}. You will be notified when it completes — do not poll; ` +
1382
- `end your turn if nothing else is pending. Use TaskOutput("${taskId}") to check interim output; TaskStop("${taskId}") to stop.`
1451
+ `end your turn if nothing else is pending. Use TaskOutput("${taskId}") to check interim output; TaskStop("${taskId}") to stop.` +
1452
+ budgetNote
1383
1453
  : `Started in background; task_id=${taskId}. ` +
1384
1454
  `Use TaskOutput("${taskId}") to read output (poll until status is no longer "running"); ` +
1385
- `TaskStop("${taskId}") to stop. No '&' needed.`;
1455
+ `TaskStop("${taskId}") to stop. No '&' needed.` +
1456
+ budgetNote;
1386
1457
  }
1387
1458
  return (`Started in background; task_id=${r.value.shellId}. Legacy bash_id=${r.value.shellId}. ` +
1388
1459
  `Use TaskOutput("${r.value.shellId}") to read output (poll until status is no longer "running"); ` +
1389
- `TaskStop("${r.value.shellId}") to stop. No '&' needed.`);
1460
+ `TaskStop("${r.value.shellId}") to stop. No '&' needed.` +
1461
+ budgetNote);
1390
1462
  }
1391
1463
  const detachChain = taskOpts.detachHub && taskOpts.taskRegistry && hasBackgroundShell(env) && env.backgroundCapabilities.supportsDetach === true
1392
1464
  ? {
1393
1465
  signal: taskOpts.detachHub.signalFor(ctx.toolCallId),
1394
- onDetached: (shellId, stdoutSoFar, _stderrSoFar) => {
1466
+ onDetached: async (shellId, stdoutSoFar, stderrSoFar) => {
1395
1467
  const sessionScoped = taskOpts.sessionId !== undefined;
1396
1468
  const owner = sessionScoped ? taskOpts.sessionId : (ctx.taskId ?? taskOpts.taskOwner);
1397
1469
  const scope = ctx.principal ?? taskOpts.taskScope;
@@ -1410,18 +1482,48 @@ export function makeBashTool(env, rootCanonical, coAuthor = DEFAULT_COMMIT_COAUT
1410
1482
  });
1411
1483
  }
1412
1484
  catch (e) {
1485
+ const reason = e instanceof Error ? e.message : String(e);
1486
+ let kill;
1487
+ try {
1488
+ kill = await env.killBackground(shellId);
1489
+ }
1490
+ catch {
1491
+ kill = undefined;
1492
+ }
1493
+ const killed = kill !== undefined && (kill.ok || kill.error.code === "not_found");
1494
+ const outTail = stdoutSoFar.slice(-1_000);
1495
+ const errTail = stderrSoFar.slice(-1_000);
1496
+ const tails = (outTail ? `\n--- output so far (tail) ---\n${outTail}` : "") +
1497
+ (errTail ? `\n--- stderr so far (tail) ---\n${errTail}` : "");
1498
+ if (killed) {
1499
+ return {
1500
+ content: `Error (Bash): command was detached to the background but could not be registered (${reason}); ` +
1501
+ `it has been terminated (an unregistered background process would have no task_id and could not be read or stopped).` +
1502
+ tails,
1503
+ details: { type: "bash", detached: true, registerFailed: true },
1504
+ };
1505
+ }
1506
+ const killMsg = kill !== undefined && !kill.ok ? kill.error.message : "killBackground threw";
1413
1507
  return {
1414
- content: `Command moved to background but could not be registered (${e instanceof Error ? e.message : String(e)}); it keeps running under the env's background limits.`,
1415
- details: { type: "bash", detached: true },
1508
+ content: `Error (Bash): command was detached to the background but could not be registered (${reason}), and the cleanup kill failed (${killMsg}). ` +
1509
+ `The process could NOT be terminated — it may still be running, but it has no task_id, so TaskOutput/TaskStop cannot reach it. ` +
1510
+ `If it must be stopped, kill it manually (e.g. \`pkill -f\` on its command line).` +
1511
+ tails,
1512
+ details: { type: "bash", detached: true, registerFailed: true, killFailed: true },
1416
1513
  };
1417
1514
  }
1418
1515
  const tail = stdoutSoFar.slice(-1_000);
1516
+ const adoptCaps = env.backgroundCapabilities;
1517
+ const adoptBudgetNote = typeof adoptCaps.defaultBgTimeoutSec === "number" && typeof adoptCaps.maxBgTimeoutSec === "number"
1518
+ ? ` Time budget: auto-terminates if still running after ${Math.min(adoptCaps.defaultBgTimeoutSec, adoptCaps.maxBgTimeoutSec)}s (hard cap ${adoptCaps.maxBgTimeoutSec}s).`
1519
+ : "";
1419
1520
  return {
1420
1521
  content: `Command moved to background; task_id=${taskId}. ` +
1421
1522
  `Use TaskOutput("${taskId}") to read its output; TaskStop("${taskId}") to stop it. ` +
1422
1523
  (onNotify !== undefined
1423
1524
  ? `You will be notified when it completes — do not poll.`
1424
1525
  : `Poll TaskOutput until its status is no longer "running".`) +
1526
+ adoptBudgetNote +
1425
1527
  (tail ? `\n--- output so far (tail) ---\n${tail}` : ""),
1426
1528
  details: { type: "bash", detached: true, task_id: taskId, ...(description !== undefined ? { description } : {}) },
1427
1529
  };