Skip to content

Centralize app persistence in SQLite and harden state/event flows - #86

Merged
juliusmarminge merged 1 commit into
codething/48364d50from
codething/142d0619
Feb 21, 2026
Merged

Centralize app persistence in SQLite and harden state/event flows#86
juliusmarminge merged 1 commit into
codething/48364d50from
codething/142d0619

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Feb 20, 2026

Copy link
Copy Markdown
Member

Summary

  • Move canonical app state to server-side SQLite (~/.t3/state.sqlite) via a new PersistenceService, covering projects, threads, messages, app settings, and state event sequencing.
  • Add DB foundations and compatibility layers (stateDb, migrations, sqlite adapter) and wire server startup/runtime to the new persistence path.
  • Introduce durable provider-event catch-up by sequence and strengthen WebSocket/state APIs for bootstrap, updates, and reconnect consistency.
  • Remove legacy renderer localStorage migration/schema paths; keep browser-scoped UX preferences separate from canonical state.
  • Tighten correctness around transactional/event behavior, pagination, thread update ownership, and fallback client flows.
  • Expand backend/web/contracts test coverage substantially for persistence, WS flows, state schemas, and reducer/settings behavior.
  • Add desktop backend launch runtime resolution and document the SQLite persistence + CR closure workstreams in new plan docs.

Testing

  • Not run (not provided in commit metadata): bun run lint
  • Not run (not provided in commit metadata): bun --cwd apps/server test
  • Not run (not provided in commit metadata): bun --cwd apps/web test
  • Not run (not provided in commit metadata): bun --cwd packages/contracts test

Open with Devin

Note

Medium Risk
Touches core provider streaming/persistence paths and introduces new replay logic; bugs could cause dropped/duplicated provider events or inconsistent UI state on reconnect.

Overview
Adds durable provider-event sequencing and replay. ProviderEvent now optionally includes a persisted seq, PersistenceService.ingestProviderEvent returns the stored event (or null when ignored), and a new PersistenceService.providerCatchUp endpoint pages provider events by sequence and reports lastProviderSeq.

Wires catch-up through server + client. WebSocket RPC adds providers.catchUp, ProviderManager centralizes provider event publishing to emit the persisted/seq-tagged version (and suppress emission when persistence dedupes), and the web client now tracks lastProviderSeq to replay missing provider events on reconnect/gaps for ordered, idempotent application.

Tests update/expand to assert monotonic provider seq assignment, correct catch-up ordering/filters, and propagation of persisted sequence metadata through ProviderManager and WebSocket.

Written by Cursor Bugbot for commit f8aadcf. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

Release Notes

  • New Features
    • Server-backed data persistence: projects, threads, and messages now stored centrally with enhanced durability
    • Automatic bidirectional state synchronization between client and server
    • App settings and preferences persist reliably across sessions
    • Terminal states, layouts, and configurations recover after restarts
    • Real-time state updates through event streaming
    • Enhanced thread and project management capabilities

@coderabbitai

coderabbitai Bot commented Feb 20, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Introduces a centralized SQLite persistence layer (.t3/state.sqlite) with a new PersistenceService for durable server-side state management of projects, threads, messages, and turn summaries. Establishes state event protocol (bootstrap, catchUp, listMessages) for client-server synchronization, removes legacy localStorage migration paths, and updates desktop/web clients to use server-authoritative persistence via new API surfaces.

Changes

