mnemocyte 0.2.0 → 0.4.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 retries, operation timeouts, and caller `AbortSignal` cancellation.
19
+ - Provider retries, per-attempt 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`.
@@ -66,10 +66,16 @@ await client.close();
66
66
  ```
67
67
 
68
68
  When `databaseUrl` is omitted, Mnemocyte uses the in-memory backend.
69
+ That backend is intended for development, tests, and prototyping. For
70
+ persistent or larger workloads, use Postgres.
69
71
 
70
72
  ## Embedder
71
73
 
72
74
  Mnemocyte accepts any embedder that returns one vector per input text.
75
+ Each vector must match `embedder.dimensions`, contain only finite numbers, and
76
+ have at least one nonzero component. Invalid provider output, including an
77
+ exact zero-norm vector, fails with `MnemocyteError` code `"EMBEDDING"` before
78
+ storage or vector comparison. Tiny nonzero vectors remain valid.
73
79
 
74
80
  ```ts
75
81
  const client = createMnemocyte({
@@ -99,7 +105,11 @@ const embedder = openaiEmbedder({
99
105
  explicitly when you use a different key source. The helper uses `fetch`
100
106
  directly and does not depend on the OpenAI SDK; plain `import "mnemocyte"` also
101
107
  stays provider-free. The direct subpath `mnemocyte/embedders/openai` is also
102
- supported for consumers that prefer provider-specific imports.
108
+ supported for consumers that prefer provider-specific imports. Provider
109
+ responses must contain exactly one uniquely indexed array embedding per input;
110
+ malformed response shapes fail with `MnemocyteError` code `"EMBEDDING"`.
111
+ Dimension and numeric-component validation remains at the shared embedder
112
+ boundary.
103
113
 
104
114
  ## Postgres
105
115
 
@@ -113,11 +123,16 @@ const client = createMnemocyte({
113
123
  });
114
124
  ```
115
125
 
126
+ `databaseUrl` must use the `postgres:` or `postgresql:` protocol. Other URL
127
+ protocols reject synchronously with `MnemocyteError` code `"CONFIG"`; detailed
128
+ host, database, and credential validation remains the driver's responsibility.
129
+
116
130
  The package includes:
117
131
 
118
132
  ```txt
119
133
  migrations/0000_initial.sql
120
134
  migrations/0001_add_mnemocyte_meta.sql
135
+ migrations/0002_add_embedding_model.sql
121
136
  migrations/0000_initial.sql.template
122
137
  migrations/render-initial.mjs
123
138
  ```
@@ -128,16 +143,19 @@ order:
128
143
  ```txt
129
144
  migrations/0000_initial.sql
130
145
  migrations/0001_add_mnemocyte_meta.sql
146
+ migrations/0002_add_embedding_model.sql
131
147
  ```
132
148
 
133
149
  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.
150
+ `migrations/0001_add_mnemocyte_meta.sql` and then
151
+ `migrations/0002_add_embedding_model.sql`. Existing 0.2.x or 0.3.x installs
152
+ only need `0002_add_embedding_model.sql`.
136
153
 
137
154
  For a different embedding dimension on a fresh installation, render an explicit
138
155
  initial migration from the template and apply that rendered file instead of the
139
156
  default `0000_initial.sql`. The rendered initial migration includes the
140
- matching `mnemocyte_meta` row.
157
+ matching `mnemocyte_meta` dimensions and the installation-model column, so it
158
+ does not also need `0001` or `0002`.
141
159
 
142
160
  ```bash
143
161
  node node_modules/mnemocyte/migrations/render-initial.mjs --dimensions 768 --out migrations/0000_initial.768.sql
@@ -149,14 +167,41 @@ Inside this repository, the equivalent development shortcut is:
149
167
  pnpm migration:render -- --dimensions 768 --out migrations/0000_initial.768.sql
150
168
  ```
151
169
 
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"`.
170
+ The Postgres backend supports one embedding model and dimension per
171
+ installation. Before writes, recall, or duplicate scans, Mnemocyte reads
172
+ `mnemocyte_meta` and validates both values before calling the embedder or
173
+ comparing vectors. An empty installation atomically records the configured
174
+ model on its first embedding-dependent operation. Migration `0002` records the
175
+ single model already present in historical rows when that value is
176
+ unambiguous.
177
+
178
+ A configured model or dimension mismatch throws `MnemocyteError` code
179
+ `"CONFIG"`. If historical rows contain multiple model values, the installation
180
+ model remains unset and embedding-dependent operations throw `"MIGRATION"`
181
+ until an operator re-embeds or removes the mixed rows and explicitly records
182
+ the intended installation model. Non-embedding recovery operations such as
183
+ `stats`, `forget`, and pruning remain available.
184
+
185
+ Inspect mixed history before repairing it:
186
+
187
+ ```sql
188
+ SELECT embedding_model, count(*)
189
+ FROM mnemocyte_memories
190
+ GROUP BY embedding_model;
191
+ ```
192
+
193
+ Only after every retained row belongs to the intended vector space, record that
194
+ model explicitly:
195
+
196
+ ```sql
197
+ UPDATE mnemocyte_meta
198
+ SET embedding_model = 'your-model-id'
199
+ WHERE key = 'installation';
200
+ ```
157
201
 
