Skip to content

fix: #3346 AdvancedSQLiteSession.delete_branch orphaned base rows#3380

Open
john-rocky wants to merge 1 commit into
openai:mainfrom
john-rocky:fix/3346-delete-branch-orphans
Open

fix: #3346 AdvancedSQLiteSession.delete_branch orphaned base rows#3380
john-rocky wants to merge 1 commit into
openai:mainfrom
john-rocky:fix/3346-delete-branch-orphans

Conversation

@john-rocky
Copy link
Copy Markdown

Summary

AdvancedSQLiteSession.delete_branch() deleted turn_usage and message_structure rows for the branch, but never touched the underlying messages table. Any base row whose only message_structure reference was on the deleted branch — the common case for follow-ups added after create_branch_from_turn — stayed behind: invisible to advanced reads (which join through message_structure) but still padding the on-disk size, which made branch-heavy workflows accumulate ghost rows.

After the structure rows are deleted, drop any base rows that no longer have a matching message_structure entry, inside the same transaction. The existing _cleanup_orphaned_messages_sync helper does exactly that, so the change is minimal and matches the cleanup pattern already used elsewhere in this module. Messages still referenced by another branch (e.g. shared turns from before the split) are left alone. The log line now also reports the orphan count so operators can spot unexpected cleanup volumes.

Test plan

Added test_delete_branch_removes_branch_only_base_messages. It reproduces the issue script: create a main turn, branch from it, append branch-only messages, then delete_branch(..., force=True). After the call, the test asserts that:

  • the main-branch base rows (ids 1 and 2) remain,
  • the branch-only base rows (ids 3 and 4) are gone,
  • only main structure rows remain,
  • get_items(branch_id="main") returns the original main payload.
$ uv run pytest tests/extensions/memory/test_advanced_sqlite_session.py
============================== 37 passed in 0.20s ==============================
$ make format
All checks passed!
$ make lint
All checks passed!
$ uv run mypy src/agents/extensions/memory/advanced_sqlite_session.py tests/extensions/memory/test_advanced_sqlite_session.py
Success: no issues found in 2 source files

Issue number

Closes #3346. Adjacent #3348 (silent partial save in add_items) is fixed by a separate PR and not touched here.

Checks

  • I've added new tests (if relevant)
  • I've added/updated the relevant documentation
  • I've run `make lint` and `make format`
  • I've made sure tests pass

`delete_branch` removed `turn_usage` and `message_structure` rows for the
branch but never touched the underlying messages table. When a branch was the
only reference for a given message (typical for branch-only follow-ups added
after `create_branch_from_turn`), the base row stayed behind: invisible to
advanced reads, which join through `message_structure`, but still padding the
on-disk size and clouding any direct audit of the messages table.

After the structure rows are deleted, drop any base rows that no longer have a
matching `message_structure` entry inside the same transaction by reusing the
existing `_cleanup_orphaned_messages_sync` helper. Messages still referenced
by another branch (e.g. shared turns) are left alone. The log line now reports
the orphan count alongside the existing usage and structure counts.

Fixes openai#3346.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 98393785e7

ℹ️ 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".

# reference for. Without this step, branch-only rows become
# invisible to advanced reads (which join through
# `message_structure`) but linger in the base table.
orphans_deleted = self._cleanup_orphaned_messages_sync(conn)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid single-statement cleanup for large branch deletes

When deleting a branch with more branch-only messages than SQLite's host-parameter limit, this new call feeds every orphan id into _cleanup_orphaned_messages_sync, which builds one DELETE ... WHERE id IN (?, ...) statement. On SQLite builds with lower variable limits this raises OperationalError: too many SQL variables, so large branches cannot be deleted; chunk the cleanup or use a set-based DELETE ... NOT EXISTS query instead.

Useful? React with 👍 / 👎.

@seratch seratch added duplicate This issue or pull request already exists feature:sessions labels May 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

duplicate This issue or pull request already exists feature:sessions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

AdvancedSQLiteSession.delete_branch() leaves branch-only messages in the base table

2 participants