diff --git a/src/cli/stdin-reader.ts b/src/cli/stdin-reader.ts index 8aeb4c77..04173de8 100644 --- a/src/cli/stdin-reader.ts +++ b/src/cli/stdin-reader.ts @@ -1,10 +1,12 @@ // Stdin reading utility extracted from hook patterns // See src/hooks/save-hook.ts for the original pattern -// Timeout for stdin reading - if Claude Code doesn't close stdin within this time, +// Inactivity timeout for stdin reading - if no data arrives within this time, // we parse whatever data we have. This fixes issue #727 where hooks hang at "1/2 done" // because stdin.on('end') never fires. -const STDIN_TIMEOUT_MS = 5000; +// Using inactivity timeout (reset on each data chunk) instead of absolute timeout +// to avoid truncating large/slow payloads. +const STDIN_INACTIVITY_TIMEOUT_MS = 5000; /** * Check if stdin is available and readable. @@ -24,11 +26,13 @@ function isStdinAvailable(): boolean { return false; } - // Check if we can access basic stdin properties without crashing - // This triggers Bun's lazy initialization - const readable = stdin.readable; - return readable !== false; - } catch (err) { + // Accessing stdin.readable triggers Bun's lazy initialization. + // If we get here without throwing, stdin is available. + // Note: We don't check the value since Node/Bun don't reliably set it to false. + // eslint-disable-next-line @typescript-eslint/no-unused-expressions + stdin.readable; + return true; + } catch { // Bun crashed trying to access stdin (EINVAL from fstat) // This is expected when Claude Code doesn't provide valid stdin return false; @@ -45,6 +49,7 @@ export async function readJsonFromStdin(): Promise { return new Promise((resolve, reject) => { let input = ''; let resolved = false; + let timeoutId: ReturnType; const cleanup = () => { try { @@ -59,6 +64,7 @@ export async function readJsonFromStdin(): Promise { const resolveWithData = () => { if (resolved) return; resolved = true; + clearTimeout(timeoutId); cleanup(); try { resolve(input.trim() ? JSON.parse(input) : undefined); @@ -67,25 +73,31 @@ export async function readJsonFromStdin(): Promise { } }; - // Timeout handler - resolve with whatever data we have - // This fixes issue #727 where stdin.on('end') never fires - const timeoutId = setTimeout(() => { - if (!resolved) { - resolveWithData(); - } - }, STDIN_TIMEOUT_MS); + // Reset the inactivity timeout - called on each data chunk + const resetTimeout = () => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + if (!resolved) { + resolveWithData(); + } + }, STDIN_INACTIVITY_TIMEOUT_MS); + }; + + // Start initial timeout + resetTimeout(); try { process.stdin.on('data', (chunk) => { input += chunk; + // Reset timeout on each data chunk to avoid truncating large/slow payloads + resetTimeout(); }); process.stdin.on('end', () => { - clearTimeout(timeoutId); resolveWithData(); }); - process.stdin.on('error', (err) => { + process.stdin.on('error', () => { if (resolved) return; resolved = true; clearTimeout(timeoutId); @@ -94,9 +106,11 @@ export async function readJsonFromStdin(): Promise { // This is more graceful for hook execution resolve(undefined); }); - } catch (err) { + } catch { // If attaching listeners fails (Bun stdin issue), resolve with undefined + resolved = true; clearTimeout(timeoutId); + cleanup(); resolve(undefined); } });