From f5a1a5e400e75c0bd8e9af98bcd992499ee991ea Mon Sep 17 00:00:00 2001 From: Charles Packer Date: Mon, 29 Dec 2025 20:12:03 -0800 Subject: [PATCH] fix: prevent infinite loop on ESC interrupt (#425) Co-authored-by: Letta --- src/cli/App.tsx | 7 ------- src/index.ts | 16 ++++++++++++---- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/cli/App.tsx b/src/cli/App.tsx index e5b0100..aa531e5 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -1911,13 +1911,6 @@ export default function App({ processConversationRef.current = processConversation; }, [processConversation]); - // Reset interrupt flag when streaming ends - useEffect(() => { - if (!streaming) { - setInterruptRequested(false); - } - }, [streaming]); - const handleAgentSelect = useCallback( async (targetAgentId: string, _opts?: { profileName?: string }) => { // Close selector immediately diff --git a/src/index.ts b/src/index.ts index 538f49d..d4d2e96 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,11 +1,13 @@ #!/usr/bin/env bun import { parseArgs } from "node:util"; import type { AgentState } from "@letta-ai/letta-client/resources/agents/agents"; +import type { Message } from "@letta-ai/letta-client/resources/agents/messages"; import { getResumeData, type ResumeData } from "./agent/check-approval"; import { getClient } from "./agent/client"; import { initializeLoadedSkillsFlag, setAgentContext } from "./agent/context"; import type { AgentProvenance } from "./agent/create"; import { LETTA_CLOUD_API_URL } from "./auth/oauth"; +import type { ApprovalRequest } from "./cli/helpers/stream"; import { ProfileSelectionInline } from "./cli/profile-selection"; import { permissionMode } from "./permissions/mode"; import { settingsManager } from "./settings-manager"; @@ -17,6 +19,12 @@ import { upsertToolsIfNeeded, } from "./tools/manager"; +// Stable empty array constants to prevent new references on every render +// These are used as fallbacks when resumeData is null, avoiding the React +// anti-pattern of creating new [] on every render which triggers useEffect re-runs +const EMPTY_APPROVAL_ARRAY: ApprovalRequest[] = []; +const EMPTY_MESSAGE_ARRAY: Message[] = []; + function printHelp() { // Keep this plaintext (no colors) so output pipes cleanly const usage = ` @@ -1342,8 +1350,8 @@ async function main(): Promise { loadingState, continueSession: isResumingSession, startupApproval: resumeData?.pendingApproval ?? null, - startupApprovals: resumeData?.pendingApprovals ?? [], - messageHistory: resumeData?.messageHistory ?? [], + startupApprovals: resumeData?.pendingApprovals ?? EMPTY_APPROVAL_ARRAY, + messageHistory: resumeData?.messageHistory ?? EMPTY_MESSAGE_ARRAY, tokenStreaming: settings.tokenStreaming, agentProvenance, }); @@ -1355,8 +1363,8 @@ async function main(): Promise { loadingState, continueSession: isResumingSession, startupApproval: resumeData?.pendingApproval ?? null, - startupApprovals: resumeData?.pendingApprovals ?? [], - messageHistory: resumeData?.messageHistory ?? [], + startupApprovals: resumeData?.pendingApprovals ?? EMPTY_APPROVAL_ARRAY, + messageHistory: resumeData?.messageHistory ?? EMPTY_MESSAGE_ARRAY, tokenStreaming: settings.tokenStreaming, agentProvenance, });