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
package/src/ingest.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { readFileSync, readdirSync, existsSync, statSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { join, basename, resolve, dirname } from "node:path";
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
|
+
import { detectLang, tokenizeFts } from "./language-hook.js";
|
|
14
15
|
import type { MemoryStore, MemoryEntry } from "./store.js";
|
|
15
16
|
import type { Embedder } from "./embedder.js";
|
|
16
17
|
import { chunkDocument, type ChunkerConfig } from "./chunker.js";
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
samePreferenceSlot,
|
|
27
28
|
} from "./preference-slots.js";
|
|
28
29
|
import { compressToolOutput } from "./tool-output-compressor.js";
|
|
30
|
+
import { tagNarrativeIfEnabled } from "./narrative-tagger.js";
|
|
29
31
|
|
|
30
32
|
// ============================================================================
|
|
31
33
|
// Types
|
|
@@ -178,7 +180,7 @@ function normalizeDedupText(value: string): string {
|
|
|
178
180
|
* 3. Explicit memory instructions (记住, remember, 以后注意, 别忘了)
|
|
179
181
|
* 4. Feedback/corrections (不要, 别再, stop doing, don't)
|
|
180
182
|
*/
|
|
181
|
-
function isTemporalOrActionContent(text: string): boolean {
|
|
183
|
+
export function isTemporalOrActionContent(text: string): boolean {
|
|
182
184
|
// Date patterns: ISO dates, Chinese relative dates
|
|
183
185
|
const datePattern = /\b20\d{2}[-/]\d{2}[-/]\d{2}\b|昨天|今天|前天|上次|刚才|刚刚|本周|这周|上周/;
|
|
184
186
|
// File operation patterns
|
|
@@ -586,13 +588,18 @@ const PENDING_EXTRACTION_FILE = resolve(
|
|
|
586
588
|
function queueForLaterExtraction(chunks: Array<{ text: string; scope: string }>): void {
|
|
587
589
|
let pending: Array<{ text: string; scope: string; queuedAt: string }> = [];
|
|
588
590
|
try {
|
|
589
|
-
|
|
591
|
+
const raw = JSON.parse(readFileSync(PENDING_EXTRACTION_FILE, "utf-8"));
|
|
592
|
+
if (Array.isArray(raw)) pending = raw;
|
|
590
593
|
} catch { /* empty or missing */ }
|
|
591
594
|
const now = new Date().toISOString();
|
|
592
595
|
for (const chunk of chunks) {
|
|
593
596
|
pending.push({ ...chunk, queuedAt: now });
|
|
594
597
|
}
|
|
595
|
-
|
|
598
|
+
try {
|
|
599
|
+
writeFileSync(PENDING_EXTRACTION_FILE, JSON.stringify(pending, null, 2));
|
|
600
|
+
} catch (err) {
|
|
601
|
+
console.error("[recallnest] Failed to write pending extraction queue:", err instanceof Error ? err.message : String(err));
|
|
602
|
+
}
|
|
596
603
|
}
|
|
597
604
|
|
|
598
605
|
export async function drainPendingQueue(
|
|
@@ -602,7 +609,12 @@ export async function drainPendingQueue(
|
|
|
602
609
|
): Promise<{ processed: number; errors: number }> {
|
|
603
610
|
let pending: Array<{ text: string; scope: string; queuedAt: string }> = [];
|
|
604
611
|
try {
|
|
605
|
-
|
|
612
|
+
const raw = JSON.parse(readFileSync(PENDING_EXTRACTION_FILE, "utf-8"));
|
|
613
|
+
if (!Array.isArray(raw)) {
|
|
614
|
+
console.error("[recallnest] Pending extraction queue is malformed (not an array), resetting");
|
|
615
|
+
return { processed: 0, errors: 0 };
|
|
616
|
+
}
|
|
617
|
+
pending = raw;
|
|
606
618
|
} catch { return { processed: 0, errors: 0 }; }
|
|
607
619
|
|
|
608
620
|
if (pending.length === 0) return { processed: 0, errors: 0 };
|
|
@@ -623,13 +635,18 @@ export async function drainPendingQueue(
|
|
|
623
635
|
|
|
624
636
|
for (const j of keepIndices) {
|
|
625
637
|
try {
|
|
638
|
+
const entryText = extractions[j].l1 || extractions[j].l0;
|
|
639
|
+
const language = detectLang(entryText);
|
|
640
|
+
const fts_text = tokenizeFts(entryText, language);
|
|
626
641
|
await store.store({
|
|
627
|
-
text:
|
|
642
|
+
text: entryText,
|
|
628
643
|
vector: vectors[j],
|
|
629
644
|
category: extractions[j].category as any,
|
|
630
645
|
scope: batch[j].scope,
|
|
631
646
|
importance: extractions[j].importance,
|
|
632
647
|
metadata: JSON.stringify({ source: batch[j].scope.split(":")[0], l0_abstract: extractions[j].l0, l1_overview: extractions[j].l1, l2_content: extractions[j].l1 || extractions[j].l0 }),
|
|
648
|
+
language,
|
|
649
|
+
fts_text,
|
|
633
650
|
});
|
|
634
651
|
processed++;
|
|
635
652
|
} catch { errors++; }
|
|
@@ -637,7 +654,11 @@ export async function drainPendingQueue(
|
|
|
637
654
|
}
|
|
638
655
|
|
|
639
656
|
// Clear the queue
|
|
640
|
-
|
|
657
|
+
try {
|
|
658
|
+
writeFileSync(PENDING_EXTRACTION_FILE, "[]");
|
|
659
|
+
} catch (err) {
|
|
660
|
+
console.error("[recallnest] Failed to clear pending extraction queue:", err instanceof Error ? err.message : String(err));
|
|
661
|
+
}
|
|
641
662
|
return { processed, errors };
|
|
642
663
|
}
|
|
643
664
|
|
|
@@ -684,6 +705,14 @@ function buildIngestedEntry(params: {
|
|
|
684
705
|
importance: params.extraction.importance,
|
|
685
706
|
});
|
|
686
707
|
|
|
708
|
+
// HP-narrative: Tag with autobiographical narrative metadata when enabled
|
|
709
|
+
const narrative = tagNarrativeIfEnabled({
|
|
710
|
+
scope: params.scope,
|
|
711
|
+
text: params.text,
|
|
712
|
+
timestamp: Date.now(),
|
|
713
|
+
sessionId: params.sessionId,
|
|
714
|
+
});
|
|
715
|
+
|
|
687
716
|
return {
|
|
688
717
|
text: params.text,
|
|
689
718
|
vector: params.vector,
|
|
@@ -701,6 +730,7 @@ function buildIngestedEntry(params: {
|
|
|
701
730
|
...(params.coreSummary ? { core_summary: params.coreSummary } : {}),
|
|
702
731
|
tier,
|
|
703
732
|
boundary: resolution.boundary,
|
|
733
|
+
...(narrative ? { narrative } : {}),
|
|
704
734
|
}),
|
|
705
735
|
};
|
|
706
736
|
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* F2: Interference Detector — detects semantic interference clusters
|
|
3
|
+
* and measures interference density within a scope.
|
|
4
|
+
*
|
|
5
|
+
* Brain-science basis: proactive interference (PI) occurs when
|
|
6
|
+
* semantically similar memories compete during retrieval, degrading
|
|
7
|
+
* signal-to-noise. This module identifies interference clusters and
|
|
8
|
+
* ranks members so the weakest can be deprioritized.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { MemoryEntry } from "./store.js";
|
|
12
|
+
import { parseEvolution } from "./memory-evolution.js";
|
|
13
|
+
import { getConfidence } from "./confidence-tracker.js";
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Types
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
export type InterferenceRisk = "high" | "medium" | "low";
|
|
20
|
+
|
|
21
|
+
export interface InterferenceResult {
|
|
22
|
+
entryId: string;
|
|
23
|
+
clusterId: number;
|
|
24
|
+
clusterRank: number; // 0 = strongest in cluster
|
|
25
|
+
interferenceRisk: InterferenceRisk;
|
|
26
|
+
clusterSize: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface InterferenceDensity {
|
|
30
|
+
totalMemories: number;
|
|
31
|
+
clusterCount: number;
|
|
32
|
+
avgClusterSize: number;
|
|
33
|
+
highRiskCount: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Cosine Similarity (shared with rif.ts — keep inline to avoid circular dep)
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
function cosineSimilarity(a: number[] | Float32Array, b: number[] | Float32Array): number {
|
|
41
|
+
if (!a || !b || a.length !== b.length || a.length === 0) return 0;
|
|
42
|
+
let dot = 0, normA = 0, normB = 0;
|
|
43
|
+
for (let i = 0; i < a.length; i++) {
|
|
44
|
+
dot += a[i] * b[i];
|
|
45
|
+
normA += a[i] * a[i];
|
|
46
|
+
normB += b[i] * b[i];
|
|
47
|
+
}
|
|
48
|
+
const denom = Math.sqrt(normA) * Math.sqrt(normB);
|
|
49
|
+
return denom > 0 ? dot / denom : 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Composite score for cluster ranking
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
function compositeScore(entry: MemoryEntry, now: number): number {
|
|
57
|
+
const evo = parseEvolution(entry.metadata, entry.timestamp);
|
|
58
|
+
const confidence = getConfidence(entry);
|
|
59
|
+
const importance = entry.importance ?? 0.5;
|
|
60
|
+
// Recency: exponential decay with 60-day half-life
|
|
61
|
+
const daysSince = Math.max(0, (now - entry.timestamp) / 86_400_000);
|
|
62
|
+
const recency = Math.pow(0.5, daysSince / 60);
|
|
63
|
+
return confidence * importance * recency;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Cluster detection via greedy single-linkage
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Detect interference clusters within a set of memories.
|
|
72
|
+
* Uses greedy single-linkage: if sim(A,B) > threshold, they join the same cluster.
|
|
73
|
+
*
|
|
74
|
+
* @param memories - Memory entries with vectors
|
|
75
|
+
* @param similarityThreshold - Cosine sim threshold for "same cluster" (default: 0.80)
|
|
76
|
+
*/
|
|
77
|
+
export function detectInterference(
|
|
78
|
+
memories: MemoryEntry[],
|
|
79
|
+
similarityThreshold = 0.80,
|
|
80
|
+
): InterferenceResult[] {
|
|
81
|
+
if (memories.length === 0) return [];
|
|
82
|
+
|
|
83
|
+
const now = Date.now();
|
|
84
|
+
// Union-Find for cluster assignment
|
|
85
|
+
const parent = memories.map((_, i) => i);
|
|
86
|
+
function find(x: number): number {
|
|
87
|
+
while (parent[x] !== x) { parent[x] = parent[parent[x]]; x = parent[x]; }
|
|
88
|
+
return x;
|
|
89
|
+
}
|
|
90
|
+
function union(a: number, b: number): void {
|
|
91
|
+
const ra = find(a), rb = find(b);
|
|
92
|
+
if (ra !== rb) parent[ra] = rb;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Pairwise similarity check
|
|
96
|
+
for (let i = 0; i < memories.length; i++) {
|
|
97
|
+
for (let j = i + 1; j < memories.length; j++) {
|
|
98
|
+
const sim = cosineSimilarity(memories[i].vector, memories[j].vector);
|
|
99
|
+
if (sim > similarityThreshold) {
|
|
100
|
+
union(i, j);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Group by cluster root
|
|
106
|
+
const clusters = new Map<number, number[]>(); // root → indices
|
|
107
|
+
for (let i = 0; i < memories.length; i++) {
|
|
108
|
+
const root = find(i);
|
|
109
|
+
if (!clusters.has(root)) clusters.set(root, []);
|
|
110
|
+
clusters.get(root)!.push(i);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const results: InterferenceResult[] = [];
|
|
114
|
+
let clusterId = 0;
|
|
115
|
+
|
|
116
|
+
for (const members of clusters.values()) {
|
|
117
|
+
if (members.length < 2) {
|
|
118
|
+
// Singleton — no interference
|
|
119
|
+
results.push({
|
|
120
|
+
entryId: memories[members[0]].id,
|
|
121
|
+
clusterId,
|
|
122
|
+
clusterRank: 0,
|
|
123
|
+
interferenceRisk: "low",
|
|
124
|
+
clusterSize: 1,
|
|
125
|
+
});
|
|
126
|
+
clusterId++;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Rank within cluster by composite score
|
|
131
|
+
const ranked = members
|
|
132
|
+
.map(idx => ({ idx, score: compositeScore(memories[idx], now) }))
|
|
133
|
+
.sort((a, b) => b.score - a.score);
|
|
134
|
+
|
|
135
|
+
for (let rank = 0; rank < ranked.length; rank++) {
|
|
136
|
+
const risk: InterferenceRisk =
|
|
137
|
+
rank === 0 ? "low" :
|
|
138
|
+
rank <= 2 ? "medium" : "high";
|
|
139
|
+
|
|
140
|
+
results.push({
|
|
141
|
+
entryId: memories[ranked[rank].idx].id,
|
|
142
|
+
clusterId,
|
|
143
|
+
clusterRank: rank,
|
|
144
|
+
interferenceRisk: risk,
|
|
145
|
+
clusterSize: ranked.length,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
clusterId++;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return results;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
// Density measurement
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Measure interference density: how clustered are the memories?
|
|
160
|
+
*/
|
|
161
|
+
export function measureInterferenceDensity(
|
|
162
|
+
memories: MemoryEntry[],
|
|
163
|
+
similarityThreshold = 0.80,
|
|
164
|
+
): InterferenceDensity {
|
|
165
|
+
const results = detectInterference(memories, similarityThreshold);
|
|
166
|
+
const multiMemberClusters = new Map<number, number>();
|
|
167
|
+
|
|
168
|
+
for (const r of results) {
|
|
169
|
+
if (r.clusterSize >= 2) {
|
|
170
|
+
multiMemberClusters.set(r.clusterId, r.clusterSize);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const clusterSizes = [...multiMemberClusters.values()];
|
|
175
|
+
const clusterCount = clusterSizes.length;
|
|
176
|
+
const avgClusterSize = clusterCount > 0
|
|
177
|
+
? clusterSizes.reduce((a, b) => a + b, 0) / clusterCount
|
|
178
|
+
: 0;
|
|
179
|
+
const highRiskCount = results.filter(r => r.interferenceRisk === "high").length;
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
totalMemories: memories.length,
|
|
183
|
+
clusterCount,
|
|
184
|
+
avgClusterSize,
|
|
185
|
+
highRiskCount,
|
|
186
|
+
};
|
|
187
|
+
}
|
package/src/kg-extractor.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import type { LLMClient } from "./llm-client.js";
|
|
8
8
|
import type { KGStore } from "./kg-store.js";
|
|
9
9
|
import { logInfo, logWarn } from "./stderr-log.js";
|
|
10
|
+
import { detectLang, getKgPromptHook } from "./language-hook.js";
|
|
10
11
|
|
|
11
12
|
// ============================================================================
|
|
12
13
|
// Types
|
|
@@ -86,33 +87,19 @@ export function normalizePredicate(raw: string): string {
|
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
// ============================================================================
|
|
89
|
-
// Extraction Prompt
|
|
90
|
+
// Extraction Prompt (fallback when babel-memory is not installed)
|
|
90
91
|
// ============================================================================
|
|
91
92
|
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
function buildKGExtractionPrompt(text: string): string {
|
|
95
|
-
return `Extract knowledge graph triples from the text below.
|
|
93
|
+
const DEFAULT_KG_SYSTEM = `You are a knowledge graph extraction assistant. Extract (subject, predicate, object, confidence) tuples from text. Respond with valid JSON only.`;
|
|
96
94
|
|
|
95
|
+
const DEFAULT_KG_USER_TEMPLATE = `Extract knowledge graph triples from the following text.
|
|
97
96
|
Rules:
|
|
98
|
-
-
|
|
99
|
-
-
|
|
100
|
-
-
|
|
101
|
-
-
|
|
102
|
-
- 3-8 triples per text typically
|
|
103
|
-
|
|
104
|
-
Examples:
|
|
105
|
-
|
|
106
|
-
Text: "Alice uses Python for her data science projects at Google"
|
|
107
|
-
{"triples":[{"subject":"Alice","predicate":"uses","object":"Python","confidence":0.95},{"subject":"Alice","predicate":"works_at","object":"Google","confidence":0.90}]}
|
|
97
|
+
- Use simple predicates: uses, created_by, works_with, is_a, part_of, has, located_in, belongs_to, depends_on, implements, extends, related_to, caused_by, results_in, precedes, follows
|
|
98
|
+
- Assign confidence 0.0-1.0
|
|
99
|
+
- Extract 3-8 triples
|
|
100
|
+
- Return JSON: { "triples": [{ "subject": "", "predicate": "", "object": "", "confidence": 0.0 }] }
|
|
108
101
|
|
|
109
|
-
Text:
|
|
110
|
-
{"triples":[{"subject":"RecallNest","predicate":"depends_on","object":"LanceDB","confidence":0.95},{"subject":"RecallNest","predicate":"uses","object":"OpenAI","confidence":0.90}]}
|
|
111
|
-
|
|
112
|
-
Now extract triples from:
|
|
113
|
-
|
|
114
|
-
${text}`;
|
|
115
|
-
}
|
|
102
|
+
Text: {text}`;
|
|
116
103
|
|
|
117
104
|
// ============================================================================
|
|
118
105
|
// Extractor
|
|
@@ -163,12 +150,16 @@ export class KGExtractor {
|
|
|
163
150
|
async extract(text: string): Promise<RawTriple[]> {
|
|
164
151
|
if (!text || text.length < 10) return [];
|
|
165
152
|
|
|
166
|
-
const
|
|
153
|
+
const lang = detectLang(text);
|
|
154
|
+
const babelPrompt = getKgPromptHook(lang);
|
|
155
|
+
const system = babelPrompt?.system ?? DEFAULT_KG_SYSTEM;
|
|
156
|
+
const userTemplate = babelPrompt?.userTemplate ?? DEFAULT_KG_USER_TEMPLATE;
|
|
157
|
+
const prompt = userTemplate.replace("{text}", text);
|
|
167
158
|
|
|
168
159
|
let response: LlmExtractionResponse | null = null;
|
|
169
160
|
try {
|
|
170
161
|
response = await this.llm.chatJson<LlmExtractionResponse>(
|
|
171
|
-
|
|
162
|
+
system,
|
|
172
163
|
prompt,
|
|
173
164
|
);
|
|
174
165
|
} catch (err) {
|
package/src/kg-store.ts
CHANGED
|
@@ -125,8 +125,9 @@ export class KGStore {
|
|
|
125
125
|
const lance = await loadLanceDB();
|
|
126
126
|
await table.createIndex("subject", { config: (lance as any).Index.fts() });
|
|
127
127
|
}
|
|
128
|
-
} catch {
|
|
128
|
+
} catch (err) {
|
|
129
129
|
// FTS on subject is optional — BFS still works without it
|
|
130
|
+
console.error("[recallnest] KG FTS index creation skipped:", err instanceof Error ? err.message : String(err));
|
|
130
131
|
}
|
|
131
132
|
|
|
132
133
|
this.db = db;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language processing hook interface.
|
|
3
|
+
* Allows RecallNest to use any language processor (babel-memory or custom).
|
|
4
|
+
* If no processor is registered, all functions return safe defaults.
|
|
5
|
+
*/
|
|
6
|
+
export interface LanguageProcessor {
|
|
7
|
+
detectLanguage(text: string): string;
|
|
8
|
+
tokenizeForFts(text: string, lang: string): string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface PromptProvider {
|
|
12
|
+
getKgPrompt(lang: string): { system: string; userTemplate: string };
|
|
13
|
+
getSessionPrompt(lang: string): { system: string; dimensionLabels: Record<string, string> };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let processor: LanguageProcessor | null = null;
|
|
17
|
+
let promptProvider: PromptProvider | null = null;
|
|
18
|
+
|
|
19
|
+
export function registerLanguageProcessor(p: LanguageProcessor): void {
|
|
20
|
+
processor = p;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function registerPromptProvider(p: PromptProvider): void {
|
|
24
|
+
promptProvider = p;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function detectLang(text: string): string {
|
|
28
|
+
return processor?.detectLanguage(text) ?? "en";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function tokenizeFts(text: string, lang: string): string {
|
|
32
|
+
return processor?.tokenizeForFts(text, lang) ?? text;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getKgPromptHook(lang: string): { system: string; userTemplate: string } | null {
|
|
36
|
+
return promptProvider?.getKgPrompt(lang) ?? null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getSessionPromptHook(lang: string): { system: string; dimensionLabels: Record<string, string> } | null {
|
|
40
|
+
return promptProvider?.getSessionPrompt(lang) ?? null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Auto-detect and register babel-memory if installed.
|
|
45
|
+
* Call once at server startup. Non-blocking.
|
|
46
|
+
*/
|
|
47
|
+
export async function autoRegisterBabelMemory(): Promise<boolean> {
|
|
48
|
+
try {
|
|
49
|
+
const bm = await import("babel-memory");
|
|
50
|
+
if (typeof bm.initTokenizer === "function") {
|
|
51
|
+
await bm.initTokenizer();
|
|
52
|
+
}
|
|
53
|
+
registerLanguageProcessor({
|
|
54
|
+
detectLanguage: bm.detectLanguage,
|
|
55
|
+
tokenizeForFts: bm.tokenizeForFts,
|
|
56
|
+
});
|
|
57
|
+
if (bm.getKgPrompt && bm.getSessionPrompt) {
|
|
58
|
+
registerPromptProvider({
|
|
59
|
+
getKgPrompt: bm.getKgPrompt,
|
|
60
|
+
getSessionPrompt: bm.getSessionPrompt,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
} catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|