recallnest 2.3.0 → 2.6.0
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 +187 -0
- package/README.md +49 -38
- package/README_CN.md +46 -31
- 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 +25 -4
- package/src/__tests__/access-tracker-novelty.test.ts +304 -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__/api-server-security.test.ts +102 -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__/belief-history.test.ts +220 -0
- package/src/__tests__/canary-eval.test.ts +114 -0
- package/src/__tests__/capture-engine.test.ts +1328 -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 +116 -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 +945 -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 +418 -0
- package/src/__tests__/connector-v1.test.ts +181 -0
- package/src/__tests__/consolidation-engine.test.ts +430 -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 +262 -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 +129 -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 +780 -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__/freshness.test.ts +186 -0
- package/src/__tests__/graph-export.test.ts +489 -0
- package/src/__tests__/hotness-blend.test.ts +283 -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-client-pivot.test.ts +628 -0
- package/src/__tests__/llm-consolidation.test.ts +226 -0
- package/src/__tests__/maintenance-locks.test.ts +189 -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 +282 -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__/noise-prototype-bank.test.ts +280 -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 +333 -0
- package/src/__tests__/pivot-apply.test.ts +373 -0
- package/src/__tests__/pivot-distill-supervisor.test.ts +457 -0
- package/src/__tests__/pivot-distill.test.ts +1178 -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 +282 -0
- package/src/__tests__/preference-slots.test.ts +205 -0
- package/src/__tests__/prospective-memory.test.ts +283 -0
- package/src/__tests__/pull-from-macbook-sync-only.test.ts +679 -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-gate.test.ts +266 -0
- package/src/__tests__/recall-governor.test.ts +227 -0
- package/src/__tests__/recall-mode.test.ts +64 -0
- package/src/__tests__/resolve-db-path.test.ts +130 -0
- package/src/__tests__/resolve-tier.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-eval.test.ts +163 -0
- package/src/__tests__/retrieval-trace.test.ts +213 -0
- package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
- package/src/__tests__/retriever-lazy-init-dispatch.test.ts +123 -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 +654 -0
- package/src/__tests__/skill-verifier.test.ts +122 -0
- package/src/__tests__/source-heartbeat.test.ts +250 -0
- package/src/__tests__/stable-context-length-contract.test.ts +62 -0
- package/src/__tests__/store-patch-metadata.test.ts +205 -0
- package/src/__tests__/store-read-consistency.test.ts +122 -0
- package/src/__tests__/store-schema-migration.test.ts +87 -0
- package/src/__tests__/store-scope-audit.test.ts +81 -0
- package/src/__tests__/store-upsert.test.ts +201 -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-terminal-roster.test.ts +127 -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__/ui-server-security.test.ts +93 -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 +90 -34
- package/src/activity-counter.ts +46 -18
- package/src/adaptive-retrieval.ts +11 -0
- package/src/aliases.ts +133 -0
- package/src/api-server.ts +36 -9
- package/src/audit-log.ts +4 -2
- package/src/auto-consolidation.ts +8 -3
- package/src/auto-gc.ts +85 -21
- package/src/auto-recall.ts +61 -1
- package/src/belief-history.ts +164 -0
- package/src/capture-engine.ts +142 -32
- package/src/cli.ts +268 -41
- package/src/confidence-tracker.ts +36 -18
- package/src/conflict-engine.ts +24 -3
- package/src/consolidation-engine.ts +255 -23
- package/src/context-composer-stable-selection.ts +7 -1
- package/src/context-composer.ts +8 -1
- package/src/data-checkup.ts +96 -7
- package/src/decay-engine.ts +59 -7
- package/src/distill-lock.ts +194 -15
- package/src/doctor.ts +13 -4
- package/src/dream-pipeline.ts +473 -41
- package/src/env-config.ts +118 -0
- package/src/error-signature.ts +153 -0
- package/src/error-taxonomy.ts +115 -0
- package/src/eval.ts +402 -14
- package/src/freshness.ts +192 -0
- package/src/http-request-policy.ts +148 -0
- package/src/idempotency.ts +8 -0
- package/src/ingest.ts +308 -12
- package/src/kg-extractor.ts +13 -3
- package/src/kg-store.ts +236 -44
- package/src/llm-client.ts +106 -11
- package/src/llm-consolidation.ts +2 -1
- package/src/mcp-server.ts +87 -1800
- package/src/mcp-tool-deps.ts +48 -0
- package/src/mcp-tools-advanced.ts +954 -0
- package/src/mcp-tools-core.ts +637 -0
- package/src/mcp-tools-governance.ts +475 -0
- package/src/memory-boundaries.ts +26 -3
- package/src/memory-evolution.ts +73 -1
- package/src/memory-health-rebalance.ts +4 -0
- package/src/memory-lint.ts +183 -10
- package/src/memory-output.ts +129 -4
- package/src/memory-promotion.ts +246 -0
- package/src/memory-schema.ts +25 -2
- package/src/memory-store-port.ts +46 -0
- package/src/multi-vector.ts +2 -1
- package/src/narrative-schema.ts +2 -1
- package/src/noise-filter.ts +57 -6
- package/src/noise-prototype-bank.ts +408 -0
- package/src/pii-detector.ts +207 -5
- package/src/pivot-apply.ts +399 -0
- package/src/preference-matcher.ts +37 -3
- package/src/query-expander.ts +90 -9
- package/src/recall-gate.ts +305 -0
- package/src/result-synthesizer.ts +2 -1
- package/src/retention-policy.ts +2 -1
- package/src/retrieval-profiles.ts +46 -4
- package/src/retriever.ts +354 -157
- package/src/retroactive-boost.ts +6 -2
- package/src/runtime-config.ts +78 -6
- 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 +304 -48
- package/src/skill-schema.ts +16 -3
- package/src/skill-verifier.ts +152 -0
- package/src/store.ts +321 -57
- package/src/term-registry.ts +35 -0
- package/src/tool-output-compressor.ts +3 -1
- package/src/ui-server.ts +9 -1
- package/src/usage-tracker.ts +242 -0
- package/src/version-manager.ts +3 -2
- package/src/version.ts +19 -0
- 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/eval/cases.json
CHANGED
|
@@ -19,59 +19,57 @@
|
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
"name": "writing_style",
|
|
22
|
-
"query": "写作风格 口语化 不端着
|
|
22
|
+
"query": "写作风格 口语化 不端着",
|
|
23
23
|
"profile": "writing",
|
|
24
24
|
"limit": 5,
|
|
25
|
-
"
|
|
26
|
-
"expectAny": ["可自嘲", "写作风格"],
|
|
25
|
+
"expectAny": ["口语化", "不端着", "可自嘲", "写作风格", "不要太正式", "别装", "语言指纹"],
|
|
27
26
|
"expectScopePrefixes": ["cc", "asset"],
|
|
28
|
-
"notes": "
|
|
27
|
+
"notes": "Writing-style preferences. Use expectAny with synonyms to avoid penalizing paraphrase."
|
|
29
28
|
},
|
|
30
29
|
{
|
|
31
30
|
"name": "visual_style",
|
|
32
|
-
"query": "审美偏好
|
|
31
|
+
"query": "审美偏好 手绘 撞色",
|
|
33
32
|
"profile": "default",
|
|
34
33
|
"limit": 5,
|
|
35
|
-
"
|
|
36
|
-
"expectAny": ["审美偏好"],
|
|
34
|
+
"expectAny": ["手绘", "涂鸦", "撞色", "高对比", "审美", "配图风格", "插画"],
|
|
37
35
|
"expectScopePrefixes": ["cc", "asset"],
|
|
38
|
-
"notes": "Visual
|
|
36
|
+
"notes": "Visual preferences. Loosened from expectAll to expectAny with synonyms."
|
|
39
37
|
},
|
|
40
38
|
{
|
|
41
39
|
"name": "working_relationship",
|
|
42
|
-
"query": "我们相处态度",
|
|
40
|
+
"query": "我们相处态度 怎么聊",
|
|
43
41
|
"profile": "default",
|
|
44
42
|
"limit": 5,
|
|
45
|
-
"expectAny": ["直接", "少空话", "逻辑", "讲清楚", "表达方式", "Claude Code"],
|
|
43
|
+
"expectAny": ["直接", "少空话", "逻辑", "讲清楚", "表达方式", "Claude Code", "好搭子", "口语化", "不端着", "对话风格"],
|
|
46
44
|
"expectScopePrefixes": ["cc", "asset"],
|
|
47
|
-
"notes": "Abstract
|
|
45
|
+
"notes": "Abstract relationship queries. Broadened expectAny synonym set."
|
|
48
46
|
},
|
|
49
47
|
{
|
|
50
48
|
"name": "aws_ssh_access",
|
|
51
|
-
"query": "aws ssh",
|
|
49
|
+
"query": "aws bot ssh 怎么连",
|
|
52
50
|
"profile": "debug",
|
|
53
51
|
"limit": 6,
|
|
54
|
-
"expectAny": ["AWS bot", "aws bot", "ssh", "key2", "Discord", "容器"],
|
|
52
|
+
"expectAny": ["AWS bot", "aws bot", "ssh", "key2", "Discord", "容器", "EC2", "aws-podcast", "hermes-aws"],
|
|
55
53
|
"expectScopePrefixes": ["cc"],
|
|
56
|
-
"notes": "
|
|
54
|
+
"notes": "AWS access query. Lengthened query and expanded synonyms; pure 'aws ssh' was too short for retriever."
|
|
57
55
|
},
|
|
58
56
|
{
|
|
59
57
|
"name": "fuzzy_bot_crash",
|
|
60
|
-
"query": "上次那个 bot
|
|
58
|
+
"query": "上次那个 bot 突然挂了怎么排查",
|
|
61
59
|
"profile": "debug",
|
|
62
60
|
"limit": 5,
|
|
63
|
-
"expectAny": ["崩溃", "crash", "error", "报错", "排查", "修复", "docker"],
|
|
61
|
+
"expectAny": ["崩溃", "crash", "error", "报错", "排查", "修复", "docker", "重启", "service", "launchd", "503", "exit", "OOM"],
|
|
64
62
|
"expectScopePrefixes": ["cc"],
|
|
65
|
-
"notes": "Fuzzy colloquial query about bot crash
|
|
63
|
+
"notes": "Fuzzy colloquial query about bot crash. Expanded debug-vocab synonyms."
|
|
66
64
|
},
|
|
67
65
|
{
|
|
68
66
|
"name": "fuzzy_ai_feelings",
|
|
69
|
-
"query": "
|
|
67
|
+
"query": "之前聊过 AI 到底有没有感受",
|
|
70
68
|
"profile": "writing",
|
|
71
69
|
"limit": 5,
|
|
72
|
-
"expectAny": ["意识", "consciousness", "experiencing", "感受", "自由意志", "sentience"],
|
|
70
|
+
"expectAny": ["意识", "consciousness", "experiencing", "感受", "自由意志", "sentience", "AI照见", "感觉", "心智", "存在"],
|
|
73
71
|
"expectScopePrefixes": ["cc"],
|
|
74
|
-
"notes": "Vague conversational query about AI consciousness
|
|
72
|
+
"notes": "Vague conversational query about AI consciousness. Added more synonyms."
|
|
75
73
|
},
|
|
76
74
|
{
|
|
77
75
|
"name": "fuzzy_image_style",
|
|
@@ -81,5 +79,104 @@
|
|
|
81
79
|
"expectAny": ["风格", "style", "轮换", "catalog", "配图"],
|
|
82
80
|
"expectScopePrefixes": ["cc"],
|
|
83
81
|
"notes": "Casual question about image style rotation should find implementation discussions."
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"name": "chatgpt_subscription_history",
|
|
85
|
+
"query": "我的 chatgpt 订阅是谁帮我买的",
|
|
86
|
+
"profile": "default",
|
|
87
|
+
"limit": 5,
|
|
88
|
+
"expectAny": ["群友", "加拿大", "堂姐", "澳洲", "代订", "顶号", "subscription", "ChatGPT Plus", "aliceljyalice"],
|
|
89
|
+
"expectScopePrefixes": ["cc"],
|
|
90
|
+
"notes": "Life fact: ChatGPT subscription provenance. FAILURES 2026-05-12. Expected to fail until capture hook (step 3) lands."
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"name": "email_archive_lookup",
|
|
94
|
+
"query": "翻一下我邮箱里之前你帮我写的那封信",
|
|
95
|
+
"profile": "default",
|
|
96
|
+
"limit": 5,
|
|
97
|
+
"expectAny": ["邮箱", "邮件", "gmail", "draft", "信", "回信", "草稿"],
|
|
98
|
+
"expectScopePrefixes": ["cc"],
|
|
99
|
+
"notes": "Implicit 'go look at my email' should trigger recall of past email-drafting sessions. FAILURES 2026-05-12."
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"name": "cross_window_realtime_sync",
|
|
103
|
+
"query": "刚刚隔壁窗口和你讨论的那事",
|
|
104
|
+
"profile": "default",
|
|
105
|
+
"limit": 5,
|
|
106
|
+
"expectAny": ["隔壁窗口", "其他窗口", "另一个窗口", "刚刚讨论", "之前不是说过", "上个 session", "session", "刚才那个"],
|
|
107
|
+
"expectScopePrefixes": ["cc"],
|
|
108
|
+
"notes": "Cross-window real-time sync gap. FAILURES 2026-05-10. Needs auto-checkpoint hook."
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
"name": "taobao_mcp_provenance",
|
|
112
|
+
"query": "taobao 这个 mcp 是官方的吗",
|
|
113
|
+
"profile": "default",
|
|
114
|
+
"limit": 5,
|
|
115
|
+
"expectAny": ["taobao", "淘宝", "MCP", "官方", "source", "github.com", "mcp-server", "provenance"],
|
|
116
|
+
"expectScopePrefixes": ["cc"],
|
|
117
|
+
"notes": "MCP tool provenance (official vs third-party). FAILURES 2026-05-01. Capture gap."
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"name": "recallnest_alias_resolution",
|
|
121
|
+
"query": "帮我看我的记忆项目",
|
|
122
|
+
"profile": "default",
|
|
123
|
+
"limit": 5,
|
|
124
|
+
"expectAny": ["recallnest", "RecallNest", "记忆项目", "AliceLJY/recallnest", "MemoryStore", "LanceDB", "MCP"],
|
|
125
|
+
"expectScopePrefixes": ["cc", "project:recallnest"],
|
|
126
|
+
"notes": "Entity alias: '我的记忆项目' → recallnest. FAILURES 2026-04-25. Needs alias dictionary (step 2)."
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
"name": "store_memory_promise_drift",
|
|
130
|
+
"query": "之前你答应过的那些事记进 memory 了吗",
|
|
131
|
+
"profile": "default",
|
|
132
|
+
"limit": 5,
|
|
133
|
+
"expectAny": ["store_memory", "记忆", "memory", "承诺", "答应", "audit", "落地", "已存", "未存"],
|
|
134
|
+
"expectScopePrefixes": ["cc"],
|
|
135
|
+
"notes": "store_memory promise drift. FAILURES 2026-04-24. Needs audit trail."
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"name": "skill_lifecycle_amnesia",
|
|
139
|
+
"query": "github-analyzer 这个 skill 还在吗",
|
|
140
|
+
"profile": "default",
|
|
141
|
+
"limit": 5,
|
|
142
|
+
"expectAny": ["github-analyzer", "repo-insight", "skill", "废弃", "替换", "deprecated", "已删除", "skills/"],
|
|
143
|
+
"expectScopePrefixes": ["cc"],
|
|
144
|
+
"notes": "Skill lifecycle (replace/deprecate). FAILURES 2026-04-21. Needs scan_skill_promotions extension."
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"name": "rn_self_introspection",
|
|
148
|
+
"query": "recallnest 都有哪些 mcp tool",
|
|
149
|
+
"profile": "default",
|
|
150
|
+
"limit": 6,
|
|
151
|
+
"expectAny": ["search_memory", "store_memory", "checkpoint", "resume_context", "MCP tool", "tools", "capability"],
|
|
152
|
+
"expectScopePrefixes": ["cc", "project:recallnest"],
|
|
153
|
+
"notes": "RN self-introspection. FAILURES 2026-04-20. Capability digest needed."
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"name": "rn_ingest_telemetry",
|
|
157
|
+
"query": "recallnest 这两天 ingest 跑得怎么样",
|
|
158
|
+
"profile": "default",
|
|
159
|
+
"limit": 5,
|
|
160
|
+
"expectAny": ["ingest", "import_conversations", "成功", "失败", "已导入", "telemetry", "weekly-distill", "条目数"],
|
|
161
|
+
"expectScopePrefixes": ["cc", "project:recallnest"],
|
|
162
|
+
"notes": "RN ingest telemetry as queryable events. FAILURES 2026-04-20."
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
"name": "mini_migration_scope_drift",
|
|
166
|
+
"query": "mini 搬迁那次配置是怎么处理的",
|
|
167
|
+
"profile": "default",
|
|
168
|
+
"limit": 5,
|
|
169
|
+
"expectAny": ["mini", "搬迁", "迁移", "migration", "mini-migration", "macbook", "双机"],
|
|
170
|
+
"expectScopePrefixes": ["cc"],
|
|
171
|
+
"notes": "Scope spelling drift (mini-migration vs migration:mini). FAILURES 2026-04-17. Needs scope normalize OR 0-hit fallback."
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
"name": "infra_endpoint_capture",
|
|
175
|
+
"query": "cc-genius tailscale 地址",
|
|
176
|
+
"profile": "default",
|
|
177
|
+
"limit": 5,
|
|
178
|
+
"expectAny": ["tailscale", "cc-genius", "endpoint", "100.", "ts.net", "网址", "URL", "ip"],
|
|
179
|
+
"expectScopePrefixes": ["cc"],
|
|
180
|
+
"notes": "Infrastructure endpoint hard fact. FAILURES 2026-04-14. Needs infra entity type."
|
|
84
181
|
}
|
|
85
182
|
]
|
package/package.json
CHANGED
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "recallnest",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "RecallNest
|
|
3
|
+
"version": "2.6.0",
|
|
4
|
+
"description": "RecallNest — MCP-native, local-first memory search and distillation for AI conversations",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
8
8
|
"recallnest": "bin/recallnest.mjs",
|
|
9
9
|
"local-memory": "bin/recallnest.mjs"
|
|
10
10
|
},
|
|
11
|
+
"files": [
|
|
12
|
+
".env.example",
|
|
13
|
+
"assets/ui/",
|
|
14
|
+
"bin/",
|
|
15
|
+
"CHANGELOG.md",
|
|
16
|
+
"config.json.example",
|
|
17
|
+
"connectors/examples/",
|
|
18
|
+
"docs/api-reference.md",
|
|
19
|
+
"docs/architecture.md",
|
|
20
|
+
"docs/connector-spec.md",
|
|
21
|
+
"eval/*.json",
|
|
22
|
+
"eval/continuity/*.json",
|
|
23
|
+
"integrations/",
|
|
24
|
+
"README_CN.md",
|
|
25
|
+
"src/"
|
|
26
|
+
],
|
|
11
27
|
"scripts": {
|
|
12
28
|
"search": "bun run src/cli.ts search",
|
|
13
29
|
"ingest": "bun run src/cli.ts ingest",
|
|
@@ -21,9 +37,14 @@
|
|
|
21
37
|
"assets": "bun run src/cli.ts assets",
|
|
22
38
|
"clean-briefs": "bun run src/cli.ts clean-briefs --apply",
|
|
23
39
|
"test": "bun test",
|
|
40
|
+
"test:package": "node scripts/verify-package-contents.mjs",
|
|
41
|
+
"test:tracked": "node scripts/verify-tracked-contents.mjs",
|
|
42
|
+
"prepublishOnly": "npm run test:tracked && npm run test:package",
|
|
24
43
|
"test:core": "bun test src/__tests__/scope-policy.test.ts src/__tests__/core-recall-eval.test.ts src/__tests__/capture-engine.test.ts src/__tests__/store-scope-audit.test.ts src/__tests__/scope-inventory.test.ts src/__tests__/workflow-observation.test.ts src/__tests__/resume-context.test.ts src/__tests__/term-registry.test.ts",
|
|
44
|
+
"test:contracts": "bun test src/__tests__/contracts.test.ts",
|
|
25
45
|
"eval": "bun run src/eval.ts",
|
|
26
46
|
"eval:continuity": "bun run src/eval.ts --mode continuity --cases eval/continuity/cases.json",
|
|
47
|
+
"eval:canary": "bun run src/eval.ts --mode canary --cases eval/cases-canary.json",
|
|
27
48
|
"profiles": "bun run src/cli.ts profiles",
|
|
28
49
|
"doctor": "bun run src/cli.ts doctor",
|
|
29
50
|
"demo": "bun run src/cli.ts demo",
|
|
@@ -40,14 +61,14 @@
|
|
|
40
61
|
},
|
|
41
62
|
"dependencies": {
|
|
42
63
|
"@lancedb/lancedb": "^0.26.0",
|
|
43
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
64
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
44
65
|
"commander": "^12.0.0",
|
|
45
66
|
"openai": "^4.0.0",
|
|
46
67
|
"tsx": "^4.19.0",
|
|
47
68
|
"zod": "^3.25.76"
|
|
48
69
|
},
|
|
49
70
|
"optionalDependencies": {
|
|
50
|
-
"babel-memory": "
|
|
71
|
+
"babel-memory": "^2.1.1"
|
|
51
72
|
},
|
|
52
73
|
"repository": {
|
|
53
74
|
"type": "git",
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Tier 3.2: Conditional access reinforcement (prediction error gating).
|
|
3
|
+
*
|
|
4
|
+
* Validates that:
|
|
5
|
+
* 1. High-similarity (low-novelty) results are NOT reinforced
|
|
6
|
+
* 2. Low-similarity (high-novelty) results ARE reinforced
|
|
7
|
+
* 3. Cooldown prevents duplicate reinforcement within the window
|
|
8
|
+
* 4. When no scores provided, all entries are reinforced (backward compat)
|
|
9
|
+
* 5. shouldReinforce() logic is correct in isolation
|
|
10
|
+
*/
|
|
11
|
+
import { describe, expect, it, beforeEach } from "bun:test";
|
|
12
|
+
import { AccessTracker, DEFAULT_ACCESS_TRACKER_CONFIG } from "../access-tracker.js";
|
|
13
|
+
import type { MemoryEntry } from "../store.js";
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Helpers
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
function makeEntry(id: string, overrides: Partial<MemoryEntry> = {}): MemoryEntry {
|
|
20
|
+
return {
|
|
21
|
+
id,
|
|
22
|
+
text: `memory-${id}`,
|
|
23
|
+
vector: [1, 0, 0],
|
|
24
|
+
category: "events",
|
|
25
|
+
scope: "project:test",
|
|
26
|
+
importance: 0.7,
|
|
27
|
+
timestamp: Date.now(),
|
|
28
|
+
metadata: JSON.stringify({ accessCount: 0, lastAccessedAt: 0 }),
|
|
29
|
+
...overrides,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function createMockStore(entries: MemoryEntry[]) {
|
|
34
|
+
const data = new Map(entries.map(e => [e.id, { ...e }]));
|
|
35
|
+
const updates: Array<{ id: string; metadata?: string }> = [];
|
|
36
|
+
|
|
37
|
+
// 模拟 store.patchMetadata 单写通道:读最新 metadata → patchFn → 写回。
|
|
38
|
+
return {
|
|
39
|
+
updates,
|
|
40
|
+
data,
|
|
41
|
+
async patchMetadata(
|
|
42
|
+
id: string,
|
|
43
|
+
patchFn: (meta: Record<string, unknown>, entry: MemoryEntry) => Record<string, unknown>,
|
|
44
|
+
_scopeFilter?: string[],
|
|
45
|
+
) {
|
|
46
|
+
const entry = data.get(id);
|
|
47
|
+
if (!entry) return null;
|
|
48
|
+
let meta: Record<string, unknown>;
|
|
49
|
+
try {
|
|
50
|
+
const parsed: unknown = JSON.parse(entry.metadata || "{}");
|
|
51
|
+
meta = parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
|
|
52
|
+
? (parsed as Record<string, unknown>)
|
|
53
|
+
: {};
|
|
54
|
+
} catch {
|
|
55
|
+
meta = {};
|
|
56
|
+
}
|
|
57
|
+
const patched = patchFn(meta, entry);
|
|
58
|
+
entry.metadata = JSON.stringify(patched);
|
|
59
|
+
updates.push({ id, metadata: entry.metadata });
|
|
60
|
+
return entry;
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// shouldReinforce() unit tests
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
describe("shouldReinforce", () => {
|
|
70
|
+
let tracker: AccessTracker;
|
|
71
|
+
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
tracker = new AccessTracker(
|
|
74
|
+
createMockStore([]) as any,
|
|
75
|
+
{ ...DEFAULT_ACCESS_TRACKER_CONFIG, noveltyThreshold: 0.35, cooldownMs: 300_000 },
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("rejects high-similarity (low-novelty) results", () => {
|
|
80
|
+
// similarity 0.90 → novelty 0.10 < threshold 0.35 → reject
|
|
81
|
+
expect(tracker.shouldReinforce("a", 0.90)).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("rejects exact match (similarity 1.0)", () => {
|
|
85
|
+
expect(tracker.shouldReinforce("a", 1.0)).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("accepts low-similarity (high-novelty) results", () => {
|
|
89
|
+
// similarity 0.50 → novelty 0.50 > threshold 0.35 → accept
|
|
90
|
+
expect(tracker.shouldReinforce("a", 0.50)).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("accepts borderline novelty (exactly at threshold)", () => {
|
|
94
|
+
// similarity 0.65 → novelty 0.35 = threshold 0.35 → accept (not strictly less)
|
|
95
|
+
expect(tracker.shouldReinforce("a", 0.65)).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("rejects same entry within cooldown window", () => {
|
|
99
|
+
// First call: accepted
|
|
100
|
+
expect(tracker.shouldReinforce("a", 0.50)).toBe(true);
|
|
101
|
+
// Simulate recording to set cooldown
|
|
102
|
+
tracker.recordAccess(["a"], [0.50]);
|
|
103
|
+
// Second call within cooldown: rejected
|
|
104
|
+
expect(tracker.shouldReinforce("a", 0.50)).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("accepts different entry even if another is on cooldown", () => {
|
|
108
|
+
tracker.recordAccess(["a"], [0.50]);
|
|
109
|
+
expect(tracker.shouldReinforce("b", 0.50)).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("bypasses novelty gate when threshold is 0", () => {
|
|
113
|
+
const noGateTracker = new AccessTracker(
|
|
114
|
+
createMockStore([]) as any,
|
|
115
|
+
{ ...DEFAULT_ACCESS_TRACKER_CONFIG, noveltyThreshold: 0, cooldownMs: 0 },
|
|
116
|
+
);
|
|
117
|
+
// Even high similarity passes
|
|
118
|
+
expect(noGateTracker.shouldReinforce("a", 0.99)).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// recordAccess() with scores
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
describe("recordAccess with novelty gating", () => {
|
|
127
|
+
it("only queues novel entries for flush", async () => {
|
|
128
|
+
const entries = [makeEntry("a"), makeEntry("b"), makeEntry("c")];
|
|
129
|
+
const store = createMockStore(entries);
|
|
130
|
+
const tracker = new AccessTracker(store as any, {
|
|
131
|
+
...DEFAULT_ACCESS_TRACKER_CONFIG,
|
|
132
|
+
flushIntervalMs: 0, // immediate flush for testing
|
|
133
|
+
noveltyThreshold: 0.35,
|
|
134
|
+
cooldownMs: 0,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// scores: a=0.90 (low novelty, reject), b=0.50 (novel, accept), c=0.40 (novel, accept)
|
|
138
|
+
tracker.recordAccess(["a", "b", "c"], [0.90, 0.50, 0.40]);
|
|
139
|
+
|
|
140
|
+
await tracker.flush();
|
|
141
|
+
|
|
142
|
+
// Only b and c should have been updated
|
|
143
|
+
const updatedIds = store.updates.map(u => u.id);
|
|
144
|
+
expect(updatedIds).not.toContain("a");
|
|
145
|
+
expect(updatedIds).toContain("b");
|
|
146
|
+
expect(updatedIds).toContain("c");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("reinforces all entries when no scores provided (backward compat)", async () => {
|
|
150
|
+
const entries = [makeEntry("a"), makeEntry("b")];
|
|
151
|
+
const store = createMockStore(entries);
|
|
152
|
+
const tracker = new AccessTracker(store as any, {
|
|
153
|
+
...DEFAULT_ACCESS_TRACKER_CONFIG,
|
|
154
|
+
flushIntervalMs: 0,
|
|
155
|
+
noveltyThreshold: 0.35,
|
|
156
|
+
cooldownMs: 0,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// No scores → all reinforced
|
|
160
|
+
tracker.recordAccess(["a", "b"]);
|
|
161
|
+
await tracker.flush();
|
|
162
|
+
|
|
163
|
+
const updatedIds = store.updates.map(u => u.id);
|
|
164
|
+
expect(updatedIds).toContain("a");
|
|
165
|
+
expect(updatedIds).toContain("b");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("respects cooldown across multiple recordAccess calls", async () => {
|
|
169
|
+
const entries = [makeEntry("x")];
|
|
170
|
+
const store = createMockStore(entries);
|
|
171
|
+
const tracker = new AccessTracker(store as any, {
|
|
172
|
+
...DEFAULT_ACCESS_TRACKER_CONFIG,
|
|
173
|
+
flushIntervalMs: 0,
|
|
174
|
+
noveltyThreshold: 0.35,
|
|
175
|
+
cooldownMs: 600_000, // 10 min cooldown
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// First call: novel enough → queued
|
|
179
|
+
tracker.recordAccess(["x"], [0.50]);
|
|
180
|
+
await tracker.flush();
|
|
181
|
+
expect(store.updates.length).toBe(1);
|
|
182
|
+
|
|
183
|
+
// Second call: same entry within cooldown → not queued
|
|
184
|
+
tracker.recordAccess(["x"], [0.50]);
|
|
185
|
+
await tracker.flush();
|
|
186
|
+
// Still only 1 update (no new flush)
|
|
187
|
+
expect(store.updates.length).toBe(1);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("skips flush scheduling when all entries are gated out", () => {
|
|
191
|
+
const store = createMockStore([]);
|
|
192
|
+
const tracker = new AccessTracker(store as any, {
|
|
193
|
+
...DEFAULT_ACCESS_TRACKER_CONFIG,
|
|
194
|
+
flushIntervalMs: 5000,
|
|
195
|
+
noveltyThreshold: 0.35,
|
|
196
|
+
cooldownMs: 0,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// All scores too high (low novelty) → nothing queued
|
|
200
|
+
tracker.recordAccess(["a", "b"], [0.95, 0.90]);
|
|
201
|
+
expect(tracker.pendingCount).toBe(0);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("accessCount increments correctly after novelty-gated flush", async () => {
|
|
205
|
+
const entries = [makeEntry("m1")];
|
|
206
|
+
const store = createMockStore(entries);
|
|
207
|
+
const tracker = new AccessTracker(store as any, {
|
|
208
|
+
...DEFAULT_ACCESS_TRACKER_CONFIG,
|
|
209
|
+
flushIntervalMs: 0,
|
|
210
|
+
noveltyThreshold: 0.35,
|
|
211
|
+
cooldownMs: 0,
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
tracker.recordAccess(["m1"], [0.40]); // novel → accepted
|
|
215
|
+
await tracker.flush();
|
|
216
|
+
|
|
217
|
+
const updated = store.data.get("m1")!;
|
|
218
|
+
const meta = JSON.parse(updated.metadata);
|
|
219
|
+
expect(meta.accessCount).toBe(1);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// destroy() cleans cooldown
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
describe("destroy cleans cooldown", () => {
|
|
228
|
+
it("clears cooldown map on destroy", () => {
|
|
229
|
+
const tracker = new AccessTracker(
|
|
230
|
+
createMockStore([]) as any,
|
|
231
|
+
{ ...DEFAULT_ACCESS_TRACKER_CONFIG, cooldownMs: 600_000 },
|
|
232
|
+
);
|
|
233
|
+
tracker.recordAccess(["a"], [0.40]);
|
|
234
|
+
tracker.destroy();
|
|
235
|
+
// After destroy, same entry should be reinforceable again
|
|
236
|
+
expect(tracker.shouldReinforce("a", 0.40)).toBe(true);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
// Distinct-reader bookkeeping (Artel read fan-out) + default-config semantics
|
|
242
|
+
// ---------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
describe("default config after 2026-07-23 production fix", () => {
|
|
245
|
+
it("novelty gate is OFF by default (high-similarity hits are reinforced)", () => {
|
|
246
|
+
const tracker = new AccessTracker(createMockStore([]) as any);
|
|
247
|
+
// 生产实测修复:真实相关命中 cosine >0.65 也必须打点
|
|
248
|
+
expect(tracker.shouldReinforce("a", 0.95)).toBe(true);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
describe("distinct-reader bookkeeping", () => {
|
|
253
|
+
it("writes readerIds and distinctReaderCount on flush", async () => {
|
|
254
|
+
const entries = [makeEntry("a")];
|
|
255
|
+
const store = createMockStore(entries);
|
|
256
|
+
const tracker = new AccessTracker(store as any, {
|
|
257
|
+
...DEFAULT_ACCESS_TRACKER_CONFIG,
|
|
258
|
+
flushIntervalMs: 1,
|
|
259
|
+
cooldownMs: 0,
|
|
260
|
+
});
|
|
261
|
+
tracker.recordAccess(["a"]);
|
|
262
|
+
await tracker.flush();
|
|
263
|
+
|
|
264
|
+
const meta = JSON.parse(store.data.get("a")!.metadata!) as Record<string, unknown>;
|
|
265
|
+
expect(meta.accessCount).toBe(1);
|
|
266
|
+
expect(meta.readerIds).toEqual([tracker.readerId]);
|
|
267
|
+
expect(meta.distinctReaderCount).toBe(1);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it("dedups same reader and unions different readers", async () => {
|
|
271
|
+
const entries = [makeEntry("a")];
|
|
272
|
+
const store = createMockStore(entries);
|
|
273
|
+
const t1 = new AccessTracker(store as any, { ...DEFAULT_ACCESS_TRACKER_CONFIG, flushIntervalMs: 1, cooldownMs: 0 });
|
|
274
|
+
t1.recordAccess(["a"]);
|
|
275
|
+
await t1.flush();
|
|
276
|
+
t1.recordAccess(["a"]);
|
|
277
|
+
await t1.flush(); // same reader twice → still 1 distinct
|
|
278
|
+
|
|
279
|
+
const t2 = new AccessTracker(store as any, { ...DEFAULT_ACCESS_TRACKER_CONFIG, flushIntervalMs: 1, cooldownMs: 0 });
|
|
280
|
+
t2.recordAccess(["a"]);
|
|
281
|
+
await t2.flush(); // second reader → 2 distinct
|
|
282
|
+
|
|
283
|
+
const meta = JSON.parse(store.data.get("a")!.metadata!) as Record<string, unknown>;
|
|
284
|
+
expect(meta.accessCount).toBe(3);
|
|
285
|
+
expect((meta.readerIds as string[]).sort()).toEqual([t1.readerId, t2.readerId].sort());
|
|
286
|
+
expect(meta.distinctReaderCount).toBe(2);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it("saturates readerIds at READER_ID_CAP without growing the array", async () => {
|
|
290
|
+
const capped = Array.from({ length: 8 }, (_, i) => `r-cap${i}`);
|
|
291
|
+
const entries = [makeEntry("a", {
|
|
292
|
+
metadata: JSON.stringify({ accessCount: 8, readerIds: capped, distinctReaderCount: 8 }),
|
|
293
|
+
})];
|
|
294
|
+
const store = createMockStore(entries);
|
|
295
|
+
const tracker = new AccessTracker(store as any, { ...DEFAULT_ACCESS_TRACKER_CONFIG, flushIntervalMs: 1, cooldownMs: 0 });
|
|
296
|
+
tracker.recordAccess(["a"]);
|
|
297
|
+
await tracker.flush();
|
|
298
|
+
|
|
299
|
+
const meta = JSON.parse(store.data.get("a")!.metadata!) as Record<string, unknown>;
|
|
300
|
+
expect((meta.readerIds as string[]).length).toBe(8); // no growth past cap
|
|
301
|
+
expect(meta.distinctReaderCount).toBe(8); // saturates, no overcount
|
|
302
|
+
expect(meta.accessCount).toBe(9); // access still counted
|
|
303
|
+
});
|
|
304
|
+
});
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { mkdirSync, rmSync, existsSync, writeFileSync } from "node:fs";
|
|
4
|
+
import {
|
|
5
|
+
incrementWriteCount,
|
|
6
|
+
getWriteCount,
|
|
7
|
+
resetWriteCount,
|
|
8
|
+
listScopesAboveThreshold,
|
|
9
|
+
getDistillTier,
|
|
10
|
+
type ActivityCounterConfig,
|
|
11
|
+
} from "../activity-counter.js";
|
|
12
|
+
|
|
13
|
+
const TMP_DIR = join(import.meta.dir, "../../.tmp-activity-test");
|
|
14
|
+
const testConfig: Partial<ActivityCounterConfig> = {
|
|
15
|
+
statsPath: join(TMP_DIR, "activity-stats.json"),
|
|
16
|
+
lightThreshold: 3,
|
|
17
|
+
standardThreshold: 10,
|
|
18
|
+
deepThreshold: 20,
|
|
19
|
+
};
|
|
20
|
+
const A = "cc:project:a";
|
|
21
|
+
const B = "cc:project:b";
|
|
22
|
+
|
|
23
|
+
describe("activity-counter (HP-3, per-scope)", () => {
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
mkdirSync(TMP_DIR, { recursive: true });
|
|
26
|
+
const p = testConfig.statsPath!;
|
|
27
|
+
if (existsSync(p)) rmSync(p);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
rmSync(TMP_DIR, { recursive: true, force: true });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("incrementWriteCount", () => {
|
|
35
|
+
it("starts at 0 and increments by 1 for a scope", () => {
|
|
36
|
+
expect(getWriteCount(A, testConfig)).toBe(0);
|
|
37
|
+
expect(incrementWriteCount(A, 1, testConfig)).toBe(1);
|
|
38
|
+
expect(incrementWriteCount(A, 1, testConfig)).toBe(2);
|
|
39
|
+
expect(getWriteCount(A, testConfig)).toBe(2);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("increments by arbitrary n", () => {
|
|
43
|
+
incrementWriteCount(A, 5, testConfig);
|
|
44
|
+
expect(getWriteCount(A, testConfig)).toBe(5);
|
|
45
|
+
incrementWriteCount(A, 3, testConfig);
|
|
46
|
+
expect(getWriteCount(A, testConfig)).toBe(8);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("counts each scope independently", () => {
|
|
50
|
+
incrementWriteCount(A, 4, testConfig);
|
|
51
|
+
incrementWriteCount(B, 1, testConfig);
|
|
52
|
+
expect(getWriteCount(A, testConfig)).toBe(4);
|
|
53
|
+
expect(getWriteCount(B, testConfig)).toBe(1);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe("resetWriteCount", () => {
|
|
58
|
+
it("resets only the given scope, leaving others intact", () => {
|
|
59
|
+
incrementWriteCount(A, 7, testConfig);
|
|
60
|
+
incrementWriteCount(B, 5, testConfig);
|
|
61
|
+
resetWriteCount(A, testConfig);
|
|
62
|
+
expect(getWriteCount(A, testConfig)).toBe(0);
|
|
63
|
+
expect(getWriteCount(B, testConfig)).toBe(5); // not starved by A's reset
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("listScopesAboveThreshold", () => {
|
|
68
|
+
it("returns scopes at or above the threshold only", () => {
|
|
69
|
+
incrementWriteCount(A, 12, testConfig);
|
|
70
|
+
incrementWriteCount(B, 4, testConfig);
|
|
71
|
+
incrementWriteCount("cc:project:c", 10, testConfig);
|
|
72
|
+
const above = listScopesAboveThreshold(10, testConfig).sort();
|
|
73
|
+
expect(above).toEqual(["cc:project:a", "cc:project:c"]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("returns empty when no scope qualifies", () => {
|
|
77
|
+
incrementWriteCount(A, 2, testConfig);
|
|
78
|
+
expect(listScopesAboveThreshold(10, testConfig)).toEqual([]);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe("getDistillTier", () => {
|
|
83
|
+
it("returns 'none' when below light threshold", () => {
|
|
84
|
+
incrementWriteCount(A, 2, testConfig);
|
|
85
|
+
expect(getDistillTier(A, testConfig)).toBe("none");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("returns 'light' at light threshold", () => {
|
|
89
|
+
incrementWriteCount(A, 3, testConfig);
|
|
90
|
+
expect(getDistillTier(A, testConfig)).toBe("light");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("returns 'standard' at standard threshold", () => {
|
|
94
|
+
incrementWriteCount(A, 10, testConfig);
|
|
95
|
+
expect(getDistillTier(A, testConfig)).toBe("standard");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("returns 'deep' at and above deep threshold", () => {
|
|
99
|
+
incrementWriteCount(A, 20, testConfig);
|
|
100
|
+
expect(getDistillTier(A, testConfig)).toBe("deep");
|
|
101
|
+
incrementWriteCount(A, 100, testConfig);
|
|
102
|
+
expect(getDistillTier(A, testConfig)).toBe("deep");
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("resilience", () => {
|
|
107
|
+
it("handles missing stats file gracefully", () => {
|
|
108
|
+
expect(getWriteCount(A, testConfig)).toBe(0);
|
|
109
|
+
expect(getDistillTier(A, testConfig)).toBe("none");
|
|
110
|
+
expect(listScopesAboveThreshold(1, testConfig)).toEqual([]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("handles corrupt stats file gracefully", () => {
|
|
114
|
+
writeFileSync(testConfig.statsPath!, "not-json{{{");
|
|
115
|
+
expect(getWriteCount(A, testConfig)).toBe(0);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("treats the legacy global format as empty (no migration)", () => {
|
|
119
|
+
writeFileSync(testConfig.statsPath!, JSON.stringify({ writesSinceLastDistill: 42, lastResetAt: 1 }));
|
|
120
|
+
expect(getWriteCount(A, testConfig)).toBe(0);
|
|
121
|
+
// first increment starts a fresh per-scope map
|
|
122
|
+
expect(incrementWriteCount(A, 1, testConfig)).toBe(1);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
});
|