158
- Use the embedding dimension documented for your chosen model, render or apply a
159
- matching schema, and keep one dimension per Mnemocyte installation.
202
+ Use the model identifier and embedding dimension documented for your embedder,
203
+ render or apply a matching schema, and keep one vector space per Mnemocyte
204
+ installation.
160
205
 
161
206
  The migration creates the bundled HNSW pgvector index
162
207
  `mnemocyte_memories_embedding_hnsw_idx` for cosine search. HNSW is approximate:
@@ -191,6 +236,44 @@ await client.remember({
191
236
  });
192
237
  ```
193
238
 
239
+ Memory metadata is persisted JSON value data. Use the exported `JsonObject`
240
+ and `JsonValue` types; unsupported values such as `undefined`, functions,
241
+ bigints, non-finite numbers, class instances, or cyclic objects are rejected
242
+ with `MnemocyteError` code `"VALIDATION"`. Mnemocyte deep-clones metadata when
243
+ writing and reading, so later mutations do not change stored state.
244
+
245
+ JavaScript callers receive the same runtime domain checks as TypeScript
246
+ callers: `type` and `importance` must be known values, tags must be an array of
247
+ strings, `source` must be a string when supplied, and `expiresAt` must be a
248
+ valid `Date`. Invalid values reject with `"VALIDATION"` before embedding or
249
+ storage. Mutable tags, metadata, and expiration dates are snapshotted when
250
+ `remember` or `rememberMany` is invoked, so later caller mutations cannot
251
+ change the pending write.
252
+
253
+ When observability is configured, snapshot and validation failures follow the
254
+ same operation lifecycle as provider or storage failures: one `"start"` event
255
+ and one `"error"` event carrying the thrown value. Snapshotting remains
256
+ synchronous before any awaited `onEvent` hook.
257
+
258
+ For batches, cancellation belongs to the whole operation:
259
+
260
+ ```ts
261
+ const abortController = new AbortController();
262
+
263
+ const memories = await client.rememberMany({
264
+ inputs: [
265
+ { entityId: "user_123", content: "Likes concise answers." },
266
+ { entityId: "user_123", content: "Uses TypeScript." },
267
+ ],
268
+ signal: abortController.signal,
269
+ });
270
+ ```
271
+
272
+ The positional `rememberMany(inputs)` form remains as a deprecated pre-v1
273
+ compatibility overload and treats its first item's signal as the batch signal.
274
+ New code should use `{ inputs, signal }`; individual items in the object form do
275
+ not have cancellation signals.
276
+
194
277
  ### `recall`
195
278
 
196
279
  ```ts
@@ -202,6 +285,11 @@ const memories = await client.recall({
202
285
  });
203
286
  ```
204
287
 
288
+ Successful recall results include the access metadata written by that call:
289
+ `accessCount`, `lastAccessedAt`, and `updatedAt` match stored state when the
290
+ promise resolves. Ranking and score explanations use the count from before the
291
+ current recall so the access update cannot change its own ordering.
292
+
205
293
  ### `buildContext`
206
294
 
207
295
  ```ts
@@ -213,6 +301,36 @@ const context = await client.buildContext({
213
301
  });
