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.
Files changed (233) hide show
  1. package/.env +4 -0
  2. package/CLAUDE.md +7 -3
  3. package/README.md +202 -130
  4. package/README_CN.md +263 -190
  5. package/bin/recallnest.mjs +62 -0
  6. package/config.json +55 -0
  7. package/daily-recall.sh +23 -0
  8. package/integrations/setup-all.sh +31 -0
  9. package/logs/daily-recall.error.log +0 -0
  10. package/logs/daily-recall.log +30 -0
  11. package/logs/ingest-2026-04-11.log +110 -0
  12. package/logs/ingest-2026-04-12.log +90 -0
  13. package/logs/launchagent-stderr.log +0 -0
  14. package/logs/launchagent-stdout.log +0 -0
  15. package/package.json +10 -3
  16. package/src/access-tracker.ts +3 -2
  17. package/src/activity-counter.ts +109 -0
  18. package/src/adaptive-retrieval.ts +6 -5
  19. package/src/admission-control.ts +163 -0
  20. package/src/anchor-generator.ts +80 -0
  21. package/src/api-server.ts +10 -0
  22. package/src/audit-log.ts +122 -0
  23. package/src/auto-gc.ts +98 -29
  24. package/src/auto-recall.ts +33 -4
  25. package/src/capture-engine.ts +372 -23
  26. package/src/cascade-forget.ts +3 -2
  27. package/src/cli.ts +107 -5
  28. package/src/compat.ts +22 -0
  29. package/src/confidence-tracker.ts +210 -47
  30. package/src/conflict-store.ts +12 -3
  31. package/src/consolidation-engine.ts +296 -8
  32. package/src/context-collapse-renderer.ts +222 -0
  33. package/src/context-composer-stable-selection.ts +5 -1
  34. package/src/context-composer.ts +239 -4
  35. package/src/context-reconstructor.ts +404 -0
  36. package/src/conversation-importer.ts +441 -0
  37. package/src/data-checkup.ts +232 -0
  38. package/src/decay-engine.ts +84 -0
  39. package/src/distill-lock.ts +122 -0
  40. package/src/doctor.ts +8 -10
  41. package/src/dream-pipeline.ts +227 -0
  42. package/src/emotion-detector.ts +90 -0
  43. package/src/eval.ts +3 -2
  44. package/src/forget-engine.ts +269 -0
  45. package/src/frequency-tracker.ts +175 -0
  46. package/src/graph-export.ts +644 -0
  47. package/src/ingest.ts +36 -6
  48. package/src/interference-detector.ts +187 -0
  49. package/src/kg-extractor.ts +15 -24
  50. package/src/kg-store.ts +2 -1
  51. package/src/language-hook.ts +67 -0
  52. package/src/llm-client.ts +289 -6
  53. package/src/mcp-server.ts +707 -80
  54. package/src/memory-assets.ts +22 -5
  55. package/src/memory-evolution.ts +365 -0
  56. package/src/memory-lint.ts +429 -0
  57. package/src/memory-output.ts +69 -0
  58. package/src/memory-schema.ts +74 -2
  59. package/src/narrative-schema.ts +79 -0
  60. package/src/narrative-tagger.ts +164 -0
  61. package/src/pii-detector.ts +98 -0
  62. package/src/post-retrieval-filter.ts +76 -0
  63. package/src/prediction-engine.ts +253 -0
  64. package/src/preference-slots.ts +65 -2
  65. package/src/prospective-memory.ts +215 -0
  66. package/src/query-expander.ts +67 -0
  67. package/src/recall-governor.ts +183 -0
  68. package/src/retention-policy.ts +90 -0
  69. package/src/retriever.ts +828 -55
  70. package/src/rif.ts +34 -8
  71. package/src/runtime-config.ts +23 -8
  72. package/src/scope-inventory.ts +4 -3
  73. package/src/scope-policy.ts +4 -0
  74. package/src/session-distiller.ts +307 -0
  75. package/src/session-output.ts +40 -0
  76. package/src/session-schema.ts +37 -0
  77. package/src/session-store.ts +7 -2
  78. package/src/skill-engine.ts +208 -0
  79. package/src/skill-promotion.ts +293 -0
  80. package/src/skill-schema.ts +39 -0
  81. package/src/store.ts +96 -10
  82. package/src/topic-tag.ts +66 -0
  83. package/src/tracker.ts +3 -1
  84. package/src/ui-server.ts +80 -8
  85. package/src/workflow-observation-scope-review.ts +2 -1
  86. package/src/workflow-observation-store.ts +2 -1
  87. package/src/write-verifier.ts +126 -0
  88. package/.claude-plugin/CLAUDE.md +0 -29
  89. package/.claude-plugin/marketplace.json +0 -18
  90. package/.github/workflows/ci.yml +0 -28
  91. package/.mcp.json +0 -9
  92. package/CHANGELOG.md +0 -51
  93. package/DEMO.md +0 -94
  94. package/EVAL.md +0 -89
  95. package/FAILURES.md +0 -75
  96. package/OPERATIONS.md +0 -59
  97. package/ROADMAP.md +0 -198
  98. package/assets/recallnest-workbench.png +0 -0
  99. package/assets/ui/app.js +0 -807
  100. package/assets/ui/index.html +0 -154
  101. package/assets/ui/styles.css +0 -605
  102. package/assets/wechat-qr.jpg +0 -0
  103. package/daily-reflections/2026-03-05.md +0 -15
  104. package/daily-reflections/2026-03-06.md +0 -15
  105. package/daily-reflections/2026-03-07.md +0 -7
  106. package/daily-reflections/2026-03-08.md +0 -34
  107. package/daily-reflections/2026-03-09.md +0 -35
  108. package/daily-reflections/2026-03-10.md +0 -36
  109. package/daily-reflections/2026-03-11.md +0 -35
  110. package/daily-reflections/2026-03-12.md +0 -36
  111. package/daily-reflections/2026-03-13.md +0 -35
  112. package/daily-reflections/2026-03-14.md +0 -35
  113. package/daily-reflections/2026-03-15.md +0 -35
  114. package/daily-reflections/2026-03-16.md +0 -41
  115. package/daily-reflections/2026-03-17.md +0 -10
  116. package/daily-reflections/2026-03-18.md +0 -10
  117. package/daily-reflections/2026-03-19.md +0 -10
  118. package/daily-reflections/2026-03-20.md +0 -10
  119. package/daily-reflections/2026-03-21.md +0 -10
  120. package/daily-reflections/2026-03-22.md +0 -10
  121. package/daily-reflections/2026-03-23.md +0 -10
  122. package/daily-reflections/2026-03-24.md +0 -10
  123. package/daily-reflections/2026-03-25.md +0 -32
  124. package/daily-reflections/2026-03-26.md +0 -37
  125. package/daily-reflections/2026-03-27.md +0 -35
  126. package/daily-reflections/2026-03-28.md +0 -35
  127. package/daily-reflections/2026-03-29.md +0 -35
  128. package/daily-reflections/2026-03-30.md +0 -36
  129. package/daily-reflections/2026-03-31.md +0 -35
  130. package/daily-reflections/2026-04-01.md +0 -35
  131. package/docs/api-reference.md +0 -1291
  132. package/docs/architecture.md +0 -168
  133. package/docs/memory-boundary-contract.md +0 -226
  134. package/docs/memory-categories.md +0 -119
  135. package/docs/pipeline-audit-ultramemory.md +0 -177
  136. package/docs/plans/2026-03-08-query-expansion.md +0 -270
  137. package/docs/plans/2026-03-16-continuity-layer.md +0 -196
  138. package/docs/plans/mcp-tiering-plan.md +0 -126
  139. package/docs/research/recall-quality-audit-2026-03-22.md +0 -153
  140. package/docs/self-evolution.md +0 -198
  141. package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +0 -782
  142. package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +0 -77
  143. package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +0 -95
  144. package/eval/continuity/results-2026-03-16.md +0 -117
  145. package/eval/continuity/results-2026-03-19.md +0 -453
  146. package/eval/continuity/runbook.md +0 -110
  147. package/eval/continuity/seed-cases.md +0 -54
  148. package/eval/reports/2026-03-06-abstract-query-baseline.md +0 -96
  149. package/eval/reports/2026-03-06-aws-ssh-baseline.md +0 -82
  150. package/eval/reports/2026-03-06-baseline.md +0 -82
  151. package/eval/reports/query-expansion.md +0 -138
  152. package/lm +0 -19
  153. package/plan-v1.2-distributable.md +0 -105
  154. package/scripts/archive-distilled-junk.ts +0 -116
  155. package/scripts/backfill-l0l1l2.ts +0 -98
  156. package/scripts/blind-scan.ts +0 -588
  157. package/scripts/cleanup-step1-exact-dups.ts +0 -117
  158. package/scripts/cleanup-step2-template-dups.ts +0 -113
  159. package/scripts/cleanup-step3-stale-content.ts +0 -149
  160. package/scripts/cleanup-step4-orphan-scopes.ts +0 -131
  161. package/scripts/delete-distilled-junk.ts +0 -88
  162. package/scripts/distill-facts.ts +0 -305
  163. package/scripts/health-check.ts +0 -343
  164. package/scripts/incremental-ingest.sh +0 -71
  165. package/scripts/mlp-health-check.ts +0 -382
  166. package/scripts/pull-claude-desktop.ts +0 -515
  167. package/scripts/re-ingest-cc.sh +0 -66
  168. package/scripts/rebalance-memory-health.ts +0 -181
  169. package/scripts/run-desktop-ingest.sh +0 -11
  170. package/scripts/start-server.sh +0 -12
  171. package/scripts/viz-dashboard.html +0 -423
  172. package/scripts/viz-export.ts +0 -703
  173. package/src/__tests__/access-tracker-novelty.test.ts +0 -228
  174. package/src/__tests__/auto-consolidation.test.ts +0 -168
  175. package/src/__tests__/auto-recall.test.ts +0 -131
  176. package/src/__tests__/batch-dedup.test.ts +0 -65
  177. package/src/__tests__/capture-engine.test.ts +0 -1142
  178. package/src/__tests__/capture-heuristic.test.ts +0 -171
  179. package/src/__tests__/cascade-forget.test.ts +0 -210
  180. package/src/__tests__/category-min-scores.test.ts +0 -33
  181. package/src/__tests__/confidence-tracker.test.ts +0 -196
  182. package/src/__tests__/conflict-advisor.test.ts +0 -206
  183. package/src/__tests__/conflict-escalation.test.ts +0 -149
  184. package/src/__tests__/conflict-lifecycle.test.ts +0 -83
  185. package/src/__tests__/conflict-output.test.ts +0 -85
  186. package/src/__tests__/conflict-store.test.ts +0 -374
  187. package/src/__tests__/consolidation-engine.test.ts +0 -181
  188. package/src/__tests__/context-composer-pins.test.ts +0 -64
  189. package/src/__tests__/context-composer-task-selection.test.ts +0 -24
  190. package/src/__tests__/context-renderer.test.ts +0 -91
  191. package/src/__tests__/continuity-eval.test.ts +0 -275
  192. package/src/__tests__/continuity-schema.test.ts +0 -97
  193. package/src/__tests__/core-recall-eval.test.ts +0 -235
  194. package/src/__tests__/core-summary.test.ts +0 -109
  195. package/src/__tests__/dedup-actions.test.ts +0 -58
  196. package/src/__tests__/deterministic-id.test.ts +0 -39
  197. package/src/__tests__/doctor.test.ts +0 -303
  198. package/src/__tests__/drill-down.test.ts +0 -108
  199. package/src/__tests__/embedder.test.ts +0 -122
  200. package/src/__tests__/entity-resolver.test.ts +0 -58
  201. package/src/__tests__/event-segmenter.test.ts +0 -153
  202. package/src/__tests__/hotness-blend.test.ts +0 -76
  203. package/src/__tests__/ingest.test.ts +0 -360
  204. package/src/__tests__/kg-extractor.test.ts +0 -184
  205. package/src/__tests__/kg-store.test.ts +0 -198
  206. package/src/__tests__/llm-consolidation.test.ts +0 -226
  207. package/src/__tests__/memory-boundaries.test.ts +0 -237
  208. package/src/__tests__/memory-health-rebalance.test.ts +0 -144
  209. package/src/__tests__/memory-output.test.ts +0 -215
  210. package/src/__tests__/multi-vector.test.ts +0 -242
  211. package/src/__tests__/ppr-traversal.test.ts +0 -256
  212. package/src/__tests__/preference-guard-smoke.test.ts +0 -211
  213. package/src/__tests__/preference-matcher.test.ts +0 -230
  214. package/src/__tests__/preference-slots.test.ts +0 -106
  215. package/src/__tests__/prospective-memory.test.ts +0 -283
  216. package/src/__tests__/query-entity-detector.test.ts +0 -171
  217. package/src/__tests__/query-expander.test.ts +0 -34
  218. package/src/__tests__/recall-mode.test.ts +0 -64
  219. package/src/__tests__/result-synthesizer.test.ts +0 -198
  220. package/src/__tests__/resume-context.test.ts +0 -4743
  221. package/src/__tests__/retrieval-trace.test.ts +0 -213
  222. package/src/__tests__/retriever-boundary-weight.test.ts +0 -136
  223. package/src/__tests__/retroactive-boost.test.ts +0 -223
  224. package/src/__tests__/scope-inventory.test.ts +0 -200
  225. package/src/__tests__/scope-policy.test.ts +0 -69
  226. package/src/__tests__/session-checkpoint.test.ts +0 -259
  227. package/src/__tests__/session-gc.test.ts +0 -215
  228. package/src/__tests__/store-scope-audit.test.ts +0 -81
  229. package/src/__tests__/temporal-parser.test.ts +0 -241
  230. package/src/__tests__/term-registry.test.ts +0 -121
  231. package/src/__tests__/tool-output-compressor.test.ts +0 -133
  232. package/src/__tests__/version-manager.test.ts +0 -213
  233. 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
- pending = JSON.parse(readFileSync(PENDING_EXTRACTION_FILE, "utf-8"));
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
- writeFileSync(PENDING_EXTRACTION_FILE, JSON.stringify(pending, null, 2));
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
- pending = JSON.parse(readFileSync(PENDING_EXTRACTION_FILE, "utf-8"));
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: extractions[j].l1 || extractions[j].l0,
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
- writeFileSync(PENDING_EXTRACTION_FILE, "[]");
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
+ }
@@ -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 KG_EXTRACT_SYSTEM = `You are a knowledge graph extraction assistant. Extract (subject, predicate, object) triples from text. Respond with valid JSON only.`;
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
- - Extract relationships between named entities, concepts, tools, people, projects
99
- - Use simple predicates: uses, created_by, works_with, friend_of, depends_on, located_in, member_of, prefers, knows, discussed, mentioned_in, related_to, is_a, has, manages, built, wrote, lives_in
100
- - Assign confidence (0-1) based on how explicit the relationship is
101
- - Skip vague or speculative relationships (confidence < 0.5)
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: "RecallNest depends on LanceDB for vector storage and uses OpenAI for embeddings"
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 prompt = buildKGExtractionPrompt(text);
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
- KG_EXTRACT_SYSTEM,
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
+ }