The Storage Abstraction Layer in src/core/store/types.ts defines all storage contracts that every backend must implement; upper-layer modules depend only on these interfaces (IMemoryStore), never on concrete implementations (SQLite or TCVDB), enforcing a backend-agnostic design. The Store Factory in src/core/store/factory.ts selects and constructs the active backend, embedding service, and BM25 encoder from config, bundling them into a StoreBundle that carries both runtime collaborators and a manifest snapshot of the deployment choice.
src/core/store/types.ts is the Memory Store Abstraction Layer — it defines all storage contracts (interfaces and types) that every backend implementation must satisfy, and is the single import point for upper-layer modules.[1] Upper-layer modules (hooks, tools, pipeline, record) depend only on the interfaces in src/core/store/types.ts, never on concrete store implementations — an explicit design principle the codebase calls "backend-agnostic".[1] StoreBackend in src/config.ts is a union type "sqlite" | "tcvdb" that selects the storage backend for vector and memory data.[2] Two concrete implementations satisfy IMemoryStore: SqliteMemoryStore (local SQLite + sqlite-vec + FTS5, in sqlite.ts) and TcvdbMemoryStore (Tencent Cloud VectorDB, in tcvdb.ts) — details of each live on the SQLite backend and TCVDB backend sibling pages.[1]
All IMemoryStore methods are documented as fault-tolerant: they return empty results or false on failure rather than throwing, unless explicitly documented otherwise.[1] IMemoryStore uses the MaybePromise<T> return type (T | Promise<T>) for most methods — callers must always await the result to work safely with both sync and async backends.[1] StoreCapabilities exposes four boolean flags — vectorSearch, ftsSearch, nativeHybridSearch, and sparseVectors — that callers inspect to select search strategies and degrade gracefully when a feature is absent.[1] Similarity and BM25 scores in L1SearchResult, L1FtsResult, L0SearchResult, and L0FtsResult are normalized to the range 0–1, where higher is better.[1] L1QueryFilter.updatedAfter accepts an ISO 8601 UTC timestamp and returns only records with updated_time strictly after (not equal to) that timestamp.[1]
The optional supportsDeferredEmbedding flag on IMemoryStore controls embedding write strategy: when true, auto-capture writes metadata-only via upsertL0(record, undefined) and later calls updateL0Embedding() as a background task; when false or absent, embedding is computed inline.[1] updateL0Embedding() is an optional IMemoryStore method that updates only the vector embedding for an existing L0 record — it exists specifically to support the SQLite background-embedding path.[1]
ProfileRecord.id is a stable, deterministic ID derived as profile:v1:${sha256(scope + "\0" + type + "\0" + filename)} — callers must not generate ad-hoc IDs for profile records.[1] ProfileSyncRecord extends ProfileRecord with an optional baselineVersion field that carries the optimistic-lock baseline from the last pull, used to detect concurrent modification during sync.[1] L0Record.timestamp holds the original message timestamp in epoch milliseconds, while L0SessionGroup messages carry recordedAtMs (also epoch ms) representing when the message was recorded into L0 — these two fields serve different cursor purposes.[1] IEmbeddingService exported from src/core/store/types.ts is a re-export alias of EmbeddingService from ./embedding.ts for backward compatibility — all concrete implementations (LocalEmbeddingService, OpenAIEmbeddingService, NoopEmbeddingService) implement the canonical EmbeddingService interface.[1]
src/core/store/factory.ts is the Store Factory — it selects and constructs the correct storage backend (sqlite or tcvdb), embedding service, and optional BM25 encoder from the resolved plugin config, returning them as a StoreBundle.[3] StoreBundle groups three runtime collaborators — store (IMemoryStore), embedding (IEmbeddingService), and optional bm25Encoder (BM25LocalEncoder) — plus a storeSnapshot for manifest writing.[3] "sqlite" is the default backend in createStoreBundle() — the switch statement's default branch handles both an explicit "sqlite" value and any unrecognized value.[3] The BM25 local encoder is always constructed first in createStoreBundle(), regardless of backend, and is passed into both the TCVDB store and returned in the StoreBundle.[3] The SQLite store database file is always placed at vectors.db inside the plugin dataDir, constructed as path.join(options.dataDir, "vectors.db").[3] When the sqlite backend is selected, a local embedding service is only created when config.embedding.enabled is true, config.embedding.provider is not "local", and config.embedding.apiKey is present; otherwise embeddingService is undefined.[3] When the tcvdb backend is selected, the embedding service is always NoopEmbeddingService — TCVDB performs server-side embedding, so no local embedding service is constructed.[3] createStoreBundle() throws a hard error (not a fault-tolerant return) when the tcvdb backend is selected but tcvdb.url, tcvdb.apiKey, or tcvdb.database are missing from config.[3] The storeSnapshot embedded in the returned StoreBundle differs by backend: the TCVDB snapshot records type, tcvdbUrl, tcvdbDatabase, and optionally tcvdbAlias; the SQLite snapshot records type and a relative sqlitePath.[3]
Sources