Cohort / File(s) Summary
Planning & Strategy
.plans/11-centralized-sqlite-persistence-revamp.md, .plans/12-remove-legacy-localstorage-migrations-and-cr-hardening.md, .plans/README.md
Two comprehensive maintenance plans detailing SQLite persistence architecture, migration strategy, and legacy localStorage removal; registry entries added to README.
Server SQLite Persistence
apps/server/src/sqliteAdapter.ts, apps/server/src/sqliteAdapter.test.ts, apps/server/src/stateDb.ts, apps/server/src/stateDb.test.ts, apps/server/src/stateMigrations.ts
New cross-runtime SQLite adapter (Node.js/Bun), StateDb wrapper with transaction support, schema migration system, and comprehensive test coverage for database initialization and error handling.
Server Persistence Service
apps/server/src/persistenceService.ts, apps/server/src/persistenceService.test.ts
Introduces PersistenceService with full state lifecycle management (projects, threads, messages, turn diffs), event emission, session binding, legacy import, and extensive integration tests validating snapshot/catch-up flows and robustness.
Server Bootstrap & Integration
apps/server/src/index.ts, apps/server/src/wsServer.ts, apps/server/src/wsServer.test.ts
Wires PersistenceService into server startup, replaces ProjectRegistry with persistence backing, adds state event broadcasting, bootstrapping routes, and new thread/app settings WS handlers.
Server Event Handling
apps/server/src/providerManager.ts, apps/server/src/providerManager.test.ts
Integrates persistence checkpoints, session-thread binding, and durable provider event sequencing; adds publishProviderEvent and checkpoint diff persistence paths.
Server Dependency & Refactoring
apps/server/src/projectRegistry.ts, apps/server/src/projectRegistry.test.ts, apps/server/package.json
Delegates ProjectRegistry operations to PersistenceService backend, adds close() cleanup; adds @pierre/diffs dependency for diff support.
Contract Schemas & API Surface
packages/contracts/src/state.ts, packages/contracts/src/state.test.ts, packages/contracts/src/appSettings.ts, packages/contracts/src/appSettings.test.ts
Comprehensive Zod schemas for state domain (messages, threads, turn diffs, bootstrap, catch-up events) and app settings with strong type inference; new test coverage.
WS Protocol & IPC Extensions
packages/contracts/src/ws.ts, packages/contracts/src/ws.test.ts, packages/contracts/src/ipc.ts, packages/contracts/src/provider.ts, packages/contracts/src/provider.test.ts, packages/contracts/src/index.ts
Defines new WS methods/channels (stateBootstrap, stateCatchUp, appSettings, threads.*, stateEvent), extends NativeApi with state/threads/appSettings namespaces, adds provider catch-up and optional metadata fields.
Web App Settings Management
apps/web/src/appSettings.ts, apps/web/src/appSettings.test.ts
Replaces single-source appSettings with dual-backend/local model; adds hydration, optimistic updates, caching, reconciliation logic, and snapshot generation.
Web Persistence Schema Removal
apps/web/src/persistenceSchema.ts, apps/web/src/persistenceSchema.test.ts
Completely removes legacy schema migrations, validation, and persistence layer (399 lines deleted); no longer persists state to localStorage.
Web Store & State Management
apps/web/src/store.ts, apps/web/src/store.test.ts
Adds HYDRATE_FROM_SERVER and APPLY_STATE_EVENT actions for server-driven hydration; extends AppState with runtimeMode; preserves terminal state and turn summaries on upserts; new test coverage.
Web Client Bootstrap & Routing
apps/web/src/routes/__root.tsx
Replaces EventRouter/project bootstrap with StateSyncRouter; implements server state bootstrap, event hydration, provider catch-up with sequencing and replay, local fallback thread/project creation.
Web Client Components
apps/web/src/components/ChatView.tsx, apps/web/src/components/Sidebar.tsx, apps/web/src/components/Sidebar.logic.ts, apps/web/src/components/Sidebar.logic.test.ts, apps/web/src/components/ThreadTerminalDrawer.tsx
Adds terminal state synchronization, thread visitation tracking, clientMessageId generation, API-driven thread/project operations with offline fallbacks, and hydration-aware event handling.
Web API Layer
apps/web/src/wsNativeApi.ts
Expands NativeApi with state (bootstrap, listMessages, catchUp, onEvent), appSettings (get, update), and threads (create, delete, markVisited, updateTerminalState/Model/Title/Branch) namespaces.
Desktop Runtime
apps/desktop/src/main.ts
Introduces resolveBackendLaunch abstraction to support configurable backend runtime (electron-node, custom via T3CODE_BACKEND_RUNTIME) instead of hardcoded process.execPath.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Web Client
    participant WS as WebSocket Server
    participant Persistence as PersistenceService
    participant StateDb as SQLite State DB

    Client->>WS: state.bootstrap()
    WS->>Persistence: loadSnapshot()
    Persistence->>StateDb: Query projects, threads, messages
    StateDb-->>Persistence: Return snapshot data
    Persistence-->>WS: StateBootstrapResult
    WS-->>Client: Bootstrap complete

    Client->>Client: HYDRATE_FROM_SERVER

    loop Event Synchronization
        Persistence->>StateDb: Emit stateEvent (upsert/delete)
        StateDb-->>Persistence: Transaction committed
        Persistence->>WS: emit('stateEvent')
        WS->>Client: broadcast stateEvent
        Client->>Client: APPLY_STATE_EVENT
    end
