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
+9 -5
View File
@@ -12,6 +12,8 @@ import { SupervisionModelPool } from '../src/main/assistant/supervision-model-po
import { ModelAgentRuntime } from '../src/main/agent/model-runtime'
import type { SupervisionRunRequest } from '../src/shared/supervision-contracts'
import { supervisionReviewSettingsSchema } from '../src/shared/supervision-review-contracts'
import { ReadonlyQueryReader } from '../src/main/readonly-query-reader'
import { asyncSupervisionStorage } from '../tests/support/async-supervision-storage'
async function main() {
const [mode, parent, envPath] = process.argv.slice(2)
@@ -25,7 +27,8 @@ async function main() {
bundle: true, platform: 'node', format: 'cjs', logLevel: 'silent' })
const db = new AssistantDatabase(path)
db.initialize(directory)
db.enableReadonlyWorker(worker)
const reader = new ReadonlyQueryReader('assistant', path, worker)
const storage = asyncSupervisionStorage(db, undefined, reader).supervision
const sql = (db as unknown as { requireDatabase(): DatabaseSync }).requireDatabase()
const pool = new SupervisionModelPool()
const report: Record<string, unknown> = { mode, directory, externalCalls: 0 }
@@ -49,7 +52,7 @@ async function main() {
const counts = () => ['supervision_entities', 'supervision_events', 'supervision_event_entities', 'supervision_sources'].map(table =>
[table, Number(sql.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get()!.n)])
const before = counts()
await db.supervisionCandidates(request, { projectId: projects[0]!.id, crossProject: false })
await storage.supervisionCandidates(request, { projectId: projects[0]!.id, crossProject: false })
const legacy = sql.prepare(`SELECT e.id FROM supervision_entities e WHERE e.confirmation_state != 'revoked' AND EXISTS (
SELECT 1 FROM supervision_event_entities ee JOIN supervision_events ev ON ev.id = ee.event_id
WHERE ee.entity_id = e.id AND ev.superseded_by IS NULL AND COALESCE(ev.project_id, '') = ?)
@@ -61,14 +64,14 @@ async function main() {
const legacyMs = performance.now() - legacyStart
const start = performance.now()
for (const project of projects) {
const candidates = await db.supervisionCandidates(request, { projectId: project.id, crossProject: false })
const candidates = await storage.supervisionCandidates(request, { projectId: project.id, crossProject: false })
assert.equal(candidates.length, 100)
assert(candidates.every(item => item.label.startsWith(project.id + ':')))
assert.deepEqual(candidates.map(item => item.id), expected.get(project.id))
}
const isolatedMs = performance.now() - start
const crossStart = performance.now()
const candidates = await db.supervisionCandidates(request, { projectId: projects[0]!.id, crossProject: true })
const candidates = await storage.supervisionCandidates(request, { projectId: projects[0]!.id, crossProject: true })
assert.equal(candidates.length, 100)
rounds.push({ legacyMs, isolatedMs, crossProjectMs: performance.now() - crossStart })
}
@@ -106,7 +109,7 @@ async function main() {
calls++
return fetch(input, init)
} })
const service = createProductionSupervisorService(db, async () => ({ supervisorOrganizeTimeoutSeconds: 45,
const service = createProductionSupervisorService(storage, async () => ({ supervisorOrganizeTimeoutSeconds: 45,
supervisionReview: supervisionReviewSettingsSchema.parse({ crossProject: false, experienceMinEvents: 200 }) }), async () => runtime, pool)
const result = await service.run(request)
assert.equal(result.status, 'completed')
@@ -133,6 +136,7 @@ async function main() {
} finally {
report.externalCalls = calls
report.elapsedMs = performance.now() - started
await reader.close()
db.close()
pool.dispose()
report.databaseBytes = (await stat(path)).size