recallnest 2.3.0 → 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 (221) hide show
  1. package/CHANGELOG.md +150 -0
  2. package/README.md +17 -31
  3. package/README_CN.md +12 -22
  4. package/assets/ui/app.js +1031 -0
  5. package/assets/ui/index.html +224 -0
  6. package/assets/ui/styles.css +781 -0
  7. package/config.json.example +4 -1
  8. package/docs/api-reference.md +1291 -0
  9. package/docs/architecture.md +168 -0
  10. package/docs/connector-spec.md +126 -0
  11. package/eval/cases-canary.json +58 -0
  12. package/eval/cases-scope-robustness.json +278 -0
  13. package/eval/cases.json +117 -20
  14. package/package.json +22 -1
  15. package/src/__tests__/access-tracker-novelty.test.ts +238 -0
  16. package/src/__tests__/activity-counter.test.ts +125 -0
  17. package/src/__tests__/adaptive-fidelity.test.ts +102 -0
  18. package/src/__tests__/admission-control.test.ts +206 -0
  19. package/src/__tests__/anchor-generator.test.ts +67 -0
  20. package/src/__tests__/auto-consolidation.test.ts +168 -0
  21. package/src/__tests__/auto-recall.test.ts +131 -0
  22. package/src/__tests__/babel-integration.test.ts +144 -0
  23. package/src/__tests__/batch-dedup.test.ts +65 -0
  24. package/src/__tests__/canary-eval.test.ts +114 -0
  25. package/src/__tests__/capture-engine.test.ts +1238 -0
  26. package/src/__tests__/capture-heuristic.test.ts +171 -0
  27. package/src/__tests__/cascade-forget.test.ts +210 -0
  28. package/src/__tests__/category-min-scores.test.ts +33 -0
  29. package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
  30. package/src/__tests__/circuit-breaker.test.ts +99 -0
  31. package/src/__tests__/cluster-consolidation.test.ts +782 -0
  32. package/src/__tests__/confidence-metadata.test.ts +125 -0
  33. package/src/__tests__/confidence-tracker.test.ts +261 -0
  34. package/src/__tests__/conflict-advisor.test.ts +206 -0
  35. package/src/__tests__/conflict-escalation.test.ts +149 -0
  36. package/src/__tests__/conflict-lifecycle.test.ts +83 -0
  37. package/src/__tests__/conflict-output.test.ts +85 -0
  38. package/src/__tests__/conflict-store.test.ts +374 -0
  39. package/src/__tests__/connector-v1.test.ts +181 -0
  40. package/src/__tests__/consolidation-engine.test.ts +333 -0
  41. package/src/__tests__/constructive-retrieval.test.ts +509 -0
  42. package/src/__tests__/context-collapse-renderer.test.ts +214 -0
  43. package/src/__tests__/context-composer-pins.test.ts +64 -0
  44. package/src/__tests__/context-composer-task-selection.test.ts +24 -0
  45. package/src/__tests__/context-reconstructor.test.ts +65 -0
  46. package/src/__tests__/context-renderer.test.ts +91 -0
  47. package/src/__tests__/continuity-eval.test.ts +275 -0
  48. package/src/__tests__/continuity-schema.test.ts +97 -0
  49. package/src/__tests__/contracts.test.ts +216 -0
  50. package/src/__tests__/conversation-importer.test.ts +692 -0
  51. package/src/__tests__/core-recall-eval.test.ts +235 -0
  52. package/src/__tests__/core-summary.test.ts +109 -0
  53. package/src/__tests__/data-checkup.test.ts +276 -0
  54. package/src/__tests__/decay-exemption.test.ts +84 -0
  55. package/src/__tests__/dedup-actions.test.ts +58 -0
  56. package/src/__tests__/dedup-guard.test.ts +39 -0
  57. package/src/__tests__/deterministic-id.test.ts +39 -0
  58. package/src/__tests__/distill-lock.test.ts +305 -0
  59. package/src/__tests__/distill-session-argshape.test.ts +79 -0
  60. package/src/__tests__/doctor.test.ts +319 -0
  61. package/src/__tests__/dream-pipeline.test.ts +321 -0
  62. package/src/__tests__/drill-down.test.ts +108 -0
  63. package/src/__tests__/embedder.test.ts +122 -0
  64. package/src/__tests__/emotion-decay.test.ts +46 -0
  65. package/src/__tests__/emotion-detector.test.ts +56 -0
  66. package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
  67. package/src/__tests__/emotion-retrieval.test.ts +56 -0
  68. package/src/__tests__/entity-resolver.test.ts +58 -0
  69. package/src/__tests__/env-config.test.ts +151 -0
  70. package/src/__tests__/error-signature-boost.test.ts +87 -0
  71. package/src/__tests__/error-signature.test.ts +95 -0
  72. package/src/__tests__/error-taxonomy.test.ts +129 -0
  73. package/src/__tests__/ethics-layer.test.ts +476 -0
  74. package/src/__tests__/event-segmenter.test.ts +153 -0
  75. package/src/__tests__/feature-flags-integration.test.ts +54 -0
  76. package/src/__tests__/frequency-tracker.test.ts +113 -0
  77. package/src/__tests__/graph-export.test.ts +489 -0
  78. package/src/__tests__/hotness-blend.test.ts +76 -0
  79. package/src/__tests__/ingest-chunking.test.ts +44 -0
  80. package/src/__tests__/ingest.test.ts +360 -0
  81. package/src/__tests__/interference-detector.test.ts +203 -0
  82. package/src/__tests__/kg-extractor.test.ts +192 -0
  83. package/src/__tests__/kg-store.test.ts +300 -0
  84. package/src/__tests__/llm-consolidation.test.ts +226 -0
  85. package/src/__tests__/maintenance-locks.test.ts +174 -0
  86. package/src/__tests__/mcp-efficiency.test.ts +288 -0
  87. package/src/__tests__/mcp-related-scopes.test.ts +151 -0
  88. package/src/__tests__/memory-boundaries.test.ts +250 -0
  89. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +345 -0
  90. package/src/__tests__/memory-evolution.test.ts +271 -0
  91. package/src/__tests__/memory-health-rebalance.test.ts +144 -0
  92. package/src/__tests__/memory-lint.test.ts +421 -0
  93. package/src/__tests__/memory-output.test.ts +215 -0
  94. package/src/__tests__/memory-promotion.test.ts +315 -0
  95. package/src/__tests__/migrate-dedup-ids.test.ts +65 -0
  96. package/src/__tests__/multi-vector.test.ts +242 -0
  97. package/src/__tests__/narrative-architecture.test.ts +456 -0
  98. package/src/__tests__/noise-filter.test.ts +205 -0
  99. package/src/__tests__/obsidian-connector.test.ts +363 -0
  100. package/src/__tests__/omitted-reporting.test.ts +98 -0
  101. package/src/__tests__/pending-review.test.ts +77 -0
  102. package/src/__tests__/pii-redact.test.ts +314 -0
  103. package/src/__tests__/post-retrieval-filter.test.ts +178 -0
  104. package/src/__tests__/ppr-traversal.test.ts +256 -0
  105. package/src/__tests__/prediction-engine.test.ts +349 -0
  106. package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
  107. package/src/__tests__/preference-guard-smoke.test.ts +211 -0
  108. package/src/__tests__/preference-matcher.test.ts +230 -0
  109. package/src/__tests__/preference-slots.test.ts +205 -0
  110. package/src/__tests__/prospective-memory.test.ts +283 -0
  111. package/src/__tests__/query-entity-detector.test.ts +171 -0
  112. package/src/__tests__/query-expander-alias.test.ts +146 -0
  113. package/src/__tests__/query-expander.test.ts +34 -0
  114. package/src/__tests__/recall-governor.test.ts +227 -0
  115. package/src/__tests__/recall-mode.test.ts +64 -0
  116. package/src/__tests__/result-synthesizer.test.ts +198 -0
  117. package/src/__tests__/resume-context.test.ts +4898 -0
  118. package/src/__tests__/retrieval-trace.test.ts +213 -0
  119. package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
  120. package/src/__tests__/retroactive-boost.test.ts +223 -0
  121. package/src/__tests__/salience-scoring.test.ts +106 -0
  122. package/src/__tests__/scope-inventory.test.ts +200 -0
  123. package/src/__tests__/scope-policy.test.ts +69 -0
  124. package/src/__tests__/scope-suggester.test.ts +80 -0
  125. package/src/__tests__/session-checkpoint.test.ts +291 -0
  126. package/src/__tests__/session-distiller.test.ts +640 -0
  127. package/src/__tests__/session-gc.test.ts +215 -0
  128. package/src/__tests__/skill-engine.test.ts +552 -0
  129. package/src/__tests__/skill-promotion.test.ts +601 -0
  130. package/src/__tests__/skill-verifier.test.ts +122 -0
  131. package/src/__tests__/source-heartbeat.test.ts +250 -0
  132. package/src/__tests__/store-patch-metadata.test.ts +205 -0
  133. package/src/__tests__/store-scope-audit.test.ts +81 -0
  134. package/src/__tests__/store-upsert.test.ts +184 -0
  135. package/src/__tests__/supersede-chain.test.ts +154 -0
  136. package/src/__tests__/temporal-parser.test.ts +241 -0
  137. package/src/__tests__/temporal-validity.test.ts +160 -0
  138. package/src/__tests__/term-registry.test.ts +121 -0
  139. package/src/__tests__/tool-error-surfacing.test.ts +98 -0
  140. package/src/__tests__/tool-output-compressor.test.ts +160 -0
  141. package/src/__tests__/topic-tag.test.ts +113 -0
  142. package/src/__tests__/trusted-memory.test.ts +839 -0
  143. package/src/__tests__/usage-tracker.test.ts +287 -0
  144. package/src/__tests__/version-manager.test.ts +213 -0
  145. package/src/__tests__/workflow-observation-skill-outcome.test.ts +352 -0
  146. package/src/__tests__/workflow-observation.test.ts +433 -0
  147. package/src/__tests__/write-verifier.test.ts +122 -0
  148. package/src/access-tracker.ts +38 -32
  149. package/src/activity-counter.ts +46 -18
  150. package/src/aliases.ts +133 -0
  151. package/src/api-server.ts +27 -7
  152. package/src/audit-log.ts +4 -2
  153. package/src/auto-consolidation.ts +8 -3
  154. package/src/auto-gc.ts +85 -21
  155. package/src/capture-engine.ts +54 -21
  156. package/src/cli.ts +169 -37
  157. package/src/confidence-tracker.ts +36 -18
  158. package/src/consolidation-engine.ts +109 -11
  159. package/src/context-composer.ts +8 -1
  160. package/src/data-checkup.ts +95 -6
  161. package/src/distill-lock.ts +194 -15
  162. package/src/doctor.ts +9 -2
  163. package/src/dream-pipeline.ts +118 -34
  164. package/src/env-config.ts +77 -0
  165. package/src/error-signature.ts +153 -0
  166. package/src/error-taxonomy.ts +115 -0
  167. package/src/eval.ts +251 -6
  168. package/src/idempotency.ts +8 -0
  169. package/src/ingest.ts +31 -10
  170. package/src/kg-extractor.ts +6 -2
  171. package/src/kg-store.ts +236 -44
  172. package/src/llm-consolidation.ts +2 -1
  173. package/src/mcp-server.ts +85 -1799
  174. package/src/mcp-tool-deps.ts +48 -0
  175. package/src/mcp-tools-advanced.ts +941 -0
  176. package/src/mcp-tools-core.ts +633 -0
  177. package/src/mcp-tools-governance.ts +475 -0
  178. package/src/memory-boundaries.ts +3 -1
  179. package/src/memory-evolution.ts +73 -1
  180. package/src/memory-lint.ts +183 -10
  181. package/src/memory-output.ts +114 -3
  182. package/src/memory-promotion.ts +246 -0
  183. package/src/memory-schema.ts +25 -2
  184. package/src/memory-store-port.ts +43 -0
  185. package/src/multi-vector.ts +2 -1
  186. package/src/narrative-schema.ts +2 -1
  187. package/src/noise-filter.ts +57 -6
  188. package/src/pii-detector.ts +157 -5
  189. package/src/query-expander.ts +90 -9
  190. package/src/result-synthesizer.ts +2 -1
  191. package/src/retention-policy.ts +2 -1
  192. package/src/retriever.ts +259 -139
  193. package/src/runtime-config.ts +8 -1
  194. package/src/scope-suggester.ts +113 -0
  195. package/src/session-distiller.ts +23 -2
  196. package/src/session-engine.ts +2 -1
  197. package/src/session-schema.ts +1 -0
  198. package/src/session-store.ts +5 -0
  199. package/src/skill-engine.ts +78 -0
  200. package/src/skill-promotion.ts +247 -47
  201. package/src/skill-schema.ts +16 -3
  202. package/src/skill-verifier.ts +152 -0
  203. package/src/store.ts +267 -38
  204. package/src/tool-output-compressor.ts +3 -1
  205. package/src/ui-server.ts +2 -1
  206. package/src/usage-tracker.ts +242 -0
  207. package/src/version-manager.ts +3 -2
  208. package/src/workflow-observation-engine.ts +2 -1
  209. package/src/workflow-observation-schema.ts +2 -1
  210. package/src/workflow-observation-store.ts +6 -2
  211. package/.env +0 -4
  212. package/CLAUDE.md +0 -46
  213. package/config.json +0 -55
  214. package/daily-recall.sh +0 -110
  215. package/logs/daily-recall.error.log +0 -0
  216. package/logs/daily-recall.log +0 -60
  217. package/logs/ingest-2026-04-11.log +0 -110
  218. package/logs/ingest-2026-04-12.log +0 -90
  219. package/logs/ingest-2026-04-13.log +0 -87
  220. package/logs/launchagent-stderr.log +0 -0
  221. package/logs/launchagent-stdout.log +0 -0