Loading
sequenceDiagram
    participant Provider as Provider/ProviderManager
    participant Persistence as PersistenceService
    participant StateDb as SQLite State DB
    participant WS as WebSocket Server

    Provider->>Persistence: ingestProviderEvent(event)
    Persistence->>StateDb: Transactional write (message, turn summary, state event)
    StateDb-->>Persistence: Transaction committed
    Persistence->>Persistence: Emit stateEvent
    Persistence->>WS: Broadcast stateEvent
    WS->>Client: state.event (with seq metadata)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main architectural change: centralized SQLite persistence and hardened state/event flows, which is the primary focus of this large PR introducing PersistenceService and related infrastructure.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch codething/142d0619

Comment @coderabbitai help to get the list of available commands and usage tips.

@macroscopeapp

macroscopeapp Bot commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

Centralize server state in SQLite via PersistenceService and route WebSocket RPCs for state, threads, app settings, and provider catch-up through apps/server/src/wsServer.ts

Replace file-based persistence with a SQLite-backed PersistenceService, wire it into server startup and WebSocket RPC routing, broadcast WS_CHANNELS.stateEvent, and add client/server state bootstrap and catch-up flows alongside thread CRUD and app settings APIs. Core entry points are updated in apps/server/src/index.ts, apps/server/src/wsServer.ts, and the SQLite layer in apps/server/src/stateDb.ts with migrations in apps/server/src/stateMigrations.ts. The web client consumes the new APIs in apps/web/src/wsNativeApi.ts and synchronizes state in apps/web/src/routes/__root.tsx.

📍Where to Start

Begin with server wiring in createServer within apps/server/src/wsServer.ts, then review main in apps/server/src/index.ts for PersistenceService initialization and DB path resolution, followed by the DB/migrations in apps/server/src/stateDb.ts and apps/server/src/stateMigrations.ts.


📊 Macroscope summarized 451b295. 22 files reviewed, 76 issues evaluated, 1 issue filtered, 6 comments posted. View details

@greptile-apps

greptile-apps Bot commented Feb 20, 2026

Copy link
Copy Markdown

Greptile Summary

Migrated canonical app state from client-side localStorage to server-side SQLite (~/.t3/state.sqlite) via a new PersistenceService. This centralizes projects, threads, messages, app settings, and event sequencing in a durable database with transactional consistency.

Key Changes:

  • Added PersistenceService (1900+ lines) handling all state CRUD operations with event sourcing via state_events table
  • Removed 399 lines of client-side persistence schema and localStorage migrations from apps/web/src/persistenceSchema.ts
  • Implemented sequence-based catch-up protocol for both state events and provider events to handle reconnects and missed updates
  • Client now bootstraps from server via state.bootstrap() and applies incremental updates through APPLY_STATE_EVENT actions
  • Provider events are deduplicated via INSERT OR IGNORE on event ID and enriched with sequence numbers for replay
  • Added runtime SQLite adapter supporting both Node.js 22+ (node:sqlite) and Bun (bun:sqlite)
  • Desktop backend resolves correct runtime and passes dbPath to server initialization
  • Comprehensive test coverage: 471 lines for persistence service, 330+ for WebSocket integration, 162 for client reducer

Implementation Quality:

  • Proper WAL mode, foreign keys, and transactional writes with BEGIN IMMEDIATE
  • Thread-to-session binding tracked in-memory with fallback to DB queries via codexThreadId
  • Turn diff summaries captured from filesystem checkpoints and stored separately from thread documents
  • Normalized terminal state (IDs, groups, running terminals) with validation at persistence layer
  • Legacy projects.json migration with backup and metadata tracking

