fix: improve approval retry idempotency check for server-side tool calls (#9136)

**Problem:**
When retrying an approval response, the idempotency check only looked at
the last message. If the approved tool triggered server-side tool calls
(e.g., `memory`), those tool returns would be the last message, causing
the idempotency check to fail with:
"Cannot process approval response: No tool call is currently awaiting approval."

**Root Cause:**
The check at line 249 only validated `current_in_context_messages[-1]`,
but server-side tool calls can add additional tool return messages after
the original approved tool's return.

**Fix:**
Search the last 10 messages (instead of just the last one) for a tool
return matching the approval's tool_call_ids. This handles the case where
server-side tool calls happen after the approved tool executes, while
keeping the search bounded and efficient.

👾 Generated with [Letta Code](https://letta.com)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
cthomas
2026-01-27 16:37:15 -08:00
committed by Caren Thomas
parent 0c016d3ee3
commit 6f8f227e64

View File

@@ -246,9 +246,21 @@ async def _prepare_in_context_messages_no_persist_async(
if input_messages[0].type == "approval":
# User is trying to send an approval response
if current_in_context_messages and current_in_context_messages[-1].role != "approval":
if current_in_context_messages[-1].role == "tool" and validate_persisted_tool_call_ids(
current_in_context_messages[-1], input_messages[0]
):
# No pending approval request - check if this is an idempotent retry
# Check last few messages for a tool return matching the approval's tool_call_ids
# (approved tool return should be recent, but server-side tool calls may come after it)
approval_already_processed = False
recent_messages = current_in_context_messages[-10:] # Only check last 10 messages
for msg in reversed(recent_messages):
if msg.role == "tool" and validate_persisted_tool_call_ids(msg, input_messages[0]):
logger.info(
f"Idempotency check: Found matching tool return in recent history. "
f"tool_returns={msg.tool_returns}, approval_response.approvals={input_messages[0].approvals}"
)
approval_already_processed = True
break
if approval_already_processed:
# Approval already handled, just process follow-up messages if any or manually inject keep-alive message
keep_alive_messages = input_messages[1:] or [
MessageCreate(