claude-brain 0.14.2 → 0.14.4

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