@amemhq/core 1.1.0 → 2.1.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/dist/index.d.cts CHANGED
@@ -6,15 +6,37 @@ declare function configure(opts: {
6
6
  * embedding.ts — Local ONNX embedding via @huggingface/transformers
7
7
  * Matches Python: SentenceTransformer.encode(text, normalize_embeddings=True)
8
8
  *
9
- * The model is selectable because the default is not a good retrieval model: it
10
- * caps at 128 tokens, so anything longer is truncated before it reaches the
11
- * vector. Changing it is a breaking change whenever the dimension differs —
12
- * Qdrant fixes a collection's vector size at creation so the default stays put
13
- * and the switch is opt-in. See docs/reference/embedding-models.md.
14
- */
15
- /** The model shipped since the beginning. Not changed here on purpose. */
16
- declare const DEFAULT_EMBEDDING_MODEL = "Xenova/paraphrase-multilingual-MiniLM-L12-v2";
17
- /** Which model this process embeds with. */
9
+ * The model is selectable, and changing it is a breaking change whenever the
10
+ * dimension differs Qdrant fixes a collection's vector size at creation. So
11
+ * nothing here decides on its own: a collection that already exists keeps the
12
+ * model it was built with (see `pinEmbeddingModel`), and moving is a deliberate
13
+ * `amem-migrate` run. See docs/reference/embedding-models.md.
14
+ */
15
+ /**
16
+ * What a fresh install embeds with.
17
+ *
18
+ * Chosen for having no architectural question mark rather than for topping a
19
+ * leaderboard: XLM-RoBERTa, natively supported by Transformers.js, its ONNX
20
+ * maintained by that library's own author. 8192 tokens against the 128 of the
21
+ * model this replaced, which is the whole reason for the change — anything longer
22
+ * than a couple of sentences was being truncated before it reached the vector.
23
+ */
24
+ declare const DEFAULT_EMBEDDING_MODEL = "Xenova/bge-m3";
25
+ /**
26
+ * The default before 2.0.0. Every store built by an earlier version holds its
27
+ * vectors, and nothing recorded that at the time — `LEGACY_DEFAULT_DIM` is how a
28
+ * collection from back then is recognised.
29
+ */
30
+ declare const LEGACY_DEFAULT_EMBEDDING_MODEL = "Xenova/paraphrase-multilingual-MiniLM-L12-v2";
31
+ declare const LEGACY_DEFAULT_DIM = 384;
32
+ /**
33
+ * Which model this process embeds with: the env var, else whatever the open
34
+ * collection was built with, else the shipped default.
35
+ *
36
+ * An explicit `AMEM_EMBED_MODEL` outranks the pin on purpose — someone who set it
37
+ * is migrating deliberately, and silently overriding them with the collection's
38
+ * old model would make the setting look broken.
39
+ */
18
40
  declare function getEmbeddingModel(): string;
19
41
  /** How token embeddings are collapsed into one sentence vector. */
20
42
  type PoolingMode = 'mean' | 'cls';
@@ -48,6 +70,14 @@ declare function getEmbeddingDevice(): string | undefined;
48
70
  * Weight precision. Unset means Transformers.js picks, which on Node is `fp32` —
49
71
  * the largest download of every variant a model publishes.
50
72
  *
73
+ * 2.0.0 defaulted this to `fp16` for bge-m3, to halve a 2.27 GB download. That
74
+ * shipped broken: `onnxruntime-node` 1.24.3 aborts loading those weights in
75
+ * `SimplifiedLayerNormFusion`, on a node the fp16 conversion inserts, so the
76
+ * default install could not embed at all. Reproduced on bge-m3 and on
77
+ * gte-multilingual-base, on two machines — it is the runtime and fp16, not one
78
+ * model. Nothing here picks a dtype now; saving the download is not worth
79
+ * choosing a build we have not loaded.
80
+ *
51
81
  * Passed through rather than validated against a list: Transformers.js already
52
82
  * rejects an unknown value and names the valid ones, and a list here would go
53
83
  * stale the moment it gains a quantization.
@@ -63,8 +93,9 @@ declare function getEmbeddingDtype(): string | undefined;
63
93
  */
64
94
  declare function getEmbeddingDim(): Promise<number>;
65
95
  /**
66
- * Encode text to a normalized embedding vector. The width is the model's — 384
67
- * for the default, 1024 for bge-m3 — so nothing here should assume a number.
96
+ * Encode text to a normalized embedding vector. The width is the model's — 1024
97
+ * for the default, 384 for the one before it — so nothing here should assume a
98
+ * number.
68
99
  * Singleton model, loaded once and reused.
69
100
  */
70
101
  declare function encode(text: string): Promise<number[]>;
@@ -83,7 +114,8 @@ declare function isModelLoaded(): boolean;
83
114
  /**
84
115
  * storage.ts — Qdrant vector storage for A-MEM
85
116
  * Uses native fetch (Node 18+) to avoid undici compatibility issues with Node v26
86
- * Collection: amem_notes, 384-dim cosine, with agent_id isolation
117
+ * Collection: amem_notes, cosine, width set by the embedding model, with
118
+ * agent_id isolation
87
119
  */
88
120
  /** Per-agent override config. If collection is set, mode B (isolated collection) is used. */
89
121
  interface AgentAmemConfig {
@@ -190,6 +222,21 @@ declare class EmbeddingModelMismatchError extends Error {
190
222
  readonly configuredModel: string;
191
223
  constructor(collection: string, collectionModel: string, configuredModel: string);
192
224
  }
225
+ /**
226
+ * Two collections open in one process that need two different models.
227
+ *
228
+ * Only reachable in mode B, and normally only mid-migration: per-agent
229
+ * collections built before 2.0.0 all resolve to the same old model, until one of
230
+ * them is migrated and the others are not. One process embeds with one model, so
231
+ * this has to stop rather than pick a winner — picking would write vectors of the
232
+ * wrong width into whichever collection lost.
233
+ */
234
+ declare class MixedEmbeddingModelsError extends Error {
235
+ readonly collection: string;
236
+ readonly wanted: string;
237
+ readonly inUse: string;
238
+ constructor(collection: string, wanted: string, inUse: string);
239
+ }
193
240
  /**
194
241
  * Ask Qdrant whether it can serve, right now.
195
242
  *
@@ -215,28 +262,34 @@ declare function makeCrud(collectionName: string, modeBIsolated?: boolean): {
215
262
  addNote(note: MemoryNote): Promise<void>;
216
263
  /**
217
264
  * Story 36: this is the one read that bypasses the agent filter — it fetches
218
- * straight by UUID. Pass `readerAgentId` to enforce `readers`; an unreadable
219
- * note comes back as `null` (indistinguishable from missing, so nothing leaks,
220
- * and callers already handle null). Omitting it skips the check, preserving
221
- * behaviour for internal callers that only ever hold their own ids.
265
+ * straight by UUID. An unreadable note comes back as `null`, indistinguishable
266
+ * from missing, so nothing leaks and callers already handle it.
267
+ *
268
+ * `reader` is required. It used to be optional, and omitting it skipped the
269
+ * check — which meant the safe behaviour was the one you had to remember to
270
+ * ask for. Pass `SYSTEM_ACTOR` to read as the engine itself; that reads as a
271
+ * deliberate act at the call site, where an absent argument did not.
222
272
  */
223
- getNote(id: string, readerAgentId?: string): Promise<MemoryNote | null>;
273
+ getNote(id: string, reader: string): Promise<MemoryNote | null>;
224
274
  updateNote(note: MemoryNote): Promise<void>;
225
275
  findByHash(hash: string, agentId: string): Promise<MemoryNote | null>;
226
276
  /**
227
- * Story 33: pass `callerAgentId` to enforce the writers policy. Callers that
228
- * hold the note already should prefer checking `canWrite` themselves; this
229
- * fetch-then-check path exists for callers that only have an id (the plugin's
230
- * CRUD hook). Returns false without writing when the caller may not write.
231
- * Omitting `callerAgentId` skips the check, preserving existing behaviour for
232
- * internal callers that are already scoped to their own notes.
277
+ * Story 33: enforces the writers policy. Returns false — without writing —
278
+ * when the caller may not write. This fetch-then-check path exists for callers
279
+ * that only have an id (the plugin's CRUD hook); callers already holding the
280
+ * note can check `canWrite` themselves and skip a round trip.
281
+ *
282
+ * `caller` is required. Pass `SYSTEM_ACTOR` for the engine's own maintenance
283
+ * writes. The self-read below is a genuine `SYSTEM_ACTOR` case: it fetches the
284
+ * note in order to decide whether the caller may write it, and gating that
285
+ * fetch on the same policy it exists to evaluate would be circular.
233
286
  */
234
- updateNoteContent(id: string, content: string, embedding: number[], hash: string, callerAgentId?: string): Promise<boolean>;
287
+ updateNoteContent(id: string, content: string, embedding: number[], hash: string, caller: string): Promise<boolean>;
235
288
  queryByEmbedding(embedding: number[], topK: number, agentId: string, scoreThreshold?: number, subject?: string): Promise<QueryResult[]>;
236
289
  listNotes(agentId?: string, subject?: string): Promise<MemoryNote[]>;
237
290
  deleteNote(id: string): Promise<void>;
238
291
  /** Story 33: see `updateNoteContent` — returns false, unwritten, when denied. */
239
- invalidateNote(id: string, callerAgentId?: string): Promise<boolean>;
292
+ invalidateNote(id: string, caller: string): Promise<boolean>;
240
293
  getNotesByDatePrefix(datePrefix: string, agentId: string): Promise<MemoryNote[]>;
241
294
  countNotes(agentId?: string): Promise<number>;
242
295
  updateNoteLinks(id: string, links: string[]): Promise<void>;
@@ -250,11 +303,11 @@ type StorageContext = ReturnType<typeof makeCrud>;
250
303
  * Mode B (isolated collection): pass collectionName = 'amem_notes_<agentId>' and modeBIsolated = true.
251
304
  */
252
305
  declare function createStorageContext(collectionName?: string, modeBIsolated?: boolean): StorageContext;
253
- declare function getNote(id: string, readerAgentId?: string): Promise<MemoryNote | null>;
306
+ declare function getNote(id: string, reader: string): Promise<MemoryNote | null>;
254
307
  declare function updateNote(note: MemoryNote): Promise<void>;
255
308
  declare function listNotes(agentId?: string, subject?: string): Promise<MemoryNote[]>;
256
309
  declare function deleteNote(id: string): Promise<void>;
257
- declare function invalidateNote(id: string, callerAgentId?: string): Promise<boolean>;
310
+ declare function invalidateNote(id: string, caller: string): Promise<boolean>;
258
311
  declare function patchNotePayload(id: string, fields: Record<string, unknown>): Promise<void>;
259
312
 
260
313
  /**
@@ -304,8 +357,28 @@ interface SearchResult {
304
357
  keywords: string[];
305
358
  links: string[];
306
359
  timestamp: string;
360
+ /**
361
+ * Cosine similarity to the query. **Not** what ordered this list — that is
362
+ * `rrf`, which fuses the dense and BM25 rankings and then applies a heat/recency
363
+ * boost. The two disagree often, and a consumer that reads `similarity` as the
364
+ * ranking score concludes the ranking is broken.
365
+ */
307
366
  similarity: number;
367
+ /**
368
+ * The fused score the matches are sorted by, and 0 for anything no retriever
369
+ * ranked. It is `via`, not this, that says why a row is here.
370
+ */
308
371
  rrf: number;
372
+ /**
373
+ * Why this note is in the results.
374
+ *
375
+ * `match` — it was retrieved for the query and ranked by `rrf`.
376
+ * `link` — it was **not** retrieved; it is here because it links to one that
377
+ * was, within two hops and above the relevance gate. These are appended in
378
+ * discovery order after the matches and have no `rrf` of their own, so reading
379
+ * the tail of the list as "lower-ranked matches" is wrong.
380
+ */
381
+ via: 'match' | 'link';
309
382
  topics: string[];
310
383
  note_type: 'memory' | 'knowledge';
311
384
  }
@@ -411,6 +484,28 @@ declare function generateReviewBatch(agentId: string, outputPath?: string): Prom
411
484
  * policy is unit-testable on its own and identical everywhere it is applied.
412
485
  */
413
486
 
487
+ /**
488
+ * The engine acting as itself, rather than on behalf of any agent.
489
+ *
490
+ * `getNote`, `updateNoteContent` and `invalidateNote` all used to take an
491
+ * *optional* identity, and omitting it skipped the authorization check. That put
492
+ * the safe behaviour behind remembering to ask for it, and made "no check here"
493
+ * invisible — an absent argument looks the same as an oversight. The identity is
494
+ * now required, and this is what a call declares when it genuinely has no agent
495
+ * on whose behalf it acts.
496
+ *
497
+ * Two call sites use it, both inside storage.ts: the fetch that
498
+ * `updateNoteContent` and `invalidateNote` perform in order to *evaluate* the
499
+ * write policy. Gating that read on the policy it exists to check would be
500
+ * circular. Everything else passes a real agent id — the audit that prompted this
501
+ * found the identity was already in scope at every one of them.
502
+ *
503
+ * The prefix keeps it from colliding with any plausible agent id. It is not a
504
+ * secret and does not need to be: amem is self-hosted, the operator owns every
505
+ * memory in the store, and there is no privilege boundary here to defend. This
506
+ * guards against a call site forgetting to pass an identity, not against a user.
507
+ */
508
+ declare const SYSTEM_ACTOR = "__amem_system__";
414
509
  /**
415
510
  * May `callerAgentId` mutate `note`?
416
511
  *
@@ -440,8 +535,10 @@ interface MigrateResult {
440
535
  missingDerived: number;
441
536
  /** Notes whose fields were re-extracted. 0 unless refreshFields. */
442
537
  refreshed: number;
443
- /** Notes written into the target. 0 on a dry run. */
538
+ /** Notes written into the target by THIS run. 0 on a dry run. */
444
539
  migrated: number;
540
+ /** Notes a previous interrupted run had already written. */
541
+ skipped: number;
445
542
  sourceDim: number | null;
446
543
  targetDim: number;
447
544
  model: string;
@@ -461,6 +558,47 @@ declare function migrateCollection(opts: {
461
558
  warn: (m: string) => void;
462
559
  };
463
560
  }): Promise<MigrateResult>;
561
+ /**
562
+ * Put the migrated collection behind the name the source used, and drop the
563
+ * source.
564
+ *
565
+ * This is the only irreversible step in the whole migration, which is why it is
566
+ * a separate call rather than the tail of `migrateCollection`. Everything before
567
+ * it leaves the original untouched and can simply be abandoned.
568
+ *
569
+ * Qdrant cannot rename a collection and cannot create an alias over a name a real
570
+ * collection holds (409), so freeing the name means deleting it — after checking
571
+ * the target holds at least as much as the source, because that check is the last
572
+ * thing standing between a half-finished migration and a deleted store.
573
+ */
574
+ declare function switchToMigrated(opts: {
575
+ /** The name readers are configured with. Becomes an alias. */
576
+ name: string;
577
+ /** The collection built by `migrateCollection`. */
578
+ to: string;
579
+ /**
580
+ * Snapshot the source before dropping it. Default true.
581
+ *
582
+ * Freeing the name means deleting the collection, but it does not have to mean
583
+ * losing the data, and those should not be the same decision. The snapshot is
584
+ * what makes "switch over" reversible and leaves "throw the old vectors away"
585
+ * as a separate thing to do later, by hand, once the new store has proven
586
+ * itself in use.
587
+ */
588
+ snapshot?: boolean;
589
+ logger?: {
590
+ info: (m: string) => void;
591
+ warn: (m: string) => void;
592
+ };
593
+ }): Promise<{
594
+ name: string;
595
+ to: string;
596
+ moved: number;
597
+ snapshot?: {
598
+ name: string;
599
+ size: number;
600
+ };
601
+ }>;
464
602
 
465
603
  /**
466
604
  * Similarity floor for accepting an UPDATE target.
@@ -549,4 +687,4 @@ declare function llmCrudDecision(userText: string, assistantText: string, existi
549
687
  content: string;
550
688
  }>): Promise<MemoryOperation[]>;
551
689
 
552
- export { type AgentAmemConfig, type AmemPluginConfig, type ConflictMode, type ConflictSweepResult, DEFAULT_CRUD_UPDATE_MIN_SIM, DEFAULT_EMBEDDING_MODEL, EmbeddingDimensionMismatchError, EmbeddingModelMismatchError, type EvolutionEntry, type LlmConfig, type LowQualityItem, type LowQualityReason, type MemoryNote, type MemoryOperation, type MigrateResult, type PoolingMode, type QueryResult, type SearchResult, type StorageContext, addEpisodic, addMemory, canRead, canWrite, checkQuality, configure, configureLlm, conflictSweep, consolidateMemories, createStorageContext, deleteNote, encode, ensureCollection, generateReviewBatch, getEmbeddingDevice, getEmbeddingDim, getEmbeddingDtype, getEmbeddingModel, getEmbeddingPooling, getNote, invalidateNote, isModelLoaded, isPlausibleUpdateTarget, listMemories, listNotes, llmCrudDecision, loadModel, mergeSimilarNotes, migrateCollection, patchNotePayload, pingQdrant, resolveCrudUpdateMinSim, scanLowQuality, searchMemory, updateNote };
690
+ export { type AgentAmemConfig, type AmemPluginConfig, type ConflictMode, type ConflictSweepResult, DEFAULT_CRUD_UPDATE_MIN_SIM, DEFAULT_EMBEDDING_MODEL, EmbeddingDimensionMismatchError, EmbeddingModelMismatchError, type EvolutionEntry, LEGACY_DEFAULT_DIM, LEGACY_DEFAULT_EMBEDDING_MODEL, type LlmConfig, type LowQualityItem, type LowQualityReason, type MemoryNote, type MemoryOperation, type MigrateResult, MixedEmbeddingModelsError, type PoolingMode, type QueryResult, SYSTEM_ACTOR, type SearchResult, type StorageContext, addEpisodic, addMemory, canRead, canWrite, checkQuality, configure, configureLlm, conflictSweep, consolidateMemories, createStorageContext, deleteNote, encode, ensureCollection, generateReviewBatch, getEmbeddingDevice, getEmbeddingDim, getEmbeddingDtype, getEmbeddingModel, getEmbeddingPooling, getNote, invalidateNote, isModelLoaded, isPlausibleUpdateTarget, listMemories, listNotes, llmCrudDecision, loadModel, mergeSimilarNotes, migrateCollection, patchNotePayload, pingQdrant, resolveCrudUpdateMinSim, scanLowQuality, searchMemory, switchToMigrated, updateNote };
package/dist/index.d.ts CHANGED
@@ -6,15 +6,37 @@ declare function configure(opts: {
6
6
  * embedding.ts — Local ONNX embedding via @huggingface/transformers
7
7
  * Matches Python: SentenceTransformer.encode(text, normalize_embeddings=True)
8
8
  *
9
- * The model is selectable because the default is not a good retrieval model: it
10
- * caps at 128 tokens, so anything longer is truncated before it reaches the
11
- * vector. Changing it is a breaking change whenever the dimension differs —
12
- * Qdrant fixes a collection's vector size at creation so the default stays put
13
- * and the switch is opt-in. See docs/reference/embedding-models.md.
14
- */
15
- /** The model shipped since the beginning. Not changed here on purpose. */
16
- declare const DEFAULT_EMBEDDING_MODEL = "Xenova/paraphrase-multilingual-MiniLM-L12-v2";
17
- /** Which model this process embeds with. */
9
+ * The model is selectable, and changing it is a breaking change whenever the
10
+ * dimension differs Qdrant fixes a collection's vector size at creation. So
11
+ * nothing here decides on its own: a collection that already exists keeps the
12
+ * model it was built with (see `pinEmbeddingModel`), and moving is a deliberate
13
+ * `amem-migrate` run. See docs/reference/embedding-models.md.
14
+ */
15
+ /**
16
+ * What a fresh install embeds with.
17
+ *
18
+ * Chosen for having no architectural question mark rather than for topping a
19
+ * leaderboard: XLM-RoBERTa, natively supported by Transformers.js, its ONNX
20
+ * maintained by that library's own author. 8192 tokens against the 128 of the
21
+ * model this replaced, which is the whole reason for the change — anything longer
22
+ * than a couple of sentences was being truncated before it reached the vector.
23
+ */
24
+ declare const DEFAULT_EMBEDDING_MODEL = "Xenova/bge-m3";
25
+ /**
26
+ * The default before 2.0.0. Every store built by an earlier version holds its
27
+ * vectors, and nothing recorded that at the time — `LEGACY_DEFAULT_DIM` is how a
28
+ * collection from back then is recognised.
29
+ */
30
+ declare const LEGACY_DEFAULT_EMBEDDING_MODEL = "Xenova/paraphrase-multilingual-MiniLM-L12-v2";
31
+ declare const LEGACY_DEFAULT_DIM = 384;
32
+ /**
33
+ * Which model this process embeds with: the env var, else whatever the open
34
+ * collection was built with, else the shipped default.
35
+ *
36
+ * An explicit `AMEM_EMBED_MODEL` outranks the pin on purpose — someone who set it
37
+ * is migrating deliberately, and silently overriding them with the collection's
38
+ * old model would make the setting look broken.
39
+ */
18
40
  declare function getEmbeddingModel(): string;
19
41
  /** How token embeddings are collapsed into one sentence vector. */
20
42
  type PoolingMode = 'mean' | 'cls';
@@ -48,6 +70,14 @@ declare function getEmbeddingDevice(): string | undefined;
48
70
  * Weight precision. Unset means Transformers.js picks, which on Node is `fp32` —
49
71
  * the largest download of every variant a model publishes.
50
72
  *
73
+ * 2.0.0 defaulted this to `fp16` for bge-m3, to halve a 2.27 GB download. That
74
+ * shipped broken: `onnxruntime-node` 1.24.3 aborts loading those weights in
75
+ * `SimplifiedLayerNormFusion`, on a node the fp16 conversion inserts, so the
76
+ * default install could not embed at all. Reproduced on bge-m3 and on
77
+ * gte-multilingual-base, on two machines — it is the runtime and fp16, not one
78
+ * model. Nothing here picks a dtype now; saving the download is not worth
79
+ * choosing a build we have not loaded.
80
+ *
51
81
  * Passed through rather than validated against a list: Transformers.js already
52
82
  * rejects an unknown value and names the valid ones, and a list here would go
53
83
  * stale the moment it gains a quantization.
@@ -63,8 +93,9 @@ declare function getEmbeddingDtype(): string | undefined;
63
93
  */
64
94
  declare function getEmbeddingDim(): Promise<number>;
65
95
  /**
66
- * Encode text to a normalized embedding vector. The width is the model's — 384
67
- * for the default, 1024 for bge-m3 — so nothing here should assume a number.
96
+ * Encode text to a normalized embedding vector. The width is the model's — 1024
97
+ * for the default, 384 for the one before it — so nothing here should assume a
98
+ * number.
68
99
  * Singleton model, loaded once and reused.
69
100
  */
70
101
  declare function encode(text: string): Promise<number[]>;
@@ -83,7 +114,8 @@ declare function isModelLoaded(): boolean;
83
114
  /**
84
115
  * storage.ts — Qdrant vector storage for A-MEM
85
116
  * Uses native fetch (Node 18+) to avoid undici compatibility issues with Node v26
86
- * Collection: amem_notes, 384-dim cosine, with agent_id isolation
117
+ * Collection: amem_notes, cosine, width set by the embedding model, with
118
+ * agent_id isolation
87
119
  */
88
120
  /** Per-agent override config. If collection is set, mode B (isolated collection) is used. */
89
121
  interface AgentAmemConfig {
@@ -190,6 +222,21 @@ declare class EmbeddingModelMismatchError extends Error {
190
222
  readonly configuredModel: string;
191
223
  constructor(collection: string, collectionModel: string, configuredModel: string);
192
224
  }
225
+ /**
226
+ * Two collections open in one process that need two different models.
227
+ *
228
+ * Only reachable in mode B, and normally only mid-migration: per-agent
229
+ * collections built before 2.0.0 all resolve to the same old model, until one of
230
+ * them is migrated and the others are not. One process embeds with one model, so
231
+ * this has to stop rather than pick a winner — picking would write vectors of the
232
+ * wrong width into whichever collection lost.
233
+ */
234
+ declare class MixedEmbeddingModelsError extends Error {
235
+ readonly collection: string;
236
+ readonly wanted: string;
237
+ readonly inUse: string;
238
+ constructor(collection: string, wanted: string, inUse: string);
239
+ }
193
240
  /**
194
241
  * Ask Qdrant whether it can serve, right now.
195
242
  *
@@ -215,28 +262,34 @@ declare function makeCrud(collectionName: string, modeBIsolated?: boolean): {
215
262
  addNote(note: MemoryNote): Promise<void>;
216
263
  /**
217
264
  * Story 36: this is the one read that bypasses the agent filter — it fetches
218
- * straight by UUID. Pass `readerAgentId` to enforce `readers`; an unreadable
219
- * note comes back as `null` (indistinguishable from missing, so nothing leaks,
220
- * and callers already handle null). Omitting it skips the check, preserving
221
- * behaviour for internal callers that only ever hold their own ids.
265
+ * straight by UUID. An unreadable note comes back as `null`, indistinguishable
266
+ * from missing, so nothing leaks and callers already handle it.
267
+ *
268
+ * `reader` is required. It used to be optional, and omitting it skipped the
269
+ * check — which meant the safe behaviour was the one you had to remember to
270
+ * ask for. Pass `SYSTEM_ACTOR` to read as the engine itself; that reads as a
271
+ * deliberate act at the call site, where an absent argument did not.
222
272
  */
223
- getNote(id: string, readerAgentId?: string): Promise<MemoryNote | null>;
273
+ getNote(id: string, reader: string): Promise<MemoryNote | null>;
224
274
  updateNote(note: MemoryNote): Promise<void>;
225
275
  findByHash(hash: string, agentId: string): Promise<MemoryNote | null>;
226
276
  /**
227
- * Story 33: pass `callerAgentId` to enforce the writers policy. Callers that
228
- * hold the note already should prefer checking `canWrite` themselves; this
229
- * fetch-then-check path exists for callers that only have an id (the plugin's
230
- * CRUD hook). Returns false without writing when the caller may not write.
231
- * Omitting `callerAgentId` skips the check, preserving existing behaviour for
232
- * internal callers that are already scoped to their own notes.
277
+ * Story 33: enforces the writers policy. Returns false — without writing —
278
+ * when the caller may not write. This fetch-then-check path exists for callers
279
+ * that only have an id (the plugin's CRUD hook); callers already holding the
280
+ * note can check `canWrite` themselves and skip a round trip.
281
+ *
282
+ * `caller` is required. Pass `SYSTEM_ACTOR` for the engine's own maintenance
283
+ * writes. The self-read below is a genuine `SYSTEM_ACTOR` case: it fetches the
284
+ * note in order to decide whether the caller may write it, and gating that
285
+ * fetch on the same policy it exists to evaluate would be circular.
233
286
  */
234
- updateNoteContent(id: string, content: string, embedding: number[], hash: string, callerAgentId?: string): Promise<boolean>;
287
+ updateNoteContent(id: string, content: string, embedding: number[], hash: string, caller: string): Promise<boolean>;
235
288
  queryByEmbedding(embedding: number[], topK: number, agentId: string, scoreThreshold?: number, subject?: string): Promise<QueryResult[]>;
236
289
  listNotes(agentId?: string, subject?: string): Promise<MemoryNote[]>;
237
290
  deleteNote(id: string): Promise<void>;
238
291
  /** Story 33: see `updateNoteContent` — returns false, unwritten, when denied. */
239
- invalidateNote(id: string, callerAgentId?: string): Promise<boolean>;
292
+ invalidateNote(id: string, caller: string): Promise<boolean>;
240
293
  getNotesByDatePrefix(datePrefix: string, agentId: string): Promise<MemoryNote[]>;
241
294
  countNotes(agentId?: string): Promise<number>;
242
295
  updateNoteLinks(id: string, links: string[]): Promise<void>;
@@ -250,11 +303,11 @@ type StorageContext = ReturnType<typeof makeCrud>;
250
303
  * Mode B (isolated collection): pass collectionName = 'amem_notes_<agentId>' and modeBIsolated = true.
251
304
  */
252
305
  declare function createStorageContext(collectionName?: string, modeBIsolated?: boolean): StorageContext;
253
- declare function getNote(id: string, readerAgentId?: string): Promise<MemoryNote | null>;
306
+ declare function getNote(id: string, reader: string): Promise<MemoryNote | null>;
254
307
  declare function updateNote(note: MemoryNote): Promise<void>;
255
308
  declare function listNotes(agentId?: string, subject?: string): Promise<MemoryNote[]>;
256
309
  declare function deleteNote(id: string): Promise<void>;
257
- declare function invalidateNote(id: string, callerAgentId?: string): Promise<boolean>;
310
+ declare function invalidateNote(id: string, caller: string): Promise<boolean>;
258
311
  declare function patchNotePayload(id: string, fields: Record<string, unknown>): Promise<void>;
259
312
 
260
313
  /**
@@ -304,8 +357,28 @@ interface SearchResult {
304
357
  keywords: string[];
305
358
  links: string[];
306
359
  timestamp: string;
360
+ /**
361
+ * Cosine similarity to the query. **Not** what ordered this list — that is
362
+ * `rrf`, which fuses the dense and BM25 rankings and then applies a heat/recency
363
+ * boost. The two disagree often, and a consumer that reads `similarity` as the
364
+ * ranking score concludes the ranking is broken.
365
+ */
307
366
  similarity: number;
367
+ /**
368
+ * The fused score the matches are sorted by, and 0 for anything no retriever
369
+ * ranked. It is `via`, not this, that says why a row is here.
370
+ */
308
371
  rrf: number;
372
+ /**
373
+ * Why this note is in the results.
374
+ *
375
+ * `match` — it was retrieved for the query and ranked by `rrf`.
376
+ * `link` — it was **not** retrieved; it is here because it links to one that
377
+ * was, within two hops and above the relevance gate. These are appended in
378
+ * discovery order after the matches and have no `rrf` of their own, so reading
379
+ * the tail of the list as "lower-ranked matches" is wrong.
380
+ */
381
+ via: 'match' | 'link';
309
382
  topics: string[];
310
383
  note_type: 'memory' | 'knowledge';
311
384
  }
@@ -411,6 +484,28 @@ declare function generateReviewBatch(agentId: string, outputPath?: string): Prom
411
484
  * policy is unit-testable on its own and identical everywhere it is applied.
412
485
  */
413
486
 
487
+ /**
488
+ * The engine acting as itself, rather than on behalf of any agent.
489
+ *
490
+ * `getNote`, `updateNoteContent` and `invalidateNote` all used to take an
491
+ * *optional* identity, and omitting it skipped the authorization check. That put
492
+ * the safe behaviour behind remembering to ask for it, and made "no check here"
493
+ * invisible — an absent argument looks the same as an oversight. The identity is
494
+ * now required, and this is what a call declares when it genuinely has no agent
495
+ * on whose behalf it acts.
496
+ *
497
+ * Two call sites use it, both inside storage.ts: the fetch that
498
+ * `updateNoteContent` and `invalidateNote` perform in order to *evaluate* the
499
+ * write policy. Gating that read on the policy it exists to check would be
500
+ * circular. Everything else passes a real agent id — the audit that prompted this
501
+ * found the identity was already in scope at every one of them.
502
+ *
503
+ * The prefix keeps it from colliding with any plausible agent id. It is not a
504
+ * secret and does not need to be: amem is self-hosted, the operator owns every
505
+ * memory in the store, and there is no privilege boundary here to defend. This
506
+ * guards against a call site forgetting to pass an identity, not against a user.
507
+ */
508
+ declare const SYSTEM_ACTOR = "__amem_system__";
414
509
  /**
415
510
  * May `callerAgentId` mutate `note`?
416
511
  *
@@ -440,8 +535,10 @@ interface MigrateResult {
440
535
  missingDerived: number;
441
536
  /** Notes whose fields were re-extracted. 0 unless refreshFields. */
442
537
  refreshed: number;
443
- /** Notes written into the target. 0 on a dry run. */
538
+ /** Notes written into the target by THIS run. 0 on a dry run. */
444
539
  migrated: number;
540
+ /** Notes a previous interrupted run had already written. */
541
+ skipped: number;
445
542
  sourceDim: number | null;
446
543
  targetDim: number;
447
544
  model: string;
@@ -461,6 +558,47 @@ declare function migrateCollection(opts: {
461
558
  warn: (m: string) => void;
462
559
  };
463
560
  }): Promise<MigrateResult>;
