mnemocyte 0.1.5 → 0.2.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
@@ -16,7 +16,7 @@ logs, and duplicate-consolidation tools on top of your existing infrastructure.
16
16
  - Hybrid recall with vector similarity, lexical matching, recency, confidence,
17
17
  access count, and importance.
18
18
  - Prompt-ready context output in Markdown, plain text, or XML.
19
- - Provider timeouts, retries, and `AbortSignal` cancellation.
19
+ - Provider retries, operation timeouts, and caller `AbortSignal` cancellation.
20
20
  - Pruning, duplicate detection, audit logs, and experimental consolidation.
21
21
 
22
22
  Mnemocyte is ESM-only. Use `import`, not CommonJS `require`.
@@ -103,8 +103,8 @@ supported for consumers that prefer provider-specific imports.
103
103
 
104
104
  ## Postgres
105
105
 
106
- For persistent storage, apply the bundled migration to a Postgres database with
107
- pgvector enabled, then pass `databaseUrl`.
106
+ For persistent storage, enable pgvector in your Postgres database, apply the
107
+ bundled migrations, then pass `databaseUrl`.
108
108
 
109
109
  ```ts
110
110
  const client = createMnemocyte({
@@ -117,13 +117,46 @@ The package includes:
117
117
 
118
118
  ```txt
119
119
  migrations/0000_initial.sql
120
+ migrations/0001_add_mnemocyte_meta.sql
121
+ migrations/0000_initial.sql.template
122
+ migrations/render-initial.mjs
120
123
  ```
121
124
 
122
- The current Postgres schema uses `embedding vector(1536)`, so the Postgres
123
- backend currently requires a 1536-dimensional embedder.
125
+ For the default 1536-dimensional schema on a fresh install, apply these in
126
+ order:
124
127
 
125
- If dimensions do not match, `createMnemocyte` throws a `MnemocyteError` with
126
- code `"CONFIG"` before opening the connection pool.
128
+ ```txt
129
+ migrations/0000_initial.sql
130
+ migrations/0001_add_mnemocyte_meta.sql
131
+ ```
132
+
133
+ For an existing 0.1.x Postgres install, apply
134
+ `migrations/0001_add_mnemocyte_meta.sql` to record the current 1536-dimensional
135
+ installation metadata.
136
+
137
+ For a different embedding dimension on a fresh installation, render an explicit
138
+ initial migration from the template and apply that rendered file instead of the
139
+ default `0000_initial.sql`. The rendered initial migration includes the
140
+ matching `mnemocyte_meta` row.
141
+
142
+ ```bash
143
+ node node_modules/mnemocyte/migrations/render-initial.mjs --dimensions 768 --out migrations/0000_initial.768.sql
144
+ ```
145
+
146
+ Inside this repository, the equivalent development shortcut is:
147
+
148
+ ```bash
149
+ pnpm migration:render -- --dimensions 768 --out migrations/0000_initial.768.sql
150
+ ```
151
+
152
+ The Postgres backend supports one embedding dimension per installation. On the
153
+ first Postgres operation, Mnemocyte reads `mnemocyte_meta` and validates it
154
+ against `embedder.dimensions` before calling the embedder. A mismatch throws a
155
+ `MnemocyteError` with code `"CONFIG"`; missing v0.2.0 metadata throws code
156
+ `"MIGRATION"`.
157
+
158
+ Use the embedding dimension documented for your chosen model, render or apply a
159
+ matching schema, and keep one dimension per Mnemocyte installation.
127
160
 
128
161
  The migration creates the bundled HNSW pgvector index
129
162
  `mnemocyte_memories_embedding_hnsw_idx` for cosine search. HNSW is approximate:
@@ -141,7 +174,8 @@ add full-text or tag-specific GIN indexes yet; add and benchmark expression
141
174
  indexes such as `to_tsvector('english', content)` or tag-oriented indexes only
142
175
  when your workload needs them.
143
176
 
144
- Configurable embedding dimensions are planned for `0.2.0`.
177
+ Existing 0.1.x deployments remain on 1536 unless you plan and run your own
178
+ Postgres migration to a different pgvector dimension.
145
179
 
146
180
  ## API
147
181
 
@@ -266,6 +300,7 @@ VALIDATION
266
300
  CONFIG
267
301
  DB
268
302
  NOT_FOUND
303
+ MIGRATION
269
304
  ```
