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

Commit 1565215

Browse files
committed
PERF: Paginate design dashboard sessions
- Add bounded session page and single-session detail database reads - Move sessions list, Kanban, and detail views off all-session detail loads - Add pagination regression coverage and bump desktop version Testing: desktop lint, desktop typecheck, and focused session pagination test passed Risks: Full desktop test suite was rerun after regenerating legacy agent-monitor output but was still in progress when requested to push
1 parent 09cf797 commit 1565215

11 files changed

Lines changed: 426 additions & 124 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.15.114",
3+
"version": "0.15.115",
44
"description": "ClosedLoop Desktop",
55
"author": "ClosedLoop AI <support@closedloop.ai>",
66
"private": true,

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
loadMeteredUsageRows,
77
type MeteredUsageRow,
88
} from "./reconciliation-worker.js";
9+
import type { SessionPageRequest } from "../shared/agent-db-contract.js";
910
import { detectBillingMode } from "./billing-mode-detector.js";
1011
import { openAgentDatabase, type AgentDatabase } from "./database/index.js";
1112
import { coerceDbId } from "./database/ipc-validation.js";
@@ -14,7 +15,9 @@ import { isAgentMonitorHooksEnabled } from "./agent-monitor-hooks.js";
1415

1516
const DESIGN_SYSTEM_DB_IPC_CHANNELS = [
1617
"desktop:db:get-sessions",
18+
"desktop:db:get-sessions-page",
1719
"desktop:db:get-session",
20+
"desktop:db:get-session-details",
1821
"desktop:db:get-agents",
1922
"desktop:db:get-events",
2023
"desktop:db:get-dashboard-summary",
@@ -153,12 +156,22 @@ export function createAgentDashboardDesignSystemRuntime(
153156
function registerDesignSystemDbIpcHandlers(agentDatabase: AgentDatabase): void {
154157
ipcMain.handle("desktop:db:get-sessions", () => agentDatabase.sessions.getAll());
155158

159+
ipcMain.handle("desktop:db:get-sessions-page", (_event, request: unknown) =>
160+
agentDatabase.sessions.getPage(coerceSessionPageRequest(request)),
161+
);
162+
156163
ipcMain.handle("desktop:db:get-session", (_event, id: unknown) => {
157164
const sessionId = coerceDbId(id);
158165
if (sessionId === null) return undefined;
159166
return agentDatabase.sessions.getById(sessionId);
160167
});
161168

169+
ipcMain.handle("desktop:db:get-session-details", (_event, id: unknown) => {
170+
const sessionId = coerceDbId(id);
171+
if (sessionId === null) return undefined;
172+
return agentDatabase.sessions.getDetailsById(sessionId);
173+
});
174+
162175
ipcMain.handle("desktop:db:get-agents", (_event, sessionId: unknown) => {
163176
const id = coerceDbId(sessionId);
164177
if (id === null) return [];
@@ -224,3 +237,16 @@ function unregisterDesignSystemDbIpcHandlers(): void {
224237
ipcMain.removeHandler(channel);
225238
}
226239
}
240+
241+
function coerceSessionPageRequest(value: unknown): SessionPageRequest | undefined {
242+
if (typeof value !== "object" || value === null) {
243+
return undefined;
244+
}
245+
const raw = value as Record<string, unknown>;
246+
return {
247+
limit: typeof raw.limit === "number" ? raw.limit : undefined,
248+
offset: typeof raw.offset === "number" ? raw.offset : undefined,
249+
status: typeof raw.status === "string" ? raw.status : undefined,
250+
q: typeof raw.q === "string" ? raw.q : undefined,
251+
};
252+
}

apps/desktop/src/main/database/schema.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export const CURRENT_SCHEMA_VERSION = 5;
1+
export const CURRENT_SCHEMA_VERSION = 6;
22

33
/**
44
* Each migration runs against the DB when user_version < CURRENT_SCHEMA_VERSION.
@@ -121,5 +121,14 @@ ALTER TABLE sessions ADD COLUMN billing_mode TEXT;
121121
`
122122
CREATE INDEX IF NOT EXISTS idx_sessions_started_at ON sessions(started_at DESC);
123123
CREATE INDEX IF NOT EXISTS idx_sessions_status_started_at ON sessions(status, started_at DESC);
124+
`,
125+
126+
// Version 5 -> 6: keep the in-process dashboard waiting-session page bounded
127+
// to the sessions index instead of scanning large historical databases.
128+
`
129+
CREATE INDEX IF NOT EXISTS idx_sessions_waiting_started_at
130+
ON sessions(started_at DESC)
131+
WHERE awaiting_input_since IS NOT NULL
132+
AND status NOT IN ('completed', 'abandoned', 'error');
124133
`,
125134
];

apps/desktop/src/main/database/sessions.ts

Lines changed: 100 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,29 @@
1-
import type { DatabaseSync } from "node:sqlite";
2-
import type { SessionRow, SessionWithAgents } from "../../shared/agent-db-contract.js";
1+
import type { DatabaseSync, SQLInputValue } from "node:sqlite";
2+
import type {
3+
SessionPage,
4+
SessionPageRequest,
5+
SessionRow,
6+
SessionWithAgents,
7+
} from "../../shared/agent-db-contract.js";
38

49
// Terminal session statuses (vendor + canonical AgentSession vocabulary). A
510
// session not in this set is treated as active. Writes are owned by
611
// `lifecycle.ts`; this store is read-only.
712
const TERMINAL_STATUSES = "('completed', 'abandoned', 'error')";
813
const TERMINAL_STATUS_SET = new Set(["completed", "abandoned", "error"]);
9-
const SESSION_DETAILS_CTES = `
10-
WITH agent_counts AS (
11-
SELECT session_id, COUNT(*) as agent_count
12-
FROM agents
13-
GROUP BY session_id
14-
),
15-
event_counts AS (
16-
SELECT session_id, COUNT(*) as event_count
17-
FROM events
18-
GROUP BY session_id
19-
),
20-
token_totals AS (
21-
SELECT
22-
session_id,
23-
COALESCE(SUM(COALESCE(input_tokens, 0) + COALESCE(output_tokens, 0)), 0) as total_tokens
24-
FROM token_usage
25-
GROUP BY session_id
26-
)
14+
const MAX_SESSION_PAGE_LIMIT = 100;
15+
const DEFAULT_SESSION_PAGE_LIMIT = 25;
16+
const SESSION_DETAIL_SELECT = `
17+
SELECT
18+
s.*,
19+
(SELECT COUNT(*) FROM agents a WHERE a.session_id = s.id) as agent_count,
20+
(SELECT COUNT(*) FROM events e WHERE e.session_id = s.id) as event_count,
21+
(
22+
SELECT COALESCE(SUM(COALESCE(t.input_tokens, 0) + COALESCE(t.output_tokens, 0)), 0)
23+
FROM token_usage t
24+
WHERE t.session_id = s.id
25+
) as total_tokens
26+
FROM sessions s
2727
`;
2828

2929
export function createSessionStore(db: DatabaseSync) {
@@ -32,32 +32,18 @@ export function createSessionStore(db: DatabaseSync) {
3232
const getActiveStmt = db.prepare(
3333
`SELECT * FROM sessions WHERE status NOT IN ${TERMINAL_STATUSES} ORDER BY started_at DESC`,
3434
);
35+
const getDetailsByIdStmt = db.prepare(`
36+
${SESSION_DETAIL_SELECT}
37+
WHERE s.id = ?
38+
`);
3539

3640
const getActiveWithDetailsStmt = db.prepare(`
37-
${SESSION_DETAILS_CTES}
38-
SELECT
39-
s.*,
40-
COALESCE(ac.agent_count, 0) as agent_count,
41-
COALESCE(ec.event_count, 0) as event_count,
42-
COALESCE(tt.total_tokens, 0) as total_tokens
43-
FROM sessions s
44-
LEFT JOIN agent_counts ac ON ac.session_id = s.id
45-
LEFT JOIN event_counts ec ON ec.session_id = s.id
46-
LEFT JOIN token_totals tt ON tt.session_id = s.id
41+
${SESSION_DETAIL_SELECT}
4742
WHERE s.status NOT IN ${TERMINAL_STATUSES}
4843
ORDER BY s.started_at DESC
4944
`);
5045
const getHistoricalWithDetailsStmt = db.prepare(`
51-
${SESSION_DETAILS_CTES}
52-
SELECT
53-
s.*,
54-
COALESCE(ac.agent_count, 0) as agent_count,
55-
COALESCE(ec.event_count, 0) as event_count,
56-
COALESCE(tt.total_tokens, 0) as total_tokens
57-
FROM sessions s
58-
LEFT JOIN agent_counts ac ON ac.session_id = s.id
59-
LEFT JOIN event_counts ec ON ec.session_id = s.id
60-
LEFT JOIN token_totals tt ON tt.session_id = s.id
46+
${SESSION_DETAIL_SELECT}
6147
WHERE s.status IN ${TERMINAL_STATUSES}
6248
ORDER BY s.started_at DESC
6349
`);
@@ -97,6 +83,54 @@ export function createSessionStore(db: DatabaseSync) {
9783
});
9884
}
9985

86+
function coercePageRequest(request: SessionPageRequest | undefined): {
87+
limit: number;
88+
offset: number;
89+
status: string | null;
90+
q: string | null;
91+
} {
92+
const requestedLimit = request?.limit;
93+
const limit = typeof requestedLimit === "number" && Number.isInteger(requestedLimit)
94+
? Math.min(Math.max(requestedLimit, 1), MAX_SESSION_PAGE_LIMIT)
95+
: DEFAULT_SESSION_PAGE_LIMIT;
96+
const requestedOffset = request?.offset;
97+
const offset = typeof requestedOffset === "number" && Number.isInteger(requestedOffset)
98+
? Math.max(requestedOffset, 0)
99+
: 0;
100+
const status =
101+
typeof request?.status === "string" && request.status.length > 0
102+
? request.status
103+
: null;
104+
const q =
105+
typeof request?.q === "string" && request.q.trim().length > 0
106+
? request.q.trim()
107+
: null;
108+
return { limit, offset, status, q };
109+
}
110+
111+
function pageWhereClause(status: string | null, q: string | null): {
112+
whereSql: string;
113+
params: SQLInputValue[];
114+
} {
115+
const where: string[] = [];
116+
const params: SQLInputValue[] = [];
117+
if (status === "waiting") {
118+
where.push("s.status NOT IN ('completed', 'abandoned', 'error') AND s.awaiting_input_since IS NOT NULL");
119+
} else if (status && status !== "all") {
120+
where.push("s.status = ?");
121+
params.push(status);
122+
}
123+
if (q) {
124+
const like = `%${q}%`;
125+
where.push("(s.id LIKE ? OR s.name LIKE ? OR s.cwd LIKE ? OR s.model LIKE ?)");
126+
params.push(like, like, like, like);
127+
}
128+
return {
129+
whereSql: where.length > 0 ? `WHERE ${where.join(" AND ")}` : "",
130+
params,
131+
};
132+
}
133+
100134
return {
101135
getById(id: string): SessionRow | undefined {
102136
return toRow(getByIdStmt.get(id) as Record<string, unknown> | undefined);
@@ -110,6 +144,11 @@ export function createSessionStore(db: DatabaseSync) {
110144
return rowsToList(getActiveStmt.all() as Record<string, unknown>[]);
111145
},
112146

147+
getDetailsById(id: string): SessionWithAgents | undefined {
148+
const row = getDetailsByIdStmt.get(id) as Record<string, unknown> | undefined;
149+
return row ? detailRowsToList([row])[0] : undefined;
150+
},
151+
113152
getActiveWithDetails(): SessionWithAgents[] {
114153
return detailRowsToList(
115154
getActiveWithDetailsStmt.all() as Record<string, unknown>[],
@@ -133,6 +172,27 @@ export function createSessionStore(db: DatabaseSync) {
133172
];
134173
},
135174

175+
getPage(request?: SessionPageRequest): SessionPage {
176+
const { limit, offset, status, q } = coercePageRequest(request);
177+
const { whereSql, params } = pageWhereClause(status, q);
178+
const totalRow = db.prepare(
179+
`SELECT COUNT(*) as count FROM sessions s ${whereSql}`,
180+
).get(...params) as { count: number };
181+
const rows = db.prepare(`
182+
${SESSION_DETAIL_SELECT}
183+
${whereSql}
184+
ORDER BY s.started_at DESC
185+
LIMIT ? OFFSET ?
186+
`).all(...params, limit, offset) as Record<string, unknown>[];
187+
188+
return {
189+
sessions: detailRowsToList(rows),
190+
total: totalRow.count,
191+
limit,
192+
offset,
193+
};
194+
},
195+
136196
invalidateHistoricalDetails(): void {
137197
historicalDetailsCache = null;
138198
},

apps/desktop/src/main/preload-design-system.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import type {
77
EventCountByType,
88
EventRow,
99
EventWithSession,
10+
SessionPage,
11+
SessionPageRequest,
1012
SessionRow,
1113
SessionWithAgents,
1214
TokenAnalytics,
@@ -18,10 +20,12 @@ const designSystemDashboardApi = {
1820
db: {
1921
getSessions: () => ipcRenderer.invoke("desktop:db:get-sessions") as Promise<SessionRow[]>,
2022
getSession: (id: string) => ipcRenderer.invoke("desktop:db:get-session", id) as Promise<SessionRow | undefined>,
23+
getSessionDetails: (id: string) => ipcRenderer.invoke("desktop:db:get-session-details", id) as Promise<SessionWithAgents | undefined>,
2124
getAgents: (sessionId: string) => ipcRenderer.invoke("desktop:db:get-agents", sessionId) as Promise<AgentRow[]>,
2225
getEvents: (sessionId: string, agentId?: string) => ipcRenderer.invoke("desktop:db:get-events", sessionId, agentId) as Promise<EventRow[]>,
2326
getDashboardSummary: () => ipcRenderer.invoke("desktop:db:get-dashboard-summary") as Promise<DashboardSummary>,
2427
getSessionsWithDetails: () => ipcRenderer.invoke("desktop:db:get-sessions-with-details") as Promise<SessionWithAgents[]>,
28+
getSessionsPage: (request?: SessionPageRequest) => ipcRenderer.invoke("desktop:db:get-sessions-page", request) as Promise<SessionPage>,
2529
getEventFeed: () => ipcRenderer.invoke("desktop:db:get-event-feed") as Promise<EventWithSession[]>,
2630
getEventsWithSession: (sessionId: string) => ipcRenderer.invoke("desktop:db:get-events-with-session", sessionId) as Promise<EventWithSession[]>,
2731
getEventCountByType: () => ipcRenderer.invoke("desktop:db:get-event-count-by-type") as Promise<EventCountByType[]>,

0 commit comments

Comments
 (0)