Confidence Score: 4/5

  • This is a well-structured architectural refactor with strong test coverage and careful migration handling
  • Score reflects solid implementation of centralized persistence with event sourcing, comprehensive test coverage (471+ new test lines), proper transaction handling, and thoughtful deduplication logic. Minor concerns around runtime error recovery paths and edge cases in concurrent event processing prevent a perfect score.
  • Pay close attention to apps/server/src/persistenceService.ts (complex event ingestion logic) and apps/web/src/routes/__root.tsx (catch-up sequencing)

Important Files Changed

Filename Overview
apps/server/src/persistenceService.ts New 1900+ line service centralizing all app state (projects, threads, messages, settings) in SQLite with event sourcing, transactional writes, and deduplication logic
apps/server/src/stateDb.ts Database wrapper providing transaction support and migration execution for SQLite state storage
apps/server/src/sqliteAdapter.ts Runtime adapter supporting both Node.js 22+ node:sqlite and Bun bun:sqlite with unified interface
apps/server/src/stateMigrations.ts Schema v1 migration creating documents, provider_events, state_events, and metadata tables with proper indexes and WAL mode
apps/server/src/wsServer.ts Routes all state/thread/settings RPC methods through PersistenceService, adds state event WebSocket channel, handles checkpoint revert persistence
apps/server/src/providerManager.ts Integrates persistence for provider events with deduplication, session-thread binding, turn summaries, and checkpoint diff capture
apps/web/src/store.ts Removed localStorage persistence, added HYDRATE_FROM_SERVER and APPLY_STATE_EVENT actions for server-driven state sync with event replay
apps/web/src/routes/__root.tsx Bootstrap state from server via state.bootstrap, implement catch-up flow for both state and provider events with sequence tracking
apps/web/src/persistenceSchema.ts Deleted 399 lines - removed all client-side localStorage schema, migration, hydration logic
packages/contracts/src/state.ts New shared schemas for StateBootstrap, StateCatchUp, StateEvent, threads CRUD, and messages with proper Zod validation

Sequence Diagram

sequenceDiagram
    participant Client as Web Client
    participant WS as WebSocket Server
    participant PS as PersistenceService
    participant DB as SQLite DB
    participant PM as ProviderManager

    Note over Client,DB: Initial Bootstrap Flow
    Client->>WS: state.bootstrap()
    WS->>PS: loadSnapshot()
    PS->>DB: SELECT projects, threads, messages
    DB-->>PS: State snapshot + lastStateSeq
    PS-->>WS: StateBootstrapResult
    WS-->>Client: {projects, threads, lastStateSeq}
    Client->>Client: dispatch(HYDRATE_FROM_SERVER)

    Note over Client,DB: Provider Event Ingestion
    PM->>PS: ingestProviderEvent(event)
    PS->>DB: INSERT OR IGNORE into provider_events
    PS->>DB: UPDATE thread, message, turn_summary
    PS->>DB: INSERT state_event
    DB-->>PS: event.seq
    PS->>PS: emit('stateEvent', event)
    PS-->>PM: ProviderEvent with seq
    PM->>WS: emit('event', providerEvent)
    WS->>Client: push providers.event
    WS->>Client: push state.event
    Client->>Client: dispatch(APPLY_STATE_EVENT)

    Note over Client,DB: Catch-Up After Reconnect
    Client->>WS: state.catchUp({afterSeq: lastSeq})
    WS->>PS: catchUp(afterSeq)
    PS->>DB: SELECT from state_events WHERE seq > ?
    DB-->>PS: Missing events
    PS-->>WS: {events, lastStateSeq}
    WS-->>Client: StateCatchUpResult
    Client->>Client: Replay missing events

    Note over Client,DB: Thread Operations
    Client->>WS: threads.create({projectId, title})
    WS->>PS: createThread(input)
    PS->>DB: BEGIN IMMEDIATE
    PS->>DB: INSERT thread document
    PS->>DB: INSERT state_event
    PS->>DB: COMMIT
    PS->>PS: emit('stateEvent')
    PS-->>WS: {thread}
    WS->>Client: push state.event
    WS-->>Client: ThreadsUpdateResult
    Client->>Client: dispatch(APPLY_STATE_EVENT)
