Skip to content
This repository was archived by the owner on Jun 8, 2026. It is now read-only.

Commit d0f662c

Browse files
committed
FEA-1550: Review feedback hardening for SQLite->PGlite migration
- Restrict backup cleanup to exact naming pattern, regular files only, no recursive delete - Stamp backup mtime with migration time instead of preserving SQLite mtime - Stage PGlite in temp directory, promote only after backup succeeds - Sanitize absolute paths and error stacks from logs - Batch inserts (500 rows) instead of per-row INSERT - Validate all SQLite tables are managed before migration - Require .bak + .pgdata for already_migrated; detect downgrade scenario - Wire migration into startup with keepSource, non-blocking background execution - Pass startup result to runtime for PGlite readiness signaling Testing: Full migration test suite (9 tests), typecheck, and lint pass
1 parent 56a7efb commit d0f662c

6 files changed

Lines changed: 286 additions & 97 deletions

File tree

apps/desktop/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "desktop",
3-
"version": "0.16.0",
3+
"version": "0.16.1",
44
"description": "ClosedLoop Desktop",
55
"author": "ClosedLoop AI <support@closedloop.ai>",
66
"private": true,
Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import path from "node:path";
1+
import { basename, join } from "node:path";
22
import {
33
cleanupExpiredSqliteBackups,
44
migrateSqliteToPglite,
@@ -19,22 +19,16 @@ export type AgentDashboardDatabaseStartupResult =
1919
backend: "pglite";
2020
sqlitePath: string;
2121
pgliteDataDir: string;
22-
migration: SqliteToPgliteMigrationResult;
22+
migration?: SqliteToPgliteMigrationResult;
23+
migrationPromise: Promise<SqliteToPgliteMigrationResult>;
2324
};
2425

2526
export function resolveAgentDashboardDatabasePathForUserData(
2627
userDataPath: string,
2728
): string {
28-
return path.join(userDataPath, "agent-dashboard.sqlite");
29+
return join(userDataPath, "agent-dashboard.sqlite");
2930
}
3031

