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

Commit b07437c

Browse files
committed
FEA-1550: Cut Agent Dashboard over to PGlite
- Replace the design-system Agent Dashboard SQLite runtime with a fresh PGlite data directory - Remove the SQLite-to-PGlite migration startup path and migration tests - Add async PGlite-backed dashboard stores, hook writes, collector imports, session sync, and reconciliation reads - Cover fresh PGlite startup and first hook ingestion Testing: Desktop typecheck, lint, focused PGlite dashboard tests, sync tests, and boot-recovery test passed; full desktop suite progressed through touched coverage but was interrupted after hanging in boot-recovery, which passed in isolation Risks: Existing agent-dashboard.sqlite data is intentionally not migrated; the new PGlite database starts empty and repopulates from hooks and collectors after first start
1 parent b6cdcaa commit b07437c

14 files changed

Lines changed: 2242 additions & 1370 deletions

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.1",
3+
"version": "0.16.0",
44
"description": "ClosedLoop Desktop",
55
"author": "ClosedLoop AI <support@closedloop.ai>",
66
"private": true,

apps/desktop/src/main/agent-dashboard-database-startup.ts

Lines changed: 0 additions & 93 deletions
This file was deleted.

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

Lines changed: 33 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,17 @@ import path from "node:path";
22
import { app, ipcMain, type BrowserWindow } from "electron";
33
import { AgentHookListener } from "./agent-monitor-listener.js";
44
import { CollectorManager } from "./collectors/collector-manager.js";
5-
import {
6-
loadMeteredUsageRows,
7-
type MeteredUsageRow,
8-
} from "./reconciliation-worker.js";
5+
import type { MeteredUsageRow } from "./reconciliation-worker.js";
6+
import type { AgentSessionSyncSource } from "./agent-session-sync-service.js";
97
import type { SessionPageRequest } from "../shared/agent-db-contract.js";
108
import { detectBillingMode } from "./billing-mode-detector.js";
11-
import { openAgentDatabase, type AgentDatabase } from "./database/index.js";
12-
import { coerceDbId } from "./database/ipc-validation.js";
13-
import { createLifecycle } from "./database/lifecycle.js";
149
import {
15-
resolveAgentDashboardDatabasePathForUserData,
16-
type AgentDashboardDatabaseStartupResult,
17-
} from "./agent-dashboard-database-startup.js";
10+
openPgliteAgentDatabase,
11+
type PgliteAgentDatabase,
12+
} from "./database/pglite.js";
13+
import { coerceDbId } from "./database/ipc-validation.js";
1814
import { isAgentMonitorHooksEnabled } from "./agent-monitor-hooks.js";
1915

