openclaw-amem 1.4.1 → 1.4.3

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.js CHANGED
@@ -56,41 +56,66 @@ var init_config = __esm({
56
56
  function getEmbeddingModel() {
57
57
  return process.env.AMEM_EMBED_MODEL?.trim() || DEFAULT_EMBEDDING_MODEL;
58
58
  }
59
+ function getEmbeddingPooling() {
60
+ const explicit = process.env.AMEM_EMBED_POOLING?.trim().toLowerCase();
61
+ if (explicit === "mean" || explicit === "cls") return explicit;
62
+ const basename2 = getEmbeddingModel().split("/").pop()?.toLowerCase() ?? "";
63
+ return CLS_POOLED_MODELS.has(basename2) ? "cls" : "mean";
64
+ }
65
+ function getEmbeddingDevice() {
66
+ return process.env.AMEM_EMBED_DEVICE?.trim() || void 0;
67
+ }
68
+ function getEmbeddingDtype() {
69
+ return process.env.AMEM_EMBED_DTYPE?.trim() || void 0;
70
+ }
71
+ function extractorKey() {
72
+ return `${getEmbeddingModel()}|${getEmbeddingDevice() ?? ""}|${getEmbeddingDtype() ?? ""}`;
73
+ }
59
74
  async function getExtractor() {
60
- const wanted = getEmbeddingModel();
61
- if (extractor && loadedModelName === wanted) return extractor;
75
+ const wanted = extractorKey();
76
+ if (extractor && loadedKey === wanted) return extractor;
62
77
  if (!pipeline) {
63
78
  const mod = await import("@huggingface/transformers");
64
79
  pipeline = mod.pipeline;
65
80
  }
66
- extractor = await pipeline("feature-extraction", wanted, {
67
- revision: "main"
81
+ const device = getEmbeddingDevice();
82
+ const dtype = getEmbeddingDtype();
83
+ extractor = await pipeline("feature-extraction", getEmbeddingModel(), {
84
+ revision: "main",
85
+ // Omitted entirely when unset, so an unconfigured install gets exactly the
86
+ // library defaults it got before these existed.
87
+ ...device ? { device } : {},
88
+ ...dtype ? { dtype } : {}
68
89
  });
69
- loadedModelName = wanted;
90
+ loadedKey = wanted;
70
91
  cachedDim = null;
71
92
  return extractor;
72
93
  }
73
94
  async function getEmbeddingDim() {
74
- if (cachedDim !== null && loadedModelName === getEmbeddingModel()) return cachedDim;
95
+ if (cachedDim !== null && loadedKey === extractorKey()) return cachedDim;
75
96
  const probe = await encode("dimension probe");
76
97
  cachedDim = probe.length;
77
98
  return cachedDim;
78
99
  }
79
- function meanPoolingNormalize(output, attentionMask) {
100
+ function poolNormalize(output, attentionMask, mode) {
80
101
  const seqLen = output.length;
81
102
  const dim = output[0].length;
82
103
  const pooled = new Array(dim).fill(0);
83
- let maskSum = 0;
84
- for (let i = 0; i < seqLen; i++) {
85
- const m = attentionMask[i];
86
- maskSum += m;
104
+ if (mode === "cls") {
105
+ for (let j = 0; j < dim; j++) pooled[j] = output[0][j];
106
+ } else {
107
+ let maskSum = 0;
108
+ for (let i = 0; i < seqLen; i++) {
109
+ const m = attentionMask[i];
110
+ maskSum += m;
111
+ for (let j = 0; j < dim; j++) {
112
+ pooled[j] += output[i][j] * m;
113
+ }
114
+ }
87
115
  for (let j = 0; j < dim; j++) {
88
- pooled[j] += output[i][j] * m;
116
+ pooled[j] /= Math.max(maskSum, 1e-9);
89
117
  }
90
118
  }
91
- for (let j = 0; j < dim; j++) {
92
- pooled[j] /= Math.max(maskSum, 1e-9);
93
- }
94
119
  let norm = 0;
95
120
  for (const v of pooled) norm += v * v;
96
121
  norm = Math.sqrt(norm);
@@ -98,7 +123,8 @@ function meanPoolingNormalize(output, attentionMask) {
98
123
  }
99
124
  async function encode(text) {
100
125
  const ext = await getExtractor();
101
- const result = await ext(text, { pooling: "mean", normalize: true });
126
+ const pooling = getEmbeddingPooling();
127
+ const result = await ext(text, { pooling, normalize: true });
102
128
  if (result && result.data) {
103
129
  return Array.from(result.data);
104
130
  }
@@ -114,7 +140,7 @@ async function encode(text) {
114
140
  }
115
141
  raw.push(row);
116
142
  }
117
- return meanPoolingNormalize(raw, new Array(seqLen).fill(1));
143
+ return poolNormalize(raw, new Array(seqLen).fill(1), pooling);
118
144
  }
119
145
  throw new Error("Unexpected embedding output shape");
120
146
  }
@@ -129,15 +155,28 @@ function cosineSimilarity(a, b) {
129
155
  for (let i = 0; i < a.length; i++) dot += a[i] * b[i];
130
156
  return dot;
131
157
  }
132
- var pipeline, extractor, loadedModelName, cachedDim, DEFAULT_EMBEDDING_MODEL;
158
+ var pipeline, extractor, loadedKey, cachedDim, DEFAULT_EMBEDDING_MODEL, CLS_POOLED_MODELS;
133
159
  var init_embedding = __esm({
134
160
  "../amem-core/src/embedding.ts"() {
135
161
  "use strict";
136
162
  pipeline = null;
137
163
  extractor = null;
138
- loadedModelName = null;
164
+ loadedKey = null;
139
165
  cachedDim = null;
140
166
  DEFAULT_EMBEDDING_MODEL = "Xenova/paraphrase-multilingual-MiniLM-L12-v2";
167
+ CLS_POOLED_MODELS = /* @__PURE__ */ new Set([
168
+ "bge-m3",
169
+ "bge-base-zh-v1.5",
170
+ "bge-small-zh-v1.5",
171
+ "bge-base-en-v1.5",
172
+ "bge-small-en-v1.5",
173
+ "bge-large-en-v1.5",
174
+ "gte-multilingual-base",
175
+ "gte-modernbert-base",
176
+ "gte-large-en-v1.5",
177
+ "snowflake-arctic-embed-m",
178
+ "snowflake-arctic-embed-l"
179
+ ]);
141
180
  }
142
181
  });
143
182
 
@@ -155,6 +194,20 @@ var init_auth = __esm({
155
194
  });
156
195
 
157
196
  // ../amem-core/src/storage.ts
197
+ function migrationHint(collection, targetModel) {
198
+ return `migrate to a new collection:
199
+
200
+ AMEM_EMBED_MODEL=${targetModel} \\
201
+ npx --package=@amemhq/core amem-migrate --to ${collection}_v2
202
+
203
+ That is a dry run; add --apply to write. "${collection}" is only read, so nothing is lost either way. When it looks right, point whatever names this collection at the new one \u2014 AMEM_COLLECTION, or the plugin's "collection" setting if this agent has its own. See https://amem.owo.lc/reference/embedding-models.`;
204
+ }
205
+ async function recordCollectionModel(collection, model) {
206
+ try {
207
+ await qdrant("PATCH", `/collections/${collection}`, { metadata: { embedding_model: model } });
208
+ } catch {
209
+ }
210
+ }
158
211
  async function qdrant(method, path6, body) {
159
212
  const res = await fetch(`${QDRANT_URL}${path6}`, {
160
213
  method,
@@ -195,6 +248,14 @@ async function ensureCollection(collectionName) {
195
248
  throw new EmbeddingDimensionMismatchError(col, collectionDim, modelDim, getEmbeddingModel());
196
249
  }
197
250
  }
251
+ const recorded = existing.config?.metadata?.embedding_model;
252
+ const current = getEmbeddingModel();
253
+ if (typeof recorded === "string" && recorded !== current) {
254
+ throw new EmbeddingModelMismatchError(col, recorded, current);
255
+ }
256
+ if (recorded === void 0) {
257
+ await recordCollectionModel(col, current);
258
+ }
198
259
  markReady();
199
260
  return;
200
261
  }
@@ -206,6 +267,7 @@ async function ensureCollection(collectionName) {
206
267
  } catch (err) {
207
268
  if (!(err instanceof Error) || !err.message.includes("already exists")) throw err;
208
269
  }
270
+ await recordCollectionModel(col, getEmbeddingModel());
209
271
  await qdrant("PUT", `/collections/${col}/index`, {
210
272
  field_name: "agent_id",
211
273
  field_schema: "keyword"
@@ -251,6 +313,7 @@ async function collectionDimRaw(collection) {
251
313
  }
252
314
  async function createCollectionRaw(collection, size) {
253
315
  await qdrant("PUT", `/collections/${collection}`, { vectors: { size, distance: "Cosine" } });
316
+ await recordCollectionModel(collection, getEmbeddingModel());
254
317
  for (const field_name of ["agent_id", "hash", "topics", "subjects"]) {
255
318
  await qdrant("PUT", `/collections/${collection}/index`, { field_name, field_schema: "keyword" });
256
319
  }
@@ -643,7 +706,7 @@ async function invalidateNote(id, callerAgentId) {
643
706
  async function patchNotePayload(id, fields) {
644
707
  return makeCrud(getCollection()).patchNotePayload(id, fields);
645
708
  }
646
- var QDRANT_URL, getCollection, EmbeddingDimensionMismatchError, _collectionReady, _collectionReadyMap;
709
+ var QDRANT_URL, getCollection, EmbeddingDimensionMismatchError, EmbeddingModelMismatchError, _collectionReady, _collectionReadyMap;
647
710
  var init_storage = __esm({
648
711
  "../amem-core/src/storage.ts"() {
649
712
  "use strict";
@@ -655,7 +718,7 @@ var init_storage = __esm({
655
718
  constructor(collection, collectionDim, modelDim, model) {
656
719
  super(
657
720
  `Collection "${collection}" stores ${collectionDim}-dimension vectors, but the embedding model "${model}" produces ${modelDim}. Qdrant fixes a collection's vector size at creation and cannot change it, so writes and searches would both fail.
658
- Either set AMEM_EMBED_MODEL back to the model this collection was built with, or migrate: build a new collection with the new model, backfill it, then point AMEM_COLLECTION at it. See docs/reference/embedding-models.md.`
721
+ Either set AMEM_EMBED_MODEL back to the model this collection was built with, or ${migrationHint(collection, model)}`
659
722
  );
660
723
  this.collection = collection;
661
724
  this.collectionDim = collectionDim;
@@ -668,6 +731,21 @@ Either set AMEM_EMBED_MODEL back to the model this collection was built with, or
668
731
  modelDim;
669
732
  model;
670
733
  };
734
+ EmbeddingModelMismatchError = class extends Error {
735
+ constructor(collection, collectionModel, configuredModel) {
736
+ super(
737
+ `Collection "${collection}" was built with the embedding model "${collectionModel}", but this process is configured for "${configuredModel}". Both produce vectors of the same width, so nothing would fail \u2014 searches would just quietly compare vectors from two different models.
738
+ Either set AMEM_EMBED_MODEL back to "${collectionModel}", or ` + migrationHint(collection, configuredModel)
739
+ );
740
+ this.collection = collection;
741
+ this.collectionModel = collectionModel;
742
+ this.configuredModel = configuredModel;
743
+ this.name = "EmbeddingModelMismatchError";
744
+ }
745
+ collection;
746
+ collectionModel;
747
+ configuredModel;
748
+ };
671
749
  _collectionReady = false;
672
750
  _collectionReadyMap = /* @__PURE__ */ new Map();
673
751
  }
@@ -2379,6 +2457,7 @@ __export(src_exports, {
2379
2457
  DEFAULT_CRUD_UPDATE_MIN_SIM: () => DEFAULT_CRUD_UPDATE_MIN_SIM,
2380
2458
  DEFAULT_EMBEDDING_MODEL: () => DEFAULT_EMBEDDING_MODEL,
2381
2459
  EmbeddingDimensionMismatchError: () => EmbeddingDimensionMismatchError,
2460
+ EmbeddingModelMismatchError: () => EmbeddingModelMismatchError,
2382
2461
  addEpisodic: () => addEpisodic,
2383
2462
  addMemory: () => addMemory,
2384
2463
  canRead: () => canRead,
@@ -2393,8 +2472,11 @@ __export(src_exports, {
2393
2472
  encode: () => encode,
2394
2473
  ensureCollection: () => ensureCollection,
2395
2474
  generateReviewBatch: () => generateReviewBatch,
2475
+ getEmbeddingDevice: () => getEmbeddingDevice,
2396
2476
  getEmbeddingDim: () => getEmbeddingDim,
2477
+ getEmbeddingDtype: () => getEmbeddingDtype,
2397
2478
  getEmbeddingModel: () => getEmbeddingModel,
2479
+ getEmbeddingPooling: () => getEmbeddingPooling,
2398
2480
  getNote: () => getNote,
2399
2481
  invalidateNote: () => invalidateNote,
2400
2482
  isModelLoaded: () => isModelLoaded,
@@ -2527,7 +2609,7 @@ function register(api) {
2527
2609
  `openclaw-amem: registered (native TS, Qdrant, default agent_id=${defaultScope.agentId}, default collection=${pluginConfig.collection ?? "amem_notes (default)"}, per-agent scope resolved per call)`
2528
2610
  );
2529
2611
  ensureCollection(pluginConfig.collection).catch((e) => {
2530
- if (e instanceof EmbeddingDimensionMismatchError) {
2612
+ if (e instanceof EmbeddingDimensionMismatchError || e instanceof EmbeddingModelMismatchError) {
2531
2613
  logger.error(`openclaw-amem: memory is UNUSABLE \u2014 ${e.message}`);
2532
2614
  } else {
2533
2615
  logger.warn(`openclaw-amem: ensureCollection failed \u2014 ${e.message}`);
@@ -2,7 +2,7 @@
2
2
  "id": "openclaw-amem",
3
3
  "name": "amem",
4
4
  "description": "Catches memories that contradict each other. Notes rewrite themselves as new ones arrive, link into a graph, and stay separated per agent and per person. Runs on a small model. Requires a local Qdrant. 中文走 jieba 分词,提示词有中文版。",
5
- "version": "1.4.1",
5
+ "version": "1.4.3",
6
6
  "kind": "memory",
7
7
  "openclaw": {
8
8
  "compat": {
@@ -39,6 +39,9 @@
39
39
  "AMEM_LLM_TIMEOUT",
40
40
  "AMEM_CRUD_UPDATE_MIN_SIM",
41
41
  "AMEM_EMBED_MODEL",
42
+ "AMEM_EMBED_POOLING",
43
+ "AMEM_EMBED_DEVICE",
44
+ "AMEM_EMBED_DTYPE",
42
45
  "AMEM_COLLECTION",
43
46
  "AMEM_DATA_DIR",
44
47
  "AMEM_EVO_COUNTER_PATH",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-amem",
3
- "version": "1.4.1",
3
+ "version": "1.4.3",
4
4
  "description": "Catches memories that contradict each other. Notes rewrite themselves as new ones arrive, link into a graph, and stay separated per agent and per person. Runs on a small model. Requires a local Qdrant. 中文走 jieba 分词,提示词有中文版。",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -16,7 +16,7 @@
16
16
  }
17
17
  },
18
18
  "dependencies": {
19
- "@anthropic-ai/sdk": "^0.112.1",
19
+ "@anthropic-ai/sdk": "^0.112.5",
20
20
  "@huggingface/transformers": "^4.2.0",
21
21
  "@node-rs/jieba": "^2.0.1",
22
22
  "@qdrant/js-client-rest": "^1.18.0",
@@ -28,7 +28,7 @@
28
28
  "@types/node": "^26.1.1",
29
29
  "@types/uuid": "^11.0.0",
30
30
  "eslint": "^10.7.0",
31
- "prettier": "^3.9.5",
31
+ "prettier": "^3.9.6",
32
32
  "tsup": "^8.4.0",
33
33
  "tsx": "^4.23.1",
34
34
  "typescript": "^6.0.3",