Files
goodbuddy/tests/support/paged-output-backing.ts
mesalogo d80d42b032 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.
2026-10-06 15:40:18 +08:00

80 lines
3.1 KiB
TypeScript

import { mkdir, mkdtemp, open, rm, stat, type FileHandle } from 'node:fs/promises'
import { join, resolve } from 'node:path'
import type { PagedOutputBackingStore } from '../../src/main/agent/paged-output-store'
export const outputFixtureRoot = resolve('temp/goodbuddy-output-upgrade')
// Real asynchronous file IO for contract tests, not a production storage owner.
export class FileOutputBacking implements PagedOutputBackingStore {
private directory?: Promise<string>
private readonly entries = new Map<string, { owner: string; path: string; file?: FileHandle }>()
writes = 0
maximumWriteBytes = 0
async create(owner: string, handle: string): Promise<void> {
this.directory ??= mkdir(outputFixtureRoot, { recursive: true }).then(() =>
mkdtemp(join(outputFixtureRoot, 'backing-')))
const path = join(await this.directory, handle.replace(':', '-'))
const file = await open(path, 'wx+')
this.entries.set(handle, { owner, path, file })
}
private entry(owner: string, handle: string) {
const entry = this.entries.get(handle)
if (!entry || entry.owner !== owner) throw new Error('Unknown output owner')
return entry
}
async append(owner: string, handle: string, bytes: Uint8Array): Promise<void> {
const { file } = this.entry(owner, handle)
if (!file) throw new Error('Output already finished')
this.writes += 1
this.maximumWriteBytes = Math.max(this.maximumWriteBytes, bytes.byteLength)
for (let offset = 0; offset < bytes.byteLength;) {
const { bytesWritten } = await file.write(bytes, offset, bytes.byteLength - offset)
if (!bytesWritten) throw new Error('Output write made no progress')
offset += bytesWritten
}
}
async finish(owner: string, handle: string, totalBytes: number): Promise<void> {
const entry = this.entry(owner, handle)
await entry.file?.close()
entry.file = undefined
if ((await stat(entry.path)).size !== totalBytes) throw new Error('Output size mismatch')
}
async read(owner: string, handle: string, cursor: number, length: number): Promise<Uint8Array> {
const file = await open(this.entry(owner, handle).path, 'r')
try {
const bytes = Buffer.alloc(length)
let offset = 0
while (offset < length) {
const { bytesRead } = await file.read(bytes, offset, length - offset, cursor + offset)
if (!bytesRead) break
offset += bytesRead
}
return bytes.subarray(0, offset)
} finally {
await file.close()
}
}
async release(owner: string, handle: string): Promise<void> {
if (!this.entries.has(handle)) return
const entry = this.entry(owner, handle)
await entry.file?.close()
entry.file = undefined
await rm(entry.path, { force: true })
this.entries.delete(handle)
}
get count(): number { return this.entries.size }
get createdDirectory(): boolean { return this.directory !== undefined }
async dispose(): Promise<void> {
for (const [handle, entry] of this.entries) await this.release(entry.owner, handle)
if (this.directory) await rm(await this.directory, { recursive: true, force: true })
}
}