@amemhq/core 1.1.0 → 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.
@@ -31,6 +31,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  // src/cli-migrate.ts
32
32
  var cli_migrate_exports = {};
33
33
  __export(cli_migrate_exports, {
34
+ carried: () => carried,
35
+ deriveTarget: () => deriveTarget,
34
36
  parseArgs: () => parseArgs
35
37
  });
36
38
  module.exports = __toCommonJS(cli_migrate_exports);
@@ -40,9 +42,11 @@ var pipeline = null;
40
42
  var extractor = null;
41
43
  var loadedKey = null;
42
44
  var cachedDim = null;
43
- var DEFAULT_EMBEDDING_MODEL = "Xenova/paraphrase-multilingual-MiniLM-L12-v2";
45
+ var DEFAULT_EMBEDDING_MODEL = "Xenova/bge-m3";
46
+ var DEFAULT_MODEL_DTYPE = "fp16";
47
+ var pinnedModel = null;
44
48
  function getEmbeddingModel() {
45
- return process.env.AMEM_EMBED_MODEL?.trim() || DEFAULT_EMBEDDING_MODEL;
49
+ return process.env.AMEM_EMBED_MODEL?.trim() || pinnedModel || DEFAULT_EMBEDDING_MODEL;
46
50
  }
47
51
  var CLS_POOLED_MODELS = /* @__PURE__ */ new Set([
48
52
  "bge-m3",
@@ -67,7 +71,9 @@ function getEmbeddingDevice() {
67
71
  return process.env.AMEM_EMBED_DEVICE?.trim() || void 0;
68
72
  }
69
73
  function getEmbeddingDtype() {
70
- return process.env.AMEM_EMBED_DTYPE?.trim() || void 0;
74
+ const explicit = process.env.AMEM_EMBED_DTYPE?.trim();
75
+ if (explicit) return explicit;
76
+ return getEmbeddingModel() === DEFAULT_EMBEDDING_MODEL ? DEFAULT_MODEL_DTYPE : void 0;
71
77
  }
72
78
  function extractorKey() {
73
79
  return `${getEmbeddingModel()}|${getEmbeddingDevice() ?? ""}|${getEmbeddingDtype() ?? ""}`;
@@ -202,6 +208,43 @@ async function createCollectionRaw(collection, size) {
202
208
  async function upsertPointsRaw(collection, points) {
203
209
  await qdrant("PUT", `/collections/${collection}/points?wait=true`, { points });
204
210
  }
211
+ async function scrollIdsRaw(collection, limit = 1e4) {
212
+ const ids = /* @__PURE__ */ new Set();
213
+ let offset = void 0;
214
+ for (; ; ) {
215
+ const body = { with_payload: false, with_vector: false, limit };
216
+ if (offset !== void 0 && offset !== null) body.offset = offset;
217
+ const res = await qdrant("POST", `/collections/${collection}/points/scroll`, body);
218
+ for (const p of res.points) ids.add(String(p.id));
219
+ offset = res.next_page_offset;
220
+ if (offset === void 0 || offset === null || res.points.length === 0) break;
221
+ }
222
+ return ids;
223
+ }
224
+ async function deleteCollectionRaw(collection) {
225
+ await qdrant("DELETE", `/collections/${collection}`);
226
+ }
227
+ async function resolveAliasRaw(alias) {
228
+ try {
229
+ const res = await qdrant("GET", `/aliases`);
230
+ return res.aliases.find((a) => a.alias_name === alias)?.collection_name ?? null;
231
+ } catch {
232
+ return null;
233
+ }
234
+ }
235
+ async function createAliasRaw(alias, collection) {
236
+ await qdrant("POST", `/collections/aliases`, {
237
+ actions: [{ create_alias: { collection_name: collection, alias_name: alias } }]
238
+ });
239
+ }
240
+ async function setAliasRaw(alias, collection) {
241
+ await qdrant("POST", `/collections/aliases`, {
242
+ actions: [
243
+ { delete_alias: { alias_name: alias } },
244
+ { create_alias: { collection_name: collection, alias_name: alias } }
245
+ ]
246
+ });
247
+ }
205
248
  function noteToPoint(note) {
206
249
  return {
207
250
  id: note.id,
@@ -795,8 +838,19 @@ async function migrateCollection(opts) {
795
838
  );
796
839
  if (dryRun) {
797
840
  log("[migrate] dry run \u2014 nothing written. Pass dryRun: false to apply.");
798
- return { total: notes.length, missingDerived, refreshed: 0, migrated: 0, sourceDim, targetDim, model, dryRun: true };
841
+ return {
842
+ total: notes.length,
843
+ missingDerived,
844
+ refreshed: 0,
845
+ migrated: 0,
846
+ skipped: 0,
847
+ sourceDim,
848
+ targetDim,
849
+ model,
850
+ dryRun: true
851
+ };
799
852
  }
853
+ let alreadyDone = /* @__PURE__ */ new Set();
800
854
  const existingTargetDim = await collectionDimRaw(to);
801
855
  if (existingTargetDim === null) {
802
856
  await createCollectionRaw(to, targetDim);
@@ -805,9 +859,17 @@ async function migrateCollection(opts) {
805
859
  if (existingTargetDim !== targetDim) {
806
860
  throw new Error(`migrate: target "${to}" exists at ${existingTargetDim}d but the model produces ${targetDim}d`);
807
861
  }
808
- const existingCount = await countPointsRaw(to);
809
- if (existingCount > 0) {
810
- throw new Error(`migrate: target "${to}" already holds ${existingCount} point(s); use an empty collection`);
862
+ const present = await scrollIdsRaw(to);
863
+ if (present.size > 0) {
864
+ const sourceIds = new Set(notes.map((n) => n.id));
865
+ const foreign = [...present].filter((id) => !sourceIds.has(id));
866
+ if (foreign.length > 0) {
867
+ throw new Error(
868
+ `migrate: target "${to}" holds ${foreign.length} point(s) that are not in "${from}" (e.g. ${foreign[0]}). That is not an interrupted migration \u2014 use a different target.`
869
+ );
870
+ }
871
+ alreadyDone = present;
872
+ log(`[migrate] resuming: ${present.size} of ${notes.length} already in ${to}`);
811
873
  }
812
874
  }
813
875
  let refreshed = 0;
@@ -821,6 +883,7 @@ async function migrateCollection(opts) {
821
883
  buffer = [];
822
884
  };
823
885
  for (const note of notes) {
886
+ if (alreadyDone.has(note.id)) continue;
824
887
  if (refreshFields && missingDerivedFields(note)) {
825
888
  try {
826
889
  const built = await llmConstructNote(note.content);
@@ -836,7 +899,7 @@ async function migrateCollection(opts) {
836
899
  buffer.push(point);
837
900
  if (buffer.length >= BATCH) {
838
901
  await flush();
839
- log(`[migrate] ${migrated}/${notes.length}`);
902
+ log(`[migrate] ${alreadyDone.size + migrated}/${notes.length}`);
840
903
  }
841
904
  }
842
905
  await flush();
@@ -844,32 +907,68 @@ async function migrateCollection(opts) {
844
907
  if (finalCount !== notes.length) {
845
908
  warn(`[migrate] target holds ${finalCount} point(s) but the source had ${notes.length} \u2014 check before switching`);
846
909
  }
847
- log(
848
- `[migrate] done: ${migrated} migrated, ${refreshed} re-extracted. "${from}" is untouched \u2014 switch with AMEM_COLLECTION=${to}, and keep the old one until you are satisfied.`
849
- );
850
- return { total: notes.length, missingDerived, refreshed, migrated, sourceDim, targetDim, model, dryRun: false };
910
+ log(`[migrate] done: ${migrated} written, ${alreadyDone.size} already present, ${refreshed} re-extracted.`);
911
+ return {
912
+ total: notes.length,
913
+ missingDerived,
914
+ refreshed,
915
+ migrated,
916
+ skipped: alreadyDone.size,
917
+ sourceDim,
918
+ targetDim,
919
+ model,
920
+ dryRun: false
921
+ };
922
+ }
923
+ async function switchToMigrated(opts) {
924
+ const { name, to } = opts;
925
+ const log = opts.logger?.info ?? ((m) => console.log(m));
926
+ if (name === to) throw new Error(`switch: "${name}" and "${to}" are the same collection`);
927
+ const already = await resolveAliasRaw(name);
928
+ if (already === to) {
929
+ log(`[switch] "${name}" already points at "${to}" \u2014 nothing to do`);
930
+ return { name, to, moved: await countPointsRaw(to) };
931
+ }
932
+ const targetCount = await countPointsRaw(to);
933
+ if (targetCount === 0) throw new Error(`switch: "${to}" is empty \u2014 migrate into it first`);
934
+ if (already === null) {
935
+ const sourceCount = await countPointsRaw(name);
936
+ if (targetCount < sourceCount) {
937
+ throw new Error(
938
+ `switch: "${to}" holds ${targetCount} point(s) but "${name}" still holds ${sourceCount}. The migration is not finished \u2014 run it again before switching.`
939
+ );
940
+ }
941
+ log(`[switch] verified ${targetCount} in "${to}" against ${sourceCount} in "${name}"`);
942
+ await deleteCollectionRaw(name);
943
+ log(`[switch] dropped "${name}"`);
944
+ await createAliasRaw(name, to);
945
+ } else {
946
+ await setAliasRaw(name, to);
947
+ }
948
+ log(`[switch] "${name}" now resolves to "${to}"`);
949
+ return { name, to, moved: targetCount };
851
950
  }
852
951
 
853
952
  // src/cli-migrate.ts
854
- var USAGE = `amem-migrate \u2014 rebuild a collection under the current embedding model
953
+ var USAGE = `amem-migrate \u2014 move a memory store onto a different embedding model
855
954
 
856
- npx --package=@amemhq/core amem-migrate --to <collection> [options]
955
+ amem-migrate what state the store is in, and what comes next
956
+ amem-migrate --apply do the next step; safe to interrupt and re-run
957
+ amem-migrate --switch put the new store behind the old name (irreversible)
857
958
 
858
959
  Options
859
- --to <name> Target collection. Required. Must not already hold points.
860
- --from <name> Source collection. Defaults to AMEM_COLLECTION.
861
- --apply Actually write. Without this it is a dry run.
862
- --no-refresh-fields Skip re-extracting keywords/tags for notes that never
863
- had them. Faster, and makes no LLM calls at all.
960
+ --from-collection <name> the store to migrate. Defaults to AMEM_COLLECTION.
961
+ --to-collection <name> where to build it. Derived from the source if omitted.
962
+ --no-refresh-fields skip re-extracting keywords for notes that never had
963
+ them. Makes the run completely offline.
864
964
  -h, --help
865
965
 
866
- The model is whatever AMEM_EMBED_MODEL says, so set that first:
966
+ Migrates onto amem's current default unless AMEM_EMBED_MODEL says otherwise:
867
967
 
868
- AMEM_EMBED_MODEL=Xenova/bge-m3 \\
869
- npx --package=@amemhq/core amem-migrate --to amem_notes_v2 --apply
968
+ AMEM_EMBED_MODEL=Alibaba-NLP/gte-multilingual-base amem-migrate --apply
870
969
 
871
- The source is only ever read. If the result looks wrong, point AMEM_COLLECTION
872
- back at it and nothing has been lost.`;
970
+ Nothing before --switch touches the original. If a run looks wrong, delete the
971
+ target and start again.`;
873
972
  function parseArgs(argv) {
874
973
  const value = (flag) => {
875
974
  const i = argv.indexOf(flag);
@@ -877,49 +976,102 @@ function parseArgs(argv) {
877
976
  };
878
977
  return {
879
978
  help: argv.includes("-h") || argv.includes("--help"),
880
- to: value("--to"),
881
- from: value("--from"),
882
- dryRun: !argv.includes("--apply"),
979
+ apply: argv.includes("--apply"),
980
+ switchOver: argv.includes("--switch"),
981
+ from: value("--from-collection"),
982
+ to: value("--to-collection"),
883
983
  refreshFields: !argv.includes("--no-refresh-fields")
884
984
  };
885
985
  }
986
+ function deriveTarget(source) {
987
+ const m = source.match(/^(.*)_v(\d+)$/);
988
+ return m ? `${m[1]}_v${Number(m[2]) + 1}` : `${source}_v2`;
989
+ }
990
+ function carried(args) {
991
+ return (args.from ? ` --from-collection ${args.from}` : "") + (args.to ? ` --to-collection ${args.to}` : "");
992
+ }
993
+ async function detect(from, to) {
994
+ const alias = await resolveAliasRaw(from);
995
+ if (alias !== null) return { kind: "switched", points: await countPointsRaw(from) };
996
+ const sourceDim = await collectionDimRaw(from);
997
+ if (sourceDim === null) return { kind: "no-source" };
998
+ const modelDim = await getEmbeddingDim();
999
+ if (sourceDim === modelDim) return { kind: "already-current", model: getEmbeddingModel() };
1000
+ const notes = await countPointsRaw(from);
1001
+ const targetDim = await collectionDimRaw(to);
1002
+ if (targetDim === null) return { kind: "not-started", notes };
1003
+ const done = (await scrollIdsRaw(to)).size;
1004
+ return done >= notes ? { kind: "ready-to-switch", notes } : { kind: "partial", done, notes };
1005
+ }
886
1006
  async function main() {
887
1007
  const args = parseArgs(process.argv.slice(2));
888
1008
  if (args.help) {
889
1009
  console.log(USAGE);
890
1010
  return;
891
1011
  }
892
- const to = args.to;
893
- if (!to) {
894
- console.error("amem-migrate: --to <collection> is required\n");
895
- console.error(USAGE);
896
- process.exitCode = 1;
897
- return;
898
- }
899
1012
  const from = args.from ?? getCollection();
900
- const { dryRun, refreshFields } = args;
1013
+ const to = args.to ?? deriveTarget(from);
1014
+ const flags = carried(args);
1015
+ const phase = await detect(from, to);
1016
+ console.log(`store: ${from}`);
901
1017
  console.log(`model: ${getEmbeddingModel()}`);
902
- console.log(`from: ${from}`);
903
- console.log(`to: ${to}`);
904
- console.log(`mode: ${dryRun ? "dry run \u2014 nothing will be written" : "APPLY"}
905
- `);
906
- const result = await migrateCollection({ from, to, dryRun, refreshFields });
907
- console.log(`
908
- ${result.total} notes in source (${result.sourceDim}-dim)`);
909
- console.log(`target is ${result.targetDim}-dim`);
910
- if (result.missingDerived > 0) {
911
- console.log(
912
- `${result.missingDerived} note(s) predate the extraction pipeline` + (refreshFields ? `, ${result.refreshed} re-extracted` : " \u2014 skipped (--no-refresh-fields)")
913
- );
1018
+ switch (phase.kind) {
1019
+ case "no-source":
1020
+ console.error(`
1021
+ No collection named "${from}". Nothing to migrate.`);
1022
+ process.exitCode = 1;
1023
+ return;
1024
+ case "switched":
1025
+ console.log(`
1026
+ "${from}" is already an alias \u2014 ${phase.points} notes, nothing to do.`);
1027
+ return;
1028
+ case "already-current":
1029
+ console.log(`
1030
+ Already on ${phase.model}. Nothing to migrate.`);
1031
+ return;
1032
+ case "not-started":
1033
+ if (!args.apply) {
1034
+ console.log(`
1035
+ ${phase.notes} notes to rebuild into "${to}".`);
1036
+ console.log(`Run "amem-migrate${flags} --apply" to start. "${from}" is only read.`);
1037
+ return;
1038
+ }
1039
+ break;
1040
+ case "partial":
1041
+ if (!args.apply) {
1042
+ console.log(`
1043
+ ${phase.done} of ${phase.notes} rebuilt into "${to}".`);
1044
+ console.log(`Run "amem-migrate${flags} --apply" to carry on from there.`);
1045
+ return;
1046
+ }
1047
+ break;
1048
+ case "ready-to-switch":
1049
+ if (!args.switchOver) {
1050
+ console.log(`
1051
+ All ${phase.notes} notes are in "${to}". "${from}" is untouched.`);
1052
+ console.log(`Check it, then run "amem-migrate${flags} --switch" to put "${to}" behind the name "${from}".`);
1053
+ console.log(`That drops "${from}" and cannot be undone.`);
1054
+ return;
1055
+ }
1056
+ await switchToMigrated({ name: from, to });
1057
+ console.log(`
1058
+ Done. Nothing to change in your config \u2014 "${from}" now resolves to "${to}".`);
1059
+ return;
914
1060
  }
915
- if (dryRun) {
916
- console.log(`
917
- Nothing was written. Re-run with --apply to migrate.`);
1061
+ if (args.switchOver) {
1062
+ console.error(`
1063
+ Not finished yet \u2014 run "amem-migrate${flags} --apply" until it is before switching.`);
1064
+ process.exitCode = 1;
918
1065
  return;
919
1066
  }
1067
+ const result = await migrateCollection({ from, to, dryRun: false, refreshFields: args.refreshFields });
920
1068
  console.log(`
921
- ${result.migrated} notes written to "${to}".`);
922
- console.log(`Point AMEM_COLLECTION at "${to}" to start using it. "${from}" is untouched.`);
1069
+ ${result.migrated + result.skipped} of ${result.total} rebuilt.`);
1070
+ if (result.migrated + result.skipped >= result.total) {
1071
+ console.log(`Check "${to}", then run "amem-migrate${flags} --switch".`);
1072
+ } else {
1073
+ console.log(`Run "amem-migrate${flags} --apply" again to carry on.`);
1074
+ }
923
1075
  }
924
1076
  main().catch((err) => {
925
1077
  console.error(`amem-migrate: ${err instanceof Error ? err.message : String(err)}`);
@@ -927,6 +1079,8 @@ main().catch((err) => {
927
1079
  });
928
1080
  // Annotate the CommonJS export names for ESM import in node:
929
1081
  0 && (module.exports = {
1082
+ carried,
1083
+ deriveTarget,
930
1084
  parseArgs
931
1085
  });
932
1086
  //# sourceMappingURL=cli-migrate.cjs.map