Skip to content

feat: durable SQLite event store for session persistence#79

Merged
dimakis merged 11 commits intomainfrom
feat/event-store
Apr 4, 2026
Merged

feat: durable SQLite event store for session persistence#79
dimakis merged 11 commits intomainfrom
feat/event-store

Conversation

@dimakis
Copy link
Copy Markdown
Owner

@dimakis dimakis commented Apr 4, 2026

Summary

  • Adds a SQLite-backed event store (better-sqlite3, WAL mode) that durably persists every v2 protocol event with a global monotonic sequence number
  • Client tracks lastSeq and sends it on reconnect — server replays only missed events from the store, replacing four fragile recovery paths (in-memory buffer, detached buffer, localStorage cache, SDK JSONL reconstruction)
  • Removes ~540 lines of dead recovery code (client/server buffers, localStorage caching, restoredViaReattach flag) and increases detach TTL from 10min to 1hr

What changed

Area Added Removed
Server EventStore class, append() in query loop, seq-based reattach replay, events API endpoint, replayEventsToMessages() detachedBuffer, bufferDetached(), drainDetachedBuffer(), DETACHED_BUFFER_MAX
Frontend lastSeq tracking in WS pool messageBuffer, wsDrainBuffer(), BUFFERABLE_TYPES, WS_MAX_BUFFER_SIZE, localStorage cache, restoredViaReattach
Fallback SDK JSONL reconstruction kept for pre-migration sessions

How it fixes each failure mode

Failure Before After
10-min detach TTL Session aborted TTL → 1hr, events survive regardless
Buffer overflow Messages silently dropped Append-only log, no cap
Server crash All in-memory state lost SQLite WAL survives crashes
Dual-source conflicts localStorage vs API races Single source: event store
Message ordering Multiple finalization guards Events stored in seq order

Test plan

  • 21 new EventStore tests (:memory: DB)
  • 4 new query-loop tests (store receives events, seq injection, detached persistence, backward compat)
  • All 380 existing tests pass
  • Manual: send message, kill server mid-stream, restart — session resumes
  • Manual: lock phone 15min, unlock — messages replay from lastSeq
  • Manual: open past session — loads from event store

🤖 Generated with Claude Code

dimakis and others added 11 commits April 4, 2026 17:32
Durable append-only event log backed by better-sqlite3. Every v2
protocol event will be persisted with a global sequence number,
enabling deterministic replay on reconnect instead of fragile
in-memory buffers.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
sendOrBuffer now appends every v2 event to the durable store before
sending over WebSocket. Each event gets a monotonic seq number
injected into the payload. Store parameter is optional for backward
compatibility — existing callers without a store continue to work.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Create EventStore instance at startup backed by .mitzo/events.db.
Pass store to runQueryLoop, which now calls upsertSession() when the
SDK assigns a session ID and markSessionInactive() when the query
loop ends. Sessions appear in the durable store immediately on
creation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a client reattaches with lastSeq, the server replays all events
after that sequence number from the durable EventStore instead of
relying on the in-memory detached buffer. Falls back to buffer-based
replay for clients that don't send lastSeq (backward compat).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
GET /api/sessions/:id/events?after=N returns all v2 events after the
given sequence number. This provides an HTTP fallback for event replay
when WebSocket reattach isn't possible.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Pool entries now track the highest seq seen from any v2 event.
On reconnect, lastSeq is included in the reattach request so the
server can replay only missed events from the durable store.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove all localStorage read/write for message caching and the
restoredViaReattach flag. Session restore now relies solely on the
API (backed by the durable event store) — no more stale cache races.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Messages arriving while unmounted are now dropped — recovery happens
via seq-based replay from the durable event store on reconnect.
Removes messageBuffer, wsDrainBuffer, BUFFERABLE_TYPES, WS_MAX_BUFFER_SIZE,
and CHAT_CACHE_KEY_PREFIX.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
With the durable event store, detached sessions no longer need
in-memory buffering — missed events replay from SQLite on reattach.
Removes bufferDetached(), drainDetachedBuffer(), DETACHED_BUFFER_MAX.
Increases DETACHED_TTL_MS from 10min to 1hr since detached sessions
are now cheap (no buffer memory pressure).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
getMessages() now replays v2 events from the durable event store as
the primary path. Falls back to SDK JSONL reconstruction for sessions
that predate the event store migration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cast WsMsg via unknown for seq field access. Use registry.get()!
directly instead of casting to Mock type.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@dimakis dimakis merged commit e3cf703 into main Apr 4, 2026
1 check passed
@dimakis dimakis deleted the feat/event-store branch April 4, 2026 17:05
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.

1 participant