diff --git a/src/cli/App.tsx b/src/cli/App.tsx index ebd571c..7966155 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -680,11 +680,17 @@ export default function App({ initialInput: Array, ): Promise => { const currentInput = initialInput; + // Track lastRunId outside the while loop so it's available in catch block + let lastKnownRunId: string | null = null; try { setStreaming(true); abortControllerRef.current = new AbortController(); + // Clear any stale pending tool calls from previous turns + // If we're sending a new message, old pending state is no longer relevant + markIncompleteToolsAsCancelled(buffersRef.current); + while (true) { // Stream one turn const stream = await sendMessageStream(agentId, currentInput); @@ -696,6 +702,11 @@ export default function App({ abortControllerRef.current?.signal, ); + // Update lastKnownRunId for error handling in catch block + if (lastRunId) { + lastKnownRunId = lastRunId; + } + // Track API duration sessionStatsRef.current.endTurn(apiDurationMs); sessionStatsRef.current.updateUsageFromBuffers(buffersRef.current); @@ -902,8 +913,13 @@ export default function App({ // Mark incomplete tool calls as finished to prevent stuck blinking UI markIncompleteToolsAsCancelled(buffersRef.current); + // Build run info suffix for debugging + const runInfoSuffix = lastRunId + ? `\n(run_id: ${lastRunId}, stop_reason: ${stopReason})` + : `\n(stop_reason: ${stopReason})`; + // Fetch error details from the run if available - let errorDetails = `Unexpected stop reason: ${stopReason}`; + let errorDetails = `An error occurred during agent execution`; if (lastRunId) { try { const client = await getClient(); @@ -924,31 +940,43 @@ export default function App({ } catch (_e) { // If we can't fetch error details, let user know appendError( - `${errorDetails}\n(Unable to fetch additional error details from server)`, + `${errorDetails}${runInfoSuffix}\n(Unable to fetch additional error details from server)`, ); return; } } - appendError(errorDetails); + appendError(`${errorDetails}${runInfoSuffix}`); setStreaming(false); refreshDerived(); return; } } catch (e) { + // Mark incomplete tool calls as cancelled to prevent stuck blinking UI + markIncompleteToolsAsCancelled(buffersRef.current); + + // Build error message with run_id for debugging + const runIdSuffix = lastKnownRunId + ? `\n(run_id: ${lastKnownRunId}, stop_reason: error)` + : ""; + // Handle APIError from streaming (event: error) if (e instanceof APIError && e.error?.error) { const { type, message, detail } = e.error.error; const errorType = type ? `[${type}] ` : ""; const errorMessage = message || "An error occurred"; const errorDetail = detail ? `:\n${detail}` : ""; - appendError(`${errorType}${errorMessage}${errorDetail}`); + appendError( + `${errorType}${errorMessage}${errorDetail}${runIdSuffix}`, + ); } else { // Fallback for non-API errors - appendError(e instanceof Error ? e.message : String(e)); + const errorMessage = e instanceof Error ? e.message : String(e); + appendError(`${errorMessage}${runIdSuffix}`); } setStreaming(false); + refreshDerived(); } finally { abortControllerRef.current = null; } diff --git a/src/headless.ts b/src/headless.ts index fd29071..8292ec8 100644 --- a/src/headless.ts +++ b/src/headless.ts @@ -13,7 +13,11 @@ import { createAgent } from "./agent/create"; import { sendMessageStream } from "./agent/message"; import { getModelUpdateArgs } from "./agent/model"; import { SessionStats } from "./agent/stats"; -import { createBuffers, toLines } from "./cli/helpers/accumulator"; +import { + createBuffers, + markIncompleteToolsAsCancelled, + toLines, +} from "./cli/helpers/accumulator"; import { safeJsonParseOr } from "./cli/helpers/safeJsonParse"; import { drainStreamWithResume } from "./cli/helpers/stream"; import { settingsManager } from "./settings-manager"; @@ -424,6 +428,9 @@ export async function handleHeadlessCommand( }, ]; + // Track lastRunId outside the while loop so it's available in catch block + let lastKnownRunId: string | null = null; + try { while (true) { const stream = await sendMessageStream(agent.id, currentInput); @@ -643,6 +650,7 @@ export async function handleHeadlessCommand( apiDurationMs = performance.now() - startTime; // Use the last run_id we saw (if any) lastRunId = runIds.size > 0 ? Array.from(runIds).pop() || null : null; + if (lastRunId) lastKnownRunId = lastRunId; // Mark final line as finished const { markCurrentLineAsFinished } = await import( @@ -660,6 +668,7 @@ export async function handleHeadlessCommand( approvals = result.approvals || []; apiDurationMs = result.apiDurationMs; lastRunId = result.lastRunId || null; + if (lastRunId) lastKnownRunId = lastRunId; } // Track API duration for this stream @@ -772,6 +781,9 @@ export async function handleHeadlessCommand( } // Unexpected stop reason (error, llm_api_error, etc.) + // Mark incomplete tool calls as cancelled to prevent stuck state + markIncompleteToolsAsCancelled(buffers); + // Extract error details from buffers if available const errorLines = toLines(buffers).filter( (line) => line.kind === "error", @@ -813,24 +825,39 @@ export async function handleHeadlessCommand( type: "error", message: errorMessage, stop_reason: stopReason, + run_id: lastRunId, }), ); } else { - console.error(errorMessage); + // Include run_id and stop_reason for debugging + const runInfoSuffix = lastRunId + ? ` (run_id: ${lastRunId}, stop_reason: ${stopReason})` + : ` (stop_reason: ${stopReason})`; + console.error(`${errorMessage}${runInfoSuffix}`); } process.exit(1); } } catch (error) { + // Mark incomplete tool calls as cancelled + markIncompleteToolsAsCancelled(buffers); + + // Build run info suffix for debugging + const runInfoSuffix = lastKnownRunId + ? ` (run_id: ${lastKnownRunId}, stop_reason: error)` + : ""; + // Handle APIError from streaming (event: error) if (error instanceof APIError && error.error?.error) { const { type, message, detail } = error.error.error; const errorType = type ? `[${type}] ` : ""; const errorMessage = message || "An error occurred"; const errorDetail = detail ? `: ${detail}` : ""; - console.error(`Error: ${errorType}${errorMessage}${errorDetail}`); + console.error( + `Error: ${errorType}${errorMessage}${errorDetail}${runInfoSuffix}`, + ); } else { // Fallback for non-API errors - console.error(`Error: ${error}`); + console.error(`Error: ${error}${runInfoSuffix}`); } process.exit(1); }