recallnest 1.4.1 → 2.2.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/.dockerignore +11 -0
- package/CLAUDE.md +7 -3
- package/Dockerfile +20 -0
- package/README.md +202 -130
- package/README_CN.md +263 -190
- package/ROADMAP.md +128 -81
- package/SKILL.md +250 -0
- package/assets/dashboard.png +0 -0
- package/assets/knowledge-graph.png +0 -0
- package/assets/screenshots/ui-full.png +0 -0
- package/assets/screenshots/ui-overview.png +0 -0
- package/assets/screenshots/ui-search.png +0 -0
- package/assets/ui/app.js +227 -3
- package/assets/ui/index.html +81 -11
- package/assets/ui/styles.css +178 -2
- package/bin/recallnest.mjs +62 -0
- package/bun.lock +337 -0
- package/daily-recall.sh +23 -0
- package/daily-reflections/2026-04-02.md +35 -0
- package/daily-reflections/2026-04-03.md +37 -0
- package/daily-reflections/2026-04-04.md +37 -0
- package/daily-reflections/2026-04-05.md +35 -0
- package/daily-reflections/2026-04-06.md +36 -0
- package/daily-reflections/2026-04-07.md +35 -0
- package/daily-reflections/2026-04-08.md +35 -0
- package/daily-reflections/2026-04-12.md +35 -0
- package/data/alias-map.example.json +22 -0
- package/docs/plans/2026-04-01-recall-boost.md +100 -0
- package/docs/superpowers/plans/2026-04-11-constructive-retrieval-emotion.md +1195 -0
- package/docs/superpowers/specs/2026-04-11-constructive-retrieval-emotion-design.md +398 -0
- package/glama.json +6 -0
- package/integrations/setup-all.sh +31 -0
- package/package.json +10 -3
- package/plan-philosophy-upgrades.md +363 -0
- package/plan-product-upgrades.md +155 -0
- package/plan.md +140 -0
- package/research-memory-evolution.md +440 -0
- package/research-rl-agent-memory.md +330 -0
- package/scripts/backfill-anchors.ts +130 -0
- package/scripts/diagnose-vector-search.ts +132 -0
- package/scripts/migrate-language.ts +75 -0
- package/scripts/verify-emotion-decay.ts +148 -0
- package/src/__tests__/activity-counter.test.ts +96 -0
- package/src/__tests__/admission-control.test.ts +206 -0
- package/src/__tests__/anchor-generator.test.ts +67 -0
- package/src/__tests__/babel-integration.test.ts +139 -0
- package/src/__tests__/capture-engine.test.ts +5 -3
- package/src/__tests__/cascade-forget.test.ts +1 -1
- 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 +712 -0
- package/src/__tests__/confidence-metadata.test.ts +125 -0
- package/src/__tests__/confidence-tracker.test.ts +53 -0
- package/src/__tests__/consolidation-engine.test.ts +1 -1
- package/src/__tests__/constructive-retrieval.test.ts +509 -0
- package/src/__tests__/context-collapse-renderer.test.ts +214 -0
- package/src/__tests__/context-reconstructor.test.ts +65 -0
- package/src/__tests__/conversation-importer.test.ts +692 -0
- package/src/__tests__/data-checkup.test.ts +225 -0
- package/src/__tests__/decay-exemption.test.ts +84 -0
- package/src/__tests__/dedup-guard.test.ts +39 -0
- package/src/__tests__/distill-lock.test.ts +176 -0
- package/src/__tests__/dream-pipeline.test.ts +232 -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__/ethics-layer.test.ts +476 -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__/interference-detector.test.ts +203 -0
- package/src/__tests__/mcp-efficiency.test.ts +288 -0
- package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +243 -0
- package/src/__tests__/memory-evolution.test.ts +271 -0
- package/src/__tests__/memory-lint.test.ts +271 -0
- package/src/__tests__/narrative-architecture.test.ts +456 -0
- package/src/__tests__/pending-review.test.ts +77 -0
- package/src/__tests__/post-retrieval-filter.test.ts +178 -0
- package/src/__tests__/prediction-engine.test.ts +349 -0
- package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
- package/src/__tests__/preference-slots.test.ts +99 -0
- package/src/__tests__/query-expander-alias.test.ts +89 -0
- package/src/__tests__/recall-governor.test.ts +227 -0
- package/src/__tests__/resume-context.test.ts +156 -1
- package/src/__tests__/salience-scoring.test.ts +106 -0
- package/src/__tests__/session-distiller.test.ts +611 -0
- package/src/__tests__/skill-engine.test.ts +546 -0
- package/src/__tests__/skill-promotion.test.ts +349 -0
- package/src/__tests__/supersede-chain.test.ts +154 -0
- package/src/__tests__/temporal-validity.test.ts +160 -0
- package/src/__tests__/topic-tag.test.ts +113 -0
- package/src/__tests__/trusted-memory.test.ts +828 -0
- package/src/__tests__/write-verifier.test.ts +122 -0
- package/src/access-tracker.ts +3 -2
- package/src/activity-counter.ts +109 -0
- package/src/adaptive-retrieval.ts +6 -5
- package/src/admission-control.ts +163 -0
- package/src/anchor-generator.ts +80 -0
- package/src/api-server.ts +10 -0
- package/src/audit-log.ts +122 -0
- package/src/auto-gc.ts +98 -29
- package/src/auto-recall.ts +33 -4
- package/src/capture-engine.ts +372 -23
- package/src/cascade-forget.ts +3 -2
- package/src/cli.ts +107 -5
- package/src/compat.ts +22 -0
- package/src/confidence-tracker.ts +210 -47
- package/src/conflict-store.ts +12 -3
- package/src/consolidation-engine.ts +296 -8
- package/src/context-collapse-renderer.ts +222 -0
- package/src/context-composer-stable-selection.ts +5 -1
- package/src/context-composer.ts +239 -4
- package/src/context-reconstructor.ts +404 -0
- package/src/conversation-importer.ts +441 -0
- package/src/data-checkup.ts +232 -0
- package/src/decay-engine.ts +84 -0
- package/src/distill-lock.ts +122 -0
- package/src/doctor.ts +8 -10
- package/src/dream-pipeline.ts +227 -0
- package/src/emotion-detector.ts +90 -0
- package/src/eval.ts +3 -2
- package/src/forget-engine.ts +269 -0
- package/src/frequency-tracker.ts +175 -0
- package/src/graph-export.ts +644 -0
- package/src/ingest.ts +36 -6
- package/src/interference-detector.ts +187 -0
- package/src/kg-extractor.ts +15 -24
- package/src/kg-store.ts +2 -1
- package/src/language-hook.ts +67 -0
- package/src/llm-client.ts +289 -6
- package/src/mcp-server.ts +707 -80
- package/src/memory-assets.ts +22 -5
- package/src/memory-evolution.ts +365 -0
- package/src/memory-lint.ts +429 -0
- package/src/memory-output.ts +69 -0
- package/src/memory-schema.ts +74 -2
- package/src/narrative-schema.ts +79 -0
- package/src/narrative-tagger.ts +164 -0
- package/src/pii-detector.ts +98 -0
- package/src/post-retrieval-filter.ts +76 -0
- package/src/prediction-engine.ts +253 -0
- package/src/preference-slots.ts +65 -2
- package/src/prospective-memory.ts +215 -0
- package/src/query-expander.ts +67 -0
- package/src/recall-governor.ts +183 -0
- package/src/retention-policy.ts +90 -0
- package/src/retriever.ts +828 -55
- package/src/rif.ts +34 -8
- package/src/runtime-config.ts +23 -8
- package/src/scope-inventory.ts +4 -3
- package/src/scope-policy.ts +4 -0
- package/src/session-distiller.ts +307 -0
- package/src/session-output.ts +40 -0
- package/src/session-schema.ts +37 -0
- package/src/session-store.ts +7 -2
- package/src/skill-engine.ts +208 -0
- package/src/skill-promotion.ts +293 -0
- package/src/skill-schema.ts +39 -0
- package/src/store.ts +96 -10
- package/src/topic-tag.ts +66 -0
- package/src/tracker.ts +3 -1
- package/src/ui-server.ts +80 -8
- package/src/workflow-observation-scope-review.ts +2 -1
- package/src/workflow-observation-store.ts +2 -1
- package/src/write-verifier.ts +126 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
GovernorSession,
|
|
5
|
+
governResults,
|
|
6
|
+
resolveGovernorConfig,
|
|
7
|
+
truncateQuery,
|
|
8
|
+
} from "../recall-governor.js";
|
|
9
|
+
import type { RetrievalResult } from "../retriever.js";
|
|
10
|
+
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Helpers
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
|
|
15
|
+
function makeResult(
|
|
16
|
+
id: string,
|
|
17
|
+
text: string,
|
|
18
|
+
score: number,
|
|
19
|
+
metadata = "{}",
|
|
20
|
+
): RetrievalResult {
|
|
21
|
+
return {
|
|
22
|
+
entry: {
|
|
23
|
+
id,
|
|
24
|
+
text,
|
|
25
|
+
vector: [],
|
|
26
|
+
category: "events",
|
|
27
|
+
scope: "project:test",
|
|
28
|
+
importance: 0.8,
|
|
29
|
+
timestamp: Date.now(),
|
|
30
|
+
metadata,
|
|
31
|
+
},
|
|
32
|
+
score,
|
|
33
|
+
sources: { fused: { score } },
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// truncateQuery
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
describe("truncateQuery", () => {
|
|
42
|
+
it("returns query unchanged when within limit", () => {
|
|
43
|
+
expect(truncateQuery("short", 1000)).toBe("short");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("truncates query exceeding maxQueryChars", () => {
|
|
47
|
+
const long = "a".repeat(2000);
|
|
48
|
+
const result = truncateQuery(long, 500);
|
|
49
|
+
expect(result.length).toBe(500);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("handles exact boundary", () => {
|
|
53
|
+
const exact = "x".repeat(100);
|
|
54
|
+
expect(truncateQuery(exact, 100)).toBe(exact);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// resolveGovernorConfig
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
describe("resolveGovernorConfig", () => {
|
|
63
|
+
it("returns defaults when no overrides", () => {
|
|
64
|
+
const cfg = resolveGovernorConfig();
|
|
65
|
+
expect(cfg.maxQueryChars).toBe(1000);
|
|
66
|
+
expect(cfg.charBudget).toBe(8000);
|
|
67
|
+
expect(cfg.maxItems).toBe(10);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("merges partial overrides", () => {
|
|
71
|
+
const cfg = resolveGovernorConfig({ charBudget: 4000 });
|
|
72
|
+
expect(cfg.charBudget).toBe(4000);
|
|
73
|
+
expect(cfg.maxItems).toBe(10); // unchanged
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// GovernorSession
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
describe("GovernorSession", () => {
|
|
82
|
+
it("tracks injected IDs", () => {
|
|
83
|
+
const session = new GovernorSession();
|
|
84
|
+
expect(session.wasInjected("a")).toBe(false);
|
|
85
|
+
session.markInjected("a");
|
|
86
|
+
expect(session.wasInjected("a")).toBe(true);
|
|
87
|
+
expect(session.size).toBe(1);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("markAll registers all result IDs", () => {
|
|
91
|
+
const session = new GovernorSession();
|
|
92
|
+
const results = [makeResult("x", "hi", 0.9), makeResult("y", "hello", 0.8)];
|
|
93
|
+
session.markAll(results);
|
|
94
|
+
expect(session.wasInjected("x")).toBe(true);
|
|
95
|
+
expect(session.wasInjected("y")).toBe(true);
|
|
96
|
+
expect(session.size).toBe(2);
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// governResults — evolution state filter
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
describe("governResults — evolution filter", () => {
|
|
105
|
+
it("keeps active memories", () => {
|
|
106
|
+
const results = [makeResult("a1", "active memory", 0.9, '{"evolution":{"status":"active"}}')];
|
|
107
|
+
const governed = governResults(results);
|
|
108
|
+
expect(governed).toHaveLength(1);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("keeps pending_review memories", () => {
|
|
112
|
+
const results = [makeResult("p1", "pending review", 0.9, '{"evolution":{"status":"pending_review"}}')];
|
|
113
|
+
const governed = governResults(results);
|
|
114
|
+
expect(governed).toHaveLength(1);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("drops archived memories", () => {
|
|
118
|
+
const results = [makeResult("ar1", "archived", 0.9, '{"evolution":{"status":"archived"}}')];
|
|
119
|
+
const governed = governResults(results);
|
|
120
|
+
expect(governed).toHaveLength(0);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("drops superseded memories", () => {
|
|
124
|
+
const results = [makeResult("s1", "superseded", 0.9, '{"evolution":{"status":"superseded"}}')];
|
|
125
|
+
const governed = governResults(results);
|
|
126
|
+
expect(governed).toHaveLength(0);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("keeps memories with no evolution (defaults to active)", () => {
|
|
130
|
+
const results = [makeResult("n1", "no status", 0.9, "{}")];
|
|
131
|
+
const governed = governResults(results);
|
|
132
|
+
expect(governed).toHaveLength(1);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
// governResults — budget control
|
|
138
|
+
// ---------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
describe("governResults — budget control", () => {
|
|
141
|
+
it("respects maxItems limit", () => {
|
|
142
|
+
const results = Array.from({ length: 5 }, (_, i) =>
|
|
143
|
+
makeResult(`b${i}`, "x".repeat(10), 0.9 - i * 0.01),
|
|
144
|
+
);
|
|
145
|
+
const governed = governResults(results, undefined, { maxItems: 3 });
|
|
146
|
+
expect(governed).toHaveLength(3);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("respects charBudget limit", () => {
|
|
150
|
+
const results = [
|
|
151
|
+
makeResult("c1", "a".repeat(3000), 0.95),
|
|
152
|
+
makeResult("c2", "b".repeat(3000), 0.90),
|
|
153
|
+
makeResult("c3", "c".repeat(3000), 0.85),
|
|
154
|
+
];
|
|
155
|
+
const governed = governResults(results, undefined, { charBudget: 5000 });
|
|
156
|
+
// First item (3000) fits, second (6000 total) would exceed 5000 → stops after first
|
|
157
|
+
expect(governed).toHaveLength(1);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("always allows at least one result even if it exceeds budget", () => {
|
|
161
|
+
const results = [makeResult("big", "x".repeat(10000), 0.95)];
|
|
162
|
+
const governed = governResults(results, undefined, { charBudget: 100 });
|
|
163
|
+
expect(governed).toHaveLength(1);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// governResults — session dedup
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
describe("governResults — session dedup", () => {
|
|
172
|
+
it("drops already-injected IDs", () => {
|
|
173
|
+
const session = new GovernorSession();
|
|
174
|
+
session.markInjected("dup1");
|
|
175
|
+
|
|
176
|
+
const results = [
|
|
177
|
+
makeResult("dup1", "already seen", 0.95),
|
|
178
|
+
makeResult("new1", "fresh memory", 0.90),
|
|
179
|
+
];
|
|
180
|
+
const governed = governResults(results, session);
|
|
181
|
+
expect(governed).toHaveLength(1);
|
|
182
|
+
expect(governed[0].entry.id).toBe("new1");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("marks newly governed results in the session", () => {
|
|
186
|
+
const session = new GovernorSession();
|
|
187
|
+
const results = [makeResult("x1", "hello", 0.9)];
|
|
188
|
+
governResults(results, session);
|
|
189
|
+
expect(session.wasInjected("x1")).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("skips dedup when no session provided", () => {
|
|
193
|
+
const results = [makeResult("a", "text", 0.9), makeResult("a", "text", 0.8)];
|
|
194
|
+
// Without session, same ID can appear (though unusual from retriever)
|
|
195
|
+
const governed = governResults(results);
|
|
196
|
+
expect(governed).toHaveLength(2);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// governResults — empty input
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
describe("governResults — edge cases", () => {
|
|
205
|
+
it("returns empty array for empty input", () => {
|
|
206
|
+
expect(governResults([])).toHaveLength(0);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("composes all layers correctly", () => {
|
|
210
|
+
const session = new GovernorSession();
|
|
211
|
+
session.markInjected("already");
|
|
212
|
+
|
|
213
|
+
const results = [
|
|
214
|
+
makeResult("already", "seen before", 0.99), // dedup
|
|
215
|
+
makeResult("archived", "old", 0.95, '{"evolution":{"status":"archived"}}'), // evolution
|
|
216
|
+
makeResult("keep1", "a".repeat(4000), 0.90), // kept
|
|
217
|
+
makeResult("keep2", "b".repeat(4000), 0.85), // kept
|
|
218
|
+
makeResult("over-budget", "c".repeat(4000), 0.80), // budget
|
|
219
|
+
];
|
|
220
|
+
const governed = governResults(results, session, { charBudget: 8000 });
|
|
221
|
+
// archived dropped by evolution, already dropped by dedup,
|
|
222
|
+
// keep1 (4000) + keep2 (8000) = at budget, over-budget dropped
|
|
223
|
+
expect(governed.map((r) => r.entry.id)).toEqual(["keep1", "keep2"]);
|
|
224
|
+
expect(session.wasInjected("keep1")).toBe(true);
|
|
225
|
+
expect(session.wasInjected("keep2")).toBe(true);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
2
|
|
|
3
|
-
import { composeResumeContext } from "../context-composer.js";
|
|
3
|
+
import { composeLightResumeContext, composeResumeContext } from "../context-composer.js";
|
|
4
4
|
import type { RetrievalContext, RetrievalResult } from "../retriever.js";
|
|
5
5
|
import type { SessionCheckpointRecord } from "../session-schema.js";
|
|
6
6
|
|
|
@@ -4741,3 +4741,158 @@ describe("composeResumeContext", () => {
|
|
|
4741
4741
|
expect(response.relevantPatterns.join(" ")).toContain("resume_context");
|
|
4742
4742
|
});
|
|
4743
4743
|
});
|
|
4744
|
+
|
|
4745
|
+
// ---------------------------------------------------------------------------
|
|
4746
|
+
// MP-3: composeLightResumeContext — Ultra-Light Wake-up Mode
|
|
4747
|
+
// ---------------------------------------------------------------------------
|
|
4748
|
+
|
|
4749
|
+
describe("composeLightResumeContext", () => {
|
|
4750
|
+
const checkpoint: SessionCheckpointRecord = {
|
|
4751
|
+
checkpointId: "cp-light-1",
|
|
4752
|
+
sessionId: "session-light",
|
|
4753
|
+
resolvedScope: "project:recallnest",
|
|
4754
|
+
summary: "Implemented LME-9 circuit breaker for LLM degradation",
|
|
4755
|
+
task: "LME-9 circuit breaker",
|
|
4756
|
+
decisions: ["Three-layer degradation"],
|
|
4757
|
+
openLoops: ["Soak test pending"],
|
|
4758
|
+
nextActions: ["Run full eval"],
|
|
4759
|
+
entities: ["RecallNest"],
|
|
4760
|
+
files: ["src/llm-client.ts"],
|
|
4761
|
+
updatedAt: "2026-04-06T10:00:00.000Z",
|
|
4762
|
+
};
|
|
4763
|
+
|
|
4764
|
+
it("returns checkpoint summary and stable memories under token budget", async () => {
|
|
4765
|
+
const retriever = {
|
|
4766
|
+
async retrieve(): Promise<RetrievalResult[]> {
|
|
4767
|
+
return [
|
|
4768
|
+
buildResult("p1", "profile", "User builds local-first memory systems with RecallNest."),
|
|
4769
|
+
buildResult("pref1", "preferences", "User prefers concise technical replies without fluff."),
|
|
4770
|
+
buildResult("e1", "entities", "RecallNest is a cross-terminal shared memory layer."),
|
|
4771
|
+
];
|
|
4772
|
+
},
|
|
4773
|
+
};
|
|
4774
|
+
|
|
4775
|
+
const result = await composeLightResumeContext({
|
|
4776
|
+
retriever,
|
|
4777
|
+
checkpointStore: { async getLatest() { return checkpoint; } },
|
|
4778
|
+
listPins: () => [],
|
|
4779
|
+
}, {
|
|
4780
|
+
task: "继续 RecallNest 项目",
|
|
4781
|
+
scope: "project:recallnest",
|
|
4782
|
+
includeLatestCheckpoint: true,
|
|
4783
|
+
limitPerSection: 3,
|
|
4784
|
+
});
|
|
4785
|
+
|
|
4786
|
+
expect(result.text).toContain("session-light");
|
|
4787
|
+
expect(result.text).toContain("LME-9");
|
|
4788
|
+
expect(result.text).toContain("resume_context(mode='full')");
|
|
4789
|
+
expect(result.resolvedScope).toBe("project:recallnest");
|
|
4790
|
+
expect(result.generatedAt).toBeTruthy();
|
|
4791
|
+
// Budget check: <300 tokens ≈ <1200 chars
|
|
4792
|
+
expect(result.text.length).toBeLessThan(1200);
|
|
4793
|
+
});
|
|
4794
|
+
|
|
4795
|
+
it("works without checkpoint", async () => {
|
|
4796
|
+
const retriever = {
|
|
4797
|
+
async retrieve(): Promise<RetrievalResult[]> {
|
|
4798
|
+
return [
|
|
4799
|
+
buildResult("p1", "profile", "Data scientist working on observability."),
|
|
4800
|
+
];
|
|
4801
|
+
},
|
|
4802
|
+
};
|
|
4803
|
+
|
|
4804
|
+
const result = await composeLightResumeContext({
|
|
4805
|
+
retriever,
|
|
4806
|
+
checkpointStore: { async getLatest() { return null; } },
|
|
4807
|
+
listPins: () => [],
|
|
4808
|
+
}, {
|
|
4809
|
+
includeLatestCheckpoint: true,
|
|
4810
|
+
limitPerSection: 3,
|
|
4811
|
+
});
|
|
4812
|
+
|
|
4813
|
+
expect(result.text).not.toContain("Last session");
|
|
4814
|
+
expect(result.text).toContain("observability");
|
|
4815
|
+
expect(result.text).toContain("resume_context(mode='full')");
|
|
4816
|
+
});
|
|
4817
|
+
|
|
4818
|
+
it("includes pin assets as stable context", async () => {
|
|
4819
|
+
const retriever = {
|
|
4820
|
+
async retrieve(): Promise<RetrievalResult[]> {
|
|
4821
|
+
return [];
|
|
4822
|
+
},
|
|
4823
|
+
};
|
|
4824
|
+
|
|
4825
|
+
const result = await composeLightResumeContext({
|
|
4826
|
+
retriever,
|
|
4827
|
+
checkpointStore: { async getLatest() { return checkpoint; } },
|
|
4828
|
+
listPins: () => [{
|
|
4829
|
+
path: "/tmp/pin1.md",
|
|
4830
|
+
title: "Auth migration decision",
|
|
4831
|
+
summary: "Session tokens must comply with new legal requirements",
|
|
4832
|
+
pinId: "pin-1",
|
|
4833
|
+
memoryId: "mem-1",
|
|
4834
|
+
createdAt: "2026-04-01T00:00:00.000Z",
|
|
4835
|
+
}],
|
|
4836
|
+
}, {
|
|
4837
|
+
scope: "project:recallnest",
|
|
4838
|
+
includeLatestCheckpoint: true,
|
|
4839
|
+
limitPerSection: 3,
|
|
4840
|
+
});
|
|
4841
|
+
|
|
4842
|
+
expect(result.text).toContain("legal requirements");
|
|
4843
|
+
});
|
|
4844
|
+
|
|
4845
|
+
it("deduplicates pin content from stable memories", async () => {
|
|
4846
|
+
const retriever = {
|
|
4847
|
+
async retrieve(): Promise<RetrievalResult[]> {
|
|
4848
|
+
return [
|
|
4849
|
+
buildResult("dup1", "profile", "Session tokens must comply with new legal requirements"),
|
|
4850
|
+
];
|
|
4851
|
+
},
|
|
4852
|
+
};
|
|
4853
|
+
|
|
4854
|
+
const result = await composeLightResumeContext({
|
|
4855
|
+
retriever,
|
|
4856
|
+
checkpointStore: { async getLatest() { return null; } },
|
|
4857
|
+
listPins: () => [{
|
|
4858
|
+
path: "/tmp/pin1.md",
|
|
4859
|
+
title: "Auth migration",
|
|
4860
|
+
summary: "Session tokens must comply with new legal requirements",
|
|
4861
|
+
pinId: "pin-1",
|
|
4862
|
+
memoryId: "mem-1",
|
|
4863
|
+
createdAt: "2026-04-01T00:00:00.000Z",
|
|
4864
|
+
}],
|
|
4865
|
+
}, {
|
|
4866
|
+
includeLatestCheckpoint: true,
|
|
4867
|
+
limitPerSection: 3,
|
|
4868
|
+
});
|
|
4869
|
+
|
|
4870
|
+
// Should not have duplicate entries
|
|
4871
|
+
const matches = result.text.match(/legal requirements/g);
|
|
4872
|
+
expect(matches?.length).toBeLessThanOrEqual(1);
|
|
4873
|
+
});
|
|
4874
|
+
|
|
4875
|
+
it("respects scope from input over checkpoint", async () => {
|
|
4876
|
+
const calls: RetrievalContext[] = [];
|
|
4877
|
+
const retriever = {
|
|
4878
|
+
async retrieve(ctx: RetrievalContext): Promise<RetrievalResult[]> {
|
|
4879
|
+
calls.push(ctx);
|
|
4880
|
+
return [];
|
|
4881
|
+
},
|
|
4882
|
+
};
|
|
4883
|
+
|
|
4884
|
+
const result = await composeLightResumeContext({
|
|
4885
|
+
retriever,
|
|
4886
|
+
checkpointStore: { async getLatest() { return checkpoint; } },
|
|
4887
|
+
listPins: () => [],
|
|
4888
|
+
}, {
|
|
4889
|
+
scope: "project:custom",
|
|
4890
|
+
includeLatestCheckpoint: true,
|
|
4891
|
+
limitPerSection: 3,
|
|
4892
|
+
});
|
|
4893
|
+
|
|
4894
|
+
expect(result.resolvedScope).toBe("project:custom");
|
|
4895
|
+
// retrieveCandidates fires scoped + global in parallel; at least one must have the scope filter
|
|
4896
|
+
expect(calls.some(c => c.scopeFilter?.includes("project:custom"))).toBe(true);
|
|
4897
|
+
});
|
|
4898
|
+
});
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { describe, it, expect } from "bun:test";
|
|
2
|
+
import type { LLMClient } from "../llm-client.js";
|
|
3
|
+
|
|
4
|
+
function createMockLLM(response: string): LLMClient {
|
|
5
|
+
return {
|
|
6
|
+
async chat() {
|
|
7
|
+
return response;
|
|
8
|
+
},
|
|
9
|
+
async assessImportance(text: string) {
|
|
10
|
+
const parsed = JSON.parse(response);
|
|
11
|
+
return parsed.importance ?? null;
|
|
12
|
+
},
|
|
13
|
+
async reassessImportanceBatch(
|
|
14
|
+
entries: Array<{ id: string; text: string; category: string; currentImportance: number }>,
|
|
15
|
+
) {
|
|
16
|
+
const results = new Map<string, number>();
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(response);
|
|
19
|
+
if (Array.isArray(parsed)) {
|
|
20
|
+
for (const item of parsed) {
|
|
21
|
+
if (item.idx >= 0 && item.idx < entries.length) {
|
|
22
|
+
results.set(entries[item.idx].id, item.importance);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
// match real implementation: non-fatal
|
|
28
|
+
}
|
|
29
|
+
return results;
|
|
30
|
+
},
|
|
31
|
+
} as any;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
describe("HP-4: Salience scoring anchoring", () => {
|
|
35
|
+
describe("assessImportance anchored tiers", () => {
|
|
36
|
+
it("identity/correction → 0.85+", async () => {
|
|
37
|
+
const llm = createMockLLM(JSON.stringify({ importance: 0.92, reason: "用户明确纠正" }));
|
|
38
|
+
const result = await llm.assessImportance("不要用 rm 删除文件", "profile");
|
|
39
|
+
expect(result).toBeGreaterThanOrEqual(0.85);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("decision/case → 0.70-0.84", async () => {
|
|
43
|
+
const llm = createMockLLM(JSON.stringify({ importance: 0.75, reason: "架构决策" }));
|
|
44
|
+
const result = await llm.assessImportance("选用 LanceDB 作为向量存储", "cases");
|
|
45
|
+
expect(result).toBeGreaterThanOrEqual(0.70);
|
|
46
|
+
expect(result).toBeLessThan(0.85);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("entity/background → 0.55-0.69", async () => {
|
|
50
|
+
const llm = createMockLLM(JSON.stringify({ importance: 0.60, reason: "项目信息" }));
|
|
51
|
+
const result = await llm.assessImportance("RecallNest 使用 Bun 运行时", "entities");
|
|
52
|
+
expect(result).toBeGreaterThanOrEqual(0.55);
|
|
53
|
+
expect(result).toBeLessThan(0.70);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("event/one-off → 0.40-0.54", async () => {
|
|
57
|
+
const llm = createMockLLM(JSON.stringify({ importance: 0.45, reason: "一次性操作" }));
|
|
58
|
+
const result = await llm.assessImportance("今天部署了 v2.1", "events");
|
|
59
|
+
expect(result).toBeGreaterThanOrEqual(0.40);
|
|
60
|
+
expect(result).toBeLessThan(0.55);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("chatter/temp → 0.10-0.30", async () => {
|
|
64
|
+
const llm = createMockLLM(JSON.stringify({ importance: 0.15, reason: "临时上下文" }));
|
|
65
|
+
const result = await llm.assessImportance("好的,我看一下", "events");
|
|
66
|
+
expect(result).toBeGreaterThanOrEqual(0.10);
|
|
67
|
+
expect(result).toBeLessThan(0.40);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("reassessImportanceBatch", () => {
|
|
72
|
+
it("returns adjusted entries with ≥ 0.1 delta", async () => {
|
|
73
|
+
const batchResponse = JSON.stringify([
|
|
74
|
+
{ idx: 0, importance: 0.90 },
|
|
75
|
+
{ idx: 2, importance: 0.30 },
|
|
76
|
+
]);
|
|
77
|
+
const llm = createMockLLM(batchResponse);
|
|
78
|
+
const entries = [
|
|
79
|
+
{ id: "a", text: "永远不要用 rm", category: "profile", currentImportance: 0.70 },
|
|
80
|
+
{ id: "b", text: "选用 LanceDB", category: "cases", currentImportance: 0.75 },
|
|
81
|
+
{ id: "c", text: "今天天气不错", category: "events", currentImportance: 0.70 },
|
|
82
|
+
];
|
|
83
|
+
const result = await llm.reassessImportanceBatch(entries);
|
|
84
|
+
expect(result.size).toBe(2);
|
|
85
|
+
expect(result.get("a")).toBe(0.90);
|
|
86
|
+
expect(result.get("c")).toBe(0.30);
|
|
87
|
+
expect(result.has("b")).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("returns empty map on empty input", async () => {
|
|
91
|
+
const llm = createMockLLM("[]");
|
|
92
|
+
const result = await llm.reassessImportanceBatch([]);
|
|
93
|
+
expect(result.size).toBe(0);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("handles invalid LLM response gracefully", async () => {
|
|
97
|
+
const llm = createMockLLM("not-json");
|
|
98
|
+
const entries = [
|
|
99
|
+
{ id: "a", text: "test", category: "events", currentImportance: 0.5 },
|
|
100
|
+
];
|
|
101
|
+
// Should not throw
|
|
102
|
+
const result = await llm.reassessImportanceBatch(entries);
|
|
103
|
+
expect(result.size).toBe(0);
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|