@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.
@@ -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
  /**
@@ -178,7 +178,7 @@ import {
178
178
  updateVaultMemoryOp,
179
179
  useEncryption,
180
180
  vaultMemoryToStored
181
- } from "./chunk-POCLKALT.mjs";
181
+ } from "./chunk-EJ65X4GI.mjs";
182
182
  import {
183
183
  PiiRedactor,
184
184
  createStreamingDeAnonymizer,
@@ -8357,7 +8357,7 @@ function createAutoExtractor(options) {
8357
8357
  const t0 = Date.now();
8358
8358
  void (async () => {
8359
8359
  try {
8360
- const { candidates, results, failedCount, outcome } = await extractAndRetain(
8360
+ const { candidates, results, failedCount, outcome, failure } = await extractAndRetain(
8361
8361
  window2,
8362
8362
  retainCtx,
8363
8363
  {
@@ -8397,7 +8397,11 @@ function createAutoExtractor(options) {
8397
8397
  failedCount,
8398
8398
  durationMs: Date.now() - t0,
8399
8399
  conversationId,
8400
- outcome
8400
+ outcome,
8401
+ // Only set alongside `outcome: "empty-after-retry"`. Spread so the key
8402
+ // is absent rather than explicitly undefined on a healthy turn —
8403
+ // analytics backends store an explicit undefined as a real value.
8404
+ ...failure !== void 0 && { failure }
8401
8405
  });
8402
8406
  } catch (err) {
8403
8407
  options.onError?.(err instanceof Error ? err : new Error(String(err)), conversationId);
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  recall
3
- } from "./chunk-POCLKALT.mjs";
3
+ } from "./chunk-EJ65X4GI.mjs";
4
4
  import "./chunk-CJ32HO6Q.mjs";
5
5
  import "./chunk-GYKJOWEO.mjs";
6
6
  export {
@@ -15964,9 +15964,10 @@ async function updateMessageChunksOp(ctx, uniqueId, chunks, embeddingModel) {
15964
15964
  } catch {
15965
15965
  return null;
15966
15966
  }
15967
+ const storedChunks = chunks.map(({ text: _text, ...rest }) => rest);
15967
15968
  await ctx.database.write(async () => {
15968
15969
  await message.update((msg) => {
15969
- msg._setRaw("chunks", JSON.stringify(chunks));
15970
+ msg._setRaw("chunks", JSON.stringify(storedChunks));
15970
15971
  msg._setRaw("embedding_model", embeddingModel);
15971
15972
  });
15972
15973
  });
@@ -16061,6 +16062,16 @@ async function _updateMessageOp(ctx, uniqueId, opts) {
16061
16062
  });
16062
16063
  return messageToStored(message, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner);
16063
16064
  }
16065
+ function resolveChunkText(content, chunk, chunksInRow) {
16066
+ if (!chunk) return content;
16067
+ if (typeof chunk.text === "string" && chunk.text.length > 0) return chunk.text;
16068
+ const { startOffset: start, endOffset: end } = chunk;
16069
+ if (!Number.isInteger(start) || !Number.isInteger(end)) return content;
16070
+ if (start < 0 || end <= start || end > content.length) return content;
16071
+ const covered = chunksInRow?.reduce((max, c) => Math.max(max, c.endOffset ?? 0), 0) ?? 0;
16072
+ if (covered !== content.length) return content;
16073
+ return content.slice(start, end);
16074
+ }
16064
16075
  async function readJsonField(model, column, walletAddress) {
16065
16076
  const raw = model._getRaw(column);
16066
16077
  if (!raw) return void 0;
@@ -16174,7 +16185,7 @@ async function searchChunksOp(ctx, queryVector, options) {
16174
16185
  candidates.push({
16175
16186
  message,
16176
16187
  similarity,
16177
- chunkTextSource: { kind: "chunk", text: chunks[ci].text }
16188
+ chunkTextSource: { kind: "chunk", chunk: chunks[ci], siblings: chunks }
16178
16189
  });
16179
16190
  }
16180
16191
  }
@@ -16232,11 +16243,11 @@ async function searchChunksOp(ctx, queryVector, options) {
16232
16243
  const stored = { ...storedById.get(message.id) };
16233
16244
  let chunkText2;
16234
16245
  if (chunkTextSource.kind === "chunk") {
16235
- chunkText2 = chunkTextSource.text;
16246
+ chunkText2 = resolveChunkText(stored.content, chunkTextSource.chunk, chunkTextSource.siblings);
16236
16247
  } else if (chunkTextSource.kind === "chunk-cached") {
16237
16248
  const state = chunkStateByMsgId.get(message.id);
16238
16249
  const chunk = state && state.version === chunkTextSource.version ? state.chunks[chunkTextSource.chunkIndex] : void 0;
16239
- chunkText2 = chunk?.text ?? stored.content;
16250
+ chunkText2 = resolveChunkText(stored.content, chunk, state?.chunks);
16240
16251
  } else {
16241
16252
  chunkText2 = stored.content;
16242
16253
  }
@@ -16380,13 +16391,13 @@ async function upsertEntitiesInWrite(ctx, entities) {
16380
16391
  }
16381
16392
  const unique = Array.from(kindByName.keys());
16382
16393
  const out = /* @__PURE__ */ new Map();
