recallnest 1.4.1 → 2.2.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/.env +4 -0
- package/CLAUDE.md +7 -3
- package/README.md +202 -130
- package/README_CN.md +263 -190
- package/bin/recallnest.mjs +62 -0
- package/config.json +55 -0
- package/daily-recall.sh +23 -0
- package/integrations/setup-all.sh +31 -0
- package/logs/daily-recall.error.log +0 -0
- package/logs/daily-recall.log +30 -0
- package/logs/ingest-2026-04-11.log +110 -0
- package/logs/ingest-2026-04-12.log +90 -0
- package/logs/launchagent-stderr.log +0 -0
- package/logs/launchagent-stdout.log +0 -0
- package/package.json +10 -3
- 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
- package/.claude-plugin/CLAUDE.md +0 -29
- package/.claude-plugin/marketplace.json +0 -18
- package/.github/workflows/ci.yml +0 -28
- package/.mcp.json +0 -9
- package/CHANGELOG.md +0 -51
- package/DEMO.md +0 -94
- package/EVAL.md +0 -89
- package/FAILURES.md +0 -75
- package/OPERATIONS.md +0 -59
- package/ROADMAP.md +0 -198
- package/assets/recallnest-workbench.png +0 -0
- package/assets/ui/app.js +0 -807
- package/assets/ui/index.html +0 -154
- package/assets/ui/styles.css +0 -605
- package/assets/wechat-qr.jpg +0 -0
- package/daily-reflections/2026-03-05.md +0 -15
- package/daily-reflections/2026-03-06.md +0 -15
- package/daily-reflections/2026-03-07.md +0 -7
- package/daily-reflections/2026-03-08.md +0 -34
- package/daily-reflections/2026-03-09.md +0 -35
- package/daily-reflections/2026-03-10.md +0 -36
- package/daily-reflections/2026-03-11.md +0 -35
- package/daily-reflections/2026-03-12.md +0 -36
- package/daily-reflections/2026-03-13.md +0 -35
- package/daily-reflections/2026-03-14.md +0 -35
- package/daily-reflections/2026-03-15.md +0 -35
- package/daily-reflections/2026-03-16.md +0 -41
- package/daily-reflections/2026-03-17.md +0 -10
- package/daily-reflections/2026-03-18.md +0 -10
- package/daily-reflections/2026-03-19.md +0 -10
- package/daily-reflections/2026-03-20.md +0 -10
- package/daily-reflections/2026-03-21.md +0 -10
- package/daily-reflections/2026-03-22.md +0 -10
- package/daily-reflections/2026-03-23.md +0 -10
- package/daily-reflections/2026-03-24.md +0 -10
- package/daily-reflections/2026-03-25.md +0 -32
- package/daily-reflections/2026-03-26.md +0 -37
- package/daily-reflections/2026-03-27.md +0 -35
- package/daily-reflections/2026-03-28.md +0 -35
- package/daily-reflections/2026-03-29.md +0 -35
- package/daily-reflections/2026-03-30.md +0 -36
- package/daily-reflections/2026-03-31.md +0 -35
- package/daily-reflections/2026-04-01.md +0 -35
- package/docs/api-reference.md +0 -1291
- package/docs/architecture.md +0 -168
- package/docs/memory-boundary-contract.md +0 -226
- package/docs/memory-categories.md +0 -119
- package/docs/pipeline-audit-ultramemory.md +0 -177
- package/docs/plans/2026-03-08-query-expansion.md +0 -270
- package/docs/plans/2026-03-16-continuity-layer.md +0 -196
- package/docs/plans/mcp-tiering-plan.md +0 -126
- package/docs/research/recall-quality-audit-2026-03-22.md +0 -153
- package/docs/self-evolution.md +0 -198
- package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +0 -782
- package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +0 -77
- package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +0 -95
- package/eval/continuity/results-2026-03-16.md +0 -117
- package/eval/continuity/results-2026-03-19.md +0 -453
- package/eval/continuity/runbook.md +0 -110
- package/eval/continuity/seed-cases.md +0 -54
- package/eval/reports/2026-03-06-abstract-query-baseline.md +0 -96
- package/eval/reports/2026-03-06-aws-ssh-baseline.md +0 -82
- package/eval/reports/2026-03-06-baseline.md +0 -82
- package/eval/reports/query-expansion.md +0 -138
- package/lm +0 -19
- package/plan-v1.2-distributable.md +0 -105
- package/scripts/archive-distilled-junk.ts +0 -116
- package/scripts/backfill-l0l1l2.ts +0 -98
- package/scripts/blind-scan.ts +0 -588
- package/scripts/cleanup-step1-exact-dups.ts +0 -117
- package/scripts/cleanup-step2-template-dups.ts +0 -113
- package/scripts/cleanup-step3-stale-content.ts +0 -149
- package/scripts/cleanup-step4-orphan-scopes.ts +0 -131
- package/scripts/delete-distilled-junk.ts +0 -88
- package/scripts/distill-facts.ts +0 -305
- package/scripts/health-check.ts +0 -343
- package/scripts/incremental-ingest.sh +0 -71
- package/scripts/mlp-health-check.ts +0 -382
- package/scripts/pull-claude-desktop.ts +0 -515
- package/scripts/re-ingest-cc.sh +0 -66
- package/scripts/rebalance-memory-health.ts +0 -181
- package/scripts/run-desktop-ingest.sh +0 -11
- package/scripts/start-server.sh +0 -12
- package/scripts/viz-dashboard.html +0 -423
- package/scripts/viz-export.ts +0 -703
- package/src/__tests__/access-tracker-novelty.test.ts +0 -228
- package/src/__tests__/auto-consolidation.test.ts +0 -168
- package/src/__tests__/auto-recall.test.ts +0 -131
- package/src/__tests__/batch-dedup.test.ts +0 -65
- package/src/__tests__/capture-engine.test.ts +0 -1142
- 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 -33
- package/src/__tests__/confidence-tracker.test.ts +0 -196
- 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 -374
- package/src/__tests__/consolidation-engine.test.ts +0 -181
- 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-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__/core-recall-eval.test.ts +0 -235
- package/src/__tests__/core-summary.test.ts +0 -109
- package/src/__tests__/dedup-actions.test.ts +0 -58
- package/src/__tests__/deterministic-id.test.ts +0 -39
- package/src/__tests__/doctor.test.ts +0 -303
- package/src/__tests__/drill-down.test.ts +0 -108
- package/src/__tests__/embedder.test.ts +0 -122
- package/src/__tests__/entity-resolver.test.ts +0 -58
- package/src/__tests__/event-segmenter.test.ts +0 -153
- package/src/__tests__/hotness-blend.test.ts +0 -76
- package/src/__tests__/ingest.test.ts +0 -360
- package/src/__tests__/kg-extractor.test.ts +0 -184
- package/src/__tests__/kg-store.test.ts +0 -198
- package/src/__tests__/llm-consolidation.test.ts +0 -226
- package/src/__tests__/memory-boundaries.test.ts +0 -237
- package/src/__tests__/memory-health-rebalance.test.ts +0 -144
- package/src/__tests__/memory-output.test.ts +0 -215
- package/src/__tests__/multi-vector.test.ts +0 -242
- package/src/__tests__/ppr-traversal.test.ts +0 -256
- package/src/__tests__/preference-guard-smoke.test.ts +0 -211
- package/src/__tests__/preference-matcher.test.ts +0 -230
- package/src/__tests__/preference-slots.test.ts +0 -106
- package/src/__tests__/prospective-memory.test.ts +0 -283
- package/src/__tests__/query-entity-detector.test.ts +0 -171
- package/src/__tests__/query-expander.test.ts +0 -34
- package/src/__tests__/recall-mode.test.ts +0 -64
- package/src/__tests__/result-synthesizer.test.ts +0 -198
- package/src/__tests__/resume-context.test.ts +0 -4743
- package/src/__tests__/retrieval-trace.test.ts +0 -213
- package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
- package/src/__tests__/retroactive-boost.test.ts +0 -223
- package/src/__tests__/scope-inventory.test.ts +0 -200
- package/src/__tests__/scope-policy.test.ts +0 -69
- package/src/__tests__/session-checkpoint.test.ts +0 -259
- package/src/__tests__/session-gc.test.ts +0 -215
- package/src/__tests__/store-scope-audit.test.ts +0 -81
- package/src/__tests__/temporal-parser.test.ts +0 -241
- package/src/__tests__/term-registry.test.ts +0 -121
- package/src/__tests__/tool-output-compressor.test.ts +0 -133
- package/src/__tests__/version-manager.test.ts +0 -213
- package/src/__tests__/workflow-observation.test.ts +0 -400
|
@@ -1,228 +0,0 @@
|
|
|
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
|
-
return {
|
|
38
|
-
updates,
|
|
39
|
-
data,
|
|
40
|
-
async getById(id: string) {
|
|
41
|
-
return data.get(id) ?? null;
|
|
42
|
-
},
|
|
43
|
-
async update(id: string, upd: { metadata?: string }) {
|
|
44
|
-
const entry = data.get(id);
|
|
45
|
-
if (!entry) return null;
|
|
46
|
-
if (upd.metadata) {
|
|
47
|
-
entry.metadata = upd.metadata;
|
|
48
|
-
updates.push({ id, metadata: upd.metadata });
|
|
49
|
-
}
|
|
50
|
-
return entry;
|
|
51
|
-
},
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// ---------------------------------------------------------------------------
|
|
56
|
-
// shouldReinforce() unit tests
|
|
57
|
-
// ---------------------------------------------------------------------------
|
|
58
|
-
|
|
59
|
-
describe("shouldReinforce", () => {
|
|
60
|
-
let tracker: AccessTracker;
|
|
61
|
-
|
|
62
|
-
beforeEach(() => {
|
|
63
|
-
tracker = new AccessTracker(
|
|
64
|
-
createMockStore([]) as any,
|
|
65
|
-
{ ...DEFAULT_ACCESS_TRACKER_CONFIG, noveltyThreshold: 0.35, cooldownMs: 300_000 },
|
|
66
|
-
);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it("rejects high-similarity (low-novelty) results", () => {
|
|
70
|
-
// similarity 0.90 → novelty 0.10 < threshold 0.35 → reject
|
|
71
|
-
expect(tracker.shouldReinforce("a", 0.90)).toBe(false);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it("rejects exact match (similarity 1.0)", () => {
|
|
75
|
-
expect(tracker.shouldReinforce("a", 1.0)).toBe(false);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("accepts low-similarity (high-novelty) results", () => {
|
|
79
|
-
// similarity 0.50 → novelty 0.50 > threshold 0.35 → accept
|
|
80
|
-
expect(tracker.shouldReinforce("a", 0.50)).toBe(true);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("accepts borderline novelty (exactly at threshold)", () => {
|
|
84
|
-
// similarity 0.65 → novelty 0.35 = threshold 0.35 → accept (not strictly less)
|
|
85
|
-
expect(tracker.shouldReinforce("a", 0.65)).toBe(true);
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it("rejects same entry within cooldown window", () => {
|
|
89
|
-
// First call: accepted
|
|
90
|
-
expect(tracker.shouldReinforce("a", 0.50)).toBe(true);
|
|
91
|
-
// Simulate recording to set cooldown
|
|
92
|
-
tracker.recordAccess(["a"], [0.50]);
|
|
93
|
-
// Second call within cooldown: rejected
|
|
94
|
-
expect(tracker.shouldReinforce("a", 0.50)).toBe(false);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it("accepts different entry even if another is on cooldown", () => {
|
|
98
|
-
tracker.recordAccess(["a"], [0.50]);
|
|
99
|
-
expect(tracker.shouldReinforce("b", 0.50)).toBe(true);
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it("bypasses novelty gate when threshold is 0", () => {
|
|
103
|
-
const noGateTracker = new AccessTracker(
|
|
104
|
-
createMockStore([]) as any,
|
|
105
|
-
{ ...DEFAULT_ACCESS_TRACKER_CONFIG, noveltyThreshold: 0, cooldownMs: 0 },
|
|
106
|
-
);
|
|
107
|
-
// Even high similarity passes
|
|
108
|
-
expect(noGateTracker.shouldReinforce("a", 0.99)).toBe(true);
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// ---------------------------------------------------------------------------
|
|
113
|
-
// recordAccess() with scores
|
|
114
|
-
// ---------------------------------------------------------------------------
|
|
115
|
-
|
|
116
|
-
describe("recordAccess with novelty gating", () => {
|
|
117
|
-
it("only queues novel entries for flush", async () => {
|
|
118
|
-
const entries = [makeEntry("a"), makeEntry("b"), makeEntry("c")];
|
|
119
|
-
const store = createMockStore(entries);
|
|
120
|
-
const tracker = new AccessTracker(store as any, {
|
|
121
|
-
...DEFAULT_ACCESS_TRACKER_CONFIG,
|
|
122
|
-
flushIntervalMs: 0, // immediate flush for testing
|
|
123
|
-
noveltyThreshold: 0.35,
|
|
124
|
-
cooldownMs: 0,
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
// scores: a=0.90 (low novelty, reject), b=0.50 (novel, accept), c=0.40 (novel, accept)
|
|
128
|
-
tracker.recordAccess(["a", "b", "c"], [0.90, 0.50, 0.40]);
|
|
129
|
-
|
|
130
|
-
await tracker.flush();
|
|
131
|
-
|
|
132
|
-
// Only b and c should have been updated
|
|
133
|
-
const updatedIds = store.updates.map(u => u.id);
|
|
134
|
-
expect(updatedIds).not.toContain("a");
|
|
135
|
-
expect(updatedIds).toContain("b");
|
|
136
|
-
expect(updatedIds).toContain("c");
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it("reinforces all entries when no scores provided (backward compat)", async () => {
|
|
140
|
-
const entries = [makeEntry("a"), makeEntry("b")];
|
|
141
|
-
const store = createMockStore(entries);
|
|
142
|
-
const tracker = new AccessTracker(store as any, {
|
|
143
|
-
...DEFAULT_ACCESS_TRACKER_CONFIG,
|
|
144
|
-
flushIntervalMs: 0,
|
|
145
|
-
noveltyThreshold: 0.35,
|
|
146
|
-
cooldownMs: 0,
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
// No scores → all reinforced
|
|
150
|
-
tracker.recordAccess(["a", "b"]);
|
|
151
|
-
await tracker.flush();
|
|
152
|
-
|
|
153
|
-
const updatedIds = store.updates.map(u => u.id);
|
|
154
|
-
expect(updatedIds).toContain("a");
|
|
155
|
-
expect(updatedIds).toContain("b");
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it("respects cooldown across multiple recordAccess calls", async () => {
|
|
159
|
-
const entries = [makeEntry("x")];
|
|
160
|
-
const store = createMockStore(entries);
|
|
161
|
-
const tracker = new AccessTracker(store as any, {
|
|
162
|
-
...DEFAULT_ACCESS_TRACKER_CONFIG,
|
|
163
|
-
flushIntervalMs: 0,
|
|
164
|
-
noveltyThreshold: 0.35,
|
|
165
|
-
cooldownMs: 600_000, // 10 min cooldown
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
// First call: novel enough → queued
|
|
169
|
-
tracker.recordAccess(["x"], [0.50]);
|
|
170
|
-
await tracker.flush();
|
|
171
|
-
expect(store.updates.length).toBe(1);
|
|
172
|
-
|
|
173
|
-
// Second call: same entry within cooldown → not queued
|
|
174
|
-
tracker.recordAccess(["x"], [0.50]);
|
|
175
|
-
await tracker.flush();
|
|
176
|
-
// Still only 1 update (no new flush)
|
|
177
|
-
expect(store.updates.length).toBe(1);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it("skips flush scheduling when all entries are gated out", () => {
|
|
181
|
-
const store = createMockStore([]);
|
|
182
|
-
const tracker = new AccessTracker(store as any, {
|
|
183
|
-
...DEFAULT_ACCESS_TRACKER_CONFIG,
|
|
184
|
-
flushIntervalMs: 5000,
|
|
185
|
-
noveltyThreshold: 0.35,
|
|
186
|
-
cooldownMs: 0,
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
// All scores too high (low novelty) → nothing queued
|
|
190
|
-
tracker.recordAccess(["a", "b"], [0.95, 0.90]);
|
|
191
|
-
expect(tracker.pendingCount).toBe(0);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it("accessCount increments correctly after novelty-gated flush", async () => {
|
|
195
|
-
const entries = [makeEntry("m1")];
|
|
196
|
-
const store = createMockStore(entries);
|
|
197
|
-
const tracker = new AccessTracker(store as any, {
|
|
198
|
-
...DEFAULT_ACCESS_TRACKER_CONFIG,
|
|
199
|
-
flushIntervalMs: 0,
|
|
200
|
-
noveltyThreshold: 0.35,
|
|
201
|
-
cooldownMs: 0,
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
tracker.recordAccess(["m1"], [0.40]); // novel → accepted
|
|
205
|
-
await tracker.flush();
|
|
206
|
-
|
|
207
|
-
const updated = store.data.get("m1")!;
|
|
208
|
-
const meta = JSON.parse(updated.metadata);
|
|
209
|
-
expect(meta.accessCount).toBe(1);
|
|
210
|
-
});
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
// ---------------------------------------------------------------------------
|
|
214
|
-
// destroy() cleans cooldown
|
|
215
|
-
// ---------------------------------------------------------------------------
|
|
216
|
-
|
|
217
|
-
describe("destroy cleans cooldown", () => {
|
|
218
|
-
it("clears cooldown map on destroy", () => {
|
|
219
|
-
const tracker = new AccessTracker(
|
|
220
|
-
createMockStore([]) as any,
|
|
221
|
-
{ ...DEFAULT_ACCESS_TRACKER_CONFIG, cooldownMs: 600_000 },
|
|
222
|
-
);
|
|
223
|
-
tracker.recordAccess(["a"], [0.40]);
|
|
224
|
-
tracker.destroy();
|
|
225
|
-
// After destroy, same entry should be reinforceable again
|
|
226
|
-
expect(tracker.shouldReinforce("a", 0.40)).toBe(true);
|
|
227
|
-
});
|
|
228
|
-
});
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, beforeEach } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
maybeConsolidate,
|
|
5
|
-
resetConsolidationState,
|
|
6
|
-
DEFAULT_AUTO_CONSOLIDATION_CONFIG,
|
|
7
|
-
type AutoConsolidationConfig,
|
|
8
|
-
} from "../auto-consolidation.js";
|
|
9
|
-
import type { MemoryEntry, MemorySearchResult } from "../store.js";
|
|
10
|
-
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
// Helpers
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
|
|
15
|
-
function makeEntry(id: string, text: string, overrides: Partial<MemoryEntry> = {}): MemoryEntry {
|
|
16
|
-
return {
|
|
17
|
-
id,
|
|
18
|
-
text,
|
|
19
|
-
vector: [1, 0, 0],
|
|
20
|
-
category: "events",
|
|
21
|
-
scope: "project:test",
|
|
22
|
-
importance: 0.7,
|
|
23
|
-
timestamp: Date.now(),
|
|
24
|
-
metadata: "{}",
|
|
25
|
-
...overrides,
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function createMockStore(entries: MemoryEntry[]) {
|
|
30
|
-
const data = new Map(entries.map(e => [e.id, { ...e }]));
|
|
31
|
-
return {
|
|
32
|
-
async list(scopeFilter?: string[], _category?: string, limit = 500, _offset = 0) {
|
|
33
|
-
return [...data.values()]
|
|
34
|
-
.filter(e => !scopeFilter || scopeFilter.some(s => e.scope === s))
|
|
35
|
-
.slice(0, limit);
|
|
36
|
-
},
|
|
37
|
-
async getById(id: string) {
|
|
38
|
-
return data.get(id) ?? null;
|
|
39
|
-
},
|
|
40
|
-
async vectorSearch(_vector: number[], _limit = 5, _minScore = 0.3, _scopeFilter?: string[]) {
|
|
41
|
-
return [] as MemorySearchResult[];
|
|
42
|
-
},
|
|
43
|
-
async update(id: string, updates: any, _scopeFilter?: string[]) {
|
|
44
|
-
const entry = data.get(id);
|
|
45
|
-
if (!entry) return null;
|
|
46
|
-
Object.assign(entry, updates);
|
|
47
|
-
return entry;
|
|
48
|
-
},
|
|
49
|
-
async stats(scopeFilter?: string[]) {
|
|
50
|
-
const filtered = [...data.values()].filter(
|
|
51
|
-
e => !scopeFilter || scopeFilter.some(s => e.scope === s),
|
|
52
|
-
);
|
|
53
|
-
return { total: filtered.length, byCategory: {}, byScope: {} };
|
|
54
|
-
},
|
|
55
|
-
hasFtsSupport: false,
|
|
56
|
-
} as any;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// ---------------------------------------------------------------------------
|
|
60
|
-
// Tests
|
|
61
|
-
// ---------------------------------------------------------------------------
|
|
62
|
-
|
|
63
|
-
describe("auto-consolidation", () => {
|
|
64
|
-
beforeEach(() => {
|
|
65
|
-
resetConsolidationState();
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("skips when not enough new memories", async () => {
|
|
69
|
-
const entries = Array.from({ length: 10 }, (_, i) =>
|
|
70
|
-
makeEntry(`id-${i}`, `memory ${i}`),
|
|
71
|
-
);
|
|
72
|
-
const store = createMockStore(entries);
|
|
73
|
-
|
|
74
|
-
const result = await maybeConsolidate(store, "project:test");
|
|
75
|
-
expect(result.triggered).toBe(false);
|
|
76
|
-
expect(result.reason).toBe("insufficient_new_memories");
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("skips when too soon since last run", async () => {
|
|
80
|
-
// Start with 60 entries, trigger first run, then grow to 120 so
|
|
81
|
-
// the memory-count gate passes but the time gate blocks.
|
|
82
|
-
const data: MemoryEntry[] = Array.from({ length: 60 }, (_, i) =>
|
|
83
|
-
makeEntry(`id-${i}`, `memory ${i}`),
|
|
84
|
-
);
|
|
85
|
-
const store = createMockStore(data);
|
|
86
|
-
|
|
87
|
-
const config: AutoConsolidationConfig = {
|
|
88
|
-
minNewMemories: 50,
|
|
89
|
-
minHoursSinceLastRun: 12,
|
|
90
|
-
consolidation: DEFAULT_AUTO_CONSOLIDATION_CONFIG.consolidation,
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
// First run: triggers (never run before, 60 > 50 new memories)
|
|
94
|
-
const first = await maybeConsolidate(store, "project:test", config);
|
|
95
|
-
expect(first.triggered).toBe(true);
|
|
96
|
-
|
|
97
|
-
// Grow the store so memory-count gate would pass
|
|
98
|
-
for (let i = 60; i < 120; i++) {
|
|
99
|
-
data.push(makeEntry(`id-${i}`, `memory ${i}`));
|
|
100
|
-
}
|
|
101
|
-
const bigStore = createMockStore(data);
|
|
102
|
-
|
|
103
|
-
// Second run immediately: time gate blocks (< 12 hours)
|
|
104
|
-
const second = await maybeConsolidate(bigStore, "project:test", config);
|
|
105
|
-
expect(second.triggered).toBe(false);
|
|
106
|
-
expect(second.reason).toBe("too_soon");
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it("triggers when both conditions met", async () => {
|
|
110
|
-
const entries = Array.from({ length: 60 }, (_, i) =>
|
|
111
|
-
makeEntry(`id-${i}`, `memory ${i}`),
|
|
112
|
-
);
|
|
113
|
-
const store = createMockStore(entries);
|
|
114
|
-
|
|
115
|
-
const config: AutoConsolidationConfig = {
|
|
116
|
-
minNewMemories: 50,
|
|
117
|
-
minHoursSinceLastRun: 0, // disable time gate for test
|
|
118
|
-
consolidation: {
|
|
119
|
-
clusterThreshold: 0.82,
|
|
120
|
-
mergeThreshold: 0.92,
|
|
121
|
-
maxEntriesPerRun: 500,
|
|
122
|
-
},
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
const result = await maybeConsolidate(store, "project:test", config);
|
|
126
|
-
expect(result.triggered).toBe(true);
|
|
127
|
-
expect(result.consolidation).toBeDefined();
|
|
128
|
-
expect(result.consolidation!.scope).toBe("project:test");
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it("returns consolidation result when triggered", async () => {
|
|
132
|
-
const entries = Array.from({ length: 60 }, (_, i) =>
|
|
133
|
-
makeEntry(`id-${i}`, `memory ${i}`),
|
|
134
|
-
);
|
|
135
|
-
const store = createMockStore(entries);
|
|
136
|
-
|
|
137
|
-
const config: AutoConsolidationConfig = {
|
|
138
|
-
minNewMemories: 50,
|
|
139
|
-
minHoursSinceLastRun: 0,
|
|
140
|
-
consolidation: {
|
|
141
|
-
clusterThreshold: 0.82,
|
|
142
|
-
mergeThreshold: 0.92,
|
|
143
|
-
maxEntriesPerRun: 500,
|
|
144
|
-
},
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
const result = await maybeConsolidate(store, "project:test", config);
|
|
148
|
-
expect(result.triggered).toBe(true);
|
|
149
|
-
expect(result.consolidation!.originalCount).toBe(60);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it("respects custom thresholds", async () => {
|
|
153
|
-
const entries = Array.from({ length: 200 }, (_, i) =>
|
|
154
|
-
makeEntry(`id-${i}`, `memory ${i}`),
|
|
155
|
-
);
|
|
156
|
-
const store = createMockStore(entries);
|
|
157
|
-
|
|
158
|
-
const config: AutoConsolidationConfig = {
|
|
159
|
-
minNewMemories: 300, // higher than available
|
|
160
|
-
minHoursSinceLastRun: 0,
|
|
161
|
-
consolidation: DEFAULT_AUTO_CONSOLIDATION_CONFIG.consolidation,
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
const result = await maybeConsolidate(store, "project:test", config);
|
|
165
|
-
expect(result.triggered).toBe(false);
|
|
166
|
-
expect(result.reason).toBe("insufficient_new_memories");
|
|
167
|
-
});
|
|
168
|
-
});
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import { runAutoRecall } from "../auto-recall.js";
|
|
4
|
-
import type { RetrievalContext, RetrievalResult } from "../retriever.js";
|
|
5
|
-
import type { SessionCheckpointRecord } from "../session-schema.js";
|
|
6
|
-
|
|
7
|
-
function buildResult(id: string, category: "profile" | "preferences" | "entities" | "patterns" | "cases" | "events", text: string, scope = "project:test"): RetrievalResult {
|
|
8
|
-
return {
|
|
9
|
-
entry: {
|
|
10
|
-
id,
|
|
11
|
-
text,
|
|
12
|
-
vector: [],
|
|
13
|
-
category,
|
|
14
|
-
scope,
|
|
15
|
-
importance: 0.8,
|
|
16
|
-
timestamp: Date.parse("2026-03-17T00:00:00.000Z"),
|
|
17
|
-
metadata: "{}",
|
|
18
|
-
},
|
|
19
|
-
score: 0.9,
|
|
20
|
-
sources: {
|
|
21
|
-
fused: { score: 0.9 },
|
|
22
|
-
},
|
|
23
|
-
};
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
describe("runAutoRecall", () => {
|
|
27
|
-
it("reuses an explicit scope for focused recall after composing resume context", async () => {
|
|
28
|
-
const calls: RetrievalContext[] = [];
|
|
29
|
-
const retriever = {
|
|
30
|
-
async retrieve(context: RetrievalContext): Promise<RetrievalResult[]> {
|
|
31
|
-
calls.push(context);
|
|
32
|
-
if (context.category === "profile") {
|
|
33
|
-
return [buildResult("profile-1", "profile", "Profile: RecallNest maintainer.", "project:alpha")];
|
|
34
|
-
}
|
|
35
|
-
if (!context.category) {
|
|
36
|
-
return [buildResult("event-1", "events", "RecallNest uses Weibull decay.", "project:alpha")];
|
|
37
|
-
}
|
|
38
|
-
return [];
|
|
39
|
-
},
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
const response = await runAutoRecall({
|
|
43
|
-
retriever,
|
|
44
|
-
checkpointStore: { async getLatest() { return null; } },
|
|
45
|
-
listPins: () => [],
|
|
46
|
-
}, {
|
|
47
|
-
message: "那个衰减曲线用的什么公式来着",
|
|
48
|
-
scope: "project:alpha",
|
|
49
|
-
limit: 5,
|
|
50
|
-
profile: "default",
|
|
51
|
-
operation: "test:auto-recall",
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
expect(response.mode).toBe("resume+search");
|
|
55
|
-
expect(response.resolvedScope).toBe("project:alpha");
|
|
56
|
-
expect(response.results[0]?.entry.id).toBe("event-1");
|
|
57
|
-
const searchCall = calls.find((call) => !call.category);
|
|
58
|
-
expect(searchCall?.scopeFilter).toEqual(["project:alpha"]);
|
|
59
|
-
expect(searchCall?.source).toBe("auto-recall");
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("uses the checkpoint-resolved scope when the request only carries sessionId", async () => {
|
|
63
|
-
const calls: RetrievalContext[] = [];
|
|
64
|
-
const checkpoint: SessionCheckpointRecord = {
|
|
65
|
-
checkpointId: "checkpoint-1",
|
|
66
|
-
sessionId: "session-123",
|
|
67
|
-
resolvedScope: "project:recallnest",
|
|
68
|
-
summary: "Continue proactive recall work.",
|
|
69
|
-
task: "Implement auto recall route",
|
|
70
|
-
decisions: [],
|
|
71
|
-
openLoops: [],
|
|
72
|
-
nextActions: [],
|
|
73
|
-
entities: [],
|
|
74
|
-
files: [],
|
|
75
|
-
updatedAt: "2026-03-17T10:00:00.000Z",
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const retriever = {
|
|
79
|
-
async retrieve(context: RetrievalContext): Promise<RetrievalResult[]> {
|
|
80
|
-
calls.push(context);
|
|
81
|
-
if (!context.category) {
|
|
82
|
-
return [buildResult("event-1", "events", "Add a true auto recall route.", "project:recallnest")];
|
|
83
|
-
}
|
|
84
|
-
return [];
|
|
85
|
-
},
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const response = await runAutoRecall({
|
|
89
|
-
retriever,
|
|
90
|
-
checkpointStore: {
|
|
91
|
-
async getLatest(query?: { sessionId?: string }) {
|
|
92
|
-
return query?.sessionId === "session-123" ? checkpoint : null;
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
|
-
listPins: () => [],
|
|
96
|
-
}, {
|
|
97
|
-
message: "继续做主动 recall",
|
|
98
|
-
sessionId: "session-123",
|
|
99
|
-
operation: "test:auto-recall",
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
expect(response.resolvedScope).toBe("project:recallnest");
|
|
103
|
-
expect(response.resume.latestCheckpoint?.resolvedScope).toBe("project:recallnest");
|
|
104
|
-
const searchCall = calls.find((call) => !call.category);
|
|
105
|
-
expect(searchCall?.scopeFilter).toEqual(["project:recallnest"]);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it("falls back to resume-only mode when no scope can be inferred for focused search", async () => {
|
|
109
|
-
const calls: RetrievalContext[] = [];
|
|
110
|
-
const retriever = {
|
|
111
|
-
async retrieve(context: RetrievalContext): Promise<RetrievalResult[]> {
|
|
112
|
-
calls.push(context);
|
|
113
|
-
return [];
|
|
114
|
-
},
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
const response = await runAutoRecall({
|
|
118
|
-
retriever,
|
|
119
|
-
checkpointStore: { async getLatest() { return null; } },
|
|
120
|
-
listPins: () => [],
|
|
121
|
-
}, {
|
|
122
|
-
message: "what do you remember about this",
|
|
123
|
-
operation: "test:auto-recall",
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
expect(response.mode).toBe("resume-only");
|
|
127
|
-
expect(response.results).toHaveLength(0);
|
|
128
|
-
expect(response.searchSkippedReason).toContain("No explicit or inferred scope");
|
|
129
|
-
expect(calls.some((call) => !call.category)).toBe(false);
|
|
130
|
-
});
|
|
131
|
-
});
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "bun:test";
|
|
2
|
-
import { batchInternalDedup } from "../ingest.js";
|
|
3
|
-
|
|
4
|
-
describe("batchInternalDedup", () => {
|
|
5
|
-
it("keeps all entries when vectors are dissimilar", () => {
|
|
6
|
-
const vectors = [
|
|
7
|
-
[1, 0, 0],
|
|
8
|
-
[0, 1, 0],
|
|
9
|
-
[0, 0, 1],
|
|
10
|
-
];
|
|
11
|
-
const importances = [0.8, 0.7, 0.6];
|
|
12
|
-
const kept = batchInternalDedup(vectors, importances);
|
|
13
|
-
expect(kept).toEqual([0, 1, 2]);
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it("drops lower-importance duplicate when vectors are near-identical", () => {
|
|
17
|
-
const vectors = [
|
|
18
|
-
[1, 0, 0], // importance 0.6
|
|
19
|
-
[1, 0.01, 0], // importance 0.9 — nearly identical to [0]
|
|
20
|
-
[0, 1, 0], // importance 0.7 — dissimilar
|
|
21
|
-
];
|
|
22
|
-
const importances = [0.6, 0.9, 0.7];
|
|
23
|
-
const kept = batchInternalDedup(vectors, importances);
|
|
24
|
-
// [1] has higher importance and is near-identical to [0], so [0] gets dropped
|
|
25
|
-
expect(kept).toContain(1);
|
|
26
|
-
expect(kept).toContain(2);
|
|
27
|
-
expect(kept).not.toContain(0);
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
it("returns single-item batch unchanged", () => {
|
|
31
|
-
expect(batchInternalDedup([[1, 0, 0]], [0.5])).toEqual([0]);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("returns empty for empty input", () => {
|
|
35
|
-
expect(batchInternalDedup([], [])).toEqual([]);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it("preserves original order in output", () => {
|
|
39
|
-
const vectors = [
|
|
40
|
-
[0, 1, 0], // idx 0, imp 0.9
|
|
41
|
-
[1, 0, 0], // idx 1, imp 0.5
|
|
42
|
-
[0, 0, 1], // idx 2, imp 0.7
|
|
43
|
-
];
|
|
44
|
-
const importances = [0.9, 0.5, 0.7];
|
|
45
|
-
const kept = batchInternalDedup(vectors, importances);
|
|
46
|
-
// All dissimilar, all kept, in original order
|
|
47
|
-
expect(kept).toEqual([0, 1, 2]);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it("respects custom threshold", () => {
|
|
51
|
-
const vectors = [
|
|
52
|
-
[1, 0, 0],
|
|
53
|
-
[0.95, 0.31, 0], // cosine ~0.95 with [0]
|
|
54
|
-
];
|
|
55
|
-
const importances = [0.8, 0.7];
|
|
56
|
-
|
|
57
|
-
// Default threshold 0.85 → should dedup
|
|
58
|
-
const strictKept = batchInternalDedup(vectors, importances, 0.85);
|
|
59
|
-
expect(strictKept).toHaveLength(1);
|
|
60
|
-
|
|
61
|
-
// Loose threshold 0.99 → should keep both
|
|
62
|
-
const looseKept = batchInternalDedup(vectors, importances, 0.99);
|
|
63
|
-
expect(looseKept).toHaveLength(2);
|
|
64
|
-
});
|
|
65
|
-
});
|