561
+ /**
562
+ * Put the migrated collection behind the name the source used, and drop the
563
+ * source.
564
+ *
565
+ * This is the only irreversible step in the whole migration, which is why it is
566
+ * a separate call rather than the tail of `migrateCollection`. Everything before
567
+ * it leaves the original untouched and can simply be abandoned.
568
+ *
569
+ * Qdrant cannot rename a collection and cannot create an alias over a name a real
570
+ * collection holds (409), so freeing the name means deleting it — after checking
571
+ * the target holds at least as much as the source, because that check is the last
572
+ * thing standing between a half-finished migration and a deleted store.
573
+ */
574
+ declare function switchToMigrated(opts: {
575
+ /** The name readers are configured with. Becomes an alias. */
576
+ name: string;
577
+ /** The collection built by `migrateCollection`. */
578
+ to: string;
579
+ /**
580
+ * Snapshot the source before dropping it. Default true.
581
+ *
582
+ * Freeing the name means deleting the collection, but it does not have to mean
583
+ * losing the data, and those should not be the same decision. The snapshot is
584
+ * what makes "switch over" reversible and leaves "throw the old vectors away"
585
+ * as a separate thing to do later, by hand, once the new store has proven
586
+ * itself in use.
587
+ */
588
+ snapshot?: boolean;
589
+ logger?: {
590
+ info: (m: string) => void;
591
+ warn: (m: string) => void;
592
+ };
593
+ }): Promise<{
594
+ name: string;
595
+ to: string;
596
+ moved: number;
597
+ snapshot?: {
598
+ name: string;
599
+ size: number;
600
+ };
601
+ }>;
464
602
 
