memorix 1.8.9 → 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 (53) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +7 -0
  3. package/README.zh-CN.md +6 -0
  4. package/dist/cli/index.js +3261 -1348
  5. package/dist/index.js +324 -187
  6. package/dist/maintenance-runner.js +285 -103
  7. package/dist/media-audio-runner.js +285 -103
  8. package/dist/media-video-runner.js +285 -103
  9. package/dist/memcode-runtime/CHANGELOG.md +18 -0
  10. package/dist/sdk.js +323 -186
  11. package/dist/vector-backfill-runner.d.ts +4 -1
  12. package/dist/vector-backfill-runner.js +288 -104
  13. package/docs/API_REFERENCE.md +14 -0
  14. package/docs/CONFIGURATION.md +46 -0
  15. package/docs/DEVELOPMENT.md +2 -2
  16. package/docs/MULTI-DEVICE-SYNC-RESEARCH.md +344 -0
  17. package/docs/PERFORMANCE.md +32 -0
  18. package/docs/README.md +4 -2
  19. package/llms-full.txt +1 -0
  20. package/llms.txt +1 -0
  21. package/package.json +1 -1
  22. package/plugins/antigravity/memorix/plugin.json +1 -1
  23. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  24. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  25. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  26. package/plugins/copilot/memorix/plugin.json +1 -1
  27. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  28. package/plugins/hermes/memorix/plugin.yaml +1 -1
  29. package/plugins/omp/memorix/package.json +1 -1
  30. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  31. package/plugins/pi/memorix/package.json +1 -1
  32. package/server.json +2 -2
  33. package/src/cli/command-guide.ts +9 -1
  34. package/src/cli/commands/serve-http.ts +11 -0
  35. package/src/cli/commands/sync-store.ts +191 -0
  36. package/src/cli/commands/sync.ts +8 -0
  37. package/src/memory/observations.ts +15 -2
  38. package/src/runtime/maintenance-targets.ts +6 -0
  39. package/src/runtime/vector-backfill-runner.ts +185 -7
  40. package/src/store/orama-store.ts +85 -102
  41. package/src/sync/adapters/fs.ts +179 -0
  42. package/src/sync/adapters/github.ts +258 -0
  43. package/src/sync/adapters/object-store.ts +267 -0
  44. package/src/sync/adapters/postgres.ts +269 -0
  45. package/src/sync/engine.ts +353 -0
  46. package/src/sync/journal.ts +162 -0
  47. package/src/sync/merge.ts +116 -0
  48. package/src/sync/namespace.ts +15 -0
  49. package/src/sync/paging.ts +45 -0
  50. package/src/sync/policy.ts +37 -0
  51. package/src/sync/remote-factory.ts +49 -0
  52. package/src/sync/store-port.ts +288 -0
  53. package/src/sync/types.ts +183 -0
@@ -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;
@@ -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/knowledge/workspace-store.ts
8326
8361
  import { randomUUID as randomUUID4 } from "crypto";
