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
+20 -8
View File
@@ -1,9 +1,9 @@
import { randomUUID } from 'node:crypto'
import { execFile } from 'node:child_process'
import { cp, mkdir, mkdtemp, readFile, rename, rm, stat, writeFile } from 'node:fs/promises'
import { cp, mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { promisify } from 'node:util'
import { AgentModelCallLedger, AgentModelGateway } from '../../agent-daemon/agent-model-gateway'
import { AgentModelGateway, type ModelCallLedger, type ModelCallLedgerFactory } from '../../agent-daemon/agent-model-gateway'
import {
ModelBridgeLoopbackProxy,
MODEL_BRIDGE_SDK_AUTH_SENTINEL,
@@ -20,6 +20,7 @@ import { ContinueHostAdapter, loadContinueConfig } from './continue-host-adapter
import { detectRuntimeBinary } from './runtime-discovery'
import { applyLaunchEnvironmentPath, buildCredentialFilteredUserEnvironment, runtimePrivacyEnvironment } from './process-environment'
import { stageRuntimeSkillPackages } from './runtime-skill-packages'
import { createRuntimeTemporaryDirectory, recordRuntimeTemporaryChild, removeRuntimeTemporaryDirectory, runtimeTemporaryProcessIsActive } from '../runtime-temporary-directory'
export type NativeTerminalClientInput = {
projectId: string
@@ -34,9 +35,12 @@ export type NativeTerminalClientInput = {
}
export type NativeTerminalClientOptions = {
openModelCallLedger: ModelCallLedgerFactory
terminalManager: Pick<TerminalSessionManager, 'create'>
/** GoodBuddy user-data directory; native history is retained here. */
rootDirectory: string
/** Application version/run launch root; retained histories stay in rootDirectory. */
temporaryRoot?: string
bundledRuntimePaths: BundledRuntimePaths
/** Standard Node resolved by the local tool environment, never Electron. */
nodeExecutable?: string
@@ -137,9 +141,12 @@ export class NativeTerminalClient {
}
const root = this.options.rootDirectory
await mkdir(root, { recursive: true })
const temporary = await mkdtemp(join(root, 'launch-'))
const temporary = await createRuntimeTemporaryDirectory(
this.options.temporaryRoot ?? join(root, 'goodbuddy-runtime-launch'), `goodbuddy-${input.runtime}-`)
let childPid: number | undefined
const history = join(root, input.runtime, randomUUID())
let ledger: AgentModelCallLedger | undefined
let ledger: ModelCallLedger | undefined
let ledgerOpening = false
let gateway: AgentModelGateway
const bindingId = randomUUID()
const proxy = new ModelBridgeLoopbackProxy({ exchange: (request, context) => gateway.dispatch({
@@ -148,13 +155,16 @@ export class NativeTerminalClient {
}, request, context.signal) })
let disposal: Promise<void> | undefined
const dispose = (): Promise<void> => disposal ??= (async () => {
if (childPid && runtimeTemporaryProcessIsActive(childPid)) throw new Error('Native terminal child exit is unconfirmed')
try { await proxy.close() } finally {
ledger?.close()
await rm(temporary, { recursive: true, force: true })
await ledger?.close()
// A lost open reply can leave a live connection in the host.
if (!ledgerOpening || ledger) await removeRuntimeTemporaryDirectory(temporary)
}
})()
try {
ledger = new AgentModelCallLedger(join(temporary, 'model-calls.sqlite'))
ledgerOpening = true
ledger = await this.options.openModelCallLedger(join(temporary, 'model-calls.sqlite'))
gateway = new AgentModelGateway({ ledger, fetcher: this.options.fetcher })
await mkdir(history, { recursive: true })
const origin = await proxy.listen()
@@ -212,7 +222,9 @@ export class NativeTerminalClient {
})
args.push('--model', config.model)
}
return { spawnSpec: { executable, args, cwd: input.directory, env, label: input.runtime }, title: `${input.runtime} · ${input.projectName}`, dispose }
return { spawnSpec: { executable, args, cwd: input.directory, env, label: input.runtime,
onSpawn: async pid => { childPid = pid; await recordRuntimeTemporaryChild(temporary, pid) }
}, title: `${input.runtime} · ${input.projectName}`, dispose }
} catch (error) {
await dispose()
throw error