Loading

Last reviewed commit: 451b295

const input = stateCatchUpInputSchema.parse(raw);
const rows = this.db
.prepare(
"SELECT seq, event_type, entity_id, payload_json, created_at FROM state_events WHERE seq > ? ORDER BY seq ASC;",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium

src/persistenceService.ts:775 This query has no LIMIT clause, so a low afterSeq could load the entire event history into memory. Consider adding pagination or a LIMIT to prevent potential OOM issues, or document why unbounded results are acceptable here.

🚀 Want me to fix this? Reply ex: "fix it for me".

🤖 Prompt for AI
In file apps/server/src/persistenceService.ts around line 775:

This query has no `LIMIT` clause, so a low `afterSeq` could load the entire event history into memory. Consider adding pagination or a `LIMIT` to prevent potential OOM issues, or document why unbounded results are acceptable here.

Evidence trail:
apps/server/src/persistenceService.ts lines 773-778 at commit 451b295d56e35deb8970760592be311e0f4c457c - shows SQL query `SELECT seq, event_type, entity_id, payload_json, created_at FROM state_events WHERE seq > ? ORDER BY seq ASC;` with no LIMIT clause, results loaded via `.all(input.afterSeq)`.

const raw = fs.readFileSync(normalizedPath, "utf8");
const payload = JSON.parse(raw) as { projects?: unknown };
const candidates = Array.isArray(payload.projects) ? payload.projects : [];
for (const candidate of candidates) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium

src/persistenceService.ts:1386 The try/catch wraps the entire loop, so one failing project aborts all remaining imports. Consider moving the try/catch inside the for loop so each candidate is handled independently.

🚀 Want me to fix this? Reply ex: "fix it for me".

🤖 Prompt for AI
In file apps/server/src/persistenceService.ts around line 1386:

The `try/catch` wraps the entire loop, so one failing project aborts all remaining imports. Consider moving the `try/catch` inside the `for` loop so each candidate is handled independently.

Evidence trail:
apps/server/src/persistenceService.ts lines 1382-1405 at commit 451b295d56e35deb8970760592be311e0f4c457c. The `try` block starts at line 1382, the `for (const candidate of candidates)` loop runs from approximately line 1387-1402, and the `catch` block is at line 1403. This confirms the try/catch wraps the entire loop.

}
}

