@melandlabs/memory-store 0.3.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,8 +7,8 @@
7
7
 
8
8
  OpenContext memory storage + search SDK with optional HTTP and MCP server
9
9
  entry points. The package is intentionally decoupled from the opencontext
10
- web app's database and env layers — every consumer wires up its own
11
- implementation via `MemoryStoreConfig`.
10
+ web app's database layer — every consumer wires up its own backend via
11
+ the `db` contract on `MemoryStoreConfig`.
12
12
 
13
13
  - **SDK** (`memory-store`) — embed in a Node.js host
14
14
  - **HTTP** (`memory-store/http`) — REST daemon
@@ -23,20 +23,35 @@ pnpm add @melandlabs/opencontext
23
23
  ## Quick start
24
24
 
25
25
  ```ts
26
- import { createMemoryStore } from "memory-store";
26
+ import { createMemoryStore } from "@melandlabs/opencontext";
27
27
 
28
28
  const store = await createMemoryStore({
29
+ // Required for the postgres backend (OPENCONTEXT_MEMORY_STORE_BACKEND=postgres).
30
+ // For the default sqlite backend, omit db and the SDK writes to
31
+ // ~/.opencontext/memory/store.db (override with MEMORY_STORE_DB_PATH).
29
32
  db: { getDb: () => drizzleDb() },
30
- env: { isTauriMode: () => false },
31
33
  unified: {
32
34
  embedQuery: async ({ query }) => myEmbed(query),
33
35
  },
34
36
  });
35
37
 
36
- await store.getRawMessageManager();
37
- const hits = await store.searchUnifiedMemory({ userId, query });
38
+ const hits = await store.search({ userId, query });
38
39
  ```
39
40
 
41
+ ## Capabilities
42
+
43
+ The store exposes the four-verb memory API plus two read/write reflection
44
+ endpoints:
45
+
46
+ | Capability | SDK method | HTTP | MCP tool |
47
+ |---|---|---|---|
48
+ | Read-only search across all sources | `store.search(input)` | `POST /v1/search` | `memory.search` |
49
+ | Read a single `RawMessage` | `manager.getMessageById(id)` | `GET /v1/raw-messages/:id?userId=…` | `memory.getRawMessage` |
50
+ | Persist a `RawMessage` | `manager.storeMessages([…])` | `POST /v1/raw-messages` | `memory.writeRawMessage` |
51
+ | Read-only LLM synthesis over evidence | `store.search({ ...input, synthesize: true })` | `POST /v1/search` (set `synthesize: true`) | `memory.search` (set `synthesize: true`) |
52
+ | **Agentic write-back** (gather → plan → vet → persist) | `store.consolidate(input)` | `POST /v1/consolidate:apply` | `memory.consolidate` |
53
+ | Health probe | — | `GET /health` | `memory.health` |
54
+
40
55
  ## Entry points
41
56
 
42
57
  | Import | What you get |
@@ -56,10 +71,9 @@ opencontext-memory-mcp
56
71
 
57
72
  | Key | Required | Description |
58
73
  | ----------------------------------------------------- | ------------------------------- | --------------------------------------------------------- |
59
- | `db.getDb()` | yes for persistence | Drizzle DB handle factory |
60
- | `db.tables.rawMessages` / `db.tables.memorySummaries` | yes for postgres backend | Drizzle table references owned by the host |
61
- | `env.isTauriMode()` | yes | selects sqlite vs postgres backend |
62
- | `env.getTauriDbPath()` / `env.getTauriDataDir()` | for Tauri | where to place the SQLite files |
74
+ | `db.getDb()` | yes for postgres backend | Drizzle DB handle factory |
75
+ | `db.tables.rawMessages` / `db.tables.memorySummaries` | when using postgres backend | Drizzle table references owned by the host |
76
+ | `dbPath` | optional | SQLite path override (also reads `MEMORY_STORE_DB_PATH`) |
63
77
  | `vector.backend` | one of `sqlite-vec` or `chroma` | vector index backend (auto-detected if omitted) |
