@amemhq/core 1.0.1 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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';
@@ -29,6 +51,30 @@ type PoolingMode = 'mean' | 'cls';
29
51
  * assumed.
30
52
  */
31
53
  declare function getEmbeddingPooling(): PoolingMode;
54
+ /**
55
+ * Where inference runs. Unset means Transformers.js picks, which on Node is `cpu`.
56
+ *
57
+ * It is not CPU-only for lack of anything else: `onnxruntime-node`'s macOS arm64
58
+ * binary links CoreML.framework and exports the CoreML provider, and
59
+ * Transformers.js lists `coreml` (macOS), `dml` (Windows), `cuda` (Linux x64) and
60
+ * `webgpu` alongside `cpu`. It simply defaults to `cpu` and amem never asked for
61
+ * anything else.
62
+ *
63
+ * Whether asking helps is **unmeasured**. CoreML partitions a graph operator by
64
+ * operator and falls back to CPU for the ones it cannot take, so it can lose to
65
+ * plain CPU on some models and pay a compile cost on first load. Hence: opt-in,
66
+ * default unchanged, and no recommendation until someone benchmarks it.
67
+ */
68
+ declare function getEmbeddingDevice(): string | undefined;
69
+ /**
70
+ * Weight precision. Unset means Transformers.js picks, which on Node is `fp32` —
71
+ * the largest download of every variant a model publishes.
72
+ *
73
+ * Passed through rather than validated against a list: Transformers.js already
74
+ * rejects an unknown value and names the valid ones, and a list here would go
75
+ * stale the moment it gains a quantization.
76
+ */
77
+ declare function getEmbeddingDtype(): string | undefined;
32
78
  /**
33
79
  * The vector width this model produces, measured rather than looked up.
34
80
  *
@@ -39,7 +85,9 @@ declare function getEmbeddingPooling(): PoolingMode;
39
85
  */
40
86
  declare function getEmbeddingDim(): Promise<number>;
41
87
  /**
42
- * Encode text to 384-dim normalized embedding vector.
88
+ * Encode text to a normalized embedding vector. The width is the model's — 1024
89
+ * for the default, 384 for the one before it — so nothing here should assume a
90
+ * number.
43
91
  * Singleton model, loaded once and reused.
44
92
  */
45
93
  declare function encode(text: string): Promise<number[]>;
@@ -58,7 +106,8 @@ declare function isModelLoaded(): boolean;
58
106
  /**
59
107
  * storage.ts — Qdrant vector storage for A-MEM
60
108
  * Uses native fetch (Node 18+) to avoid undici compatibility issues with Node v26
61
- * Collection: amem_notes, 384-dim cosine, with agent_id isolation
109
+ * Collection: amem_notes, cosine, width set by the embedding model, with
110
+ * agent_id isolation
62
111
  */
63
112
  /** Per-agent override config. If collection is set, mode B (isolated collection) is used. */
