recallnest 2.2.1 → 2.5.4

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 (227) hide show
  1. package/CHANGELOG.md +150 -0
  2. package/README.md +46 -35
  3. package/README_CN.md +41 -26
  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/connectors/examples/email-adapter.ts +73 -0
  9. package/connectors/examples/logs-adapter.ts +103 -0
  10. package/connectors/examples/rss-adapter.ts +95 -0
  11. package/docs/api-reference.md +1291 -0
  12. package/docs/architecture.md +168 -0
  13. package/docs/connector-spec.md +126 -0
  14. package/eval/cases-canary.json +58 -0
  15. package/eval/cases-scope-robustness.json +278 -0
  16. package/eval/cases.json +117 -20
  17. package/package.json +22 -1
  18. package/src/__tests__/access-tracker-novelty.test.ts +238 -0
  19. package/src/__tests__/activity-counter.test.ts +125 -0
  20. package/src/__tests__/adaptive-fidelity.test.ts +102 -0
  21. package/src/__tests__/admission-control.test.ts +206 -0
  22. package/src/__tests__/anchor-generator.test.ts +67 -0
  23. package/src/__tests__/auto-consolidation.test.ts +168 -0
  24. package/src/__tests__/auto-recall.test.ts +131 -0
  25. package/src/__tests__/babel-integration.test.ts +144 -0
  26. package/src/__tests__/batch-dedup.test.ts +65 -0
  27. package/src/__tests__/canary-eval.test.ts +114 -0
  28. package/src/__tests__/capture-engine.test.ts +1238 -0
  29. package/src/__tests__/capture-heuristic.test.ts +171 -0
  30. package/src/__tests__/cascade-forget.test.ts +210 -0
  31. package/src/__tests__/category-min-scores.test.ts +33 -0
  32. package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
  33. package/src/__tests__/circuit-breaker.test.ts +99 -0
  34. package/src/__tests__/cluster-consolidation.test.ts +782 -0
  35. package/src/__tests__/confidence-metadata.test.ts +125 -0
  36. package/src/__tests__/confidence-tracker.test.ts +261 -0
  37. package/src/__tests__/conflict-advisor.test.ts +206 -0
  38. package/src/__tests__/conflict-escalation.test.ts +149 -0
  39. package/src/__tests__/conflict-lifecycle.test.ts +83 -0
  40. package/src/__tests__/conflict-output.test.ts +85 -0
  41. package/src/__tests__/conflict-store.test.ts +374 -0
  42. package/src/__tests__/connector-v1.test.ts +181 -0
  43. package/src/__tests__/consolidation-engine.test.ts +333 -0
  44. package/src/__tests__/constructive-retrieval.test.ts +509 -0
  45. package/src/__tests__/context-collapse-renderer.test.ts +214 -0
  46. package/src/__tests__/context-composer-pins.test.ts +64 -0
  47. package/src/__tests__/context-composer-task-selection.test.ts +24 -0
  48. package/src/__tests__/context-reconstructor.test.ts +65 -0
  49. package/src/__tests__/context-renderer.test.ts +91 -0
  50. package/src/__tests__/continuity-eval.test.ts +275 -0
  51. package/src/__tests__/continuity-schema.test.ts +97 -0
  52. package/src/__tests__/contracts.test.ts +216 -0
  53. package/src/__tests__/conversation-importer.test.ts +692 -0
  54. package/src/__tests__/core-recall-eval.test.ts +235 -0
  55. package/src/__tests__/core-summary.test.ts +109 -0
  56. package/src/__tests__/data-checkup.test.ts +276 -0
  57. package/src/__tests__/decay-exemption.test.ts +84 -0
  58. package/src/__tests__/dedup-actions.test.ts +58 -0
  59. package/src/__tests__/dedup-guard.test.ts +39 -0
  60. package/src/__tests__/deterministic-id.test.ts +39 -0
  61. package/src/__tests__/distill-lock.test.ts +305 -0
  62. package/src/__tests__/distill-session-argshape.test.ts +79 -0
  63. package/src/__tests__/doctor.test.ts +319 -0
  64. package/src/__tests__/dream-pipeline.test.ts +321 -0
  65. package/src/__tests__/drill-down.test.ts +108 -0
  66. package/src/__tests__/embedder.test.ts +122 -0
  67. package/src/__tests__/emotion-decay.test.ts +46 -0
  68. package/src/__tests__/emotion-detector.test.ts +56 -0
  69. package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
  70. package/src/__tests__/emotion-retrieval.test.ts +56 -0
  71. package/src/__tests__/entity-resolver.test.ts +58 -0
  72. package/src/__tests__/env-config.test.ts +151 -0
  73. package/src/__tests__/error-signature-boost.test.ts +87 -0
  74. package/src/__tests__/error-signature.test.ts +95 -0
  75. package/src/__tests__/error-taxonomy.test.ts +129 -0
  76. package/src/__tests__/ethics-layer.test.ts +476 -0
  77. package/src/__tests__/event-segmenter.test.ts +153 -0
  78. package/src/__tests__/feature-flags-integration.test.ts +54 -0
  79. package/src/__tests__/frequency-tracker.test.ts +113 -0
  80. package/src/__tests__/graph-export.test.ts +489 -0
  81. package/src/__tests__/hotness-blend.test.ts +76 -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-consolidation.test.ts +226 -0
  88. package/src/__tests__/maintenance-locks.test.ts +174 -0
  89. package/src/__tests__/mcp-efficiency.test.ts +288 -0
  90. package/src/__tests__/mcp-related-scopes.test.ts +151 -0
  91. package/src/__tests__/memory-boundaries.test.ts +250 -0
  92. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +345 -0
  93. package/src/__tests__/memory-evolution.test.ts +271 -0
  94. package/src/__tests__/memory-health-rebalance.test.ts +144 -0
  95. package/src/__tests__/memory-lint.test.ts +421 -0
  96. package/src/__tests__/memory-output.test.ts +215 -0
  97. package/src/__tests__/memory-promotion.test.ts +315 -0
  98. package/src/__tests__/migrate-dedup-ids.test.ts +65 -0
  99. package/src/__tests__/multi-vector.test.ts +242 -0
  100. package/src/__tests__/narrative-architecture.test.ts +456 -0
  101. package/src/__tests__/noise-filter.test.ts +205 -0
  102. package/src/__tests__/obsidian-connector.test.ts +363 -0
  103. package/src/__tests__/omitted-reporting.test.ts +98 -0
  104. package/src/__tests__/pending-review.test.ts +77 -0
  105. package/src/__tests__/pii-redact.test.ts +314 -0
  106. package/src/__tests__/post-retrieval-filter.test.ts +178 -0
  107. package/src/__tests__/ppr-traversal.test.ts +256 -0
  108. package/src/__tests__/prediction-engine.test.ts +349 -0
  109. package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
  110. package/src/__tests__/preference-guard-smoke.test.ts +211 -0
  111. package/src/__tests__/preference-matcher.test.ts +230 -0
  112. package/src/__tests__/preference-slots.test.ts +205 -0
  113. package/src/__tests__/prospective-memory.test.ts +283 -0
  114. package/src/__tests__/query-entity-detector.test.ts +171 -0
  115. package/src/__tests__/query-expander-alias.test.ts +146 -0
  116. package/src/__tests__/query-expander.test.ts +34 -0
  117. package/src/__tests__/recall-governor.test.ts +227 -0
  118. package/src/__tests__/recall-mode.test.ts +64 -0
  119. package/src/__tests__/result-synthesizer.test.ts +198 -0
  120. package/src/__tests__/resume-context.test.ts +4898 -0
  121. package/src/__tests__/retrieval-trace.test.ts +213 -0
  122. package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
  123. package/src/__tests__/retroactive-boost.test.ts +223 -0
  124. package/src/__tests__/salience-scoring.test.ts +106 -0
  125. package/src/__tests__/scope-inventory.test.ts +200 -0
  126. package/src/__tests__/scope-policy.test.ts +69 -0
  127. package/src/__tests__/scope-suggester.test.ts +80 -0
  128. package/src/__tests__/session-checkpoint.test.ts +291 -0
  129. package/src/__tests__/session-distiller.test.ts +640 -0
  130. package/src/__tests__/session-gc.test.ts +215 -0
  131. package/src/__tests__/skill-engine.test.ts +552 -0
  132. package/src/__tests__/skill-promotion.test.ts +601 -0
  133. package/src/__tests__/skill-verifier.test.ts +122 -0
  134. package/src/__tests__/source-heartbeat.test.ts +250 -0
  135. package/src/__tests__/store-patch-metadata.test.ts +205 -0
  136. package/src/__tests__/store-scope-audit.test.ts +81 -0
  137. package/src/__tests__/store-upsert.test.ts +184 -0
  138. package/src/__tests__/supersede-chain.test.ts +154 -0
  139. package/src/__tests__/temporal-parser.test.ts +241 -0
  140. package/src/__tests__/temporal-validity.test.ts +160 -0
  141. package/src/__tests__/term-registry.test.ts +121 -0
  142. package/src/__tests__/tool-error-surfacing.test.ts +98 -0
  143. package/src/__tests__/tool-output-compressor.test.ts +160 -0
  144. package/src/__tests__/topic-tag.test.ts +113 -0
  145. package/src/__tests__/trusted-memory.test.ts +839 -0
  146. package/src/__tests__/usage-tracker.test.ts +287 -0
  147. package/src/__tests__/version-manager.test.ts +213 -0
  148. package/src/__tests__/workflow-observation-skill-outcome.test.ts +352 -0
  149. package/src/__tests__/workflow-observation.test.ts +433 -0
  150. package/src/__tests__/write-verifier.test.ts +122 -0
  151. package/src/access-tracker.ts +38 -32
  152. package/src/activity-counter.ts +46 -18
  153. package/src/aliases.ts +133 -0
  154. package/src/api-server.ts +27 -7
  155. package/src/audit-log.ts +4 -2
  156. package/src/auto-consolidation.ts +8 -3
  157. package/src/auto-gc.ts +85 -21
  158. package/src/capture-engine.ts +54 -21
  159. package/src/cli.ts +199 -37
  160. package/src/confidence-tracker.ts +36 -18
  161. package/src/connector-types.ts +57 -0
  162. package/src/consolidation-engine.ts +109 -11
  163. package/src/context-composer.ts +8 -1
  164. package/src/conversation-importer.ts +25 -1
  165. package/src/data-checkup.ts +126 -7
  166. package/src/distill-lock.ts +194 -15
  167. package/src/doctor.ts +38 -2
  168. package/src/dream-pipeline.ts +118 -34
  169. package/src/env-config.ts +77 -0
  170. package/src/error-signature.ts +153 -0
  171. package/src/error-taxonomy.ts +115 -0
  172. package/src/eval.ts +251 -6
  173. package/src/idempotency.ts +8 -0
  174. package/src/ingest.ts +234 -10
  175. package/src/kg-extractor.ts +6 -2
  176. package/src/kg-store.ts +236 -44
  177. package/src/llm-consolidation.ts +2 -1
  178. package/src/mcp-server.ts +85 -1799
  179. package/src/mcp-tool-deps.ts +48 -0
  180. package/src/mcp-tools-advanced.ts +941 -0
  181. package/src/mcp-tools-core.ts +633 -0
  182. package/src/mcp-tools-governance.ts +475 -0
  183. package/src/memory-boundaries.ts +3 -1
  184. package/src/memory-evolution.ts +73 -1
  185. package/src/memory-lint.ts +183 -10
  186. package/src/memory-output.ts +114 -3
  187. package/src/memory-promotion.ts +246 -0
  188. package/src/memory-schema.ts +25 -2
  189. package/src/memory-store-port.ts +43 -0
  190. package/src/multi-vector.ts +2 -1
  191. package/src/narrative-schema.ts +2 -1
  192. package/src/noise-filter.ts +57 -6
  193. package/src/obsidian-connector.ts +282 -0
  194. package/src/pii-detector.ts +157 -5
  195. package/src/query-expander.ts +90 -9
  196. package/src/result-synthesizer.ts +2 -1
  197. package/src/retention-policy.ts +2 -1
  198. package/src/retriever.ts +259 -139
  199. package/src/runtime-config.ts +8 -1
  200. package/src/scope-suggester.ts +113 -0
  201. package/src/session-distiller.ts +23 -2
  202. package/src/session-engine.ts +2 -1
  203. package/src/session-schema.ts +3 -2
  204. package/src/session-store.ts +5 -0
  205. package/src/skill-engine.ts +78 -0
  206. package/src/skill-promotion.ts +247 -47
  207. package/src/skill-schema.ts +16 -3
  208. package/src/skill-verifier.ts +152 -0
  209. package/src/source-heartbeat.ts +108 -0
  210. package/src/store.ts +267 -38
  211. package/src/tool-output-compressor.ts +3 -1
  212. package/src/ui-server.ts +2 -1
  213. package/src/usage-tracker.ts +242 -0
  214. package/src/version-manager.ts +3 -2
  215. package/src/workflow-observation-engine.ts +2 -1
  216. package/src/workflow-observation-schema.ts +2 -1
  217. package/src/workflow-observation-store.ts +6 -2
  218. package/.env +0 -4
  219. package/CLAUDE.md +0 -46
  220. package/config.json +0 -55
  221. package/daily-recall.sh +0 -110
  222. package/logs/daily-recall.error.log +0 -0
  223. package/logs/daily-recall.log +0 -30
  224. package/logs/ingest-2026-04-11.log +0 -110
  225. package/logs/ingest-2026-04-12.log +0 -90
  226. package/logs/launchagent-stderr.log +0 -0
  227. package/logs/launchagent-stdout.log +0 -0
