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.
- package/VERSION +1 -1
- package/assets/CLAUDE.md +9 -1
- package/package.json +1 -1
- package/src/automation/phase12-manager.ts +456 -0
- package/src/automation/project-detector.ts +13 -0
- package/src/automation/repo-scanner.ts +205 -0
- package/src/cli/bin.ts +30 -0
- package/src/cli/commands/git-hook.ts +189 -0
- package/src/cli/commands/hooks.ts +8 -9
- package/src/cli/commands/init.ts +98 -0
- package/src/cli/commands/serve.ts +7 -20
- package/src/cli/commands/update.ts +3 -3
- package/src/config/defaults.ts +4 -1
- package/src/config/schema.ts +27 -7
- package/src/hooks/brain-hook.ts +8 -6
- package/src/hooks/capture.ts +9 -2
- package/src/hooks/git-capture.ts +94 -0
- package/src/hooks/git-hook-installer.ts +197 -0
- package/src/hooks/index.ts +1 -0
- package/src/hooks/session-tracker.ts +79 -3
- package/src/hooks/types.ts +1 -1
- package/src/intelligence/index.ts +24 -0
- package/src/knowledge/graph/builder.ts +26 -0
- package/src/memory/chroma/store.ts +18 -2
- package/src/memory/episodic/manager.ts +17 -0
- package/src/memory/index.ts +48 -18
- package/src/phase12/index.ts +3 -454
- package/src/routing/intent-classifier.ts +107 -9
- package/src/routing/response-filter.ts +50 -17
- package/src/routing/router.ts +472 -224
- package/src/routing/search-engine.ts +464 -0
- package/src/routing/types.ts +84 -0
- package/src/server/handlers/call-tool.ts +4 -49
- package/src/server/handlers/tools/analyze-decision-evolution.ts +1 -1
- package/src/server/handlers/tools/detect-trends.ts +1 -1
- package/src/server/handlers/tools/find-cross-project-patterns.ts +1 -1
- package/src/server/handlers/tools/get-decision-timeline.ts +2 -2
- package/src/server/handlers/tools/get-recommendations.ts +1 -1
- package/src/server/handlers/tools/index.ts +5 -7
- package/src/server/handlers/tools/what-if-analysis.ts +1 -1
- package/src/server/providers/resources.ts +195 -0
- package/src/server/services.ts +81 -6
- package/src/tools/schemas.ts +7 -329
- package/src/utils/phase12-helper.ts +2 -2
- package/src/utils/timing.ts +47 -0
- package/src/vault/writer.ts +22 -2
- /package/src/{cross-project → intelligence/cross-project}/affinity.ts +0 -0
- /package/src/{cross-project → intelligence/cross-project}/generalizer.ts +0 -0
- /package/src/{cross-project → intelligence/cross-project}/index.ts +0 -0
- /package/src/{cross-project → intelligence/cross-project}/transfer.ts +0 -0
- /package/src/{optimization → intelligence/optimization}/index.ts +0 -0
- /package/src/{optimization → intelligence/optimization}/precompute.ts +0 -0
- /package/src/{optimization → intelligence/optimization}/semantic-cache.ts +0 -0
- /package/src/{prediction → intelligence/prediction}/context-anticipator.ts +0 -0
- /package/src/{prediction → intelligence/prediction}/decision-predictor.ts +0 -0
- /package/src/{prediction → intelligence/prediction}/index.ts +0 -0
- /package/src/{prediction → intelligence/prediction}/recommender.ts +0 -0
- /package/src/{reasoning → intelligence/reasoning}/chain-retrieval.ts +0 -0
- /package/src/{reasoning → intelligence/reasoning}/counterfactual.ts +0 -0
- /package/src/{reasoning → intelligence/reasoning}/index.ts +0 -0
- /package/src/{reasoning → intelligence/reasoning}/synthesizer.ts +0 -0
- /package/src/{temporal → intelligence/temporal}/evolution.ts +0 -0
- /package/src/{temporal → intelligence/temporal}/index.ts +0 -0
- /package/src/{temporal → intelligence/temporal}/query-processor.ts +0 -0
- /package/src/{temporal → intelligence/temporal}/timeline.ts +0 -0
- /package/src/{temporal → intelligence/temporal}/trends.ts +0 -0
package/src/routing/router.ts
CHANGED
|
@@ -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
|
|
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(
|
|
194
|
-
parts.push(`**[${similarity}%]** ${r.
|
|
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
|
-
|
|
227
|
-
|
|
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: '
|
|
259
|
-
results:
|
|
260
|
-
content:
|
|
261
|
-
score:
|
|
262
|
-
source:
|
|
263
|
-
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
|
-
}
|
|
268
|
-
|
|
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
|
-
//
|
|
313
|
-
this.
|
|
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
|
-
//
|
|
360
|
-
this.
|
|
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
|
|
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
|
|
511
|
+
// Progress update failed
|
|
463
512
|
}
|
|
464
513
|
|
|
465
|
-
// Also store as a searchable memory
|
|
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
|
|
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
|
|
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
|
-
*
|
|
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:
|
|
645
|
+
minSimilarity: UPDATE_MIN_SIMILARITY
|
|
593
646
|
})
|
|
594
647
|
|
|
595
|
-
const matchId = results[0]?.memory?.id || results[0]?.decision?.id
|
|
596
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
}
|
|
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
|
-
*
|
|
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:
|
|
734
|
+
minSimilarity: DELETE_MIN_SIMILARITY
|
|
694
735
|
})
|
|
695
736
|
|
|
696
|
-
const matchId = results[0]?.memory?.id || results[0]?.decision?.id
|
|
697
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
778
|
-
content: r.
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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(
|
|
792
|
-
content: p.
|
|
793
|
-
score: p.
|
|
794
|
-
source: `Pattern
|
|
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(
|
|
804
|
-
content:
|
|
805
|
-
score: c.
|
|
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
|
-
//
|
|
813
|
-
if (
|
|
814
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
834
|
-
|
|
835
|
-
const
|
|
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
|
-
|
|
838
|
-
|
|
956
|
+
topic: query,
|
|
957
|
+
limit: 20
|
|
839
958
|
})
|
|
840
959
|
|
|
841
|
-
if (
|
|
960
|
+
if (timeline?.entries?.length) {
|
|
842
961
|
tiers.push({
|
|
843
|
-
label: '
|
|
844
|
-
results:
|
|
845
|
-
content:
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
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
|
-
//
|
|
858
|
-
|
|
859
|
-
const
|
|
860
|
-
if (
|
|
861
|
-
const
|
|
862
|
-
|
|
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: '
|
|
865
|
-
results:
|
|
866
|
-
content:
|
|
867
|
-
score:
|
|
868
|
-
source: '
|
|
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
|
-
}
|
|
874
|
-
|
|
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
|
|
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
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
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
|
|
1133
|
+
// Vault write failed
|
|
939
1134
|
}
|
|
940
1135
|
}
|
|
941
1136
|
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
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
|
|
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
|
-
}
|
|
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
|
-
//
|
|
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 = [
|