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
@@ -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
 
@@ -78,6 +78,10 @@ memorix audit project
78
78
  memorix transfer export --format markdown --out ./memorix-export.md
79
79
  memorix skills show --name auth-pattern
80
80
  memorix sync workspace --action scan
81
+ memorix sync store status --json
82
+ memorix sync store push
83
+ memorix sync store pull
84
+ memorix sync store device rotate
81
85
  memorix ingest image --path ./diagram.png
82
86
  memorix media import --path ./architecture.png
83
87
  memorix media attach --asset <asset-id> --title "Architecture diagram"
@@ -87,6 +91,16 @@ memorix receipt --json --probe "release blocker"
87
91
 
88
92
  The CLI is for direct terminal use, not a 1:1 mirror of MCP tool names. It does not require an MCP connection. `--cwd` selects a Git project from any shell; an unbound terminal has project-visible access only, including transfer exports. `memorix identity join|use|clear` makes personal/team access and coordination explicit, while `--as <agent-id>` is the one-command equivalent for scripts. The only MCP-only area is the optional graph-compatibility tools (`create_entities`, `read_graph`, and related tools) for workflows that expect the official memory-server style graph API.
89
93
 
94
+ `memorix sync store` is an explicit, opt-in multi-device observation relay. It
95
+ uses the current Git project as its namespace, excludes personal/agent-targeted
96
+ and unqualified records, keeps a local retry outbox, and stores cursors locally.
97
+ The default `status` action is preview-only. GitHub stores immutable JSONL event
98
+ blobs; it does not receive `memorix.db`, `memorix.db-wal`, or `memorix.db-shm`.
99
+ Use `memorix sync store compact --dry --through device=sequence` to preview
100
+ remote log cleanup; add `--yes` only when the acknowledged cutoffs are correct.
101
+ The S3 adapter supports an optional `MEMORIX_SYNC_S3_PREFIX` so several
102
+ independent stores can share one bucket without crossing key spaces.
103
+
90
104
  ### Memory Autopilot, Code State, and Context Packs
91
105
 
92
106
  Memory Autopilot is the default agent-facing path for coding context. It builds a bounded task Workset instead of dumping old chat text: current project facts, Code State, selected observations, source-backed claims, matching knowledge/workflow starts, cautions, and verification hints. Code State stores versioned local file, symbol, import-edge, and memory-to-code reference facts beside project memory. It is not a replacement for normal file reads. Its job is to help agents decide which memories still point at current code, which ones are stale, and which files/symbols deserve inspection next.