465
603
  /**
466
604
  * Similarity floor for accepting an UPDATE target.
@@ -549,4 +687,4 @@ declare function llmCrudDecision(userText: string, assistantText: string, existi
549
687
  content: string;
550
688
  }>): Promise<MemoryOperation[]>;
551
689
 
552
- export { type AgentAmemConfig, type AmemPluginConfig, type ConflictMode, type ConflictSweepResult, DEFAULT_CRUD_UPDATE_MIN_SIM, DEFAULT_EMBEDDING_MODEL, EmbeddingDimensionMismatchError, EmbeddingModelMismatchError, type EvolutionEntry, type LlmConfig, type LowQualityItem, type LowQualityReason, type MemoryNote, type MemoryOperation, type MigrateResult, type PoolingMode, type QueryResult, type SearchResult, type StorageContext, addEpisodic, addMemory, canRead, canWrite, checkQuality, configure, configureLlm, conflictSweep, consolidateMemories, createStorageContext, deleteNote, encode, ensureCollection, generateReviewBatch, getEmbeddingDevice, getEmbeddingDim, getEmbeddingDtype, getEmbeddingModel, getEmbeddingPooling, getNote, invalidateNote, isModelLoaded, isPlausibleUpdateTarget, listMemories, listNotes, llmCrudDecision, loadModel, mergeSimilarNotes, migrateCollection, patchNotePayload, pingQdrant, resolveCrudUpdateMinSim, scanLowQuality, searchMemory, updateNote };
690
+ export { type AgentAmemConfig, type AmemPluginConfig, type ConflictMode, type ConflictSweepResult, DEFAULT_CRUD_UPDATE_MIN_SIM, DEFAULT_EMBEDDING_MODEL, EmbeddingDimensionMismatchError, EmbeddingModelMismatchError, type EvolutionEntry, LEGACY_DEFAULT_DIM, LEGACY_DEFAULT_EMBEDDING_MODEL, type LlmConfig, type LowQualityItem, type LowQualityReason, type MemoryNote, type MemoryOperation, type MigrateResult, MixedEmbeddingModelsError, type PoolingMode, type QueryResult, SYSTEM_ACTOR, type SearchResult, type StorageContext, addEpisodic, addMemory, canRead, canWrite, checkQuality, configure, configureLlm, conflictSweep, consolidateMemories, createStorageContext, deleteNote, encode, ensureCollection, generateReviewBatch, getEmbeddingDevice, getEmbeddingDim, getEmbeddingDtype, getEmbeddingModel, getEmbeddingPooling, getNote, invalidateNote, isModelLoaded, isPlausibleUpdateTarget, listMemories, listNotes, llmCrudDecision, loadModel, mergeSimilarNotes, migrateCollection, patchNotePayload, pingQdrant, resolveCrudUpdateMinSim, scanLowQuality, searchMemory, switchToMigrated, updateNote };
package/dist/index.js CHANGED
@@ -2,6 +2,10 @@ import {
2
2
  DEFAULT_EMBEDDING_MODEL,
3
3
  EmbeddingDimensionMismatchError,
4
4
  EmbeddingModelMismatchError,
5
+ LEGACY_DEFAULT_DIM,
6
+ LEGACY_DEFAULT_EMBEDDING_MODEL,
7
+ MixedEmbeddingModelsError,
8
+ SYSTEM_ACTOR,
5
9
  addEpisodic,
6
10
  addMemory,
7
11
  canRead,
@@ -33,8 +37,9 @@ import {
33
37
  patchNotePayload,
34
38
  pingQdrant,
35
39
  searchMemory,
40
+ switchToMigrated,
36
41
  updateNote
37
- } from "./chunk-B2NS7WAM.js";
42
+ } from "./chunk-XEMQZNLD.js";
38
43
 
39
44
  // src/quality.ts
40
45
  import * as fs from "fs";
@@ -236,6 +241,10 @@ export {
236
241
  DEFAULT_EMBEDDING_MODEL,
237
242
  EmbeddingDimensionMismatchError,
238
243
  EmbeddingModelMismatchError,
244
+ LEGACY_DEFAULT_DIM,
245
+ LEGACY_DEFAULT_EMBEDDING_MODEL,
246
+ MixedEmbeddingModelsError,
247
+ SYSTEM_ACTOR,
239
248
  addEpisodic,
240
249
  addMemory,
241
250
  canRead,
@@ -270,6 +279,7 @@ export {
270
279
  resolveCrudUpdateMinSim,
271
280
  scanLowQuality,
272
281
  searchMemory,
282
+ switchToMigrated,
273
283
  updateNote
274
284
  };
275
285
  //# sourceMappingURL=index.js.map