From a526614e28e7aa4fc2381056b421407db5db08e8 Mon Sep 17 00:00:00 2001 From: Charles Packer Date: Mon, 19 Jan 2026 13:02:12 -0800 Subject: [PATCH] feat: revert default startup to use "default" conversation (#594) Co-authored-by: Letta --- src/cli/App.tsx | 76 +++++++++++-------- src/cli/commands/registry.ts | 18 +---- src/headless.ts | 57 ++++++++------ src/index.ts | 68 +++++++++++------ src/release-notes.ts | 4 + src/tests/headless-input-format.test.ts | 3 +- src/tests/headless-scenario.ts | 7 +- src/tests/headless-stream-json-format.test.ts | 3 +- src/tests/headless-windows.ts | 7 +- src/tests/lazy-approval-recovery.test.ts | 3 +- src/tests/startup-flow.test.ts | 3 +- 11 files changed, 152 insertions(+), 97 deletions(-) diff --git a/src/cli/App.tsx b/src/cli/App.tsx index 985f983..4907ed0 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -1456,22 +1456,33 @@ export default function App({ ? `Resuming conversation with **${agentName}**` : `Starting new conversation with **${agentName}**`; - // Command hints - for pinned agents show /memory, for unpinned show /pin - const commandHints = isPinned + // Command hints - vary based on agent state: + // - Resuming: show /new (they may want a fresh conversation) + // - New session + unpinned: show /pin (they should save their agent) + // - New session + pinned: show /memory (they're already saved) + const commandHints = isResumingConversation ? [ "→ **/agents** list all agents", - "→ **/resume** resume a previous conversation", - "→ **/memory** view your agent's memory blocks", + "→ **/resume** browse all conversations", + "→ **/new** start a new conversation", "→ **/init** initialize your agent's memory", "→ **/remember** teach your agent", ] - : [ - "→ **/agents** list all agents", - "→ **/resume** resume a previous conversation", - "→ **/pin** save + name your agent", - "→ **/init** initialize your agent's memory", - "→ **/remember** teach your agent", - ]; + : isPinned + ? [ + "→ **/agents** list all agents", + "→ **/resume** resume a previous conversation", + "→ **/memory** view your agent's memory blocks", + "→ **/init** initialize your agent's memory", + "→ **/remember** teach your agent", + ] + : [ + "→ **/agents** list all agents", + "→ **/resume** resume a previous conversation", + "→ **/pin** save + name your agent", + "→ **/init** initialize your agent's memory", + "→ **/remember** teach your agent", + ]; // Build status lines with optional release notes above header const statusLines: string[] = []; @@ -3185,13 +3196,9 @@ export default function App({ // Fetch new agent const agent = await client.agents.retrieve(targetAgentId); - // Always create a new conversation when switching agents - // User can /resume to get back to a previous conversation if needed - const newConversation = await client.conversations.create({ - agent_id: targetAgentId, - isolated_block_labels: [...ISOLATED_BLOCK_LABELS], - }); - const targetConversationId = newConversation.id; + // Use the agent's default conversation when switching agents + // User can /new to start a fresh conversation if needed + const targetConversationId = "default"; // Update project settings with new agent await updateProjectSettings({ lastAgent: targetAgentId }); @@ -3225,11 +3232,12 @@ export default function App({ setLlmConfig(agent.llm_config); setConversationId(targetConversationId); - // Build success message - always a new conversation + // Build success message - resumed default conversation const agentLabel = agent.name || targetAgentId; const successOutput = [ - `Started a new conversation with **${agentLabel}**.`, - `⎿ Type /resume to resume a previous conversation`, + `Resumed the default conversation with **${agentLabel}**.`, + `⎿ Type /resume to browse all conversations`, + `⎿ Type /new to start a new conversation`, ].join("\n"); const successItem: StaticItem = { kind: "command", @@ -4270,9 +4278,8 @@ export default function App({ return { submitted: true }; } - // Special handling for /clear and /new commands - start new conversation - // (/new used to create a new agent, now it's just an alias for /clear) - if (msg.trim() === "/clear" || msg.trim() === "/new") { + // Special handling for /new command - start new conversation + if (msg.trim() === "/new") { const cmdId = uid("cmd"); buffersRef.current.byId.set(cmdId, { kind: "command", @@ -4339,14 +4346,14 @@ export default function App({ return { submitted: true }; } - // Special handling for /clear-messages command - reset all agent messages (destructive) - if (msg.trim() === "/clear-messages") { + // Special handling for /clear command - reset all agent messages (destructive) + if (msg.trim() === "/clear") { const cmdId = uid("cmd"); buffersRef.current.byId.set(cmdId, { kind: "command", id: cmdId, input: msg, - output: "Resetting agent messages...", + output: "Clearing in-context messages...", phase: "running", }); buffersRef.current.order.push(cmdId); @@ -7917,11 +7924,16 @@ Plan file path: ${planFilePath}`; ? `letta -n "${agentName}"` : `letta --agent ${agentId}`} - - Resume this conversation with: - - {`letta --conv ${conversationId}`} - + {/* Only show conversation hint if not on default (default is resumed automatically) */} + {conversationId !== "default" && ( + <> + + Resume this conversation with: + + {`letta --conv ${conversationId}`} + + + )} )} diff --git a/src/cli/commands/registry.ts b/src/cli/commands/registry.ts index ac373c4..0dc44fd 100644 --- a/src/cli/commands/registry.ts +++ b/src/cli/commands/registry.ts @@ -77,29 +77,20 @@ export const commands: Record = { }, }, "/clear": { - desc: "Start a new conversation (keep agent memory)", + desc: "Clear in-context messages", order: 18, - handler: () => { - // Handled specially in App.tsx to create new conversation - return "Starting new conversation..."; - }, - }, - "/clear-messages": { - desc: "Reset all agent messages (destructive)", - order: 19, - hidden: true, // Advanced command, not shown in autocomplete handler: () => { // Handled specially in App.tsx to reset agent messages - return "Resetting agent messages..."; + return "Clearing in-context messages..."; }, }, // === Page 2: Agent management (order 20-29) === "/new": { - desc: "Start a new conversation (same as /clear)", + desc: "Start a new conversation (keep agent memory)", order: 20, handler: () => { - // Handled specially in App.tsx - same as /clear + // Handled specially in App.tsx to create new conversation return "Starting new conversation..."; }, }, @@ -334,7 +325,6 @@ export const commands: Record = { }, "/compact": { desc: "Summarize conversation history (compaction)", - hidden: true, handler: () => { // Handled specially in App.tsx to access client and agent ID return "Compacting conversation..."; diff --git a/src/headless.ts b/src/headless.ts index 4dcbbf4..7a0a0d4 100644 --- a/src/headless.ts +++ b/src/headless.ts @@ -191,14 +191,8 @@ export async function handleHeadlessCommand( process.exit(1); } - // Check for deprecated --new flag - if (values.new) { - console.error( - "Error: --new has been renamed to --new-agent\n" + - 'Usage: letta -p "..." --new-agent', - ); - process.exit(1); - } + // --new: Create a new conversation (for concurrent sessions) + const forceNewConversation = (values.new as boolean | undefined) ?? false; // Resolve agent (same logic as interactive mode) let agent: AgentState | null = null; @@ -269,6 +263,18 @@ export async function handleHeadlessCommand( } } + // Validate --new flag (create new conversation) + if (forceNewConversation) { + if (shouldContinue) { + console.error("Error: --new cannot be used with --continue"); + process.exit(1); + } + if (specifiedConversationId) { + console.error("Error: --new cannot be used with --conversation"); + process.exit(1); + } + } + // Validate --from-af flag if (fromAfFile) { if (specifiedAgentId) { @@ -617,30 +623,35 @@ export async function handleHeadlessCommand( await client.conversations.retrieve(lastSession.conversationId); conversationId = lastSession.conversationId; } catch { - // Conversation no longer exists, create new - const conversation = await client.conversations.create({ - agent_id: agent.id, - isolated_block_labels: isolatedBlockLabels, - }); - conversationId = conversation.id; + // Conversation no longer exists - error with helpful message + console.error( + `Attempting to resume conversation ${lastSession.conversationId}, but conversation was not found.`, + ); + console.error( + "Resume the default conversation with 'letta -p ...', view recent conversations with 'letta --resume', or start a new conversation with 'letta -p ... --new'.", + ); + process.exit(1); } } } else { - // No matching session, create new conversation - const conversation = await client.conversations.create({ - agent_id: agent.id, - isolated_block_labels: isolatedBlockLabels, - }); - conversationId = conversation.id; + // No matching session - error with helpful message + console.error("No previous session found for this agent to resume."); + console.error( + "Resume the default conversation with 'letta -p ...', or start a new conversation with 'letta -p ... --new'.", + ); + process.exit(1); } - } else { - // Default: create a new conversation - // This ensures isolated message history per CLI invocation + } else if (forceNewConversation || forceNew) { + // --new flag (new conversation) or --new-agent (new agent): create a new conversation + // When creating a new agent, always create a new conversation alongside it const conversation = await client.conversations.create({ agent_id: agent.id, isolated_block_labels: isolatedBlockLabels, }); conversationId = conversation.id; + } else { + // Default: use the agent's "default" conversation (OG single-threaded behavior) + conversationId = "default"; } markMilestone("HEADLESS_CONVERSATION_READY"); diff --git a/src/index.ts b/src/index.ts index 3480340..1bd4ef3 100755 --- a/src/index.ts +++ b/src/index.ts @@ -63,10 +63,11 @@ Letta Code is a general purpose CLI for interacting with Letta agents USAGE # interactive TUI - letta Resume from profile or create new agent (shows selector) + letta Resume default conversation (OG single-threaded experience) + letta --new Create a new conversation (for concurrent sessions) letta --continue Resume last session (agent + conversation) directly letta --resume Open agent selector UI to pick agent/conversation - letta --new Create a new agent directly (skip profile selector) + letta --new-agent Create a new agent directly (skip profile selector) letta --agent Open a specific agent by ID # headless @@ -81,9 +82,10 @@ OPTIONS --info Show current directory, skills, and pinned agents --continue Resume last session (agent + conversation) directly -r, --resume Open agent selector UI after loading - --new Create new agent directly (skip profile selection) - --init-blocks Comma-separated memory blocks to initialize when using --new (e.g., "persona,skills") - --base-tools Comma-separated base tools to attach when using --new (e.g., "memory,web_search,conversation_search") + --new Create new conversation (for concurrent sessions) + --new-agent Create new agent directly (skip profile selection) + --init-blocks Comma-separated memory blocks to initialize when using --new-agent (e.g., "persona,skills") + --base-tools Comma-separated base tools to attach when using --new-agent (e.g., "memory,web_search,conversation_search") -a, --agent Use a specific agent ID -n, --name Resume agent by name (from pinned agents, case-insensitive) -m, --model Model ID or handle (e.g., "opus-4.5" or "anthropic/claude-opus-4-5") @@ -489,14 +491,8 @@ async function main(): Promise { specifiedConversationId = "default"; } - // Check for deprecated --new flag - if (values.new) { - console.error( - "Error: --new has been renamed to --new-agent\n" + - "Usage: letta --new-agent", - ); - process.exit(1); - } + // --new: Create a new conversation (for concurrent sessions) + const forceNewConversation = (values.new as boolean | undefined) ?? false; const initBlocksRaw = values["init-blocks"] as string | undefined; const baseToolsRaw = values["base-tools"] as string | undefined; @@ -686,6 +682,22 @@ async function main(): Promise { } } + // Validate --new flag (create new conversation) + if (forceNewConversation) { + if (shouldContinue) { + console.error("Error: --new cannot be used with --continue"); + process.exit(1); + } + if (specifiedConversationId) { + console.error("Error: --new cannot be used with --conversation"); + process.exit(1); + } + if (shouldResume) { + console.error("Error: --new cannot be used with --resume"); + process.exit(1); + } + } + // Validate --from-af flag if (fromAfFile) { if (specifiedAgentId) { @@ -1687,12 +1699,14 @@ async function main(): Promise { } if (!resumedSuccessfully) { - // No valid session to resume for this agent, or it failed - create new - const conversation = await client.conversations.create({ - agent_id: agent.id, - isolated_block_labels: [...ISOLATED_BLOCK_LABELS], - }); - conversationIdToUse = conversation.id; + // No valid session to resume - error with helpful message + console.error( + `Attempting to resume conversation ${lastSession?.conversationId ?? "(unknown)"}, but conversation was not found.`, + ); + console.error( + "Resume the default conversation with 'letta', view recent conversations with 'letta --resume', or start a new conversation with 'letta --new'.", + ); + process.exit(1); } } else if (selectedConversationId) { // User selected a specific conversation from the --resume selector @@ -1717,14 +1731,24 @@ async function main(): Promise { } throw error; } - } else { - // Default: create a new conversation on startup - // This ensures each CLI session has isolated message history + } else if (forceNewConversation || forceNew) { + // --new flag (new conversation) or --new-agent (new agent): create a new conversation + // When creating a new agent, always create a new conversation alongside it const conversation = await client.conversations.create({ agent_id: agent.id, isolated_block_labels: [...ISOLATED_BLOCK_LABELS], }); conversationIdToUse = conversation.id; + } else { + // Default: use the agent's "default" conversation (OG single-threaded behavior) + conversationIdToUse = "default"; + + // Load message history from the default conversation + setLoadingState("checking"); + const freshAgent = await client.agents.retrieve(agent.id); + const data = await getResumeData(client, freshAgent, "default"); + setResumeData(data); + setResumedExistingConversation(true); } // Save the session (agent + conversation) to settings diff --git a/src/release-notes.ts b/src/release-notes.ts index dbd3a57..bc572a3 100644 --- a/src/release-notes.ts +++ b/src/release-notes.ts @@ -17,6 +17,10 @@ export const releaseNotes: Record = { // Add release notes for new versions here. // Keep concise - 3-4 bullet points max. // Use → for bullets to match the command hints below. + "0.13.4": `🔄 **Letta Code 0.13.4: Back to the OG experience** +→ Running **letta** now resumes your "default" conversation (instead of spawning a new one) +→ Use **letta --new** if you want to create a new conversation for concurrent sessions +→ Read more: https://docs.letta.com/letta-code/changelog#0134`, "0.13.0": `🎁 **Letta Code 0.13.0: Introducing Conversations!** → Letta Code now starts a new conversation on each startup (memory is shared across all conversations) → Use **/resume** to switch conversations, or run **letta --continue** to continue an existing conversation diff --git a/src/tests/headless-input-format.test.ts b/src/tests/headless-input-format.test.ts index 1148bb2..7c3045e 100644 --- a/src/tests/headless-input-format.test.ts +++ b/src/tests/headless-input-format.test.ts @@ -46,7 +46,8 @@ async function runBidirectional( ], { cwd: process.cwd(), - env: { ...process.env }, + // Mark as subagent to prevent polluting user's LRU settings + env: { ...process.env, LETTA_CODE_AGENT_ROLE: "subagent" }, }, ); diff --git a/src/tests/headless-scenario.ts b/src/tests/headless-scenario.ts index 7a73e39..437d1be 100644 --- a/src/tests/headless-scenario.ts +++ b/src/tests/headless-scenario.ts @@ -76,7 +76,12 @@ async function runCLI( "-m", model, ]; - const proc = Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe" }); + // Mark as subagent to prevent polluting user's LRU settings + const proc = Bun.spawn(cmd, { + stdout: "pipe", + stderr: "pipe", + env: { ...process.env, LETTA_CODE_AGENT_ROLE: "subagent" }, + }); const out = await new Response(proc.stdout).text(); const err = await new Response(proc.stderr).text(); const code = await proc.exited; diff --git a/src/tests/headless-stream-json-format.test.ts b/src/tests/headless-stream-json-format.test.ts index 1953bee..231ce70 100644 --- a/src/tests/headless-stream-json-format.test.ts +++ b/src/tests/headless-stream-json-format.test.ts @@ -34,7 +34,8 @@ async function runHeadlessCommand( ], { cwd: process.cwd(), - env: { ...process.env }, + // Mark as subagent to prevent polluting user's LRU settings + env: { ...process.env, LETTA_CODE_AGENT_ROLE: "subagent" }, }, ); diff --git a/src/tests/headless-windows.ts b/src/tests/headless-windows.ts index 5ed71c5..37d829f 100644 --- a/src/tests/headless-windows.ts +++ b/src/tests/headless-windows.ts @@ -67,7 +67,12 @@ async function runCLI( "-m", model, ]; - const proc = Bun.spawn(cmd, { stdout: "pipe", stderr: "pipe" }); + // Mark as subagent to prevent polluting user's LRU settings + const proc = Bun.spawn(cmd, { + stdout: "pipe", + stderr: "pipe", + env: { ...process.env, LETTA_CODE_AGENT_ROLE: "subagent" }, + }); const out = await new Response(proc.stdout).text(); const err = await new Response(proc.stderr).text(); const code = await proc.exited; diff --git a/src/tests/lazy-approval-recovery.test.ts b/src/tests/lazy-approval-recovery.test.ts index 3273227..f498ec5 100644 --- a/src/tests/lazy-approval-recovery.test.ts +++ b/src/tests/lazy-approval-recovery.test.ts @@ -64,7 +64,8 @@ async function runLazyRecoveryTest(timeoutMs = 180000): Promise<{ ], { cwd: process.cwd(), - env: { ...process.env }, + // Mark as subagent to prevent polluting user's LRU settings + env: { ...process.env, LETTA_CODE_AGENT_ROLE: "subagent" }, }, ); diff --git a/src/tests/startup-flow.test.ts b/src/tests/startup-flow.test.ts index 5ba9c03..47c2775 100644 --- a/src/tests/startup-flow.test.ts +++ b/src/tests/startup-flow.test.ts @@ -30,7 +30,8 @@ async function runCli( return new Promise((resolve, reject) => { const proc = spawn("bun", ["run", "dev", ...args], { cwd: projectRoot, - env: { ...process.env }, + // Mark as subagent to prevent polluting user's LRU settings + env: { ...process.env, LETTA_CODE_AGENT_ROLE: "subagent" }, }); let stdout = "";