@anuma/sdk 3.8.3 → 3.8.4

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.
@@ -4229,7 +4229,7 @@ function entityToStored(e) {
4229
4229
  updatedAt: e.updatedAt
4230
4230
  };
4231
4231
  }
4232
- async function upsertEntitiesOp(ctx, entities) {
4232
+ async function upsertEntitiesInWrite(ctx, entities) {
4233
4233
  const kindByName = /* @__PURE__ */ new Map();
4234
4234
  for (const e of entities) {
4235
4235
  const name = normalizeEntityName(e.name);
@@ -4243,60 +4243,62 @@ async function upsertEntitiesOp(ctx, entities) {
4243
4243
  }
4244
4244
  const unique = Array.from(kindByName.keys());
4245
4245
  const out = /* @__PURE__ */ new Map();
4246
- if (unique.length === 0) return out;
4247
- return await ctx.database.write(async () => {
4248
- const existing = await ctx.entityCollection.query(import_watermelondb9.Q.where("canonical_name", import_watermelondb9.Q.oneOf(unique))).fetch();
4249
- const existingNames = new Set(existing.map((e) => e.canonicalName));
4250
- const updates = existing.filter((e) => {
4251
- const incoming = kindByName.get(e.canonicalName);
4252
- return incoming !== void 0 && (e.kind === null || e.kind === void 0 || e.kind === "");
4253
- });
4254
- const missing = unique.filter((n) => !existingNames.has(n));
4255
- const created = missing.map(
4256
- (name) => ctx.entityCollection.prepareCreate((record) => {
4257
- record._setRaw("canonical_name", name);
4258
- const kind = kindByName.get(name);
4259
- if (kind !== void 0) record._setRaw("kind", kind);
4260
- })
4261
- );
4262
- const updated = updates.map(
4263
- (e) => e.prepareUpdate((record) => {
4264
- record._setRaw("kind", kindByName.get(e.canonicalName));
4265
- })
4266
- );
4267
- if (created.length > 0 || updated.length > 0) {
4268
- await ctx.database.batch(...created, ...updated);
4269
- }
4270
- for (const e of existing) out.set(e.canonicalName, entityToStored(e));
4271
- for (const record of created) out.set(record.canonicalName, entityToStored(record));
4272
- return out;
4246
+ if (unique.length === 0) return { entities: out, operations: [] };
4247
+ const existing = await ctx.entityCollection.query(import_watermelondb9.Q.where("canonical_name", import_watermelondb9.Q.oneOf(unique))).fetch();
4248
+ const existingNames = new Set(existing.map((e) => e.canonicalName));
4249
+ const updates = existing.filter((e) => {
4250
+ const incoming = kindByName.get(e.canonicalName);
4251
+ return incoming !== void 0 && (e.kind === null || e.kind === void 0 || e.kind === "");
4273
4252
  });
4253
+ const missing = unique.filter((n) => !existingNames.has(n));
4254
+ const created = missing.map(
4255
+ (name) => ctx.entityCollection.prepareCreate((record) => {
4256
+ record._setRaw("canonical_name", name);
4257
+ const kind = kindByName.get(name);
4258
+ if (kind !== void 0) record._setRaw("kind", kind);
4259
+ })
4260
+ );
4261
+ const updated = updates.map(
4262
+ (e) => e.prepareUpdate((record) => {
4263
+ record._setRaw("kind", kindByName.get(e.canonicalName));
4264
+ })
4265
+ );
4266
+ for (const e of existing) out.set(e.canonicalName, entityToStored(e));
4267
+ for (const record of created) out.set(record.canonicalName, entityToStored(record));
4268
+ return { entities: out, operations: [...created, ...updated] };
4274
4269
  }
4275
4270
  async function linkMemoryEntitiesOp(ctx, memoryId, entityInputs, options) {
4276
4271
  if (entityInputs.length === 0) return [];
4277
4272
  const normalized = entityInputs.map((e) => typeof e === "string" ? { name: e } : e);
4278
- const byName = await upsertEntitiesOp(ctx, normalized);
4279
- const entities = Array.from(byName.values());
4280
- if (entities.length === 0) return [];
4281
4273
  const userId = ctx.userId;
4282
4274
  let skipped = false;
4275
+ let entities = [];
4283
4276
  await ctx.database.write(async () => {
4277
+ const { entities: byName, operations: entityOps } = await upsertEntitiesInWrite(
4278
+ ctx,
4279
+ normalized
4280
+ );
4281
+ entities = Array.from(byName.values());
4284
4282
  if (options?.unlessTopicsUserManaged && await autoLinkBlocked(ctx, memoryId)) {
4283
+ if (entityOps.length > 0) {
4284
+ await ctx.database.batch(...entityOps);
4285
+ }
4285
4286
  skipped = true;
4286
4287
  return;
4287
4288
  }
4289
+ if (entities.length === 0) return;
4288
4290
  const existingLinks = await ctx.memoryEntityCollection.query(import_watermelondb9.Q.where("memory_id", memoryId)).fetch();
4289
4291
  const existingEntityIds = new Set(existingLinks.map((l) => String(l.entityId)));
4290
4292
  const toCreate = entities.filter((e) => !existingEntityIds.has(e.uniqueId));
4291
- if (toCreate.length === 0) return;
4292
- const prepared = toCreate.map(
4293
+ if (entityOps.length === 0 && toCreate.length === 0) return;
4294
+ const linkOps = toCreate.map(
4293
4295
  (e) => ctx.memoryEntityCollection.prepareCreate((record) => {
4294
4296
  record._setRaw("memory_id", memoryId);
4295
4297
  record._setRaw("entity_id", e.uniqueId);
4296
4298
  if (userId !== void 0) record._setRaw("user_id", userId);
4297
4299
  })
4298
4300
  );
4299
- await ctx.database.batch(...prepared);
4301
+ await ctx.database.batch(...entityOps, ...linkOps);
4300
4302
  });
4301
4303
  return skipped ? [] : entities;
4302
4304
  }
@@ -4311,22 +4313,28 @@ async function autoLinkBlocked(ctx, memoryId) {
4311
4313
  }
4312
4314
  async function replaceMemoryEntitiesGuardedOp(ctx, memoryId, entityInputs) {
4313
4315
  const normalized = entityInputs.map((e) => typeof e === "string" ? { name: e } : e);
4314
- const byName = await upsertEntitiesOp(ctx, normalized);
4315
- const entities = Array.from(byName.values());
4316
4316
  const userId = ctx.userId;
4317
4317
  let skipped = false;
4318
+ let entities = [];
4318
4319
  await ctx.database.write(async () => {
4319
4320
  if (await autoLinkBlocked(ctx, memoryId)) {
4320
4321
  skipped = true;
4321
4322
  return;
4322
4323
  }
4324
+ const { entities: byName, operations: entityOps } = await upsertEntitiesInWrite(
4325
+ ctx,
4326
+ normalized
4327
+ );
4328
+ entities = Array.from(byName.values());
4323
4329
  const existingLinks = await ctx.memoryEntityCollection.query(import_watermelondb9.Q.where("memory_id", memoryId)).fetch();
4324
4330
  const keep = new Set(entities.map((e) => e.uniqueId));
4325
4331
  const existingEntityIds = new Set(existingLinks.map((l) => String(l.entityId)));
4326
4332
  const toCreate = entities.filter((e) => !existingEntityIds.has(e.uniqueId));
4327
4333
  const toDestroy = existingLinks.filter((l) => !keep.has(String(l.entityId)));
4328
- if (toCreate.length === 0 && toDestroy.length === 0) return;
4334
+ if (entityOps.length === 0 && toCreate.length === 0 && toDestroy.length === 0) return;
4335
+ const orphans = await findOrphanedEntities(ctx, memoryId, toDestroy);
4329
4336
  await ctx.database.batch(
4337
+ ...entityOps,
4330
4338
  ...toCreate.map(
4331
4339
  (e) => ctx.memoryEntityCollection.prepareCreate((record) => {
4332
4340
  record._setRaw("memory_id", memoryId);
@@ -4334,11 +4342,23 @@ async function replaceMemoryEntitiesGuardedOp(ctx, memoryId, entityInputs) {
4334
4342
  if (userId !== void 0) record._setRaw("user_id", userId);
4335
4343
  })
4336
4344
  ),
4337
- ...toDestroy.map((l) => l.prepareDestroyPermanently())
4345
+ ...toDestroy.map((l) => l.prepareDestroyPermanently()),
4346
+ ...orphans.map((e) => e.prepareDestroyPermanently())
4338
4347
  );
4339
4348
  });
4340
4349
  return skipped ? null : entities;
4341
4350
  }
4351
+ async function findOrphanedEntities(ctx, memoryId, toDestroy) {
4352
+ const candidateIds = [...new Set(toDestroy.map((l) => String(l.entityId)))];
4353
+ if (candidateIds.length === 0) return [];
4354
+ const links = await ctx.memoryEntityCollection.query(import_watermelondb9.Q.where("entity_id", import_watermelondb9.Q.oneOf(candidateIds))).fetch();
4355
+ const stillLinked = new Set(
4356
+ links.filter((l) => String(l.memoryId) !== memoryId).map((l) => String(l.entityId))
4357
+ );
4358
+ const orphanIds = candidateIds.filter((id) => !stillLinked.has(id));
4359
+ if (orphanIds.length === 0) return [];
4360
+ return await ctx.entityCollection.query(import_watermelondb9.Q.where("id", import_watermelondb9.Q.oneOf(orphanIds))).fetch();
4361
+ }
4342
4362
  async function unlinkMemoryEntitiesOp(ctx, memoryIds) {
4343
4363
  if (memoryIds.length === 0) return;
4344
4364
  const links = await ctx.memoryEntityCollection.query(import_watermelondb9.Q.where("memory_id", import_watermelondb9.Q.oneOf(Array.from(memoryIds)))).fetch();
@@ -5275,7 +5295,7 @@ var init_operations3 = __esm({
5275
5295
  init_operations2();
5276
5296
  init_encryption2();
5277
5297
  KNOWN_TRUST_TIERS = /* @__PURE__ */ new Set(["quarantined", "trusted"]);
5278
- TOPICS_EXTRACTION_VERSION = 2;
5298
+ TOPICS_EXTRACTION_VERSION = 3;
5279
5299
  }
5280
5300
  });
5281
5301
 
@@ -15034,6 +15054,162 @@ init_types3();
15034
15054
  init_logger();
15035
15055
  init_redactor();
15036
15056
 
15057
+ // src/lib/memory/entitySalience.ts
15058
+ var GENERIC_ENTITY_NAMES = /* @__PURE__ */ new Set([
15059
+ // Relative and absolute time
15060
+ "today",
15061
+ "tomorrow",
15062
+ "yesterday",
15063
+ "tonight",
15064
+ "morning",
15065
+ "afternoon",
15066
+ "evening",
15067
+ "night",
15068
+ "day",
15069
+ "date",
15070
+ "time",
15071
+ "week",
15072
+ "weekend",
15073
+ "weekday",
15074
+ "month",
15075
+ "year",
15076
+ "monday",
15077
+ "tuesday",
15078
+ "wednesday",
15079
+ "thursday",
15080
+ "friday",
15081
+ "saturday",
15082
+ "sunday",
15083
+ "january",
15084
+ "february",
15085
+ "march",
15086
+ "april",
15087
+ "may",
15088
+ "june",
15089
+ "july",
15090
+ "august",
15091
+ "september",
15092
+ "october",
15093
+ "november",
15094
+ "december",
15095
+ // Generic containers and venues — the named ones ("Blue Bottle on Valencia",
15096
+ // "Stanford") are unaffected because they are not bare tokens.
15097
+ "home",
15098
+ "house",
15099
+ "apartment",
15100
+ "room",
15101
+ "office",
15102
+ "work",
15103
+ "workplace",
15104
+ "school",
15105
+ "gym",
15106
+ "store",
15107
+ "shop",
15108
+ "hospital",
15109
+ "airport",
15110
+ "hotel",
15111
+ "city",
15112
+ "town",
15113
+ "country",
15114
+ "place",
15115
+ // Generic calendar entries — "Chicago Marathon" survives, "meeting" doesn't.
15116
+ "meeting",
15117
+ "call",
15118
+ "appointment",
15119
+ "reminder",
15120
+ "event",
15121
+ "birthday",
15122
+ "breakfast",
15123
+ "lunch",
15124
+ "dinner",
15125
+ "brunch",
15126
+ "vacation",
15127
+ "holiday",
15128
+ "trip",
15129
+ // Plurals are listed explicitly rather than stemmed: naive `s`-stripping
15130
+ // would fold real titles into generics ("The Times" → "time"). Only plurals
15131
+ // that are junk as topics in their own right belong here — note the absence
15132
+ // of "times", "places", and anything else that reads as a name.
15133
+ "days",
15134
+ "weeks",
15135
+ "weekends",
15136
+ "months",
15137
+ "years",
15138
+ "rooms",
15139
+ "offices",
15140
+ "houses",
15141
+ "apartments",
15142
+ "stores",
15143
+ "shops",
15144
+ "hotels",
15145
+ "meetings",
15146
+ "calls",
15147
+ "appointments",
15148
+ "reminders",
15149
+ "events",
15150
+ "birthdays",
15151
+ "trips",
15152
+ "vacations",
15153
+ "holidays",
15154
+ // Meta / filler the models occasionally echo back as an entity
15155
+ "user",
15156
+ "assistant",
15157
+ "memory",
15158
+ "memories",
15159
+ "note",
15160
+ "notes",
15161
+ "thing",
15162
+ "things",
15163
+ "stuff",
15164
+ "something",
15165
+ "someone",
15166
+ "anything",
15167
+ "everything",
15168
+ "none",
15169
+ "other",
15170
+ "misc",
15171
+ "general",
15172
+ "unknown"
15173
+ ]);
15174
+ function isGenericEntityName(name) {
15175
+ const tokens = name.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((token) => token.length > 0);
15176
+ if (tokens.length === 0) return true;
15177
+ const distinctive = tokens.filter(
15178
+ (token) => !GENERIC_ENTITY_NAMES.has(token) && !ARTICLES.has(token) && !WEAK_MODIFIERS.has(token)
15179
+ );
15180
+ if (distinctive.length > 0) return false;
15181
+ const generic = tokens.filter((token) => GENERIC_ENTITY_NAMES.has(token));
15182
+ if (generic.length === 0) return true;
15183
+ const titleShaped = generic.length === 1 && tokens.some((token) => ARTICLES.has(token)) && !tokens.some((token) => WEAK_MODIFIERS.has(token));
15184
+ return !titleShaped;
15185
+ }
15186
+ var ARTICLES = /* @__PURE__ */ new Set(["the", "a", "an"]);
15187
+ var WEAK_MODIFIERS = /* @__PURE__ */ new Set([
15188
+ "my",
15189
+ "our",
15190
+ "their",
15191
+ "his",
15192
+ "her",
15193
+ "its",
15194
+ "this",
15195
+ "that",
15196
+ "next",
15197
+ "last",
15198
+ "upcoming",
15199
+ "every",
15200
+ "at",
15201
+ "in",
15202
+ "on",
15203
+ "of",
15204
+ "for",
15205
+ "with",
15206
+ "from",
15207
+ "to",
15208
+ "by",
15209
+ "and",
15210
+ "or"
15211
+ ]);
15212
+
15037
15213
  // src/lib/memory/injectionClassifier.ts
15038
15214
  init_logger();
15039
15215
  init_redactor();
@@ -15974,13 +16150,13 @@ function parseEntities(raw) {
15974
16150
  for (const e of raw) {
15975
16151
  if (typeof e === "string") {
15976
16152
  const name2 = e.trim();
15977
- if (name2.length > 0) out.push({ name: name2 });
16153
+ if (name2.length > 0 && !isGenericEntityName(name2)) out.push({ name: name2 });
15978
16154
  continue;
15979
16155
  }
15980
16156
  if (typeof e !== "object" || e === null) continue;
15981
16157
  const obj = e;
15982
16158
  const name = typeof obj.name === "string" ? obj.name.trim() : "";
15983
- if (name.length === 0) continue;
16159
+ if (name.length === 0 || isGenericEntityName(name)) continue;
15984
16160
  const kind = typeof obj.kind === "string" && ENTITY_KINDS.includes(obj.kind) ? obj.kind : void 0;
15985
16161
  out.push(kind !== void 0 ? { name, kind } : { name });
15986
16162
  }
@@ -17322,7 +17498,11 @@ var SYSTEM_PROMPT6 = `You assign topics to saved memories for a personal memory
17322
17498
 
17323
17499
  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.
17324
17500
 
17325
- Include only NAMED entities, skip generic/common nouns. ${ENTITY_KIND_GUIDELINES}
17501
+ 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:
17502
+ - 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.
17503
+ - KEEP the specific thing instead, when the memory names one: "Blue Bottle on Valencia" over "cafe", "Chicago Marathon" over "event", "Hollowpoint Labs" over "work".
17504
+ - Prefer 0 entities over a weak one. A memory whose only candidate is a generic noun has NO entities.
17505
+ ${ENTITY_KIND_GUIDELINES}
17326
17506
 
17327
17507
  Each memory below is written as "<id>: <text>". Output strict JSON: {"memories": [{"id": string, "entities": [{"name": string, "kind": string}]}]}
17328
17508
  - exactly one element per input memory; copy its id EXACTLY as written (the token before the first ": "), with no brackets, quotes, or extra characters
@@ -17334,7 +17514,7 @@ async function extractEntitiesForMemories(memories, options) {
17334
17514
  const redactor = resolvePiiRedactor(options.piiRedaction);
17335
17515
  const vocabulary = (options.existingEntityNames ?? []).filter((n) => n.trim().length > 0).slice(0, MAX_VOCABULARY_NAMES).map((n) => redactor ? redactor.redactText(n).text : n);
17336
17516
  const vocabularyNote = vocabulary.length > 0 ? `The user's existing topics: ${vocabulary.join(", ")}.
17337
- When a memory refers to one of these, reuse the EXACT existing name (same spelling and casing) instead of a variant.
17517
+ 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".
17338
17518
 
17339
17519
  ` : "";
17340
17520
  for (let i = 0; i < memories.length; i += TOPIC_EXTRACTION_BATCH_SIZE) {
@@ -5771,7 +5771,7 @@ interface EntityOperationsContext {
5771
5771
  * Link a memory to one or more entities. Accepts bare names (back-compat)
5772
5772
  * or `{ name, kind }` objects. Names are normalized; missing entities are
5773
5773
  * auto-created (with their kind), and an existing entity's null kind is
5774
- * back-filled — see {@link upsertEntitiesOp}. Idempotent — duplicate
5774
+ * back-filled — see {@link upsertEntitiesInWrite}. Idempotent — duplicate
5775
5775
  * (memory_id, entity_id) pairs are skipped.
5776
5776
  *
5777
5777
  * `options.unlessTopicsUserManaged` re-checks the memory's vault row INSIDE
@@ -5782,8 +5782,9 @@ interface EntityOperationsContext {
5782
5782
  * mid-call (orphaning links the cascade already swept) — so only an in-write
5783
5783
  * check guarantees a user's manual edit or delete can't be grafted over. The
5784
5784
  * row read fails CLOSED (skip links) so a transient fault never attaches
5785
- * topics to a memory we couldn't verify. Entity upserts still happen
5786
- * (vocabulary is global); returns [] when links were skipped.
5785
+ * topics to a memory we couldn't verify. Entity upserts and link creation
5786
+ * run in ONE writer to prevent orphan-prune races; returns [] when links
5787
+ * were skipped.
5787
5788
  */
5788
5789
  declare function linkMemoryEntitiesOp(ctx: EntityOperationsContext, memoryId: string, entityInputs: ReadonlyArray<EntityInput>, options?: {
5789
5790
  unlessTopicsUserManaged?: boolean;
@@ -5798,6 +5799,10 @@ declare function linkMemoryEntitiesOp(ctx: EntityOperationsContext, memoryId: st
5798
5799
  * destroy-stale are batched in ONE writer, after the same in-write guard as
5799
5800
  * the link op (user-managed / deleted / absent / read-fault ⇒ skip).
5800
5801
  *
5802
+ * That batch also prunes `entity` rows left with no links at all (see
5803
+ * {@link findOrphanedEntities}) — otherwise a topic the extractor has disowned
5804
+ * keeps rendering as a chip that matches no memory.
5805
+ *
5801
5806
  * Returns the linked entities ([] for an answered-empty set), or null when
5802
5807
  * the guard skipped — callers must treat null as "not persisted" (e.g. don't
5803
5808
  * stamp `topics_extracted_at`).
@@ -7208,7 +7213,7 @@ declare function getUnfiledVaultMemoriesOp(ctx: VaultMemoryOperationsContext): P
7208
7213
  * by the next sweep, so topic-quality improvements propagate across the existing
7209
7214
  * vault. The worker's `limit` drains that re-extraction across sweeps.
7210
7215
  */
7211
- declare const TOPICS_EXTRACTION_VERSION = 2;
7216
+ declare const TOPICS_EXTRACTION_VERSION = 3;
7212
7217
  /**
7213
7218
  * Result of {@link getMemoriesNeedingTopicExtractionOp}: which memories the
7214
7219
  * background topic worker should run LLM entity extraction on, and which it
@@ -5771,7 +5771,7 @@ interface EntityOperationsContext {
5771
5771
  * Link a memory to one or more entities. Accepts bare names (back-compat)
5772
5772
  * or `{ name, kind }` objects. Names are normalized; missing entities are
5773
5773
  * auto-created (with their kind), and an existing entity's null kind is
5774
- * back-filled — see {@link upsertEntitiesOp}. Idempotent — duplicate
5774
+ * back-filled — see {@link upsertEntitiesInWrite}. Idempotent — duplicate
5775
5775
  * (memory_id, entity_id) pairs are skipped.
5776
5776
  *
5777
5777
  * `options.unlessTopicsUserManaged` re-checks the memory's vault row INSIDE
@@ -5782,8 +5782,9 @@ interface EntityOperationsContext {
5782
5782
  * mid-call (orphaning links the cascade already swept) — so only an in-write
5783
5783
  * check guarantees a user's manual edit or delete can't be grafted over. The
5784
5784
  * row read fails CLOSED (skip links) so a transient fault never attaches
5785
- * topics to a memory we couldn't verify. Entity upserts still happen
5786
- * (vocabulary is global); returns [] when links were skipped.
5785
+ * topics to a memory we couldn't verify. Entity upserts and link creation
5786
+ * run in ONE writer to prevent orphan-prune races; returns [] when links
5787
+ * were skipped.
5787
5788
  */
5788
5789
  declare function linkMemoryEntitiesOp(ctx: EntityOperationsContext, memoryId: string, entityInputs: ReadonlyArray<EntityInput>, options?: {
5789
5790
  unlessTopicsUserManaged?: boolean;
@@ -5798,6 +5799,10 @@ declare function linkMemoryEntitiesOp(ctx: EntityOperationsContext, memoryId: st
5798
5799
  * destroy-stale are batched in ONE writer, after the same in-write guard as
5799
5800
  * the link op (user-managed / deleted / absent / read-fault ⇒ skip).
5800
5801
  *
5802
+ * That batch also prunes `entity` rows left with no links at all (see
5803
+ * {@link findOrphanedEntities}) — otherwise a topic the extractor has disowned
5804
+ * keeps rendering as a chip that matches no memory.
5805
+ *
5801
5806
  * Returns the linked entities ([] for an answered-empty set), or null when
5802
5807
  * the guard skipped — callers must treat null as "not persisted" (e.g. don't
5803
5808
  * stamp `topics_extracted_at`).
@@ -7208,7 +7213,7 @@ declare function getUnfiledVaultMemoriesOp(ctx: VaultMemoryOperationsContext): P
7208
7213
  * by the next sweep, so topic-quality improvements propagate across the existing
7209
7214
  * vault. The worker's `limit` drains that re-extraction across sweeps.
7210
7215
  */
7211
- declare const TOPICS_EXTRACTION_VERSION = 2;
7216
+ declare const TOPICS_EXTRACTION_VERSION = 3;
7212
7217
  /**
7213
7218
  * Result of {@link getMemoriesNeedingTopicExtractionOp}: which memories the
7214
7219
  * background topic worker should run LLM entity extraction on, and which it
@@ -149,7 +149,7 @@ import {
149
149
  updateVaultMemoryOp,
150
150
  useEncryption,
151
151
  vaultMemoryToStored
152
- } from "./chunk-35BHTCK4.mjs";
152
+ } from "./chunk-NLVSQAYH.mjs";
153
153
  import {
154
154
  PiiRedactor,
155
155
  createStreamingDeAnonymizer,
@@ -6743,6 +6743,162 @@ __decorateClass([
6743
6743
  // src/lib/memory/autoExtract.ts
6744
6744
  import { Q as Q4 } from "@nozbe/watermelondb";
6745
6745
 
6746
+ // src/lib/memory/entitySalience.ts
6747
+ var GENERIC_ENTITY_NAMES = /* @__PURE__ */ new Set([
6748
+ // Relative and absolute time
6749
+ "today",
6750
+ "tomorrow",
6751
+ "yesterday",
6752
+ "tonight",
6753
+ "morning",
6754
+ "afternoon",
6755
+ "evening",
6756
+ "night",
6757
+ "day",
6758
+ "date",
6759
+ "time",
6760
+ "week",
6761
+ "weekend",
6762
+ "weekday",
6763
+ "month",
6764
+ "year",
6765
+ "monday",
6766
+ "tuesday",
6767
+ "wednesday",
6768
+ "thursday",
6769
+ "friday",
6770
+ "saturday",
6771
+ "sunday",
6772
+ "january",
6773
+ "february",
6774
+ "march",
6775
+ "april",
6776
+ "may",
6777
+ "june",
6778
+ "july",
6779
+ "august",
6780
+ "september",
6781
+ "october",
6782
+ "november",
6783
+ "december",
6784
+ // Generic containers and venues — the named ones ("Blue Bottle on Valencia",
6785
+ // "Stanford") are unaffected because they are not bare tokens.
6786
+ "home",
6787
+ "house",
6788
+ "apartment",
6789
+ "room",
6790
+ "office",
6791
+ "work",
6792
+ "workplace",
6793
+ "school",
6794
+ "gym",
6795
+ "store",
6796
+ "shop",
6797
+ "hospital",
6798
+ "airport",
6799
+ "hotel",
6800
+ "city",
6801
+ "town",
6802
+ "country",
6803
+ "place",
6804
+ // Generic calendar entries — "Chicago Marathon" survives, "meeting" doesn't.
6805
+ "meeting",
6806
+ "call",
6807
+ "appointment",
6808
+ "reminder",
6809
+ "event",
6810
+ "birthday",
6811
+ "breakfast",
6812
+ "lunch",
6813
+ "dinner",
6814
+ "brunch",
6815
+ "vacation",
6816
+ "holiday",
6817
+ "trip",
6818
+ // Plurals are listed explicitly rather than stemmed: naive `s`-stripping
6819
+ // would fold real titles into generics ("The Times" → "time"). Only plurals
6820
+ // that are junk as topics in their own right belong here — note the absence
6821
+ // of "times", "places", and anything else that reads as a name.
6822
+ "days",
6823
+ "weeks",
6824
+ "weekends",
6825
+ "months",
6826
+ "years",
6827
+ "rooms",
6828
+ "offices",
6829
+ "houses",
6830
+ "apartments",
6831
+ "stores",
6832
+ "shops",
6833
+ "hotels",
6834
+ "meetings",
6835
+ "calls",
6836
+ "appointments",
6837
+ "reminders",
6838
+ "events",
6839
+ "birthdays",
6840
+ "trips",
6841
+ "vacations",
6842
+ "holidays",
6843
+ // Meta / filler the models occasionally echo back as an entity
6844
+ "user",
6845
+ "assistant",
6846
+ "memory",
6847
+ "memories",
6848
+ "note",
6849
+ "notes",
6850
+ "thing",
6851
+ "things",
6852
+ "stuff",
6853
+ "something",
6854
+ "someone",
6855
+ "anything",
6856
+ "everything",
6857
+ "none",
6858
+ "other",
6859
+ "misc",
6860
+ "general",
6861
+ "unknown"
6862
+ ]);
6863
+ function isGenericEntityName(name) {
6864
+ const tokens = name.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter((token) => token.length > 0);
6865
+ if (tokens.length === 0) return true;
6866
+ const distinctive = tokens.filter(
6867
+ (token) => !GENERIC_ENTITY_NAMES.has(token) && !ARTICLES.has(token) && !WEAK_MODIFIERS.has(token)
6868
+ );
6869
+ if (distinctive.length > 0) return false;
6870
+ const generic = tokens.filter((token) => GENERIC_ENTITY_NAMES.has(token));
6871
+ if (generic.length === 0) return true;
6872
+ const titleShaped = generic.length === 1 && tokens.some((token) => ARTICLES.has(token)) && !tokens.some((token) => WEAK_MODIFIERS.has(token));
6873
+ return !titleShaped;
6874
+ }
6875
+ var ARTICLES = /* @__PURE__ */ new Set(["the", "a", "an"]);
6876
+ var WEAK_MODIFIERS = /* @__PURE__ */ new Set([
6877
+ "my",
6878
+ "our",
6879
+ "their",
6880
+ "his",
6881
+ "her",
6882
+ "its",
6883
+ "this",
6884
+ "that",
6885
+ "next",
6886
+ "last",
6887
+ "upcoming",
6888
+ "every",
6889
+ "at",
6890
+ "in",
6891
+ "on",
6892
+ "of",
6893
+ "for",
6894
+ "with",
6895
+ "from",
6896
+ "to",
6897
+ "by",
6898
+ "and",
6899
+ "or"
6900
+ ]);
6901
+
6746
6902
  // src/lib/memory/injectionClassifier.ts
6747
6903
  var DEFAULT_MODEL = "inclusionai/ling-2.6-flash";
6748
6904
  var DEFAULT_ATTEMPTS = 2;
@@ -7671,13 +7827,13 @@ function parseEntities(raw) {
7671
7827
  for (const e of raw) {
7672
7828
  if (typeof e === "string") {
7673
7829
  const name2 = e.trim();
7674
- if (name2.length > 0) out.push({ name: name2 });
7830
+ if (name2.length > 0 && !isGenericEntityName(name2)) out.push({ name: name2 });
7675
7831
  continue;
7676
7832
  }
7677
7833
  if (typeof e !== "object" || e === null) continue;
7678
7834
  const obj = e;
7679
7835
  const name = typeof obj.name === "string" ? obj.name.trim() : "";
7680
- if (name.length === 0) continue;
7836
+ if (name.length === 0 || isGenericEntityName(name)) continue;
7681
7837
  const kind = typeof obj.kind === "string" && ENTITY_KINDS.includes(obj.kind) ? obj.kind : void 0;
7682
7838
  out.push(kind !== void 0 ? { name, kind } : { name });
7683
7839
  }
@@ -8980,7 +9136,11 @@ var SYSTEM_PROMPT4 = `You assign topics to saved memories for a personal memory
8980
9136
 
8981
9137
  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.
8982
9138
 
8983
- Include only NAMED entities, skip generic/common nouns. ${ENTITY_KIND_GUIDELINES}
9139
+ 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:
9140
+ - 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.
9141
+ - KEEP the specific thing instead, when the memory names one: "Blue Bottle on Valencia" over "cafe", "Chicago Marathon" over "event", "Hollowpoint Labs" over "work".
9142
+ - Prefer 0 entities over a weak one. A memory whose only candidate is a generic noun has NO entities.
9143
+ ${ENTITY_KIND_GUIDELINES}
8984
9144
 
8985
9145
  Each memory below is written as "<id>: <text>". Output strict JSON: {"memories": [{"id": string, "entities": [{"name": string, "kind": string}]}]}
8986
9146
  - exactly one element per input memory; copy its id EXACTLY as written (the token before the first ": "), with no brackets, quotes, or extra characters
@@ -8992,7 +9152,7 @@ async function extractEntitiesForMemories(memories, options) {
8992
9152
  const redactor = resolvePiiRedactor(options.piiRedaction);
8993
9153
  const vocabulary = (options.existingEntityNames ?? []).filter((n) => n.trim().length > 0).slice(0, MAX_VOCABULARY_NAMES).map((n) => redactor ? redactor.redactText(n).text : n);
8994
9154
  const vocabularyNote = vocabulary.length > 0 ? `The user's existing topics: ${vocabulary.join(", ")}.
8995
- When a memory refers to one of these, reuse the EXACT existing name (same spelling and casing) instead of a variant.
9155
+ 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".
8996
9156
 
8997
9157
  ` : "";
8998
9158
  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-35BHTCK4.mjs";
3
+ } from "./chunk-NLVSQAYH.mjs";
4
4
  import "./chunk-BOY27A2C.mjs";
5
5
  import "./chunk-GYKJOWEO.mjs";
6
6
  export {