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
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Connector Standard Types (connector-v1)
3
+ *
4
+ * 定义外部数据源(Obsidian、Email、RSS、自定义脚本)统一产出的 JSON 格式。
5
+ * ingest.ts 的 ingestConnectorFile 消费这个格式,走已有的 dedup/extract/入库管线。
6
+ *
7
+ * 协议版本管理:version 字段用于未来兼容性。当前固定 "connector-v1"。
8
+ */
9
+
10
+ export interface ConnectorOutputV1 {
11
+ /** 格式版本,固定 "connector-v1" */
12
+ version: "connector-v1";
13
+ /** 数据源标识,如 "obsidian", "email", "rss", "x-bookmarks" */
14
+ source: string;
15
+ /** 目标 scope,如 "vault:my-notes", "project:research" */
16
+ scope: string;
17
+ /** 批次 ID(可选,用于幂等重试) */
18
+ batchId?: string;
19
+ /** 产出时间 ISO 8601 */
20
+ producedAt: string;
21
+ /** 记录列表 */
22
+ records: ConnectorRecord[];
23
+ }
24
+
25
+ export interface ConnectorRecord {
26
+ /** 记录唯一 ID(source 内唯一,用于增量去重) */
27
+ id: string;
28
+ /** 正文 */
29
+ text: string;
30
+ /** 标题/heading(可选) */
31
+ title?: string;
32
+ /** 分类提示:preferences / events / knowledge / facts / decisions */
33
+ categoryHint?: string;
34
+ /** 重要性提示 0-1(可选,默认 0.7) */
35
+ importanceHint?: number;
36
+ /** 标签 */
37
+ tags?: string[];
38
+ /** 原始时间戳 ISO 8601 */
39
+ timestamp?: string;
40
+ /** 内容哈希(用于增量同步,调用方计算) */
41
+ contentHash?: string;
42
+ /** 来源元数据(自由字段) */
43
+ sourceMetadata?: Record<string, unknown>;
44
+ }
45
+
46
+ /** 运行时格式验证:判定一个 parsed JSON 是否是合法 connector-v1 */
47
+ export function isConnectorOutputV1(value: unknown): value is ConnectorOutputV1 {
48
+ if (!value || typeof value !== "object") return false;
49
+ const obj = value as Record<string, unknown>;
50
+ return (
51
+ obj.version === "connector-v1"
52
+ && typeof obj.source === "string"
53
+ && typeof obj.scope === "string"
54
+ && typeof obj.producedAt === "string"
55
+ && Array.isArray(obj.records)
56
+ );
57
+ }
@@ -17,7 +17,9 @@
17
17
  * LLM-free — deterministic clustering and merge only.
18
18
  */
19
19
 
20
- import type { MemoryEntry, MemorySearchResult, MemoryStore } from "./store.js";
20
+ import type { MemoryEntry, MemorySearchResult } from "./store.js";
21
+ import { deterministicId } from "./store.js";
22
+ import type { MemoryStorePort } from "./memory-store-port.js";
21
23
  import { createVersionGroup } from "./version-manager.js";
22
24
  import { isActiveMemory, parseEvolution, buildSupersedeMetadata, buildConsolidatedMetadata, patchEvolution } from "./memory-evolution.js";
23
25
  import { cosineSimilarity } from "./multi-vector.js";
@@ -35,12 +37,22 @@ export interface ConsolidationConfig {
35
37
  mergeThreshold: number;
36
38
  /** Maximum entries to scan per consolidation run (default 500) */
37
39
  maxEntriesPerRun: number;
40
+ /**
41
+ * KG evidence: triple-set Jaccard at/above this merges a grey-zone pair
42
+ * (vector sim between clusterThreshold and mergeThreshold). Default 0.5 —
43
+ * provisional until calibrated on production data (plan Q3).
44
+ */
45
+ tripleJaccardThreshold?: number;
46
+ /** KG evidence: both sides need at least this many triples to qualify (default 2) */
47
+ minTriplesForEvidence?: number;
38
48
  }
