claude-brain 0.30.2 → 0.30.3

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 (236) hide show
  1. package/README.md +241 -191
  2. package/VERSION +1 -1
  3. package/assets/CLAUDE-unified.md +11 -11
  4. package/assets/CLAUDE.md +29 -29
  5. package/package.json +7 -3
  6. package/packs/backend/node.json +173 -173
  7. package/packs/core/javascript.json +176 -176
  8. package/packs/core/typescript.json +222 -222
  9. package/packs/frontend/react.json +254 -254
  10. package/packs/meta/testing.json +172 -172
  11. package/scripts/postinstall.mjs +531 -531
  12. package/src/automation/decision-detector.ts +452 -452
  13. package/src/automation/phase12-manager.ts +456 -456
  14. package/src/automation/proactive-recall.ts +373 -373
  15. package/src/automation/project-detector.ts +310 -310
  16. package/src/automation/repo-scanner.ts +210 -205
  17. package/src/cli/auto-setup.ts +75 -75
  18. package/src/cli/auto-start.ts +266 -266
  19. package/src/cli/bin.ts +264 -264
  20. package/src/cli/commands/autostart.ts +90 -90
  21. package/src/cli/commands/chroma.ts +578 -577
  22. package/src/cli/commands/export-training.ts +70 -70
  23. package/src/cli/commands/export.ts +130 -130
  24. package/src/cli/commands/git-hook.ts +183 -183
  25. package/src/cli/commands/hooks.ts +217 -217
  26. package/src/cli/commands/init.ts +123 -123
  27. package/src/cli/commands/install-mcp.ts +122 -111
  28. package/src/cli/commands/models.ts +979 -979
  29. package/src/cli/commands/pack.ts +200 -200
  30. package/src/cli/commands/refresh.ts +344 -339
  31. package/src/cli/commands/reindex.ts +120 -120
  32. package/src/cli/commands/serve.ts +466 -463
  33. package/src/cli/commands/start.ts +44 -44
  34. package/src/cli/commands/status.ts +220 -203
  35. package/src/cli/commands/uninstall-mcp.ts +45 -41
  36. package/src/cli/commands/update.ts +130 -124
  37. package/src/cli/migrate-chroma.ts +106 -106
  38. package/src/cli/ui/animations.ts +80 -80
  39. package/src/cli/ui/components.ts +82 -82
  40. package/src/cli/ui/index.ts +4 -4
  41. package/src/cli/ui/logo.ts +36 -36
  42. package/src/cli/ui/theme.ts +55 -55
  43. package/src/code-intelligence/indexer.ts +352 -352
  44. package/src/code-intelligence/linker.ts +178 -178
  45. package/src/code-intelligence/parser.ts +484 -484
  46. package/src/code-intelligence/query.ts +291 -291
  47. package/src/code-intelligence/schema.ts +83 -83
  48. package/src/code-intelligence/types.ts +95 -95
  49. package/src/config/defaults.ts +52 -52
  50. package/src/config/home.ts +56 -56
  51. package/src/config/index.ts +5 -5
  52. package/src/config/loader.ts +192 -192
  53. package/src/config/schema.ts +446 -415
  54. package/src/config/validator.ts +182 -182
  55. package/src/context/assembler.ts +407 -400
  56. package/src/context/index.ts +79 -79
  57. package/src/context/progress-tracker.ts +174 -174
  58. package/src/context/standards-manager.ts +287 -287
  59. package/src/context/validator.ts +58 -58
  60. package/src/diagnostics/index.ts +122 -121
  61. package/src/health/index.ts +233 -232
  62. package/src/hooks/brain-hook.ts +134 -131
  63. package/src/hooks/capture.ts +168 -168
  64. package/src/hooks/claude-code-mastery.md +112 -112
  65. package/src/hooks/context-hook.ts +260 -245
  66. package/src/hooks/deduplicator.ts +72 -72
  67. package/src/hooks/git-capture.ts +109 -109
  68. package/src/hooks/git-hook-installer.ts +211 -207
  69. package/src/hooks/index.ts +20 -20
  70. package/src/hooks/installer.ts +306 -288
  71. package/src/hooks/interceptor-hook.ts +204 -201
  72. package/src/hooks/passive-classifier.ts +397 -397
  73. package/src/hooks/queue.ts +160 -129
  74. package/src/hooks/session-tracker.ts +312 -312
  75. package/src/hooks/types.ts +52 -52
  76. package/src/index.ts +7 -7
  77. package/src/intelligence/cross-project/generalizer.ts +283 -283
  78. package/src/intelligence/cross-project/index.ts +7 -7
  79. package/src/intelligence/hf-downloader.ts +222 -222
  80. package/src/intelligence/hf-manifest.json +78 -78
  81. package/src/intelligence/index.ts +24 -24
  82. package/src/intelligence/inference-router.ts +762 -762
  83. package/src/intelligence/model-manager.ts +263 -245
  84. package/src/intelligence/optimization/index.ts +10 -10
  85. package/src/intelligence/optimization/precompute.ts +202 -202
  86. package/src/intelligence/optimization/semantic-cache.ts +213 -207
  87. package/src/intelligence/prediction/index.ts +7 -7
  88. package/src/intelligence/prediction/recommender.ts +276 -268
  89. package/src/intelligence/reasoning/chain-retrieval.ts +243 -247
  90. package/src/intelligence/reasoning/index.ts +7 -7
  91. package/src/intelligence/temporal/evolution.ts +193 -197
  92. package/src/intelligence/temporal/index.ts +16 -16
  93. package/src/intelligence/temporal/query-processor.ts +190 -190
  94. package/src/intelligence/temporal/timeline.ts +272 -259
  95. package/src/intelligence/temporal/trends.ts +263 -263
  96. package/src/intelligence/tokenizer.ts +118 -118
  97. package/src/knowledge/entity-extractor.ts +447 -443
  98. package/src/knowledge/graph/builder.ts +185 -185
  99. package/src/knowledge/graph/linker.ts +201 -201
  100. package/src/knowledge/graph/memory-graph.ts +359 -359
  101. package/src/knowledge/graph/schema.ts +99 -99
  102. package/src/knowledge/graph/search.ts +166 -166
  103. package/src/knowledge/relationship-extractor.ts +108 -108
  104. package/src/memory/chroma/client.ts +211 -192
  105. package/src/memory/chroma/collection-manager.ts +92 -92
  106. package/src/memory/chroma/config.ts +57 -57
  107. package/src/memory/chroma/embeddings.ts +177 -175
  108. package/src/memory/chroma/index.ts +82 -82
  109. package/src/memory/chroma/migration.ts +270 -270
  110. package/src/memory/chroma/schemas.ts +69 -69
  111. package/src/memory/chroma/search.ts +319 -315
  112. package/src/memory/chroma/store.ts +755 -747
  113. package/src/memory/compression.ts +121 -121
  114. package/src/memory/consolidation/archiver.ts +162 -165
  115. package/src/memory/consolidation/merger.ts +182 -186
  116. package/src/memory/consolidation/scorer.ts +136 -136
  117. package/src/memory/database.ts +9 -0
  118. package/src/memory/dual-write.ts +145 -0
  119. package/src/memory/embeddings.ts +226 -226
  120. package/src/memory/episodic/detector.ts +108 -108
  121. package/src/memory/episodic/manager.ts +347 -351
  122. package/src/memory/episodic/summarizer.ts +179 -179
  123. package/src/memory/episodic/types.ts +52 -52
  124. package/src/memory/fts5-search.ts +692 -633
  125. package/src/memory/index.ts +943 -1060
  126. package/src/memory/migrations/add-fts5.ts +118 -108
  127. package/src/memory/patterns.ts +438 -438
  128. package/src/memory/pruning.ts +60 -60
  129. package/src/memory/schema.ts +88 -88
  130. package/src/memory/store.ts +911 -787
  131. package/src/orchestrator/handlers/decision-handler.ts +204 -204
  132. package/src/packs/index.ts +9 -9
  133. package/src/packs/loader.ts +134 -134
  134. package/src/packs/manager.ts +204 -204
  135. package/src/packs/ranker.ts +78 -78
  136. package/src/packs/types.ts +81 -81
  137. package/src/phase12/index.ts +5 -5
  138. package/src/retrieval/bm25/index.ts +300 -297
  139. package/src/retrieval/bm25/tokenizer.ts +184 -184
  140. package/src/retrieval/feedback/adaptive.ts +221 -221
  141. package/src/retrieval/feedback/index.ts +16 -16
  142. package/src/retrieval/feedback/metrics.ts +221 -221
  143. package/src/retrieval/feedback/store.ts +283 -283
  144. package/src/retrieval/fusion/index.ts +194 -194
  145. package/src/retrieval/fusion/rrf.ts +165 -165
  146. package/src/retrieval/index.ts +12 -12
  147. package/src/retrieval/pipeline.ts +375 -375
  148. package/src/retrieval/query/expander.ts +203 -203
  149. package/src/retrieval/query/index.ts +27 -27
  150. package/src/retrieval/query/intent-classifier.ts +252 -252
  151. package/src/retrieval/query/temporal-parser.ts +295 -295
  152. package/src/retrieval/reranker/index.ts +189 -188
  153. package/src/retrieval/reranker/model.ts +99 -95
  154. package/src/retrieval/service.ts +125 -125
  155. package/src/retrieval/types.ts +162 -162
  156. package/src/routing/entity-extractor.ts +454 -454
  157. package/src/routing/handlers/exploration-handler.ts +369 -0
  158. package/src/routing/handlers/index.ts +19 -0
  159. package/src/routing/handlers/memory-handler.ts +273 -0
  160. package/src/routing/handlers/mutation-handler.ts +241 -0
  161. package/src/routing/handlers/recall-handler.ts +642 -0
  162. package/src/routing/handlers/shared.ts +515 -0
  163. package/src/routing/handlers/types.ts +48 -0
  164. package/src/routing/intent-classifier.ts +552 -552
  165. package/src/routing/response-filter.ts +399 -391
  166. package/src/routing/router.ts +245 -2193
  167. package/src/routing/search-engine.ts +521 -514
  168. package/src/routing/types.ts +104 -94
  169. package/src/scripts/health-check.ts +118 -118
  170. package/src/scripts/setup.ts +122 -122
  171. package/src/server/auto-updater.ts +283 -276
  172. package/src/server/handlers/call-tool.ts +159 -159
  173. package/src/server/handlers/list-tools.ts +35 -35
  174. package/src/server/handlers/tools/auto-remember.ts +165 -165
  175. package/src/server/handlers/tools/brain.ts +86 -86
  176. package/src/server/handlers/tools/create-project.ts +135 -135
  177. package/src/server/handlers/tools/get-code-standards.ts +123 -123
  178. package/src/server/handlers/tools/get-corrections.ts +152 -152
  179. package/src/server/handlers/tools/get-patterns.ts +156 -156
  180. package/src/server/handlers/tools/get-project-context.ts +75 -75
  181. package/src/server/handlers/tools/index.ts +30 -30
  182. package/src/server/handlers/tools/init-project.ts +756 -756
  183. package/src/server/handlers/tools/list-projects.ts +126 -126
  184. package/src/server/handlers/tools/recall-similar.ts +87 -87
  185. package/src/server/handlers/tools/recognize-pattern.ts +132 -132
  186. package/src/server/handlers/tools/record-correction.ts +131 -131
  187. package/src/server/handlers/tools/remember-decision.ts +168 -168
  188. package/src/server/handlers/tools/schemas.ts +179 -179
  189. package/src/server/handlers/tools/search-code.ts +122 -122
  190. package/src/server/handlers/tools/smart-context.ts +146 -146
  191. package/src/server/handlers/tools/update-progress.ts +131 -131
  192. package/src/server/http-api.ts +215 -1229
  193. package/src/server/mcp-proxy.ts +85 -84
  194. package/src/server/mcp-server.ts +285 -284
  195. package/src/server/middleware/auth.ts +39 -0
  196. package/src/server/middleware/error-handler.ts +37 -0
  197. package/src/server/middleware/rate-limit.ts +53 -0
  198. package/src/server/middleware/validate.ts +42 -0
  199. package/src/server/pid-manager.ts +137 -136
  200. package/src/server/providers/resources.ts +581 -581
  201. package/src/server/routes/code.ts +228 -0
  202. package/src/server/routes/context.ts +26 -0
  203. package/src/server/routes/health.ts +19 -0
  204. package/src/server/routes/helpers.ts +100 -0
  205. package/src/server/routes/hooks.ts +197 -0
  206. package/src/server/routes/mcp.ts +47 -0
  207. package/src/server/routes/memory.ts +397 -0
  208. package/src/server/routes/models.ts +96 -0
  209. package/src/server/routes/projects.ts +89 -0
  210. package/src/server/routes/types.ts +21 -0
  211. package/src/server/schemas/api-schemas.ts +202 -0
  212. package/src/server/services.ts +720 -720
  213. package/src/server/utils/memory-indicator.ts +84 -84
  214. package/src/server/utils/response-formatter.ts +129 -129
  215. package/src/server/web-viewer.ts +1145 -1115
  216. package/src/setup/index.ts +38 -38
  217. package/src/tools/registry.ts +115 -115
  218. package/src/tools/schemas.ts +666 -666
  219. package/src/tools/types.ts +412 -412
  220. package/src/training/data-store.ts +320 -298
  221. package/src/training/retrain-pipeline.ts +399 -394
  222. package/src/utils/error-handler.ts +136 -136
  223. package/src/utils/index.ts +58 -58
  224. package/src/utils/kill-port.ts +55 -53
  225. package/src/utils/phase12-helper.ts +56 -56
  226. package/src/utils/safe-path.ts +43 -0
  227. package/src/utils/timing.ts +47 -47
  228. package/src/utils/transaction.ts +63 -63
  229. package/src/vault/index.ts +4 -3
  230. package/src/vault/paths.ts +106 -106
  231. package/src/vault/query.ts +4 -1
  232. package/src/vault/reader.ts +44 -1
  233. package/src/vault/watcher.ts +24 -1
  234. package/src/vault/writer.ts +487 -413
  235. package/skills/persistent-memory/SKILL.md +0 -148
  236. package/skills/persistent-memory/references/tool-reference.md +0 -90
