fix: load full history when compacting a limited session#3827
fix: load full history when compacting a limited session#3827winklemad wants to merge 12 commits into
Conversation
OpenAIResponsesCompactionSession._ensure_compaction_candidates loaded the underlying history with a bare underlying_session.get_items(), which resolves to session_settings.limit and returns only the latest N items. In input/auto mode those truncated items are what gets sent to responses.compact, and run_compaction then clears the session and replaces it with the compacted summary — so any history older than the limit window is silently and permanently lost. The restore/replace paths already read the full history through _get_all_underlying_session_items() (limit=_ALL_SESSION_ITEMS_LIMIT), added in openai#3117; the candidate-loading path was missed. Use the same helper there so compaction always operates on the complete stored history.
|
@codex review |
|
Codex Review: Didn't find any major issues. Already looking forward to the next diff. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
seratch
left a comment
There was a problem hiding this comment.
Using _get_all_underlying_session_items() is correct for explicit input compaction. However, the current change also loads full history for thresholding while ordinary auto with a stored response_id still resolves to previous_response_id. That request does not include the full local history, but the session is then cleared and replaced, so older items can still be silently dropped; this patch can also make that compaction trigger earlier.
Please make auto fall back to full-history input compaction whenever the stored history is not fully represented by the limited session view, while leaving explicitly requested previous_response_id behavior opt-in. Add a no-force regression test covering that auto path before replacement.
…imited In auto mode with a stored response_id, compaction resolves to previous_response_id, which does not send the local history. When the underlying session's default view is limit-bounded and does not cover the full stored history, clearing and replacing the session with the server-derived summary can still drop the unrepresented items. Auto now falls back to full-history input compaction whenever the limited view does not cover the stored history; an explicitly requested previous_response_id compaction stays opt-in. Add a no-force regression test for the auto path, and guard the test assertions against an Optional input value for pyright.
|
Thanks @seratch — you're right, the
Full |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5469581c1c
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
When auto compaction falls back to input because the limited session view did not cover the full history, the stored response_id still points at a server response that omits the hidden items. Once the session later shrinks back within the limit, a forced compaction would otherwise switch back to previous_response_id and replace the full-history summary with a server-derived one that drops those items. Mark the response_id unsafe on fallback, mirroring the store=False path, and cover it with a regression test.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 10471fd7f5
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
The auto->input fallback checked the underlying session's default view, which misses a limit supplied per run (e.g. RunConfig session settings) on an otherwise-unlimited store: the response only saw the limited window, but the check reported full coverage and kept using previous_response_id. Remember the limit the Runner uses to prepare each turn's input via get_items and compare against that window instead, so the fallback triggers whenever the response did not see the full history.
|
@seratch ready for another look when you get a chance — the |
seratch
left a comment
There was a problem hiding this comment.
Thanks for addressing the previous-response fallback, unsafe ID reuse, and per-run limits. The remaining blocker is ownership of the per-run limit.
_last_prepared_input_limit is one mutable field updated by every public get_items() call, rather than state tied to a specific run or response. If A prepares with limit=3, B prepares with limit=None, and A then completes, A can read B's limit and use previous_response_id even though response A omitted older history. A fresh compaction wrapper on RunState resume has the same problem because resume skips session input preparation.
Please carry the effective history-window metadata with the specific run/response into both compaction and deferred compaction instead of inferring it from shared get_items() side effects. Please also add controlled-overlap and fresh-wrapper-resume regressions that assert the surviving operation and full stored history.
|
Thanks @seratch — good catch on the ownership issue. Reworked it so the history window travels with the specific response instead of being inferred from shared
Added the two regressions you asked for, both asserting the surviving operation compacts from the full stored history:
Both fail on the previous commit and pass now. Full |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d79002827f
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
Follow-up: I widened the fix so the per-run window now flows through every save path that can reach compaction, not just the direct Added regressions for each: the final-output path ( |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e2ace6c9ec
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
One more refinement on the efficiency edge: a limited run whose pre-existing history already fit within the window shouldn't be forced onto full-history Coverage is now resolved against the pre-save store (the state the response was actually prepared from) in |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2788e93e1c
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
This round is a correctness fix, not just efficiency. When the compaction session wraps a store that carries its own It now walks the delegate chain ( |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5570d8091a
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
|
Tightened the pre-save coverage check to be exact. It now reads one item beyond the window ( |
|
@seratch this is ready for another look — consolidating where it landed, since the fixes ended up spread across a few commits. The ownership blocker is resolved. The history-window state no longer lives in a shared mutable field ( Both regressions you asked for are in:
Also hardened along the way: per-run Green on the current head: the compaction suite (59 tests) plus ruff, mypy, and pyright clean on the changed files. |
seratch
left a comment
There was a problem hiding this comment.
Thanks, the previous shared-limit blocker is substantially addressed: the limit state is no longer stored by get_items(), and the coverage result is threaded through the normal, resumed, streaming, and deferred save paths.
One ownership issue remains in run_compaction(). The call writes its response_id into shared self._response_id, awaits candidate loading, and then combines that shared value with this call's local input_covered_full_history. If B suspends with covered=True, A overwrites the field with a limited response ID and returns without compacting, B can resume and send A's ID under B's coverage decision.
Please keep the response ID, coverage, and mode decision attempt-local across awaits. Also replace the current sequential "interleaved" test with an event-controlled two-call regression that proves the request uses its own response ID and that full stored history survives.
run_compaction wrote the call's response id into the shared self._response_id and then, after awaiting candidate loading, read that shared field back for the mode decision, the previous_response_id it sends, and the unstored marking. An interleaved run mutating the field in that gap could make this call compact under another call's id and coverage. Capture the response id in a local at the top (coverage and mode are already local) and use it for every post-await decision; self._response_id is still updated for external state but no longer read within the call. Replace the sequential interleaved test with an event-controlled two-call regression that parks one call at its first await while the other overwrites the shared field, asserting each call compacts under its own response id and full stored history survives.
|
@seratch addressed — thanks for the precise catch.
Replaced the sequential interleaved test with an event-controlled two-call regression ( |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8a43af3ff3
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| run_state._reasoning_item_id_policy | ||
| ), | ||
| store=store_setting, | ||
| session_settings=run_config.session_settings, |
There was a problem hiding this comment.
Preserve the original session limit on resume
On a RunState resume, this uses the resume call's RunConfig rather than the settings that prepared the interrupted response. If the original run used RunConfig(session_settings=SessionSettings(limit=N)) and the resume call omits or changes that setting with a fresh OpenAIResponsesCompactionSession, save_result_to_session marks input_covered_full_history as true for a response_id that only saw the limited window, so auto compaction can use previous_response_id and replace the full local history with a server summary missing older items. Carry the original turn's coverage/limit in RunState instead of recomputing it from the resume config.
Useful? React with 👍 / 👎.
| elif store is True and response_id == self._last_unstored_response_id: | ||
| self._last_unstored_response_id = None |
There was a problem hiding this comment.
Keep unsafe response IDs unsafe when store is true
Fresh evidence after the prior unsafe-ID fix: the same _last_unstored_response_id slot is now used for response IDs that did not cover full history, but this branch clears it whenever a later compaction of the same ID passes store=True. After an input fallback has shrunk the session inside the limit, that later call can pass the live coverage check and switch back to previous_response_id, replacing the full-history summary with server history that never saw the hidden older items. Track coverage-unsafe IDs separately, or do not clear them based only on store=True.
Useful? React with 👍 / 👎.
|
@seratch — the latest commit has the attempt-local response id fix plus the event-controlled two-call regression you asked for. Two of the Codex suggestions on this push I'd rather get your read on before expanding scope, since one looks unreachable and the other is broader than this PR: 1. Clearing 2. Resume uses the resume call's Kept this commit focused on your ownership fix rather than folding both in speculatively — let me know how you'd like to scope 1 and 2. |
|
Thanks for the detailed review @seratch — this is addressed in the latest commit (
I also replaced the sequential interleaved test with an event-controlled two-call regression ( |
…s-full-history # Conflicts: # src/agents/run_internal/session_persistence.py
run_compaction stored each call's response id in a shared self._response_id and, when a later call had none, read it back -- so a concurrent no-id call could compact under another run's previous_response_id and drop older history on replace. It also resolved the mode after awaiting candidate loading, where a concurrent run could shift the shared unstored-id hint. Drop the shared response-id field so each call decides from its own id, and resolve the mode before the candidate-loading await in both run_compaction and _defer_compaction. Replace the interleaving test with an event-controlled two-call regression proving a no-id call falls back to full-history input instead of inheriting a concurrent run's response id.
|
Thanks @seratch — addressed both points:
Also merged |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 80afea6095
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| prepared_view = await session.get_items(limit=history_limit + 1) | ||
| input_covered_full_history = len(prepared_view) <= history_limit |
There was a problem hiding this comment.
Don't count pre-saved turn input as missing history
Fresh evidence after the earlier exact-limit fix: in the normal Runner paths the user's input is already persisted before the model response is saved (for example run.py 771/864 and streaming run_loop.py 1516), so this length check is no longer measuring only the pre-response history. With a limited OpenAIResponsesCompactionSession, if the pre-turn history exactly filled the limit (or a multi-item user input pushes the stored length past it), the response did see all prior history plus the current input, but this marks the response ID unsafe and forces full-history input compaction; for large histories this can turn a safe server-side compaction into an oversized or unnecessarily expensive request.
Useful? React with 👍 / 👎.
What & why
OpenAIResponsesCompactionSession._ensure_compaction_candidatesloads the underlying history with a bareunderlying_session.get_items(). For a session configured with a limit (SessionSettings(limit=N)),get_items()resolves the limit tosession_settings.limitand returns only the latest N items.In
input/autocompaction mode those truncated items are exactly what gets sent toresponses.compact, andrun_compactionthen clears the underlying session and replaces it with the compacted summary. So every item older than the limit window is silently and permanently lost. Compaction runs automatically once the candidate threshold is reached (noforceneeded), so this bites any user who pairs a limited session with compaction.Minimal repro:
Fix
The restore/replace paths already read the complete history through
_get_all_underlying_session_items()(limit=_ALL_SESSION_ITEMS_LIMIT), introduced in #3117; the candidate-loading path was missed. Use the same helper so compaction always operates on the full stored history.Tests
Added
test_run_compaction_uses_full_history_when_underlying_session_has_limit: a realSQLiteSession(limit=3)holding 8 items must send all 8 toresponses.compact(fails before the fix — only the latest 3 arrive). Fulltests/memory/suite (146 tests) passes; ruff and mypy clean.Found by reading the code; no linked issue.