@anuma/sdk 2.1.0 → 2.2.1

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.
@@ -16643,14 +16643,19 @@ async function upsertEntitiesOp(ctx, entities) {
16643
16643
  return out;
16644
16644
  });
16645
16645
  }
16646
- async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs) {
16646
+ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
16647
16647
  if (entityInputs.length === 0) return [];
16648
16648
  const normalized = entityInputs.map((e) => typeof e === "string" ? { name: e } : e);
16649
16649
  const byName = await upsertEntitiesOp(ctx, normalized);
16650
16650
  const entities = Array.from(byName.values());
16651
16651
  if (entities.length === 0) return [];
16652
16652
  const userId = ctx.userId;
16653
+ let skipped = false;
16653
16654
  await ctx.database.write(async () => {
16655
+ if (options?.unlessTopicsUserManaged && await autoLinkBlocked(ctx, memoryId)) {
16656
+ skipped = true;
16657
+ return;
16658
+ }
16654
16659
  const existingLinks = await ctx.memoryEntityCollection.query(import_watermelondb16.Q.where("memory_id", memoryId)).fetch();
16655
16660
  const existingEntityIds = new Set(existingLinks.map((l) => String(l.entityId)));
16656
16661
  const toCreate = entities.filter((e) => !existingEntityIds.has(e.uniqueId));
@@ -16664,7 +16669,46 @@ async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs) {
16664
16669
  );
16665
16670
  await ctx.database.batch(...prepared);
16666
16671
  });
16667
- return entities;
16672
+ return skipped ? [] : entities;
16673
+ }
16674
+ async function autoLinkBlocked(ctx, memoryId) {
16675
+ try {
16676
+ const rows = await ctx.database.get("memory_vault").query(import_watermelondb16.Q.where("id", memoryId)).fetch();
16677
+ const row = rows[0];
16678
+ return !row || !!row.isDeleted || !!row.topicsUserManaged;
16679
+ } catch {
16680
+ return true;
16681
+ }
16682
+ }
16683
+ async function replaceMemoryEntitiesGuardedOp(ctx, memoryId, entityInputs) {
16684
+ const normalized = entityInputs.map((e) => typeof e === "string" ? { name: e } : e);
16685
+ const byName = await upsertEntitiesOp(ctx, normalized);
16686
+ const entities = Array.from(byName.values());
16687
+ const userId = ctx.userId;
16688
+ let skipped = false;
16689
+ await ctx.database.write(async () => {
16690
+ if (await autoLinkBlocked(ctx, memoryId)) {
16691
+ skipped = true;
16692
+ return;
16693
+ }
16694
+ const existingLinks = await ctx.memoryEntityCollection.query(import_watermelondb16.Q.where("memory_id", memoryId)).fetch();
16695
+ const keep = new Set(entities.map((e) => e.uniqueId));
16696
+ const existingEntityIds = new Set(existingLinks.map((l) => String(l.entityId)));
16697
+ const toCreate = entities.filter((e) => !existingEntityIds.has(e.uniqueId));
16698
+ const toDestroy = existingLinks.filter((l) => !keep.has(String(l.entityId)));
16699
+ if (toCreate.length === 0 && toDestroy.length === 0) return;
16700
+ await ctx.database.batch(
16701
+ ...toCreate.map(
16702
+ (e) => ctx.memoryEntityCollection.prepareCreate((record) => {
16703
+ record._setRaw("memory_id", memoryId);
16704
+ record._setRaw("entity_id", e.uniqueId);
16705
+ if (userId !== void 0) record._setRaw("user_id", userId);
16706
+ })
16707
+ ),
16708
+ ...toDestroy.map((l) => l.prepareDestroyPermanently())
16709
+ );
16710
+ });
16711
+ return skipped ? null : entities;
16668
16712
  }
