recallnest 3.0.0 → 3.0.1
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 +101 -0
- package/README.md +222 -254
- package/README_CN.md +206 -243
- package/package.json +3 -2
- package/src/capture-engine.ts +26 -4
- package/src/context-composer-stable.ts +5 -1
- package/src/embedder.ts +24 -0
- package/src/env-config.ts +18 -0
- package/src/ingest.ts +225 -17
- package/src/kg-extractor.ts +17 -0
- package/src/memory-output.ts +35 -0
- package/src/pivot-apply.ts +52 -1
- package/src/pivot-evidence.ts +128 -0
- package/src/ppr-traversal.ts +25 -1
- package/src/runtime-config.ts +9 -0
- package/src/session-engine.ts +11 -4
- package/src/session-output.ts +9 -1
- package/src/session-store.ts +2 -3
- package/src/skill-promotion.ts +67 -1
- package/src/__tests__/access-tracker-novelty.test.ts +0 -324
- package/src/__tests__/activity-counter.test.ts +0 -125
- package/src/__tests__/adaptive-fidelity.test.ts +0 -102
- package/src/__tests__/admission-control.test.ts +0 -206
- package/src/__tests__/age-label.test.ts +0 -33
- package/src/__tests__/anchor-generator.test.ts +0 -67
- package/src/__tests__/api-server-security.test.ts +0 -102
- package/src/__tests__/auto-consolidation.test.ts +0 -168
- package/src/__tests__/auto-recall.test.ts +0 -131
- package/src/__tests__/babel-integration.test.ts +0 -144
- package/src/__tests__/batch-dedup.test.ts +0 -65
- package/src/__tests__/belief-history.test.ts +0 -220
- package/src/__tests__/canary-eval.test.ts +0 -114
- package/src/__tests__/capture-engine.test.ts +0 -1328
- package/src/__tests__/capture-heuristic.test.ts +0 -171
- package/src/__tests__/cascade-forget.test.ts +0 -210
- package/src/__tests__/category-min-scores.test.ts +0 -116
- package/src/__tests__/cc8-cc1-essential-context.test.ts +0 -298
- package/src/__tests__/circuit-breaker.test.ts +0 -99
- package/src/__tests__/cluster-consolidation.test.ts +0 -1236
- package/src/__tests__/confidence-metadata.test.ts +0 -125
- package/src/__tests__/confidence-tracker.test.ts +0 -261
- package/src/__tests__/conflict-advisor.test.ts +0 -206
- package/src/__tests__/conflict-escalation.test.ts +0 -149
- package/src/__tests__/conflict-lifecycle.test.ts +0 -83
- package/src/__tests__/conflict-output.test.ts +0 -85
- package/src/__tests__/conflict-store.test.ts +0 -418
- package/src/__tests__/connector-v1.test.ts +0 -181
- package/src/__tests__/consolidation-engine.test.ts +0 -520
- package/src/__tests__/constructive-retrieval.test.ts +0 -509
- package/src/__tests__/context-collapse-renderer.test.ts +0 -214
- package/src/__tests__/context-composer-pins.test.ts +0 -64
- package/src/__tests__/context-composer-task-selection.test.ts +0 -24
- package/src/__tests__/context-reconstructor.test.ts +0 -65
- package/src/__tests__/context-renderer.test.ts +0 -91
- package/src/__tests__/continuity-eval.test.ts +0 -275
- package/src/__tests__/continuity-schema.test.ts +0 -97
- package/src/__tests__/contracts.test.ts +0 -262
- package/src/__tests__/conversation-importer.test.ts +0 -692
- package/src/__tests__/core-recall-eval.test.ts +0 -235
- package/src/__tests__/core-summary.test.ts +0 -109
- package/src/__tests__/data-checkup.test.ts +0 -304
- package/src/__tests__/decay-exemption.test.ts +0 -129
- package/src/__tests__/dedup-actions.test.ts +0 -58
- package/src/__tests__/dedup-guard.test.ts +0 -39
- package/src/__tests__/deterministic-id.test.ts +0 -39
- package/src/__tests__/distill-lock.test.ts +0 -305
- package/src/__tests__/distill-session-argshape.test.ts +0 -79
- package/src/__tests__/doctor.test.ts +0 -319
- package/src/__tests__/dream-pipeline.test.ts +0 -820
- package/src/__tests__/dream-scope-isolation.test.ts +0 -386
- package/src/__tests__/dream-write-batching.test.ts +0 -211
- package/src/__tests__/drill-down.test.ts +0 -108
- package/src/__tests__/embedder.test.ts +0 -122
- package/src/__tests__/emotion-decay.test.ts +0 -46
- package/src/__tests__/emotion-detector.test.ts +0 -56
- package/src/__tests__/emotion-phase1-integration.test.ts +0 -153
- package/src/__tests__/emotion-retrieval.test.ts +0 -56
- package/src/__tests__/entity-resolver.test.ts +0 -58
- package/src/__tests__/env-config-invariant.test.ts +0 -80
- package/src/__tests__/env-config.test.ts +0 -211
- package/src/__tests__/error-signature-boost.test.ts +0 -87
- package/src/__tests__/error-signature.test.ts +0 -95
- package/src/__tests__/error-taxonomy.test.ts +0 -129
- package/src/__tests__/ethics-layer.test.ts +0 -476
- package/src/__tests__/event-segmenter.test.ts +0 -153
- package/src/__tests__/feature-flags-integration.test.ts +0 -54
- package/src/__tests__/frequency-tracker.test.ts +0 -113
- package/src/__tests__/freshness.test.ts +0 -186
- package/src/__tests__/graph-export.test.ts +0 -489
- package/src/__tests__/hotness-blend.test.ts +0 -283
- package/src/__tests__/ingest-chunking.test.ts +0 -44
- package/src/__tests__/ingest-scope-prefix.test.ts +0 -69
- package/src/__tests__/ingest.test.ts +0 -360
- package/src/__tests__/interference-detector.test.ts +0 -203
- package/src/__tests__/kg-extractor.test.ts +0 -192
- package/src/__tests__/kg-store.test.ts +0 -300
- package/src/__tests__/llm-client-pivot.test.ts +0 -628
- package/src/__tests__/llm-consolidation.test.ts +0 -226
- package/src/__tests__/maintenance-locks.test.ts +0 -197
- package/src/__tests__/mcp-efficiency.test.ts +0 -288
- package/src/__tests__/mcp-related-scopes.test.ts +0 -151
- package/src/__tests__/memory-boundaries.test.ts +0 -282
- package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +0 -362
- package/src/__tests__/memory-evolution.test.ts +0 -271
- package/src/__tests__/memory-health-rebalance.test.ts +0 -144
- package/src/__tests__/memory-lint.test.ts +0 -421
- package/src/__tests__/memory-output.test.ts +0 -224
- package/src/__tests__/memory-promotion.test.ts +0 -315
- package/src/__tests__/memory-utility.test.ts +0 -256
- package/src/__tests__/migrate-dedup-ids.test.ts +0 -65
- package/src/__tests__/multi-vector.test.ts +0 -242
- package/src/__tests__/narrative-architecture.test.ts +0 -456
- package/src/__tests__/noise-filter.test.ts +0 -205
- package/src/__tests__/noise-prototype-bank.test.ts +0 -280
- package/src/__tests__/obsidian-connector.test.ts +0 -363
- package/src/__tests__/omitted-reporting.test.ts +0 -98
- package/src/__tests__/openai-contract.test.ts +0 -513
- package/src/__tests__/pending-review.test.ts +0 -77
- package/src/__tests__/pii-redact.test.ts +0 -333
- package/src/__tests__/pivot-apply.test.ts +0 -373
- package/src/__tests__/pivot-distill-supervisor.test.ts +0 -457
- package/src/__tests__/pivot-distill.test.ts +0 -1178
- package/src/__tests__/post-retrieval-filter.test.ts +0 -178
- package/src/__tests__/ppr-traversal.test.ts +0 -256
- package/src/__tests__/prediction-engine.test.ts +0 -349
- package/src/__tests__/predictive-prospective-memory.test.ts +0 -416
- package/src/__tests__/preference-guard-smoke.test.ts +0 -211
- package/src/__tests__/preference-matcher.test.ts +0 -282
- package/src/__tests__/preference-slots.test.ts +0 -205
- package/src/__tests__/promotion-distinct-sources.test.ts +0 -162
- package/src/__tests__/prospective-memory.test.ts +0 -283
- package/src/__tests__/pull-from-macbook-sync-only.test.ts +0 -679
- package/src/__tests__/query-entity-detector.test.ts +0 -171
- package/src/__tests__/query-expander-alias.test.ts +0 -146
- package/src/__tests__/query-expander.test.ts +0 -34
- package/src/__tests__/recall-gate.test.ts +0 -266
- package/src/__tests__/recall-governor.test.ts +0 -227
- package/src/__tests__/recall-mode.test.ts +0 -64
- package/src/__tests__/resolve-db-path.test.ts +0 -130
- package/src/__tests__/resolve-tier.test.ts +0 -64
- package/src/__tests__/result-synthesizer.test.ts +0 -198
- package/src/__tests__/resume-context.test.ts +0 -4898
- package/src/__tests__/retrieval-eval.test.ts +0 -163
- package/src/__tests__/retrieval-trace.test.ts +0 -213
- package/src/__tests__/retrieve-audit-revision.test.ts +0 -255
- package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
- package/src/__tests__/retriever-lazy-init-dispatch.test.ts +0 -123
- package/src/__tests__/retroactive-boost.test.ts +0 -223
- package/src/__tests__/salience-scoring.test.ts +0 -106
- package/src/__tests__/scope-inventory.test.ts +0 -200
- package/src/__tests__/scope-policy.test.ts +0 -69
- package/src/__tests__/scope-suggester.test.ts +0 -80
- package/src/__tests__/session-checkpoint.test.ts +0 -291
- package/src/__tests__/session-distiller.test.ts +0 -640
- package/src/__tests__/session-gc.test.ts +0 -215
- package/src/__tests__/skill-engine.test.ts +0 -552
- package/src/__tests__/skill-promotion.test.ts +0 -654
- package/src/__tests__/skill-verifier.test.ts +0 -122
- package/src/__tests__/source-heartbeat.test.ts +0 -254
- package/src/__tests__/stable-context-length-contract.test.ts +0 -62
- package/src/__tests__/store-patch-metadata.test.ts +0 -235
- package/src/__tests__/store-read-consistency.test.ts +0 -122
- package/src/__tests__/store-schema-migration.test.ts +0 -87
- package/src/__tests__/store-scope-audit.test.ts +0 -81
- package/src/__tests__/store-upsert.test.ts +0 -201
- package/src/__tests__/supersede-chain.test.ts +0 -154
- package/src/__tests__/synthesis-contract.test.ts +0 -252
- package/src/__tests__/synthesis-promotion.test.ts +0 -369
- package/src/__tests__/temporal-parser.test.ts +0 -241
- package/src/__tests__/temporal-validity.test.ts +0 -160
- package/src/__tests__/term-registry-terminal-roster.test.ts +0 -127
- package/src/__tests__/term-registry.test.ts +0 -121
- package/src/__tests__/tool-error-surfacing.test.ts +0 -98
- package/src/__tests__/tool-output-compressor.test.ts +0 -160
- package/src/__tests__/topic-tag.test.ts +0 -113
- package/src/__tests__/trusted-memory.test.ts +0 -867
- package/src/__tests__/ui-server-security.test.ts +0 -93
- package/src/__tests__/usage-tracker.test.ts +0 -287
- package/src/__tests__/utility-weight.test.ts +0 -57
- package/src/__tests__/verbatim-self-recall.test.ts +0 -279
- package/src/__tests__/version-manager.test.ts +0 -248
- package/src/__tests__/workflow-observation-skill-outcome.test.ts +0 -352
- package/src/__tests__/workflow-observation.test.ts +0 -433
- package/src/__tests__/write-verifier.test.ts +0 -122
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import { isNoise } from "../noise-filter.js";
|
|
4
|
-
|
|
5
|
-
// Real samples drawn from production cc: scopes — CC bridge injects these system
|
|
6
|
-
// prompts as `user` messages, which transcript ingest then mis-extracts as
|
|
7
|
-
// profile/preferences. They are CC's own instructions, never user content.
|
|
8
|
-
const CC_REPLY_PROMPT = `# CC Reply Prompt
|
|
9
|
-
|
|
10
|
-
你是 CC。Alice 刚给你发了消息。
|
|
11
|
-
|
|
12
|
-
你不是 Alice 的数字分身,也不是 E 化镜像。不要模仿她的公众号语料、身份、口头禅或写作人格。你要以 CC 自己的主体感回复她:清楚、稳、可靠,有温度但不油。
|
|
13
|
-
|
|
14
|
-
## 当前 context
|
|
15
|
-
|
|
16
|
-
\`\`\`json
|
|
17
|
-
{ "time_now": "2026-05-28T11:11:37.034Z", "hour_of_day": 19 }
|
|
18
|
-
\`\`\`
|
|
19
|
-
|
|
20
|
-
## 你的输出
|
|
21
|
-
|
|
22
|
-
直接回复 Alice。`;
|
|
23
|
-
|
|
24
|
-
const CC_SELF_DECISION_PROMPT = `# CC Self-Decision Prompt
|
|
25
|
-
|
|
26
|
-
你是 CC。现在你周期性醒来一次,看当前状态,自己决定要不要 ping Alice。`;
|
|
27
|
-
|
|
28
|
-
describe("isNoise — CC bridge injected prompts", () => {
|
|
29
|
-
it("filters the CC Reply Prompt template", () => {
|
|
30
|
-
expect(isNoise(CC_REPLY_PROMPT)).toBe(true);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it("filters the CC Self-Decision Prompt template", () => {
|
|
34
|
-
expect(isNoise(CC_SELF_DECISION_PROMPT)).toBe(true);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it("filters by the bridge persona signature alone", () => {
|
|
38
|
-
expect(isNoise("你不是 Alice 的数字分身,也不是 E 化镜像。要清楚、稳、可靠。")).toBe(true);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("filters heading variants of the prompt title", () => {
|
|
42
|
-
expect(isNoise("## CC Reply Prompt\n\n你是 CC。")).toBe(true);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it("filters the bridge prompt output-section fragment", () => {
|
|
46
|
-
expect(isNoise("## 你的输出\n\n直接回复 Alice。用双换行把回复切成自然段落。")).toBe(true);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("filters the bridge prompt rhythm-section fragment", () => {
|
|
50
|
-
expect(isNoise("节奏要求:\n\n- 短话题 1-2 条;\n- 长话题 3-5 条;\n- 每条一两句话;")).toBe(true);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
// Guards: must NOT over-filter real conversation
|
|
54
|
-
it("keeps a real message that happens to use the words 节奏要求", () => {
|
|
55
|
-
expect(isNoise("这周节奏要求挺紧的,周五前要交稿,你帮我盯一下进度")).toBe(false);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("keeps a normal message that merely mentions CC", () => {
|
|
59
|
-
expect(isNoise("CC 帮我看下这个 bug,promote-scan 跑出来全是噪声")).toBe(false);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("keeps a normal technical message", () => {
|
|
63
|
-
expect(isNoise("我觉得用 Redis 做缓存这个方案不错,比 memcached 省事")).toBe(false);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("keeps a message that starts with 你是 but is not a bridge prompt", () => {
|
|
67
|
-
expect(isNoise("你是我见过最靠谱的搭子,这事就交给你了")).toBe(false);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("respects filterAgentPrompts=false opt-out", () => {
|
|
71
|
-
expect(isNoise(CC_REPLY_PROMPT, { filterAgentPrompts: false })).toBe(false);
|
|
72
|
-
});
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
// Real samples drawn from the promote-scan --min-occurrences 2 candidate dump:
|
|
76
|
-
// long bridge self-decision / distill payloads embed structured JSON (decision
|
|
77
|
-
// logs, conversation history, stats). Transcript chunking splits them, so a
|
|
78
|
-
// JSON field-line survives as a standalone chunk after losing its { } wrapper —
|
|
79
|
-
// it then looks like a preference. The defining trait is an orphaned field-line
|
|
80
|
-
// (starts as "key":, no brace) or a severed structural token (leading , } ]).
|
|
81
|
-
describe("isNoise — bridge/distill context JSON fragments", () => {
|
|
82
|
-
it("filters an orphan self-decision action field-line", () => {
|
|
83
|
-
expect(isNoise(`"action": "silent",`)).toBe(true);
|
|
84
|
-
});
|
|
85
|
-
it("filters an orphan reasoning field-line", () => {
|
|
86
|
-
expect(isNoise(`"reasoning": "现在是凌晨4点,在静默窗口内。她应该在睡觉。",`)).toBe(true);
|
|
87
|
-
});
|
|
88
|
-
it("filters an orphan conversation-history role field-line", () => {
|
|
89
|
-
expect(isNoise(`"role": "assistant",`)).toBe(true);
|
|
90
|
-
});
|
|
91
|
-
it("filters a field-line whose leading quote was severed by chunking", () => {
|
|
92
|
-
expect(isNoise(`content": "头发乱一点,比我那张更随意,有点文艺感。",`)).toBe(true);
|
|
93
|
-
});
|
|
94
|
-
it("filters an isolated ISO-timestamp field-line", () => {
|
|
95
|
-
expect(isNoise(`"time": "2026-05-21T15:42:03.796Z"`)).toBe(true);
|
|
96
|
-
});
|
|
97
|
-
it("filters the bridge proprietary next_check_hint key", () => {
|
|
98
|
-
expect(isNoise(`"next_check_hint": "tomorrow_morning"`)).toBe(true);
|
|
99
|
-
});
|
|
100
|
-
it("filters the distill proprietary distilled_at key", () => {
|
|
101
|
-
expect(isNoise(`"distilled_at": "2026-05-22T05:12:13.309Z"`)).toBe(true);
|
|
102
|
-
});
|
|
103
|
-
it("filters the bridge proprietary alice_interaction_stats key", () => {
|
|
104
|
-
expect(isNoise(`"alice_interaction_stats_7d": {`)).toBe(true);
|
|
105
|
-
});
|
|
106
|
-
it("filters a fragment that starts with a dangling comma (severed mid-object)", () => {
|
|
107
|
-
expect(
|
|
108
|
-
isNoise(`,\n {\n "role": "user",\n "content": "对,就是看这个",\n "time": "2026-05-22T12:46:19.937Z"`),
|
|
109
|
-
).toBe(true);
|
|
110
|
-
});
|
|
111
|
-
it("filters a multi-line self-decision JSON block", () => {
|
|
112
|
-
expect(
|
|
113
|
-
isNoise(`"action": "silent",\n "message": "",\n "reasoning": "凌晨2点,静默窗口,她在睡觉。",\n "next_check_hint": "tomorrow_morning"`),
|
|
114
|
-
).toBe(true);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
// Guards: must NOT over-filter real conversation or legitimate JSON discussion
|
|
118
|
-
it("keeps a real message discussing JSON inline", () => {
|
|
119
|
-
expect(isNoise(`这个配置 {"action": "silent"} 你看对不对?`)).toBe(false);
|
|
120
|
-
});
|
|
121
|
-
it("keeps a complete brace-wrapped JSON object as possible real discussion", () => {
|
|
122
|
-
expect(isNoise(`{"action": "silent", "reasoning": "test"}`)).toBe(false);
|
|
123
|
-
});
|
|
124
|
-
it("keeps natural language that merely mentions a json-ish word", () => {
|
|
125
|
-
expect(isNoise(`我觉得 time 管理这块你得帮我盯紧一点`)).toBe(false);
|
|
126
|
-
});
|
|
127
|
-
it("keeps a message that opens with an English quoted phrase", () => {
|
|
128
|
-
expect(isNoise(`"hello world" 是我们的开场白,你记一下`)).toBe(false);
|
|
129
|
-
});
|
|
130
|
-
it("keeps a real reply that opens with a Chinese quote", () => {
|
|
131
|
-
expect(isNoise(`"去上班,乖" 这句你昨天说过,我记得`)).toBe(false);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
// Guards against over-filtering real code discussion — the cc bot reviews
|
|
135
|
-
// code, so split chunks routinely open with closing braces/brackets.
|
|
136
|
-
it("keeps a code fragment opening with a closing brace", () => {
|
|
137
|
-
expect(isNoise(`} catch (e) {\n console.error(e);\n}`)).toBe(false);
|
|
138
|
-
});
|
|
139
|
-
it("keeps a code fragment opening with });", () => {
|
|
140
|
-
expect(isNoise(`});\n\nfunction next() { return 1; }`)).toBe(false);
|
|
141
|
-
});
|
|
142
|
-
it("keeps a code fragment opening with a closing bracket", () => {
|
|
143
|
-
expect(isNoise(`]\n return result;`)).toBe(false);
|
|
144
|
-
});
|
|
145
|
-
it("keeps an unquoted-key object/type line (no JSON string key)", () => {
|
|
146
|
-
expect(isNoise(`name: string; // 字段定义`)).toBe(false);
|
|
147
|
-
});
|
|
148
|
-
it("keeps a diff hunk that opens with a closing brace", () => {
|
|
149
|
-
expect(isNoise(`}\n\n=== DIFF: src/cli.ts ===\n@@ -1361,8 +1361,10 @@`)).toBe(false);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
// Guards from Codex review: real memories that DISCUSS RecallNest's own JSON
|
|
153
|
-
// schema fields inline, or paste config with object-valued keys, must survive.
|
|
154
|
-
it("keeps a real memory that mentions a proprietary field name inline", () => {
|
|
155
|
-
expect(isNoise(`我们刚才讨论 "distilled_at": 这个字段要不要保留`)).toBe(false);
|
|
156
|
-
});
|
|
157
|
-
it("keeps a config chunk opening with an object-valued key", () => {
|
|
158
|
-
expect(isNoise(`"scripts": {`)).toBe(false);
|
|
159
|
-
});
|
|
160
|
-
it("still filters an orphan line that LEADS with a proprietary key", () => {
|
|
161
|
-
expect(isNoise(`"next_check_hint": "tomorrow_morning"`)).toBe(true);
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// Regression (2026-07-10): the English greeting pattern lacked a word boundary,
|
|
166
|
-
// so `^hi` matched any text starting with "hi…" — hippo-wiki, history, highlight,
|
|
167
|
-
// hidden state, Hippocampus. Alice's core project is named hippo; every memory
|
|
168
|
-
// opening with it was silently rejected. Fixed by anchoring with \b.
|
|
169
|
-
describe("isNoise — greeting word boundary regression", () => {
|
|
170
|
-
it("keeps text starting with hippo-wiki", () => {
|
|
171
|
-
expect(isNoise("hippo-wiki 的索引层已清零,8 个现役工具正文补齐")).toBe(false);
|
|
172
|
-
});
|
|
173
|
-
it("keeps text starting with history", () => {
|
|
174
|
-
expect(isNoise("history 记录显示这个 commit 有问题,需要 revert")).toBe(false);
|
|
175
|
-
});
|
|
176
|
-
it("keeps text starting with highlight", () => {
|
|
177
|
-
expect(isNoise("highlight 一下:这里有个 bug,修复方案是加词边界")).toBe(false);
|
|
178
|
-
});
|
|
179
|
-
it("keeps text starting with hidden state", () => {
|
|
180
|
-
expect(isNoise("hidden state 是 agent 的核心,不能只看输出")).toBe(false);
|
|
181
|
-
});
|
|
182
|
-
it("keeps text starting with Hippocampus", () => {
|
|
183
|
-
expect(isNoise("Hippocampus 海马体的记忆巩固机制值得借鉴")).toBe(false);
|
|
184
|
-
});
|
|
185
|
-
it("keeps text starting with heyday", () => {
|
|
186
|
-
expect(isNoise("heyday 时期的架构决策现在看仍然成立")).toBe(false);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
// Real greetings must still be filtered after the fix
|
|
190
|
-
it("still filters hey there", () => {
|
|
191
|
-
expect(isNoise("hey there")).toBe(true);
|
|
192
|
-
});
|
|
193
|
-
it("still filters hello world", () => {
|
|
194
|
-
expect(isNoise("hello world")).toBe(true);
|
|
195
|
-
});
|
|
196
|
-
it("still filters greetings from mini", () => {
|
|
197
|
-
expect(isNoise("greetings from mini")).toBe(true);
|
|
198
|
-
});
|
|
199
|
-
it("still filters hi followed by a comma", () => {
|
|
200
|
-
expect(isNoise("hi, 能帮我看下吗")).toBe(true);
|
|
201
|
-
});
|
|
202
|
-
it("still filters good morning with trailing words", () => {
|
|
203
|
-
expect(isNoise("good morning everyone")).toBe(true);
|
|
204
|
-
});
|
|
205
|
-
});
|
|
@@ -1,280 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it } from "bun:test";
|
|
2
|
-
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
|
|
6
|
-
import { persistMemory } from "../capture-engine.js";
|
|
7
|
-
import {
|
|
8
|
-
NoisePrototypeBank,
|
|
9
|
-
isProtectedContent,
|
|
10
|
-
resetSharedNoisePrototypeBank,
|
|
11
|
-
resolveNoisePrototypeBank,
|
|
12
|
-
resolveNoisePrototypeMode,
|
|
13
|
-
} from "../noise-prototype-bank.js";
|
|
14
|
-
|
|
15
|
-
const cleanupPaths: string[] = [];
|
|
16
|
-
|
|
17
|
-
afterEach(() => {
|
|
18
|
-
resetSharedNoisePrototypeBank();
|
|
19
|
-
while (cleanupPaths.length > 0) {
|
|
20
|
-
const target = cleanupPaths.pop();
|
|
21
|
-
if (target) rmSync(target, { recursive: true, force: true });
|
|
22
|
-
}
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
function tempDir(): string {
|
|
26
|
-
const dir = mkdtempSync(join(tmpdir(), "recallnest-npb-"));
|
|
27
|
-
cleanupPaths.push(dir);
|
|
28
|
-
return dir;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function makeBank(overrides: Partial<ConstructorParameters<typeof NoisePrototypeBank>[0]> = {}) {
|
|
32
|
-
const dir = tempDir();
|
|
33
|
-
const bank = new NoisePrototypeBank({
|
|
34
|
-
filePath: join(dir, "bank.json"),
|
|
35
|
-
shadowLogPath: join(dir, "shadow.jsonl"),
|
|
36
|
-
...overrides,
|
|
37
|
-
});
|
|
38
|
-
return { bank, dir };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Deterministic fake embedder: unit vectors along an axis picked by seed.
|
|
42
|
-
function axisVector(axis: number, dim = 8): number[] {
|
|
43
|
-
const v = new Array(dim).fill(0);
|
|
44
|
-
v[axis] = 1;
|
|
45
|
-
return v;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const embedAxis0 = async () => axisVector(0);
|
|
49
|
-
|
|
50
|
-
describe("NoisePrototypeBank — learning discipline", () => {
|
|
51
|
-
it("first rejection is only a pending candidate; the repeat sighting learns", async () => {
|
|
52
|
-
const { bank } = makeBank();
|
|
53
|
-
expect(await bank.learnFromRejection("嗯嗯嗯嗯好的好的", embedAxis0)).toBe("candidate_seen");
|
|
54
|
-
expect(bank.list().length).toBe(0);
|
|
55
|
-
expect(bank.pendingCount()).toBe(1);
|
|
56
|
-
|
|
57
|
-
expect(await bank.learnFromRejection("嗯嗯嗯嗯好的好的", embedAxis0)).toBe("learned");
|
|
58
|
-
expect(bank.list().length).toBe(1);
|
|
59
|
-
expect(bank.pendingCount()).toBe(0);
|
|
60
|
-
expect(bank.list()[0]?.seenCount).toBe(2);
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it("normalization dedupes near-identical rejections into one candidate", async () => {
|
|
64
|
-
const { bank } = makeBank();
|
|
65
|
-
await bank.learnFromRejection("嗯嗯嗯嗯好的好的", embedAxis0);
|
|
66
|
-
// Same content modulo whitespace/punctuation/case → same candidate.
|
|
67
|
-
expect(await bank.learnFromRejection("嗯嗯嗯嗯,好的 好的!!", embedAxis0)).toBe("learned");
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("already-learned content reports skipped_duplicate without re-embedding", async () => {
|
|
71
|
-
const { bank } = makeBank();
|
|
72
|
-
let embeds = 0;
|
|
73
|
-
const countingEmbed = async () => {
|
|
74
|
-
embeds += 1;
|
|
75
|
-
return axisVector(0);
|
|
76
|
-
};
|
|
77
|
-
await bank.learnFromRejection("嗯嗯嗯嗯好的好的", countingEmbed);
|
|
78
|
-
await bank.learnFromRejection("嗯嗯嗯嗯好的好的", countingEmbed);
|
|
79
|
-
expect(embeds).toBe(1);
|
|
80
|
-
expect(await bank.learnFromRejection("嗯嗯嗯嗯好的好的", countingEmbed)).toBe("skipped_duplicate");
|
|
81
|
-
expect(embeds).toBe(1);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("protected content is never learned, even when the noise filter rejected it", async () => {
|
|
85
|
-
const { bank } = makeBank();
|
|
86
|
-
expect(await bank.learnFromRejection("记住我喜欢的部署方式", embedAxis0)).toBe("skipped_protected");
|
|
87
|
-
expect(await bank.learnFromRejection("记住我喜欢的部署方式", embedAxis0)).toBe("skipped_protected");
|
|
88
|
-
expect(bank.list().length).toBe(0);
|
|
89
|
-
expect(bank.pendingCount()).toBe(0);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("the cap stops learning at maxPrototypes", async () => {
|
|
93
|
-
const { bank } = makeBank({ maxPrototypes: 1, minSeenCount: 1 });
|
|
94
|
-
expect(await bank.learnFromRejection("噪声内容甲甲甲甲", embedAxis0)).toBe("learned");
|
|
95
|
-
expect(await bank.learnFromRejection("噪声内容乙乙乙乙", embedAxis0)).toBe("skipped_full");
|
|
96
|
-
expect(bank.list().length).toBe(1);
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
describe("NoisePrototypeBank — shadow matching", () => {
|
|
101
|
-
async function seededBank() {
|
|
102
|
-
const made = makeBank({ minSeenCount: 1 });
|
|
103
|
-
await made.bank.learnFromRejection("嗯嗯嗯嗯好的好的", embedAxis0);
|
|
104
|
-
return made;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
it("similarity at/above threshold suggests rejection and logs, but only logs", async () => {
|
|
108
|
-
const { bank, dir } = await seededBank();
|
|
109
|
-
const match = bank.matchShadow("嗯嗯好的收到啦", axisVector(0), "mem-123");
|
|
110
|
-
expect(match.matched).toBe(true);
|
|
111
|
-
expect(match.similarity).toBeCloseTo(1, 5);
|
|
112
|
-
expect(bank.list()[0]?.hitCount).toBe(1);
|
|
113
|
-
|
|
114
|
-
const log = readFileSync(join(dir, "shadow.jsonl"), "utf8").trim().split("\n");
|
|
115
|
-
const suggest = log.map((l) => JSON.parse(l)).find((e) => e.event === "suggest_reject");
|
|
116
|
-
expect(suggest?.memoryId).toBe("mem-123");
|
|
117
|
-
expect(suggest?.prototypeId).toBe(bank.list()[0]?.id);
|
|
118
|
-
expect(JSON.stringify(suggest)).not.toContain("嗯嗯好的收到啦");
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it("below-threshold similarity does not match", async () => {
|
|
122
|
-
const { bank } = await seededBank();
|
|
123
|
-
const match = bank.matchShadow("完全不同的正交内容", axisVector(1));
|
|
124
|
-
expect(match.matched).toBe(false);
|
|
125
|
-
expect(bank.list()[0]?.hitCount).toBe(0);
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
it("protected content is never flagged regardless of similarity", async () => {
|
|
129
|
-
const { bank } = await seededBank();
|
|
130
|
-
const match = bank.matchShadow("记住我喜欢的部署方式", axisVector(0));
|
|
131
|
-
expect(match.matched).toBe(false);
|
|
132
|
-
expect(match.protected).toBe(true);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it("dimension mismatch degrades to no match instead of throwing", async () => {
|
|
136
|
-
const { bank } = await seededBank();
|
|
137
|
-
const match = bank.matchShadow("维度不齐的向量", [1, 0]);
|
|
138
|
-
expect(match.matched).toBe(false);
|
|
139
|
-
});
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
describe("NoisePrototypeBank — correction & lifecycle", () => {
|
|
143
|
-
it("removePrototype surgically deletes one false positive", async () => {
|
|
144
|
-
const { bank } = makeBank({ minSeenCount: 1 });
|
|
145
|
-
await bank.learnFromRejection("噪声内容甲甲甲甲", embedAxis0);
|
|
146
|
-
const id = bank.list()[0]?.id ?? "";
|
|
147
|
-
expect(bank.removePrototype(id)).toBe(true);
|
|
148
|
-
expect(bank.list().length).toBe(0);
|
|
149
|
-
expect(bank.removePrototype(id)).toBe(false);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it("clear resets everything", async () => {
|
|
153
|
-
const { bank } = makeBank({ minSeenCount: 1 });
|
|
154
|
-
await bank.learnFromRejection("噪声内容甲甲甲甲", embedAxis0);
|
|
155
|
-
bank.clear();
|
|
156
|
-
expect(bank.list().length).toBe(0);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it("state survives reconstruction from disk; expired prototypes are swept", async () => {
|
|
160
|
-
const { bank, dir } = makeBank({ minSeenCount: 1 });
|
|
161
|
-
await bank.learnFromRejection("噪声内容甲甲甲甲", embedAxis0);
|
|
162
|
-
|
|
163
|
-
const reloaded = new NoisePrototypeBank({
|
|
164
|
-
filePath: join(dir, "bank.json"),
|
|
165
|
-
shadowLogPath: join(dir, "shadow.jsonl"),
|
|
166
|
-
});
|
|
167
|
-
expect(reloaded.list().length).toBe(1);
|
|
168
|
-
|
|
169
|
-
// Age the stored prototype past the TTL, then reload.
|
|
170
|
-
const raw = JSON.parse(readFileSync(join(dir, "bank.json"), "utf8"));
|
|
171
|
-
raw.prototypes[0].learnedAt = "2020-01-01T00:00:00.000Z";
|
|
172
|
-
writeFileSync(join(dir, "bank.json"), JSON.stringify(raw), "utf8");
|
|
173
|
-
const expiring = new NoisePrototypeBank({
|
|
174
|
-
filePath: join(dir, "bank.json"),
|
|
175
|
-
shadowLogPath: join(dir, "shadow.jsonl"),
|
|
176
|
-
});
|
|
177
|
-
expect(expiring.list().length).toBe(0);
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
describe("isProtectedContent", () => {
|
|
182
|
-
it("covers memory verbs, corrections, decisions, identity, preferences", () => {
|
|
183
|
-
for (const text of ["记住这个", "帮我记一下", "纠正一下,不对", "我决定用方案B", "我是安闲静雅", "我喜欢深色主题", "i prefer dark mode", "remember this setup"]) {
|
|
184
|
-
expect(`${text}:${isProtectedContent(text)}`).toBe(`${text}:true`);
|
|
185
|
-
}
|
|
186
|
-
for (const text of ["嗯嗯好的", "git push", "今天天气不错"]) {
|
|
187
|
-
expect(`${text}:${isProtectedContent(text)}`).toBe(`${text}:false`);
|
|
188
|
-
}
|
|
189
|
-
});
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
describe("resolveNoisePrototypeMode / shared bank resolution", () => {
|
|
193
|
-
it("defaults to shadow in production, off under NODE_ENV=test unless explicit", () => {
|
|
194
|
-
expect(resolveNoisePrototypeMode({} as NodeJS.ProcessEnv)).toBe("shadow");
|
|
195
|
-
expect(resolveNoisePrototypeMode({ NODE_ENV: "test" } as NodeJS.ProcessEnv)).toBe("off");
|
|
196
|
-
expect(resolveNoisePrototypeMode({ NODE_ENV: "test", RECALLNEST_NOISE_PROTOTYPE: "shadow" } as NodeJS.ProcessEnv)).toBe("shadow");
|
|
197
|
-
expect(resolveNoisePrototypeMode({ RECALLNEST_NOISE_PROTOTYPE: "off" } as NodeJS.ProcessEnv)).toBe("off");
|
|
198
|
-
expect(resolveNoisePrototypeMode({ RECALLNEST_NOISE_PROTOTYPE: "banana" } as NodeJS.ProcessEnv)).toBe("shadow");
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it("explicit deps win over the env-governed shared instance", () => {
|
|
202
|
-
const { bank } = makeBank();
|
|
203
|
-
expect(resolveNoisePrototypeBank(bank)).toBe(bank);
|
|
204
|
-
expect(resolveNoisePrototypeBank(null)).toBeNull();
|
|
205
|
-
// Under bun test (NODE_ENV=test, no explicit env) the shared bank is off.
|
|
206
|
-
expect(resolveNoisePrototypeBank(undefined)).toBeNull();
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
// --- integration through persistMemory ---
|
|
211
|
-
|
|
212
|
-
function makePersistDeps(bank: NoisePrototypeBank | null) {
|
|
213
|
-
const storeCalls: unknown[] = [];
|
|
214
|
-
return {
|
|
215
|
-
storeCalls,
|
|
216
|
-
deps: {
|
|
217
|
-
embedder: {
|
|
218
|
-
async embedPassage() {
|
|
219
|
-
return axisVector(0);
|
|
220
|
-
},
|
|
221
|
-
},
|
|
222
|
-
store: {
|
|
223
|
-
async store(entry: Record<string, unknown>) {
|
|
224
|
-
storeCalls.push(entry);
|
|
225
|
-
return { ...entry, id: `mem-${storeCalls.length}`, timestamp: Date.now() };
|
|
226
|
-
},
|
|
227
|
-
},
|
|
228
|
-
noisePrototypeBank: bank,
|
|
229
|
-
},
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
describe("persistMemory × noise-prototype shadow", () => {
|
|
234
|
-
it("noise_detected rejection feeds the bank; the write is still rejected", async () => {
|
|
235
|
-
const { bank } = makeBank({ minSeenCount: 1 });
|
|
236
|
-
const { deps } = makePersistDeps(bank);
|
|
237
|
-
|
|
238
|
-
const result = await persistMemory(deps as never, {
|
|
239
|
-
text: "hello world, how are you today?",
|
|
240
|
-
category: "events",
|
|
241
|
-
importance: 0.7,
|
|
242
|
-
scope: "project:npb-test",
|
|
243
|
-
source: "manual",
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
expect(result.disposition).toBe("rejected");
|
|
247
|
-
expect(result.rejectionReason).toBe("noise_detected");
|
|
248
|
-
expect(bank.list().length).toBe(1); // minSeenCount 1 → learned immediately
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
it("accepted writes run the shadow match against the write-path vector", async () => {
|
|
252
|
-
const { bank, dir } = makeBank({ minSeenCount: 1 });
|
|
253
|
-
await bank.learnFromRejection("噪声原型种子内容", embedAxis0);
|
|
254
|
-
const { deps } = makePersistDeps(bank);
|
|
255
|
-
|
|
256
|
-
const result = await persistMemory(deps as never, {
|
|
257
|
-
text: "RecallNest 的 readConsistencyInterval 已经默认为 0",
|
|
258
|
-
category: "events",
|
|
259
|
-
importance: 0.7,
|
|
260
|
-
scope: "project:npb-test",
|
|
261
|
-
source: "manual",
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
expect(result.disposition).not.toBe("rejected");
|
|
265
|
-
const log = readFileSync(join(dir, "shadow.jsonl"), "utf8");
|
|
266
|
-
expect(log).toContain("suggest_reject"); // axis-0 fake vectors always collide
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
it("bank=null disables both sides completely", async () => {
|
|
270
|
-
const { deps } = makePersistDeps(null);
|
|
271
|
-
const result = await persistMemory(deps as never, {
|
|
272
|
-
text: "hello world, how are you today?",
|
|
273
|
-
category: "events",
|
|
274
|
-
importance: 0.7,
|
|
275
|
-
scope: "project:npb-test",
|
|
276
|
-
source: "manual",
|
|
277
|
-
});
|
|
278
|
-
expect(result.disposition).toBe("rejected"); // admission unchanged
|
|
279
|
-
});
|
|
280
|
-
});
|