@anuma/sdk 2.0.1 → 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
  });
@@ -21447,7 +21766,7 @@ function startObserve(entry, walletAddress) {
21447
21766
  isInitialEmission = false;
21448
21767
  }
21449
21768
  function subscribeUserSettings(database, walletAddress, listener) {
21450
- if (!walletAddress) return () => void 0;
21769
+ if (!database || !walletAddress) return () => void 0;
21451
21770
  const perDb = getOrCreatePerDb(database);
21452
21771
  let entry = perDb.get(walletAddress);
21453
21772
  if (!entry) {
@@ -21477,17 +21796,18 @@ function subscribeUserSettings(database, walletAddress, listener) {
21477
21796
  };
21478
21797
  }
21479
21798
  function getUserSettingsSnapshot(database, walletAddress) {
21480
- if (!walletAddress) return EMPTY_SNAPSHOT;
21799
+ if (!database || !walletAddress) return EMPTY_SNAPSHOT;
21481
21800
  return pool.get(database)?.get(walletAddress)?.snapshot ?? EMPTY_SNAPSHOT;
21482
21801
  }
21483
21802
  function patchUserSettingsSnapshot(database, walletAddress, patch) {
21484
- if (!walletAddress) return;
21803
+ if (!database || !walletAddress) return;
21485
21804
  const entry = pool.get(database)?.get(walletAddress);
21486
21805
  if (!entry) return;
21487
21806
  patchSnapshot(entry, patch);
21488
21807
  }
21489
21808
 
21490
21809
  // src/react/useSettings.ts
21810
+ var DB_NOT_READY = "Database not ready";
21491
21811
  function useSettings(options) {
21492
21812
  const { database, walletAddress } = options;
21493
21813
  const subscribe = (0, import_react9.useCallback)(
@@ -21500,24 +21820,25 @@ function useSettings(options) {
21500
21820
  );
21501
21821
  const snapshot = (0, import_react9.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
21502
21822
  const storageCtx = (0, import_react9.useMemo)(
21503
- () => ({
21823
+ () => database ? {
21504
21824
  database,
21505
21825
  userPreferencesCollection: database.get("userPreferences"),
21506
21826
  modelPreferencesCollection: database.get("modelPreferences")
21507
- }),
21827
+ } : null,
21508
21828
  [database]
21509
21829
  );
21510
21830
  const legacyStorageCtx = (0, import_react9.useMemo)(
21511
- () => ({
21831
+ () => database ? {
21512
21832
  database,
21513
21833
  modelPreferencesCollection: database.get("modelPreferences")
21514
- }),
21834
+ } : null,
21515
21835
  [database]
21516
21836
  );
21517
21837
  const getModelPreference = (0, import_react9.useCallback)(
21518
21838
  async (address) => {
21519
21839
  try {
21520
21840
  if (!address) throw new Error("Wallet address is required");
21841
+ if (!legacyStorageCtx) throw new Error(DB_NOT_READY);
21521
21842
  const result = await getModelPreferenceOp(legacyStorageCtx, address);
21522
21843
  return result;
21523
21844
  } catch (error) {
@@ -21530,6 +21851,7 @@ function useSettings(options) {
21530
21851
  async (address, models) => {
21531
21852
  try {
21532
21853
  if (!address) throw new Error("Wallet address is required");
21854
+ if (!legacyStorageCtx) throw new Error(DB_NOT_READY);
21533
21855
  const result = await setModelPreferenceOp(legacyStorageCtx, address, models);
21534
21856
  if (walletAddress && address === walletAddress) {
21535
21857
  patchUserSettingsSnapshot(database, walletAddress, { modelPreference: result });
@@ -21545,6 +21867,7 @@ function useSettings(options) {
21545
21867
  async (address) => {
21546
21868
  try {
21547
21869
  if (!address) throw new Error("Wallet address is required");
21870
+ if (!legacyStorageCtx) throw new Error(DB_NOT_READY);
21548
21871
  const deleted = await deleteModelPreferenceOp(legacyStorageCtx, address);
21549
21872
  if (deleted && walletAddress && address === walletAddress) {
21550
21873
  patchUserSettingsSnapshot(database, walletAddress, { modelPreference: null });
@@ -21560,6 +21883,7 @@ function useSettings(options) {
21560
21883
  async (address) => {
21561
21884
  try {
21562
21885
  if (!address) throw new Error("Wallet address is required");
21886
+ if (!storageCtx) throw new Error(DB_NOT_READY);
21563
21887
  const result = await getUserPreferenceOp(storageCtx, address);
21564
21888
  return result;
21565
21889
  } catch (error) {
@@ -21572,6 +21896,7 @@ function useSettings(options) {
21572
21896
  async (address, opts) => {
21573
21897
  try {
21574
21898
  if (!address) throw new Error("Wallet address is required");
21899
+ if (!storageCtx) throw new Error(DB_NOT_READY);
21575
21900
  const result = await setUserPreferenceOp(storageCtx, address, opts);
21576
21901
  if (walletAddress && address === walletAddress) {
21577
21902
  patchUserSettingsSnapshot(database, walletAddress, { userPreference: result });
@@ -21587,6 +21912,7 @@ function useSettings(options) {
21587
21912
  async (address, profile) => {
21588
21913
  try {
21589
21914
  if (!address) throw new Error("Wallet address is required");
21915
+ if (!storageCtx) throw new Error(DB_NOT_READY);
21590
21916
  const result = await updateProfileOp(storageCtx, address, profile);
21591
21917
  if (result && walletAddress && address === walletAddress) {
21592
21918
  patchUserSettingsSnapshot(database, walletAddress, { userPreference: result });
@@ -21602,6 +21928,7 @@ function useSettings(options) {
21602
21928
  async (address, personality) => {
21603
21929
  try {
21604
21930
  if (!address) throw new Error("Wallet address is required");
21931
+ if (!storageCtx) throw new Error(DB_NOT_READY);
21605
21932
  const result = await updatePersonalityOp(storageCtx, address, personality);
21606
21933
  if (result && walletAddress && address === walletAddress) {
21607
21934
  patchUserSettingsSnapshot(database, walletAddress, { userPreference: result });
@@ -21617,6 +21944,7 @@ function useSettings(options) {
21617
21944
  async (address, models) => {
21618
21945
  try {
21619
21946
  if (!address) throw new Error("Wallet address is required");
21947
+ if (!storageCtx) throw new Error(DB_NOT_READY);
21620
21948
  const result = await updateModelsOp(storageCtx, address, models);
21621
21949
  if (result && walletAddress && address === walletAddress) {
21622
21950
  patchUserSettingsSnapshot(database, walletAddress, { userPreference: result });
@@ -21632,6 +21960,7 @@ function useSettings(options) {
21632
21960
  async (address) => {
21633
21961
  try {
21634
21962
  if (!address) throw new Error("Wallet address is required");
21963
+ if (!storageCtx) throw new Error(DB_NOT_READY);
21635
21964
  const deleted = await deleteUserPreferenceOp(storageCtx, address);
21636
21965
  if (deleted && walletAddress && address === walletAddress) {
21637
21966
  patchUserSettingsSnapshot(database, walletAddress, { userPreference: null });
@@ -29605,6 +29934,7 @@ function renderAnumaJsx(jsx3) {
29605
29934
  StoredVaultMemoryModel,
29606
29935
  StreamExpiredError,
29607
29936
  THEME_ATTRS,
29937
+ TOPIC_EXTRACTION_BATCH_SIZE,
29608
29938
  TextProcessor,
29609
29939
  UIInteractionProvider,
29610
29940
  WalletPoller,
@@ -29696,7 +30026,9 @@ function renderAnumaJsx(jsx3) {
29696
30026
  exportElementToPdf,
29697
30027
  exportMarkdownToPdf,
29698
30028
  exportPublicKey,
30029
+ extractAndLinkEntitiesForMemoriesOp,
29699
30030
  extractAndRetain,
30031
+ extractEntitiesForMemories,
29700
30032
  extractFacts,
29701
30033
  extractFileIds,
29702
30034
  fileExists,
@@ -29752,6 +30084,7 @@ function renderAnumaJsx(jsx3) {
29752
30084
  getMediaOp,
29753
30085
  getMediaTypeFromMime,
29754
30086
  getMemoriesByEntityNamesOp,
30087
+ getMemoriesNeedingTopicExtractionOp,
29755
30088
  getNotionAccessToken,
29756
30089
  getNotionMCPUrl,
29757
30090
  getNumberAttr,
@@ -29832,6 +30165,7 @@ function renderAnumaJsx(jsx3) {
29832
30165
  renderAnumaTree,
29833
30166
  renderElementToCanvas,
29834
30167
  replaceById,
30168
+ replaceMemoryEntitiesGuardedOp,
29835
30169
  requestEncryptionKey,
29836
30170
  requestKeyPair,
29837
30171
  resolveFilePlaceholders,
@@ -29857,6 +30191,7 @@ function renderAnumaJsx(jsx3) {
29857
30191
  settingsStorageSchema,
29858
30192
  shouldChunkMessage,
29859
30193
  shouldRefreshTools,
30194
+ stampTopicsExtractedAtOp,
29860
30195
  startGithubAuth,
29861
30196
  startNotionAuth,
29862
30197
  storeCalendarPendingMessage,