@sema-agent/core 1.427.0 → 1.429.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.
@@ -18,6 +18,8 @@ export declare class SubagentStepRecorder {
18
18
  private readonly pending;
19
19
  private readonly edits;
20
20
  private action;
21
+ private actionTool;
22
+ private actionTarget;
21
23
  constructor(parentToolCallId?: string);
22
24
  lockTo(taskId: string): void;
23
25
  private resetRecorded;
@@ -26,5 +28,9 @@ export declare class SubagentStepRecorder {
26
28
  recentSteps(): SubagentStep[] | undefined;
27
29
  editedFiles(): SubagentEditedFile[] | undefined;
28
30
  currentAction(): string | undefined;
31
+ currentActionStructured(): {
32
+ toolName: string;
33
+ target?: string;
34
+ } | undefined;
29
35
  }
30
36
  export declare function stepsFromMessages(messages: readonly unknown[], lastN: number): SubagentStep[];
@@ -58,6 +58,8 @@ export class SubagentStepRecorder {
58
58
  pending = new Map();
59
59
  edits = new Map();
60
60
  action;
61
+ actionTool;
62
+ actionTarget;
61
63
  constructor(parentToolCallId) {
62
64
  this.parentToolCallId = parentToolCallId;
63
65
  }
@@ -72,6 +74,8 @@ export class SubagentStepRecorder {
72
74
  this.pending.clear();
73
75
  this.edits.clear();
74
76
  this.action = undefined;
77
+ this.actionTool = undefined;
78
+ this.actionTarget = undefined;
75
79
  }
76
80
  inScope(e) {
77
81
  const { sourceTaskId: src, parentToolCallId: ptc } = e;
@@ -94,6 +98,8 @@ export class SubagentStepRecorder {
94
98
  const target = extractTarget(e.args);
95
99
  this.pending.set(e.toolCallId, { tool, target, ...(editTargetPath(e.toolName, e.args) ? { edit: editTargetPath(e.toolName, e.args) } : {}) });
96
100
  this.action = target ? `${tool} ${target}`.slice(0, FIELD_MAX + tool.length + 1) : tool;
101
+ this.actionTool = tool;
102
+ this.actionTarget = target || undefined;
97
103
  }
98
104
  else if (e.type === "tool_end") {
99
105
  if (!this.inScope(e))
@@ -128,6 +134,9 @@ export class SubagentStepRecorder {
128
134
  currentAction() {
129
135
  return this.action;
130
136
  }
137
+ currentActionStructured() {
138
+ return this.actionTool === undefined ? undefined : { toolName: this.actionTool, ...(this.actionTarget !== undefined ? { target: this.actionTarget } : {}) };
139
+ }
131
140
  }
132
141
  export function stepsFromMessages(messages, lastN) {
133
142
  const results = new Map();
@@ -528,6 +528,7 @@ function makeSubagentResume(deps) {
528
528
  ...(deps.rowAgentType !== undefined ? { agentType: deps.rowAgentType } : {}),
529
529
  ...(deps.rowName !== undefined ? { name: deps.rowName } : {}),
530
530
  sessionId: entry.childSessionId,
531
+ transcriptId: entry.childSessionId,
531
532
  ...(deps.rowParentTaskId !== undefined ? { parentTaskId: deps.rowParentTaskId } : {}),
532
533
  ...(deps.rowParentSessionId !== undefined ? { parentSessionId: deps.rowParentSessionId } : {}),
533
534
  ...(deps.rowRootSessionId !== undefined ? { rootSessionId: deps.rowRootSessionId } : {}),
@@ -553,6 +554,7 @@ function makeSubagentResume(deps) {
553
554
  taskId: deps.taskId,
554
555
  sessionScoped: deps.sessionScoped === true,
555
556
  ...(deps.rowAgentType !== undefined ? { agentType: deps.rowAgentType } : {}),
557
+ transcriptId: entry.childSessionId,
556
558
  progressTaskId: e.taskId,
557
559
  ...(e.parentTaskId !== undefined ? { progressParentTaskId: e.parentTaskId } : {}),
558
560
  ...(e.name !== undefined ? { name: e.name } : {}),
@@ -685,6 +687,7 @@ function makeSubagentResume(deps) {
685
687
  taskId: deps.taskId,
686
688
  sessionScoped: deps.sessionScoped === true,
687
689
  sessionId: entry.childSessionId,
690
+ transcriptId: entry.childSessionId,
688
691
  status: abort.signal.aborted ? "killed" : "failed",
689
692
  seq: entry.cycleSeq,
690
693
  ...(stoppedByReject !== undefined ? { stoppedBy: stoppedByReject } : {}),
@@ -1657,6 +1660,7 @@ function makeSubagentTool(opts, depth, excluded, extraToolsBudget) {
1657
1660
  agentType: spawnAgentType,
1658
1661
  ...(agentName !== undefined ? { name: agentName } : {}),
1659
1662
  sessionId: forkedId,
1663
+ transcriptId: forkedId,
1660
1664
  ...(ctx.taskId !== undefined && ctx.taskId !== ctx.sessionId ? { parentTaskId: ctx.taskId } : {}),
1661
1665
  ...(ctx.sessionId !== undefined ? { parentSessionId: ctx.sessionId } : {}),
1662
1666
  ...((ctx.rootSessionId ?? ctx.sessionId) !== undefined ? { rootSessionId: ctx.rootSessionId ?? ctx.sessionId } : {}),
@@ -1682,15 +1686,18 @@ function makeSubagentTool(opts, depth, excluded, extraToolsBudget) {
1682
1686
  }
1683
1687
  if (e.type === "task_progress") {
1684
1688
  const currentAction = stepRecorder.currentAction();
1689
+ const currentTool = stepRecorder.currentActionStructured();
1685
1690
  sinkEmit({
1686
1691
  kind: "tick",
1687
1692
  taskId,
1688
1693
  sessionScoped: sessionScopedBg === true,
1694
+ transcriptId: forkedId,
1689
1695
  progressTaskId: e.taskId,
1690
1696
  ...(e.parentTaskId !== undefined ? { progressParentTaskId: e.parentTaskId } : {}),
1691
1697
  agentType: spawnAgentType,
1692
1698
  ...(e.name !== undefined ? { name: e.name } : {}),
1693
1699
  ...(currentAction !== undefined ? { currentAction } : {}),
1700
+ ...(currentTool !== undefined ? { currentTool } : {}),
1694
1701
  usage: e.usage,
1695
1702
  });
1696
1703
  }
@@ -1790,6 +1797,7 @@ function makeSubagentTool(opts, depth, excluded, extraToolsBudget) {
1790
1797
  taskId,
1791
1798
  sessionScoped: sessionScopedBg === true,
1792
1799
  sessionId: forkedId,
1800
+ transcriptId: forkedId,
1793
1801
  status: settledBg,
1794
1802
  ...(stoppedByBg !== undefined ? { stoppedBy: stoppedByBg } : {}),
1795
1803
  summary: summaryBg,
@@ -2006,6 +2014,7 @@ task_id: ${taskId}
2006
2014
  agentType: spawnAgentType,
2007
2015
  ...(agentName !== undefined ? { name: agentName } : {}),
2008
2016
  sessionId: bgChildSessionId,
2017
+ transcriptId: bgChildSessionId,
2009
2018
  ...(reviveRow !== undefined
2010
2019
  ? {
2011
2020
  ...(reviveRow.parentTaskId !== undefined ? { parentTaskId: reviveRow.parentTaskId } : {}),
@@ -2071,15 +2080,18 @@ task_id: ${taskId}
2071
2080
  }
2072
2081
  if (e.type === "task_progress") {
2073
2082
  const currentAction = stepRecorder.currentAction();
2083
+ const currentTool = stepRecorder.currentActionStructured();
2074
2084
  sinkEmit({
2075
2085
  kind: "tick",
2076
2086
  taskId,
2077
2087
  sessionScoped: sessionScopedBg === true,
2088
+ transcriptId: bgChildSessionId,
2078
2089
  progressTaskId: e.taskId,
2079
2090
  ...(e.parentTaskId !== undefined ? { progressParentTaskId: e.parentTaskId } : {}),
2080
2091
  agentType: spawnAgentType,
2081
2092
  ...(e.name !== undefined ? { name: e.name } : {}),
2082
2093
  ...(currentAction !== undefined ? { currentAction } : {}),
2094
+ ...(currentTool !== undefined ? { currentTool } : {}),
2083
2095
  usage: e.usage,
2084
2096
  });
2085
2097
  }
@@ -2388,6 +2400,7 @@ task_id: ${taskId}
2388
2400
  taskId,
2389
2401
  sessionScoped: sessionScopedBg === true,
2390
2402
  sessionId: bgChildSessionId,
2403
+ transcriptId: bgChildSessionId,
2391
2404
  status: settled,
2392
2405
  ...(seqAtSettle !== undefined ? { seq: seqAtSettle } : {}),
2393
2406
  ...(stoppedBy !== undefined ? { stoppedBy } : {}),
@@ -1164,6 +1164,7 @@ export async function prepareTask(spec, deps, sessions, resume, _memorySelector,
1164
1164
  ...(deps.hands?.bashReadonlyAllow !== undefined ? { bashReadonlyAllow: deps.hands.bashReadonlyAllow } : {}),
1165
1165
  ...(deps.hands?.commitCoAuthor !== undefined ? { commitCoAuthor: deps.hands.commitCoAuthor } : {}),
1166
1166
  ...(deps.hands?.readImageDownsampler !== undefined ? { readImageDownsampler: deps.hands.readImageDownsampler } : {}),
1167
+ ...(deps.hands?.autoBackgroundOnTimeout !== undefined ? { autoBackgroundOnTimeout: deps.hands.autoBackgroundOnTimeout } : {}),
1167
1168
  beforeWrite: async (w) => {
1168
1169
  const deploymentGate = deps.hands?.beforeWrite;
1169
1170
  if (deploymentGate) {
@@ -2320,6 +2321,7 @@ export async function prepareTask(spec, deps, sessions, resume, _memorySelector,
2320
2321
  args: editArgs,
2321
2322
  message: re.message ?? re.reason ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
2322
2323
  ...askSourceIdentity(),
2324
+ ...(re.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
2323
2325
  }, onAskOf, csignal ?? abortController.signal);
2324
2326
  if (rr.action !== "allow")
2325
2327
  return rr;
@@ -2365,6 +2367,7 @@ export async function prepareTask(spec, deps, sessions, resume, _memorySelector,
2365
2367
  args: presentedArgs,
2366
2368
  message: first.message ?? first.reason ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
2367
2369
  ...askSourceIdentity(),
2370
+ ...(first.action === "ask" && first.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
2368
2371
  }, pc.onAsk, csignal ?? abortController.signal);
2369
2372
  const askWaitMs = Math.max(0, now() - askT0);
2370
2373
  if (resolved.action === "deny" && resolved.approverUnavailable === true) {
@@ -2420,6 +2423,7 @@ export async function prepareTask(spec, deps, sessions, resume, _memorySelector,
2420
2423
  args: presentedArgs,
2421
2424
  message: decision.message ?? decision.reason ?? `approval required for "${creq.toolName}" (inherited parent policy)`,
2422
2425
  ...askSourceIdentity(),
2426
+ ...(decision.action === "ask" && decision.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
2423
2427
  }, pc.onAsk, csignal ?? abortController.signal);
2424
2428
  const askWaitMs = Math.max(0, now() - askT0);
2425
2429
  if (resolved.action === "deny" && resolved.approverUnavailable === true) {
@@ -2654,6 +2658,7 @@ export async function prepareTask(spec, deps, sessions, resume, _memorySelector,
2654
2658
  })(),
2655
2659
  message: decision.message ?? decision.reason ?? `approval required for "${req.toolName}"`,
2656
2660
  ...askSourceIdentity(),
2661
+ ...(decision.action === "ask" && decision.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
2657
2662
  }, onAsk, abortController.signal);
2658
2663
  const waitMs = Math.max(0, now() - t0);
2659
2664
  if (resolved.approverUnavailable !== true) {
@@ -21,6 +21,7 @@ export type PermissionResult = {
21
21
  message?: string;
22
22
  reason?: string;
23
23
  decisionReason?: DecisionReason;
24
+ requiresRealApproval?: boolean;
24
25
  } | {
25
26
  action: "deny";
26
27
  updatedInput?: unknown;
@@ -82,6 +83,7 @@ export interface AskRequest {
82
83
  readonly sourceTaskId?: string;
83
84
  readonly fromSubagent?: true;
84
85
  readonly sourceAgentName?: string;
86
+ readonly requiresRealApproval?: boolean;
85
87
  }
86
88
  export type OnAsk = "deny" | "allow" | ((req: AskRequest, signal?: AbortSignal) => AskOutcome | Promise<AskOutcome>);
87
89
  export type AskOutcome = boolean | "unavailable" | {
@@ -380,9 +380,10 @@ export function createUnverifiableDeletePolicy(opts) {
380
380
  if (finding === undefined)
381
381
  return ALLOW;
382
382
  return {
383
- action: "deny",
383
+ action: "ask",
384
384
  decisionReason: "safety",
385
- reason: `Unverifiable recursive delete blocked (fail-closed): ${finding}. ` +
385
+ requiresRealApproval: true,
386
+ reason: `Unverifiable recursive delete (fail-closed unless cleared): ${finding}. ` +
386
387
  `Re-run the delete with the resolved literal path written into the command itself ` +
387
388
  `(or assign the variable in the same command, e.g. \`DIR=/exact/path; rm -rf "$DIR"\`) so the target can be verified.`,
388
389
  };
@@ -410,13 +411,14 @@ export function createTranscriptIntegrityPolicy(opts) {
410
411
  return false;
411
412
  return dirs.some((d) => abs === d || abs.startsWith(d + sep));
412
413
  };
413
- const denyReason = (what) => ({
414
- action: "deny",
414
+ const askReason = (what) => ({
415
+ action: "ask",
415
416
  decisionReason: "safety",
416
- reason: `Session-transcript write blocked: ${what}. Session transcripts (the .jsonl files under the agent data ` +
417
- `dir's sessions/ directory) are harness-written session state, not agent working files — modifying or ` +
418
- `deleting them tampers with the run's own audit trail. Reading them (ls/cat/grep as a single simple ` +
419
- `command) is fine.`,
417
+ requiresRealApproval: true,
418
+ reason: `Session-transcript write (fail-closed unless cleared): ${what}. Session transcripts (the .jsonl files ` +
419
+ `under the agent data dir's sessions/ directory) are harness-written session state, not agent working ` +
420
+ `files modifying or deleting them tampers with the run's own audit trail. Reading them (ls/cat/grep ` +
421
+ `as a single simple command) is fine.`,
420
422
  });
421
423
  return {
422
424
  check(req) {
@@ -431,7 +433,7 @@ export function createTranscriptIntegrityPolicy(opts) {
431
433
  const parsed = parseLeadingCommandName(command);
432
434
  if ("name" in parsed && readAllow.has(parsed.name))
433
435
  return ALLOW;
434
- return denyReason("name" in parsed
436
+ return askReason("name" in parsed
435
437
  ? `\`${parsed.name}\` is not a read-only command`
436
438
  : "the command references the transcript directory and is not a single read-only command (fail-closed)");
437
439
  }
@@ -439,7 +441,7 @@ export function createTranscriptIntegrityPolicy(opts) {
439
441
  const a = req.args;
440
442
  const p = a?.file_path ?? a?.notebook_path;
441
443
  if (typeof p === "string" && inProtectedDir(p)) {
442
- return denyReason(`"${p}" is inside the session-transcript directory`);
444
+ return askReason(`"${p}" is inside the session-transcript directory`);
443
445
  }
444
446
  }
445
447
  return ALLOW;
@@ -486,6 +488,15 @@ function containsSharedMemory(v, seen = new Set()) {
486
488
  }
487
489
  export async function resolveAsk(req, onAsk, signal) {
488
490
  if (onAsk === "allow") {
491
+ if (req.requiresRealApproval === true) {
492
+ return {
493
+ action: "deny",
494
+ message: `approval required for "${req.toolName}" but the wired approver is a blanket "allow" (no ` +
495
+ `judgment was applied) — this decision requires an actual auto-mode classifier verdict or a ` +
496
+ `real approval callback, neither of which a blanket bypass can provide: ${req.message}`,
497
+ decisionReason: "mode",
498
+ };
499
+ }
489
500
  return { action: "allow", decisionReason: "mode" };
490
501
  }
491
502
  if (onAsk === undefined || onAsk === "deny") {
@@ -146,6 +146,7 @@ export interface HandsBandOptions {
146
146
  commitCoAuthor?: string | false;
147
147
  readImageDownsampler?: ((input: Buffer, mimeType: string) => Promise<import("./mcp.js").DownsampledImage | undefined>) | false;
148
148
  beforeWrite?: BeforeWriteHook;
149
+ autoBackgroundOnTimeout?: boolean;
149
150
  }
150
151
  export interface AgentDefinition {
151
152
  name: string;
@@ -645,6 +646,10 @@ export interface BackgroundChildEvent {
645
646
  progressParentTaskId?: string;
646
647
  name?: string;
647
648
  currentAction?: string;
649
+ currentTool?: {
650
+ toolName: string;
651
+ target?: string;
652
+ };
648
653
  sessionId?: string;
649
654
  transcriptId?: string;
650
655
  recentSteps?: import("../agents/subagent-steps.js").SubagentStep[];
@@ -54,12 +54,14 @@ export declare class NodeExecutionEnv implements ExecutionEnv, BackgroundShellCa
54
54
  kill: () => void;
55
55
  }) => void;
56
56
  onDetachAdopted?: () => void;
57
+ autoBackgroundOnTimeout?: boolean;
57
58
  }): Promise<Result<{
58
59
  stdout: string;
59
60
  stderr: string;
60
61
  exitCode: number;
61
62
  detached?: {
62
63
  shellId: string;
64
+ cause?: "timeout";
63
65
  };
64
66
  }, ExecutionError>>;
65
67
  readTextFile(path: string, abortSignal?: AbortSignal): Promise<Result<string, FileError>>;
@@ -298,12 +298,14 @@ export class NodeExecutionEnv {
298
298
  const stderrTail = new RollingTailBuffer();
299
299
  let settled = false;
300
300
  let timedOut = false;
301
+ let abortKillInFlight = false;
301
302
  let callbackError;
302
303
  let child;
303
304
  const timeoutRef = {};
304
305
  const forceSettleRef = {};
305
306
  const spoolPumpRef = {};
306
307
  const execSpoolRef = {};
308
+ const adoptOnTimeoutRef = {};
307
309
  const pumpSpoolRef = {};
308
310
  const drainSpoolRef = {};
309
311
  const armForceSettle = (result) => {
@@ -327,6 +329,7 @@ export class NodeExecutionEnv {
327
329
  return { stdout: markTruncated(so.text, so.droppedBytes), stderr: markTruncated(se.text, se.droppedBytes) };
328
330
  };
329
331
  const onAbort = () => {
332
+ abortKillInFlight = true;
330
333
  if (child?.pid) {
331
334
  killProcessTree(child.pid, { force: true });
332
335
  }
@@ -415,9 +418,12 @@ export class NodeExecutionEnv {
415
418
  try {
416
419
  options.onSpawn({
417
420
  pid: spawnedPid,
418
- kill: () => killProcessTree(spawnedPid, {
419
- stillRunning: () => spawnedChild.exitCode === null && spawnedChild.signalCode === null,
420
- }),
421
+ kill: () => {
422
+ abortKillInFlight = true;
423
+ killProcessTree(spawnedPid, {
424
+ stillRunning: () => spawnedChild.exitCode === null && spawnedChild.signalCode === null,
425
+ });
426
+ },
421
427
  });
422
428
  }
423
429
  catch {
@@ -428,6 +434,10 @@ export class NodeExecutionEnv {
428
434
  timeoutMs === undefined
429
435
  ? undefined
430
436
  : setTimeout(() => {
437
+ if (options?.autoBackgroundOnTimeout && !abortKillInFlight && adoptOnTimeoutRef.current) {
438
+ adoptOnTimeoutRef.current();
439
+ return;
440
+ }
431
441
  timedOut = true;
432
442
  if (child?.pid) {
433
443
  killProcessTree(child.pid, { force: true });
@@ -543,13 +553,13 @@ export class NodeExecutionEnv {
543
553
  spoolPumpRef.current = setInterval(pumpSpool, EXEC_SPOOL_PUMP_MS);
544
554
  spoolPumpRef.current.unref?.();
545
555
  }
546
- const onDetach = () => {
556
+ const onDetach = (cause) => {
547
557
  if (settled || !child)
548
- return;
558
+ return false;
549
559
  pumpSpoolRef.current?.();
550
560
  const shellId = this.adoptRunningChild(child, stdoutTail.result(), stderrTail.result(), execSpoolRef.current !== undefined ? { spool: execSpoolRef.current, cursors: { out: spoolCursor.out, err: spoolCursor.err } } : undefined);
551
561
  if (shellId === undefined)
552
- return;
562
+ return false;
553
563
  if (spoolPumpRef.current)
554
564
  clearInterval(spoolPumpRef.current);
555
565
  try {
@@ -561,7 +571,16 @@ export class NodeExecutionEnv {
561
571
  clearTimeout(timeoutRef.current);
562
572
  const dso = stdoutTail.result();
563
573
  const dse = stderrTail.result();
564
- settle(ok({ stdout: markTruncated(dso.text, dso.droppedBytes), stderr: markTruncated(dse.text, dse.droppedBytes), exitCode: 0, detached: { shellId } }));
574
+ settle(ok({ stdout: markTruncated(dso.text, dso.droppedBytes), stderr: markTruncated(dse.text, dse.droppedBytes), exitCode: 0, detached: { shellId, ...(cause !== undefined ? { cause } : {}) } }));
575
+ return true;
576
+ };
577
+ adoptOnTimeoutRef.current = () => {
578
+ if (onDetach("timeout"))
579
+ return;
580
+ timedOut = true;
581
+ if (child?.pid)
582
+ killProcessTree(child.pid, { force: true });
583
+ armForceSettle(err(new ExecutionError("timeout", `timeout:${options?.timeout}`, undefined, partialOutput())));
565
584
  };
566
585
  if (options?.detachSignal) {
567
586
  if (options.detachSignal.aborted)
@@ -92,6 +92,7 @@ export interface ExecutionEnvExecOptions {
92
92
  kill: () => void;
93
93
  }) => void;
94
94
  onDetachAdopted?: () => void;
95
+ autoBackgroundOnTimeout?: boolean;
95
96
  }
96
97
  export interface FileSystem {
97
98
  cwd: string;
@@ -135,6 +136,7 @@ export interface Shell {
135
136
  exitCode: number;
136
137
  detached?: {
137
138
  shellId: string;
139
+ cause?: "timeout";
138
140
  };
139
141
  }, ExecutionError>>;
140
142
  cleanup(): Promise<void>;
@@ -47,6 +47,7 @@ export interface ExecClampOption {
47
47
  }) => () => void;
48
48
  }
49
49
  export declare function bashExitOneInterpretation(command: string): string | undefined;
50
+ export declare function canAutoBackground(command: string): boolean;
50
51
  export declare function makeBashTool(env: ExecutionEnv, rootCanonical: string, coAuthor?: string | false, cwdRef?: CwdRef, taskOpts?: {
51
52
  taskRegistry?: TaskRegistry;
52
53
  taskOwner?: string;
@@ -57,6 +58,7 @@ export declare function makeBashTool(env: ExecutionEnv, rootCanonical: string, c
57
58
  }) => void;
58
59
  detachHub?: import("../../core/tool-detach.js").ToolDetachHub;
59
60
  execClamp?: ExecClampOption;
61
+ autoBackgroundOnTimeout?: boolean;
60
62
  }): AgentTool;
61
63
  export declare function makeBashReadonlyTool(env: ExecutionEnv, rootCanonical: string, allow: ReadonlySet<string>, execClamp?: ExecClampOption): AgentTool;
62
64
  export declare function makeBashOutputTool(env: ExecutionEnv): AgentTool;
@@ -78,6 +80,7 @@ export interface HandsToolkitOptions {
78
80
  }) => void;
79
81
  detachHub?: import("../../core/tool-detach.js").ToolDetachHub;
80
82
  execClamp?: ExecClampOption;
83
+ autoBackgroundOnTimeout?: boolean;
81
84
  readImageDownsampler?: ReadImageDownsamplerOption;
82
85
  pdfModelCapabilities?: PdfModelCapabilities;
83
86
  beforeWrite?: BeforeWriteHook;
@@ -1359,6 +1359,57 @@ async function diagnoseDeadCwd(env, toolName, effectiveCwd, root, cwdRef) {
1359
1359
  `It was likely deleted by a previous command (e.g. rm -rf on the current directory). There is no directory to reset to; ` +
1360
1360
  `this task cannot recover its working tree.`);
1361
1361
  }
1362
+ const AUTO_BACKGROUND_BLOCKED_FIRST_WORDS = new Set(["sleep"]);
1363
+ function stripAutoBackgroundWrapperPrefixes(segment) {
1364
+ let s = segment.trim();
1365
+ const wrappers = [
1366
+ /^timeout\s+(?:-[A-Za-z]\S*\s+)*\d+(?:\.\d+)?[smhd]?\s+/,
1367
+ /^time\s+/,
1368
+ /^nice(?:\s+-n?\s*-?\d+)?\s+/,
1369
+ /^nohup\s+/,
1370
+ /^command\s+/,
1371
+ /^builtin\s+/,
1372
+ /^stdbuf(?:\s+-[ioe]\S+)+\s+/,
1373
+ /^noglob\s+/,
1374
+ /^[A-Za-z_][A-Za-z0-9_]*=\S+\s+/,
1375
+ ];
1376
+ let changed = true;
1377
+ while (changed) {
1378
+ changed = false;
1379
+ for (const re of wrappers) {
1380
+ const stripped = s.replace(re, "");
1381
+ if (stripped !== s) {
1382
+ s = stripped.trim();
1383
+ changed = true;
1384
+ }
1385
+ }
1386
+ }
1387
+ return s;
1388
+ }
1389
+ function commandBasename(token) {
1390
+ const idx = token.lastIndexOf("/");
1391
+ return idx === -1 ? token : token.slice(idx + 1);
1392
+ }
1393
+ export function canAutoBackground(command) {
1394
+ const segments = command
1395
+ .split(/&&|\|\||;|\||\r?\n/)
1396
+ .map((s) => s.trim())
1397
+ .filter((s) => s.length > 0);
1398
+ if (segments.length === 0)
1399
+ return true;
1400
+ for (const seg of segments) {
1401
+ const tokens = stripAutoBackgroundWrapperPrefixes(seg).split(/\s+/);
1402
+ const word = commandBasename(tokens[0] ?? "");
1403
+ if (word === "git")
1404
+ return false;
1405
+ if (word === "xargs" && tokens.slice(1).some((t) => commandBasename(t) === "git"))
1406
+ return false;
1407
+ }
1408
+ const firstWord = commandBasename(stripAutoBackgroundWrapperPrefixes(segments[0]).split(/\s+/)[0] ?? "");
1409
+ if (AUTO_BACKGROUND_BLOCKED_FIRST_WORDS.has(firstWord))
1410
+ return false;
1411
+ return true;
1412
+ }
1362
1413
  async function runShell(env, cwd, toolName, command, timeoutSec, signal, cwdRef, detach, execClamp, toolCallId) {
1363
1414
  let timeout = Math.min(BASH_MAX_TIMEOUT_SEC, Math.max(1, Math.floor(timeoutSec ?? BASH_DEFAULT_TIMEOUT_SEC)));
1364
1415
  const requestedSec = timeout;
@@ -1398,6 +1449,7 @@ async function runShell(env, cwd, toolName, command, timeoutSec, signal, cwdRef,
1398
1449
  deregisterKill?.();
1399
1450
  deregisterKill = undefined;
1400
1451
  },
1452
+ autoBackgroundOnTimeout: detach.autoBackgroundEligible === true && !clampedByDeadline,
1401
1453
  }
1402
1454
  : {}),
1403
1455
  ...(registerKillHandle !== undefined && toolCallId !== undefined
@@ -1460,7 +1512,7 @@ async function runShell(env, cwd, toolName, command, timeoutSec, signal, cwdRef,
1460
1512
  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.`;
1461
1513
  }
1462
1514
  if (res.value.detached && detach) {
1463
- return await detach.onDetached(res.value.detached.shellId, res.value.stdout, res.value.stderr);
1515
+ return await detach.onDetached(res.value.detached.shellId, res.value.stdout, res.value.stderr, res.value.detached.cause);
1464
1516
  }
1465
1517
  let { stderr } = res.value;
1466
1518
  const { stdout, exitCode } = res.value;
@@ -1656,7 +1708,7 @@ export function makeBashTool(env, rootCanonical, coAuthor = false, cwdRef = { cu
1656
1708
  const detachChain = taskOpts.detachHub && taskOpts.taskRegistry && hasBackgroundShell(env) && env.backgroundCapabilities.supportsDetach === true
1657
1709
  ? {
1658
1710
  signal: taskOpts.detachHub.signalFor(ctx.toolCallId),
1659
- onDetached: async (shellId, stdoutSoFar, stderrSoFar) => {
1711
+ onDetached: async (shellId, stdoutSoFar, stderrSoFar, cause) => {
1660
1712
  const sessionScoped = taskOpts.sessionId !== undefined;
1661
1713
  const owner = sessionScoped ? taskOpts.sessionId : (ctx.taskId ?? taskOpts.taskOwner);
1662
1714
  const scope = ctx.principal ?? taskOpts.taskScope;
@@ -1720,7 +1772,9 @@ export function makeBashTool(env, rootCanonical, coAuthor = false, cwdRef = { cu
1720
1772
  ? ` Time budget: auto-terminates if still running after ${Math.min(adoptCaps.defaultBgTimeoutSec, adoptCaps.maxBgTimeoutSec)}s (hard cap ${adoptCaps.maxBgTimeoutSec}s).`
1721
1773
  : "";
1722
1774
  return {
1723
- content: `Command moved to background; task_id=${taskId}. ` +
1775
+ content: (cause === "timeout"
1776
+ ? `Command exceeded its timeout but is still running — moved to background instead of being killed (task_id=${taskId}). `
1777
+ : `Command moved to background; task_id=${taskId}. `) +
1724
1778
  (outputFile !== undefined
1725
1779
  ? `Output file: ${outputFile} (full output is appended there — Read it any time). `
1726
1780
  : `Use TaskOutput("${taskId}") to read its output. `) +
@@ -1730,9 +1784,10 @@ export function makeBashTool(env, rootCanonical, coAuthor = false, cwdRef = { cu
1730
1784
  : `Poll TaskOutput until its status is no longer "running".`) +
1731
1785
  adoptBudgetNote +
1732
1786
  (tail ? `\n--- output so far (tail) ---\n${tail}` : ""),
1733
- details: { type: "bash", detached: true, task_id: taskId, ...(description !== undefined ? { description } : {}), ...(outputFile !== undefined ? { output_file: outputFile } : {}) },
1787
+ details: { type: "bash", detached: true, task_id: taskId, ...(description !== undefined ? { description } : {}), ...(outputFile !== undefined ? { output_file: outputFile } : {}), ...(cause === "timeout" ? { autoBackgrounded: true } : {}) },
1734
1788
  };
1735
1789
  },
1790
+ autoBackgroundEligible: taskOpts.autoBackgroundOnTimeout === true && canAutoBackground(command),
1736
1791
  }
1737
1792
  : undefined;
1738
1793
  let res;
@@ -1882,6 +1937,7 @@ export function createHandsToolkit(env, readFileState, rootCanonical, opts = {})
1882
1937
  onTaskNotification: opts.taskNotification,
1883
1938
  detachHub: opts.detachHub,
1884
1939
  execClamp: opts.execClamp,
1940
+ ...(opts.autoBackgroundOnTimeout !== undefined ? { autoBackgroundOnTimeout: opts.autoBackgroundOnTimeout } : {}),
1885
1941
  }));
1886
1942
  if (!readOnly && mountBackgroundTaskTools && hasBackgroundShell(env)) {
1887
1943
  tools.push(opts.taskRegistry
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "1.427.0",
3
+ "version": "1.429.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",