recallnest 2.6.1 → 3.0.1

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 (207) hide show
  1. package/CHANGELOG.md +292 -0
  2. package/README.md +280 -195
  3. package/README_CN.md +266 -199
  4. package/config.json.example +8 -1
  5. package/docs/api-reference.md +1 -1
  6. package/integrations/README.md +1 -1
  7. package/integrations/agy/README.md +73 -0
  8. package/integrations/agy/setup.sh +75 -0
  9. package/integrations/setup-all.sh +1 -1
  10. package/package.json +7 -5
  11. package/src/access-tracker.ts +10 -1
  12. package/src/activity-counter.ts +69 -11
  13. package/src/age-label.ts +29 -0
  14. package/src/audit-log.ts +35 -0
  15. package/src/auto-gc.ts +97 -30
  16. package/src/capture-engine.ts +26 -4
  17. package/src/cli.ts +224 -10
  18. package/src/consolidation-engine.ts +317 -123
  19. package/src/context-composer-stable.ts +5 -1
  20. package/src/data-checkup.ts +17 -3
  21. package/src/dream-pipeline.ts +90 -12
  22. package/src/embedder.ts +44 -5
  23. package/src/env-config.ts +71 -0
  24. package/src/forget-engine.ts +8 -2
  25. package/src/gateway-server.ts +272 -0
  26. package/src/ingest.ts +248 -22
  27. package/src/kg-extractor.ts +17 -0
  28. package/src/llm-client.ts +86 -25
  29. package/src/mcp-server.ts +2 -1
  30. package/src/mcp-tools-advanced.ts +7 -2
  31. package/src/mcp-tools-core.ts +11 -1
  32. package/src/mcp-tools-governance.ts +45 -3
  33. package/src/memory-evolution.ts +16 -0
  34. package/src/memory-output.ts +47 -8
  35. package/src/memory-promotion.ts +280 -0
  36. package/src/memory-store-port.ts +32 -3
  37. package/src/memory-utility.ts +314 -0
  38. package/src/pivot-apply.ts +52 -1
  39. package/src/pivot-evidence.ts +128 -0
  40. package/src/ppr-traversal.ts +25 -1
  41. package/src/recall-ledger.ts +76 -0
  42. package/src/retriever.ts +82 -9
  43. package/src/runtime-config.ts +9 -0
  44. package/src/scope-policy.ts +22 -1
  45. package/src/session-engine.ts +11 -4
  46. package/src/session-output.ts +13 -4
  47. package/src/session-store.ts +2 -3
  48. package/src/skill-promotion.ts +218 -1
  49. package/src/store.ts +231 -71
  50. package/src/synthesis-contract.ts +286 -0
  51. package/src/version-manager.ts +83 -0
  52. package/src/workflow-observation-schema.ts +26 -0
  53. package/integrations/gemini-cli/README.md +0 -57
  54. package/integrations/gemini-cli/setup.sh +0 -64
  55. package/src/__tests__/access-tracker-novelty.test.ts +0 -304
  56. package/src/__tests__/activity-counter.test.ts +0 -125
  57. package/src/__tests__/adaptive-fidelity.test.ts +0 -102
  58. package/src/__tests__/admission-control.test.ts +0 -206
  59. package/src/__tests__/anchor-generator.test.ts +0 -67
  60. package/src/__tests__/api-server-security.test.ts +0 -102
  61. package/src/__tests__/auto-consolidation.test.ts +0 -168
  62. package/src/__tests__/auto-recall.test.ts +0 -131
  63. package/src/__tests__/babel-integration.test.ts +0 -144
  64. package/src/__tests__/batch-dedup.test.ts +0 -65
  65. package/src/__tests__/belief-history.test.ts +0 -220
  66. package/src/__tests__/canary-eval.test.ts +0 -114
  67. package/src/__tests__/capture-engine.test.ts +0 -1328
  68. package/src/__tests__/capture-heuristic.test.ts +0 -171
  69. package/src/__tests__/cascade-forget.test.ts +0 -210
  70. package/src/__tests__/category-min-scores.test.ts +0 -116
  71. package/src/__tests__/cc8-cc1-essential-context.test.ts +0 -298
  72. package/src/__tests__/circuit-breaker.test.ts +0 -99
  73. package/src/__tests__/cluster-consolidation.test.ts +0 -945
  74. package/src/__tests__/confidence-metadata.test.ts +0 -125
  75. package/src/__tests__/confidence-tracker.test.ts +0 -261
  76. package/src/__tests__/conflict-advisor.test.ts +0 -206
  77. package/src/__tests__/conflict-escalation.test.ts +0 -149
  78. package/src/__tests__/conflict-lifecycle.test.ts +0 -83
  79. package/src/__tests__/conflict-output.test.ts +0 -85
  80. package/src/__tests__/conflict-store.test.ts +0 -418
  81. package/src/__tests__/connector-v1.test.ts +0 -181
  82. package/src/__tests__/consolidation-engine.test.ts +0 -430
  83. package/src/__tests__/constructive-retrieval.test.ts +0 -509
  84. package/src/__tests__/context-collapse-renderer.test.ts +0 -214
  85. package/src/__tests__/context-composer-pins.test.ts +0 -64
  86. package/src/__tests__/context-composer-task-selection.test.ts +0 -24
  87. package/src/__tests__/context-reconstructor.test.ts +0 -65
  88. package/src/__tests__/context-renderer.test.ts +0 -91
  89. package/src/__tests__/continuity-eval.test.ts +0 -275
  90. package/src/__tests__/continuity-schema.test.ts +0 -97
  91. package/src/__tests__/contracts.test.ts +0 -262
  92. package/src/__tests__/conversation-importer.test.ts +0 -692
  93. package/src/__tests__/core-recall-eval.test.ts +0 -235
  94. package/src/__tests__/core-summary.test.ts +0 -109
  95. package/src/__tests__/data-checkup.test.ts +0 -276
  96. package/src/__tests__/decay-exemption.test.ts +0 -129
  97. package/src/__tests__/dedup-actions.test.ts +0 -58
  98. package/src/__tests__/dedup-guard.test.ts +0 -39
  99. package/src/__tests__/deterministic-id.test.ts +0 -39
  100. package/src/__tests__/distill-lock.test.ts +0 -305
  101. package/src/__tests__/distill-session-argshape.test.ts +0 -79
  102. package/src/__tests__/doctor.test.ts +0 -319
  103. package/src/__tests__/dream-pipeline.test.ts +0 -780
  104. package/src/__tests__/drill-down.test.ts +0 -108
  105. package/src/__tests__/embedder.test.ts +0 -122
  106. package/src/__tests__/emotion-decay.test.ts +0 -46
  107. package/src/__tests__/emotion-detector.test.ts +0 -56
  108. package/src/__tests__/emotion-phase1-integration.test.ts +0 -153
  109. package/src/__tests__/emotion-retrieval.test.ts +0 -56
  110. package/src/__tests__/entity-resolver.test.ts +0 -58
  111. package/src/__tests__/env-config.test.ts +0 -151
  112. package/src/__tests__/error-signature-boost.test.ts +0 -87
  113. package/src/__tests__/error-signature.test.ts +0 -95
  114. package/src/__tests__/error-taxonomy.test.ts +0 -129
  115. package/src/__tests__/ethics-layer.test.ts +0 -476
  116. package/src/__tests__/event-segmenter.test.ts +0 -153
  117. package/src/__tests__/feature-flags-integration.test.ts +0 -54
  118. package/src/__tests__/frequency-tracker.test.ts +0 -113
  119. package/src/__tests__/freshness.test.ts +0 -186
  120. package/src/__tests__/graph-export.test.ts +0 -489
  121. package/src/__tests__/hotness-blend.test.ts +0 -283
  122. package/src/__tests__/ingest-chunking.test.ts +0 -44
  123. package/src/__tests__/ingest.test.ts +0 -360
  124. package/src/__tests__/interference-detector.test.ts +0 -203
  125. package/src/__tests__/kg-extractor.test.ts +0 -192
  126. package/src/__tests__/kg-store.test.ts +0 -300
  127. package/src/__tests__/llm-client-pivot.test.ts +0 -628
  128. package/src/__tests__/llm-consolidation.test.ts +0 -226
  129. package/src/__tests__/maintenance-locks.test.ts +0 -189
  130. package/src/__tests__/mcp-efficiency.test.ts +0 -288
  131. package/src/__tests__/mcp-related-scopes.test.ts +0 -151
  132. package/src/__tests__/memory-boundaries.test.ts +0 -282
  133. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +0 -345
  134. package/src/__tests__/memory-evolution.test.ts +0 -271
  135. package/src/__tests__/memory-health-rebalance.test.ts +0 -144
  136. package/src/__tests__/memory-lint.test.ts +0 -421
  137. package/src/__tests__/memory-output.test.ts +0 -215
  138. package/src/__tests__/memory-promotion.test.ts +0 -315
  139. package/src/__tests__/migrate-dedup-ids.test.ts +0 -65
  140. package/src/__tests__/multi-vector.test.ts +0 -242
  141. package/src/__tests__/narrative-architecture.test.ts +0 -456
  142. package/src/__tests__/noise-filter.test.ts +0 -205
  143. package/src/__tests__/noise-prototype-bank.test.ts +0 -280
  144. package/src/__tests__/obsidian-connector.test.ts +0 -363
  145. package/src/__tests__/omitted-reporting.test.ts +0 -98
  146. package/src/__tests__/pending-review.test.ts +0 -77
  147. package/src/__tests__/pii-redact.test.ts +0 -333
  148. package/src/__tests__/pivot-apply.test.ts +0 -373
  149. package/src/__tests__/pivot-distill-supervisor.test.ts +0 -457
  150. package/src/__tests__/pivot-distill.test.ts +0 -1178
  151. package/src/__tests__/post-retrieval-filter.test.ts +0 -178
  152. package/src/__tests__/ppr-traversal.test.ts +0 -256
  153. package/src/__tests__/prediction-engine.test.ts +0 -349
  154. package/src/__tests__/predictive-prospective-memory.test.ts +0 -416
  155. package/src/__tests__/preference-guard-smoke.test.ts +0 -211
  156. package/src/__tests__/preference-matcher.test.ts +0 -282
  157. package/src/__tests__/preference-slots.test.ts +0 -205
  158. package/src/__tests__/prospective-memory.test.ts +0 -283
  159. package/src/__tests__/pull-from-macbook-sync-only.test.ts +0 -679
  160. package/src/__tests__/query-entity-detector.test.ts +0 -171
  161. package/src/__tests__/query-expander-alias.test.ts +0 -146
  162. package/src/__tests__/query-expander.test.ts +0 -34
  163. package/src/__tests__/recall-gate.test.ts +0 -266
  164. package/src/__tests__/recall-governor.test.ts +0 -227
  165. package/src/__tests__/recall-mode.test.ts +0 -64
  166. package/src/__tests__/resolve-db-path.test.ts +0 -130
  167. package/src/__tests__/resolve-tier.test.ts +0 -64
  168. package/src/__tests__/result-synthesizer.test.ts +0 -198
  169. package/src/__tests__/resume-context.test.ts +0 -4898
  170. package/src/__tests__/retrieval-eval.test.ts +0 -163
  171. package/src/__tests__/retrieval-trace.test.ts +0 -213
  172. package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
  173. package/src/__tests__/retriever-lazy-init-dispatch.test.ts +0 -123
  174. package/src/__tests__/retroactive-boost.test.ts +0 -223
  175. package/src/__tests__/salience-scoring.test.ts +0 -106
  176. package/src/__tests__/scope-inventory.test.ts +0 -200
  177. package/src/__tests__/scope-policy.test.ts +0 -69
  178. package/src/__tests__/scope-suggester.test.ts +0 -80
  179. package/src/__tests__/session-checkpoint.test.ts +0 -291
  180. package/src/__tests__/session-distiller.test.ts +0 -640
  181. package/src/__tests__/session-gc.test.ts +0 -215
  182. package/src/__tests__/skill-engine.test.ts +0 -552
  183. package/src/__tests__/skill-promotion.test.ts +0 -654
  184. package/src/__tests__/skill-verifier.test.ts +0 -122
  185. package/src/__tests__/source-heartbeat.test.ts +0 -250
  186. package/src/__tests__/stable-context-length-contract.test.ts +0 -62
  187. package/src/__tests__/store-patch-metadata.test.ts +0 -205
  188. package/src/__tests__/store-read-consistency.test.ts +0 -122
  189. package/src/__tests__/store-schema-migration.test.ts +0 -87
  190. package/src/__tests__/store-scope-audit.test.ts +0 -81
  191. package/src/__tests__/store-upsert.test.ts +0 -201
  192. package/src/__tests__/supersede-chain.test.ts +0 -154
  193. package/src/__tests__/temporal-parser.test.ts +0 -241
  194. package/src/__tests__/temporal-validity.test.ts +0 -160
  195. package/src/__tests__/term-registry-terminal-roster.test.ts +0 -127
  196. package/src/__tests__/term-registry.test.ts +0 -121
  197. package/src/__tests__/tool-error-surfacing.test.ts +0 -98
  198. package/src/__tests__/tool-output-compressor.test.ts +0 -160
  199. package/src/__tests__/topic-tag.test.ts +0 -113
  200. package/src/__tests__/trusted-memory.test.ts +0 -839
  201. package/src/__tests__/ui-server-security.test.ts +0 -93
  202. package/src/__tests__/usage-tracker.test.ts +0 -287
  203. package/src/__tests__/version-manager.test.ts +0 -213
  204. package/src/__tests__/workflow-observation-skill-outcome.test.ts +0 -352
  205. package/src/__tests__/workflow-observation.test.ts +0 -433
  206. package/src/__tests__/write-verifier.test.ts +0 -122
  207. /package/integrations/{gemini-cli/gemini-md-snippet.md → agy/agy-md-snippet.md} +0 -0
