@anuma/sdk 2.2.0 → 2.3.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.
@@ -14067,6 +14067,7 @@ init_portalLlm();
14067
14067
  init_operations3();
14068
14068
  init_constants();
14069
14069
  init_embeddings();
14070
+ init_vector();
14070
14071
  init_searchTool();
14071
14072
  var DEFAULT_AUTO_MERGE_THRESHOLD = 0.85;
14072
14073
  var DEFAULT_SCOPE = "private";
@@ -14128,6 +14129,15 @@ async function retain(content, ctx, options = {}) {
14128
14129
  }
14129
14130
  const embedding = await generateEmbedding(trimmed, ctx.embeddingOptions);
14130
14131
  const embeddingModel = ctx.embeddingOptions.model ?? DEFAULT_API_EMBEDDING_MODEL;
14132
+ if (options.respectTombstones) {
14133
+ const tombstoneId = await findTombstoneMatch(embedding, embeddingModel, resolvedScope, ctx, {
14134
+ threshold,
14135
+ folderId: options.folderId
14136
+ });
14137
+ if (tombstoneId) {
14138
+ return { action: "suppressed", memoryId: tombstoneId, tombstoneId, proofCount: 0 };
14139
+ }
14140
+ }
14131
14141
  const created = await createVaultMemoryOp(ctx.vaultCtx, {
14132
14142
  content: trimmed,
14133
14143
  scope: resolvedScope,
@@ -14157,6 +14167,32 @@ async function retain(content, ctx, options = {}) {
14157
14167
  function unionStrings(a, b) {
14158
14168
  return [.../* @__PURE__ */ new Set([...a, ...b])];
14159
14169
  }
14170
+ async function findTombstoneMatch(embedding, embeddingModel, scope, ctx, opts) {
14171
+ const rows = await getAllVaultMemoriesOp(ctx.vaultCtx, {
14172
+ includeDeleted: true,
14173
+ scopes: [scope],
14174
+ ...opts.folderId !== void 0 && { folderId: opts.folderId }
14175
+ });
14176
+ let bestId = null;
14177
+ let bestSim = opts.threshold;
14178
+ for (const row of rows) {
14179
+ if (!row.isDeleted || !row.embedding) continue;
14180
+ if (row.embeddingModel !== embeddingModel) continue;
14181
+ let vec;
14182
+ try {
14183
+ vec = JSON.parse(row.embedding);
14184
+ } catch {
14185
+ continue;
14186
+ }
14187
+ if (!Array.isArray(vec) || vec.length !== embedding.length) continue;
14188
+ const sim = cosineSimilarity(embedding, vec);
14189
+ if (sim >= bestSim) {
14190
+ bestSim = sim;
14191
+ bestId = row.uniqueId;
14192
+ }
14193
+ }
14194
+ return bestId;
14195
+ }
14160
14196
  function pickEventTimeUpdate(existing, incoming) {
14161
14197
  if (incoming === void 0 || incoming === null) return void 0;
14162
14198
  if (existing.eventTimeStart !== null) return void 0;
@@ -14430,6 +14466,8 @@ async function extractAndRetain(messages, retainCtx, options) {
14430
14466
  const result = await retain(candidate.content, retainCtx, {
14431
14467
  source: "auto-extracted",
14432
14468
  sourceChunkIds: candidate.sourceMessageIds,
14469
+ // Don't let extraction silently resurrect a fact the user deleted.
14470
+ respectTombstones: true,
14433
14471
  ...options.scope !== void 0 && { scope: options.scope },
14434
14472
  ...options.folderId !== void 0 && { folderId: options.folderId },
14435
14473
  ...consolidateOptions !== void 0 && { consolidateOptions },
@@ -14437,7 +14475,7 @@ async function extractAndRetain(messages, retainCtx, options) {
14437
14475
  });
14438
14476
  succeededCandidates.push(candidate);
14439
14477
  results.push(result);
14440
- if (options.entityCtx && candidate.entities.length > 0) {
14478
+ if (result.action !== "suppressed" && options.entityCtx && candidate.entities.length > 0) {
14441
14479
  try {
14442
14480
  if (!await isMemoryTopicsUserManaged(options.entityCtx, result.memoryId)) {
14443
14481
  await linkMemoryEntitiesOp(options.entityCtx, result.memoryId, candidate.entities, {
@@ -14681,6 +14719,7 @@ function createAutoExtractor(options) {
14681
14719
  );
14682
14720
  stateFor(conversationId).watermark = window2[window2.length - 1].id;
14683
14721
  for (let i = 0; i < results.length; i++) {
14722
+ if (results[i].action === "suppressed") continue;
14684
14723
  options.onMemoryExtracted?.({
14685
14724
  candidate: candidates[i],
14686
14725
  result: results[i],
@@ -6256,7 +6256,7 @@ interface RecallResult {
6256
6256
  /** Diagnostic: total memories in the vault when fact lane was queried. */
6257
6257
  vaultSize?: number;
6258
6258
  }
6259
- type RetainAction = "create" | "merge" | "update" | "skip";
6259
+ type RetainAction = "create" | "merge" | "update" | "skip" | "suppressed";
6260
6260
  type RetainSource = "manual" | "auto-extracted" | "capsule";
6261
6261
  /** Why the consolidator fell back to "create" instead of a real decision. */
6262
6262
  type ConsolidationFallbackReason = "llm_error" | "invalid_response";
@@ -6267,6 +6267,14 @@ interface RetainOptions {
6267
6267
  folderId?: string | null;
6268
6268
  /** When provided, applies merge-on-write logic instead of plain insert. */
6269
6269
  enableAutoMerge?: boolean;
6270
+ /**
6271
+ * When true, a would-be create is suppressed if it matches a soft-deleted
6272
+ * ("tombstoned") memory above the auto-merge threshold — so auto-extraction
6273
+ * can't silently resurrect a fact the user deleted. Off by default so manual
6274
+ * and other `retain()` callers are unaffected; auto-extraction opts in.
6275
+ * Returns `action: 'suppressed'` with the matched `tombstoneId`.
6276
+ */
6277
+ respectTombstones?: boolean;
6270
6278
  /** Cosine similarity threshold for auto-merge. Default: 0.85. */
6271
6279
  autoMergeThreshold?: number;
6272
6280
  /**
@@ -6316,7 +6324,12 @@ interface RetainResult {
6316
6324
  memoryId: string;
6317
6325
  /** When action is 'merge' or 'update', the prior memory's id. */
6318
6326
  targetId?: string;
6319
- /** Updated proof_count after this write. */
6327
+ /**
6328
+ * When action is 'suppressed', the id of the soft-deleted memory that blocked
6329
+ * re-creation. `memoryId` is set to the same id (no new memory was written).
6330
+ */
6331
+ tombstoneId?: string;
6332
+ /** Updated proof_count after this write. 0 when nothing was written (suppressed). */
6320
6333
  proofCount: number;
6321
6334
  }
6322
6335
 
@@ -6256,7 +6256,7 @@ interface RecallResult {
6256
6256
  /** Diagnostic: total memories in the vault when fact lane was queried. */
6257
6257
  vaultSize?: number;
6258
6258
  }
6259
- type RetainAction = "create" | "merge" | "update" | "skip";
6259
+ type RetainAction = "create" | "merge" | "update" | "skip" | "suppressed";
6260
6260
  type RetainSource = "manual" | "auto-extracted" | "capsule";
6261
6261
  /** Why the consolidator fell back to "create" instead of a real decision. */
6262
6262
  type ConsolidationFallbackReason = "llm_error" | "invalid_response";
@@ -6267,6 +6267,14 @@ interface RetainOptions {
6267
6267
  folderId?: string | null;
6268
6268
  /** When provided, applies merge-on-write logic instead of plain insert. */
6269
6269
  enableAutoMerge?: boolean;
6270
+ /**
6271
+ * When true, a would-be create is suppressed if it matches a soft-deleted
6272
+ * ("tombstoned") memory above the auto-merge threshold — so auto-extraction
6273
+ * can't silently resurrect a fact the user deleted. Off by default so manual
6274
+ * and other `retain()` callers are unaffected; auto-extraction opts in.
6275
+ * Returns `action: 'suppressed'` with the matched `tombstoneId`.
6276
+ */
6277
+ respectTombstones?: boolean;
6270
6278
  /** Cosine similarity threshold for auto-merge. Default: 0.85. */
6271
6279
  autoMergeThreshold?: number;
6272
6280
  /**
@@ -6316,7 +6324,12 @@ interface RetainResult {
6316
6324
  memoryId: string;
6317
6325
  /** When action is 'merge' or 'update', the prior memory's id. */
6318
6326
  targetId?: string;
6319
- /** Updated proof_count after this write. */
6327
+ /**
6328
+ * When action is 'suppressed', the id of the soft-deleted memory that blocked
6329
+ * re-creation. `memoryId` is set to the same id (no new memory was written).
6330
+ */
6331
+ tombstoneId?: string;
6332
+ /** Updated proof_count after this write. 0 when nothing was written (suppressed). */
6320
6333
  proofCount: number;
6321
6334
  }
6322
6335
 
@@ -125,7 +125,7 @@ import {
125
125
  updateVaultMemoryOp,
126
126
  useEncryption,
127
127
  vaultMemoryToStored
128
- } from "./chunk-KRNJVLI6.mjs";
128
+ } from "./chunk-JUDJVX66.mjs";
129
129
  import {
130
130
  PiiRedactor,
131
131
  createStreamingDeAnonymizer,
@@ -6741,6 +6741,15 @@ async function retain(content, ctx, options = {}) {
6741
6741
  }
6742
6742
  const embedding = await generateEmbedding(trimmed, ctx.embeddingOptions);
6743
6743
  const embeddingModel = ctx.embeddingOptions.model ?? DEFAULT_API_EMBEDDING_MODEL;
6744
+ if (options.respectTombstones) {
6745
+ const tombstoneId = await findTombstoneMatch(embedding, embeddingModel, resolvedScope, ctx, {
6746
+ threshold,
6747
+ folderId: options.folderId
6748
+ });
6749
+ if (tombstoneId) {
6750
+ return { action: "suppressed", memoryId: tombstoneId, tombstoneId, proofCount: 0 };
6751
+ }
6752
+ }
6744
6753
  const created = await createVaultMemoryOp(ctx.vaultCtx, {
6745
6754
  content: trimmed,
6746
6755
  scope: resolvedScope,
@@ -6770,6 +6779,32 @@ async function retain(content, ctx, options = {}) {
6770
6779
  function unionStrings(a, b) {
6771
6780
  return [.../* @__PURE__ */ new Set([...a, ...b])];
6772
6781
  }
6782
+ async function findTombstoneMatch(embedding, embeddingModel, scope, ctx, opts) {
6783
+ const rows = await getAllVaultMemoriesOp(ctx.vaultCtx, {
6784
+ includeDeleted: true,
6785
+ scopes: [scope],
6786
+ ...opts.folderId !== void 0 && { folderId: opts.folderId }
6787
+ });
6788
+ let bestId = null;
6789
+ let bestSim = opts.threshold;
6790
+ for (const row of rows) {
6791
+ if (!row.isDeleted || !row.embedding) continue;
6792
+ if (row.embeddingModel !== embeddingModel) continue;
6793
+ let vec;
6794
+ try {
6795
+ vec = JSON.parse(row.embedding);
6796
+ } catch {
6797
+ continue;
6798
+ }
6799
+ if (!Array.isArray(vec) || vec.length !== embedding.length) continue;
6800
+ const sim = cosineSimilarity(embedding, vec);
6801
+ if (sim >= bestSim) {
6802
+ bestSim = sim;
6803
+ bestId = row.uniqueId;
6804
+ }
6805
+ }
6806
+ return bestId;
6807
+ }
6773
6808
  function pickEventTimeUpdate(existing, incoming) {
6774
6809
  if (incoming === void 0 || incoming === null) return void 0;
6775
6810
  if (existing.eventTimeStart !== null) return void 0;
@@ -7043,6 +7078,8 @@ async function extractAndRetain(messages, retainCtx, options) {
7043
7078
  const result = await retain(candidate.content, retainCtx, {
7044
7079
  source: "auto-extracted",
7045
7080
  sourceChunkIds: candidate.sourceMessageIds,
7081
+ // Don't let extraction silently resurrect a fact the user deleted.
7082
+ respectTombstones: true,
7046
7083
  ...options.scope !== void 0 && { scope: options.scope },
7047
7084
  ...options.folderId !== void 0 && { folderId: options.folderId },
7048
7085
  ...consolidateOptions !== void 0 && { consolidateOptions },
@@ -7050,7 +7087,7 @@ async function extractAndRetain(messages, retainCtx, options) {
7050
7087
  });
7051
7088
  succeededCandidates.push(candidate);
7052
7089
  results.push(result);
7053
- if (options.entityCtx && candidate.entities.length > 0) {
7090
+ if (result.action !== "suppressed" && options.entityCtx && candidate.entities.length > 0) {
7054
7091
  try {
7055
7092
  if (!await isMemoryTopicsUserManaged(options.entityCtx, result.memoryId)) {
7056
7093
  await linkMemoryEntitiesOp(options.entityCtx, result.memoryId, candidate.entities, {
@@ -7292,6 +7329,7 @@ function createAutoExtractor(options) {
7292
7329
  );
7293
7330
  stateFor(conversationId).watermark = window2[window2.length - 1].id;
7294
7331
  for (let i = 0; i < results.length; i++) {
7332
+ if (results[i].action === "suppressed") continue;
7295
7333
  options.onMemoryExtracted?.({
7296
7334
  candidate: candidates[i],
7297
7335
  result: results[i],
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  recall
3
- } from "./chunk-KRNJVLI6.mjs";
3
+ } from "./chunk-JUDJVX66.mjs";
4
4
  import "./chunk-DFVCS3QQ.mjs";
5
5
  import "./chunk-GYKJOWEO.mjs";
6
6
  export {
@@ -18975,7 +18975,7 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
18975
18975
  legacy.map((r) => ({ id: r.uniqueId, content: r.content, score: r.similarity }))
18976
18976
  );
18977
18977
  }
18978
- const { recall: recall2 } = await import("./recall-K2YHLBBV.mjs");
18978
+ const { recall: recall2 } = await import("./recall-EWMI64SA.mjs");
18979
18979
  const result = await recall2(
18980
18980
  query,
18981
18981
  { vaultCtx, embeddingOptions, vaultCache: cache },
@@ -25740,6 +25740,7 @@ init_portalLlm();
25740
25740
  init_operations3();
25741
25741
  init_constants();
25742
25742
  init_embeddings();
25743
+ init_vector();
25743
25744
  init_searchTool();
25744
25745
  var DEFAULT_AUTO_MERGE_THRESHOLD = 0.85;
25745
25746
  var DEFAULT_SCOPE = "private";
@@ -25801,6 +25802,15 @@ async function retain(content, ctx, options = {}) {
25801
25802
  }
25802
25803
  const embedding = await generateEmbedding(trimmed, ctx.embeddingOptions);
25803
25804
  const embeddingModel = ctx.embeddingOptions.model ?? DEFAULT_API_EMBEDDING_MODEL;
25805
+ if (options.respectTombstones) {
25806
+ const tombstoneId = await findTombstoneMatch(embedding, embeddingModel, resolvedScope, ctx, {
25807
+ threshold,
25808
+ folderId: options.folderId
25809
+ });
25810
+ if (tombstoneId) {
25811
+ return { action: "suppressed", memoryId: tombstoneId, tombstoneId, proofCount: 0 };
25812
+ }
25813
+ }
25804
25814
  const created = await createVaultMemoryOp(ctx.vaultCtx, {
25805
25815
  content: trimmed,
25806
25816
  scope: resolvedScope,
@@ -25830,6 +25840,32 @@ async function retain(content, ctx, options = {}) {
25830
25840
  function unionStrings(a, b) {
25831
25841
  return [.../* @__PURE__ */ new Set([...a, ...b])];
25832
25842
  }
25843
+ async function findTombstoneMatch(embedding, embeddingModel, scope, ctx, opts) {
25844
+ const rows = await getAllVaultMemoriesOp(ctx.vaultCtx, {
25845
+ includeDeleted: true,
25846
+ scopes: [scope],
25847
+ ...opts.folderId !== void 0 && { folderId: opts.folderId }
25848
+ });
25849
+ let bestId = null;
25850
+ let bestSim = opts.threshold;
25851
+ for (const row of rows) {
25852
+ if (!row.isDeleted || !row.embedding) continue;
25853
+ if (row.embeddingModel !== embeddingModel) continue;
25854
+ let vec;
25855
+ try {
25856
+ vec = JSON.parse(row.embedding);
25857
+ } catch {
25858
+ continue;
25859
+ }
25860
+ if (!Array.isArray(vec) || vec.length !== embedding.length) continue;
25861
+ const sim = cosineSimilarity(embedding, vec);
25862
+ if (sim >= bestSim) {
25863
+ bestSim = sim;
25864
+ bestId = row.uniqueId;
25865
+ }
25866
+ }
25867
+ return bestId;
25868
+ }
25833
25869
  function pickEventTimeUpdate(existing, incoming) {
25834
25870
  if (incoming === void 0 || incoming === null) return void 0;
25835
25871
  if (existing.eventTimeStart !== null) return void 0;
@@ -26103,6 +26139,8 @@ async function extractAndRetain(messages, retainCtx, options) {
26103
26139
  const result = await retain(candidate.content, retainCtx, {
26104
26140
  source: "auto-extracted",
26105
26141
  sourceChunkIds: candidate.sourceMessageIds,
26142
+ // Don't let extraction silently resurrect a fact the user deleted.
26143
+ respectTombstones: true,
26106
26144
  ...options.scope !== void 0 && { scope: options.scope },
26107
26145
  ...options.folderId !== void 0 && { folderId: options.folderId },
26108
26146
  ...consolidateOptions !== void 0 && { consolidateOptions },
@@ -26110,7 +26148,7 @@ async function extractAndRetain(messages, retainCtx, options) {
26110
26148
  });
26111
26149
  succeededCandidates.push(candidate);
26112
26150
  results.push(result);
26113
- if (options.entityCtx && candidate.entities.length > 0) {
26151
+ if (result.action !== "suppressed" && options.entityCtx && candidate.entities.length > 0) {
26114
26152
  try {
26115
26153
  if (!await isMemoryTopicsUserManaged(options.entityCtx, result.memoryId)) {
26116
26154
  await linkMemoryEntitiesOp(options.entityCtx, result.memoryId, candidate.entities, {
@@ -26354,6 +26392,7 @@ function createAutoExtractor(options) {
26354
26392
  );
26355
26393
  stateFor(conversationId).watermark = window2[window2.length - 1].id;
26356
26394
  for (let i = 0; i < results.length; i++) {
26395
+ if (results[i].action === "suppressed") continue;
26357
26396
  options.onMemoryExtracted?.({
26358
26397
  candidate: candidates[i],
26359
26398
  result: results[i],
@@ -5759,7 +5759,7 @@ interface RecallResult {
5759
5759
  /** Diagnostic: total memories in the vault when fact lane was queried. */
5760
5760
  vaultSize?: number;
5761
5761
  }
5762
- type RetainAction = "create" | "merge" | "update" | "skip";
5762
+ type RetainAction = "create" | "merge" | "update" | "skip" | "suppressed";
5763
5763
  type RetainSource = "manual" | "auto-extracted" | "capsule";
5764
5764
  /** Why the consolidator fell back to "create" instead of a real decision. */
5765
5765
  type ConsolidationFallbackReason = "llm_error" | "invalid_response";
@@ -5770,6 +5770,14 @@ interface RetainOptions {
5770
5770
  folderId?: string | null;
5771
5771
  /** When provided, applies merge-on-write logic instead of plain insert. */
5772
5772
  enableAutoMerge?: boolean;
5773
+ /**
5774
+ * When true, a would-be create is suppressed if it matches a soft-deleted
5775
+ * ("tombstoned") memory above the auto-merge threshold — so auto-extraction
5776
+ * can't silently resurrect a fact the user deleted. Off by default so manual
5777
+ * and other `retain()` callers are unaffected; auto-extraction opts in.
5778
+ * Returns `action: 'suppressed'` with the matched `tombstoneId`.
5779
+ */
5780
+ respectTombstones?: boolean;
5773
5781
  /** Cosine similarity threshold for auto-merge. Default: 0.85. */
5774
5782
  autoMergeThreshold?: number;
5775
5783
  /**
@@ -5819,7 +5827,12 @@ interface RetainResult {
5819
5827
  memoryId: string;
5820
5828
  /** When action is 'merge' or 'update', the prior memory's id. */
5821
5829
  targetId?: string;
5822
- /** Updated proof_count after this write. */
5830
+ /**
5831
+ * When action is 'suppressed', the id of the soft-deleted memory that blocked
5832
+ * re-creation. `memoryId` is set to the same id (no new memory was written).
5833
+ */
5834
+ tombstoneId?: string;
5835
+ /** Updated proof_count after this write. 0 when nothing was written (suppressed). */
5823
5836
  proofCount: number;
5824
5837
  }
5825
5838
 
@@ -5759,7 +5759,7 @@ interface RecallResult {
5759
5759
  /** Diagnostic: total memories in the vault when fact lane was queried. */
5760
5760
  vaultSize?: number;
5761
5761
  }
5762
- type RetainAction = "create" | "merge" | "update" | "skip";
5762
+ type RetainAction = "create" | "merge" | "update" | "skip" | "suppressed";
5763
5763
  type RetainSource = "manual" | "auto-extracted" | "capsule";
5764
5764
  /** Why the consolidator fell back to "create" instead of a real decision. */
5765
5765
  type ConsolidationFallbackReason = "llm_error" | "invalid_response";
@@ -5770,6 +5770,14 @@ interface RetainOptions {
5770
5770
  folderId?: string | null;
5771
5771
  /** When provided, applies merge-on-write logic instead of plain insert. */
5772
5772
  enableAutoMerge?: boolean;
5773
+ /**
5774
+ * When true, a would-be create is suppressed if it matches a soft-deleted
5775
+ * ("tombstoned") memory above the auto-merge threshold — so auto-extraction
5776
+ * can't silently resurrect a fact the user deleted. Off by default so manual
5777
+ * and other `retain()` callers are unaffected; auto-extraction opts in.
5778
+ * Returns `action: 'suppressed'` with the matched `tombstoneId`.
5779
+ */
5780
+ respectTombstones?: boolean;
5773
5781
  /** Cosine similarity threshold for auto-merge. Default: 0.85. */
5774
5782
  autoMergeThreshold?: number;
5775
5783
  /**
@@ -5819,7 +5827,12 @@ interface RetainResult {
5819
5827
  memoryId: string;
5820
5828
  /** When action is 'merge' or 'update', the prior memory's id. */
5821
5829
  targetId?: string;
5822
- /** Updated proof_count after this write. */
5830
+ /**
5831
+ * When action is 'suppressed', the id of the soft-deleted memory that blocked
5832
+ * re-creation. `memoryId` is set to the same id (no new memory was written).
5833
+ */
5834
+ tombstoneId?: string;
5835
+ /** Updated proof_count after this write. 0 when nothing was written (suppressed). */
5823
5836
  proofCount: number;
5824
5837
  }
5825
5838
 
@@ -99,7 +99,7 @@ import {
99
99
  updateVaultMemoryOp,
100
100
  upsertMessageOp,
101
101
  vaultMemoryToStored
102
- } from "./chunk-KEMPJE6C.mjs";
102
+ } from "./chunk-3AWYZRTS.mjs";
103
103
  import {
104
104
  createStreamingDeAnonymizer,
105
105
  resolvePiiRedactor
@@ -4722,6 +4722,15 @@ async function retain(content, ctx, options = {}) {
4722
4722
  }
4723
4723
  const embedding = await generateEmbedding(trimmed, ctx.embeddingOptions);
4724
4724
  const embeddingModel = ctx.embeddingOptions.model ?? DEFAULT_API_EMBEDDING_MODEL;
4725
+ if (options.respectTombstones) {
4726
+ const tombstoneId = await findTombstoneMatch(embedding, embeddingModel, resolvedScope, ctx, {
4727
+ threshold,
4728
+ folderId: options.folderId
4729
+ });
4730
+ if (tombstoneId) {
4731
+ return { action: "suppressed", memoryId: tombstoneId, tombstoneId, proofCount: 0 };
4732
+ }
4733
+ }
4725
4734
  const created = await createVaultMemoryOp(ctx.vaultCtx, {
4726
4735
  content: trimmed,
4727
4736
  scope: resolvedScope,
@@ -4751,6 +4760,32 @@ async function retain(content, ctx, options = {}) {
4751
4760
  function unionStrings(a, b) {
4752
4761
  return [.../* @__PURE__ */ new Set([...a, ...b])];
4753
4762
  }
4763
+ async function findTombstoneMatch(embedding, embeddingModel, scope, ctx, opts) {
4764
+ const rows = await getAllVaultMemoriesOp(ctx.vaultCtx, {
4765
+ includeDeleted: true,
4766
+ scopes: [scope],
4767
+ ...opts.folderId !== void 0 && { folderId: opts.folderId }
4768
+ });
4769
+ let bestId = null;
4770
+ let bestSim = opts.threshold;
4771
+ for (const row of rows) {
4772
+ if (!row.isDeleted || !row.embedding) continue;
4773
+ if (row.embeddingModel !== embeddingModel) continue;
4774
+ let vec;
4775
+ try {
4776
+ vec = JSON.parse(row.embedding);
4777
+ } catch {
4778
+ continue;
4779
+ }
4780
+ if (!Array.isArray(vec) || vec.length !== embedding.length) continue;
4781
+ const sim = cosineSimilarity(embedding, vec);
4782
+ if (sim >= bestSim) {
4783
+ bestSim = sim;
4784
+ bestId = row.uniqueId;
4785
+ }
4786
+ }
4787
+ return bestId;
4788
+ }
4754
4789
  function pickEventTimeUpdate(existing, incoming) {
4755
4790
  if (incoming === void 0 || incoming === null) return void 0;
4756
4791
  if (existing.eventTimeStart !== null) return void 0;
@@ -5024,6 +5059,8 @@ async function extractAndRetain(messages, retainCtx, options) {
5024
5059
  const result = await retain(candidate.content, retainCtx, {
5025
5060
  source: "auto-extracted",
5026
5061
  sourceChunkIds: candidate.sourceMessageIds,
5062
+ // Don't let extraction silently resurrect a fact the user deleted.
5063
+ respectTombstones: true,
5027
5064
  ...options.scope !== void 0 && { scope: options.scope },
5028
5065
  ...options.folderId !== void 0 && { folderId: options.folderId },
5029
5066
  ...consolidateOptions !== void 0 && { consolidateOptions },
@@ -5031,7 +5068,7 @@ async function extractAndRetain(messages, retainCtx, options) {
5031
5068
  });
5032
5069
  succeededCandidates.push(candidate);
5033
5070
  results.push(result);
5034
- if (options.entityCtx && candidate.entities.length > 0) {
5071
+ if (result.action !== "suppressed" && options.entityCtx && candidate.entities.length > 0) {
5035
5072
  try {
5036
5073
  if (!await isMemoryTopicsUserManaged(options.entityCtx, result.memoryId)) {
5037
5074
  await linkMemoryEntitiesOp(options.entityCtx, result.memoryId, candidate.entities, {
@@ -5273,6 +5310,7 @@ function createAutoExtractor(options) {
5273
5310
  );
5274
5311
  stateFor(conversationId).watermark = window2[window2.length - 1].id;
5275
5312
  for (let i = 0; i < results.length; i++) {
5313
+ if (results[i].action === "suppressed") continue;
5276
5314
  options.onMemoryExtracted?.({
5277
5315
  candidate: candidates[i],
5278
5316
  result: results[i],
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  recall
3
- } from "./chunk-KEMPJE6C.mjs";
3
+ } from "./chunk-3AWYZRTS.mjs";
4
4
  import "./chunk-AHJT66EO.mjs";
5
5
  import "./chunk-3RPGXXPZ.mjs";
6
6
  export {
@@ -1516,6 +1516,11 @@ declare function exportAppToHtml(options: ExportAppOptions): string;
1516
1516
  * The workflow this prompt mandates (create → critique → patch → audit →
1517
1517
  * patch → verify) needs 7–8 tool rounds on a substantial turn. Hosts capping
1518
1518
  * `maxToolRounds` below 8 starve the trailing `verify_app` step.
1519
+ *
1520
+ * ⚠ The substring "App Builder tools (create_file, patch_file," is relied on by
1521
+ * backend infrastructure — matched anywhere in the text (substring), not as a
1522
+ * prefix/head — so keep it byte-identical; see internal docs.
1523
+ * Guarded by the backend-sync test in appBuilderPrompt.test.ts.
1519
1524
  */
1520
1525
  declare const APP_BUILDER_PROMPT = "App Builder tools (create_file, patch_file, and the rest described below) are available this turn, but they are OPTIONAL \u2014 having them does not mean you should build anything.\n\nBuild ONLY when the user gives an explicit instruction to build, create, generate, or change an app, tool, or UI \u2014 e.g. \"build a todo app\", \"make a snake game\", \"add a footer to the app\", \"change the background to blue\". A bare app name or feature reference is NOT a build instruction: if the user just says \"todo app\", \"a snake game\", or \"qr code\", or asks a question about an app, do NOT generate anything \u2014 reply normally, or ask what they'd like you to build. The same holds for greetings, questions, stories, explanations, advice, or code that isn't a runnable app: respond normally and do not create files. When it's unclear whether the user actually wants an app built, ask instead of building.\n\nOnce the user HAS given an explicit build instruction, you are in App Builder mode: you produce polished, production-quality React apps that feel designed \u2014 not generic.\n\nDESIGN: on the initial build, before writing code, briefly state (2-3 sentences) what you want this app to feel like \u2014 name the specific fonts, the palette in hex, and one signature detail. Be concrete: \"Fraunces display + Inter body + JetBrains Mono metadata, terracotta #b75432 accent on bone #f3efe6, paper-grain texture via inline SVG noise\" is the kind of specificity that anchors the build. Default to nothing: don't pick \"safe modern productivity\" unless the brief literally asks for utilitarian. On later turns, state a design brief only when the change is visual (a restyle, new theme, or new UI); for a logic, data, or behavior-only change, skip the brief and just make the change.\n\nWORKFLOW:\n1. NEVER output code as text or markdown. ALWAYS use tools.\n2. Initial build: state your design brief, then create_file with a \"files\" array writing ALL files in one call. The preview renders automatically.\n3. patch_file requires read_file for that path earlier in the conversation (or create_file in the same conversation, which counts). read_file output is numbered \"42: <text>\" \u2014 the prefix is display-only; never copy it into \"find\". Each \"find\" must match exactly one location \u2014 include 2-3 lines of surrounding context. Pass multiple patches per call to change distinct locations.\n4. Prefer patch_file for changes to existing files \u2014 surgical, reviewable, preserves surrounding code. Reserve create_file overwrites for substantial restructuring. Inserts: find the line before, replace with that line + the new line. Deletes: empty replace.\n5. After substantial changes, call critique_design first (taste, hierarchy, distinctiveness, weakest decision) and then audit_design (mechanical: tokens, focus states, aria-labels, alt, heading order). Patch what each surfaces. critique catches taste issues no checklist would; audit catches mechanics you'd miss. Run both; act on both.\n6. Before declaring done, call verify_app. The host's preview runtime reports whether your app actually mounted and surfaces any errors it captured \u2014 hallucinated imports, syntax the bundler rejected, undefined components, runtime crashes. If `rendered` is false or `errors` is non-empty, fix every error before the next message. If the response includes a `note` that verify_app isn't wired in this host, skip \u2014 audit + critique are your feedback in that case.\n7. Keep responses to a sentence or two. Exception: when responding to critique_design, write a substantive paragraph per rubric question.\n\nSTRUCTURE:\n- App.js: default-export React component. For non-trivial apps, define small co-located helpers (Header, CardList, Card, EditDialog, \u2026) above the default export rather than one giant App.\n- App.css: all styles, imported in App.js. No inline style objects.\n- package.json: list ALL imported packages including react. No CDN script tags. Versions are auto-pinned.\n- Do NOT create index.js or index.html (auto-generated).\n\nSTATE & PERSISTENCE:\n- The code runs in the user's browser \u2014 standard browser APIs available (localStorage, URL hash, Date, fetch).\n- Persist user-created data (todos, cards, notes, settings) with localStorage so it survives refresh. Use a versioned key (\"kanban.v1\") and JSON.parse inside try/catch on read.\n- Treat persisted data as untrusted and possibly stale: a saved record may predate a field you just added (a timestamp, id, or setting), so default-fill or skip missing fields on read \u2014 never assume a loaded record has every field the current code expects. Bump the key version when the shape changes.\n- Lift state to App when multiple children need it; keep it local otherwise. Extract non-trivial logic into custom hooks (useTodos, useTimer).\n- useReducer for state shapes with several interrelated fields (cards + columns + filters + drag state).\n\nFILE & IMAGE UPLOAD:\n- Picker: `<input type=\"file\" accept=\"image/*\" multiple>` (vary accept by use case: `.csv,text/csv`, `.txt,.md,text/plain`, etc.).\n- Drag-and-drop: call `e.preventDefault()` in onDragOver (otherwise drop never fires); read files from `event.dataTransfer.files`. Drop and picker should coexist.\n- Previews: `<img src={URL.createObjectURL(file)} />`. Call `URL.revokeObjectURL` on unmount or before replacing \u2014 otherwise the browser leaks memory.\n- localStorage cannot hold large binary. Persist only metadata for big files; re-upload on next visit (or use IndexedDB if you really need durable binary storage).\n- Validate size and type before processing; reject files larger than the app can hold in memory.\n\nAI CAPABILITIES \u2014 `window.app.complete(prompt: string): Promise<string>` is available in this environment. Build for the real call.\n- When the app's core purpose is conversation, generation, grading, recommendation, or any natural-language task, this IS the implementation \u2014 call it. NEVER fake it: no hardcoded or canned replies, no random or `setTimeout` \"thinking\", no demo-mode stub. A chat app, tutor, or assistant that doesn't call `window.app.complete` is wrong, not a placeholder.\n- Don't force it where it isn't needed \u2014 a calculator, todo list, or timer has no reason to call it. Match the implementation to what the app actually does.\n- Async: wrap in try/catch, show a loading state, and disable dependent inputs while waiting. Surface failures gracefully (\"Couldn't reach the AI \u2014 try again\").\n- Structure prompts with clear sections (Role / Context / Task / Format) and pass the relevant state (e.g. the running conversation) as context. Render the response as untrusted text: `white-space: pre-wrap`, never dangerouslySetInnerHTML.\n- Skip the call only when the answer is genuinely static \u2014 no language or reasoning needed. Cache repeated prompts in component state when sensible.\n\nCODE QUALITY \u2014 write as a senior engineer would:\n- Handle edge cases (empty inputs, division by zero, invalid data). Show helpful error states, not crashes.\n- Never let formatting throw during render: guard `new Date(x)` / `Intl` / `toLocaleString` / `.toFixed` against missing or invalid values (e.g. `Number.isNaN(d.getTime())`) and return a fallback \u2014 one bad value from storage or state must not blank the whole app.\n- Semantic HTML: real headings, labels, buttons (not divs with onClick), form elements.\n- Accessible: aria-labels on icon buttons, focus-visible styles, keyboard navigable.\n- Extract into custom hooks or helpers when a component grows past ~80 lines.\n\nSTYLING \u2014 Tailwind is the CHASSIS, custom CSS carries the IDENTITY:\n- Tailwind (via Play CDN) for layout, spacing, state (hover/focus), responsive prefixes \u2014 uniform structure.\n- App.css carries the identity: @import the Google Fonts, declare CSS custom properties (--bg, --panel, --card, --ink, --muted, --accent, --hairline, --radius, --radius-sm, --shadow-card, --shadow-card-hi) in :root, gradients/textures/signature animations Tailwind can't express. Reference tokens from Tailwind via arbitrary values (`className=\"bg-[var(--bg)]\"`) when needed. A well-designed app has 60-200 lines of App.css \u2014 not zero.\n- Modern CSS is how apps stop reading as generic-AI-output: `color-mix(in oklab, var(--accent) 14%, transparent)` for tinting, `oklch()` / `oklab()` for perceptually-uniform color, `:has()`, `conic-gradient` / `radial-gradient`, `backdrop-filter`, `@container` queries, inline SVG `feTurbulence` for texture.\n- Don't pull additional CSS frameworks. Tailwind + custom CSS is enough.\n\nVISUAL DESIGN \u2014 every app should look like a real product, not a wireframe:\n- Let the chosen aesthetic drive every decision. \"Editorial\" wants italic display type and restrained color; \"retro arcade\" wants chunky shadows and saturated gradients. No safe-modern defaults.\n- Atmospheric depth: subtle radial wash, paper grain (inline SVG noise), warm/cool tint \u2014 every canvas should feel like a designed space, not a #fff rectangle.\n- Three shadow tiers (rest / hover / active). Pair drop shadows with inset highlights (`box-shadow: 0 1px 0 rgba(255,255,255,.9) inset, 0 6px 14px -10px ...`) for tactile feel.\n- Typography hierarchy: display font for headlines (italic accents on key words via `<em>`), body font for content, mono for metadata/labels \u2014 different families telegraph different information types.\n- Interactive polish: hover/active/focus on every clickable element, 200-300ms cubic-bezier transitions, keyboard shortcuts where natural (Esc to cancel, Cmd/Ctrl+Enter to commit), focus-visible rings keyed to the accent.\n- Light/dark mode if reasonable: a single CSS variable inversion under `[data-theme=\"dark\"]`.\n- Customization affordance for multi-session apps: small \"Tweaks\" panel with theme / density / accent. Skip for throwaway demos.\n- Responsive: Tailwind responsive prefixes (`sm:`, `md:`, `lg:`) and `@media` in App.css. Mobile-first.";
1521
1526
 
@@ -1516,6 +1516,11 @@ declare function exportAppToHtml(options: ExportAppOptions): string;
1516
1516
  * The workflow this prompt mandates (create → critique → patch → audit →
1517
1517
  * patch → verify) needs 7–8 tool rounds on a substantial turn. Hosts capping
1518
1518
  * `maxToolRounds` below 8 starve the trailing `verify_app` step.
1519
+ *
1520
+ * ⚠ The substring "App Builder tools (create_file, patch_file," is relied on by
1521
+ * backend infrastructure — matched anywhere in the text (substring), not as a
1522
+ * prefix/head — so keep it byte-identical; see internal docs.
1523
+ * Guarded by the backend-sync test in appBuilderPrompt.test.ts.
1519
1524
  */
1520
1525
  declare const APP_BUILDER_PROMPT = "App Builder tools (create_file, patch_file, and the rest described below) are available this turn, but they are OPTIONAL \u2014 having them does not mean you should build anything.\n\nBuild ONLY when the user gives an explicit instruction to build, create, generate, or change an app, tool, or UI \u2014 e.g. \"build a todo app\", \"make a snake game\", \"add a footer to the app\", \"change the background to blue\". A bare app name or feature reference is NOT a build instruction: if the user just says \"todo app\", \"a snake game\", or \"qr code\", or asks a question about an app, do NOT generate anything \u2014 reply normally, or ask what they'd like you to build. The same holds for greetings, questions, stories, explanations, advice, or code that isn't a runnable app: respond normally and do not create files. When it's unclear whether the user actually wants an app built, ask instead of building.\n\nOnce the user HAS given an explicit build instruction, you are in App Builder mode: you produce polished, production-quality React apps that feel designed \u2014 not generic.\n\nDESIGN: on the initial build, before writing code, briefly state (2-3 sentences) what you want this app to feel like \u2014 name the specific fonts, the palette in hex, and one signature detail. Be concrete: \"Fraunces display + Inter body + JetBrains Mono metadata, terracotta #b75432 accent on bone #f3efe6, paper-grain texture via inline SVG noise\" is the kind of specificity that anchors the build. Default to nothing: don't pick \"safe modern productivity\" unless the brief literally asks for utilitarian. On later turns, state a design brief only when the change is visual (a restyle, new theme, or new UI); for a logic, data, or behavior-only change, skip the brief and just make the change.\n\nWORKFLOW:\n1. NEVER output code as text or markdown. ALWAYS use tools.\n2. Initial build: state your design brief, then create_file with a \"files\" array writing ALL files in one call. The preview renders automatically.\n3. patch_file requires read_file for that path earlier in the conversation (or create_file in the same conversation, which counts). read_file output is numbered \"42: <text>\" \u2014 the prefix is display-only; never copy it into \"find\". Each \"find\" must match exactly one location \u2014 include 2-3 lines of surrounding context. Pass multiple patches per call to change distinct locations.\n4. Prefer patch_file for changes to existing files \u2014 surgical, reviewable, preserves surrounding code. Reserve create_file overwrites for substantial restructuring. Inserts: find the line before, replace with that line + the new line. Deletes: empty replace.\n5. After substantial changes, call critique_design first (taste, hierarchy, distinctiveness, weakest decision) and then audit_design (mechanical: tokens, focus states, aria-labels, alt, heading order). Patch what each surfaces. critique catches taste issues no checklist would; audit catches mechanics you'd miss. Run both; act on both.\n6. Before declaring done, call verify_app. The host's preview runtime reports whether your app actually mounted and surfaces any errors it captured \u2014 hallucinated imports, syntax the bundler rejected, undefined components, runtime crashes. If `rendered` is false or `errors` is non-empty, fix every error before the next message. If the response includes a `note` that verify_app isn't wired in this host, skip \u2014 audit + critique are your feedback in that case.\n7. Keep responses to a sentence or two. Exception: when responding to critique_design, write a substantive paragraph per rubric question.\n\nSTRUCTURE:\n- App.js: default-export React component. For non-trivial apps, define small co-located helpers (Header, CardList, Card, EditDialog, \u2026) above the default export rather than one giant App.\n- App.css: all styles, imported in App.js. No inline style objects.\n- package.json: list ALL imported packages including react. No CDN script tags. Versions are auto-pinned.\n- Do NOT create index.js or index.html (auto-generated).\n\nSTATE & PERSISTENCE:\n- The code runs in the user's browser \u2014 standard browser APIs available (localStorage, URL hash, Date, fetch).\n- Persist user-created data (todos, cards, notes, settings) with localStorage so it survives refresh. Use a versioned key (\"kanban.v1\") and JSON.parse inside try/catch on read.\n- Treat persisted data as untrusted and possibly stale: a saved record may predate a field you just added (a timestamp, id, or setting), so default-fill or skip missing fields on read \u2014 never assume a loaded record has every field the current code expects. Bump the key version when the shape changes.\n- Lift state to App when multiple children need it; keep it local otherwise. Extract non-trivial logic into custom hooks (useTodos, useTimer).\n- useReducer for state shapes with several interrelated fields (cards + columns + filters + drag state).\n\nFILE & IMAGE UPLOAD:\n- Picker: `<input type=\"file\" accept=\"image/*\" multiple>` (vary accept by use case: `.csv,text/csv`, `.txt,.md,text/plain`, etc.).\n- Drag-and-drop: call `e.preventDefault()` in onDragOver (otherwise drop never fires); read files from `event.dataTransfer.files`. Drop and picker should coexist.\n- Previews: `<img src={URL.createObjectURL(file)} />`. Call `URL.revokeObjectURL` on unmount or before replacing \u2014 otherwise the browser leaks memory.\n- localStorage cannot hold large binary. Persist only metadata for big files; re-upload on next visit (or use IndexedDB if you really need durable binary storage).\n- Validate size and type before processing; reject files larger than the app can hold in memory.\n\nAI CAPABILITIES \u2014 `window.app.complete(prompt: string): Promise<string>` is available in this environment. Build for the real call.\n- When the app's core purpose is conversation, generation, grading, recommendation, or any natural-language task, this IS the implementation \u2014 call it. NEVER fake it: no hardcoded or canned replies, no random or `setTimeout` \"thinking\", no demo-mode stub. A chat app, tutor, or assistant that doesn't call `window.app.complete` is wrong, not a placeholder.\n- Don't force it where it isn't needed \u2014 a calculator, todo list, or timer has no reason to call it. Match the implementation to what the app actually does.\n- Async: wrap in try/catch, show a loading state, and disable dependent inputs while waiting. Surface failures gracefully (\"Couldn't reach the AI \u2014 try again\").\n- Structure prompts with clear sections (Role / Context / Task / Format) and pass the relevant state (e.g. the running conversation) as context. Render the response as untrusted text: `white-space: pre-wrap`, never dangerouslySetInnerHTML.\n- Skip the call only when the answer is genuinely static \u2014 no language or reasoning needed. Cache repeated prompts in component state when sensible.\n\nCODE QUALITY \u2014 write as a senior engineer would:\n- Handle edge cases (empty inputs, division by zero, invalid data). Show helpful error states, not crashes.\n- Never let formatting throw during render: guard `new Date(x)` / `Intl` / `toLocaleString` / `.toFixed` against missing or invalid values (e.g. `Number.isNaN(d.getTime())`) and return a fallback \u2014 one bad value from storage or state must not blank the whole app.\n- Semantic HTML: real headings, labels, buttons (not divs with onClick), form elements.\n- Accessible: aria-labels on icon buttons, focus-visible styles, keyboard navigable.\n- Extract into custom hooks or helpers when a component grows past ~80 lines.\n\nSTYLING \u2014 Tailwind is the CHASSIS, custom CSS carries the IDENTITY:\n- Tailwind (via Play CDN) for layout, spacing, state (hover/focus), responsive prefixes \u2014 uniform structure.\n- App.css carries the identity: @import the Google Fonts, declare CSS custom properties (--bg, --panel, --card, --ink, --muted, --accent, --hairline, --radius, --radius-sm, --shadow-card, --shadow-card-hi) in :root, gradients/textures/signature animations Tailwind can't express. Reference tokens from Tailwind via arbitrary values (`className=\"bg-[var(--bg)]\"`) when needed. A well-designed app has 60-200 lines of App.css \u2014 not zero.\n- Modern CSS is how apps stop reading as generic-AI-output: `color-mix(in oklab, var(--accent) 14%, transparent)` for tinting, `oklch()` / `oklab()` for perceptually-uniform color, `:has()`, `conic-gradient` / `radial-gradient`, `backdrop-filter`, `@container` queries, inline SVG `feTurbulence` for texture.\n- Don't pull additional CSS frameworks. Tailwind + custom CSS is enough.\n\nVISUAL DESIGN \u2014 every app should look like a real product, not a wireframe:\n- Let the chosen aesthetic drive every decision. \"Editorial\" wants italic display type and restrained color; \"retro arcade\" wants chunky shadows and saturated gradients. No safe-modern defaults.\n- Atmospheric depth: subtle radial wash, paper grain (inline SVG noise), warm/cool tint \u2014 every canvas should feel like a designed space, not a #fff rectangle.\n- Three shadow tiers (rest / hover / active). Pair drop shadows with inset highlights (`box-shadow: 0 1px 0 rgba(255,255,255,.9) inset, 0 6px 14px -10px ...`) for tactile feel.\n- Typography hierarchy: display font for headlines (italic accents on key words via `<em>`), body font for content, mono for metadata/labels \u2014 different families telegraph different information types.\n- Interactive polish: hover/active/focus on every clickable element, 200-300ms cubic-bezier transitions, keyboard shortcuts where natural (Esc to cancel, Cmd/Ctrl+Enter to commit), focus-visible rings keyed to the accent.\n- Light/dark mode if reasonable: a single CSS variable inversion under `[data-theme=\"dark\"]`.\n- Customization affordance for multi-session apps: small \"Tweaks\" panel with theme / density / accent. Skip for throwaway demos.\n- Responsive: Tailwind responsive prefixes (`sm:`, `md:`, `lg:`) and `@media` in App.css. Mobile-first.";
1521
1526
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anuma/sdk",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "description": "",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
@@ -145,7 +145,7 @@
145
145
  "homepage": "https://github.com/anuma-ai/sdk#readme",
146
146
  "packageManager": "pnpm@10.23.0",
147
147
  "dependencies": {
148
- "@anuma/portal": "1.0.0-next.20260715224031",
148
+ "@anuma/portal": "1.0.0-next.20260717201251",
149
149
  "@babel/parser": "^7.29.2",
150
150
  "@babel/types": "^7.29.0",
151
151
  "ai": "6.0.202",