@anuma/sdk 4.0.0 → 4.0.2

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.
@@ -6083,6 +6083,12 @@ declare function getUnfiledVaultMemoriesOp(ctx: VaultMemoryOperationsContext): P
6083
6083
  * older version (including pre-v37 rows, read as version 0) is then re-extracted
6084
6084
  * by the next sweep, so topic-quality improvements propagate across the existing
6085
6085
  * vault. The worker's `limit` drains that re-extraction across sweeps.
6086
+ *
6087
+ * Bumping this is a WHOLE-VAULT re-extraction: the gate is unconditional across
6088
+ * every stamped row, and `stampTopicsExtractedAtOp` stamps this version on every
6089
+ * healthy row, so a bump sends each user's entire extracted vault back to the
6090
+ * LLM. Never reach for it to repair a subset — see the pre-v42-restore repair in
6091
+ * {@link getMemoriesNeedingTopicExtractionOp}, which targets the damaged rows.
6086
6092
  */
6087
6093
  declare const TOPICS_EXTRACTION_VERSION = 3;
6088
6094
  /**
@@ -6095,9 +6101,10 @@ interface MemoriesNeedingTopicExtraction {
6095
6101
  * Memories to run LLM topic extraction on (decrypted): never-extracted rows
6096
6102
  * with no entity links, plus stamped rows edited since their last pass
6097
6103
  * (`updated_at` > `topics_extracted_at`) or extracted under an older
6098
- * `topics_extracted_version` than {@link TOPICS_EXTRACTION_VERSION}. Edited /
6099
- * stale-version rows come first (they get priority under `limit`), each group
6100
- * newest-created first.
6104
+ * `topics_extracted_version` than {@link TOPICS_EXTRACTION_VERSION}, plus the
6105
+ * pre-v42-restore repair: a stamped row left with neither links nor a `topics`
6106
+ * record, which no no-LLM bucket can reach. Edited / stale-version rows come
6107
+ * first (they get priority under `limit`), each group newest-created first.
6101
6108
  */
6102
6109
  pending: StoredVaultMemory[];
6103
6110
  /**
@@ -6149,6 +6156,11 @@ interface MemoriesNeedingTopicExtraction {
6149
6156
  * between the record and the index and never re-derive them. That's why the
6150
6157
  * ownership filter lives in the partition below rather than in the query: a
6151
6158
  * restored curated memory is exactly the row whose index must be rebuilt.
6159
+ *
6160
+ * NOT purely a read: a curated row with no `topics` record AND no usable link is
6161
+ * a contradiction only a pre-v42 restore produces, and this is the one place
6162
+ * that can see all three facts at once, so it clears the flag there (capped by
6163
+ * `limit`) before returning. See the branch for why that's safe.
6152
6164
  */
6153
6165
  declare function getMemoriesNeedingTopicExtractionOp(ctx: VaultMemoryOperationsContext, options?: {
6154
6166
  limit?: number;
@@ -6083,6 +6083,12 @@ declare function getUnfiledVaultMemoriesOp(ctx: VaultMemoryOperationsContext): P
6083
6083
  * older version (including pre-v37 rows, read as version 0) is then re-extracted
6084
6084
  * by the next sweep, so topic-quality improvements propagate across the existing
6085
6085
  * vault. The worker's `limit` drains that re-extraction across sweeps.
6086
+ *
6087
+ * Bumping this is a WHOLE-VAULT re-extraction: the gate is unconditional across
6088
+ * every stamped row, and `stampTopicsExtractedAtOp` stamps this version on every
6089
+ * healthy row, so a bump sends each user's entire extracted vault back to the
6090
+ * LLM. Never reach for it to repair a subset — see the pre-v42-restore repair in
6091
+ * {@link getMemoriesNeedingTopicExtractionOp}, which targets the damaged rows.
6086
6092
  */
6087
6093
  declare const TOPICS_EXTRACTION_VERSION = 3;
6088
6094
  /**
@@ -6095,9 +6101,10 @@ interface MemoriesNeedingTopicExtraction {
6095
6101
  * Memories to run LLM topic extraction on (decrypted): never-extracted rows
6096
6102
  * with no entity links, plus stamped rows edited since their last pass
6097
6103
  * (`updated_at` > `topics_extracted_at`) or extracted under an older
6098
- * `topics_extracted_version` than {@link TOPICS_EXTRACTION_VERSION}. Edited /
6099
- * stale-version rows come first (they get priority under `limit`), each group
6100
- * newest-created first.
6104
+ * `topics_extracted_version` than {@link TOPICS_EXTRACTION_VERSION}, plus the
6105
+ * pre-v42-restore repair: a stamped row left with neither links nor a `topics`
6106
+ * record, which no no-LLM bucket can reach. Edited / stale-version rows come
6107
+ * first (they get priority under `limit`), each group newest-created first.
6101
6108
  */
6102
6109
  pending: StoredVaultMemory[];
6103
6110
  /**
@@ -6149,6 +6156,11 @@ interface MemoriesNeedingTopicExtraction {
6149
6156
  * between the record and the index and never re-derive them. That's why the
6150
6157
  * ownership filter lives in the partition below rather than in the query: a
6151
6158
  * restored curated memory is exactly the row whose index must be rebuilt.
6159
+ *
6160
+ * NOT purely a read: a curated row with no `topics` record AND no usable link is
6161
+ * a contradiction only a pre-v42 restore produces, and this is the one place
6162
+ * that can see all three facts at once, so it clears the flag there (capped by
6163
+ * `limit`) before returning. See the branch for why that's safe.
6152
6164
  */
6153
6165
  declare function getMemoriesNeedingTopicExtractionOp(ctx: VaultMemoryOperationsContext, options?: {
6154
6166
  limit?: number;
@@ -116,7 +116,7 @@ import {
116
116
  upsertMessageOp,
117
117
  useEncryption,
118
118
  vaultMemoryToStored
119
- } from "./chunk-BOAKGY4R.mjs";
119
+ } from "./chunk-VBO6KSNQ.mjs";
120
120
  import {
121
121
  PII_PATTERNS,
122
122
  PiiRedactor,
@@ -6673,7 +6673,7 @@ async function tryConsolidate(trimmed, ctx, options, prepared) {
6673
6673
  content: m.content,
6674
6674
  similarity: m.similarity
6675
6675
  }));
6676
- const { consolidateMemory: doConsolidate } = await import("./consolidate-AURTG64H.mjs");
6676
+ const { consolidateMemory: doConsolidate } = await import("./consolidate-KASO7JDS.mjs");
6677
6677
  const decision = await doConsolidate(trimmed, candidates, consolidateOptions);
6678
6678
  if (decision.action === "create") return null;
6679
6679
  if (decision.action === "supersede" && decision.content) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  recall
3
- } from "./chunk-XXCZVG4Z.mjs";
3
+ } from "./chunk-VBO6KSNQ.mjs";
4
4
  import "./chunk-ILAWDQDE.mjs";