@@ -461,6 +461,15 @@ export async function writeDurableEntry(
461
461
  entry: MemoryEntry;
462
462
  disposition: "stored" | "updated" | "deduped" | "promoted" | "conflict";
463
463
  conflictId?: string;
464
+ /**
465
+ * True when this write REPLACED the content of an existing canonical row
466
+ * (a belief version was archived). Two different paths can do that — the
467
+ * latest-wins branch reports `updated`, while the deterministic-id branch
468
+ * below still reports `stored` — so disposition alone cannot tell them
469
+ * apart. Consumers that mirror memory content elsewhere (KG triples) must
470
+ * drop the old projection when this is set.
471
+ */
472
+ supersededPrevious?: boolean;
464
473
  }> {
465
474
  const matches = await findCanonicalMatches(deps.store, params.canonicalKey);
466
475
  const categoryMatches = matches.filter((entry) => entry.category === params.category);
@@ -605,6 +614,7 @@ export async function writeDurableEntry(
605
614
  return {
606
615
  entry: updated,
607
616
  disposition: params.promotedFrom ? "promoted" : "updated",
617
+ supersededPrevious: true,
608
618
  };
609
619
  }
610
620
 
@@ -617,6 +627,7 @@ export async function writeDurableEntry(
617
627
  ? deterministicId(params.scope, params.canonicalKey)
618
628
  : undefined;
619
629
  let metadataForStore = params.metadata;
630
+ let supersededPrevious = false;
620
631
 
621
632
  if (targetId && deps.store.getById) {
622
633
  const rowAtTargetId = await deps.store.getById(targetId);
@@ -629,6 +640,7 @@ export async function writeDurableEntry(
629
640
  { now },
630
641
  );
631
642
  metadataForStore = buildSupersedingBeliefMetadata(params.metadata, previousEvo, historyId, now);
643
+ supersededPrevious = true;
632
644
  }
633
645
  }
634
646
 
@@ -647,6 +659,7 @@ export async function writeDurableEntry(
647
659
  return {
648
660
  entry: stored,
649
661
  disposition: params.promotedFrom ? "promoted" : "stored",
662
+ supersededPrevious,
650
663
  };
651
664
  }
652
665
 
@@ -1106,7 +1119,7 @@ export async function persistMemory(
1106
1119
  metadata = JSON.stringify(parsed);
1107
1120
  }
1108
1121
 
1109
- const { entry, disposition, conflictId } = await writeDurableEntry(deps, {
1122
+ const { entry, disposition, conflictId, supersededPrevious } = await writeDurableEntry(deps, {
1110
1123
  text: input.text,
1111
1124
  vector,
1112
1125
  category: input.category,
@@ -1124,9 +1137,18 @@ export async function persistMemory(
1124
1137
  const entryPrivacyTier = parsePrivacyTier(metadata);
1125
1138
  const kgAllowed = entryPrivacyTier !== "ephemeral" && entryPrivacyTier !== "private";
1126
1139
  if (deps.kgExtractor && disposition !== "deduped" && kgAllowed) {
1127
- deps.kgExtractor
1128
- .extractAndStore(input.text, entry.id, resolvedScope)
1129
- .catch(() => {}); // Silently ignore — KG extraction must never block memory writes
1140
+ const kgExtractor = deps.kgExtractor;
1141
+ // When this write replaced existing content, the edges extracted from the OLD
1142
+ // text are still in the graph: triple ids are content-derived, so re-extracting
1143
+ // merely adds new rows beside the stale ones and both compete during traversal.
1144
+ // Purge first, then re-extract — strictly ordered, or the purge would race ahead
1145
+ // and delete what we just wrote.
1146
+ const purged = supersededPrevious
1147
+ ? kgExtractor.purgeForMemory(entry.id)
1148
+ : Promise.resolve();
1149
+ purged
1150
+ .then(() => kgExtractor.extractAndStore(input.text, entry.id, resolvedScope))
1151
+ .catch(() => {}); // Silently ignore — KG upkeep must never block memory writes
1130
1152
  }
1131
1153
 
1132
1154
  // F-1: Audit log — record store operation (non-blocking, silent on failure)
package/src/cli.ts CHANGED
@@ -38,9 +38,18 @@ import {
38
38
  } from "./ingest.js";
39
39
  import { runDoctor, formatDoctorResults } from "./doctor.js";
40
40
  import { persistCaseMemory, persistMemory, persistWorkflowPattern } from "./capture-engine.js";
41
- import { scanMemoryPromotions, buildPromoteScanDeps, formatPromoteScanResult } from "./memory-promotion.js";
41
+ import {
42
+ scanMemoryPromotions,
43
+ scanSynthesisPromotions,
44
+ buildPromoteScanDeps,
45
+ formatPromoteScanResult,
46
+ formatSynthesisPromoteResult,
47
+ } from "./memory-promotion.js";
42
48
  import { runDream, formatDreamResult, formatDreamMetrics, DEFAULT_DREAM_CONFIG, classifyDreamFailure, shouldBlockDreamRun, partitionAutoDreamScopes } from "./dream-pipeline.js";
43
- import { listScopesAboveThreshold } from "./activity-counter.js";
49
+ import { dreamBudgetMs } from "./env-config.js";
50
+ import { listScopesAboveThreshold, pruneWriteCounts } from "./activity-counter.js";
51
+ import { isTranscriptScope } from "./memory-boundaries.js";
52
+ import { maybeRunGc } from "./auto-gc.js";
44
53
  import {
45
54
  CaseMemoryInputSchema,
46
55
  type CaseMemoryInput,
@@ -62,6 +71,7 @@ import { WorkflowObservationOutcomeSchema } from "./workflow-observation-schema.
62
71
  import { buildWorkflowEvidence, buildWorkflowObservationRecord, inspectWorkflowDashboard, inspectWorkflowHealth } from "./workflow-observation-engine.js";
63
72
  import { formatWorkflowEvidencePack, formatWorkflowHealthDashboard, formatWorkflowHealthReport, formatWorkflowObservationSaved } from "./workflow-observation-output.js";
64
73
  import { WorkflowObservationStore } from "./workflow-observation-store.js";
74
+ import { applyUtilityToMeta, computeMemoryUtility, formatMemoryUtility } from "./memory-utility.js";
65
75
  import {
66
76
  applyWorkflowObservationScopeSuggestions,
67
77
  formatWorkflowObservationScopeReview,
@@ -702,6 +712,28 @@ program
702
712
  console.log(formatPromoteScanResult(result));
703
713
  });
704
714
 
715
+ program
716
+ .command("promote-synthesis")
717
+ .description("扫描 dream 合成出的结论(evidence 层),把证据充分的那些晋升出一份 durable 副本")
718
+ .requiredOption("--scope <scope>", "要扫描的 scope,如 memory")
719
+ .option("--apply", "执行晋升;默认 dry-run 只预览")
720
+ .option("--min-evidence <n>", "最少几条仍存活的不同证据(默认 2)", "2")
721
+ .option("--min-importance <n>", "合成条目自身的最低 importance(默认 0.6)", "0.6")
722
+ .action(async (options) => {
723
+ const config = loadConfig();
724
+ const { store, embedder } = createComponents(config);
725
+ const conflictStore = new ConflictCandidateStore();
726
+ const deps = buildPromoteScanDeps({ store, embedder, conflictStore });
727
+ const minDistinctEvidence = Number(options.minEvidence);
728
+ const minImportance = Number(options.minImportance);
729
+ const result = await scanSynthesisPromotions(deps, options.scope, {
730
+ dryRun: !options.apply,
731
+ ...(Number.isFinite(minDistinctEvidence) ? { minDistinctEvidence } : {}),
732
+ ...(Number.isFinite(minImportance) ? { minImportance } : {}),
733
+ });
734
+ console.log(formatSynthesisPromoteResult(result));
735
+ });
736
+
705
737
  program
706
738
  .command("export <query>")
707
739
  .description("导出检索与蒸馏结果到 markdown/json")
@@ -1360,7 +1392,7 @@ program
1360
1392
  program
1361
1393
  .command("ingest")
1362
1394
  .description("导入对话记录到索引")
1363
- .option("-s, --source <source>", "数据源: cc / codex / gemini / kimi / memory / desktop / all", "all")
1395
+ .option("-s, --source <source>", "数据源: cc / codex / gemini / kimi / memory / desktop / minis / all", "all")
1364
1396
  .option("-l, --limit <n>", "限制处理文件数(调试用)")
1365
1397
  .option("-v, --verbose", "详细输出")
1366
1398
  .option("--no-dedup", "跳过向量去重")
@@ -1503,6 +1535,27 @@ program
1503
1535
  }
1504
1536
  }
1505
1537
 
1538
+ // Minis (iPhone) —— 手机 agent 自己把对话写进这个目录,ingest 顺带扫走。
1539
+ // 为什么是独立源而不是复用 desktop:scope 前缀决定了 memory-boundaries 怎么降权,
1540
+ // 混在 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
+ }
1558
+
1506
1559
  // Memory markdown files
1507
1560
  if (source === "all" || source === "memory") {
1508
1561
  console.log("📚 导入记忆文件...");
@@ -1587,10 +1640,11 @@ program
1587
1640
  program
1588
1641
  .command("dream")
1589
1642
  .description("运行 dream 离线整固管线 (Orient/Gather/Consolidate/Prune)")
1590
- .option("--scope <scope>", "目标 scope(前缀或精确)", "cc")
1643
+ .option("--scope <scope>", "目标 scope(默认前缀家族匹配;加 --exact-scope 变精确)", "cc")
1644
+ .option("--exact-scope", "把 --scope 当成一个具体 scope 名精确匹配,不做前缀家族展开(调度入口必带)")
1591
1645
  .option("--auto", "扫描 activity-counter 中写计数达标的所有 scope,逐个跑 dream(单 scope 失败不阻断其他)")
1592
1646
  .option("--force", "跳过 min-writes 门槛强制运行")
1593
- .action(async (options: { scope: string; auto?: boolean; force?: boolean }) => {
1647
+ .action(async (options: { scope: string; auto?: boolean; force?: boolean; exactScope?: boolean }) => {
1594
1648
  const config = loadConfig();
1595
1649
  const { store, embedder, llm } = createComponents(config);
1596
1650
  // Same activity-stats file the store writes per-scope counts to (beside the LanceDB dir).
@@ -1602,20 +1656,76 @@ program
1602
1656
  const eligible = listScopesAboveThreshold(DEFAULT_DREAM_CONFIG.minWritesForDream, { statsPath: activityStatsPath });
1603
1657
  // 巨型 scope 交给专用 schedule,不占日常轮次的 wall-clock 预算(理由见
1604
1658
  // DreamConfig.autoExcludeScopes:07-30 那轮 6h 只跑了 15/507,而 memory 独占 79% 的条数)。
1605
- const { run: scopes, deferred } = partitionAutoDreamScopes(eligible, DEFAULT_DREAM_CONFIG.autoExcludeScopes);
1659
+ const { run: nonMemoryScopes, deferred } = partitionAutoDreamScopes(eligible, DEFAULT_DREAM_CONFIG.autoExcludeScopes);
1606
1660
  if (deferred.length > 0) {
1607
1661
  // 不静默:被挪走的 scope 必须在日志里看得见,否则"今天怎么没跑 memory"只能靠翻代码回答。
1608
1662
  // 前缀刻意用 `[dream]` 而不是 `dream --auto:`——后者是 dream-checkup.sh 抓「达标 scope 数」
1609
1663
  // 的锚点,多一行同前缀的会混进它的取数。
1610
1664
  console.log(`[dream] ${deferred.length} 个 scope 交给专用 schedule,本轮不跑 → ${deferred.join(", ")}`);
1611
1665
  }
1666
+
1667
+ // 2026-08-16 策略排除(互审 C2/K5):与上面的 deferred **语义不同,别合并**——
1668
+ // deferred 是"别处会跑",这里是"没有任何 schedule 会跑"。日志措辞必须分开,
1669
+ // 否则将来查"pivot 怎么没巩固"会被误导去找那个并不存在的专用 job。
1670
+ // 同时清写计数:runDream 对它返回 skipped 不会 resetWriteCount,不清则计数无限
1671
+ // 累积、每天都进达标队列白跑一次(这正是 07-02 那个"处理完不出队"bug 的形状)。
1672
+ const neverDream = DEFAULT_DREAM_CONFIG.neverDreamScopes;
1673
+ const policyExcluded = nonMemoryScopes.filter(s => neverDream.includes(s));
1674
+ const afterPolicy = nonMemoryScopes.filter(s => !neverDream.includes(s));
1675
+ if (policyExcluded.length > 0) {
1676
+ const prunedPolicy = await pruneWriteCounts(s => neverDream.includes(s), { statsPath: activityStatsPath });
1677
+ console.log(
1678
+ `[dream] ${policyExcluded.length} 个 scope 按策略不参与巩固(neverDreamScopes,无专用 schedule)` +
1679
+ ` → ${policyExcluded.join(", ")};已清写计数 ${prunedPolicy.length} 个`,
1680
+ );
1681
+ }
1682
+
1683
+ // 2026-08-14 transcript scope 整体出队(dream 写爆库根治的准入侧):
1684
+ // transcript 是原文层(shared-behaviors §5.3——原文归 Deja/ingest 管,提炼归
1685
+ // pivot 池),历史会话被 ingest 批量灌入就把写计数打过门槛,2026-08 实测达标
1686
+ // 队列 95% 是 codex/cc 会话 scope、dream 派生产物 96.6% 落在不参与日常检索的
1687
+ // transcript scope 上——巩固算力几乎全部空转,还把 _versions 写到 7.7GB。
1688
+ // 出队同时清扫计数(含未达标条目——历史会话只增不减,不清则 stats 文件无限膨胀;
1689
+ // ingest 白天会再写回、次日 04:00 再清,稳态保持小)。判定用 memory-boundaries
1690
+ // 的 isTranscriptScope(接新端时那边本来就要求同步前缀清单)。
1691
+ const scopes = afterPolicy.filter(s => !isTranscriptScope(s));
1692
+ const transcriptEligible = afterPolicy.length - scopes.length;
1693
+ const prunedTranscripts = await pruneWriteCounts(isTranscriptScope, { statsPath: activityStatsPath });
1694
+ if (prunedTranscripts.length > 0 || transcriptEligible > 0) {
1695
+ console.log(
1696
+ `[dream] transcript 原文层不参与巩固(§5.3):清扫 ${prunedTranscripts.length} 个 scope 的写计数` +
1697
+ `(其中达标 ${transcriptEligible} 个)→ 样例: ${prunedTranscripts.slice(0, 5).join(", ")}`,
1698
+ );
1699
+ }
1700
+
1701
+ // 2026-08-14 全库 GC 兜底(互审 C5-P0):dream 是 auto-gc 的唯一生产触发点,而
1702
+ // maybeRunGc 藏在 runDreamInner 里 —— transcript 出队后 durable 队列可为空,若在
1703
+ // 进循环前就 exit,全库 GC 检查会静默消失、归档管线停转。这里无条件补调一次:
1704
+ // GC 自带 24h stamp + 跨进程锁,循环内已触发过则本次 too_soon 跳过,幂等零成本。
1705
+ // 失败只记日志不并入 blocked:GC 是 best-effort 维护,明天还有机会。
1706
+ const runGcBackstop = async (): Promise<void> => {
1707
+ try {
1708
+ const gc = await maybeRunGc(store, DEFAULT_DREAM_CONFIG.gc);
1709
+ console.log(
1710
+ `[dream] 全库 GC 兜底: ${gc.triggered
1711
+ ? `archived ${gc.archivedCount}, dissolved version groups ${gc.dissolvedVersionGroups}`
1712
+ : gc.dissolvedVersionGroups > 0
1713
+ ? `dissolved version groups ${gc.dissolvedVersionGroups}; archive skipped — ${gc.reason}`
1714
+ : `skipped — ${gc.reason}`}`,
1715
+ );
1716
+ } catch (err) {
1717
+ console.error(`[dream] 全库 GC 兜底失败(不阻断本轮判定): ${err instanceof Error ? err.message : String(err)}`);
1718
+ }
1719
+ };
1720
+
1612
1721
  if (scopes.length === 0) {
1613
- // 两种空:一种是真没人达标,一种是达标的全被挪走了——混成一句会让排障从这里开始跑偏。
1722
+ // 三种空分开说:真没人达标 / 达标的全被挪走或全是 transcript——混成一句会让排障跑偏。
1614
1723
  console.log(
1615
1724
  eligible.length === 0
1616
1725
  ? "dream --auto: 无达标 scope(写计数均低于门槛)"
1617
- : "dream --auto: 达标 scope 全部交给专用 schedule,本轮无事可做",
1726
+ : "dream --auto: 达标 scope 均为 transcript(已清扫)、已交专用 schedule 或按策略排除,本轮无事可做",
1618
1727
  );
1728
+ await runGcBackstop();
1619
1729
  console.log("[[DREAM_STATUS]] skip");
1620
1730
  process.exit(0);
1621
1731
  }
@@ -1625,7 +1735,7 @@ program
1625
1735
  // 把 07-25~27 三天的调度全堵死(上一次没结束,launchd 起不了新的)。
1626
1736
  // 天花板见 DreamConfig.autoRunBudgetMs 的注释——截断会让靠后 scope 系统性饿死,
1627
1737
  // 真解是 session scope TTL,不是把预算调大。
1628
- const budgetMs = Number(process.env.RECALLNEST_DREAM_BUDGET_MS ?? DEFAULT_DREAM_CONFIG.autoRunBudgetMs);
1738
+ const budgetMs = dreamBudgetMs(DEFAULT_DREAM_CONFIG.autoRunBudgetMs);
1629
1739
  const deadline = Date.now() + budgetMs;
1630
1740
 
1631
1741
  let fatalFailures = 0;
@@ -1647,7 +1757,11 @@ program
1647
1757
  }
1648
1758
  processed++;
1649
1759
  try {
1650
- const result = await runDream({ store, llm, embedder, scope, force: Boolean(options.force), activityStatsPath, kgStore });
1760
+ // scopeMatch: "exact" —— 这些 scope 名直接来自 activity-stats 的键,是具体
1761
+ // scope 不是 family selector。无冒号的键(实测 11 个,含 `cc` / `global` /
1762
+ // `memory`)走前缀会把整个家族卷进来:`cc` 一旦达标就以 `LIKE 'cc%'` 把 08-14
1763
+ // 刚出队的 transcript 原文层整批捞回(2026-08-16 互审 K4)。
1764
+ const result = await runDream({ store, llm, embedder, scope, force: Boolean(options.force), activityStatsPath, kgStore, scopeMatch: "exact" });
1651
1765
  tally[result.output.kind]++;
1652
1766
  effectsTotal += result.output.effectsWritten;
1653
1767
  if (result.output.degraded) degradedScopes.push(`${scope}=${result.output.degraded}`);
@@ -1665,6 +1779,10 @@ program
1665
1779
  }
1666
1780
  }
1667
1781
 
1782
+ // 全库 GC 兜底:循环内的 per-scope GC 若已触发过,这里 too_soon 幂等跳过;
1783
+ // 若全部 scope 都 skip/失败没走到 prune 阶段,这里补上当天唯一一次 GC 检查。
1784
+ await runGcBackstop();
1785
+
1668
1786
  // 旧语义是「任一 scope 抛异常即 blocked」,在 475 个 scope 的规模下等于「有一个撞上
1669
1787
  // store-write 锁就整轮红」,而整轮红会让 shell 脚本重试 → 重跑全量。改为:代码/数据
1670
1788
  // 缺陷一票否决,环境性失败要成规模才算整轮失败。比例按已处理数算,不按总数。
@@ -1702,6 +1820,9 @@ program
1702
1820
  force: Boolean(options.force),
1703
1821
  activityStatsPath,
1704
1822
  kgStore,
1823
+ // 手动调用默认保留 family(`--scope cc` 扫 `cc:*` 是既有用法);
1824
+ // 调度入口(weekly job)必须显式带 --exact-scope。
1825
+ scopeMatch: options.exactScope ? "exact" : "family",
1705
1826
  });
1706
1827
 
1707
1828
  console.log(formatDreamResult(result));
@@ -1936,6 +2057,92 @@ program
1936
2057
 
1937
2058
  // ─── doctor ──────────────────────────────────────────────────────────────────
1938
2059
 
2060
+ program
2061
+ .command("memory-utility")
2062
+ .description("价值回溯:查一条 memory 参与过的任务成功率与 utility(不传 id 时按 scope 批量)")
2063
+ .argument("[memoryId]", "memory id(完整 UUID 或前缀)")
2064
+ .option("--scope <scope>", "批量模式:扫这个 scope 下的条目")
2065
+ .option("--category <category>", "批量模式:只看这个 category")
2066
+ .option("--limit <n>", "批量模式扫描上限", "500")
2067
+ .option("--min-samples <n>", "批量模式只列出有效样本量 ≥ 此值的条目", "0")
2068
+ .option("--links <n>", "展示最近 N 条参与记录", "5")
2069
+ .option("--apply", "把算出的 utility 写回 metadata(默认只读,不写)")
2070
+ .option("--observations-dir <dir>", "从指定目录读 workflow observations(默认生产目录)")
2071
+ .option("--json", "输出 JSON")
2072
+ .action(async (memoryId, options) => {
2073
+ const { store } = createComponents(loadConfig());
2074
+ const observationStore = options.observationsDir
2075
+ ? new WorkflowObservationStore(expandHome(options.observationsDir))
2076
+ : new WorkflowObservationStore();
2077
+ // 全量拉 observation:join 要看的是历史全集,不是最近 200 条。
2078
+ const observations = await observationStore.listRecent({ limit: 100_000 });
2079
+ const showLinks = Number(options.links) || 0;
2080
+
2081
+ if (memoryId) {
2082
+ const entry = await store.getById(memoryId);
2083
+ if (!entry) {
2084
+ console.error(`未找到 memory: ${memoryId}`);
2085
+ process.exitCode = 1;
2086
+ return;
2087
+ }
2088
+ const summary = computeMemoryUtility(entry, observations);
2089
+ if (options.apply) {
2090
+ await store.patchMetadata(entry.id, (meta) => applyUtilityToMeta(meta, summary));
2091
+ }
2092
+ if (options.json) {
2093
+ console.log(JSON.stringify({ ...summary, applied: Boolean(options.apply) }, null, 2));
2094
+ return;
2095
+ }
2096
+ console.log(formatMemoryUtility(summary, { showLinks }));
2097
+ console.log(` observations 池: ${observations.length} 条`);
2098
+ if (options.apply) console.log(" 已写回 metadata.utility");
2099
+ return;
2100
+ }
2101
+
2102
+ if (!options.scope) {
2103
+ console.error("需要 <memoryId> 或 --scope");
2104
+ process.exitCode = 1;
2105
+ return;
2106
+ }
2107
+
2108
+ const limit = Number(options.limit) || 500;
2109
+ const minSamples = Number(options.minSamples) || 0;
2110
+ const entries = await store.listPage({
2111
+ scopeFilter: [options.scope],
2112
+ category: options.category,
2113
+ limit,
2114
+ offset: 0,
2115
+ });
2116
+
2117
+ const summaries = entries
2118
+ .map((entry) => computeMemoryUtility(entry, observations))
2119
+ .filter((s) => s.effectiveSamples >= minSamples && s.total > 0)
2120
+ .sort((a, b) => (b.utility ?? 0) - (a.utility ?? 0));
2121
+
2122
+ if (options.apply) {
2123
+ for (const summary of summaries) {
2124
+ await store.patchMetadata(summary.memoryId, (meta) => applyUtilityToMeta(meta, summary));
2125
+ }
2126
+ }
2127
+
2128
+ if (options.json) {
2129
+ console.log(JSON.stringify({ scanned: entries.length, matched: summaries.length, summaries }, null, 2));
2130
+ return;
2131
+ }
2132
+
2133
+ console.log(`scope=${options.scope} 扫描 ${entries.length} 条,其中 ${summaries.length} 条有参与记录`);
2134
+ console.log(`observations 池: ${observations.length} 条`);
2135
+ for (const summary of summaries.slice(0, 20)) {
2136
+ const utility = summary.utility === null ? " null" : summary.utility.toFixed(4).padStart(7);
2137
+ console.log(
2138
+ `${utility} samples=${summary.effectiveSamples.toFixed(2).padStart(6)}`
2139
+ + ` exact=${String(summary.exactCount).padStart(3)} session=${String(summary.sessionCount).padStart(3)}`
2140
+ + ` ${summary.memoryId.slice(0, 8)}`,
2141
+ );
2142
+ }
2143
+ if (options.apply) console.log(`已写回 ${summaries.length} 条的 metadata.utility`);
2144
+ });
2145
+
1939
2146
  program
1940
2147
  .command("workflow-observe")
1941
2148
  .description("记录一条 workflow primitive observation,作为自进化观测层的 append-only 输入")
@@ -1949,9 +2156,14 @@ program
1949
2156
  .option("--tags <tags>", "逗号分隔 tags")
1950
2157
  .option("--tools <tools>", "逗号分隔 tools")
1951
2158
  .option("--idempotency-key <key>", "同一请求重试时用于替换旧 observation 的稳定键")
2159
+ .option("--reader-id <id>", "P0 join key:读者身份(默认本进程)。CLI 每次是新进程,跨会话关联要显式传")
2160
+ .option("--recalled-ids <ids>", "P0 join key:逗号分隔的 memory id,本次任务实际用到的记忆")
1952
2161
  .option("--json", "输出 JSON")
1953
2162
  .action(async (workflowId, summary, options) => {
1954
2163
  const store = new WorkflowObservationStore();
2164
+ // CLI 是一次性进程,检索账本必然是空的(这次进程没检索过),所以不做自动补,
2165
+ // 只认显式传入——自动补在这里只会写出一个恒空的字段,看着像记了其实没记。
2166
+ const recalledIds = splitCsvOption(options.recalledIds);
1955
2167
  const record = buildWorkflowObservationRecord({
1956
2168
  workflowId,
1957
2169
  summary,
@@ -1963,6 +2175,8 @@ program
1963
2175
  tags: splitCsvOption(options.tags),
1964
2176
  tools: splitCsvOption(options.tools),
1965
2177
  idempotencyKey: options.idempotencyKey,
2178
+ readerId: options.readerId,
2179
+ ...(recalledIds.length > 0 ? { recalledIds } : {}),
1966
2180
  });
1967
2181
  const stored = await store.save(record);
1968
2182
  if (options.json) {