@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.
@@ -4003,6 +4003,31 @@ var init_endpointOverride = __esm({
4003
4003
  function normalizeEntityName(name) {
4004
4004
  return name.trim().toLowerCase();
4005
4005
  }
4006
+ function serializeTopics(topics) {
4007
+ return JSON.stringify(topics);
4008
+ }
4009
+ function parseTopics(value) {
4010
+ if (typeof value !== "string" || value.length === 0) return null;
4011
+ let parsed;
4012
+ try {
4013
+ parsed = JSON.parse(value);
4014
+ } catch {
4015
+ return null;
4016
+ }
4017
+ if (!Array.isArray(parsed)) return null;
4018
+ const out = [];
4019
+ for (const raw of parsed) {
4020
+ if (typeof raw !== "object" || raw === null) continue;
4021
+ const { name, kind, source } = raw;
4022
+ if (typeof name !== "string" || name.trim().length === 0) continue;
4023
+ out.push({
4024
+ name,
4025
+ ...typeof kind === "string" && kind.length > 0 ? { kind } : {},
4026
+ source: source === "user" ? "user" : "auto"
4027
+ });
4028
+ }
4029
+ return out;
4030
+ }
4006
4031
  var ENTITY_KINDS;
4007
4032
  var init_types3 = __esm({
4008
4033
  "src/lib/db/entities/types.ts"() {
@@ -4068,9 +4093,62 @@ async function upsertEntitiesInWrite(ctx, entities) {
4068
4093
  for (const record of created) out.set(record.canonicalName, entityToStored(record));
4069
4094
  return { entities: out, operations: [...created, ...updated] };
4070
4095
  }
4096
+ async function findVaultRowInWrite(ctx, memoryId) {
4097
+ try {
4098
+ const rows = await ctx.database.get("memory_vault").query(import_watermelondb8.Q.where("id", memoryId)).fetch();
4099
+ return rows[0] ?? null;
4100
+ } catch {
4101
+ return null;
4102
+ }
4103
+ }
4104
+ function prepareTopicsUpdate(row, topics, now) {
4105
+ const originalUpdatedAt = row.updatedAt.getTime();
4106
+ return row.prepareUpdate((r) => {
4107
+ r._setRaw("topics", serializeTopics(topics));
4108
+ r._setRaw("topics_updated_at", now);
4109
+ r._setRaw("updated_at", originalUpdatedAt);
4110
+ });
4111
+ }
4112
+ function displayNamesOf(entities) {
4113
+ const out = /* @__PURE__ */ new Map();
4114
+ for (const e of entities) {
4115
+ const name = e.name.trim();
4116
+ const normalized = normalizeEntityName(name);
4117
+ if (normalized.length === 0 || out.has(normalized)) continue;
4118
+ out.set(normalized, name);
4119
+ }
4120
+ return out;
4121
+ }
4122
+ function topicsEqual(stored, computed) {
4123
+ if (stored === null || stored.length !== computed.length) return false;
4124
+ const key = (t) => JSON.stringify([t.name, t.kind ?? null, t.source]);
4125
+ const storedKeys = stored.map(key).sort();
4126
+ const computedKeys = computed.map(key).sort();
4127
+ return storedKeys.every((k, i) => k === computedKeys[i]);
4128
+ }
4129
+ function resolveTopicsWrite(row, linked, displayNames, source) {
4130
+ if (row === null || source === null) return null;
4131
+ const topics = topicsForEntities(linked, displayNames, source);
4132
+ if (topicsEqual(parseTopics(row.topics), topics)) return null;
4133
+ return { row, topics };
4134
+ }
4135
+ function topicsForEntities(entities, displayNames, source) {
4136
+ return entities.map((e) => {
4137
+ const name = displayNames.get(e.canonicalName) ?? e.canonicalName;
4138
+ return { name, ...e.kind ? { kind: e.kind } : {}, source };
4139
+ });
4140
+ }
4141
+ function toEntityObjects(entityInputs) {
4142
+ return entityInputs.map((e) => typeof e === "string" ? { name: e } : e);
4143
+ }
4144
+ async function prepareMemoryTopicsUpdate(ctx, memoryId, linked, inputs, source) {
4145
+ const row = await findVaultRowInWrite(ctx, memoryId);
4146
+ const write = resolveTopicsWrite(row, linked, displayNamesOf(toEntityObjects(inputs)), source);
4147
+ return write === null ? null : prepareTopicsUpdate(write.row, write.topics, Date.now());
4148
+ }
4071
4149
  async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
4072
4150
  if (entityInputs.length === 0) return [];
4073
- const normalized = entityInputs.map((e) => typeof e === "string" ? { name: e } : e);
4151
+ const normalized = toEntityObjects(entityInputs);
4074
4152
  const userId = ctx.userId;
4075
4153
  let skipped = false;
4076
4154
  let entities = [];
@@ -4080,7 +4158,8 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
4080
4158
  normalized
4081
4159
  );
4082
4160
  entities = Array.from(byName.values());
4083
- if (options?.unlessTopicsUserManaged && await autoLinkBlocked(ctx, memoryId)) {
4161
+ const vaultRow = await findVaultRowInWrite(ctx, memoryId);
4162
+ if (options?.unlessTopicsUserManaged && autoLinkBlocked(vaultRow)) {
4084
4163
  if (entityOps.length > 0) {
4085
4164
  await ctx.database.batch(...entityOps);
4086
4165
  }
@@ -4091,7 +4170,18 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
4091
4170
  const existingLinks = await ctx.memoryEntityCollection.query(import_watermelondb8.Q.where("memory_id", memoryId)).fetch();
4092
4171
  const existingEntityIds = new Set(existingLinks.map((l) => String(l.entityId)));
4093
4172
  const toCreate = entities.filter((e) => !existingEntityIds.has(e.uniqueId));
4094
- if (entityOps.length === 0 && toCreate.length === 0) return;
4173
+ const keptIds = entities.map((e) => e.uniqueId);
4174
+ const carriedOver = await resolveEntitiesByIds(
4175
+ ctx,
4176
+ [...existingEntityIds].filter((id) => !keptIds.includes(id))
4177
+ );
4178
+ const topicsWrite = resolveTopicsWrite(
4179
+ vaultRow,
4180
+ [...carriedOver, ...entities],
4181
+ displayNamesOf(normalized),
4182
+ options?.topicsSource ?? "auto"
4183
+ );
4184
+ if (entityOps.length === 0 && toCreate.length === 0 && topicsWrite === null) return;
4095
4185
  const linkOps = toCreate.map(
4096
4186
  (e) => ctx.memoryEntityCollection.prepareCreate((record) => {
4097
4187
  record._setRaw("memory_id", memoryId);
@@ -4099,26 +4189,42 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
4099
4189
  if (userId !== void 0) record._setRaw("user_id", userId);
4100
4190
  })
4101
4191
  );
4102
- await ctx.database.batch(...entityOps, ...linkOps);
4192
+ await ctx.database.batch(
4193
+ ...entityOps,
4194
+ ...linkOps,
4195
+ ...topicsWrite ? [prepareTopicsUpdate(topicsWrite.row, topicsWrite.topics, Date.now())] : []
4196
+ );
4103
4197
  });
4104
4198
  return skipped ? [] : entities;
4105
4199
  }
4106
- async function autoLinkBlocked(ctx, memoryId) {
4107
- try {
4108
- const rows = await ctx.database.get("memory_vault").query(import_watermelondb8.Q.where("id", memoryId)).fetch();
4109
- const row = rows[0];
4110
- return !row || !!row.isDeleted || !!row.topicsUserManaged;
4111
- } catch {
4112
- return true;
4113
- }
4200
+ async function resolveEntitiesByIds(ctx, entityIds) {
4201
+ if (entityIds.length === 0) return [];
4202
+ const rows = await ctx.entityCollection.query(import_watermelondb8.Q.where("id", import_watermelondb8.Q.oneOf(Array.from(entityIds)))).fetch();
4203
+ return rows.map(entityToStored);
4204
+ }
4205
+ function autoLinkBlocked(row) {
4206
+ return !row || !!row.isDeleted || !!row.topicsUserManaged;
4114
4207
  }
4115
4208
  async function replaceMemoryEntitiesGuardedOp(ctx, memoryId, entityInputs) {
4116
- const normalized = entityInputs.map((e) => typeof e === "string" ? { name: e } : e);
4209
+ return replaceMemoryEntities(ctx, memoryId, entityInputs, {
4210
+ guarded: true,
4211
+ topicsSource: "auto"
4212
+ });
4213
+ }
4214
+ async function relinkMemoryEntitiesFromTopicsOp(ctx, memoryId, topics) {
4215
+ return replaceMemoryEntities(ctx, memoryId, topics, {
4216
+ guarded: false,
4217
+ topicsSource: null
4218
+ });
4219
+ }
4220
+ async function replaceMemoryEntities(ctx, memoryId, entityInputs, options) {
4221
+ const normalized = toEntityObjects(entityInputs);
4117
4222
  const userId = ctx.userId;
4118
4223
  let skipped = false;
4119
4224
  let entities = [];
4120
4225
  await ctx.database.write(async () => {
4121
- if (await autoLinkBlocked(ctx, memoryId)) {
4226
+ const vaultRow = await findVaultRowInWrite(ctx, memoryId);
4227
+ if (options.guarded && autoLinkBlocked(vaultRow)) {
4122
4228
  skipped = true;
4123
4229
  return;
4124
4230
  }
@@ -4132,7 +4238,15 @@ async function replaceMemoryEntitiesGuardedOp(ctx, memoryId, entityInputs) {
4132
4238
  const existingEntityIds = new Set(existingLinks.map((l) => String(l.entityId)));
4133
4239
  const toCreate = entities.filter((e) => !existingEntityIds.has(e.uniqueId));
4134
4240
  const toDestroy = existingLinks.filter((l) => !keep.has(String(l.entityId)));
4135
- if (entityOps.length === 0 && toCreate.length === 0 && toDestroy.length === 0) return;
4241
+ const topicsWrite = resolveTopicsWrite(
4242
+ vaultRow,
4243
+ entities,
4244
+ displayNamesOf(normalized),
4245
+ options.topicsSource
4246
+ );
4247
+ if (entityOps.length === 0 && toCreate.length === 0 && toDestroy.length === 0 && topicsWrite === null) {
4248
+ return;
4249
+ }
4136
4250
  const orphans = await findOrphanedEntities(ctx, memoryId, toDestroy);
4137
4251
  await ctx.database.batch(
4138
4252
  ...entityOps,
@@ -4144,7 +4258,8 @@ async function replaceMemoryEntitiesGuardedOp(ctx, memoryId, entityInputs) {
4144
4258
  })
4145
4259
  ),
4146
4260
  ...toDestroy.map((l) => l.prepareDestroyPermanently()),
4147
- ...orphans.map((e) => e.prepareDestroyPermanently())
4261
+ ...orphans.map((e) => e.prepareDestroyPermanently()),
4262
+ ...topicsWrite ? [prepareTopicsUpdate(topicsWrite.row, topicsWrite.topics, Date.now())] : []
4148
4263
  );
4149
4264
  });
4150
4265
  return skipped ? null : entities;
@@ -4343,6 +4458,8 @@ function vaultMemoryToStoredRaw(memory) {
4343
4458
  eventTimeEnd: memory.eventTimeEnd ?? null,
4344
4459
  eventTimeKind: memory.eventTimeKind ?? null,
4345
4460
  topicsUserManaged: memory.topicsUserManaged ?? false,
4461
+ topics: parseTopics(memory.topics),
4462
+ topicsUpdatedAt: memory.topicsUpdatedAt ?? null,
4346
4463
  topicsExtractedAt: memory.topicsExtractedAt ?? null,
4347
4464
  topicsExtractedVersion: memory.topicsExtractedVersion ?? null,
4348
4465
  supersededBy: memory.supersededBy ?? null,
@@ -4619,6 +4736,8 @@ function vaultMemoryRawToStoredRaw(raw) {
4619
4736
  eventTimeKind: raw.event_time_kind ?? null,
4620
4737
  // SQLite stores booleans as 0/1, LokiJS as true/false — coerce both.
4621
4738
  topicsUserManaged: raw.topics_user_managed === true || raw.topics_user_managed === 1,
4739
+ topics: parseTopics(raw.topics),
4740
+ topicsUpdatedAt: raw.topics_updated_at ?? null,
4622
4741
  topicsExtractedAt: raw.topics_extracted_at ?? null,
4623
4742
  topicsExtractedVersion: raw.topics_extracted_version ?? null,
4624
4743
  supersededBy: raw.superseded_by ?? null,
@@ -4965,56 +5084,111 @@ async function getUnfiledVaultMemoriesOp(ctx) {
4965
5084
  (record) => vaultMemoryToStored(record, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner)
4966
5085
  );
4967
5086
  }
5087
+ async function linkedEntityNamesByMemory(entityCtx, memoryIds) {
5088
+ const CHUNK = 500;
5089
+ const entityIdByMemory = /* @__PURE__ */ new Map();
5090
+ const allEntityIds = /* @__PURE__ */ new Set();
5091
+ for (let i = 0; i < memoryIds.length; i += CHUNK) {
5092
+ const links = await entityCtx.memoryEntityCollection.query(import_watermelondb9.Q.where("memory_id", import_watermelondb9.Q.oneOf(memoryIds.slice(i, i + CHUNK)))).unsafeFetchRaw();
5093
+ for (const link of links) {
5094
+ const memoryId = String(link.memory_id);
5095
+ const entityId = String(link.entity_id);
5096
+ allEntityIds.add(entityId);
5097
+ let bucket = entityIdByMemory.get(memoryId);
5098
+ if (!bucket) {
5099
+ bucket = /* @__PURE__ */ new Set();
5100
+ entityIdByMemory.set(memoryId, bucket);
5101
+ }
5102
+ bucket.add(entityId);
5103
+ }
5104
+ }
5105
+ const entityIds = Array.from(allEntityIds);
5106
+ const nameById = /* @__PURE__ */ new Map();
5107
+ for (let i = 0; i < entityIds.length; i += CHUNK) {
5108
+ const entities = await entityCtx.entityCollection.query(import_watermelondb9.Q.where("id", import_watermelondb9.Q.oneOf(entityIds.slice(i, i + CHUNK)))).unsafeFetchRaw();
5109
+ for (const e of entities) {
5110
+ if (typeof e.canonical_name === "string") nameById.set(String(e.id), e.canonical_name);
5111
+ }
5112
+ }
5113
+ const out = /* @__PURE__ */ new Map();
5114
+ for (const [memoryId, ids] of entityIdByMemory) {
5115
+ const names = /* @__PURE__ */ new Set();
5116
+ for (const id of ids) {
5117
+ const name = nameById.get(id);
5118
+ if (name !== void 0) names.add(name);
5119
+ }
5120
+ if (names.size > 0) out.set(memoryId, names);
5121
+ }
5122
+ return out;
5123
+ }
5124
+ function linksDivergeFromTopics(topics, linked) {
5125
+ const wanted = new Set(
5126
+ topics.map((t) => normalizeEntityName(t.name)).filter((n) => n.length > 0)
5127
+ );
5128
+ if (wanted.size !== linked.size) return true;
5129
+ for (const name of wanted) if (!linked.has(name)) return true;
5130
+ return false;
5131
+ }
4968
5132
  async function getMemoriesNeedingTopicExtractionOp(ctx, options) {
4969
5133
  const entityCtx = ctx.entityCtx;
4970
5134
  if (!entityCtx) {
4971
5135
  throw new Error("getMemoriesNeedingTopicExtractionOp requires ctx.entityCtx");
4972
5136
  }
4973
- const conditions = [
4974
- // Boolean optional column: null (legacy) and false both mean auto-managed.
4975
- // Q.notEq(true) is a NULL trap on SQLite (NULL <> TRUE is NULL) — enumerate.
4976
- import_watermelondb9.Q.or(import_watermelondb9.Q.where("topics_user_managed", null), import_watermelondb9.Q.where("topics_user_managed", false)),
4977
- ...baseVaultConditions(ctx),
4978
- import_watermelondb9.Q.sortBy("created_at", import_watermelondb9.Q.desc)
4979
- ];
5137
+ const conditions = [...baseVaultConditions(ctx), import_watermelondb9.Q.sortBy("created_at", import_watermelondb9.Q.desc)];
4980
5138
  const rows = await ctx.vaultMemoryCollection.query(...conditions).unsafeFetchRaw();
4981
- const stampedPending = [];
4982
- const unstamped = [];
5139
+ const linkedNames = await linkedEntityNamesByMemory(
5140
+ entityCtx,
5141
+ rows.map((r) => r.id)
5142
+ );
5143
+ const pendingRaw = [];
5144
+ const stampedPendingRaw = [];
5145
+ const linkedUnstampedAll = [];
5146
+ const topicsToRelinkAll = [];
5147
+ const topicsBackfillAll = [];
4983
5148
  for (const raw of rows) {
5149
+ const id = raw.id;
5150
+ const topics = parseTopics(raw.topics);
5151
+ const linked = linkedNames.get(id);
5152
+ if (topics !== null && topics.length > 0 && linksDivergeFromTopics(topics, linked ?? /* @__PURE__ */ new Set())) {
5153
+ topicsToRelinkAll.push(id);
5154
+ continue;
5155
+ }
5156
+ if (raw.topics_user_managed) {
5157
+ if (topics === null && linked !== void 0) topicsBackfillAll.push(id);
5158
+ continue;
5159
+ }
4984
5160
  const stamp = raw.topics_extracted_at ?? null;
5161
+ let isPending = false;
4985
5162
  if (stamp !== null) {
4986
5163
  const version = raw.topics_extracted_version ?? 0;
4987
5164
  if (raw.updated_at > stamp || version < TOPICS_EXTRACTION_VERSION) {
4988
- stampedPending.push(raw);
5165
+ stampedPendingRaw.push(raw);
5166
+ isPending = true;
4989
5167
  }
4990
- } else {
4991
- unstamped.push(raw);
4992
- }
4993
- }
4994
- const linkedIds = /* @__PURE__ */ new Set();
4995
- const CHUNK = 500;
4996
- for (let i = 0; i < unstamped.length; i += CHUNK) {
4997
- const ids = unstamped.slice(i, i + CHUNK).map((r) => r.id);
4998
- const links = await entityCtx.memoryEntityCollection.query(import_watermelondb9.Q.where("memory_id", import_watermelondb9.Q.oneOf(ids))).unsafeFetchRaw();
4999
- for (const link of links) linkedIds.add(String(link.memory_id));
5000
- }
5001
- const linkedUnstampedAll = [];
5002
- const pendingRaw = [...stampedPending];
5003
- for (const raw of unstamped) {
5004
- if (linkedIds.has(raw.id)) {
5005
- linkedUnstampedAll.push(raw.id);
5168
+ } else if (linked !== void 0) {
5169
+ linkedUnstampedAll.push(id);
5006
5170
  } else {
5007
5171
  pendingRaw.push(raw);
5172
+ isPending = true;
5173
+ }
5174
+ if (!isPending && topics === null && linked !== void 0) {
5175
+ topicsBackfillAll.push(id);
5008
5176
  }
5009
5177
  }
5010
5178
  const cap = options?.limit !== void 0 && options.limit > 0 ? options.limit : void 0;
5011
- const limitedPendingRaw = cap !== void 0 ? pendingRaw.slice(0, cap) : pendingRaw;
5012
- const linkedUnstamped = cap !== void 0 ? linkedUnstampedAll.slice(0, cap) : linkedUnstampedAll;
5179
+ const orderedPendingRaw = [...stampedPendingRaw, ...pendingRaw];
5180
+ const limitedPendingRaw = cap !== void 0 ? orderedPendingRaw.slice(0, cap) : orderedPendingRaw;
5013
5181
  const pending = await mapInBatches(
5014
5182
  limitedPendingRaw,
5015
5183
  (raw) => vaultMemoryRawToStored(raw, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner)
5016
5184
  );
5017
- return { pending, linkedUnstamped };
5185
+ const applyCap = (ids) => cap !== void 0 ? ids.slice(0, cap) : ids;
5186
+ return {
5187
+ pending,
5188
+ linkedUnstamped: applyCap(linkedUnstampedAll),
5189
+ topicsToRelink: applyCap(topicsToRelinkAll),
5190
+ topicsBackfill: applyCap(topicsBackfillAll)
5191
+ };
5018
5192
  }
5019
5193
  async function stampTopicsExtractedAtOp(ctx, memoryIds, extractedAt, version = TOPICS_EXTRACTION_VERSION) {
5020
5194
  if (memoryIds.length === 0) return [];
@@ -5048,6 +5222,61 @@ async function stampTopicsExtractedAtOp(ctx, memoryIds, extractedAt, version = T
5048
5222
  }
5049
5223
  return stamped;
5050
5224
  }
5225
+ async function relinkMemoryTopicsOp(ctx, memoryIds) {
5226
+ const entityCtx = ctx.entityCtx;
5227
+ if (!entityCtx) {
5228
+ throw new Error("relinkMemoryTopicsOp requires ctx.entityCtx");
5229
+ }
5230
+ const relinked = [];
5231
+ for (const id of Array.from(new Set(memoryIds))) {
5232
+ let record;
5233
+ try {
5234
+ record = await ctx.vaultMemoryCollection.find(id);
5235
+ } catch {
5236
+ continue;
5237
+ }
5238
+ if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) continue;
5239
+ const topics = parseTopics(record.topics);
5240
+ if (topics === null || topics.length === 0) continue;
5241
+ try {
5242
+ await relinkMemoryEntitiesFromTopicsOp(entityCtx, id, topics);
5243
+ relinked.push(id);
5244
+ } catch (err) {
5245
+ getLogger().warn("[memory/topics] relink failed", err);
5246
+ }
5247
+ }
5248
+ return relinked;
5249
+ }
5250
+ async function backfillMemoryTopicsOp(ctx, memoryIds) {
5251
+ const entityCtx = ctx.entityCtx;
5252
+ if (!entityCtx) {
5253
+ throw new Error("backfillMemoryTopicsOp requires ctx.entityCtx");
5254
+ }
5255
+ const filled = [];
5256
+ for (const id of Array.from(new Set(memoryIds))) {
5257
+ let record;
5258
+ try {
5259
+ record = await ctx.vaultMemoryCollection.find(id);
5260
+ } catch {
5261
+ continue;
5262
+ }
5263
+ if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) continue;
5264
+ if (parseTopics(record.topics) !== null) continue;
5265
+ const links = await entityCtx.memoryEntityCollection.query(import_watermelondb9.Q.where("memory_id", id)).unsafeFetchRaw();
5266
+ const entityIds = links.map((l) => String(l.entity_id));
5267
+ if (entityIds.length === 0) continue;
5268
+ const entities = await entityCtx.entityCollection.query(import_watermelondb9.Q.where("id", import_watermelondb9.Q.oneOf(entityIds))).fetch();
5269
+ if (entities.length === 0) continue;
5270
+ const source = record.topicsUserManaged ? "user" : "auto";
5271
+ await ctx.database.write(async () => {
5272
+ const topicsOp = await prepareMemoryTopicsUpdate(entityCtx, id, entities, [], source);
5273
+ if (!topicsOp) return;
5274
+ await ctx.database.batch(topicsOp);
5275
+ filled.push(id);
5276
+ });
5277
+ }
5278
+ return filled;
5279
+ }
5051
5280
  async function deleteAllVaultMemoriesForUserOp(ctx, userId) {
5052
5281
  if (ctx.userId !== void 0 && ctx.userId !== userId) return 0;
5053
5282
  const records = await ctx.vaultMemoryCollection.query(import_watermelondb9.Q.where("user_id", userId), import_watermelondb9.Q.where("is_deleted", false)).fetch();
@@ -5189,6 +5418,7 @@ var init_operations3 = __esm({
5189
5418
  import_watermelondb9 = require("@nozbe/watermelondb");
5190
5419
  init_logger();
5191
5420
  init_operations2();
5421
+ init_types3();
5192
5422
  init_encryption2();
5193
5423
  NON_PRIVATE_VISIBILITIES = ["public"];
5194
5424
  KNOWN_TRUST_TIERS = /* @__PURE__ */ new Set(["quarantined", "trusted"]);
@@ -8385,6 +8615,7 @@ __export(index_exports, {
8385
8615
  WalletPoller: () => WalletPoller,
8386
8616
  addConversationMemoriesOp: () => addConversationMemoriesOp,
8387
8617
  archiveVaultMemoryOp: () => archiveVaultMemoryOp,
8618
+ backfillMemoryTopicsOp: () => backfillMemoryTopicsOp,
8388
8619
  buildNotionAuthUrl: () => buildNotionAuthUrl,
8389
8620
  capHopsForDensity: () => capHopsForDensity,
8390
8621
  chatStorageMigrations: () => chatStorageMigrations,
@@ -8482,6 +8713,7 @@ __export(index_exports, {
8482
8713
  reflect: () => reflect,
8483
8714
  refreshNotionAccessToken: () => refreshNotionAccessToken,
8484
8715
  registerNotionClient: () => registerNotionClient,
8716
+ relinkMemoryTopicsOp: () => relinkMemoryTopicsOp,
8485
8717
  requestEncryptionKey: () => requestEncryptionKey,
8486
8718
  resolvePiiRedactor: () => resolvePiiRedactor,
8487
8719
  restoreVaultMemoryOp: () => restoreVaultMemoryOp,
@@ -14027,6 +14259,12 @@ __decorateClass([
14027
14259
  __decorateClass([
14028
14260
  (0, import_decorators3.field)("topics_user_managed")
14029
14261
  ], VaultMemory.prototype, "topicsUserManaged", 2);
14262
+ __decorateClass([
14263
+ (0, import_decorators3.field)("topics")
14264
+ ], VaultMemory.prototype, "topics", 2);
14265
+ __decorateClass([
14266
+ (0, import_decorators3.field)("topics_updated_at")
14267
+ ], VaultMemory.prototype, "topicsUpdatedAt", 2);
14030
14268
  __decorateClass([
14031
14269
  (0, import_decorators3.field)("topics_extracted_at")
14032
14270
  ], VaultMemory.prototype, "topicsExtractedAt", 2);
@@ -19350,6 +19588,31 @@ function useChatStorage(options) {
19350
19588
  };
19351
19589
  }
19352
19590
  const { callRedactor, callPiiRedaction, maskForCall } = resolvePiiForCall(convId);
19591
+ const effectiveApiType = resolveApiType(requestApiType ?? apiType ?? "auto", model);
19592
+ const isServerToolsFunction = typeof serverToolsFilter === "function";
19593
+ const fetchServerToolsSettled = async (token) => {
19594
+ try {
19595
+ return {
19596
+ tools: await getServerTools({
19597
+ baseUrl,
19598
+ cacheExpirationMs: serverToolsConfig?.cacheExpirationMs,
19599
+ getToken: token,
19600
+ cache: serverToolsConfig?.cache
19601
+ })
19602
+ };
19603
+ } catch (error) {
19604
+ return { error };
19605
+ }
19606
+ };
19607
+ const embedForToolsSettled = async (token) => {
19608
+ try {
19609
+ return { embedding: await embedToolText(contentForStorage, maskForCall, token) };
19610
+ } catch (error) {
19611
+ return { error };
19612
+ }
19613
+ };
19614
+ const serverToolsPromise = getTokenRef.current && !(Array.isArray(serverToolsFilter) && serverToolsFilter.length === 0) ? fetchServerToolsSettled(getTokenRef.current) : null;
19615
+ const userMessageEmbeddingPromise = getTokenRef.current && contentForStorage.length >= MIN_CONTENT_LENGTH_FOR_TOOLS && (isServerToolsFunction && !(serverToolsConfig?.deferLoading?.enabled && effectiveApiType === "responses") || !!clientTools?.length) ? embedForToolsSettled(getTokenRef.current) : null;
19353
19616
  let messagesToSend;
19354
19617
  const storedMessages = await getMessages(convId);
19355
19618
  const knownToolCallEventIds = /* @__PURE__ */ new Set();
@@ -19424,29 +19687,26 @@ function useChatStorage(options) {
19424
19687
  };
19425
19688
  }
19426
19689
  const startTime = Date.now();
19427
- const effectiveApiType = resolveApiType(requestApiType ?? apiType ?? "auto", model);
19428
19690
  let mergedTools = clientTools;
19429
19691
  let userMessageEmbedding;
19430
19692
  let userMessageEmbeddingFailed = false;
19693
+ let serverFilterEmbeddingFailed = false;
19431
19694
  let filteredServerTools = [];
19432
- const isServerToolsFunction = typeof serverToolsFilter === "function";
19433
19695
  if (getTokenRef.current && !(Array.isArray(serverToolsFilter) && serverToolsFilter.length === 0)) {
19434
19696
  try {
19435
- const allServerTools = await getServerTools({
19436
- baseUrl,
19437
- cacheExpirationMs: serverToolsConfig?.cacheExpirationMs,
19438
- getToken: getTokenRef.current,
19439
- cache: serverToolsConfig?.cache
19440
- });
19697
+ const settledTools = await (serverToolsPromise ?? fetchServerToolsSettled(getTokenRef.current));
19698
+ if ("error" in settledTools) throw settledTools.error;
19699
+ const allServerTools = settledTools.tools;
19441
19700
  if (serverToolsConfig?.deferLoading?.enabled && effectiveApiType === "responses") {
19442
19701
  filteredServerTools = allServerTools;
19443
19702
  } else if (isServerToolsFunction) {
19444
19703
  if (contentForStorage.length >= MIN_CONTENT_LENGTH_FOR_TOOLS) {
19445
- userMessageEmbedding = await embedToolText(
19446
- contentForStorage,
19447
- maskForCall,
19448
- getTokenRef.current
19449
- );
19704
+ const settledEmbedding = await (userMessageEmbeddingPromise ?? embedForToolsSettled(getTokenRef.current));
19705
+ if ("error" in settledEmbedding) {
19706
+ serverFilterEmbeddingFailed = true;
19707
+ throw settledEmbedding.error;
19708
+ }
19709
+ userMessageEmbedding = settledEmbedding.embedding;
19450
19710
  const toolNames = serverToolsFilter(userMessageEmbedding, allServerTools);
19451
19711
  filteredServerTools = filterServerTools(allServerTools, toolNames);
19452
19712
  }
@@ -19470,14 +19730,11 @@ function useChatStorage(options) {
19470
19730
  if (clientTools?.length) {
19471
19731
  try {
19472
19732
  if (!userMessageEmbedding && getTokenRef.current && contentForStorage.length >= MIN_CONTENT_LENGTH_FOR_TOOLS) {
19473
- try {
19474
- userMessageEmbedding = await embedToolText(
19475
- contentForStorage,
19476
- maskForCall,
19477
- getTokenRef.current
19478
- );
19479
- } catch {
19733
+ const settledEmbedding = !userMessageEmbeddingPromise || serverFilterEmbeddingFailed ? await embedForToolsSettled(getTokenRef.current) : await userMessageEmbeddingPromise;
19734
+ if ("error" in settledEmbedding) {
19480
19735
  userMessageEmbeddingFailed = true;
19736
+ } else {
19737
+ userMessageEmbedding = settledEmbedding.embedding;
19481
19738
  }
19482
19739
  }
19483
19740
  if (typeof clientToolsFilter === "function") {
@@ -20294,7 +20551,7 @@ __decorateClass([
20294
20551
  ], VaultFolder.prototype, "context", 2);
20295
20552
 
20296
20553
  // src/lib/db/schema.ts
20297
- var SDK_SCHEMA_VERSION = 41;
20554
+ var SDK_SCHEMA_VERSION = 42;
20298
20555
  var sdkSchema = (0, import_watermelondb19.appSchema)({
20299
20556
  version: SDK_SCHEMA_VERSION,
20300
20557
  tables: [
@@ -20416,9 +20673,33 @@ var sdkSchema = (0, import_watermelondb19.appSchema)({
20416
20673
  // links). Auto-extraction then leaves its links alone — the user owns
20417
20674
  // them. Null/false = topics are auto-derived (default).
20418
20675
  { name: "topics_user_managed", type: "boolean", isOptional: true },
20676
+ // The memory's topics as a DURABLE, SYNCED record: a JSON array of
20677
+ // `{name, kind?, source}`, `name` in the caller's display casing (unlike
20678
+ // `entity.canonical_name`, which is lowercased and has no display
20679
+ // column). `entity` / `memory_entity` are a device-local INDEX over this
20680
+ // — their ids are locally generated, so they can never sync — and a
20681
+ // restored device rebuilds them from these names with no LLM call.
20682
+ // Null = predates v42 (backfilled from the row's current links by the
20683
+ // sweep's topicsBackfill bucket). See getMemoriesNeedingTopicExtractionOp.
20684
+ { name: "topics", type: "string", isOptional: true },
20685
+ // Unix ms of the last write to `topics`. A SECOND timestamp is required:
20686
+ // every topic writer deliberately pins `updated_at` so a topic change
20687
+ // doesn't inflate recall's recency multiplier, and both client sync
20688
+ // paths key on `updated_at` — so without this a topic-only change would
20689
+ // neither upload nor merge. Null = `topics` never written.
20690
+ { name: "topics_updated_at", type: "number", isOptional: true },
20419
20691
  // Unix ms of the last LLM topic-extraction pass over this memory's
20420
20692
  // content. Null = never extracted standalone (legacy rows with entity
20421
20693
  // links are grandfathered — see getMemoriesNeedingTopicExtractionOp).
20694
+ //
20695
+ // DEPRECATED (v42) — `topics_updated_at` subsumes this and
20696
+ // `topics_extracted_version` both: null there means never processed, and
20697
+ // non-null with an empty `topics` means processed and found nothing
20698
+ // (today's "answered empty" case), while a release-time
20699
+ // EXTRACTOR_CHANGED_AT constant compared against `topics_updated_at`
20700
+ // replaces the version check. Both columns are kept only to avoid a
20701
+ // column-drop migration in an otherwise additive list; removing them is
20702
+ // a follow-up once `topics` is proven in production.
20422
20703
  { name: "topics_extracted_at", type: "number", isOptional: true },
20423
20704
  // Write-time supersession (A2). When set, this fact was replaced by a
20424
20705
  // newer, incompatible-value fact (e.g. "Lives in Portland" superseded by
@@ -20432,6 +20713,9 @@ var sdkSchema = (0, import_watermelondb19.appSchema)({
20432
20713
  // The extraction-logic version this memory was last stamped under. Null
20433
20714
  // (pre-v38) is treated as version 0, so a bump of TOPICS_EXTRACTION_VERSION
20434
20715
  // re-extracts stale rows. See getMemoriesNeedingTopicExtractionOp.
20716
+ //
20717
+ // DEPRECATED (v42) alongside `topics_extracted_at` — same rationale and
20718
+ // same removal follow-up; see that column's note above.
20435
20719
  { name: "topics_extracted_version", type: "number", isOptional: true },
20436
20720
  // Re-observation watermark (C3). Unix ms of the last time retain() merged
20437
20721
  // a duplicate observation into this fact (proof_count++). Distinct from
@@ -21165,6 +21449,23 @@ var sdkMigrations = (0, import_migrations2.schemaMigrations)({
21165
21449
  ]
21166
21450
  })
21167
21451
  ]
21452
+ },
21453
+ // v41 -> v42: topics + topics_updated_at on memory_vault — the durable,
21454
+ // synced record of a memory's topics. Existing rows keep both NULL: the
21455
+ // sweep's topicsBackfill bucket fills them from each row's current entity
21456
+ // links (no LLM), capped under the worker's `limit` so the one-time
21457
+ // re-upload it triggers drains across sweeps instead of spiking.
21458
+ {
21459
+ toVersion: 42,
21460
+ steps: [
21461
+ (0, import_migrations2.addColumns)({
21462
+ table: "memory_vault",
21463
+ columns: [
21464
+ { name: "topics", type: "string", isOptional: true },
21465
+ { name: "topics_updated_at", type: "number", isOptional: true }
21466
+ ]
21467
+ })
21468
+ ]
21168
21469
  }
21169
21470
  ]
21170
21471
  });
@@ -22550,6 +22851,7 @@ function createNotionTools(getAccessToken, requestNotionAccess) {
22550
22851
  WalletPoller,
22551
22852
  addConversationMemoriesOp,
22552
22853
  archiveVaultMemoryOp,
22854
+ backfillMemoryTopicsOp,
22553
22855
  buildNotionAuthUrl,
22554
22856
  capHopsForDensity,
22555
22857
  chatStorageMigrations,
@@ -22647,6 +22949,7 @@ function createNotionTools(getAccessToken, requestNotionAccess) {
22647
22949
  reflect,
22648
22950
  refreshNotionAccessToken,
22649
22951
  registerNotionClient,
22952
+ relinkMemoryTopicsOp,
22650
22953
  requestEncryptionKey,
22651
22954
  resolvePiiRedactor,
22652
22955
  restoreVaultMemoryOp,