@anuma/sdk 6.8.1 → 6.9.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.
@@ -1891,9 +1891,10 @@ async function updateMessageChunksOp(ctx, uniqueId, chunks, embeddingModel) {
1891
1891
  } catch {
1892
1892
  return null;
1893
1893
  }
1894
+ const storedChunks = chunks.map(({ text: _text, ...rest }) => rest);
1894
1895
  await ctx.database.write(async () => {
1895
1896
  await message.update((msg) => {
1896
- msg._setRaw("chunks", JSON.stringify(chunks));
1897
+ msg._setRaw("chunks", JSON.stringify(storedChunks));
1897
1898
  msg._setRaw("embedding_model", embeddingModel);
1898
1899
  });
1899
1900
  });
@@ -1991,6 +1992,16 @@ async function _updateMessageOp(ctx, uniqueId, opts) {
1991
1992
  });
1992
1993
  return messageToStored(message, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner);
1993
1994
  }
1995
+ function resolveChunkText(content, chunk, chunksInRow) {
1996
+ if (!chunk) return content;
1997
+ if (typeof chunk.text === "string" && chunk.text.length > 0) return chunk.text;
1998
+ const { startOffset: start, endOffset: end } = chunk;
1999
+ if (!Number.isInteger(start) || !Number.isInteger(end)) return content;
2000
+ if (start < 0 || end <= start || end > content.length) return content;
2001
+ const covered = chunksInRow?.reduce((max, c) => Math.max(max, c.endOffset ?? 0), 0) ?? 0;
2002
+ if (covered !== content.length) return content;
2003
+ return content.slice(start, end);
2004
+ }
1994
2005
  async function readJsonField(model, column, walletAddress) {
1995
2006
  const raw = model._getRaw(column);
1996
2007
  if (!raw) return void 0;
@@ -2104,7 +2115,7 @@ async function searchChunksOp(ctx, queryVector, options) {
2104
2115
  candidates.push({
2105
2116
  message,
2106
2117
  similarity,
2107
- chunkTextSource: { kind: "chunk", text: chunks[ci].text }
2118
+ chunkTextSource: { kind: "chunk", chunk: chunks[ci], siblings: chunks }
2108
2119
  });
2109
2120
  }
2110
2121
  }
