OMMS runs one memory engine behind three host adapters: the OpenCode plugin, the Pi coding-agent extension, and the Claude Code hooks. This page sets out:
- the boundary between the shared code and the adapters
- the import rules
- the compatibility promise for existing data
src/core + src/services + src/importer
(shared memory core and engine)
▲ ▲ ▲
│ │ │
src/adapters/opencode src/adapters/pi src/adapters/claude-code
+ src/index.ts (Pi extension) (hook client, reached
+ src/v2 (compat) through src/cli)
Rules:
src/core/*andsrc/services/*must not import host SDKs (@opencode-ai/*,@earendil-works/*) or anything fromsrc/adapters/*.tests/host-neutral-capture-boundary.test.tsandtests/pi-adapter-boundary.test.tsenforce this.src/core/*,src/services/*, andsrc/types/*must not importsrc/importer/*. The one exception issrc/services/web-server.ts. It reaches the importer only through dynamic imports ofsrc/importer/web-import-api.ts,src/importer/settings-health.ts,src/importer/web-import-jobs.ts,src/importer/settings-models.ts, andsrc/importer/claude-hook-api.ts.tests/pi-adapter-boundary.test.tsenforces this.- The OpenCode entry points (
src/index.ts,src/v2/adapter.ts,src/v2/plugin.ts) must not importimporter/or@earendil-worksdirectly. They load importer code throughsrc/adapters/opencode/*. - Adapters own everything that is specific to a host: lifecycle events, session reading, host UI, host model access, and tool registration.
- An adapter must not import another host's host-coupled modules.
src/adapters/claude-code/*must not import the OpenCode or Pi adapters, or a host SDK.src/core/*,src/services/*,src/importer/*,src/types/*, and the OpenCode and Pi hosts must not import it. Onlysrc/cli/index.tsloads it, with dynamicimport().tests/claude-code-adapter-boundary.test.tsenforces this.- The Claude Code hook client loads no store and no embedding model. It sends each hook event to the web app, which does the memory work.
- Load host SDKs and heavy modules with dynamic
import().tests/plugin-bundle-boundary.test.tschecks the plugin bundle. It also checks that the OpenCode and Pi bundles do not includeadapters/claude-code.
src/importer/ is shared by all hosts and never imports an adapter.
tests/pi-adapter-boundary.test.ts checks this.
- The readers for each host's history format live in the importer:
opencode-reader.tsfor OpenCode's database,pi-conversation.tswithsession-loader.tsfor Pi session files, andclaude-conversation.tswithclaude-reader.tsfor Claude Code transcripts. - The Pi adapter imports
pi-conversation.tsfor live capture. Adapters may depend on shared code; shared code never depends on an adapter. pi-sdk.tsloads the Pi SDK with dynamicimport(). When OMMS's own dependencies lack it, it uses the copy in Pi's install.session-loader.ts,import-readiness.ts, andsettings-models.tsget the SDK through it, only when they read Pi history or Pi's model list.- Only these importer files name a host SDK. The boundary test checks every file.
- OpenCode's model access for web imports, Health, and Settings reaches the
importer through
registerOpencodeHostModelsinbackfill-controls.ts. The OpenCode adapter registers it at plugin start. With nothing registered, as in the standalone web app, OpenCode models report as unavailable. src/core/internal-prompt.tsrecognises omms's own summary and profile prompts, which are the same text on every host.
The shared capture pipeline depends on two interfaces only:
CaptureSummaryProvider.summarize(request)does structured extraction.- The OpenCode provider path (
src/adapters/opencode/opencode-provider.ts) and the Pi model bridge (src/adapters/pi/provider.ts) both implement it. - Throw to defer or skip the work unit. Do not return partial data. Manual memory operations stay available.
- The OpenCode provider path (
AutoCaptureHostextendsCaptureSummaryProvider. It adds session conversation access (getConversation), readiness (isCaptureReady), and optional notifications (notify).- The OpenCode adapter implements it.
- The Pi adapter calls
captureConversationdirectly fromagent_settled. - For Claude Code,
src/importer/claude-hook-api.tsin the web app callscaptureConversationwith the external API provider fromselectImportModel.
ModelPort in src/core/profile-analysis.ts is the profile model port.
It has complete for plain text and an optional completeStructured for
host-enforced JSON. Profile dedup, conflict, description, and cleanup calls in
src/services/user-profile/ use the model a host registers with
registerHostProfileModel (profile-model.ts). With none, they use the
external API. OpenCode registers adaptOpencodeProfileModel. Pi registers
nothing, so its behaviour does not change. Claude Code registers nothing. Its
profile learning calls the external API profile model directly, because a
registration applies to the whole process. When the web app runs inside
OpenCode, a Claude Code registration would replace OpenCode's model.
CaptureWorkUnit in src/core/capture.ts is the only shape the shared
pipeline accepts. It holds visible conversation content and provenance.
Hidden reasoning and host SDK objects never go into it.
| Layer | Owns |
|---|---|
src/core |
Ports, capture pipeline, context budgeting, shared extraction schema and parsing, retrieval, memory tool operations |
src/services |
Storage (Turso/libSQL), embeddings, vector search, privacy, deduplication, project identity, profiles, portability, cleanup, web backend, live-model rule (ai/live-model-choice.ts) |
src/importer |
History import and automatic backfill, import ledger, import runs and progress, backfill controls, path maps, the web import API |
src/adapters/opencode + src/index.ts + src/v2 |
OpenCode lifecycle, session reading, provider bridge, OpenCode model code and profile learning, OpenCode backfill model resolver, V2 compatibility |
src/adapters/pi |
Pi lifecycle (session_start, before_agent_start, agent_settled, session_shutdown), retrieval injection, model bridge, Pi backfill model resolver, memory tool registration |
src/adapters/claude-code |
The claude-hook <event> command: hook input, finding or starting the web app, one request, added context output, one log line. No store and no model |
| Module | Purpose |
|---|---|
src/importer/import-runs.ts |
Progress for each host's import run, and one run at a time for each host across every surface (auto, web, cli, slash) |
src/importer/backfill-controls.ts |
Run now, Pause, and Resume. Each host registers its backfill model resolver with registerHostBackfillModels |
src/importer/external-backfill-models.ts |
Backfill models from the external API settings (memoryModel, memoryApiUrl, memoryApiKey) |
src/importer/external-api-test.ts |
A short test call to the external API |
src/importer/import-path-maps.ts |
Checks and normalises importPathMaps from the global config |
src/importer/map-suggestions.ts |
Suggests path maps by looking for project markers such as .git and package.json |
src/importer/settings-health.ts |
Settings health checks for the web UI |
src/importer/web-import-api.ts, web-import-jobs.ts |
The importer functions and jobs that the web server calls |
src/services/private-path.ts |
Limits a file or folder to the current user. Capture traces and memory-key-source.ts use it |
src/services/memory-key-source.ts |
How the web UI External API card supplies memoryApiKey (env, file, or a pasted value) |
src/services/global-version.ts |
The version of the global om-memory-system command, if installed |
src/services/package-version.ts |
This package's version, read from its package.json |
| Module | Purpose |
|---|---|
src/importer/claude-conversation.ts |
Pure transcript parsing and window building, for live capture and history import |
src/importer/claude-reader.ts |
Transcript discovery under ~/.claude/projects or --root, as a LazyImportSource |
src/importer/claude-import.ts |
importClaudeHistory: the shared importer, ledger, and profile steps with host claude-code |
src/importer/claude-hook-api.ts |
Web app handlers for POST /api/claude/retrieve and POST /api/claude/capture, capture worker, retry drain |
src/services/claude-capture-cursor.ts |
The last captured user entry for each Claude Code session, in user-prompts.db |
src/cli/memory-command.ts |
om-memory-system memory <mode>: the shared memory operations from a terminal, with host claude-code |
src/types/host-label.ts |
hostLabel(): the display name of each host |
resolveClaudeCodeLiveModel in src/services/ai/live-model-choice.ts is the
Claude Code model rule: the external API only. See
ADR-013.
OpenCode, Pi, the web app, and the memory command can run in separate
processes against the same store:
- Every connection sets
busy_timeout=5000and uses one pooled handle. - Every write goes through
withScopeWriteLock. It nests a cross-process lock for each scope (src/services/turso/cross-process-write-lock.ts). That lock publishes a complete PID payload through a hard link before entering the write section. It checks if the owner PID is alive and takes over stale locks. See TDR-039. - So shard allocation, vector-count sync, insert, and increment run as one critical section for each scope, across processes.
- Races to create a shard end at the
UNIQUE(scope, scope_hash, shard_index)constraint. The loser reads the row again and uses it. tests/two-process-storage.test.tschecks this with two spawned processes on the production write path. It covers concurrent start, mixed same-project writes, read-after-write, close and reopen, exact counts, and rollover.
The default rollback journal is on purpose. File-level migration copies need the main database file to be current after every commit.
Phase 1 kept these, and later phases must keep them:
- The default store is
~/.omms/data.- A one-time verified migration moves it from
~/.opencode-mem/data(see omms-migration.md). - The legacy folder is backed up and copied, never changed.
- Storage uses the legacy layout until the migration succeeds.
- The container tag prefix changed from
opencode_project_<hash>toomms_project_<hash>. A one-time verified rewrite of stored rows did this, with a backup first. - The same project folder resolves to the same shard set from any host.
- A one-time verified migration moves it from
- Memories written before provenance fields existed stay valid and
searchable. Provenance (
host,hostSessionId,sourceType,sourceEntryIds,sourceTimestamp,sourceFile,importId) is optional metadata. It never controls retrieval. - An upgrade never needs new embeddings. Schema changes must only add, and must be safe to run again on open.
- The OpenCode V1 entry point (
src/index.ts) keeps its behaviour.- The V2 entry (
src/v2/adapter.ts) uses native V2 session hooks. - It retrieves for each prompt through the shared
src/core/retrieval.ts, the same code Pi uses. - It restores memories after compaction through the
compactionhook. - The
memorytool, idle capture, and profile learning still go through the shared operations layer.
- The V2 entry (
- Implement
CaptureSummaryProviderfor the host's model runtime. - Convert the host's transcript into
CaptureConversation. Include visible text and bounded tool inputs only. - Call
captureConversationwith aCaptureWorkUnitthat carries provenance. - Send the host's
memorytool calls throughexecuteMemoryOperation. - Register a backfill model resolver with
registerHostBackfillModels. - Add the same features as the other hosts, so all hosts stay equal.
- Add boundary tests. Keep host SDKs out of the shared path. Use type-only host imports in the adapter.