feat(storage): move desktop SQLite ownership into a storage utility process

Main no longer opens desktop business SQLite. A single utility-process
storage owner holds the Assistant, Knowledge, attachment, document-result,
remote-binding and native model-ledger databases and runs whole domain
transactions; Main and Runtime callers use explicit async domain operations.

- Remove the Main readWithFallback path and the supervision worker's
  separate business write connection; manifests are scanned read-only and
  committed by the sole writer.
- Remote events are ACKed only after commit; process loss reports
  unconfirmed writes instead of replaying them.
- Use native utilityProcess messaging with one in-flight bound (32) and
  ordered waiting instead of capacity rejection and retries.
- Start the storage process before the window so startup overlaps.
- Keep short tool output in memory and store full output once in owned
  storage; paging, branching, restart and deletion reuse that copy.
- Put required runtime launch files under goodbuddy-prefixed owned temp
  directories and reclaim confirmed stale ones; stop writing duplicate
  parsed.md.
- Add lint guards for SQLite ownership and architecture, concurrency
  acceptance fixtures, and maintenance/architecture documentation.

Validation: typecheck and lint pass. Alternating baseline (edf019aa) vs
candidate runs show no regression on single-session, C01, C02, C04 and a
5-minute C06; Main p99 drops from ~21 ms to ~16.7 ms under concurrency.
Fresh portable copy (1.75M events) upgrade content unchanged; real-model
output paging passes with 4 external requests. Full npm test runs had
1-9 different timing-sensitive failures per run that pass when rerun.
Not covered: C03 review publication, C07 source body, app-level C05.
This commit is contained in:
mesalogo
2026-10-06 15:40:18 +08:00
parent bc677a3718
commit d80d42b032
229 changed files with 13632 additions and 3800 deletions
+31 -17
View File
@@ -3,16 +3,16 @@ import { z } from 'zod'
import { AssistantDatabase } from './assistant/assistant-database'
import { KnowledgeDatabase } from './knowledge/knowledge-database'
import type { ReadonlyQueryKind, ReadonlyQueryRequest } from './readonly-query-reader'
import type { ReviewState } from './assistant/supervision-review-store'
import type { ReviewRevisionRow, ReviewState } from './assistant/supervision-review-store'
// Read-only query worker (PERF-15/16 first slice). One worker per database file.
// It reuses the production classes on a read-only connection, so results are
// produced by exactly the same code as the synchronous path.
// This packaged entry also hosts the WAL checkpointer and the dedicated,
// separately connected supervision manifest writer.
// This packaged entry also hosts the WAL checkpointer. Supervision manifests
// are scanned read-only and transferred in pages to their sole business writer.
const { kind, databasePath, intervalMs } = workerData as {
kind: ReadonlyQueryKind | 'checkpoint' | 'supervision'
kind: ReadonlyQueryKind | 'checkpoint'
databasePath: string
intervalMs?: number
}
@@ -22,8 +22,7 @@ let handlers: Record<string, Handler>
let close: () => void
if (kind === 'checkpoint') {
// WAL checkpointer for AssistantDatabase.enableWalCheckpointWorker (PERF-15):
// PASSIVE checkpoints never wait for, or block, Main's readers and writers.
// Storage-owner-managed PASSIVE checkpoints never wait for business writers.
const database = new AssistantDatabase(databasePath)
database.openCheckpointer()
const timer = setInterval(() => {
@@ -39,22 +38,14 @@ if (kind === 'checkpoint') {
}
handlers = {}
parentPort!.postMessage({ ready: true })
} else if (kind === 'supervision') {
// Only manifest initialization and resume validation write here. Publication
// and checkpoints remain in Main's existing atomic result transaction.
const database = new AssistantDatabase(databasePath)
database.openSupervisionWorker()
close = () => database.close()
handlers = {
initialize: ([runId, state], signal) => database.supervisionReviewStore(signal).initializeSources(String(runId), state as ReviewState, signal),
resume: ([runId], signal) => database.supervisionReviewStore(signal).resume(String(runId), signal)
}
} else if (kind === 'knowledge') {
const database = new KnowledgeDatabase(databasePath)
database.openReadOnly()
close = () => database.close()
handlers = {
search: ([options]) => database.search(options as Parameters<KnowledgeDatabase['search']>[0]),
vectorSearch: ([options], signal) => database.vectorSearch({ ...(options as Parameters<KnowledgeDatabase['vectorSearch']>[0]), signal }),
graphSearch: args => database.graphSearch(...args as Parameters<KnowledgeDatabase['graphSearch']>),
hybridSearchWithDiagnostics: ([options], signal) => database.hybridSearchWithDiagnostics({
...(options as Parameters<KnowledgeDatabase['hybridSearchWithDiagnostics']>[0]), signal
})
@@ -63,7 +54,25 @@ if (kind === 'checkpoint') {
const database = new AssistantDatabase(databasePath)
database.openReadOnly()
close = () => database.close()
let manifestRunId: string | undefined
handlers = {
reviewManifestScan: ([runId, state], signal) => {
if (manifestRunId !== undefined) throw new Error('Review manifest reader is busy')
manifestRunId = String(runId)
try { database.supervisionReviewStore(signal).createSourceManifest(state as ReviewState, signal) }
catch (error) { manifestRunId = undefined; throw error }
},
reviewManifestPage: ([runId, offset]) => {
if (manifestRunId !== runId) throw new Error('Review manifest reader lost its snapshot; resume initialization')
return database.supervisionReviewStore().sourceManifestPage(Number(offset))
},
reviewManifestRelease: ([runId]) => {
if (manifestRunId !== runId) return
database.supervisionReviewStore().releaseSourceManifest()
manifestRunId = undefined
},
reviewChangedSource: ([rows], signal) => database.readSnapshot(() =>
database.supervisionReviewStore(signal).changedSource(rows as ReviewRevisionRow[], signal)),
readStoryGraph: ([name, input, projectId], signal) => database.readStoryGraph(
name as Parameters<AssistantDatabase['readStoryGraph']>[0], input, projectId as string | undefined, signal),
supervisionOverview: ([limit, target, resultId]) => database.readSnapshot(() => database.listSupervisionResults(
@@ -106,7 +115,12 @@ function serializeError(error: unknown): { name: string; message: string; issues
return { name: 'Error', message: String(error) }
}
parentPort!.on('message', (message: ReadonlyQueryRequest) => {
parentPort!.on('message', (message: ReadonlyQueryRequest | { type: 'close' }) => {
if (message.type === 'close') {
close()
parentPort!.close()
return
}
try {
const handler = Object.hasOwn(handlers, message.op) ? handlers[message.op] : undefined
if (!handler) throw new Error(`Unknown readonly query: ${message.op}`)