@anuma/sdk 9.1.2 → 9.1.4

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.
@@ -2326,6 +2326,7 @@ async function getEncryptionKey(address, version = "v3") {
2326
2326
  if (cachedKey) {
2327
2327
  return cachedKey;
2328
2328
  }
2329
+ const startEpoch = sessionEpoch;
2329
2330
  const keyHex = getStoredKeyByVersion(address, version);
2330
2331
  if (!keyHex) {
2331
2332
  throw new EncryptionKeyMissingError(address, version);
@@ -2338,6 +2339,9 @@ async function getEncryptionKey(address, version = "v3") {
2338
2339
  false,
2339
2340
  ["encrypt", "decrypt"]
2340
2341
  );
2342
+ if (sessionEpoch !== startEpoch || getStoredKeyByVersion(address, version) !== keyHex) {
2343
+ throw new EncryptionKeyMissingError(address, version);
2344
+ }
2341
2345
  cryptoKeyCache.set(cacheKey, cryptoKey);
2342
2346
  return cryptoKey;
2343
2347
  }
@@ -22923,6 +22927,8 @@ async function backfillTopics(ctx) {
22923
22927
  if (topicless.length > 0) await backfillMemoryTopicsOp(ctx, topicless);
22924
22928
  }
22925
22929
  async function migrateMemoriesToRemote(options) {
22930
+ if (typeof options.withWriteBarrier !== "function" || typeof options.onActivated !== "function")
22931
+ throw new Error("A write barrier and a remote store handoff are required");
22926
22932
  const { canonical, signal } = options;
22927
22933
  const rings = [canonical, ...options.fallbacks ?? []];
22928
22934
  const model = options.embeddingOptions?.model ?? DEFAULT_API_EMBEDDING_MODEL;
@@ -22950,93 +22956,98 @@ async function migrateMemoriesToRemote(options) {
22950
22956
  const placeholders = /* @__PURE__ */ new Set();
22951
22957
  const truncated = /* @__PURE__ */ new Set();
22952
22958
  let imported = 0;
22953
- for (let pass = 0; ; pass++) {
22954
- const rows = await getAllVaultMemoryRowsOp(options.vaultCtx);
22955
- for (const raw of rows) if (invalid2(raw)) skipped.set(String(raw.id), "invalid");
22956
- const candidates = rows.filter((raw) => !skipped.has(String(raw.id)));
22957
- const missing = await missingIds(request, candidates.map(revision), signal);
22958
- const pending = /* @__PURE__ */ new Map();
22959
- for (const raw of candidates)
22960
- if (missing.has(raw.id)) pending.set(raw.id, raw.updated_at);
22961
- if (pending.size === 0 && !active) {
22962
- const remote = await remoteMetadata(
22963
- request,
22964
- candidates.map((raw) => raw.id),
22965
- signal
22966
- );
22967
- const needsVector = (raw, server) => !!options.embeddingOptions && !embedding(raw, model) && server.embedding_model !== model;
22968
- for (const raw of candidates) {
22969
- const server = remote.get(raw.id);
22970
- if (server && server.updated_at === raw.updated_at && (!sameMetadata2(raw, server, model) || needsVector(raw, server)))
22971
- pending.set(raw.id, raw.updated_at + 1);
22972
- }
22973
- }
22974
- if (pending.size === 0) {
22975
- const result = (state) => ({
22976
- state,
22977
- total: rows.length,
22978
- imported,
22979
- skipped: [...skipped].map(([memoryId, reason]) => ({ memoryId, reason })),
22980
- reembedded: reembedded.size,
22981
- unembedded: unembedded.size,
22982
- placeholderTombstones: placeholders.size,
22983
- truncatedSourceChunkIds: truncated.size
22984
- });
22985
- if (skipped.size > 0 && !options.allowSkipped) return result("migrating");
22986
- if (!claimed) await importBatch(request, canonical.keyId, [], signal);
22987
- if (!active)
22988
- await request(
22989
- "/account",
22990
- { method: "PUT", body: JSON.stringify({ state: "active" }) },
22959
+ const uploadPasses = async (finalizing = false) => {
22960
+ for (let pass = 0; ; pass++) {
22961
+ const rows = await getAllVaultMemoryRowsOp(options.vaultCtx);
22962
+ for (const raw of rows) if (invalid2(raw)) skipped.set(String(raw.id), "invalid");
22963
+ const candidates = rows.filter((raw) => !skipped.has(String(raw.id)));
22964
+ const missing = await missingIds(request, candidates.map(revision), signal);
22965
+ const pending = /* @__PURE__ */ new Map();
22966
+ for (const raw of candidates)
22967
+ if (missing.has(raw.id)) pending.set(raw.id, raw.updated_at);
22968
+ if (pending.size === 0 && !active) {
22969
+ const remote = await remoteMetadata(
22970
+ request,
22971
+ candidates.map((raw) => raw.id),
22991
22972
  signal
22992
22973
  );
22993
- return result("active");
22994
- }
22995
- if (pass >= MAX_UPLOAD_PASSES)
22996
- throw new RemoteMemoryError(
22997
- "Nearby still reports memories missing after importing them",
22998
- 409,
22999
- "migration_incomplete"
23000
- );
23001
- const prepared = [];
23002
- for (const raw of candidates) {
23003
- const updatedAt = pending.get(raw.id);
23004
- if (updatedAt === void 0) continue;
23005
- try {
23006
- prepared.push(await prepareRow(raw, canonical, rings, model, updatedAt));
23007
- } catch (error) {
23008
- if (!(error instanceof MemoryKeyError)) throw error;
23009
- skipped.set(raw.id, "unreadable");
23010
- }
23011
- }
23012
- const usable = new Set(prepared.filter((p) => p.row.embedding).map((p) => p.row.memory_id));
23013
- if (options.embeddingOptions)
23014
- await embedMissing(prepared, options.embeddingOptions, model, signal);
23015
- const local = new Map(candidates.map((raw) => [raw.id, raw]));
23016
- const upload = [];
23017
- for (const { row, plaintext } of prepared) {
23018
- const id2 = row.memory_id;
23019
- if (!fitsNearby(row)) {
23020
- skipped.set(id2, "invalid");
23021
- continue;
22974
+ const needsVector = (raw, server) => !!options.embeddingOptions && !embedding(raw, model) && server.embedding_model !== model;
22975
+ for (const raw of candidates) {
22976
+ const server = remote.get(raw.id);
22977
+ if (server && server.updated_at === raw.updated_at && (!sameMetadata2(raw, server, model) || needsVector(raw, server)))
22978
+ pending.set(raw.id, raw.updated_at + 1);
22979
+ }
22980
+ }
22981
+ if (pending.size === 0) {
22982
+ const result = (state) => ({
22983
+ state,
22984
+ total: rows.length,
22985
+ imported,
22986
+ skipped: [...skipped].map(([memoryId, reason]) => ({ memoryId, reason })),
22987
+ reembedded: reembedded.size,
22988
+ unembedded: unembedded.size,
22989
+ placeholderTombstones: placeholders.size,
22990
+ truncatedSourceChunkIds: truncated.size
22991
+ });
22992
+ if (skipped.size > 0 && !options.allowSkipped) return result("migrating");
22993
+ if (!finalizing) return options.withWriteBarrier(() => uploadPasses(true));
22994
+ if (!claimed) await importBatch(request, canonical.keyId, [], signal);
22995
+ if (!active)
22996
+ await request(
22997
+ "/account",
22998
+ { method: "PUT", body: JSON.stringify({ state: "active" }) },
22999
+ signal
23000
+ );
23001
+ await options.onActivated();
23002
+ return result("active");
23003
+ }
23004
+ if (pass >= MAX_UPLOAD_PASSES)
23005
+ throw new RemoteMemoryError(
23006
+ "Nearby still reports memories missing after importing them",
23007
+ 409,
23008
+ "migration_incomplete"
23009
+ );
23010
+ const prepared = [];
23011
+ for (const raw of candidates) {
23012
+ const updatedAt = pending.get(raw.id);
23013
+ if (updatedAt === void 0) continue;
23014
+ try {
23015
+ prepared.push(await prepareRow(raw, canonical, rings, model, updatedAt));
23016
+ } catch (error) {
23017
+ if (!(error instanceof MemoryKeyError)) throw error;
23018
+ skipped.set(raw.id, "unreadable");
23019
+ }
23020
+ }
23021
+ const usable = new Set(prepared.filter((p) => p.row.embedding).map((p) => p.row.memory_id));
23022
+ if (options.embeddingOptions)
23023
+ await embedMissing(prepared, options.embeddingOptions, model, signal);
23024
+ const local = new Map(candidates.map((raw) => [raw.id, raw]));
23025
+ const upload = [];
23026
+ for (const { row, plaintext } of prepared) {
23027
+ const id2 = row.memory_id;
23028
+ if (!fitsNearby(row)) {
23029
+ skipped.set(id2, "invalid");
23030
+ continue;
23031
+ }
23032
+ upload.push(row);
23033
+ const track = (set, on) => on ? set.add(id2) : set.delete(id2);
23034
+ track(reembedded, !!row.embedding && !usable.has(id2));
23035
+ track(unembedded, !row.embedding);
23036
+ track(placeholders, plaintext === void 0);
23037
+ track(truncated, row.source_chunk_ids !== text13(local.get(id2).source_chunk_ids));
23022
23038
  }
23023
- upload.push(row);
23024
- const track = (set, on) => on ? set.add(id2) : set.delete(id2);
23025
- track(reembedded, !!row.embedding && !usable.has(id2));
23026
- track(unembedded, !row.embedding);
23027
- track(placeholders, plaintext === void 0);
23028
- track(truncated, row.source_chunk_ids !== text13(local.get(id2).source_chunk_ids));
23029
- }
23030
- let sent = 0;
23031
- options.onProgress?.({ imported: sent, toImport: upload.length });
23032
- for (const batch of chunks(upload, IMPORT_BATCH)) {
23033
- await importBatch(request, canonical.keyId, batch, signal);
23034
- claimed = true;
23035
- sent += batch.length;
23039
+ let sent = 0;
23036
23040
  options.onProgress?.({ imported: sent, toImport: upload.length });
23041
+ for (const batch of chunks(upload, IMPORT_BATCH)) {
23042
+ await importBatch(request, canonical.keyId, batch, signal);
23043
+ claimed = true;
23044
+ sent += batch.length;
23045
+ options.onProgress?.({ imported: sent, toImport: upload.length });
23046
+ }
23047
+ imported += sent;
23037
23048
  }
23038
- imported += sent;
23039
- }
23049
+ };
23050
+ return uploadPasses();
23040
23051
  }
23041
23052
 
23042
23053
  // src/lib/memory/store/remoteStore.ts
@@ -25015,6 +25026,18 @@ async function assembleMemoryContext(options) {
25015
25026
 
25016
25027
  // src/lib/memory/durableExtraction.ts
25017
25028
  var import_watermelondb25 = require("@nozbe/watermelondb");
25029
+ async function messagePositions(storage, ids) {
25030
+ try {
25031
+ return await storage.messagesCollection.query(
25032
+ import_watermelondb25.Q.unsafeSqlQuery(
25033
+ `select "id", "conversation_id", "message_id" from "history" where "_status" is not 'deleted' and "id" in (${ids.map(() => "?").join(",")})`,
25034
+ ids
25035
+ )
25036
+ ).unsafeFetchRaw();
25037
+ } catch {
25038
+ return await storage.messagesCollection.query(import_watermelondb25.Q.where("id", import_watermelondb25.Q.oneOf(ids))).unsafeFetchRaw();
25039
+ }
25040
+ }
25018
25041
  var MAX_JOBS_PER_PASS = 3;
25019
25042
  var MAX_ATTEMPTS = 3;
25020
25043
  var MAX_FAILED_SESSIONS = 3;
@@ -25583,9 +25606,9 @@ function createDurableAutoExtractor(options) {
25583
25606
  r._setRaw("watermark_seq", null);
25584
25607
  });
25585
25608
  }
25586
- const rows = fromSourceConversation ? [] : await storage.messagesCollection.query(import_watermelondb25.Q.where("id", import_watermelondb25.Q.oneOf(ids))).fetch();
25609
+ const rows = fromSourceConversation ? [] : await messagePositions(storage, ids);
25587
25610
  const foreignRows = new Set(
25588
- rows.filter((row) => row._getRaw("conversation_id") !== conversationId).map((row) => row.id)
25611
+ rows.filter((row) => row.conversation_id !== conversationId).map((row) => row.id)
25589
25612
  );
25590
25613
  const sequences = /* @__PURE__ */ new Map();
25591
25614
  if (fromSourceConversation) {
@@ -25595,7 +25618,7 @@ function createDurableAutoExtractor(options) {
25595
25618
  }
25596
25619
  for (const row of rows) {
25597
25620
  if (foreignRows.has(row.id)) continue;
25598
- const sequence = Number(row._getRaw("message_id"));
25621
+ const sequence = Number(row.message_id);
25599
25622
  if (Number.isFinite(sequence) && sequence > 0) sequences.set(row.id, sequence);
25600
25623
  }
25601
25624
  let boundary = -1;
@@ -9931,6 +9931,10 @@ interface MemoryMigrationOptions extends Pick<RemoteMemoryPersistenceOptions, "b
9931
9931
  name: string;
9932
9932
  kind?: string;
9933
9933
  }[];
9934
+ /** Drain all local memory writes, block new writes until the task returns, and retain the block if the remote handoff fails. */
9935
+ withWriteBarrier: (task: () => Promise<MemoryMigrationResult>) => Promise<MemoryMigrationResult>;
9936
+ /** Route all subsequent memory writes to the remote store before this callback returns. */
9937
+ onActivated: () => void | Promise<void>;
9934
9938
  signal?: AbortSignal;
9935
9939
  onProgress?: (progress: MemoryMigrationProgress) => void;
9936
9940
  }
@@ -9931,6 +9931,10 @@ interface MemoryMigrationOptions extends Pick<RemoteMemoryPersistenceOptions, "b
9931
9931
  name: string;
9932
9932
  kind?: string;
9933
9933
  }[];
9934
+ /** Drain all local memory writes, block new writes until the task returns, and retain the block if the remote handoff fails. */
9935
+ withWriteBarrier: (task: () => Promise<MemoryMigrationResult>) => Promise<MemoryMigrationResult>;
9936
+ /** Route all subsequent memory writes to the remote store before this callback returns. */
9937
+ onActivated: () => void | Promise<void>;
9934
9938
  signal?: AbortSignal;
9935
9939
  onProgress?: (progress: MemoryMigrationProgress) => void;
9936
9940
  }
@@ -729,6 +729,7 @@ async function getEncryptionKey(address, version = "v3") {
729
729
  if (cachedKey) {
730
730
  return cachedKey;
731
731
  }
732
+ const startEpoch = sessionEpoch;
732
733
  const keyHex = getStoredKeyByVersion(address, version);
733
734
  if (!keyHex) {
734
735
  throw new EncryptionKeyMissingError(address, version);
@@ -741,6 +742,9 @@ async function getEncryptionKey(address, version = "v3") {
741
742
  false,
742
743
  ["encrypt", "decrypt"]
743
744
  );
745
+ if (sessionEpoch !== startEpoch || getStoredKeyByVersion(address, version) !== keyHex) {
746
+ throw new EncryptionKeyMissingError(address, version);
747
+ }
744
748
  cryptoKeyCache.set(cacheKey, cryptoKey);
745
749
  return cryptoKey;
746
750
  }
@@ -21248,6 +21252,8 @@ async function backfillTopics(ctx) {
21248
21252
  if (topicless.length > 0) await backfillMemoryTopicsOp(ctx, topicless);
21249
21253
  }
21250
21254
  async function migrateMemoriesToRemote(options) {
21255
+ if (typeof options.withWriteBarrier !== "function" || typeof options.onActivated !== "function")
21256
+ throw new Error("A write barrier and a remote store handoff are required");
21251
21257
  const { canonical, signal } = options;
21252
21258
  const rings = [canonical, ...options.fallbacks ?? []];
21253
21259
  const model = options.embeddingOptions?.model ?? DEFAULT_API_EMBEDDING_MODEL;
@@ -21275,93 +21281,98 @@ async function migrateMemoriesToRemote(options) {
21275
21281
  const placeholders = /* @__PURE__ */ new Set();
21276
21282
  const truncated = /* @__PURE__ */ new Set();
21277
21283
  let imported = 0;
21278
- for (let pass = 0; ; pass++) {
21279
- const rows = await getAllVaultMemoryRowsOp(options.vaultCtx);
21280
- for (const raw of rows) if (invalid2(raw)) skipped.set(String(raw.id), "invalid");
21281
- const candidates = rows.filter((raw) => !skipped.has(String(raw.id)));
21282
- const missing = await missingIds(request, candidates.map(revision), signal);
21283
- const pending = /* @__PURE__ */ new Map();
21284
- for (const raw of candidates)
21285
- if (missing.has(raw.id)) pending.set(raw.id, raw.updated_at);
21286
- if (pending.size === 0 && !active) {
21287
- const remote = await remoteMetadata(
21288
- request,
21289
- candidates.map((raw) => raw.id),
21290
- signal
21291
- );
21292
- const needsVector = (raw, server) => !!options.embeddingOptions && !embedding(raw, model) && server.embedding_model !== model;
21293
- for (const raw of candidates) {
21294
- const server = remote.get(raw.id);
21295
- if (server && server.updated_at === raw.updated_at && (!sameMetadata2(raw, server, model) || needsVector(raw, server)))
21296
- pending.set(raw.id, raw.updated_at + 1);
21297
- }
21298
- }
21299
- if (pending.size === 0) {
21300
- const result = (state) => ({
21301
- state,
21302
- total: rows.length,
21303
- imported,
21304
- skipped: [...skipped].map(([memoryId, reason]) => ({ memoryId, reason })),
21305
- reembedded: reembedded.size,
21306
- unembedded: unembedded.size,
21307
- placeholderTombstones: placeholders.size,
21308
- truncatedSourceChunkIds: truncated.size
21309
- });
21310
- if (skipped.size > 0 && !options.allowSkipped) return result("migrating");
21311
- if (!claimed) await importBatch(request, canonical.keyId, [], signal);
21312
- if (!active)
21313
- await request(
21314
- "/account",
21315
- { method: "PUT", body: JSON.stringify({ state: "active" }) },
21284
+ const uploadPasses = async (finalizing = false) => {
21285
+ for (let pass = 0; ; pass++) {
21286
+ const rows = await getAllVaultMemoryRowsOp(options.vaultCtx);
21287
+ for (const raw of rows) if (invalid2(raw)) skipped.set(String(raw.id), "invalid");
21288
+ const candidates = rows.filter((raw) => !skipped.has(String(raw.id)));
21289
+ const missing = await missingIds(request, candidates.map(revision), signal);
21290
+ const pending = /* @__PURE__ */ new Map();
21291
+ for (const raw of candidates)
21292
+ if (missing.has(raw.id)) pending.set(raw.id, raw.updated_at);
21293
+ if (pending.size === 0 && !active) {
21294
+ const remote = await remoteMetadata(
21295
+ request,
21296
+ candidates.map((raw) => raw.id),
21316
21297
  signal
21317
21298
  );
21318
- return result("active");
21319
- }
21320
- if (pass >= MAX_UPLOAD_PASSES)
21321
- throw new RemoteMemoryError(
21322
- "Nearby still reports memories missing after importing them",
21323
- 409,
21324
- "migration_incomplete"
21325
- );
21326
- const prepared = [];
21327
- for (const raw of candidates) {
21328
- const updatedAt = pending.get(raw.id);
21329
- if (updatedAt === void 0) continue;
21330
- try {
21331
- prepared.push(await prepareRow(raw, canonical, rings, model, updatedAt));
21332
- } catch (error) {
21333
- if (!(error instanceof MemoryKeyError)) throw error;
21334
- skipped.set(raw.id, "unreadable");
21335
- }
21336
- }
21337
- const usable = new Set(prepared.filter((p) => p.row.embedding).map((p) => p.row.memory_id));
21338
- if (options.embeddingOptions)
21339
- await embedMissing(prepared, options.embeddingOptions, model, signal);
21340
- const local = new Map(candidates.map((raw) => [raw.id, raw]));
21341
- const upload = [];
21342
- for (const { row, plaintext } of prepared) {
21343
- const id2 = row.memory_id;
21344
- if (!fitsNearby(row)) {
21345
- skipped.set(id2, "invalid");
21346
- continue;
21299
+ const needsVector = (raw, server) => !!options.embeddingOptions && !embedding(raw, model) && server.embedding_model !== model;
21300
+ for (const raw of candidates) {
21301
+ const server = remote.get(raw.id);
21302
+ if (server && server.updated_at === raw.updated_at && (!sameMetadata2(raw, server, model) || needsVector(raw, server)))
21303
+ pending.set(raw.id, raw.updated_at + 1);
21304
+ }
21305
+ }
21306
+ if (pending.size === 0) {
21307
+ const result = (state) => ({
21308
+ state,
21309
+ total: rows.length,
21310
+ imported,
21311
+ skipped: [...skipped].map(([memoryId, reason]) => ({ memoryId, reason })),
21312
+ reembedded: reembedded.size,
21313
+ unembedded: unembedded.size,
21314
+ placeholderTombstones: placeholders.size,
21315
+ truncatedSourceChunkIds: truncated.size
21316
+ });
21317
+ if (skipped.size > 0 && !options.allowSkipped) return result("migrating");
21318
+ if (!finalizing) return options.withWriteBarrier(() => uploadPasses(true));
21319
+ if (!claimed) await importBatch(request, canonical.keyId, [], signal);
21320
+ if (!active)
21321
+ await request(
21322
+ "/account",
21323
+ { method: "PUT", body: JSON.stringify({ state: "active" }) },
21324
+ signal
21325
+ );
21326
+ await options.onActivated();
21327
+ return result("active");
21328
+ }
21329
+ if (pass >= MAX_UPLOAD_PASSES)
21330
+ throw new RemoteMemoryError(
21331
+ "Nearby still reports memories missing after importing them",
21332
+ 409,
21333
+ "migration_incomplete"
21334
+ );
21335
+ const prepared = [];
21336
+ for (const raw of candidates) {
21337
+ const updatedAt = pending.get(raw.id);
21338
+ if (updatedAt === void 0) continue;
21339
+ try {
21340
+ prepared.push(await prepareRow(raw, canonical, rings, model, updatedAt));
21341
+ } catch (error) {
21342
+ if (!(error instanceof MemoryKeyError)) throw error;
21343
+ skipped.set(raw.id, "unreadable");
21344
+ }
21347
21345
  }
21348
- upload.push(row);
21349
- const track = (set, on) => on ? set.add(id2) : set.delete(id2);
21350
- track(reembedded, !!row.embedding && !usable.has(id2));
21351
- track(unembedded, !row.embedding);
21352
- track(placeholders, plaintext === void 0);
21353
- track(truncated, row.source_chunk_ids !== text13(local.get(id2).source_chunk_ids));
21354
- }
21355
- let sent = 0;
21356
- options.onProgress?.({ imported: sent, toImport: upload.length });
21357
- for (const batch of chunks(upload, IMPORT_BATCH)) {
21358
- await importBatch(request, canonical.keyId, batch, signal);
21359
- claimed = true;
21360
- sent += batch.length;
21346
+ const usable = new Set(prepared.filter((p) => p.row.embedding).map((p) => p.row.memory_id));
21347
+ if (options.embeddingOptions)
21348
+ await embedMissing(prepared, options.embeddingOptions, model, signal);
21349
+ const local = new Map(candidates.map((raw) => [raw.id, raw]));
21350
+ const upload = [];
21351
+ for (const { row, plaintext } of prepared) {
21352
+ const id2 = row.memory_id;
21353
+ if (!fitsNearby(row)) {
21354
+ skipped.set(id2, "invalid");
21355
+ continue;
21356
+ }
21357
+ upload.push(row);
21358
+ const track = (set, on) => on ? set.add(id2) : set.delete(id2);
21359
+ track(reembedded, !!row.embedding && !usable.has(id2));
21360
+ track(unembedded, !row.embedding);
21361
+ track(placeholders, plaintext === void 0);
21362
+ track(truncated, row.source_chunk_ids !== text13(local.get(id2).source_chunk_ids));
21363
+ }
21364
+ let sent = 0;
21361
21365
  options.onProgress?.({ imported: sent, toImport: upload.length });
21366
+ for (const batch of chunks(upload, IMPORT_BATCH)) {
21367
+ await importBatch(request, canonical.keyId, batch, signal);
21368
+ claimed = true;
21369
+ sent += batch.length;
21370
+ options.onProgress?.({ imported: sent, toImport: upload.length });
21371
+ }
21372
+ imported += sent;
21362
21373
  }
21363
- imported += sent;
21364
- }
21374
+ };
21375
+ return uploadPasses();
21365
21376
  }
21366
21377
 
21367
21378
  // src/lib/memory/store/remoteStore.ts
@@ -23337,6 +23348,18 @@ async function assembleMemoryContext(options) {
23337
23348
 
23338
23349
  // src/lib/memory/durableExtraction.ts
23339
23350
  import { Q as Q10 } from "@nozbe/watermelondb";
23351
+ async function messagePositions(storage, ids) {
23352
+ try {
23353
+ return await storage.messagesCollection.query(
23354
+ Q10.unsafeSqlQuery(
23355
+ `select "id", "conversation_id", "message_id" from "history" where "_status" is not 'deleted' and "id" in (${ids.map(() => "?").join(",")})`,
23356
+ ids
23357
+ )
23358
+ ).unsafeFetchRaw();
23359
+ } catch {
23360
+ return await storage.messagesCollection.query(Q10.where("id", Q10.oneOf(ids))).unsafeFetchRaw();
23361
+ }
23362
+ }
23340
23363
  var MAX_JOBS_PER_PASS = 3;
23341
23364
  var MAX_ATTEMPTS = 3;
23342
23365
  var MAX_FAILED_SESSIONS = 3;
@@ -23905,9 +23928,9 @@ function createDurableAutoExtractor(options) {
23905
23928
  r._setRaw("watermark_seq", null);
23906
23929
  });
23907
23930
  }
23908
- const rows = fromSourceConversation ? [] : await storage.messagesCollection.query(Q10.where("id", Q10.oneOf(ids))).fetch();
23931
+ const rows = fromSourceConversation ? [] : await messagePositions(storage, ids);
23909
23932
  const foreignRows = new Set(
23910
- rows.filter((row) => row._getRaw("conversation_id") !== conversationId).map((row) => row.id)
23933
+ rows.filter((row) => row.conversation_id !== conversationId).map((row) => row.id)
23911
23934
  );
23912
23935
  const sequences = /* @__PURE__ */ new Map();
23913
23936
  if (fromSourceConversation) {
@@ -23917,7 +23940,7 @@ function createDurableAutoExtractor(options) {
23917
23940
  }
23918
23941
  for (const row of rows) {
23919
23942
  if (foreignRows.has(row.id)) continue;
23920
- const sequence = Number(row._getRaw("message_id"));
23943
+ const sequence = Number(row.message_id);
23921
23944
  if (Number.isFinite(sequence) && sequence > 0) sequences.set(row.id, sequence);
23922
23945
  }
23923
23946
  let boundary = -1;
package/dist/index.cjs CHANGED
@@ -1404,6 +1404,7 @@ var EncryptionKeyMissingError = class extends Error {
1404
1404
  }
1405
1405
  };
1406
1406
  var encryptionKeyStore = /* @__PURE__ */ new Map();
1407
+ var sessionEpoch = 0;
1407
1408
  var cryptoKeyCache = /* @__PURE__ */ new Map();
1408
1409
  var clearAllEncryptionStateCallbacks = /* @__PURE__ */ new Set();
1409
1410
  function onClearAllEncryptionState(callback) {
@@ -1455,6 +1456,7 @@ async function getEncryptionKey(address, version = "v3") {
1455
1456
  if (cachedKey) {
1456
1457
  return cachedKey;
1457
1458
  }
1459
+ const startEpoch = sessionEpoch;
1458
1460
  const keyHex = getStoredKeyByVersion(address, version);
1459
1461
  if (!keyHex) {
1460
1462
  throw new EncryptionKeyMissingError(address, version);
@@ -1467,6 +1469,9 @@ async function getEncryptionKey(address, version = "v3") {
1467
1469
  false,
1468
1470
  ["encrypt", "decrypt"]
1469
1471
  );
1472
+ if (sessionEpoch !== startEpoch || getStoredKeyByVersion(address, version) !== keyHex) {
1473
+ throw new EncryptionKeyMissingError(address, version);
1474
+ }
1470
1475
  cryptoKeyCache.set(cacheKey, cryptoKey);
1471
1476
  return cryptoKey;
1472
1477
  }
@@ -2159,7 +2164,7 @@ async function createRemoteChatStore(options) {
2159
2164
  var LRU_CAPACITY = 256;
2160
2165
  var titleCache = /* @__PURE__ */ new Map();
2161
2166
  var pendingDecrypts = /* @__PURE__ */ new Map();
2162
- var sessionEpoch = 0;
2167
+ var sessionEpoch2 = 0;
2163
2168
  function buildCacheKey(address, encryptedTitle) {
2164
2169
  return `${address}:${encryptedTitle}`;
2165
2170
  }
@@ -2178,7 +2183,7 @@ function insertWithEviction(key, value) {
2178
2183
  function clearLazyTitleCache() {
2179
2184
  titleCache.clear();
2180
2185
  pendingDecrypts.clear();
2181
- sessionEpoch += 1;
2186
+ sessionEpoch2 += 1;
2182
2187
  }
2183
2188
  onClearAllEncryptionState(clearLazyTitleCache);
2184
2189
  async function decryptConversationTitle(encryptedTitle, address) {
@@ -2200,13 +2205,13 @@ async function decryptConversationTitle(encryptedTitle, address) {
2200
2205
  }
2201
2206
  const inFlight = pendingDecrypts.get(cacheKey);
2202
2207
  if (inFlight) return inFlight;
2203
- const epochAtStart = sessionEpoch;
2208
+ const epochAtStart = sessionEpoch2;
2204
2209
  const promise = (async () => {
2205
2210
  const plaintext = await decryptField(encryptedTitle, address);
2206
2211
  if (isEncrypted(plaintext)) {
2207
2212
  throw new Error("decryptConversationTitle: decryption failed (returned ciphertext)");
2208
2213
  }
2209
- if (sessionEpoch === epochAtStart) {
2214
+ if (sessionEpoch2 === epochAtStart) {
2210
2215
  insertWithEviction(cacheKey, plaintext);
2211
2216
  }
2212
2217
  return plaintext;
package/dist/index.mjs CHANGED
@@ -1201,6 +1201,7 @@ var EncryptionKeyMissingError = class extends Error {
1201
1201
  }
1202
1202
  };
1203
1203
  var encryptionKeyStore = /* @__PURE__ */ new Map();
1204
+ var sessionEpoch = 0;
1204
1205
  var cryptoKeyCache = /* @__PURE__ */ new Map();
1205
1206
  var clearAllEncryptionStateCallbacks = /* @__PURE__ */ new Set();
1206
1207
  function onClearAllEncryptionState(callback) {
@@ -1252,6 +1253,7 @@ async function getEncryptionKey(address, version = "v3") {
1252
1253
  if (cachedKey) {
1253
1254
  return cachedKey;
1254
1255
  }
1256
+ const startEpoch = sessionEpoch;
1255
1257
  const keyHex = getStoredKeyByVersion(address, version);
1256
1258
  if (!keyHex) {
1257
1259
  throw new EncryptionKeyMissingError(address, version);
@@ -1264,6 +1266,9 @@ async function getEncryptionKey(address, version = "v3") {
1264
1266
  false,
1265
1267
  ["encrypt", "decrypt"]
1266
1268
  );
1269
+ if (sessionEpoch !== startEpoch || getStoredKeyByVersion(address, version) !== keyHex) {
1270
+ throw new EncryptionKeyMissingError(address, version);
1271
+ }
1267
1272
  cryptoKeyCache.set(cacheKey, cryptoKey);
1268
1273
  return cryptoKey;
1269
1274
  }
@@ -1956,7 +1961,7 @@ async function createRemoteChatStore(options) {
1956
1961
  var LRU_CAPACITY = 256;
1957
1962
  var titleCache = /* @__PURE__ */ new Map();
1958
1963
  var pendingDecrypts = /* @__PURE__ */ new Map();
1959
- var sessionEpoch = 0;
1964
+ var sessionEpoch2 = 0;
1960
1965
  function buildCacheKey(address, encryptedTitle) {
1961
1966
  return `${address}:${encryptedTitle}`;
1962
1967
  }
@@ -1975,7 +1980,7 @@ function insertWithEviction(key, value) {
1975
1980
  function clearLazyTitleCache() {
1976
1981
  titleCache.clear();
1977
1982
  pendingDecrypts.clear();
1978
- sessionEpoch += 1;
1983
+ sessionEpoch2 += 1;
1979
1984
  }
1980
1985
  onClearAllEncryptionState(clearLazyTitleCache);
1981
1986
  async function decryptConversationTitle(encryptedTitle, address) {
@@ -1997,13 +2002,13 @@ async function decryptConversationTitle(encryptedTitle, address) {
1997
2002
  }
1998
2003
  const inFlight = pendingDecrypts.get(cacheKey);
1999
2004
  if (inFlight) return inFlight;
2000
- const epochAtStart = sessionEpoch;
2005
+ const epochAtStart = sessionEpoch2;
2001
2006
  const promise = (async () => {
2002
2007
  const plaintext = await decryptField(encryptedTitle, address);
2003
2008
  if (isEncrypted(plaintext)) {
2004
2009
  throw new Error("decryptConversationTitle: decryption failed (returned ciphertext)");
2005
2010
  }
2006
- if (sessionEpoch === epochAtStart) {
2011
+ if (sessionEpoch2 === epochAtStart) {
2007
2012
  insertWithEviction(cacheKey, plaintext);
2008
2013
  }
2009
2014
  return plaintext;
@@ -2093,6 +2093,7 @@ async function getEncryptionKey(address, version = "v3") {
2093
2093
  if (cachedKey) {
2094
2094
  return cachedKey;
2095
2095
  }
2096
+ const startEpoch = sessionEpoch;
2096
2097
  const keyHex = getStoredKeyByVersion(address, version);
2097
2098
  if (!keyHex) {
2098
2099
  throw new EncryptionKeyMissingError(address, version);
@@ -2105,6 +2106,9 @@ async function getEncryptionKey(address, version = "v3") {
2105
2106
  false,
2106
2107
  ["encrypt", "decrypt"]
2107
2108
  );
2109
+ if (sessionEpoch !== startEpoch || getStoredKeyByVersion(address, version) !== keyHex) {
2110
+ throw new EncryptionKeyMissingError(address, version);
2111
+ }
2108
2112
  cryptoKeyCache.set(cacheKey, cryptoKey);
2109
2113
  return cryptoKey;
2110
2114
  }
@@ -25236,6 +25240,8 @@ async function backfillTopics(ctx) {
25236
25240
  if (topicless.length > 0) await backfillMemoryTopicsOp(ctx, topicless);
25237
25241
  }
25238
25242
  async function migrateMemoriesToRemote(options) {
25243
+ if (typeof options.withWriteBarrier !== "function" || typeof options.onActivated !== "function")
25244
+ throw new Error("A write barrier and a remote store handoff are required");
25239
25245
  const { canonical, signal } = options;
25240
25246
  const rings = [canonical, ...options.fallbacks ?? []];
25241
25247
  const model = options.embeddingOptions?.model ?? DEFAULT_API_EMBEDDING_MODEL;
@@ -25263,93 +25269,98 @@ async function migrateMemoriesToRemote(options) {
25263
25269
  const placeholders = /* @__PURE__ */ new Set();
25264
25270
  const truncated = /* @__PURE__ */ new Set();
25265
25271
  let imported = 0;
25266
- for (let pass = 0; ; pass++) {
25267
- const rows = await getAllVaultMemoryRowsOp(options.vaultCtx);
25268
- for (const raw of rows) if (invalid2(raw)) skipped.set(String(raw.id), "invalid");
25269
- const candidates = rows.filter((raw) => !skipped.has(String(raw.id)));
25270
- const missing = await missingIds(request, candidates.map(revision), signal);
25271
- const pending = /* @__PURE__ */ new Map();
25272
- for (const raw of candidates)
25273
- if (missing.has(raw.id)) pending.set(raw.id, raw.updated_at);
25274
- if (pending.size === 0 && !active) {
25275
- const remote = await remoteMetadata(
25276
- request,
25277
- candidates.map((raw) => raw.id),
25278
- signal
25279
- );
25280
- const needsVector = (raw, server) => !!options.embeddingOptions && !embedding(raw, model) && server.embedding_model !== model;
25281
- for (const raw of candidates) {
25282
- const server = remote.get(raw.id);
25283
- if (server && server.updated_at === raw.updated_at && (!sameMetadata2(raw, server, model) || needsVector(raw, server)))
25284
- pending.set(raw.id, raw.updated_at + 1);
25285
- }
25286
- }
25287
- if (pending.size === 0) {
25288
- const result = (state) => ({
25289
- state,
25290
- total: rows.length,
25291
- imported,
25292
- skipped: [...skipped].map(([memoryId, reason]) => ({ memoryId, reason })),
25293
- reembedded: reembedded.size,
25294
- unembedded: unembedded.size,
25295
- placeholderTombstones: placeholders.size,
25296
- truncatedSourceChunkIds: truncated.size
25297
- });
25298
- if (skipped.size > 0 && !options.allowSkipped) return result("migrating");
25299
- if (!claimed) await importBatch(request, canonical.keyId, [], signal);
25300
- if (!active)
25301
- await request(
25302
- "/account",
25303
- { method: "PUT", body: JSON.stringify({ state: "active" }) },
25272
+ const uploadPasses = async (finalizing = false) => {
25273
+ for (let pass = 0; ; pass++) {
25274
+ const rows = await getAllVaultMemoryRowsOp(options.vaultCtx);
25275
+ for (const raw of rows) if (invalid2(raw)) skipped.set(String(raw.id), "invalid");
25276
+ const candidates = rows.filter((raw) => !skipped.has(String(raw.id)));
25277
+ const missing = await missingIds(request, candidates.map(revision), signal);
25278
+ const pending = /* @__PURE__ */ new Map();
25279
+ for (const raw of candidates)
25280
+ if (missing.has(raw.id)) pending.set(raw.id, raw.updated_at);
25281
+ if (pending.size === 0 && !active) {
25282
+ const remote = await remoteMetadata(
25283
+ request,
25284
+ candidates.map((raw) => raw.id),
25304
25285
  signal
25305
25286
  );
25306
- return result("active");
25307
- }
25308
- if (pass >= MAX_UPLOAD_PASSES)
25309
- throw new RemoteMemoryError(
25310
- "Nearby still reports memories missing after importing them",
25311
- 409,
25312
- "migration_incomplete"
25313
- );
25314
- const prepared = [];
25315
- for (const raw of candidates) {
25316
- const updatedAt = pending.get(raw.id);
25317
- if (updatedAt === void 0) continue;
25318
- try {
25319
- prepared.push(await prepareRow(raw, canonical, rings, model, updatedAt));
25320
- } catch (error) {
25321
- if (!(error instanceof MemoryKeyError)) throw error;
25322
- skipped.set(raw.id, "unreadable");
25323
- }
25324
- }
25325
- const usable = new Set(prepared.filter((p) => p.row.embedding).map((p) => p.row.memory_id));
25326
- if (options.embeddingOptions)
25327
- await embedMissing(prepared, options.embeddingOptions, model, signal);
25328
- const local = new Map(candidates.map((raw) => [raw.id, raw]));
25329
- const upload = [];
25330
- for (const { row, plaintext } of prepared) {
25331
- const id2 = row.memory_id;
25332
- if (!fitsNearby(row)) {
25333
- skipped.set(id2, "invalid");
25334
- continue;
25287
+ const needsVector = (raw, server) => !!options.embeddingOptions && !embedding(raw, model) && server.embedding_model !== model;
25288
+ for (const raw of candidates) {
25289
+ const server = remote.get(raw.id);
25290
+ if (server && server.updated_at === raw.updated_at && (!sameMetadata2(raw, server, model) || needsVector(raw, server)))
25291
+ pending.set(raw.id, raw.updated_at + 1);
25292
+ }
25293
+ }
25294
+ if (pending.size === 0) {
25295
+ const result = (state) => ({
25296
+ state,
25297
+ total: rows.length,
25298
+ imported,
25299
+ skipped: [...skipped].map(([memoryId, reason]) => ({ memoryId, reason })),
25300
+ reembedded: reembedded.size,
25301
+ unembedded: unembedded.size,
25302
+ placeholderTombstones: placeholders.size,
25303
+ truncatedSourceChunkIds: truncated.size
25304
+ });
25305
+ if (skipped.size > 0 && !options.allowSkipped) return result("migrating");
25306
+ if (!finalizing) return options.withWriteBarrier(() => uploadPasses(true));
25307
+ if (!claimed) await importBatch(request, canonical.keyId, [], signal);
25308
+ if (!active)
25309
+ await request(
25310
+ "/account",
25311
+ { method: "PUT", body: JSON.stringify({ state: "active" }) },
25312
+ signal
25313
+ );
25314
+ await options.onActivated();
25315
+ return result("active");
25316
+ }
25317
+ if (pass >= MAX_UPLOAD_PASSES)
25318
+ throw new RemoteMemoryError(
25319
+ "Nearby still reports memories missing after importing them",
25320
+ 409,
25321
+ "migration_incomplete"
25322
+ );
25323
+ const prepared = [];
25324
+ for (const raw of candidates) {
25325
+ const updatedAt = pending.get(raw.id);
25326
+ if (updatedAt === void 0) continue;
25327
+ try {
25328
+ prepared.push(await prepareRow(raw, canonical, rings, model, updatedAt));
25329
+ } catch (error) {
25330
+ if (!(error instanceof MemoryKeyError)) throw error;
25331
+ skipped.set(raw.id, "unreadable");
25332
+ }
25333
+ }
25334
+ const usable = new Set(prepared.filter((p) => p.row.embedding).map((p) => p.row.memory_id));
25335
+ if (options.embeddingOptions)
25336
+ await embedMissing(prepared, options.embeddingOptions, model, signal);
25337
+ const local = new Map(candidates.map((raw) => [raw.id, raw]));
25338
+ const upload = [];
25339
+ for (const { row, plaintext } of prepared) {
25340
+ const id2 = row.memory_id;
25341
+ if (!fitsNearby(row)) {
25342
+ skipped.set(id2, "invalid");
25343
+ continue;
25344
+ }
25345
+ upload.push(row);
25346
+ const track = (set, on) => on ? set.add(id2) : set.delete(id2);
25347
+ track(reembedded, !!row.embedding && !usable.has(id2));
25348
+ track(unembedded, !row.embedding);
25349
+ track(placeholders, plaintext === void 0);
25350
+ track(truncated, row.source_chunk_ids !== text13(local.get(id2).source_chunk_ids));
25335
25351
  }
25336
- upload.push(row);
25337
- const track = (set, on) => on ? set.add(id2) : set.delete(id2);
25338
- track(reembedded, !!row.embedding && !usable.has(id2));
25339
- track(unembedded, !row.embedding);
25340
- track(placeholders, plaintext === void 0);
25341
- track(truncated, row.source_chunk_ids !== text13(local.get(id2).source_chunk_ids));
25342
- }
25343
- let sent = 0;
25344
- options.onProgress?.({ imported: sent, toImport: upload.length });
25345
- for (const batch of chunks(upload, IMPORT_BATCH)) {
25346
- await importBatch(request, canonical.keyId, batch, signal);
25347
- claimed = true;
25348
- sent += batch.length;
25352
+ let sent = 0;
25349
25353
  options.onProgress?.({ imported: sent, toImport: upload.length });
25354
+ for (const batch of chunks(upload, IMPORT_BATCH)) {
25355
+ await importBatch(request, canonical.keyId, batch, signal);
25356
+ claimed = true;
25357
+ sent += batch.length;
25358
+ options.onProgress?.({ imported: sent, toImport: upload.length });
25359
+ }
25360
+ imported += sent;
25350
25361
  }
25351
- imported += sent;
25352
- }
25362
+ };
25363
+ return uploadPasses();
25353
25364
  }
25354
25365
 
25355
25366
  // src/lib/memory/store/remoteStore.ts
@@ -27328,6 +27339,18 @@ async function assembleMemoryContext(options) {
27328
27339
 
27329
27340
  // src/lib/memory/durableExtraction.ts
27330
27341
  var import_watermelondb34 = require("@nozbe/watermelondb");
27342
+ async function messagePositions(storage, ids) {
27343
+ try {
27344
+ return await storage.messagesCollection.query(
27345
+ import_watermelondb34.Q.unsafeSqlQuery(
27346
+ `select "id", "conversation_id", "message_id" from "history" where "_status" is not 'deleted' and "id" in (${ids.map(() => "?").join(",")})`,
27347
+ ids
27348
+ )
27349
+ ).unsafeFetchRaw();
27350
+ } catch {
27351
+ return await storage.messagesCollection.query(import_watermelondb34.Q.where("id", import_watermelondb34.Q.oneOf(ids))).unsafeFetchRaw();
27352
+ }
27353
+ }
27331
27354
  var MAX_JOBS_PER_PASS = 3;
27332
27355
  var MAX_ATTEMPTS = 3;
27333
27356
  var MAX_FAILED_SESSIONS = 3;
@@ -27896,9 +27919,9 @@ function createDurableAutoExtractor(options) {
27896
27919
  r._setRaw("watermark_seq", null);
27897
27920
  });
27898
27921
  }
27899
- const rows = fromSourceConversation ? [] : await storage.messagesCollection.query(import_watermelondb34.Q.where("id", import_watermelondb34.Q.oneOf(ids))).fetch();
27922
+ const rows = fromSourceConversation ? [] : await messagePositions(storage, ids);
27900
27923
  const foreignRows = new Set(
27901
- rows.filter((row) => row._getRaw("conversation_id") !== conversationId).map((row) => row.id)
27924
+ rows.filter((row) => row.conversation_id !== conversationId).map((row) => row.id)
27902
27925
  );
27903
27926
  const sequences = /* @__PURE__ */ new Map();
27904
27927
  if (fromSourceConversation) {
@@ -27908,7 +27931,7 @@ function createDurableAutoExtractor(options) {
27908
27931
  }
27909
27932
  for (const row of rows) {
27910
27933
  if (foreignRows.has(row.id)) continue;
27911
- const sequence = Number(row._getRaw("message_id"));
27934
+ const sequence = Number(row.message_id);
27912
27935
  if (Number.isFinite(sequence) && sequence > 0) sequences.set(row.id, sequence);
27913
27936
  }
27914
27937
  let boundary = -1;
@@ -11665,6 +11665,10 @@ interface MemoryMigrationOptions extends Pick<RemoteMemoryPersistenceOptions, "b
11665
11665
  name: string;
11666
11666
  kind?: string;
11667
11667
  }[];
11668
+ /** Drain all local memory writes, block new writes until the task returns, and retain the block if the remote handoff fails. */
11669
+ withWriteBarrier: (task: () => Promise<MemoryMigrationResult>) => Promise<MemoryMigrationResult>;
11670
+ /** Route all subsequent memory writes to the remote store before this callback returns. */
11671
+ onActivated: () => void | Promise<void>;
11668
11672
  signal?: AbortSignal;
11669
11673
  onProgress?: (progress: MemoryMigrationProgress) => void;
11670
11674
  }
@@ -11665,6 +11665,10 @@ interface MemoryMigrationOptions extends Pick<RemoteMemoryPersistenceOptions, "b
11665
11665
  name: string;
11666
11666
  kind?: string;
11667
11667
  }[];
11668
+ /** Drain all local memory writes, block new writes until the task returns, and retain the block if the remote handoff fails. */
11669
+ withWriteBarrier: (task: () => Promise<MemoryMigrationResult>) => Promise<MemoryMigrationResult>;
11670
+ /** Route all subsequent memory writes to the remote store before this callback returns. */
11671
+ onActivated: () => void | Promise<void>;
11668
11672
  signal?: AbortSignal;
11669
11673
  onProgress?: (progress: MemoryMigrationProgress) => void;
11670
11674
  }
@@ -295,6 +295,7 @@ async function getEncryptionKey(address, version = "v3") {
295
295
  if (cachedKey) {
296
296
  return cachedKey;
297
297
  }
298
+ const startEpoch = sessionEpoch;
298
299
  const keyHex = getStoredKeyByVersion(address, version);
299
300
  if (!keyHex) {
300
301
  throw new EncryptionKeyMissingError(address, version);
@@ -307,6 +308,9 @@ async function getEncryptionKey(address, version = "v3") {
307
308
  false,
308
309
  ["encrypt", "decrypt"]
309
310
  );
311
+ if (sessionEpoch !== startEpoch || getStoredKeyByVersion(address, version) !== keyHex) {
312
+ throw new EncryptionKeyMissingError(address, version);
313
+ }
310
314
  cryptoKeyCache.set(cacheKey, cryptoKey);
311
315
  return cryptoKey;
312
316
  }
@@ -23353,6 +23357,8 @@ async function backfillTopics(ctx) {
23353
23357
  if (topicless.length > 0) await backfillMemoryTopicsOp(ctx, topicless);
23354
23358
  }
23355
23359
  async function migrateMemoriesToRemote(options) {
23360
+ if (typeof options.withWriteBarrier !== "function" || typeof options.onActivated !== "function")
23361
+ throw new Error("A write barrier and a remote store handoff are required");
23356
23362
  const { canonical, signal } = options;
23357
23363
  const rings = [canonical, ...options.fallbacks ?? []];
23358
23364
  const model = options.embeddingOptions?.model ?? DEFAULT_API_EMBEDDING_MODEL;
@@ -23380,93 +23386,98 @@ async function migrateMemoriesToRemote(options) {
23380
23386
  const placeholders = /* @__PURE__ */ new Set();
23381
23387
  const truncated = /* @__PURE__ */ new Set();
23382
23388
  let imported = 0;
23383
- for (let pass = 0; ; pass++) {
23384
- const rows = await getAllVaultMemoryRowsOp(options.vaultCtx);
23385
- for (const raw of rows) if (invalid2(raw)) skipped.set(String(raw.id), "invalid");
23386
- const candidates = rows.filter((raw) => !skipped.has(String(raw.id)));
23387
- const missing = await missingIds(request, candidates.map(revision), signal);
23388
- const pending = /* @__PURE__ */ new Map();
23389
- for (const raw of candidates)
23390
- if (missing.has(raw.id)) pending.set(raw.id, raw.updated_at);
23391
- if (pending.size === 0 && !active) {
23392
- const remote = await remoteMetadata(
23393
- request,
23394
- candidates.map((raw) => raw.id),
23395
- signal
23396
- );
23397
- const needsVector = (raw, server) => !!options.embeddingOptions && !embedding(raw, model) && server.embedding_model !== model;
23398
- for (const raw of candidates) {
23399
- const server = remote.get(raw.id);
23400
- if (server && server.updated_at === raw.updated_at && (!sameMetadata2(raw, server, model) || needsVector(raw, server)))
23401
- pending.set(raw.id, raw.updated_at + 1);
23402
- }
23403
- }
23404
- if (pending.size === 0) {
23405
- const result = (state) => ({
23406
- state,
23407
- total: rows.length,
23408
- imported,
23409
- skipped: [...skipped].map(([memoryId, reason]) => ({ memoryId, reason })),
23410
- reembedded: reembedded.size,
23411
- unembedded: unembedded.size,
23412
- placeholderTombstones: placeholders.size,
23413
- truncatedSourceChunkIds: truncated.size
23414
- });
23415
- if (skipped.size > 0 && !options.allowSkipped) return result("migrating");
23416
- if (!claimed) await importBatch(request, canonical.keyId, [], signal);
23417
- if (!active)
23418
- await request(
23419
- "/account",
23420
- { method: "PUT", body: JSON.stringify({ state: "active" }) },
23389
+ const uploadPasses = async (finalizing = false) => {
23390
+ for (let pass = 0; ; pass++) {
23391
+ const rows = await getAllVaultMemoryRowsOp(options.vaultCtx);
23392
+ for (const raw of rows) if (invalid2(raw)) skipped.set(String(raw.id), "invalid");
23393
+ const candidates = rows.filter((raw) => !skipped.has(String(raw.id)));
23394
+ const missing = await missingIds(request, candidates.map(revision), signal);
23395
+ const pending = /* @__PURE__ */ new Map();
23396
+ for (const raw of candidates)
23397
+ if (missing.has(raw.id)) pending.set(raw.id, raw.updated_at);
23398
+ if (pending.size === 0 && !active) {
23399
+ const remote = await remoteMetadata(
23400
+ request,
23401
+ candidates.map((raw) => raw.id),
23421
23402
  signal
23422
23403
  );
23423
- return result("active");
23424
- }
23425
- if (pass >= MAX_UPLOAD_PASSES)
23426
- throw new RemoteMemoryError(
23427
- "Nearby still reports memories missing after importing them",
23428
- 409,
23429
- "migration_incomplete"
23430
- );
23431
- const prepared = [];
23432
- for (const raw of candidates) {
23433
- const updatedAt = pending.get(raw.id);
23434
- if (updatedAt === void 0) continue;
23435
- try {
23436
- prepared.push(await prepareRow(raw, canonical, rings, model, updatedAt));
23437
- } catch (error) {
23438
- if (!(error instanceof MemoryKeyError)) throw error;
23439
- skipped.set(raw.id, "unreadable");
23440
- }
23441
- }
23442
- const usable = new Set(prepared.filter((p) => p.row.embedding).map((p) => p.row.memory_id));
23443
- if (options.embeddingOptions)
23444
- await embedMissing(prepared, options.embeddingOptions, model, signal);
23445
- const local = new Map(candidates.map((raw) => [raw.id, raw]));
23446
- const upload = [];
23447
- for (const { row, plaintext } of prepared) {
23448
- const id2 = row.memory_id;
23449
- if (!fitsNearby(row)) {
23450
- skipped.set(id2, "invalid");
23451
- continue;
23404
+ const needsVector = (raw, server) => !!options.embeddingOptions && !embedding(raw, model) && server.embedding_model !== model;
23405
+ for (const raw of candidates) {
23406
+ const server = remote.get(raw.id);
23407
+ if (server && server.updated_at === raw.updated_at && (!sameMetadata2(raw, server, model) || needsVector(raw, server)))
23408
+ pending.set(raw.id, raw.updated_at + 1);
23409
+ }
23410
+ }
23411
+ if (pending.size === 0) {
23412
+ const result = (state) => ({
23413
+ state,
23414
+ total: rows.length,
23415
+ imported,
23416
+ skipped: [...skipped].map(([memoryId, reason]) => ({ memoryId, reason })),
23417
+ reembedded: reembedded.size,
23418
+ unembedded: unembedded.size,
23419
+ placeholderTombstones: placeholders.size,
23420
+ truncatedSourceChunkIds: truncated.size
23421
+ });
23422
+ if (skipped.size > 0 && !options.allowSkipped) return result("migrating");
23423
+ if (!finalizing) return options.withWriteBarrier(() => uploadPasses(true));
23424
+ if (!claimed) await importBatch(request, canonical.keyId, [], signal);
23425
+ if (!active)
23426
+ await request(
23427
+ "/account",
23428
+ { method: "PUT", body: JSON.stringify({ state: "active" }) },
23429
+ signal
23430
+ );
23431
+ await options.onActivated();
23432
+ return result("active");
23433
+ }
23434
+ if (pass >= MAX_UPLOAD_PASSES)
23435
+ throw new RemoteMemoryError(
23436
+ "Nearby still reports memories missing after importing them",
23437
+ 409,
23438
+ "migration_incomplete"
23439
+ );
23440
+ const prepared = [];
23441
+ for (const raw of candidates) {
23442
+ const updatedAt = pending.get(raw.id);
23443
+ if (updatedAt === void 0) continue;
23444
+ try {
23445
+ prepared.push(await prepareRow(raw, canonical, rings, model, updatedAt));
23446
+ } catch (error) {
23447
+ if (!(error instanceof MemoryKeyError)) throw error;
23448
+ skipped.set(raw.id, "unreadable");
23449
+ }
23450
+ }
23451
+ const usable = new Set(prepared.filter((p) => p.row.embedding).map((p) => p.row.memory_id));
23452
+ if (options.embeddingOptions)
23453
+ await embedMissing(prepared, options.embeddingOptions, model, signal);
23454
+ const local = new Map(candidates.map((raw) => [raw.id, raw]));
23455
+ const upload = [];
23456
+ for (const { row, plaintext } of prepared) {
23457
+ const id2 = row.memory_id;
23458
+ if (!fitsNearby(row)) {
23459
+ skipped.set(id2, "invalid");
23460
+ continue;
23461
+ }
23462
+ upload.push(row);
23463
+ const track = (set, on) => on ? set.add(id2) : set.delete(id2);
23464
+ track(reembedded, !!row.embedding && !usable.has(id2));
23465
+ track(unembedded, !row.embedding);
23466
+ track(placeholders, plaintext === void 0);
23467
+ track(truncated, row.source_chunk_ids !== text13(local.get(id2).source_chunk_ids));
23452
23468
  }
23453
- upload.push(row);
23454
- const track = (set, on) => on ? set.add(id2) : set.delete(id2);
23455
- track(reembedded, !!row.embedding && !usable.has(id2));
23456
- track(unembedded, !row.embedding);
23457
- track(placeholders, plaintext === void 0);
23458
- track(truncated, row.source_chunk_ids !== text13(local.get(id2).source_chunk_ids));
23459
- }
23460
- let sent = 0;
23461
- options.onProgress?.({ imported: sent, toImport: upload.length });
23462
- for (const batch of chunks(upload, IMPORT_BATCH)) {
23463
- await importBatch(request, canonical.keyId, batch, signal);
23464
- claimed = true;
23465
- sent += batch.length;
23469
+ let sent = 0;
23466
23470
  options.onProgress?.({ imported: sent, toImport: upload.length });
23471
+ for (const batch of chunks(upload, IMPORT_BATCH)) {
23472
+ await importBatch(request, canonical.keyId, batch, signal);
23473
+ claimed = true;
23474
+ sent += batch.length;
23475
+ options.onProgress?.({ imported: sent, toImport: upload.length });
23476
+ }
23477
+ imported += sent;
23467
23478
  }
23468
- imported += sent;
23469
- }
23479
+ };
23480
+ return uploadPasses();
23470
23481
  }
23471
23482
 
23472
23483
  // src/lib/memory/store/remoteStore.ts
@@ -25442,6 +25453,18 @@ async function assembleMemoryContext(options) {
25442
25453
 
25443
25454
  // src/lib/memory/durableExtraction.ts
25444
25455
  import { Q as Q17 } from "@nozbe/watermelondb";
25456
+ async function messagePositions(storage, ids) {
25457
+ try {
25458
+ return await storage.messagesCollection.query(
25459
+ Q17.unsafeSqlQuery(
25460
+ `select "id", "conversation_id", "message_id" from "history" where "_status" is not 'deleted' and "id" in (${ids.map(() => "?").join(",")})`,
25461
+ ids
25462
+ )
25463
+ ).unsafeFetchRaw();
25464
+ } catch {
25465
+ return await storage.messagesCollection.query(Q17.where("id", Q17.oneOf(ids))).unsafeFetchRaw();
25466
+ }
25467
+ }
25445
25468
  var MAX_JOBS_PER_PASS = 3;
25446
25469
  var MAX_ATTEMPTS = 3;
25447
25470
  var MAX_FAILED_SESSIONS = 3;
@@ -26010,9 +26033,9 @@ function createDurableAutoExtractor(options) {
26010
26033
  r._setRaw("watermark_seq", null);
26011
26034
  });
26012
26035
  }
26013
- const rows = fromSourceConversation ? [] : await storage.messagesCollection.query(Q17.where("id", Q17.oneOf(ids))).fetch();
26036
+ const rows = fromSourceConversation ? [] : await messagePositions(storage, ids);
26014
26037
  const foreignRows = new Set(
26015
- rows.filter((row) => row._getRaw("conversation_id") !== conversationId).map((row) => row.id)
26038
+ rows.filter((row) => row.conversation_id !== conversationId).map((row) => row.id)
26016
26039
  );
26017
26040
  const sequences = /* @__PURE__ */ new Map();
26018
26041
  if (fromSourceConversation) {
@@ -26022,7 +26045,7 @@ function createDurableAutoExtractor(options) {
26022
26045
  }
26023
26046
  for (const row of rows) {
26024
26047
  if (foreignRows.has(row.id)) continue;
26025
- const sequence = Number(row._getRaw("message_id"));
26048
+ const sequence = Number(row.message_id);
26026
26049
  if (Number.isFinite(sequence) && sequence > 0) sequences.set(row.id, sequence);
26027
26050
  }
26028
26051
  let boundary = -1;
@@ -17078,6 +17078,7 @@ async function getEncryptionKey(address, version = "v3") {
17078
17078
  if (cachedKey) {
17079
17079
  return cachedKey;
17080
17080
  }
17081
+ const startEpoch = sessionEpoch;
17081
17082
  const keyHex = getStoredKeyByVersion(address, version);
17082
17083
  if (!keyHex) {
17083
17084
  throw new EncryptionKeyMissingError(address, version);
@@ -17090,6 +17091,9 @@ async function getEncryptionKey(address, version = "v3") {
17090
17091
  false,
17091
17092
  ["encrypt", "decrypt"]
17092
17093
  );
17094
+ if (sessionEpoch !== startEpoch || getStoredKeyByVersion(address, version) !== keyHex) {
17095
+ throw new EncryptionKeyMissingError(address, version);
17096
+ }
17093
17097
  cryptoKeyCache.set(cacheKey, cryptoKey);
17094
17098
  return cryptoKey;
17095
17099
  }
@@ -37141,6 +37145,18 @@ async function assembleMemoryContext(options) {
37141
37145
 
37142
37146
  // src/lib/memory/durableExtraction.ts
37143
37147
  var import_watermelondb25 = __toESM(require_watermelondb());
37148
+ async function messagePositions(storage, ids) {
37149
+ try {
37150
+ return await storage.messagesCollection.query(
37151
+ import_watermelondb25.Q.unsafeSqlQuery(
37152
+ `select "id", "conversation_id", "message_id" from "history" where "_status" is not 'deleted' and "id" in (${ids.map(() => "?").join(",")})`,
37153
+ ids
37154
+ )
37155
+ ).unsafeFetchRaw();
37156
+ } catch {
37157
+ return await storage.messagesCollection.query(import_watermelondb25.Q.where("id", import_watermelondb25.Q.oneOf(ids))).unsafeFetchRaw();
37158
+ }
37159
+ }
37144
37160
  var MAX_JOBS_PER_PASS = 3;
37145
37161
  var MAX_ATTEMPTS = 3;
37146
37162
  var MAX_FAILED_SESSIONS = 3;
@@ -37709,9 +37725,9 @@ function createDurableAutoExtractor(options) {
37709
37725
  r._setRaw("watermark_seq", null);
37710
37726
  });
37711
37727
  }
37712
- const rows = fromSourceConversation ? [] : await storage.messagesCollection.query(import_watermelondb25.Q.where("id", import_watermelondb25.Q.oneOf(ids))).fetch();
37728
+ const rows = fromSourceConversation ? [] : await messagePositions(storage, ids);
37713
37729
  const foreignRows = new Set(
37714
- rows.filter((row) => row._getRaw("conversation_id") !== conversationId).map((row) => row.id)
37730
+ rows.filter((row) => row.conversation_id !== conversationId).map((row) => row.id)
37715
37731
  );
37716
37732
  const sequences = /* @__PURE__ */ new Map();
37717
37733
  if (fromSourceConversation) {
@@ -37721,7 +37737,7 @@ function createDurableAutoExtractor(options) {
37721
37737
  }
37722
37738
  for (const row of rows) {
37723
37739
  if (foreignRows.has(row.id)) continue;
37724
- const sequence = Number(row._getRaw("message_id"));
37740
+ const sequence = Number(row.message_id);
37725
37741
  if (Number.isFinite(sequence) && sequence > 0) sequences.set(row.id, sequence);
37726
37742
  }
37727
37743
  let boundary = -1;
@@ -15407,6 +15407,7 @@ async function getEncryptionKey(address, version = "v3") {
15407
15407
  if (cachedKey) {
15408
15408
  return cachedKey;
15409
15409
  }
15410
+ const startEpoch = sessionEpoch;
15410
15411
  const keyHex = getStoredKeyByVersion(address, version);
15411
15412
  if (!keyHex) {
15412
15413
  throw new EncryptionKeyMissingError(address, version);
@@ -15419,6 +15420,9 @@ async function getEncryptionKey(address, version = "v3") {
15419
15420
  false,
15420
15421
  ["encrypt", "decrypt"]
15421
15422
  );
15423
+ if (sessionEpoch !== startEpoch || getStoredKeyByVersion(address, version) !== keyHex) {
15424
+ throw new EncryptionKeyMissingError(address, version);
15425
+ }
15422
15426
  cryptoKeyCache.set(cacheKey, cryptoKey);
15423
15427
  return cryptoKey;
15424
15428
  }
@@ -35388,6 +35392,18 @@ async function assembleMemoryContext(options) {
35388
35392
 
35389
35393
  // src/lib/memory/durableExtraction.ts
35390
35394
  var import_watermelondb25 = __toESM(require_watermelondb());
35395
+ async function messagePositions(storage, ids) {
35396
+ try {
35397
+ return await storage.messagesCollection.query(
35398
+ import_watermelondb25.Q.unsafeSqlQuery(
35399
+ `select "id", "conversation_id", "message_id" from "history" where "_status" is not 'deleted' and "id" in (${ids.map(() => "?").join(",")})`,
35400
+ ids
35401
+ )
35402
+ ).unsafeFetchRaw();
35403
+ } catch {
35404
+ return await storage.messagesCollection.query(import_watermelondb25.Q.where("id", import_watermelondb25.Q.oneOf(ids))).unsafeFetchRaw();
35405
+ }
35406
+ }
35391
35407
  var MAX_JOBS_PER_PASS = 3;
35392
35408
  var MAX_ATTEMPTS = 3;
35393
35409
  var MAX_FAILED_SESSIONS = 3;
@@ -35956,9 +35972,9 @@ function createDurableAutoExtractor(options) {
35956
35972
  r._setRaw("watermark_seq", null);
35957
35973
  });
35958
35974
  }
35959
- const rows = fromSourceConversation ? [] : await storage.messagesCollection.query(import_watermelondb25.Q.where("id", import_watermelondb25.Q.oneOf(ids))).fetch();
35975
+ const rows = fromSourceConversation ? [] : await messagePositions(storage, ids);
35960
35976
  const foreignRows = new Set(
35961
- rows.filter((row) => row._getRaw("conversation_id") !== conversationId).map((row) => row.id)
35977
+ rows.filter((row) => row.conversation_id !== conversationId).map((row) => row.id)
35962
35978
  );
35963
35979
  const sequences = /* @__PURE__ */ new Map();
35964
35980
  if (fromSourceConversation) {
@@ -35968,7 +35984,7 @@ function createDurableAutoExtractor(options) {
35968
35984
  }
35969
35985
  for (const row of rows) {
35970
35986
  if (foreignRows.has(row.id)) continue;
35971
- const sequence = Number(row._getRaw("message_id"));
35987
+ const sequence = Number(row.message_id);
35972
35988
  if (Number.isFinite(sequence) && sequence > 0) sequences.set(row.id, sequence);
35973
35989
  }
35974
35990
  let boundary = -1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anuma/sdk",
3
- "version": "9.1.2",
3
+ "version": "9.1.4",
4
4
  "description": "",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",