memorix 1.1.11 → 1.1.13
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 +25 -0
- package/README.md +3 -1
- package/README.zh-CN.md +3 -1
- package/dist/cli/index.js +679 -78
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +289 -44
- package/dist/index.js.map +1 -1
- package/dist/maintenance-runner.js +252 -36
- package/dist/maintenance-runner.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +25 -0
- package/dist/memcode-runtime/package.json +5 -4
- package/dist/sdk.js +289 -44
- package/dist/sdk.js.map +1 -1
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +2 -2
- package/docs/DEVELOPMENT.md +3 -10
- package/docs/INTEGRATIONS.md +1 -1
- package/docs/SETUP.md +3 -1
- package/docs/dev-log/progress.txt +34 -53
- package/docs/hooks-architecture.md +24 -12
- package/package.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +2 -1
- package/plugins/codex/memorix/hooks/hooks.json +30 -0
- package/src/cli/commands/agent-integrations.ts +384 -2
- package/src/cli/commands/hooks-install.ts +2 -1
- package/src/cli/commands/setup.ts +20 -2
- package/src/cli/tui/chat-service.ts +3 -9
- package/src/cli/tui/data.ts +9 -11
- package/src/embedding/api-provider.ts +105 -6
- package/src/embedding/provider.ts +31 -3
- package/src/hooks/handler.ts +51 -14
- package/src/hooks/installers/index.ts +48 -7
- package/src/hooks/normalizer.ts +15 -0
- package/src/memory/observations.ts +90 -15
- package/src/store/orama-store.ts +169 -17
package/src/store/orama-store.ts
CHANGED
|
@@ -18,8 +18,12 @@ import { detectQueryIntent, applyIntentBoost } from '../search/intent-detector.j
|
|
|
18
18
|
import { maybeExpandSearchQuery } from '../search/query-expansion.js';
|
|
19
19
|
|
|
20
20
|
let db: AnyOrama | null = null;
|
|
21
|
+
let dbInitPromise: Promise<AnyOrama> | null = null;
|
|
22
|
+
let dbGeneration = 0;
|
|
21
23
|
let embeddingEnabled = false;
|
|
22
24
|
let embeddingDimensions: number | null = null;
|
|
25
|
+
let indexEmbeddingProvider: EmbeddingProvider | null = null;
|
|
26
|
+
let deferredCachedVectorHydration: Promise<void> | null = null;
|
|
23
27
|
const docByObservationKey = new Map<string, MemorixDocument>();
|
|
24
28
|
const NON_CJK_HYBRID_SIMILARITY = 0.45;
|
|
25
29
|
const lastSearchModeByProject = new Map<string, string>();
|
|
@@ -143,13 +147,14 @@ function stripVectorSearchParams(params: Record<string, unknown>): Record<string
|
|
|
143
147
|
* Schema conditionally includes vector field based on embedding provider.
|
|
144
148
|
* Graceful degradation: no provider → fulltext only, provider → hybrid.
|
|
145
149
|
*/
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
150
|
+
async function initializeDb(
|
|
151
|
+
options: { allowNetworkProbe?: boolean },
|
|
152
|
+
generation: number,
|
|
153
|
+
): Promise<AnyOrama> {
|
|
154
|
+
// Check if embedding provider is available.
|
|
155
|
+
const provider = await getEmbeddingProvider({ allowNetworkProbe: options.allowNetworkProbe });
|
|
156
|
+
const nextEmbeddingEnabled = provider !== null;
|
|
157
|
+
const nextEmbeddingDimensions = provider?.dimensions ?? null;
|
|
153
158
|
|
|
154
159
|
const baseSchema = {
|
|
155
160
|
id: 'string' as const,
|
|
@@ -175,23 +180,54 @@ export async function getDb(): Promise<AnyOrama> {
|
|
|
175
180
|
};
|
|
176
181
|
|
|
177
182
|
// Dynamic vector dimensions based on provider (384 for local, 1024+ for API)
|
|
178
|
-
const dims =
|
|
179
|
-
const schema =
|
|
183
|
+
const dims = nextEmbeddingDimensions ?? 384;
|
|
184
|
+
const schema = nextEmbeddingEnabled
|
|
180
185
|
? { ...baseSchema, embedding: `vector[${dims}]` as const }
|
|
181
186
|
: baseSchema;
|
|
182
187
|
|
|
183
|
-
|
|
188
|
+
const nextDb = await create({ schema });
|
|
189
|
+
|
|
190
|
+
// resetDb() may have started a new lifecycle while the provider was loading.
|
|
191
|
+
// Discard this obsolete instance rather than letting it overwrite the new one.
|
|
192
|
+
if (generation !== dbGeneration) {
|
|
193
|
+
return getDb(options);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
indexEmbeddingProvider = provider;
|
|
197
|
+
embeddingEnabled = nextEmbeddingEnabled;
|
|
198
|
+
embeddingDimensions = nextEmbeddingDimensions;
|
|
199
|
+
db = nextDb;
|
|
200
|
+
|
|
201
|
+
return nextDb;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function getDb(options: { allowNetworkProbe?: boolean } = {}): Promise<AnyOrama> {
|
|
205
|
+
if (db) return db;
|
|
206
|
+
if (dbInitPromise) return dbInitPromise;
|
|
207
|
+
|
|
208
|
+
const initPromise = initializeDb(options, dbGeneration);
|
|
209
|
+
dbInitPromise = initPromise;
|
|
184
210
|
|
|
185
|
-
|
|
211
|
+
try {
|
|
212
|
+
return await initPromise;
|
|
213
|
+
} finally {
|
|
214
|
+
if (dbInitPromise === initPromise) {
|
|
215
|
+
dbInitPromise = null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
186
218
|
}
|
|
187
219
|
|
|
188
220
|
/**
|
|
189
221
|
* Reset the database instance (useful for testing).
|
|
190
222
|
*/
|
|
191
223
|
export async function resetDb(): Promise<void> {
|
|
224
|
+
dbGeneration++;
|
|
192
225
|
db = null;
|
|
226
|
+
dbInitPromise = null;
|
|
193
227
|
embeddingEnabled = false;
|
|
194
228
|
embeddingDimensions = null;
|
|
229
|
+
indexEmbeddingProvider = null;
|
|
230
|
+
deferredCachedVectorHydration = null;
|
|
195
231
|
lastSearchModeByProject.clear();
|
|
196
232
|
docByObservationKey.clear();
|
|
197
233
|
}
|
|
@@ -240,20 +276,103 @@ export async function batchGenerateEmbeddings(texts: string[]): Promise<(number[
|
|
|
240
276
|
}
|
|
241
277
|
}
|
|
242
278
|
|
|
279
|
+
async function getCachedEmbeddings(texts: string[]): Promise<(number[] | null)[]> {
|
|
280
|
+
if (!embeddingEnabled || texts.length === 0) return texts.map(() => null);
|
|
281
|
+
const provider = indexEmbeddingProvider;
|
|
282
|
+
if (!provider?.getCachedEmbeddings) return texts.map(() => null);
|
|
283
|
+
try {
|
|
284
|
+
return await provider.getCachedEmbeddings(texts);
|
|
285
|
+
} catch {
|
|
286
|
+
// Cached vectors are an optimization. Missing entries use background recovery.
|
|
287
|
+
return texts.map(() => null);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
243
291
|
/**
|
|
244
292
|
* Hydrate the Orama index from persisted observations.
|
|
245
293
|
* Must be called before searching if the index was freshly created (TUI / CLI startup).
|
|
246
294
|
* Skips observations already in the index (idempotent).
|
|
247
295
|
*/
|
|
248
|
-
export
|
|
249
|
-
|
|
296
|
+
export interface HydrateIndexOptions {
|
|
297
|
+
/** Do not issue an API dimension probe while building the startup index. */
|
|
298
|
+
allowNetworkProbe?: boolean;
|
|
299
|
+
/** Attach cached vectors after lexical hydration instead of awaiting cache I/O. */
|
|
300
|
+
deferCachedVectors?: boolean;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
type HydrationCandidate = { observation: any; id: string };
|
|
304
|
+
|
|
305
|
+
function observationEmbeddingText(observation: any): string {
|
|
306
|
+
return [
|
|
307
|
+
observation.title ?? '',
|
|
308
|
+
observation.narrative ?? '',
|
|
309
|
+
...(Array.isArray(observation.facts) ? observation.facts : []),
|
|
310
|
+
].join(' ');
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function documentEmbeddingText(document: Pick<MemorixDocument, 'title' | 'narrative' | 'facts'>): string {
|
|
314
|
+
return [document.title ?? '', document.narrative ?? '', document.facts ?? ''].join(' ');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function isCompatibleCachedVector(vector: number[] | null): vector is number[] {
|
|
318
|
+
return Boolean(
|
|
319
|
+
vector &&
|
|
320
|
+
embeddingDimensions !== null &&
|
|
321
|
+
vector.length === embeddingDimensions &&
|
|
322
|
+
vector.every(Number.isFinite),
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function attachCachedVectors(
|
|
327
|
+
database: AnyOrama,
|
|
328
|
+
candidates: HydrationCandidate[],
|
|
329
|
+
): Promise<void> {
|
|
330
|
+
const cachedVectors = await getCachedEmbeddings(
|
|
331
|
+
candidates.map(({ observation }) => observationEmbeddingText(observation)),
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
// A reset or project switch created a new index while the cache was loading.
|
|
335
|
+
// Do not attach stale vectors to that new index.
|
|
336
|
+
if (db !== database) return;
|
|
337
|
+
|
|
338
|
+
for (let index = 0; index < candidates.length; index++) {
|
|
339
|
+
const vector = cachedVectors[index];
|
|
340
|
+
if (!isCompatibleCachedVector(vector)) continue;
|
|
341
|
+
const existing = getByID(database, candidates[index].id) as MemorixDocument | undefined;
|
|
342
|
+
if (!existing || documentEmbeddingText(existing) !== observationEmbeddingText(candidates[index].observation)) continue;
|
|
343
|
+
try {
|
|
344
|
+
await update(database, candidates[index].id, { ...existing, embedding: vector });
|
|
345
|
+
} catch {
|
|
346
|
+
// Vector cache hydration is best-effort. The normal backfill lane owns misses.
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export async function hydrateIndex(
|
|
352
|
+
observations: any[],
|
|
353
|
+
options: HydrateIndexOptions = {},
|
|
354
|
+
): Promise<number> {
|
|
355
|
+
const database = await getDb({ allowNetworkProbe: options.allowNetworkProbe });
|
|
356
|
+
|
|
357
|
+
const candidates: HydrationCandidate[] = [];
|
|
358
|
+
for (const observation of observations) {
|
|
359
|
+
if (!observation || !observation.id || !observation.projectId) continue;
|
|
360
|
+
const id = makeOramaObservationId(observation.projectId, observation.id);
|
|
361
|
+
if (getByID(database, id)) continue;
|
|
362
|
+
candidates.push({ observation, id });
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const deferCachedVectors = options.deferCachedVectors === true && Boolean(indexEmbeddingProvider?.getCachedEmbeddings);
|
|
366
|
+
const cachedVectors = deferCachedVectors
|
|
367
|
+
? candidates.map(() => null)
|
|
368
|
+
: await getCachedEmbeddings(candidates.map(({ observation }) => observationEmbeddingText(observation)));
|
|
250
369
|
|
|
251
370
|
let inserted = 0;
|
|
252
|
-
for (
|
|
253
|
-
|
|
371
|
+
for (let index = 0; index < candidates.length; index++) {
|
|
372
|
+
const { observation: obs, id } = candidates[index];
|
|
254
373
|
try {
|
|
255
|
-
const
|
|
256
|
-
|
|
374
|
+
const vector = cachedVectors[index];
|
|
375
|
+
const compatibleVector = isCompatibleCachedVector(vector) ? vector : null;
|
|
257
376
|
const doc: MemorixDocument = {
|
|
258
377
|
id,
|
|
259
378
|
observationId: obs.id,
|
|
@@ -273,15 +392,48 @@ export async function hydrateIndex(observations: any[]): Promise<number> {
|
|
|
273
392
|
source: obs.source || 'agent',
|
|
274
393
|
documentType: 'observation',
|
|
275
394
|
knowledgeLayer: resolveKnowledgeLayer('observation', obs.sourceDetail, obs.source),
|
|
395
|
+
...(compatibleVector ? { embedding: compatibleVector } : {}),
|
|
276
396
|
};
|
|
277
397
|
await insert(database, doc);
|
|
278
398
|
rememberObservationDoc(doc);
|
|
279
399
|
inserted++;
|
|
280
400
|
} catch { /* skip malformed entries */ }
|
|
281
401
|
}
|
|
402
|
+
|
|
403
|
+
deferredCachedVectorHydration = deferCachedVectors
|
|
404
|
+
? attachCachedVectors(database, candidates).catch(() => {})
|
|
405
|
+
: null;
|
|
282
406
|
return inserted;
|
|
283
407
|
}
|
|
284
408
|
|
|
409
|
+
/**
|
|
410
|
+
* Prepare the lexical index without probing a remote embedding API. Cached
|
|
411
|
+
* vectors attach after the disk cache is ready, outside the startup path.
|
|
412
|
+
*/
|
|
413
|
+
export async function hydrateIndexForStartup(observations: any[]): Promise<number> {
|
|
414
|
+
return hydrateIndex(observations, {
|
|
415
|
+
allowNetworkProbe: false,
|
|
416
|
+
deferCachedVectors: true,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Returns the background cache-only hydration started by the most recent
|
|
422
|
+
* startup index pass. It never makes an embedding API call.
|
|
423
|
+
*/
|
|
424
|
+
export function getDeferredCachedVectorHydration(): Promise<void> | null {
|
|
425
|
+
return deferredCachedVectorHydration;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/** Return whether the current in-memory index already has a usable vector. */
|
|
429
|
+
export function hasObservationVector(projectId: string, observationId: number): boolean {
|
|
430
|
+
if (!db || !embeddingEnabled || embeddingDimensions === null) return false;
|
|
431
|
+
const document = getByID(db, makeOramaObservationId(projectId, observationId)) as MemorixDocument | undefined;
|
|
432
|
+
return Array.isArray(document?.embedding) &&
|
|
433
|
+
document.embedding.length === embeddingDimensions &&
|
|
434
|
+
document.embedding.every(Number.isFinite);
|
|
435
|
+
}
|
|
436
|
+
|
|
285
437
|
/**
|
|
286
438
|
* Insert an observation document into the store.
|
|
287
439
|
*/
|