@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.
@@ -112,14 +112,11 @@ function notifyKeyAvailable(address) {
112
112
  }
113
113
  }
114
114
  }
115
- function getStoredKey(address) {
116
- const keys = encryptionKeyStore.get(address);
117
- return keys?.current ?? null;
118
- }
119
115
  function getStoredKeyByVersion(address, version) {
120
116
  const keys = encryptionKeyStore.get(address);
121
117
  if (!keys) return null;
122
- return version === "v2" ? keys.legacy : keys.current;
118
+ const value = version === "v2" ? keys.legacy : keys.current;
119
+ return value ?? null;
123
120
  }
124
121
  function setStoredKey(address, keys) {
125
122
  encryptionKeyStore.set(address, keys);
@@ -128,6 +125,8 @@ function clearEncryptionKey(address) {
128
125
  encryptionKeyStore.delete(address);
129
126
  cryptoKeyCache.delete(`${address}:v2`);
130
127
  cryptoKeyCache.delete(`${address}:v3`);
128
+ refreshCandidatesByAddress.delete(address);
129
+ divergentDeriveAddresses.delete(address);
131
130
  }
132
131
  function clearAllEncryptionState() {
133
132
  sessionEpoch += 1;
@@ -135,6 +134,9 @@ function clearAllEncryptionState() {
135
134
  cryptoKeyCache.clear();
136
135
  keyAvailableCallbacks.clear();
137
136
  pendingKeyRequests.clear();
137
+ pendingCandidateDerives.clear();
138
+ refreshCandidatesByAddress.clear();
139
+ divergentDeriveAddresses.clear();
138
140
  clearAllKeyPairs();
139
141
  for (const cb of clearAllEncryptionStateCallbacks) {
140
142
  try {
@@ -318,9 +320,7 @@ async function getEncryptionKey(address, version = "v3") {
318
320
  }
319
321
  const keyHex = getStoredKeyByVersion(address, version);
320
322
  if (!keyHex) {
321
- throw new Error(
322
- "Encryption key not found. Please sign a message to generate your encryption key."
323
- );
323
+ throw new EncryptionKeyMissingError(address, version);
324
324
  }
325
325
  const keyBytes = hexToBytes(keyHex);
326
326
  const cryptoKey = await crypto.subtle.importKey(
@@ -346,8 +346,42 @@ async function decryptData(encryptedHex, address, version = "v3") {
346
346
  const key = await getEncryptionKey(address, version);
347
347
  return decryptDataWithKey(encryptedHex, key);
348
348
  }
349
- function hasEncryptionKey(address) {
350
- return getStoredKey(address) !== null;
349
+ function hasEncryptionKey(address, version) {
350
+ return getStoredKeyByVersion(address, version ?? "v3") !== null;
351
+ }
352
+ function seedEncryptionKeys(address, keys) {
353
+ if (!isValidWalletAddress(address)) {
354
+ throw new Error(
355
+ `Invalid wallet address: ${address}. Address must start with 0x and be 42 characters (0x + 40 hex characters).`
356
+ );
357
+ }
358
+ if (!keys.legacy && !keys.current) {
359
+ throw new Error("seedEncryptionKeys: at least one of legacy or current is required");
360
+ }
361
+ if (keys.legacy !== void 0) {
362
+ assertAes256KeyHex("legacy", keys.legacy);
363
+ }
364
+ if (keys.current !== void 0) {
365
+ assertAes256KeyHex("current", keys.current);
366
+ }
367
+ const existing = encryptionKeyStore.get(address);
368
+ const next = {
369
+ legacy: keys.legacy ?? existing?.legacy,
370
+ current: keys.current ?? existing?.current
371
+ };
372
+ setStoredKey(address, next);
373
+ if (keys.legacy !== void 0) cryptoKeyCache.delete(`${address}:v2`);
374
+ if (keys.current !== void 0) cryptoKeyCache.delete(`${address}:v3`);
375
+ refreshCandidatesByAddress.delete(address);
376
+ divergentDeriveAddresses.delete(address);
377
+ notifyKeyAvailable(address);
378
+ }
379
+ function assertAes256KeyHex(field10, value) {
380
+ if (!AES_256_KEY_HEX.test(value)) {
381
+ throw new Error(
382
+ `seedEncryptionKeys: ${field10} must be 64 hex characters (32-byte AES key), got length ${value.length}`
383
+ );
384
+ }
351
385
  }
352
386
  async function encryptBytesWithKey(plaintext, key) {
353
387
  const plaintextBytes = typeof plaintext === "string" ? SHARED_TEXT_ENCODER.encode(plaintext) : plaintext;
@@ -383,20 +417,23 @@ async function decryptDataWithKey(encryptedHex, key) {
383
417
  );
384
418
  return SHARED_TEXT_DECODER.decode(decryptedData);
385
419
  }
386
- async function requestEncryptionKey(walletAddress, signMessage, embeddedWalletSigner) {
420
+ async function requestEncryptionKey(walletAddress, signMessage, embeddedWalletSigner, options) {
387
421
  if (!isValidWalletAddress(walletAddress)) {
388
422
  throw new Error(
389
423
  `Invalid wallet address: ${walletAddress}. Address must start with 0x and be 42 characters (0x + 40 hex characters).`
390
424
  );
391
425
  }
392
- const existingKey = getStoredKey(walletAddress);
393
- if (existingKey) {
394
- return;
426
+ if (!options?.force && getStoredKeyByVersion(walletAddress, "v3") && getStoredKeyByVersion(walletAddress, "v2")) {
427
+ return true;
395
428
  }
396
- const pending = pendingKeyRequests.get(walletAddress);
397
- if (pending) {
398
- await pending;
399
- return;
429
+ if (!options?.force && divergentDeriveAddresses.has(walletAddress)) {
430
+ return false;
431
+ }
432
+ if (!options?.force) {
433
+ const pending = pendingKeyRequests.get(walletAddress);
434
+ if (pending) {
435
+ return pending;
436
+ }
400
437
  }
401
438
  const startEpoch = sessionEpoch;
402
439
  const promise = (async () => {
@@ -424,20 +461,143 @@ async function requestEncryptionKey(walletAddress, signMessage, embeddedWalletSi
424
461
  deriveKeyFromSignatureV3(signature)
425
462
  ]);
426
463
  if (sessionEpoch !== startEpoch) {
427
- return;
464
+ return false;
428
465
  }
429
- setStoredKey(walletAddress, { legacy: legacyKey, current: currentKey });
466
+ const existing = encryptionKeyStore.get(walletAddress);
467
+ if (!options?.force && existing && (existing.legacy || existing.current)) {
468
+ const matchesCurrent = Boolean(existing.current && existing.current === currentKey);
469
+ const matchesLegacy = Boolean(existing.legacy && existing.legacy === legacyKey);
470
+ if (!matchesCurrent && !matchesLegacy) {
471
+ getLogger().warn(
472
+ "requestEncryptionKey: derived keys do not match existing store; leaving unchanged"
473
+ );
474
+ divergentDeriveAddresses.add(walletAddress);
475
+ return false;
476
+ }
477
+ setStoredKey(walletAddress, {
478
+ legacy: existing.legacy ?? legacyKey,
479
+ current: existing.current ?? currentKey
480
+ });
481
+ } else {
482
+ setStoredKey(walletAddress, { legacy: legacyKey, current: currentKey });
483
+ }
484
+ cryptoKeyCache.delete(`${walletAddress}:v2`);
485
+ cryptoKeyCache.delete(`${walletAddress}:v3`);
486
+ refreshCandidatesByAddress.delete(walletAddress);
487
+ divergentDeriveAddresses.delete(walletAddress);
430
488
  notifyKeyAvailable(walletAddress);
489
+ return true;
431
490
  })();
432
491
  pendingKeyRequests.set(walletAddress, promise);
433
492
  try {
434
- await promise;
493
+ return await promise;
435
494
  } finally {
436
495
  if (pendingKeyRequests.get(walletAddress) === promise) {
437
496
  pendingKeyRequests.delete(walletAddress);
438
497
  }
439
498
  }
440
499
  }
500
+ async function refreshEncryptionKeyIfMatches(walletAddress, probeCiphertext, signMessage, embeddedWalletSigner) {
501
+ if (!isValidWalletAddress(walletAddress)) {
502
+ throw new Error(
503
+ `Invalid wallet address: ${walletAddress}. Address must start with 0x and be 42 characters (0x + 40 hex characters).`
504
+ );
505
+ }
506
+ const parsed = parsePrefixedCiphertext(probeCiphertext);
507
+ if (!parsed) {
508
+ return false;
509
+ }
510
+ const cached = refreshCandidatesByAddress.get(walletAddress);
511
+ if (cached) {
512
+ return tryCommitCandidatesIfProbeMatches(walletAddress, parsed, cached);
513
+ }
514
+ let derivePromise = pendingCandidateDerives.get(walletAddress);
515
+ if (!derivePromise) {
516
+ derivePromise = (async () => {
517
+ const candidates2 = await deriveRefreshCandidates(signMessage, embeddedWalletSigner);
518
+ if (candidates2) {
519
+ refreshCandidatesByAddress.set(walletAddress, candidates2);
520
+ }
521
+ return candidates2;
522
+ })().finally(() => {
523
+ if (pendingCandidateDerives.get(walletAddress) === derivePromise) {
524
+ pendingCandidateDerives.delete(walletAddress);
525
+ }
526
+ });
527
+ pendingCandidateDerives.set(walletAddress, derivePromise);
528
+ }
529
+ const candidates = await derivePromise;
530
+ if (!candidates) {
531
+ return false;
532
+ }
533
+ return tryCommitCandidatesIfProbeMatches(walletAddress, parsed, candidates);
534
+ }
535
+ function parsePrefixedCiphertext(probeCiphertext) {
536
+ const version = probeCiphertext.startsWith("enc:v3:") ? "v3" : probeCiphertext.startsWith("enc:v2:") ? "v2" : null;
537
+ if (!version) return null;
538
+ const encryptedData = probeCiphertext.slice(
539
+ version === "v3" ? "enc:v3:".length : "enc:v2:".length
540
+ );
541
+ if (encryptedData.length < 56 || !/^[0-9a-f]+$/i.test(encryptedData)) {
542
+ return null;
543
+ }
544
+ return { version, encryptedData };
545
+ }
546
+ async function deriveRefreshCandidates(signMessage, embeddedWalletSigner) {
547
+ const startEpoch = sessionEpoch;
548
+ const signOptions = { showWalletUIs: false };
549
+ let signature;
550
+ try {
551
+ if (embeddedWalletSigner) {
552
+ signature = await embeddedWalletSigner(SIGN_MESSAGE, signOptions);
553
+ } else {
554
+ signature = await signMessage(SIGN_MESSAGE, signOptions);
555
+ }
556
+ } catch (error) {
557
+ if (embeddedWalletSigner && error instanceof Error) {
558
+ getLogger().warn(
559
+ "Embedded wallet signing failed during key refresh, falling back to standard signMessage:",
560
+ error.message
561
+ );
562
+ signature = await signMessage(SIGN_MESSAGE, signOptions);
563
+ } else {
564
+ throw error;
565
+ }
566
+ }
567
+ const [legacyKey, currentKey] = await Promise.all([
568
+ deriveKeyFromSignature(signature),
569
+ deriveKeyFromSignatureV3(signature)
570
+ ]);
571
+ if (sessionEpoch !== startEpoch) {
572
+ return null;
573
+ }
574
+ return { legacy: legacyKey, current: currentKey };
575
+ }
576
+ async function tryCommitCandidatesIfProbeMatches(walletAddress, parsed, candidates) {
577
+ const candidateHex = parsed.version === "v2" ? candidates.legacy : candidates.current;
578
+ const keyBytes = hexToBytes(candidateHex);
579
+ const candidateCryptoKey = await crypto.subtle.importKey(
580
+ "raw",
581
+ keyBytes.buffer,
582
+ { name: "AES-GCM" },
583
+ false,
584
+ ["encrypt", "decrypt"]
585
+ );
586
+ try {
587
+ await decryptDataWithKey(parsed.encryptedData, candidateCryptoKey);
588
+ } catch {
589
+ getLogger().warn(
590
+ "refreshEncryptionKeyIfMatches: candidate keys failed to decrypt probe; leaving store unchanged"
591
+ );
592
+ return false;
593
+ }
594
+ setStoredKey(walletAddress, { legacy: candidates.legacy, current: candidates.current });
595
+ cryptoKeyCache.delete(`${walletAddress}:v2`);
596
+ cryptoKeyCache.delete(`${walletAddress}:v3`);
597
+ divergentDeriveAddresses.delete(walletAddress);
598
+ notifyKeyAvailable(walletAddress);
599
+ return true;
600
+ }
441
601
  async function persistKeyPair(address, startEpoch) {
442
602
  if (typeof window === "undefined") {
443
603
  return;
@@ -671,15 +831,28 @@ function useEncryption(signMessage, embeddedWalletSigner) {
671
831
  clearKeyPair: (walletAddress) => clearKeyPair(walletAddress)
672
832
  };
673
833
  }
674
- var SIGN_MESSAGE, encryptionKeyStore, pendingKeyRequests, sessionEpoch, cryptoKeyCache, keyAvailableCallbacks, clearAllEncryptionStateCallbacks, keyPairStore, SHARED_TEXT_ENCODER, SHARED_TEXT_DECODER, KEYPAIR_STORAGE_PREFIX;
834
+ 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;
675
835
  var init_useEncryption = __esm({
676
836
  "src/react/useEncryption.ts"() {
677
837
  "use strict";
678
838
  "use client";
679
839
  init_logger();
680
840
  SIGN_MESSAGE = "The app is asking you to sign this message to generate a key, which will be used to encrypt data.";
841
+ EncryptionKeyMissingError = class extends Error {
842
+ constructor(address, version) {
843
+ super(
844
+ `Encryption key (${version}) not found for ${address}. Please sign a message to generate your encryption key.`
845
+ );
846
+ this.name = "EncryptionKeyMissingError";
847
+ this.address = address;
848
+ this.version = version;
849
+ }
850
+ };
681
851
  encryptionKeyStore = /* @__PURE__ */ new Map();
682
852
  pendingKeyRequests = /* @__PURE__ */ new Map();
853
+ pendingCandidateDerives = /* @__PURE__ */ new Map();
854
+ refreshCandidatesByAddress = /* @__PURE__ */ new Map();
855
+ divergentDeriveAddresses = /* @__PURE__ */ new Set();
683
856
  sessionEpoch = 0;
684
857
  cryptoKeyCache = /* @__PURE__ */ new Map();
685
858
  keyAvailableCallbacks = /* @__PURE__ */ new Map();
@@ -687,6 +860,7 @@ var init_useEncryption = __esm({
687
860
  keyPairStore = /* @__PURE__ */ new Map();
688
861
  SHARED_TEXT_ENCODER = new TextEncoder();
689
862
  SHARED_TEXT_DECODER = new TextDecoder();
863
+ AES_256_KEY_HEX = /^[0-9a-fA-F]{64}$/;
690
864
  KEYPAIR_STORAGE_PREFIX = "ecdh_keypair_";
691
865
  }
692
866
  });
@@ -739,20 +913,29 @@ async function encryptField(value, address, signMessage, embeddedWalletSigner, s
739
913
  const encrypted = await encryptData(value, address);
740
914
  return `${ENCRYPTION_PREFIX}${encrypted}`;
741
915
  }
742
- async function decryptField(value, address) {
743
- if (!value) return value;
916
+ async function decryptFieldDetailed(value, address) {
917
+ if (!value) return { status: "plaintext", value };
744
918
  const detected = detectEncryptionVersion(value);
745
- if (!detected) return value;
919
+ if (!detected) return { status: "plaintext", value };
746
920
  if (detected.encryptedData.length < 56 || !/^[0-9a-f]+$/i.test(detected.encryptedData)) {
747
- return value;
921
+ return { status: "invalid_payload", value, version: detected.version };
748
922
  }
749
923
  try {
750
- return await decryptData(detected.encryptedData, address, detected.version);
924
+ const plaintext = await decryptData(detected.encryptedData, address, detected.version);
925
+ return { status: "ok", value: plaintext, version: detected.version };
751
926
  } catch (error) {
752
- getLogger().warn("Failed to decrypt field, returning as-is:", error);
753
- return value;
927
+ if (error instanceof EncryptionKeyMissingError) {
928
+ getLogger().warn("Failed to decrypt field (key missing for version):", error.message);
929
+ return { status: "key_missing", value, version: detected.version };
930
+ }
931
+ getLogger().warn("Failed to decrypt field (auth mismatch or crypto error):", error);
932
+ return { status: "auth_mismatch", value, version: detected.version };
754
933
  }
755
934
  }
935
+ async function decryptField(value, address) {
936
+ const result = await decryptFieldDetailed(value, address);
937
+ return result.value;
938
+ }
756
939
  async function encryptJsonField(value, address, signMessage, embeddedWalletSigner, skipKeyRequest) {
757
940
  if (!value) return void 0;
758
941
  const jsonString = JSON.stringify(value);
@@ -869,20 +1052,40 @@ async function encryptMessageFields(message, address, signMessage, embeddedWalle
869
1052
  throw error;
870
1053
  }
871
1054
  }
872
- async function decryptMaybeJsonField(value, address) {
873
- if (value === void 0) return void 0;
874
- if (value === null) return value;
1055
+ async function decryptMaybeJsonFieldDetailed(value, address) {
1056
+ if (value === void 0) return { value: void 0, status: "plaintext" };
1057
+ if (value === null) return { value, status: "plaintext" };
875
1058
  if (typeof value === "string") {
876
1059
  if (isEncrypted(value)) {
877
- return await decryptJsonField(value, address);
1060
+ const detailed = await decryptFieldDetailed(value, address);
1061
+ if (detailed.status !== "ok") {
1062
+ return { value: void 0, status: detailed.status, probe: value };
1063
+ }
1064
+ try {
1065
+ return { value: JSON.parse(detailed.value), status: "ok" };
1066
+ } catch (error) {
1067
+ getLogger().warn("Failed to parse decrypted JSON field:", error);
1068
+ return { value: void 0, status: "invalid_payload", probe: value };
1069
+ }
878
1070
  }
879
1071
  try {
880
- return JSON.parse(value);
1072
+ return { value: JSON.parse(value), status: "plaintext" };
881
1073
  } catch {
882
- return void 0;
1074
+ return { value: void 0, status: "plaintext" };
883
1075
  }
884
1076
  }
885
- return value;
1077
+ return { value, status: "plaintext" };
1078
+ }
1079
+ function worseDecryptStatus(a, b) {
1080
+ if (b === "ok" || b === "plaintext") return a;
1081
+ if (!a || a === "ok" || a === "plaintext") return b;
1082
+ const rank = (s) => {
1083
+ if (s === "auth_mismatch") return 3;
1084
+ if (s === "key_missing") return 2;
1085
+ if (s === "invalid_payload") return 1;
1086
+ return 0;
1087
+ };
1088
+ return rank(b) >= rank(a) ? b : a;
886
1089
  }
887
1090
  async function decryptMessageFields(message, address, signMessage, embeddedWalletSigner) {
888
1091
  if (!address) {
@@ -895,33 +1098,82 @@ async function decryptMessageFields(message, address, signMessage, embeddedWalle
895
1098
  getLogger().warn("Failed to request encryption key for decryption:", error);
896
1099
  }
897
1100
  }
898
- const decryptedContent = await decryptField(message.content, address);
899
- const decryptedThinking = message.thinking ? await decryptField(message.thinking, address) : message.thinking;
900
- const decryptedVector = await decryptMaybeJsonField(
1101
+ let assembled = await assembleDecryptedMessage(message, address);
1102
+ const needsRefresh = assembled.decryptionStatus === "auth_mismatch" || assembled.decryptionStatus === "key_missing";
1103
+ if (needsRefresh && signMessage && assembled.refreshProbe) {
1104
+ try {
1105
+ const refreshed = await refreshEncryptionKeyIfMatches(
1106
+ address,
1107
+ assembled.refreshProbe,
1108
+ signMessage,
1109
+ embeddedWalletSigner
1110
+ );
1111
+ if (refreshed) {
1112
+ assembled = await assembleDecryptedMessage(message, address);
1113
+ }
1114
+ } catch (error) {
1115
+ getLogger().warn("Failed to refresh encryption key after decrypt failure:", error);
1116
+ }
1117
+ }
1118
+ const { refreshProbe: _probe, ...result } = assembled;
1119
+ return result;
1120
+ }
1121
+ async function assembleDecryptedMessage(message, address) {
1122
+ const contentResult = await decryptFieldDetailed(message.content, address);
1123
+ const thinkingResult = message.thinking ? await decryptFieldDetailed(message.thinking, address) : null;
1124
+ const vectorResult = await decryptMaybeJsonFieldDetailed(
901
1125
  message.vector,
902
1126
  address
903
1127
  );
904
- const decryptedChunks = await decryptMaybeJsonField(
1128
+ const chunksResult = await decryptMaybeJsonFieldDetailed(
905
1129
  message.chunks,
906
1130
  address
907
1131
  );
908
- const decryptedSources = await decryptMaybeJsonField(
1132
+ const sourcesResult = await decryptMaybeJsonFieldDetailed(
909
1133
  message.sources,
910
1134
  address
911
1135
  );
912
- const decryptedThoughtProcess = await decryptMaybeJsonField(
1136
+ const thoughtProcessResult = await decryptMaybeJsonFieldDetailed(
913
1137
  message.thoughtProcess,
914
1138
  address
915
1139
  );
916
- return {
1140
+ let decryptionStatus;
1141
+ let refreshProbe;
1142
+ const consider = (status, probe) => {
1143
+ decryptionStatus = worseDecryptStatus(decryptionStatus, status);
1144
+ if (!refreshProbe && probe && (status === "auth_mismatch" || status === "key_missing")) {
1145
+ refreshProbe = probe;
1146
+ }
1147
+ };
1148
+ consider(contentResult.status, isEncrypted(message.content) ? message.content : void 0);
1149
+ if (thinkingResult && message.thinking && typeof message.thinking === "string") {
1150
+ consider(thinkingResult.status, isEncrypted(message.thinking) ? message.thinking : void 0);
1151
+ }
1152
+ if (vectorResult.probe) consider(vectorResult.status, vectorResult.probe);
1153
+ if (chunksResult.probe) consider(chunksResult.status, chunksResult.probe);
1154
+ if (sourcesResult.probe) consider(sourcesResult.status, sourcesResult.probe);
1155
+ if (thoughtProcessResult.probe) {
1156
+ consider(thoughtProcessResult.status, thoughtProcessResult.probe);
1157
+ }
1158
+ const statusForConsumer = decryptionStatus === "ok" || decryptionStatus === "plaintext" || !decryptionStatus ? void 0 : decryptionStatus;
1159
+ const result = {
917
1160
  ...message,
918
- content: decryptedContent,
919
- thinking: decryptedThinking,
920
- vector: decryptedVector,
921
- chunks: decryptedChunks,
922
- sources: decryptedSources,
923
- thoughtProcess: decryptedThoughtProcess
1161
+ content: contentResult.value,
1162
+ thinking: thinkingResult ? thinkingResult.value : message.thinking,
1163
+ vector: vectorResult.value,
1164
+ chunks: chunksResult.value,
1165
+ sources: sourcesResult.value,
1166
+ thoughtProcess: thoughtProcessResult.value
924
1167
  };
1168
+ if (statusForConsumer) {
1169
+ result.decryptionStatus = statusForConsumer;
1170
+ } else {
1171
+ delete result.decryptionStatus;
1172
+ }
1173
+ if (refreshProbe) {
1174
+ result.refreshProbe = refreshProbe;
1175
+ }
1176
+ return result;
925
1177
  }
926
1178
  var init_encryption = __esm({
927
1179
  "src/lib/db/chat/encryption.ts"() {
@@ -6149,11 +6401,11 @@ async function decomposeQuery(query, options) {
6149
6401
  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.
6150
6402
 
6151
6403
  Query: ${trimmed}`,
6152
- // Decompose runs on the recall hot path — tighter than the
6404
+ // Decompose runs ahead of recall in the tool layer — tighter than the
6153
6405
  // portalLlm default (consolidate/extract can wait longer).
6154
6406
  timeoutMs: 2e4,
6155
6407
  // No internal retry: a failure degrades to the safe `{specific,[query]}`
6156
- // fallback below, so retrying would only add latency on the hot path.
6408
+ // fallback below. The agent/tool loop is the retry surface (719/B4).
6157
6409
  maxAttempts: 1,
6158
6410
  tag: "memory/decompose",
6159
6411
  ...options.fetchFn && { fetchFn: options.fetchFn }
@@ -6165,12 +6417,28 @@ Query: ${trimmed}`,
6165
6417
  if (parsed === null) return fallback;
6166
6418
  return validate(parsed, trimmed) ?? fallback;
6167
6419
  }
6420
+ function normalizeSubQueries(raw) {
6421
+ if (!raw || raw.length === 0) return [];
6422
+ const seen = /* @__PURE__ */ new Set();
6423
+ const out = [];
6424
+ for (const s of raw) {
6425
+ if (typeof s !== "string") continue;
6426
+ const t = s.trim();
6427
+ if (t.length === 0) continue;
6428
+ const key = t.toLowerCase();
6429
+ if (seen.has(key)) continue;
6430
+ seen.add(key);
6431
+ out.push(t);
6432
+ if (out.length >= MAX_SUB_QUERIES) break;
6433
+ }
6434
+ return out;
6435
+ }
6168
6436
  function validate(parsed, originalQuery) {
6169
6437
  if (typeof parsed !== "object" || parsed === null) return null;
6170
6438
  const obj = parsed;
6171
6439
  const mode = obj.mode === "composite" ? "composite" : "specific";
6172
6440
  if (!Array.isArray(obj.subQueries)) return null;
6173
- const subQueries = obj.subQueries.filter((s) => typeof s === "string").map((s) => s.trim()).filter((s) => s.length > 0).slice(0, MAX_SUB_QUERIES);
6441
+ const subQueries = normalizeSubQueries(obj.subQueries);
6174
6442
  if (subQueries.length === 0) return null;
6175
6443
  if (mode === "specific") {
6176
6444
  return { mode: "specific", subQueries: [originalQuery] };
@@ -6787,12 +7055,12 @@ __export(recall_exports, {
6787
7055
  function flagsForBudget(budget) {
6788
7056
  switch (budget) {
6789
7057
  case "high":
6790
- return { rerank: true, decompose: true, traverse: true };
7058
+ return { rerank: true, traverse: true };
6791
7059
  case "mid":
6792
- return { rerank: true, decompose: false, traverse: false };
7060
+ return { rerank: true, traverse: false };
6793
7061
  case "low":
6794
7062
  default:
6795
- return { rerank: false, decompose: false, traverse: false };
7063
+ return { rerank: false, traverse: false };
6796
7064
  }
6797
7065
  }
6798
7066
  function dedupeBy(items, ...keys) {
@@ -6811,10 +7079,10 @@ function dedupeBy(items, ...keys) {
6811
7079
  async function recall(query, ctx, options = {}) {
6812
7080
  const types = options.types ?? ["fact"];
6813
7081
  const limit = options.limit ?? DEFAULT_LIMIT;
6814
- const requestedBudget = options.budget ?? DEFAULT_BUDGET;
6815
- const flags = flagsForBudget(requestedBudget);
6816
- const decomposeAvailable = flags.decompose && !!options.decomposeOptions;
6817
- const usedBudget = requestedBudget === "high" && !decomposeAvailable ? "mid" : requestedBudget;
7082
+ const usedBudget = options.budget ?? DEFAULT_BUDGET;
7083
+ const flags = flagsForBudget(usedBudget);
7084
+ const normalizedFacets = normalizeSubQueries(options.subQueries);
7085
+ const subQueries = normalizedFacets.length >= 2 ? normalizedFacets : void 0;
6818
7086
  const t0 = nowMs();
6819
7087
  let prepMs = 0;
6820
7088
  let factLaneMs = 0;
@@ -6835,7 +7103,9 @@ async function recall(query, ctx, options = {}) {
6835
7103
  if (flags.rerank && factResults.length > 0 && hadV2Head && !didRerank) {
6836
7104
  degraded.push("rerank-unavailable");
6837
7105
  }
6838
- if (flags.decompose && !decomposeAvailable) degraded.push("decompose-unavailable");
7106
+ if (usedBudget === "high" && options.decomposeOptions && !subQueries && !options.graphRefine) {
7107
+ degraded.push("decompose-moved");
7108
+ }
6839
7109
  if (embeddingsUnavailable) degraded.push("embeddings-unavailable");
6840
7110
  const diagnostics = {
6841
7111
  usedBudget,
@@ -6948,10 +7218,8 @@ async function recall(query, ctx, options = {}) {
6948
7218
  },
6949
7219
  ...options.rrfK !== void 0 && { rrfK: options.rrfK },
6950
7220
  ...options.decryptLast !== void 0 && { decryptLast: options.decryptLast },
6951
- ...decomposeAvailable && {
6952
- decompose: "llm",
6953
- decomposeOptions: options.decomposeOptions
6954
- },
7221
+ // 719/B4 — composite facets arrive pre-built; no LLM call here.
7222
+ ...subQueries && { subQueries },
6955
7223
  ...options.scopes && { scopes: options.scopes },
6956
7224
  ...options.folderId !== void 0 && { folderId: options.folderId },
6957
7225
  ...options.factTypes?.length && { factTypes: options.factTypes },
@@ -7188,6 +7456,7 @@ var init_recall = __esm({
7188
7456
  init_logger();
7189
7457
  init_constants();
7190
7458
  init_embeddings();
7459
+ init_decomposeQuery();
7191
7460
  init_searchTool();
7192
7461
  init_graphTraversal();
7193
7462
  init_observationTrend();
@@ -8133,52 +8402,51 @@ async function rankPreparedVaultCandidates(query, prepared, embeddingOptions, se
8133
8402
  ...searchOptions?.rrfK !== void 0 && { rrfK: searchOptions.rrfK },
8134
8403
  ...searchOptions?.factTypeWeights && { factTypeWeights: searchOptions.factTypeWeights }
8135
8404
  };
8136
- if (useFusion && !embeddingsUnavailable && searchOptions?.decompose === "llm" && searchOptions.decomposeOptions) {
8137
- const decomp = await decomposeQuery(query, searchOptions.decomposeOptions);
8138
- if (decomp.mode === "composite") {
8139
- let subEmbeddings;
8140
- try {
8141
- subEmbeddings = await generateEmbeddings(decomp.subQueries, embeddingOptions);
8142
- } catch (err) {
8143
- getLogger().warn(
8144
- "memoryVault: sub-query embedding failed \u2014 falling back to single-query ranking: " + (err instanceof Error ? err.message : String(err))
8145
- );
8146
- subEmbeddings = [];
8147
- }
8148
- const subEmbeddingsUsable = decomp.subQueries.length > 0 && subEmbeddings.length === decomp.subQueries.length && subEmbeddings.every((v) => v.length > 0 && v.length === queryEmbedding.length);
8149
- if (subEmbeddings.length > 0 && !subEmbeddingsUsable) {
8150
- getLogger().warn(
8151
- `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`
8152
- );
8153
- }
8154
- if (subEmbeddingsUsable) {
8155
- const subQueries = decomp.subQueries.map((sq, i) => ({
8156
- query: sq,
8157
- embedding: subEmbeddings[i]
8158
- }));
8159
- const v2HeadStats = { hadResults: false };
8160
- const composite = await rankComposite(query, queryEmbedding, subQueries, embeddedItems, {
8161
- limit,
8162
- minSimilarity,
8163
- rerank: !!searchOptions.rerank,
8164
- ...searchOptions.rerankTopN !== void 0 && {
8165
- rerankTopN: searchOptions.rerankTopN
8166
- },
8167
- ...searchOptions.ceWeight !== void 0 && { ceWeight: searchOptions.ceWeight },
8168
- ...searchOptions.mmr !== void 0 && { mmr: searchOptions.mmr },
8169
- ...tuning,
8170
- ...searchOptions.entityRanking && { entityRanking: searchOptions.entityRanking },
8171
- ...searchOptions.temporalRanking && { temporalRanking: searchOptions.temporalRanking },
8172
- rerankStats,
8173
- v2HeadStats
8174
- });
8175
- return stampTimestamps({
8176
- results: composite,
8177
- vaultSize,
8178
- reranked: rerankStats.applied,
8179
- hadV2Head: v2HeadStats.hadResults
8180
- });
8181
- }
8405
+ const normalizedFacets = normalizeSubQueries(searchOptions?.subQueries);
8406
+ const facetQueries = normalizedFacets.length >= 2 ? normalizedFacets : void 0;
8407
+ if (useFusion && !embeddingsUnavailable && facetQueries) {
8408
+ let subEmbeddings;
8409
+ try {
8410
+ subEmbeddings = await generateEmbeddings(facetQueries, embeddingOptions);
8411
+ } catch (err) {
8412
+ getLogger().warn(
8413
+ "memoryVault: sub-query embedding failed \u2014 falling back to single-query ranking: " + (err instanceof Error ? err.message : String(err))
8414
+ );
8415
+ subEmbeddings = [];
8416
+ }
8417
+ const subEmbeddingsUsable = facetQueries.length > 0 && subEmbeddings.length === facetQueries.length && subEmbeddings.every((v) => v.length > 0 && v.length === queryEmbedding.length);
8418
+ if (subEmbeddings.length > 0 && !subEmbeddingsUsable) {
8419
+ getLogger().warn(
8420
+ `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`
8421
+ );
8422
+ }
8423
+ if (subEmbeddingsUsable) {
8424
+ const subQueries = facetQueries.map((sq, i) => ({
8425
+ query: sq,
8426
+ embedding: subEmbeddings[i]
8427
+ }));
8428
+ const v2HeadStats = { hadResults: false };
8429
+ const composite = await rankComposite(query, queryEmbedding, subQueries, embeddedItems, {
8430
+ limit,
8431
+ minSimilarity,
8432
+ rerank: !!searchOptions?.rerank,
8433
+ ...searchOptions?.rerankTopN !== void 0 && {
8434
+ rerankTopN: searchOptions.rerankTopN
8435
+ },
8436
+ ...searchOptions?.ceWeight !== void 0 && { ceWeight: searchOptions.ceWeight },
8437
+ ...searchOptions?.mmr !== void 0 && { mmr: searchOptions.mmr },
8438
+ ...tuning,
8439
+ ...searchOptions?.entityRanking && { entityRanking: searchOptions.entityRanking },
8440
+ ...searchOptions?.temporalRanking && { temporalRanking: searchOptions.temporalRanking },
8441
+ rerankStats,
8442
+ v2HeadStats
8443
+ });
8444
+ return stampTimestamps({
8445
+ results: composite,
8446
+ vaultSize,
8447
+ reranked: rerankStats.applied,
8448
+ hadV2Head: v2HeadStats.hadResults
8449
+ });
8182
8450
  }
8183
8451
  }
8184
8452
  if (useFusion && searchOptions?.rerank) {
@@ -8238,6 +8506,12 @@ async function searchVaultMemoriesWithSize(query, vaultCtx, embeddingOptions, ca
8238
8506
  rankedOnCosine: false
8239
8507
  };
8240
8508
  }
8509
+ if (searchOptions?.decompose === "llm" && normalizeSubQueries(searchOptions.subQueries).length < 2 && !warnedProgrammaticDecomposeIgnored) {
8510
+ warnedProgrammaticDecomposeIgnored = true;
8511
+ getLogger().warn(
8512
+ 'memoryVault: decompose:"llm" is ignored by searchVaultMemories \u2014 pass subQueries or use createMemoryVaultSearchTool / createRecallTool (719/B4)'
8513
+ );
8514
+ }
8241
8515
  const prepared = await prepareVaultCandidates(
8242
8516
  query,
8243
8517
  vaultCtx,
@@ -8342,7 +8616,8 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
8342
8616
  return "Error: A search query is required.";
8343
8617
  }
8344
8618
  try {
8345
- const budget = searchOptions?.decompose === "llm" && searchOptions.decomposeOptions ? "high" : searchOptions?.rerank ? "mid" : "low";
8619
+ const wantsDecompose = searchOptions?.decompose === "llm" && !!searchOptions.decomposeOptions;
8620
+ const budget = wantsDecompose ? "high" : searchOptions?.rerank ? "mid" : "low";
8346
8621
  const folderId = searchOptions?.folderId ?? argFolderId;
8347
8622
  if (searchOptions?.useFusion === false) {
8348
8623
  const { results: legacy, embeddingsUnavailable } = await searchVaultMemoriesWithSize(
@@ -8366,6 +8641,13 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
8366
8641
  legacy.map((r) => ({ id: r.uniqueId, content: r.content, score: r.similarity }))
8367
8642
  );
8368
8643
  }
8644
+ let subQueries;
8645
+ if (wantsDecompose && searchOptions?.decomposeOptions) {
8646
+ const decomp = await decomposeQuery(query, searchOptions.decomposeOptions);
8647
+ if (decomp.mode === "composite" && decomp.subQueries.length >= 2) {
8648
+ subQueries = decomp.subQueries;
8649
+ }
8650
+ }
8369
8651
  const { recall: recall2 } = await Promise.resolve().then(() => (init_recall(), recall_exports));
8370
8652
  let recallDegraded = [];
8371
8653
  const result = await recall2(
@@ -8382,9 +8664,7 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
8382
8664
  ...tuningForward,
8383
8665
  ...folderId !== void 0 && { folderId },
8384
8666
  ...searchOptions?.scopes && { scopes: searchOptions.scopes },
8385
- ...searchOptions?.decompose === "llm" && searchOptions.decomposeOptions && {
8386
- decomposeOptions: searchOptions.decomposeOptions
8387
- }
8667
+ ...subQueries && { subQueries }
8388
8668
  }
8389
8669
  );
8390
8670
  if (result.vaultSize === 0) {
@@ -8414,7 +8694,7 @@ ${formatted}`;
8414
8694
  }
8415
8695
  };
8416
8696
  }
8417
- var SUPERSESSION_SIMILARITY_THRESHOLD, SUPERSESSION_MIN_AGE_GAP_MS, SUPERSESSION_BOOST_FACTOR, SUPERSESSION_MAX_WINDOW, EMBEDDINGS_DEGRADED_EMPTY_NO_LEXICAL;
8697
+ var warnedProgrammaticDecomposeIgnored, SUPERSESSION_SIMILARITY_THRESHOLD, SUPERSESSION_MIN_AGE_GAP_MS, SUPERSESSION_BOOST_FACTOR, SUPERSESSION_MAX_WINDOW, EMBEDDINGS_DEGRADED_EMPTY_NO_LEXICAL;
8418
8698
  var init_searchTool = __esm({
8419
8699
  "src/lib/memoryVault/searchTool.ts"() {
8420
8700
  "use strict";
@@ -8432,6 +8712,7 @@ var init_searchTool = __esm({
8432
8712
  init_bm25();
8433
8713
  init_decomposeQuery();
8434
8714
  init_lruCache();
8715
+ warnedProgrammaticDecomposeIgnored = false;
8435
8716
  SUPERSESSION_SIMILARITY_THRESHOLD = 0.7;
8436
8717
  SUPERSESSION_MIN_AGE_GAP_MS = 30 * 24 * 60 * 60 * 1e3;
8437
8718
  SUPERSESSION_BOOST_FACTOR = 0.8;
@@ -8627,6 +8908,7 @@ __export(index_exports, {
8627
8908
  DEFAULT_VAULT_CACHE_SIZE: () => DEFAULT_VAULT_CACHE_SIZE,
8628
8909
  DatabaseManager: () => DatabaseManager,
8629
8910
  ENTITY_FANOUT: () => ENTITY_FANOUT,
8911
+ EncryptionKeyMissingError: () => EncryptionKeyMissingError,
8630
8912
  HARD_DELETE_WINDOW_MS: () => HARD_DELETE_WINDOW_MS,
8631
8913
  INFERENCE_ID_HEADER: () => INFERENCE_ID_HEADER,
8632
8914
  LoggerProvider: () => LoggerProvider,
@@ -8753,6 +9035,7 @@ __export(index_exports, {
8753
9035
  rankProfileCandidates: () => rankProfileCandidates,
8754
9036
  recall: () => recall,
8755
9037
  reflect: () => reflect,
9038
+ refreshEncryptionKeyIfMatches: () => refreshEncryptionKeyIfMatches,
8756
9039
  refreshNotionAccessToken: () => refreshNotionAccessToken,
8757
9040
  registerNotionClient: () => registerNotionClient,
8758
9041
  relinkMemoryTopicsOp: () => relinkMemoryTopicsOp,
@@ -8767,6 +9050,7 @@ __export(index_exports, {
8767
9050
  sdkModelClasses: () => sdkModelClasses,
8768
9051
  sdkSchema: () => sdkSchema,
8769
9052
  searchVaultMemories: () => searchVaultMemories,
9053
+ seedEncryptionKeys: () => seedEncryptionKeys,
8770
9054
  setLogger: () => setLogger,
8771
9055
  setMemoryVisibilityOp: () => setMemoryVisibilityOp,
8772
9056
  shouldChunkMessage: () => shouldChunkMessage,
@@ -16460,6 +16744,7 @@ function rankProfileCandidates(inputs, now = Date.now(), options = {}) {
16460
16744
  init_recall();
16461
16745
 
16462
16746
  // src/lib/memory/recallTool.ts
16747
+ init_decomposeQuery();
16463
16748
  init_recall();
16464
16749
  init_recallConstants();
16465
16750
  var DEFAULT_LIMIT2 = 8;
@@ -16557,7 +16842,7 @@ function createRecallTool(ctx, toolOptions, callbacks) {
16557
16842
  type: "function",
16558
16843
  function: {
16559
16844
  name: RECALL_TOOL_NAME,
16560
- 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.",
16845
+ 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.`,
16561
16846
  arguments: {
16562
16847
  type: "object",
16563
16848
  properties: {
@@ -16612,6 +16897,13 @@ function createRecallTool(ctx, toolOptions, callbacks) {
16612
16897
  let surfaced = 0;
16613
16898
  try {
16614
16899
  let recallDegraded = [];
16900
+ let subQueries;
16901
+ if (defaultBudget === "high" && toolOptions?.decomposeOptions) {
16902
+ const decomp = await decomposeQuery(query, toolOptions.decomposeOptions);
16903
+ if (decomp.mode === "composite" && decomp.subQueries.length >= 2) {
16904
+ subQueries = decomp.subQueries;
16905
+ }
16906
+ }
16615
16907
  const recallOpts = {
16616
16908
  onDiagnostics: (d) => {
16617
16909
  recallDegraded = d.degraded;
@@ -16625,10 +16917,12 @@ function createRecallTool(ctx, toolOptions, callbacks) {
16625
16917
  ...toolOptions?.excludeConversationId && {
16626
16918
  excludeConversationId: toolOptions.excludeConversationId
16627
16919
  },
16628
- ...toolOptions?.decomposeOptions && {
16629
- decomposeOptions: toolOptions.decomposeOptions
16630
- },
16631
- ...toolOptions?.now !== void 0 && { now: toolOptions.now }
16920
+ // Do NOT forward decomposeOptions into recall() — rewrite already
16921
+ // ran above, and forwarding would falsely trip `decompose-moved`
16922
+ // on every specific-mode high-budget call. graphRefine auth can
16923
+ // be threaded later when the tool opts into that path.
16924
+ ...toolOptions?.now !== void 0 && { now: toolOptions.now },
16925
+ ...subQueries && { subQueries }
16632
16926
  };
16633
16927
  const result = await recall(query, ctx, recallOpts);
16634
16928
  if (callbacks?.onChunksRetrieved) {
@@ -23032,6 +23326,7 @@ function createNotionTools(getAccessToken, requestNotionAccess) {
23032
23326
  DEFAULT_VAULT_CACHE_SIZE,
23033
23327
  DatabaseManager,
23034
23328
  ENTITY_FANOUT,
23329
+ EncryptionKeyMissingError,
23035
23330
  HARD_DELETE_WINDOW_MS,
23036
23331
  INFERENCE_ID_HEADER,
23037
23332
  LoggerProvider,
@@ -23158,6 +23453,7 @@ function createNotionTools(getAccessToken, requestNotionAccess) {
23158
23453
  rankProfileCandidates,
23159
23454
  recall,
23160
23455
  reflect,
23456
+ refreshEncryptionKeyIfMatches,
23161
23457
  refreshNotionAccessToken,
23162
23458
  registerNotionClient,
23163
23459
  relinkMemoryTopicsOp,
@@ -23172,6 +23468,7 @@ function createNotionTools(getAccessToken, requestNotionAccess) {
23172
23468
  sdkModelClasses,
23173
23469
  sdkSchema,
23174
23470
  searchVaultMemories,
23471
+ seedEncryptionKeys,
23175
23472
  setLogger,
23176
23473
  setMemoryVisibilityOp,
23177
23474
  shouldChunkMessage,