20-
export { prepareAgentDashboardDatabaseStartup } from "./agent-dashboard-database-startup.js";
21-
2216
const DESIGN_SYSTEM_DB_IPC_CHANNELS = [
2317
"desktop:db:get-sessions",
2418
"desktop:db:get-sessions-page",
@@ -44,88 +38,68 @@ export interface AgentDashboardDesignSystemRuntimeOptions {
4438
onTerminalFailure: (reason: string) => void;
4539
userDataPath?: string;
4640
log?: (scope: string, message: string) => void;
47-
startupResult?: AgentDashboardDatabaseStartupResult;
4841
}
4942

5043
export interface AgentDashboardDesignSystemRuntime {
51-
connection: AgentDatabase["connection"];
44+
connection: null;
45+
syncSource: AgentSessionSyncSource | null;
5246
getUrl: () => string | null;
5347
isReady: () => boolean;
5448
start: () => void;
5549
stop: () => Promise<void>;
5650
close: () => void;
5751
restartCollectors: () => void;
5852
registerIpcHandlers: () => void;
59-
loadMeteredUsageRows: (cutoffIso: string) => MeteredUsageRow[];
53+
loadMeteredUsageRows: (cutoffIso: string) => MeteredUsageRow[] | Promise<MeteredUsageRow[]>;
6054
}
6155

6256
/**
6357
* Resolve the opt-in design-system dashboard database. This helper lives inside
6458
* the dynamic boundary so default/legacy boot never imports code that can create
65-
* or migrate `agent-dashboard.sqlite`.
59+
* the PGlite data directory.
6660
*/
6761
export function resolveAgentDashboardDatabasePath(
6862
userDataPath = app.getPath("userData"),
6963
): string {
70-
return resolveAgentDashboardDatabasePathForUserData(userDataPath);
64+
return path.join(userDataPath, "agent-dashboard.pgdata");
7165
}
7266

7367
/**
7468
* Create the in-process design-system dashboard runtime. Import this module only
7569
* after the Labs flag has selected design-system mode; all imports below this
76-
* boundary can open SQLite, bind the hook port, register IPC, or start watchers.
70+
* boundary can open PGlite, bind the hook port, register IPC, or start watchers.
7771
*/
78-
export function createAgentDashboardDesignSystemRuntime(
72+
export async function createAgentDashboardDesignSystemRuntime(
7973
options: AgentDashboardDesignSystemRuntimeOptions,
80-
): AgentDashboardDesignSystemRuntime {
74+
): Promise<AgentDashboardDesignSystemRuntime> {
8175
const log = options.log ?? (() => {});
82-
const startupResult = options.startupResult;
83-
84-
if (startupResult?.backend === "pglite") {
85-
log(
86-
"agent-dashboard-migration",
87-
"PGlite migration kicked off in background; SQLite runtime active during migration",
88-
);
89-
void startupResult.migrationPromise.then((migration) => {
90-
if (migration.status === "failed") {
91-
log(
92-
"agent-dashboard-migration",
93-
`PGlite migration failed: ${migration.error}`,
94-
);
95-
} else {
96-
log(
97-
"agent-dashboard-migration",
98-
`PGlite migration completed (${migration.status === "migrated" ? `${migration.rowCounts.sessions} sessions` : "skipped"})`,
99-
);
100-
}
101-
});
102-
}
103-
104-
const agentDatabase = openAgentDatabase(
105-
resolveAgentDashboardDatabasePath(options.userDataPath),
106-
);
107-
let dbIpcRegistered = false;
108-
let closed = false;
109-
110-
const lifecycle = createLifecycle(agentDatabase.connection, {
111-
tokenUsage: agentDatabase.tokenUsage,
76+
let pgliteDatabase: PgliteAgentDatabase | null = null;
77+
const agentDatabase = await openPgliteAgentDatabase({
78+
dataDir: resolveAgentDashboardDatabasePath(options.userDataPath),
11279
detectBillingMode,
11380
emit: (sessionId: string) => {
114-
agentDatabase.sessions.handleSessionMutation(sessionId);
81+
void pgliteDatabase?.sessions.handleSessionMutation(sessionId);
11582
options.getWindow()?.webContents.send("desktop:db:changed", { sessionId });
11683
},
117-
log: (message: string) => log("agent-lifecycle", message),
84+
log: (message: string) => log("agent-pglite", message),
11885
});
86+
pgliteDatabase = agentDatabase;
87+
log(
88+
"agent-dashboard",
89+
"PGlite runtime active for Agent Dashboard database",
90+
);
91+
let dbIpcRegistered = false;
92+
let closed = false;
11993

12094
const hookListener = new AgentHookListener({
121-
lifecycle,
95+
lifecycle: { processEvent: agentDatabase.processEvent },
12296
getSandboxBaseDirectory: options.getSandboxBaseDirectory,
12397
log: (message: string) => log("agent-monitor-listener", message),
12498
onBindError: options.onTerminalFailure,
12599
});
126100

127101
const collectorManager = new CollectorManager({
128-
agentDatabase,
102+
importer: agentDatabase.importer,
129103
detectBillingMode,
130104
getSandboxBaseDirectory: options.getSandboxBaseDirectory,
131105
stateDir: path.join(options.userDataPath ?? app.getPath("userData"), "agent-monitor"),
@@ -138,6 +112,7 @@ export function createAgentDashboardDesignSystemRuntime(
138112

139113
const runtime: AgentDashboardDesignSystemRuntime = {
140114
connection: agentDatabase.connection,
115+
syncSource: agentDatabase.syncSource,
141116
getUrl: () => hookListener.getUrl(),
142117
isReady: () => hookListener.isReady(),
143118
start: () => {
@@ -160,7 +135,7 @@ export function createAgentDashboardDesignSystemRuntime(
160135
}
161136
closed = true;
162137
unregisterDesignSystemDbIpcHandlers();
163-
agentDatabase.close();
138+
void agentDatabase.close();
164139
},
165140
restartCollectors: () => {
166141
if (closed) {
@@ -177,13 +152,13 @@ export function createAgentDashboardDesignSystemRuntime(
177152
registerDesignSystemDbIpcHandlers(agentDatabase);
178153
},
179154
loadMeteredUsageRows: (cutoffIso: string) =>
180-
loadMeteredUsageRows(agentDatabase.connection, cutoffIso),
155+
agentDatabase.loadMeteredUsageRows(cutoffIso),
181156
};
182157

183158
return runtime;
184159
}
185160

186-
function registerDesignSystemDbIpcHandlers(agentDatabase: AgentDatabase): void {
161+
function registerDesignSystemDbIpcHandlers(agentDatabase: PgliteAgentDatabase): void {
187162
ipcMain.handle("desktop:db:get-sessions", () => agentDatabase.sessions.getAll());
188163

189164
ipcMain.handle("desktop:db:get-sessions-page", (_event, request: unknown) =>
@@ -256,7 +231,7 @@ function registerDesignSystemDbIpcHandlers(agentDatabase: AgentDatabase): void {
256231
ipcMain.handle("desktop:db:get-agent-hierarchy", (_event, sessionId: unknown) => {
257232
const id = coerceDbId(sessionId);
258233
if (id === null) return [];
259-
return agentDatabase.agents.getBySessionWithChildren(id, agentDatabase.events);
234+
return agentDatabase.agents.getBySessionWithChildren(id);
260235
});
261236

262237
ipcMain.handle("desktop:db:get-analytics", () =>

apps/desktop/src/main/agent-monitor-listener.ts

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { IncomingMessage, ServerResponse } from "node:http";
44
import { z } from "zod";
55
import { AGENT_MONITOR_PORT } from "../shared/contracts.js";
66
import { isSessionInSandbox } from "./agent-session-sync-service.js";
7-
import type { createLifecycle, HookData } from "./database/lifecycle.js";
7+
import type { HookData } from "./database/lifecycle.js";
88

99
// CLOSEDLOOP-TICKET FEA-1500: remove legacy HTTP hook listener on 4820 after
1010
// transport migration (FEA-1497 breaking-change discipline contract #1). The hook
@@ -21,6 +21,14 @@ const CODEX_HOOK_EVENT_PATH = "/api/hooks/codex/event";
2121
const PROVIDER_HINT_FIELD = "__provider";
2222
type HookHarness = "claude" | "codex";
2323

24+
export interface AgentHookLifecycle {
25+
processEvent(
26+
hookType: string,
27+
data: HookData,
28+
harness: string,
29+
): boolean | Promise<boolean>;
30+
}
31+
2432
/** The `{ hook_type, data }` envelope every hook handler POSTs. */
2533
const HookEnvelopeSchema = z.object({
2634
hook_type: z.string(),
@@ -29,7 +37,7 @@ const HookEnvelopeSchema = z.object({
2937

3038
export interface AgentHookListenerOptions {
3139
/** The lifecycle processor that owns all DB writes. */
32-
lifecycle: ReturnType<typeof createLifecycle>;
40+
lifecycle: AgentHookLifecycle;
3341
/** FEA-1407 sandbox base directory (empty string ⇒ capture nothing). */
3442
getSandboxBaseDirectory: () => string;
3543
/** Key-free diagnostic sink (gatewayLog). */
@@ -181,8 +189,16 @@ export class AgentHookListener {
181189
return;
182190
}
183191

184-
this.options.lifecycle.processEvent(hookType, data, harness);
185-
this.json(res, 200, { ok: true });
192+
Promise.resolve(
193+
this.options.lifecycle.processEvent(hookType, data, harness),
194+
)
195+
.then(() => this.json(res, 200, { ok: true }))
196+
.catch((error: unknown) => {
197+
this.log(
198+
`agent hook listener: failed to process event: ${error instanceof Error ? error.message : String(error)}`,
199+
);
200+
this.json(res, 200, { ok: false });
201+
});
186202
} catch (error) {
187203
// Malformed JSON or unexpected error: ack 200 (fail-soft) + log.
188204
this.log(

0 commit comments

Comments
 (0)