@anuma/sdk 5.1.1 → 6.0.0

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.
@@ -643,10 +643,16 @@ function createStreamingDeAnonymizer(redactor, emit) {
643
643
 
644
644
  // src/lib/memory/consolidationFallback.ts
645
645
  function notifyConsolidationFallback(reason, onFallback, detail) {
646
+ const refused = reason === "subject_mismatch";
647
+ const message = refused ? `memory/consolidate: refused a cross-subject supersede, created instead (${reason})` : `memory/consolidate: degraded to create (${reason})`;
648
+ const log = getLogger();
646
649
  if (detail !== void 0) {
647
- getLogger().warn(`memory/consolidate: degraded to create (${reason})`, detail);
650
+ if (refused) log.info(message, detail);
651
+ else log.warn(message, detail);
652
+ } else if (refused) {
653
+ log.info(message);
648
654
  } else {
649
- getLogger().warn(`memory/consolidate: degraded to create (${reason})`);
655
+ log.warn(message);
650
656
  }
651
657
  try {
652
658
  onFallback?.(reason);
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  notifyConsolidationFallback,
3
3
  resolvePiiRedactor
4
- } from "./chunk-WV7C7VHF.mjs";
4
+ } from "./chunk-DT6QNRZL.mjs";
5
5
  import {
6
6
  callPortalJsonCompletion
7
7
  } from "./chunk-ILAWDQDE.mjs";
@@ -39,12 +39,15 @@ OUTPUT \u2014 strict JSON, no prose:
39
39
  "action": "create" | "update" | "supersede" | "noop",
40
40
  "targetId": "<existing memory id \u2014 required for update/noop, omit for create/supersede>",
41
41
  "targetIds": ["<id>", ...],
42
- "content": "<content \u2014 required for create/update/supersede, omit for noop>"
42
+ "content": "<content \u2014 required for create/update/supersede, omit for noop>",
43
+ "newSubject": "<required for supersede: WHO the new memory is about>",
44
+ "targetSubject": "<required for supersede: WHO the memory in targetId is about>",
45
+ "targetSubjects": ["<required for supersede: WHO each memory you are retiring is about \u2014 one entry per retired id>"]
43
46
  }
44
47
 
45
48
  For "create": content is the new memory verbatim (or a slight refinement); omit targetId/targetIds.
46
49
  For "update": content is the merged richest-version, \u226480 words; targetId is the single memory to update.
47
- For "supersede": content is the NEW fact only (\u226480 words); "targetIds" lists EVERY stale memory being retired (all candidates describing the same now-changed attribute).
50
+ For "supersede": content is the NEW fact only (\u226480 words); "targetIds" lists EVERY stale memory being retired (all candidates describing the same now-changed attribute). You MUST also name the subjects: "newSubject" for the new memory, and one entry in "targetSubjects" for EACH memory you are retiring (use "targetSubject" as well when you retire a single memory). List a subject ONLY for the memories you are actually retiring, not for every candidate you were shown. Write the plain subject, not a sentence: "the user", "the user's sister", "the user's manager", "Biscuit the dog". Rule 1a means a supersede is only valid when every one of those subjects is the SAME as "newSubject"; name them and check them against each other before you commit to the action. If ANY of them differs, the answer is "create" \u2014 do not retire the ones that match and keep the rest, because a group that mixes subjects was grouped wrongly in the first place. A subjectless fact ("Lives in Denver", "Works at Riverbend") is about "the user" \u2014 the extractor omits the subject when it is the user, so treat the absence of a subject as the user, never as unknown.
48
51
  For "noop": no content (existing memory is already correct); targetId is that memory.`;
49
52
  async function consolidateMemory(newContent, candidates, options) {
50
53
  const fallback = {
@@ -101,7 +104,13 @@ ${candidateText}`;
101
104
  if (restored.unresolved) {
102
105
  return degrade("invalid_response", fallback, options);
103
106
  }
104
- return { ...result, content: restored.text };
107
+ return reportRefusal({ ...result, content: restored.text }, options);
108
+ }
109
+ return reportRefusal(result, options);
110
+ }
111
+ function reportRefusal(result, options) {
112
+ if (result.fallbackReason) {
113
+ notifyConsolidationFallback(result.fallbackReason, options.onFallback);
105
114
  }
106
115
  return result;
107
116
  }
@@ -109,6 +118,19 @@ function degrade(reason, fallback, options, detail) {
109
118
  notifyConsolidationFallback(reason, options.onFallback, detail);
110
119
  return { ...fallback, fallbackReason: reason };
111
120
  }
121
+ function normalizeSubject(raw) {
122
+ if (typeof raw !== "string") return "";
123
+ let s = raw.trim().toLowerCase().replace(/[.,;:!?]+$/u, "").trim();
124
+ s = s.replace(/^the\s+/u, "");
125
+ s = s.replace(/^users'?\s+/u, "").replace(/^user's\s+/u, "");
126
+ return s.trim();
127
+ }
128
+ var USER_SUBJECT_ALIASES = /* @__PURE__ */ new Set(["user", "themselves", "themself", "they", "me", "self"]);
129
+ function subjectKey(raw) {
130
+ const s = normalizeSubject(raw);
131
+ if (s.length === 0) return "";
132
+ return USER_SUBJECT_ALIASES.has(s) ? "user" : s;
133
+ }
112
134
  function validate(parsed, newContent, validIds) {
113
135
  if (typeof parsed !== "object" || parsed === null) return null;
114
136
  const obj = parsed;
@@ -136,6 +158,18 @@ function validate(parsed, newContent, validIds) {
136
158
  ...new Set(rawIds.filter((id) => typeof id === "string" && validIds.has(id)))
137
159
  ];
138
160
  if (targetIds.length === 0) return null;
161
+ const newSubject = subjectKey(obj.newSubject);
162
+ if (newSubject.length > 0) {
163
+ const positional = Array.isArray(obj.targetSubjects) ? obj.targetSubjects : [];
164
+ const statedSubjects = [
165
+ ...positional.map((s) => subjectKey(s)),
166
+ // The singular pair, which is the whole story for a one-target supersede.
167
+ subjectKey(obj.targetSubject)
168
+ ];
169
+ if (statedSubjects.some((s) => s.length > 0 && s !== newSubject)) {
170
+ return { action: "create", content: c, fallbackReason: "subject_mismatch" };
171
+ }
172
+ }
139
173
  return { action: "supersede", targetId: targetIds[0], targetIds, content: c };
140
174
  }
141
175
  const targetId = typeof obj.targetId === "string" ? obj.targetId : null;