214
302
  ```
215
303
 
304
+ Plain-text context uses a deterministic `=` fence longer than any `=` run in
305
+ the query, rendered metadata, or included memory content. The resulting
306
+ `MEMORY N START/END` boundaries cannot be reproduced by untrusted content.
307
+ Markdown continues to choose a content-safe backtick fence, and XML continues
308
+ to escape content.
309
+
310
+ When supplied, `maxTokens` must be a positive integer or `buildContext` rejects
311
+ with `"VALIDATION"`. Omitting it keeps the default token-budget path. Returned
312
+ context always fits the supplied counter and budget; when an extremely small
313
+ budget cannot fit the full truncation marker, the result is the longest marker
314
+ fragment that fits or an empty string.
315
+
316
+ ### `forget` and `forgetAll`
317
+
318
+ ```ts
319
+ await client.forget({
320
+ entityId: "user_123",
321
+ memoryId: "mem_old",
322
+ });
323
+
324
+ await client.forgetAll({ entityId: "user_123" });
325
+ ```
326
+
327
+ A memory cannot be deleted while another memory's `supersededBy` still points
328
+ to it as a consolidation survivor. `forget` and `forgetAll` reject with
329
+ `"CONFLICT"` before deleting anything in that case. This also applies when
330
+ `forgetAll` selects both the survivor and its superseded dependent. Delete the
331
+ superseded memories first; deleting a loser, or any memory with no dependents,
332
+ continues to work normally.
333
+
216
334
  ### `prune`
217
335
 
218
336
  ```ts
@@ -228,6 +346,23 @@ await client.prune({
228
346
  });
229
347
  ```
230
348
 
349
+ Prune selectors are validated and normalized before either backend is called.
350
+ Dates must be valid `Date` instances, enum values must be known, arrays must
351
+ contain valid values, and boolean fields must be actual booleans. `false`
352
+ selector flags and empty selector arrays do not count toward the required
353
+ selector; malformed or selector-free input rejects with `"VALIDATION"` without
354
+ deleting anything. Tag selectors are trimmed and deduplicated.
355
+
356
+ A dry run may preview a survivor among its matches. A non-dry-run prune rejects
357
+ with `"CONFLICT"` if any matching memory still has consolidation dependents,
358
+ and the entire prune batch remains unchanged; unrelated matching rows are not
359
+ partially deleted.
360
+
361
+ When audit logging is enabled, every successful non-dry prune writes one
362
+ best-effort `"memory.pruned"` event per affected entity, including global
363
+ prunes without an `entityId`. Dry runs and zero-deletion runs do not write prune
364
+ events.
365
+
231
366
  ### `findDuplicates`
232
367
 
233
368
  ```ts
@@ -238,6 +373,10 @@ const pairs = await client.findDuplicates({
238
373
  });
239
374
  ```
240
375
 
376
+ The in-memory implementation compares memory pairs quadratically. It degrades
377
+ noticeably once an entity grows beyond roughly a few thousand memories, so use
378
+ the Postgres backend for duplicate detection beyond that scale.
379
+
241
380
  ### `listAuditLog`
242
381
 
243
382
  ```ts
@@ -250,8 +389,30 @@ const events = await client.listAuditLog({
250
389
  entityId: "user_123",
251
390
  limit: 100,
252
391
  });
392
+
393
+ const last = events.at(-1);
394
+ const nextPage = last
395
+ ? await client.listAuditLog({
396
+ entityId: "user_123",
397
+ limit: 100,
398
+ beforeCursor: { timestamp: last.timestamp, id: last.id },
399
+ })
400
+ : [];
253
401
  ```
254
402
 
403
+ Audit entries are ordered newest first by timestamp and event ID. The
404
+ experimental composite `beforeCursor` / `afterCursor` fields provide stable
405
+ pagination when multiple events share a timestamp. The existing `before` and
406
+ `after` fields remain strict timestamp filters; they exclude all events at the
407
+ boundary timestamp and should not be used as the sole cursor when complete
408
+ tie-safe pagination is required.
409
+
410
+ Ordinary audit writes are best-effort: an audit storage failure does not fail
411
+ the primary client operation, so `listAuditLog` returns only events that were
412
+ successfully persisted. Postgres consolidation is the exception; its
413
+ `"memory.superseded"` events share the consolidation transaction and commit or
414
+ roll back with it.
415
+
255
416
  ### `experimental.consolidate`
256
417
 
257
418
  ```ts
@@ -262,6 +423,18 @@ await client.experimental.consolidate({
262
423
  });
263
424
  ```
264
425
 
426
+ Consolidation records each loser's `supersededBy` reference to the survivor.
427
+ Retrying the same loser with that same survivor is an idempotent no-op. If any
428
+ requested loser already points to a different survivor, the call rejects with
429
+ `"CONFLICT"`. Mixed batches are atomic: no newly eligible loser is changed, no
430
+ tags are merged, and no consolidation audit event is written when any item
431
+ conflicts. Concurrent calls follow the same target-specific rule in both
432
+ backends.
433
+
434
+ The survivor cannot later be deleted while any such reference remains:
435
+ `forget`, `forgetAll`, and matching non-dry-run `prune` calls reject with
436
+ `"CONFLICT"`. Deleting a superseded loser remains allowed.
437
+
265
438
  The `experimental` namespace may change before v1.0.
266
439
 
267
440
  ## Provider Resilience
@@ -278,6 +451,12 @@ const client = createMnemocyte({
278
451
  });
279
452
  ```
