From d33fefd1336129572a03d983fae5c32bf5f4998f Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Tue, 16 Dec 2025 23:07:11 -0500 Subject: [PATCH] feat(websearch): dynamic hook timeout from config + grok-code default - Hook timeout now computed from max provider timeout + 30s buffer - Removes hardcoded HOOK_TIMEOUT_SECONDS constant - ensureHookConfig() now updates timeout when it changes - Default OpenCode model changed from gpt-5-nano to grok-code - Default OpenCode timeout changed from 60s to 90s - Single source of truth: config.yaml controls all timeouts --- lib/hooks/websearch-transformer.cjs | 6 ++-- src/config/unified-config-loader.ts | 8 ++--- src/config/unified-config-types.ts | 2 +- src/utils/websearch-manager.ts | 51 ++++++++++++++++++++++++----- src/web-server/routes.ts | 2 +- 5 files changed, 52 insertions(+), 17 deletions(-) diff --git a/lib/hooks/websearch-transformer.cjs b/lib/hooks/websearch-transformer.cjs index 1658e439..bdb40ec4 100644 --- a/lib/hooks/websearch-transformer.cjs +++ b/lib/hooks/websearch-transformer.cjs @@ -13,7 +13,7 @@ * CCS_WEBSEARCH_GEMINI=1 - Enable Gemini CLI provider * CCS_WEBSEARCH_OPENCODE=1 - Enable OpenCode provider * CCS_WEBSEARCH_GROK=1 - Enable Grok CLI provider - * CCS_WEBSEARCH_OPENCODE_MODEL - OpenCode model (default: opencode/gpt-5-nano) + * CCS_WEBSEARCH_OPENCODE_MODEL - OpenCode model (default: opencode/grok-code) * CCS_DEBUG=1 - Enable debug output * * Exit codes: @@ -61,8 +61,8 @@ const PROVIDER_CONFIG = { opencode: { // Model to use (can be overridden via CCS_WEBSEARCH_OPENCODE_MODEL env var) - model: 'opencode/gpt-5-nano', - // Alternative models: opencode/gpt-4o, opencode/claude-3.5-sonnet + model: 'opencode/grok-code', + // Alternative models: opencode/gpt-4o, opencode/claude-3.5-sonnet, opencode/gpt-5-nano // Provider-specific: OpenCode has built-in web search via Zen toolInstruction: 'Search the web using your built-in capabilities.', diff --git a/src/config/unified-config-loader.ts b/src/config/unified-config-loader.ts index fc81d8ac..9dbf877f 100644 --- a/src/config/unified-config-loader.ts +++ b/src/config/unified-config-loader.ts @@ -144,8 +144,8 @@ function mergeWithDefaults(partial: Partial): UnifiedConfig { }, opencode: { enabled: partial.websearch?.providers?.opencode?.enabled ?? false, - model: partial.websearch?.providers?.opencode?.model ?? 'opencode/gpt-5-nano', - timeout: partial.websearch?.providers?.opencode?.timeout ?? 60, + model: partial.websearch?.providers?.opencode?.model ?? 'opencode/grok-code', + timeout: partial.websearch?.providers?.opencode?.timeout ?? 90, }, grok: { enabled: partial.websearch?.providers?.grok?.enabled ?? false, @@ -392,8 +392,8 @@ export function getWebSearchConfig(): { const opencodeConfig = { enabled: config.websearch?.providers?.opencode?.enabled ?? false, - model: config.websearch?.providers?.opencode?.model ?? 'opencode/gpt-5-nano', - timeout: config.websearch?.providers?.opencode?.timeout ?? 60, + model: config.websearch?.providers?.opencode?.model ?? 'opencode/grok-code', + timeout: config.websearch?.providers?.opencode?.timeout ?? 90, }; const grokConfig = { diff --git a/src/config/unified-config-types.ts b/src/config/unified-config-types.ts index 14f0e135..4027c2d2 100644 --- a/src/config/unified-config-types.ts +++ b/src/config/unified-config-types.ts @@ -127,7 +127,7 @@ export interface GrokWebSearchConfig { export interface OpenCodeWebSearchConfig { /** Enable OpenCode CLI for WebSearch (default: false) */ enabled?: boolean; - /** Model to use (default: opencode/gpt-5-nano) */ + /** Model to use (default: opencode/grok-code) */ model?: string; /** Timeout in seconds (default: 60) */ timeout?: number; diff --git a/src/utils/websearch-manager.ts b/src/utils/websearch-manager.ts index aae724cc..82d52e5c 100644 --- a/src/utils/websearch-manager.ts +++ b/src/utils/websearch-manager.ts @@ -26,8 +26,11 @@ const CCS_HOOKS_DIR = path.join(os.homedir(), '.ccs', 'hooks'); // Hook file name const WEBSEARCH_HOOK = 'websearch-transformer.cjs'; -// Default hook timeout in seconds (Gemini CLI needs time) -const HOOK_TIMEOUT_SECONDS = 60; +// Buffer time added to max provider timeout for hook timeout (seconds) +const HOOK_TIMEOUT_BUFFER = 30; + +// Minimum hook timeout in seconds (fallback if no providers configured) +const MIN_HOOK_TIMEOUT = 60; // Path to Claude settings.json const CLAUDE_SETTINGS_PATH = path.join(os.homedir(), '.claude', 'settings.json'); @@ -221,7 +224,7 @@ let opencodeCliCache: OpenCodeCliStatus | null = null; /** * Check if OpenCode CLI is installed globally * - * OpenCode provides built-in web search via opencode/gpt-5-nano model. + * OpenCode provides built-in web search via opencode/grok-code model. * Install: curl -fsSL https://opencode.ai/install | bash * * @returns OpenCode CLI status with path and version @@ -482,9 +485,27 @@ export function hasWebSearchHook(): boolean { /** * Get WebSearch hook configuration for settings.json + * Timeout is computed from max provider timeout in config.yaml + buffer */ export function getWebSearchHookConfig(): Record { const hookPath = path.join(CCS_HOOKS_DIR, WEBSEARCH_HOOK); + const wsConfig = getWebSearchConfig(); + + // Compute max timeout from enabled providers + const timeouts: number[] = []; + if (wsConfig.providers?.gemini?.enabled && wsConfig.providers.gemini.timeout) { + timeouts.push(wsConfig.providers.gemini.timeout); + } + if (wsConfig.providers?.opencode?.enabled && wsConfig.providers.opencode.timeout) { + timeouts.push(wsConfig.providers.opencode.timeout); + } + if (wsConfig.providers?.grok?.enabled && wsConfig.providers.grok.timeout) { + timeouts.push(wsConfig.providers.grok.timeout); + } + + // Hook timeout = max provider timeout + buffer (or minimum if none configured) + const maxProviderTimeout = timeouts.length > 0 ? Math.max(...timeouts) : MIN_HOOK_TIMEOUT; + const hookTimeout = maxProviderTimeout + HOOK_TIMEOUT_BUFFER; return { PreToolUse: [ @@ -494,7 +515,7 @@ export function getWebSearchHookConfig(): Record { { type: 'command', command: `node "${hookPath}"`, - timeout: HOOK_TIMEOUT_SECONDS, + timeout: hookTimeout, }, ], }, @@ -538,16 +559,30 @@ function ensureHookConfig(): boolean { }); if (webSearchHookIndex !== -1) { - // Hook exists - check if it needs updating (different command path) + // Hook exists - check if it needs updating (different command path or timeout) const existingHook = hooks.PreToolUse[webSearchHookIndex] as Record; const existingHooks = existingHook.hooks as Array>; + const currentHookConfig = getWebSearchHookConfig(); + const expectedHooks = (currentHookConfig.PreToolUse as Array>)[0] + .hooks as Array>; + const expectedTimeout = expectedHooks[0].timeout as number; + + let needsUpdate = false; if (existingHooks?.[0]?.command !== expectedCommand) { - // Update the hook command to point to new file existingHooks[0].command = expectedCommand; + needsUpdate = true; + } + + if (existingHooks?.[0]?.timeout !== expectedTimeout) { + existingHooks[0].timeout = expectedTimeout; + needsUpdate = true; + } + + if (needsUpdate) { fs.writeFileSync(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2), 'utf8'); if (process.env.CCS_DEBUG) { - console.error(info('Updated WebSearch hook command in settings.json')); + console.error(info('Updated WebSearch hook config in settings.json')); } } return true; @@ -629,7 +664,7 @@ export function getWebSearchHookEnv(): Record { } // Use opencode timeout if no gemini timeout set if (!env.CCS_WEBSEARCH_TIMEOUT) { - env.CCS_WEBSEARCH_TIMEOUT = String(wsConfig.providers.opencode.timeout || 60); + env.CCS_WEBSEARCH_TIMEOUT = String(wsConfig.providers.opencode.timeout || 90); } } diff --git a/src/web-server/routes.ts b/src/web-server/routes.ts index 93328eca..9d54701b 100644 --- a/src/web-server/routes.ts +++ b/src/web-server/routes.ts @@ -1498,7 +1498,7 @@ apiRoutes.put('/websearch', (req: Request, res: Response): void => { model: providers.opencode?.model ?? existingConfig.websearch?.providers?.opencode?.model ?? - 'opencode/gpt-5-nano', + 'opencode/grok-code', timeout: providers.opencode?.timeout ?? existingConfig.websearch?.providers?.opencode?.timeout ??