memorix 1.8.8 → 1.9.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.
Files changed (66) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +15 -1
  3. package/README.zh-CN.md +15 -1
  4. package/dist/cli/index.js +3722 -1599
  5. package/dist/cli/memcode.js +0 -0
  6. package/dist/index.js +634 -363
  7. package/dist/maintenance-runner.js +285 -103
  8. package/dist/media-audio-runner.js +285 -103
  9. package/dist/media-video-runner.js +285 -103
  10. package/dist/memcode-runtime/CHANGELOG.md +36 -0
  11. package/dist/sdk.js +627 -356
  12. package/dist/vector-backfill-runner.d.ts +4 -1
  13. package/dist/vector-backfill-runner.js +288 -104
  14. package/docs/AGENT_OPERATOR_PLAYBOOK.md +4 -2
  15. package/docs/API_REFERENCE.md +14 -0
  16. package/docs/CONFIGURATION.md +46 -0
  17. package/docs/DEVELOPMENT.md +2 -2
  18. package/docs/INTEGRATIONS.md +1 -0
  19. package/docs/MULTI-DEVICE-SYNC-RESEARCH.md +344 -0
  20. package/docs/PERFORMANCE.md +32 -0
  21. package/docs/README.md +4 -2
  22. package/docs/hooks-architecture.md +18 -13
  23. package/llms-full.txt +4 -1
  24. package/llms.txt +3 -1
  25. package/package.json +2 -1
  26. package/plugins/antigravity/memorix/plugin.json +1 -1
  27. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  28. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  29. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  30. package/plugins/copilot/memorix/plugin.json +1 -1
  31. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  32. package/plugins/hermes/memorix/plugin.yaml +1 -1
  33. package/plugins/omp/memorix/package.json +1 -1
  34. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  35. package/plugins/pi/memorix/package.json +1 -1
  36. package/server.json +2 -2
  37. package/src/cli/command-guide.ts +9 -1
  38. package/src/cli/commands/agent-integrations.ts +2 -1
  39. package/src/cli/commands/hooks-install.ts +2 -0
  40. package/src/cli/commands/integrate.ts +2 -0
  41. package/src/cli/commands/serve-http.ts +11 -0
  42. package/src/cli/commands/setup.ts +11 -4
  43. package/src/cli/commands/sync-store.ts +191 -0
  44. package/src/cli/commands/sync.ts +8 -0
  45. package/src/hooks/handler.ts +6 -0
  46. package/src/hooks/installers/index.ts +181 -6
  47. package/src/hooks/normalizer.ts +60 -1
  48. package/src/hooks/types.ts +3 -1
  49. package/src/integrations/registry.ts +9 -0
  50. package/src/memory/observations.ts +15 -2
  51. package/src/runtime/maintenance-targets.ts +6 -0
  52. package/src/runtime/vector-backfill-runner.ts +185 -7
  53. package/src/store/orama-store.ts +85 -102
  54. package/src/sync/adapters/fs.ts +179 -0
  55. package/src/sync/adapters/github.ts +258 -0
  56. package/src/sync/adapters/object-store.ts +267 -0
  57. package/src/sync/adapters/postgres.ts +269 -0
  58. package/src/sync/engine.ts +353 -0
  59. package/src/sync/journal.ts +162 -0
  60. package/src/sync/merge.ts +116 -0
  61. package/src/sync/namespace.ts +15 -0
  62. package/src/sync/paging.ts +45 -0
  63. package/src/sync/policy.ts +37 -0
  64. package/src/sync/remote-factory.ts +49 -0
  65. package/src/sync/store-port.ts +288 -0
  66. package/src/sync/types.ts +183 -0
@@ -6490,6 +6490,14 @@ __export(orama_store_exports, {
6490
6490
  updateObservationMetadata: () => updateObservationMetadata
6491
6491
  });
6492
6492
  import { create, insert as insert2, insertMultiple, search, remove as remove2, update, count, getByID } from "@orama/orama";
6493
+ function rememberSearchMode(key, value) {
6494
+ if (!lastSearchModeByProject.has(key) && lastSearchModeByProject.size >= MAX_SEARCH_MODE_KEYS) {
6495
+ const oldest = lastSearchModeByProject.keys().next().value;
6496
+ if (oldest !== void 0) lastSearchModeByProject.delete(oldest);
6497
+ }
6498
+ lastSearchModeByProject.delete(key);
6499
+ lastSearchModeByProject.set(key, value);
6500
+ }
6493
6501
  function getLastSearchMode(projectId) {
6494
6502
  return lastSearchModeByProject.get(projectId ?? SEARCH_MODE_DEFAULT_KEY) ?? "fulltext";
6495
6503
  }
