Skip to content

fix(memory): backfill sessions row in hooks; add SQLite busy_timeout (#49)#51

Open
fazleelahhee wants to merge 2 commits intomainfrom
fix/memory-hooks-session-fk-and-busy-timeout
Open

fix(memory): backfill sessions row in hooks; add SQLite busy_timeout (#49)#51
fazleelahhee wants to merge 2 commits intomainfrom
fix/memory-hooks-session-fk-and-busy-timeout

Conversation

@fazleelahhee
Copy link
Copy Markdown
Contributor

Summary

Fixes #49.

Two related bugs in the memory-hook write path:

  1. FOREIGN KEY constraint failed on UserPromptSubmit / PostToolUse when those hooks arrive before SessionStart for a given session_id (cce serve started mid-Claude-Code-session, resumed session_id without re-fired SessionStart, dropped POST). Make each handler self-healing via a new _ensure_session() helper that does INSERT OR IGNORE INTO sessions before the FK-dependent inserts. INSERT OR IGNORE means a real SessionStart arriving later does not clobber the placeholder's started_at_epoch.

  2. sqlite3.OperationalError: database is locked in auto_prune_loop — WAL was already on but no busy_timeout, so contention with hot-path inserts crashed immediately instead of retrying. Set PRAGMA busy_timeout = 5000 in connect().

Test plan

  • tests/memory/test_hooks.py — new tests cover:
    • UserPromptSubmit without prior SessionStart succeeds and backfills the session row
    • PostToolUse without prior SessionStart succeeds and backfills the session row
    • Real SessionStart arriving after backfill does not overwrite the placeholder row
  • tests/memory/test_db.py — pin PRAGMA busy_timeout >= 1000ms on every new connection
  • Full memory test suite: 339 passed
  • Lint clean (ruff check)

Files

  • src/context_engine/memory/hooks.py_ensure_session() helper + calls in 4 handlers
  • src/context_engine/memory/db.pyPRAGMA busy_timeout = 5000 in connect()
  • tests/memory/test_hooks.py — 3 new tests
  • tests/memory/test_db.py — 1 new test

…49)

The lifecycle invariant — \"SessionStart fires before any other hook for
the same session_id\" — breaks in real deployments (cce serve started
mid-Claude-Code-session, resumed session_ids that don't re-fire
SessionStart, dropped POST). Each subsequent UserPromptSubmit and
PostToolUse then trips the FK on sessions(id) and crashes the handler:

    sqlite3.IntegrityError: FOREIGN KEY constraint failed

Make each handler self-healing: a new _ensure_session() helper does an
INSERT OR IGNORE INTO sessions before any insert that depends on the
parent row. The placeholder uses app[\"project_name\"] and the current
epoch; INSERT OR IGNORE means a real SessionStart arriving later does
not clobber the existing row's started_at.

Applied to UserPromptSubmit, PostToolUse, Stop, and SessionEnd. (Stop
writes only to pending_compressions which has no FK; SessionEnd's
UPDATE is a no-op on a missing row. Adding the backfill anyway keeps
the row queryable in dashboard listings.)

Separately, the same issue report includes
'sqlite3.OperationalError: database is locked' from the auto_prune
background task contending with hot-path inserts. WAL was already
enabled but no busy_timeout was set, so contention returned
SQLITE_BUSY immediately. Add PRAGMA busy_timeout = 5000 so writers
retry for up to 5s instead of crashing on the first collision.

Tests:
- UserPromptSubmit / PostToolUse without prior SessionStart succeed
  and create the session row
- Real SessionStart arriving after backfill keeps the original
  started_at_epoch (INSERT OR IGNORE semantics)
- PRAGMA busy_timeout >= 1000ms on every new connection

Closes #49.
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes two reliability issues in the memory hook write path: (1) FK failures when hook events arrive before SessionStart, and (2) SQLite lock contention causing immediate SQLITE_BUSY errors instead of waiting/retrying.

Changes:

  • Add _ensure_session() to backfill a placeholder sessions row before FK-dependent inserts in multiple hook handlers.
  • Configure SQLite connections with PRAGMA busy_timeout = 5000 to reduce “database is locked” failures under contention.
  • Add regression tests covering orphan hook events and verifying busy_timeout is set.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

File Description
src/context_engine/memory/hooks.py Adds _ensure_session() and calls it from multiple hook handlers to prevent FK failures when SessionStart hasn’t been seen.
src/context_engine/memory/db.py Sets PRAGMA busy_timeout = 5000 in connect() to handle write contention more gracefully.
tests/memory/test_hooks.py Adds regression tests for orphan UserPromptSubmit/PostToolUse and late SessionStart behavior.
tests/memory/test_db.py Adds a test that asserts busy_timeout is configured on new connections.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +60 to +62
handler proceed; the row will be reconciled if a real SessionStart
arrives later (INSERT OR IGNORE), and the session_id remains queryable
in the meantime.
Comment on lines +352 to +353
assert prompts == [{"prompt_number": 1, "prompt_text": "hi"}] \
if isinstance(prompts[0], dict) else len(prompts) == 1
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.

sqlite3.IntegrityError

3 participants