package/src/eval.ts CHANGED
@@ -15,7 +15,7 @@ import type { WorkflowObservationInput } from "./workflow-observation-schema.js"
15
15
  import { WorkflowObservationStore } from "./workflow-observation-store.js";
16
16
 
17
17
  export type ProfileName = "default" | "writing" | "debug" | "fact-check";
18
- export type EvalMode = "retrieval" | "continuity";
18
+ export type EvalMode = "retrieval" | "continuity" | "canary";
19
19
 
20
20
  export interface RetrievalEvalCase {
21
21
  name: string;
@@ -59,6 +59,47 @@ export interface ContinuityEvalCase {
59
59
  notes?: string;
60
60
  }
61
61
 
62
+ export interface CanaryEvalCase {
63
+ name: string;
64
+ query: string;
65
+ profile?: ProfileName;
66
+ scope?: string;
67
+ limit?: number;
68
+ /** A 类:该召回的 memory id(按 target id 算 rank) */
69
+ targets?: string[];
70
+ /** C 类:不该进 TopK 的干扰项 memory id */
71
+ hardNegatives?: string[];
72
+ /** 目标应进前几(默认 3) */
73
+ expectTopK?: number;
74
+ /** B 类新旧序:按此顺序排列,newer 在前,前者 rank 应小于后者 */
75
+ expectOrder?: string[];
76
+ /** D 类稳定偏好:召回文本命中任一要点即算对(不钉 id) */
77
+ expectContentAny?: string[];
78
+ /** 文本层禁词(沿用 retrieval 语义) */
79
+ forbid?: string[];
80
+ notes?: string;
81
+ }
82
+
83
+ export interface CanaryCaseReport {
84
+ mode: "canary";
85
+ name: string;
86
+ query: string;
87
+ profile: ProfileName;
88
+ score: number;
89
+ passed: boolean;
90
+ hitCount: number;
91
+ targetRanks: Array<{ id: string; rank: number | null }>;
92
+ top1Hit: boolean;
93
+ top3Hit: boolean;
94
+ forbiddenIdMatches: string[];
95
+ orderOk: boolean | null;
96
+ matchedContentAny: string[];
97
+ forbiddenMatches: string[];
98
+ topIds: string[];
99
+ topSnippet: string;
100
+ notes?: string;
101
+ }
102
+
62
103
  export interface RetrievalCaseReport {
63
104
  mode: "retrieval";
64
105
  name: string;
@@ -100,7 +141,7 @@ export interface ContinuityCaseReport {
100
141
  notes?: string;
101
142
  }
102
143
 
103
- type EvalReport = RetrievalCaseReport | ContinuityCaseReport;
144
+ type EvalReport = RetrievalCaseReport | ContinuityCaseReport | CanaryCaseReport;
104
145
 
105
146
  interface EvalArgs {
106
147
  mode: EvalMode;
@@ -144,7 +185,7 @@ function parseArgs(args: string[]): EvalArgs {
144
185
  const observationScopeIdx = args.indexOf("--observation-scope");
145
186
  const observationSourceIdx = args.indexOf("--observation-source");
146
187
  const modeRaw = modeIdx >= 0 ? args[modeIdx + 1] : "retrieval";
147
- const mode = modeRaw === "continuity" ? "continuity" : "retrieval";
188
+ const mode: EvalMode = modeRaw === "continuity" ? "continuity" : modeRaw === "canary" ? "canary" : "retrieval";
148
189
 
149
190
  return {
150
191
  mode,
@@ -158,9 +199,9 @@ function parseArgs(args: string[]): EvalArgs {
158
199
  }
159
200
 
160
201
  function defaultCasesPath(mode: EvalMode): string {
161
- return mode === "continuity"
162
- ? resolve(metaDir(import.meta), "../eval/continuity/cases.json")
163
- : resolve(metaDir(import.meta), "../eval/cases.json");
202
+ if (mode === "continuity") return resolve(metaDir(import.meta), "../eval/continuity/cases.json");
203
+ if (mode === "canary") return resolve(metaDir(import.meta), "../eval/cases-canary.json");
204
+ return resolve(metaDir(import.meta), "../eval/cases.json");
164
205
  }
165
206
 
166
207
  function loadCases<T>(mode: EvalMode, pathArg?: string): T[] {
@@ -221,6 +262,109 @@ export function scoreRetrievalCase(
221
262
  };
222
263
  }
223
264
 
265
+ export function scoreCanaryCase(
266
+ evalCase: CanaryEvalCase,
267
+ results: Array<{ entry: { id: string; text: string; scope: string; metadata?: string } }>,
268
+ ): CanaryCaseReport {
269
+ const profile = evalCase.profile || "default";
270
+ const ids = results.map((r) => r.entry.id);
271
+ const topK = evalCase.expectTopK ?? 3;
272
+ const limit = evalCase.limit ?? results.length;
273
+ const joined = results
274
+ .map((r) => `${r.entry.scope}\n${r.entry.text}\n${r.entry.metadata || ""}`)
275
+ .join("\n")
276
+ .toLowerCase();
277
+
278
+ // memory id 用前缀匹配:targets/expectOrder/hardNegatives 常写 8 位短前缀,
279
+ // 而召回结果的 entry.id 是完整 UUID(recallnest 惯例:≥8 hex 前缀即可定位)。
280
+ const idMatches = (fullId: string, target: string): boolean =>
281
+ fullId === target || fullId.startsWith(target);
282
+ const rankOf = (target: string): number | null => {
283
+ const idx = ids.findIndex((fullId) => idMatches(fullId, target));
284
+ return idx === -1 ? null : idx + 1;
285
+ };
286
+
287
+ // A 类:单目标命中
288
+ const targets = evalCase.targets || [];
289
+ const targetRanks = targets.map((id) => ({ id, rank: rankOf(id) }));
290
+ const top1Hit = targetRanks.some((t) => t.rank === 1);
291
+ const top3Hit = targetRanks.some((t) => t.rank !== null && t.rank <= Math.min(3, topK));
292
+
293
+ // C 类:干扰项进 TopK(limit 内)即违规(同样前缀匹配)
294
+ const forbiddenIdMatches = (evalCase.hardNegatives || []).filter((neg) => {
295
+ const rank = rankOf(neg);
296
+ return rank !== null && rank <= limit;
297
+ });
298
+
299
+ // B 类:新旧序,前者须召回且 rank 小于后者
300
+ let orderOk: boolean | null = null;
301
+ const order = evalCase.expectOrder || [];
302
+ if (order.length >= 2) {
303
+ orderOk = true;
304
+ for (let i = 0; i < order.length - 1; i += 1) {
305
+ const a = rankOf(order[i]);
306
+ const b = rankOf(order[i + 1]);
307
+ if (a === null) {
308
+ orderOk = false;
309
+ break;
310
+ }
311
+ if (b !== null && a >= b) {
312
+ orderOk = false;
313
+ break;
314
+ }
315
+ }
316
+ }
317
+
318
+ // D 类:内容要点命中
319
+ const matchedContentAny = matchedTerms(evalCase.expectContentAny, joined);
320
+
321
+ // 文本禁词
322
+ const forbiddenMatches = matchedTerms(evalCase.forbid, joined);
323
+
324
+ // 动态加权:只计 case 实际声明的维度
325
+ const parts: Array<{ weight: number; value: number }> = [];
326
+ if (targets.length > 0) {
327
+ const ranks = targetRanks.map((t) => t.rank).filter((r): r is number => r !== null);
328
+ const bestRank = ranks.length > 0 ? Math.min(...ranks) : null;
329
+ const targetScore = bestRank === null ? 0 : bestRank === 1 ? 1 : bestRank <= 3 ? 0.7 : 0.4;
330
+ parts.push({ weight: 0.5, value: targetScore });
331
+ }
332
+ if (orderOk !== null) {
333
+ parts.push({ weight: 0.2, value: orderOk ? 1 : 0 });
334
+ }
335
+ const contentAny = evalCase.expectContentAny || [];
336
+ if (contentAny.length > 0) {
337
+ parts.push({ weight: 0.3, value: matchedContentAny.length / contentAny.length });
338
+ }
339
+ const totalWeight = parts.reduce((sum, p) => sum + p.weight, 0);
340
+ let score = totalWeight > 0
341
+ ? parts.reduce((sum, p) => sum + p.weight * p.value, 0) / totalWeight
342
+ : (results.length > 0 ? 0.5 : 0);
343
+ if (forbiddenIdMatches.length > 0) score -= 0.3;
344
+ if (forbiddenMatches.length > 0) score -= 0.3;
345
+ score = Math.max(0, Math.min(1, score));
346
+
347
+ return {
348
+ mode: "canary",
349
+ name: evalCase.name,
350
+ query: evalCase.query,
351
+ profile,
352
+ score,
353
+ passed: score >= 0.7 && forbiddenIdMatches.length === 0 && forbiddenMatches.length === 0,
354
+ hitCount: results.length,
355
+ targetRanks,
356
+ top1Hit,
357
+ top3Hit,
358
+ forbiddenIdMatches,
359
+ orderOk,
360
+ matchedContentAny,
361
+ forbiddenMatches,
362
+ topIds: ids.slice(0, 5),
363
+ topSnippet: results[0] ? clip(results[0].entry.text) : "-",
364
+ notes: evalCase.notes,
365
+ };
366
+ }
367
+
224
368
  function joinResumeSections(response: ResumeContextResponse): string {
225
369
  return [
226
370
  response.summary,
@@ -438,6 +582,50 @@ function markdownRetrievalReport(reports: RetrievalCaseReport[]): string {
438
582
  return lines.join("\n");
439
583
  }
440
584
 
585
+ function markdownCanaryReport(reports: CanaryCaseReport[]): string {
586
+ const { passed, average } = summarizeReports(reports);
587
+ const top1 = reports.filter((r) => r.top1Hit).length;
588
+ const top3 = reports.filter((r) => r.top3Hit).length;
589
+
590
+ const lines = [
591
+ "# RecallNest Canary Eval",
592
+ "",
593
+ `- Generated: ${new Date().toISOString()}`,
594
+ `- Cases: ${reports.length}`,
595
+ `- Passed: ${passed}/${reports.length}`,
596
+ `- Average score: ${formatPercent(average)}`,
597
+ `- Top1 hit: ${top1}/${reports.length}`,
598
+ `- Top3 hit: ${top3}/${reports.length}`,
599
+ "",
600
+ "| Case | Profile | Score | Pass | Top1 | Top3 | Order | Forbid |",
601
+ "|------|---------|-------|------|------|------|-------|--------|",
602
+ ...reports.map((item) =>
603
+ `| ${item.name} | ${item.profile} | ${(item.score * 100).toFixed(0)}% | ${item.passed ? "yes" : "no"} | ${item.top1Hit ? "yes" : "-"} | ${item.top3Hit ? "yes" : "-"} | ${item.orderOk === null ? "-" : item.orderOk ? "ok" : "bad"} | ${item.forbiddenIdMatches.length + item.forbiddenMatches.length || "-"} |`,
604
+ ),
605
+ "",
606
+ "## Case Notes",
607
+ "",
608
+ ];
609
+
610
+ for (const item of reports) {
611
+ lines.push(`### ${item.name}`);
612
+ lines.push(`- Query: ${item.query}`);
613
+ lines.push(`- Score: ${(item.score * 100).toFixed(0)}% (${item.passed ? "pass" : "fail"})`);
614
+ lines.push(`- Target ranks: ${item.targetRanks.map((t) => `${t.id}=${t.rank ?? "miss"}`).join(", ") || "-"}`);
615
+ lines.push(`- Top1 / Top3: ${item.top1Hit ? "yes" : "no"} / ${item.top3Hit ? "yes" : "no"}`);
616
+ lines.push(`- Order ok: ${item.orderOk === null ? "n/a" : item.orderOk}`);
617
+ lines.push(`- Matched content: ${item.matchedContentAny.join(", ") || "-"}`);
618
+ lines.push(`- Forbidden id matches: ${item.forbiddenIdMatches.join(", ") || "-"}`);
619
+ lines.push(`- Forbidden term matches: ${item.forbiddenMatches.join(", ") || "-"}`);
620
+ lines.push(`- Top ids: ${item.topIds.join(", ") || "-"}`);
621
+ lines.push(`- Top snippet: ${item.topSnippet}`);
622
+ if (item.notes) lines.push(`- Notes: ${item.notes}`);
623
+ lines.push("");
624
+ }
625
+
626
+ return lines.join("\n");
627
+ }
628
+
441
629
  function markdownContinuityReport(reports: ContinuityCaseReport[]): string {
442
630
  const { passed, average } = summarizeReports(reports);
443
631
 
@@ -523,6 +711,44 @@ export async function runRetrievalEval(
523
711
  return reports;
524
712
  }
525
713
 
714
+ export async function runCanaryEval(
715
+ cases: CanaryEvalCase[],
716
+ deps: {
717
+ createEvalComponents?: EvalComponentFactory;
718
+ } = {},
719
+ ): Promise<CanaryCaseReport[]> {
720
+ const config = deps.createEvalComponents ? null : loadConfig();
721
+ const createEvalComponentsForCase = deps.createEvalComponents || createFreshEvalComponentsFactory(config!);
722
+ const reports: CanaryCaseReport[] = [];
723
+
724
+ for (const [index, evalCase] of cases.entries()) {
725
+ if (cases.length > 1) {
726
+ logInfo(`[INFO] canary-eval ${index + 1}/${cases.length}: ${evalCase.name}`);
727
+ }
728
+ const profileName = evalCase.profile || "default";
729
+ const { retriever, accessTracker } = createEvalComponentsForCase(profileName);
730
+ try {
731
+ const results = await retriever.retrieve({
732
+ query: evalCase.query,
733
+ limit: evalCase.limit || 8,
734
+ scopeFilter: evalCase.scope ? [evalCase.scope] : undefined,
735
+ source: "auto-recall",
736
+ });
737
+ const report = scoreCanaryCase(evalCase, results);
738
+ reports.push(report);
739
+ if (cases.length > 1) {
740
+ logInfo(
741
+ `[INFO] canary-eval ${index + 1}/${cases.length} done: ${evalCase.name} ${formatPercent(report.score)} ${report.passed ? "pass" : "fail"}`,
742
+ );
743
+ }
744
+ } finally {
745
+ accessTracker.destroy();
746
+ }
747
+ }
748
+
749
+ return reports;
750
+ }
751
+
526
752
  export async function runContinuityEval(
527
753
  cases: ContinuityEvalCase[],
528
754
  options: { recordObservations?: boolean; observationScope?: string; observationSource?: string } = {},
@@ -610,6 +836,25 @@ async function main() {
610
836
  return;
611
837
  }
612
838
 
839
+ if (args.mode === "canary") {
840
+ const cases = loadCases<CanaryEvalCase>("canary", args.casesPath);
841
+ const reports = await runCanaryEval(cases);
842
+ if (args.jsonMode) {
843
+ const payload = JSON.stringify({
844
+ mode: "canary",
845
+ generatedAt: new Date().toISOString(),
846
+ reports,
847
+ }, null, 2);
848
+ writeOutput(args.outputPath, payload);
849
+ console.log(payload);
850
+ return;
851
+ }
852
+ const output = markdownCanaryReport(reports);
853
+ writeOutput(args.outputPath, output);
854
+ console.log(output);
855
+ return;
856
+ }
857
+
613
858
  const cases = loadCases<RetrievalEvalCase>("retrieval", args.casesPath);
614
859
  const reports = await runRetrievalEval(cases);
615
860
  if (args.jsonMode) {
@@ -0,0 +1,8 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ export function buildIdempotentRecordId(prefix: string, key?: string): string | undefined {
4
+ const normalized = key?.replace(/\s+/g, " ").trim();
5
+ if (!normalized) return undefined;
6
+ const digest = createHash("sha256").update(normalized).digest("hex").slice(0, 32);
7
+ return `${prefix}-${digest}`;
8
+ }
package/src/ingest.ts CHANGED
@@ -12,6 +12,7 @@ import { readFileSync, readdirSync, existsSync, statSync, writeFileSync } from "
12
12
  import { join, basename, resolve, dirname } from "node:path";
13
13
  import { homedir } from "node:os";
14
14
  import { detectLang, tokenizeFts } from "./language-hook.js";
15
+ import { redactSecrets } from "./pii-detector.js";
15
16
  import type { MemoryStore, MemoryEntry } from "./store.js";
16
17
  import type { Embedder } from "./embedder.js";
17
18
  import { chunkDocument, type ChunkerConfig } from "./chunker.js";
@@ -20,6 +21,7 @@ import { isConnectorOutputV1 } from "./connector-types.js";
20
21
  import { isObsidianVault, scanVault } from "./obsidian-connector.js";
21
22
  import { segmentEvents, type EventSegmenterConfig, DEFAULT_EVENT_SEGMENTER_CONFIG } from "./event-segmenter.js";
22
23
  import { isProcessed, markProcessed } from "./tracker.js";
24
+ import * as envConfig from "./env-config.js";
23
25
  import type { LLMClient, SmartExtraction } from "./llm-client.js";
24
26
  import { resolveIngestBoundary } from "./memory-boundaries.js";
25
27
  import { isNoise } from "./noise-filter.js";
@@ -507,7 +509,7 @@ async function smartExtractBatch(
507
509
  * Tier 3.1: Check if core summary generation is enabled.
508
510
  */
509
511
  function isCoreSummaryEnabled(): boolean {
510
- return process.env.RECALLNEST_CORE_SUMMARY === "true";
512
+ return envConfig.coreSummary();
511
513
  }
512
514
 
513
515
  /**
@@ -625,7 +627,8 @@ export async function drainPendingQueue(
625
627
  let processed = 0, errors = 0;
626
628
  for (let i = 0; i < pending.length; i += 20) {
627
629
  const batch = pending.slice(i, i + 20);
628
- const texts = batch.map(c => c.text);
630
+ // F-3b: scrub before the LLM extraction call and downstream embedding
631
+ const texts = batch.map(c => redactSecrets(c.text).text);
629
632
  const extractions = await smartExtractBatch(texts, llm);
630
633
  const embeddingTexts = extractions.map(e => e.l1 || e.l0);
631
634
  const vectors = await embedder.embedBatchPassage(embeddingTexts);
@@ -860,7 +863,7 @@ function filterNoiseTurns(turns: ConversationTurn[]): ConversationTurn[] {
860
863
  return turns.filter((turn) => !isNoise(turn.text));
861
864
  }
862
865
 
863
- function groupTurnsIntoChunks(turns: ConversationTurn[]): Array<{
866
+ export function groupTurnsIntoChunks(turns: ConversationTurn[]): Array<{
864
867
  text: string;
865
868
  timestamp: string;
866
869
  sessionId: string;
@@ -933,7 +936,18 @@ function groupTurnsIntoChunks(turns: ConversationTurn[]): Array<{
933
936
  }
934
937
  }
935
938
 
936
- return chunks;
939
+ // P1 defence-in-depth: re-filter AFTER splitting. Long turns pass the
940
+ // turn-level filterNoiseTurns gate, but chunking can peel their tail into
941
+ // severed context-JSON fragments that the turn-level check never saw.
942
+ // F-3b: scrub secrets at the chunk boundary — this is the shared text
943
+ // finalization point for all session-ingest paths (CC/Codex/Gemini), before
944
+ // any chunk reaches embedding, LLM extraction, or storage.
945
+ return chunks
946
+ .filter((c) => !isNoise(c.text))
947
+ .map((c) => {
948
+ const r = redactSecrets(c.text);
949
+ return r.redacted > 0 ? { ...c, text: r.text } : c;
950
+ });
937
951
  }
938
952
 
939
953
  // ============================================================================
@@ -1027,9 +1041,14 @@ export async function ingestCodexSessions(
1027
1041
  errors: [],
1028
1042
  };
1029
1043
 
1030
- const baseDir = expandHome("~/.codex/sessions");
1031
- if (!existsSync(baseDir)) {
1032
- result.errors.push(`Codex sessions directory not found: ${baseDir}`);
1044
+ // 同时扫 sessions(活跃)+ archived_sessions(App 内归档会把文件移到这里,否则会漏)
1045
+ const baseDirs = [
1046
+ expandHome("~/.codex/sessions"),
1047
+ expandHome("~/.codex/archived_sessions"),
1048
+ ];
1049
+ const existingDirs = baseDirs.filter((d) => existsSync(d));
1050
+ if (existingDirs.length === 0) {
1051
+ result.errors.push(`Codex sessions directory not found: ${baseDirs.join(", ")}`);
1033
1052
  return result;
1034
1053
  }
1035
1054
 
@@ -1048,7 +1067,7 @@ export async function ingestCodexSessions(
1048
1067
  }
1049
1068
  }
1050
1069
  }
1051
- walk(baseDir);
1070
+ for (const dir of existingDirs) walk(dir);
1052
1071
  allFiles.sort();
1053
1072
 
1054
1073
  // --recent: only keep files modified within N hours
@@ -1650,7 +1669,8 @@ export async function ingestMarkdownFiles(
1650
1669
  continue;
1651
1670
  }
1652
1671
 
1653
- const texts = sections.map((s) => s.text);
1672
+ // F-3b: scrub before embedding and storage
1673
+ const texts = sections.map((s) => redactSecrets(s.text).text);
1654
1674
  const batchSize = 32;
1655
1675
  let fileChunks = 0;
1656
1676
 
@@ -1788,7 +1808,8 @@ export async function ingestGenericText(
1788
1808
 
1789
1809
  const chunkResult = chunkDocument(textToChunk, CONVERSATION_CHUNK_CONFIG);
1790
1810
 
1791
- const texts = chunkResult.chunks;
1811
+ // F-3b: scrub before embedding and storage
1812
+ const texts = chunkResult.chunks.map((t) => redactSecrets(t).text);
1792
1813
  const batchSize = 32;
1793
1814
 
1794
1815
  for (let i = 0; i < texts.length; i += batchSize) {
@@ -8,6 +8,7 @@ import type { LLMClient } from "./llm-client.js";
8
8
  import type { KGStore } from "./kg-store.js";
9
9
  import { logInfo, logWarn } from "./stderr-log.js";
10
10
  import { detectLang, getKgPromptHook } from "./language-hook.js";
11
+ import * as envConfig from "./env-config.js";
11
12
 
12
13
  // ============================================================================
13
14
  // Types
@@ -39,7 +40,7 @@ interface LlmExtractionResponse {
39
40
 
40
41
  /** Check if KG mode is enabled (env var) */
41
42
  export function isKGModeEnabled(): boolean {
42
- return process.env.RECALLNEST_KG_MODE === "true";
43
+ return envConfig.kgMode();
43
44
  }
44
45
 
45
46
  // ============================================================================
@@ -163,8 +164,11 @@ export class KGExtractor {
163
164
  prompt,
164
165
  );
165
166
  } catch (err) {
167
+ // Transport/API failure is NOT "zero triples" — propagate so callers can
168
+ // distinguish (backfill must not journal it as processed). The capture
169
+ // path's fire-and-forget .catch() keeps memory writes unaffected.
166
170
  logWarn(`[KG] LLM extraction failed: ${String(err)}`);
167
- return [];
171
+ throw err instanceof Error ? err : new Error(String(err));
168
172
  }
169
173
 
170
174
  if (!response?.triples || !Array.isArray(response.triples)) {