@remnic/core 9.3.563 → 9.3.564
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/dist/access-cli.js +40 -39
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.js +16 -16
- package/dist/access-mcp.js +13 -13
- package/dist/access-schema.js +3 -3
- package/dist/access-service.js +11 -11
- package/dist/active-recall.js +1 -1
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/briefing.js +4 -4
- package/dist/causal-consolidation.js +5 -5
- package/dist/{chunk-I2K6KCVC.js → chunk-2FHLI4U6.js} +49 -49
- package/dist/chunk-3ONXXHQO.js +57 -0
- package/dist/chunk-3ONXXHQO.js.map +1 -0
- package/dist/{chunk-5GX5MUQ2.js → chunk-574MU2Y3.js} +3 -3
- package/dist/{chunk-65OLPXBU.js → chunk-5WB4C7KM.js} +6 -6
- package/dist/chunk-6PTSXBPE.js +483 -0
- package/dist/chunk-6PTSXBPE.js.map +1 -0
- package/dist/{chunk-Z56KAZQL.js → chunk-74VA26CT.js} +2 -2
- package/dist/{chunk-CC2ESOOG.js → chunk-7X7TBJRX.js} +2 -2
- package/dist/{chunk-O4M4WH6V.js → chunk-ARY5OOLG.js} +2 -2
- package/dist/{chunk-JBPKEARU.js → chunk-AU7Q3LSC.js} +4 -4
- package/dist/{chunk-PM3QHTFT.js → chunk-CF3ZF2YU.js} +3 -3
- package/dist/{chunk-SI3QCHWF.js → chunk-DARLGSFX.js} +5 -5
- package/dist/chunk-EWLQPEO6.js +308 -0
- package/dist/chunk-EWLQPEO6.js.map +1 -0
- package/dist/{chunk-FVCZINOF.js → chunk-FHBEL473.js} +2 -2
- package/dist/{chunk-7Q3RCKAQ.js → chunk-FXKPZ3H6.js} +2 -2
- package/dist/{chunk-5WLYNZPC.js → chunk-GBXGCFRH.js} +2 -2
- package/dist/{chunk-ILJXM3FV.js → chunk-HQO5EBUC.js} +10 -10
- package/dist/{chunk-FK556DDH.js → chunk-I4UNL747.js} +4 -4
- package/dist/{chunk-RLPIT4YI.js → chunk-IOTTZLFF.js} +38 -38
- package/dist/{chunk-TVZ6LKKS.js → chunk-IRFF6LSF.js} +8 -8
- package/dist/{chunk-M5T4Q2ZU.js → chunk-KGK2QKWL.js} +1 -1
- package/dist/chunk-KGK2QKWL.js.map +1 -0
- package/dist/{chunk-IPLYGWQF.js → chunk-KQAFEZQX.js} +5 -5
- package/dist/chunk-M46RYSMW.js +597 -0
- package/dist/chunk-M46RYSMW.js.map +1 -0
- package/dist/{chunk-KXULCVOC.js → chunk-M6I5Z4SR.js} +4 -2
- package/dist/chunk-M6I5Z4SR.js.map +1 -0
- package/dist/{chunk-JFN6K74Q.js → chunk-MQEIWDYW.js} +2 -2
- package/dist/{chunk-7H6CFEBJ.js → chunk-NZPF2SYV.js} +8 -1
- package/dist/{chunk-7H6CFEBJ.js.map → chunk-NZPF2SYV.js.map} +1 -1
- package/dist/{chunk-SML26KED.js → chunk-OB6353F7.js} +16 -12
- package/dist/chunk-OB6353F7.js.map +1 -0
- package/dist/{chunk-SOTR74FK.js → chunk-OPYFD6PD.js} +2 -2
- package/dist/{chunk-3C5RPJAX.js → chunk-OXJBNGBK.js} +2 -2
- package/dist/{chunk-BD5LHQWD.js → chunk-PPPZY2EU.js} +2 -2
- package/dist/{chunk-25BY3HHZ.js → chunk-SUTSSOYU.js} +2 -2
- package/dist/{chunk-KS7WO6EQ.js → chunk-VFB2G5YL.js} +20 -20
- package/dist/{chunk-BUUYY2H2.js → chunk-WP5OWVLZ.js} +4 -4
- package/dist/{chunk-6URPAY2D.js → chunk-XCAZF7KQ.js} +207 -53
- package/dist/chunk-XCAZF7KQ.js.map +1 -0
- package/dist/{chunk-S53PAX2V.js → chunk-XM7BYXT7.js} +2 -2
- package/dist/{chunk-FADZBOR4.js → chunk-XRWTAEZM.js} +2 -2
- package/dist/{chunk-E5OECWZ5.js → chunk-XT7XVA53.js} +2 -2
- package/dist/{chunk-R3PS27B4.js → chunk-Z4R6RI2N.js} +2 -2
- package/dist/cli.js +44 -43
- package/dist/compounding/engine.js +4 -4
- package/dist/config.js +1 -1
- package/dist/connectors/codex-materialize-runner.js +4 -4
- package/dist/connectors/index.js +4 -4
- package/dist/embedding-fallback.d.ts +12 -1
- package/dist/embedding-fallback.js +4 -1
- package/dist/entity-retrieval.js +4 -4
- package/dist/host-embedding-provider.d.ts +21 -0
- package/dist/host-embedding-provider.js +14 -0
- package/dist/host-embedding-provider.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +71 -63
- package/dist/index.js.map +1 -1
- package/dist/lcm/index.js +3 -3
- package/dist/maintenance/memory-governance.js +4 -4
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +4 -4
- package/dist/maintenance/rebuild-memory-projection.js +5 -5
- package/dist/namespaces/migrate.js +14 -13
- package/dist/namespaces/search.js +9 -8
- package/dist/namespaces/storage.js +4 -4
- package/dist/operator-toolkit.js +17 -16
- package/dist/orchestrator.js +32 -31
- package/dist/recall-explain-renderer.js +3 -3
- package/dist/recall-xray-cli.js +4 -4
- package/dist/recall-xray-renderer.js +3 -3
- package/dist/recall-xray.js +2 -2
- package/dist/resume-bundles.js +2 -2
- package/dist/search/embed-helper.d.ts +48 -4
- package/dist/search/embed-helper.js +2 -1
- package/dist/search/factory.js +8 -7
- package/dist/search/index.d.ts +1 -0
- package/dist/search/index.js +12 -11
- package/dist/search/lancedb-backend.d.ts +11 -0
- package/dist/search/lancedb-backend.js +2 -2
- package/dist/search/meilisearch-backend.js +2 -2
- package/dist/search/orama-backend.d.ts +16 -0
- package/dist/search/orama-backend.js +2 -2
- package/dist/semantic-consolidation.js +5 -5
- package/dist/semantic-rule-promotion.js +4 -4
- package/dist/semantic-rule-verifier.js +4 -4
- package/dist/storage.js +3 -3
- package/dist/transfer/autodetect.js +1 -1
- package/dist/transfer/backup.js +1 -1
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/types.d.ts +6 -6
- package/dist/types.d.ts +17 -0
- package/dist/types.js +1 -1
- package/dist/verified-recall.js +4 -4
- package/package.json +11 -1
- package/src/config.ts +18 -0
- package/src/embedding-fallback.ts +293 -61
- package/src/host-embedding-provider.ts +84 -0
- package/src/index.ts +7 -0
- package/src/namespaces/search.ts +9 -1
- package/src/qmd.test.ts +28 -0
- package/src/search/embed-helper.ts +319 -51
- package/src/search/factory.ts +6 -2
- package/src/search/lancedb-backend.ts +297 -41
- package/src/search/orama-backend.ts +418 -47
- package/src/types.ts +17 -0
- package/dist/chunk-6URPAY2D.js.map +0 -1
- package/dist/chunk-FUC4LZMD.js +0 -301
- package/dist/chunk-FUC4LZMD.js.map +0 -1
- package/dist/chunk-KXULCVOC.js.map +0 -1
- package/dist/chunk-M5T4Q2ZU.js.map +0 -1
- package/dist/chunk-ONPLNAPX.js +0 -133
- package/dist/chunk-ONPLNAPX.js.map +0 -1
- package/dist/chunk-QVJ4NWL2.js +0 -335
- package/dist/chunk-QVJ4NWL2.js.map +0 -1
- package/dist/chunk-SML26KED.js.map +0 -1
- /package/dist/{chunk-I2K6KCVC.js.map → chunk-2FHLI4U6.js.map} +0 -0
- /package/dist/{chunk-5GX5MUQ2.js.map → chunk-574MU2Y3.js.map} +0 -0
- /package/dist/{chunk-65OLPXBU.js.map → chunk-5WB4C7KM.js.map} +0 -0
- /package/dist/{chunk-Z56KAZQL.js.map → chunk-74VA26CT.js.map} +0 -0
- /package/dist/{chunk-CC2ESOOG.js.map → chunk-7X7TBJRX.js.map} +0 -0
- /package/dist/{chunk-O4M4WH6V.js.map → chunk-ARY5OOLG.js.map} +0 -0
- /package/dist/{chunk-JBPKEARU.js.map → chunk-AU7Q3LSC.js.map} +0 -0
- /package/dist/{chunk-PM3QHTFT.js.map → chunk-CF3ZF2YU.js.map} +0 -0
- /package/dist/{chunk-SI3QCHWF.js.map → chunk-DARLGSFX.js.map} +0 -0
- /package/dist/{chunk-FVCZINOF.js.map → chunk-FHBEL473.js.map} +0 -0
- /package/dist/{chunk-7Q3RCKAQ.js.map → chunk-FXKPZ3H6.js.map} +0 -0
- /package/dist/{chunk-5WLYNZPC.js.map → chunk-GBXGCFRH.js.map} +0 -0
- /package/dist/{chunk-ILJXM3FV.js.map → chunk-HQO5EBUC.js.map} +0 -0
- /package/dist/{chunk-FK556DDH.js.map → chunk-I4UNL747.js.map} +0 -0
- /package/dist/{chunk-RLPIT4YI.js.map → chunk-IOTTZLFF.js.map} +0 -0
- /package/dist/{chunk-TVZ6LKKS.js.map → chunk-IRFF6LSF.js.map} +0 -0
- /package/dist/{chunk-IPLYGWQF.js.map → chunk-KQAFEZQX.js.map} +0 -0
- /package/dist/{chunk-JFN6K74Q.js.map → chunk-MQEIWDYW.js.map} +0 -0
- /package/dist/{chunk-SOTR74FK.js.map → chunk-OPYFD6PD.js.map} +0 -0
- /package/dist/{chunk-3C5RPJAX.js.map → chunk-OXJBNGBK.js.map} +0 -0
- /package/dist/{chunk-BD5LHQWD.js.map → chunk-PPPZY2EU.js.map} +0 -0
- /package/dist/{chunk-25BY3HHZ.js.map → chunk-SUTSSOYU.js.map} +0 -0
- /package/dist/{chunk-KS7WO6EQ.js.map → chunk-VFB2G5YL.js.map} +0 -0
- /package/dist/{chunk-BUUYY2H2.js.map → chunk-WP5OWVLZ.js.map} +0 -0
- /package/dist/{chunk-S53PAX2V.js.map → chunk-XM7BYXT7.js.map} +0 -0
- /package/dist/{chunk-FADZBOR4.js.map → chunk-XRWTAEZM.js.map} +0 -0
- /package/dist/{chunk-E5OECWZ5.js.map → chunk-XT7XVA53.js.map} +0 -0
- /package/dist/{chunk-R3PS27B4.js.map → chunk-Z4R6RI2N.js.map} +0 -0
|
@@ -3,14 +3,20 @@ import { mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { log } from "./logger.js";
|
|
4
4
|
import { readEnvVar } from "./runtime/env.js";
|
|
5
5
|
import type { PluginConfig } from "./types.js";
|
|
6
|
+
import {
|
|
7
|
+
getHostEmbeddingProvider,
|
|
8
|
+
type HostEmbeddingProvider,
|
|
9
|
+
normalizeHostEmbeddingVector,
|
|
10
|
+
} from "./host-embedding-provider.js";
|
|
6
11
|
|
|
7
|
-
type EmbeddingProviderType = "openai" | "local";
|
|
12
|
+
type EmbeddingProviderType = "openai" | "local" | "host";
|
|
8
13
|
|
|
9
14
|
type ProviderConfig = {
|
|
10
15
|
type: EmbeddingProviderType;
|
|
11
16
|
model: string;
|
|
12
|
-
endpoint
|
|
13
|
-
headers
|
|
17
|
+
endpoint?: string;
|
|
18
|
+
headers?: Record<string, string>;
|
|
19
|
+
hostProvider?: HostEmbeddingProvider;
|
|
14
20
|
};
|
|
15
21
|
|
|
16
22
|
type EmbeddingIndexEntry = {
|
|
@@ -18,6 +24,11 @@ type EmbeddingIndexEntry = {
|
|
|
18
24
|
path: string;
|
|
19
25
|
};
|
|
20
26
|
|
|
27
|
+
type EmbeddingResult = {
|
|
28
|
+
provider: ProviderConfig;
|
|
29
|
+
vector: number[];
|
|
30
|
+
};
|
|
31
|
+
|
|
21
32
|
type EmbeddingIndexFile = {
|
|
22
33
|
version: 1;
|
|
23
34
|
provider: EmbeddingProviderType;
|
|
@@ -25,6 +36,11 @@ type EmbeddingIndexFile = {
|
|
|
25
36
|
entries: Record<string, EmbeddingIndexEntry>;
|
|
26
37
|
};
|
|
27
38
|
|
|
39
|
+
type EmbeddingIndexIdentity = Pick<EmbeddingIndexFile, "provider" | "model">;
|
|
40
|
+
type EmbeddingIndexComparable =
|
|
41
|
+
| EmbeddingIndexIdentity
|
|
42
|
+
| Pick<ProviderConfig, "type" | "model">;
|
|
43
|
+
|
|
28
44
|
const DEFAULT_OPENAI_MODEL = "text-embedding-3-small";
|
|
29
45
|
|
|
30
46
|
/**
|
|
@@ -63,6 +79,20 @@ export class EmbeddingTimeoutError extends Error {
|
|
|
63
79
|
}
|
|
64
80
|
}
|
|
65
81
|
|
|
82
|
+
export class EmbeddingProviderUnavailableError extends Error {
|
|
83
|
+
override readonly name = "EmbeddingProviderUnavailableError" as const;
|
|
84
|
+
constructor(message: string) {
|
|
85
|
+
super(message);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isLookupBackendUnavailableError(err: unknown): boolean {
|
|
90
|
+
return (
|
|
91
|
+
err instanceof EmbeddingTimeoutError ||
|
|
92
|
+
err instanceof EmbeddingProviderUnavailableError
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
66
96
|
/**
|
|
67
97
|
* Maximum time to wait for an embedding HTTP request on the LOOKUP/query
|
|
68
98
|
* path before giving up.
|
|
@@ -228,26 +258,33 @@ export class EmbeddingFallback {
|
|
|
228
258
|
const provider = await this.resolveProvider();
|
|
229
259
|
if (!provider) return [];
|
|
230
260
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
261
|
+
let queryResult = await this.embedForSearch(query, provider, options);
|
|
262
|
+
if (!queryResult) return [];
|
|
263
|
+
|
|
264
|
+
const diskIdentity = await this.readIndexIdentityFromDisk();
|
|
265
|
+
if (diskIdentity && !sameIndexIdentity(diskIdentity, queryResult.provider)) {
|
|
266
|
+
const diskProvider = await this.resolveFallbackProviderForIndexIdentity(diskIdentity);
|
|
267
|
+
if (diskProvider) {
|
|
268
|
+
const diskQueryResult = await this.embedForSearch(query, diskProvider, options);
|
|
269
|
+
if (diskQueryResult && sameIndexIdentity(diskIdentity, diskQueryResult.provider)) {
|
|
270
|
+
queryResult = diskQueryResult;
|
|
271
|
+
} else {
|
|
272
|
+
log.debug(
|
|
273
|
+
`embedding fallback search skipped: preserved ${diskIdentity.provider}/${diskIdentity.model} index is unavailable for lookup`,
|
|
274
|
+
);
|
|
275
|
+
return [];
|
|
242
276
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
277
|
+
} else {
|
|
278
|
+
log.debug(
|
|
279
|
+
`embedding fallback search skipped: query provider ${queryResult.provider.type}/${queryResult.provider.model} does not match existing ${diskIdentity.provider}/${diskIdentity.model} index`,
|
|
280
|
+
);
|
|
246
281
|
return [];
|
|
247
282
|
}
|
|
248
|
-
throw err;
|
|
249
283
|
}
|
|
250
|
-
|
|
284
|
+
|
|
285
|
+
const index = await this.loadIndex(queryResult.provider);
|
|
286
|
+
const ids = Object.keys(index.entries);
|
|
287
|
+
if (ids.length === 0) return [];
|
|
251
288
|
|
|
252
289
|
const includePrefix = normalizePathPrefix(options.pathPrefix);
|
|
253
290
|
const excludePrefixes = (options.pathExcludePrefixes ?? [])
|
|
@@ -260,7 +297,7 @@ export class EmbeddingFallback {
|
|
|
260
297
|
return {
|
|
261
298
|
id,
|
|
262
299
|
path: entry.path,
|
|
263
|
-
score: cosineSimilarity(
|
|
300
|
+
score: cosineSimilarity(queryResult.vector, entry.vector),
|
|
264
301
|
};
|
|
265
302
|
})
|
|
266
303
|
.filter((r) => {
|
|
@@ -289,14 +326,27 @@ export class EmbeddingFallback {
|
|
|
289
326
|
// add the entry to the index. Previously this used the short lookup
|
|
290
327
|
// budget and silently dropped updates, leaving later dedup lookups
|
|
291
328
|
// blind to the memory. Related: PR #399 P2.
|
|
292
|
-
const
|
|
293
|
-
|
|
329
|
+
const result = await this.embedWithEffectiveProvider(content, provider, {
|
|
330
|
+
mode: "index",
|
|
331
|
+
});
|
|
332
|
+
if (!result) return;
|
|
294
333
|
|
|
295
334
|
await this.enqueueIndexMutation(async () => {
|
|
296
|
-
const
|
|
335
|
+
const existing = await this.readIndexIdentityFromDisk();
|
|
336
|
+
if (
|
|
337
|
+
existing &&
|
|
338
|
+
!sameIndexIdentity(existing, result.provider) &&
|
|
339
|
+
!canReplaceIndexIdentity(existing, result.provider)
|
|
340
|
+
) {
|
|
341
|
+
log.debug(
|
|
342
|
+
`embedding fallback index update skipped: ${result.provider.type}/${result.provider.model} would replace existing ${existing.provider}/${existing.model} index`,
|
|
343
|
+
);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const index = await this.loadIndex(result.provider);
|
|
297
347
|
const relPath = toMemoryRelativePath(this.config.memoryDir, filePath);
|
|
298
348
|
index.entries[memoryId] = {
|
|
299
|
-
vector,
|
|
349
|
+
vector: result.vector,
|
|
300
350
|
path: relPath,
|
|
301
351
|
};
|
|
302
352
|
await this.saveIndex(index);
|
|
@@ -308,10 +358,30 @@ export class EmbeddingFallback {
|
|
|
308
358
|
if (!provider) return;
|
|
309
359
|
|
|
310
360
|
await this.enqueueIndexMutation(async () => {
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
361
|
+
const providers = [provider];
|
|
362
|
+
const diskIdentity = await this.readIndexIdentityFromDisk();
|
|
363
|
+
if (
|
|
364
|
+
diskIdentity &&
|
|
365
|
+
!providers.some((entry) => sameIndexIdentity(entry, diskIdentity))
|
|
366
|
+
) {
|
|
367
|
+
providers.push(providerFromIndexIdentity(diskIdentity));
|
|
368
|
+
}
|
|
369
|
+
if (provider.type === "host") {
|
|
370
|
+
const fallbackProvider = await this.resolveProvider({ includeHost: false });
|
|
371
|
+
if (
|
|
372
|
+
fallbackProvider &&
|
|
373
|
+
!providers.some((entry) => sameIndexIdentity(entry, fallbackProvider))
|
|
374
|
+
) {
|
|
375
|
+
providers.push(fallbackProvider);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
for (const indexProvider of providers) {
|
|
380
|
+
const index = await this.loadIndex(indexProvider);
|
|
381
|
+
if (!index.entries[memoryId]) continue;
|
|
382
|
+
delete index.entries[memoryId];
|
|
383
|
+
await this.saveIndex(index);
|
|
384
|
+
}
|
|
315
385
|
});
|
|
316
386
|
}
|
|
317
387
|
|
|
@@ -324,56 +394,130 @@ export class EmbeddingFallback {
|
|
|
324
394
|
return run;
|
|
325
395
|
}
|
|
326
396
|
|
|
327
|
-
private async resolveProvider(
|
|
397
|
+
private async resolveProvider(
|
|
398
|
+
options: { includeHost?: boolean } = {},
|
|
399
|
+
): Promise<ProviderConfig | null> {
|
|
328
400
|
if (!this.config.embeddingFallbackEnabled) return null;
|
|
329
401
|
|
|
402
|
+
if (
|
|
403
|
+
options.includeHost !== false &&
|
|
404
|
+
this.config.hostEmbeddingProviderEnabled !== false
|
|
405
|
+
) {
|
|
406
|
+
const hostProvider = getHostEmbeddingProvider(this.config.memoryDir);
|
|
407
|
+
if (hostProvider) {
|
|
408
|
+
return {
|
|
409
|
+
type: "host",
|
|
410
|
+
model: hostProvider.model || hostProvider.id,
|
|
411
|
+
hostProvider,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
330
416
|
const preferred = this.config.embeddingFallbackProvider;
|
|
331
417
|
const providers = preferred === "auto" ? ["openai", "local"] : [preferred];
|
|
332
418
|
|
|
333
419
|
for (const p of providers) {
|
|
334
|
-
if (p === "openai"
|
|
335
|
-
const
|
|
336
|
-
return
|
|
337
|
-
type: "openai",
|
|
338
|
-
model: DEFAULT_OPENAI_MODEL,
|
|
339
|
-
endpoint: `${baseUrl.replace(/\/$/, "")}/embeddings`,
|
|
340
|
-
headers: {
|
|
341
|
-
"Content-Type": "application/json",
|
|
342
|
-
Authorization: `Bearer ${this.config.openaiApiKey}`,
|
|
343
|
-
},
|
|
344
|
-
};
|
|
420
|
+
if (p === "openai") {
|
|
421
|
+
const provider = this.createOpenAiProvider();
|
|
422
|
+
if (provider) return provider;
|
|
345
423
|
}
|
|
346
424
|
|
|
347
|
-
if (p === "local"
|
|
348
|
-
const
|
|
349
|
-
|
|
350
|
-
const headers: Record<string, string> = {
|
|
351
|
-
"Content-Type": "application/json",
|
|
352
|
-
...(this.config.localLlmHeaders ?? {}),
|
|
353
|
-
};
|
|
354
|
-
if (this.config.localLlmApiKey && this.config.localLlmAuthHeader !== false) {
|
|
355
|
-
headers.Authorization = `Bearer ${this.config.localLlmApiKey}`;
|
|
356
|
-
}
|
|
357
|
-
return {
|
|
358
|
-
type: "local",
|
|
359
|
-
model:
|
|
360
|
-
this.config.embeddingFallbackModel ||
|
|
361
|
-
this.config.localLlmModel ||
|
|
362
|
-
DEFAULT_OPENAI_MODEL,
|
|
363
|
-
endpoint,
|
|
364
|
-
headers,
|
|
365
|
-
};
|
|
425
|
+
if (p === "local") {
|
|
426
|
+
const provider = this.createLocalProvider();
|
|
427
|
+
if (provider) return provider;
|
|
366
428
|
}
|
|
367
429
|
}
|
|
368
430
|
|
|
369
431
|
return null;
|
|
370
432
|
}
|
|
371
433
|
|
|
434
|
+
private async resolveFallbackProviderForIndexIdentity(
|
|
435
|
+
identity: EmbeddingIndexIdentity,
|
|
436
|
+
): Promise<ProviderConfig | null> {
|
|
437
|
+
if (identity.provider === "openai") {
|
|
438
|
+
const provider = this.createOpenAiProvider();
|
|
439
|
+
return provider && sameIndexIdentity(provider, identity) ? provider : null;
|
|
440
|
+
}
|
|
441
|
+
if (identity.provider === "local") {
|
|
442
|
+
const provider = this.createLocalProvider();
|
|
443
|
+
return provider && sameIndexIdentity(provider, identity) ? provider : null;
|
|
444
|
+
}
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private createOpenAiProvider(): ProviderConfig | null {
|
|
449
|
+
if (!this.config.openaiApiKey) return null;
|
|
450
|
+
const baseUrl = this.config.openaiBaseUrl ?? "https://api.openai.com/v1";
|
|
451
|
+
return {
|
|
452
|
+
type: "openai",
|
|
453
|
+
model: DEFAULT_OPENAI_MODEL,
|
|
454
|
+
endpoint: `${baseUrl.replace(/\/$/, "")}/embeddings`,
|
|
455
|
+
headers: {
|
|
456
|
+
"Content-Type": "application/json",
|
|
457
|
+
Authorization: `Bearer ${this.config.openaiApiKey}`,
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
private createLocalProvider(): ProviderConfig | null {
|
|
463
|
+
if (!this.config.localLlmEnabled || !this.config.localLlmUrl) return null;
|
|
464
|
+
const base = this.config.localLlmUrl.replace(/\/$/, "");
|
|
465
|
+
const endpoint = /\/v1$/i.test(base) ? `${base}/embeddings` : `${base}/v1/embeddings`;
|
|
466
|
+
const headers: Record<string, string> = {
|
|
467
|
+
"Content-Type": "application/json",
|
|
468
|
+
...(this.config.localLlmHeaders ?? {}),
|
|
469
|
+
};
|
|
470
|
+
if (this.config.localLlmApiKey && this.config.localLlmAuthHeader !== false) {
|
|
471
|
+
headers.Authorization = `Bearer ${this.config.localLlmApiKey}`;
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
type: "local",
|
|
475
|
+
model:
|
|
476
|
+
this.config.embeddingFallbackModel ||
|
|
477
|
+
this.config.localLlmModel ||
|
|
478
|
+
DEFAULT_OPENAI_MODEL,
|
|
479
|
+
endpoint,
|
|
480
|
+
headers,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private async embedForSearch(
|
|
485
|
+
query: string,
|
|
486
|
+
provider: ProviderConfig,
|
|
487
|
+
options: { throwOnTimeout?: boolean } = {},
|
|
488
|
+
): Promise<EmbeddingResult | null> {
|
|
489
|
+
try {
|
|
490
|
+
return await this.embedWithEffectiveProvider(query, provider, {
|
|
491
|
+
mode: "lookup",
|
|
492
|
+
});
|
|
493
|
+
} catch (err) {
|
|
494
|
+
if (isLookupBackendUnavailableError(err)) {
|
|
495
|
+
if (options.throwOnTimeout) {
|
|
496
|
+
throw err;
|
|
497
|
+
}
|
|
498
|
+
// Fail-open: recall-path callers get an empty result rather than an
|
|
499
|
+
// unhandled rejection that would abort recall entirely.
|
|
500
|
+
log.debug("embedding fallback search: backend unavailable on lookup, returning [] (throwOnTimeout=false)");
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
throw err;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
372
507
|
private async embed(
|
|
373
508
|
input: string,
|
|
374
509
|
provider: ProviderConfig,
|
|
375
510
|
options: { mode?: EmbedMode } = {},
|
|
376
511
|
): Promise<number[] | null> {
|
|
512
|
+
const result = await this.embedWithEffectiveProvider(input, provider, options);
|
|
513
|
+
return result?.vector ?? null;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
private async embedWithEffectiveProvider(
|
|
517
|
+
input: string,
|
|
518
|
+
provider: ProviderConfig,
|
|
519
|
+
options: { mode?: EmbedMode } = {},
|
|
520
|
+
): Promise<EmbeddingResult | null> {
|
|
377
521
|
// Bound the fetch so a hung embedding endpoint cannot stall callers.
|
|
378
522
|
// The lookup path uses a short budget (see DEFAULT_EMBEDDING_LOOKUP_TIMEOUT_MS
|
|
379
523
|
// docblock) so semantic dedup fails open fast. The index path uses a
|
|
@@ -385,6 +529,21 @@ export class EmbeddingFallback {
|
|
|
385
529
|
mode === "index"
|
|
386
530
|
? resolveEmbeddingIndexTimeoutMs()
|
|
387
531
|
: resolveEmbeddingLookupTimeoutMs();
|
|
532
|
+
if (provider.type === "host") {
|
|
533
|
+
const vector = await this.embedWithHostProvider(input, provider, mode, timeoutMs);
|
|
534
|
+
if (vector) return { provider, vector };
|
|
535
|
+
const fallbackProvider = await this.resolveProvider({ includeHost: false });
|
|
536
|
+
if (!fallbackProvider) {
|
|
537
|
+
if (mode === "lookup") {
|
|
538
|
+
throw new EmbeddingProviderUnavailableError(
|
|
539
|
+
`host embedding provider unavailable (${provider.hostProvider?.id ?? provider.model})`,
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
return this.embedWithEffectiveProvider(input, fallbackProvider, options);
|
|
545
|
+
}
|
|
546
|
+
if (!provider.endpoint || !provider.headers) return null;
|
|
388
547
|
try {
|
|
389
548
|
const res = await fetch(provider.endpoint, {
|
|
390
549
|
method: "POST",
|
|
@@ -417,12 +576,15 @@ export class EmbeddingFallback {
|
|
|
417
576
|
const payload = (await res.json()) as any;
|
|
418
577
|
const vector = payload?.data?.[0]?.embedding;
|
|
419
578
|
if (!Array.isArray(vector)) return null;
|
|
420
|
-
|
|
579
|
+
const normalized = vector
|
|
580
|
+
.map((n: unknown) => Number(n))
|
|
581
|
+
.filter((n: number) => Number.isFinite(n));
|
|
582
|
+
return normalized.length > 0 ? { provider, vector: normalized } : null;
|
|
421
583
|
} catch (err) {
|
|
422
584
|
// Round 11 (Finding Ur_J): the !res.ok branch above throws
|
|
423
585
|
// EmbeddingTimeoutError directly. Re-throw it here so the catch does
|
|
424
586
|
// not swallow our own intentional signal back into a null return.
|
|
425
|
-
if (err
|
|
587
|
+
if (isLookupBackendUnavailableError(err)) {
|
|
426
588
|
throw err;
|
|
427
589
|
}
|
|
428
590
|
// AbortSignal.timeout throws a DOMException with name "TimeoutError";
|
|
@@ -479,6 +641,26 @@ export class EmbeddingFallback {
|
|
|
479
641
|
}
|
|
480
642
|
}
|
|
481
643
|
|
|
644
|
+
private async embedWithHostProvider(
|
|
645
|
+
input: string,
|
|
646
|
+
provider: ProviderConfig,
|
|
647
|
+
mode: EmbedMode,
|
|
648
|
+
timeoutMs: number,
|
|
649
|
+
): Promise<number[] | null> {
|
|
650
|
+
const hostProvider = provider.hostProvider;
|
|
651
|
+
if (!hostProvider) return null;
|
|
652
|
+
try {
|
|
653
|
+
const vector = await hostProvider.embed(input.slice(0, 8000), {
|
|
654
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
655
|
+
inputType: mode === "lookup" ? "query" : "document",
|
|
656
|
+
});
|
|
657
|
+
return normalizeHostEmbeddingVector(vector);
|
|
658
|
+
} catch (err) {
|
|
659
|
+
log.debug(`host embedding provider error: ${hostProvider.id}: ${err}`);
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
482
664
|
private async loadIndex(provider: ProviderConfig): Promise<EmbeddingIndexFile> {
|
|
483
665
|
if (this.loaded && this.loaded.provider === provider.type && this.loaded.model === provider.model) {
|
|
484
666
|
return this.loaded;
|
|
@@ -516,6 +698,31 @@ export class EmbeddingFallback {
|
|
|
516
698
|
return this.loaded;
|
|
517
699
|
}
|
|
518
700
|
|
|
701
|
+
private async readIndexIdentityFromDisk(): Promise<EmbeddingIndexIdentity | null> {
|
|
702
|
+
try {
|
|
703
|
+
const raw = await readFile(this.indexPath, "utf-8");
|
|
704
|
+
const parsed = JSON.parse(raw) as Partial<EmbeddingIndexFile> | null;
|
|
705
|
+
if (
|
|
706
|
+
parsed &&
|
|
707
|
+
parsed.version === 1 &&
|
|
708
|
+
(parsed.provider === "openai" ||
|
|
709
|
+
parsed.provider === "local" ||
|
|
710
|
+
parsed.provider === "host") &&
|
|
711
|
+
typeof parsed.model === "string" &&
|
|
712
|
+
parsed.model.length > 0
|
|
713
|
+
) {
|
|
714
|
+
return {
|
|
715
|
+
provider: parsed.provider,
|
|
716
|
+
model: parsed.model,
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
} catch {
|
|
720
|
+
// Missing or invalid indexes are treated as absent; loadIndex() owns
|
|
721
|
+
// creating a fresh file when it is safe to write.
|
|
722
|
+
}
|
|
723
|
+
return null;
|
|
724
|
+
}
|
|
725
|
+
|
|
519
726
|
private async saveIndex(index: EmbeddingIndexFile): Promise<void> {
|
|
520
727
|
const dir = path.dirname(this.indexPath);
|
|
521
728
|
await mkdir(dir, { recursive: true });
|
|
@@ -578,6 +785,31 @@ function normalizePathPrefix(prefix: string | undefined): string | undefined {
|
|
|
578
785
|
return p;
|
|
579
786
|
}
|
|
580
787
|
|
|
788
|
+
function sameIndexIdentity(
|
|
789
|
+
left: EmbeddingIndexComparable,
|
|
790
|
+
right: EmbeddingIndexComparable,
|
|
791
|
+
): boolean {
|
|
792
|
+
return indexIdentityProvider(left) === indexIdentityProvider(right) && left.model === right.model;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
function indexIdentityProvider(identity: EmbeddingIndexComparable): EmbeddingProviderType {
|
|
796
|
+
return "provider" in identity ? identity.provider : identity.type;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function canReplaceIndexIdentity(
|
|
800
|
+
existing: EmbeddingIndexIdentity,
|
|
801
|
+
replacement: Pick<ProviderConfig, "type" | "model">,
|
|
802
|
+
): boolean {
|
|
803
|
+
return existing.provider === "host" && !sameIndexIdentity(existing, replacement);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function providerFromIndexIdentity(identity: EmbeddingIndexIdentity): ProviderConfig {
|
|
807
|
+
return {
|
|
808
|
+
type: identity.provider,
|
|
809
|
+
model: identity.model,
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
|
|
581
813
|
function cosineSimilarity(a: number[], b: number[]): number {
|
|
582
814
|
const n = Math.min(a.length, b.length);
|
|
583
815
|
if (n === 0) return 0;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export type HostEmbeddingInputType =
|
|
2
|
+
| "query"
|
|
3
|
+
| "document"
|
|
4
|
+
| "semantic"
|
|
5
|
+
| "classification"
|
|
6
|
+
| "clustering";
|
|
7
|
+
|
|
8
|
+
export interface HostEmbeddingProvider {
|
|
9
|
+
id: string;
|
|
10
|
+
model?: string;
|
|
11
|
+
dimensions?: number;
|
|
12
|
+
embed(
|
|
13
|
+
text: string,
|
|
14
|
+
options?: { signal?: AbortSignal; inputType?: HostEmbeddingInputType },
|
|
15
|
+
): Promise<ArrayLike<number> | null>;
|
|
16
|
+
embedBatch?(
|
|
17
|
+
texts: string[],
|
|
18
|
+
options?: { signal?: AbortSignal; inputType?: HostEmbeddingInputType },
|
|
19
|
+
): Promise<Array<ArrayLike<number> | null>>;
|
|
20
|
+
close?: () => Promise<void> | void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const HOST_EMBEDDING_PROVIDERS_KEY = Symbol.for(
|
|
24
|
+
"remnic.hostEmbeddingProviders",
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
function providerMap(): Map<string, HostEmbeddingProvider> {
|
|
28
|
+
const store = globalThis as Record<PropertyKey, unknown>;
|
|
29
|
+
const existing = store[HOST_EMBEDDING_PROVIDERS_KEY];
|
|
30
|
+
if (existing instanceof Map) {
|
|
31
|
+
return existing as Map<string, HostEmbeddingProvider>;
|
|
32
|
+
}
|
|
33
|
+
const created = new Map<string, HostEmbeddingProvider>();
|
|
34
|
+
store[HOST_EMBEDDING_PROVIDERS_KEY] = created;
|
|
35
|
+
return created;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function registerHostEmbeddingProvider(
|
|
39
|
+
scope: string,
|
|
40
|
+
provider: HostEmbeddingProvider,
|
|
41
|
+
): () => void {
|
|
42
|
+
const key = normalizeScope(scope);
|
|
43
|
+
const providers = providerMap();
|
|
44
|
+
const previous = providers.get(key);
|
|
45
|
+
if (previous && previous !== provider) {
|
|
46
|
+
void previous.close?.();
|
|
47
|
+
}
|
|
48
|
+
providers.set(key, provider);
|
|
49
|
+
return () => {
|
|
50
|
+
const providers = providerMap();
|
|
51
|
+
if (providers.get(key) === provider) {
|
|
52
|
+
providers.delete(key);
|
|
53
|
+
void provider.close?.();
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getHostEmbeddingProvider(
|
|
59
|
+
scope: string,
|
|
60
|
+
): HostEmbeddingProvider | undefined {
|
|
61
|
+
return providerMap().get(normalizeScope(scope));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function clearHostEmbeddingProvidersForTest(): void {
|
|
65
|
+
providerMap().clear();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function normalizeHostEmbeddingVector(value: unknown): number[] | null {
|
|
69
|
+
if (!Array.isArray(value) && (!ArrayBuffer.isView(value) || value instanceof DataView)) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
const vector = Array.from(value as unknown as ArrayLike<unknown>);
|
|
73
|
+
return vector.length > 0 &&
|
|
74
|
+
vector.every((component): component is number => {
|
|
75
|
+
return typeof component === "number" && Number.isFinite(component);
|
|
76
|
+
})
|
|
77
|
+
? vector
|
|
78
|
+
: null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeScope(scope: string): string {
|
|
82
|
+
const normalized = typeof scope === "string" ? scope.trim() : "";
|
|
83
|
+
return normalized || "default";
|
|
84
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -104,6 +104,13 @@ export {
|
|
|
104
104
|
parseEntityFile,
|
|
105
105
|
serializeEntityFile,
|
|
106
106
|
} from "./storage.js";
|
|
107
|
+
export {
|
|
108
|
+
getHostEmbeddingProvider,
|
|
109
|
+
normalizeHostEmbeddingVector,
|
|
110
|
+
registerHostEmbeddingProvider,
|
|
111
|
+
type HostEmbeddingInputType,
|
|
112
|
+
type HostEmbeddingProvider,
|
|
113
|
+
} from "./host-embedding-provider.js";
|
|
107
114
|
|
|
108
115
|
// ---------------------------------------------------------------------------
|
|
109
116
|
// Extraction
|
package/src/namespaces/search.ts
CHANGED
|
@@ -35,6 +35,10 @@ type NamespaceBackendRecord = {
|
|
|
35
35
|
collectionState: "present" | "missing" | "unknown" | "skipped";
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
+
type NamespaceScopedSearchConfig = PluginConfig & {
|
|
39
|
+
hostEmbeddingProviderScope?: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
38
42
|
export class NamespaceSearchRouter {
|
|
39
43
|
private readonly cache = new Map<string, Promise<NamespaceBackendRecord>>();
|
|
40
44
|
|
|
@@ -183,9 +187,13 @@ export class NamespaceSearchRouter {
|
|
|
183
187
|
const storage = await this.storageRouter.storageFor(key);
|
|
184
188
|
const useLegacyDefaultCollection =
|
|
185
189
|
key === this.config.defaultNamespace && storage.dir === this.config.memoryDir;
|
|
186
|
-
const
|
|
190
|
+
const rootHostEmbeddingScope =
|
|
191
|
+
(this.config as NamespaceScopedSearchConfig).hostEmbeddingProviderScope ??
|
|
192
|
+
this.config.memoryDir;
|
|
193
|
+
const scopedConfig: NamespaceScopedSearchConfig = {
|
|
187
194
|
...this.config,
|
|
188
195
|
memoryDir: storage.dir,
|
|
196
|
+
hostEmbeddingProviderScope: rootHostEmbeddingScope,
|
|
189
197
|
qmdCollection: namespaceCollectionName(this.config.qmdCollection, key, {
|
|
190
198
|
defaultNamespace: this.config.defaultNamespace,
|
|
191
199
|
useLegacyDefaultCollection,
|
package/src/qmd.test.ts
CHANGED
|
@@ -277,6 +277,34 @@ test("parseConfig exposes qmd 2.5 integration defaults and opt-in auto upgrade",
|
|
|
277
277
|
assert.equal(falseGpuBackend.qmdGpuBackend, "false");
|
|
278
278
|
});
|
|
279
279
|
|
|
280
|
+
test("parseConfig exposes gated OpenClaw host feature defaults", () => {
|
|
281
|
+
const defaults = parseConfig({});
|
|
282
|
+
assert.equal(defaults.hostEmbeddingProviderEnabled, true);
|
|
283
|
+
assert.equal(defaults.hostEmbeddingProviderId, undefined);
|
|
284
|
+
assert.equal(defaults.hostEmbeddingProviderModel, undefined);
|
|
285
|
+
assert.equal(defaults.openclawMessageReceivedCaptureEnabled, true);
|
|
286
|
+
assert.equal(defaults.openclawReplyMetadataCaptureEnabled, true);
|
|
287
|
+
assert.equal(defaults.openclawReplyMetadataExtractionHintsEnabled, false);
|
|
288
|
+
assert.equal(defaults.openclawChannelEnvelopeCleaningEnabled, true);
|
|
289
|
+
|
|
290
|
+
const configured = parseConfig({
|
|
291
|
+
openclawHostEmbeddingProviderEnabled: "false",
|
|
292
|
+
openclawHostEmbeddingProviderId: "openai-compatible",
|
|
293
|
+
openclawHostEmbeddingProviderModel: "text-embedding-3-large",
|
|
294
|
+
openclawMessageReceivedCaptureEnabled: "0",
|
|
295
|
+
openclawReplyMetadataCaptureEnabled: "off",
|
|
296
|
+
openclawReplyMetadataExtractionHintsEnabled: "yes",
|
|
297
|
+
openclawChannelEnvelopeCleaningEnabled: "no",
|
|
298
|
+
});
|
|
299
|
+
assert.equal(configured.hostEmbeddingProviderEnabled, false);
|
|
300
|
+
assert.equal(configured.hostEmbeddingProviderId, "openai-compatible");
|
|
301
|
+
assert.equal(configured.hostEmbeddingProviderModel, "text-embedding-3-large");
|
|
302
|
+
assert.equal(configured.openclawMessageReceivedCaptureEnabled, false);
|
|
303
|
+
assert.equal(configured.openclawReplyMetadataCaptureEnabled, false);
|
|
304
|
+
assert.equal(configured.openclawReplyMetadataExtractionHintsEnabled, true);
|
|
305
|
+
assert.equal(configured.openclawChannelEnvelopeCleaningEnabled, false);
|
|
306
|
+
});
|
|
307
|
+
|
|
280
308
|
test("parseConfig rejects invalid qmd version and integer config values", () => {
|
|
281
309
|
assert.throws(
|
|
282
310
|
() => parseConfig({ qmdSupportedVersion: "latest" }),
|