@@ -15,11 +15,20 @@ export type ToolWatchdogConfig = {
1515 waitForApproval ?: boolean ;
1616} ;
1717
18- // Default exceeds shell-guard's per-command max so run_shell is not cut off by
19- // this layer before its own timeout fires.
18+ // Outer budget for tools that have no per-call timeout. run_shell with a longer
19+ // requested timeout is resolved separately (see resolveToolExecutionTimeoutMs)
20+ // so this default — and MAX_TOOL_EXECUTION_TIMEOUT_MS / tools.maxTimeoutMs —
21+ // cannot abort it first. Omitting run_shell timeout still defaults to 15s
22+ // inside shell-guard.
2023export const DEFAULT_TOOL_EXECUTION_TIMEOUT_MS = 660_000 ;
2124export const MAX_TOOL_EXECUTION_TIMEOUT_MS = 1_800_000 ;
2225
26+ /**
27+ * Watchdog arms before shell-guard, so the outer budget must outlast a matching
28+ * requested run_shell timeout or this layer wins the race and aborts first.
29+ */
30+ export const RUN_SHELL_WATCHDOG_SLACK_MS = 1_000 ;
31+
2332/**
2433 * After budget/parent abort wins the race, wait this long for the in-flight
2534 * execute to settle with a usable (non-error) body — e.g. task-tool salvage —
@@ -37,12 +46,44 @@ export const MAX_TOOL_APPROVAL_PAUSE_MS = 1_800_000;
3746
3847const BUDGET_EXPIRED = Symbol ( "tool-execution-budget-expired" ) ;
3948
40- export function resolveToolExecutionTimeoutMs ( config ?: ToolWatchdogConfig ) : number {
49+ export function resolveToolExecutionTimeoutMs (
50+ config ?: ToolWatchdogConfig ,
51+ call ?: ToolCall ,
52+ ) : number {
53+ if ( call ?. name === "run_shell" ) {
54+ return resolveRunShellWatchdogTimeoutMs ( config , call ) ;
55+ }
4156 const max = config ?. maxMs ?? MAX_TOOL_EXECUTION_TIMEOUT_MS ;
4257 const raw = config ?. defaultMs ?? DEFAULT_TOOL_EXECUTION_TIMEOUT_MS ;
4358 return Math . min ( max , Math . max ( 1 , Math . floor ( raw ) ) ) ;
4459}
4560
61+ function requestedRunShellTimeoutMs ( call : ToolCall ) : number | undefined {
62+ const timeout = call . arguments . timeout ;
63+ if ( typeof timeout !== "number" || ! Number . isFinite ( timeout ) || timeout <= 0 ) {
64+ return undefined ;
65+ }
66+ return Math . floor ( timeout ) ;
67+ }
68+
69+ /**
70+ * run_shell's watchdog floor is the requested command timeout (plus slack so
71+ * this outer timer cannot beat shell-guard). tools.maxTimeoutMs /
72+ * MAX_TOOL_EXECUTION_TIMEOUT_MS still bound other tools only — they must not
73+ * reimpose a cap when the operator/model passed a longer run_shell timeout.
74+ * Omitting timeout leaves the default outer budget (shell-guard still uses 15s).
75+ */
76+ function resolveRunShellWatchdogTimeoutMs (
77+ config : ToolWatchdogConfig | undefined ,
78+ call : ToolCall ,
79+ ) : number {
80+ const raw = config ?. defaultMs ?? DEFAULT_TOOL_EXECUTION_TIMEOUT_MS ;
81+ const floor = Math . max ( 1 , Math . floor ( raw ) ) ;
82+ const requested = requestedRunShellTimeoutMs ( call ) ;
83+ if ( requested === undefined ) return floor ;
84+ return Math . max ( floor , requested + RUN_SHELL_WATCHDOG_SLACK_MS ) ;
85+ }
86+
4687/** Default true: freeze tool budget while a permission prompt is open. */
4788export function resolveWaitForApproval ( config ?: ToolWatchdogConfig ) : boolean {
4889 return config ?. waitForApproval !== false ;
0 commit comments