16383
- if (unique.length === 0) return { entities: out, operations: [] };
16394
+ if (unique.length === 0) return { entities: out, operations: [], kindBackfills: [] };
16384
16395
  const existing = await ctx.entityCollection.query(import_watermelondb2.Q.where("canonical_name", import_watermelondb2.Q.oneOf(unique))).fetch();
16385
16396
  const existingNames = new Set(existing.map((e) => e.canonicalName));
16386
- const updates = existing.filter((e) => {
16397
+ const kindBackfills = existing.filter((e) => {
16387
16398
  const incoming = kindByName.get(e.canonicalName);
16388
16399
  return incoming !== void 0 && (e.kind === null || e.kind === void 0 || e.kind === "");
16389
- });
16400
+ }).map((record) => ({ record, kind: kindByName.get(record.canonicalName) }));
16390
16401
  const missing = unique.filter((n) => !existingNames.has(n));
16391
16402
  const created = missing.map(
16392
16403
  (name) => ctx.entityCollection.prepareCreate((record) => {
@@ -16395,14 +16406,19 @@ async function upsertEntitiesInWrite(ctx, entities) {
16395
16406
  if (kind !== void 0) record._setRaw("kind", kind);
16396
16407
  })
16397
16408
  );
16398
- const updated = updates.map(
16399
- (e) => e.prepareUpdate((record) => {
16400
- record._setRaw("kind", kindByName.get(e.canonicalName));
16401
- })
16402
- );
16403
16409
  for (const e of existing) out.set(e.canonicalName, entityToStored(e));
16404
16410
  for (const record of created) out.set(record.canonicalName, entityToStored(record));
16405
- return { entities: out, operations: [...created, ...updated] };
16411
+ for (const { record, kind } of kindBackfills) {
16412
+ out.set(record.canonicalName, { ...entityToStored(record), kind });
16413
+ }
16414
+ return { entities: out, operations: created, kindBackfills };
16415
+ }
16416
+ function prepareKindBackfills(backfills) {
16417
+ return backfills.map(
16418
+ ({ record, kind }) => record.prepareUpdate((r) => {
16419
+ r._setRaw("kind", kind);
16420
+ })
16421
+ );
16406
16422
  }