5
5
  import "./chunk-GYKJOWEO.mjs";
6
6
  export {
@@ -2927,7 +2927,7 @@ async function setMemoryEntitiesOp(ctx, memoryId, entities) {
2927
2927
  if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) return null;
2928
2928
  let stale = false;
2929
2929
  const originalUpdatedAt = record.updatedAt.getTime();
2930
- await ctx.database.write(async () => {
2930
+ await ctx.database.write(async (writer) => {
2931
2931
  if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) {
2932
2932
  stale = true;
2933
2933
  return;
@@ -2936,30 +2936,23 @@ async function setMemoryEntitiesOp(ctx, memoryId, entities) {
2936
2936
  r._setRaw("topics_user_managed", true);
2937
2937
  r._setRaw("updated_at", originalUpdatedAt);
2938
2938
  });
2939
+ const linked = entities.length > 0 ? await writer.callWriter(
2940
+ () => linkMemoryEntitiesOp(entityCtx, memoryId, entities, { topicsSource: "user" })
2941
+ ) : [];
2942
+ const keep = new Set(linked.map((e) => e.uniqueId));
2943
+ const existing = await entityCtx.memoryEntityCollection.query(Q3.where("memory_id", memoryId)).fetch();
2944
+ const staleLinks = existing.filter((l) => !keep.has(String(l.entityId)));
2945
+ if (staleLinks.length === 0 && entities.length > 0) return;
2946
+ const topicsOp = await prepareMemoryTopicsUpdate(entityCtx, memoryId, linked, entities, "user");
2947
+ await ctx.database.batch(
2948
+ ...staleLinks.map((l) => l.prepareDestroyPermanently()),
2949
+ ...topicsOp ? [topicsOp] : []
2950
+ );
2939
2951
  });
2940
2952
  if (stale) return null;
2941
- const linked = entities.length > 0 ? await linkMemoryEntitiesOp(entityCtx, memoryId, entities, { topicsSource: "user" }) : [];
2942
- const keep = new Set(linked.map((e) => e.uniqueId));
2943
- const existing = await entityCtx.memoryEntityCollection.query(Q3.where("memory_id", memoryId)).fetch();
2944
- const staleLinks = existing.filter((l) => !keep.has(String(l.entityId)));
2945
- if (staleLinks.length > 0 || entities.length === 0) {
2946
- await ctx.database.write(async () => {
2947
- const topicsOp = await prepareMemoryTopicsUpdate(
2948
- entityCtx,
2949
- memoryId,
2950
- linked,
2951
- entities,
2952
- "user"
2953
- );
2954
- await ctx.database.batch(
2955
- ...staleLinks.map((l) => l.prepareDestroyPermanently()),
2956
- ...topicsOp ? [topicsOp] : []
2957
- );
2958
- });
2959
- }
2960
2953
  return vaultMemoryToStored(record, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner);
2961
2954
  }
2962
- async function clearMemoryTopicsOverrideOp(ctx, memoryId) {
2955
+ async function clearMemoryTopicsOverrideOp(ctx, memoryId, options) {
2963
2956
  let record;
2964
2957
  try {
2965
2958
  record = await ctx.vaultMemoryCollection.find(memoryId);
@@ -2968,7 +2961,10 @@ async function clearMemoryTopicsOverrideOp(ctx, memoryId) {
2968
2961
  }
2969
2962
  if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) return false;
2970
2963
  const originalUpdatedAt = record.updatedAt.getTime();
2964
+ let cleared = false;
2971
2965
  await ctx.database.write(async () => {
2966
+ if (options?.unlessTopicsRecorded && parseTopics(record.topics) !== null) return;
2967
+ cleared = true;
2972
2968
  await record.update((r) => {
2973
2969
  r._setRaw("topics_user_managed", false);
2974
2970
  r._setRaw("topics_extracted_version", null);
@@ -2978,7 +2974,7 @@ async function clearMemoryTopicsOverrideOp(ctx, memoryId) {
2978
2974
  r._setRaw("updated_at", originalUpdatedAt);
2979
2975
  });
2980
2976
  });
2981
- return true;
2977
+ return cleared;
2982
2978
  }
2983
2979
  async function setMemoryVisibilityOp(ctx, id, opts) {
2984
2980
  let record;
@@ -3141,11 +3137,13 @@ async function getMemoriesNeedingTopicExtractionOp(ctx, options) {
3141
3137
  entityCtx,
3142
3138
  rows.map((r) => r.id)
3143
3139
  );
3140
+ const cap = options?.limit !== void 0 && options.limit > 0 ? options.limit : void 0;
3144
3141
  const pendingRaw = [];
3145
3142
  const stampedPendingRaw = [];
3146
3143
  const linkedUnstampedAll = [];
3147
3144
  const topicsToRelinkAll = [];
3148
3145
  const topicsBackfillAll = [];
3146
+ const emptyCurationToClear = [];
3149
3147
  for (const raw of rows) {
3150
3148
  const id = raw.id;
3151
3149
  const topics = parseTopics(raw.topics);
@@ -3155,8 +3153,12 @@ async function getMemoriesNeedingTopicExtractionOp(ctx, options) {
3155
3153
  continue;
3156
3154
  }
3157
3155
  if (raw.topics_user_managed) {
3158
- if (topics === null && linked !== void 0) topicsBackfillAll.push(id);
3159
- continue;
3156
+ if (topics !== null || linked !== void 0) {
3157
+ if (topics === null) topicsBackfillAll.push(id);
3158
+ continue;
3159
+ }
3160
+ if (cap !== void 0 && emptyCurationToClear.length >= cap) continue;
3161
+ emptyCurationToClear.push(id);
3160
3162
  }
3161
3163
  const stamp = raw.topics_extracted_at ?? null;
3162
3164
  let isPending = false;
@@ -3172,11 +3174,21 @@ async function getMemoriesNeedingTopicExtractionOp(ctx, options) {
3172
3174
  pendingRaw.push(raw);
3173
3175
  isPending = true;
3174
3176
  }
3177
+ if (!isPending && linked === void 0 && topics === null) {
3178
+ pendingRaw.push(raw);
3179
+ isPending = true;
3180
+ }
3175
3181
  if (!isPending && topics === null && linked !== void 0) {
3176
3182
  topicsBackfillAll.push(id);
3177
3183
  }
3178
3184
  }
3179
- const cap = options?.limit !== void 0 && options.limit > 0 ? options.limit : void 0;
3185
+ for (const id of emptyCurationToClear) {
3186
+ try {
3187
+ await clearMemoryTopicsOverrideOp(ctx, id, { unlessTopicsRecorded: true });
3188
+ } catch (err) {
3189
+ getLogger().warn("[memory/topics] repair clear failed", err);
3190
+ }
3191
+ }
3180
3192
  const orderedPendingRaw = [...stampedPendingRaw, ...pendingRaw];
3181
3193
  const limitedPendingRaw = cap !== void 0 ? orderedPendingRaw.slice(0, cap) : orderedPendingRaw;
3182
3194
  const pending = await mapInBatches(
@@ -6430,7 +6442,7 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
6430
6442
  legacy.map((r) => ({ id: r.uniqueId, content: r.content, score: r.similarity }))
6431
6443
  );
6432
6444
  }
6433
- const { recall: recall2 } = await import("./recall-KTJCUAXN.mjs");
6445
+ const { recall: recall2 } = await import("./recall-WXCZHXHZ.mjs");
6434
6446
  let recallDegraded = [];
6435
6447
  const result = await recall2(
6436
6448
  query,
@@ -18,19 +18,21 @@ A "memory" is a self-contained natural-language fact about the user. Multiple me
18
18
  Decide one of four actions:
19
19
 
20
20
  - "create": the new memory describes a distinct fact not covered by any existing memory. Write it as new.
21
- - "update": the new memory describes the SAME FACET as exactly one existing memory and is COMPATIBLE with it \u2014 it adds detail, fixes an incomplete value, or rephrases the same still-true fact. Return targetId of that existing memory + the consolidated content (richest version of the two).
22
- - "supersede": the new memory REPLACES a standing fact whose value has CHANGED and is now INCOMPATIBLE with an existing memory \u2014 the old fact was true before but is false now (a state change). Return "targetIds": an array of the ids of EVERY existing candidate that describes the SAME standing attribute now being changed \u2014 retire ALL stale duplicates of the old value, not just one (the candidate list may contain several paraphrases of the same old fact) + content = the NEW fact. The old memories are kept as history, not overwritten or deleted; do not re-state them.
23
- - "noop": the new memory is already fully captured by an existing memory \u2014 same facet, no new information. Skip the write.
21
+ - "update": the new memory describes the SAME FACET of the SAME SUBJECT as exactly one existing memory, is COMPATIBLE with it, and ADDS INFORMATION \u2014 it adds detail or fills in an incomplete value. Return targetId of that existing memory + the consolidated content (richest version of the two). A pure rewording that adds nothing is NOT an update; it is "noop".
22
+ - "supersede": the new memory REPLACES a standing fact whose value has CHANGED and is now INCOMPATIBLE with an existing memory ABOUT THE SAME SUBJECT \u2014 the old fact was true before but is false now (a state change). Before choosing this, name the subject of the new memory and the subject of each candidate out loud to yourself; if they are different people, the old fact is still TRUE and superseding it would delete a correct memory \u2014 choose "create" instead. Return "targetIds": an array of the ids of EVERY existing candidate that describes the SAME standing attribute now being changed \u2014 retire ALL stale duplicates of the old value, not just one (the candidate list may contain several paraphrases of the same old fact) + content = the NEW fact. The old memories are kept as history, not overwritten or deleted; do not re-state them.
23
+ - "noop": the new memory is already fully captured by an existing memory \u2014 same facet of the same subject, no new information. This INCLUDES a restatement in different words ("has two kids" / "has two children"): if the existing memory already tells you everything the new one does, the answer is noop no matter how differently it is worded. Skip the write.
24
24
 
25
25
  RULES (carry these strictly):
26
26
 
27
27
  1. ONE OBSERVATION PER DISTINCT FACET. If the new memory is about "user's aunt's twins" and an existing memory is also about "user's aunt's twins", they are the same facet \u2192 update, never create.
28
+ 1a. SAME SUBJECT REQUIRED \u2014 check this FIRST, before you look at the facet. The subject is WHO the fact is about: the user, or a specific person or thing in their life (a relative, their manager, their dog Biscuit). "update", "supersede" and "noop" all require the new memory and the existing one to be about the SAME subject. Two memories can share a facet and still have different subjects, and then they are simply two different facts: "User's daughter is allergic to peanuts" and "User is allergic to peanuts" are both about a peanut allergy, but one is about the DAUGHTER and one is about the USER \u2192 create, keep both. This holds for EVERY attribute, not just the one in this example \u2014 where someone lives, who they work for, what they own, how old they are. A possessive like "user's X" makes X the subject, not the user; strip it and ask "whose fact is this?" before comparing anything. Never retire the user's own value because a fact about somebody else resembles it \u2014 the user's fact is still true, and retiring it destroys it.
28
29
  2. MATCH BY FACET, NOT TOPIC. Two memories both mentioning "Zara" is not enough to merge \u2014 only merge if they describe the same property (e.g. both about "user's pending Zara boot exchange"). Two memories about Zara on different topics (e.g. "user shops at Zara" + "user returned a sweater to Zara last week") are separate facets.
29
30
  3. NO COMPUTATION. Do not sum counts, decrement quantities, or derive new facts from existing ones. If the new memory says "user spent $200 today" and an existing memory says "user spent $150 yesterday", these are TWO SEPARATE EVENTS \u2014 create.
30
31
  4. EVENTS vs STANDING STATE.
31
32
  - Distinct EVENTS (episodic \u2014 something that happened on a date) are separate memories, even if the same activity: "user went to gym Monday" vs "user went to gym Friday" \u2192 create; keep both.
32
- - A STANDING ATTRIBUTE (an ongoing state: where the user lives/works, relationship status, current role) has ONE current value. When the new memory changes that value ("Lives in Portland" \u2192 "Lives in San Francisco"; "Works at Google" \u2192 "Works at Riverbend"; "Engaged" \u2192 "Broke up"), the old value is no longer true \u2192 supersede (retire the old, record the new). Do NOT create (that leaves a stale contradiction) and do NOT update (that erases the history that the old value was once true).
33
- 5. When in doubt between create and update, choose create \u2014 EXCEPT when the new memory directly contradicts the current value of a standing attribute in an existing memory; then choose supersede. Over-merging loses information; a live contradiction is worse than either.
33
+ - A STANDING ATTRIBUTE (an ongoing state: where someone lives/works, relationship status, current role) has ONE current value PER SUBJECT. When the new memory changes that value FOR THE SAME SUBJECT ("Lives in Portland" \u2192 "Lives in San Francisco"; "Works at Google" \u2192 "Works at Riverbend"; "Engaged" \u2192 "Broke up"), the old value is no longer true \u2192 supersede (retire the old, record the new). Do NOT create (that leaves a stale contradiction) and do NOT update (that erases the history that the old value was once true). A different subject holding the same attribute is not a value change at all \u2192 create.
34
+ 5. When in doubt between create and update, choose create \u2014 EXCEPT when the new memory directly contradicts the current value of a standing attribute in an existing memory ABOUT THE SAME SUBJECT; then choose supersede. Two facts about different subjects never contradict each other, however similarly they read. Over-merging loses information; a live contradiction is worse than either.
35
+ 6. When in doubt between update and noop, ask whether the existing memory ALONE already answers everything the new one would. If it does, choose noop \u2014 a write that changes only the wording costs a re-embed and makes an old fact look newly observed.
34
36
 
35
37
  OUTPUT \u2014 strict JSON, no prose:
36
38
  {
@@ -5238,7 +5238,7 @@ async function setMemoryEntitiesOp(ctx, memoryId, entities) {
5238
5238
  if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) return null;
5239
5239
  let stale = false;
5240
5240
  const originalUpdatedAt = record.updatedAt.getTime();
5241
- await ctx.database.write(async () => {
5241
+ await ctx.database.write(async (writer) => {
5242
5242
  if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) {
5243
5243
  stale = true;
5244
5244
  return;
@@ -5247,30 +5247,23 @@ async function setMemoryEntitiesOp(ctx, memoryId, entities) {
5247
5247
  r._setRaw("topics_user_managed", true);
5248
5248
  r._setRaw("updated_at", originalUpdatedAt);
5249
5249
  });
5250
+ const linked = entities.length > 0 ? await writer.callWriter(
5251
+ () => linkMemoryEntitiesOp(entityCtx, memoryId, entities, { topicsSource: "user" })
5252
+ ) : [];
5253
+ const keep = new Set(linked.map((e) => e.uniqueId));
5254
+ const existing = await entityCtx.memoryEntityCollection.query(import_watermelondb10.Q.where("memory_id", memoryId)).fetch();
5255
+ const staleLinks = existing.filter((l) => !keep.has(String(l.entityId)));
5256
+ if (staleLinks.length === 0 && entities.length > 0) return;
5257
+ const topicsOp = await prepareMemoryTopicsUpdate(entityCtx, memoryId, linked, entities, "user");
5258
+ await ctx.database.batch(
5259
+ ...staleLinks.map((l) => l.prepareDestroyPermanently()),
5260
+ ...topicsOp ? [topicsOp] : []
5261
+ );
5250
5262
  });
5251
5263
  if (stale) return null;
5252
- const linked = entities.length > 0 ? await linkMemoryEntitiesOp(entityCtx, memoryId, entities, { topicsSource: "user" }) : [];
5253
- const keep = new Set(linked.map((e) => e.uniqueId));
5254
- const existing = await entityCtx.memoryEntityCollection.query(import_watermelondb10.Q.where("memory_id", memoryId)).fetch();
5255
- const staleLinks = existing.filter((l) => !keep.has(String(l.entityId)));
5256
- if (staleLinks.length > 0 || entities.length === 0) {
5257
- await ctx.database.write(async () => {
5258
- const topicsOp = await prepareMemoryTopicsUpdate(
5259
- entityCtx,
5260
- memoryId,
5261
- linked,
5262
- entities,
5263
- "user"
5264
- );
5265
- await ctx.database.batch(
5266
- ...staleLinks.map((l) => l.prepareDestroyPermanently()),
5267
- ...topicsOp ? [topicsOp] : []
5268
- );
5269
- });
5270
- }
5271
5264
  return vaultMemoryToStored(record, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner);
5272
5265
  }
5273
- async function clearMemoryTopicsOverrideOp(ctx, memoryId) {
5266
+ async function clearMemoryTopicsOverrideOp(ctx, memoryId, options) {
5274
5267
  let record;
5275
5268
  try {
5276
5269
  record = await ctx.vaultMemoryCollection.find(memoryId);
@@ -5279,7 +5272,10 @@ async function clearMemoryTopicsOverrideOp(ctx, memoryId) {
5279
5272
  }
5280
5273
  if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) return false;
5281
5274
  const originalUpdatedAt = record.updatedAt.getTime();
5275
+ let cleared = false;
5282
5276
  await ctx.database.write(async () => {
5277
+ if (options?.unlessTopicsRecorded && parseTopics(record.topics) !== null) return;
5278
+ cleared = true;
5283
5279
  await record.update((r) => {
5284
5280
  r._setRaw("topics_user_managed", false);
5285
5281
  r._setRaw("topics_extracted_version", null);
@@ -5289,7 +5285,7 @@ async function clearMemoryTopicsOverrideOp(ctx, memoryId) {
5289
5285
  r._setRaw("updated_at", originalUpdatedAt);
5290
5286
  });
5291
5287
  });
5292
- return true;
5288
+ return cleared;
5293
5289
  }
5294
5290
  async function setMemoryVisibilityOp(ctx, id, opts) {
5295
5291
  let record;
@@ -5451,11 +5447,13 @@ async function getMemoriesNeedingTopicExtractionOp(ctx, options) {
5451
5447
  entityCtx,
5452
5448
  rows.map((r) => r.id)
5453
5449
  );
5450
+ const cap = options?.limit !== void 0 && options.limit > 0 ? options.limit : void 0;
5454
5451
  const pendingRaw = [];
5455
5452
  const stampedPendingRaw = [];
5456
5453
  const linkedUnstampedAll = [];
5457
5454
  const topicsToRelinkAll = [];
5458
5455
  const topicsBackfillAll = [];
5456
+ const emptyCurationToClear = [];
5459
5457
  for (const raw of rows) {
5460
5458
  const id = raw.id;
5461
5459
  const topics = parseTopics(raw.topics);
@@ -5465,8 +5463,12 @@ async function getMemoriesNeedingTopicExtractionOp(ctx, options) {
5465
5463
  continue;
5466
5464
  }
5467
5465
  if (raw.topics_user_managed) {
5468
- if (topics === null && linked !== void 0) topicsBackfillAll.push(id);
5469
- continue;
5466
+ if (topics !== null || linked !== void 0) {
5467
+ if (topics === null) topicsBackfillAll.push(id);
5468
+ continue;
5469
+ }
5470
+ if (cap !== void 0 && emptyCurationToClear.length >= cap) continue;
5471
+ emptyCurationToClear.push(id);
5470
5472
  }
5471
5473
  const stamp = raw.topics_extracted_at ?? null;
5472
5474
  let isPending = false;
@@ -5482,11 +5484,21 @@ async function getMemoriesNeedingTopicExtractionOp(ctx, options) {
5482
5484
  pendingRaw.push(raw);
5483
5485
  isPending = true;
5484
5486
  }
5487
+ if (!isPending && linked === void 0 && topics === null) {
5488
+ pendingRaw.push(raw);
5489
+ isPending = true;
5490
+ }
5485
5491
  if (!isPending && topics === null && linked !== void 0) {
5486
5492
  topicsBackfillAll.push(id);
5487
5493
  }
5488
5494
  }
5489
- const cap = options?.limit !== void 0 && options.limit > 0 ? options.limit : void 0;
5495
+ for (const id of emptyCurationToClear) {
5496
+ try {
5497
+ await clearMemoryTopicsOverrideOp(ctx, id, { unlessTopicsRecorded: true });
5498
+ } catch (err) {
5499
+ getLogger().warn("[memory/topics] repair clear failed", err);
5500
+ }
5501
+ }
5490
5502
  const orderedPendingRaw = [...stampedPendingRaw, ...pendingRaw];
5491
5503
  const limitedPendingRaw = cap !== void 0 ? orderedPendingRaw.slice(0, cap) : orderedPendingRaw;
5492
5504
  const pending = await mapInBatches(
@@ -8847,19 +8859,21 @@ A "memory" is a self-contained natural-language fact about the user. Multiple me
8847
8859
  Decide one of four actions:
8848
8860
 
8849
8861
  - "create": the new memory describes a distinct fact not covered by any existing memory. Write it as new.
8850
- - "update": the new memory describes the SAME FACET as exactly one existing memory and is COMPATIBLE with it \u2014 it adds detail, fixes an incomplete value, or rephrases the same still-true fact. Return targetId of that existing memory + the consolidated content (richest version of the two).
8851
- - "supersede": the new memory REPLACES a standing fact whose value has CHANGED and is now INCOMPATIBLE with an existing memory \u2014 the old fact was true before but is false now (a state change). Return "targetIds": an array of the ids of EVERY existing candidate that describes the SAME standing attribute now being changed \u2014 retire ALL stale duplicates of the old value, not just one (the candidate list may contain several paraphrases of the same old fact) + content = the NEW fact. The old memories are kept as history, not overwritten or deleted; do not re-state them.
8852
- - "noop": the new memory is already fully captured by an existing memory \u2014 same facet, no new information. Skip the write.
8862
+ - "update": the new memory describes the SAME FACET of the SAME SUBJECT as exactly one existing memory, is COMPATIBLE with it, and ADDS INFORMATION \u2014 it adds detail or fills in an incomplete value. Return targetId of that existing memory + the consolidated content (richest version of the two). A pure rewording that adds nothing is NOT an update; it is "noop".
8863
+ - "supersede": the new memory REPLACES a standing fact whose value has CHANGED and is now INCOMPATIBLE with an existing memory ABOUT THE SAME SUBJECT \u2014 the old fact was true before but is false now (a state change). Before choosing this, name the subject of the new memory and the subject of each candidate out loud to yourself; if they are different people, the old fact is still TRUE and superseding it would delete a correct memory \u2014 choose "create" instead. Return "targetIds": an array of the ids of EVERY existing candidate that describes the SAME standing attribute now being changed \u2014 retire ALL stale duplicates of the old value, not just one (the candidate list may contain several paraphrases of the same old fact) + content = the NEW fact. The old memories are kept as history, not overwritten or deleted; do not re-state them.
8864
+ - "noop": the new memory is already fully captured by an existing memory \u2014 same facet of the same subject, no new information. This INCLUDES a restatement in different words ("has two kids" / "has two children"): if the existing memory already tells you everything the new one does, the answer is noop no matter how differently it is worded. Skip the write.
8853
8865
 
8854
8866
  RULES (carry these strictly):
8855
8867
 
8856
8868
  1. ONE OBSERVATION PER DISTINCT FACET. If the new memory is about "user's aunt's twins" and an existing memory is also about "user's aunt's twins", they are the same facet \u2192 update, never create.
8869
+ 1a. SAME SUBJECT REQUIRED \u2014 check this FIRST, before you look at the facet. The subject is WHO the fact is about: the user, or a specific person or thing in their life (a relative, their manager, their dog Biscuit). "update", "supersede" and "noop" all require the new memory and the existing one to be about the SAME subject. Two memories can share a facet and still have different subjects, and then they are simply two different facts: "User's daughter is allergic to peanuts" and "User is allergic to peanuts" are both about a peanut allergy, but one is about the DAUGHTER and one is about the USER \u2192 create, keep both. This holds for EVERY attribute, not just the one in this example \u2014 where someone lives, who they work for, what they own, how old they are. A possessive like "user's X" makes X the subject, not the user; strip it and ask "whose fact is this?" before comparing anything. Never retire the user's own value because a fact about somebody else resembles it \u2014 the user's fact is still true, and retiring it destroys it.
8857
8870
  2. MATCH BY FACET, NOT TOPIC. Two memories both mentioning "Zara" is not enough to merge \u2014 only merge if they describe the same property (e.g. both about "user's pending Zara boot exchange"). Two memories about Zara on different topics (e.g. "user shops at Zara" + "user returned a sweater to Zara last week") are separate facets.
8858
8871
  3. NO COMPUTATION. Do not sum counts, decrement quantities, or derive new facts from existing ones. If the new memory says "user spent $200 today" and an existing memory says "user spent $150 yesterday", these are TWO SEPARATE EVENTS \u2014 create.
8859
8872
  4. EVENTS vs STANDING STATE.
8860
8873
  - Distinct EVENTS (episodic \u2014 something that happened on a date) are separate memories, even if the same activity: "user went to gym Monday" vs "user went to gym Friday" \u2192 create; keep both.
8861
- - A STANDING ATTRIBUTE (an ongoing state: where the user lives/works, relationship status, current role) has ONE current value. When the new memory changes that value ("Lives in Portland" \u2192 "Lives in San Francisco"; "Works at Google" \u2192 "Works at Riverbend"; "Engaged" \u2192 "Broke up"), the old value is no longer true \u2192 supersede (retire the old, record the new). Do NOT create (that leaves a stale contradiction) and do NOT update (that erases the history that the old value was once true).
8862
- 5. When in doubt between create and update, choose create \u2014 EXCEPT when the new memory directly contradicts the current value of a standing attribute in an existing memory; then choose supersede. Over-merging loses information; a live contradiction is worse than either.
8874
+ - A STANDING ATTRIBUTE (an ongoing state: where someone lives/works, relationship status, current role) has ONE current value PER SUBJECT. When the new memory changes that value FOR THE SAME SUBJECT ("Lives in Portland" \u2192 "Lives in San Francisco"; "Works at Google" \u2192 "Works at Riverbend"; "Engaged" \u2192 "Broke up"), the old value is no longer true \u2192 supersede (retire the old, record the new). Do NOT create (that leaves a stale contradiction) and do NOT update (that erases the history that the old value was once true). A different subject holding the same attribute is not a value change at all \u2192 create.
8875
+ 5. When in doubt between create and update, choose create \u2014 EXCEPT when the new memory directly contradicts the current value of a standing attribute in an existing memory ABOUT THE SAME SUBJECT; then choose supersede. Two facts about different subjects never contradict each other, however similarly they read. Over-merging loses information; a live contradiction is worse than either.
8876
+ 6. When in doubt between update and noop, ask whether the existing memory ALONE already answers everything the new one would. If it does, choose noop \u2014 a write that changes only the wording costs a re-embed and makes an old fact look newly observed.
8863
8877
 
8864
8878
  OUTPUT \u2014 strict JSON, no prose:
8865
8879
  {
@@ -7457,8 +7457,18 @@ declare function setMemoryEntitiesOp(ctx: VaultMemoryOperationsContext, memoryId
7457
7457
  * see the schema note. This op's version-invalidation trick is the reason the
7458
7458
  * earlier plan to exclude them from sync was dropped, and it's the last piece
7459
7459
  * that has to move before they can go.
7460
+ *
7461
+ * `options.unlessTopicsRecorded` declines the reset when the row already has a
7462
+ * `topics` record, re-checked INSIDE the serialized writer. Only the repair path
7463
+ * in {@link getMemoriesNeedingTopicExtractionOp} passes it: that path clears the
7464
+ * flag off rows whose curation is provably empty, and a `setMemoryEntitiesOp`
7465
+ * committing in the gap would have written a real record the autotagger must not
7466
+ * be handed. The user-facing reset leaves it off — resetting a memory that HAS
7467
+ * curated topics is the whole point there.
7460
7468
  */
7461
- declare function clearMemoryTopicsOverrideOp(ctx: VaultMemoryOperationsContext, memoryId: string): Promise<boolean>;
7469
+ declare function clearMemoryTopicsOverrideOp(ctx: VaultMemoryOperationsContext, memoryId: string, options?: {
7470
+ unlessTopicsRecorded?: boolean;
7471
+ }): Promise<boolean>;
7462
7472
  /**
7463
7473
  * Set a memory's People Nearby visibility (and optionally its twin opt-in).
7464
7474
  *
@@ -7500,6 +7510,12 @@ declare function getUnfiledVaultMemoriesOp(ctx: VaultMemoryOperationsContext): P
7500
7510
  * older version (including pre-v37 rows, read as version 0) is then re-extracted
7501
7511
  * by the next sweep, so topic-quality improvements propagate across the existing
7502
7512
  * vault. The worker's `limit` drains that re-extraction across sweeps.
7513
+ *
7514
+ * Bumping this is a WHOLE-VAULT re-extraction: the gate is unconditional across
7515
+ * every stamped row, and `stampTopicsExtractedAtOp` stamps this version on every
7516
+ * healthy row, so a bump sends each user's entire extracted vault back to the
7517
+ * LLM. Never reach for it to repair a subset — see the pre-v42-restore repair in
7518
+ * {@link getMemoriesNeedingTopicExtractionOp}, which targets the damaged rows.
7503
7519
  */
7504
7520
  declare const TOPICS_EXTRACTION_VERSION = 3;
7505
7521
  /**
@@ -7512,9 +7528,10 @@ interface MemoriesNeedingTopicExtraction {
7512
7528
  * Memories to run LLM topic extraction on (decrypted): never-extracted rows
7513
7529
  * with no entity links, plus stamped rows edited since their last pass
7514
7530
  * (`updated_at` > `topics_extracted_at`) or extracted under an older
7515
- * `topics_extracted_version` than {@link TOPICS_EXTRACTION_VERSION}. Edited /
7516
- * stale-version rows come first (they get priority under `limit`), each group
7517
- * newest-created first.
7531
+ * `topics_extracted_version` than {@link TOPICS_EXTRACTION_VERSION}, plus the
7532
+ * pre-v42-restore repair: a stamped row left with neither links nor a `topics`
7533
+ * record, which no no-LLM bucket can reach. Edited / stale-version rows come
7534
+ * first (they get priority under `limit`), each group newest-created first.
7518
7535
  */
7519
7536
  pending: StoredVaultMemory[];
7520
7537
  /**
@@ -7566,6 +7583,11 @@ interface MemoriesNeedingTopicExtraction {
7566
7583
  * between the record and the index and never re-derive them. That's why the
7567
7584
  * ownership filter lives in the partition below rather than in the query: a
7568
7585
  * restored curated memory is exactly the row whose index must be rebuilt.
7586
+ *
7587
+ * NOT purely a read: a curated row with no `topics` record AND no usable link is
7588
+ * a contradiction only a pre-v42 restore produces, and this is the one place
7589
+ * that can see all three facts at once, so it clears the flag there (capped by
7590
+ * `limit`) before returning. See the branch for why that's safe.
7569
7591
  */
7570
7592
  declare function getMemoriesNeedingTopicExtractionOp(ctx: VaultMemoryOperationsContext, options?: {
7571
7593
  limit?: number;
@@ -7457,8 +7457,18 @@ declare function setMemoryEntitiesOp(ctx: VaultMemoryOperationsContext, memoryId
7457
7457
  * see the schema note. This op's version-invalidation trick is the reason the
7458
7458
  * earlier plan to exclude them from sync was dropped, and it's the last piece
7459
7459
  * that has to move before they can go.
7460
+ *
7461
+ * `options.unlessTopicsRecorded` declines the reset when the row already has a
7462
+ * `topics` record, re-checked INSIDE the serialized writer. Only the repair path
7463
+ * in {@link getMemoriesNeedingTopicExtractionOp} passes it: that path clears the
7464
+ * flag off rows whose curation is provably empty, and a `setMemoryEntitiesOp`
7465
+ * committing in the gap would have written a real record the autotagger must not
7466
+ * be handed. The user-facing reset leaves it off — resetting a memory that HAS
7467
+ * curated topics is the whole point there.
7460
7468
  */
7461
- declare function clearMemoryTopicsOverrideOp(ctx: VaultMemoryOperationsContext, memoryId: string): Promise<boolean>;
7469
+ declare function clearMemoryTopicsOverrideOp(ctx: VaultMemoryOperationsContext, memoryId: string, options?: {
7470
+ unlessTopicsRecorded?: boolean;
7471
+ }): Promise<boolean>;
7462
7472
  /**
7463
7473
  * Set a memory's People Nearby visibility (and optionally its twin opt-in).
7464
7474
  *
@@ -7500,6 +7510,12 @@ declare function getUnfiledVaultMemoriesOp(ctx: VaultMemoryOperationsContext): P
7500
7510
  * older version (including pre-v37 rows, read as version 0) is then re-extracted
7501
7511
  * by the next sweep, so topic-quality improvements propagate across the existing
7502
7512
  * vault. The worker's `limit` drains that re-extraction across sweeps.
7513
+ *
7514
+ * Bumping this is a WHOLE-VAULT re-extraction: the gate is unconditional across
7515
+ * every stamped row, and `stampTopicsExtractedAtOp` stamps this version on every
7516
+ * healthy row, so a bump sends each user's entire extracted vault back to the
7517
+ * LLM. Never reach for it to repair a subset — see the pre-v42-restore repair in
7518
+ * {@link getMemoriesNeedingTopicExtractionOp}, which targets the damaged rows.
7503
7519
  */
7504
7520
  declare const TOPICS_EXTRACTION_VERSION = 3;
7505
7521
  /**
@@ -7512,9 +7528,10 @@ interface MemoriesNeedingTopicExtraction {
7512
7528
  * Memories to run LLM topic extraction on (decrypted): never-extracted rows
7513
7529
  * with no entity links, plus stamped rows edited since their last pass
7514
7530
  * (`updated_at` > `topics_extracted_at`) or extracted under an older
7515
- * `topics_extracted_version` than {@link TOPICS_EXTRACTION_VERSION}. Edited /
7516
- * stale-version rows come first (they get priority under `limit`), each group
7517
- * newest-created first.
7531
+ * `topics_extracted_version` than {@link TOPICS_EXTRACTION_VERSION}, plus the
7532
+ * pre-v42-restore repair: a stamped row left with neither links nor a `topics`
7533
+ * record, which no no-LLM bucket can reach. Edited / stale-version rows come
7534
+ * first (they get priority under `limit`), each group newest-created first.
7518
7535
  */
7519
7536
  pending: StoredVaultMemory[];
7520
7537
  /**
@@ -7566,6 +7583,11 @@ interface MemoriesNeedingTopicExtraction {
7566
7583
  * between the record and the index and never re-derive them. That's why the
7567
7584
  * ownership filter lives in the partition below rather than in the query: a
7568
7585
  * restored curated memory is exactly the row whose index must be rebuilt.
7586
+ *
7587
+ * NOT purely a read: a curated row with no `topics` record AND no usable link is
7588
+ * a contradiction only a pre-v42 restore produces, and this is the one place
7589
+ * that can see all three facts at once, so it clears the flag there (capped by
7590
+ * `limit`) before returning. See the branch for why that's safe.
7569
7591
  */
7570
7592
  declare function getMemoriesNeedingTopicExtractionOp(ctx: VaultMemoryOperationsContext, options?: {
7571
7593
  limit?: number;
@@ -161,7 +161,7 @@ import {
161
161
  updateVaultMemoryOp,
162
162
  useEncryption,
163
163
  vaultMemoryToStored
164
- } from "./chunk-XXCZVG4Z.mjs";
164
+ } from "./chunk-BS7C4CUW.mjs";
165
165
  import {
166
166
  PiiRedactor,
167
167
  createStreamingDeAnonymizer,
@@ -7490,7 +7490,7 @@ async function tryConsolidate(trimmed, ctx, options, prepared) {
7490
7490
  content: m.content,
7491
7491
  similarity: m.similarity
7492
7492
  }));
7493
- const { consolidateMemory: doConsolidate } = await import("./consolidate-R6FH7XMV.mjs");
7493
+ const { consolidateMemory: doConsolidate } = await import("./consolidate-IFIKT25Z.mjs");
7494
7494
  const decision = await doConsolidate(trimmed, candidates, consolidateOptions);
7495
7495
  if (decision.action === "create") return null;
7496
7496
  if (decision.action === "supersede" && decision.content) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  recall
3
- } from "./chunk-BOAKGY4R.mjs";
3
+ } from "./chunk-BS7C4CUW.mjs";
4
4
  import "./chunk-ILAWDQDE.mjs";
5
5
  import "./chunk-GYKJOWEO.mjs";
6
6
  export {