fix(bash): add 120s default timeout to prevent autonomous mode hangs
- Add BUILT_IN_DEFAULT_TIMEOUT_SECS = 120 constant to bash tool - Compute effectiveTimeout = timeout ?? resolvedDefaultTimeout so LLM calls without a timeout get the 120s guard automatically - Add defaultTimeoutSeconds? to BashToolOptions for override at creation - Dynamic bashSchemaWithDefault describes the actual default in the LLM tool description, improving model awareness - Add BashSettings interface + getBashDefaultTimeoutSeconds() to SettingsManager so users can override or disable via settings.json - Wire defaultTimeoutSeconds into agent-session.ts _buildRuntime() Root cause: npx sf --help triggered npm package download, hanging for 4+ minutes without timeout, consuming entire autonomous run budget. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
7ef58422b1
commit
ca7368e5f1
4 changed files with 63 additions and 4 deletions
|
|
@ -2530,6 +2530,8 @@ export class AgentSession {
|
|||
read: { autoResizeImages },
|
||||
bash: {
|
||||
commandPrefix: shellCommandPrefix,
|
||||
defaultTimeoutSeconds:
|
||||
this.settingsManager.getBashDefaultTimeoutSeconds(),
|
||||
interceptor: {
|
||||
enabled: this.settingsManager.getBashInterceptorEnabled(),
|
||||
rules: this.settingsManager.getBashInterceptorRules(),
|
||||
|
|
|
|||
|
|
@ -51,6 +51,14 @@ export interface BashInterceptorSettings {
|
|||
rules?: BashInterceptorRule[]; // override default rules
|
||||
}
|
||||
|
||||
export interface BashSettings {
|
||||
/**
|
||||
* Default timeout in seconds for bash commands when the LLM does not provide one.
|
||||
* Set to 0 to disable (commands run without a timeout). Defaults to 120.
|
||||
*/
|
||||
defaultTimeoutSeconds?: number;
|
||||
}
|
||||
|
||||
export interface MarkdownSettings {
|
||||
codeBlockIndent?: string; // default: " "
|
||||
}
|
||||
|
|
@ -173,6 +181,7 @@ export interface Settings {
|
|||
memory?: MemorySettings;
|
||||
async?: AsyncSettings;
|
||||
bashInterceptor?: BashInterceptorSettings;
|
||||
bash?: BashSettings;
|
||||
taskIsolation?: TaskIsolationSettings;
|
||||
fallback?: FallbackSettings;
|
||||
modelDiscovery?: ModelDiscoverySettings;
|
||||
|
|
@ -505,6 +514,16 @@ export class SettingsManager {
|
|||
return this.settings.bashInterceptor?.rules;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default bash command timeout in seconds. Returns undefined when unset,
|
||||
* which lets the bash tool use its built-in default (120s).
|
||||
* Returns 0 explicitly when the user has disabled the default.
|
||||
*/
|
||||
getBashDefaultTimeoutSeconds(): number | undefined {
|
||||
const val = this.settings.bash?.defaultTimeoutSeconds;
|
||||
return typeof val === "number" ? val : undefined;
|
||||
}
|
||||
|
||||
reload(): void {
|
||||
const globalLoad = SettingsManager.tryLoadFromStorage(
|
||||
this.storage,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,15 @@ import {
|
|||
truncateTail,
|
||||
} from "./truncate.js";
|
||||
|
||||
/**
|
||||
* Built-in default timeout (seconds) applied when the caller does not provide
|
||||
* one and `BashToolOptions.defaultTimeoutSeconds` is not set.
|
||||
* Prevents stuck commands (e.g. `npx pkg --help` triggering a download) from
|
||||
* consuming the entire autonomous-mode run budget.
|
||||
* Set `defaultTimeoutSeconds: 0` in BashToolOptions to opt out.
|
||||
*/
|
||||
const BUILT_IN_DEFAULT_TIMEOUT_SECS = 120;
|
||||
|
||||
// Cached Win32 FFI handles for restoring VT input after child processes
|
||||
let _vtHandles: {
|
||||
GetConsoleMode: any;
|
||||
|
|
@ -145,7 +154,8 @@ const bashSchema = Type.Object({
|
|||
command: Type.String({ description: "Bash command to execute" }),
|
||||
timeout: Type.Optional(
|
||||
Type.Number({
|
||||
description: "Timeout in seconds (optional, no default timeout)",
|
||||
description:
|
||||
"Timeout in seconds. Omitting uses the built-in default (see createBashTool). Pass 0 to run without timeout.",
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
|
@ -316,6 +326,12 @@ export interface BashToolOptions {
|
|||
};
|
||||
/** Tool names available in the session, used by the interceptor to check if replacement tools exist */
|
||||
availableToolNames?: string[] | (() => string[]);
|
||||
/**
|
||||
* Default timeout in seconds applied when the LLM does not provide one.
|
||||
* Set to 0 to disable the built-in default (allows commands to run indefinitely).
|
||||
* Defaults to BUILT_IN_DEFAULT_TIMEOUT_SECS (120s) when not set.
|
||||
*/
|
||||
defaultTimeoutSeconds?: number;
|
||||
}
|
||||
|
||||
export function createBashTool(
|
||||
|
|
@ -326,6 +342,12 @@ export function createBashTool(
|
|||
const commandPrefix = options?.commandPrefix;
|
||||
const spawnHook = options?.spawnHook;
|
||||
const artifactManager = options?.artifactManager;
|
||||
// Resolve the default timeout once at construction time.
|
||||
// Explicit 0 in options disables the built-in default; undefined falls through to BUILT_IN_DEFAULT_TIMEOUT_SECS.
|
||||
const resolvedDefaultTimeout =
|
||||
options?.defaultTimeoutSeconds !== undefined
|
||||
? options.defaultTimeoutSeconds
|
||||
: BUILT_IN_DEFAULT_TIMEOUT_SECS;
|
||||
|
||||
// Pre-compile interceptor rules once at construction time
|
||||
const interceptorInstance = options?.interceptor?.enabled
|
||||
|
|
@ -334,17 +356,32 @@ export function createBashTool(
|
|||
)
|
||||
: null;
|
||||
|
||||
const timeoutDescription =
|
||||
resolvedDefaultTimeout > 0
|
||||
? `Default: ${resolvedDefaultTimeout}s. Pass 0 to run without a timeout.`
|
||||
: "No default timeout — commands run until completion.";
|
||||
const bashSchemaWithDefault = Type.Object({
|
||||
command: Type.String({ description: "Bash command to execute" }),
|
||||
timeout: Type.Optional(
|
||||
Type.Number({
|
||||
description: `Timeout in seconds. ${timeoutDescription}`,
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
return {
|
||||
name: "bash",
|
||||
label: "bash",
|
||||
description: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Optionally provide a timeout in seconds.`,
|
||||
parameters: bashSchema,
|
||||
description: `Execute a bash command in the current working directory. Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Default timeout: ${resolvedDefaultTimeout > 0 ? `${resolvedDefaultTimeout}s` : "none"} — pass a higher value for long-running commands (builds, installs, tests).`,
|
||||
parameters: bashSchemaWithDefault,
|
||||
execute: async (
|
||||
_toolCallId: string,
|
||||
{ command, timeout }: { command: string; timeout?: number },
|
||||
signal?: AbortSignal,
|
||||
onUpdate?,
|
||||
) => {
|
||||
// Apply the resolved default when the LLM does not provide a timeout.
|
||||
const effectiveTimeout = timeout ?? resolvedDefaultTimeout;
|
||||
// Check bash interceptor — block commands that duplicate dedicated tools
|
||||
if (interceptorInstance) {
|
||||
const toolNames =
|
||||
|
|
@ -463,7 +500,7 @@ export function createBashTool(
|
|||
.exec(spawnContext.command, spawnContext.cwd, {
|
||||
onData: handleData,
|
||||
signal,
|
||||
timeout,
|
||||
timeout: effectiveTimeout,
|
||||
env: spawnContext.env,
|
||||
})
|
||||
.then(({ exitCode }) => {
|
||||
|
|
|
|||
|
|
@ -280,6 +280,7 @@ export {
|
|||
} from "./core/session-manager.js";
|
||||
export {
|
||||
type AsyncSettings,
|
||||
type BashSettings,
|
||||
type CompactionSettings,
|
||||
type ImageSettings,
|
||||
type MemorySettings,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue