@anuma/sdk 2.1.0 → 2.2.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.
@@ -4160,14 +4160,19 @@ async function upsertEntitiesOp(ctx, entities) {
4160
4160
  return out;
4161
4161
  });
4162
4162
  }
4163
- async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs) {
4163
+ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
4164
4164
  if (entityInputs.length === 0) return [];
4165
4165
  const normalized = entityInputs.map((e) => typeof e === "string" ? { name: e } : e);
4166
4166
  const byName = await upsertEntitiesOp(ctx, normalized);
4167
4167
  const entities = Array.from(byName.values());
4168
4168
  if (entities.length === 0) return [];
4169
4169
  const userId = ctx.userId;
4170
+ let skipped = false;
4170
4171
  await ctx.database.write(async () => {
4172
+ if (options?.unlessTopicsUserManaged && await autoLinkBlocked(ctx, memoryId)) {
4173
+ skipped = true;
4174
+ return;
4175
+ }
4171
4176
  const existingLinks = await ctx.memoryEntityCollection.query(import_watermelondb9.Q.where("memory_id", memoryId)).fetch();
4172
4177
  const existingEntityIds = new Set(existingLinks.map((l) => String(l.entityId)));
4173
4178
  const toCreate = entities.filter((e) => !existingEntityIds.has(e.uniqueId));
@@ -4181,7 +4186,46 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs) {
4181
4186
  );
4182
4187
  await ctx.database.batch(...prepared);
4183
4188
  });
4184
- return entities;
4189
+ return skipped ? [] : entities;
4190
+ }
4191
+ async function autoLinkBlocked(ctx, memoryId) {
4192
+ try {
4193
+ const rows = await ctx.database.get("memory_vault").query(import_watermelondb9.Q.where("id", memoryId)).fetch();
4194
+ const row = rows[0];
4195
+ return !row || !!row.isDeleted || !!row.topicsUserManaged;
4196
+ } catch {
4197
+ return true;
4198
+ }
4199
+ }
4200
+ async function replaceMemoryEntitiesGuardedOp(ctx, memoryId, entityInputs) {
4201
+ const normalized = entityInputs.map((e) => typeof e === "string" ? { name: e } : e);
4202
+ const byName = await upsertEntitiesOp(ctx, normalized);
4203
+ const entities = Array.from(byName.values());
4204
+ const userId = ctx.userId;
4205
+ let skipped = false;
4206
+ await ctx.database.write(async () => {
4207
+ if (await autoLinkBlocked(ctx, memoryId)) {
4208
+ skipped = true;
4209
+ return;
4210
+ }
4211
+ const existingLinks = await ctx.memoryEntityCollection.query(import_watermelondb9.Q.where("memory_id", memoryId)).fetch();
4212
+ const keep = new Set(entities.map((e) => e.uniqueId));
4213
+ const existingEntityIds = new Set(existingLinks.map((l) => String(l.entityId)));
4214
+ const toCreate = entities.filter((e) => !existingEntityIds.has(e.uniqueId));
4215
+ const toDestroy = existingLinks.filter((l) => !keep.has(String(l.entityId)));
4216
+ if (toCreate.length === 0 && toDestroy.length === 0) return;
4217
+ await ctx.database.batch(
4218
+ ...toCreate.map(
4219
+ (e) => ctx.memoryEntityCollection.prepareCreate((record) => {
4220
+ record._setRaw("memory_id", memoryId);
4221
+ record._setRaw("entity_id", e.uniqueId);
4222
+ if (userId !== void 0) record._setRaw("user_id", userId);
4223
+ })
4224
+ ),
4225
+ ...toDestroy.map((l) => l.prepareDestroyPermanently())
4226
+ );
4227
+ });
4228
+ return skipped ? null : entities;
4185
4229
  }
4186
4230
  async function unlinkMemoryEntitiesOp(ctx, memoryIds) {
4187
4231
  if (memoryIds.length === 0) return;
@@ -4339,6 +4383,7 @@ function vaultMemoryToStoredRaw(memory) {
4339
4383
  eventTimeEnd: memory.eventTimeEnd ?? null,
4340
4384
  eventTimeKind: memory.eventTimeKind ?? null,
4341
4385
  topicsUserManaged: memory.topicsUserManaged ?? false,
4386
+ topicsExtractedAt: memory.topicsExtractedAt ?? null,
4342
4387
  createdAt: memory.createdAt,
4343
4388
  updatedAt: memory.updatedAt,
4344
4389
  isDeleted: memory.isDeleted
@@ -4522,6 +4567,7 @@ function vaultMemoryRawToStoredRaw(raw) {
4522
4567
  eventTimeKind: raw.event_time_kind ?? null,
4523
4568
  // SQLite stores booleans as 0/1, LokiJS as true/false — coerce both.
4524
4569
  topicsUserManaged: raw.topics_user_managed === true || raw.topics_user_managed === 1,
4570
+ topicsExtractedAt: raw.topics_extracted_at ?? null,
4525
4571
  createdAt: new Date(raw.created_at),
4526
4572
  updatedAt: new Date(raw.updated_at),
4527
4573
  isDeleted: raw.is_deleted === true || raw.is_deleted === 1
@@ -4674,6 +4720,7 @@ async function clearMemoryTopicsOverrideOp(ctx, memoryId) {
4674
4720
  await ctx.database.write(async () => {
4675
4721
  await record.update((r) => {
4676
4722
  r._setRaw("topics_user_managed", false);
4723
+ r._setRaw("topics_extracted_at", null);
4677
4724
  r._setRaw("updated_at", originalUpdatedAt);
4678
4725
  });
4679
4726
  });
@@ -4717,6 +4764,87 @@ async function getUnfiledVaultMemoriesOp(ctx) {
4717
4764
  (record) => vaultMemoryToStored(record, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner)
4718
4765
  );
4719
4766
  }
4767
+ async function getMemoriesNeedingTopicExtractionOp(ctx, options) {
4768
+ const entityCtx = ctx.entityCtx;
4769
+ if (!entityCtx) {
4770
+ throw new Error("getMemoriesNeedingTopicExtractionOp requires ctx.entityCtx");
4771
+ }
4772
+ const conditions = [
4773
+ // Boolean optional column: null (legacy) and false both mean auto-managed.
4774
+ // Q.notEq(true) is a NULL trap on SQLite (NULL <> TRUE is NULL) — enumerate.
4775
+ import_watermelondb10.Q.or(import_watermelondb10.Q.where("topics_user_managed", null), import_watermelondb10.Q.where("topics_user_managed", false)),
4776
+ ...baseVaultConditions(ctx),
4777
+ import_watermelondb10.Q.sortBy("created_at", import_watermelondb10.Q.desc)
4778
+ ];
4779
+ const rows = await ctx.vaultMemoryCollection.query(...conditions).unsafeFetchRaw();
4780
+ const stampedPending = [];
4781
+ const unstamped = [];
4782
+ for (const raw of rows) {
4783
+ const stamp = raw.topics_extracted_at ?? null;
4784
+ if (stamp !== null) {
4785
+ if (raw.updated_at > stamp) stampedPending.push(raw);
4786
+ } else {
4787
+ unstamped.push(raw);
4788
+ }
4789
+ }
4790
+ const linkedIds = /* @__PURE__ */ new Set();
4791
+ const CHUNK = 500;
4792
+ for (let i = 0; i < unstamped.length; i += CHUNK) {
4793
+ const ids = unstamped.slice(i, i + CHUNK).map((r) => r.id);
4794
+ const links = await entityCtx.memoryEntityCollection.query(import_watermelondb10.Q.where("memory_id", import_watermelondb10.Q.oneOf(ids))).unsafeFetchRaw();
4795
+ for (const link of links) linkedIds.add(String(link.memory_id));
4796
+ }
4797
+ const linkedUnstampedAll = [];
4798
+ const pendingRaw = [...stampedPending];
4799
+ for (const raw of unstamped) {
4800
+ if (linkedIds.has(raw.id)) {
4801
+ linkedUnstampedAll.push(raw.id);
4802
+ } else {
4803
+ pendingRaw.push(raw);
4804
+ }
4805
+ }
4806
+ const cap = options?.limit !== void 0 && options.limit > 0 ? options.limit : void 0;
4807
+ const limitedPendingRaw = cap !== void 0 ? pendingRaw.slice(0, cap) : pendingRaw;
4808
+ const linkedUnstamped = cap !== void 0 ? linkedUnstampedAll.slice(0, cap) : linkedUnstampedAll;
4809
+ const pending = await mapInBatches(
4810
+ limitedPendingRaw,
4811
+ (raw) => vaultMemoryRawToStored(raw, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner)
4812
+ );
4813
+ return { pending, linkedUnstamped };
4814
+ }
4815
+ async function stampTopicsExtractedAtOp(ctx, memoryIds, extractedAt) {
4816
+ if (memoryIds.length === 0) return [];
4817
+ const uniqueIds = Array.from(new Set(memoryIds));
4818
+ const CHUNK = 500;
4819
+ const stamped = [];
4820
+ for (let i = 0; i < uniqueIds.length; i += CHUNK) {
4821
+ const chunkIds = uniqueIds.slice(i, i + CHUNK);
4822
+ await ctx.database.write(async () => {
4823
+ const prepared = [];
4824
+ for (const id of chunkIds) {
4825
+ let record;
4826
+ try {
4827
+ record = await ctx.vaultMemoryCollection.find(id);
4828
+ } catch {
4829
+ continue;
4830
+ }
4831
+ if (record.isDeleted || !isOwnedByCtxUser(ctx, record) || record.topicsUserManaged) {
4832
+ continue;
4833
+ }
4834
+ const originalUpdatedAt = record.updatedAt.getTime();
4835
+ prepared.push(
4836
+ record.prepareUpdate((r) => {
4837
+ r._setRaw("topics_extracted_at", extractedAt);
4838
+ r._setRaw("updated_at", originalUpdatedAt);
4839
+ })
4840
+ );
4841
+ stamped.push(id);
4842
+ }
4843
+ if (prepared.length > 0) await ctx.database.batch(...prepared);
4844
+ });
4845
+ }
4846
+ return stamped;
4847
+ }
4720
4848
  async function deleteAllVaultMemoriesForUserOp(ctx, userId) {
4721
4849
  if (ctx.userId !== void 0 && ctx.userId !== userId) return 0;
4722
4850
  const records = await ctx.vaultMemoryCollection.query(import_watermelondb10.Q.where("user_id", userId), import_watermelondb10.Q.where("is_deleted", false)).fetch();
@@ -7036,6 +7164,7 @@ __export(index_exports, {
7036
7164
  StoredVaultMemoryModel: () => VaultMemory,
7037
7165
  StreamExpiredError: () => StreamExpiredError,
7038
7166
  THEME_ATTRS: () => THEME_ATTRS,
7167
+ TOPIC_EXTRACTION_BATCH_SIZE: () => TOPIC_EXTRACTION_BATCH_SIZE,
7039
7168
  TextProcessor: () => TextProcessor,
7040
7169
  UIInteractionProvider: () => UIInteractionProvider,
7041
7170
  WalletPoller: () => WalletPoller,
@@ -7127,7 +7256,9 @@ __export(index_exports, {
7127
7256
  exportElementToPdf: () => exportElementToPdf,
7128
7257
  exportMarkdownToPdf: () => exportMarkdownToPdf,
7129
7258
  exportPublicKey: () => exportPublicKey,
7259
+ extractAndLinkEntitiesForMemoriesOp: () => extractAndLinkEntitiesForMemoriesOp,
7130
7260
  extractAndRetain: () => extractAndRetain,
7261
+ extractEntitiesForMemories: () => extractEntitiesForMemories,
7131
7262
  extractFacts: () => extractFacts,
7132
7263
  extractFileIds: () => extractFileIds,
7133
7264
  fileExists: () => fileExists,
@@ -7183,6 +7314,7 @@ __export(index_exports, {
7183
7314
  getMediaOp: () => getMediaOp,
7184
7315
  getMediaTypeFromMime: () => getMediaTypeFromMime,
7185
7316
  getMemoriesByEntityNamesOp: () => getMemoriesByEntityNamesOp,
7317
+ getMemoriesNeedingTopicExtractionOp: () => getMemoriesNeedingTopicExtractionOp,
7186
7318
  getNotionAccessToken: () => getNotionAccessToken,
7187
7319
  getNotionMCPUrl: () => getNotionMCPUrl,
7188
7320
  getNumberAttr: () => getNumberAttr,
@@ -7263,6 +7395,7 @@ __export(index_exports, {
7263
7395
  renderAnumaTree: () => renderAnumaTree,
7264
7396
  renderElementToCanvas: () => renderElementToCanvas,
7265
7397
  replaceById: () => replaceById,
7398
+ replaceMemoryEntitiesGuardedOp: () => replaceMemoryEntitiesGuardedOp,
7266
7399
  requestEncryptionKey: () => requestEncryptionKey,
7267
7400
  requestKeyPair: () => requestKeyPair,
7268
7401
  resolveFilePlaceholders: () => resolveFilePlaceholders,
@@ -7288,6 +7421,7 @@ __export(index_exports, {
7288
7421
  settingsStorageSchema: () => settingsStorageSchema,
7289
7422
  shouldChunkMessage: () => shouldChunkMessage,
7290
7423
  shouldRefreshTools: () => shouldRefreshTools,
7424
+ stampTopicsExtractedAtOp: () => stampTopicsExtractedAtOp,
7291
7425
  startGithubAuth: () => startGithubAuth,
7292
7426
  startNotionAuth: () => startNotionAuth,
7293
7427
  storeCalendarPendingMessage: () => storeCalendarPendingMessage,
@@ -13901,6 +14035,9 @@ __decorateClass([
13901
14035
  __decorateClass([
13902
14036
  (0, import_decorators3.field)("topics_user_managed")
13903
14037
  ], VaultMemory.prototype, "topicsUserManaged", 2);
14038
+ __decorateClass([
14039
+ (0, import_decorators3.field)("topics_extracted_at")
14040
+ ], VaultMemory.prototype, "topicsExtractedAt", 2);
13904
14041
  __decorateClass([
13905
14042
  import_decorators3.readonly,
13906
14043
  (0, import_decorators3.date)("created_at")
@@ -14132,7 +14269,7 @@ async function isMemoryTopicsUserManaged(ctx, memoryId) {
14132
14269
  return true;
14133
14270
  }
14134
14271
  }
14135
- var DEFAULT_MODEL3 = "gpt-oss/gpt-oss-120b";
14272
+ var DEFAULT_EXTRACTION_MODEL = "gpt-oss/gpt-oss-120b";
14136
14273
  var DEFAULT_MIN_CONFIDENCE = 0.7;
14137
14274
  var MAX_CONTENT_LENGTH = 200;
14138
14275
  var MIN_CONTENT_LENGTH = 3;
@@ -14145,6 +14282,16 @@ var FACT_TYPES = [
14145
14282
  "constraint",
14146
14283
  "other"
14147
14284
  ];
14285
+ var ENTITY_KIND_GUIDELINES = `Choose the MOST SPECIFIC kind:
14286
+ - person: a named individual ("Sara", "Dr. Lee")
14287
+ - organization: a company, team, school, or group ("Google", "the Warriors", "Stanford")
14288
+ - place: a geographic location or venue ("San Francisco", "Blue Bottle on Valencia")
14289
+ - event: a dated or nameable occurrence ("Chicago Marathon", "the Japan trip", "their wedding")
14290
+ - product: a named app, tool, brand, device, book, film, or other media title ("Linear", "iPhone", "Dune", "Ableton")
14291
+ - thing: a concrete physical object that isn't a branded product ("road bike", "sourdough starter")
14292
+ - concept: a topic, skill, field, language, or idea ("machine learning", "Spanish", "async communication")
14293
+ - other: a named entity that genuinely fits none of the above \u2014 use sparingly
14294
+ Prefer organization / product / event / place over the generic "thing"; fall back to "thing" or "other" only when no specific kind fits.`;
14148
14295
  var SYSTEM_PROMPT3 = `You extract durable user facts from conversations for a personal memory system.
14149
14296
 
14150
14297
  A "durable fact" is something the user shared about themselves that should still be true in future conversations. Examples that ARE durable:
@@ -14173,16 +14320,7 @@ For each durable fact, output:
14173
14320
  - type: one of identity | preference | relationship | plan | ongoing_context | constraint | other
14174
14321
  - confidence: 0.0-1.0; how sure you are this is durable AND true. Only include facts >= 0.7.
14175
14322
  - sourceMessageIds: which message IDs from the conversation contained the evidence
14176
- - entities: named entities mentioned, each as an object { "name": string, "kind": one of "person" | "organization" | "place" | "event" | "product" | "thing" | "concept" | "other" }. Optional \u2014 include only NAMED entities, skip generic/common nouns. Choose the MOST SPECIFIC kind:
14177
- - person: a named individual ("Sara", "Dr. Lee")
14178
- - organization: a company, team, school, or group ("Google", "the Warriors", "Stanford")
14179
- - place: a geographic location or venue ("San Francisco", "Blue Bottle on Valencia")
14180
- - event: a dated or nameable occurrence ("Chicago Marathon", "the Japan trip", "their wedding")
14181
- - product: a named app, tool, brand, device, book, film, or other media title ("Linear", "iPhone", "Dune", "Ableton")
14182
- - thing: a concrete physical object that isn't a branded product ("road bike", "sourdough starter")
14183
- - concept: a topic, skill, field, language, or idea ("machine learning", "Spanish", "async communication")
14184
- - other: a named entity that genuinely fits none of the above \u2014 use sparingly
14185
- Prefer organization / product / event / place over the generic "thing"; fall back to "thing" or "other" only when no specific kind fits. Examples:
14323
+ - entities: named entities mentioned, each as an object { "name": string, "kind": one of "person" | "organization" | "place" | "event" | "product" | "thing" | "concept" | "other" }. Optional \u2014 include only NAMED entities, skip generic/common nouns. ${ENTITY_KIND_GUIDELINES} Examples:
14186
14324
  - "started a new job at Hollowpoint Labs and I'm learning Haskell for it" \u2192 entities: [{ "name": "Hollowpoint Labs", "kind": "organization" }, { "name": "Haskell", "kind": "concept" }]
14187
14325
  - "running the Berlin Half in May and I track workouts in Whoop" \u2192 entities: [{ "name": "Berlin Half", "kind": "event" }, { "name": "Whoop", "kind": "product" }]
14188
14326
  - eventTime: when the event in this fact occurs/occurred. Resolve relative phrases ("yesterday", "next week") against the current date given in the user message. Output one of:
@@ -14208,7 +14346,7 @@ async function extractFacts(messages, options) {
14208
14346
  ...options.apiKey !== void 0 && { apiKey: options.apiKey },
14209
14347
  ...options.getToken !== void 0 && { getToken: options.getToken },
14210
14348
  ...options.baseUrl !== void 0 && { baseUrl: options.baseUrl },
14211
- model: options.model ?? DEFAULT_MODEL3,
14349
+ model: options.model ?? DEFAULT_EXTRACTION_MODEL,
14212
14350
  systemPrompt: SYSTEM_PROMPT3,
14213
14351
  userMessage: `Today's date is ${today}. Resolve any relative dates ("yesterday", "next week") against it.
14214
14352
 
@@ -14302,7 +14440,9 @@ async function extractAndRetain(messages, retainCtx, options) {
14302
14440
  if (options.entityCtx && candidate.entities.length > 0) {
14303
14441
  try {
14304
14442
  if (!await isMemoryTopicsUserManaged(options.entityCtx, result.memoryId)) {
14305
- await linkMemoryEntitiesOp(options.entityCtx, result.memoryId, candidate.entities);
14443
+ await linkMemoryEntitiesOp(options.entityCtx, result.memoryId, candidate.entities, {
14444
+ unlessTopicsUserManaged: true
14445
+ });
14306
14446
  }
14307
14447
  } catch (err) {
14308
14448
  log.warn("[memory/extract] linkMemoryEntitiesOp failed", err);
@@ -14707,7 +14847,7 @@ init_logger();
14707
14847
  init_portalLlm();
14708
14848
  init_recall();
14709
14849
  var DEFAULT_BASE_URL2 = BASE_URL;
14710
- var DEFAULT_MODEL4 = "anthropic/claude-sonnet-4-6";
14850
+ var DEFAULT_MODEL3 = "anthropic/claude-sonnet-4-6";
14711
14851
  var DEFAULT_MAX_TOKENS = 4096;
14712
14852
  var REQUEST_TIMEOUT_MS = 6e4;
14713
14853
  var DEFAULT_SYSTEM_PROMPT = `You are a personal assistant with access to the user's memory. Answer the user's question using the supplied memories as evidence.
@@ -14739,7 +14879,7 @@ async function reflect(query, ctx, options) {
14739
14879
  const evidence = recalled.memories.map((m, i) => `[${i + 1}] (id: ${m.id}, kind: ${m.kind})
14740
14880
  ${m.content}`).join("\n\n");
14741
14881
  const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL2;
14742
- const model = options.llmModel ?? DEFAULT_MODEL4;
14882
+ const model = options.llmModel ?? DEFAULT_MODEL3;
14743
14883
  const wantsStructured = !!options.responseSchema;
14744
14884
  const sendResponseFormat = wantsStructured && supportsResponseFormat(model, "json_schema");
14745
14885
  const basePrompt = options.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
@@ -14849,6 +14989,168 @@ function parseAnswer(body, base, parseSchema) {
14849
14989
  };
14850
14990
  }
14851
14991
 
14992
+ // src/lib/memory/topicExtract.ts
14993
+ init_operations2();
14994
+ init_operations3();
14995
+ init_logger();
14996
+ init_redactor();
14997
+ init_portalLlm();
14998
+ var TOPIC_EXTRACTION_BATCH_SIZE = 10;
14999
+ var MAX_CHARS_PER_MEMORY = 300;
15000
+ var MAX_VOCABULARY_NAMES = 100;
15001
+ var SYSTEM_PROMPT4 = `You assign topics to saved memories for a personal memory system.
15002
+
15003
+ Each memory is a short statement about the user. For each memory, list the NAMED entities it mentions \u2014 these become the memory's topics, used to connect related memories in a knowledge graph.
15004
+
15005
+ Include only NAMED entities, skip generic/common nouns. ${ENTITY_KIND_GUIDELINES}
15006
+
15007
+ Output strict JSON: {"memories": [{"id": string, "entities": [{"name": string, "kind": string}]}]}
15008
+ - exactly one element per input memory, echoing its id verbatim
15009
+ - "entities" may be empty when a memory mentions no named entities \u2014 most short memories have 0-3
15010
+ No prose.`;
15011
+ async function extractEntitiesForMemories(memories, options) {
15012
+ const out = /* @__PURE__ */ new Map();
15013
+ if (memories.length === 0) return out;
15014
+ const redactor = resolvePiiRedactor(options.piiRedaction);
15015
+ const vocabulary = (options.existingEntityNames ?? []).filter((n) => n.trim().length > 0).slice(0, MAX_VOCABULARY_NAMES).map((n) => redactor ? redactor.redactText(n).text : n);
15016
+ const vocabularyNote = vocabulary.length > 0 ? `The user's existing topics: ${vocabulary.join(", ")}.
15017
+ When a memory refers to one of these, reuse the EXACT existing name (same spelling and casing) instead of a variant.
15018
+
15019
+ ` : "";
15020
+ for (let i = 0; i < memories.length; i += TOPIC_EXTRACTION_BATCH_SIZE) {
15021
+ const batch = memories.slice(i, i + TOPIC_EXTRACTION_BATCH_SIZE);
15022
+ const listing = batch.map((m) => {
15023
+ const content = m.content.slice(0, MAX_CHARS_PER_MEMORY);
15024
+ return `[${m.id}] ${redactor ? redactor.redactText(content).text : content}`;
15025
+ }).join("\n");
15026
+ const parsed = await callPortalJsonCompletion({
15027
+ ...options.apiKey !== void 0 && { apiKey: options.apiKey },
15028
+ ...options.getToken !== void 0 && { getToken: options.getToken },
15029
+ ...options.baseUrl !== void 0 && { baseUrl: options.baseUrl },
15030
+ model: options.model ?? DEFAULT_EXTRACTION_MODEL,
15031
+ systemPrompt: SYSTEM_PROMPT4,
15032
+ userMessage: `${vocabularyNote}Memories:
15033
+ ${listing}
15034
+
15035
+ List each memory's named entities.`,
15036
+ tag: "memory/topics",
15037
+ ...options.fetchFn && { fetchFn: options.fetchFn },
15038
+ ...options.maxAttempts !== void 0 && { maxAttempts: options.maxAttempts },
15039
+ ...options.timeoutMs !== void 0 && { timeoutMs: options.timeoutMs },
15040
+ ...options.totalTimeoutMs !== void 0 && { totalTimeoutMs: options.totalTimeoutMs },
15041
+ ...options.backoffMs && { backoffMs: options.backoffMs }
15042
+ });
15043
+ if (parsed === null) {
15044
+ getLogger().warn(
15045
+ `[memory/topics] batch of ${batch.length} failed after retries \u2014 will retry next sweep`
15046
+ );
15047
+ continue;
15048
+ }
15049
+ const validIds = new Set(batch.map((m) => m.id));
15050
+ const byId = parseTopicResponse(parsed, validIds);
15051
+ if (byId === null) {
15052
+ getLogger().warn(
15053
+ `[memory/topics] batch of ${batch.length} returned an unrecognized shape \u2014 will retry next sweep`
15054
+ );
15055
+ continue;
15056
+ }
15057
+ for (const m of batch) {
15058
+ const entities = byId.get(m.id);
15059
+ if (entities === void 0) {
15060
+ continue;
15061
+ }
15062
+ out.set(m.id, redactor ? restoreEntities(entities, redactor) : entities);
15063
+ }
15064
+ }
15065
+ return out;
15066
+ }
15067
+ function restoreEntities(entities, redactor) {
15068
+ return entities.map((e) => ({ kind: e.kind, restored: redactor.restoreForStorage(e.name) })).filter((e) => !e.restored.unresolved).map(
15069
+ (e) => e.kind !== void 0 ? { name: e.restored.text, kind: e.kind } : { name: e.restored.text }
15070
+ );
15071
+ }
15072
+ function parseTopicResponse(parsed, validIds) {
15073
+ if (typeof parsed !== "object" || parsed === null) return null;
15074
+ const list = parsed.memories;
15075
+ if (!Array.isArray(list)) return null;
15076
+ const out = /* @__PURE__ */ new Map();
15077
+ for (const raw of list) {
15078
+ if (typeof raw !== "object" || raw === null) continue;
15079
+ const obj = raw;
15080
+ if (typeof obj.id !== "string" || !validIds.has(obj.id) || out.has(obj.id)) continue;
15081
+ const entities = Array.isArray(obj.entities) ? parseEntities(obj.entities) : [];
15082
+ out.set(obj.id, entities);
15083
+ }
15084
+ return out;
15085
+ }
15086
+ async function extractAndLinkEntitiesForMemoriesOp(ctx, memoryIds, options) {
15087
+ const entityCtx = ctx.entityCtx;
15088
+ if (!entityCtx) {
15089
+ throw new Error("extractAndLinkEntitiesForMemoriesOp requires ctx.entityCtx");
15090
+ }
15091
+ const log = getLogger();
15092
+ const extractedAt = options.now ?? Date.now();
15093
+ const skippedIds = [];
15094
+ const inputs = [];
15095
+ for (const id of memoryIds) {
15096
+ let record;
15097
+ try {
15098
+ record = await ctx.vaultMemoryCollection.find(id);
15099
+ } catch {
15100
+ skippedIds.push(id);
15101
+ continue;
15102
+ }
15103
+ if (record.isDeleted || ctx.userId !== void 0 && record.userId !== ctx.userId || record.topicsUserManaged) {
15104
+ skippedIds.push(id);
15105
+ continue;
15106
+ }
15107
+ try {
15108
+ const stored = await vaultMemoryToStored(
15109
+ record,
15110
+ ctx.walletAddress,
15111
+ ctx.signMessage,
15112
+ ctx.embeddedWalletSigner
15113
+ );
15114
+ inputs.push({ id, content: stored.content });
15115
+ } catch (err) {
15116
+ log.warn("[memory/topics] failed to load memory for extraction", err);
15117
+ skippedIds.push(id);
15118
+ }
15119
+ }
15120
+ const entitiesByMemory = await extractEntitiesForMemories(inputs, options);
15121
+ const toStamp = [];
15122
+ for (const input of inputs) {
15123
+ const entities = entitiesByMemory.get(input.id);
15124
+ if (entities === void 0) {
15125
+ skippedIds.push(input.id);
15126
+ continue;
15127
+ }
15128
+ try {
15129
+ const linked = await replaceMemoryEntitiesGuardedOp(entityCtx, input.id, entities);
15130
+ if (linked === null) {
15131
+ skippedIds.push(input.id);
15132
+ entitiesByMemory.delete(input.id);
15133
+ continue;
15134
+ }
15135
+ } catch (err) {
15136
+ log.warn("[memory/topics] replaceMemoryEntitiesGuardedOp failed", err);
15137
+ skippedIds.push(input.id);
15138
+ entitiesByMemory.delete(input.id);
15139
+ continue;
15140
+ }
15141
+ toStamp.push(input.id);
15142
+ }
15143
+ const stampedIds = await stampTopicsExtractedAtOp(ctx, toStamp, extractedAt);
15144
+ const stampedSet = new Set(stampedIds);
15145
+ for (const id of toStamp) {
15146
+ if (!stampedSet.has(id)) {
15147
+ skippedIds.push(id);
15148
+ entitiesByMemory.delete(id);
15149
+ }
15150
+ }
15151
+ return { entitiesByMemory, stampedIds, skippedIds };
15152
+ }
15153
+
14852
15154
  // src/lib/memoryEngine/index.ts
14853
15155
  init_chunking();
14854
15156
  init_embeddings();
@@ -19218,7 +19520,7 @@ __decorateClass([
19218
19520
  ], VaultFolder.prototype, "context", 2);
19219
19521
 
19220
19522
  // src/lib/db/schema.ts
19221
- var SDK_SCHEMA_VERSION = 35;
19523
+ var SDK_SCHEMA_VERSION = 36;
19222
19524
  var sdkSchema = (0, import_watermelondb20.appSchema)({
19223
19525
  version: SDK_SCHEMA_VERSION,
19224
19526
  tables: [
@@ -19339,7 +19641,11 @@ var sdkSchema = (0, import_watermelondb20.appSchema)({
19339
19641
  // When true, the user has manually set this memory's topics (entity
19340
19642
  // links). Auto-extraction then leaves its links alone — the user owns
19341
19643
  // them. Null/false = topics are auto-derived (default).
19342
- { name: "topics_user_managed", type: "boolean", isOptional: true }
19644
+ { name: "topics_user_managed", type: "boolean", isOptional: true },
19645
+ // Unix ms of the last LLM topic-extraction pass over this memory's
19646
+ // content. Null = never extracted standalone (legacy rows with entity
19647
+ // links are grandfathered — see getMemoriesNeedingTopicExtractionOp).
19648
+ { name: "topics_extracted_at", type: "number", isOptional: true }
19343
19649
  ]
19344
19650
  }),
19345
19651
  // Entity table — canonical names extracted from auto-extraction (W5).
@@ -19937,6 +20243,19 @@ var sdkMigrations = (0, import_migrations2.schemaMigrations)({
19937
20243
  ]
19938
20244
  })
19939
20245
  ]
20246
+ },
20247
+ // v35 -> v36: topics_extracted_at watermark on memory_vault. Existing rows
20248
+ // keep it NULL — rows that already have entity links are grandfathered as
20249
+ // extracted (no mass re-extraction on upgrade), rows without links are the
20250
+ // backfill target. See getMemoriesNeedingTopicExtractionOp.
20251
+ {
20252
+ toVersion: 36,
20253
+ steps: [
20254
+ (0, import_migrations2.addColumns)({
20255
+ table: "memory_vault",
20256
+ columns: [{ name: "topics_extracted_at", type: "number", isOptional: true }]
20257
+ })
20258
+ ]
19940
20259
  }
19941
20260
  ]
19942
20261
  });
@@ -29615,6 +29934,7 @@ function renderAnumaJsx(jsx3) {
29615
29934
  StoredVaultMemoryModel,
29616
29935
  StreamExpiredError,
29617
29936
  THEME_ATTRS,
29937
+ TOPIC_EXTRACTION_BATCH_SIZE,
29618
29938
  TextProcessor,
29619
29939
  UIInteractionProvider,
29620
29940
  WalletPoller,
@@ -29706,7 +30026,9 @@ function renderAnumaJsx(jsx3) {
29706
30026
  exportElementToPdf,
29707
30027
  exportMarkdownToPdf,
29708
30028
  exportPublicKey,
30029
+ extractAndLinkEntitiesForMemoriesOp,
29709
30030
  extractAndRetain,
30031
+ extractEntitiesForMemories,
29710
30032
  extractFacts,
29711
30033
  extractFileIds,
29712
30034
  fileExists,
@@ -29762,6 +30084,7 @@ function renderAnumaJsx(jsx3) {
29762
30084
  getMediaOp,
29763
30085
  getMediaTypeFromMime,
29764
30086
  getMemoriesByEntityNamesOp,
30087
+ getMemoriesNeedingTopicExtractionOp,
29765
30088
  getNotionAccessToken,
29766
30089
  getNotionMCPUrl,
29767
30090
  getNumberAttr,
@@ -29842,6 +30165,7 @@ function renderAnumaJsx(jsx3) {
29842
30165
  renderAnumaTree,
29843
30166
  renderElementToCanvas,
29844
30167
  replaceById,
30168
+ replaceMemoryEntitiesGuardedOp,
29845
30169
  requestEncryptionKey,
29846
30170
  requestKeyPair,
29847
30171
  resolveFilePlaceholders,
@@ -29867,6 +30191,7 @@ function renderAnumaJsx(jsx3) {
29867
30191
  settingsStorageSchema,
29868
30192
  shouldChunkMessage,
29869
30193
  shouldRefreshTools,
30194
+ stampTopicsExtractedAtOp,
29870
30195
  startGithubAuth,
29871
30196
  startNotionAuth,
29872
30197
  storeCalendarPendingMessage,