Skip to content

Commit 6d42274

Browse files
mabry1985Automakerclaude
authored
fix(cli): cancel must stop the tool-result feedback loop (#152)
* fix(recap): skip when there's no LLM conversation to summarize Reported: the recap card fires after slash commands like /commit and the model has nothing to summarize, so it hallucinates output like "I don't have access to any previous conversation — this appears to be the start of our session." Cause: useRecap watches streamingState transitions and counts UI-level tool_group entries to decide if a turn was tool-heavy. Slash commands do flip streamingState and do produce many tool_group entries, but they bypass GeminiChat — `geminiClient.getHistory()` is empty when the recap fires. generateRecap then ships its prompt with no prior context and the model can only hallucinate. Fix: gate the recap on the conversation containing at least one model-role AND one user-role entry. Slash-command-only turns short-circuit before the LLM call. Real LLM turns (single or multi-turn) still recap as before. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(cli): cancel must stop the tool-result feedback loop The earlier fix unblocked streamingState by force-cancelling stuck toolCalls in the UI display state, but it missed the upstream cause of why submitQuery kept dropping new prompts: handleCompletedTools was still feeding tool results back to the model even after the user cancelled. Repro from Langfuse session d795ff07 at 01:11:47Z: - User starts a turn. Turn span opens (4.4s) - User presses Esc, cancelOngoingRequest fires: - abort signal sent - turnCancelledRef = true - endTurnSpan('ok') closes the turn span - But a shell command was already in-flight. It runs another 11.4s and finishes with status='success' (didn't honor abort) - handleCompletedTools fires for the completed shell. The existing isResponding guard passes (cancel set it false). The function decides this is a legitimate gemini-bound tool result and calls submitQuery(SendMessageType.ToolResult, ...) - submitQuery(ToolResult) at line 1341: abortControllerRef.current = abortController; // fresh, not aborted turnCancelledRef.current = false; // cancel undone - Loop continues. New LLM call (3.9s) at 01:12:03, then another (1.2s) at 01:12:07 — all OUTSIDE any turn span (orphan), and the streamingState ping-pongs between Idle and Responding. - Every user-typed prompt during this cascade hits submitQuery's silent-drop guard at line 1305 because streamingState !== Idle at the moment of submission. Net symptom: "any time Request Cancelled I cant restart" — exactly the user's report. Fix: handleCompletedTools, immediately after the isResponding guard, checks turnCancelledRef.current. If the user already cancelled, it marks the completed tools as submitted (so streamingState clears) but does NOT call submitQuery. The loop ends where the user wanted it to. Tools that finished after the abort signal but before cleanup have their results discarded — the desired behavior for an explicit cancel. Tests: 3,767 cli pass; 49 cancellation tests in useGeminiStream.test.tsx remain green. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Automaker <automaker@localhost> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c11682d commit 6d42274

1 file changed

Lines changed: 27 additions & 0 deletions

File tree

packages/cli/src/ui/hooks/useGeminiStream.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1799,6 +1799,33 @@ export const useGeminiStream = (
17991799
return;
18001800
}
18011801

1802+
// If the user already pressed Esc, do NOT feed tool results back to
1803+
// the model. Some tools (long shells, slow subagents) finish with
1804+
// status='success' even after the abort signal because they didn't
1805+
// honor it before completing. submitQuery(ToolResult) below would
1806+
// then create a fresh AbortController and reset turnCancelledRef
1807+
// (line 1343), undoing the cancel. Net effect: cancel briefly stops
1808+
// things, then the loop resumes for as long as tool results keep
1809+
// landing — turn span stays closed but new LLM calls fire orphan,
1810+
// and streamingState ping-pongs Idle/Responding so user input gets
1811+
// dropped by the silent guard in submitQuery.
1812+
// Just mark the completed tools as submitted (so streamingState
1813+
// clears) and bail.
1814+
if (turnCancelledRef.current) {
1815+
const terminalCallIds = completedToolCallsFromScheduler
1816+
.filter(
1817+
(tc) =>
1818+
tc.status === 'success' ||
1819+
tc.status === 'error' ||
1820+
tc.status === 'cancelled',
1821+
)
1822+
.map((tc) => tc.request.callId);
1823+
if (terminalCallIds.length > 0) {
1824+
markToolsAsSubmitted(terminalCallIds);
1825+
}
1826+
return;
1827+
}
1828+
18021829
const completedAndReadyToSubmitTools =
18031830
completedToolCallsFromScheduler.filter(
18041831
(

0 commit comments

Comments
 (0)