@@ -8723,10 +8723,16 @@ var init_searchTool = __esm({
8723
8723
 
8724
8724
  // src/lib/memory/consolidationFallback.ts
8725
8725
  function notifyConsolidationFallback(reason, onFallback, detail) {
8726
+ const refused = reason === "subject_mismatch";
8727
+ const message = refused ? `memory/consolidate: refused a cross-subject supersede, created instead (${reason})` : `memory/consolidate: degraded to create (${reason})`;
8728
+ const log = getLogger();
8726
8729
  if (detail !== void 0) {
8727
- getLogger().warn(`memory/consolidate: degraded to create (${reason})`, detail);
8730
+ if (refused) log.info(message, detail);
8731
+ else log.warn(message, detail);
8732
+ } else if (refused) {
8733
+ log.info(message);
8728
8734
  } else {
8729
- getLogger().warn(`memory/consolidate: degraded to create (${reason})`);
8735
+ log.warn(message);
8730
8736
  }
8731
8737
  try {
8732
8738
  onFallback?.(reason);
@@ -8801,7 +8807,13 @@ ${candidateText}`;
8801
8807
  if (restored.unresolved) {
8802
8808
  return degrade("invalid_response", fallback, options);
8803
8809
  }
8804
- return { ...result, content: restored.text };
8810
+ return reportRefusal({ ...result, content: restored.text }, options);
8811
+ }
8812
+ return reportRefusal(result, options);
8813
+ }
8814
+ function reportRefusal(result, options) {
8815
+ if (result.fallbackReason) {
8816
+ notifyConsolidationFallback(result.fallbackReason, options.onFallback);
8805
8817
  }
8806
8818
  return result;
8807
8819
  }
@@ -8809,6 +8821,18 @@ function degrade(reason, fallback, options, detail) {
8809
8821
  notifyConsolidationFallback(reason, options.onFallback, detail);
8810
8822
  return { ...fallback, fallbackReason: reason };
8811
8823
  }
8824
+ function normalizeSubject(raw) {
8825
+ if (typeof raw !== "string") return "";
8826
+ let s = raw.trim().toLowerCase().replace(/[.,;:!?]+$/u, "").trim();
8827
+ s = s.replace(/^the\s+/u, "");
8828
+ s = s.replace(/^users'?\s+/u, "").replace(/^user's\s+/u, "");
8829
+ return s.trim();
8830
+ }
8831
+ function subjectKey(raw) {
8832
+ const s = normalizeSubject(raw);
8833
+ if (s.length === 0) return "";
8834
+ return USER_SUBJECT_ALIASES.has(s) ? "user" : s;
8835
+ }
8812
8836
  function validate2(parsed, newContent, validIds) {
8813
8837
  if (typeof parsed !== "object" || parsed === null) return null;
8814
8838
  const obj = parsed;
@@ -8836,13 +8860,25 @@ function validate2(parsed, newContent, validIds) {
8836
8860
  ...new Set(rawIds.filter((id) => typeof id === "string" && validIds.has(id)))
8837
8861
  ];
8838
8862
  if (targetIds.length === 0) return null;
8863
+ const newSubject = subjectKey(obj.newSubject);
8864
+ if (newSubject.length > 0) {
8865
+ const positional = Array.isArray(obj.targetSubjects) ? obj.targetSubjects : [];
8866
+ const statedSubjects = [
8867
+ ...positional.map((s) => subjectKey(s)),
8868
+ // The singular pair, which is the whole story for a one-target supersede.
8869
+ subjectKey(obj.targetSubject)
8870
+ ];
8871
+ if (statedSubjects.some((s) => s.length > 0 && s !== newSubject)) {
8872
+ return { action: "create", content: c, fallbackReason: "subject_mismatch" };
8873
+ }
8874
+ }
8839
8875
  return { action: "supersede", targetId: targetIds[0], targetIds, content: c };
8840
8876
  }
8841
8877
  const targetId = typeof obj.targetId === "string" ? obj.targetId : null;
8842
8878
  if (!targetId || !validIds.has(targetId)) return null;
8843
8879
  return { action: "update", targetId, content: c };
8844
8880
  }
8845
- var DEFAULT_CONSOLIDATION_MODEL, DEFAULT_CONSOLIDATE_ATTEMPTS, DEFAULT_CONSOLIDATE_TOTAL_TIMEOUT_MS, SYSTEM_PROMPT3;
8881
+ var DEFAULT_CONSOLIDATION_MODEL, DEFAULT_CONSOLIDATE_ATTEMPTS, DEFAULT_CONSOLIDATE_TOTAL_TIMEOUT_MS, SYSTEM_PROMPT3, USER_SUBJECT_ALIASES;
8846
8882
  var init_consolidate = __esm({
8847
8883
  "src/lib/memory/consolidate.ts"() {
8848
8884
  "use strict";
@@ -8880,13 +8916,17 @@ OUTPUT \u2014 strict JSON, no prose:
8880
8916
  "action": "create" | "update" | "supersede" | "noop",
8881
8917
  "targetId": "<existing memory id \u2014 required for update/noop, omit for create/supersede>",
8882
8918
  "targetIds": ["<id>", ...],
8883
- "content": "<content \u2014 required for create/update/supersede, omit for noop>"
8919
+ "content": "<content \u2014 required for create/update/supersede, omit for noop>",
8920
+ "newSubject": "<required for supersede: WHO the new memory is about>",
8921
+ "targetSubject": "<required for supersede: WHO the memory in targetId is about>",
8922
+ "targetSubjects": ["<required for supersede: WHO each memory you are retiring is about \u2014 one entry per retired id>"]
8884
8923
  }
8885
8924
 
8886
8925
  For "create": content is the new memory verbatim (or a slight refinement); omit targetId/targetIds.
8887
8926
  For "update": content is the merged richest-version, \u226480 words; targetId is the single memory to update.
8888
- For "supersede": content is the NEW fact only (\u226480 words); "targetIds" lists EVERY stale memory being retired (all candidates describing the same now-changed attribute).
8927
+ For "supersede": content is the NEW fact only (\u226480 words); "targetIds" lists EVERY stale memory being retired (all candidates describing the same now-changed attribute). You MUST also name the subjects: "newSubject" for the new memory, and one entry in "targetSubjects" for EACH memory you are retiring (use "targetSubject" as well when you retire a single memory). List a subject ONLY for the memories you are actually retiring, not for every candidate you were shown. Write the plain subject, not a sentence: "the user", "the user's sister", "the user's manager", "Biscuit the dog". Rule 1a means a supersede is only valid when every one of those subjects is the SAME as "newSubject"; name them and check them against each other before you commit to the action. If ANY of them differs, the answer is "create" \u2014 do not retire the ones that match and keep the rest, because a group that mixes subjects was grouped wrongly in the first place. A subjectless fact ("Lives in Denver", "Works at Riverbend") is about "the user" \u2014 the extractor omits the subject when it is the user, so treat the absence of a subject as the user, never as unknown.
8889
8928
  For "noop": no content (existing memory is already correct); targetId is that memory.`;
8929
+ USER_SUBJECT_ALIASES = /* @__PURE__ */ new Set(["user", "themselves", "themself", "they", "me", "self"]);
8890
8930
  }
8891
8931
  });
8892
8932
 
@@ -5528,9 +5528,9 @@ interface RecallDiagnostics {
5528
5528
  type RetainAction = "create" | "merge" | "update" | "skip" | "suppressed" | "supersede";
5529
5529
  type RetainSource = "manual" | "auto-extracted" | "capsule";
5530
5530
  /**
5531
- * Why a retain fell back to "create" instead of applying a consolidation
5532
- * decision. Each value names a DIFFERENT thing to go fix, which is the point of
5533
- * keeping them apart:
5531
+ * Why a retain returned "create" instead of applying a consolidation decision.
5532
+ * Each value names a DIFFERENT thing to go fix, which is the point of keeping
5533
+ * them apart — and one of them is not a fault at all, see `subject_mismatch`:
5534
5534
  *
5535
5535
  * - `llm_error` — the consolidation call never produced a response (network,
5536
5536
  * timeout, 5xx, 429, empty completion, or missing credentials). Look at the
@@ -5545,8 +5545,27 @@ type RetainSource = "manual" | "auto-extracted" | "capsule";
5545
5545
  * rate points at write contention (for example an auto-extraction worker and a
5546
5546
  * manual write racing over the same vault), which quietly costs you the
5547
5547
  * dedup that decision would have performed.
5548
- */
5549
- type ConsolidationFallbackReason = "llm_error" | "invalid_response" | "target_vanished";
5548
+ * - `subject_mismatch` — NOT a fault. The model returned a well-formed
5549
+ * `supersede` and we refused it because the two subjects it named were
5550
+ * different people, so retiring the target would have hidden a memory that is
5551
+ * still true (#822 — "User's sister lives in Denver" retiring "User lives in
5552
+ * Denver"). The refusal is the feature; do not alarm on it. What the rate does
5553
+ * tell you is how often the model reaches for a cross-subject supersede, and —
5554
+ * because the guard is inert unless the model fills in its subject fields — a
5555
+ * rate of exactly zero is as likely to mean "the fields are being omitted" as
5556
+ * "the mistake never happens". Check compliance before reading zero as health.
5557
+ */
5558
+ type ConsolidationFallbackReason = "llm_error" | "invalid_response" | "target_vanished"
5559
+ /**
5560
+ * A `supersede` was downgraded to `create` because the model's own stated
5561
+ * subjects disagreed: the new fact is about someone other than the fact it
5562
+ * wanted to retire (#822 — "User's sister lives in Denver" retiring "User
5563
+ * lives in Denver"). NOT an error; the decision was refused on purpose, and
5564
+ * the refusal preserved a true memory. Retiring a superseded row hides it from
5565
+ * recall, so this is the one fallback that prevents data loss rather than
5566
+ * reporting degraded quality.
5567
+ */
5568
+ | "subject_mismatch";
5550
5569
  interface RetainOptions {
5551
5570
  source?: RetainSource;
5552
5571
  sourceChunkIds?: string[];
@@ -5582,6 +5601,12 @@ interface RetainOptions {
5582
5601
  * JSON that violates the schema (unknown action, bad targetId).
5583
5602
  * A flaky consolidator silently accumulates duplicate memories;
5584
5603
  * wire this to logging/metrics so the fallback rate is observable.
5604
+ *
5605
+ * `subject_mismatch` is the odd one out and worth reading separately: the
5606
+ * model answered fine and we REFUSED its supersede because its own stated
5607
+ * subjects disagreed (#822). A non-zero rate is the guard doing its job, not
5608
+ * a problem — the rate to watch is how often it fires, which is also the
5609
+ * only signal for how reliably the model fills the subject fields in at all.
5585
5610
  */
5586
5611
  onFallback?: (reason: ConsolidationFallbackReason) => void;
5587
5612
  /**
@@ -9533,7 +9558,9 @@ declare function ensureDefaultFoldersOp(ctx: VaultFolderOperationsContext): Prom
9533
9558
  */
9534
9559
  interface Logger {
9535
9560
  debug: (...args: unknown[]) => void;
9536
- /** Not used internally by the SDK today, but included so custom loggers can receive all standard levels. */
9561
+ /** Used for outcomes that are noteworthy but expected — a consolidation
9562
+ * refusal that preserved a memory, for example. Keep genuinely-wrong things at
9563
+ * `warn` so the two stay distinguishable in a log search. */
9537
9564
  info: (...args: unknown[]) => void;
9538
9565
  warn: (...args: unknown[]) => void;
9539
9566
  error: (...args: unknown[]) => void;
@@ -5528,9 +5528,9 @@ interface RecallDiagnostics {
5528
5528
  type RetainAction = "create" | "merge" | "update" | "skip" | "suppressed" | "supersede";
5529
5529
  type RetainSource = "manual" | "auto-extracted" | "capsule";
5530
5530
  /**
5531
- * Why a retain fell back to "create" instead of applying a consolidation
5532
- * decision. Each value names a DIFFERENT thing to go fix, which is the point of
5533
- * keeping them apart:
5531
+ * Why a retain returned "create" instead of applying a consolidation decision.
5532
+ * Each value names a DIFFERENT thing to go fix, which is the point of keeping
5533
+ * them apart — and one of them is not a fault at all, see `subject_mismatch`:
5534
5534
  *
5535
5535
  * - `llm_error` — the consolidation call never produced a response (network,
5536
5536
  * timeout, 5xx, 429, empty completion, or missing credentials). Look at the
@@ -5545,8 +5545,27 @@ type RetainSource = "manual" | "auto-extracted" | "capsule";
5545
5545
  * rate points at write contention (for example an auto-extraction worker and a
5546
5546
  * manual write racing over the same vault), which quietly costs you the
5547
5547
  * dedup that decision would have performed.
5548
- */
5549
- type ConsolidationFallbackReason = "llm_error" | "invalid_response" | "target_vanished";
5548
+ * - `subject_mismatch` — NOT a fault. The model returned a well-formed
5549
+ * `supersede` and we refused it because the two subjects it named were
5550
+ * different people, so retiring the target would have hidden a memory that is
5551
+ * still true (#822 — "User's sister lives in Denver" retiring "User lives in
5552
+ * Denver"). The refusal is the feature; do not alarm on it. What the rate does
5553
+ * tell you is how often the model reaches for a cross-subject supersede, and —
5554
+ * because the guard is inert unless the model fills in its subject fields — a
5555
+ * rate of exactly zero is as likely to mean "the fields are being omitted" as
5556
+ * "the mistake never happens". Check compliance before reading zero as health.
5557
+ */
5558
+ type ConsolidationFallbackReason = "llm_error" | "invalid_response" | "target_vanished"
5559
+ /**
5560
+ * A `supersede` was downgraded to `create` because the model's own stated
5561
+ * subjects disagreed: the new fact is about someone other than the fact it
5562
+ * wanted to retire (#822 — "User's sister lives in Denver" retiring "User
5563
+ * lives in Denver"). NOT an error; the decision was refused on purpose, and
5564
+ * the refusal preserved a true memory. Retiring a superseded row hides it from
5565
+ * recall, so this is the one fallback that prevents data loss rather than
5566
+ * reporting degraded quality.
5567
+ */
5568
+ | "subject_mismatch";
5550
5569
  interface RetainOptions {
5551
5570
  source?: RetainSource;
5552
5571
  sourceChunkIds?: string[];
@@ -5582,6 +5601,12 @@ interface RetainOptions {
5582
5601
  * JSON that violates the schema (unknown action, bad targetId).
5583
5602
  * A flaky consolidator silently accumulates duplicate memories;
5584
5603
  * wire this to logging/metrics so the fallback rate is observable.
5604
+ *
5605
+ * `subject_mismatch` is the odd one out and worth reading separately: the
5606
+ * model answered fine and we REFUSED its supersede because its own stated
5607
+ * subjects disagreed (#822). A non-zero rate is the guard doing its job, not
5608
+ * a problem — the rate to watch is how often it fires, which is also the
5609
+ * only signal for how reliably the model fills the subject fields in at all.
5585
5610
  */
5586
5611
  onFallback?: (reason: ConsolidationFallbackReason) => void;
5587
5612
  /**
@@ -9533,7 +9558,9 @@ declare function ensureDefaultFoldersOp(ctx: VaultFolderOperationsContext): Prom
9533
9558
  */
9534
9559
  interface Logger {
9535
9560
  debug: (...args: unknown[]) => void;
9536
- /** Not used internally by the SDK today, but included so custom loggers can receive all standard levels. */
9561
+ /** Used for outcomes that are noteworthy but expected — a consolidation
9562
+ * refusal that preserved a memory, for example. Keep genuinely-wrong things at
9563
+ * `warn` so the two stay distinguishable in a log search. */
9537
9564
  info: (...args: unknown[]) => void;
9538
9565
  warn: (...args: unknown[]) => void;
9539
9566
  error: (...args: unknown[]) => void;
@@ -129,7 +129,7 @@ import {
129
129
  isPiiRedactor,
130
130
  notifyConsolidationFallback,
131
131
  resolvePiiRedactor
132
- } from "./chunk-WV7C7VHF.mjs";
132
+ } from "./chunk-DT6QNRZL.mjs";
133
133
  import {
134
134
  callPortalJsonCompletion,
135
135
  consoleLogger,
@@ -6705,7 +6705,7 @@ async function tryConsolidate(trimmed, ctx, options, prepared) {
6705
6705
  content: m.content,
6706
6706
  similarity: m.similarity
6707
6707
  }));
6708
- const { consolidateMemory: doConsolidate } = await import("./consolidate-T6GZ5YQ4.mjs");
6708
+ const { consolidateMemory: doConsolidate } = await import("./consolidate-3MEWQNM3.mjs");
6709
6709
  const decision = await doConsolidate(trimmed, candidates, consolidateOptions);
6710
6710
  if (decision.action === "create") return null;
6711
6711
  if (decision.action === "supersede" && decision.content) {
package/dist/index.d.mts CHANGED
@@ -8449,7 +8449,9 @@ interface AgentConfig {
8449
8449
  */
8450
8450
  interface Logger {
8451
8451
  debug: (...args: unknown[]) => void;
8452
- /** Not used internally by the SDK today, but included so custom loggers can receive all standard levels. */
8452
+ /** Used for outcomes that are noteworthy but expected — a consolidation
8453
+ * refusal that preserved a memory, for example. Keep genuinely-wrong things at
8454
+ * `warn` so the two stay distinguishable in a log search. */
8453
8455
  info: (...args: unknown[]) => void;
8454
8456
  warn: (...args: unknown[]) => void;
8455
8457
  error: (...args: unknown[]) => void;
package/dist/index.d.ts CHANGED
@@ -8449,7 +8449,9 @@ interface AgentConfig {
8449
8449
  */
8450
8450
  interface Logger {
8451
8451
  debug: (...args: unknown[]) => void;
8452
- /** Not used internally by the SDK today, but included so custom loggers can receive all standard levels. */
8452
+ /** Used for outcomes that are noteworthy but expected — a consolidation
8453
+ * refusal that preserved a memory, for example. Keep genuinely-wrong things at
8454
+ * `warn` so the two stay distinguishable in a log search. */
8453
8455
  info: (...args: unknown[]) => void;
8454
8456
  warn: (...args: unknown[]) => void;
8455
8457
  error: (...args: unknown[]) => void;
@@ -643,10 +643,16 @@ function createStreamingDeAnonymizer(redactor, emit) {
643
643
 
644
644
  // src/lib/memory/consolidationFallback.ts
645
645
  function notifyConsolidationFallback(reason, onFallback, detail) {
646
+ const refused = reason === "subject_mismatch";
647
+ const message = refused ? `memory/consolidate: refused a cross-subject supersede, created instead (${reason})` : `memory/consolidate: degraded to create (${reason})`;
648
+ const log = getLogger();
646
649
  if (detail !== void 0) {
647
- getLogger().warn(`memory/consolidate: degraded to create (${reason})`, detail);
650
+ if (refused) log.info(message, detail);
651
+ else log.warn(message, detail);
652
+ } else if (refused) {
653
+ log.info(message);
648
654
  } else {
649
- getLogger().warn(`memory/consolidate: degraded to create (${reason})`);
655
+ log.warn(message);
650
656
  }
651
657
  try {
652
658
  onFallback?.(reason);
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  notifyConsolidationFallback,
3
3
  resolvePiiRedactor
4
- } from "./chunk-3NPDZZGO.mjs";
4
+ } from "./chunk-VMI74IUX.mjs";
5
5
  import {
6
6
  callPortalJsonCompletion
7
7
  } from "./chunk-ILAWDQDE.mjs";
@@ -39,12 +39,15 @@ OUTPUT \u2014 strict JSON, no prose:
39
39
  "action": "create" | "update" | "supersede" | "noop",
40
40
  "targetId": "<existing memory id \u2014 required for update/noop, omit for create/supersede>",
41
41
  "targetIds": ["<id>", ...],
42
- "content": "<content \u2014 required for create/update/supersede, omit for noop>"
42
+ "content": "<content \u2014 required for create/update/supersede, omit for noop>",
43
+ "newSubject": "<required for supersede: WHO the new memory is about>",
44
+ "targetSubject": "<required for supersede: WHO the memory in targetId is about>",
45
+ "targetSubjects": ["<required for supersede: WHO each memory you are retiring is about \u2014 one entry per retired id>"]
43
46
  }
44
47
 
45
48
  For "create": content is the new memory verbatim (or a slight refinement); omit targetId/targetIds.
46
49
  For "update": content is the merged richest-version, \u226480 words; targetId is the single memory to update.
47
- For "supersede": content is the NEW fact only (\u226480 words); "targetIds" lists EVERY stale memory being retired (all candidates describing the same now-changed attribute).
50
+ For "supersede": content is the NEW fact only (\u226480 words); "targetIds" lists EVERY stale memory being retired (all candidates describing the same now-changed attribute). You MUST also name the subjects: "newSubject" for the new memory, and one entry in "targetSubjects" for EACH memory you are retiring (use "targetSubject" as well when you retire a single memory). List a subject ONLY for the memories you are actually retiring, not for every candidate you were shown. Write the plain subject, not a sentence: "the user", "the user's sister", "the user's manager", "Biscuit the dog". Rule 1a means a supersede is only valid when every one of those subjects is the SAME as "newSubject"; name them and check them against each other before you commit to the action. If ANY of them differs, the answer is "create" \u2014 do not retire the ones that match and keep the rest, because a group that mixes subjects was grouped wrongly in the first place. A subjectless fact ("Lives in Denver", "Works at Riverbend") is about "the user" \u2014 the extractor omits the subject when it is the user, so treat the absence of a subject as the user, never as unknown.
48
51
  For "noop": no content (existing memory is already correct); targetId is that memory.`;
49
52
  async function consolidateMemory(newContent, candidates, options) {
50
53
  const fallback = {
@@ -101,7 +104,13 @@ ${candidateText}`;
101
104
  if (restored.unresolved) {
102
105
  return degrade("invalid_response", fallback, options);
103
106
  }
104
- return { ...result, content: restored.text };
107
+ return reportRefusal({ ...result, content: restored.text }, options);
108
+ }
109
+ return reportRefusal(result, options);
110
+ }
111
+ function reportRefusal(result, options) {
112
+ if (result.fallbackReason) {
113
+ notifyConsolidationFallback(result.fallbackReason, options.onFallback);
105
114
  }
106
115
  return result;
107
116
  }
@@ -109,6 +118,19 @@ function degrade(reason, fallback, options, detail) {
109
118
  notifyConsolidationFallback(reason, options.onFallback, detail);
110
119
  return { ...fallback, fallbackReason: reason };
111
120
  }
121
+ function normalizeSubject(raw) {
122
+ if (typeof raw !== "string") return "";
123
+ let s = raw.trim().toLowerCase().replace(/[.,;:!?]+$/u, "").trim();
124
+ s = s.replace(/^the\s+/u, "");
125
+ s = s.replace(/^users'?\s+/u, "").replace(/^user's\s+/u, "");
126
+ return s.trim();
127
+ }
128
+ var USER_SUBJECT_ALIASES = /* @__PURE__ */ new Set(["user", "themselves", "themself", "they", "me", "self"]);
129
+ function subjectKey(raw) {
130
+ const s = normalizeSubject(raw);
131
+ if (s.length === 0) return "";
132
+ return USER_SUBJECT_ALIASES.has(s) ? "user" : s;
133
+ }
112
134
  function validate(parsed, newContent, validIds) {
113
135
  if (typeof parsed !== "object" || parsed === null) return null;
114
136
  const obj = parsed;
@@ -136,6 +158,18 @@ function validate(parsed, newContent, validIds) {
136
158
  ...new Set(rawIds.filter((id) => typeof id === "string" && validIds.has(id)))
137
159
  ];
138
160
  if (targetIds.length === 0) return null;
161
+ const newSubject = subjectKey(obj.newSubject);
162
+ if (newSubject.length > 0) {
163
+ const positional = Array.isArray(obj.targetSubjects) ? obj.targetSubjects : [];
164
+ const statedSubjects = [
165
+ ...positional.map((s) => subjectKey(s)),
166
+ // The singular pair, which is the whole story for a one-target supersede.
167
+ subjectKey(obj.targetSubject)
168
+ ];
169
+ if (statedSubjects.some((s) => s.length > 0 && s !== newSubject)) {
170
+ return { action: "create", content: c, fallbackReason: "subject_mismatch" };
171
+ }
172
+ }
139
173
  return { action: "supersede", targetId: targetIds[0], targetIds, content: c };
140
174
  }
141
175
  const targetId = typeof obj.targetId === "string" ? obj.targetId : null;
@@ -9006,10 +9006,16 @@ var init_searchTool = __esm({
9006
9006
 
9007
9007
  // src/lib/memory/consolidationFallback.ts
9008
9008
  function notifyConsolidationFallback(reason, onFallback, detail) {
9009
+ const refused = reason === "subject_mismatch";
9010
+ const message = refused ? `memory/consolidate: refused a cross-subject supersede, created instead (${reason})` : `memory/consolidate: degraded to create (${reason})`;
9011
+ const log = getLogger();
9009
9012
  if (detail !== void 0) {
9010
- getLogger().warn(`memory/consolidate: degraded to create (${reason})`, detail);
9013
+ if (refused) log.info(message, detail);
9014
+ else log.warn(message, detail);
9015
+ } else if (refused) {
9016
+ log.info(message);
9011
9017
  } else {
9012
- getLogger().warn(`memory/consolidate: degraded to create (${reason})`);
9018
+ log.warn(message);
9013
9019
  }
9014
9020
  try {
9015
9021
  onFallback?.(reason);
@@ -9084,7 +9090,13 @@ ${candidateText}`;
9084
9090
  if (restored.unresolved) {
9085
9091
  return degrade("invalid_response", fallback, options);
9086
9092
  }
9087
- return { ...result, content: restored.text };
9093
+ return reportRefusal({ ...result, content: restored.text }, options);
9094
+ }
9095
+ return reportRefusal(result, options);
9096
+ }
9097
+ function reportRefusal(result, options) {
9098
+ if (result.fallbackReason) {
9099
+ notifyConsolidationFallback(result.fallbackReason, options.onFallback);
9088
9100
  }
9089
9101
  return result;
9090
9102
  }
@@ -9092,6 +9104,18 @@ function degrade(reason, fallback, options, detail) {
9092
9104
  notifyConsolidationFallback(reason, options.onFallback, detail);
9093
9105
  return { ...fallback, fallbackReason: reason };
9094
9106
  }
9107
+ function normalizeSubject(raw) {
9108
+ if (typeof raw !== "string") return "";
9109
+ let s = raw.trim().toLowerCase().replace(/[.,;:!?]+$/u, "").trim();
9110
+ s = s.replace(/^the\s+/u, "");
9111
+ s = s.replace(/^users'?\s+/u, "").replace(/^user's\s+/u, "");
9112
+ return s.trim();
9113
+ }
9114
+ function subjectKey(raw) {
9115
+ const s = normalizeSubject(raw);
9116
+ if (s.length === 0) return "";
9117
+ return USER_SUBJECT_ALIASES.has(s) ? "user" : s;
9118
+ }
9095
9119
  function validate2(parsed, newContent, validIds) {
9096
9120
  if (typeof parsed !== "object" || parsed === null) return null;
9097
9121
  const obj = parsed;
@@ -9119,13 +9143,25 @@ function validate2(parsed, newContent, validIds) {
9119
9143
  ...new Set(rawIds.filter((id) => typeof id === "string" && validIds.has(id)))
9120
9144
  ];
9121
9145
  if (targetIds.length === 0) return null;
9146
+ const newSubject = subjectKey(obj.newSubject);
9147
+ if (newSubject.length > 0) {
9148
+ const positional = Array.isArray(obj.targetSubjects) ? obj.targetSubjects : [];
9149
+ const statedSubjects = [
9150
+ ...positional.map((s) => subjectKey(s)),
9151
+ // The singular pair, which is the whole story for a one-target supersede.
9152
+ subjectKey(obj.targetSubject)
9153
+ ];
9154
+ if (statedSubjects.some((s) => s.length > 0 && s !== newSubject)) {
9155
+ return { action: "create", content: c, fallbackReason: "subject_mismatch" };
9156
+ }
9157
+ }
9122
9158
  return { action: "supersede", targetId: targetIds[0], targetIds, content: c };
9123
9159
  }
9124
9160
  const targetId = typeof obj.targetId === "string" ? obj.targetId : null;
9125
9161
  if (!targetId || !validIds.has(targetId)) return null;
9126
9162
  return { action: "update", targetId, content: c };
9127
9163
  }
9128
- var DEFAULT_CONSOLIDATION_MODEL, DEFAULT_CONSOLIDATE_ATTEMPTS, DEFAULT_CONSOLIDATE_TOTAL_TIMEOUT_MS, SYSTEM_PROMPT3;
9164
+ var DEFAULT_CONSOLIDATION_MODEL, DEFAULT_CONSOLIDATE_ATTEMPTS, DEFAULT_CONSOLIDATE_TOTAL_TIMEOUT_MS, SYSTEM_PROMPT3, USER_SUBJECT_ALIASES;
9129
9165
  var init_consolidate = __esm({
9130
9166
  "src/lib/memory/consolidate.ts"() {
9131
9167
  "use strict";
@@ -9163,13 +9199,17 @@ OUTPUT \u2014 strict JSON, no prose:
9163
9199
  "action": "create" | "update" | "supersede" | "noop",
9164
9200
  "targetId": "<existing memory id \u2014 required for update/noop, omit for create/supersede>",
9165
9201
  "targetIds": ["<id>", ...],
9166
- "content": "<content \u2014 required for create/update/supersede, omit for noop>"
9202
+ "content": "<content \u2014 required for create/update/supersede, omit for noop>",
9203
+ "newSubject": "<required for supersede: WHO the new memory is about>",
9204
+ "targetSubject": "<required for supersede: WHO the memory in targetId is about>",
9205
+ "targetSubjects": ["<required for supersede: WHO each memory you are retiring is about \u2014 one entry per retired id>"]
9167
9206
  }
9168
9207
 
9169
9208
  For "create": content is the new memory verbatim (or a slight refinement); omit targetId/targetIds.
9170
9209
  For "update": content is the merged richest-version, \u226480 words; targetId is the single memory to update.
9171
- For "supersede": content is the NEW fact only (\u226480 words); "targetIds" lists EVERY stale memory being retired (all candidates describing the same now-changed attribute).
9210
+ For "supersede": content is the NEW fact only (\u226480 words); "targetIds" lists EVERY stale memory being retired (all candidates describing the same now-changed attribute). You MUST also name the subjects: "newSubject" for the new memory, and one entry in "targetSubjects" for EACH memory you are retiring (use "targetSubject" as well when you retire a single memory). List a subject ONLY for the memories you are actually retiring, not for every candidate you were shown. Write the plain subject, not a sentence: "the user", "the user's sister", "the user's manager", "Biscuit the dog". Rule 1a means a supersede is only valid when every one of those subjects is the SAME as "newSubject"; name them and check them against each other before you commit to the action. If ANY of them differs, the answer is "create" \u2014 do not retire the ones that match and keep the rest, because a group that mixes subjects was grouped wrongly in the first place. A subjectless fact ("Lives in Denver", "Works at Riverbend") is about "the user" \u2014 the extractor omits the subject when it is the user, so treat the absence of a subject as the user, never as unknown.
9172
9211
  For "noop": no content (existing memory is already correct); targetId is that memory.`;
9212
+ USER_SUBJECT_ALIASES = /* @__PURE__ */ new Set(["user", "themselves", "themself", "they", "me", "self"]);
9173
9213
  }
9174
9214
  });
9175
9215
 
@@ -3238,7 +3238,9 @@ declare function createWebSearchPreProcessor(options?: WebSearchPreProcessorOpti
3238
3238
  */
3239
3239
  interface Logger {
3240
3240
  debug: (...args: unknown[]) => void;
3241
- /** Not used internally by the SDK today, but included so custom loggers can receive all standard levels. */
3241
+ /** Used for outcomes that are noteworthy but expected — a consolidation
3242
+ * refusal that preserved a memory, for example. Keep genuinely-wrong things at
3243
+ * `warn` so the two stay distinguishable in a log search. */
3242
3244
  info: (...args: unknown[]) => void;
3243
3245
  warn: (...args: unknown[]) => void;
3244
3246
  error: (...args: unknown[]) => void;
@@ -6800,9 +6802,9 @@ interface RecallDiagnostics {
6800
6802
  type RetainAction = "create" | "merge" | "update" | "skip" | "suppressed" | "supersede";
6801
6803
  type RetainSource = "manual" | "auto-extracted" | "capsule";
6802
6804
  /**
6803
- * Why a retain fell back to "create" instead of applying a consolidation
6804
- * decision. Each value names a DIFFERENT thing to go fix, which is the point of
6805
- * keeping them apart:
6805
+ * Why a retain returned "create" instead of applying a consolidation decision.
6806
+ * Each value names a DIFFERENT thing to go fix, which is the point of keeping
6807
+ * them apart — and one of them is not a fault at all, see `subject_mismatch`:
6806
6808
  *
6807
6809
  * - `llm_error` — the consolidation call never produced a response (network,
6808
6810
  * timeout, 5xx, 429, empty completion, or missing credentials). Look at the
@@ -6817,8 +6819,27 @@ type RetainSource = "manual" | "auto-extracted" | "capsule";
6817
6819
  * rate points at write contention (for example an auto-extraction worker and a
6818
6820
  * manual write racing over the same vault), which quietly costs you the
6819
6821
  * dedup that decision would have performed.
6820
- */
6821
- type ConsolidationFallbackReason = "llm_error" | "invalid_response" | "target_vanished";
6822
+ * - `subject_mismatch` — NOT a fault. The model returned a well-formed
6823
+ * `supersede` and we refused it because the two subjects it named were
6824
+ * different people, so retiring the target would have hidden a memory that is
6825
+ * still true (#822 — "User's sister lives in Denver" retiring "User lives in
6826
+ * Denver"). The refusal is the feature; do not alarm on it. What the rate does
6827
+ * tell you is how often the model reaches for a cross-subject supersede, and —
6828
+ * because the guard is inert unless the model fills in its subject fields — a
6829
+ * rate of exactly zero is as likely to mean "the fields are being omitted" as
6830
+ * "the mistake never happens". Check compliance before reading zero as health.
6831
+ */
6832
+ type ConsolidationFallbackReason = "llm_error" | "invalid_response" | "target_vanished"
6833
+ /**
6834
+ * A `supersede` was downgraded to `create` because the model's own stated
6835
+ * subjects disagreed: the new fact is about someone other than the fact it
6836
+ * wanted to retire (#822 — "User's sister lives in Denver" retiring "User
6837
+ * lives in Denver"). NOT an error; the decision was refused on purpose, and
6838
+ * the refusal preserved a true memory. Retiring a superseded row hides it from
6839
+ * recall, so this is the one fallback that prevents data loss rather than
6840
+ * reporting degraded quality.
6841
+ */
6842
+ | "subject_mismatch";
6822
6843
  interface RetainOptions {
6823
6844
  source?: RetainSource;
6824
6845
  sourceChunkIds?: string[];
@@ -6854,6 +6875,12 @@ interface RetainOptions {
6854
6875
  * JSON that violates the schema (unknown action, bad targetId).
6855
6876
  * A flaky consolidator silently accumulates duplicate memories;
6856
6877
  * wire this to logging/metrics so the fallback rate is observable.
6878
+ *
6879
+ * `subject_mismatch` is the odd one out and worth reading separately: the
6880
+ * model answered fine and we REFUSED its supersede because its own stated
6881
+ * subjects disagreed (#822). A non-zero rate is the guard doing its job, not
6882
+ * a problem — the rate to watch is how often it fires, which is also the
6883
+ * only signal for how reliably the model fills the subject fields in at all.
6857
6884
  */
6858
6885
  onFallback?: (reason: ConsolidationFallbackReason) => void;
6859
6886
  /**
@@ -3238,7 +3238,9 @@ declare function createWebSearchPreProcessor(options?: WebSearchPreProcessorOpti
3238
3238
  */
3239
3239
  interface Logger {
3240
3240
  debug: (...args: unknown[]) => void;
3241
- /** Not used internally by the SDK today, but included so custom loggers can receive all standard levels. */
3241
+ /** Used for outcomes that are noteworthy but expected — a consolidation
3242
+ * refusal that preserved a memory, for example. Keep genuinely-wrong things at
3243
+ * `warn` so the two stay distinguishable in a log search. */
3242
3244
  info: (...args: unknown[]) => void;
3243
3245
  warn: (...args: unknown[]) => void;
3244
3246
  error: (...args: unknown[]) => void;
@@ -6800,9 +6802,9 @@ interface RecallDiagnostics {
6800
6802
  type RetainAction = "create" | "merge" | "update" | "skip" | "suppressed" | "supersede";
6801
6803
  type RetainSource = "manual" | "auto-extracted" | "capsule";
6802
6804
  /**
6803
- * Why a retain fell back to "create" instead of applying a consolidation
6804
- * decision. Each value names a DIFFERENT thing to go fix, which is the point of
6805
- * keeping them apart:
6805
+ * Why a retain returned "create" instead of applying a consolidation decision.
6806
+ * Each value names a DIFFERENT thing to go fix, which is the point of keeping
6807
+ * them apart — and one of them is not a fault at all, see `subject_mismatch`:
6806
6808
  *
6807
6809
  * - `llm_error` — the consolidation call never produced a response (network,
6808
6810
  * timeout, 5xx, 429, empty completion, or missing credentials). Look at the
@@ -6817,8 +6819,27 @@ type RetainSource = "manual" | "auto-extracted" | "capsule";
6817
6819
  * rate points at write contention (for example an auto-extraction worker and a
6818
6820
  * manual write racing over the same vault), which quietly costs you the
6819
6821
  * dedup that decision would have performed.
6820
- */
6821
- type ConsolidationFallbackReason = "llm_error" | "invalid_response" | "target_vanished";
6822
+ * - `subject_mismatch` — NOT a fault. The model returned a well-formed
6823
+ * `supersede` and we refused it because the two subjects it named were
6824
+ * different people, so retiring the target would have hidden a memory that is
6825
+ * still true (#822 — "User's sister lives in Denver" retiring "User lives in
6826
+ * Denver"). The refusal is the feature; do not alarm on it. What the rate does
6827
+ * tell you is how often the model reaches for a cross-subject supersede, and —
6828
+ * because the guard is inert unless the model fills in its subject fields — a
6829
+ * rate of exactly zero is as likely to mean "the fields are being omitted" as
6830
+ * "the mistake never happens". Check compliance before reading zero as health.
6831
+ */
6832
+ type ConsolidationFallbackReason = "llm_error" | "invalid_response" | "target_vanished"
6833
+ /**
6834
+ * A `supersede` was downgraded to `create` because the model's own stated
6835
+ * subjects disagreed: the new fact is about someone other than the fact it
6836
+ * wanted to retire (#822 — "User's sister lives in Denver" retiring "User
6837
+ * lives in Denver"). NOT an error; the decision was refused on purpose, and
6838
+ * the refusal preserved a true memory. Retiring a superseded row hides it from
6839
+ * recall, so this is the one fallback that prevents data loss rather than
6840
+ * reporting degraded quality.
6841
+ */
6842
+ | "subject_mismatch";
6822
6843
  interface RetainOptions {
6823
6844
  source?: RetainSource;
6824
6845
  sourceChunkIds?: string[];
@@ -6854,6 +6875,12 @@ interface RetainOptions {
6854
6875
  * JSON that violates the schema (unknown action, bad targetId).
6855
6876
  * A flaky consolidator silently accumulates duplicate memories;
6856
6877
  * wire this to logging/metrics so the fallback rate is observable.
6878
+ *
6879
+ * `subject_mismatch` is the odd one out and worth reading separately: the
6880
+ * model answered fine and we REFUSED its supersede because its own stated
6881
+ * subjects disagreed (#822). A non-zero rate is the guard doing its job, not
6882
+ * a problem — the rate to watch is how often it fires, which is also the
6883
+ * only signal for how reliably the model fills the subject fields in at all.
6857
6884
  */
6858
6885
  onFallback?: (reason: ConsolidationFallbackReason) => void;
6859
6886
  /**
@@ -173,7 +173,7 @@ import {
173
173
  isPiiRedactor,
174
174
  notifyConsolidationFallback,
175
175
  resolvePiiRedactor
176
- } from "./chunk-3NPDZZGO.mjs";
176
+ } from "./chunk-VMI74IUX.mjs";
177
177
  import {
178
178
  callPortalJsonCompletion,
179
179
  consoleLogger,
@@ -7522,7 +7522,7 @@ async function tryConsolidate(trimmed, ctx, options, prepared) {
7522
7522
  content: m.content,
7523
7523
  similarity: m.similarity
7524
7524
  }));
7525
- const { consolidateMemory: doConsolidate } = await import("./consolidate-POPTUI54.mjs");
7525
+ const { consolidateMemory: doConsolidate } = await import("./consolidate-AMPKH6YJ.mjs");
7526
7526
  const decision = await doConsolidate(trimmed, candidates, consolidateOptions);
7527
7527
  if (decision.action === "create") return null;
7528
7528
  if (decision.action === "supersede" && decision.content) {
@@ -643,10 +643,16 @@ function createStreamingDeAnonymizer(redactor, emit) {
643
643
 
644
644
  // src/lib/memory/consolidationFallback.ts
645
645
  function notifyConsolidationFallback(reason, onFallback, detail) {
646
+ const refused = reason === "subject_mismatch";
647
+ const message = refused ? `memory/consolidate: refused a cross-subject supersede, created instead (${reason})` : `memory/consolidate: degraded to create (${reason})`;
648
+ const log = getLogger();
646
649
  if (detail !== void 0) {
647
- getLogger().warn(`memory/consolidate: degraded to create (${reason})`, detail);
650
+ if (refused) log.info(message, detail);
651
+ else log.warn(message, detail);
652
+ } else if (refused) {
653
+ log.info(message);
648
654
  } else {
649
- getLogger().warn(`memory/consolidate: degraded to create (${reason})`);
655
+ log.warn(message);
650
656
  }
651
657
  try {
652
658
  onFallback?.(reason);
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  notifyConsolidationFallback,
3
3
  resolvePiiRedactor
4
- } from "./chunk-RT75PNSJ.mjs";
4
+ } from "./chunk-KF4HV3QE.mjs";
5
5
  import {
6
6
  callPortalJsonCompletion
7
7
  } from "./chunk-OBDRKQQI.mjs";
@@ -39,12 +39,15 @@ OUTPUT \u2014 strict JSON, no prose:
39
39
  "action": "create" | "update" | "supersede" | "noop",
40
40
  "targetId": "<existing memory id \u2014 required for update/noop, omit for create/supersede>",
41
41
  "targetIds": ["<id>", ...],
42
- "content": "<content \u2014 required for create/update/supersede, omit for noop>"
42
+ "content": "<content \u2014 required for create/update/supersede, omit for noop>",
43
+ "newSubject": "<required for supersede: WHO the new memory is about>",
44
+ "targetSubject": "<required for supersede: WHO the memory in targetId is about>",
45
+ "targetSubjects": ["<required for supersede: WHO each memory you are retiring is about \u2014 one entry per retired id>"]
43
46
  }
44
47
 
45
48
  For "create": content is the new memory verbatim (or a slight refinement); omit targetId/targetIds.
46
49
  For "update": content is the merged richest-version, \u226480 words; targetId is the single memory to update.
47
- For "supersede": content is the NEW fact only (\u226480 words); "targetIds" lists EVERY stale memory being retired (all candidates describing the same now-changed attribute).
50
+ For "supersede": content is the NEW fact only (\u226480 words); "targetIds" lists EVERY stale memory being retired (all candidates describing the same now-changed attribute). You MUST also name the subjects: "newSubject" for the new memory, and one entry in "targetSubjects" for EACH memory you are retiring (use "targetSubject" as well when you retire a single memory). List a subject ONLY for the memories you are actually retiring, not for every candidate you were shown. Write the plain subject, not a sentence: "the user", "the user's sister", "the user's manager", "Biscuit the dog". Rule 1a means a supersede is only valid when every one of those subjects is the SAME as "newSubject"; name them and check them against each other before you commit to the action. If ANY of them differs, the answer is "create" \u2014 do not retire the ones that match and keep the rest, because a group that mixes subjects was grouped wrongly in the first place. A subjectless fact ("Lives in Denver", "Works at Riverbend") is about "the user" \u2014 the extractor omits the subject when it is the user, so treat the absence of a subject as the user, never as unknown.
48
51
  For "noop": no content (existing memory is already correct); targetId is that memory.`;
49
52
  async function consolidateMemory(newContent, candidates, options) {
50
53
  const fallback = {
@@ -101,7 +104,13 @@ ${candidateText}`;
101
104
  if (restored.unresolved) {
102
105
  return degrade("invalid_response", fallback, options);
103
106
  }
104
- return { ...result, content: restored.text };
107
+ return reportRefusal({ ...result, content: restored.text }, options);
108
+ }
109
+ return reportRefusal(result, options);
110
+ }
111
+ function reportRefusal(result, options) {
112
+ if (result.fallbackReason) {
113
+ notifyConsolidationFallback(result.fallbackReason, options.onFallback);
105
114
  }
106
115
  return result;
107
116
  }
@@ -109,6 +118,19 @@ function degrade(reason, fallback, options, detail) {
109
118
  notifyConsolidationFallback(reason, options.onFallback, detail);
110
119
  return { ...fallback, fallbackReason: reason };
111
120
  }
121
+ function normalizeSubject(raw) {
122
+ if (typeof raw !== "string") return "";
123
+ let s = raw.trim().toLowerCase().replace(/[.,;:!?]+$/u, "").trim();
124
+ s = s.replace(/^the\s+/u, "");
125
+ s = s.replace(/^users'?\s+/u, "").replace(/^user's\s+/u, "");
126
+ return s.trim();
127
+ }
128
+ var USER_SUBJECT_ALIASES = /* @__PURE__ */ new Set(["user", "themselves", "themself", "they", "me", "self"]);
129
+ function subjectKey(raw) {
130
+ const s = normalizeSubject(raw);
131
+ if (s.length === 0) return "";
132
+ return USER_SUBJECT_ALIASES.has(s) ? "user" : s;
133
+ }
112
134
  function validate(parsed, newContent, validIds) {
113
135
  if (typeof parsed !== "object" || parsed === null) return null;
114
136
  const obj = parsed;
@@ -136,6 +158,18 @@ function validate(parsed, newContent, validIds) {
136
158
  ...new Set(rawIds.filter((id) => typeof id === "string" && validIds.has(id)))
137
159
  ];
138
160
  if (targetIds.length === 0) return null;
161
+ const newSubject = subjectKey(obj.newSubject);
162
+ if (newSubject.length > 0) {
163
+ const positional = Array.isArray(obj.targetSubjects) ? obj.targetSubjects : [];
164
+ const statedSubjects = [
165
+ ...positional.map((s) => subjectKey(s)),
166
+ // The singular pair, which is the whole story for a one-target supersede.
167
+ subjectKey(obj.targetSubject)
168
+ ];
169
+ if (statedSubjects.some((s) => s.length > 0 && s !== newSubject)) {
170
+ return { action: "create", content: c, fallbackReason: "subject_mismatch" };
171
+ }
172
+ }
139
173
  return { action: "supersede", targetId: targetIds[0], targetIds, content: c };
140
174
  }
141
175
  const targetId = typeof obj.targetId === "string" ? obj.targetId : null;
@@ -23548,10 +23548,16 @@ var init_redactor = __esm({
23548
23548
 
23549
23549
  // src/lib/memory/consolidationFallback.ts
23550
23550
  function notifyConsolidationFallback(reason, onFallback, detail) {
23551
+ const refused = reason === "subject_mismatch";
23552
+ const message = refused ? `memory/consolidate: refused a cross-subject supersede, created instead (${reason})` : `memory/consolidate: degraded to create (${reason})`;
23553
+ const log = getLogger();
23551
23554
  if (detail !== void 0) {
23552
- getLogger().warn(`memory/consolidate: degraded to create (${reason})`, detail);
23555
+ if (refused) log.info(message, detail);
23556
+ else log.warn(message, detail);
23557
+ } else if (refused) {
23558
+ log.info(message);
23553
23559
  } else {
23554
- getLogger().warn(`memory/consolidate: degraded to create (${reason})`);
23560
+ log.warn(message);
23555
23561
  }
23556
23562
  try {
23557
23563
  onFallback?.(reason);
@@ -23626,7 +23632,13 @@ ${candidateText}`;
23626
23632
  if (restored.unresolved) {
23627
23633
  return degrade("invalid_response", fallback, options);
23628
23634
  }
23629
- return { ...result, content: restored.text };
23635
+ return reportRefusal({ ...result, content: restored.text }, options);
23636
+ }
23637
+ return reportRefusal(result, options);
23638
+ }
23639
+ function reportRefusal(result, options) {
23640
+ if (result.fallbackReason) {
23641
+ notifyConsolidationFallback(result.fallbackReason, options.onFallback);
23630
23642
  }
23631
23643
  return result;
23632
23644
  }
@@ -23634,6 +23646,18 @@ function degrade(reason, fallback, options, detail) {
23634
23646
  notifyConsolidationFallback(reason, options.onFallback, detail);
23635
23647
  return { ...fallback, fallbackReason: reason };
23636
23648
  }
23649
+ function normalizeSubject(raw) {
23650
+ if (typeof raw !== "string") return "";
23651
+ let s = raw.trim().toLowerCase().replace(/[.,;:!?]+$/u, "").trim();
23652
+ s = s.replace(/^the\s+/u, "");
23653
+ s = s.replace(/^users'?\s+/u, "").replace(/^user's\s+/u, "");
23654
+ return s.trim();
23655
+ }
23656
+ function subjectKey(raw) {
23657
+ const s = normalizeSubject(raw);
23658
+ if (s.length === 0) return "";
23659
+ return USER_SUBJECT_ALIASES.has(s) ? "user" : s;
23660
+ }
23637
23661
  function validate2(parsed, newContent, validIds) {
23638
23662
  if (typeof parsed !== "object" || parsed === null) return null;
23639
23663
  const obj = parsed;
@@ -23661,13 +23685,25 @@ function validate2(parsed, newContent, validIds) {
23661
23685
  ...new Set(rawIds.filter((id) => typeof id === "string" && validIds.has(id)))
23662
23686
  ];
23663
23687
  if (targetIds.length === 0) return null;
23688
+ const newSubject = subjectKey(obj.newSubject);
23689
+ if (newSubject.length > 0) {
23690
+ const positional = Array.isArray(obj.targetSubjects) ? obj.targetSubjects : [];
23691
+ const statedSubjects = [
23692
+ ...positional.map((s) => subjectKey(s)),
23693
+ // The singular pair, which is the whole story for a one-target supersede.
23694
+ subjectKey(obj.targetSubject)
23695
+ ];
23696
+ if (statedSubjects.some((s) => s.length > 0 && s !== newSubject)) {
23697
+ return { action: "create", content: c, fallbackReason: "subject_mismatch" };
23698
+ }
23699
+ }
23664
23700
  return { action: "supersede", targetId: targetIds[0], targetIds, content: c };
23665
23701
  }
23666
23702
  const targetId = typeof obj.targetId === "string" ? obj.targetId : null;
23667
23703
  if (!targetId || !validIds.has(targetId)) return null;
23668
23704
  return { action: "update", targetId, content: c };
23669
23705
  }
23670
- var DEFAULT_CONSOLIDATION_MODEL, DEFAULT_CONSOLIDATE_ATTEMPTS, DEFAULT_CONSOLIDATE_TOTAL_TIMEOUT_MS, SYSTEM_PROMPT3;
23706
+ var DEFAULT_CONSOLIDATION_MODEL, DEFAULT_CONSOLIDATE_ATTEMPTS, DEFAULT_CONSOLIDATE_TOTAL_TIMEOUT_MS, SYSTEM_PROMPT3, USER_SUBJECT_ALIASES;
23671
23707
  var init_consolidate = __esm({
23672
23708
  "src/lib/memory/consolidate.ts"() {
23673
23709
  "use strict";
@@ -23705,13 +23741,17 @@ OUTPUT \u2014 strict JSON, no prose:
23705
23741
  "action": "create" | "update" | "supersede" | "noop",
23706
23742
  "targetId": "<existing memory id \u2014 required for update/noop, omit for create/supersede>",
23707
23743
  "targetIds": ["<id>", ...],
23708
- "content": "<content \u2014 required for create/update/supersede, omit for noop>"
23744
+ "content": "<content \u2014 required for create/update/supersede, omit for noop>",
23745
+ "newSubject": "<required for supersede: WHO the new memory is about>",
23746
+ "targetSubject": "<required for supersede: WHO the memory in targetId is about>",
23747
+ "targetSubjects": ["<required for supersede: WHO each memory you are retiring is about \u2014 one entry per retired id>"]
23709
23748
  }
23710
23749
 
23711
23750
  For "create": content is the new memory verbatim (or a slight refinement); omit targetId/targetIds.
23712
23751
  For "update": content is the merged richest-version, \u226480 words; targetId is the single memory to update.
23713
- For "supersede": content is the NEW fact only (\u226480 words); "targetIds" lists EVERY stale memory being retired (all candidates describing the same now-changed attribute).
23752
+ For "supersede": content is the NEW fact only (\u226480 words); "targetIds" lists EVERY stale memory being retired (all candidates describing the same now-changed attribute). You MUST also name the subjects: "newSubject" for the new memory, and one entry in "targetSubjects" for EACH memory you are retiring (use "targetSubject" as well when you retire a single memory). List a subject ONLY for the memories you are actually retiring, not for every candidate you were shown. Write the plain subject, not a sentence: "the user", "the user's sister", "the user's manager", "Biscuit the dog". Rule 1a means a supersede is only valid when every one of those subjects is the SAME as "newSubject"; name them and check them against each other before you commit to the action. If ANY of them differs, the answer is "create" \u2014 do not retire the ones that match and keep the rest, because a group that mixes subjects was grouped wrongly in the first place. A subjectless fact ("Lives in Denver", "Works at Riverbend") is about "the user" \u2014 the extractor omits the subject when it is the user, so treat the absence of a subject as the user, never as unknown.
23714
23753
  For "noop": no content (existing memory is already correct); targetId is that memory.`;
23754
+ USER_SUBJECT_ALIASES = /* @__PURE__ */ new Set(["user", "themselves", "themself", "they", "me", "self"]);
23715
23755
  }
23716
23756
  });
23717
23757
 
@@ -5469,9 +5469,9 @@ interface RecallDiagnostics {
5469
5469
  type RetainAction = "create" | "merge" | "update" | "skip" | "suppressed" | "supersede";
5470
5470
  type RetainSource = "manual" | "auto-extracted" | "capsule";
5471
5471
  /**
5472
- * Why a retain fell back to "create" instead of applying a consolidation
5473
- * decision. Each value names a DIFFERENT thing to go fix, which is the point of
5474
- * keeping them apart:
5472
+ * Why a retain returned "create" instead of applying a consolidation decision.
5473
+ * Each value names a DIFFERENT thing to go fix, which is the point of keeping
5474
+ * them apart — and one of them is not a fault at all, see `subject_mismatch`:
5475
5475
  *
5476
5476
  * - `llm_error` — the consolidation call never produced a response (network,
5477
5477
  * timeout, 5xx, 429, empty completion, or missing credentials). Look at the
@@ -5486,8 +5486,27 @@ type RetainSource = "manual" | "auto-extracted" | "capsule";
5486
5486
  * rate points at write contention (for example an auto-extraction worker and a
5487
5487
  * manual write racing over the same vault), which quietly costs you the
5488
5488
  * dedup that decision would have performed.
5489
- */
5490
- type ConsolidationFallbackReason = "llm_error" | "invalid_response" | "target_vanished";
5489
+ * - `subject_mismatch` — NOT a fault. The model returned a well-formed
5490
+ * `supersede` and we refused it because the two subjects it named were
5491
+ * different people, so retiring the target would have hidden a memory that is
5492
+ * still true (#822 — "User's sister lives in Denver" retiring "User lives in
5493
+ * Denver"). The refusal is the feature; do not alarm on it. What the rate does
5494
+ * tell you is how often the model reaches for a cross-subject supersede, and —
5495
+ * because the guard is inert unless the model fills in its subject fields — a
5496
+ * rate of exactly zero is as likely to mean "the fields are being omitted" as
5497
+ * "the mistake never happens". Check compliance before reading zero as health.
5498
+ */
5499
+ type ConsolidationFallbackReason = "llm_error" | "invalid_response" | "target_vanished"
5500
+ /**
5501
+ * A `supersede` was downgraded to `create` because the model's own stated
5502
+ * subjects disagreed: the new fact is about someone other than the fact it
5503
+ * wanted to retire (#822 — "User's sister lives in Denver" retiring "User
5504
+ * lives in Denver"). NOT an error; the decision was refused on purpose, and
5505
+ * the refusal preserved a true memory. Retiring a superseded row hides it from
5506
+ * recall, so this is the one fallback that prevents data loss rather than
5507
+ * reporting degraded quality.
5508
+ */
5509
+ | "subject_mismatch";
5491
5510
  interface RetainOptions {
5492
5511
  source?: RetainSource;
5493
5512
  sourceChunkIds?: string[];
@@ -5523,6 +5542,12 @@ interface RetainOptions {
5523
5542
  * JSON that violates the schema (unknown action, bad targetId).
5524
5543
  * A flaky consolidator silently accumulates duplicate memories;
5525
5544
  * wire this to logging/metrics so the fallback rate is observable.
5545
+ *
5546
+ * `subject_mismatch` is the odd one out and worth reading separately: the
5547
+ * model answered fine and we REFUSED its supersede because its own stated
5548
+ * subjects disagreed (#822). A non-zero rate is the guard doing its job, not
5549
+ * a problem — the rate to watch is how often it fires, which is also the
5550
+ * only signal for how reliably the model fills the subject fields in at all.
5526
5551
  */
5527
5552
  onFallback?: (reason: ConsolidationFallbackReason) => void;
5528
5553
  /**
@@ -5469,9 +5469,9 @@ interface RecallDiagnostics {
5469
5469
  type RetainAction = "create" | "merge" | "update" | "skip" | "suppressed" | "supersede";
5470
5470
  type RetainSource = "manual" | "auto-extracted" | "capsule";
5471
5471
  /**
5472
- * Why a retain fell back to "create" instead of applying a consolidation
5473
- * decision. Each value names a DIFFERENT thing to go fix, which is the point of
5474
- * keeping them apart:
5472
+ * Why a retain returned "create" instead of applying a consolidation decision.
5473
+ * Each value names a DIFFERENT thing to go fix, which is the point of keeping
5474
+ * them apart — and one of them is not a fault at all, see `subject_mismatch`:
5475
5475
  *
5476
5476
  * - `llm_error` — the consolidation call never produced a response (network,
5477
5477
  * timeout, 5xx, 429, empty completion, or missing credentials). Look at the
@@ -5486,8 +5486,27 @@ type RetainSource = "manual" | "auto-extracted" | "capsule";
5486
5486
  * rate points at write contention (for example an auto-extraction worker and a
5487
5487
  * manual write racing over the same vault), which quietly costs you the
5488
5488
  * dedup that decision would have performed.
5489
- */
5490
- type ConsolidationFallbackReason = "llm_error" | "invalid_response" | "target_vanished";
5489
+ * - `subject_mismatch` — NOT a fault. The model returned a well-formed
5490
+ * `supersede` and we refused it because the two subjects it named were
5491
+ * different people, so retiring the target would have hidden a memory that is
5492
+ * still true (#822 — "User's sister lives in Denver" retiring "User lives in
5493
+ * Denver"). The refusal is the feature; do not alarm on it. What the rate does
5494
+ * tell you is how often the model reaches for a cross-subject supersede, and —
5495
+ * because the guard is inert unless the model fills in its subject fields — a
5496
+ * rate of exactly zero is as likely to mean "the fields are being omitted" as
5497
+ * "the mistake never happens". Check compliance before reading zero as health.
5498
+ */
5499
+ type ConsolidationFallbackReason = "llm_error" | "invalid_response" | "target_vanished"
5500
+ /**
5501
+ * A `supersede` was downgraded to `create` because the model's own stated
5502
+ * subjects disagreed: the new fact is about someone other than the fact it
5503
+ * wanted to retire (#822 — "User's sister lives in Denver" retiring "User
5504
+ * lives in Denver"). NOT an error; the decision was refused on purpose, and
5505
+ * the refusal preserved a true memory. Retiring a superseded row hides it from
5506
+ * recall, so this is the one fallback that prevents data loss rather than
5507
+ * reporting degraded quality.
5508
+ */
5509
+ | "subject_mismatch";
5491
5510
  interface RetainOptions {
5492
5511
  source?: RetainSource;
5493
5512
  sourceChunkIds?: string[];
@@ -5523,6 +5542,12 @@ interface RetainOptions {
5523
5542
  * JSON that violates the schema (unknown action, bad targetId).
5524
5543
  * A flaky consolidator silently accumulates duplicate memories;
5525
5544
  * wire this to logging/metrics so the fallback rate is observable.
5545
+ *
5546
+ * `subject_mismatch` is the odd one out and worth reading separately: the
5547
+ * model answered fine and we REFUSED its supersede because its own stated
5548
+ * subjects disagreed (#822). A non-zero rate is the guard doing its job, not
5549
+ * a problem — the rate to watch is how often it fires, which is also the
5550
+ * only signal for how reliably the model fills the subject fields in at all.
5526
5551
  */
5527
5552
  onFallback?: (reason: ConsolidationFallbackReason) => void;
5528
5553
  /**
@@ -142,7 +142,7 @@ import {
142
142
  createStreamingDeAnonymizer,
143
143
  notifyConsolidationFallback,
144
144
  resolvePiiRedactor
145
- } from "./chunk-RT75PNSJ.mjs";
145
+ } from "./chunk-KF4HV3QE.mjs";
146
146
  import {
147
147
  callPortalJsonCompletion,
148
148
  extractJsonCandidate,
@@ -5503,7 +5503,7 @@ async function tryConsolidate(trimmed, ctx, options, prepared) {
5503
5503
  content: m.content,
5504
5504
  similarity: m.similarity
5505
5505
  }));
5506
- const { consolidateMemory: doConsolidate } = await import("./consolidate-LMIDJFR6.mjs");
5506
+ const { consolidateMemory: doConsolidate } = await import("./consolidate-I6TOH7CB.mjs");
5507
5507
  const decision = await doConsolidate(trimmed, candidates, consolidateOptions);
5508
5508
  if (decision.action === "create") return null;
5509
5509
  if (decision.action === "supersede" && decision.content) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anuma/sdk",
3
- "version": "5.1.1",
3
+ "version": "6.0.0",
4
4
  "description": "",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",