From cbc9f8dce95a28c4a244aeecafb68cf2df93cbc2 Mon Sep 17 00:00:00 2001 From: Charles Packer Date: Fri, 16 Jan 2026 14:17:35 -0800 Subject: [PATCH] feat: simplify startup flow with auto-select Memo for fresh users (#568) Co-authored-by: Letta --- src/agent/defaults.ts | 9 +- src/headless.ts | 78 +++++--- src/index.ts | 315 ++++++++++++++++++++++++------ src/tests/startup-flow.test.ts | 338 +++++++++++++++++++++++++++++++++ 4 files changed, 652 insertions(+), 88 deletions(-) create mode 100644 src/tests/startup-flow.test.ts diff --git a/src/agent/defaults.ts b/src/agent/defaults.ts index 7f08ef3..57facae 100644 --- a/src/agent/defaults.ts +++ b/src/agent/defaults.ts @@ -13,8 +13,8 @@ import { parseMdxFrontmatter } from "./memory"; import { MEMORY_PROMPTS } from "./promptAssets"; // Tags used to identify default agents -const MEMO_TAG = "default:memo"; -const INCOGNITO_TAG = "default:incognito"; +export const MEMO_TAG = "default:memo"; +export const INCOGNITO_TAG = "default:incognito"; // Memo's persona - loaded from persona_memo.mdx const MEMO_PERSONA = parseMdxFrontmatter( @@ -125,8 +125,9 @@ export async function ensureDefaultAgents( settingsManager.pinGlobal(agent.id); } } catch (err) { - console.warn( - `Warning: Failed to ensure default agents: ${err instanceof Error ? err.message : String(err)}`, + // Re-throw so caller can handle/exit appropriately + throw new Error( + `Failed to create default agents: ${err instanceof Error ? err.message : String(err)}`, ); } diff --git a/src/headless.ts b/src/headless.ts index c09fe94..759b53e 100644 --- a/src/headless.ts +++ b/src/headless.ts @@ -206,6 +206,26 @@ export async function handleHeadlessCommand( const sleeptimeFlag = (values.sleeptime as boolean | undefined) ?? undefined; const fromAfFile = values["from-af"] as string | undefined; + // Validate --conversation flag (mutually exclusive with agent-selection flags) + if (specifiedConversationId) { + if (specifiedAgentId) { + console.error("Error: --conversation cannot be used with --agent"); + process.exit(1); + } + if (forceNew) { + console.error("Error: --conversation cannot be used with --new-agent"); + process.exit(1); + } + if (fromAfFile) { + console.error("Error: --conversation cannot be used with --from-af"); + process.exit(1); + } + if (shouldContinue) { + console.error("Error: --conversation cannot be used with --continue"); + process.exit(1); + } + } + // Validate --from-af flag if (fromAfFile) { if (specifiedAgentId) { @@ -340,8 +360,21 @@ export async function handleHeadlessCommand( } } + // Priority 0: --conversation derives agent from conversation ID + if (specifiedConversationId) { + try { + const conversation = await client.conversations.retrieve( + specifiedConversationId, + ); + agent = await client.agents.retrieve(conversation.agent_id); + } catch (_error) { + console.error(`Conversation ${specifiedConversationId} not found`); + process.exit(1); + } + } + // Priority 1: Import from AgentFile template - if (fromAfFile) { + if (!agent && fromAfFile) { const { importAgentFromFile } = await import("./agent/import"); const result = await importAgentFromFile({ filePath: fromAfFile, @@ -356,7 +389,8 @@ export async function handleHeadlessCommand( try { agent = await client.agents.retrieve(specifiedAgentId); } catch (_error) { - console.error(`Agent ${specifiedAgentId} not found, creating new one...`); + console.error(`Agent ${specifiedAgentId} not found`); + process.exit(1); } } @@ -389,38 +423,36 @@ export async function handleHeadlessCommand( try { agent = await client.agents.retrieve(localProjectSettings.lastAgent); } catch (_error) { + // Local LRU agent doesn't exist - log and continue console.error( - `Project agent ${localProjectSettings.lastAgent} not found, creating new one...`, + `Unable to locate agent ${localProjectSettings.lastAgent} in .letta/`, ); } } } // Priority 5: Try to reuse global lastAgent if --continue flag is passed - if (!agent && shouldContinue && settings.lastAgent) { - try { - agent = await client.agents.retrieve(settings.lastAgent); - } catch (_error) { - console.error( - `Previous agent ${settings.lastAgent} not found, creating new one...`, - ); + if (!agent && shouldContinue) { + if (settings.lastAgent) { + try { + agent = await client.agents.retrieve(settings.lastAgent); + } catch (_error) { + // Global LRU agent doesn't exist + } + } + // --continue requires an LRU agent to exist + if (!agent) { + console.error("No recent session found in .letta/ or ~/.letta."); + console.error("Run 'letta' to get started."); + process.exit(1); } } - // Priority 6: Create a new agent + // All paths should have resolved to an agent by now + // If not, it's an unexpected state - error out instead of auto-creating if (!agent) { - const updateArgs = getModelUpdateArgs(model); - const createOptions = { - model, - updateArgs, - skillsDirectory, - parallelToolCalls: true, - enableSleeptime: sleeptimeFlag ?? settings.enableSleeptime, - systemPromptPreset, - // Note: systemCustom, systemAppend, and memoryBlocks only apply with --new flag - }; - const result = await createAgent(createOptions); - agent = result.agent; + console.error("No agent found. Use --new-agent to create a new agent."); + process.exit(1); } // Check if we're resuming an existing agent (not creating a new one) diff --git a/src/index.ts b/src/index.ts index 0f2376d..f2ec2f8 100755 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ 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 { INCOGNITO_TAG, MEMO_TAG } from "./agent/defaults"; import { ensureSkillsBlocks, ISOLATED_BLOCK_LABELS } from "./agent/memory"; import { LETTA_CLOUD_API_URL } from "./auth/oauth"; import { ConversationSelector } from "./cli/components/ConversationSelector"; @@ -23,6 +24,37 @@ import { loadTools } from "./tools/manager"; const EMPTY_APPROVAL_ARRAY: ApprovalRequest[] = []; const EMPTY_MESSAGE_ARRAY: Message[] = []; +/** + * Check if pinned agents consist only of default agents (Memo + Incognito). + * Used to auto-select Memo for fresh users without showing a selector. + */ +async function hasOnlyDefaultAgents( + pinnedIds: string[], +): Promise<{ onlyDefaults: boolean; memoId: string | null }> { + if (pinnedIds.length === 0) return { onlyDefaults: true, memoId: null }; + if (pinnedIds.length > 2) return { onlyDefaults: false, memoId: null }; + + const client = await getClient(); + let memoId: string | null = null; + + for (const id of pinnedIds) { + try { + const agent = await client.agents.retrieve(id); + const tags = agent.tags || []; + if (tags.includes(MEMO_TAG)) { + memoId = agent.id; + } else if (!tags.includes(INCOGNITO_TAG)) { + // Found a non-default agent + return { onlyDefaults: false, memoId: null }; + } + } catch { + // Agent doesn't exist, skip it + } + } + + return { onlyDefaults: true, memoId }; +} + function printHelp() { // Keep this plaintext (no colors) so output pipes cleanly const usage = ` @@ -576,6 +608,34 @@ async function main(): Promise { } } + // Validate --conversation flag (mutually exclusive with agent-selection flags) + if (specifiedConversationId) { + if (specifiedAgentId) { + console.error("Error: --conversation cannot be used with --agent"); + process.exit(1); + } + if (specifiedAgentName) { + console.error("Error: --conversation cannot be used with --name"); + process.exit(1); + } + if (forceNew) { + console.error("Error: --conversation cannot be used with --new-agent"); + process.exit(1); + } + if (fromAfFile) { + console.error("Error: --conversation cannot be used with --from-af"); + process.exit(1); + } + if (shouldResume) { + console.error("Error: --conversation cannot be used with --resume"); + process.exit(1); + } + if (shouldContinue) { + console.error("Error: --conversation cannot be used with --continue"); + process.exit(1); + } + } + // Validate --from-af flag if (fromAfFile) { if (specifiedAgentId) { @@ -936,70 +996,206 @@ async function main(): Promise { await settingsManager.loadLocalProjectSettings(); const localSettings = settingsManager.getLocalProjectSettings(); let globalPinned = settingsManager.getGlobalPinnedAgents(); - - // Check if user would see selector (fresh dir, no bypass flags) - const wouldShowSelector = - !localSettings.lastAgent && - !forceNew && - !agentIdArg && - !fromAfFile && - !continueSession; - - // Ensure default agents (Memo/Incognito) exist for all users const client = await getClient(); - const { ensureDefaultAgents } = await import("./agent/defaults"); - if (wouldShowSelector && globalPinned.length === 0) { - // New user with no agents - block and show loading while creating defaults - setLoadingState("assembling"); + // ===================================================================== + // TOP-LEVEL PATH: --conversation + // Conversation ID is unique, so we can derive the agent from it + // ===================================================================== + if (specifiedConversationId) { try { - await ensureDefaultAgents(client); - // Refresh pinned list after defaults created - globalPinned = settingsManager.getGlobalPinnedAgents(); - } catch (err) { - console.warn( - `Warning: Failed to create default agents: ${err instanceof Error ? err.message : String(err)}`, + const conversation = await client.conversations.retrieve( + specifiedConversationId, ); + // Use the agent that owns this conversation + setSelectedGlobalAgentId(conversation.agent_id); + setSelectedConversationId(specifiedConversationId); + setLoadingState("assembling"); + return; + } catch (error) { + if ( + error instanceof APIError && + (error.status === 404 || error.status === 422) + ) { + console.error( + `Conversation ${specifiedConversationId} not found`, + ); + process.exit(1); + } + throw error; } - } else { - // Existing user - fire and forget, don't block startup - ensureDefaultAgents(client).catch((err) => - console.warn( - `Warning: Failed to ensure default agents: ${err instanceof Error ? err.message : String(err)}`, - ), - ); } - // Handle --resume flag: show conversation selector directly + // ===================================================================== + // TOP-LEVEL PATH: --resume + // Show conversation selector for last-used agent (local → global fallback) + // ===================================================================== if (shouldResume) { - // Find the last-used agent for this project - const lastSession = - settingsManager.getLocalLastSession(process.cwd()) ?? - settingsManager.getGlobalLastSession(); - const lastAgentId = lastSession?.agentId ?? localSettings.lastAgent; + const localSession = settingsManager.getLocalLastSession( + process.cwd(), + ); + const localAgentId = localSession?.agentId ?? localSettings.lastAgent; - if (lastAgentId) { - // Verify agent exists + // Try local LRU first + if (localAgentId) { try { - const agent = await client.agents.retrieve(lastAgentId); - setResumeAgentId(lastAgentId); + const agent = await client.agents.retrieve(localAgentId); + setResumeAgentId(localAgentId); setResumeAgentName(agent.name ?? null); setLoadingState("selecting_conversation"); return; } catch { - // Agent doesn't exist, fall through to normal flow + // Local agent doesn't exist, try global + console.log( + `Unable to locate agent ${localAgentId} in .letta/, checking global (~/.letta)`, + ); + } + } else { + console.log("No recent agent in .letta/, using global (~/.letta)"); + } + + // Try global LRU + const globalSession = settingsManager.getGlobalLastSession(); + const globalAgentId = globalSession?.agentId; + if (globalAgentId) { + try { + const agent = await client.agents.retrieve(globalAgentId); + setResumeAgentId(globalAgentId); + setResumeAgentName(agent.name ?? null); + setLoadingState("selecting_conversation"); + return; + } catch { + // Global agent also doesn't exist } } - // No valid agent found, fall through to normal startup + + // No valid agent found anywhere + console.error("No recent session found in .letta/ or ~/.letta."); + console.error("Run 'letta' to get started."); + process.exit(1); } - // Show selector if: - // 1. No lastAgent in this project (fresh directory) - // 2. No explicit flags that bypass selection (--new, --agent, --from-af, --continue) - // 3. Has global pinned agents available - const shouldShowSelector = wouldShowSelector && globalPinned.length > 0; + // ===================================================================== + // TOP-LEVEL PATH: --continue + // Resume last session directly (local → global fallback) + // ===================================================================== + if (continueSession) { + const localSession = settingsManager.getLocalLastSession( + process.cwd(), + ); + const localAgentId = localSession?.agentId ?? localSettings.lastAgent; - if (shouldShowSelector) { + // Try local LRU first + if (localAgentId) { + try { + await client.agents.retrieve(localAgentId); + setSelectedGlobalAgentId(localAgentId); + if (localSession?.conversationId) { + setSelectedConversationId(localSession.conversationId); + } + setLoadingState("assembling"); + return; + } catch { + // Local agent doesn't exist, try global + console.log( + `Unable to locate agent ${localAgentId} in .letta/, checking global (~/.letta)`, + ); + } + } else { + console.log("No recent agent in .letta/, using global (~/.letta)"); + } + + // Try global LRU + const globalSession = settingsManager.getGlobalLastSession(); + const globalAgentId = globalSession?.agentId; + if (globalAgentId) { + try { + await client.agents.retrieve(globalAgentId); + setSelectedGlobalAgentId(globalAgentId); + if (globalSession?.conversationId) { + setSelectedConversationId(globalSession.conversationId); + } + setLoadingState("assembling"); + return; + } catch { + // Global agent also doesn't exist + } + } + + // No valid agent found anywhere + console.error("No recent session found in .letta/ or ~/.letta."); + console.error("Run 'letta' to get started."); + process.exit(1); + } + + // ===================================================================== + // DEFAULT PATH: No special flags + // Check local LRU, then selector, then defaults + // ===================================================================== + + // Check if user would see selector (fresh dir, no bypass flags) + const wouldShowSelector = + !localSettings.lastAgent && !forceNew && !agentIdArg && !fromAfFile; + + // Ensure default agents (Memo/Incognito) exist for all users + const { ensureDefaultAgents } = await import("./agent/defaults"); + + if (wouldShowSelector && globalPinned.length === 0) { + // New user with no agents - create defaults first, then trigger init + // NOTE: Don't set loadingState to "assembling" until we have the agent ID, + // otherwise init will run before we've set selectedGlobalAgentId + try { + const memoAgent = await ensureDefaultAgents(client); + // Refresh pinned list after defaults created + globalPinned = settingsManager.getGlobalPinnedAgents(); + // Auto-select Memo for fresh users + if (memoAgent) { + setSelectedGlobalAgentId(memoAgent.id); + setLoadingState("assembling"); + return; + } + // If memoAgent is null (createDefaultAgents disabled), fall through + } catch (err) { + console.error( + `Failed to create default agents: ${err instanceof Error ? err.message : String(err)}`, + ); + process.exit(1); + } + } else { + // Existing user - fire and forget, don't block startup + ensureDefaultAgents(client).catch(() => { + // Silently ignore - defaults may already exist + }); + } + + // If there's a local LRU, use it directly + if (localSettings.lastAgent) { + try { + await client.agents.retrieve(localSettings.lastAgent); + setLoadingState("assembling"); + return; + } catch { + // LRU agent doesn't exist, show message and fall through to selector + console.log( + `Unable to locate recently used agent ${localSettings.lastAgent}`, + ); + } + } + + // Check if we should show selector or auto-select Memo + if (wouldShowSelector && globalPinned.length > 0) { + // Check if only default agents are pinned + const { onlyDefaults, memoId } = + await hasOnlyDefaultAgents(globalPinned); + + if (onlyDefaults && memoId) { + // Only defaults pinned - auto-select Memo + setSelectedGlobalAgentId(memoId); + setLoadingState("assembling"); + return; + } + + // Has custom agents - show selector setLoadingState("selecting_global"); return; } @@ -1007,7 +1203,14 @@ async function main(): Promise { setLoadingState("assembling"); } checkAndStart(); - }, [forceNew, agentIdArg, fromAfFile, continueSession, shouldResume]); + }, [ + forceNew, + agentIdArg, + fromAfFile, + continueSession, + shouldResume, + specifiedConversationId, + ]); // Main initialization effect - runs after profile selection useEffect(() => { @@ -1196,23 +1399,13 @@ async function main(): Promise { } } - // Priority 7: Create a new agent + // All paths should have resolved to an agent by now + // If not, it's an unexpected state - error out instead of auto-creating if (!agent) { - const updateArgs = getModelUpdateArgs(model); - const result = await createAgent( - undefined, - model, - undefined, - updateArgs, - skillsDirectory, - true, // parallelToolCalls always enabled - sleeptimeFlag ?? settings.enableSleeptime, - systemPromptPreset, - undefined, - undefined, + console.error( + "No agent found. Use --new-agent to create a new agent.", ); - agent = result.agent; - setAgentProvenance(result.provenance); + process.exit(1); } // Ensure local project settings are loaded before updating diff --git a/src/tests/startup-flow.test.ts b/src/tests/startup-flow.test.ts new file mode 100644 index 0000000..5ba9c03 --- /dev/null +++ b/src/tests/startup-flow.test.ts @@ -0,0 +1,338 @@ +import { describe, expect, test } from "bun:test"; +import { spawn } from "node:child_process"; + +/** + * Integration tests for CLI startup flows. + * + * These tests verify the boot flow decision tree: + * - Flag conflict detection + * - --conversation: derives agent from conversation + * - --agent: uses specified agent + * - --new-agent: creates new agent + * - Error messages for invalid inputs + * + * Note: Tests that depend on settings files (.letta/) are harder to isolate + * because the CLI uses process.cwd(). For now, we focus on flag-based tests. + */ + +const projectRoot = process.cwd(); + +// Helper to run CLI and capture output +async function runCli( + args: string[], + options: { + timeoutMs?: number; + expectExit?: number; + } = {}, +): Promise<{ stdout: string; stderr: string; exitCode: number | null }> { + const { timeoutMs = 30000, expectExit } = options; + + return new Promise((resolve, reject) => { + const proc = spawn("bun", ["run", "dev", ...args], { + cwd: projectRoot, + env: { ...process.env }, + }); + + let stdout = ""; + let stderr = ""; + + proc.stdout?.on("data", (data) => { + stdout += data.toString(); + }); + + proc.stderr?.on("data", (data) => { + stderr += data.toString(); + }); + + const timeout = setTimeout(() => { + proc.kill(); + reject( + new Error( + `Timeout after ${timeoutMs}ms. stdout: ${stdout}, stderr: ${stderr}`, + ), + ); + }, timeoutMs); + + proc.on("close", (code) => { + clearTimeout(timeout); + if (expectExit !== undefined && code !== expectExit) { + reject( + new Error( + `Expected exit code ${expectExit}, got ${code}. stdout: ${stdout}, stderr: ${stderr}`, + ), + ); + } else { + resolve({ stdout, stderr, exitCode: code }); + } + }); + + proc.on("error", (err) => { + clearTimeout(timeout); + reject(err); + }); + }); +} + +// ============================================================================ +// Flag Conflict Tests (fast, no API calls needed) +// ============================================================================ + +describe("Startup Flow - Flag Conflicts", () => { + test("--conversation conflicts with --agent", async () => { + const result = await runCli( + ["--conversation", "conv-123", "--agent", "agent-123"], + { expectExit: 1 }, + ); + expect(result.stderr).toContain( + "--conversation cannot be used with --agent", + ); + }); + + test("--conversation conflicts with --new-agent", async () => { + const result = await runCli(["--conversation", "conv-123", "--new-agent"], { + expectExit: 1, + }); + expect(result.stderr).toContain( + "--conversation cannot be used with --new-agent", + ); + }); + + test("--conversation conflicts with --resume", async () => { + const result = await runCli(["--conversation", "conv-123", "--resume"], { + expectExit: 1, + }); + expect(result.stderr).toContain( + "--conversation cannot be used with --resume", + ); + }); + + test("--conversation conflicts with --continue", async () => { + const result = await runCli(["--conversation", "conv-123", "--continue"], { + expectExit: 1, + }); + expect(result.stderr).toContain( + "--conversation cannot be used with --continue", + ); + }); + + test("--conversation conflicts with --from-af", async () => { + const result = await runCli( + ["--conversation", "conv-123", "--from-af", "test.af"], + { expectExit: 1 }, + ); + expect(result.stderr).toContain( + "--conversation cannot be used with --from-af", + ); + }); + + test("--conversation conflicts with --name", async () => { + const result = await runCli( + ["--conversation", "conv-123", "--name", "MyAgent"], + { expectExit: 1 }, + ); + expect(result.stderr).toContain( + "--conversation cannot be used with --name", + ); + }); +}); + +// ============================================================================ +// Invalid Input Tests (require API calls but fail fast) +// ============================================================================ + +describe("Startup Flow - Invalid Inputs", () => { + test( + "--agent with nonexistent ID shows error", + async () => { + const result = await runCli( + ["--agent", "agent-definitely-does-not-exist-12345", "-p", "test"], + { expectExit: 1, timeoutMs: 60000 }, + ); + expect(result.stderr).toContain("not found"); + }, + { timeout: 70000 }, + ); + + test( + "--conversation with nonexistent ID shows error", + async () => { + const result = await runCli( + [ + "--conversation", + "conversation-definitely-does-not-exist-12345", + "-p", + "test", + ], + { expectExit: 1, timeoutMs: 60000 }, + ); + expect(result.stderr).toContain("not found"); + }, + { timeout: 70000 }, + ); + + test("--from-af with nonexistent file shows error", async () => { + const result = await runCli( + ["--from-af", "/nonexistent/path/agent.af", "-p", "test"], + { expectExit: 1 }, + ); + expect(result.stderr).toContain("not found"); + }); +}); + +// ============================================================================ +// Integration Tests (require API access, create real agents) +// ============================================================================ + +describe("Startup Flow - Integration", () => { + // Store created agent/conversation IDs for cleanup and reuse + let testAgentId: string | null = null; + let testConversationId: string | null = null; + + test( + "--new-agent creates agent and responds", + async () => { + const result = await runCli( + [ + "--new-agent", + "-m", + "haiku", + "-p", + "Say OK and nothing else", + "--output-format", + "json", + ], + { timeoutMs: 120000 }, + ); + + expect(result.exitCode).toBe(0); + // stdout includes the bun invocation line, extract just the JSON + const jsonStart = result.stdout.indexOf("{"); + const output = JSON.parse(result.stdout.slice(jsonStart)); + expect(output.agent_id).toBeDefined(); + expect(output.result).toBeDefined(); + + // Save for later tests + testAgentId = output.agent_id; + testConversationId = output.conversation_id; + }, + { timeout: 130000 }, + ); + + test( + "--agent with valid ID uses that agent", + async () => { + // Skip if previous test didn't create an agent + if (!testAgentId) { + console.log("Skipping: no test agent available"); + return; + } + + const result = await runCli( + [ + "--agent", + testAgentId, + "-m", + "haiku", + "-p", + "Say OK", + "--output-format", + "json", + ], + { timeoutMs: 120000 }, + ); + + expect(result.exitCode).toBe(0); + const jsonStart = result.stdout.indexOf("{"); + const output = JSON.parse(result.stdout.slice(jsonStart)); + expect(output.agent_id).toBe(testAgentId); + }, + { timeout: 130000 }, + ); + + test( + "--conversation with valid ID derives agent and uses conversation", + async () => { + // Skip if previous test didn't create an agent/conversation + if (!testAgentId || !testConversationId) { + console.log("Skipping: no test conversation available"); + return; + } + + const result = await runCli( + [ + "--conversation", + testConversationId, + "-m", + "haiku", + "-p", + "Say OK", + "--output-format", + "json", + ], + { timeoutMs: 120000 }, + ); + + expect(result.exitCode).toBe(0); + const jsonStart = result.stdout.indexOf("{"); + const output = JSON.parse(result.stdout.slice(jsonStart)); + // Should use the same agent that owns the conversation + expect(output.agent_id).toBe(testAgentId); + // Should use the specified conversation + expect(output.conversation_id).toBe(testConversationId); + }, + { timeout: 130000 }, + ); + + test( + "--new-agent with --init-blocks none creates minimal agent", + async () => { + const result = await runCli( + [ + "--new-agent", + "--init-blocks", + "none", + "-m", + "haiku", + "-p", + "Say OK", + "--output-format", + "json", + ], + { timeoutMs: 120000 }, + ); + + expect(result.exitCode).toBe(0); + // stdout includes the bun invocation line, extract just the JSON + const jsonStart = result.stdout.indexOf("{"); + const output = JSON.parse(result.stdout.slice(jsonStart)); + expect(output.agent_id).toBeDefined(); + }, + { timeout: 130000 }, + ); +}); + +// ============================================================================ +// --continue Tests (depend on LRU state, harder to isolate) +// ============================================================================ + +describe("Startup Flow - Continue Flag", () => { + test( + "--continue with no LRU shows error", + async () => { + // This test relies on running in a directory with no .letta/ settings + // In practice, this might use the project's .letta/ which has an LRU + // So we check for either success (if LRU exists) or error (if not) + const result = await runCli( + ["--continue", "-p", "Say OK", "--output-format", "json"], + { timeoutMs: 60000 }, + ); + + // Either succeeds (LRU exists) or fails with specific error + if (result.exitCode !== 0) { + expect(result.stderr).toContain("No recent session found"); + } + // If it succeeds, that's also valid (test env has LRU) + }, + { timeout: 70000 }, + ); +});