@@ -1,2193 +1,245 @@
1
- /**
2
- * Brain Router
3
- * Phase 16 + Phase 19: Core orchestrator for the unified brain() tool
4
- *
5
- * Routes classified intents to internal service calls and
6
- * returns unified BrainResponse objects.
7
- *
8
- * Phase 19: Uses SearchEngine for all searches (hybrid, cached, temporal).
9
- * Wires Timeline, Evolution, Trends, ChainRetrieval, Episodes,
10
- * Recommender, CrossProject patterns, and KnowledgeGraph enrichment.
11
- */
12
-
13
- import type { Logger } from 'pino'
14
- import { IntentClassifier, type ClassificationResult } from './intent-classifier'
15
- import type { InferenceRouter } from '@/intelligence/inference-router'
16
- import { BrainEntityExtractor, type BrainExtractedEntities } from './entity-extractor'
17
- import { ResponseFilter, type BrainResponse, type TierResults, type FilterableResult, formatCompactResponse, formatDetailResponse, formatTimeline, groupByDay } from './response-filter'
18
- import { SearchEngine } from './search-engine'
19
- import type { NormalizedResult } from './types'
20
- import {
21
- getMemoryService,
22
- getVaultService,
23
- getContextService,
24
- getPhase12Service,
25
- getEpisodeService,
26
- getSessionTracker,
27
- getCodeLinker,
28
- getCodeQuery,
29
- isServicesInitialized
30
- } from '@/server/services'
31
- import { timed } from '@/utils/timing'
32
- import type { ObservationCompressor } from '@/memory/compression'
33
-
34
- /** Default project when none can be detected */
35
- const DEFAULT_PROJECT = 'general'
36
-
37
- /** Phase 19 D4: Minimum similarity for destructive operations */
38
- const DELETE_MIN_SIMILARITY = 0.3
39
- const UPDATE_MIN_SIMILARITY = 0.3
40
-
41
- export interface BrainInput {
42
- message: string
43
- project?: string
44
- action?: 'auto' | 'store' | 'recall' | 'update' | 'delete'
45
- }
46
-
47
- /** Recently stored decision for recency fast path (Bug 4 fix) */
48
- interface RecentDecision {
49
- content: string
50
- project: string
51
- storedAt: number
52
- /** Keyword overlap ratio with query (0-1), computed by findRecentDecisions */
53
- overlapScore?: number
54
- }
55
-
56
- /** How long recent decisions stay eligible for fast-path recall (ms) */
57
- const RECENT_DECISION_WINDOW_MS = 60_000
58
-
59
- export class BrainRouter {
60
- private classifier: IntentClassifier
61
- private entityExtractor: BrainEntityExtractor
62
- private responseFilter: ResponseFilter
63
- private searchEngine: SearchEngine
64
- private logger: Logger
65
-
66
- /** SLM Upgrade: Optional inference router for model-based classification */
67
- private inferenceRouter: InferenceRouter | null = null
68
-
69
- /** Phase 30: Optional LLM compressor for long observations */
70
- private compressor: ObservationCompressor | null = null
71
-
72
- /** Track the most recently stored decision ID for update/delete operations */
73
- private lastStoredId: string | null = null
74
- private lastStoredProject: string | null = null
75
-
76
- /** Bug 4: Recently stored decisions for recency fast path */
77
- private recentDecisions: RecentDecision[] = []
78
-
79
- constructor(logger: Logger) {
80
- this.classifier = new IntentClassifier()
81
- this.entityExtractor = new BrainEntityExtractor()
82
- this.responseFilter = new ResponseFilter()
83
- this.searchEngine = new SearchEngine(logger)
84
- this.logger = logger.child({ component: 'brain-router' })
85
- }
86
-
87
- /** SLM Upgrade: Set the optional inference router for model-based classification */
88
- setInferenceRouter(router: InferenceRouter): void {
89
- this.inferenceRouter = router
90
- this.entityExtractor.setInferenceRouter(router)
91
- }
92
-
93
- /** Phase 30: Set the optional LLM compressor */
94
- setCompressor(compressor: ObservationCompressor): void {
95
- this.compressor = compressor
96
- }
97
-
98
- async route(input: BrainInput): Promise<BrainResponse> {
99
- const { message, project: inputProject, action } = input
100
-
101
- // Extract entities (needed for all paths)
102
- const entities = await this.entityExtractor.extract(message, inputProject)
103
- const project = entities.project || inputProject
104
- this.logger.debug({ project, technologies: entities.technologies }, 'Entities extracted')
105
-
106
- // P1: Explicit action override — bypass intent classifier entirely
107
- if (action && action !== 'auto') {
108
- this.logger.debug({ action }, 'Using explicit action override')
109
- try {
110
- switch (action) {
111
- case 'store':
112
- return this.handleStoreThis(message, project, entities)
113
- case 'recall':
114
- return this.handleContextNeeded(message, project, entities)
115
- case 'update':
116
- return this.handleUpdateMemory(message, project, entities)
117
- case 'delete':
118
- return this.handleDeleteMemory(message, project, entities)
119
- default:
120
- break
121
- }
122
- } catch (error) {
123
- this.logger.error({ error, action }, 'Router action override error')
124
- return {
125
- action: 'none',
126
- summary: `Error processing ${action} request`,
127
- content: `Failed to process: ${error instanceof Error ? error.message : 'Unknown error'}`,
128
- relevantItems: 0
129
- }
130
- }
131
- }
132
-
133
- // Classify intent (SLM: use inference router if available, falls back to regex)
134
- const classification = this.inferenceRouter
135
- ? await this.inferenceRouter.classifyIntent(message)
136
- : this.classifier.classify(message)
137
- this.logger.debug({ intent: classification.primary, confidence: classification.confidence }, 'Intent classified')
138
-
139
- // Route to handler
140
- try {
141
- switch (classification.primary) {
142
- case 'no_action':
143
- return this.handleNoAction(message)
144
-
145
- case 'session_start':
146
- return this.handleSessionStart(message, project, entities)
147
-
148
- case 'context_needed':
149
- return this.handleContextNeeded(message, project, entities, classification)
150
-
151
- case 'decision_made':
152
- return this.handleDecisionMade(message, project, entities)
153
-
154
- case 'store_this':
155
- return this.handleStoreThis(message, project, entities)
156
-
157
- case 'pattern_found':
158
- return this.handlePatternFound(message, project, entities)
159
-
160
- case 'mistake_learned':
161
- return this.handleMistakeLearned(message, project, entities)
162
-
163
- case 'progress_update':
164
- return this.handleProgressUpdate(message, project, entities)
165
-
166
- case 'question':
167
- return this.handleQuestion(message, project, entities, classification)
168
-
169
- case 'comparison':
170
- return this.handleComparison(message, project, entities)
171
-
172
- case 'exploration':
173
- return this.handleExploration(message, project, entities)
174
-
175
- case 'list_all':
176
- return this.handleListAll(message, project, entities)
177
-
178
- case 'update_memory':
179
- return this.handleUpdateMemory(message, project, entities)
180
-
181
- case 'delete_memory':
182
- return this.handleDeleteMemory(message, project, entities)
183
-
184
- case 'detail_request':
185
- return this.handleDetailRequest(message, project, entities)
186
-
187
- case 'timeline':
188
- return this.handleTimeline(message, project, entities)
189
-
190
- default:
191
- return this.handleContextNeeded(message, project, entities, classification)
192
- }
193
- } catch (error) {
194
- this.logger.error({ error, intent: classification.primary }, 'Router handler error')
195
- return {
196
- action: 'none',
197
- summary: `Error processing request`,
198
- content: `Failed to process: ${error instanceof Error ? error.message : 'Unknown error'}`,
199
- relevantItems: 0
200
- }
201
- }
202
- }
203
-
204
- // ===== Intent Handlers =====
205
-
206
- private handleNoAction(_message: string): BrainResponse {
207
- return {
208
- action: 'none',
209
- summary: 'No action needed',
210
- content: '',
211
- relevantItems: 0
212
- }
213
- }
214
-
215
- private async handleSessionStart(
216
- message: string,
217
- project: string | undefined,
218
- entities: BrainExtractedEntities
219
- ): Promise<BrainResponse> {
220
- if (!project) {
221
- return {
222
- action: 'none',
223
- summary: 'No project detected',
224
- content: 'Could not determine project. Please specify which project you are working on.',
225
- relevantItems: 0
226
- }
227
- }
228
-
229
- if (!isServicesInitialized()) {
230
- return this.servicesNotReady()
231
- }
232
-
233
- const contextService = getContextService()
234
- const phase12 = getPhase12Service()
235
-
236
- // Get project context
237
- const context = await contextService.getContext(project, {
238
- includeMemories: false,
239
- includeProgress: true,
240
- includeStandards: true,
241
- maxTokens: 6000,
242
- relevanceThreshold: 0.5
243
- })
244
-
245
- const formattedContext = contextService.formatter.format(context)
246
-
247
- // Process with Phase 12 for proactive recall
248
- const phase12Result = await phase12.processMessage(
249
- entities.topic || message,
250
- project
251
- )
252
-
253
- // Build response
254
- const parts: string[] = [formattedContext]
255
-
256
- if (phase12Result.recalledMemories?.memories.length) {
257
- parts.push('\n---\n## Relevant Past Decisions\n')
258
- for (const mem of phase12Result.recalledMemories.memories) {
259
- const similarity = Math.round((mem.similarity || 0) * 100)
260
- const decisionText = mem.decision?.decision || mem.memory?.content || ''
261
- const safeText = typeof decisionText === 'string' ? decisionText : JSON.stringify(decisionText)
262
- parts.push(`**[${similarity}%]** ${safeText.slice(0, 200)}`)
263
- if (mem.decision?.reasoning) {
264
- const reasoning = typeof mem.decision.reasoning === 'string' ? mem.decision.reasoning : JSON.stringify(mem.decision.reasoning)
265
- parts.push(` _${reasoning}_`)
266
- }
267
- }
268
- }
269
-
270
- // If Phase 12 found nothing, do a direct search fallback via SearchEngine
271
- if (!phase12Result.recalledMemories?.memories.length) {
272
- try {
273
- const directResults = await this.searchEngine.enhancedSearch(entities.topic || message, {
274
- project,
275
- limit: 5,
276
- minSimilarity: 0.3
277
- })
278
- if (directResults.length > 0) {
279
- parts.push('\n---\n## Related Memories\n')
280
- for (const r of directResults) {
281
- const similarity = Math.round((r.score || 0) * 100)
282
- const safeContent = typeof r.content === 'string' ? r.content : JSON.stringify(r.content ?? '')
283
- parts.push(`**[${similarity}%]** ${safeContent.slice(0, 200)}`)
284
- }
285
- }
286
- } catch {
287
- // Direct search failed, continue without
288
- }
289
- }
290
-
291
- // Phase 32: Include code file map in session context if code index is available
292
- try {
293
- const codeQuery = getCodeQuery()
294
- if (codeQuery && project) {
295
- const fileMap = codeQuery.getFileMap(project, 50)
296
- if (fileMap.length > 0) {
297
- const formatted = codeQuery.formatFileMap(fileMap)
298
- parts.push('\n---\n## Code Structure\n')
299
- parts.push(formatted)
300
- }
301
- }
302
- } catch {
303
- // Code intelligence not available
304
- }
305
-
306
- // C8: Register with episode manager
307
- this.registerEpisodeMessage(message, project, 'session_start')
308
-
309
- // C9: Append proactive recommendations
310
- try {
311
- const recommendations = await this.searchEngine.getRecommendations(
312
- entities.topic || message,
313
- { project, limit: 3 }
314
- )
315
- if (recommendations?.recommendations?.length) {
316
- parts.push('\n---\n## Proactive Recommendations\n')
317
- for (const rec of recommendations.recommendations) {
318
- parts.push(`- **${rec.type}**: ${rec.content?.slice(0, 150) || ''}`)
319
- if (rec.reasoning) parts.push(` _${rec.reasoning}_`)
320
- }
321
- }
322
- } catch {
323
- // Recommendations not available
324
- }
325
-
326
- // Phase 25: Query past session summaries for this project
327
- try {
328
- let sessionSummaries = await this.searchEngine.plainSearch(
329
- 'session summary',
330
- { project: project || undefined, limit: 5, minSimilarity: 0.3 }
331
- )
332
-
333
- // Fallback: if plainSearch returns 0, try enhancedSearch with broader query
334
- if (sessionSummaries.length === 0) {
335
- sessionSummaries = await this.searchEngine.enhancedSearch(
336
- `session summary ${project || ''}`.trim(),
337
- { project: project || undefined, limit: 5, minSimilarity: 0.2 }
338
- )
339
- }
340
-
341
- let summaryItems = sessionSummaries.filter(r =>
342
- (r.metadata as Record<string, unknown>)?.tags &&
343
- Array.isArray((r.metadata as Record<string, unknown>).tags) &&
344
- ((r.metadata as Record<string, unknown>).tags as string[]).includes('session-summary') ||
345
- r.content.toLowerCase().startsWith('session summary:') ||
346
- r.content.toLowerCase().includes('worked on')
347
- )
348
-
349
- // Sort by date descending (most recent first)
350
- summaryItems.sort((a, b) => {
351
- const dateA = a.date || (a.metadata as Record<string, unknown>)?.created_at as string || ''
352
- const dateB = b.date || (b.metadata as Record<string, unknown>)?.created_at as string || ''
353
- return dateB.localeCompare(dateA)
354
- })
355
-
356
- if (summaryItems.length > 0) {
357
- parts.push('\n---\n## Past Sessions\n')
358
- for (const s of summaryItems.slice(0, 3)) {
359
- const safeContent = typeof s.content === 'string' ? s.content : JSON.stringify(s.content ?? '')
360
- parts.push(`- ${safeContent.slice(0, 300)}`)
361
- }
362
- }
363
- } catch {
364
- // Non-critical — skip if search fails
365
- }
366
-
367
- const totalRecalled = phase12Result.recalledMemories?.memories.length || 0
368
- return {
369
- action: 'retrieved',
370
- summary: `Session context for ${project}${totalRecalled ? ` (${totalRecalled} memories)` : ''}`,
371
- content: parts.join('\n'),
372
- relevantItems: totalRecalled
373
- }
374
- }
375
-
376
- private async handleContextNeeded(
377
- message: string,
378
- project: string | undefined,
379
- entities: BrainExtractedEntities,
380
- classification?: ClassificationResult
381
- ): Promise<BrainResponse> {
382
- if (!isServicesInitialized()) {
383
- return this.servicesNotReady()
384
- }
385
-
386
- // Phase 25: Use undefined for search when no project detected (search all projects)
387
- const searchProject = project || undefined
388
- const displayProject = project || DEFAULT_PROJECT
389
- const query = entities.topic || message
390
- const tiers: TierResults[] = []
391
- const hasTemporal = classification?.secondary.includes('exploration') ||
392
- this.classifier.hasTemporalSignal(message.toLowerCase())
393
-
394
- // Use temporal search if temporal signals detected
395
- if (hasTemporal) {
396
- const { results } = await this.searchEngine.temporalSearch(query, { project: searchProject, limit: 5 })
397
- if (results.length > 0) {
398
- tiers.push({
399
- label: 'Memories',
400
- results: results.map(r => ({
401
- content: r.content,
402
- score: r.score,
403
- source: r.source === 'decision' ? 'Past Decision' : r.source,
404
- metadata: r.metadata as Record<string, unknown>
405
- }))
406
- })
407
- }
408
- } else {
409
- // Standard enhanced search
410
- const searchResults = await this.searchEngine.enhancedSearch(query, {
411
- project: searchProject,
412
- limit: 5,
413
- minSimilarity: 0.3
414
- })
415
- if (searchResults.length > 0) {
416
- tiers.push({
417
- label: 'Memories',
418
- results: searchResults.map(r => ({
419
- content: r.content,
420
- score: r.score,
421
- source: r.source === 'decision' ? 'Past Decision' : r.source,
422
- metadata: r.metadata as Record<string, unknown>
423
- }))
424
- })
425
- }
426
- }
427
-
428
- // Also search patterns
429
- const patternResults = await this.searchEngine.searchPatterns(query, { project: searchProject, limit: 3 })
430
- if (patternResults.length > 0) {
431
- tiers.push({
432
- label: 'Patterns',
433
- results: patternResults.map(p => ({
434
- content: p.content,
435
- score: p.score,
436
- source: `Pattern`,
437
- metadata: p.metadata as Record<string, unknown>
438
- }))
439
- })
440
- }
441
-
442
- // Phase 32: Code intelligence — query code index for matching symbols/files
443
- const codeTier = this.queryCodeIntelligence(query, searchProject)
444
- if (codeTier) {
445
- tiers.push(codeTier)
446
- }
447
-
448
- const response = this.responseFilter.synthesize(tiers, message, displayProject)
449
-
450
- // P0: When no results found and message looks like a plain statement (not a question),
451
- // add a hint that the user may have intended to store it
452
- if (response.action === 'none' && response.relevantItems === 0) {
453
- const isPlainStatement = message.length > 15 &&
454
- !message.trim().endsWith('?') &&
455
- message.split(/\s+/).length > 3
456
- if (isPlainStatement) {
457
- response.content += `\n\n**Tip:** If you meant to save this, try:\n- \`brain("Remember: ${message.slice(0, 60)}")\`\n- Or use \`action: "store"\` to force storage: \`brain({ message: "...", action: "store" })\``
458
- }
459
- }
460
-
461
- return response
462
- }
463
-
464
- /**
465
- * Handle explicit "store this" requests — always uses the FULL message as the decision text.
466
- */
467
- private async handleStoreThis(
468
- message: string,
469
- project: string | undefined,
470
- entities: BrainExtractedEntities
471
- ): Promise<BrainResponse> {
472
- const effectiveProject = project || DEFAULT_PROJECT
473
-
474
- if (!isServicesInitialized()) {
475
- return this.servicesNotReady()
476
- }
477
-
478
- const memory = getMemoryService()
479
- const reasoning = entities.reasoning || ''
480
- const context = entities.topic || message.slice(0, 200)
481
-
482
- // Phase 30: Optional LLM compression
483
- const { content: decision, rawContent } = await this.maybeCompress(message, 'store')
484
-
485
- const decisionId = await timed('brain:store', () => memory.rememberDecision(
486
- effectiveProject,
487
- context,
488
- decision,
489
- reasoning,
490
- {
491
- alternatives: entities.alternatives,
492
- tags: entities.technologies.length > 0 ? entities.technologies : ['user-stored']
493
- }
494
- ))
495
-
496
- // Track for potential update/delete
497
- this.lastStoredId = decisionId
498
- this.lastStoredProject = effectiveProject
499
-
500
- // Bug 4: Track for recency fast path
501
- this.trackRecentDecision(decision, effectiveProject)
502
-
503
- // Invalidate cache for this project
504
- this.searchEngine.invalidateCache(effectiveProject)
505
-
506
- // Background: vault write + episode linking + code linkage (non-critical)
507
- setImmediate(() => {
508
- this.writeToVault(effectiveProject, decision, reasoning, context, entities.alternatives)
509
- this.linkToActiveEpisode(effectiveProject, decisionId, 'decision')
510
- this.linkToCodeFiles(decisionId, decision, effectiveProject)
511
- })
512
-
513
- const compressedNote = rawContent ? '\n*(compressed)*' : ''
514
- return {
515
- action: 'stored',
516
- summary: `Stored: ${message.slice(0, 60)}`,
517
- content: `Memory stored (ID: ${decisionId})\n\n**Project:** ${effectiveProject}\n**Content:** ${decision}${reasoning ? `\n**Reasoning:** ${reasoning}` : ''}${compressedNote}`,
518
- relevantItems: 1
519
- }
520
- }
521
-
522
- private async handleDecisionMade(
523
- message: string,
524
- project: string | undefined,
525
- entities: BrainExtractedEntities
526
- ): Promise<BrainResponse> {
527
- const effectiveProject = project || DEFAULT_PROJECT
528
-
529
- if (!isServicesInitialized()) {
530
- return this.servicesNotReady()
531
- }
532
-
533
- const memory = getMemoryService()
534
- const reasoning = entities.reasoning || ''
535
- const context = entities.topic || message.slice(0, 200)
536
- const alternatives = entities.alternatives
537
-
538
- // Phase 30: Optional LLM compression
539
- const { content: decision, rawContent } = await this.maybeCompress(message, 'decision')
540
-
541
- const decisionId = await timed('brain:store', () => memory.rememberDecision(
542
- effectiveProject,
543
- context,
544
- decision,
545
- reasoning,
546
- {
547
- alternatives,
548
- tags: entities.technologies.length > 0 ? entities.technologies : ['auto-detected']
549
- }
550
- ))
551
-
552
- this.lastStoredId = decisionId
553
- this.lastStoredProject = effectiveProject
554
-
555
- // Bug 4: Track for recency fast path
556
- this.trackRecentDecision(decision, effectiveProject)
557
-
558
- // Invalidate cache
559
- this.searchEngine.invalidateCache(effectiveProject)
560
-
561
- // Background: vault write + episode linking + code linkage (non-critical)
562
- setImmediate(() => {
563
- this.writeToVault(effectiveProject, decision, reasoning, context, alternatives)
564
- this.linkToActiveEpisode(effectiveProject, decisionId, 'decision')
565
- this.linkToCodeFiles(decisionId, decision, effectiveProject)
566
- })
567
-
568
- const compressedNote = rawContent ? '\n*(compressed)*' : ''
569
- return {
570
- action: 'stored',
571
- summary: `Stored decision: ${message.slice(0, 60)}`,
572
- content: `Decision stored (ID: ${decisionId})\n\n**Project:** ${effectiveProject}\n**Decision:** ${decision}\n**Reasoning:** ${reasoning}${compressedNote}`,
573
- relevantItems: 1
574
- }
575
- }
576
-
577
- private async handlePatternFound(
578
- message: string,
579
- project: string | undefined,
580
- entities: BrainExtractedEntities
581
- ): Promise<BrainResponse> {
582
- const effectiveProject = project || DEFAULT_PROJECT
583
-
584
- if (!isServicesInitialized()) {
585
- return this.servicesNotReady()
586
- }
587
-
588
- const memory = getMemoryService()
589
- const patternType = entities.patternType || 'solution'
590
- const description = message
591
-
592
- const patternId = await memory.storePattern({
593
- project: effectiveProject,
594
- pattern_type: patternType,
595
- description,
596
- confidence: 0.8,
597
- context: message.slice(0, 300)
598
- })
599
-
600
- // Invalidate cache
601
- this.searchEngine.invalidateCache(effectiveProject)
602
-
603
- // Link to active episode + code files
604
- this.linkToActiveEpisode(effectiveProject, patternId, 'pattern')
605
- this.linkToCodeFiles(patternId, description, effectiveProject)
606
-
607
- return {
608
- action: 'stored',
609
- summary: `Stored ${patternType}: ${description.slice(0, 60)}`,
610
- content: `Pattern stored (ID: ${patternId})\n\n**Type:** ${patternType}\n**Project:** ${effectiveProject}\n**Description:** ${description}`,
611
- relevantItems: 1
612
- }
613
- }
614
-
615
- private async handleMistakeLearned(
616
- message: string,
617
- project: string | undefined,
618
- entities: BrainExtractedEntities
619
- ): Promise<BrainResponse> {
620
- const effectiveProject = project || DEFAULT_PROJECT
621
-
622
- if (!isServicesInitialized()) {
623
- return this.servicesNotReady()
624
- }
625
-
626
- const memory = getMemoryService()
627
- const original = entities.original || message
628
- const correction = entities.correction || ''
629
- const reasoning = entities.reasoning || 'Lesson learned from experience'
630
-
631
- const correctionId = await memory.storeCorrection({
632
- project: effectiveProject,
633
- original,
634
- correction: correction || message,
635
- reasoning,
636
- context: entities.topic || '',
637
- confidence: 0.9
638
- })
639
-
640
- // Invalidate cache
641
- this.searchEngine.invalidateCache(effectiveProject)
642
-
643
- // Link to active episode + code files
644
- this.linkToActiveEpisode(effectiveProject, correctionId, 'correction')
645
- this.linkToCodeFiles(correctionId, original, effectiveProject)
646
-
647
- return {
648
- action: 'stored',
649
- summary: `Stored correction: ${original.slice(0, 60)}`,
650
- content: `Correction stored (ID: ${correctionId})\n\n**Project:** ${effectiveProject}\n**Original:** ${original}\n**Correction:** ${correction || '(see original message)'}`,
651
- relevantItems: 1
652
- }
653
- }
654
-
655
- private async handleProgressUpdate(
656
- message: string,
657
- project: string | undefined,
658
- entities: BrainExtractedEntities
659
- ): Promise<BrainResponse> {
660
- const effectiveProject = project || DEFAULT_PROJECT
661
-
662
- if (!isServicesInitialized()) {
663
- return this.servicesNotReady()
664
- }
665
-
666
- const contextService = getContextService()
667
- const memory = getMemoryService()
668
-
669
- const completedTask = entities.completedTask || message
670
- const nextSteps = entities.nextSteps || 'Continue development'
671
-
672
- // Store in session context
673
- try {
674
- await contextService.progress.addCompletedTask(effectiveProject, {
675
- id: this.generateTaskId(completedTask),
676
- title: completedTask,
677
- status: 'done',
678
- completedAt: new Date()
679
- })
680
- } catch {
681
- // Progress update failed
682
- }
683
-
684
- // Also store as a searchable memory
685
- try {
686
- await memory.rememberDecision(
687
- effectiveProject,
688
- `Progress update`,
689
- `Progress: ${completedTask}${nextSteps !== 'Continue development' ? `. Next: ${nextSteps}` : ''}`,
690
- 'Progress tracking',
691
- { tags: ['progress', ...entities.technologies] }
692
- )
693
-
694
- // Invalidate cache
695
- this.searchEngine.invalidateCache(effectiveProject)
696
- } catch {
697
- // Memory storage failed
698
- }
699
-
700
- // Register with episode
701
- this.registerEpisodeMessage(message, effectiveProject, 'progress_update')
702
-
703
- return {
704
- action: 'stored',
705
- summary: `Progress: ${completedTask.slice(0, 60)}`,
706
- content: `Progress updated for ${effectiveProject}\n\n**Completed:** ${completedTask}\n**Next:** ${nextSteps}`,
707
- relevantItems: 1
708
- }
709
- }
710
-
711
- /**
712
- * List all decisions for a project
713
- */
714
- private async handleListAll(
715
- _message: string,
716
- project: string | undefined,
717
- _entities: BrainExtractedEntities
718
- ): Promise<BrainResponse> {
719
- if (!isServicesInitialized()) {
720
- return this.servicesNotReady()
721
- }
722
-
723
- const memory = getMemoryService()
724
- const effectiveProject = project
725
-
726
- try {
727
- const decisions = await memory.fetchAllDecisions(effectiveProject)
728
- const patterns = await memory.fetchAllPatterns(effectiveProject)
729
- const corrections = await memory.fetchAllCorrections(effectiveProject)
730
-
731
- const projectLabel = effectiveProject || 'all projects'
732
- const total = decisions.length + patterns.length + corrections.length
733
-
734
- if (total === 0) {
735
- return {
736
- action: 'none',
737
- summary: `No memories found for ${projectLabel}`,
738
- content: `No decisions, patterns, or corrections stored yet for ${projectLabel}.`,
739
- relevantItems: 0
740
- }
741
- }
742
-
743
- // Phase 27: Use compact format for list_all
744
- const allItems: any[] = [
745
- ...decisions.map(d => ({
746
- id: d.id || (d as any).decision_id,
747
- content: typeof (d.decision || d.document || d.content) === 'string'
748
- ? (d.decision || d.document || d.content)
749
- : JSON.stringify(d.decision || d.document || d.content || ''),
750
- category: 'decision',
751
- project: d.project || effectiveProject || 'general',
752
- created_at: d.created_at || d.date || '',
753
- })),
754
- ...patterns.map(p => ({
755
- id: p.id,
756
- content: typeof (p.description || p.document || p.content) === 'string'
757
- ? (p.description || p.document || p.content)
758
- : JSON.stringify(p.description || p.document || p.content || ''),
759
- category: p.pattern_type || 'pattern',
760
- project: p.project || effectiveProject || 'general',
761
- created_at: p.created_at || '',
762
- })),
763
- ...corrections.map(c => ({
764
- id: c.id,
765
- content: typeof (c.original || c.document || c.content) === 'string'
766
- ? (c.original || c.document || c.content)
767
- : JSON.stringify(c.original || c.document || c.content || ''),
768
- category: 'correction',
769
- project: c.project || effectiveProject || 'general',
770
- created_at: c.created_at || '',
771
- })),
772
- ]
773
-
774
- const compactContent = formatCompactResponse(allItems, `all memories for ${projectLabel}`)
775
- return {
776
- action: 'retrieved',
777
- summary: `${total} memories for ${projectLabel}`,
778
- content: compactContent,
779
- relevantItems: total
780
- }
781
- } catch (error) {
782
- return {
783
- action: 'none',
784
- summary: 'Error listing memories',
785
- content: `Failed to list memories: ${error instanceof Error ? error.message : 'Unknown error'}`,
786
- relevantItems: 0
787
- }
788
- }
789
- }
790
-
791
- /**
792
- * Phase 19 D4: Update with higher similarity threshold
793
- */
794
- private async handleUpdateMemory(
795
- message: string,
796
- project: string | undefined,
797
- entities: BrainExtractedEntities
798
- ): Promise<BrainResponse> {
799
- const effectiveProject = project || this.lastStoredProject || DEFAULT_PROJECT
800
-
801
- if (!isServicesInitialized()) {
802
- return this.servicesNotReady()
803
- }
804
-
805
- const memory = getMemoryService()
806
- const topic = entities.topic || message
807
- const hasSpecificContent = this.hasDescriptiveContent(message)
808
-
809
- if (hasSpecificContent) {
810
- try {
811
- const results = await memory.searchRaw(topic, {
812
- project: effectiveProject,
813
- limit: 1,
814
- minSimilarity: UPDATE_MIN_SIMILARITY
815
- })
816
-
817
- const matchId = results[0]?.memory?.id || results[0]?.decision?.id || results[0]?.id
818
- const matchSimilarity = results[0]?.similarity || 0
819
-
820
- if (results.length > 0 && matchId && matchSimilarity >= UPDATE_MIN_SIMILARITY) {
821
- const oldId = matchId
822
- const newId = await memory.updateDecision(
823
- oldId,
824
- effectiveProject,
825
- `Updated: ${topic.slice(0, 200)}`,
826
- message,
827
- entities.reasoning || 'Updated via brain tool',
828
- { tags: entities.technologies.length > 0 ? entities.technologies : ['updated'] }
829
- )
830
-
831
- this.lastStoredId = newId
832
- this.lastStoredProject = effectiveProject
833
-
834
- // Invalidate cache
835
- this.searchEngine.invalidateCache(effectiveProject)
836
-
837
- const oldContent = results[0].decision?.decision || results[0].memory?.content?.slice(0, 100) || results[0].content?.slice(0, 100) || ''
838
- return {
839
- action: 'stored',
840
- summary: `Updated decision`,
841
- content: `Replaced: "${oldContent.slice(0, 80)}"\n\nWith:\n**New content:** ${message}`,
842
- relevantItems: 1
843
- }
844
- } else if (results.length > 0 && matchSimilarity < UPDATE_MIN_SIMILARITY) {
845
- // D4: No confident match — store as new instead of updating wrong memory
846
- return this.storeAsNew(memory, effectiveProject, message, topic, entities, 'No confident match to update (similarity too low)')
847
- }
848
- } catch {
849
- // Search failed, fall through
850
- }
851
- }
852
-
853
- // Generic update — use lastStoredId
854
- if (this.lastStoredId) {
855
- const newId = await memory.updateDecision(
856
- this.lastStoredId,
857
- effectiveProject,
858
- `Updated: ${topic.slice(0, 200)}`,
859
- message,
860
- entities.reasoning || 'Updated via brain tool',
861
- { tags: entities.technologies.length > 0 ? entities.technologies : ['updated'] }
862
- )
863
-
864
- this.lastStoredId = newId
865
- this.lastStoredProject = effectiveProject
866
- this.searchEngine.invalidateCache(effectiveProject)
867
-
868
- return {
869
- action: 'stored',
870
- summary: `Updated decision`,
871
- content: `Previous decision replaced with:\n\n**Project:** ${effectiveProject}\n**New content:** ${message}`,
872
- relevantItems: 1
873
- }
874
- }
875
-
876
- // Fallback: store as new decision
877
- return this.storeAsNew(memory, effectiveProject, message, topic, entities, 'No matching decision found to update')
878
- }
879
-
880
- /**
881
- * Phase 19 D4: Delete with higher similarity threshold
882
- */
883
- private async handleDeleteMemory(
884
- message: string,
885
- project: string | undefined,
886
- entities: BrainExtractedEntities
887
- ): Promise<BrainResponse> {
888
- const effectiveProject = project || this.lastStoredProject || DEFAULT_PROJECT
889
-
890
- if (!isServicesInitialized()) {
891
- return this.servicesNotReady()
892
- }
893
-
894
- const memory = getMemoryService()
895
- const topic = entities.topic || message
896
- const hasSpecificContent = this.hasDescriptiveContent(message)
897
- const lower = message.toLowerCase()
898
-
899
- // BUG-006 T6: Bulk delete — "delete all memories for project X"
900
- if (lower.includes('delete all') || lower.includes('remove all') || lower.includes('clear all')) {
901
- const bulkProject = entities.project || project || this.lastStoredProject
902
- if (bulkProject) {
903
- try {
904
- const decisions = await memory.chroma.store.getDecisionsByProject(bulkProject)
905
- if (decisions.length === 0) {
906
- return {
907
- action: 'none',
908
- summary: 'No memories found',
909
- content: `No memories found for project "${bulkProject}".`,
910
- relevantItems: 0
911
- }
912
- }
913
- for (const d of decisions) {
914
- await memory.deleteDecision(d.id)
915
- }
916
- this.searchEngine.invalidateCache(bulkProject)
917
- return {
918
- action: 'stored',
919
- summary: `Bulk deleted ${decisions.length} memories`,
920
- content: `Deleted all ${decisions.length} memories for project "${bulkProject}".`,
921
- relevantItems: 0
922
- }
923
- } catch {
924
- // Bulk delete failed, fall through to single-item delete
925
- }
926
- }
927
- }
928
-
929
- if (hasSpecificContent) {
930
- try {
931
- const results = await memory.searchRaw(topic, {
932
- project: effectiveProject,
933
- limit: 3,
934
- minSimilarity: DELETE_MIN_SIMILARITY
935
- })
936
-
937
- const matchId = results[0]?.memory?.id || results[0]?.decision?.id || results[0]?.id
938
- const matchSimilarity = results[0]?.similarity || 0
939
-
940
- if (results.length > 0 && matchId && matchSimilarity >= DELETE_MIN_SIMILARITY) {
941
- const targetId = matchId
942
- const content = results[0].decision?.decision || results[0].memory?.content?.slice(0, 100) || results[0].content?.slice(0, 100) || ''
943
-
944
- await memory.deleteDecision(targetId)
945
- if (this.lastStoredId === targetId) this.lastStoredId = null
946
-
947
- // Invalidate cache
948
- this.searchEngine.invalidateCache(effectiveProject)
949
-
950
- return {
951
- action: 'stored',
952
- summary: `Deleted memory`,
953
- content: `Deleted: "${content.slice(0, 100)}" (ID: ${targetId})`,
954
- relevantItems: 0
955
- }
956
- } else if (results.length > 0 && matchSimilarity < DELETE_MIN_SIMILARITY) {
957
- // D4: No confident match — try direct FTS5 content search as fallback
958
- const memory = getMemoryService()
959
- if (memory.fts5) {
960
- const ftsResults = memory.fts5.search(topic, effectiveProject, 3)
961
- if (ftsResults.length > 0) {
962
- const ftsTarget = ftsResults[0]
963
- await memory.deleteDecision(ftsTarget.id)
964
- if (this.lastStoredId === ftsTarget.id) this.lastStoredId = null
965
- this.searchEngine.invalidateCache(effectiveProject)
966
- return {
967
- action: 'stored',
968
- summary: `Deleted memory`,
969
- content: `Deleted: "${ftsTarget.content.slice(0, 100)}" (ID: ${ftsTarget.id})`,
970
- relevantItems: 0
971
- }
972
- }
973
- }
974
- return {
975
- action: 'none',
976
- summary: 'No confident match to delete',
977
- content: `Found a possible match but similarity (${Math.round(matchSimilarity * 100)}%) is too low for safe deletion. Try being more specific about what to delete.`,
978
- relevantItems: 0
979
- }
980
- }
981
- } catch {
982
- // Search failed, fall through
983
- }
984
- }
985
-
986
- // Generic request — use lastStoredId
987
- if (this.lastStoredId) {
988
- try {
989
- await memory.deleteDecision(this.lastStoredId)
990
- const deletedId = this.lastStoredId
991
- this.lastStoredId = null
992
- this.searchEngine.invalidateCache(effectiveProject)
993
- return {
994
- action: 'stored',
995
- summary: `Deleted most recent memory`,
996
- content: `Deleted memory (ID: ${deletedId})`,
997
- relevantItems: 0
998
- }
999
- } catch {
1000
- // Deletion failed
1001
- }
1002
- }
1003
-
1004
- return {
1005
- action: 'none',
1006
- summary: 'No matching memory found to delete',
1007
- content: 'Could not find a memory matching your request. Try being more specific about what to delete.',
1008
- relevantItems: 0
1009
- }
1010
- }
1011
-
1012
- /**
1013
- * Phase 19 C7+C10+C11: Enhanced question handler
1014
- * - Uses SearchEngine for all searches (hybrid, cached)
1015
- * - Temporal search when temporal signals detected
1016
- * - ChainRetrieval for complex multi-part questions
1017
- * - CrossProject patterns for general/unspecified project
1018
- * - KnowledgeGraph enrichment
1019
- */
1020
- private async handleQuestion(
1021
- message: string,
1022
- project: string | undefined,
1023
- entities: BrainExtractedEntities,
1024
- classification: ClassificationResult
1025
- ): Promise<BrainResponse> {
1026
- if (!isServicesInitialized()) {
1027
- return this.servicesNotReady()
1028
- }
1029
-
1030
- // BUG-002: Detect category-based queries and route to fetchAll
1031
- const categoryIntent = this.detectCategoryIntent(message)
1032
- if (categoryIntent) {
1033
- return this.handleCategoryQuery(message, project, categoryIntent)
1034
- }
1035
-
1036
- // Phase 25: Use undefined for search (no project filter = search all) when no project detected
1037
- const searchProject = project || undefined
1038
- const displayProject = project || DEFAULT_PROJECT
1039
- const query = entities.topic || message
1040
- const tiers: TierResults[] = []
1041
- const hasTemporal = classification.secondary.includes('exploration') ||
1042
- this.classifier.hasTemporalSignal(message.toLowerCase())
1043
-
1044
- // C7: Detect multi-part questions for chain retrieval
1045
- const isComplex = this.isComplexQuestion(message)
1046
-
1047
- // Main search — temporal or standard
1048
- let searchResults: NormalizedResult[]
1049
- if (hasTemporal) {
1050
- const { results } = await this.searchEngine.temporalSearch(query, { project: searchProject, limit: 5 })
1051
- searchResults = results
1052
- } else if (isComplex) {
1053
- // C7: Try multi-hop chain retrieval
1054
- const chainResult = await this.searchEngine.chainSearch(query, { project: searchProject })
1055
- if (chainResult?.allResults?.length) {
1056
- searchResults = chainResult.allResults.map((r: any) => ({
1057
- id: r.id || '',
1058
- content: r.content || '',
1059
- score: r.similarity || 0,
1060
- source: 'decision' as const,
1061
- project: r.metadata?.project || displayProject || '',
1062
- date: r.metadata?.created_at || '',
1063
- metadata: r.metadata || {}
1064
- }))
1065
- } else {
1066
- searchResults = await this.searchEngine.enhancedSearch(query, { project: searchProject, limit: 5 })
1067
- }
1068
- } else {
1069
- searchResults = await this.searchEngine.enhancedSearch(query, { project: searchProject, limit: 5 })
1070
- }
1071
-
1072
- if (searchResults.length > 0) {
1073
- tiers.push({
1074
- label: 'Memories',
1075
- results: searchResults.map(r => ({
1076
- content: r.content,
1077
- score: r.score,
1078
- source: r.source === 'decision' ? 'Past Decision' : r.source,
1079
- metadata: {
1080
- ...(r.metadata as Record<string, unknown>),
1081
- id: r.id,
1082
- project: r.project,
1083
- created_at: r.date
1084
- }
1085
- }))
1086
- })
1087
- }
1088
-
1089
- // Phase 32: Code intelligence — synchronous SQLite query, runs alongside async searches
1090
- const codeTier = this.queryCodeIntelligence(query, searchProject)
1091
- if (codeTier) {
1092
- tiers.push(codeTier)
1093
- }
1094
-
1095
- // Parallel: patterns + corrections + graph
1096
- const [patternResults, correctionResults, graphResults] = await Promise.all([
1097
- this.searchEngine.searchPatterns(query, { project: searchProject, limit: 3 }),
1098
- this.searchEngine.searchCorrections(query, { project: searchProject, limit: 3 }),
1099
- // C11: KnowledgeGraph enrichment for all questions
1100
- this.searchEngine.searchGraph(query, 5)
1101
- ])
1102
-
1103
- if (patternResults.length > 0) {
1104
- tiers.push({
1105
- label: 'Patterns',
1106
- results: patternResults.map(p => ({
1107
- content: p.content,
1108
- score: p.score,
1109
- source: `Pattern`,
1110
- metadata: {
1111
- ...(p.metadata as Record<string, unknown>),
1112
- id: p.id,
1113
- project: p.project,
1114
- created_at: p.date,
1115
- category: 'pattern'
1116
- }
1117
- }))
1118
- })
1119
- }
1120
-
1121
- if (correctionResults.length > 0) {
1122
- tiers.push({
1123
- label: 'Corrections',
1124
- results: correctionResults.map(c => ({
1125
- content: c.content,
1126
- score: c.score,
1127
- source: 'Lesson Learned',
1128
- metadata: {
1129
- ...(c.metadata as Record<string, unknown>),
1130
- id: c.id,
1131
- project: c.project,
1132
- created_at: c.date,
1133
- category: 'correction'
1134
- }
1135
- }))
1136
- })
1137
- }
1138
-
1139
- // C11: Add graph results as "Related Concepts"
1140
- // When a project filter is active, cap graph results lower (0.25) so they don't
1141
- // outrank project-scoped ChromaDB results. Without a project filter, cap at 0.4.
1142
- // BUG-006 T4: Also require keyword overlap with query to prevent noise
1143
- const graphScoreCap = searchProject ? 0.25 : 0.4
1144
- const queryWordsForGraph = new Set(query.toLowerCase().split(/\s+/).filter(w => w.length > 2))
1145
- const relevantGraphResults = graphResults.filter(g => {
1146
- if (g.score < 0.6) return false
1147
- // Require at least one query keyword to appear in graph content
1148
- if (queryWordsForGraph.size > 0) {
1149
- const contentLower = g.content.toLowerCase()
1150
- const hasOverlap = [...queryWordsForGraph].some(w => contentLower.includes(w))
1151
- if (!hasOverlap) return false
1152
- }
1153
- return true
1154
- })
1155
- if (relevantGraphResults.length > 0) {
1156
- tiers.push({
1157
- label: 'Related Concepts',
1158
- results: relevantGraphResults.map(g => ({
1159
- content: g.content,
1160
- score: Math.min(g.score, graphScoreCap),
1161
- source: 'Knowledge Graph',
1162
- metadata: g.metadata as Record<string, unknown>
1163
- }))
1164
- })
1165
- }
1166
-
1167
- // C10: CrossProject patterns for general/unspecified project queries (Bug 2: cap to 0.3)
1168
- if (!project) {
1169
- try {
1170
- const crossProject = await this.searchEngine.findCrossProjectPatterns({
1171
- query,
1172
- limit: 3
1173
- })
1174
- if (crossProject?.patterns?.length) {
1175
- tiers.push({
1176
- label: 'Cross-Project Patterns',
1177
- results: crossProject.patterns.map((p: any) => ({
1178
- content: `${p.description || ''} (${p.projects?.join(', ') || 'multiple projects'})`,
1179
- score: Math.min(p.confidence || 0.5, 0.3),
1180
- source: 'Cross-Project',
1181
- metadata: {}
1182
- }))
1183
- })
1184
- }
1185
- } catch {
1186
- // Cross-project not available
1187
- }
1188
- }
1189
-
1190
- // Phase 23b Fix 5: Session recall — "what have we discussed" queries session tracker
1191
- if (this.isSessionRecallQuery(message)) {
1192
- try {
1193
- const tracker = getSessionTracker()
1194
- if (tracker) {
1195
- const stats = tracker.getStats()
1196
- if (stats.totalItems > 0) {
1197
- tiers.push({
1198
- label: 'Current Session',
1199
- results: [{
1200
- content: `Active sessions: ${stats.activeSessions}, items tracked: ${stats.totalItems}`,
1201
- score: 0.95,
1202
- source: 'Session Tracker',
1203
- metadata: {}
1204
- }]
1205
- })
1206
- }
1207
- }
1208
- } catch {
1209
- // Session tracker not available
1210
- }
1211
- }
1212
-
1213
- // Bug 4: Inject recently stored decisions using keyword overlap as score
1214
- const recentMatches = this.findRecentDecisions(query, searchProject)
1215
- // Only include results whose overlap score meets the hard floor (0.35)
1216
- const qualifiedRecent = recentMatches.filter(d => (d.overlapScore || 0) >= 0.35)
1217
- if (qualifiedRecent.length > 0) {
1218
- tiers.unshift({
1219
- label: 'Recent Decisions',
1220
- results: qualifiedRecent.map(d => ({
1221
- content: d.content,
1222
- score: d.overlapScore || 0,
1223
- source: 'Recent Decision',
1224
- metadata: { storedAt: d.storedAt, project: d.project }
1225
- }))
1226
- })
1227
- }
1228
-
1229
- // Register with episode
1230
- this.registerEpisodeMessage(message, displayProject, 'question')
1231
-
1232
- // Phase 27: Use compact format for progressive disclosure
1233
- // Combine all tier results, filter, then format compactly
1234
- const allResults: import('./response-filter').FilterableResult[] = []
1235
- for (const tier of tiers) {
1236
- allResults.push(...tier.results)
1237
- }
1238
-
1239
- if (allResults.length === 0) {
1240
- return {
1241
- action: 'none',
1242
- summary: 'No relevant information found',
1243
- content: `No results found for: "${message.slice(0, 100)}"`,
1244
- relevantItems: 0
1245
- }
1246
- }
1247
-
1248
- const filtered = this.responseFilter.filter(allResults, message, displayProject)
1249
- if (filtered.length === 0) {
1250
- return {
1251
- action: 'none',
1252
- summary: 'Results filtered out as noise or irrelevant',
1253
- content: `No relevant results after filtering for: "${message.slice(0, 100)}"`,
1254
- relevantItems: 0
1255
- }
1256
- }
1257
-
1258
- // Collect result IDs for detail lookups
1259
- const resultIds = filtered
1260
- .map(r => (r.metadata as any)?.id || (r.metadata as any)?.decision_id)
1261
- .filter(Boolean)
1262
-
1263
- const compactContent = formatCompactResponse(filtered, message)
1264
- return {
1265
- action: 'retrieved',
1266
- summary: `Found ${filtered.length} result${filtered.length === 1 ? '' : 's'}`,
1267
- content: compactContent,
1268
- relevantItems: filtered.length
1269
- }
1270
- }
1271
-
1272
- /**
1273
- * Phase 19 C6: Enhanced exploration handler
1274
- * - Timeline for "timeline"/"chronological" queries
1275
- * - DecisionEvolutionTracker for "how has X changed" queries
1276
- * - TrendDetector for "trends"/"emerging" queries
1277
- */
1278
- private async handleExploration(
1279
- message: string,
1280
- project: string | undefined,
1281
- entities: BrainExtractedEntities
1282
- ): Promise<BrainResponse> {
1283
- if (!isServicesInitialized()) {
1284
- return this.servicesNotReady()
1285
- }
1286
-
1287
- // Phase 25: Use undefined for search when no project detected
1288
- const searchProject = project || undefined
1289
- const displayProject = project || DEFAULT_PROJECT
1290
- const query = entities.topic || message
1291
- const lower = message.toLowerCase()
1292
- const tiers: TierResults[] = []
1293
-
1294
- // C6: Timeline queries
1295
- if (lower.includes('timeline') || lower.includes('chronological') || lower.includes('history of')) {
1296
- const timeline = await this.searchEngine.buildTimeline({
1297
- project: searchProject,
1298
- topic: query,
1299
- limit: 20
1300
- })
1301
-
1302
- if (timeline?.entries?.length) {
1303
- tiers.push({
1304
- label: 'Timeline',
1305
- results: timeline.entries.map((e: any) => ({
1306
- content: `**${e.date || ''}** — ${e.content || ''}`,
1307
- score: 0.8,
1308
- source: `Timeline (${e.type || 'event'})`,
1309
- metadata: e.metadata || {}
1310
- }))
1311
- })
1312
- }
1313
- }
1314
-
1315
- // C6: Evolution queries
1316
- if (lower.includes('evolution') || lower.includes('how has') || lower.includes('changed') || lower.includes('evolved')) {
1317
- const evolution = await this.searchEngine.analyzeEvolution(query, { project: searchProject })
1318
- if (evolution?.timeline?.length) {
1319
- const parts = []
1320
- parts.push(`**Stability:** ${evolution.stability || 'unknown'}`)
1321
- if (evolution.currentState) parts.push(`**Current:** ${evolution.currentState}`)
1322
- for (const change of (evolution.changes || []).slice(0, 5)) {
1323
- parts.push(`- ${change.description || change}`)
1324
- }
1325
-
1326
- tiers.push({
1327
- label: 'Decision Evolution',
1328
- results: [{
1329
- content: parts.join('\n'),
1330
- score: 0.8,
1331
- source: 'Evolution Analysis',
1332
- metadata: {}
1333
- }]
1334
- })
1335
- }
1336
- }
1337
-
1338
- // C6: Trend queries
1339
- if (lower.includes('trend') || lower.includes('emerging') || lower.includes('declining')) {
1340
- const trends = await this.searchEngine.detectTrends({ project: searchProject })
1341
- if (trends?.topTrends?.length) {
1342
- tiers.push({
1343
- label: 'Trends',
1344
- results: trends.topTrends.slice(0, 5).map((t: any) => ({
1345
- content: `**${t.term}** — ${t.trend} (${t.occurrences} occurrences, momentum: ${t.momentum || 'unknown'})`,
1346
- score: t.occurrences > 5 ? 0.9 : 0.7,
1347
- source: `Trend (${t.trend})`,
1348
- metadata: {}
1349
- }))
1350
- })
1351
- }
1352
- }
1353
-
1354
- // Always include graph exploration — cap scores when project filter is active
1355
- // BUG-006 T4: Also require keyword overlap with query to prevent noise
1356
- const explorationGraphCap = searchProject ? 0.25 : 0.5
1357
- const explorationQueryWords = new Set(query.toLowerCase().split(/\s+/).filter(w => w.length > 2))
1358
- const graphResults = await this.searchEngine.searchGraph(query, 10)
1359
- const filteredGraphResults = graphResults.filter(g => {
1360
- if (explorationQueryWords.size > 0) {
1361
- const contentLower = g.content.toLowerCase()
1362
- return [...explorationQueryWords].some(w => contentLower.includes(w))
1363
- }
1364
- return true
1365
- })
1366
- if (filteredGraphResults.length > 0) {
1367
- tiers.push({
1368
- label: 'Knowledge Graph',
1369
- results: filteredGraphResults.map(g => ({
1370
- content: g.content,
1371
- score: Math.min(g.score, explorationGraphCap),
1372
- source: 'Knowledge Graph',
1373
- metadata: g.metadata as Record<string, unknown>
1374
- }))
1375
- })
1376
- }
1377
-
1378
- // Issue 9: Cross-project patterns for exploration queries
1379
- if (!project) {
1380
- try {
1381
- const crossProject = await this.searchEngine.findCrossProjectPatterns({
1382
- query,
1383
- limit: 3
1384
- })
1385
- if (crossProject?.patterns?.length) {
1386
- tiers.push({
1387
- label: 'Cross-Project Patterns',
1388
- results: crossProject.patterns.map((p: any) => ({
1389
- content: `${p.description || ''} (${p.projects?.join(', ') || 'multiple projects'})`,
1390
- score: p.confidence || 0.5,
1391
- source: 'Cross-Project',
1392
- metadata: {}
1393
- }))
1394
- })
1395
- }
1396
- } catch {
1397
- // Cross-project not available
1398
- }
1399
- }
1400
-
1401
- // Also do a basic memory search as fallback
1402
- const searchResults = await this.searchEngine.enhancedSearch(query, {
1403
- project: searchProject,
1404
- limit: 5,
1405
- minSimilarity: 0.2
1406
- })
1407
- if (searchResults.length > 0) {
1408
- tiers.push({
1409
- label: 'Memories',
1410
- results: searchResults.map(r => ({
1411
- content: r.content,
1412
- score: r.score,
1413
- source: r.source === 'decision' ? 'Past Decision' : r.source,
1414
- metadata: r.metadata as Record<string, unknown>
1415
- }))
1416
- })
1417
- }
1418
-
1419
- // Phase 32: Code intelligence for exploration queries
1420
- const codeTier = this.queryCodeIntelligence(query, searchProject)
1421
- if (codeTier) {
1422
- tiers.push(codeTier)
1423
- }
1424
-
1425
- return this.responseFilter.synthesize(tiers, message, displayProject, 'analyzed')
1426
- }
1427
-
1428
- private async handleComparison(
1429
- message: string,
1430
- project: string | undefined,
1431
- entities: BrainExtractedEntities
1432
- ): Promise<BrainResponse> {
1433
- if (!isServicesInitialized()) {
1434
- return this.servicesNotReady()
1435
- }
1436
-
1437
- // Phase 25: Use undefined for search when no project detected
1438
- const searchProject = project || undefined
1439
- const displayProject = project || DEFAULT_PROJECT
1440
- const query = entities.topic || message
1441
- const tiers: TierResults[] = []
1442
-
1443
- // Search for related decisions
1444
- const searchResults = await this.searchEngine.enhancedSearch(query, {
1445
- project: searchProject,
1446
- limit: 5,
1447
- minSimilarity: 0.2
1448
- })
1449
- if (searchResults.length > 0) {
1450
- tiers.push({
1451
- label: 'Related Decisions',
1452
- results: searchResults.map(r => ({
1453
- content: r.content,
1454
- score: r.score,
1455
- source: 'Past Decision',
1456
- metadata: r.metadata as Record<string, unknown>
1457
- }))
1458
- })
1459
- }
1460
-
1461
- // Graph search for comparison context
1462
- const graphResults = await this.searchEngine.searchGraph(query, 5)
1463
- if (graphResults.length > 0) {
1464
- tiers.push({
1465
- label: 'Knowledge Graph',
1466
- results: graphResults.map(g => ({
1467
- content: g.content,
1468
- score: g.score,
1469
- source: 'Knowledge Graph',
1470
- metadata: g.metadata as Record<string, unknown>
1471
- }))
1472
- })
1473
- }
1474
-
1475
- return this.responseFilter.synthesize(tiers, message, displayProject, 'analyzed')
1476
- }
1477
-
1478
- /**
1479
- * Phase 27: Handle detail requests — "details obs_abc123", "expand <id>"
1480
- * Looks up a single observation by ID and returns full detail view.
1481
- */
1482
- private async handleDetailRequest(
1483
- message: string,
1484
- project: string | undefined,
1485
- _entities: BrainExtractedEntities
1486
- ): Promise<BrainResponse> {
1487
- if (!isServicesInitialized()) {
1488
- return this.servicesNotReady()
1489
- }
1490
-
1491
- // Extract ID from message using regex
1492
- const idMatch = message.match(/\b(obs_\w+|[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}|[a-f0-9]{8,})\b/i)
1493
- if (!idMatch) {
1494
- return {
1495
- action: 'none',
1496
- summary: 'No observation ID provided',
1497
- content: 'Please provide an observation ID. Use brain("details {ID}") with an ID from search results.',
1498
- relevantItems: 0
1499
- }
1500
- }
1501
-
1502
- const id = idMatch[1]
1503
- const observation = await this.lookupById(id)
1504
-
1505
- if (!observation) {
1506
- return {
1507
- action: 'none',
1508
- summary: `No observation found: ${id}`,
1509
- content: `No observation found with ID: ${id}`,
1510
- relevantItems: 0
1511
- }
1512
- }
1513
-
1514
- // Increment access count
1515
- this.incrementAccess(id)
1516
-
1517
- return {
1518
- action: 'retrieved',
1519
- summary: `Details for ${id.slice(0, 12)}...`,
1520
- content: formatDetailResponse(observation),
1521
- relevantItems: 1
1522
- }
1523
- }
1524
-
1525
- /**
1526
- * Phase 27: Handle timeline requests — "timeline for project", "recent activity"
1527
- * Fetches observations in a time range and groups by day.
1528
- */
1529
- private async handleTimeline(
1530
- message: string,
1531
- project: string | undefined,
1532
- entities: BrainExtractedEntities
1533
- ): Promise<BrainResponse> {
1534
- if (!isServicesInitialized()) {
1535
- return this.servicesNotReady()
1536
- }
1537
-
1538
- const effectiveProject = entities.project || project
1539
- const displayProject = effectiveProject || DEFAULT_PROJECT
1540
-
1541
- // Parse time range from message (default: last 7 days)
1542
- const now = new Date()
1543
- let start: Date
1544
- let daysBack = 7
1545
- const lower = message.toLowerCase()
1546
- if (lower.includes('yesterday')) {
1547
- daysBack = 1
1548
- start = new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000)
1549
- } else if (lower.includes('today')) {
1550
- daysBack = 0
1551
- // Start of today, not "now"
1552
- start = new Date(now.getFullYear(), now.getMonth(), now.getDate())
1553
- } else if (lower.includes('last month') || lower.includes('past month')) {
1554
- daysBack = 30
1555
- start = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000)
1556
- } else if (lower.includes('last week') || lower.includes('past week')) {
1557
- daysBack = 7
1558
- start = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
1559
- } else if (lower.includes('last 3 days') || lower.includes('past 3 days')) {
1560
- daysBack = 3
1561
- start = new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000)
1562
- } else {
1563
- start = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
1564
- }
1565
-
1566
- const observations = await this.fetchRecentObservations(effectiveProject, start, now)
1567
- if (observations.length === 0) {
1568
- return {
1569
- action: 'none',
1570
- summary: `No activity found for ${displayProject}`,
1571
- content: `No observations found in the last ${daysBack === 0 ? 'day' : `${daysBack} day(s)`} for ${displayProject}.`,
1572
- relevantItems: 0
1573
- }
1574
- }
1575
-
1576
- const grouped = groupByDay(observations)
1577
- const timelineContent = formatTimeline(grouped, displayProject)
1578
-
1579
- return {
1580
- action: 'retrieved',
1581
- summary: `Timeline for ${displayProject} (${observations.length} items)`,
1582
- content: timelineContent,
1583
- relevantItems: observations.length
1584
- }
1585
- }
1586
-
1587
- /**
1588
- * Phase 27: Look up a single observation by ID.
1589
- * Tries FTS5 first, then falls back to ChromaDB search.
1590
- */
1591
- private async lookupById(id: string): Promise<any | null> {
1592
- const memory = getMemoryService()
1593
-
1594
- // Try FTS5 first (fast, direct lookup)
1595
- if (memory.fts5) {
1596
- const result = memory.fts5.getById(id)
1597
- if (result) return result
1598
- }
1599
-
1600
- // Fallback: search ChromaDB by ID if available
1601
- if (memory.isChromaDBEnabled()) {
1602
- try {
1603
- const collections = memory.chroma.collections
1604
- // Try decisions collection
1605
- const decisions = await collections.getDecisions()
1606
- if (decisions) {
1607
- try {
1608
- const result = await decisions.get({ ids: [id] })
1609
- if (result?.documents?.[0]) {
1610
- return {
1611
- id,
1612
- content: result.documents[0],
1613
- metadata: result.metadatas?.[0] || {},
1614
- category: 'decision',
1615
- project: (result.metadatas?.[0] as any)?.project || '',
1616
- created_at: (result.metadatas?.[0] as any)?.created_at || '',
1617
- reasoning: (result.metadatas?.[0] as any)?.reasoning || null,
1618
- tags: [],
1619
- confidence: 0.8
1620
- }
1621
- }
1622
- } catch { /* ID not in this collection */ }
1623
- }
1624
- } catch {
1625
- // ChromaDB lookup failed
1626
- }
1627
- }
1628
-
1629
- return null
1630
- }
1631
-
1632
- /**
1633
- * Phase 27: Fetch recent observations in a date range.
1634
- * Tries FTS5 first, then falls back to ChromaDB temporal search.
1635
- */
1636
- private async fetchRecentObservations(
1637
- project: string | undefined,
1638
- start: Date,
1639
- end: Date
1640
- ): Promise<any[]> {
1641
- const memory = getMemoryService()
1642
-
1643
- // Try FTS5 first (efficient SQL-level date filtering)
1644
- if (memory.fts5 && project) {
1645
- return memory.fts5.fetchByTimeRange(project, start, end)
1646
- }
1647
-
1648
- // Also try FTS5 without project filter
1649
- if (memory.fts5 && !project) {
1650
- // FTS5 fetchByTimeRange requires project, use fetchAll with date filtering
1651
- try {
1652
- const all = memory.fts5.search('*', undefined, 100)
1653
- return all.filter(r => {
1654
- const date = new Date(r.created_at)
1655
- if (isNaN(date.getTime())) return false
1656
- return date >= start && date <= end
1657
- }).sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
1658
- } catch {
1659
- // FTS5 search failed, fall through
1660
- }
1661
- }
1662
-
1663
- // Fallback: use memory fetchAll with date filtering
1664
- try {
1665
- const allDecisions = await memory.fetchAllDecisions(project)
1666
- const allPatterns = await memory.fetchAllPatterns(project)
1667
- const allCorrections = await memory.fetchAllCorrections(project)
1668
-
1669
- const allItems = [
1670
- ...allDecisions.map((d: any) => ({
1671
- id: d.id || d.decision_id,
1672
- content: d.decision || d.document || d.content || '',
1673
- category: 'decision',
1674
- project: d.project || project || DEFAULT_PROJECT,
1675
- created_at: d.created_at || d.date || '',
1676
- })),
1677
- ...allPatterns.map((p: any) => ({
1678
- id: p.id,
1679
- content: p.description || p.document || p.content || '',
1680
- category: p.pattern_type || 'pattern',
1681
- project: p.project || project || DEFAULT_PROJECT,
1682
- created_at: p.created_at || '',
1683
- })),
1684
- ...allCorrections.map((c: any) => ({
1685
- id: c.id,
1686
- content: c.original || c.document || c.content || '',
1687
- category: 'correction',
1688
- project: c.project || project || DEFAULT_PROJECT,
1689
- created_at: c.created_at || '',
1690
- })),
1691
- ]
1692
-
1693
- return allItems.filter(item => {
1694
- if (!item.created_at) return true // include items without dates
1695
- const date = new Date(item.created_at)
1696
- if (isNaN(date.getTime())) return true
1697
- return date >= start && date <= end
1698
- }).sort((a, b) => {
1699
- const dateA = new Date(a.created_at || 0).getTime()
1700
- const dateB = new Date(b.created_at || 0).getTime()
1701
- return dateB - dateA
1702
- })
1703
- } catch {
1704
- return []
1705
- }
1706
- }
1707
-
1708
- /**
1709
- * Phase 27: Increment access count for an observation.
1710
- */
1711
- private incrementAccess(id: string): void {
1712
- try {
1713
- const memory = getMemoryService()
1714
- if (memory.fts5) {
1715
- memory.fts5.recordAccess(id)
1716
- }
1717
- } catch {
1718
- // Non-critical
1719
- }
1720
- }
1721
-
1722
- // ===== Helpers =====
1723
-
1724
- /**
1725
- * Phase 32: Query code intelligence for symbols and files matching a query.
1726
- * Returns a TierResults if matches found, null otherwise.
1727
- * Fast operation — direct SQLite queries, no network calls.
1728
- */
1729
- private queryCodeIntelligence(query: string, project: string | undefined): TierResults | null {
1730
- try {
1731
- const codeQuery = getCodeQuery()
1732
- if (!codeQuery) return null
1733
-
1734
- // Extract meaningful keywords from query for code search
1735
- const keywords = this.extractCodeKeywords(query)
1736
- if (keywords.length === 0) return null
1737
-
1738
- const results: FilterableResult[] = []
1739
-
1740
- for (const keyword of keywords) {
1741
- // Search symbols across all indexed projects (project may not match code index project name)
1742
- const symbolResults = codeQuery.findSymbols(keyword, project || '', 5)
1743
-
1744
- // If no results with project filter, try without (code index may use path-based project names)
1745
- const symbols = symbolResults.length > 0
1746
- ? symbolResults
1747
- : codeQuery.findSymbols(keyword, '', 5)
1748
-
1749
- for (const sym of symbols.slice(0, 3)) {
1750
- const location = sym.lineEnd
1751
- ? `${sym.filePath}:${sym.lineStart}-${sym.lineEnd}`
1752
- : `${sym.filePath}:${sym.lineStart}`
1753
- const sigPart = sym.signature ? ` — \`${sym.signature}\`` : ''
1754
- results.push({
1755
- content: `**${sym.symbol}** (${sym.type}) at \`${location}\`${sigPart}`,
1756
- score: sym.confidence * 0.6, // Scale down so memories rank higher
1757
- source: 'Code Index',
1758
- metadata: {
1759
- filePath: sym.filePath,
1760
- lineStart: sym.lineStart,
1761
- lineEnd: sym.lineEnd,
1762
- symbolType: sym.type,
1763
- }
1764
- })
1765
- }
1766
-
1767
- // Also search files by name/summary
1768
- const fileResults = codeQuery.findFiles(keyword, project || '')
1769
- const files = fileResults.length > 0
1770
- ? fileResults
1771
- : codeQuery.findFiles(keyword, '')
1772
-
1773
- for (const file of files.slice(0, 3)) {
1774
- // Skip if we already have symbol results from this file
1775
- if (results.some(r => (r.metadata as any)?.filePath === file.filePath)) continue
1776
- const summaryPart = file.summary ? ` — ${file.summary}` : ''
1777
- results.push({
1778
- content: `**${file.filePath}** (${file.language || 'unknown'}, ${file.symbolCount} symbols, ${file.lineCount} lines)${summaryPart}`,
1779
- score: 0.4, // File matches are lower confidence
1780
- source: 'Code Index',
1781
- metadata: {
1782
- filePath: file.filePath,
1783
- symbolCount: file.symbolCount,
1784
- }
1785
- })
1786
- }
1787
- }
1788
-
1789
- // Deduplicate by filePath
1790
- const seen = new Set<string>()
1791
- const deduped = results.filter(r => {
1792
- const fp = (r.metadata as any)?.filePath
1793
- if (fp && seen.has(fp)) return false
1794
- if (fp) seen.add(fp)
1795
- return true
1796
- })
1797
-
1798
- if (deduped.length === 0) return null
1799
-
1800
- return {
1801
- label: 'Code Intelligence',
1802
- results: deduped.slice(0, 5)
1803
- }
1804
- } catch (error) {
1805
- this.logger.debug({ error }, 'Code intelligence query failed (non-fatal)')
1806
- return null
1807
- }
1808
- }
1809
-
1810
- /**
1811
- * Extract meaningful keywords from a natural language query for code search.
1812
- * Filters out stop words and common filler, returns words likely to match code symbols.
1813
- */
1814
- private extractCodeKeywords(query: string): string[] {
1815
- const STOP_WORDS = new Set([
1816
- 'how', 'what', 'where', 'when', 'why', 'who', 'which', 'are', 'is', 'was',
1817
- 'were', 'been', 'being', 'have', 'has', 'had', 'having', 'do', 'does', 'did',
1818
- 'doing', 'will', 'would', 'could', 'should', 'shall', 'can', 'may', 'might',
1819
- 'must', 'the', 'a', 'an', 'and', 'but', 'or', 'nor', 'for', 'yet', 'so',
1820
- 'in', 'on', 'at', 'to', 'from', 'by', 'with', 'about', 'into', 'through',
1821
- 'during', 'before', 'after', 'above', 'below', 'between', 'out', 'off', 'over',
1822
- 'under', 'again', 'further', 'then', 'once', 'here', 'there', 'all', 'each',
1823
- 'every', 'both', 'few', 'more', 'most', 'other', 'some', 'such', 'only', 'own',
1824
- 'same', 'than', 'too', 'very', 'just', 'because', 'not', 'this', 'that', 'these',
1825
- 'those', 'its', 'our', 'their', 'your', 'my', 'me', 'we', 'you', 'they', 'them',
1826
- 'know', 'tell', 'show', 'find', 'get', 'make', 'use', 'using', 'used', 'managing',
1827
- 'managed', 'handle', 'handling', 'handled', 'work', 'working', 'works', 'implement',
1828
- 'implementing', 'implemented', 'doing', 'done', 'project', 'code', 'file', 'function',
1829
- ])
1830
-
1831
- const words = query.toLowerCase()
1832
- .replace(/[^a-z0-9\s-_]/g, ' ')
1833
- .split(/\s+/)
1834
- .filter(w => w.length > 2 && !STOP_WORDS.has(w))
1835
-
1836
- // Also try multi-word phrases (camelCase/kebab-case patterns the user might reference)
1837
- const phrases: string[] = []
1838
- for (let i = 0; i < words.length - 1; i++) {
1839
- // Create camelCase combo: "brain router" → "brainRouter"
1840
- phrases.push(words[i] + words[i + 1].charAt(0).toUpperCase() + words[i + 1].slice(1))
1841
- }
1842
-
1843
- // Return unique keywords, single words first then phrases
1844
- return [...new Set([...words, ...phrases])].slice(0, 5)
1845
- }
1846
-
1847
- /**
1848
- * Phase 29: Link a stored observation to code files (non-blocking, fire-and-forget).
1849
- */
1850
- private linkToCodeFiles(observationId: string, content: string, project: string): void {
1851
- try {
1852
- const linker = getCodeLinker()
1853
- if (linker) {
1854
- linker.linkObservation(observationId, content, project).catch(err => {
1855
- this.logger.debug({ err }, 'Code linkage failed (non-fatal)')
1856
- })
1857
- }
1858
- } catch {
1859
- // Linker not available
1860
- }
1861
- }
1862
-
1863
- private async writeToVault(
1864
- project: string,
1865
- decision: string,
1866
- reasoning: string,
1867
- context: string,
1868
- alternatives?: string
1869
- ): Promise<void> {
1870
- try {
1871
- const vault = getVaultService()
1872
- const projectPaths = vault.getProjectPaths(project)
1873
- const date = new Date().toISOString().split('T')[0]
1874
- const entry = `### Decision: ${decision.slice(0, 100)}\n\n**Date:** ${date}\n**Context:** ${context}\n**Decision:** ${decision}\n**Reasoning:** ${reasoning}\n${alternatives ? `**Alternatives:** ${alternatives}\n` : ''}\n---\n\n`
1875
- await vault.writer.appendContent(projectPaths.decisions, entry, '\n')
1876
- } catch {
1877
- // Vault write failed
1878
- }
1879
- }
1880
-
1881
- /**
1882
- * Store as a new decision (fallback for update when no match found)
1883
- */
1884
- private async storeAsNew(
1885
- memory: any,
1886
- project: string,
1887
- message: string,
1888
- topic: string,
1889
- entities: BrainExtractedEntities,
1890
- reason: string
1891
- ): Promise<BrainResponse> {
1892
- const decisionId = await memory.rememberDecision(
1893
- project,
1894
- topic.slice(0, 200),
1895
- message,
1896
- entities.reasoning || '',
1897
- { tags: entities.technologies.length > 0 ? entities.technologies : ['updated'] }
1898
- )
1899
-
1900
- this.lastStoredId = decisionId
1901
- this.lastStoredProject = project
1902
- this.searchEngine.invalidateCache(project)
1903
-
1904
- return {
1905
- action: 'stored',
1906
- summary: `Stored as new (${reason})`,
1907
- content: `Stored as new decision (ID: ${decisionId})\n\n**Project:** ${project}\n**Content:** ${message}`,
1908
- relevantItems: 1
1909
- }
1910
- }
1911
-
1912
- /**
1913
- * C8: Register a message with the episode manager
1914
- */
1915
- private registerEpisodeMessage(message: string, project?: string, role: string = 'user'): void {
1916
- try {
1917
- const episodeManager = getEpisodeService()
1918
- if (!episodeManager) return
1919
- episodeManager.processMessage(
1920
- { role: role as 'user' | 'assistant' | 'system', content: message, timestamp: new Date().toISOString() },
1921
- project
1922
- ).catch(() => {})
1923
- } catch {
1924
- // Episode manager not available
1925
- }
1926
- }
1927
-
1928
- /**
1929
- * C8: Link a stored decision/pattern/correction to the active episode
1930
- */
1931
- private linkToActiveEpisode(project: string, id: string, type: 'decision' | 'pattern' | 'correction'): void {
1932
- try {
1933
- const episodeManager = getEpisodeService()
1934
- if (!episodeManager) return
1935
- const activeEpisode = episodeManager.getActiveEpisode(project)
1936
- if (!activeEpisode) return
1937
-
1938
- if (type === 'decision') episodeManager.linkDecision(activeEpisode.id, id)
1939
- else if (type === 'pattern') episodeManager.linkPattern(activeEpisode.id, id)
1940
- else if (type === 'correction') episodeManager.linkCorrection(activeEpisode.id, id)
1941
- } catch {
1942
- // Episode manager not available
1943
- }
1944
- }
1945
-
1946
- /**
1947
- * Phase 23b: Detect session recall queries ("what have we discussed", "this session", etc.)
1948
- */
1949
- private isSessionRecallQuery(message: string): boolean {
1950
- const lower = message.toLowerCase()
1951
- const SESSION_RECALL_PHRASES = [
1952
- 'what have we discussed',
1953
- 'what did we discuss',
1954
- 'what have we talked about',
1955
- 'what did we talk about',
1956
- 'this session',
1957
- 'current session',
1958
- 'session so far',
1959
- 'what have we done',
1960
- 'what did we do',
1961
- 'session summary',
1962
- 'summarize this session',
1963
- 'recap this session',
1964
- 'what happened this session',
1965
- 'what have we covered'
1966
- ]
1967
- return SESSION_RECALL_PHRASES.some(p => lower.includes(p))
1968
- }
1969
-
1970
- /**
1971
- * C7: Detect complex multi-part questions
1972
- */
1973
- private isComplexQuestion(message: string): boolean {
1974
- // Multiple question marks
1975
- const questionMarks = (message.match(/\?/g) || []).length
1976
- if (questionMarks >= 2) return true
1977
- // Very long question (likely multi-part)
1978
- if (message.length > 200 && message.includes('?')) return true
1979
- // Multiple clauses with "and" or "also"
1980
- if (message.includes(' and ') && message.includes('?') && message.length > 100) return true
1981
- return false
1982
- }
1983
-
1984
- private servicesNotReady(): BrainResponse {
1985
- return {
1986
- action: 'none',
1987
- summary: 'Services not initialized',
1988
- content: 'Claude Brain services are not ready. The server may still be starting up.',
1989
- relevantItems: 0
1990
- }
1991
- }
1992
-
1993
- /**
1994
- * Check if a delete/update message has descriptive content beyond just the command words.
1995
- */
1996
- private hasDescriptiveContent(message: string): boolean {
1997
- const COMMAND_WORDS = [
1998
- 'forget', 'delete', 'remove', 'discard', 'erase', 'undo', 'clear', 'drop',
1999
- 'actually', 'correction', 'update', 'change', 'replace', 'modify', 'revise',
2000
- 'amend', 'override', 'scratch', 'no', 'wait', 'instead',
2001
- 'that', 'this', 'the', 'it', 'about', 'memory', 'decision', 'to', 'a', 'an'
2002
- ]
2003
- const words = message.toLowerCase().split(/[\s,;:.!?]+/).filter(w => w.length > 1)
2004
- const meaningfulWords = words.filter(w => !COMMAND_WORDS.includes(w))
2005
- return meaningfulWords.length >= 2
2006
- }
2007
-
2008
- /**
2009
- * Bug 4: Track a recently stored decision for recency fast path
2010
- */
2011
- private trackRecentDecision(content: string, project: string): void {
2012
- const now = Date.now()
2013
- // Prune expired entries
2014
- this.recentDecisions = this.recentDecisions.filter(
2015
- d => now - d.storedAt < RECENT_DECISION_WINDOW_MS
2016
- )
2017
- this.recentDecisions.push({ content, project, storedAt: now })
2018
- }
2019
-
2020
- /**
2021
- * Bug 4: Find recent decisions matching a query by keyword overlap.
2022
- * Returns matches with their overlapScore (ratio of query keywords found in content).
2023
- */
2024
- private findRecentDecisions(query: string, project?: string): RecentDecision[] {
2025
- const now = Date.now()
2026
- // Prune expired
2027
- this.recentDecisions = this.recentDecisions.filter(
2028
- d => now - d.storedAt < RECENT_DECISION_WINDOW_MS
2029
- )
2030
-
2031
- const queryWords = new Set(
2032
- query.toLowerCase().split(/\s+/).filter(w => w.length > 2)
2033
- )
2034
- if (queryWords.size === 0) return []
2035
-
2036
- const matches: RecentDecision[] = []
2037
- for (const d of this.recentDecisions) {
2038
- // Project filter: skip if searching a specific project and it doesn't match
2039
- if (project && d.project !== project) continue
2040
- const contentWords = new Set(d.content.toLowerCase().split(/\s+/).filter(w => w.length > 2))
2041
- const overlap = [...queryWords].filter(w => contentWords.has(w)).length
2042
- if (overlap >= 1) {
2043
- const overlapScore = overlap / queryWords.size
2044
- matches.push({ ...d, overlapScore })
2045
- }
2046
- }
2047
- return matches
2048
- }
2049
-
2050
- /**
2051
- * Phase 30: Optionally compress content before storing.
2052
- * Returns the (possibly compressed) content and original if compressed.
2053
- */
2054
- private async maybeCompress(content: string, category: string): Promise<{ content: string; rawContent?: string }> {
2055
- if (!this.compressor) return { content }
2056
- try {
2057
- const result = await this.compressor.compress(content, category)
2058
- if (result.compressed) {
2059
- return { content: result.summary, rawContent: result.original }
2060
- }
2061
- } catch {
2062
- // Compression failed, use original
2063
- }
2064
- return { content }
2065
- }
2066
-
2067
- /**
2068
- * BUG-002: Detect category-based intent from question messages.
2069
- * Returns the category if the user is asking about a specific type of memory.
2070
- */
2071
- private detectCategoryIntent(message: string): 'decision' | 'pattern' | 'correction' | null {
2072
- const lower = message.toLowerCase()
2073
-
2074
- // Decision-oriented queries
2075
- if (
2076
- lower.includes('what decisions') || lower.includes('my decisions') ||
2077
- lower.includes('what have i decided') || lower.includes('what did i decide') ||
2078
- lower.includes('what choices') || lower.includes('decisions i') ||
2079
- lower.includes('list decisions') || lower.includes('show decisions') ||
2080
- lower.includes('all decisions') || lower.includes('what did i choose')
2081
- ) {
2082
- return 'decision'
2083
- }
2084
-
2085
- // Pattern-oriented queries
2086
- if (
2087
- lower.includes('what patterns') || lower.includes('my patterns') ||
2088
- lower.includes('best practices') || lower.includes('conventions') ||
2089
- lower.includes('list patterns') || lower.includes('show patterns') ||
2090
- lower.includes('all patterns')
2091
- ) {
2092
- return 'pattern'
2093
- }
2094
-
2095
- // Correction-oriented queries
2096
- if (
2097
- lower.includes('what mistakes') || lower.includes('my mistakes') ||
2098
- lower.includes('what bugs') || lower.includes('lessons learned') ||
2099
- lower.includes('what corrections') || lower.includes('my corrections') ||
2100
- lower.includes('list corrections') || lower.includes('show corrections') ||
2101
- lower.includes('all corrections') || lower.includes('what have i fixed') ||
2102
- lower.includes('what did i fix')
2103
- ) {
2104
- return 'correction'
2105
- }
2106
-
2107
- return null
2108
- }
2109
-
2110
- /**
2111
- * BUG-002: Handle category-scoped queries by fetching all items of that category.
2112
- */
2113
- private async handleCategoryQuery(
2114
- message: string,
2115
- project: string | undefined,
2116
- category: 'decision' | 'pattern' | 'correction'
2117
- ): Promise<BrainResponse> {
2118
- const memory = getMemoryService()
2119
- const projectLabel = project || 'all projects'
2120
-
2121
- let items: any[]
2122
- switch (category) {
2123
- case 'decision':
2124
- items = await memory.fetchAllDecisions(project)
2125
- break
2126
- case 'pattern':
2127
- items = await memory.fetchAllPatterns(project)
2128
- break
2129
- case 'correction':
2130
- items = await memory.fetchAllCorrections(project)
2131
- break
2132
- }
2133
-
2134
- if (items.length === 0) {
2135
- return {
2136
- action: 'none',
2137
- summary: `No ${category}s found`,
2138
- content: `No ${category}s found for ${projectLabel}.`,
2139
- relevantItems: 0
2140
- }
2141
- }
2142
-
2143
- // Format as compact items
2144
- const allItems = items.map(item => ({
2145
- id: item.id || item.decision_id,
2146
- content: typeof (item.decision || item.description || item.correction || item.original || item.document || item.content) === 'string'
2147
- ? (item.decision || item.description || item.correction || item.original || item.document || item.content)
2148
- : JSON.stringify(item.decision || item.description || item.correction || item.original || item.document || item.content || ''),
2149
- category,
2150
- project: item.project || project || 'general',
2151
- created_at: item.created_at || item.date || '',
2152
- }))
2153
-
2154
- const compactContent = formatCompactResponse(allItems, `${category}s for ${projectLabel}`)
2155
- return {
2156
- action: 'retrieved',
2157
- summary: `${items.length} ${category}s for ${projectLabel}`,
2158
- content: compactContent,
2159
- relevantItems: items.length
2160
- }
2161
- }
2162
-
2163
- private generateTaskId(title: string): string {
2164
- return title
2165
- .toLowerCase()
2166
- .replace(/[^a-z0-9]+/g, '-')
2167
- .replace(/^-+|-+$/g, '')
2168
- .slice(0, 50)
2169
- }
2170
- }
2171
-
2172
- // Lazy singleton
2173
- let routerInstance: BrainRouter | null = null
2174
-
2175
- export function getBrainRouter(logger: Logger): BrainRouter {
2176
- if (!routerInstance) {
2177
- routerInstance = new BrainRouter(logger)
2178
- // SLM Upgrade: Wire inference router if available
2179
- try {
2180
- const { getInferenceRouter } = require('@/server/services')
2181
- const ir = getInferenceRouter()
2182
- if (ir) routerInstance.setInferenceRouter(ir)
2183
- } catch {
2184
- // Services not initialized yet — will use regex fallback
2185
- }
2186
- }
2187
- return routerInstance
2188
- }
2189
-
2190
- /** Reset singleton for testing */
2191
- export function _resetBrainRouterForTesting(): void {
2192
- routerInstance = null
2193
- }
1
+ /**
2
+ * Brain Router
3
+ * Phase 16 + Phase 19: Core orchestrator for the unified brain() tool
4
+ *
5
+ * Routes classified intents to internal service calls and
6
+ * returns unified BrainResponse objects.
7
+ *
8
+ * Task 3.1: Refactored to delegate to domain handler modules.
9
+ * Intent handlers live in src/routing/handlers/.
10
+ */
11
+
12
+ import type { Logger } from 'pino'
13
+ import { IntentClassifier } from './intent-classifier'
14
+ import type { InferenceRouter } from '@/intelligence/inference-router'
15
+ import { BrainEntityExtractor } from './entity-extractor'
16
+ import type { BrainResponse } from './response-filter'
17
+ import { SearchEngine } from './search-engine'
18
+ import type { ObservationCompressor } from '@/memory/compression'
19
+
20
+ import type { HandlerContext, RecentDecision } from './handlers/types'
21
+ import { RECENT_DECISION_WINDOW_MS } from './handlers/types'
22
+
23
+ // Domain handlers
24
+ import {
25
+ handleStoreThis,
26
+ handleDecisionMade,
27
+ handlePatternFound,
28
+ handleMistakeLearned,
29
+ handleProgressUpdate
30
+ } from './handlers/memory-handler'
31
+ import {
32
+ handleSessionStart,
33
+ handleContextNeeded,
34
+ handleQuestion,
35
+ handleDetailRequest
36
+ } from './handlers/recall-handler'
37
+ import {
38
+ handleUpdateMemory,
39
+ handleDeleteMemory
40
+ } from './handlers/mutation-handler'
41
+ import {
42
+ handleExploration,
43
+ handleComparison,
44
+ handleListAll,
45
+ handleTimeline
46
+ } from './handlers/exploration-handler'
47
+
48
+ export interface BrainInput {
49
+ message: string
50
+ project?: string
51
+ action?: 'auto' | 'store' | 'recall' | 'update' | 'delete'
52
+ }
53
+
54
+ export class BrainRouter {
55
+ private classifier: IntentClassifier
56
+ private entityExtractor: BrainEntityExtractor
57
+ private searchEngine: SearchEngine
58
+ private logger: Logger
59
+
60
+ /** SLM Upgrade: Optional inference router for model-based classification */
61
+ private inferenceRouter: InferenceRouter | null = null
62
+
63
+ /** Phase 30: Optional LLM compressor for long observations */
64
+ private compressor: ObservationCompressor | null = null
65
+
66
+ /** Track the most recently stored decision ID for update/delete operations */
67
+ private lastStoredId: string | null = null
68
+ private lastStoredProject: string | null = null
69
+
70
+ /** Bug 4: Recently stored decisions for recency fast path */
71
+ private recentDecisions: RecentDecision[] = []
72
+
73
+ constructor(logger: Logger) {
74
+ this.classifier = new IntentClassifier()
75
+ this.entityExtractor = new BrainEntityExtractor()
76
+ this.searchEngine = new SearchEngine(logger)
77
+ this.logger = logger.child({ component: 'brain-router' })
78
+ }
79
+
80
+ /** SLM Upgrade: Set the optional inference router for model-based classification */
81
+ setInferenceRouter(router: InferenceRouter): void {
82
+ this.inferenceRouter = router
83
+ this.entityExtractor.setInferenceRouter(router)
84
+ }
85
+
86
+ /** Phase 30: Set the optional LLM compressor */
87
+ setCompressor(compressor: ObservationCompressor): void {
88
+ this.compressor = compressor
89
+ }
90
+
91
+ /** Build handler context to pass to domain handlers */
92
+ private buildContext(): HandlerContext {
93
+ return {
94
+ logger: this.logger,
95
+ searchEngine: this.searchEngine,
96
+ classifier: this.classifier,
97
+ compressor: this.compressor,
98
+ lastStoredId: this.lastStoredId,
99
+ lastStoredProject: this.lastStoredProject,
100
+ recentDecisions: this.recentDecisions,
101
+ setLastStored: (id: string, project: string) => {
102
+ this.lastStoredId = id || null
103
+ this.lastStoredProject = project || null
104
+ },
105
+ trackRecentDecision: (content: string, project: string) => {
106
+ const now = Date.now()
107
+ this.recentDecisions = this.recentDecisions.filter(
108
+ d => now - d.storedAt < RECENT_DECISION_WINDOW_MS
109
+ )
110
+ this.recentDecisions.push({ content, project, storedAt: now })
111
+ }
112
+ }
113
+ }
114
+
115
+ async route(input: BrainInput): Promise<BrainResponse> {
116
+ const { message, project: inputProject, action } = input
117
+
118
+ // Extract entities (needed for all paths)
119
+ const entities = await this.entityExtractor.extract(message, inputProject)
120
+ const project = entities.project || inputProject
121
+ this.logger.debug({ project, technologies: entities.technologies }, 'Entities extracted')
122
+
123
+ const ctx = this.buildContext()
124
+
125
+ // P1: Explicit action override — bypass intent classifier entirely
126
+ if (action && action !== 'auto') {
127
+ this.logger.debug({ action }, 'Using explicit action override')
128
+ try {
129
+ switch (action) {
130
+ case 'store':
131
+ return handleStoreThis(ctx, message, project, entities)
132
+ case 'recall':
133
+ return handleContextNeeded(ctx, message, project, entities)
134
+ case 'update':
135
+ return handleUpdateMemory(ctx, message, project, entities)
136
+ case 'delete':
137
+ return handleDeleteMemory(ctx, message, project, entities)
138
+ default:
139
+ break
140
+ }
141
+ } catch (error) {
142
+ this.logger.error({ error, action }, 'Router action override error')
143
+ return {
144
+ action: 'none',
145
+ summary: `Error processing ${action} request`,
146
+ content: `Failed to process: ${error instanceof Error ? error.message : 'Unknown error'}`,
147
+ relevantItems: 0
148
+ }
149
+ }
150
+ }
151
+
152
+ // Classify intent (SLM: use inference router if available, falls back to regex)
153
+ const classification = this.inferenceRouter
154
+ ? await this.inferenceRouter.classifyIntent(message)
155
+ : this.classifier.classify(message)
156
+ this.logger.debug({ intent: classification.primary, confidence: classification.confidence }, 'Intent classified')
157
+
158
+ // Route to handler
159
+ try {
160
+ switch (classification.primary) {
161
+ case 'no_action':
162
+ return { action: 'none', summary: 'No action needed', content: '', relevantItems: 0 }
163
+
164
+ case 'session_start':
165
+ return handleSessionStart(ctx, message, project, entities)
166
+
167
+ case 'context_needed':
168
+ return handleContextNeeded(ctx, message, project, entities, classification)
169
+
170
+ case 'decision_made':
171
+ return handleDecisionMade(ctx, message, project, entities)
172
+
173
+ case 'store_this':
174
+ return handleStoreThis(ctx, message, project, entities)
175
+
176
+ case 'pattern_found':
177
+ return handlePatternFound(ctx, message, project, entities)
178
+
179
+ case 'mistake_learned':
180
+ return handleMistakeLearned(ctx, message, project, entities)
181
+
182
+ case 'progress_update':
183
+ return handleProgressUpdate(ctx, message, project, entities)
184
+
185
+ case 'question':
186
+ return handleQuestion(ctx, message, project, entities, classification)
187
+
188
+ case 'comparison':
189
+ return handleComparison(ctx, message, project, entities)
190
+
191
+ case 'exploration':
192
+ return handleExploration(ctx, message, project, entities)
193
+
194
+ case 'list_all':
195
+ return handleListAll(ctx, message, project, entities)
196
+
197
+ case 'update_memory':
198
+ return handleUpdateMemory(ctx, message, project, entities)
199
+
200
+ case 'delete_memory':
201
+ return handleDeleteMemory(ctx, message, project, entities)
202
+
203
+ case 'detail_request':
204
+ return handleDetailRequest(ctx, message, project, entities)
205
+
206
+ case 'timeline':
207
+ return handleTimeline(ctx, message, project, entities)
208
+
209
+ default:
210
+ return handleContextNeeded(ctx, message, project, entities, classification)
211
+ }
212
+ } catch (error) {
213
+ this.logger.error({ error, intent: classification.primary }, 'Router handler error')
214
+ return {
215
+ action: 'none',
216
+ summary: `Error processing request`,
217
+ content: `Failed to process: ${error instanceof Error ? error.message : 'Unknown error'}`,
218
+ relevantItems: 0
219
+ }
220
+ }
221
+ }
222
+ }
223
+
224
+ // Lazy singleton
225
+ let routerInstance: BrainRouter | null = null
226
+
227
+ export function getBrainRouter(logger: Logger): BrainRouter {
228
+ if (!routerInstance) {
229
+ routerInstance = new BrainRouter(logger)
230
+ // SLM Upgrade: Wire inference router if available
231
+ try {
232
+ const { getInferenceRouter } = require('@/server/services')
233
+ const ir = getInferenceRouter()
234
+ if (ir) routerInstance.setInferenceRouter(ir)
235
+ } catch {
236
+ // Services not initialized yet — will use regex fallback
237
+ }
238
+ }
239
+ return routerInstance
240
+ }
241
+
242
+ /** Reset singleton for testing */
243
+ export function _resetBrainRouterForTesting(): void {
244
+ routerInstance = null
245
+ }