fix: prevent cross-session nested SQLite transactions during bootstrap#261
Conversation
…ed-transaction failures Fixes Martian-Engineering#260 Root cause: Multiple async sessions share one synchronous DatabaseSync handle. SQLite's transaction state is per-connection, so concurrent async code paths that both issue BEGIN while the other is mid-transaction (awaiting async work) cause 'cannot start a transaction within a transaction' errors. Fix: Introduce acquireTransactionLock() — a per-database async mutex using a WeakMap<DatabaseSync, promise-chain>. Applied to all three explicit transaction entry points: - ConversationStore.withTransaction() — BEGIN IMMEDIATE - SummaryStore.replaceContextRangeWithSummary() — BEGIN - lcm-doctor-apply.ts applyScopedDoctorRepair() — BEGIN IMMEDIATE The mutex serializes transaction acquisition per DB instance while allowing different databases to proceed independently. Includes regression tests covering: - Concurrent withTransaction from multiple sessions on one DB - Concurrent replaceContextRangeWithSummary calls - Cross-store (ConversationStore + SummaryStore) concurrent transactions - Error propagation without mutex deadlock - 10-session stress test - Independent database isolation
There was a problem hiding this comment.
Pull request overview
Introduces a per-database async mutex to serialize explicit SQLite transaction entry points across sessions, preventing cannot start a transaction within a transaction failures when multiple sessions share one DatabaseSync connection during bootstrap/other awaited transaction scopes.
Changes:
- Added
acquireTransactionLock(db)(per-DatabaseSyncmutex viaWeakMap) to serialize transaction entry. - Wrapped explicit transaction entry points (
ConversationStore.withTransaction,SummaryStore.replaceContextRangeWithSummary,applyScopedDoctorRepair) with the mutex. - Added regression tests covering same-DB serialization, cross-store contention, and basic concurrency stress.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
src/transaction-mutex.ts |
Adds per-DB async mutex used to serialize transaction entry across sessions. |
src/store/conversation-store.ts |
Wraps withTransaction() with the per-DB mutex to prevent cross-session nested BEGIN IMMEDIATE. |
src/store/summary-store.ts |
Wraps replaceContextRangeWithSummary() with the per-DB mutex to prevent nested BEGIN. |
src/plugin/lcm-doctor-apply.ts |
Wraps applyScopedDoctorRepair() transaction with the per-DB mutex. |
test/transaction-mutex.test.ts |
Adds regression coverage for concurrent transaction entry across sessions/stores. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
TLDR: last patch fix created permanent lockout loop potential for any agent using LCM. For users experiencing the bug, the fix is to switch to legacy and agent will become responsive again. Bug can be recreated via fast restart, /new or /reset. Extremely difficult to debug ie code looked right but timing issues were unforeseeable until tested in production. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
@jalehman discovered this bug last night after rollout. critical bug that locks user main agent up + subagents (if they are not isolated) and leaves them unresponsive without any log/errors. |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Summary
DatabaseSynchandleRoot cause
Lossless-Claw shares one SQLite
DatabaseSyncconnection across sessions, but transaction serialization was only per session. Some transaction scopes span awaited async work (especially bootstrap), so a second session could issueBEGINon the same connection while the first transaction was still open, triggering:cannot start a transaction within a transactionWhat changed
acquireTransactionLock(db)insrc/transaction-mutex.tsConversationStore.withTransaction()SummaryStore.replaceContextRangeWithSummary()applyScopedDoctorRepair()test/transaction-mutex.test.tsTesting
npx vitest run test/transaction-mutex.test.tsnpx vitest run test/summary-store.test.ts test/migration.test.ts test/fts5-sanitize.test.tsNotes / caveats