39
49
 
40
50
  export const DEFAULT_CONSOLIDATION_CONFIG: ConsolidationConfig = {
41
51
  clusterThreshold: 0.82,
42
52
  mergeThreshold: 0.92,
43
53
  maxEntriesPerRun: 500,
54
+ tripleJaccardThreshold: 0.5,
55
+ minTriplesForEvidence: 2,
44
56
  };
45
57
 
46
58
  export interface ConflictEvent {
@@ -54,10 +66,52 @@ export interface ConsolidationResult {
54
66
  clustersFound: number;
55
67
  mergedCount: number;
56
68
  relationsAdded: number;
69
+ /** Of mergedCount, how many were below mergeThreshold and merged on KG triple evidence */
70
+ tripleEvidenceMerges: number;
57
71
  conflictsDetected: ConflictEvent[];
58
72
  scope: string;
59
73
  }
60
74
 
75
+ // ---------------------------------------------------------------------------
76
+ // KG evidence source (duck-typed subset of KGStore)
77
+ // ---------------------------------------------------------------------------
78
+
79
+ /** The two triple fields consolidation needs — structurally satisfied by KGTriple. */
80
+ export interface ConsolidationTripleEvidence {
81
+ id: string;
82
+ mention_count: number;
83
+ }
84
+
85
+ export interface ConsolidationKGSource {
86
+ getTriplesBySourceMemories(memoryIds: string[]): Promise<Map<string, ConsolidationTripleEvidence[]>>;
87
+ }
88
+
89
+ /**
90
+ * Jaccard overlap of two triple-id sets. Below `minSize` on either side the
91
+ * evidence is considered too weak and 0 is returned (a single shared triple
92
+ * must not merge two memories on its own).
93
+ */
94
+ export function tripleJaccard(
95
+ a: ReadonlySet<string> | undefined,
96
+ b: ReadonlySet<string> | undefined,
97
+ minSize = 2,
98
+ ): number {
99
+ if (!a || !b || a.size < minSize || b.size < minSize) return 0;
100
+ let inter = 0;
101
+ for (const x of a) if (b.has(x)) inter++;
102
+ return inter / (a.size + b.size - inter);
103
+ }
104
+
105
+ /**
106
+ * Mention-frequency boost in canonical selection: tie-breaker ONLY, never an
107
+ * override of a clear canonicalScore gap (an entry with one hot triple must
108
+ * not demote a substantially more important memory to consolidated status).
109
+ * α=0.06 keeps the boost differentiating (m=1 → 1.042, m=9 → 1.138) while the
110
+ * 1.1 cap bounds the maximum flip at ~10% of the base score — effectively ties.
111
+ */
112
+ const MENTION_ALPHA = 0.06;
113
+ const MENTION_BOOST_CAP = 1.1;
114
+
61
115
  // ---------------------------------------------------------------------------
62
116
  // Helpers
63
117
  // ---------------------------------------------------------------------------
@@ -108,7 +162,7 @@ export function detectHeuristicContradiction(textA: string, textB: string): bool
108
162
  // Store interface (duck-typed to avoid hard dependency)
109
163
  // ---------------------------------------------------------------------------
110
164
 
111
- type ConsolidationStore = Pick<MemoryStore, "list" | "getById" | "vectorSearch" | "update">;
165
+ type ConsolidationStore = Pick<MemoryStorePort, "list" | "getById" | "vectorSearch" | "update">;
112
166
 
113
167
  // ---------------------------------------------------------------------------
114
168
  // Engine
@@ -118,17 +172,25 @@ export class ConsolidationEngine {
118
172
  constructor(
119
173
  private store: ConsolidationStore,
120
174
  private config: ConsolidationConfig = DEFAULT_CONSOLIDATION_CONFIG,
175
+ /** Optional KG evidence source — absent = pure vector behavior, unchanged */
176
+ private kgSource: ConsolidationKGSource | null = null,
121
177
  ) {}
122
178
 
123
179
  async run(scope: string): Promise<ConsolidationResult> {
124
- const { clusterThreshold, mergeThreshold, maxEntriesPerRun } = this.config;
180
+ const {
181
+ clusterThreshold,
182
+ mergeThreshold,
183
+ maxEntriesPerRun,
184
+ tripleJaccardThreshold = 0.5,
185
+ minTriplesForEvidence = 2,
186
+ } = this.config;
125
187
 
126
188
  // 1. Fetch entries in scope
127
189
  const entries = await this.store.list([scope], undefined, maxEntriesPerRun, 0);
128
190
  const active = entries.filter(isActive);
129
191
 
130
192
  if (active.length === 0) {
131
- return { originalCount: 0, clustersFound: 0, mergedCount: 0, relationsAdded: 0, conflictsDetected: [], scope };
193
+ return { originalCount: 0, clustersFound: 0, mergedCount: 0, relationsAdded: 0, tripleEvidenceMerges: 0, conflictsDetected: [], scope };
132
194
  }
133
195
 
134
196
  // 2. Group by category
@@ -142,6 +204,7 @@ export class ConsolidationEngine {
142
204
  let clustersFound = 0;
143
205
  let mergedCount = 0;
144
206
  let relationsAdded = 0;
207
+ let tripleEvidenceMerges = 0;
145
208
  const conflictsDetected: ConflictEvent[] = [];
146
209
 
147
210
  // 3. Cluster within each category
@@ -183,8 +246,27 @@ export class ConsolidationEngine {
183
246
  }
184
247
  if (memberEntries.length < 2) continue;
185
248
 
186
- // Determine canonical: highest canonicalScore
187
- const sorted = [...memberEntries].sort((a, b) => canonicalScore(b) - canonicalScore(a));
249
+ // KG triple evidence for this cluster (single batch query; best-effort —
250
+ // on failure consolidation proceeds vector-only)
251
+ const tripleIdsByMember = new Map<string, Set<string>>();
252
+ const maxMentionByMember = new Map<string, number>();
253
+ if (this.kgSource) {
254
+ try {
255
+ const triplesByMember = await this.kgSource.getTriplesBySourceMemories(memberEntries.map(e => e.id));
256
+ for (const [mid, triples] of triplesByMember) {
257
+ tripleIdsByMember.set(mid, new Set(triples.map(t => t.id)));
258
+ maxMentionByMember.set(mid, triples.reduce((m, t) => Math.max(m, t.mention_count), 0));
259
+ }
260
+ } catch { /* KG evidence is best-effort */ }
261
+ }
262
+
263
+ // Determine canonical: highest canonicalScore, frequency-boosted when
264
+ // KG evidence exists (memories carrying often-mentioned facts win ties)
265
+ const mentionBoost = (e: MemoryEntry) =>
266
+ Math.min(MENTION_BOOST_CAP, 1 + MENTION_ALPHA * Math.log((maxMentionByMember.get(e.id) ?? 0) + 1));
267
+ const sorted = [...memberEntries].sort(
268
+ (a, b) => canonicalScore(b) * mentionBoost(b) - canonicalScore(a) * mentionBoost(a),
269
+ );
188
270
  const canonical = sorted[0];
189
271
 
190
272
  if (!canonical.vector?.length) continue;
@@ -199,7 +281,16 @@ export class ConsolidationEngine {
199
281
  for (const member of sorted.slice(1)) {
200
282
  const sim = scoreMap.get(member.id) ?? clusterThreshold;
201
283
 
202
- if (sim >= mergeThreshold) {
284
+ // Second evidence source: two memories whose extracted fact sets
285
+ // overlap heavily are duplicates even when vector sim sits in the
286
+ // grey zone (the "pairwise near-duplicates" the 0.92 bar misses).
287
+ const jaccard = tripleJaccard(
288
+ tripleIdsByMember.get(canonical.id),
289
+ tripleIdsByMember.get(member.id),
290
+ minTriplesForEvidence,
291
+ );
292
+
293
+ if (sim >= mergeThreshold || jaccard >= tripleJaccardThreshold) {
203
294
  // Tier 3.3: Version coexistence — both stay but grouped.
204
295
  await createVersionGroup(this.store, canonical, member, scope);
205
296
  // C-1: Also mark the weaker entry as consolidated via evolution metadata
@@ -214,6 +305,7 @@ export class ConsolidationEngine {
214
305
  await this.store.update(canonical.id, { metadata: updatedCanon }, [scope]);
215
306
  }
216
307
  mergedCount++;
308
+ if (sim < mergeThreshold) tripleEvidenceMerges++;
217
309
  } else {
218
310
  // Link: add clustered_with relation
219
311
  const memberMeta = parseMetadata(member);
@@ -245,7 +337,7 @@ export class ConsolidationEngine {
245
337
  }
246
338
  }
247
339
 
248
- return { originalCount: active.length, clustersFound, mergedCount, relationsAdded, conflictsDetected, scope };
340
+ return { originalCount: active.length, clustersFound, mergedCount, relationsAdded, tripleEvidenceMerges, conflictsDetected, scope };
249
341
  }
250
342
  }
251
343
 
@@ -278,7 +370,7 @@ export async function clusterAndConsolidate(params: {
278
370
  entries: MemoryEntry[];
279
371
  embedder: Pick<Embedder, "embedPassage">;
280
372
  llm: LLMClient;
281
- store: Pick<MemoryStore, "store" | "update">;
373
+ store: Pick<MemoryStorePort, "store" | "update">;
282
374
  scope: string;
283
375
  /** Minimum cluster size to trigger consolidation (default: 3) */
284
376
  minClusterSize?: number;
@@ -400,8 +492,13 @@ export async function clusterAndConsolidate(params: {
400
492
  // Embed the insight text
401
493
  const insightVector = await params.embedder.embedPassage(insight);
402
494
 
403
- // Store the insight as a new memory
495
+ // Store the insight as a new memory. P0-2/P1-2: derive a deterministic id from the
496
+ // (sorted) source member ids so re-running dream/consolidate on the same cluster
497
+ // upserts the same insight row instead of appending a near-duplicate each run
498
+ // (the highest-frequency dup-id source). Cross-run stable because sourceIds are stable.
499
+ const sortedSourceIds = cluster.map(m => m.id).slice().sort();
404
500
  const insightEntry = await store.store({
501
+ id: deterministicId(scope, `cluster-insight:${sortedSourceIds.join(",")}`),
405
502
  text: insight,
406
503
  vector: insightVector,
407
504
  category: bestCat,
@@ -444,6 +541,7 @@ export async function clusterAndConsolidate(params: {
444
541
  const patternImportance = Math.min(maxImportance + 0.1, 1.0);
445
542
  const patternVector = await params.embedder.embedPassage(patternText);
446
543
  const patternEntry = await store.store({
544
+ id: deterministicId(scope, `cluster-pattern:${sortedSourceIds.join(",")}`),
447
545
  text: patternText,
448
546
  vector: patternVector,
449
547
  category: "patterns",
@@ -492,7 +590,7 @@ export function formatConsolidationResult(result: ConsolidationResult): string {
492
590
  `Consolidation complete for scope: ${result.scope}`,
493
591
  `Scanned: ${result.originalCount} active entries`,
494
592
  `Clusters found: ${result.clustersFound}`,
495
- `Merged (versioned): ${result.mergedCount}`,
593
+ `Merged (versioned): ${result.mergedCount}${result.tripleEvidenceMerges > 0 ? ` (${result.tripleEvidenceMerges} on triple evidence)` : ""}`,
496
594
  `Relations added: ${result.relationsAdded}`,
497
595
  `Conflicts detected: ${result.conflictsDetected.length}`,
498
596
  ];
@@ -1,4 +1,5 @@
1
1
  import { listPinAssets, type PinAsset } from "./memory-assets.js";
2
+ import * as envConfig from "./env-config.js";
2
3
  import { buildStableContextSections } from "./context-composer-stable.js";
3
4
  import {
4
5
  buildAssociativeNestEntityFallbackQuery,
@@ -32,6 +33,8 @@ type ResumeCategory = "profile" | "preferences" | "entities" | "patterns" | "cas
32
33
 
33
34
  interface ResumeRetriever {
34
35
  retrieve(context: RetrievalContext): Promise<RetrievalResult[]>;
36
+ /** Optional: 影子记录 resume reconstruction 引用的记忆(injection-vs-use)。未实现则 no-op。 */
37
+ recordReconstructionUsage?(citedIds: string[]): Promise<void>;
35
38
  }
36
39
 
37
40
  interface CheckpointLookup {
@@ -467,7 +470,7 @@ export async function composeResumeContext(
467
470
  let reconstructedContext: string | undefined;
468
471
  let reconstructionConfidence: number | undefined;
469
472
  let reconstructionContradictions: Array<{ memoryIds: [string, string]; description: string }> | undefined;
470
- const constructiveFlag = process.env.RECALLNEST_CONSTRUCTIVE_RETRIEVAL === "true";
473
+ const constructiveFlag = envConfig.constructiveRetrieval();
471
474
  if (constructiveFlag && deps.llm?.isAvailable?.()) {
472
475
  const allReconResults = [...profileResults, ...preferenceResults, ...entityResults, ...filteredPatterns, ...filteredCases];
473
476
  if (allReconResults.length >= 3) {
@@ -489,6 +492,10 @@ export async function composeResumeContext(
489
492
  if (recon.contradictions.length > 0) {
490
493
  reconstructionContradictions = recon.contradictions;
491
494
  }
495
+ // Shadow usage:resume reconstruction 真正引用的记忆记 use 计数(不动 ranking/forget)。
496
+ if (recon.sources.length > 0) {
497
+ await deps.retriever.recordReconstructionUsage?.(recon.sources.map(s => s.id));
498
+ }
492
499
  } catch { /* silent fallback */ }
493
500
  }
494
501
  }
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
2
2
  import type { PersistMemoryDeps } from "./capture-engine.js";
3
3
  import { persistMemory } from "./capture-engine.js";
4
4
  import type { StoredMemoryRecord } from "./memory-schema.js";
5
+ import { isConnectorOutputV1, type ConnectorOutputV1 } from "./connector-types.js";
5
6
 
6
7
  export interface NormalizedMessage {
7
8
  role: "user" | "assistant" | "system";
@@ -14,7 +15,8 @@ export type ConversationFormat =
14
15
  | "claude-ai"
15
16
  | "chatgpt"
16
17
  | "slack"
17
- | "plaintext";
18
+ | "plaintext"
19
+ | "connector-v1";
18
20
 
19
21
  // ---------------------------------------------------------------------------
20
22
  // Format detection
@@ -40,6 +42,10 @@ export function detectFormat(content: string): ConversationFormat {
40
42
  }
41
43
 
42
44
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
45
+ // connector-v1: highest priority — explicit version tag
46
+ if (isConnectorOutputV1(parsed)) {
47
+ return "connector-v1";
48
+ }
43
49
  if (parsed.chat_conversations !== undefined) {
44
50
  return "claude-ai";
45
51
  }
@@ -366,6 +372,22 @@ export function normalizePlaintext(content: string): NormalizedMessage[] {
366
372
  return messages;
367
373
  }
368
374
 
375
+ // ---------------------------------------------------------------------------
376
+ // Connector-v1 normalizer
377
+ // ---------------------------------------------------------------------------
378
+
379
+ export function normalizeConnectorV1(content: string): NormalizedMessage[] {
380
+ const parsed: ConnectorOutputV1 = JSON.parse(content);
381
+ if (!isConnectorOutputV1(parsed)) {
382
+ throw new Error("Invalid connector-v1 format: missing version/source/scope/records");
383
+ }
384
+ return parsed.records.map((r) => ({
385
+ role: "user" as const,
386
+ content: r.title ? `[${r.title}] ${r.text}` : r.text,
387
+ ...(r.timestamp ? { timestamp: r.timestamp } : {}),
388
+ }));
389
+ }
390
+
369
391
  // ---------------------------------------------------------------------------
370
392
  // Unified normalize dispatcher
371
393
  // ---------------------------------------------------------------------------
@@ -382,6 +404,8 @@ export function normalizeConversation(content: string, format: ConversationForma
382
404
  return normalizeSlack(content);
383
405
  case "plaintext":
384
406
  return normalizePlaintext(content);
407
+ case "connector-v1":
408
+ return normalizeConnectorV1(content);
385
409
  }
386
410
  }
387
411
 
@@ -15,6 +15,8 @@
15
15
  import type { MemoryEntry, MemoryStore } from "./store.js";
16
16
  import { resolveTier, type MemoryTier } from "./decay-engine.js";
17
17
  import { measureInterferenceDensity } from "./interference-detector.js";
18
+ import { readHeartbeats, checkSourceStaleness, formatAge } from "./source-heartbeat.js";
19
+ import { deriveUsageStatus, isUsageSignalActive, type UsageStatus } from "./usage-tracker.js";
18
20
 
19
21
  // ---------------------------------------------------------------------------
20
22
  // Types
@@ -29,6 +31,8 @@ export interface CheckResult {
29
31
  export interface CheckupReport {
30
32
  checks: CheckResult[];
31
33
  totalEntries: number;
34
+ /** Total entries in store; when > totalEntries, only the most recent were scanned. */
35
+ totalAvailable: number;
32
36
  timestamp: string;
33
37
  }
34
38
 
@@ -38,15 +42,33 @@ export interface CheckupReport {
38
42
 
39
43
  /** Check 1: All vectors should have the same dimension. */
40
44
  function checkVectorDimensions(entries: MemoryEntry[]): CheckResult {
45
+ // 只对真正取到向量的条目判维度一致性;取不到向量的(dim=0)单独计数后跳过——
46
+ // 否则会把"未补到向量"误报成"维度不一致",或把"全 0 维空向量"假报成健康。
41
47
  const dims = new Map<number, number>(); // dim → count
48
+ let missingVectors = 0;
42
49
  for (const e of entries) {
43
50
  const d = e.vector?.length ?? 0;
51
+ if (d === 0) { missingVectors++; continue; }
44
52
  dims.set(d, (dims.get(d) ?? 0) + 1);
45
53
  }
46
54
 
47
- if (dims.size <= 1) {
55
+ const withVectors = entries.length - missingVectors;
56
+ if (entries.length === 0) {
57
+ return { name: "vector_dimensions", status: "ok", detail: "No entries to check" };
58
+ }
59
+ if (withVectors === 0) {
60
+ return {
61
+ name: "vector_dimensions",
62
+ status: "warning",
63
+ detail: `No retrievable vectors among ${entries.length} entries — dimension consistency not verified`,
64
+ };
65
+ }
66
+
67
+ const suffix = missingVectors > 0 ? ` (${missingVectors} without retrievable vector, skipped)` : "";
68
+
69
+ if (dims.size === 1) {
48
70
  const dim = dims.keys().next().value ?? 0;
49
- return { name: "vector_dimensions", status: "ok", detail: `All ${entries.length} entries have dimension ${dim}` };
71
+ return { name: "vector_dimensions", status: "ok", detail: `All ${withVectors} vectors have dimension ${dim}${suffix}` };
50
72
  }
51
73
 
52
74
  const sorted = [...dims.entries()].sort((a, b) => b[1] - a[1]);
@@ -55,7 +77,7 @@ function checkVectorDimensions(entries: MemoryEntry[]): CheckResult {
55
77
  return {
56
78
  name: "vector_dimensions",
57
79
  status: "error",
58
- detail: `${mismatched} entries have wrong dimension (expected ${expected}): ${sorted.map(([d, c]) => `dim=${d}: ${c}`).join(", ")}`,
80
+ detail: `${mismatched} entries have wrong dimension (expected ${expected}): ${sorted.map(([d, c]) => `dim=${d}: ${c}`).join(", ")}${suffix}`,
59
81
  };
60
82
  }
61
83
 
@@ -151,7 +173,33 @@ function checkVersionGroups(entries: MemoryEntry[]): CheckResult {
151
173
  };
152
174
  }
153
175
 
154
- /** Check 6 (F2): Interference density — detect semantic clustering pressure. */
176
+ /** Check 6 (GB-3): Source health — detect stale connector data sources. */
177
+ function checkSourceHealth(heartbeatPath?: string): CheckResult {
178
+ const heartbeats = readHeartbeats(heartbeatPath);
179
+ const sources = Object.values(heartbeats);
180
+ if (sources.length === 0) {
181
+ return { name: "source_health", status: "ok", detail: "No connector sources tracked yet" };
182
+ }
183
+
184
+ const stale30 = checkSourceStaleness(30, heartbeatPath);
185
+ const stale7 = checkSourceStaleness(7, heartbeatPath);
186
+
187
+ const summary = sources
188
+ .map((s) => `${s.source}: ${formatAge(s.lastIngest)}`)
189
+ .join(", ");
190
+
191
+ if (stale30.length > 0) {
192
+ const names = stale30.map((s) => `${s.source} (${s.daysSince}d)`).join(", ");
193
+ return { name: "source_health", status: "error", detail: `${summary} — critically stale: ${names}` };
194
+ }
195
+ if (stale7.length > 0) {
196
+ const names = stale7.map((s) => `${s.source} (${s.daysSince}d)`).join(", ");
197
+ return { name: "source_health", status: "warning", detail: `${summary} — stale: ${names}` };
198
+ }
199
+ return { name: "source_health", status: "ok", detail: `All sources fresh: ${summary}` };
200
+ }
201
+
202
+ /** Check 7 (F2): Interference density — detect semantic clustering pressure. */
155
203
  function checkInterferenceDensity(entries: MemoryEntry[]): CheckResult {
156
204
  // Only check entries with vectors (skip schema/empty entries)
157
205
  const withVectors = entries.filter(e => e.vector && e.vector.length > 0);
@@ -189,13 +237,78 @@ function checkInterferenceDensity(entries: MemoryEntry[]): CheckResult {
189
237
  // Main
190
238
  // ---------------------------------------------------------------------------
191
239
 
240
+ /** Check 8 (P0 B-1 观察): usage 四档分布 — injection-vs-use 影子统计的离线观察口。
241
+ * 纯报告,不接任何决策;cold = 被反复 surface(accessCount≥6)却从未被
242
+ * reconstruction 引用,是第二阶段 decay 联动前要先观察校准的核心对象。 */
243
+ function checkUsageDistribution(entries: MemoryEntry[]): CheckResult {
244
+ // use 信号未采集时(reconstruction flag 关),useCount 恒零 → "cold" 全是假阳性。
245
+ // 如实报告信号缺失,不输出不可解释的判定。
246
+ if (!isUsageSignalActive()) {
247
+ return {
248
+ name: "usage_distribution",
249
+ status: "ok",
250
+ detail: "use signal inactive (RECALLNEST_CONSTRUCTIVE_RETRIEVAL off) — useCount 无写入路径,cold/warm/hot 不可判,统计跳过。该 flag 因合成召回缺失被 Alice 主动关闭(2026-06-11),usage 线搁置中。",
251
+ };
252
+ }
253
+
254
+ const counts: Record<UsageStatus, number> = { unused: 0, cold: 0, warm: 0, hot: 0 };
255
+ const coldSamples: Array<{ id: string; injection: number; ageDays: number; scope: string }> = [];
256
+ const now = Date.now();
257
+
258
+ for (const e of entries) {
259
+ const status = deriveUsageStatus(e);
260
+ counts[status]++;
261
+ if (status === "cold") {
262
+ let injection = 0;
263
+ try {
264
+ const meta = JSON.parse(e.metadata || "{}") as Record<string, unknown>;
265
+ injection = typeof meta.accessCount === "number" ? meta.accessCount : 0;
266
+ } catch { /* keep 0 */ }
267
+ coldSamples.push({
268
+ id: e.id.slice(0, 8),
269
+ injection,
270
+ ageDays: Math.floor((now - e.timestamp) / 86_400_000),
271
+ scope: e.scope || "(none)",
272
+ });
273
+ }
274
+ }
275
+
276
+ const total = entries.length;
277
+ const coldPct = total > 0 ? (counts.cold / total) * 100 : 0;
278
+ const top = coldSamples
279
+ .sort((a, b) => b.injection - a.injection)
280
+ .slice(0, 20)
281
+ .map(s => `${s.id}(inj=${s.injection}, ${s.ageDays}d, ${s.scope})`)
282
+ .join(", ");
283
+
284
+ return {
285
+ name: "usage_distribution",
286
+ status: coldPct > 10 ? "warning" : "ok",
287
+ detail:
288
+ `unused=${counts.unused} cold=${counts.cold} (${coldPct.toFixed(1)}%) ` +
289
+ `warm=${counts.warm} hot=${counts.hot}` +
290
+ (coldSamples.length > 0 ? `; cold top-${Math.min(20, coldSamples.length)}: ${top}` : ""),
291
+ };
292
+ }
293
+
192
294
  export interface CheckupDeps {
193
- store: Pick<MemoryStore, "list" | "stats">;
295
+ store: Pick<MemoryStore, "list" | "stats" | "getVectors">;
194
296
  openConflictCount: number;
297
+ /** Optional override path for source-heartbeat.json (used in tests). */
298
+ heartbeatPath?: string;
195
299
  }
196
300
 
197
301
  export async function runDataCheckup(deps: CheckupDeps): Promise<CheckupReport> {
198
- const entries = await deps.store.list(undefined, undefined, 10000, 0);
302
+ const SCAN_LIMIT = 10000;
303
+ const listed = await deps.store.list(undefined, undefined, SCAN_LIMIT, 0);
304
+
305
+ // list() 为性能不返回向量(vector: []);维度/干扰检查靠向量,否则把"全 0 维"
306
+ // 误判成健康、干扰分析永远"样本不足"。补回真实向量再检查。
307
+ const vectorMap = await deps.store.getVectors(listed.map(e => e.id));
308
+ const entries = listed.map(e => ({ ...e, vector: vectorMap.get(e.id) ?? [] }));
309
+
310
+ // 截断披露:库总量 > 已扫,说明只体检了最近 SCAN_LIMIT 条。
311
+ const totalAvailable = (await deps.store.stats()).totalCount;
199
312
 
200
313
  return {
201
314
  checks: [
@@ -204,17 +317,23 @@ export async function runDataCheckup(deps: CheckupDeps): Promise<CheckupReport>
204
317
  checkTierDistribution(entries),
205
318
  checkConflictBacklog(deps.openConflictCount),
206
319
  checkVersionGroups(entries),
320
+ checkSourceHealth(deps.heartbeatPath),
207
321
  checkInterferenceDensity(entries),
322
+ checkUsageDistribution(entries),
208
323
  ],
209
324
  totalEntries: entries.length,
325
+ totalAvailable,
210
326
  timestamp: new Date().toISOString(),
211
327
  };
212
328
  }
213
329
 
214
330
  export function formatCheckupReport(report: CheckupReport): string {
331
+ const truncationNote = report.totalAvailable > report.totalEntries
332
+ ? ` (of ${report.totalAvailable} total — only the most recent ${report.totalEntries} analyzed)`
333
+ : "";
215
334
  const lines = [
216
335
  `Data Checkup Report (${report.timestamp})`,
217
- `Total entries scanned: ${report.totalEntries}`,
336
+ `Total entries scanned: ${report.totalEntries}${truncationNote}`,
218
337
  "",
219
338
  ];
220
339