@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.
@@ -48,6 +48,7 @@ import {
48
48
  getConversationsLazyOp,
49
49
  getConversationsOp,
50
50
  getImageModel,
51
+ getMemoriesNeedingTopicExtractionOp,
51
52
  getMessageCountOp,
52
53
  getMessageSkeletonsOp,
53
54
  getMessagesOp,
@@ -65,11 +66,13 @@ import {
65
66
  postApiV1CreditsPurchase,
66
67
  preEmbedVaultMemories,
67
68
  recall,
69
+ replaceMemoryEntitiesGuardedOp,
68
70
  requestEncryptionKey,
69
71
  resolveStoredUserContent,
70
72
  searchChunksOp,
71
73
  searchVaultMemories,
72
74
  shouldChunkMessage,
75
+ stampTopicsExtractedAtOp,
73
76
  stripToolCalls,
74
77
  updateConversationPinnedOp,
75
78
  updateConversationTitleOp,
@@ -78,8 +81,9 @@ import {
78
81
  updateVaultMemoryEmbeddingOp,
79
82
  updateVaultMemoryOp,
80
83
  upsertMessageOp,
81
- useEncryption
82
- } from "./chunk-N4FEY34G.mjs";
84
+ useEncryption,
85
+ vaultMemoryToStored
86
+ } from "./chunk-7PAISHUN.mjs";
83
87
  import {
84
88
  PII_PATTERNS,
85
89
  PiiRedactor,
@@ -5498,6 +5502,9 @@ __decorateClass([
5498
5502
  __decorateClass([
5499
5503
  field3("topics_user_managed")
5500
5504
  ], VaultMemory.prototype, "topicsUserManaged", 2);
5505
+ __decorateClass([
5506
+ field3("topics_extracted_at")
5507
+ ], VaultMemory.prototype, "topicsExtractedAt", 2);
5501
5508
  __decorateClass([
5502
5509
  readonly2,
5503
5510
  date3("created_at")
@@ -6053,7 +6060,7 @@ async function isMemoryTopicsUserManaged(ctx, memoryId) {
6053
6060
  return true;
6054
6061
  }
6055
6062
  }
6056
- var DEFAULT_MODEL = "gpt-oss/gpt-oss-120b";
6063
+ var DEFAULT_EXTRACTION_MODEL = "gpt-oss/gpt-oss-120b";
6057
6064
  var DEFAULT_MIN_CONFIDENCE = 0.7;
6058
6065
  var MAX_CONTENT_LENGTH = 200;
6059
6066
  var MIN_CONTENT_LENGTH = 3;
@@ -6066,6 +6073,16 @@ var FACT_TYPES = [
6066
6073
  "constraint",
6067
6074
  "other"
6068
6075
  ];
6076
+ var ENTITY_KIND_GUIDELINES = `Choose the MOST SPECIFIC kind:
6077
+ - person: a named individual ("Sara", "Dr. Lee")
6078
+ - organization: a company, team, school, or group ("Google", "the Warriors", "Stanford")
6079
+ - place: a geographic location or venue ("San Francisco", "Blue Bottle on Valencia")
6080
+ - event: a dated or nameable occurrence ("Chicago Marathon", "the Japan trip", "their wedding")
6081
+ - product: a named app, tool, brand, device, book, film, or other media title ("Linear", "iPhone", "Dune", "Ableton")
6082
+ - thing: a concrete physical object that isn't a branded product ("road bike", "sourdough starter")
6083
+ - concept: a topic, skill, field, language, or idea ("machine learning", "Spanish", "async communication")
6084
+ - other: a named entity that genuinely fits none of the above \u2014 use sparingly
6085
+ Prefer organization / product / event / place over the generic "thing"; fall back to "thing" or "other" only when no specific kind fits.`;
6069
6086
  var SYSTEM_PROMPT = `You extract durable user facts from conversations for a personal memory system.
6070
6087
 
6071
6088
  A "durable fact" is something the user shared about themselves that should still be true in future conversations. Examples that ARE durable:
@@ -6094,16 +6111,7 @@ For each durable fact, output:
6094
6111
  - type: one of identity | preference | relationship | plan | ongoing_context | constraint | other
6095
6112
  - confidence: 0.0-1.0; how sure you are this is durable AND true. Only include facts >= 0.7.
6096
6113
  - sourceMessageIds: which message IDs from the conversation contained the evidence
6097
- - 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:
6098
- - person: a named individual ("Sara", "Dr. Lee")
6099
- - organization: a company, team, school, or group ("Google", "the Warriors", "Stanford")
6100
- - place: a geographic location or venue ("San Francisco", "Blue Bottle on Valencia")
6101
- - event: a dated or nameable occurrence ("Chicago Marathon", "the Japan trip", "their wedding")
6102
- - product: a named app, tool, brand, device, book, film, or other media title ("Linear", "iPhone", "Dune", "Ableton")
6103
- - thing: a concrete physical object that isn't a branded product ("road bike", "sourdough starter")
6104
- - concept: a topic, skill, field, language, or idea ("machine learning", "Spanish", "async communication")
6105
- - other: a named entity that genuinely fits none of the above \u2014 use sparingly
6106
- Prefer organization / product / event / place over the generic "thing"; fall back to "thing" or "other" only when no specific kind fits. Examples:
6114
+ - 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:
6107
6115
  - "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" }]
6108
6116
  - "running the Berlin Half in May and I track workouts in Whoop" \u2192 entities: [{ "name": "Berlin Half", "kind": "event" }, { "name": "Whoop", "kind": "product" }]
6109
6117
  - 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:
@@ -6129,7 +6137,7 @@ async function extractFacts(messages, options) {
6129
6137
  ...options.apiKey !== void 0 && { apiKey: options.apiKey },
6130
6138
  ...options.getToken !== void 0 && { getToken: options.getToken },
6131
6139
  ...options.baseUrl !== void 0 && { baseUrl: options.baseUrl },
6132
- model: options.model ?? DEFAULT_MODEL,
6140
+ model: options.model ?? DEFAULT_EXTRACTION_MODEL,
6133
6141
  systemPrompt: SYSTEM_PROMPT,
6134
6142
  userMessage: `Today's date is ${today}. Resolve any relative dates ("yesterday", "next week") against it.
6135
6143
 
@@ -6223,7 +6231,9 @@ async function extractAndRetain(messages, retainCtx, options) {
6223
6231
  if (options.entityCtx && candidate.entities.length > 0) {
6224
6232
  try {
6225
6233
  if (!await isMemoryTopicsUserManaged(options.entityCtx, result.memoryId)) {
6226
- await linkMemoryEntitiesOp(options.entityCtx, result.memoryId, candidate.entities);
6234
+ await linkMemoryEntitiesOp(options.entityCtx, result.memoryId, candidate.entities, {
6235
+ unlessTopicsUserManaged: true
6236
+ });
6227
6237
  }
6228
6238
  } catch (err) {
6229
6239
  log.warn("[memory/extract] linkMemoryEntitiesOp failed", err);
@@ -6618,7 +6628,7 @@ function createRecallTool(ctx, toolOptions, callbacks) {
6618
6628
 
6619
6629
  // src/lib/memory/reflect.ts
6620
6630
  var DEFAULT_BASE_URL = BASE_URL;
6621
- var DEFAULT_MODEL2 = "anthropic/claude-sonnet-4-6";
6631
+ var DEFAULT_MODEL = "anthropic/claude-sonnet-4-6";
6622
6632
  var DEFAULT_MAX_TOKENS = 4096;
6623
6633
  var REQUEST_TIMEOUT_MS = 6e4;
6624
6634
  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.
@@ -6650,7 +6660,7 @@ async function reflect(query, ctx, options) {
6650
6660
  const evidence = recalled.memories.map((m, i) => `[${i + 1}] (id: ${m.id}, kind: ${m.kind})
6651
6661
  ${m.content}`).join("\n\n");
6652
6662
  const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
6653
- const model = options.llmModel ?? DEFAULT_MODEL2;
6663
+ const model = options.llmModel ?? DEFAULT_MODEL;
6654
6664
  const wantsStructured = !!options.responseSchema;
6655
6665
  const sendResponseFormat = wantsStructured && supportsResponseFormat(model, "json_schema");
6656
6666
  const basePrompt = options.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
@@ -6760,6 +6770,163 @@ function parseAnswer(body, base, parseSchema) {
6760
6770
  };
6761
6771
  }
6762
6772
 
6773
+ // src/lib/memory/topicExtract.ts
6774
+ var TOPIC_EXTRACTION_BATCH_SIZE = 10;
6775
+ var MAX_CHARS_PER_MEMORY = 300;
6776
+ var MAX_VOCABULARY_NAMES = 100;
6777
+ var SYSTEM_PROMPT2 = `You assign topics to saved memories for a personal memory system.
6778
+
6779
+ 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.
6780
+
6781
+ Include only NAMED entities, skip generic/common nouns. ${ENTITY_KIND_GUIDELINES}
6782
+
6783
+ Output strict JSON: {"memories": [{"id": string, "entities": [{"name": string, "kind": string}]}]}
6784
+ - exactly one element per input memory, echoing its id verbatim
6785
+ - "entities" may be empty when a memory mentions no named entities \u2014 most short memories have 0-3
6786
+ No prose.`;
6787
+ async function extractEntitiesForMemories(memories, options) {
6788
+ const out = /* @__PURE__ */ new Map();
6789
+ if (memories.length === 0) return out;
6790
+ const redactor = resolvePiiRedactor(options.piiRedaction);
6791
+ const vocabulary = (options.existingEntityNames ?? []).filter((n) => n.trim().length > 0).slice(0, MAX_VOCABULARY_NAMES).map((n) => redactor ? redactor.redactText(n).text : n);
6792
+ const vocabularyNote = vocabulary.length > 0 ? `The user's existing topics: ${vocabulary.join(", ")}.
6793
+ When a memory refers to one of these, reuse the EXACT existing name (same spelling and casing) instead of a variant.
6794
+
6795
+ ` : "";
6796
+ for (let i = 0; i < memories.length; i += TOPIC_EXTRACTION_BATCH_SIZE) {
6797
+ const batch = memories.slice(i, i + TOPIC_EXTRACTION_BATCH_SIZE);
6798
+ const listing = batch.map((m) => {
6799
+ const content = m.content.slice(0, MAX_CHARS_PER_MEMORY);
6800
+ return `[${m.id}] ${redactor ? redactor.redactText(content).text : content}`;
6801
+ }).join("\n");
6802
+ const parsed = await callPortalJsonCompletion({
6803
+ ...options.apiKey !== void 0 && { apiKey: options.apiKey },
6804
+ ...options.getToken !== void 0 && { getToken: options.getToken },
6805
+ ...options.baseUrl !== void 0 && { baseUrl: options.baseUrl },
6806
+ model: options.model ?? DEFAULT_EXTRACTION_MODEL,
6807
+ systemPrompt: SYSTEM_PROMPT2,
6808
+ userMessage: `${vocabularyNote}Memories:
6809
+ ${listing}
6810
+
6811
+ List each memory's named entities.`,
6812
+ tag: "memory/topics",
6813
+ ...options.fetchFn && { fetchFn: options.fetchFn },
6814
+ ...options.maxAttempts !== void 0 && { maxAttempts: options.maxAttempts },
6815
+ ...options.timeoutMs !== void 0 && { timeoutMs: options.timeoutMs },
6816
+ ...options.totalTimeoutMs !== void 0 && { totalTimeoutMs: options.totalTimeoutMs },
6817
+ ...options.backoffMs && { backoffMs: options.backoffMs }
6818
+ });
6819
+ if (parsed === null) {
6820
+ getLogger().warn(
6821
+ `[memory/topics] batch of ${batch.length} failed after retries \u2014 will retry next sweep`
6822
+ );
6823
+ continue;
6824
+ }
6825
+ const validIds = new Set(batch.map((m) => m.id));
6826
+ const byId = parseTopicResponse(parsed, validIds);
6827
+ if (byId === null) {
6828
+ getLogger().warn(
6829
+ `[memory/topics] batch of ${batch.length} returned an unrecognized shape \u2014 will retry next sweep`
6830
+ );
6831
+ continue;
6832
+ }
6833
+ for (const m of batch) {
6834
+ const entities = byId.get(m.id);
6835
+ if (entities === void 0) {
6836
+ continue;
6837
+ }
6838
+ out.set(m.id, redactor ? restoreEntities(entities, redactor) : entities);
6839
+ }
6840
+ }
6841
+ return out;
6842
+ }
6843
+ function restoreEntities(entities, redactor) {
6844
+ return entities.map((e) => ({ kind: e.kind, restored: redactor.restoreForStorage(e.name) })).filter((e) => !e.restored.unresolved).map(
6845
+ (e) => e.kind !== void 0 ? { name: e.restored.text, kind: e.kind } : { name: e.restored.text }
6846
+ );
6847
+ }
6848
+ function parseTopicResponse(parsed, validIds) {
6849
+ if (typeof parsed !== "object" || parsed === null) return null;
6850
+ const list = parsed.memories;
6851
+ if (!Array.isArray(list)) return null;
6852
+ const out = /* @__PURE__ */ new Map();
6853
+ for (const raw of list) {
6854
+ if (typeof raw !== "object" || raw === null) continue;
6855
+ const obj = raw;
6856
+ if (typeof obj.id !== "string" || !validIds.has(obj.id) || out.has(obj.id)) continue;
6857
+ const entities = Array.isArray(obj.entities) ? parseEntities(obj.entities) : [];
6858
+ out.set(obj.id, entities);
6859
+ }
6860
+ return out;
6861
+ }
6862
+ async function extractAndLinkEntitiesForMemoriesOp(ctx, memoryIds, options) {
6863
+ const entityCtx = ctx.entityCtx;
6864
+ if (!entityCtx) {
6865
+ throw new Error("extractAndLinkEntitiesForMemoriesOp requires ctx.entityCtx");
6866
+ }
6867
+ const log = getLogger();
6868
+ const extractedAt = options.now ?? Date.now();
6869
+ const skippedIds = [];
6870
+ const inputs = [];
6871
+ for (const id of memoryIds) {
6872
+ let record;
6873
+ try {
6874
+ record = await ctx.vaultMemoryCollection.find(id);
6875
+ } catch {
6876
+ skippedIds.push(id);
6877
+ continue;
6878
+ }
6879
+ if (record.isDeleted || ctx.userId !== void 0 && record.userId !== ctx.userId || record.topicsUserManaged) {
6880
+ skippedIds.push(id);
6881
+ continue;
6882
+ }
6883
+ try {
6884
+ const stored = await vaultMemoryToStored(
6885
+ record,
6886
+ ctx.walletAddress,
6887
+ ctx.signMessage,
6888
+ ctx.embeddedWalletSigner
6889
+ );
6890
+ inputs.push({ id, content: stored.content });
6891
+ } catch (err) {
6892
+ log.warn("[memory/topics] failed to load memory for extraction", err);
6893
+ skippedIds.push(id);
6894
+ }
6895
+ }
6896
+ const entitiesByMemory = await extractEntitiesForMemories(inputs, options);
6897
+ const toStamp = [];
6898
+ for (const input of inputs) {
6899
+ const entities = entitiesByMemory.get(input.id);
6900
+ if (entities === void 0) {
6901
+ skippedIds.push(input.id);
6902
+ continue;
6903
+ }
6904
+ try {
6905
+ const linked = await replaceMemoryEntitiesGuardedOp(entityCtx, input.id, entities);
6906
+ if (linked === null) {
6907
+ skippedIds.push(input.id);
6908
+ entitiesByMemory.delete(input.id);
6909
+ continue;
6910
+ }
6911
+ } catch (err) {
6912
+ log.warn("[memory/topics] replaceMemoryEntitiesGuardedOp failed", err);
6913
+ skippedIds.push(input.id);
6914
+ entitiesByMemory.delete(input.id);
6915
+ continue;
6916
+ }
6917
+ toStamp.push(input.id);
6918
+ }
6919
+ const stampedIds = await stampTopicsExtractedAtOp(ctx, toStamp, extractedAt);
6920
+ const stampedSet = new Set(stampedIds);
6921
+ for (const id of toStamp) {
6922
+ if (!stampedSet.has(id)) {
6923
+ skippedIds.push(id);
6924
+ entitiesByMemory.delete(id);
6925
+ }
6926
+ }
6927
+ return { entitiesByMemory, stampedIds, skippedIds };
6928
+ }
6929
+
6763
6930
  // src/lib/memoryEngine/quantization.ts
6764
6931
  var INT8_MAX = 127;
6765
6932
  function quantizeEmbedding(v) {
@@ -9261,7 +9428,7 @@ __decorateClass([
9261
9428
  ], VaultFolder.prototype, "context", 2);
9262
9429
 
9263
9430
  // src/lib/db/schema.ts
9264
- var SDK_SCHEMA_VERSION = 35;
9431
+ var SDK_SCHEMA_VERSION = 36;
9265
9432
  var sdkSchema = appSchema2({
9266
9433
  version: SDK_SCHEMA_VERSION,
9267
9434
  tables: [
@@ -9382,7 +9549,11 @@ var sdkSchema = appSchema2({
9382
9549
  // When true, the user has manually set this memory's topics (entity
9383
9550
  // links). Auto-extraction then leaves its links alone — the user owns
9384
9551
  // them. Null/false = topics are auto-derived (default).
9385
- { name: "topics_user_managed", type: "boolean", isOptional: true }
9552
+ { name: "topics_user_managed", type: "boolean", isOptional: true },
9553
+ // Unix ms of the last LLM topic-extraction pass over this memory's
9554
+ // content. Null = never extracted standalone (legacy rows with entity
9555
+ // links are grandfathered — see getMemoriesNeedingTopicExtractionOp).
9556
+ { name: "topics_extracted_at", type: "number", isOptional: true }
9386
9557
  ]
9387
9558
  }),
9388
9559
  // Entity table — canonical names extracted from auto-extraction (W5).
@@ -9980,6 +10151,19 @@ var sdkMigrations = schemaMigrations2({
9980
10151
  ]
9981
10152
  })
9982
10153
  ]
10154
+ },
10155
+ // v35 -> v36: topics_extracted_at watermark on memory_vault. Existing rows
10156
+ // keep it NULL — rows that already have entity links are grandfathered as
10157
+ // extracted (no mass re-extraction on upgrade), rows without links are the
10158
+ // backfill target. See getMemoriesNeedingTopicExtractionOp.
10159
+ {
10160
+ toVersion: 36,
10161
+ steps: [
10162
+ addColumns2({
10163
+ table: "memory_vault",
10164
+ columns: [{ name: "topics_extracted_at", type: "number", isOptional: true }]
10165
+ })
10166
+ ]
9983
10167
  }
9984
10168
  ]
9985
10169
  });
@@ -11337,6 +11521,7 @@ export {
11337
11521
  VaultFolder as StoredVaultFolderModel,
11338
11522
  VaultMemory as StoredVaultMemoryModel,
11339
11523
  StreamExpiredError,
11524
+ TOPIC_EXTRACTION_BATCH_SIZE,
11340
11525
  WalletPoller,
11341
11526
  addConversationMemoriesOp,
11342
11527
  buildNotionAuthUrl,
@@ -11388,7 +11573,9 @@ export {
11388
11573
  embedMessage,
11389
11574
  ensureDefaultFoldersOp,
11390
11575
  exchangeNotionCode,
11576
+ extractAndLinkEntitiesForMemoriesOp,
11391
11577
  extractAndRetain,
11578
+ extractEntitiesForMemories,
11392
11579
  extractFacts,
11393
11580
  generateConversationId,
11394
11581
  generateEmbedding,
@@ -11402,6 +11589,7 @@ export {
11402
11589
  getConversationsByProjectLazyOp,
11403
11590
  getConversationsLazyOp,
11404
11591
  getLogger,
11592
+ getMemoriesNeedingTopicExtractionOp,
11405
11593
  getServerTools,
11406
11594
  getUnfiledVaultMemoriesOp,
11407
11595
  getVaultFolderMemoryCountOp,
@@ -11428,6 +11616,7 @@ export {
11428
11616
  searchVaultMemories,
11429
11617
  setLogger,
11430
11618
  shouldChunkMessage,
11619
+ stampTopicsExtractedAtOp,
11431
11620
  streamCancelPath,
11432
11621
  streamReplayPath,
11433
11622
  updateVaultFolderContextOp,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  recall
3
- } from "./chunk-N4FEY34G.mjs";
3
+ } from "./chunk-7PAISHUN.mjs";
4
4
  import "./chunk-DFVCS3QQ.mjs";
5
5
  import "./chunk-GYKJOWEO.mjs";
6
6
  export {
@@ -1797,14 +1797,19 @@ async function upsertEntitiesOp(ctx, entities) {
1797
1797
  return out;
1798
1798
  });
1799
1799
  }
1800
- async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs) {
1800
+ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
1801
1801
  if (entityInputs.length === 0) return [];
1802
1802
  const normalized = entityInputs.map((e) => typeof e === "string" ? { name: e } : e);
1803
1803
  const byName = await upsertEntitiesOp(ctx, normalized);
1804
1804
  const entities = Array.from(byName.values());
1805
1805
  if (entities.length === 0) return [];
1806
1806
  const userId = ctx.userId;
1807
+ let skipped = false;
1807
1808
  await ctx.database.write(async () => {
1809
+ if (options?.unlessTopicsUserManaged && await autoLinkBlocked(ctx, memoryId)) {
1810
+ skipped = true;
1811
+ return;
1812
+ }
1808
1813
  const existingLinks = await ctx.memoryEntityCollection.query(Q2.where("memory_id", memoryId)).fetch();
1809
1814
  const existingEntityIds = new Set(existingLinks.map((l) => String(l.entityId)));
1810
1815
  const toCreate = entities.filter((e) => !existingEntityIds.has(e.uniqueId));
@@ -1818,7 +1823,46 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs) {
1818
1823
  );
1819
1824
  await ctx.database.batch(...prepared);
1820
1825
  });
1821
- return entities;
1826
+ return skipped ? [] : entities;
1827
+ }
1828
+ async function autoLinkBlocked(ctx, memoryId) {
1829
+ try {
1830
+ const rows = await ctx.database.get("memory_vault").query(Q2.where("id", memoryId)).fetch();
1831
+ const row = rows[0];
1832
+ return !row || !!row.isDeleted || !!row.topicsUserManaged;
1833
+ } catch {
1834
+ return true;
1835
+ }
1836
+ }
1837
+ async function replaceMemoryEntitiesGuardedOp(ctx, memoryId, entityInputs) {
1838
+ const normalized = entityInputs.map((e) => typeof e === "string" ? { name: e } : e);
1839
+ const byName = await upsertEntitiesOp(ctx, normalized);
1840
+ const entities = Array.from(byName.values());
1841
+ const userId = ctx.userId;
1842
+ let skipped = false;
1843
+ await ctx.database.write(async () => {
1844
+ if (await autoLinkBlocked(ctx, memoryId)) {
1845
+ skipped = true;
1846
+ return;
1847
+ }
1848
+ const existingLinks = await ctx.memoryEntityCollection.query(Q2.where("memory_id", memoryId)).fetch();
1849
+ const keep = new Set(entities.map((e) => e.uniqueId));
1850
+ const existingEntityIds = new Set(existingLinks.map((l) => String(l.entityId)));
1851
+ const toCreate = entities.filter((e) => !existingEntityIds.has(e.uniqueId));
1852
+ const toDestroy = existingLinks.filter((l) => !keep.has(String(l.entityId)));
1853
+ if (toCreate.length === 0 && toDestroy.length === 0) return;
1854
+ await ctx.database.batch(
1855
+ ...toCreate.map(
1856
+ (e) => ctx.memoryEntityCollection.prepareCreate((record) => {
1857
+ record._setRaw("memory_id", memoryId);
1858
+ record._setRaw("entity_id", e.uniqueId);
1859
+ if (userId !== void 0) record._setRaw("user_id", userId);
1860
+ })
1861
+ ),
1862
+ ...toDestroy.map((l) => l.prepareDestroyPermanently())
1863
+ );
1864
+ });
1865
+ return skipped ? null : entities;
1822
1866
  }
1823
1867
  async function unlinkMemoryEntitiesOp(ctx, memoryIds) {
1824
1868
  if (memoryIds.length === 0) return;
@@ -1963,6 +2007,7 @@ function vaultMemoryToStoredRaw(memory) {
1963
2007
  eventTimeEnd: memory.eventTimeEnd ?? null,
1964
2008
  eventTimeKind: memory.eventTimeKind ?? null,
1965
2009
  topicsUserManaged: memory.topicsUserManaged ?? false,
2010
+ topicsExtractedAt: memory.topicsExtractedAt ?? null,
1966
2011
  createdAt: memory.createdAt,
1967
2012
  updatedAt: memory.updatedAt,
1968
2013
  isDeleted: memory.isDeleted
@@ -2146,6 +2191,7 @@ function vaultMemoryRawToStoredRaw(raw) {
2146
2191
  eventTimeKind: raw.event_time_kind ?? null,
2147
2192
  // SQLite stores booleans as 0/1, LokiJS as true/false — coerce both.
2148
2193
  topicsUserManaged: raw.topics_user_managed === true || raw.topics_user_managed === 1,
2194
+ topicsExtractedAt: raw.topics_extracted_at ?? null,
2149
2195
  createdAt: new Date(raw.created_at),
2150
2196
  updatedAt: new Date(raw.updated_at),
2151
2197
  isDeleted: raw.is_deleted === true || raw.is_deleted === 1
@@ -2298,6 +2344,7 @@ async function clearMemoryTopicsOverrideOp(ctx, memoryId) {
2298
2344
  await ctx.database.write(async () => {
2299
2345
  await record.update((r) => {
2300
2346
  r._setRaw("topics_user_managed", false);
2347
+ r._setRaw("topics_extracted_at", null);
2301
2348
  r._setRaw("updated_at", originalUpdatedAt);
2302
2349
  });
2303
2350
  });
@@ -2341,6 +2388,87 @@ async function getUnfiledVaultMemoriesOp(ctx) {
2341
2388
  (record) => vaultMemoryToStored(record, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner)
2342
2389
  );
2343
2390
  }
2391
+ async function getMemoriesNeedingTopicExtractionOp(ctx, options) {
2392
+ const entityCtx = ctx.entityCtx;
2393
+ if (!entityCtx) {
2394
+ throw new Error("getMemoriesNeedingTopicExtractionOp requires ctx.entityCtx");
2395
+ }
2396
+ const conditions = [
2397
+ // Boolean optional column: null (legacy) and false both mean auto-managed.
2398
+ // Q.notEq(true) is a NULL trap on SQLite (NULL <> TRUE is NULL) — enumerate.
2399
+ Q3.or(Q3.where("topics_user_managed", null), Q3.where("topics_user_managed", false)),
2400
+ ...baseVaultConditions(ctx),
2401
+ Q3.sortBy("created_at", Q3.desc)
2402
+ ];
2403
+ const rows = await ctx.vaultMemoryCollection.query(...conditions).unsafeFetchRaw();
2404
+ const stampedPending = [];
2405
+ const unstamped = [];
2406
+ for (const raw of rows) {
2407
+ const stamp = raw.topics_extracted_at ?? null;
2408
+ if (stamp !== null) {
2409
+ if (raw.updated_at > stamp) stampedPending.push(raw);
2410
+ } else {
2411
+ unstamped.push(raw);
2412
+ }
2413
+ }
2414
+ const linkedIds = /* @__PURE__ */ new Set();
2415
+ const CHUNK = 500;
2416
+ for (let i = 0; i < unstamped.length; i += CHUNK) {
2417
+ const ids = unstamped.slice(i, i + CHUNK).map((r) => r.id);
2418
+ const links = await entityCtx.memoryEntityCollection.query(Q3.where("memory_id", Q3.oneOf(ids))).unsafeFetchRaw();
2419
+ for (const link of links) linkedIds.add(String(link.memory_id));
2420
+ }
2421
+ const linkedUnstampedAll = [];
2422
+ const pendingRaw = [...stampedPending];
2423
+ for (const raw of unstamped) {
2424
+ if (linkedIds.has(raw.id)) {
2425
+ linkedUnstampedAll.push(raw.id);
2426
+ } else {
2427
+ pendingRaw.push(raw);
2428
+ }
2429
+ }
2430
+ const cap = options?.limit !== void 0 && options.limit > 0 ? options.limit : void 0;
2431
+ const limitedPendingRaw = cap !== void 0 ? pendingRaw.slice(0, cap) : pendingRaw;
2432
+ const linkedUnstamped = cap !== void 0 ? linkedUnstampedAll.slice(0, cap) : linkedUnstampedAll;
2433
+ const pending = await mapInBatches(
2434
+ limitedPendingRaw,
2435
+ (raw) => vaultMemoryRawToStored(raw, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner)
2436
+ );
2437
+ return { pending, linkedUnstamped };
2438
+ }
2439
+ async function stampTopicsExtractedAtOp(ctx, memoryIds, extractedAt) {
2440
+ if (memoryIds.length === 0) return [];
2441
+ const uniqueIds = Array.from(new Set(memoryIds));
2442
+ const CHUNK = 500;
2443
+ const stamped = [];
2444
+ for (let i = 0; i < uniqueIds.length; i += CHUNK) {
2445
+ const chunkIds = uniqueIds.slice(i, i + CHUNK);
2446
+ await ctx.database.write(async () => {
2447
+ const prepared = [];
2448
+ for (const id of chunkIds) {
2449
+ let record;
2450
+ try {
2451
+ record = await ctx.vaultMemoryCollection.find(id);
2452
+ } catch {
2453
+ continue;
2454
+ }
2455
+ if (record.isDeleted || !isOwnedByCtxUser(ctx, record) || record.topicsUserManaged) {
2456
+ continue;
2457
+ }
2458
+ const originalUpdatedAt = record.updatedAt.getTime();
2459
+ prepared.push(
2460
+ record.prepareUpdate((r) => {
2461
+ r._setRaw("topics_extracted_at", extractedAt);
2462
+ r._setRaw("updated_at", originalUpdatedAt);
2463
+ })
2464
+ );
2465
+ stamped.push(id);
2466
+ }
2467
+ if (prepared.length > 0) await ctx.database.batch(...prepared);
2468
+ });
2469
+ }
2470
+ return stamped;
2471
+ }
2344
2472
  async function deleteAllVaultMemoriesForUserOp(ctx, userId) {
2345
2473
  if (ctx.userId !== void 0 && ctx.userId !== userId) return 0;
2346
2474
  const records = await ctx.vaultMemoryCollection.query(Q3.where("user_id", userId), Q3.where("is_deleted", false)).fetch();
@@ -5021,7 +5149,7 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
5021
5149
  legacy.map((r) => ({ id: r.uniqueId, content: r.content, score: r.similarity }))
5022
5150
  );
5023
5151
  }
5024
- const { recall: recall2 } = await import("./recall-PUXEQHAP.mjs");
5152
+ const { recall: recall2 } = await import("./recall-ASTJPM5Z.mjs");
5025
5153
  const result = await recall2(
5026
5154
  query,
5027
5155
  { vaultCtx, embeddingOptions, vaultCache: cache },
@@ -5684,8 +5812,10 @@ export {
5684
5812
  chunkAndEmbedAllMessages,
5685
5813
  ENTITY_KINDS,
5686
5814
  linkMemoryEntitiesOp,
5815
+ replaceMemoryEntitiesGuardedOp,
5687
5816
  backfillMemoryEntityUserIdsOp,
5688
5817
  getMemoriesByEntityNamesOp,
5818
+ vaultMemoryToStored,
5689
5819
  createVaultMemoryOp,
5690
5820
  createVaultMemoriesBatchOp,
5691
5821
  getVaultMemoryOp,
@@ -5696,6 +5826,8 @@ export {
5696
5826
  clearMemoryTopicsOverrideOp,
5697
5827
  deleteVaultMemoryOp,
5698
5828
  getUnfiledVaultMemoriesOp,
5829
+ getMemoriesNeedingTopicExtractionOp,
5830
+ stampTopicsExtractedAtOp,
5699
5831
  deleteAllVaultMemoriesForUserOp,
5700
5832
  updateVaultMemoryEmbeddingOp,
5701
5833
  DEFAULT_VAULT_CACHE_SIZE,