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
+54 -52
View File
@@ -3,6 +3,7 @@ import type { DesktopDiagnosticFailureObserver, DesktopMcpDiagnosticMetadata } f
import { isStoryGraphTool, storyGraphToolNames, type StoryGraphToolName } from '../../shared/story-graph-tools'
import type { RuntimeTarget } from '../../shared/capability-contracts'
import { builtinModelTools } from '../../shared/builtin-model-tools'
import type { Awaitable } from '../assistant-storage-port'
export type StoryGraphBinding = { projectId?: string; conversationId?: string; runtimeTarget: RuntimeTarget }
export type StoryGraphRemoteBinding = {
@@ -143,35 +144,35 @@ const {
} = magicNoteScopedDataToolCatalog
export type MagicNotesDatabase = {
listMagicNotes(input?: { tags?: string[] }): MagicNoteSummary[]
getMagicNote(noteId: string): MagicNoteDetail
getMagicNoteEntry(entryId: string): MagicNoteEntry
searchMagicNotes(query: string, limit: number): MagicNoteSearchResult[]
listMagicNotes(input?: { tags?: string[] }): Awaitable<MagicNoteSummary[]>
getMagicNote(noteId: string): Awaitable<MagicNoteDetail>
getMagicNoteEntry(entryId: string): Awaitable<MagicNoteEntry>
searchMagicNotes(query: string, limit: number): Awaitable<MagicNoteSearchResult[]>
createMagicNote(input: {
title: string
content?: MagicNoteContent
tags?: string[]
}): MagicNoteDetail
}): Awaitable<MagicNoteDetail>
updateMagicNote(input: {
noteId: string
title?: string
pinned?: boolean
tags?: string[]
expectedRevision: number
}): MagicNoteDetail
deleteMagicNote(noteId: string): void
}): Awaitable<MagicNoteDetail>
deleteMagicNote(noteId: string): Awaitable<void>
createMagicNoteEntry(input: {
noteId: string
content: MagicNoteContent
plainText: string
}): MagicNoteDetail
}): Awaitable<MagicNoteDetail>
updateMagicNoteEntry(input: {
entryId: string
content: MagicNoteContent
plainText: string
expectedRevision: number
}): MagicNoteDetail
deleteMagicNoteEntry(entryId: string): MagicNoteDetail
}): Awaitable<MagicNoteDetail>
deleteMagicNoteEntry(entryId: string): Awaitable<MagicNoteDetail>
}
export type MagicNotesCapabilityAccess = 'none' | 'read' | 'write'
@@ -757,7 +758,7 @@ export class KnowledgeMcpGateway {
)
const effectiveSignal = AbortSignal.any([capability.signal, capability.brokerController.signal, ...(signal ? [signal] : [])])
effectiveSignal.throwIfAborted()
const libraries = this.knowledgeService.database.listKnowledgeBases(500)
const libraries = await this.knowledgeService.database.listKnowledgeBases(500)
const libraryNames = new Map(
libraries.map((library) => [library.id, library.name])
)
@@ -801,15 +802,15 @@ export class KnowledgeMcpGateway {
return references
}
listLibraries(
async listLibraries(
token: string,
input: unknown = {}
): KnowledgeLibraryListItem[] {
): Promise<KnowledgeLibraryListItem[]> {
const capability = this.getCapability(token)
knowledgeListTool.inputSchema.parse(input)
const librariesById = new Map(
this.knowledgeService.database
.listKnowledgeBases(500)
(await this.knowledgeService.database
.listKnowledgeBases(500))
.map((library) => [library.id, library])
)
const libraries: KnowledgeLibraryListItem[] = []
@@ -1431,15 +1432,15 @@ export class KnowledgeMcpGateway {
return { capability, database: this.magicNotesDatabase }
}
listMagicNotes(
async listMagicNotes(
token: string,
input: unknown = {}
): MagicNoteToolSummary[] {
): Promise<MagicNoteToolSummary[]> {
const { database } = this.requireMagicNotes(token, magicNoteListTool.access)
const { limit, tags } = magicNoteListTool.inputSchema.parse(input)
const filter = tags ? magicNoteTagListSchema.parse(tags) : undefined
const notes: MagicNoteToolSummary[] = []
const listed = database.listMagicNotes(filter?.length ? { tags: filter } : {})
const listed = await database.listMagicNotes(filter?.length ? { tags: filter } : {})
for (const note of listed.slice(0, limit)) {
const item = toMagicNoteToolSummary(note)
if (
@@ -1453,10 +1454,10 @@ export class KnowledgeMcpGateway {
return notes
}
getMagicNote(token: string, input: unknown): MagicNoteToolDetail {
async getMagicNote(token: string, input: unknown): Promise<MagicNoteToolDetail> {
const { database } = this.requireMagicNotes(token, magicNoteGetTool.access)
const { noteId } = magicNoteGetTool.inputSchema.parse(input)
const detail = database.getMagicNote(noteId)
const detail = await database.getMagicNote(noteId)
const result: MagicNoteToolDetail = {
...toMagicNoteToolSummary(detail),
entries: [],
@@ -1491,18 +1492,19 @@ export class KnowledgeMcpGateway {
return result
}
searchMagicNotes(
async searchMagicNotes(
token: string,
input: unknown,
signal?: AbortSignal
): MagicNoteSearchResult[] {
): Promise<MagicNoteSearchResult[]> {
const { capability, database } = this.requireMagicNotes(token, magicNoteSearchTool.access)
const { query, limit } = magicNoteSearchTool.inputSchema.parse(input)
const effectiveSignal = signal
? AbortSignal.any([signal, capability.signal])
: capability.signal
effectiveSignal.throwIfAborted()
const notes = database.searchMagicNotes(query, limit)
const notes = await database.searchMagicNotes(query, limit)
effectiveSignal.throwIfAborted()
const bounded: MagicNoteSearchResult[] = []
for (const note of notes) {
const candidate = [...bounded, note]
@@ -1517,7 +1519,7 @@ export class KnowledgeMcpGateway {
return bounded
}
createMagicNote(token: string, input: unknown): MagicNoteToolDetail {
async createMagicNote(token: string, input: unknown): Promise<MagicNoteToolDetail> {
const { database } = this.requireMagicNotes(token, magicNoteCreateTool.access)
const parsed = magicNoteCreateTool.inputSchema.parse(input)
const content =
@@ -1527,33 +1529,33 @@ export class KnowledgeMcpGateway {
return this.getMagicNote(
token,
{
noteId: database.createMagicNote({
noteId: (await database.createMagicNote({
title: parsed.title,
...(content ? { content } : {}),
...(parsed.tags ? { tags: magicNoteTagListSchema.parse(parsed.tags) } : {})
}).id
})).id
}
)
}
updateMagicNote(token: string, input: unknown): MagicNoteToolDetail {
async updateMagicNote(token: string, input: unknown): Promise<MagicNoteToolDetail> {
const { database } = this.requireMagicNotes(token, magicNoteUpdateTool.access)
const parsed = magicNoteUpdateTool.inputSchema.parse(input)
database.updateMagicNote({
await database.updateMagicNote({
...parsed,
...(parsed.tags ? { tags: magicNoteTagListSchema.parse(parsed.tags) } : {})
})
return this.getMagicNote(token, { noteId: parsed.noteId })
}
createMagicNoteEntry(
async createMagicNoteEntry(
token: string,
input: unknown
): MagicNoteToolDetail {
): Promise<MagicNoteToolDetail> {
const { database } = this.requireMagicNotes(token, magicNoteEntryCreateTool.access)
const parsed = magicNoteEntryCreateTool.inputSchema.parse(input)
const content = textContent(parsed.content)
database.createMagicNoteEntry({
await database.createMagicNoteEntry({
noteId: parsed.noteId,
content,
plainText: magicNotePlainText(content)
@@ -1561,17 +1563,17 @@ export class KnowledgeMcpGateway {
return this.getMagicNote(token, { noteId: parsed.noteId })
}
updateMagicNoteEntry(
async updateMagicNoteEntry(
token: string,
input: unknown
): MagicNoteToolDetail {
): Promise<MagicNoteToolDetail> {
const { database } = this.requireMagicNotes(token, magicNoteEntryUpdateTool.access)
const parsed = magicNoteEntryUpdateTool.inputSchema.parse(input)
if (database.getMagicNoteEntry(parsed.entryId).content.version === 2) {
if ((await database.getMagicNoteEntry(parsed.entryId)).content.version === 2) {
throw new Error('画布记录不能通过纯文本工具覆盖,请在画布编辑器中修改,或追加新的纯文本记录')
}
const content = textContent(parsed.content)
const detail = database.updateMagicNoteEntry({
const detail = await database.updateMagicNoteEntry({
entryId: parsed.entryId,
content,
plainText: magicNotePlainText(content),
@@ -1580,31 +1582,31 @@ export class KnowledgeMcpGateway {
return this.getMagicNote(token, { noteId: detail.id })
}
deleteMagicNoteEntry(
async deleteMagicNoteEntry(
token: string,
input: unknown
): MagicNoteToolDetail {
): Promise<MagicNoteToolDetail> {
const { database } = this.requireMagicNotes(token, magicNoteEntryDeleteTool.access)
const parsed = magicNoteEntryDeleteTool.inputSchema.parse(input)
const entry = database.getMagicNoteEntry(parsed.entryId)
const entry = await database.getMagicNoteEntry(parsed.entryId)
if (entry.revision !== parsed.expectedRevision) {
throw new Error('记录已被更新,请重新读取后重试')
}
const detail = database.deleteMagicNoteEntry(parsed.entryId)
const detail = await database.deleteMagicNoteEntry(parsed.entryId)
return this.getMagicNote(token, { noteId: detail.id })
}
deleteMagicNote(
async deleteMagicNote(
token: string,
input: unknown
): { deleted: true; noteId: string } {
): Promise<{ deleted: true; noteId: string }> {
const { database } = this.requireMagicNotes(token, magicNoteDeleteTool.access)
const parsed = magicNoteDeleteTool.inputSchema.parse(input)
const note = database.getMagicNote(parsed.noteId)
const note = await database.getMagicNote(parsed.noteId)
if (note.revision !== parsed.expectedRevision) {
throw new Error('笔记已被更新,请重新读取后重试')
}
database.deleteMagicNote(parsed.noteId)
await database.deleteMagicNote(parsed.noteId)
return { deleted: true, noteId: parsed.noteId }
}
@@ -1643,25 +1645,25 @@ export class KnowledgeMcpGateway {
case 'story_graph_read_source':
return this.callStoryGraphTool(token, name, input, signal)
case 'knowledge_list':
return { libraries: this.listLibraries(token, input) }
return { libraries: await this.listLibraries(token, input) }
case 'knowledge_search':
return { references: await this.search(token, input, signal) }
case 'note_list':
return { notes: this.listMagicNotes(token, input) }
return { notes: await this.listMagicNotes(token, input) }
case 'note_get':
return { note: this.getMagicNote(token, input) }
return { note: await this.getMagicNote(token, input) }
case 'note_search':
return { notes: this.searchMagicNotes(token, input) }
return { notes: await this.searchMagicNotes(token, input, signal) }
case 'note_create':
return { note: this.createMagicNote(token, input) }
return { note: await this.createMagicNote(token, input) }
case 'note_update':
return { note: this.updateMagicNote(token, input) }
return { note: await this.updateMagicNote(token, input) }
case 'note_entry_create':
return { note: this.createMagicNoteEntry(token, input) }
return { note: await this.createMagicNoteEntry(token, input) }
case 'note_entry_update':
return { note: this.updateMagicNoteEntry(token, input) }
return { note: await this.updateMagicNoteEntry(token, input) }
case 'note_entry_delete':
return { note: this.deleteMagicNoteEntry(token, input) }
return { note: await this.deleteMagicNoteEntry(token, input) }
case 'note_delete':
return this.deleteMagicNote(token, input)
case 'goodbuddy_config_capabilities':