Files
orca/src/main/ai-vault/session-scanner-opencode-sqlite-worker-entry.ts
T
Brennan Benson ca4003a9e3 Keep Agent Session History from freezing the app on huge OpenCode databases (#8924)
* fix(agent-history): keep the app responsive while scanning huge OpenCode databases (#8864)

Opening or refreshing Agent Session History froze the entire app when
OpenCode's opencode.db had grown to multiple GB (reporter: 29 GB with
20+ live opencode writers). All OpenCode SQLite reads ran synchronously
(node:sqlite DatabaseSync) on the Electron main-process event loop, the
discovery query evaluated a COUNT+json_extract subquery for every
session before LIMIT, and the preview query JSON-parsed every part blob
of each session. Live writers bump session.time_updated continuously,
so the mtime parse cache missed every 15s/focus/manual refresh and the
multi-tens-of-seconds freeze repeated forever.

Fix:
- Run OpenCode SQLite discovery and per-session parsing on a persistent
  worker thread (lazy spawn, unref'd, idle teardown, FIFO dispatch,
  per-call timeouts, crash-loop cap). Faults surface as per-source scan
  issues instead of stalls; other providers' results always arrive.
- Fall back to the in-process reader when no worker bundle exists,
  surfacing a degraded-mode scan issue so a persistent fallback cannot
  silently reintroduce the hang.
- Bound the queries: sort+LIMIT sessions before computing message
  counts, and source previews from the newest 100 messages via the
  (session_id, time_created, id) index instead of scanning every part.

Measured on a 22 GB synthetic DB matching the reporter's shape: main-
process IPC RTT during a fully cache-invalidated scan went from a
32.8 s continuous block (UI click timeout) to 1 ms max, with the panel
populating normally (previews and message counts intact).

* fix(agent-history): drop the cached worker handle after a clean exit and document worker-client exports

A worker that exits cleanly on its own left this.worker pointing at the dead
thread; the next request would post into it and stall to its timeout instead
of respawning. Clean idle exits now drop the handle without counting as a
death. Also adds JSDoc to the new exported scan-worker surfaces.

* fix(agent-history): keep OpenCode scans off the main thread

* fix(agent-history): align OpenCode recency ordering
2026-07-19 14:40:54 -07:00

59 lines
2.1 KiB
TypeScript

import { parentPort } from 'node:worker_threads'
import type { AiVaultScanIssue } from '../../shared/ai-vault-types'
import { listOpenCodeSqliteSessions } from './session-scanner-opencode-sqlite-list'
import { parseOpenCodeSqliteSession } from './session-scanner-opencode-sqlite'
import type {
OpenCodeSqliteWorkerRequest,
OpenCodeSqliteWorkerResponse
} from './session-scanner-opencode-sqlite-worker-protocol'
// Why (#8864): OpenCode SQLite reads use synchronous node:sqlite. Running them
// on this worker thread keeps the multi-GB-DB scan off the Electron main-process
// event loop. The client dispatches one request at a time, so this loop stays
// serial; imports must remain electron-free (see the worker-protocol note).
if (!parentPort) {
throw new Error('OpenCode SQLite worker must run with a parent port.')
}
const port = parentPort
async function handleRequest(
request: OpenCodeSqliteWorkerRequest
): Promise<OpenCodeSqliteWorkerResponse> {
try {
if (request.kind === 'list') {
const issues: AiVaultScanIssue[] = []
const candidates = await listOpenCodeSqliteSessions({
dbPaths: request.dbPaths,
limit: request.limit,
issues
})
return { id: request.id, ok: true, value: { candidates, issues } }
}
const session = await parseOpenCodeSqliteSession({
dbPath: request.dbPath,
sessionId: request.sessionId,
platform: request.platform
})
return { id: request.id, ok: true, value: session }
} catch (err) {
return { id: request.id, ok: false, error: err instanceof Error ? err.message : String(err) }
}
}
port.on('message', (request: OpenCodeSqliteWorkerRequest) => {
void handleRequest(request).then((response) => {
try {
port.postMessage(response)
} catch {
// A non-cloneable result would otherwise post nothing and leave the client
// waiting out its timeout; fail that request fast instead.
port.postMessage({
id: request.id,
ok: false,
error: 'OpenCode SQLite worker result could not be serialized.'
})
}
})
})