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:
@@ -6,12 +6,19 @@ import type { AgentPromptModelProfile } from '../shared/model-bridge-contracts'
|
||||
import { REMOTE_MODEL_GATEWAY_LIMITS } from '../shared/remote-model-gateway-contracts'
|
||||
import {
|
||||
AgentModelCallLedger,
|
||||
AgentModelGateway
|
||||
AgentModelGateway,
|
||||
type ModelCallLedger
|
||||
} from './agent-model-gateway'
|
||||
|
||||
const temporary: string[] = []
|
||||
const secret = 'provider-secret-never-persist'
|
||||
|
||||
function gate() {
|
||||
let resolve!: () => void
|
||||
const promise = new Promise<void>(done => { resolve = done })
|
||||
return { promise, resolve }
|
||||
}
|
||||
|
||||
function setup(fetcher: typeof fetch) {
|
||||
const root = mkdtempSync(join(tmpdir(), 'goodbuddy-agent-model-'))
|
||||
temporary.push(root)
|
||||
@@ -66,6 +73,70 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe('AgentModelGateway', () => {
|
||||
it('awaits host claim, completion and delivery before their dependent actions', async () => {
|
||||
const claimGate = gate()
|
||||
const completeGate = gate()
|
||||
const deliveryGate = gate()
|
||||
const ledger: ModelCallLedger = {
|
||||
claim: vi.fn(() => claimGate.promise), complete: vi.fn(() => completeGate.promise),
|
||||
delivered: vi.fn(() => deliveryGate.promise), outcomeUnknown: vi.fn(), get: vi.fn(), close: vi.fn()
|
||||
}
|
||||
const fetcher = vi.fn<typeof fetch>(async () => new Response('{}'))
|
||||
const gateway = new AgentModelGateway({ ledger, fetcher })
|
||||
const settled = vi.fn()
|
||||
const pending = gateway.dispatch(context, request, new AbortController().signal).then(result => { settled(); return result })
|
||||
await Promise.resolve()
|
||||
expect(fetcher).not.toHaveBeenCalled()
|
||||
claimGate.resolve()
|
||||
await vi.waitFor(() => expect(ledger.complete).toHaveBeenCalledOnce())
|
||||
expect(settled).not.toHaveBeenCalled()
|
||||
completeGate.resolve()
|
||||
const result = await pending
|
||||
const acknowledged = vi.fn()
|
||||
const delivery = result.acknowledgeDelivery().then(acknowledged)
|
||||
await Promise.resolve()
|
||||
expect(acknowledged).not.toHaveBeenCalled()
|
||||
deliveryGate.resolve()
|
||||
await delivery
|
||||
expect(acknowledged).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it.each(['claim', 'complete', 'delivered'] as const)('propagates a rejected host %s without claiming success', async method => {
|
||||
const failure = new Error('storage unavailable')
|
||||
const ledger: ModelCallLedger = {
|
||||
claim: vi.fn(), complete: vi.fn(), delivered: vi.fn(), outcomeUnknown: vi.fn(), get: vi.fn(), close: vi.fn(),
|
||||
[method]: vi.fn(async () => { throw failure })
|
||||
}
|
||||
const fetcher = vi.fn<typeof fetch>(async () => new Response('{}'))
|
||||
const gateway = new AgentModelGateway({ ledger, fetcher })
|
||||
const pending = gateway.dispatch(context, request, new AbortController().signal)
|
||||
if (method === 'delivered') await expect((await pending).acknowledgeDelivery()).rejects.toBe(failure)
|
||||
else await expect(pending).rejects.toBe(failure)
|
||||
expect(fetcher).toHaveBeenCalledTimes(method === 'claim' ? 0 : 1)
|
||||
})
|
||||
|
||||
it('settles cancellation during claim only after recording uncertainty, without dispatch', async () => {
|
||||
const claimGate = gate()
|
||||
const unknownGate = gate()
|
||||
const ledger: ModelCallLedger = {
|
||||
claim: () => claimGate.promise, complete: vi.fn(), delivered: vi.fn(),
|
||||
outcomeUnknown: vi.fn(() => unknownGate.promise), get: vi.fn(), close: vi.fn()
|
||||
}
|
||||
const fetcher = vi.fn<typeof fetch>()
|
||||
const gateway = new AgentModelGateway({ ledger, fetcher })
|
||||
const controller = new AbortController()
|
||||
const settled = vi.fn()
|
||||
const pending = gateway.dispatch(context, request, controller.signal).catch(error => { settled(); throw error })
|
||||
const rejected = expect(pending).rejects.toMatchObject({ code: 'cancelled' })
|
||||
controller.abort()
|
||||
claimGate.resolve()
|
||||
await vi.waitFor(() => expect(ledger.outcomeUnknown).toHaveBeenCalledOnce())
|
||||
expect(fetcher).not.toHaveBeenCalled()
|
||||
expect(settled).not.toHaveBeenCalled()
|
||||
unknownGate.resolve()
|
||||
await rejected
|
||||
})
|
||||
|
||||
it('keeps a slow provider request alive past 60 seconds and still cancels it', async () => {
|
||||
vi.useFakeTimers()
|
||||
let providerSignal: AbortSignal | undefined
|
||||
|
||||
Reference in New Issue
Block a user