8327
8362
  function arrayValue(raw) {
@@ -150007,10 +150042,21 @@ __export(vector_backfill_runner_exports, {
150007
150042
  launchDetachedVectorBackfill: () => launchDetachedVectorBackfill,
150008
150043
  main: () => main,
150009
150044
  parseVectorBackfillRequest: () => parseVectorBackfillRequest,
150010
- resolveVectorBackfillRunnerPath: () => resolveVectorBackfillRunnerPath
150045
+ releaseVectorBackfillLock: () => releaseVectorBackfillLock,
150046
+ resolveVectorBackfillRunnerPath: () => resolveVectorBackfillRunnerPath,
150047
+ vectorBackfillLockPath: () => vectorBackfillLockPath
150011
150048
  });
150012
150049
  import { spawn as spawn3 } from "child_process";
150013
- import { existsSync as existsSync11 } from "fs";
150050
+ import {
150051
+ closeSync,
150052
+ existsSync as existsSync11,
150053
+ mkdirSync as mkdirSync2,
150054
+ openSync,
150055
+ readFileSync as readFileSync9,
150056
+ statSync as statSync4,
150057
+ unlinkSync,
150058
+ writeFileSync as writeFileSync2
150059
+ } from "fs";
150014
150060
  import path20 from "path";
150015
150061
  import { fileURLToPath as fileURLToPath4 } from "url";
150016
150062
  function isNonEmptyString(value) {
@@ -150037,10 +150083,104 @@ function parseVectorBackfillRequest(raw) {
150037
150083
  dataDir: value.dataDir
150038
150084
  };
150039
150085
  }
150086
+ function vectorBackfillLockPath(dataDir) {
150087
+ return path20.join(dataDir, VECTOR_BACKFILL_LOCK_FILE);
150088
+ }
150089
+ function processIsAlive(pid) {
150090
+ if (!Number.isSafeInteger(pid) || pid == null || pid <= 0) return false;
150091
+ try {
150092
+ process.kill(pid, 0);
150093
+ return true;
150094
+ } catch (error) {
150095
+ return error.code === "EPERM";
150096
+ }
150097
+ }
150098
+ function readVectorBackfillLock(lockPath) {
150099
+ try {
150100
+ const value = JSON.parse(readFileSync9(lockPath, "utf8"));
150101
+ if (typeof value.parentPid !== "number" || !Number.isSafeInteger(value.parentPid) || typeof value.startedAt !== "number" || !Number.isFinite(value.startedAt) || typeof value.projectId !== "string") return void 0;
150102
+ return {
150103
+ parentPid: value.parentPid,
150104
+ ...typeof value.childPid === "number" ? { childPid: value.childPid } : {},
150105
+ startedAt: value.startedAt,
150106
+ projectId: value.projectId
150107
+ };
150108
+ } catch {
150109
+ return void 0;
150110
+ }
150111
+ }
150112
+ function writeVectorBackfillLock(lockPath, value) {
150113
+ writeFileSync2(lockPath, `${JSON.stringify(value)}
150114
+ `, "utf8");
150115
+ }
150116
+ function acquireVectorBackfillLock(dataDir, projectId, preferredPath) {
150117
+ const lockPath = preferredPath ?? vectorBackfillLockPath(dataDir);
150118
+ mkdirSync2(path20.dirname(lockPath), { recursive: true });
150119
+ const initial = {
150120
+ parentPid: process.pid,
150121
+ startedAt: Date.now(),
150122
+ projectId
150123
+ };
150124
+ for (let attempt = 0; attempt < 2; attempt++) {
150125
+ try {
150126
+ const fd = openSync(lockPath, "wx");
150127
+ try {
150128
+ writeFileSync2(fd, `${JSON.stringify(initial)}
150129
+ `, "utf8");
150130
+ } finally {
150131
+ closeSync(fd);
150132
+ }
150133
+ return {
150134
+ path: lockPath,
150135
+ setChildPid(pid) {
150136
+ try {
150137
+ writeVectorBackfillLock(lockPath, { ...initial, childPid: pid });
150138
+ } catch {
150139
+ }
150140
+ },
150141
+ release() {
150142
+ try {
150143
+ unlinkSync(lockPath);
150144
+ } catch {
150145
+ }
150146
+ }
150147
+ };
150148
+ } catch (error) {
150149
+ if (error.code !== "EEXIST") return void 0;
150150
+ const existing = readVectorBackfillLock(lockPath);
150151
+ const ownerAlive = existing && (processIsAlive(existing.childPid) || processIsAlive(existing.parentPid));
150152
+ if (ownerAlive) return void 0;
150153
+ let stale = false;
150154
+ try {
150155
+ stale = Date.now() - statSync4(lockPath).mtimeMs >= STALE_LOCK_RECOVERY_MS;
150156
+ } catch {
150157
+ stale = false;
150158
+ }
150159
+ if (!existing && !stale) return void 0;
150160
+ try {
150161
+ unlinkSync(lockPath);
150162
+ } catch {
150163
+ return void 0;
150164
+ }
150165
+ }
150166
+ }
150167
+ return void 0;
150168
+ }
150169
+ function releaseVectorBackfillLock(lockPath = process.env.MEMORIX_VECTOR_BACKFILL_LOCK) {
150170
+ if (!lockPath) return;
150171
+ const lock = readVectorBackfillLock(lockPath);
150172
+ if (lock && lock.childPid !== process.pid) return;
150173
+ try {
150174
+ unlinkSync(lockPath);
150175
+ } catch {
150176
+ }
150177
+ }
150040
150178
  function launchDetachedVectorBackfill(request, options = {}) {
150041
150179
  const runnerPath = options.runnerPath ?? resolveVectorBackfillRunnerPath();
150042
150180
  const exists = options.exists ?? existsSync11;
150043
150181
  if (!exists(runnerPath)) return false;
150182
+ const lock = acquireVectorBackfillLock(request.dataDir, request.projectId, options.lockPath);
150183
+ if (!lock) return false;
150044
150184
  try {
150045
150185
  const child = (options.spawn ?? spawn3)(process.execPath, [runnerPath], {
150046
150186
  cwd: request.projectRoot,
@@ -150050,14 +150190,23 @@ function launchDetachedVectorBackfill(request, options = {}) {
150050
150190
  stdio: "ignore",
150051
150191
  // The request contains only local project metadata, never credentials.
150052
150192
  // Environment transport avoids a live stdin pipe keeping the CLI alive.
150053
- env: { ...process.env, MEMORIX_VECTOR_BACKFILL_REQUEST: JSON.stringify(request) },
150193
+ env: {
150194
+ ...process.env,
150195
+ MEMORIX_VECTOR_BACKFILL_REQUEST: JSON.stringify(request),
150196
+ MEMORIX_VECTOR_BACKFILL_LOCK: lock.path
150197
+ },
150054
150198
  windowsHide: true
150055
150199
  });
150200
+ if (child.pid) lock.setChildPid(child.pid);
150201
+ else lock.release();
150056
150202
  child.once?.("error", () => {
150057
150203
  });
150204
+ child.once?.("close", () => lock.release());
150205
+ if (typeof child.once !== "function") lock.release();
150058
150206
  child.unref();
150059
- return true;
150207
+ return Boolean(child.pid);
150060
150208
  } catch {
150209
+ lock.release();
150061
150210
  return false;
150062
150211
  }
150063
150212
  }
@@ -150069,12 +150218,32 @@ async function executeVectorBackfill(request) {
150069
150218
  await prepareSearchIndex();
150070
150219
  await getDeferredCachedVectorHydration()?.catch(() => {
150071
150220
  });
150221
+ const queue = new MaintenanceJobStore(request.dataDir);
150222
+ const targets = new MaintenanceTargetStore(request.dataDir);
150223
+ let preparedRoot = request.projectRoot;
150224
+ const prepareTarget = async (target) => {
150225
+ if (target.projectRoot === preparedRoot) return;
150226
+ initProjectRoot(target.projectRoot);
150227
+ loadDotenv(target.projectRoot);
150228
+ preparedRoot = target.projectRoot;
150229
+ };
150230
+ const handler = async (job) => {
150231
+ const target = targets.get(job.projectId) ?? (job.projectId === request.projectId ? { projectId: request.projectId, projectRoot: request.projectRoot, dataDir: request.dataDir, updatedAt: Date.now() } : void 0);
150232
+ if (!target) return { action: "reschedule", delayMs: 3e4 };
150233
+ await prepareTarget(target);
150234
+ return createProjectMaintenanceHandler(job.projectId, target.dataDir, target.projectRoot)(job);
150235
+ };
150072
150236
  const worker = new MaintenanceJobWorker(
150073
- new MaintenanceJobStore(request.dataDir),
150074
- createProjectMaintenanceHandler(request.projectId, request.dataDir, request.projectRoot),
150075
- { projectId: request.projectId, kinds: ["vector-backfill"] }
150237
+ queue,
150238
+ handler,
150239
+ { kinds: ["vector-backfill"] }
150076
150240
  );
150077
- return worker.runOnce();
150241
+ let result = { state: "idle" };
150242
+ while (true) {
150243
+ result = await worker.runOnce();
150244
+ if (result.state === "idle") break;
150245
+ }
150246
+ return result;
150078
150247
  }
150079
150248
  async function readStdin() {
150080
150249
  return new Promise((resolve2, reject) => {
@@ -150097,9 +150266,11 @@ async function main() {
150097
150266
  `);
150098
150267
  process.exitCode = 1;
150099
150268
  } finally {
150269
+ releaseVectorBackfillLock();
150100
150270
  closeAllDatabases();
150101
150271
  }
150102
150272
  }
150273
+ var VECTOR_BACKFILL_LOCK_FILE, STALE_LOCK_RECOVERY_MS;
150103
150274
  var init_vector_backfill_runner = __esm({
150104
150275
  "src/runtime/vector-backfill-runner.ts"() {
150105
150276
  "use strict";
@@ -150112,7 +150283,10 @@ var init_vector_backfill_runner = __esm({
150112
150283
  init_orama_store();
150113
150284
  init_sqlite_db();
150114
150285
  init_maintenance_jobs();
150286
+ init_maintenance_targets();
150115
150287
  init_project_maintenance();
150288
+ VECTOR_BACKFILL_LOCK_FILE = ".memorix-vector-backfill.lock";
150289
+ STALE_LOCK_RECOVERY_MS = 24 * 60 * 60 * 1e3;
150116
150290
  if (process.argv[1] && process.argv[1].endsWith("vector-backfill-runner.js")) {
150117
150291
  void main();
150118
150292
  }
@@ -150154,11 +150328,17 @@ function resetObservationRuntime() {
150154
150328
  embeddingWorkerProjectRoot = void 0;
150155
150329
  vectorMissingIds.clear();
150156
150330
  vectorSchemaUpgradePromise = null;
150331
+ embeddingFailureLogTimestamps.clear();
150157
150332
  }
150158
150333
  function logEmbeddingFailureOnce(key, message) {
150159
150334
  const now3 = Date.now();
150160
150335
  const last = embeddingFailureLogTimestamps.get(key) ?? 0;
150161
150336
  if (now3 - last < EMBEDDING_FAILURE_LOG_COOLDOWN_MS) return;
150337
+ if (!embeddingFailureLogTimestamps.has(key) && embeddingFailureLogTimestamps.size >= MAX_EMBEDDING_FAILURE_KEYS) {
150338
+ const oldest = embeddingFailureLogTimestamps.keys().next().value;
150339
+ if (oldest !== void 0) embeddingFailureLogTimestamps.delete(oldest);
150340
+ }
150341
+ embeddingFailureLogTimestamps.delete(key);
150162
150342
  embeddingFailureLogTimestamps.set(key, now3);
150163
150343
  console.error(message);
150164
150344
  }
@@ -150188,13 +150368,14 @@ function queueVectorBackfill(projectId, options = {}) {
150188
150368
  const dataDir = projectDir;
150189
150369
  if (!dataDir) return;
150190
150370
  void Promise.resolve().then(() => (init_maintenance_jobs(), maintenance_jobs_exports)).then(({ MaintenanceJobStore: MaintenanceJobStore2 }) => {
150191
- new MaintenanceJobStore2(dataDir).enqueue({
150371
+ const job = new MaintenanceJobStore2(dataDir).enqueue({
150192
150372
  projectId,
150193
150373
  kind: "vector-backfill",
150194
150374
  dedupeKey: "vector-backfill",
150195
150375
  payload: { limit: 12 }
150196
150376
  });
150197
- if (options.detachedWorker && options.projectRoot) {
150377
+ const due = !job || job.status === "pending" && job.runAfter <= Date.now();
150378
+ if (due && options.detachedWorker && options.projectRoot) {
150198
150379
  void Promise.resolve().then(() => (init_vector_backfill_runner(), vector_backfill_runner_exports)).then(({ launchDetachedVectorBackfill: launchDetachedVectorBackfill2 }) => {
150199
150380
  launchDetachedVectorBackfill2({
150200
150381
  projectId,
@@ -151125,7 +151306,7 @@ async function backfillVectorEmbeddings(options = {}) {
151125
151306
  ...lastFailure ? { lastError: lastFailure } : {}
151126
151307
  };
151127
151308
  }
151128
- var observations, nextId, projectDir, searchIndexPrepared, embeddingWriteMode, embeddingWorkerProjectRoot, corpusLoaded, loadedObservationStore, vectorMissingIds, vectorBackfillRunning, vectorSchemaUpgradePromise, lastVectorBackfill, embeddingFailureLogTimestamps, EMBEDDING_FAILURE_LOG_COOLDOWN_MS;
151309
+ var observations, nextId, projectDir, searchIndexPrepared, embeddingWriteMode, embeddingWorkerProjectRoot, corpusLoaded, loadedObservationStore, vectorMissingIds, vectorBackfillRunning, vectorSchemaUpgradePromise, lastVectorBackfill, embeddingFailureLogTimestamps, EMBEDDING_FAILURE_LOG_COOLDOWN_MS, MAX_EMBEDDING_FAILURE_KEYS;
151129
151310
  var init_observations = __esm({
151130
151311
  "src/memory/observations.ts"() {
151131
151312
  "use strict";
@@ -151153,6 +151334,7 @@ var init_observations = __esm({
151153
151334
  lastVectorBackfill = null;
151154
151335
  embeddingFailureLogTimestamps = /* @__PURE__ */ new Map();
151155
151336
  EMBEDDING_FAILURE_LOG_COOLDOWN_MS = 3e4;
151337
+ MAX_EMBEDDING_FAILURE_KEYS = 1024;
151156
151338
  }
151157
151339
  });
151158
151340
 
@@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.9.0] - 2026-09-07
8
+
9
+ ### Added
10
+ - **Opt-in multi-device sync** -- adds a project-scoped observation
11
+ event contract with privacy filtering, local outbox retry, conflict evidence,
12
+ device-clone protection, bounded relay pulls, and filesystem/GitHub/S3/
13
+ Postgres transport boundaries. It is disabled by default and keeps local
14
+ SQLite as the canonical store.
15
+ - **Sync hardening** -- retracts records that become private or agent-targeted,
16
+ keeps imported observations and sync state atomic, rejects immutable-key
17
+ collisions, verifies every pulled content hash, and uses stable continuation
18
+ cursors so large pulls do not skip events.
19
+ - **Resource hardening** -- coalesces deferred vector workers across hook
20
+ processes, respects retry cooldowns, drains due vector jobs through one
21
+ recoverable worker, removes the redundant full-document detail cache, and
22
+ batches startup vector hydration to reduce CPU/RAM peaks without imposing a
23
+ small corpus limit.
24
+
7
25
  ## [1.8.9] - 2026-09-06
8
26
 
9
27
  ### Added