mnemocyte 0.3.0 → 0.5.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
@@ -12,6 +12,8 @@ logs, and duplicate-consolidation tools on top of your existing infrastructure.
12
12
 
13
13
  - In-memory backend for tests and demos.
14
14
  - Postgres + pgvector backend for persistent storage.
15
+ - Caller-owned postgres.js Drizzle instances through
16
+ `mnemocyte/stores/drizzle`.
15
17
  - User-supplied `Embedder` interface.
16
18
  - Hybrid recall with vector similarity, lexical matching, recency, confidence,
17
19
  access count, and importance.
@@ -65,14 +67,17 @@ const context = await client.buildContext({
65
67
  await client.close();
66
68
  ```
67
69
 
68
- When `databaseUrl` is omitted, Mnemocyte uses the in-memory backend.
70
+ When both `databaseUrl` and `store` are omitted, Mnemocyte uses the in-memory
71
+ backend. That backend is intended for development, tests, and prototyping. For
72
+ persistent or larger workloads, use Postgres.
69
73
 
70
74
  ## Embedder
71
75
 
72
76
  Mnemocyte accepts any embedder that returns one vector per input text.
73
- Each vector must match `embedder.dimensions` and contain only finite numbers;
74
- invalid provider output fails with `MnemocyteError` code `"EMBEDDING"` before
75
- anything is stored.
77
+ Each vector must match `embedder.dimensions`, contain only finite numbers, and
78
+ have at least one nonzero component. Invalid provider output, including an
79
+ exact zero-norm vector, fails with `MnemocyteError` code `"EMBEDDING"` before
80
+ storage or vector comparison. Tiny nonzero vectors remain valid.
76
81
 
77
82
  ```ts
78
83
  const client = createMnemocyte({
@@ -102,7 +107,11 @@ const embedder = openaiEmbedder({
102
107
  explicitly when you use a different key source. The helper uses `fetch`
103
108
  directly and does not depend on the OpenAI SDK; plain `import "mnemocyte"` also
104
109
  stays provider-free. The direct subpath `mnemocyte/embedders/openai` is also
105
- supported for consumers that prefer provider-specific imports.
110
+ supported for consumers that prefer provider-specific imports. Provider
111
+ responses must contain exactly one uniquely indexed array embedding per input;
112
+ malformed response shapes fail with `MnemocyteError` code `"EMBEDDING"`.
113
+ Dimension and numeric-component validation remains at the shared embedder
114
+ boundary.
106
115
 
107
116
  ## Postgres
108
117
 
@@ -116,11 +125,52 @@ const client = createMnemocyte({
116
125
  });
117
126
  ```
118
127
 
128
+ Applications that already own a postgres.js Drizzle instance can pass it
129
+ through the `mnemocyte/stores/drizzle` subpath:
130
+
131
+ ```ts
132
+ import { createMnemocyte } from "mnemocyte";
133
+ import { drizzleStore } from "mnemocyte/stores/drizzle";
134
+ import { drizzle } from "drizzle-orm/postgres-js";
135
+ import postgres from "postgres";
136
+ import * as applicationSchema from "./db/schema.js";
137
+
138
+ const sql = postgres(process.env.DATABASE_URL!);
139
+ const db = drizzle(sql, { schema: applicationSchema });
140
+
141
+ const client = createMnemocyte({
142
+ store: drizzleStore(db),
143
+ embedder,
144
+ });
145
+
146
+ try {
147
+ await client.remember({
148
+ entityId: "user_123",
149
+ content: "Uses the application's existing Drizzle connection.",
150
+ });
151
+ } finally {
152
+ await client.close(); // does not close db or sql
153
+ await sql.end(); // the caller retains connection ownership
154
+ }
155
+ ```
156
+
157
+ `drizzleStore(db)` v1 accepts only Drizzle instances backed by postgres.js. It
158
+ uses Mnemocyte's fixed tables in the `public` schema; the bundled migrations
159
+ must already have been applied, and the adapter does not create or migrate
160
+ tables. The caller's Drizzle schema map does not need to include Mnemocyte's
161
+ tables. Supplying both `databaseUrl` and `store` is ambiguous and throws
162
+ synchronously with `MnemocyteError` code `"CONFIG"`.
163
+
164
+ `databaseUrl` must use the `postgres:` or `postgresql:` protocol. Other URL
165
+ protocols reject synchronously with `MnemocyteError` code `"CONFIG"`; detailed
166
+ host, database, and credential validation remains the driver's responsibility.
167
+
119
168
  The package includes:
120
169
 
121
170
  ```txt
122
171
  migrations/0000_initial.sql
123
172
  migrations/0001_add_mnemocyte_meta.sql
173
+ migrations/0002_add_embedding_model.sql
124
174
  migrations/0000_initial.sql.template
125
175
  migrations/render-initial.mjs
126
176
  ```
@@ -131,16 +181,19 @@ order:
131
181
  ```txt
132
182
  migrations/0000_initial.sql
133
183
  migrations/0001_add_mnemocyte_meta.sql
184
+ migrations/0002_add_embedding_model.sql
134
185
  ```
135
186
 
136
187
  For an existing 0.1.x Postgres install, apply
137
- `migrations/0001_add_mnemocyte_meta.sql` to record the current 1536-dimensional
138
- installation metadata.
188
+ `migrations/0001_add_mnemocyte_meta.sql` and then
189
+ `migrations/0002_add_embedding_model.sql`. Existing 0.2.x or 0.3.x installs
190
+ only need `0002_add_embedding_model.sql`.
139
191
 
140
192
  For a different embedding dimension on a fresh installation, render an explicit
141
193
  initial migration from the template and apply that rendered file instead of the
142
194
  default `0000_initial.sql`. The rendered initial migration includes the
143
- matching `mnemocyte_meta` row.
195
+ matching `mnemocyte_meta` dimensions and the installation-model column, so it
196
+ does not also need `0001` or `0002`.
144
197
 
145
198
  ```bash
146
199
  node node_modules/mnemocyte/migrations/render-initial.mjs --dimensions 768 --out migrations/0000_initial.768.sql
@@ -152,14 +205,41 @@ Inside this repository, the equivalent development shortcut is:
152
205
  pnpm migration:render -- --dimensions 768 --out migrations/0000_initial.768.sql
153
206
  ```
154
207
 
155
- The Postgres backend supports one embedding dimension per installation. Before
156
- the first embedding-dependent Postgres operation, Mnemocyte reads
157
- `mnemocyte_meta` and validates it against `embedder.dimensions` before calling
158
- the embedder. A mismatch throws a `MnemocyteError` with code `"CONFIG"`; missing
159
- v0.2.0 metadata throws code `"MIGRATION"`.
208
+ The Postgres backend supports one embedding model and dimension per
209
+ installation. Before writes, recall, or duplicate scans, Mnemocyte reads
210
+ `mnemocyte_meta` and validates both values before calling the embedder or
211
+ comparing vectors. An empty installation atomically records the configured
212
+ model on its first embedding-dependent operation. Migration `0002` records the
213
+ single model already present in historical rows when that value is
214
+ unambiguous.
215
+
216
+ A configured model or dimension mismatch throws `MnemocyteError` code
217
+ `"CONFIG"`. If historical rows contain multiple model values, the installation
218
+ model remains unset and embedding-dependent operations throw `"MIGRATION"`
219
+ until an operator re-embeds or removes the mixed rows and explicitly records
220
+ the intended installation model. Non-embedding recovery operations such as
221
+ `stats`, `forget`, and pruning remain available.
222
+
223
+ Inspect mixed history before repairing it:
224
+
225
+ ```sql
226
+ SELECT embedding_model, count(*)
227
+ FROM mnemocyte_memories
228
+ GROUP BY embedding_model;
229
+ ```
230
+
231
+ Only after every retained row belongs to the intended vector space, record that
232
+ model explicitly:
160
233
 
161
- Use the embedding dimension documented for your chosen model, render or apply a
162
- matching schema, and keep one dimension per Mnemocyte installation.
234
+ ```sql
235
+ UPDATE mnemocyte_meta
236
+ SET embedding_model = 'your-model-id'
237
+ WHERE key = 'installation';
238
+ ```
239
+
240
+ Use the model identifier and embedding dimension documented for your embedder,
241
+ render or apply a matching schema, and keep one vector space per Mnemocyte
242
+ installation.
163
243
 
164
244
  The migration creates the bundled HNSW pgvector index
165
245
  `mnemocyte_memories_embedding_hnsw_idx` for cosine search. HNSW is approximate:
@@ -200,6 +280,19 @@ bigints, non-finite numbers, class instances, or cyclic objects are rejected
200
280
  with `MnemocyteError` code `"VALIDATION"`. Mnemocyte deep-clones metadata when
201
281
  writing and reading, so later mutations do not change stored state.
202
282
 
283
+ JavaScript callers receive the same runtime domain checks as TypeScript
284
+ callers: `type` and `importance` must be known values, tags must be an array of
285
+ strings, `source` must be a string when supplied, and `expiresAt` must be a
286
+ valid `Date`. Invalid values reject with `"VALIDATION"` before embedding or
287
+ storage. Mutable tags, metadata, and expiration dates are snapshotted when
288
+ `remember` or `rememberMany` is invoked, so later caller mutations cannot
289
+ change the pending write.
290
+
291
+ When observability is configured, snapshot and validation failures follow the
292
+ same operation lifecycle as provider or storage failures: one `"start"` event
293
+ and one `"error"` event carrying the thrown value. Snapshotting remains
294
+ synchronous before any awaited `onEvent` hook.
295
+
203
296
  For batches, cancellation belongs to the whole operation:
204
297
 
205
298
  ```ts
@@ -230,6 +323,11 @@ const memories = await client.recall({
230
323
  });
231
324
  ```
232
325
 
326
+ Successful recall results include the access metadata written by that call:
327
+ `accessCount`, `lastAccessedAt`, and `updatedAt` match stored state when the
328
+ promise resolves. Ranking and score explanations use the count from before the
329
+ current recall so the access update cannot change its own ordering.
330
+
233
331
  ### `buildContext`
234
332
 
235
333
  ```ts
@@ -241,8 +339,35 @@ const context = await client.buildContext({
241
339
  });
242
340
  ```
243
341
 
342
+ Plain-text context uses a deterministic `=` fence longer than any `=` run in
343
+ the query, rendered metadata, or included memory content. The resulting
344
+ `MEMORY N START/END` boundaries cannot be reproduced by untrusted content.
345
+ Markdown continues to choose a content-safe backtick fence, and XML continues
346
+ to escape content.
347
+
244
348
  When supplied, `maxTokens` must be a positive integer or `buildContext` rejects
245
- with `"VALIDATION"`. Omitting it keeps the default token-budget path.
349
+ with `"VALIDATION"`. Omitting it keeps the default token-budget path. Returned
350
+ context always fits the supplied counter and budget; when an extremely small
351
+ budget cannot fit the full truncation marker, the result is the longest marker
352
+ fragment that fits or an empty string.
353
+
354
+ ### `forget` and `forgetAll`
355
+
356
+ ```ts
357
+ await client.forget({
358
+ entityId: "user_123",
359
+ memoryId: "mem_old",
360
+ });
361
+
362
+ await client.forgetAll({ entityId: "user_123" });
363
+ ```
364
+
365
+ A memory cannot be deleted while another memory's `supersededBy` still points
366
+ to it as a consolidation survivor. `forget` and `forgetAll` reject with
367
+ `"CONFLICT"` before deleting anything in that case. This also applies when
368
+ `forgetAll` selects both the survivor and its superseded dependent. Delete the
369
+ superseded memories first; deleting a loser, or any memory with no dependents,
370
+ continues to work normally.
246
371
 
247
372
  ### `prune`
248
373
 
@@ -259,6 +384,23 @@ await client.prune({
259
384
  });
260
385
  ```
261
386
 
387
+ Prune selectors are validated and normalized before either backend is called.
388
+ Dates must be valid `Date` instances, enum values must be known, arrays must
389
+ contain valid values, and boolean fields must be actual booleans. `false`
390
+ selector flags and empty selector arrays do not count toward the required
391
+ selector; malformed or selector-free input rejects with `"VALIDATION"` without
392
+ deleting anything. Tag selectors are trimmed and deduplicated.
393
+
394
+ A dry run may preview a survivor among its matches. A non-dry-run prune rejects
395
+ with `"CONFLICT"` if any matching memory still has consolidation dependents,
396
+ and the entire prune batch remains unchanged; unrelated matching rows are not
397
+ partially deleted.
398
+
399
+ When audit logging is enabled, every successful non-dry prune writes one
400
+ best-effort `"memory.pruned"` event per affected entity, including global
401
+ prunes without an `entityId`. Dry runs and zero-deletion runs do not write prune
402
+ events.
403
+
262
404
  ### `findDuplicates`
263
405
 
264
406
  ```ts
@@ -269,6 +411,10 @@ const pairs = await client.findDuplicates({
269
411
  });
270
412
  ```
271
413
 
414
+ The in-memory implementation compares memory pairs quadratically. It degrades
415
+ noticeably once an entity grows beyond roughly a few thousand memories, so use
416
+ the Postgres backend for duplicate detection beyond that scale.
417
+
272
418
  ### `listAuditLog`
273
419
 
274
420
  ```ts
@@ -281,8 +427,24 @@ const events = await client.listAuditLog({
281
427
  entityId: "user_123",
282
428
  limit: 100,
283
429
  });
430
+
431
+ const last = events.at(-1);
432
+ const nextPage = last
433
+ ? await client.listAuditLog({
434
+ entityId: "user_123",
435
+ limit: 100,
436
+ beforeCursor: { timestamp: last.timestamp, id: last.id },
437
+ })
438
+ : [];
284
439
  ```
285
440
 
441
+ Audit entries are ordered newest first by timestamp and event ID. The
442
+ experimental composite `beforeCursor` / `afterCursor` fields provide stable
443
+ pagination when multiple events share a timestamp. The existing `before` and
444
+ `after` fields remain strict timestamp filters; they exclude all events at the
445
+ boundary timestamp and should not be used as the sole cursor when complete
446
+ tie-safe pagination is required.
447
+
286
448
  Ordinary audit writes are best-effort: an audit storage failure does not fail
287
449
  the primary client operation, so `listAuditLog` returns only events that were
288
450
  successfully persisted. Postgres consolidation is the exception; its
@@ -299,6 +461,26 @@ await client.experimental.consolidate({
299
461
  });
300
462
  ```
301
463
 
464
+ Consolidation records each loser's `supersededBy` reference to the survivor.
465
+ Retrying the same loser with that same survivor is an idempotent no-op. If any
466
+ requested loser already points to a different survivor, the call rejects with
467
+ `"CONFLICT"`. Mixed batches are atomic: no newly eligible loser is changed, no
468
+ tags are merged, and no consolidation audit event is written when any item
469
+ conflicts. Concurrent calls follow the same target-specific rule in both
470
+ backends. `supersededIds` must contain distinct IDs; duplicates reject with
471
+ `"VALIDATION"` before either store is accessed.
472
+
473
+ The store re-reads and protects the survivor in the same atomic boundary as
474
+ the loser updates, tag merge, and enabled audit events. If the survivor is
475
+ deleted or becomes superseded after initial validation but before that
476
+ boundary, the call rejects with `"CONFLICT"` without a partial mutation. Tag
477
+ merges start from the survivor's state inside that boundary, so concurrent
478
+ successful consolidations preserve every committed tag.
479
+
480
+ The survivor cannot later be deleted while any such reference remains:
481
+ `forget`, `forgetAll`, and matching non-dry-run `prune` calls reject with
482
+ `"CONFLICT"`. Deleting a superseded loser remains allowed.
483
+
302
484
  The `experimental` namespace may change before v1.0.
303
485
 
304
486
  ## Provider Resilience
@@ -315,6 +497,12 @@ const client = createMnemocyte({
315
497
  });
316
498
  ```
317
499
 
500
+ Provider resilience values are validated synchronously when the client is
501
+ created. Timeouts and delays must be finite non-negative numbers, retries must
502
+ be a non-negative integer, and `shouldRetry` must be a function when supplied;
503
+ invalid values throw `"CONFIG"`. A `maxDelayMs` below `baseDelayMs` remains
504
+ valid and is normalized up to the base delay.
505
+
318
506
  Pass `signal` to cancel an operation:
319
507
 
320
508
  ```ts
@@ -327,6 +515,17 @@ await client.remember({
327
515
  });
328
516
  ```
329
517
 
518
+ Pre-aborted signals reject with `"ABORTED"` before storage work begins.
519
+ In-memory maintenance scans check cancellation cooperatively. For Postgres,
520
+ standalone prune, duplicate-search, and audit-log queries request postgres.js
521
+ query cancellation while they are in flight.
522
+
523
+ Postgres consolidation has a transaction-specific boundary: cancellation is
524
+ checked between mutation steps and immediately before the transaction callback
525
+ returns. If a statement is already in flight, it may finish before the next
526
+ check throws and rolls the transaction back. An abort after the final check,
527
+ including while commit is in flight, may still leave the mutation committed.
528
+
330
529
  Common `MnemocyteError` codes:
331
530
 
332
531
  ```txt
@@ -337,6 +536,7 @@ VALIDATION
337
536
  CONFIG
338
537
  DB
339
538
  NOT_FOUND
539
+ CONFLICT
340
540
  MIGRATION
341
541
  ```
342
542
 
@@ -367,12 +567,51 @@ must be positive finite numbers, and `candidateMultiplier` must be an integer
367
567
  of at least 1. Invalid tuning is rejected at client construction with
368
568
  `MnemocyteError` code `"CONFIG"`.
369
569
 
570
+ ## Storage Adapter Contract
571
+
572
+ `MemoryStore` is still an internal boundary rather than a public package
573
+ export, but its future-adapter contract is now documented directly in
574
+ [`src/memory/store.ts`](./src/memory/store.ts). The contract is based on
575
+ postconditions, not identical implementation mechanics: every method is
576
+ mandatory, and no capability-flag surface exists or is currently planned.
577
+
578
+ For adapter implementers, the main rules are:
579
+
580
+ - `ensureSchema` is a readiness hook, not permission for hidden schema
581
+ creation. `ensureEmbeddingCompatibility` unconditionally enforces whatever
582
+ compatibility is meaningful to the persistence model; a store without
583
+ persistent installation state may no-op.
584
+ - `insertMemories` takes ownership of prepared rows and returns exactly one
585
+ detached record per prepared ID. Store return order is untrusted; shared
586
+ orchestration rejects missing, duplicate, or unknown IDs and restores input
587
+ order. `getMemoryEmbeddings`, `getMemory`, and `stats` use the exact keyed,
588
+ detached, and count postconditions described in the source comments.
589
+ - `markMemoriesAccessed` requires distinct IDs and returns the committed
590
+ post-update access fields. Duplicate-ID behavior is intentionally not a
591
+ portable contract. Ordinary `addAuditEvents` calls are best-effort, and a
592
+ rejected batch may have partially persisted on some backends.
593
+ - `listAuditLog` uses stable event IDs, comparable timestamps, strict tuple
594
+ filters, and matching newest-first `(timestamp, id)` order. Tuple pagination
595
+ needs no transaction. Cancellation may be cooperative or active depending on
596
+ the implementation.
597
+ - `vectorSearch` may scan or use an approximate, planner-dependent index.
598
+ `lexicalSearch` may legitimately return different candidates and rankings
599
+ across implementations. `findDuplicatePairs` compares qualifying memories
600
+ pairwise and does not imply indexed nearest-neighbor pair generation.
601
+ - `deleteMemory`, `deleteMemoriesForEntity`, and non-dry-run `prune` must
602
+ protect referenced consolidation survivors with one atomic,
603
+ non-interleavable check-and-mutate operation and reject with `"CONFLICT"`
604
+ before any deletion. Other cancellation boundaries remain method- and
605
+ implementation-specific as documented by the public operations.
606
+ - `close` releases or destroys only implementation-owned state and resources.
607
+ A future adapter around a caller-supplied connection must not close that
608
+ connection.
609
+
370
610
  ## Roadmap
371
611
 
372
612
  Planned larger milestones:
373
613
 
374
614
  - public `MemoryStore` adapter surface
375
- - `drizzleStore(db)` for caller-owned Drizzle clients
376
615
  - `@mnemocyte/mcp`
377
616
 
378
617
  See [ROADMAP.md](./docs/ROADMAP.md).
@@ -381,11 +620,15 @@ See [ROADMAP.md](./docs/ROADMAP.md).
381
620
 
382
621
  Current behavior:
383
622
 
384
- - `createMnemocyte()` selects either the in-memory backend or the
385
- Postgres/pgvector backend.
623
+ - `createMnemocyte()` selects an explicitly supplied `store`, the URL-owned
624
+ Postgres/pgvector backend, or the in-memory backend; supplying both `store`
625
+ and `databaseUrl` rejects instead of selecting a precedence.
386
626
  - Both backends run through an internal `MemoryStore` boundary and shared
387
627
  client orchestration; `MemoryStore` is not exported as a public adapter API
388
628
  yet.
629
+ - `mnemocyte/stores/drizzle` exposes the first supported public adapter factory
630
+ for a caller-owned postgres.js Drizzle instance without exporting the full
631
+ `MemoryStore` contract.
389
632
  - Postgres schema setup is explicit; the client does not create tables or
390
633
  indexes for you.
391
634
  - `findDuplicates`, audit-log workflows, and `experimental.consolidate` are
@@ -394,12 +637,13 @@ Current behavior:
394
637
  Known limitations before v1:
395
638
 
396
639
  - The internal `MemoryStore` boundary is not a public adapter API yet.
397
- `drizzleStore(db)` is the planned first public store adapter.
640
+ `drizzleStore(db)` returns an opaque config value and currently supports only
641
+ postgres.js, the `public` schema, and pre-applied Mnemocyte migrations.
398
642
  - Configured provider timeouts abort the per-attempt `AbortSignal`; the
399
643
  underlying provider request only stops promptly when the embedder honors that
400
644
  signal.
401
- - Postgres dimension metadata is installation-wide. Mixed embedding dimensions
402
- in one database are intentionally out of scope for now.
645
+ - Postgres model and dimension metadata are installation-wide. Mixed embedding
646
+ vector spaces in one database are intentionally unsupported.
403
647
 
404
648
  Planned v1 direction:
405
649
 
@@ -1,4 +1,4 @@
1
- import { c as Embedder } from "../types-gOPLre4V.mjs";
1
+ import { l as Embedder } from "../types-XUWYDoXM.mjs";
2
2
 
3
3
  //#region src/embedders/openai.d.ts
4
4
  interface OpenAIEmbedderOptions {
@@ -1 +1 @@
1
- {"version":3,"file":"openai.d.mts","names":[],"sources":["../../src/embedders/openai.ts"],"mappings":";;;UAciB,qBAAA;;AAAjB;;WAIU,MAAA;EAJ4B;;;EAAA,SAQ5B,KAAA;EAUA;;AAAO;AA4EjB;EA5EU,SALA,UAAA;;;;;WAKA,OAAA;AAAA;AA4E8D;;;;AAAA,iBAAxD,cAAA,CAAe,OAAA,EAAS,qBAAA,GAAwB,QAAQ"}
1
+ {"version":3,"file":"openai.d.mts","names":[],"sources":["../../src/embedders/openai.ts"],"mappings":";;;UAciB,qBAAA;;AAAjB;;WAIU,MAAA;EAJ4B;;;EAAA,SAQ5B,KAAA;EAUA;;AAAO;AAoIjB;EApIU,SALA,UAAA;;;;;WAKA,OAAA;AAAA;AAoI8D;;;;AAAA,iBAAxD,cAAA,CAAe,OAAA,EAAS,qBAAA,GAAwB,QAAQ"}
@@ -1,4 +1,4 @@
1
- import { t as MnemocyteError } from "../errors-B_7ZdjEO.mjs";
1
+ import { t as MnemocyteError } from "../errors-DIPysVWl.mjs";
2
2
  //#region src/embedders/openai.ts
3
3
  const KNOWN_MODEL_DIMENSIONS = {
4
4
  "text-embedding-3-small": 1536,
@@ -6,6 +6,28 @@ const KNOWN_MODEL_DIMENSIONS = {
6
6
  "text-embedding-ada-002": 1536
7
7
  };
8
8
  const CONFIGURABLE_DIMENSION_MODELS = new Set(["text-embedding-3-small", "text-embedding-3-large"]);
9
+ function isRecord(value) {
10
+ return typeof value === "object" && value !== null && !Array.isArray(value);
11
+ }
12
+ function validateEmbeddingResponse(body, expectedCount) {
13
+ if (!isRecord(body) || !Array.isArray(body.data)) throw new MnemocyteError("OpenAI returned malformed embedding response data.", "EMBEDDING");
14
+ if (body.data.length !== expectedCount) throw new MnemocyteError("OpenAI returned an unexpected number of embeddings.", "EMBEDDING");
15
+ const indices = /* @__PURE__ */ new Set();
16
+ const items = [];
17
+ for (const item of body.data) {
18
+ if (!isRecord(item)) throw new MnemocyteError("OpenAI returned a malformed embedding item.", "EMBEDDING");
19
+ const { embedding, index } = item;
20
+ if (typeof index !== "number" || !Number.isInteger(index) || index < 0 || index >= expectedCount) throw new MnemocyteError("OpenAI returned an embedding with an invalid index.", "EMBEDDING");
21
+ if (indices.has(index)) throw new MnemocyteError("OpenAI returned a duplicate embedding index.", "EMBEDDING");
22
+ if (!Array.isArray(embedding)) throw new MnemocyteError("OpenAI returned an embedding that is not an array.", "EMBEDDING");
23
+ indices.add(index);
24
+ items.push({
25
+ embedding,
26
+ index
27
+ });
28
+ }
29
+ return items;
30
+ }
9
31
  function assertDimensions(dimensions) {
10
32
  if (!Number.isInteger(dimensions) || dimensions < 1) throw new MnemocyteError("openaiEmbedder dimensions must be a positive integer.", "CONFIG");
11
33
  }
@@ -61,14 +83,7 @@ function openaiEmbedder(options) {
61
83
  ...embedOptions?.signal === void 0 ? {} : { signal: embedOptions.signal }
62
84
  });
63
85
  if (!response.ok) throw Object.assign(/* @__PURE__ */ new Error(`OpenAI embeddings request failed with status ${response.status}.`), { status: response.status });
64
- const body = await response.json();
65
- const embeddings = Array.from({ length: texts.length });
66
- for (const item of body.data) {
67
- if (!Number.isInteger(item.index) || item.index < 0 || item.index >= texts.length) throw new MnemocyteError("OpenAI returned an embedding with an invalid index.", "EMBEDDING");
68
- embeddings[item.index] = item.embedding;
69
- }
70
- if (embeddings.some((embedding) => embedding === void 0)) throw new MnemocyteError("OpenAI returned fewer embeddings than requested.", "EMBEDDING");
71
- return embeddings;
86
+ return validateEmbeddingResponse(await response.json(), texts.length).sort((left, right) => left.index - right.index).map((item) => item.embedding);
72
87
  }
73
88
  };
74
89
  }
@@ -1 +1 @@
1
- {"version":3,"file":"openai.mjs","names":[],"sources":["../../src/embedders/openai.ts"],"sourcesContent":["import { MnemocyteError } from \"../errors.js\";\nimport type { Embedder } from \"../types.js\";\n\nconst KNOWN_MODEL_DIMENSIONS: Record<string, number> = {\n\t\"text-embedding-3-small\": 1536,\n\t\"text-embedding-3-large\": 3072,\n\t\"text-embedding-ada-002\": 1536,\n};\n\nconst CONFIGURABLE_DIMENSION_MODELS = new Set([\n\t\"text-embedding-3-small\",\n\t\"text-embedding-3-large\",\n]);\n\nexport interface OpenAIEmbedderOptions {\n\t/**\n\t * OpenAI API key. Defaults to `process.env.OPENAI_API_KEY`.\n\t */\n\treadonly apiKey?: string;\n\t/**\n\t * OpenAI embedding model name.\n\t */\n\treadonly model: string;\n\t/**\n\t * Output dimensions. Required for unknown/future model names and accepted\n\t * only for OpenAI models that support configurable dimensions.\n\t */\n\treadonly dimensions?: number;\n\t/**\n\t * Override the OpenAI API base URL. Intended for tests and compatible\n\t * deployments; defaults to `https://api.openai.com/v1`.\n\t */\n\treadonly baseUrl?: string;\n}\n\ninterface OpenAIEmbeddingItem {\n\treadonly embedding: number[];\n\treadonly index: number;\n}\n\ninterface OpenAIEmbeddingResponse {\n\treadonly data: OpenAIEmbeddingItem[];\n}\n\nfunction assertDimensions(dimensions: number): void {\n\tif (!Number.isInteger(dimensions) || dimensions < 1) {\n\t\tthrow new MnemocyteError(\n\t\t\t\"openaiEmbedder dimensions must be a positive integer.\",\n\t\t\t\"CONFIG\",\n\t\t);\n\t}\n}\n\nfunction resolveApiKey(apiKey: string | undefined): string {\n\tconst resolved = apiKey ?? process.env.OPENAI_API_KEY;\n\tif (!resolved || resolved.trim().length === 0) {\n\t\tthrow new MnemocyteError(\n\t\t\t\"openaiEmbedder requires an API key. Pass apiKey or set OPENAI_API_KEY.\",\n\t\t\t\"CONFIG\",\n\t\t);\n\t}\n\treturn resolved;\n}\n\nfunction resolveDimensions(options: OpenAIEmbedderOptions): {\n\tembedderDimensions: number;\n\trequestDimensions?: number;\n} {\n\tconst defaultDimensions = KNOWN_MODEL_DIMENSIONS[options.model];\n\tif (defaultDimensions === undefined) {\n\t\tif (options.dimensions === undefined) {\n\t\t\tthrow new MnemocyteError(\n\t\t\t\t`openaiEmbedder requires dimensions for unknown model \"${options.model}\".`,\n\t\t\t\t\"CONFIG\",\n\t\t\t);\n\t\t}\n\t\tassertDimensions(options.dimensions);\n\t\treturn {\n\t\t\tembedderDimensions: options.dimensions,\n\t\t\trequestDimensions: options.dimensions,\n\t\t};\n\t}\n\n\tif (\n\t\toptions.dimensions !== undefined &&\n\t\t!CONFIGURABLE_DIMENSION_MODELS.has(options.model)\n\t) {\n\t\tthrow new MnemocyteError(\n\t\t\t`openaiEmbedder does not support a dimensions override for \"${options.model}\".`,\n\t\t\t\"CONFIG\",\n\t\t);\n\t}\n\n\tif (options.dimensions !== undefined) {\n\t\tassertDimensions(options.dimensions);\n\t\treturn {\n\t\t\tembedderDimensions: options.dimensions,\n\t\t\trequestDimensions: options.dimensions,\n\t\t};\n\t}\n\n\treturn { embedderDimensions: defaultDimensions };\n}\n\n/**\n * Create an OpenAI-backed {@link Embedder}. This adapter uses `fetch`\n * directly so provider SDKs never become part of Mnemocyte's dependency graph.\n */\nexport function openaiEmbedder(options: OpenAIEmbedderOptions): Embedder {\n\tconst apiKey = resolveApiKey(options.apiKey);\n\tconst { embedderDimensions, requestDimensions } = resolveDimensions(options);\n\tconst baseUrl = options.baseUrl ?? \"https://api.openai.com/v1\";\n\n\treturn {\n\t\tmodel: options.model,\n\t\tdimensions: embedderDimensions,\n\t\tasync embed(texts, embedOptions) {\n\t\t\tconst response = await fetch(`${baseUrl}/embeddings`, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: {\n\t\t\t\t\tauthorization: `Bearer ${apiKey}`,\n\t\t\t\t\t\"content-type\": \"application/json\",\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\tmodel: options.model,\n\t\t\t\t\tinput: [...texts],\n\t\t\t\t\tencoding_format: \"float\",\n\t\t\t\t\t...(requestDimensions === undefined\n\t\t\t\t\t\t? {}\n\t\t\t\t\t\t: { dimensions: requestDimensions }),\n\t\t\t\t}),\n\t\t\t\t...(embedOptions?.signal === undefined\n\t\t\t\t\t? {}\n\t\t\t\t\t: { signal: embedOptions.signal }),\n\t\t\t});\n\n\t\t\tif (!response.ok) {\n\t\t\t\tthrow Object.assign(\n\t\t\t\t\tnew Error(\n\t\t\t\t\t\t`OpenAI embeddings request failed with status ${response.status}.`,\n\t\t\t\t\t),\n\t\t\t\t\t{ status: response.status },\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst body = (await response.json()) as OpenAIEmbeddingResponse;\n\n\t\t\tconst embeddings: Array<number[] | undefined> = Array.from({\n\t\t\t\tlength: texts.length,\n\t\t\t});\n\t\t\tfor (const item of body.data) {\n\t\t\t\tif (\n\t\t\t\t\t!Number.isInteger(item.index) ||\n\t\t\t\t\titem.index < 0 ||\n\t\t\t\t\titem.index >= texts.length\n\t\t\t\t) {\n\t\t\t\t\tthrow new MnemocyteError(\n\t\t\t\t\t\t\"OpenAI returned an embedding with an invalid index.\",\n\t\t\t\t\t\t\"EMBEDDING\",\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tembeddings[item.index] = item.embedding;\n\t\t\t}\n\t\t\tif (embeddings.some((embedding) => embedding === undefined)) {\n\t\t\t\tthrow new MnemocyteError(\n\t\t\t\t\t\"OpenAI returned fewer embeddings than requested.\",\n\t\t\t\t\t\"EMBEDDING\",\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn embeddings as number[][];\n\t\t},\n\t};\n}\n"],"mappings":";;AAGA,MAAM,yBAAiD;CACtD,0BAA0B;CAC1B,0BAA0B;CAC1B,0BAA0B;AAC3B;AAEA,MAAM,gCAAgC,IAAI,IAAI,CAC7C,0BACA,wBACD,CAAC;AAgCD,SAAS,iBAAiB,YAA0B;CACnD,IAAI,CAAC,OAAO,UAAU,UAAU,KAAK,aAAa,GACjD,MAAM,IAAI,eACT,yDACA,QACD;AAEF;AAEA,SAAS,cAAc,QAAoC;CAC1D,MAAM,WAAW,UAAU,QAAQ,IAAI;CACvC,IAAI,CAAC,YAAY,SAAS,KAAK,EAAE,WAAW,GAC3C,MAAM,IAAI,eACT,0EACA,QACD;CAED,OAAO;AACR;AAEA,SAAS,kBAAkB,SAGzB;CACD,MAAM,oBAAoB,uBAAuB,QAAQ;CACzD,IAAI,sBAAsB,KAAA,GAAW;EACpC,IAAI,QAAQ,eAAe,KAAA,GAC1B,MAAM,IAAI,eACT,yDAAyD,QAAQ,MAAM,KACvE,QACD;EAED,iBAAiB,QAAQ,UAAU;EACnC,OAAO;GACN,oBAAoB,QAAQ;GAC5B,mBAAmB,QAAQ;EAC5B;CACD;CAEA,IACC,QAAQ,eAAe,KAAA,KACvB,CAAC,8BAA8B,IAAI,QAAQ,KAAK,GAEhD,MAAM,IAAI,eACT,8DAA8D,QAAQ,MAAM,KAC5E,QACD;CAGD,IAAI,QAAQ,eAAe,KAAA,GAAW;EACrC,iBAAiB,QAAQ,UAAU;EACnC,OAAO;GACN,oBAAoB,QAAQ;GAC5B,mBAAmB,QAAQ;EAC5B;CACD;CAEA,OAAO,EAAE,oBAAoB,kBAAkB;AAChD;;;;;AAMA,SAAgB,eAAe,SAA0C;CACxE,MAAM,SAAS,cAAc,QAAQ,MAAM;CAC3C,MAAM,EAAE,oBAAoB,sBAAsB,kBAAkB,OAAO;CAC3E,MAAM,UAAU,QAAQ,WAAW;CAEnC,OAAO;EACN,OAAO,QAAQ;EACf,YAAY;EACZ,MAAM,MAAM,OAAO,cAAc;GAChC,MAAM,WAAW,MAAM,MAAM,GAAG,QAAQ,cAAc;IACrD,QAAQ;IACR,SAAS;KACR,eAAe,UAAU;KACzB,gBAAgB;IACjB;IACA,MAAM,KAAK,UAAU;KACpB,OAAO,QAAQ;KACf,OAAO,CAAC,GAAG,KAAK;KAChB,iBAAiB;KACjB,GAAI,sBAAsB,KAAA,IACvB,CAAC,IACD,EAAE,YAAY,kBAAkB;IACpC,CAAC;IACD,GAAI,cAAc,WAAW,KAAA,IAC1B,CAAC,IACD,EAAE,QAAQ,aAAa,OAAO;GAClC,CAAC;GAED,IAAI,CAAC,SAAS,IACb,MAAM,OAAO,uBACZ,IAAI,MACH,gDAAgD,SAAS,OAAO,EACjE,GACA,EAAE,QAAQ,SAAS,OAAO,CAC3B;GAGD,MAAM,OAAQ,MAAM,SAAS,KAAK;GAElC,MAAM,aAA0C,MAAM,KAAK,EAC1D,QAAQ,MAAM,OACf,CAAC;GACD,KAAK,MAAM,QAAQ,KAAK,MAAM;IAC7B,IACC,CAAC,OAAO,UAAU,KAAK,KAAK,KAC5B,KAAK,QAAQ,KACb,KAAK,SAAS,MAAM,QAEpB,MAAM,IAAI,eACT,uDACA,WACD;IAED,WAAW,KAAK,SAAS,KAAK;GAC/B;GACA,IAAI,WAAW,MAAM,cAAc,cAAc,KAAA,CAAS,GACzD,MAAM,IAAI,eACT,oDACA,WACD;GAED,OAAO;EACR;CACD;AACD"}
1
+ {"version":3,"file":"openai.mjs","names":[],"sources":["../../src/embedders/openai.ts"],"sourcesContent":["import { MnemocyteError } from \"../errors.js\";\nimport type { Embedder } from \"../types.js\";\n\nconst KNOWN_MODEL_DIMENSIONS: Record<string, number> = {\n\t\"text-embedding-3-small\": 1536,\n\t\"text-embedding-3-large\": 3072,\n\t\"text-embedding-ada-002\": 1536,\n};\n\nconst CONFIGURABLE_DIMENSION_MODELS = new Set([\n\t\"text-embedding-3-small\",\n\t\"text-embedding-3-large\",\n]);\n\nexport interface OpenAIEmbedderOptions {\n\t/**\n\t * OpenAI API key. Defaults to `process.env.OPENAI_API_KEY`.\n\t */\n\treadonly apiKey?: string;\n\t/**\n\t * OpenAI embedding model name.\n\t */\n\treadonly model: string;\n\t/**\n\t * Output dimensions. Required for unknown/future model names and accepted\n\t * only for OpenAI models that support configurable dimensions.\n\t */\n\treadonly dimensions?: number;\n\t/**\n\t * Override the OpenAI API base URL. Intended for tests and compatible\n\t * deployments; defaults to `https://api.openai.com/v1`.\n\t */\n\treadonly baseUrl?: string;\n}\n\ninterface OpenAIEmbeddingItem {\n\treadonly embedding: number[];\n\treadonly index: number;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction validateEmbeddingResponse(\n\tbody: unknown,\n\texpectedCount: number,\n): OpenAIEmbeddingItem[] {\n\tif (!isRecord(body) || !Array.isArray(body.data)) {\n\t\tthrow new MnemocyteError(\n\t\t\t\"OpenAI returned malformed embedding response data.\",\n\t\t\t\"EMBEDDING\",\n\t\t);\n\t}\n\tif (body.data.length !== expectedCount) {\n\t\tthrow new MnemocyteError(\n\t\t\t\"OpenAI returned an unexpected number of embeddings.\",\n\t\t\t\"EMBEDDING\",\n\t\t);\n\t}\n\n\tconst indices = new Set<number>();\n\tconst items: OpenAIEmbeddingItem[] = [];\n\tfor (const item of body.data) {\n\t\tif (!isRecord(item)) {\n\t\t\tthrow new MnemocyteError(\n\t\t\t\t\"OpenAI returned a malformed embedding item.\",\n\t\t\t\t\"EMBEDDING\",\n\t\t\t);\n\t\t}\n\t\tconst { embedding, index } = item;\n\t\tif (\n\t\t\ttypeof index !== \"number\" ||\n\t\t\t!Number.isInteger(index) ||\n\t\t\tindex < 0 ||\n\t\t\tindex >= expectedCount\n\t\t) {\n\t\t\tthrow new MnemocyteError(\n\t\t\t\t\"OpenAI returned an embedding with an invalid index.\",\n\t\t\t\t\"EMBEDDING\",\n\t\t\t);\n\t\t}\n\t\tif (indices.has(index)) {\n\t\t\tthrow new MnemocyteError(\n\t\t\t\t\"OpenAI returned a duplicate embedding index.\",\n\t\t\t\t\"EMBEDDING\",\n\t\t\t);\n\t\t}\n\t\tif (!Array.isArray(embedding)) {\n\t\t\tthrow new MnemocyteError(\n\t\t\t\t\"OpenAI returned an embedding that is not an array.\",\n\t\t\t\t\"EMBEDDING\",\n\t\t\t);\n\t\t}\n\t\tindices.add(index);\n\t\titems.push({ embedding: embedding as number[], index });\n\t}\n\treturn items;\n}\n\nfunction assertDimensions(dimensions: number): void {\n\tif (!Number.isInteger(dimensions) || dimensions < 1) {\n\t\tthrow new MnemocyteError(\n\t\t\t\"openaiEmbedder dimensions must be a positive integer.\",\n\t\t\t\"CONFIG\",\n\t\t);\n\t}\n}\n\nfunction resolveApiKey(apiKey: string | undefined): string {\n\tconst resolved = apiKey ?? process.env.OPENAI_API_KEY;\n\tif (!resolved || resolved.trim().length === 0) {\n\t\tthrow new MnemocyteError(\n\t\t\t\"openaiEmbedder requires an API key. Pass apiKey or set OPENAI_API_KEY.\",\n\t\t\t\"CONFIG\",\n\t\t);\n\t}\n\treturn resolved;\n}\n\nfunction resolveDimensions(options: OpenAIEmbedderOptions): {\n\tembedderDimensions: number;\n\trequestDimensions?: number;\n} {\n\tconst defaultDimensions = KNOWN_MODEL_DIMENSIONS[options.model];\n\tif (defaultDimensions === undefined) {\n\t\tif (options.dimensions === undefined) {\n\t\t\tthrow new MnemocyteError(\n\t\t\t\t`openaiEmbedder requires dimensions for unknown model \"${options.model}\".`,\n\t\t\t\t\"CONFIG\",\n\t\t\t);\n\t\t}\n\t\tassertDimensions(options.dimensions);\n\t\treturn {\n\t\t\tembedderDimensions: options.dimensions,\n\t\t\trequestDimensions: options.dimensions,\n\t\t};\n\t}\n\n\tif (\n\t\toptions.dimensions !== undefined &&\n\t\t!CONFIGURABLE_DIMENSION_MODELS.has(options.model)\n\t) {\n\t\tthrow new MnemocyteError(\n\t\t\t`openaiEmbedder does not support a dimensions override for \"${options.model}\".`,\n\t\t\t\"CONFIG\",\n\t\t);\n\t}\n\n\tif (options.dimensions !== undefined) {\n\t\tassertDimensions(options.dimensions);\n\t\treturn {\n\t\t\tembedderDimensions: options.dimensions,\n\t\t\trequestDimensions: options.dimensions,\n\t\t};\n\t}\n\n\treturn { embedderDimensions: defaultDimensions };\n}\n\n/**\n * Create an OpenAI-backed {@link Embedder}. This adapter uses `fetch`\n * directly so provider SDKs never become part of Mnemocyte's dependency graph.\n */\nexport function openaiEmbedder(options: OpenAIEmbedderOptions): Embedder {\n\tconst apiKey = resolveApiKey(options.apiKey);\n\tconst { embedderDimensions, requestDimensions } = resolveDimensions(options);\n\tconst baseUrl = options.baseUrl ?? \"https://api.openai.com/v1\";\n\n\treturn {\n\t\tmodel: options.model,\n\t\tdimensions: embedderDimensions,\n\t\tasync embed(texts, embedOptions) {\n\t\t\tconst response = await fetch(`${baseUrl}/embeddings`, {\n\t\t\t\tmethod: \"POST\",\n\t\t\t\theaders: {\n\t\t\t\t\tauthorization: `Bearer ${apiKey}`,\n\t\t\t\t\t\"content-type\": \"application/json\",\n\t\t\t\t},\n\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\tmodel: options.model,\n\t\t\t\t\tinput: [...texts],\n\t\t\t\t\tencoding_format: \"float\",\n\t\t\t\t\t...(requestDimensions === undefined\n\t\t\t\t\t\t? {}\n\t\t\t\t\t\t: { dimensions: requestDimensions }),\n\t\t\t\t}),\n\t\t\t\t...(embedOptions?.signal === undefined\n\t\t\t\t\t? {}\n\t\t\t\t\t: { signal: embedOptions.signal }),\n\t\t\t});\n\n\t\t\tif (!response.ok) {\n\t\t\t\tthrow Object.assign(\n\t\t\t\t\tnew Error(\n\t\t\t\t\t\t`OpenAI embeddings request failed with status ${response.status}.`,\n\t\t\t\t\t),\n\t\t\t\t\t{ status: response.status },\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst items = validateEmbeddingResponse(\n\t\t\t\tawait response.json(),\n\t\t\t\ttexts.length,\n\t\t\t);\n\t\t\treturn items\n\t\t\t\t.sort((left, right) => left.index - right.index)\n\t\t\t\t.map((item) => item.embedding);\n\t\t},\n\t};\n}\n"],"mappings":"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"}
@@ -51,4 +51,4 @@ function isMnemocyteError(error) {
51
51
  //#endregion
52
52
  export { isMnemocyteError as n, MnemocyteError as t };
53
53
 
54
- //# sourceMappingURL=errors-B_7ZdjEO.mjs.map
54
+ //# sourceMappingURL=errors-DIPysVWl.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors-DIPysVWl.mjs","names":[],"sources":["../src/errors.ts"],"sourcesContent":["/**\n * Stable, machine-readable error code attached to every {@link MnemocyteError}.\n *\n * - `\"CONFIG\"` — invalid client configuration (e.g. missing or malformed embedder).\n * - `\"VALIDATION\"` — invalid argument passed to a client method.\n * - `\"DB\"` — Postgres or driver-level failure.\n * - `\"EMBEDDING\"` — embedder threw or returned an invalid vector.\n * - `\"NOT_FOUND\"` — referenced memory or entity does not exist.\n * - `\"CONFLICT\"` — the requested mutation conflicts with stored relationships.\n * - `\"MIGRATION\"` — schema/migration check failed against the target database.\n * - `\"TIMEOUT\"` — a provider call exceeded its configured timeout.\n * - `\"ABORTED\"` — the operation was cancelled via an `AbortSignal`.\n */\nexport type MnemocyteErrorCode =\n\t| \"CONFIG\"\n\t| \"VALIDATION\"\n\t| \"DB\"\n\t| \"EMBEDDING\"\n\t| \"NOT_FOUND\"\n\t| \"CONFLICT\"\n\t| \"MIGRATION\"\n\t| \"TIMEOUT\"\n\t| \"ABORTED\";\n\n/**\n * Error type thrown by every public Mnemocyte API.\n *\n * Use the {@link MnemocyteError.code} property — not the message — to branch\n * on failure modes, or call {@link isMnemocyteError} as a type guard when\n * working with `unknown` errors.\n *\n * @example\n * ```ts\n * try {\n * await client.recall({ entityId: \"u1\", query: \"hi\" });\n * } catch (err) {\n * if (isMnemocyteError(err) && err.code === \"DB\") {\n * // handle database failure\n * }\n * throw err;\n * }\n * ```\n */\nexport class MnemocyteError extends Error {\n\t/** Stable, machine-readable failure code. See {@link MnemocyteErrorCode}. */\n\treadonly code: MnemocyteErrorCode;\n\t/** Underlying cause, if any (e.g. an original driver or embedder error). */\n\toverride readonly cause?: unknown;\n\n\t/**\n\t * @param message - Human-readable error message.\n\t * @param code - Stable failure code; see {@link MnemocyteErrorCode}.\n\t * @param cause - Optional underlying cause (driver error, network error, etc.).\n\t */\n\tconstructor(message: string, code: MnemocyteErrorCode, cause?: unknown) {\n\t\tsuper(message);\n\t\tthis.name = \"MnemocyteError\";\n\t\tthis.code = code;\n\t\tthis.cause = cause;\n\t}\n}\n\n/**\n * Type guard for {@link MnemocyteError}.\n *\n * This guard uses `instanceof` and recognizes errors created by the same loaded\n * Mnemocyte class. It does not recognize structurally similar errors or errors\n * created by another installed copy of the package.\n *\n * @param error - Value to test (typically a caught `unknown`).\n * @returns `true` if `error` is a {@link MnemocyteError}.\n */\nexport function isMnemocyteError(error: unknown): error is MnemocyteError {\n\treturn error instanceof MnemocyteError;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AA2CA,IAAa,iBAAb,cAAoC,MAAM;;CAEzC;;CAEA;;;;;;CAOA,YAAY,SAAiB,MAA0B,OAAiB;EACvE,MAAM,OAAO;EACb,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,KAAK,QAAQ;CACd;AACD;;;;;;;;;;;AAYA,SAAgB,iBAAiB,OAAyC;CACzE,OAAO,iBAAiB;AACzB"}
package/dist/index.d.mts CHANGED
@@ -1,23 +1,26 @@
1
- import { A as RecallInput, C as MnemocyteObservation, D as ProviderResilienceConfig, E as ObservabilityConfig, F as RetrievalScoreWeights, I as RetrievalScores, L as TokenCounter, M as RememberManyInput, N as RetrievalConfig, O as PruneInput, P as RetrievalExplanation, S as MnemocyteConfig, T as MnemocyteOperation, _ as Memory, a as ConsolidateResult, b as MnemocyteBackend, c as Embedder, d as FindDuplicatesInput, f as GlobalStats, g as ListAuditLogInput, h as JsonValue, i as ConsolidateInput, j as RememberInput, k as PruneResult, l as EntityStats, m as JsonObject, n as AuditEvent, o as ContextFormat, p as ImportanceLevel, r as BuildContextInput, s as DuplicatePair, t as AuditConfig, u as ExperimentalMnemocyteClient, v as MemoryType, w as MnemocyteObservationPhase, x as MnemocyteClient, y as MemoryWithScore } from "./types-gOPLre4V.mjs";
1
+ import { A as PruneInput, C as MnemocyteConfig, D as MnemocyteStoreConfig, E as MnemocyteOperation, F as RetrievalConfig, I as RetrievalExplanation, L as RetrievalScoreWeights, M as RecallInput, N as RememberInput, O as ObservabilityConfig, P as RememberManyInput, R as RetrievalScores, S as MnemocyteClient, T as MnemocyteObservationPhase, _ as ListAuditLogInput, a as ConsolidateInput, b as MemoryWithScore, c as DuplicatePair, d as ExperimentalMnemocyteClient, f as FindDuplicatesInput, g as JsonValue, h as JsonObject, i as BuildContextInput, j as PruneResult, k as ProviderResilienceConfig, l as Embedder, m as ImportanceLevel, n as AuditEvent, o as ConsolidateResult, p as GlobalStats, r as AuditLogCursor, s as ContextFormat, t as AuditConfig, u as EntityStats, v as Memory, w as MnemocyteObservation, x as MnemocyteBackend, y as MemoryType, z as TokenCounter } from "./types-XUWYDoXM.mjs";
2
2
 
3
3
  //#region src/client.d.ts
4
4
  /**
5
5
  * Create a Mnemocyte client.
6
6
  *
7
- * When {@link MnemocyteConfig.databaseUrl} is provided, a Postgres-backed
8
- * client is returned (requires the `pgvector` extension and the bundled
9
- * migration applied). Otherwise an in-memory client is returned, intended
10
- * for tests, demos, and short-lived processes.
7
+ * When {@link MnemocyteConfig.store} is provided, that adapter is used. When
8
+ * {@link MnemocyteConfig.databaseUrl} is provided, a URL-owned Postgres client
9
+ * is used (requires the `pgvector` extension and the bundled migrations).
10
+ * When neither is supplied, an in-memory client is returned for tests, demos,
11
+ * and short-lived processes.
11
12
  *
12
- * The returned client owns any underlying resources (e.g. the Postgres
13
- * connection pool). Call {@link MnemocyteClient.close} when finished to
14
- * release them.
13
+ * Call {@link MnemocyteClient.close} when finished. URL-created database
14
+ * resources are owned and closed by Mnemocyte; resources behind a supplied
15
+ * store retain the ownership policy documented by that adapter.
15
16
  *
16
17
  * @param config - Client configuration. {@link MnemocyteConfig.embedder} is required.
17
18
  * @returns A {@link MnemocyteClient} instance.
18
19
  * @throws {MnemocyteError} With code `"CONFIG"` if `embedder`, retrieval
19
- * tuning, or the database URL is malformed, or `"VALIDATION"` if
20
- * `databaseUrl` is explicitly empty.
20
+ * tuning, provider resilience, or the database URL is malformed or does not
21
+ * use the `postgres:` / `postgresql:` protocol, or if `databaseUrl` and
22
+ * `store` are both provided; or `"VALIDATION"` if `databaseUrl` is explicitly
23
+ * empty.
21
24
  *
22
25
  * @example Postgres-backed client
23
26
  * ```ts
@@ -38,11 +41,12 @@ declare function createMnemocyte(config: MnemocyteConfig): MnemocyteClient;
38
41
  * - `"DB"` — Postgres or driver-level failure.
39
42
  * - `"EMBEDDING"` — embedder threw or returned an invalid vector.
40
43
  * - `"NOT_FOUND"` — referenced memory or entity does not exist.
44
+ * - `"CONFLICT"` — the requested mutation conflicts with stored relationships.
41
45
  * - `"MIGRATION"` — schema/migration check failed against the target database.
42
46
  * - `"TIMEOUT"` — a provider call exceeded its configured timeout.
43
47
  * - `"ABORTED"` — the operation was cancelled via an `AbortSignal`.
44
48
  */
45
- type MnemocyteErrorCode = "CONFIG" | "VALIDATION" | "DB" | "EMBEDDING" | "NOT_FOUND" | "MIGRATION" | "TIMEOUT" | "ABORTED";
49
+ type MnemocyteErrorCode = "CONFIG" | "VALIDATION" | "DB" | "EMBEDDING" | "NOT_FOUND" | "CONFLICT" | "MIGRATION" | "TIMEOUT" | "ABORTED";
46
50
  /**
47
51
  * Error type thrown by every public Mnemocyte API.
48
52
  *
@@ -86,5 +90,5 @@ declare class MnemocyteError extends Error {
86
90
  */
87
91
  declare function isMnemocyteError(error: unknown): error is MnemocyteError;
88
92
  //#endregion
89
- export { type AuditConfig, type AuditEvent, type BuildContextInput, type ConsolidateInput, type ConsolidateResult, type ContextFormat, type DuplicatePair, type Embedder, type EntityStats, type ExperimentalMnemocyteClient, type FindDuplicatesInput, type GlobalStats, type ImportanceLevel, type JsonObject, type JsonValue, type ListAuditLogInput, type Memory, type MemoryType, type MemoryWithScore, type MnemocyteBackend, type MnemocyteClient, type MnemocyteConfig, MnemocyteError, type MnemocyteErrorCode, type MnemocyteObservation, type MnemocyteObservationPhase, type MnemocyteOperation, type ObservabilityConfig, type ProviderResilienceConfig, type PruneInput, type PruneResult, type RecallInput, type RememberInput, type RememberManyInput, type RetrievalConfig, type RetrievalExplanation, type RetrievalScoreWeights, type RetrievalScores, type TokenCounter, createMnemocyte, isMnemocyteError };
93
+ export { type AuditConfig, type AuditEvent, type AuditLogCursor, type BuildContextInput, type ConsolidateInput, type ConsolidateResult, type ContextFormat, type DuplicatePair, type Embedder, type EntityStats, type ExperimentalMnemocyteClient, type FindDuplicatesInput, type GlobalStats, type ImportanceLevel, type JsonObject, type JsonValue, type ListAuditLogInput, type Memory, type MemoryType, type MemoryWithScore, type MnemocyteBackend, type MnemocyteClient, type MnemocyteConfig, MnemocyteError, type MnemocyteErrorCode, type MnemocyteObservation, type MnemocyteObservationPhase, type MnemocyteOperation, type MnemocyteStoreConfig, type ObservabilityConfig, type ProviderResilienceConfig, type PruneInput, type PruneResult, type RecallInput, type RememberInput, type RememberManyInput, type RetrievalConfig, type RetrievalExplanation, type RetrievalScoreWeights, type RetrievalScores, type TokenCounter, createMnemocyte, isMnemocyteError };
90
94
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/client.ts","../src/errors.ts"],"mappings":";;;;;AAoCA;;;;;;;;AAAyE;;;;ACxBzE;;;;AAA8B;AA6B9B;;;;;;;iBDLgB,eAAA,CAAgB,MAAA,EAAQ,eAAA,GAAkB,eAAe;;;;;;AAAzE;;;;;;;;AAAyE;KCxB7D,kBAAA;;;AAAZ;;;;AAA8B;AA6B9B;;;;;;;;;;;;cAAa,cAAA,SAAuB,KAAA;EAWvB;EAAA,SATH,IAAA,EAAM,kBAAA;EASc;EAAA,SAPX,KAAA;EAOoD;AAAA;AAkBvE;;;cAlBa,OAAA,UAAiB,IAAA,EAAM,kBAAA,EAAoB,KAAA;AAAA;;;;AAkBiB;;;;;;;iBAAzD,gBAAA,CAAiB,KAAA,YAAiB,KAAA,IAAS,cAAc"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/client.ts","../src/errors.ts"],"mappings":";;;;;AA2CA;;;;;;;;AAAyE;;;;AC9BzE;;;;AAA8B;AA8B9B;;;;;;;;;;iBDAgB,eAAA,CAAgB,MAAA,EAAQ,eAAA,GAAkB,eAAe;;;;;;AAAzE;;;;;;;;AAAyE;;KC9B7D,kBAAA;;AAAZ;;;;AAA8B;AA8B9B;;;;;;;;;;;;;cAAa,cAAA,SAAuB,KAAA;EAWA;EAAA,SAT1B,IAAA,EAAM,kBAAA;EASwC;EAAA,SAPrC,KAAA;EAOoD;AAkBvE;;;;cAlBa,OAAA,UAAiB,IAAA,EAAM,kBAAA,EAAoB,KAAA;AAAA;;;AAkBiB;;;;;;;;iBAAzD,gBAAA,CAAiB,KAAA,YAAiB,KAAA,IAAS,cAAc"}