Repository navigation
fix(memory): reject a second base-table pair in one AdvancedSQLiteSession database - #4441
ayaangazali wants to merge 4 commits into
Conversation
…sion file The structure tables are hardcoded names created with CREATE TABLE IF NOT EXISTS, so the first sessions_table/messages_table pair to touch a database file owns them. A later session configured with different base table names reused message_structure and joined it against its own messages table, so both sessions read back duplicated history. Validate the scope in __init__ instead: read the existing message_structure foreign keys and raise when they belong to a different pair.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b6f2b1a92f
ℹ️ 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".
…eck both Address the automated review on this PR. SQLite resolves table identifiers case-insensitively while PRAGMA foreign_key_list reports the CREATE-time spelling, so reopening a file with a recased base-table pair raised ValueError for a configuration that is in fact the same pair. Verification now covers turn_usage as well as message_structure, and runs inside the BEGIN IMMEDIATE transaction that creates the layout, so a file whose structure tables were created for two different pairs is rejected instead of being accepted by whichever table happens to match.
|
Both Codex points were real. Fixed in f68b409. Identifier casing. This was a regression my patch introduced. SQLite resolves table names case-insensitively, but Split layout. Verification now checks Checked by hand on top of the existing test: recased pair accepted with correct history, a file where only
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f68b40940c
ℹ️ 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".
seratch
left a comment
There was a problem hiding this comment.
Thanks for the contribution. The underlying issue is real: in the released implementation, two custom base-table pairs sharing one database can silently duplicate or mix persisted history. Enforcing one pair per database is the right direction and is preferable to renaming every structure table.
Before another review, please reset the validation around one contract: create_tables=True must atomically create and claim a complete structure-table layout, while create_tables=False must accept only an already-existing complete layout owned by the configured pair. Please delegate identifier equality to SQLite's ASCII-only NOCASE semantics instead of Python casefold().
Please add regression tests for a no-create instance opened before ownership exists, ASCII recasing versus distinct non-ASCII identifiers, and controlled concurrent initialization with final foreign-key coherence. The current head still reproduces silent duplicated history in the first two cases.
…onfigured pair Address review feedback. Validation is now one contract. create_tables=True creates and claims the layout inside a single BEGIN IMMEDIATE transaction, and create_tables=False accepts only an already-existing layout owned by the configured pair. An incomplete or unclaimed layout is rejected, so a no-create session can no longer open a file before any pair owns it and then have another pair claim the structure tables underneath it. Identifier equality is delegated to SQLite's ASCII-only NOCASE collation instead of Python casefold(), which equated names SQLite keeps distinct such as 'sssessions' and the sharp-s spelling.
|
Thanks, that framing helped. Reset around the single contract in 6517db9. One contract.
Both owner-bearing tables, Identifier equality. Now delegated to SQLite via Tests, all three cases:
Four of the five fail against One note on how that concurrency test is written, since it looks over-specified otherwise. It pre-creates the file with WAL enabled before the race. Switching journal mode needs an exclusive lock, so two processes opening a brand-new file collide in Behavior change worth confirming:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6517db95f5
ℹ️ 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".
| with self._locked_connection() as conn: | ||
| self._claim_structure_tables(conn) |
There was a problem hiding this comment.
Validate ownership before creating the rejected base pair
When an existing file is owned by pair A and this constructor is called with pair B, super().__init__() has already created and committed B's sessions table, messages table, and index before this validation raises. The rejected construction therefore leaves persistent schema artifacts behind, potentially occupying names or conflicting with later migrations even though pair B was never accepted; preflight ownership before base-schema initialization or otherwise ensure the rejected initialization is rolled back.
AGENTS.md reference: AGENTS.md:L98-L98
Useful? React with 👍 / 👎.
Address the automated review on this PR. Rejecting a second base-table pair happens after SQLiteSession.__init__ has already created that pair's sessions table, messages table and index, so a refused construction left those artifacts in a file it was never allowed to use. Snapshot sqlite_master before the base schema is created and, when the claim is rejected, drop only the names this construction added.
|
Fair point, and my change is what made that state reachable, so it should clean up after itself. Fixed in the latest commit. I took the rollback option rather than preflighting before
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5151119c1f
ℹ️ 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".
| self.messages_table, | ||
| self.sessions_table, | ||
| ] | ||
| added = [name for name in candidates if name not in preexisting] |
There was a problem hiding this comment.
Preserve recased preexisting tables during cleanup
When one configured table name is a case-only variant of the existing owner and the other name is different (for example, existing FooSessions/FooMessages versus configured foosessions/other_messages), ownership validation correctly rejects the pair, but this exact, case-sensitive membership check classifies foosessions as newly created. Cleanup then executes DROP TABLE foosessions, which SQLite resolves to the preexisting FooSessions table, deleting the accepted owner's schema and data. Although validation now follows SQLite identifier casing, the new rollback cleanup does not; reject before base-schema creation or use SQLite-equivalent identity when determining what this construction actually created.
AGENTS.md reference: AGENTS.md:L98-L98
Useful? React with 👍 / 👎.
| kind = "INDEX" if name.startswith("idx_") else "TABLE" | ||
| conn.execute(f"DROP {kind} IF EXISTS {name}") |
There was a problem hiding this comment.
Preserve object types when cleaning custom tables
When a rejected configuration uses a valid custom sessions_table or messages_table name beginning with idx_, this prefix test treats that table as an index. DROP INDEX IF EXISTS then fails because the schema object is a table; the outer exception handler suppresses the cleanup error, leaving the rejected pair's base tables behind. Store each candidate together with its known schema-object type rather than inferring the type from its user-controlled name.
Useful? React with 👍 / 👎.
|
Resolved by #4454, which includes you as a co-author. |
Summary
AdvancedSQLiteSessioninherits the configurablesessions_table/messages_tablefromSQLiteSession, but its four structure tables (message_structure,turn_usage,branch_reservations,session_clear_generations) are hardcoded names created withCREATE TABLE IF NOT EXISTS(advanced_sqlite_session.py:150,:170). The first base-table pair to touch a database file therefore owns them, and every later session on that file silently reusesmessage_structurewith the first pair's foreign keys regardless of how it was configured.get_items()joinsmessage_structureagainst the configured messages table (advanced_sqlite_session.py:279,:297,:318), so once two pairs share one file each session's join picks up structure rows written by the other one:Two
message_structurerows now exist for sessions1, both withmessage_id = 1, and each session matches both of them against its own messages table. Nothing errors, the history is just wrong.turn_usagecollides the same way through itsUNIQUE(session_id, branch_id, user_turn_number)constraint.Namespacing the structure tables per base-table pair would mean renaming them in over 90 places in the module and would orphan the rows in every database file that already exists, so this takes the other direction: the layout really does only support one pair per file, so
__init__now checks that instead of corrupting data. It readsPRAGMA foreign_key_list(message_structure)and raises aValueErrornaming both pairs when the structure tables already belong to a different one.Supported configurations are untouched, since the check only fires on a mismatch that is already broken today. Verified by hand: default table names with two sessions on one file, a custom pair on its own file, reopening that file with
create_tables=Trueand withcreate_tables=False, a fresh file with no structure tables yet, and:memory:.Test plan
test_structure_tables_reject_a_second_base_table_pair. Reverting the source change makes it fail withDID NOT RAISE <class 'ValueError'>, so it covers the fix rather than restating it.uv run pytest tests/extensions/memory/ tests/memory/: 613 passed, 11 skipped..agents/skills/code-change-verification/scripts/run.sh: format and lint clean, pyright 0 errors, 8587 passed / 28 skipped.Issue number
N/A, found while reading through the session backends.
Checks
.agents/skills/code-change-verification/scripts/run.sh/reviewbefore submitting this PRsorry in advance if I got any of this wrong. I worked through the logic myself and used Claude Code as a sounding board for the design call (validate vs rename the tables), and I confirmed the duplication with the snippet above before writing anything. happy to be corrected on the approach, still a freshman in college trying to be useful here :)