fix(session): auto-clear stuck conversation on invalid tool call ID mismatch (#555)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -59,6 +59,16 @@ export function isAgentMissingFromInitError(error: unknown): boolean {
|
||||
return agentMissingPatterns.some((pattern) => pattern.test(msg));
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if a recovery details string indicates mismatched tool call IDs.
|
||||
* When this happens, the conversation is permanently stuck -- the pending
|
||||
* approval can never be resolved because the server expects different IDs.
|
||||
* The conversation must be cleared and recreated.
|
||||
*/
|
||||
export function isInvalidToolCallIdsError(details: string): boolean {
|
||||
return details.toLowerCase().includes('invalid tool call id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a structured API error into a clear, user-facing message.
|
||||
* The `error` object comes from the SDK's new SDKErrorMessage type.
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import { createAgent, createSession, resumeSession, type Session, type SendMessage, type CanUseToolCallback } from '@letta-ai/letta-code-sdk';
|
||||
import type { BotConfig, StreamMsg } from './types.js';
|
||||
import { isApprovalConflictError, isConversationMissingError, isAgentMissingFromInitError } from './errors.js';
|
||||
import { isApprovalConflictError, isConversationMissingError, isAgentMissingFromInitError, isInvalidToolCallIdsError } from './errors.js';
|
||||
import { Store } from './store.js';
|
||||
import { updateAgentName, recoverOrphanedConversationApproval, isRecoverableConversationId, recoverPendingApprovalsForAgent } from '../tools/letta-api.js';
|
||||
import { installSkillsToAgent, prependSkillDirsToPath } from '../skills/loader.js';
|
||||
@@ -399,6 +399,16 @@ export class SessionManager {
|
||||
} else {
|
||||
log.warn(`Proactive approval recovery did not find resolvable approvals: ${result.details}`);
|
||||
}
|
||||
// Even on partial recovery, if any denial failed with mismatched IDs the
|
||||
// conversation may still be stuck. Clear it so the retry creates a fresh one.
|
||||
if (isInvalidToolCallIdsError(result.details)) {
|
||||
log.warn(`Clearing stuck conversation (key=${key}) due to invalid tool call IDs mismatch`);
|
||||
if (key !== 'shared') {
|
||||
this.store.clearConversation(key);
|
||||
} else {
|
||||
this.store.conversationId = null;
|
||||
}
|
||||
}
|
||||
return this._createSessionForKey(key, true, generation);
|
||||
}
|
||||
}
|
||||
@@ -585,6 +595,17 @@ export class SessionManager {
|
||||
const result = isRecoverableConversationId(convId)
|
||||
? await recoverOrphanedConversationApproval(this.store.agentId, convId)
|
||||
: await recoverPendingApprovalsForAgent(this.store.agentId);
|
||||
// Even on partial recovery, if any denial failed with mismatched IDs the
|
||||
// conversation may still be stuck. Clear it so the retry creates a fresh one.
|
||||
if (isInvalidToolCallIdsError(result.details)) {
|
||||
log.warn(`Clearing stuck conversation (key=${convKey}) due to invalid tool call IDs mismatch, retrying with fresh conversation`);
|
||||
if (convKey !== 'shared') {
|
||||
this.store.clearConversation(convKey);
|
||||
} else {
|
||||
this.store.conversationId = null;
|
||||
}
|
||||
return this.runSession(message, { retried: true, canUseTool, convKey });
|
||||
}
|
||||
if (result.recovered) {
|
||||
log.info(`Recovery succeeded (${result.details}), retrying...`);
|
||||
return this.runSession(message, { retried: true, canUseTool, convKey });
|
||||
|
||||
@@ -895,8 +895,9 @@ export async function recoverOrphanedConversationApproval(
|
||||
streaming: false,
|
||||
});
|
||||
} catch (batchError) {
|
||||
const batchErrMsg = batchError instanceof Error ? batchError.message : String(batchError);
|
||||
log.warn(`Failed to submit approval denial batch for run ${runId} (${approvals.length} tool call(s)):`, batchError);
|
||||
details.push(`Failed to deny ${approvals.length} approval(s) from run ${runId}`);
|
||||
details.push(`Failed to deny ${approvals.length} approval(s) from run ${runId}: ${batchErrMsg}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user