claude-brain 0.14.0 → 0.14.2

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