claude-brain 0.9.0 → 0.9.2
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 +2 -34
- 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 +11 -0
- package/src/memory/index.ts +32 -0
- package/src/routing/entity-extractor.ts +20 -2
- package/src/routing/intent-classifier.ts +117 -16
- package/src/routing/router.ts +393 -52
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.9.
|
|
1
|
+
0.9.2
|
package/assets/CLAUDE.md
CHANGED
|
@@ -1,35 +1,3 @@
|
|
|
1
|
-
# Claude Brain
|
|
1
|
+
# Claude Brain
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## Session Start (Parallel)
|
|
6
|
-
|
|
7
|
-
Every conversation, immediately call in parallel: `smart_context(project, task)`, `recall_similar(query, min_similarity: 0.3)`, `get_recommendations(context, project)`. If project unknown, also call `list_projects()`. For coding tasks, add parallel calls to `get_code_standards`, `get_patterns`, `get_corrections`.
|
|
8
|
-
|
|
9
|
-
## Before Responding
|
|
10
|
-
|
|
11
|
-
- Project mentioned: `smart_context`
|
|
12
|
-
- Technical topic: `recall_similar`
|
|
13
|
-
- Writing code: `get_code_standards`
|
|
14
|
-
- Debugging: `get_corrections`
|
|
15
|
-
- Architecture: `get_patterns`, `search_knowledge_graph`, `detect_trends`
|
|
16
|
-
- Choosing between options: `what_if_analysis`
|
|
17
|
-
- Recurring topic: `analyze_decision_evolution`, `get_decision_timeline`
|
|
18
|
-
- Cross-project work: `find_cross_project_patterns`
|
|
19
|
-
- Resuming previous work: `get_episode`, `list_episodes`
|
|
20
|
-
|
|
21
|
-
## After Responding
|
|
22
|
-
|
|
23
|
-
- Any recommendation or choice made: `remember_decision(project, context, decision, reasoning)`
|
|
24
|
-
- Reusable solution found: `recognize_pattern(project, type, description)`
|
|
25
|
-
- Mistake or lesson learned: `record_correction(project, original, correction, reasoning)`
|
|
26
|
-
- Task completed: `update_progress(project, completed_task, next_steps)`
|
|
27
|
-
- Memory was retrieved: `rate_memory(memory_id, rating, query)`
|
|
28
|
-
|
|
29
|
-
## New Projects
|
|
30
|
-
|
|
31
|
-
Call `init_project(path)` once for any unregistered project directory to auto-detect tech stack and create project files.
|
|
32
|
-
|
|
33
|
-
## Critical Rules
|
|
34
|
-
|
|
35
|
-
Never answer technical questions without `recall_similar` first. Never recommend without saving via `remember_decision`. Never start project work without `smart_context`. Always call `get_recommendations` before major implementation work. Always call `rate_memory` when memories are retrieved.
|
|
3
|
+
Claude Brain gives you persistent memory across sessions. Just use the `brain` tool naturally — tell it what you're doing, what you decided, or what you need to recall. It handles everything automatically.
|
package/package.json
CHANGED
package/src/config/defaults.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { PartialConfig } from './schema'
|
|
|
3
3
|
/** Default configuration values for Claude Brain */
|
|
4
4
|
export const defaultConfig: PartialConfig = {
|
|
5
5
|
serverName: 'claude-brain',
|
|
6
|
-
serverVersion: '0.9.
|
|
6
|
+
serverVersion: '0.9.2',
|
|
7
7
|
logLevel: 'info',
|
|
8
8
|
logFilePath: './logs/claude-brain.log',
|
|
9
9
|
dbPath: './data/memory.db',
|
package/src/config/schema.ts
CHANGED
|
@@ -270,7 +270,7 @@ export const ConfigSchema = z.object({
|
|
|
270
270
|
serverName: z.string().default('claude-brain'),
|
|
271
271
|
|
|
272
272
|
/** Server version in semver format */
|
|
273
|
-
serverVersion: z.string().regex(/^\d+\.\d+\.\d+$/, 'Version must be semver format').default('0.
|
|
273
|
+
serverVersion: z.string().regex(/^\d+\.\d+\.\d+$/, 'Version must be semver format').default('0.9.2'),
|
|
274
274
|
|
|
275
275
|
/** Logging level */
|
|
276
276
|
logLevel: LogLevelSchema.default('info'),
|
|
@@ -489,8 +489,19 @@ export class ChromaMemoryStore {
|
|
|
489
489
|
|
|
490
490
|
async deleteDecision(id: string): Promise<void> {
|
|
491
491
|
try {
|
|
492
|
+
// Delete from decisions collection
|
|
492
493
|
const collection = await this.collections.getDecisions()
|
|
493
494
|
await collection.delete({ ids: [id] })
|
|
495
|
+
|
|
496
|
+
// ALSO delete from memories collection (dual storage uses same ID)
|
|
497
|
+
try {
|
|
498
|
+
const memoriesCollection = await this.collections.getMemories()
|
|
499
|
+
await memoriesCollection.delete({ ids: [id] })
|
|
500
|
+
this.logger.debug({ id }, 'Decision also deleted from memories collection')
|
|
501
|
+
} catch {
|
|
502
|
+
// Memories collection entry may not exist, that's ok
|
|
503
|
+
}
|
|
504
|
+
|
|
494
505
|
this.logger.info({ id }, 'Decision deleted')
|
|
495
506
|
|
|
496
507
|
} catch (error) {
|
package/src/memory/index.ts
CHANGED
|
@@ -507,6 +507,38 @@ export class MemoryManager {
|
|
|
507
507
|
}
|
|
508
508
|
return this.store.searchCorrections(query, options)
|
|
509
509
|
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Delete a decision by ID — routes to ChromaDB or SQLite
|
|
513
|
+
*/
|
|
514
|
+
async deleteDecision(id: string): Promise<void> {
|
|
515
|
+
if (this.useChromaDB) {
|
|
516
|
+
await this.chroma.store.deleteDecision(id)
|
|
517
|
+
} else {
|
|
518
|
+
this.store.deleteMemory(id)
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Update a decision by storing a new version and deleting the old one
|
|
524
|
+
*/
|
|
525
|
+
async updateDecision(
|
|
526
|
+
oldId: string,
|
|
527
|
+
project: string,
|
|
528
|
+
context: string,
|
|
529
|
+
decision: string,
|
|
530
|
+
reasoning: string,
|
|
531
|
+
options?: { alternatives?: string; tags?: string[] }
|
|
532
|
+
): Promise<string> {
|
|
533
|
+
// Delete old version
|
|
534
|
+
try {
|
|
535
|
+
await this.deleteDecision(oldId)
|
|
536
|
+
} catch {
|
|
537
|
+
// Old ID might not exist, continue with storing new version
|
|
538
|
+
}
|
|
539
|
+
// Store new version
|
|
540
|
+
return this.rememberDecision(project, context, decision, reasoning, options)
|
|
541
|
+
}
|
|
510
542
|
}
|
|
511
543
|
|
|
512
544
|
/**
|
|
@@ -84,6 +84,19 @@ const TECH_ALIASES: Record<string, string> = {
|
|
|
84
84
|
'gql': 'graphql', 'golang': 'go',
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
// Common English words that should never be treated as project names
|
|
88
|
+
const GENERIC_WORDS: Set<string> = new Set([
|
|
89
|
+
'small', 'large', 'big', 'new', 'old', 'good', 'bad', 'best', 'worst',
|
|
90
|
+
'simple', 'complex', 'easy', 'hard', 'fast', 'slow', 'quick', 'all',
|
|
91
|
+
'this', 'that', 'these', 'those', 'some', 'any', 'every', 'each',
|
|
92
|
+
'other', 'another', 'next', 'last', 'first', 'final', 'main', 'side',
|
|
93
|
+
'personal', 'general', 'specific', 'local', 'global', 'internal', 'external',
|
|
94
|
+
'current', 'future', 'past', 'recent', 'data', 'test', 'code', 'file',
|
|
95
|
+
'the', 'for', 'with', 'from', 'into', 'about', 'like', 'just', 'more',
|
|
96
|
+
'production', 'development', 'staging', 'projects', 'apps', 'tools',
|
|
97
|
+
'frontend', 'backend', 'fullstack', 'mobile', 'web', 'desktop', 'cli'
|
|
98
|
+
])
|
|
99
|
+
|
|
87
100
|
export interface BrainExtractedEntities {
|
|
88
101
|
project?: string
|
|
89
102
|
technologies: string[]
|
|
@@ -159,8 +172,13 @@ export class BrainEntityExtractor {
|
|
|
159
172
|
const match = message.match(pattern)
|
|
160
173
|
if (match && match[1]) {
|
|
161
174
|
const candidate = match[1].toLowerCase()
|
|
162
|
-
// Filter out common false positives
|
|
163
|
-
if (
|
|
175
|
+
// Filter out common false positives: tech names, generic words, adjectives
|
|
176
|
+
if (
|
|
177
|
+
!COMMON_TECH.has(candidate) &&
|
|
178
|
+
!GENERIC_WORDS.has(candidate) &&
|
|
179
|
+
candidate.length > 2 &&
|
|
180
|
+
candidate.length < 50
|
|
181
|
+
) {
|
|
164
182
|
return candidate
|
|
165
183
|
}
|
|
166
184
|
}
|
|
@@ -10,12 +10,16 @@ export type Intent =
|
|
|
10
10
|
| 'session_start'
|
|
11
11
|
| 'context_needed'
|
|
12
12
|
| 'decision_made'
|
|
13
|
+
| 'store_this'
|
|
13
14
|
| 'pattern_found'
|
|
14
15
|
| 'mistake_learned'
|
|
15
16
|
| 'progress_update'
|
|
16
17
|
| 'question'
|
|
17
18
|
| 'comparison'
|
|
18
19
|
| 'exploration'
|
|
20
|
+
| 'list_all'
|
|
21
|
+
| 'update_memory'
|
|
22
|
+
| 'delete_memory'
|
|
19
23
|
| 'no_action'
|
|
20
24
|
|
|
21
25
|
export interface ClassificationResult {
|
|
@@ -24,13 +28,30 @@ export interface ClassificationResult {
|
|
|
24
28
|
secondary: Intent[]
|
|
25
29
|
}
|
|
26
30
|
|
|
27
|
-
// Decision-indicating phrases
|
|
31
|
+
// Decision-indicating phrases
|
|
28
32
|
const DECISION_PHRASES = [
|
|
29
33
|
'i recommend', 'you should use', 'the best approach',
|
|
30
34
|
'i suggest', 'better to use', 'prefer using',
|
|
31
35
|
'go with', 'decided to', "let's use", 'we will use',
|
|
32
36
|
'the solution is', 'implement using', 'going with',
|
|
33
|
-
'switching to', 'adopting', 'we chose', 'the plan is to'
|
|
37
|
+
'switching to', 'adopting', 'we chose', 'the plan is to',
|
|
38
|
+
'i decided that', 'decided that', 'choosing', 'we picked',
|
|
39
|
+
'settled on', 'committing to', 'opting for', 'sticking with'
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
// Explicit "store this" phrases — the user wants to persist something
|
|
43
|
+
const STORE_PHRASES = [
|
|
44
|
+
'remember:', 'remember this:', 'remember that',
|
|
45
|
+
'note to self:', 'note to self', 'note:', 'save this:',
|
|
46
|
+
'save this', 'store this', 'keep this', 'record this',
|
|
47
|
+
'i prefer', 'my preference is', 'my convention is',
|
|
48
|
+
'i like to', 'i always', 'always use', 'never use this',
|
|
49
|
+
'from now on', 'going forward', 'the rule is',
|
|
50
|
+
'my approach is', 'my standard is', 'the standard is',
|
|
51
|
+
'we should always', 'we should never',
|
|
52
|
+
'i want to remember', 'don\'t forget',
|
|
53
|
+
'for future reference', 'for the record',
|
|
54
|
+
'important:', 'key decision:', 'takeaway:'
|
|
34
55
|
]
|
|
35
56
|
|
|
36
57
|
const REASONING_PHRASES = [
|
|
@@ -43,16 +64,20 @@ const REASONING_PHRASES = [
|
|
|
43
64
|
const MISTAKE_PHRASES = [
|
|
44
65
|
'bug was', 'the issue was', 'the problem was', 'mistake was',
|
|
45
66
|
'should have', "shouldn't have", 'should not have',
|
|
46
|
-
'lesson learned', "don't use", 'avoid using',
|
|
67
|
+
'lesson learned', "don't use", 'avoid using',
|
|
47
68
|
'the fix is', 'the fix was', 'fixed by', 'solved by',
|
|
48
|
-
'was wrong', 'was broken', 'was incorrect'
|
|
69
|
+
'was wrong', 'was broken', 'was incorrect',
|
|
70
|
+
'gotcha:', 'pitfall:', 'watch out for', 'be careful with',
|
|
71
|
+
'i learned that', 'turns out that'
|
|
49
72
|
]
|
|
50
73
|
|
|
51
74
|
// Progress indicators
|
|
52
75
|
const PROGRESS_PHRASES = [
|
|
53
76
|
'finished', 'completed', 'done with', 'implemented',
|
|
54
77
|
'built', 'created', 'added', 'fixed', 'resolved',
|
|
55
|
-
'shipped', 'deployed', 'merged', 'released'
|
|
78
|
+
'shipped', 'deployed', 'merged', 'released',
|
|
79
|
+
"i'm working on", 'currently working', 'making progress',
|
|
80
|
+
'just did', 'just finished'
|
|
56
81
|
]
|
|
57
82
|
|
|
58
83
|
// Comparison indicators
|
|
@@ -97,6 +122,32 @@ const NO_ACTION_PHRASES = [
|
|
|
97
122
|
'understood', 'noted', 'alright', 'right'
|
|
98
123
|
]
|
|
99
124
|
|
|
125
|
+
// List/browse indicators
|
|
126
|
+
const LIST_PHRASES = [
|
|
127
|
+
'list all', 'show all', 'what decisions',
|
|
128
|
+
'what have i decided', 'all my decisions', 'all decisions',
|
|
129
|
+
'everything about', 'what do i know', 'what do we know',
|
|
130
|
+
'show me everything', 'list decisions', 'list memories',
|
|
131
|
+
'browse', 'dump all', 'show my'
|
|
132
|
+
]
|
|
133
|
+
|
|
134
|
+
// Update/correct existing memory
|
|
135
|
+
const UPDATE_PHRASES = [
|
|
136
|
+
'actually,', 'actually ', 'correction:', 'update that',
|
|
137
|
+
'i changed my mind', 'change that to', 'instead of what i said',
|
|
138
|
+
'supersedes', 'override', 'revise that', 'amend that',
|
|
139
|
+
'that should be', 'update the decision', 'modify that',
|
|
140
|
+
'replace that with', 'no wait', 'scratch that, use'
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
// Delete/forget memory
|
|
144
|
+
const DELETE_PHRASES = [
|
|
145
|
+
'forget that', 'forget about', 'delete that', 'delete the',
|
|
146
|
+
'remove that memory', 'remove that decision', 'that was wrong',
|
|
147
|
+
'discard that', 'erase that', 'undo that decision',
|
|
148
|
+
'remove the memory', 'clear that', 'drop that'
|
|
149
|
+
]
|
|
150
|
+
|
|
100
151
|
export class IntentClassifier {
|
|
101
152
|
/**
|
|
102
153
|
* Classify the intent of a message
|
|
@@ -112,54 +163,75 @@ export class IntentClassifier {
|
|
|
112
163
|
return { primary: 'no_action', confidence: 0.95, secondary: [] }
|
|
113
164
|
}
|
|
114
165
|
|
|
115
|
-
// 2.
|
|
116
|
-
if (this.
|
|
166
|
+
// 2. delete_memory: "forget that", "delete", "remove"
|
|
167
|
+
if (this.isDeleteMemory(lower)) {
|
|
168
|
+
return { primary: 'delete_memory', confidence: 0.90, secondary }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// 3. update_memory: "actually", "correction:", "change that to"
|
|
172
|
+
if (this.isUpdateMemory(lower)) {
|
|
173
|
+
return { primary: 'update_memory', confidence: 0.85, secondary }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// 4. store_this: explicit "remember:", "save this:", "I prefer" (never for questions)
|
|
177
|
+
if (this.isStoreThis(lower, message)) {
|
|
178
|
+
if (this.hasDecisionSignal(lower)) secondary.push('decision_made')
|
|
179
|
+
return { primary: 'store_this', confidence: 0.90, secondary }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 5. decision_made: decision phrases + reasoning (never for questions)
|
|
183
|
+
if (this.isDecisionMade(lower, message)) {
|
|
117
184
|
if (this.hasComparisonSignal(lower)) secondary.push('comparison')
|
|
118
185
|
return { primary: 'decision_made', confidence: 0.85, secondary }
|
|
119
186
|
}
|
|
120
187
|
|
|
121
|
-
//
|
|
188
|
+
// 6. mistake_learned: correction/bug/lesson indicators
|
|
122
189
|
if (this.isMistakeLearned(lower)) {
|
|
123
190
|
return { primary: 'mistake_learned', confidence: 0.85, secondary }
|
|
124
191
|
}
|
|
125
192
|
|
|
126
|
-
//
|
|
193
|
+
// 7. list_all: "list all", "what decisions", "show all"
|
|
194
|
+
if (this.isListAll(lower, message)) {
|
|
195
|
+
return { primary: 'list_all', confidence: 0.85, secondary }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 8. progress_update: completed task indicators (NOT questions)
|
|
127
199
|
if (this.isProgressUpdate(lower, message)) {
|
|
128
200
|
if (this.hasSessionSignal(lower)) secondary.push('session_start')
|
|
129
201
|
return { primary: 'progress_update', confidence: 0.85, secondary }
|
|
130
202
|
}
|
|
131
203
|
|
|
132
|
-
//
|
|
204
|
+
// 9. comparison: vs, which is better, etc.
|
|
133
205
|
if (this.isComparison(lower)) {
|
|
134
206
|
if (this.isQuestion(lower, message)) secondary.push('question')
|
|
135
207
|
return { primary: 'comparison', confidence: 0.85, secondary }
|
|
136
208
|
}
|
|
137
209
|
|
|
138
|
-
//
|
|
210
|
+
// 10. pattern_found: explicit pattern documentation
|
|
139
211
|
if (this.isPatternFound(lower)) {
|
|
140
212
|
return { primary: 'pattern_found', confidence: 0.80, secondary }
|
|
141
213
|
}
|
|
142
214
|
|
|
143
|
-
//
|
|
215
|
+
// 11. session_start: starting/resuming work
|
|
144
216
|
if (this.isSessionStart(lower)) {
|
|
145
217
|
secondary.push('context_needed')
|
|
146
218
|
return { primary: 'session_start', confidence: 0.90, secondary }
|
|
147
219
|
}
|
|
148
220
|
|
|
149
|
-
//
|
|
221
|
+
// 12. exploration: timeline, trends, graph, history
|
|
150
222
|
if (this.isExploration(lower)) {
|
|
151
223
|
if (this.isQuestion(lower, message)) secondary.push('question')
|
|
152
224
|
return { primary: 'exploration', confidence: 0.75, secondary }
|
|
153
225
|
}
|
|
154
226
|
|
|
155
|
-
//
|
|
227
|
+
// 13. question: starts with question word or ends with ?
|
|
156
228
|
if (this.isQuestion(lower, message)) {
|
|
157
229
|
if (this.hasComparisonSignal(lower)) secondary.push('comparison')
|
|
158
230
|
if (this.hasExplorationSignal(lower)) secondary.push('exploration')
|
|
159
231
|
return { primary: 'question', confidence: 0.80, secondary }
|
|
160
232
|
}
|
|
161
233
|
|
|
162
|
-
//
|
|
234
|
+
// 14. Default: context_needed
|
|
163
235
|
return { primary: 'context_needed', confidence: 0.60, secondary }
|
|
164
236
|
}
|
|
165
237
|
|
|
@@ -170,7 +242,20 @@ export class IntentClassifier {
|
|
|
170
242
|
return NO_ACTION_PHRASES.some(p => lower === p || lower === p + '.' || lower === p + '!')
|
|
171
243
|
}
|
|
172
244
|
|
|
173
|
-
private
|
|
245
|
+
private isStoreThis(lower: string, original?: string): boolean {
|
|
246
|
+
// Questions are never storage requests — "Do I prefer X?" is a query, not "I prefer X"
|
|
247
|
+
if (original?.trim().endsWith('?')) return false
|
|
248
|
+
const firstWord = lower.split(/\s+/)[0] || ''
|
|
249
|
+
if (QUESTION_WORDS.includes(firstWord)) return false
|
|
250
|
+
return STORE_PHRASES.some(p => lower.includes(p))
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private isDecisionMade(lower: string, original?: string): boolean {
|
|
254
|
+
// Questions are never decisions — "Did I decide to use X?" is a query
|
|
255
|
+
if (original?.trim().endsWith('?')) return false
|
|
256
|
+
const firstWord = lower.split(/\s+/)[0] || ''
|
|
257
|
+
if (QUESTION_WORDS.includes(firstWord)) return false
|
|
258
|
+
|
|
174
259
|
const hasDecision = DECISION_PHRASES.some(p => lower.includes(p))
|
|
175
260
|
if (!hasDecision) return false
|
|
176
261
|
// Higher confidence if reasoning is also present
|
|
@@ -182,6 +267,10 @@ export class IntentClassifier {
|
|
|
182
267
|
return MISTAKE_PHRASES.some(p => lower.includes(p))
|
|
183
268
|
}
|
|
184
269
|
|
|
270
|
+
private isListAll(lower: string, _original: string): boolean {
|
|
271
|
+
return LIST_PHRASES.some(p => lower.includes(p))
|
|
272
|
+
}
|
|
273
|
+
|
|
185
274
|
private isProgressUpdate(lower: string, original: string): boolean {
|
|
186
275
|
// Must not be a question
|
|
187
276
|
if (original.trim().endsWith('?')) return false
|
|
@@ -214,7 +303,19 @@ export class IntentClassifier {
|
|
|
214
303
|
return QUESTION_WORDS.includes(firstWord)
|
|
215
304
|
}
|
|
216
305
|
|
|
306
|
+
private isUpdateMemory(lower: string): boolean {
|
|
307
|
+
return UPDATE_PHRASES.some(p => lower.includes(p))
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
private isDeleteMemory(lower: string): boolean {
|
|
311
|
+
return DELETE_PHRASES.some(p => lower.includes(p))
|
|
312
|
+
}
|
|
313
|
+
|
|
217
314
|
// Secondary signal checks
|
|
315
|
+
private hasDecisionSignal(lower: string): boolean {
|
|
316
|
+
return DECISION_PHRASES.some(p => lower.includes(p))
|
|
317
|
+
}
|
|
318
|
+
|
|
218
319
|
private hasComparisonSignal(lower: string): boolean {
|
|
219
320
|
return COMPARISON_PHRASES.some(p => lower.includes(p))
|
|
220
321
|
}
|
package/src/routing/router.ts
CHANGED
|
@@ -19,6 +19,9 @@ import {
|
|
|
19
19
|
isServicesInitialized
|
|
20
20
|
} from '@/server/services'
|
|
21
21
|
|
|
22
|
+
/** Default project when none can be detected */
|
|
23
|
+
const DEFAULT_PROJECT = 'general'
|
|
24
|
+
|
|
22
25
|
export interface BrainInput {
|
|
23
26
|
message: string
|
|
24
27
|
project?: string
|
|
@@ -30,6 +33,10 @@ export class BrainRouter {
|
|
|
30
33
|
private responseFilter: ResponseFilter
|
|
31
34
|
private logger: Logger
|
|
32
35
|
|
|
36
|
+
/** Track the most recently stored decision ID for update/delete operations */
|
|
37
|
+
private lastStoredId: string | null = null
|
|
38
|
+
private lastStoredProject: string | null = null
|
|
39
|
+
|
|
33
40
|
constructor(logger: Logger) {
|
|
34
41
|
this.classifier = new IntentClassifier()
|
|
35
42
|
this.entityExtractor = new BrainEntityExtractor()
|
|
@@ -64,6 +71,9 @@ export class BrainRouter {
|
|
|
64
71
|
case 'decision_made':
|
|
65
72
|
return this.handleDecisionMade(message, project, entities)
|
|
66
73
|
|
|
74
|
+
case 'store_this':
|
|
75
|
+
return this.handleStoreThis(message, project, entities)
|
|
76
|
+
|
|
67
77
|
case 'pattern_found':
|
|
68
78
|
return this.handlePatternFound(message, project, entities)
|
|
69
79
|
|
|
@@ -82,6 +92,15 @@ export class BrainRouter {
|
|
|
82
92
|
case 'exploration':
|
|
83
93
|
return this.handleExploration(message, project, entities)
|
|
84
94
|
|
|
95
|
+
case 'list_all':
|
|
96
|
+
return this.handleListAll(message, project, entities)
|
|
97
|
+
|
|
98
|
+
case 'update_memory':
|
|
99
|
+
return this.handleUpdateMemory(message, project, entities)
|
|
100
|
+
|
|
101
|
+
case 'delete_memory':
|
|
102
|
+
return this.handleDeleteMemory(message, project, entities)
|
|
103
|
+
|
|
85
104
|
default:
|
|
86
105
|
return this.handleContextNeeded(message, project, entities)
|
|
87
106
|
}
|
|
@@ -252,35 +271,78 @@ export class BrainRouter {
|
|
|
252
271
|
return this.responseFilter.synthesize(tiers, message, project)
|
|
253
272
|
}
|
|
254
273
|
|
|
255
|
-
|
|
274
|
+
/**
|
|
275
|
+
* Handle explicit "store this" requests — always uses the FULL message as the decision text.
|
|
276
|
+
* This prevents content mangling from entity extraction.
|
|
277
|
+
*/
|
|
278
|
+
private async handleStoreThis(
|
|
256
279
|
message: string,
|
|
257
280
|
project: string | undefined,
|
|
258
281
|
entities: BrainExtractedEntities
|
|
259
282
|
): Promise<BrainResponse> {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
283
|
+
// Use "general" as fallback when no project is detected
|
|
284
|
+
const effectiveProject = project || DEFAULT_PROJECT
|
|
285
|
+
|
|
286
|
+
if (!isServicesInitialized()) {
|
|
287
|
+
return this.servicesNotReady()
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const memory = getMemoryService()
|
|
291
|
+
|
|
292
|
+
// ALWAYS use the full message as the decision — never the extracted fragment
|
|
293
|
+
const decision = message
|
|
294
|
+
const reasoning = entities.reasoning || ''
|
|
295
|
+
const context = entities.topic || message.slice(0, 200)
|
|
296
|
+
|
|
297
|
+
const decisionId = await memory.rememberDecision(
|
|
298
|
+
effectiveProject,
|
|
299
|
+
context,
|
|
300
|
+
decision,
|
|
301
|
+
reasoning,
|
|
302
|
+
{
|
|
303
|
+
alternatives: entities.alternatives,
|
|
304
|
+
tags: entities.technologies.length > 0 ? entities.technologies : ['user-stored']
|
|
266
305
|
}
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
// Track for potential update/delete
|
|
309
|
+
this.lastStoredId = decisionId
|
|
310
|
+
this.lastStoredProject = effectiveProject
|
|
311
|
+
|
|
312
|
+
// Also write to vault
|
|
313
|
+
this.writeToVault(effectiveProject, decision, reasoning, context, entities.alternatives)
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
action: 'stored',
|
|
317
|
+
summary: `Stored: ${message.slice(0, 60)}`,
|
|
318
|
+
content: `Memory stored (ID: ${decisionId})\n\n**Project:** ${effectiveProject}\n**Content:** ${decision}${reasoning ? `\n**Reasoning:** ${reasoning}` : ''}`,
|
|
319
|
+
relevantItems: 1
|
|
267
320
|
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private async handleDecisionMade(
|
|
324
|
+
message: string,
|
|
325
|
+
project: string | undefined,
|
|
326
|
+
entities: BrainExtractedEntities
|
|
327
|
+
): Promise<BrainResponse> {
|
|
328
|
+
// Use "general" as fallback when no project is detected
|
|
329
|
+
const effectiveProject = project || DEFAULT_PROJECT
|
|
268
330
|
|
|
269
331
|
if (!isServicesInitialized()) {
|
|
270
332
|
return this.servicesNotReady()
|
|
271
333
|
}
|
|
272
334
|
|
|
273
335
|
const memory = getMemoryService()
|
|
274
|
-
const vault = getVaultService()
|
|
275
336
|
|
|
276
|
-
//
|
|
277
|
-
|
|
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
|
+
const decision = message
|
|
278
340
|
const reasoning = entities.reasoning || ''
|
|
279
341
|
const context = entities.topic || message.slice(0, 200)
|
|
280
342
|
const alternatives = entities.alternatives
|
|
281
343
|
|
|
282
344
|
const decisionId = await memory.rememberDecision(
|
|
283
|
-
|
|
345
|
+
effectiveProject,
|
|
284
346
|
context,
|
|
285
347
|
decision,
|
|
286
348
|
reasoning,
|
|
@@ -290,20 +352,17 @@ export class BrainRouter {
|
|
|
290
352
|
}
|
|
291
353
|
)
|
|
292
354
|
|
|
355
|
+
// Track for potential update/delete
|
|
356
|
+
this.lastStoredId = decisionId
|
|
357
|
+
this.lastStoredProject = effectiveProject
|
|
358
|
+
|
|
293
359
|
// Also write to vault
|
|
294
|
-
|
|
295
|
-
const projectPaths = vault.getProjectPaths(project)
|
|
296
|
-
const date = new Date().toISOString().split('T')[0]
|
|
297
|
-
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`
|
|
298
|
-
await vault.writer.appendContent(projectPaths.decisions, entry, '\n')
|
|
299
|
-
} catch {
|
|
300
|
-
// Vault write failed — memory storage still succeeded
|
|
301
|
-
}
|
|
360
|
+
this.writeToVault(effectiveProject, decision, reasoning, context, alternatives)
|
|
302
361
|
|
|
303
362
|
return {
|
|
304
363
|
action: 'stored',
|
|
305
|
-
summary: `Stored decision: ${
|
|
306
|
-
content: `Decision stored (ID: ${decisionId})\n\n**Project:** ${
|
|
364
|
+
summary: `Stored decision: ${message.slice(0, 60)}`,
|
|
365
|
+
content: `Decision stored (ID: ${decisionId})\n\n**Project:** ${effectiveProject}\n**Decision:** ${decision}\n**Reasoning:** ${reasoning}`,
|
|
307
366
|
relevantItems: 1
|
|
308
367
|
}
|
|
309
368
|
}
|
|
@@ -313,14 +372,7 @@ export class BrainRouter {
|
|
|
313
372
|
project: string | undefined,
|
|
314
373
|
entities: BrainExtractedEntities
|
|
315
374
|
): Promise<BrainResponse> {
|
|
316
|
-
|
|
317
|
-
return {
|
|
318
|
-
action: 'none',
|
|
319
|
-
summary: 'Cannot store pattern without a project',
|
|
320
|
-
content: 'Please specify which project this pattern relates to.',
|
|
321
|
-
relevantItems: 0
|
|
322
|
-
}
|
|
323
|
-
}
|
|
375
|
+
const effectiveProject = project || DEFAULT_PROJECT
|
|
324
376
|
|
|
325
377
|
if (!isServicesInitialized()) {
|
|
326
378
|
return this.servicesNotReady()
|
|
@@ -328,10 +380,10 @@ export class BrainRouter {
|
|
|
328
380
|
|
|
329
381
|
const memory = getMemoryService()
|
|
330
382
|
const patternType = entities.patternType || 'solution'
|
|
331
|
-
const description =
|
|
383
|
+
const description = message // Use full message
|
|
332
384
|
|
|
333
385
|
const patternId = await memory.storePattern({
|
|
334
|
-
project,
|
|
386
|
+
project: effectiveProject,
|
|
335
387
|
pattern_type: patternType,
|
|
336
388
|
description,
|
|
337
389
|
confidence: 0.8,
|
|
@@ -341,7 +393,7 @@ export class BrainRouter {
|
|
|
341
393
|
return {
|
|
342
394
|
action: 'stored',
|
|
343
395
|
summary: `Stored ${patternType}: ${description.slice(0, 60)}`,
|
|
344
|
-
content: `Pattern stored (ID: ${patternId})\n\n**Type:** ${patternType}\n**Project:** ${
|
|
396
|
+
content: `Pattern stored (ID: ${patternId})\n\n**Type:** ${patternType}\n**Project:** ${effectiveProject}\n**Description:** ${description}`,
|
|
345
397
|
relevantItems: 1
|
|
346
398
|
}
|
|
347
399
|
}
|
|
@@ -351,14 +403,7 @@ export class BrainRouter {
|
|
|
351
403
|
project: string | undefined,
|
|
352
404
|
entities: BrainExtractedEntities
|
|
353
405
|
): Promise<BrainResponse> {
|
|
354
|
-
|
|
355
|
-
return {
|
|
356
|
-
action: 'none',
|
|
357
|
-
summary: 'Cannot store correction without a project',
|
|
358
|
-
content: 'Please specify which project this lesson relates to.',
|
|
359
|
-
relevantItems: 0
|
|
360
|
-
}
|
|
361
|
-
}
|
|
406
|
+
const effectiveProject = project || DEFAULT_PROJECT
|
|
362
407
|
|
|
363
408
|
if (!isServicesInitialized()) {
|
|
364
409
|
return this.servicesNotReady()
|
|
@@ -366,12 +411,13 @@ export class BrainRouter {
|
|
|
366
411
|
|
|
367
412
|
const memory = getMemoryService()
|
|
368
413
|
|
|
414
|
+
// Use full message as the original content to prevent mangling
|
|
369
415
|
const original = entities.original || message
|
|
370
416
|
const correction = entities.correction || ''
|
|
371
417
|
const reasoning = entities.reasoning || 'Lesson learned from experience'
|
|
372
418
|
|
|
373
419
|
const correctionId = await memory.storeCorrection({
|
|
374
|
-
project,
|
|
420
|
+
project: effectiveProject,
|
|
375
421
|
original,
|
|
376
422
|
correction: correction || message,
|
|
377
423
|
reasoning,
|
|
@@ -382,7 +428,7 @@ export class BrainRouter {
|
|
|
382
428
|
return {
|
|
383
429
|
action: 'stored',
|
|
384
430
|
summary: `Stored correction: ${original.slice(0, 60)}`,
|
|
385
|
-
content: `Correction stored (ID: ${correctionId})\n\n**Project:** ${
|
|
431
|
+
content: `Correction stored (ID: ${correctionId})\n\n**Project:** ${effectiveProject}\n**Original:** ${original}\n**Correction:** ${correction || '(see original message)'}`,
|
|
386
432
|
relevantItems: 1
|
|
387
433
|
}
|
|
388
434
|
}
|
|
@@ -392,26 +438,21 @@ export class BrainRouter {
|
|
|
392
438
|
project: string | undefined,
|
|
393
439
|
entities: BrainExtractedEntities
|
|
394
440
|
): Promise<BrainResponse> {
|
|
395
|
-
|
|
396
|
-
return {
|
|
397
|
-
action: 'none',
|
|
398
|
-
summary: 'Cannot update progress without a project',
|
|
399
|
-
content: 'Please specify which project to update progress for.',
|
|
400
|
-
relevantItems: 0
|
|
401
|
-
}
|
|
402
|
-
}
|
|
441
|
+
const effectiveProject = project || DEFAULT_PROJECT
|
|
403
442
|
|
|
404
443
|
if (!isServicesInitialized()) {
|
|
405
444
|
return this.servicesNotReady()
|
|
406
445
|
}
|
|
407
446
|
|
|
408
447
|
const contextService = getContextService()
|
|
448
|
+
const memory = getMemoryService()
|
|
409
449
|
|
|
410
450
|
const completedTask = entities.completedTask || message
|
|
411
451
|
const nextSteps = entities.nextSteps || 'Continue development'
|
|
412
452
|
|
|
453
|
+
// Store in session context
|
|
413
454
|
try {
|
|
414
|
-
await contextService.progress.addCompletedTask(
|
|
455
|
+
await contextService.progress.addCompletedTask(effectiveProject, {
|
|
415
456
|
id: this.generateTaskId(completedTask),
|
|
416
457
|
title: completedTask,
|
|
417
458
|
status: 'done',
|
|
@@ -421,14 +462,280 @@ export class BrainRouter {
|
|
|
421
462
|
// Progress update failed — still report what we found
|
|
422
463
|
}
|
|
423
464
|
|
|
465
|
+
// Also store as a searchable memory in ChromaDB so it appears in search results
|
|
466
|
+
try {
|
|
467
|
+
await memory.rememberDecision(
|
|
468
|
+
effectiveProject,
|
|
469
|
+
`Progress update`,
|
|
470
|
+
`Progress: ${completedTask}${nextSteps !== 'Continue development' ? `. Next: ${nextSteps}` : ''}`,
|
|
471
|
+
'Progress tracking',
|
|
472
|
+
{ tags: ['progress', ...entities.technologies] }
|
|
473
|
+
)
|
|
474
|
+
} catch {
|
|
475
|
+
// Memory storage failed, context-only storage still succeeded
|
|
476
|
+
}
|
|
477
|
+
|
|
424
478
|
return {
|
|
425
479
|
action: 'stored',
|
|
426
480
|
summary: `Progress: ${completedTask.slice(0, 60)}`,
|
|
427
|
-
content: `Progress updated for ${
|
|
481
|
+
content: `Progress updated for ${effectiveProject}\n\n**Completed:** ${completedTask}\n**Next:** ${nextSteps}`,
|
|
482
|
+
relevantItems: 1
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* List all decisions for a project
|
|
488
|
+
*/
|
|
489
|
+
private async handleListAll(
|
|
490
|
+
message: string,
|
|
491
|
+
project: string | undefined,
|
|
492
|
+
entities: BrainExtractedEntities
|
|
493
|
+
): Promise<BrainResponse> {
|
|
494
|
+
if (!isServicesInitialized()) {
|
|
495
|
+
return this.servicesNotReady()
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const memory = getMemoryService()
|
|
499
|
+
const effectiveProject = project // No fallback — list all if no project
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
const decisions = await memory.fetchAllDecisions(effectiveProject)
|
|
503
|
+
const patterns = await memory.fetchAllPatterns(effectiveProject)
|
|
504
|
+
const corrections = await memory.fetchAllCorrections(effectiveProject)
|
|
505
|
+
|
|
506
|
+
const parts: string[] = []
|
|
507
|
+
const projectLabel = effectiveProject || 'all projects'
|
|
508
|
+
|
|
509
|
+
if (decisions.length > 0) {
|
|
510
|
+
parts.push(`## Decisions (${decisions.length})`)
|
|
511
|
+
for (const d of decisions.slice(0, 20)) {
|
|
512
|
+
const decision = d.decision || d.document || d.content || ''
|
|
513
|
+
const date = d.created_at ? ` (${String(d.created_at).split('T')[0]})` : ''
|
|
514
|
+
parts.push(`- ${decision.slice(0, 150)}${date}`)
|
|
515
|
+
}
|
|
516
|
+
if (decisions.length > 20) {
|
|
517
|
+
parts.push(`_...and ${decisions.length - 20} more_`)
|
|
518
|
+
}
|
|
519
|
+
parts.push('')
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (patterns.length > 0) {
|
|
523
|
+
parts.push(`## Patterns (${patterns.length})`)
|
|
524
|
+
for (const p of patterns.slice(0, 10)) {
|
|
525
|
+
const desc = p.description || p.document || p.content || ''
|
|
526
|
+
parts.push(`- **${p.pattern_type || 'solution'}**: ${desc.slice(0, 120)}`)
|
|
527
|
+
}
|
|
528
|
+
parts.push('')
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (corrections.length > 0) {
|
|
532
|
+
parts.push(`## Corrections (${corrections.length})`)
|
|
533
|
+
for (const c of corrections.slice(0, 10)) {
|
|
534
|
+
const orig = c.original || c.document || c.content || ''
|
|
535
|
+
parts.push(`- ${orig.slice(0, 120)}`)
|
|
536
|
+
}
|
|
537
|
+
parts.push('')
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const total = decisions.length + patterns.length + corrections.length
|
|
541
|
+
|
|
542
|
+
if (total === 0) {
|
|
543
|
+
return {
|
|
544
|
+
action: 'none',
|
|
545
|
+
summary: `No memories found for ${projectLabel}`,
|
|
546
|
+
content: `No decisions, patterns, or corrections stored yet for ${projectLabel}.`,
|
|
547
|
+
relevantItems: 0
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return {
|
|
552
|
+
action: 'retrieved',
|
|
553
|
+
summary: `${total} memories for ${projectLabel}`,
|
|
554
|
+
content: parts.join('\n'),
|
|
555
|
+
relevantItems: total
|
|
556
|
+
}
|
|
557
|
+
} catch (error) {
|
|
558
|
+
return {
|
|
559
|
+
action: 'none',
|
|
560
|
+
summary: 'Error listing memories',
|
|
561
|
+
content: `Failed to list memories: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
562
|
+
relevantItems: 0
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Update a previous decision — searches by content to find the right one, then replaces it.
|
|
569
|
+
* Uses lastStoredId only for generic "actually, X" with no descriptive subject.
|
|
570
|
+
*/
|
|
571
|
+
private async handleUpdateMemory(
|
|
572
|
+
message: string,
|
|
573
|
+
project: string | undefined,
|
|
574
|
+
entities: BrainExtractedEntities
|
|
575
|
+
): Promise<BrainResponse> {
|
|
576
|
+
const effectiveProject = project || this.lastStoredProject || DEFAULT_PROJECT
|
|
577
|
+
|
|
578
|
+
if (!isServicesInitialized()) {
|
|
579
|
+
return this.servicesNotReady()
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const memory = getMemoryService()
|
|
583
|
+
const topic = entities.topic || message
|
|
584
|
+
const hasSpecificContent = this.hasDescriptiveContent(message)
|
|
585
|
+
|
|
586
|
+
// If the message has specific content, search for the matching decision first
|
|
587
|
+
if (hasSpecificContent) {
|
|
588
|
+
try {
|
|
589
|
+
const results = await memory.searchRaw(topic, {
|
|
590
|
+
project: effectiveProject,
|
|
591
|
+
limit: 1,
|
|
592
|
+
minSimilarity: 0.3
|
|
593
|
+
})
|
|
594
|
+
|
|
595
|
+
if (results.length > 0 && results[0].id) {
|
|
596
|
+
const oldId = results[0].id
|
|
597
|
+
const newId = await memory.updateDecision(
|
|
598
|
+
oldId,
|
|
599
|
+
effectiveProject,
|
|
600
|
+
`Updated: ${topic.slice(0, 200)}`,
|
|
601
|
+
message,
|
|
602
|
+
entities.reasoning || 'Updated via brain tool',
|
|
603
|
+
{ tags: entities.technologies.length > 0 ? entities.technologies : ['updated'] }
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
this.lastStoredId = newId
|
|
607
|
+
this.lastStoredProject = effectiveProject
|
|
608
|
+
|
|
609
|
+
const oldContent = results[0].decision?.decision || results[0].content?.slice(0, 100) || ''
|
|
610
|
+
return {
|
|
611
|
+
action: 'stored',
|
|
612
|
+
summary: `Updated decision`,
|
|
613
|
+
content: `Replaced: "${oldContent.slice(0, 80)}"\n\nWith:\n**New content:** ${message}`,
|
|
614
|
+
relevantItems: 1
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
} catch {
|
|
618
|
+
// Search failed, fall through
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Generic update ("actually, use X instead") — use lastStoredId
|
|
623
|
+
if (this.lastStoredId) {
|
|
624
|
+
const newId = await memory.updateDecision(
|
|
625
|
+
this.lastStoredId,
|
|
626
|
+
effectiveProject,
|
|
627
|
+
`Updated: ${topic.slice(0, 200)}`,
|
|
628
|
+
message,
|
|
629
|
+
entities.reasoning || 'Updated via brain tool',
|
|
630
|
+
{ tags: entities.technologies.length > 0 ? entities.technologies : ['updated'] }
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
this.lastStoredId = newId
|
|
634
|
+
this.lastStoredProject = effectiveProject
|
|
635
|
+
|
|
636
|
+
return {
|
|
637
|
+
action: 'stored',
|
|
638
|
+
summary: `Updated decision`,
|
|
639
|
+
content: `Previous decision replaced with:\n\n**Project:** ${effectiveProject}\n**New content:** ${message}`,
|
|
640
|
+
relevantItems: 1
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// Fallback: just store as a new decision
|
|
645
|
+
const decisionId = await memory.rememberDecision(
|
|
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}`,
|
|
428
660
|
relevantItems: 1
|
|
429
661
|
}
|
|
430
662
|
}
|
|
431
663
|
|
|
664
|
+
/**
|
|
665
|
+
* Delete a memory — searches by content to find the right one.
|
|
666
|
+
* Only uses lastStoredId for very generic requests like "forget that" with no descriptive words.
|
|
667
|
+
*/
|
|
668
|
+
private async handleDeleteMemory(
|
|
669
|
+
message: string,
|
|
670
|
+
project: string | undefined,
|
|
671
|
+
entities: BrainExtractedEntities
|
|
672
|
+
): Promise<BrainResponse> {
|
|
673
|
+
const effectiveProject = project || this.lastStoredProject || DEFAULT_PROJECT
|
|
674
|
+
|
|
675
|
+
if (!isServicesInitialized()) {
|
|
676
|
+
return this.servicesNotReady()
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const memory = getMemoryService()
|
|
680
|
+
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
|
+
const hasSpecificContent = this.hasDescriptiveContent(message)
|
|
685
|
+
|
|
686
|
+
// If the user described what to delete, ALWAYS search by content first
|
|
687
|
+
if (hasSpecificContent) {
|
|
688
|
+
try {
|
|
689
|
+
const results = await memory.searchRaw(topic, {
|
|
690
|
+
project: effectiveProject,
|
|
691
|
+
limit: 3,
|
|
692
|
+
minSimilarity: 0.3
|
|
693
|
+
})
|
|
694
|
+
|
|
695
|
+
if (results.length > 0 && results[0].id) {
|
|
696
|
+
const targetId = results[0].id
|
|
697
|
+
const content = results[0].decision?.decision || results[0].content?.slice(0, 100) || ''
|
|
698
|
+
|
|
699
|
+
await memory.deleteDecision(targetId)
|
|
700
|
+
if (this.lastStoredId === targetId) this.lastStoredId = null
|
|
701
|
+
|
|
702
|
+
return {
|
|
703
|
+
action: 'stored',
|
|
704
|
+
summary: `Deleted memory`,
|
|
705
|
+
content: `Deleted: "${content.slice(0, 100)}" (ID: ${targetId})`,
|
|
706
|
+
relevantItems: 0
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
} catch {
|
|
710
|
+
// Search failed, fall through
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// Generic request ("forget that", "delete that") — use lastStoredId
|
|
715
|
+
if (this.lastStoredId) {
|
|
716
|
+
try {
|
|
717
|
+
await memory.deleteDecision(this.lastStoredId)
|
|
718
|
+
const deletedId = this.lastStoredId
|
|
719
|
+
this.lastStoredId = null
|
|
720
|
+
return {
|
|
721
|
+
action: 'stored',
|
|
722
|
+
summary: `Deleted most recent memory`,
|
|
723
|
+
content: `Deleted memory (ID: ${deletedId})`,
|
|
724
|
+
relevantItems: 0
|
|
725
|
+
}
|
|
726
|
+
} catch {
|
|
727
|
+
// Deletion failed
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
return {
|
|
732
|
+
action: 'none',
|
|
733
|
+
summary: 'No matching memory found to delete',
|
|
734
|
+
content: 'Could not find a memory matching your request. Try being more specific about what to delete.',
|
|
735
|
+
relevantItems: 0
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
432
739
|
private async handleQuestion(
|
|
433
740
|
message: string,
|
|
434
741
|
project: string | undefined,
|
|
@@ -612,6 +919,24 @@ export class BrainRouter {
|
|
|
612
919
|
|
|
613
920
|
// ===== Helpers =====
|
|
614
921
|
|
|
922
|
+
private async writeToVault(
|
|
923
|
+
project: string,
|
|
924
|
+
decision: string,
|
|
925
|
+
reasoning: string,
|
|
926
|
+
context: string,
|
|
927
|
+
alternatives?: string
|
|
928
|
+
): Promise<void> {
|
|
929
|
+
try {
|
|
930
|
+
const vault = getVaultService()
|
|
931
|
+
const projectPaths = vault.getProjectPaths(project)
|
|
932
|
+
const date = new Date().toISOString().split('T')[0]
|
|
933
|
+
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
|
+
await vault.writer.appendContent(projectPaths.decisions, entry, '\n')
|
|
935
|
+
} catch {
|
|
936
|
+
// Vault write failed — memory storage still succeeded
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
615
940
|
private async addExplorationResults(
|
|
616
941
|
query: string,
|
|
617
942
|
_project: string | undefined,
|
|
@@ -646,6 +971,22 @@ export class BrainRouter {
|
|
|
646
971
|
}
|
|
647
972
|
}
|
|
648
973
|
|
|
974
|
+
/**
|
|
975
|
+
* 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
|
+
*/
|
|
978
|
+
private hasDescriptiveContent(message: string): boolean {
|
|
979
|
+
const COMMAND_WORDS = [
|
|
980
|
+
'forget', 'delete', 'remove', 'discard', 'erase', 'undo', 'clear', 'drop',
|
|
981
|
+
'actually', 'correction', 'update', 'change', 'replace', 'modify', 'revise',
|
|
982
|
+
'amend', 'override', 'scratch', 'no', 'wait', 'instead',
|
|
983
|
+
'that', 'this', 'the', 'it', 'about', 'memory', 'decision', 'to', 'a', 'an'
|
|
984
|
+
]
|
|
985
|
+
const words = message.toLowerCase().split(/[\s,;:.!?]+/).filter(w => w.length > 1)
|
|
986
|
+
const meaningfulWords = words.filter(w => !COMMAND_WORDS.includes(w))
|
|
987
|
+
return meaningfulWords.length >= 2
|
|
988
|
+
}
|
|
989
|
+
|
|
649
990
|
private generateTaskId(title: string): string {
|
|
650
991
|
return title
|
|
651
992
|
.toLowerCase()
|