64
113
  interface AgentAmemConfig {
@@ -153,6 +202,33 @@ declare class EmbeddingDimensionMismatchError extends Error {
153
202
  readonly model: string;
154
203
  constructor(collection: string, collectionDim: number, modelDim: number, model: string);
155
204
  }
205
+ /**
206
+ * Which embedding model built a collection, and what the process wants to use.
207
+ *
208
+ * Vector width is the only thing Qdrant can check for us, and two models of the
209
+ * same width are indistinguishable to it. This is the case that check misses.
210
+ */
211
+ declare class EmbeddingModelMismatchError extends Error {
212
+ readonly collection: string;
213
+ readonly collectionModel: string;
214
+ readonly configuredModel: string;
215
+ constructor(collection: string, collectionModel: string, configuredModel: string);
216
+ }
217
+ /**
218
+ * Two collections open in one process that need two different models.
219
+ *
220
+ * Only reachable in mode B, and normally only mid-migration: per-agent
221
+ * collections built before 2.0.0 all resolve to the same old model, until one of
222
+ * them is migrated and the others are not. One process embeds with one model, so
223
+ * this has to stop rather than pick a winner — picking would write vectors of the
224
+ * wrong width into whichever collection lost.
225
+ */
226
+ declare class MixedEmbeddingModelsError extends Error {
227
+ readonly collection: string;
228
+ readonly wanted: string;
229
+ readonly inUse: string;
230
+ constructor(collection: string, wanted: string, inUse: string);
231
+ }
156
232
  /**
157
233
  * Ask Qdrant whether it can serve, right now.
158
234
  *
@@ -178,28 +254,34 @@ declare function makeCrud(collectionName: string, modeBIsolated?: boolean): {
178
254
  addNote(note: MemoryNote): Promise<void>;
179
255
  /**
180
256
  * Story 36: this is the one read that bypasses the agent filter — it fetches
181
- * straight by UUID. Pass `readerAgentId` to enforce `readers`; an unreadable
182
- * note comes back as `null` (indistinguishable from missing, so nothing leaks,
183
- * and callers already handle null). Omitting it skips the check, preserving
184
- * behaviour for internal callers that only ever hold their own ids.
257
+ * straight by UUID. An unreadable note comes back as `null`, indistinguishable
258
+ * from missing, so nothing leaks and callers already handle it.
259
+ *
260
+ * `reader` is required. It used to be optional, and omitting it skipped the
261
+ * check — which meant the safe behaviour was the one you had to remember to
262
+ * ask for. Pass `SYSTEM_ACTOR` to read as the engine itself; that reads as a
263
+ * deliberate act at the call site, where an absent argument did not.
185
264
  */
186
- getNote(id: string, readerAgentId?: string): Promise<MemoryNote | null>;
265
+ getNote(id: string, reader: string): Promise<MemoryNote | null>;
187
266
  updateNote(note: MemoryNote): Promise<void>;
188
267
  findByHash(hash: string, agentId: string): Promise<MemoryNote | null>;
189
268
  /**
190
- * Story 33: pass `callerAgentId` to enforce the writers policy. Callers that
191
- * hold the note already should prefer checking `canWrite` themselves; this
192
- * fetch-then-check path exists for callers that only have an id (the plugin's
193
- * CRUD hook). Returns false without writing when the caller may not write.
194
- * Omitting `callerAgentId` skips the check, preserving existing behaviour for
195
- * internal callers that are already scoped to their own notes.
269
+ * Story 33: enforces the writers policy. Returns false — without writing —
270
+ * when the caller may not write. This fetch-then-check path exists for callers
271
+ * that only have an id (the plugin's CRUD hook); callers already holding the
272
+ * note can check `canWrite` themselves and skip a round trip.
273
+ *
274
+ * `caller` is required. Pass `SYSTEM_ACTOR` for the engine's own maintenance
275
+ * writes. The self-read below is a genuine `SYSTEM_ACTOR` case: it fetches the
276
+ * note in order to decide whether the caller may write it, and gating that
277
+ * fetch on the same policy it exists to evaluate would be circular.
196
278
  */
197
- updateNoteContent(id: string, content: string, embedding: number[], hash: string, callerAgentId?: string): Promise<boolean>;
279
+ updateNoteContent(id: string, content: string, embedding: number[], hash: string, caller: string): Promise<boolean>;
198
280
  queryByEmbedding(embedding: number[], topK: number, agentId: string, scoreThreshold?: number, subject?: string): Promise<QueryResult[]>;
199
281
  listNotes(agentId?: string, subject?: string): Promise<MemoryNote[]>;
200
282
  deleteNote(id: string): Promise<void>;
201
283
  /** Story 33: see `updateNoteContent` — returns false, unwritten, when denied. */
202
- invalidateNote(id: string, callerAgentId?: string): Promise<boolean>;
284
+ invalidateNote(id: string, caller: string): Promise<boolean>;
203
285
  getNotesByDatePrefix(datePrefix: string, agentId: string): Promise<MemoryNote[]>;
204
286
  countNotes(agentId?: string): Promise<number>;
205
287
  updateNoteLinks(id: string, links: string[]): Promise<void>;
@@ -213,11 +295,11 @@ type StorageContext = ReturnType<typeof makeCrud>;
213
295
  * Mode B (isolated collection): pass collectionName = 'amem_notes_<agentId>' and modeBIsolated = true.
214
296
  */
215
297
  declare function createStorageContext(collectionName?: string, modeBIsolated?: boolean): StorageContext;
216
- declare function getNote(id: string, readerAgentId?: string): Promise<MemoryNote | null>;
298
+ declare function getNote(id: string, reader: string): Promise<MemoryNote | null>;
217
299
  declare function updateNote(note: MemoryNote): Promise<void>;
218
300
  declare function listNotes(agentId?: string, subject?: string): Promise<MemoryNote[]>;
219
301
  declare function deleteNote(id: string): Promise<void>;
220
- declare function invalidateNote(id: string, callerAgentId?: string): Promise<boolean>;
302
+ declare function invalidateNote(id: string, caller: string): Promise<boolean>;
221
303
  declare function patchNotePayload(id: string, fields: Record<string, unknown>): Promise<void>;
222
304
 
223
305
  /**
@@ -267,8 +349,28 @@ interface SearchResult {
267
349
  keywords: string[];
268
350
  links: string[];
269
351
  timestamp: string;
352
+ /**
353
+ * Cosine similarity to the query. **Not** what ordered this list — that is
354
+ * `rrf`, which fuses the dense and BM25 rankings and then applies a heat/recency
355
+ * boost. The two disagree often, and a consumer that reads `similarity` as the
356
+ * ranking score concludes the ranking is broken.
357
+ */
270
358
  similarity: number;
359
+ /**
360
+ * The fused score the matches are sorted by, and 0 for anything no retriever
361
+ * ranked. It is `via`, not this, that says why a row is here.
362
+ */
271
363
  rrf: number;
364
+ /**
365
+ * Why this note is in the results.
366
+ *
367
+ * `match` — it was retrieved for the query and ranked by `rrf`.
368
+ * `link` — it was **not** retrieved; it is here because it links to one that
369
+ * was, within two hops and above the relevance gate. These are appended in
370
+ * discovery order after the matches and have no `rrf` of their own, so reading
371
+ * the tail of the list as "lower-ranked matches" is wrong.
372
+ */
373
+ via: 'match' | 'link';
272
374
  topics: string[];
273
375
  note_type: 'memory' | 'knowledge';
274
376
  }
