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
+49 -42
View File
@@ -2,7 +2,7 @@ import spawn from 'cross-spawn'
import { parseContinueChecklist, runtimeChecklistSchema, type RuntimeChecklist } from '../../shared/runtime-checklist'
import { toolOperationSummary } from './tool-operation-summary'
import { createHash, randomBytes } from 'node:crypto'
import {
import fs, {
copyFile,
mkdir,
readFile,
@@ -55,6 +55,7 @@ import { readBoundedResponseText } from './bounded-response'
import { readBoundedFile } from '../workspace-file-access'
import { terminateProcessTreeAndWait } from './child-process-termination'
import { CONTINUE_HOST_LAYOUT_VERSION } from './continue-host-layout'
import { createRuntimeTemporaryDirectory, ensureRuntimeTemporaryRoot, recordRuntimeTemporaryChild, removeRuntimeTemporaryDirectory } from '../runtime-temporary-directory'
const supportedVersion = '1.5.47'
const supportedBundleHashes = new Set([
@@ -1066,8 +1067,9 @@ export class ContinueHostAdapter {
const stagingRoot = `${targetRoot}.staging-${crypto.randomUUID()}`
const stagingDist = join(stagingRoot, 'dist')
try {
await ensureRuntimeTemporaryRoot(this.options.cacheRoot)
await mkdir(stagingDist, { recursive: true })
await Promise.all([
const writes = await Promise.allSettled([
writeFile(join(stagingDist, 'index.js'), patched, 'utf8'),
copyFile(join(distribution, 'cn.js'), join(stagingDist, 'cn.js')),
copyFile(
@@ -1081,6 +1083,8 @@ export class ContinueHostAdapter {
),
copyFile(packagePath, join(stagingRoot, 'package.json'))
])
const failedWrite = writes.find(result => result.status === 'rejected')
if (failedWrite?.status === 'rejected') throw failedWrite.reason
await writeFile(
join(stagingRoot, '.ready'),
JSON.stringify({ sourceHash, patchedHash }),
@@ -1233,23 +1237,32 @@ export class ContinueHostAdapter {
private async writeTemporaryConfig(
prefix: string,
config: Record<string, unknown>
config: Record<string, unknown>,
runRoot: string
): Promise<string> {
await mkdir(this.options.cacheRoot, { recursive: true })
const configPath = join(
this.options.cacheRoot,
`${prefix}-${crypto.randomUUID()}.yaml`
runRoot,
`goodbuddy-${prefix}-${crypto.randomUUID()}.yaml`
)
await writeFile(configPath, JSON.stringify(config), {
encoding: 'utf8',
mode: 0o600,
flag: 'wx'
})
const contents = JSON.stringify(config)
const handle = await fs.open(configPath, 'wx', 0o600)
try {
try {
await handle.writeFile(contents, 'utf8')
} finally {
await handle.close()
}
} catch (error) {
await rm(configPath, { force: true })
throw error
}
return configPath
}
private async createRunConfig(
runOptions: ContinueHostRunOptions
runOptions: ContinueHostRunOptions,
runRoot: string
): Promise<string | undefined> {
const knowledgeCapability = runOptions.knowledgeCapability
const customMcpCapability = runOptions.customMcpCapability
@@ -1357,7 +1370,7 @@ export class ContinueHostAdapter {
...retainedServers,
...capabilityServers
]
}
}, runRoot
)
}
@@ -1447,21 +1460,23 @@ export class ContinueHostAdapter {
mcpServers: capabilityServers
}
: {})
})
}, runRoot)
}
private async createRunGlobalDirectory(): Promise<string> {
private async createRunGlobalDirectory(runRoot: string): Promise<string> {
const root = join(
this.options.cacheRoot,
`isolated-global-${crypto.randomUUID()}`
runRoot,
`goodbuddy-isolated-global-${crypto.randomUUID()}`
)
await mkdir(root, { recursive: false, mode: 0o700 })
const skillPackages = this.options.skillPackages ?? []
if (skillPackages.length === 0) {
try {
const skillPackages = this.options.skillPackages ?? []
if (skillPackages.length) await stageRuntimeSkillPackages(root, skillPackages, 'Continue')
return root
} catch (error) {
await rm(root, { recursive: true, force: true })
throw error
}
await stageRuntimeSkillPackages(root, skillPackages, 'Continue')
return root
}
async run(
@@ -1481,20 +1496,18 @@ export class ContinueHostAdapter {
}
let generatedConfigPath: string | undefined
let isolatedGlobalDirectory: string | undefined
const runRoot = await createRuntimeTemporaryDirectory(this.options.cacheRoot, 'goodbuddy-continue-')
let launchedChild: ContinueHostChild | undefined
let childClosed = false
try {
generatedConfigPath = await this.createRunConfig(runOptions)
generatedConfigPath = await this.createRunConfig(runOptions, runRoot)
const [{ entryPath }, port] = await Promise.all([
this.getPreparedHost(),
getAvailableLoopbackPort()
]).catch(async (error) => {
if (generatedConfigPath) {
await rm(generatedConfigPath, { force: true })
}
throw error
})
])
const token = randomBytes(32).toString('base64url')
const origin = `http://127.0.0.1:${port}`
isolatedGlobalDirectory = await this.createRunGlobalDirectory()
isolatedGlobalDirectory = await this.createRunGlobalDirectory(runRoot)
const args: string[] = []
const configPath =
generatedConfigPath ?? this.options.configPath.trim()
@@ -1574,12 +1587,15 @@ export class ContinueHostAdapter {
throw error
}
this.children.add(child)
launchedChild = child
child.once('close', () => { childClosed = true })
let childFailure: Error | undefined
child.once('error', (error) => {
childFailure = new Error('Continue 宿主进程启动失败', {
cause: error
})
})
await recordRuntimeTemporaryChild(runRoot, child.pid)
let stderrBytes = 0
child.stderr?.on('data', (chunk: Buffer | string) => {
stderrBytes += Buffer.byteLength(chunk)
@@ -1867,24 +1883,15 @@ export class ContinueHostAdapter {
} finally {
await this.terminate(child)
this.children.delete(child)
if (generatedConfigPath) {
await rm(generatedConfigPath, { force: true })
}
await rm(isolatedGlobalDirectory, {
recursive: true,
force: true
})
}
}
} finally {
if (generatedConfigPath) {
await rm(generatedConfigPath, { force: true })
if (launchedChild) {
await this.terminate(launchedChild)
this.children.delete(launchedChild)
}
if (isolatedGlobalDirectory) {
await rm(isolatedGlobalDirectory, {
recursive: true,
force: true
})
if (!launchedChild || childClosed || launchedChild.exitCode !== null || !launchedChild.pid) {
await removeRuntimeTemporaryDirectory(runRoot)
}
}
}