Please read this first
Describe the bug
rewind_session_items() in src/agents/run_internal/session_persistence.py can delete session records that do not belong to the retry it is trying to rewind.
There are two related problems in the current implementation:
- It calls
pop_item() before confirming that the session tail actually matches the target suffix being rewound.
- In the
server_tracker cleanup path, it keeps popping items until it finds a known server item, which also removes the first known server item it encounters.
That means a session tail like:
[known_server_item, unrelated_new_item, target_item]
can become:
after rewinding target_item, even though unrelated_new_item and known_server_item did not belong to the retry being rolled back.
This is especially dangerous for shared or externally-updated sessions, because a retry path can silently erase history added by another run or another actor.
Debug information
- Agents SDK version:
main at f2fb9ffb / latest release boundary v0.15.1
- Python version: Python 3.12
Repro steps
Minimal reproducer:
import asyncio
from typing import Any
from agents.items import TResponseInputItem
from agents.run_internal.oai_conversation import OpenAIServerConversationTracker
from agents.run_internal.session_persistence import rewind_session_items
class InMemorySession:
def __init__(self, history: list[dict[str, Any]]) -> None:
self._items = list(history)
async def get_items(self, limit: int | None = None) -> list[TResponseInputItem]:
if limit is None:
return list(self._items)
return self._items[-limit:]
async def add_items(self, items: list[TResponseInputItem]) -> None:
self._items.extend(items)
async def pop_item(self) -> TResponseInputItem | None:
if not self._items:
return None
return self._items.pop()
async def main() -> None:
known_server_item = {
"id": "msg_server_1",
"type": "message",
"role": "assistant",
"content": "server item",
}
unrelated_new_item = {
"type": "message",
"role": "user",
"content": "unrelated tail item",
}
target_item = {
"type": "message",
"role": "user",
"content": "retry target",
}
session = InMemorySession([known_server_item, unrelated_new_item, target_item])
tracker = OpenAIServerConversationTracker()
tracker.server_item_ids.add("msg_server_1")
await rewind_session_items(session, [target_item], tracker)
print(await session.get_items())
asyncio.run(main())
Current behavior:
The target_item is removed, but the helper also strips unrelated_new_item and the sentinel known_server_item.
Expected behavior
rewind_session_items() should only remove the exact persisted suffix that belongs to the retry being rolled back.
If the session tail does not exactly match the target suffix, it should abort the rewind and log a warning instead of deleting unrelated records. In the server_tracker cleanup path, it should never remove an unrelated tail item or the known server item that terminates cleanup.
Please read this first
Describe the bug
rewind_session_items()insrc/agents/run_internal/session_persistence.pycan delete session records that do not belong to the retry it is trying to rewind.There are two related problems in the current implementation:
pop_item()before confirming that the session tail actually matches the target suffix being rewound.server_trackercleanup path, it keeps popping items until it finds a known server item, which also removes the first known server item it encounters.That means a session tail like:
can become:
after rewinding
target_item, even thoughunrelated_new_itemandknown_server_itemdid not belong to the retry being rolled back.This is especially dangerous for shared or externally-updated sessions, because a retry path can silently erase history added by another run or another actor.
Debug information
mainatf2fb9ffb/ latest release boundaryv0.15.1Repro steps
Minimal reproducer:
Current behavior:
The
target_itemis removed, but the helper also stripsunrelated_new_itemand the sentinelknown_server_item.Expected behavior
rewind_session_items()should only remove the exact persisted suffix that belongs to the retry being rolled back.If the session tail does not exactly match the target suffix, it should abort the rewind and log a warning instead of deleting unrelated records. In the
server_trackercleanup path, it should never remove an unrelated tail item or the known server item that terminates cleanup.