fix: /resume nonexistent conversation (#548)
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
// Check for pending approvals and retrieve recent message history when resuming an agent/conversation
|
||||
|
||||
import type Letta from "@letta-ai/letta-client";
|
||||
import { APIError } from "@letta-ai/letta-client/core/error";
|
||||
import type { AgentState } from "@letta-ai/letta-client/resources/agents/agents";
|
||||
import type { Message } from "@letta-ai/letta-client/resources/agents/messages";
|
||||
import type { ApprovalRequest } from "../cli/helpers/stream";
|
||||
@@ -293,6 +294,14 @@ export async function getResumeData(
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
// Re-throw "not found" errors (404/422) so callers can handle appropriately
|
||||
// (e.g., /resume command should fail for non-existent conversations)
|
||||
if (
|
||||
error instanceof APIError &&
|
||||
(error.status === 404 || error.status === 422)
|
||||
) {
|
||||
throw error;
|
||||
}
|
||||
console.error("Error getting resume data:", error);
|
||||
return { pendingApproval: null, pendingApprovals: [], messageHistory: [] };
|
||||
}
|
||||
|
||||
@@ -4332,18 +4332,8 @@ export default function App({
|
||||
refreshDerived();
|
||||
|
||||
try {
|
||||
// Update conversation ID and settings
|
||||
setConversationId(targetConvId);
|
||||
settingsManager.setLocalLastSession(
|
||||
{ agentId, conversationId: targetConvId },
|
||||
process.cwd(),
|
||||
);
|
||||
settingsManager.setGlobalLastSession({
|
||||
agentId,
|
||||
conversationId: targetConvId,
|
||||
});
|
||||
|
||||
// Fetch message history for the selected conversation
|
||||
// Validate conversation exists BEFORE updating state
|
||||
// (getResumeData throws 404/422 for non-existent conversations)
|
||||
if (agentState) {
|
||||
const client = await getClient();
|
||||
const resumeData = await getResumeData(
|
||||
@@ -4352,6 +4342,17 @@ export default function App({
|
||||
targetConvId,
|
||||
);
|
||||
|
||||
// Only update state after validation succeeds
|
||||
setConversationId(targetConvId);
|
||||
settingsManager.setLocalLastSession(
|
||||
{ agentId, conversationId: targetConvId },
|
||||
process.cwd(),
|
||||
);
|
||||
settingsManager.setGlobalLastSession({
|
||||
agentId,
|
||||
conversationId: targetConvId,
|
||||
});
|
||||
|
||||
// Clear current transcript and static items
|
||||
buffersRef.current.byId.clear();
|
||||
buffersRef.current.order = [];
|
||||
@@ -4442,16 +4443,28 @@ export default function App({
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const errorCmdId = uid("cmd");
|
||||
buffersRef.current.byId.set(errorCmdId, {
|
||||
// Update existing loading message instead of creating new one
|
||||
// Format error message to be user-friendly (avoid raw JSON/internal details)
|
||||
let errorMsg = "Unknown error";
|
||||
if (error instanceof APIError) {
|
||||
if (error.status === 404) {
|
||||
errorMsg = "Conversation not found";
|
||||
} else if (error.status === 422) {
|
||||
errorMsg = "Invalid conversation ID";
|
||||
} else {
|
||||
errorMsg = error.message;
|
||||
}
|
||||
} else if (error instanceof Error) {
|
||||
errorMsg = error.message;
|
||||
}
|
||||
buffersRef.current.byId.set(cmdId, {
|
||||
kind: "command",
|
||||
id: errorCmdId,
|
||||
id: cmdId,
|
||||
input: msg.trim(),
|
||||
output: `Failed to switch conversation: ${error instanceof Error ? error.message : String(error)}`,
|
||||
output: `Failed to switch conversation: ${errorMsg}`,
|
||||
phase: "finished",
|
||||
success: false,
|
||||
});
|
||||
buffersRef.current.order.push(errorCmdId);
|
||||
refreshDerived();
|
||||
} finally {
|
||||
setCommandRunning(false);
|
||||
@@ -7454,18 +7467,8 @@ Plan file path: ${planFilePath}`;
|
||||
refreshDerived();
|
||||
|
||||
try {
|
||||
// Update conversation ID and settings
|
||||
setConversationId(convId);
|
||||
settingsManager.setLocalLastSession(
|
||||
{ agentId, conversationId: convId },
|
||||
process.cwd(),
|
||||
);
|
||||
settingsManager.setGlobalLastSession({
|
||||
agentId,
|
||||
conversationId: convId,
|
||||
});
|
||||
|
||||
// Fetch message history for the selected conversation
|
||||
// Validate conversation exists BEFORE updating state
|
||||
// (getResumeData throws 404/422 for non-existent conversations)
|
||||
if (agentState) {
|
||||
const client = await getClient();
|
||||
const resumeData = await getResumeData(
|
||||
@@ -7474,6 +7477,17 @@ Plan file path: ${planFilePath}`;
|
||||
convId,
|
||||
);
|
||||
|
||||
// Only update state after validation succeeds
|
||||
setConversationId(convId);
|
||||
settingsManager.setLocalLastSession(
|
||||
{ agentId, conversationId: convId },
|
||||
process.cwd(),
|
||||
);
|
||||
settingsManager.setGlobalLastSession({
|
||||
agentId,
|
||||
conversationId: convId,
|
||||
});
|
||||
|
||||
// Clear current transcript and static items
|
||||
buffersRef.current.byId.clear();
|
||||
buffersRef.current.order = [];
|
||||
@@ -7574,16 +7588,28 @@ Plan file path: ${planFilePath}`;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
const errorCmdId = uid("cmd");
|
||||
buffersRef.current.byId.set(errorCmdId, {
|
||||
// Update existing loading message instead of creating new one
|
||||
// Format error message to be user-friendly (avoid raw JSON/internal details)
|
||||
let errorMsg = "Unknown error";
|
||||
if (error instanceof APIError) {
|
||||
if (error.status === 404) {
|
||||
errorMsg = "Conversation not found";
|
||||
} else if (error.status === 422) {
|
||||
errorMsg = "Invalid conversation ID";
|
||||
} else {
|
||||
errorMsg = error.message;
|
||||
}
|
||||
} else if (error instanceof Error) {
|
||||
errorMsg = error.message;
|
||||
}
|
||||
buffersRef.current.byId.set(cmdId, {
|
||||
kind: "command",
|
||||
id: errorCmdId,
|
||||
id: cmdId,
|
||||
input: inputCmd,
|
||||
output: `Failed to switch conversation: ${error instanceof Error ? error.message : String(error)}`,
|
||||
output: `Failed to switch conversation: ${errorMsg}`,
|
||||
phase: "finished",
|
||||
success: false,
|
||||
});
|
||||
buffersRef.current.order.push(errorCmdId);
|
||||
refreshDerived();
|
||||
} finally {
|
||||
setCommandRunning(false);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { parseArgs } from "node:util";
|
||||
import type { Letta } from "@letta-ai/letta-client";
|
||||
import { APIError } from "@letta-ai/letta-client/core/error";
|
||||
import type {
|
||||
AgentState,
|
||||
MessageCreate,
|
||||
@@ -563,7 +564,20 @@ export async function handleHeadlessCommand(
|
||||
while (true) {
|
||||
// Re-fetch agent to get latest in-context messages (source of truth for backend)
|
||||
const freshAgent = await client.agents.retrieve(agent.id);
|
||||
const resume = await getResumeData(client, freshAgent);
|
||||
|
||||
let resume: Awaited<ReturnType<typeof getResumeData>>;
|
||||
try {
|
||||
resume = await getResumeData(client, freshAgent);
|
||||
} catch (error) {
|
||||
// Treat 404/422 as "no approvals" - stale message/conversation state
|
||||
if (
|
||||
error instanceof APIError &&
|
||||
(error.status === 404 || error.status === 422)
|
||||
) {
|
||||
break;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Use plural field for parallel tool calls
|
||||
const pendingApprovals = resume.pendingApprovals || [];
|
||||
|
||||
90
src/index.ts
90
src/index.ts
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env bun
|
||||
import { parseArgs } from "node:util";
|
||||
import { APIError } from "@letta-ai/letta-client/core/error";
|
||||
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";
|
||||
@@ -1277,7 +1278,8 @@ async function main(): Promise<void> {
|
||||
}
|
||||
|
||||
// Handle conversation: either resume existing or create new
|
||||
let conversationIdToUse: string;
|
||||
// Using definite assignment assertion - all branches below either set this or exit/throw
|
||||
let conversationIdToUse!: string;
|
||||
|
||||
// Debug: log resume flag status
|
||||
if (process.env.DEBUG) {
|
||||
@@ -1289,19 +1291,33 @@ async function main(): Promise<void> {
|
||||
|
||||
if (specifiedConversationId) {
|
||||
// Use the explicitly specified conversation ID
|
||||
// User explicitly requested this conversation, so error if it doesn't exist
|
||||
conversationIdToUse = specifiedConversationId;
|
||||
setResumedExistingConversation(true);
|
||||
|
||||
// Load message history and pending approvals from the conversation
|
||||
// Re-fetch agent to get fresh message_ids for accurate pending approval detection
|
||||
setLoadingState("checking");
|
||||
const freshAgent = await client.agents.retrieve(agent.id);
|
||||
const data = await getResumeData(
|
||||
client,
|
||||
freshAgent,
|
||||
specifiedConversationId,
|
||||
);
|
||||
setResumeData(data);
|
||||
try {
|
||||
// Load message history and pending approvals from the conversation
|
||||
// Re-fetch agent to get fresh message_ids for accurate pending approval detection
|
||||
setLoadingState("checking");
|
||||
const freshAgent = await client.agents.retrieve(agent.id);
|
||||
const data = await getResumeData(
|
||||
client,
|
||||
freshAgent,
|
||||
specifiedConversationId,
|
||||
);
|
||||
setResumeData(data);
|
||||
} catch (error) {
|
||||
// Only treat 404/422 as "not found", rethrow other errors
|
||||
if (
|
||||
error instanceof APIError &&
|
||||
(error.status === 404 || error.status === 422)
|
||||
) {
|
||||
console.error(
|
||||
`Conversation ${specifiedConversationId} not found`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
} else if (shouldResume) {
|
||||
// Try to load the last session for this agent
|
||||
const lastSession =
|
||||
@@ -1313,23 +1329,43 @@ async function main(): Promise<void> {
|
||||
console.log(`[DEBUG] agent.id=${agent.id}`);
|
||||
}
|
||||
|
||||
let resumedSuccessfully = false;
|
||||
if (lastSession && lastSession.agentId === agent.id) {
|
||||
// Resume the exact last conversation
|
||||
conversationIdToUse = lastSession.conversationId;
|
||||
setResumedExistingConversation(true);
|
||||
// Try to resume the exact last conversation
|
||||
// If it no longer exists, fall back to creating new
|
||||
try {
|
||||
// Load message history and pending approvals from the conversation
|
||||
// Re-fetch agent to get fresh message_ids for accurate pending approval detection
|
||||
setLoadingState("checking");
|
||||
const freshAgent = await client.agents.retrieve(agent.id);
|
||||
const data = await getResumeData(
|
||||
client,
|
||||
freshAgent,
|
||||
lastSession.conversationId,
|
||||
);
|
||||
// Only set state after validation succeeds
|
||||
conversationIdToUse = lastSession.conversationId;
|
||||
setResumedExistingConversation(true);
|
||||
setResumeData(data);
|
||||
resumedSuccessfully = true;
|
||||
} catch (error) {
|
||||
// Only treat 404/422 as "not found", rethrow other errors
|
||||
if (
|
||||
error instanceof APIError &&
|
||||
(error.status === 404 || error.status === 422)
|
||||
) {
|
||||
// Conversation no longer exists, will create new below
|
||||
console.warn(
|
||||
`Previous conversation ${lastSession.conversationId} not found, creating new`,
|
||||
);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load message history and pending approvals from the conversation
|
||||
// Re-fetch agent to get fresh message_ids for accurate pending approval detection
|
||||
setLoadingState("checking");
|
||||
const freshAgent = await client.agents.retrieve(agent.id);
|
||||
const data = await getResumeData(
|
||||
client,
|
||||
freshAgent,
|
||||
lastSession.conversationId,
|
||||
);
|
||||
setResumeData(data);
|
||||
} else {
|
||||
// No valid session to resume for this agent, create new
|
||||
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],
|
||||
|
||||
Reference in New Issue
Block a user