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:
@@ -309,6 +309,15 @@ export class AgentModelCallLedger {
|
||||
}
|
||||
}
|
||||
|
||||
/** Remote Agent uses the concrete ledger; desktop awaits its storage host. */
|
||||
export type ModelCallLedger = {
|
||||
[K in keyof AgentModelCallLedger]: (
|
||||
...args: Parameters<AgentModelCallLedger[K]>
|
||||
) => ReturnType<AgentModelCallLedger[K]> | Promise<ReturnType<AgentModelCallLedger[K]>>
|
||||
}
|
||||
|
||||
export type ModelCallLedgerFactory = (path: string) => Promise<ModelCallLedger>
|
||||
|
||||
export type AgentModelGatewayContext = {
|
||||
bindingId: string
|
||||
operationId: string
|
||||
@@ -319,11 +328,11 @@ export type AgentModelGatewayContext = {
|
||||
}
|
||||
|
||||
export class AgentModelGateway {
|
||||
readonly #ledger: AgentModelCallLedger
|
||||
readonly #ledger: ModelCallLedger
|
||||
readonly #fetch: typeof fetch
|
||||
|
||||
constructor(options: {
|
||||
ledger: AgentModelCallLedger
|
||||
ledger: ModelCallLedger
|
||||
fetcher?: typeof fetch
|
||||
}) {
|
||||
this.#ledger = options.ledger
|
||||
@@ -348,7 +357,7 @@ export class AgentModelGateway {
|
||||
)
|
||||
const requestDigest = sha256(prepared.canonicalRequest)
|
||||
const callId = createAgentModelCallId(context)
|
||||
this.#ledger.claim({
|
||||
await this.#ledger.claim({
|
||||
callId,
|
||||
bindingId: context.bindingId,
|
||||
operationId: context.operationId,
|
||||
@@ -366,6 +375,7 @@ export class AgentModelGateway {
|
||||
timer?.unref?.()
|
||||
let response: Response
|
||||
try {
|
||||
signal.throwIfAborted()
|
||||
response = await this.#fetch(prepared.url, {
|
||||
method: 'POST',
|
||||
redirect: 'error',
|
||||
@@ -380,7 +390,7 @@ export class AgentModelGateway {
|
||||
: signal.aborted
|
||||
? 'cancelled'
|
||||
: 'outcome-unknown'
|
||||
this.#ledger.outcomeUnknown(callId, code)
|
||||
await this.#ledger.outcomeUnknown(callId, code)
|
||||
throw new AgentModelGatewayError(
|
||||
code,
|
||||
'Provider dispatch outcome is unknown'
|
||||
@@ -403,7 +413,7 @@ export class AgentModelGateway {
|
||||
: error instanceof BoundedResponseTooLargeError
|
||||
? 'response-too-large'
|
||||
: 'outcome-unknown'
|
||||
this.#ledger.outcomeUnknown(callId, code)
|
||||
await this.#ledger.outcomeUnknown(callId, code)
|
||||
throw error instanceof AgentModelGatewayError
|
||||
? error
|
||||
: new AgentModelGatewayError(
|
||||
@@ -418,7 +428,7 @@ export class AgentModelGateway {
|
||||
clearTimeout(timer)
|
||||
if (signal.aborted || timeout.signal.aborted) {
|
||||
const code = timeout.signal.aborted ? 'timeout' : 'cancelled'
|
||||
this.#ledger.outcomeUnknown(callId, code)
|
||||
await this.#ledger.outcomeUnknown(callId, code)
|
||||
throw new AgentModelGatewayError(
|
||||
code,
|
||||
'Provider response arrived after cancellation'
|
||||
@@ -441,7 +451,7 @@ export class AgentModelGateway {
|
||||
headers,
|
||||
bodyBase64: Buffer.from(bytes).toString('base64')
|
||||
})
|
||||
this.#ledger.complete(callId)
|
||||
await this.#ledger.complete(callId)
|
||||
let finalized = false
|
||||
return {
|
||||
response: bridgeResponse,
|
||||
@@ -453,7 +463,7 @@ export class AgentModelGateway {
|
||||
)
|
||||
}
|
||||
finalized = true
|
||||
this.#ledger.delivered(callId)
|
||||
await this.#ledger.delivered(callId)
|
||||
},
|
||||
failDelivery: () => {
|
||||
finalized = true
|
||||
|
||||
Reference in New Issue
Block a user