Severity: Medium · Verify confidence: high
File: apps/desktop/src/main/services/state/kvDb.ts:3054
What
The post-init runtime run() wrapper (kvDb.ts:3051-3064) detects an ALTER on a CRR table and calls crsql_begin_alter, runs the statement, then crsql_commit_alter. But its catch block merely throw error WITHOUT first calling crsql_commit_alter (lines 3054-3060). This diverges from the migrate-time wrapper makeMigrateDb (kvDb.ts:2930-2941) which deliberately commits the alter even on failure before re-throwing. crsql_begin_alter drops the table's change-capture triggers and enters alter mode; if commit_alter never runs, the CRR table is left mid-alter with its triggers removed. Services run runtime ALTERs through this path and swallow the rethrow — e.g. automationService.ensureSchema()/safeAlter() catches all errors silently (apps/desktop/src/main/services/automations/automationService.ts:912-917, target table automation_runs is CRR). If such an ALTER throws for any reason other than (already-guarded) duplicate-column — transient lock/SQLITE_BUSY, NOT NULL add quirk, etc. — the table stays in mid-alter state and every subsequent local write to it is no longer captured into crsql_changes and therefore stops replicating to peers until the next process restart re-runs ensureCrrTables/trigger repair. This is a mid-alter capture leak (the class #363 addressed) reintroduced on the runtime path.
Trigger
Cause a runtime alter table <crr_table> add column ... to throw (e.g. under DB contention/SQLITE_BUSY) via a service's safeAlter; begin_alter has run but commit_alter has not, capture triggers are gone, and writes to that table silently stop syncing until app restart.
Verification (adversarial)
Independently confirmed at the cited locations. The runtime run() wrapper (apps/desktop/src/main/services/state/kvDb.ts:3051-3064) calls crsql_begin_alter (3054) and only commits crsql_commit_alter on the success path (3060); its catch block (3057-3059) is catch (error) { throw error; } with NO commit_alter. This diverges from makeMigrateDb (2926-2944) whose catch DOES commit_alter before rethrowing (line 2937) — the exact asymmetry claimed. crsql_begin_alter drops the change-capture triggers; without commit_alter the table is left mid-alter with triggers removed, so writes stop landing in crsql_changes and stop replicating.\n\nTarget is a genuine CRR table: automation_runs has a text primary key (1870), is not in LOCAL_ONLY_CRR_EXCLUDED_TABLES (459-466), and is converted via crsql_as_crr by ensureCrrTables/listEligibleCrrTables — so the begin/commit_alter branch at 3053 is taken for its ALTERs. automationService.safeAlter (912-917) swallows all errors and ensureSchema (920+) guards each alter with columnExists, which pre-filters the duplicate-column case; therefore when safeAlter actually fires the column is genuinely missing and the ALTER truly executes (entering alter mode), so any non-duplicate failure (transient SQLITE_BUSY/lock, constraint quirk) leaks mid-alter.\n\nProduction reachability is confirmed, not test-only: both apps/desktop/src/main/main.ts:2898 and apps/ade-cli/src/bootstrap.ts:946 build createAutomationService({ db, ... }) on the raw openKvDb handle whose .run is this exact wrapper, and ensureSchema() runs eagerly at construction (automationService.ts:1051). crsqliteLoaded is true in normal production. The self-heal (ensureCrrTables -> tableNeedsCrrTriggerRepair < 3 triggers -> crsql_as_crr) runs ONLY at startup (kvDb.ts:3011) with no runtime re-trigger, so a leak persists until process restart — exactly as claimed.\n\nGit evidence: this is a re-introduction of the #363-class fix. The runtime-path fix exists on the unmerged branch origin/cursor/critical-correctness-bugs-bb8c (commit 6ebfaea) applied at line 3055 (the runtime wrapper); neither that nor the sibling commit c283594 is an ancestor of current HEAD, so HEAD genuinely lacks it while retaining only the migrate-time fix at line 2937.\n\nSeverity stays medium: the trigger requires a real schema-upgrade ALTER (missing column) to coincide with a transient failure — an uncommon conjunction — and the damage self-heals on restart with no data corruption; it only silently delays replication of that table's writes for the rest of the session.
Filed from the 2026-05-29 ADE codebase audit (adversarially verified).
Related: open PR #363 (cursor/critical-bug-detection-1798) fixes this exact runtime-path CRR alter leak — merging #363 resolves this.
Severity: Medium · Verify confidence: high
File:
apps/desktop/src/main/services/state/kvDb.ts:3054What
The post-init runtime run() wrapper (kvDb.ts:3051-3064) detects an ALTER on a CRR table and calls crsql_begin_alter, runs the statement, then crsql_commit_alter. But its catch block merely
throw errorWITHOUT first calling crsql_commit_alter (lines 3054-3060). This diverges from the migrate-time wrapper makeMigrateDb (kvDb.ts:2930-2941) which deliberately commits the alter even on failure before re-throwing. crsql_begin_alter drops the table's change-capture triggers and enters alter mode; if commit_alter never runs, the CRR table is left mid-alter with its triggers removed. Services run runtime ALTERs through this path and swallow the rethrow — e.g. automationService.ensureSchema()/safeAlter() catches all errors silently (apps/desktop/src/main/services/automations/automationService.ts:912-917, target table automation_runs is CRR). If such an ALTER throws for any reason other than (already-guarded) duplicate-column — transient lock/SQLITE_BUSY, NOT NULL add quirk, etc. — the table stays in mid-alter state and every subsequent local write to it is no longer captured into crsql_changes and therefore stops replicating to peers until the next process restart re-runs ensureCrrTables/trigger repair. This is a mid-alter capture leak (the class #363 addressed) reintroduced on the runtime path.Trigger
Cause a runtime
alter table <crr_table> add column ...to throw (e.g. under DB contention/SQLITE_BUSY) via a service's safeAlter; begin_alter has run but commit_alter has not, capture triggers are gone, and writes to that table silently stop syncing until app restart.Verification (adversarial)
Independently confirmed at the cited locations. The runtime run() wrapper (apps/desktop/src/main/services/state/kvDb.ts:3051-3064) calls crsql_begin_alter (3054) and only commits crsql_commit_alter on the success path (3060); its catch block (3057-3059) is
catch (error) { throw error; }with NO commit_alter. This diverges from makeMigrateDb (2926-2944) whose catch DOES commit_alter before rethrowing (line 2937) — the exact asymmetry claimed. crsql_begin_alter drops the change-capture triggers; without commit_alter the table is left mid-alter with triggers removed, so writes stop landing in crsql_changes and stop replicating.\n\nTarget is a genuine CRR table: automation_runs has a text primary key (1870), is not in LOCAL_ONLY_CRR_EXCLUDED_TABLES (459-466), and is converted via crsql_as_crr by ensureCrrTables/listEligibleCrrTables — so the begin/commit_alter branch at 3053 is taken for its ALTERs. automationService.safeAlter (912-917) swallows all errors and ensureSchema (920+) guards each alter with columnExists, which pre-filters the duplicate-column case; therefore when safeAlter actually fires the column is genuinely missing and the ALTER truly executes (entering alter mode), so any non-duplicate failure (transient SQLITE_BUSY/lock, constraint quirk) leaks mid-alter.\n\nProduction reachability is confirmed, not test-only: both apps/desktop/src/main/main.ts:2898 and apps/ade-cli/src/bootstrap.ts:946 build createAutomationService({ db, ... }) on the raw openKvDb handle whose .run is this exact wrapper, and ensureSchema() runs eagerly at construction (automationService.ts:1051). crsqliteLoaded is true in normal production. The self-heal (ensureCrrTables -> tableNeedsCrrTriggerRepair < 3 triggers -> crsql_as_crr) runs ONLY at startup (kvDb.ts:3011) with no runtime re-trigger, so a leak persists until process restart — exactly as claimed.\n\nGit evidence: this is a re-introduction of the #363-class fix. The runtime-path fix exists on the unmerged branch origin/cursor/critical-correctness-bugs-bb8c (commit 6ebfaea) applied at line 3055 (the runtime wrapper); neither that nor the sibling commit c283594 is an ancestor of current HEAD, so HEAD genuinely lacks it while retaining only the migrate-time fix at line 2937.\n\nSeverity stays medium: the trigger requires a real schema-upgrade ALTER (missing column) to coincide with a transient failure — an uncommon conjunction — and the damage self-heals on restart with no data corruption; it only silently delays replication of that table's writes for the rest of the session.Filed from the 2026-05-29 ADE codebase audit (adversarially verified).
Related: open PR #363 (
cursor/critical-bug-detection-1798) fixes this exact runtime-path CRR alter leak — merging #363 resolves this.