From 6f8f227e64042a0c0ef4da47f0f5027639b5bcc4 Mon Sep 17 00:00:00 2001 From: cthomas Date: Tue, 27 Jan 2026 16:37:15 -0800 Subject: [PATCH] fix: improve approval retry idempotency check for server-side tool calls (#9136) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **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 --- letta/agents/helpers.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/letta/agents/helpers.py b/letta/agents/helpers.py index ca1506f6..2ce15c0f 100644 --- a/letta/agents/helpers.py +++ b/letta/agents/helpers.py @@ -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(