@anuma/sdk 4.2.0 → 5.0.0
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/expo/{chunk-VBO6KSNQ.mjs → chunk-SIMGZQ4P.mjs} +480 -196
- package/dist/expo/index.cjs +417 -120
- package/dist/expo/index.d.mts +161 -26
- package/dist/expo/index.d.ts +161 -26
- package/dist/expo/index.mjs +22 -6
- package/dist/expo/{recall-OCQOA7M3.mjs → recall-PIDYDFIQ.mjs} +1 -1
- package/dist/index.cjs +31 -17
- package/dist/index.mjs +31 -17
- package/dist/react/{chunk-BS7C4CUW.mjs → chunk-KTBOHRRE.mjs} +480 -196
- package/dist/react/index.cjs +417 -120
- package/dist/react/index.d.mts +155 -26
- package/dist/react/index.d.ts +155 -26
- package/dist/react/index.mjs +22 -6
- package/dist/react/{recall-WXCZHXHZ.mjs → recall-DVX4W2UD.mjs} +1 -1
- package/dist/server/{chunk-WADHFDMA.mjs → chunk-FKVYUOSD.mjs} +480 -196
- package/dist/server/index.cjs +417 -120
- package/dist/server/index.d.mts +154 -25
- package/dist/server/index.d.ts +154 -25
- package/dist/server/index.mjs +22 -6
- package/dist/server/{recall-QTVT7HCZ.mjs → recall-74L75BJ3.mjs} +1 -1
- package/package.json +1 -1
|
@@ -13,8 +13,21 @@ import { v7 as uuidv72 } from "uuid";
|
|
|
13
13
|
|
|
14
14
|
// src/react/useEncryption.ts
|
|
15
15
|
var SIGN_MESSAGE = "The app is asking you to sign this message to generate a key, which will be used to encrypt data.";
|
|
16
|
+
var EncryptionKeyMissingError = class extends Error {
|
|
17
|
+
constructor(address, version) {
|
|
18
|
+
super(
|
|
19
|
+
`Encryption key (${version}) not found for ${address}. Please sign a message to generate your encryption key.`
|
|
20
|
+
);
|
|
21
|
+
this.name = "EncryptionKeyMissingError";
|
|
22
|
+
this.address = address;
|
|
23
|
+
this.version = version;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
16
26
|
var encryptionKeyStore = /* @__PURE__ */ new Map();
|
|
17
27
|
var pendingKeyRequests = /* @__PURE__ */ new Map();
|
|
28
|
+
var pendingCandidateDerives = /* @__PURE__ */ new Map();
|
|
29
|
+
var refreshCandidatesByAddress = /* @__PURE__ */ new Map();
|
|
30
|
+
var divergentDeriveAddresses = /* @__PURE__ */ new Set();
|
|
18
31
|
var sessionEpoch = 0;
|
|
19
32
|
var cryptoKeyCache = /* @__PURE__ */ new Map();
|
|
20
33
|
var keyAvailableCallbacks = /* @__PURE__ */ new Map();
|
|
@@ -57,14 +70,11 @@ function notifyKeyAvailable(address) {
|
|
|
57
70
|
}
|
|
58
71
|
}
|
|
59
72
|
var keyPairStore = /* @__PURE__ */ new Map();
|
|
60
|
-
function getStoredKey(address) {
|
|
61
|
-
const keys = encryptionKeyStore.get(address);
|
|
62
|
-
return keys?.current ?? null;
|
|
63
|
-
}
|
|
64
73
|
function getStoredKeyByVersion(address, version) {
|
|
65
74
|
const keys = encryptionKeyStore.get(address);
|
|
66
75
|
if (!keys) return null;
|
|
67
|
-
|
|
76
|
+
const value = version === "v2" ? keys.legacy : keys.current;
|
|
77
|
+
return value ?? null;
|
|
68
78
|
}
|
|
69
79
|
function setStoredKey(address, keys) {
|
|
70
80
|
encryptionKeyStore.set(address, keys);
|
|
@@ -73,6 +83,8 @@ function clearEncryptionKey(address) {
|
|
|
73
83
|
encryptionKeyStore.delete(address);
|
|
74
84
|
cryptoKeyCache.delete(`${address}:v2`);
|
|
75
85
|
cryptoKeyCache.delete(`${address}:v3`);
|
|
86
|
+
refreshCandidatesByAddress.delete(address);
|
|
87
|
+
divergentDeriveAddresses.delete(address);
|
|
76
88
|
}
|
|
77
89
|
function clearAllEncryptionState() {
|
|
78
90
|
sessionEpoch += 1;
|
|
@@ -80,6 +92,9 @@ function clearAllEncryptionState() {
|
|
|
80
92
|
cryptoKeyCache.clear();
|
|
81
93
|
keyAvailableCallbacks.clear();
|
|
82
94
|
pendingKeyRequests.clear();
|
|
95
|
+
pendingCandidateDerives.clear();
|
|
96
|
+
refreshCandidatesByAddress.clear();
|
|
97
|
+
divergentDeriveAddresses.clear();
|
|
83
98
|
clearAllKeyPairs();
|
|
84
99
|
for (const cb of clearAllEncryptionStateCallbacks) {
|
|
85
100
|
try {
|
|
@@ -265,9 +280,7 @@ async function getEncryptionKey(address, version = "v3") {
|
|
|
265
280
|
}
|
|
266
281
|
const keyHex = getStoredKeyByVersion(address, version);
|
|
267
282
|
if (!keyHex) {
|
|
268
|
-
throw new
|
|
269
|
-
"Encryption key not found. Please sign a message to generate your encryption key."
|
|
270
|
-
);
|
|
283
|
+
throw new EncryptionKeyMissingError(address, version);
|
|
271
284
|
}
|
|
272
285
|
const keyBytes = hexToBytes(keyHex);
|
|
273
286
|
const cryptoKey = await crypto.subtle.importKey(
|
|
@@ -293,8 +306,43 @@ async function decryptData(encryptedHex, address, version = "v3") {
|
|
|
293
306
|
const key = await getEncryptionKey(address, version);
|
|
294
307
|
return decryptDataWithKey(encryptedHex, key);
|
|
295
308
|
}
|
|
296
|
-
function hasEncryptionKey(address) {
|
|
297
|
-
return
|
|
309
|
+
function hasEncryptionKey(address, version) {
|
|
310
|
+
return getStoredKeyByVersion(address, version ?? "v3") !== null;
|
|
311
|
+
}
|
|
312
|
+
function seedEncryptionKeys(address, keys) {
|
|
313
|
+
if (!isValidWalletAddress(address)) {
|
|
314
|
+
throw new Error(
|
|
315
|
+
`Invalid wallet address: ${address}. Address must start with 0x and be 42 characters (0x + 40 hex characters).`
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
if (!keys.legacy && !keys.current) {
|
|
319
|
+
throw new Error("seedEncryptionKeys: at least one of legacy or current is required");
|
|
320
|
+
}
|
|
321
|
+
if (keys.legacy !== void 0) {
|
|
322
|
+
assertAes256KeyHex("legacy", keys.legacy);
|
|
323
|
+
}
|
|
324
|
+
if (keys.current !== void 0) {
|
|
325
|
+
assertAes256KeyHex("current", keys.current);
|
|
326
|
+
}
|
|
327
|
+
const existing = encryptionKeyStore.get(address);
|
|
328
|
+
const next = {
|
|
329
|
+
legacy: keys.legacy ?? existing?.legacy,
|
|
330
|
+
current: keys.current ?? existing?.current
|
|
331
|
+
};
|
|
332
|
+
setStoredKey(address, next);
|
|
333
|
+
if (keys.legacy !== void 0) cryptoKeyCache.delete(`${address}:v2`);
|
|
334
|
+
if (keys.current !== void 0) cryptoKeyCache.delete(`${address}:v3`);
|
|
335
|
+
refreshCandidatesByAddress.delete(address);
|
|
336
|
+
divergentDeriveAddresses.delete(address);
|
|
337
|
+
notifyKeyAvailable(address);
|
|
338
|
+
}
|
|
339
|
+
var AES_256_KEY_HEX = /^[0-9a-fA-F]{64}$/;
|
|
340
|
+
function assertAes256KeyHex(field, value) {
|
|
341
|
+
if (!AES_256_KEY_HEX.test(value)) {
|
|
342
|
+
throw new Error(
|
|
343
|
+
`seedEncryptionKeys: ${field} must be 64 hex characters (32-byte AES key), got length ${value.length}`
|
|
344
|
+
);
|
|
345
|
+
}
|
|
298
346
|
}
|
|
299
347
|
async function encryptBytesWithKey(plaintext, key) {
|
|
300
348
|
const plaintextBytes = typeof plaintext === "string" ? SHARED_TEXT_ENCODER.encode(plaintext) : plaintext;
|
|
@@ -330,20 +378,23 @@ async function decryptDataWithKey(encryptedHex, key) {
|
|
|
330
378
|
);
|
|
331
379
|
return SHARED_TEXT_DECODER.decode(decryptedData);
|
|
332
380
|
}
|
|
333
|
-
async function requestEncryptionKey(walletAddress, signMessage, embeddedWalletSigner) {
|
|
381
|
+
async function requestEncryptionKey(walletAddress, signMessage, embeddedWalletSigner, options) {
|
|
334
382
|
if (!isValidWalletAddress(walletAddress)) {
|
|
335
383
|
throw new Error(
|
|
336
384
|
`Invalid wallet address: ${walletAddress}. Address must start with 0x and be 42 characters (0x + 40 hex characters).`
|
|
337
385
|
);
|
|
338
386
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
return;
|
|
387
|
+
if (!options?.force && getStoredKeyByVersion(walletAddress, "v3") && getStoredKeyByVersion(walletAddress, "v2")) {
|
|
388
|
+
return true;
|
|
342
389
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
390
|
+
if (!options?.force && divergentDeriveAddresses.has(walletAddress)) {
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
if (!options?.force) {
|
|
394
|
+
const pending = pendingKeyRequests.get(walletAddress);
|
|
395
|
+
if (pending) {
|
|
396
|
+
return pending;
|
|
397
|
+
}
|
|
347
398
|
}
|
|
348
399
|
const startEpoch = sessionEpoch;
|
|
349
400
|
const promise = (async () => {
|
|
@@ -371,20 +422,143 @@ async function requestEncryptionKey(walletAddress, signMessage, embeddedWalletSi
|
|
|
371
422
|
deriveKeyFromSignatureV3(signature)
|
|
372
423
|
]);
|
|
373
424
|
if (sessionEpoch !== startEpoch) {
|
|
374
|
-
return;
|
|
425
|
+
return false;
|
|
426
|
+
}
|
|
427
|
+
const existing = encryptionKeyStore.get(walletAddress);
|
|
428
|
+
if (!options?.force && existing && (existing.legacy || existing.current)) {
|
|
429
|
+
const matchesCurrent = Boolean(existing.current && existing.current === currentKey);
|
|
430
|
+
const matchesLegacy = Boolean(existing.legacy && existing.legacy === legacyKey);
|
|
431
|
+
if (!matchesCurrent && !matchesLegacy) {
|
|
432
|
+
getLogger().warn(
|
|
433
|
+
"requestEncryptionKey: derived keys do not match existing store; leaving unchanged"
|
|
434
|
+
);
|
|
435
|
+
divergentDeriveAddresses.add(walletAddress);
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
setStoredKey(walletAddress, {
|
|
439
|
+
legacy: existing.legacy ?? legacyKey,
|
|
440
|
+
current: existing.current ?? currentKey
|
|
441
|
+
});
|
|
442
|
+
} else {
|
|
443
|
+
setStoredKey(walletAddress, { legacy: legacyKey, current: currentKey });
|
|
375
444
|
}
|
|
376
|
-
|
|
445
|
+
cryptoKeyCache.delete(`${walletAddress}:v2`);
|
|
446
|
+
cryptoKeyCache.delete(`${walletAddress}:v3`);
|
|
447
|
+
refreshCandidatesByAddress.delete(walletAddress);
|
|
448
|
+
divergentDeriveAddresses.delete(walletAddress);
|
|
377
449
|
notifyKeyAvailable(walletAddress);
|
|
450
|
+
return true;
|
|
378
451
|
})();
|
|
379
452
|
pendingKeyRequests.set(walletAddress, promise);
|
|
380
453
|
try {
|
|
381
|
-
await promise;
|
|
454
|
+
return await promise;
|
|
382
455
|
} finally {
|
|
383
456
|
if (pendingKeyRequests.get(walletAddress) === promise) {
|
|
384
457
|
pendingKeyRequests.delete(walletAddress);
|
|
385
458
|
}
|
|
386
459
|
}
|
|
387
460
|
}
|
|
461
|
+
async function refreshEncryptionKeyIfMatches(walletAddress, probeCiphertext, signMessage, embeddedWalletSigner) {
|
|
462
|
+
if (!isValidWalletAddress(walletAddress)) {
|
|
463
|
+
throw new Error(
|
|
464
|
+
`Invalid wallet address: ${walletAddress}. Address must start with 0x and be 42 characters (0x + 40 hex characters).`
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
const parsed = parsePrefixedCiphertext(probeCiphertext);
|
|
468
|
+
if (!parsed) {
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
const cached = refreshCandidatesByAddress.get(walletAddress);
|
|
472
|
+
if (cached) {
|
|
473
|
+
return tryCommitCandidatesIfProbeMatches(walletAddress, parsed, cached);
|
|
474
|
+
}
|
|
475
|
+
let derivePromise = pendingCandidateDerives.get(walletAddress);
|
|
476
|
+
if (!derivePromise) {
|
|
477
|
+
derivePromise = (async () => {
|
|
478
|
+
const candidates2 = await deriveRefreshCandidates(signMessage, embeddedWalletSigner);
|
|
479
|
+
if (candidates2) {
|
|
480
|
+
refreshCandidatesByAddress.set(walletAddress, candidates2);
|
|
481
|
+
}
|
|
482
|
+
return candidates2;
|
|
483
|
+
})().finally(() => {
|
|
484
|
+
if (pendingCandidateDerives.get(walletAddress) === derivePromise) {
|
|
485
|
+
pendingCandidateDerives.delete(walletAddress);
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
pendingCandidateDerives.set(walletAddress, derivePromise);
|
|
489
|
+
}
|
|
490
|
+
const candidates = await derivePromise;
|
|
491
|
+
if (!candidates) {
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
return tryCommitCandidatesIfProbeMatches(walletAddress, parsed, candidates);
|
|
495
|
+
}
|
|
496
|
+
function parsePrefixedCiphertext(probeCiphertext) {
|
|
497
|
+
const version = probeCiphertext.startsWith("enc:v3:") ? "v3" : probeCiphertext.startsWith("enc:v2:") ? "v2" : null;
|
|
498
|
+
if (!version) return null;
|
|
499
|
+
const encryptedData = probeCiphertext.slice(
|
|
500
|
+
version === "v3" ? "enc:v3:".length : "enc:v2:".length
|
|
501
|
+
);
|
|
502
|
+
if (encryptedData.length < 56 || !/^[0-9a-f]+$/i.test(encryptedData)) {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
return { version, encryptedData };
|
|
506
|
+
}
|
|
507
|
+
async function deriveRefreshCandidates(signMessage, embeddedWalletSigner) {
|
|
508
|
+
const startEpoch = sessionEpoch;
|
|
509
|
+
const signOptions = { showWalletUIs: false };
|
|
510
|
+
let signature;
|
|
511
|
+
try {
|
|
512
|
+
if (embeddedWalletSigner) {
|
|
513
|
+
signature = await embeddedWalletSigner(SIGN_MESSAGE, signOptions);
|
|
514
|
+
} else {
|
|
515
|
+
signature = await signMessage(SIGN_MESSAGE, signOptions);
|
|
516
|
+
}
|
|
517
|
+
} catch (error) {
|
|
518
|
+
if (embeddedWalletSigner && error instanceof Error) {
|
|
519
|
+
getLogger().warn(
|
|
520
|
+
"Embedded wallet signing failed during key refresh, falling back to standard signMessage:",
|
|
521
|
+
error.message
|
|
522
|
+
);
|
|
523
|
+
signature = await signMessage(SIGN_MESSAGE, signOptions);
|
|
524
|
+
} else {
|
|
525
|
+
throw error;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
const [legacyKey, currentKey] = await Promise.all([
|
|
529
|
+
deriveKeyFromSignature(signature),
|
|
530
|
+
deriveKeyFromSignatureV3(signature)
|
|
531
|
+
]);
|
|
532
|
+
if (sessionEpoch !== startEpoch) {
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
return { legacy: legacyKey, current: currentKey };
|
|
536
|
+
}
|
|
537
|
+
async function tryCommitCandidatesIfProbeMatches(walletAddress, parsed, candidates) {
|
|
538
|
+
const candidateHex = parsed.version === "v2" ? candidates.legacy : candidates.current;
|
|
539
|
+
const keyBytes = hexToBytes(candidateHex);
|
|
540
|
+
const candidateCryptoKey = await crypto.subtle.importKey(
|
|
541
|
+
"raw",
|
|
542
|
+
keyBytes.buffer,
|
|
543
|
+
{ name: "AES-GCM" },
|
|
544
|
+
false,
|
|
545
|
+
["encrypt", "decrypt"]
|
|
546
|
+
);
|
|
547
|
+
try {
|
|
548
|
+
await decryptDataWithKey(parsed.encryptedData, candidateCryptoKey);
|
|
549
|
+
} catch {
|
|
550
|
+
getLogger().warn(
|
|
551
|
+
"refreshEncryptionKeyIfMatches: candidate keys failed to decrypt probe; leaving store unchanged"
|
|
552
|
+
);
|
|
553
|
+
return false;
|
|
554
|
+
}
|
|
555
|
+
setStoredKey(walletAddress, { legacy: candidates.legacy, current: candidates.current });
|
|
556
|
+
cryptoKeyCache.delete(`${walletAddress}:v2`);
|
|
557
|
+
cryptoKeyCache.delete(`${walletAddress}:v3`);
|
|
558
|
+
divergentDeriveAddresses.delete(walletAddress);
|
|
559
|
+
notifyKeyAvailable(walletAddress);
|
|
560
|
+
return true;
|
|
561
|
+
}
|
|
388
562
|
var KEYPAIR_STORAGE_PREFIX = "ecdh_keypair_";
|
|
389
563
|
async function persistKeyPair(address, startEpoch) {
|
|
390
564
|
if (typeof window === "undefined") {
|
|
@@ -664,20 +838,29 @@ async function encryptField(value, address, signMessage, embeddedWalletSigner, s
|
|
|
664
838
|
const encrypted = await encryptData(value, address);
|
|
665
839
|
return `${ENCRYPTION_PREFIX}${encrypted}`;
|
|
666
840
|
}
|
|
667
|
-
async function
|
|
668
|
-
if (!value) return value;
|
|
841
|
+
async function decryptFieldDetailed(value, address) {
|
|
842
|
+
if (!value) return { status: "plaintext", value };
|
|
669
843
|
const detected = detectEncryptionVersion(value);
|
|
670
|
-
if (!detected) return value;
|
|
844
|
+
if (!detected) return { status: "plaintext", value };
|
|
671
845
|
if (detected.encryptedData.length < 56 || !/^[0-9a-f]+$/i.test(detected.encryptedData)) {
|
|
672
|
-
return value;
|
|
846
|
+
return { status: "invalid_payload", value, version: detected.version };
|
|
673
847
|
}
|
|
674
848
|
try {
|
|
675
|
-
|
|
849
|
+
const plaintext = await decryptData(detected.encryptedData, address, detected.version);
|
|
850
|
+
return { status: "ok", value: plaintext, version: detected.version };
|
|
676
851
|
} catch (error) {
|
|
677
|
-
|
|
678
|
-
|
|
852
|
+
if (error instanceof EncryptionKeyMissingError) {
|
|
853
|
+
getLogger().warn("Failed to decrypt field (key missing for version):", error.message);
|
|
854
|
+
return { status: "key_missing", value, version: detected.version };
|
|
855
|
+
}
|
|
856
|
+
getLogger().warn("Failed to decrypt field (auth mismatch or crypto error):", error);
|
|
857
|
+
return { status: "auth_mismatch", value, version: detected.version };
|
|
679
858
|
}
|
|
680
859
|
}
|
|
860
|
+
async function decryptField(value, address) {
|
|
861
|
+
const result = await decryptFieldDetailed(value, address);
|
|
862
|
+
return result.value;
|
|
863
|
+
}
|
|
681
864
|
async function encryptJsonField(value, address, signMessage, embeddedWalletSigner, skipKeyRequest) {
|
|
682
865
|
if (!value) return void 0;
|
|
683
866
|
const jsonString = JSON.stringify(value);
|
|
@@ -777,20 +960,40 @@ async function encryptMessageFields(message, address, signMessage, embeddedWalle
|
|
|
777
960
|
throw error;
|
|
778
961
|
}
|
|
779
962
|
}
|
|
780
|
-
async function
|
|
781
|
-
if (value === void 0) return void 0;
|
|
782
|
-
if (value === null) return value;
|
|
963
|
+
async function decryptMaybeJsonFieldDetailed(value, address) {
|
|
964
|
+
if (value === void 0) return { value: void 0, status: "plaintext" };
|
|
965
|
+
if (value === null) return { value, status: "plaintext" };
|
|
783
966
|
if (typeof value === "string") {
|
|
784
967
|
if (isEncrypted(value)) {
|
|
785
|
-
|
|
968
|
+
const detailed = await decryptFieldDetailed(value, address);
|
|
969
|
+
if (detailed.status !== "ok") {
|
|
970
|
+
return { value: void 0, status: detailed.status, probe: value };
|
|
971
|
+
}
|
|
972
|
+
try {
|
|
973
|
+
return { value: JSON.parse(detailed.value), status: "ok" };
|
|
974
|
+
} catch (error) {
|
|
975
|
+
getLogger().warn("Failed to parse decrypted JSON field:", error);
|
|
976
|
+
return { value: void 0, status: "invalid_payload", probe: value };
|
|
977
|
+
}
|
|
786
978
|
}
|
|
787
979
|
try {
|
|
788
|
-
return JSON.parse(value);
|
|
980
|
+
return { value: JSON.parse(value), status: "plaintext" };
|
|
789
981
|
} catch {
|
|
790
|
-
return void 0;
|
|
982
|
+
return { value: void 0, status: "plaintext" };
|
|
791
983
|
}
|
|
792
984
|
}
|
|
793
|
-
return value;
|
|
985
|
+
return { value, status: "plaintext" };
|
|
986
|
+
}
|
|
987
|
+
function worseDecryptStatus(a, b) {
|
|
988
|
+
if (b === "ok" || b === "plaintext") return a;
|
|
989
|
+
if (!a || a === "ok" || a === "plaintext") return b;
|
|
990
|
+
const rank = (s) => {
|
|
991
|
+
if (s === "auth_mismatch") return 3;
|
|
992
|
+
if (s === "key_missing") return 2;
|
|
993
|
+
if (s === "invalid_payload") return 1;
|
|
994
|
+
return 0;
|
|
995
|
+
};
|
|
996
|
+
return rank(b) >= rank(a) ? b : a;
|
|
794
997
|
}
|
|
795
998
|
async function decryptMessageFields(message, address, signMessage, embeddedWalletSigner) {
|
|
796
999
|
if (!address) {
|
|
@@ -803,33 +1006,82 @@ async function decryptMessageFields(message, address, signMessage, embeddedWalle
|
|
|
803
1006
|
getLogger().warn("Failed to request encryption key for decryption:", error);
|
|
804
1007
|
}
|
|
805
1008
|
}
|
|
806
|
-
|
|
807
|
-
const
|
|
808
|
-
|
|
1009
|
+
let assembled = await assembleDecryptedMessage(message, address);
|
|
1010
|
+
const needsRefresh = assembled.decryptionStatus === "auth_mismatch" || assembled.decryptionStatus === "key_missing";
|
|
1011
|
+
if (needsRefresh && signMessage && assembled.refreshProbe) {
|
|
1012
|
+
try {
|
|
1013
|
+
const refreshed = await refreshEncryptionKeyIfMatches(
|
|
1014
|
+
address,
|
|
1015
|
+
assembled.refreshProbe,
|
|
1016
|
+
signMessage,
|
|
1017
|
+
embeddedWalletSigner
|
|
1018
|
+
);
|
|
1019
|
+
if (refreshed) {
|
|
1020
|
+
assembled = await assembleDecryptedMessage(message, address);
|
|
1021
|
+
}
|
|
1022
|
+
} catch (error) {
|
|
1023
|
+
getLogger().warn("Failed to refresh encryption key after decrypt failure:", error);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
const { refreshProbe: _probe, ...result } = assembled;
|
|
1027
|
+
return result;
|
|
1028
|
+
}
|
|
1029
|
+
async function assembleDecryptedMessage(message, address) {
|
|
1030
|
+
const contentResult = await decryptFieldDetailed(message.content, address);
|
|
1031
|
+
const thinkingResult = message.thinking ? await decryptFieldDetailed(message.thinking, address) : null;
|
|
1032
|
+
const vectorResult = await decryptMaybeJsonFieldDetailed(
|
|
809
1033
|
message.vector,
|
|
810
1034
|
address
|
|
811
1035
|
);
|
|
812
|
-
const
|
|
1036
|
+
const chunksResult = await decryptMaybeJsonFieldDetailed(
|
|
813
1037
|
message.chunks,
|
|
814
1038
|
address
|
|
815
1039
|
);
|
|
816
|
-
const
|
|
1040
|
+
const sourcesResult = await decryptMaybeJsonFieldDetailed(
|
|
817
1041
|
message.sources,
|
|
818
1042
|
address
|
|
819
1043
|
);
|
|
820
|
-
const
|
|
1044
|
+
const thoughtProcessResult = await decryptMaybeJsonFieldDetailed(
|
|
821
1045
|
message.thoughtProcess,
|
|
822
1046
|
address
|
|
823
1047
|
);
|
|
824
|
-
|
|
1048
|
+
let decryptionStatus;
|
|
1049
|
+
let refreshProbe;
|
|
1050
|
+
const consider = (status, probe) => {
|
|
1051
|
+
decryptionStatus = worseDecryptStatus(decryptionStatus, status);
|
|
1052
|
+
if (!refreshProbe && probe && (status === "auth_mismatch" || status === "key_missing")) {
|
|
1053
|
+
refreshProbe = probe;
|
|
1054
|
+
}
|
|
1055
|
+
};
|
|
1056
|
+
consider(contentResult.status, isEncrypted(message.content) ? message.content : void 0);
|
|
1057
|
+
if (thinkingResult && message.thinking && typeof message.thinking === "string") {
|
|
1058
|
+
consider(thinkingResult.status, isEncrypted(message.thinking) ? message.thinking : void 0);
|
|
1059
|
+
}
|
|
1060
|
+
if (vectorResult.probe) consider(vectorResult.status, vectorResult.probe);
|
|
1061
|
+
if (chunksResult.probe) consider(chunksResult.status, chunksResult.probe);
|
|
1062
|
+
if (sourcesResult.probe) consider(sourcesResult.status, sourcesResult.probe);
|
|
1063
|
+
if (thoughtProcessResult.probe) {
|
|
1064
|
+
consider(thoughtProcessResult.status, thoughtProcessResult.probe);
|
|
1065
|
+
}
|
|
1066
|
+
const statusForConsumer = decryptionStatus === "ok" || decryptionStatus === "plaintext" || !decryptionStatus ? void 0 : decryptionStatus;
|
|
1067
|
+
const result = {
|
|
825
1068
|
...message,
|
|
826
|
-
content:
|
|
827
|
-
thinking:
|
|
828
|
-
vector:
|
|
829
|
-
chunks:
|
|
830
|
-
sources:
|
|
831
|
-
thoughtProcess:
|
|
1069
|
+
content: contentResult.value,
|
|
1070
|
+
thinking: thinkingResult ? thinkingResult.value : message.thinking,
|
|
1071
|
+
vector: vectorResult.value,
|
|
1072
|
+
chunks: chunksResult.value,
|
|
1073
|
+
sources: sourcesResult.value,
|
|
1074
|
+
thoughtProcess: thoughtProcessResult.value
|
|
832
1075
|
};
|
|
1076
|
+
if (statusForConsumer) {
|
|
1077
|
+
result.decryptionStatus = statusForConsumer;
|
|
1078
|
+
} else {
|
|
1079
|
+
delete result.decryptionStatus;
|
|
1080
|
+
}
|
|
1081
|
+
if (refreshProbe) {
|
|
1082
|
+
result.refreshProbe = refreshProbe;
|
|
1083
|
+
}
|
|
1084
|
+
return result;
|
|
833
1085
|
}
|
|
834
1086
|
|
|
835
1087
|
// src/lib/db/chat/types.ts
|
|
@@ -4496,6 +4748,107 @@ async function chunkAndEmbedAllMessages(ctx, options, filter) {
|
|
|
4496
4748
|
return embeddedCount;
|
|
4497
4749
|
}
|
|
4498
4750
|
|
|
4751
|
+
// src/lib/memoryVault/decomposeQuery.ts
|
|
4752
|
+
var DEFAULT_MODEL = "inclusionai/ling-2.6-flash";
|
|
4753
|
+
var MAX_SUB_QUERIES = 5;
|
|
4754
|
+
var SYSTEM_PROMPT = `You classify a memory query and, if needed, decompose it into concrete sub-queries.
|
|
4755
|
+
|
|
4756
|
+
CLASSIFICATION RULES \u2014 be strict:
|
|
4757
|
+
- "specific": asks for one fact OR a list of items in ONE category (one dimension).
|
|
4758
|
+
Examples: a name, a date, a preference, a location, an ID, a value, "what hobbies",
|
|
4759
|
+
"which conferences", "what open source projects", "what does X do for exercise".
|
|
4760
|
+
Even when the answer is a list, if all items belong to one dimension, it is specific.
|
|
4761
|
+
- "composite": asks about a person, system, or setup MULTI-DIMENSIONALLY \u2014 a summary,
|
|
4762
|
+
an overview, a profile, "tell me about X", "what is my X setup", "describe the team's
|
|
4763
|
+
X". The answer must span multiple distinct fact dimensions (e.g. for a "tech stack":
|
|
4764
|
+
language + database + framework + tools \u2014 four different dimensions).
|
|
4765
|
+
|
|
4766
|
+
When in doubt, prefer "specific". Mis-classifying a single-dimension list as composite
|
|
4767
|
+
hurts retrieval more than the reverse.
|
|
4768
|
+
|
|
4769
|
+
SUB-QUERY RULES (composite only, 3\u20135 questions):
|
|
4770
|
+
- Each sub-query targets a DIFFERENT fact dimension.
|
|
4771
|
+
- Use SHORT parenthetical examples \u2014 2\u20134 concrete options that anchor the
|
|
4772
|
+
embedding to likely memory content. Examples must be terms a user would
|
|
4773
|
+
actually have in their notes (e.g., "Postgres" not "PostgreSQL 15.2 with
|
|
4774
|
+
read replicas"). No verbose qualifiers like "and versions" or "and any
|
|
4775
|
+
relevant configuration details".
|
|
4776
|
+
- 8\u201314 words including the parenthetical. Like a human asking a follow-up.
|
|
4777
|
+
|
|
4778
|
+
Examples:
|
|
4779
|
+
"What is my dog's name?" \u2192 {"mode":"specific","subQueries":["What is my dog's name?"]}
|
|
4780
|
+
"What does the user do for exercise?" \u2192 {"mode":"specific","subQueries":["What does the user do for exercise?"]}
|
|
4781
|
+
"What conferences has the user attended?" \u2192 {"mode":"specific","subQueries":["What conferences has the user attended?"]}
|
|
4782
|
+
"What did I do last Tuesday?" \u2192 {"mode":"specific","subQueries":["What did I do last Tuesday?"]}
|
|
4783
|
+
"Tell me about the user as a person" \u2192 {"mode":"composite","subQueries":["What is the user's name?","Where does the user live?","What does the user do for work?","Any health conditions or allergies?","What are the user's hobbies (sports, music, games)?"]}
|
|
4784
|
+
"What is the user's tech stack?" \u2192 {"mode":"composite","subQueries":["What programming languages does the user use (Python, Go, TypeScript)?","What database does the user use (Postgres, MySQL, Mongo)?","What framework or ORM (React, Django, Prisma)?","What infrastructure (AWS, Docker, K8s)?","What testing tools (Jest, pytest, Cypress)?"]}
|
|
4785
|
+
"Describe the user's development environment" \u2192 {"mode":"composite","subQueries":["What operating system does the user use (macOS, Linux, Windows)?","What editor or IDE (VS Code, Vim, IntelliJ)?","What shell or terminal setup (zsh, bash, tmux)?","What keyboard or display setup (mechanical, vertical monitor)?","Any UI preferences (dark mode, font, vim keybindings)?"]}
|
|
4786
|
+
|
|
4787
|
+
Output strict JSON. No prose.`;
|
|
4788
|
+
async function decomposeQuery(query, options) {
|
|
4789
|
+
const fallback = { mode: "specific", subQueries: [query] };
|
|
4790
|
+
const trimmed = query.trim();
|
|
4791
|
+
if (trimmed.length === 0) return fallback;
|
|
4792
|
+
let parsed;
|
|
4793
|
+
try {
|
|
4794
|
+
parsed = await callPortalJsonCompletion({
|
|
4795
|
+
...options.apiKey !== void 0 && { apiKey: options.apiKey },
|
|
4796
|
+
...options.getToken !== void 0 && { getToken: options.getToken },
|
|
4797
|
+
...options.baseUrl !== void 0 && { baseUrl: options.baseUrl },
|
|
4798
|
+
model: options.model ?? DEFAULT_MODEL,
|
|
4799
|
+
systemPrompt: SYSTEM_PROMPT,
|
|
4800
|
+
// Wrap the bare query in a task framing so the model treats it as
|
|
4801
|
+
// input to classify, not as a question to answer conversationally.
|
|
4802
|
+
// Bare-query inputs caused Anthropic models to respond with prose
|
|
4803
|
+
// like "Do you mean...?" before this wrap.
|
|
4804
|
+
userMessage: `Classify the following memory query and decompose if composite. Respond with JSON only \u2014 do not answer the question, do not ask for clarification.
|
|
4805
|
+
|
|
4806
|
+
Query: ${trimmed}`,
|
|
4807
|
+
// Decompose runs ahead of recall in the tool layer — tighter than the
|
|
4808
|
+
// portalLlm default (consolidate/extract can wait longer).
|
|
4809
|
+
timeoutMs: 2e4,
|
|
4810
|
+
// No internal retry: a failure degrades to the safe `{specific,[query]}`
|
|
4811
|
+
// fallback below. The agent/tool loop is the retry surface (719/B4).
|
|
4812
|
+
maxAttempts: 1,
|
|
4813
|
+
tag: "memory/decompose",
|
|
4814
|
+
...options.fetchFn && { fetchFn: options.fetchFn }
|
|
4815
|
+
});
|
|
4816
|
+
} catch (err) {
|
|
4817
|
+
getLogger().warn("memoryVault/decompose: portal call failed, falling back to specific", err);
|
|
4818
|
+
return fallback;
|
|
4819
|
+
}
|
|
4820
|
+
if (parsed === null) return fallback;
|
|
4821
|
+
return validate(parsed, trimmed) ?? fallback;
|
|
4822
|
+
}
|
|
4823
|
+
function normalizeSubQueries(raw) {
|
|
4824
|
+
if (!raw || raw.length === 0) return [];
|
|
4825
|
+
const seen = /* @__PURE__ */ new Set();
|
|
4826
|
+
const out = [];
|
|
4827
|
+
for (const s of raw) {
|
|
4828
|
+
if (typeof s !== "string") continue;
|
|
4829
|
+
const t = s.trim();
|
|
4830
|
+
if (t.length === 0) continue;
|
|
4831
|
+
const key = t.toLowerCase();
|
|
4832
|
+
if (seen.has(key)) continue;
|
|
4833
|
+
seen.add(key);
|
|
4834
|
+
out.push(t);
|
|
4835
|
+
if (out.length >= MAX_SUB_QUERIES) break;
|
|
4836
|
+
}
|
|
4837
|
+
return out;
|
|
4838
|
+
}
|
|
4839
|
+
function validate(parsed, originalQuery) {
|
|
4840
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
4841
|
+
const obj = parsed;
|
|
4842
|
+
const mode = obj.mode === "composite" ? "composite" : "specific";
|
|
4843
|
+
if (!Array.isArray(obj.subQueries)) return null;
|
|
4844
|
+
const subQueries = normalizeSubQueries(obj.subQueries);
|
|
4845
|
+
if (subQueries.length === 0) return null;
|
|
4846
|
+
if (mode === "specific") {
|
|
4847
|
+
return { mode: "specific", subQueries: [originalQuery] };
|
|
4848
|
+
}
|
|
4849
|
+
return { mode, subQueries };
|
|
4850
|
+
}
|
|
4851
|
+
|
|
4499
4852
|
// src/lib/memory/mmr.ts
|
|
4500
4853
|
function applyMMR(candidates, k, lambda = 0.5) {
|
|
4501
4854
|
if (candidates.length === 0 || k <= 0) return [];
|
|
@@ -4873,91 +5226,6 @@ function scoreBM25(query, items) {
|
|
|
4873
5226
|
return scoreBM25Prepared(query, prepareBM25Corpus(items));
|
|
4874
5227
|
}
|
|
4875
5228
|
|
|
4876
|
-
// src/lib/memoryVault/decomposeQuery.ts
|
|
4877
|
-
var DEFAULT_MODEL = "inclusionai/ling-2.6-flash";
|
|
4878
|
-
var MAX_SUB_QUERIES = 5;
|
|
4879
|
-
var SYSTEM_PROMPT = `You classify a memory query and, if needed, decompose it into concrete sub-queries.
|
|
4880
|
-
|
|
4881
|
-
CLASSIFICATION RULES \u2014 be strict:
|
|
4882
|
-
- "specific": asks for one fact OR a list of items in ONE category (one dimension).
|
|
4883
|
-
Examples: a name, a date, a preference, a location, an ID, a value, "what hobbies",
|
|
4884
|
-
"which conferences", "what open source projects", "what does X do for exercise".
|
|
4885
|
-
Even when the answer is a list, if all items belong to one dimension, it is specific.
|
|
4886
|
-
- "composite": asks about a person, system, or setup MULTI-DIMENSIONALLY \u2014 a summary,
|
|
4887
|
-
an overview, a profile, "tell me about X", "what is my X setup", "describe the team's
|
|
4888
|
-
X". The answer must span multiple distinct fact dimensions (e.g. for a "tech stack":
|
|
4889
|
-
language + database + framework + tools \u2014 four different dimensions).
|
|
4890
|
-
|
|
4891
|
-
When in doubt, prefer "specific". Mis-classifying a single-dimension list as composite
|
|
4892
|
-
hurts retrieval more than the reverse.
|
|
4893
|
-
|
|
4894
|
-
SUB-QUERY RULES (composite only, 3\u20135 questions):
|
|
4895
|
-
- Each sub-query targets a DIFFERENT fact dimension.
|
|
4896
|
-
- Use SHORT parenthetical examples \u2014 2\u20134 concrete options that anchor the
|
|
4897
|
-
embedding to likely memory content. Examples must be terms a user would
|
|
4898
|
-
actually have in their notes (e.g., "Postgres" not "PostgreSQL 15.2 with
|
|
4899
|
-
read replicas"). No verbose qualifiers like "and versions" or "and any
|
|
4900
|
-
relevant configuration details".
|
|
4901
|
-
- 8\u201314 words including the parenthetical. Like a human asking a follow-up.
|
|
4902
|
-
|
|
4903
|
-
Examples:
|
|
4904
|
-
"What is my dog's name?" \u2192 {"mode":"specific","subQueries":["What is my dog's name?"]}
|
|
4905
|
-
"What does the user do for exercise?" \u2192 {"mode":"specific","subQueries":["What does the user do for exercise?"]}
|
|
4906
|
-
"What conferences has the user attended?" \u2192 {"mode":"specific","subQueries":["What conferences has the user attended?"]}
|
|
4907
|
-
"What did I do last Tuesday?" \u2192 {"mode":"specific","subQueries":["What did I do last Tuesday?"]}
|
|
4908
|
-
"Tell me about the user as a person" \u2192 {"mode":"composite","subQueries":["What is the user's name?","Where does the user live?","What does the user do for work?","Any health conditions or allergies?","What are the user's hobbies (sports, music, games)?"]}
|
|
4909
|
-
"What is the user's tech stack?" \u2192 {"mode":"composite","subQueries":["What programming languages does the user use (Python, Go, TypeScript)?","What database does the user use (Postgres, MySQL, Mongo)?","What framework or ORM (React, Django, Prisma)?","What infrastructure (AWS, Docker, K8s)?","What testing tools (Jest, pytest, Cypress)?"]}
|
|
4910
|
-
"Describe the user's development environment" \u2192 {"mode":"composite","subQueries":["What operating system does the user use (macOS, Linux, Windows)?","What editor or IDE (VS Code, Vim, IntelliJ)?","What shell or terminal setup (zsh, bash, tmux)?","What keyboard or display setup (mechanical, vertical monitor)?","Any UI preferences (dark mode, font, vim keybindings)?"]}
|
|
4911
|
-
|
|
4912
|
-
Output strict JSON. No prose.`;
|
|
4913
|
-
async function decomposeQuery(query, options) {
|
|
4914
|
-
const fallback = { mode: "specific", subQueries: [query] };
|
|
4915
|
-
const trimmed = query.trim();
|
|
4916
|
-
if (trimmed.length === 0) return fallback;
|
|
4917
|
-
let parsed;
|
|
4918
|
-
try {
|
|
4919
|
-
parsed = await callPortalJsonCompletion({
|
|
4920
|
-
...options.apiKey !== void 0 && { apiKey: options.apiKey },
|
|
4921
|
-
...options.getToken !== void 0 && { getToken: options.getToken },
|
|
4922
|
-
...options.baseUrl !== void 0 && { baseUrl: options.baseUrl },
|
|
4923
|
-
model: options.model ?? DEFAULT_MODEL,
|
|
4924
|
-
systemPrompt: SYSTEM_PROMPT,
|
|
4925
|
-
// Wrap the bare query in a task framing so the model treats it as
|
|
4926
|
-
// input to classify, not as a question to answer conversationally.
|
|
4927
|
-
// Bare-query inputs caused Anthropic models to respond with prose
|
|
4928
|
-
// like "Do you mean...?" before this wrap.
|
|
4929
|
-
userMessage: `Classify the following memory query and decompose if composite. Respond with JSON only \u2014 do not answer the question, do not ask for clarification.
|
|
4930
|
-
|
|
4931
|
-
Query: ${trimmed}`,
|
|
4932
|
-
// Decompose runs on the recall hot path — tighter than the
|
|
4933
|
-
// portalLlm default (consolidate/extract can wait longer).
|
|
4934
|
-
timeoutMs: 2e4,
|
|
4935
|
-
// No internal retry: a failure degrades to the safe `{specific,[query]}`
|
|
4936
|
-
// fallback below, so retrying would only add latency on the hot path.
|
|
4937
|
-
maxAttempts: 1,
|
|
4938
|
-
tag: "memory/decompose",
|
|
4939
|
-
...options.fetchFn && { fetchFn: options.fetchFn }
|
|
4940
|
-
});
|
|
4941
|
-
} catch (err) {
|
|
4942
|
-
getLogger().warn("memoryVault/decompose: portal call failed, falling back to specific", err);
|
|
4943
|
-
return fallback;
|
|
4944
|
-
}
|
|
4945
|
-
if (parsed === null) return fallback;
|
|
4946
|
-
return validate(parsed, trimmed) ?? fallback;
|
|
4947
|
-
}
|
|
4948
|
-
function validate(parsed, originalQuery) {
|
|
4949
|
-
if (typeof parsed !== "object" || parsed === null) return null;
|
|
4950
|
-
const obj = parsed;
|
|
4951
|
-
const mode = obj.mode === "composite" ? "composite" : "specific";
|
|
4952
|
-
if (!Array.isArray(obj.subQueries)) return null;
|
|
4953
|
-
const subQueries = obj.subQueries.filter((s) => typeof s === "string").map((s) => s.trim()).filter((s) => s.length > 0).slice(0, MAX_SUB_QUERIES);
|
|
4954
|
-
if (subQueries.length === 0) return null;
|
|
4955
|
-
if (mode === "specific") {
|
|
4956
|
-
return { mode: "specific", subQueries: [originalQuery] };
|
|
4957
|
-
}
|
|
4958
|
-
return { mode, subQueries };
|
|
4959
|
-
}
|
|
4960
|
-
|
|
4961
5229
|
// src/lib/memoryVault/lruCache.ts
|
|
4962
5230
|
var DEFAULT_VAULT_CACHE_SIZE = 5e3;
|
|
4963
5231
|
var LRUCache = class extends Map {
|
|
@@ -4991,6 +5259,7 @@ function createVaultEmbeddingCache(maxSize = DEFAULT_VAULT_CACHE_SIZE) {
|
|
|
4991
5259
|
}
|
|
4992
5260
|
|
|
4993
5261
|
// src/lib/memoryVault/searchTool.ts
|
|
5262
|
+
var warnedProgrammaticDecomposeIgnored = false;
|
|
4994
5263
|
function normalizeEventTimeKind(kind) {
|
|
4995
5264
|
return kind === "point" || kind === "range" || kind === "ongoing" ? kind : null;
|
|
4996
5265
|
}
|
|
@@ -5925,52 +6194,51 @@ async function rankPreparedVaultCandidates(query, prepared, embeddingOptions, se
|
|
|
5925
6194
|
...searchOptions?.rrfK !== void 0 && { rrfK: searchOptions.rrfK },
|
|
5926
6195
|
...searchOptions?.factTypeWeights && { factTypeWeights: searchOptions.factTypeWeights }
|
|
5927
6196
|
};
|
|
5928
|
-
|
|
5929
|
-
|
|
5930
|
-
|
|
5931
|
-
|
|
5932
|
-
|
|
5933
|
-
|
|
5934
|
-
|
|
5935
|
-
|
|
5936
|
-
|
|
5937
|
-
|
|
5938
|
-
|
|
5939
|
-
|
|
5940
|
-
|
|
5941
|
-
|
|
5942
|
-
|
|
5943
|
-
|
|
5944
|
-
|
|
5945
|
-
|
|
5946
|
-
|
|
5947
|
-
|
|
5948
|
-
|
|
5949
|
-
|
|
5950
|
-
|
|
5951
|
-
|
|
5952
|
-
|
|
5953
|
-
|
|
5954
|
-
|
|
5955
|
-
|
|
5956
|
-
|
|
5957
|
-
|
|
5958
|
-
|
|
5959
|
-
|
|
5960
|
-
|
|
5961
|
-
|
|
5962
|
-
|
|
5963
|
-
|
|
5964
|
-
|
|
5965
|
-
|
|
5966
|
-
|
|
5967
|
-
|
|
5968
|
-
|
|
5969
|
-
|
|
5970
|
-
|
|
5971
|
-
|
|
5972
|
-
|
|
5973
|
-
}
|
|
6197
|
+
const normalizedFacets = normalizeSubQueries(searchOptions?.subQueries);
|
|
6198
|
+
const facetQueries = normalizedFacets.length >= 2 ? normalizedFacets : void 0;
|
|
6199
|
+
if (useFusion && !embeddingsUnavailable && facetQueries) {
|
|
6200
|
+
let subEmbeddings;
|
|
6201
|
+
try {
|
|
6202
|
+
subEmbeddings = await generateEmbeddings(facetQueries, embeddingOptions);
|
|
6203
|
+
} catch (err) {
|
|
6204
|
+
getLogger().warn(
|
|
6205
|
+
"memoryVault: sub-query embedding failed \u2014 falling back to single-query ranking: " + (err instanceof Error ? err.message : String(err))
|
|
6206
|
+
);
|
|
6207
|
+
subEmbeddings = [];
|
|
6208
|
+
}
|
|
6209
|
+
const subEmbeddingsUsable = facetQueries.length > 0 && subEmbeddings.length === facetQueries.length && subEmbeddings.every((v) => v.length > 0 && v.length === queryEmbedding.length);
|
|
6210
|
+
if (subEmbeddings.length > 0 && !subEmbeddingsUsable) {
|
|
6211
|
+
getLogger().warn(
|
|
6212
|
+
`memoryVault: sub-query embedding returned ${subEmbeddings.length} vectors (dims ${subEmbeddings.map((v) => v.length).join(",")}) for ${facetQueries.length} sub-queries against a ${queryEmbedding.length}-dim query \u2014 falling back to single-query ranking`
|
|
6213
|
+
);
|
|
6214
|
+
}
|
|
6215
|
+
if (subEmbeddingsUsable) {
|
|
6216
|
+
const subQueries = facetQueries.map((sq, i) => ({
|
|
6217
|
+
query: sq,
|
|
6218
|
+
embedding: subEmbeddings[i]
|
|
6219
|
+
}));
|
|
6220
|
+
const v2HeadStats = { hadResults: false };
|
|
6221
|
+
const composite = await rankComposite(query, queryEmbedding, subQueries, embeddedItems, {
|
|
6222
|
+
limit,
|
|
6223
|
+
minSimilarity,
|
|
6224
|
+
rerank: !!searchOptions?.rerank,
|
|
6225
|
+
...searchOptions?.rerankTopN !== void 0 && {
|
|
6226
|
+
rerankTopN: searchOptions.rerankTopN
|
|
6227
|
+
},
|
|
6228
|
+
...searchOptions?.ceWeight !== void 0 && { ceWeight: searchOptions.ceWeight },
|
|
6229
|
+
...searchOptions?.mmr !== void 0 && { mmr: searchOptions.mmr },
|
|
6230
|
+
...tuning,
|
|
6231
|
+
...searchOptions?.entityRanking && { entityRanking: searchOptions.entityRanking },
|
|
6232
|
+
...searchOptions?.temporalRanking && { temporalRanking: searchOptions.temporalRanking },
|
|
6233
|
+
rerankStats,
|
|
6234
|
+
v2HeadStats
|
|
6235
|
+
});
|
|
6236
|
+
return stampTimestamps({
|
|
6237
|
+
results: composite,
|
|
6238
|
+
vaultSize,
|
|
6239
|
+
reranked: rerankStats.applied,
|
|
6240
|
+
hadV2Head: v2HeadStats.hadResults
|
|
6241
|
+
});
|
|
5974
6242
|
}
|
|
5975
6243
|
}
|
|
5976
6244
|
if (useFusion && searchOptions?.rerank) {
|
|
@@ -6030,6 +6298,12 @@ async function searchVaultMemoriesWithSize(query, vaultCtx, embeddingOptions, ca
|
|
|
6030
6298
|
rankedOnCosine: false
|
|
6031
6299
|
};
|
|
6032
6300
|
}
|
|
6301
|
+
if (searchOptions?.decompose === "llm" && normalizeSubQueries(searchOptions.subQueries).length < 2 && !warnedProgrammaticDecomposeIgnored) {
|
|
6302
|
+
warnedProgrammaticDecomposeIgnored = true;
|
|
6303
|
+
getLogger().warn(
|
|
6304
|
+
'memoryVault: decompose:"llm" is ignored by searchVaultMemories \u2014 pass subQueries or use createMemoryVaultSearchTool / createRecallTool (719/B4)'
|
|
6305
|
+
);
|
|
6306
|
+
}
|
|
6033
6307
|
const prepared = await prepareVaultCandidates(
|
|
6034
6308
|
query,
|
|
6035
6309
|
vaultCtx,
|
|
@@ -6135,7 +6409,8 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
|
|
|
6135
6409
|
return "Error: A search query is required.";
|
|
6136
6410
|
}
|
|
6137
6411
|
try {
|
|
6138
|
-
const
|
|
6412
|
+
const wantsDecompose = searchOptions?.decompose === "llm" && !!searchOptions.decomposeOptions;
|
|
6413
|
+
const budget = wantsDecompose ? "high" : searchOptions?.rerank ? "mid" : "low";
|
|
6139
6414
|
const folderId = searchOptions?.folderId ?? argFolderId;
|
|
6140
6415
|
if (searchOptions?.useFusion === false) {
|
|
6141
6416
|
const { results: legacy, embeddingsUnavailable } = await searchVaultMemoriesWithSize(
|
|
@@ -6159,7 +6434,14 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
|
|
|
6159
6434
|
legacy.map((r) => ({ id: r.uniqueId, content: r.content, score: r.similarity }))
|
|
6160
6435
|
);
|
|
6161
6436
|
}
|
|
6162
|
-
|
|
6437
|
+
let subQueries;
|
|
6438
|
+
if (wantsDecompose && searchOptions?.decomposeOptions) {
|
|
6439
|
+
const decomp = await decomposeQuery(query, searchOptions.decomposeOptions);
|
|
6440
|
+
if (decomp.mode === "composite" && decomp.subQueries.length >= 2) {
|
|
6441
|
+
subQueries = decomp.subQueries;
|
|
6442
|
+
}
|
|
6443
|
+
}
|
|
6444
|
+
const { recall: recall2 } = await import("./recall-PIDYDFIQ.mjs");
|
|
6163
6445
|
let recallDegraded = [];
|
|
6164
6446
|
const result = await recall2(
|
|
6165
6447
|
query,
|
|
@@ -6175,9 +6457,7 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
|
|
|
6175
6457
|
...tuningForward,
|
|
6176
6458
|
...folderId !== void 0 && { folderId },
|
|
6177
6459
|
...searchOptions?.scopes && { scopes: searchOptions.scopes },
|
|
6178
|
-
...
|
|
6179
|
-
decomposeOptions: searchOptions.decomposeOptions
|
|
6180
|
-
}
|
|
6460
|
+
...subQueries && { subQueries }
|
|
6181
6461
|
}
|
|
6182
6462
|
);
|
|
6183
6463
|
if (result.vaultSize === 0) {
|
|
@@ -6705,12 +6985,12 @@ var DEFAULT_CHUNK_MIN_SCORE = 0.5;
|
|
|
6705
6985
|
function flagsForBudget(budget) {
|
|
6706
6986
|
switch (budget) {
|
|
6707
6987
|
case "high":
|
|
6708
|
-
return { rerank: true,
|
|
6988
|
+
return { rerank: true, traverse: true };
|
|
6709
6989
|
case "mid":
|
|
6710
|
-
return { rerank: true,
|
|
6990
|
+
return { rerank: true, traverse: false };
|
|
6711
6991
|
case "low":
|
|
6712
6992
|
default:
|
|
6713
|
-
return { rerank: false,
|
|
6993
|
+
return { rerank: false, traverse: false };
|
|
6714
6994
|
}
|
|
6715
6995
|
}
|
|
6716
6996
|
function dedupeBy(items, ...keys) {
|
|
@@ -6729,10 +7009,10 @@ function dedupeBy(items, ...keys) {
|
|
|
6729
7009
|
async function recall(query, ctx, options = {}) {
|
|
6730
7010
|
const types = options.types ?? ["fact"];
|
|
6731
7011
|
const limit = options.limit ?? DEFAULT_LIMIT;
|
|
6732
|
-
const
|
|
6733
|
-
const flags = flagsForBudget(
|
|
6734
|
-
const
|
|
6735
|
-
const
|
|
7012
|
+
const usedBudget = options.budget ?? DEFAULT_BUDGET;
|
|
7013
|
+
const flags = flagsForBudget(usedBudget);
|
|
7014
|
+
const normalizedFacets = normalizeSubQueries(options.subQueries);
|
|
7015
|
+
const subQueries = normalizedFacets.length >= 2 ? normalizedFacets : void 0;
|
|
6736
7016
|
const t0 = nowMs();
|
|
6737
7017
|
let prepMs = 0;
|
|
6738
7018
|
let factLaneMs = 0;
|
|
@@ -6753,7 +7033,9 @@ async function recall(query, ctx, options = {}) {
|
|
|
6753
7033
|
if (flags.rerank && factResults.length > 0 && hadV2Head && !didRerank) {
|
|
6754
7034
|
degraded.push("rerank-unavailable");
|
|
6755
7035
|
}
|
|
6756
|
-
if (
|
|
7036
|
+
if (usedBudget === "high" && options.decomposeOptions && !subQueries && !options.graphRefine) {
|
|
7037
|
+
degraded.push("decompose-moved");
|
|
7038
|
+
}
|
|
6757
7039
|
if (embeddingsUnavailable) degraded.push("embeddings-unavailable");
|
|
6758
7040
|
const diagnostics = {
|
|
6759
7041
|
usedBudget,
|
|
@@ -6866,10 +7148,8 @@ async function recall(query, ctx, options = {}) {
|
|
|
6866
7148
|
},
|
|
6867
7149
|
...options.rrfK !== void 0 && { rrfK: options.rrfK },
|
|
6868
7150
|
...options.decryptLast !== void 0 && { decryptLast: options.decryptLast },
|
|
6869
|
-
|
|
6870
|
-
|
|
6871
|
-
decomposeOptions: options.decomposeOptions
|
|
6872
|
-
},
|
|
7151
|
+
// 719/B4 — composite facets arrive pre-built; no LLM call here.
|
|
7152
|
+
...subQueries && { subQueries },
|
|
6873
7153
|
...options.scopes && { scopes: options.scopes },
|
|
6874
7154
|
...options.folderId !== void 0 && { folderId: options.folderId },
|
|
6875
7155
|
...options.factTypes?.length && { factTypes: options.factTypes },
|
|
@@ -7098,13 +7378,16 @@ function toChunkMemory(r) {
|
|
|
7098
7378
|
}
|
|
7099
7379
|
|
|
7100
7380
|
export {
|
|
7381
|
+
EncryptionKeyMissingError,
|
|
7101
7382
|
onClearAllEncryptionState,
|
|
7102
7383
|
onKeyAvailable,
|
|
7103
7384
|
clearEncryptionKey,
|
|
7104
7385
|
clearAllEncryptionState,
|
|
7105
7386
|
clearAllEncryptionKeys,
|
|
7106
7387
|
hasEncryptionKey,
|
|
7388
|
+
seedEncryptionKeys,
|
|
7107
7389
|
requestEncryptionKey,
|
|
7390
|
+
refreshEncryptionKeyIfMatches,
|
|
7108
7391
|
useEncryption,
|
|
7109
7392
|
cosineSimilarity,
|
|
7110
7393
|
isEncrypted,
|
|
@@ -7194,6 +7477,7 @@ export {
|
|
|
7194
7477
|
EMBEDDINGS_DEGRADED_EMPTY,
|
|
7195
7478
|
RerankerUnavailableError,
|
|
7196
7479
|
isRerankerAvailable,
|
|
7480
|
+
decomposeQuery,
|
|
7197
7481
|
DEFAULT_VAULT_CACHE_SIZE,
|
|
7198
7482
|
LRUCache,
|
|
7199
7483
|
createVaultEmbeddingCache,
|