fix(sessions): restore session history when compaction replacement is cancelled - #4298
Conversation
…ancelled CancelledError is a BaseException, so the Exception-only restore path left sessions empty after a successful clear during compaction replacement.
seratch
left a comment
There was a problem hiding this comment.
Thanks for the focused cancellation fix. The main path works, but the rollback currently has no ownership boundary over the session state it rewrites. I reproduced this with SQLiteSession: pause cancellation restore before its clear, append a newer item through the same OpenAIResponsesCompactionSession, then resume restore. The rollback deletes the newer item, while _session_items still retains it, so persistent history and the compaction cache diverge.
Before merge, please add one session-owned mutation lock covering the snapshot, clear/add replacement, and restore, and use the same lock for wrapper add_items(), pop_item(), and clear_session(). Add a controlled interleaving regression test proving the newer write waits and survives in chronological order after cancellation recovery.
|
Yeah cool will get on it, should have done that earlier mb. I can reproduce the failure mode you described: if cancellation restore is paused before its clear, a newer I'll add a session-owned mutation lock that covers:
I'll also add a controlled interleaving regression test with |
…writes Add a session-owned mutation lock around snapshot/replace/restore and wrapper mutators so cancel-restore cannot rewrite past a newer concurrent write.
seratch
left a comment
There was a problem hiding this comment.
Thanks for addressing the original rollback ownership issue. A broader cancellation probe found one remaining data-loss path. If replacement add_items() first raises a normal Exception, then the caller cancels while restore is awaiting, the Exception path uses shield_restore=False; restore is cancelled, SQLite history remains empty, and the wrapper caches retain the previous history.
Please make restore settlement cancellation-safe regardless of whether replacement first failed with Exception or CancelledError. Remove the conditional shield behavior and drain every restore before propagating cancellation. Add a controlled SQLite regression test that gates restore, cancels the outer compaction task, proves it remains pending until restore is released, and then asserts CancelledError, restored history, cache agreement, a released lock, and no orphan restore task. Existing behavior should still re-raise the original exception when no cancellation occurs.
|
Thanks — good catch. Confirmed on the current head: the Exception path still uses I'll make restore settlement cancellation-safe for both |
Always settle restore before CancelledError propagates, including when replacement failed with a normal Exception. Adds a gated SQLite regression for cancel-during-Exception-restore.
Summary
This pull request fixes a data-loss path in
OpenAIResponsesCompactionSessionwhere cancelling a run during compaction replacement could permanently wipe the underlying session history.PR #3117 already restored previous history when replacement failed with a normal
Exception(for example, a failedadd_items()afterclear_session()). That restore path does not run forasyncio.CancelledError, because on supported Python versionsCancelledErroris aBaseException, not anException.What goes wrong today
Compaction replacement is intentionally destructive:
clear_session()— history is now empty.add_items(compacted_output)— write the compacted replacement.If cancellation lands after step 2 and before step 3 finishes, the
except Exceptionhandlers from #3117 do not run. The compacted write never lands, restore never runs, and the session remains empty.This is especially sharp for immediate cancel (
task.cancel()), which is delivered atawaitpoints. The post-clearadd_items()await is exactly such a point.What this PR changes
ExceptionandCancelledError.CancelledError, restore previous history with the same clear-failed vs post-clear routing used forException.CancelledErrorreaches the caller.Exceptionrestore behavior unchanged (no shield on that path).Restore helpers now accept
BaseExceptionso cancellation can be logged through the existing warning helpers.Relationship to nearby work
CancelledErrorgap left by Exception-only handlersTest plan
Added regression coverage next to the #3117 tests in
tests/memory/test_openai_responses_compaction_session.py:add_items()raisesCancelledErrorafter a successful clearclear_session()mutates then raisesCancelledErrortask.cancel()+ yield while restore is gated), asserting history is already restored whenCancelledErrorsurfacesLocal verification:
uv run ruff format/uv run ruff checkuv run pyright/uv run mypyon the touched compaction moduleuv run pytest -q tests/memory/test_openai_responses_compaction_session.py(51 passed)make testsvia the verification script could not be run here becausemakeis unavailable in this Windows environment; broaderpytestrun showed only unrelated local tracing environment failuresIssue number
N/A — discovered by code-path review against the #3117 restore contract; no open issue tracks the cancel-specific gap.
Checks
/reviewbefore submitting this PR