Skip to content

Commit 8bd0a0a

Browse files
cursoragentarul28
andcommitted
Fix synced queue wipe replication and preserve dirty file session buffers
The stacked-PR queue overhaul migration deleted queue_landing_state while CRR triggers were active, replicating DELETE tombstones to peers that had not upgraded yet. Strip CRR metadata before the one-shot local wipe and run it after migrations but before ensureCrrTables re-registers replication. Files tab session caching dropped unsaved buffers over 8 MiB when switching project/lane scope. Always retain dirty tabs in the session snapshot so large in-flight edits are not lost silently on tab reentry. Co-authored-by: Arul Sharma <arul28@users.noreply.github.com>
1 parent c2f28f8 commit 8bd0a0a

3 files changed

Lines changed: 117 additions & 28 deletions

File tree

apps/desktop/src/main/services/state/kvDb.sync.test.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,59 @@ describe.skipIf(!isCrsqliteAvailable())("kvDb sync foundation", () => {
178178
repaired.close();
179179
});
180180

181+
it("does not replicate queue_landing_state overhaul wipe deletes to synced peers", async () => {
182+
const dbPathA = makeDbPath("ade-kvdb-sync-queue-wipe-a-");
183+
const dbA = await openKvDb(dbPathA, createLogger() as any);
184+
const projectId = "project-queue-wipe";
185+
const groupId = "group-queue-wipe";
186+
const queueId = "queue-wipe-1";
187+
188+
dbA.run(
189+
`insert into projects(id, root_path, display_name, default_base_ref, created_at, last_opened_at)
190+
values (?, ?, ?, ?, ?, ?)`,
191+
[projectId, "/repo/queue-wipe", "Queue Wipe", "main", "2026-03-15T00:00:00.000Z", "2026-03-15T00:00:00.000Z"],
192+
);
193+
dbA.run(
194+
`insert into pr_groups(id, project_id, group_type, name, auto_rebase, ci_gating, target_branch, created_at)
195+
values (?, ?, ?, ?, 0, 0, ?, ?)`,
196+
[groupId, projectId, "stack", "Stack", "main", "2026-03-15T00:00:00.000Z"],
197+
);
198+
dbA.run(
199+
`insert into queue_landing_state(
200+
id, group_id, project_id, state, entries_json, config_json, current_position, started_at
201+
) values (?, ?, ?, ?, ?, ?, 0, ?)`,
202+
[queueId, groupId, projectId, "active", "[]", "{}", "2026-03-15T00:00:00.000Z"],
203+
);
204+
205+
const dbB = await openKvDb(makeDbPath("ade-kvdb-sync-queue-wipe-b-"), createLogger() as any);
206+
const baselineChanges = dbA.sync.exportChangesSince(0);
207+
expect(baselineChanges.some((change) => change.table === "queue_landing_state")).toBe(true);
208+
dbB.sync.applyChanges(baselineChanges);
209+
expect(
210+
dbB.get<{ id: string }>("select id from queue_landing_state where id = ?", [queueId])?.id,
211+
).toBe(queueId);
212+
213+
const versionBeforeWipe = dbA.sync.getDbVersion();
214+
dbA.run("delete from kv where key = ?", ["queue_landing_state.wiped_for_stacked_overhaul.v1"]);
215+
dbA.close();
216+
217+
const dbAReopened = await openKvDb(dbPathA, createLogger() as any);
218+
expect(
219+
dbAReopened.get<{ id: string }>("select id from queue_landing_state where id = ?", [queueId]),
220+
).toBeNull();
221+
222+
const wipeChanges = dbAReopened.sync.exportChangesSince(versionBeforeWipe);
223+
expect(wipeChanges.some((change) => change.table === "queue_landing_state")).toBe(false);
224+
225+
dbB.sync.applyChanges(wipeChanges);
226+
expect(
227+
dbB.get<{ id: string }>("select id from queue_landing_state where id = ?", [queueId])?.id,
228+
).toBe(queueId);
229+
230+
dbAReopened.close();
231+
dbB.close();
232+
});
233+
181234
it("ignores CRDT changes for legacy unified_memories tables removed in #329", async () => {
182235
const db2 = await openKvDb(makeDbPath("ade-kvdb-sync-mem-skip-"), createLogger() as any);
183236
const legacyChange = {

apps/desktop/src/main/services/state/kvDb.ts

Lines changed: 61 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,65 @@ function dropCrrTriggers(db: DatabaseSyncType, tableName: string, logger?: Logge
521521
return triggers.length;
522522
}
523523

524+
/** Strip CRR triggers/metadata so row deletes stay local (no replicated tombstones). */
525+
function deleteAllRowsWithoutCrrReplication(
526+
db: DatabaseSyncType,
527+
tableName: string,
528+
logger?: Logger,
529+
): void {
530+
if (!rawHasTable(db, tableName)) return;
531+
532+
const clockTableName = `${tableName}__crsql_clock`;
533+
const pksTableName = `${tableName}__crsql_pks`;
534+
if (rawHasTable(db, clockTableName) || rawHasTable(db, pksTableName) || listCrrTriggers(db, tableName).length > 0) {
535+
if (rawHasTable(db, "crsql_master") && rawHasColumn(db, "crsql_master", "tbl_name")) {
536+
runStatement(db, "delete from crsql_master where tbl_name = ?", [tableName]);
537+
}
538+
if (rawHasTable(db, "crsql_changes") && rawHasColumn(db, "crsql_changes", "table")) {
539+
runStatement(db, "delete from crsql_changes where [table] = ?", [tableName]);
540+
}
541+
try {
542+
getRow(db, "select crsql_as_table(?) as ok", [tableName]);
543+
} catch {
544+
// Table may not be registered enough for crsql_as_table; shadow cleanup below still applies.
545+
}
546+
dropCrrTriggers(db, tableName, logger);
547+
runStatement(db, `drop table if exists ${quoteIdentifier(clockTableName)}`);
548+
runStatement(db, `drop table if exists ${quoteIdentifier(pksTableName)}`);
549+
}
550+
551+
runStatement(db, `delete from ${quoteIdentifier(tableName)}`);
552+
}
553+
554+
const QUEUE_OVERHAUL_WIPE_MARKER = "queue_landing_state.wiped_for_stacked_overhaul.v1";
555+
556+
/**
557+
* One-shot local wipe of legacy queue_landing_state on upgrade to the stacked-PR
558+
* queue overhaul. Must run after migrations and before ensureCrrTables so deletes
559+
* do not replicate to peers that have not upgraded yet.
560+
*/
561+
function wipeQueueLandingStateForStackedOverhaulIfNeeded(db: DatabaseSyncType, logger?: Logger): void {
562+
try {
563+
const row = getRow<{ value: string }>(
564+
db,
565+
"select value from kv where key = ?",
566+
[QUEUE_OVERHAUL_WIPE_MARKER],
567+
);
568+
if (row) return;
569+
570+
deleteAllRowsWithoutCrrReplication(db, "queue_landing_state", logger);
571+
runStatement(
572+
db,
573+
"insert into kv (key, value) values (?, ?) on conflict(key) do update set value = excluded.value",
574+
[QUEUE_OVERHAUL_WIPE_MARKER, new Date().toISOString()],
575+
);
576+
} catch {
577+
// Table may not exist on a brand-new DB; initialization will create both
578+
// tables and the next startup will record the marker. Skipping the wipe
579+
// on a fresh DB is correct (nothing to wipe).
580+
}
581+
}
582+
524583
function removeExcludedCrrMetadata(db: DatabaseSyncType, logger?: Logger): void {
525584
for (const tableName of LOCAL_ONLY_CRR_EXCLUDED_TABLES) {
526585
const clockTableName = `${tableName}__crsql_clock`;
@@ -1821,32 +1880,6 @@ function migrate(db: MigrationDb) {
18211880
try { db.run("alter table queue_landing_state add column wait_reason text"); } catch {}
18221881
try { db.run("alter table queue_landing_state add column updated_at text"); } catch {}
18231882

1824-
// One-shot wipe of legacy queue_landing_state on upgrade to the stacked-PR
1825-
// queue overhaul. The new queue creates PRs with chain bases (PR_N's base =
1826-
// previous lane's branch) instead of all-into-main, so any in-flight queue
1827-
// from the old code path would be misinterpreted by the new landing loop.
1828-
// Wiping rather than migrating is a deliberate choice — the user accepts
1829-
// losing in-flight queues in exchange for not maintaining a translation
1830-
// layer for every legacy field shape.
1831-
const QUEUE_OVERHAUL_WIPE_MARKER = "queue_landing_state.wiped_for_stacked_overhaul.v1";
1832-
try {
1833-
const row = db.get<{ value: string }>(
1834-
"select value from kv where key = ?",
1835-
[QUEUE_OVERHAUL_WIPE_MARKER],
1836-
);
1837-
if (!row) {
1838-
db.run("delete from queue_landing_state");
1839-
db.run(
1840-
"insert into kv (key, value) values (?, ?) on conflict(key) do update set value = excluded.value",
1841-
[QUEUE_OVERHAUL_WIPE_MARKER, new Date().toISOString()],
1842-
);
1843-
}
1844-
} catch {
1845-
// Table may not exist on a brand-new DB; initialization will create both
1846-
// tables and the next startup will record the marker. Skipping the wipe
1847-
// on a fresh DB is correct (nothing to wipe).
1848-
}
1849-
18501883
// Rebase dismiss/defer persistence
18511884
db.run(`
18521885
create table if not exists rebase_dismissed (
@@ -2849,6 +2882,8 @@ export async function openKvDb(dbPath: string, logger: Logger): Promise<AdeDb> {
28492882
removeExcludedCrrMetadata(db, logger);
28502883
}
28512884

2885+
wipeQueueLandingStateForStackedOverhaulIfNeeded(db, logger);
2886+
28522887
if (crsqliteLoaded) {
28532888
loadCrsqliteIfAvailable();
28542889
ensureCrrTables(db, logger);

apps/desktop/src/renderer/components/files/FilesPage.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,6 @@ const filesRootTreeCacheByWorkspace = new Map<string, FileTreeNode[]>();
159159
const MAX_FILES_PAGE_CACHED_SCOPES = 8;
160160
const MAX_FILES_TREE_CACHED_WORKSPACES = 32;
161161
const MAX_CACHED_CLEAN_TAB_CHARS = 256 * 1024;
162-
const MAX_CACHED_DIRTY_TAB_CHARS = 8 * 1024 * 1024;
163162
const MAX_QUEUED_TREE_PARENT_REFRESHES = 24;
164163
const FILES_WATCH_START_DELAY_MS = import.meta.env.MODE === "test" || (window as any).__adeBrowserMock ? 0 : 2_000;
165164

@@ -249,8 +248,10 @@ function filesPageSessionHasUnsavedTabs(session: FilesPageSessionState | undefin
249248
function cacheableSessionTabs(openTabs: OpenTab[]): OpenTab[] {
250249
return openTabs
251250
.filter((tab) => {
251+
// Always retain unsaved buffers across session scope changes; size limits
252+
// apply only to clean tabs so large in-flight edits are not dropped silently.
252253
if (tab.content !== tab.savedContent) {
253-
return tab.content.length <= MAX_CACHED_DIRTY_TAB_CHARS;
254+
return true;
254255
}
255256
return isTextTab(tab) && tab.content.length <= MAX_CACHED_CLEAN_TAB_CHARS;
256257
})

0 commit comments

Comments
 (0)