@@ -374,6 +476,28 @@ declare function generateReviewBatch(agentId: string, outputPath?: string): Prom
374
476
  * policy is unit-testable on its own and identical everywhere it is applied.
375
477
  */
376
478
 
479
+ /**
480
+ * The engine acting as itself, rather than on behalf of any agent.
481
+ *
482
+ * `getNote`, `updateNoteContent` and `invalidateNote` all used to take an
483
+ * *optional* identity, and omitting it skipped the authorization check. That put
484
+ * the safe behaviour behind remembering to ask for it, and made "no check here"
485
+ * invisible — an absent argument looks the same as an oversight. The identity is
486
+ * now required, and this is what a call declares when it genuinely has no agent
487
+ * on whose behalf it acts.
488
+ *
489
+ * Two call sites use it, both inside storage.ts: the fetch that
490
+ * `updateNoteContent` and `invalidateNote` perform in order to *evaluate* the
491
+ * write policy. Gating that read on the policy it exists to check would be
492
+ * circular. Everything else passes a real agent id — the audit that prompted this
493
+ * found the identity was already in scope at every one of them.
494
+ *
495
+ * The prefix keeps it from colliding with any plausible agent id. It is not a
496
+ * secret and does not need to be: amem is self-hosted, the operator owns every
497
+ * memory in the store, and there is no privilege boundary here to defend. This
498
+ * guards against a call site forgetting to pass an identity, not against a user.
499
+ */
500
+ declare const SYSTEM_ACTOR = "__amem_system__";
377
501
  /**
378
502
  * May `callerAgentId` mutate `note`?
379
503
  *
@@ -403,8 +527,10 @@ interface MigrateResult {
403
527
  missingDerived: number;
404
528
  /** Notes whose fields were re-extracted. 0 unless refreshFields. */
405
529
  refreshed: number;
406
- /** Notes written into the target. 0 on a dry run. */
530
+ /** Notes written into the target by THIS run. 0 on a dry run. */
407
531
  migrated: number;
532
+ /** Notes a previous interrupted run had already written. */
533
+ skipped: number;
408
534
  sourceDim: number | null;
409
535
  targetDim: number;
410
536
  model: string;
@@ -424,6 +550,33 @@ declare function migrateCollection(opts: {
424
550
  warn: (m: string) => void;
425
551
  };
426
552
  }): Promise<MigrateResult>;
