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