@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.
@@ -72,6 +72,7 @@ import {
72
72
  getEncryptionKey,
73
73
  getImageModel,
74
74
  getMemoriesByEntityNamesOp,
75
+ getMemoriesNeedingTopicExtractionOp,
75
76
  getMessageCountOp,
76
77
  getMessageSkeletonsOp,
77
78
  getMessagesOp,
@@ -101,6 +102,7 @@ import {
101
102
  postAuthOauthByProviderRevoke,
102
103
  preEmbedVaultMemories,
103
104
  recall,
105
+ replaceMemoryEntitiesGuardedOp,
104
106
  requestEncryptionKey,
105
107
  requestKeyPair,
106
108
  resolveStoredUserContent,
@@ -109,6 +111,7 @@ import {
109
111
  searchVaultMemories,
110
112
  setMemoryEntitiesOp,
111
113
  shouldChunkMessage,
114
+ stampTopicsExtractedAtOp,
112
115
  stripToolCalls,
113
116
  updateConversationPinnedOp,
114
117
  updateConversationProjectOp,
@@ -120,8 +123,9 @@ import {
120
123
  updateMessageFileIdsOp,
121
124
  updateVaultMemoryEmbeddingOp,
122
125
  updateVaultMemoryOp,
123
- useEncryption
124
- } from "./chunk-63IW5BS5.mjs";
126
+ useEncryption,
127
+ vaultMemoryToStored
128
+ } from "./chunk-KRNJVLI6.mjs";
125
129
  import {
126
130
  PiiRedactor,
127
131
  createStreamingDeAnonymizer,
@@ -6659,6 +6663,9 @@ __decorateClass([
6659
6663
  __decorateClass([
6660
6664
  field3("topics_user_managed")
6661
6665
  ], VaultMemory.prototype, "topicsUserManaged", 2);
6666
+ __decorateClass([
6667
+ field3("topics_extracted_at")
6668
+ ], VaultMemory.prototype, "topicsExtractedAt", 2);
6662
6669
  __decorateClass([
6663
6670
  readonly2,
6664
6671
  date3("created_at")
@@ -6875,7 +6882,7 @@ async function isMemoryTopicsUserManaged(ctx, memoryId) {
6875
6882
  return true;
6876
6883
  }
6877
6884
  }
6878
- var DEFAULT_MODEL = "gpt-oss/gpt-oss-120b";
6885
+ var DEFAULT_EXTRACTION_MODEL = "gpt-oss/gpt-oss-120b";
6879
6886
  var DEFAULT_MIN_CONFIDENCE = 0.7;
6880
6887
  var MAX_CONTENT_LENGTH = 200;
6881
6888
  var MIN_CONTENT_LENGTH = 3;
@@ -6888,6 +6895,16 @@ var FACT_TYPES = [
6888
6895
  "constraint",
6889
6896
  "other"
6890
6897
  ];
6898
+ var ENTITY_KIND_GUIDELINES = `Choose the MOST SPECIFIC kind:
6899
+ - person: a named individual ("Sara", "Dr. Lee")
6900
+ - organization: a company, team, school, or group ("Google", "the Warriors", "Stanford")
6901
+ - place: a geographic location or venue ("San Francisco", "Blue Bottle on Valencia")
6902
+ - event: a dated or nameable occurrence ("Chicago Marathon", "the Japan trip", "their wedding")
6903
+ - product: a named app, tool, brand, device, book, film, or other media title ("Linear", "iPhone", "Dune", "Ableton")
6904
+ - thing: a concrete physical object that isn't a branded product ("road bike", "sourdough starter")
6905
+ - concept: a topic, skill, field, language, or idea ("machine learning", "Spanish", "async communication")
6906
+ - other: a named entity that genuinely fits none of the above \u2014 use sparingly
6907
+ Prefer organization / product / event / place over the generic "thing"; fall back to "thing" or "other" only when no specific kind fits.`;
6891
6908
  var SYSTEM_PROMPT = `You extract durable user facts from conversations for a personal memory system.
6892
6909
 
6893
6910
  A "durable fact" is something the user shared about themselves that should still be true in future conversations. Examples that ARE durable:
@@ -6916,16 +6933,7 @@ For each durable fact, output:
6916
6933
  - type: one of identity | preference | relationship | plan | ongoing_context | constraint | other
6917
6934
  - confidence: 0.0-1.0; how sure you are this is durable AND true. Only include facts >= 0.7.
6918
6935
  - sourceMessageIds: which message IDs from the conversation contained the evidence
6919
- - 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:
6920
- - person: a named individual ("Sara", "Dr. Lee")
6921
- - organization: a company, team, school, or group ("Google", "the Warriors", "Stanford")
6922
- - place: a geographic location or venue ("San Francisco", "Blue Bottle on Valencia")
6923
- - event: a dated or nameable occurrence ("Chicago Marathon", "the Japan trip", "their wedding")
6924
- - product: a named app, tool, brand, device, book, film, or other media title ("Linear", "iPhone", "Dune", "Ableton")
6925
- - thing: a concrete physical object that isn't a branded product ("road bike", "sourdough starter")
6926
- - concept: a topic, skill, field, language, or idea ("machine learning", "Spanish", "async communication")
6927
- - other: a named entity that genuinely fits none of the above \u2014 use sparingly
6928
- Prefer organization / product / event / place over the generic "thing"; fall back to "thing" or "other" only when no specific kind fits. Examples:
6936
+ - 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:
6929
6937
  - "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" }]
6930
6938
  - "running the Berlin Half in May and I track workouts in Whoop" \u2192 entities: [{ "name": "Berlin Half", "kind": "event" }, { "name": "Whoop", "kind": "product" }]
6931
6939
  - 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:
@@ -6951,7 +6959,7 @@ async function extractFacts(messages, options) {
6951
6959
  ...options.apiKey !== void 0 && { apiKey: options.apiKey },
6952
6960
  ...options.getToken !== void 0 && { getToken: options.getToken },
6953
6961
  ...options.baseUrl !== void 0 && { baseUrl: options.baseUrl },
6954
- model: options.model ?? DEFAULT_MODEL,
6962
+ model: options.model ?? DEFAULT_EXTRACTION_MODEL,
6955
6963
  systemPrompt: SYSTEM_PROMPT,
6956
6964
  userMessage: `Today's date is ${today}. Resolve any relative dates ("yesterday", "next week") against it.
6957
6965
 
@@ -7045,7 +7053,9 @@ async function extractAndRetain(messages, retainCtx, options) {
7045
7053
  if (options.entityCtx && candidate.entities.length > 0) {
7046
7054
  try {
7047
7055
  if (!await isMemoryTopicsUserManaged(options.entityCtx, result.memoryId)) {
7048
- await linkMemoryEntitiesOp(options.entityCtx, result.memoryId, candidate.entities);
7056
+ await linkMemoryEntitiesOp(options.entityCtx, result.memoryId, candidate.entities, {
7057
+ unlessTopicsUserManaged: true
7058
+ });
7049
7059
  }
7050
7060
  } catch (err) {
7051
7061
  log.warn("[memory/extract] linkMemoryEntitiesOp failed", err);
@@ -7440,7 +7450,7 @@ function createRecallTool(ctx, toolOptions, callbacks) {
7440
7450
 
7441
7451
  // src/lib/memory/reflect.ts
7442
7452
  var DEFAULT_BASE_URL = BASE_URL;
7443
- var DEFAULT_MODEL2 = "anthropic/claude-sonnet-4-6";
7453
+ var DEFAULT_MODEL = "anthropic/claude-sonnet-4-6";
7444
7454
  var DEFAULT_MAX_TOKENS = 4096;
7445
7455
  var REQUEST_TIMEOUT_MS = 6e4;
7446
7456
  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.
@@ -7472,7 +7482,7 @@ async function reflect(query, ctx, options) {
7472
7482
  const evidence = recalled.memories.map((m, i) => `[${i + 1}] (id: ${m.id}, kind: ${m.kind})
7473
7483
  ${m.content}`).join("\n\n");
7474
7484
  const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
7475
- const model = options.llmModel ?? DEFAULT_MODEL2;
7485
+ const model = options.llmModel ?? DEFAULT_MODEL;
7476
7486
  const wantsStructured = !!options.responseSchema;
7477
7487
  const sendResponseFormat = wantsStructured && supportsResponseFormat(model, "json_schema");
7478
7488
  const basePrompt = options.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
@@ -7582,6 +7592,163 @@ function parseAnswer(body, base, parseSchema) {
7582
7592
  };
7583
7593
  }
7584
7594
 
7595
+ // src/lib/memory/topicExtract.ts
7596
+ var TOPIC_EXTRACTION_BATCH_SIZE = 10;
7597
+ var MAX_CHARS_PER_MEMORY = 300;
7598
+ var MAX_VOCABULARY_NAMES = 100;
7599
+ var SYSTEM_PROMPT2 = `You assign topics to saved memories for a personal memory system.
7600
+
7601
+ 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.
7602
+
7603
+ Include only NAMED entities, skip generic/common nouns. ${ENTITY_KIND_GUIDELINES}
7604
+
7605
+ Output strict JSON: {"memories": [{"id": string, "entities": [{"name": string, "kind": string}]}]}
7606
+ - exactly one element per input memory, echoing its id verbatim
7607
+ - "entities" may be empty when a memory mentions no named entities \u2014 most short memories have 0-3
7608
+ No prose.`;
7609
+ async function extractEntitiesForMemories(memories, options) {
7610
+ const out = /* @__PURE__ */ new Map();
7611
+ if (memories.length === 0) return out;
7612
+ const redactor = resolvePiiRedactor(options.piiRedaction);
7613
+ const vocabulary = (options.existingEntityNames ?? []).filter((n) => n.trim().length > 0).slice(0, MAX_VOCABULARY_NAMES).map((n) => redactor ? redactor.redactText(n).text : n);
7614
+ const vocabularyNote = vocabulary.length > 0 ? `The user's existing topics: ${vocabulary.join(", ")}.
7615
+ When a memory refers to one of these, reuse the EXACT existing name (same spelling and casing) instead of a variant.
7616
+
7617
+ ` : "";
7618
+ for (let i = 0; i < memories.length; i += TOPIC_EXTRACTION_BATCH_SIZE) {
7619
+ const batch = memories.slice(i, i + TOPIC_EXTRACTION_BATCH_SIZE);
7620
+ const listing = batch.map((m) => {
7621
+ const content = m.content.slice(0, MAX_CHARS_PER_MEMORY);
7622
+ return `[${m.id}] ${redactor ? redactor.redactText(content).text : content}`;
7623
+ }).join("\n");
7624
+ const parsed = await callPortalJsonCompletion({
7625
+ ...options.apiKey !== void 0 && { apiKey: options.apiKey },
7626
+ ...options.getToken !== void 0 && { getToken: options.getToken },
7627
+ ...options.baseUrl !== void 0 && { baseUrl: options.baseUrl },
7628
+ model: options.model ?? DEFAULT_EXTRACTION_MODEL,
7629
+ systemPrompt: SYSTEM_PROMPT2,
7630
+ userMessage: `${vocabularyNote}Memories:
7631
+ ${listing}
7632
+
7633
+ List each memory's named entities.`,
7634
+ tag: "memory/topics",
7635
+ ...options.fetchFn && { fetchFn: options.fetchFn },
7636
+ ...options.maxAttempts !== void 0 && { maxAttempts: options.maxAttempts },
7637
+ ...options.timeoutMs !== void 0 && { timeoutMs: options.timeoutMs },
7638
+ ...options.totalTimeoutMs !== void 0 && { totalTimeoutMs: options.totalTimeoutMs },
7639
+ ...options.backoffMs && { backoffMs: options.backoffMs }
7640
+ });
7641
+ if (parsed === null) {
7642
+ getLogger().warn(
7643
+ `[memory/topics] batch of ${batch.length} failed after retries \u2014 will retry next sweep`
7644
+ );
7645
+ continue;
7646
+ }
7647
+ const validIds = new Set(batch.map((m) => m.id));
7648
+ const byId = parseTopicResponse(parsed, validIds);
7649
+ if (byId === null) {
7650
+ getLogger().warn(
7651
+ `[memory/topics] batch of ${batch.length} returned an unrecognized shape \u2014 will retry next sweep`
7652
+ );
7653
+ continue;
7654
+ }
7655
+ for (const m of batch) {
7656
+ const entities = byId.get(m.id);
7657
+ if (entities === void 0) {
7658
+ continue;
7659
+ }
7660
+ out.set(m.id, redactor ? restoreEntities(entities, redactor) : entities);
7661
+ }
7662
+ }
7663
+ return out;
7664
+ }
7665
+ function restoreEntities(entities, redactor) {
7666
+ return entities.map((e) => ({ kind: e.kind, restored: redactor.restoreForStorage(e.name) })).filter((e) => !e.restored.unresolved).map(
7667
+ (e) => e.kind !== void 0 ? { name: e.restored.text, kind: e.kind } : { name: e.restored.text }
7668
+ );
7669
+ }
7670
+ function parseTopicResponse(parsed, validIds) {
7671
+ if (typeof parsed !== "object" || parsed === null) return null;
7672
+ const list = parsed.memories;
7673
+ if (!Array.isArray(list)) return null;
7674
+ const out = /* @__PURE__ */ new Map();
7675
+ for (const raw of list) {
7676
+ if (typeof raw !== "object" || raw === null) continue;
7677
+ const obj = raw;
7678
+ if (typeof obj.id !== "string" || !validIds.has(obj.id) || out.has(obj.id)) continue;
7679
+ const entities = Array.isArray(obj.entities) ? parseEntities(obj.entities) : [];
7680
+ out.set(obj.id, entities);
7681
+ }
7682
+ return out;
7683
+ }
7684
+ async function extractAndLinkEntitiesForMemoriesOp(ctx, memoryIds, options) {
7685
+ const entityCtx = ctx.entityCtx;
7686
+ if (!entityCtx) {
7687
+ throw new Error("extractAndLinkEntitiesForMemoriesOp requires ctx.entityCtx");
7688
+ }
7689
+ const log = getLogger();
7690
+ const extractedAt = options.now ?? Date.now();
7691
+ const skippedIds = [];
7692
+ const inputs = [];
7693
+ for (const id of memoryIds) {
7694
+ let record;
7695
+ try {
7696
+ record = await ctx.vaultMemoryCollection.find(id);
7697
+ } catch {
7698
+ skippedIds.push(id);
7699
+ continue;
7700
+ }
7701
+ if (record.isDeleted || ctx.userId !== void 0 && record.userId !== ctx.userId || record.topicsUserManaged) {
7702
+ skippedIds.push(id);
7703
+ continue;
7704
+ }
7705
+ try {
7706
+ const stored = await vaultMemoryToStored(
7707
+ record,
7708
+ ctx.walletAddress,
7709
+ ctx.signMessage,
7710
+ ctx.embeddedWalletSigner
7711
+ );
7712
+ inputs.push({ id, content: stored.content });
7713
+ } catch (err) {
7714
+ log.warn("[memory/topics] failed to load memory for extraction", err);
7715
+ skippedIds.push(id);
7716
+ }
7717
+ }
7718
+ const entitiesByMemory = await extractEntitiesForMemories(inputs, options);
7719
+ const toStamp = [];
7720
+ for (const input of inputs) {
7721
+ const entities = entitiesByMemory.get(input.id);
7722
+ if (entities === void 0) {
7723
+ skippedIds.push(input.id);
7724
+ continue;
7725
+ }
7726
+ try {
7727
+ const linked = await replaceMemoryEntitiesGuardedOp(entityCtx, input.id, entities);
7728
+ if (linked === null) {
7729
+ skippedIds.push(input.id);
7730
+ entitiesByMemory.delete(input.id);
7731
+ continue;
7732
+ }
7733
+ } catch (err) {
7734
+ log.warn("[memory/topics] replaceMemoryEntitiesGuardedOp failed", err);
7735
+ skippedIds.push(input.id);
7736
+ entitiesByMemory.delete(input.id);
7737
+ continue;
7738
+ }
7739
+ toStamp.push(input.id);
7740
+ }
7741
+ const stampedIds = await stampTopicsExtractedAtOp(ctx, toStamp, extractedAt);
7742
+ const stampedSet = new Set(stampedIds);
7743
+ for (const id of toStamp) {
7744
+ if (!stampedSet.has(id)) {
7745
+ skippedIds.push(id);
7746
+ entitiesByMemory.delete(id);
7747
+ }
7748
+ }
7749
+ return { entitiesByMemory, stampedIds, skippedIds };
7750
+ }
7751
+
7585
7752
  // src/lib/memoryEngine/quantization.ts
7586
7753
  var INT8_MAX = 127;
7587
7754
  function quantizeEmbedding(v) {
@@ -11921,7 +12088,7 @@ __decorateClass([
11921
12088
  ], VaultFolder.prototype, "context", 2);
11922
12089
 
11923
12090
  // src/lib/db/schema.ts
11924
- var SDK_SCHEMA_VERSION = 35;
12091
+ var SDK_SCHEMA_VERSION = 36;
11925
12092
  var sdkSchema = appSchema2({
11926
12093
  version: SDK_SCHEMA_VERSION,
11927
12094
  tables: [
@@ -12042,7 +12209,11 @@ var sdkSchema = appSchema2({
12042
12209
  // When true, the user has manually set this memory's topics (entity
12043
12210
  // links). Auto-extraction then leaves its links alone — the user owns
12044
12211
  // them. Null/false = topics are auto-derived (default).
12045
- { name: "topics_user_managed", type: "boolean", isOptional: true }
12212
+ { name: "topics_user_managed", type: "boolean", isOptional: true },
12213
+ // Unix ms of the last LLM topic-extraction pass over this memory's
12214
+ // content. Null = never extracted standalone (legacy rows with entity
12215
+ // links are grandfathered — see getMemoriesNeedingTopicExtractionOp).
12216
+ { name: "topics_extracted_at", type: "number", isOptional: true }
12046
12217
  ]
12047
12218
  }),
12048
12219
  // Entity table — canonical names extracted from auto-extraction (W5).
@@ -12640,6 +12811,19 @@ var sdkMigrations = schemaMigrations2({
12640
12811
  ]
12641
12812
  })
12642
12813
  ]
12814
+ },
12815
+ // v35 -> v36: topics_extracted_at watermark on memory_vault. Existing rows
12816
+ // keep it NULL — rows that already have entity links are grandfathered as
12817
+ // extracted (no mass re-extraction on upgrade), rows without links are the
12818
+ // backfill target. See getMemoriesNeedingTopicExtractionOp.
12819
+ {
12820
+ toVersion: 36,
12821
+ steps: [
12822
+ addColumns2({
12823
+ table: "memory_vault",
12824
+ columns: [{ name: "topics_extracted_at", type: "number", isOptional: true }]
12825
+ })
12826
+ ]
12643
12827
  }
12644
12828
  ]
12645
12829
  });
@@ -22293,6 +22477,7 @@ export {
22293
22477
  VaultMemory as StoredVaultMemoryModel,
22294
22478
  StreamExpiredError,
22295
22479
  THEME_ATTRS,
22480
+ TOPIC_EXTRACTION_BATCH_SIZE,
22296
22481
  TextProcessor,
22297
22482
  UIInteractionProvider,
22298
22483
  WalletPoller,
@@ -22384,7 +22569,9 @@ export {
22384
22569
  exportElementToPdf,
22385
22570
  exportMarkdownToPdf,
22386
22571
  exportPublicKey,
22572
+ extractAndLinkEntitiesForMemoriesOp,
22387
22573
  extractAndRetain,
22574
+ extractEntitiesForMemories,
22388
22575
  extractFacts,
22389
22576
  extractFileIds,
22390
22577
  fileExists,
@@ -22440,6 +22627,7 @@ export {
22440
22627
  getMediaOp,
22441
22628
  getMediaTypeFromMime,
22442
22629
  getMemoriesByEntityNamesOp,
22630
+ getMemoriesNeedingTopicExtractionOp,
22443
22631
  getNotionAccessToken,
22444
22632
  getNotionMCPUrl,
22445
22633
  getNumberAttr,
@@ -22520,6 +22708,7 @@ export {
22520
22708
  renderAnumaTree,
22521
22709
  renderElementToCanvas,
22522
22710
  replaceById,
22711
+ replaceMemoryEntitiesGuardedOp,
22523
22712
  requestEncryptionKey,
22524
22713
  requestKeyPair,
22525
22714
  resolveFilePlaceholders,
@@ -22545,6 +22734,7 @@ export {
22545
22734
  settingsStorageSchema,
22546
22735
  shouldChunkMessage,
22547
22736
  shouldRefreshTools,
22737
+ stampTopicsExtractedAtOp,
22548
22738
  startGithubAuth,
22549
22739
  startNotionAuth,
22550
22740
  storeCalendarPendingMessage,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  recall
3
- } from "./chunk-63IW5BS5.mjs";
3
+ } from "./chunk-KRNJVLI6.mjs";
4
4
  import "./chunk-DFVCS3QQ.mjs";
5
5
  import "./chunk-GYKJOWEO.mjs";
6
6
  export {
@@ -15849,14 +15849,19 @@ async function upsertEntitiesOp(ctx, entities) {
15849
15849
  return out;
15850
15850
  });
15851
15851
  }
15852
- async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs) {
15852
+ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
15853
15853
  if (entityInputs.length === 0) return [];
15854
15854
  const normalized = entityInputs.map((e) => typeof e === "string" ? { name: e } : e);
15855
15855
  const byName = await upsertEntitiesOp(ctx, normalized);
15856
15856
  const entities = Array.from(byName.values());
15857
15857
  if (entities.length === 0) return [];
15858
15858
  const userId = ctx.userId;
15859
+ let skipped = false;
15859
15860
  await ctx.database.write(async () => {
15861
+ if (options?.unlessTopicsUserManaged && await autoLinkBlocked(ctx, memoryId)) {
15862
+ skipped = true;
15863
+ return;
15864
+ }
15860
15865
  const existingLinks = await ctx.memoryEntityCollection.query(import_watermelondb2.Q.where("memory_id", memoryId)).fetch();
15861
15866
  const existingEntityIds = new Set(existingLinks.map((l) => String(l.entityId)));
15862
15867
  const toCreate = entities.filter((e) => !existingEntityIds.has(e.uniqueId));
@@ -15870,7 +15875,46 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs) {
15870
15875
  );
15871
15876
  await ctx.database.batch(...prepared);
15872
15877
  });
15873
- return entities;
15878
+ return skipped ? [] : entities;
15879
+ }
15880
+ async function autoLinkBlocked(ctx, memoryId) {
15881
+ try {
15882
+ const rows = await ctx.database.get("memory_vault").query(import_watermelondb2.Q.where("id", memoryId)).fetch();
15883
+ const row = rows[0];
15884
+ return !row || !!row.isDeleted || !!row.topicsUserManaged;
15885
+ } catch {
15886
+ return true;
15887
+ }
15888
+ }
15889
+ async function replaceMemoryEntitiesGuardedOp(ctx, memoryId, entityInputs) {
15890
+ const normalized = entityInputs.map((e) => typeof e === "string" ? { name: e } : e);
15891
+ const byName = await upsertEntitiesOp(ctx, normalized);
15892
+ const entities = Array.from(byName.values());
15893
+ const userId = ctx.userId;
15894
+ let skipped = false;
15895
+ await ctx.database.write(async () => {
15896
+ if (await autoLinkBlocked(ctx, memoryId)) {
15897
+ skipped = true;
15898
+ return;
15899
+ }
15900
+ const existingLinks = await ctx.memoryEntityCollection.query(import_watermelondb2.Q.where("memory_id", memoryId)).fetch();
15901
+ const keep = new Set(entities.map((e) => e.uniqueId));
15902
+ const existingEntityIds = new Set(existingLinks.map((l) => String(l.entityId)));
15903
+ const toCreate = entities.filter((e) => !existingEntityIds.has(e.uniqueId));
15904
+ const toDestroy = existingLinks.filter((l) => !keep.has(String(l.entityId)));
15905
+ if (toCreate.length === 0 && toDestroy.length === 0) return;
15906
+ await ctx.database.batch(
15907
+ ...toCreate.map(
15908
+ (e) => ctx.memoryEntityCollection.prepareCreate((record) => {
15909
+ record._setRaw("memory_id", memoryId);
15910
+ record._setRaw("entity_id", e.uniqueId);
15911
+ if (userId !== void 0) record._setRaw("user_id", userId);
15912
+ })
15913
+ ),
15914
+ ...toDestroy.map((l) => l.prepareDestroyPermanently())
15915
+ );
15916
+ });
15917
+ return skipped ? null : entities;
15874
15918
  }
15875
15919
  async function unlinkMemoryEntitiesOp(ctx, memoryIds) {
15876
15920
  if (memoryIds.length === 0) return;
@@ -15993,6 +16037,7 @@ function vaultMemoryToStoredRaw(memory) {
15993
16037
  eventTimeEnd: memory.eventTimeEnd ?? null,
15994
16038
  eventTimeKind: memory.eventTimeKind ?? null,
15995
16039
  topicsUserManaged: memory.topicsUserManaged ?? false,
16040
+ topicsExtractedAt: memory.topicsExtractedAt ?? null,
15996
16041
  createdAt: memory.createdAt,
15997
16042
  updatedAt: memory.updatedAt,
15998
16043
  isDeleted: memory.isDeleted
@@ -16176,6 +16221,7 @@ function vaultMemoryRawToStoredRaw(raw) {
16176
16221
  eventTimeKind: raw.event_time_kind ?? null,
16177
16222
  // SQLite stores booleans as 0/1, LokiJS as true/false — coerce both.
16178
16223
  topicsUserManaged: raw.topics_user_managed === true || raw.topics_user_managed === 1,
16224
+ topicsExtractedAt: raw.topics_extracted_at ?? null,
16179
16225
  createdAt: new Date(raw.created_at),
16180
16226
  updatedAt: new Date(raw.updated_at),
16181
16227
  isDeleted: raw.is_deleted === true || raw.is_deleted === 1
@@ -16318,6 +16364,87 @@ async function getUnfiledVaultMemoriesOp(ctx) {
16318
16364
  (record) => vaultMemoryToStored(record, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner)
16319
16365
  );
16320
16366
  }
16367
+ async function getMemoriesNeedingTopicExtractionOp(ctx, options) {
16368
+ const entityCtx = ctx.entityCtx;
16369
+ if (!entityCtx) {
16370
+ throw new Error("getMemoriesNeedingTopicExtractionOp requires ctx.entityCtx");
16371
+ }
16372
+ const conditions = [
16373
+ // Boolean optional column: null (legacy) and false both mean auto-managed.
16374
+ // Q.notEq(true) is a NULL trap on SQLite (NULL <> TRUE is NULL) — enumerate.
16375
+ import_watermelondb3.Q.or(import_watermelondb3.Q.where("topics_user_managed", null), import_watermelondb3.Q.where("topics_user_managed", false)),
16376
+ ...baseVaultConditions(ctx),
16377
+ import_watermelondb3.Q.sortBy("created_at", import_watermelondb3.Q.desc)
16378
+ ];
16379
+ const rows = await ctx.vaultMemoryCollection.query(...conditions).unsafeFetchRaw();
16380
+ const stampedPending = [];
16381
+ const unstamped = [];
16382
+ for (const raw of rows) {
16383
+ const stamp = raw.topics_extracted_at ?? null;
16384
+ if (stamp !== null) {
16385
+ if (raw.updated_at > stamp) stampedPending.push(raw);
16386
+ } else {
16387
+ unstamped.push(raw);
16388
+ }
16389
+ }
16390
+ const linkedIds = /* @__PURE__ */ new Set();
16391
+ const CHUNK = 500;
16392
+ for (let i = 0; i < unstamped.length; i += CHUNK) {
16393
+ const ids = unstamped.slice(i, i + CHUNK).map((r) => r.id);
16394
+ const links = await entityCtx.memoryEntityCollection.query(import_watermelondb3.Q.where("memory_id", import_watermelondb3.Q.oneOf(ids))).unsafeFetchRaw();
16395
+ for (const link of links) linkedIds.add(String(link.memory_id));
16396
+ }
16397
+ const linkedUnstampedAll = [];
16398
+ const pendingRaw = [...stampedPending];
16399
+ for (const raw of unstamped) {
16400
+ if (linkedIds.has(raw.id)) {
16401
+ linkedUnstampedAll.push(raw.id);
16402
+ } else {
16403
+ pendingRaw.push(raw);
16404
+ }
16405
+ }
16406
+ const cap = options?.limit !== void 0 && options.limit > 0 ? options.limit : void 0;
16407
+ const limitedPendingRaw = cap !== void 0 ? pendingRaw.slice(0, cap) : pendingRaw;
16408
+ const linkedUnstamped = cap !== void 0 ? linkedUnstampedAll.slice(0, cap) : linkedUnstampedAll;
16409
+ const pending = await mapInBatches(
16410
+ limitedPendingRaw,
16411
+ (raw) => vaultMemoryRawToStored(raw, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner)
16412
+ );
16413
+ return { pending, linkedUnstamped };
16414
+ }
16415
+ async function stampTopicsExtractedAtOp(ctx, memoryIds, extractedAt) {
16416
+ if (memoryIds.length === 0) return [];
16417
+ const uniqueIds = Array.from(new Set(memoryIds));
16418
+ const CHUNK = 500;
16419
+ const stamped = [];
16420
+ for (let i = 0; i < uniqueIds.length; i += CHUNK) {
16421
+ const chunkIds = uniqueIds.slice(i, i + CHUNK);
16422
+ await ctx.database.write(async () => {
16423
+ const prepared = [];
16424
+ for (const id of chunkIds) {
16425
+ let record;
16426
+ try {
16427
+ record = await ctx.vaultMemoryCollection.find(id);
16428
+ } catch {
16429
+ continue;
16430
+ }
16431
+ if (record.isDeleted || !isOwnedByCtxUser(ctx, record) || record.topicsUserManaged) {
16432
+ continue;
16433
+ }
16434
+ const originalUpdatedAt = record.updatedAt.getTime();
16435
+ prepared.push(
16436
+ record.prepareUpdate((r) => {
16437
+ r._setRaw("topics_extracted_at", extractedAt);
16438
+ r._setRaw("updated_at", originalUpdatedAt);
16439
+ })
16440
+ );
16441
+ stamped.push(id);
16442
+ }
16443
+ if (prepared.length > 0) await ctx.database.batch(...prepared);
16444
+ });
16445
+ }
16446
+ return stamped;
16447
+ }
16321
16448
  async function deleteAllVaultMemoriesForUserOp(ctx, userId) {
16322
16449
  if (ctx.userId !== void 0 && ctx.userId !== userId) return 0;
16323
16450
  const records = await ctx.vaultMemoryCollection.query(import_watermelondb3.Q.where("user_id", userId), import_watermelondb3.Q.where("is_deleted", false)).fetch();
@@ -18848,7 +18975,7 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
18848
18975
  legacy.map((r) => ({ id: r.uniqueId, content: r.content, score: r.similarity }))
18849
18976
  );
18850
18977
  }
18851
- const { recall: recall2 } = await import("./recall-RMBNCEG6.mjs");
18978
+ const { recall: recall2 } = await import("./recall-K2YHLBBV.mjs");
18852
18979
  const result = await recall2(
18853
18980
  query,
18854
18981
  { vaultCtx, embeddingOptions, vaultCache: cache },
@@ -19476,6 +19603,8 @@ export {
19476
19603
  makeSyntheticStoredConversation,
19477
19604
  ENTITY_KINDS,
19478
19605
  linkMemoryEntitiesOp,
19606
+ replaceMemoryEntitiesGuardedOp,
19607
+ vaultMemoryToStored,
19479
19608
  createVaultMemoryOp,
19480
19609
  createVaultMemoriesBatchOp,
19481
19610
  getVaultMemoryOp,
@@ -19484,6 +19613,8 @@ export {
19484
19613
  updateVaultMemoryOp,
19485
19614
  deleteVaultMemoryOp,
19486
19615
  getUnfiledVaultMemoriesOp,
19616
+ getMemoriesNeedingTopicExtractionOp,
19617
+ stampTopicsExtractedAtOp,
19487
19618
  deleteAllVaultMemoriesForUserOp,
19488
19619
  updateVaultMemoryEmbeddingOp,
19489
19620
  DEFAULT_CHUNK_SIZE,