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.
- package/CHANGELOG.md +150 -0
- package/README.md +17 -31
- package/README_CN.md +12 -22
- package/assets/ui/app.js +1031 -0
- package/assets/ui/index.html +224 -0
- package/assets/ui/styles.css +781 -0
- package/config.json.example +4 -1
- package/docs/api-reference.md +1291 -0
- package/docs/architecture.md +168 -0
- package/docs/connector-spec.md +126 -0
- package/eval/cases-canary.json +58 -0
- package/eval/cases-scope-robustness.json +278 -0
- package/eval/cases.json +117 -20
- package/package.json +22 -1
- package/src/__tests__/access-tracker-novelty.test.ts +238 -0
- package/src/__tests__/activity-counter.test.ts +125 -0
- package/src/__tests__/adaptive-fidelity.test.ts +102 -0
- package/src/__tests__/admission-control.test.ts +206 -0
- package/src/__tests__/anchor-generator.test.ts +67 -0
- package/src/__tests__/auto-consolidation.test.ts +168 -0
- package/src/__tests__/auto-recall.test.ts +131 -0
- package/src/__tests__/babel-integration.test.ts +144 -0
- package/src/__tests__/batch-dedup.test.ts +65 -0
- package/src/__tests__/canary-eval.test.ts +114 -0
- package/src/__tests__/capture-engine.test.ts +1238 -0
- package/src/__tests__/capture-heuristic.test.ts +171 -0
- package/src/__tests__/cascade-forget.test.ts +210 -0
- package/src/__tests__/category-min-scores.test.ts +33 -0
- package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
- package/src/__tests__/circuit-breaker.test.ts +99 -0
- package/src/__tests__/cluster-consolidation.test.ts +782 -0
- package/src/__tests__/confidence-metadata.test.ts +125 -0
- package/src/__tests__/confidence-tracker.test.ts +261 -0
- package/src/__tests__/conflict-advisor.test.ts +206 -0
- package/src/__tests__/conflict-escalation.test.ts +149 -0
- package/src/__tests__/conflict-lifecycle.test.ts +83 -0
- package/src/__tests__/conflict-output.test.ts +85 -0
- package/src/__tests__/conflict-store.test.ts +374 -0
- package/src/__tests__/connector-v1.test.ts +181 -0
- package/src/__tests__/consolidation-engine.test.ts +333 -0
- package/src/__tests__/constructive-retrieval.test.ts +509 -0
- package/src/__tests__/context-collapse-renderer.test.ts +214 -0
- package/src/__tests__/context-composer-pins.test.ts +64 -0
- package/src/__tests__/context-composer-task-selection.test.ts +24 -0
- package/src/__tests__/context-reconstructor.test.ts +65 -0
- package/src/__tests__/context-renderer.test.ts +91 -0
- package/src/__tests__/continuity-eval.test.ts +275 -0
- package/src/__tests__/continuity-schema.test.ts +97 -0
- package/src/__tests__/contracts.test.ts +216 -0
- package/src/__tests__/conversation-importer.test.ts +692 -0
- package/src/__tests__/core-recall-eval.test.ts +235 -0
- package/src/__tests__/core-summary.test.ts +109 -0
- package/src/__tests__/data-checkup.test.ts +276 -0
- package/src/__tests__/decay-exemption.test.ts +84 -0
- package/src/__tests__/dedup-actions.test.ts +58 -0
- package/src/__tests__/dedup-guard.test.ts +39 -0
- package/src/__tests__/deterministic-id.test.ts +39 -0
- package/src/__tests__/distill-lock.test.ts +305 -0
- package/src/__tests__/distill-session-argshape.test.ts +79 -0
- package/src/__tests__/doctor.test.ts +319 -0
- package/src/__tests__/dream-pipeline.test.ts +321 -0
- package/src/__tests__/drill-down.test.ts +108 -0
- package/src/__tests__/embedder.test.ts +122 -0
- package/src/__tests__/emotion-decay.test.ts +46 -0
- package/src/__tests__/emotion-detector.test.ts +56 -0
- package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
- package/src/__tests__/emotion-retrieval.test.ts +56 -0
- package/src/__tests__/entity-resolver.test.ts +58 -0
- package/src/__tests__/env-config.test.ts +151 -0
- package/src/__tests__/error-signature-boost.test.ts +87 -0
- package/src/__tests__/error-signature.test.ts +95 -0
- package/src/__tests__/error-taxonomy.test.ts +129 -0
- package/src/__tests__/ethics-layer.test.ts +476 -0
- package/src/__tests__/event-segmenter.test.ts +153 -0
- package/src/__tests__/feature-flags-integration.test.ts +54 -0
- package/src/__tests__/frequency-tracker.test.ts +113 -0
- package/src/__tests__/graph-export.test.ts +489 -0
- package/src/__tests__/hotness-blend.test.ts +76 -0
- package/src/__tests__/ingest-chunking.test.ts +44 -0
- package/src/__tests__/ingest.test.ts +360 -0
- package/src/__tests__/interference-detector.test.ts +203 -0
- package/src/__tests__/kg-extractor.test.ts +192 -0
- package/src/__tests__/kg-store.test.ts +300 -0
- package/src/__tests__/llm-consolidation.test.ts +226 -0
- package/src/__tests__/maintenance-locks.test.ts +174 -0
- package/src/__tests__/mcp-efficiency.test.ts +288 -0
- package/src/__tests__/mcp-related-scopes.test.ts +151 -0
- package/src/__tests__/memory-boundaries.test.ts +250 -0
- package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +345 -0
- package/src/__tests__/memory-evolution.test.ts +271 -0
- package/src/__tests__/memory-health-rebalance.test.ts +144 -0
- package/src/__tests__/memory-lint.test.ts +421 -0
- package/src/__tests__/memory-output.test.ts +215 -0
- package/src/__tests__/memory-promotion.test.ts +315 -0
- package/src/__tests__/migrate-dedup-ids.test.ts +65 -0
- package/src/__tests__/multi-vector.test.ts +242 -0
- package/src/__tests__/narrative-architecture.test.ts +456 -0
- package/src/__tests__/noise-filter.test.ts +205 -0
- package/src/__tests__/obsidian-connector.test.ts +363 -0
- package/src/__tests__/omitted-reporting.test.ts +98 -0
- package/src/__tests__/pending-review.test.ts +77 -0
- package/src/__tests__/pii-redact.test.ts +314 -0
- package/src/__tests__/post-retrieval-filter.test.ts +178 -0
- package/src/__tests__/ppr-traversal.test.ts +256 -0
- package/src/__tests__/prediction-engine.test.ts +349 -0
- package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
- package/src/__tests__/preference-guard-smoke.test.ts +211 -0
- package/src/__tests__/preference-matcher.test.ts +230 -0
- package/src/__tests__/preference-slots.test.ts +205 -0
- package/src/__tests__/prospective-memory.test.ts +283 -0
- package/src/__tests__/query-entity-detector.test.ts +171 -0
- package/src/__tests__/query-expander-alias.test.ts +146 -0
- package/src/__tests__/query-expander.test.ts +34 -0
- package/src/__tests__/recall-governor.test.ts +227 -0
- package/src/__tests__/recall-mode.test.ts +64 -0
- package/src/__tests__/result-synthesizer.test.ts +198 -0
- package/src/__tests__/resume-context.test.ts +4898 -0
- package/src/__tests__/retrieval-trace.test.ts +213 -0
- package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
- package/src/__tests__/retroactive-boost.test.ts +223 -0
- package/src/__tests__/salience-scoring.test.ts +106 -0
- package/src/__tests__/scope-inventory.test.ts +200 -0
- package/src/__tests__/scope-policy.test.ts +69 -0
- package/src/__tests__/scope-suggester.test.ts +80 -0
- package/src/__tests__/session-checkpoint.test.ts +291 -0
- package/src/__tests__/session-distiller.test.ts +640 -0
- package/src/__tests__/session-gc.test.ts +215 -0
- package/src/__tests__/skill-engine.test.ts +552 -0
- package/src/__tests__/skill-promotion.test.ts +601 -0
- package/src/__tests__/skill-verifier.test.ts +122 -0
- package/src/__tests__/source-heartbeat.test.ts +250 -0
- package/src/__tests__/store-patch-metadata.test.ts +205 -0
- package/src/__tests__/store-scope-audit.test.ts +81 -0
- package/src/__tests__/store-upsert.test.ts +184 -0
- package/src/__tests__/supersede-chain.test.ts +154 -0
- package/src/__tests__/temporal-parser.test.ts +241 -0
- package/src/__tests__/temporal-validity.test.ts +160 -0
- package/src/__tests__/term-registry.test.ts +121 -0
- package/src/__tests__/tool-error-surfacing.test.ts +98 -0
- package/src/__tests__/tool-output-compressor.test.ts +160 -0
- package/src/__tests__/topic-tag.test.ts +113 -0
- package/src/__tests__/trusted-memory.test.ts +839 -0
- package/src/__tests__/usage-tracker.test.ts +287 -0
- package/src/__tests__/version-manager.test.ts +213 -0
- package/src/__tests__/workflow-observation-skill-outcome.test.ts +352 -0
- package/src/__tests__/workflow-observation.test.ts +433 -0
- package/src/__tests__/write-verifier.test.ts +122 -0
- package/src/access-tracker.ts +38 -32
- package/src/activity-counter.ts +46 -18
- package/src/aliases.ts +133 -0
- package/src/api-server.ts +27 -7
- package/src/audit-log.ts +4 -2
- package/src/auto-consolidation.ts +8 -3
- package/src/auto-gc.ts +85 -21
- package/src/capture-engine.ts +54 -21
- package/src/cli.ts +169 -37
- package/src/confidence-tracker.ts +36 -18
- package/src/consolidation-engine.ts +109 -11
- package/src/context-composer.ts +8 -1
- package/src/data-checkup.ts +95 -6
- package/src/distill-lock.ts +194 -15
- package/src/doctor.ts +9 -2
- package/src/dream-pipeline.ts +118 -34
- package/src/env-config.ts +77 -0
- package/src/error-signature.ts +153 -0
- package/src/error-taxonomy.ts +115 -0
- package/src/eval.ts +251 -6
- package/src/idempotency.ts +8 -0
- package/src/ingest.ts +31 -10
- package/src/kg-extractor.ts +6 -2
- package/src/kg-store.ts +236 -44
- package/src/llm-consolidation.ts +2 -1
- package/src/mcp-server.ts +85 -1799
- package/src/mcp-tool-deps.ts +48 -0
- package/src/mcp-tools-advanced.ts +941 -0
- package/src/mcp-tools-core.ts +633 -0
- package/src/mcp-tools-governance.ts +475 -0
- package/src/memory-boundaries.ts +3 -1
- package/src/memory-evolution.ts +73 -1
- package/src/memory-lint.ts +183 -10
- package/src/memory-output.ts +114 -3
- package/src/memory-promotion.ts +246 -0
- package/src/memory-schema.ts +25 -2
- package/src/memory-store-port.ts +43 -0
- package/src/multi-vector.ts +2 -1
- package/src/narrative-schema.ts +2 -1
- package/src/noise-filter.ts +57 -6
- package/src/pii-detector.ts +157 -5
- package/src/query-expander.ts +90 -9
- package/src/result-synthesizer.ts +2 -1
- package/src/retention-policy.ts +2 -1
- package/src/retriever.ts +259 -139
- package/src/runtime-config.ts +8 -1
- package/src/scope-suggester.ts +113 -0
- package/src/session-distiller.ts +23 -2
- package/src/session-engine.ts +2 -1
- package/src/session-schema.ts +1 -0
- package/src/session-store.ts +5 -0
- package/src/skill-engine.ts +78 -0
- package/src/skill-promotion.ts +247 -47
- package/src/skill-schema.ts +16 -3
- package/src/skill-verifier.ts +152 -0
- package/src/store.ts +267 -38
- package/src/tool-output-compressor.ts +3 -1
- package/src/ui-server.ts +2 -1
- package/src/usage-tracker.ts +242 -0
- package/src/version-manager.ts +3 -2
- package/src/workflow-observation-engine.ts +2 -1
- package/src/workflow-observation-schema.ts +2 -1
- package/src/workflow-observation-store.ts +6 -2
- package/.env +0 -4
- package/CLAUDE.md +0 -46
- package/config.json +0 -55
- package/daily-recall.sh +0 -110
- package/logs/daily-recall.error.log +0 -0
- package/logs/daily-recall.log +0 -60
- package/logs/ingest-2026-04-11.log +0 -110
- package/logs/ingest-2026-04-12.log +0 -90
- package/logs/ingest-2026-04-13.log +0 -87
- package/logs/launchagent-stderr.log +0 -0
- 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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
package/src/kg-extractor.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
171
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
168
172
|
}
|
|
169
173
|
|
|
170
174
|
if (!response?.triples || !Array.isArray(response.triples)) {
|