280
453
 
454
+ Provider resilience values are validated synchronously when the client is
455
+ created. Timeouts and delays must be finite non-negative numbers, retries must
456
+ be a non-negative integer, and `shouldRetry` must be a function when supplied;
457
+ invalid values throw `"CONFIG"`. A `maxDelayMs` below `baseDelayMs` remains
458
+ valid and is normalized up to the base delay.
459
+
281
460
  Pass `signal` to cancel an operation:
282
461
 
283
462
  ```ts
@@ -290,6 +469,17 @@ await client.remember({
290
469
  });
291
470
  ```
292
471
 
472
+ Pre-aborted signals reject with `"ABORTED"` before storage work begins.
473
+ In-memory maintenance scans check cancellation cooperatively. For Postgres,
474
+ standalone prune, duplicate-search, and audit-log queries request postgres.js
475
+ query cancellation while they are in flight.
476
+
477
+ Postgres consolidation has a transaction-specific boundary: cancellation is
478
+ checked between mutation steps and immediately before the transaction callback
479
+ returns. If a statement is already in flight, it may finish before the next
480
+ check throws and rolls the transaction back. An abort after the final check,
481
+ including while commit is in flight, may still leave the mutation committed.
482
+
293
483
  Common `MnemocyteError` codes:
294
484
 
295
485
  ```txt
@@ -300,6 +490,7 @@ VALIDATION
300
490
  CONFIG
301
491
  DB
302
492
  NOT_FOUND
493
+ CONFLICT
303
494
  MIGRATION
304
495
  ```
305
496
 
@@ -324,15 +515,21 @@ const client = createMnemocyte({
324
515
  });
325
516
  ```
326
517
 
518
+ Retrieval weights must be finite and non-negative, with a non-zero effective
519
+ total after defaults are applied. `recencyHalfLifeDays` and `accessSaturation`
520
+ must be positive finite numbers, and `candidateMultiplier` must be an integer
521
+ of at least 1. Invalid tuning is rejected at client construction with
522
+ `MnemocyteError` code `"CONFIG"`.
523
+
327
524
  ## Roadmap
328
525
 
329
526
  Planned larger milestones:
330
527
 
331
- - `MemoryStore` abstraction
528
+ - public `MemoryStore` adapter surface
332
529
  - `drizzleStore(db)` for caller-owned Drizzle clients
333
530
  - `@mnemocyte/mcp`
334
531
 
335
- See [ROADMAP.md](./ROADMAP.md).
532
+ See [ROADMAP.md](./docs/ROADMAP.md).
336
533
 
337
534
  ## Pre-v1 Notes
338
535
 
@@ -340,6 +537,9 @@ Current behavior:
340
537
 
341
538
  - `createMnemocyte()` selects either the in-memory backend or the
342
539
  Postgres/pgvector backend.
540
+ - Both backends run through an internal `MemoryStore` boundary and shared
541
+ client orchestration; `MemoryStore` is not exported as a public adapter API
542
+ yet.
343
543
  - Postgres schema setup is explicit; the client does not create tables or
344
544
  indexes for you.
345
545
  - `findDuplicates`, audit-log workflows, and `experimental.consolidate` are
@@ -347,34 +547,99 @@ Current behavior:
347
547
 
348
548
  Known limitations before v1:
349
549
 
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.
550
+ - The internal `MemoryStore` boundary is not a public adapter API yet.
551
+ `drizzleStore(db)` is the planned first public store adapter.
552
+ - Configured provider timeouts abort the per-attempt `AbortSignal`; the
553
+ underlying provider request only stops promptly when the embedder honors that
554
+ signal.
555
+ - Postgres model and dimension metadata are installation-wide. Mixed embedding
556
+ vector spaces in one database are intentionally unsupported.
357
557
 
358
558
  Planned v1 direction:
359
559
 
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.
560
+ - Keep storage adapters behind `MemoryStore` while shared core code owns
561
+ validation, embedding, scoring, observability, resilience, and context
562
+ building.
363
563
  - Keep root `mnemocyte` imports provider-SDK-free.
364
564
  - Keep migrations, dimensions, and production index choices explicit.
365
565
 
366
566
  ## Development
367
567
 
568
+ ### Prerequisites
569
+
570
+ - Node.js 22.18 or newer. CI tests the minimum 22.18 release and Node 24.
571
+ - pnpm 11.1.1, matching the `packageManager` field in `package.json`.
572
+ - Postgres with pgvector only when you want to run the integration suite.
573
+
574
+ Clone the repository and install its locked dependencies:
575
+
576
+ ```bash
577
+ git clone https://github.com/Meenic/mnemocyte.git
578
+ cd mnemocyte
579
+ pnpm install --frozen-lockfile
580
+ ```
581
+
582
+ Mnemocyte is a library, not a standalone server. Use the quick-start example
583
+ from a consuming TypeScript project, or run the repository build in watch mode
584
+ while developing:
585
+
586
+ ```bash
587
+ pnpm dev
588
+ ```
589
+
590
+ Create the publishable ESM artifacts in `dist/` with:
591
+
592
+ ```bash
593
+ pnpm build
594
+ ```
595
+
596
+ ### Validation
597
+
598
+ Run the same main gates used by CI before committing:
599
+
368
600
  ```bash
