recallnest 2.3.0 → 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.
Files changed (258) hide show
  1. package/CHANGELOG.md +187 -0
  2. package/README.md +49 -38
  3. package/README_CN.md +46 -31
  4. package/assets/ui/app.js +1031 -0
  5. package/assets/ui/index.html +224 -0
  6. package/assets/ui/styles.css +781 -0
  7. package/config.json.example +4 -1
  8. package/docs/api-reference.md +1291 -0
  9. package/docs/architecture.md +168 -0
  10. package/docs/connector-spec.md +126 -0
  11. package/eval/cases-canary.json +58 -0
  12. package/eval/cases-scope-robustness.json +278 -0
  13. package/eval/cases.json +117 -20
  14. package/package.json +25 -4
  15. package/src/__tests__/access-tracker-novelty.test.ts +304 -0
  16. package/src/__tests__/activity-counter.test.ts +125 -0
  17. package/src/__tests__/adaptive-fidelity.test.ts +102 -0
  18. package/src/__tests__/admission-control.test.ts +206 -0
  19. package/src/__tests__/anchor-generator.test.ts +67 -0
  20. package/src/__tests__/api-server-security.test.ts +102 -0
  21. package/src/__tests__/auto-consolidation.test.ts +168 -0
  22. package/src/__tests__/auto-recall.test.ts +131 -0
  23. package/src/__tests__/babel-integration.test.ts +144 -0
  24. package/src/__tests__/batch-dedup.test.ts +65 -0
  25. package/src/__tests__/belief-history.test.ts +220 -0
  26. package/src/__tests__/canary-eval.test.ts +114 -0
  27. package/src/__tests__/capture-engine.test.ts +1328 -0
  28. package/src/__tests__/capture-heuristic.test.ts +171 -0
  29. package/src/__tests__/cascade-forget.test.ts +210 -0
  30. package/src/__tests__/category-min-scores.test.ts +116 -0
  31. package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
  32. package/src/__tests__/circuit-breaker.test.ts +99 -0
  33. package/src/__tests__/cluster-consolidation.test.ts +945 -0
  34. package/src/__tests__/confidence-metadata.test.ts +125 -0
  35. package/src/__tests__/confidence-tracker.test.ts +261 -0
  36. package/src/__tests__/conflict-advisor.test.ts +206 -0
  37. package/src/__tests__/conflict-escalation.test.ts +149 -0
  38. package/src/__tests__/conflict-lifecycle.test.ts +83 -0
  39. package/src/__tests__/conflict-output.test.ts +85 -0
  40. package/src/__tests__/conflict-store.test.ts +418 -0
  41. package/src/__tests__/connector-v1.test.ts +181 -0
  42. package/src/__tests__/consolidation-engine.test.ts +430 -0
  43. package/src/__tests__/constructive-retrieval.test.ts +509 -0
  44. package/src/__tests__/context-collapse-renderer.test.ts +214 -0
  45. package/src/__tests__/context-composer-pins.test.ts +64 -0
  46. package/src/__tests__/context-composer-task-selection.test.ts +24 -0
  47. package/src/__tests__/context-reconstructor.test.ts +65 -0
  48. package/src/__tests__/context-renderer.test.ts +91 -0
  49. package/src/__tests__/continuity-eval.test.ts +275 -0
  50. package/src/__tests__/continuity-schema.test.ts +97 -0
  51. package/src/__tests__/contracts.test.ts +262 -0
  52. package/src/__tests__/conversation-importer.test.ts +692 -0
  53. package/src/__tests__/core-recall-eval.test.ts +235 -0
  54. package/src/__tests__/core-summary.test.ts +109 -0
  55. package/src/__tests__/data-checkup.test.ts +276 -0
  56. package/src/__tests__/decay-exemption.test.ts +129 -0
  57. package/src/__tests__/dedup-actions.test.ts +58 -0
  58. package/src/__tests__/dedup-guard.test.ts +39 -0
  59. package/src/__tests__/deterministic-id.test.ts +39 -0
  60. package/src/__tests__/distill-lock.test.ts +305 -0
  61. package/src/__tests__/distill-session-argshape.test.ts +79 -0
  62. package/src/__tests__/doctor.test.ts +319 -0
  63. package/src/__tests__/dream-pipeline.test.ts +780 -0
  64. package/src/__tests__/drill-down.test.ts +108 -0
  65. package/src/__tests__/embedder.test.ts +122 -0
  66. package/src/__tests__/emotion-decay.test.ts +46 -0
  67. package/src/__tests__/emotion-detector.test.ts +56 -0
  68. package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
  69. package/src/__tests__/emotion-retrieval.test.ts +56 -0
  70. package/src/__tests__/entity-resolver.test.ts +58 -0
  71. package/src/__tests__/env-config.test.ts +151 -0
  72. package/src/__tests__/error-signature-boost.test.ts +87 -0
  73. package/src/__tests__/error-signature.test.ts +95 -0
  74. package/src/__tests__/error-taxonomy.test.ts +129 -0
  75. package/src/__tests__/ethics-layer.test.ts +476 -0
  76. package/src/__tests__/event-segmenter.test.ts +153 -0
  77. package/src/__tests__/feature-flags-integration.test.ts +54 -0
  78. package/src/__tests__/frequency-tracker.test.ts +113 -0
  79. package/src/__tests__/freshness.test.ts +186 -0
  80. package/src/__tests__/graph-export.test.ts +489 -0
  81. package/src/__tests__/hotness-blend.test.ts +283 -0
  82. package/src/__tests__/ingest-chunking.test.ts +44 -0
  83. package/src/__tests__/ingest.test.ts +360 -0
  84. package/src/__tests__/interference-detector.test.ts +203 -0
  85. package/src/__tests__/kg-extractor.test.ts +192 -0
  86. package/src/__tests__/kg-store.test.ts +300 -0
  87. package/src/__tests__/llm-client-pivot.test.ts +628 -0
  88. package/src/__tests__/llm-consolidation.test.ts +226 -0
  89. package/src/__tests__/maintenance-locks.test.ts +189 -0
  90. package/src/__tests__/mcp-efficiency.test.ts +288 -0
  91. package/src/__tests__/mcp-related-scopes.test.ts +151 -0
  92. package/src/__tests__/memory-boundaries.test.ts +282 -0
  93. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +345 -0
  94. package/src/__tests__/memory-evolution.test.ts +271 -0
  95. package/src/__tests__/memory-health-rebalance.test.ts +144 -0
  96. package/src/__tests__/memory-lint.test.ts +421 -0
  97. package/src/__tests__/memory-output.test.ts +215 -0
  98. package/src/__tests__/memory-promotion.test.ts +315 -0
  99. package/src/__tests__/migrate-dedup-ids.test.ts +65 -0
  100. package/src/__tests__/multi-vector.test.ts +242 -0
  101. package/src/__tests__/narrative-architecture.test.ts +456 -0
  102. package/src/__tests__/noise-filter.test.ts +205 -0
  103. package/src/__tests__/noise-prototype-bank.test.ts +280 -0
  104. package/src/__tests__/obsidian-connector.test.ts +363 -0
  105. package/src/__tests__/omitted-reporting.test.ts +98 -0
  106. package/src/__tests__/pending-review.test.ts +77 -0
  107. package/src/__tests__/pii-redact.test.ts +333 -0
  108. package/src/__tests__/pivot-apply.test.ts +373 -0
  109. package/src/__tests__/pivot-distill-supervisor.test.ts +457 -0
  110. package/src/__tests__/pivot-distill.test.ts +1178 -0
  111. package/src/__tests__/post-retrieval-filter.test.ts +178 -0
  112. package/src/__tests__/ppr-traversal.test.ts +256 -0
  113. package/src/__tests__/prediction-engine.test.ts +349 -0
  114. package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
  115. package/src/__tests__/preference-guard-smoke.test.ts +211 -0
  116. package/src/__tests__/preference-matcher.test.ts +282 -0
  117. package/src/__tests__/preference-slots.test.ts +205 -0
  118. package/src/__tests__/prospective-memory.test.ts +283 -0
  119. package/src/__tests__/pull-from-macbook-sync-only.test.ts +679 -0
  120. package/src/__tests__/query-entity-detector.test.ts +171 -0
  121. package/src/__tests__/query-expander-alias.test.ts +146 -0
  122. package/src/__tests__/query-expander.test.ts +34 -0
  123. package/src/__tests__/recall-gate.test.ts +266 -0
  124. package/src/__tests__/recall-governor.test.ts +227 -0
  125. package/src/__tests__/recall-mode.test.ts +64 -0
  126. package/src/__tests__/resolve-db-path.test.ts +130 -0
  127. package/src/__tests__/resolve-tier.test.ts +64 -0
  128. package/src/__tests__/result-synthesizer.test.ts +198 -0
  129. package/src/__tests__/resume-context.test.ts +4898 -0
  130. package/src/__tests__/retrieval-eval.test.ts +163 -0
  131. package/src/__tests__/retrieval-trace.test.ts +213 -0
  132. package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
  133. package/src/__tests__/retriever-lazy-init-dispatch.test.ts +123 -0
  134. package/src/__tests__/retroactive-boost.test.ts +223 -0
  135. package/src/__tests__/salience-scoring.test.ts +106 -0
  136. package/src/__tests__/scope-inventory.test.ts +200 -0
  137. package/src/__tests__/scope-policy.test.ts +69 -0
  138. package/src/__tests__/scope-suggester.test.ts +80 -0
  139. package/src/__tests__/session-checkpoint.test.ts +291 -0
  140. package/src/__tests__/session-distiller.test.ts +640 -0
  141. package/src/__tests__/session-gc.test.ts +215 -0
  142. package/src/__tests__/skill-engine.test.ts +552 -0
  143. package/src/__tests__/skill-promotion.test.ts +654 -0
  144. package/src/__tests__/skill-verifier.test.ts +122 -0
  145. package/src/__tests__/source-heartbeat.test.ts +250 -0
  146. package/src/__tests__/stable-context-length-contract.test.ts +62 -0
  147. package/src/__tests__/store-patch-metadata.test.ts +205 -0
  148. package/src/__tests__/store-read-consistency.test.ts +122 -0
  149. package/src/__tests__/store-schema-migration.test.ts +87 -0
  150. package/src/__tests__/store-scope-audit.test.ts +81 -0
  151. package/src/__tests__/store-upsert.test.ts +201 -0
  152. package/src/__tests__/supersede-chain.test.ts +154 -0
  153. package/src/__tests__/temporal-parser.test.ts +241 -0
  154. package/src/__tests__/temporal-validity.test.ts +160 -0
  155. package/src/__tests__/term-registry-terminal-roster.test.ts +127 -0
  156. package/src/__tests__/term-registry.test.ts +121 -0
  157. package/src/__tests__/tool-error-surfacing.test.ts +98 -0
  158. package/src/__tests__/tool-output-compressor.test.ts +160 -0
  159. package/src/__tests__/topic-tag.test.ts +113 -0
  160. package/src/__tests__/trusted-memory.test.ts +839 -0
  161. package/src/__tests__/ui-server-security.test.ts +93 -0
  162. package/src/__tests__/usage-tracker.test.ts +287 -0
  163. package/src/__tests__/version-manager.test.ts +213 -0
  164. package/src/__tests__/workflow-observation-skill-outcome.test.ts +352 -0
  165. package/src/__tests__/workflow-observation.test.ts +433 -0
  166. package/src/__tests__/write-verifier.test.ts +122 -0
  167. package/src/access-tracker.ts +90 -34
  168. package/src/activity-counter.ts +46 -18
  169. package/src/adaptive-retrieval.ts +11 -0
  170. package/src/aliases.ts +133 -0
  171. package/src/api-server.ts +36 -9
  172. package/src/audit-log.ts +4 -2
  173. package/src/auto-consolidation.ts +8 -3
  174. package/src/auto-gc.ts +85 -21
  175. package/src/auto-recall.ts +61 -1
  176. package/src/belief-history.ts +164 -0
  177. package/src/capture-engine.ts +142 -32
  178. package/src/cli.ts +268 -41
  179. package/src/confidence-tracker.ts +36 -18
  180. package/src/conflict-engine.ts +24 -3
  181. package/src/consolidation-engine.ts +255 -23
  182. package/src/context-composer-stable-selection.ts +7 -1
  183. package/src/context-composer.ts +8 -1
  184. package/src/data-checkup.ts +96 -7
  185. package/src/decay-engine.ts +59 -7
  186. package/src/distill-lock.ts +194 -15
  187. package/src/doctor.ts +13 -4
  188. package/src/dream-pipeline.ts +473 -41
  189. package/src/env-config.ts +118 -0
  190. package/src/error-signature.ts +153 -0
  191. package/src/error-taxonomy.ts +115 -0
  192. package/src/eval.ts +402 -14
  193. package/src/freshness.ts +192 -0
  194. package/src/http-request-policy.ts +148 -0
  195. package/src/idempotency.ts +8 -0
  196. package/src/ingest.ts +308 -12
  197. package/src/kg-extractor.ts +13 -3
  198. package/src/kg-store.ts +236 -44
  199. package/src/llm-client.ts +106 -11
  200. package/src/llm-consolidation.ts +2 -1
  201. package/src/mcp-server.ts +87 -1800
  202. package/src/mcp-tool-deps.ts +48 -0
  203. package/src/mcp-tools-advanced.ts +954 -0
  204. package/src/mcp-tools-core.ts +637 -0
  205. package/src/mcp-tools-governance.ts +475 -0
  206. package/src/memory-boundaries.ts +26 -3
  207. package/src/memory-evolution.ts +73 -1
  208. package/src/memory-health-rebalance.ts +4 -0
  209. package/src/memory-lint.ts +183 -10
  210. package/src/memory-output.ts +129 -4
  211. package/src/memory-promotion.ts +246 -0
  212. package/src/memory-schema.ts +25 -2
  213. package/src/memory-store-port.ts +46 -0
  214. package/src/multi-vector.ts +2 -1
  215. package/src/narrative-schema.ts +2 -1
  216. package/src/noise-filter.ts +57 -6
  217. package/src/noise-prototype-bank.ts +408 -0
  218. package/src/pii-detector.ts +207 -5
  219. package/src/pivot-apply.ts +399 -0
  220. package/src/preference-matcher.ts +37 -3
  221. package/src/query-expander.ts +90 -9
  222. package/src/recall-gate.ts +305 -0
  223. package/src/result-synthesizer.ts +2 -1
  224. package/src/retention-policy.ts +2 -1
  225. package/src/retrieval-profiles.ts +46 -4
  226. package/src/retriever.ts +354 -157
  227. package/src/retroactive-boost.ts +6 -2
  228. package/src/runtime-config.ts +78 -6
  229. package/src/scope-suggester.ts +113 -0
  230. package/src/session-distiller.ts +23 -2
  231. package/src/session-engine.ts +2 -1
  232. package/src/session-schema.ts +1 -0
  233. package/src/session-store.ts +5 -0
  234. package/src/skill-engine.ts +78 -0
  235. package/src/skill-promotion.ts +304 -48
  236. package/src/skill-schema.ts +16 -3
  237. package/src/skill-verifier.ts +152 -0
  238. package/src/store.ts +321 -57
  239. package/src/term-registry.ts +35 -0
  240. package/src/tool-output-compressor.ts +3 -1
  241. package/src/ui-server.ts +9 -1
  242. package/src/usage-tracker.ts +242 -0
  243. package/src/version-manager.ts +3 -2
  244. package/src/version.ts +19 -0
  245. package/src/workflow-observation-engine.ts +2 -1
  246. package/src/workflow-observation-schema.ts +2 -1
  247. package/src/workflow-observation-store.ts +6 -2
  248. package/.env +0 -4
  249. package/CLAUDE.md +0 -46
  250. package/config.json +0 -55
  251. package/daily-recall.sh +0 -110
  252. package/logs/daily-recall.error.log +0 -0
  253. package/logs/daily-recall.log +0 -60
  254. package/logs/ingest-2026-04-11.log +0 -110
  255. package/logs/ingest-2026-04-12.log +0 -90
  256. package/logs/ingest-2026-04-13.log +0 -87
  257. package/logs/launchagent-stderr.log +0 -0
  258. package/logs/launchagent-stdout.log +0 -0
