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.
19 KiB
Desktop Storage Foundation Integration
Implemented 2026-10-05. This is the storage foundation for
the desktop storage direction. The production Main
composition now opens one DesktopStorageClient and supplies its typed facades to
the migrated domains. Full application acceptance is still incomplete. Do not open
the existing Main databases alongside the owner.
Client API
Import DesktopStorageClient from src/main/desktop-storage-client.ts in Main.
Construct it after Electron readiness and the application's single-instance check.
The existing database directories must exist. Preserve the current database paths.
const storage = new DesktopStorageClient({
assistantPath,
knowledgePath,
defaultRootPath,
userDataPath,
onProgress: progress => publishExistingUpgradeProgress(progress),
onChanged: domain => publishExistingDomainNotification(domain),
onFailure: error => reportStorageFailure(error)
})
await storage.ready
await storage.call('assistant', 'saveLocalConversations', [changes])
const conversation = await storage.call(
'assistant', 'getConversation', [conversationId], { signal }
)
const results = await storage.call('knowledge', 'search', [searchOptions], { signal })
// After stopping producers and awaiting their persistence flushes:
await storage.close()
call(domain, method, argumentTuple, { signal? }) infers argument and result types
from the explicit allowlists in desktop-storage-contracts.ts. All results are
Promises. Pass cancellation as the transport option, not inside a DTO. Functions,
signals and live class instances are rejected before posting. Maps and Uint8Arrays
are supported; Node Buffers arrive as Uint8Arrays across Electron IPC.
userDataPath is the existing application user-data root, not a database directory
inferred from a filename. ready means upgrade worker exit, database initialization,
attachment startup reconciliation and runtime-binding owner initialization completed.
Calls before readiness fail without queueing. retry() retains the same public
client and facades. A live failed startup retries in that process; a failed/exited
utility is replaced only after exit confirmation and old-request settlement. The
replacement gets fresh request correlation, never the old writes. The confirmed
upgrade decision is acknowledged into Main memory before upgrade execution, retained
across startup loss, and cleared when that upgrade completes. Wire retry() to the
existing retry UI. There is no automatic restart or replay of unconfirmed writes.
The defaults locate desktop-storage-entry.js, readonly-query-worker.js and
assistant-storage-worker.js as packaged siblings. entryPath, readerWorkerPath
and upgradeWorkerPath overrides support isolated tests and explicit composition.
The new entry is included in electron.vite.config.ts.
Owner-Side Operations
| Existing call shape | Storage operation |
|---|---|
| Assistant/Knowledge ordinary database methods | call('assistant' / 'knowledge', method, args) using the explicit allowlists |
| SSH project create/update | assistant.createSshProject / assistant.updateSshProject accept Omit<SshProjectWrite, 'assertCurrent'>; Main checks the live host/runtime before dispatch, and the owner retains cancellation and project-revision checks inside the existing transaction |
activityHistory() live repository |
Existing getActivityHistoryPage, getActivityHistorySummary, updateActivityHistory, replaceActivityHistory, clearActivityHistory, reconcileActivityHistory operations |
supervisionReviewStore() |
call('review', method, args); initialization/resume execute on the write owner, not the old independent writer |
supervisionStories() |
call('stories', method, args) |
supervisionExperiences() |
call('experiences', method, args) |
supervisionSuggestions() |
call('suggestions', method, args); resolution uses assistant.resolveSupervisionSuggestion |
Knowledge externalStore |
call('external', method, args) |
readSnapshot(callback) for review context |
call('assistant', 'supervisionContext', [request]); summary/background share one reader snapshot |
Knowledge publishDocument(..., { afterChunksInserted }) |
call('knowledge', 'publishDocument', [input, chunks, { graph, embeddingReplacement, embeddingError }]) |
| Knowledge graph-only replacement | call('knowledge', 'replaceDocumentGraph', [library, document, graph]); reuses the existing evidence replacement transaction |
| Attachment references and full-output backing | call('attachments', method, args); includes owner-local reconcile(startup?, protectedIds?) |
| Temporary and persisted document results | call('documentResults', method, args) |
| Runtime bindings and native model-call ledgers | call('runtime', method, args) |
Repository instances never leave the owner. The publication graph DTO is the
existing GraphExtractionResult. The owner creates the graph callback and performs
entity identity resolution, evidence insertion, chunk/embedding publication and
pruning within KnowledgeDatabase.publishDocument's existing transaction. Replace
the consumer's publication callback with this DTO; remove its duplicate graph
publication path when migrating that consumer. Extraction/model work remains outside
storage. Publication and replacement share storeExtractedGraph from
desktop-storage-knowledge-operations.ts. Callback-bearing replaceEvidenceForDocument
is excluded from the transport allowlist. File adoption/release and task-status
orchestration remain outside this SQLite transaction.
After readiness, createDesktopStorageFiles(storage) supplies attachments, results
and output backing; createDesktopRuntimeStorageAdapters((domain, method, args) => storage.call(domain, method, args)) supplies runtime bindings and ledger access.
The facade caller types now share the core DTO types, so these calls need no casts.
StorageData preserves the existing recursive JsonValue as-is rather than
recursively rebuilding it; runtime validation still rejects callbacks and handles.
The owner supplies both file callbacks. Attachment reconciliation checks
assistant.hasAttachmentOwner. Persistent document lookup reads
knowledge.getDocumentByParsedResultId and resolves the existing
<userDataPath>/knowledge-assets/<libraryId>/<documentId>/<resultId> directory and
source location. Main must not register another persistent lookup on the results
facade. The existing onProgress, onChanged and onFailure APIs are unchanged,
including createAssistantStorageOnChanged's four notification mappings.
Keep these composites intact during integration:
- Remote task/conversation batches use the existing
appendRemote*BatchorappendRemote*Oncemethods. Await commit before forwarding or ACK. Never translate a batch into independently acknowledged per-event writes. - Review publication uses
assistant.saveSupervisionResult, which owns result facts and checkpoint commits.commitCheckpointsis deliberately not an external review operation. No callback-based transaction API is exposed. - Story/experience apply and review batch save retain their existing repository transactions. A model request must finish before calling these operations.
- Additional multi-statement snapshots need a named owner/reader handler; do not
recreate
readSnapshotas several awaited Main calls.
Bounds And Lifecycle
The utility adapter retains ReadonlyQueryReader request correlation, errors and
admission. Its existing request limit is 32, with 32 MiB of argument admission
credit. This is no longer a content-size restriction: one oversized complete input
uses the entire credit budget exclusively. Smaller concurrent inputs still share
the budget; saturation returns STORAGE_CAPACITY without dropping work.
desktop-storage-wire.ts transfers complete values on the same channel in frames
of at most 256 KiB accounted data. Each direction sends one frame and waits for its
acknowledgement before constructing the next. Strings are split without assembling
a whole JSON/serialized copy; binary frames copy only their slice and fill the
receiver's final buffer. The receiver exposes a value only after its last frame.
These frame acknowledgements provide transport backpressure, not durable write ACKs.
Cancellation bypasses the frame queue and is retained for an incompletely received
request until dispatch can reject it safely.
Known complete-note/history/result reads share one materialization/transfer slot,
so queued requests do not build many huge reply objects while another reply drains.
Writes and bounded queries retain ordinary admission. A complete user value itself
still needs memory proportional to its size; the frame budget is not a claim that
total process memory or every supported history fits in 32 MiB. There is no result
size rejection or truncation, and no STORAGE_RESULT_CAPACITY outcome after a commit.
Renderer conversation persistence now targets 8 MiB batches as well as the existing
100-conversation bound. A single oversized conversation stays complete and uses the
framed transport. Quit flushing drains all remaining batches and propagates a failed
write. Oversized single histories no longer fail solely because of the transport cap.
STORAGE_UNCONFIRMED means the process exited without confirming a pending operation;
reconcile using existing domain identities, not blind retries.
Queued cancellation is checked before dispatch. In-flight reader cancellation uses the existing thread SharedArrayBuffer inside the owner; that buffer is never sent over utility-process IPC. Synchronous commits cannot be interrupted by an incoming message, so a completed commit returns success even when cancellation was requested too late. Request capacity remains occupied until settlement.
close() stops client admission, drains admitted requests, awaits file-owner cleanup,
closes runtime ledgers/bindings and read workers, awaits the existing PASSIVE
checkpoint worker's exit, closes both database owners
and observes utility-process exit. An exit without the close acknowledgement rejects
close. Apply the application's existing shutdown deadline to the whole operation;
the transport separately reports an unconfirmed exit after its 10-second exit wait.
Startup failure follows the same release barriers. A failed close attempts the
remaining independent closes, reports STORAGE_CLOSE_FAILED, and exits unsuccessfully;
it does not acknowledge a clean drain or permit a second owner to open in that host.
Remaining Integration And Acceptance
Supervision And Heartbeat Callers
createSupervisionDomainPorts(storage) in
src/main/assistant/supervision-domain-ports.ts supplies Main-side RPC facades:
const ports = createSupervisionDomainPorts(storage)
const supervisor = createProductionSupervisorService(ports.supervision, getSettings, resolveRuntime, pool, persistUsage)
const phrase = createProductionSuggestionPhraser(ports.supervision, getSettings, resolveRuntime, pool, persistUsage)
const heartbeat = new HeartbeatService(ports.heartbeat, actions)
await deriveSuggestions(ports.suggestions, phrase, input)
The production factory's other arguments are unchanged. If provided, persistUsage
must return the persistence Promise. All heartbeat methods, including configuration
CRUD and history, now return Promises. Review repository factories may return a
Promise of the typed facade. The facades contain closures only; no database or live
repository is transported. initializeSupervisionReview and resumeSupervisionReview
route to review.initialize / review.resume, with signals in transport options.
The caller does not invoke the old independent supervision writer.
story-assignment-service.ts and experience-extraction-service.ts are the sole
story/experience model orchestrators, including their prompts, output schemas and
model-facing types. Production, tests and live scripts import those services.
The repository files retain their SQL methods and transactions, with type-only
imports for validated model output. supervision-suggester.ts awaits candidate
selection and suggestion publication. The heartbeat live script also awaits plan
creation before using its ID.
Callers await leaf/navigation writes, final publication, task usage/status, heartbeat projections, story/experience results and error bookkeeping. Cancellation aborts model work immediately, but review admission remains occupied until the cancel write and active execution settle. A late rejected cancel does not overturn a confirmed publication. Failure to persist a suggestion failure is propagated; a model phrasing failure whose status was saved preserves the completed review.
Validation includes unchanged behavioral assertions in the existing review/story/
experience suites, delayed-commit tests, and supervision-storage.test.ts: a real
Electron storage utility process runs heartbeat, two review leaves, navigation,
stories, experiences, suggestion publication, usage, a no-change heartbeat and
cancellation using a local deterministic Runtime. This does not validate final
Main/IPC composition or concurrent latency budgets.
On 2026-10-05, after removing the duplicate orchestrators, the targeted supervision/ heartbeat run passed 215 tests across 21 files, including the real utility-process test. Scoped ESLint and Node type checking passed.
The bounded scripts/supervision-storage-live.cjs <runtime-settings.json> probe
used the current supervisor profile (gpt-6-astra, OpenAI Responses) and production
utility storage with three synthetic calibration messages. Exactly four actual
HTTP requests completed, all HTTP 200: review, story assignment, experience
extraction and suggestion phrasing. It committed one leaf covering all three
sources (589 characters), assigned three events to one story, created one experience
and published two suggestions. The unchanged review made zero additional requests.
Persisted usage was four calls, 3,495 input and 1,251 output tokens. Two initial
preflight attempts sent zero requests; the probe was corrected to install the
isolated Electron encryption state before readiness. Current settings remained
byte-identical; credentials were not logged, and isolated databases, encryption
state and bundles were removed. This is functional evidence, not concurrent
performance acceptance.
Integrated Owner
The foundation registers assistant, knowledge, external metadata, supervision,
attachments, document results and runtime operations without Main SQL fallback. The
production composition in src/main/index.ts constructs the Assistant, Knowledge,
file and Runtime facades after storage readiness and passes them to the relevant
Main services. Remote Agent database ownership is unchanged.
The remaining work is product-level acceptance: complete C01-C07/A01-A10 coverage, packaged cross-platform checks, and removal or verification of any caller that is outside the migrated desktop domains. Focused domain tests do not establish those results.
The S05 runtime-domain owner, Main adapters, required composition wiring and focused validation are documented in Runtime Storage Integration.
Supervision now uses ReadonlyQueryReader.reviewManifest(runId). The existing
read-only assistant worker scans once under a read snapshot into connection-local
temporary metadata, ends the snapshot, and returns at most 200 metadata rows per
page. It holds at most one live scan manifest, rejects a competing scan explicitly,
and releases that temporary data in the operation's settlement path. It never
opens a business-write connection.
The sole write owner clears/rebuilds disposable manifest rows in bounded batches,
commits each insertion batch atomically, and yields between commits. Cancellation
and persisted pause/cancel status are checked between batches. initializing
remains set after interruption; clearing it and deleted-source cleanup share the
final transaction. Resume rebuilds partial manifests, or validates existing frozen
revisions in read-worker pages of at most 200 rows, yielding between pages. A failed
scan never falls back to synchronous owner scanning. Final result/checkpoint
publication remains the existing atomic operation.
The old writable supervision worker mode, connection opener and separate reader
field were removed. AssistantDatabase's direct initialization/resume methods use
the same bounded implementation; in-memory owner-local use retains its synchronous
algorithm because a separate read connection cannot share that database. Other
non-worker operations still execute synchronously inside storage; no latency
improvement or full concurrent performance acceptance is claimed.
The current focused validation passes 70 tests across eleven suites: core storage, file/runtime domains, assistant facade, read workers, bounded review-store algorithms, existing supervision worker regressions, startup, wire backpressure, renderer batching and real supervision/storage orchestration. Real Electron coverage includes graph replacement rollback, SSH revision conflicts, owner-side persistent document lookup after reopen, full-output retention and crash reconciliation, ledger recovery, runtime startup failure/retry and pending file-save drain before cleanup. Review tests cover 200-row bounds, concurrent owner writes, frozen scan revisions, cancellation, failed-batch rollback, reader loss, marker recovery, deleted sources and final marker/cleanup atomicity.
Real Electron large-value tests save an explicitly typed 16 MiB MP4 payload, read a complete two-entry note, and round-trip all 25,600,230 characters of an 80-message history. Hashes verify complete video and message bodies. The measured accounted frame high-water marks were 248,332 bytes in Main and 252,578 bytes in storage, below 262,144 bytes; argument credit peaked at 33,554,432 bytes. Main RSS peaked at 426,049,536 bytes including fixture inputs, complete results and application memory. This is queue-bound evidence, not a whole-process memory or latency budget claim. The real process tests also kill an in-progress write transfer and confirm no replay after retry, and verify startup decision retention/clearing across process loss.
Full Node, Agent and renderer typechecking passes, as does scoped ESLint. Temporary checks and databases use
temp/goodbuddy-storage-foundation; tests remove their generated bundles/data.
The earlier foundation production-build check predates this domain integration.
Packaged cross-platform and full UI-to-storage concurrent acceptance remain outstanding.
Reader Ownership Cleanup
The obsolete readWithFallback export, database-owned readonly-reader fields and
checkpoint lifecycle methods, and the database async wrappers were removed. Heavy
reads are dispatched by DesktopStorageOwner; owner-local synchronous reads remain
available inside transactions and focused database tests. Read-worker and checkpoint
tests now create explicit ReadonlyQueryReader/Worker owners and await their close.
The three perf harnesses create explicit owner-local reader/checkpoint resources under
the repository temporary root, report executionBoundary: owner-local, and close
those resources before deleting their copies. No production or test source reference
remains to the removed compatibility APIs.
The cleanup validation adds termination barriers and failed-frame poisoning: a reader cannot reopen capacity while its prior worker termination is pending, a transport posting failure cannot feed later values into a partial decoder, and close waits for wire drain and owner settlement. The focused cleanup run passes 122 tests; Node and renderer type checks and scoped lint pass.