369
601
  pnpm checktypes
370
602
  pnpm lint
371
603
  pnpm test
372
- pnpm run test:integration
373
604
  pnpm run pack:check
374
605
  ```
375
606
 
376
- `test:integration` skips when `DATABASE_URL` is not set.
607
+ `pnpm test` builds the package, runs unit behavior, verifies package exports,
608
+ and type-checks the public declarations. `pnpm lint` is a read-only formatting,
609
+ lint, and import-order check; use `pnpm lint:fix` to apply its safe fixes. The
610
+ Postgres suite is separate:
611
+
612
+ ```bash
613
+ DATABASE_URL=postgres://... pnpm run test:integration
614
+ ```
615
+
616
+ On PowerShell, set the environment variable for the current process first:
617
+
618
+ ```powershell
619
+ $env:DATABASE_URL = "postgres://..."
620
+ pnpm run test:integration
621
+ ```
622
+
623
+ `test:integration` skips when `DATABASE_URL` is not set. The database must have
624
+ pgvector available; the test applies the bundled migrations.
625
+
626
+ ### Where changes belong
627
+
628
+ - Public exports and types: `src/index.ts`, `src/types.ts`, and `src/errors.ts`.
629
+ - Shared client behavior: `src/memory/client-core.ts`; backend mechanics stay
630
+ in `src/memory/in-memory.ts` or `src/memory/postgres.ts`.
631
+ - Postgres schema and SQL: `src/db/schema.ts` and `src/db/queries/`. Schema
632
+ changes require an explicit file under `migrations/`.
633
+ - Provider helpers: `src/embedders/`; keep the root package provider-SDK-free.
634
+ - Retrieval and context rendering: `src/retrieval/` and `src/context/`.
635
+ - Tests: mirror the affected responsibility under `test/`; package boundary
636
+ checks live under `test/package/`, and real Postgres coverage under
637
+ `test/integration/`.
638
+
639
+ See [AGENTS.md](./AGENTS.md) for repository rules and the validation/release
640
+ policy. Read [ARCHITECTURE.md](./docs/ARCHITECTURE.md) before changing module
641
+ boundaries or the public surface.
377
642
 
378
643
  ## Architecture
379
644
 
380
- See [ARCHITECTURE.md](./ARCHITECTURE.md).
645
+ See [ARCHITECTURE.md](./docs/ARCHITECTURE.md).
@@ -1,4 +1,4 @@
1
- import { c as Embedder } from "../types-DXEGcc47.mjs";
1
+ import { l as Embedder } from "../types-DH0inEay.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-BG3-mNur.mjs";
1
+ import { t as MnemocyteError } from "../errors-B_7ZdjEO.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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"}
@@ -38,8 +38,9 @@ var MnemocyteError = class extends Error {
38
38
  /**
39
39
  * Type guard for {@link MnemocyteError}.
40
40
  *
41
- * Prefer this over `instanceof` when working across module boundaries or
42
- * bundlers where multiple copies of the class may exist.
41
+ * This guard uses `instanceof` and recognizes errors created by the same loaded
42
+ * Mnemocyte class. It does not recognize structurally similar errors or errors
43
+ * created by another installed copy of the package.
43
44
  *
44
45
  * @param error - Value to test (typically a caught `unknown`).
45
46
  * @returns `true` if `error` is a {@link MnemocyteError}.
@@ -50,4 +51,4 @@ function isMnemocyteError(error) {
50
51
  //#endregion
51
52
  export { isMnemocyteError as n, MnemocyteError as t };
52
53
 
53
- //# sourceMappingURL=errors-BG3-mNur.mjs.map
54
+ //# sourceMappingURL=errors-B_7ZdjEO.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors-B_7ZdjEO.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"}