@@ -6502,9 +6510,6 @@ function makeEntryKey(projectId, observationId2) {
6502
6510
  function rememberObservationDoc(doc) {
6503
6511
  const publicDoc = { ...doc };
6504
6512
  delete publicDoc.embedding;
6505
- if (doc.projectId && typeof doc.observationId === "number") {
6506
- docByObservationKey.set(makeEntryKey(doc.projectId, doc.observationId), publicDoc);
6507
- }
6508
6513
  return publicDoc;
6509
6514
  }
6510
6515
  function isCommandLikeQuery(query) {
@@ -6608,7 +6613,6 @@ async function resetDb() {
6608
6613
  indexEmbeddingProvider = null;
6609
6614
  deferredCachedVectorHydration = null;
6610
6615
  lastSearchModeByProject.clear();
6611
- docByObservationKey.clear();
6612
6616
  }
6613
6617
  function isEmbeddingEnabled() {
6614
6618
  return embeddingEnabled;
@@ -6671,78 +6675,72 @@ function isCompatibleCachedVector(vector) {
6671
6675
  vector && embeddingDimensions !== null && vector.length === embeddingDimensions && vector.every(Number.isFinite)
6672
6676
  );
6673
6677
  }
6674
- async function attachCachedVectors(database, candidates) {
6675
- const cachedVectors = await getCachedEmbeddings(
6676
- candidates.map(({ observation }) => observationEmbeddingText(observation))
6677
- );
6678
- if (db !== database) return;
6679
- for (let index = 0; index < candidates.length; index++) {
6680
- const vector = cachedVectors[index];
6681
- if (!isCompatibleCachedVector(vector)) continue;
6682
- const existing = getByID(database, candidates[index].id);
6683
- if (!existing || documentEmbeddingText(existing) !== observationEmbeddingText(candidates[index].observation)) continue;
6684
- try {
6685
- await update(database, candidates[index].id, { ...existing, embedding: vector });
6686
- } catch {
6687
- }
6688
- if (index % 50 === 0) {
6689
- await new Promise((resolve2) => setImmediate(resolve2));
6678
+ async function attachCachedVectors(database, observations2) {
6679
+ const batchSize = 200;
6680
+ for (let start = 0; start < observations2.length; start += batchSize) {
6681
+ const batch = observations2.slice(start, start + batchSize);
6682
+ const cachedVectors = await getCachedEmbeddings(batch.map(observationEmbeddingText));
6683
+ if (db !== database) return;
6684
+ for (let index = 0; index < batch.length; index++) {
6685
+ const vector = cachedVectors[index];
6686
+ if (!isCompatibleCachedVector(vector)) continue;
6687
+ const id = makeOramaObservationId(batch[index].projectId, batch[index].id);
6688
+ const existing = getByID(database, id);
6689
+ if (!existing || documentEmbeddingText(existing) !== observationEmbeddingText(batch[index])) continue;
6690
+ try {
6691
+ await update(database, id, { ...existing, embedding: vector });
6692
+ } catch {
6693
+ }
6690
6694
  }
6695
+ await new Promise((resolve2) => setImmediate(resolve2));
6691
6696
  }
6692
6697
  }
6693
6698
  async function hydrateIndex(observations2, options = {}) {
6694
6699
  const database = await getDb({ allowNetworkProbe: options.allowNetworkProbe });
6695
- const candidates = [];
6696
- for (const observation of observations2) {
6697
- if (!observation || !observation.id || !observation.projectId) continue;
6698
- const id = makeOramaObservationId(observation.projectId, observation.id);
6699
- if (getByID(database, id)) continue;
6700
- candidates.push({ observation, id });
6701
- }
6702
6700
  const deferCachedVectors = options.deferCachedVectors === true && Boolean(indexEmbeddingProvider?.getCachedEmbeddings);
6703
- const cachedVectors = deferCachedVectors ? candidates.map(() => null) : await getCachedEmbeddings(candidates.map(({ observation }) => observationEmbeddingText(observation)));
6704
- const documents = [];
6705
- for (let index = 0; index < candidates.length; index++) {
6706
- const { observation: obs, id } = candidates[index];
6707
- try {
6708
- const vector = cachedVectors[index];
6709
- const compatibleVector = isCompatibleCachedVector(vector) ? vector : null;
6710
- const doc = {
6711
- id,
6712
- observationId: obs.id,
6713
- entityName: obs.entityName || "",
6714
- type: obs.type || "discovery",
6715
- title: obs.title || "",
6716
- narrative: obs.narrative || "",
6717
- facts: Array.isArray(obs.facts) ? obs.facts.join(" ") : "",
6718
- filesModified: Array.isArray(obs.filesModified) ? obs.filesModified.join(" ") : "",
6719
- concepts: Array.isArray(obs.concepts) ? obs.concepts.join(" ") : "",
6720
- tokens: obs.tokens ?? 0,
6721
- createdAt: obs.createdAt || "",
6722
- projectId: obs.projectId,
6723
- accessCount: obs.accessCount ?? 0,
6724
- lastAccessedAt: obs.lastAccessedAt || "",
6725
- status: obs.status ?? "active",
6726
- source: obs.source || "agent",
6727
- sourceDetail: obs.sourceDetail ?? "",
6728
- valueCategory: obs.valueCategory ?? "",
6729
- admissionState: obs.admissionState ?? "",
6730
- admissionReason: obs.admissionReason ?? "",
6731
- visibility: obs.visibility ?? "project",
6732
- createdByAgentId: obs.createdByAgentId ?? "",
6733
- sharedWithAgentIds: JSON.stringify(obs.sharedWithAgentIds ?? []),
6734
- documentType: "observation",
6735
- knowledgeLayer: resolveKnowledgeLayer("observation", obs.sourceDetail, obs.source),
6736
- ...compatibleVector ? { embedding: compatibleVector } : {}
6737
- };
6738
- documents.push(doc);
6739
- } catch {
6740
- }
6741
- }
6742
6701
  let inserted = 0;
6743
6702
  const hydrationBatchSize = 200;
6744
- for (let start = 0; start < documents.length; start += hydrationBatchSize) {
6745
- const batch = documents.slice(start, start + hydrationBatchSize);
6703
+ for (let start = 0; start < observations2.length; start += hydrationBatchSize) {
6704
+ const batchObservations = observations2.slice(start, start + hydrationBatchSize).filter((observation) => observation && observation.id && observation.projectId).filter((observation) => !getByID(database, makeOramaObservationId(observation.projectId, observation.id)));
6705
+ if (batchObservations.length === 0) continue;
6706
+ const cachedVectors = deferCachedVectors || options.skipCachedVectors ? batchObservations.map(() => null) : await getCachedEmbeddings(batchObservations.map(observationEmbeddingText));
6707
+ const batch = [];
6708
+ for (let index = 0; index < batchObservations.length; index++) {
6709
+ const obs = batchObservations[index];
6710
+ try {
6711
+ const compatibleVector = isCompatibleCachedVector(cachedVectors[index]) ? cachedVectors[index] : null;
6712
+ batch.push({
6713
+ id: makeOramaObservationId(obs.projectId, obs.id),
6714
+ observationId: obs.id,
6715
+ entityName: obs.entityName || "",
6716
+ type: obs.type || "discovery",
6717
+ title: obs.title || "",
6718
+ narrative: obs.narrative || "",
6719
+ facts: Array.isArray(obs.facts) ? obs.facts.join(" ") : "",
6720
+ filesModified: Array.isArray(obs.filesModified) ? obs.filesModified.join(" ") : "",
6721
+ concepts: Array.isArray(obs.concepts) ? obs.concepts.join(" ") : "",
6722
+ tokens: obs.tokens ?? 0,
6723
+ createdAt: obs.createdAt || "",
6724
+ projectId: obs.projectId,
6725
+ accessCount: obs.accessCount ?? 0,
6726
+ lastAccessedAt: obs.lastAccessedAt || "",
6727
+ status: obs.status ?? "active",
6728
+ source: obs.source || "agent",
6729
+ sourceDetail: obs.sourceDetail ?? "",
6730
+ valueCategory: obs.valueCategory ?? "",
6731
+ admissionState: obs.admissionState ?? "",
6732
+ admissionReason: obs.admissionReason ?? "",
6733
+ visibility: obs.visibility ?? "project",
6734
+ createdByAgentId: obs.createdByAgentId ?? "",
6735
+ sharedWithAgentIds: JSON.stringify(obs.sharedWithAgentIds ?? []),
6736
+ documentType: "observation",
6737
+ knowledgeLayer: resolveKnowledgeLayer("observation", obs.sourceDetail, obs.source),
6738
+ ...compatibleVector ? { embedding: compatibleVector } : {}
6739
+ });
6740
+ } catch {
6741
+ }
6742
+ }
6743
+ if (batch.length === 0) continue;
6746
6744
  try {
6747
6745
  await insertMultiple(database, batch, batch.length);
6748
6746
  for (const doc of batch) rememberObservationDoc(doc);
@@ -6764,7 +6762,7 @@ async function hydrateIndex(observations2, options = {}) {
6764
6762
  }
6765
6763
  await new Promise((resolve2) => setImmediate(resolve2));
6766
6764
  }
6767
- deferredCachedVectorHydration = options.skipCachedVectors ? null : deferCachedVectors ? attachCachedVectors(database, candidates).catch(() => {
6765
+ deferredCachedVectorHydration = options.skipCachedVectors ? null : deferCachedVectors ? attachCachedVectors(database, observations2).catch(() => {
6768
6766
  }) : null;
6769
6767
  return inserted;
6770
6768
  }
@@ -6801,12 +6799,6 @@ async function updateObservationMetadata(projectId, observationId2, patch) {
6801
6799
  async function removeObservation(oramaId) {
6802
6800
  const database = await getDb();
6803
6801
  await remove2(database, oramaId);
6804
- for (const [key, doc] of docByObservationKey.entries()) {
6805
- if (doc.id === oramaId) {
6806
- docByObservationKey.delete(key);
6807
- break;
6808
- }
6809
- }
6810
6802
  }
6811
6803
  async function searchObservations(options) {
6812
6804
  const perf = !!process.env.MEMORIX_PERF;
@@ -6821,7 +6813,7 @@ async function searchObservations(options) {
6821
6813
  const modeKey = options.projectId ?? SEARCH_MODE_DEFAULT_KEY;
6822
6814
  const quality = options.quality ?? "balanced";
6823
6815
  const database = await getDb();
6824
- lastSearchModeByProject.set(
6816
+ rememberSearchMode(
6825
6817
  modeKey,
6826
6818
  quality === "fast" ? "fulltext (fast profile)" : embeddingEnabled ? "hybrid" : "fulltext"
6827
6819
  );
@@ -6888,7 +6880,7 @@ async function searchObservations(options) {
6888
6880
  if (provider2) {
6889
6881
  const activeVectorDimensions = getVectorDimensions();
6890
6882
  if (activeVectorDimensions !== null && provider2.dimensions !== activeVectorDimensions) {
6891
- lastSearchModeByProject.set(
6883
+ rememberSearchMode(
6892
6884
  modeKey,
6893
6885
  `fulltext (embedding dimension mismatch: provider ${provider2.dimensions}d vs index ${activeVectorDimensions}d)`
6894
6886
  );
@@ -6913,7 +6905,7 @@ async function searchObservations(options) {
6913
6905
  mark("embedding");
6914
6906
  const cjkRatio = (originalQuery.match(/[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/g) || []).length / originalQuery.length;
6915
6907
  const isCJKHeavy = cjkRatio > 0.3;
6916
- lastSearchModeByProject.set(modeKey, "hybrid");
6908
+ rememberSearchMode(modeKey, "hybrid");
6917
6909
  searchParams = {
6918
6910
  ...searchParams,
6919
6911
  mode: "hybrid",
@@ -6929,7 +6921,7 @@ async function searchObservations(options) {
6929
6921
  }
6930
6922
  }
6931
6923
  } catch (error) {
6932
- lastSearchModeByProject.set(modeKey, "fulltext (embedding unavailable)");
6924
+ rememberSearchMode(modeKey, "fulltext (embedding unavailable)");
6933
6925
  console.error("[memorix] Embedding failed or timed out, falling back to fulltext search");
6934
6926
  }
6935
6927
  }
@@ -6939,7 +6931,7 @@ async function searchObservations(options) {
6939
6931
  results = await search(database, searchParams);
6940
6932
  } catch (error) {
6941
6933
  if (queryVector && isVectorDimensionMismatchError(error)) {
6942
- lastSearchModeByProject.set(modeKey, "fulltext (embedding dimension mismatch)");
6934
+ rememberSearchMode(modeKey, "fulltext (embedding dimension mismatch)");
6943
6935
  console.error("[memorix] Vector search dimension mismatch detected, retrying without embeddings");
6944
6936
  results = await search(database, stripVectorSearchParams(searchParams));
6945
6937
  } else {
@@ -6961,7 +6953,7 @@ async function searchObservations(options) {
6961
6953
  },
6962
6954
  similarity: 0.25
6963
6955
  };
6964
- lastSearchModeByProject.set(modeKey, "vector-only (hybrid empty fallback)");
6956
+ rememberSearchMode(modeKey, "vector-only (hybrid empty fallback)");
6965
6957
  results = await search(database, vectorOnlyParams);
6966
6958
  } catch {
6967
6959
  }
@@ -7202,7 +7194,7 @@ async function searchObservations(options) {
7202
7194
  const rerankedTop = neural.map((row) => candidateMap.get(row.id)).filter((entry) => entry != null);
7203
7195
  if (rerankedTop.length > 0) {
7204
7196
  intermediate = [...rerankedTop, ...intermediate.slice(RERANK_TOP_K)];
7205
- lastSearchModeByProject.set(modeKey, (lastSearchModeByProject.get(modeKey) ?? "fulltext") + " + neural rerank");
7197
+ rememberSearchMode(modeKey, (lastSearchModeByProject.get(modeKey) ?? "fulltext") + " + neural rerank");
7206
7198
  mark("rerank(usedNeural=true)");
7207
7199
  }
7208
7200
  }
@@ -7219,7 +7211,7 @@ async function searchObservations(options) {
7219
7211
  );
7220
7212
  mark(`rerank(usedLLM=${usedLLM})`);
7221
7213
  if (usedLLM) {
7222
- lastSearchModeByProject.set(modeKey, (lastSearchModeByProject.get(modeKey) ?? "fulltext") + " + LLM rerank");
7214
+ rememberSearchMode(modeKey, (lastSearchModeByProject.get(modeKey) ?? "fulltext") + " + LLM rerank");
7223
7215
  const rerankedTop = reranked.map((row) => candidateMap.get(row.id)).filter((entry) => entry != null);
7224
7216
  if (rerankedTop.length > 0) {
7225
7217
  intermediate = [...rerankedTop, ...intermediate.slice(RERANK_TOP_K)];
@@ -7292,13 +7284,6 @@ async function getObservationsByIds(ids, projectId) {
7292
7284
  const database = await getDb();
7293
7285
  const results = [];
7294
7286
  for (const id of ids) {
7295
- if (projectId) {
7296
- const cached = docByObservationKey.get(makeEntryKey(projectId, id));
7297
- if (cached) {
7298
- results.push(cached);
7299
- continue;
7300
- }
7301
- }
7302
7287
  const searchResult = await search(database, {
7303
7288
  term: "",
7304
7289
  includeVectors: true,
@@ -7398,7 +7383,7 @@ function formatTime(isoDate) {
7398
7383
  return isoDate;
7399
7384
  }
7400
7385
  }
7401
- var db, dbInitPromise, dbGeneration, embeddingEnabled, embeddingDimensions, indexEmbeddingProvider, deferredCachedVectorHydration, docByObservationKey, NON_CJK_HYBRID_SIMILARITY, lastSearchModeByProject, SEARCH_MODE_DEFAULT_KEY, COMMAND_LOG_TITLE, COMMAND_STYLE_TITLE, COMMAND_LIKE_QUERY2, COMMAND_INTENT_QUERY, NATURAL_LANGUAGE_MARKERS;
7386
+ var db, dbInitPromise, dbGeneration, embeddingEnabled, embeddingDimensions, indexEmbeddingProvider, deferredCachedVectorHydration, NON_CJK_HYBRID_SIMILARITY, lastSearchModeByProject, SEARCH_MODE_DEFAULT_KEY, MAX_SEARCH_MODE_KEYS, COMMAND_LOG_TITLE, COMMAND_STYLE_TITLE, COMMAND_LIKE_QUERY2, COMMAND_INTENT_QUERY, NATURAL_LANGUAGE_MARKERS;
7402
7387
  var init_orama_store = __esm({
7403
7388
  "src/store/orama-store.ts"() {
7404
7389
  "use strict";
@@ -7419,10 +7404,10 @@ var init_orama_store = __esm({
7419
7404
  embeddingDimensions = null;
7420
7405
  indexEmbeddingProvider = null;
7421
7406
  deferredCachedVectorHydration = null;
7422
- docByObservationKey = /* @__PURE__ */ new Map();
7423
7407
  NON_CJK_HYBRID_SIMILARITY = 0.45;
7424
7408
  lastSearchModeByProject = /* @__PURE__ */ new Map();
7425
7409
  SEARCH_MODE_DEFAULT_KEY = "__global__";
7410
+ MAX_SEARCH_MODE_KEYS = 4096;
7426
7411
  COMMAND_LOG_TITLE = /^(Ran:|Command:|Executed:)\s/i;
7427
7412
  COMMAND_STYLE_TITLE = /(\bfindstr\b|\bSelect-String\b|\bGet-Content\b|\bnpx\s+vitest\b|\bnpx\s+tsc\b|\b2>&1\b)/i;
7428
7413
  COMMAND_LIKE_QUERY2 = /\b(git|npm|npx|pnpm|yarn|node|bash|powershell|curl|memorix)\b/i;
@@ -8322,6 +8307,56 @@ var init_evidence_store = __esm({
8322
8307
  }
8323
8308
  });
8324
8309
 
8310
+ // src/runtime/maintenance-targets.ts
8311
+ function rowToTarget(row) {
8312
+ return {
8313
+ projectId: row.project_id,
8314
+ projectRoot: row.project_root,
8315
+ dataDir: row.data_dir,
8316
+ updatedAt: Number(row.updated_at)
8317
+ };
8318
+ }
8319
+ var MaintenanceTargetStore;
8320
+ var init_maintenance_targets = __esm({
8321
+ "src/runtime/maintenance-targets.ts"() {
8322
+ "use strict";
8323
+ init_esm_shims();
8324
+ init_sqlite_db();
8325
+ MaintenanceTargetStore = class {
8326
+ db;
8327
+ constructor(dataDir) {
8328
+ this.db = getDatabase(dataDir);
8329
+ }
8330
+ register(input) {
8331
+ if (!input.projectId.trim()) throw new Error("Maintenance target requires a project ID");
8332
+ if (!input.projectRoot.trim()) throw new Error("Maintenance target requires a project root");
8333
+ if (!input.dataDir.trim()) throw new Error("Maintenance target requires a data directory");
8334
+ const now3 = input.now ?? Date.now();
8335
+ this.db.prepare(`
8336
+ INSERT INTO maintenance_targets (project_id, project_root, data_dir, updated_at)
8337
+ VALUES (?, ?, ?, ?)
8338
+ ON CONFLICT(project_id) DO UPDATE SET
8339
+ project_root = excluded.project_root,
8340
+ data_dir = excluded.data_dir,
8341
+ updated_at = excluded.updated_at
8342
+ `).run(input.projectId, input.projectRoot, input.dataDir, now3);
8343
+ return this.get(input.projectId);
8344
+ }
8345
+ get(projectId) {
8346
+ const row = this.db.prepare(`
8347
+ SELECT * FROM maintenance_targets WHERE project_id = ?
8348
+ `).get(projectId);
8349
+ return row ? rowToTarget(row) : void 0;
8350
+ }
8351
+ list() {
8352
+ return this.db.prepare(`
8353
+ SELECT * FROM maintenance_targets ORDER BY updated_at DESC
8354
+ `).all().map(rowToTarget);
8355
+ }
8356
+ };
8357
+ }
8358
+ });
8359
+
8325
8360
  // src/runtime/isolated-maintenance.ts
8326
8361
  import { spawn } from "child_process";
8327
8362
  import { existsSync as existsSync8 } from "fs";
@@ -150037,10 +150072,21 @@ __export(vector_backfill_runner_exports, {
150037
150072
  launchDetachedVectorBackfill: () => launchDetachedVectorBackfill,
150038
150073
  main: () => main,
150039
150074
  parseVectorBackfillRequest: () => parseVectorBackfillRequest,
150040
- resolveVectorBackfillRunnerPath: () => resolveVectorBackfillRunnerPath
150075
+ releaseVectorBackfillLock: () => releaseVectorBackfillLock,
150076
+ resolveVectorBackfillRunnerPath: () => resolveVectorBackfillRunnerPath,
150077
+ vectorBackfillLockPath: () => vectorBackfillLockPath
150041
150078
  });
150042
150079
  import { spawn as spawn4 } from "child_process";
150043
- import { existsSync as existsSync12 } from "fs";
150080
+ import {
150081
+ closeSync,
150082
+ existsSync as existsSync12,
150083
+ mkdirSync as mkdirSync2,
150084
+ openSync,
150085
+ readFileSync as readFileSync9,
150086
+ statSync as statSync4,
150087
+ unlinkSync,
150088
+ writeFileSync as writeFileSync2
150089
+ } from "fs";
150044
150090
  import path21 from "path";
150045
150091
  import { fileURLToPath as fileURLToPath5 } from "url";
150046
150092
  function isNonEmptyString(value) {
@@ -150067,10 +150113,104 @@ function parseVectorBackfillRequest(raw) {
150067
150113
  dataDir: value.dataDir
150068
150114
  };
150069
150115
  }
150116
+ function vectorBackfillLockPath(dataDir) {
150117
+ return path21.join(dataDir, VECTOR_BACKFILL_LOCK_FILE);
150118
+ }
150119
+ function processIsAlive(pid) {
150120
+ if (!Number.isSafeInteger(pid) || pid == null || pid <= 0) return false;
150121
+ try {
150122
+ process.kill(pid, 0);
150123
+ return true;
150124
+ } catch (error) {
150125
+ return error.code === "EPERM";
150126
+ }
150127
+ }
150128
+ function readVectorBackfillLock(lockPath) {
150129
+ try {
150130
+ const value = JSON.parse(readFileSync9(lockPath, "utf8"));
150131
+ if (typeof value.parentPid !== "number" || !Number.isSafeInteger(value.parentPid) || typeof value.startedAt !== "number" || !Number.isFinite(value.startedAt) || typeof value.projectId !== "string") return void 0;
150132
+ return {
150133
+ parentPid: value.parentPid,
150134
+ ...typeof value.childPid === "number" ? { childPid: value.childPid } : {},
150135
+ startedAt: value.startedAt,
150136
+ projectId: value.projectId
150137
+ };
150138
+ } catch {
150139
+ return void 0;
150140
+ }
150141
+ }
150142
+ function writeVectorBackfillLock(lockPath, value) {
150143
+ writeFileSync2(lockPath, `${JSON.stringify(value)}
150144
+ `, "utf8");
150145
+ }
150146
+ function acquireVectorBackfillLock(dataDir, projectId, preferredPath) {
150147
+ const lockPath = preferredPath ?? vectorBackfillLockPath(dataDir);
150148
+ mkdirSync2(path21.dirname(lockPath), { recursive: true });
150149
+ const initial = {
150150
+ parentPid: process.pid,
150151
+ startedAt: Date.now(),
150152
+ projectId
150153
+ };
150154
+ for (let attempt = 0; attempt < 2; attempt++) {
150155
+ try {
150156
+ const fd = openSync(lockPath, "wx");
150157
+ try {
150158
+ writeFileSync2(fd, `${JSON.stringify(initial)}
150159
+ `, "utf8");
150160
+ } finally {
150161
+ closeSync(fd);
150162
+ }
150163
+ return {
150164
+ path: lockPath,
150165
+ setChildPid(pid) {
150166
+ try {
150167
+ writeVectorBackfillLock(lockPath, { ...initial, childPid: pid });
150168
+ } catch {
150169
+ }
150170
+ },
150171
+ release() {
150172
+ try {
150173
+ unlinkSync(lockPath);
150174
+ } catch {
150175
+ }
150176
+ }
150177
+ };
150178
+ } catch (error) {
150179
+ if (error.code !== "EEXIST") return void 0;
150180
+ const existing = readVectorBackfillLock(lockPath);
150181
+ const ownerAlive = existing && (processIsAlive(existing.childPid) || processIsAlive(existing.parentPid));
150182
+ if (ownerAlive) return void 0;
150183
+ let stale = false;
150184
+ try {
150185
+ stale = Date.now() - statSync4(lockPath).mtimeMs >= STALE_LOCK_RECOVERY_MS;
150186
+ } catch {
150187
+ stale = false;
150188
+ }
150189
+ if (!existing && !stale) return void 0;
150190
+ try {
150191
+ unlinkSync(lockPath);
150192
+ } catch {
150193
+ return void 0;
150194
+ }
150195
+ }
150196
+ }
150197
+ return void 0;
150198
+ }
150199
+ function releaseVectorBackfillLock(lockPath = process.env.MEMORIX_VECTOR_BACKFILL_LOCK) {
150200
+ if (!lockPath) return;
150201
+ const lock = readVectorBackfillLock(lockPath);
150202
+ if (lock && lock.childPid !== process.pid) return;
150203
+ try {
150204
+ unlinkSync(lockPath);
150205
+ } catch {
150206
+ }
150207
+ }
150070
150208
  function launchDetachedVectorBackfill(request, options = {}) {
150071
150209
  const runnerPath = options.runnerPath ?? resolveVectorBackfillRunnerPath();
150072
150210
  const exists = options.exists ?? existsSync12;
150073
150211
  if (!exists(runnerPath)) return false;
150212
+ const lock = acquireVectorBackfillLock(request.dataDir, request.projectId, options.lockPath);
150213
+ if (!lock) return false;
150074
150214
  try {
150075
150215
  const child = (options.spawn ?? spawn4)(process.execPath, [runnerPath], {
150076
150216
  cwd: request.projectRoot,
@@ -150080,14 +150220,23 @@ function launchDetachedVectorBackfill(request, options = {}) {
150080
150220
  stdio: "ignore",
150081
150221
  // The request contains only local project metadata, never credentials.
150082
150222
  // Environment transport avoids a live stdin pipe keeping the CLI alive.
150083
- env: { ...process.env, MEMORIX_VECTOR_BACKFILL_REQUEST: JSON.stringify(request) },
150223
+ env: {
150224
+ ...process.env,
150225
+ MEMORIX_VECTOR_BACKFILL_REQUEST: JSON.stringify(request),
150226
+ MEMORIX_VECTOR_BACKFILL_LOCK: lock.path
150227
+ },
150084
150228
  windowsHide: true
150085
150229
  });
150230
+ if (child.pid) lock.setChildPid(child.pid);
150231
+ else lock.release();
150086
150232
  child.once?.("error", () => {
150087
150233
  });
150234
+ child.once?.("close", () => lock.release());
150235
+ if (typeof child.once !== "function") lock.release();
150088
150236
  child.unref();
150089
- return true;
150237
+ return Boolean(child.pid);
150090
150238
  } catch {
150239
+ lock.release();
150091
150240
  return false;
150092
150241
  }
150093
150242
  }
@@ -150099,12 +150248,32 @@ async function executeVectorBackfill(request) {
150099
150248
  await prepareSearchIndex();
150100
150249
  await getDeferredCachedVectorHydration()?.catch(() => {
150101
150250
  });
150251
+ const queue = new MaintenanceJobStore(request.dataDir);
150252
+ const targets = new MaintenanceTargetStore(request.dataDir);
150253
+ let preparedRoot = request.projectRoot;
150254
+ const prepareTarget = async (target) => {
150255
+ if (target.projectRoot === preparedRoot) return;
150256
+ initProjectRoot(target.projectRoot);
150257
+ loadDotenv(target.projectRoot);
150258
+ preparedRoot = target.projectRoot;
150259
+ };
150260
+ const handler = async (job) => {
150261
+ const target = targets.get(job.projectId) ?? (job.projectId === request.projectId ? { projectId: request.projectId, projectRoot: request.projectRoot, dataDir: request.dataDir, updatedAt: Date.now() } : void 0);
150262
+ if (!target) return { action: "reschedule", delayMs: 3e4 };
150263
+ await prepareTarget(target);
150264
+ return createProjectMaintenanceHandler(job.projectId, target.dataDir, target.projectRoot)(job);
150265
+ };
150102
150266
  const worker = new MaintenanceJobWorker(
150103
- new MaintenanceJobStore(request.dataDir),
150104
- createProjectMaintenanceHandler(request.projectId, request.dataDir, request.projectRoot),
150105
- { projectId: request.projectId, kinds: ["vector-backfill"] }
150267
+ queue,
150268
+ handler,
150269
+ { kinds: ["vector-backfill"] }
150106
150270
  );
150107
- return worker.runOnce();
150271
+ let result = { state: "idle" };
150272
+ while (true) {
150273
+ result = await worker.runOnce();
150274
+ if (result.state === "idle") break;
150275
+ }
150276
+ return result;
150108
150277
  }
150109
150278
  async function readStdin() {
150110
150279
  return new Promise((resolve2, reject) => {
@@ -150127,9 +150296,11 @@ async function main() {
150127
150296
  `);
150128
150297
  process.exitCode = 1;
150129
150298
  } finally {
150299
+ releaseVectorBackfillLock();
150130
150300
  closeAllDatabases();
150131
150301
  }
150132
150302
  }
150303
+ var VECTOR_BACKFILL_LOCK_FILE, STALE_LOCK_RECOVERY_MS;
150133
150304
  var init_vector_backfill_runner = __esm({
150134
150305
  "src/runtime/vector-backfill-runner.ts"() {
150135
150306
  "use strict";
@@ -150142,7 +150313,10 @@ var init_vector_backfill_runner = __esm({
150142
150313
  init_orama_store();
150143
150314
  init_sqlite_db();
150144
150315
  init_maintenance_jobs();
150316
+ init_maintenance_targets();
150145
150317
  init_project_maintenance();
150318
+ VECTOR_BACKFILL_LOCK_FILE = ".memorix-vector-backfill.lock";
150319
+ STALE_LOCK_RECOVERY_MS = 24 * 60 * 60 * 1e3;
150146
150320
  if (process.argv[1] && process.argv[1].endsWith("vector-backfill-runner.js")) {
150147
150321
  void main();
150148
150322
  }
@@ -150184,11 +150358,17 @@ function resetObservationRuntime() {
150184
150358
  embeddingWorkerProjectRoot = void 0;
150185
150359
  vectorMissingIds.clear();
150186
150360
  vectorSchemaUpgradePromise = null;
150361
+ embeddingFailureLogTimestamps.clear();
150187
150362
  }
150188
150363
  function logEmbeddingFailureOnce(key, message) {
150189
150364
  const now3 = Date.now();
150190
150365
  const last = embeddingFailureLogTimestamps.get(key) ?? 0;
150191
150366
  if (now3 - last < EMBEDDING_FAILURE_LOG_COOLDOWN_MS) return;
150367
+ if (!embeddingFailureLogTimestamps.has(key) && embeddingFailureLogTimestamps.size >= MAX_EMBEDDING_FAILURE_KEYS) {
150368
+ const oldest = embeddingFailureLogTimestamps.keys().next().value;
150369
+ if (oldest !== void 0) embeddingFailureLogTimestamps.delete(oldest);
150370
+ }
150371
+ embeddingFailureLogTimestamps.delete(key);
150192
150372
  embeddingFailureLogTimestamps.set(key, now3);
150193
150373
  console.error(message);
150194
150374
  }
@@ -150218,13 +150398,14 @@ function queueVectorBackfill(projectId, options = {}) {
150218
150398
  const dataDir = projectDir;
150219
150399
  if (!dataDir) return;
150220
150400
  void Promise.resolve().then(() => (init_maintenance_jobs(), maintenance_jobs_exports)).then(({ MaintenanceJobStore: MaintenanceJobStore2 }) => {
150221
- new MaintenanceJobStore2(dataDir).enqueue({
150401
+ const job = new MaintenanceJobStore2(dataDir).enqueue({
150222
150402
  projectId,
150223
150403
  kind: "vector-backfill",
150224
150404
  dedupeKey: "vector-backfill",
150225
150405
  payload: { limit: 12 }
150226
150406
  });
150227
- if (options.detachedWorker && options.projectRoot) {
150407
+ const due = !job || job.status === "pending" && job.runAfter <= Date.now();
150408
+ if (due && options.detachedWorker && options.projectRoot) {
150228
150409
  void Promise.resolve().then(() => (init_vector_backfill_runner(), vector_backfill_runner_exports)).then(({ launchDetachedVectorBackfill: launchDetachedVectorBackfill2 }) => {
150229
150410
  launchDetachedVectorBackfill2({
150230
150411
  projectId,
@@ -151155,7 +151336,7 @@ async function backfillVectorEmbeddings(options = {}) {
151155
151336
  ...lastFailure ? { lastError: lastFailure } : {}
151156
151337
  };
151157
151338
  }
151158
- var observations, nextId, projectDir, searchIndexPrepared, embeddingWriteMode, embeddingWorkerProjectRoot, corpusLoaded, loadedObservationStore, vectorMissingIds, vectorBackfillRunning, vectorSchemaUpgradePromise, lastVectorBackfill, embeddingFailureLogTimestamps, EMBEDDING_FAILURE_LOG_COOLDOWN_MS;
151339
+ var observations, nextId, projectDir, searchIndexPrepared, embeddingWriteMode, embeddingWorkerProjectRoot, corpusLoaded, loadedObservationStore, vectorMissingIds, vectorBackfillRunning, vectorSchemaUpgradePromise, lastVectorBackfill, embeddingFailureLogTimestamps, EMBEDDING_FAILURE_LOG_COOLDOWN_MS, MAX_EMBEDDING_FAILURE_KEYS;
151159
151340
  var init_observations = __esm({
151160
151341
  "src/memory/observations.ts"() {
151161
151342
  "use strict";
@@ -151183,6 +151364,7 @@ var init_observations = __esm({
151183
151364
  lastVectorBackfill = null;
151184
151365
  embeddingFailureLogTimestamps = /* @__PURE__ */ new Map();
151185
151366
  EMBEDDING_FAILURE_LOG_COOLDOWN_MS = 3e4;
151367
+ MAX_EMBEDDING_FAILURE_KEYS = 1024;
151186
151368
  }
151187
151369
  });
151188
151370