@anuma/sdk 2.4.1 → 2.5.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.
@@ -26,6 +26,7 @@ import {
26
26
  createMemoryVaultSearchTool,
27
27
  createMessageOp,
28
28
  createSseClient,
29
+ createSupersedingMemoryOp,
29
30
  createVaultEmbeddingCache,
30
31
  createVaultMemoriesBatchOp,
31
32
  createVaultMemoryOp,
@@ -116,6 +117,7 @@ import {
116
117
  shouldChunkMessage,
117
118
  stampTopicsExtractedAtOp,
118
119
  stripToolCalls,
120
+ supersedeVaultMemoryOp,
119
121
  updateConversationPinnedOp,
120
122
  updateConversationProjectOp,
121
123
  updateConversationTitleOp,
@@ -128,7 +130,7 @@ import {
128
130
  updateVaultMemoryOp,
129
131
  useEncryption,
130
132
  vaultMemoryToStored
131
- } from "./chunk-IYDQBFS2.mjs";
133
+ } from "./chunk-V5GC3ES7.mjs";
132
134
  import {
133
135
  PiiRedactor,
134
136
  createStreamingDeAnonymizer,
@@ -6669,6 +6671,12 @@ __decorateClass([
6669
6671
  __decorateClass([
6670
6672
  field3("topics_extracted_at")
6671
6673
  ], VaultMemory.prototype, "topicsExtractedAt", 2);
6674
+ __decorateClass([
6675
+ field3("superseded_by")
6676
+ ], VaultMemory.prototype, "supersededBy", 2);
6677
+ __decorateClass([
6678
+ field3("superseded_at")
6679
+ ], VaultMemory.prototype, "supersededAt", 2);
6672
6680
  __decorateClass([
6673
6681
  readonly2,
6674
6682
  date3("created_at")
@@ -6696,53 +6704,62 @@ async function retain(content, ctx, options = {}) {
6696
6704
  const enableAutoMerge = options.enableAutoMerge ?? true;
6697
6705
  const threshold = options.autoMergeThreshold ?? DEFAULT_AUTO_MERGE_THRESHOLD;
6698
6706
  const resolvedScope = options.scope ?? DEFAULT_SCOPE;
6707
+ let supersedeTargetId;
6708
+ let supersedeContent;
6699
6709
  if (enableAutoMerge) {
6700
6710
  if (options.consolidateOptions) {
6701
- const consolidationResult = await tryConsolidate(trimmed, ctx, options);
6702
- if (consolidationResult) return consolidationResult;
6703
- }
6704
- const matches = await searchVaultMemories(
6705
- trimmed,
6706
- ctx.vaultCtx,
6707
- ctx.embeddingOptions,
6708
- ctx.vaultCache,
6709
- {
6710
- limit: 1,
6711
- minSimilarity: threshold,
6712
- useFusion: false,
6713
- scopes: [resolvedScope],
6714
- ...options.folderId !== void 0 && { folderId: options.folderId }
6715
- }
6716
- );
6717
- if (matches.length > 0) {
6718
- const targetId = matches[0].uniqueId;
6719
- const existing = await getVaultMemoryOp(ctx.vaultCtx, targetId);
6720
- if (existing) {
6721
- const mergedSourceIds = unionStrings(
6722
- existing.sourceChunkIds ?? [],
6723
- options.sourceChunkIds ?? []
6724
- );
6725
- const eventTimeUpdate = pickEventTimeUpdate(existing, options.eventTime);
6726
- const updated = await updateVaultMemoryOp(ctx.vaultCtx, targetId, {
6727
- content: existing.content,
6728
- proofCountIncrement: 1,
6729
- sourceChunkIds: mergedSourceIds,
6730
- preserveUpdatedAt: true,
6731
- ...eventTimeUpdate && { eventTime: eventTimeUpdate }
6732
- });
6733
- if (updated) {
6734
- return {
6735
- action: "merge",
6736
- memoryId: targetId,
6737
- targetId,
6738
- proofCount: updated.proofCount ?? (existing.proofCount ?? 1) + 1
6739
- };
6711
+ const outcome = await tryConsolidate(trimmed, ctx, options);
6712
+ if (outcome) {
6713
+ if ("done" in outcome) return outcome.done;
6714
+ supersedeTargetId = outcome.supersede;
6715
+ supersedeContent = outcome.content;
6716
+ }
6717
+ }
6718
+ if (!supersedeTargetId) {
6719
+ const matches = await searchVaultMemories(
6720
+ trimmed,
6721
+ ctx.vaultCtx,
6722
+ ctx.embeddingOptions,
6723
+ ctx.vaultCache,
6724
+ {
6725
+ limit: 1,
6726
+ minSimilarity: threshold,
6727
+ useFusion: false,
6728
+ scopes: [resolvedScope],
6729
+ ...options.folderId !== void 0 && { folderId: options.folderId }
6730
+ }
6731
+ );
6732
+ if (matches.length > 0) {
6733
+ const targetId = matches[0].uniqueId;
6734
+ const existing = await getVaultMemoryOp(ctx.vaultCtx, targetId);
6735
+ if (existing && !existing.supersededBy) {
6736
+ const mergedSourceIds = unionStrings(
6737
+ existing.sourceChunkIds ?? [],
6738
+ options.sourceChunkIds ?? []
6739
+ );
6740
+ const eventTimeUpdate = pickEventTimeUpdate(existing, options.eventTime);
6741
+ const updated = await updateVaultMemoryOp(ctx.vaultCtx, targetId, {
6742
+ content: existing.content,
6743
+ proofCountIncrement: 1,
6744
+ sourceChunkIds: mergedSourceIds,
6745
+ preserveUpdatedAt: true,
6746
+ ...eventTimeUpdate && { eventTime: eventTimeUpdate }
6747
+ });
6748
+ if (updated) {
6749
+ return {
6750
+ action: "merge",
6751
+ memoryId: targetId,
6752
+ targetId,
6753
+ proofCount: updated.proofCount ?? (existing.proofCount ?? 1) + 1
6754
+ };
6755
+ }
6756
+ await assertMergeTargetGoneOrThrow(ctx, targetId);
6740
6757
  }
6741
- await assertMergeTargetGoneOrThrow(ctx, targetId);
6742
6758
  }
6743
6759
  }
6744
6760
  }
6745
- const embedding = await generateEmbedding(trimmed, ctx.embeddingOptions);
6761
+ const contentToWrite = supersedeContent ?? trimmed;
6762
+ const embedding = await generateEmbedding(contentToWrite, ctx.embeddingOptions);
6746
6763
  const embeddingModel = ctx.embeddingOptions.model ?? DEFAULT_API_EMBEDDING_MODEL;
6747
6764
  if (options.respectTombstones) {
6748
6765
  const tombstoneId = await findTombstoneMatch(embedding, embeddingModel, resolvedScope, ctx, {
@@ -6753,8 +6770,8 @@ async function retain(content, ctx, options = {}) {
6753
6770
  return { action: "suppressed", memoryId: tombstoneId, tombstoneId, proofCount: 0 };
6754
6771
  }
6755
6772
  }
6756
- const created = await createVaultMemoryOp(ctx.vaultCtx, {
6757
- content: trimmed,
6773
+ const createOpts = {
6774
+ content: contentToWrite,
6758
6775
  scope: resolvedScope,
6759
6776
  ...options.folderId !== void 0 && { folderId: options.folderId },
6760
6777
  embedding: JSON.stringify(embedding),
@@ -6771,7 +6788,24 @@ async function retain(content, ctx, options = {}) {
6771
6788
  kind: options.eventTime.kind
6772
6789
  }
6773
6790
  }
6774
- });
6791
+ };
6792
+ if (supersedeTargetId) {
6793
+ const { created: created2, retired } = await createSupersedingMemoryOp(
6794
+ ctx.vaultCtx,
6795
+ createOpts,
6796
+ supersedeTargetId
6797
+ );
6798
+ if (created2 && retired) {
6799
+ ctx.vaultCache.set(created2.uniqueId, Float32Array.from(embedding));
6800
+ return {
6801
+ action: "supersede",
6802
+ memoryId: created2.uniqueId,
6803
+ targetId: supersedeTargetId,
6804
+ proofCount: 1
6805
+ };
6806
+ }
6807
+ }
6808
+ const created = await createVaultMemoryOp(ctx.vaultCtx, createOpts);
6775
6809
  ctx.vaultCache.set(created.uniqueId, Float32Array.from(embedding));
6776
6810
  return {
6777
6811
  action: "create",
@@ -6838,12 +6872,17 @@ async function tryConsolidate(trimmed, ctx, options) {
6838
6872
  content: m.content,
6839
6873
  similarity: m.similarity
6840
6874
  }));
6841
- const { consolidateMemory: doConsolidate } = await import("./consolidate-IJH7LD2T.mjs");
6875
+ const { consolidateMemory: doConsolidate } = await import("./consolidate-3MXZV3G7.mjs");
6842
6876
  const decision = await doConsolidate(trimmed, candidates, consolidateOptions);
6843
6877
  if (decision.action === "create") return null;
6878
+ if (decision.action === "supersede" && decision.targetId && decision.content) {
6879
+ const existing = await getVaultMemoryOp(ctx.vaultCtx, decision.targetId);
6880
+ if (!existing || existing.supersededBy) return null;
6881
+ return { supersede: decision.targetId, content: decision.content };
6882
+ }
6844
6883
  if (decision.action === "noop" && decision.targetId) {
6845
6884
  const existing = await getVaultMemoryOp(ctx.vaultCtx, decision.targetId);
6846
- if (!existing) return null;
6885
+ if (!existing || existing.supersededBy) return null;
6847
6886
  const mergedSourceIds = unionStrings(
6848
6887
  existing.sourceChunkIds ?? [],
6849
6888
  options.sourceChunkIds ?? []
@@ -6861,15 +6900,17 @@ async function tryConsolidate(trimmed, ctx, options) {
6861
6900
  return null;
6862
6901
  }
6863
6902
  return {
6864
- action: "merge",
6865
- memoryId: decision.targetId,
6866
- targetId: decision.targetId,
6867
- proofCount: updated.proofCount ?? (existing.proofCount ?? 1) + 1
6903
+ done: {
6904
+ action: "merge",
6905
+ memoryId: decision.targetId,
6906
+ targetId: decision.targetId,
6907
+ proofCount: updated.proofCount ?? (existing.proofCount ?? 1) + 1
6908
+ }
6868
6909
  };
6869
6910
  }
6870
6911
  if (decision.action === "update" && decision.targetId && decision.content) {
6871
6912
  const existing = await getVaultMemoryOp(ctx.vaultCtx, decision.targetId);
6872
- if (!existing) return null;
6913
+ if (!existing || existing.supersededBy) return null;
6873
6914
  const mergedSourceIds = unionStrings(
6874
6915
  existing.sourceChunkIds ?? [],
6875
6916
  options.sourceChunkIds ?? []
@@ -6896,10 +6937,12 @@ async function tryConsolidate(trimmed, ctx, options) {
6896
6937
  }
6897
6938
  ctx.vaultCache.set(decision.targetId, Float32Array.from(newEmbedding));
6898
6939
  return {
6899
- action: "update",
6900
- memoryId: decision.targetId,
6901
- targetId: decision.targetId,
6902
- proofCount: updated.proofCount ?? (existing.proofCount ?? 1) + 1
6940
+ done: {
6941
+ action: "update",
6942
+ memoryId: decision.targetId,
6943
+ targetId: decision.targetId,
6944
+ proofCount: updated.proofCount ?? (existing.proofCount ?? 1) + 1
6945
+ }
6903
6946
  };
6904
6947
  }
6905
6948
  return null;
@@ -12187,7 +12230,7 @@ __decorateClass([
12187
12230
  ], VaultFolder.prototype, "context", 2);
12188
12231
 
12189
12232
  // src/lib/db/schema.ts
12190
- var SDK_SCHEMA_VERSION = 36;
12233
+ var SDK_SCHEMA_VERSION = 37;
12191
12234
  var sdkSchema = appSchema2({
12192
12235
  version: SDK_SCHEMA_VERSION,
12193
12236
  tables: [
@@ -12312,7 +12355,16 @@ var sdkSchema = appSchema2({
12312
12355
  // Unix ms of the last LLM topic-extraction pass over this memory's
12313
12356
  // content. Null = never extracted standalone (legacy rows with entity
12314
12357
  // links are grandfathered — see getMemoriesNeedingTopicExtractionOp).
12315
- { name: "topics_extracted_at", type: "number", isOptional: true }
12358
+ { name: "topics_extracted_at", type: "number", isOptional: true },
12359
+ // Write-time supersession (A2). When set, this fact was replaced by a
12360
+ // newer, incompatible-value fact (e.g. "Lives in Portland" superseded by
12361
+ // "Lives in SF"); `superseded_by` points at the newer memory's id and
12362
+ // `superseded_at` is the Unix ms it happened. Superseded rows stay in
12363
+ // the table (history / read-time fallback) but are excluded from recall
12364
+ // and dedup by default. Null = live (not superseded). Indexed so the
12365
+ // recall filter stays cheap.
12366
+ { name: "superseded_by", type: "string", isOptional: true, isIndexed: true },
12367
+ { name: "superseded_at", type: "number", isOptional: true }
12316
12368
  ]
12317
12369
  }),
12318
12370
  // Entity table — canonical names extracted from auto-extraction (W5).
@@ -12923,6 +12975,21 @@ var sdkMigrations = schemaMigrations2({
12923
12975
  columns: [{ name: "topics_extracted_at", type: "number", isOptional: true }]
12924
12976
  })
12925
12977
  ]
12978
+ },
12979
+ // v36 -> v37: write-time supersession columns on memory_vault. Existing rows
12980
+ // keep both NULL (= live / not superseded), so no backfill — legacy
12981
+ // contradictory rows are handled by the read-time supersession fallback.
12982
+ {
12983
+ toVersion: 37,
12984
+ steps: [
12985
+ addColumns2({
12986
+ table: "memory_vault",
12987
+ columns: [
12988
+ { name: "superseded_by", type: "string", isOptional: true, isIndexed: true },
12989
+ { name: "superseded_at", type: "number", isOptional: true }
12990
+ ]
12991
+ })
12992
+ ]
12926
12993
  }
12927
12994
  ]
12928
12995
  });
@@ -22854,6 +22921,7 @@ export {
22854
22921
  storeNotionReturnUrl,
22855
22922
  streamCancelPath,
22856
22923
  streamReplayPath,
22924
+ supersedeVaultMemoryOp,
22857
22925
  updateAttrs,
22858
22926
  updateConversationProjectOp,
22859
22927
  updateMediaMessageIdBatchOp,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  recall
3
- } from "./chunk-UOUJUZM4.mjs";
3
+ } from "./chunk-V5GC3ES7.mjs";
4
4
  import "./chunk-DFVCS3QQ.mjs";
5
5
  import "./chunk-GYKJOWEO.mjs";
6
6
  export {
@@ -16057,6 +16057,7 @@ function isOwnedByCtxUser(ctx, record) {
16057
16057
  function baseVaultConditions(ctx, options) {
16058
16058
  return [
16059
16059
  ...options?.includeDeleted ? [] : [import_watermelondb3.Q.where("is_deleted", false)],
16060
+ ...options?.includeSuperseded ? [] : [import_watermelondb3.Q.where("superseded_by", null)],
16060
16061
  ...ctx.userId !== void 0 ? [import_watermelondb3.Q.where("user_id", ctx.userId)] : [],
16061
16062
  ...options?.since ? [import_watermelondb3.Q.where("updated_at", import_watermelondb3.Q.gt(options.since.getTime()))] : []
16062
16063
  ];
@@ -16097,6 +16098,8 @@ function vaultMemoryToStoredRaw(memory) {
16097
16098
  eventTimeKind: memory.eventTimeKind ?? null,
16098
16099
  topicsUserManaged: memory.topicsUserManaged ?? false,
16099
16100
  topicsExtractedAt: memory.topicsExtractedAt ?? null,
16101
+ supersededBy: memory.supersededBy ?? null,
16102
+ supersededAt: memory.supersededAt ?? null,
16100
16103
  createdAt: memory.createdAt,
16101
16104
  updatedAt: memory.updatedAt,
16102
16105
  isDeleted: memory.isDeleted
@@ -16142,6 +16145,59 @@ async function createVaultMemoryOp(ctx, opts) {
16142
16145
  });
16143
16146
  return vaultMemoryToStored(created, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner);
16144
16147
  }
16148
+ async function createSupersedingMemoryOp(ctx, opts, targetId) {
16149
+ if (!targetId) return { created: null, retired: false };
16150
+ const scope = opts.scope ?? "private";
16151
+ const encryptedContent = ctx.walletAddress && ctx.signMessage ? await encryptVaultMemoryContent(
16152
+ opts.content,
16153
+ ctx.walletAddress,
16154
+ ctx.signMessage,
16155
+ ctx.embeddedWalletSigner
16156
+ ) : opts.content;
16157
+ let createdRecord = null;
16158
+ await ctx.database.write(async () => {
16159
+ let target;
16160
+ try {
16161
+ target = await ctx.vaultMemoryCollection.find(targetId);
16162
+ } catch {
16163
+ return;
16164
+ }
16165
+ if (target.isDeleted || target.supersededBy || !isOwnedByCtxUser(ctx, target)) return;
16166
+ createdRecord = await ctx.vaultMemoryCollection.create((record) => {
16167
+ record._setRaw("content", encryptedContent);
16168
+ record._setRaw("scope", scope);
16169
+ record._setRaw("folder_id", opts.folderId ?? null);
16170
+ record._setRaw("user_id", ctx.userId ?? null);
16171
+ record._setRaw("is_deleted", false);
16172
+ if (opts.embedding !== void 0) {
16173
+ record._setRaw("embedding", opts.embedding);
16174
+ record._setRaw("embedding_model", opts.embeddingModel ?? null);
16175
+ }
16176
+ if (opts.sourceChunkIds !== void 0) {
16177
+ record._setRaw("source_chunk_ids", JSON.stringify(opts.sourceChunkIds));
16178
+ }
16179
+ record._setRaw("proof_count", opts.proofCount ?? 1);
16180
+ record._setRaw("source", opts.source ?? "manual");
16181
+ if (opts.eventTime) {
16182
+ record._setRaw("event_time_start", opts.eventTime.start ?? null);
16183
+ record._setRaw("event_time_end", opts.eventTime.end ?? null);
16184
+ record._setRaw("event_time_kind", opts.eventTime.kind ?? null);
16185
+ }
16186
+ });
16187
+ await target.update((r) => {
16188
+ r._setRaw("superseded_by", createdRecord.id);
16189
+ r._setRaw("superseded_at", Date.now());
16190
+ });
16191
+ });
16192
+ if (!createdRecord) return { created: null, retired: false };
16193
+ const created = await vaultMemoryToStored(
16194
+ createdRecord,
16195
+ ctx.walletAddress,
16196
+ ctx.signMessage,
16197
+ ctx.embeddedWalletSigner
16198
+ );
16199
+ return { created, retired: true };
16200
+ }
16145
16201
  async function getMemoriesByEventTimeOp(ctx, windowStart, windowEnd) {
16146
16202
  const records = await ctx.vaultMemoryCollection.query(
16147
16203
  ...baseVaultConditions(ctx),
@@ -16281,6 +16337,8 @@ function vaultMemoryRawToStoredRaw(raw) {
16281
16337
  // SQLite stores booleans as 0/1, LokiJS as true/false — coerce both.
16282
16338
  topicsUserManaged: raw.topics_user_managed === true || raw.topics_user_managed === 1,
16283
16339
  topicsExtractedAt: raw.topics_extracted_at ?? null,
16340
+ supersededBy: raw.superseded_by ?? null,
16341
+ supersededAt: raw.superseded_at ?? null,
16284
16342
  createdAt: new Date(raw.created_at),
16285
16343
  updatedAt: new Date(raw.updated_at),
16286
16344
  isDeleted: raw.is_deleted === true || raw.is_deleted === 1
@@ -16322,7 +16380,7 @@ async function getAllVaultMemoryContentsOp(ctx, options) {
16322
16380
  async function updateVaultMemoryOp(ctx, id, opts) {
16323
16381
  try {
16324
16382
  const probe = await ctx.vaultMemoryCollection.find(id);
16325
- if (probe.isDeleted || !isOwnedByCtxUser(ctx, probe)) return null;
16383
+ if (probe.isDeleted || probe.supersededBy || !isOwnedByCtxUser(ctx, probe)) return null;
16326
16384
  const encryptedContent = ctx.walletAddress && ctx.signMessage ? await encryptVaultMemoryContent(
16327
16385
  opts.content,
16328
16386
  ctx.walletAddress,
@@ -16333,7 +16391,7 @@ async function updateVaultMemoryOp(ctx, id, opts) {
16333
16391
  const record = probe;
16334
16392
  const originalUpdatedAt = record.updatedAt.getTime();
16335
16393
  await ctx.database.write(async () => {
16336
- if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) {
16394
+ if (record.isDeleted || record.supersededBy || !isOwnedByCtxUser(ctx, record)) {
16337
16395
  stale = true;
16338
16396
  return;
16339
16397
  }
@@ -16411,6 +16469,40 @@ async function deleteVaultMemoryOp(ctx, id) {
16411
16469
  return false;
16412
16470
  }
16413
16471
  }
16472
+ async function supersedeVaultMemoryOp(ctx, id, supersededById) {
16473
+ if (id === supersededById) return false;
16474
+ try {
16475
+ const record = await ctx.vaultMemoryCollection.find(id);
16476
+ if (record.isDeleted || record.supersededBy || !isOwnedByCtxUser(ctx, record)) return false;
16477
+ let successor;
16478
+ try {
16479
+ successor = await ctx.vaultMemoryCollection.find(supersededById);
16480
+ } catch {
16481
+ return false;
16482
+ }
16483
+ if (successor.isDeleted || successor.supersededBy || !isOwnedByCtxUser(ctx, successor)) {
16484
+ return false;
16485
+ }
16486
+ let stale = false;
16487
+ await ctx.database.write(async () => {
16488
+ if (record.isDeleted || record.supersededBy || !isOwnedByCtxUser(ctx, record)) {
16489
+ stale = true;
16490
+ return;
16491
+ }
16492
+ if (successor.isDeleted || successor.supersededBy || !isOwnedByCtxUser(ctx, successor)) {
16493
+ stale = true;
16494
+ return;
16495
+ }
16496
+ await record.update((r) => {
16497
+ r._setRaw("superseded_by", supersededById);
16498
+ r._setRaw("superseded_at", Date.now());
16499
+ });
16500
+ });
16501
+ return !stale;
16502
+ } catch {
16503
+ return false;
16504
+ }
16505
+ }
16414
16506
  async function getUnfiledVaultMemoriesOp(ctx) {
16415
16507
  const conditions = [
16416
16508
  import_watermelondb3.Q.where("folder_id", null),
@@ -19108,7 +19200,7 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
19108
19200
  legacy.map((r) => ({ id: r.uniqueId, content: r.content, score: r.similarity }))
19109
19201
  );
19110
19202
  }
19111
- const { recall: recall2 } = await import("./recall-MJHGXMHZ.mjs");
19203
+ const { recall: recall2 } = await import("./recall-44FIK2MJ.mjs");
19112
19204
  const result = await recall2(
19113
19205
  query,
19114
19206
  { vaultCtx, embeddingOptions, vaultCache: cache },
@@ -19797,12 +19889,14 @@ export {
19797
19889
  replaceMemoryEntitiesGuardedOp,
19798
19890
  vaultMemoryToStored,
19799
19891
  createVaultMemoryOp,
19892
+ createSupersedingMemoryOp,
19800
19893
  createVaultMemoriesBatchOp,
19801
19894
  getVaultMemoryOp,
19802
19895
  getAllVaultMemoriesOp,
19803
19896
  getAllVaultMemoryContentsOp,
19804
19897
  updateVaultMemoryOp,
19805
19898
  deleteVaultMemoryOp,
19899
+ supersedeVaultMemoryOp,
19806
19900
  getUnfiledVaultMemoriesOp,
19807
19901
  getMemoriesNeedingTopicExtractionOp,
19808
19902
  stampTopicsExtractedAtOp,
@@ -15,10 +15,11 @@ var SYSTEM_PROMPT = `You consolidate a new memory against existing memories from
15
15
 
16
16
  A "memory" is a self-contained natural-language fact about the user. Multiple memories about the same EXACT FACET (the same dimension of the same subject) should never coexist \u2014 they should be merged into one. Different facets on the same subject (e.g. user's dog's name vs user's dog's age) are SEPARATE memories.
17
17
 
18
- Decide one of three actions:
18
+ Decide one of four actions:
19
19
 
20
20
  - "create": the new memory describes a distinct fact not covered by any existing memory. Write it as new.
21
- - "update": the new memory describes the SAME FACET as exactly one existing memory. Either it adds detail, corrects a value, or rephrases the same fact. Return targetId of that existing memory + the consolidated content (richest version of the two).
21
+ - "update": the new memory describes the SAME FACET as exactly one existing memory and is COMPATIBLE with it \u2014 it adds detail, fixes an incomplete value, or rephrases the same still-true fact. Return targetId of that existing memory + the consolidated content (richest version of the two).
22
+ - "supersede": the new memory REPLACES a standing fact whose value has CHANGED and is now INCOMPATIBLE with an existing memory \u2014 the old fact was true before but is false now (a state change). Return targetId of the STALE existing memory to retire + content = the NEW fact. The old memory is kept as history, not overwritten or deleted; do not re-state it.
22
23
  - "noop": the new memory is already fully captured by an existing memory \u2014 same facet, no new information. Skip the write.
23
24
 
24
25
  RULES (carry these strictly):
@@ -26,18 +27,21 @@ RULES (carry these strictly):
26
27
  1. ONE OBSERVATION PER DISTINCT FACET. If the new memory is about "user's aunt's twins" and an existing memory is also about "user's aunt's twins", they are the same facet \u2192 update, never create.
27
28
  2. MATCH BY FACET, NOT TOPIC. Two memories both mentioning "Zara" is not enough to merge \u2014 only merge if they describe the same property (e.g. both about "user's pending Zara boot exchange"). Two memories about Zara on different topics (e.g. "user shops at Zara" + "user returned a sweater to Zara last week") are separate facets.
28
29
  3. NO COMPUTATION. Do not sum counts, decrement quantities, or derive new facts from existing ones. If the new memory says "user spent $200 today" and an existing memory says "user spent $150 yesterday", these are TWO SEPARATE EVENTS \u2014 create.
29
- 4. Events with different occurredAt dates are different memories even if the same activity ("user went to gym Monday" vs "user went to gym Friday" \u2014 separate events).
30
- 5. When in doubt between create and update, choose create. Over-merging loses information; under-merging is recoverable.
30
+ 4. EVENTS vs STANDING STATE.
31
+ - Distinct EVENTS (episodic \u2014 something that happened on a date) are separate memories, even if the same activity: "user went to gym Monday" vs "user went to gym Friday" \u2192 create; keep both.
32
+ - A STANDING ATTRIBUTE (an ongoing state: where the user lives/works, relationship status, current role) has ONE current value. When the new memory changes that value ("Lives in Portland" \u2192 "Lives in San Francisco"; "Works at Google" \u2192 "Works at Riverbend"; "Engaged" \u2192 "Broke up"), the old value is no longer true \u2192 supersede (retire the old, record the new). Do NOT create (that leaves a stale contradiction) and do NOT update (that erases the history that the old value was once true).
33
+ 5. When in doubt between create and update, choose create \u2014 EXCEPT when the new memory directly contradicts the current value of a standing attribute in an existing memory; then choose supersede. Over-merging loses information; a live contradiction is worse than either.
31
34
 
32
35
  OUTPUT \u2014 strict JSON, no prose:
33
36
  {
34
- "action": "create" | "update" | "noop",
35
- "targetId": "<existing memory id, required for update/noop, omit for create>",
36
- "content": "<consolidated content, required for create/update, omit for noop>"
37
+ "action": "create" | "update" | "supersede" | "noop",
38
+ "targetId": "<existing memory id \u2014 required for update/supersede/noop, omit for create>",
39
+ "content": "<content \u2014 required for create/update/supersede, omit for noop>"
37
40
  }
38
41
 
39
42
  For "create": content is the new memory verbatim (or a slight refinement).
40
43
  For "update": content is the merged richest-version, \u226480 words.
44
+ For "supersede": content is the NEW fact only (\u226480 words); targetId is the stale memory being retired.
41
45
  For "noop": no content (existing memory is already correct).`;
42
46
  async function consolidateMemory(newContent, candidates, options) {
43
47
  const fallback = {
@@ -112,7 +116,9 @@ function validate(parsed, newContent, validIds) {
112
116
  if (typeof parsed !== "object" || parsed === null) return null;
113
117
  const obj = parsed;
114
118
  const action = obj.action;
115
- if (action !== "create" && action !== "update" && action !== "noop") return null;
119
+ if (action !== "create" && action !== "update" && action !== "noop" && action !== "supersede") {
120
+ return null;
121
+ }
116
122
  if (action === "create") {
117
123
  const c2 = typeof obj.content === "string" ? obj.content.trim() : "";
118
124
  return { action: "create", content: c2.length > 0 ? c2 : newContent };
@@ -124,7 +130,7 @@ function validate(parsed, newContent, validIds) {
124
130
  }
125
131
  const c = typeof obj.content === "string" ? obj.content.trim() : "";
126
132
  if (c.length === 0) return null;
127
- return { action: "update", targetId, content: c };
133
+ return { action, targetId, content: c };
128
134
  }
129
135
  export {
130
136
  consolidateMemory