recallnest 1.4.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/.claude-plugin/CLAUDE.md +29 -0
- package/.claude-plugin/marketplace.json +18 -0
- package/.env.example +3 -0
- package/.github/workflows/ci.yml +28 -0
- package/.mcp.json +9 -0
- package/CHANGELOG.md +51 -0
- package/CLAUDE.md +42 -0
- package/DEMO.md +94 -0
- package/EVAL.md +89 -0
- package/FAILURES.md +75 -0
- package/LICENSE +21 -0
- package/OPERATIONS.md +59 -0
- package/README.md +427 -0
- package/README_CN.md +425 -0
- package/ROADMAP.md +198 -0
- package/assets/recallnest-workbench.png +0 -0
- package/assets/ui/app.js +807 -0
- package/assets/ui/index.html +154 -0
- package/assets/ui/styles.css +605 -0
- package/assets/wechat-qr.jpg +0 -0
- package/config.json.example +38 -0
- package/daily-recall.sh +87 -0
- package/daily-reflections/2026-03-05.md +15 -0
- package/daily-reflections/2026-03-06.md +15 -0
- package/daily-reflections/2026-03-07.md +7 -0
- package/daily-reflections/2026-03-08.md +34 -0
- package/daily-reflections/2026-03-09.md +35 -0
- package/daily-reflections/2026-03-10.md +36 -0
- package/daily-reflections/2026-03-11.md +35 -0
- package/daily-reflections/2026-03-12.md +36 -0
- package/daily-reflections/2026-03-13.md +35 -0
- package/daily-reflections/2026-03-14.md +35 -0
- package/daily-reflections/2026-03-15.md +35 -0
- package/daily-reflections/2026-03-16.md +41 -0
- package/daily-reflections/2026-03-17.md +10 -0
- package/daily-reflections/2026-03-18.md +10 -0
- package/daily-reflections/2026-03-19.md +10 -0
- package/daily-reflections/2026-03-20.md +10 -0
- package/daily-reflections/2026-03-21.md +10 -0
- package/daily-reflections/2026-03-22.md +10 -0
- package/daily-reflections/2026-03-23.md +10 -0
- package/daily-reflections/2026-03-24.md +10 -0
- package/daily-reflections/2026-03-25.md +32 -0
- package/daily-reflections/2026-03-26.md +37 -0
- package/daily-reflections/2026-03-27.md +35 -0
- package/daily-reflections/2026-03-28.md +35 -0
- package/daily-reflections/2026-03-29.md +35 -0
- package/daily-reflections/2026-03-30.md +36 -0
- package/daily-reflections/2026-03-31.md +35 -0
- package/daily-reflections/2026-04-01.md +35 -0
- package/docs/api-reference.md +1291 -0
- package/docs/architecture.md +168 -0
- package/docs/memory-boundary-contract.md +226 -0
- package/docs/memory-categories.md +119 -0
- package/docs/pipeline-audit-ultramemory.md +177 -0
- package/docs/plans/2026-03-08-query-expansion.md +270 -0
- package/docs/plans/2026-03-16-continuity-layer.md +196 -0
- package/docs/plans/mcp-tiering-plan.md +126 -0
- package/docs/research/recall-quality-audit-2026-03-22.md +153 -0
- package/docs/self-evolution.md +198 -0
- package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +782 -0
- package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +77 -0
- package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +95 -0
- package/eval/cases.json +85 -0
- package/eval/continuity/case-seeds.json +90 -0
- package/eval/continuity/cases.json +302 -0
- package/eval/continuity/memory-seeds.json +26 -0
- package/eval/continuity/pattern-seeds.json +100 -0
- package/eval/continuity/results-2026-03-16.md +117 -0
- package/eval/continuity/results-2026-03-19.md +453 -0
- package/eval/continuity/runbook.md +110 -0
- package/eval/continuity/seed-cases.md +54 -0
- package/eval/reports/2026-03-06-abstract-query-baseline.md +96 -0
- package/eval/reports/2026-03-06-aws-ssh-baseline.md +82 -0
- package/eval/reports/2026-03-06-baseline.md +82 -0
- package/eval/reports/query-expansion.md +138 -0
- package/integrations/README.md +57 -0
- package/integrations/claude-code/README.md +133 -0
- package/integrations/claude-code/claude-md-snippet.md +31 -0
- package/integrations/claude-code/setup.sh +66 -0
- package/integrations/claude-code/smoke-continuity.sh +452 -0
- package/integrations/codex/README.md +49 -0
- package/integrations/codex/agents-md-snippet.md +31 -0
- package/integrations/codex/setup.sh +58 -0
- package/integrations/examples/claude-agent-sdk/memory-agent.ts +182 -0
- package/integrations/examples/langchain/memory-chain.py +111 -0
- package/integrations/examples/openai-agents-sdk/memory-agent.py +109 -0
- package/integrations/gemini-cli/README.md +57 -0
- package/integrations/gemini-cli/gemini-md-snippet.md +31 -0
- package/integrations/gemini-cli/setup.sh +64 -0
- package/integrations/shared/install-managed-block.sh +51 -0
- package/lm +19 -0
- package/package.json +50 -0
- package/plan-v1.2-distributable.md +105 -0
- package/scripts/archive-distilled-junk.ts +116 -0
- package/scripts/backfill-l0l1l2.ts +98 -0
- package/scripts/blind-scan.ts +588 -0
- package/scripts/cleanup-step1-exact-dups.ts +117 -0
- package/scripts/cleanup-step2-template-dups.ts +113 -0
- package/scripts/cleanup-step3-stale-content.ts +149 -0
- package/scripts/cleanup-step4-orphan-scopes.ts +131 -0
- package/scripts/delete-distilled-junk.ts +88 -0
- package/scripts/distill-facts.ts +305 -0
- package/scripts/health-check.ts +343 -0
- package/scripts/incremental-ingest.sh +71 -0
- package/scripts/mlp-health-check.ts +382 -0
- package/scripts/pull-claude-desktop.ts +515 -0
- package/scripts/re-ingest-cc.sh +66 -0
- package/scripts/rebalance-memory-health.ts +181 -0
- package/scripts/run-desktop-ingest.sh +11 -0
- package/scripts/start-server.sh +12 -0
- package/scripts/viz-dashboard.html +423 -0
- package/scripts/viz-export.ts +703 -0
- package/src/__tests__/access-tracker-novelty.test.ts +228 -0
- package/src/__tests__/auto-consolidation.test.ts +168 -0
- package/src/__tests__/auto-recall.test.ts +131 -0
- package/src/__tests__/batch-dedup.test.ts +65 -0
- package/src/__tests__/capture-engine.test.ts +1142 -0
- package/src/__tests__/capture-heuristic.test.ts +171 -0
- package/src/__tests__/cascade-forget.test.ts +210 -0
- package/src/__tests__/category-min-scores.test.ts +33 -0
- package/src/__tests__/confidence-tracker.test.ts +196 -0
- package/src/__tests__/conflict-advisor.test.ts +206 -0
- package/src/__tests__/conflict-escalation.test.ts +149 -0
- package/src/__tests__/conflict-lifecycle.test.ts +83 -0
- package/src/__tests__/conflict-output.test.ts +85 -0
- package/src/__tests__/conflict-store.test.ts +374 -0
- package/src/__tests__/consolidation-engine.test.ts +181 -0
- package/src/__tests__/context-composer-pins.test.ts +64 -0
- package/src/__tests__/context-composer-task-selection.test.ts +24 -0
- package/src/__tests__/context-renderer.test.ts +91 -0
- package/src/__tests__/continuity-eval.test.ts +275 -0
- package/src/__tests__/continuity-schema.test.ts +97 -0
- package/src/__tests__/core-recall-eval.test.ts +235 -0
- package/src/__tests__/core-summary.test.ts +109 -0
- package/src/__tests__/dedup-actions.test.ts +58 -0
- package/src/__tests__/deterministic-id.test.ts +39 -0
- package/src/__tests__/doctor.test.ts +303 -0
- package/src/__tests__/drill-down.test.ts +108 -0
- package/src/__tests__/embedder.test.ts +122 -0
- package/src/__tests__/entity-resolver.test.ts +58 -0
- package/src/__tests__/event-segmenter.test.ts +153 -0
- package/src/__tests__/hotness-blend.test.ts +76 -0
- package/src/__tests__/ingest.test.ts +360 -0
- package/src/__tests__/kg-extractor.test.ts +184 -0
- package/src/__tests__/kg-store.test.ts +198 -0
- package/src/__tests__/llm-consolidation.test.ts +226 -0
- package/src/__tests__/memory-boundaries.test.ts +237 -0
- package/src/__tests__/memory-health-rebalance.test.ts +144 -0
- package/src/__tests__/memory-output.test.ts +215 -0
- package/src/__tests__/multi-vector.test.ts +242 -0
- package/src/__tests__/ppr-traversal.test.ts +256 -0
- package/src/__tests__/preference-guard-smoke.test.ts +211 -0
- package/src/__tests__/preference-matcher.test.ts +230 -0
- package/src/__tests__/preference-slots.test.ts +106 -0
- package/src/__tests__/prospective-memory.test.ts +283 -0
- package/src/__tests__/query-entity-detector.test.ts +171 -0
- package/src/__tests__/query-expander.test.ts +34 -0
- package/src/__tests__/recall-mode.test.ts +64 -0
- package/src/__tests__/result-synthesizer.test.ts +198 -0
- package/src/__tests__/resume-context.test.ts +4743 -0
- package/src/__tests__/retrieval-trace.test.ts +213 -0
- package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
- package/src/__tests__/retroactive-boost.test.ts +223 -0
- package/src/__tests__/scope-inventory.test.ts +200 -0
- package/src/__tests__/scope-policy.test.ts +69 -0
- package/src/__tests__/session-checkpoint.test.ts +259 -0
- package/src/__tests__/session-gc.test.ts +215 -0
- package/src/__tests__/store-scope-audit.test.ts +81 -0
- package/src/__tests__/temporal-parser.test.ts +241 -0
- package/src/__tests__/term-registry.test.ts +121 -0
- package/src/__tests__/tool-output-compressor.test.ts +133 -0
- package/src/__tests__/version-manager.test.ts +213 -0
- package/src/__tests__/workflow-observation.test.ts +400 -0
- package/src/access-tracker.ts +316 -0
- package/src/adaptive-retrieval.ts +64 -0
- package/src/api-server.ts +667 -0
- package/src/asset-sync.ts +79 -0
- package/src/auto-consolidation.ts +133 -0
- package/src/auto-gc.ts +124 -0
- package/src/auto-recall.ts +95 -0
- package/src/capture-engine.ts +931 -0
- package/src/capture-heuristic.ts +177 -0
- package/src/cascade-forget.ts +135 -0
- package/src/chunker.ts +253 -0
- package/src/cli.ts +1917 -0
- package/src/confidence-tracker.ts +166 -0
- package/src/conflict-advisor.ts +433 -0
- package/src/conflict-engine.ts +181 -0
- package/src/conflict-escalation.ts +138 -0
- package/src/conflict-lifecycle.ts +67 -0
- package/src/conflict-output.ts +218 -0
- package/src/conflict-schema.ts +130 -0
- package/src/conflict-store.ts +127 -0
- package/src/consolidation-engine.ts +256 -0
- package/src/context-composer-pins.ts +183 -0
- package/src/context-composer-queries.ts +121 -0
- package/src/context-composer-scope.ts +56 -0
- package/src/context-composer-stable-selection.ts +200 -0
- package/src/context-composer-stable.ts +217 -0
- package/src/context-composer-task-fallbacks.ts +36 -0
- package/src/context-composer-task-ranking.ts +197 -0
- package/src/context-composer-task-results.ts +137 -0
- package/src/context-composer-task-selection.ts +342 -0
- package/src/context-composer-text.ts +57 -0
- package/src/context-composer.ts +300 -0
- package/src/context-renderer.ts +179 -0
- package/src/decay-engine.ts +189 -0
- package/src/doctor.ts +543 -0
- package/src/embedder.ts +557 -0
- package/src/entity-resolver.ts +131 -0
- package/src/eval.ts +632 -0
- package/src/event-segmenter.ts +216 -0
- package/src/ingest.ts +1822 -0
- package/src/kg-extractor.ts +217 -0
- package/src/kg-store.ts +353 -0
- package/src/legacy-scope-review.ts +53 -0
- package/src/llm-client.ts +629 -0
- package/src/llm-consolidation.ts +168 -0
- package/src/mcp-server.ts +1337 -0
- package/src/memory-assets.ts +475 -0
- package/src/memory-boundaries.ts +354 -0
- package/src/memory-health-rebalance.ts +220 -0
- package/src/memory-output.ts +474 -0
- package/src/memory-schema.ts +184 -0
- package/src/multi-vector.ts +234 -0
- package/src/noise-filter.ts +155 -0
- package/src/ppr-traversal.ts +279 -0
- package/src/preference-matcher.ts +174 -0
- package/src/preference-slots.ts +259 -0
- package/src/prospective-memory.ts +198 -0
- package/src/query-entity-detector.ts +207 -0
- package/src/query-expander.ts +98 -0
- package/src/result-synthesizer.ts +114 -0
- package/src/retrieval-profiles.ts +101 -0
- package/src/retrieval-trace.ts +94 -0
- package/src/retriever.ts +1338 -0
- package/src/retroactive-boost.ts +154 -0
- package/src/rif.ts +73 -0
- package/src/runtime-config.ts +172 -0
- package/src/schema-utils.ts +56 -0
- package/src/scope-inventory.ts +310 -0
- package/src/scope-policy.ts +142 -0
- package/src/session-engine.ts +168 -0
- package/src/session-output.ts +96 -0
- package/src/session-schema.ts +60 -0
- package/src/session-store.ts +293 -0
- package/src/stderr-log.ts +16 -0
- package/src/store.ts +903 -0
- package/src/temporal-parser.ts +318 -0
- package/src/term-registry.ts +791 -0
- package/src/tool-output-compressor.ts +115 -0
- package/src/tracker.ts +57 -0
- package/src/ui-server.ts +430 -0
- package/src/version-manager.ts +172 -0
- package/src/workflow-observation-engine.ts +316 -0
- package/src/workflow-observation-managed.ts +83 -0
- package/src/workflow-observation-output.ts +86 -0
- package/src/workflow-observation-schema.ts +87 -0
- package/src/workflow-observation-scope-review.ts +409 -0
- package/src/workflow-observation-store.ts +69 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retroactive Association Boost (STC — Synaptic Tagging & Capture)
|
|
3
|
+
*
|
|
4
|
+
* Agent-First reasoning: when a new high-importance memory is stored
|
|
5
|
+
* (e.g. "I'm moving to Singapore"), older weak fragments that are
|
|
6
|
+
* semantically related (e.g. "Singapore weather notes") become more
|
|
7
|
+
* valuable. Without this, those old fragments stay buried at low
|
|
8
|
+
* importance and never surface in relevant retrievals.
|
|
9
|
+
*
|
|
10
|
+
* Brain-science label: STC hypothesis — a strong event "captures"
|
|
11
|
+
* weakly tagged memories and promotes them. We keep the name for
|
|
12
|
+
* branding; the real mechanism is a vector similarity search +
|
|
13
|
+
* importance bump on related old entries.
|
|
14
|
+
*
|
|
15
|
+
* Rules:
|
|
16
|
+
* - Only triggers on high-importance writes (≥ threshold)
|
|
17
|
+
* - Only boosts entries that are currently low-importance (below cap)
|
|
18
|
+
* - Boost is additive with a ceiling — never exceeds maxBoostedImportance
|
|
19
|
+
* - Writes metadata breadcrumb for auditability
|
|
20
|
+
* - No LLM calls — pure vector search + arithmetic
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { MemoryStore, MemoryEntry } from "./store.js";
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Config
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
export interface RetroactiveBoostConfig {
|
|
30
|
+
/** Minimum importance of the NEW memory to trigger a boost sweep (default: 0.8) */
|
|
31
|
+
triggerImportanceMin: number;
|
|
32
|
+
/** Only boost entries whose current importance is below this (default: 0.6) */
|
|
33
|
+
boostCandidateMaxImportance: number;
|
|
34
|
+
/** Additive importance bump (default: 0.15) */
|
|
35
|
+
boostAmount: number;
|
|
36
|
+
/** Ceiling: boosted importance never exceeds this (default: 0.75) */
|
|
37
|
+
maxBoostedImportance: number;
|
|
38
|
+
/** Minimum vector similarity to consider "related" (default: 0.72) */
|
|
39
|
+
similarityThreshold: number;
|
|
40
|
+
/** Maximum entries to boost per trigger (default: 5) */
|
|
41
|
+
maxBoostPerTrigger: number;
|
|
42
|
+
/** Minimum age in days for a candidate (avoid boosting very recent entries) (default: 1) */
|
|
43
|
+
minAgeDays: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const DEFAULT_RETROACTIVE_BOOST_CONFIG: RetroactiveBoostConfig = {
|
|
47
|
+
triggerImportanceMin: 0.8,
|
|
48
|
+
boostCandidateMaxImportance: 0.6,
|
|
49
|
+
boostAmount: 0.15,
|
|
50
|
+
maxBoostedImportance: 0.75,
|
|
51
|
+
similarityThreshold: 0.72,
|
|
52
|
+
maxBoostPerTrigger: 5,
|
|
53
|
+
minAgeDays: 1,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Core
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
export interface BoostResult {
|
|
61
|
+
triggered: boolean;
|
|
62
|
+
reason?: string;
|
|
63
|
+
boostedCount: number;
|
|
64
|
+
boostedIds: string[];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* After a high-importance memory is stored, search for related older
|
|
69
|
+
* low-importance entries and boost them.
|
|
70
|
+
*
|
|
71
|
+
* Call this right after store.add() for the new entry.
|
|
72
|
+
*/
|
|
73
|
+
export async function retroactiveBoost(
|
|
74
|
+
store: MemoryStore,
|
|
75
|
+
newEntry: Pick<MemoryEntry, "id" | "text" | "vector" | "importance" | "scope" | "timestamp">,
|
|
76
|
+
config: RetroactiveBoostConfig = DEFAULT_RETROACTIVE_BOOST_CONFIG,
|
|
77
|
+
): Promise<BoostResult> {
|
|
78
|
+
// Gate: only trigger for high-importance new memories
|
|
79
|
+
if (newEntry.importance < config.triggerImportanceMin) {
|
|
80
|
+
return { triggered: false, reason: "below_importance_threshold", boostedCount: 0, boostedIds: [] };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!newEntry.vector?.length) {
|
|
84
|
+
return { triggered: false, reason: "no_vector", boostedCount: 0, boostedIds: [] };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Find related entries in the same scope
|
|
88
|
+
const candidates = await store.vectorSearch(
|
|
89
|
+
newEntry.vector,
|
|
90
|
+
config.maxBoostPerTrigger * 3, // fetch more, filter down
|
|
91
|
+
config.similarityThreshold,
|
|
92
|
+
[newEntry.scope],
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const now = Date.now();
|
|
96
|
+
const minAgeMs = config.minAgeDays * 86_400_000;
|
|
97
|
+
const boostedIds: string[] = [];
|
|
98
|
+
|
|
99
|
+
for (const candidate of candidates) {
|
|
100
|
+
if (boostedIds.length >= config.maxBoostPerTrigger) break;
|
|
101
|
+
|
|
102
|
+
const entry = candidate.entry;
|
|
103
|
+
|
|
104
|
+
// Skip self
|
|
105
|
+
if (entry.id === newEntry.id) continue;
|
|
106
|
+
|
|
107
|
+
// Skip entries that are already high importance
|
|
108
|
+
if (entry.importance >= config.boostCandidateMaxImportance) continue;
|
|
109
|
+
|
|
110
|
+
// Skip very recent entries (they'll get their own natural scoring)
|
|
111
|
+
const ageDays = (now - entry.timestamp) / 86_400_000;
|
|
112
|
+
if (ageDays < config.minAgeDays) continue;
|
|
113
|
+
|
|
114
|
+
// Compute boost: additive, capped
|
|
115
|
+
const newImportance = Math.min(
|
|
116
|
+
entry.importance + config.boostAmount,
|
|
117
|
+
config.maxBoostedImportance,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// Skip if no actual change
|
|
121
|
+
if (newImportance <= entry.importance) continue;
|
|
122
|
+
|
|
123
|
+
// Update metadata with audit trail
|
|
124
|
+
let meta: Record<string, any> = {};
|
|
125
|
+
try { meta = JSON.parse(entry.metadata || "{}"); } catch { /* skip */ }
|
|
126
|
+
|
|
127
|
+
if (!Array.isArray(meta.stc_boosts)) meta.stc_boosts = [];
|
|
128
|
+
meta.stc_boosts.push({
|
|
129
|
+
triggeredBy: newEntry.id.slice(0, 8),
|
|
130
|
+
from: entry.importance,
|
|
131
|
+
to: newImportance,
|
|
132
|
+
similarity: candidate.score,
|
|
133
|
+
date: new Date().toISOString().slice(0, 10),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Promote tier if appropriate
|
|
137
|
+
if (meta.tier === "peripheral" && newImportance >= 0.5) {
|
|
138
|
+
meta.tier = "working";
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
await store.update(entry.id, {
|
|
142
|
+
importance: newImportance,
|
|
143
|
+
metadata: JSON.stringify(meta),
|
|
144
|
+
}, [newEntry.scope]);
|
|
145
|
+
|
|
146
|
+
boostedIds.push(entry.id);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
triggered: true,
|
|
151
|
+
boostedCount: boostedIds.length,
|
|
152
|
+
boostedIds,
|
|
153
|
+
};
|
|
154
|
+
}
|
package/src/rif.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retrieval Interference Filter (RIF)
|
|
3
|
+
*
|
|
4
|
+
* Brain-inspired: retrieval-induced forgetting — retrieving A actively
|
|
5
|
+
* suppresses similar-but-not-selected B, improving future signal-to-noise.
|
|
6
|
+
*
|
|
7
|
+
* In practice: after scoring, demote near-duplicate results from the same
|
|
8
|
+
* topic that are significantly weaker than a higher-ranked result.
|
|
9
|
+
* They're moved to the end (not removed) as fallback.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { RetrievalResult } from "./retriever.js";
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Core
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Filter interference: demote near-duplicate results that are significantly
|
|
20
|
+
* weaker than a higher-ranked similar result.
|
|
21
|
+
*
|
|
22
|
+
* @param results - Scored results sorted by score descending
|
|
23
|
+
* @param similarityThreshold - Cosine similarity above which two results are "near-duplicate" (default: 0.85)
|
|
24
|
+
* @param scoreRatio - A result scoring below this ratio of its near-duplicate is demoted (default: 0.80)
|
|
25
|
+
*/
|
|
26
|
+
export function filterInterference(
|
|
27
|
+
results: RetrievalResult[],
|
|
28
|
+
similarityThreshold = 0.85,
|
|
29
|
+
scoreRatio = 0.80,
|
|
30
|
+
): RetrievalResult[] {
|
|
31
|
+
if (results.length <= 2) return results;
|
|
32
|
+
|
|
33
|
+
const kept: RetrievalResult[] = [];
|
|
34
|
+
const demoted: RetrievalResult[] = [];
|
|
35
|
+
|
|
36
|
+
for (const candidate of results) {
|
|
37
|
+
// Check if this candidate is a weak near-duplicate of any kept result
|
|
38
|
+
let isDemoted = false;
|
|
39
|
+
|
|
40
|
+
for (const selected of kept) {
|
|
41
|
+
const sim = cosineSimilarity(candidate.entry.vector, selected.entry.vector);
|
|
42
|
+
if (sim > similarityThreshold && candidate.score < selected.score * scoreRatio) {
|
|
43
|
+
isDemoted = true;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (isDemoted) {
|
|
49
|
+
demoted.push(candidate);
|
|
50
|
+
} else {
|
|
51
|
+
kept.push(candidate);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Demoted results go to the end as fallback
|
|
56
|
+
return [...kept, ...demoted];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Helpers
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
function cosineSimilarity(a: number[] | Float32Array, b: number[] | Float32Array): number {
|
|
64
|
+
if (!a || !b || a.length !== b.length || a.length === 0) return 0;
|
|
65
|
+
let dot = 0, normA = 0, normB = 0;
|
|
66
|
+
for (let i = 0; i < a.length; i++) {
|
|
67
|
+
dot += a[i] * b[i];
|
|
68
|
+
normA += a[i] * a[i];
|
|
69
|
+
normB += b[i] * b[i];
|
|
70
|
+
}
|
|
71
|
+
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
72
|
+
return denom > 0 ? dot / denom : 0;
|
|
73
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { resolve, join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
|
|
5
|
+
import { MemoryStore, validateStoragePath } from "./store.js";
|
|
6
|
+
import { createEmbedder, getVectorDimensions, type EmbeddingConfig } from "./embedder.js";
|
|
7
|
+
import { createRetriever, type RetrievalConfig, DEFAULT_RETRIEVAL_CONFIG } from "./retriever.js";
|
|
8
|
+
import { applyRetrievalProfile } from "./retrieval-profiles.js";
|
|
9
|
+
import { AccessTracker } from "./access-tracker.js";
|
|
10
|
+
import { createLLMClient, type LLMClient, type LLMConfig } from "./llm-client.js";
|
|
11
|
+
import { logInfo } from "./stderr-log.js";
|
|
12
|
+
|
|
13
|
+
export type RecallMode = "full" | "summary" | "off";
|
|
14
|
+
|
|
15
|
+
export interface LocalMemoryConfig {
|
|
16
|
+
dbPath: string;
|
|
17
|
+
recallMode?: RecallMode;
|
|
18
|
+
embedding: {
|
|
19
|
+
provider: string;
|
|
20
|
+
apiKey: string;
|
|
21
|
+
model: string;
|
|
22
|
+
baseURL?: string;
|
|
23
|
+
dimensions?: number;
|
|
24
|
+
taskQuery?: string;
|
|
25
|
+
taskPassage?: string;
|
|
26
|
+
};
|
|
27
|
+
llm?: {
|
|
28
|
+
apiKey: string;
|
|
29
|
+
model: string;
|
|
30
|
+
baseURL: string;
|
|
31
|
+
};
|
|
32
|
+
sources: Record<string, { path: string; glob: string; description: string }>;
|
|
33
|
+
retrieval?: Partial<RetrievalConfig>;
|
|
34
|
+
/**
|
|
35
|
+
* Default depth for auto-recall injection.
|
|
36
|
+
* - "full": inject complete text (default, backward compatible)
|
|
37
|
+
* - "l1": inject L1 overview from metadata (~500 tokens)
|
|
38
|
+
* - "l0": inject L0 abstract from metadata (~100 tokens)
|
|
39
|
+
* Agent can use memory_drill_down tool to get deeper content on demand.
|
|
40
|
+
*/
|
|
41
|
+
recallDepthDefault?: "l0" | "l1" | "full";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function loadDotEnv(): void {
|
|
45
|
+
const envPath = resolve(import.meta.dir, "../.env");
|
|
46
|
+
if (!existsSync(envPath)) return;
|
|
47
|
+
const content = readFileSync(envPath, "utf-8");
|
|
48
|
+
for (const line of content.split("\n")) {
|
|
49
|
+
const trimmed = line.trim();
|
|
50
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
51
|
+
const eqIdx = trimmed.indexOf("=");
|
|
52
|
+
if (eqIdx > 0) {
|
|
53
|
+
const key = trimmed.slice(0, eqIdx).trim();
|
|
54
|
+
const val = trimmed.slice(eqIdx + 1).trim();
|
|
55
|
+
if (!process.env[key]) process.env[key] = val;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function findConfigPath(): string {
|
|
61
|
+
if (process.env.LOCAL_MEMORY_CONFIG) return resolve(process.env.LOCAL_MEMORY_CONFIG);
|
|
62
|
+
|
|
63
|
+
const localConfig = resolve(import.meta.dir, "../config.json");
|
|
64
|
+
if (existsSync(localConfig)) return localConfig;
|
|
65
|
+
|
|
66
|
+
const branded = join(homedir(), ".config", "recallnest", "config.json");
|
|
67
|
+
if (existsSync(branded)) return branded;
|
|
68
|
+
|
|
69
|
+
const exampleExists = existsSync(resolve(import.meta.dir, "../config.json.example"));
|
|
70
|
+
throw new Error(
|
|
71
|
+
"Config not found.\n" +
|
|
72
|
+
(exampleExists
|
|
73
|
+
? " Quick fix: cp config.json.example config.json\n"
|
|
74
|
+
: "") +
|
|
75
|
+
" Or set LOCAL_MEMORY_CONFIG env var, or place config.json in ~/.config/recallnest/"
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function loadConfig(): LocalMemoryConfig {
|
|
80
|
+
const raw = readFileSync(findConfigPath(), "utf-8");
|
|
81
|
+
return JSON.parse(raw) as LocalMemoryConfig;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function resolveEnv(value: string): string {
|
|
85
|
+
return value.replace(/\$\{([^}]+)\}/g, (_, name) => {
|
|
86
|
+
const envVal = process.env[name];
|
|
87
|
+
if (!envVal) throw new Error(`Environment variable ${name} not set`);
|
|
88
|
+
return envVal;
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function expandHome(p: string): string {
|
|
93
|
+
if (p.startsWith("~/")) return join(homedir(), p.slice(2));
|
|
94
|
+
return p;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function createComponents(config: LocalMemoryConfig, profileName?: string) {
|
|
98
|
+
const dbPath = resolve(import.meta.dir, "..", expandHome(config.dbPath));
|
|
99
|
+
validateStoragePath(dbPath);
|
|
100
|
+
|
|
101
|
+
const embeddingConfig: EmbeddingConfig = {
|
|
102
|
+
provider: "openai-compatible",
|
|
103
|
+
apiKey: resolveEnv(config.embedding.apiKey),
|
|
104
|
+
model: config.embedding.model,
|
|
105
|
+
baseURL: config.embedding.baseURL,
|
|
106
|
+
dimensions: config.embedding.dimensions,
|
|
107
|
+
taskQuery: config.embedding.taskQuery,
|
|
108
|
+
taskPassage: config.embedding.taskPassage,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const embedder = createEmbedder(embeddingConfig);
|
|
112
|
+
const store = new MemoryStore({ dbPath, vectorDim: embedder.dimensions });
|
|
113
|
+
const baseRetrievalConfig = {
|
|
114
|
+
...DEFAULT_RETRIEVAL_CONFIG,
|
|
115
|
+
...(config.retrieval || {}),
|
|
116
|
+
};
|
|
117
|
+
const { profile, config: retrieverConfig } = applyRetrievalProfile(baseRetrievalConfig, profileName);
|
|
118
|
+
const retriever = createRetriever(store, embedder, retrieverConfig);
|
|
119
|
+
|
|
120
|
+
// Attach access tracker for reinforcement-based decay
|
|
121
|
+
const accessTracker = new AccessTracker(store);
|
|
122
|
+
retriever.setAccessTracker(accessTracker);
|
|
123
|
+
|
|
124
|
+
// Create LLM client if configured (optional, graceful)
|
|
125
|
+
let llm: LLMClient | null = null;
|
|
126
|
+
if (config.llm) {
|
|
127
|
+
llm = createLLMClient(config.llm);
|
|
128
|
+
if (llm) {
|
|
129
|
+
logInfo(`[INFO] LLM client initialized: ${config.llm.model} @ ${config.llm.baseURL}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { store, embedder, retriever, profile, accessTracker, llm };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function createStoreOnly(config: LocalMemoryConfig): MemoryStore {
|
|
137
|
+
const dbPath = resolve(import.meta.dir, "..", expandHome(config.dbPath));
|
|
138
|
+
validateStoragePath(dbPath);
|
|
139
|
+
return new MemoryStore({
|
|
140
|
+
dbPath,
|
|
141
|
+
vectorDim: getVectorDimensions(config.embedding.model, config.embedding.dimensions),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function createComponentResolver(config: LocalMemoryConfig) {
|
|
146
|
+
const cache = new Map<string, ReturnType<typeof createComponents>>();
|
|
147
|
+
|
|
148
|
+
return function getComponents(profileName?: string) {
|
|
149
|
+
const key = profileName || "default";
|
|
150
|
+
const cached = cache.get(key);
|
|
151
|
+
if (cached) return cached;
|
|
152
|
+
const created = createComponents(config, profileName);
|
|
153
|
+
cache.set(key, created);
|
|
154
|
+
return created;
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const VALID_RECALL_MODES: RecallMode[] = ["full", "summary", "off"];
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Resolve effective recall mode: per-call override > env var > config > default ("summary").
|
|
162
|
+
*/
|
|
163
|
+
export function resolveRecallMode(config: LocalMemoryConfig, perCallOverride?: string): RecallMode {
|
|
164
|
+
if (perCallOverride && VALID_RECALL_MODES.includes(perCallOverride as RecallMode)) {
|
|
165
|
+
return perCallOverride as RecallMode;
|
|
166
|
+
}
|
|
167
|
+
const envMode = process.env.RECALLNEST_RECALL_MODE;
|
|
168
|
+
if (envMode && VALID_RECALL_MODES.includes(envMode as RecallMode)) {
|
|
169
|
+
return envMode as RecallMode;
|
|
170
|
+
}
|
|
171
|
+
return config.recallMode ?? "summary";
|
|
172
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
function collapseWhitespace(value: string): string {
|
|
4
|
+
return value.replace(/\s+/g, " ").trim();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function boundedStringSchema(field: string, maxLength: number) {
|
|
8
|
+
return z.string({ required_error: `${field} is required` })
|
|
9
|
+
.transform(collapseWhitespace)
|
|
10
|
+
.pipe(
|
|
11
|
+
z.string()
|
|
12
|
+
.min(1, `${field} is required`)
|
|
13
|
+
.max(maxLength, `${field} must be at most ${maxLength} characters`),
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function optionalBoundedStringSchema(maxLength: number) {
|
|
18
|
+
return z.string()
|
|
19
|
+
.transform(collapseWhitespace)
|
|
20
|
+
.pipe(z.string().min(1).max(maxLength))
|
|
21
|
+
.optional();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function identifierSchema(field: string, maxLength = 160) {
|
|
25
|
+
return z.string({ required_error: `${field} is required` })
|
|
26
|
+
.transform(collapseWhitespace)
|
|
27
|
+
.pipe(
|
|
28
|
+
z.string()
|
|
29
|
+
.min(1, `${field} is required`)
|
|
30
|
+
.max(maxLength, `${field} must be at most ${maxLength} characters`),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function normalizedStringListSchema(
|
|
35
|
+
field: string,
|
|
36
|
+
maxItems: number,
|
|
37
|
+
maxItemLength: number,
|
|
38
|
+
) {
|
|
39
|
+
return z.array(boundedStringSchema(field, maxItemLength))
|
|
40
|
+
.default([])
|
|
41
|
+
.transform((items) => {
|
|
42
|
+
const deduped: string[] = [];
|
|
43
|
+
const seen = new Set<string>();
|
|
44
|
+
for (const item of items) {
|
|
45
|
+
const key = item.toLowerCase();
|
|
46
|
+
if (seen.has(key)) continue;
|
|
47
|
+
seen.add(key);
|
|
48
|
+
deduped.push(item);
|
|
49
|
+
}
|
|
50
|
+
return deduped;
|
|
51
|
+
})
|
|
52
|
+
.pipe(
|
|
53
|
+
z.array(z.string())
|
|
54
|
+
.max(maxItems, `${field} must contain at most ${maxItems} items`),
|
|
55
|
+
);
|
|
56
|
+
}
|