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
package/src/cli.ts CHANGED
@@ -10,19 +10,20 @@
10
10
  */
11
11
 
12
12
  import { Command } from "commander";
13
- import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
13
+ import { existsSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "node:fs";
14
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
18
  import { PACKAGE_VERSION } from "./version.js";
19
- import type { RetrievalResult } from "./retriever.js";
20
19
  import { applyRetrievalProfile, listRetrievalProfiles } from "./retrieval-profiles.js";
21
20
  import { distillResults, formatExplainResults, formatSearchResults, selectBriefSeedResults, summarizeResults } from "./memory-output.js";
22
- import { archiveDirtyBriefAsset, assetSummaryLine, buildBriefAsset, buildPinAsset, getExportsDir, listDirtyBriefAssets, listMemoryAssets, listPinAssets, pinSummaryLine, saveBriefAsset, savePinAsset, writeExportArtifact } from "./memory-assets.js";
23
- import { indexAsset, indexPinnedAsset } from "./asset-sync.js";
24
- import { createComponents, createStoreOnly, expandHome, loadConfig, loadDotEnv, resolveDbPath, type LocalMemoryConfig } from "./runtime-config.js";
21
+ import { archiveDirtyBriefAsset, assetSummaryLine, buildBriefAsset, formatDirtyBriefEmptyMessage, formatDirtyBriefReadErrors, getExportsDir, listMemoryAssets, scanDirtyBriefAssets, listPinAssets, pinSummaryLine, saveBriefAsset, writeExportArtifact } from "./memory-assets.js";
22
+ import { indexAsset, pinMemory } from "./asset-sync.js";
23
+ import { createComponents, createStoreOnly, expandHome, loadConfig, loadDotEnv, resolveDataDir, resolveDbPath, type LocalMemoryConfig } from "./runtime-config.js";
25
24
  import { KGStore } from "./kg-store.js";
25
+ import { extractTriggerCandidates, normalizeTriggerTexts } from "./trigger-store.js";
26
+ import { isActiveMemory } from "./memory-evolution.js";
26
27
  import { isKGModeEnabled } from "./kg-extractor.js";
27
28
  import {
28
29
  formatDedupReasonSummary,
@@ -35,8 +36,11 @@ import {
35
36
  ingestMarkdownFiles,
36
37
  ingestObsidianVault,
37
38
  ingestConnectorFile,
39
+ parseCCTranscript,
38
40
  } from "./ingest.js";
39
41
  import { runDoctor, formatDoctorResults } from "./doctor.js";
42
+ import { archiveIngestedMinisFiles, isCompleteJsonl, settledCheck } from "./minis-archive.js";
43
+ import { isProcessed } from "./tracker.js";
40
44
  import { persistCaseMemory, persistMemory, persistWorkflowPattern } from "./capture-engine.js";
41
45
  import {
42
46
  scanMemoryPromotions,
@@ -46,10 +50,17 @@ import {
46
50
  formatSynthesisPromoteResult,
47
51
  } from "./memory-promotion.js";
48
52
  import { runDream, formatDreamResult, formatDreamMetrics, DEFAULT_DREAM_CONFIG, classifyDreamFailure, shouldBlockDreamRun, partitionAutoDreamScopes } from "./dream-pipeline.js";
49
- import { dreamBudgetMs } from "./env-config.js";
53
+ import { dataDir as envDataDir, dreamBudgetMs, transcriptIngest } from "./env-config.js";
54
+ import { planIngest } from "./ingest-plan.js";
50
55
  import { listScopesAboveThreshold, pruneWriteCounts } from "./activity-counter.js";
51
56
  import { isTranscriptScope } from "./memory-boundaries.js";
52
57
  import { maybeRunGc } from "./auto-gc.js";
58
+ import {
59
+ formatReconcileSummary,
60
+ reconcileMemoryDocuments,
61
+ undoMemoryReconcile,
62
+ type ReconcileOutcome,
63
+ } from "./memory-reconcile.js";
53
64
  import {
54
65
  CaseMemoryInputSchema,
55
66
  type CaseMemoryInput,
@@ -58,7 +69,9 @@ import {
58
69
  WorkflowPatternInputSchema,
59
70
  type WorkflowPatternInput,
60
71
  } from "./memory-schema.js";
61
- import { classifyLegacyScope, type MemoryEntry } from "./store.js";
72
+ import { classifyLegacyScope, loadLanceDB, MemoryStore, type MemoryEntry } from "./store.js";
73
+ import { backfillCheckpointMirror, openReadOnlyMirrorReader, purgeCheckpointMirror, type CheckpointMirrorDeps } from "./checkpoint-mirror.js";
74
+ import { autoRegisterBabelMemory } from "./language-hook.js";
62
75
  import { ConflictStatusSchema } from "./conflict-schema.js";
63
76
  import { resolveConflictCandidate } from "./conflict-engine.js";
64
77
  import { escalateConflicts } from "./conflict-escalation.js";
@@ -146,7 +159,8 @@ function resolveSourcePath(source: string, key: string): string {
146
159
  }
147
160
 
148
161
  function formatIngestSummary(result: IngestResult): string {
149
- return `${result.filesProcessed} files, ${result.chunksIngested} ingested, ${result.chunksDeduped} deduped (${formatDedupReasonSummary(result)}), ${result.errors.length} errors`;
162
+ const subagentNote = result.subagentFilesSkipped ? `, ${result.subagentFilesSkipped} subagent files skipped` : "";
163
+ return `${result.filesProcessed} files, ${result.chunksIngested} ingested, ${result.chunksDeduped} deduped (${formatDedupReasonSummary(result)}), ${result.errors.length} errors${subagentNote}`;
150
164
  }
151
165
 
152
166
  function maybeWarnHighCCDedupRate(result: IngestResult): void {
@@ -268,6 +282,42 @@ function parseRequiredLimitOption(value: string | undefined, field: string, min
268
282
  return Math.min(max, Math.max(min, parsed));
269
283
  }
270
284
 
285
+ /**
286
+ * 记忆对账动手前的路径核对。锁与审计日志走 envConfig.dataDir()(RECALLNEST_DATA_DIR,否则当前目录下的 data),
287
+ * 库走 config.dbPath(相对路径按代码所在仓库解析)——两者必须是同一个 data 目录,否则对账拿的锁不是 dream / GC
288
+ * 用的那把、forget 记录也读错地方;从别的工作区跑时尤其容易错位(第三轮互审 N1)。库里必须已有 memories 表,
289
+ * 免得路径写错时对着一个新建的空库「对账」。
290
+ */
291
+ function checkReconcilePaths(
292
+ config: LocalMemoryConfig,
293
+ options: { requireTable?: boolean } = {},
294
+ ): { ok: boolean; reason: string; dataDir: string; dbPath: string } {
295
+ const dbPath = resolveDbPath(config);
296
+ const dataDir = resolve(envDataDir());
297
+ const real = (p: string): string => {
298
+ try { return realpathSync(p); } catch { return p; }
299
+ };
300
+ if (real(dataDir) !== real(resolveDataDir(config))) {
301
+ return { ok: false, reason: `锁与审计目录 ${dataDir} 和库所在目录 ${resolveDataDir(config)} 不一致(从仓库根目录运行,或设 RECALLNEST_DATA_DIR)`, dataDir, dbPath };
302
+ }
303
+ if (options.requireTable !== false && !existsSync(join(dbPath, "memories.lance"))) {
304
+ return { ok: false, reason: `库目录里没有 memories 表:${dbPath}`, dataDir, dbPath };
305
+ }
306
+ return { ok: true, reason: "", dataDir, dbPath };
307
+ }
308
+
309
+ function reconcileToIngestResult(outcome: ReconcileOutcome): IngestResult {
310
+ return {
311
+ source: "memory",
312
+ filesProcessed: outcome.plan?.currentFiles ?? 0,
313
+ chunksIngested: outcome.applied?.inserted ?? 0,
314
+ chunksSkipped: 0,
315
+ chunksDeduped: 0,
316
+ dedupReasonCounts: { hard: 0, exact: 0, "llm-skip": 0, "llm-merge": 0, unique: 0 },
317
+ errors: outcome.applied?.insertErrors ?? [],
318
+ };
319
+ }
320
+
271
321
  function toScopeFilter(scope?: string): string[] | undefined {
272
322
  const trimmed = typeof scope === "string" ? scope.trim() : "";
273
323
  return trimmed ? [trimmed] : undefined;
@@ -459,18 +509,6 @@ function collectRepeatedOption(value: string, previous: string[] = []): string[]
459
509
  return [...previous, value];
460
510
  }
461
511
 
462
- function entryToRetrievalResult(entry: Awaited<ReturnType<MemoryStore["get"]>>): RetrievalResult {
463
- if (!entry) {
464
- throw new Error("Memory entry not found.");
465
- }
466
- return {
467
- entry,
468
- score: entry.importance || 0.7,
469
- sources: {
470
- fused: { score: entry.importance || 0.7 },
471
- },
472
- };
473
- }
474
512
 
475
513
  // ─── search ──────────────────────────────────────────────────────────────────
476
514
 
@@ -554,17 +592,17 @@ program
554
592
  return;
555
593
  }
556
594
 
557
- const pinnedImportance = Math.max(entry.importance || 0.7, 0.95);
558
- await store.update(entry.id, { importance: pinnedImportance }, scopeSelection.scopeFilter);
559
-
560
- const asset = buildPinAsset(entryToRetrievalResult(entry), {
595
+ const pinned = await pinMemory(store, embedder, entry, scopeSelection.scopeFilter, {
561
596
  title: options.title,
562
597
  summary: options.summary,
563
598
  query: options.query,
564
599
  profile: options.profile,
565
600
  });
566
- const path = savePinAsset(asset);
567
- await indexPinnedAsset(store, embedder, asset);
601
+ if (!pinned) {
602
+ console.log(`Memory not found: ${memoryId}`);
603
+ return;
604
+ }
605
+ const { asset, path } = pinned;
568
606
 
569
607
  console.log([
570
608
  `Pinned : ${asset.id.slice(0, 8)}`,
@@ -657,9 +695,11 @@ program
657
695
  .description("归档旧规则生成的脏 brief,并从索引删除对应 asset scope")
658
696
  .option("--apply", "执行清理;默认只预览")
659
697
  .action(async (options) => {
660
- const dirty = listDirtyBriefAssets();
698
+ const scan = scanDirtyBriefAssets();
699
+ const dirty = scan.items;
700
+ const readErrors = formatDirtyBriefReadErrors(scan.readErrors);
661
701
  if (dirty.length === 0) {
662
- console.log("No dirty briefs found.");
702
+ console.log(formatDirtyBriefEmptyMessage(scan));
663
703
  return;
664
704
  }
665
705
 
@@ -669,6 +709,7 @@ program
669
709
  console.log(`${item.id.slice(0, 8)} ${item.title} [${item.scope}] ${item.reasons.join("; ")}`);
670
710
  }
671
711
  console.log("\nRun with --apply to archive files and delete their indexed asset scopes.");
712
+ if (readErrors) console.log(`\n${readErrors}`);
672
713
  return;
673
714
  }
674
715
 
@@ -688,6 +729,7 @@ program
688
729
  `Archived : ${archived}`,
689
730
  `Index rows : ${deleted}`,
690
731
  ].join("\n"));
732
+ if (readErrors) console.log(`\n${readErrors}`);
691
733
  });
692
734
 
693
735
  program
@@ -1103,6 +1145,109 @@ conflictsCommand
1103
1145
  console.log(formatConflictResolution(result));
1104
1146
  });
1105
1147
 
1148
+ const kgFilterHelp = (cmd: string) => `
1149
+ 过滤选项(默认全关;一个都不传时输出与不带这组选项时完全一样):
1150
+ ${cmd === "kg-compare" ? "只过滤分组候选一侧,再拿过滤后的候选做双向对照;正则一侧不变。\n " : ""}依次过 谓词白名单 → 函数型判定 → 证据门槛,输出 funnel:每一步还剩多少组,没启用的标 skipped。
1151
+ JSON 另带 predicateStats:本 scope 每个谓词的 subjects / pairs / functionality / multiValuedSubjects,
1152
+ 按 functionality 降序。
1153
+ functionality = 用到该谓词的不同 subject 数 ÷ 不同 (subject, 归一化 object) 对数,取值 (0, 1],
1154
+ 越接近 1 越像「一个主体同一时间只有一个值」(located_in 这类)。有候选组的谓词恒 < 1。
1155
+
1156
+ 示例:
1157
+ conflicts ${cmd} --scope memory:pivot --functional-min 0.8 --json
1158
+ conflicts ${cmd} --scope memory:pivot --functional-min 0.8 --min-second-mentions 2
1159
+ conflicts ${cmd} --scope memory:pivot --predicates located_in,owned_by
1160
+ `;
1161
+
1162
+ /** 四个过滤选项挂到命令上(kg-candidates / kg-compare 共用)。 */
1163
+ function withKGFilterOptions(command: Command): Command {
1164
+ const kgFilterHelpText = kgFilterHelp(command.name());
1165
+ return command
1166
+ .option("--predicates <list>", "谓词白名单,逗号分隔,精确匹配(谓词本身不归一化)")
1167
+ .option("--functional-min <ratio>", "只留 functionality ≥ ratio 的谓词上的候选组,ratio ∈ (0, 1],例如 0.8")
1168
+ .option("--functional-min-subjects <n>", "配合 --functional-min:谓词的 subject 少于 n 个按不满足处理(默认 3)")
1169
+ .option("--min-second-mentions <n>", "只留第二多取值的 mention_count ≥ n 的组(默认 1,即不过滤)")
1170
+ .addHelpText("after", kgFilterHelpText);
1171
+ }
1172
+
1173
+ withKGFilterOptions(
1174
+ conflictsCommand
1175
+ .command("kg-candidates")
1176
+ .description("只读:KG 三元组按 (subject, predicate) 分组的多值候选(影子期,候选非判决)")
1177
+ .requiredOption("--scope <scope>", "scope(精确匹配)")
1178
+ .option("-n, --limit <n>", "按排序取前 n 组", "20")
1179
+ .option("--sample <n>", "改为随机抽 n 组(确定性,配 --seed)")
1180
+ .option("--seed <seed>", "抽样种子", "1")
1181
+ .option("--json", "JSON 格式输出"),
1182
+ ).action(async (options) => {
1183
+ const {
1184
+ filterKGCandidates,
1185
+ findKGContradictionCandidates,
1186
+ formatCandidateReport,
1187
+ hasCandidateFilter,
1188
+ loadScopeTriples,
1189
+ parseCandidateFilterArgs,
1190
+ sampleCandidates,
1191
+ } = await import("./kg-contradiction.js");
1192
+ const filterOpts = parseCandidateFilterArgs(options);
1193
+ const dbPath = resolveDbPath(loadConfig());
1194
+ const triples = await loadScopeTriples(dbPath, options.scope);
1195
+ const all = findKGContradictionCandidates(triples);
1196
+ const filtered = hasCandidateFilter(filterOpts) ? filterKGCandidates(all, triples, filterOpts) : undefined;
1197
+ const candidates = filtered ? filtered.candidates : all;
1198
+ const sampling = options.sample !== undefined;
1199
+ const n = sampling ? parseRequiredLimitOption(options.sample, "--sample", 1, 1000) : parseLimitOption(options.limit, 20, 1, 1000);
1200
+ const seed = Number.parseInt(String(options.seed), 10) || 1;
1201
+ const shown = sampling ? sampleCandidates(candidates, n, seed) : candidates.slice(0, n);
1202
+ const mode = sampling ? `random sample n=${n} seed=${seed}` : `top ${n}`;
1203
+
1204
+ if (options.json) {
1205
+ const filterFields = filtered ? { funnel: filtered.funnel, predicateStats: filtered.predicateStats } : {};
1206
+ console.log(JSON.stringify({ scope: options.scope, tripleCount: triples.length, candidateCount: candidates.length, mode, ...filterFields, candidates: shown }, null, 2));
1207
+ return;
1208
+ }
1209
+ console.log(formatCandidateReport(options.scope, triples.length, candidates.length, shown, mode, filtered?.funnel));
1210
+ });
1211
+
1212
+ withKGFilterOptions(
1213
+ conflictsCommand
1214
+ .command("kg-compare")
1215
+ .description("只读:同一 scope 上现有正则矛盾检测 (memory lint) 与 KG 分组候选的双向对照")
1216
+ .requiredOption("--scope <scope>", "scope(精确匹配)")
1217
+ .option("-n, --limit <n>", "文本输出每节最多列几条;0 = 全部", "0")
1218
+ .option("--json", "JSON 格式输出(始终完整)"),
1219
+ ).action(async (options) => {
1220
+ const {
1221
+ compareRegexWithCandidates,
1222
+ filterKGCandidates,
1223
+ findKGContradictionCandidates,
1224
+ formatComparisonReport,
1225
+ hasCandidateFilter,
1226
+ loadRegexInput,
1227
+ loadScopeTriples,
1228
+ parseCandidateFilterArgs,
1229
+ regexHitPairs,
1230
+ } = await import("./kg-contradiction.js");
1231
+ const filterOpts = parseCandidateFilterArgs(options);
1232
+ const dbPath = resolveDbPath(loadConfig());
1233
+ const [triples, regexInput] = await Promise.all([loadScopeTriples(dbPath, options.scope), loadRegexInput(dbPath, options.scope)]);
1234
+ const all = findKGContradictionCandidates(triples);
1235
+ // 过滤只作用在分组候选一侧;正则一侧与判断「有没有三元组」用的全部三元组都不变
1236
+ const filtered = hasCandidateFilter(filterOpts) ? filterKGCandidates(all, triples, filterOpts) : undefined;
1237
+ const candidates = filtered ? filtered.candidates : all;
1238
+ const pairs = regexHitPairs(regexInput.entries);
1239
+ const cmp = compareRegexWithCandidates(pairs, candidates, triples, new Set(regexInput.entries.map((e) => e.id)));
1240
+ const meta = { tripleCount: triples.length, regexInputCount: regexInput.entries.length, scanLimited: regexInput.scanLimited };
1241
+
1242
+ if (options.json) {
1243
+ const filterFields = filtered ? { funnel: filtered.funnel, predicateStats: filtered.predicateStats } : {};
1244
+ console.log(JSON.stringify({ scope: options.scope, ...meta, ...filterFields, ...cmp }, null, 2));
1245
+ return;
1246
+ }
1247
+ const limit = Number.parseInt(String(options.limit), 10);
1248
+ console.log(formatComparisonReport(options.scope, meta, cmp, Number.isFinite(limit) && limit > 0 ? limit : 0, filtered?.funnel));
1249
+ });
1250
+
1106
1251
  program
1107
1252
  .command("seed-patterns [file]")
1108
1253
  .description("批量写入 continuity / workflow pattern seeds")
@@ -1420,6 +1565,51 @@ program
1420
1565
  return;
1421
1566
  }
1422
1567
  const results: any[] = [];
1568
+ // 记忆对账没执行 / 出错 / 护栏拦下(24 小时去抖后)→ 整条 ingest 以退出码 3 结束,调用脚本据此发专门的报警
1569
+ let memoryReconcileAlert = false;
1570
+ // 对话原文 2026-10-06 起默认不入库(RECALLNEST_TRANSCRIPT_INGEST=on 才入);这一轮跑哪些来源见 ingest-plan.ts
1571
+ const plan = planIngest(source, transcriptIngest());
1572
+ if (plan.skippedTranscriptSources.length > 0) {
1573
+ console.log(`\n⏸ 对话入库已关(RECALLNEST_TRANSCRIPT_INGEST 没有设为 on):${plan.skippedTranscriptSources.join(" / ")} 不切片、不嵌入、不写库`);
1574
+ console.log(" 原文在 Deja 里查;Minis 投递目录的文件照样挪进存档;记忆文件照常。要重开见 env-config.ts transcriptIngest");
1575
+ }
1576
+
1577
+ // Minis 投递目录 → data/minis-archive(Deja 从那里读,记忆库不读它),原因见 minis-archive.ts 文件头。
1578
+ // 入库开着:先入库,再只挪台账里记着已入库的。入库关着:没有「已入库」可等,放稳了(至少 60 秒没被改过)、
1579
+ // 整个文件写完了、能解析出对话的就挪——这里要是照旧拿台账当闸,文件永远不会被标成已处理,也就永远进不了 Deja,而且不报错。
1580
+ const runMinis = async (ingestOpts?: { limit?: number; verbose: boolean; noDedup: boolean; llm: typeof llm | null; recentHours?: number }) => {
1581
+ const minisSource = config.sources.minis;
1582
+ if (!minisSource) return;
1583
+ const minisPath = resolveSourcePath(minisSource.path, "minis");
1584
+ if (!existsSync(minisPath)) {
1585
+ if (source === "minis") console.log(`⚠️ Minis 目录不存在: ${minisPath}`);
1586
+ return;
1587
+ }
1588
+ if (plan.minis === "ingest-then-archive" && ingestOpts) {
1589
+ console.log("📱 导入 Minis 对话...");
1590
+ const r = await ingestCCTranscripts(store, embedder, minisPath, { ...ingestOpts, scopePrefix: "minis" });
1591
+ results.push(r);
1592
+ console.log(` ✅ Minis: ${formatIngestSummary(r)}`);
1593
+ } else {
1594
+ console.log("📱 Minis 对话:不入库,只挪存档...");
1595
+ }
1596
+ const archiveDir = resolve(metaDir(import.meta), "..", "data", "minis-archive");
1597
+ const moved = archiveIngestedMinisFiles(
1598
+ minisPath,
1599
+ archiveDir,
1600
+ plan.minis === "ingest-then-archive" ? isProcessed : settledCheck(),
1601
+ // 关着时多看一条「文件写完了」:解析会跳过残缺行,半截文件也解析得出对话
1602
+ (p) => (plan.minis === "ingest-then-archive" || isCompleteJsonl(p)) && parseCCTranscript(p).length > 0,
1603
+ );
1604
+ if (moved.archived.length > 0 || moved.errors.length > 0) {
1605
+ const failed = moved.errors.length > 0
1606
+ ? `,${moved.errors.length} 个出错、原件留在原处:${moved.errors.join(";")}`
1607
+ : "";
1608
+ console.log(` 📦 Minis 存档: ${moved.archived.length} 个移入 data/minis-archive${failed}`);
1609
+ }
1610
+ };
1611
+ // 只挪存档用不着嵌入接口,所以排在接口预检之前:接口挂了(预检失败会直接退出)也不耽误对话进 Deja
1612
+ if (plan.minis === "archive-only") await runMinis();
1423
1613
 
1424
1614
  // Pre-flight: validate embedding API before processing any files
1425
1615
  console.log("\n🔑 验证 Embedding API...");
@@ -1449,8 +1639,8 @@ program
1449
1639
  console.log(" ⚠️ LLM: 未配置,原始对话将跳过不存入(配置 config.json → llm 以启用)");
1450
1640
  }