270
305
 
271
306
  ## Retrieval Tuning
@@ -293,13 +328,41 @@ const client = createMnemocyte({
293
328
 
294
329
  Planned larger milestones:
295
330
 
296
- - configurable embedding dimensions with `mnemocyte_meta`
297
331
  - `MemoryStore` abstraction
298
332
  - `drizzleStore(db)` for caller-owned Drizzle clients
299
333
  - `@mnemocyte/mcp`
300
334
 
301
335
  See [ROADMAP.md](./ROADMAP.md).
302
336
 
337
+ ## Pre-v1 Notes
338
+
339
+ Current behavior:
340
+
341
+ - `createMnemocyte()` selects either the in-memory backend or the
342
+ Postgres/pgvector backend.
343
+ - Postgres schema setup is explicit; the client does not create tables or
344
+ indexes for you.
345
+ - `findDuplicates`, audit-log workflows, and `experimental.consolidate` are
346
+ available before v1 but still subject to API refinement.
347
+
348
+ Known limitations before v1:
349
+
350
+ - The two current backends share behavior by convention rather than through a
351
+ formal `MemoryStore` adapter boundary.
352
+ - Configured provider timeouts fail the Mnemocyte operation with `"TIMEOUT"`;
353
+ actively aborting the underlying provider request on timeout remains a
354
+ pre-v1 hardening follow-up.
355
+ - Postgres dimension metadata is installation-wide. Mixed embedding dimensions
356
+ in one database are intentionally out of scope for now.
357
+
358
+ Planned v1 direction:
359
+
360
+ - Extract a `MemoryStore` boundary so storage adapters own persistence while
361
+ shared core code owns validation, embedding, scoring, observability,
362
+ resilience, and context building.
363
+ - Keep root `mnemocyte` imports provider-SDK-free.
364
+ - Keep migrations, dimensions, and production index choices explicit.
365
+
303
366
  ## Development
304
367
 
305
368
  ```bash
@@ -1,4 +1,4 @@
1
- import { c as Embedder } from "../types-QK_43oKJ.mjs";
1
+ import { c as Embedder } from "../types-DXEGcc47.mjs";
2
2
 
3
3
  //#region src/embedders/openai.d.ts
4
4
  interface OpenAIEmbedderOptions {
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { A as RetrievalConfig, C as MnemocyteOperation, D as PruneResult, E as PruneInput, M as RetrievalScoreWeights, N as RetrievalScores, O as RecallInput, P as TokenCounter, S as MnemocyteObservationPhase, T as ProviderResilienceConfig, _ as MemoryWithScore, a as ConsolidateResult, b as MnemocyteConfig, c as Embedder, d as FindDuplicatesInput, f as GlobalStats, g as MemoryType, h as Memory, i as ConsolidateInput, j as RetrievalExplanation, k as RememberInput, l as EntityStats, m as ListAuditLogInput, n as AuditEvent, o as ContextFormat, p as ImportanceLevel, r as BuildContextInput, s as DuplicatePair, t as AuditConfig, u as ExperimentalMnemocyteClient, v as MnemocyteBackend, w as ObservabilityConfig, x as MnemocyteObservation, y as MnemocyteClient } from "./types-QK_43oKJ.mjs";
1
+ import { A as RetrievalConfig, C as MnemocyteOperation, D as PruneResult, E as PruneInput, M as RetrievalScoreWeights, N as RetrievalScores, O as RecallInput, P as TokenCounter, S as MnemocyteObservationPhase, T as ProviderResilienceConfig, _ as MemoryWithScore, a as ConsolidateResult, b as MnemocyteConfig, c as Embedder, d as FindDuplicatesInput, f as GlobalStats, g as MemoryType, h as Memory, i as ConsolidateInput, j as RetrievalExplanation, k as RememberInput, l as EntityStats, m as ListAuditLogInput, n as AuditEvent, o as ContextFormat, p as ImportanceLevel, r as BuildContextInput, s as DuplicatePair, t as AuditConfig, u as ExperimentalMnemocyteClient, v as MnemocyteBackend, w as ObservabilityConfig, x as MnemocyteObservation, y as MnemocyteClient } from "./types-DXEGcc47.mjs";
2
2
 
3
3
  //#region src/client.d.ts
4
4
  /**
@@ -16,15 +16,13 @@ import { A as RetrievalConfig, C as MnemocyteOperation, D as PruneResult, E as P
16
16
  * @param config - Client configuration. {@link MnemocyteConfig.embedder} is required.
17
17
  * @returns A {@link MnemocyteClient} instance.
18
18
  * @throws {MnemocyteError} With code `"CONFIG"` if `embedder` is missing or
19
- * invalid (including when `databaseUrl` is set and `embedder.dimensions`
20
- * is not 1536 — the dimensionality pinned by the bundled migration), or
21
- * `"VALIDATION"` if `databaseUrl` is provided but empty.
19
+ * invalid, or `"VALIDATION"` if `databaseUrl` is provided but empty.
22
20
  *
23
21
  * @example Postgres-backed client
24
22
  * ```ts
25
23
  * const client = createMnemocyte({
26
24
  * databaseUrl: process.env.DATABASE_URL,
27
- * embedder, // embedder.dimensions MUST be 1536 for the Postgres backend
25
+ * embedder, // embedder.dimensions must match mnemocyte_meta
28
26
  * });
29
27
  * ```
30
28
  */
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/client.ts","../src/errors.ts"],"mappings":";;;;;AAqCA;;;;;;;;AAAyE;;;;ACzBzE;;;;AAA8B;AA6B9B;;;;;;;;iBDJgB,eAAA,CAAgB,MAAA,EAAQ,eAAA,GAAkB,eAAe;;;;;;AAAzE;;;;;;;;AAAyE;KCzB7D,kBAAA;;;AAAZ;;;;AAA8B;AA6B9B;;;;;;;;;;;;cAAa,cAAA,SAAuB,KAAA;EAWvB;EAAA,SATH,IAAA,EAAM,kBAAA;EASc;EAAA,SAPX,KAAA;EAOoD;AAAA;AAiBvE;;;cAjBa,OAAA,UAAiB,IAAA,EAAM,kBAAA,EAAoB,KAAA;AAAA;;;;AAiBiB;;;;;;iBAAzD,gBAAA,CAAiB,KAAA,YAAiB,KAAA,IAAS,cAAc"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/client.ts","../src/errors.ts"],"mappings":";;;;;AA+BA;;;;;;;;AAAyE;;;;ACnBzE;;;;AAA8B;AA6B9B;;;;;;iBDVgB,eAAA,CAAgB,MAAA,EAAQ,eAAA,GAAkB,eAAe;;;;;;AAAzE;;;;;;;;AAAyE;KCnB7D,kBAAA;;;AAAZ;;;;AAA8B;AA6B9B;;;;;;;;;;;;cAAa,cAAA,SAAuB,KAAA;EAWvB;EAAA,SATH,IAAA,EAAM,kBAAA;EASc;EAAA,SAPX,KAAA;EAOoD;AAAA;AAiBvE;;;cAjBa,OAAA,UAAiB,IAAA,EAAM,kBAAA,EAAoB,KAAA;AAAA;;;;AAiBiB;;;;;;iBAAzD,gBAAA,CAAiB,KAAA,YAAiB,KAAA,IAAS,cAAc"}
package/dist/index.mjs CHANGED
@@ -18,11 +18,12 @@ var __exportAll = (all, no_symbols) => {
18
18
  //#region src/db/schema.ts
19
19
  var schema_exports = /* @__PURE__ */ __exportAll({
20
20
  eventsTable: () => eventsTable,
21
- memoriesTable: () => memoriesTable
21
+ memoriesTable: () => memoriesTable,
22
+ metaTable: () => metaTable
22
23
  });
23
24
  const vector = customType({
24
25
  dataType(config) {
25
- return `vector(${typeof config === "number" ? config : 1536})`;
26
+ return `vector(${config?.dimensions ?? 1536})`;
26
27
  },
27
28
  toDriver(value) {
28
29
  return `[${value.map(formatVectorComponent$1).join(",")}]`;
@@ -67,6 +68,10 @@ const eventsTable = pgTable("mnemocyte_events", {
67
68
  metadata: jsonb("metadata").notNull().default({}),
68
69
  timestamp: timestamp("timestamp", { withTimezone: true }).notNull().defaultNow()
69
70
  }, (table) => [index("mnemocyte_events_entity_time_idx").on(table.entityId, table.timestamp)]);
71
+ const metaTable = pgTable("mnemocyte_meta", {
72
+ key: text("key").primaryKey(),
73
+ embeddingDimensions: integer("embedding_dimensions").notNull()
74
+ });
70
75
  //#endregion
71
76
  //#region src/db/index.ts
72
77
  function parseSslFromUrl(url) {
@@ -1393,6 +1398,9 @@ async function lexicalSearch(db, input) {
1393
1398
  LIMIT ${input.limit}
1394
1399
  `);
1395
1400
  }
