@anuma/sdk 6.8.2 → 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.
@@ -2175,13 +2175,13 @@ async function upsertEntitiesInWrite(ctx, entities) {
2175
2175
  }
2176
2176
  const unique = Array.from(kindByName.keys());
2177
2177
  const out = /* @__PURE__ */ new Map();
2178
- if (unique.length === 0) return { entities: out, operations: [] };
2178
+ if (unique.length === 0) return { entities: out, operations: [], kindBackfills: [] };
2179
2179
  const existing = await ctx.entityCollection.query(Q2.where("canonical_name", Q2.oneOf(unique))).fetch();
2180
2180
  const existingNames = new Set(existing.map((e) => e.canonicalName));
2181
- const updates = existing.filter((e) => {
2181
+ const kindBackfills = existing.filter((e) => {
2182
2182
  const incoming = kindByName.get(e.canonicalName);
2183
2183
  return incoming !== void 0 && (e.kind === null || e.kind === void 0 || e.kind === "");
2184
- });
2184
+ }).map((record) => ({ record, kind: kindByName.get(record.canonicalName) }));
2185
2185
  const missing = unique.filter((n) => !existingNames.has(n));
2186
2186
  const created = missing.map(
2187
2187
  (name) => ctx.entityCollection.prepareCreate((record) => {
@@ -2190,14 +2190,19 @@ async function upsertEntitiesInWrite(ctx, entities) {
2190
2190
  if (kind !== void 0) record._setRaw("kind", kind);
2191
2191
  })
2192
2192
  );
2193
- const updated = updates.map(
2194
- (e) => e.prepareUpdate((record) => {
2195
- record._setRaw("kind", kindByName.get(e.canonicalName));
2196
- })
2197
- );
2198
2193
  for (const e of existing) out.set(e.canonicalName, entityToStored(e));
2199
2194
  for (const record of created) out.set(record.canonicalName, entityToStored(record));
2200
- return { entities: out, operations: [...created, ...updated] };
2195
+ for (const { record, kind } of kindBackfills) {
2196
+ out.set(record.canonicalName, { ...entityToStored(record), kind });
2197
+ }
2198
+ return { entities: out, operations: created, kindBackfills };
2199
+ }
2200
+ function prepareKindBackfills(backfills) {
2201
+ return backfills.map(
2202
+ ({ record, kind }) => record.prepareUpdate((r) => {
2203
+ r._setRaw("kind", kind);
2204
+ })
2205
+ );
2201
2206
  }
