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,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capture Heuristic — lightweight salience filter + pattern-based signal extraction.
|
|
3
|
+
*
|
|
4
|
+
* Borrowed from UltraMemory's auto-capture.ts, adapted for RecallNest:
|
|
5
|
+
* - Reuses RecallNest's own noise-filter (richer than UltraMemory's)
|
|
6
|
+
* - Extended Chinese patterns (primary user language)
|
|
7
|
+
* - Returns items compatible with CaptureMemoryInput for batch persistence
|
|
8
|
+
*
|
|
9
|
+
* Zero LLM calls — pure regex/heuristic.
|
|
10
|
+
* Persistence is the caller's responsibility (via persistMemoryBatch).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { isNoise } from "./noise-filter.js";
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Types
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
export interface AutoCaptureItem {
|
|
20
|
+
text: string;
|
|
21
|
+
category: "profile" | "preferences" | "events" | "cases" | "patterns";
|
|
22
|
+
importance: number;
|
|
23
|
+
sourceContext: string;
|
|
24
|
+
/** Replay priority for consolidation: higher = consolidate first. */
|
|
25
|
+
replayPriority: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface AutoCaptureResult {
|
|
29
|
+
/** True if text was rejected by salience pre-filter (too short / noise / greeting) */
|
|
30
|
+
skippedSalience: boolean;
|
|
31
|
+
items: AutoCaptureItem[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Salience pre-filter
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
/** Max items extracted per turn to avoid flooding the store. */
|
|
39
|
+
const MAX_ITEMS_PER_TURN = 5;
|
|
40
|
+
|
|
41
|
+
/** Minimum text length worth analyzing (below this is almost always noise). */
|
|
42
|
+
const MIN_TEXT_LENGTH = 20;
|
|
43
|
+
|
|
44
|
+
/** Greetings / affirmations / single-word responses (EN + ZH). */
|
|
45
|
+
const GREETING_RE =
|
|
46
|
+
/^(hi|hello|hey|thanks|thank you|ok|yes|no|sure|got it|好的|谢谢|嗯|是的|好吧|行|可以|没问题|收到|明白|了解|知道了)[\s!.!。]*$/i;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Heuristic salience check — should this conversation turn be analyzed
|
|
50
|
+
* for memory extraction?
|
|
51
|
+
*/
|
|
52
|
+
export function shouldCapture(text: string): boolean {
|
|
53
|
+
if (!text || text.trim().length < MIN_TEXT_LENGTH) return false;
|
|
54
|
+
if (isNoise(text)) return false;
|
|
55
|
+
if (GREETING_RE.test(text.trim().toLowerCase())) return false;
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
// Heuristic extraction patterns
|
|
61
|
+
// ---------------------------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
interface SignalPattern {
|
|
64
|
+
re: RegExp;
|
|
65
|
+
category: AutoCaptureItem["category"];
|
|
66
|
+
importance: number;
|
|
67
|
+
sourceContext: string;
|
|
68
|
+
/** Replay priority: corrections/explicit-memory > decisions > preferences/patterns. */
|
|
69
|
+
replayPriority: number;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const SIGNAL_PATTERNS: SignalPattern[] = [
|
|
73
|
+
// --- Preference signals (0.8) ---
|
|
74
|
+
{
|
|
75
|
+
re: /(?:\b(?:i prefer|i like|i don't like|i hate|i love|i want|i need|i always use|i never use)\b|我喜欢|我不喜欢|我偏好|我想要|我习惯|我一般用|我从不用|我讨厌|以后都用|以后不要)/i,
|
|
76
|
+
category: "preferences",
|
|
77
|
+
importance: 0.8,
|
|
78
|
+
sourceContext: "preference signal",
|
|
79
|
+
replayPriority: 0.5,
|
|
80
|
+
},
|
|
81
|
+
// --- Identity / profile signals (0.9) ---
|
|
82
|
+
{
|
|
83
|
+
re: /(?:\b(?:my name is|i am a|i'm a|i work at|i live in|my role is|i'm responsible for)\b|我叫|我是|我在|我的角色|我负责|我的工作是|我住在)/i,
|
|
84
|
+
category: "profile",
|
|
85
|
+
importance: 0.9,
|
|
86
|
+
sourceContext: "identity signal",
|
|
87
|
+
replayPriority: 0.6,
|
|
88
|
+
},
|
|
89
|
+
// --- Decision signals (0.7) ---
|
|
90
|
+
{
|
|
91
|
+
re: /(?:\b(?:i decided|we decided|let's go with|the decision is|we agreed|final call)\b|决定了|我们选择|最终方案|确定用|就这么定了|敲定)/i,
|
|
92
|
+
category: "events",
|
|
93
|
+
importance: 0.7,
|
|
94
|
+
sourceContext: "decision signal",
|
|
95
|
+
replayPriority: 0.7,
|
|
96
|
+
},
|
|
97
|
+
// --- Correction signals (0.85 — high value, user explicitly correcting agent) ---
|
|
98
|
+
{
|
|
99
|
+
re: /(?:\b(?:actually|no,? not|that's wrong|correction:|you're wrong|that's incorrect)\b|更正|其实不是|不对|搞错了|纠正一下|你说错了|应该是)/i,
|
|
100
|
+
category: "cases",
|
|
101
|
+
importance: 0.85,
|
|
102
|
+
sourceContext: "correction signal",
|
|
103
|
+
replayPriority: 0.9,
|
|
104
|
+
},
|
|
105
|
+
// --- Explicit memory instruction signals (0.85) ---
|
|
106
|
+
{
|
|
107
|
+
re: /(?:\b(?:remember that|don't forget|keep in mind|note that|important:)\b|记住|别忘了|注意|以后记得|帮我记|你要记住)/i,
|
|
108
|
+
category: "events",
|
|
109
|
+
importance: 0.85,
|
|
110
|
+
sourceContext: "explicit memory instruction",
|
|
111
|
+
replayPriority: 0.9,
|
|
112
|
+
},
|
|
113
|
+
// --- Pattern / workflow signals (0.75) ---
|
|
114
|
+
{
|
|
115
|
+
re: /(?:\b(?:the pattern is|the workflow is|the process is|step 1|the rule is|always do)\b|流程是|步骤是|规则是|每次都要|固定做法|标准流程)/i,
|
|
116
|
+
category: "patterns",
|
|
117
|
+
importance: 0.75,
|
|
118
|
+
sourceContext: "pattern signal",
|
|
119
|
+
replayPriority: 0.6,
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Extract memory-worthy items from conversation text using simple heuristics.
|
|
125
|
+
*
|
|
126
|
+
* Lightweight alternative to LLM-based extraction.
|
|
127
|
+
* Returns items ready for persistence via persistMemoryBatch.
|
|
128
|
+
*/
|
|
129
|
+
export function extractHeuristic(text: string): AutoCaptureItem[] {
|
|
130
|
+
const items: AutoCaptureItem[] = [];
|
|
131
|
+
const hasCJK = /[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/.test(text);
|
|
132
|
+
const minSentenceLen = hasCJK ? 6 : 15;
|
|
133
|
+
|
|
134
|
+
const sentences = text
|
|
135
|
+
.split(/[.!?。!?\n]+/)
|
|
136
|
+
.map((s) => s.trim())
|
|
137
|
+
.filter((s) => s.length > minSentenceLen);
|
|
138
|
+
|
|
139
|
+
for (let i = 0; i < sentences.length; i++) {
|
|
140
|
+
const sentence = sentences[i];
|
|
141
|
+
|
|
142
|
+
for (const pattern of SIGNAL_PATTERNS) {
|
|
143
|
+
if (pattern.re.test(sentence)) {
|
|
144
|
+
// For correction signals, include the next sentence to capture
|
|
145
|
+
// the actual corrected fact (e.g. "Actually, that's wrong. The limit is 200.")
|
|
146
|
+
let capturedText = sentence;
|
|
147
|
+
if (pattern.sourceContext === "correction signal" && i + 1 < sentences.length) {
|
|
148
|
+
capturedText = `${sentence}. ${sentences[i + 1]}`;
|
|
149
|
+
i++; // skip next sentence since we consumed it
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
items.push({
|
|
153
|
+
text: capturedText,
|
|
154
|
+
category: pattern.category,
|
|
155
|
+
importance: pattern.importance,
|
|
156
|
+
sourceContext: pattern.sourceContext,
|
|
157
|
+
replayPriority: pattern.replayPriority,
|
|
158
|
+
});
|
|
159
|
+
break; // first matching pattern wins for this sentence
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (items.length >= MAX_ITEMS_PER_TURN) break;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return items.slice(0, MAX_ITEMS_PER_TURN);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Full auto-capture pipeline: salience check → heuristic extraction.
|
|
171
|
+
*/
|
|
172
|
+
export function autoCapture(text: string): AutoCaptureResult {
|
|
173
|
+
if (!shouldCapture(text)) {
|
|
174
|
+
return { skippedSalience: true, items: [] };
|
|
175
|
+
}
|
|
176
|
+
return { skippedSalience: false, items: extractHeuristic(text) };
|
|
177
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cascade Forget — Directed forgetting with association propagation.
|
|
3
|
+
*
|
|
4
|
+
* Agent-First reasoning: when a user says "forget X", the primary memory
|
|
5
|
+
* gets deleted/archived. But if related memories (meeting notes, decisions,
|
|
6
|
+
* preferences mentioning X) keep their original importance, X still
|
|
7
|
+
* surfaces indirectly through retrieval. Cascade forget finds related
|
|
8
|
+
* entries and demotes them so X doesn't leak back via associations.
|
|
9
|
+
*
|
|
10
|
+
* Brain-science label: "directed forgetting" — suppressing a memory also
|
|
11
|
+
* weakens retrieval of associated memories. We keep the name for branding;
|
|
12
|
+
* the real mechanism is vector search + importance reduction.
|
|
13
|
+
*
|
|
14
|
+
* Rules:
|
|
15
|
+
* - Does NOT delete related entries — only demotes importance and tier
|
|
16
|
+
* - Demotion is proportional to similarity (higher sim = bigger demotion)
|
|
17
|
+
* - Writes metadata breadcrumb for auditability
|
|
18
|
+
* - No LLM calls — pure vector search + arithmetic
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { MemoryStore, MemoryEntry } from "./store.js";
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Config
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
export interface CascadeForgetConfig {
|
|
28
|
+
/** Minimum vector similarity to consider "related" (default: 0.70) */
|
|
29
|
+
similarityThreshold: number;
|
|
30
|
+
/** Maximum entries to demote per forget operation (default: 10) */
|
|
31
|
+
maxDemotePerForget: number;
|
|
32
|
+
/** Maximum importance reduction (at similarity=1.0) (default: 0.3) */
|
|
33
|
+
maxDemotion: number;
|
|
34
|
+
/** Floor: importance never drops below this (default: 0.05) */
|
|
35
|
+
importanceFloor: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const DEFAULT_CASCADE_FORGET_CONFIG: CascadeForgetConfig = {
|
|
39
|
+
similarityThreshold: 0.70,
|
|
40
|
+
maxDemotePerForget: 10,
|
|
41
|
+
maxDemotion: 0.3,
|
|
42
|
+
importanceFloor: 0.05,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Core
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
export interface CascadeForgetResult {
|
|
50
|
+
demotedCount: number;
|
|
51
|
+
demotedIds: string[];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* After deleting/archiving a memory, cascade-demote related entries.
|
|
56
|
+
*
|
|
57
|
+
* @param store - Memory store
|
|
58
|
+
* @param forgottenEntry - The entry that was just deleted/archived (need its vector + scope)
|
|
59
|
+
* @param config - Cascade config
|
|
60
|
+
*/
|
|
61
|
+
export async function cascadeForget(
|
|
62
|
+
store: MemoryStore,
|
|
63
|
+
forgottenEntry: Pick<MemoryEntry, "id" | "vector" | "scope">,
|
|
64
|
+
config: CascadeForgetConfig = DEFAULT_CASCADE_FORGET_CONFIG,
|
|
65
|
+
): Promise<CascadeForgetResult> {
|
|
66
|
+
if (!forgottenEntry.vector?.length) {
|
|
67
|
+
return { demotedCount: 0, demotedIds: [] };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Find related entries in the same scope
|
|
71
|
+
const candidates = await store.vectorSearch(
|
|
72
|
+
forgottenEntry.vector,
|
|
73
|
+
config.maxDemotePerForget * 2, // fetch extra, filter down
|
|
74
|
+
config.similarityThreshold,
|
|
75
|
+
[forgottenEntry.scope],
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const demotedIds: string[] = [];
|
|
79
|
+
|
|
80
|
+
for (const candidate of candidates) {
|
|
81
|
+
if (demotedIds.length >= config.maxDemotePerForget) break;
|
|
82
|
+
|
|
83
|
+
const entry = candidate.entry;
|
|
84
|
+
|
|
85
|
+
// Skip the forgotten entry itself (may still be in index)
|
|
86
|
+
if (entry.id === forgottenEntry.id) continue;
|
|
87
|
+
|
|
88
|
+
// Skip already-archived entries
|
|
89
|
+
let meta: Record<string, any> = {};
|
|
90
|
+
try { meta = JSON.parse(entry.metadata || "{}"); } catch { /* skip */ }
|
|
91
|
+
if (meta.state === "archived" || meta.state === "superseded") continue;
|
|
92
|
+
|
|
93
|
+
// Proportional demotion: higher similarity → bigger cut
|
|
94
|
+
// At sim=1.0 → full maxDemotion, at threshold → near-zero
|
|
95
|
+
const simRange = 1.0 - config.similarityThreshold;
|
|
96
|
+
const simNormalized = simRange > 0
|
|
97
|
+
? (candidate.score - config.similarityThreshold) / simRange
|
|
98
|
+
: 1.0;
|
|
99
|
+
const demotion = config.maxDemotion * simNormalized;
|
|
100
|
+
|
|
101
|
+
const newImportance = Math.max(
|
|
102
|
+
entry.importance - demotion,
|
|
103
|
+
config.importanceFloor,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Skip if no meaningful change
|
|
107
|
+
if (Math.abs(newImportance - entry.importance) < 0.01) continue;
|
|
108
|
+
|
|
109
|
+
// Audit trail
|
|
110
|
+
if (!Array.isArray(meta.cascade_forget)) meta.cascade_forget = [];
|
|
111
|
+
meta.cascade_forget.push({
|
|
112
|
+
forgottenId: forgottenEntry.id.slice(0, 8),
|
|
113
|
+
from: entry.importance,
|
|
114
|
+
to: newImportance,
|
|
115
|
+
similarity: candidate.score,
|
|
116
|
+
date: new Date().toISOString().slice(0, 10),
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Demote tier if importance drops below working threshold
|
|
120
|
+
if (meta.tier === "working" && newImportance < 0.5) {
|
|
121
|
+
meta.tier = "peripheral";
|
|
122
|
+
} else if (meta.tier === "core" && newImportance < 0.8) {
|
|
123
|
+
meta.tier = "working";
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
await store.update(entry.id, {
|
|
127
|
+
importance: newImportance,
|
|
128
|
+
metadata: JSON.stringify(meta),
|
|
129
|
+
}, [forgottenEntry.scope]);
|
|
130
|
+
|
|
131
|
+
demotedIds.push(entry.id);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { demotedCount: demotedIds.length, demotedIds };
|
|
135
|
+
}
|
package/src/chunker.ts
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Long Context Chunking System
|
|
3
|
+
*
|
|
4
|
+
* Goal: split documents that exceed embedding model context limits into smaller,
|
|
5
|
+
* semantically coherent chunks with overlap.
|
|
6
|
+
*
|
|
7
|
+
* Notes:
|
|
8
|
+
* - We use *character counts* as a conservative proxy for tokens.
|
|
9
|
+
* - The embedder triggers this only after a provider throws a context-length error.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Types & Constants
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
export interface ChunkMetadata {
|
|
17
|
+
startIndex: number;
|
|
18
|
+
endIndex: number;
|
|
19
|
+
length: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ChunkResult {
|
|
23
|
+
chunks: string[];
|
|
24
|
+
metadatas: ChunkMetadata[];
|
|
25
|
+
totalOriginalLength: number;
|
|
26
|
+
chunkCount: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ChunkerConfig {
|
|
30
|
+
/** Maximum characters per chunk. */
|
|
31
|
+
maxChunkSize: number;
|
|
32
|
+
/** Overlap between chunks in characters. */
|
|
33
|
+
overlapSize: number;
|
|
34
|
+
/** Minimum chunk size (except the final chunk). */
|
|
35
|
+
minChunkSize: number;
|
|
36
|
+
/** Attempt to split on sentence boundaries for better semantic coherence. */
|
|
37
|
+
semanticSplit: boolean;
|
|
38
|
+
/** Max lines per chunk before we try to split earlier on a line boundary. */
|
|
39
|
+
maxLinesPerChunk: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Common embedding context limits (provider/model specific). These are typically
|
|
43
|
+
// token limits, but we treat them as inputs to a conservative char-based heuristic.
|
|
44
|
+
export const EMBEDDING_CONTEXT_LIMITS: Record<string, number> = {
|
|
45
|
+
// Jina v5
|
|
46
|
+
"jina-embeddings-v5-text-small": 8192,
|
|
47
|
+
"jina-embeddings-v5-text-nano": 8192,
|
|
48
|
+
|
|
49
|
+
// OpenAI
|
|
50
|
+
"text-embedding-3-small": 8192,
|
|
51
|
+
"text-embedding-3-large": 8192,
|
|
52
|
+
|
|
53
|
+
// Google
|
|
54
|
+
"text-embedding-004": 8192,
|
|
55
|
+
"gemini-embedding-001": 2048,
|
|
56
|
+
|
|
57
|
+
// Local/common
|
|
58
|
+
"nomic-embed-text": 8192,
|
|
59
|
+
"all-MiniLM-L6-v2": 512,
|
|
60
|
+
"all-mpnet-base-v2": 512,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export const DEFAULT_CHUNKER_CONFIG: ChunkerConfig = {
|
|
64
|
+
maxChunkSize: 4000,
|
|
65
|
+
overlapSize: 200,
|
|
66
|
+
minChunkSize: 200,
|
|
67
|
+
semanticSplit: true,
|
|
68
|
+
maxLinesPerChunk: 50,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Sentence ending patterns (English + CJK-ish punctuation)
|
|
72
|
+
const SENTENCE_ENDING = /[.!?。!?]/;
|
|
73
|
+
|
|
74
|
+
// ============================================================================
|
|
75
|
+
// Helpers
|
|
76
|
+
// ============================================================================
|
|
77
|
+
|
|
78
|
+
function clamp(n: number, lo: number, hi: number): number {
|
|
79
|
+
return Math.max(lo, Math.min(hi, n));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function countLines(s: string): number {
|
|
83
|
+
// Count \n (treat CRLF as one line break)
|
|
84
|
+
return s.split(/\r\n|\n|\r/).length;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function findLastIndexWithin(text: string, re: RegExp, start: number, end: number): number {
|
|
88
|
+
// Find last match start index for regex within [start, end).
|
|
89
|
+
// NOTE: `re` must NOT be global; we will scan manually.
|
|
90
|
+
let last = -1;
|
|
91
|
+
for (let i = end - 1; i >= start; i--) {
|
|
92
|
+
if (re.test(text[i])) return i;
|
|
93
|
+
}
|
|
94
|
+
return last;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function findSplitEnd(text: string, start: number, maxEnd: number, minEnd: number, config: ChunkerConfig): number {
|
|
98
|
+
const safeMinEnd = clamp(minEnd, start + 1, maxEnd);
|
|
99
|
+
const safeMaxEnd = clamp(maxEnd, safeMinEnd, text.length);
|
|
100
|
+
|
|
101
|
+
// Respect line limit: if we exceed maxLinesPerChunk, force earlier split at a line break.
|
|
102
|
+
if (config.maxLinesPerChunk > 0) {
|
|
103
|
+
const candidate = text.slice(start, safeMaxEnd);
|
|
104
|
+
if (countLines(candidate) > config.maxLinesPerChunk) {
|
|
105
|
+
// Find the position of the Nth line break.
|
|
106
|
+
let breaks = 0;
|
|
107
|
+
for (let i = start; i < safeMaxEnd; i++) {
|
|
108
|
+
const ch = text[i];
|
|
109
|
+
if (ch === "\n") {
|
|
110
|
+
breaks++;
|
|
111
|
+
if (breaks >= config.maxLinesPerChunk) {
|
|
112
|
+
// Split right after this newline.
|
|
113
|
+
return Math.max(i + 1, safeMinEnd);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (config.semanticSplit) {
|
|
121
|
+
// Prefer a sentence boundary near the end.
|
|
122
|
+
// Scan backward from safeMaxEnd to safeMinEnd.
|
|
123
|
+
for (let i = safeMaxEnd - 1; i >= safeMinEnd; i--) {
|
|
124
|
+
if (SENTENCE_ENDING.test(text[i])) {
|
|
125
|
+
// Include trailing whitespace after punctuation.
|
|
126
|
+
let j = i + 1;
|
|
127
|
+
while (j < safeMaxEnd && /\s/.test(text[j])) j++;
|
|
128
|
+
return j;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Next best: newline boundary.
|
|
133
|
+
for (let i = safeMaxEnd - 1; i >= safeMinEnd; i--) {
|
|
134
|
+
if (text[i] === "\n") return i + 1;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Fallback: last whitespace boundary.
|
|
139
|
+
for (let i = safeMaxEnd - 1; i >= safeMinEnd; i--) {
|
|
140
|
+
if (/\s/.test(text[i])) return i;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return safeMaxEnd;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function sliceTrimWithIndices(text: string, start: number, end: number): { chunk: string; meta: ChunkMetadata } {
|
|
147
|
+
const raw = text.slice(start, end);
|
|
148
|
+
const leading = raw.match(/^\s*/)?.[0]?.length ?? 0;
|
|
149
|
+
const trailing = raw.match(/\s*$/)?.[0]?.length ?? 0;
|
|
150
|
+
const chunk = raw.trim();
|
|
151
|
+
|
|
152
|
+
const trimmedStart = start + leading;
|
|
153
|
+
const trimmedEnd = end - trailing;
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
chunk,
|
|
157
|
+
meta: {
|
|
158
|
+
startIndex: trimmedStart,
|
|
159
|
+
endIndex: Math.max(trimmedStart, trimmedEnd),
|
|
160
|
+
length: chunk.length,
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ============================================================================
|
|
166
|
+
// Chunking Core
|
|
167
|
+
// ============================================================================
|
|
168
|
+
|
|
169
|
+
export function chunkDocument(text: string, config: ChunkerConfig = DEFAULT_CHUNKER_CONFIG): ChunkResult {
|
|
170
|
+
if (!text || text.trim().length === 0) {
|
|
171
|
+
return { chunks: [], metadatas: [], totalOriginalLength: 0, chunkCount: 0 };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const totalOriginalLength = text.length;
|
|
175
|
+
const chunks: string[] = [];
|
|
176
|
+
const metadatas: ChunkMetadata[] = [];
|
|
177
|
+
|
|
178
|
+
let pos = 0;
|
|
179
|
+
const maxGuard = Math.max(4, Math.ceil(text.length / Math.max(1, config.maxChunkSize - config.overlapSize)) + 5);
|
|
180
|
+
let guard = 0;
|
|
181
|
+
|
|
182
|
+
while (pos < text.length && guard < maxGuard) {
|
|
183
|
+
guard++;
|
|
184
|
+
|
|
185
|
+
const remaining = text.length - pos;
|
|
186
|
+
if (remaining <= config.maxChunkSize) {
|
|
187
|
+
const { chunk, meta } = sliceTrimWithIndices(text, pos, text.length);
|
|
188
|
+
if (chunk.length > 0) {
|
|
189
|
+
chunks.push(chunk);
|
|
190
|
+
metadatas.push(meta);
|
|
191
|
+
}
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const maxEnd = Math.min(pos + config.maxChunkSize, text.length);
|
|
196
|
+
const minEnd = Math.min(pos + config.minChunkSize, maxEnd);
|
|
197
|
+
|
|
198
|
+
const end = findSplitEnd(text, pos, maxEnd, minEnd, config);
|
|
199
|
+
const { chunk, meta } = sliceTrimWithIndices(text, pos, end);
|
|
200
|
+
|
|
201
|
+
// If trimming made it too small, fall back to a hard split.
|
|
202
|
+
if (chunk.length < config.minChunkSize) {
|
|
203
|
+
const hardEnd = Math.min(pos + config.maxChunkSize, text.length);
|
|
204
|
+
const hard = sliceTrimWithIndices(text, pos, hardEnd);
|
|
205
|
+
if (hard.chunk.length > 0) {
|
|
206
|
+
chunks.push(hard.chunk);
|
|
207
|
+
metadatas.push(hard.meta);
|
|
208
|
+
}
|
|
209
|
+
if (hardEnd >= text.length) break;
|
|
210
|
+
pos = Math.max(hardEnd - config.overlapSize, pos + 1);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
chunks.push(chunk);
|
|
215
|
+
metadatas.push(meta);
|
|
216
|
+
|
|
217
|
+
if (end >= text.length) break;
|
|
218
|
+
|
|
219
|
+
// Move forward with overlap.
|
|
220
|
+
const nextPos = Math.max(end - config.overlapSize, pos + 1);
|
|
221
|
+
pos = nextPos;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
chunks,
|
|
226
|
+
metadatas,
|
|
227
|
+
totalOriginalLength,
|
|
228
|
+
chunkCount: chunks.length,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Smart chunker that adapts to model context limits.
|
|
234
|
+
*
|
|
235
|
+
* We intentionally pick conservative char limits (70% of the reported limit)
|
|
236
|
+
* since token/char ratios vary.
|
|
237
|
+
*/
|
|
238
|
+
export function smartChunk(text: string, embedderModel?: string): ChunkResult {
|
|
239
|
+
const limit = embedderModel ? EMBEDDING_CONTEXT_LIMITS[embedderModel] : undefined;
|
|
240
|
+
const base = limit ?? 8192;
|
|
241
|
+
|
|
242
|
+
const config: ChunkerConfig = {
|
|
243
|
+
maxChunkSize: Math.max(1000, Math.floor(base * 0.7)),
|
|
244
|
+
overlapSize: Math.max(0, Math.floor(base * 0.05)),
|
|
245
|
+
minChunkSize: Math.max(100, Math.floor(base * 0.1)),
|
|
246
|
+
semanticSplit: true,
|
|
247
|
+
maxLinesPerChunk: 50,
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
return chunkDocument(text, config);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export default chunkDocument;
|