package/src/cli.ts CHANGED
@@ -11,16 +11,19 @@
11
11
 
12
12
  import { Command } from "commander";
13
13
  import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
14
- import { join, resolve } from "node:path";
14
+ import { basename, dirname, join, resolve } from "node:path";
15
15
  import { homedir } from "node:os";
16
16
 
17
17
  import { metaDir } from "./compat.js";
18
+ import { PACKAGE_VERSION } from "./version.js";
18
19
  import type { RetrievalResult } from "./retriever.js";
19
20
  import { applyRetrievalProfile, listRetrievalProfiles } from "./retrieval-profiles.js";
20
21
  import { distillResults, formatExplainResults, formatSearchResults, selectBriefSeedResults, summarizeResults } from "./memory-output.js";
21
22
  import { archiveDirtyBriefAsset, assetSummaryLine, buildBriefAsset, buildPinAsset, getExportsDir, listDirtyBriefAssets, listMemoryAssets, listPinAssets, pinSummaryLine, saveBriefAsset, savePinAsset, writeExportArtifact } from "./memory-assets.js";
22
23
  import { indexAsset, indexPinnedAsset } from "./asset-sync.js";
23
- import { createComponents, createStoreOnly, expandHome, loadConfig, loadDotEnv, type LocalMemoryConfig } from "./runtime-config.js";
24
+ import { createComponents, createStoreOnly, expandHome, loadConfig, loadDotEnv, resolveDbPath, type LocalMemoryConfig } from "./runtime-config.js";
25
+ import { KGStore } from "./kg-store.js";
26
+ import { isKGModeEnabled } from "./kg-extractor.js";
24
27
  import {
25
28
  formatDedupReasonSummary,
26
29
  getDedupSkipRate,
@@ -28,12 +31,16 @@ import {
28
31
  ingestCCTranscripts,
29
32
  ingestCodexSessions,
30
33
  ingestGeminiSessions,
34
+ ingestKimiSessions,
31
35
  ingestMarkdownFiles,
32
36
  ingestObsidianVault,
33
37
  ingestConnectorFile,
34
38
  } from "./ingest.js";
35
39
  import { runDoctor, formatDoctorResults } from "./doctor.js";
36
40
  import { persistCaseMemory, persistMemory, persistWorkflowPattern } from "./capture-engine.js";
41
+ import { scanMemoryPromotions, buildPromoteScanDeps, formatPromoteScanResult } from "./memory-promotion.js";
42
+ import { runDream, formatDreamResult, formatDreamMetrics, DEFAULT_DREAM_CONFIG, classifyDreamFailure, shouldBlockDreamRun, partitionAutoDreamScopes } from "./dream-pipeline.js";
43
+ import { listScopesAboveThreshold } from "./activity-counter.js";
37
44
  import {
38
45
  CaseMemoryInputSchema,
39
46
  type CaseMemoryInput,
@@ -47,6 +54,7 @@ import { ConflictStatusSchema } from "./conflict-schema.js";
47
54
  import { resolveConflictCandidate } from "./conflict-engine.js";
48
55
  import { escalateConflicts } from "./conflict-escalation.js";
49
56
  import { ConflictCandidateStore } from "./conflict-store.js";
57
+ import { createAuditLogger } from "./audit-log.js";
50
58
  import { formatConflictAudit, formatConflictAuditMarkdown, formatConflictClusters, formatConflictEscalation, formatConflictList, formatConflictRecord, formatConflictResolution } from "./conflict-output.js";
51
59
  import { CONFLICT_ATTENTION_LEVELS, parseConflictAttention, summarizeConflictLifecycle } from "./conflict-lifecycle.js";
52
60
  import { buildConflictAuditSummary, clusterConflicts, summarizeConflictAdvice } from "./conflict-advisor.js";
@@ -63,49 +71,66 @@ import {
63
71
  } from "./workflow-observation-scope-review.js";
64
72
  import { collectScopeInventory, formatScopeInventoryReport } from "./scope-inventory.js";
65
73
  import { buildRetrievalContext, resolveScopeSelection } from "./scope-policy.js";
74
+ import { createScopeSuggester } from "./scope-suggester.js";
66
75
 
67
76
  /**
68
- * Auto-detect Claude Code transcript directory.
69
- * Scans ~/.claude/projects/ for subdirectories containing .jsonl files.
77
+ * Auto-detect Claude Code transcript directories.
78
+ * Scans ~/.claude/projects/ for ALL subdirectories containing .jsonl files.
79
+ *
80
+ * CC 按 cwd 编码 project 子目录,凡是不在 ~ 下跑的会话(Codex spawn 的子进程 / cd 到 repo 跑 /
81
+ * bridge 临时工作目录)会落到 ~/.claude/projects/-XXX/ 别的子目录。旧版只挑 jsonl 最多的根
82
+ * 目录返回,导致 28+ 个子目录全失声。修复:返回所有有 jsonl 的子目录,调用方循环 ingest。
70
83
  */
71
- function detectCCPath(): string | null {
84
+ function detectCCPath(): string[] {
72
85
  const projectsDir = join(homedir(), ".claude", "projects");
73
- if (!existsSync(projectsDir)) return null;
86
+ if (!existsSync(projectsDir)) return [];
74
87
  try {
75
- const dirs = readdirSync(projectsDir, { withFileTypes: true })
88
+ return readdirSync(projectsDir, { withFileTypes: true })
76
89
  .filter(d => d.isDirectory())
77
- .map(d => join(projectsDir, d.name));
78
- // Pick the directory with most .jsonl files
79
- let best = "";
80
- let bestCount = 0;
81
- for (const dir of dirs) {
82
- const jsonlCount = readdirSync(dir).filter(f => f.endsWith(".jsonl")).length;
83
- if (jsonlCount > bestCount) {
84
- bestCount = jsonlCount;
85
- best = dir;
86
- }
87
- }
88
- return best || null;
90
+ .map(d => join(projectsDir, d.name))
91
+ .filter(dir => {
92
+ try { return readdirSync(dir).some(f => f.endsWith(".jsonl")); }
93
+ catch { return false; }
94
+ });
89
95
  } catch {
90
- return null;
96
+ return [];
91
97
  }
92
98
  }
93
99
 
100
+ /**
101
+ * Pick the CC project subdirectory with the most jsonl files (heuristic: user's primary cwd).
102
+ * Used by memory source which still expects a single directory.
103
+ */
104
+ function pickPrimaryCCPath(detected: string[]): string {
105
+ let best = detected[0];
106
+ let bestCount = 0;
107
+ for (const dir of detected) {
108
+ try {
109
+ const c = readdirSync(dir).filter(f => f.endsWith(".jsonl")).length;
110
+ if (c > bestCount) { bestCount = c; best = dir; }
111
+ } catch { /* skip */ }
112
+ }
113
+ return best;
114
+ }
115
+
94
116
  /**
95
117
  * Resolve "auto" paths in config to actual directories.
118
+ *
119
+ * Note: cc 分支只返回第一个 detect 到的目录(fallback),ingest 入口已改为直接调
120
+ * detectCCPath() 拿全量子目录循环处理。这里保留是为兼容潜在的其它调用方。
96
121
  */
97
122
  function resolveSourcePath(source: string, key: string): string {
98
123
  if (source !== "auto") return expandHome(source);
99
124
 
100
125
  if (key === "cc") {
101
126
  const detected = detectCCPath();
102
- if (!detected) throw new Error("Could not auto-detect Claude Code transcript path. Set sources.cc.path in config.json.");
103
- return detected;
127
+ if (detected.length === 0) throw new Error("Could not auto-detect Claude Code transcript path. Set sources.cc.path in config.json.");
128
+ return pickPrimaryCCPath(detected);
104
129
  }
105
130
  if (key === "memory") {
106
- const ccPath = detectCCPath();
107
- if (!ccPath) throw new Error("Could not auto-detect memory path. Set sources.memory.path in config.json.");
108
- return join(ccPath, "memory");
131
+ const detected = detectCCPath();
132
+ if (detected.length === 0) throw new Error("Could not auto-detect memory path. Set sources.memory.path in config.json.");
133
+ return join(pickPrimaryCCPath(detected), "memory");
109
134
  }
110
135
  throw new Error(`Cannot auto-detect path for source "${key}". Set it manually in config.json.`);
111
136
  }
@@ -130,7 +155,7 @@ const program = new Command();
130
155
  program
131
156
  .name("recallnest")
132
157
  .description("本地优先 AI 对话记忆搜索与蒸馏层")
133
- .version("1.4.0");
158
+ .version(PACKAGE_VERSION);
134
159
 
135
160
  async function runRetrievalView(
136
161
  view: "search" | "explain" | "distill",
@@ -147,7 +172,7 @@ async function runRetrievalView(
147
172
  },
148
173
  ) {
149
174
  const config = loadConfig();
150
- const { retriever, profile } = createComponents(config, options.profile);
175
+ const { retriever, profile, store } = createComponents(config, options.profile);
151
176
 
152
177
  const limit = parseLimitOption(options.limit, 5, 1, 20);
153
178
 
@@ -206,6 +231,17 @@ async function runRetrievalView(
206
231
  : formatSearchResults(results, context);
207
232
 
208
233
  console.log(rendered);
234
+
235
+ // P1-A: 显式 scope 0 命中 → 相近 scope 提示打到 stderr(不污染 stdout)
236
+ if (results.length === 0 && options.scope) {
237
+ try {
238
+ const suggest = createScopeSuggester(async () => (await store.stats()).scopeCounts);
239
+ const suggestions = (await suggest(options.scope)).filter(s => s !== options.scope);
240
+ if (suggestions.length > 0) {
241
+ console.error(`⚠️ scope '${options.scope}' 命中 0 条,相近 scope: [${suggestions.join(", ")}]`);
242
+ }
243
+ } catch { /* best-effort */ }
244
+ }
209
245
  }
210
246
 
211
247
  function parseLimitOption(value: string | undefined, fallback: number, min = 1, max = 50): number {
@@ -644,6 +680,28 @@ program
644
680
  ].join("\n"));
645
681
  });
646
682
 
683
+ program
684
+ .command("promote-scan")
685
+ .description("扫描 transcript 降级的 profile/preferences evidence,聚类高频高 importance 项晋升回 durable")
686
+ .requiredOption("--scope <scope>", "要扫描的 scope,如 cc:project:recallnest")
687
+ .option("--apply", "执行晋升;默认 dry-run 只预览")
688
+ .option("--min-occurrences <n>", "聚类最小成员数(默认 3)", "3")
689
+ .option("--min-importance <n>", "聚类最小平均 importance(默认 0.6)", "0.6")
690
+ .action(async (options) => {
691
+ const config = loadConfig();
692
+ const { store, embedder } = createComponents(config);
693
+ const conflictStore = new ConflictCandidateStore();
694
+ const deps = buildPromoteScanDeps({ store, embedder, conflictStore });
695
+ const minOccurrences = Number(options.minOccurrences);
696
+ const minImportance = Number(options.minImportance);
697
+ const result = await scanMemoryPromotions(deps, options.scope, {
698
+ dryRun: !options.apply,
699
+ ...(Number.isFinite(minOccurrences) ? { minOccurrences } : {}),
700
+ ...(Number.isFinite(minImportance) ? { minImportance } : {}),
701
+ });
702
+ console.log(formatPromoteScanResult(result));
703
+ });
704
+
647
705
  program
648
706
  .command("export <query>")
649
707
  .description("导出检索与蒸馏结果到 markdown/json")
@@ -1054,7 +1112,7 @@ program
1054
1112
  continue;
1055
1113
  }