2202
2207
  async function findVaultRowInWrite(ctx, memoryId) {
2203
2208
  try {
@@ -2247,10 +2252,12 @@ function topicsForEntities(entities, displayNames, source) {
2247
2252
  function toEntityObjects(entityInputs) {
2248
2253
  return entityInputs.map((e) => typeof e === "string" ? { name: e } : e);
2249
2254
  }
2250
- async function prepareMemoryTopicsUpdate(ctx, memoryId, linked, inputs, source) {
2255
+ async function resolveMemoryTopicsWrite(ctx, memoryId, linked, inputs, source) {
2251
2256
  const row = await findVaultRowInWrite(ctx, memoryId);
2252
- const write = resolveTopicsWrite(row, linked, displayNamesOf(toEntityObjects(inputs)), source);
2253
- return write === null ? null : prepareTopicsUpdate(write.row, write.topics, Date.now());
2257
+ return resolveTopicsWrite(row, linked, displayNamesOf(toEntityObjects(inputs)), source);
2258
+ }
2259
+ function prepareMemoryTopicsUpdate(write) {
2260
+ return prepareTopicsUpdate(write.row, write.topics, Date.now());
2254
2261
  }
2255
2262
  async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
2256
2263
  if (entityInputs.length === 0) return [];
@@ -2259,15 +2266,16 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
2259
2266
  let skipped = false;
2260
2267
  let entities = [];
2261
2268
  await ctx.database.write(async () => {
2262
- const { entities: byName, operations: entityOps } = await upsertEntitiesInWrite(
2263
- ctx,
2264
- normalized
2265
- );
2269
+ const {
2270
+ entities: byName,
2271
+ operations: entityOps,
2272
+ kindBackfills
2273
+ } = await upsertEntitiesInWrite(ctx, normalized);
2266
2274
  entities = Array.from(byName.values());
2267
2275
  const vaultRow = await findVaultRowInWrite(ctx, memoryId);
2268
2276
  if (options?.unlessTopicsUserManaged && autoLinkBlocked(vaultRow)) {
2269
- if (entityOps.length > 0) {
2270
- await ctx.database.batch(...entityOps);
2277
+ if (entityOps.length > 0 || kindBackfills.length > 0) {
2278
+ await ctx.database.batch(...entityOps, ...prepareKindBackfills(kindBackfills));
2271
2279
  }
2272
2280
  skipped = true;
2273
2281
  return;
@@ -2287,7 +2295,9 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
2287
2295
  displayNamesOf(normalized),
2288
2296
  options?.topicsSource ?? "auto"
2289
2297
  );
2290
- if (entityOps.length === 0 && toCreate.length === 0 && topicsWrite === null) return;
2298
+ if (entityOps.length === 0 && kindBackfills.length === 0 && toCreate.length === 0 && topicsWrite === null) {
2299
+ return;
2300
+ }
2291
2301
  const linkOps = toCreate.map(
2292
2302
  (e) => ctx.memoryEntityCollection.prepareCreate((record) => {
2293
2303
  record._setRaw("memory_id", memoryId);
@@ -2297,6 +2307,7 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
2297
2307
  );
2298
2308
  await ctx.database.batch(
2299
2309
  ...entityOps,
2310
+ ...prepareKindBackfills(kindBackfills),
2300
2311
  ...linkOps,
2301
2312
  ...topicsWrite ? [prepareTopicsUpdate(topicsWrite.row, topicsWrite.topics, Date.now())] : []
2302
2313
  );
@@ -2334,10 +2345,11 @@ async function replaceMemoryEntities(ctx, memoryId, entityInputs, options) {
2334
2345
  skipped = true;
2335
2346
  return;
2336
2347
  }
2337
- const { entities: byName, operations: entityOps } = await upsertEntitiesInWrite(
2338
- ctx,
2339
- normalized
2340
- );
2348
+ const {
2349
+ entities: byName,
2350
+ operations: entityOps,
2351
+ kindBackfills
2352
+ } = await upsertEntitiesInWrite(ctx, normalized);
2341
2353
  entities = Array.from(byName.values());
2342
2354
  const existingLinks = await ctx.memoryEntityCollection.query(Q2.where("memory_id", memoryId)).fetch();
2343
2355
  const keep = new Set(entities.map((e) => e.uniqueId));
@@ -2350,12 +2362,13 @@ async function replaceMemoryEntities(ctx, memoryId, entityInputs, options) {
2350
2362
  displayNamesOf(normalized),
2351
2363
  options.topicsSource
2352
2364
  );
2353
- if (entityOps.length === 0 && toCreate.length === 0 && toDestroy.length === 0 && topicsWrite === null) {
2365
+ if (entityOps.length === 0 && kindBackfills.length === 0 && toCreate.length === 0 && toDestroy.length === 0 && topicsWrite === null) {
2354
2366
  return;
2355
2367
  }
2356
2368
  const orphans = await findOrphanedEntities(ctx, memoryId, toDestroy);
2357
2369
  await ctx.database.batch(
2358
2370
  ...entityOps,
2371
+ ...prepareKindBackfills(kindBackfills),
2359
2372
  ...toCreate.map(
2360
2373
  (e) => ctx.memoryEntityCollection.prepareCreate((record) => {
2361
2374
  record._setRaw("memory_id", memoryId);
@@ -3429,27 +3442,36 @@ async function backfillMemoryTopicsOp(ctx, memoryIds) {
3429
3442
  if (!entityCtx) {
3430
3443
  throw new Error("backfillMemoryTopicsOp requires ctx.entityCtx");
3431
3444
  }
3445
+ const uniqueIds = Array.from(new Set(memoryIds));
3446
+ if (uniqueIds.length === 0) return [];
3432
3447
  const filled = [];
3433
- for (const id of Array.from(new Set(memoryIds))) {
3434
- let record;
3435
- try {
3436
- record = await ctx.vaultMemoryCollection.find(id);
3437
- } catch {
3438
- continue;
3439
- }
3440
- if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) continue;
3441
- if (parseTopics(record.topics) !== null) continue;
3442
- const links = await entityCtx.memoryEntityCollection.query(Q3.where("memory_id", id)).unsafeFetchRaw();
3443
- const entityIds = links.map((l) => String(l.entity_id));
3444
- if (entityIds.length === 0) continue;
3445
- const entities = await entityCtx.entityCollection.query(Q3.where("id", Q3.oneOf(entityIds))).fetch();
3446
- if (entities.length === 0) continue;
3447
- const source = record.topicsUserManaged ? "user" : "auto";
3448
+ const CHUNK = 500;
3449
+ for (let i = 0; i < uniqueIds.length; i += CHUNK) {
3450
+ const chunkIds = uniqueIds.slice(i, i + CHUNK);
3448
3451
  await ctx.database.write(async () => {
3449
- const topicsOp = await prepareMemoryTopicsUpdate(entityCtx, id, entities, [], source);
3450
- if (!topicsOp) return;
3451
- await ctx.database.batch(topicsOp);
3452
- filled.push(id);
3452
+ const writes = [];
3453
+ for (const id of chunkIds) {
3454
+ let record;
3455
+ try {
3456
+ record = await ctx.vaultMemoryCollection.find(id);
3457
+ } catch {
3458
+ continue;
3459
+ }
3460
+ if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) continue;
3461
+ if (parseTopics(record.topics) !== null) continue;
3462
+ const links = await entityCtx.memoryEntityCollection.query(Q3.where("memory_id", id)).unsafeFetchRaw();
3463
+ const entityIds = links.map((l) => String(l.entity_id));
3464
+ if (entityIds.length === 0) continue;
3465
+ const entities = await entityCtx.entityCollection.query(Q3.where("id", Q3.oneOf(entityIds))).fetch();
3466
+ if (entities.length === 0) continue;
3467
+ const source = record.topicsUserManaged ? "user" : "auto";
3468
+ const write = await resolveMemoryTopicsWrite(entityCtx, id, entities, [], source);
3469
+ if (write) writes.push(write);
3470
+ }
3471
+ if (writes.length === 0) return;
3472
+ const prepared = writes.map(prepareMemoryTopicsUpdate);
3473
+ await ctx.database.batch(...prepared);
3474
+ for (const write of writes) filled.push(write.row.id);
3453
3475
  });
3454
3476
  }
3455
3477
  return filled;
@@ -6818,7 +6840,7 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
6818
6840
  subQueries = decomp.subQueries;
6819
6841
  }
6820
6842
  }
6821
- const { recall: recall2 } = await import("./recall-77QK4RT6.mjs");
6843
+ const { recall: recall2 } = await import("./recall-WIB7RQQZ.mjs");
6822
6844
  let recallDegraded = [];
6823
6845
  const result = await recall2(
6824
6846
  query,
@@ -4671,13 +4671,13 @@ async function upsertEntitiesInWrite(ctx, entities) {
4671
4671
  }
4672
4672
  const unique = Array.from(kindByName.keys());
4673
4673
  const out = /* @__PURE__ */ new Map();
4674
- if (unique.length === 0) return { entities: out, operations: [] };
4674
+ if (unique.length === 0) return { entities: out, operations: [], kindBackfills: [] };
4675
4675
  const existing = await ctx.entityCollection.query(import_watermelondb8.Q.where("canonical_name", import_watermelondb8.Q.oneOf(unique))).fetch();
4676
4676
  const existingNames = new Set(existing.map((e) => e.canonicalName));
4677
- const updates = existing.filter((e) => {
4677
+ const kindBackfills = existing.filter((e) => {
4678
4678
  const incoming = kindByName.get(e.canonicalName);
4679
4679
  return incoming !== void 0 && (e.kind === null || e.kind === void 0 || e.kind === "");
4680
- });
4680
+ }).map((record) => ({ record, kind: kindByName.get(record.canonicalName) }));
4681
4681
  const missing = unique.filter((n) => !existingNames.has(n));
4682
4682
  const created = missing.map(
4683
4683
  (name) => ctx.entityCollection.prepareCreate((record) => {
@@ -4686,14 +4686,19 @@ async function upsertEntitiesInWrite(ctx, entities) {
4686
4686
  if (kind !== void 0) record._setRaw("kind", kind);
4687
4687
  })
4688
4688
  );
4689
- const updated = updates.map(
4690
- (e) => e.prepareUpdate((record) => {
4691
- record._setRaw("kind", kindByName.get(e.canonicalName));
4692
- })
4693
- );
4694
4689
  for (const e of existing) out.set(e.canonicalName, entityToStored(e));
4695
4690
  for (const record of created) out.set(record.canonicalName, entityToStored(record));
4696
- return { entities: out, operations: [...created, ...updated] };
4691
+ for (const { record, kind } of kindBackfills) {
4692
+ out.set(record.canonicalName, { ...entityToStored(record), kind });
4693
+ }
4694
+ return { entities: out, operations: created, kindBackfills };
4695
+ }
4696
+ function prepareKindBackfills(backfills) {
4697
+ return backfills.map(
4698
+ ({ record, kind }) => record.prepareUpdate((r) => {
4699
+ r._setRaw("kind", kind);
4700
+ })
4701
+ );
4697
4702
  }
4698
4703
  async function findVaultRowInWrite(ctx, memoryId) {
4699
4704
  try {
@@ -4743,10 +4748,12 @@ function topicsForEntities(entities, displayNames, source) {
4743
4748
  function toEntityObjects(entityInputs) {
4744
4749
  return entityInputs.map((e) => typeof e === "string" ? { name: e } : e);
4745
4750
  }
4746
- async function prepareMemoryTopicsUpdate(ctx, memoryId, linked, inputs, source) {
4751
+ async function resolveMemoryTopicsWrite(ctx, memoryId, linked, inputs, source) {
4747
4752
  const row = await findVaultRowInWrite(ctx, memoryId);
4748
- const write = resolveTopicsWrite(row, linked, displayNamesOf(toEntityObjects(inputs)), source);
4749
- return write === null ? null : prepareTopicsUpdate(write.row, write.topics, Date.now());
4753
+ return resolveTopicsWrite(row, linked, displayNamesOf(toEntityObjects(inputs)), source);
4754
+ }
4755
+ function prepareMemoryTopicsUpdate(write) {
4756
+ return prepareTopicsUpdate(write.row, write.topics, Date.now());
4750
4757
  }
4751
4758
  async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
4752
4759
  if (entityInputs.length === 0) return [];
@@ -4755,15 +4762,16 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
4755
4762
  let skipped = false;
4756
4763
  let entities = [];
4757
4764
  await ctx.database.write(async () => {
4758
- const { entities: byName, operations: entityOps } = await upsertEntitiesInWrite(
4759
- ctx,
4760
- normalized
4761
- );
4765
+ const {
4766
+ entities: byName,
4767
+ operations: entityOps,
4768
+ kindBackfills
4769
+ } = await upsertEntitiesInWrite(ctx, normalized);
4762
4770
  entities = Array.from(byName.values());
4763
4771
  const vaultRow = await findVaultRowInWrite(ctx, memoryId);
4764
4772
  if (options?.unlessTopicsUserManaged && autoLinkBlocked(vaultRow)) {
4765
- if (entityOps.length > 0) {
4766
- await ctx.database.batch(...entityOps);
4773
+ if (entityOps.length > 0 || kindBackfills.length > 0) {
4774
+ await ctx.database.batch(...entityOps, ...prepareKindBackfills(kindBackfills));
4767
4775
  }
4768
4776
  skipped = true;
4769
4777
  return;
@@ -4783,7 +4791,9 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
4783
4791
  displayNamesOf(normalized),
4784
4792
  options?.topicsSource ?? "auto"
4785
4793
  );
4786
- if (entityOps.length === 0 && toCreate.length === 0 && topicsWrite === null) return;
4794
+ if (entityOps.length === 0 && kindBackfills.length === 0 && toCreate.length === 0 && topicsWrite === null) {
4795
+ return;
4796
+ }
4787
4797
  const linkOps = toCreate.map(
4788
4798
  (e) => ctx.memoryEntityCollection.prepareCreate((record) => {
4789
4799
  record._setRaw("memory_id", memoryId);
@@ -4793,6 +4803,7 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
4793
4803
  );
4794
4804
  await ctx.database.batch(
4795
4805
  ...entityOps,
4806
+ ...prepareKindBackfills(kindBackfills),
4796
4807
  ...linkOps,
4797
4808
  ...topicsWrite ? [prepareTopicsUpdate(topicsWrite.row, topicsWrite.topics, Date.now())] : []
4798
4809
  );
@@ -4830,10 +4841,11 @@ async function replaceMemoryEntities(ctx, memoryId, entityInputs, options) {
4830
4841
  skipped = true;
4831
4842
  return;
4832
4843
  }
4833
- const { entities: byName, operations: entityOps } = await upsertEntitiesInWrite(
4834
- ctx,
4835
- normalized
4836
- );
4844
+ const {
4845
+ entities: byName,
4846
+ operations: entityOps,
4847
+ kindBackfills
4848
+ } = await upsertEntitiesInWrite(ctx, normalized);
4837
4849
  entities = Array.from(byName.values());
4838
4850
  const existingLinks = await ctx.memoryEntityCollection.query(import_watermelondb8.Q.where("memory_id", memoryId)).fetch();
4839
4851
  const keep = new Set(entities.map((e) => e.uniqueId));
@@ -4846,12 +4858,13 @@ async function replaceMemoryEntities(ctx, memoryId, entityInputs, options) {
4846
4858
  displayNamesOf(normalized),
4847
4859
  options.topicsSource
4848
4860
  );
4849
- if (entityOps.length === 0 && toCreate.length === 0 && toDestroy.length === 0 && topicsWrite === null) {
4861
+ if (entityOps.length === 0 && kindBackfills.length === 0 && toCreate.length === 0 && toDestroy.length === 0 && topicsWrite === null) {
4850
4862
  return;
4851
4863
  }
4852
4864
  const orphans = await findOrphanedEntities(ctx, memoryId, toDestroy);
4853
4865
  await ctx.database.batch(
4854
4866
  ...entityOps,
4867
+ ...prepareKindBackfills(kindBackfills),
4855
4868
  ...toCreate.map(
4856
4869
  (e) => ctx.memoryEntityCollection.prepareCreate((record) => {
4857
4870
  record._setRaw("memory_id", memoryId);
@@ -5940,27 +5953,36 @@ async function backfillMemoryTopicsOp(ctx, memoryIds) {
5940
5953
  if (!entityCtx) {
5941
5954
  throw new Error("backfillMemoryTopicsOp requires ctx.entityCtx");
5942
5955
  }
5956
+ const uniqueIds = Array.from(new Set(memoryIds));
5957
+ if (uniqueIds.length === 0) return [];
5943
5958
  const filled = [];
5944
- for (const id of Array.from(new Set(memoryIds))) {
5945
- let record;
5946
- try {
5947
- record = await ctx.vaultMemoryCollection.find(id);
5948
- } catch {
5949
- continue;
5950
- }
5951
- if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) continue;
5952
- if (parseTopics(record.topics) !== null) continue;
5953
- const links = await entityCtx.memoryEntityCollection.query(import_watermelondb9.Q.where("memory_id", id)).unsafeFetchRaw();
5954
- const entityIds = links.map((l) => String(l.entity_id));
5955
- if (entityIds.length === 0) continue;
5956
- const entities = await entityCtx.entityCollection.query(import_watermelondb9.Q.where("id", import_watermelondb9.Q.oneOf(entityIds))).fetch();
5957
- if (entities.length === 0) continue;
5958
- const source = record.topicsUserManaged ? "user" : "auto";
5959
+ const CHUNK = 500;
5960
+ for (let i = 0; i < uniqueIds.length; i += CHUNK) {
5961
+ const chunkIds = uniqueIds.slice(i, i + CHUNK);
5959
5962
  await ctx.database.write(async () => {
5960
- const topicsOp = await prepareMemoryTopicsUpdate(entityCtx, id, entities, [], source);
5961
- if (!topicsOp) return;
5962
- await ctx.database.batch(topicsOp);
5963
- filled.push(id);
5963
+ const writes = [];
5964
+ for (const id of chunkIds) {
5965
+ let record;
5966
+ try {
5967
+ record = await ctx.vaultMemoryCollection.find(id);
5968
+ } catch {
5969
+ continue;
5970
+ }
5971
+ if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) continue;
5972
+ if (parseTopics(record.topics) !== null) continue;
5973
+ const links = await entityCtx.memoryEntityCollection.query(import_watermelondb9.Q.where("memory_id", id)).unsafeFetchRaw();
5974
+ const entityIds = links.map((l) => String(l.entity_id));
5975
+ if (entityIds.length === 0) continue;
5976
+ const entities = await entityCtx.entityCollection.query(import_watermelondb9.Q.where("id", import_watermelondb9.Q.oneOf(entityIds))).fetch();
5977
+ if (entities.length === 0) continue;
5978
+ const source = record.topicsUserManaged ? "user" : "auto";
5979
+ const write = await resolveMemoryTopicsWrite(entityCtx, id, entities, [], source);
5980
+ if (write) writes.push(write);
5981
+ }
5982
+ if (writes.length === 0) return;
5983
+ const prepared = writes.map(prepareMemoryTopicsUpdate);
5984
+ await ctx.database.batch(...prepared);
5985
+ for (const write of writes) filled.push(write.row.id);
5964
5986
  });
5965
5987
  }
5966
5988
  return filled;
@@ -17095,7 +17117,7 @@ function createAutoExtractor(options) {
17095
17117
  const t0 = Date.now();
17096
17118
  void (async () => {
17097
17119
  try {
17098
- const { candidates, results, failedCount, outcome } = await extractAndRetain(
17120
+ const { candidates, results, failedCount, outcome, failure } = await extractAndRetain(
17099
17121
  window2,
17100
17122
  retainCtx,
17101
17123
  {
@@ -17135,7 +17157,11 @@ function createAutoExtractor(options) {
17135
17157
  failedCount,
17136
17158
  durationMs: Date.now() - t0,
17137
17159
  conversationId,
17138
- outcome
17160
+ outcome,
17161
+ // Only set alongside `outcome: "empty-after-retry"`. Spread so the key
17162
+ // is absent rather than explicitly undefined on a healthy turn —
17163
+ // analytics backends store an explicit undefined as a real value.
17164
+ ...failure !== void 0 && { failure }
17139
17165
  });
17140
17166
  } catch (err) {
17141
17167
  options.onError?.(err instanceof Error ? err : new Error(String(err)), conversationId);
@@ -6768,6 +6768,14 @@ declare function relinkMemoryTopicsOp(ctx: VaultMemoryOperationsContext, memoryI
6768
6768
  * row has: a curated memory's topics are recorded as `user`, everything else as
6769
6769
  * `auto`. Skips deleted, foreign-user, unlinked rows, and rows that already have
6770
6770
  * a record. Returns the ids filled.
6771
+ *
6772
+ * Runs as ONE writer over the whole (caller-bounded) list, in two phases:
6773
+ * resolve every row's write — all the awaits — and only then prepare and batch,
6774
+ * synchronously. Resolving and preparing per row inside its own writer is what
6775
+ * fired WatermelonDB's "wasn't sent to batch() synchronously" diagnostic once
6776
+ * per memory (~107 on a single dev launch, sdk#891); it also cost N writes.
6777
+ * Same treatment as {@link stampTopicsExtractedAtOp}, including its
6778
+ * transpilation hazard — the prepare pass MUST stay a `.map()`.
6771
6779
  */
6772
6780
  declare function backfillMemoryTopicsOp(ctx: VaultMemoryOperationsContext, memoryIds: readonly string[]): Promise<string[]>;
6773
6781
  declare function deleteAllVaultMemoriesForUserOp(ctx: VaultMemoryOperationsContext, userId: string): Promise<number>;
@@ -7230,6 +7238,22 @@ interface TurnCompleteEvent {
7230
7238
  * previously indistinguishable (both surfaced as zero candidates).
7231
7239
  */
7232
7240
  outcome: ExtractOutcome;
7241
+ /**
7242
+ * Present only alongside `outcome: "empty-after-retry"` — WHICH failure ended
7243
+ * the turn (#888).
7244
+ *
7245
+ * `outcome` says extraction gave up; this says why, from a stable enum. The
7246
+ * distinction is the whole point: a 2026-08-11 audit measured ~63% of
7247
+ * production extraction turns ending in `empty-after-retry` and could not tell
7248
+ * from telemetry whether the cause was the freeloader 403 everyone assumed or
7249
+ * something else. It took a Prometheus cross-check to find the real one — the
7250
+ * portal returning HTTP 200 with an empty body, which it counts as a success.
7251
+ *
7252
+ * Forward `failure.reason` into your extraction analytics event; all three
7253
+ * fields are bounded (an enum, an HTTP status, a small attempt count) and none
7254
+ * carries content.
7255
+ */
7256
+ failure?: PortalLlmFailure;
7233
7257
  }
7234
7258
  /** @public */
7235
7259
  /**
@@ -6768,6 +6768,14 @@ declare function relinkMemoryTopicsOp(ctx: VaultMemoryOperationsContext, memoryI
6768
6768
  * row has: a curated memory's topics are recorded as `user`, everything else as
6769
6769
  * `auto`. Skips deleted, foreign-user, unlinked rows, and rows that already have
6770
6770
  * a record. Returns the ids filled.
6771
+ *
6772
+ * Runs as ONE writer over the whole (caller-bounded) list, in two phases:
6773
+ * resolve every row's write — all the awaits — and only then prepare and batch,
6774
+ * synchronously. Resolving and preparing per row inside its own writer is what
6775
+ * fired WatermelonDB's "wasn't sent to batch() synchronously" diagnostic once
6776
+ * per memory (~107 on a single dev launch, sdk#891); it also cost N writes.
6777
+ * Same treatment as {@link stampTopicsExtractedAtOp}, including its
6778
+ * transpilation hazard — the prepare pass MUST stay a `.map()`.
6771
6779
  */
6772
6780
  declare function backfillMemoryTopicsOp(ctx: VaultMemoryOperationsContext, memoryIds: readonly string[]): Promise<string[]>;
6773
6781
  declare function deleteAllVaultMemoriesForUserOp(ctx: VaultMemoryOperationsContext, userId: string): Promise<number>;
@@ -7230,6 +7238,22 @@ interface TurnCompleteEvent {
7230
7238
  * previously indistinguishable (both surfaced as zero candidates).
7231
7239
  */
7232
7240
  outcome: ExtractOutcome;
7241
+ /**
7242
+ * Present only alongside `outcome: "empty-after-retry"` — WHICH failure ended
7243
+ * the turn (#888).
7244
+ *
7245
+ * `outcome` says extraction gave up; this says why, from a stable enum. The
7246
+ * distinction is the whole point: a 2026-08-11 audit measured ~63% of
7247
+ * production extraction turns ending in `empty-after-retry` and could not tell
7248
+ * from telemetry whether the cause was the freeloader 403 everyone assumed or
7249
+ * something else. It took a Prometheus cross-check to find the real one — the
7250
+ * portal returning HTTP 200 with an empty body, which it counts as a success.
7251
+ *
7252
+ * Forward `failure.reason` into your extraction analytics event; all three
7253
+ * fields are bounded (an enum, an HTTP status, a small attempt count) and none
7254
+ * carries content.
7255
+ */
7256
+ failure?: PortalLlmFailure;
7233
7257
  }
7234
7258
  /** @public */
7235
7259
  /**
@@ -132,7 +132,7 @@ import {
132
132
  upsertMessageOp,
133
133
  useEncryption,
134
134
  vaultMemoryToStored
135
- } from "./chunk-LUGYCFVI.mjs";
135
+ } from "./chunk-4M56MKBV.mjs";
136
136
  import {
137
137
  PII_PATTERNS,
138
138
  PiiRedactor,
@@ -7539,7 +7539,7 @@ function createAutoExtractor(options) {
7539
7539
  const t0 = Date.now();
7540
7540
  void (async () => {
7541
7541
  try {
7542
- const { candidates, results, failedCount, outcome } = await extractAndRetain(
7542
+ const { candidates, results, failedCount, outcome, failure } = await extractAndRetain(
7543
7543
  window,
7544
7544
  retainCtx,
7545
7545
  {
@@ -7579,7 +7579,11 @@ function createAutoExtractor(options) {
7579
7579
  failedCount,
7580
7580
  durationMs: Date.now() - t0,
7581
7581
  conversationId,
7582
- outcome
7582
+ outcome,
7583
+ // Only set alongside `outcome: "empty-after-retry"`. Spread so the key
7584
+ // is absent rather than explicitly undefined on a healthy turn —
7585
+ // analytics backends store an explicit undefined as a real value.
7586
+ ...failure !== void 0 && { failure }
7583
7587
  });
7584
7588
  } catch (err) {
7585
7589
  options.onError?.(err instanceof Error ? err : new Error(String(err)), conversationId);
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  recall
3
- } from "./chunk-LUGYCFVI.mjs";
3
+ } from "./chunk-4M56MKBV.mjs";
4
4
  import "./chunk-CJ32HO6Q.mjs";
5
5
  import "./chunk-GYKJOWEO.mjs";
6
6
  export {