memorix 1.2.8 → 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 +7 -0
- package/dist/cli/index.js +19355 -19126
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/memcode.js +0 -0
- package/dist/index.js +7849 -7628
- package/dist/index.js.map +1 -1
- package/dist/maintenance-jobs-o1rYSFcM.d.ts +36 -0
- package/dist/maintenance-runner.d.ts +1 -34
- package/dist/maintenance-runner.js +5892 -5692
- package/dist/maintenance-runner.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +7 -0
- package/dist/sdk.js +7782 -7561
- 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/package.json +1 -1
- 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/operator-shared.ts +4 -1
- package/src/cli/commands/serve-http.ts +44 -47
- package/src/dashboard/server.ts +43 -63
- package/src/memory/observations.ts +108 -79
- package/src/memory/retention.ts +106 -28
- package/src/memory/session.ts +71 -12
- package/src/runtime/vector-backfill-runner.ts +144 -0
- package/src/search/intent-detector.ts +39 -1
- package/src/server.ts +5 -5
package/package.json
CHANGED
|
@@ -110,7 +110,10 @@ export async function getCliProjectContext(options?: CliContextOptions): Promise
|
|
|
110
110
|
} catch {
|
|
111
111
|
// CLI reads remain available even if optional background maintenance metadata fails.
|
|
112
112
|
}
|
|
113
|
-
await initObservations(dataDir
|
|
113
|
+
await initObservations(dataDir, {
|
|
114
|
+
embeddingWriteMode: 'deferred',
|
|
115
|
+
projectRoot: project.rootPath,
|
|
116
|
+
});
|
|
114
117
|
await initSessionStore(dataDir);
|
|
115
118
|
const teamStore = await initTeamStore(dataDir);
|
|
116
119
|
|
|
@@ -24,6 +24,7 @@ import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
|
24
24
|
import type { ObservationStore } from '../../store/obs-store.js';
|
|
25
25
|
import { resolveToolProfile } from '../../server/tool-profile.js';
|
|
26
26
|
import { scopeKnowledgeGraphToProject } from '../../memory/graph-scope.js';
|
|
27
|
+
import { projectObservationRetention, summarizeRetentionProjections } from '../../memory/retention.js';
|
|
27
28
|
import { canManageObservation, filterReadableObservations } from '../../memory/visibility.js';
|
|
28
29
|
|
|
29
30
|
export const DEFAULT_SESSION_TIMEOUT_MS = 30 * 60 * 1000;
|
|
@@ -760,20 +761,7 @@ export default defineCommand({
|
|
|
760
761
|
await initGraphStore(statsDataDir);
|
|
761
762
|
const graph = { entities: getGraphStore().loadEntities(), relations: getGraphStore().loadRelations() };
|
|
762
763
|
|
|
763
|
-
const observations = await loadDashboardProjectObservations(statsDataDir, statsProjectId, 'active')
|
|
764
|
-
type?: string;
|
|
765
|
-
id?: number;
|
|
766
|
-
title?: string;
|
|
767
|
-
entityName?: string;
|
|
768
|
-
createdAt?: string;
|
|
769
|
-
source?: string;
|
|
770
|
-
commitHash?: string;
|
|
771
|
-
filesModified?: string[];
|
|
772
|
-
relatedEntities?: string[];
|
|
773
|
-
status?: string;
|
|
774
|
-
importance?: number;
|
|
775
|
-
accessCount?: number;
|
|
776
|
-
}>;
|
|
764
|
+
const observations = await loadDashboardProjectObservations(statsDataDir, statsProjectId, 'active');
|
|
777
765
|
const statsStore = await getDashboardObservationStore(statsDataDir);
|
|
778
766
|
const nextId = await statsStore.loadIdCounter();
|
|
779
767
|
const typeCounts: Record<string, number> = {};
|
|
@@ -811,20 +799,17 @@ export default defineCommand({
|
|
|
811
799
|
filesModified: o.filesModified,
|
|
812
800
|
}));
|
|
813
801
|
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
else if (score >= 1) retentionSummary.stale++;
|
|
826
|
-
else retentionSummary.archive++;
|
|
827
|
-
}
|
|
802
|
+
const retention = summarizeRetentionProjections(
|
|
803
|
+
observations
|
|
804
|
+
.filter((observation) => observation.type !== 'probe')
|
|
805
|
+
.map((observation) => projectObservationRetention(observation, { referenceTime: new Date(now) })),
|
|
806
|
+
);
|
|
807
|
+
const retentionSummary = {
|
|
808
|
+
active: retention.active,
|
|
809
|
+
stale: retention.stale,
|
|
810
|
+
archive: retention.archiveCandidates,
|
|
811
|
+
immune: retention.immune,
|
|
812
|
+
};
|
|
828
813
|
|
|
829
814
|
const sorted = [...observations].filter(o => o.type !== 'probe').sort((a, b) => (b.id || 0) - (a.id || 0)).slice(0, 10);
|
|
830
815
|
|
|
@@ -959,26 +944,38 @@ export default defineCommand({
|
|
|
959
944
|
|
|
960
945
|
if (apiPath === '/retention') {
|
|
961
946
|
const { projectId: retProjectId, dataDir: retDataDir } = await resolveRequestProject(url);
|
|
962
|
-
const observations = await loadDashboardProjectObservations(retDataDir, retProjectId, 'active')
|
|
963
|
-
const
|
|
964
|
-
const
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
947
|
+
const observations = await loadDashboardProjectObservations(retDataDir, retProjectId, 'active');
|
|
948
|
+
const referenceTime = new Date();
|
|
949
|
+
const rows = observations
|
|
950
|
+
.filter((observation) => observation.type !== 'probe')
|
|
951
|
+
.map((observation) => ({
|
|
952
|
+
observation,
|
|
953
|
+
retention: projectObservationRetention(observation, { referenceTime }),
|
|
954
|
+
}))
|
|
955
|
+
.sort((a, b) => b.retention.displayScore - a.retention.displayScore);
|
|
956
|
+
const summary = summarizeRetentionProjections(rows.map((row) => row.retention));
|
|
957
|
+
const items = rows.map(({ observation, retention }) => ({
|
|
958
|
+
id: observation.id,
|
|
959
|
+
title: observation.title,
|
|
960
|
+
type: observation.type,
|
|
961
|
+
entityName: observation.entityName,
|
|
962
|
+
score: retention.displayScore,
|
|
963
|
+
isImmune: retention.immune,
|
|
964
|
+
zone: retention.zone,
|
|
965
|
+
ageHours: retention.ageHours,
|
|
966
|
+
accessCount: retention.accessCount,
|
|
967
|
+
effectiveRetentionDays: retention.effectiveRetentionDays,
|
|
968
|
+
immunityReason: retention.immunityReason,
|
|
969
|
+
}));
|
|
970
|
+
sendJson({
|
|
971
|
+
summary: {
|
|
972
|
+
active: summary.active,
|
|
973
|
+
stale: summary.stale,
|
|
974
|
+
archive: summary.archiveCandidates,
|
|
975
|
+
immune: summary.immune,
|
|
976
|
+
},
|
|
977
|
+
items,
|
|
975
978
|
});
|
|
976
|
-
scored.sort((a, b) => b.score - a.score);
|
|
977
|
-
const activeCount = scored.filter(s => s.score >= 3).length;
|
|
978
|
-
const staleCount = scored.filter(s => s.score < 3 && s.score >= 1).length;
|
|
979
|
-
const archiveCount = scored.filter(s => s.score < 1).length;
|
|
980
|
-
const immuneCount = scored.filter(s => s.isImmune).length;
|
|
981
|
-
sendJson({ summary: { active: activeCount, stale: staleCount, archive: archiveCount, immune: immuneCount }, items: scored });
|
|
982
979
|
return;
|
|
983
980
|
}
|
|
984
981
|
|
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')
|
|
@@ -399,56 +397,38 @@ async function handleApi(
|
|
|
399
397
|
const observations = filterDashboardObservations(
|
|
400
398
|
await getObservationStore().loadByProject(effectiveProjectId, { status: 'active' }),
|
|
401
399
|
effectiveProjectId,
|
|
402
|
-
)
|
|
403
|
-
id?: number;
|
|
404
|
-
title?: string;
|
|
405
|
-
type?: string;
|
|
406
|
-
importance?: number;
|
|
407
|
-
accessCount?: number;
|
|
408
|
-
lastAccessedAt?: string;
|
|
409
|
-
createdAt?: string;
|
|
410
|
-
entityName?: string;
|
|
411
|
-
}>;
|
|
412
|
-
|
|
413
|
-
const now = Date.now();
|
|
414
|
-
// Exclude probe from retention display -- not durable knowledge
|
|
415
|
-
const scored = observations.filter(obs => obs.type !== 'probe').map((obs) => {
|
|
416
|
-
const age = now - new Date(obs.createdAt || now).getTime();
|
|
417
|
-
const ageHours = age / (1000 * 60 * 60);
|
|
418
|
-
const importance = obs.importance ?? 5;
|
|
419
|
-
const accessCount = obs.accessCount ?? 0;
|
|
420
|
-
|
|
421
|
-
// Exponential decay: score = importance * e^(-λt) + access_bonus
|
|
422
|
-
const lambda = 0.01;
|
|
423
|
-
const decayScore = importance * Math.exp(-lambda * ageHours);
|
|
424
|
-
const accessBonus = Math.min(accessCount * 0.5, 3);
|
|
425
|
-
const score = Math.min(decayScore + accessBonus, 10);
|
|
426
|
-
|
|
427
|
-
// Immune if importance >= 8 or type is 'gotcha' or 'decision'
|
|
428
|
-
const isImmune = importance >= 8 || obs.type === 'gotcha' || obs.type === 'decision';
|
|
429
|
-
|
|
430
|
-
return {
|
|
431
|
-
id: obs.id,
|
|
432
|
-
title: obs.title,
|
|
433
|
-
type: obs.type,
|
|
434
|
-
entityName: obs.entityName,
|
|
435
|
-
score: Math.round(score * 100) / 100,
|
|
436
|
-
isImmune,
|
|
437
|
-
ageHours: Math.round(ageHours * 10) / 10,
|
|
438
|
-
accessCount,
|
|
439
|
-
};
|
|
440
|
-
});
|
|
441
|
-
|
|
442
|
-
// Sort by score descending
|
|
443
|
-
scored.sort((a, b) => b.score - a.score);
|
|
400
|
+
);
|
|
444
401
|
|
|
445
|
-
const
|
|
446
|
-
const
|
|
447
|
-
|
|
448
|
-
|
|
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
|
+
}));
|
|
449
424
|
|
|
450
425
|
sendJson(res, {
|
|
451
|
-
summary: {
|
|
426
|
+
summary: {
|
|
427
|
+
active: summary.active,
|
|
428
|
+
stale: summary.stale,
|
|
429
|
+
archive: summary.archiveCandidates,
|
|
430
|
+
immune: summary.immune,
|
|
431
|
+
},
|
|
452
432
|
items: scored,
|
|
453
433
|
});
|
|
454
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.
|
|
@@ -106,7 +117,10 @@ function vectorBackfillError(error: unknown): string {
|
|
|
106
117
|
return sanitizeCredentials(normalizeEmbeddingFailure(error).message).slice(0, 1_000);
|
|
107
118
|
}
|
|
108
119
|
|
|
109
|
-
function queueVectorBackfill(
|
|
120
|
+
function queueVectorBackfill(
|
|
121
|
+
projectId: string,
|
|
122
|
+
options: { detachedWorker?: boolean; projectRoot?: string } = {},
|
|
123
|
+
): void {
|
|
110
124
|
const dataDir = projectDir;
|
|
111
125
|
if (!dataDir) return;
|
|
112
126
|
void import('../runtime/maintenance-jobs.js')
|
|
@@ -117,6 +131,19 @@ function queueVectorBackfill(projectId: string): void {
|
|
|
117
131
|
dedupeKey: 'vector-backfill',
|
|
118
132
|
payload: { limit: 12 },
|
|
119
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
|
+
}
|
|
120
147
|
})
|
|
121
148
|
.catch(() => {
|
|
122
149
|
// Memory writes remain durable even when the optional maintenance queue is unavailable.
|
|
@@ -199,7 +226,12 @@ async function upgradeVectorSchemaAfterFirstEmbedding(embedding: number[]): Prom
|
|
|
199
226
|
* Initialize the observations manager with a project directory.
|
|
200
227
|
* Auto-initializes the ObservationStore if not already set.
|
|
201
228
|
*/
|
|
202
|
-
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;
|
|
203
235
|
if (projectDir === dir) return;
|
|
204
236
|
await initObservationStore(dir);
|
|
205
237
|
const store = getObservationStore();
|
|
@@ -210,6 +242,70 @@ export async function initObservations(dir: string): Promise<void> {
|
|
|
210
242
|
vectorSchemaUpgradePromise = null;
|
|
211
243
|
}
|
|
212
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
|
+
|
|
213
309
|
/**
|
|
214
310
|
* Check cross-process freshness and reload if another process has written.
|
|
215
311
|
*
|
|
@@ -507,49 +603,11 @@ export async function storeObservation(input: {
|
|
|
507
603
|
queueClaimDerivation(observation);
|
|
508
604
|
}
|
|
509
605
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
generateEmbedding(searchableText).then(async (embedding) => {
|
|
516
|
-
if (embedding) {
|
|
517
|
-
if (!isVectorCompatibleWithCurrentIndex(embedding)) {
|
|
518
|
-
await upgradeVectorSchemaAfterFirstEmbedding(embedding);
|
|
519
|
-
}
|
|
520
|
-
if (!isVectorCompatibleWithCurrentIndex(embedding)) {
|
|
521
|
-
const vectorDimensions = getVectorDimensions();
|
|
522
|
-
console.error(
|
|
523
|
-
`[memorix] Embedding dimension mismatch for obs-${obsId}: provider returned ${embedding.length}d, index expects ${vectorDimensions ?? 'unknown'}d (kept in backfill queue)`,
|
|
524
|
-
);
|
|
525
|
-
queueVectorBackfill(input.projectId);
|
|
526
|
-
return;
|
|
527
|
-
}
|
|
528
|
-
try {
|
|
529
|
-
const { removeObservation: removeObs } = await import('../store/orama-store.js');
|
|
530
|
-
await removeObs(makeOramaObservationId(input.projectId, obsId));
|
|
531
|
-
await insertObservation(Object.assign({}, doc, { embedding }));
|
|
532
|
-
vectorMissingIds.delete(obsId);
|
|
533
|
-
} catch {
|
|
534
|
-
console.error(`[memorix] Embedding index update failed for obs-${obsId} (kept in backfill queue)`);
|
|
535
|
-
queueVectorBackfill(input.projectId);
|
|
536
|
-
}
|
|
537
|
-
} else if (isEmbeddingExplicitlyDisabled()) {
|
|
538
|
-
vectorMissingIds.delete(obsId);
|
|
539
|
-
} else {
|
|
540
|
-
queueVectorBackfill(input.projectId);
|
|
541
|
-
logEmbeddingFailureOnce(
|
|
542
|
-
'provider-unavailable',
|
|
543
|
-
`[memorix] Embedding provider unavailable (using BM25 until embedding recovers; queued obs-${obsId} for retry)`,
|
|
544
|
-
);
|
|
545
|
-
}
|
|
546
|
-
}).catch((err) => {
|
|
547
|
-
queueVectorBackfill(input.projectId);
|
|
548
|
-
const failure = normalizeEmbeddingFailure(err);
|
|
549
|
-
logEmbeddingFailureOnce(
|
|
550
|
-
failure.key,
|
|
551
|
-
`[memorix] Async embedding failed (using BM25 until embedding recovers; queued obs-${obsId} for retry): ${failure.message}`,
|
|
552
|
-
);
|
|
606
|
+
scheduleObservationEmbedding({
|
|
607
|
+
observationId: observation.id,
|
|
608
|
+
projectId: input.projectId,
|
|
609
|
+
searchableText: [input.title, input.narrative, ...(input.facts ?? [])].join(' '),
|
|
610
|
+
doc,
|
|
553
611
|
});
|
|
554
612
|
|
|
555
613
|
return { observation, upserted: false };
|
|
@@ -678,40 +736,11 @@ async function upsertObservation(
|
|
|
678
736
|
queueClaimDerivation(existing);
|
|
679
737
|
}
|
|
680
738
|
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
if (embedding) {
|
|
687
|
-
if (!isVectorCompatibleWithCurrentIndex(embedding)) {
|
|
688
|
-
await upgradeVectorSchemaAfterFirstEmbedding(embedding);
|
|
689
|
-
}
|
|
690
|
-
if (!isVectorCompatibleWithCurrentIndex(embedding)) {
|
|
691
|
-
queueVectorBackfill(existing.projectId);
|
|
692
|
-
return;
|
|
693
|
-
}
|
|
694
|
-
try {
|
|
695
|
-
const { removeObservation: removeObs } = await import('../store/orama-store.js');
|
|
696
|
-
await removeObs(makeOramaObservationId(existing.projectId, obsId));
|
|
697
|
-
await insertObservation(Object.assign({}, doc, { embedding }));
|
|
698
|
-
vectorMissingIds.delete(obsId);
|
|
699
|
-
} catch {
|
|
700
|
-
// Embedding index update failed — observation still persisted without vector
|
|
701
|
-
queueVectorBackfill(existing.projectId);
|
|
702
|
-
}
|
|
703
|
-
} else if (isEmbeddingExplicitlyDisabled()) {
|
|
704
|
-
vectorMissingIds.delete(obsId);
|
|
705
|
-
} else {
|
|
706
|
-
queueVectorBackfill(existing.projectId);
|
|
707
|
-
}
|
|
708
|
-
}).catch((err) => {
|
|
709
|
-
queueVectorBackfill(existing.projectId);
|
|
710
|
-
const failure = normalizeEmbeddingFailure(err);
|
|
711
|
-
logEmbeddingFailureOnce(
|
|
712
|
-
failure.key,
|
|
713
|
-
`[memorix] Async embedding failed (using BM25 until embedding recovers; queued obs-${obsId} for retry): ${failure.message}`,
|
|
714
|
-
);
|
|
739
|
+
scheduleObservationEmbedding({
|
|
740
|
+
observationId: existing.id,
|
|
741
|
+
projectId: existing.projectId,
|
|
742
|
+
searchableText: [input.title, input.narrative, ...(input.facts ?? [])].join(' '),
|
|
743
|
+
doc,
|
|
715
744
|
});
|
|
716
745
|
|
|
717
746
|
return existing;
|