1056
1114
 
1057
- const record = await persistWorkflowPattern({ store, embedder, conflictStore }, input);
1115
+ const record = await persistWorkflowPattern({ store, embedder, conflictStore, auditLogger: createAuditLogger() }, input);
1058
1116
  existingKeys.add(key);
1059
1117
  stored.push({ title: record.title, id: record.id, scope: record.resolvedScope });
1060
1118
  }
@@ -1130,7 +1188,7 @@ program
1130
1188
  continue;
1131
1189
  }
1132
1190
 
1133
- const record = await persistCaseMemory({ store, embedder, conflictStore }, input);
1191
+ const record = await persistCaseMemory({ store, embedder, conflictStore, auditLogger: createAuditLogger() }, input);
1134
1192
  existingKeys.add(key);
1135
1193
  stored.push({ title: record.title, id: record.id, scope: record.resolvedScope });
1136
1194
  }
@@ -1208,7 +1266,7 @@ program
1208
1266
  continue;
1209
1267
  }
1210
1268
 
1211
- const record = await persistMemory({ store, embedder, conflictStore }, input);
1269
+ const record = await persistMemory({ store, embedder, conflictStore, auditLogger: createAuditLogger() }, input);
1212
1270
  existingKeys.add(storeMemoryIdentity({
1213
1271
  category: input.category,
1214
1272
  scope: record.resolvedScope,
@@ -1302,7 +1360,7 @@ program
1302
1360
  program
1303
1361
  .command("ingest")
1304
1362
  .description("导入对话记录到索引")
1305
- .option("-s, --source <source>", "数据源: cc / codex / memory / desktop / all", "all")
1363
+ .option("-s, --source <source>", "数据源: cc / codex / gemini / kimi / memory / desktop / all", "all")
1306
1364
  .option("-l, --limit <n>", "限制处理文件数(调试用)")
1307
1365
  .option("-v, --verbose", "详细输出")
1308
1366
  .option("--no-dedup", "跳过向量去重")
@@ -1345,8 +1403,10 @@ program
1345
1403
  console.log(` ✅ ${config.embedding.model} (${testResult.dimensions}d)`);
1346
1404
 
1347
1405
  // LLM status — respect --no-llm flag
1348
- const effectiveLlm = options.noLlm ? null : llm;
1349
- if (options.noLlm) {
1406
+ // commander 把 --no-llm 解析成 options.llm=false(不产生 options.noLlm),对齐 --no-dedup 的取反键写法
1407
+ const noLlm = options.llm === false;
1408
+ const effectiveLlm = noLlm ? null : llm;
1409
+ if (noLlm) {
1350
1410
  console.log(" ⚠️ LLM: 已通过 --no-llm 禁用,原始对话将跳过不存入");
1351
1411
  } else if (llm) {
1352
1412
  const llmTest = await llm.test();
@@ -1376,15 +1436,23 @@ program
1376
1436
  const ingestOpts = { limit, verbose, noDedup, llm: effectiveLlm, recentHours };
1377
1437
 
1378
1438
  // CC Transcripts
1439
+ // CC 按 cwd 编码 project 子目录,所有有 jsonl 的子目录都要 ingest(不只是 jsonl 最多的那个)
1379
1440
  if (source === "all" || source === "cc") {
1380
1441
  console.log("📝 导入 Claude Code 对话...");
1381
1442
  const ccSource = config.sources.cc;
1382
1443
  if (ccSource) {
1383
- const ccPath = resolveSourcePath(ccSource.path, "cc");
1384
- const r = await ingestCCTranscripts(store, embedder, ccPath, ingestOpts);
1385
- results.push(r);
1386
- console.log(` ✅ CC: ${formatIngestSummary(r)}`);
1387
- maybeWarnHighCCDedupRate(r);
1444
+ const ccPaths = ccSource.path === "auto"
1445
+ ? detectCCPath()
1446
+ : [expandHome(ccSource.path)];
1447
+ if (ccPaths.length === 0) {
1448
+ throw new Error("Could not auto-detect Claude Code transcript path. Set sources.cc.path in config.json.");
1449
+ }
1450
+ for (const ccPath of ccPaths) {
1451
+ const r = await ingestCCTranscripts(store, embedder, ccPath, ingestOpts);
1452
+ results.push(r);
1453
+ console.log(` ✅ CC [${basename(ccPath)}]: ${formatIngestSummary(r)}`);
1454
+ maybeWarnHighCCDedupRate(r);
1455
+ }
1388
1456
  }
1389
1457
  }
1390
1458
 
@@ -1396,6 +1464,14 @@ program
1396
1464
  console.log(` ✅ Codex: ${formatIngestSummary(r)}`);
1397
1465
  }
1398
1466
 
1467
+ // Kimi Code Sessions (wire.jsonl 事件流,~/.kimi-code/sessions/)
1468
+ if (source === "all" || source === "kimi") {
1469
+ console.log("🌙 导入 Kimi Code 对话...");
1470
+ const r = await ingestKimiSessions(store, embedder, ingestOpts);
1471
+ results.push(r);
1472
+ console.log(` ✅ Kimi: ${formatIngestSummary(r)}`);
1473
+ }
1474
+
1399
1475
  // Gemini Sessions (JSON format under ~/.gemini/tmp/*/chats/)
1400
1476
  if (source === "all" || source === "gemini") {
1401
1477
  console.log("💎 导入 Gemini 对话...");
@@ -1403,7 +1479,7 @@ program
1403
1479
  limit,
1404
1480
  verbose,
1405
1481
  noDedup,
1406
- llm,
1482
+ llm: effectiveLlm,
1407
1483
  recentHours,
1408
1484
  });
1409
1485
  results.push(r);
@@ -1436,7 +1512,7 @@ program
1436
1512
  const r = await ingestMarkdownFiles(store, embedder, memPath, "memory", {
1437
1513
  verbose,
1438
1514
  noDedup,
1439
- llm,
1515
+ llm: effectiveLlm,
1440
1516
  recentHours,
1441
1517
  });
1442
1518
  results.push(r);
@@ -1502,6 +1578,155 @@ program
1502
1578
  console.log();
1503
1579
  });
1504
1580
 
1581
+ // ─── dream ───────────────────────────────────────────────────────────────────
1582
+ // P3 调度化入口:launchd 经 scripts/dream-consolidation.sh 调用本命令,
1583
+ // 输出 [[DREAM_STATUS]] ok|skip|blocked 标记供脚本判定(同 weekly-distill 的
1584
+ // [[DISTILL_STATUS]] 模式)。dream 是 auto-gc / usageStatus 快照 / consolidation
1585
+ // 的唯一生产触发点——没有它们整条维护链就是死代码(2026-06-11 临床重审实锤)。
1586
+
1587
+ program
1588
+ .command("dream")
1589
+ .description("运行 dream 离线整固管线 (Orient/Gather/Consolidate/Prune)")
1590
+ .option("--scope <scope>", "目标 scope(前缀或精确)", "cc")
1591
+ .option("--auto", "扫描 activity-counter 中写计数达标的所有 scope,逐个跑 dream(单 scope 失败不阻断其他)")
1592
+ .option("--force", "跳过 min-writes 门槛强制运行")
1593
+ .action(async (options: { scope: string; auto?: boolean; force?: boolean }) => {
1594
+ const config = loadConfig();
1595
+ const { store, embedder, llm } = createComponents(config);
1596
+ // Same activity-stats file the store writes per-scope counts to (beside the LanceDB dir).
1597
+ const activityStatsPath = join(dirname(store.dbPath), "activity-stats.json");
1598
+ // KG evidence for triple-overlap merging — same gate as the MCP server
1599
+ const kgStore = isKGModeEnabled() ? new KGStore({ dbPath: store.dbPath }) : null;
1600
+
1601
+ if (options.auto) {
1602
+ const eligible = listScopesAboveThreshold(DEFAULT_DREAM_CONFIG.minWritesForDream, { statsPath: activityStatsPath });
1603
+ // 巨型 scope 交给专用 schedule,不占日常轮次的 wall-clock 预算(理由见
1604
+ // DreamConfig.autoExcludeScopes:07-30 那轮 6h 只跑了 15/507,而 memory 独占 79% 的条数)。
1605
+ const { run: scopes, deferred } = partitionAutoDreamScopes(eligible, DEFAULT_DREAM_CONFIG.autoExcludeScopes);
1606
+ if (deferred.length > 0) {
1607
+ // 不静默:被挪走的 scope 必须在日志里看得见,否则"今天怎么没跑 memory"只能靠翻代码回答。
1608
+ // 前缀刻意用 `[dream]` 而不是 `dream --auto:`——后者是 dream-checkup.sh 抓「达标 scope 数」
1609
+ // 的锚点,多一行同前缀的会混进它的取数。
1610
+ console.log(`[dream] ${deferred.length} 个 scope 交给专用 schedule,本轮不跑 → ${deferred.join(", ")}`);
1611
+ }
1612
+ if (scopes.length === 0) {
1613
+ // 两种空:一种是真没人达标,一种是达标的全被挪走了——混成一句会让排障从这里开始跑偏。
1614
+ console.log(
1615
+ eligible.length === 0
1616
+ ? "dream --auto: 无达标 scope(写计数均低于门槛)"
1617
+ : "dream --auto: 达标 scope 全部交给专用 schedule,本轮无事可做",
1618
+ );
1619
+ console.log("[[DREAM_STATUS]] skip");
1620
+ process.exit(0);
1621
+ }
1622
+ console.log(`dream --auto: ${scopes.length} 个达标 scope → ${scopes.join(", ")}`);
1623
+
1624
+ // Wall-clock 预算(2026-07-29 补):防单轮跑成跨天。07-24 那轮跑了 4 天 15 小时,
1625
+ // 把 07-25~27 三天的调度全堵死(上一次没结束,launchd 起不了新的)。
1626
+ // 天花板见 DreamConfig.autoRunBudgetMs 的注释——截断会让靠后 scope 系统性饿死,
1627
+ // 真解是 session scope TTL,不是把预算调大。
1628
+ const budgetMs = Number(process.env.RECALLNEST_DREAM_BUDGET_MS ?? DEFAULT_DREAM_CONFIG.autoRunBudgetMs);
1629
+ const deadline = Date.now() + budgetMs;
1630
+
1631
+ let fatalFailures = 0;
1632
+ let transientFailures = 0;
1633
+ let processed = 0;
1634
+ // 2026-08-12 产出计量:`[[DREAM_STATUS]] ok` 只证明「没崩」,不证明「产出了东西」。
1635
+ // 2026-07-23 查出的语义阈值卡在真实 p99 之上,就是这样静默跑了 2716 次零 insight
1636
+ // 而无人察觉 —— 当时日志里每一轮都是 ok。
1637
+ const tally = { produced: 0, noop: 0, partial: 0, skipped: 0 };
1638
+ let effectsTotal = 0;
1639
+ const degradedScopes: string[] = [];
1640
+ for (const scope of scopes) {
1641
+ if (Date.now() > deadline) {
1642
+ console.warn(
1643
+ `[dream] wall-clock 预算 ${Math.round(budgetMs / 60_000)} 分钟耗尽:已处理 ` +
1644
+ `${processed}/${scopes.length},剩余 ${scopes.length - processed} 个 scope 本轮跳过`,
1645
+ );
1646
+ break;
1647
+ }
1648
+ processed++;
1649
+ try {
1650
+ const result = await runDream({ store, llm, embedder, scope, force: Boolean(options.force), activityStatsPath, kgStore });
1651
+ tally[result.output.kind]++;
1652
+ effectsTotal += result.output.effectsWritten;
1653
+ if (result.output.degraded) degradedScopes.push(`${scope}=${result.output.degraded}`);
1654
+ console.log(`[scope=${scope}] ${formatDreamResult(result)}`);
1655
+ } catch (err) {
1656
+ // 单 scope 失败不阻断其他 scope。
1657
+ // 打完整 stack 而不只是 message:像「Array.from requires an array-like object」
1658
+ // 这种原生错误,只有 message 时根本定位不到是哪一行抛的——2026-07-18/19 连着两天
1659
+ // 因此 blocked,回头查日志除了这一句什么线索都没有,只能靠猜。
1660
+ // 分类基于 message(不是 stack):stack 里的文件路径不该参与判定。
1661
+ const message = err instanceof Error ? err.message : String(err);
1662
+ if (classifyDreamFailure(message) === "transient") transientFailures++;
1663
+ else fatalFailures++;
1664
+ console.error(`[scope=${scope}] dream failed: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
1665
+ }
1666
+ }
1667
+
1668
+ // 旧语义是「任一 scope 抛异常即 blocked」,在 475 个 scope 的规模下等于「有一个撞上
1669
+ // store-write 锁就整轮红」,而整轮红会让 shell 脚本重试 → 重跑全量。改为:代码/数据
1670
+ // 缺陷一票否决,环境性失败要成规模才算整轮失败。比例按已处理数算,不按总数。
1671
+ // 结构性降级一票否决:它不是「今天数据不成簇」(那是合法的 noop),而是某条路径
1672
+ // 整段断了 —— 向量管线空、或压根没配 LLM。判据只认确定性零值,不含任何阈值,
1673
+ // 所以不会因为数据分布变化而误伤(详见 assertDreamSweepHealth 的注释)。
1674
+ const structurallyBroken = degradedScopes.length > 0;
1675
+ if (structurallyBroken) {
1676
+ console.error(`[dream] ⚠ 结构性降级 ${degradedScopes.length} 个 scope:${degradedScopes.join(", ")}`);
1677
+ }
1678
+ const blocked = shouldBlockDreamRun({ totalScopes: processed, fatalFailures, transientFailures })
1679
+ || structurallyBroken;
1680
+ if (fatalFailures > 0 || transientFailures > 0) {
1681
+ console.log(`[dream] 失败统计:fatal=${fatalFailures} transient=${transientFailures}(已处理 ${processed})`);
1682
+ }
1683
+ // 这一行是 dream-consolidation.sh 存在性闸的锚点:STATUS=ok 却没有它 = 判失败。
1684
+ // 它防的是「断言本身被某次重构悄悄删掉」—— 一个不需要调参的自检。
1685
+ console.log(formatDreamMetrics({
1686
+ tally,
1687
+ degraded: degradedScopes.length,
1688
+ effects: effectsTotal,
1689
+ processed,
1690
+ total: scopes.length,
1691
+ }));
1692
+ console.log(blocked ? "[[DREAM_STATUS]] blocked" : "[[DREAM_STATUS]] ok");
1693
+ process.exit(blocked ? 1 : 0);
1694
+ }
1695
+
1696
+ try {
1697
+ const result = await runDream({
1698
+ store,
1699
+ llm,
1700
+ embedder,
1701
+ scope: options.scope,
1702
+ force: Boolean(options.force),
1703
+ activityStatsPath,
1704
+ kgStore,
1705
+ });
1706
+
1707
+ console.log(formatDreamResult(result));
1708
+ // 单 scope 也必须打产出计量:dream-consolidation.sh 的存在性闸对 STATUS=ok
1709
+ // 一视同仁地要求这行,而本分支此前从不打 —— 08-12 那道闸上线后,周日的
1710
+ // memory 轮次即使跑成功也会被判"产出断言缺失"。tally 在这里只有一格是 1。
1711
+ const tally = { produced: 0, noop: 0, partial: 0, skipped: 0 };
1712
+ tally[result.output.kind] = 1;
1713
+ console.log(formatDreamMetrics({
1714
+ tally,
1715
+ degraded: result.output.degraded ? 1 : 0,
1716
+ effects: result.output.effectsWritten,
1717
+ processed: 1,
1718
+ total: 1,
1719
+ }));
1720
+ console.log(result.ran ? "[[DREAM_STATUS]] ok" : "[[DREAM_STATUS]] skip");
1721
+ process.exit(0);
1722
+ } catch (err) {
1723
+ // 同 --auto 分支:带 stack,否则原生错误无从定位。
1724
+ console.error(`dream failed: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
1725
+ console.log("[[DREAM_STATUS]] blocked");
1726
+ process.exit(1);
1727
+ }
1728
+ });
1729
+
1505
1730
  // ─── stats ───────────────────────────────────────────────────────────────────
1506
1731
 
1507
1732
  program
@@ -1611,7 +1836,7 @@ program
1611
1836
  }
1612
1837
 
1613
1838
  const config = loadConfig();
1614
- const dbPath = resolve(metaDir(import.meta), "..", expandHome(config.dbPath));
1839
+ const dbPath = resolveDbPath(config);
1615
1840
 
1616
1841
  if (existsSync(dbPath)) {
1617
1842
  const { rmSync } = await import("node:fs");
@@ -1723,6 +1948,7 @@ program
1723
1948
  .option("--task <task>", "相关任务")
1724
1949
  .option("--tags <tags>", "逗号分隔 tags")
1725
1950
  .option("--tools <tools>", "逗号分隔 tools")
1951
+ .option("--idempotency-key <key>", "同一请求重试时用于替换旧 observation 的稳定键")
1726
1952
  .option("--json", "输出 JSON")
1727
1953
  .action(async (workflowId, summary, options) => {
1728
1954
  const store = new WorkflowObservationStore();
@@ -1736,6 +1962,7 @@ program
1736
1962
  task: options.task,
1737
1963
  tags: splitCsvOption(options.tags),
1738
1964
  tools: splitCsvOption(options.tools),
1965
+ idempotencyKey: options.idempotencyKey,
1739
1966
  });
1740
1967
  const stored = await store.save(record);
1741
1968
  if (options.json) {
@@ -261,50 +261,68 @@ export function buildContradictPatch(
261
261
  // Convenience wrappers (for callers who DO have a store)
262
262
  // ---------------------------------------------------------------------------
263
263
 
264
+ /** 单写通道端口:读改写经 store.patchMetadata per-id 队列,防并发覆盖。 */
265
+ type ConfidenceStorePort = {
266
+ patchMetadata(
267
+ id: string,
268
+ patchFn: (meta: Record<string, unknown>, entry: MemoryEntry) => Record<string, unknown>,
269
+ scopeFilter?: string[],
270
+ ): Promise<MemoryEntry | null>;
271
+ };
272
+
273
+ function applyConfidencePatch(
274
+ store: ConfidenceStorePort,
275
+ entryId: string,
276
+ scope: string,
277
+ build: (entry: MemoryEntry) => ConfidencePatch | null,
278
+ ): Promise<ConfidenceUpdate | null> {
279
+ let update: ConfidenceUpdate | null = null;
280
+ return store
281
+ .patchMetadata(
282
+ entryId,
283
+ (meta, entry) => {
284
+ const patch = build(entry);
285
+ if (!patch) return meta;
286
+ update = patch.update;
287
+ return patch.metadata;
288
+ },
289
+ [scope],
290
+ )
291
+ .then(result => (result === null ? null : update));
292
+ }
293
+
264
294
  /**
265
295
  * Confirm a memory and write back to store. Convenience wrapper.
266
296
  */
267
297
  export async function confirmMemory(
268
- store: { getById(id: string): Promise<MemoryEntry | null>; update(id: string, data: { metadata: string }, scopes: string[]): Promise<void> },
298
+ store: ConfidenceStorePort,
269
299
  entryId: string,
270
300
  scope: string,
271
301
  ): Promise<ConfidenceUpdate | null> {
272
- const entry = await store.getById(entryId);
273
- const patch = buildConfirmPatch(entry);
274
- if (!patch) return null;
275
- await store.update(entryId, { metadata: JSON.stringify(patch.metadata) }, [scope]);
276
- return patch.update;
302
+ return applyConfidencePatch(store, entryId, scope, entry => buildConfirmPatch(entry));
277
303
  }
278
304
 
279
305
  /**
280
306
  * Correct a memory and write back to store. Convenience wrapper.
281
307
  */
282
308
  export async function correctMemory(
283
- store: { getById(id: string): Promise<MemoryEntry | null>; update(id: string, data: { metadata: string }, scopes: string[]): Promise<void> },
309
+ store: ConfidenceStorePort,
284
310
  entryId: string,
285
311
  scope: string,
286
312
  correctedById?: string,
287
313
  ): Promise<ConfidenceUpdate | null> {
288
- const entry = await store.getById(entryId);
289
- const patch = buildCorrectPatch(entry, correctedById);
290
- if (!patch) return null;
291
- await store.update(entryId, { metadata: JSON.stringify(patch.metadata) }, [scope]);
292
- return patch.update;
314
+ return applyConfidencePatch(store, entryId, scope, entry => buildCorrectPatch(entry, correctedById));
293
315
  }
294
316
 
295
317
  /**
296
318
  * Contradict a memory and write back to store. Convenience wrapper.
297
319
  */
298
320
  export async function contradictMemory(
299
- store: { getById(id: string): Promise<MemoryEntry | null>; update(id: string, data: { metadata: string }, scopes: string[]): Promise<void> },
321
+ store: ConfidenceStorePort,
300
322
  entryId: string,
301
323
  scope: string,
302
324
  ): Promise<ConfidenceUpdate | null> {
303
- const entry = await store.getById(entryId);
304
- const patch = buildContradictPatch(entry);
305
- if (!patch) return null;
306
- await store.update(entryId, { metadata: JSON.stringify(patch.metadata) }, [scope]);
307
- return patch.update;
325
+ return applyConfidencePatch(store, entryId, scope, entry => buildContradictPatch(entry));
308
326
  }
309
327
 
310
328
  // ---------------------------------------------------------------------------
@@ -9,6 +9,8 @@ import {
9
9
  parseMetadataObject,
10
10
  } from "./memory-boundaries.js";
11
11
  import type { MemoryStore } from "./store.js";
12
+ import { archiveBeliefVersion, buildSupersedingBeliefMetadata } from "./belief-history.js";
13
+ import { parseEvolution } from "./memory-evolution.js";
12
14
  import {
13
15
  type ConflictCandidateInput,
14
16
  ConflictCandidateInputSchema,
@@ -95,7 +97,8 @@ function buildMergedConflictMetadata(
95
97
  }
96
98
 
97
99
  export interface ResolveConflictDeps {
98
- store: Pick<MemoryStore, "getById" | "update">;
100
+ // `store`/`upsert` are needed to archive the belief being replaced before overwriting it.
101
+ store: Pick<MemoryStore, "getById" | "update" | "store"> & Partial<Pick<MemoryStore, "upsert">>;
99
102
  embedder: Pick<Embedder, "embedPassage">;
100
103
  conflictStore: Pick<ConflictCandidateStore, "getById" | "replace">;
101
104
  }
@@ -144,13 +147,31 @@ export async function resolveConflictCandidate(
144
147
  }
145
148
 
146
149
  const vector = await deps.embedder.embedPassage(nextText);
150
+
151
+ // Conflict resolution is the most deliberate belief change there is — accepting the
152
+ // incoming version or merging both used to overwrite the existing row in place, losing
153
+ // precisely the "before" side of the decision. Archive it first.
154
+ const resolvedAtMs = Date.now();
155
+ const previousEvo = parseEvolution(existing.metadata, existing.timestamp);
156
+ const { historyId } = await archiveBeliefVersion(
157
+ { store: deps.store, embedder: deps.embedder },
158
+ existing,
159
+ { now: resolvedAtMs },
160
+ );
161
+
147
162
  const updated = await deps.store.update(existing.id, {
148
163
  text: nextText,
149
164
  vector,
150
165
  importance: nextImportance,
151
166
  category: nextCategory,
152
- metadata: nextMetadata,
153
- timestamp: Date.now(),
167
+ metadata: buildSupersedingBeliefMetadata(
168
+ nextMetadata,
169
+ previousEvo,
170
+ historyId,
171
+ resolvedAtMs,
172
+ `conflict:${input.resolution}`,
173
+ ),
174
+ timestamp: resolvedAtMs,
154
175
  });
155
176
  if (!updated) {
156
177
  throw new Error(`Failed to update durable memory ${existing.id}`);