31-
/**
32-
* Startup-owned preparation for the dashboard database engine. SQLite mode only
33-
* performs stale backup cleanup so the current SQLite runtime cannot rename its
34-
* own live database. PGlite mode runs the forward migration before the PGlite
35-
* runtime opens; failure returns a SQLite fallback result and leaves the source
36-
* DB intact for retry on the next launch.
37-
*/
3832
export async function prepareAgentDashboardDatabaseStartup(options: {
3933
userDataPath: string;
4034
backend: AgentDashboardDatabaseBackend;
@@ -46,39 +40,54 @@ export async function prepareAgentDashboardDatabaseStartup(options: {
4640
const pgliteDataDir = resolvePgliteDataDir(sqlitePath);
4741
const log = options.log ?? (() => {});
4842

43+
const removed = await cleanupExpiredSqliteBackups(sqlitePath);
44+
if (removed > 0) {
45+
log(
46+
"agent-dashboard-migration",
47+
`Removed ${removed} expired SQLite backup(s) for ${basename(sqlitePath)}`,
48+
);
49+
}
50+
4951
if (options.backend === "sqlite") {
50-
const removed = await cleanupExpiredSqliteBackups(sqlitePath);
51-
if (removed > 0) {
52+
return { backend: "sqlite", sqlitePath, pgliteDataDir };
53+
}
54+
55+
const onSettled = (migration: SqliteToPgliteMigrationResult) => {
56+
if (migration.status === "failed") {
5257
log(
5358
"agent-dashboard-migration",
54-
`Removed ${removed} expired SQLite backup(s) for ${sqlitePath}`,
59+
`PGlite migration failed (runtime continues on SQLite): ${migration.error}`,
60+
);
61+
} else if (migration.status === "migrated") {
62+
log(
63+
"agent-dashboard-migration",
64+
`PGlite migration completed (${migration.rowCounts.sessions} sessions); SQLite preserved for sync runtime`,
5565
);
5666
}
57-
return { backend: "sqlite", sqlitePath, pgliteDataDir };
58-
}
67+
return migration;
68+
};
5969

60-
const migration = await migrateSqliteToPglite({
70+
const migrationPromise = migrateSqliteToPglite({
6171
sqlitePath,
6272
pgliteDataDir,
73+
keepSource: true,
6374
log: (message) => log("agent-dashboard-migration", message),
64-
});
65-
if (migration.status === "failed") {
66-
log(
67-
"agent-dashboard-migration",
68-
`Falling back to SQLite after PGlite migration failure: ${migration.error}`,
69-
);
75+
}).then(onSettled, (error: unknown) => {
76+
const err = error instanceof Error ? error.message : String(error);
77+
log("agent-dashboard-migration", `PGlite migration failed: ${err}`);
7078
return {
71-
backend: "sqlite",
79+
status: "failed" as const,
7280
sqlitePath,
7381
pgliteDataDir,
74-
migration,
82+
error: err,
83+
failedAt: new Date().toISOString(),
7584
};
76-
}
85+
});
7786

7887
return {
7988
backend: "pglite",
8089
sqlitePath,
8190
pgliteDataDir,
82-
migration,
91+
migrationPromise,
8392
};
8493
}

apps/desktop/src/main/agent-dashboard-design-system-runtime.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ import { detectBillingMode } from "./billing-mode-detector.js";
1010
import { openAgentDatabase, type AgentDatabase } from "./database/index.js";
1111
import { coerceDbId } from "./database/ipc-validation.js";
1212
import { createLifecycle } from "./database/lifecycle.js";
13-
import { resolveAgentDashboardDatabasePathForUserData } from "./agent-dashboard-database-startup.js";
13+
import {
14+
resolveAgentDashboardDatabasePathForUserData,
15+
type AgentDashboardDatabaseStartupResult,
16+
} from "./agent-dashboard-database-startup.js";
1417
import { isAgentMonitorHooksEnabled } from "./agent-monitor-hooks.js";
1518

1619
export { prepareAgentDashboardDatabaseStartup } from "./agent-dashboard-database-startup.js";
@@ -37,6 +40,7 @@ export interface AgentDashboardDesignSystemRuntimeOptions {
3740
onTerminalFailure: (reason: string) => void;
3841
userDataPath?: string;
3942
log?: (scope: string, message: string) => void;
43+
startupResult?: AgentDashboardDatabaseStartupResult;
4044
}
4145

4246
export interface AgentDashboardDesignSystemRuntime {
@@ -71,6 +75,28 @@ export function createAgentDashboardDesignSystemRuntime(
7175
options: AgentDashboardDesignSystemRuntimeOptions,
7276
): AgentDashboardDesignSystemRuntime {
7377
const log = options.log ?? (() => {});
78+
const startupResult = options.startupResult;
79+
80+
if (startupResult?.backend === "pglite") {
81+
log(
82+
"agent-dashboard-migration",
83+
"PGlite migration kicked off in background; SQLite runtime active during migration",
84+
);
85+
void startupResult.migrationPromise.then((migration) => {
86+
if (migration.status === "failed") {
87+
log(
88+
"agent-dashboard-migration",
89+
`PGlite migration failed: ${migration.error}`,
90+
);
91+
} else {
92+
log(
93+
"agent-dashboard-migration",
94+
`PGlite migration completed (${migration.status === "migrated" ? `${migration.rowCounts.sessions} sessions` : "skipped"})`,
95+
);
96+
}
97+
});
98+
}
99+
74100
const agentDatabase = openAgentDatabase(
75101
resolveAgentDashboardDatabasePath(options.userDataPath),
76102
);

apps/desktop/src/main/app.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1515,9 +1515,9 @@ export class DesktopApplication {
15151515
} = await import(
15161516
"./agent-dashboard-design-system-runtime.js"
15171517
);
1518-
await prepareAgentDashboardDatabaseStartup({
1518+
const startupResult = await prepareAgentDashboardDatabaseStartup({
15191519
userDataPath: app.getPath("userData"),
1520-
backend: "sqlite",
1520+
backend: "pglite",
15211521
log: (scope, message) => gatewayLog.info(scope, message),
15221522
});
15231523
this.agentDashboardDesignSystem =
@@ -1537,6 +1537,7 @@ export class DesktopApplication {
15371537
this.refreshTrayState();
15381538
},
15391539
log: (scope, message) => gatewayLog.info(scope, message),
1540+
startupResult,
15401541
});
15411542
this.agentDashboardDesignSystem.registerIpcHandlers();
15421543
}

0 commit comments

Comments
 (0)