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
@@ -10,11 +10,14 @@ interface VectorBackfillLauncherOptions {
10
10
  runnerPath?: string;
11
11
  exists?: (path: string) => boolean;
12
12
  spawn?: typeof spawn;
13
+ lockPath?: string;
13
14
  }
14
15
  /** Resolve the standalone worker next to either the library or CLI bundle. */
15
16
  declare function resolveVectorBackfillRunnerPath(moduleUrl?: string): string;
16
17
  /** Parse the internal request passed from a short-lived CLI process. */
17
18
  declare function parseVectorBackfillRequest(raw: string): VectorBackfillRequest;
19
+ declare function vectorBackfillLockPath(dataDir: string): string;
20
+ declare function releaseVectorBackfillLock(lockPath?: string | undefined): void;
18
21
  /**
19
22
  * Start a detached one-shot worker. The caller has already persisted the
20
23
  * observation and durable vector job, so failure to start is recoverable by a
@@ -28,4 +31,4 @@ declare function executeVectorBackfill(request: VectorBackfillRequest): Promise<
28
31
  }>;
29
32
  declare function main(): Promise<void>;
30
33
 
31
- export { type VectorBackfillLauncherOptions, type VectorBackfillRequest, executeVectorBackfill, launchDetachedVectorBackfill, main, parseVectorBackfillRequest, resolveVectorBackfillRunnerPath };
34
+ export { type VectorBackfillLauncherOptions, type VectorBackfillRequest, executeVectorBackfill, launchDetachedVectorBackfill, main, parseVectorBackfillRequest, releaseVectorBackfillLock, resolveVectorBackfillRunnerPath, vectorBackfillLockPath };
@@ -5993,6 +5993,14 @@ __export(orama_store_exports, {
5993
5993
  updateObservationMetadata: () => updateObservationMetadata
5994
5994
  });
5995
5995
  import { create, insert as insert2, insertMultiple, search, remove as remove2, update, count, getByID } from "@orama/orama";
5996
+ function rememberSearchMode(key, value) {
5997
+ if (!lastSearchModeByProject.has(key) && lastSearchModeByProject.size >= MAX_SEARCH_MODE_KEYS) {
5998
+ const oldest = lastSearchModeByProject.keys().next().value;
5999
+ if (oldest !== void 0) lastSearchModeByProject.delete(oldest);
6000
+ }
6001
+ lastSearchModeByProject.delete(key);
6002
+ lastSearchModeByProject.set(key, value);
6003
+ }
5996
6004
  function getLastSearchMode(projectId) {
5997
6005
  return lastSearchModeByProject.get(projectId ?? SEARCH_MODE_DEFAULT_KEY) ?? "fulltext";
5998
6006
  }
@@ -6005,9 +6013,6 @@ function makeEntryKey(projectId, observationId2) {
6005
6013
  function rememberObservationDoc(doc) {
6006
6014
  const publicDoc = { ...doc };
6007
6015
  delete publicDoc.embedding;
6008
- if (doc.projectId && typeof doc.observationId === "number") {
6009
- docByObservationKey.set(makeEntryKey(doc.projectId, doc.observationId), publicDoc);
6010
- }
6011
6016
  return publicDoc;
6012
6017
  }
6013
6018
  function isCommandLikeQuery(query) {
@@ -6111,7 +6116,6 @@ async function resetDb() {
6111
6116
  indexEmbeddingProvider = null;
6112
6117
  deferredCachedVectorHydration = null;
6113
6118
  lastSearchModeByProject.clear();
6114
- docByObservationKey.clear();
6115
6119
  }
6116
6120
  function isEmbeddingEnabled() {
6117
6121
  return embeddingEnabled;
@@ -6174,78 +6178,72 @@ function isCompatibleCachedVector(vector) {
6174
6178
  vector && embeddingDimensions !== null && vector.length === embeddingDimensions && vector.every(Number.isFinite)
6175
6179
  );
6176
6180
  }
6177
- async function attachCachedVectors(database, candidates) {
6178
- const cachedVectors = await getCachedEmbeddings(
6179
- candidates.map(({ observation }) => observationEmbeddingText(observation))
6180
- );
6181
- if (db !== database) return;
6182
- for (let index = 0; index < candidates.length; index++) {
6183
- const vector = cachedVectors[index];
6184
- if (!isCompatibleCachedVector(vector)) continue;
6185
- const existing = getByID(database, candidates[index].id);
6186
- if (!existing || documentEmbeddingText(existing) !== observationEmbeddingText(candidates[index].observation)) continue;
6187
- try {
6188
- await update(database, candidates[index].id, { ...existing, embedding: vector });
6189
- } catch {
6190
- }
6191
- if (index % 50 === 0) {
6192
- await new Promise((resolve2) => setImmediate(resolve2));
6181
+ async function attachCachedVectors(database, observations2) {
6182
+ const batchSize = 200;
6183
+ for (let start = 0; start < observations2.length; start += batchSize) {
6184
+ const batch = observations2.slice(start, start + batchSize);
6185
+ const cachedVectors = await getCachedEmbeddings(batch.map(observationEmbeddingText));
6186
+ if (db !== database) return;
6187
+ for (let index = 0; index < batch.length; index++) {
6188
+ const vector = cachedVectors[index];
6189
+ if (!isCompatibleCachedVector(vector)) continue;
6190
+ const id = makeOramaObservationId(batch[index].projectId, batch[index].id);
6191
+ const existing = getByID(database, id);
6192
+ if (!existing || documentEmbeddingText(existing) !== observationEmbeddingText(batch[index])) continue;
6193
+ try {
6194
+ await update(database, id, { ...existing, embedding: vector });
6195
+ } catch {
6196
+ }
6193
6197
  }
6198
+ await new Promise((resolve2) => setImmediate(resolve2));
6194
6199
  }
6195
6200
  }
6196
6201
  async function hydrateIndex(observations2, options = {}) {
6197
6202
  const database = await getDb({ allowNetworkProbe: options.allowNetworkProbe });
6198
- const candidates = [];
6199
- for (const observation of observations2) {
6200
- if (!observation || !observation.id || !observation.projectId) continue;
6201
- const id = makeOramaObservationId(observation.projectId, observation.id);
6202
- if (getByID(database, id)) continue;
6203
- candidates.push({ observation, id });
6204
- }
6205
6203
  const deferCachedVectors = options.deferCachedVectors === true && Boolean(indexEmbeddingProvider?.getCachedEmbeddings);
6206
- const cachedVectors = deferCachedVectors ? candidates.map(() => null) : await getCachedEmbeddings(candidates.map(({ observation }) => observationEmbeddingText(observation)));
6207
- const documents = [];
6208
- for (let index = 0; index < candidates.length; index++) {
6209
- const { observation: obs, id } = candidates[index];
6210
- try {
6211
- const vector = cachedVectors[index];
6212
- const compatibleVector = isCompatibleCachedVector(vector) ? vector : null;
6213
- const doc = {
6214
- id,
6215
- observationId: obs.id,
6216
- entityName: obs.entityName || "",
6217
- type: obs.type || "discovery",
6218
- title: obs.title || "",
6219
- narrative: obs.narrative || "",
6220
- facts: Array.isArray(obs.facts) ? obs.facts.join(" ") : "",
6221
- filesModified: Array.isArray(obs.filesModified) ? obs.filesModified.join(" ") : "",
6222
- concepts: Array.isArray(obs.concepts) ? obs.concepts.join(" ") : "",
6223
- tokens: obs.tokens ?? 0,
6224
- createdAt: obs.createdAt || "",
6225
- projectId: obs.projectId,
6226
- accessCount: obs.accessCount ?? 0,
6227
- lastAccessedAt: obs.lastAccessedAt || "",
6228
- status: obs.status ?? "active",
6229
- source: obs.source || "agent",
6230
- sourceDetail: obs.sourceDetail ?? "",
6231
- valueCategory: obs.valueCategory ?? "",
6232
- admissionState: obs.admissionState ?? "",
6233
- admissionReason: obs.admissionReason ?? "",
6234
- visibility: obs.visibility ?? "project",
6235
- createdByAgentId: obs.createdByAgentId ?? "",
6236
- sharedWithAgentIds: JSON.stringify(obs.sharedWithAgentIds ?? []),
6237
- documentType: "observation",
6238
- knowledgeLayer: resolveKnowledgeLayer("observation", obs.sourceDetail, obs.source),
6239
- ...compatibleVector ? { embedding: compatibleVector } : {}
6240
- };
6241
- documents.push(doc);
6242
- } catch {
6243
- }
6244
- }
6245
6204
  let inserted = 0;
6246
6205
  const hydrationBatchSize = 200;
6247
- for (let start = 0; start < documents.length; start += hydrationBatchSize) {
6248
- const batch = documents.slice(start, start + hydrationBatchSize);
6206
+ for (let start = 0; start < observations2.length; start += hydrationBatchSize) {
6207
+ const batchObservations = observations2.slice(start, start + hydrationBatchSize).filter((observation) => observation && observation.id && observation.projectId).filter((observation) => !getByID(database, makeOramaObservationId(observation.projectId, observation.id)));
6208
+ if (batchObservations.length === 0) continue;
6209
+ const cachedVectors = deferCachedVectors || options.skipCachedVectors ? batchObservations.map(() => null) : await getCachedEmbeddings(batchObservations.map(observationEmbeddingText));
6210
+ const batch = [];
6211
+ for (let index = 0; index < batchObservations.length; index++) {
6212
+ const obs = batchObservations[index];
6213
+ try {
6214
+ const compatibleVector = isCompatibleCachedVector(cachedVectors[index]) ? cachedVectors[index] : null;
6215
+ batch.push({
6216
+ id: makeOramaObservationId(obs.projectId, obs.id),
6217
+ observationId: obs.id,
6218
+ entityName: obs.entityName || "",
6219
+ type: obs.type || "discovery",
6220
+ title: obs.title || "",
6221
+ narrative: obs.narrative || "",
6222
+ facts: Array.isArray(obs.facts) ? obs.facts.join(" ") : "",
6223
+ filesModified: Array.isArray(obs.filesModified) ? obs.filesModified.join(" ") : "",
6224
+ concepts: Array.isArray(obs.concepts) ? obs.concepts.join(" ") : "",
6225
+ tokens: obs.tokens ?? 0,
6226
+ createdAt: obs.createdAt || "",
6227
+ projectId: obs.projectId,
6228
+ accessCount: obs.accessCount ?? 0,
6229
+ lastAccessedAt: obs.lastAccessedAt || "",
6230
+ status: obs.status ?? "active",
6231
+ source: obs.source || "agent",
6232
+ sourceDetail: obs.sourceDetail ?? "",
6233
+ valueCategory: obs.valueCategory ?? "",
6234
+ admissionState: obs.admissionState ?? "",
6235
+ admissionReason: obs.admissionReason ?? "",
6236
+ visibility: obs.visibility ?? "project",
6237
+ createdByAgentId: obs.createdByAgentId ?? "",
6238
+ sharedWithAgentIds: JSON.stringify(obs.sharedWithAgentIds ?? []),
6239
+ documentType: "observation",
6240
+ knowledgeLayer: resolveKnowledgeLayer("observation", obs.sourceDetail, obs.source),
6241
+ ...compatibleVector ? { embedding: compatibleVector } : {}
6242
+ });
6243
+ } catch {
6244
+ }
6245
+ }
6246
+ if (batch.length === 0) continue;
6249
6247
  try {
6250
6248
  await insertMultiple(database, batch, batch.length);
6251
6249
  for (const doc of batch) rememberObservationDoc(doc);
@@ -6267,7 +6265,7 @@ async function hydrateIndex(observations2, options = {}) {
6267
6265
  }
6268
6266
  await new Promise((resolve2) => setImmediate(resolve2));
6269
6267
  }
6270
- deferredCachedVectorHydration = options.skipCachedVectors ? null : deferCachedVectors ? attachCachedVectors(database, candidates).catch(() => {
6268
+ deferredCachedVectorHydration = options.skipCachedVectors ? null : deferCachedVectors ? attachCachedVectors(database, observations2).catch(() => {
6271
6269
  }) : null;
6272
6270
  return inserted;
6273
6271
  }
@@ -6304,12 +6302,6 @@ async function updateObservationMetadata(projectId, observationId2, patch) {
6304
6302
  async function removeObservation(oramaId) {
6305
6303
  const database = await getDb();
6306
6304
  await remove2(database, oramaId);
6307
- for (const [key, doc] of docByObservationKey.entries()) {
6308
- if (doc.id === oramaId) {
6309
- docByObservationKey.delete(key);
6310
- break;
6311
- }
6312
- }
6313
6305
  }
6314
6306
  async function searchObservations(options) {
6315
6307
  const perf = !!process.env.MEMORIX_PERF;
@@ -6324,7 +6316,7 @@ async function searchObservations(options) {
6324
6316
  const modeKey = options.projectId ?? SEARCH_MODE_DEFAULT_KEY;
6325
6317
  const quality = options.quality ?? "balanced";
6326
6318
  const database = await getDb();
6327
- lastSearchModeByProject.set(
6319
+ rememberSearchMode(
6328
6320
  modeKey,
6329
6321
  quality === "fast" ? "fulltext (fast profile)" : embeddingEnabled ? "hybrid" : "fulltext"
6330
6322
  );
@@ -6391,7 +6383,7 @@ async function searchObservations(options) {
6391
6383
  if (provider2) {
6392
6384
  const activeVectorDimensions = getVectorDimensions();
6393
6385
  if (activeVectorDimensions !== null && provider2.dimensions !== activeVectorDimensions) {
6394
- lastSearchModeByProject.set(
6386
+ rememberSearchMode(
6395
6387
  modeKey,
6396
6388
  `fulltext (embedding dimension mismatch: provider ${provider2.dimensions}d vs index ${activeVectorDimensions}d)`
6397
6389
  );
@@ -6416,7 +6408,7 @@ async function searchObservations(options) {
6416
6408
  mark("embedding");
6417
6409
  const cjkRatio = (originalQuery.match(/[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/g) || []).length / originalQuery.length;
6418
6410
  const isCJKHeavy = cjkRatio > 0.3;
6419
- lastSearchModeByProject.set(modeKey, "hybrid");
6411
+ rememberSearchMode(modeKey, "hybrid");
6420
6412
  searchParams = {
6421
6413
  ...searchParams,
6422
6414
  mode: "hybrid",
@@ -6432,7 +6424,7 @@ async function searchObservations(options) {
6432
6424
  }
6433
6425
  }
6434
6426
  } catch (error) {
6435
- lastSearchModeByProject.set(modeKey, "fulltext (embedding unavailable)");
6427
+ rememberSearchMode(modeKey, "fulltext (embedding unavailable)");
6436
6428
  console.error("[memorix] Embedding failed or timed out, falling back to fulltext search");
6437
6429
  }
6438
6430
  }
@@ -6442,7 +6434,7 @@ async function searchObservations(options) {
6442
6434
  results = await search(database, searchParams);
6443
6435
  } catch (error) {
6444
6436
  if (queryVector && isVectorDimensionMismatchError(error)) {
6445
- lastSearchModeByProject.set(modeKey, "fulltext (embedding dimension mismatch)");
6437
+ rememberSearchMode(modeKey, "fulltext (embedding dimension mismatch)");
6446
6438
  console.error("[memorix] Vector search dimension mismatch detected, retrying without embeddings");
6447
6439
  results = await search(database, stripVectorSearchParams(searchParams));
6448
6440
  } else {
@@ -6464,7 +6456,7 @@ async function searchObservations(options) {
6464
6456
  },
6465
6457
  similarity: 0.25
6466
6458
  };
6467
- lastSearchModeByProject.set(modeKey, "vector-only (hybrid empty fallback)");
6459
+ rememberSearchMode(modeKey, "vector-only (hybrid empty fallback)");
6468
6460
  results = await search(database, vectorOnlyParams);
6469
6461
  } catch {
6470
6462
  }
@@ -6705,7 +6697,7 @@ async function searchObservations(options) {
6705
6697
  const rerankedTop = neural.map((row) => candidateMap.get(row.id)).filter((entry) => entry != null);
6706
6698
  if (rerankedTop.length > 0) {
6707
6699
  intermediate = [...rerankedTop, ...intermediate.slice(RERANK_TOP_K)];
6708
- lastSearchModeByProject.set(modeKey, (lastSearchModeByProject.get(modeKey) ?? "fulltext") + " + neural rerank");
6700
+ rememberSearchMode(modeKey, (lastSearchModeByProject.get(modeKey) ?? "fulltext") + " + neural rerank");
6709
6701
  mark("rerank(usedNeural=true)");
6710
6702
  }
6711
6703
  }
@@ -6722,7 +6714,7 @@ async function searchObservations(options) {
6722
6714
  );
6723
6715
  mark(`rerank(usedLLM=${usedLLM})`);
6724
6716
  if (usedLLM) {
6725
- lastSearchModeByProject.set(modeKey, (lastSearchModeByProject.get(modeKey) ?? "fulltext") + " + LLM rerank");
6717
+ rememberSearchMode(modeKey, (lastSearchModeByProject.get(modeKey) ?? "fulltext") + " + LLM rerank");
6726
6718
  const rerankedTop = reranked.map((row) => candidateMap.get(row.id)).filter((entry) => entry != null);
6727
6719
  if (rerankedTop.length > 0) {
6728
6720
  intermediate = [...rerankedTop, ...intermediate.slice(RERANK_TOP_K)];
@@ -6795,13 +6787,6 @@ async function getObservationsByIds(ids, projectId) {
6795
6787
  const database = await getDb();
6796
6788
  const results = [];
6797
6789
  for (const id of ids) {
6798
- if (projectId) {
6799
- const cached = docByObservationKey.get(makeEntryKey(projectId, id));
6800
- if (cached) {
6801
- results.push(cached);
6802
- continue;
6803
- }
6804
- }
6805
6790
  const searchResult = await search(database, {
6806
6791
  term: "",
6807
6792
  includeVectors: true,
@@ -6901,7 +6886,7 @@ function formatTime(isoDate) {
6901
6886
  return isoDate;
6902
6887
  }
6903
6888
  }
6904
- 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;
6889
+ 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;
6905
6890
  var init_orama_store = __esm({
6906
6891
  "src/store/orama-store.ts"() {
6907
6892
  "use strict";
@@ -6922,10 +6907,10 @@ var init_orama_store = __esm({
6922
6907
  embeddingDimensions = null;
6923
6908
  indexEmbeddingProvider = null;
6924
6909
  deferredCachedVectorHydration = null;
6925
- docByObservationKey = /* @__PURE__ */ new Map();
6926
6910
  NON_CJK_HYBRID_SIMILARITY = 0.45;
