@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.
@@ -20,6 +20,7 @@ import {
20
20
  VAULT_SIZE_HOP_CAP,
21
21
  archiveVaultMemoryOp,
22
22
  assertVaultScopeForSweep,
23
+ backfillMemoryTopicsOp,
23
24
  capHopsForDensity,
24
25
  chunkAndEmbedAllMessages,
25
26
  chunkAndEmbedMessage,
@@ -91,6 +92,7 @@ import {
91
92
  prepareVaultCandidates,
92
93
  rankPreparedVaultCandidates,
93
94
  recall,
95
+ relinkMemoryTopicsOp,
94
96
  replaceMemoryEntitiesGuardedOp,
95
97
  requestEncryptionKey,
96
98
  resolveStoredUserContent,
@@ -114,7 +116,7 @@ import {
114
116
  upsertMessageOp,
115
117
  useEncryption,
116
118
  vaultMemoryToStored
117
- } from "./chunk-ORVYPS3R.mjs";
119
+ } from "./chunk-BOAKGY4R.mjs";
118
120
  import {
119
121
  PII_PATTERNS,
120
122
  PiiRedactor,
@@ -5596,6 +5598,12 @@ __decorateClass([
5596
5598
  __decorateClass([
5597
5599
  field3("topics_user_managed")
5598
5600
  ], VaultMemory.prototype, "topicsUserManaged", 2);
5601
+ __decorateClass([
5602
+ field3("topics")
5603
+ ], VaultMemory.prototype, "topics", 2);
5604
+ __decorateClass([
5605
+ field3("topics_updated_at")
5606
+ ], VaultMemory.prototype, "topicsUpdatedAt", 2);
5599
5607
  __decorateClass([
5600
5608
  field3("topics_extracted_at")
5601
5609
  ], VaultMemory.prototype, "topicsExtractedAt", 2);
@@ -7368,7 +7376,6 @@ function createAutoExtractor(options) {
7368
7376
  return true;
7369
7377
  }
7370
7378
  function drainPending() {
7371
- if (disposed) return;
7372
7379
  for (const [conversationId, state] of conversations) {
7373
7380
  if (state.pending) {
7374
7381
  const messages = state.pending;
@@ -7383,7 +7390,7 @@ function createAutoExtractor(options) {
7383
7390
  isProcessing: () => inflight > 0,
7384
7391
  dispose: () => {
7385
7392
  disposed = true;
7386
- for (const state of conversations.values()) state.pending = void 0;
7393
+ if (inflight === 0) drainPending();
7387
7394
  }
7388
7395
  };
7389
7396
  }
@@ -11774,7 +11781,7 @@ __decorateClass([
11774
11781
  ], VaultFolder.prototype, "context", 2);
11775
11782
 
11776
11783
  // src/lib/db/schema.ts
11777
- var SDK_SCHEMA_VERSION = 41;
11784
+ var SDK_SCHEMA_VERSION = 42;
11778
11785
  var sdkSchema = appSchema2({
11779
11786
  version: SDK_SCHEMA_VERSION,
11780
11787
  tables: [
@@ -11896,9 +11903,33 @@ var sdkSchema = appSchema2({
11896
11903
  // links). Auto-extraction then leaves its links alone — the user owns
11897
11904
  // them. Null/false = topics are auto-derived (default).
11898
11905
  { name: "topics_user_managed", type: "boolean", isOptional: true },
11906
+ // The memory's topics as a DURABLE, SYNCED record: a JSON array of
11907
+ // `{name, kind?, source}`, `name` in the caller's display casing (unlike
11908
+ // `entity.canonical_name`, which is lowercased and has no display
11909
+ // column). `entity` / `memory_entity` are a device-local INDEX over this
11910
+ // — their ids are locally generated, so they can never sync — and a
11911
+ // restored device rebuilds them from these names with no LLM call.
11912
+ // Null = predates v42 (backfilled from the row's current links by the
11913
+ // sweep's topicsBackfill bucket). See getMemoriesNeedingTopicExtractionOp.
11914
+ { name: "topics", type: "string", isOptional: true },
11915
+ // Unix ms of the last write to `topics`. A SECOND timestamp is required:
11916
+ // every topic writer deliberately pins `updated_at` so a topic change
11917
+ // doesn't inflate recall's recency multiplier, and both client sync
11918
+ // paths key on `updated_at` — so without this a topic-only change would
11919
+ // neither upload nor merge. Null = `topics` never written.
11920
+ { name: "topics_updated_at", type: "number", isOptional: true },
11899
11921
  // Unix ms of the last LLM topic-extraction pass over this memory's
11900
11922
  // content. Null = never extracted standalone (legacy rows with entity
11901
11923
  // links are grandfathered — see getMemoriesNeedingTopicExtractionOp).
11924
+ //
11925
+ // DEPRECATED (v42) — `topics_updated_at` subsumes this and
11926
+ // `topics_extracted_version` both: null there means never processed, and
11927
+ // non-null with an empty `topics` means processed and found nothing
11928
+ // (today's "answered empty" case), while a release-time
11929
+ // EXTRACTOR_CHANGED_AT constant compared against `topics_updated_at`
11930
+ // replaces the version check. Both columns are kept only to avoid a
11931
+ // column-drop migration in an otherwise additive list; removing them is
11932
+ // a follow-up once `topics` is proven in production.
11902
11933
  { name: "topics_extracted_at", type: "number", isOptional: true },
11903
11934
  // Write-time supersession (A2). When set, this fact was replaced by a
11904
11935
  // newer, incompatible-value fact (e.g. "Lives in Portland" superseded by
@@ -11912,6 +11943,9 @@ var sdkSchema = appSchema2({
11912
11943
  // The extraction-logic version this memory was last stamped under. Null
11913
11944
  // (pre-v38) is treated as version 0, so a bump of TOPICS_EXTRACTION_VERSION
11914
11945
  // re-extracts stale rows. See getMemoriesNeedingTopicExtractionOp.
11946
+ //
11947
+ // DEPRECATED (v42) alongside `topics_extracted_at` — same rationale and
11948
+ // same removal follow-up; see that column's note above.
11915
11949
  { name: "topics_extracted_version", type: "number", isOptional: true },
11916
11950
  // Re-observation watermark (C3). Unix ms of the last time retain() merged
11917
11951
  // a duplicate observation into this fact (proof_count++). Distinct from
@@ -12645,6 +12679,23 @@ var sdkMigrations = schemaMigrations2({
12645
12679
  ]
12646
12680
  })
12647
12681
  ]
12682
+ },
12683
+ // v41 -> v42: topics + topics_updated_at on memory_vault — the durable,
12684
+ // synced record of a memory's topics. Existing rows keep both NULL: the
12685
+ // sweep's topicsBackfill bucket fills them from each row's current entity
12686
+ // links (no LLM), capped under the worker's `limit` so the one-time
12687
+ // re-upload it triggers drains across sweeps instead of spiking.
12688
+ {
12689
+ toVersion: 42,
12690
+ steps: [
12691
+ addColumns2({
12692
+ table: "memory_vault",
12693
+ columns: [
12694
+ { name: "topics", type: "string", isOptional: true },
12695
+ { name: "topics_updated_at", type: "number", isOptional: true }
12696
+ ]
12697
+ })
12698
+ ]
12648
12699
  }
12649
12700
  ]
12650
12701
  });
@@ -14025,6 +14076,7 @@ export {
14025
14076
  WalletPoller,
14026
14077
  addConversationMemoriesOp,
14027
14078
  archiveVaultMemoryOp,
14079
+ backfillMemoryTopicsOp,
14028
14080
  buildNotionAuthUrl,
14029
14081
  capHopsForDensity,
14030
14082
  chatStorageMigrations,
@@ -14122,6 +14174,7 @@ export {
14122
14174
  reflect,
14123
14175
  refreshNotionAccessToken,
14124
14176
  registerNotionClient,
14177
+ relinkMemoryTopicsOp,
14125
14178
  requestEncryptionKey,
14126
14179
  resolvePiiRedactor,
14127
14180
  restoreVaultMemoryOp,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  recall
3
- } from "./chunk-ORVYPS3R.mjs";
3
+ } from "./chunk-BOAKGY4R.mjs";
4
4
  import "./chunk-ILAWDQDE.mjs";
5
5
  import "./chunk-GYKJOWEO.mjs";
6
6
  export {
@@ -1918,6 +1918,31 @@ var ENTITY_KINDS = [
1918
1918
  function normalizeEntityName(name) {
1919
1919
  return name.trim().toLowerCase();
1920
1920
  }
1921
+ function serializeTopics(topics) {
1922
+ return JSON.stringify(topics);
1923
+ }
1924
+ function parseTopics(value) {
1925
+ if (typeof value !== "string" || value.length === 0) return null;
1926
+ let parsed;
1927
+ try {
1928
+ parsed = JSON.parse(value);
1929
+ } catch {
1930
+ return null;
1931
+ }
1932
+ if (!Array.isArray(parsed)) return null;
1933
+ const out = [];
1934
+ for (const raw of parsed) {
1935
+ if (typeof raw !== "object" || raw === null) continue;
1936
+ const { name, kind, source } = raw;
1937
+ if (typeof name !== "string" || name.trim().length === 0) continue;
1938
+ out.push({
1939
+ name,
1940
+ ...typeof kind === "string" && kind.length > 0 ? { kind } : {},
1941
+ source: source === "user" ? "user" : "auto"
1942
+ });
1943
+ }
1944
+ return out;
1945
+ }
1921
1946
 
1922
1947
  // src/lib/db/entities/operations.ts
1923
1948
  function entityToStored(e) {
@@ -1967,9 +1992,62 @@ async function upsertEntitiesInWrite(ctx, entities) {
1967
1992
  for (const record of created) out.set(record.canonicalName, entityToStored(record));
1968
1993
  return { entities: out, operations: [...created, ...updated] };
1969
1994
  }
1995
+ async function findVaultRowInWrite(ctx, memoryId) {
1996
+ try {
1997
+ const rows = await ctx.database.get("memory_vault").query(Q2.where("id", memoryId)).fetch();
1998
+ return rows[0] ?? null;
1999
+ } catch {
2000
+ return null;
2001
+ }
2002
+ }
2003
+ function prepareTopicsUpdate(row, topics, now) {
2004
+ const originalUpdatedAt = row.updatedAt.getTime();
2005
+ return row.prepareUpdate((r) => {
2006
+ r._setRaw("topics", serializeTopics(topics));
2007
+ r._setRaw("topics_updated_at", now);
2008
+ r._setRaw("updated_at", originalUpdatedAt);
2009
+ });
2010
+ }
2011
+ function displayNamesOf(entities) {
2012
+ const out = /* @__PURE__ */ new Map();
2013
+ for (const e of entities) {
2014
+ const name = e.name.trim();
2015
+ const normalized = normalizeEntityName(name);
2016
+ if (normalized.length === 0 || out.has(normalized)) continue;
2017
+ out.set(normalized, name);
2018
+ }
2019
+ return out;
2020
+ }
2021
+ function topicsEqual(stored, computed) {
2022
+ if (stored === null || stored.length !== computed.length) return false;
2023
+ const key = (t) => JSON.stringify([t.name, t.kind ?? null, t.source]);
2024
+ const storedKeys = stored.map(key).sort();
2025
+ const computedKeys = computed.map(key).sort();
2026
+ return storedKeys.every((k, i) => k === computedKeys[i]);
2027
+ }
2028
+ function resolveTopicsWrite(row, linked, displayNames, source) {
2029
+ if (row === null || source === null) return null;
2030
+ const topics = topicsForEntities(linked, displayNames, source);
2031
+ if (topicsEqual(parseTopics(row.topics), topics)) return null;
2032
+ return { row, topics };
2033
+ }
2034
+ function topicsForEntities(entities, displayNames, source) {
2035
+ return entities.map((e) => {
2036
+ const name = displayNames.get(e.canonicalName) ?? e.canonicalName;
2037
+ return { name, ...e.kind ? { kind: e.kind } : {}, source };
2038
+ });
2039
+ }
2040
+ function toEntityObjects(entityInputs) {
2041
+ return entityInputs.map((e) => typeof e === "string" ? { name: e } : e);
2042
+ }
2043
+ async function prepareMemoryTopicsUpdate(ctx, memoryId, linked, inputs, source) {
2044
+ const row = await findVaultRowInWrite(ctx, memoryId);
2045
+ const write = resolveTopicsWrite(row, linked, displayNamesOf(toEntityObjects(inputs)), source);
2046
+ return write === null ? null : prepareTopicsUpdate(write.row, write.topics, Date.now());
2047
+ }
1970
2048
  async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
1971
2049
  if (entityInputs.length === 0) return [];
1972
- const normalized = entityInputs.map((e) => typeof e === "string" ? { name: e } : e);
2050
+ const normalized = toEntityObjects(entityInputs);
1973
2051
  const userId = ctx.userId;
1974
2052
  let skipped = false;
1975
2053
  let entities = [];
@@ -1979,7 +2057,8 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
1979
2057
  normalized
1980
2058
  );
1981
2059
  entities = Array.from(byName.values());
1982
- if (options?.unlessTopicsUserManaged && await autoLinkBlocked(ctx, memoryId)) {
2060
+ const vaultRow = await findVaultRowInWrite(ctx, memoryId);
2061
+ if (options?.unlessTopicsUserManaged && autoLinkBlocked(vaultRow)) {
1983
2062
  if (entityOps.length > 0) {
1984
2063
  await ctx.database.batch(...entityOps);
1985
2064
  }
@@ -1990,7 +2069,18 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
1990
2069
  const existingLinks = await ctx.memoryEntityCollection.query(Q2.where("memory_id", memoryId)).fetch();
1991
2070
  const existingEntityIds = new Set(existingLinks.map((l) => String(l.entityId)));
1992
2071
  const toCreate = entities.filter((e) => !existingEntityIds.has(e.uniqueId));
1993
- if (entityOps.length === 0 && toCreate.length === 0) return;
2072
+ const keptIds = entities.map((e) => e.uniqueId);
2073
+ const carriedOver = await resolveEntitiesByIds(
2074
+ ctx,
2075
+ [...existingEntityIds].filter((id) => !keptIds.includes(id))
2076
+ );
2077
+ const topicsWrite = resolveTopicsWrite(
2078
+ vaultRow,
2079
+ [...carriedOver, ...entities],
2080
+ displayNamesOf(normalized),
2081
+ options?.topicsSource ?? "auto"
2082
+ );
2083
+ if (entityOps.length === 0 && toCreate.length === 0 && topicsWrite === null) return;
1994
2084
  const linkOps = toCreate.map(
1995
2085
  (e) => ctx.memoryEntityCollection.prepareCreate((record) => {
1996
2086
  record._setRaw("memory_id", memoryId);
@@ -1998,26 +2088,42 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
1998
2088
  if (userId !== void 0) record._setRaw("user_id", userId);
1999
2089
  })
2000
2090
  );
2001
- await ctx.database.batch(...entityOps, ...linkOps);
2091
+ await ctx.database.batch(
2092
+ ...entityOps,
2093
+ ...linkOps,
2094
+ ...topicsWrite ? [prepareTopicsUpdate(topicsWrite.row, topicsWrite.topics, Date.now())] : []
2095
+ );
2002
2096
  });
2003
2097
  return skipped ? [] : entities;
2004
2098
  }
2005
- async function autoLinkBlocked(ctx, memoryId) {
2006
- try {
2007
- const rows = await ctx.database.get("memory_vault").query(Q2.where("id", memoryId)).fetch();
2008
- const row = rows[0];
2009
- return !row || !!row.isDeleted || !!row.topicsUserManaged;
2010
- } catch {
2011
- return true;
2012
- }
2099
+ async function resolveEntitiesByIds(ctx, entityIds) {
2100
+ if (entityIds.length === 0) return [];
2101
+ const rows = await ctx.entityCollection.query(Q2.where("id", Q2.oneOf(Array.from(entityIds)))).fetch();
2102
+ return rows.map(entityToStored);
2103
+ }
2104
+ function autoLinkBlocked(row) {
2105
+ return !row || !!row.isDeleted || !!row.topicsUserManaged;
2013
2106
  }
2014
2107
  async function replaceMemoryEntitiesGuardedOp(ctx, memoryId, entityInputs) {
2015
- const normalized = entityInputs.map((e) => typeof e === "string" ? { name: e } : e);
2108
+ return replaceMemoryEntities(ctx, memoryId, entityInputs, {
2109
+ guarded: true,
2110
+ topicsSource: "auto"
2111
+ });
2112
+ }
2113
+ async function relinkMemoryEntitiesFromTopicsOp(ctx, memoryId, topics) {
2114
+ return replaceMemoryEntities(ctx, memoryId, topics, {
2115
+ guarded: false,
2116
+ topicsSource: null
2117
+ });
2118
+ }
2119
+ async function replaceMemoryEntities(ctx, memoryId, entityInputs, options) {
2120
+ const normalized = toEntityObjects(entityInputs);
2016
2121
  const userId = ctx.userId;
2017
2122
  let skipped = false;
2018
2123
  let entities = [];
2019
2124
  await ctx.database.write(async () => {
2020
- if (await autoLinkBlocked(ctx, memoryId)) {
2125
+ const vaultRow = await findVaultRowInWrite(ctx, memoryId);
2126
+ if (options.guarded && autoLinkBlocked(vaultRow)) {
2021
2127
  skipped = true;
2022
2128
  return;
2023
2129
  }
@@ -2031,7 +2137,15 @@ async function replaceMemoryEntitiesGuardedOp(ctx, memoryId, entityInputs) {
2031
2137
  const existingEntityIds = new Set(existingLinks.map((l) => String(l.entityId)));
2032
2138
  const toCreate = entities.filter((e) => !existingEntityIds.has(e.uniqueId));
2033
2139
  const toDestroy = existingLinks.filter((l) => !keep.has(String(l.entityId)));
2034
- if (entityOps.length === 0 && toCreate.length === 0 && toDestroy.length === 0) return;
2140
+ const topicsWrite = resolveTopicsWrite(
2141
+ vaultRow,
2142
+ entities,
2143
+ displayNamesOf(normalized),
2144
+ options.topicsSource
2145
+ );
2146
+ if (entityOps.length === 0 && toCreate.length === 0 && toDestroy.length === 0 && topicsWrite === null) {
2147
+ return;
2148
+ }
2035
2149
  const orphans = await findOrphanedEntities(ctx, memoryId, toDestroy);
2036
2150
  await ctx.database.batch(
2037
2151
  ...entityOps,
@@ -2043,7 +2157,8 @@ async function replaceMemoryEntitiesGuardedOp(ctx, memoryId, entityInputs) {
2043
2157
  })
2044
2158
  ),
2045
2159
  ...toDestroy.map((l) => l.prepareDestroyPermanently()),
2046
- ...orphans.map((e) => e.prepareDestroyPermanently())
2160
+ ...orphans.map((e) => e.prepareDestroyPermanently()),
2161
+ ...topicsWrite ? [prepareTopicsUpdate(topicsWrite.row, topicsWrite.topics, Date.now())] : []
2047
2162
  );
2048
2163
  });
2049
2164
  return skipped ? null : entities;
@@ -2253,6 +2368,8 @@ function vaultMemoryToStoredRaw(memory) {
2253
2368
  eventTimeEnd: memory.eventTimeEnd ?? null,
2254
2369
  eventTimeKind: memory.eventTimeKind ?? null,
2255
2370
  topicsUserManaged: memory.topicsUserManaged ?? false,
2371
+ topics: parseTopics(memory.topics),
2372
+ topicsUpdatedAt: memory.topicsUpdatedAt ?? null,
2256
2373
  topicsExtractedAt: memory.topicsExtractedAt ?? null,
2257
2374
  topicsExtractedVersion: memory.topicsExtractedVersion ?? null,
2258
2375
  supersededBy: memory.supersededBy ?? null,
@@ -2529,6 +2646,8 @@ function vaultMemoryRawToStoredRaw(raw) {
2529
2646
  eventTimeKind: raw.event_time_kind ?? null,
2530
2647
  // SQLite stores booleans as 0/1, LokiJS as true/false — coerce both.
2531
2648
  topicsUserManaged: raw.topics_user_managed === true || raw.topics_user_managed === 1,
2649
+ topics: parseTopics(raw.topics),
2650
+ topicsUpdatedAt: raw.topics_updated_at ?? null,
2532
2651
  topicsExtractedAt: raw.topics_extracted_at ?? null,
2533
2652
  topicsExtractedVersion: raw.topics_extracted_version ?? null,
2534
2653
  supersededBy: raw.superseded_by ?? null,
@@ -2819,13 +2938,23 @@ async function setMemoryEntitiesOp(ctx, memoryId, entities) {
2819
2938
  });
2820
2939
  });
2821
2940
  if (stale) return null;
2822
- const linked = entities.length > 0 ? await linkMemoryEntitiesOp(entityCtx, memoryId, entities) : [];
2941
+ const linked = entities.length > 0 ? await linkMemoryEntitiesOp(entityCtx, memoryId, entities, { topicsSource: "user" }) : [];
2823
2942
  const keep = new Set(linked.map((e) => e.uniqueId));
2824
2943
  const existing = await entityCtx.memoryEntityCollection.query(Q3.where("memory_id", memoryId)).fetch();
2825
2944
  const staleLinks = existing.filter((l) => !keep.has(String(l.entityId)));
2826
- if (staleLinks.length > 0) {
2945
+ if (staleLinks.length > 0 || entities.length === 0) {
2827
2946
  await ctx.database.write(async () => {
2828
- await ctx.database.batch(...staleLinks.map((l) => l.prepareDestroyPermanently()));
2947
+ const topicsOp = await prepareMemoryTopicsUpdate(
2948
+ entityCtx,
2949
+ memoryId,
2950
+ linked,
2951
+ entities,
2952
+ "user"
2953
+ );
2954
+ await ctx.database.batch(
2955
+ ...staleLinks.map((l) => l.prepareDestroyPermanently()),
2956
+ ...topicsOp ? [topicsOp] : []
2957
+ );
2829
2958
  });
2830
2959
  }
2831
2960
  return vaultMemoryToStored(record, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner);
@@ -2956,56 +3085,111 @@ async function getUnfiledVaultMemoriesOp(ctx) {
2956
3085
  );
2957
3086
  }
2958
3087
  var TOPICS_EXTRACTION_VERSION = 3;
3088
+ async function linkedEntityNamesByMemory(entityCtx, memoryIds) {
3089
+ const CHUNK = 500;
3090
+ const entityIdByMemory = /* @__PURE__ */ new Map();
3091
+ const allEntityIds = /* @__PURE__ */ new Set();
3092
+ for (let i = 0; i < memoryIds.length; i += CHUNK) {
3093
+ const links = await entityCtx.memoryEntityCollection.query(Q3.where("memory_id", Q3.oneOf(memoryIds.slice(i, i + CHUNK)))).unsafeFetchRaw();
3094
+ for (const link of links) {
3095
+ const memoryId = String(link.memory_id);
3096
+ const entityId = String(link.entity_id);
3097
+ allEntityIds.add(entityId);
3098
+ let bucket = entityIdByMemory.get(memoryId);
3099
+ if (!bucket) {
3100
+ bucket = /* @__PURE__ */ new Set();
3101
+ entityIdByMemory.set(memoryId, bucket);
3102
+ }
3103
+ bucket.add(entityId);
3104
+ }
3105
+ }
3106
+ const entityIds = Array.from(allEntityIds);
3107
+ const nameById = /* @__PURE__ */ new Map();
3108
+ for (let i = 0; i < entityIds.length; i += CHUNK) {
3109
+ const entities = await entityCtx.entityCollection.query(Q3.where("id", Q3.oneOf(entityIds.slice(i, i + CHUNK)))).unsafeFetchRaw();
3110
+ for (const e of entities) {
3111
+ if (typeof e.canonical_name === "string") nameById.set(String(e.id), e.canonical_name);
3112
+ }
3113
+ }
3114
+ const out = /* @__PURE__ */ new Map();
3115
+ for (const [memoryId, ids] of entityIdByMemory) {
3116
+ const names = /* @__PURE__ */ new Set();
3117
+ for (const id of ids) {
3118
+ const name = nameById.get(id);
3119
+ if (name !== void 0) names.add(name);
3120
+ }
3121
+ if (names.size > 0) out.set(memoryId, names);
3122
+ }
3123
+ return out;
3124
+ }
3125
+ function linksDivergeFromTopics(topics, linked) {
3126
+ const wanted = new Set(
3127
+ topics.map((t) => normalizeEntityName(t.name)).filter((n) => n.length > 0)
3128
+ );
3129
+ if (wanted.size !== linked.size) return true;
3130
+ for (const name of wanted) if (!linked.has(name)) return true;
3131
+ return false;
3132
+ }
2959
3133
  async function getMemoriesNeedingTopicExtractionOp(ctx, options) {
2960
3134
  const entityCtx = ctx.entityCtx;
2961
3135
  if (!entityCtx) {
2962
3136
  throw new Error("getMemoriesNeedingTopicExtractionOp requires ctx.entityCtx");
2963
3137
  }
2964
- const conditions = [
2965
- // Boolean optional column: null (legacy) and false both mean auto-managed.
2966
- // Q.notEq(true) is a NULL trap on SQLite (NULL <> TRUE is NULL) — enumerate.
2967
- Q3.or(Q3.where("topics_user_managed", null), Q3.where("topics_user_managed", false)),
2968
- ...baseVaultConditions(ctx),
2969
- Q3.sortBy("created_at", Q3.desc)
2970
- ];
3138
+ const conditions = [...baseVaultConditions(ctx), Q3.sortBy("created_at", Q3.desc)];
2971
3139
  const rows = await ctx.vaultMemoryCollection.query(...conditions).unsafeFetchRaw();
2972
- const stampedPending = [];
2973
- const unstamped = [];
3140
+ const linkedNames = await linkedEntityNamesByMemory(
3141
+ entityCtx,
3142
+ rows.map((r) => r.id)
3143
+ );
3144
+ const pendingRaw = [];
3145
+ const stampedPendingRaw = [];
3146
+ const linkedUnstampedAll = [];
3147
+ const topicsToRelinkAll = [];
3148
+ const topicsBackfillAll = [];
2974
3149
  for (const raw of rows) {
3150
+ const id = raw.id;
3151
+ const topics = parseTopics(raw.topics);
3152
+ const linked = linkedNames.get(id);
3153
+ if (topics !== null && topics.length > 0 && linksDivergeFromTopics(topics, linked ?? /* @__PURE__ */ new Set())) {
3154
+ topicsToRelinkAll.push(id);
3155
+ continue;
3156
+ }
3157
+ if (raw.topics_user_managed) {
3158
+ if (topics === null && linked !== void 0) topicsBackfillAll.push(id);
3159
+ continue;
3160
+ }
2975
3161
  const stamp = raw.topics_extracted_at ?? null;
3162
+ let isPending = false;
2976
3163
  if (stamp !== null) {
2977
3164
  const version = raw.topics_extracted_version ?? 0;
2978
3165
  if (raw.updated_at > stamp || version < TOPICS_EXTRACTION_VERSION) {
2979
- stampedPending.push(raw);
3166
+ stampedPendingRaw.push(raw);
3167
+ isPending = true;
2980
3168
  }
2981
- } else {
2982
- unstamped.push(raw);
2983
- }
2984
- }
2985
- const linkedIds = /* @__PURE__ */ new Set();
2986
- const CHUNK = 500;
2987
- for (let i = 0; i < unstamped.length; i += CHUNK) {
2988
- const ids = unstamped.slice(i, i + CHUNK).map((r) => r.id);
2989
- const links = await entityCtx.memoryEntityCollection.query(Q3.where("memory_id", Q3.oneOf(ids))).unsafeFetchRaw();
2990
- for (const link of links) linkedIds.add(String(link.memory_id));
2991
- }
2992
- const linkedUnstampedAll = [];
2993
- const pendingRaw = [...stampedPending];
2994
- for (const raw of unstamped) {
2995
- if (linkedIds.has(raw.id)) {
2996
- linkedUnstampedAll.push(raw.id);
3169
+ } else if (linked !== void 0) {
3170
+ linkedUnstampedAll.push(id);
2997
3171
  } else {
2998
3172
  pendingRaw.push(raw);
3173
+ isPending = true;
3174
+ }
3175
+ if (!isPending && topics === null && linked !== void 0) {
3176
+ topicsBackfillAll.push(id);
2999
3177
  }
3000
3178
  }
3001
3179
  const cap = options?.limit !== void 0 && options.limit > 0 ? options.limit : void 0;
3002
- const limitedPendingRaw = cap !== void 0 ? pendingRaw.slice(0, cap) : pendingRaw;
3003
- const linkedUnstamped = cap !== void 0 ? linkedUnstampedAll.slice(0, cap) : linkedUnstampedAll;
3180
+ const orderedPendingRaw = [...stampedPendingRaw, ...pendingRaw];
3181
+ const limitedPendingRaw = cap !== void 0 ? orderedPendingRaw.slice(0, cap) : orderedPendingRaw;
3004
3182
  const pending = await mapInBatches(
3005
3183
  limitedPendingRaw,
3006
3184
  (raw) => vaultMemoryRawToStored(raw, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner)
3007
3185
  );
3008
- return { pending, linkedUnstamped };
3186
+ const applyCap = (ids) => cap !== void 0 ? ids.slice(0, cap) : ids;
3187
+ return {
3188
+ pending,
3189
+ linkedUnstamped: applyCap(linkedUnstampedAll),
3190
+ topicsToRelink: applyCap(topicsToRelinkAll),
3191
+ topicsBackfill: applyCap(topicsBackfillAll)
3192
+ };
3009
3193
  }
3010
3194
  async function stampTopicsExtractedAtOp(ctx, memoryIds, extractedAt, version = TOPICS_EXTRACTION_VERSION) {
3011
3195
  if (memoryIds.length === 0) return [];
@@ -3039,6 +3223,61 @@ async function stampTopicsExtractedAtOp(ctx, memoryIds, extractedAt, version = T
3039
3223
  }
3040
3224
  return stamped;
3041
3225
  }
3226
+ async function relinkMemoryTopicsOp(ctx, memoryIds) {
3227
+ const entityCtx = ctx.entityCtx;
3228
+ if (!entityCtx) {
3229
+ throw new Error("relinkMemoryTopicsOp requires ctx.entityCtx");
3230
+ }
3231
+ const relinked = [];
3232
+ for (const id of Array.from(new Set(memoryIds))) {
3233
+ let record;
3234
+ try {
3235
+ record = await ctx.vaultMemoryCollection.find(id);
3236
+ } catch {
3237
+ continue;
3238
+ }
3239
+ if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) continue;
3240
+ const topics = parseTopics(record.topics);
3241
+ if (topics === null || topics.length === 0) continue;
3242
+ try {
3243
+ await relinkMemoryEntitiesFromTopicsOp(entityCtx, id, topics);
3244
+ relinked.push(id);
3245
+ } catch (err) {
3246
+ getLogger().warn("[memory/topics] relink failed", err);
3247
+ }
3248
+ }
3249
+ return relinked;
3250
+ }
3251
+ async function backfillMemoryTopicsOp(ctx, memoryIds) {
3252
+ const entityCtx = ctx.entityCtx;
3253
+ if (!entityCtx) {
3254
+ throw new Error("backfillMemoryTopicsOp requires ctx.entityCtx");
3255
+ }
3256
+ const filled = [];
3257
+ for (const id of Array.from(new Set(memoryIds))) {
3258
+ let record;
3259
+ try {
3260
+ record = await ctx.vaultMemoryCollection.find(id);
3261
+ } catch {
3262
+ continue;
3263
+ }
3264
+ if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) continue;
3265
+ if (parseTopics(record.topics) !== null) continue;
3266
+ const links = await entityCtx.memoryEntityCollection.query(Q3.where("memory_id", id)).unsafeFetchRaw();
3267
+ const entityIds = links.map((l) => String(l.entity_id));
3268
+ if (entityIds.length === 0) continue;
3269
+ const entities = await entityCtx.entityCollection.query(Q3.where("id", Q3.oneOf(entityIds))).fetch();
3270
+ if (entities.length === 0) continue;
3271
+ const source = record.topicsUserManaged ? "user" : "auto";
3272
+ await ctx.database.write(async () => {
3273
+ const topicsOp = await prepareMemoryTopicsUpdate(entityCtx, id, entities, [], source);
3274
+ if (!topicsOp) return;
3275
+ await ctx.database.batch(topicsOp);
3276
+ filled.push(id);
3277
+ });
3278
+ }
3279
+ return filled;
3280
+ }
3042
3281
  async function deleteAllVaultMemoriesForUserOp(ctx, userId) {
3043
3282
  if (ctx.userId !== void 0 && ctx.userId !== userId) return 0;
3044
3283
  const records = await ctx.vaultMemoryCollection.query(Q3.where("user_id", userId), Q3.where("is_deleted", false)).fetch();
@@ -6191,7 +6430,7 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
6191
6430
  legacy.map((r) => ({ id: r.uniqueId, content: r.content, score: r.similarity }))
6192
6431
  );
6193
6432
  }
6194
- const { recall: recall2 } = await import("./recall-NQNZDPNU.mjs");
6433
+ const { recall: recall2 } = await import("./recall-KTJCUAXN.mjs");
6195
6434
  let recallDegraded = [];
6196
6435
  const result = await recall2(
6197
6436
  query,
@@ -7257,6 +7496,8 @@ export {
7257
7496
  TOPICS_EXTRACTION_VERSION,
7258
7497
  getMemoriesNeedingTopicExtractionOp,
7259
7498
  stampTopicsExtractedAtOp,
7499
+ relinkMemoryTopicsOp,
7500
+ backfillMemoryTopicsOp,
7260
7501
  deleteAllVaultMemoriesForUserOp,
7261
7502
  assertVaultScopeForSweep,
7262
7503
  getDecayCandidatesRawOp,