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.
121 lines
5.6 KiB
TypeScript
121 lines
5.6 KiB
TypeScript
import { randomUUID } from 'node:crypto'
|
|
import fs, { mkdir, mkdtemp, readFile, rm, rmdir, writeFile } from 'node:fs/promises'
|
|
import { basename, dirname, join } from 'node:path'
|
|
import { tmpdir } from 'node:os'
|
|
|
|
const prefixes = ['goodbuddy-opencode-', 'goodbuddy-continue-', 'goodbuddy-opencode-plugin-', 'goodbuddy-native-dsh-']
|
|
const ownerPath = (directory: string): string => join(dirname(directory), `.${basename(directory)}.owner.json`)
|
|
|
|
/** Computing the application run path must not create anything at startup. */
|
|
export function runtimeTemporaryRoot(userDataPath: string, version: string, runId: string = randomUUID()): string {
|
|
if (![version, runId].every(value => /^[a-zA-Z0-9][a-zA-Z0-9.+_-]*$/u.test(value))) {
|
|
throw new Error('Invalid runtime temporary directory identity')
|
|
}
|
|
return join(userDataPath, 'temp', 'goodbuddy-runtime', version, runId, 'goodbuddy-runtime-launch')
|
|
}
|
|
|
|
export async function createRuntimeTemporaryDirectory(root: string, prefix: string): Promise<string> {
|
|
await ensureRuntimeTemporaryRoot(root)
|
|
const directory = await mkdtemp(join(root, prefix))
|
|
try {
|
|
// A missing child PID is deliberately ambiguous if the creator dies during spawn.
|
|
await writeFile(ownerPath(directory), JSON.stringify({ creatorPid: process.pid, childPid: null }), { mode: 0o600 })
|
|
return directory
|
|
} catch (error) {
|
|
await removeRuntimeTemporaryDirectory(directory)
|
|
throw error
|
|
}
|
|
}
|
|
|
|
export async function ensureRuntimeTemporaryRoot(root: string): Promise<void> {
|
|
await mkdir(root, { recursive: true, mode: 0o700 })
|
|
if (basename(root) !== 'goodbuddy-runtime-launch') return
|
|
await writeFile(ownerPath(root), JSON.stringify({ creatorPid: process.pid }), { flag: 'wx', mode: 0o600 })
|
|
.catch((error: NodeJS.ErrnoException) => { if (error.code !== 'EEXIST') throw error })
|
|
}
|
|
|
|
export async function recordRuntimeTemporaryChild(directory: string, childPid: number | undefined): Promise<void> {
|
|
await writeFile(ownerPath(directory), JSON.stringify({ creatorPid: process.pid, childPid: childPid ?? null }), { mode: 0o600 })
|
|
}
|
|
|
|
export async function removeRuntimeTemporaryDirectory(directory: string): Promise<void> {
|
|
// Keep the owner outside the tree so a partial recursive deletion cannot erase retry evidence.
|
|
await fs.rm(directory, { recursive: true, force: true })
|
|
await rm(ownerPath(directory), { force: true })
|
|
}
|
|
|
|
export function runtimeTemporaryProcessIsActive(pid: number): boolean {
|
|
try {
|
|
process.kill(pid, 0)
|
|
return true
|
|
} catch (error) {
|
|
// Access denial and PID reuse retain material rather than guessing ownership.
|
|
return (error as NodeJS.ErrnoException).code !== 'ESRCH'
|
|
}
|
|
}
|
|
|
|
/** Called in background after the single-instance check, never awaited by first paint. */
|
|
export async function cleanupRuntimeTemporaryDirectories(options: {
|
|
userDataPath: string
|
|
legacyTempRoot?: string
|
|
signal?: AbortSignal
|
|
maximumEntries?: number
|
|
isProcessActive?: (pid: number) => boolean
|
|
}): Promise<{ removed: string[]; retained: string[] }> {
|
|
const result = { removed: [] as string[], retained: [] as string[] }
|
|
let remaining = options.maximumEntries ?? 256
|
|
const active = options.isProcessActive ?? runtimeTemporaryProcessIsActive
|
|
async function* directories(root: string): AsyncGenerator<string> {
|
|
const handle = await fs.opendir(root).catch((error: NodeJS.ErrnoException) => {
|
|
if (error.code !== 'ENOENT') result.retained.push(root)
|
|
return undefined
|
|
})
|
|
if (!handle) return
|
|
for await (const entry of handle) {
|
|
if (options.signal?.aborted || remaining-- <= 0) return
|
|
if (entry.isDirectory() && !entry.isSymbolicLink()) yield entry.name
|
|
}
|
|
}
|
|
async function clean(directory: string): Promise<void> {
|
|
try {
|
|
const owner = JSON.parse(await readFile(ownerPath(directory), 'utf8')) as { creatorPid?: number; childPid?: number }
|
|
if (![owner.creatorPid, owner.childPid].every(pid => Number.isSafeInteger(pid) && pid! > 0) ||
|
|
active(owner.creatorPid!) || active(owner.childPid!)) {
|
|
result.retained.push(directory)
|
|
return
|
|
}
|
|
await removeRuntimeTemporaryDirectory(directory)
|
|
result.removed.push(directory)
|
|
} catch {
|
|
result.retained.push(directory)
|
|
}
|
|
}
|
|
const root = join(options.userDataPath, 'temp', 'goodbuddy-runtime')
|
|
for await (const version of directories(root)) {
|
|
for await (const run of directories(join(root, version))) {
|
|
const launch = join(root, version, run, 'goodbuddy-runtime-launch')
|
|
const retainedBefore = result.retained.length
|
|
for await (const name of directories(launch)) {
|
|
if (prefixes.some(prefix => name.startsWith(prefix))) await clean(join(launch, name))
|
|
}
|
|
if (remaining > 0 && !options.signal?.aborted && result.retained.length === retainedBefore) {
|
|
try {
|
|
const owner = JSON.parse(await readFile(ownerPath(launch), 'utf8')) as { creatorPid: number }
|
|
if (Number.isSafeInteger(owner.creatorPid) && owner.creatorPid > 0 && !active(owner.creatorPid)) {
|
|
await removeRuntimeTemporaryDirectory(launch)
|
|
result.removed.push(launch)
|
|
}
|
|
} catch { /* Unmarked roots and failed removals remain for a later start. */ }
|
|
}
|
|
// Never recursively remove an unmarked run or version parent.
|
|
await rmdir(launch).catch(() => undefined)
|
|
await rmdir(join(root, version, run)).catch(() => undefined)
|
|
}
|
|
await rmdir(join(root, version)).catch(() => undefined)
|
|
}
|
|
for await (const name of directories(options.legacyTempRoot ?? tmpdir())) {
|
|
if (prefixes.some(prefix => name.startsWith(prefix))) await clean(join(options.legacyTempRoot ?? tmpdir(), name))
|
|
}
|
|
return result
|
|
}
|