@anuma/sdk 3.8.3 → 3.8.5

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.
@@ -16797,7 +16797,7 @@ function entityToStored(e) {
16797
16797
  updatedAt: e.updatedAt
16798
16798
  };
16799
16799
  }
16800
- async function upsertEntitiesOp(ctx, entities) {
16800
+ async function upsertEntitiesInWrite(ctx, entities) {
16801
16801
  const kindByName = /* @__PURE__ */ new Map();
16802
16802
  for (const e of entities) {
16803
16803
  const name = normalizeEntityName(e.name);
@@ -16811,60 +16811,62 @@ async function upsertEntitiesOp(ctx, entities) {
16811
16811
  }
16812
16812
  const unique = Array.from(kindByName.keys());
16813
16813
  const out = /* @__PURE__ */ new Map();
16814
- if (unique.length === 0) return out;
16815
- return await ctx.database.write(async () => {
16816
- const existing = await ctx.entityCollection.query(import_watermelondb16.Q.where("canonical_name", import_watermelondb16.Q.oneOf(unique))).fetch();
16817
- const existingNames = new Set(existing.map((e) => e.canonicalName));
16818
- const updates = existing.filter((e) => {
16819
- const incoming = kindByName.get(e.canonicalName);
16820
- return incoming !== void 0 && (e.kind === null || e.kind === void 0 || e.kind === "");
16821
- });
16822
- const missing = unique.filter((n) => !existingNames.has(n));
16823
- const created = missing.map(
16824
- (name) => ctx.entityCollection.prepareCreate((record) => {
16825
- record._setRaw("canonical_name", name);
16826
- const kind = kindByName.get(name);
16827
- if (kind !== void 0) record._setRaw("kind", kind);
16828
- })
16829
- );
16830
- const updated = updates.map(
16831
- (e) => e.prepareUpdate((record) => {
16832
- record._setRaw("kind", kindByName.get(e.canonicalName));
16833
- })
16834
- );
16835
- if (created.length > 0 || updated.length > 0) {
16836
- await ctx.database.batch(...created, ...updated);
16837
- }
16838
- for (const e of existing) out.set(e.canonicalName, entityToStored(e));
16839
- for (const record of created) out.set(record.canonicalName, entityToStored(record));
16840
- return out;
16814
+ if (unique.length === 0) return { entities: out, operations: [] };
16815
+ const existing = await ctx.entityCollection.query(import_watermelondb16.Q.where("canonical_name", import_watermelondb16.Q.oneOf(unique))).fetch();
16816
+ const existingNames = new Set(existing.map((e) => e.canonicalName));
16817
+ const updates = existing.filter((e) => {
16818
+ const incoming = kindByName.get(e.canonicalName);
16819
+ return incoming !== void 0 && (e.kind === null || e.kind === void 0 || e.kind === "");
16841
16820
  });
16821
+ const missing = unique.filter((n) => !existingNames.has(n));
16822
+ const created = missing.map(
16823
+ (name) => ctx.entityCollection.prepareCreate((record) => {
16824
+ record._setRaw("canonical_name", name);
16825
+ const kind = kindByName.get(name);
16826
+ if (kind !== void 0) record._setRaw("kind", kind);
16827
+ })
16828
+ );
16829
+ const updated = updates.map(
16830
+ (e) => e.prepareUpdate((record) => {
16831
+ record._setRaw("kind", kindByName.get(e.canonicalName));
16832
+ })
16833
+ );
16834
+ for (const e of existing) out.set(e.canonicalName, entityToStored(e));
16835
+ for (const record of created) out.set(record.canonicalName, entityToStored(record));
16836
+ return { entities: out, operations: [...created, ...updated] };
16842
16837
  }
16843
16838
  async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
16844
16839
  if (entityInputs.length === 0) return [];
16845
16840
  const normalized = entityInputs.map((e) => typeof e === "string" ? { name: e } : e);
16846
- const byName = await upsertEntitiesOp(ctx, normalized);
16847
- const entities = Array.from(byName.values());
16848
- if (entities.length === 0) return [];
16849
16841
  const userId = ctx.userId;
16850
16842
  let skipped = false;
16843
+ let entities = [];
16851
16844
  await ctx.database.write(async () => {
16845
+ const { entities: byName, operations: entityOps } = await upsertEntitiesInWrite(
16846
+ ctx,
16847
+ normalized
16848
+ );
16849
+ entities = Array.from(byName.values());
16852
16850
  if (options?.unlessTopicsUserManaged && await autoLinkBlocked(ctx, memoryId)) {
16851
+ if (entityOps.length > 0) {
16852
+ await ctx.database.batch(...entityOps);
16853
+ }
16853
16854
  skipped = true;
16854
16855
  return;
16855
16856
  }
16857
+ if (entities.length === 0) return;
16856
16858
  const existingLinks = await ctx.memoryEntityCollection.query(import_watermelondb16.Q.where("memory_id", memoryId)).fetch();
16857
16859
  const existingEntityIds = new Set(existingLinks.map((l) => String(l.entityId)));
16858
16860
  const toCreate = entities.filter((e) => !existingEntityIds.has(e.uniqueId));
16859
- if (toCreate.length === 0) return;
16860
- const prepared = toCreate.map(
16861
+ if (entityOps.length === 0 && toCreate.length === 0) return;
16862
+ const linkOps = toCreate.map(
16861
16863
  (e) => ctx.memoryEntityCollection.prepareCreate((record) => {
16862
16864
  record._setRaw("memory_id", memoryId);
16863
16865
  record._setRaw("entity_id", e.uniqueId);
16864
16866
  if (userId !== void 0) record._setRaw("user_id", userId);
16865
16867
  })
16866
16868
  );
16867
- await ctx.database.batch(...prepared);
16869
+ await ctx.database.batch(...entityOps, ...linkOps);
16868
16870
  });
16869
16871
  return skipped ? [] : entities;
16870
16872
  }
@@ -16879,22 +16881,28 @@ async function autoLinkBlocked(ctx, memoryId) {
16879
16881
  }
16880
16882
  async function replaceMemoryEntitiesGuardedOp(ctx, memoryId, entityInputs) {
16881
16883
  const normalized = entityInputs.map((e) => typeof e === "string" ? { name: e } : e);
16882
- const byName = await upsertEntitiesOp(ctx, normalized);
16883
- const entities = Array.from(byName.values());
16884
16884
  const userId = ctx.userId;
16885
16885
  let skipped = false;
16886
+ let entities = [];
16886
16887
  await ctx.database.write(async () => {
16887
16888
  if (await autoLinkBlocked(ctx, memoryId)) {
16888
16889
  skipped = true;
16889
16890
  return;
16890
16891
  }
16892
+ const { entities: byName, operations: entityOps } = await upsertEntitiesInWrite(
16893
+ ctx,
16894
+ normalized
16895
+ );
16896
+ entities = Array.from(byName.values());
16891
16897
  const existingLinks = await ctx.memoryEntityCollection.query(import_watermelondb16.Q.where("memory_id", memoryId)).fetch();
16892
16898
  const keep = new Set(entities.map((e) => e.uniqueId));
16893
16899
  const existingEntityIds = new Set(existingLinks.map((l) => String(l.entityId)));
16894
16900
  const toCreate = entities.filter((e) => !existingEntityIds.has(e.uniqueId));
16895
16901
  const toDestroy = existingLinks.filter((l) => !keep.has(String(l.entityId)));
16896
- if (toCreate.length === 0 && toDestroy.length === 0) return;
16902
+ if (entityOps.length === 0 && toCreate.length === 0 && toDestroy.length === 0) return;
16903
+ const orphans = await findOrphanedEntities(ctx, memoryId, toDestroy);
16897
16904
  await ctx.database.batch(
16905
+ ...entityOps,
16898
16906
  ...toCreate.map(
16899
16907
  (e) => ctx.memoryEntityCollection.prepareCreate((record) => {
16900
16908
  record._setRaw("memory_id", memoryId);
@@ -16902,11 +16910,23 @@ async function replaceMemoryEntitiesGuardedOp(ctx, memoryId, entityInputs) {
16902
16910
  if (userId !== void 0) record._setRaw("user_id", userId);
16903
16911
  })
16904
16912
  ),
16905
- ...toDestroy.map((l) => l.prepareDestroyPermanently())
16913
+ ...toDestroy.map((l) => l.prepareDestroyPermanently()),
16914
+ ...orphans.map((e) => e.prepareDestroyPermanently())
16906
16915
  );
16907
16916
  });
16908
16917
  return skipped ? null : entities;
16909
16918
  }
16919
+ async function findOrphanedEntities(ctx, memoryId, toDestroy) {
16920
+ const candidateIds = [...new Set(toDestroy.map((l) => String(l.entityId)))];
16921
+ if (candidateIds.length === 0) return [];
16922
+ const links = await ctx.memoryEntityCollection.query(import_watermelondb16.Q.where("entity_id", import_watermelondb16.Q.oneOf(candidateIds))).fetch();
16923
+ const stillLinked = new Set(
16924
+ links.filter((l) => String(l.memoryId) !== memoryId).map((l) => String(l.entityId))
16925
+ );
16926
+ const orphanIds = candidateIds.filter((id) => !stillLinked.has(id));
16927
+ if (orphanIds.length === 0) return [];
16928
+ return await ctx.entityCollection.query(import_watermelondb16.Q.where("id", import_watermelondb16.Q.oneOf(orphanIds))).fetch();
16929
+ }
16910
16930
  async function unlinkMemoryEntitiesOp(ctx, memoryIds) {
16911
16931
  if (memoryIds.length === 0) return;
16912
16932
  const links = await ctx.memoryEntityCollection.query(import_watermelondb16.Q.where("memory_id", import_watermelondb16.Q.oneOf(Array.from(memoryIds)))).fetch();
@@ -17764,7 +17784,7 @@ var init_operations3 = __esm({
17764
17784
  init_operations2();
17765
17785
  init_encryption2();
17766
17786
  KNOWN_TRUST_TIERS = /* @__PURE__ */ new Set(["quarantined", "trusted"]);
17767
- TOPICS_EXTRACTION_VERSION = 2;
17787
+ TOPICS_EXTRACTION_VERSION = 3;
17768
17788
  }
17769
17789
  });
17770
17790
 
@@ -26900,6 +26920,162 @@ init_types3();
26900
26920
  init_logger();
26901
26921
  init_redactor();
26902
26922
 
26923
+ // src/lib/memory/entitySalience.ts
26924
+ var GENERIC_ENTITY_NAMES = /* @__PURE__ */ new Set([
26925
+ // Relative and absolute time
26926
+ "today",
26927
+ "tomorrow",
26928
+ "yesterday",
26929
+ "tonight",
26930
+ "morning",
26931
+ "afternoon",
26932
+ "evening",
26933
+ "night",
26934
+ "day",
26935
+ "date",
26936
+ "time",
26937
+ "week",
26938
+ "weekend",
26939
+ "weekday",
26940
+ "month",
26941
+ "year",
26942
+ "monday",
26943
+ "tuesday",
26944
+ "wednesday",
26945
+ "thursday",
26946
+ "friday",
26947
+ "saturday",
26948
+ "sunday",
26949
+ "january",
26950
+ "february",
26951
+ "march",
26952
+ "april",
26953
+ "may",
26954
+ "june",
26955
+ "july",
26956
+ "august",
26957
+ "september",
26958
+ "october",
26959
+ "november",
26960
+ "december",
26961
+ // Generic containers and venues — the named ones ("Blue Bottle on Valencia",
26962
+ // "Stanford") are unaffected because they are not bare tokens.
26963
+ "home",
26964
+ "house",
26965
+ "apartment",
26966
+ "room",
26967
+ "office",
26968
+ "work",
26969
+ "workplace",
26970
+ "school",
26971
+ "gym",
26972
+ "store",
26973
+ "shop",
26974
+ "hospital",
26975
+ "airport",
26976
+ "hotel",
26977
+ "city",
26978
+ "town",
26979
+ "country",
26980
+ "place",
26981
+ // Generic calendar entries — "Chicago Marathon" survives, "meeting" doesn't.
26982
+ "meeting",
26983
+ "call",
26984
+ "appointment",
26985
+ "reminder",
26986
+ "event",
26987
+ "birthday",
26988
+ "breakfast",
26989
+ "lunch",
26990
+ "dinner",
26991
+ "brunch",
26992
+ "vacation",
26993
+ "holiday",
26994
+ "trip",
26995
+ // Plurals are listed explicitly rather than stemmed: naive `s`-stripping
26996
+ // would fold real titles into generics ("The Times" → "time"). Only plurals
26997
+ // that are junk as topics in their own right belong here — note the absence
26998
+ // of "times", "places", and anything else that reads as a name.
26999
+ "days",
27000
+ "weeks",
27001
+ "weekends",
27002
+ "months",
27003
+ "years",
27004
+ "rooms",
27005
+ "offices",
27006
+ "houses",
27007
+ "apartments",
27008
+ "stores",
27009
+ "shops",
27010
+ "hotels",
27011
+ "meetings",
27012
+ "calls",
27013
+ "appointments",
27014
+ "reminders",
27015
+ "events",
27016
+ "birthdays",
27017
+ "trips",
27018
+ "vacations",
27019
+ "holidays",
27020
+ // Meta / filler the models occasionally echo back as an entity
27021
+ "user",
27022
+ "assistant",
27023
+ "memory",
27024
+ "memories",
27025
+ "note",
27026
+ "notes",
27027
+ "thing",
27028
+ "things",
27029
+ "stuff",
27030
+ "something",
27031
+ "someone",
27032
+ "anything",
27033
+ "everything",
27034
+ "none",
27035
+ "other",
27036
+ "misc",
27037
+ "general",
27038
+ "unknown"
27039
+ ]);
27040
+ function isGenericEntityName(name) {
27041
+ const tokens = name.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((token) => token.length > 0);
27042
+ if (tokens.length === 0) return true;
27043
+ const distinctive = tokens.filter(
27044
+ (token) => !GENERIC_ENTITY_NAMES.has(token) && !ARTICLES.has(token) && !WEAK_MODIFIERS.has(token)
27045
+ );
27046
+ if (distinctive.length > 0) return false;
27047
+ const generic = tokens.filter((token) => GENERIC_ENTITY_NAMES.has(token));
27048
+ if (generic.length === 0) return true;
27049
+ const titleShaped = generic.length === 1 && tokens.some((token) => ARTICLES.has(token)) && !tokens.some((token) => WEAK_MODIFIERS.has(token));
27050
+ return !titleShaped;
27051
+ }
27052
+ var ARTICLES = /* @__PURE__ */ new Set(["the", "a", "an"]);
27053
+ var WEAK_MODIFIERS = /* @__PURE__ */ new Set([
27054
+ "my",
27055
+ "our",
27056
+ "their",
27057
+ "his",
27058
+ "her",
27059
+ "its",
27060
+ "this",
27061
+ "that",
27062
+ "next",
27063
+ "last",
27064
+ "upcoming",
27065
+ "every",
27066
+ "at",
27067
+ "in",
27068
+ "on",
27069
+ "of",
27070
+ "for",
27071
+ "with",
27072
+ "from",
27073
+ "to",
27074
+ "by",
27075
+ "and",
27076
+ "or"
27077
+ ]);
27078
+
26903
27079
  // src/lib/memory/injectionClassifier.ts
26904
27080
  init_logger();
26905
27081
  init_redactor();
@@ -27840,13 +28016,13 @@ function parseEntities(raw) {
27840
28016
  for (const e of raw) {
27841
28017
  if (typeof e === "string") {
27842
28018
  const name2 = e.trim();
27843
- if (name2.length > 0) out.push({ name: name2 });
28019
+ if (name2.length > 0 && !isGenericEntityName(name2)) out.push({ name: name2 });
27844
28020
  continue;
27845
28021
  }
27846
28022
  if (typeof e !== "object" || e === null) continue;
27847
28023
  const obj = e;
27848
28024
  const name = typeof obj.name === "string" ? obj.name.trim() : "";
27849
- if (name.length === 0) continue;
28025
+ if (name.length === 0 || isGenericEntityName(name)) continue;
27850
28026
  const kind = typeof obj.kind === "string" && ENTITY_KINDS.includes(obj.kind) ? obj.kind : void 0;
27851
28027
  out.push(kind !== void 0 ? { name, kind } : { name });
27852
28028
  }
@@ -28068,9 +28244,11 @@ function createAutoExtractor(options) {
28068
28244
  }
28069
28245
  }
28070
28246
  );
28071
- const advancedTo = window2[window2.length - 1].id;
28072
- stateFor(conversationId).watermark = advancedTo;
28073
- persistCursor(conversationId, messages, advancedTo, contiguous);
28247
+ if (outcome !== "empty-after-retry") {
28248
+ const advancedTo = window2[window2.length - 1].id;
28249
+ stateFor(conversationId).watermark = advancedTo;
28250
+ persistCursor(conversationId, messages, advancedTo, contiguous);
28251
+ }
28074
28252
  for (let i = 0; i < results.length; i++) {
28075
28253
  if (results[i].action === "suppressed") continue;
28076
28254
  options.onMemoryExtracted?.({
@@ -29188,7 +29366,11 @@ var SYSTEM_PROMPT6 = `You assign topics to saved memories for a personal memory
29188
29366
 
29189
29367
  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.
29190
29368
 
29191
- Include only NAMED entities, skip generic/common nouns. ${ENTITY_KIND_GUIDELINES}
29369
+ Include only NAMED entities, skip generic/common nouns. A topic must identify a specific thing the user could recognize by name; if it would be equally true of thousands of unrelated memories, leave it out:
29370
+ - SKIP bare generic nouns even when the memory capitalizes them or a calendar entry is titled that way \u2014 "Home", "Work", "Meeting", "Lunch", "Birthday", "Appointment", "Trip", "Today", "Friday", "March" are NOT topics.
29371
+ - KEEP the specific thing instead, when the memory names one: "Blue Bottle on Valencia" over "cafe", "Chicago Marathon" over "event", "Hollowpoint Labs" over "work".
29372
+ - Prefer 0 entities over a weak one. A memory whose only candidate is a generic noun has NO entities.
29373
+ ${ENTITY_KIND_GUIDELINES}
29192
29374
 
29193
29375
  Each memory below is written as "<id>: <text>". Output strict JSON: {"memories": [{"id": string, "entities": [{"name": string, "kind": string}]}]}
29194
29376
  - exactly one element per input memory; copy its id EXACTLY as written (the token before the first ": "), with no brackets, quotes, or extra characters
@@ -29200,7 +29382,7 @@ async function extractEntitiesForMemories(memories, options) {
29200
29382
  const redactor = resolvePiiRedactor(options.piiRedaction);
29201
29383
  const vocabulary = (options.existingEntityNames ?? []).filter((n) => n.trim().length > 0).slice(0, MAX_VOCABULARY_NAMES).map((n) => redactor ? redactor.redactText(n).text : n);
29202
29384
  const vocabularyNote = vocabulary.length > 0 ? `The user's existing topics: ${vocabulary.join(", ")}.
29203
- When a memory refers to one of these, reuse the EXACT existing name (same spelling and casing) instead of a variant.
29385
+ When a memory refers to one of these, reuse the EXACT existing name (same spelling, casing, spacing and punctuation) instead of a variant \u2014 if "Foo.ai" is listed, never emit "Foo ai" or "Foo AI", and if "Jane Roe" is listed, emit that rather than a bare "Jane".
29204
29386
 
29205
29387
  ` : "";
29206
29388
  for (let i = 0; i < memories.length; i += TOPIC_EXTRACTION_BATCH_SIZE) {
@@ -5805,7 +5805,7 @@ declare function getUnfiledVaultMemoriesOp(ctx: VaultMemoryOperationsContext): P
5805
5805
  * by the next sweep, so topic-quality improvements propagate across the existing
5806
5806
  * vault. The worker's `limit` drains that re-extraction across sweeps.
5807
5807
  */
5808
- declare const TOPICS_EXTRACTION_VERSION = 2;
5808
+ declare const TOPICS_EXTRACTION_VERSION = 3;
5809
5809
  /**
5810
5810
  * Result of {@link getMemoriesNeedingTopicExtractionOp}: which memories the
5811
5811
  * background topic worker should run LLM entity extraction on, and which it
@@ -6943,6 +6943,12 @@ declare function createMemoryVaultTool(vaultCtx: VaultMemoryOperationsContext, o
6943
6943
  * during a busy window are still examined instead of scrolling out of a last-N
6944
6944
  * window.
6945
6945
  *
6946
+ * The watermark advances only when the extractor genuinely EXAMINED the window.
6947
+ * A pipeline throw and an `empty-after-retry` outcome (the extraction LLM
6948
+ * returned empty/malformed after exhausting its retries) both leave it in place,
6949
+ * so the next turn's window re-covers those messages instead of stranding them.
6950
+ * A quiet turn that legitimately yielded no facts *does* advance it.
6951
+ *
6946
6952
  * Durability: the in-memory watermark alone does NOT survive process death, so
6947
6953
  * a session killed after messages accumulate but before extraction would, on
6948
6954
  * the next launch, fall back to a trailing-window guess and could skip the
@@ -5805,7 +5805,7 @@ declare function getUnfiledVaultMemoriesOp(ctx: VaultMemoryOperationsContext): P
5805
5805
  * by the next sweep, so topic-quality improvements propagate across the existing
5806
5806
  * vault. The worker's `limit` drains that re-extraction across sweeps.
5807
5807
  */
5808
- declare const TOPICS_EXTRACTION_VERSION = 2;
5808
+ declare const TOPICS_EXTRACTION_VERSION = 3;
5809
5809
  /**
5810
5810
  * Result of {@link getMemoriesNeedingTopicExtractionOp}: which memories the
5811
5811
  * background topic worker should run LLM entity extraction on, and which it
@@ -6943,6 +6943,12 @@ declare function createMemoryVaultTool(vaultCtx: VaultMemoryOperationsContext, o
6943
6943
  * during a busy window are still examined instead of scrolling out of a last-N
6944
6944
  * window.
6945
6945
  *
6946
+ * The watermark advances only when the extractor genuinely EXAMINED the window.
6947
+ * A pipeline throw and an `empty-after-retry` outcome (the extraction LLM
6948
+ * returned empty/malformed after exhausting its retries) both leave it in place,
6949
+ * so the next turn's window re-covers those messages instead of stranding them.
6950
+ * A quiet turn that legitimately yielded no facts *does* advance it.
6951
+ *
6946
6952
  * Durability: the in-memory watermark alone does NOT survive process death, so
6947
6953
  * a session killed after messages accumulate but before extraction would, on
6948
6954
  * the next launch, fall back to a trailing-window guess and could skip the
@@ -125,7 +125,7 @@ import {
125
125
  updateVaultMemoryOp,
126
126
  upsertMessageOp,
127
127
  vaultMemoryToStored
128
- } from "./chunk-GKN5NJKZ.mjs";
128
+ } from "./chunk-FS56IDOW.mjs";
129
129
  import {
130
130
  createStreamingDeAnonymizer,
131
131
  resolvePiiRedactor
@@ -4831,6 +4831,162 @@ function createMemoryVaultTool(vaultCtx, options, embeddingOptions, cache) {
4831
4831
  // src/lib/memory/autoExtract.ts
4832
4832
  var import_watermelondb20 = __toESM(require_watermelondb());
4833
4833
 
4834
+ // src/lib/memory/entitySalience.ts
4835
+ var GENERIC_ENTITY_NAMES = /* @__PURE__ */ new Set([
4836
+ // Relative and absolute time
4837
+ "today",
4838
+ "tomorrow",
4839
+ "yesterday",
4840
+ "tonight",
4841
+ "morning",
4842
+ "afternoon",
4843
+ "evening",
4844
+ "night",
4845
+ "day",
4846
+ "date",
4847
+ "time",
4848
+ "week",
4849
+ "weekend",
4850
+ "weekday",
4851
+ "month",
4852
+ "year",
4853
+ "monday",
4854
+ "tuesday",
4855
+ "wednesday",
4856
+ "thursday",
4857
+ "friday",
4858
+ "saturday",
4859
+ "sunday",
4860
+ "january",
4861
+ "february",
4862
+ "march",
4863
+ "april",
4864
+ "may",
4865
+ "june",
4866
+ "july",
4867
+ "august",
4868
+ "september",
4869
+ "october",
4870
+ "november",
4871
+ "december",
4872
+ // Generic containers and venues — the named ones ("Blue Bottle on Valencia",
4873
+ // "Stanford") are unaffected because they are not bare tokens.
4874
+ "home",
4875
+ "house",
4876
+ "apartment",
4877
+ "room",
4878
+ "office",
4879
+ "work",
4880
+ "workplace",
4881
+ "school",
4882
+ "gym",
4883
+ "store",
4884
+ "shop",
4885
+ "hospital",
4886
+ "airport",
4887
+ "hotel",
4888
+ "city",
4889
+ "town",
4890
+ "country",
4891
+ "place",
4892
+ // Generic calendar entries — "Chicago Marathon" survives, "meeting" doesn't.
4893
+ "meeting",
4894
+ "call",
4895
+ "appointment",
4896
+ "reminder",
4897
+ "event",
4898
+ "birthday",
4899
+ "breakfast",
4900
+ "lunch",
4901
+ "dinner",
4902
+ "brunch",
4903
+ "vacation",
4904
+ "holiday",
4905
+ "trip",
4906
+ // Plurals are listed explicitly rather than stemmed: naive `s`-stripping
4907
+ // would fold real titles into generics ("The Times" → "time"). Only plurals
4908
+ // that are junk as topics in their own right belong here — note the absence
4909
+ // of "times", "places", and anything else that reads as a name.
4910
+ "days",
4911
+ "weeks",
4912
+ "weekends",
4913
+ "months",
4914
+ "years",
4915
+ "rooms",
4916
+ "offices",
4917
+ "houses",
4918
+ "apartments",
4919
+ "stores",
4920
+ "shops",
4921
+ "hotels",
4922
+ "meetings",
4923
+ "calls",
4924
+ "appointments",
4925
+ "reminders",
4926
+ "events",
4927
+ "birthdays",
4928
+ "trips",
4929
+ "vacations",
4930
+ "holidays",
4931
+ // Meta / filler the models occasionally echo back as an entity
4932
+ "user",
4933
+ "assistant",
4934
+ "memory",
4935
+ "memories",
4936
+ "note",
4937
+ "notes",
4938
+ "thing",
4939
+ "things",
4940
+ "stuff",
4941
+ "something",
4942
+ "someone",
4943
+ "anything",
4944
+ "everything",
4945
+ "none",
4946
+ "other",
4947
+ "misc",
4948
+ "general",
4949
+ "unknown"
4950
+ ]);
4951
+ function isGenericEntityName(name) {
4952
+ const tokens = name.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((token) => token.length > 0);
4953
+ if (tokens.length === 0) return true;
4954
+ const distinctive = tokens.filter(
4955
+ (token) => !GENERIC_ENTITY_NAMES.has(token) && !ARTICLES.has(token) && !WEAK_MODIFIERS.has(token)
4956
+ );
4957
+ if (distinctive.length > 0) return false;
4958
+ const generic = tokens.filter((token) => GENERIC_ENTITY_NAMES.has(token));
4959
+ if (generic.length === 0) return true;
4960
+ const titleShaped = generic.length === 1 && tokens.some((token) => ARTICLES.has(token)) && !tokens.some((token) => WEAK_MODIFIERS.has(token));
4961
+ return !titleShaped;
4962
+ }
4963
+ var ARTICLES = /* @__PURE__ */ new Set(["the", "a", "an"]);
4964
+ var WEAK_MODIFIERS = /* @__PURE__ */ new Set([
4965
+ "my",
4966
+ "our",
4967
+ "their",
4968
+ "his",
4969
+ "her",
4970
+ "its",
4971
+ "this",
4972
+ "that",
4973
+ "next",
4974
+ "last",
4975
+ "upcoming",
4976
+ "every",
4977
+ "at",
4978
+ "in",
4979
+ "on",
4980
+ "of",
4981
+ "for",
4982
+ "with",
4983
+ "from",
4984
+ "to",
4985
+ "by",
4986
+ "and",
4987
+ "or"
4988
+ ]);
4989
+
4834
4990
  // src/lib/memory/injectionClassifier.ts
4835
4991
  var DEFAULT_MODEL = "inclusionai/ling-2.6-flash";
4836
4992
  var DEFAULT_ATTEMPTS = 2;
@@ -5759,13 +5915,13 @@ function parseEntities(raw) {
5759
5915
  for (const e of raw) {
5760
5916
  if (typeof e === "string") {
5761
5917
  const name2 = e.trim();
5762
- if (name2.length > 0) out.push({ name: name2 });
5918
+ if (name2.length > 0 && !isGenericEntityName(name2)) out.push({ name: name2 });
5763
5919
  continue;
5764
5920
  }
5765
5921
  if (typeof e !== "object" || e === null) continue;
5766
5922
  const obj = e;
5767
5923
  const name = typeof obj.name === "string" ? obj.name.trim() : "";
5768
- if (name.length === 0) continue;
5924
+ if (name.length === 0 || isGenericEntityName(name)) continue;
5769
5925
  const kind = typeof obj.kind === "string" && ENTITY_KINDS.includes(obj.kind) ? obj.kind : void 0;
5770
5926
  out.push(kind !== void 0 ? { name, kind } : { name });
5771
5927
  }
@@ -5985,9 +6141,11 @@ function createAutoExtractor(options) {
5985
6141
  }
5986
6142
  }
5987
6143
  );
5988
- const advancedTo = window2[window2.length - 1].id;
5989
- stateFor(conversationId).watermark = advancedTo;
5990
- persistCursor(conversationId, messages, advancedTo, contiguous);
6144
+ if (outcome !== "empty-after-retry") {
6145
+ const advancedTo = window2[window2.length - 1].id;
6146
+ stateFor(conversationId).watermark = advancedTo;
6147
+ persistCursor(conversationId, messages, advancedTo, contiguous);
6148
+ }
5991
6149
  for (let i = 0; i < results.length; i++) {
5992
6150
  if (results[i].action === "suppressed") continue;
5993
6151
  options.onMemoryExtracted?.({
@@ -7068,7 +7226,11 @@ var SYSTEM_PROMPT4 = `You assign topics to saved memories for a personal memory
7068
7226
 
7069
7227
  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.
7070
7228
 
7071
- Include only NAMED entities, skip generic/common nouns. ${ENTITY_KIND_GUIDELINES}
7229
+ Include only NAMED entities, skip generic/common nouns. A topic must identify a specific thing the user could recognize by name; if it would be equally true of thousands of unrelated memories, leave it out:
7230
+ - SKIP bare generic nouns even when the memory capitalizes them or a calendar entry is titled that way \u2014 "Home", "Work", "Meeting", "Lunch", "Birthday", "Appointment", "Trip", "Today", "Friday", "March" are NOT topics.
7231
+ - KEEP the specific thing instead, when the memory names one: "Blue Bottle on Valencia" over "cafe", "Chicago Marathon" over "event", "Hollowpoint Labs" over "work".
7232
+ - Prefer 0 entities over a weak one. A memory whose only candidate is a generic noun has NO entities.
7233
+ ${ENTITY_KIND_GUIDELINES}
7072
7234
 
7073
7235
  Each memory below is written as "<id>: <text>". Output strict JSON: {"memories": [{"id": string, "entities": [{"name": string, "kind": string}]}]}
7074
7236
  - exactly one element per input memory; copy its id EXACTLY as written (the token before the first ": "), with no brackets, quotes, or extra characters
@@ -7080,7 +7242,7 @@ async function extractEntitiesForMemories(memories, options) {
7080
7242
  const redactor = resolvePiiRedactor(options.piiRedaction);
7081
7243
  const vocabulary = (options.existingEntityNames ?? []).filter((n) => n.trim().length > 0).slice(0, MAX_VOCABULARY_NAMES).map((n) => redactor ? redactor.redactText(n).text : n);
7082
7244
  const vocabularyNote = vocabulary.length > 0 ? `The user's existing topics: ${vocabulary.join(", ")}.
7083
- When a memory refers to one of these, reuse the EXACT existing name (same spelling and casing) instead of a variant.
7245
+ When a memory refers to one of these, reuse the EXACT existing name (same spelling, casing, spacing and punctuation) instead of a variant \u2014 if "Foo.ai" is listed, never emit "Foo ai" or "Foo AI", and if "Jane Roe" is listed, emit that rather than a bare "Jane".
7084
7246
 
7085
7247
  ` : "";
7086
7248
  for (let i = 0; i < memories.length; i += TOPIC_EXTRACTION_BATCH_SIZE) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  recall
3
- } from "./chunk-GKN5NJKZ.mjs";
3
+ } from "./chunk-FS56IDOW.mjs";
4
4
  import "./chunk-MIXEASUP.mjs";
5
5
  import "./chunk-3RPGXXPZ.mjs";
6
6
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anuma/sdk",
3
- "version": "3.8.3",
3
+ "version": "3.8.5",
4
4
  "description": "",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",