16407
16423
  async function findVaultRowInWrite(ctx, memoryId) {
16408
16424
  try {
@@ -16452,10 +16468,12 @@ function topicsForEntities(entities, displayNames, source) {
16452
16468
  function toEntityObjects(entityInputs) {
16453
16469
  return entityInputs.map((e) => typeof e === "string" ? { name: e } : e);
16454
16470
  }
16455
- async function prepareMemoryTopicsUpdate(ctx, memoryId, linked, inputs, source) {
16471
+ async function resolveMemoryTopicsWrite(ctx, memoryId, linked, inputs, source) {
16456
16472
  const row = await findVaultRowInWrite(ctx, memoryId);
16457
- const write = resolveTopicsWrite(row, linked, displayNamesOf(toEntityObjects(inputs)), source);
16458
- return write === null ? null : prepareTopicsUpdate(write.row, write.topics, Date.now());
16473
+ return resolveTopicsWrite(row, linked, displayNamesOf(toEntityObjects(inputs)), source);
16474
+ }
16475
+ function prepareMemoryTopicsUpdate(write) {
16476
+ return prepareTopicsUpdate(write.row, write.topics, Date.now());
16459
16477
  }
16460
16478
  async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
16461
16479
  if (entityInputs.length === 0) return [];
@@ -16464,15 +16482,16 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
16464
16482
  let skipped = false;
16465
16483
  let entities = [];
16466
16484
  await ctx.database.write(async () => {
16467
- const { entities: byName, operations: entityOps } = await upsertEntitiesInWrite(
16468
- ctx,
16469
- normalized
16470
- );
16485
+ const {
16486
+ entities: byName,
16487
+ operations: entityOps,
16488
+ kindBackfills
16489
+ } = await upsertEntitiesInWrite(ctx, normalized);
16471
16490
  entities = Array.from(byName.values());
16472
16491
  const vaultRow = await findVaultRowInWrite(ctx, memoryId);
16473
16492
  if (options?.unlessTopicsUserManaged && autoLinkBlocked(vaultRow)) {
16474
- if (entityOps.length > 0) {
16475
- await ctx.database.batch(...entityOps);
16493
+ if (entityOps.length > 0 || kindBackfills.length > 0) {
16494
+ await ctx.database.batch(...entityOps, ...prepareKindBackfills(kindBackfills));
16476
16495
  }
16477
16496
  skipped = true;
16478
16497
  return;
@@ -16492,7 +16511,9 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
16492
16511
  displayNamesOf(normalized),
16493
16512
  options?.topicsSource ?? "auto"
16494
16513
  );
16495
- if (entityOps.length === 0 && toCreate.length === 0 && topicsWrite === null) return;
16514
+ if (entityOps.length === 0 && kindBackfills.length === 0 && toCreate.length === 0 && topicsWrite === null) {
16515
+ return;
16516
+ }
16496
16517
  const linkOps = toCreate.map(
16497
16518
  (e) => ctx.memoryEntityCollection.prepareCreate((record) => {
16498
16519
  record._setRaw("memory_id", memoryId);
@@ -16502,6 +16523,7 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
16502
16523
  );
16503
16524
  await ctx.database.batch(
16504
16525
  ...entityOps,
16526
+ ...prepareKindBackfills(kindBackfills),
16505
16527
  ...linkOps,
16506
16528
  ...topicsWrite ? [prepareTopicsUpdate(topicsWrite.row, topicsWrite.topics, Date.now())] : []
16507
16529
  );
@@ -16539,10 +16561,11 @@ async function replaceMemoryEntities(ctx, memoryId, entityInputs, options) {
16539
16561
  skipped = true;
16540
16562
  return;
16541
16563
  }
16542
- const { entities: byName, operations: entityOps } = await upsertEntitiesInWrite(
16543
- ctx,
16544
- normalized
16545
- );
16564
+ const {
16565
+ entities: byName,
16566
+ operations: entityOps,
16567
+ kindBackfills
16568
+ } = await upsertEntitiesInWrite(ctx, normalized);
16546
16569
  entities = Array.from(byName.values());
16547
16570
  const existingLinks = await ctx.memoryEntityCollection.query(import_watermelondb2.Q.where("memory_id", memoryId)).fetch();
16548
16571
  const keep = new Set(entities.map((e) => e.uniqueId));
@@ -16555,12 +16578,13 @@ async function replaceMemoryEntities(ctx, memoryId, entityInputs, options) {
16555
16578
  displayNamesOf(normalized),
16556
16579
  options.topicsSource
16557
16580
  );
16558
- if (entityOps.length === 0 && toCreate.length === 0 && toDestroy.length === 0 && topicsWrite === null) {
16581
+ if (entityOps.length === 0 && kindBackfills.length === 0 && toCreate.length === 0 && toDestroy.length === 0 && topicsWrite === null) {
16559
16582
  return;
16560
16583
  }
16561
16584
  const orphans = await findOrphanedEntities(ctx, memoryId, toDestroy);
16562
16585
  await ctx.database.batch(
16563
16586
  ...entityOps,
16587
+ ...prepareKindBackfills(kindBackfills),
16564
16588
  ...toCreate.map(
16565
16589
  (e) => ctx.memoryEntityCollection.prepareCreate((record) => {
16566
16590
  record._setRaw("memory_id", memoryId);
@@ -17611,27 +17635,36 @@ async function backfillMemoryTopicsOp(ctx, memoryIds) {
17611
17635
  if (!entityCtx) {
17612
17636
  throw new Error("backfillMemoryTopicsOp requires ctx.entityCtx");
17613
17637
  }
17638
+ const uniqueIds = Array.from(new Set(memoryIds));
17639
+ if (uniqueIds.length === 0) return [];
17614
17640
  const filled = [];
17615
- for (const id of Array.from(new Set(memoryIds))) {
17616
- let record;
17617
- try {
17618
- record = await ctx.vaultMemoryCollection.find(id);
17619
- } catch {
17620
- continue;
17621
- }
17622
- if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) continue;
17623
- if (parseTopics(record.topics) !== null) continue;
17624
- const links = await entityCtx.memoryEntityCollection.query(import_watermelondb3.Q.where("memory_id", id)).unsafeFetchRaw();
17625
- const entityIds = links.map((l) => String(l.entity_id));
17626
- if (entityIds.length === 0) continue;
17627
- const entities = await entityCtx.entityCollection.query(import_watermelondb3.Q.where("id", import_watermelondb3.Q.oneOf(entityIds))).fetch();
17628
- if (entities.length === 0) continue;
17629
- const source = record.topicsUserManaged ? "user" : "auto";
17641
+ const CHUNK = 500;
17642
+ for (let i = 0; i < uniqueIds.length; i += CHUNK) {
17643
+ const chunkIds = uniqueIds.slice(i, i + CHUNK);
17630
17644
  await ctx.database.write(async () => {
17631
- const topicsOp = await prepareMemoryTopicsUpdate(entityCtx, id, entities, [], source);
17632
- if (!topicsOp) return;
17633
- await ctx.database.batch(topicsOp);
17634
- filled.push(id);
17645
+ const writes = [];
17646
+ for (const id of chunkIds) {
17647
+ let record;
17648
+ try {
17649
+ record = await ctx.vaultMemoryCollection.find(id);
17650
+ } catch {
17651
+ continue;
17652
+ }
17653
+ if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) continue;
17654
+ if (parseTopics(record.topics) !== null) continue;
17655
+ const links = await entityCtx.memoryEntityCollection.query(import_watermelondb3.Q.where("memory_id", id)).unsafeFetchRaw();
17656
+ const entityIds = links.map((l) => String(l.entity_id));
17657
+ if (entityIds.length === 0) continue;
17658
+ const entities = await entityCtx.entityCollection.query(import_watermelondb3.Q.where("id", import_watermelondb3.Q.oneOf(entityIds))).fetch();
17659
+ if (entities.length === 0) continue;
17660
+ const source = record.topicsUserManaged ? "user" : "auto";
17661
+ const write = await resolveMemoryTopicsWrite(entityCtx, id, entities, [], source);
17662
+ if (write) writes.push(write);
17663
+ }
17664
+ if (writes.length === 0) return;
17665
+ const prepared = writes.map(prepareMemoryTopicsUpdate);
17666
+ await ctx.database.batch(...prepared);
17667
+ for (const write of writes) filled.push(write.row.id);
17635
17668
  });
17636
17669
  }
17637
17670
  return filled;
@@ -20845,7 +20878,7 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
20845
20878
  subQueries = decomp.subQueries;
20846
20879
  }
20847
20880
  }
20848
- const { recall: recall2 } = await import("./recall-2GAPE2XH.mjs");
20881
+ const { recall: recall2 } = await import("./recall-T7CNXTQ3.mjs");
20849
20882
  let recallDegraded = [];
20850
20883
  const result = await recall2(
20851
20884
  query,
@@ -16722,9 +16722,10 @@ async function updateMessageChunksOp(ctx, uniqueId, chunks, embeddingModel) {
16722
16722
  } catch {
16723
16723
  return null;
16724
16724
  }
16725
+ const storedChunks = chunks.map(({ text: _text, ...rest }) => rest);
16725
16726
  await ctx.database.write(async () => {
16726
16727
  await message.update((msg) => {
16727
- msg._setRaw("chunks", JSON.stringify(chunks));
16728
+ msg._setRaw("chunks", JSON.stringify(storedChunks));
16728
16729
  msg._setRaw("embedding_model", embeddingModel);
16729
16730
  });
16730
16731
  });
@@ -16819,6 +16820,16 @@ async function _updateMessageOp(ctx, uniqueId, opts) {
16819
16820
  });
16820
16821
  return messageToStored(message, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner);
16821
16822
  }
16823
+ function resolveChunkText(content, chunk, chunksInRow) {
16824
+ if (!chunk) return content;
16825
+ if (typeof chunk.text === "string" && chunk.text.length > 0) return chunk.text;
16826
+ const { startOffset: start, endOffset: end } = chunk;
16827
+ if (!Number.isInteger(start) || !Number.isInteger(end)) return content;
16828
+ if (start < 0 || end <= start || end > content.length) return content;
16829
+ const covered = chunksInRow?.reduce((max, c) => Math.max(max, c.endOffset ?? 0), 0) ?? 0;
16830
+ if (covered !== content.length) return content;
16831
+ return content.slice(start, end);
16832
+ }
16822
16833
  async function readJsonField(model, column, walletAddress) {
16823
16834
  const raw = model._getRaw(column);
16824
16835
  if (!raw) return void 0;
@@ -16932,7 +16943,7 @@ async function searchChunksOp(ctx, queryVector, options) {
16932
16943
  candidates.push({
16933
16944
  message,
16934
16945
  similarity,
16935
- chunkTextSource: { kind: "chunk", text: chunks[ci].text }
16946
+ chunkTextSource: { kind: "chunk", chunk: chunks[ci], siblings: chunks }
16936
16947
  });
16937
16948
  }
16938
16949
  }
@@ -16990,11 +17001,11 @@ async function searchChunksOp(ctx, queryVector, options) {
16990
17001
  const stored = { ...storedById.get(message.id) };
16991
17002
  let chunkText2;
16992
17003
  if (chunkTextSource.kind === "chunk") {
16993
- chunkText2 = chunkTextSource.text;
17004
+ chunkText2 = resolveChunkText(stored.content, chunkTextSource.chunk, chunkTextSource.siblings);
16994
17005
  } else if (chunkTextSource.kind === "chunk-cached") {
16995
17006
  const state = chunkStateByMsgId.get(message.id);
16996
17007
  const chunk = state && state.version === chunkTextSource.version ? state.chunks[chunkTextSource.chunkIndex] : void 0;
16997
- chunkText2 = chunk?.text ?? stored.content;
17008
+ chunkText2 = resolveChunkText(stored.content, chunk, state?.chunks);
16998
17009
  } else {
16999
17010
  chunkText2 = stored.content;
17000
17011
  }
@@ -17214,13 +17225,13 @@ async function upsertEntitiesInWrite(ctx, entities) {
17214
17225
  }
17215
17226
  const unique = Array.from(kindByName.keys());
17216
17227
  const out = /* @__PURE__ */ new Map();
17217
- if (unique.length === 0) return { entities: out, operations: [] };
17228
+ if (unique.length === 0) return { entities: out, operations: [], kindBackfills: [] };
17218
17229
  const existing = await ctx.entityCollection.query(import_watermelondb16.Q.where("canonical_name", import_watermelondb16.Q.oneOf(unique))).fetch();
17219
17230
  const existingNames = new Set(existing.map((e) => e.canonicalName));
17220
- const updates = existing.filter((e) => {
17231
+ const kindBackfills = existing.filter((e) => {
17221
17232
  const incoming = kindByName.get(e.canonicalName);
17222
17233
  return incoming !== void 0 && (e.kind === null || e.kind === void 0 || e.kind === "");
17223
- });
17234
+ }).map((record) => ({ record, kind: kindByName.get(record.canonicalName) }));
17224
17235
  const missing = unique.filter((n) => !existingNames.has(n));
17225
17236
  const created = missing.map(
17226
17237
  (name) => ctx.entityCollection.prepareCreate((record) => {
@@ -17229,14 +17240,19 @@ async function upsertEntitiesInWrite(ctx, entities) {
17229
17240
  if (kind !== void 0) record._setRaw("kind", kind);
17230
17241
  })
17231
17242
  );
17232
- const updated = updates.map(
17233
- (e) => e.prepareUpdate((record) => {
17234
- record._setRaw("kind", kindByName.get(e.canonicalName));
17235
- })
17236
- );
17237
17243
  for (const e of existing) out.set(e.canonicalName, entityToStored(e));
17238
17244
  for (const record of created) out.set(record.canonicalName, entityToStored(record));
17239
- return { entities: out, operations: [...created, ...updated] };
17245
+ for (const { record, kind } of kindBackfills) {
17246
+ out.set(record.canonicalName, { ...entityToStored(record), kind });
17247
+ }
17248
+ return { entities: out, operations: created, kindBackfills };
17249
+ }
17250
+ function prepareKindBackfills(backfills) {
17251
+ return backfills.map(
17252
+ ({ record, kind }) => record.prepareUpdate((r) => {
17253
+ r._setRaw("kind", kind);
17254
+ })
17255
+ );
17240
17256
  }
17241
17257
  async function findVaultRowInWrite(ctx, memoryId) {
17242
17258
  try {
@@ -17286,10 +17302,12 @@ function topicsForEntities(entities, displayNames, source) {
17286
17302
  function toEntityObjects(entityInputs) {
17287
17303
  return entityInputs.map((e) => typeof e === "string" ? { name: e } : e);
17288
17304
  }
17289
- async function prepareMemoryTopicsUpdate(ctx, memoryId, linked, inputs, source) {
17305
+ async function resolveMemoryTopicsWrite(ctx, memoryId, linked, inputs, source) {
17290
17306
  const row = await findVaultRowInWrite(ctx, memoryId);
17291
- const write = resolveTopicsWrite(row, linked, displayNamesOf(toEntityObjects(inputs)), source);
17292
- return write === null ? null : prepareTopicsUpdate(write.row, write.topics, Date.now());
17307
+ return resolveTopicsWrite(row, linked, displayNamesOf(toEntityObjects(inputs)), source);
17308
+ }
17309
+ function prepareMemoryTopicsUpdate(write) {
17310
+ return prepareTopicsUpdate(write.row, write.topics, Date.now());
17293
17311
  }
17294
17312
  async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
17295
17313
  if (entityInputs.length === 0) return [];
@@ -17298,15 +17316,16 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
17298
17316
  let skipped = false;
17299
17317
  let entities = [];
17300
17318
  await ctx.database.write(async () => {
17301
- const { entities: byName, operations: entityOps } = await upsertEntitiesInWrite(
17302
- ctx,
17303
- normalized
17304
- );
17319
+ const {
17320
+ entities: byName,
17321
+ operations: entityOps,
17322
+ kindBackfills
17323
+ } = await upsertEntitiesInWrite(ctx, normalized);
17305
17324
  entities = Array.from(byName.values());
17306
17325
  const vaultRow = await findVaultRowInWrite(ctx, memoryId);
17307
17326
  if (options?.unlessTopicsUserManaged && autoLinkBlocked(vaultRow)) {
17308
- if (entityOps.length > 0) {
17309
- await ctx.database.batch(...entityOps);
17327
+ if (entityOps.length > 0 || kindBackfills.length > 0) {
17328
+ await ctx.database.batch(...entityOps, ...prepareKindBackfills(kindBackfills));
17310
17329
  }
17311
17330
  skipped = true;
17312
17331
  return;
@@ -17326,7 +17345,9 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
17326
17345
  displayNamesOf(normalized),
17327
17346
  options?.topicsSource ?? "auto"
17328
17347
  );
17329
- if (entityOps.length === 0 && toCreate.length === 0 && topicsWrite === null) return;
17348
+ if (entityOps.length === 0 && kindBackfills.length === 0 && toCreate.length === 0 && topicsWrite === null) {
17349
+ return;
17350
+ }
17330
17351
  const linkOps = toCreate.map(
17331
17352
  (e) => ctx.memoryEntityCollection.prepareCreate((record) => {
17332
17353
  record._setRaw("memory_id", memoryId);
@@ -17336,6 +17357,7 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
17336
17357
  );
17337
17358
  await ctx.database.batch(
17338
17359
  ...entityOps,
17360
+ ...prepareKindBackfills(kindBackfills),
17339
17361
  ...linkOps,
17340
17362
  ...topicsWrite ? [prepareTopicsUpdate(topicsWrite.row, topicsWrite.topics, Date.now())] : []
17341
17363
  );
@@ -17373,10 +17395,11 @@ async function replaceMemoryEntities(ctx, memoryId, entityInputs, options) {
17373
17395
  skipped = true;
17374
17396
  return;
17375
17397
  }
17376
- const { entities: byName, operations: entityOps } = await upsertEntitiesInWrite(
17377
- ctx,
17378
- normalized
17379
- );
17398
+ const {
17399
+ entities: byName,
17400
+ operations: entityOps,
17401
+ kindBackfills
17402
+ } = await upsertEntitiesInWrite(ctx, normalized);
17380
17403
  entities = Array.from(byName.values());
17381
17404
  const existingLinks = await ctx.memoryEntityCollection.query(import_watermelondb16.Q.where("memory_id", memoryId)).fetch();
17382
17405
  const keep = new Set(entities.map((e) => e.uniqueId));
@@ -17389,12 +17412,13 @@ async function replaceMemoryEntities(ctx, memoryId, entityInputs, options) {
17389
17412
  displayNamesOf(normalized),
17390
17413
  options.topicsSource
17391
17414
  );
17392
- if (entityOps.length === 0 && toCreate.length === 0 && toDestroy.length === 0 && topicsWrite === null) {
17415
+ if (entityOps.length === 0 && kindBackfills.length === 0 && toCreate.length === 0 && toDestroy.length === 0 && topicsWrite === null) {
17393
17416
  return;
17394
17417
  }
17395
17418
  const orphans = await findOrphanedEntities(ctx, memoryId, toDestroy);
17396
17419
  await ctx.database.batch(
17397
17420
  ...entityOps,
17421
+ ...prepareKindBackfills(kindBackfills),
17398
17422
  ...toCreate.map(
17399
17423
  (e) => ctx.memoryEntityCollection.prepareCreate((record) => {
17400
17424
  record._setRaw("memory_id", memoryId);
@@ -18460,27 +18484,36 @@ async function backfillMemoryTopicsOp(ctx, memoryIds) {
18460
18484
  if (!entityCtx) {
18461
18485
  throw new Error("backfillMemoryTopicsOp requires ctx.entityCtx");
18462
18486
  }
18487
+ const uniqueIds = Array.from(new Set(memoryIds));
18488
+ if (uniqueIds.length === 0) return [];
18463
18489
  const filled = [];
18464
- for (const id of Array.from(new Set(memoryIds))) {
18465
- let record;
18466
- try {
18467
- record = await ctx.vaultMemoryCollection.find(id);
18468
- } catch {
18469
- continue;
18470
- }
18471
- if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) continue;
18472
- if (parseTopics(record.topics) !== null) continue;
18473
- const links = await entityCtx.memoryEntityCollection.query(import_watermelondb17.Q.where("memory_id", id)).unsafeFetchRaw();
18474
- const entityIds = links.map((l) => String(l.entity_id));
18475
- if (entityIds.length === 0) continue;
18476
- const entities = await entityCtx.entityCollection.query(import_watermelondb17.Q.where("id", import_watermelondb17.Q.oneOf(entityIds))).fetch();
18477
- if (entities.length === 0) continue;
18478
- const source = record.topicsUserManaged ? "user" : "auto";
18490
+ const CHUNK = 500;
18491
+ for (let i = 0; i < uniqueIds.length; i += CHUNK) {
18492
+ const chunkIds = uniqueIds.slice(i, i + CHUNK);
18479
18493
  await ctx.database.write(async () => {
18480
- const topicsOp = await prepareMemoryTopicsUpdate(entityCtx, id, entities, [], source);
18481
- if (!topicsOp) return;
18482
- await ctx.database.batch(topicsOp);
18483
- filled.push(id);
18494
+ const writes = [];
18495
+ for (const id of chunkIds) {
18496
+ let record;
18497
+ try {
18498
+ record = await ctx.vaultMemoryCollection.find(id);
18499
+ } catch {
18500
+ continue;
18501
+ }
18502
+ if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) continue;
18503
+ if (parseTopics(record.topics) !== null) continue;
18504
+ const links = await entityCtx.memoryEntityCollection.query(import_watermelondb17.Q.where("memory_id", id)).unsafeFetchRaw();
18505
+ const entityIds = links.map((l) => String(l.entity_id));
18506
+ if (entityIds.length === 0) continue;
18507
+ const entities = await entityCtx.entityCollection.query(import_watermelondb17.Q.where("id", import_watermelondb17.Q.oneOf(entityIds))).fetch();
18508
+ if (entities.length === 0) continue;
18509
+ const source = record.topicsUserManaged ? "user" : "auto";
18510
+ const write = await resolveMemoryTopicsWrite(entityCtx, id, entities, [], source);
18511
+ if (write) writes.push(write);
18512
+ }
18513
+ if (writes.length === 0) return;
18514
+ const prepared = writes.map(prepareMemoryTopicsUpdate);
18515
+ await ctx.database.batch(...prepared);
18516
+ for (const write of writes) filled.push(write.row.id);
18484
18517
  });
18485
18518
  }
18486
18519
  return filled;
@@ -30000,7 +30033,7 @@ function createAutoExtractor(options) {
30000
30033
  const t0 = Date.now();
30001
30034
  void (async () => {
30002
30035
  try {
30003
- const { candidates, results, failedCount, outcome } = await extractAndRetain(
30036
+ const { candidates, results, failedCount, outcome, failure } = await extractAndRetain(
30004
30037
  window2,
30005
30038
  retainCtx,
30006
30039
  {
@@ -30040,7 +30073,11 @@ function createAutoExtractor(options) {
30040
30073
  failedCount,
30041
30074
  durationMs: Date.now() - t0,
30042
30075
  conversationId,
30043
- outcome
30076
+ outcome,
30077
+ // Only set alongside `outcome: "empty-after-retry"`. Spread so the key
30078
+ // is absent rather than explicitly undefined on a healthy turn —
30079
+ // analytics backends store an explicit undefined as a real value.
30080
+ ...failure !== void 0 && { failure }
30044
30081
  });
30045
30082
  } catch (err) {
30046
30083
  options.onError?.(err instanceof Error ? err : new Error(String(err)), conversationId);