64
78
  | `vector.sqliteVec.dbPath` | when backend = sqlite-vec | sqlite-vec DB path |
65
79
  | `vector.chroma.url` | when backend = chroma | chroma server URL |
@@ -67,8 +81,19 @@ opencontext-memory-mcp
67
81
  | `unified.searchKnowledge` | optional | RAG over uploaded documents |
68
82
  | `unified.searchInsights` | optional | semantic search over extracted insights |
69
83
  | `unified.searchRawMessagesAnn` | optional | postgres-side ANN over `raw_messages` (database fallback) |
84
+ | `unified.searchRawMessagesLexical` | optional | BM25/FTS5 fallback when lexical search is enabled |
85
+ | `unified.searchSummaries` | optional | L1/L2/L3 summary recall (used by `reflect`) |
86
+ | `unified.peerScopeCheck` | optional | host check that gates `peerFilter` narrowing |
87
+ | `unified.reranker` | optional | cross-encoder / learned ranker applied after merge |
88
+ | `graphStore` | optional | `MemoryGraphStoreWithOperationHistory`; powers `consolidate` |
89
+ | `storage` | optional | `MemoryStorageAdapter` for `deprecateRecords` writes |
70
90
  | `logger` | optional | `console`-shaped logger; defaults to `console` |
71
91
 
92
+ Backend selection is **env-var driven, not host-injected**: the default is
93
+ SQLite at `~/.opencontext/memory/store.db`; set
94
+ `OPENCONTEXT_MEMORY_STORE_BACKEND=postgres` to opt into the host's
95
+ registered Postgres factory (call `registerPostgresFactory` at startup).
96
+
72
97
  If any of `unified.embedQuery`, `unified.searchKnowledge`, `unified.searchInsights`,
73
98
  or `unified.searchRawMessagesAnn` are absent, the corresponding source in
74
99
  `searchUnifiedMemory` returns empty results with a warning. The SDK still
@@ -86,7 +111,7 @@ import {
86
111
  createMemoryStore,
87
112
  registerPostgresFactory,
88
113
  type PostgresRawMessageManagerLike,
89
- } from "memory-store";
114
+ } from "@melandlabs/memory-store";
90
115
  import { myPostgresRawMessageManager } from "./postgres-raw-message-store";
91
116
  import * as schema from "./schema";
92
117
 
@@ -105,7 +130,6 @@ const store = await createMemoryStore({
105
130
  memorySummaries: schema.memorySummaries,
106
131
  },
107
132
  },