16669
16713
  async function unlinkMemoryEntitiesOp(ctx, memoryIds) {
16670
16714
  if (memoryIds.length === 0) return;
@@ -16800,6 +16844,7 @@ function vaultMemoryToStoredRaw(memory) {
16800
16844
  eventTimeEnd: memory.eventTimeEnd ?? null,
16801
16845
  eventTimeKind: memory.eventTimeKind ?? null,
16802
16846
  topicsUserManaged: memory.topicsUserManaged ?? false,
16847
+ topicsExtractedAt: memory.topicsExtractedAt ?? null,
16803
16848
  createdAt: memory.createdAt,
16804
16849
  updatedAt: memory.updatedAt,
16805
16850
  isDeleted: memory.isDeleted
@@ -16983,6 +17028,7 @@ function vaultMemoryRawToStoredRaw(raw) {
16983
17028
  eventTimeKind: raw.event_time_kind ?? null,
16984
17029
  // SQLite stores booleans as 0/1, LokiJS as true/false — coerce both.
16985
17030
  topicsUserManaged: raw.topics_user_managed === true || raw.topics_user_managed === 1,
17031
+ topicsExtractedAt: raw.topics_extracted_at ?? null,
16986
17032
  createdAt: new Date(raw.created_at),
16987
17033
  updatedAt: new Date(raw.updated_at),
16988
17034
  isDeleted: raw.is_deleted === true || raw.is_deleted === 1
@@ -17125,6 +17171,87 @@ async function getUnfiledVaultMemoriesOp(ctx) {
17125
17171
  (record) => vaultMemoryToStored(record, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner)
17126
17172
  );
17127
17173
  }
17174
+ async function getMemoriesNeedingTopicExtractionOp(ctx, options) {
17175
+ const entityCtx = ctx.entityCtx;
17176
+ if (!entityCtx) {
17177
+ throw new Error("getMemoriesNeedingTopicExtractionOp requires ctx.entityCtx");
17178
+ }
17179
+ const conditions = [
17180
+ // Boolean optional column: null (legacy) and false both mean auto-managed.
17181
+ // Q.notEq(true) is a NULL trap on SQLite (NULL <> TRUE is NULL) — enumerate.
17182
+ import_watermelondb17.Q.or(import_watermelondb17.Q.where("topics_user_managed", null), import_watermelondb17.Q.where("topics_user_managed", false)),
17183
+ ...baseVaultConditions(ctx),
17184
+ import_watermelondb17.Q.sortBy("created_at", import_watermelondb17.Q.desc)
17185
+ ];
17186
+ const rows = await ctx.vaultMemoryCollection.query(...conditions).unsafeFetchRaw();
17187
+ const stampedPending = [];
17188
+ const unstamped = [];
17189
+ for (const raw of rows) {
17190
+ const stamp = raw.topics_extracted_at ?? null;
17191
+ if (stamp !== null) {
17192
+ if (raw.updated_at > stamp) stampedPending.push(raw);
17193
+ } else {
17194
+ unstamped.push(raw);
17195
+ }
17196
+ }
17197
+ const linkedIds = /* @__PURE__ */ new Set();
17198
+ const CHUNK = 500;
17199
+ for (let i = 0; i < unstamped.length; i += CHUNK) {
17200
+ const ids = unstamped.slice(i, i + CHUNK).map((r) => r.id);
17201
+ const links = await entityCtx.memoryEntityCollection.query(import_watermelondb17.Q.where("memory_id", import_watermelondb17.Q.oneOf(ids))).unsafeFetchRaw();
17202
+ for (const link of links) linkedIds.add(String(link.memory_id));
17203
+ }
17204
+ const linkedUnstampedAll = [];
17205
+ const pendingRaw = [...stampedPending];
17206
+ for (const raw of unstamped) {
17207
+ if (linkedIds.has(raw.id)) {
17208
+ linkedUnstampedAll.push(raw.id);
17209
+ } else {
17210
+ pendingRaw.push(raw);
17211
+ }
17212
+ }
17213
+ const cap = options?.limit !== void 0 && options.limit > 0 ? options.limit : void 0;
17214
+ const limitedPendingRaw = cap !== void 0 ? pendingRaw.slice(0, cap) : pendingRaw;
17215
+ const linkedUnstamped = cap !== void 0 ? linkedUnstampedAll.slice(0, cap) : linkedUnstampedAll;
17216
+ const pending = await mapInBatches(
17217
+ limitedPendingRaw,
17218
+ (raw) => vaultMemoryRawToStored(raw, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner)
17219
+ );
17220
+ return { pending, linkedUnstamped };
17221
+ }
17222
+ async function stampTopicsExtractedAtOp(ctx, memoryIds, extractedAt) {
17223
+ if (memoryIds.length === 0) return [];
17224
+ const uniqueIds = Array.from(new Set(memoryIds));
17225
+ const CHUNK = 500;
17226
+ const stamped = [];
17227
+ for (let i = 0; i < uniqueIds.length; i += CHUNK) {
17228
+ const chunkIds = uniqueIds.slice(i, i + CHUNK);
17229
+ await ctx.database.write(async () => {
17230
+ const prepared = [];
17231
+ for (const id of chunkIds) {
17232
+ let record;
17233
+ try {
17234
+ record = await ctx.vaultMemoryCollection.find(id);
17235
+ } catch {
17236
+ continue;
17237
+ }
17238
+ if (record.isDeleted || !isOwnedByCtxUser(ctx, record) || record.topicsUserManaged) {
17239
+ continue;
17240
+ }
17241
+ const originalUpdatedAt = record.updatedAt.getTime();
17242
+ prepared.push(
17243
+ record.prepareUpdate((r) => {
17244
+ r._setRaw("topics_extracted_at", extractedAt);
17245
+ r._setRaw("updated_at", originalUpdatedAt);
17246
+ })
17247
+ );
17248
+ stamped.push(id);
17249
+ }
17250
+ if (prepared.length > 0) await ctx.database.batch(...prepared);
17251
+ });
17252
+ }
17253
+ return stamped;
17254
+ }
17128
17255
  async function deleteAllVaultMemoriesForUserOp(ctx, userId) {
17129
17256
  if (ctx.userId !== void 0 && ctx.userId !== userId) return 0;
17130
17257
  const records = await ctx.vaultMemoryCollection.query(import_watermelondb17.Q.where("user_id", userId), import_watermelondb17.Q.where("is_deleted", false)).fetch();
@@ -21469,6 +21596,7 @@ __export(index_exports, {
21469
21596
  STREAM_RESUMABLE_HEADER: () => STREAM_RESUMABLE_HEADER,
21470
21597
  StreamExpiredError: () => StreamExpiredError,
21471
21598
  TOOL_CATALOG: () => TOOL_CATALOG,
21599
+ TOPIC_EXTRACTION_BATCH_SIZE: () => TOPIC_EXTRACTION_BATCH_SIZE,
21472
21600
  TextProcessor: () => TextProcessor,
21473
21601
  UserPreference: () => UserPreference,
21474
21602
  VaultFolder: () => VaultFolder,
@@ -21549,7 +21677,9 @@ __export(index_exports, {
21549
21677
  estimateMessagesTokens: () => estimateMessagesTokens,
21550
21678
  estimateTokens: () => estimateTokens,
21551
21679
  exportPublicKey: () => exportPublicKey,
21680
+ extractAndLinkEntitiesForMemoriesOp: () => extractAndLinkEntitiesForMemoriesOp,
21552
21681
  extractAndRetain: () => extractAndRetain,
21682
+ extractEntitiesForMemories: () => extractEntitiesForMemories,
21553
21683
  extractFacts: () => extractFacts,
21554
21684
  findMatchingTools: () => findMatchingTools,
21555
21685
  generateConversationId: () => generateConversationId,
@@ -21585,6 +21715,7 @@ __export(index_exports, {
21585
21715
  getMediaCountsByTypeOp: () => getMediaCountsByTypeOp,
21586
21716
  getMediaOp: () => getMediaOp,
21587
21717
  getMediaTypeFromMime: () => getMediaTypeFromMime,
21718
+ getMemoriesNeedingTopicExtractionOp: () => getMemoriesNeedingTopicExtractionOp,
21588
21719
  getMessagesOp: () => getMessagesOp,
21589
21720
  getModelPreferenceOp: () => getModelPreferenceOp,
21590
21721
  getProjectConversationCountOp: () => getProjectConversationCountOp,
@@ -21644,6 +21775,7 @@ __export(index_exports, {
21644
21775
  shouldChunkMessage: () => shouldChunkMessage,
21645
21776
  shouldRefreshTools: () => shouldRefreshTools,
21646
21777
  splitMessagesAtThreshold: () => splitMessagesAtThreshold,
21778
+ stampTopicsExtractedAtOp: () => stampTopicsExtractedAtOp,
21647
21779
  streamCancelPath: () => streamCancelPath,
21648
21780
  streamReplayPath: () => streamReplayPath,
21649
21781
  summaryToSystemMessage: () => summaryToSystemMessage,
@@ -21998,6 +22130,9 @@ __decorateClass([
21998
22130
  __decorateClass([
21999
22131
  (0, import_decorators6.field)("topics_user_managed")
22000
22132
  ], VaultMemory.prototype, "topicsUserManaged", 2);
22133
+ __decorateClass([
22134
+ (0, import_decorators6.field)("topics_extracted_at")
22135
+ ], VaultMemory.prototype, "topicsExtractedAt", 2);
22001
22136
  __decorateClass([
22002
22137
  import_decorators6.readonly,
22003
22138
  (0, import_decorators6.date)("created_at")
@@ -22145,7 +22280,7 @@ __decorateClass([
22145
22280
  ], VaultFolder.prototype, "context", 2);
22146
22281
 
22147
22282
  // src/lib/db/schema.ts
22148
- var SDK_SCHEMA_VERSION = 35;
22283
+ var SDK_SCHEMA_VERSION = 36;
22149
22284
  var sdkSchema = (0, import_watermelondb12.appSchema)({
22150
22285
  version: SDK_SCHEMA_VERSION,
22151
22286
  tables: [
@@ -22266,7 +22401,11 @@ var sdkSchema = (0, import_watermelondb12.appSchema)({
22266
22401
  // When true, the user has manually set this memory's topics (entity
22267
22402
  // links). Auto-extraction then leaves its links alone — the user owns
22268
22403
  // them. Null/false = topics are auto-derived (default).
22269
- { name: "topics_user_managed", type: "boolean", isOptional: true }
22404
+ { name: "topics_user_managed", type: "boolean", isOptional: true },
22405
+ // Unix ms of the last LLM topic-extraction pass over this memory's
22406
+ // content. Null = never extracted standalone (legacy rows with entity
22407
+ // links are grandfathered — see getMemoriesNeedingTopicExtractionOp).
22408
+ { name: "topics_extracted_at", type: "number", isOptional: true }
22270
22409
  ]
22271
22410
  }),
22272
22411
  // Entity table — canonical names extracted from auto-extraction (W5).
@@ -22864,6 +23003,19 @@ var sdkMigrations = (0, import_migrations.schemaMigrations)({
22864
23003
  ]
22865
23004
  })
22866
23005
  ]
23006
+ },
23007
+ // v35 -> v36: topics_extracted_at watermark on memory_vault. Existing rows
23008
+ // keep it NULL — rows that already have entity links are grandfathered as
23009
+ // extracted (no mass re-extraction on upgrade), rows without links are the
23010
+ // backfill target. See getMemoriesNeedingTopicExtractionOp.
23011
+ {
23012
+ toVersion: 36,
23013
+ steps: [
23014
+ (0, import_migrations.addColumns)({
23015
+ table: "memory_vault",
23016
+ columns: [{ name: "topics_extracted_at", type: "number", isOptional: true }]
23017
+ })
23018
+ ]
22867
23019
  }
22868
23020
  ]
22869
23021
  });
@@ -25790,7 +25942,7 @@ async function isMemoryTopicsUserManaged(ctx, memoryId) {
25790
25942
  return true;
25791
25943
  }
25792
25944
  }
25793
- var DEFAULT_MODEL3 = "gpt-oss/gpt-oss-120b";
25945
+ var DEFAULT_EXTRACTION_MODEL = "gpt-oss/gpt-oss-120b";
25794
25946
  var DEFAULT_MIN_CONFIDENCE = 0.7;
25795
25947
  var MAX_CONTENT_LENGTH = 200;
25796
25948
  var MIN_CONTENT_LENGTH = 3;
@@ -25803,6 +25955,16 @@ var FACT_TYPES = [
25803
25955
  "constraint",
25804
25956
  "other"
25805
25957
  ];
25958
+ var ENTITY_KIND_GUIDELINES = `Choose the MOST SPECIFIC kind:
25959
+ - person: a named individual ("Sara", "Dr. Lee")
25960
+ - organization: a company, team, school, or group ("Google", "the Warriors", "Stanford")
25961
+ - place: a geographic location or venue ("San Francisco", "Blue Bottle on Valencia")
25962
+ - event: a dated or nameable occurrence ("Chicago Marathon", "the Japan trip", "their wedding")
25963
+ - product: a named app, tool, brand, device, book, film, or other media title ("Linear", "iPhone", "Dune", "Ableton")
25964
+ - thing: a concrete physical object that isn't a branded product ("road bike", "sourdough starter")
25965
+ - concept: a topic, skill, field, language, or idea ("machine learning", "Spanish", "async communication")
25966
+ - other: a named entity that genuinely fits none of the above \u2014 use sparingly
25967
+ Prefer organization / product / event / place over the generic "thing"; fall back to "thing" or "other" only when no specific kind fits.`;
25806
25968
  var SYSTEM_PROMPT3 = `You extract durable user facts from conversations for a personal memory system.
25807
25969
 
25808
25970
  A "durable fact" is something the user shared about themselves that should still be true in future conversations. Examples that ARE durable:
@@ -25831,16 +25993,7 @@ For each durable fact, output:
25831
25993
  - type: one of identity | preference | relationship | plan | ongoing_context | constraint | other
25832
25994
  - confidence: 0.0-1.0; how sure you are this is durable AND true. Only include facts >= 0.7.
25833
25995
  - sourceMessageIds: which message IDs from the conversation contained the evidence
25834
- - 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:
25835
- - person: a named individual ("Sara", "Dr. Lee")
25836
- - organization: a company, team, school, or group ("Google", "the Warriors", "Stanford")
25837
- - place: a geographic location or venue ("San Francisco", "Blue Bottle on Valencia")
25838
- - event: a dated or nameable occurrence ("Chicago Marathon", "the Japan trip", "their wedding")
25839
- - product: a named app, tool, brand, device, book, film, or other media title ("Linear", "iPhone", "Dune", "Ableton")
25840
- - thing: a concrete physical object that isn't a branded product ("road bike", "sourdough starter")
25841
- - concept: a topic, skill, field, language, or idea ("machine learning", "Spanish", "async communication")
25842
- - other: a named entity that genuinely fits none of the above \u2014 use sparingly
25843
- Prefer organization / product / event / place over the generic "thing"; fall back to "thing" or "other" only when no specific kind fits. Examples:
25996
+ - 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:
25844
25997
  - "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" }]
25845
25998
  - "running the Berlin Half in May and I track workouts in Whoop" \u2192 entities: [{ "name": "Berlin Half", "kind": "event" }, { "name": "Whoop", "kind": "product" }]
25846
25999
  - 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:
@@ -25866,7 +26019,7 @@ async function extractFacts(messages, options) {
25866
26019
  ...options.apiKey !== void 0 && { apiKey: options.apiKey },
25867
26020
  ...options.getToken !== void 0 && { getToken: options.getToken },
25868
26021
  ...options.baseUrl !== void 0 && { baseUrl: options.baseUrl },
25869
- model: options.model ?? DEFAULT_MODEL3,
26022
+ model: options.model ?? DEFAULT_EXTRACTION_MODEL,
25870
26023
  systemPrompt: SYSTEM_PROMPT3,
25871
26024
  userMessage: `Today's date is ${today}. Resolve any relative dates ("yesterday", "next week") against it.
25872
26025
 
@@ -25960,7 +26113,9 @@ async function extractAndRetain(messages, retainCtx, options) {
25960
26113
  if (options.entityCtx && candidate.entities.length > 0) {
25961
26114
  try {
25962
26115
  if (!await isMemoryTopicsUserManaged(options.entityCtx, result.memoryId)) {
25963
- await linkMemoryEntitiesOp(options.entityCtx, result.memoryId, candidate.entities);
26116
+ await linkMemoryEntitiesOp(options.entityCtx, result.memoryId, candidate.entities, {
26117
+ unlessTopicsUserManaged: true
26118
+ });
25964
26119
  }
25965
26120
  } catch (err) {
25966
26121
  log.warn("[memory/extract] linkMemoryEntitiesOp failed", err);
@@ -26365,7 +26520,7 @@ init_logger();
26365
26520
  init_portalLlm();
26366
26521
  init_recall();
26367
26522
  var DEFAULT_BASE_URL2 = BASE_URL;
26368
- var DEFAULT_MODEL4 = "anthropic/claude-sonnet-4-6";
26523
+ var DEFAULT_MODEL3 = "anthropic/claude-sonnet-4-6";
26369
26524
  var DEFAULT_MAX_TOKENS = 4096;
26370
26525
  var REQUEST_TIMEOUT_MS = 6e4;
26371
26526
  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.
@@ -26397,7 +26552,7 @@ async function reflect(query, ctx, options) {
26397
26552
  const evidence = recalled.memories.map((m, i) => `[${i + 1}] (id: ${m.id}, kind: ${m.kind})
26398
26553
  ${m.content}`).join("\n\n");
26399
26554
  const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL2;
26400
- const model = options.llmModel ?? DEFAULT_MODEL4;
26555
+ const model = options.llmModel ?? DEFAULT_MODEL3;
26401
26556
  const wantsStructured = !!options.responseSchema;
26402
26557
  const sendResponseFormat = wantsStructured && supportsResponseFormat(model, "json_schema");
26403
26558
  const basePrompt = options.systemPrompt ?? DEFAULT_SYSTEM_PROMPT;
@@ -26507,6 +26662,168 @@ function parseAnswer(body, base, parseSchema) {
26507
26662
  };
26508
26663
  }
26509
26664
 
26665
+ // src/lib/memory/topicExtract.ts
26666
+ init_operations2();
26667
+ init_operations3();
26668
+ init_logger();
26669
+ init_redactor();
26670
+ init_portalLlm();
26671
+ var TOPIC_EXTRACTION_BATCH_SIZE = 10;
26672
+ var MAX_CHARS_PER_MEMORY = 300;
26673
+ var MAX_VOCABULARY_NAMES = 100;
26674
+ var SYSTEM_PROMPT4 = `You assign topics to saved memories for a personal memory system.
26675
+
26676
+ 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.
26677
+
26678
+ Include only NAMED entities, skip generic/common nouns. ${ENTITY_KIND_GUIDELINES}
26679
+
26680
+ Output strict JSON: {"memories": [{"id": string, "entities": [{"name": string, "kind": string}]}]}
26681
+ - exactly one element per input memory, echoing its id verbatim
26682
+ - "entities" may be empty when a memory mentions no named entities \u2014 most short memories have 0-3
26683
+ No prose.`;
26684
+ async function extractEntitiesForMemories(memories, options) {
26685
+ const out = /* @__PURE__ */ new Map();
26686
+ if (memories.length === 0) return out;
26687
+ const redactor = resolvePiiRedactor(options.piiRedaction);
26688
+ const vocabulary = (options.existingEntityNames ?? []).filter((n) => n.trim().length > 0).slice(0, MAX_VOCABULARY_NAMES).map((n) => redactor ? redactor.redactText(n).text : n);
26689
+ const vocabularyNote = vocabulary.length > 0 ? `The user's existing topics: ${vocabulary.join(", ")}.
26690
+ When a memory refers to one of these, reuse the EXACT existing name (same spelling and casing) instead of a variant.
26691
+
26692
+ ` : "";
26693
+ for (let i = 0; i < memories.length; i += TOPIC_EXTRACTION_BATCH_SIZE) {
26694
+ const batch = memories.slice(i, i + TOPIC_EXTRACTION_BATCH_SIZE);
26695
+ const listing = batch.map((m) => {
26696
+ const content = m.content.slice(0, MAX_CHARS_PER_MEMORY);
26697
+ return `[${m.id}] ${redactor ? redactor.redactText(content).text : content}`;
26698
+ }).join("\n");
26699
+ const parsed = await callPortalJsonCompletion({
26700
+ ...options.apiKey !== void 0 && { apiKey: options.apiKey },
26701
+ ...options.getToken !== void 0 && { getToken: options.getToken },
26702
+ ...options.baseUrl !== void 0 && { baseUrl: options.baseUrl },
26703
+ model: options.model ?? DEFAULT_EXTRACTION_MODEL,
26704
+ systemPrompt: SYSTEM_PROMPT4,
26705
+ userMessage: `${vocabularyNote}Memories:
26706
+ ${listing}
26707
+
26708
+ List each memory's named entities.`,
26709
+ tag: "memory/topics",
26710
+ ...options.fetchFn && { fetchFn: options.fetchFn },
26711
+ ...options.maxAttempts !== void 0 && { maxAttempts: options.maxAttempts },
26712
+ ...options.timeoutMs !== void 0 && { timeoutMs: options.timeoutMs },
26713
+ ...options.totalTimeoutMs !== void 0 && { totalTimeoutMs: options.totalTimeoutMs },
26714
+ ...options.backoffMs && { backoffMs: options.backoffMs }
26715
+ });
26716
+ if (parsed === null) {
26717
+ getLogger().warn(
26718
+ `[memory/topics] batch of ${batch.length} failed after retries \u2014 will retry next sweep`
26719
+ );
26720
+ continue;
26721
+ }
26722
+ const validIds = new Set(batch.map((m) => m.id));
26723
+ const byId = parseTopicResponse(parsed, validIds);
26724
+ if (byId === null) {
26725
+ getLogger().warn(
26726
+ `[memory/topics] batch of ${batch.length} returned an unrecognized shape \u2014 will retry next sweep`
26727
+ );
26728
+ continue;
26729
+ }
26730
+ for (const m of batch) {
26731
+ const entities = byId.get(m.id);
26732
+ if (entities === void 0) {
26733
+ continue;
26734
+ }
26735
+ out.set(m.id, redactor ? restoreEntities(entities, redactor) : entities);
26736
+ }
26737
+ }
26738
+ return out;
26739
+ }
26740
+ function restoreEntities(entities, redactor) {
26741
+ return entities.map((e) => ({ kind: e.kind, restored: redactor.restoreForStorage(e.name) })).filter((e) => !e.restored.unresolved).map(
26742
+ (e) => e.kind !== void 0 ? { name: e.restored.text, kind: e.kind } : { name: e.restored.text }
26743
+ );
26744
+ }
26745
+ function parseTopicResponse(parsed, validIds) {
26746
+ if (typeof parsed !== "object" || parsed === null) return null;
26747
+ const list = parsed.memories;
26748
+ if (!Array.isArray(list)) return null;
26749
+ const out = /* @__PURE__ */ new Map();
26750
+ for (const raw of list) {
26751
+ if (typeof raw !== "object" || raw === null) continue;
26752
+ const obj = raw;
26753
+ if (typeof obj.id !== "string" || !validIds.has(obj.id) || out.has(obj.id)) continue;
26754
+ const entities = Array.isArray(obj.entities) ? parseEntities(obj.entities) : [];
26755
+ out.set(obj.id, entities);
26756
+ }
26757
+ return out;
26758
+ }
26759
+ async function extractAndLinkEntitiesForMemoriesOp(ctx, memoryIds, options) {
26760
+ const entityCtx = ctx.entityCtx;
26761
+ if (!entityCtx) {
26762
+ throw new Error("extractAndLinkEntitiesForMemoriesOp requires ctx.entityCtx");
26763
+ }
26764
+ const log = getLogger();
26765
+ const extractedAt = options.now ?? Date.now();
26766
+ const skippedIds = [];
26767
+ const inputs = [];
26768
+ for (const id of memoryIds) {
26769
+ let record;
26770
+ try {
26771
+ record = await ctx.vaultMemoryCollection.find(id);
26772
+ } catch {
26773
+ skippedIds.push(id);
26774
+ continue;
26775
+ }
26776
+ if (record.isDeleted || ctx.userId !== void 0 && record.userId !== ctx.userId || record.topicsUserManaged) {
26777
+ skippedIds.push(id);
26778
+ continue;
26779
+ }
26780
+ try {
26781
+ const stored = await vaultMemoryToStored(
26782
+ record,
26783
+ ctx.walletAddress,
26784
+ ctx.signMessage,
26785
+ ctx.embeddedWalletSigner
26786
+ );
26787
+ inputs.push({ id, content: stored.content });
26788
+ } catch (err) {
26789
+ log.warn("[memory/topics] failed to load memory for extraction", err);
26790
+ skippedIds.push(id);
26791
+ }
26792
+ }
26793
+ const entitiesByMemory = await extractEntitiesForMemories(inputs, options);
26794
+ const toStamp = [];
26795
+ for (const input of inputs) {
26796
+ const entities = entitiesByMemory.get(input.id);
26797
+ if (entities === void 0) {
26798
+ skippedIds.push(input.id);
26799
+ continue;
26800
+ }
26801
+ try {
26802
+ const linked = await replaceMemoryEntitiesGuardedOp(entityCtx, input.id, entities);
26803
+ if (linked === null) {
26804
+ skippedIds.push(input.id);
26805
+ entitiesByMemory.delete(input.id);
26806
+ continue;
26807
+ }
26808
+ } catch (err) {
26809
+ log.warn("[memory/topics] replaceMemoryEntitiesGuardedOp failed", err);
26810
+ skippedIds.push(input.id);
26811
+ entitiesByMemory.delete(input.id);
26812
+ continue;
26813
+ }
26814
+ toStamp.push(input.id);
26815
+ }
26816
+ const stampedIds = await stampTopicsExtractedAtOp(ctx, toStamp, extractedAt);
26817
+ const stampedSet = new Set(stampedIds);
26818
+ for (const id of toStamp) {
26819
+ if (!stampedSet.has(id)) {
26820
+ skippedIds.push(id);
26821
+ entitiesByMemory.delete(id);
26822
+ }
26823
+ }
26824
+ return { entitiesByMemory, stampedIds, skippedIds };
26825
+ }
26826
+
26510
26827
  // src/tools/appBuilderPrompt.ts
26511
26828
  var APP_BUILDER_PROMPT = `App Builder tools (create_file, patch_file, and the rest described below) are available this turn, but they are OPTIONAL \u2014 having them does not mean you should build anything.
26512
26829
 
@@ -32092,6 +32409,7 @@ function buildSystemPrompt(ctx, options) {
32092
32409
  STREAM_RESUMABLE_HEADER,
32093
32410
  StreamExpiredError,
32094
32411
  TOOL_CATALOG,
32412
+ TOPIC_EXTRACTION_BATCH_SIZE,
32095
32413
  TextProcessor,
32096
32414
  UserPreference,
32097
32415
  VaultFolder,
@@ -32172,7 +32490,9 @@ function buildSystemPrompt(ctx, options) {
32172
32490
  estimateMessagesTokens,
32173
32491
  estimateTokens,
32174
32492
  exportPublicKey,
32493
+ extractAndLinkEntitiesForMemoriesOp,
32175
32494
  extractAndRetain,
32495
+ extractEntitiesForMemories,
32176
32496
  extractFacts,
32177
32497
  findMatchingTools,
32178
32498
  generateConversationId,
@@ -32208,6 +32528,7 @@ function buildSystemPrompt(ctx, options) {
32208
32528
  getMediaCountsByTypeOp,
32209
32529
  getMediaOp,
32210
32530
  getMediaTypeFromMime,
32531
+ getMemoriesNeedingTopicExtractionOp,
32211
32532
  getMessagesOp,
32212
32533
  getModelPreferenceOp,
32213
32534
  getProjectConversationCountOp,
@@ -32267,6 +32588,7 @@ function buildSystemPrompt(ctx, options) {
32267
32588
  shouldChunkMessage,
32268
32589
  shouldRefreshTools,
32269
32590
  splitMessagesAtThreshold,
32591
+ stampTopicsExtractedAtOp,
32270
32592
  streamCancelPath,
32271
32593
  streamReplayPath,
32272
32594
  summaryToSystemMessage,