@anuma/sdk 3.12.2 → 3.13.1

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.
@@ -4202,6 +4202,31 @@ var init_endpointOverride = __esm({
4202
4202
  function normalizeEntityName(name) {
4203
4203
  return name.trim().toLowerCase();
4204
4204
  }
4205
+ function serializeTopics(topics) {
4206
+ return JSON.stringify(topics);
4207
+ }
4208
+ function parseTopics(value) {
4209
+ if (typeof value !== "string" || value.length === 0) return null;
4210
+ let parsed;
4211
+ try {
4212
+ parsed = JSON.parse(value);
4213
+ } catch {
4214
+ return null;
4215
+ }
4216
+ if (!Array.isArray(parsed)) return null;
4217
+ const out = [];
4218
+ for (const raw of parsed) {
4219
+ if (typeof raw !== "object" || raw === null) continue;
4220
+ const { name, kind, source } = raw;
4221
+ if (typeof name !== "string" || name.trim().length === 0) continue;
4222
+ out.push({
4223
+ name,
4224
+ ...typeof kind === "string" && kind.length > 0 ? { kind } : {},
4225
+ source: source === "user" ? "user" : "auto"
4226
+ });
4227
+ }
4228
+ return out;
4229
+ }
4205
4230
  var ENTITY_KINDS;
4206
4231
  var init_types3 = __esm({
4207
4232
  "src/lib/db/entities/types.ts"() {
@@ -4267,9 +4292,62 @@ async function upsertEntitiesInWrite(ctx, entities) {
4267
4292
  for (const record of created) out.set(record.canonicalName, entityToStored(record));
4268
4293
  return { entities: out, operations: [...created, ...updated] };
4269
4294
  }
4295
+ async function findVaultRowInWrite(ctx, memoryId) {
4296
+ try {
4297
+ const rows = await ctx.database.get("memory_vault").query(import_watermelondb9.Q.where("id", memoryId)).fetch();
4298
+ return rows[0] ?? null;
4299
+ } catch {
4300
+ return null;
4301
+ }
4302
+ }
4303
+ function prepareTopicsUpdate(row, topics, now) {
4304
+ const originalUpdatedAt = row.updatedAt.getTime();
4305
+ return row.prepareUpdate((r) => {
4306
+ r._setRaw("topics", serializeTopics(topics));
4307
+ r._setRaw("topics_updated_at", now);
4308
+ r._setRaw("updated_at", originalUpdatedAt);
4309
+ });
4310
+ }
4311
+ function displayNamesOf(entities) {
4312
+ const out = /* @__PURE__ */ new Map();
4313
+ for (const e of entities) {
4314
+ const name = e.name.trim();
4315
+ const normalized = normalizeEntityName(name);
4316
+ if (normalized.length === 0 || out.has(normalized)) continue;
4317
+ out.set(normalized, name);
4318
+ }
4319
+ return out;
4320
+ }
4321
+ function topicsEqual(stored, computed) {
4322
+ if (stored === null || stored.length !== computed.length) return false;
4323
+ const key = (t) => JSON.stringify([t.name, t.kind ?? null, t.source]);
4324
+ const storedKeys = stored.map(key).sort();
4325
+ const computedKeys = computed.map(key).sort();
4326
+ return storedKeys.every((k, i) => k === computedKeys[i]);
4327
+ }
4328
+ function resolveTopicsWrite(row, linked, displayNames, source) {
4329
+ if (row === null || source === null) return null;
4330
+ const topics = topicsForEntities(linked, displayNames, source);
4331
+ if (topicsEqual(parseTopics(row.topics), topics)) return null;
4332
+ return { row, topics };
4333
+ }
4334
+ function topicsForEntities(entities, displayNames, source) {
4335
+ return entities.map((e) => {
4336
+ const name = displayNames.get(e.canonicalName) ?? e.canonicalName;
4337
+ return { name, ...e.kind ? { kind: e.kind } : {}, source };
4338
+ });
4339
+ }
4340
+ function toEntityObjects(entityInputs) {
4341
+ return entityInputs.map((e) => typeof e === "string" ? { name: e } : e);
4342
+ }
4343
+ async function prepareMemoryTopicsUpdate(ctx, memoryId, linked, inputs, source) {
4344
+ const row = await findVaultRowInWrite(ctx, memoryId);
4345
+ const write = resolveTopicsWrite(row, linked, displayNamesOf(toEntityObjects(inputs)), source);
4346
+ return write === null ? null : prepareTopicsUpdate(write.row, write.topics, Date.now());
4347
+ }
4270
4348
  async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
4271
4349
  if (entityInputs.length === 0) return [];
4272
- const normalized = entityInputs.map((e) => typeof e === "string" ? { name: e } : e);
4350
+ const normalized = toEntityObjects(entityInputs);
4273
4351
  const userId = ctx.userId;
4274
4352
  let skipped = false;
4275
4353
  let entities = [];
@@ -4279,7 +4357,8 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
4279
4357
  normalized
4280
4358
  );
4281
4359
  entities = Array.from(byName.values());
4282
- if (options?.unlessTopicsUserManaged && await autoLinkBlocked(ctx, memoryId)) {
4360
+ const vaultRow = await findVaultRowInWrite(ctx, memoryId);
4361
+ if (options?.unlessTopicsUserManaged && autoLinkBlocked(vaultRow)) {
4283
4362
  if (entityOps.length > 0) {
4284
4363
  await ctx.database.batch(...entityOps);
4285
4364
  }
@@ -4290,7 +4369,18 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
4290
4369
  const existingLinks = await ctx.memoryEntityCollection.query(import_watermelondb9.Q.where("memory_id", memoryId)).fetch();
4291
4370
  const existingEntityIds = new Set(existingLinks.map((l) => String(l.entityId)));
4292
4371
  const toCreate = entities.filter((e) => !existingEntityIds.has(e.uniqueId));
4293
- if (entityOps.length === 0 && toCreate.length === 0) return;
4372
+ const keptIds = entities.map((e) => e.uniqueId);
4373
+ const carriedOver = await resolveEntitiesByIds(
4374
+ ctx,
4375
+ [...existingEntityIds].filter((id) => !keptIds.includes(id))
4376
+ );
4377
+ const topicsWrite = resolveTopicsWrite(
4378
+ vaultRow,
4379
+ [...carriedOver, ...entities],
4380
+ displayNamesOf(normalized),
4381
+ options?.topicsSource ?? "auto"
4382
+ );
4383
+ if (entityOps.length === 0 && toCreate.length === 0 && topicsWrite === null) return;
4294
4384
  const linkOps = toCreate.map(
4295
4385
  (e) => ctx.memoryEntityCollection.prepareCreate((record) => {
4296
4386
  record._setRaw("memory_id", memoryId);
@@ -4298,26 +4388,42 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
4298
4388
  if (userId !== void 0) record._setRaw("user_id", userId);
4299
4389
  })
4300
4390
  );
4301
- await ctx.database.batch(...entityOps, ...linkOps);
4391
+ await ctx.database.batch(
4392
+ ...entityOps,
4393
+ ...linkOps,
4394
+ ...topicsWrite ? [prepareTopicsUpdate(topicsWrite.row, topicsWrite.topics, Date.now())] : []
4395
+ );
4302
4396
  });
4303
4397
  return skipped ? [] : entities;
4304
4398
  }
4305
- async function autoLinkBlocked(ctx, memoryId) {
4306
- try {
4307
- const rows = await ctx.database.get("memory_vault").query(import_watermelondb9.Q.where("id", memoryId)).fetch();
4308
- const row = rows[0];
4309
- return !row || !!row.isDeleted || !!row.topicsUserManaged;
4310
- } catch {
4311
- return true;
4312
- }
4399
+ async function resolveEntitiesByIds(ctx, entityIds) {
4400
+ if (entityIds.length === 0) return [];
4401
+ const rows = await ctx.entityCollection.query(import_watermelondb9.Q.where("id", import_watermelondb9.Q.oneOf(Array.from(entityIds)))).fetch();
4402
+ return rows.map(entityToStored);
4403
+ }
4404
+ function autoLinkBlocked(row) {
4405
+ return !row || !!row.isDeleted || !!row.topicsUserManaged;
4313
4406
  }
4314
4407
  async function replaceMemoryEntitiesGuardedOp(ctx, memoryId, entityInputs) {
4315
- const normalized = entityInputs.map((e) => typeof e === "string" ? { name: e } : e);
4408
+ return replaceMemoryEntities(ctx, memoryId, entityInputs, {
4409
+ guarded: true,
4410
+ topicsSource: "auto"
4411
+ });
4412
+ }
4413
+ async function relinkMemoryEntitiesFromTopicsOp(ctx, memoryId, topics) {
4414
+ return replaceMemoryEntities(ctx, memoryId, topics, {
4415
+ guarded: false,
4416
+ topicsSource: null
4417
+ });
4418
+ }
4419
+ async function replaceMemoryEntities(ctx, memoryId, entityInputs, options) {
4420
+ const normalized = toEntityObjects(entityInputs);
4316
4421
  const userId = ctx.userId;
4317
4422
  let skipped = false;
4318
4423
  let entities = [];
4319
4424
  await ctx.database.write(async () => {
4320
- if (await autoLinkBlocked(ctx, memoryId)) {
4425
+ const vaultRow = await findVaultRowInWrite(ctx, memoryId);
4426
+ if (options.guarded && autoLinkBlocked(vaultRow)) {
4321
4427
  skipped = true;
4322
4428
  return;
4323
4429
  }
@@ -4331,7 +4437,15 @@ async function replaceMemoryEntitiesGuardedOp(ctx, memoryId, entityInputs) {
4331
4437
  const existingEntityIds = new Set(existingLinks.map((l) => String(l.entityId)));
4332
4438
  const toCreate = entities.filter((e) => !existingEntityIds.has(e.uniqueId));
4333
4439
  const toDestroy = existingLinks.filter((l) => !keep.has(String(l.entityId)));
4334
- if (entityOps.length === 0 && toCreate.length === 0 && toDestroy.length === 0) return;
4440
+ const topicsWrite = resolveTopicsWrite(
4441
+ vaultRow,
4442
+ entities,
4443
+ displayNamesOf(normalized),
4444
+ options.topicsSource
4445
+ );
4446
+ if (entityOps.length === 0 && toCreate.length === 0 && toDestroy.length === 0 && topicsWrite === null) {
4447
+ return;
4448
+ }
4335
4449
  const orphans = await findOrphanedEntities(ctx, memoryId, toDestroy);
4336
4450
  await ctx.database.batch(
4337
4451
  ...entityOps,
@@ -4343,7 +4457,8 @@ async function replaceMemoryEntitiesGuardedOp(ctx, memoryId, entityInputs) {
4343
4457
  })
4344
4458
  ),
4345
4459
  ...toDestroy.map((l) => l.prepareDestroyPermanently()),
4346
- ...orphans.map((e) => e.prepareDestroyPermanently())
4460
+ ...orphans.map((e) => e.prepareDestroyPermanently()),
4461
+ ...topicsWrite ? [prepareTopicsUpdate(topicsWrite.row, topicsWrite.topics, Date.now())] : []
4347
4462
  );
4348
4463
  });
4349
4464
  return skipped ? null : entities;
@@ -4564,6 +4679,8 @@ function vaultMemoryToStoredRaw(memory) {
4564
4679
  eventTimeEnd: memory.eventTimeEnd ?? null,
4565
4680
  eventTimeKind: memory.eventTimeKind ?? null,
4566
4681
  topicsUserManaged: memory.topicsUserManaged ?? false,
4682
+ topics: parseTopics(memory.topics),
4683
+ topicsUpdatedAt: memory.topicsUpdatedAt ?? null,
4567
4684
  topicsExtractedAt: memory.topicsExtractedAt ?? null,
4568
4685
  topicsExtractedVersion: memory.topicsExtractedVersion ?? null,
4569
4686
  supersededBy: memory.supersededBy ?? null,
@@ -4840,6 +4957,8 @@ function vaultMemoryRawToStoredRaw(raw) {
4840
4957
  eventTimeKind: raw.event_time_kind ?? null,
4841
4958
  // SQLite stores booleans as 0/1, LokiJS as true/false — coerce both.
4842
4959
  topicsUserManaged: raw.topics_user_managed === true || raw.topics_user_managed === 1,
4960
+ topics: parseTopics(raw.topics),
4961
+ topicsUpdatedAt: raw.topics_updated_at ?? null,
4843
4962
  topicsExtractedAt: raw.topics_extracted_at ?? null,
4844
4963
  topicsExtractedVersion: raw.topics_extracted_version ?? null,
4845
4964
  supersededBy: raw.superseded_by ?? null,
@@ -5130,13 +5249,23 @@ async function setMemoryEntitiesOp(ctx, memoryId, entities) {
5130
5249
  });
5131
5250
  });
5132
5251
  if (stale) return null;
5133
- const linked = entities.length > 0 ? await linkMemoryEntitiesOp(entityCtx, memoryId, entities) : [];
5252
+ const linked = entities.length > 0 ? await linkMemoryEntitiesOp(entityCtx, memoryId, entities, { topicsSource: "user" }) : [];
5134
5253
  const keep = new Set(linked.map((e) => e.uniqueId));
5135
5254
  const existing = await entityCtx.memoryEntityCollection.query(import_watermelondb10.Q.where("memory_id", memoryId)).fetch();
5136
5255
  const staleLinks = existing.filter((l) => !keep.has(String(l.entityId)));
5137
- if (staleLinks.length > 0) {
5256
+ if (staleLinks.length > 0 || entities.length === 0) {
5138
5257
  await ctx.database.write(async () => {
5139
- await ctx.database.batch(...staleLinks.map((l) => l.prepareDestroyPermanently()));
5258
+ const topicsOp = await prepareMemoryTopicsUpdate(
5259
+ entityCtx,
5260
+ memoryId,
5261
+ linked,
5262
+ entities,
5263
+ "user"
5264
+ );
5265
+ await ctx.database.batch(
5266
+ ...staleLinks.map((l) => l.prepareDestroyPermanently()),
5267
+ ...topicsOp ? [topicsOp] : []
5268
+ );
5140
5269
  });
5141
5270
  }
5142
5271
  return vaultMemoryToStored(record, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner);
@@ -5266,56 +5395,111 @@ async function getUnfiledVaultMemoriesOp(ctx) {
5266
5395
  (record) => vaultMemoryToStored(record, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner)
5267
5396
  );
5268
5397
  }
5398
+ async function linkedEntityNamesByMemory(entityCtx, memoryIds) {
5399
+ const CHUNK = 500;
5400
+ const entityIdByMemory = /* @__PURE__ */ new Map();
5401
+ const allEntityIds = /* @__PURE__ */ new Set();
5402
+ for (let i = 0; i < memoryIds.length; i += CHUNK) {
5403
+ const links = await entityCtx.memoryEntityCollection.query(import_watermelondb10.Q.where("memory_id", import_watermelondb10.Q.oneOf(memoryIds.slice(i, i + CHUNK)))).unsafeFetchRaw();
5404
+ for (const link of links) {
5405
+ const memoryId = String(link.memory_id);
5406
+ const entityId = String(link.entity_id);
5407
+ allEntityIds.add(entityId);
5408
+ let bucket = entityIdByMemory.get(memoryId);
5409
+ if (!bucket) {
5410
+ bucket = /* @__PURE__ */ new Set();
5411
+ entityIdByMemory.set(memoryId, bucket);
5412
+ }
5413
+ bucket.add(entityId);
5414
+ }
5415
+ }
5416
+ const entityIds = Array.from(allEntityIds);
5417
+ const nameById = /* @__PURE__ */ new Map();
5418
+ for (let i = 0; i < entityIds.length; i += CHUNK) {
5419
+ const entities = await entityCtx.entityCollection.query(import_watermelondb10.Q.where("id", import_watermelondb10.Q.oneOf(entityIds.slice(i, i + CHUNK)))).unsafeFetchRaw();
5420
+ for (const e of entities) {
5421
+ if (typeof e.canonical_name === "string") nameById.set(String(e.id), e.canonical_name);
5422
+ }
5423
+ }
5424
+ const out = /* @__PURE__ */ new Map();
5425
+ for (const [memoryId, ids] of entityIdByMemory) {
5426
+ const names = /* @__PURE__ */ new Set();
5427
+ for (const id of ids) {
5428
+ const name = nameById.get(id);
5429
+ if (name !== void 0) names.add(name);
5430
+ }
5431
+ if (names.size > 0) out.set(memoryId, names);
5432
+ }
5433
+ return out;
5434
+ }
5435
+ function linksDivergeFromTopics(topics, linked) {
5436
+ const wanted = new Set(
5437
+ topics.map((t) => normalizeEntityName(t.name)).filter((n) => n.length > 0)
5438
+ );
5439
+ if (wanted.size !== linked.size) return true;
5440
+ for (const name of wanted) if (!linked.has(name)) return true;
5441
+ return false;
5442
+ }
5269
5443
  async function getMemoriesNeedingTopicExtractionOp(ctx, options) {
5270
5444
  const entityCtx = ctx.entityCtx;
5271
5445
  if (!entityCtx) {
5272
5446
  throw new Error("getMemoriesNeedingTopicExtractionOp requires ctx.entityCtx");
5273
5447
  }
5274
- const conditions = [
5275
- // Boolean optional column: null (legacy) and false both mean auto-managed.
5276
- // Q.notEq(true) is a NULL trap on SQLite (NULL <> TRUE is NULL) — enumerate.
5277
- import_watermelondb10.Q.or(import_watermelondb10.Q.where("topics_user_managed", null), import_watermelondb10.Q.where("topics_user_managed", false)),
5278
- ...baseVaultConditions(ctx),
5279
- import_watermelondb10.Q.sortBy("created_at", import_watermelondb10.Q.desc)
5280
- ];
5448
+ const conditions = [...baseVaultConditions(ctx), import_watermelondb10.Q.sortBy("created_at", import_watermelondb10.Q.desc)];
5281
5449
  const rows = await ctx.vaultMemoryCollection.query(...conditions).unsafeFetchRaw();
5282
- const stampedPending = [];
5283
- const unstamped = [];
5450
+ const linkedNames = await linkedEntityNamesByMemory(
5451
+ entityCtx,
5452
+ rows.map((r) => r.id)
5453
+ );
5454
+ const pendingRaw = [];
5455
+ const stampedPendingRaw = [];
5456
+ const linkedUnstampedAll = [];
5457
+ const topicsToRelinkAll = [];
5458
+ const topicsBackfillAll = [];
5284
5459
  for (const raw of rows) {
5460
+ const id = raw.id;
5461
+ const topics = parseTopics(raw.topics);
5462
+ const linked = linkedNames.get(id);
5463
+ if (topics !== null && topics.length > 0 && linksDivergeFromTopics(topics, linked ?? /* @__PURE__ */ new Set())) {
5464
+ topicsToRelinkAll.push(id);
5465
+ continue;
5466
+ }
5467
+ if (raw.topics_user_managed) {
5468
+ if (topics === null && linked !== void 0) topicsBackfillAll.push(id);
5469
+ continue;
5470
+ }
5285
5471
  const stamp = raw.topics_extracted_at ?? null;
5472
+ let isPending = false;
5286
5473
  if (stamp !== null) {
5287
5474
  const version = raw.topics_extracted_version ?? 0;
5288
5475
  if (raw.updated_at > stamp || version < TOPICS_EXTRACTION_VERSION) {
5289
- stampedPending.push(raw);
5476
+ stampedPendingRaw.push(raw);
5477
+ isPending = true;
5290
5478
  }
5291
- } else {
5292
- unstamped.push(raw);
5293
- }
5294
- }
5295
- const linkedIds = /* @__PURE__ */ new Set();
5296
- const CHUNK = 500;
5297
- for (let i = 0; i < unstamped.length; i += CHUNK) {
5298
- const ids = unstamped.slice(i, i + CHUNK).map((r) => r.id);
5299
- const links = await entityCtx.memoryEntityCollection.query(import_watermelondb10.Q.where("memory_id", import_watermelondb10.Q.oneOf(ids))).unsafeFetchRaw();
5300
- for (const link of links) linkedIds.add(String(link.memory_id));
5301
- }
5302
- const linkedUnstampedAll = [];
5303
- const pendingRaw = [...stampedPending];
5304
- for (const raw of unstamped) {
5305
- if (linkedIds.has(raw.id)) {
5306
- linkedUnstampedAll.push(raw.id);
5479
+ } else if (linked !== void 0) {
5480
+ linkedUnstampedAll.push(id);
5307
5481
  } else {
5308
5482
  pendingRaw.push(raw);
5483
+ isPending = true;
5484
+ }
5485
+ if (!isPending && topics === null && linked !== void 0) {
5486
+ topicsBackfillAll.push(id);
5309
5487
  }
5310
5488
  }
5311
5489
  const cap = options?.limit !== void 0 && options.limit > 0 ? options.limit : void 0;
5312
- const limitedPendingRaw = cap !== void 0 ? pendingRaw.slice(0, cap) : pendingRaw;
5313
- const linkedUnstamped = cap !== void 0 ? linkedUnstampedAll.slice(0, cap) : linkedUnstampedAll;
5490
+ const orderedPendingRaw = [...stampedPendingRaw, ...pendingRaw];
5491
+ const limitedPendingRaw = cap !== void 0 ? orderedPendingRaw.slice(0, cap) : orderedPendingRaw;
5314
5492
  const pending = await mapInBatches(
5315
5493
  limitedPendingRaw,
5316
5494
  (raw) => vaultMemoryRawToStored(raw, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner)
5317
5495
  );
5318
- return { pending, linkedUnstamped };
5496
+ const applyCap = (ids) => cap !== void 0 ? ids.slice(0, cap) : ids;
5497
+ return {
5498
+ pending,
5499
+ linkedUnstamped: applyCap(linkedUnstampedAll),
5500
+ topicsToRelink: applyCap(topicsToRelinkAll),
5501
+ topicsBackfill: applyCap(topicsBackfillAll)
5502
+ };
5319
5503
  }
5320
5504
  async function stampTopicsExtractedAtOp(ctx, memoryIds, extractedAt, version = TOPICS_EXTRACTION_VERSION) {
5321
5505
  if (memoryIds.length === 0) return [];
@@ -5349,6 +5533,61 @@ async function stampTopicsExtractedAtOp(ctx, memoryIds, extractedAt, version = T
5349
5533
  }
5350
5534
  return stamped;
5351
5535
  }
5536
+ async function relinkMemoryTopicsOp(ctx, memoryIds) {
5537
+ const entityCtx = ctx.entityCtx;
5538
+ if (!entityCtx) {
5539
+ throw new Error("relinkMemoryTopicsOp requires ctx.entityCtx");
5540
+ }
5541
+ const relinked = [];
5542
+ for (const id of Array.from(new Set(memoryIds))) {
5543
+ let record;
5544
+ try {
5545
+ record = await ctx.vaultMemoryCollection.find(id);
5546
+ } catch {
5547
+ continue;
5548
+ }
5549
+ if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) continue;
5550
+ const topics = parseTopics(record.topics);
5551
+ if (topics === null || topics.length === 0) continue;
5552
+ try {
5553
+ await relinkMemoryEntitiesFromTopicsOp(entityCtx, id, topics);
5554
+ relinked.push(id);
5555
+ } catch (err) {
5556
+ getLogger().warn("[memory/topics] relink failed", err);
5557
+ }
5558
+ }
5559
+ return relinked;
5560
+ }
5561
+ async function backfillMemoryTopicsOp(ctx, memoryIds) {
5562
+ const entityCtx = ctx.entityCtx;
5563
+ if (!entityCtx) {
5564
+ throw new Error("backfillMemoryTopicsOp requires ctx.entityCtx");
5565
+ }
5566
+ const filled = [];
5567
+ for (const id of Array.from(new Set(memoryIds))) {
5568
+ let record;
5569
+ try {
5570
+ record = await ctx.vaultMemoryCollection.find(id);
5571
+ } catch {
5572
+ continue;
5573
+ }
5574
+ if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) continue;
5575
+ if (parseTopics(record.topics) !== null) continue;
5576
+ const links = await entityCtx.memoryEntityCollection.query(import_watermelondb10.Q.where("memory_id", id)).unsafeFetchRaw();
5577
+ const entityIds = links.map((l) => String(l.entity_id));
5578
+ if (entityIds.length === 0) continue;
5579
+ const entities = await entityCtx.entityCollection.query(import_watermelondb10.Q.where("id", import_watermelondb10.Q.oneOf(entityIds))).fetch();
5580
+ if (entities.length === 0) continue;
5581
+ const source = record.topicsUserManaged ? "user" : "auto";
5582
+ await ctx.database.write(async () => {
5583
+ const topicsOp = await prepareMemoryTopicsUpdate(entityCtx, id, entities, [], source);
5584
+ if (!topicsOp) return;
5585
+ await ctx.database.batch(topicsOp);
5586
+ filled.push(id);
5587
+ });
5588
+ }
5589
+ return filled;
5590
+ }
5352
5591
  async function deleteAllVaultMemoriesForUserOp(ctx, userId) {
5353
5592
  if (ctx.userId !== void 0 && ctx.userId !== userId) return 0;
5354
5593
  const records = await ctx.vaultMemoryCollection.query(import_watermelondb10.Q.where("user_id", userId), import_watermelondb10.Q.where("is_deleted", false)).fetch();
@@ -5490,6 +5729,7 @@ var init_operations3 = __esm({
5490
5729
  import_watermelondb10 = require("@nozbe/watermelondb");
5491
5730
  init_logger();
5492
5731
  init_operations2();
5732
+ init_types3();
5493
5733
  init_encryption2();
5494
5734
  NON_PRIVATE_VISIBILITIES = ["public"];
5495
5735
  KNOWN_TRUST_TIERS = /* @__PURE__ */ new Set(["quarantined", "trusted"]);
@@ -8740,6 +8980,7 @@ __export(index_exports, {
8740
8980
  appFileToStored: () => appFileToStored,
8741
8981
  archiveVaultMemoryOp: () => archiveVaultMemoryOp,
8742
8982
  backfillMemoryEntityUserIdsOp: () => backfillMemoryEntityUserIdsOp,
8983
+ backfillMemoryTopicsOp: () => backfillMemoryTopicsOp,
8743
8984
  capHopsForDensity: () => capHopsForDensity,
8744
8985
  chatStorageMigrations: () => chatStorageMigrations,
8745
8986
  chatStorageSchema: () => chatStorageSchema,
@@ -8975,6 +9216,7 @@ __export(index_exports, {
8975
9216
  refreshDriveToken: () => refreshDriveToken,
8976
9217
  refreshGithubToken: () => refreshGithubToken,
8977
9218
  refreshNotionToken: () => refreshNotionToken,
9219
+ relinkMemoryTopicsOp: () => relinkMemoryTopicsOp,
8978
9220
  relinkMisclassifiedVideosOp: () => relinkMisclassifiedVideosOp,
8979
9221
  removeById: () => removeById,
8980
9222
  renderAnumaJsx: () => renderAnumaJsx,
@@ -15684,6 +15926,12 @@ __decorateClass([
15684
15926
  __decorateClass([
15685
15927
  (0, import_decorators3.field)("topics_user_managed")
15686
15928
  ], VaultMemory.prototype, "topicsUserManaged", 2);
15929
+ __decorateClass([
15930
+ (0, import_decorators3.field)("topics")
15931
+ ], VaultMemory.prototype, "topics", 2);
15932
+ __decorateClass([
15933
+ (0, import_decorators3.field)("topics_updated_at")
15934
+ ], VaultMemory.prototype, "topicsUpdatedAt", 2);
15687
15935
  __decorateClass([
15688
15936
  (0, import_decorators3.field)("topics_extracted_at")
15689
15937
  ], VaultMemory.prototype, "topicsExtractedAt", 2);
@@ -22026,6 +22274,45 @@ function useChatStorage(options) {
22026
22274
  }
22027
22275
  }
22028
22276
  const { callRedactor, callPiiRedaction, maskForCall } = resolvePiiForCall(convId);
22277
+ const isServerToolsFunction = typeof serverToolsFilter === "function";
22278
+ const needsEmbeddings = isServerToolsFunction || !!clientToolsFilter || !!clientTools?.length;
22279
+ const embeddingsPromise = needsEmbeddings && getToken ? (async () => {
22280
+ try {
22281
+ const embeddingOptions = { getToken, baseUrl, model: embeddingModel };
22282
+ if (shouldChunkMessage(contentForStorage, DEFAULT_CHUNK_SIZE)) {
22283
+ const textChunks = chunkText(contentForStorage);
22284
+ const chunkTexts = textChunks.map((c) => maskForCall(c.text));
22285
+ return {
22286
+ embeddings: await generateEmbeddings(chunkTexts, embeddingOptions),
22287
+ failed: false
22288
+ };
22289
+ }
22290
+ if (contentForStorage.length >= MIN_CONTENT_LENGTH_FOR_TOOLS) {
22291
+ return {
22292
+ embeddings: await generateEmbedding(
22293
+ maskForCall(contentForStorage),
22294
+ embeddingOptions
22295
+ ),
22296
+ failed: false
22297
+ };
22298
+ }
22299
+ return { failed: false };
22300
+ } catch {
22301
+ return { failed: true };
22302
+ }
22303
+ })() : null;
22304
+ const serverToolsPromise = getToken && !(Array.isArray(serverToolsFilter) && serverToolsFilter.length === 0) ? (async () => {
22305
+ try {
22306
+ return await getServerTools({
22307
+ baseUrl,
22308
+ cacheExpirationMs: serverToolsConfig?.cacheExpirationMs,
22309
+ getToken,
22310
+ cache: serverToolsConfig?.cache
22311
+ });
22312
+ } catch {
22313
+ return null;
22314
+ }
22315
+ })() : null;
22029
22316
  let messagesToSend;
22030
22317
  const storedMessages = await getMessagesOp(storageCtx, convId);
22031
22318
  const knownToolCallEventIds = /* @__PURE__ */ new Set();
@@ -22205,44 +22492,27 @@ function useChatStorage(options) {
22205
22492
  let filteredServerTools = [];
22206
22493
  let userMessageEmbeddings;
22207
22494
  let userMessageEmbeddingsFailed = false;
22208
- const isServerToolsFunction = typeof serverToolsFilter === "function";
22209
- const needsEmbeddings = isServerToolsFunction || !!clientToolsFilter || !!clientTools?.length;
22210
- if (needsEmbeddings && getToken) {
22211
- try {
22212
- const embeddingOptions = { getToken, baseUrl, model: embeddingModel };
22213
- if (shouldChunkMessage(contentForStorage, DEFAULT_CHUNK_SIZE)) {
22214
- const textChunks = chunkText(contentForStorage);
22215
- const chunkTexts = textChunks.map((c) => maskForCall(c.text));
22216
- userMessageEmbeddings = await generateEmbeddings(chunkTexts, embeddingOptions);
22217
- } else if (contentForStorage.length >= MIN_CONTENT_LENGTH_FOR_TOOLS) {
22218
- userMessageEmbeddings = await generateEmbedding(
22219
- maskForCall(contentForStorage),
22220
- embeddingOptions
22221
- );
22222
- }
22223
- } catch {
22224
- userMessageEmbeddingsFailed = true;
22225
- }
22226
- }
22227
- if (getToken && !(Array.isArray(serverToolsFilter) && serverToolsFilter.length === 0)) {
22228
- try {
22229
- const allServerTools = await getServerTools({
22230
- baseUrl,
22231
- cacheExpirationMs: serverToolsConfig?.cacheExpirationMs,
22232
- getToken,
22233
- cache: serverToolsConfig?.cache
22234
- });
22235
- if (serverToolsConfig?.deferLoading?.enabled && effectiveApiType === "responses") {
22236
- filteredServerTools = allServerTools;
22237
- } else if (isServerToolsFunction) {
22238
- if (userMessageEmbeddings) {
22239
- const toolNames = serverToolsFilter(userMessageEmbeddings, allServerTools);
22240
- filteredServerTools = filterServerTools(allServerTools, toolNames);
22495
+ if (embeddingsPromise) {
22496
+ const embeddingsResult = await embeddingsPromise;
22497
+ userMessageEmbeddings = embeddingsResult.embeddings;
22498
+ userMessageEmbeddingsFailed = embeddingsResult.failed;
22499
+ }
22500
+ if (serverToolsPromise) {
22501
+ const allServerTools = await serverToolsPromise;
22502
+ if (allServerTools) {
22503
+ try {
22504
+ if (serverToolsConfig?.deferLoading?.enabled && effectiveApiType === "responses") {
22505
+ filteredServerTools = allServerTools;
22506
+ } else if (isServerToolsFunction) {
22507
+ if (userMessageEmbeddings) {
22508
+ const toolNames = serverToolsFilter(userMessageEmbeddings, allServerTools);
22509
+ filteredServerTools = filterServerTools(allServerTools, toolNames);
22510
+ }
22511
+ } else {
22512
+ filteredServerTools = filterServerTools(allServerTools, serverToolsFilter);
22241
22513
  }
22242
- } else {
22243
- filteredServerTools = filterServerTools(allServerTools, serverToolsFilter);
22514
+ } catch {
22244
22515
  }
22245
- } catch {
22246
22516
  }
22247
22517
  }
22248
22518
  let filteredClientTools = clientTools;
@@ -22876,7 +23146,7 @@ __decorateClass([
22876
23146
  ], VaultFolder.prototype, "context", 2);
22877
23147
 
22878
23148
  // src/lib/db/schema.ts
22879
- var SDK_SCHEMA_VERSION = 41;
23149
+ var SDK_SCHEMA_VERSION = 42;
22880
23150
  var sdkSchema = (0, import_watermelondb20.appSchema)({
22881
23151
  version: SDK_SCHEMA_VERSION,
22882
23152
  tables: [
@@ -22998,9 +23268,33 @@ var sdkSchema = (0, import_watermelondb20.appSchema)({
22998
23268
  // links). Auto-extraction then leaves its links alone — the user owns
22999
23269
  // them. Null/false = topics are auto-derived (default).
23000
23270
  { name: "topics_user_managed", type: "boolean", isOptional: true },
23271
+ // The memory's topics as a DURABLE, SYNCED record: a JSON array of
23272
+ // `{name, kind?, source}`, `name` in the caller's display casing (unlike
23273
+ // `entity.canonical_name`, which is lowercased and has no display
23274
+ // column). `entity` / `memory_entity` are a device-local INDEX over this
23275
+ // — their ids are locally generated, so they can never sync — and a
23276
+ // restored device rebuilds them from these names with no LLM call.
23277
+ // Null = predates v42 (backfilled from the row's current links by the
23278
+ // sweep's topicsBackfill bucket). See getMemoriesNeedingTopicExtractionOp.
23279
+ { name: "topics", type: "string", isOptional: true },
23280
+ // Unix ms of the last write to `topics`. A SECOND timestamp is required:
23281
+ // every topic writer deliberately pins `updated_at` so a topic change
23282
+ // doesn't inflate recall's recency multiplier, and both client sync
23283
+ // paths key on `updated_at` — so without this a topic-only change would
23284
+ // neither upload nor merge. Null = `topics` never written.
23285
+ { name: "topics_updated_at", type: "number", isOptional: true },
23001
23286
  // Unix ms of the last LLM topic-extraction pass over this memory's
23002
23287
  // content. Null = never extracted standalone (legacy rows with entity
23003
23288
  // links are grandfathered — see getMemoriesNeedingTopicExtractionOp).
23289
+ //
23290
+ // DEPRECATED (v42) — `topics_updated_at` subsumes this and
23291
+ // `topics_extracted_version` both: null there means never processed, and
23292
+ // non-null with an empty `topics` means processed and found nothing
23293
+ // (today's "answered empty" case), while a release-time
23294
+ // EXTRACTOR_CHANGED_AT constant compared against `topics_updated_at`
23295
+ // replaces the version check. Both columns are kept only to avoid a
23296
+ // column-drop migration in an otherwise additive list; removing them is
23297
+ // a follow-up once `topics` is proven in production.
23004
23298
  { name: "topics_extracted_at", type: "number", isOptional: true },
23005
23299
  // Write-time supersession (A2). When set, this fact was replaced by a
23006
23300
  // newer, incompatible-value fact (e.g. "Lives in Portland" superseded by
@@ -23014,6 +23308,9 @@ var sdkSchema = (0, import_watermelondb20.appSchema)({
23014
23308
  // The extraction-logic version this memory was last stamped under. Null
23015
23309
  // (pre-v38) is treated as version 0, so a bump of TOPICS_EXTRACTION_VERSION
23016
23310
  // re-extracts stale rows. See getMemoriesNeedingTopicExtractionOp.
23311
+ //
23312
+ // DEPRECATED (v42) alongside `topics_extracted_at` — same rationale and
23313
+ // same removal follow-up; see that column's note above.
23017
23314
  { name: "topics_extracted_version", type: "number", isOptional: true },
23018
23315
  // Re-observation watermark (C3). Unix ms of the last time retain() merged
23019
23316
  // a duplicate observation into this fact (proof_count++). Distinct from
@@ -23747,6 +24044,23 @@ var sdkMigrations = (0, import_migrations2.schemaMigrations)({
23747
24044
  ]
23748
24045
  })
23749
24046
  ]
24047
+ },
24048
+ // v41 -> v42: topics + topics_updated_at on memory_vault — the durable,
24049
+ // synced record of a memory's topics. Existing rows keep both NULL: the
24050
+ // sweep's topicsBackfill bucket fills them from each row's current entity
24051
+ // links (no LLM), capped under the worker's `limit` so the one-time
24052
+ // re-upload it triggers drains across sweeps instead of spiking.
24053
+ {
24054
+ toVersion: 42,
24055
+ steps: [
24056
+ (0, import_migrations2.addColumns)({
24057
+ table: "memory_vault",
24058
+ columns: [
24059
+ { name: "topics", type: "string", isOptional: true },
24060
+ { name: "topics_updated_at", type: "number", isOptional: true }
24061
+ ]
24062
+ })
24063
+ ]
23750
24064
  }
23751
24065
  ]
23752
24066
  });
@@ -33451,6 +33765,7 @@ function renderAnumaJsx(jsx3) {
33451
33765
  appFileToStored,
33452
33766
  archiveVaultMemoryOp,
33453
33767
  backfillMemoryEntityUserIdsOp,
33768
+ backfillMemoryTopicsOp,
33454
33769
  capHopsForDensity,
33455
33770
  chatStorageMigrations,
33456
33771
  chatStorageSchema,
@@ -33686,6 +34001,7 @@ function renderAnumaJsx(jsx3) {
33686
34001
  refreshDriveToken,
33687
34002
  refreshGithubToken,
33688
34003
  refreshNotionToken,
34004
+ relinkMemoryTopicsOp,
33689
34005
  relinkMisclassifiedVideosOp,
33690
34006
  removeById,
33691
34007
  renderAnumaJsx,