if (!hasOptimisticWriteSinceHydration()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 High

src/appSettings.ts:169 Race condition: legacy settings are removed unconditionally at line 184, but the migration that persists them (lines 172-176) may be skipped if hasOptimisticWriteSinceHydration() is true. Consider only removing legacy settings when the migration actually ran.

🚀 Want me to fix this? Reply ex: "fix it for me".

🤖 Prompt for AI
In file apps/web/src/appSettings.ts around line 169:

Race condition: legacy settings are removed unconditionally at line 184, but the migration that persists them (lines 172-176) may be skipped if `hasOptimisticWriteSinceHydration()` is true. Consider only removing legacy settings when the migration actually ran.

Evidence trail:
apps/web/src/appSettings.ts lines 160-195 at commit 451b295d56e35deb8970760592be311e0f4c457c. The `persistLocalSettings` call (lines 172-176) is inside the `if (!hasOptimisticWriteSinceHydration())` block (line 169), while `window.localStorage.removeItem(LEGACY_APP_SETTINGS_STORAGE_KEY)` (around line 184) is outside that conditional, after `backendHydrated = true` is set.

Comment on lines +92 to +93
} catch (error) {
db.exec("ROLLBACK;");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium

src/stateMigrations.ts:92 If ROLLBACK throws (e.g., closed connection), the original error is masked. Consider wrapping the rollback in a try-catch to preserve the original error.

-  } catch (error) {
-    db.exec("ROLLBACK;");
+  } catch (error) {
+    try {
+      db.exec("ROLLBACK;");
+    } catch {
+      // Ignore rollback failure to preserve original error
+    }

🚀 Want me to fix this? Reply ex: "fix it for me".

🤖 Prompt for AI
In file apps/server/src/stateMigrations.ts around lines 92-93:

If `ROLLBACK` throws (e.g., closed connection), the original error is masked. Consider wrapping the rollback in a try-catch to preserve the original error.

Evidence trail:
apps/server/src/stateMigrations.ts lines 91-95 at commit 451b295d56e35deb8970760592be311e0f4c457c. The catch block at line 92-95 shows `db.exec("ROLLBACK;");` followed by `throw error;` with no try-catch around the ROLLBACK call.

Comment on lines +5 to +10
export function applyStateDbPragmas(db: SqliteDatabase): void {
db.exec("PRAGMA journal_mode=WAL;");
db.exec("PRAGMA synchronous=FULL;");
db.exec("PRAGMA busy_timeout=5000;");
db.exec("PRAGMA foreign_keys=ON;");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium

src/stateMigrations.ts:5 Consider setting busy_timeout before journal_mode=WAL since changing to WAL requires an exclusive lock and will fail immediately with SQLITE_BUSY if another process holds the lock.

Suggested change
export function applyStateDbPragmas(db: SqliteDatabase): void {
db.exec("PRAGMA journal_mode=WAL;");
db.exec("PRAGMA synchronous=FULL;");
db.exec("PRAGMA busy_timeout=5000;");
db.exec("PRAGMA foreign_keys=ON;");
}
export function applyStateDbPragmas(db: SqliteDatabase): void {
db.exec("PRAGMA busy_timeout=5000;");
db.exec("PRAGMA journal_mode=WAL;");
db.exec("PRAGMA synchronous=FULL;");
db.exec("PRAGMA foreign_keys=ON;");
}

🚀 Want me to fix this? Reply ex: "fix it for me".

🤖 Prompt for AI
In file apps/server/src/stateMigrations.ts around lines 5-10:

Consider setting `busy_timeout` before `journal_mode=WAL` since changing to WAL requires an exclusive lock and will fail immediately with `SQLITE_BUSY` if another process holds the lock.

Evidence trail:
apps/server/src/stateMigrations.ts lines 5-10 at commit 451b295d56e35deb8970760592be311e0f4c457c - shows `journal_mode=WAL` on line 6 and `busy_timeout=5000` on line 8, confirming the ordering issue described in the claim.


updateProjectScripts(raw: ProjectUpdateScriptsInput): ProjectUpdateScriptsResult {
const input = projectUpdateScriptsInputSchema.parse(raw);
const existing = this.getProjectById(input.id);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Medium

src/persistenceService.ts:588 Suggestion: Make read–modify–write operations atomic. getProjectById and getThreadById are called before entering withTransaction, so another writer can change the row between the read and the write, losing updates. Consider moving the reads into the same withTransaction block (or using a single atomic upsert) to avoid races.

🚀 Want me to fix this? Reply ex: "fix it for me".

🤖 Prompt for AI
In file apps/server/src/persistenceService.ts around line 588:

Suggestion: Make read–modify–write operations atomic. `getProjectById` and `getThreadById` are called before entering `withTransaction`, so another writer can change the row between the read and the write, losing updates. Consider moving the reads into the same `withTransaction` block (or using a single atomic upsert) to avoid races.

Evidence trail:
apps/server/src/persistenceService.ts line 587: `const existing = this.getProjectById(input.id);` (read before transaction)
apps/server/src/persistenceService.ts line 597: `this.withTransaction((pendingEvents) => {` (transaction starts after read)
apps/server/src/persistenceService.ts lines 1343-1358: `updateThreadWith` method definition
apps/server/src/persistenceService.ts line 1347: `const existing = this.getThreadById(threadId);` (read before transaction)
apps/server/src/persistenceService.ts line 1353: `this.withTransaction((pendingEvents) => {` (transaction starts after read)

@juliusmarminge
juliusmarminge changed the base branch from main to codething/48364d50 February 20, 2026 17:03
@macroscopeapp

macroscopeapp Bot commented Feb 20, 2026

Copy link
Copy Markdown
Contributor

Add durable provider event sequencing backed by SQLite and expose providers.catchUp RPC for ordered replay across server and web client

Persist provider events with a durable seq, return the persisted event from PersistenceService.ingestProviderEvent, add PersistenceService.providerCatchUp for paging with afterSeq and limit, route WS_METHODS.providersCatchUp to this method, have ProviderManager.publishProviderEvent gate emission on persistence and propagate seq, and update the web client to queue, validate, and catch up events using providers.catchUp.

📍Where to Start

Start with PersistenceService.providerCatchUp and ingestProviderEvent in persistenceService.ts, then review ProviderManager.publishProviderEvent in providerManager.ts, and the providers.catchUp handler in wsServer.ts.


📊 Macroscope summarized f8aadcf. 6 files reviewed, 7 issues evaluated, 1 issue filtered, 2 comments posted. View details

- Persist provider event `seq` metadata and return persisted events from ingestion
- Add `providers.catchUp` WS/API contract and server persistence query
- Replay missing provider events on web reconnect using ordered sequence catch-up
- Extend server/contracts tests for sequence ordering and catch-up behavior
@juliusmarminge
juliusmarminge merged commit df5e8c8 into codething/48364d50 Feb 21, 2026
3 of 4 checks passed
}
};

const enqueueProviderWork = (work: () => Promise<void>) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 High routes/__root.tsx:260

The queued async work in providerQueueRef continues executing after effect cleanup. Consider adding a disposed flag (similar to the bootstrap effect) and checking it before calling applyProviderEvent or updating refs, to prevent stale closures from dispatching after unmount or re-subscription.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/web/src/routes/__root.tsx around line 260:

The queued async work in `providerQueueRef` continues executing after effect cleanup. Consider adding a `disposed` flag (similar to the bootstrap effect) and checking it before calling `applyProviderEvent` or updating refs, to prevent stale closures from dispatching after unmount or re-subscription.

Evidence trail:
apps/web/src/routes/__root.tsx lines 144, 214-298 (commit f8aadcff890d5bc7a8741d048d9c1d034bb91333): `providerQueueRef` defined at line 144, `enqueueProviderWork` at lines 260-262 queues async work, `replayProviderCatchUp` at lines 239-257 calls `applyProviderEvent()` after async operations, cleanup at lines 294-297 only calls unsubscribe functions with no disposed flag. Compare to bootstrap effect at lines 146-179 which has `disposed` flag at line 148 and checks at lines 155, 162.

Comment on lines +253 to +256
}

