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
+22 -4
View File
@@ -32,6 +32,7 @@ import {
type ModelToolCallContext,
type ModelToolDefinition,
type ModelToolProviderLike,
type ModelToolProviderProgrammingOptions,
type ModelToolResult,
type ModelToolResultPart,
type ModelSubagentRequestContext
@@ -87,6 +88,7 @@ type ConversationMessage = {
// the parent's browser tab and its owning conversation. They acquire no lease.
type ModelExecutionRequest = AgentExecutionRequest & {
browserConversationId?: string
outputHistory?: ModelToolCallContext['outputHistory']
}
type ProviderConversationMessage = Pick<
@@ -233,6 +235,8 @@ function getCurrentTimeInstruction(now = new Date()): string {
}
export type ModelRuntimeOptions = {
outputStore?: ModelToolProviderProgrammingOptions['outputStore']
outputAdopt?: ModelToolProviderProgrammingOptions['outputAdopt']
apiKey?: string
baseUrl: string
model: string
@@ -867,7 +871,7 @@ function getChatToolResultText(parts: ModelToolResultPart[]): string {
.join('\n\n')
}
function getToolResultPreview(parts: ModelToolResultPart[]): string {
function getToolResultPreview(parts: ModelToolResultPart[], limit = 16_000): string {
let imageNumber = 0
return parts
.map((part) => {
@@ -879,7 +883,7 @@ function getToolResultPreview(parts: ModelToolResultPart[]): string {
})
.filter(Boolean)
.join('\n\n')
.slice(0, 16_000)
.slice(0, limit)
}
function createToolErrorResult(
@@ -1666,6 +1670,7 @@ export class ModelAgentRuntime implements AgentRuntime {
const directModelSubagentService =
options.directModelSubagentScheduler
? new DirectModelSubagentService<ModelSubagentRequestContext>({
outputStore: options.outputStore,
scheduler: options.directModelSubagentScheduler,
runChild: (input) =>
this.runDirectModelSubagent(input),
@@ -1682,7 +1687,10 @@ export class ModelAgentRuntime implements AgentRuntime {
options.knowledgeGateway,
options.webSearchEnabled,
{
outputStore: options.outputStore,
outputAdopt: options.outputAdopt,
processService: new LocalDirectModelProcessService({
outputStore: options.outputStore,
environment: options.launchEnvironmentProvider?.(),
...(options.ripgrepExecutablePath
? { toolBinDirectory: dirname(options.ripgrepExecutablePath) }
@@ -1704,6 +1712,7 @@ export class ModelAgentRuntime implements AgentRuntime {
{
requestId: input.context.childRunId,
conversationId: input.context.conversationId,
outputHistory: input.requestContext.outputHistory,
browserTabId: input.requestContext.browserTabId,
browserConversationId:
input.requestContext.browserConversationId,
@@ -1722,7 +1731,7 @@ export class ModelAgentRuntime implements AgentRuntime {
input.requestContext.authorize
)) {
if (event.type === 'text') {
input.onOutput(event.delta)
await input.onOutput(event.delta)
} else if (event.type === 'model-usage') {
input.onModelUsage(event)
} else if (event.type === 'tool') {
@@ -3415,7 +3424,11 @@ export class ModelAgentRuntime implements AgentRuntime {
wakeNestedEvents?.()
wakeNestedEvents = undefined
}
const outputHistory = request.outputHistory ?? (request.currentAssistantMessageId
? { conversationId: request.conversationId, messageId: request.currentAssistantMessageId }
: undefined)
const toolContext: ModelToolCallContext = {
outputHistory,
imageToolBinding: request.imageToolBinding,
conversationId: request.conversationId,
browserTabId: request.browserTabId,
@@ -3430,6 +3443,7 @@ export class ModelAgentRuntime implements AgentRuntime {
? {
subagentBridge: {
requestContext: {
outputHistory,
authorize,
browserTabId: request.browserTabId,
browserConversationId:
@@ -3879,7 +3893,11 @@ export class ModelAgentRuntime implements AgentRuntime {
state: 'completed',
summary,
input,
output: getToolResultPreview(result.parts)
// These results already bound previews and reference spilled streams.
// A second truncation would discard inline stdout/stderr from history.
output: getToolResultPreview(result.parts,
tool.name === 'process_execute' || tool.name === 'workspace_rg' ? 256 * 1024 : 16_000),
...(result.outputReferences ? { outputReferences: result.outputReferences } : {})
}
}
}