@anuma/sdk 3.12.1 → 3.13.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.
@@ -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);
@@ -17142,7 +17390,6 @@ function createAutoExtractor(options) {
17142
17390
  return true;
17143
17391
  }
17144
17392
  function drainPending() {
17145
- if (disposed) return;
17146
17393
  for (const [conversationId, state] of conversations) {
17147
17394
  if (state.pending) {
17148
17395
  const messages = state.pending;
@@ -17157,7 +17404,7 @@ function createAutoExtractor(options) {
17157
17404
  isProcessing: () => inflight > 0,
17158
17405
  dispose: () => {
17159
17406
  disposed = true;
17160
- for (const state of conversations.values()) state.pending = void 0;
17407
+ if (inflight === 0) drainPending();
17161
17408
  }
17162
17409
  };
17163
17410
  }
@@ -22877,7 +23124,7 @@ __decorateClass([
22877
23124
  ], VaultFolder.prototype, "context", 2);
22878
23125
 
22879
23126
  // src/lib/db/schema.ts
22880
- var SDK_SCHEMA_VERSION = 41;
23127
+ var SDK_SCHEMA_VERSION = 42;
22881
23128
  var sdkSchema = (0, import_watermelondb20.appSchema)({
22882
23129
  version: SDK_SCHEMA_VERSION,
22883
23130
  tables: [
@@ -22999,9 +23246,33 @@ var sdkSchema = (0, import_watermelondb20.appSchema)({
22999
23246
  // links). Auto-extraction then leaves its links alone — the user owns
23000
23247
  // them. Null/false = topics are auto-derived (default).
23001
23248
  { name: "topics_user_managed", type: "boolean", isOptional: true },
23249
+ // The memory's topics as a DURABLE, SYNCED record: a JSON array of
23250
+ // `{name, kind?, source}`, `name` in the caller's display casing (unlike
23251
+ // `entity.canonical_name`, which is lowercased and has no display
23252
+ // column). `entity` / `memory_entity` are a device-local INDEX over this
23253
+ // — their ids are locally generated, so they can never sync — and a
23254
+ // restored device rebuilds them from these names with no LLM call.
23255
+ // Null = predates v42 (backfilled from the row's current links by the
23256
+ // sweep's topicsBackfill bucket). See getMemoriesNeedingTopicExtractionOp.
23257
+ { name: "topics", type: "string", isOptional: true },
23258
+ // Unix ms of the last write to `topics`. A SECOND timestamp is required:
23259
+ // every topic writer deliberately pins `updated_at` so a topic change
23260
+ // doesn't inflate recall's recency multiplier, and both client sync
23261
+ // paths key on `updated_at` — so without this a topic-only change would
23262
+ // neither upload nor merge. Null = `topics` never written.
23263
+ { name: "topics_updated_at", type: "number", isOptional: true },
23002
23264
  // Unix ms of the last LLM topic-extraction pass over this memory's
23003
23265
  // content. Null = never extracted standalone (legacy rows with entity
23004
23266
  // links are grandfathered — see getMemoriesNeedingTopicExtractionOp).
23267
+ //
23268
+ // DEPRECATED (v42) — `topics_updated_at` subsumes this and
23269
+ // `topics_extracted_version` both: null there means never processed, and
23270
+ // non-null with an empty `topics` means processed and found nothing
23271
+ // (today's "answered empty" case), while a release-time
23272
+ // EXTRACTOR_CHANGED_AT constant compared against `topics_updated_at`
23273
+ // replaces the version check. Both columns are kept only to avoid a
23274
+ // column-drop migration in an otherwise additive list; removing them is
23275
+ // a follow-up once `topics` is proven in production.
23005
23276
  { name: "topics_extracted_at", type: "number", isOptional: true },
23006
23277
  // Write-time supersession (A2). When set, this fact was replaced by a
23007
23278
  // newer, incompatible-value fact (e.g. "Lives in Portland" superseded by
@@ -23015,6 +23286,9 @@ var sdkSchema = (0, import_watermelondb20.appSchema)({
23015
23286
  // The extraction-logic version this memory was last stamped under. Null
23016
23287
  // (pre-v38) is treated as version 0, so a bump of TOPICS_EXTRACTION_VERSION
23017
23288
  // re-extracts stale rows. See getMemoriesNeedingTopicExtractionOp.
23289
+ //
23290
+ // DEPRECATED (v42) alongside `topics_extracted_at` — same rationale and
23291
+ // same removal follow-up; see that column's note above.
23018
23292
  { name: "topics_extracted_version", type: "number", isOptional: true },
23019
23293
  // Re-observation watermark (C3). Unix ms of the last time retain() merged
23020
23294
  // a duplicate observation into this fact (proof_count++). Distinct from
@@ -23748,6 +24022,23 @@ var sdkMigrations = (0, import_migrations2.schemaMigrations)({
23748
24022
  ]
23749
24023
  })
23750
24024
  ]
24025
+ },
24026
+ // v41 -> v42: topics + topics_updated_at on memory_vault — the durable,
24027
+ // synced record of a memory's topics. Existing rows keep both NULL: the
24028
+ // sweep's topicsBackfill bucket fills them from each row's current entity
24029
+ // links (no LLM), capped under the worker's `limit` so the one-time
24030
+ // re-upload it triggers drains across sweeps instead of spiking.
24031
+ {
24032
+ toVersion: 42,
24033
+ steps: [
24034
+ (0, import_migrations2.addColumns)({
24035
+ table: "memory_vault",
24036
+ columns: [
24037
+ { name: "topics", type: "string", isOptional: true },
24038
+ { name: "topics_updated_at", type: "number", isOptional: true }
24039
+ ]
24040
+ })
24041
+ ]
23751
24042
  }
23752
24043
  ]
23753
24044
  });
@@ -33452,6 +33743,7 @@ function renderAnumaJsx(jsx3) {
33452
33743
  appFileToStored,
33453
33744
  archiveVaultMemoryOp,
33454
33745
  backfillMemoryEntityUserIdsOp,
33746
+ backfillMemoryTopicsOp,
33455
33747
  capHopsForDensity,
33456
33748
  chatStorageMigrations,
33457
33749
  chatStorageSchema,
@@ -33687,6 +33979,7 @@ function renderAnumaJsx(jsx3) {
33687
33979
  refreshDriveToken,
33688
33980
  refreshGithubToken,
33689
33981
  refreshNotionToken,
33982
+ relinkMemoryTopicsOp,
33690
33983
  relinkMisclassifiedVideosOp,
33691
33984
  removeById,
33692
33985
  renderAnumaJsx,