553
+ /**
554
+ * Put the migrated collection behind the name the source used, and drop the
555
+ * source.
556
+ *
557
+ * This is the only irreversible step in the whole migration, which is why it is
558
+ * a separate call rather than the tail of `migrateCollection`. Everything before
559
+ * it leaves the original untouched and can simply be abandoned.
560
+ *
561
+ * Qdrant cannot rename a collection and cannot create an alias over a name a real
562
+ * collection holds (409), so freeing the name means deleting it — after checking
563
+ * the target holds at least as much as the source, because that check is the last
564
+ * thing standing between a half-finished migration and a deleted store.
565
+ */
566
+ declare function switchToMigrated(opts: {
567
+ /** The name readers are configured with. Becomes an alias. */
568
+ name: string;
569
+ /** The collection built by `migrateCollection`. */
570
+ to: string;
571
+ logger?: {
572
+ info: (m: string) => void;
573
+ warn: (m: string) => void;
574
+ };
575
+ }): Promise<{
576
+ name: string;
577
+ to: string;
578
+ moved: number;
579
+ }>;
427
580
 
428
581
  /**
429
582
  * Similarity floor for accepting an UPDATE target.
@@ -512,4 +665,4 @@ declare function llmCrudDecision(userText: string, assistantText: string, existi
512
665
  content: string;
513
666
  }>): Promise<MemoryOperation[]>;
514
667
 
515
- export { type AgentAmemConfig, type AmemPluginConfig, type ConflictMode, type ConflictSweepResult, DEFAULT_CRUD_UPDATE_MIN_SIM, DEFAULT_EMBEDDING_MODEL, EmbeddingDimensionMismatchError, 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, getEmbeddingDim, getEmbeddingModel, getEmbeddingPooling, getNote, invalidateNote, isModelLoaded, isPlausibleUpdateTarget, listMemories, listNotes, llmCrudDecision, loadModel, mergeSimilarNotes, migrateCollection, patchNotePayload, pingQdrant, resolveCrudUpdateMinSim, scanLowQuality, searchMemory, updateNote };
668
+ 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';
@@ -29,6 +51,30 @@ type PoolingMode = 'mean' | 'cls';
29
51
  * assumed.
30
52
  */
31
53
  declare function getEmbeddingPooling(): PoolingMode;
54
+ /**
55
+ * Where inference runs. Unset means Transformers.js picks, which on Node is `cpu`.
56
+ *
57
+ * It is not CPU-only for lack of anything else: `onnxruntime-node`'s macOS arm64
58
+ * binary links CoreML.framework and exports the CoreML provider, and
59
+ * Transformers.js lists `coreml` (macOS), `dml` (Windows), `cuda` (Linux x64) and
60
+ * `webgpu` alongside `cpu`. It simply defaults to `cpu` and amem never asked for
61
+ * anything else.
62
+ *
63
+ * Whether asking helps is **unmeasured**. CoreML partitions a graph operator by
64
+ * operator and falls back to CPU for the ones it cannot take, so it can lose to
65
+ * plain CPU on some models and pay a compile cost on first load. Hence: opt-in,
66
+ * default unchanged, and no recommendation until someone benchmarks it.
67
+ */
68
+ declare function getEmbeddingDevice(): string | undefined;
69
+ /**
70
+ * Weight precision. Unset means Transformers.js picks, which on Node is `fp32` —
71
+ * the largest download of every variant a model publishes.
72
+ *
73
+ * Passed through rather than validated against a list: Transformers.js already
74
+ * rejects an unknown value and names the valid ones, and a list here would go
75
+ * stale the moment it gains a quantization.
76
+ */
77
+ declare function getEmbeddingDtype(): string | undefined;
32
78
  /**
33
79
  * The vector width this model produces, measured rather than looked up.
34
80
  *
@@ -39,7 +85,9 @@ declare function getEmbeddingPooling(): PoolingMode;
39
85
  */
40
86
  declare function getEmbeddingDim(): Promise<number>;
41
87
  /**
42
- * Encode text to 384-dim normalized embedding vector.
88
+ * Encode text to a normalized embedding vector. The width is the model's — 1024
89
+ * for the default, 384 for the one before it — so nothing here should assume a
90
+ * number.
43
91
  * Singleton model, loaded once and reused.
44
92
  */
