recallnest 3.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (246) hide show
  1. package/CHANGELOG.md +304 -0
  2. package/README.md +251 -258
  3. package/README_CN.md +226 -247
  4. package/assets/ui/app.js +6 -1
  5. package/bin/recallnest.mjs +20 -37
  6. package/docs/api-reference.md +8 -0
  7. package/docs/architecture.md +18 -13
  8. package/eval/cases-canary.json +107 -9
  9. package/integrations/examples/claude-agent-sdk/memory-agent.ts +2 -0
  10. package/integrations/examples/langchain/memory-chain.py +4 -0
  11. package/integrations/examples/openai-agents-sdk/memory-agent.py +4 -0
  12. package/package.json +12 -3
  13. package/src/activity-counter.ts +1 -0
  14. package/src/adaptive-retrieval.ts +23 -0
  15. package/src/api-server.ts +13 -11
  16. package/src/asset-sync.ts +26 -2
  17. package/src/audit-log.ts +2 -0
  18. package/src/auto-gc.ts +4 -3
  19. package/src/belief-history.ts +137 -31
  20. package/src/capture-engine.ts +459 -71
  21. package/src/cascade-forget.ts +7 -3
  22. package/src/checkpoint-mirror.ts +417 -0
  23. package/src/cli.ts +574 -54
  24. package/src/conflict-engine.ts +43 -15
  25. package/src/consolidation-engine.ts +24 -3
  26. package/src/context-collapse-renderer.ts +8 -1
  27. package/src/context-composer-stable.ts +5 -1
  28. package/src/context-composer.ts +11 -2
  29. package/src/distill-lock.ts +246 -74
  30. package/src/dream-pipeline.ts +3 -1
  31. package/src/embedder.ts +24 -0
  32. package/src/env-config.ts +136 -0
  33. package/src/flock.ts +97 -0
  34. package/src/forget-engine.ts +342 -58
  35. package/src/gateway-server.ts +4 -0
  36. package/src/ingest-plan.ts +56 -0
  37. package/src/ingest.ts +294 -121
  38. package/src/kg-contradiction.ts +753 -0
  39. package/src/kg-extractor.ts +17 -0
  40. package/src/kg-store.ts +1 -1
  41. package/src/known-item-eval.ts +455 -0
  42. package/src/llm-client.ts +13 -26
  43. package/src/llm-consolidation.ts +2 -2
  44. package/src/mcp-tool-deps.ts +6 -0
  45. package/src/mcp-tools-advanced.ts +80 -45
  46. package/src/mcp-tools-core.ts +23 -17
  47. package/src/mcp-tools-governance.ts +13 -7
  48. package/src/memory-assets.ts +144 -8
  49. package/src/memory-boundaries.ts +19 -0
  50. package/src/memory-evolution.ts +9 -0
  51. package/src/memory-lint.ts +1 -1
  52. package/src/memory-output.ts +311 -14
  53. package/src/memory-reconcile.ts +1437 -0
  54. package/src/memory-schema.ts +11 -0
  55. package/src/memory-store-port.ts +1 -0
  56. package/src/minis-archive.ts +151 -0
  57. package/src/noise-filter.ts +15 -2
  58. package/src/pii-detector.ts +31 -1
  59. package/src/pivot-apply.ts +55 -5
  60. package/src/pivot-evidence.ts +128 -0
  61. package/src/ppr-traversal.ts +25 -1
  62. package/src/preference-matcher.ts +40 -28
  63. package/src/prospective-memory.ts +7 -4
  64. package/src/retriever.ts +297 -57
  65. package/src/retroactive-boost.ts +3 -2
  66. package/src/runtime-config.ts +15 -1
  67. package/src/scope-policy.ts +1 -0
  68. package/src/session-distiller.ts +6 -2
  69. package/src/session-engine.ts +11 -4
  70. package/src/session-output.ts +20 -1
  71. package/src/session-schema.ts +5 -2
  72. package/src/session-store.ts +59 -12
  73. package/src/skill-promotion.ts +67 -1
  74. package/src/store-receipts.ts +109 -0
  75. package/src/store-write-budget.ts +37 -0
  76. package/src/store.ts +298 -57
  77. package/src/text-fingerprint.ts +25 -0
  78. package/src/trigger-store.ts +443 -0
  79. package/src/ui-server.ts +22 -21
  80. package/src/version-manager.ts +19 -5
  81. package/src/write-verifier.ts +16 -11
  82. package/src/__tests__/access-tracker-novelty.test.ts +0 -324
  83. package/src/__tests__/activity-counter.test.ts +0 -125
  84. package/src/__tests__/adaptive-fidelity.test.ts +0 -102
  85. package/src/__tests__/admission-control.test.ts +0 -206
  86. package/src/__tests__/age-label.test.ts +0 -33
  87. package/src/__tests__/anchor-generator.test.ts +0 -67
  88. package/src/__tests__/api-server-security.test.ts +0 -102
  89. package/src/__tests__/auto-consolidation.test.ts +0 -168
  90. package/src/__tests__/auto-recall.test.ts +0 -131
  91. package/src/__tests__/babel-integration.test.ts +0 -144
  92. package/src/__tests__/batch-dedup.test.ts +0 -65
  93. package/src/__tests__/belief-history.test.ts +0 -220
  94. package/src/__tests__/canary-eval.test.ts +0 -114
  95. package/src/__tests__/capture-engine.test.ts +0 -1328
  96. package/src/__tests__/capture-heuristic.test.ts +0 -171
  97. package/src/__tests__/cascade-forget.test.ts +0 -210
  98. package/src/__tests__/category-min-scores.test.ts +0 -116
  99. package/src/__tests__/cc8-cc1-essential-context.test.ts +0 -298
  100. package/src/__tests__/circuit-breaker.test.ts +0 -99
  101. package/src/__tests__/cluster-consolidation.test.ts +0 -1236
  102. package/src/__tests__/confidence-metadata.test.ts +0 -125
  103. package/src/__tests__/confidence-tracker.test.ts +0 -261
  104. package/src/__tests__/conflict-advisor.test.ts +0 -206
  105. package/src/__tests__/conflict-escalation.test.ts +0 -149
  106. package/src/__tests__/conflict-lifecycle.test.ts +0 -83
  107. package/src/__tests__/conflict-output.test.ts +0 -85
  108. package/src/__tests__/conflict-store.test.ts +0 -418
  109. package/src/__tests__/connector-v1.test.ts +0 -181
  110. package/src/__tests__/consolidation-engine.test.ts +0 -520
  111. package/src/__tests__/constructive-retrieval.test.ts +0 -509
  112. package/src/__tests__/context-collapse-renderer.test.ts +0 -214
  113. package/src/__tests__/context-composer-pins.test.ts +0 -64
  114. package/src/__tests__/context-composer-task-selection.test.ts +0 -24
  115. package/src/__tests__/context-reconstructor.test.ts +0 -65
  116. package/src/__tests__/context-renderer.test.ts +0 -91
  117. package/src/__tests__/continuity-eval.test.ts +0 -275
  118. package/src/__tests__/continuity-schema.test.ts +0 -97
  119. package/src/__tests__/contracts.test.ts +0 -262
  120. package/src/__tests__/conversation-importer.test.ts +0 -692
  121. package/src/__tests__/core-recall-eval.test.ts +0 -235
  122. package/src/__tests__/core-summary.test.ts +0 -109
  123. package/src/__tests__/data-checkup.test.ts +0 -304
  124. package/src/__tests__/decay-exemption.test.ts +0 -129
  125. package/src/__tests__/dedup-actions.test.ts +0 -58
  126. package/src/__tests__/dedup-guard.test.ts +0 -39
  127. package/src/__tests__/deterministic-id.test.ts +0 -39
  128. package/src/__tests__/distill-lock.test.ts +0 -305
  129. package/src/__tests__/distill-session-argshape.test.ts +0 -79
  130. package/src/__tests__/doctor.test.ts +0 -319
  131. package/src/__tests__/dream-pipeline.test.ts +0 -820
  132. package/src/__tests__/dream-scope-isolation.test.ts +0 -386
  133. package/src/__tests__/dream-write-batching.test.ts +0 -211
  134. package/src/__tests__/drill-down.test.ts +0 -108
  135. package/src/__tests__/embedder.test.ts +0 -122
  136. package/src/__tests__/emotion-decay.test.ts +0 -46
  137. package/src/__tests__/emotion-detector.test.ts +0 -56
  138. package/src/__tests__/emotion-phase1-integration.test.ts +0 -153
  139. package/src/__tests__/emotion-retrieval.test.ts +0 -56
  140. package/src/__tests__/entity-resolver.test.ts +0 -58
  141. package/src/__tests__/env-config-invariant.test.ts +0 -80
  142. package/src/__tests__/env-config.test.ts +0 -211
  143. package/src/__tests__/error-signature-boost.test.ts +0 -87
  144. package/src/__tests__/error-signature.test.ts +0 -95
  145. package/src/__tests__/error-taxonomy.test.ts +0 -129
  146. package/src/__tests__/ethics-layer.test.ts +0 -476
  147. package/src/__tests__/event-segmenter.test.ts +0 -153
  148. package/src/__tests__/feature-flags-integration.test.ts +0 -54
  149. package/src/__tests__/frequency-tracker.test.ts +0 -113
  150. package/src/__tests__/freshness.test.ts +0 -186
  151. package/src/__tests__/graph-export.test.ts +0 -489
  152. package/src/__tests__/hotness-blend.test.ts +0 -283
  153. package/src/__tests__/ingest-chunking.test.ts +0 -44
  154. package/src/__tests__/ingest-scope-prefix.test.ts +0 -69
  155. package/src/__tests__/ingest.test.ts +0 -360
  156. package/src/__tests__/interference-detector.test.ts +0 -203
  157. package/src/__tests__/kg-extractor.test.ts +0 -192
  158. package/src/__tests__/kg-store.test.ts +0 -300
  159. package/src/__tests__/llm-client-pivot.test.ts +0 -628
  160. package/src/__tests__/llm-consolidation.test.ts +0 -226
  161. package/src/__tests__/maintenance-locks.test.ts +0 -197
  162. package/src/__tests__/mcp-efficiency.test.ts +0 -288
  163. package/src/__tests__/mcp-related-scopes.test.ts +0 -151
  164. package/src/__tests__/memory-boundaries.test.ts +0 -282
  165. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +0 -362
  166. package/src/__tests__/memory-evolution.test.ts +0 -271
  167. package/src/__tests__/memory-health-rebalance.test.ts +0 -144
  168. package/src/__tests__/memory-lint.test.ts +0 -421
  169. package/src/__tests__/memory-output.test.ts +0 -224
  170. package/src/__tests__/memory-promotion.test.ts +0 -315
  171. package/src/__tests__/memory-utility.test.ts +0 -256
  172. package/src/__tests__/migrate-dedup-ids.test.ts +0 -65
  173. package/src/__tests__/multi-vector.test.ts +0 -242
  174. package/src/__tests__/narrative-architecture.test.ts +0 -456
  175. package/src/__tests__/noise-filter.test.ts +0 -205
  176. package/src/__tests__/noise-prototype-bank.test.ts +0 -280
  177. package/src/__tests__/obsidian-connector.test.ts +0 -363
  178. package/src/__tests__/omitted-reporting.test.ts +0 -98
  179. package/src/__tests__/openai-contract.test.ts +0 -513
  180. package/src/__tests__/pending-review.test.ts +0 -77
  181. package/src/__tests__/pii-redact.test.ts +0 -333
  182. package/src/__tests__/pivot-apply.test.ts +0 -373
  183. package/src/__tests__/pivot-distill-supervisor.test.ts +0 -457
  184. package/src/__tests__/pivot-distill.test.ts +0 -1178
  185. package/src/__tests__/post-retrieval-filter.test.ts +0 -178
  186. package/src/__tests__/ppr-traversal.test.ts +0 -256
  187. package/src/__tests__/prediction-engine.test.ts +0 -349
  188. package/src/__tests__/predictive-prospective-memory.test.ts +0 -416
  189. package/src/__tests__/preference-guard-smoke.test.ts +0 -211
  190. package/src/__tests__/preference-matcher.test.ts +0 -282
  191. package/src/__tests__/preference-slots.test.ts +0 -205
  192. package/src/__tests__/promotion-distinct-sources.test.ts +0 -162
  193. package/src/__tests__/prospective-memory.test.ts +0 -283
  194. package/src/__tests__/pull-from-macbook-sync-only.test.ts +0 -679
  195. package/src/__tests__/query-entity-detector.test.ts +0 -171
  196. package/src/__tests__/query-expander-alias.test.ts +0 -146
  197. package/src/__tests__/query-expander.test.ts +0 -34
  198. package/src/__tests__/recall-gate.test.ts +0 -266
  199. package/src/__tests__/recall-governor.test.ts +0 -227
  200. package/src/__tests__/recall-mode.test.ts +0 -64
  201. package/src/__tests__/resolve-db-path.test.ts +0 -130
  202. package/src/__tests__/resolve-tier.test.ts +0 -64
  203. package/src/__tests__/result-synthesizer.test.ts +0 -198
  204. package/src/__tests__/resume-context.test.ts +0 -4898
  205. package/src/__tests__/retrieval-eval.test.ts +0 -163
  206. package/src/__tests__/retrieval-trace.test.ts +0 -213
  207. package/src/__tests__/retrieve-audit-revision.test.ts +0 -255
  208. package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
  209. package/src/__tests__/retriever-lazy-init-dispatch.test.ts +0 -123
  210. package/src/__tests__/retroactive-boost.test.ts +0 -223
  211. package/src/__tests__/salience-scoring.test.ts +0 -106
  212. package/src/__tests__/scope-inventory.test.ts +0 -200
  213. package/src/__tests__/scope-policy.test.ts +0 -69
  214. package/src/__tests__/scope-suggester.test.ts +0 -80
  215. package/src/__tests__/session-checkpoint.test.ts +0 -291
  216. package/src/__tests__/session-distiller.test.ts +0 -640
  217. package/src/__tests__/session-gc.test.ts +0 -215
  218. package/src/__tests__/skill-engine.test.ts +0 -552
  219. package/src/__tests__/skill-promotion.test.ts +0 -654
  220. package/src/__tests__/skill-verifier.test.ts +0 -122
  221. package/src/__tests__/source-heartbeat.test.ts +0 -254
  222. package/src/__tests__/stable-context-length-contract.test.ts +0 -62
  223. package/src/__tests__/store-patch-metadata.test.ts +0 -235
  224. package/src/__tests__/store-read-consistency.test.ts +0 -122
  225. package/src/__tests__/store-schema-migration.test.ts +0 -87
  226. package/src/__tests__/store-scope-audit.test.ts +0 -81
  227. package/src/__tests__/store-upsert.test.ts +0 -201
  228. package/src/__tests__/supersede-chain.test.ts +0 -154
  229. package/src/__tests__/synthesis-contract.test.ts +0 -252
  230. package/src/__tests__/synthesis-promotion.test.ts +0 -369
  231. package/src/__tests__/temporal-parser.test.ts +0 -241
  232. package/src/__tests__/temporal-validity.test.ts +0 -160
  233. package/src/__tests__/term-registry-terminal-roster.test.ts +0 -127
  234. package/src/__tests__/term-registry.test.ts +0 -121
  235. package/src/__tests__/tool-error-surfacing.test.ts +0 -98
  236. package/src/__tests__/tool-output-compressor.test.ts +0 -160
  237. package/src/__tests__/topic-tag.test.ts +0 -113
  238. package/src/__tests__/trusted-memory.test.ts +0 -867
  239. package/src/__tests__/ui-server-security.test.ts +0 -93
  240. package/src/__tests__/usage-tracker.test.ts +0 -287
  241. package/src/__tests__/utility-weight.test.ts +0 -57
  242. package/src/__tests__/verbatim-self-recall.test.ts +0 -279
  243. package/src/__tests__/version-manager.test.ts +0 -248
  244. package/src/__tests__/workflow-observation-skill-outcome.test.ts +0 -352
  245. package/src/__tests__/workflow-observation.test.ts +0 -433
  246. package/src/__tests__/write-verifier.test.ts +0 -122
