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