fix: make AdvancedSQLiteSession.pop_item branch-aware#3766
Conversation
AdvancedSQLiteSession inherits SQLiteSession.pop_item, which deletes the newest row for the whole session (ORDER BY id DESC) with no branch filter. On a non-main branch that pops an item belonging to another branch (usually main) and silently corrupts it: pop_item returns the wrong item and the other branch loses its tail. Override pop_item to scope the delete to the current branch via message_structure. Because branches share message rows and foreign keys are not enforced, it drops the branch's newest message_structure row and deletes the underlying message only when no other branch still references it. Adds a regression test.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ddbb945a02
ℹ️ 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".
|
|
||
| structure_rowid, message_id, message_data = row | ||
| conn.execute( | ||
| "DELETE FROM message_structure WHERE rowid = ?", |
There was a problem hiding this comment.
Preserve branches when popping their last item
When pop_item() removes the final message_structure row for a branch, that branch effectively disappears because list_branches() groups only rows in message_structure and switch_to_branch() validates existence with COUNT(*) from the same table. In a branch with a single item (or after popping down to one item), popping the tail leaves _current_branch_id set but the branch is no longer listable or switchable once the user switches away, so branch-aware rewinds can silently orphan an otherwise valid empty branch.
Useful? React with 👍 / 👎.
|
Thanks for the contribution. The reported bug is real: AdvancedSQLiteSession.get_items() is branch-scoped, while the inherited pop_item() deletes the session-wide tail. However, #3755 already addresses the same pop_item bug and also covers the metadata and ownership invariants needed for a safe fix, including turn_usage cleanup, a call-time branch snapshot, shared-message ownership, and controlled interleaving tests. This PR still leaves stale turn_usage when a pop empties a turn, and a concurrent branch switch can redirect an in-flight pop because the branch is read inside the worker. To avoid maintaining competing implementations, I am going to close this PR in favor of #3755. Thank you for the clear reproduction and regression test. |
What
AdvancedSQLiteSessiontracks branches (_current_branch_id, amessage_structuretable) and itsget_itemsis branch-scoped, but it does not overridepop_item. The inheritedSQLiteSession.pop_itemdeletes the newest row for the whole session:There is no branch filter, so on a non-
mainbranch it pops an item that belongs to another branch (usuallymain) and silently corrupts it —pop_itemreturns the wrong item and the other branch loses its tail. This is reachable via the runner's session-rewind path, which verifies the tail with branch-awareget_itemsand then removes it withpop_item.Reproduction
Fix
Override
pop_iteminAdvancedSQLiteSessionto scope the pop to the current branch (joinmessage_structure, filter bybranch_id, order by the branch'ssequence_number). Because branches share message rows and foreign keys are not enforced (soON DELETE CASCADEdoes not run), it drops the branch's newestmessage_structurerow and deletes the underlying message only when no other branch still references it. Flat (non-branched) sessions behave exactly as before.Tests
Adds
test_pop_item_is_branch_scoped: pops on a branch and asserts it returns the branch tail, leavesmainuntouched, still pops normally onmain, and returnsNoneon an empty branch. It fails on the current code (returnsmain's tail and corruptsmain) and passes with the fix.ruffandmypyare clean; the fullAdvancedSQLiteSessiontest file passes (46).