1401
+ async function getInstallationMeta(db) {
1402
+ return (await db.select().from(metaTable).where(eq(metaTable.key, "installation")).limit(1))[0] ?? null;
1403
+ }
1396
1404
  //#endregion
1397
1405
  //#region src/retrieval/index.ts
1398
1406
  async function hybridRecall(input) {
@@ -1458,6 +1466,9 @@ const IMPORTANCE_LEVELS = [
1458
1466
  function importanceCeilingLevels(max) {
1459
1467
  return IMPORTANCE_LEVELS.filter((level) => IMPORTANCE_RANK[level] <= IMPORTANCE_RANK[max]);
1460
1468
  }
1469
+ function hasPostgresErrorCode(error, code) {
1470
+ return typeof error === "object" && error !== null && "code" in error && error.code === code;
1471
+ }
1461
1472
  function toPruneFilter(input) {
1462
1473
  const filter = {};
1463
1474
  if (input.entityId !== void 0) filter.entityId = input.entityId;
@@ -1472,9 +1483,31 @@ function toPruneFilter(input) {
1472
1483
  }
1473
1484
  function createPostgresClient(config, handle) {
1474
1485
  let closed = false;
1486
+ let schemaValidated = false;
1487
+ let schemaValidationPromise;
1475
1488
  function assertOpen() {
1476
1489
  if (closed) throw new MnemocyteError("Mnemocyte client is closed.", "DB");
1477
1490
  }
1491
+ async function ensureSchemaMatchesEmbedder() {
1492
+ if (schemaValidated) return;
1493
+ if (!schemaValidationPromise) schemaValidationPromise = validateSchemaMatchesEmbedder().catch((error) => {
1494
+ schemaValidationPromise = void 0;
1495
+ throw error;
1496
+ });
1497
+ await schemaValidationPromise;
1498
+ }
1499
+ async function validateSchemaMatchesEmbedder() {
1500
+ let meta;
1501
+ try {
1502
+ meta = await getInstallationMeta(handle.db);
1503
+ } catch (error) {
1504
+ if (hasPostgresErrorCode(error, "42P01")) throw new MnemocyteError("mnemocyte_meta is missing. Apply the v0.2.0 migration or render a dimension-specific initial migration before using the Postgres backend.", "MIGRATION", error);
1505
+ throw error;
1506
+ }
1507
+ if (!meta) throw new MnemocyteError("mnemocyte_meta is missing the \"installation\" row. Apply the v0.2.0 migration or render a dimension-specific initial migration before using the Postgres backend.", "MIGRATION");
1508
+ if (meta.embeddingDimensions !== config.embedder.dimensions) throw new MnemocyteError(`embedder.dimensions (${config.embedder.dimensions}) must match mnemocyte_meta.embedding_dimensions (${meta.embeddingDimensions}). Render and apply a migration for the selected embedding dimension, or configure an embedder that matches this installation.`, "CONFIG");
1509
+ schemaValidated = true;
1510
+ }
1478
1511
  async function recordAudit(entityId, description, metadata) {
1479
1512
  if (config.audit?.enabled !== true) return;
1480
1513
  try {
@@ -1491,6 +1524,7 @@ function createPostgresClient(config, handle) {
1491
1524
  return observe(config, "postgres", "remember", { entityId: input.entityId }, async () => {
1492
1525
  assertOpen();
1493
1526
  validateRememberInput(input);
1527
+ await ensureSchemaMatchesEmbedder();
1494
1528
  const embedding = await embedOne(config.embedder, input.content, {
1495
1529
  ...input.signal === void 0 ? {} : { signal: input.signal },
1496
1530
  ...config.provider === void 0 ? {} : { resilience: config.provider }
@@ -1535,6 +1569,7 @@ function createPostgresClient(config, handle) {
1535
1569
  assertOpen();
1536
1570
  if (inputs.length === 0) return [];
1537
1571
  for (const input of inputs) validateRememberInput(input);
1572
+ await ensureSchemaMatchesEmbedder();
1538
1573
  const embeddings = await embedMany(config.embedder, inputs.map((i) => i.content), {
1539
1574
  ...inputs[0]?.signal === void 0 ? {} : { signal: inputs[0].signal },
1540
1575
  ...config.provider === void 0 ? {} : { resilience: config.provider }
@@ -1578,6 +1613,7 @@ function createPostgresClient(config, handle) {
1578
1613
  const minScore = input.minScore ?? config.defaults?.minScore ?? 0;
1579
1614
  assertLimit(limit);
1580
1615
  assertMinScore(minScore);
1616
+ await ensureSchemaMatchesEmbedder();
1581
1617
  return hybridRecall({
1582
1618
  db: handle.db,
1583
1619
  embedder: config.embedder,
@@ -1593,6 +1629,7 @@ function createPostgresClient(config, handle) {
1593
1629
  async buildContext(input) {
1594
1630
  return observe(config, "postgres", "buildContext", { entityId: input.entityId }, async () => {
1595
1631
  assertOpen();
1632
+ await ensureSchemaMatchesEmbedder();
1596
1633
  return buildContext({
1597
1634
  input,
1598
1635
  recall: (contextInput) => {
@@ -1613,6 +1650,7 @@ function createPostgresClient(config, handle) {
1613
1650
  assertOpen();
1614
1651
  assertNonEmptyString(input.entityId, "entityId");
1615
1652
  assertNonEmptyString(input.memoryId, "memoryId");
1653
+ await ensureSchemaMatchesEmbedder();
1616
1654
  if (!await deleteMemory(handle.db, input.entityId, input.memoryId)) throw new MnemocyteError("Memory was not found.", "NOT_FOUND");
1617
1655
  await recordAudit(input.entityId, "memory.deleted", { memoryId: input.memoryId });
1618
1656
  });
@@ -1621,6 +1659,7 @@ function createPostgresClient(config, handle) {
1621
1659
  return observe(config, "postgres", "forgetAll", { entityId: input.entityId }, async () => {
1622
1660
  assertOpen();
1623
1661
  assertNonEmptyString(input.entityId, "entityId");
1662
+ await ensureSchemaMatchesEmbedder();
1624
1663
  const deletedCount = await deleteMemoriesForEntity(handle.db, input.entityId);
1625
1664
  await recordAudit(input.entityId, "entity.cleared", { count: deletedCount });
1626
1665
  return deletedCount;
@@ -1630,6 +1669,7 @@ function createPostgresClient(config, handle) {
1630
1669
  return observe(config, "postgres", "prune", input.entityId === void 0 ? {} : { entityId: input.entityId }, async () => {
1631
1670
  assertOpen();
1632
1671
  validatePruneInput(input);
1672
+ await ensureSchemaMatchesEmbedder();
1633
1673
  const filter = toPruneFilter(input);
1634
1674
  if (input.dryRun === true) return {
1635
1675
  matchedCount: await countPruneMatches(handle.db, filter),
@@ -1649,6 +1689,7 @@ function createPostgresClient(config, handle) {
1649
1689
  return observe(config, "postgres", "findDuplicates", { entityId: input.entityId }, async () => {
1650
1690
  assertOpen();
1651
1691
  validateFindDuplicatesInput(input);
1692
+ await ensureSchemaMatchesEmbedder();
1652
1693
  const threshold = input.threshold ?? .95;
1653
1694
  const limit = input.limit ?? 100;
1654
1695
  return (await findDuplicatePairs(handle.db, {
@@ -1670,6 +1711,7 @@ function createPostgresClient(config, handle) {
1670
1711
  return observe(config, "postgres", "listAuditLog", { entityId: input.entityId }, async () => {
1671
1712
  assertOpen();
1672
1713
  validateListAuditLogInput(input);
1714
+ await ensureSchemaMatchesEmbedder();
1673
1715
  return (await listEvents(handle.db, {
1674
1716
  entityId: input.entityId,
1675
1717
  limit: input.limit ?? 50,
@@ -1681,6 +1723,7 @@ function createPostgresClient(config, handle) {
1681
1723
  async stats(input) {
1682
1724
  return observe(config, "postgres", "stats", input?.entityId ? { entityId: input.entityId } : {}, async () => {
1683
1725
  assertOpen();
1726
+ await ensureSchemaMatchesEmbedder();
1684
1727
  const now = /* @__PURE__ */ new Date();
1685
1728
  if (input?.entityId) {
1686
1729
  const counts = await getEntityMemoryStatsCounts(handle.db, input.entityId, now);
@@ -1709,6 +1752,7 @@ function createPostgresClient(config, handle) {
1709
1752
  }, async () => {
1710
1753
  assertOpen();
1711
1754
  validateConsolidateInput(input);
1755
+ await ensureSchemaMatchesEmbedder();
1712
1756
  const survivor = await getMemoryById(handle.db, input.entityId, input.survivorId);
1713
1757
  if (!survivor) throw new MnemocyteError("Survivor memory was not found.", "NOT_FOUND");
1714
1758
  if (survivor.supersededBy !== null) throw new MnemocyteError("Survivor memory is itself superseded.", "VALIDATION");
@@ -1766,8 +1810,6 @@ function createPostgresClient(config, handle) {
1766
1810
  }
1767
1811
  //#endregion
1768
1812
  //#region src/client.ts
1769
- /** Embedding dimensionality pinned by `migrations/0000_initial.sql`. */
1770
- const POSTGRES_EMBEDDING_DIMENSIONS = 1536;
1771
1813
  /**
1772
1814
  * Create a Mnemocyte client.
1773
1815
  *
@@ -1783,15 +1825,13 @@ const POSTGRES_EMBEDDING_DIMENSIONS = 1536;
1783
1825
  * @param config - Client configuration. {@link MnemocyteConfig.embedder} is required.
1784
1826
  * @returns A {@link MnemocyteClient} instance.
1785
1827
  * @throws {MnemocyteError} With code `"CONFIG"` if `embedder` is missing or
1786
- * invalid (including when `databaseUrl` is set and `embedder.dimensions`
1787
- * is not 1536 — the dimensionality pinned by the bundled migration), or
1788
- * `"VALIDATION"` if `databaseUrl` is provided but empty.
1828
+ * invalid, or `"VALIDATION"` if `databaseUrl` is provided but empty.
1789
1829
  *
1790
1830
  * @example Postgres-backed client
1791
1831
  * ```ts
1792
1832
  * const client = createMnemocyte({
1793
1833
  * databaseUrl: process.env.DATABASE_URL,
1794
- * embedder, // embedder.dimensions MUST be 1536 for the Postgres backend
1834
+ * embedder, // embedder.dimensions must match mnemocyte_meta
1795
1835
  * });
1796
1836
  * ```
1797
1837
  */
@@ -1799,7 +1839,6 @@ function createMnemocyte(config) {
1799
1839
  assertEmbedder(config.embedder);
1800
1840
  if (config.databaseUrl) {
1801
1841
  assertNonEmptyString(config.databaseUrl, "databaseUrl");
1802
- if (config.embedder.dimensions !== POSTGRES_EMBEDDING_DIMENSIONS) throw new MnemocyteError(`embedder.dimensions must be ${POSTGRES_EMBEDDING_DIMENSIONS} for the Postgres backend (got ${config.embedder.dimensions}). The bundled migration pins the vector column to vector(${POSTGRES_EMBEDDING_DIMENSIONS}).`, "CONFIG");
1803
1842
  return createPostgresClient(config, createDatabase(config.databaseUrl));
1804
1843
  }
1805
1844
  return createInMemoryClient(config);