claude-brain 0.9.3 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/VERSION +1 -1
  2. package/assets/CLAUDE.md +9 -1
  3. package/package.json +1 -1
  4. package/src/automation/phase12-manager.ts +456 -0
  5. package/src/automation/project-detector.ts +13 -0
  6. package/src/automation/repo-scanner.ts +205 -0
  7. package/src/cli/bin.ts +30 -0
  8. package/src/cli/commands/git-hook.ts +189 -0
  9. package/src/cli/commands/hooks.ts +8 -9
  10. package/src/cli/commands/init.ts +98 -0
  11. package/src/cli/commands/serve.ts +7 -20
  12. package/src/cli/commands/update.ts +3 -3
  13. package/src/config/defaults.ts +4 -1
  14. package/src/config/schema.ts +27 -7
  15. package/src/hooks/brain-hook.ts +8 -6
  16. package/src/hooks/capture.ts +9 -2
  17. package/src/hooks/git-capture.ts +94 -0
  18. package/src/hooks/git-hook-installer.ts +197 -0
  19. package/src/hooks/index.ts +1 -0
  20. package/src/hooks/session-tracker.ts +79 -3
  21. package/src/hooks/types.ts +1 -1
  22. package/src/intelligence/index.ts +24 -0
  23. package/src/knowledge/graph/builder.ts +26 -0
  24. package/src/memory/chroma/store.ts +18 -2
  25. package/src/memory/episodic/manager.ts +17 -0
  26. package/src/memory/index.ts +48 -18
  27. package/src/phase12/index.ts +3 -454
  28. package/src/routing/intent-classifier.ts +107 -9
  29. package/src/routing/response-filter.ts +50 -17
  30. package/src/routing/router.ts +472 -224
  31. package/src/routing/search-engine.ts +464 -0
  32. package/src/routing/types.ts +84 -0
  33. package/src/server/handlers/call-tool.ts +4 -49
  34. package/src/server/handlers/tools/analyze-decision-evolution.ts +1 -1
  35. package/src/server/handlers/tools/detect-trends.ts +1 -1
  36. package/src/server/handlers/tools/find-cross-project-patterns.ts +1 -1
  37. package/src/server/handlers/tools/get-decision-timeline.ts +2 -2
  38. package/src/server/handlers/tools/get-recommendations.ts +1 -1
  39. package/src/server/handlers/tools/index.ts +5 -7
  40. package/src/server/handlers/tools/what-if-analysis.ts +1 -1
  41. package/src/server/providers/resources.ts +195 -0
  42. package/src/server/services.ts +81 -6
  43. package/src/tools/schemas.ts +7 -329
  44. package/src/utils/phase12-helper.ts +2 -2
  45. package/src/utils/timing.ts +47 -0
  46. package/src/vault/writer.ts +22 -2
  47. /package/src/{cross-project → intelligence/cross-project}/affinity.ts +0 -0
  48. /package/src/{cross-project → intelligence/cross-project}/generalizer.ts +0 -0
  49. /package/src/{cross-project → intelligence/cross-project}/index.ts +0 -0
  50. /package/src/{cross-project → intelligence/cross-project}/transfer.ts +0 -0
  51. /package/src/{optimization → intelligence/optimization}/index.ts +0 -0
  52. /package/src/{optimization → intelligence/optimization}/precompute.ts +0 -0
  53. /package/src/{optimization → intelligence/optimization}/semantic-cache.ts +0 -0
  54. /package/src/{prediction → intelligence/prediction}/context-anticipator.ts +0 -0
  55. /package/src/{prediction → intelligence/prediction}/decision-predictor.ts +0 -0
  56. /package/src/{prediction → intelligence/prediction}/index.ts +0 -0
  57. /package/src/{prediction → intelligence/prediction}/recommender.ts +0 -0
  58. /package/src/{reasoning → intelligence/reasoning}/chain-retrieval.ts +0 -0
  59. /package/src/{reasoning → intelligence/reasoning}/counterfactual.ts +0 -0
  60. /package/src/{reasoning → intelligence/reasoning}/index.ts +0 -0
  61. /package/src/{reasoning → intelligence/reasoning}/synthesizer.ts +0 -0
  62. /package/src/{temporal → intelligence/temporal}/evolution.ts +0 -0
  63. /package/src/{temporal → intelligence/temporal}/index.ts +0 -0
  64. /package/src/{temporal → intelligence/temporal}/query-processor.ts +0 -0
  65. /package/src/{temporal → intelligence/temporal}/timeline.ts +0 -0
  66. /package/src/{temporal → intelligence/temporal}/trends.ts +0 -0
@@ -1,27 +1,39 @@
1
1
  /**
2
2
  * Brain Router
3
- * Phase 16: Core orchestrator for the unified brain() tool
3
+ * Phase 16 + Phase 19: Core orchestrator for the unified brain() tool
4
4
  *
5
5
  * Routes classified intents to internal service calls and
6
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.
7
11
  */
8
12
 
9
13
  import type { Logger } from 'pino'
10
14
  import { IntentClassifier, type ClassificationResult } from './intent-classifier'
11
15
  import { BrainEntityExtractor, type BrainExtractedEntities } from './entity-extractor'
12
16
  import { ResponseFilter, type BrainResponse, type TierResults } from './response-filter'
17
+ import { SearchEngine } from './search-engine'
18
+ import type { NormalizedResult } from './types'
13
19
  import {
14
20
  getMemoryService,
15
21
  getVaultService,
16
22
  getContextService,
17
23
  getPhase12Service,
18
24
  getKnowledgeGraphService,
25
+ getEpisodeService,
19
26
  isServicesInitialized
20
27
  } from '@/server/services'
28
+ import { timed } from '@/utils/timing'
21
29
 
22
30
  /** Default project when none can be detected */
23
31
  const DEFAULT_PROJECT = 'general'
24
32
 
