earendil-works/pi · error · Error

Invalid timeout: must be a finite number of seconds

Error message

Invalid timeout: must be a finite number of seconds

What it means

The bash tool validates its optional timeout at the top of execute (bash.ts:60): it must be a finite number of seconds greater than zero, or omitted entirely (omission means no timeout at all). NaN, Infinity, 0 and negative values throw a plain Error before any command runs. The field is seconds, not milliseconds.

Source

Thrown at packages/agent/src/harness/tools/bash.ts:44

	env: Record<string, string>;
	inheritEnv: boolean;
}

export type BashPrepare<TContext extends ExecutionToolContext = ExecutionToolContext> = (
	execution: BashExecution,
	context: TContext,
	signal?: AbortSignal,
) => void | Promise<void>;

export interface BashToolOptions<TContext extends ExecutionToolContext = ExecutionToolContext> {
	commandPrefix?: string;
	prepare?: BashPrepare<TContext>;
}

function validateTimeout(timeout: number | undefined): void {
	if (timeout === undefined) return;
	if (!Number.isFinite(timeout) || timeout <= 0) {
		throw new Error("Invalid timeout: must be a finite number of seconds");
	}
	if (timeout > MAX_TIMEOUT_SECONDS) {
		throw new Error(`Invalid timeout: maximum is ${MAX_TIMEOUT_SECONDS} seconds`);
	}
}

export function createBashTool<TContext extends ExecutionToolContext = ExecutionToolContext>(
	options?: BashToolOptions<TContext>,
): AgentHarnessTool<TContext, typeof bashSchema, BashToolDetails | undefined> {
	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,
		async execute(_toolCallId, { command, timeout }, signal, onUpdate, context) {
			validateTimeout(timeout);
			const { env } = context;
			const execution: BashExecution = {

View on GitHub (pinned to 4af9d21d3b)

Solutions

  1. Omit timeout when you want no limit instead of passing a sentinel
  2. Clamp computed values: Number.isFinite(t) ? Math.min(Math.max(t, 1), 2147483.647) : undefined
  3. Fix the unit: divide millisecond values by 1000 before passing

Example fix

// before
await bashTool.execute(id, { command: 'npm test', timeout: 0 }); // throws

// after
await bashTool.execute(id, { command: 'npm test' }); // no timeout
Defensive patterns

Strategy: validation

Validate before calling

const MAX_TIMEOUT_SECONDS = 2_147_483_647 / 1000;
const normalizeTimeout = (t: number | undefined): number | undefined =>
  t === undefined || !Number.isFinite(t) || t <= 0
    ? undefined
    : Math.min(t, MAX_TIMEOUT_SECONDS);

await bashTool.execute(id, { command, timeout: normalizeTimeout(rawTimeout) });

Type guard

function isValidTimeout(t: unknown): t is number {
  return typeof t === 'number' && Number.isFinite(t) && t > 0;
}

Prevention

When it happens

Trigger: Tool input { command, timeout: 0 }; timeout computed as (end - start) / 1000 where an operand is undefined and the result is NaN; -1 passed as an 'unlimited' sentinel; Infinity leaking from unconstrained division.

Common situations: A config default of 0 meaning 'disabled' forwarded as the timeout; millisecond/second mix-ups that collapse to zero or negative differences; model-generated tool calls emitting timeout: 0.

Understand the failure class

Related errors


AI-assisted analysis of earendil-works/pi@4af9d21d3b (2026-08-24). Data as JSON: /api/errors/0229892c349f5cf9. Report an issue: GitHub.