@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.
@@ -16770,6 +16770,31 @@ var init_clientConfig = __esm({
16770
16770
  function normalizeEntityName(name) {
16771
16771
  return name.trim().toLowerCase();
16772
16772
  }
16773
+ function serializeTopics(topics) {
16774
+ return JSON.stringify(topics);
16775
+ }
16776
+ function parseTopics(value) {
16777
+ if (typeof value !== "string" || value.length === 0) return null;
16778
+ let parsed;
16779
+ try {
16780
+ parsed = JSON.parse(value);
16781
+ } catch {
16782
+ return null;
16783
+ }
16784
+ if (!Array.isArray(parsed)) return null;
16785
+ const out = [];
16786
+ for (const raw of parsed) {
16787
+ if (typeof raw !== "object" || raw === null) continue;
16788
+ const { name, kind, source } = raw;
16789
+ if (typeof name !== "string" || name.trim().length === 0) continue;
16790
+ out.push({
16791
+ name,
16792
+ ...typeof kind === "string" && kind.length > 0 ? { kind } : {},
16793
+ source: source === "user" ? "user" : "auto"
16794
+ });
16795
+ }
16796
+ return out;
16797
+ }
16773
16798
  var ENTITY_KINDS;
16774
16799
  var init_types3 = __esm({
16775
16800
  "src/lib/db/entities/types.ts"() {
@@ -16835,9 +16860,62 @@ async function upsertEntitiesInWrite(ctx, entities) {
16835
16860
  for (const record of created) out.set(record.canonicalName, entityToStored(record));
16836
16861
  return { entities: out, operations: [...created, ...updated] };
16837
16862
  }
16863
+ async function findVaultRowInWrite(ctx, memoryId) {
16864
+ try {
16865
+ const rows = await ctx.database.get("memory_vault").query(import_watermelondb16.Q.where("id", memoryId)).fetch();
16866
+ return rows[0] ?? null;
16867
+ } catch {
16868
+ return null;
16869
+ }
16870
+ }
16871
+ function prepareTopicsUpdate(row, topics, now) {
16872
+ const originalUpdatedAt = row.updatedAt.getTime();
16873
+ return row.prepareUpdate((r) => {
16874
+ r._setRaw("topics", serializeTopics(topics));
16875
+ r._setRaw("topics_updated_at", now);
16876
+ r._setRaw("updated_at", originalUpdatedAt);
16877
+ });
16878
+ }
16879
+ function displayNamesOf(entities) {
16880
+ const out = /* @__PURE__ */ new Map();
16881
+ for (const e of entities) {
16882
+ const name = e.name.trim();
16883
+ const normalized = normalizeEntityName(name);
16884
+ if (normalized.length === 0 || out.has(normalized)) continue;
16885
+ out.set(normalized, name);
16886
+ }
16887
+ return out;
16888
+ }
16889
+ function topicsEqual(stored, computed) {
16890
+ if (stored === null || stored.length !== computed.length) return false;
16891
+ const key = (t) => JSON.stringify([t.name, t.kind ?? null, t.source]);
16892
+ const storedKeys = stored.map(key).sort();
16893
+ const computedKeys = computed.map(key).sort();
16894
+ return storedKeys.every((k, i) => k === computedKeys[i]);
16895
+ }
16896
+ function resolveTopicsWrite(row, linked, displayNames, source) {
16897
+ if (row === null || source === null) return null;
16898
+ const topics = topicsForEntities(linked, displayNames, source);
16899
+ if (topicsEqual(parseTopics(row.topics), topics)) return null;
16900
+ return { row, topics };
16901
+ }
16902
+ function topicsForEntities(entities, displayNames, source) {
16903
+ return entities.map((e) => {
16904
+ const name = displayNames.get(e.canonicalName) ?? e.canonicalName;
16905
+ return { name, ...e.kind ? { kind: e.kind } : {}, source };
16906
+ });
16907
+ }
16908
+ function toEntityObjects(entityInputs) {
16909
+ return entityInputs.map((e) => typeof e === "string" ? { name: e } : e);
16910
+ }
16911
+ async function prepareMemoryTopicsUpdate(ctx, memoryId, linked, inputs, source) {
16912
+ const row = await findVaultRowInWrite(ctx, memoryId);
16913
+ const write = resolveTopicsWrite(row, linked, displayNamesOf(toEntityObjects(inputs)), source);
16914
+ return write === null ? null : prepareTopicsUpdate(write.row, write.topics, Date.now());
16915
+ }
16838
16916
  async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
16839
16917
  if (entityInputs.length === 0) return [];
16840
- const normalized = entityInputs.map((e) => typeof e === "string" ? { name: e } : e);
16918
+ const normalized = toEntityObjects(entityInputs);
16841
16919
  const userId = ctx.userId;
16842
16920
  let skipped = false;
16843
16921
  let entities = [];
@@ -16847,7 +16925,8 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
16847
16925
  normalized
16848
16926
  );
16849
16927
  entities = Array.from(byName.values());
16850
- if (options?.unlessTopicsUserManaged && await autoLinkBlocked(ctx, memoryId)) {
16928
+ const vaultRow = await findVaultRowInWrite(ctx, memoryId);
16929
+ if (options?.unlessTopicsUserManaged && autoLinkBlocked(vaultRow)) {
16851
16930
  if (entityOps.length > 0) {
16852
16931
  await ctx.database.batch(...entityOps);
16853
16932
  }
@@ -16858,7 +16937,18 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
16858
16937
  const existingLinks = await ctx.memoryEntityCollection.query(import_watermelondb16.Q.where("memory_id", memoryId)).fetch();
16859
16938
  const existingEntityIds = new Set(existingLinks.map((l) => String(l.entityId)));
16860
16939
  const toCreate = entities.filter((e) => !existingEntityIds.has(e.uniqueId));
16861
- if (entityOps.length === 0 && toCreate.length === 0) return;
16940
+ const keptIds = entities.map((e) => e.uniqueId);
16941
+ const carriedOver = await resolveEntitiesByIds(
16942
+ ctx,
16943
+ [...existingEntityIds].filter((id) => !keptIds.includes(id))
16944
+ );
16945
+ const topicsWrite = resolveTopicsWrite(
16946
+ vaultRow,
16947
+ [...carriedOver, ...entities],
16948
+ displayNamesOf(normalized),
16949
+ options?.topicsSource ?? "auto"
16950
+ );
16951
+ if (entityOps.length === 0 && toCreate.length === 0 && topicsWrite === null) return;
16862
16952
  const linkOps = toCreate.map(
16863
16953
  (e) => ctx.memoryEntityCollection.prepareCreate((record) => {
16864
16954
  record._setRaw("memory_id", memoryId);
@@ -16866,26 +16956,42 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
16866
16956
  if (userId !== void 0) record._setRaw("user_id", userId);
16867
16957
  })
16868
16958
  );
16869
- await ctx.database.batch(...entityOps, ...linkOps);
16959
+ await ctx.database.batch(
16960
+ ...entityOps,
16961
+ ...linkOps,
16962
+ ...topicsWrite ? [prepareTopicsUpdate(topicsWrite.row, topicsWrite.topics, Date.now())] : []
16963
+ );
16870
16964
  });
16871
16965
  return skipped ? [] : entities;
16872
16966
  }
16873
- async function autoLinkBlocked(ctx, memoryId) {
16874
- try {
16875
- const rows = await ctx.database.get("memory_vault").query(import_watermelondb16.Q.where("id", memoryId)).fetch();
16876
- const row = rows[0];
16877
- return !row || !!row.isDeleted || !!row.topicsUserManaged;
16878
- } catch {
16879
- return true;
16880
- }
16967
+ async function resolveEntitiesByIds(ctx, entityIds) {
16968
+ if (entityIds.length === 0) return [];
16969
+ const rows = await ctx.entityCollection.query(import_watermelondb16.Q.where("id", import_watermelondb16.Q.oneOf(Array.from(entityIds)))).fetch();
16970
+ return rows.map(entityToStored);
16971
+ }
16972
+ function autoLinkBlocked(row) {
16973
+ return !row || !!row.isDeleted || !!row.topicsUserManaged;
16881
16974
  }
16882
16975
  async function replaceMemoryEntitiesGuardedOp(ctx, memoryId, entityInputs) {
16883
- const normalized = entityInputs.map((e) => typeof e === "string" ? { name: e } : e);
16976
+ return replaceMemoryEntities(ctx, memoryId, entityInputs, {
16977
+ guarded: true,
16978
+ topicsSource: "auto"
16979
+ });
16980
+ }
16981
+ async function relinkMemoryEntitiesFromTopicsOp(ctx, memoryId, topics) {
16982
+ return replaceMemoryEntities(ctx, memoryId, topics, {
16983
+ guarded: false,
16984
+ topicsSource: null
16985
+ });
16986
+ }
16987
+ async function replaceMemoryEntities(ctx, memoryId, entityInputs, options) {
16988
+ const normalized = toEntityObjects(entityInputs);
16884
16989
  const userId = ctx.userId;
16885
16990
  let skipped = false;
16886
16991
  let entities = [];
16887
16992
  await ctx.database.write(async () => {
16888
- if (await autoLinkBlocked(ctx, memoryId)) {
16993
+ const vaultRow = await findVaultRowInWrite(ctx, memoryId);
16994
+ if (options.guarded && autoLinkBlocked(vaultRow)) {
16889
16995
  skipped = true;
16890
16996
  return;
16891
16997
  }
@@ -16899,7 +17005,15 @@ async function replaceMemoryEntitiesGuardedOp(ctx, memoryId, entityInputs) {
16899
17005
  const existingEntityIds = new Set(existingLinks.map((l) => String(l.entityId)));
16900
17006
  const toCreate = entities.filter((e) => !existingEntityIds.has(e.uniqueId));
16901
17007
  const toDestroy = existingLinks.filter((l) => !keep.has(String(l.entityId)));
16902
- if (entityOps.length === 0 && toCreate.length === 0 && toDestroy.length === 0) return;
17008
+ const topicsWrite = resolveTopicsWrite(
17009
+ vaultRow,
17010
+ entities,
17011
+ displayNamesOf(normalized),
17012
+ options.topicsSource
17013
+ );
17014
+ if (entityOps.length === 0 && toCreate.length === 0 && toDestroy.length === 0 && topicsWrite === null) {
17015
+ return;
17016
+ }
16903
17017
  const orphans = await findOrphanedEntities(ctx, memoryId, toDestroy);
16904
17018
  await ctx.database.batch(
16905
17019
  ...entityOps,
@@ -16911,7 +17025,8 @@ async function replaceMemoryEntitiesGuardedOp(ctx, memoryId, entityInputs) {
16911
17025
  })
16912
17026
  ),
16913
17027
  ...toDestroy.map((l) => l.prepareDestroyPermanently()),
16914
- ...orphans.map((e) => e.prepareDestroyPermanently())
17028
+ ...orphans.map((e) => e.prepareDestroyPermanently()),
17029
+ ...topicsWrite ? [prepareTopicsUpdate(topicsWrite.row, topicsWrite.topics, Date.now())] : []
16915
17030
  );
16916
17031
  });
16917
17032
  return skipped ? null : entities;
@@ -17110,6 +17225,8 @@ function vaultMemoryToStoredRaw(memory) {
17110
17225
  eventTimeEnd: memory.eventTimeEnd ?? null,
17111
17226
  eventTimeKind: memory.eventTimeKind ?? null,
17112
17227
  topicsUserManaged: memory.topicsUserManaged ?? false,
17228
+ topics: parseTopics(memory.topics),
17229
+ topicsUpdatedAt: memory.topicsUpdatedAt ?? null,
17113
17230
  topicsExtractedAt: memory.topicsExtractedAt ?? null,
17114
17231
  topicsExtractedVersion: memory.topicsExtractedVersion ?? null,
17115
17232
  supersededBy: memory.supersededBy ?? null,
@@ -17386,6 +17503,8 @@ function vaultMemoryRawToStoredRaw(raw) {
17386
17503
  eventTimeKind: raw.event_time_kind ?? null,
17387
17504
  // SQLite stores booleans as 0/1, LokiJS as true/false — coerce both.
17388
17505
  topicsUserManaged: raw.topics_user_managed === true || raw.topics_user_managed === 1,
17506
+ topics: parseTopics(raw.topics),
17507
+ topicsUpdatedAt: raw.topics_updated_at ?? null,
17389
17508
  topicsExtractedAt: raw.topics_extracted_at ?? null,
17390
17509
  topicsExtractedVersion: raw.topics_extracted_version ?? null,
17391
17510
  supersededBy: raw.superseded_by ?? null,
@@ -17732,56 +17851,111 @@ async function getUnfiledVaultMemoriesOp(ctx) {
17732
17851
  (record) => vaultMemoryToStored(record, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner)
17733
17852
  );
17734
17853
  }
17854
+ async function linkedEntityNamesByMemory(entityCtx, memoryIds) {
17855
+ const CHUNK = 500;
17856
+ const entityIdByMemory = /* @__PURE__ */ new Map();
17857
+ const allEntityIds = /* @__PURE__ */ new Set();
17858
+ for (let i = 0; i < memoryIds.length; i += CHUNK) {
17859
+ const links = await entityCtx.memoryEntityCollection.query(import_watermelondb17.Q.where("memory_id", import_watermelondb17.Q.oneOf(memoryIds.slice(i, i + CHUNK)))).unsafeFetchRaw();
17860
+ for (const link of links) {
17861
+ const memoryId = String(link.memory_id);
17862
+ const entityId = String(link.entity_id);
17863
+ allEntityIds.add(entityId);
17864
+ let bucket = entityIdByMemory.get(memoryId);
17865
+ if (!bucket) {
17866
+ bucket = /* @__PURE__ */ new Set();
17867
+ entityIdByMemory.set(memoryId, bucket);
17868
+ }
17869
+ bucket.add(entityId);
17870
+ }
17871
+ }
17872
+ const entityIds = Array.from(allEntityIds);
17873
+ const nameById = /* @__PURE__ */ new Map();
17874
+ for (let i = 0; i < entityIds.length; i += CHUNK) {
17875
+ const entities = await entityCtx.entityCollection.query(import_watermelondb17.Q.where("id", import_watermelondb17.Q.oneOf(entityIds.slice(i, i + CHUNK)))).unsafeFetchRaw();
17876
+ for (const e of entities) {
17877
+ if (typeof e.canonical_name === "string") nameById.set(String(e.id), e.canonical_name);
17878
+ }
17879
+ }
17880
+ const out = /* @__PURE__ */ new Map();
17881
+ for (const [memoryId, ids] of entityIdByMemory) {
17882
+ const names = /* @__PURE__ */ new Set();
17883
+ for (const id of ids) {
17884
+ const name = nameById.get(id);
17885
+ if (name !== void 0) names.add(name);
17886
+ }
17887
+ if (names.size > 0) out.set(memoryId, names);
17888
+ }
17889
+ return out;
17890
+ }
17891
+ function linksDivergeFromTopics(topics, linked) {
17892
+ const wanted = new Set(
17893
+ topics.map((t) => normalizeEntityName(t.name)).filter((n) => n.length > 0)
17894
+ );
17895
+ if (wanted.size !== linked.size) return true;
17896
+ for (const name of wanted) if (!linked.has(name)) return true;
17897
+ return false;
17898
+ }
17735
17899
  async function getMemoriesNeedingTopicExtractionOp(ctx, options) {
17736
17900
  const entityCtx = ctx.entityCtx;
17737
17901
  if (!entityCtx) {
17738
17902
  throw new Error("getMemoriesNeedingTopicExtractionOp requires ctx.entityCtx");
17739
17903
  }
17740
- const conditions = [
17741
- // Boolean optional column: null (legacy) and false both mean auto-managed.
17742
- // Q.notEq(true) is a NULL trap on SQLite (NULL <> TRUE is NULL) — enumerate.
17743
- import_watermelondb17.Q.or(import_watermelondb17.Q.where("topics_user_managed", null), import_watermelondb17.Q.where("topics_user_managed", false)),
17744
- ...baseVaultConditions(ctx),
17745
- import_watermelondb17.Q.sortBy("created_at", import_watermelondb17.Q.desc)
17746
- ];
17904
+ const conditions = [...baseVaultConditions(ctx), import_watermelondb17.Q.sortBy("created_at", import_watermelondb17.Q.desc)];
17747
17905
  const rows = await ctx.vaultMemoryCollection.query(...conditions).unsafeFetchRaw();
17748
- const stampedPending = [];
17749
- const unstamped = [];
17906
+ const linkedNames = await linkedEntityNamesByMemory(
17907
+ entityCtx,
17908
+ rows.map((r) => r.id)
17909
+ );
17910
+ const pendingRaw = [];
17911
+ const stampedPendingRaw = [];
17912
+ const linkedUnstampedAll = [];
17913
+ const topicsToRelinkAll = [];
17914
+ const topicsBackfillAll = [];
17750
17915
  for (const raw of rows) {
17916
+ const id = raw.id;
17917
+ const topics = parseTopics(raw.topics);
17918
+ const linked = linkedNames.get(id);
17919
+ if (topics !== null && topics.length > 0 && linksDivergeFromTopics(topics, linked ?? /* @__PURE__ */ new Set())) {
17920
+ topicsToRelinkAll.push(id);
17921
+ continue;
17922
+ }
17923
+ if (raw.topics_user_managed) {
17924
+ if (topics === null && linked !== void 0) topicsBackfillAll.push(id);
17925
+ continue;
17926
+ }
17751
17927
  const stamp = raw.topics_extracted_at ?? null;
17928
+ let isPending = false;
17752
17929
  if (stamp !== null) {
17753
17930
  const version = raw.topics_extracted_version ?? 0;
17754
17931
  if (raw.updated_at > stamp || version < TOPICS_EXTRACTION_VERSION) {
17755
- stampedPending.push(raw);
17932
+ stampedPendingRaw.push(raw);
17933
+ isPending = true;
17756
17934
  }
17757
- } else {
17758
- unstamped.push(raw);
17759
- }
17760
- }
17761
- const linkedIds = /* @__PURE__ */ new Set();
17762
- const CHUNK = 500;
17763
- for (let i = 0; i < unstamped.length; i += CHUNK) {
17764
- const ids = unstamped.slice(i, i + CHUNK).map((r) => r.id);
17765
- const links = await entityCtx.memoryEntityCollection.query(import_watermelondb17.Q.where("memory_id", import_watermelondb17.Q.oneOf(ids))).unsafeFetchRaw();
17766
- for (const link of links) linkedIds.add(String(link.memory_id));
17767
- }
17768
- const linkedUnstampedAll = [];
17769
- const pendingRaw = [...stampedPending];
17770
- for (const raw of unstamped) {
17771
- if (linkedIds.has(raw.id)) {
17772
- linkedUnstampedAll.push(raw.id);
17935
+ } else if (linked !== void 0) {
17936
+ linkedUnstampedAll.push(id);
17773
17937
  } else {
17774
17938
  pendingRaw.push(raw);
17939
+ isPending = true;
17940
+ }
17941
+ if (!isPending && topics === null && linked !== void 0) {
17942
+ topicsBackfillAll.push(id);
17775
17943
  }
17776
17944
  }
17777
17945
  const cap = options?.limit !== void 0 && options.limit > 0 ? options.limit : void 0;
17778
- const limitedPendingRaw = cap !== void 0 ? pendingRaw.slice(0, cap) : pendingRaw;
17779
- const linkedUnstamped = cap !== void 0 ? linkedUnstampedAll.slice(0, cap) : linkedUnstampedAll;
17946
+ const orderedPendingRaw = [...stampedPendingRaw, ...pendingRaw];
17947
+ const limitedPendingRaw = cap !== void 0 ? orderedPendingRaw.slice(0, cap) : orderedPendingRaw;
17780
17948
  const pending = await mapInBatches(
17781
17949
  limitedPendingRaw,
17782
17950
  (raw) => vaultMemoryRawToStored(raw, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner)
17783
17951
  );
17784
- return { pending, linkedUnstamped };
17952
+ const applyCap = (ids) => cap !== void 0 ? ids.slice(0, cap) : ids;
17953
+ return {
17954
+ pending,
17955
+ linkedUnstamped: applyCap(linkedUnstampedAll),
17956
+ topicsToRelink: applyCap(topicsToRelinkAll),
17957
+ topicsBackfill: applyCap(topicsBackfillAll)
17958
+ };
17785
17959
  }
17786
17960
  async function stampTopicsExtractedAtOp(ctx, memoryIds, extractedAt, version = TOPICS_EXTRACTION_VERSION) {
17787
17961
  if (memoryIds.length === 0) return [];
@@ -17815,6 +17989,61 @@ async function stampTopicsExtractedAtOp(ctx, memoryIds, extractedAt, version = T
17815
17989
  }
17816
17990
  return stamped;
17817
17991
  }
17992
+ async function relinkMemoryTopicsOp(ctx, memoryIds) {
17993
+ const entityCtx = ctx.entityCtx;
17994
+ if (!entityCtx) {
17995
+ throw new Error("relinkMemoryTopicsOp requires ctx.entityCtx");
17996
+ }
17997
+ const relinked = [];
17998
+ for (const id of Array.from(new Set(memoryIds))) {
17999
+ let record;
18000
+ try {
18001
+ record = await ctx.vaultMemoryCollection.find(id);
18002
+ } catch {
18003
+ continue;
18004
+ }
18005
+ if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) continue;
18006
+ const topics = parseTopics(record.topics);
18007
+ if (topics === null || topics.length === 0) continue;
18008
+ try {
18009
+ await relinkMemoryEntitiesFromTopicsOp(entityCtx, id, topics);
18010
+ relinked.push(id);
18011
+ } catch (err) {
18012
+ getLogger().warn("[memory/topics] relink failed", err);
18013
+ }
18014
+ }
18015
+ return relinked;
18016
+ }
18017
+ async function backfillMemoryTopicsOp(ctx, memoryIds) {
18018
+ const entityCtx = ctx.entityCtx;
18019
+ if (!entityCtx) {
18020
+ throw new Error("backfillMemoryTopicsOp requires ctx.entityCtx");
18021
+ }
18022
+ const filled = [];
18023
+ for (const id of Array.from(new Set(memoryIds))) {
18024
+ let record;
18025
+ try {
18026
+ record = await ctx.vaultMemoryCollection.find(id);
18027
+ } catch {
18028
+ continue;
18029
+ }
18030
+ if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) continue;
18031
+ if (parseTopics(record.topics) !== null) continue;
18032
+ const links = await entityCtx.memoryEntityCollection.query(import_watermelondb17.Q.where("memory_id", id)).unsafeFetchRaw();
18033
+ const entityIds = links.map((l) => String(l.entity_id));
18034
+ if (entityIds.length === 0) continue;
18035
+ const entities = await entityCtx.entityCollection.query(import_watermelondb17.Q.where("id", import_watermelondb17.Q.oneOf(entityIds))).fetch();
18036
+ if (entities.length === 0) continue;
18037
+ const source = record.topicsUserManaged ? "user" : "auto";
18038
+ await ctx.database.write(async () => {
18039
+ const topicsOp = await prepareMemoryTopicsUpdate(entityCtx, id, entities, [], source);
18040
+ if (!topicsOp) return;
18041
+ await ctx.database.batch(topicsOp);
18042
+ filled.push(id);
18043
+ });
18044
+ }
18045
+ return filled;
18046
+ }
17818
18047
  async function deleteAllVaultMemoriesForUserOp(ctx, userId) {
17819
18048
  if (ctx.userId !== void 0 && ctx.userId !== userId) return 0;
17820
18049
  const records = await ctx.vaultMemoryCollection.query(import_watermelondb17.Q.where("user_id", userId), import_watermelondb17.Q.where("is_deleted", false)).fetch();
@@ -17956,6 +18185,7 @@ var init_operations3 = __esm({
17956
18185
  import_watermelondb17 = __toESM(require_watermelondb());
17957
18186
  init_logger();
17958
18187
  init_operations2();
18188
+ init_types3();
17959
18189
  init_encryption2();
17960
18190
  NON_PRIVATE_VISIBILITIES = ["public"];
17961
18191
  KNOWN_TRUST_TIERS = /* @__PURE__ */ new Set(["quarantined", "trusted"]);
@@ -23235,6 +23465,7 @@ __export(index_exports, {
23235
23465
  activatedToolSetNames: () => activatedToolSetNames,
23236
23466
  analyzeStyle: () => analyzeStyle,
23237
23467
  archiveVaultMemoryOp: () => archiveVaultMemoryOp,
23468
+ backfillMemoryTopicsOp: () => backfillMemoryTopicsOp,
23238
23469
  buildConnectorGuidance: () => buildConnectorGuidance,
23239
23470
  buildDeniedToolsRider: () => buildDeniedToolsRider,
23240
23471
  buildSystemPrompt: () => buildSystemPrompt,
@@ -23392,6 +23623,7 @@ __export(index_exports, {
23392
23623
  rankProfileCandidates: () => rankProfileCandidates,
23393
23624
  recall: () => recall,
23394
23625
  reflect: () => reflect,
23626
+ relinkMemoryTopicsOp: () => relinkMemoryTopicsOp,
23395
23627
  relinkMisclassifiedVideosOp: () => relinkMisclassifiedVideosOp,
23396
23628
  renderTemplate: () => renderTemplate,
23397
23629
  requestEncryptionKey: () => requestEncryptionKey,
@@ -23778,6 +24010,12 @@ __decorateClass([
23778
24010
  __decorateClass([
23779
24011
  (0, import_decorators6.field)("topics_user_managed")
23780
24012
  ], VaultMemory.prototype, "topicsUserManaged", 2);
24013
+ __decorateClass([
24014
+ (0, import_decorators6.field)("topics")
24015
+ ], VaultMemory.prototype, "topics", 2);
24016
+ __decorateClass([
24017
+ (0, import_decorators6.field)("topics_updated_at")
24018
+ ], VaultMemory.prototype, "topicsUpdatedAt", 2);
23781
24019
  __decorateClass([
23782
24020
  (0, import_decorators6.field)("topics_extracted_at")
23783
24021
  ], VaultMemory.prototype, "topicsExtractedAt", 2);
@@ -23961,7 +24199,7 @@ __decorateClass([
23961
24199
  ], VaultFolder.prototype, "context", 2);
23962
24200
 
23963
24201
  // src/lib/db/schema.ts
23964
- var SDK_SCHEMA_VERSION = 41;
24202
+ var SDK_SCHEMA_VERSION = 42;
23965
24203
  var sdkSchema = (0, import_watermelondb12.appSchema)({
23966
24204
  version: SDK_SCHEMA_VERSION,
23967
24205
  tables: [
@@ -24083,9 +24321,33 @@ var sdkSchema = (0, import_watermelondb12.appSchema)({
24083
24321
  // links). Auto-extraction then leaves its links alone — the user owns
24084
24322
  // them. Null/false = topics are auto-derived (default).
24085
24323
  { name: "topics_user_managed", type: "boolean", isOptional: true },
24324
+ // The memory's topics as a DURABLE, SYNCED record: a JSON array of
24325
+ // `{name, kind?, source}`, `name` in the caller's display casing (unlike
24326
+ // `entity.canonical_name`, which is lowercased and has no display
24327
+ // column). `entity` / `memory_entity` are a device-local INDEX over this
24328
+ // — their ids are locally generated, so they can never sync — and a
24329
+ // restored device rebuilds them from these names with no LLM call.
24330
+ // Null = predates v42 (backfilled from the row's current links by the
24331
+ // sweep's topicsBackfill bucket). See getMemoriesNeedingTopicExtractionOp.
24332
+ { name: "topics", type: "string", isOptional: true },
24333
+ // Unix ms of the last write to `topics`. A SECOND timestamp is required:
24334
+ // every topic writer deliberately pins `updated_at` so a topic change
24335
+ // doesn't inflate recall's recency multiplier, and both client sync
24336
+ // paths key on `updated_at` — so without this a topic-only change would
24337
+ // neither upload nor merge. Null = `topics` never written.
24338
+ { name: "topics_updated_at", type: "number", isOptional: true },
24086
24339
  // Unix ms of the last LLM topic-extraction pass over this memory's
24087
24340
  // content. Null = never extracted standalone (legacy rows with entity
24088
24341
  // links are grandfathered — see getMemoriesNeedingTopicExtractionOp).
24342
+ //
24343
+ // DEPRECATED (v42) — `topics_updated_at` subsumes this and
24344
+ // `topics_extracted_version` both: null there means never processed, and
24345
+ // non-null with an empty `topics` means processed and found nothing
24346
+ // (today's "answered empty" case), while a release-time
24347
+ // EXTRACTOR_CHANGED_AT constant compared against `topics_updated_at`
24348
+ // replaces the version check. Both columns are kept only to avoid a
24349
+ // column-drop migration in an otherwise additive list; removing them is
24350
+ // a follow-up once `topics` is proven in production.
24089
24351
  { name: "topics_extracted_at", type: "number", isOptional: true },
24090
24352
  // Write-time supersession (A2). When set, this fact was replaced by a
24091
24353
  // newer, incompatible-value fact (e.g. "Lives in Portland" superseded by
@@ -24099,6 +24361,9 @@ var sdkSchema = (0, import_watermelondb12.appSchema)({
24099
24361
  // The extraction-logic version this memory was last stamped under. Null
24100
24362
  // (pre-v38) is treated as version 0, so a bump of TOPICS_EXTRACTION_VERSION
24101
24363
  // re-extracts stale rows. See getMemoriesNeedingTopicExtractionOp.
24364
+ //
24365
+ // DEPRECATED (v42) alongside `topics_extracted_at` — same rationale and
24366
+ // same removal follow-up; see that column's note above.
24102
24367
  { name: "topics_extracted_version", type: "number", isOptional: true },
24103
24368
  // Re-observation watermark (C3). Unix ms of the last time retain() merged
24104
24369
  // a duplicate observation into this fact (proof_count++). Distinct from
@@ -24832,6 +25097,23 @@ var sdkMigrations = (0, import_migrations.schemaMigrations)({
24832
25097
  ]
24833
25098
  })
24834
25099
  ]
25100
+ },
25101
+ // v41 -> v42: topics + topics_updated_at on memory_vault — the durable,
25102
+ // synced record of a memory's topics. Existing rows keep both NULL: the
25103
+ // sweep's topicsBackfill bucket fills them from each row's current entity
25104
+ // links (no LLM), capped under the worker's `limit` so the one-time
25105
+ // re-upload it triggers drains across sweeps instead of spiking.
25106
+ {
25107
+ toVersion: 42,
25108
+ steps: [
25109
+ (0, import_migrations.addColumns)({
25110
+ table: "memory_vault",
25111
+ columns: [
25112
+ { name: "topics", type: "string", isOptional: true },
25113
+ { name: "topics_updated_at", type: "number", isOptional: true }
25114
+ ]
25115
+ })
25116
+ ]
24835
25117
  }
24836
25118
  ]
24837
25119
  });
@@ -28799,7 +29081,6 @@ function createAutoExtractor(options) {
28799
29081
  return true;
28800
29082
  }
28801
29083
  function drainPending() {
28802
- if (disposed) return;
28803
29084
  for (const [conversationId, state] of conversations) {
28804
29085
  if (state.pending) {
28805
29086
  const messages = state.pending;
@@ -28814,7 +29095,7 @@ function createAutoExtractor(options) {
28814
29095
  isProcessing: () => inflight > 0,
28815
29096
  dispose: () => {
28816
29097
  disposed = true;
28817
- for (const state of conversations.values()) state.pending = void 0;
29098
+ if (inflight === 0) drainPending();
28818
29099
  }
28819
29100
  };
28820
29101
  }
@@ -35749,6 +36030,7 @@ function buildSystemPrompt(ctx, options) {
35749
36030
  activatedToolSetNames,
35750
36031
  analyzeStyle,
35751
36032
  archiveVaultMemoryOp,
36033
+ backfillMemoryTopicsOp,
35752
36034
  buildConnectorGuidance,
35753
36035
  buildDeniedToolsRider,
35754
36036
  buildSystemPrompt,
@@ -35906,6 +36188,7 @@ function buildSystemPrompt(ctx, options) {
35906
36188
  rankProfileCandidates,
35907
36189
  recall,
35908
36190
  reflect,
36191
+ relinkMemoryTopicsOp,
35909
36192
  relinkMisclassifiedVideosOp,
35910
36193
  renderTemplate,
35911
36194
  requestEncryptionKey,