45
93
  declare function encode(text: string): Promise<number[]>;
@@ -58,7 +106,8 @@ declare function isModelLoaded(): boolean;
58
106
  /**
59
107
  * storage.ts — Qdrant vector storage for A-MEM
60
108
  * Uses native fetch (Node 18+) to avoid undici compatibility issues with Node v26
61
- * Collection: amem_notes, 384-dim cosine, with agent_id isolation
109
+ * Collection: amem_notes, cosine, width set by the embedding model, with
110
+ * agent_id isolation
62
111
  */
63
112
  /** Per-agent override config. If collection is set, mode B (isolated collection) is used. */
64
113
  interface AgentAmemConfig {
@@ -153,6 +202,33 @@ declare class EmbeddingDimensionMismatchError extends Error {
153
202
  readonly model: string;
154
203
  constructor(collection: string, collectionDim: number, modelDim: number, model: string);
155
204
  }
205
+ /**
206
+ * Which embedding model built a collection, and what the process wants to use.
207
+ *
208
+ * Vector width is the only thing Qdrant can check for us, and two models of the
209
+ * same width are indistinguishable to it. This is the case that check misses.
210
+ */
211
+ declare class EmbeddingModelMismatchError extends Error {
212
+ readonly collection: string;
213
+ readonly collectionModel: string;
214
+ readonly configuredModel: string;
215
+ constructor(collection: string, collectionModel: string, configuredModel: string);
216
+ }
217
+ /**
218
+ * Two collections open in one process that need two different models.
219
+ *
220
+ * Only reachable in mode B, and normally only mid-migration: per-agent
221
+ * collections built before 2.0.0 all resolve to the same old model, until one of
222
+ * them is migrated and the others are not. One process embeds with one model, so
223
+ * this has to stop rather than pick a winner — picking would write vectors of the
224
+ * wrong width into whichever collection lost.
225
+ */
226
+ declare class MixedEmbeddingModelsError extends Error {
227
+ readonly collection: string;
228
+ readonly wanted: string;
229
+ readonly inUse: string;
230
+ constructor(collection: string, wanted: string, inUse: string);
231
+ }
156
232
  /**
157
233
  * Ask Qdrant whether it can serve, right now.
158
234
  *
@@ -178,28 +254,34 @@ declare function makeCrud(collectionName: string, modeBIsolated?: boolean): {
178
254
  addNote(note: MemoryNote): Promise<void>;
179
255
  /**
180
256
  * Story 36: this is the one read that bypasses the agent filter — it fetches
181
- * straight by UUID. Pass `readerAgentId` to enforce `readers`; an unreadable
182
- * note comes back as `null` (indistinguishable from missing, so nothing leaks,
183
- * and callers already handle null). Omitting it skips the check, preserving
184
- * behaviour for internal callers that only ever hold their own ids.
257
+ * straight by UUID. An unreadable note comes back as `null`, indistinguishable
258
+ * from missing, so nothing leaks and callers already handle it.
259
+ *
260
+ * `reader` is required. It used to be optional, and omitting it skipped the
261
+ * check — which meant the safe behaviour was the one you had to remember to
262
+ * ask for. Pass `SYSTEM_ACTOR` to read as the engine itself; that reads as a
263
+ * deliberate act at the call site, where an absent argument did not.
185
264
  */
186
- getNote(id: string, readerAgentId?: string): Promise<MemoryNote | null>;
265
+ getNote(id: string, reader: string): Promise<MemoryNote | null>;
187
266
  updateNote(note: MemoryNote): Promise<void>;
188
267
  findByHash(hash: string, agentId: string): Promise<MemoryNote | null>;
189
268
  /**
190
- * Story 33: pass `callerAgentId` to enforce the writers policy. Callers that
191
- * hold the note already should prefer checking `canWrite` themselves; this
192
- * fetch-then-check path exists for callers that only have an id (the plugin's
193
- * CRUD hook). Returns false without writing when the caller may not write.
194
- * Omitting `callerAgentId` skips the check, preserving existing behaviour for
195
- * internal callers that are already scoped to their own notes.
269
+ * Story 33: enforces the writers policy. Returns false — without writing —
270
+ * when the caller may not write. This fetch-then-check path exists for callers
271
+ * that only have an id (the plugin's CRUD hook); callers already holding the
272
+ * note can check `canWrite` themselves and skip a round trip.
273
+ *
274
+ * `caller` is required. Pass `SYSTEM_ACTOR` for the engine's own maintenance
275
+ * writes. The self-read below is a genuine `SYSTEM_ACTOR` case: it fetches the
276
+ * note in order to decide whether the caller may write it, and gating that
277
+ * fetch on the same policy it exists to evaluate would be circular.
196
278
  */
197
- updateNoteContent(id: string, content: string, embedding: number[], hash: string, callerAgentId?: string): Promise<boolean>;
279
+ updateNoteContent(id: string, content: string, embedding: number[], hash: string, caller: string): Promise<boolean>;
198
280
  queryByEmbedding(embedding: number[], topK: number, agentId: string, scoreThreshold?: number, subject?: string): Promise<QueryResult[]>;
199
281
  listNotes(agentId?: string, subject?: string): Promise<MemoryNote[]>;
200
282
  deleteNote(id: string): Promise<void>;
201
283
  /** Story 33: see `updateNoteContent` — returns false, unwritten, when denied. */
202
- invalidateNote(id: string, callerAgentId?: string): Promise<boolean>;
284
+ invalidateNote(id: string, caller: string): Promise<boolean>;
203
285
  getNotesByDatePrefix(datePrefix: string, agentId: string): Promise<MemoryNote[]>;
204
286
  countNotes(agentId?: string): Promise<number>;
205
287
  updateNoteLinks(id: string, links: string[]): Promise<void>;
@@ -213,11 +295,11 @@ type StorageContext = ReturnType<typeof makeCrud>;
213
295
  * Mode B (isolated collection): pass collectionName = 'amem_notes_<agentId>' and modeBIsolated = true.
214
296
  */
215
297
  declare function createStorageContext(collectionName?: string, modeBIsolated?: boolean): StorageContext;
216
- declare function getNote(id: string, readerAgentId?: string): Promise<MemoryNote | null>;
298
+ declare function getNote(id: string, reader: string): Promise<MemoryNote | null>;
217
299
  declare function updateNote(note: MemoryNote): Promise<void>;
218
300
  declare function listNotes(agentId?: string, subject?: string): Promise<MemoryNote[]>;
219
301
  declare function deleteNote(id: string): Promise<void>;
220
- declare function invalidateNote(id: string, callerAgentId?: string): Promise<boolean>;
302
+ declare function invalidateNote(id: string, caller: string): Promise<boolean>;
221
303
  declare function patchNotePayload(id: string, fields: Record<string, unknown>): Promise<void>;
222
304
 
223
305
  /**
@@ -267,8 +349,28 @@ interface SearchResult {
267
349
  keywords: string[];
268
350
  links: string[];
269
351
  timestamp: string;
352
+ /**
353
+ * Cosine similarity to the query. **Not** what ordered this list — that is
354
+ * `rrf`, which fuses the dense and BM25 rankings and then applies a heat/recency
355
+ * boost. The two disagree often, and a consumer that reads `similarity` as the
356
+ * ranking score concludes the ranking is broken.
357
+ */
270
358
  similarity: number;
359
+ /**
360
+ * The fused score the matches are sorted by, and 0 for anything no retriever
361
+ * ranked. It is `via`, not this, that says why a row is here.
362
+ */
271
363
  rrf: number;
364
+ /**
365
+ * Why this note is in the results.
366
+ *
367
+ * `match` — it was retrieved for the query and ranked by `rrf`.
368
+ * `link` — it was **not** retrieved; it is here because it links to one that
369
+ * was, within two hops and above the relevance gate. These are appended in
370
+ * discovery order after the matches and have no `rrf` of their own, so reading
371
+ * the tail of the list as "lower-ranked matches" is wrong.
372
+ */
373
+ via: 'match' | 'link';
272
374
  topics: string[];
273
375
  note_type: 'memory' | 'knowledge';
274
376
  }