if (lastProviderSeqRef.current < catchUp.lastProviderSeq) {
await replayProviderCatchUp();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟠 High routes/__root.tsx:253

If all events in catchUp.events are skipped (filtered out at lines 249-250), lastProviderSeqRef.current stays unchanged while the recursion check at line 255 still triggers, causing infinite recursion. Consider updating lastProviderSeqRef.current to catchUp.lastProviderSeq after the loop to ensure progress.

-        lastProviderSeqRef.current = missingSeq;
-      }
-
-      if (lastProviderSeqRef.current < catchUp.lastProviderSeq) {
+        lastProviderSeqRef.current = missingSeq;
+      }
+      lastProviderSeqRef.current = Math.max(lastProviderSeqRef.current, catchUp.lastProviderSeq);
+
+      if (lastProviderSeqRef.current < catchUp.lastProviderSeq) {
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file apps/web/src/routes/__root.tsx around lines 253-256:

If all events in `catchUp.events` are skipped (filtered out at lines 249-250), `lastProviderSeqRef.current` stays unchanged while the recursion check at line 255 still triggers, causing infinite recursion. Consider updating `lastProviderSeqRef.current` to `catchUp.lastProviderSeq` after the loop to ensure progress.

Evidence trail:
apps/web/src/routes/__root.tsx lines 240-257 at commit f8aadcff890d5bc7a8741d048d9c1d034bb91333: Line 243-246 handles empty events case with early return and update. Lines 248-253 show loop with `continue` condition that can skip events without updating `lastProviderSeqRef.current`. Lines 254-256 show recursion check that can trigger when `lastProviderSeqRef.current < catchUp.lastProviderSeq` even if no events were processed in the loop.

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