@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(
|
|
@@ -331,8 +344,43 @@ async function decryptDataBytesFromBytes(encrypted, address, version = "v3") {
|
|
|
331
344
|
);
|
|
332
345
|
return new Uint8Array(decryptedData);
|
|
333
346
|
}
|
|
334
|
-
function hasEncryptionKey(address) {
|
|
335
|
-
return
|
|
347
|
+
function hasEncryptionKey(address, version) {
|
|
348
|
+
return getStoredKeyByVersion(address, version ?? "v3") !== null;
|
|
349
|
+
}
|
|
350
|
+
function seedEncryptionKeys(address, keys) {
|
|
351
|
+
if (!isValidWalletAddress(address)) {
|
|
352
|
+
throw new Error(
|
|
353
|
+
`Invalid wallet address: ${address}. Address must start with 0x and be 42 characters (0x + 40 hex characters).`
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
if (!keys.legacy && !keys.current) {
|
|
357
|
+
throw new Error("seedEncryptionKeys: at least one of legacy or current is required");
|
|
358
|
+
}
|
|
359
|
+
if (keys.legacy !== void 0) {
|
|
360
|
+
assertAes256KeyHex("legacy", keys.legacy);
|
|
361
|
+
}
|
|
362
|
+
if (keys.current !== void 0) {
|
|
363
|
+
assertAes256KeyHex("current", keys.current);
|
|
364
|
+
}
|
|
365
|
+
const existing = encryptionKeyStore.get(address);
|
|
366
|
+
const next = {
|
|
367
|
+
legacy: keys.legacy ?? existing?.legacy,
|
|
368
|
+
current: keys.current ?? existing?.current
|
|
369
|
+
};
|
|
370
|
+
setStoredKey(address, next);
|
|
371
|
+
if (keys.legacy !== void 0) cryptoKeyCache.delete(`${address}:v2`);
|
|
372
|
+
if (keys.current !== void 0) cryptoKeyCache.delete(`${address}:v3`);
|
|
373
|
+
refreshCandidatesByAddress.delete(address);
|
|
374
|
+
divergentDeriveAddresses.delete(address);
|
|
375
|
+
notifyKeyAvailable(address);
|
|
376
|
+
}
|
|
377
|
+
var AES_256_KEY_HEX = /^[0-9a-fA-F]{64}$/;
|
|
378
|
+
function assertAes256KeyHex(field, value) {
|
|
379
|
+
if (!AES_256_KEY_HEX.test(value)) {
|
|
380
|
+
throw new Error(
|
|
381
|
+
`seedEncryptionKeys: ${field} must be 64 hex characters (32-byte AES key), got length ${value.length}`
|
|
382
|
+
);
|
|
383
|
+
}
|
|
336
384
|
}
|
|
337
385
|
async function encryptBytesWithKey(plaintext, key) {
|
|
338
386
|
const plaintextBytes = typeof plaintext === "string" ? SHARED_TEXT_ENCODER.encode(plaintext) : plaintext;
|
|
@@ -381,20 +429,23 @@ async function decryptDataBatch(encryptedValues, address) {
|
|
|
381
429
|
const key = await getEncryptionKey(address);
|
|
382
430
|
return Promise.all(encryptedValues.map((value) => decryptDataWithKey(value, key)));
|
|
383
431
|
}
|
|
384
|
-
async function requestEncryptionKey(walletAddress, signMessage, embeddedWalletSigner) {
|
|
432
|
+
async function requestEncryptionKey(walletAddress, signMessage, embeddedWalletSigner, options) {
|
|
385
433
|
if (!isValidWalletAddress(walletAddress)) {
|
|
386
434
|
throw new Error(
|
|
387
435
|
`Invalid wallet address: ${walletAddress}. Address must start with 0x and be 42 characters (0x + 40 hex characters).`
|
|
388
436
|
);
|
|
389
437
|
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
return;
|
|
438
|
+
if (!options?.force && getStoredKeyByVersion(walletAddress, "v3") && getStoredKeyByVersion(walletAddress, "v2")) {
|
|
439
|
+
return true;
|
|
393
440
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
441
|
+
if (!options?.force && divergentDeriveAddresses.has(walletAddress)) {
|
|
442
|
+
return false;
|
|
443
|
+
}
|
|
444
|
+
if (!options?.force) {
|
|
445
|
+
const pending = pendingKeyRequests.get(walletAddress);
|
|
446
|
+
if (pending) {
|
|
447
|
+
return pending;
|
|
448
|
+
}
|
|
398
449
|
}
|
|
399
450
|
const startEpoch = sessionEpoch;
|
|
400
451
|
const promise = (async () => {
|
|
@@ -422,20 +473,143 @@ async function requestEncryptionKey(walletAddress, signMessage, embeddedWalletSi
|
|
|
422
473
|
deriveKeyFromSignatureV3(signature)
|
|
423
474
|
]);
|
|
424
475
|
if (sessionEpoch !== startEpoch) {
|
|
425
|
-
return;
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
478
|
+
const existing = encryptionKeyStore.get(walletAddress);
|
|
479
|
+
if (!options?.force && existing && (existing.legacy || existing.current)) {
|
|
480
|
+
const matchesCurrent = Boolean(existing.current && existing.current === currentKey);
|
|
481
|
+
const matchesLegacy = Boolean(existing.legacy && existing.legacy === legacyKey);
|
|
482
|
+
if (!matchesCurrent && !matchesLegacy) {
|
|
483
|
+
getLogger().warn(
|
|
484
|
+
"requestEncryptionKey: derived keys do not match existing store; leaving unchanged"
|
|
485
|
+
);
|
|
486
|
+
divergentDeriveAddresses.add(walletAddress);
|
|
487
|
+
return false;
|
|
488
|
+
}
|
|
489
|
+
setStoredKey(walletAddress, {
|
|
490
|
+
legacy: existing.legacy ?? legacyKey,
|
|
491
|
+
current: existing.current ?? currentKey
|
|
492
|
+
});
|
|
493
|
+
} else {
|
|
494
|
+
setStoredKey(walletAddress, { legacy: legacyKey, current: currentKey });
|
|
426
495
|
}
|
|
427
|
-
|
|
496
|
+
cryptoKeyCache.delete(`${walletAddress}:v2`);
|
|
497
|
+
cryptoKeyCache.delete(`${walletAddress}:v3`);
|
|
498
|
+
refreshCandidatesByAddress.delete(walletAddress);
|
|
499
|
+
divergentDeriveAddresses.delete(walletAddress);
|
|
428
500
|
notifyKeyAvailable(walletAddress);
|
|
501
|
+
return true;
|
|
429
502
|
})();
|
|
430
503
|
pendingKeyRequests.set(walletAddress, promise);
|
|
431
504
|
try {
|
|
432
|
-
await promise;
|
|
505
|
+
return await promise;
|
|
433
506
|
} finally {
|
|
434
507
|
if (pendingKeyRequests.get(walletAddress) === promise) {
|
|
435
508
|
pendingKeyRequests.delete(walletAddress);
|
|
436
509
|
}
|
|
437
510
|
}
|
|
438
511
|
}
|
|
512
|
+
async function refreshEncryptionKeyIfMatches(walletAddress, probeCiphertext, signMessage, embeddedWalletSigner) {
|
|
513
|
+
if (!isValidWalletAddress(walletAddress)) {
|
|
514
|
+
throw new Error(
|
|
515
|
+
`Invalid wallet address: ${walletAddress}. Address must start with 0x and be 42 characters (0x + 40 hex characters).`
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
const parsed = parsePrefixedCiphertext(probeCiphertext);
|
|
519
|
+
if (!parsed) {
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
const cached = refreshCandidatesByAddress.get(walletAddress);
|
|
523
|
+
if (cached) {
|
|
524
|
+
return tryCommitCandidatesIfProbeMatches(walletAddress, parsed, cached);
|
|
525
|
+
}
|
|
526
|
+
let derivePromise = pendingCandidateDerives.get(walletAddress);
|
|
527
|
+
if (!derivePromise) {
|
|
528
|
+
derivePromise = (async () => {
|
|
529
|
+
const candidates2 = await deriveRefreshCandidates(signMessage, embeddedWalletSigner);
|
|
530
|
+
if (candidates2) {
|
|
531
|
+
refreshCandidatesByAddress.set(walletAddress, candidates2);
|
|
532
|
+
}
|
|
533
|
+
return candidates2;
|
|
534
|
+
})().finally(() => {
|
|
535
|
+
if (pendingCandidateDerives.get(walletAddress) === derivePromise) {
|
|
536
|
+
pendingCandidateDerives.delete(walletAddress);
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
pendingCandidateDerives.set(walletAddress, derivePromise);
|
|
540
|
+
}
|
|
541
|
+
const candidates = await derivePromise;
|
|
542
|
+
if (!candidates) {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
return tryCommitCandidatesIfProbeMatches(walletAddress, parsed, candidates);
|
|
546
|
+
}
|
|
547
|
+
function parsePrefixedCiphertext(probeCiphertext) {
|
|
548
|
+
const version = probeCiphertext.startsWith("enc:v3:") ? "v3" : probeCiphertext.startsWith("enc:v2:") ? "v2" : null;
|
|
549
|
+
if (!version) return null;
|
|
550
|
+
const encryptedData = probeCiphertext.slice(
|
|
551
|
+
version === "v3" ? "enc:v3:".length : "enc:v2:".length
|
|
552
|
+
);
|
|
553
|
+
if (encryptedData.length < 56 || !/^[0-9a-f]+$/i.test(encryptedData)) {
|
|
554
|
+
return null;
|
|
555
|
+
}
|
|
556
|
+
return { version, encryptedData };
|
|
557
|
+
}
|
|
558
|
+
async function deriveRefreshCandidates(signMessage, embeddedWalletSigner) {
|
|
559
|
+
const startEpoch = sessionEpoch;
|
|
560
|
+
const signOptions = { showWalletUIs: false };
|
|
561
|
+
let signature;
|
|
562
|
+
try {
|
|
563
|
+
if (embeddedWalletSigner) {
|
|
564
|
+
signature = await embeddedWalletSigner(SIGN_MESSAGE, signOptions);
|
|
565
|
+
} else {
|
|
566
|
+
signature = await signMessage(SIGN_MESSAGE, signOptions);
|
|
567
|
+
}
|
|
568
|
+
} catch (error) {
|
|
569
|
+
if (embeddedWalletSigner && error instanceof Error) {
|
|
570
|
+
getLogger().warn(
|
|
571
|
+
"Embedded wallet signing failed during key refresh, falling back to standard signMessage:",
|
|
572
|
+
error.message
|
|
573
|
+
);
|
|
574
|
+
signature = await signMessage(SIGN_MESSAGE, signOptions);
|
|
575
|
+
} else {
|
|
576
|
+
throw error;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
const [legacyKey, currentKey] = await Promise.all([
|
|
580
|
+
deriveKeyFromSignature(signature),
|
|
581
|
+
deriveKeyFromSignatureV3(signature)
|
|
582
|
+
]);
|
|
583
|
+
if (sessionEpoch !== startEpoch) {
|
|
584
|
+
return null;
|
|
585
|
+
}
|
|
586
|
+
return { legacy: legacyKey, current: currentKey };
|
|
587
|
+
}
|
|
588
|
+
async function tryCommitCandidatesIfProbeMatches(walletAddress, parsed, candidates) {
|
|
589
|
+
const candidateHex = parsed.version === "v2" ? candidates.legacy : candidates.current;
|
|
590
|
+
const keyBytes = hexToBytes(candidateHex);
|
|
591
|
+
const candidateCryptoKey = await crypto.subtle.importKey(
|
|
592
|
+
"raw",
|
|
593
|
+
keyBytes.buffer,
|
|
594
|
+
{ name: "AES-GCM" },
|
|
595
|
+
false,
|
|
596
|
+
["encrypt", "decrypt"]
|
|
597
|
+
);
|
|
598
|
+
try {
|
|
599
|
+
await decryptDataWithKey(parsed.encryptedData, candidateCryptoKey);
|
|
600
|
+
} catch {
|
|
601
|
+
getLogger().warn(
|
|
602
|
+
"refreshEncryptionKeyIfMatches: candidate keys failed to decrypt probe; leaving store unchanged"
|
|
603
|
+
);
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
setStoredKey(walletAddress, { legacy: candidates.legacy, current: candidates.current });
|
|
607
|
+
cryptoKeyCache.delete(`${walletAddress}:v2`);
|
|
608
|
+
cryptoKeyCache.delete(`${walletAddress}:v3`);
|
|
609
|
+
divergentDeriveAddresses.delete(walletAddress);
|
|
610
|
+
notifyKeyAvailable(walletAddress);
|
|
611
|
+
return true;
|
|
612
|
+
}
|
|
439
613
|
var KEYPAIR_STORAGE_PREFIX = "ecdh_keypair_";
|
|
440
614
|
async function persistKeyPair(address, startEpoch) {
|
|
441
615
|
if (typeof window === "undefined") {
|
|
@@ -715,20 +889,29 @@ async function encryptField(value, address, signMessage, embeddedWalletSigner, s
|
|
|
715
889
|
const encrypted = await encryptData(value, address);
|
|
716
890
|
return `${ENCRYPTION_PREFIX}${encrypted}`;
|
|
717
891
|
}
|
|
718
|
-
async function
|
|
719
|
-
if (!value) return value;
|
|
892
|
+
async function decryptFieldDetailed(value, address) {
|
|
893
|
+
if (!value) return { status: "plaintext", value };
|
|
720
894
|
const detected = detectEncryptionVersion(value);
|
|
721
|
-
if (!detected) return value;
|
|
895
|
+
if (!detected) return { status: "plaintext", value };
|
|
722
896
|
if (detected.encryptedData.length < 56 || !/^[0-9a-f]+$/i.test(detected.encryptedData)) {
|
|
723
|
-
return value;
|
|
897
|
+
return { status: "invalid_payload", value, version: detected.version };
|
|
724
898
|
}
|
|
725
899
|
try {
|
|
726
|
-
|
|
900
|
+
const plaintext = await decryptData(detected.encryptedData, address, detected.version);
|
|
901
|
+
return { status: "ok", value: plaintext, version: detected.version };
|
|
727
902
|
} catch (error) {
|
|
728
|
-
|
|
729
|
-
|
|
903
|
+
if (error instanceof EncryptionKeyMissingError) {
|
|
904
|
+
getLogger().warn("Failed to decrypt field (key missing for version):", error.message);
|
|
905
|
+
return { status: "key_missing", value, version: detected.version };
|
|
906
|
+
}
|
|
907
|
+
getLogger().warn("Failed to decrypt field (auth mismatch or crypto error):", error);
|
|
908
|
+
return { status: "auth_mismatch", value, version: detected.version };
|
|
730
909
|
}
|
|
731
910
|
}
|
|
911
|
+
async function decryptField(value, address) {
|
|
912
|
+
const result = await decryptFieldDetailed(value, address);
|
|
913
|
+
return result.value;
|
|
914
|
+
}
|
|
732
915
|
async function encryptJsonField(value, address, signMessage, embeddedWalletSigner, skipKeyRequest) {
|
|
733
916
|
if (!value) return void 0;
|
|
734
917
|
const jsonString = JSON.stringify(value);
|
|
@@ -828,20 +1011,40 @@ async function encryptMessageFields(message, address, signMessage, embeddedWalle
|
|
|
828
1011
|
throw error;
|
|
829
1012
|
}
|
|
830
1013
|
}
|
|
831
|
-
async function
|
|
832
|
-
if (value === void 0) return void 0;
|
|
833
|
-
if (value === null) return value;
|
|
1014
|
+
async function decryptMaybeJsonFieldDetailed(value, address) {
|
|
1015
|
+
if (value === void 0) return { value: void 0, status: "plaintext" };
|
|
1016
|
+
if (value === null) return { value, status: "plaintext" };
|
|
834
1017
|
if (typeof value === "string") {
|
|
835
1018
|
if (isEncrypted(value)) {
|
|
836
|
-
|
|
1019
|
+
const detailed = await decryptFieldDetailed(value, address);
|
|
1020
|
+
if (detailed.status !== "ok") {
|
|
1021
|
+
return { value: void 0, status: detailed.status, probe: value };
|
|
1022
|
+
}
|
|
1023
|
+
try {
|
|
1024
|
+
return { value: JSON.parse(detailed.value), status: "ok" };
|
|
1025
|
+
} catch (error) {
|
|
1026
|
+
getLogger().warn("Failed to parse decrypted JSON field:", error);
|
|
1027
|
+
return { value: void 0, status: "invalid_payload", probe: value };
|
|
1028
|
+
}
|
|
837
1029
|
}
|
|
838
1030
|
try {
|
|
839
|
-
return JSON.parse(value);
|
|
1031
|
+
return { value: JSON.parse(value), status: "plaintext" };
|
|
840
1032
|
} catch {
|
|
841
|
-
return void 0;
|
|
1033
|
+
return { value: void 0, status: "plaintext" };
|
|
842
1034
|
}
|
|
843
1035
|
}
|
|
844
|
-
return value;
|
|
1036
|
+
return { value, status: "plaintext" };
|
|
1037
|
+
}
|
|
1038
|
+
function worseDecryptStatus(a, b) {
|
|
1039
|
+
if (b === "ok" || b === "plaintext") return a;
|
|
1040
|
+
if (!a || a === "ok" || a === "plaintext") return b;
|
|
1041
|
+
const rank = (s) => {
|
|
1042
|
+
if (s === "auth_mismatch") return 3;
|
|
1043
|
+
if (s === "key_missing") return 2;
|
|
1044
|
+
if (s === "invalid_payload") return 1;
|
|
1045
|
+
return 0;
|
|
1046
|
+
};
|
|
1047
|
+
return rank(b) >= rank(a) ? b : a;
|
|
845
1048
|
}
|
|
846
1049
|
async function decryptMessageFields(message, address, signMessage, embeddedWalletSigner) {
|
|
847
1050
|
if (!address) {
|
|
@@ -854,33 +1057,82 @@ async function decryptMessageFields(message, address, signMessage, embeddedWalle
|
|
|
854
1057
|
getLogger().warn("Failed to request encryption key for decryption:", error);
|
|
855
1058
|
}
|
|
856
1059
|
}
|
|
857
|
-
|
|
858
|
-
const
|
|
859
|
-
|
|
1060
|
+
let assembled = await assembleDecryptedMessage(message, address);
|
|
1061
|
+
const needsRefresh = assembled.decryptionStatus === "auth_mismatch" || assembled.decryptionStatus === "key_missing";
|
|
1062
|
+
if (needsRefresh && signMessage && assembled.refreshProbe) {
|
|
1063
|
+
try {
|
|
1064
|
+
const refreshed = await refreshEncryptionKeyIfMatches(
|
|
1065
|
+
address,
|
|
1066
|
+
assembled.refreshProbe,
|
|
1067
|
+
signMessage,
|
|
1068
|
+
embeddedWalletSigner
|
|
1069
|
+
);
|
|
1070
|
+
if (refreshed) {
|
|
1071
|
+
assembled = await assembleDecryptedMessage(message, address);
|
|
1072
|
+
}
|
|
1073
|
+
} catch (error) {
|
|
1074
|
+
getLogger().warn("Failed to refresh encryption key after decrypt failure:", error);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
const { refreshProbe: _probe, ...result } = assembled;
|
|
1078
|
+
return result;
|
|
1079
|
+
}
|
|
1080
|
+
async function assembleDecryptedMessage(message, address) {
|
|
1081
|
+
const contentResult = await decryptFieldDetailed(message.content, address);
|
|
1082
|
+
const thinkingResult = message.thinking ? await decryptFieldDetailed(message.thinking, address) : null;
|
|
1083
|
+
const vectorResult = await decryptMaybeJsonFieldDetailed(
|
|
860
1084
|
message.vector,
|
|
861
1085
|
address
|
|
862
1086
|
);
|
|
863
|
-
const
|
|
1087
|
+
const chunksResult = await decryptMaybeJsonFieldDetailed(
|
|
864
1088
|
message.chunks,
|
|
865
1089
|
address
|
|
866
1090
|
);
|
|
867
|
-
const
|
|
1091
|
+
const sourcesResult = await decryptMaybeJsonFieldDetailed(
|
|
868
1092
|
message.sources,
|
|
869
1093
|
address
|
|
870
1094
|
);
|
|
871
|
-
const
|
|
1095
|
+
const thoughtProcessResult = await decryptMaybeJsonFieldDetailed(
|
|
872
1096
|
message.thoughtProcess,
|
|
873
1097
|
address
|
|
874
1098
|
);
|
|
875
|
-
|
|
1099
|
+
let decryptionStatus;
|
|
1100
|
+
let refreshProbe;
|
|
1101
|
+
const consider = (status, probe) => {
|
|
1102
|
+
decryptionStatus = worseDecryptStatus(decryptionStatus, status);
|
|
1103
|
+
if (!refreshProbe && probe && (status === "auth_mismatch" || status === "key_missing")) {
|
|
1104
|
+
refreshProbe = probe;
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
consider(contentResult.status, isEncrypted(message.content) ? message.content : void 0);
|
|
1108
|
+
if (thinkingResult && message.thinking && typeof message.thinking === "string") {
|
|
1109
|
+
consider(thinkingResult.status, isEncrypted(message.thinking) ? message.thinking : void 0);
|
|
1110
|
+
}
|
|
1111
|
+
if (vectorResult.probe) consider(vectorResult.status, vectorResult.probe);
|
|
1112
|
+
if (chunksResult.probe) consider(chunksResult.status, chunksResult.probe);
|
|
1113
|
+
if (sourcesResult.probe) consider(sourcesResult.status, sourcesResult.probe);
|
|
1114
|
+
if (thoughtProcessResult.probe) {
|
|
1115
|
+
consider(thoughtProcessResult.status, thoughtProcessResult.probe);
|
|
1116
|
+
}
|
|
1117
|
+
const statusForConsumer = decryptionStatus === "ok" || decryptionStatus === "plaintext" || !decryptionStatus ? void 0 : decryptionStatus;
|
|
1118
|
+
const result = {
|
|
876
1119
|
...message,
|
|
877
|
-
content:
|
|
878
|
-
thinking:
|
|
879
|
-
vector:
|
|
880
|
-
chunks:
|
|
881
|
-
sources:
|
|
882
|
-
thoughtProcess:
|
|
1120
|
+
content: contentResult.value,
|
|
1121
|
+
thinking: thinkingResult ? thinkingResult.value : message.thinking,
|
|
1122
|
+
vector: vectorResult.value,
|
|
1123
|
+
chunks: chunksResult.value,
|
|
1124
|
+
sources: sourcesResult.value,
|
|
1125
|
+
thoughtProcess: thoughtProcessResult.value
|
|
883
1126
|
};
|
|
1127
|
+
if (statusForConsumer) {
|
|
1128
|
+
result.decryptionStatus = statusForConsumer;
|
|
1129
|
+
} else {
|
|
1130
|
+
delete result.decryptionStatus;
|
|
1131
|
+
}
|
|
1132
|
+
if (refreshProbe) {
|
|
1133
|
+
result.refreshProbe = refreshProbe;
|
|
1134
|
+
}
|
|
1135
|
+
return result;
|
|
884
1136
|
}
|
|
885
1137
|
|
|
886
1138
|
// src/lib/db/chat/types.ts
|
|
@@ -4779,6 +5031,107 @@ async function chunkAndEmbedAllMessages(ctx, options, filter) {
|
|
|
4779
5031
|
return embeddedCount;
|
|
4780
5032
|
}
|
|
4781
5033
|
|
|
5034
|
+
// src/lib/memoryVault/decomposeQuery.ts
|
|
5035
|
+
var DEFAULT_MODEL = "inclusionai/ling-2.6-flash";
|
|
5036
|
+
var MAX_SUB_QUERIES = 5;
|
|
5037
|
+
var SYSTEM_PROMPT = `You classify a memory query and, if needed, decompose it into concrete sub-queries.
|
|
5038
|
+
|
|
5039
|
+
CLASSIFICATION RULES \u2014 be strict:
|
|
5040
|
+
- "specific": asks for one fact OR a list of items in ONE category (one dimension).
|
|
5041
|
+
Examples: a name, a date, a preference, a location, an ID, a value, "what hobbies",
|
|
5042
|
+
"which conferences", "what open source projects", "what does X do for exercise".
|
|
5043
|
+
Even when the answer is a list, if all items belong to one dimension, it is specific.
|
|
5044
|
+
- "composite": asks about a person, system, or setup MULTI-DIMENSIONALLY \u2014 a summary,
|
|
5045
|
+
an overview, a profile, "tell me about X", "what is my X setup", "describe the team's
|
|
5046
|
+
X". The answer must span multiple distinct fact dimensions (e.g. for a "tech stack":
|
|
5047
|
+
language + database + framework + tools \u2014 four different dimensions).
|
|
5048
|
+
|
|
5049
|
+
When in doubt, prefer "specific". Mis-classifying a single-dimension list as composite
|
|
5050
|
+
hurts retrieval more than the reverse.
|
|
5051
|
+
|
|
5052
|
+
SUB-QUERY RULES (composite only, 3\u20135 questions):
|
|
5053
|
+
- Each sub-query targets a DIFFERENT fact dimension.
|
|
5054
|
+
- Use SHORT parenthetical examples \u2014 2\u20134 concrete options that anchor the
|
|
5055
|
+
embedding to likely memory content. Examples must be terms a user would
|
|
5056
|
+
actually have in their notes (e.g., "Postgres" not "PostgreSQL 15.2 with
|
|
5057
|
+
read replicas"). No verbose qualifiers like "and versions" or "and any
|
|
5058
|
+
relevant configuration details".
|
|
5059
|
+
- 8\u201314 words including the parenthetical. Like a human asking a follow-up.
|
|
5060
|
+
|
|
5061
|
+
Examples:
|
|
5062
|
+
"What is my dog's name?" \u2192 {"mode":"specific","subQueries":["What is my dog's name?"]}
|
|
5063
|
+
"What does the user do for exercise?" \u2192 {"mode":"specific","subQueries":["What does the user do for exercise?"]}
|
|
5064
|
+
"What conferences has the user attended?" \u2192 {"mode":"specific","subQueries":["What conferences has the user attended?"]}
|
|
5065
|
+
"What did I do last Tuesday?" \u2192 {"mode":"specific","subQueries":["What did I do last Tuesday?"]}
|
|
5066
|
+
"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)?"]}
|
|
5067
|
+
"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)?"]}
|
|
5068
|
+
"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)?"]}
|
|
5069
|
+
|
|
5070
|
+
Output strict JSON. No prose.`;
|
|
5071
|
+
async function decomposeQuery(query, options) {
|
|
5072
|
+
const fallback = { mode: "specific", subQueries: [query] };
|
|
5073
|
+
const trimmed = query.trim();
|
|
5074
|
+
if (trimmed.length === 0) return fallback;
|
|
5075
|
+
let parsed;
|
|
5076
|
+
try {
|
|
5077
|
+
parsed = await callPortalJsonCompletion({
|
|
5078
|
+
...options.apiKey !== void 0 && { apiKey: options.apiKey },
|
|
5079
|
+
...options.getToken !== void 0 && { getToken: options.getToken },
|
|
5080
|
+
...options.baseUrl !== void 0 && { baseUrl: options.baseUrl },
|
|
5081
|
+
model: options.model ?? DEFAULT_MODEL,
|
|
5082
|
+
systemPrompt: SYSTEM_PROMPT,
|
|
5083
|
+
// Wrap the bare query in a task framing so the model treats it as
|
|
5084
|
+
// input to classify, not as a question to answer conversationally.
|
|
5085
|
+
// Bare-query inputs caused Anthropic models to respond with prose
|
|
5086
|
+
// like "Do you mean...?" before this wrap.
|
|
5087
|
+
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.
|
|
5088
|
+
|
|
5089
|
+
Query: ${trimmed}`,
|
|
5090
|
+
// Decompose runs ahead of recall in the tool layer — tighter than the
|
|
5091
|
+
// portalLlm default (consolidate/extract can wait longer).
|
|
5092
|
+
timeoutMs: 2e4,
|
|
5093
|
+
// No internal retry: a failure degrades to the safe `{specific,[query]}`
|
|
5094
|
+
// fallback below. The agent/tool loop is the retry surface (719/B4).
|
|
5095
|
+
maxAttempts: 1,
|
|
5096
|
+
tag: "memory/decompose",
|
|
5097
|
+
...options.fetchFn && { fetchFn: options.fetchFn }
|
|
5098
|
+
});
|
|
5099
|
+
} catch (err) {
|
|
5100
|
+
getLogger().warn("memoryVault/decompose: portal call failed, falling back to specific", err);
|
|
5101
|
+
return fallback;
|
|
5102
|
+
}
|
|
5103
|
+
if (parsed === null) return fallback;
|
|
5104
|
+
return validate(parsed, trimmed) ?? fallback;
|
|
5105
|
+
}
|
|
5106
|
+
function normalizeSubQueries(raw) {
|
|
5107
|
+
if (!raw || raw.length === 0) return [];
|
|
5108
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5109
|
+
const out = [];
|
|
5110
|
+
for (const s of raw) {
|
|
5111
|
+
if (typeof s !== "string") continue;
|
|
5112
|
+
const t = s.trim();
|
|
5113
|
+
if (t.length === 0) continue;
|
|
5114
|
+
const key = t.toLowerCase();
|
|
5115
|
+
if (seen.has(key)) continue;
|
|
5116
|
+
seen.add(key);
|
|
5117
|
+
out.push(t);
|
|
5118
|
+
if (out.length >= MAX_SUB_QUERIES) break;
|
|
5119
|
+
}
|
|
5120
|
+
return out;
|
|
5121
|
+
}
|
|
5122
|
+
function validate(parsed, originalQuery) {
|
|
5123
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
5124
|
+
const obj = parsed;
|
|
5125
|
+
const mode = obj.mode === "composite" ? "composite" : "specific";
|
|
5126
|
+
if (!Array.isArray(obj.subQueries)) return null;
|
|
5127
|
+
const subQueries = normalizeSubQueries(obj.subQueries);
|
|
5128
|
+
if (subQueries.length === 0) return null;
|
|
5129
|
+
if (mode === "specific") {
|
|
5130
|
+
return { mode: "specific", subQueries: [originalQuery] };
|
|
5131
|
+
}
|
|
5132
|
+
return { mode, subQueries };
|
|
5133
|
+
}
|
|
5134
|
+
|
|
4782
5135
|
// src/lib/memory/mmr.ts
|
|
4783
5136
|
function applyMMR(candidates, k, lambda = 0.5) {
|
|
4784
5137
|
if (candidates.length === 0 || k <= 0) return [];
|
|
@@ -5156,91 +5509,6 @@ function scoreBM25(query, items) {
|
|
|
5156
5509
|
return scoreBM25Prepared(query, prepareBM25Corpus(items));
|
|
5157
5510
|
}
|
|
5158
5511
|
|
|
5159
|
-
// src/lib/memoryVault/decomposeQuery.ts
|
|
5160
|
-
var DEFAULT_MODEL = "inclusionai/ling-2.6-flash";
|
|
5161
|
-
var MAX_SUB_QUERIES = 5;
|
|
5162
|
-
var SYSTEM_PROMPT = `You classify a memory query and, if needed, decompose it into concrete sub-queries.
|
|
5163
|
-
|
|
5164
|
-
CLASSIFICATION RULES \u2014 be strict:
|
|
5165
|
-
- "specific": asks for one fact OR a list of items in ONE category (one dimension).
|
|
5166
|
-
Examples: a name, a date, a preference, a location, an ID, a value, "what hobbies",
|
|
5167
|
-
"which conferences", "what open source projects", "what does X do for exercise".
|
|
5168
|
-
Even when the answer is a list, if all items belong to one dimension, it is specific.
|
|
5169
|
-
- "composite": asks about a person, system, or setup MULTI-DIMENSIONALLY \u2014 a summary,
|
|
5170
|
-
an overview, a profile, "tell me about X", "what is my X setup", "describe the team's
|
|
5171
|
-
X". The answer must span multiple distinct fact dimensions (e.g. for a "tech stack":
|
|
5172
|
-
language + database + framework + tools \u2014 four different dimensions).
|
|
5173
|
-
|
|
5174
|
-
When in doubt, prefer "specific". Mis-classifying a single-dimension list as composite
|
|
5175
|
-
hurts retrieval more than the reverse.
|
|
5176
|
-
|
|
5177
|
-
SUB-QUERY RULES (composite only, 3\u20135 questions):
|
|
5178
|
-
- Each sub-query targets a DIFFERENT fact dimension.
|
|
5179
|
-
- Use SHORT parenthetical examples \u2014 2\u20134 concrete options that anchor the
|
|
5180
|
-
embedding to likely memory content. Examples must be terms a user would
|
|
5181
|
-
actually have in their notes (e.g., "Postgres" not "PostgreSQL 15.2 with
|
|
5182
|
-
read replicas"). No verbose qualifiers like "and versions" or "and any
|
|
5183
|
-
relevant configuration details".
|
|
5184
|
-
- 8\u201314 words including the parenthetical. Like a human asking a follow-up.
|
|
5185
|
-
|
|
5186
|
-
Examples:
|
|
5187
|
-
"What is my dog's name?" \u2192 {"mode":"specific","subQueries":["What is my dog's name?"]}
|
|
5188
|
-
"What does the user do for exercise?" \u2192 {"mode":"specific","subQueries":["What does the user do for exercise?"]}
|
|
5189
|
-
"What conferences has the user attended?" \u2192 {"mode":"specific","subQueries":["What conferences has the user attended?"]}
|
|
5190
|
-
"What did I do last Tuesday?" \u2192 {"mode":"specific","subQueries":["What did I do last Tuesday?"]}
|
|
5191
|
-
"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)?"]}
|
|
5192
|
-
"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)?"]}
|
|
5193
|
-
"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)?"]}
|
|
5194
|
-
|
|
5195
|
-
Output strict JSON. No prose.`;
|
|
5196
|
-
async function decomposeQuery(query, options) {
|
|
5197
|
-
const fallback = { mode: "specific", subQueries: [query] };
|
|
5198
|
-
const trimmed = query.trim();
|
|
5199
|
-
if (trimmed.length === 0) return fallback;
|
|
5200
|
-
let parsed;
|
|
5201
|
-
try {
|
|
5202
|
-
parsed = await callPortalJsonCompletion({
|
|
5203
|
-
...options.apiKey !== void 0 && { apiKey: options.apiKey },
|
|
5204
|
-
...options.getToken !== void 0 && { getToken: options.getToken },
|
|
5205
|
-
...options.baseUrl !== void 0 && { baseUrl: options.baseUrl },
|
|
5206
|
-
model: options.model ?? DEFAULT_MODEL,
|
|
5207
|
-
systemPrompt: SYSTEM_PROMPT,
|
|
5208
|
-
// Wrap the bare query in a task framing so the model treats it as
|
|
5209
|
-
// input to classify, not as a question to answer conversationally.
|
|
5210
|
-
// Bare-query inputs caused Anthropic models to respond with prose
|
|
5211
|
-
// like "Do you mean...?" before this wrap.
|
|
5212
|
-
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.
|
|
5213
|
-
|
|
5214
|
-
Query: ${trimmed}`,
|
|
5215
|
-
// Decompose runs on the recall hot path — tighter than the
|
|
5216
|
-
// portalLlm default (consolidate/extract can wait longer).
|
|
5217
|
-
timeoutMs: 2e4,
|
|
5218
|
-
// No internal retry: a failure degrades to the safe `{specific,[query]}`
|
|
5219
|
-
// fallback below, so retrying would only add latency on the hot path.
|
|
5220
|
-
maxAttempts: 1,
|
|
5221
|
-
tag: "memory/decompose",
|
|
5222
|
-
...options.fetchFn && { fetchFn: options.fetchFn }
|
|
5223
|
-
});
|
|
5224
|
-
} catch (err) {
|
|
5225
|
-
getLogger().warn("memoryVault/decompose: portal call failed, falling back to specific", err);
|
|
5226
|
-
return fallback;
|
|
5227
|
-
}
|
|
5228
|
-
if (parsed === null) return fallback;
|
|
5229
|
-
return validate(parsed, trimmed) ?? fallback;
|
|
5230
|
-
}
|
|
5231
|
-
function validate(parsed, originalQuery) {
|
|
5232
|
-
if (typeof parsed !== "object" || parsed === null) return null;
|
|
5233
|
-
const obj = parsed;
|
|
5234
|
-
const mode = obj.mode === "composite" ? "composite" : "specific";
|
|
5235
|
-
if (!Array.isArray(obj.subQueries)) return null;
|
|
5236
|
-
const subQueries = obj.subQueries.filter((s) => typeof s === "string").map((s) => s.trim()).filter((s) => s.length > 0).slice(0, MAX_SUB_QUERIES);
|
|
5237
|
-
if (subQueries.length === 0) return null;
|
|
5238
|
-
if (mode === "specific") {
|
|
5239
|
-
return { mode: "specific", subQueries: [originalQuery] };
|
|
5240
|
-
}
|
|
5241
|
-
return { mode, subQueries };
|
|
5242
|
-
}
|
|
5243
|
-
|
|
5244
5512
|
// src/lib/memoryVault/lruCache.ts
|
|
5245
5513
|
var DEFAULT_VAULT_CACHE_SIZE = 5e3;
|
|
5246
5514
|
var LRUCache = class extends Map {
|
|
@@ -5274,6 +5542,7 @@ function createVaultEmbeddingCache(maxSize = DEFAULT_VAULT_CACHE_SIZE) {
|
|
|
5274
5542
|
}
|
|
5275
5543
|
|
|
5276
5544
|
// src/lib/memoryVault/searchTool.ts
|
|
5545
|
+
var warnedProgrammaticDecomposeIgnored = false;
|
|
5277
5546
|
function normalizeEventTimeKind(kind) {
|
|
5278
5547
|
return kind === "point" || kind === "range" || kind === "ongoing" ? kind : null;
|
|
5279
5548
|
}
|
|
@@ -6208,52 +6477,51 @@ async function rankPreparedVaultCandidates(query, prepared, embeddingOptions, se
|
|
|
6208
6477
|
...searchOptions?.rrfK !== void 0 && { rrfK: searchOptions.rrfK },
|
|
6209
6478
|
...searchOptions?.factTypeWeights && { factTypeWeights: searchOptions.factTypeWeights }
|
|
6210
6479
|
};
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
6226
|
-
|
|
6227
|
-
|
|
6228
|
-
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
|
|
6248
|
-
|
|
6249
|
-
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
}
|
|
6480
|
+
const normalizedFacets = normalizeSubQueries(searchOptions?.subQueries);
|
|
6481
|
+
const facetQueries = normalizedFacets.length >= 2 ? normalizedFacets : void 0;
|
|
6482
|
+
if (useFusion && !embeddingsUnavailable && facetQueries) {
|
|
6483
|
+
let subEmbeddings;
|
|
6484
|
+
try {
|
|
6485
|
+
subEmbeddings = await generateEmbeddings(facetQueries, embeddingOptions);
|
|
6486
|
+
} catch (err) {
|
|
6487
|
+
getLogger().warn(
|
|
6488
|
+
"memoryVault: sub-query embedding failed \u2014 falling back to single-query ranking: " + (err instanceof Error ? err.message : String(err))
|
|
6489
|
+
);
|
|
6490
|
+
subEmbeddings = [];
|
|
6491
|
+
}
|
|
6492
|
+
const subEmbeddingsUsable = facetQueries.length > 0 && subEmbeddings.length === facetQueries.length && subEmbeddings.every((v) => v.length > 0 && v.length === queryEmbedding.length);
|
|
6493
|
+
if (subEmbeddings.length > 0 && !subEmbeddingsUsable) {
|
|
6494
|
+
getLogger().warn(
|
|
6495
|
+
`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`
|
|
6496
|
+
);
|
|
6497
|
+
}
|
|
6498
|
+
if (subEmbeddingsUsable) {
|
|
6499
|
+
const subQueries = facetQueries.map((sq, i) => ({
|
|
6500
|
+
query: sq,
|
|
6501
|
+
embedding: subEmbeddings[i]
|
|
6502
|
+
}));
|
|
6503
|
+
const v2HeadStats = { hadResults: false };
|
|
6504
|
+
const composite = await rankComposite(query, queryEmbedding, subQueries, embeddedItems, {
|
|
6505
|
+
limit,
|
|
6506
|
+
minSimilarity,
|
|
6507
|
+
rerank: !!searchOptions?.rerank,
|
|
6508
|
+
...searchOptions?.rerankTopN !== void 0 && {
|
|
6509
|
+
rerankTopN: searchOptions.rerankTopN
|
|
6510
|
+
},
|
|
6511
|
+
...searchOptions?.ceWeight !== void 0 && { ceWeight: searchOptions.ceWeight },
|
|
6512
|
+
...searchOptions?.mmr !== void 0 && { mmr: searchOptions.mmr },
|
|
6513
|
+
...tuning,
|
|
6514
|
+
...searchOptions?.entityRanking && { entityRanking: searchOptions.entityRanking },
|
|
6515
|
+
...searchOptions?.temporalRanking && { temporalRanking: searchOptions.temporalRanking },
|
|
6516
|
+
rerankStats,
|
|
6517
|
+
v2HeadStats
|
|
6518
|
+
});
|
|
6519
|
+
return stampTimestamps({
|
|
6520
|
+
results: composite,
|
|
6521
|
+
vaultSize,
|
|
6522
|
+
reranked: rerankStats.applied,
|
|
6523
|
+
hadV2Head: v2HeadStats.hadResults
|
|
6524
|
+
});
|
|
6257
6525
|
}
|
|
6258
6526
|
}
|
|
6259
6527
|
if (useFusion && searchOptions?.rerank) {
|
|
@@ -6313,6 +6581,12 @@ async function searchVaultMemoriesWithSize(query, vaultCtx, embeddingOptions, ca
|
|
|
6313
6581
|
rankedOnCosine: false
|
|
6314
6582
|
};
|
|
6315
6583
|
}
|
|
6584
|
+
if (searchOptions?.decompose === "llm" && normalizeSubQueries(searchOptions.subQueries).length < 2 && !warnedProgrammaticDecomposeIgnored) {
|
|
6585
|
+
warnedProgrammaticDecomposeIgnored = true;
|
|
6586
|
+
getLogger().warn(
|
|
6587
|
+
'memoryVault: decompose:"llm" is ignored by searchVaultMemories \u2014 pass subQueries or use createMemoryVaultSearchTool / createRecallTool (719/B4)'
|
|
6588
|
+
);
|
|
6589
|
+
}
|
|
6316
6590
|
const prepared = await prepareVaultCandidates(
|
|
6317
6591
|
query,
|
|
6318
6592
|
vaultCtx,
|
|
@@ -6418,7 +6692,8 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
|
|
|
6418
6692
|
return "Error: A search query is required.";
|
|
6419
6693
|
}
|
|
6420
6694
|
try {
|
|
6421
|
-
const
|
|
6695
|
+
const wantsDecompose = searchOptions?.decompose === "llm" && !!searchOptions.decomposeOptions;
|
|
6696
|
+
const budget = wantsDecompose ? "high" : searchOptions?.rerank ? "mid" : "low";
|
|
6422
6697
|
const folderId = searchOptions?.folderId ?? argFolderId;
|
|
6423
6698
|
if (searchOptions?.useFusion === false) {
|
|
6424
6699
|
const { results: legacy, embeddingsUnavailable } = await searchVaultMemoriesWithSize(
|
|
@@ -6442,7 +6717,14 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
|
|
|
6442
6717
|
legacy.map((r) => ({ id: r.uniqueId, content: r.content, score: r.similarity }))
|
|
6443
6718
|
);
|
|
6444
6719
|
}
|
|
6445
|
-
|
|
6720
|
+
let subQueries;
|
|
6721
|
+
if (wantsDecompose && searchOptions?.decomposeOptions) {
|
|
6722
|
+
const decomp = await decomposeQuery(query, searchOptions.decomposeOptions);
|
|
6723
|
+
if (decomp.mode === "composite" && decomp.subQueries.length >= 2) {
|
|
6724
|
+
subQueries = decomp.subQueries;
|
|
6725
|
+
}
|
|
6726
|
+
}
|
|
6727
|
+
const { recall: recall2 } = await import("./recall-DVX4W2UD.mjs");
|
|
6446
6728
|
let recallDegraded = [];
|
|
6447
6729
|
const result = await recall2(
|
|
6448
6730
|
query,
|
|
@@ -6458,9 +6740,7 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
|
|
|
6458
6740
|
...tuningForward,
|
|
6459
6741
|
...folderId !== void 0 && { folderId },
|
|
6460
6742
|
...searchOptions?.scopes && { scopes: searchOptions.scopes },
|
|
6461
|
-
...
|
|
6462
|
-
decomposeOptions: searchOptions.decomposeOptions
|
|
6463
|
-
}
|
|
6743
|
+
...subQueries && { subQueries }
|
|
6464
6744
|
}
|
|
6465
6745
|
);
|
|
6466
6746
|
if (result.vaultSize === 0) {
|
|
@@ -6988,12 +7268,12 @@ var DEFAULT_CHUNK_MIN_SCORE = 0.5;
|
|
|
6988
7268
|
function flagsForBudget(budget) {
|
|
6989
7269
|
switch (budget) {
|
|
6990
7270
|
case "high":
|
|
6991
|
-
return { rerank: true,
|
|
7271
|
+
return { rerank: true, traverse: true };
|
|
6992
7272
|
case "mid":
|
|
6993
|
-
return { rerank: true,
|
|
7273
|
+
return { rerank: true, traverse: false };
|
|
6994
7274
|
case "low":
|
|
6995
7275
|
default:
|
|
6996
|
-
return { rerank: false,
|
|
7276
|
+
return { rerank: false, traverse: false };
|
|
6997
7277
|
}
|
|
6998
7278
|
}
|
|
6999
7279
|
function dedupeBy(items, ...keys) {
|
|
@@ -7012,10 +7292,10 @@ function dedupeBy(items, ...keys) {
|
|
|
7012
7292
|
async function recall(query, ctx, options = {}) {
|
|
7013
7293
|
const types = options.types ?? ["fact"];
|
|
7014
7294
|
const limit = options.limit ?? DEFAULT_LIMIT;
|
|
7015
|
-
const
|
|
7016
|
-
const flags = flagsForBudget(
|
|
7017
|
-
const
|
|
7018
|
-
const
|
|
7295
|
+
const usedBudget = options.budget ?? DEFAULT_BUDGET;
|
|
7296
|
+
const flags = flagsForBudget(usedBudget);
|
|
7297
|
+
const normalizedFacets = normalizeSubQueries(options.subQueries);
|
|
7298
|
+
const subQueries = normalizedFacets.length >= 2 ? normalizedFacets : void 0;
|
|
7019
7299
|
const t0 = nowMs();
|
|
7020
7300
|
let prepMs = 0;
|
|
7021
7301
|
let factLaneMs = 0;
|
|
@@ -7036,7 +7316,9 @@ async function recall(query, ctx, options = {}) {
|
|
|
7036
7316
|
if (flags.rerank && factResults.length > 0 && hadV2Head && !didRerank) {
|
|
7037
7317
|
degraded.push("rerank-unavailable");
|
|
7038
7318
|
}
|
|
7039
|
-
if (
|
|
7319
|
+
if (usedBudget === "high" && options.decomposeOptions && !subQueries && !options.graphRefine) {
|
|
7320
|
+
degraded.push("decompose-moved");
|
|
7321
|
+
}
|
|
7040
7322
|
if (embeddingsUnavailable) degraded.push("embeddings-unavailable");
|
|
7041
7323
|
const diagnostics = {
|
|
7042
7324
|
usedBudget,
|
|
@@ -7149,10 +7431,8 @@ async function recall(query, ctx, options = {}) {
|
|
|
7149
7431
|
},
|
|
7150
7432
|
...options.rrfK !== void 0 && { rrfK: options.rrfK },
|
|
7151
7433
|
...options.decryptLast !== void 0 && { decryptLast: options.decryptLast },
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
decomposeOptions: options.decomposeOptions
|
|
7155
|
-
},
|
|
7434
|
+
// 719/B4 — composite facets arrive pre-built; no LLM call here.
|
|
7435
|
+
...subQueries && { subQueries },
|
|
7156
7436
|
...options.scopes && { scopes: options.scopes },
|
|
7157
7437
|
...options.folderId !== void 0 && { folderId: options.folderId },
|
|
7158
7438
|
...options.factTypes?.length && { factTypes: options.factTypes },
|
|
@@ -7382,6 +7662,7 @@ function toChunkMemory(r) {
|
|
|
7382
7662
|
|
|
7383
7663
|
export {
|
|
7384
7664
|
createSseClient,
|
|
7665
|
+
EncryptionKeyMissingError,
|
|
7385
7666
|
onClearAllEncryptionState,
|
|
7386
7667
|
onKeyAvailable,
|
|
7387
7668
|
clearEncryptionKey,
|
|
@@ -7394,11 +7675,13 @@ export {
|
|
|
7394
7675
|
decryptDataBytes,
|
|
7395
7676
|
decryptDataBytesFromBytes,
|
|
7396
7677
|
hasEncryptionKey,
|
|
7678
|
+
seedEncryptionKeys,
|
|
7397
7679
|
encryptDataWithKey,
|
|
7398
7680
|
decryptDataWithKey,
|
|
7399
7681
|
encryptDataBatch,
|
|
7400
7682
|
decryptDataBatch,
|
|
7401
7683
|
requestEncryptionKey,
|
|
7684
|
+
refreshEncryptionKeyIfMatches,
|
|
7402
7685
|
requestKeyPair,
|
|
7403
7686
|
exportPublicKey,
|
|
7404
7687
|
hasKeyPair,
|
|
@@ -7522,6 +7805,7 @@ export {
|
|
|
7522
7805
|
EMBEDDINGS_DEGRADED_EMPTY,
|
|
7523
7806
|
RerankerUnavailableError,
|
|
7524
7807
|
isRerankerAvailable,
|
|
7808
|
+
decomposeQuery,
|
|
7525
7809
|
DEFAULT_VAULT_CACHE_SIZE,
|
|
7526
7810
|
LRUCache,
|
|
7527
7811
|
createVaultEmbeddingCache,
|