@@ -2162,11 +2173,11 @@ async function searchChunksOp(ctx, queryVector, options) {
2162
2173
  const stored = { ...storedById.get(message.id) };
2163
2174
  let chunkText2;
2164
2175
  if (chunkTextSource.kind === "chunk") {
2165
- chunkText2 = chunkTextSource.text;
2176
+ chunkText2 = resolveChunkText(stored.content, chunkTextSource.chunk, chunkTextSource.siblings);
2166
2177
  } else if (chunkTextSource.kind === "chunk-cached") {
2167
2178
  const state = chunkStateByMsgId.get(message.id);
2168
2179
  const chunk = state && state.version === chunkTextSource.version ? state.chunks[chunkTextSource.chunkIndex] : void 0;
2169
- chunkText2 = chunk?.text ?? stored.content;
2180
+ chunkText2 = resolveChunkText(stored.content, chunk, state?.chunks);
2170
2181
  } else {
2171
2182
  chunkText2 = stored.content;
2172
2183
  }
@@ -2310,13 +2321,13 @@ async function upsertEntitiesInWrite(ctx, entities) {
2310
2321
  }
2311
2322
  const unique = Array.from(kindByName.keys());
2312
2323
  const out = /* @__PURE__ */ new Map();
2313
- if (unique.length === 0) return { entities: out, operations: [] };
2324
+ if (unique.length === 0) return { entities: out, operations: [], kindBackfills: [] };
2314
2325
  const existing = await ctx.entityCollection.query(Q2.where("canonical_name", Q2.oneOf(unique))).fetch();
2315
2326
  const existingNames = new Set(existing.map((e) => e.canonicalName));
2316
- const updates = existing.filter((e) => {
2327
+ const kindBackfills = existing.filter((e) => {
2317
2328
  const incoming = kindByName.get(e.canonicalName);
2318
2329
  return incoming !== void 0 && (e.kind === null || e.kind === void 0 || e.kind === "");
2319
- });
2330
+ }).map((record) => ({ record, kind: kindByName.get(record.canonicalName) }));
2320
2331
  const missing = unique.filter((n) => !existingNames.has(n));
2321
2332
  const created = missing.map(
2322
2333
  (name) => ctx.entityCollection.prepareCreate((record) => {
@@ -2325,14 +2336,19 @@ async function upsertEntitiesInWrite(ctx, entities) {
2325
2336
  if (kind !== void 0) record._setRaw("kind", kind);
2326
2337
  })
2327
2338
  );
2328
- const updated = updates.map(
2329
- (e) => e.prepareUpdate((record) => {
2330
- record._setRaw("kind", kindByName.get(e.canonicalName));
2331
- })
2332
- );
2333
2339
  for (const e of existing) out.set(e.canonicalName, entityToStored(e));
2334
2340
  for (const record of created) out.set(record.canonicalName, entityToStored(record));
2335
- return { entities: out, operations: [...created, ...updated] };
2341
+ for (const { record, kind } of kindBackfills) {
2342
+ out.set(record.canonicalName, { ...entityToStored(record), kind });
2343
+ }
2344
+ return { entities: out, operations: created, kindBackfills };
2345
+ }
2346
+ function prepareKindBackfills(backfills) {
2347
+ return backfills.map(
2348
+ ({ record, kind }) => record.prepareUpdate((r) => {
2349
+ r._setRaw("kind", kind);
2350
+ })
2351
+ );
2336
2352
  }
2337
2353
  async function findVaultRowInWrite(ctx, memoryId) {
2338
2354
  try {
@@ -2382,10 +2398,12 @@ function topicsForEntities(entities, displayNames, source) {
2382
2398
  function toEntityObjects(entityInputs) {
2383
2399
  return entityInputs.map((e) => typeof e === "string" ? { name: e } : e);
2384
2400
  }
2385
- async function prepareMemoryTopicsUpdate(ctx, memoryId, linked, inputs, source) {
2401
+ async function resolveMemoryTopicsWrite(ctx, memoryId, linked, inputs, source) {
2386
2402
  const row = await findVaultRowInWrite(ctx, memoryId);
2387
- const write = resolveTopicsWrite(row, linked, displayNamesOf(toEntityObjects(inputs)), source);
2388
- return write === null ? null : prepareTopicsUpdate(write.row, write.topics, Date.now());
2403
+ return resolveTopicsWrite(row, linked, displayNamesOf(toEntityObjects(inputs)), source);
2404
+ }
2405
+ function prepareMemoryTopicsUpdate(write) {
2406
+ return prepareTopicsUpdate(write.row, write.topics, Date.now());
2389
2407
  }
2390
2408
  async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
2391
2409
  if (entityInputs.length === 0) return [];
@@ -2394,15 +2412,16 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
2394
2412
  let skipped = false;
2395
2413
  let entities = [];
2396
2414
  await ctx.database.write(async () => {
2397
- const { entities: byName, operations: entityOps } = await upsertEntitiesInWrite(
2398
- ctx,
2399
- normalized
2400
- );
2415
+ const {
2416
+ entities: byName,
2417
+ operations: entityOps,
2418
+ kindBackfills
2419
+ } = await upsertEntitiesInWrite(ctx, normalized);
2401
2420
  entities = Array.from(byName.values());
2402
2421
  const vaultRow = await findVaultRowInWrite(ctx, memoryId);
2403
2422
  if (options?.unlessTopicsUserManaged && autoLinkBlocked(vaultRow)) {
2404
- if (entityOps.length > 0) {
2405
- await ctx.database.batch(...entityOps);
2423
+ if (entityOps.length > 0 || kindBackfills.length > 0) {
2424
+ await ctx.database.batch(...entityOps, ...prepareKindBackfills(kindBackfills));
2406
2425
  }
2407
2426
  skipped = true;
2408
2427
  return;
@@ -2422,7 +2441,9 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
2422
2441
  displayNamesOf(normalized),
2423
2442
  options?.topicsSource ?? "auto"
2424
2443
  );
2425
- if (entityOps.length === 0 && toCreate.length === 0 && topicsWrite === null) return;
2444
+ if (entityOps.length === 0 && kindBackfills.length === 0 && toCreate.length === 0 && topicsWrite === null) {
2445
+ return;
2446
+ }
2426
2447
  const linkOps = toCreate.map(
2427
2448
  (e) => ctx.memoryEntityCollection.prepareCreate((record) => {
2428
2449
  record._setRaw("memory_id", memoryId);
@@ -2432,6 +2453,7 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
2432
2453
  );
2433
2454
  await ctx.database.batch(
2434
2455
  ...entityOps,
2456
+ ...prepareKindBackfills(kindBackfills),
2435
2457
  ...linkOps,
2436
2458
  ...topicsWrite ? [prepareTopicsUpdate(topicsWrite.row, topicsWrite.topics, Date.now())] : []
2437
2459
  );
@@ -2469,10 +2491,11 @@ async function replaceMemoryEntities(ctx, memoryId, entityInputs, options) {
2469
2491
  skipped = true;
2470
2492
  return;
2471
2493
  }
2472
- const { entities: byName, operations: entityOps } = await upsertEntitiesInWrite(
2473
- ctx,
2474
- normalized
2475
- );
2494
+ const {
2495
+ entities: byName,
2496
+ operations: entityOps,
2497
+ kindBackfills
2498
+ } = await upsertEntitiesInWrite(ctx, normalized);
2476
2499
  entities = Array.from(byName.values());
2477
2500
  const existingLinks = await ctx.memoryEntityCollection.query(Q2.where("memory_id", memoryId)).fetch();
2478
2501
  const keep = new Set(entities.map((e) => e.uniqueId));
@@ -2485,12 +2508,13 @@ async function replaceMemoryEntities(ctx, memoryId, entityInputs, options) {
2485
2508
  displayNamesOf(normalized),
2486
2509
  options.topicsSource
2487
2510
  );
2488
- if (entityOps.length === 0 && toCreate.length === 0 && toDestroy.length === 0 && topicsWrite === null) {
2511
+ if (entityOps.length === 0 && kindBackfills.length === 0 && toCreate.length === 0 && toDestroy.length === 0 && topicsWrite === null) {
2489
2512
  return;
2490
2513
  }
2491
2514
  const orphans = await findOrphanedEntities(ctx, memoryId, toDestroy);
2492
2515
  await ctx.database.batch(
2493
2516
  ...entityOps,
2517
+ ...prepareKindBackfills(kindBackfills),
2494
2518
  ...toCreate.map(
2495
2519
  (e) => ctx.memoryEntityCollection.prepareCreate((record) => {
2496
2520
  record._setRaw("memory_id", memoryId);
@@ -3303,10 +3327,16 @@ async function setMemoryEntitiesOp(ctx, memoryId, entities) {
3303
3327
  const existing = await entityCtx.memoryEntityCollection.query(Q3.where("memory_id", memoryId)).fetch();
3304
3328
  const staleLinks = existing.filter((l) => !keep.has(String(l.entityId)));
3305
3329
  if (staleLinks.length === 0 && entities.length > 0) return;
3306
- const topicsOp = await prepareMemoryTopicsUpdate(entityCtx, memoryId, linked, entities, "user");
3330
+ const topicsWrite = await resolveMemoryTopicsWrite(
3331
+ entityCtx,
3332
+ memoryId,
3333
+ linked,
3334
+ entities,
3335
+ "user"
3336
+ );
3307
3337
  await ctx.database.batch(
3308
3338
  ...staleLinks.map((l) => l.prepareDestroyPermanently()),
3309
- ...topicsOp ? [topicsOp] : []
3339
+ ...topicsWrite ? [prepareMemoryTopicsUpdate(topicsWrite)] : []
3310
3340
  );
3311
3341
  });
3312
3342
  if (stale) return null;
@@ -3625,27 +3655,36 @@ async function backfillMemoryTopicsOp(ctx, memoryIds) {
3625
3655
  if (!entityCtx) {
3626
3656
  throw new Error("backfillMemoryTopicsOp requires ctx.entityCtx");
3627
3657
  }
3658
+ const uniqueIds = Array.from(new Set(memoryIds));
3659
+ if (uniqueIds.length === 0) return [];
3628
3660
  const filled = [];
3629
- for (const id of Array.from(new Set(memoryIds))) {
3630
- let record;
3631
- try {
3632
- record = await ctx.vaultMemoryCollection.find(id);
3633
- } catch {
3634
- continue;
3635
- }
3636
- if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) continue;
3637
- if (parseTopics(record.topics) !== null) continue;
3638
- const links = await entityCtx.memoryEntityCollection.query(Q3.where("memory_id", id)).unsafeFetchRaw();
3639
- const entityIds = links.map((l) => String(l.entity_id));
3640
- if (entityIds.length === 0) continue;
3641
- const entities = await entityCtx.entityCollection.query(Q3.where("id", Q3.oneOf(entityIds))).fetch();
3642
- if (entities.length === 0) continue;
3643
- const source = record.topicsUserManaged ? "user" : "auto";
3661
+ const CHUNK = 500;
3662
+ for (let i = 0; i < uniqueIds.length; i += CHUNK) {
3663
+ const chunkIds = uniqueIds.slice(i, i + CHUNK);
3644
3664
  await ctx.database.write(async () => {
3645
- const topicsOp = await prepareMemoryTopicsUpdate(entityCtx, id, entities, [], source);
3646
- if (!topicsOp) return;
3647
- await ctx.database.batch(topicsOp);
3648
- filled.push(id);
3665
+ const writes = [];
3666
+ for (const id of chunkIds) {
3667
+ let record;
3668
+ try {
3669
+ record = await ctx.vaultMemoryCollection.find(id);
3670
+ } catch {
3671
+ continue;
3672
+ }
3673
+ if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) continue;
3674
+ if (parseTopics(record.topics) !== null) continue;
3675
+ const links = await entityCtx.memoryEntityCollection.query(Q3.where("memory_id", id)).unsafeFetchRaw();
3676
+ const entityIds = links.map((l) => String(l.entity_id));
3677
+ if (entityIds.length === 0) continue;
3678
+ const entities = await entityCtx.entityCollection.query(Q3.where("id", Q3.oneOf(entityIds))).fetch();
3679
+ if (entities.length === 0) continue;
3680
+ const source = record.topicsUserManaged ? "user" : "auto";
3681
+ const write = await resolveMemoryTopicsWrite(entityCtx, id, entities, [], source);
3682
+ if (write) writes.push(write);
3683
+ }
3684
+ if (writes.length === 0) return;
3685
+ const prepared = writes.map(prepareMemoryTopicsUpdate);
3686
+ await ctx.database.batch(...prepared);
3687
+ for (const write of writes) filled.push(write.row.id);
3649
3688
  });
3650
3689
  }
3651
3690
  return filled;
@@ -7085,7 +7124,7 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
7085
7124
  subQueries = decomp.subQueries;
7086
7125
  }
7087
7126
  }
7088
- const { recall: recall2 } = await import("./recall-B2AIAPCM.mjs");
7127
+ const { recall: recall2 } = await import("./recall-FWILZSFH.mjs");
7089
7128
  let recallDegraded = [];
7090
7129
  const result = await recall2(
7091
7130
  query,
@@ -2338,9 +2338,10 @@ async function updateMessageChunksOp(ctx, uniqueId, chunks, embeddingModel) {
2338
2338
  } catch {
2339
2339
  return null;
2340
2340
  }
2341
+ const storedChunks = chunks.map(({ text: _text, ...rest }) => rest);
2341
2342
  await ctx.database.write(async () => {
2342
2343
  await message.update((msg) => {
2343
- msg._setRaw("chunks", JSON.stringify(chunks));
2344
+ msg._setRaw("chunks", JSON.stringify(storedChunks));
2344
2345
  msg._setRaw("embedding_model", embeddingModel);
2345
2346
  });
2346
2347
  });
@@ -2438,6 +2439,16 @@ async function _updateMessageOp(ctx, uniqueId, opts) {
2438
2439
  });
2439
2440
  return messageToStored(message, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner);
2440
2441
  }
2442
+ function resolveChunkText(content, chunk, chunksInRow) {
2443
+ if (!chunk) return content;
2444
+ if (typeof chunk.text === "string" && chunk.text.length > 0) return chunk.text;
2445
+ const { startOffset: start, endOffset: end } = chunk;
2446
+ if (!Number.isInteger(start) || !Number.isInteger(end)) return content;
2447
+ if (start < 0 || end <= start || end > content.length) return content;
2448
+ const covered = chunksInRow?.reduce((max, c) => Math.max(max, c.endOffset ?? 0), 0) ?? 0;
2449
+ if (covered !== content.length) return content;
2450
+ return content.slice(start, end);
2451
+ }
2441
2452
  async function readJsonField(model, column, walletAddress) {
2442
2453
  const raw = model._getRaw(column);
2443
2454
  if (!raw) return void 0;
@@ -2551,7 +2562,7 @@ async function searchChunksOp(ctx, queryVector, options) {
2551
2562
  candidates.push({
2552
2563
  message,
2553
2564
  similarity,
2554
- chunkTextSource: { kind: "chunk", text: chunks[ci].text }
2565
+ chunkTextSource: { kind: "chunk", chunk: chunks[ci], siblings: chunks }
2555
2566
  });
2556
2567
  }
2557
2568
  }
@@ -2609,11 +2620,11 @@ async function searchChunksOp(ctx, queryVector, options) {
2609
2620
  const stored = { ...storedById.get(message.id) };
2610
2621
  let chunkText2;
2611
2622
  if (chunkTextSource.kind === "chunk") {
2612
- chunkText2 = chunkTextSource.text;
2623
+ chunkText2 = resolveChunkText(stored.content, chunkTextSource.chunk, chunkTextSource.siblings);
2613
2624
  } else if (chunkTextSource.kind === "chunk-cached") {
2614
2625
  const state = chunkStateByMsgId.get(message.id);
2615
2626
  const chunk = state && state.version === chunkTextSource.version ? state.chunks[chunkTextSource.chunkIndex] : void 0;
2616
- chunkText2 = chunk?.text ?? stored.content;
2627
+ chunkText2 = resolveChunkText(stored.content, chunk, state?.chunks);
2617
2628
  } else {
2618
2629
  chunkText2 = stored.content;
2619
2630
  }
@@ -4877,13 +4888,13 @@ async function upsertEntitiesInWrite(ctx, entities) {
4877
4888
  }
4878
4889
  const unique = Array.from(kindByName.keys());
4879
4890
  const out = /* @__PURE__ */ new Map();
4880
- if (unique.length === 0) return { entities: out, operations: [] };
4891
+ if (unique.length === 0) return { entities: out, operations: [], kindBackfills: [] };
4881
4892
  const existing = await ctx.entityCollection.query(import_watermelondb9.Q.where("canonical_name", import_watermelondb9.Q.oneOf(unique))).fetch();
4882
4893
  const existingNames = new Set(existing.map((e) => e.canonicalName));
4883
- const updates = existing.filter((e) => {
4894
+ const kindBackfills = existing.filter((e) => {
4884
4895
  const incoming = kindByName.get(e.canonicalName);
4885
4896
  return incoming !== void 0 && (e.kind === null || e.kind === void 0 || e.kind === "");
4886
- });
4897
+ }).map((record) => ({ record, kind: kindByName.get(record.canonicalName) }));
4887
4898
  const missing = unique.filter((n) => !existingNames.has(n));
4888
4899
  const created = missing.map(
4889
4900
  (name) => ctx.entityCollection.prepareCreate((record) => {
@@ -4892,14 +4903,19 @@ async function upsertEntitiesInWrite(ctx, entities) {
4892
4903
  if (kind !== void 0) record._setRaw("kind", kind);
4893
4904
  })
4894
4905
  );
4895
- const updated = updates.map(
4896
- (e) => e.prepareUpdate((record) => {
4897
- record._setRaw("kind", kindByName.get(e.canonicalName));
4898
- })
4899
- );
4900
4906
  for (const e of existing) out.set(e.canonicalName, entityToStored(e));
4901
4907
  for (const record of created) out.set(record.canonicalName, entityToStored(record));
4902
- return { entities: out, operations: [...created, ...updated] };
4908
+ for (const { record, kind } of kindBackfills) {
4909
+ out.set(record.canonicalName, { ...entityToStored(record), kind });
4910
+ }
4911
+ return { entities: out, operations: created, kindBackfills };
4912
+ }
4913
+ function prepareKindBackfills(backfills) {
4914
+ return backfills.map(
4915
+ ({ record, kind }) => record.prepareUpdate((r) => {
4916
+ r._setRaw("kind", kind);
4917
+ })
4918
+ );
4903
4919
  }
4904
4920
  async function findVaultRowInWrite(ctx, memoryId) {
4905
4921
  try {
@@ -4949,10 +4965,12 @@ function topicsForEntities(entities, displayNames, source) {
4949
4965
  function toEntityObjects(entityInputs) {
4950
4966
  return entityInputs.map((e) => typeof e === "string" ? { name: e } : e);
4951
4967
  }
4952
- async function prepareMemoryTopicsUpdate(ctx, memoryId, linked, inputs, source) {
4968
+ async function resolveMemoryTopicsWrite(ctx, memoryId, linked, inputs, source) {
4953
4969
  const row = await findVaultRowInWrite(ctx, memoryId);
4954
- const write = resolveTopicsWrite(row, linked, displayNamesOf(toEntityObjects(inputs)), source);
4955
- return write === null ? null : prepareTopicsUpdate(write.row, write.topics, Date.now());
4970
+ return resolveTopicsWrite(row, linked, displayNamesOf(toEntityObjects(inputs)), source);
4971
+ }
4972
+ function prepareMemoryTopicsUpdate(write) {
4973
+ return prepareTopicsUpdate(write.row, write.topics, Date.now());
4956
4974
  }
4957
4975
  async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
4958
4976
  if (entityInputs.length === 0) return [];
@@ -4961,15 +4979,16 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
4961
4979
  let skipped = false;
4962
4980
  let entities = [];
4963
4981
  await ctx.database.write(async () => {
4964
- const { entities: byName, operations: entityOps } = await upsertEntitiesInWrite(
4965
- ctx,
4966
- normalized
4967
- );
4982
+ const {
4983
+ entities: byName,
4984
+ operations: entityOps,
4985
+ kindBackfills
4986
+ } = await upsertEntitiesInWrite(ctx, normalized);
4968
4987
  entities = Array.from(byName.values());
4969
4988
  const vaultRow = await findVaultRowInWrite(ctx, memoryId);
4970
4989
  if (options?.unlessTopicsUserManaged && autoLinkBlocked(vaultRow)) {
4971
- if (entityOps.length > 0) {
4972
- await ctx.database.batch(...entityOps);
4990
+ if (entityOps.length > 0 || kindBackfills.length > 0) {
4991
+ await ctx.database.batch(...entityOps, ...prepareKindBackfills(kindBackfills));
4973
4992
  }
4974
4993
  skipped = true;
4975
4994
  return;
@@ -4989,7 +5008,9 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
4989
5008
  displayNamesOf(normalized),
4990
5009
  options?.topicsSource ?? "auto"
4991
5010
  );
4992
- if (entityOps.length === 0 && toCreate.length === 0 && topicsWrite === null) return;
5011
+ if (entityOps.length === 0 && kindBackfills.length === 0 && toCreate.length === 0 && topicsWrite === null) {
5012
+ return;
5013
+ }
4993
5014
  const linkOps = toCreate.map(
4994
5015
  (e) => ctx.memoryEntityCollection.prepareCreate((record) => {
4995
5016
  record._setRaw("memory_id", memoryId);
@@ -4999,6 +5020,7 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
4999
5020
  );
5000
5021
  await ctx.database.batch(
5001
5022
  ...entityOps,
5023
+ ...prepareKindBackfills(kindBackfills),
5002
5024
  ...linkOps,
5003
5025
  ...topicsWrite ? [prepareTopicsUpdate(topicsWrite.row, topicsWrite.topics, Date.now())] : []
5004
5026
  );
@@ -5036,10 +5058,11 @@ async function replaceMemoryEntities(ctx, memoryId, entityInputs, options) {
5036
5058
  skipped = true;
5037
5059
  return;
5038
5060
  }
5039
- const { entities: byName, operations: entityOps } = await upsertEntitiesInWrite(
5040
- ctx,
5041
- normalized
5042
- );
5061
+ const {
5062
+ entities: byName,
5063
+ operations: entityOps,
5064
+ kindBackfills
5065
+ } = await upsertEntitiesInWrite(ctx, normalized);
5043
5066
  entities = Array.from(byName.values());
5044
5067
  const existingLinks = await ctx.memoryEntityCollection.query(import_watermelondb9.Q.where("memory_id", memoryId)).fetch();
5045
5068
  const keep = new Set(entities.map((e) => e.uniqueId));
@@ -5052,12 +5075,13 @@ async function replaceMemoryEntities(ctx, memoryId, entityInputs, options) {
5052
5075
  displayNamesOf(normalized),
5053
5076
  options.topicsSource
5054
5077
  );
5055
- if (entityOps.length === 0 && toCreate.length === 0 && toDestroy.length === 0 && topicsWrite === null) {
5078
+ if (entityOps.length === 0 && kindBackfills.length === 0 && toCreate.length === 0 && toDestroy.length === 0 && topicsWrite === null) {
5056
5079
  return;
5057
5080
  }
5058
5081
  const orphans = await findOrphanedEntities(ctx, memoryId, toDestroy);
5059
5082
  await ctx.database.batch(
5060
5083
  ...entityOps,
5084
+ ...prepareKindBackfills(kindBackfills),
5061
5085
  ...toCreate.map(
5062
5086
  (e) => ctx.memoryEntityCollection.prepareCreate((record) => {
5063
5087
  record._setRaw("memory_id", memoryId);
@@ -5886,10 +5910,16 @@ async function setMemoryEntitiesOp(ctx, memoryId, entities) {
5886
5910
  const existing = await entityCtx.memoryEntityCollection.query(import_watermelondb10.Q.where("memory_id", memoryId)).fetch();
5887
5911
  const staleLinks = existing.filter((l) => !keep.has(String(l.entityId)));
5888
5912
  if (staleLinks.length === 0 && entities.length > 0) return;
5889
- const topicsOp = await prepareMemoryTopicsUpdate(entityCtx, memoryId, linked, entities, "user");
5913
+ const topicsWrite = await resolveMemoryTopicsWrite(
5914
+ entityCtx,
5915
+ memoryId,
5916
+ linked,
5917
+ entities,
5918
+ "user"
5919
+ );
5890
5920
  await ctx.database.batch(
5891
5921
  ...staleLinks.map((l) => l.prepareDestroyPermanently()),
5892
- ...topicsOp ? [topicsOp] : []
5922
+ ...topicsWrite ? [prepareMemoryTopicsUpdate(topicsWrite)] : []
5893
5923
  );
5894
5924
  });
5895
5925
  if (stale) return null;
@@ -6207,27 +6237,36 @@ async function backfillMemoryTopicsOp(ctx, memoryIds) {
6207
6237
  if (!entityCtx) {
6208
6238
  throw new Error("backfillMemoryTopicsOp requires ctx.entityCtx");
6209
6239
  }
6240
+ const uniqueIds = Array.from(new Set(memoryIds));
6241
+ if (uniqueIds.length === 0) return [];
6210
6242
  const filled = [];
6211
- for (const id of Array.from(new Set(memoryIds))) {
6212
- let record;
6213
- try {
6214
- record = await ctx.vaultMemoryCollection.find(id);
6215
- } catch {
6216
- continue;
6217
- }
6218
- if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) continue;
6219
- if (parseTopics(record.topics) !== null) continue;
6220
- const links = await entityCtx.memoryEntityCollection.query(import_watermelondb10.Q.where("memory_id", id)).unsafeFetchRaw();
6221
- const entityIds = links.map((l) => String(l.entity_id));
6222
- if (entityIds.length === 0) continue;
6223
- const entities = await entityCtx.entityCollection.query(import_watermelondb10.Q.where("id", import_watermelondb10.Q.oneOf(entityIds))).fetch();
6224
- if (entities.length === 0) continue;
6225
- const source = record.topicsUserManaged ? "user" : "auto";
6243
+ const CHUNK = 500;
6244
+ for (let i = 0; i < uniqueIds.length; i += CHUNK) {
6245
+ const chunkIds = uniqueIds.slice(i, i + CHUNK);
6226
6246
  await ctx.database.write(async () => {
6227
- const topicsOp = await prepareMemoryTopicsUpdate(entityCtx, id, entities, [], source);
6228
- if (!topicsOp) return;
6229
- await ctx.database.batch(topicsOp);
6230
- filled.push(id);
6247
+ const writes = [];
6248
+ for (const id of chunkIds) {
6249
+ let record;
6250
+ try {
6251
+ record = await ctx.vaultMemoryCollection.find(id);
6252
+ } catch {
6253
+ continue;
6254
+ }
6255
+ if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) continue;
6256
+ if (parseTopics(record.topics) !== null) continue;
6257
+ const links = await entityCtx.memoryEntityCollection.query(import_watermelondb10.Q.where("memory_id", id)).unsafeFetchRaw();
6258
+ const entityIds = links.map((l) => String(l.entity_id));
6259
+ if (entityIds.length === 0) continue;
6260
+ const entities = await entityCtx.entityCollection.query(import_watermelondb10.Q.where("id", import_watermelondb10.Q.oneOf(entityIds))).fetch();
6261
+ if (entities.length === 0) continue;
6262
+ const source = record.topicsUserManaged ? "user" : "auto";
6263
+ const write = await resolveMemoryTopicsWrite(entityCtx, id, entities, [], source);
6264
+ if (write) writes.push(write);
6265
+ }
6266
+ if (writes.length === 0) return;
6267
+ const prepared = writes.map(prepareMemoryTopicsUpdate);
6268
+ await ctx.database.batch(...prepared);
6269
+ for (const write of writes) filled.push(write.row.id);
6231
6270
  });
6232
6271
  }
6233
6272
  return filled;
@@ -18376,7 +18415,7 @@ function createAutoExtractor(options) {
18376
18415
  const t0 = Date.now();
18377
18416
  void (async () => {
18378
18417
  try {
18379
- const { candidates, results, failedCount, outcome } = await extractAndRetain(
18418
+ const { candidates, results, failedCount, outcome, failure } = await extractAndRetain(
18380
18419
  window2,
18381
18420
  retainCtx,
18382
18421
  {
@@ -18416,7 +18455,11 @@ function createAutoExtractor(options) {
18416
18455
  failedCount,
18417
18456
  durationMs: Date.now() - t0,
18418
18457
  conversationId,
18419
- outcome
18458
+ outcome,
18459
+ // Only set alongside `outcome: "empty-after-retry"`. Spread so the key
18460
+ // is absent rather than explicitly undefined on a healthy turn —
18461
+ // analytics backends store an explicit undefined as a real value.
18462
+ ...failure !== void 0 && { failure }
18420
18463
  });
18421
18464
  } catch (err) {
18422
18465
  options.onError?.(err instanceof Error ? err : new Error(String(err)), conversationId);
@@ -5055,8 +5055,26 @@ interface StoredMessageWithSimilarity extends StoredMessage {
5055
5055
  * A chunk of a message with its own embedding for fine-grained search
5056
5056
  */
5057
5057
  interface MessageChunk {
5058
- /** The chunk text */
5059
- text: string;
5058
+ /**
5059
+ * The chunk text — IN MEMORY ONLY. Not persisted (sdk#880).
5060
+ *
5061
+ * `chunkText` covers a message end to end with a 50-character overlap, so the
5062
+ * chunk set of any message over 400 characters reconstructs the whole thing.
5063
+ * Storing it put a fully readable copy of the message next to its own
5064
+ * ciphertext `content` on devices with at-rest encryption enabled.
5065
+ *
5066
+ * `updateMessageChunksOp` now strips this before writing and readers rebuild
5067
+ * the snippet from {@link startOffset}/{@link endOffset} against the message's
5068
+ * own (decrypted) `content` — so the text exists exactly once, under the
5069
+ * protection `content` already has. Encrypting the column instead was
5070
+ * rejected: the client reads this column raw and `JSON.parse`s it in four
5071
+ * places, each swallowing the throw and silently scoring 0.
5072
+ *
5073
+ * Optional because rows written before that change still carry it, and
5074
+ * readers prefer a stored value when present. It is populated in memory by
5075
+ * `chunkText()` and on the way out of a search.
5076
+ */
5077
+ text?: string;
5060
5078
  /**
5061
5079
  * Embedding vector for this chunk.
5062
5080
  *
@@ -8096,6 +8114,14 @@ declare function relinkMemoryTopicsOp(ctx: VaultMemoryOperationsContext, memoryI
8096
8114
  * row has: a curated memory's topics are recorded as `user`, everything else as
8097
8115
  * `auto`. Skips deleted, foreign-user, unlinked rows, and rows that already have
8098
8116
  * a record. Returns the ids filled.
8117
+ *
8118
+ * Runs as ONE writer over the whole (caller-bounded) list, in two phases:
8119
+ * resolve every row's write — all the awaits — and only then prepare and batch,
8120
+ * synchronously. Resolving and preparing per row inside its own writer is what
8121
+ * fired WatermelonDB's "wasn't sent to batch() synchronously" diagnostic once
8122
+ * per memory (~107 on a single dev launch, sdk#891); it also cost N writes.
8123
+ * Same treatment as {@link stampTopicsExtractedAtOp}, including its
8124
+ * transpilation hazard — the prepare pass MUST stay a `.map()`.
8099
8125
  */
8100
8126
  declare function backfillMemoryTopicsOp(ctx: VaultMemoryOperationsContext, memoryIds: readonly string[]): Promise<string[]>;
8101
8127
  declare function deleteAllVaultMemoriesForUserOp(ctx: VaultMemoryOperationsContext, userId: string): Promise<number>;
@@ -8380,6 +8406,22 @@ interface TurnCompleteEvent {
8380
8406
  * previously indistinguishable (both surfaced as zero candidates).
8381
8407
  */
8382
8408
  outcome: ExtractOutcome;
8409
+ /**
8410
+ * Present only alongside `outcome: "empty-after-retry"` — WHICH failure ended
8411
+ * the turn (#888).
8412
+ *
8413
+ * `outcome` says extraction gave up; this says why, from a stable enum. The
8414
+ * distinction is the whole point: a 2026-08-11 audit measured ~63% of
8415
+ * production extraction turns ending in `empty-after-retry` and could not tell
8416
+ * from telemetry whether the cause was the freeloader 403 everyone assumed or
8417
+ * something else. It took a Prometheus cross-check to find the real one — the
8418
+ * portal returning HTTP 200 with an empty body, which it counts as a success.
8419
+ *
8420
+ * Forward `failure.reason` into your extraction analytics event; all three
8421
+ * fields are bounded (an enum, an HTTP status, a small attempt count) and none
8422
+ * carries content.
8423
+ */
8424
+ failure?: PortalLlmFailure;
8383
8425
  }
8384
8426
  /** @public */
8385
8427
  /**