@anuma/sdk 2.4.2 → 2.6.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.
@@ -8,6 +8,7 @@ import {
8
8
  ENTITY_KINDS,
9
9
  LRUCache,
10
10
  RerankerUnavailableError,
11
+ TOPICS_EXTRACTION_VERSION,
11
12
  chunkAndEmbedAllMessages,
12
13
  chunkAndEmbedMessage,
13
14
  chunkText,
@@ -21,6 +22,7 @@ import {
21
22
  createMemoryVaultSearchTool,
22
23
  createMessageOp,
23
24
  createSseClient,
25
+ createSupersedingMemoryOp,
24
26
  createVaultEmbeddingCache,
25
27
  createVaultMemoriesBatchOp,
26
28
  createVaultMemoryOp,
@@ -77,6 +79,7 @@ import {
77
79
  shouldChunkMessage,
78
80
  stampTopicsExtractedAtOp,
79
81
  stripToolCalls,
82
+ supersedeVaultMemoryOp,
80
83
  updateConversationPinnedOp,
81
84
  updateConversationTitleOp,
82
85
  updateMessageEmbeddingOp,
@@ -86,7 +89,7 @@ import {
86
89
  upsertMessageOp,
87
90
  useEncryption,
88
91
  vaultMemoryToStored
89
- } from "./chunk-UOUJUZM4.mjs";
92
+ } from "./chunk-TCRDSUC6.mjs";
90
93
  import {
91
94
  PII_PATTERNS,
92
95
  PiiRedactor,
@@ -5508,6 +5511,15 @@ __decorateClass([
5508
5511
  __decorateClass([
5509
5512
  field3("topics_extracted_at")
5510
5513
  ], VaultMemory.prototype, "topicsExtractedAt", 2);
5514
+ __decorateClass([
5515
+ field3("superseded_by")
5516
+ ], VaultMemory.prototype, "supersededBy", 2);
5517
+ __decorateClass([
5518
+ field3("superseded_at")
5519
+ ], VaultMemory.prototype, "supersededAt", 2);
5520
+ __decorateClass([
5521
+ field3("topics_extracted_version")
5522
+ ], VaultMemory.prototype, "topicsExtractedVersion", 2);
5511
5523
  __decorateClass([
5512
5524
  readonly2,
5513
5525
  date3("created_at")
@@ -5874,53 +5886,62 @@ async function retain(content, ctx, options = {}) {
5874
5886
  const enableAutoMerge = options.enableAutoMerge ?? true;
5875
5887
  const threshold = options.autoMergeThreshold ?? DEFAULT_AUTO_MERGE_THRESHOLD;
5876
5888
  const resolvedScope = options.scope ?? DEFAULT_SCOPE;
5889
+ let supersedeTargetId;
5890
+ let supersedeContent;
5877
5891
  if (enableAutoMerge) {
5878
5892
  if (options.consolidateOptions) {
5879
- const consolidationResult = await tryConsolidate(trimmed, ctx, options);
5880
- if (consolidationResult) return consolidationResult;
5881
- }
5882
- const matches = await searchVaultMemories(
5883
- trimmed,
5884
- ctx.vaultCtx,
5885
- ctx.embeddingOptions,
5886
- ctx.vaultCache,
5887
- {
5888
- limit: 1,
5889
- minSimilarity: threshold,
5890
- useFusion: false,
5891
- scopes: [resolvedScope],
5892
- ...options.folderId !== void 0 && { folderId: options.folderId }
5893
- }
5894
- );
5895
- if (matches.length > 0) {
5896
- const targetId = matches[0].uniqueId;
5897
- const existing = await getVaultMemoryOp(ctx.vaultCtx, targetId);
5898
- if (existing) {
5899
- const mergedSourceIds = unionStrings(
5900
- existing.sourceChunkIds ?? [],
5901
- options.sourceChunkIds ?? []
5902
- );
5903
- const eventTimeUpdate = pickEventTimeUpdate(existing, options.eventTime);
5904
- const updated = await updateVaultMemoryOp(ctx.vaultCtx, targetId, {
5905
- content: existing.content,
5906
- proofCountIncrement: 1,
5907
- sourceChunkIds: mergedSourceIds,
5908
- preserveUpdatedAt: true,
5909
- ...eventTimeUpdate && { eventTime: eventTimeUpdate }
5910
- });
5911
- if (updated) {
5912
- return {
5913
- action: "merge",
5914
- memoryId: targetId,
5915
- targetId,
5916
- proofCount: updated.proofCount ?? (existing.proofCount ?? 1) + 1
5917
- };
5893
+ const outcome = await tryConsolidate(trimmed, ctx, options);
5894
+ if (outcome) {
5895
+ if ("done" in outcome) return outcome.done;
5896
+ supersedeTargetId = outcome.supersede;
5897
+ supersedeContent = outcome.content;
5898
+ }
5899
+ }
5900
+ if (!supersedeTargetId) {
5901
+ const matches = await searchVaultMemories(
5902
+ trimmed,
5903
+ ctx.vaultCtx,
5904
+ ctx.embeddingOptions,
5905
+ ctx.vaultCache,
5906
+ {
5907
+ limit: 1,
5908
+ minSimilarity: threshold,
5909
+ useFusion: false,
5910
+ scopes: [resolvedScope],
5911
+ ...options.folderId !== void 0 && { folderId: options.folderId }
5912
+ }
5913
+ );
5914
+ if (matches.length > 0) {
5915
+ const targetId = matches[0].uniqueId;
5916
+ const existing = await getVaultMemoryOp(ctx.vaultCtx, targetId);
5917
+ if (existing && !existing.supersededBy) {
5918
+ const mergedSourceIds = unionStrings(
5919
+ existing.sourceChunkIds ?? [],
5920
+ options.sourceChunkIds ?? []
5921
+ );
5922
+ const eventTimeUpdate = pickEventTimeUpdate(existing, options.eventTime);
5923
+ const updated = await updateVaultMemoryOp(ctx.vaultCtx, targetId, {
5924
+ content: existing.content,
5925
+ proofCountIncrement: 1,
5926
+ sourceChunkIds: mergedSourceIds,
5927
+ preserveUpdatedAt: true,
5928
+ ...eventTimeUpdate && { eventTime: eventTimeUpdate }
5929
+ });
5930
+ if (updated) {
5931
+ return {
5932
+ action: "merge",
5933
+ memoryId: targetId,
5934
+ targetId,
5935
+ proofCount: updated.proofCount ?? (existing.proofCount ?? 1) + 1
5936
+ };
5937
+ }
5938
+ await assertMergeTargetGoneOrThrow(ctx, targetId);
5918
5939
  }
5919
- await assertMergeTargetGoneOrThrow(ctx, targetId);
5920
5940
  }
5921
5941
  }
5922
5942
  }
5923
- const embedding = await generateEmbedding(trimmed, ctx.embeddingOptions);
5943
+ const contentToWrite = supersedeContent ?? trimmed;
5944
+ const embedding = await generateEmbedding(contentToWrite, ctx.embeddingOptions);
5924
5945
  const embeddingModel = ctx.embeddingOptions.model ?? DEFAULT_API_EMBEDDING_MODEL;
5925
5946
  if (options.respectTombstones) {
5926
5947
  const tombstoneId = await findTombstoneMatch(embedding, embeddingModel, resolvedScope, ctx, {
@@ -5931,8 +5952,8 @@ async function retain(content, ctx, options = {}) {
5931
5952
  return { action: "suppressed", memoryId: tombstoneId, tombstoneId, proofCount: 0 };
5932
5953
  }
5933
5954
  }
5934
- const created = await createVaultMemoryOp(ctx.vaultCtx, {
5935
- content: trimmed,
5955
+ const createOpts = {
5956
+ content: contentToWrite,
5936
5957
  scope: resolvedScope,
5937
5958
  ...options.folderId !== void 0 && { folderId: options.folderId },
5938
5959
  embedding: JSON.stringify(embedding),
@@ -5949,7 +5970,24 @@ async function retain(content, ctx, options = {}) {
5949
5970
  kind: options.eventTime.kind
5950
5971
  }
5951
5972
  }
5952
- });
5973
+ };
5974
+ if (supersedeTargetId) {
5975
+ const { created: created2, retired } = await createSupersedingMemoryOp(
5976
+ ctx.vaultCtx,
5977
+ createOpts,
5978
+ supersedeTargetId
5979
+ );
5980
+ if (created2 && retired) {
5981
+ ctx.vaultCache.set(created2.uniqueId, Float32Array.from(embedding));
5982
+ return {
5983
+ action: "supersede",
5984
+ memoryId: created2.uniqueId,
5985
+ targetId: supersedeTargetId,
5986
+ proofCount: 1
5987
+ };
5988
+ }
5989
+ }
5990
+ const created = await createVaultMemoryOp(ctx.vaultCtx, createOpts);
5953
5991
  ctx.vaultCache.set(created.uniqueId, Float32Array.from(embedding));
5954
5992
  return {
5955
5993
  action: "create",
@@ -6016,12 +6054,17 @@ async function tryConsolidate(trimmed, ctx, options) {
6016
6054
  content: m.content,
6017
6055
  similarity: m.similarity
6018
6056
  }));
6019
- const { consolidateMemory: doConsolidate } = await import("./consolidate-HEMQGOUD.mjs");
6057
+ const { consolidateMemory: doConsolidate } = await import("./consolidate-3QQPUWD6.mjs");
6020
6058
  const decision = await doConsolidate(trimmed, candidates, consolidateOptions);
6021
6059
  if (decision.action === "create") return null;
6060
+ if (decision.action === "supersede" && decision.targetId && decision.content) {
6061
+ const existing = await getVaultMemoryOp(ctx.vaultCtx, decision.targetId);
6062
+ if (!existing || existing.supersededBy) return null;
6063
+ return { supersede: decision.targetId, content: decision.content };
6064
+ }
6022
6065
  if (decision.action === "noop" && decision.targetId) {
6023
6066
  const existing = await getVaultMemoryOp(ctx.vaultCtx, decision.targetId);
6024
- if (!existing) return null;
6067
+ if (!existing || existing.supersededBy) return null;
6025
6068
  const mergedSourceIds = unionStrings(
6026
6069
  existing.sourceChunkIds ?? [],
6027
6070
  options.sourceChunkIds ?? []
@@ -6039,15 +6082,17 @@ async function tryConsolidate(trimmed, ctx, options) {
6039
6082
  return null;
6040
6083
  }
6041
6084
  return {
6042
- action: "merge",
6043
- memoryId: decision.targetId,
6044
- targetId: decision.targetId,
6045
- proofCount: updated.proofCount ?? (existing.proofCount ?? 1) + 1
6085
+ done: {
6086
+ action: "merge",
6087
+ memoryId: decision.targetId,
6088
+ targetId: decision.targetId,
6089
+ proofCount: updated.proofCount ?? (existing.proofCount ?? 1) + 1
6090
+ }
6046
6091
  };
6047
6092
  }
6048
6093
  if (decision.action === "update" && decision.targetId && decision.content) {
6049
6094
  const existing = await getVaultMemoryOp(ctx.vaultCtx, decision.targetId);
6050
- if (!existing) return null;
6095
+ if (!existing || existing.supersededBy) return null;
6051
6096
  const mergedSourceIds = unionStrings(
6052
6097
  existing.sourceChunkIds ?? [],
6053
6098
  options.sourceChunkIds ?? []
@@ -6074,10 +6119,12 @@ async function tryConsolidate(trimmed, ctx, options) {
6074
6119
  }
6075
6120
  ctx.vaultCache.set(decision.targetId, Float32Array.from(newEmbedding));
6076
6121
  return {
6077
- action: "update",
6078
- memoryId: decision.targetId,
6079
- targetId: decision.targetId,
6080
- proofCount: updated.proofCount ?? (existing.proofCount ?? 1) + 1
6122
+ done: {
6123
+ action: "update",
6124
+ memoryId: decision.targetId,
6125
+ targetId: decision.targetId,
6126
+ proofCount: updated.proofCount ?? (existing.proofCount ?? 1) + 1
6127
+ }
6081
6128
  };
6082
6129
  }
6083
6130
  return null;
@@ -9530,7 +9577,7 @@ __decorateClass([
9530
9577
  ], VaultFolder.prototype, "context", 2);
9531
9578
 
9532
9579
  // src/lib/db/schema.ts
9533
- var SDK_SCHEMA_VERSION = 36;
9580
+ var SDK_SCHEMA_VERSION = 38;
9534
9581
  var sdkSchema = appSchema2({
9535
9582
  version: SDK_SCHEMA_VERSION,
9536
9583
  tables: [
@@ -9655,7 +9702,20 @@ var sdkSchema = appSchema2({
9655
9702
  // Unix ms of the last LLM topic-extraction pass over this memory's
9656
9703
  // content. Null = never extracted standalone (legacy rows with entity
9657
9704
  // links are grandfathered — see getMemoriesNeedingTopicExtractionOp).
9658
- { name: "topics_extracted_at", type: "number", isOptional: true }
9705
+ { name: "topics_extracted_at", type: "number", isOptional: true },
9706
+ // Write-time supersession (A2). When set, this fact was replaced by a
9707
+ // newer, incompatible-value fact (e.g. "Lives in Portland" superseded by
9708
+ // "Lives in SF"); `superseded_by` points at the newer memory's id and
9709
+ // `superseded_at` is the Unix ms it happened. Superseded rows stay in
9710
+ // the table (history / read-time fallback) but are excluded from recall
9711
+ // and dedup by default. Null = live (not superseded). Indexed so the
9712
+ // recall filter stays cheap.
9713
+ { name: "superseded_by", type: "string", isOptional: true, isIndexed: true },
9714
+ { name: "superseded_at", type: "number", isOptional: true },
9715
+ // The extraction-logic version this memory was last stamped under. Null
9716
+ // (pre-v38) is treated as version 0, so a bump of TOPICS_EXTRACTION_VERSION
9717
+ // re-extracts stale rows. See getMemoriesNeedingTopicExtractionOp.
9718
+ { name: "topics_extracted_version", type: "number", isOptional: true }
9659
9719
  ]
9660
9720
  }),
9661
9721
  // Entity table — canonical names extracted from auto-extraction (W5).
@@ -10266,6 +10326,34 @@ var sdkMigrations = schemaMigrations2({
10266
10326
  columns: [{ name: "topics_extracted_at", type: "number", isOptional: true }]
10267
10327
  })
10268
10328
  ]
10329
+ },
10330
+ // v36 -> v37: write-time supersession columns on memory_vault. Existing rows
10331
+ // keep both NULL (= live / not superseded), so no backfill — legacy
10332
+ // contradictory rows are handled by the read-time supersession fallback.
10333
+ {
10334
+ toVersion: 37,
10335
+ steps: [
10336
+ addColumns2({
10337
+ table: "memory_vault",
10338
+ columns: [
10339
+ { name: "superseded_by", type: "string", isOptional: true, isIndexed: true },
10340
+ { name: "superseded_at", type: "number", isOptional: true }
10341
+ ]
10342
+ })
10343
+ ]
10344
+ },
10345
+ // v37 -> v38: topics_extracted_version on memory_vault. Existing rows keep it
10346
+ // NULL, read as version 0 by getMemoriesNeedingTopicExtractionOp — so the
10347
+ // first sweep after a TOPICS_EXTRACTION_VERSION bump re-extracts them (drained
10348
+ // across sweeps by the worker's limit). See topicExtract.ts.
10349
+ {
10350
+ toVersion: 38,
10351
+ steps: [
10352
+ addColumns2({
10353
+ table: "memory_vault",
10354
+ columns: [{ name: "topics_extracted_version", type: "number", isOptional: true }]
10355
+ })
10356
+ ]
10269
10357
  }
10270
10358
  ]
10271
10359
  });
@@ -11625,6 +11713,7 @@ export {
11625
11713
  VaultFolder as StoredVaultFolderModel,
11626
11714
  VaultMemory as StoredVaultMemoryModel,
11627
11715
  StreamExpiredError,
11716
+ TOPICS_EXTRACTION_VERSION,
11628
11717
  TOPIC_EXTRACTION_BATCH_SIZE,
11629
11718
  WalletPoller,
11630
11719
  addConversationMemoriesOp,
@@ -11726,6 +11815,7 @@ export {
11726
11815
  stampTopicsExtractedAtOp,
11727
11816
  streamCancelPath,
11728
11817
  streamReplayPath,
11818
+ supersedeVaultMemoryOp,
11729
11819
  updateVaultFolderContextOp,
11730
11820
  updateVaultFolderOp,
11731
11821
  updateVaultMemoryEmbeddingOp,
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  recall
3
- } from "./chunk-IYDQBFS2.mjs";
3
+ } from "./chunk-TCRDSUC6.mjs";
4
4
  import "./chunk-DFVCS3QQ.mjs";
5
5
  import "./chunk-GYKJOWEO.mjs";
6
6
  export {
@@ -2027,6 +2027,7 @@ function isOwnedByCtxUser(ctx, record) {
2027
2027
  function baseVaultConditions(ctx, options) {
2028
2028
  return [
2029
2029
  ...options?.includeDeleted ? [] : [Q3.where("is_deleted", false)],
2030
+ ...options?.includeSuperseded ? [] : [Q3.where("superseded_by", null)],
2030
2031
  ...ctx.userId !== void 0 ? [Q3.where("user_id", ctx.userId)] : [],
2031
2032
  ...options?.since ? [Q3.where("updated_at", Q3.gt(options.since.getTime()))] : []
2032
2033
  ];
@@ -2067,6 +2068,9 @@ function vaultMemoryToStoredRaw(memory) {
2067
2068
  eventTimeKind: memory.eventTimeKind ?? null,
2068
2069
  topicsUserManaged: memory.topicsUserManaged ?? false,
2069
2070
  topicsExtractedAt: memory.topicsExtractedAt ?? null,
2071
+ topicsExtractedVersion: memory.topicsExtractedVersion ?? null,
2072
+ supersededBy: memory.supersededBy ?? null,
2073
+ supersededAt: memory.supersededAt ?? null,
2070
2074
  createdAt: memory.createdAt,
2071
2075
  updatedAt: memory.updatedAt,
2072
2076
  isDeleted: memory.isDeleted
@@ -2112,6 +2116,59 @@ async function createVaultMemoryOp(ctx, opts) {
2112
2116
  });
2113
2117
  return vaultMemoryToStored(created, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner);
2114
2118
  }
2119
+ async function createSupersedingMemoryOp(ctx, opts, targetId) {
2120
+ if (!targetId) return { created: null, retired: false };
2121
+ const scope = opts.scope ?? "private";
2122
+ const encryptedContent = ctx.walletAddress && ctx.signMessage ? await encryptVaultMemoryContent(
2123
+ opts.content,
2124
+ ctx.walletAddress,
2125
+ ctx.signMessage,
2126
+ ctx.embeddedWalletSigner
2127
+ ) : opts.content;
2128
+ let createdRecord = null;
2129
+ await ctx.database.write(async () => {
2130
+ let target;
2131
+ try {
2132
+ target = await ctx.vaultMemoryCollection.find(targetId);
2133
+ } catch {
2134
+ return;
2135
+ }
2136
+ if (target.isDeleted || target.supersededBy || !isOwnedByCtxUser(ctx, target)) return;
2137
+ createdRecord = await ctx.vaultMemoryCollection.create((record) => {
2138
+ record._setRaw("content", encryptedContent);
2139
+ record._setRaw("scope", scope);
2140
+ record._setRaw("folder_id", opts.folderId ?? null);
2141
+ record._setRaw("user_id", ctx.userId ?? null);
2142
+ record._setRaw("is_deleted", false);
2143
+ if (opts.embedding !== void 0) {
2144
+ record._setRaw("embedding", opts.embedding);
2145
+ record._setRaw("embedding_model", opts.embeddingModel ?? null);
2146
+ }
2147
+ if (opts.sourceChunkIds !== void 0) {
2148
+ record._setRaw("source_chunk_ids", JSON.stringify(opts.sourceChunkIds));
2149
+ }
2150
+ record._setRaw("proof_count", opts.proofCount ?? 1);
2151
+ record._setRaw("source", opts.source ?? "manual");
2152
+ if (opts.eventTime) {
2153
+ record._setRaw("event_time_start", opts.eventTime.start ?? null);
2154
+ record._setRaw("event_time_end", opts.eventTime.end ?? null);
2155
+ record._setRaw("event_time_kind", opts.eventTime.kind ?? null);
2156
+ }
2157
+ });
2158
+ await target.update((r) => {
2159
+ r._setRaw("superseded_by", createdRecord.id);
2160
+ r._setRaw("superseded_at", Date.now());
2161
+ });
2162
+ });
2163
+ if (!createdRecord) return { created: null, retired: false };
2164
+ const created = await vaultMemoryToStored(
2165
+ createdRecord,
2166
+ ctx.walletAddress,
2167
+ ctx.signMessage,
2168
+ ctx.embeddedWalletSigner
2169
+ );
2170
+ return { created, retired: true };
2171
+ }
2115
2172
  async function getMemoriesByEventTimeOp(ctx, windowStart, windowEnd) {
2116
2173
  const records = await ctx.vaultMemoryCollection.query(
2117
2174
  ...baseVaultConditions(ctx),
@@ -2251,6 +2308,9 @@ function vaultMemoryRawToStoredRaw(raw) {
2251
2308
  // SQLite stores booleans as 0/1, LokiJS as true/false — coerce both.
2252
2309
  topicsUserManaged: raw.topics_user_managed === true || raw.topics_user_managed === 1,
2253
2310
  topicsExtractedAt: raw.topics_extracted_at ?? null,
2311
+ topicsExtractedVersion: raw.topics_extracted_version ?? null,
2312
+ supersededBy: raw.superseded_by ?? null,
2313
+ supersededAt: raw.superseded_at ?? null,
2254
2314
  createdAt: new Date(raw.created_at),
2255
2315
  updatedAt: new Date(raw.updated_at),
2256
2316
  isDeleted: raw.is_deleted === true || raw.is_deleted === 1
@@ -2292,7 +2352,7 @@ async function getAllVaultMemoryContentsOp(ctx, options) {
2292
2352
  async function updateVaultMemoryOp(ctx, id, opts) {
2293
2353
  try {
2294
2354
  const probe = await ctx.vaultMemoryCollection.find(id);
2295
- if (probe.isDeleted || !isOwnedByCtxUser(ctx, probe)) return null;
2355
+ if (probe.isDeleted || probe.supersededBy || !isOwnedByCtxUser(ctx, probe)) return null;
2296
2356
  const encryptedContent = ctx.walletAddress && ctx.signMessage ? await encryptVaultMemoryContent(
2297
2357
  opts.content,
2298
2358
  ctx.walletAddress,
@@ -2303,7 +2363,7 @@ async function updateVaultMemoryOp(ctx, id, opts) {
2303
2363
  const record = probe;
2304
2364
  const originalUpdatedAt = record.updatedAt.getTime();
2305
2365
  await ctx.database.write(async () => {
2306
- if (record.isDeleted || !isOwnedByCtxUser(ctx, record)) {
2366
+ if (record.isDeleted || record.supersededBy || !isOwnedByCtxUser(ctx, record)) {
2307
2367
  stale = true;
2308
2368
  return;
2309
2369
  }
@@ -2403,7 +2463,10 @@ async function clearMemoryTopicsOverrideOp(ctx, memoryId) {
2403
2463
  await ctx.database.write(async () => {
2404
2464
  await record.update((r) => {
2405
2465
  r._setRaw("topics_user_managed", false);
2406
- r._setRaw("topics_extracted_at", null);
2466
+ r._setRaw("topics_extracted_version", null);
2467
+ if (record.topicsExtractedAt === null) {
2468
+ r._setRaw("topics_extracted_at", originalUpdatedAt);
2469
+ }
2407
2470
  r._setRaw("updated_at", originalUpdatedAt);
2408
2471
  });
2409
2472
  });
@@ -2435,6 +2498,40 @@ async function deleteVaultMemoryOp(ctx, id) {
2435
2498
  return false;
2436
2499
  }
2437
2500
  }
2501
+ async function supersedeVaultMemoryOp(ctx, id, supersededById) {
2502
+ if (id === supersededById) return false;
2503
+ try {
2504
+ const record = await ctx.vaultMemoryCollection.find(id);
2505
+ if (record.isDeleted || record.supersededBy || !isOwnedByCtxUser(ctx, record)) return false;
2506
+ let successor;
2507
+ try {
2508
+ successor = await ctx.vaultMemoryCollection.find(supersededById);
2509
+ } catch {
2510
+ return false;
2511
+ }
2512
+ if (successor.isDeleted || successor.supersededBy || !isOwnedByCtxUser(ctx, successor)) {
2513
+ return false;
2514
+ }
2515
+ let stale = false;
2516
+ await ctx.database.write(async () => {
2517
+ if (record.isDeleted || record.supersededBy || !isOwnedByCtxUser(ctx, record)) {
2518
+ stale = true;
2519
+ return;
2520
+ }
2521
+ if (successor.isDeleted || successor.supersededBy || !isOwnedByCtxUser(ctx, successor)) {
2522
+ stale = true;
2523
+ return;
2524
+ }
2525
+ await record.update((r) => {
2526
+ r._setRaw("superseded_by", supersededById);
2527
+ r._setRaw("superseded_at", Date.now());
2528
+ });
2529
+ });
2530
+ return !stale;
2531
+ } catch {
2532
+ return false;
2533
+ }
2534
+ }
2438
2535
  async function getUnfiledVaultMemoriesOp(ctx) {
2439
2536
  const conditions = [
2440
2537
  Q3.where("folder_id", null),
@@ -2447,6 +2544,7 @@ async function getUnfiledVaultMemoriesOp(ctx) {
2447
2544
  (record) => vaultMemoryToStored(record, ctx.walletAddress, ctx.signMessage, ctx.embeddedWalletSigner)
2448
2545
  );
2449
2546
  }
2547
+ var TOPICS_EXTRACTION_VERSION = 1;
2450
2548
  async function getMemoriesNeedingTopicExtractionOp(ctx, options) {
2451
2549
  const entityCtx = ctx.entityCtx;
2452
2550
  if (!entityCtx) {
@@ -2465,7 +2563,10 @@ async function getMemoriesNeedingTopicExtractionOp(ctx, options) {
2465
2563
  for (const raw of rows) {
2466
2564
  const stamp = raw.topics_extracted_at ?? null;
2467
2565
  if (stamp !== null) {
2468
- if (raw.updated_at > stamp) stampedPending.push(raw);
2566
+ const version = raw.topics_extracted_version ?? 0;
2567
+ if (raw.updated_at > stamp || version < TOPICS_EXTRACTION_VERSION) {
2568
+ stampedPending.push(raw);
2569
+ }
2469
2570
  } else {
2470
2571
  unstamped.push(raw);
2471
2572
  }
@@ -2495,7 +2596,7 @@ async function getMemoriesNeedingTopicExtractionOp(ctx, options) {
2495
2596
  );
2496
2597
  return { pending, linkedUnstamped };
2497
2598
  }
2498
- async function stampTopicsExtractedAtOp(ctx, memoryIds, extractedAt) {
2599
+ async function stampTopicsExtractedAtOp(ctx, memoryIds, extractedAt, version = TOPICS_EXTRACTION_VERSION) {
2499
2600
  if (memoryIds.length === 0) return [];
2500
2601
  const uniqueIds = Array.from(new Set(memoryIds));
2501
2602
  const CHUNK = 500;
@@ -2518,6 +2619,7 @@ async function stampTopicsExtractedAtOp(ctx, memoryIds, extractedAt) {
2518
2619
  prepared.push(
2519
2620
  record.prepareUpdate((r) => {
2520
2621
  r._setRaw("topics_extracted_at", extractedAt);
2622
+ r._setRaw("topics_extracted_version", version);
2521
2623
  r._setRaw("updated_at", originalUpdatedAt);
2522
2624
  })
2523
2625
  );
@@ -5282,7 +5384,7 @@ function createMemoryVaultSearchTool(vaultCtx, embeddingOptions, cache, searchOp
5282
5384
  legacy.map((r) => ({ id: r.uniqueId, content: r.content, score: r.similarity }))
5283
5385
  );
5284
5386
  }
5285
- const { recall: recall2 } = await import("./recall-G3CS444F.mjs");
5387
+ const { recall: recall2 } = await import("./recall-PV7L3SN3.mjs");
5286
5388
  const result = await recall2(
5287
5389
  query,
5288
5390
  { vaultCtx, embeddingOptions, vaultCache: cache },
@@ -6008,6 +6110,7 @@ export {
6008
6110
  getMemoriesByEntityNamesOp,
6009
6111
  vaultMemoryToStored,
6010
6112
  createVaultMemoryOp,
6113
+ createSupersedingMemoryOp,
6011
6114
  createVaultMemoriesBatchOp,
6012
6115
  getVaultMemoryOp,
6013
6116
  getAllVaultMemoriesOp,
@@ -6016,7 +6119,9 @@ export {
6016
6119
  setMemoryEntitiesOp,
6017
6120
  clearMemoryTopicsOverrideOp,
6018
6121
  deleteVaultMemoryOp,
6122
+ supersedeVaultMemoryOp,
6019
6123
  getUnfiledVaultMemoriesOp,
6124
+ TOPICS_EXTRACTION_VERSION,
6020
6125
  getMemoriesNeedingTopicExtractionOp,
6021
6126
  stampTopicsExtractedAtOp,
6022
6127
  deleteAllVaultMemoriesForUserOp,
@@ -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