1451
1641
 
1452
- // Drain pending queue if LLM is available
1453
- if (effectiveLlm) {
1642
+ // Drain pending queue if LLM is available(队列里是对话切片,回填等于入库,跟着对话入库的开关走)
1643
+ if (effectiveLlm && plan.drainPendingQueue) {
1454
1644
  const { drainPendingQueue } = await import("./ingest.js");
1455
1645
  const drained = await drainPendingQueue(store, embedder, effectiveLlm);
1456
1646
  if (drained.processed > 0) {
@@ -1469,7 +1659,7 @@ program
1469
1659
 
1470
1660
  // CC Transcripts
1471
1661
  // CC 按 cwd 编码 project 子目录,所有有 jsonl 的子目录都要 ingest(不只是 jsonl 最多的那个)
1472
- if (source === "all" || source === "cc") {
1662
+ if (plan.transcriptSources.includes("cc")) {
1473
1663
  console.log("📝 导入 Claude Code 对话...");
1474
1664
  const ccSource = config.sources.cc;
1475
1665
  if (ccSource) {
@@ -1489,7 +1679,7 @@ program
1489
1679
  }
1490
1680
 
1491
1681
  // Codex Sessions
1492
- if (source === "all" || source === "codex") {
1682
+ if (plan.transcriptSources.includes("codex")) {
1493
1683
  console.log("🤖 导入 Codex 对话...");
1494
1684
  const r = await ingestCodexSessions(store, embedder, ingestOpts);
1495
1685
  results.push(r);
@@ -1497,7 +1687,7 @@ program
1497
1687
  }
1498
1688
 
1499
1689
  // Kimi Code Sessions (wire.jsonl 事件流,~/.kimi-code/sessions/)
1500
- if (source === "all" || source === "kimi") {
1690
+ if (plan.transcriptSources.includes("kimi")) {
1501
1691
  console.log("🌙 导入 Kimi Code 对话...");
1502
1692
  const r = await ingestKimiSessions(store, embedder, ingestOpts);
1503
1693
  results.push(r);
@@ -1505,7 +1695,7 @@ program
1505
1695
  }
1506
1696
 
1507
1697
  // Gemini Sessions (JSON format under ~/.gemini/tmp/*/chats/)
1508
- if (source === "all" || source === "gemini") {
1698
+ if (plan.transcriptSources.includes("gemini")) {
1509
1699
  console.log("💎 导入 Gemini 对话...");
1510
1700
  const r = await ingestGeminiSessions(store, embedder, {
1511
1701
  limit,
@@ -1519,7 +1709,7 @@ program
1519
1709
  }
1520
1710
 
1521
1711
  // Claude Desktop / claude.ai web conversations (CC transcript format)
1522
- if (source === "all" || source === "desktop") {
1712
+ if (plan.transcriptSources.includes("desktop")) {
1523
1713
  const desktopSource = config.sources.desktop;
1524
1714
  if (desktopSource) {
1525
1715
  const desktopPath = resolve(metaDir(import.meta), "..", desktopSource.path);
@@ -1538,29 +1728,14 @@ program
1538
1728
  // Minis (iPhone) —— 手机 agent 自己把对话写进这个目录,ingest 顺带扫走。
1539
1729
  // 为什么是独立源而不是复用 desktop:scope 前缀决定了 memory-boundaries 怎么降权,
1540
1730
  // 混在 cc: 里的话来源在库里就分辨不出来(那正是 2026-08-12 起挂着的缺口)。
1541
- if (source === "all" || source === "minis") {
1542
- const minisSource = config.sources.minis;
1543
- if (minisSource) {
1544
- const minisPath = resolveSourcePath(minisSource.path, "minis");
1545
- if (existsSync(minisPath)) {
1546
- console.log("📱 导入 Minis 对话...");
1547
- const r = await ingestCCTranscripts(store, embedder, minisPath, {
1548
- ...ingestOpts,
1549
- scopePrefix: "minis",
1550
- });
1551
- results.push(r);
1552
- console.log(` ✅ Minis: ${formatIngestSummary(r)}`);
1553
- } else if (source === "minis") {
1554
- console.log(`⚠️ Minis 目录不存在: ${minisPath}`);
1555
- }
1556
- }
1557
- }
1731
+ if (plan.minis === "ingest-then-archive") await runMinis(ingestOpts);
1558
1732
 
1559
1733
  // Memory markdown files
1560
- if (source === "all" || source === "memory") {
1734
+ if (plan.memory) {
1561
1735
  console.log("📚 导入记忆文件...");
1562
1736
  const memSource = config.sources.memory;
1563
- if (memSource) {
1737
+ if (memSource && memSource.path === "auto") {
1738
+ // auto 路径保持旧行为(追加式导入 + 全库判重);对账只在显式配置路径时启用(互审 K4)
1564
1739
  const memPath = resolveSourcePath(memSource.path, "memory");
1565
1740
  const r = await ingestMarkdownFiles(store, embedder, memPath, "memory", {
1566
1741
  verbose,
@@ -1570,6 +1745,36 @@ program
1570
1745
  });
1571
1746
  results.push(r);
1572
1747
  console.log(` ✅ Memory: ${formatIngestSummary(r)}`);
1748
+ } else if (memSource) {
1749
+ // 显式路径:按文件对账(src/memory-reconcile.ts)。它每轮全量比对现行文件,--recent / --limit 不适用(互审 K2)
1750
+ if (recentHours !== undefined || limit !== undefined) {
1751
+ console.log(" ℹ️ 记忆文件走对账(每轮全量比对现行文件),--recent / --limit 对它不生效");
1752
+ }
1753
+ const paths = checkReconcilePaths(config);
1754
+ if (!paths.ok) {
1755
+ console.log(` ⚠️ Memory: 对账没有执行——${paths.reason}`);
1756
+ memoryReconcileAlert = true;
1757
+ } else {
1758
+ try {
1759
+ const outcome = await reconcileMemoryDocuments({
1760
+ store,
1761
+ embedder,
1762
+ llm: effectiveLlm,
1763
+ auditLogger: createAuditLogger(),
1764
+ memDir: resolveSourcePath(memSource.path, "memory"),
1765
+ dataDir: paths.dataDir,
1766
+ explicitPath: true,
1767
+ apply: true,
1768
+ });
1769
+ results.push(reconcileToIngestResult(outcome));
1770
+ console.log(` ✅ Memory: ${formatReconcileSummary(outcome)}`);
1771
+ if (outcome.journalPath) console.log(` 对账日志: ${outcome.journalPath}`);
1772
+ if (outcome.alert) memoryReconcileAlert = true;
1773
+ } catch (err) {
1774
+ console.log(` ❌ Memory: 对账出错——${err instanceof Error ? err.message : String(err)}`);
1775
+ memoryReconcileAlert = true;
1776
+ }
1777
+ }
1573
1778
  }
1574
1779
  }
1575
1780
 
@@ -1629,6 +1834,233 @@ program
1629
1834
  ` 总计: ${totalChunks} chunks 已索引, ${totalDeduped} chunks 去重 (hard:${totalDedupReasons.hard}, exact:${totalDedupReasons.exact}, llm-skip:${totalDedupReasons["llm-skip"]}, llm-merge:${totalDedupReasons["llm-merge"]}), ${totalErrors} errors`,
1630
1835
  );
1631
1836
  console.log();
1837
+ if (memoryReconcileAlert) {
1838
+ console.log("⚠️ 记忆对账需要人看(见上面 Memory: 行),以退出码 3 结束");
1839
+ process.exitCode = 3;
1840
+ }
1841
+ });
1842
+
1843
+ // ─── checkpoint-mirror-backfill ───────────────────────────────────────────────
1844
+ // checkpoint 镜像(src/checkpoint-mirror.ts)的一次性回填:每场会话取最新的 rich checkpoint 写进 scope
1845
+ // `checkpoint`,已是这一版的跳过,可重复跑。输出只有计数,不打印 checkpoint 正文。
1846
+ // --db-path / --checkpoints-dir 只给演练用:指向生产库以外的副本;指向配置里的生产库会被拒绝(生产库不传这个参数)。
1847
+
1848
+ program
1849
+ .command("checkpoint-mirror-backfill")
1850
+ .description("把现有 checkpoint 按会话取最新一份镜像进记忆库(scope checkpoint;默认写库,--dry-run 只数)")
1851
+ .option("--dry-run", "只数会做什么,不嵌入、不写库")
1852
+ .option("--db-path <dir>", "演练用:写进这个 LanceDB 副本,而不是配置里的生产库")
1853
+ .option("--checkpoints-dir <dir>", "演练用:从这个目录读 checkpoint,而不是 data/session-checkpoints")
1854
+ .action(async (options: { dryRun?: boolean; dbPath?: string; checkpointsDir?: string }) => {
1855
+ const config = loadConfig();
1856
+ // The mirror's write lock lives under the data dir resolved from the working directory, the
1857
+ // database under the repo; both must be the ones the running MCP / API processes use, or the
1858
+ // backfill and a live save would hold different locks (互审 R3 N1). Run from the repo root.
1859
+ // A drill copy given on the command line — not config.dbPath, which only resolveDbPath reads.
1860
+ const drillCopy = options.dbPath;
1861
+ const paths = checkReconcilePaths(config, { requireTable: !drillCopy });
1862
+ if (!paths.ok) {
1863
+ console.error(`❌ ${paths.reason}`);
1864
+ process.exitCode = 2;
1865
+ return;
1866
+ }
1867
+ const { store: prodStore, embedder } = createComponents(config);
1868
+ let dbPath = paths.dbPath;
1869
+ if (drillCopy) {
1870
+ const target = realpathSync(resolve(expandHome(drillCopy)));
1871
+ if (existsSync(paths.dbPath) && target === realpathSync(paths.dbPath)) {
1872
+ console.error("❌ --db-path 指向的就是生产库;写生产库不传这个参数");
1873
+ process.exitCode = 2;
1874
+ return;
1875
+ }
1876
+ if (!existsSync(join(target, "memories.lance"))) {
1877
+ console.error(`❌ 库目录里没有 memories 表:${target}`);
1878
+ process.exitCode = 2;
1879
+ return;
1880
+ }
1881
+ dbPath = target;
1882
+ }
1883
+ const { SessionCheckpointStore } = await import("./session-store.js");
1884
+ const checkpointStore = options.checkpointsDir
1885
+ ? new SessionCheckpointStore(resolve(expandHome(options.checkpointsDir)))
1886
+ : new SessionCheckpointStore();
1887
+ const records = await checkpointStore.listRecent({ limit: Number.MAX_SAFE_INTEGER });
1888
+ console.log(`库: ${dbPath}`);
1889
+ console.log(`checkpoint 目录: ${checkpointStore.dataDir}`);
1890
+ let deps: CheckpointMirrorDeps;
1891
+ if (options.dryRun) {
1892
+ // Plain reads only — MemoryStore's initialization could add columns or build an index.
1893
+ const reader = await openReadOnlyMirrorReader(dbPath);
1894
+ deps = {
1895
+ store: { getById: (id: string) => reader.getById(id), upsertUnlessNewer: async () => { throw new Error("dry run does not write"); } },
1896
+ embedder,
1897
+ };
1898
+ } else {
1899
+ // Same tokenizer the MCP server registers at startup, so backfilled rows get the same
1900
+ // language / fts_text as rows the MCP tool writes (互审 R3 N3).
1901
+ await autoRegisterBabelMemory();
1902
+ deps = { store: drillCopy ? new MemoryStore({ dbPath, vectorDim: embedder.dimensions }) : prodStore, embedder };
1903
+ }
1904
+ const result = await backfillCheckpointMirror(deps, records, {
1905
+ dryRun: Boolean(options.dryRun),
1906
+ onProgress: (done, total) => {
1907
+ if (done % 50 === 0 || done === total) console.log(` ${done}/${total}`);
1908
+ },
1909
+ });
1910
+ console.log(JSON.stringify({ dryRun: Boolean(options.dryRun), ...result }, null, 2));
1911
+ if ((result.statuses.failed ?? 0) > 0) process.exitCode = 1;
1912
+ });
1913
+
1914
+ // ─── checkpoint-mirror-purge ─────────────────────────────────────────────────
1915
+ // 撤回 checkpoint 镜像:建 data/checkpoint-mirror.off 停掉所有进程的镜像写入(每次写都读它),
1916
+ // 再持镜像写锁按 scope 精确等于 checkpoint 删除。不带 --yes 只数不删。恢复镜像:删掉开关文件。
1917
+
1918
+ program
1919
+ .command("checkpoint-mirror-purge")
1920
+ .description("撤回 checkpoint 镜像:停写并删除 scope 恰为 checkpoint 的全部行(不带 --yes 只数)")
1921
+ .option("--yes", "确认执行")
1922
+ .action(async (options: { yes?: boolean }) => {
1923
+ const config = loadConfig();
1924
+ const paths = checkReconcilePaths(config);
1925
+ if (!paths.ok) {
1926
+ console.error(`❌ ${paths.reason}`);
1927
+ process.exitCode = 2;
1928
+ return;
1929
+ }
1930
+ console.log(`库: ${paths.dbPath}`);
1931
+ if (!options.yes) {
1932
+ const table = await (await (await loadLanceDB()).connect(paths.dbPath)).openTable("memories");
1933
+ console.log(`scope = 'checkpoint' 的行数: ${await table.countRows("scope = 'checkpoint'")}(加 --yes 才会停写并删除)`);
1934
+ return;
1935
+ }
1936
+ const result = await purgeCheckpointMirror(paths.dbPath);
1937
+ console.log(JSON.stringify(result, null, 2));
1938
+ console.log("开关文件 data/checkpoint-mirror.off 已建,镜像写入已停;要恢复,删掉这个文件。几分钟后再跑一次本命令(不带 --yes)确认仍是 0。");
1939
+ if (result.rowsAfter !== 0) process.exitCode = 1;
1940
+ });
1941
+
1942
+ // ─── reconcile-memory ────────────────────────────────────────────────────────
1943
+ // 记忆文件对账的手动入口:默认只打印计划不写库;--apply 执行;--undo 按对账日志逐条撤销。
1944
+ // 定时导入(ingest)在显式配置 sources.memory.path 时自动做同一件事,这里是给首轮存量清理、
1945
+ // 护栏拦下后人工放行、以及出问题时撤销用的。输出只有数量、文件名和 id 前 8 位,不打印切片正文。
1946
+
1947
+ program
1948
+ .command("reconcile-memory")
1949
+ .description("记忆文件对账:让 memory 源的文档切片与现行记忆文件一一对应(默认只看计划,不写库)")
1950
+ .option("--apply", "执行:插入 → 恢复 → 下架")
1951
+ .option("--max-retire <n>", "本次下架上限(默认 max(300, 活跃文档切片数的 25%),也可用 RECALLNEST_MEMORY_RECONCILE_MAX_RETIRE)")
1952
+ .option("--undo <journal>", "按对账日志(data/reconcile-journals/*.jsonl)逐条撤销")
1953
+ .option("--json", "以 JSON 输出计划与结果")
1954
+ .action(async (options) => {
1955
+ const config = loadConfig();
1956
+ const paths = checkReconcilePaths(config);
1957
+ console.log(`库: ${paths.dbPath}`);
1958
+ console.log(`数据目录(锁 / 审计 / 对账日志): ${paths.dataDir}`);
1959
+ if (!paths.ok) {
1960
+ console.error(`❌ ${paths.reason}`);
1961
+ process.exitCode = 2;
1962
+ return;
1963
+ }
1964
+ const { store, embedder, llm } = createComponents(config);
1965
+
1966
+ if (options.undo) {
1967
+ const journalPath = resolve(String(options.undo));
1968
+ let r: Awaited<ReturnType<typeof undoMemoryReconcile>>;
1969
+ try {
1970
+ r = await undoMemoryReconcile({ store, auditLogger: createAuditLogger() }, journalPath, { auditPath: join(paths.dataDir, "audit.jsonl") });
1971
+ } catch (err) {
1972
+ console.error(`❌ ${err instanceof Error ? err.message : String(err)}`);
1973
+ process.exitCode = 2;
1974
+ return;
1975
+ }
1976
+ const ids8 = (ids: string[]) => `${ids.slice(0, 20).map((id) => id.slice(0, 8)).join(" ")}${ids.length > 20 ? ` …(共 ${ids.length})` : ""}`;
1977
+ console.log(`撤销完成:写回 ${r.restored} 条,本轮插入的 ${r.insertsRetired} 条改为下架,期间被改过而跳过 ${r.skippedConflict.length} 条,合并链依赖未满足而保留 ${r.skippedDependency.length} 条,文字已被 forget 而保留非活跃 ${r.skippedForgotten.length} 条`);
1978
+ if (r.skippedConflict.length > 0) console.log(` 被改过而跳过的 id(前 8 位): ${ids8(r.skippedConflict)}`);
1979
+ if (r.skippedDependency.length > 0) console.log(` 保留的合并链成员 id(它原来指向的那一行已不活跃、或本次撤销会把它撤掉,撤了会断链): ${ids8(r.skippedDependency)}`);
1980
+ if (r.skippedForgotten.length > 0) console.log(` 文字已被 forget、没撤回活跃的 id: ${ids8(r.skippedForgotten)}`);
1981
+ if (r.unparsedLines > 0) console.log(` ⚠️ 日志里有 ${r.unparsedLines} 行解析不了(多半是写到一半的截断尾行),已跳过`);
1982
+ console.log(`撤销日志: ${r.undoJournalPath}`);
1983
+ console.log("⚠️ 撤销后把 sources.memory.path 改回 auto 或暂停导入,否则下一轮对账会按现行文件重新对一遍");
1984
+ return;
1985
+ }
1986
+
1987
+ const memSource = config.sources.memory;
1988
+ if (!memSource || memSource.path === "auto") {
1989
+ console.error("❌ 需要在 config.json 里显式配置 sources.memory.path(当前为 auto 或未配置)");
1990
+ process.exitCode = 2;
1991
+ return;
1992
+ }
1993
+ const memDir = resolveSourcePath(memSource.path, "memory");
1994
+ console.log(`记忆目录: ${memDir}`);
1995
+ const maxRetire = options.maxRetire !== undefined
1996
+ ? parseRequiredLimitOption(options.maxRetire, "--max-retire", 0, Number.MAX_SAFE_INTEGER)
1997
+ : undefined;
1998
+
1999
+ const outcome = await reconcileMemoryDocuments({
2000
+ store,
2001
+ embedder,
2002
+ llm,
2003
+ auditLogger: createAuditLogger(),
2004
+ memDir,
2005
+ dataDir: paths.dataDir,
2006
+ explicitPath: true,
2007
+ apply: Boolean(options.apply),
2008
+ maxRetire,
2009
+ });
2010
+
2011
+ const plan = outcome.plan;
2012
+ const countBy = <T,>(items: T[], key: (item: T) => string): Record<string, number> => {
2013
+ const out: Record<string, number> = {};
2014
+ for (const item of items) out[key(item)] = (out[key(item)] ?? 0) + 1;
2015
+ return out;
2016
+ };
2017
+ const topFiles = (items: Array<{ file: string | null }>): Array<[string, number]> =>
2018
+ Object.entries(countBy(items, (i) => i.file ?? "(无文件名)")).sort((a, b) => b[1] - a[1]).slice(0, 10);
2019
+
2020
+ if (options.json) {
2021
+ console.log(JSON.stringify({
2022
+ ran: outcome.ran,
2023
+ skippedReason: outcome.skippedReason ?? null,
2024
+ mode: options.apply ? "apply" : "dry-run",
2025
+ maxRetire: outcome.maxRetire,
2026
+ guard: outcome.guard,
2027
+ alert: outcome.alert,
2028
+ journalPath: outcome.journalPath,
2029
+ plan: plan && {
2030
+ currentFiles: plan.currentFiles,
2031
+ currentChunks: plan.currentChunks,
2032
+ currentDistinct: plan.currentDistinct,
2033
+ existingRows: plan.existingRows,
2034
+ activeDocRows: plan.activeDocRows,
2035
+ filesWithActiveDocRows: plan.filesWithActiveDocRows,
2036
+ keep: plan.keep,
2037
+ insert: plan.insert.length,
2038
+ insertAltId: plan.insertAltId,
2039
+ insertByFile: topFiles(plan.insert.map((i) => ({ file: i.chunk.file }))),
2040
+ retire: plan.retire.length,
2041
+ retireByReason: countBy(plan.retire, (r) => r.reason),
2042
+ retireByFile: topFiles(plan.retire),
2043
+ reactivate: countBy(plan.reactivate, (r) => `${r.kind}${r.kind === "broken-consolidation" && r.targetRetiredThisRun ? "(目标本轮下架)" : ""}`),
2044
+ representedByConsolidation: plan.representedByConsolidation,
2045
+ exceptionForgotten: plan.exceptionForgotten,
2046
+ exceptionOtherInactive: plan.exceptionOtherInactive,
2047
+ exceptionOtherInactiveIds: plan.exceptionOtherInactiveIds.map((id) => id.slice(0, 8)),
2048
+ },
2049
+ applied: outcome.applied,
2050
+ }, null, 2));
2051
+ } else {
2052
+ console.log(`\n${options.apply ? "执行结果" : "计划(未写库,加 --apply 执行)"}: ${formatReconcileSummary(outcome)}`);
2053
+ if (plan) {
2054
+ console.log(` 库里 scope "memory" 共 ${plan.existingRows} 行,其中活跃文档切片 ${plan.activeDocRows} 条、涉及 ${plan.filesWithActiveDocRows} 个文件;单轮下架上限 ${outcome.maxRetire}`);
2055
+ const byReason = countBy(plan.retire, (r) => r.reason);
2056
+ if (plan.retire.length > 0) console.log(` 下架按原因: ${Object.entries(byReason).map(([k, v]) => `${k} ${v}`).join(",")}`);
2057
+ if (plan.retire.length > 0) console.log(` 下架最多的文件: ${topFiles(plan.retire).map(([f, n]) => `${f} ${n}`).join(",")}`);
2058
+ if (plan.insert.length > 0) console.log(` 插入最多的文件: ${topFiles(plan.insert.map((i) => ({ file: i.chunk.file }))).map(([f, n]) => `${f} ${n}`).join(",")}`);
2059
+ if (plan.exceptionOtherInactive > 0) console.log(` 例外②(同文只剩被别的机制下架的行,未推翻)id 前 8 位: ${plan.exceptionOtherInactiveIds.slice(0, 20).map((id) => id.slice(0, 8)).join(" ")}`);
2060
+ }
2061
+ if (outcome.journalPath) console.log(` 对账日志(撤销用): ${outcome.journalPath}`);
2062
+ }
2063
+ if (outcome.alert) process.exitCode = 3;
1632
2064
  });
1633
2065
 
1634
2066
  // ─── dream ───────────────────────────────────────────────────────────────────
@@ -2057,6 +2489,94 @@ program
2057
2489
 
2058
2490
  // ─── doctor ──────────────────────────────────────────────────────────────────
2059
2491
 
2492
+ program
2493
+ .command("triggers-backfill")
2494
+ .description("T-Mem triggers 回填:从正文抠「她以后会怎么问」句写入 memory_triggers 侧表(默认 dry-run,零 LLM)")
2495
+ .option("-s, --scope <scope>", "限定 scope", "memory:pivot")
2496
+ .option("-n, --limit <n>", "最多扫描条数", "5000")
2497
+ .option("--apply", "真写入(默认只预览)")
2498
+ .option("--include-batch", "连 tags 含 pivot-apply 的批量提炼条目一起回填(默认跳过:09-21 校准其走样率约七成,先别给它们加联想入口)")
2499
+ .option("--rebuild", "不看正文,只按 metadata.triggers 重建侧表(迁移 / 回滚后恢复用)")
2500
+ .option("--samples <n>", "预览时打印几条样例", "12")
2501
+ .action(async (options) => {
2502
+ const config = loadConfig();
2503
+ const { store, embedder, triggerStore } = createComponents(config);
2504
+ const scopeFilter = toScopeFilter(options.scope);
2505
+ const limit = parseLimitOption(options.limit, 5000, 1, 100000);
2506
+ const samples = parseLimitOption(options.samples, 12, 0, 200);
2507
+ const entries = await store.list(scopeFilter, undefined, limit, 0);
2508
+ const already = options.rebuild ? new Set<string>() : await triggerStore.listMemoryIds(scopeFilter);
2509
+ const counts = { scanned: 0, skippedBatch: 0, skippedInactive: 0, skippedHas: 0, noLine: 0, planned: 0, written: 0, rows: 0 };
2510
+ const preview: Array<{ id: string; triggers: string[] }> = [];
2511
+ for (const entry of entries) {
2512
+ counts.scanned += 1;
2513
+ if (!isActiveMemory(entry.metadata)) { counts.skippedInactive += 1; continue; }
2514
+ let meta: Record<string, unknown> = {};
2515
+ try { meta = JSON.parse(entry.metadata || "{}"); } catch { meta = {}; }
2516
+ const tags = Array.isArray(meta.tags) ? (meta.tags as unknown[]).map(String) : [];
2517
+ let texts: string[];
2518
+ if (options.rebuild) {
2519
+ texts = normalizeTriggerTexts(Array.isArray(meta.triggers) ? (meta.triggers as unknown[]).map(String) : []);
2520
+ if (texts.length === 0) { counts.noLine += 1; continue; }
2521
+ } else {
2522
+ if (!options.includeBatch && tags.includes("pivot-apply")) { counts.skippedBatch += 1; continue; }
2523
+ const hasMeta = Array.isArray(meta.triggers) && (meta.triggers as unknown[]).length > 0;
2524
+ if (already.has(entry.id) || hasMeta) { counts.skippedHas += 1; continue; }
2525
+ texts = extractTriggerCandidates(entry.text);
2526
+ if (texts.length === 0) { counts.noLine += 1; continue; }
2527
+ }
2528
+ counts.planned += 1;
2529
+ if (preview.length < samples) preview.push({ id: entry.id.slice(0, 8), triggers: texts });
2530
+ if (!options.apply) continue;
2531
+ const n = await triggerStore.upsertForMemory(entry.id, entry.scope, texts, (t) => embedder.embedBatchQuery(t));
2532
+ counts.rows += n;
2533
+ counts.written += 1;
2534
+ if (!options.rebuild) {
2535
+ meta.triggers = texts;
2536
+ await store.update(entry.id, { metadata: JSON.stringify(meta) });
2537
+ }
2538
+ }
2539
+ const mode = options.apply ? "APPLY" : "DRY-RUN";
2540
+ console.log(`[triggers-backfill ${mode}] scope=${options.scope} scanned=${counts.scanned} planned=${counts.planned}` +
2541
+ ` skipped(batch=${counts.skippedBatch} inactive=${counts.skippedInactive} already=${counts.skippedHas}) noLine=${counts.noLine}` +
2542
+ (options.apply ? ` written=${counts.written} rows=${counts.rows}` : ""));
2543
+ for (const p of preview) console.log(` ${p.id} ${p.triggers.map((t) => `「${t}」`).join(" ")}`);
2544
+ if (options.apply) {
2545
+ const stat = await triggerStore.stats(scopeFilter);
2546
+ console.log(`侧表现状:${stat.rows} 条 trigger / ${stat.memories} 个宿主(scope=${options.scope})`);
2547
+ } else {
2548
+ console.log("(预览。加 --apply 真写入)");
2549
+ }
2550
+ });
2551
+
2552
+ program
2553
+ .command("triggers-calibrate")
2554
+ .description("T-Mem trigger 硬闸校准:对 canary 用例逐条打印 query 对侧表 trigger 的最高余弦(不看 gate),用来定 RECALLNEST_TRIGGER_GATE")
2555
+ .option("--cases <path>", "canary 文件", "eval/cases-canary.json")
2556
+ .option("-s, --scope <scope>", "限定 scope(缺省用各 case 自己的 scope)")
2557
+ .option("-k, --top <n>", "每条打印几个命中", "4")
2558
+ .action(async (options) => {
2559
+ const config = loadConfig();
2560
+ const { embedder, triggerStore } = createComponents(config);
2561
+ const { readFileSync } = require("node:fs");
2562
+ const { resolve: resolvePath } = require("node:path");
2563
+ const raw = JSON.parse(readFileSync(resolvePath(options.cases), "utf8"));
2564
+ const cases: Array<{ name: string; query: string; scope?: string; targets?: string[] }> = Array.isArray(raw) ? raw : raw.cases;
2565
+ const top = parseLimitOption(options.top, 4, 1, 20);
2566
+ const stat = await triggerStore.stats();
2567
+ console.log(`侧表:${stat.rows} 条 trigger / ${stat.memories} 个宿主`);
2568
+ for (const c of cases) {
2569
+ const scope = options.scope || c.scope;
2570
+ const qv = await embedder.embedQuery(c.query);
2571
+ const hits = await triggerStore.search(qv, top, scope ? [scope] : undefined, { minCosine: 0, softCosine: 0, minOverlap: 0, minQueryTokens: 0, queryText: c.query });
2572
+ const targets = c.targets || [];
2573
+ const line = hits
2574
+ .map((h) => `${h.memoryId.slice(0, 8)}${targets.some((t) => h.memoryId.startsWith(t)) ? "✓" : " "} cos=${h.cosine.toFixed(3)} ov=${(h.overlap ?? 0).toFixed(2)}「${h.text.slice(0, 28)}」`)
2575
+ .join(" | ");
2576
+ console.log(`${c.name}\n ${line || "(no trigger rows in scope)"}`);
2577
+ }
2578
+ });
2579
+
2060
2580
  program
2061
2581
  .command("memory-utility")
2062
2582
  .description("价值回溯:查一条 memory 参与过的任务成功率与 utility(不传 id 时按 scope 批量)")