6927
6911
  lastSearchModeByProject = /* @__PURE__ */ new Map();
6928
6912
  SEARCH_MODE_DEFAULT_KEY = "__global__";
6913
+ MAX_SEARCH_MODE_KEYS = 4096;
6929
6914
  COMMAND_LOG_TITLE = /^(Ran:|Command:|Executed:)\s/i;
6930
6915
  COMMAND_STYLE_TITLE = /(\bfindstr\b|\bSelect-String\b|\bGet-Content\b|\bnpx\s+vitest\b|\bnpx\s+tsc\b|\b2>&1\b)/i;
6931
6916
  COMMAND_LIKE_QUERY2 = /\b(git|npm|npx|pnpm|yarn|node|bash|powershell|curl|memorix)\b/i;
@@ -9563,11 +9548,17 @@ function resetObservationRuntime() {
9563
9548
  embeddingWorkerProjectRoot = void 0;
9564
9549
  vectorMissingIds.clear();
9565
9550
  vectorSchemaUpgradePromise = null;
9551
+ embeddingFailureLogTimestamps.clear();
9566
9552
  }
9567
9553
  function logEmbeddingFailureOnce(key, message) {
9568
9554
  const now3 = Date.now();
9569
9555
  const last = embeddingFailureLogTimestamps.get(key) ?? 0;
9570
9556
  if (now3 - last < EMBEDDING_FAILURE_LOG_COOLDOWN_MS) return;
9557
+ if (!embeddingFailureLogTimestamps.has(key) && embeddingFailureLogTimestamps.size >= MAX_EMBEDDING_FAILURE_KEYS) {
9558
+ const oldest = embeddingFailureLogTimestamps.keys().next().value;
9559
+ if (oldest !== void 0) embeddingFailureLogTimestamps.delete(oldest);
9560
+ }
9561
+ embeddingFailureLogTimestamps.delete(key);
9571
9562
  embeddingFailureLogTimestamps.set(key, now3);
9572
9563
  console.error(message);
9573
9564
  }
