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.
Files changed (261) hide show
  1. package/.claude-plugin/CLAUDE.md +29 -0
  2. package/.claude-plugin/marketplace.json +18 -0
  3. package/.env.example +3 -0
  4. package/.github/workflows/ci.yml +28 -0
  5. package/.mcp.json +9 -0
  6. package/CHANGELOG.md +51 -0
  7. package/CLAUDE.md +42 -0
  8. package/DEMO.md +94 -0
  9. package/EVAL.md +89 -0
  10. package/FAILURES.md +75 -0
  11. package/LICENSE +21 -0
  12. package/OPERATIONS.md +59 -0
  13. package/README.md +427 -0
  14. package/README_CN.md +425 -0
  15. package/ROADMAP.md +198 -0
  16. package/assets/recallnest-workbench.png +0 -0
  17. package/assets/ui/app.js +807 -0
  18. package/assets/ui/index.html +154 -0
  19. package/assets/ui/styles.css +605 -0
  20. package/assets/wechat-qr.jpg +0 -0
  21. package/config.json.example +38 -0
  22. package/daily-recall.sh +87 -0
  23. package/daily-reflections/2026-03-05.md +15 -0
  24. package/daily-reflections/2026-03-06.md +15 -0
  25. package/daily-reflections/2026-03-07.md +7 -0
  26. package/daily-reflections/2026-03-08.md +34 -0
  27. package/daily-reflections/2026-03-09.md +35 -0
  28. package/daily-reflections/2026-03-10.md +36 -0
  29. package/daily-reflections/2026-03-11.md +35 -0
  30. package/daily-reflections/2026-03-12.md +36 -0
  31. package/daily-reflections/2026-03-13.md +35 -0
  32. package/daily-reflections/2026-03-14.md +35 -0
  33. package/daily-reflections/2026-03-15.md +35 -0
  34. package/daily-reflections/2026-03-16.md +41 -0
  35. package/daily-reflections/2026-03-17.md +10 -0
  36. package/daily-reflections/2026-03-18.md +10 -0
  37. package/daily-reflections/2026-03-19.md +10 -0
  38. package/daily-reflections/2026-03-20.md +10 -0
  39. package/daily-reflections/2026-03-21.md +10 -0
  40. package/daily-reflections/2026-03-22.md +10 -0
  41. package/daily-reflections/2026-03-23.md +10 -0
  42. package/daily-reflections/2026-03-24.md +10 -0
  43. package/daily-reflections/2026-03-25.md +32 -0
  44. package/daily-reflections/2026-03-26.md +37 -0
  45. package/daily-reflections/2026-03-27.md +35 -0
  46. package/daily-reflections/2026-03-28.md +35 -0
  47. package/daily-reflections/2026-03-29.md +35 -0
  48. package/daily-reflections/2026-03-30.md +36 -0
  49. package/daily-reflections/2026-03-31.md +35 -0
  50. package/daily-reflections/2026-04-01.md +35 -0
  51. package/docs/api-reference.md +1291 -0
  52. package/docs/architecture.md +168 -0
  53. package/docs/memory-boundary-contract.md +226 -0
  54. package/docs/memory-categories.md +119 -0
  55. package/docs/pipeline-audit-ultramemory.md +177 -0
  56. package/docs/plans/2026-03-08-query-expansion.md +270 -0
  57. package/docs/plans/2026-03-16-continuity-layer.md +196 -0
  58. package/docs/plans/mcp-tiering-plan.md +126 -0
  59. package/docs/research/recall-quality-audit-2026-03-22.md +153 -0
  60. package/docs/self-evolution.md +198 -0
  61. package/docs/superpowers/plans/2026-03-19-ingest-pipeline-upgrade.md +782 -0
  62. package/docs/superpowers/specs/2026-03-19-ingest-pipeline-upgrade-design.md +77 -0
  63. package/docs/superpowers/specs/2026-03-23-pull-claude-desktop-design.md +95 -0
  64. package/eval/cases.json +85 -0
  65. package/eval/continuity/case-seeds.json +90 -0
  66. package/eval/continuity/cases.json +302 -0
  67. package/eval/continuity/memory-seeds.json +26 -0
  68. package/eval/continuity/pattern-seeds.json +100 -0
  69. package/eval/continuity/results-2026-03-16.md +117 -0
  70. package/eval/continuity/results-2026-03-19.md +453 -0
  71. package/eval/continuity/runbook.md +110 -0
  72. package/eval/continuity/seed-cases.md +54 -0
  73. package/eval/reports/2026-03-06-abstract-query-baseline.md +96 -0
  74. package/eval/reports/2026-03-06-aws-ssh-baseline.md +82 -0
  75. package/eval/reports/2026-03-06-baseline.md +82 -0
  76. package/eval/reports/query-expansion.md +138 -0
  77. package/integrations/README.md +57 -0
  78. package/integrations/claude-code/README.md +133 -0
  79. package/integrations/claude-code/claude-md-snippet.md +31 -0
  80. package/integrations/claude-code/setup.sh +66 -0
  81. package/integrations/claude-code/smoke-continuity.sh +452 -0
  82. package/integrations/codex/README.md +49 -0
  83. package/integrations/codex/agents-md-snippet.md +31 -0
  84. package/integrations/codex/setup.sh +58 -0
  85. package/integrations/examples/claude-agent-sdk/memory-agent.ts +182 -0
  86. package/integrations/examples/langchain/memory-chain.py +111 -0
  87. package/integrations/examples/openai-agents-sdk/memory-agent.py +109 -0
  88. package/integrations/gemini-cli/README.md +57 -0
  89. package/integrations/gemini-cli/gemini-md-snippet.md +31 -0
  90. package/integrations/gemini-cli/setup.sh +64 -0
  91. package/integrations/shared/install-managed-block.sh +51 -0
  92. package/lm +19 -0
  93. package/package.json +50 -0
  94. package/plan-v1.2-distributable.md +105 -0
  95. package/scripts/archive-distilled-junk.ts +116 -0
  96. package/scripts/backfill-l0l1l2.ts +98 -0
  97. package/scripts/blind-scan.ts +588 -0
  98. package/scripts/cleanup-step1-exact-dups.ts +117 -0
  99. package/scripts/cleanup-step2-template-dups.ts +113 -0
  100. package/scripts/cleanup-step3-stale-content.ts +149 -0
  101. package/scripts/cleanup-step4-orphan-scopes.ts +131 -0
  102. package/scripts/delete-distilled-junk.ts +88 -0
  103. package/scripts/distill-facts.ts +305 -0
  104. package/scripts/health-check.ts +343 -0
  105. package/scripts/incremental-ingest.sh +71 -0
  106. package/scripts/mlp-health-check.ts +382 -0
  107. package/scripts/pull-claude-desktop.ts +515 -0
  108. package/scripts/re-ingest-cc.sh +66 -0
  109. package/scripts/rebalance-memory-health.ts +181 -0
  110. package/scripts/run-desktop-ingest.sh +11 -0
  111. package/scripts/start-server.sh +12 -0
  112. package/scripts/viz-dashboard.html +423 -0
  113. package/scripts/viz-export.ts +703 -0
  114. package/src/__tests__/access-tracker-novelty.test.ts +228 -0
  115. package/src/__tests__/auto-consolidation.test.ts +168 -0
  116. package/src/__tests__/auto-recall.test.ts +131 -0
  117. package/src/__tests__/batch-dedup.test.ts +65 -0
  118. package/src/__tests__/capture-engine.test.ts +1142 -0
  119. package/src/__tests__/capture-heuristic.test.ts +171 -0
  120. package/src/__tests__/cascade-forget.test.ts +210 -0
  121. package/src/__tests__/category-min-scores.test.ts +33 -0
  122. package/src/__tests__/confidence-tracker.test.ts +196 -0
  123. package/src/__tests__/conflict-advisor.test.ts +206 -0
  124. package/src/__tests__/conflict-escalation.test.ts +149 -0
  125. package/src/__tests__/conflict-lifecycle.test.ts +83 -0
  126. package/src/__tests__/conflict-output.test.ts +85 -0
  127. package/src/__tests__/conflict-store.test.ts +374 -0
  128. package/src/__tests__/consolidation-engine.test.ts +181 -0
  129. package/src/__tests__/context-composer-pins.test.ts +64 -0
  130. package/src/__tests__/context-composer-task-selection.test.ts +24 -0
  131. package/src/__tests__/context-renderer.test.ts +91 -0
  132. package/src/__tests__/continuity-eval.test.ts +275 -0
  133. package/src/__tests__/continuity-schema.test.ts +97 -0
  134. package/src/__tests__/core-recall-eval.test.ts +235 -0
  135. package/src/__tests__/core-summary.test.ts +109 -0
  136. package/src/__tests__/dedup-actions.test.ts +58 -0
  137. package/src/__tests__/deterministic-id.test.ts +39 -0
  138. package/src/__tests__/doctor.test.ts +303 -0
  139. package/src/__tests__/drill-down.test.ts +108 -0
  140. package/src/__tests__/embedder.test.ts +122 -0
  141. package/src/__tests__/entity-resolver.test.ts +58 -0
  142. package/src/__tests__/event-segmenter.test.ts +153 -0
  143. package/src/__tests__/hotness-blend.test.ts +76 -0
  144. package/src/__tests__/ingest.test.ts +360 -0
  145. package/src/__tests__/kg-extractor.test.ts +184 -0
  146. package/src/__tests__/kg-store.test.ts +198 -0
  147. package/src/__tests__/llm-consolidation.test.ts +226 -0
  148. package/src/__tests__/memory-boundaries.test.ts +237 -0
  149. package/src/__tests__/memory-health-rebalance.test.ts +144 -0
  150. package/src/__tests__/memory-output.test.ts +215 -0
  151. package/src/__tests__/multi-vector.test.ts +242 -0
  152. package/src/__tests__/ppr-traversal.test.ts +256 -0
  153. package/src/__tests__/preference-guard-smoke.test.ts +211 -0
  154. package/src/__tests__/preference-matcher.test.ts +230 -0
  155. package/src/__tests__/preference-slots.test.ts +106 -0
  156. package/src/__tests__/prospective-memory.test.ts +283 -0
  157. package/src/__tests__/query-entity-detector.test.ts +171 -0
  158. package/src/__tests__/query-expander.test.ts +34 -0
  159. package/src/__tests__/recall-mode.test.ts +64 -0
  160. package/src/__tests__/result-synthesizer.test.ts +198 -0
  161. package/src/__tests__/resume-context.test.ts +4743 -0
  162. package/src/__tests__/retrieval-trace.test.ts +213 -0
  163. package/src/__tests__/retriever-boundary-weight.test.ts +136 -0
  164. package/src/__tests__/retroactive-boost.test.ts +223 -0
  165. package/src/__tests__/scope-inventory.test.ts +200 -0
  166. package/src/__tests__/scope-policy.test.ts +69 -0
  167. package/src/__tests__/session-checkpoint.test.ts +259 -0
  168. package/src/__tests__/session-gc.test.ts +215 -0
  169. package/src/__tests__/store-scope-audit.test.ts +81 -0
  170. package/src/__tests__/temporal-parser.test.ts +241 -0
  171. package/src/__tests__/term-registry.test.ts +121 -0
  172. package/src/__tests__/tool-output-compressor.test.ts +133 -0
  173. package/src/__tests__/version-manager.test.ts +213 -0
  174. package/src/__tests__/workflow-observation.test.ts +400 -0
  175. package/src/access-tracker.ts +316 -0
  176. package/src/adaptive-retrieval.ts +64 -0
  177. package/src/api-server.ts +667 -0
  178. package/src/asset-sync.ts +79 -0
  179. package/src/auto-consolidation.ts +133 -0
  180. package/src/auto-gc.ts +124 -0
  181. package/src/auto-recall.ts +95 -0
  182. package/src/capture-engine.ts +931 -0
  183. package/src/capture-heuristic.ts +177 -0
  184. package/src/cascade-forget.ts +135 -0
  185. package/src/chunker.ts +253 -0
  186. package/src/cli.ts +1917 -0
  187. package/src/confidence-tracker.ts +166 -0
  188. package/src/conflict-advisor.ts +433 -0
  189. package/src/conflict-engine.ts +181 -0
  190. package/src/conflict-escalation.ts +138 -0
  191. package/src/conflict-lifecycle.ts +67 -0
  192. package/src/conflict-output.ts +218 -0
  193. package/src/conflict-schema.ts +130 -0
  194. package/src/conflict-store.ts +127 -0
  195. package/src/consolidation-engine.ts +256 -0
  196. package/src/context-composer-pins.ts +183 -0
  197. package/src/context-composer-queries.ts +121 -0
  198. package/src/context-composer-scope.ts +56 -0
  199. package/src/context-composer-stable-selection.ts +200 -0
  200. package/src/context-composer-stable.ts +217 -0
  201. package/src/context-composer-task-fallbacks.ts +36 -0
  202. package/src/context-composer-task-ranking.ts +197 -0
  203. package/src/context-composer-task-results.ts +137 -0
  204. package/src/context-composer-task-selection.ts +342 -0
  205. package/src/context-composer-text.ts +57 -0
  206. package/src/context-composer.ts +300 -0
  207. package/src/context-renderer.ts +179 -0
  208. package/src/decay-engine.ts +189 -0
  209. package/src/doctor.ts +543 -0
  210. package/src/embedder.ts +557 -0
  211. package/src/entity-resolver.ts +131 -0
  212. package/src/eval.ts +632 -0
  213. package/src/event-segmenter.ts +216 -0
  214. package/src/ingest.ts +1822 -0
  215. package/src/kg-extractor.ts +217 -0
  216. package/src/kg-store.ts +353 -0
  217. package/src/legacy-scope-review.ts +53 -0
  218. package/src/llm-client.ts +629 -0
  219. package/src/llm-consolidation.ts +168 -0
  220. package/src/mcp-server.ts +1337 -0
  221. package/src/memory-assets.ts +475 -0
  222. package/src/memory-boundaries.ts +354 -0
  223. package/src/memory-health-rebalance.ts +220 -0
  224. package/src/memory-output.ts +474 -0
  225. package/src/memory-schema.ts +184 -0
  226. package/src/multi-vector.ts +234 -0
  227. package/src/noise-filter.ts +155 -0
  228. package/src/ppr-traversal.ts +279 -0
  229. package/src/preference-matcher.ts +174 -0
  230. package/src/preference-slots.ts +259 -0
  231. package/src/prospective-memory.ts +198 -0
  232. package/src/query-entity-detector.ts +207 -0
  233. package/src/query-expander.ts +98 -0
  234. package/src/result-synthesizer.ts +114 -0
  235. package/src/retrieval-profiles.ts +101 -0
  236. package/src/retrieval-trace.ts +94 -0
  237. package/src/retriever.ts +1338 -0
  238. package/src/retroactive-boost.ts +154 -0
  239. package/src/rif.ts +73 -0
  240. package/src/runtime-config.ts +172 -0
  241. package/src/schema-utils.ts +56 -0
  242. package/src/scope-inventory.ts +310 -0
  243. package/src/scope-policy.ts +142 -0
  244. package/src/session-engine.ts +168 -0
  245. package/src/session-output.ts +96 -0
  246. package/src/session-schema.ts +60 -0
  247. package/src/session-store.ts +293 -0
  248. package/src/stderr-log.ts +16 -0
  249. package/src/store.ts +903 -0
  250. package/src/temporal-parser.ts +318 -0
  251. package/src/term-registry.ts +791 -0
  252. package/src/tool-output-compressor.ts +115 -0
  253. package/src/tracker.ts +57 -0
  254. package/src/ui-server.ts +430 -0
  255. package/src/version-manager.ts +172 -0
  256. package/src/workflow-observation-engine.ts +316 -0
  257. package/src/workflow-observation-managed.ts +83 -0
  258. package/src/workflow-observation-output.ts +86 -0
  259. package/src/workflow-observation-schema.ts +87 -0
  260. package/src/workflow-observation-scope-review.ts +409 -0
  261. package/src/workflow-observation-store.ts +69 -0
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Knowledge Graph Triple Extractor
3
+ * Uses LLM few-shot OpenIE to extract (subject, predicate, object) triples from memory text.
4
+ * Gated by RECALLNEST_KG_MODE env var.
5
+ */
6
+
7
+ import type { LLMClient } from "./llm-client.js";
8
+ import type { KGStore } from "./kg-store.js";
9
+ import { logInfo, logWarn } from "./stderr-log.js";
10
+
11
+ // ============================================================================
12
+ // Types
13
+ // ============================================================================
14
+
15
+ export interface RawTriple {
16
+ subject: string;
17
+ predicate: string;
18
+ object: string;
19
+ confidence: number;
20
+ }
21
+
22
+ export interface KGExtractorConfig {
23
+ /** Minimum confidence to keep a triple (0-1). Default: 0.6 */
24
+ minConfidence?: number;
25
+ /** LLM client for extraction */
26
+ llmClient: LLMClient;
27
+ /** KG store for persisting triples */
28
+ kgStore: KGStore;
29
+ }
30
+
31
+ interface LlmExtractionResponse {
32
+ triples: RawTriple[];
33
+ }
34
+
35
+ // ============================================================================
36
+ // Feature Gate
37
+ // ============================================================================
38
+
39
+ /** Check if KG mode is enabled (env var) */
40
+ export function isKGModeEnabled(): boolean {
41
+ return process.env.RECALLNEST_KG_MODE === "true";
42
+ }
43
+
44
+ // ============================================================================
45
+ // Entity Normalization
46
+ // ============================================================================
47
+
48
+ /**
49
+ * Normalize entity names for consistent matching:
50
+ * - Trim whitespace, collapse internal whitespace
51
+ * - Title case for Latin text, CJK unchanged
52
+ * - Strip surrounding quotes/brackets
53
+ */
54
+ export function normalizeEntity(raw: string): string {
55
+ let entity = raw.trim();
56
+
57
+ // Strip surrounding quotes and brackets
58
+ entity = entity.replace(/^[\s"'""''「」『』【】\[\]()()]+/, "");
59
+ entity = entity.replace(/[\s"'""''「」『』【】\[\]()()]+$/, "");
60
+
61
+ // Collapse whitespace
62
+ entity = entity.replace(/\s+/g, " ").trim();
63
+
64
+ if (!entity) return "";
65
+
66
+ // For CJK text, return as-is
67
+ if (/[\u4e00-\u9fff\u3400-\u4dbf]/.test(entity)) {
68
+ return entity;
69
+ }
70
+
71
+ // Title Case for English entities
72
+ return entity
73
+ .toLowerCase()
74
+ .replace(/\b\w/g, (ch) => ch.toUpperCase());
75
+ }
76
+
77
+ /**
78
+ * Normalize predicate to snake_case for consistency.
79
+ */
80
+ export function normalizePredicate(raw: string): string {
81
+ return raw
82
+ .trim()
83
+ .toLowerCase()
84
+ .replace(/[\s-]+/g, "_")
85
+ .replace(/[^a-z0-9_\u4e00-\u9fff]/g, "");
86
+ }
87
+
88
+ // ============================================================================
89
+ // Extraction Prompt
90
+ // ============================================================================
91
+
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.
96
+
97
+ 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}]}
108
+
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
+ }
116
+
117
+ // ============================================================================
118
+ // Extractor
119
+ // ============================================================================
120
+
121
+ export class KGExtractor {
122
+ private readonly minConfidence: number;
123
+ private readonly llm: LLMClient;
124
+ private readonly kgStore: KGStore;
125
+
126
+ constructor(config: KGExtractorConfig) {
127
+ this.minConfidence = config.minConfidence ?? 0.6;
128
+ this.llm = config.llmClient;
129
+ this.kgStore = config.kgStore;
130
+ }
131
+
132
+ /**
133
+ * Extract triples from text and persist to KG store.
134
+ * Returns the number of triples stored.
135
+ */
136
+ async extractAndStore(
137
+ text: string,
138
+ sourceMemoryId: string,
139
+ scope: string,
140
+ ): Promise<number> {
141
+ const rawTriples = await this.extract(text);
142
+ if (rawTriples.length === 0) return 0;
143
+
144
+ const triples = await this.kgStore.createTriples(
145
+ rawTriples.map((t) => ({
146
+ scope,
147
+ subject: t.subject,
148
+ predicate: t.predicate,
149
+ object: t.object,
150
+ confidence: t.confidence,
151
+ source_memory_id: sourceMemoryId,
152
+ source_text: text.slice(0, 500), // Cap source text
153
+ })),
154
+ );
155
+
156
+ logInfo(`[KG] stored ${triples.length} triples from memory ${sourceMemoryId}`);
157
+ return triples.length;
158
+ }
159
+
160
+ /**
161
+ * Extract raw triples from text via LLM. Does not persist.
162
+ */
163
+ async extract(text: string): Promise<RawTriple[]> {
164
+ if (!text || text.length < 10) return [];
165
+
166
+ const prompt = buildKGExtractionPrompt(text);
167
+
168
+ let response: LlmExtractionResponse | null = null;
169
+ try {
170
+ response = await this.llm.chatJson<LlmExtractionResponse>(
171
+ KG_EXTRACT_SYSTEM,
172
+ prompt,
173
+ );
174
+ } catch (err) {
175
+ logWarn(`[KG] LLM extraction failed: ${String(err)}`);
176
+ return [];
177
+ }
178
+
179
+ if (!response?.triples || !Array.isArray(response.triples)) {
180
+ return [];
181
+ }
182
+
183
+ // Validate, normalize, filter
184
+ const valid: RawTriple[] = [];
185
+ for (const t of response.triples) {
186
+ if (!t.subject || !t.predicate || !t.object) continue;
187
+
188
+ const subject = normalizeEntity(t.subject);
189
+ const predicate = normalizePredicate(t.predicate);
190
+ const object = normalizeEntity(t.object);
191
+ const confidence = Number(t.confidence) || 0;
192
+
193
+ if (!subject || !predicate || !object) continue;
194
+ if (confidence < this.minConfidence) continue;
195
+ if (subject === object) continue; // Skip self-referencing
196
+
197
+ valid.push({ subject, predicate, object, confidence });
198
+ }
199
+
200
+ // Deduplicate within batch
201
+ const seen = new Set<string>();
202
+ return valid.filter((t) => {
203
+ const key = `${t.subject}\x00${t.predicate}\x00${t.object}`;
204
+ if (seen.has(key)) return false;
205
+ seen.add(key);
206
+ return true;
207
+ });
208
+ }
209
+ }
210
+
211
+ // ============================================================================
212
+ // Factory
213
+ // ============================================================================
214
+
215
+ export function createKGExtractor(config: KGExtractorConfig): KGExtractor {
216
+ return new KGExtractor(config);
217
+ }
@@ -0,0 +1,353 @@
1
+ /**
2
+ * Knowledge Graph Triple Store
3
+ * Stores (subject, predicate, object) triples in a dedicated LanceDB table.
4
+ * Pure relational — no vector columns.
5
+ */
6
+
7
+ import type * as LanceDB from "@lancedb/lancedb";
8
+ import { createHash } from "node:crypto";
9
+ import { loadLanceDB } from "./store.js";
10
+
11
+ // ============================================================================
12
+ // Types
13
+ // ============================================================================
14
+
15
+ export interface KGTriple {
16
+ /** Deterministic ID: sha256(scope + subject + predicate + object) */
17
+ id: string;
18
+ scope: string;
19
+ subject: string;
20
+ predicate: string;
21
+ object: string;
22
+ /** Extraction confidence 0-1 */
23
+ confidence: number;
24
+ /** ID of the source memory entry */
25
+ source_memory_id: string;
26
+ /** Original text snippet used for extraction */
27
+ source_text: string;
28
+ timestamp: number;
29
+ }
30
+
31
+ export interface KGStoreConfig {
32
+ /** Reuse the same LanceDB connection path as MemoryStore */
33
+ dbPath: string;
34
+ }
35
+
36
+ export interface NeighborhoodResult {
37
+ entity: string;
38
+ triples: KGTriple[];
39
+ /** How many hops from the seed entity */
40
+ hops: number;
41
+ }
42
+
43
+ // ============================================================================
44
+ // Helpers
45
+ // ============================================================================
46
+
47
+ const KG_TABLE_NAME = "kg_triples";
48
+
49
+ /** Deterministic triple ID for dedup */
50
+ export function tripleId(scope: string, subject: string, predicate: string, object: string): string {
51
+ const raw = `${scope}\x00${subject}\x00${predicate}\x00${object}`;
52
+ return createHash("sha256").update(raw).digest("hex").slice(0, 32);
53
+ }
54
+
55
+ function escapeSql(value: string): string {
56
+ return value.replace(/'/g, "''");
57
+ }
58
+
59
+ // ============================================================================
60
+ // KG Store
61
+ // ============================================================================
62
+
63
+ export class KGStore {
64
+ private db: LanceDB.Connection | null = null;
65
+ private table: LanceDB.Table | null = null;
66
+ private initPromise: Promise<void> | null = null;
67
+
68
+ constructor(private readonly config: KGStoreConfig) {}
69
+
70
+ // --------------------------------------------------------------------------
71
+ // Initialization
72
+ // --------------------------------------------------------------------------
73
+
74
+ private async ensureInitialized(): Promise<void> {
75
+ if (this.table) return;
76
+ if (this.initPromise) return this.initPromise;
77
+
78
+ this.initPromise = this.doInitialize().catch((err) => {
79
+ this.initPromise = null;
80
+ throw err;
81
+ });
82
+ return this.initPromise;
83
+ }
84
+
85
+ private async doInitialize(): Promise<void> {
86
+ const lancedb = await loadLanceDB();
87
+ const db = await lancedb.connect(this.config.dbPath);
88
+
89
+ let table: LanceDB.Table;
90
+ try {
91
+ table = await db.openTable(KG_TABLE_NAME);
92
+ } catch {
93
+ // Table doesn't exist — create with schema entry then remove it
94
+ const schemaEntry: KGTriple = {
95
+ id: "__schema__",
96
+ scope: "global",
97
+ subject: "",
98
+ predicate: "",
99
+ object: "",
100
+ confidence: 0,
101
+ source_memory_id: "",
102
+ source_text: "",
103
+ timestamp: 0,
104
+ };
105
+
106
+ try {
107
+ table = await db.createTable(KG_TABLE_NAME, [schemaEntry]);
108
+ await table.delete('id = "__schema__"');
109
+ } catch (createErr) {
110
+ if (String(createErr).includes("already exists")) {
111
+ table = await db.openTable(KG_TABLE_NAME);
112
+ } else {
113
+ throw createErr;
114
+ }
115
+ }
116
+ }
117
+
118
+ // Create FTS index on subject for entity lookup (optional)
119
+ try {
120
+ const indices = await table.listIndices();
121
+ const hasSubjectFts = indices?.some(
122
+ (idx: any) => idx.indexType === "FTS" && idx.columns?.includes("subject"),
123
+ );
124
+ if (!hasSubjectFts) {
125
+ const lance = await loadLanceDB();
126
+ await table.createIndex("subject", { config: (lance as any).Index.fts() });
127
+ }
128
+ } catch {
129
+ // FTS on subject is optional — BFS still works without it
130
+ }
131
+
132
+ this.db = db;
133
+ this.table = table;
134
+ }
135
+
136
+ // --------------------------------------------------------------------------
137
+ // Write
138
+ // --------------------------------------------------------------------------
139
+
140
+ async createTriple(triple: Omit<KGTriple, "id" | "timestamp">): Promise<KGTriple> {
141
+ await this.ensureInitialized();
142
+
143
+ const full: KGTriple = {
144
+ ...triple,
145
+ id: tripleId(triple.scope, triple.subject, triple.predicate, triple.object),
146
+ timestamp: Date.now(),
147
+ };
148
+
149
+ // Upsert: delete old then add new (LanceDB has no native upsert)
150
+ try {
151
+ await this.table!.delete(`id = '${escapeSql(full.id)}'`);
152
+ } catch {
153
+ // OK if not found
154
+ }
155
+ await this.table!.add([full]);
156
+ return full;
157
+ }
158
+
159
+ async createTriples(triples: Array<Omit<KGTriple, "id" | "timestamp">>): Promise<KGTriple[]> {
160
+ if (triples.length === 0) return [];
161
+ await this.ensureInitialized();
162
+
163
+ const now = Date.now();
164
+ const fullTriples: KGTriple[] = triples.map((t) => ({
165
+ ...t,
166
+ id: tripleId(t.scope, t.subject, t.predicate, t.object),
167
+ timestamp: now,
168
+ }));
169
+
170
+ // Deduplicate by id (same triple in batch)
171
+ const seen = new Set<string>();
172
+ const deduped = fullTriples.filter((t) => {
173
+ if (seen.has(t.id)) return false;
174
+ seen.add(t.id);
175
+ return true;
176
+ });
177
+
178
+ // Batch delete existing then add
179
+ const ids = deduped.map((t) => `'${escapeSql(t.id)}'`).join(", ");
180
+ try {
181
+ await this.table!.delete(`id IN (${ids})`);
182
+ } catch {
183
+ // OK if none found
184
+ }
185
+ await this.table!.add(deduped);
186
+ return deduped;
187
+ }
188
+
189
+ // --------------------------------------------------------------------------
190
+ // Read — Edge queries
191
+ // --------------------------------------------------------------------------
192
+
193
+ /** Get all triples where entity is the subject */
194
+ async getOutgoingEdges(entity: string, scope?: string): Promise<KGTriple[]> {
195
+ await this.ensureInitialized();
196
+
197
+ let filter = `subject = '${escapeSql(entity)}'`;
198
+ if (scope) filter += ` AND scope = '${escapeSql(scope)}'`;
199
+
200
+ const rows = await this.table!.query().where(filter).limit(500).toArray();
201
+ return rows.map(rowToTriple);
202
+ }
203
+
204
+ /** Get all triples where entity is the object */
205
+ async getIncomingEdges(entity: string, scope?: string): Promise<KGTriple[]> {
206
+ await this.ensureInitialized();
207
+
208
+ let filter = `object = '${escapeSql(entity)}'`;
209
+ if (scope) filter += ` AND scope = '${escapeSql(scope)}'`;
210
+
211
+ const rows = await this.table!.query().where(filter).limit(500).toArray();
212
+ return rows.map(rowToTriple);
213
+ }
214
+
215
+ /**
216
+ * BFS neighborhood traversal up to `maxHops` hops.
217
+ * Returns all triples reachable from the seed entity.
218
+ */
219
+ async getNeighborhood(
220
+ seedEntities: string[],
221
+ maxHops = 2,
222
+ scope?: string,
223
+ ): Promise<NeighborhoodResult[]> {
224
+ await this.ensureInitialized();
225
+
226
+ const visited = new Map<string, number>(); // entity -> min hops
227
+ const allTriples: KGTriple[] = [];
228
+ let frontier = [...seedEntities];
229
+
230
+ for (const seed of seedEntities) {
231
+ visited.set(seed, 0);
232
+ }
233
+
234
+ for (let hop = 1; hop <= maxHops; hop++) {
235
+ if (frontier.length === 0) break;
236
+
237
+ const nextFrontier: string[] = [];
238
+
239
+ for (const entity of frontier) {
240
+ const [outgoing, incoming] = await Promise.all([
241
+ this.getOutgoingEdges(entity, scope),
242
+ this.getIncomingEdges(entity, scope),
243
+ ]);
244
+
245
+ for (const t of [...outgoing, ...incoming]) {
246
+ allTriples.push(t);
247
+
248
+ for (const neighbor of [t.subject, t.object]) {
249
+ if (!visited.has(neighbor)) {
250
+ visited.set(neighbor, hop);
251
+ nextFrontier.push(neighbor);
252
+ }
253
+ }
254
+ }
255
+ }
256
+
257
+ frontier = nextFrontier;
258
+ }
259
+
260
+ // Group by entity
261
+ const results: NeighborhoodResult[] = [];
262
+ for (const [entity, hops] of visited) {
263
+ const related = allTriples.filter(
264
+ (t) => t.subject === entity || t.object === entity,
265
+ );
266
+ if (related.length > 0 || seedEntities.includes(entity)) {
267
+ results.push({ entity, triples: related, hops });
268
+ }
269
+ }
270
+
271
+ return results;
272
+ }
273
+
274
+ // --------------------------------------------------------------------------
275
+ // Read — Entity listing
276
+ // --------------------------------------------------------------------------
277
+
278
+ /** Get all unique entities in scope */
279
+ async getAllEntities(scope?: string): Promise<string[]> {
280
+ await this.ensureInitialized();
281
+
282
+ const filter = scope ? `scope = '${escapeSql(scope)}'` : "1=1";
283
+ const rows = await this.table!.query()
284
+ .select(["subject", "object"])
285
+ .where(filter)
286
+ .limit(10000)
287
+ .toArray();
288
+
289
+ const entities = new Set<string>();
290
+ for (const row of rows) {
291
+ if (row.subject) entities.add(row.subject as string);
292
+ if (row.object) entities.add(row.object as string);
293
+ }
294
+ return [...entities];
295
+ }
296
+
297
+ /** Check if an entity exists in the KG */
298
+ async hasEntity(entity: string, scope?: string): Promise<boolean> {
299
+ await this.ensureInitialized();
300
+
301
+ const safeEntity = escapeSql(entity);
302
+ let filter = `subject = '${safeEntity}' OR object = '${safeEntity}'`;
303
+ if (scope) filter = `(${filter}) AND scope = '${escapeSql(scope)}'`;
304
+
305
+ const rows = await this.table!.query().where(filter).limit(1).toArray();
306
+ return rows.length > 0;
307
+ }
308
+
309
+ // --------------------------------------------------------------------------
310
+ // Delete
311
+ // --------------------------------------------------------------------------
312
+
313
+ /** Delete all triples extracted from a given source memory */
314
+ async deleteBySource(sourceMemoryId: string): Promise<void> {
315
+ await this.ensureInitialized();
316
+ await this.table!.delete(`source_memory_id = '${escapeSql(sourceMemoryId)}'`);
317
+ }
318
+
319
+ /** Delete all triples in scope */
320
+ async deleteByScope(scope: string): Promise<void> {
321
+ await this.ensureInitialized();
322
+ await this.table!.delete(`scope = '${escapeSql(scope)}'`);
323
+ }
324
+
325
+ // --------------------------------------------------------------------------
326
+ // Stats
327
+ // --------------------------------------------------------------------------
328
+
329
+ async countTriples(scope?: string): Promise<number> {
330
+ await this.ensureInitialized();
331
+ const filter = scope ? `scope = '${escapeSql(scope)}'` : "1=1";
332
+ const rows = await this.table!.query().where(filter).select(["id"]).limit(100000).toArray();
333
+ return rows.length;
334
+ }
335
+ }
336
+
337
+ // ============================================================================
338
+ // Row mapping
339
+ // ============================================================================
340
+
341
+ function rowToTriple(row: Record<string, unknown>): KGTriple {
342
+ return {
343
+ id: row.id as string,
344
+ scope: (row.scope as string) ?? "global",
345
+ subject: row.subject as string,
346
+ predicate: row.predicate as string,
347
+ object: row.object as string,
348
+ confidence: Number(row.confidence),
349
+ source_memory_id: row.source_memory_id as string,
350
+ source_text: row.source_text as string,
351
+ timestamp: Number(row.timestamp),
352
+ };
353
+ }
@@ -0,0 +1,53 @@
1
+ import type { LegacyScopeIssueKind } from "./store.js";
2
+
3
+ export interface LegacyScopeReviewRecord {
4
+ decision: "keep";
5
+ kind: LegacyScopeIssueKind;
6
+ reason: string;
7
+ reviewedAt: string;
8
+ }
9
+
10
+ function asObject(value: unknown): Record<string, unknown> | undefined {
11
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
12
+ return value as Record<string, unknown>;
13
+ }
14
+
15
+ export function parseLegacyScopeReview(raw: unknown): LegacyScopeReviewRecord | undefined {
16
+ const record = asObject(raw);
17
+ if (!record) return undefined;
18
+ if (record.decision !== "keep") return undefined;
19
+ if (record.kind !== "missing" && record.kind !== "empty" && record.kind !== "global") return undefined;
20
+ if (typeof record.reason !== "string" || !record.reason.trim()) return undefined;
21
+ if (typeof record.reviewedAt !== "string" || !Number.isFinite(Date.parse(record.reviewedAt))) return undefined;
22
+ return {
23
+ decision: "keep",
24
+ kind: record.kind,
25
+ reason: record.reason.trim(),
26
+ reviewedAt: record.reviewedAt,
27
+ };
28
+ }
29
+
30
+ export function buildLegacyScopeKeepReview(
31
+ kind: LegacyScopeIssueKind,
32
+ reason: string,
33
+ reviewedAt = new Date().toISOString(),
34
+ ): LegacyScopeReviewRecord {
35
+ const normalizedReason = reason.trim();
36
+ if (!normalizedReason) {
37
+ throw new Error("legacy scope keep review reason must not be empty.");
38
+ }
39
+ return {
40
+ decision: "keep",
41
+ kind,
42
+ reason: normalizedReason,
43
+ reviewedAt,
44
+ };
45
+ }
46
+
47
+ export function suppressesLegacyScopeIssue(
48
+ raw: unknown,
49
+ kind: LegacyScopeIssueKind,
50
+ ): boolean {
51
+ const review = parseLegacyScopeReview(raw);
52
+ return review?.decision === "keep" && review.kind === kind;
53
+ }