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,207 @@
1
+ /**
2
+ * Query Entity Detector
3
+ * Extracts entities from search queries for KG graph traversal.
4
+ * Combines heuristic extraction with KG entity matching.
5
+ */
6
+
7
+ import { normalizeEntity } from "./kg-extractor.js";
8
+ import type { KGStore } from "./kg-store.js";
9
+
10
+ // ============================================================================
11
+ // Types
12
+ // ============================================================================
13
+
14
+ export interface DetectedEntities {
15
+ /** Entities found in the query */
16
+ entities: string[];
17
+ /** Whether the query signals multi-hop intent */
18
+ isMultiHop: boolean;
19
+ /** Detected multi-hop predicate (e.g. "friend_of", "uses") */
20
+ hopPredicate?: string;
21
+ }
22
+
23
+ // ============================================================================
24
+ // Multi-hop signal patterns
25
+ // ============================================================================
26
+
27
+ interface HopPattern {
28
+ pattern: RegExp;
29
+ predicate: string;
30
+ }
31
+
32
+ const MULTI_HOP_PATTERNS: HopPattern[] = [
33
+ // English patterns
34
+ { pattern: /(\w+)'s\s+friends?/i, predicate: "friend_of" },
35
+ { pattern: /friends?\s+of\s+(\w+)/i, predicate: "friend_of" },
36
+ { pattern: /who\s+(?:does|did)\s+(\w+)\s+(?:work|collaborate)\s+with/i, predicate: "works_with" },
37
+ { pattern: /what\s+does\s+(\w+)\s+use/i, predicate: "uses" },
38
+ { pattern: /what\s+(?:tools?|tech|technologies?)\s+does\s+(\w+)\s+use/i, predicate: "uses" },
39
+ { pattern: /who\s+(?:uses?|works?\s+with)\s+(\w+)/i, predicate: "uses" },
40
+ { pattern: /who\s+(?:created|built|wrote|made)\s+(\w+)/i, predicate: "created_by" },
41
+ { pattern: /who\s+(?:knows?|is\s+familiar\s+with)\s+(\w+)/i, predicate: "knows" },
42
+ { pattern: /what\s+depends\s+on\s+(\w+)/i, predicate: "depends_on" },
43
+ { pattern: /(\w+)\s+depend(?:s|encies)/i, predicate: "depends_on" },
44
+ { pattern: /who\s+manages?\s+(\w+)/i, predicate: "manages" },
45
+ { pattern: /(\w+)'s\s+team/i, predicate: "member_of" },
46
+ // Chinese patterns
47
+ { pattern: /(.+?)的朋友/u, predicate: "friend_of" },
48
+ { pattern: /谁和(.+?)一起/u, predicate: "works_with" },
49
+ { pattern: /(.+?)用(?:了|什么)/u, predicate: "uses" },
50
+ { pattern: /谁(?:创建|开发|写)了(.+)/u, predicate: "created_by" },
51
+ { pattern: /(.+?)(?:依赖|需要)什么/u, predicate: "depends_on" },
52
+ { pattern: /谁(?:认识|了解)(.+)/u, predicate: "knows" },
53
+ { pattern: /(.+?)的(?:团队|成员)/u, predicate: "member_of" },
54
+ { pattern: /谁管理(.+)/u, predicate: "manages" },
55
+ ];
56
+
57
+ // ============================================================================
58
+ // Heuristic entity extraction
59
+ // ============================================================================
60
+
61
+ /**
62
+ * Extract candidate entities from query text using heuristics:
63
+ * - Quoted strings: "RecallNest", 'LanceDB'
64
+ * - Capitalized words/phrases: Alice, OpenAI, Claude Code
65
+ * - CamelCase compound words: RecallNest, LanceDB
66
+ */
67
+ function extractCandidates(query: string): string[] {
68
+ const candidates: string[] = [];
69
+
70
+ // 1. Quoted strings (single, double, backtick)
71
+ const quoted = query.match(/["'`]([^"'`]+)["'`]/g);
72
+ if (quoted) {
73
+ for (const q of quoted) {
74
+ const inner = q.slice(1, -1).trim();
75
+ if (inner.length >= 2) candidates.push(inner);
76
+ }
77
+ }
78
+
79
+ // 2. Capitalized words/phrases (English)
80
+ // Match sequences of capitalized words: "Claude Code", "Open AI", "LanceDB"
81
+ const capPattern = /\b([A-Z][a-zA-Z]*(?:[\s-][A-Z][a-zA-Z]*)*)\b/g;
82
+ let match;
83
+ while ((match = capPattern.exec(query)) !== null) {
84
+ const word = match[1].trim();
85
+ // Skip common English words that happen to be at start of sentence
86
+ if (STOP_WORDS.has(word.toLowerCase())) continue;
87
+ if (word.length >= 2) candidates.push(word);
88
+ }
89
+
90
+ // 3. CamelCase / PascalCase compound words: "RecallNest", "LanceDB"
91
+ const camelPattern = /\b([A-Z][a-z]+(?:[A-Z][a-z]*)+)\b/g;
92
+ while ((match = camelPattern.exec(query)) !== null) {
93
+ candidates.push(match[1]);
94
+ }
95
+
96
+ return candidates;
97
+ }
98
+
99
+ const STOP_WORDS = new Set([
100
+ "the", "a", "an", "is", "are", "was", "were", "be", "been",
101
+ "do", "does", "did", "have", "has", "had", "will", "would",
102
+ "can", "could", "should", "may", "might", "must", "shall",
103
+ "what", "who", "where", "when", "why", "how", "which",
104
+ "this", "that", "these", "those", "it", "its",
105
+ "not", "no", "nor", "but", "and", "or", "if", "then",
106
+ "use", "uses", "used", "using",
107
+ "important", "search", "find", "get", "set", "let", "new",
108
+ "all", "any", "some", "each", "every", "many", "much",
109
+ "my", "your", "his", "her", "our", "their",
110
+ "i", "you", "he", "she", "we", "they", "me", "him", "us", "them",
111
+ ]);
112
+
113
+ // ============================================================================
114
+ // Main detection
115
+ // ============================================================================
116
+
117
+ /**
118
+ * Detect entities in a query for KG graph traversal.
119
+ * Optionally validates against KG-known entities.
120
+ */
121
+ export async function detectEntities(
122
+ query: string,
123
+ kgStore?: KGStore,
124
+ scope?: string,
125
+ ): Promise<DetectedEntities> {
126
+ // Check for multi-hop signals
127
+ let isMultiHop = false;
128
+ let hopPredicate: string | undefined;
129
+
130
+ for (const hp of MULTI_HOP_PATTERNS) {
131
+ if (hp.pattern.test(query)) {
132
+ isMultiHop = true;
133
+ hopPredicate = hp.predicate;
134
+ break;
135
+ }
136
+ }
137
+
138
+ // Extract candidates
139
+ const raw = extractCandidates(query);
140
+
141
+ // Normalize
142
+ const normalized = raw
143
+ .map(normalizeEntity)
144
+ .filter(e => e.length >= 2);
145
+
146
+ // Deduplicate (case-insensitive)
147
+ const seen = new Set<string>();
148
+ const unique = normalized.filter(e => {
149
+ const key = e.toLowerCase();
150
+ if (seen.has(key)) return false;
151
+ seen.add(key);
152
+ return true;
153
+ });
154
+
155
+ // If KG store available, validate entities exist in KG
156
+ if (kgStore && unique.length > 0) {
157
+ const validated: string[] = [];
158
+ const kgEntities = await kgStore.getAllEntities(scope);
159
+ const kgEntitySet = new Set(kgEntities.map(e => e.toLowerCase()));
160
+
161
+ for (const entity of unique) {
162
+ if (kgEntitySet.has(entity.toLowerCase())) {
163
+ // Find the KG-canonical casing
164
+ const canonical = kgEntities.find(e => e.toLowerCase() === entity.toLowerCase());
165
+ validated.push(canonical ?? entity);
166
+ }
167
+ }
168
+
169
+ // If validation eliminated everything, fall back to heuristic candidates
170
+ if (validated.length === 0 && unique.length > 0) {
171
+ return { entities: unique, isMultiHop, hopPredicate };
172
+ }
173
+
174
+ return { entities: validated, isMultiHop, hopPredicate };
175
+ }
176
+
177
+ return { entities: unique, isMultiHop, hopPredicate };
178
+ }
179
+
180
+ /**
181
+ * Synchronous version without KG validation — for testing or lightweight use.
182
+ */
183
+ export function detectEntitiesSync(query: string): DetectedEntities {
184
+ let isMultiHop = false;
185
+ let hopPredicate: string | undefined;
186
+
187
+ for (const hp of MULTI_HOP_PATTERNS) {
188
+ if (hp.pattern.test(query)) {
189
+ isMultiHop = true;
190
+ hopPredicate = hp.predicate;
191
+ break;
192
+ }
193
+ }
194
+
195
+ const raw = extractCandidates(query);
196
+ const normalized = raw.map(normalizeEntity).filter(e => e.length >= 2);
197
+
198
+ const seen = new Set<string>();
199
+ const unique = normalized.filter(e => {
200
+ const key = e.toLowerCase();
201
+ if (seen.has(key)) return false;
202
+ seen.add(key);
203
+ return true;
204
+ });
205
+
206
+ return { entities: unique, isMultiHop, hopPredicate };
207
+ }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Lightweight Chinese query expansion via static synonym dictionary.
3
+ * Expands colloquial/fuzzy terms into technical equivalents for BM25 boost.
4
+ * No API calls — pure local dictionary lookup.
5
+ *
6
+ * v1.1: Word-boundary matching for English triggers (prevents "download" matching "down").
7
+ * MAX_EXPANSION_TERMS cap to prevent query bloat hurting BM25 precision.
8
+ * Structured synonym entries separating CN (substring) from EN (word-boundary).
9
+ * v1.2: Entity resolution pre-pass normalizes aliases before synonym expansion.
10
+ */
11
+
12
+ import { resolveQueryEntities } from "./entity-resolver.js";
13
+
14
+ interface SynonymEntry {
15
+ /** Chinese triggers: matched by substring (CJK has no word boundaries) */
16
+ cn: string[];
17
+ /** English triggers: matched by word boundary (\b) to prevent false positives */
18
+ en: string[];
19
+ /** Terms to append when triggered */
20
+ expansions: string[];
21
+ }
22
+
23
+ const SYNONYM_MAP: SynonymEntry[] = [
24
+ // --- Status / Failure ---
25
+ { cn: ["挂了", "挂掉", "宕机"], en: ["shutdown", "crashed"], expansions: ["崩溃", "crash", "error", "报错", "挂了", "宕机", "失败"] },
26
+ { cn: ["卡住", "卡死", "没反应"], en: ["hang", "stuck"], expansions: ["hang", "timeout", "超时", "卡住", "无响应", "stuck"] },
27
+ { cn: ["炸了", "爆了"], en: ["OOM"], expansions: ["崩溃", "crash", "OOM", "内存溢出", "error"] },
28
+
29
+ // --- AI / Consciousness ---
30
+ { cn: ["感受", "感觉", "情感"], en: ["feeling", "emotion"], expansions: ["意识", "consciousness", "experiencing", "感受", "情感", "qualia"] },
31
+ { cn: ["有没有意识", "是否有意识"], en: ["consciousness", "sentience"], expansions: ["consciousness", "意识", "sentience", "感知", "自我意识"] },
32
+ { cn: ["自由意志"], en: ["free will"], expansions: ["free will", "自由意志", "决定论", "determinism"] },
33
+
34
+ // --- Config / Deploy ---
35
+ { cn: ["配置", "设置"], en: ["config", "configuration", "settings"], expansions: ["配置", "config", "configuration", "settings", "设置"] },
36
+ { cn: ["部署", "上线"], en: ["deploy"], expansions: ["deploy", "部署", "上线", "发布", "release"] },
37
+ { cn: ["容器"], en: ["docker", "container"], expansions: ["Docker", "容器", "container", "docker-compose"] },
38
+
39
+ // --- Code / Debug ---
40
+ { cn: ["报错", "出错", "错误"], en: ["error", "exception"], expansions: ["error", "报错", "exception", "错误", "失败", "bug"] },
41
+ { cn: ["修复", "修了", "修好"], en: ["fix", "patch"], expansions: ["fix", "修复", "patch", "修了", "解决"] },
42
+ { cn: ["踩坑", "坑"], en: ["troubleshoot"], expansions: ["踩坑", "bug", "问题", "教训", "排查", "troubleshoot"] },
43
+ { cn: ["日志"], en: ["log", "logging"], expansions: ["日志", "log", "logging", "输出", "stdout", "stderr"] },
44
+ { cn: ["权限"], en: ["permission", "access"], expansions: ["权限", "permission", "access", "授权", "认证"] },
45
+
46
+ // --- Writing / Content ---
47
+ { cn: ["配图", "插图"], en: [], expansions: ["配图", "封面", "style-catalog", "风格", "图片", "image"] },
48
+ { cn: ["排版", "版式"], en: ["layout"], expansions: ["排版", "layout", "主题", "theme", "样式"] },
49
+ { cn: ["风格"], en: ["style"], expansions: ["风格", "style", "轮换", "catalog"] },
50
+ { cn: ["写作", "写文章"], en: ["writing"], expansions: ["写作", "writing", "文章", "公众号", "content-alchemy"] },
51
+
52
+ // --- Infrastructure ---
53
+ { cn: ["机器人"], en: ["bot", "agent"], expansions: ["bot", "机器人", "OpenClaw", "agent", "gateway"] },
54
+ { cn: ["推送"], en: ["push"], expansions: ["push", "推送", "git push", "commit"] },
55
+ { cn: ["记忆"], en: ["memory"], expansions: ["记忆", "memory", "记忆系统", "LanceDB", "索引"] },
56
+ { cn: ["搜索", "查找", "找"], en: ["search", "retrieval"], expansions: ["搜索", "search", "retrieval", "检索", "查找"] },
57
+ ];
58
+
59
+ /** Maximum expansion terms to prevent query bloat hurting BM25 precision */
60
+ const MAX_EXPANSION_TERMS = 5;
61
+
62
+ /**
63
+ * Expand a query by appending synonym terms from the dictionary.
64
+ * Returns the original query with additional terms appended.
65
+ * Idempotent — already-precise queries pass through unchanged.
66
+ */
67
+ export function expandQuery(query: string): string {
68
+ if (!query || query.trim().length < 2) return query;
69
+
70
+ // Entity resolution pre-pass: normalize aliases before synonym expansion
71
+ const resolved = resolveQueryEntities(query);
72
+ const lower = resolved.toLowerCase();
73
+ const additions = new Set<string>();
74
+
75
+ for (const entry of SYNONYM_MAP) {
76
+ // Chinese triggers: substring match (CJK has no word boundaries)
77
+ const cnMatch = entry.cn.some(t => lower.includes(t.toLowerCase()));
78
+ // English triggers: word-boundary match to prevent false positives
79
+ const enMatch = entry.en.length > 0 && entry.en.some(t => {
80
+ const regex = new RegExp(`\\b${t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i');
81
+ return regex.test(lower);
82
+ });
83
+
84
+ if (cnMatch || enMatch) {
85
+ for (const exp of entry.expansions) {
86
+ if (!lower.includes(exp.toLowerCase())) {
87
+ additions.add(exp);
88
+ }
89
+ }
90
+ }
91
+ }
92
+
93
+ if (additions.size === 0) return resolved;
94
+
95
+ // Cap expansion terms to prevent query bloat
96
+ const limited = [...additions].slice(0, MAX_EXPANSION_TERMS);
97
+ return `${resolved} ${limited.join(" ")}`;
98
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Result Synthesizer — Tier 3.5
3
+ *
4
+ * Converts multiple retrieval fragments into a single coherent narrative
5
+ * using LLM. Falls back to original fragments when LLM is unavailable.
6
+ *
7
+ * Integration point: context-composer calls synthesize() on section outputs
8
+ * (stableContext, relevantPatterns, recentCases) when enabled.
9
+ *
10
+ * Enable: RECALLNEST_SYNTHESIZE=true environment variable.
11
+ */
12
+
13
+ import type { LLMClient } from "./llm-client.js";
14
+ import { logWarn } from "./stderr-log.js";
15
+
16
+ // ============================================================================
17
+ // Configuration
18
+ // ============================================================================
19
+
20
+ export interface SynthesizerConfig {
21
+ /** Minimum fragments required to trigger synthesis (default: 3).
22
+ * Below this count, fragments are returned as-is. */
23
+ minFragments: number;
24
+ /** Maximum output length in chars (default: 500). */
25
+ maxOutputChars: number;
26
+ }
27
+
28
+ export const DEFAULT_SYNTHESIZER_CONFIG: SynthesizerConfig = {
29
+ minFragments: 3,
30
+ maxOutputChars: 500,
31
+ };
32
+
33
+ // ============================================================================
34
+ // Core
35
+ // ============================================================================
36
+
37
+ /**
38
+ * Check if synthesis is enabled via environment variable.
39
+ */
40
+ export function isSynthesisEnabled(): boolean {
41
+ return process.env.RECALLNEST_SYNTHESIZE === "true";
42
+ }
43
+
44
+ /**
45
+ * Synthesize an array of text fragments into a coherent narrative.
46
+ *
47
+ * @param fragments Array of text snippets (e.g. from retrieval results)
48
+ * @param query The original query/task that produced these fragments
49
+ * @param llm LLM client instance (null = fallback to pass-through)
50
+ * @param config Optional configuration overrides
51
+ * @returns Object with synthesized text (or original fragments on fallback)
52
+ */
53
+ export async function synthesize(
54
+ fragments: string[],
55
+ query: string,
56
+ llm: LLMClient | null,
57
+ config: SynthesizerConfig = DEFAULT_SYNTHESIZER_CONFIG,
58
+ ): Promise<SynthesisResult> {
59
+ // Not enough fragments to justify synthesis
60
+ if (fragments.length < config.minFragments) {
61
+ return { text: null, fragments, synthesized: false, reason: "below-threshold" };
62
+ }
63
+
64
+ // No LLM available
65
+ if (!llm) {
66
+ return { text: null, fragments, synthesized: false, reason: "no-llm" };
67
+ }
68
+
69
+ try {
70
+ const result = await llm.synthesizeFragments(fragments, query, config.maxOutputChars);
71
+ if (result && result.length > 0) {
72
+ return { text: result, fragments, synthesized: true, reason: "ok" };
73
+ }
74
+ return { text: null, fragments, synthesized: false, reason: "empty-response" };
75
+ } catch (err) {
76
+ logWarn("Result synthesis failed, using raw fragments:", err);
77
+ return { text: null, fragments, synthesized: false, reason: "error" };
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Synthesize a section of context-composer output.
83
+ * If synthesis succeeds, returns a single-element array with the narrative.
84
+ * Otherwise returns the original array unchanged.
85
+ */
86
+ export async function synthesizeSection(
87
+ sectionItems: string[],
88
+ query: string,
89
+ llm: LLMClient | null,
90
+ config: SynthesizerConfig = DEFAULT_SYNTHESIZER_CONFIG,
91
+ ): Promise<string[]> {
92
+ if (!isSynthesisEnabled()) return sectionItems;
93
+
94
+ const result = await synthesize(sectionItems, query, llm, config);
95
+ if (result.synthesized && result.text) {
96
+ return [result.text];
97
+ }
98
+ return sectionItems;
99
+ }
100
+
101
+ // ============================================================================
102
+ // Types
103
+ // ============================================================================
104
+
105
+ export interface SynthesisResult {
106
+ /** Synthesized narrative text, or null if synthesis was skipped/failed */
107
+ text: string | null;
108
+ /** Original fragments (preserved for fallback) */
109
+ fragments: string[];
110
+ /** Whether synthesis was actually performed */
111
+ synthesized: boolean;
112
+ /** Reason for the outcome */
113
+ reason: "ok" | "below-threshold" | "no-llm" | "empty-response" | "error";
114
+ }
@@ -0,0 +1,101 @@
1
+ import type { RetrievalConfig } from "./retriever.js";
2
+
3
+ export type RetrievalProfileName = "default" | "writing" | "debug" | "fact-check";
4
+
5
+ export interface RetrievalProfileDefinition {
6
+ name: RetrievalProfileName;
7
+ label: string;
8
+ description: string;
9
+ overrides: Partial<RetrievalConfig>;
10
+ }
11
+
12
+ export const RETRIEVAL_PROFILES: Record<RetrievalProfileName, RetrievalProfileDefinition> = {
13
+ default: {
14
+ name: "default",
15
+ label: "General Recall",
16
+ description: "Balanced hybrid retrieval for everyday memory search.",
17
+ overrides: {},
18
+ },
19
+ writing: {
20
+ name: "writing",
21
+ label: "Writing Brief",
22
+ description: "Prefer broader semantic recall and tolerate older but reusable material.",
23
+ overrides: {
24
+ vectorWeight: 0.8,
25
+ bm25Weight: 0.2,
26
+ candidatePoolSize: 28,
27
+ recencyHalfLifeDays: 90,
28
+ recencyWeight: 0.06,
29
+ timeDecayHalfLifeDays: 365,
30
+ hardMinScore: 0.24,
31
+ lengthNormAnchor: 800,
32
+ },
33
+ },
34
+ debug: {
35
+ name: "debug",
36
+ label: "Debug Trace",
37
+ description: "Favor recent, exact-match operational details such as errors, commands, and fixes.",
38
+ overrides: {
39
+ vectorWeight: 0.55,
40
+ bm25Weight: 0.45,
41
+ candidatePoolSize: 24,
42
+ recencyHalfLifeDays: 7,
43
+ recencyWeight: 0.15,
44
+ timeDecayHalfLifeDays: 30,
45
+ hardMinScore: 0.34,
46
+ lengthNormAnchor: 420,
47
+ },
48
+ },
49
+ "fact-check": {
50
+ name: "fact-check",
51
+ label: "Fact Check",
52
+ description: "Favor exact evidence and fresher records; tighten the cutoff to reduce weak matches.",
53
+ overrides: {
54
+ vectorWeight: 0.5,
55
+ bm25Weight: 0.5,
56
+ candidatePoolSize: 26,
57
+ recencyHalfLifeDays: 21,
58
+ recencyWeight: 0.08,
59
+ timeDecayHalfLifeDays: 45,
60
+ hardMinScore: 0.38,
61
+ lengthNormAnchor: 360,
62
+ },
63
+ },
64
+ };
65
+
66
+ export function normalizeRetrievalProfile(input?: string): RetrievalProfileName {
67
+ const normalized = (input || "default").trim().toLowerCase();
68
+ if (!normalized) return "default";
69
+ if (normalized === "factcheck" || normalized === "fact_check") return "fact-check";
70
+ if (normalized === "general") return "default";
71
+ if (normalized in RETRIEVAL_PROFILES) {
72
+ return normalized as RetrievalProfileName;
73
+ }
74
+ throw new Error(
75
+ `Unknown retrieval profile: ${input}. Available: ${Object.keys(RETRIEVAL_PROFILES).join(", ")}`,
76
+ );
77
+ }
78
+
79
+ export function applyRetrievalProfile(
80
+ baseConfig: RetrievalConfig,
81
+ profileName?: string,
82
+ ): { profile: RetrievalProfileDefinition; config: RetrievalConfig } {
83
+ const resolvedName = normalizeRetrievalProfile(profileName);
84
+ const profile = RETRIEVAL_PROFILES[resolvedName];
85
+ return {
86
+ profile,
87
+ config: {
88
+ ...baseConfig,
89
+ ...profile.overrides,
90
+ },
91
+ };
92
+ }
93
+
94
+ export function listRetrievalProfiles(): RetrievalProfileDefinition[] {
95
+ return [
96
+ RETRIEVAL_PROFILES.default,
97
+ RETRIEVAL_PROFILES.writing,
98
+ RETRIEVAL_PROFILES.debug,
99
+ RETRIEVAL_PROFILES["fact-check"],
100
+ ];
101
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Retrieval Trace — per-stage observability for the retrieval pipeline.
3
+ *
4
+ * Usage: pass an optional TraceCollector into retrieve(); each scoring stage
5
+ * calls startStage / endStage. After retrieve() returns, call summarize()
6
+ * for a human-readable breakdown or toJSON() for machine consumption.
7
+ */
8
+
9
+ // ── Types ────────────────────────────────────────────────────────────────────
10
+
11
+ export interface RetrievalStageResult {
12
+ name: string;
13
+ inputCount: number;
14
+ outputCount: number;
15
+ droppedCount: number;
16
+ scoreRange: [number, number] | null;
17
+ durationMs: number;
18
+ }
19
+
20
+ export interface RetrievalTrace {
21
+ query: string;
22
+ mode: "hybrid" | "vector";
23
+ stages: RetrievalStageResult[];
24
+ finalCount: number;
25
+ totalMs: number;
26
+ }
27
+
28
+ // ── TraceCollector ───────────────────────────────────────────────────────────
29
+
30
+ export class TraceCollector {
31
+ private stages: RetrievalStageResult[] = [];
32
+ private stageStart = 0;
33
+ private currentStageName = "";
34
+ private currentInputCount = 0;
35
+ private traceStart = 0;
36
+
37
+ constructor() {
38
+ this.traceStart = Date.now();
39
+ }
40
+
41
+ startStage(name: string, inputCount: number): void {
42
+ this.currentStageName = name;
43
+ this.currentInputCount = inputCount;
44
+ this.stageStart = Date.now();
45
+ }
46
+
47
+ endStage(outputCount: number, scores?: number[]): void {
48
+ const durationMs = Date.now() - this.stageStart;
49
+ let scoreRange: [number, number] | null = null;
50
+ if (scores && scores.length > 0) {
51
+ const finite = scores.filter(Number.isFinite);
52
+ if (finite.length > 0) {
53
+ scoreRange = [Math.min(...finite), Math.max(...finite)];
54
+ }
55
+ }
56
+ this.stages.push({
57
+ name: this.currentStageName,
58
+ inputCount: this.currentInputCount,
59
+ outputCount,
60
+ droppedCount: this.currentInputCount - outputCount,
61
+ scoreRange,
62
+ durationMs,
63
+ });
64
+ }
65
+
66
+ finalize(query: string, mode: "hybrid" | "vector"): RetrievalTrace {
67
+ const finalCount = this.stages.length > 0
68
+ ? this.stages[this.stages.length - 1].outputCount
69
+ : 0;
70
+ return {
71
+ query,
72
+ mode,
73
+ stages: this.stages,
74
+ finalCount,
75
+ totalMs: Date.now() - this.traceStart,
76
+ };
77
+ }
78
+
79
+ summarize(query: string, mode: "hybrid" | "vector"): string {
80
+ const trace = this.finalize(query, mode);
81
+ const lines: string[] = [];
82
+ lines.push(`[retrieve] query="${trace.query}" mode=${trace.mode} total=${trace.totalMs}ms`);
83
+ for (const s of trace.stages) {
84
+ const range = s.scoreRange
85
+ ? ` [${s.scoreRange[0].toFixed(3)}–${s.scoreRange[1].toFixed(3)}]`
86
+ : "";
87
+ lines.push(
88
+ ` ${s.name}: ${s.inputCount} → ${s.outputCount} (${s.droppedCount} dropped)${range} ${s.durationMs}ms`,
89
+ );
90
+ }
91
+ lines.push(` final: ${trace.finalCount} results`);
92
+ return lines.join("\n");
93
+ }
94
+ }