claude-brain 0.15.2 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (250) hide show
  1. package/README.md +191 -191
  2. package/VERSION +1 -1
  3. package/assets/CLAUDE-unified.md +11 -11
  4. package/assets/CLAUDE.md +29 -11
  5. package/bunfig.toml +8 -8
  6. package/package.json +82 -82
  7. package/packs/backend/node.json +173 -173
  8. package/packs/core/javascript.json +176 -176
  9. package/packs/core/typescript.json +222 -222
  10. package/packs/frontend/react.json +254 -254
  11. package/packs/meta/testing.json +172 -172
  12. package/scripts/postinstall.mjs +341 -341
  13. package/src/automation/auto-context.ts +240 -240
  14. package/src/automation/decision-detector.ts +452 -452
  15. package/src/automation/index.ts +11 -11
  16. package/src/automation/phase12-manager.ts +456 -456
  17. package/src/automation/proactive-recall.ts +373 -373
  18. package/src/automation/project-detector.ts +310 -310
  19. package/src/automation/repo-scanner.ts +205 -205
  20. package/src/cli/auto-setup.ts +82 -82
  21. package/src/cli/bin.ts +209 -202
  22. package/src/cli/commands/chroma.ts +573 -573
  23. package/src/cli/commands/git-hook.ts +189 -189
  24. package/src/cli/commands/hooks.ts +213 -213
  25. package/src/cli/commands/init.ts +122 -122
  26. package/src/cli/commands/install-mcp.ts +92 -92
  27. package/src/cli/commands/pack.ts +197 -197
  28. package/src/cli/commands/refresh.ts +323 -0
  29. package/src/cli/commands/serve.ts +167 -173
  30. package/src/cli/commands/start.ts +42 -42
  31. package/src/cli/commands/uninstall-mcp.ts +41 -41
  32. package/src/cli/commands/update.ts +124 -121
  33. package/src/cli/diagnose.ts +4 -4
  34. package/src/cli/health-check.ts +4 -4
  35. package/src/cli/migrate-chroma.ts +106 -106
  36. package/src/cli/setup.ts +4 -4
  37. package/src/cli/ui/animations.ts +80 -80
  38. package/src/cli/ui/components.ts +82 -82
  39. package/src/cli/ui/index.ts +4 -4
  40. package/src/cli/ui/logo.ts +36 -36
  41. package/src/cli/ui/theme.ts +55 -55
  42. package/src/config/defaults.ts +50 -50
  43. package/src/config/home.ts +55 -55
  44. package/src/config/index.ts +7 -7
  45. package/src/config/loader.ts +166 -166
  46. package/src/config/migration.ts +76 -76
  47. package/src/config/schema.ts +360 -360
  48. package/src/config/validator.ts +184 -184
  49. package/src/config/watcher.ts +86 -86
  50. package/src/context/assembler.ts +398 -398
  51. package/src/context/cache-manager.ts +101 -101
  52. package/src/context/formatter.ts +84 -84
  53. package/src/context/hierarchy.ts +85 -85
  54. package/src/context/index.ts +83 -83
  55. package/src/context/progress-tracker.ts +174 -174
  56. package/src/context/standards-manager.ts +287 -287
  57. package/src/context/types.ts +252 -252
  58. package/src/context/validator.ts +58 -58
  59. package/src/diagnostics/index.ts +123 -123
  60. package/src/health/index.ts +229 -229
  61. package/src/hooks/brain-hook.ts +128 -112
  62. package/src/hooks/capture.ts +168 -205
  63. package/src/hooks/context-hook.ts +137 -0
  64. package/src/hooks/deduplicator.ts +72 -72
  65. package/src/hooks/git-capture.ts +109 -109
  66. package/src/hooks/git-hook-installer.ts +207 -207
  67. package/src/hooks/index.ts +20 -20
  68. package/src/hooks/installer.ts +244 -194
  69. package/src/hooks/passive-classifier.ts +404 -723
  70. package/src/hooks/queue.ts +129 -129
  71. package/src/hooks/session-tracker.ts +312 -275
  72. package/src/hooks/types.ts +52 -47
  73. package/src/index.ts +7 -7
  74. package/src/intelligence/cross-project/affinity.ts +162 -162
  75. package/src/intelligence/cross-project/generalizer.ts +283 -283
  76. package/src/intelligence/cross-project/index.ts +13 -13
  77. package/src/intelligence/cross-project/transfer.ts +201 -201
  78. package/src/intelligence/index.ts +24 -24
  79. package/src/intelligence/optimization/index.ts +10 -10
  80. package/src/intelligence/optimization/precompute.ts +202 -202
  81. package/src/intelligence/optimization/semantic-cache.ts +207 -207
  82. package/src/intelligence/prediction/context-anticipator.ts +198 -198
  83. package/src/intelligence/prediction/decision-predictor.ts +184 -184
  84. package/src/intelligence/prediction/index.ts +13 -13
  85. package/src/intelligence/prediction/recommender.ts +268 -268
  86. package/src/intelligence/reasoning/chain-retrieval.ts +247 -247
  87. package/src/intelligence/reasoning/counterfactual.ts +248 -248
  88. package/src/intelligence/reasoning/index.ts +13 -13
  89. package/src/intelligence/reasoning/synthesizer.ts +169 -169
  90. package/src/intelligence/temporal/evolution.ts +197 -197
  91. package/src/intelligence/temporal/index.ts +16 -16
  92. package/src/intelligence/temporal/query-processor.ts +190 -190
  93. package/src/intelligence/temporal/timeline.ts +259 -259
  94. package/src/intelligence/temporal/trends.ts +263 -263
  95. package/src/knowledge/entity-extractor.ts +416 -416
  96. package/src/knowledge/graph/builder.ts +185 -185
  97. package/src/knowledge/graph/linker.ts +201 -201
  98. package/src/knowledge/graph/memory-graph.ts +359 -359
  99. package/src/knowledge/graph/schema.ts +99 -99
  100. package/src/knowledge/graph/search.ts +168 -168
  101. package/src/knowledge/relationship-extractor.ts +108 -108
  102. package/src/memory/chroma/client.ts +174 -174
  103. package/src/memory/chroma/collection-manager.ts +94 -94
  104. package/src/memory/chroma/config.ts +57 -57
  105. package/src/memory/chroma/embeddings.ts +155 -155
  106. package/src/memory/chroma/index.ts +82 -82
  107. package/src/memory/chroma/migration.ts +270 -270
  108. package/src/memory/chroma/schemas.ts +69 -69
  109. package/src/memory/chroma/search.ts +315 -315
  110. package/src/memory/chroma/store.ts +741 -741
  111. package/src/memory/consolidation/archiver.ts +164 -164
  112. package/src/memory/consolidation/merger.ts +186 -186
  113. package/src/memory/consolidation/scorer.ts +138 -138
  114. package/src/memory/context-builder.ts +236 -236
  115. package/src/memory/database.ts +169 -169
  116. package/src/memory/embedding-utils.ts +156 -156
  117. package/src/memory/embeddings.ts +226 -226
  118. package/src/memory/episodic/detector.ts +108 -108
  119. package/src/memory/episodic/manager.ts +351 -351
  120. package/src/memory/episodic/summarizer.ts +179 -179
  121. package/src/memory/episodic/types.ts +52 -52
  122. package/src/memory/index.ts +582 -582
  123. package/src/memory/knowledge-extractor.ts +455 -455
  124. package/src/memory/learning.ts +378 -378
  125. package/src/memory/patterns.ts +396 -396
  126. package/src/memory/schema.ts +88 -88
  127. package/src/memory/search.ts +309 -309
  128. package/src/memory/store.ts +787 -787
  129. package/src/memory/types.ts +121 -121
  130. package/src/orchestrator/coordinator.ts +272 -272
  131. package/src/orchestrator/decision-logger.ts +228 -228
  132. package/src/orchestrator/event-emitter.ts +198 -198
  133. package/src/orchestrator/event-queue.ts +184 -184
  134. package/src/orchestrator/handlers/base-handler.ts +70 -70
  135. package/src/orchestrator/handlers/context-handler.ts +73 -73
  136. package/src/orchestrator/handlers/decision-handler.ts +204 -204
  137. package/src/orchestrator/handlers/index.ts +10 -10
  138. package/src/orchestrator/handlers/status-handler.ts +131 -131
  139. package/src/orchestrator/handlers/task-handler.ts +171 -171
  140. package/src/orchestrator/index.ts +275 -275
  141. package/src/orchestrator/task-parser.ts +284 -284
  142. package/src/orchestrator/types.ts +98 -98
  143. package/src/packs/index.ts +9 -9
  144. package/src/packs/loader.ts +134 -134
  145. package/src/packs/manager.ts +204 -204
  146. package/src/packs/ranker.ts +78 -78
  147. package/src/packs/types.ts +81 -81
  148. package/src/phase12/index.ts +5 -5
  149. package/src/retrieval/bm25/index.ts +300 -300
  150. package/src/retrieval/bm25/tokenizer.ts +184 -184
  151. package/src/retrieval/feedback/adaptive.ts +223 -223
  152. package/src/retrieval/feedback/index.ts +16 -16
  153. package/src/retrieval/feedback/metrics.ts +223 -223
  154. package/src/retrieval/feedback/store.ts +283 -283
  155. package/src/retrieval/fusion/index.ts +194 -194
  156. package/src/retrieval/fusion/rrf.ts +163 -163
  157. package/src/retrieval/index.ts +12 -12
  158. package/src/retrieval/pipeline.ts +375 -375
  159. package/src/retrieval/query/expander.ts +198 -198
  160. package/src/retrieval/query/index.ts +27 -27
  161. package/src/retrieval/query/intent-classifier.ts +236 -236
  162. package/src/retrieval/query/temporal-parser.ts +295 -295
  163. package/src/retrieval/reranker/index.ts +188 -188
  164. package/src/retrieval/reranker/model.ts +95 -95
  165. package/src/retrieval/service.ts +125 -125
  166. package/src/retrieval/types.ts +162 -162
  167. package/src/routing/entity-extractor.ts +428 -428
  168. package/src/routing/intent-classifier.ts +450 -436
  169. package/src/routing/response-filter.ts +261 -258
  170. package/src/routing/router.ts +1441 -1322
  171. package/src/routing/search-engine.ts +515 -475
  172. package/src/routing/types.ts +94 -94
  173. package/src/scripts/health-check.ts +118 -118
  174. package/src/scripts/setup.ts +122 -122
  175. package/src/server/handlers/call-tool.ts +156 -156
  176. package/src/server/handlers/index.ts +9 -9
  177. package/src/server/handlers/list-tools.ts +35 -35
  178. package/src/server/handlers/tools/analyze-decision-evolution.ts +151 -151
  179. package/src/server/handlers/tools/auto-remember.ts +200 -200
  180. package/src/server/handlers/tools/brain.ts +85 -85
  181. package/src/server/handlers/tools/create-project.ts +135 -135
  182. package/src/server/handlers/tools/detect-trends.ts +144 -144
  183. package/src/server/handlers/tools/find-cross-project-patterns.ts +168 -168
  184. package/src/server/handlers/tools/get-activity-log.ts +194 -194
  185. package/src/server/handlers/tools/get-code-standards.ts +124 -124
  186. package/src/server/handlers/tools/get-corrections.ts +154 -154
  187. package/src/server/handlers/tools/get-decision-timeline.ts +172 -172
  188. package/src/server/handlers/tools/get-episode.ts +103 -103
  189. package/src/server/handlers/tools/get-patterns.ts +158 -158
  190. package/src/server/handlers/tools/get-phase12-status.ts +63 -63
  191. package/src/server/handlers/tools/get-project-context.ts +75 -75
  192. package/src/server/handlers/tools/get-recommendations.ts +145 -145
  193. package/src/server/handlers/tools/index.ts +31 -31
  194. package/src/server/handlers/tools/init-project.ts +757 -757
  195. package/src/server/handlers/tools/list-episodes.ts +90 -90
  196. package/src/server/handlers/tools/list-projects.ts +125 -125
  197. package/src/server/handlers/tools/rate-memory.ts +101 -101
  198. package/src/server/handlers/tools/recall-similar.ts +87 -87
  199. package/src/server/handlers/tools/recognize-pattern.ts +126 -126
  200. package/src/server/handlers/tools/record-correction.ts +125 -125
  201. package/src/server/handlers/tools/remember-decision.ts +153 -153
  202. package/src/server/handlers/tools/schemas.ts +253 -253
  203. package/src/server/handlers/tools/search-knowledge-graph.ts +102 -102
  204. package/src/server/handlers/tools/smart-context.ts +146 -146
  205. package/src/server/handlers/tools/update-progress.ts +131 -131
  206. package/src/server/handlers/tools/what-if-analysis.ts +135 -135
  207. package/src/server/http-api.ts +761 -693
  208. package/src/server/index.ts +40 -40
  209. package/src/server/mcp-server.ts +283 -283
  210. package/src/server/providers/index.ts +7 -7
  211. package/src/server/providers/prompts.ts +327 -327
  212. package/src/server/providers/resources.ts +622 -622
  213. package/src/server/services.ts +468 -468
  214. package/src/server/types.ts +39 -39
  215. package/src/server/utils/error-handler.ts +155 -155
  216. package/src/server/utils/index.ts +13 -13
  217. package/src/server/utils/memory-indicator.ts +83 -83
  218. package/src/server/utils/request-context.ts +122 -122
  219. package/src/server/utils/response-formatter.ts +129 -129
  220. package/src/server/utils/validators.ts +210 -210
  221. package/src/setup/index.ts +48 -48
  222. package/src/setup/wizard.ts +461 -461
  223. package/src/tools/index.ts +24 -24
  224. package/src/tools/registry.ts +115 -115
  225. package/src/tools/schemas.test.ts +30 -30
  226. package/src/tools/schemas.ts +617 -617
  227. package/src/tools/types.ts +412 -412
  228. package/src/utils/circuit-breaker.ts +130 -130
  229. package/src/utils/cleanup.ts +34 -34
  230. package/src/utils/error-handler.ts +132 -132
  231. package/src/utils/error-messages.ts +60 -60
  232. package/src/utils/fallback.ts +45 -45
  233. package/src/utils/index.ts +54 -54
  234. package/src/utils/logger-utils.ts +80 -80
  235. package/src/utils/logger.ts +88 -88
  236. package/src/utils/phase12-helper.ts +56 -56
  237. package/src/utils/retry.ts +94 -94
  238. package/src/utils/timing.ts +47 -47
  239. package/src/utils/transaction.ts +63 -63
  240. package/src/vault/frontmatter.ts +264 -264
  241. package/src/vault/index.ts +318 -318
  242. package/src/vault/paths.ts +106 -106
  243. package/src/vault/query.ts +422 -422
  244. package/src/vault/reader.ts +264 -264
  245. package/src/vault/templates.ts +186 -186
  246. package/src/vault/types.ts +73 -73
  247. package/src/vault/watcher.ts +277 -277
  248. package/src/vault/writer.ts +413 -413
  249. package/tsconfig.json +30 -30
  250. package/src/cli/auto-update.ts +0 -157
