memorix 1.2.7 → 1.2.9
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.
- package/CHANGELOG.md +15 -0
- package/dist/cli/index.js +19531 -19017
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/static/app.js +7 -2
- package/dist/index.js +7870 -7497
- package/dist/index.js.map +1 -1
- package/dist/maintenance-jobs-o1rYSFcM.d.ts +36 -0
- package/dist/maintenance-runner.d.ts +1 -28
- package/dist/maintenance-runner.js +5937 -5596
- package/dist/maintenance-runner.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +15 -0
- package/dist/sdk.js +7866 -7493
- package/dist/sdk.js.map +1 -1
- package/dist/vector-backfill-runner.d.ts +31 -0
- package/dist/vector-backfill-runner.js +12670 -0
- package/dist/vector-backfill-runner.js.map +1 -0
- package/docs/dev-log/progress.txt +25 -0
- package/package.json +3 -3
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/memorix/package.json +1 -1
- package/src/cli/commands/agent-integrations.ts +102 -5
- package/src/cli/commands/operator-shared.ts +4 -1
- package/src/cli/commands/serve-http.ts +58 -50
- package/src/cli/commands/setup.ts +44 -0
- package/src/dashboard/server.ts +54 -63
- package/src/memory/observations.ts +175 -99
- package/src/memory/retention.ts +106 -28
- package/src/memory/session.ts +71 -12
- package/src/runtime/maintenance-jobs.ts +84 -4
- package/src/runtime/project-maintenance.ts +63 -6
- package/src/runtime/vector-backfill-runner.ts +144 -0
- package/src/search/intent-detector.ts +39 -1
- package/src/server.ts +5 -5
package/src/dashboard/server.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { resetDotenv } from '../config/dotenv-loader.js';
|
|
|
23
23
|
import { initProjectRoot } from '../config/yaml-loader.js';
|
|
24
24
|
import { clearProjectRoot } from '../config/yaml-loader.js';
|
|
25
25
|
import { scopeKnowledgeGraphToProject } from '../memory/graph-scope.js';
|
|
26
|
+
import { projectObservationRetention, summarizeRetentionProjections } from '../memory/retention.js';
|
|
26
27
|
import { canManageObservation, filterReadableObservations } from '../memory/visibility.js';
|
|
27
28
|
import type { Observation } from '../types.js';
|
|
28
29
|
|
|
@@ -249,7 +250,7 @@ async function handleApi(
|
|
|
249
250
|
const observations = filterDashboardObservations(
|
|
250
251
|
await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' }),
|
|
251
252
|
effectiveProjectId,
|
|
252
|
-
)
|
|
253
|
+
);
|
|
253
254
|
const nextId = await getObservationStore().loadIdCounter();
|
|
254
255
|
|
|
255
256
|
// Project-scoped graph counts (must match /api/graph and /api/export)
|
|
@@ -290,21 +291,18 @@ async function handleApi(
|
|
|
290
291
|
filesModified: (o as any).filesModified,
|
|
291
292
|
}));
|
|
292
293
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
else if (score >= 1) retentionSummary.stale++;
|
|
306
|
-
else retentionSummary.archive++;
|
|
307
|
-
}
|
|
294
|
+
const retentionReferenceTime = new Date(now);
|
|
295
|
+
const retention = summarizeRetentionProjections(
|
|
296
|
+
observations
|
|
297
|
+
.filter((observation) => observation.type !== 'probe')
|
|
298
|
+
.map((observation) => projectObservationRetention(observation, { referenceTime: retentionReferenceTime })),
|
|
299
|
+
);
|
|
300
|
+
const retentionSummary = {
|
|
301
|
+
active: retention.active,
|
|
302
|
+
stale: retention.stale,
|
|
303
|
+
archive: retention.archiveCandidates,
|
|
304
|
+
immune: retention.immune,
|
|
305
|
+
};
|
|
308
306
|
|
|
309
307
|
// Recent observations (last 10, exclude probe)
|
|
310
308
|
const sorted = [...observations].filter(o => o.type !== 'probe')
|
|
@@ -351,6 +349,17 @@ async function handleApi(
|
|
|
351
349
|
sourceCounts,
|
|
352
350
|
recentObservations: sorted,
|
|
353
351
|
embedding: embeddingStatus,
|
|
352
|
+
// A standalone dashboard has no access to an active MCP
|
|
353
|
+
// process's in-memory Orama index. Keep this explicit so the
|
|
354
|
+
// UI never presents an empty local singleton as a healthy
|
|
355
|
+
// all-vectors-indexed result.
|
|
356
|
+
vectorStatus: {
|
|
357
|
+
available: false,
|
|
358
|
+
total: 0,
|
|
359
|
+
missing: 0,
|
|
360
|
+
missingIds: [],
|
|
361
|
+
backfillRunning: false,
|
|
362
|
+
},
|
|
354
363
|
storage: storageInfo,
|
|
355
364
|
maintenance,
|
|
356
365
|
...(lifecycle ? { lifecycle } : {}),
|
|
@@ -388,56 +397,38 @@ async function handleApi(
|
|
|
388
397
|
const observations = filterDashboardObservations(
|
|
389
398
|
await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' }),
|
|
390
399
|
effectiveProjectId,
|
|
391
|
-
)
|
|
392
|
-
id?: number;
|
|
393
|
-
title?: string;
|
|
394
|
-
type?: string;
|
|
395
|
-
importance?: number;
|
|
396
|
-
accessCount?: number;
|
|
397
|
-
lastAccessedAt?: string;
|
|
398
|
-
createdAt?: string;
|
|
399
|
-
entityName?: string;
|
|
400
|
-
}>;
|
|
401
|
-
|
|
402
|
-
const now = Date.now();
|
|
403
|
-
// Exclude probe from retention display -- not durable knowledge
|
|
404
|
-
const scored = observations.filter(obs => obs.type !== 'probe').map((obs) => {
|
|
405
|
-
const age = now - new Date(obs.createdAt || now).getTime();
|
|
406
|
-
const ageHours = age / (1000 * 60 * 60);
|
|
407
|
-
const importance = obs.importance ?? 5;
|
|
408
|
-
const accessCount = obs.accessCount ?? 0;
|
|
409
|
-
|
|
410
|
-
// Exponential decay: score = importance * e^(-λt) + access_bonus
|
|
411
|
-
const lambda = 0.01;
|
|
412
|
-
const decayScore = importance * Math.exp(-lambda * ageHours);
|
|
413
|
-
const accessBonus = Math.min(accessCount * 0.5, 3);
|
|
414
|
-
const score = Math.min(decayScore + accessBonus, 10);
|
|
415
|
-
|
|
416
|
-
// Immune if importance >= 8 or type is 'gotcha' or 'decision'
|
|
417
|
-
const isImmune = importance >= 8 || obs.type === 'gotcha' || obs.type === 'decision';
|
|
418
|
-
|
|
419
|
-
return {
|
|
420
|
-
id: obs.id,
|
|
421
|
-
title: obs.title,
|
|
422
|
-
type: obs.type,
|
|
423
|
-
entityName: obs.entityName,
|
|
424
|
-
score: Math.round(score * 100) / 100,
|
|
425
|
-
isImmune,
|
|
426
|
-
ageHours: Math.round(ageHours * 10) / 10,
|
|
427
|
-
accessCount,
|
|
428
|
-
};
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
// Sort by score descending
|
|
432
|
-
scored.sort((a, b) => b.score - a.score);
|
|
400
|
+
);
|
|
433
401
|
|
|
434
|
-
const
|
|
435
|
-
const
|
|
436
|
-
|
|
437
|
-
|
|
402
|
+
const referenceTime = new Date();
|
|
403
|
+
const rows = observations
|
|
404
|
+
.filter((observation) => observation.type !== 'probe')
|
|
405
|
+
.map((observation) => ({
|
|
406
|
+
observation,
|
|
407
|
+
retention: projectObservationRetention(observation, { referenceTime }),
|
|
408
|
+
}))
|
|
409
|
+
.sort((a, b) => b.retention.displayScore - a.retention.displayScore);
|
|
410
|
+
const summary = summarizeRetentionProjections(rows.map((row) => row.retention));
|
|
411
|
+
const scored = rows.map(({ observation, retention }) => ({
|
|
412
|
+
id: observation.id,
|
|
413
|
+
title: observation.title,
|
|
414
|
+
type: observation.type,
|
|
415
|
+
entityName: observation.entityName,
|
|
416
|
+
score: retention.displayScore,
|
|
417
|
+
isImmune: retention.immune,
|
|
418
|
+
zone: retention.zone,
|
|
419
|
+
ageHours: retention.ageHours,
|
|
420
|
+
accessCount: retention.accessCount,
|
|
421
|
+
effectiveRetentionDays: retention.effectiveRetentionDays,
|
|
422
|
+
immunityReason: retention.immunityReason,
|
|
423
|
+
}));
|
|
438
424
|
|
|
439
425
|
sendJson(res, {
|
|
440
|
-
summary: {
|
|
426
|
+
summary: {
|
|
427
|
+
active: summary.active,
|
|
428
|
+
stale: summary.stale,
|
|
429
|
+
archive: summary.archiveCandidates,
|
|
430
|
+
immune: summary.immune,
|
|
431
|
+
},
|
|
441
432
|
items: scored,
|
|
442
433
|
});
|
|
443
434
|
break;
|
|
@@ -49,6 +49,17 @@ let observations: Observation[] = [];
|
|
|
49
49
|
let nextId = 1;
|
|
50
50
|
let projectDir: string | null = null;
|
|
51
51
|
let searchIndexPrepared = false;
|
|
52
|
+
export type ObservationEmbeddingWriteMode = 'background' | 'deferred';
|
|
53
|
+
|
|
54
|
+
export interface ObservationRuntimeOptions {
|
|
55
|
+
/** Defer remote/local embedding to a detached worker for short-lived CLI writes. */
|
|
56
|
+
embeddingWriteMode?: ObservationEmbeddingWriteMode;
|
|
57
|
+
/** Git root used by the detached vector worker. */
|
|
58
|
+
projectRoot?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let embeddingWriteMode: ObservationEmbeddingWriteMode = 'background';
|
|
62
|
+
let embeddingWorkerProjectRoot: string | undefined;
|
|
52
63
|
|
|
53
64
|
// ── Vector-missing tracking ──────────────────────────────────────
|
|
54
65
|
// Tracks observation IDs whose async embedding write failed or was skipped.
|
|
@@ -102,7 +113,14 @@ function normalizeEmbeddingFailure(error: unknown): { key: string; message: stri
|
|
|
102
113
|
};
|
|
103
114
|
}
|
|
104
115
|
|
|
105
|
-
function
|
|
116
|
+
function vectorBackfillError(error: unknown): string {
|
|
117
|
+
return sanitizeCredentials(normalizeEmbeddingFailure(error).message).slice(0, 1_000);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function queueVectorBackfill(
|
|
121
|
+
projectId: string,
|
|
122
|
+
options: { detachedWorker?: boolean; projectRoot?: string } = {},
|
|
123
|
+
): void {
|
|
106
124
|
const dataDir = projectDir;
|
|
107
125
|
if (!dataDir) return;
|
|
108
126
|
void import('../runtime/maintenance-jobs.js')
|
|
@@ -113,6 +131,19 @@ function queueVectorBackfill(projectId: string): void {
|
|
|
113
131
|
dedupeKey: 'vector-backfill',
|
|
114
132
|
payload: { limit: 12 },
|
|
115
133
|
});
|
|
134
|
+
if (options.detachedWorker && options.projectRoot) {
|
|
135
|
+
void import('../runtime/vector-backfill-runner.js')
|
|
136
|
+
.then(({ launchDetachedVectorBackfill }) => {
|
|
137
|
+
launchDetachedVectorBackfill({
|
|
138
|
+
projectId,
|
|
139
|
+
projectRoot: options.projectRoot!,
|
|
140
|
+
dataDir,
|
|
141
|
+
});
|
|
142
|
+
})
|
|
143
|
+
.catch(() => {
|
|
144
|
+
// The durable queue remains available to a later MCP session.
|
|
145
|
+
});
|
|
146
|
+
}
|
|
116
147
|
})
|
|
117
148
|
.catch(() => {
|
|
118
149
|
// Memory writes remain durable even when the optional maintenance queue is unavailable.
|
|
@@ -195,7 +226,12 @@ async function upgradeVectorSchemaAfterFirstEmbedding(embedding: number[]): Prom
|
|
|
195
226
|
* Initialize the observations manager with a project directory.
|
|
196
227
|
* Auto-initializes the ObservationStore if not already set.
|
|
197
228
|
*/
|
|
198
|
-
export async function initObservations(
|
|
229
|
+
export async function initObservations(
|
|
230
|
+
dir: string,
|
|
231
|
+
options: ObservationRuntimeOptions = {},
|
|
232
|
+
): Promise<void> {
|
|
233
|
+
embeddingWriteMode = options.embeddingWriteMode ?? 'background';
|
|
234
|
+
embeddingWorkerProjectRoot = options.projectRoot;
|
|
199
235
|
if (projectDir === dir) return;
|
|
200
236
|
await initObservationStore(dir);
|
|
201
237
|
const store = getObservationStore();
|
|
@@ -206,6 +242,70 @@ export async function initObservations(dir: string): Promise<void> {
|
|
|
206
242
|
vectorSchemaUpgradePromise = null;
|
|
207
243
|
}
|
|
208
244
|
|
|
245
|
+
function scheduleObservationEmbedding(input: {
|
|
246
|
+
observationId: number;
|
|
247
|
+
projectId: string;
|
|
248
|
+
searchableText: string;
|
|
249
|
+
doc: MemorixDocument;
|
|
250
|
+
}): void {
|
|
251
|
+
const { observationId, projectId, searchableText, doc } = input;
|
|
252
|
+
vectorMissingIds.add(observationId);
|
|
253
|
+
|
|
254
|
+
// A network fetch keeps Node's event loop alive even when its Promise is not
|
|
255
|
+
// awaited. One-shot CLI commands therefore persist first and let a detached
|
|
256
|
+
// worker consume the same durable queue.
|
|
257
|
+
if (embeddingWriteMode === 'deferred') {
|
|
258
|
+
if (isEmbeddingExplicitlyDisabled()) {
|
|
259
|
+
vectorMissingIds.delete(observationId);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
queueVectorBackfill(projectId, {
|
|
263
|
+
detachedWorker: true,
|
|
264
|
+
projectRoot: embeddingWorkerProjectRoot,
|
|
265
|
+
});
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
void generateEmbedding(searchableText).then(async (embedding) => {
|
|
270
|
+
if (embedding) {
|
|
271
|
+
if (!isVectorCompatibleWithCurrentIndex(embedding)) {
|
|
272
|
+
await upgradeVectorSchemaAfterFirstEmbedding(embedding);
|
|
273
|
+
}
|
|
274
|
+
if (!isVectorCompatibleWithCurrentIndex(embedding)) {
|
|
275
|
+
const vectorDimensions = getVectorDimensions();
|
|
276
|
+
console.error(
|
|
277
|
+
`[memorix] Embedding dimension mismatch for obs-${observationId}: provider returned ${embedding.length}d, index expects ${vectorDimensions ?? 'unknown'}d (kept in backfill queue)`,
|
|
278
|
+
);
|
|
279
|
+
queueVectorBackfill(projectId);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
await removeObservation(makeOramaObservationId(projectId, observationId));
|
|
284
|
+
await insertObservation(Object.assign({}, doc, { embedding }));
|
|
285
|
+
vectorMissingIds.delete(observationId);
|
|
286
|
+
} catch {
|
|
287
|
+
console.error(`[memorix] Embedding index update failed for obs-${observationId} (kept in backfill queue)`);
|
|
288
|
+
queueVectorBackfill(projectId);
|
|
289
|
+
}
|
|
290
|
+
} else if (isEmbeddingExplicitlyDisabled()) {
|
|
291
|
+
vectorMissingIds.delete(observationId);
|
|
292
|
+
} else {
|
|
293
|
+
queueVectorBackfill(projectId);
|
|
294
|
+
logEmbeddingFailureOnce(
|
|
295
|
+
'provider-unavailable',
|
|
296
|
+
`[memorix] Embedding provider unavailable (using BM25 until embedding recovers; queued obs-${observationId} for retry)`,
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
}).catch((err) => {
|
|
300
|
+
queueVectorBackfill(projectId);
|
|
301
|
+
const failure = normalizeEmbeddingFailure(err);
|
|
302
|
+
logEmbeddingFailureOnce(
|
|
303
|
+
failure.key,
|
|
304
|
+
`[memorix] Async embedding failed (using BM25 until embedding recovers; queued obs-${observationId} for retry): ${failure.message}`,
|
|
305
|
+
);
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
209
309
|
/**
|
|
210
310
|
* Check cross-process freshness and reload if another process has written.
|
|
211
311
|
*
|
|
@@ -503,49 +603,11 @@ export async function storeObservation(input: {
|
|
|
503
603
|
queueClaimDerivation(observation);
|
|
504
604
|
}
|
|
505
605
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
generateEmbedding(searchableText).then(async (embedding) => {
|
|
512
|
-
if (embedding) {
|
|
513
|
-
if (!isVectorCompatibleWithCurrentIndex(embedding)) {
|
|
514
|
-
await upgradeVectorSchemaAfterFirstEmbedding(embedding);
|
|
515
|
-
}
|
|
516
|
-
if (!isVectorCompatibleWithCurrentIndex(embedding)) {
|
|
517
|
-
const vectorDimensions = getVectorDimensions();
|
|
518
|
-
console.error(
|
|
519
|
-
`[memorix] Embedding dimension mismatch for obs-${obsId}: provider returned ${embedding.length}d, index expects ${vectorDimensions ?? 'unknown'}d (kept in backfill queue)`,
|
|
520
|
-
);
|
|
521
|
-
queueVectorBackfill(input.projectId);
|
|
522
|
-
return;
|
|
523
|
-
}
|
|
524
|
-
try {
|
|
525
|
-
const { removeObservation: removeObs } = await import('../store/orama-store.js');
|
|
526
|
-
await removeObs(makeOramaObservationId(input.projectId, obsId));
|
|
527
|
-
await insertObservation(Object.assign({}, doc, { embedding }));
|
|
528
|
-
vectorMissingIds.delete(obsId);
|
|
529
|
-
} catch {
|
|
530
|
-
console.error(`[memorix] Embedding index update failed for obs-${obsId} (kept in backfill queue)`);
|
|
531
|
-
queueVectorBackfill(input.projectId);
|
|
532
|
-
}
|
|
533
|
-
} else if (isEmbeddingExplicitlyDisabled()) {
|
|
534
|
-
vectorMissingIds.delete(obsId);
|
|
535
|
-
} else {
|
|
536
|
-
queueVectorBackfill(input.projectId);
|
|
537
|
-
logEmbeddingFailureOnce(
|
|
538
|
-
'provider-unavailable',
|
|
539
|
-
`[memorix] Embedding provider unavailable (using BM25 until embedding recovers; queued obs-${obsId} for retry)`,
|
|
540
|
-
);
|
|
541
|
-
}
|
|
542
|
-
}).catch((err) => {
|
|
543
|
-
queueVectorBackfill(input.projectId);
|
|
544
|
-
const failure = normalizeEmbeddingFailure(err);
|
|
545
|
-
logEmbeddingFailureOnce(
|
|
546
|
-
failure.key,
|
|
547
|
-
`[memorix] Async embedding failed (using BM25 until embedding recovers; queued obs-${obsId} for retry): ${failure.message}`,
|
|
548
|
-
);
|
|
606
|
+
scheduleObservationEmbedding({
|
|
607
|
+
observationId: observation.id,
|
|
608
|
+
projectId: input.projectId,
|
|
609
|
+
searchableText: [input.title, input.narrative, ...(input.facts ?? [])].join(' '),
|
|
610
|
+
doc,
|
|
549
611
|
});
|
|
550
612
|
|
|
551
613
|
return { observation, upserted: false };
|
|
@@ -674,40 +736,11 @@ async function upsertObservation(
|
|
|
674
736
|
queueClaimDerivation(existing);
|
|
675
737
|
}
|
|
676
738
|
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
if (embedding) {
|
|
683
|
-
if (!isVectorCompatibleWithCurrentIndex(embedding)) {
|
|
684
|
-
await upgradeVectorSchemaAfterFirstEmbedding(embedding);
|
|
685
|
-
}
|
|
686
|
-
if (!isVectorCompatibleWithCurrentIndex(embedding)) {
|
|
687
|
-
queueVectorBackfill(existing.projectId);
|
|
688
|
-
return;
|
|
689
|
-
}
|
|
690
|
-
try {
|
|
691
|
-
const { removeObservation: removeObs } = await import('../store/orama-store.js');
|
|
692
|
-
await removeObs(makeOramaObservationId(existing.projectId, obsId));
|
|
693
|
-
await insertObservation(Object.assign({}, doc, { embedding }));
|
|
694
|
-
vectorMissingIds.delete(obsId);
|
|
695
|
-
} catch {
|
|
696
|
-
// Embedding index update failed — observation still persisted without vector
|
|
697
|
-
queueVectorBackfill(existing.projectId);
|
|
698
|
-
}
|
|
699
|
-
} else if (isEmbeddingExplicitlyDisabled()) {
|
|
700
|
-
vectorMissingIds.delete(obsId);
|
|
701
|
-
} else {
|
|
702
|
-
queueVectorBackfill(existing.projectId);
|
|
703
|
-
}
|
|
704
|
-
}).catch((err) => {
|
|
705
|
-
queueVectorBackfill(existing.projectId);
|
|
706
|
-
const failure = normalizeEmbeddingFailure(err);
|
|
707
|
-
logEmbeddingFailureOnce(
|
|
708
|
-
failure.key,
|
|
709
|
-
`[memorix] Async embedding failed (using BM25 until embedding recovers; queued obs-${obsId} for retry): ${failure.message}`,
|
|
710
|
-
);
|
|
739
|
+
scheduleObservationEmbedding({
|
|
740
|
+
observationId: existing.id,
|
|
741
|
+
projectId: existing.projectId,
|
|
742
|
+
searchableText: [input.title, input.narrative, ...(input.facts ?? [])].join(' '),
|
|
743
|
+
doc,
|
|
711
744
|
});
|
|
712
745
|
|
|
713
746
|
return existing;
|
|
@@ -1202,6 +1235,7 @@ export async function backfillVectorEmbeddings(options: {
|
|
|
1202
1235
|
attempted: number;
|
|
1203
1236
|
succeeded: number;
|
|
1204
1237
|
failed: number;
|
|
1238
|
+
lastError?: string;
|
|
1205
1239
|
}> {
|
|
1206
1240
|
if (vectorBackfillRunning) {
|
|
1207
1241
|
return { attempted: 0, succeeded: 0, failed: 0 };
|
|
@@ -1218,19 +1252,63 @@ export async function backfillVectorEmbeddings(options: {
|
|
|
1218
1252
|
let succeeded = 0;
|
|
1219
1253
|
let failed = 0;
|
|
1220
1254
|
let lastFailure: string | undefined;
|
|
1255
|
+
const recordFailure = (message: string): void => {
|
|
1256
|
+
// A single batch may have several symptoms after one root cause. Preserve
|
|
1257
|
+
// the first one so an informative provider/index error is not overwritten
|
|
1258
|
+
// by later missing-result fallout.
|
|
1259
|
+
if (!lastFailure) lastFailure = message;
|
|
1260
|
+
};
|
|
1261
|
+
|
|
1262
|
+
const candidates: Array<{ id: number; observation: Observation; text: string }> = [];
|
|
1263
|
+
for (const id of ids) {
|
|
1264
|
+
const observation = observationById.get(id);
|
|
1265
|
+
if (!observation) {
|
|
1266
|
+
vectorMissingIds.delete(id);
|
|
1267
|
+
continue;
|
|
1268
|
+
}
|
|
1269
|
+
candidates.push({
|
|
1270
|
+
id,
|
|
1271
|
+
observation,
|
|
1272
|
+
text: [observation.title, observation.narrative, ...observation.facts].join(' '),
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1221
1275
|
|
|
1222
1276
|
try {
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1277
|
+
if (candidates.length === 0) {
|
|
1278
|
+
return { attempted: ids.length, succeeded, failed };
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
let embeddings: number[][] | null = null;
|
|
1282
|
+
try {
|
|
1283
|
+
const provider = await getEmbeddingProvider();
|
|
1284
|
+
if (!provider) {
|
|
1285
|
+
if (isEmbeddingExplicitlyDisabled()) {
|
|
1286
|
+
for (const candidate of candidates) vectorMissingIds.delete(candidate.id);
|
|
1287
|
+
} else {
|
|
1288
|
+
recordFailure('embedding provider unavailable');
|
|
1289
|
+
failed += candidates.length;
|
|
1290
|
+
}
|
|
1291
|
+
} else {
|
|
1292
|
+
// API providers batch and cache this efficiently; local providers can use
|
|
1293
|
+
// their native batch path. Index writes stay sequential because Orama is
|
|
1294
|
+
// process-local mutable state.
|
|
1295
|
+
embeddings = await provider.embedBatch(candidates.map((candidate) => candidate.text));
|
|
1228
1296
|
}
|
|
1297
|
+
} catch (error) {
|
|
1298
|
+
recordFailure(vectorBackfillError(error));
|
|
1299
|
+
failed += candidates.length;
|
|
1300
|
+
}
|
|
1229
1301
|
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
const
|
|
1233
|
-
|
|
1302
|
+
if (embeddings) {
|
|
1303
|
+
for (let index = 0; index < candidates.length; index++) {
|
|
1304
|
+
const { id, observation: obs } = candidates[index];
|
|
1305
|
+
const embedding = embeddings[index];
|
|
1306
|
+
if (!embedding || embedding.length === 0) {
|
|
1307
|
+
recordFailure('embedding provider returned no vector');
|
|
1308
|
+
failed++;
|
|
1309
|
+
continue;
|
|
1310
|
+
}
|
|
1311
|
+
try {
|
|
1234
1312
|
if (!isVectorCompatibleWithCurrentIndex(embedding)) {
|
|
1235
1313
|
await upgradeVectorSchemaAfterFirstEmbedding(embedding);
|
|
1236
1314
|
}
|
|
@@ -1239,7 +1317,7 @@ export async function backfillVectorEmbeddings(options: {
|
|
|
1239
1317
|
console.error(
|
|
1240
1318
|
`[memorix] Backfill embedding mismatch for obs-${id}: provider returned ${embedding.length}d, index expects ${vectorDimensions ?? 'unknown'}d (kept in queue)`,
|
|
1241
1319
|
);
|
|
1242
|
-
|
|
1320
|
+
recordFailure(`dimension mismatch: provider returned ${embedding.length}d, index expects ${vectorDimensions ?? 'unknown'}d`);
|
|
1243
1321
|
failed++;
|
|
1244
1322
|
continue;
|
|
1245
1323
|
}
|
|
@@ -1277,17 +1355,10 @@ export async function backfillVectorEmbeddings(options: {
|
|
|
1277
1355
|
await insertObservation(doc);
|
|
1278
1356
|
vectorMissingIds.delete(id);
|
|
1279
1357
|
succeeded++;
|
|
1280
|
-
}
|
|
1281
|
-
|
|
1282
|
-
vectorMissingIds.delete(id);
|
|
1283
|
-
} else {
|
|
1284
|
-
// Provider temporarily unavailable — keep in queue for next backfill cycle
|
|
1285
|
-
lastFailure = 'embedding provider unavailable';
|
|
1358
|
+
} catch (error) {
|
|
1359
|
+
recordFailure(vectorBackfillError(error));
|
|
1286
1360
|
failed++;
|
|
1287
1361
|
}
|
|
1288
|
-
} catch (err) {
|
|
1289
|
-
lastFailure = err instanceof Error ? err.message : String(err);
|
|
1290
|
-
failed++;
|
|
1291
1362
|
}
|
|
1292
1363
|
}
|
|
1293
1364
|
} finally {
|
|
@@ -1301,5 +1372,10 @@ export async function backfillVectorEmbeddings(options: {
|
|
|
1301
1372
|
};
|
|
1302
1373
|
}
|
|
1303
1374
|
|
|
1304
|
-
return {
|
|
1375
|
+
return {
|
|
1376
|
+
attempted: ids.length,
|
|
1377
|
+
succeeded,
|
|
1378
|
+
failed,
|
|
1379
|
+
...(lastFailure ? { lastError: lastFailure } : {}),
|
|
1380
|
+
};
|
|
1305
1381
|
}
|