claude-brain 0.9.2 → 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 +29 -15
- package/src/routing/intent-classifier.ts +94 -6
- package/src/routing/response-filter.ts +50 -17
- package/src/routing/router.ts +447 -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,19 @@ 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
|
-
|
|
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) {
|
|
651
|
+
const oldId = matchId
|
|
597
652
|
const newId = await memory.updateDecision(
|
|
598
653
|
oldId,
|
|
599
654
|
effectiveProject,
|
|
@@ -606,20 +661,26 @@ export class BrainRouter {
|
|
|
606
661
|
this.lastStoredId = newId
|
|
607
662
|
this.lastStoredProject = effectiveProject
|
|
608
663
|
|
|
609
|
-
|
|
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) || ''
|
|
610
668
|
return {
|
|
611
669
|
action: 'stored',
|
|
612
670
|
summary: `Updated decision`,
|
|
613
671
|
content: `Replaced: "${oldContent.slice(0, 80)}"\n\nWith:\n**New content:** ${message}`,
|
|
614
672
|
relevantItems: 1
|
|
615
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)')
|
|
616
677
|
}
|
|
617
678
|
} catch {
|
|
618
679
|
// Search failed, fall through
|
|
619
680
|
}
|
|
620
681
|
}
|
|
621
682
|
|
|
622
|
-
// Generic update
|
|
683
|
+
// Generic update — use lastStoredId
|
|
623
684
|
if (this.lastStoredId) {
|
|
624
685
|
const newId = await memory.updateDecision(
|
|
625
686
|
this.lastStoredId,
|
|
@@ -632,6 +693,7 @@ export class BrainRouter {
|
|
|
632
693
|
|
|
633
694
|
this.lastStoredId = newId
|
|
634
695
|
this.lastStoredProject = effectiveProject
|
|
696
|
+
this.searchEngine.invalidateCache(effectiveProject)
|
|
635
697
|
|
|
636
698
|
return {
|
|
637
699
|
action: 'stored',
|
|
@@ -641,29 +703,12 @@ export class BrainRouter {
|
|
|
641
703
|
}
|
|
642
704
|
}
|
|
643
705
|
|
|
644
|
-
// Fallback:
|
|
645
|
-
|
|
646
|
-
effectiveProject,
|
|
647
|
-
topic.slice(0, 200),
|
|
648
|
-
message,
|
|
649
|
-
entities.reasoning || '',
|
|
650
|
-
{ tags: entities.technologies.length > 0 ? entities.technologies : ['updated'] }
|
|
651
|
-
)
|
|
652
|
-
|
|
653
|
-
this.lastStoredId = decisionId
|
|
654
|
-
this.lastStoredProject = effectiveProject
|
|
655
|
-
|
|
656
|
-
return {
|
|
657
|
-
action: 'stored',
|
|
658
|
-
summary: `Stored as new (no matching decision found to update)`,
|
|
659
|
-
content: `Stored as new decision (ID: ${decisionId})\n\n**Project:** ${effectiveProject}\n**Content:** ${message}`,
|
|
660
|
-
relevantItems: 1
|
|
661
|
-
}
|
|
706
|
+
// Fallback: store as new decision
|
|
707
|
+
return this.storeAsNew(memory, effectiveProject, message, topic, entities, 'No matching decision found to update')
|
|
662
708
|
}
|
|
663
709
|
|
|
664
710
|
/**
|
|
665
|
-
*
|
|
666
|
-
* Only uses lastStoredId for very generic requests like "forget that" with no descriptive words.
|
|
711
|
+
* Phase 19 D4: Delete with higher similarity threshold
|
|
667
712
|
*/
|
|
668
713
|
private async handleDeleteMemory(
|
|
669
714
|
message: string,
|
|
@@ -678,45 +723,56 @@ export class BrainRouter {
|
|
|
678
723
|
|
|
679
724
|
const memory = getMemoryService()
|
|
680
725
|
const topic = entities.topic || message
|
|
681
|
-
|
|
682
|
-
// Check if the message has meaningful content to search by
|
|
683
|
-
// (strip common delete phrases to see if there's a real subject)
|
|
684
726
|
const hasSpecificContent = this.hasDescriptiveContent(message)
|
|
685
727
|
|
|
686
|
-
// If the user described what to delete, ALWAYS search by content first
|
|
687
728
|
if (hasSpecificContent) {
|
|
688
729
|
try {
|
|
689
730
|
const results = await memory.searchRaw(topic, {
|
|
690
731
|
project: effectiveProject,
|
|
691
732
|
limit: 3,
|
|
692
|
-
minSimilarity:
|
|
733
|
+
minSimilarity: DELETE_MIN_SIMILARITY
|
|
693
734
|
})
|
|
694
735
|
|
|
695
|
-
|
|
696
|
-
|
|
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) {
|
|
740
|
+
const targetId = matchId
|
|
741
|
+
const content = results[0].decision?.decision || results[0].memory?.content?.slice(0, 100) || results[0].content?.slice(0, 100) || ''
|
|
698
742
|
|
|
699
743
|
await memory.deleteDecision(targetId)
|
|
700
744
|
if (this.lastStoredId === targetId) this.lastStoredId = null
|
|
701
745
|
|
|
746
|
+
// Invalidate cache
|
|
747
|
+
this.searchEngine.invalidateCache(effectiveProject)
|
|
748
|
+
|
|
702
749
|
return {
|
|
703
750
|
action: 'stored',
|
|
704
751
|
summary: `Deleted memory`,
|
|
705
752
|
content: `Deleted: "${content.slice(0, 100)}" (ID: ${targetId})`,
|
|
706
753
|
relevantItems: 0
|
|
707
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
|
+
}
|
|
708
763
|
}
|
|
709
764
|
} catch {
|
|
710
765
|
// Search failed, fall through
|
|
711
766
|
}
|
|
712
767
|
}
|
|
713
768
|
|
|
714
|
-
// Generic request
|
|
769
|
+
// Generic request — use lastStoredId
|
|
715
770
|
if (this.lastStoredId) {
|
|
716
771
|
try {
|
|
717
772
|
await memory.deleteDecision(this.lastStoredId)
|
|
718
773
|
const deletedId = this.lastStoredId
|
|
719
774
|
this.lastStoredId = null
|
|
775
|
+
this.searchEngine.invalidateCache(effectiveProject)
|
|
720
776
|
return {
|
|
721
777
|
action: 'stored',
|
|
722
778
|
summary: `Deleted most recent memory`,
|
|
@@ -736,6 +792,14 @@ export class BrainRouter {
|
|
|
736
792
|
}
|
|
737
793
|
}
|
|
738
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
|
+
*/
|
|
739
803
|
private async handleQuestion(
|
|
740
804
|
message: string,
|
|
741
805
|
project: string | undefined,
|
|
@@ -746,51 +810,67 @@ export class BrainRouter {
|
|
|
746
810
|
return this.servicesNotReady()
|
|
747
811
|
}
|
|
748
812
|
|
|
749
|
-
const memory = getMemoryService()
|
|
750
813
|
const query = entities.topic || message
|
|
751
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
|
+
}
|
|
752
845
|
|
|
753
|
-
|
|
754
|
-
const [rawResults, patternResults, correctionResults] = await Promise.all([
|
|
755
|
-
memory.searchRaw(query, {
|
|
756
|
-
project,
|
|
757
|
-
limit: 5,
|
|
758
|
-
minSimilarity: 0.3
|
|
759
|
-
}).catch(() => [] as any[]),
|
|
760
|
-
memory.searchPatterns(query, {
|
|
761
|
-
project,
|
|
762
|
-
limit: 3,
|
|
763
|
-
minSimilarity: 0.3
|
|
764
|
-
}).catch(() => [] as any[]),
|
|
765
|
-
memory.searchCorrections(query, {
|
|
766
|
-
project,
|
|
767
|
-
limit: 3,
|
|
768
|
-
minSimilarity: 0.3
|
|
769
|
-
}).catch(() => [] as any[])
|
|
770
|
-
])
|
|
771
|
-
|
|
772
|
-
if (rawResults.length > 0) {
|
|
846
|
+
if (searchResults.length > 0) {
|
|
773
847
|
tiers.push({
|
|
774
848
|
label: 'Memories',
|
|
775
|
-
results:
|
|
776
|
-
content: r.
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
source: 'Past Decision',
|
|
781
|
-
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>
|
|
782
854
|
}))
|
|
783
855
|
})
|
|
784
856
|
}
|
|
785
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
|
+
|
|
786
866
|
if (patternResults.length > 0) {
|
|
787
867
|
tiers.push({
|
|
788
868
|
label: 'Patterns',
|
|
789
|
-
results: patternResults.map(
|
|
790
|
-
content: p.
|
|
791
|
-
score: p.
|
|
792
|
-
source: `Pattern
|
|
793
|
-
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>
|
|
794
874
|
}))
|
|
795
875
|
})
|
|
796
876
|
}
|
|
@@ -798,24 +878,64 @@ export class BrainRouter {
|
|
|
798
878
|
if (correctionResults.length > 0) {
|
|
799
879
|
tiers.push({
|
|
800
880
|
label: 'Corrections',
|
|
801
|
-
results: correctionResults.map(
|
|
802
|
-
content:
|
|
803
|
-
score: c.
|
|
881
|
+
results: correctionResults.map(c => ({
|
|
882
|
+
content: c.content,
|
|
883
|
+
score: c.score,
|
|
804
884
|
source: 'Lesson Learned',
|
|
805
|
-
metadata: c.metadata
|
|
885
|
+
metadata: c.metadata as Record<string, unknown>
|
|
806
886
|
}))
|
|
807
887
|
})
|
|
808
888
|
}
|
|
809
889
|
|
|
810
|
-
//
|
|
811
|
-
if (
|
|
812
|
-
|
|
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
|
+
})
|
|
813
901
|
}
|
|
814
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
|
+
|
|
815
929
|
return this.responseFilter.synthesize(tiers, message, project)
|
|
816
930
|
}
|
|
817
931
|
|
|
818
|
-
|
|
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(
|
|
819
939
|
message: string,
|
|
820
940
|
project: string | undefined,
|
|
821
941
|
entities: BrainExtractedEntities
|
|
@@ -824,58 +944,106 @@ export class BrainRouter {
|
|
|
824
944
|
return this.servicesNotReady()
|
|
825
945
|
}
|
|
826
946
|
|
|
827
|
-
const memory = getMemoryService()
|
|
828
947
|
const query = entities.topic || message
|
|
948
|
+
const lower = message.toLowerCase()
|
|
829
949
|
const tiers: TierResults[] = []
|
|
830
950
|
|
|
831
|
-
//
|
|
832
|
-
|
|
833
|
-
const
|
|
951
|
+
// C6: Timeline queries
|
|
952
|
+
if (lower.includes('timeline') || lower.includes('chronological') || lower.includes('history of')) {
|
|
953
|
+
const timeline = await this.searchEngine.buildTimeline({
|
|
834
954
|
project,
|
|
835
|
-
|
|
836
|
-
|
|
955
|
+
topic: query,
|
|
956
|
+
limit: 20
|
|
837
957
|
})
|
|
838
958
|
|
|
839
|
-
if (
|
|
959
|
+
if (timeline?.entries?.length) {
|
|
840
960
|
tiers.push({
|
|
841
|
-
label: '
|
|
842
|
-
results:
|
|
843
|
-
content:
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
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 || {}
|
|
848
967
|
}))
|
|
849
968
|
})
|
|
850
969
|
}
|
|
851
|
-
} catch {
|
|
852
|
-
// Search failed
|
|
853
970
|
}
|
|
854
971
|
|
|
855
|
-
//
|
|
856
|
-
|
|
857
|
-
const
|
|
858
|
-
if (
|
|
859
|
-
const
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
content: `**${n.label || n.name}** (${n.type})\n${n.metadata?.description || ''}`,
|
|
865
|
-
score: n.score || 0.5,
|
|
866
|
-
source: 'Knowledge Graph'
|
|
867
|
-
}))
|
|
868
|
-
})
|
|
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}`)
|
|
869
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
|
+
})
|
|
870
992
|
}
|
|
871
|
-
}
|
|
872
|
-
|
|
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
|
+
})
|
|
873
1041
|
}
|
|
874
1042
|
|
|
875
1043
|
return this.responseFilter.synthesize(tiers, message, project, 'analyzed')
|
|
876
1044
|
}
|
|
877
1045
|
|
|
878
|
-
private async
|
|
1046
|
+
private async handleComparison(
|
|
879
1047
|
message: string,
|
|
880
1048
|
project: string | undefined,
|
|
881
1049
|
entities: BrainExtractedEntities
|
|
@@ -887,31 +1055,36 @@ export class BrainRouter {
|
|
|
887
1055
|
const query = entities.topic || message
|
|
888
1056
|
const tiers: TierResults[] = []
|
|
889
1057
|
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
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
|
+
}))
|
|
899
1073
|
})
|
|
1074
|
+
}
|
|
900
1075
|
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
})
|
|
912
|
-
}
|
|
913
|
-
} catch {
|
|
914
|
-
// 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
|
+
})
|
|
915
1088
|
}
|
|
916
1089
|
|
|
917
1090
|
return this.responseFilter.synthesize(tiers, message, project, 'analyzed')
|
|
@@ -933,35 +1106,89 @@ export class BrainRouter {
|
|
|
933
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`
|
|
934
1107
|
await vault.writer.appendContent(projectPaths.decisions, entry, '\n')
|
|
935
1108
|
} catch {
|
|
936
|
-
// Vault write failed
|
|
1109
|
+
// Vault write failed
|
|
937
1110
|
}
|
|
938
1111
|
}
|
|
939
1112
|
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
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 {
|
|
945
1148
|
try {
|
|
946
|
-
const
|
|
947
|
-
if (
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
results: graphResults.nodes.map((n: any) => ({
|
|
953
|
-
content: `**${n.label || n.name}** (${n.type})${n.metadata?.description ? `\n${n.metadata.description}` : ''}`,
|
|
954
|
-
score: n.score || 0.5,
|
|
955
|
-
source: 'Knowledge Graph'
|
|
956
|
-
}))
|
|
957
|
-
})
|
|
958
|
-
}
|
|
959
|
-
}
|
|
1149
|
+
const episodeManager = getEpisodeService()
|
|
1150
|
+
if (!episodeManager) return
|
|
1151
|
+
episodeManager.processMessage(
|
|
1152
|
+
{ role, content: message, timestamp: new Date().toISOString() },
|
|
1153
|
+
project
|
|
1154
|
+
).catch(() => {})
|
|
960
1155
|
} catch {
|
|
961
|
-
//
|
|
1156
|
+
// Episode manager not available
|
|
962
1157
|
}
|
|
963
1158
|
}
|
|
964
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
|
+
|
|
965
1192
|
private servicesNotReady(): BrainResponse {
|
|
966
1193
|
return {
|
|
967
1194
|
action: 'none',
|
|
@@ -973,7 +1200,6 @@ export class BrainRouter {
|
|
|
973
1200
|
|
|
974
1201
|
/**
|
|
975
1202
|
* Check if a delete/update message has descriptive content beyond just the command words.
|
|
976
|
-
* "forget that" → false (generic), "forget the migrations note" → true (specific)
|
|
977
1203
|
*/
|
|
978
1204
|
private hasDescriptiveContent(message: string): boolean {
|
|
979
1205
|
const COMMAND_WORDS = [
|