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
@@ -6976,6 +6976,14 @@ __export(orama_store_exports, {
6976
6976
  updateObservationMetadata: () => updateObservationMetadata
6977
6977
  });
6978
6978
  import { create, insert as insert2, insertMultiple, search, remove as remove2, update, count, getByID } from "@orama/orama";
6979
+ function rememberSearchMode(key, value) {
6980
+ if (!lastSearchModeByProject.has(key) && lastSearchModeByProject.size >= MAX_SEARCH_MODE_KEYS) {
6981
+ const oldest = lastSearchModeByProject.keys().next().value;
6982
+ if (oldest !== void 0) lastSearchModeByProject.delete(oldest);
6983
+ }
6984
+ lastSearchModeByProject.delete(key);
6985
+ lastSearchModeByProject.set(key, value);
6986
+ }
6979
6987
  function getLastSearchMode(projectId) {
6980
6988
  return lastSearchModeByProject.get(projectId ?? SEARCH_MODE_DEFAULT_KEY) ?? "fulltext";
6981
6989
  }
@@ -6988,9 +6996,6 @@ function makeEntryKey(projectId, observationId2) {
6988
6996
  function rememberObservationDoc(doc) {
6989
6997
  const publicDoc = { ...doc };
6990
6998
  delete publicDoc.embedding;
6991
- if (doc.projectId && typeof doc.observationId === "number") {
6992
- docByObservationKey.set(makeEntryKey(doc.projectId, doc.observationId), publicDoc);
6993
- }
6994
6999
  return publicDoc;
6995
7000
  }
6996
7001
  function isCommandLikeQuery(query) {
@@ -7094,7 +7099,6 @@ async function resetDb() {
7094
7099
  indexEmbeddingProvider = null;
7095
7100
  deferredCachedVectorHydration = null;
7096
7101
  lastSearchModeByProject.clear();
7097
- docByObservationKey.clear();
7098
7102
  }
7099
7103
  function isEmbeddingEnabled() {
7100
7104
  return embeddingEnabled;
@@ -7157,78 +7161,72 @@ function isCompatibleCachedVector(vector) {
7157
7161
  vector && embeddingDimensions !== null && vector.length === embeddingDimensions && vector.every(Number.isFinite)
7158
7162
  );
7159
7163
  }
7160
- async function attachCachedVectors(database, candidates) {
7161
- const cachedVectors = await getCachedEmbeddings(
7162
- candidates.map(({ observation }) => observationEmbeddingText(observation))
7163
- );
7164
- if (db !== database) return;
7165
- for (let index = 0; index < candidates.length; index++) {
7166
- const vector = cachedVectors[index];
7167
- if (!isCompatibleCachedVector(vector)) continue;
7168
- const existing = getByID(database, candidates[index].id);
7169
- if (!existing || documentEmbeddingText(existing) !== observationEmbeddingText(candidates[index].observation)) continue;
7170
- try {
7171
- await update(database, candidates[index].id, { ...existing, embedding: vector });
7172
- } catch {
7173
- }
7174
- if (index % 50 === 0) {
7175
- await new Promise((resolve2) => setImmediate(resolve2));
7164
+ async function attachCachedVectors(database, observations2) {
7165
+ const batchSize = 200;
7166
+ for (let start = 0; start < observations2.length; start += batchSize) {
7167
+ const batch = observations2.slice(start, start + batchSize);
7168
+ const cachedVectors = await getCachedEmbeddings(batch.map(observationEmbeddingText));
7169
+ if (db !== database) return;
7170
+ for (let index = 0; index < batch.length; index++) {
7171
+ const vector = cachedVectors[index];
7172
+ if (!isCompatibleCachedVector(vector)) continue;
7173
+ const id = makeOramaObservationId(batch[index].projectId, batch[index].id);
7174
+ const existing = getByID(database, id);
7175
+ if (!existing || documentEmbeddingText(existing) !== observationEmbeddingText(batch[index])) continue;
7176
+ try {
7177
+ await update(database, id, { ...existing, embedding: vector });
7178
+ } catch {
7179
+ }
7176
7180
  }
7181
+ await new Promise((resolve2) => setImmediate(resolve2));
7177
7182
  }
7178
7183
  }
7179
7184
  async function hydrateIndex(observations2, options = {}) {
7180
7185
  const database = await getDb({ allowNetworkProbe: options.allowNetworkProbe });
7181
- const candidates = [];
7182
- for (const observation of observations2) {
7183
- if (!observation || !observation.id || !observation.projectId) continue;
7184
- const id = makeOramaObservationId(observation.projectId, observation.id);
7185
- if (getByID(database, id)) continue;
7186
- candidates.push({ observation, id });
7187
- }
7188
7186
  const deferCachedVectors = options.deferCachedVectors === true && Boolean(indexEmbeddingProvider?.getCachedEmbeddings);
7189
- const cachedVectors = deferCachedVectors ? candidates.map(() => null) : await getCachedEmbeddings(candidates.map(({ observation }) => observationEmbeddingText(observation)));
7190
- const documents = [];
7191
- for (let index = 0; index < candidates.length; index++) {
7192
- const { observation: obs, id } = candidates[index];
7193
- try {
7194
- const vector = cachedVectors[index];
7195
- const compatibleVector = isCompatibleCachedVector(vector) ? vector : null;
7196
- const doc = {
7197
- id,
7198
- observationId: obs.id,
7199
- entityName: obs.entityName || "",
7200
- type: obs.type || "discovery",
7201
- title: obs.title || "",
7202
- narrative: obs.narrative || "",
7203
- facts: Array.isArray(obs.facts) ? obs.facts.join(" ") : "",
7204
- filesModified: Array.isArray(obs.filesModified) ? obs.filesModified.join(" ") : "",
7205
- concepts: Array.isArray(obs.concepts) ? obs.concepts.join(" ") : "",
7206
- tokens: obs.tokens ?? 0,
7207
- createdAt: obs.createdAt || "",
7208
- projectId: obs.projectId,
7209
- accessCount: obs.accessCount ?? 0,
7210
- lastAccessedAt: obs.lastAccessedAt || "",
7211
- status: obs.status ?? "active",
7212
- source: obs.source || "agent",
7213
- sourceDetail: obs.sourceDetail ?? "",
7214
- valueCategory: obs.valueCategory ?? "",
7215
- admissionState: obs.admissionState ?? "",
7216
- admissionReason: obs.admissionReason ?? "",
7217
- visibility: obs.visibility ?? "project",
7218
- createdByAgentId: obs.createdByAgentId ?? "",
7219
- sharedWithAgentIds: JSON.stringify(obs.sharedWithAgentIds ?? []),
7220
- documentType: "observation",
7221
- knowledgeLayer: resolveKnowledgeLayer("observation", obs.sourceDetail, obs.source),
7222
- ...compatibleVector ? { embedding: compatibleVector } : {}
7223
- };
7224
- documents.push(doc);
7225
- } catch {
7226
- }
7227
- }
7228
7187
  let inserted = 0;
7229
7188
  const hydrationBatchSize = 200;
7230
- for (let start = 0; start < documents.length; start += hydrationBatchSize) {
7231
- const batch = documents.slice(start, start + hydrationBatchSize);
7189
+ for (let start = 0; start < observations2.length; start += hydrationBatchSize) {
7190
+ const batchObservations = observations2.slice(start, start + hydrationBatchSize).filter((observation) => observation && observation.id && observation.projectId).filter((observation) => !getByID(database, makeOramaObservationId(observation.projectId, observation.id)));
7191
+ if (batchObservations.length === 0) continue;
7192
+ const cachedVectors = deferCachedVectors || options.skipCachedVectors ? batchObservations.map(() => null) : await getCachedEmbeddings(batchObservations.map(observationEmbeddingText));
7193
+ const batch = [];
7194
+ for (let index = 0; index < batchObservations.length; index++) {
7195
+ const obs = batchObservations[index];
7196
+ try {
7197
+ const compatibleVector = isCompatibleCachedVector(cachedVectors[index]) ? cachedVectors[index] : null;
7198
+ batch.push({
7199
+ id: makeOramaObservationId(obs.projectId, obs.id),
7200
+ observationId: obs.id,
7201
+ entityName: obs.entityName || "",
7202
+ type: obs.type || "discovery",
7203
+ title: obs.title || "",
7204
+ narrative: obs.narrative || "",
7205
+ facts: Array.isArray(obs.facts) ? obs.facts.join(" ") : "",
7206
+ filesModified: Array.isArray(obs.filesModified) ? obs.filesModified.join(" ") : "",
7207
+ concepts: Array.isArray(obs.concepts) ? obs.concepts.join(" ") : "",
7208
+ tokens: obs.tokens ?? 0,
7209
+ createdAt: obs.createdAt || "",
7210
+ projectId: obs.projectId,
7211
+ accessCount: obs.accessCount ?? 0,
7212
+ lastAccessedAt: obs.lastAccessedAt || "",
7213
+ status: obs.status ?? "active",
7214
+ source: obs.source || "agent",
7215
+ sourceDetail: obs.sourceDetail ?? "",
7216
+ valueCategory: obs.valueCategory ?? "",
7217
+ admissionState: obs.admissionState ?? "",
7218
+ admissionReason: obs.admissionReason ?? "",
7219
+ visibility: obs.visibility ?? "project",
7220
+ createdByAgentId: obs.createdByAgentId ?? "",
7221
+ sharedWithAgentIds: JSON.stringify(obs.sharedWithAgentIds ?? []),
7222
+ documentType: "observation",
7223
+ knowledgeLayer: resolveKnowledgeLayer("observation", obs.sourceDetail, obs.source),
7224
+ ...compatibleVector ? { embedding: compatibleVector } : {}
7225
+ });
7226
+ } catch {
7227
+ }
7228
+ }
7229
+ if (batch.length === 0) continue;
7232
7230
  try {
7233
7231
  await insertMultiple(database, batch, batch.length);
7234
7232
  for (const doc of batch) rememberObservationDoc(doc);
@@ -7250,7 +7248,7 @@ async function hydrateIndex(observations2, options = {}) {
7250
7248
  }
7251
7249
  await new Promise((resolve2) => setImmediate(resolve2));
7252
7250
  }
7253
- deferredCachedVectorHydration = options.skipCachedVectors ? null : deferCachedVectors ? attachCachedVectors(database, candidates).catch(() => {
7251
+ deferredCachedVectorHydration = options.skipCachedVectors ? null : deferCachedVectors ? attachCachedVectors(database, observations2).catch(() => {
7254
7252
  }) : null;
7255
7253
  return inserted;
7256
7254
  }
@@ -7287,12 +7285,6 @@ async function updateObservationMetadata(projectId, observationId2, patch) {
7287
7285
  async function removeObservation(oramaId) {
7288
7286
  const database = await getDb();
7289
7287
  await remove2(database, oramaId);
7290
- for (const [key, doc] of docByObservationKey.entries()) {
7291
- if (doc.id === oramaId) {
7292
- docByObservationKey.delete(key);
7293
- break;
7294
- }
7295
- }
7296
7288
  }
7297
7289
  async function searchObservations(options) {
7298
7290
  const perf = !!process.env.MEMORIX_PERF;
@@ -7307,7 +7299,7 @@ async function searchObservations(options) {
7307
7299
  const modeKey = options.projectId ?? SEARCH_MODE_DEFAULT_KEY;
7308
7300
  const quality = options.quality ?? "balanced";
7309
7301
  const database = await getDb();
7310
- lastSearchModeByProject.set(
7302
+ rememberSearchMode(
7311
7303
  modeKey,
7312
7304
  quality === "fast" ? "fulltext (fast profile)" : embeddingEnabled ? "hybrid" : "fulltext"
7313
7305
  );
@@ -7374,7 +7366,7 @@ async function searchObservations(options) {
7374
7366
  if (provider2) {
7375
7367
  const activeVectorDimensions = getVectorDimensions();
7376
7368
  if (activeVectorDimensions !== null && provider2.dimensions !== activeVectorDimensions) {
7377
- lastSearchModeByProject.set(
7369
+ rememberSearchMode(
7378
7370
  modeKey,
7379
7371
  `fulltext (embedding dimension mismatch: provider ${provider2.dimensions}d vs index ${activeVectorDimensions}d)`
7380
7372
  );
@@ -7399,7 +7391,7 @@ async function searchObservations(options) {
7399
7391
  mark("embedding");
7400
7392
  const cjkRatio = (originalQuery.match(/[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/g) || []).length / originalQuery.length;
7401
7393
  const isCJKHeavy = cjkRatio > 0.3;
7402
- lastSearchModeByProject.set(modeKey, "hybrid");
7394
+ rememberSearchMode(modeKey, "hybrid");
7403
7395
  searchParams = {
7404
7396
  ...searchParams,
7405
7397
  mode: "hybrid",
@@ -7415,7 +7407,7 @@ async function searchObservations(options) {
7415
7407
  }
7416
7408
  }
7417
7409
  } catch (error) {
7418
- lastSearchModeByProject.set(modeKey, "fulltext (embedding unavailable)");
7410
+ rememberSearchMode(modeKey, "fulltext (embedding unavailable)");
7419
7411
  console.error("[memorix] Embedding failed or timed out, falling back to fulltext search");
7420
7412
  }
7421
7413
  }
@@ -7425,7 +7417,7 @@ async function searchObservations(options) {
7425
7417
  results = await search(database, searchParams);
7426
7418
  } catch (error) {
7427
7419
  if (queryVector && isVectorDimensionMismatchError(error)) {
7428
- lastSearchModeByProject.set(modeKey, "fulltext (embedding dimension mismatch)");
7420
+ rememberSearchMode(modeKey, "fulltext (embedding dimension mismatch)");
7429
7421
  console.error("[memorix] Vector search dimension mismatch detected, retrying without embeddings");
7430
7422
  results = await search(database, stripVectorSearchParams(searchParams));
7431
7423
  } else {
@@ -7447,7 +7439,7 @@ async function searchObservations(options) {
7447
7439
  },
7448
7440
  similarity: 0.25
7449
7441
  };
7450
- lastSearchModeByProject.set(modeKey, "vector-only (hybrid empty fallback)");
7442
+ rememberSearchMode(modeKey, "vector-only (hybrid empty fallback)");
7451
7443
  results = await search(database, vectorOnlyParams);
7452
7444
  } catch {
7453
7445
  }
@@ -7688,7 +7680,7 @@ async function searchObservations(options) {
7688
7680
  const rerankedTop = neural.map((row) => candidateMap.get(row.id)).filter((entry) => entry != null);
7689
7681
  if (rerankedTop.length > 0) {
7690
7682
  intermediate = [...rerankedTop, ...intermediate.slice(RERANK_TOP_K)];
7691
- lastSearchModeByProject.set(modeKey, (lastSearchModeByProject.get(modeKey) ?? "fulltext") + " + neural rerank");
7683
+ rememberSearchMode(modeKey, (lastSearchModeByProject.get(modeKey) ?? "fulltext") + " + neural rerank");
7692
7684
  mark("rerank(usedNeural=true)");
7693
7685
  }
7694
7686
  }
@@ -7705,7 +7697,7 @@ async function searchObservations(options) {
7705
7697
  );
7706
7698
  mark(`rerank(usedLLM=${usedLLM})`);
7707
7699
  if (usedLLM) {
7708
- lastSearchModeByProject.set(modeKey, (lastSearchModeByProject.get(modeKey) ?? "fulltext") + " + LLM rerank");
7700
+ rememberSearchMode(modeKey, (lastSearchModeByProject.get(modeKey) ?? "fulltext") + " + LLM rerank");
7709
7701
  const rerankedTop = reranked.map((row) => candidateMap.get(row.id)).filter((entry) => entry != null);
7710
7702
  if (rerankedTop.length > 0) {
7711
7703
  intermediate = [...rerankedTop, ...intermediate.slice(RERANK_TOP_K)];
@@ -7778,13 +7770,6 @@ async function getObservationsByIds(ids, projectId) {
7778
7770
  const database = await getDb();
7779
7771
  const results = [];
7780
7772
  for (const id of ids) {
7781
- if (projectId) {
7782
- const cached = docByObservationKey.get(makeEntryKey(projectId, id));
7783
- if (cached) {
7784
- results.push(cached);
7785
- continue;
7786
- }
7787
- }
7788
7773
  const searchResult = await search(database, {
7789
7774
  term: "",
7790
7775
  includeVectors: true,
@@ -7884,7 +7869,7 @@ function formatTime(isoDate) {
7884
7869
  return isoDate;
7885
7870
  }
7886
7871
  }
7887
- 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;
7872
+ 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;
7888
7873
  var init_orama_store = __esm({
7889
7874
  "src/store/orama-store.ts"() {
7890
7875
  "use strict";
@@ -7905,10 +7890,10 @@ var init_orama_store = __esm({
7905
7890
  embeddingDimensions = null;
7906
7891
  indexEmbeddingProvider = null;
7907
7892
  deferredCachedVectorHydration = null;
7908
- docByObservationKey = /* @__PURE__ */ new Map();
7909
7893
  NON_CJK_HYBRID_SIMILARITY = 0.45;
7910
7894
  lastSearchModeByProject = /* @__PURE__ */ new Map();
7911
7895
  SEARCH_MODE_DEFAULT_KEY = "__global__";
7896
+ MAX_SEARCH_MODE_KEYS = 4096;
7912
7897
  COMMAND_LOG_TITLE = /^(Ran:|Command:|Executed:)\s/i;
7913
7898
  COMMAND_STYLE_TITLE = /(\bfindstr\b|\bSelect-String\b|\bGet-Content\b|\bnpx\s+vitest\b|\bnpx\s+tsc\b|\b2>&1\b)/i;
7914
7899
  COMMAND_LIKE_QUERY2 = /\b(git|npm|npx|pnpm|yarn|node|bash|powershell|curl|memorix)\b/i;
@@ -8804,6 +8789,56 @@ var init_evidence_store = __esm({
8804
8789
  }
8805
8790
  });
8806
8791
 
8792
+ // src/runtime/maintenance-targets.ts
8793
+ function rowToTarget(row) {
8794
+ return {
8795
+ projectId: row.project_id,
8796
+ projectRoot: row.project_root,
8797
+ dataDir: row.data_dir,
8798
+ updatedAt: Number(row.updated_at)
8799
+ };
8800
+ }
8801
+ var MaintenanceTargetStore;
8802
+ var init_maintenance_targets = __esm({
8803
+ "src/runtime/maintenance-targets.ts"() {
8804
+ "use strict";
8805
+ init_esm_shims();
8806
+ init_sqlite_db();
8807
+ MaintenanceTargetStore = class {
8808
+ db;
8809
+ constructor(dataDir) {
8810
+ this.db = getDatabase(dataDir);
8811
+ }
8812
+ register(input) {
8813
+ if (!input.projectId.trim()) throw new Error("Maintenance target requires a project ID");
8814
+ if (!input.projectRoot.trim()) throw new Error("Maintenance target requires a project root");
8815
+ if (!input.dataDir.trim()) throw new Error("Maintenance target requires a data directory");
8816
+ const now3 = input.now ?? Date.now();
8817
+ this.db.prepare(`
8818
+ INSERT INTO maintenance_targets (project_id, project_root, data_dir, updated_at)
8819
+ VALUES (?, ?, ?, ?)
8820
+ ON CONFLICT(project_id) DO UPDATE SET
8821
+ project_root = excluded.project_root,
8822
+ data_dir = excluded.data_dir,
8823
+ updated_at = excluded.updated_at
8824
+ `).run(input.projectId, input.projectRoot, input.dataDir, now3);
8825
+ return this.get(input.projectId);
8826
+ }
8827
+ get(projectId) {
8828
+ const row = this.db.prepare(`
8829
+ SELECT * FROM maintenance_targets WHERE project_id = ?
8830
+ `).get(projectId);
8831
+ return row ? rowToTarget(row) : void 0;
8832
+ }
8833
+ list() {
8834
+ return this.db.prepare(`
8835
+ SELECT * FROM maintenance_targets ORDER BY updated_at DESC
8836
+ `).all().map(rowToTarget);
8837
+ }
8838
+ };
8839
+ }
8840
+ });
8841
+
8807
8842
  // src/knowledge/workspace-store.ts
8808
8843
  import { randomUUID as randomUUID5 } from "crypto";
8809
8844
  function arrayValue(raw) {
@@ -149668,10 +149703,21 @@ __export(vector_backfill_runner_exports, {
149668
149703
  launchDetachedVectorBackfill: () => launchDetachedVectorBackfill,
149669
149704
  main: () => main,
149670
149705
  parseVectorBackfillRequest: () => parseVectorBackfillRequest,
149671
- resolveVectorBackfillRunnerPath: () => resolveVectorBackfillRunnerPath
149706
+ releaseVectorBackfillLock: () => releaseVectorBackfillLock,
149707
+ resolveVectorBackfillRunnerPath: () => resolveVectorBackfillRunnerPath,
149708
+ vectorBackfillLockPath: () => vectorBackfillLockPath
149672
149709
  });
149673
149710
  import { spawn as spawn2 } from "child_process";
149674
- import { existsSync as existsSync10 } from "fs";
149711
+ import {
149712
+ closeSync,
149713
+ existsSync as existsSync10,
149714
+ mkdirSync as mkdirSync2,
149715
+ openSync,
149716
+ readFileSync as readFileSync9,
149717
+ statSync as statSync4,
149718
+ unlinkSync,
149719
+ writeFileSync as writeFileSync2
149720
+ } from "fs";
149675
149721
  import path19 from "path";
149676
149722
  import { fileURLToPath as fileURLToPath3 } from "url";
149677
149723
  function isNonEmptyString(value) {
@@ -149698,10 +149744,104 @@ function parseVectorBackfillRequest(raw) {
149698
149744
  dataDir: value.dataDir
149699
149745
  };
149700
149746
  }
149747
+ function vectorBackfillLockPath(dataDir) {
149748
+ return path19.join(dataDir, VECTOR_BACKFILL_LOCK_FILE);
149749
+ }
149750
+ function processIsAlive(pid) {
149751
+ if (!Number.isSafeInteger(pid) || pid == null || pid <= 0) return false;
149752
+ try {
149753
+ process.kill(pid, 0);
149754
+ return true;
149755
+ } catch (error) {
149756
+ return error.code === "EPERM";
149757
+ }
149758
+ }
149759
+ function readVectorBackfillLock(lockPath) {
149760
+ try {
149761
+ const value = JSON.parse(readFileSync9(lockPath, "utf8"));
149762
+ if (typeof value.parentPid !== "number" || !Number.isSafeInteger(value.parentPid) || typeof value.startedAt !== "number" || !Number.isFinite(value.startedAt) || typeof value.projectId !== "string") return void 0;
149763
+ return {
149764
+ parentPid: value.parentPid,
149765
+ ...typeof value.childPid === "number" ? { childPid: value.childPid } : {},
149766
+ startedAt: value.startedAt,
149767
+ projectId: value.projectId
149768
+ };
149769
+ } catch {
149770
+ return void 0;
149771
+ }
149772
+ }
149773
+ function writeVectorBackfillLock(lockPath, value) {
149774
+ writeFileSync2(lockPath, `${JSON.stringify(value)}
149775
+ `, "utf8");
149776
+ }
149777
+ function acquireVectorBackfillLock(dataDir, projectId, preferredPath) {
149778
+ const lockPath = preferredPath ?? vectorBackfillLockPath(dataDir);
149779
+ mkdirSync2(path19.dirname(lockPath), { recursive: true });
149780
+ const initial = {
149781
+ parentPid: process.pid,
149782
+ startedAt: Date.now(),
149783
+ projectId
149784
+ };
149785
+ for (let attempt = 0; attempt < 2; attempt++) {
149786
+ try {
149787
+ const fd = openSync(lockPath, "wx");
149788
+ try {
149789
+ writeFileSync2(fd, `${JSON.stringify(initial)}
149790
+ `, "utf8");
149791
+ } finally {
149792
+ closeSync(fd);
149793
+ }
149794
+ return {
149795
+ path: lockPath,
149796
+ setChildPid(pid) {
149797
+ try {
149798
+ writeVectorBackfillLock(lockPath, { ...initial, childPid: pid });
149799
+ } catch {
149800
+ }
149801
+ },
149802
+ release() {
149803
+ try {
149804
+ unlinkSync(lockPath);
149805
+ } catch {
149806
+ }
149807
+ }
149808
+ };
149809
+ } catch (error) {
149810
+ if (error.code !== "EEXIST") return void 0;
149811
+ const existing = readVectorBackfillLock(lockPath);
149812
+ const ownerAlive = existing && (processIsAlive(existing.childPid) || processIsAlive(existing.parentPid));
149813
+ if (ownerAlive) return void 0;
149814
+ let stale = false;
149815
+ try {
149816
+ stale = Date.now() - statSync4(lockPath).mtimeMs >= STALE_LOCK_RECOVERY_MS;
149817
+ } catch {
149818
+ stale = false;
149819
+ }
149820
+ if (!existing && !stale) return void 0;
149821
+ try {
149822
+ unlinkSync(lockPath);
149823
+ } catch {
149824
+ return void 0;
149825
+ }
149826
+ }
149827
+ }
149828
+ return void 0;
149829
+ }
149830
+ function releaseVectorBackfillLock(lockPath = process.env.MEMORIX_VECTOR_BACKFILL_LOCK) {
149831
+ if (!lockPath) return;
149832
+ const lock = readVectorBackfillLock(lockPath);
149833
+ if (lock && lock.childPid !== process.pid) return;
149834
+ try {
149835
+ unlinkSync(lockPath);
149836
+ } catch {
149837
+ }
149838
+ }
149701
149839
  function launchDetachedVectorBackfill(request, options = {}) {
149702
149840
  const runnerPath = options.runnerPath ?? resolveVectorBackfillRunnerPath();
149703
149841
  const exists = options.exists ?? existsSync10;
149704
149842
  if (!exists(runnerPath)) return false;
149843
+ const lock = acquireVectorBackfillLock(request.dataDir, request.projectId, options.lockPath);
149844
+ if (!lock) return false;
149705
149845
  try {
149706
149846
  const child = (options.spawn ?? spawn2)(process.execPath, [runnerPath], {
149707
149847
  cwd: request.projectRoot,
@@ -149711,14 +149851,23 @@ function launchDetachedVectorBackfill(request, options = {}) {
149711
149851
  stdio: "ignore",
149712
149852
  // The request contains only local project metadata, never credentials.
149713
149853
  // Environment transport avoids a live stdin pipe keeping the CLI alive.
149714
- env: { ...process.env, MEMORIX_VECTOR_BACKFILL_REQUEST: JSON.stringify(request) },
149854
+ env: {
149855
+ ...process.env,
149856
+ MEMORIX_VECTOR_BACKFILL_REQUEST: JSON.stringify(request),
149857
+ MEMORIX_VECTOR_BACKFILL_LOCK: lock.path
149858
+ },
149715
149859
  windowsHide: true
149716
149860
  });
149861
+ if (child.pid) lock.setChildPid(child.pid);
149862
+ else lock.release();
149717
149863
  child.once?.("error", () => {
149718
149864
  });
149865
+ child.once?.("close", () => lock.release());
149866
+ if (typeof child.once !== "function") lock.release();
149719
149867
  child.unref();
149720
- return true;
149868
+ return Boolean(child.pid);
149721
149869
  } catch {
149870
+ lock.release();
149722
149871
  return false;
149723
149872
  }
149724
149873
  }
@@ -149730,12 +149879,32 @@ async function executeVectorBackfill(request) {
149730
149879
  await prepareSearchIndex();
149731
149880
  await getDeferredCachedVectorHydration()?.catch(() => {
149732
149881
  });
149882
+ const queue = new MaintenanceJobStore(request.dataDir);
149883
+ const targets = new MaintenanceTargetStore(request.dataDir);
149884
+ let preparedRoot = request.projectRoot;
149885
+ const prepareTarget = async (target) => {
149886
+ if (target.projectRoot === preparedRoot) return;
149887
+ initProjectRoot(target.projectRoot);
149888
+ loadDotenv(target.projectRoot);
149889
+ preparedRoot = target.projectRoot;
149890
+ };
149891
+ const handler = async (job) => {
149892
+ const target = targets.get(job.projectId) ?? (job.projectId === request.projectId ? { projectId: request.projectId, projectRoot: request.projectRoot, dataDir: request.dataDir, updatedAt: Date.now() } : void 0);
149893
+ if (!target) return { action: "reschedule", delayMs: 3e4 };
149894
+ await prepareTarget(target);
149895
+ return createProjectMaintenanceHandler(job.projectId, target.dataDir, target.projectRoot)(job);
149896
+ };
149733
149897
  const worker = new MaintenanceJobWorker(
149734
- new MaintenanceJobStore(request.dataDir),
149735
- createProjectMaintenanceHandler(request.projectId, request.dataDir, request.projectRoot),
149736
- { projectId: request.projectId, kinds: ["vector-backfill"] }
149898
+ queue,
149899
+ handler,
149900
+ { kinds: ["vector-backfill"] }
149737
149901
  );
149738
- return worker.runOnce();
149902
+ let result = { state: "idle" };
149903
+ while (true) {
149904
+ result = await worker.runOnce();
149905
+ if (result.state === "idle") break;
149906
+ }
149907
+ return result;
149739
149908
  }
149740
149909
  async function readStdin() {
149741
149910
  return new Promise((resolve2, reject) => {
@@ -149758,9 +149927,11 @@ async function main() {
149758
149927
  `);
149759
149928
  process.exitCode = 1;
149760
149929
  } finally {
149930
+ releaseVectorBackfillLock();
149761
149931
  closeAllDatabases();
149762
149932
  }
149763
149933
  }
149934
+ var VECTOR_BACKFILL_LOCK_FILE, STALE_LOCK_RECOVERY_MS;
149764
149935
  var init_vector_backfill_runner = __esm({
149765
149936
  "src/runtime/vector-backfill-runner.ts"() {
149766
149937
  "use strict";
@@ -149773,7 +149944,10 @@ var init_vector_backfill_runner = __esm({
149773
149944
  init_orama_store();
149774
149945
  init_sqlite_db();
149775
149946
  init_maintenance_jobs();
149947
+ init_maintenance_targets();
149776
149948
  init_project_maintenance();
149949
+ VECTOR_BACKFILL_LOCK_FILE = ".memorix-vector-backfill.lock";
149950
+ STALE_LOCK_RECOVERY_MS = 24 * 60 * 60 * 1e3;
149777
149951
  if (process.argv[1] && process.argv[1].endsWith("vector-backfill-runner.js")) {
149778
149952
  void main();
149779
149953
  }
@@ -149815,11 +149989,17 @@ function resetObservationRuntime() {
149815
149989
  embeddingWorkerProjectRoot = void 0;
149816
149990
  vectorMissingIds.clear();
149817
149991
  vectorSchemaUpgradePromise = null;
149992
+ embeddingFailureLogTimestamps.clear();
149818
149993
  }
149819
149994
  function logEmbeddingFailureOnce(key, message) {
149820
149995
  const now3 = Date.now();
149821
149996
  const last = embeddingFailureLogTimestamps.get(key) ?? 0;
149822
149997
  if (now3 - last < EMBEDDING_FAILURE_LOG_COOLDOWN_MS) return;
149998
+ if (!embeddingFailureLogTimestamps.has(key) && embeddingFailureLogTimestamps.size >= MAX_EMBEDDING_FAILURE_KEYS) {
149999
+ const oldest = embeddingFailureLogTimestamps.keys().next().value;
150000
+ if (oldest !== void 0) embeddingFailureLogTimestamps.delete(oldest);
150001
+ }
150002
+ embeddingFailureLogTimestamps.delete(key);
149823
150003
  embeddingFailureLogTimestamps.set(key, now3);
149824
150004
  console.error(message);
149825
150005
  }
@@ -149849,13 +150029,14 @@ function queueVectorBackfill(projectId, options = {}) {
149849
150029
  const dataDir = projectDir;
149850
150030
  if (!dataDir) return;
149851
150031
  void Promise.resolve().then(() => (init_maintenance_jobs(), maintenance_jobs_exports)).then(({ MaintenanceJobStore: MaintenanceJobStore2 }) => {
149852
- new MaintenanceJobStore2(dataDir).enqueue({
150032
+ const job = new MaintenanceJobStore2(dataDir).enqueue({
149853
150033
  projectId,
149854
150034
  kind: "vector-backfill",
149855
150035
  dedupeKey: "vector-backfill",
149856
150036
  payload: { limit: 12 }
149857
150037
  });
149858
- if (options.detachedWorker && options.projectRoot) {
150038
+ const due = !job || job.status === "pending" && job.runAfter <= Date.now();
150039
+ if (due && options.detachedWorker && options.projectRoot) {
149859
150040
  void Promise.resolve().then(() => (init_vector_backfill_runner(), vector_backfill_runner_exports)).then(({ launchDetachedVectorBackfill: launchDetachedVectorBackfill2 }) => {
149860
150041
  launchDetachedVectorBackfill2({
149861
150042
  projectId,
@@ -150786,7 +150967,7 @@ async function backfillVectorEmbeddings(options = {}) {
150786
150967
  ...lastFailure ? { lastError: lastFailure } : {}
150787
150968
  };
150788
150969
  }
150789
- var observations, nextId, projectDir, searchIndexPrepared, embeddingWriteMode, embeddingWorkerProjectRoot, corpusLoaded, loadedObservationStore, vectorMissingIds, vectorBackfillRunning, vectorSchemaUpgradePromise, lastVectorBackfill, embeddingFailureLogTimestamps, EMBEDDING_FAILURE_LOG_COOLDOWN_MS;
150970
+ var observations, nextId, projectDir, searchIndexPrepared, embeddingWriteMode, embeddingWorkerProjectRoot, corpusLoaded, loadedObservationStore, vectorMissingIds, vectorBackfillRunning, vectorSchemaUpgradePromise, lastVectorBackfill, embeddingFailureLogTimestamps, EMBEDDING_FAILURE_LOG_COOLDOWN_MS, MAX_EMBEDDING_FAILURE_KEYS;
150790
150971
  var init_observations = __esm({
150791
150972
  "src/memory/observations.ts"() {
150792
150973
  "use strict";
@@ -150814,6 +150995,7 @@ var init_observations = __esm({
150814
150995
  lastVectorBackfill = null;
150815
150996
  embeddingFailureLogTimestamps = /* @__PURE__ */ new Map();
150816
150997
  EMBEDDING_FAILURE_LOG_COOLDOWN_MS = 3e4;
150998
+ MAX_EMBEDDING_FAILURE_KEYS = 1024;
150817
150999
  }
150818
151000
  });
150819
151001