@@ -19,6 +19,7 @@
19
19
  */
20
20
 
21
21
  import { deterministicId, type MemoryEntry, type MemoryStore } from "./store.js";
22
+ import type { MemoryStoreUpdate } from "./memory-store-port.js";
22
23
  import { parseEvolution, patchEvolution, type EvolutionMetadata } from "./memory-evolution.js";
23
24
  import { detectLang, tokenizeFts } from "./language-hook.js";
24
25
  import type { Embedder } from "./embedder.js";
@@ -45,41 +46,25 @@ export interface BeliefArchiveResult {
45
46
  * canonical-match candidates come from — so the entry handed to us usually has no vector.
46
47
  * Re-read the full row first; only re-embed if the store can't give it back.
47
48
  */
48
- async function resolveVector(deps: BeliefHistoryDeps, current: MemoryEntry): Promise<number[]> {
49
+ async function resolveVector(
50
+ store: Partial<Pick<MemoryStore, "getById">>,
51
+ embedder: Pick<Embedder, "embedPassage">,
52
+ current: MemoryEntry,
53
+ ): Promise<number[]> {
49
54
  if (current.vector && current.vector.length > 0) return current.vector;
50
55
 
51
- if (deps.store.getById) {
52
- const full = await deps.store.getById(current.id);
56
+ if (store.getById) {
57
+ const full = await store.getById(current.id);
53
58
  if (full?.vector && full.vector.length > 0) return full.vector;
54
59
  }
55
60
 
56
- return await deps.embedder.embedPassage(current.text);
61
+ return await embedder.embedPassage(current.text);
57
62
  }
58
63
 
59
- /**
60
- * Archive the current content of a canonical memory as a `superseded` history row.
61
- *
62
- * Throws rather than degrading to a silent overwrite: a belief-changing write that cannot
63
- * preserve the old belief is exactly the failure this module exists to prevent, so callers
64
- * must not proceed with the overwrite if this rejects.
65
- *
66
- * Known limitation — archive and overwrite are two writes with no transaction around them.
67
- * If the caller's update() fails after this returns, the history row is already committed
68
- * while the canonical row still holds that same text, leaving a superseded duplicate of a
69
- * belief that is in fact still live. Damage is bounded: nothing is destroyed, and the
70
- * deterministic id means a retry reuses the same row rather than piling up copies.
71
- */
72
- export async function archiveBeliefVersion(
73
- deps: BeliefHistoryDeps,
74
- current: MemoryEntry,
75
- options: { now?: number } = {},
76
- ): Promise<BeliefArchiveResult> {
77
- const now = options.now ?? Date.now();
78
- const currentEvo = parseEvolution(current.metadata, current.timestamp);
79
-
80
- // Text is part of the id, not just (id, timestamp): two belief changes landing in the
81
- // same millisecond would otherwise collide and the second archive would overwrite the
82
- // first. With the text folded in, a collision means the content is identical anyway.
64
+ // Text is part of the id, not just (id, timestamp): two belief changes landing in the
65
+ // same millisecond would otherwise collide and the second archive would overwrite the
66
+ // first. With the text folded in, a collision means the content is identical anyway.
67
+ function beliefHistoryId(current: MemoryEntry): string {
83
68
  const historyId = deterministicId(
84
69
  current.scope,
85
70
  `${BELIEF_HISTORY_ID_PREFIX}${current.id}@${current.timestamp}\n${current.text}`,
@@ -87,11 +72,19 @@ export async function archiveBeliefVersion(
87
72
  if (historyId === current.id) {
88
73
  throw new Error(`Belief history id collided with canonical id ${current.id}`);
89
74
  }
75
+ return historyId;
76
+ }
90
77
 
91
- const vector = await resolveVector(deps, current);
78
+ /**
79
+ * Build the `superseded` history row for `current` (pure — no I/O). Throws when no vector is
80
+ * available or the history id would collide with the canonical id.
81
+ */
82
+ function buildBeliefHistoryRow(current: MemoryEntry, vector: number[], now: number): MemoryEntry {
83
+ const historyId = beliefHistoryId(current);
92
84
  if (vector.length === 0) {
93
85
  throw new Error(`Cannot archive belief ${current.id}: no vector available`);
94
86
  }
87
+ const currentEvo = parseEvolution(current.metadata, current.timestamp);
95
88
 
96
89
  // The history row inherits the old metadata wholesale (canonicalKey, boundary, emotion…)
97
90
  // and only has its evolution block rewritten to close the interval.
@@ -104,7 +97,7 @@ export async function archiveBeliefVersion(
104
97
  supersedes: currentEvo.supersedes,
105
98
  });
106
99
 
107
- const historyRow: MemoryEntry = {
100
+ return {
108
101
  id: historyId,
109
102
  text: current.text,
110
103
  vector,
@@ -123,6 +116,35 @@ export async function archiveBeliefVersion(
123
116
  language: current.language || detectLang(current.text),
124
117
  fts_text: current.fts_text || tokenizeFts(current.text, current.language || detectLang(current.text)),
125
118
  };
119
+ }
120
+
121
+ /**
122
+ * Archive the current content of a canonical memory as a `superseded` history row.
123
+ *
124
+ * Throws rather than degrading to a silent overwrite: a belief-changing write that cannot
125
+ * preserve the old belief is exactly the failure this module exists to prevent, so callers
126
+ * must not proceed with the overwrite if this rejects.
127
+ *
128
+ * This is the two-write form: the history row is committed here and the caller overwrites
129
+ * the canonical row afterwards, with no lock held across the two. Callers that hold a full
130
+ * MemoryStore use replaceBeliefVersion() instead, which re-reads the canonical row and does
131
+ * both writes in one store-write critical section; this function remains only as the
132
+ * fallback for narrower store deps (test doubles) that lack replaceWithArchivedVersion.
133
+ * On that fallback path, if the canonical row is deleted between the caller's read and its
134
+ * overwrite, the history row written here stays behind pointing at a row that no longer
135
+ * exists; and if the overwrite fails for another reason, it is a superseded duplicate of a
136
+ * belief that is in fact still live. The deterministic id means a retry reuses the same row
137
+ * rather than piling up copies.
138
+ */
139
+ export async function archiveBeliefVersion(
140
+ deps: BeliefHistoryDeps,
141
+ current: MemoryEntry,
142
+ options: { now?: number } = {},
143
+ ): Promise<BeliefArchiveResult> {
144
+ const now = options.now ?? Date.now();
145
+ beliefHistoryId(current); // fail on an id collision before doing any I/O
146
+ const vector = await resolveVector(deps.store, deps.embedder, current);
147
+ const historyRow = buildBeliefHistoryRow(current, vector, now);
126
148
 
127
149
  if (deps.store.upsert) {
128
150
  await deps.store.upsert(historyRow);
@@ -132,7 +154,91 @@ export async function archiveBeliefVersion(
132
154
  await deps.store.store(historyRow);
133
155
  }
134
156
 
135
- return { historyId, validUntil: now };
157
+ return { historyId: historyRow.id, validUntil: now };
158
+ }
159
+
160
+ /** The canonical row a belief replacement was aimed at no longer exists; nothing was written. */
161
+ export class BeliefTargetMissingError extends Error {
162
+ readonly memoryId: string;
163
+ constructor(memoryId: string) {
164
+ super(`Belief target ${memoryId} no longer exists; no history row was written and nothing was overwritten`);
165
+ this.name = "BeliefTargetMissingError";
166
+ this.memoryId = memoryId;
167
+ }
168
+ }
169
+
170
+ export interface BeliefReplaceDeps {
171
+ store: Pick<MemoryStore, "replaceWithArchivedVersion"> & Partial<Pick<MemoryStore, "getById">>;
172
+ embedder: Pick<Embedder, "embedPassage">;
173
+ }
174
+
175
+ /** What the caller's updates builder sees — all derived from the row re-read under the lock. */
176
+ export interface BeliefReplaceContext {
177
+ current: MemoryEntry;
178
+ previousEvolution: EvolutionMetadata;
179
+ historyId: string;
180
+ now: number;
181
+ }
182
+
183
+ export type BeliefReplaceResult =
184
+ | { status: "missing" }
185
+ | { status: "unchanged"; current: MemoryEntry }
186
+ | { status: "replaced"; entry: MemoryEntry; historyId: string; validUntil: number };
187
+
188
+ /**
189
+ * Archive the canonical row's current version and overwrite it, in one store-write critical
190
+ * section (MemoryStore.replaceWithArchivedVersion): the row is re-read under the lock, and
191
+ * if it is gone nothing at all is written — no history row is left pointing at a row that a
192
+ * concurrent forget just removed.
193
+ *
194
+ * The history row and the updates are built from the row as re-read under the lock, not from
195
+ * `expected` (the caller's earlier read), so a concurrent edit is what gets archived. The one
196
+ * slow step — resolving a vector when the row carries none (re-read or re-embed) — runs before
197
+ * the lock is taken, for `expected`'s text; it is used only if the re-read row has no vector of
198
+ * its own and still holds that same text.
199
+ *
200
+ * `skip(current)` returning true leaves everything as it is (result "unchanged").
201
+ * `buildUpdates` runs under the lock: it must be pure and synchronous.
202
+ *
203
+ * Time: unless `options.now` is given, the single timestamp used for the history row's
204
+ * validUntil, the canonical row's timestamp / validFrom (via ctx.now) and the returned
205
+ * validUntil is taken inside the lock, and is strictly later than the re-read row's own
206
+ * timestamp and validFrom. Strictly: history ids hash (canonical id, timestamp, text), so a
207
+ * revision time that repeats (canonical timestamp in the future because another machine's
208
+ * clock is fast) would let A → B → A → C reuse version 1's history id, overwrite it and close
209
+ * an A↔B backward-link cycle. Taking it before the lock (and before vector resolution) let a
210
+ * replacement that committed in between leave a history row with validFrom > validUntil and
211
+ * move the canonical timestamp backwards.
212
+ *
213
+ * language / fts_text of the history row are always recomputed from its text, as the old
214
+ * archive path did: the row re-read under the lock carries the "en" / raw-text placeholders
215
+ * legacy rows were migrated with, and copying those would mislabel Chinese history rows.
216
+ */
217
+ export async function replaceBeliefVersion(
218
+ deps: BeliefReplaceDeps,
219
+ expected: MemoryEntry,
220
+ buildUpdates: (ctx: BeliefReplaceContext) => MemoryStoreUpdate,
221
+ options: { now?: number; scopeFilter?: string[]; skip?: (current: MemoryEntry) => boolean } = {},
222
+ ): Promise<BeliefReplaceResult> {
223
+ const fallbackVector = await resolveVector(deps.store, deps.embedder, expected);
224
+
225
+ let lockedNow = 0;
226
+ const result = await deps.store.replaceWithArchivedVersion(expected.id, (current) => {
227
+ if (options.skip?.(current)) return null;
228
+ const previousEvolution = parseEvolution(current.metadata, current.timestamp);
229
+ const now = options.now ?? Math.max(Date.now(), current.timestamp + 1, previousEvolution.validFrom + 1);
230
+ lockedNow = now;
231
+ const vector = current.vector.length > 0
232
+ ? current.vector
233
+ : current.text === expected.text ? fallbackVector : [];
234
+ const history = buildBeliefHistoryRow({ ...current, language: undefined, fts_text: undefined }, vector, now);
235
+ return { history, updates: buildUpdates({ current, previousEvolution, historyId: history.id, now }) };
236
+ }, options.scopeFilter);
237
+
238
+ if (result.status === "replaced") {
239
+ return { status: "replaced", entry: result.entry, historyId: result.history.id, validUntil: lockedNow };
240
+ }
241
+ return result;
136
242
  }
137
243
 
138
244
  /**