diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index ba5451bd..e7b346a4 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -42,6 +42,7 @@ - Renderers return inner content; the TUI wraps it in a styled Box - New types: `HookMessage`, `RegisteredCommand`, `HookContext` - Handler types renamed: `SendHandler` → `SendMessageHandler`, new `AppendEntryHandler` + - Removed `hookTimeout` setting - hooks no longer have execution timeouts (use Ctrl+C to abort hung hooks) - **SessionManager**: - `getSessionFile()` now returns `string | undefined` (undefined for in-memory sessions) - **Themes**: Custom themes must add `selectedBg`, `customMessageBg`, `customMessageText`, `customMessageLabel` color tokens (50 total) diff --git a/packages/coding-agent/docs/hooks.md b/packages/coding-agent/docs/hooks.md index ea79bc60..7e720f6e 100644 --- a/packages/coding-agent/docs/hooks.md +++ b/packages/coding-agent/docs/hooks.md @@ -51,13 +51,10 @@ Additional paths via `settings.json`: ```json { - "hooks": ["/path/to/hook.ts"], - "hookTimeout": 30000 + "hooks": ["/path/to/hook.ts"] } ``` -The `hookTimeout` (default 30s) applies to most events. `tool_call` has no timeout since it may prompt the user. - ## Available Imports | Package | Purpose | @@ -329,7 +326,7 @@ pi.on("context", async (event, ctx) => { #### tool_call -Fired before tool executes. **Can block.** No timeout. +Fired before tool executes. **Can block.** ```typescript pi.on("tool_call", async (event, ctx) => { @@ -654,8 +651,8 @@ In print mode, `select()` returns `undefined`, `confirm()` returns `false`, `inp - Hook errors are logged, agent continues - `tool_call` errors block the tool (fail-safe) -- Timeout errors (default 30s) are logged but don't block - Errors display in UI with hook path and message +- If a hook hangs, use Ctrl+C to abort ## Debugging diff --git a/packages/coding-agent/src/core/hooks/runner.ts b/packages/coding-agent/src/core/hooks/runner.ts index 998e39b7..da15fc85 100644 --- a/packages/coding-agent/src/core/hooks/runner.ts +++ b/packages/coding-agent/src/core/hooks/runner.ts @@ -25,11 +25,6 @@ import type { ToolResultEventResult, } from "./types.js"; -/** - * Default timeout for hook execution (30 seconds). - */ -const DEFAULT_TIMEOUT = 30000; - /** * Listener for hook errors. */ @@ -38,20 +33,6 @@ export type HookErrorListener = (error: HookError) => void; // Re-export execCommand for backward compatibility export { execCommand } from "../exec.js"; -/** - * Create a promise that rejects after a timeout. - */ -function createTimeout(ms: number): { promise: Promise; clear: () => void } { - let timeoutId: NodeJS.Timeout; - const promise = new Promise((_, reject) => { - timeoutId = setTimeout(() => reject(new Error(`Hook timed out after ${ms}ms`)), ms); - }); - return { - promise, - clear: () => clearTimeout(timeoutId), - }; -} - /** No-op UI context used when no UI is available */ const noOpUIContext: HookUIContext = { select: async () => undefined, @@ -71,24 +52,16 @@ export class HookRunner { private cwd: string; private sessionManager: SessionManager; private modelRegistry: ModelRegistry; - private timeout: number; private errorListeners: Set = new Set(); private getModel: () => Model | undefined = () => undefined; - constructor( - hooks: LoadedHook[], - cwd: string, - sessionManager: SessionManager, - modelRegistry: ModelRegistry, - timeout: number = DEFAULT_TIMEOUT, - ) { + constructor(hooks: LoadedHook[], cwd: string, sessionManager: SessionManager, modelRegistry: ModelRegistry) { this.hooks = hooks; this.uiContext = noOpUIContext; this.hasUI = false; this.cwd = cwd; this.sessionManager = sessionManager; this.modelRegistry = modelRegistry; - this.timeout = timeout; } /** @@ -262,16 +235,7 @@ export class HookRunner { for (const handler of handlers) { try { - // No timeout for session_before_compact events (like tool_call, they may take a while) - let handlerResult: unknown; - - if (event.type === "session_before_compact") { - handlerResult = await handler(event, ctx); - } else { - const timeout = createTimeout(this.timeout); - handlerResult = await Promise.race([handler(event, ctx), timeout.promise]); - timeout.clear(); - } + const handlerResult = await handler(event, ctx); // For session before_* events, capture the result (for cancellation) if (this.isSessionBeforeEvent(event.type) && handlerResult) { @@ -348,9 +312,7 @@ export class HookRunner { for (const handler of handlers) { try { const event: ContextEvent = { type: "context", messages: currentMessages }; - const timeout = createTimeout(this.timeout); - const handlerResult = await Promise.race([handler(event, ctx), timeout.promise]); - timeout.clear(); + const handlerResult = await handler(event, ctx); if (handlerResult && (handlerResult as ContextEventResult).messages) { currentMessages = (handlerResult as ContextEventResult).messages!; @@ -387,9 +349,7 @@ export class HookRunner { for (const handler of handlers) { try { const event: BeforeAgentStartEvent = { type: "before_agent_start", prompt, images }; - const timeout = createTimeout(this.timeout); - const handlerResult = await Promise.race([handler(event, ctx), timeout.promise]); - timeout.clear(); + const handlerResult = await handler(event, ctx); // Take the first message returned if (handlerResult && (handlerResult as BeforeAgentStartEventResult).message && !result) { diff --git a/packages/coding-agent/src/core/sdk.ts b/packages/coding-agent/src/core/sdk.ts index d2779ced..e2eb11e0 100644 --- a/packages/coding-agent/src/core/sdk.ts +++ b/packages/coding-agent/src/core/sdk.ts @@ -313,7 +313,6 @@ export function loadSettings(cwd?: string, agentDir?: string): Settings { shellPath: manager.getShellPath(), collapseChangelog: manager.getCollapseChangelog(), hooks: manager.getHookPaths(), - hookTimeout: manager.getHookTimeout(), customTools: manager.getCustomToolPaths(), skills: manager.getSkillsSettings(), terminal: { showImages: manager.getShowImages() }, @@ -536,7 +535,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} if (options.hooks !== undefined) { if (options.hooks.length > 0) { const loadedHooks = createLoadedHooksFromDefinitions(options.hooks); - hookRunner = new HookRunner(loadedHooks, cwd, sessionManager, modelRegistry, settingsManager.getHookTimeout()); + hookRunner = new HookRunner(loadedHooks, cwd, sessionManager, modelRegistry); } } else { // Discover hooks, merging with additional paths @@ -547,7 +546,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} console.error(`Failed to load hook "${path}": ${error}`); } if (hooks.length > 0) { - hookRunner = new HookRunner(hooks, cwd, sessionManager, modelRegistry, settingsManager.getHookTimeout()); + hookRunner = new HookRunner(hooks, cwd, sessionManager, modelRegistry); } } diff --git a/packages/coding-agent/src/core/settings-manager.ts b/packages/coding-agent/src/core/settings-manager.ts index 3a116d63..4231655f 100644 --- a/packages/coding-agent/src/core/settings-manager.ts +++ b/packages/coding-agent/src/core/settings-manager.ts @@ -48,7 +48,6 @@ export interface Settings { shellPath?: string; // Custom shell path (e.g., for Cygwin users on Windows) collapseChangelog?: boolean; // Show condensed changelog after update (use /changelog for full) hooks?: string[]; // Array of hook file paths - hookTimeout?: number; // Timeout for hook execution in ms (default: 30000) customTools?: string[]; // Array of custom tool file paths skills?: SkillsSettings; terminal?: TerminalSettings; @@ -322,15 +321,6 @@ export class SettingsManager { this.save(); } - getHookTimeout(): number { - return this.settings.hookTimeout ?? 30000; - } - - setHookTimeout(timeout: number): void { - this.globalSettings.hookTimeout = timeout; - this.save(); - } - getCustomToolPaths(): string[] { return [...(this.settings.customTools ?? [])]; }