@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.
@@ -16942,6 +16942,30 @@ async function updateVaultMemoryOp(ctx, id, opts) {
16942
16942
  return null;
16943
16943
  }
16944
16944
  }
16945
+ async function clearMemoryTopicsOverrideOp(ctx, memoryId, options) {
16946
+ let record;
16947
+ try {
16948
+ record = await ctx.vaultMemoryCollection.find(memoryId);
16949
+ } catch {
16950
+ return false;
16951
+ }
16952
+ if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) return false;
16953
+ const originalUpdatedAt = record.updatedAt.getTime();
16954
+ let cleared = false;
16955
+ await ctx.database.write(async () => {
16956
+ if (options?.unlessTopicsRecorded && parseTopics(record.topics) !== null) return;
16957
+ cleared = true;
16958
+ await record.update((r) => {
16959
+ r._setRaw("topics_user_managed", false);
16960
+ r._setRaw("topics_extracted_version", null);
16961
+ if (record.topicsExtractedAt === null) {
16962
+ r._setRaw("topics_extracted_at", originalUpdatedAt);
16963
+ }
16964
+ r._setRaw("updated_at", originalUpdatedAt);
16965
+ });
16966
+ });
16967
+ return cleared;
16968
+ }
16945
16969
  async function setMemoryVisibilityOp(ctx, id, opts) {
16946
16970
  let record;
16947
16971
  try {
@@ -17103,11 +17127,13 @@ async function getMemoriesNeedingTopicExtractionOp(ctx, options) {
17103
17127
  entityCtx,
17104
17128
  rows.map((r) => r.id)
17105
17129
  );
17130
+ const cap = options?.limit !== void 0 && options.limit > 0 ? options.limit : void 0;
17106
17131
  const pendingRaw = [];
17107
17132
  const stampedPendingRaw = [];
17108
17133
  const linkedUnstampedAll = [];
17109
17134
  const topicsToRelinkAll = [];
17110
17135
  const topicsBackfillAll = [];
17136
+ const emptyCurationToClear = [];
17111
17137
  for (const raw of rows) {
17112
17138
  const id = raw.id;
17113
17139
  const topics = parseTopics(raw.topics);
@@ -17117,8 +17143,12 @@ async function getMemoriesNeedingTopicExtractionOp(ctx, options) {
17117
17143
  continue;
17118
17144
  }
17119
17145
  if (raw.topics_user_managed) {
17120
- if (topics === null && linked !== void 0) topicsBackfillAll.push(id);
17121
- continue;
17146
+ if (topics !== null || linked !== void 0) {
17147
+ if (topics === null) topicsBackfillAll.push(id);
17148
+ continue;
17149
+ }
17150
+ if (cap !== void 0 && emptyCurationToClear.length >= cap) continue;
17151
+ emptyCurationToClear.push(id);
17122
17152
  }
17123
17153
  const stamp = raw.topics_extracted_at ?? null;
17124
17154
  let isPending = false;
@@ -17134,11 +17164,21 @@ async function getMemoriesNeedingTopicExtractionOp(ctx, options) {
17134
17164
  pendingRaw.push(raw);
17135
17165
  isPending = true;
17136
17166
  }
17167
+ if (!isPending && linked === void 0 && topics === null) {
17168
+ pendingRaw.push(raw);
17169
+ isPending = true;
17170
+ }
17137
17171
  if (!isPending && topics === null && linked !== void 0) {
17138
17172
  topicsBackfillAll.push(id);
17139
17173
  }
17140
17174
  }
17141
- const cap = options?.limit !== void 0 && options.limit > 0 ? options.limit : void 0;
17175
+ for (const id of emptyCurationToClear) {
17176
+ try {
17177
+ await clearMemoryTopicsOverrideOp(ctx, id, { unlessTopicsRecorded: true });
17178
+ } catch (err) {
17179
+ getLogger().warn("[memory/topics] repair clear failed", err);
17180
+ }
17181
+ }
17142
17182
  const orderedPendingRaw = [...stampedPendingRaw, ...pendingRaw];
17143
17183
  const limitedPendingRaw = cap !== void 0 ? orderedPendingRaw.slice(0, cap) : orderedPendingRaw;
17144
17184
  const pending = await mapInBatches(
@@ -20310,7 +20350,7 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
20310
20350
  legacy.map((r) => ({ id: r.uniqueId, content: r.content, score: r.similarity }))
20311
20351
  );
20312
20352
  }
20313
- const { recall: recall2 } = await import("./recall-YZWIV6IH.mjs");
20353
+ const { recall: recall2 } = await import("./recall-QTVT7HCZ.mjs");
20314
20354
  let recallDegraded = [];
20315
20355
  const result = await recall2(
20316
20356
  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
  {
@@ -17747,6 +17747,30 @@ async function updateVaultMemoryOp(ctx, id, opts) {
17747
17747
  return null;
17748
17748
  }
17749
17749
  }
17750
+ async function clearMemoryTopicsOverrideOp(ctx, memoryId, options) {
17751
+ let record;
17752
+ try {
17753
+ record = await ctx.vaultMemoryCollection.find(memoryId);
17754
+ } catch {
17755
+ return false;
17756
+ }
17757
+ if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) return false;
17758
+ const originalUpdatedAt = record.updatedAt.getTime();
17759
+ let cleared = false;
17760
+ await ctx.database.write(async () => {
17761
+ if (options?.unlessTopicsRecorded && parseTopics(record.topics) !== null) return;
17762
+ cleared = true;
17763
+ await record.update((r) => {
17764
+ r._setRaw("topics_user_managed", false);
17765
+ r._setRaw("topics_extracted_version", null);
17766
+ if (record.topicsExtractedAt === null) {
17767
+ r._setRaw("topics_extracted_at", originalUpdatedAt);
17768
+ }
17769
+ r._setRaw("updated_at", originalUpdatedAt);
17770
+ });
17771
+ });
17772
+ return cleared;
17773
+ }
17750
17774
  async function setMemoryVisibilityOp(ctx, id, opts) {
17751
17775
  let record;
17752
17776
  try {
@@ -17907,11 +17931,13 @@ async function getMemoriesNeedingTopicExtractionOp(ctx, options) {
17907
17931
  entityCtx,
17908
17932
  rows.map((r) => r.id)
17909
17933
  );
17934
+ const cap = options?.limit !== void 0 && options.limit > 0 ? options.limit : void 0;
17910
17935
  const pendingRaw = [];
17911
17936
  const stampedPendingRaw = [];
17912
17937
  const linkedUnstampedAll = [];
17913
17938
  const topicsToRelinkAll = [];
17914
17939
  const topicsBackfillAll = [];
17940
+ const emptyCurationToClear = [];
17915
17941
  for (const raw of rows) {
17916
17942
  const id = raw.id;
17917
17943
  const topics = parseTopics(raw.topics);
@@ -17921,8 +17947,12 @@ async function getMemoriesNeedingTopicExtractionOp(ctx, options) {
17921
17947
  continue;
17922
17948
  }
17923
17949
  if (raw.topics_user_managed) {
17924
- if (topics === null && linked !== void 0) topicsBackfillAll.push(id);
17925
- continue;
17950
+ if (topics !== null || linked !== void 0) {
17951
+ if (topics === null) topicsBackfillAll.push(id);
17952
+ continue;
17953
+ }
17954
+ if (cap !== void 0 && emptyCurationToClear.length >= cap) continue;
17955
+ emptyCurationToClear.push(id);
17926
17956
  }
17927
17957
  const stamp = raw.topics_extracted_at ?? null;
17928
17958
  let isPending = false;
@@ -17938,11 +17968,21 @@ async function getMemoriesNeedingTopicExtractionOp(ctx, options) {
17938
17968
  pendingRaw.push(raw);
17939
17969
  isPending = true;
17940
17970
  }
17971
+ if (!isPending && linked === void 0 && topics === null) {
17972
+ pendingRaw.push(raw);
17973
+ isPending = true;
17974
+ }
17941
17975
  if (!isPending && topics === null && linked !== void 0) {
17942
17976
  topicsBackfillAll.push(id);
17943
17977
  }
17944
17978
  }
17945
- const cap = options?.limit !== void 0 && options.limit > 0 ? options.limit : void 0;
17979
+ for (const id of emptyCurationToClear) {
17980
+ try {
17981
+ await clearMemoryTopicsOverrideOp(ctx, id, { unlessTopicsRecorded: true });
17982
+ } catch (err) {
17983
+ getLogger().warn("[memory/topics] repair clear failed", err);
17984
+ }
17985
+ }
17946
17986
  const orderedPendingRaw = [...stampedPendingRaw, ...pendingRaw];
17947
17987
  const limitedPendingRaw = cap !== void 0 ? orderedPendingRaw.slice(0, cap) : orderedPendingRaw;
17948
17988
  const pending = await mapInBatches(
@@ -23361,19 +23401,21 @@ A "memory" is a self-contained natural-language fact about the user. Multiple me
23361
23401
  Decide one of four actions:
23362
23402
 
23363
23403
  - "create": the new memory describes a distinct fact not covered by any existing memory. Write it as new.
23364
- - "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).
23365
- - "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.
23366
- - "noop": the new memory is already fully captured by an existing memory \u2014 same facet, no new information. Skip the write.
23404
+ - "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".
23405
+ - "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.
23406
+ - "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.
23367
23407
 
23368
23408
  RULES (carry these strictly):
23369
23409
 
23370
23410
  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.
23411
+ 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.
23371
23412
  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.
23372
23413
  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.
23373
23414
  4. EVENTS vs STANDING STATE.
23374
23415
  - 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.
23375
- - 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).
23376
- 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.
23416
+ - 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.
23417
+ 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.
23418
+ 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.
23377
23419
 
23378
23420
  OUTPUT \u2014 strict JSON, no prose:
23379
23421
  {
@@ -6025,6 +6025,12 @@ declare function getUnfiledVaultMemoriesOp(ctx: VaultMemoryOperationsContext): P
6025
6025
  * older version (including pre-v37 rows, read as version 0) is then re-extracted
6026
6026
  * by the next sweep, so topic-quality improvements propagate across the existing
6027
6027
  * vault. The worker's `limit` drains that re-extraction across sweeps.
6028
+ *
6029
+ * Bumping this is a WHOLE-VAULT re-extraction: the gate is unconditional across
6030
+ * every stamped row, and `stampTopicsExtractedAtOp` stamps this version on every
6031
+ * healthy row, so a bump sends each user's entire extracted vault back to the
6032
+ * LLM. Never reach for it to repair a subset — see the pre-v42-restore repair in
6033
+ * {@link getMemoriesNeedingTopicExtractionOp}, which targets the damaged rows.
6028
6034
  */
6029
6035
  declare const TOPICS_EXTRACTION_VERSION = 3;
6030
6036
  /**
@@ -6037,9 +6043,10 @@ interface MemoriesNeedingTopicExtraction {
6037
6043
  * Memories to run LLM topic extraction on (decrypted): never-extracted rows
6038
6044
  * with no entity links, plus stamped rows edited since their last pass
6039
6045
  * (`updated_at` > `topics_extracted_at`) or extracted under an older
6040
- * `topics_extracted_version` than {@link TOPICS_EXTRACTION_VERSION}. Edited /
6041
- * stale-version rows come first (they get priority under `limit`), each group
6042
- * newest-created first.
6046
+ * `topics_extracted_version` than {@link TOPICS_EXTRACTION_VERSION}, plus the
6047
+ * pre-v42-restore repair: a stamped row left with neither links nor a `topics`
6048
+ * record, which no no-LLM bucket can reach. Edited / stale-version rows come
6049
+ * first (they get priority under `limit`), each group newest-created first.
6043
6050
  */
6044
6051
  pending: StoredVaultMemory[];
6045
6052
  /**
@@ -6091,6 +6098,11 @@ interface MemoriesNeedingTopicExtraction {
6091
6098
  * between the record and the index and never re-derive them. That's why the
6092
6099
  * ownership filter lives in the partition below rather than in the query: a
6093
6100
  * restored curated memory is exactly the row whose index must be rebuilt.
6101
+ *
6102
+ * NOT purely a read: a curated row with no `topics` record AND no usable link is
6103
+ * a contradiction only a pre-v42 restore produces, and this is the one place
6104
+ * that can see all three facts at once, so it clears the flag there (capped by
6105
+ * `limit`) before returning. See the branch for why that's safe.
6094
6106
  */
6095
6107
  declare function getMemoriesNeedingTopicExtractionOp(ctx: VaultMemoryOperationsContext, options?: {
6096
6108
  limit?: number;
@@ -6025,6 +6025,12 @@ declare function getUnfiledVaultMemoriesOp(ctx: VaultMemoryOperationsContext): P
6025
6025
  * older version (including pre-v37 rows, read as version 0) is then re-extracted
6026
6026
  * by the next sweep, so topic-quality improvements propagate across the existing
6027
6027
  * vault. The worker's `limit` drains that re-extraction across sweeps.
6028
+ *
6029
+ * Bumping this is a WHOLE-VAULT re-extraction: the gate is unconditional across
6030
+ * every stamped row, and `stampTopicsExtractedAtOp` stamps this version on every
6031
+ * healthy row, so a bump sends each user's entire extracted vault back to the
6032
+ * LLM. Never reach for it to repair a subset — see the pre-v42-restore repair in
6033
+ * {@link getMemoriesNeedingTopicExtractionOp}, which targets the damaged rows.
6028
6034
  */
6029
6035
  declare const TOPICS_EXTRACTION_VERSION = 3;
6030
6036
  /**
@@ -6037,9 +6043,10 @@ interface MemoriesNeedingTopicExtraction {
6037
6043
  * Memories to run LLM topic extraction on (decrypted): never-extracted rows
6038
6044
  * with no entity links, plus stamped rows edited since their last pass
6039
6045
  * (`updated_at` > `topics_extracted_at`) or extracted under an older
6040
- * `topics_extracted_version` than {@link TOPICS_EXTRACTION_VERSION}. Edited /
6041
- * stale-version rows come first (they get priority under `limit`), each group
6042
- * newest-created first.
6046
+ * `topics_extracted_version` than {@link TOPICS_EXTRACTION_VERSION}, plus the
6047
+ * pre-v42-restore repair: a stamped row left with neither links nor a `topics`
6048
+ * record, which no no-LLM bucket can reach. Edited / stale-version rows come
6049
+ * first (they get priority under `limit`), each group newest-created first.
6043
6050
  */
6044
6051
  pending: StoredVaultMemory[];
6045
6052
  /**
@@ -6091,6 +6098,11 @@ interface MemoriesNeedingTopicExtraction {
6091
6098
  * between the record and the index and never re-derive them. That's why the
6092
6099
  * ownership filter lives in the partition below rather than in the query: a
6093
6100
  * restored curated memory is exactly the row whose index must be rebuilt.
6101
+ *
6102
+ * NOT purely a read: a curated row with no `topics` record AND no usable link is
6103
+ * a contradiction only a pre-v42 restore produces, and this is the one place
6104
+ * that can see all three facts at once, so it clears the flag there (capped by
6105
+ * `limit`) before returning. See the branch for why that's safe.
6094
6106
  */
6095
6107
  declare function getMemoriesNeedingTopicExtractionOp(ctx: VaultMemoryOperationsContext, options?: {
6096
6108
  limit?: number;
@@ -133,7 +133,7 @@ import {
133
133
  updateVaultMemoryOp,
134
134
  upsertMessageOp,
135
135
  vaultMemoryToStored
136
- } from "./chunk-WZE6ZVKE.mjs";
136
+ } from "./chunk-WADHFDMA.mjs";
137
137
  import {
138
138
  createStreamingDeAnonymizer,
139
139
  notifyConsolidationFallback,
@@ -5445,7 +5445,7 @@ async function tryConsolidate(trimmed, ctx, options, prepared) {
5445
5445
  content: m.content,
5446
5446
  similarity: m.similarity
5447
5447
  }));
5448
- const { consolidateMemory: doConsolidate } = await import("./consolidate-4VEAS3YN.mjs");
5448
+ const { consolidateMemory: doConsolidate } = await import("./consolidate-GRMEDWKK.mjs");
5449
5449
  const decision = await doConsolidate(trimmed, candidates, consolidateOptions);
5450
5450
  if (decision.action === "create") return null;
5451
5451
  if (decision.action === "supersede" && decision.content) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  recall
3
- } from "./chunk-WZE6ZVKE.mjs";
3
+ } from "./chunk-WADHFDMA.mjs";
4
4
  import "./chunk-OBDRKQQI.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": "4.0.0",
3
+ "version": "4.0.2",
4
4
  "description": "",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
@@ -159,7 +159,7 @@
159
159
  "homepage": "https://github.com/anuma-ai/sdk#readme",
160
160
  "packageManager": "pnpm@10.23.0",
161
161
  "dependencies": {
162
- "@anuma/portal": "1.0.0-next.20260727223242",
162
+ "@anuma/portal": "1.0.0-next.20260728181242",
163
163
  "@babel/parser": "^7.29.2",
164
164
  "@babel/types": "^7.29.0",
165
165
  "ai": "6.0.202",