feat: revert default startup to use "default" conversation (#594)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Charles Packer
2026-01-19 13:02:12 -08:00
committed by GitHub
parent 7760e2a2b9
commit a526614e28
11 changed files with 152 additions and 97 deletions

View File

@@ -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}`}
</Text>
<Text> </Text>
<Text dimColor>Resume this conversation with:</Text>
<Text color={colors.link.url}>
{`letta --conv ${conversationId}`}
</Text>
{/* Only show conversation hint if not on default (default is resumed automatically) */}
{conversationId !== "default" && (
<>
<Text> </Text>
<Text dimColor>Resume this conversation with:</Text>
<Text color={colors.link.url}>
{`letta --conv ${conversationId}`}
</Text>
</>
)}
</Box>
)}

View File

@@ -77,29 +77,20 @@ export const commands: Record<string, Command> = {
},
},
"/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<string, Command> = {
},
"/compact": {
desc: "Summarize conversation history (compaction)",
hidden: true,
handler: () => {
// Handled specially in App.tsx to access client and agent ID
return "Compacting conversation...";

View File

@@ -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");

View File

@@ -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 <id> 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 <list> Comma-separated memory blocks to initialize when using --new (e.g., "persona,skills")
--base-tools <list> 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 <list> Comma-separated memory blocks to initialize when using --new-agent (e.g., "persona,skills")
--base-tools <list> Comma-separated base tools to attach when using --new-agent (e.g., "memory,web_search,conversation_search")
-a, --agent <id> Use a specific agent ID
-n, --name <name> Resume agent by name (from pinned agents, case-insensitive)
-m, --model <id> Model ID or handle (e.g., "opus-4.5" or "anthropic/claude-opus-4-5")
@@ -489,14 +491,8 @@ async function main(): Promise<void> {
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<void> {
}
}
// 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<void> {
}
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<void> {
}
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

View File

@@ -17,6 +17,10 @@ export const releaseNotes: Record<string, string> = {
// 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

View File

@@ -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" },
},
);

View File

@@ -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;

View File

@@ -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" },
},
);

View File

@@ -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;

View File

@@ -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" },
},
);

View File

@@ -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 = "";