@@ -374,6 +476,28 @@ declare function generateReviewBatch(agentId: string, outputPath?: string): Prom
374
476
  * policy is unit-testable on its own and identical everywhere it is applied.
375
477
  */
376
478
 
479
+ /**
480
+ * The engine acting as itself, rather than on behalf of any agent.
481
+ *
482
+ * `getNote`, `updateNoteContent` and `invalidateNote` all used to take an
483
+ * *optional* identity, and omitting it skipped the authorization check. That put
484
+ * the safe behaviour behind remembering to ask for it, and made "no check here"
485
+ * invisible — an absent argument looks the same as an oversight. The identity is
486
+ * now required, and this is what a call declares when it genuinely has no agent
487
+ * on whose behalf it acts.
488
+ *
489
+ * Two call sites use it, both inside storage.ts: the fetch that
490
+ * `updateNoteContent` and `invalidateNote` perform in order to *evaluate* the
491
+ * write policy. Gating that read on the policy it exists to check would be
492
+ * circular. Everything else passes a real agent id — the audit that prompted this
493
+ * found the identity was already in scope at every one of them.
494
+ *
495
+ * The prefix keeps it from colliding with any plausible agent id. It is not a
496
+ * secret and does not need to be: amem is self-hosted, the operator owns every
497
+ * memory in the store, and there is no privilege boundary here to defend. This
498
+ * guards against a call site forgetting to pass an identity, not against a user.
499
+ */
500
+ declare const SYSTEM_ACTOR = "__amem_system__";
377
501
  /**
378
502
  * May `callerAgentId` mutate `note`?
379
503
  *
@@ -403,8 +527,10 @@ interface MigrateResult {
403
527
  missingDerived: number;
404
528
  /** Notes whose fields were re-extracted. 0 unless refreshFields. */
