recallnest 1.4.1 → 2.2.0

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 (166) hide show
  1. package/.dockerignore +11 -0
  2. package/CLAUDE.md +7 -3
  3. package/Dockerfile +20 -0
  4. package/README.md +202 -130
  5. package/README_CN.md +263 -190
  6. package/ROADMAP.md +128 -81
  7. package/SKILL.md +250 -0
  8. package/assets/dashboard.png +0 -0
  9. package/assets/knowledge-graph.png +0 -0
  10. package/assets/screenshots/ui-full.png +0 -0
  11. package/assets/screenshots/ui-overview.png +0 -0
  12. package/assets/screenshots/ui-search.png +0 -0
  13. package/assets/ui/app.js +227 -3
  14. package/assets/ui/index.html +81 -11
  15. package/assets/ui/styles.css +178 -2
  16. package/bin/recallnest.mjs +62 -0
  17. package/bun.lock +337 -0
  18. package/daily-recall.sh +23 -0
  19. package/daily-reflections/2026-04-02.md +35 -0
  20. package/daily-reflections/2026-04-03.md +37 -0
  21. package/daily-reflections/2026-04-04.md +37 -0
  22. package/daily-reflections/2026-04-05.md +35 -0
  23. package/daily-reflections/2026-04-06.md +36 -0
  24. package/daily-reflections/2026-04-07.md +35 -0
  25. package/daily-reflections/2026-04-08.md +35 -0
  26. package/daily-reflections/2026-04-12.md +35 -0
  27. package/data/alias-map.example.json +22 -0
  28. package/docs/plans/2026-04-01-recall-boost.md +100 -0
  29. package/docs/superpowers/plans/2026-04-11-constructive-retrieval-emotion.md +1195 -0
  30. package/docs/superpowers/specs/2026-04-11-constructive-retrieval-emotion-design.md +398 -0
  31. package/glama.json +6 -0
  32. package/integrations/setup-all.sh +31 -0
  33. package/package.json +10 -3
  34. package/plan-philosophy-upgrades.md +363 -0
  35. package/plan-product-upgrades.md +155 -0
  36. package/plan.md +140 -0
  37. package/research-memory-evolution.md +440 -0
  38. package/research-rl-agent-memory.md +330 -0
  39. package/scripts/backfill-anchors.ts +130 -0
  40. package/scripts/diagnose-vector-search.ts +132 -0
  41. package/scripts/migrate-language.ts +75 -0
  42. package/scripts/verify-emotion-decay.ts +148 -0
  43. package/src/__tests__/activity-counter.test.ts +96 -0
  44. package/src/__tests__/admission-control.test.ts +206 -0
  45. package/src/__tests__/anchor-generator.test.ts +67 -0
  46. package/src/__tests__/babel-integration.test.ts +139 -0
  47. package/src/__tests__/capture-engine.test.ts +5 -3
  48. package/src/__tests__/cascade-forget.test.ts +1 -1
  49. package/src/__tests__/cc8-cc1-essential-context.test.ts +298 -0
  50. package/src/__tests__/circuit-breaker.test.ts +99 -0
  51. package/src/__tests__/cluster-consolidation.test.ts +712 -0
  52. package/src/__tests__/confidence-metadata.test.ts +125 -0
  53. package/src/__tests__/confidence-tracker.test.ts +53 -0
  54. package/src/__tests__/consolidation-engine.test.ts +1 -1
  55. package/src/__tests__/constructive-retrieval.test.ts +509 -0
  56. package/src/__tests__/context-collapse-renderer.test.ts +214 -0
  57. package/src/__tests__/context-reconstructor.test.ts +65 -0
  58. package/src/__tests__/conversation-importer.test.ts +692 -0
  59. package/src/__tests__/data-checkup.test.ts +225 -0
  60. package/src/__tests__/decay-exemption.test.ts +84 -0
  61. package/src/__tests__/dedup-guard.test.ts +39 -0
  62. package/src/__tests__/distill-lock.test.ts +176 -0
  63. package/src/__tests__/dream-pipeline.test.ts +232 -0
  64. package/src/__tests__/emotion-decay.test.ts +46 -0
  65. package/src/__tests__/emotion-detector.test.ts +56 -0
  66. package/src/__tests__/emotion-phase1-integration.test.ts +153 -0
  67. package/src/__tests__/emotion-retrieval.test.ts +56 -0
  68. package/src/__tests__/ethics-layer.test.ts +476 -0
  69. package/src/__tests__/feature-flags-integration.test.ts +54 -0
  70. package/src/__tests__/frequency-tracker.test.ts +113 -0
  71. package/src/__tests__/graph-export.test.ts +489 -0
  72. package/src/__tests__/interference-detector.test.ts +203 -0
  73. package/src/__tests__/mcp-efficiency.test.ts +288 -0
  74. package/src/__tests__/memory-evolution-b2-b3-e1.test.ts +243 -0
  75. package/src/__tests__/memory-evolution.test.ts +271 -0
  76. package/src/__tests__/memory-lint.test.ts +271 -0
  77. package/src/__tests__/narrative-architecture.test.ts +456 -0
  78. package/src/__tests__/pending-review.test.ts +77 -0
  79. package/src/__tests__/post-retrieval-filter.test.ts +178 -0
  80. package/src/__tests__/prediction-engine.test.ts +349 -0
  81. package/src/__tests__/predictive-prospective-memory.test.ts +416 -0
  82. package/src/__tests__/preference-slots.test.ts +99 -0
  83. package/src/__tests__/query-expander-alias.test.ts +89 -0
  84. package/src/__tests__/recall-governor.test.ts +227 -0
  85. package/src/__tests__/resume-context.test.ts +156 -1
  86. package/src/__tests__/salience-scoring.test.ts +106 -0
  87. package/src/__tests__/session-distiller.test.ts +611 -0
  88. package/src/__tests__/skill-engine.test.ts +546 -0
  89. package/src/__tests__/skill-promotion.test.ts +349 -0
  90. package/src/__tests__/supersede-chain.test.ts +154 -0
  91. package/src/__tests__/temporal-validity.test.ts +160 -0
  92. package/src/__tests__/topic-tag.test.ts +113 -0
  93. package/src/__tests__/trusted-memory.test.ts +828 -0
  94. package/src/__tests__/write-verifier.test.ts +122 -0
  95. package/src/access-tracker.ts +3 -2
  96. package/src/activity-counter.ts +109 -0
  97. package/src/adaptive-retrieval.ts +6 -5
  98. package/src/admission-control.ts +163 -0
  99. package/src/anchor-generator.ts +80 -0
  100. package/src/api-server.ts +10 -0
  101. package/src/audit-log.ts +122 -0
  102. package/src/auto-gc.ts +98 -29
  103. package/src/auto-recall.ts +33 -4
  104. package/src/capture-engine.ts +372 -23
  105. package/src/cascade-forget.ts +3 -2
  106. package/src/cli.ts +107 -5
  107. package/src/compat.ts +22 -0
  108. package/src/confidence-tracker.ts +210 -47
  109. package/src/conflict-store.ts +12 -3
  110. package/src/consolidation-engine.ts +296 -8
  111. package/src/context-collapse-renderer.ts +222 -0
  112. package/src/context-composer-stable-selection.ts +5 -1
  113. package/src/context-composer.ts +239 -4
  114. package/src/context-reconstructor.ts +404 -0
  115. package/src/conversation-importer.ts +441 -0
  116. package/src/data-checkup.ts +232 -0
  117. package/src/decay-engine.ts +84 -0
  118. package/src/distill-lock.ts +122 -0
  119. package/src/doctor.ts +8 -10
  120. package/src/dream-pipeline.ts +227 -0
  121. package/src/emotion-detector.ts +90 -0
  122. package/src/eval.ts +3 -2
  123. package/src/forget-engine.ts +269 -0
  124. package/src/frequency-tracker.ts +175 -0
  125. package/src/graph-export.ts +644 -0
  126. package/src/ingest.ts +36 -6
  127. package/src/interference-detector.ts +187 -0
  128. package/src/kg-extractor.ts +15 -24
  129. package/src/kg-store.ts +2 -1
  130. package/src/language-hook.ts +67 -0
  131. package/src/llm-client.ts +289 -6
  132. package/src/mcp-server.ts +707 -80
  133. package/src/memory-assets.ts +22 -5
  134. package/src/memory-evolution.ts +365 -0
  135. package/src/memory-lint.ts +429 -0
  136. package/src/memory-output.ts +69 -0
  137. package/src/memory-schema.ts +74 -2
  138. package/src/narrative-schema.ts +79 -0
  139. package/src/narrative-tagger.ts +164 -0
  140. package/src/pii-detector.ts +98 -0
  141. package/src/post-retrieval-filter.ts +76 -0
  142. package/src/prediction-engine.ts +253 -0
  143. package/src/preference-slots.ts +65 -2
  144. package/src/prospective-memory.ts +215 -0
  145. package/src/query-expander.ts +67 -0
  146. package/src/recall-governor.ts +183 -0
  147. package/src/retention-policy.ts +90 -0
  148. package/src/retriever.ts +828 -55
  149. package/src/rif.ts +34 -8
  150. package/src/runtime-config.ts +23 -8
  151. package/src/scope-inventory.ts +4 -3
  152. package/src/scope-policy.ts +4 -0
  153. package/src/session-distiller.ts +307 -0
  154. package/src/session-output.ts +40 -0
  155. package/src/session-schema.ts +37 -0
  156. package/src/session-store.ts +7 -2
  157. package/src/skill-engine.ts +208 -0
  158. package/src/skill-promotion.ts +293 -0
  159. package/src/skill-schema.ts +39 -0
  160. package/src/store.ts +96 -10
  161. package/src/topic-tag.ts +66 -0
  162. package/src/tracker.ts +3 -1
  163. package/src/ui-server.ts +80 -8
  164. package/src/workflow-observation-scope-review.ts +2 -1
  165. package/src/workflow-observation-store.ts +2 -1
  166. package/src/write-verifier.ts +126 -0
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
+ }