33
+ /** Phase 19 D4: Minimum similarity for destructive operations */
34
+ const DELETE_MIN_SIMILARITY = 0.5
35
+ const UPDATE_MIN_SIMILARITY = 0.5
36
+
25
37
  export interface BrainInput {
26
38
  message: string
27
39
  project?: string
@@ -31,6 +43,7 @@ export class BrainRouter {
31
43
  private classifier: IntentClassifier
32
44
  private entityExtractor: BrainEntityExtractor
33
45
  private responseFilter: ResponseFilter
46
+ private searchEngine: SearchEngine
34
47
  private logger: Logger
35
48
 
36
49
  /** Track the most recently stored decision ID for update/delete operations */
@@ -41,6 +54,7 @@ export class BrainRouter {
41
54
  this.classifier = new IntentClassifier()
42
55
  this.entityExtractor = new BrainEntityExtractor()
43
56
  this.responseFilter = new ResponseFilter()
57
+ this.searchEngine = new SearchEngine(logger)
44
58
  this.logger = logger.child({ component: 'brain-router' })
45
59
  }
46
60
 
@@ -66,7 +80,7 @@ export class BrainRouter {
66
80
  return this.handleSessionStart(message, project, entities)
67
81
 
68
82
  case 'context_needed':
69
- return this.handleContextNeeded(message, project, entities)
83
+ return this.handleContextNeeded(message, project, entities, classification)
70
84
 
71
85
  case 'decision_made':
72
86
  return this.handleDecisionMade(message, project, entities)
@@ -102,7 +116,7 @@ export class BrainRouter {
102
116
  return this.handleDeleteMemory(message, project, entities)
103
117
 
104
118
  default:
105
- return this.handleContextNeeded(message, project, entities)
119
+ return this.handleContextNeeded(message, project, entities, classification)
106
120
  }
107
121
  } catch (error) {
108
122
  this.logger.error({ error, intent: classification.primary }, 'Router handler error')
@@ -178,11 +192,10 @@ export class BrainRouter {
178
192
  }
179
193
  }
180
194
 
181
- // If Phase 12 found nothing, do a direct search fallback
195
+ // If Phase 12 found nothing, do a direct search fallback via SearchEngine
182
196
  if (!phase12Result.recalledMemories?.memories.length) {
183
197
  try {
184
- const memory = getMemoryService()
185
- const directResults = await memory.searchRaw(entities.topic || message, {
198
+ const directResults = await this.searchEngine.enhancedSearch(entities.topic || message, {
186
199
  project,
187
200
  limit: 5,
188
201
  minSimilarity: 0.3
@@ -190,8 +203,8 @@ export class BrainRouter {
190
203
  if (directResults.length > 0) {
191
204
  parts.push('\n---\n## Related Memories\n')
192
205
  for (const r of directResults) {
193
- const similarity = Math.round((r.similarity || 0) * 100)
194
- parts.push(`**[${similarity}%]** ${r.decision?.decision || r.content?.slice(0, 100) || ''}`)
206
+ const similarity = Math.round(r.score * 100)
207
+ parts.push(`**[${similarity}%]** ${r.content?.slice(0, 200) || ''}`)
195
208
  }
196
209
  }
197
210
  } catch {
@@ -199,6 +212,26 @@ export class BrainRouter {
199
212
  }
200
213
  }
201
214
 
215
+ // C8: Register with episode manager
216
+ this.registerEpisodeMessage(message, project, 'session_start')
217
+
218
+ // C9: Append proactive recommendations
219
+ try {
220
+ const recommendations = await this.searchEngine.getRecommendations(
221
+ entities.topic || message,
222
+ { project, limit: 3 }
223
+ )
224
+ if (recommendations?.recommendations?.length) {
225
+ parts.push('\n---\n## Proactive Recommendations\n')
226
+ for (const rec of recommendations.recommendations) {
227
+ parts.push(`- **${rec.type}**: ${rec.content?.slice(0, 150) || ''}`)
228
+ if (rec.reasoning) parts.push(` _${rec.reasoning}_`)
229
+ }
230
+ }
231
+ } catch {
232
+ // Recommendations not available
233
+ }
234
+
202
235
  const totalRecalled = phase12Result.recalledMemories?.memories.length || 0
203
236
  return {
204
237
  action: 'retrieved',
@@ -211,61 +244,64 @@ export class BrainRouter {
211
244
  private async handleContextNeeded(
212
245
  message: string,
213
246
  project: string | undefined,
214
- entities: BrainExtractedEntities
247
+ entities: BrainExtractedEntities,
248
+ classification?: ClassificationResult
215
249
  ): Promise<BrainResponse> {
216
250
  if (!isServicesInitialized()) {
217
251
  return this.servicesNotReady()
218
252
  }
219
253
 
220
- const memory = getMemoryService()
221
254
  const query = entities.topic || message
222
-
223
- // Search for relevant memories
224
255
  const tiers: TierResults[] = []
256
+ const hasTemporal = classification?.secondary.includes('exploration') ||
257
+ this.classifier.hasTemporalSignal(message.toLowerCase())
225
258
 
226
- try {
227
- const rawResults = await memory.searchRaw(query, {
259
+ // Use temporal search if temporal signals detected
260
+ if (hasTemporal) {
261
+ const { results } = await this.searchEngine.temporalSearch(query, { project, limit: 5 })
262
+ if (results.length > 0) {
263
+ tiers.push({
264
+ label: 'Memories',
265
+ results: results.map(r => ({
266
+ content: r.content,
267
+ score: r.score,
268
+ source: r.source === 'decision' ? 'Past Decision' : r.source,
269
+ metadata: r.metadata as Record<string, unknown>
270
+ }))
271
+ })
272
+ }
273
+ } else {
274
+ // Standard enhanced search
275
+ const searchResults = await this.searchEngine.enhancedSearch(query, {
228
276
  project,
229
277
  limit: 5,
230
278
  minSimilarity: 0.3
231
279
  })
232
-
233
- tiers.push({
234
- label: 'Memories',
235
- results: rawResults.map(r => ({
236
- content: r.decision?.decision
237
- ? `**${r.decision.decision}**\n${r.decision.reasoning || ''}\n_Context: ${r.decision.context || ''}_`
238
- : r.content?.slice(0, 300) || '',
239
- score: r.similarity || 0,
240
- source: 'Past Decision',
241
- metadata: r.metadata
242
- }))
243
- })
244
- } catch {
245
- // Search failed, continue
246
- }
247
-
248
- // Also search patterns if project is available
249
- try {
250
- const patternResults = await memory.searchPatterns(query, {
251
- project,
252
- limit: 3,
253
- minSimilarity: 0.3
254
- })
255
-
256
- if (patternResults.length > 0) {
280
+ if (searchResults.length > 0) {
257
281
  tiers.push({
258
- label: 'Patterns',
259
- results: patternResults.map(p => ({
260
- content: p.metadata?.description || p.content || '',
261
- score: p.similarity || 0,
262
- source: `Pattern (${p.metadata?.pattern_type || 'general'})`,
263
- metadata: p.metadata
282
+ label: 'Memories',
283
+ results: searchResults.map(r => ({
284
+ content: r.content,
285
+ score: r.score,
286
+ source: r.source === 'decision' ? 'Past Decision' : r.source,
287
+ metadata: r.metadata as Record<string, unknown>
264
288
  }))
265
289
  })
266
290
  }
267
- } catch {
268
- // Pattern search not available
291
+ }
292
+
293
+ // Also search patterns
294
+ const patternResults = await this.searchEngine.searchPatterns(query, { project, limit: 3 })
295
+ if (patternResults.length > 0) {
296
+ tiers.push({
297
+ label: 'Patterns',
298
+ results: patternResults.map(p => ({
299
+ content: p.content,
300
+ score: p.score,
301
+ source: `Pattern`,
302
+ metadata: p.metadata as Record<string, unknown>
303
+ }))
304
+ })
269
305
  }
270
306
 
271
307
  return this.responseFilter.synthesize(tiers, message, project)
@@ -273,14 +309,12 @@ export class BrainRouter {
273
309
 
274
310
  /**
275
311
  * Handle explicit "store this" requests — always uses the FULL message as the decision text.
276
- * This prevents content mangling from entity extraction.
277
312
  */
278
313
  private async handleStoreThis(
279
314
  message: string,
280
315
  project: string | undefined,
281
316
  entities: BrainExtractedEntities
282
317
  ): Promise<BrainResponse> {
283
- // Use "general" as fallback when no project is detected
284
318
  const effectiveProject = project || DEFAULT_PROJECT
285
319
 
286
320
  if (!isServicesInitialized()) {
@@ -288,13 +322,11 @@ export class BrainRouter {
288
322
  }
289
323
 
290
324
  const memory = getMemoryService()
291
-
292
- // ALWAYS use the full message as the decision — never the extracted fragment
293
325
  const decision = message
294
326
  const reasoning = entities.reasoning || ''
295
327
  const context = entities.topic || message.slice(0, 200)
296
328
 
297
- const decisionId = await memory.rememberDecision(
329
+ const decisionId = await timed('brain:store', () => memory.rememberDecision(
298
330
  effectiveProject,
299
331
  context,
300
332
  decision,
@@ -303,14 +335,20 @@ export class BrainRouter {
303
335
  alternatives: entities.alternatives,
304
336
  tags: entities.technologies.length > 0 ? entities.technologies : ['user-stored']
305
337
  }
306
- )
338
+ ))
307
339
 
308
340
  // Track for potential update/delete
309
341
  this.lastStoredId = decisionId
310
342
  this.lastStoredProject = effectiveProject
311
343
 
312
- // Also write to vault
313
- this.writeToVault(effectiveProject, decision, reasoning, context, entities.alternatives)
344
+ // Invalidate cache for this project
345
+ this.searchEngine.invalidateCache(effectiveProject)
346
+
347
+ // Background: vault write + episode linking (non-critical)
348
+ setImmediate(() => {
349
+ this.writeToVault(effectiveProject, decision, reasoning, context, entities.alternatives)
350
+ this.linkToActiveEpisode(effectiveProject, decisionId, 'decision')
351
+ })
314
352
 
315
353
  return {
316
354
  action: 'stored',
@@ -325,7 +363,6 @@ export class BrainRouter {
325
363
  project: string | undefined,
326
364
  entities: BrainExtractedEntities
327
365
  ): Promise<BrainResponse> {
328
- // Use "general" as fallback when no project is detected
329
366
  const effectiveProject = project || DEFAULT_PROJECT
330
367
 
331
368
  if (!isServicesInitialized()) {
@@ -333,15 +370,12 @@ export class BrainRouter {
333
370
  }
334
371
 
335
372
  const memory = getMemoryService()
336
-
337
- // ALWAYS use the full message as the decision text to prevent content mangling.
338
- // The extracted entities are used only for structured metadata.
339
373
  const decision = message
340
374
  const reasoning = entities.reasoning || ''
341
375
  const context = entities.topic || message.slice(0, 200)
342
376
  const alternatives = entities.alternatives
343
377
 
344
- const decisionId = await memory.rememberDecision(
378
+ const decisionId = await timed('brain:store', () => memory.rememberDecision(
345
379
  effectiveProject,
346
380
  context,
347
381
  decision,
@@ -350,14 +384,19 @@ export class BrainRouter {
350
384
  alternatives,
351
385
  tags: entities.technologies.length > 0 ? entities.technologies : ['auto-detected']
352
386
  }
353
- )
387
+ ))
354
388
 
355
- // Track for potential update/delete
356
389
  this.lastStoredId = decisionId
357
390
  this.lastStoredProject = effectiveProject
358
391
 
359
- // Also write to vault
360
- this.writeToVault(effectiveProject, decision, reasoning, context, alternatives)
392
+ // Invalidate cache
393
+ this.searchEngine.invalidateCache(effectiveProject)
394
+
395
+ // Background: vault write + episode linking (non-critical)
396
+ setImmediate(() => {
397
+ this.writeToVault(effectiveProject, decision, reasoning, context, alternatives)
398
+ this.linkToActiveEpisode(effectiveProject, decisionId, 'decision')
399
+ })
361
400
 
362
401
  return {
363
402
  action: 'stored',
@@ -380,7 +419,7 @@ export class BrainRouter {
380
419
 
381
420
  const memory = getMemoryService()
382
421
  const patternType = entities.patternType || 'solution'
383
- const description = message // Use full message
422
+ const description = message
384
423
 
385
424
  const patternId = await memory.storePattern({
386
425
  project: effectiveProject,
@@ -390,6 +429,12 @@ export class BrainRouter {
390
429
  context: message.slice(0, 300)
391
430
  })
392
431
 
432
+ // Invalidate cache
433
+ this.searchEngine.invalidateCache(effectiveProject)
434
+
435
+ // Link to active episode
436
+ this.linkToActiveEpisode(effectiveProject, patternId, 'pattern')
437
+
393
438
  return {
394
439
  action: 'stored',
395
440
  summary: `Stored ${patternType}: ${description.slice(0, 60)}`,
@@ -410,8 +455,6 @@ export class BrainRouter {
410
455
  }
411
456
 
412
457
  const memory = getMemoryService()
413
-
414
- // Use full message as the original content to prevent mangling
415
458
  const original = entities.original || message
416
459
  const correction = entities.correction || ''
417
460
  const reasoning = entities.reasoning || 'Lesson learned from experience'
@@ -425,6 +468,12 @@ export class BrainRouter {
425
468
  confidence: 0.9
426
469
  })
427
470
 
471
+ // Invalidate cache
472
+ this.searchEngine.invalidateCache(effectiveProject)
473
+
474
+ // Link to active episode
475
+ this.linkToActiveEpisode(effectiveProject, correctionId, 'correction')
476
+
428
477
  return {
429
478
  action: 'stored',
430
479
  summary: `Stored correction: ${original.slice(0, 60)}`,
@@ -459,10 +508,10 @@ export class BrainRouter {
459
508
  completedAt: new Date()
460
509
  })
461
510
  } catch {
462
- // Progress update failed — still report what we found
511
+ // Progress update failed
463
512
  }
464
513
 
465
- // Also store as a searchable memory in ChromaDB so it appears in search results
514
+ // Also store as a searchable memory
466
515
  try {
467
516
  await memory.rememberDecision(
468
517
  effectiveProject,
@@ -471,10 +520,16 @@ export class BrainRouter {
471
520
  'Progress tracking',
472
521
  { tags: ['progress', ...entities.technologies] }
473
522
  )
523
+
524
+ // Invalidate cache
525
+ this.searchEngine.invalidateCache(effectiveProject)
474
526
  } catch {
475
- // Memory storage failed, context-only storage still succeeded
527
+ // Memory storage failed
476
528
  }
477
529
 
530
+ // Register with episode
531
+ this.registerEpisodeMessage(message, effectiveProject, 'progress_update')
532
+
478
533
  return {
479
534
  action: 'stored',
480
535
  summary: `Progress: ${completedTask.slice(0, 60)}`,
@@ -496,7 +551,7 @@ export class BrainRouter {
496
551
  }
497
552
 
498
553
  const memory = getMemoryService()
499
- const effectiveProject = project // No fallback — list all if no project
554
+ const effectiveProject = project
500
555
 
501
556
  try {
502
557
  const decisions = await memory.fetchAllDecisions(effectiveProject)
@@ -565,8 +620,7 @@ export class BrainRouter {
565
620
  }
566
621
 
567
622
  /**
568
- * Update a previous decision searches by content to find the right one, then replaces it.
569
- * Uses lastStoredId only for generic "actually, X" with no descriptive subject.
623
+ * Phase 19 D4: Update with higher similarity threshold
570
624
  */
571
625
  private async handleUpdateMemory(
572
626
  message: string,
@@ -583,17 +637,18 @@ export class BrainRouter {
583
637
  const topic = entities.topic || message
584
638
  const hasSpecificContent = this.hasDescriptiveContent(message)
585
639
 
586
- // If the message has specific content, search for the matching decision first
587
640
  if (hasSpecificContent) {
588
641
  try {
589
642
  const results = await memory.searchRaw(topic, {
590
643
  project: effectiveProject,
591
644
  limit: 1,
592
- minSimilarity: 0.3
645
+ minSimilarity: UPDATE_MIN_SIMILARITY
593
646
  })
594
647
 
595
- const matchId = results[0]?.memory?.id || results[0]?.decision?.id
596
- if (results.length > 0 && matchId) {
648
+ const matchId = results[0]?.memory?.id || results[0]?.decision?.id || results[0]?.id
649
+ const matchSimilarity = results[0]?.similarity || 0
650
+
651
+ if (results.length > 0 && matchId && matchSimilarity >= UPDATE_MIN_SIMILARITY) {
597
652
  const oldId = matchId
598
653
  const newId = await memory.updateDecision(
599
654
  oldId,
@@ -607,20 +662,26 @@ export class BrainRouter {
607
662
  this.lastStoredId = newId
608
663
  this.lastStoredProject = effectiveProject
609
664
 
610
- const oldContent = results[0].decision?.decision || results[0].memory?.content?.slice(0, 100) || ''
665
+ // Invalidate cache
666
+ this.searchEngine.invalidateCache(effectiveProject)
667
+
668
+ const oldContent = results[0].decision?.decision || results[0].memory?.content?.slice(0, 100) || results[0].content?.slice(0, 100) || ''
611
669
  return {
612
670
  action: 'stored',
613
671
  summary: `Updated decision`,
614
672
  content: `Replaced: "${oldContent.slice(0, 80)}"\n\nWith:\n**New content:** ${message}`,
615
673
  relevantItems: 1
616
674
  }
675
+ } else if (results.length > 0 && matchSimilarity < UPDATE_MIN_SIMILARITY) {
676
+ // D4: No confident match — store as new instead of updating wrong memory
677
+ return this.storeAsNew(memory, effectiveProject, message, topic, entities, 'No confident match to update (similarity too low)')
617
678
  }
618
679
  } catch {
619
680
  // Search failed, fall through
620
681
  }
621
682
  }
622
683
 
623
- // Generic update ("actually, use X instead") — use lastStoredId
684
+ // Generic update — use lastStoredId
624
685
  if (this.lastStoredId) {
625
686
  const newId = await memory.updateDecision(
626
687
  this.lastStoredId,
@@ -633,6 +694,7 @@ export class BrainRouter {
633
694
 
634
695
  this.lastStoredId = newId
635
696
  this.lastStoredProject = effectiveProject
697
+ this.searchEngine.invalidateCache(effectiveProject)
636
698
 
637
699
  return {
638
700
  action: 'stored',
@@ -642,29 +704,12 @@ export class BrainRouter {
642
704
  }
643
705
  }
644
706
 
645
- // Fallback: just store as a new decision
646
- const decisionId = await memory.rememberDecision(
647
- effectiveProject,
648
- topic.slice(0, 200),
649
- message,
650
- entities.reasoning || '',
651
- { tags: entities.technologies.length > 0 ? entities.technologies : ['updated'] }
652
- )
653
-
654
- this.lastStoredId = decisionId
655
- this.lastStoredProject = effectiveProject
656
-
657
- return {
658
- action: 'stored',
659
- summary: `Stored as new (no matching decision found to update)`,
660
- content: `Stored as new decision (ID: ${decisionId})\n\n**Project:** ${effectiveProject}\n**Content:** ${message}`,
661
- relevantItems: 1
662
- }
707
+ // Fallback: store as new decision
708
+ return this.storeAsNew(memory, effectiveProject, message, topic, entities, 'No matching decision found to update')
663
709
  }
664
710
 
665
711
  /**
666
- * Delete a memory searches by content to find the right one.
667
- * Only uses lastStoredId for very generic requests like "forget that" with no descriptive words.
712
+ * Phase 19 D4: Delete with higher similarity threshold
668
713
  */
669
714
  private async handleDeleteMemory(
670
715
  message: string,
@@ -679,46 +724,56 @@ export class BrainRouter {
679
724
 
680
725
  const memory = getMemoryService()
681
726
  const topic = entities.topic || message
682
-
683
- // Check if the message has meaningful content to search by
684
- // (strip common delete phrases to see if there's a real subject)
685
727
  const hasSpecificContent = this.hasDescriptiveContent(message)
686
728
 
687
- // If the user described what to delete, ALWAYS search by content first
688
729
  if (hasSpecificContent) {
689
730
  try {
690
731
  const results = await memory.searchRaw(topic, {
691
732
  project: effectiveProject,
692
733
  limit: 3,
693
- minSimilarity: 0.3
734
+ minSimilarity: DELETE_MIN_SIMILARITY
694
735
  })
695
736
 
696
- const matchId = results[0]?.memory?.id || results[0]?.decision?.id
697
- if (results.length > 0 && matchId) {
737
+ const matchId = results[0]?.memory?.id || results[0]?.decision?.id || results[0]?.id
738
+ const matchSimilarity = results[0]?.similarity || 0
739
+
740
+ if (results.length > 0 && matchId && matchSimilarity >= DELETE_MIN_SIMILARITY) {
698
741
  const targetId = matchId
699
- const content = results[0].decision?.decision || results[0].memory?.content?.slice(0, 100) || ''
742
+ const content = results[0].decision?.decision || results[0].memory?.content?.slice(0, 100) || results[0].content?.slice(0, 100) || ''
700
743
 
701
744
  await memory.deleteDecision(targetId)
702
745
  if (this.lastStoredId === targetId) this.lastStoredId = null
703
746
 
747
+ // Invalidate cache
748
+ this.searchEngine.invalidateCache(effectiveProject)
749
+
704
750
  return {
705
751
  action: 'stored',
706
752
  summary: `Deleted memory`,
707
753
  content: `Deleted: "${content.slice(0, 100)}" (ID: ${targetId})`,
708
754
  relevantItems: 0
709
755
  }
756
+ } else if (results.length > 0 && matchSimilarity < DELETE_MIN_SIMILARITY) {
757
+ // D4: No confident match
758
+ return {
759
+ action: 'none',
760
+ summary: 'No confident match to delete',
761
+ 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.`,
762
+ relevantItems: 0
763
+ }
710
764
  }
711
765
  } catch {
712
766
  // Search failed, fall through
713
767
  }
714
768
  }
715
769
 
716
- // Generic request ("forget that", "delete that") — use lastStoredId
770
+ // Generic request — use lastStoredId
717
771
  if (this.lastStoredId) {
718
772
  try {
719
773
  await memory.deleteDecision(this.lastStoredId)
720
774
  const deletedId = this.lastStoredId
721
775
  this.lastStoredId = null
776
+ this.searchEngine.invalidateCache(effectiveProject)
722
777
  return {
723
778
  action: 'stored',
724
779
  summary: `Deleted most recent memory`,
@@ -738,6 +793,14 @@ export class BrainRouter {
738
793
  }
739
794
  }
740
795
 
796
+ /**
797
+ * Phase 19 C7+C10+C11: Enhanced question handler
798
+ * - Uses SearchEngine for all searches (hybrid, cached)
799
+ * - Temporal search when temporal signals detected
800
+ * - ChainRetrieval for complex multi-part questions
801
+ * - CrossProject patterns for general/unspecified project
802
+ * - KnowledgeGraph enrichment
803
+ */
741
804
  private async handleQuestion(
742
805
  message: string,
743
806
  project: string | undefined,
@@ -748,51 +811,67 @@ export class BrainRouter {
748
811
  return this.servicesNotReady()
749
812
  }
750
813
 
751
- const memory = getMemoryService()
752
814
  const query = entities.topic || message
753
815
  const tiers: TierResults[] = []
816
+ const hasTemporal = classification.secondary.includes('exploration') ||
817
+ this.classifier.hasTemporalSignal(message.toLowerCase())
818
+
819
+ // C7: Detect multi-part questions for chain retrieval
820
+ const isComplex = this.isComplexQuestion(message)
821
+
822
+ // Main search — temporal or standard
823
+ let searchResults: NormalizedResult[]
824
+ if (hasTemporal) {
825
+ const { results } = await this.searchEngine.temporalSearch(query, { project, limit: 5 })
826
+ searchResults = results
827
+ } else if (isComplex) {
828
+ // C7: Try multi-hop chain retrieval
829
+ const chainResult = await this.searchEngine.chainSearch(query, { project })
830
+ if (chainResult?.allResults?.length) {
831
+ searchResults = chainResult.allResults.map((r: any) => ({
832
+ id: r.id || '',
833
+ content: r.content || '',
834
+ score: r.similarity || 0,
835
+ source: 'decision' as const,
836
+ project: r.metadata?.project || project || '',
837
+ date: r.metadata?.created_at || '',
838
+ metadata: r.metadata || {}
839
+ }))
840
+ } else {
841
+ searchResults = await this.searchEngine.enhancedSearch(query, { project, limit: 5 })
842
+ }
843
+ } else {
844
+ searchResults = await this.searchEngine.enhancedSearch(query, { project, limit: 5 })
845
+ }
754
846
 
755
- // Parallel search: raw memories + patterns + corrections
756
- const [rawResults, patternResults, correctionResults] = await Promise.all([
757
- memory.searchRaw(query, {
758
- project,
759
- limit: 5,
760
- minSimilarity: 0.3
761
- }).catch(() => [] as any[]),
762
- memory.searchPatterns(query, {
763
- project,
764
- limit: 3,
765
- minSimilarity: 0.3
766
- }).catch(() => [] as any[]),
767
- memory.searchCorrections(query, {
768
- project,
769
- limit: 3,
770
- minSimilarity: 0.3
771
- }).catch(() => [] as any[])
772
- ])
773
-
774
- if (rawResults.length > 0) {
847
+ if (searchResults.length > 0) {
775
848
  tiers.push({
776
849
  label: 'Memories',
777
- results: rawResults.map((r: any) => ({
778
- content: r.decision?.decision
779
- ? `**${r.decision.decision}**\n${r.decision.reasoning || ''}`
780
- : r.content?.slice(0, 300) || '',
781
- score: r.similarity || 0,
782
- source: 'Past Decision',
783
- metadata: r.metadata
850
+ results: searchResults.map(r => ({
851
+ content: r.content,
852
+ score: r.score,
853
+ source: r.source === 'decision' ? 'Past Decision' : r.source,
854
+ metadata: r.metadata as Record<string, unknown>
784
855
  }))
785
856
  })
786
857
  }
787
858
 
859
+ // Parallel: patterns + corrections + graph
860
+ const [patternResults, correctionResults, graphResults] = await Promise.all([
861
+ this.searchEngine.searchPatterns(query, { project, limit: 3 }),
862
+ this.searchEngine.searchCorrections(query, { project, limit: 3 }),
863
+ // C11: KnowledgeGraph enrichment for all questions
864
+ this.searchEngine.searchGraph(query, 5)
865
+ ])
866
+
788
867
  if (patternResults.length > 0) {
789
868
  tiers.push({
790
869
  label: 'Patterns',
791
- results: patternResults.map((p: any) => ({
792
- content: p.metadata?.description || p.content || '',
793
- score: p.similarity || 0,
794
- source: `Pattern (${p.metadata?.pattern_type || 'general'})`,
795
- metadata: p.metadata
870
+ results: patternResults.map(p => ({
871
+ content: p.content,
872
+ score: p.score,
873
+ source: `Pattern`,
874
+ metadata: p.metadata as Record<string, unknown>
796
875
  }))
797
876
  })
798
877
  }
@@ -800,24 +879,64 @@ export class BrainRouter {
800
879
  if (correctionResults.length > 0) {
801
880
  tiers.push({
802
881
  label: 'Corrections',
803
- results: correctionResults.map((c: any) => ({
804
- content: `Original: ${c.metadata?.original || ''}\nFix: ${c.metadata?.correction || c.content || ''}`,
805
- score: c.similarity || 0,
882
+ results: correctionResults.map(c => ({
883
+ content: c.content,
884
+ score: c.score,
806
885
  source: 'Lesson Learned',
807
- metadata: c.metadata
886
+ metadata: c.metadata as Record<string, unknown>
808
887
  }))
809
888
  })
810
889
  }
811
890
 
812
- // If secondary intent is exploration, also try graph search
813
- if (classification.secondary.includes('exploration')) {
814
- await this.addExplorationResults(query, project, tiers)
891
+ // C11: Add graph results as "Related Concepts"
892
+ if (graphResults.length > 0) {
893
+ tiers.push({
894
+ label: 'Related Concepts',
895
+ results: graphResults.map(g => ({
896
+ content: g.content,
897
+ score: g.score,
898
+ source: 'Knowledge Graph',
899
+ metadata: g.metadata as Record<string, unknown>
900
+ }))
901
+ })
815
902
  }
816
903
 
904
+ // C10: CrossProject patterns for general/unspecified project queries
905
+ if (!project || project === 'general') {
906
+ try {
907
+ const crossProject = await this.searchEngine.findCrossProjectPatterns({
908
+ query,
909
+ limit: 3
910
+ })
911
+ if (crossProject?.patterns?.length) {
912
+ tiers.push({
913
+ label: 'Cross-Project Patterns',
914
+ results: crossProject.patterns.map((p: any) => ({
915
+ content: `${p.description || ''} (${p.projects?.join(', ') || 'multiple projects'})`,
916
+ score: p.confidence || 0.5,
917
+ source: 'Cross-Project',
918
+ metadata: {}
919
+ }))
920
+ })
921
+ }
922
+ } catch {
923
+ // Cross-project not available
924
+ }
925
+ }
926
+
927
+ // Register with episode
928
+ this.registerEpisodeMessage(message, project, 'question')
929
+
817
930
  return this.responseFilter.synthesize(tiers, message, project)
818
931
  }
819
932
 
820
- private async handleComparison(
933
+ /**
934
+ * Phase 19 C6: Enhanced exploration handler
935
+ * - Timeline for "timeline"/"chronological" queries
936
+ * - DecisionEvolutionTracker for "how has X changed" queries
937
+ * - TrendDetector for "trends"/"emerging" queries
938
+ */
939
+ private async handleExploration(
821
940
  message: string,
822
941
  project: string | undefined,
823
942
  entities: BrainExtractedEntities
@@ -826,58 +945,129 @@ export class BrainRouter {
826
945
  return this.servicesNotReady()
827
946
  }
828
947
 
829
- const memory = getMemoryService()
830
948
  const query = entities.topic || message
949
+ const lower = message.toLowerCase()
831
950
  const tiers: TierResults[] = []
832
951
 
833
- // Search for related decisions
834
- try {
835
- const rawResults = await memory.searchRaw(query, {
952
+ // C6: Timeline queries
953
+ if (lower.includes('timeline') || lower.includes('chronological') || lower.includes('history of')) {
954
+ const timeline = await this.searchEngine.buildTimeline({
836
955
  project,
837
- limit: 5,
838
- minSimilarity: 0.2
956
+ topic: query,
957
+ limit: 20
839
958
  })
840
959
 
841
- if (rawResults.length > 0) {
960
+ if (timeline?.entries?.length) {
842
961
  tiers.push({
843
- label: 'Related Decisions',
844
- results: rawResults.map((r: any) => ({
845
- content: r.decision?.decision
846
- ? `**${r.decision.decision}**\n${r.decision.reasoning || ''}`
847
- : r.content?.slice(0, 300) || '',
848
- score: r.similarity || 0,
849
- source: 'Past Decision'
962
+ label: 'Timeline',
963
+ results: timeline.entries.map((e: any) => ({
964
+ content: `**${e.date || ''}** — ${e.content || ''}`,
965
+ score: 0.8,
966
+ source: `Timeline (${e.type || 'event'})`,
967
+ metadata: e.metadata || {}
850
968
  }))
851
969
  })
852
970
  }
853
- } catch {
854
- // Search failed
855
971
  }
856
972
 
857
- // Try what-if analysis if knowledge graph is available
858
- try {
859
- const kgService = getKnowledgeGraphService()
860
- if (kgService?.search) {
861
- const graphResults = kgService.search.search({ query, limit: 5 })
862
- if (graphResults?.nodes?.length) {
973
+ // C6: Evolution queries
974
+ if (lower.includes('evolution') || lower.includes('how has') || lower.includes('changed') || lower.includes('evolved')) {
975
+ const evolution = await this.searchEngine.analyzeEvolution(query, { project })
976
+ if (evolution?.timeline?.length) {
977
+ const parts = []
978
+ parts.push(`**Stability:** ${evolution.stability || 'unknown'}`)
979
+ if (evolution.currentState) parts.push(`**Current:** ${evolution.currentState}`)
980
+ for (const change of (evolution.changes || []).slice(0, 5)) {
981
+ parts.push(`- ${change.description || change}`)
982
+ }
983
+
984
+ tiers.push({
985
+ label: 'Decision Evolution',
986
+ results: [{
987
+ content: parts.join('\n'),
988
+ score: 0.8,
989
+ source: 'Evolution Analysis',
990
+ metadata: {}
991
+ }]
992
+ })
993
+ }
994
+ }
995
+
996
+ // C6: Trend queries
997
+ if (lower.includes('trend') || lower.includes('emerging') || lower.includes('declining')) {
998
+ const trends = await this.searchEngine.detectTrends({ project })
999
+ if (trends?.topTrends?.length) {
1000
+ tiers.push({
1001
+ label: 'Trends',
1002
+ results: trends.topTrends.slice(0, 5).map((t: any) => ({
1003
+ content: `**${t.term}** — ${t.trend} (${t.occurrences} occurrences, momentum: ${t.momentum || 'unknown'})`,
1004
+ score: t.occurrences > 5 ? 0.9 : 0.7,
1005
+ source: `Trend (${t.trend})`,
1006
+ metadata: {}
1007
+ }))
1008
+ })
1009
+ }
1010
+ }
1011
+
1012
+ // Always include graph exploration
1013
+ const graphResults = await this.searchEngine.searchGraph(query, 10)
1014
+ if (graphResults.length > 0) {
1015
+ tiers.push({
1016
+ label: 'Knowledge Graph',
1017
+ results: graphResults.map(g => ({
1018
+ content: g.content,
1019
+ score: g.score,
1020
+ source: 'Knowledge Graph',
1021
+ metadata: g.metadata as Record<string, unknown>
1022
+ }))
1023
+ })
1024
+ }
1025
+
1026
+ // Issue 9: Cross-project patterns for exploration queries
1027
+ if (!project || project === 'general') {
1028
+ try {
1029
+ const crossProject = await this.searchEngine.findCrossProjectPatterns({
1030
+ query,
1031
+ limit: 3
1032
+ })
1033
+ if (crossProject?.patterns?.length) {
863
1034
  tiers.push({
864
- label: 'Knowledge Graph',
865
- results: graphResults.nodes.map((n: any) => ({
866
- content: `**${n.label || n.name}** (${n.type})\n${n.metadata?.description || ''}`,
867
- score: n.score || 0.5,
868
- source: 'Knowledge Graph'
1035
+ label: 'Cross-Project Patterns',
1036
+ results: crossProject.patterns.map((p: any) => ({
1037
+ content: `${p.description || ''} (${p.projects?.join(', ') || 'multiple projects'})`,
1038
+ score: p.confidence || 0.5,
1039
+ source: 'Cross-Project',
1040
+ metadata: {}
869
1041
  }))
870
1042
  })
871
1043
  }
1044
+ } catch {
1045
+ // Cross-project not available
872
1046
  }
873
- } catch {
874
- // Knowledge graph not available
1047
+ }
1048
+
1049
+ // Also do a basic memory search as fallback
1050
+ const searchResults = await this.searchEngine.enhancedSearch(query, {
1051
+ project,
1052
+ limit: 5,
1053
+ minSimilarity: 0.2
1054
+ })
1055
+ if (searchResults.length > 0) {
1056
+ tiers.push({
1057
+ label: 'Memories',
1058
+ results: searchResults.map(r => ({
1059
+ content: r.content,
1060
+ score: r.score,
1061
+ source: r.source === 'decision' ? 'Past Decision' : r.source,
1062
+ metadata: r.metadata as Record<string, unknown>
1063
+ }))
1064
+ })
875
1065
  }
876
1066
 
877
1067
  return this.responseFilter.synthesize(tiers, message, project, 'analyzed')
878
1068
  }
879
1069
 
880
- private async handleExploration(
1070
+ private async handleComparison(
881
1071
  message: string,
882
1072
  project: string | undefined,
883
1073
  entities: BrainExtractedEntities
@@ -889,31 +1079,36 @@ export class BrainRouter {
889
1079
  const query = entities.topic || message
890
1080
  const tiers: TierResults[] = []
891
1081
 
892
- await this.addExplorationResults(query, project, tiers)
893
-
894
- // Also do a basic memory search
895
- try {
896
- const memory = getMemoryService()
897
- const rawResults = await memory.searchRaw(query, {
898
- project,
899
- limit: 5,
900
- minSimilarity: 0.2
1082
+ // Search for related decisions
1083
+ const searchResults = await this.searchEngine.enhancedSearch(query, {
1084
+ project,
1085
+ limit: 5,
1086
+ minSimilarity: 0.2
1087
+ })
1088
+ if (searchResults.length > 0) {
1089
+ tiers.push({
1090
+ label: 'Related Decisions',
1091
+ results: searchResults.map(r => ({
1092
+ content: r.content,
1093
+ score: r.score,
1094
+ source: 'Past Decision',
1095
+ metadata: r.metadata as Record<string, unknown>
1096
+ }))
901
1097
  })
1098
+ }
902
1099
 
903
- if (rawResults.length > 0) {
904
- tiers.push({
905
- label: 'Memories',
906
- results: rawResults.map((r: any) => ({
907
- content: r.decision?.decision
908
- ? `**${r.decision.decision}**\n${r.decision.reasoning || ''}`
909
- : r.content?.slice(0, 300) || '',
910
- score: r.similarity || 0,
911
- source: 'Past Decision'
912
- }))
913
- })
914
- }
915
- } catch {
916
- // Search failed
1100
+ // Graph search for comparison context
1101
+ const graphResults = await this.searchEngine.searchGraph(query, 5)
1102
+ if (graphResults.length > 0) {
1103
+ tiers.push({
1104
+ label: 'Knowledge Graph',
1105
+ results: graphResults.map(g => ({
1106
+ content: g.content,
1107
+ score: g.score,
1108
+ source: 'Knowledge Graph',
1109
+ metadata: g.metadata as Record<string, unknown>
1110
+ }))
1111
+ })
917
1112
  }
918
1113
 
919
1114
  return this.responseFilter.synthesize(tiers, message, project, 'analyzed')
@@ -935,35 +1130,89 @@ export class BrainRouter {
935
1130
  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`
936
1131
  await vault.writer.appendContent(projectPaths.decisions, entry, '\n')
937
1132
  } catch {
938
- // Vault write failed — memory storage still succeeded
1133
+ // Vault write failed
939
1134
  }
940
1135
  }
941
1136
 
942
- private async addExplorationResults(
943
- query: string,
944
- _project: string | undefined,
945
- tiers: TierResults[]
946
- ): Promise<void> {
1137
+ /**
1138
+ * Store as a new decision (fallback for update when no match found)
1139
+ */
1140
+ private async storeAsNew(
1141
+ memory: any,
1142
+ project: string,
1143
+ message: string,
1144
+ topic: string,
1145
+ entities: BrainExtractedEntities,
1146
+ reason: string
1147
+ ): Promise<BrainResponse> {
1148
+ const decisionId = await memory.rememberDecision(
1149
+ project,
1150
+ topic.slice(0, 200),
1151
+ message,
1152
+ entities.reasoning || '',
1153
+ { tags: entities.technologies.length > 0 ? entities.technologies : ['updated'] }
1154
+ )
1155
+
1156
+ this.lastStoredId = decisionId
1157
+ this.lastStoredProject = project
1158
+ this.searchEngine.invalidateCache(project)
1159
+
1160
+ return {
1161
+ action: 'stored',
1162
+ summary: `Stored as new (${reason})`,
1163
+ content: `Stored as new decision (ID: ${decisionId})\n\n**Project:** ${project}\n**Content:** ${message}`,
1164
+ relevantItems: 1
1165
+ }
1166
+ }
1167
+
1168
+ /**
1169
+ * C8: Register a message with the episode manager
1170
+ */
1171
+ private registerEpisodeMessage(message: string, project?: string, role: string = 'user'): void {
947
1172
  try {
948
- const kgService = getKnowledgeGraphService()
949
- if (kgService?.search) {
950
- const graphResults = kgService.search.search({ query, limit: 10 })
951
- if (graphResults?.nodes?.length) {
952
- tiers.push({
953
- label: 'Knowledge Graph',
954
- results: graphResults.nodes.map((n: any) => ({
955
- content: `**${n.label || n.name}** (${n.type})${n.metadata?.description ? `\n${n.metadata.description}` : ''}`,
956
- score: n.score || 0.5,
957
- source: 'Knowledge Graph'
958
- }))
959
- })
960
- }
961
- }
1173
+ const episodeManager = getEpisodeService()
1174
+ if (!episodeManager) return
1175
+ episodeManager.processMessage(
1176
+ { role, content: message, timestamp: new Date().toISOString() },
1177
+ project
1178
+ ).catch(() => {})
962
1179
  } catch {
963
- // Knowledge graph not available
1180
+ // Episode manager not available
964
1181
  }
965
1182
  }
966
1183
 
1184
+ /**
1185
+ * C8: Link a stored decision/pattern/correction to the active episode
1186
+ */
1187
+ private linkToActiveEpisode(project: string, id: string, type: 'decision' | 'pattern' | 'correction'): void {
1188
+ try {
1189
+ const episodeManager = getEpisodeService()
1190
+ if (!episodeManager) return
1191
+ const activeEpisode = episodeManager.getActiveEpisode(project)
1192
+ if (!activeEpisode) return
1193
+
1194
+ if (type === 'decision') episodeManager.linkDecision(activeEpisode.id, id)
1195
+ else if (type === 'pattern') episodeManager.linkPattern(activeEpisode.id, id)
1196
+ else if (type === 'correction') episodeManager.linkCorrection(activeEpisode.id, id)
1197
+ } catch {
1198
+ // Episode manager not available
1199
+ }
1200
+ }
1201
+
1202
+ /**
1203
+ * C7: Detect complex multi-part questions
1204
+ */
1205
+ private isComplexQuestion(message: string): boolean {
1206
+ // Multiple question marks
1207
+ const questionMarks = (message.match(/\?/g) || []).length
1208
+ if (questionMarks >= 2) return true
1209
+ // Very long question (likely multi-part)
1210
+ if (message.length > 200 && message.includes('?')) return true
1211
+ // Multiple clauses with "and" or "also"
1212
+ if (message.includes(' and ') && message.includes('?') && message.length > 100) return true
1213
+ return false
1214
+ }
1215
+
967
1216
  private servicesNotReady(): BrainResponse {
968
1217
  return {
969
1218
  action: 'none',
@@ -975,7 +1224,6 @@ export class BrainRouter {
975
1224
 
976
1225
  /**
977
1226
  * Check if a delete/update message has descriptive content beyond just the command words.
978
- * "forget that" → false (generic), "forget the migrations note" → true (specific)
979
1227
  */
980
1228
  private hasDescriptiveContent(message: string): boolean {
981
1229
  const COMMAND_WORDS = [