@@ -9597,13 +9588,14 @@ function queueVectorBackfill(projectId, options = {}) {
9597
9588
  const dataDir = projectDir;
9598
9589
  if (!dataDir) return;
9599
9590
  void Promise.resolve().then(() => (init_maintenance_jobs(), maintenance_jobs_exports)).then(({ MaintenanceJobStore: MaintenanceJobStore2 }) => {
9600
- new MaintenanceJobStore2(dataDir).enqueue({
9591
+ const job = new MaintenanceJobStore2(dataDir).enqueue({
9601
9592
  projectId,
9602
9593
  kind: "vector-backfill",
9603
9594
  dedupeKey: "vector-backfill",
9604
9595
  payload: { limit: 12 }
9605
9596
  });
9606
- if (options.detachedWorker && options.projectRoot) {
9597
+ const due = !job || job.status === "pending" && job.runAfter <= Date.now();
9598
+ if (due && options.detachedWorker && options.projectRoot) {
9607
9599
  void Promise.resolve().then(() => (init_vector_backfill_runner(), vector_backfill_runner_exports)).then(({ launchDetachedVectorBackfill: launchDetachedVectorBackfill2 }) => {
9608
9600
  launchDetachedVectorBackfill2({
9609
9601
  projectId,
@@ -10534,7 +10526,7 @@ async function backfillVectorEmbeddings(options = {}) {
10534
10526
  ...lastFailure ? { lastError: lastFailure } : {}
10535
10527
  };
10536
10528
  }
10537
- var observations, nextId, projectDir, searchIndexPrepared, embeddingWriteMode, embeddingWorkerProjectRoot, corpusLoaded, loadedObservationStore, vectorMissingIds, vectorBackfillRunning, vectorSchemaUpgradePromise, lastVectorBackfill, embeddingFailureLogTimestamps, EMBEDDING_FAILURE_LOG_COOLDOWN_MS;
10529
+ var observations, nextId, projectDir, searchIndexPrepared, embeddingWriteMode, embeddingWorkerProjectRoot, corpusLoaded, loadedObservationStore, vectorMissingIds, vectorBackfillRunning, vectorSchemaUpgradePromise, lastVectorBackfill, embeddingFailureLogTimestamps, EMBEDDING_FAILURE_LOG_COOLDOWN_MS, MAX_EMBEDDING_FAILURE_KEYS;
10538
10530
  var init_observations = __esm({
10539
10531
  "src/memory/observations.ts"() {
10540
10532
  "use strict";
@@ -10562,6 +10554,57 @@ var init_observations = __esm({
10562
10554
  lastVectorBackfill = null;
10563
10555
  embeddingFailureLogTimestamps = /* @__PURE__ */ new Map();
10564
10556
  EMBEDDING_FAILURE_LOG_COOLDOWN_MS = 3e4;
10557
+ MAX_EMBEDDING_FAILURE_KEYS = 1024;
10558
+ }
10559
+ });
10560
+
10561
+ // src/runtime/maintenance-targets.ts
10562
+ function rowToTarget(row) {
10563
+ return {
10564
+ projectId: row.project_id,
10565
+ projectRoot: row.project_root,
10566
+ dataDir: row.data_dir,
10567
+ updatedAt: Number(row.updated_at)
10568
+ };
10569
+ }
10570
+ var MaintenanceTargetStore;
10571
+ var init_maintenance_targets = __esm({
10572
+ "src/runtime/maintenance-targets.ts"() {
10573
+ "use strict";
10574
+ init_esm_shims();
10575
+ init_sqlite_db();
10576
+ MaintenanceTargetStore = class {
10577
+ db;
10578
+ constructor(dataDir) {
10579
+ this.db = getDatabase(dataDir);
10580
+ }
10581
+ register(input) {
10582
+ if (!input.projectId.trim()) throw new Error("Maintenance target requires a project ID");
10583
+ if (!input.projectRoot.trim()) throw new Error("Maintenance target requires a project root");
10584
+ if (!input.dataDir.trim()) throw new Error("Maintenance target requires a data directory");
10585
+ const now3 = input.now ?? Date.now();
10586
+ this.db.prepare(`
10587
+ INSERT INTO maintenance_targets (project_id, project_root, data_dir, updated_at)
10588
+ VALUES (?, ?, ?, ?)
10589
+ ON CONFLICT(project_id) DO UPDATE SET
10590
+ project_root = excluded.project_root,
10591
+ data_dir = excluded.data_dir,
10592
+ updated_at = excluded.updated_at
10593
+ `).run(input.projectId, input.projectRoot, input.dataDir, now3);
10594
+ return this.get(input.projectId);
10595
+ }
10596
+ get(projectId) {
10597
+ const row = this.db.prepare(`
10598
+ SELECT * FROM maintenance_targets WHERE project_id = ?
10599
+ `).get(projectId);
10600
+ return row ? rowToTarget(row) : void 0;
10601
+ }
10602
+ list() {
10603
+ return this.db.prepare(`
10604
+ SELECT * FROM maintenance_targets ORDER BY updated_at DESC
10605
+ `).all().map(rowToTarget);
10606
+ }
10607
+ };
10565
10608
  }
10566
10609
  });
10567
10610
 
@@ -151044,10 +151087,21 @@ __export(vector_backfill_runner_exports, {
151044
151087
  launchDetachedVectorBackfill: () => launchDetachedVectorBackfill,
151045
151088
  main: () => main,
151046
151089
  parseVectorBackfillRequest: () => parseVectorBackfillRequest,
151047
- resolveVectorBackfillRunnerPath: () => resolveVectorBackfillRunnerPath
151090
+ releaseVectorBackfillLock: () => releaseVectorBackfillLock,
151091
+ resolveVectorBackfillRunnerPath: () => resolveVectorBackfillRunnerPath,
151092
+ vectorBackfillLockPath: () => vectorBackfillLockPath
151048
151093
  });
151049
151094
  import { spawn as spawn3 } from "child_process";
151050
- import { existsSync as existsSync11 } from "fs";
151095
+ import {
151096
+ closeSync,
151097
+ existsSync as existsSync11,
151098
+ mkdirSync as mkdirSync2,
151099
+ openSync,
151100
+ readFileSync as readFileSync9,
151101
+ statSync as statSync4,
151102
+ unlinkSync,
151103
+ writeFileSync as writeFileSync2
151104
+ } from "fs";
151051
151105
  import path20 from "path";
151052
151106
  import { fileURLToPath as fileURLToPath4 } from "url";
151053
151107
  function isNonEmptyString(value) {
@@ -151074,10 +151128,104 @@ function parseVectorBackfillRequest(raw) {
151074
151128
  dataDir: value.dataDir
151075
151129
  };
151076
151130
  }
151131
+ function vectorBackfillLockPath(dataDir) {
151132
+ return path20.join(dataDir, VECTOR_BACKFILL_LOCK_FILE);
151133
+ }
151134
+ function processIsAlive(pid) {
151135
+ if (!Number.isSafeInteger(pid) || pid == null || pid <= 0) return false;
151136
+ try {
151137
+ process.kill(pid, 0);
151138
+ return true;
151139
+ } catch (error) {
151140
+ return error.code === "EPERM";
151141
+ }
151142
+ }
151143
+ function readVectorBackfillLock(lockPath) {
151144
+ try {
151145
+ const value = JSON.parse(readFileSync9(lockPath, "utf8"));
151146
+ if (typeof value.parentPid !== "number" || !Number.isSafeInteger(value.parentPid) || typeof value.startedAt !== "number" || !Number.isFinite(value.startedAt) || typeof value.projectId !== "string") return void 0;
151147
+ return {
151148
+ parentPid: value.parentPid,
151149
+ ...typeof value.childPid === "number" ? { childPid: value.childPid } : {},
151150
+ startedAt: value.startedAt,
151151
+ projectId: value.projectId
151152
+ };
151153
+ } catch {
151154
+ return void 0;
151155
+ }
151156
+ }
151157
+ function writeVectorBackfillLock(lockPath, value) {
151158
+ writeFileSync2(lockPath, `${JSON.stringify(value)}
151159
+ `, "utf8");
151160
+ }
151161
+ function acquireVectorBackfillLock(dataDir, projectId, preferredPath) {
151162
+ const lockPath = preferredPath ?? vectorBackfillLockPath(dataDir);
151163
+ mkdirSync2(path20.dirname(lockPath), { recursive: true });
151164
+ const initial = {
151165
+ parentPid: process.pid,
151166
+ startedAt: Date.now(),
151167
+ projectId
151168
+ };
151169
+ for (let attempt = 0; attempt < 2; attempt++) {
151170
+ try {
151171
+ const fd = openSync(lockPath, "wx");
151172
+ try {
151173
+ writeFileSync2(fd, `${JSON.stringify(initial)}
151174
+ `, "utf8");
151175
+ } finally {
151176
+ closeSync(fd);
151177
+ }
151178
+ return {
151179
+ path: lockPath,
151180
+ setChildPid(pid) {
151181
+ try {
151182
+ writeVectorBackfillLock(lockPath, { ...initial, childPid: pid });
151183
+ } catch {
151184
+ }
151185
+ },
151186
+ release() {
151187
+ try {
151188
+ unlinkSync(lockPath);
151189
+ } catch {
151190
+ }
151191
+ }
151192
+ };
151193
+ } catch (error) {
151194
+ if (error.code !== "EEXIST") return void 0;
151195
+ const existing = readVectorBackfillLock(lockPath);
151196
+ const ownerAlive = existing && (processIsAlive(existing.childPid) || processIsAlive(existing.parentPid));
151197
+ if (ownerAlive) return void 0;
151198
+ let stale = false;
151199
+ try {
151200
+ stale = Date.now() - statSync4(lockPath).mtimeMs >= STALE_LOCK_RECOVERY_MS;
151201
+ } catch {
151202
+ stale = false;
151203
+ }
151204
+ if (!existing && !stale) return void 0;
151205
+ try {
151206
+ unlinkSync(lockPath);
151207
+ } catch {
151208
+ return void 0;
151209
+ }
151210
+ }
151211
+ }
151212
+ return void 0;
151213
+ }
151214
+ function releaseVectorBackfillLock(lockPath = process.env.MEMORIX_VECTOR_BACKFILL_LOCK) {
151215
+ if (!lockPath) return;
151216
+ const lock = readVectorBackfillLock(lockPath);
151217
+ if (lock && lock.childPid !== process.pid) return;
151218
+ try {
151219
+ unlinkSync(lockPath);
151220
+ } catch {
151221
+ }
151222
+ }
151077
151223
  function launchDetachedVectorBackfill(request, options = {}) {
151078
151224
  const runnerPath = options.runnerPath ?? resolveVectorBackfillRunnerPath();
151079
151225
  const exists = options.exists ?? existsSync11;
151080
151226
  if (!exists(runnerPath)) return false;
151227
+ const lock = acquireVectorBackfillLock(request.dataDir, request.projectId, options.lockPath);
151228
+ if (!lock) return false;
151081
151229
  try {
151082
151230
  const child = (options.spawn ?? spawn3)(process.execPath, [runnerPath], {
151083
151231
  cwd: request.projectRoot,
@@ -151087,14 +151235,23 @@ function launchDetachedVectorBackfill(request, options = {}) {
151087
151235
  stdio: "ignore",
151088
151236
  // The request contains only local project metadata, never credentials.
151089
151237
  // Environment transport avoids a live stdin pipe keeping the CLI alive.
151090
- env: { ...process.env, MEMORIX_VECTOR_BACKFILL_REQUEST: JSON.stringify(request) },
151238
+ env: {
151239
+ ...process.env,
151240
+ MEMORIX_VECTOR_BACKFILL_REQUEST: JSON.stringify(request),
151241
+ MEMORIX_VECTOR_BACKFILL_LOCK: lock.path
151242
+ },
151091
151243
  windowsHide: true
151092
151244
  });
151245
+ if (child.pid) lock.setChildPid(child.pid);
151246
+ else lock.release();
151093
151247
  child.once?.("error", () => {
151094
151248
  });
151249
+ child.once?.("close", () => lock.release());
151250
+ if (typeof child.once !== "function") lock.release();
151095
151251
  child.unref();
151096
- return true;
151252
+ return Boolean(child.pid);
151097
151253
  } catch {
151254
+ lock.release();
151098
151255
  return false;
151099
151256
  }
151100
151257
  }
@@ -151106,12 +151263,32 @@ async function executeVectorBackfill(request) {
151106
151263
  await prepareSearchIndex();
151107
151264
  await getDeferredCachedVectorHydration()?.catch(() => {
151108
151265
  });
151266
+ const queue = new MaintenanceJobStore(request.dataDir);
151267
+ const targets = new MaintenanceTargetStore(request.dataDir);
151268
+ let preparedRoot = request.projectRoot;
151269
+ const prepareTarget = async (target) => {
151270
+ if (target.projectRoot === preparedRoot) return;
151271
+ initProjectRoot(target.projectRoot);
151272
+ loadDotenv(target.projectRoot);
151273
+ preparedRoot = target.projectRoot;
151274
+ };
151275
+ const handler = async (job) => {
151276
+ const target = targets.get(job.projectId) ?? (job.projectId === request.projectId ? { projectId: request.projectId, projectRoot: request.projectRoot, dataDir: request.dataDir, updatedAt: Date.now() } : void 0);
151277
+ if (!target) return { action: "reschedule", delayMs: 3e4 };
151278
+ await prepareTarget(target);
151279
+ return createProjectMaintenanceHandler(job.projectId, target.dataDir, target.projectRoot)(job);
151280
+ };
151109
151281
  const worker = new MaintenanceJobWorker(
151110
- new MaintenanceJobStore(request.dataDir),
151111
- createProjectMaintenanceHandler(request.projectId, request.dataDir, request.projectRoot),
151112
- { projectId: request.projectId, kinds: ["vector-backfill"] }
151282
+ queue,
151283
+ handler,
151284
+ { kinds: ["vector-backfill"] }
151113
151285
  );
151114
- return worker.runOnce();
151286
+ let result = { state: "idle" };
151287
+ while (true) {
151288
+ result = await worker.runOnce();
151289
+ if (result.state === "idle") break;
151290
+ }
151291
+ return result;
151115
151292
  }
151116
151293
  async function readStdin() {
151117
151294
  return new Promise((resolve2, reject) => {
@@ -151134,9 +151311,11 @@ async function main() {
151134
151311
  `);
151135
151312
  process.exitCode = 1;
151136
151313
  } finally {
151314
+ releaseVectorBackfillLock();
151137
151315
  closeAllDatabases();
151138
151316
  }
151139
151317
  }
151318
+ var VECTOR_BACKFILL_LOCK_FILE, STALE_LOCK_RECOVERY_MS;
151140
151319
  var init_vector_backfill_runner = __esm({
151141
151320
  "src/runtime/vector-backfill-runner.ts"() {
151142
151321
  init_esm_shims();
@@ -151148,7 +151327,10 @@ var init_vector_backfill_runner = __esm({
151148
151327
  init_orama_store();
151149
151328
  init_sqlite_db();
151150
151329
  init_maintenance_jobs();
151330
+ init_maintenance_targets();
151151
151331
  init_project_maintenance();
151332
+ VECTOR_BACKFILL_LOCK_FILE = ".memorix-vector-backfill.lock";
151333
+ STALE_LOCK_RECOVERY_MS = 24 * 60 * 60 * 1e3;
151152
151334
  if (process.argv[1] && process.argv[1].endsWith("vector-backfill-runner.js")) {
151153
151335
  void main();
151154
151336
  }
@@ -151160,7 +151342,9 @@ export {
151160
151342
  launchDetachedVectorBackfill,
151161
151343
  main,
151162
151344
  parseVectorBackfillRequest,
151163
- resolveVectorBackfillRunnerPath
151345
+ releaseVectorBackfillLock,
151346
+ resolveVectorBackfillRunnerPath,
151347
+ vectorBackfillLockPath
151164
151348
  };
151165
151349
  /*! Bundled license information:
151166
151350
 
@@ -23,7 +23,7 @@ It supports:
23
23
  - HTTP MCP service + dashboard (`memorix background start` or `memorix serve-http --port 3211`)
24
24
  - bundled terminal agent (`memorix` or `memcode`) that uses the same shared memory pool
25
25
  - local-first project-scoped memory
26
- - cross-agent recall across Cursor, Claude Code, Codex, CodeBuddy Code, Windsurf, Gemini CLI, GitHub Copilot, OpenCode, OpenClaw, Hermes Agent, Oh-my-Pi, Pi, Kiro, Antigravity, Trae, and DeepSeek Harness
26
+ - cross-agent recall across Cursor, Claude Code, Codex, CodeBuddy Code, Windsurf, Gemini CLI, GitHub Copilot, OpenCode, Grok Build, OpenClaw, Hermes Agent, Oh-my-Pi, Pi, Kiro, Antigravity, Trae, DeepSeek Harness, and WorkBuddy
27
27
 
28
28
  ### Current 1.4 Baseline
29
29
 
@@ -41,7 +41,7 @@ For the 1.4 release line, the visible product shape is:
41
41
  - `memorix_project_context` / `memorix context "..."` is the normal black-box entry for non-trivial coding work: it assembles a bounded task Workset instead of injecting a generic memory dump; `memorix resume "..."` is the explicit CLI continuation projection
42
42
  - Code State keeps local snapshots and freshness links; a healthy pre-existing local CodeGraph index can add a bounded semantic outline, but Memorix never initializes or synchronizes that external index itself
43
43
  - `memorix knowledge` is an explicit review path for source-backed Markdown knowledge and canonical workflows. Do not initialize a versioned workspace or apply a proposal unless the user asks for that managed artifact
44
- - integration surfaces are agent-specific: Claude Code, Codex, GitHub Copilot CLI, Antigravity, and Hermes receive plugin packages; OpenClaw receives a compatible bundle; Pi and Oh-my-Pi receive package entries; Gemini CLI receives an extension package; OpenCode receives a plugin file and skill; DeepSeek Harness receives an MCP patch row, AGENTS.md guidance, and skills; Cursor and other agents receive MCP/rules/hooks where supported
44
+ - integration surfaces are agent-specific: Claude Code, Codex, GitHub Copilot CLI, Antigravity, and Hermes receive plugin packages; OpenClaw receives a compatible bundle; Pi and Oh-my-Pi receive package entries; Gemini CLI receives an extension package; OpenCode receives a plugin file and skill; Grok Build receives native hooks and `AGENTS.md` guidance while keeping MCP host-owned; DeepSeek Harness receives an MCP patch row, AGENTS.md guidance, and skills; Cursor and other agents receive MCP/rules/hooks where supported
45
45
  - privacy-safe diagnostics and receipts avoid raw chat, memory text, query text, tool payloads, and local file paths
46
46
 
47
47
  ---
@@ -245,6 +245,7 @@ memorix setup --agent opencode --global
245
245
  memorix setup --agent openclaw --global
246
246
  memorix setup --agent hermes --global
247
247
  memorix setup --agent omp --global
248
+ memorix setup --agent grok --global
248
249
  ```
249
250
 
250
251
  What this installs depends on the target agent:
@@ -256,6 +257,7 @@ What this installs depends on the target agent:
256
257
  - Pi: user Pi package with extension-based hook capture and a Memorix skill, registered through `pi install <path> --approve`; project-local setup uses `-l`.
257
258
  - Gemini CLI: local extension package under `~/.gemini/extensions/memorix`, extension-bundled stdio MCP, `GEMINI.md` context, hooks, commands, and skills. Antigravity CLI has an official Gemini CLI migration path, but Gemini CLI remains a separate target.
258
259
  - OpenCode: local plugin file, `opencode.json` MCP config, OpenCode skill, plus `AGENTS.md` guidance.
260
+ - Grok Build: native hooks in `~/.grok/hooks/memorix.json` and guidance in `~/.grok/AGENTS.md`. Global hooks are trusted; project hooks require Grok `/hooks-trust` or `--trust`. Memorix does not write Grok's `config.toml` or MCP entries.
259
261
  - Antigravity: native plugin under `~/.gemini/config/plugins/memorix` or `.agents/plugins/memorix`, with `plugin.json`, `mcp_config.json`, `hooks.json`, rules, and skills.
260
262
  - OpenClaw: OpenClaw-compatible bundle under `~/.openclaw/extensions/memorix`, bundled `.mcp.json`, skills, and OpenClaw `HOOK.md`/`handler.ts` hook pack.
261
263
  - Hermes Agent: Hermes plugin under Hermes home (`%LOCALAPPDATA%\hermes` on native Windows, `~/.hermes` elsewhere, or `HERMES_HOME`), enabled in `config.yaml`, with plugin hooks, slash command, CLI command, skills, and MCP config.