108
- env: { isTauriMode: () => false },
109
133
  unified: {
110
134
  embedQuery: async ({ query }) => myEmbedder.embedQuery(query),
111
135
  searchInsights: mySearchInsights,
@@ -121,15 +145,11 @@ const store = await createMemoryStore({
121
145
  });
122
146
  ```
123
147
 
124
- ### 2. Tauri / SQLite-vec backend (desktop)
148
+ ### 2. SQLite-vec backend (desktop / local)
125
149
 
126
150
  ```ts
127
151
  const store = await createMemoryStore({
128
- env: {
129
- isTauriMode: () => true,
130
- getTauriDataDir: () => appDataDir(),
131
- getTauriDbPath: () => `${appDataDir()}/memory.sqlite`,
132
- },
152
+ dbPath: `${appDataDir()}/memory.sqlite`,
133
153
  vector: {
134
154
  backend: "sqlite-vec",
135
155
  sqliteVec: { dbPath: `${appDataDir()}/vectors.sqlite` },
@@ -144,7 +164,6 @@ const store = await createMemoryStore({
144
164
 
145
165
  ```ts
146
166
  const store = await createMemoryStore({
147
- env: { isTauriMode: () => false },
148
167
  vector: {
149
168
  backend: "chroma",
150
169
  chroma: {
@@ -167,7 +186,7 @@ the fallback — same semantics as the web app.
167
186
  You don't need to wire all of them; the ones you omit just emit a warning:
168
187
 
169
188
  ```ts
170
- import { createUnifiedSearch } from "memory-store/unified-search";
189
+ import { createUnifiedSearch } from "@melandlabs/memory-store/unified-search";
171
190
 
172
191
  const search = createUnifiedSearch({
173
192
  embedQuery: async ({ userId, query, authToken }) =>
@@ -209,36 +228,160 @@ const result = await search.searchUnifiedMemory({
209
228
  // e.g. { source: "memory", code: "raw_message_storage_unavailable", ... }
210
229
  ```
211
230
 
212
- ### 5. Postgres manager factory (lazy registration)
231
+ ### 5. FactType filter
232
+
233
+ Atomic facts are classified as `world` (facts about the world), `experience`
234
+ (first-person events), or `mental_model` (generalised patterns). The
235
+ `FactType` union lives in `@melandlabs/ai/memory/contracts`; the
236
+ `rawMessageToMemoryRecord` adapter (in both `indexeddb` and `memory-store`)
237
+ carries it through.
238
+
239
+ ```ts
240
+ // Write side — set factType on each RawMessage.
241
+ await manager.storeMessages([
242
+ {
243
+ messageId: "f-1",
244
+ userId: "u-42",
245
+ content: "water boils at 100°C at sea level",
246
+ platform: "tutorial",
247
+ botId: "tutorial-bot",
248
+ timestamp: Date.now(),
249
+ factType: "world",
250
+ },
251
+ {
252
+ messageId: "f-2",
253
+ userId: "u-42",
254
+ content: "I went hiking last weekend",
255
+ platform: "tutorial",
256
+ botId: "tutorial-bot",
257
+ timestamp: Date.now(),
258
+ factType: "experience",
259
+ },
260
+ ]);
261
+
262
+ // Read side — filter by factType.
263
+ const hits = await store.search({
264
+ userId: "u-42",
265
+ query: "what did I do recently?",
266
+ factTypes: ["experience"],
267
+ limit: 5,
268
+ });
269
+ ```
270
+
271
+ Schema migration: IndexedDB `DB_VERSION` 3 → 4 (additive `factType` index on
272
+ `raw_messages`), SQLite `RAW_MESSAGES_SCHEMA_VERSION` 3 → 4 (additive
273
+ `fact_type` column + partial index). Both migrations are idempotent and
274
+ tolerate v3 rows whose `factType` is undefined.
275
+
276
+ ### 6. Read-only `search({ synthesize: true })` — LLM synthesis
277
+
278
+ `store.search({ ...input, synthesize: true })` is the read-only sibling of
279
+ the write-back loop. It fans out across raw messages, summaries,
280
+ insights, and knowledge chunks, then asks the LLM to produce a single
281
+ synthesised answer. No writes.
282
+
283
+ ```ts
284
+ const out = await store.search({
285
+ userId: "u-42",
286
+ query: "what does the user like to do on weekends?",
287
+ tiers: ["summary", "raw", "insight", "knowledge"],
288
+ limit: 20,
289
+ threshold: 0.7,
290
+ synthesize: true,
291
+ });
292
+
293
+ console.log(out.answer); // LLM synthesis
294
+ console.log(out.evidence); // the bracket-cited evidence items
295
+ console.log(out.warnings); // structured warnings, never throws
296
+ ```
297
+
298
+ The LLM is wired via `unified.reasoning.complete` — the same hook used by
299
+ the search-rewrite and iterative-recall strategies. When no LLM is wired,
300
+ `reflect()` returns the gathered evidence with a
301
+ `reflect_llm_not_configured` warning instead of throwing.
302
+
303
+ ### 7. Agentic write-back `consolidate()` — gather → plan → vet → persist
304
+
305
+ `store.consolidate()` is the agentic counterpart. It runs the same
306
+ evidence pipeline as `reflect()`, then:
307
+
308
+ 1. Builds a `MemoryConsolidationPlan` from the evidence (rule-based; the
309
+ LLM is not free to invent `MemoryGraphOperation`s).
310
+ 2. **Optionally** asks the LLM (`reasoning.complete`) to approve or veto
311
+ plan entries. The LLM never adds new entries — it only marks existing
312
+ ones as `approve` or `veto` with a reason.
313
+ 3. Persists via `MemoryGraphStore.persistPlan` (when `graphStore` is wired)
314
+ and always runs `storage.deprecateRecords` for `deprecate` entries.
315
+
316
+ ```ts
317
+ import { createMemoryStore, attachMemoryGraphStore } from "@melandlabs/memory-store";
318
+
319
+ // 1. Wire the graph store (opt-in).
320
+ attachMemoryGraphStore(store, {
321
+ storage: myIndexedDbStorage,
322
+ ownerScope: { userId: "u-42" },
323
+ });
324
+
325
+ // 2. Inspect the plan first (dry-run).
326
+ const dry = await store.consolidate({
327
+ userId: "u-42",
328
+ query: "summarise the last week",
329
+ ownerScope: { userId: "u-42" },
330
+ tiers: ["raw", "summary"],
331
+ dryRun: true,
332
+ });
333
+ console.log(dry.plan); // MemoryConsolidationPlan
334
+ console.log(dry.applied); // false
335
+
336
+ // 3. Apply for real.
337
+ const result = await store.consolidate({
338
+ userId: "u-42",
339
+ query: "summarise the last week",
340
+ ownerScope: { userId: "u-42" },
341
+ tiers: ["raw", "summary"],
342
+ dryRun: false,
343
+ });
344
+ console.log(result.applied); // true
345
+ console.log(result.persistenceResult); // { applied, skipped, conflicts }
346
+ console.log(result.deprecationCounts); // [{ supersededBySummaryId, count }]
347
+ ```
348
+
349
+ Failure modes are typed:
350
+ - `reflect_apply_llm_skipped` — no LLM configured; the rule-based plan runs.
351
+ - `reflect_apply_llm_vet_failed` — LLM threw; approve-all fallback.
352
+ - `reflect_apply_graph_store_not_configured` — `deprecateRecords` still runs.
353
+ - `reflect_apply_dry_run` — `dryRun: true`; nothing was written.
354
+ - `reflect_apply_no_writes` — plan contains no actionable entries.
355
+
356
+ ### 8. Postgres manager factory (lazy registration)
213
357
 
214
358
  If the host application owns the postgres raw-message manager (e.g. an
215
359
  existing Drizzle-based repo), register it via the factory pattern. The
216
360
  memory-store package will resolve it on first use:
217
361
 
218
362
  ```ts
219
- import { registerPostgresFactory } from "memory-store/postgres-raw-message-factory";
363
+ import { registerPostgresFactory } from "@melandlabs/memory-store/postgres-raw-message-factory";
220
364
  import { myPostgresManager } from "./managers/postgres-raw-message";
221
365
 
222
366
  registerPostgresFactory(async () => myPostgresManager);
223
367
  // Later — anywhere in the codebase:
224
- import { getRawMessageManager } from "memory-store";
368
+ import { getRawMessageManager } from "@melandlabs/memory-store";
225
369
  const manager = await getRawMessageManager();
226
370
  ```
227
371
 
228
- If you don't register a factory, the package falls back to the legacy
229
- sqlite path (`env.isTauriMode()` must return true). For non-Tauri hosts,
230
- registering the factory is mandatory.
372
+ If you don't register a factory, the default SQLite backend (`dbPath` or
373
+ `~/.opencontext/memory/store.db`) is used. For server-side hosts that
374
+ want Postgres, registering the factory is mandatory.
231
375
 
232
- ### 6. HTTP daemon
376
+ ### 9. HTTP daemon
233
377
 
234
378
  ```ts
235
- import { startHttpServer } from "memory-store/http";
379
+ import { startHttpServer } from "@melandlabs/memory-store/http";
236
380
 
237
381
  const { url, port, stop } = await startHttpServer({
238
382
  port: 7421,
239
383
  host: "127.0.0.1",
240
384
  // MemoryStoreConfig passthrough:
241
- env: { isTauriMode: () => false },
242
385
  vector: { backend: "chroma", chroma: { url: process.env.CHROMA_URL! } },
243
386
  unified: { embedQuery: myEmbedder },
244
387
  });
@@ -255,12 +398,13 @@ MEMORY_HTTP_HOST=0.0.0.0 MEMORY_HTTP_PORT=7421 opencontext-memory-http
255
398
 
256
399
  Endpoints:
257
400
 
258
- | Method + path | Body |
259
- | -------------------------------------- | ------------------------------------------------------------------------ |
260
- | `GET /health` | — |
261
- | `POST /v1/search` | `{ userId, query, limit?, threshold?, sources?, botIds?, documentIds? }` |
262
- | `POST /v1/raw-messages` | `{ userId, messages: RawMessage[] }` |
263
- | `GET /v1/raw-messages/:id?userId=...` | — |
401
+ | Method + path | Body |
402
+ | -------------------------------------- | --------------------------------------------------------------------------------------------------- |
403
+ | `GET /health` | — |
404
+ | `POST /v1/search` | `{ userId, query, limit?, threshold?, sources?, botIds?, documentIds?, factTypes?, synthesize? }` |
405
+ | `POST /v1/consolidate:apply` | `{ userId, query, ownerScope, tiers?, limit?, threshold?, dryRun?, expectedVersion?, llmPlanReview? }` |
406
+ | `POST /v1/raw-messages` | `{ userId, messages: RawMessage[] }` |
407
+ | `GET /v1/raw-messages/:id?userId=...` | — |
264
408
 
265
409
  For container / LAN deployment, put it behind a reverse proxy:
266
410
 
@@ -272,7 +416,7 @@ location /memory/ {
272
416
  }
273
417
  ```
274
418
 
275
- ### 7. MCP daemon
419
+ ### 10. MCP daemon
276
420
 
277
421
  ```bash
278
422
  opencontext-memory-mcp
@@ -280,12 +424,13 @@ opencontext-memory-mcp
280
424
 
281
425
  Tools exposed over stdio:
282
426
 
283
- | Tool | Required args |
284
- | ------------------------ | ------------------------------------------------------------------------------------ |
285
- | `memory.health` | — |
286
- | `memory.searchUnified` | `userId`, `query`, optional `limit`, `threshold`, `sources`, `botIds`, `documentIds` |
287
- | `memory.writeRawMessage` | `userId`, `message: { role, content, platform?, botId?, ... }` |
288
- | `memory.getRawMessage` | `userId`, `messageId` |
427
+ | Tool | Required args |
428
+ | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
429
+ | `memory.health` | — |
430
+ | `memory.search` | `userId`, `query`, optional `limit`, `threshold`, `sources`, `botIds`, `documentIds`, `factTypes`, `synthesize`, `tiers` |
431
+ | `memory.writeRawMessage` | `userId`, `message: { role, content, platform?, botId?, factType?, peer?, ... }` |
432
+ | `memory.getRawMessage` | `userId`, `messageId` |
433
+ | `memory.consolidate` | `userId`, `query`, `ownerScope`, optional `tiers`, `dryRun`, `expectedVersion`, `llmPlanReview`, `plan` |
289
434
 
290
435
  #### Claude Desktop (`claude_desktop_config.json`)
291
436
 
@@ -333,24 +478,24 @@ Same shape — Cursor reads `mcpServers` from its MCP settings
333
478
  }
334
479
  ```
335
480
 
336
- After registering, the four `memory.*` tools become available inside the
481
+ After registering, the six `memory.*` tools become available inside the
337
482
  editor. Tool calls appear in the chat like any other MCP tool.
338
483
 
339
484
  ## Subpath exports
340
485
 
341
- | Subpath | Contents |
342
- | --------------------------------------------- | ------------------------------------------------------------------------------- |
343
- | `memory-store` | `createMemoryStore`, top-level types |
344
- | `memory-store/http` | `startHttpServer`, `StartedHttpServer` |
345
- | `memory-store/mcp` | `startMcpServer` |
346
- | `memory-store/unified-search` | `createUnifiedSearch(deps)` factory + result types |
347
- | `memory-store/raw-message-store` | `createRawMessageStore`, `getRawMessageManager`, `isRawMessageStorageAvailable` |
348
- | `memory-store/sqlite-raw-message-store` | SQLite-vec raw-message manager (Tauri / desktop) |
349
- | `memory-store/postgres-raw-message-factory` | `registerPostgresFactory`, `resolvePostgresFactory` |
350
- | `memory-store/sqlite-vector-index` | Direct sqlite-vec insight index helpers |
351
- | `memory-store/chroma-memory-index` | Direct chroma upsert / search helpers |
352
- | `memory-store/memory-graph-write-policy` | `resolveMemoryGraphWritePolicy`, allowlist gating |
353
- | `memory-store/memory-graph-correction-policy` | `resolveMemoryGraphCorrectionPolicy` |
486
+ | Subpath | Contents |
487
+ | --------------------------------------------- | ------------------------------------------------------------------------------------------------- |
488
+ | `memory-store` | `createMemoryStore`, `attachMemoryGraphStore`, top-level types |
489
+ | `memory-store/http` | `startHttpServer`, `StartedHttpServer` |
490
+ | `memory-store/mcp` | `startMcpServer` |
491
+ | `memory-store/unified-search` | `createUnifiedSearch(deps)` factory + result types |
492
+ | `memory-store/raw-message-store` | `createRawMessageStore`, `getRawMessageManager`, `isRawMessageStorageAvailable` |
493
+ | `memory-store/sqlite-raw-message-store` | SQLite-vec raw-message manager (Tauri / desktop) |
494
+ | `memory-store/postgres-raw-message-factory` | `registerPostgresFactory`, `resolvePostgresFactory` |
495
+ | `memory-store/sqlite-vector-index` | Direct sqlite-vec insight index helpers |
496
+ | `memory-store/chroma-memory-index` | Direct chroma upsert / search helpers |
497
+ | `memory-store/memory-graph-write-policy` | `resolveMemoryGraphWritePolicy`, allowlist gating |
498
+ | `memory-store/memory-graph-correction-policy` | `resolveMemoryGraphCorrectionPolicy` |
354
499
 
355
500
  ## Behaviour notes
356
501
 
@@ -368,9 +513,17 @@ editor. Tool calls appear in the chat like any other MCP tool.
368
513
  silent inflation. `threshold` is clamped to `[-1, 1]`.
369
514
  - Chroma failures fall back to `unified.searchRawMessagesAnn` when both
370
515
  are configured; otherwise the memory source returns empty.
516
+ - `reflect()` and `consolidate()` reuse the same evidence pipeline;
517
+ `reflect()` is read-only, `consolidate()` additionally persists
518
+ via the graph store + storage adapter.
519
+ - `applyReflectedConsolidationPlan` is idempotent — `persistPlan`
520
+ deduplicates on `operationId` and `deprecateRecords` no-ops on
521
+ already-deprecated rows.
371
522
 
372
523
  ## See also
373
524
 
374
525
  - [`docs/architecture.md`](../../docs/architecture.md) — the overall data
375
526
  model, lifecycle, and transport surfaces that the memory primitives
376
527
  plug into.
528
+ - [`docs/tutorials/03-advanced-usage.md`](../../docs/tutorials/03-advanced-usage.md#reflection-and-write-back)
529
+ — end-to-end walkthrough of `reflect()` + `consolidate()`.