memorix 1.2.7 → 1.2.8

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.
File without changes
@@ -216,6 +216,7 @@ const i18n = {
216
216
  backfillPending: 'Backfill Pending',
217
217
  vectorsMissing: 'vectors missing',
218
218
  noBackfillNeeded: 'All vectors indexed',
219
+ vectorStatusSessionLocal: 'Active MCP session',
219
220
  providerReady: 'Ready',
220
221
  providerUnavailable: 'Unavailable',
221
222
  providerDisabled: 'Disabled (BM25 only)',
@@ -647,6 +648,7 @@ const i18n = {
647
648
  backfillPending: '回填待处理',
648
649
  vectorsMissing: '条向量缺失',
649
650
  noBackfillNeeded: '所有向量已索引',
651
+ vectorStatusSessionLocal: '由活动 MCP 会话统计',
650
652
  providerReady: '就绪',
651
653
  providerUnavailable: '不可用',
652
654
  providerDisabled: '已禁用 (仅 BM25)',
@@ -1357,6 +1359,9 @@ async function loadDashboard() {
1357
1359
  : pendingProposals > 0
1358
1360
  ? { color: 'var(--accent-blue)', text: `${pendingProposals} ${t('pendingProposals')}` }
1359
1361
  : { color: 'var(--accent-green)', text: t('knowledgeReady') };
1362
+ // Standalone dashboards and older control planes do not own the MCP process's
1363
+ // in-memory index. Absence of a proof is not proof that every vector exists.
1364
+ const vectorStatusAvailable = stats.vectorStatus?.available === true;
1360
1365
 
1361
1366
  const typeIcons = {
1362
1367
  'session-request': '<span class="iconify" data-icon="lucide:target" style="color:#f87171;"></span>', gotcha: '<span class="iconify" data-icon="lucide:alert-octagon" style="color:#ef4444;"></span>', 'problem-solution': '<span class="iconify" data-icon="lucide:lightbulb" style="color:#fbbf24;"></span>',
@@ -1418,8 +1423,8 @@ async function loadDashboard() {
1418
1423
  </div>
1419
1424
  <div>
1420
1425
  <div style="font-size:11px;color:var(--text-muted);margin-bottom:4px;">${t('backfillPending')}</div>
1421
- <div style="font-size:14px;font-weight:600;color:${(stats.vectorStatus?.missing || 0) > 0 ? 'var(--accent-amber)' : 'var(--accent-green)'};">
1422
- ${(stats.vectorStatus?.missing || 0) > 0 ? stats.vectorStatus.missing + ' ' + t('vectorsMissing') : t('noBackfillNeeded')}
1426
+ <div style="font-size:14px;font-weight:600;color:${!vectorStatusAvailable ? 'var(--text-muted)' : (stats.vectorStatus?.missing || 0) > 0 ? 'var(--accent-amber)' : 'var(--accent-green)'};">
1427
+ ${!vectorStatusAvailable ? t('vectorStatusSessionLocal') : (stats.vectorStatus?.missing || 0) > 0 ? stats.vectorStatus.missing + ' ' + t('vectorsMissing') : t('noBackfillNeeded')}
1423
1428
  </div>
1424
1429
  </div>
1425
1430
  <div>
package/dist/index.js CHANGED
@@ -7181,6 +7181,10 @@ var init_maintenance_jobs = __esm({
7181
7181
  LIMIT 1
7182
7182
  `).get(input.projectId, input.kind, dedupeKey);
7183
7183
  if (existing) {
7184
+ if (input.kind === "vector-backfill" && existing.status === "retry") {
7185
+ this.commit();
7186
+ return rowToJob(existing);
7187
+ }
7184
7188
  const nextRunAfter = Math.min(Number(existing.run_after), runAfter);
7185
7189
  this.db.prepare(`
7186
7190
  UPDATE maintenance_jobs
@@ -7191,6 +7195,26 @@ var init_maintenance_jobs = __esm({
7191
7195
  this.commit();
7192
7196
  return rowToJob(updated);
7193
7197
  }
7198
+ if (input.kind === "vector-backfill") {
7199
+ const failed = this.db.prepare(`
7200
+ SELECT * FROM maintenance_jobs
7201
+ WHERE project_id = ? AND kind = ? AND dedupe_key = ? AND status = 'failed'
7202
+ ORDER BY updated_at DESC
7203
+ LIMIT 1
7204
+ `).get(input.projectId, input.kind, dedupeKey);
7205
+ if (failed) {
7206
+ this.db.prepare(`
7207
+ UPDATE maintenance_jobs
7208
+ SET status = 'pending', attempts = 0, max_attempts = ?, run_after = ?,
7209
+ payload_json = ?, lease_owner = NULL, lease_expires_at = NULL,
7210
+ last_error = NULL, completed_at = NULL, updated_at = ?
7211
+ WHERE id = ?
7212
+ `).run(maxAttempts, runAfter, payloadJson, now3, failed.id);
7213
+ const revived = this.getRow(failed.id);
7214
+ this.commit();
7215
+ return rowToJob(revived);
7216
+ }
7217
+ }
7194
7218
  const id = randomUUID2();
7195
7219
  this.db.prepare(`
7196
7220
  INSERT INTO maintenance_jobs (
@@ -7362,15 +7386,42 @@ var init_maintenance_jobs = __esm({
7362
7386
  const now3 = options.now ?? Date.now();
7363
7387
  const delayMs = Number.isFinite(options.delayMs) ? Math.max(0, Math.floor(options.delayMs)) : 0;
7364
7388
  const payloadJson = options.payload === void 0 ? null : JSON.stringify(options.payload);
7389
+ const status = options.status === "retry" ? "retry" : "pending";
7390
+ const hasLastError = options.lastError !== void 0;
7391
+ const lastError = hasLastError ? errorText(options.lastError) : null;
7365
7392
  this.db.prepare(`
7366
7393
  UPDATE maintenance_jobs
7367
- SET status = 'pending', run_after = ?, attempts = CASE WHEN ? THEN 0 ELSE attempts END,
7394
+ SET status = ?, run_after = ?, attempts = CASE WHEN ? THEN 0 ELSE attempts END,
7368
7395
  payload_json = COALESCE(?, payload_json),
7396
+ last_error = CASE
7397
+ WHEN ? THEN NULL
7398
+ WHEN ? THEN ?
7399
+ ELSE last_error
7400
+ END,
7369
7401
  lease_owner = NULL, lease_expires_at = NULL, updated_at = ?
7370
7402
  WHERE id = ? AND status = 'running' AND lease_owner = ?
7371
- `).run(now3 + delayMs, options.resetAttempts ? 1 : 0, payloadJson, now3, id, workerId);
7403
+ `).run(
7404
+ status,
7405
+ now3 + delayMs,
7406
+ options.resetAttempts ? 1 : 0,
7407
+ payloadJson,
7408
+ options.clearLastError ? 1 : 0,
7409
+ hasLastError ? 1 : 0,
7410
+ lastError,
7411
+ now3,
7412
+ id,
7413
+ workerId
7414
+ );
7372
7415
  return this.get(id);
7373
7416
  }
7417
+ resolveFailedVectorBackfills(projectId, dedupeKey = "vector-backfill", now3 = Date.now()) {
7418
+ const result = this.db.prepare(`
7419
+ UPDATE maintenance_jobs
7420
+ SET status = 'completed', completed_at = ?, updated_at = ?
7421
+ WHERE project_id = ? AND kind = 'vector-backfill' AND dedupe_key = ? AND status = 'failed'
7422
+ `).run(now3, now3, projectId, dedupeKey);
7423
+ return Number(result.changes ?? 0);
7424
+ }
7374
7425
  fail(id, workerId, error, now3 = Date.now()) {
7375
7426
  this.begin();
7376
7427
  try {
@@ -7466,6 +7517,9 @@ var init_maintenance_jobs = __esm({
7466
7517
  delayMs: result.delayMs,
7467
7518
  resetAttempts: result.resetAttempts,
7468
7519
  payload: result.payload,
7520
+ status: result.status,
7521
+ lastError: result.lastError,
7522
+ clearLastError: result.clearLastError,
7469
7523
  now: now3
7470
7524
  });
7471
7525
  return { state: "rescheduled", job: updated2 };
@@ -8444,6 +8498,9 @@ function normalizeEmbeddingFailure(error) {
8444
8498
  message: raw
8445
8499
  };
8446
8500
  }
8501
+ function vectorBackfillError(error) {
8502
+ return sanitizeCredentials(normalizeEmbeddingFailure(error).message).slice(0, 1e3);
8503
+ }
8447
8504
  function queueVectorBackfill(projectId) {
8448
8505
  const dataDir = projectDir;
8449
8506
  if (!dataDir) return;
@@ -9199,17 +9256,53 @@ async function backfillVectorEmbeddings(options = {}) {
9199
9256
  let succeeded = 0;
9200
9257
  let failed = 0;
9201
9258
  let lastFailure;
9259
+ const recordFailure = (message) => {
9260
+ if (!lastFailure) lastFailure = message;
9261
+ };
9262
+ const candidates = [];
9263
+ for (const id of ids) {
9264
+ const observation = observationById.get(id);
9265
+ if (!observation) {
9266
+ vectorMissingIds.delete(id);
9267
+ continue;
9268
+ }
9269
+ candidates.push({
9270
+ id,
9271
+ observation,
9272
+ text: [observation.title, observation.narrative, ...observation.facts].join(" ")
9273
+ });
9274
+ }
9202
9275
  try {
9203
- for (const id of ids) {
9204
- const obs = observationById.get(id);
9205
- if (!obs) {
9206
- vectorMissingIds.delete(id);
9207
- continue;
9276
+ if (candidates.length === 0) {
9277
+ return { attempted: ids.length, succeeded, failed };
9278
+ }
9279
+ let embeddings = null;
9280
+ try {
9281
+ const provider2 = await getEmbeddingProvider();
9282
+ if (!provider2) {
9283
+ if (isEmbeddingExplicitlyDisabled()) {
9284
+ for (const candidate of candidates) vectorMissingIds.delete(candidate.id);
9285
+ } else {
9286
+ recordFailure("embedding provider unavailable");
9287
+ failed += candidates.length;
9288
+ }
9289
+ } else {
9290
+ embeddings = await provider2.embedBatch(candidates.map((candidate) => candidate.text));
9208
9291
  }
9209
- const text = [obs.title, obs.narrative, ...obs.facts].join(" ");
9210
- try {
9211
- const embedding = await generateEmbedding(text);
9212
- if (embedding) {
9292
+ } catch (error) {
9293
+ recordFailure(vectorBackfillError(error));
9294
+ failed += candidates.length;
9295
+ }
9296
+ if (embeddings) {
9297
+ for (let index = 0; index < candidates.length; index++) {
9298
+ const { id, observation: obs } = candidates[index];
9299
+ const embedding = embeddings[index];
9300
+ if (!embedding || embedding.length === 0) {
9301
+ recordFailure("embedding provider returned no vector");
9302
+ failed++;
9303
+ continue;
9304
+ }
9305
+ try {
9213
9306
  if (!isVectorCompatibleWithCurrentIndex(embedding)) {
9214
9307
  await upgradeVectorSchemaAfterFirstEmbedding(embedding);
9215
9308
  }
@@ -9218,7 +9311,7 @@ async function backfillVectorEmbeddings(options = {}) {
9218
9311
  console.error(
9219
9312
  `[memorix] Backfill embedding mismatch for obs-${id}: provider returned ${embedding.length}d, index expects ${vectorDimensions ?? "unknown"}d (kept in queue)`
9220
9313
  );
9221
- lastFailure = `dimension mismatch: provider returned ${embedding.length}d, index expects ${vectorDimensions ?? "unknown"}d`;
9314
+ recordFailure(`dimension mismatch: provider returned ${embedding.length}d, index expects ${vectorDimensions ?? "unknown"}d`);
9222
9315
  failed++;
9223
9316
  continue;
9224
9317
  }
@@ -9257,15 +9350,10 @@ async function backfillVectorEmbeddings(options = {}) {
9257
9350
  await insertObservation(doc);
9258
9351
  vectorMissingIds.delete(id);
9259
9352
  succeeded++;
9260
- } else if (isEmbeddingExplicitlyDisabled()) {
9261
- vectorMissingIds.delete(id);
9262
- } else {
9263
- lastFailure = "embedding provider unavailable";
9353
+ } catch (error) {
9354
+ recordFailure(vectorBackfillError(error));
9264
9355
  failed++;
9265
9356
  }
9266
- } catch (err) {
9267
- lastFailure = err instanceof Error ? err.message : String(err);
9268
- failed++;
9269
9357
  }
9270
9358
  }
9271
9359
  } finally {
@@ -9278,7 +9366,12 @@ async function backfillVectorEmbeddings(options = {}) {
9278
9366
  finishedAt: (/* @__PURE__ */ new Date()).toISOString()
9279
9367
  };
9280
9368
  }
9281
- return { attempted: ids.length, succeeded, failed };
9369
+ return {
9370
+ attempted: ids.length,
9371
+ succeeded,
9372
+ failed,
9373
+ ...lastFailure ? { lastError: lastFailure } : {}
9374
+ };
9282
9375
  }
9283
9376
  var observations, nextId, projectDir, searchIndexPrepared, vectorMissingIds, vectorBackfillRunning, vectorSchemaUpgradePromise, lastVectorBackfill, embeddingFailureLogTimestamps, EMBEDDING_FAILURE_LOG_COOLDOWN_MS;
9284
9377
  var init_observations = __esm({
@@ -14915,6 +15008,26 @@ function vectorBatchSize(payload) {
14915
15008
  if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_VECTOR_BATCH_SIZE;
14916
15009
  return Math.min(100, Math.max(1, Math.floor(value)));
14917
15010
  }
15011
+ function vectorBackfillFailureStreak(payload) {
15012
+ const value = payload.vectorBackfillFailureStreak;
15013
+ return typeof value === "number" && Number.isFinite(value) ? Math.min(16, Math.max(0, Math.floor(value))) : 0;
15014
+ }
15015
+ function vectorBackfillRetryDelayMs(streak) {
15016
+ return Math.min(
15017
+ VECTOR_FAILURE_MAX_RETRY_DELAY_MS,
15018
+ VECTOR_FAILURE_BASE_RETRY_DELAY_MS * 2 ** Math.max(0, streak - 1)
15019
+ );
15020
+ }
15021
+ function withoutVectorBackfillFailureStreak(payload) {
15022
+ const { vectorBackfillFailureStreak: _streak, ...rest } = payload;
15023
+ return rest;
15024
+ }
15025
+ function resolveSupersededVectorFailures(projectDir2, projectId) {
15026
+ try {
15027
+ new MaintenanceJobStore(projectDir2).resolveFailedVectorBackfills(projectId);
15028
+ } catch {
15029
+ }
15030
+ }
14918
15031
  function retentionBatchSize(payload) {
14919
15032
  const value = payload.limit;
14920
15033
  if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_RETENTION_BATCH_SIZE;
@@ -15323,20 +15436,45 @@ function createProjectMaintenanceHandler(projectId, projectDir2, projectRoot, op
15323
15436
  if (isEmbeddingExplicitlyDisabled2()) return { action: "complete" };
15324
15437
  const { backfillVectorEmbeddings: backfillVectorEmbeddings2, getVectorStatus: getVectorStatus2 } = await Promise.resolve().then(() => (init_observations(), observations_exports));
15325
15438
  const before = getVectorStatus2(projectId);
15326
- if (before.missing === 0) return { action: "complete" };
15439
+ if (before.missing === 0) {
15440
+ resolveSupersededVectorFailures(projectDir2, projectId);
15441
+ return { action: "complete" };
15442
+ }
15327
15443
  const result = await backfillVectorEmbeddings2({
15328
15444
  projectId,
15329
15445
  limit: vectorBatchSize(job.payload)
15330
15446
  });
15331
15447
  const after = getVectorStatus2(projectId);
15332
- if (after.missing === 0) return { action: "complete" };
15448
+ if (after.missing === 0) {
15449
+ resolveSupersededVectorFailures(projectDir2, projectId);
15450
+ return { action: "complete" };
15451
+ }
15333
15452
  if (result.failed > 0 && result.succeeded === 0) {
15334
- throw new Error(`vector backfill made no progress (${result.failed}/${result.attempted} failed)`);
15453
+ const streak = vectorBackfillFailureStreak(job.payload) + 1;
15454
+ return {
15455
+ action: "reschedule",
15456
+ status: "retry",
15457
+ delayMs: vectorBackfillRetryDelayMs(streak),
15458
+ // This is a recoverable provider/index state, not a terminal job error.
15459
+ // Keeping one retry row prevents new MCP processes from creating a storm.
15460
+ resetAttempts: true,
15461
+ payload: {
15462
+ ...job.payload,
15463
+ vectorBackfillFailureStreak: streak
15464
+ },
15465
+ lastError: result.lastError ?? `vector backfill made no progress (${result.failed}/${result.attempted} failed)`
15466
+ };
15335
15467
  }
15468
+ const priorFailureStreak = vectorBackfillFailureStreak(job.payload);
15469
+ const payload = withoutVectorBackfillFailureStreak(job.payload);
15336
15470
  return {
15337
15471
  action: "reschedule",
15338
15472
  delayMs: result.attempted === 0 ? VECTOR_RETRY_DELAY_MS : 0,
15339
- resetAttempts: result.succeeded > 0
15473
+ resetAttempts: result.succeeded > 0,
15474
+ ...result.succeeded > 0 ? {
15475
+ ...priorFailureStreak > 0 ? { payload } : {},
15476
+ ...result.failed > 0 && result.lastError ? { lastError: result.lastError } : priorFailureStreak > 0 ? { clearLastError: true } : {}
15477
+ } : {}
15340
15478
  };
15341
15479
  };
15342
15480
  }
@@ -15349,11 +15487,12 @@ function createProjectMaintenanceDispatcher(projectId, projectDir2, projectRoot,
15349
15487
  return isolatedRunner({ job, projectRoot, dataDir: projectDir2 });
15350
15488
  };
15351
15489
  }
15352
- var DEFAULT_VECTOR_BATCH_SIZE, DEFAULT_RETENTION_BATCH_SIZE, DEFAULT_CONSOLIDATION_BATCH_SIZE, DEFAULT_CODEGRAPH_MAX_FILES, DEFAULT_CLAIM_DERIVATION_BATCH_SIZE, DEFAULT_OBSERVATION_QUALIFICATION_BATCH_SIZE, VECTOR_RETRY_DELAY_MS, DEDUP_PER_PAIR_TIMEOUT_MS;
15490
+ var DEFAULT_VECTOR_BATCH_SIZE, DEFAULT_RETENTION_BATCH_SIZE, DEFAULT_CONSOLIDATION_BATCH_SIZE, DEFAULT_CODEGRAPH_MAX_FILES, DEFAULT_CLAIM_DERIVATION_BATCH_SIZE, DEFAULT_OBSERVATION_QUALIFICATION_BATCH_SIZE, VECTOR_RETRY_DELAY_MS, VECTOR_FAILURE_BASE_RETRY_DELAY_MS, VECTOR_FAILURE_MAX_RETRY_DELAY_MS, DEDUP_PER_PAIR_TIMEOUT_MS;
15353
15491
  var init_project_maintenance = __esm({
15354
15492
  "src/runtime/project-maintenance.ts"() {
15355
15493
  "use strict";
15356
15494
  init_esm_shims();
15495
+ init_maintenance_jobs();
15357
15496
  init_isolated_maintenance();
15358
15497
  init_lifecycle();
15359
15498
  init_timeout();
@@ -15364,6 +15503,8 @@ var init_project_maintenance = __esm({
15364
15503
  DEFAULT_CLAIM_DERIVATION_BATCH_SIZE = 100;
15365
15504
  DEFAULT_OBSERVATION_QUALIFICATION_BATCH_SIZE = 100;
15366
15505
  VECTOR_RETRY_DELAY_MS = 5e3;
15506
+ VECTOR_FAILURE_BASE_RETRY_DELAY_MS = 6e4;
15507
+ VECTOR_FAILURE_MAX_RETRY_DELAY_MS = 15 * 6e4;
15367
15508
  DEDUP_PER_PAIR_TIMEOUT_MS = 5e3;
15368
15509
  }
15369
15510
  });
@@ -20698,6 +20839,17 @@ async function handleApi(req, res, dataDir, projectId, projectName, baseDir, pro
20698
20839
  sourceCounts,
20699
20840
  recentObservations: sorted,
20700
20841
  embedding: embeddingStatus,
20842
+ // A standalone dashboard has no access to an active MCP
20843
+ // process's in-memory Orama index. Keep this explicit so the
20844
+ // UI never presents an empty local singleton as a healthy
20845
+ // all-vectors-indexed result.
20846
+ vectorStatus: {
20847
+ available: false,
20848
+ total: 0,
20849
+ missing: 0,
20850
+ missingIds: [],
20851
+ backfillRunning: false
20852
+ },
20701
20853
  storage: storageInfo,
20702
20854
  maintenance,
20703
20855
  ...lifecycle ? { lifecycle } : {},
@@ -27858,7 +28010,7 @@ The path should point to a directory containing a .git folder.`
27858
28010
  };
27859
28011
  const server = existingServer ?? new McpServer({
27860
28012
  name: "memorix",
27861
- version: true ? "1.2.7" : "1.0.1"
28013
+ version: true ? "1.2.8" : "1.0.1"
27862
28014
  });
27863
28015
  const originalRegisterTool = server.registerTool.bind(server);
27864
28016
  server.registerTool = ((name, ...args) => {