Skip to content

COG-5975 fix(cache): unique constraint + upsert for cache_session_context so retried writes can't poison a session - #4229

Closed
Vasilije1990 wants to merge 1 commit into
devfrom
fix/4226-session-context-unique-constraint
Closed

COG-5975 fix(cache): unique constraint + upsert for cache_session_context so retried writes can't poison a session#4229
Vasilije1990 wants to merge 1 commit into
devfrom
fix/4226-session-context-unique-constraint

Conversation

@Vasilije1990

Copy link
Copy Markdown
Contributor

Description

Fixes #4226cache_session_context had no uniqueness on (user_id, session_id, entry_id) (unlike cache_qa_entries, which has uq_cache_qa_entry). The session-context write path is update-then-create across two transactions, so an ordinary client retry after a timeout — or two concurrent writers — inserted a duplicate row. Once duplicates existed, scalar_one_or_none() in SqlCacheAdapter.update_session_context_entry raised on every subsequent write for that session: a permanent 503 recoverable only by manual row deletion (the reporter hit 77 copies of one bookkeeping entry and ~36 failed writes/hour for 6.5 hours).

All four parts of the fix suggested in the issue:

  • Unique index uq_cache_session_context_entry on (user_id, session_id, entry_id) — declared as a unique Index rather than a UniqueConstraint so tables created before it existed can be upgraded in place.
  • Upsert on createcreate_session_context_entry now uses ON CONFLICT DO UPDATE (Postgres + SQLite dialects, same pattern as set_value), so a retried write replaces the payload in place. Id-less entries keep their synthetic UUID and are unaffected.
  • In-place migration_ensure_initialized collapses each duplicate group to its newest row, then runs CREATE UNIQUE INDEX IF NOT EXISTS. Migration failure is non-fatal: the adapter logs a warning and falls back to plain inserts so ON CONFLICT can never error against a missing index.
  • Self-healing updateupdate_session_context_entry fetches all rows for the key ordered by seq, merges into the newest, and prunes older duplicates in the same transaction, so even an un-migrated database recovers on the next write instead of 503ing forever.

The Redis and FS adapters don't share the crash (they update the first list match), so this fix is scoped to the SQL adapter. Follow-up noted in COG-5975: their create_session_context_entry still appends duplicates on retried creates (no crash, but duplicated context) — the adapter-parametrized contract tests from #4129 would be the natural home for cross-adapter idempotency coverage.

Testing

New regression tests in test_sql_adapter_crud.py (sqlite+aiosqlite):

  • retried create with the same entry id yields exactly one row with the retried payload
  • with legacy duplicates simulated (index dropped, rows stacked), update succeeds, merges into the newest row, and exactly one row remains
  • a legacy database is deduped (newest kept per key) and re-indexed on next adapter startup, after which upserts work

pytest cognee/tests/unit/infrastructure/databases/cache/ — 139 passed; cognee/tests/unit/infrastructure/session/ — 181 passed. Ruff lint + format clean.

Related

🤖 Generated with Claude Code

…t poison a session

COG-5975 / fixes #4226

cache_session_context had no unique constraint on (user_id, session_id,
entry_id), so a client retry of a timed-out write inserted a duplicate row,
after which scalar_one_or_none() in update_session_context_entry raised on
every write for that session — a permanent 503 with no self-healing path.

- Add unique index uq_cache_session_context_entry (as a unique Index so
  pre-existing tables can be upgraded in place)
- Make create_session_context_entry an ON CONFLICT DO UPDATE upsert on
  Postgres and SQLite; retried writes now replace the payload in place
- Migrate legacy tables on init: collapse duplicate groups to the newest
  row, then CREATE UNIQUE INDEX IF NOT EXISTS; failure is non-fatal and
  falls back to plain inserts
- Self-heal in update_session_context_entry: merge into the newest row and
  prune older duplicates instead of erroring until manual cleanup

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dexters1

dexters1 commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

@Vasilije1990 added a PR with the fix here:

#4232

Fix should be handled with an alembic migration of the relational DB

That also resolves the issue for existing users who had it

@dexters1 dexters1 closed this Jul 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants