fix: avoid index-shift bug when trimming images in flush_messages - #211
fix: avoid index-shift bug when trimming images in flush_messages#211Avinash2468 wants to merge 1 commit into
Conversation
Iterate content lists in reverse when deleting image entries so deletions don't shift not-yet-visited indices. Previously a message containing multiple images could skip images (leaving more than max_trajectory_length in the prompt) or raise IndexError. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Correcting my own PR description: I originally cited #168 as something this change might address. On a closer look that was wrong, so I've edited the description. #168 is a config mismatch — The index-shift bug here is real but currently latent: every caller feeding the generator and reflection agents passes a single screenshot per message, so no deletion shifts an unvisited index today. It becomes reachable as soon as anyone uses the multi-image list form that Flagging it here rather than quietly rewriting the description. Happy to trim scope if you'd prefer just the two-line fix without tests. |
Problem
Worker.flush_messages()trims old images from the LLM message history to keep at mostmax_trajectory_lengthimages in the prompt. For long-context providers (anthropic,openai,gemini) it walks each message'scontentlist and deletes image entries once the running count exceedsmax_images:The outer loop iterates in reverse, but the inner loop does not.
delat indexjshifts every subsequent element down by one while the loop keeps walking indices computed from the original length. For a message whosecontentholds more than one image, that means:max_trajectory_lengthimages can survive into the prompt, andIndexError: list index out of range.There is also a smaller correctness problem independent of the deletion bug: because the inner loop counts front-to-back, the images kept within a single message are the oldest ones, not the newest — the opposite of the intended "keep the latest k images" policy.
Scope — this is a latent bug
I want to be upfront that I found this by reading the code, not from a failing run.
As of this commit the bug is not reachable through the current call paths: every
add_messagecall feeding the two agentsflush_messagesoperates on (worker.py:153,worker.py:160,worker.py:305) passes a singleobs["screenshot"], so each message holds exactly one image and no deletion ever shifts an unvisited index.It is reachable the moment any caller passes multiple images for one message, which
LMMAgent.add_message/add_message_to_roleexplicitly support — they accept a list and append onecontententry per image (gui_agents/s3/core/mllm.py:211-225). So this is hardening a landmine rather than fixing a live crash, plus the newest-vs-oldest fix above.To be clear about what this does not do: it does not fix #168. That report is a configuration mismatch —
max_trajectory_lengthdefaults to 8 (worker.py:30,agent_s.py:56) while the provider there caps at 3 images — andflush_messagesbehaves as designed in that scenario. Happy to look at that separately if it'd be useful.Fix
Iterate the inner loop in reverse as well. Deleting index
jthen only affects indices> j, which have already been visited, so nothing is skipped and no out-of-range access occurs. Walking content back-to-front also counts the most-recently-appended images first, matching the intended policy.One line in each of the two affected files:
gui_agents/s3/agents/worker.pygui_agents/s2_5/agents/worker.pyTests
Adds
tests/test_flush_messages.py, exercising the realWorker.flush_messageson boths3ands2_5(constructed via__new__so no network or model setup is needed). Coverage:contententries are preservedReproduction, one message with 4 images and
max_images=2:6 of the 8 new cases fail on
main; all 8 pass with the fix.black --check gui_agentspasses.If you'd rather not carry tests for a code path nothing currently exercises, I'm happy to trim them down or drop them and keep just the two-line fix.