@@ -9,6 +9,9 @@
9
9
  * 2. Duplicates — near-identical memories by vector cosine similarity
10
10
  * 3. Stale — memories never or rarely accessed and old enough to review
11
11
  * 4. Orphans — memories with missing scope or broken consolidation links
12
+ * 5. Weak lessons — workflow patterns lacking any action verbs (inspired by
13
+ * KarryViber/Orb memory-lint heuristic — a "lesson" should prescribe an
14
+ * action, not describe a phenomenon)
12
15
  *
13
16
  * Produces a health score (0-100) and a human-readable report.
14
17
  */
@@ -16,6 +19,7 @@
16
19
  import type { MemoryEntry, MemoryStore } from "./store.js";
17
20
  import { cosineSimilarity } from "./multi-vector.js";
18
21
  import { parseEvolution, isActiveMemory } from "./memory-evolution.js";
22
+ import { deriveUsageStatus, isUsageSignalActive } from "./usage-tracker.js";
19
23
 
20
24
  // ---------------------------------------------------------------------------
21
25
  // Types
@@ -24,7 +28,7 @@ import { parseEvolution, isActiveMemory } from "./memory-evolution.js";
24
28
  export type LintSeverity = "info" | "warning" | "error";
25
29
 
26
30
  export interface LintFinding {
27
- check: string; // e.g., "contradiction", "duplicate", "stale", "orphan"
31
+ check: string; // e.g., "contradiction", "duplicate", "stale", "orphan", "weakLesson"
28
32
  severity: LintSeverity;
29
33
  detail: string;
30
34
  memoryIds: string[];
@@ -34,17 +38,22 @@ export interface MemoryLintReport {
34
38
  findings: LintFinding[];
35
39
  healthScore: number; // 0-100
36
40
  totalScanned: number;
41
+ /** True if the scan hit the 10000-entry cap (older entries were not analyzed). */
42
+ scanLimited: boolean;
37
43
  timestamp: string;
38
44
  summary: {
39
45
  contradictions: number;
40
46
  duplicates: number;
41
47
  staleMemories: number;
42
48
  orphans: number;
49
+ weakLessons: number;
50
+ /** P0 B-1: cold 记忆数(被反复 surface 却从未被引用)。 */
51
+ coldMemories: number;
43
52
  };
44
53
  }
45
54
 
46
55
  export interface LintDeps {
47
- store: Pick<MemoryStore, "list">;
56
+ store: Pick<MemoryStore, "list" | "getVectors">;
48
57
  scope?: string;
49
58
  verbose?: boolean;
50
59
  }
@@ -78,6 +87,32 @@ const STALE_DAYS = 90;
78
87
 
79
88
  const STALE_MS = STALE_DAYS * 24 * 60 * 60 * 1000;
80
89
 
90
+ /**
91
+ * Categories where the action-verb lesson-quality heuristic applies.
92
+ *
93
+ * Workflow `patterns` are meant to be reusable "do it this way next time"
94
+ * prescriptions. A pattern whose text contains no imperative/action verb is
95
+ * usually too abstract for the agent to reuse and is flagged as a weak lesson.
96
+ *
97
+ * We intentionally do NOT apply this to `cases` (problem/solution pairs whose
98
+ * text may be descriptive), `events` (append-only log), `profile`/`preferences`
99
+ * (static facts), or `entities` (noun-centric).
100
+ *
101
+ * Inspired by KarryViber/Orb `lib/holographic/memory-lint.py`.
102
+ */
103
+ const LESSON_CATEGORIES = new Set(["patterns"]);
104
+
105
+ /**
106
+ * Regex matching action/imperative verbs used to judge whether a workflow
107
+ * pattern is actionable. Bilingual: English word-bounded; Chinese substring
108
+ * (CJK has no word boundaries).
109
+ *
110
+ * Intentionally wide — false-negatives here are benign (a weak-lesson flag is
111
+ * an `info` hint, not an error). We only want to catch patterns that are pure
112
+ * observations/descriptions with no prescriptive action at all.
113
+ */
114
+ const ACTION_VERBS = /\b(check|verify|validate|ensure|use|avoid|implement|run|call|set|add|remove|update|refactor|test|document|commit|push|rollback|escalate|stop|retry|install|configure|create|delete|write|read|spawn|enable|disable|init|start|restart|launch|store|fetch|query|log|match|replace|generate|prefer|require|invoke|route|build|deploy|review|fix|parse|apply|assert|include|exclude|skip|catch|throw|wrap|mock|mount|import|export|inject|extract|prepend|append|return|raise|cache|clear|reset|refresh|merge|split|rename|move|copy|scan|patrol|trigger|schedule|observe|record|emit|publish|subscribe|notify|confirm|choose|select|filter|sort|group|reduce|map|forward|resolve|reject)\b|使用|避免|检查|验证|确认|添加|删除|修改|改为|回滚|停止|重试|安装|配置|运行|调用|启用|禁用|生成|初始化|启动|重启|存储|获取|查询|记录|匹配|替换|创建|写入|读取|强制|禁止|必须|应当|保留|抛出|捕获|重构|部署|审查|修复|解析|应用|断言|包含|排除|跳过|合并|拆分|重命名|移动|复制|刷新|重置|清理|缓存|注入|提取|追加|遍历|订阅|发布|通知|选择|筛选|排序|触发|观察/iu;
115
+
81
116
  // ---------------------------------------------------------------------------
82
117
  // Contradiction Detection (inline copy from consolidation-engine.ts)
83
118
  // ---------------------------------------------------------------------------
@@ -279,6 +314,87 @@ function findOrphans(entries: MemoryEntry[]): LintFinding[] {
279
314
  return findings;
280
315
  }
281
316
 
317
+ // ---------------------------------------------------------------------------
318
+ // Check 5: Weak Lessons (action-verb heuristic)
319
+ // ---------------------------------------------------------------------------
320
+
321
+ /**
322
+ * Identify "weak lessons" — workflow `patterns` whose text contains no
323
+ * imperative/action verb. Such entries are usually descriptive rather than
324
+ * prescriptive, so the agent cannot act on them next time.
325
+ *
326
+ * Severity is `info` (not warning/error): this is a quality hint, not a bug.
327
+ * Low penalty (see computeHealthScore) reflects that.
328
+ *
329
+ * Inspired by KarryViber/Orb `lib/holographic/memory-lint.py`.
330
+ */
331
+ function findWeakLessons(entries: MemoryEntry[]): LintFinding[] {
332
+ const findings: LintFinding[] = [];
333
+
334
+ for (const entry of entries) {
335
+ if (!LESSON_CATEGORIES.has(entry.category)) continue;
336
+
337
+ // Empty/whitespace text is caught elsewhere; skip to avoid double-flagging.
338
+ if (!entry.text || entry.text.trim() === "") continue;
339
+
340
+ if (ACTION_VERBS.test(entry.text)) continue;
341
+
342
+ findings.push({
343
+ check: "weakLesson",
344
+ severity: "info",
345
+ detail: `Pattern without action verbs (too abstract to reuse): "${truncate(entry.text, 60)}"`,
346
+ memoryIds: [entry.id],
347
+ });
348
+ }
349
+
350
+ return findings;
351
+ }
352
+
353
+ /**
354
+ * Check 6 (P0 B-1 观察): cold memories — 被反复 surface(accessCount ≥ 6)却
355
+ * 从未被 reconstruction [src:ID] 引用的记忆,召回位污染信号。
356
+ *
357
+ * 聚合为单条 finding(cold 可能有几千条,逐条 finding 会淹没报告);
358
+ * memoryIds 取 injection 最高的前 20 条供抽查。severity 永远是 info:
359
+ * B-1 是纯观察阶段,降权/归档要等观察数据校准后的 B-2。
360
+ */
361
+ function findColdMemories(entries: MemoryEntry[]): { findings: LintFinding[]; coldCount: number } {
362
+ // use 信号未采集(reconstruction flag 关)→ cold 全是假阳性,跳过且不扣健康分。
363
+ if (!isUsageSignalActive()) {
364
+ return { findings: [], coldCount: 0 };
365
+ }
366
+
367
+ const cold: Array<{ id: string; injection: number }> = [];
368
+
369
+ for (const entry of entries) {
370
+ if (deriveUsageStatus(entry) !== "cold") continue;
371
+ let injection = 0;
372
+ try {
373
+ const meta = JSON.parse(entry.metadata || "{}") as Record<string, unknown>;
374
+ injection = typeof meta.accessCount === "number" ? meta.accessCount : 0;
375
+ } catch { /* keep 0 */ }
376
+ cold.push({ id: entry.id, injection });
377
+ }
378
+
379
+ if (cold.length === 0) return { findings: [], coldCount: 0 };
380
+
381
+ const pct = entries.length > 0 ? ((cold.length / entries.length) * 100).toFixed(1) : "0";
382
+ const topIds = cold
383
+ .sort((a, b) => b.injection - a.injection)
384
+ .slice(0, 20)
385
+ .map(c => c.id);
386
+
387
+ return {
388
+ coldCount: cold.length,
389
+ findings: [{
390
+ check: "coldMemory",
391
+ severity: "info",
392
+ detail: `${cold.length} cold memories (${pct}% of scanned): repeatedly surfaced (accessCount >= 6) but never cited by reconstruction. Top-20 by injection in memoryIds. Observation only (B-1) — no action taken.`,
393
+ memoryIds: topIds,
394
+ }],
395
+ };
396
+ }
397
+
282
398
  // ---------------------------------------------------------------------------
283
399
  // Health Score
284
400
  // ---------------------------------------------------------------------------
@@ -291,16 +407,36 @@ function findOrphans(entries: MemoryEntry[]): LintFinding[] {
291
407
  * - Duplicates: -5 each (waste and confusion risk)
292
408
  * - Stale: -0.5 each (minor, many are expected)
293
409
  * - Orphans: -3 each (moderate, broken references)
410
+ * - Weak lessons: -2 each (pattern lacks action verbs, reuse-unfriendly)
411
+ * - Cold memories (P0 B-1): 按占比而非条数 — 超过 10% 的部分每个百分点 -1,
412
+ * 封顶 -10。cold 是反复 surface 零引用的召回污染信号,量大但单条危害小,
413
+ * 按条扣会瞬间扣穿。纯观察反馈,不触发任何动作。
414
+ *
415
+ * `weakLessons`/`coldMemories` are optional on the input type for backward
416
+ * compatibility — callers constructing `summary` without them still get a
417
+ * correct score.
294
418
  */
295
419
  export function computeHealthScore(
296
- summary: MemoryLintReport["summary"],
297
- _total: number,
420
+ summary: {
421
+ contradictions: number;
422
+ duplicates: number;
423
+ staleMemories: number;
424
+ orphans: number;
425
+ weakLessons?: number;
426
+ coldMemories?: number;
427
+ },
428
+ total: number,
298
429
  ): number {
430
+ const coldPct = total > 0 ? ((summary.coldMemories ?? 0) / total) * 100 : 0;
431
+ const coldPenalty = Math.min(10, Math.max(0, Math.floor(coldPct - 10)));
432
+
299
433
  const penalty =
300
434
  summary.contradictions * 10 +
301
435
  summary.duplicates * 5 +
302
436
  Math.floor(summary.staleMemories * 0.5) +
303
- summary.orphans * 3;
437
+ summary.orphans * 3 +
438
+ (summary.weakLessons ?? 0) * 2 +
439
+ coldPenalty;
304
440
 
305
441
  return Math.max(0, Math.min(100, 100 - penalty));
306
442
  }
@@ -318,30 +454,42 @@ export function computeHealthScore(
318
454
  */
319
455
  export async function runMemoryLint(deps: LintDeps): Promise<MemoryLintReport> {
320
456
  const scopeFilter = deps.scope ? [deps.scope] : undefined;
321
- const allEntries = await deps.store.list(scopeFilter, undefined, 10000, 0);
457
+ const SCAN_LIMIT = 10000;
458
+ const allEntries = await deps.store.list(scopeFilter, undefined, SCAN_LIMIT, 0);
459
+ const scanLimited = allEntries.length >= SCAN_LIMIT;
322
460
 
323
461
  // Filter to active entries only
324
- const entries = allEntries.filter(e => isActiveMemory(e.metadata));
462
+ const activeEntries = allEntries.filter(e => isActiveMemory(e.metadata));
463
+
464
+ // list() 为性能不返回向量(vector: []);矛盾/去重检查靠向量算相似度,否则恒为 0、
465
+ // 静默失效并给出虚假 all-clear。补回真实向量再检查(stale/orphan/weakLesson 不依赖向量)。
466
+ const vectorMap = await deps.store.getVectors(activeEntries.map(e => e.id));
467
+ const entries = activeEntries.map(e => ({ ...e, vector: vectorMap.get(e.id) ?? [] }));
325
468
 
326
469
  // Run all checks
327
470
  const contradictions = findContradictions(entries);
328
471
  const duplicates = findDuplicates(entries);
329
472
  const stale = findStaleMemories(entries);
330
473
  const orphans = findOrphans(entries);
474
+ const weakLessons = findWeakLessons(entries);
475
+ const coldResult = findColdMemories(entries);
331
476
 
332
- const findings = [...contradictions, ...duplicates, ...stale, ...orphans];
477
+ const findings = [...contradictions, ...duplicates, ...stale, ...orphans, ...weakLessons, ...coldResult.findings];
333
478
 
334
479
  const summary = {
335
480
  contradictions: contradictions.length,
336
481
  duplicates: duplicates.length,
337
482
  staleMemories: stale.length,
338
483
  orphans: orphans.length,
484
+ weakLessons: weakLessons.length,
485
+ coldMemories: coldResult.coldCount,
339
486
  };
340
487
 
341
488
  return {
342
489
  findings,
343
490
  healthScore: computeHealthScore(summary, entries.length),
344
491
  totalScanned: entries.length,
492
+ scanLimited,
345
493
  timestamp: new Date().toISOString(),
346
494
  summary,
347
495
  };
@@ -358,7 +506,7 @@ export function formatMemoryLintReport(report: MemoryLintReport): string {
358
506
  // All clear case
359
507
  if (report.findings.length === 0) {
360
508
  lines.push("Memory Lint: All Clear!");
361
- lines.push(`Scanned: ${report.totalScanned} active memories`);
509
+ lines.push(`Scanned: ${report.totalScanned} active memories${report.scanLimited ? " (达扫描上限 10000,更早记忆未分析)" : ""}`);
362
510
  lines.push(`Health Score: ${report.healthScore}/100`);
363
511
  return lines.join("\n");
364
512
  }
@@ -366,7 +514,7 @@ export function formatMemoryLintReport(report: MemoryLintReport): string {
366
514
  // Header
367
515
  lines.push(`Memory Lint Report (${report.timestamp})`);
368
516
  lines.push("=".repeat(44));
369
- lines.push(`Scanned: ${report.totalScanned} active memories`);
517
+ lines.push(`Scanned: ${report.totalScanned} active memories${report.scanLimited ? " (达扫描上限 10000,更早记忆未分析)" : ""}`);
370
518
  lines.push("");
371
519
 
372
520
  // Contradictions
@@ -422,6 +570,31 @@ export function formatMemoryLintReport(report: MemoryLintReport): string {
422
570
  lines.push("");
423
571
  }
424
572
 
573
+ // Weak Lessons
574
+ if (report.summary.weakLessons > 0) {
575
+ const items = report.findings.filter(f => f.check === "weakLesson");
576
+ lines.push(`Weak Lessons (${items.length}):`);
577
+ if (items.length <= 5) {
578
+ for (const f of items) {
579
+ lines.push(` - ${f.detail} [${f.memoryIds.map(id => id.slice(0, 8)).join(", ")}]`);
580
+ }
581
+ } else {
582
+ lines.push(` - ${items.length} patterns without action verbs (consider rewriting or promoting)`);
583
+ }
584
+ lines.push("");
585
+ }
586
+
587
+ // Cold Memories (P0 B-1 observation)
588
+ if (report.summary.coldMemories > 0) {
589
+ const item = report.findings.find(f => f.check === "coldMemory");
590
+ lines.push(`Cold Memories (${report.summary.coldMemories}):`);
591
+ if (item) {
592
+ lines.push(` - ${item.detail}`);
593
+ lines.push(` - top by injection: ${item.memoryIds.map(id => id.slice(0, 8)).join(", ")}`);
594
+ }
595
+ lines.push("");
596
+ }
597
+
425
598
  // Health score
426
599
  lines.push(`Health Score: ${report.healthScore}/100`);
427
600
 
@@ -5,6 +5,7 @@ import { extractMemoryProvenance } from "./memory-boundaries.js";
5
5
  import { extractMultiVectorText } from "./multi-vector.js";
6
6
  import { parseNarrative } from "./narrative-schema.js";
7
7
  import { getConfidence, getConfidenceMetadata } from "./confidence-tracker.js";
8
+ import { estimateTokens } from "./context-collapse-renderer.js";
8
9
 
9
10
  interface MemoryMetadata {
10
11
  source?: string;
@@ -217,6 +218,49 @@ function pickBestSnippet(query: string, text: string): string {
217
218
  return cleanSnippet(bestSentence);
218
219
  }
219
220
 
221
+ /**
222
+ * adaptive 专用 query-aware snippet:取匹配词周围窗口(而非整句/开头),保证匹配证据可见。
223
+ * 解决 pickBestSnippet 的两个退化(Codex 审点4 C 版 P2):① 短缩写 query(如 "CI")
224
+ * extractTerms 提取不到词 → 退回开头;② 匹配在超长句/log 行后段 → cleanSnippet 从头截断丢匹配。
225
+ * 只给 adaptive 用,不改 pickBestSnippet(避免影响 normal/explain 的既有行为)。
226
+ */
227
+ function adaptiveSnippet(query: string, text: string): string {
228
+ const clean = text.replace(/\s+/g, " ").trim();
229
+ const lower = clean.toLowerCase();
230
+ // 短缩写时 extractTerms 可能为空,用 raw query 兜底直接定位。
231
+ const terms = extractTerms(query);
232
+ const isFallback = terms.length === 0;
233
+ const searchTerms = (isFallback ? [query.toLowerCase().trim()] : terms).filter(Boolean);
234
+
235
+ let matchIdx = -1;
236
+ for (const term of searchTerms) {
237
+ // term 定位策略:
238
+ // - ASCII 字母数字短缩写(CI/PR)用 word boundary,避免误匹配 "specific" 里的 "ci"(Codex C P2)
239
+ // - CJK 单字 / 符号 query(猫 / C++ / C#):JS \b 只认 ASCII、对它们永不匹配,回到 substring(收尾审 P2-1)
240
+ // - 正常 extractTerms 词:substring(容忍词根 / 词形)
241
+ // 已知限制(收尾审 P2-2,留后续):多 term 取最早出现,未按 term rarity/coverage 给窗口打分——
242
+ // 泛词在前、判别词在窗口外时可能漏掉判别词。第一版接受。
243
+ let idx: number;
244
+ if (isFallback && /^[a-z0-9]+$/i.test(term)) {
245
+ const m = new RegExp(`\\b${term}\\b`, "i").exec(clean);
246
+ idx = m ? m.index : -1;
247
+ } else {
248
+ idx = lower.indexOf(term);
249
+ }
250
+ if (idx >= 0 && (matchIdx < 0 || idx < matchIdx)) matchIdx = idx;
251
+ }
252
+ if (matchIdx < 0) return pickBestSnippet(query, text); // 完全无匹配 → 退回整句最佳
253
+
254
+ const WINDOW_BEFORE = 60;
255
+ const WINDOW_AFTER = 180;
256
+ const start = Math.max(0, matchIdx - WINDOW_BEFORE);
257
+ const end = Math.min(clean.length, matchIdx + WINDOW_AFTER);
258
+ let snip = clean.slice(start, end).trim();
259
+ if (start > 0) snip = "…" + snip;
260
+ if (end < clean.length) snip = snip + "…";
261
+ return snip;
262
+ }
263
+
220
264
  function normalizeRecallText(result: RetrievalResult): string {
221
265
  const source = getSourceLabel(result);
222
266
  if (source !== "asset") return result.entry.text;
@@ -282,7 +326,7 @@ function buildSearchRow(index: number, query: string, result: RetrievalResult):
282
326
  return [
283
327
  String(index + 1).padEnd(2),
284
328
  result.entry.id.slice(0, 8).padEnd(8),
285
- `${(result.score * 100).toFixed(0)}%`.padEnd(5),
329
+ `${(result.score * 100).toFixed(1)}%`.padEnd(6),
286
330
  getCategoryLabel(result).padEnd(12),
287
331
  getTierLabel(result).padEnd(10),
288
332
  getSourceLabel(result).padEnd(7),
@@ -308,7 +352,7 @@ export function formatBriefResults(
308
352
  for (let i = 0; i < results.length; i++) {
309
353
  const r = results[i];
310
354
  const excerpt = extractBriefExcerpt(r);
311
- lines.push(`#${i + 1} ${r.entry.id.slice(0, 8)} ${Math.round(r.score * 100)}% — ${excerpt}`);
355
+ lines.push(`#${i + 1} ${r.entry.id.slice(0, 8)} ${(r.score * 100).toFixed(1)}% — ${excerpt}`);
312
356
  }
313
357
  return lines.join("\n");
314
358
  }
@@ -382,6 +426,73 @@ export function formatSearchResults(
382
426
  return lines.join("\n");
383
427
  }
384
428
 
429
+ const ADAPTIVE_TOKEN_BUDGET = 8000;
430
+ const ADAPTIVE_FULL_SCORE = 0.85;
431
+
432
+ /**
433
+ * P-fidelity (点4): adaptive 保真度渲染 — 借鉴 RepoPrompt CE「保真度阶梯」的内核
434
+ * (按相关性分配保真度 + token 预算),但 search-native 实现。
435
+ *
436
+ * 为什么不复用 resume_context 的 collapseResults(CC+Codex 共识,Codex 三轮 review):
437
+ * collapse 的 l0 floor / 按 score 重排 / query-agnostic fallback 都是为 resume 场景定制,
438
+ * 搬到 search 连续撞三个 P2(隐藏过滤 / 覆盖 highlight 排序 / 丢失匹配证据)。search 的语义
439
+ * 不同——结果是"为什么这条命中",必须 query-aware;故第一版自实现两档:
440
+ * - 高相关(score ≥ 0.85)→ 全文
441
+ * - 其余 → query-aware snippet(pickBestSnippet,显示匹配证据)
442
+ * 按 handler 传入顺序(normal=score / highlight=contextual)依次填入 token 预算,超预算时
443
+ * 全文降级 snippet、snippet 仍超则停止(剩余计入 omitted)。完整三档保真度阶梯(每条记忆预存
444
+ * 多档摘要 + 预缓存 token 成本)留作后续大改。
445
+ */
446
+ export function formatCollapsedResults(
447
+ results: RetrievalResult[],
448
+ context: RenderContext,
449
+ ): string {
450
+ if (results.length === 0) return "No results found.";
451
+
452
+ const rendered: string[] = [];
453
+ let tokensUsed = 0;
454
+ let shown = 0;
455
+
456
+ // 保留传入顺序(不重排):handler 已按 normal=score / highlight=contextual 排好。
457
+ for (const r of results) {
458
+ const isFull = r.score >= ADAPTIVE_FULL_SCORE;
459
+ let level = isFull ? "FULL" : "SNIP";
460
+ // query-aware:snippet 取匹配词周围窗口、显示匹配证据(search 是"为什么命中")。
461
+ let text = isFull ? r.entry.text : adaptiveSnippet(context.query, r.entry.text);
462
+ let tokens = estimateTokens(text);
463
+
464
+ if (tokensUsed + tokens > ADAPTIVE_TOKEN_BUDGET) {
465
+ if (isFull) {
466
+ // 全文超预算 → 降级为 query-aware snippet
467
+ text = adaptiveSnippet(context.query, r.entry.text);
468
+ level = "SNIP";
469
+ tokens = estimateTokens(text);
470
+ }
471
+ if (tokensUsed + tokens > ADAPTIVE_TOKEN_BUDGET) break; // snippet 仍超 → 停止,剩余计入 omitted
472
+ }
473
+ tokensUsed += tokens;
474
+ shown++;
475
+
476
+ // 定位信息:id / score / category / source(Codex 约束:search 结果必须可定位)
477
+ const id = r.entry.id.slice(0, 8);
478
+ const scorePct = `${(r.score * 100).toFixed(0)}%`;
479
+ rendered.push(`[${level}] ${id} ${scorePct} ${getCategoryLabel(r)} ${getSourceLabel(r)}`);
480
+ rendered.push(text);
481
+ rendered.push("");
482
+ }
483
+
484
+ const omitted = results.length - shown;
485
+ const lines = [
486
+ `Query : ${context.query}`,
487
+ `Profile : ${context.profile}`,
488
+ `Mode : adaptive — full text for score ≥ ${ADAPTIVE_FULL_SCORE}, query-aware snippet otherwise, ${ADAPTIVE_TOKEN_BUDGET}-token budget`,
489
+ `Shown : ${shown} of ${results.length}${omitted > 0 ? ` (${omitted} omitted: token budget)` : ""}`,
490
+ "",
491
+ ...rendered,
492
+ ];
493
+ return lines.join("\n").trimEnd();
494
+ }
495
+
385
496
  export function formatExplainResults(
386
497
  results: RetrievalResult[],
387
498
  context: RenderContext,
@@ -398,7 +509,7 @@ export function formatExplainResults(
398
509
 
399
510
  for (let i = 0; i < results.length; i++) {
400
511
  const result = results[i];
401
- const score = `${(result.score * 100).toFixed(0)}%`;
512
+ const score = `${(result.score * 100).toFixed(1)}%`;
402
513
  const retrieval = getRetrievalPath(result);
403
514
  const file = getFileLabel(result);
404
515
  const session = getSessionLabel(result);