@@ -1,1322 +1,1441 @@
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 { BrainEntityExtractor, type BrainExtractedEntities } from './entity-extractor'
16
- import { ResponseFilter, type BrainResponse, type TierResults } from './response-filter'
17
- import { SearchEngine } from './search-engine'
18
- import type { NormalizedResult } from './types'
19
- import {
20
- getMemoryService,
21
- getVaultService,
22
- getContextService,
23
- getPhase12Service,
24
- getKnowledgeGraphService,
25
- getEpisodeService,
26
- getSessionTracker,
27
- isServicesInitialized
28
- } from '@/server/services'
29
- import { timed } from '@/utils/timing'
30
-
31
- /** Default project when none can be detected */
32
- const DEFAULT_PROJECT = 'general'
33
-
34
- /** Phase 19 D4: Minimum similarity for destructive operations */
35
- const DELETE_MIN_SIMILARITY = 0.5
36
- const UPDATE_MIN_SIMILARITY = 0.5
37
-
38
- export interface BrainInput {
39
- message: string
40
- project?: string
41
- }
42
-
43
- export class BrainRouter {
44
- private classifier: IntentClassifier
45
- private entityExtractor: BrainEntityExtractor
46
- private responseFilter: ResponseFilter
47
- private searchEngine: SearchEngine
48
- private logger: Logger
49
-
50
- /** Track the most recently stored decision ID for update/delete operations */
51
- private lastStoredId: string | null = null
52
- private lastStoredProject: string | null = null
53
-
54
- constructor(logger: Logger) {
55
- this.classifier = new IntentClassifier()
56
- this.entityExtractor = new BrainEntityExtractor()
57
- this.responseFilter = new ResponseFilter()
58
- this.searchEngine = new SearchEngine(logger)
59
- this.logger = logger.child({ component: 'brain-router' })
60
- }
61
-
62
- async route(input: BrainInput): Promise<BrainResponse> {
63
- const { message, project: inputProject } = input
64
-
65
- // Classify intent
66
- const classification = this.classifier.classify(message)
67
- this.logger.debug({ intent: classification.primary, confidence: classification.confidence }, 'Intent classified')
68
-
69
- // Extract entities
70
- const entities = await this.entityExtractor.extract(message, inputProject)
71
- const project = entities.project || inputProject
72
- this.logger.debug({ project, technologies: entities.technologies }, 'Entities extracted')
73
-
74
- // Route to handler
75
- try {
76
- switch (classification.primary) {
77
- case 'no_action':
78
- return this.handleNoAction(message)
79
-
80
- case 'session_start':
81
- return this.handleSessionStart(message, project, entities)
82
-
83
- case 'context_needed':
84
- return this.handleContextNeeded(message, project, entities, classification)
85
-
86
- case 'decision_made':
87
- return this.handleDecisionMade(message, project, entities)
88
-
89
- case 'store_this':
90
- return this.handleStoreThis(message, project, entities)
91
-
92
- case 'pattern_found':
93
- return this.handlePatternFound(message, project, entities)
94
-
95
- case 'mistake_learned':
96
- return this.handleMistakeLearned(message, project, entities)
97
-
98
- case 'progress_update':
99
- return this.handleProgressUpdate(message, project, entities)
100
-
101
- case 'question':
102
- return this.handleQuestion(message, project, entities, classification)
103
-
104
- case 'comparison':
105
- return this.handleComparison(message, project, entities)
106
-
107
- case 'exploration':
108
- return this.handleExploration(message, project, entities)
109
-
110
- case 'list_all':
111
- return this.handleListAll(message, project, entities)
112
-
113
- case 'update_memory':
114
- return this.handleUpdateMemory(message, project, entities)
115
-
116
- case 'delete_memory':
117
- return this.handleDeleteMemory(message, project, entities)
118
-
119
- default:
120
- return this.handleContextNeeded(message, project, entities, classification)
121
- }
122
- } catch (error) {
123
- this.logger.error({ error, intent: classification.primary }, 'Router handler error')
124
- return {
125
- action: 'none',
126
- summary: `Error processing request`,
127
- content: `Failed to process: ${error instanceof Error ? error.message : 'Unknown error'}`,
128
- relevantItems: 0
129
- }
130
- }
131
- }
132
-
133
- // ===== Intent Handlers =====
134
-
135
- private handleNoAction(_message: string): BrainResponse {
136
- return {
137
- action: 'none',
138
- summary: 'No action needed',
139
- content: '',
140
- relevantItems: 0
141
- }
142
- }
143
-
144
- private async handleSessionStart(
145
- message: string,
146
- project: string | undefined,
147
- entities: BrainExtractedEntities
148
- ): Promise<BrainResponse> {
149
- if (!project) {
150
- return {
151
- action: 'none',
152
- summary: 'No project detected',
153
- content: 'Could not determine project. Please specify which project you are working on.',
154
- relevantItems: 0
155
- }
156
- }
157
-
158
- if (!isServicesInitialized()) {
159
- return this.servicesNotReady()
160
- }
161
-
162
- const contextService = getContextService()
163
- const phase12 = getPhase12Service()
164
-
165
- // Get project context
166
- const context = await contextService.getContext(project, {
167
- includeMemories: false,
168
- includeProgress: true,
169
- includeStandards: true,
170
- maxTokens: 6000,
171
- relevanceThreshold: 0.5
172
- })
173
-
174
- const formattedContext = contextService.formatter.format(context)
175
-
176
- // Process with Phase 12 for proactive recall
177
- const phase12Result = await phase12.processMessage(
178
- entities.topic || message,
179
- project
180
- )
181
-
182
- // Build response
183
- const parts: string[] = [formattedContext]
184
-
185
- if (phase12Result.recalledMemories?.memories.length) {
186
- parts.push('\n---\n## Relevant Past Decisions\n')
187
- for (const mem of phase12Result.recalledMemories.memories) {
188
- const similarity = Math.round((mem.similarity || 0) * 100)
189
- const decisionText = mem.decision?.decision || mem.memory?.content || ''
190
- const safeText = typeof decisionText === 'string' ? decisionText : JSON.stringify(decisionText)
191
- parts.push(`**[${similarity}%]** ${safeText.slice(0, 200)}`)
192
- if (mem.decision?.reasoning) {
193
- const reasoning = typeof mem.decision.reasoning === 'string' ? mem.decision.reasoning : JSON.stringify(mem.decision.reasoning)
194
- parts.push(` _${reasoning}_`)
195
- }
196
- }
197
- }
198
-
199
- // If Phase 12 found nothing, do a direct search fallback via SearchEngine
200
- if (!phase12Result.recalledMemories?.memories.length) {
201
- try {
202
- const directResults = await this.searchEngine.enhancedSearch(entities.topic || message, {
203
- project,
204
- limit: 5,
205
- minSimilarity: 0.3
206
- })
207
- if (directResults.length > 0) {
208
- parts.push('\n---\n## Related Memories\n')
209
- for (const r of directResults) {
210
- const similarity = Math.round((r.score || 0) * 100)
211
- const safeContent = typeof r.content === 'string' ? r.content : JSON.stringify(r.content ?? '')
212
- parts.push(`**[${similarity}%]** ${safeContent.slice(0, 200)}`)
213
- }
214
- }
215
- } catch {
216
- // Direct search failed, continue without
217
- }
218
- }
219
-
220
- // C8: Register with episode manager
221
- this.registerEpisodeMessage(message, project, 'session_start')
222
-
223
- // C9: Append proactive recommendations
224
- try {
225
- const recommendations = await this.searchEngine.getRecommendations(
226
- entities.topic || message,
227
- { project, limit: 3 }
228
- )
229
- if (recommendations?.recommendations?.length) {
230
- parts.push('\n---\n## Proactive Recommendations\n')
231
- for (const rec of recommendations.recommendations) {
232
- parts.push(`- **${rec.type}**: ${rec.content?.slice(0, 150) || ''}`)
233
- if (rec.reasoning) parts.push(` _${rec.reasoning}_`)
234
- }
235
- }
236
- } catch {
237
- // Recommendations not available
238
- }
239
-
240
- const totalRecalled = phase12Result.recalledMemories?.memories.length || 0
241
- return {
242
- action: 'retrieved',
243
- summary: `Session context for ${project}${totalRecalled ? ` (${totalRecalled} memories)` : ''}`,
244
- content: parts.join('\n'),
245
- relevantItems: totalRecalled
246
- }
247
- }
248
-
249
- private async handleContextNeeded(
250
- message: string,
251
- project: string | undefined,
252
- entities: BrainExtractedEntities,
253
- classification?: ClassificationResult
254
- ): Promise<BrainResponse> {
255
- if (!isServicesInitialized()) {
256
- return this.servicesNotReady()
257
- }
258
-
259
- const effectiveProject = project || DEFAULT_PROJECT
260
- const query = entities.topic || message
261
- const tiers: TierResults[] = []
262
- const hasTemporal = classification?.secondary.includes('exploration') ||
263
- this.classifier.hasTemporalSignal(message.toLowerCase())
264
-
265
- // Use temporal search if temporal signals detected
266
- if (hasTemporal) {
267
- const { results } = await this.searchEngine.temporalSearch(query, { project: effectiveProject, limit: 5 })
268
- if (results.length > 0) {
269
- tiers.push({
270
- label: 'Memories',
271
- results: results.map(r => ({
272
- content: r.content,
273
- score: r.score,
274
- source: r.source === 'decision' ? 'Past Decision' : r.source,
275
- metadata: r.metadata as Record<string, unknown>
276
- }))
277
- })
278
- }
279
- } else {
280
- // Standard enhanced search
281
- const searchResults = await this.searchEngine.enhancedSearch(query, {
282
- project: effectiveProject,
283
- limit: 5,
284
- minSimilarity: 0.3
285
- })
286
- if (searchResults.length > 0) {
287
- tiers.push({
288
- label: 'Memories',
289
- results: searchResults.map(r => ({
290
- content: r.content,
291
- score: r.score,
292
- source: r.source === 'decision' ? 'Past Decision' : r.source,
293
- metadata: r.metadata as Record<string, unknown>
294
- }))
295
- })
296
- }
297
- }
298
-
299
- // Also search patterns
300
- const patternResults = await this.searchEngine.searchPatterns(query, { project: effectiveProject, limit: 3 })
301
- if (patternResults.length > 0) {
302
- tiers.push({
303
- label: 'Patterns',
304
- results: patternResults.map(p => ({
305
- content: p.content,
306
- score: p.score,
307
- source: `Pattern`,
308
- metadata: p.metadata as Record<string, unknown>
309
- }))
310
- })
311
- }
312
-
313
- return this.responseFilter.synthesize(tiers, message, effectiveProject)
314
- }
315
-
316
- /**
317
- * Handle explicit "store this" requests — always uses the FULL message as the decision text.
318
- */
319
- private async handleStoreThis(
320
- message: string,
321
- project: string | undefined,
322
- entities: BrainExtractedEntities
323
- ): Promise<BrainResponse> {
324
- const effectiveProject = project || DEFAULT_PROJECT
325
-
326
- if (!isServicesInitialized()) {
327
- return this.servicesNotReady()
328
- }
329
-
330
- const memory = getMemoryService()
331
- const decision = message
332
- const reasoning = entities.reasoning || ''
333
- const context = entities.topic || message.slice(0, 200)
334
-
335
- const decisionId = await timed('brain:store', () => memory.rememberDecision(
336
- effectiveProject,
337
- context,
338
- decision,
339
- reasoning,
340
- {
341
- alternatives: entities.alternatives,
342
- tags: entities.technologies.length > 0 ? entities.technologies : ['user-stored']
343
- }
344
- ))
345
-
346
- // Track for potential update/delete
347
- this.lastStoredId = decisionId
348
- this.lastStoredProject = effectiveProject
349
-
350
- // Invalidate cache for this project
351
- this.searchEngine.invalidateCache(effectiveProject)
352
-
353
- // Background: vault write + episode linking (non-critical)
354
- setImmediate(() => {
355
- this.writeToVault(effectiveProject, decision, reasoning, context, entities.alternatives)
356
- this.linkToActiveEpisode(effectiveProject, decisionId, 'decision')
357
- })
358
-
359
- return {
360
- action: 'stored',
361
- summary: `Stored: ${message.slice(0, 60)}`,
362
- content: `Memory stored (ID: ${decisionId})\n\n**Project:** ${effectiveProject}\n**Content:** ${decision}${reasoning ? `\n**Reasoning:** ${reasoning}` : ''}`,
363
- relevantItems: 1
364
- }
365
- }
366
-
367
- private async handleDecisionMade(
368
- message: string,
369
- project: string | undefined,
370
- entities: BrainExtractedEntities
371
- ): Promise<BrainResponse> {
372
- const effectiveProject = project || DEFAULT_PROJECT
373
-
374
- if (!isServicesInitialized()) {
375
- return this.servicesNotReady()
376
- }
377
-
378
- const memory = getMemoryService()
379
- const decision = message
380
- const reasoning = entities.reasoning || ''
381
- const context = entities.topic || message.slice(0, 200)
382
- const alternatives = entities.alternatives
383
-
384
- const decisionId = await timed('brain:store', () => memory.rememberDecision(
385
- effectiveProject,
386
- context,
387
- decision,
388
- reasoning,
389
- {
390
- alternatives,
391
- tags: entities.technologies.length > 0 ? entities.technologies : ['auto-detected']
392
- }
393
- ))
394
-
395
- this.lastStoredId = decisionId
396
- this.lastStoredProject = effectiveProject
397
-
398
- // Invalidate cache
399
- this.searchEngine.invalidateCache(effectiveProject)
400
-
401
- // Background: vault write + episode linking (non-critical)
402
- setImmediate(() => {
403
- this.writeToVault(effectiveProject, decision, reasoning, context, alternatives)
404
- this.linkToActiveEpisode(effectiveProject, decisionId, 'decision')
405
- })
406
-
407
- return {
408
- action: 'stored',
409
- summary: `Stored decision: ${message.slice(0, 60)}`,
410
- content: `Decision stored (ID: ${decisionId})\n\n**Project:** ${effectiveProject}\n**Decision:** ${decision}\n**Reasoning:** ${reasoning}`,
411
- relevantItems: 1
412
- }
413
- }
414
-
415
- private async handlePatternFound(
416
- message: string,
417
- project: string | undefined,
418
- entities: BrainExtractedEntities
419
- ): Promise<BrainResponse> {
420
- const effectiveProject = project || DEFAULT_PROJECT
421
-
422
- if (!isServicesInitialized()) {
423
- return this.servicesNotReady()
424
- }
425
-
426
- const memory = getMemoryService()
427
- const patternType = entities.patternType || 'solution'
428
- const description = message
429
-
430
- const patternId = await memory.storePattern({
431
- project: effectiveProject,
432
- pattern_type: patternType,
433
- description,
434
- confidence: 0.8,
435
- context: message.slice(0, 300)
436
- })
437
-
438
- // Invalidate cache
439
- this.searchEngine.invalidateCache(effectiveProject)
440
-
441
- // Link to active episode
442
- this.linkToActiveEpisode(effectiveProject, patternId, 'pattern')
443
-
444
- return {
445
- action: 'stored',
446
- summary: `Stored ${patternType}: ${description.slice(0, 60)}`,
447
- content: `Pattern stored (ID: ${patternId})\n\n**Type:** ${patternType}\n**Project:** ${effectiveProject}\n**Description:** ${description}`,
448
- relevantItems: 1
449
- }
450
- }
451
-
452
- private async handleMistakeLearned(
453
- message: string,
454
- project: string | undefined,
455
- entities: BrainExtractedEntities
456
- ): Promise<BrainResponse> {
457
- const effectiveProject = project || DEFAULT_PROJECT
458
-
459
- if (!isServicesInitialized()) {
460
- return this.servicesNotReady()
461
- }
462
-
463
- const memory = getMemoryService()
464
- const original = entities.original || message
465
- const correction = entities.correction || ''
466
- const reasoning = entities.reasoning || 'Lesson learned from experience'
467
-
468
- const correctionId = await memory.storeCorrection({
469
- project: effectiveProject,
470
- original,
471
- correction: correction || message,
472
- reasoning,
473
- context: entities.topic || '',
474
- confidence: 0.9
475
- })
476
-
477
- // Invalidate cache
478
- this.searchEngine.invalidateCache(effectiveProject)
479
-
480
- // Link to active episode
481
- this.linkToActiveEpisode(effectiveProject, correctionId, 'correction')
482
-
483
- return {
484
- action: 'stored',
485
- summary: `Stored correction: ${original.slice(0, 60)}`,
486
- content: `Correction stored (ID: ${correctionId})\n\n**Project:** ${effectiveProject}\n**Original:** ${original}\n**Correction:** ${correction || '(see original message)'}`,
487
- relevantItems: 1
488
- }
489
- }
490
-
491
- private async handleProgressUpdate(
492
- message: string,
493
- project: string | undefined,
494
- entities: BrainExtractedEntities
495
- ): Promise<BrainResponse> {
496
- const effectiveProject = project || DEFAULT_PROJECT
497
-
498
- if (!isServicesInitialized()) {
499
- return this.servicesNotReady()
500
- }
501
-
502
- const contextService = getContextService()
503
- const memory = getMemoryService()
504
-
505
- const completedTask = entities.completedTask || message
506
- const nextSteps = entities.nextSteps || 'Continue development'
507
-
508
- // Store in session context
509
- try {
510
- await contextService.progress.addCompletedTask(effectiveProject, {
511
- id: this.generateTaskId(completedTask),
512
- title: completedTask,
513
- status: 'done',
514
- completedAt: new Date()
515
- })
516
- } catch {
517
- // Progress update failed
518
- }
519
-
520
- // Also store as a searchable memory
521
- try {
522
- await memory.rememberDecision(
523
- effectiveProject,
524
- `Progress update`,
525
- `Progress: ${completedTask}${nextSteps !== 'Continue development' ? `. Next: ${nextSteps}` : ''}`,
526
- 'Progress tracking',
527
- { tags: ['progress', ...entities.technologies] }
528
- )
529
-
530
- // Invalidate cache
531
- this.searchEngine.invalidateCache(effectiveProject)
532
- } catch {
533
- // Memory storage failed
534
- }
535
-
536
- // Register with episode
537
- this.registerEpisodeMessage(message, effectiveProject, 'progress_update')
538
-
539
- return {
540
- action: 'stored',
541
- summary: `Progress: ${completedTask.slice(0, 60)}`,
542
- content: `Progress updated for ${effectiveProject}\n\n**Completed:** ${completedTask}\n**Next:** ${nextSteps}`,
543
- relevantItems: 1
544
- }
545
- }
546
-
547
- /**
548
- * List all decisions for a project
549
- */
550
- private async handleListAll(
551
- message: string,
552
- project: string | undefined,
553
- entities: BrainExtractedEntities
554
- ): Promise<BrainResponse> {
555
- if (!isServicesInitialized()) {
556
- return this.servicesNotReady()
557
- }
558
-
559
- const memory = getMemoryService()
560
- const effectiveProject = project
561
-
562
- try {
563
- const decisions = await memory.fetchAllDecisions(effectiveProject)
564
- const patterns = await memory.fetchAllPatterns(effectiveProject)
565
- const corrections = await memory.fetchAllCorrections(effectiveProject)
566
-
567
- const parts: string[] = []
568
- const projectLabel = effectiveProject || 'all projects'
569
-
570
- if (decisions.length > 0) {
571
- parts.push(`## Decisions (${decisions.length})`)
572
- for (const d of decisions.slice(0, 20)) {
573
- const rawDecision = d.decision || d.document || d.content || ''
574
- const decision = typeof rawDecision === 'string' ? rawDecision : JSON.stringify(rawDecision)
575
- const rawDate = d.created_at || d.date || ''
576
- const date = rawDate ? ` (${String(rawDate).split('T')[0]})` : ''
577
- parts.push(`- ${decision.slice(0, 150)}${date}`)
578
- }
579
- if (decisions.length > 20) {
580
- parts.push(`_...and ${decisions.length - 20} more_`)
581
- }
582
- parts.push('')
583
- }
584
-
585
- if (patterns.length > 0) {
586
- parts.push(`## Patterns (${patterns.length})`)
587
- for (const p of patterns.slice(0, 10)) {
588
- const rawDesc = p.description || p.document || p.content || ''
589
- const desc = typeof rawDesc === 'string' ? rawDesc : JSON.stringify(rawDesc)
590
- parts.push(`- **${p.pattern_type || 'solution'}**: ${desc.slice(0, 120)}`)
591
- }
592
- parts.push('')
593
- }
594
-
595
- if (corrections.length > 0) {
596
- parts.push(`## Corrections (${corrections.length})`)
597
- for (const c of corrections.slice(0, 10)) {
598
- const rawOrig = c.original || c.document || c.content || ''
599
- const orig = typeof rawOrig === 'string' ? rawOrig : JSON.stringify(rawOrig)
600
- parts.push(`- ${orig.slice(0, 120)}`)
601
- }
602
- parts.push('')
603
- }
604
-
605
- const total = decisions.length + patterns.length + corrections.length
606
-
607
- if (total === 0) {
608
- return {
609
- action: 'none',
610
- summary: `No memories found for ${projectLabel}`,
611
- content: `No decisions, patterns, or corrections stored yet for ${projectLabel}.`,
612
- relevantItems: 0
613
- }
614
- }
615
-
616
- return {
617
- action: 'retrieved',
618
- summary: `${total} memories for ${projectLabel}`,
619
- content: parts.join('\n'),
620
- relevantItems: total
621
- }
622
- } catch (error) {
623
- return {
624
- action: 'none',
625
- summary: 'Error listing memories',
626
- content: `Failed to list memories: ${error instanceof Error ? error.message : 'Unknown error'}`,
627
- relevantItems: 0
628
- }
629
- }
630
- }
631
-
632
- /**
633
- * Phase 19 D4: Update with higher similarity threshold
634
- */
635
- private async handleUpdateMemory(
636
- message: string,
637
- project: string | undefined,
638
- entities: BrainExtractedEntities
639
- ): Promise<BrainResponse> {
640
- const effectiveProject = project || this.lastStoredProject || DEFAULT_PROJECT
641
-
642
- if (!isServicesInitialized()) {
643
- return this.servicesNotReady()
644
- }
645
-
646
- const memory = getMemoryService()
647
- const topic = entities.topic || message
648
- const hasSpecificContent = this.hasDescriptiveContent(message)
649
-
650
- if (hasSpecificContent) {
651
- try {
652
- const results = await memory.searchRaw(topic, {
653
- project: effectiveProject,
654
- limit: 1,
655
- minSimilarity: UPDATE_MIN_SIMILARITY
656
- })
657
-
658
- const matchId = results[0]?.memory?.id || results[0]?.decision?.id || results[0]?.id
659
- const matchSimilarity = results[0]?.similarity || 0
660
-
661
- if (results.length > 0 && matchId && matchSimilarity >= UPDATE_MIN_SIMILARITY) {
662
- const oldId = matchId
663
- const newId = await memory.updateDecision(
664
- oldId,
665
- effectiveProject,
666
- `Updated: ${topic.slice(0, 200)}`,
667
- message,
668
- entities.reasoning || 'Updated via brain tool',
669
- { tags: entities.technologies.length > 0 ? entities.technologies : ['updated'] }
670
- )
671
-
672
- this.lastStoredId = newId
673
- this.lastStoredProject = effectiveProject
674
-
675
- // Invalidate cache
676
- this.searchEngine.invalidateCache(effectiveProject)
677
-
678
- const oldContent = results[0].decision?.decision || results[0].memory?.content?.slice(0, 100) || results[0].content?.slice(0, 100) || ''
679
- return {
680
- action: 'stored',
681
- summary: `Updated decision`,
682
- content: `Replaced: "${oldContent.slice(0, 80)}"\n\nWith:\n**New content:** ${message}`,
683
- relevantItems: 1
684
- }
685
- } else if (results.length > 0 && matchSimilarity < UPDATE_MIN_SIMILARITY) {
686
- // D4: No confident match — store as new instead of updating wrong memory
687
- return this.storeAsNew(memory, effectiveProject, message, topic, entities, 'No confident match to update (similarity too low)')
688
- }
689
- } catch {
690
- // Search failed, fall through
691
- }
692
- }
693
-
694
- // Generic update — use lastStoredId
695
- if (this.lastStoredId) {
696
- const newId = await memory.updateDecision(
697
- this.lastStoredId,
698
- effectiveProject,
699
- `Updated: ${topic.slice(0, 200)}`,
700
- message,
701
- entities.reasoning || 'Updated via brain tool',
702
- { tags: entities.technologies.length > 0 ? entities.technologies : ['updated'] }
703
- )
704
-
705
- this.lastStoredId = newId
706
- this.lastStoredProject = effectiveProject
707
- this.searchEngine.invalidateCache(effectiveProject)
708
-
709
- return {
710
- action: 'stored',
711
- summary: `Updated decision`,
712
- content: `Previous decision replaced with:\n\n**Project:** ${effectiveProject}\n**New content:** ${message}`,
713
- relevantItems: 1
714
- }
715
- }
716
-
717
- // Fallback: store as new decision
718
- return this.storeAsNew(memory, effectiveProject, message, topic, entities, 'No matching decision found to update')
719
- }
720
-
721
- /**
722
- * Phase 19 D4: Delete with higher similarity threshold
723
- */
724
- private async handleDeleteMemory(
725
- message: string,
726
- project: string | undefined,
727
- entities: BrainExtractedEntities
728
- ): Promise<BrainResponse> {
729
- const effectiveProject = project || this.lastStoredProject || DEFAULT_PROJECT
730
-
731
- if (!isServicesInitialized()) {
732
- return this.servicesNotReady()
733
- }
734
-
735
- const memory = getMemoryService()
736
- const topic = entities.topic || message
737
- const hasSpecificContent = this.hasDescriptiveContent(message)
738
-
739
- if (hasSpecificContent) {
740
- try {
741
- const results = await memory.searchRaw(topic, {
742
- project: effectiveProject,
743
- limit: 3,
744
- minSimilarity: DELETE_MIN_SIMILARITY
745
- })
746
-
747
- const matchId = results[0]?.memory?.id || results[0]?.decision?.id || results[0]?.id
748
- const matchSimilarity = results[0]?.similarity || 0
749
-
750
- if (results.length > 0 && matchId && matchSimilarity >= DELETE_MIN_SIMILARITY) {
751
- const targetId = matchId
752
- const content = results[0].decision?.decision || results[0].memory?.content?.slice(0, 100) || results[0].content?.slice(0, 100) || ''
753
-
754
- await memory.deleteDecision(targetId)
755
- if (this.lastStoredId === targetId) this.lastStoredId = null
756
-
757
- // Invalidate cache
758
- this.searchEngine.invalidateCache(effectiveProject)
759
-
760
- return {
761
- action: 'stored',
762
- summary: `Deleted memory`,
763
- content: `Deleted: "${content.slice(0, 100)}" (ID: ${targetId})`,
764
- relevantItems: 0
765
- }
766
- } else if (results.length > 0 && matchSimilarity < DELETE_MIN_SIMILARITY) {
767
- // D4: No confident match
768
- return {
769
- action: 'none',
770
- summary: 'No confident match to delete',
771
- 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.`,
772
- relevantItems: 0
773
- }
774
- }
775
- } catch {
776
- // Search failed, fall through
777
- }
778
- }
779
-
780
- // Generic request use lastStoredId
781
- if (this.lastStoredId) {
782
- try {
783
- await memory.deleteDecision(this.lastStoredId)
784
- const deletedId = this.lastStoredId
785
- this.lastStoredId = null
786
- this.searchEngine.invalidateCache(effectiveProject)
787
- return {
788
- action: 'stored',
789
- summary: `Deleted most recent memory`,
790
- content: `Deleted memory (ID: ${deletedId})`,
791
- relevantItems: 0
792
- }
793
- } catch {
794
- // Deletion failed
795
- }
796
- }
797
-
798
- return {
799
- action: 'none',
800
- summary: 'No matching memory found to delete',
801
- content: 'Could not find a memory matching your request. Try being more specific about what to delete.',
802
- relevantItems: 0
803
- }
804
- }
805
-
806
- /**
807
- * Phase 19 C7+C10+C11: Enhanced question handler
808
- * - Uses SearchEngine for all searches (hybrid, cached)
809
- * - Temporal search when temporal signals detected
810
- * - ChainRetrieval for complex multi-part questions
811
- * - CrossProject patterns for general/unspecified project
812
- * - KnowledgeGraph enrichment
813
- */
814
- private async handleQuestion(
815
- message: string,
816
- project: string | undefined,
817
- entities: BrainExtractedEntities,
818
- classification: ClassificationResult
819
- ): Promise<BrainResponse> {
820
- if (!isServicesInitialized()) {
821
- return this.servicesNotReady()
822
- }
823
-
824
- const effectiveProject = project || DEFAULT_PROJECT
825
- const query = entities.topic || message
826
- const tiers: TierResults[] = []
827
- const hasTemporal = classification.secondary.includes('exploration') ||
828
- this.classifier.hasTemporalSignal(message.toLowerCase())
829
-
830
- // C7: Detect multi-part questions for chain retrieval
831
- const isComplex = this.isComplexQuestion(message)
832
-
833
- // Main search temporal or standard
834
- let searchResults: NormalizedResult[]
835
- if (hasTemporal) {
836
- const { results } = await this.searchEngine.temporalSearch(query, { project: effectiveProject, limit: 5 })
837
- searchResults = results
838
- } else if (isComplex) {
839
- // C7: Try multi-hop chain retrieval
840
- const chainResult = await this.searchEngine.chainSearch(query, { project: effectiveProject })
841
- if (chainResult?.allResults?.length) {
842
- searchResults = chainResult.allResults.map((r: any) => ({
843
- id: r.id || '',
844
- content: r.content || '',
845
- score: r.similarity || 0,
846
- source: 'decision' as const,
847
- project: r.metadata?.project || effectiveProject || '',
848
- date: r.metadata?.created_at || '',
849
- metadata: r.metadata || {}
850
- }))
851
- } else {
852
- searchResults = await this.searchEngine.enhancedSearch(query, { project: effectiveProject, limit: 5 })
853
- }
854
- } else {
855
- searchResults = await this.searchEngine.enhancedSearch(query, { project: effectiveProject, limit: 5 })
856
- }
857
-
858
- if (searchResults.length > 0) {
859
- tiers.push({
860
- label: 'Memories',
861
- results: searchResults.map(r => ({
862
- content: r.content,
863
- score: r.score,
864
- source: r.source === 'decision' ? 'Past Decision' : r.source,
865
- metadata: r.metadata as Record<string, unknown>
866
- }))
867
- })
868
- }
869
-
870
- // Parallel: patterns + corrections + graph
871
- const [patternResults, correctionResults, graphResults] = await Promise.all([
872
- this.searchEngine.searchPatterns(query, { project: effectiveProject, limit: 3 }),
873
- this.searchEngine.searchCorrections(query, { project: effectiveProject, limit: 3 }),
874
- // C11: KnowledgeGraph enrichment for all questions
875
- this.searchEngine.searchGraph(query, 5)
876
- ])
877
-
878
- if (patternResults.length > 0) {
879
- tiers.push({
880
- label: 'Patterns',
881
- results: patternResults.map(p => ({
882
- content: p.content,
883
- score: p.score,
884
- source: `Pattern`,
885
- metadata: p.metadata as Record<string, unknown>
886
- }))
887
- })
888
- }
889
-
890
- if (correctionResults.length > 0) {
891
- tiers.push({
892
- label: 'Corrections',
893
- results: correctionResults.map(c => ({
894
- content: c.content,
895
- score: c.score,
896
- source: 'Lesson Learned',
897
- metadata: c.metadata as Record<string, unknown>
898
- }))
899
- })
900
- }
901
-
902
- // C11: Add graph results as "Related Concepts"
903
- if (graphResults.length > 0) {
904
- tiers.push({
905
- label: 'Related Concepts',
906
- results: graphResults.map(g => ({
907
- content: g.content,
908
- score: g.score,
909
- source: 'Knowledge Graph',
910
- metadata: g.metadata as Record<string, unknown>
911
- }))
912
- })
913
- }
914
-
915
- // C10: CrossProject patterns for general/unspecified project queries
916
- if (!project || project === 'general') {
917
- try {
918
- const crossProject = await this.searchEngine.findCrossProjectPatterns({
919
- query,
920
- limit: 3
921
- })
922
- if (crossProject?.patterns?.length) {
923
- tiers.push({
924
- label: 'Cross-Project Patterns',
925
- results: crossProject.patterns.map((p: any) => ({
926
- content: `${p.description || ''} (${p.projects?.join(', ') || 'multiple projects'})`,
927
- score: p.confidence || 0.5,
928
- source: 'Cross-Project',
929
- metadata: {}
930
- }))
931
- })
932
- }
933
- } catch {
934
- // Cross-project not available
935
- }
936
- }
937
-
938
- // Phase 23b Fix 5: Session recall "what have we discussed" queries session tracker
939
- if (this.isSessionRecallQuery(message)) {
940
- try {
941
- const tracker = getSessionTracker()
942
- if (tracker) {
943
- const stats = tracker.getStats()
944
- if (stats.totalItems > 0) {
945
- tiers.push({
946
- label: 'Current Session',
947
- results: [{
948
- content: `Active sessions: ${stats.activeSessions}, items tracked: ${stats.totalItems}`,
949
- score: 0.95,
950
- source: 'Session Tracker',
951
- metadata: {}
952
- }]
953
- })
954
- }
955
- }
956
- } catch {
957
- // Session tracker not available
958
- }
959
- }
960
-
961
- // Register with episode
962
- this.registerEpisodeMessage(message, effectiveProject, 'question')
963
-
964
- return this.responseFilter.synthesize(tiers, message, effectiveProject)
965
- }
966
-
967
- /**
968
- * Phase 19 C6: Enhanced exploration handler
969
- * - Timeline for "timeline"/"chronological" queries
970
- * - DecisionEvolutionTracker for "how has X changed" queries
971
- * - TrendDetector for "trends"/"emerging" queries
972
- */
973
- private async handleExploration(
974
- message: string,
975
- project: string | undefined,
976
- entities: BrainExtractedEntities
977
- ): Promise<BrainResponse> {
978
- if (!isServicesInitialized()) {
979
- return this.servicesNotReady()
980
- }
981
-
982
- const effectiveProject = project || DEFAULT_PROJECT
983
- const query = entities.topic || message
984
- const lower = message.toLowerCase()
985
- const tiers: TierResults[] = []
986
-
987
- // C6: Timeline queries
988
- if (lower.includes('timeline') || lower.includes('chronological') || lower.includes('history of')) {
989
- const timeline = await this.searchEngine.buildTimeline({
990
- project: effectiveProject,
991
- topic: query,
992
- limit: 20
993
- })
994
-
995
- if (timeline?.entries?.length) {
996
- tiers.push({
997
- label: 'Timeline',
998
- results: timeline.entries.map((e: any) => ({
999
- content: `**${e.date || ''}** — ${e.content || ''}`,
1000
- score: 0.8,
1001
- source: `Timeline (${e.type || 'event'})`,
1002
- metadata: e.metadata || {}
1003
- }))
1004
- })
1005
- }
1006
- }
1007
-
1008
- // C6: Evolution queries
1009
- if (lower.includes('evolution') || lower.includes('how has') || lower.includes('changed') || lower.includes('evolved')) {
1010
- const evolution = await this.searchEngine.analyzeEvolution(query, { project: effectiveProject })
1011
- if (evolution?.timeline?.length) {
1012
- const parts = []
1013
- parts.push(`**Stability:** ${evolution.stability || 'unknown'}`)
1014
- if (evolution.currentState) parts.push(`**Current:** ${evolution.currentState}`)
1015
- for (const change of (evolution.changes || []).slice(0, 5)) {
1016
- parts.push(`- ${change.description || change}`)
1017
- }
1018
-
1019
- tiers.push({
1020
- label: 'Decision Evolution',
1021
- results: [{
1022
- content: parts.join('\n'),
1023
- score: 0.8,
1024
- source: 'Evolution Analysis',
1025
- metadata: {}
1026
- }]
1027
- })
1028
- }
1029
- }
1030
-
1031
- // C6: Trend queries
1032
- if (lower.includes('trend') || lower.includes('emerging') || lower.includes('declining')) {
1033
- const trends = await this.searchEngine.detectTrends({ project: effectiveProject })
1034
- if (trends?.topTrends?.length) {
1035
- tiers.push({
1036
- label: 'Trends',
1037
- results: trends.topTrends.slice(0, 5).map((t: any) => ({
1038
- content: `**${t.term}** — ${t.trend} (${t.occurrences} occurrences, momentum: ${t.momentum || 'unknown'})`,
1039
- score: t.occurrences > 5 ? 0.9 : 0.7,
1040
- source: `Trend (${t.trend})`,
1041
- metadata: {}
1042
- }))
1043
- })
1044
- }
1045
- }
1046
-
1047
- // Always include graph exploration
1048
- const graphResults = await this.searchEngine.searchGraph(query, 10)
1049
- if (graphResults.length > 0) {
1050
- tiers.push({
1051
- label: 'Knowledge Graph',
1052
- results: graphResults.map(g => ({
1053
- content: g.content,
1054
- score: g.score,
1055
- source: 'Knowledge Graph',
1056
- metadata: g.metadata as Record<string, unknown>
1057
- }))
1058
- })
1059
- }
1060
-
1061
- // Issue 9: Cross-project patterns for exploration queries
1062
- if (!project || project === 'general') {
1063
- try {
1064
- const crossProject = await this.searchEngine.findCrossProjectPatterns({
1065
- query,
1066
- limit: 3
1067
- })
1068
- if (crossProject?.patterns?.length) {
1069
- tiers.push({
1070
- label: 'Cross-Project Patterns',
1071
- results: crossProject.patterns.map((p: any) => ({
1072
- content: `${p.description || ''} (${p.projects?.join(', ') || 'multiple projects'})`,
1073
- score: p.confidence || 0.5,
1074
- source: 'Cross-Project',
1075
- metadata: {}
1076
- }))
1077
- })
1078
- }
1079
- } catch {
1080
- // Cross-project not available
1081
- }
1082
- }
1083
-
1084
- // Also do a basic memory search as fallback
1085
- const searchResults = await this.searchEngine.enhancedSearch(query, {
1086
- project: effectiveProject,
1087
- limit: 5,
1088
- minSimilarity: 0.2
1089
- })
1090
- if (searchResults.length > 0) {
1091
- tiers.push({
1092
- label: 'Memories',
1093
- results: searchResults.map(r => ({
1094
- content: r.content,
1095
- score: r.score,
1096
- source: r.source === 'decision' ? 'Past Decision' : r.source,
1097
- metadata: r.metadata as Record<string, unknown>
1098
- }))
1099
- })
1100
- }
1101
-
1102
- return this.responseFilter.synthesize(tiers, message, effectiveProject, 'analyzed')
1103
- }
1104
-
1105
- private async handleComparison(
1106
- message: string,
1107
- project: string | undefined,
1108
- entities: BrainExtractedEntities
1109
- ): Promise<BrainResponse> {
1110
- if (!isServicesInitialized()) {
1111
- return this.servicesNotReady()
1112
- }
1113
-
1114
- const effectiveProject = project || DEFAULT_PROJECT
1115
- const query = entities.topic || message
1116
- const tiers: TierResults[] = []
1117
-
1118
- // Search for related decisions
1119
- const searchResults = await this.searchEngine.enhancedSearch(query, {
1120
- project: effectiveProject,
1121
- limit: 5,
1122
- minSimilarity: 0.2
1123
- })
1124
- if (searchResults.length > 0) {
1125
- tiers.push({
1126
- label: 'Related Decisions',
1127
- results: searchResults.map(r => ({
1128
- content: r.content,
1129
- score: r.score,
1130
- source: 'Past Decision',
1131
- metadata: r.metadata as Record<string, unknown>
1132
- }))
1133
- })
1134
- }
1135
-
1136
- // Graph search for comparison context
1137
- const graphResults = await this.searchEngine.searchGraph(query, 5)
1138
- if (graphResults.length > 0) {
1139
- tiers.push({
1140
- label: 'Knowledge Graph',
1141
- results: graphResults.map(g => ({
1142
- content: g.content,
1143
- score: g.score,
1144
- source: 'Knowledge Graph',
1145
- metadata: g.metadata as Record<string, unknown>
1146
- }))
1147
- })
1148
- }
1149
-
1150
- return this.responseFilter.synthesize(tiers, message, effectiveProject, 'analyzed')
1151
- }
1152
-
1153
- // ===== Helpers =====
1154
-
1155
- private async writeToVault(
1156
- project: string,
1157
- decision: string,
1158
- reasoning: string,
1159
- context: string,
1160
- alternatives?: string
1161
- ): Promise<void> {
1162
- try {
1163
- const vault = getVaultService()
1164
- const projectPaths = vault.getProjectPaths(project)
1165
- const date = new Date().toISOString().split('T')[0]
1166
- 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`
1167
- await vault.writer.appendContent(projectPaths.decisions, entry, '\n')
1168
- } catch {
1169
- // Vault write failed
1170
- }
1171
- }
1172
-
1173
- /**
1174
- * Store as a new decision (fallback for update when no match found)
1175
- */
1176
- private async storeAsNew(
1177
- memory: any,
1178
- project: string,
1179
- message: string,
1180
- topic: string,
1181
- entities: BrainExtractedEntities,
1182
- reason: string
1183
- ): Promise<BrainResponse> {
1184
- const decisionId = await memory.rememberDecision(
1185
- project,
1186
- topic.slice(0, 200),
1187
- message,
1188
- entities.reasoning || '',
1189
- { tags: entities.technologies.length > 0 ? entities.technologies : ['updated'] }
1190
- )
1191
-
1192
- this.lastStoredId = decisionId
1193
- this.lastStoredProject = project
1194
- this.searchEngine.invalidateCache(project)
1195
-
1196
- return {
1197
- action: 'stored',
1198
- summary: `Stored as new (${reason})`,
1199
- content: `Stored as new decision (ID: ${decisionId})\n\n**Project:** ${project}\n**Content:** ${message}`,
1200
- relevantItems: 1
1201
- }
1202
- }
1203
-
1204
- /**
1205
- * C8: Register a message with the episode manager
1206
- */
1207
- private registerEpisodeMessage(message: string, project?: string, role: string = 'user'): void {
1208
- try {
1209
- const episodeManager = getEpisodeService()
1210
- if (!episodeManager) return
1211
- episodeManager.processMessage(
1212
- { role, content: message, timestamp: new Date().toISOString() },
1213
- project
1214
- ).catch(() => {})
1215
- } catch {
1216
- // Episode manager not available
1217
- }
1218
- }
1219
-
1220
- /**
1221
- * C8: Link a stored decision/pattern/correction to the active episode
1222
- */
1223
- private linkToActiveEpisode(project: string, id: string, type: 'decision' | 'pattern' | 'correction'): void {
1224
- try {
1225
- const episodeManager = getEpisodeService()
1226
- if (!episodeManager) return
1227
- const activeEpisode = episodeManager.getActiveEpisode(project)
1228
- if (!activeEpisode) return
1229
-
1230
- if (type === 'decision') episodeManager.linkDecision(activeEpisode.id, id)
1231
- else if (type === 'pattern') episodeManager.linkPattern(activeEpisode.id, id)
1232
- else if (type === 'correction') episodeManager.linkCorrection(activeEpisode.id, id)
1233
- } catch {
1234
- // Episode manager not available
1235
- }
1236
- }
1237
-
1238
- /**
1239
- * Phase 23b: Detect session recall queries ("what have we discussed", "this session", etc.)
1240
- */
1241
- private isSessionRecallQuery(message: string): boolean {
1242
- const lower = message.toLowerCase()
1243
- const SESSION_RECALL_PHRASES = [
1244
- 'what have we discussed',
1245
- 'what did we discuss',
1246
- 'what have we talked about',
1247
- 'what did we talk about',
1248
- 'this session',
1249
- 'current session',
1250
- 'session so far',
1251
- 'what have we done',
1252
- 'what did we do',
1253
- 'session summary',
1254
- 'summarize this session',
1255
- 'recap this session',
1256
- 'what happened this session',
1257
- 'what have we covered'
1258
- ]
1259
- return SESSION_RECALL_PHRASES.some(p => lower.includes(p))
1260
- }
1261
-
1262
- /**
1263
- * C7: Detect complex multi-part questions
1264
- */
1265
- private isComplexQuestion(message: string): boolean {
1266
- // Multiple question marks
1267
- const questionMarks = (message.match(/\?/g) || []).length
1268
- if (questionMarks >= 2) return true
1269
- // Very long question (likely multi-part)
1270
- if (message.length > 200 && message.includes('?')) return true
1271
- // Multiple clauses with "and" or "also"
1272
- if (message.includes(' and ') && message.includes('?') && message.length > 100) return true
1273
- return false
1274
- }
1275
-
1276
- private servicesNotReady(): BrainResponse {
1277
- return {
1278
- action: 'none',
1279
- summary: 'Services not initialized',
1280
- content: 'Claude Brain services are not ready. The server may still be starting up.',
1281
- relevantItems: 0
1282
- }
1283
- }
1284
-
1285
- /**
1286
- * Check if a delete/update message has descriptive content beyond just the command words.
1287
- */
1288
- private hasDescriptiveContent(message: string): boolean {
1289
- const COMMAND_WORDS = [
1290
- 'forget', 'delete', 'remove', 'discard', 'erase', 'undo', 'clear', 'drop',
1291
- 'actually', 'correction', 'update', 'change', 'replace', 'modify', 'revise',
1292
- 'amend', 'override', 'scratch', 'no', 'wait', 'instead',
1293
- 'that', 'this', 'the', 'it', 'about', 'memory', 'decision', 'to', 'a', 'an'
1294
- ]
1295
- const words = message.toLowerCase().split(/[\s,;:.!?]+/).filter(w => w.length > 1)
1296
- const meaningfulWords = words.filter(w => !COMMAND_WORDS.includes(w))
1297
- return meaningfulWords.length >= 2
1298
- }
1299
-
1300
- private generateTaskId(title: string): string {
1301
- return title
1302
- .toLowerCase()
1303
- .replace(/[^a-z0-9]+/g, '-')
1304
- .replace(/^-+|-+$/g, '')
1305
- .slice(0, 50)
1306
- }
1307
- }
1308
-
1309
- // Lazy singleton
1310
- let routerInstance: BrainRouter | null = null
1311
-
1312
- export function getBrainRouter(logger: Logger): BrainRouter {
1313
- if (!routerInstance) {
1314
- routerInstance = new BrainRouter(logger)
1315
- }
1316
- return routerInstance
1317
- }
1318
-
1319
- /** Reset singleton for testing */
1320
- export function _resetBrainRouterForTesting(): void {
1321
- routerInstance = null
1322
- }
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 { BrainEntityExtractor, type BrainExtractedEntities } from './entity-extractor'
16
+ import { ResponseFilter, type BrainResponse, type TierResults } from './response-filter'
17
+ import { SearchEngine } from './search-engine'
18
+ import type { NormalizedResult } from './types'
19
+ import {
20
+ getMemoryService,
21
+ getVaultService,
22
+ getContextService,
23
+ getPhase12Service,
24
+ getKnowledgeGraphService,
25
+ getEpisodeService,
26
+ getSessionTracker,
27
+ isServicesInitialized
28
+ } from '@/server/services'
29
+ import { timed } from '@/utils/timing'
30
+
31
+ /** Default project when none can be detected */
32
+ const DEFAULT_PROJECT = 'general'
33
+
34
+ /** Phase 19 D4: Minimum similarity for destructive operations */
35
+ const DELETE_MIN_SIMILARITY = 0.5
36
+ const UPDATE_MIN_SIMILARITY = 0.5
37
+
38
+ export interface BrainInput {
39
+ message: string
40
+ project?: string
41
+ }
42
+
43
+ /** Recently stored decision for recency fast path (Bug 4 fix) */
44
+ interface RecentDecision {
45
+ content: string
46
+ project: string
47
+ storedAt: number
48
+ }
49
+
50
+ /** How long recent decisions stay eligible for fast-path recall (ms) */
51
+ const RECENT_DECISION_WINDOW_MS = 60_000
52
+
53
+ export class BrainRouter {
54
+ private classifier: IntentClassifier
55
+ private entityExtractor: BrainEntityExtractor
56
+ private responseFilter: ResponseFilter
57
+ private searchEngine: SearchEngine
58
+ private logger: Logger
59
+
60
+ /** Track the most recently stored decision ID for update/delete operations */
61
+ private lastStoredId: string | null = null
62
+ private lastStoredProject: string | null = null
63
+
64
+ /** Bug 4: Recently stored decisions for recency fast path */
65
+ private recentDecisions: RecentDecision[] = []
66
+
67
+ constructor(logger: Logger) {
68
+ this.classifier = new IntentClassifier()
69
+ this.entityExtractor = new BrainEntityExtractor()
70
+ this.responseFilter = new ResponseFilter()
71
+ this.searchEngine = new SearchEngine(logger)
72
+ this.logger = logger.child({ component: 'brain-router' })
73
+ }
74
+
75
+ async route(input: BrainInput): Promise<BrainResponse> {
76
+ const { message, project: inputProject } = input
77
+
78
+ // Classify intent
79
+ const classification = this.classifier.classify(message)
80
+ this.logger.debug({ intent: classification.primary, confidence: classification.confidence }, 'Intent classified')
81
+
82
+ // Extract entities
83
+ const entities = await this.entityExtractor.extract(message, inputProject)
84
+ const project = entities.project || inputProject
85
+ this.logger.debug({ project, technologies: entities.technologies }, 'Entities extracted')
86
+
87
+ // Route to handler
88
+ try {
89
+ switch (classification.primary) {
90
+ case 'no_action':
91
+ return this.handleNoAction(message)
92
+
93
+ case 'session_start':
94
+ return this.handleSessionStart(message, project, entities)
95
+
96
+ case 'context_needed':
97
+ return this.handleContextNeeded(message, project, entities, classification)
98
+
99
+ case 'decision_made':
100
+ return this.handleDecisionMade(message, project, entities)
101
+
102
+ case 'store_this':
103
+ return this.handleStoreThis(message, project, entities)
104
+
105
+ case 'pattern_found':
106
+ return this.handlePatternFound(message, project, entities)
107
+
108
+ case 'mistake_learned':
109
+ return this.handleMistakeLearned(message, project, entities)
110
+
111
+ case 'progress_update':
112
+ return this.handleProgressUpdate(message, project, entities)
113
+
114
+ case 'question':
115
+ return this.handleQuestion(message, project, entities, classification)
116
+
117
+ case 'comparison':
118
+ return this.handleComparison(message, project, entities)
119
+
120
+ case 'exploration':
121
+ return this.handleExploration(message, project, entities)
122
+
123
+ case 'list_all':
124
+ return this.handleListAll(message, project, entities)
125
+
126
+ case 'update_memory':
127
+ return this.handleUpdateMemory(message, project, entities)
128
+
129
+ case 'delete_memory':
130
+ return this.handleDeleteMemory(message, project, entities)
131
+
132
+ default:
133
+ return this.handleContextNeeded(message, project, entities, classification)
134
+ }
135
+ } catch (error) {
136
+ this.logger.error({ error, intent: classification.primary }, 'Router handler error')
137
+ return {
138
+ action: 'none',
139
+ summary: `Error processing request`,
140
+ content: `Failed to process: ${error instanceof Error ? error.message : 'Unknown error'}`,
141
+ relevantItems: 0
142
+ }
143
+ }
144
+ }
145
+
146
+ // ===== Intent Handlers =====
147
+
148
+ private handleNoAction(_message: string): BrainResponse {
149
+ return {
150
+ action: 'none',
151
+ summary: 'No action needed',
152
+ content: '',
153
+ relevantItems: 0
154
+ }
155
+ }
156
+
157
+ private async handleSessionStart(
158
+ message: string,
159
+ project: string | undefined,
160
+ entities: BrainExtractedEntities
161
+ ): Promise<BrainResponse> {
162
+ if (!project) {
163
+ return {
164
+ action: 'none',
165
+ summary: 'No project detected',
166
+ content: 'Could not determine project. Please specify which project you are working on.',
167
+ relevantItems: 0
168
+ }
169
+ }
170
+
171
+ if (!isServicesInitialized()) {
172
+ return this.servicesNotReady()
173
+ }
174
+
175
+ const contextService = getContextService()
176
+ const phase12 = getPhase12Service()
177
+
178
+ // Get project context
179
+ const context = await contextService.getContext(project, {
180
+ includeMemories: false,
181
+ includeProgress: true,
182
+ includeStandards: true,
183
+ maxTokens: 6000,
184
+ relevanceThreshold: 0.5
185
+ })
186
+
187
+ const formattedContext = contextService.formatter.format(context)
188
+
189
+ // Process with Phase 12 for proactive recall
190
+ const phase12Result = await phase12.processMessage(
191
+ entities.topic || message,
192
+ project
193
+ )
194
+
195
+ // Build response
196
+ const parts: string[] = [formattedContext]
197
+
198
+ if (phase12Result.recalledMemories?.memories.length) {
199
+ parts.push('\n---\n## Relevant Past Decisions\n')
200
+ for (const mem of phase12Result.recalledMemories.memories) {
201
+ const similarity = Math.round((mem.similarity || 0) * 100)
202
+ const decisionText = mem.decision?.decision || mem.memory?.content || ''
203
+ const safeText = typeof decisionText === 'string' ? decisionText : JSON.stringify(decisionText)
204
+ parts.push(`**[${similarity}%]** ${safeText.slice(0, 200)}`)
205
+ if (mem.decision?.reasoning) {
206
+ const reasoning = typeof mem.decision.reasoning === 'string' ? mem.decision.reasoning : JSON.stringify(mem.decision.reasoning)
207
+ parts.push(` _${reasoning}_`)
208
+ }
209
+ }
210
+ }
211
+
212
+ // If Phase 12 found nothing, do a direct search fallback via SearchEngine
213
+ if (!phase12Result.recalledMemories?.memories.length) {
214
+ try {
215
+ const directResults = await this.searchEngine.enhancedSearch(entities.topic || message, {
216
+ project,
217
+ limit: 5,
218
+ minSimilarity: 0.3
219
+ })
220
+ if (directResults.length > 0) {
221
+ parts.push('\n---\n## Related Memories\n')
222
+ for (const r of directResults) {
223
+ const similarity = Math.round((r.score || 0) * 100)
224
+ const safeContent = typeof r.content === 'string' ? r.content : JSON.stringify(r.content ?? '')
225
+ parts.push(`**[${similarity}%]** ${safeContent.slice(0, 200)}`)
226
+ }
227
+ }
228
+ } catch {
229
+ // Direct search failed, continue without
230
+ }
231
+ }
232
+
233
+ // C8: Register with episode manager
234
+ this.registerEpisodeMessage(message, project, 'session_start')
235
+
236
+ // C9: Append proactive recommendations
237
+ try {
238
+ const recommendations = await this.searchEngine.getRecommendations(
239
+ entities.topic || message,
240
+ { project, limit: 3 }
241
+ )
242
+ if (recommendations?.recommendations?.length) {
243
+ parts.push('\n---\n## Proactive Recommendations\n')
244
+ for (const rec of recommendations.recommendations) {
245
+ parts.push(`- **${rec.type}**: ${rec.content?.slice(0, 150) || ''}`)
246
+ if (rec.reasoning) parts.push(` _${rec.reasoning}_`)
247
+ }
248
+ }
249
+ } catch {
250
+ // Recommendations not available
251
+ }
252
+
253
+ // Phase 25: Query past session summaries for this project
254
+ try {
255
+ let sessionSummaries = await this.searchEngine.plainSearch(
256
+ 'session summary',
257
+ { project: project || undefined, limit: 5, minSimilarity: 0.3 }
258
+ )
259
+
260
+ // Fallback: if plainSearch returns 0, try enhancedSearch with broader query
261
+ if (sessionSummaries.length === 0) {
262
+ sessionSummaries = await this.searchEngine.enhancedSearch(
263
+ `session summary ${project || ''}`.trim(),
264
+ { project: project || undefined, limit: 5, minSimilarity: 0.2 }
265
+ )
266
+ }
267
+
268
+ let summaryItems = sessionSummaries.filter(r =>
269
+ (r.metadata as Record<string, unknown>)?.tags &&
270
+ Array.isArray((r.metadata as Record<string, unknown>).tags) &&
271
+ ((r.metadata as Record<string, unknown>).tags as string[]).includes('session-summary') ||
272
+ r.content.toLowerCase().startsWith('session summary:') ||
273
+ r.content.toLowerCase().includes('worked on')
274
+ )
275
+
276
+ // Sort by date descending (most recent first)
277
+ summaryItems.sort((a, b) => {
278
+ const dateA = a.date || (a.metadata as Record<string, unknown>)?.created_at as string || ''
279
+ const dateB = b.date || (b.metadata as Record<string, unknown>)?.created_at as string || ''
280
+ return dateB.localeCompare(dateA)
281
+ })
282
+
283
+ if (summaryItems.length > 0) {
284
+ parts.push('\n---\n## Past Sessions\n')
285
+ for (const s of summaryItems.slice(0, 3)) {
286
+ const safeContent = typeof s.content === 'string' ? s.content : JSON.stringify(s.content ?? '')
287
+ parts.push(`- ${safeContent.slice(0, 300)}`)
288
+ }
289
+ }
290
+ } catch {
291
+ // Non-critical — skip if search fails
292
+ }
293
+
294
+ const totalRecalled = phase12Result.recalledMemories?.memories.length || 0
295
+ return {
296
+ action: 'retrieved',
297
+ summary: `Session context for ${project}${totalRecalled ? ` (${totalRecalled} memories)` : ''}`,
298
+ content: parts.join('\n'),
299
+ relevantItems: totalRecalled
300
+ }
301
+ }
302
+
303
+ private async handleContextNeeded(
304
+ message: string,
305
+ project: string | undefined,
306
+ entities: BrainExtractedEntities,
307
+ classification?: ClassificationResult
308
+ ): Promise<BrainResponse> {
309
+ if (!isServicesInitialized()) {
310
+ return this.servicesNotReady()
311
+ }
312
+
313
+ // Phase 25: Use undefined for search when no project detected (search all projects)
314
+ const searchProject = project || undefined
315
+ const displayProject = project || DEFAULT_PROJECT
316
+ const query = entities.topic || message
317
+ const tiers: TierResults[] = []
318
+ const hasTemporal = classification?.secondary.includes('exploration') ||
319
+ this.classifier.hasTemporalSignal(message.toLowerCase())
320
+
321
+ // Use temporal search if temporal signals detected
322
+ if (hasTemporal) {
323
+ const { results } = await this.searchEngine.temporalSearch(query, { project: searchProject, limit: 5 })
324
+ if (results.length > 0) {
325
+ tiers.push({
326
+ label: 'Memories',
327
+ results: results.map(r => ({
328
+ content: r.content,
329
+ score: r.score,
330
+ source: r.source === 'decision' ? 'Past Decision' : r.source,
331
+ metadata: r.metadata as Record<string, unknown>
332
+ }))
333
+ })
334
+ }
335
+ } else {
336
+ // Standard enhanced search
337
+ const searchResults = await this.searchEngine.enhancedSearch(query, {
338
+ project: searchProject,
339
+ limit: 5,
340
+ minSimilarity: 0.3
341
+ })
342
+ if (searchResults.length > 0) {
343
+ tiers.push({
344
+ label: 'Memories',
345
+ results: searchResults.map(r => ({
346
+ content: r.content,
347
+ score: r.score,
348
+ source: r.source === 'decision' ? 'Past Decision' : r.source,
349
+ metadata: r.metadata as Record<string, unknown>
350
+ }))
351
+ })
352
+ }
353
+ }
354
+
355
+ // Also search patterns
356
+ const patternResults = await this.searchEngine.searchPatterns(query, { project: searchProject, limit: 3 })
357
+ if (patternResults.length > 0) {
358
+ tiers.push({
359
+ label: 'Patterns',
360
+ results: patternResults.map(p => ({
361
+ content: p.content,
362
+ score: p.score,
363
+ source: `Pattern`,
364
+ metadata: p.metadata as Record<string, unknown>
365
+ }))
366
+ })
367
+ }
368
+
369
+ return this.responseFilter.synthesize(tiers, message, displayProject)
370
+ }
371
+
372
+ /**
373
+ * Handle explicit "store this" requests — always uses the FULL message as the decision text.
374
+ */
375
+ private async handleStoreThis(
376
+ message: string,
377
+ project: string | undefined,
378
+ entities: BrainExtractedEntities
379
+ ): Promise<BrainResponse> {
380
+ const effectiveProject = project || DEFAULT_PROJECT
381
+
382
+ if (!isServicesInitialized()) {
383
+ return this.servicesNotReady()
384
+ }
385
+
386
+ const memory = getMemoryService()
387
+ const decision = message
388
+ const reasoning = entities.reasoning || ''
389
+ const context = entities.topic || message.slice(0, 200)
390
+
391
+ const decisionId = await timed('brain:store', () => memory.rememberDecision(
392
+ effectiveProject,
393
+ context,
394
+ decision,
395
+ reasoning,
396
+ {
397
+ alternatives: entities.alternatives,
398
+ tags: entities.technologies.length > 0 ? entities.technologies : ['user-stored']
399
+ }
400
+ ))
401
+
402
+ // Track for potential update/delete
403
+ this.lastStoredId = decisionId
404
+ this.lastStoredProject = effectiveProject
405
+
406
+ // Bug 4: Track for recency fast path
407
+ this.trackRecentDecision(decision, effectiveProject)
408
+
409
+ // Invalidate cache for this project
410
+ this.searchEngine.invalidateCache(effectiveProject)
411
+
412
+ // Background: vault write + episode linking (non-critical)
413
+ setImmediate(() => {
414
+ this.writeToVault(effectiveProject, decision, reasoning, context, entities.alternatives)
415
+ this.linkToActiveEpisode(effectiveProject, decisionId, 'decision')
416
+ })
417
+
418
+ return {
419
+ action: 'stored',
420
+ summary: `Stored: ${message.slice(0, 60)}`,
421
+ content: `Memory stored (ID: ${decisionId})\n\n**Project:** ${effectiveProject}\n**Content:** ${decision}${reasoning ? `\n**Reasoning:** ${reasoning}` : ''}`,
422
+ relevantItems: 1
423
+ }
424
+ }
425
+
426
+ private async handleDecisionMade(
427
+ message: string,
428
+ project: string | undefined,
429
+ entities: BrainExtractedEntities
430
+ ): Promise<BrainResponse> {
431
+ const effectiveProject = project || DEFAULT_PROJECT
432
+
433
+ if (!isServicesInitialized()) {
434
+ return this.servicesNotReady()
435
+ }
436
+
437
+ const memory = getMemoryService()
438
+ const decision = message
439
+ const reasoning = entities.reasoning || ''
440
+ const context = entities.topic || message.slice(0, 200)
441
+ const alternatives = entities.alternatives
442
+
443
+ const decisionId = await timed('brain:store', () => memory.rememberDecision(
444
+ effectiveProject,
445
+ context,
446
+ decision,
447
+ reasoning,
448
+ {
449
+ alternatives,
450
+ tags: entities.technologies.length > 0 ? entities.technologies : ['auto-detected']
451
+ }
452
+ ))
453
+
454
+ this.lastStoredId = decisionId
455
+ this.lastStoredProject = effectiveProject
456
+
457
+ // Bug 4: Track for recency fast path
458
+ this.trackRecentDecision(decision, effectiveProject)
459
+
460
+ // Invalidate cache
461
+ this.searchEngine.invalidateCache(effectiveProject)
462
+
463
+ // Background: vault write + episode linking (non-critical)
464
+ setImmediate(() => {
465
+ this.writeToVault(effectiveProject, decision, reasoning, context, alternatives)
466
+ this.linkToActiveEpisode(effectiveProject, decisionId, 'decision')
467
+ })
468
+
469
+ return {
470
+ action: 'stored',
471
+ summary: `Stored decision: ${message.slice(0, 60)}`,
472
+ content: `Decision stored (ID: ${decisionId})\n\n**Project:** ${effectiveProject}\n**Decision:** ${decision}\n**Reasoning:** ${reasoning}`,
473
+ relevantItems: 1
474
+ }
475
+ }
476
+
477
+ private async handlePatternFound(
478
+ message: string,
479
+ project: string | undefined,
480
+ entities: BrainExtractedEntities
481
+ ): Promise<BrainResponse> {
482
+ const effectiveProject = project || DEFAULT_PROJECT
483
+
484
+ if (!isServicesInitialized()) {
485
+ return this.servicesNotReady()
486
+ }
487
+
488
+ const memory = getMemoryService()
489
+ const patternType = entities.patternType || 'solution'
490
+ const description = message
491
+
492
+ const patternId = await memory.storePattern({
493
+ project: effectiveProject,
494
+ pattern_type: patternType,
495
+ description,
496
+ confidence: 0.8,
497
+ context: message.slice(0, 300)
498
+ })
499
+
500
+ // Invalidate cache
501
+ this.searchEngine.invalidateCache(effectiveProject)
502
+
503
+ // Link to active episode
504
+ this.linkToActiveEpisode(effectiveProject, patternId, 'pattern')
505
+
506
+ return {
507
+ action: 'stored',
508
+ summary: `Stored ${patternType}: ${description.slice(0, 60)}`,
509
+ content: `Pattern stored (ID: ${patternId})\n\n**Type:** ${patternType}\n**Project:** ${effectiveProject}\n**Description:** ${description}`,
510
+ relevantItems: 1
511
+ }
512
+ }
513
+
514
+ private async handleMistakeLearned(
515
+ message: string,
516
+ project: string | undefined,
517
+ entities: BrainExtractedEntities
518
+ ): Promise<BrainResponse> {
519
+ const effectiveProject = project || DEFAULT_PROJECT
520
+
521
+ if (!isServicesInitialized()) {
522
+ return this.servicesNotReady()
523
+ }
524
+
525
+ const memory = getMemoryService()
526
+ const original = entities.original || message
527
+ const correction = entities.correction || ''
528
+ const reasoning = entities.reasoning || 'Lesson learned from experience'
529
+
530
+ const correctionId = await memory.storeCorrection({
531
+ project: effectiveProject,
532
+ original,
533
+ correction: correction || message,
534
+ reasoning,
535
+ context: entities.topic || '',
536
+ confidence: 0.9
537
+ })
538
+
539
+ // Invalidate cache
540
+ this.searchEngine.invalidateCache(effectiveProject)
541
+
542
+ // Link to active episode
543
+ this.linkToActiveEpisode(effectiveProject, correctionId, 'correction')
544
+
545
+ return {
546
+ action: 'stored',
547
+ summary: `Stored correction: ${original.slice(0, 60)}`,
548
+ content: `Correction stored (ID: ${correctionId})\n\n**Project:** ${effectiveProject}\n**Original:** ${original}\n**Correction:** ${correction || '(see original message)'}`,
549
+ relevantItems: 1
550
+ }
551
+ }
552
+
553
+ private async handleProgressUpdate(
554
+ message: string,
555
+ project: string | undefined,
556
+ entities: BrainExtractedEntities
557
+ ): Promise<BrainResponse> {
558
+ const effectiveProject = project || DEFAULT_PROJECT
559
+
560
+ if (!isServicesInitialized()) {
561
+ return this.servicesNotReady()
562
+ }
563
+
564
+ const contextService = getContextService()
565
+ const memory = getMemoryService()
566
+
567
+ const completedTask = entities.completedTask || message
568
+ const nextSteps = entities.nextSteps || 'Continue development'
569
+
570
+ // Store in session context
571
+ try {
572
+ await contextService.progress.addCompletedTask(effectiveProject, {
573
+ id: this.generateTaskId(completedTask),
574
+ title: completedTask,
575
+ status: 'done',
576
+ completedAt: new Date()
577
+ })
578
+ } catch {
579
+ // Progress update failed
580
+ }
581
+
582
+ // Also store as a searchable memory
583
+ try {
584
+ await memory.rememberDecision(
585
+ effectiveProject,
586
+ `Progress update`,
587
+ `Progress: ${completedTask}${nextSteps !== 'Continue development' ? `. Next: ${nextSteps}` : ''}`,
588
+ 'Progress tracking',
589
+ { tags: ['progress', ...entities.technologies] }
590
+ )
591
+
592
+ // Invalidate cache
593
+ this.searchEngine.invalidateCache(effectiveProject)
594
+ } catch {
595
+ // Memory storage failed
596
+ }
597
+
598
+ // Register with episode
599
+ this.registerEpisodeMessage(message, effectiveProject, 'progress_update')
600
+
601
+ return {
602
+ action: 'stored',
603
+ summary: `Progress: ${completedTask.slice(0, 60)}`,
604
+ content: `Progress updated for ${effectiveProject}\n\n**Completed:** ${completedTask}\n**Next:** ${nextSteps}`,
605
+ relevantItems: 1
606
+ }
607
+ }
608
+
609
+ /**
610
+ * List all decisions for a project
611
+ */
612
+ private async handleListAll(
613
+ message: string,
614
+ project: string | undefined,
615
+ entities: BrainExtractedEntities
616
+ ): Promise<BrainResponse> {
617
+ if (!isServicesInitialized()) {
618
+ return this.servicesNotReady()
619
+ }
620
+
621
+ const memory = getMemoryService()
622
+ const effectiveProject = project
623
+
624
+ try {
625
+ const decisions = await memory.fetchAllDecisions(effectiveProject)
626
+ const patterns = await memory.fetchAllPatterns(effectiveProject)
627
+ const corrections = await memory.fetchAllCorrections(effectiveProject)
628
+
629
+ const parts: string[] = []
630
+ const projectLabel = effectiveProject || 'all projects'
631
+
632
+ if (decisions.length > 0) {
633
+ parts.push(`## Decisions (${decisions.length})`)
634
+ for (const d of decisions.slice(0, 20)) {
635
+ const rawDecision = d.decision || d.document || d.content || ''
636
+ const decision = typeof rawDecision === 'string' ? rawDecision : JSON.stringify(rawDecision)
637
+ const rawDate = d.created_at || d.date || ''
638
+ const date = rawDate ? ` (${String(rawDate).split('T')[0]})` : ''
639
+ parts.push(`- ${decision.slice(0, 150)}${date}`)
640
+ }
641
+ if (decisions.length > 20) {
642
+ parts.push(`_...and ${decisions.length - 20} more_`)
643
+ }
644
+ parts.push('')
645
+ }
646
+
647
+ if (patterns.length > 0) {
648
+ parts.push(`## Patterns (${patterns.length})`)
649
+ for (const p of patterns.slice(0, 10)) {
650
+ const rawDesc = p.description || p.document || p.content || ''
651
+ const desc = typeof rawDesc === 'string' ? rawDesc : JSON.stringify(rawDesc)
652
+ parts.push(`- **${p.pattern_type || 'solution'}**: ${desc.slice(0, 120)}`)
653
+ }
654
+ parts.push('')
655
+ }
656
+
657
+ if (corrections.length > 0) {
658
+ parts.push(`## Corrections (${corrections.length})`)
659
+ for (const c of corrections.slice(0, 10)) {
660
+ const rawOrig = c.original || c.document || c.content || ''
661
+ const orig = typeof rawOrig === 'string' ? rawOrig : JSON.stringify(rawOrig)
662
+ parts.push(`- ${orig.slice(0, 120)}`)
663
+ }
664
+ parts.push('')
665
+ }
666
+
667
+ const total = decisions.length + patterns.length + corrections.length
668
+
669
+ if (total === 0) {
670
+ return {
671
+ action: 'none',
672
+ summary: `No memories found for ${projectLabel}`,
673
+ content: `No decisions, patterns, or corrections stored yet for ${projectLabel}.`,
674
+ relevantItems: 0
675
+ }
676
+ }
677
+
678
+ return {
679
+ action: 'retrieved',
680
+ summary: `${total} memories for ${projectLabel}`,
681
+ content: parts.join('\n'),
682
+ relevantItems: total
683
+ }
684
+ } catch (error) {
685
+ return {
686
+ action: 'none',
687
+ summary: 'Error listing memories',
688
+ content: `Failed to list memories: ${error instanceof Error ? error.message : 'Unknown error'}`,
689
+ relevantItems: 0
690
+ }
691
+ }
692
+ }
693
+
694
+ /**
695
+ * Phase 19 D4: Update with higher similarity threshold
696
+ */
697
+ private async handleUpdateMemory(
698
+ message: string,
699
+ project: string | undefined,
700
+ entities: BrainExtractedEntities
701
+ ): Promise<BrainResponse> {
702
+ const effectiveProject = project || this.lastStoredProject || DEFAULT_PROJECT
703
+
704
+ if (!isServicesInitialized()) {
705
+ return this.servicesNotReady()
706
+ }
707
+
708
+ const memory = getMemoryService()
709
+ const topic = entities.topic || message
710
+ const hasSpecificContent = this.hasDescriptiveContent(message)
711
+
712
+ if (hasSpecificContent) {
713
+ try {
714
+ const results = await memory.searchRaw(topic, {
715
+ project: effectiveProject,
716
+ limit: 1,
717
+ minSimilarity: UPDATE_MIN_SIMILARITY
718
+ })
719
+
720
+ const matchId = results[0]?.memory?.id || results[0]?.decision?.id || results[0]?.id
721
+ const matchSimilarity = results[0]?.similarity || 0
722
+
723
+ if (results.length > 0 && matchId && matchSimilarity >= UPDATE_MIN_SIMILARITY) {
724
+ const oldId = matchId
725
+ const newId = await memory.updateDecision(
726
+ oldId,
727
+ effectiveProject,
728
+ `Updated: ${topic.slice(0, 200)}`,
729
+ message,
730
+ entities.reasoning || 'Updated via brain tool',
731
+ { tags: entities.technologies.length > 0 ? entities.technologies : ['updated'] }
732
+ )
733
+
734
+ this.lastStoredId = newId
735
+ this.lastStoredProject = effectiveProject
736
+
737
+ // Invalidate cache
738
+ this.searchEngine.invalidateCache(effectiveProject)
739
+
740
+ const oldContent = results[0].decision?.decision || results[0].memory?.content?.slice(0, 100) || results[0].content?.slice(0, 100) || ''
741
+ return {
742
+ action: 'stored',
743
+ summary: `Updated decision`,
744
+ content: `Replaced: "${oldContent.slice(0, 80)}"\n\nWith:\n**New content:** ${message}`,
745
+ relevantItems: 1
746
+ }
747
+ } else if (results.length > 0 && matchSimilarity < UPDATE_MIN_SIMILARITY) {
748
+ // D4: No confident match — store as new instead of updating wrong memory
749
+ return this.storeAsNew(memory, effectiveProject, message, topic, entities, 'No confident match to update (similarity too low)')
750
+ }
751
+ } catch {
752
+ // Search failed, fall through
753
+ }
754
+ }
755
+
756
+ // Generic update — use lastStoredId
757
+ if (this.lastStoredId) {
758
+ const newId = await memory.updateDecision(
759
+ this.lastStoredId,
760
+ effectiveProject,
761
+ `Updated: ${topic.slice(0, 200)}`,
762
+ message,
763
+ entities.reasoning || 'Updated via brain tool',
764
+ { tags: entities.technologies.length > 0 ? entities.technologies : ['updated'] }
765
+ )
766
+
767
+ this.lastStoredId = newId
768
+ this.lastStoredProject = effectiveProject
769
+ this.searchEngine.invalidateCache(effectiveProject)
770
+
771
+ return {
772
+ action: 'stored',
773
+ summary: `Updated decision`,
774
+ content: `Previous decision replaced with:\n\n**Project:** ${effectiveProject}\n**New content:** ${message}`,
775
+ relevantItems: 1
776
+ }
777
+ }
778
+
779
+ // Fallback: store as new decision
780
+ return this.storeAsNew(memory, effectiveProject, message, topic, entities, 'No matching decision found to update')
781
+ }
782
+
783
+ /**
784
+ * Phase 19 D4: Delete with higher similarity threshold
785
+ */
786
+ private async handleDeleteMemory(
787
+ message: string,
788
+ project: string | undefined,
789
+ entities: BrainExtractedEntities
790
+ ): Promise<BrainResponse> {
791
+ const effectiveProject = project || this.lastStoredProject || DEFAULT_PROJECT
792
+
793
+ if (!isServicesInitialized()) {
794
+ return this.servicesNotReady()
795
+ }
796
+
797
+ const memory = getMemoryService()
798
+ const topic = entities.topic || message
799
+ const hasSpecificContent = this.hasDescriptiveContent(message)
800
+
801
+ if (hasSpecificContent) {
802
+ try {
803
+ const results = await memory.searchRaw(topic, {
804
+ project: effectiveProject,
805
+ limit: 3,
806
+ minSimilarity: DELETE_MIN_SIMILARITY
807
+ })
808
+
809
+ const matchId = results[0]?.memory?.id || results[0]?.decision?.id || results[0]?.id
810
+ const matchSimilarity = results[0]?.similarity || 0
811
+
812
+ if (results.length > 0 && matchId && matchSimilarity >= DELETE_MIN_SIMILARITY) {
813
+ const targetId = matchId
814
+ const content = results[0].decision?.decision || results[0].memory?.content?.slice(0, 100) || results[0].content?.slice(0, 100) || ''
815
+
816
+ await memory.deleteDecision(targetId)
817
+ if (this.lastStoredId === targetId) this.lastStoredId = null
818
+
819
+ // Invalidate cache
820
+ this.searchEngine.invalidateCache(effectiveProject)
821
+
822
+ return {
823
+ action: 'stored',
824
+ summary: `Deleted memory`,
825
+ content: `Deleted: "${content.slice(0, 100)}" (ID: ${targetId})`,
826
+ relevantItems: 0
827
+ }
828
+ } else if (results.length > 0 && matchSimilarity < DELETE_MIN_SIMILARITY) {
829
+ // D4: No confident match
830
+ return {
831
+ action: 'none',
832
+ summary: 'No confident match to delete',
833
+ 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.`,
834
+ relevantItems: 0
835
+ }
836
+ }
837
+ } catch {
838
+ // Search failed, fall through
839
+ }
840
+ }
841
+
842
+ // Generic request use lastStoredId
843
+ if (this.lastStoredId) {
844
+ try {
845
+ await memory.deleteDecision(this.lastStoredId)
846
+ const deletedId = this.lastStoredId
847
+ this.lastStoredId = null
848
+ this.searchEngine.invalidateCache(effectiveProject)
849
+ return {
850
+ action: 'stored',
851
+ summary: `Deleted most recent memory`,
852
+ content: `Deleted memory (ID: ${deletedId})`,
853
+ relevantItems: 0
854
+ }
855
+ } catch {
856
+ // Deletion failed
857
+ }
858
+ }
859
+
860
+ return {
861
+ action: 'none',
862
+ summary: 'No matching memory found to delete',
863
+ content: 'Could not find a memory matching your request. Try being more specific about what to delete.',
864
+ relevantItems: 0
865
+ }
866
+ }
867
+
868
+ /**
869
+ * Phase 19 C7+C10+C11: Enhanced question handler
870
+ * - Uses SearchEngine for all searches (hybrid, cached)
871
+ * - Temporal search when temporal signals detected
872
+ * - ChainRetrieval for complex multi-part questions
873
+ * - CrossProject patterns for general/unspecified project
874
+ * - KnowledgeGraph enrichment
875
+ */
876
+ private async handleQuestion(
877
+ message: string,
878
+ project: string | undefined,
879
+ entities: BrainExtractedEntities,
880
+ classification: ClassificationResult
881
+ ): Promise<BrainResponse> {
882
+ if (!isServicesInitialized()) {
883
+ return this.servicesNotReady()
884
+ }
885
+
886
+ // Phase 25: Use undefined for search (no project filter = search all) when no project detected
887
+ const searchProject = project || undefined
888
+ const displayProject = project || DEFAULT_PROJECT
889
+ const query = entities.topic || message
890
+ const tiers: TierResults[] = []
891
+ const hasTemporal = classification.secondary.includes('exploration') ||
892
+ this.classifier.hasTemporalSignal(message.toLowerCase())
893
+
894
+ // C7: Detect multi-part questions for chain retrieval
895
+ const isComplex = this.isComplexQuestion(message)
896
+
897
+ // Main search temporal or standard
898
+ let searchResults: NormalizedResult[]
899
+ if (hasTemporal) {
900
+ const { results } = await this.searchEngine.temporalSearch(query, { project: searchProject, limit: 5 })
901
+ searchResults = results
902
+ } else if (isComplex) {
903
+ // C7: Try multi-hop chain retrieval
904
+ const chainResult = await this.searchEngine.chainSearch(query, { project: searchProject })
905
+ if (chainResult?.allResults?.length) {
906
+ searchResults = chainResult.allResults.map((r: any) => ({
907
+ id: r.id || '',
908
+ content: r.content || '',
909
+ score: r.similarity || 0,
910
+ source: 'decision' as const,
911
+ project: r.metadata?.project || displayProject || '',
912
+ date: r.metadata?.created_at || '',
913
+ metadata: r.metadata || {}
914
+ }))
915
+ } else {
916
+ searchResults = await this.searchEngine.enhancedSearch(query, { project: searchProject, limit: 5 })
917
+ }
918
+ } else {
919
+ searchResults = await this.searchEngine.enhancedSearch(query, { project: searchProject, limit: 5 })
920
+ }
921
+
922
+ if (searchResults.length > 0) {
923
+ tiers.push({
924
+ label: 'Memories',
925
+ results: searchResults.map(r => ({
926
+ content: r.content,
927
+ score: r.score,
928
+ source: r.source === 'decision' ? 'Past Decision' : r.source,
929
+ metadata: r.metadata as Record<string, unknown>
930
+ }))
931
+ })
932
+ }
933
+
934
+ // Parallel: patterns + corrections + graph
935
+ const [patternResults, correctionResults, graphResults] = await Promise.all([
936
+ this.searchEngine.searchPatterns(query, { project: searchProject, limit: 3 }),
937
+ this.searchEngine.searchCorrections(query, { project: searchProject, limit: 3 }),
938
+ // C11: KnowledgeGraph enrichment for all questions
939
+ this.searchEngine.searchGraph(query, 5)
940
+ ])
941
+
942
+ if (patternResults.length > 0) {
943
+ tiers.push({
944
+ label: 'Patterns',
945
+ results: patternResults.map(p => ({
946
+ content: p.content,
947
+ score: p.score,
948
+ source: `Pattern`,
949
+ metadata: p.metadata as Record<string, unknown>
950
+ }))
951
+ })
952
+ }
953
+
954
+ if (correctionResults.length > 0) {
955
+ tiers.push({
956
+ label: 'Corrections',
957
+ results: correctionResults.map(c => ({
958
+ content: c.content,
959
+ score: c.score,
960
+ source: 'Lesson Learned',
961
+ metadata: c.metadata as Record<string, unknown>
962
+ }))
963
+ })
964
+ }
965
+
966
+ // C11: Add graph results as "Related Concepts" (Bug 3: filter low-score, cap to 0.4)
967
+ const relevantGraphResults = graphResults.filter(g => g.score >= 0.6)
968
+ if (relevantGraphResults.length > 0) {
969
+ tiers.push({
970
+ label: 'Related Concepts',
971
+ results: relevantGraphResults.map(g => ({
972
+ content: g.content,
973
+ score: Math.min(g.score, 0.4),
974
+ source: 'Knowledge Graph',
975
+ metadata: g.metadata as Record<string, unknown>
976
+ }))
977
+ })
978
+ }
979
+
980
+ // C10: CrossProject patterns for general/unspecified project queries (Bug 2: cap to 0.3)
981
+ if (!project) {
982
+ try {
983
+ const crossProject = await this.searchEngine.findCrossProjectPatterns({
984
+ query,
985
+ limit: 3
986
+ })
987
+ if (crossProject?.patterns?.length) {
988
+ tiers.push({
989
+ label: 'Cross-Project Patterns',
990
+ results: crossProject.patterns.map((p: any) => ({
991
+ content: `${p.description || ''} (${p.projects?.join(', ') || 'multiple projects'})`,
992
+ score: Math.min(p.confidence || 0.5, 0.3),
993
+ source: 'Cross-Project',
994
+ metadata: {}
995
+ }))
996
+ })
997
+ }
998
+ } catch {
999
+ // Cross-project not available
1000
+ }
1001
+ }
1002
+
1003
+ // Phase 23b Fix 5: Session recall — "what have we discussed" queries session tracker
1004
+ if (this.isSessionRecallQuery(message)) {
1005
+ try {
1006
+ const tracker = getSessionTracker()
1007
+ if (tracker) {
1008
+ const stats = tracker.getStats()
1009
+ if (stats.totalItems > 0) {
1010
+ tiers.push({
1011
+ label: 'Current Session',
1012
+ results: [{
1013
+ content: `Active sessions: ${stats.activeSessions}, items tracked: ${stats.totalItems}`,
1014
+ score: 0.95,
1015
+ source: 'Session Tracker',
1016
+ metadata: {}
1017
+ }]
1018
+ })
1019
+ }
1020
+ }
1021
+ } catch {
1022
+ // Session tracker not available
1023
+ }
1024
+ }
1025
+
1026
+ // Bug 4: Inject recently stored decisions as high-score tier
1027
+ const recentMatches = this.findRecentDecisions(query, searchProject)
1028
+ if (recentMatches.length > 0) {
1029
+ tiers.unshift({
1030
+ label: 'Recent Decisions',
1031
+ results: recentMatches.map(d => ({
1032
+ content: d.content,
1033
+ score: 0.95,
1034
+ source: 'Recent Decision',
1035
+ metadata: { storedAt: d.storedAt, project: d.project }
1036
+ }))
1037
+ })
1038
+ }
1039
+
1040
+ // Register with episode
1041
+ this.registerEpisodeMessage(message, displayProject, 'question')
1042
+
1043
+ return this.responseFilter.synthesize(tiers, message, displayProject)
1044
+ }
1045
+
1046
+ /**
1047
+ * Phase 19 C6: Enhanced exploration handler
1048
+ * - Timeline for "timeline"/"chronological" queries
1049
+ * - DecisionEvolutionTracker for "how has X changed" queries
1050
+ * - TrendDetector for "trends"/"emerging" queries
1051
+ */
1052
+ private async handleExploration(
1053
+ message: string,
1054
+ project: string | undefined,
1055
+ entities: BrainExtractedEntities
1056
+ ): Promise<BrainResponse> {
1057
+ if (!isServicesInitialized()) {
1058
+ return this.servicesNotReady()
1059
+ }
1060
+
1061
+ // Phase 25: Use undefined for search when no project detected
1062
+ const searchProject = project || undefined
1063
+ const displayProject = project || DEFAULT_PROJECT
1064
+ const query = entities.topic || message
1065
+ const lower = message.toLowerCase()
1066
+ const tiers: TierResults[] = []
1067
+
1068
+ // C6: Timeline queries
1069
+ if (lower.includes('timeline') || lower.includes('chronological') || lower.includes('history of')) {
1070
+ const timeline = await this.searchEngine.buildTimeline({
1071
+ project: searchProject,
1072
+ topic: query,
1073
+ limit: 20
1074
+ })
1075
+
1076
+ if (timeline?.entries?.length) {
1077
+ tiers.push({
1078
+ label: 'Timeline',
1079
+ results: timeline.entries.map((e: any) => ({
1080
+ content: `**${e.date || ''}** — ${e.content || ''}`,
1081
+ score: 0.8,
1082
+ source: `Timeline (${e.type || 'event'})`,
1083
+ metadata: e.metadata || {}
1084
+ }))
1085
+ })
1086
+ }
1087
+ }
1088
+
1089
+ // C6: Evolution queries
1090
+ if (lower.includes('evolution') || lower.includes('how has') || lower.includes('changed') || lower.includes('evolved')) {
1091
+ const evolution = await this.searchEngine.analyzeEvolution(query, { project: searchProject })
1092
+ if (evolution?.timeline?.length) {
1093
+ const parts = []
1094
+ parts.push(`**Stability:** ${evolution.stability || 'unknown'}`)
1095
+ if (evolution.currentState) parts.push(`**Current:** ${evolution.currentState}`)
1096
+ for (const change of (evolution.changes || []).slice(0, 5)) {
1097
+ parts.push(`- ${change.description || change}`)
1098
+ }
1099
+
1100
+ tiers.push({
1101
+ label: 'Decision Evolution',
1102
+ results: [{
1103
+ content: parts.join('\n'),
1104
+ score: 0.8,
1105
+ source: 'Evolution Analysis',
1106
+ metadata: {}
1107
+ }]
1108
+ })
1109
+ }
1110
+ }
1111
+
1112
+ // C6: Trend queries
1113
+ if (lower.includes('trend') || lower.includes('emerging') || lower.includes('declining')) {
1114
+ const trends = await this.searchEngine.detectTrends({ project: searchProject })
1115
+ if (trends?.topTrends?.length) {
1116
+ tiers.push({
1117
+ label: 'Trends',
1118
+ results: trends.topTrends.slice(0, 5).map((t: any) => ({
1119
+ content: `**${t.term}** ${t.trend} (${t.occurrences} occurrences, momentum: ${t.momentum || 'unknown'})`,
1120
+ score: t.occurrences > 5 ? 0.9 : 0.7,
1121
+ source: `Trend (${t.trend})`,
1122
+ metadata: {}
1123
+ }))
1124
+ })
1125
+ }
1126
+ }
1127
+
1128
+ // Always include graph exploration
1129
+ const graphResults = await this.searchEngine.searchGraph(query, 10)
1130
+ if (graphResults.length > 0) {
1131
+ tiers.push({
1132
+ label: 'Knowledge Graph',
1133
+ results: graphResults.map(g => ({
1134
+ content: g.content,
1135
+ score: g.score,
1136
+ source: 'Knowledge Graph',
1137
+ metadata: g.metadata as Record<string, unknown>
1138
+ }))
1139
+ })
1140
+ }
1141
+
1142
+ // Issue 9: Cross-project patterns for exploration queries
1143
+ if (!project) {
1144
+ try {
1145
+ const crossProject = await this.searchEngine.findCrossProjectPatterns({
1146
+ query,
1147
+ limit: 3
1148
+ })
1149
+ if (crossProject?.patterns?.length) {
1150
+ tiers.push({
1151
+ label: 'Cross-Project Patterns',
1152
+ results: crossProject.patterns.map((p: any) => ({
1153
+ content: `${p.description || ''} (${p.projects?.join(', ') || 'multiple projects'})`,
1154
+ score: p.confidence || 0.5,
1155
+ source: 'Cross-Project',
1156
+ metadata: {}
1157
+ }))
1158
+ })
1159
+ }
1160
+ } catch {
1161
+ // Cross-project not available
1162
+ }
1163
+ }
1164
+
1165
+ // Also do a basic memory search as fallback
1166
+ const searchResults = await this.searchEngine.enhancedSearch(query, {
1167
+ project: searchProject,
1168
+ limit: 5,
1169
+ minSimilarity: 0.2
1170
+ })
1171
+ if (searchResults.length > 0) {
1172
+ tiers.push({
1173
+ label: 'Memories',
1174
+ results: searchResults.map(r => ({
1175
+ content: r.content,
1176
+ score: r.score,
1177
+ source: r.source === 'decision' ? 'Past Decision' : r.source,
1178
+ metadata: r.metadata as Record<string, unknown>
1179
+ }))
1180
+ })
1181
+ }
1182
+
1183
+ return this.responseFilter.synthesize(tiers, message, displayProject, 'analyzed')
1184
+ }
1185
+
1186
+ private async handleComparison(
1187
+ message: string,
1188
+ project: string | undefined,
1189
+ entities: BrainExtractedEntities
1190
+ ): Promise<BrainResponse> {
1191
+ if (!isServicesInitialized()) {
1192
+ return this.servicesNotReady()
1193
+ }
1194
+
1195
+ // Phase 25: Use undefined for search when no project detected
1196
+ const searchProject = project || undefined
1197
+ const displayProject = project || DEFAULT_PROJECT
1198
+ const query = entities.topic || message
1199
+ const tiers: TierResults[] = []
1200
+
1201
+ // Search for related decisions
1202
+ const searchResults = await this.searchEngine.enhancedSearch(query, {
1203
+ project: searchProject,
1204
+ limit: 5,
1205
+ minSimilarity: 0.2
1206
+ })
1207
+ if (searchResults.length > 0) {
1208
+ tiers.push({
1209
+ label: 'Related Decisions',
1210
+ results: searchResults.map(r => ({
1211
+ content: r.content,
1212
+ score: r.score,
1213
+ source: 'Past Decision',
1214
+ metadata: r.metadata as Record<string, unknown>
1215
+ }))
1216
+ })
1217
+ }
1218
+
1219
+ // Graph search for comparison context
1220
+ const graphResults = await this.searchEngine.searchGraph(query, 5)
1221
+ if (graphResults.length > 0) {
1222
+ tiers.push({
1223
+ label: 'Knowledge Graph',
1224
+ results: graphResults.map(g => ({
1225
+ content: g.content,
1226
+ score: g.score,
1227
+ source: 'Knowledge Graph',
1228
+ metadata: g.metadata as Record<string, unknown>
1229
+ }))
1230
+ })
1231
+ }
1232
+
1233
+ return this.responseFilter.synthesize(tiers, message, displayProject, 'analyzed')
1234
+ }
1235
+
1236
+ // ===== Helpers =====
1237
+
1238
+ private async writeToVault(
1239
+ project: string,
1240
+ decision: string,
1241
+ reasoning: string,
1242
+ context: string,
1243
+ alternatives?: string
1244
+ ): Promise<void> {
1245
+ try {
1246
+ const vault = getVaultService()
1247
+ const projectPaths = vault.getProjectPaths(project)
1248
+ const date = new Date().toISOString().split('T')[0]
1249
+ 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`
1250
+ await vault.writer.appendContent(projectPaths.decisions, entry, '\n')
1251
+ } catch {
1252
+ // Vault write failed
1253
+ }
1254
+ }
1255
+
1256
+ /**
1257
+ * Store as a new decision (fallback for update when no match found)
1258
+ */
1259
+ private async storeAsNew(
1260
+ memory: any,
1261
+ project: string,
1262
+ message: string,
1263
+ topic: string,
1264
+ entities: BrainExtractedEntities,
1265
+ reason: string
1266
+ ): Promise<BrainResponse> {
1267
+ const decisionId = await memory.rememberDecision(
1268
+ project,
1269
+ topic.slice(0, 200),
1270
+ message,
1271
+ entities.reasoning || '',
1272
+ { tags: entities.technologies.length > 0 ? entities.technologies : ['updated'] }
1273
+ )
1274
+
1275
+ this.lastStoredId = decisionId
1276
+ this.lastStoredProject = project
1277
+ this.searchEngine.invalidateCache(project)
1278
+
1279
+ return {
1280
+ action: 'stored',
1281
+ summary: `Stored as new (${reason})`,
1282
+ content: `Stored as new decision (ID: ${decisionId})\n\n**Project:** ${project}\n**Content:** ${message}`,
1283
+ relevantItems: 1
1284
+ }
1285
+ }
1286
+
1287
+ /**
1288
+ * C8: Register a message with the episode manager
1289
+ */
1290
+ private registerEpisodeMessage(message: string, project?: string, role: string = 'user'): void {
1291
+ try {
1292
+ const episodeManager = getEpisodeService()
1293
+ if (!episodeManager) return
1294
+ episodeManager.processMessage(
1295
+ { role, content: message, timestamp: new Date().toISOString() },
1296
+ project
1297
+ ).catch(() => {})
1298
+ } catch {
1299
+ // Episode manager not available
1300
+ }
1301
+ }
1302
+
1303
+ /**
1304
+ * C8: Link a stored decision/pattern/correction to the active episode
1305
+ */
1306
+ private linkToActiveEpisode(project: string, id: string, type: 'decision' | 'pattern' | 'correction'): void {
1307
+ try {
1308
+ const episodeManager = getEpisodeService()
1309
+ if (!episodeManager) return
1310
+ const activeEpisode = episodeManager.getActiveEpisode(project)
1311
+ if (!activeEpisode) return
1312
+
1313
+ if (type === 'decision') episodeManager.linkDecision(activeEpisode.id, id)
1314
+ else if (type === 'pattern') episodeManager.linkPattern(activeEpisode.id, id)
1315
+ else if (type === 'correction') episodeManager.linkCorrection(activeEpisode.id, id)
1316
+ } catch {
1317
+ // Episode manager not available
1318
+ }
1319
+ }
1320
+
1321
+ /**
1322
+ * Phase 23b: Detect session recall queries ("what have we discussed", "this session", etc.)
1323
+ */
1324
+ private isSessionRecallQuery(message: string): boolean {
1325
+ const lower = message.toLowerCase()
1326
+ const SESSION_RECALL_PHRASES = [
1327
+ 'what have we discussed',
1328
+ 'what did we discuss',
1329
+ 'what have we talked about',
1330
+ 'what did we talk about',
1331
+ 'this session',
1332
+ 'current session',
1333
+ 'session so far',
1334
+ 'what have we done',
1335
+ 'what did we do',
1336
+ 'session summary',
1337
+ 'summarize this session',
1338
+ 'recap this session',
1339
+ 'what happened this session',
1340
+ 'what have we covered'
1341
+ ]
1342
+ return SESSION_RECALL_PHRASES.some(p => lower.includes(p))
1343
+ }
1344
+
1345
+ /**
1346
+ * C7: Detect complex multi-part questions
1347
+ */
1348
+ private isComplexQuestion(message: string): boolean {
1349
+ // Multiple question marks
1350
+ const questionMarks = (message.match(/\?/g) || []).length
1351
+ if (questionMarks >= 2) return true
1352
+ // Very long question (likely multi-part)
1353
+ if (message.length > 200 && message.includes('?')) return true
1354
+ // Multiple clauses with "and" or "also"
1355
+ if (message.includes(' and ') && message.includes('?') && message.length > 100) return true
1356
+ return false
1357
+ }
1358
+
1359
+ private servicesNotReady(): BrainResponse {
1360
+ return {
1361
+ action: 'none',
1362
+ summary: 'Services not initialized',
1363
+ content: 'Claude Brain services are not ready. The server may still be starting up.',
1364
+ relevantItems: 0
1365
+ }
1366
+ }
1367
+
1368
+ /**
1369
+ * Check if a delete/update message has descriptive content beyond just the command words.
1370
+ */
1371
+ private hasDescriptiveContent(message: string): boolean {
1372
+ const COMMAND_WORDS = [
1373
+ 'forget', 'delete', 'remove', 'discard', 'erase', 'undo', 'clear', 'drop',
1374
+ 'actually', 'correction', 'update', 'change', 'replace', 'modify', 'revise',
1375
+ 'amend', 'override', 'scratch', 'no', 'wait', 'instead',
1376
+ 'that', 'this', 'the', 'it', 'about', 'memory', 'decision', 'to', 'a', 'an'
1377
+ ]
1378
+ const words = message.toLowerCase().split(/[\s,;:.!?]+/).filter(w => w.length > 1)
1379
+ const meaningfulWords = words.filter(w => !COMMAND_WORDS.includes(w))
1380
+ return meaningfulWords.length >= 2
1381
+ }
1382
+
1383
+ /**
1384
+ * Bug 4: Track a recently stored decision for recency fast path
1385
+ */
1386
+ private trackRecentDecision(content: string, project: string): void {
1387
+ const now = Date.now()
1388
+ // Prune expired entries
1389
+ this.recentDecisions = this.recentDecisions.filter(
1390
+ d => now - d.storedAt < RECENT_DECISION_WINDOW_MS
1391
+ )
1392
+ this.recentDecisions.push({ content, project, storedAt: now })
1393
+ }
1394
+
1395
+ /**
1396
+ * Bug 4: Find recent decisions matching a query by keyword overlap
1397
+ */
1398
+ private findRecentDecisions(query: string, project?: string): RecentDecision[] {
1399
+ const now = Date.now()
1400
+ // Prune expired
1401
+ this.recentDecisions = this.recentDecisions.filter(
1402
+ d => now - d.storedAt < RECENT_DECISION_WINDOW_MS
1403
+ )
1404
+
1405
+ const queryWords = new Set(
1406
+ query.toLowerCase().split(/\s+/).filter(w => w.length > 2)
1407
+ )
1408
+ if (queryWords.size === 0) return []
1409
+
1410
+ return this.recentDecisions.filter(d => {
1411
+ // Project filter: skip if searching a specific project and it doesn't match
1412
+ if (project && d.project !== project) return false
1413
+ const contentWords = d.content.toLowerCase().split(/\s+/).filter(w => w.length > 2)
1414
+ const overlap = contentWords.filter(w => queryWords.has(w)).length
1415
+ return overlap >= 1
1416
+ })
1417
+ }
1418
+
1419
+ private generateTaskId(title: string): string {
1420
+ return title
1421
+ .toLowerCase()
1422
+ .replace(/[^a-z0-9]+/g, '-')
1423
+ .replace(/^-+|-+$/g, '')
1424
+ .slice(0, 50)
1425
+ }
1426
+ }
1427
+
1428
+ // Lazy singleton
1429
+ let routerInstance: BrainRouter | null = null
1430
+
1431
+ export function getBrainRouter(logger: Logger): BrainRouter {
1432
+ if (!routerInstance) {
1433
+ routerInstance = new BrainRouter(logger)
1434
+ }
1435
+ return routerInstance
1436
+ }
1437
+
1438
+ /** Reset singleton for testing */
1439
+ export function _resetBrainRouterForTesting(): void {
1440
+ routerInstance = null
1441
+ }