405
529
  refreshed: number;
406
- /** Notes written into the target. 0 on a dry run. */
530
+ /** Notes written into the target by THIS run. 0 on a dry run. */
407
531
  migrated: number;
532
+ /** Notes a previous interrupted run had already written. */
533
+ skipped: number;
408
534
  sourceDim: number | null;
409
535
  targetDim: number;
410
536
  model: string;
@@ -424,6 +550,33 @@ declare function migrateCollection(opts: {
424
550
  warn: (m: string) => void;
425
551
  };
426
552
  }): Promise<MigrateResult>;
553
+ /**
554
+ * Put the migrated collection behind the name the source used, and drop the
555
+ * source.
556
+ *
557
+ * This is the only irreversible step in the whole migration, which is why it is
558
+ * a separate call rather than the tail of `migrateCollection`. Everything before
559
+ * it leaves the original untouched and can simply be abandoned.
560
+ *
561
+ * Qdrant cannot rename a collection and cannot create an alias over a name a real
562
+ * collection holds (409), so freeing the name means deleting it — after checking
563
+ * the target holds at least as much as the source, because that check is the last
564
+ * thing standing between a half-finished migration and a deleted store.
565
+ */
566
+ declare function switchToMigrated(opts: {
567
+ /** The name readers are configured with. Becomes an alias. */
568
+ name: string;
569
+ /** The collection built by `migrateCollection`. */
570
+ to: string;
571
+ logger?: {
572
+ info: (m: string) => void;
573
+ warn: (m: string) => void;
574
+ };
575
+ }): Promise<{
576
+ name: string;
577
+ to: string;
578
+ moved: number;
579
+ }>;
427
580
 
428
581
  /**
429
582
  * Similarity floor for accepting an UPDATE target.
@@ -512,4 +665,4 @@ declare function llmCrudDecision(userText: string, assistantText: string, existi
512
665
  content: string;
513
666
  }>): Promise<MemoryOperation[]>;
514
667
 
515
- export { type AgentAmemConfig, type AmemPluginConfig, type ConflictMode, type ConflictSweepResult, DEFAULT_CRUD_UPDATE_MIN_SIM, DEFAULT_EMBEDDING_MODEL, EmbeddingDimensionMismatchError, 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, getEmbeddingDim, getEmbeddingModel, getEmbeddingPooling, getNote, invalidateNote, isModelLoaded, isPlausibleUpdateTarget, listMemories, listNotes, llmCrudDecision, loadModel, mergeSimilarNotes, migrateCollection, patchNotePayload, pingQdrant, resolveCrudUpdateMinSim, scanLowQuality, searchMemory, updateNote };
668
+ 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 };