@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.
@@ -276,14 +276,11 @@ function notifyKeyAvailable(address) {
276
276
  }
277
277
  }
278
278
  }
279
- function getStoredKey(address) {
280
- const keys = encryptionKeyStore.get(address);
281
- return keys?.current ?? null;
282
- }
283
279
  function getStoredKeyByVersion(address, version) {
284
280
  const keys = encryptionKeyStore.get(address);
285
281
  if (!keys) return null;
286
- return version === "v2" ? keys.legacy : keys.current;
282
+ const value = version === "v2" ? keys.legacy : keys.current;
283
+ return value ?? null;
287
284
  }
288
285
  function setStoredKey(address, keys) {
289
286
  encryptionKeyStore.set(address, keys);
@@ -292,6 +289,8 @@ function clearEncryptionKey(address) {
292
289
  encryptionKeyStore.delete(address);
293
290
  cryptoKeyCache.delete(`${address}:v2`);
294
291
  cryptoKeyCache.delete(`${address}:v3`);
292
+ refreshCandidatesByAddress.delete(address);
293
+ divergentDeriveAddresses.delete(address);
295
294
  }
296
295
  function clearAllEncryptionState() {
297
296
  sessionEpoch += 1;
@@ -299,6 +298,9 @@ function clearAllEncryptionState() {
299
298
  cryptoKeyCache.clear();
300
299
  keyAvailableCallbacks.clear();
301
300
  pendingKeyRequests.clear();
301
+ pendingCandidateDerives.clear();
302
+ refreshCandidatesByAddress.clear();
303
+ divergentDeriveAddresses.clear();
302
304
  clearAllKeyPairs();
303
305
  for (const cb of clearAllEncryptionStateCallbacks) {
304
306
  try {
@@ -482,9 +484,7 @@ async function getEncryptionKey(address, version = "v3") {
482
484
  }
483
485
  const keyHex = getStoredKeyByVersion(address, version);
484
486
  if (!keyHex) {
485
- throw new Error(
486
- "Encryption key not found. Please sign a message to generate your encryption key."
487
- );
487
+ throw new EncryptionKeyMissingError(address, version);
488
488
  }
489
489
  const keyBytes = hexToBytes(keyHex);
490
490
  const cryptoKey = await crypto.subtle.importKey(
@@ -548,8 +548,42 @@ async function decryptDataBytesFromBytes(encrypted, address, version = "v3") {
548
548
  );
549
549
  return new Uint8Array(decryptedData);
550
550
  }
551
- function hasEncryptionKey(address) {
552
- return getStoredKey(address) !== null;
551
+ function hasEncryptionKey(address, version) {
552
+ return getStoredKeyByVersion(address, version ?? "v3") !== null;
553
+ }
554
+ function seedEncryptionKeys(address, keys) {
555
+ if (!isValidWalletAddress(address)) {
556
+ throw new Error(
557
+ `Invalid wallet address: ${address}. Address must start with 0x and be 42 characters (0x + 40 hex characters).`
558
+ );
559
+ }
560
+ if (!keys.legacy && !keys.current) {
561
+ throw new Error("seedEncryptionKeys: at least one of legacy or current is required");
562
+ }
563
+ if (keys.legacy !== void 0) {
564
+ assertAes256KeyHex("legacy", keys.legacy);
565
+ }
566
+ if (keys.current !== void 0) {
567
+ assertAes256KeyHex("current", keys.current);
568
+ }
569
+ const existing = encryptionKeyStore.get(address);
570
+ const next = {
571
+ legacy: keys.legacy ?? existing?.legacy,
572
+ current: keys.current ?? existing?.current
573
+ };
574
+ setStoredKey(address, next);
575
+ if (keys.legacy !== void 0) cryptoKeyCache.delete(`${address}:v2`);
576
+ if (keys.current !== void 0) cryptoKeyCache.delete(`${address}:v3`);
577
+ refreshCandidatesByAddress.delete(address);
578
+ divergentDeriveAddresses.delete(address);
579
+ notifyKeyAvailable(address);
580
+ }
581
+ function assertAes256KeyHex(field10, value) {
582
+ if (!AES_256_KEY_HEX.test(value)) {
583
+ throw new Error(
584
+ `seedEncryptionKeys: ${field10} must be 64 hex characters (32-byte AES key), got length ${value.length}`
585
+ );
586
+ }
553
587
  }
554
588
  async function encryptBytesWithKey(plaintext, key) {
555
589
  const plaintextBytes = typeof plaintext === "string" ? SHARED_TEXT_ENCODER.encode(plaintext) : plaintext;
@@ -598,20 +632,23 @@ async function decryptDataBatch(encryptedValues, address) {
598
632
  const key = await getEncryptionKey(address);
599
633
  return Promise.all(encryptedValues.map((value) => decryptDataWithKey(value, key)));
600
634
  }
601
- async function requestEncryptionKey(walletAddress, signMessage, embeddedWalletSigner) {
635
+ async function requestEncryptionKey(walletAddress, signMessage, embeddedWalletSigner, options) {
602
636
  if (!isValidWalletAddress(walletAddress)) {
603
637
  throw new Error(
604
638
  `Invalid wallet address: ${walletAddress}. Address must start with 0x and be 42 characters (0x + 40 hex characters).`
605
639
  );
606
640
  }
607
- const existingKey = getStoredKey(walletAddress);
608
- if (existingKey) {
609
- return;
641
+ if (!options?.force && getStoredKeyByVersion(walletAddress, "v3") && getStoredKeyByVersion(walletAddress, "v2")) {
642
+ return true;
610
643
  }
611
- const pending = pendingKeyRequests.get(walletAddress);
612
- if (pending) {
613
- await pending;
614
- return;
644
+ if (!options?.force && divergentDeriveAddresses.has(walletAddress)) {
645
+ return false;
646
+ }
647
+ if (!options?.force) {
648
+ const pending = pendingKeyRequests.get(walletAddress);
649
+ if (pending) {
650
+ return pending;
651
+ }
615
652
  }
616
653
  const startEpoch = sessionEpoch;
617
654
  const promise = (async () => {
@@ -639,20 +676,143 @@ async function requestEncryptionKey(walletAddress, signMessage, embeddedWalletSi
639
676
  deriveKeyFromSignatureV3(signature)
640
677
  ]);
641
678
  if (sessionEpoch !== startEpoch) {
642
- return;
679
+ return false;
643
680
  }
644
- setStoredKey(walletAddress, { legacy: legacyKey, current: currentKey });
681
+ const existing = encryptionKeyStore.get(walletAddress);
682
+ if (!options?.force && existing && (existing.legacy || existing.current)) {
683
+ const matchesCurrent = Boolean(existing.current && existing.current === currentKey);
684
+ const matchesLegacy = Boolean(existing.legacy && existing.legacy === legacyKey);
685
+ if (!matchesCurrent && !matchesLegacy) {
686
+ getLogger().warn(
687
+ "requestEncryptionKey: derived keys do not match existing store; leaving unchanged"
688
+ );
689
+ divergentDeriveAddresses.add(walletAddress);
690
+ return false;
691
+ }
692
+ setStoredKey(walletAddress, {
693
+ legacy: existing.legacy ?? legacyKey,
694
+ current: existing.current ?? currentKey
695
+ });
696
+ } else {
697
+ setStoredKey(walletAddress, { legacy: legacyKey, current: currentKey });
698
+ }
699
+ cryptoKeyCache.delete(`${walletAddress}:v2`);
700
+ cryptoKeyCache.delete(`${walletAddress}:v3`);
701
+ refreshCandidatesByAddress.delete(walletAddress);
702
+ divergentDeriveAddresses.delete(walletAddress);
645
703
  notifyKeyAvailable(walletAddress);
704
+ return true;
646
705
  })();
647
706
  pendingKeyRequests.set(walletAddress, promise);
648
707
  try {
649
- await promise;
708
+ return await promise;
650
709
  } finally {
651
710
  if (pendingKeyRequests.get(walletAddress) === promise) {
652
711
  pendingKeyRequests.delete(walletAddress);
653
712
  }
654
713
  }
655
714
  }
715
+ async function refreshEncryptionKeyIfMatches(walletAddress, probeCiphertext, signMessage, embeddedWalletSigner) {
716
+ if (!isValidWalletAddress(walletAddress)) {
717
+ throw new Error(
718
+ `Invalid wallet address: ${walletAddress}. Address must start with 0x and be 42 characters (0x + 40 hex characters).`
719
+ );
720
+ }
721
+ const parsed = parsePrefixedCiphertext(probeCiphertext);
722
+ if (!parsed) {
723
+ return false;
724
+ }
725
+ const cached = refreshCandidatesByAddress.get(walletAddress);
726
+ if (cached) {
727
+ return tryCommitCandidatesIfProbeMatches(walletAddress, parsed, cached);
728
+ }
729
+ let derivePromise = pendingCandidateDerives.get(walletAddress);
730
+ if (!derivePromise) {
731
+ derivePromise = (async () => {
732
+ const candidates2 = await deriveRefreshCandidates(signMessage, embeddedWalletSigner);
733
+ if (candidates2) {
734
+ refreshCandidatesByAddress.set(walletAddress, candidates2);
735
+ }
736
+ return candidates2;
737
+ })().finally(() => {
738
+ if (pendingCandidateDerives.get(walletAddress) === derivePromise) {
739
+ pendingCandidateDerives.delete(walletAddress);
740
+ }
741
+ });
742
+ pendingCandidateDerives.set(walletAddress, derivePromise);
743
+ }
744
+ const candidates = await derivePromise;
745
+ if (!candidates) {
746
+ return false;
747
+ }
748
+ return tryCommitCandidatesIfProbeMatches(walletAddress, parsed, candidates);
749
+ }
750
+ function parsePrefixedCiphertext(probeCiphertext) {
751
+ const version = probeCiphertext.startsWith("enc:v3:") ? "v3" : probeCiphertext.startsWith("enc:v2:") ? "v2" : null;
752
+ if (!version) return null;
753
+ const encryptedData = probeCiphertext.slice(
754
+ version === "v3" ? "enc:v3:".length : "enc:v2:".length
755
+ );
756
+ if (encryptedData.length < 56 || !/^[0-9a-f]+$/i.test(encryptedData)) {
757
+ return null;
758
+ }
759
+ return { version, encryptedData };
760
+ }
761
+ async function deriveRefreshCandidates(signMessage, embeddedWalletSigner) {
762
+ const startEpoch = sessionEpoch;
763
+ const signOptions = { showWalletUIs: false };
764
+ let signature;
765
+ try {
766
+ if (embeddedWalletSigner) {
767
+ signature = await embeddedWalletSigner(SIGN_MESSAGE, signOptions);
768
+ } else {
769
+ signature = await signMessage(SIGN_MESSAGE, signOptions);
770
+ }
771
+ } catch (error) {
772
+ if (embeddedWalletSigner && error instanceof Error) {
773
+ getLogger().warn(
774
+ "Embedded wallet signing failed during key refresh, falling back to standard signMessage:",
775
+ error.message
776
+ );
777
+ signature = await signMessage(SIGN_MESSAGE, signOptions);
778
+ } else {
779
+ throw error;
780
+ }
781
+ }
782
+ const [legacyKey, currentKey] = await Promise.all([
783
+ deriveKeyFromSignature(signature),
784
+ deriveKeyFromSignatureV3(signature)
785
+ ]);
786
+ if (sessionEpoch !== startEpoch) {
787
+ return null;
788
+ }
789
+ return { legacy: legacyKey, current: currentKey };
790
+ }
791
+ async function tryCommitCandidatesIfProbeMatches(walletAddress, parsed, candidates) {
792
+ const candidateHex = parsed.version === "v2" ? candidates.legacy : candidates.current;
793
+ const keyBytes = hexToBytes(candidateHex);
794
+ const candidateCryptoKey = await crypto.subtle.importKey(
795
+ "raw",
796
+ keyBytes.buffer,
797
+ { name: "AES-GCM" },
798
+ false,
799
+ ["encrypt", "decrypt"]
800
+ );
801
+ try {
802
+ await decryptDataWithKey(parsed.encryptedData, candidateCryptoKey);
803
+ } catch {
804
+ getLogger().warn(
805
+ "refreshEncryptionKeyIfMatches: candidate keys failed to decrypt probe; leaving store unchanged"
806
+ );
807
+ return false;
808
+ }
809
+ setStoredKey(walletAddress, { legacy: candidates.legacy, current: candidates.current });
810
+ cryptoKeyCache.delete(`${walletAddress}:v2`);
811
+ cryptoKeyCache.delete(`${walletAddress}:v3`);
812
+ divergentDeriveAddresses.delete(walletAddress);
813
+ notifyKeyAvailable(walletAddress);
814
+ return true;
815
+ }
656
816
  async function persistKeyPair(address, startEpoch) {
657
817
  if (typeof window === "undefined") {
658
818
  return;
@@ -886,15 +1046,28 @@ function useEncryption(signMessage, embeddedWalletSigner) {
886
1046
  clearKeyPair: (walletAddress) => clearKeyPair(walletAddress)
887
1047
  };
888
1048
  }
889
- var SIGN_MESSAGE, encryptionKeyStore, pendingKeyRequests, sessionEpoch, cryptoKeyCache, keyAvailableCallbacks, clearAllEncryptionStateCallbacks, keyPairStore, SHARED_TEXT_ENCODER, SHARED_TEXT_DECODER, KEYPAIR_STORAGE_PREFIX;
1049
+ var SIGN_MESSAGE, EncryptionKeyMissingError, encryptionKeyStore, pendingKeyRequests, pendingCandidateDerives, refreshCandidatesByAddress, divergentDeriveAddresses, sessionEpoch, cryptoKeyCache, keyAvailableCallbacks, clearAllEncryptionStateCallbacks, keyPairStore, SHARED_TEXT_ENCODER, SHARED_TEXT_DECODER, AES_256_KEY_HEX, KEYPAIR_STORAGE_PREFIX;
890
1050
  var init_useEncryption = __esm({
891
1051
  "src/react/useEncryption.ts"() {
892
1052
  "use strict";
893
1053
  "use client";
894
1054
  init_logger();
895
1055
  SIGN_MESSAGE = "The app is asking you to sign this message to generate a key, which will be used to encrypt data.";
1056
+ EncryptionKeyMissingError = class extends Error {
1057
+ constructor(address, version) {
1058
+ super(
1059
+ `Encryption key (${version}) not found for ${address}. Please sign a message to generate your encryption key.`
1060
+ );
1061
+ this.name = "EncryptionKeyMissingError";
1062
+ this.address = address;
1063
+ this.version = version;
1064
+ }
1065
+ };
896
1066
  encryptionKeyStore = /* @__PURE__ */ new Map();
897
1067
  pendingKeyRequests = /* @__PURE__ */ new Map();
1068
+ pendingCandidateDerives = /* @__PURE__ */ new Map();
1069
+ refreshCandidatesByAddress = /* @__PURE__ */ new Map();
1070
+ divergentDeriveAddresses = /* @__PURE__ */ new Set();
898
1071
  sessionEpoch = 0;
899
1072
  cryptoKeyCache = /* @__PURE__ */ new Map();
900
1073
  keyAvailableCallbacks = /* @__PURE__ */ new Map();
@@ -902,6 +1075,7 @@ var init_useEncryption = __esm({
902
1075
  keyPairStore = /* @__PURE__ */ new Map();
903
1076
  SHARED_TEXT_ENCODER = new TextEncoder();
904
1077
  SHARED_TEXT_DECODER = new TextDecoder();
1078
+ AES_256_KEY_HEX = /^[0-9a-fA-F]{64}$/;
905
1079
  KEYPAIR_STORAGE_PREFIX = "ecdh_keypair_";
906
1080
  }
907
1081
  });
@@ -954,20 +1128,29 @@ async function encryptField(value, address, signMessage, embeddedWalletSigner, s
954
1128
  const encrypted = await encryptData(value, address);
955
1129
  return `${ENCRYPTION_PREFIX}${encrypted}`;
956
1130
  }
957
- async function decryptField(value, address) {
958
- if (!value) return value;
1131
+ async function decryptFieldDetailed(value, address) {
1132
+ if (!value) return { status: "plaintext", value };
959
1133
  const detected = detectEncryptionVersion(value);
960
- if (!detected) return value;
1134
+ if (!detected) return { status: "plaintext", value };
961
1135
  if (detected.encryptedData.length < 56 || !/^[0-9a-f]+$/i.test(detected.encryptedData)) {
962
- return value;
1136
+ return { status: "invalid_payload", value, version: detected.version };
963
1137
  }
964
1138
  try {
965
- return await decryptData(detected.encryptedData, address, detected.version);
1139
+ const plaintext = await decryptData(detected.encryptedData, address, detected.version);
1140
+ return { status: "ok", value: plaintext, version: detected.version };
966
1141
  } catch (error) {
967
- getLogger().warn("Failed to decrypt field, returning as-is:", error);
968
- return value;
1142
+ if (error instanceof EncryptionKeyMissingError) {
1143
+ getLogger().warn("Failed to decrypt field (key missing for version):", error.message);
1144
+ return { status: "key_missing", value, version: detected.version };
1145
+ }
1146
+ getLogger().warn("Failed to decrypt field (auth mismatch or crypto error):", error);
1147
+ return { status: "auth_mismatch", value, version: detected.version };
969
1148
  }
970
1149
  }
1150
+ async function decryptField(value, address) {
1151
+ const result = await decryptFieldDetailed(value, address);
1152
+ return result.value;
1153
+ }
971
1154
  async function encryptJsonField(value, address, signMessage, embeddedWalletSigner, skipKeyRequest) {
972
1155
  if (!value) return void 0;
973
1156
  const jsonString = JSON.stringify(value);
@@ -1084,20 +1267,40 @@ async function encryptMessageFields(message, address, signMessage, embeddedWalle
1084
1267
  throw error;
1085
1268
  }
1086
1269
  }
1087
- async function decryptMaybeJsonField(value, address) {
1088
- if (value === void 0) return void 0;
1089
- if (value === null) return value;
1270
+ async function decryptMaybeJsonFieldDetailed(value, address) {
1271
+ if (value === void 0) return { value: void 0, status: "plaintext" };
1272
+ if (value === null) return { value, status: "plaintext" };
1090
1273
  if (typeof value === "string") {
1091
1274
  if (isEncrypted(value)) {
1092
- return await decryptJsonField(value, address);
1275
+ const detailed = await decryptFieldDetailed(value, address);
1276
+ if (detailed.status !== "ok") {
1277
+ return { value: void 0, status: detailed.status, probe: value };
1278
+ }
1279
+ try {
1280
+ return { value: JSON.parse(detailed.value), status: "ok" };
1281
+ } catch (error) {
1282
+ getLogger().warn("Failed to parse decrypted JSON field:", error);
1283
+ return { value: void 0, status: "invalid_payload", probe: value };
1284
+ }
1093
1285
  }
1094
1286
  try {
1095
- return JSON.parse(value);
1287
+ return { value: JSON.parse(value), status: "plaintext" };
1096
1288
  } catch {
1097
- return void 0;
1289
+ return { value: void 0, status: "plaintext" };
1098
1290
  }
1099
1291
  }
1100
- return value;
1292
+ return { value, status: "plaintext" };
1293
+ }
1294
+ function worseDecryptStatus(a, b) {
1295
+ if (b === "ok" || b === "plaintext") return a;
1296
+ if (!a || a === "ok" || a === "plaintext") return b;
1297
+ const rank = (s) => {
1298
+ if (s === "auth_mismatch") return 3;
1299
+ if (s === "key_missing") return 2;
1300
+ if (s === "invalid_payload") return 1;
1301
+ return 0;
1302
+ };
1303
+ return rank(b) >= rank(a) ? b : a;
1101
1304
  }
1102
1305
  async function decryptMessageFields(message, address, signMessage, embeddedWalletSigner) {
1103
1306
  if (!address) {
@@ -1110,33 +1313,82 @@ async function decryptMessageFields(message, address, signMessage, embeddedWalle
1110
1313
  getLogger().warn("Failed to request encryption key for decryption:", error);
1111
1314
  }
1112
1315
  }
1113
- const decryptedContent = await decryptField(message.content, address);
1114
- const decryptedThinking = message.thinking ? await decryptField(message.thinking, address) : message.thinking;
1115
- const decryptedVector = await decryptMaybeJsonField(
1316
+ let assembled = await assembleDecryptedMessage(message, address);
1317
+ const needsRefresh = assembled.decryptionStatus === "auth_mismatch" || assembled.decryptionStatus === "key_missing";
1318
+ if (needsRefresh && signMessage && assembled.refreshProbe) {
1319
+ try {
1320
+ const refreshed = await refreshEncryptionKeyIfMatches(
1321
+ address,
1322
+ assembled.refreshProbe,
1323
+ signMessage,
1324
+ embeddedWalletSigner
1325
+ );
1326
+ if (refreshed) {
1327
+ assembled = await assembleDecryptedMessage(message, address);
1328
+ }
1329
+ } catch (error) {
1330
+ getLogger().warn("Failed to refresh encryption key after decrypt failure:", error);
1331
+ }
1332
+ }
1333
+ const { refreshProbe: _probe, ...result } = assembled;
1334
+ return result;
1335
+ }
1336
+ async function assembleDecryptedMessage(message, address) {
1337
+ const contentResult = await decryptFieldDetailed(message.content, address);
1338
+ const thinkingResult = message.thinking ? await decryptFieldDetailed(message.thinking, address) : null;
1339
+ const vectorResult = await decryptMaybeJsonFieldDetailed(
1116
1340
  message.vector,
1117
1341
  address
1118
1342
  );
1119
- const decryptedChunks = await decryptMaybeJsonField(
1343
+ const chunksResult = await decryptMaybeJsonFieldDetailed(
1120
1344
  message.chunks,
1121
1345
  address
1122
1346
  );
1123
- const decryptedSources = await decryptMaybeJsonField(
1347
+ const sourcesResult = await decryptMaybeJsonFieldDetailed(
1124
1348
  message.sources,
1125
1349
  address
1126
1350
  );
1127
- const decryptedThoughtProcess = await decryptMaybeJsonField(
1351
+ const thoughtProcessResult = await decryptMaybeJsonFieldDetailed(
1128
1352
  message.thoughtProcess,
1129
1353
  address
1130
1354
  );
1131
- return {
1355
+ let decryptionStatus;
1356
+ let refreshProbe;
1357
+ const consider = (status, probe) => {
1358
+ decryptionStatus = worseDecryptStatus(decryptionStatus, status);
1359
+ if (!refreshProbe && probe && (status === "auth_mismatch" || status === "key_missing")) {
1360
+ refreshProbe = probe;
1361
+ }
1362
+ };
1363
+ consider(contentResult.status, isEncrypted(message.content) ? message.content : void 0);
1364
+ if (thinkingResult && message.thinking && typeof message.thinking === "string") {
1365
+ consider(thinkingResult.status, isEncrypted(message.thinking) ? message.thinking : void 0);
1366
+ }
1367
+ if (vectorResult.probe) consider(vectorResult.status, vectorResult.probe);
1368
+ if (chunksResult.probe) consider(chunksResult.status, chunksResult.probe);
1369
+ if (sourcesResult.probe) consider(sourcesResult.status, sourcesResult.probe);
1370
+ if (thoughtProcessResult.probe) {
1371
+ consider(thoughtProcessResult.status, thoughtProcessResult.probe);
1372
+ }
1373
+ const statusForConsumer = decryptionStatus === "ok" || decryptionStatus === "plaintext" || !decryptionStatus ? void 0 : decryptionStatus;
1374
+ const result = {
1132
1375
  ...message,
1133
- content: decryptedContent,
1134
- thinking: decryptedThinking,
1135
- vector: decryptedVector,
1136
- chunks: decryptedChunks,
1137
- sources: decryptedSources,
1138
- thoughtProcess: decryptedThoughtProcess
1376
+ content: contentResult.value,
1377
+ thinking: thinkingResult ? thinkingResult.value : message.thinking,
1378
+ vector: vectorResult.value,
1379
+ chunks: chunksResult.value,
1380
+ sources: sourcesResult.value,
1381
+ thoughtProcess: thoughtProcessResult.value
1139
1382
  };
1383
+ if (statusForConsumer) {
1384
+ result.decryptionStatus = statusForConsumer;
1385
+ } else {
1386
+ delete result.decryptionStatus;
1387
+ }
1388
+ if (refreshProbe) {
1389
+ result.refreshProbe = refreshProbe;
1390
+ }
1391
+ return result;
1140
1392
  }
1141
1393
  var init_encryption = __esm({
1142
1394
  "src/lib/db/chat/encryption.ts"() {
@@ -6432,11 +6684,11 @@ async function decomposeQuery(query, options) {
6432
6684
  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.
6433
6685
 
6434
6686
  Query: ${trimmed}`,
6435
- // Decompose runs on the recall hot path — tighter than the
6687
+ // Decompose runs ahead of recall in the tool layer — tighter than the
6436
6688
  // portalLlm default (consolidate/extract can wait longer).
6437
6689
  timeoutMs: 2e4,
6438
6690
  // No internal retry: a failure degrades to the safe `{specific,[query]}`
6439
- // fallback below, so retrying would only add latency on the hot path.
6691
+ // fallback below. The agent/tool loop is the retry surface (719/B4).
6440
6692
  maxAttempts: 1,
6441
6693
  tag: "memory/decompose",
6442
6694
  ...options.fetchFn && { fetchFn: options.fetchFn }
@@ -6448,12 +6700,28 @@ Query: ${trimmed}`,
6448
6700
  if (parsed === null) return fallback;
6449
6701
  return validate(parsed, trimmed) ?? fallback;
6450
6702
  }
6703
+ function normalizeSubQueries(raw) {
6704
+ if (!raw || raw.length === 0) return [];
6705
+ const seen = /* @__PURE__ */ new Set();
6706
+ const out = [];
6707
+ for (const s of raw) {
6708
+ if (typeof s !== "string") continue;
6709
+ const t = s.trim();
6710
+ if (t.length === 0) continue;
6711
+ const key = t.toLowerCase();
6712
+ if (seen.has(key)) continue;
6713
+ seen.add(key);
6714
+ out.push(t);
6715
+ if (out.length >= MAX_SUB_QUERIES) break;
6716
+ }
6717
+ return out;
6718
+ }
6451
6719
  function validate(parsed, originalQuery) {
6452
6720
  if (typeof parsed !== "object" || parsed === null) return null;
6453
6721
  const obj = parsed;
6454
6722
  const mode = obj.mode === "composite" ? "composite" : "specific";
6455
6723
  if (!Array.isArray(obj.subQueries)) return null;
6456
- const subQueries = obj.subQueries.filter((s) => typeof s === "string").map((s) => s.trim()).filter((s) => s.length > 0).slice(0, MAX_SUB_QUERIES);
6724
+ const subQueries = normalizeSubQueries(obj.subQueries);
6457
6725
  if (subQueries.length === 0) return null;
6458
6726
  if (mode === "specific") {
6459
6727
  return { mode: "specific", subQueries: [originalQuery] };
@@ -7070,12 +7338,12 @@ __export(recall_exports, {
7070
7338
  function flagsForBudget(budget) {
7071
7339
  switch (budget) {
7072
7340
  case "high":
7073
- return { rerank: true, decompose: true, traverse: true };
7341
+ return { rerank: true, traverse: true };
7074
7342
  case "mid":
7075
- return { rerank: true, decompose: false, traverse: false };
7343
+ return { rerank: true, traverse: false };
7076
7344
  case "low":
7077
7345
  default:
7078
- return { rerank: false, decompose: false, traverse: false };
7346
+ return { rerank: false, traverse: false };
7079
7347
  }
7080
7348
  }
7081
7349
  function dedupeBy(items, ...keys) {
@@ -7094,10 +7362,10 @@ function dedupeBy(items, ...keys) {
7094
7362
  async function recall(query, ctx, options = {}) {
7095
7363
  const types = options.types ?? ["fact"];
7096
7364
  const limit = options.limit ?? DEFAULT_LIMIT;
7097
- const requestedBudget = options.budget ?? DEFAULT_BUDGET;
7098
- const flags = flagsForBudget(requestedBudget);
7099
- const decomposeAvailable = flags.decompose && !!options.decomposeOptions;
7100
- const usedBudget = requestedBudget === "high" && !decomposeAvailable ? "mid" : requestedBudget;
7365
+ const usedBudget = options.budget ?? DEFAULT_BUDGET;
7366
+ const flags = flagsForBudget(usedBudget);
7367
+ const normalizedFacets = normalizeSubQueries(options.subQueries);
7368
+ const subQueries = normalizedFacets.length >= 2 ? normalizedFacets : void 0;
7101
7369
  const t0 = nowMs();
7102
7370
  let prepMs = 0;
7103
7371
  let factLaneMs = 0;
@@ -7118,7 +7386,9 @@ async function recall(query, ctx, options = {}) {
7118
7386
  if (flags.rerank && factResults.length > 0 && hadV2Head && !didRerank) {
7119
7387
  degraded.push("rerank-unavailable");
7120
7388
  }
7121
- if (flags.decompose && !decomposeAvailable) degraded.push("decompose-unavailable");
7389
+ if (usedBudget === "high" && options.decomposeOptions && !subQueries && !options.graphRefine) {
7390
+ degraded.push("decompose-moved");
7391
+ }
7122
7392
  if (embeddingsUnavailable) degraded.push("embeddings-unavailable");
7123
7393
  const diagnostics = {
7124
7394
  usedBudget,
@@ -7231,10 +7501,8 @@ async function recall(query, ctx, options = {}) {
7231
7501
  },
7232
7502
  ...options.rrfK !== void 0 && { rrfK: options.rrfK },
7233
7503
  ...options.decryptLast !== void 0 && { decryptLast: options.decryptLast },
7234
- ...decomposeAvailable && {
7235
- decompose: "llm",
7236
- decomposeOptions: options.decomposeOptions
7237
- },
7504
+ // 719/B4 — composite facets arrive pre-built; no LLM call here.
7505
+ ...subQueries && { subQueries },
7238
7506
  ...options.scopes && { scopes: options.scopes },
7239
7507
  ...options.folderId !== void 0 && { folderId: options.folderId },
7240
7508
  ...options.factTypes?.length && { factTypes: options.factTypes },
@@ -7471,6 +7739,7 @@ var init_recall = __esm({
7471
7739
  init_logger();
7472
7740
  init_constants();
7473
7741
  init_embeddings();
7742
+ init_decomposeQuery();
7474
7743
  init_searchTool();
7475
7744
  init_graphTraversal();
7476
7745
  init_observationTrend();
@@ -8416,52 +8685,51 @@ async function rankPreparedVaultCandidates(query, prepared, embeddingOptions, se
8416
8685
  ...searchOptions?.rrfK !== void 0 && { rrfK: searchOptions.rrfK },
8417
8686
  ...searchOptions?.factTypeWeights && { factTypeWeights: searchOptions.factTypeWeights }
8418
8687
  };
8419
- if (useFusion && !embeddingsUnavailable && searchOptions?.decompose === "llm" && searchOptions.decomposeOptions) {
8420
- const decomp = await decomposeQuery(query, searchOptions.decomposeOptions);
8421
- if (decomp.mode === "composite") {
8422
- let subEmbeddings;
8423
- try {
8424
- subEmbeddings = await generateEmbeddings(decomp.subQueries, embeddingOptions);
8425
- } catch (err) {
8426
- getLogger().warn(
8427
- "memoryVault: sub-query embedding failed \u2014 falling back to single-query ranking: " + (err instanceof Error ? err.message : String(err))
8428
- );
8429
- subEmbeddings = [];
8430
- }
8431
- const subEmbeddingsUsable = decomp.subQueries.length > 0 && subEmbeddings.length === decomp.subQueries.length && subEmbeddings.every((v) => v.length > 0 && v.length === queryEmbedding.length);
8432
- if (subEmbeddings.length > 0 && !subEmbeddingsUsable) {
8433
- getLogger().warn(
8434
- `memoryVault: sub-query embedding returned ${subEmbeddings.length} vectors (dims ${subEmbeddings.map((v) => v.length).join(",")}) for ${decomp.subQueries.length} sub-queries against a ${queryEmbedding.length}-dim query \u2014 falling back to single-query ranking`
8435
- );
8436
- }
8437
- if (subEmbeddingsUsable) {
8438
- const subQueries = decomp.subQueries.map((sq, i) => ({
8439
- query: sq,
8440
- embedding: subEmbeddings[i]
8441
- }));
8442
- const v2HeadStats = { hadResults: false };
8443
- const composite = await rankComposite(query, queryEmbedding, subQueries, embeddedItems, {
8444
- limit,
8445
- minSimilarity,
8446
- rerank: !!searchOptions.rerank,
8447
- ...searchOptions.rerankTopN !== void 0 && {
8448
- rerankTopN: searchOptions.rerankTopN
8449
- },
8450
- ...searchOptions.ceWeight !== void 0 && { ceWeight: searchOptions.ceWeight },
8451
- ...searchOptions.mmr !== void 0 && { mmr: searchOptions.mmr },
8452
- ...tuning,
8453
- ...searchOptions.entityRanking && { entityRanking: searchOptions.entityRanking },
8454
- ...searchOptions.temporalRanking && { temporalRanking: searchOptions.temporalRanking },
8455
- rerankStats,
8456
- v2HeadStats
8457
- });
8458
- return stampTimestamps({
8459
- results: composite,
8460
- vaultSize,
8461
- reranked: rerankStats.applied,
8462
- hadV2Head: v2HeadStats.hadResults
8463
- });
8464
- }
8688
+ const normalizedFacets = normalizeSubQueries(searchOptions?.subQueries);
8689
+ const facetQueries = normalizedFacets.length >= 2 ? normalizedFacets : void 0;
8690
+ if (useFusion && !embeddingsUnavailable && facetQueries) {
8691
+ let subEmbeddings;
8692
+ try {
8693
+ subEmbeddings = await generateEmbeddings(facetQueries, embeddingOptions);
8694
+ } catch (err) {
8695
+ getLogger().warn(
8696
+ "memoryVault: sub-query embedding failed \u2014 falling back to single-query ranking: " + (err instanceof Error ? err.message : String(err))
8697
+ );
8698
+ subEmbeddings = [];
8699
+ }
8700
+ const subEmbeddingsUsable = facetQueries.length > 0 && subEmbeddings.length === facetQueries.length && subEmbeddings.every((v) => v.length > 0 && v.length === queryEmbedding.length);
8701
+ if (subEmbeddings.length > 0 && !subEmbeddingsUsable) {
8702
+ getLogger().warn(
8703
+ `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`
8704
+ );
8705
+ }
8706
+ if (subEmbeddingsUsable) {
8707
+ const subQueries = facetQueries.map((sq, i) => ({
8708
+ query: sq,
8709
+ embedding: subEmbeddings[i]
8710
+ }));
8711
+ const v2HeadStats = { hadResults: false };
8712
+ const composite = await rankComposite(query, queryEmbedding, subQueries, embeddedItems, {
8713
+ limit,
8714
+ minSimilarity,
8715
+ rerank: !!searchOptions?.rerank,
8716
+ ...searchOptions?.rerankTopN !== void 0 && {
8717
+ rerankTopN: searchOptions.rerankTopN
8718
+ },
8719
+ ...searchOptions?.ceWeight !== void 0 && { ceWeight: searchOptions.ceWeight },
8720
+ ...searchOptions?.mmr !== void 0 && { mmr: searchOptions.mmr },
8721
+ ...tuning,
8722
+ ...searchOptions?.entityRanking && { entityRanking: searchOptions.entityRanking },
8723
+ ...searchOptions?.temporalRanking && { temporalRanking: searchOptions.temporalRanking },
8724
+ rerankStats,
8725
+ v2HeadStats
8726
+ });
8727
+ return stampTimestamps({
8728
+ results: composite,
8729
+ vaultSize,
8730
+ reranked: rerankStats.applied,
8731
+ hadV2Head: v2HeadStats.hadResults
8732
+ });
8465
8733
  }
8466
8734
  }
8467
8735
  if (useFusion && searchOptions?.rerank) {
@@ -8521,6 +8789,12 @@ async function searchVaultMemoriesWithSize(query, vaultCtx, embeddingOptions, ca
8521
8789
  rankedOnCosine: false
8522
8790
  };
8523
8791
  }
8792
+ if (searchOptions?.decompose === "llm" && normalizeSubQueries(searchOptions.subQueries).length < 2 && !warnedProgrammaticDecomposeIgnored) {
8793
+ warnedProgrammaticDecomposeIgnored = true;
8794
+ getLogger().warn(
8795
+ 'memoryVault: decompose:"llm" is ignored by searchVaultMemories \u2014 pass subQueries or use createMemoryVaultSearchTool / createRecallTool (719/B4)'
8796
+ );
8797
+ }
8524
8798
  const prepared = await prepareVaultCandidates(
8525
8799
  query,
8526
8800
  vaultCtx,
@@ -8625,7 +8899,8 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
8625
8899
  return "Error: A search query is required.";
8626
8900
  }
8627
8901
  try {
8628
- const budget = searchOptions?.decompose === "llm" && searchOptions.decomposeOptions ? "high" : searchOptions?.rerank ? "mid" : "low";
8902
+ const wantsDecompose = searchOptions?.decompose === "llm" && !!searchOptions.decomposeOptions;
8903
+ const budget = wantsDecompose ? "high" : searchOptions?.rerank ? "mid" : "low";
8629
8904
  const folderId = searchOptions?.folderId ?? argFolderId;
8630
8905
  if (searchOptions?.useFusion === false) {
8631
8906
  const { results: legacy, embeddingsUnavailable } = await searchVaultMemoriesWithSize(
@@ -8649,6 +8924,13 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
8649
8924
  legacy.map((r) => ({ id: r.uniqueId, content: r.content, score: r.similarity }))
8650
8925
  );
8651
8926
  }
8927
+ let subQueries;
8928
+ if (wantsDecompose && searchOptions?.decomposeOptions) {
8929
+ const decomp = await decomposeQuery(query, searchOptions.decomposeOptions);
8930
+ if (decomp.mode === "composite" && decomp.subQueries.length >= 2) {
8931
+ subQueries = decomp.subQueries;
8932
+ }
8933
+ }
8652
8934
  const { recall: recall2 } = await Promise.resolve().then(() => (init_recall(), recall_exports));
8653
8935
  let recallDegraded = [];
8654
8936
  const result = await recall2(
@@ -8665,9 +8947,7 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
8665
8947
  ...tuningForward,
8666
8948
  ...folderId !== void 0 && { folderId },
8667
8949
  ...searchOptions?.scopes && { scopes: searchOptions.scopes },
8668
- ...searchOptions?.decompose === "llm" && searchOptions.decomposeOptions && {
8669
- decomposeOptions: searchOptions.decomposeOptions
8670
- }
8950
+ ...subQueries && { subQueries }
8671
8951
  }
8672
8952
  );
8673
8953
  if (result.vaultSize === 0) {
@@ -8697,7 +8977,7 @@ ${formatted}`;
8697
8977
  }
8698
8978
  };
8699
8979
  }
8700
- var SUPERSESSION_SIMILARITY_THRESHOLD, SUPERSESSION_MIN_AGE_GAP_MS, SUPERSESSION_BOOST_FACTOR, SUPERSESSION_MAX_WINDOW, EMBEDDINGS_DEGRADED_EMPTY_NO_LEXICAL;
8980
+ var warnedProgrammaticDecomposeIgnored, SUPERSESSION_SIMILARITY_THRESHOLD, SUPERSESSION_MIN_AGE_GAP_MS, SUPERSESSION_BOOST_FACTOR, SUPERSESSION_MAX_WINDOW, EMBEDDINGS_DEGRADED_EMPTY_NO_LEXICAL;
8701
8981
  var init_searchTool = __esm({
8702
8982
  "src/lib/memoryVault/searchTool.ts"() {
8703
8983
  "use strict";
@@ -8715,6 +8995,7 @@ var init_searchTool = __esm({
8715
8995
  init_bm25();
8716
8996
  init_decomposeQuery();
8717
8997
  init_lruCache();
8998
+ warnedProgrammaticDecomposeIgnored = false;
8718
8999
  SUPERSESSION_SIMILARITY_THRESHOLD = 0.7;
8719
9000
  SUPERSESSION_MIN_AGE_GAP_MS = 30 * 24 * 60 * 60 * 1e3;
8720
9001
  SUPERSESSION_BOOST_FACTOR = 0.8;
@@ -8938,6 +9219,7 @@ __export(index_exports, {
8938
9219
  DropboxAuthProvider: () => DropboxAuthProvider,
8939
9220
  ENTITY_FANOUT: () => ENTITY_FANOUT,
8940
9221
  ENTITY_KINDS: () => ENTITY_KINDS,
9222
+ EncryptionKeyMissingError: () => EncryptionKeyMissingError,
8941
9223
  EntityModel: () => Entity,
8942
9224
  ExcelProcessor: () => ExcelProcessor,
8943
9225
  FILE_PLACEHOLDER_PREFIX: () => FILE_PLACEHOLDER_PREFIX,
@@ -9228,6 +9510,7 @@ __export(index_exports, {
9228
9510
  reflect: () => reflect,
9229
9511
  refreshCalendarToken: () => refreshCalendarToken,
9230
9512
  refreshDriveToken: () => refreshDriveToken,
9513
+ refreshEncryptionKeyIfMatches: () => refreshEncryptionKeyIfMatches,
9231
9514
  refreshGithubToken: () => refreshGithubToken,
9232
9515
  refreshNotionToken: () => refreshNotionToken,
9233
9516
  relinkMemoryTopicsOp: () => relinkMemoryTopicsOp,
@@ -9259,6 +9542,7 @@ __export(index_exports, {
9259
9542
  searchMediaOp: () => searchMediaOp,
9260
9543
  searchMessagesOp: () => searchMessagesOp,
9261
9544
  searchVaultMemories: () => searchVaultMemories,
9545
+ seedEncryptionKeys: () => seedEncryptionKeys,
9262
9546
  selectServerToolsForPrompt: () => selectServerToolsForPrompt,
9263
9547
  serializeJsx: () => serializeJsx,
9264
9548
  setLogger: () => setLogger,
@@ -17759,6 +18043,7 @@ function rankProfileCandidates(inputs, now = Date.now(), options = {}) {
17759
18043
  init_recall();
17760
18044
 
17761
18045
  // src/lib/memory/recallTool.ts
18046
+ init_decomposeQuery();
17762
18047
  init_recall();
17763
18048
  init_recallConstants();
17764
18049
  var DEFAULT_LIMIT2 = 8;
@@ -17856,7 +18141,7 @@ function createRecallTool(ctx, toolOptions, callbacks) {
17856
18141
  type: "function",
17857
18142
  function: {
17858
18143
  name: RECALL_TOOL_NAME,
17859
- description: "Search the user's memory across stored facts/preferences and past conversation excerpts. Returns a unified ranked list \u2014 facts carry an `id` you can reference; conversation excerpts carry a date and role. Use this whenever the user's question may relate to anything previously discussed or saved (preferences, prior decisions, past topics). Phrase the query naturally.",
18144
+ description: `Search the user's memory across stored facts/preferences and past conversation excerpts. Returns a unified ranked list \u2014 facts carry an \`id\` you can reference; conversation excerpts carry a date and role. Use this whenever the user's question may relate to anything previously discussed or saved (preferences, prior decisions, past topics). Phrase the query naturally. For multi-faceted / overview questions ("tell me about the user", "what's my tech stack"), prefer several targeted searches \u2014 one facet each (e.g. name, work, hobbies) \u2014 over a single broad query; fuse the results yourself.`,
17860
18145
  arguments: {
17861
18146
  type: "object",
17862
18147
  properties: {
@@ -17911,6 +18196,13 @@ function createRecallTool(ctx, toolOptions, callbacks) {
17911
18196
  let surfaced = 0;
17912
18197
  try {
17913
18198
  let recallDegraded = [];
18199
+ let subQueries;
18200
+ if (defaultBudget === "high" && toolOptions?.decomposeOptions) {
18201
+ const decomp = await decomposeQuery(query, toolOptions.decomposeOptions);
18202
+ if (decomp.mode === "composite" && decomp.subQueries.length >= 2) {
18203
+ subQueries = decomp.subQueries;
18204
+ }
18205
+ }
17914
18206
  const recallOpts = {
17915
18207
  onDiagnostics: (d) => {
17916
18208
  recallDegraded = d.degraded;
@@ -17924,10 +18216,12 @@ function createRecallTool(ctx, toolOptions, callbacks) {
17924
18216
  ...toolOptions?.excludeConversationId && {
17925
18217
  excludeConversationId: toolOptions.excludeConversationId
17926
18218
  },
17927
- ...toolOptions?.decomposeOptions && {
17928
- decomposeOptions: toolOptions.decomposeOptions
17929
- },
17930
- ...toolOptions?.now !== void 0 && { now: toolOptions.now }
18219
+ // Do NOT forward decomposeOptions into recall() — rewrite already
18220
+ // ran above, and forwarding would falsely trip `decompose-moved`
18221
+ // on every specific-mode high-budget call. graphRefine auth can
18222
+ // be threaded later when the tool opts into that path.
18223
+ ...toolOptions?.now !== void 0 && { now: toolOptions.now },
18224
+ ...subQueries && { subQueries }
17931
18225
  };
17932
18226
  const result = await recall(query, ctx, recallOpts);
17933
18227
  if (callbacks?.onChunksRetrieved) {
@@ -33898,6 +34192,7 @@ function renderAnumaJsx(jsx3) {
33898
34192
  DropboxAuthProvider,
33899
34193
  ENTITY_FANOUT,
33900
34194
  ENTITY_KINDS,
34195
+ EncryptionKeyMissingError,
33901
34196
  EntityModel,
33902
34197
  ExcelProcessor,
33903
34198
  FILE_PLACEHOLDER_PREFIX,
@@ -34188,6 +34483,7 @@ function renderAnumaJsx(jsx3) {
34188
34483
  reflect,
34189
34484
  refreshCalendarToken,
34190
34485
  refreshDriveToken,
34486
+ refreshEncryptionKeyIfMatches,
34191
34487
  refreshGithubToken,
34192
34488
  refreshNotionToken,
34193
34489
  relinkMemoryTopicsOp,
@@ -34219,6 +34515,7 @@ function renderAnumaJsx(jsx3) {
34219
34515
  searchMediaOp,
34220
34516
  searchMessagesOp,
34221
34517
  searchVaultMemories,
34518
+ seedEncryptionKeys,
34222
34519
  selectServerToolsForPrompt,
34223
34520
  serializeJsx,
34224
34521
  setLogger,