claude-brain 0.30.2 → 0.30.3
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/README.md +241 -191
- package/VERSION +1 -1
- package/assets/CLAUDE-unified.md +11 -11
- package/assets/CLAUDE.md +29 -29
- package/package.json +7 -3
- package/packs/backend/node.json +173 -173
- package/packs/core/javascript.json +176 -176
- package/packs/core/typescript.json +222 -222
- package/packs/frontend/react.json +254 -254
- package/packs/meta/testing.json +172 -172
- package/scripts/postinstall.mjs +531 -531
- package/src/automation/decision-detector.ts +452 -452
- package/src/automation/phase12-manager.ts +456 -456
- package/src/automation/proactive-recall.ts +373 -373
- package/src/automation/project-detector.ts +310 -310
- package/src/automation/repo-scanner.ts +210 -205
- package/src/cli/auto-setup.ts +75 -75
- package/src/cli/auto-start.ts +266 -266
- package/src/cli/bin.ts +264 -264
- package/src/cli/commands/autostart.ts +90 -90
- package/src/cli/commands/chroma.ts +578 -577
- package/src/cli/commands/export-training.ts +70 -70
- package/src/cli/commands/export.ts +130 -130
- package/src/cli/commands/git-hook.ts +183 -183
- package/src/cli/commands/hooks.ts +217 -217
- package/src/cli/commands/init.ts +123 -123
- package/src/cli/commands/install-mcp.ts +122 -111
- package/src/cli/commands/models.ts +979 -979
- package/src/cli/commands/pack.ts +200 -200
- package/src/cli/commands/refresh.ts +344 -339
- package/src/cli/commands/reindex.ts +120 -120
- package/src/cli/commands/serve.ts +466 -463
- package/src/cli/commands/start.ts +44 -44
- package/src/cli/commands/status.ts +220 -203
- package/src/cli/commands/uninstall-mcp.ts +45 -41
- package/src/cli/commands/update.ts +130 -124
- package/src/cli/migrate-chroma.ts +106 -106
- package/src/cli/ui/animations.ts +80 -80
- package/src/cli/ui/components.ts +82 -82
- package/src/cli/ui/index.ts +4 -4
- package/src/cli/ui/logo.ts +36 -36
- package/src/cli/ui/theme.ts +55 -55
- package/src/code-intelligence/indexer.ts +352 -352
- package/src/code-intelligence/linker.ts +178 -178
- package/src/code-intelligence/parser.ts +484 -484
- package/src/code-intelligence/query.ts +291 -291
- package/src/code-intelligence/schema.ts +83 -83
- package/src/code-intelligence/types.ts +95 -95
- package/src/config/defaults.ts +52 -52
- package/src/config/home.ts +56 -56
- package/src/config/index.ts +5 -5
- package/src/config/loader.ts +192 -192
- package/src/config/schema.ts +446 -415
- package/src/config/validator.ts +182 -182
- package/src/context/assembler.ts +407 -400
- package/src/context/index.ts +79 -79
- package/src/context/progress-tracker.ts +174 -174
- package/src/context/standards-manager.ts +287 -287
- package/src/context/validator.ts +58 -58
- package/src/diagnostics/index.ts +122 -121
- package/src/health/index.ts +233 -232
- package/src/hooks/brain-hook.ts +134 -131
- package/src/hooks/capture.ts +168 -168
- package/src/hooks/claude-code-mastery.md +112 -112
- package/src/hooks/context-hook.ts +260 -245
- package/src/hooks/deduplicator.ts +72 -72
- package/src/hooks/git-capture.ts +109 -109
- package/src/hooks/git-hook-installer.ts +211 -207
- package/src/hooks/index.ts +20 -20
- package/src/hooks/installer.ts +306 -288
- package/src/hooks/interceptor-hook.ts +204 -201
- package/src/hooks/passive-classifier.ts +397 -397
- package/src/hooks/queue.ts +160 -129
- package/src/hooks/session-tracker.ts +312 -312
- package/src/hooks/types.ts +52 -52
- package/src/index.ts +7 -7
- package/src/intelligence/cross-project/generalizer.ts +283 -283
- package/src/intelligence/cross-project/index.ts +7 -7
- package/src/intelligence/hf-downloader.ts +222 -222
- package/src/intelligence/hf-manifest.json +78 -78
- package/src/intelligence/index.ts +24 -24
- package/src/intelligence/inference-router.ts +762 -762
- package/src/intelligence/model-manager.ts +263 -245
- package/src/intelligence/optimization/index.ts +10 -10
- package/src/intelligence/optimization/precompute.ts +202 -202
- package/src/intelligence/optimization/semantic-cache.ts +213 -207
- package/src/intelligence/prediction/index.ts +7 -7
- package/src/intelligence/prediction/recommender.ts +276 -268
- package/src/intelligence/reasoning/chain-retrieval.ts +243 -247
- package/src/intelligence/reasoning/index.ts +7 -7
- package/src/intelligence/temporal/evolution.ts +193 -197
- package/src/intelligence/temporal/index.ts +16 -16
- package/src/intelligence/temporal/query-processor.ts +190 -190
- package/src/intelligence/temporal/timeline.ts +272 -259
- package/src/intelligence/temporal/trends.ts +263 -263
- package/src/intelligence/tokenizer.ts +118 -118
- package/src/knowledge/entity-extractor.ts +447 -443
- package/src/knowledge/graph/builder.ts +185 -185
- package/src/knowledge/graph/linker.ts +201 -201
- package/src/knowledge/graph/memory-graph.ts +359 -359
- package/src/knowledge/graph/schema.ts +99 -99
- package/src/knowledge/graph/search.ts +166 -166
- package/src/knowledge/relationship-extractor.ts +108 -108
- package/src/memory/chroma/client.ts +211 -192
- package/src/memory/chroma/collection-manager.ts +92 -92
- package/src/memory/chroma/config.ts +57 -57
- package/src/memory/chroma/embeddings.ts +177 -175
- package/src/memory/chroma/index.ts +82 -82
- package/src/memory/chroma/migration.ts +270 -270
- package/src/memory/chroma/schemas.ts +69 -69
- package/src/memory/chroma/search.ts +319 -315
- package/src/memory/chroma/store.ts +755 -747
- package/src/memory/compression.ts +121 -121
- package/src/memory/consolidation/archiver.ts +162 -165
- package/src/memory/consolidation/merger.ts +182 -186
- package/src/memory/consolidation/scorer.ts +136 -136
- package/src/memory/database.ts +9 -0
- package/src/memory/dual-write.ts +145 -0
- package/src/memory/embeddings.ts +226 -226
- package/src/memory/episodic/detector.ts +108 -108
- package/src/memory/episodic/manager.ts +347 -351
- package/src/memory/episodic/summarizer.ts +179 -179
- package/src/memory/episodic/types.ts +52 -52
- package/src/memory/fts5-search.ts +692 -633
- package/src/memory/index.ts +943 -1060
- package/src/memory/migrations/add-fts5.ts +118 -108
- package/src/memory/patterns.ts +438 -438
- package/src/memory/pruning.ts +60 -60
- package/src/memory/schema.ts +88 -88
- package/src/memory/store.ts +911 -787
- package/src/orchestrator/handlers/decision-handler.ts +204 -204
- package/src/packs/index.ts +9 -9
- package/src/packs/loader.ts +134 -134
- package/src/packs/manager.ts +204 -204
- package/src/packs/ranker.ts +78 -78
- package/src/packs/types.ts +81 -81
- package/src/phase12/index.ts +5 -5
- package/src/retrieval/bm25/index.ts +300 -297
- package/src/retrieval/bm25/tokenizer.ts +184 -184
- package/src/retrieval/feedback/adaptive.ts +221 -221
- package/src/retrieval/feedback/index.ts +16 -16
- package/src/retrieval/feedback/metrics.ts +221 -221
- package/src/retrieval/feedback/store.ts +283 -283
- package/src/retrieval/fusion/index.ts +194 -194
- package/src/retrieval/fusion/rrf.ts +165 -165
- package/src/retrieval/index.ts +12 -12
- package/src/retrieval/pipeline.ts +375 -375
- package/src/retrieval/query/expander.ts +203 -203
- package/src/retrieval/query/index.ts +27 -27
- package/src/retrieval/query/intent-classifier.ts +252 -252
- package/src/retrieval/query/temporal-parser.ts +295 -295
- package/src/retrieval/reranker/index.ts +189 -188
- package/src/retrieval/reranker/model.ts +99 -95
- package/src/retrieval/service.ts +125 -125
- package/src/retrieval/types.ts +162 -162
- package/src/routing/entity-extractor.ts +454 -454
- package/src/routing/handlers/exploration-handler.ts +369 -0
- package/src/routing/handlers/index.ts +19 -0
- package/src/routing/handlers/memory-handler.ts +273 -0
- package/src/routing/handlers/mutation-handler.ts +241 -0
- package/src/routing/handlers/recall-handler.ts +642 -0
- package/src/routing/handlers/shared.ts +515 -0
- package/src/routing/handlers/types.ts +48 -0
- package/src/routing/intent-classifier.ts +552 -552
- package/src/routing/response-filter.ts +399 -391
- package/src/routing/router.ts +245 -2193
- package/src/routing/search-engine.ts +521 -514
- package/src/routing/types.ts +104 -94
- package/src/scripts/health-check.ts +118 -118
- package/src/scripts/setup.ts +122 -122
- package/src/server/auto-updater.ts +283 -276
- package/src/server/handlers/call-tool.ts +159 -159
- package/src/server/handlers/list-tools.ts +35 -35
- package/src/server/handlers/tools/auto-remember.ts +165 -165
- package/src/server/handlers/tools/brain.ts +86 -86
- package/src/server/handlers/tools/create-project.ts +135 -135
- package/src/server/handlers/tools/get-code-standards.ts +123 -123
- package/src/server/handlers/tools/get-corrections.ts +152 -152
- package/src/server/handlers/tools/get-patterns.ts +156 -156
- package/src/server/handlers/tools/get-project-context.ts +75 -75
- package/src/server/handlers/tools/index.ts +30 -30
- package/src/server/handlers/tools/init-project.ts +756 -756
- package/src/server/handlers/tools/list-projects.ts +126 -126
- package/src/server/handlers/tools/recall-similar.ts +87 -87
- package/src/server/handlers/tools/recognize-pattern.ts +132 -132
- package/src/server/handlers/tools/record-correction.ts +131 -131
- package/src/server/handlers/tools/remember-decision.ts +168 -168
- package/src/server/handlers/tools/schemas.ts +179 -179
- package/src/server/handlers/tools/search-code.ts +122 -122
- package/src/server/handlers/tools/smart-context.ts +146 -146
- package/src/server/handlers/tools/update-progress.ts +131 -131
- package/src/server/http-api.ts +215 -1229
- package/src/server/mcp-proxy.ts +85 -84
- package/src/server/mcp-server.ts +285 -284
- package/src/server/middleware/auth.ts +39 -0
- package/src/server/middleware/error-handler.ts +37 -0
- package/src/server/middleware/rate-limit.ts +53 -0
- package/src/server/middleware/validate.ts +42 -0
- package/src/server/pid-manager.ts +137 -136
- package/src/server/providers/resources.ts +581 -581
- package/src/server/routes/code.ts +228 -0
- package/src/server/routes/context.ts +26 -0
- package/src/server/routes/health.ts +19 -0
- package/src/server/routes/helpers.ts +100 -0
- package/src/server/routes/hooks.ts +197 -0
- package/src/server/routes/mcp.ts +47 -0
- package/src/server/routes/memory.ts +397 -0
- package/src/server/routes/models.ts +96 -0
- package/src/server/routes/projects.ts +89 -0
- package/src/server/routes/types.ts +21 -0
- package/src/server/schemas/api-schemas.ts +202 -0
- package/src/server/services.ts +720 -720
- package/src/server/utils/memory-indicator.ts +84 -84
- package/src/server/utils/response-formatter.ts +129 -129
- package/src/server/web-viewer.ts +1145 -1115
- package/src/setup/index.ts +38 -38
- package/src/tools/registry.ts +115 -115
- package/src/tools/schemas.ts +666 -666
- package/src/tools/types.ts +412 -412
- package/src/training/data-store.ts +320 -298
- package/src/training/retrain-pipeline.ts +399 -394
- package/src/utils/error-handler.ts +136 -136
- package/src/utils/index.ts +58 -58
- package/src/utils/kill-port.ts +55 -53
- package/src/utils/phase12-helper.ts +56 -56
- package/src/utils/safe-path.ts +43 -0
- package/src/utils/timing.ts +47 -47
- package/src/utils/transaction.ts +63 -63
- package/src/vault/index.ts +4 -3
- package/src/vault/paths.ts +106 -106
- package/src/vault/query.ts +4 -1
- package/src/vault/reader.ts +44 -1
- package/src/vault/watcher.ts +24 -1
- package/src/vault/writer.ts +487 -413
- package/skills/persistent-memory/SKILL.md +0 -148
- package/skills/persistent-memory/references/tool-reference.md +0 -90
package/src/routing/router.ts
CHANGED
|
@@ -1,2193 +1,245 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Brain Router
|
|
3
|
-
* Phase 16 + Phase 19: Core orchestrator for the unified brain() tool
|
|
4
|
-
*
|
|
5
|
-
* Routes classified intents to internal service calls and
|
|
6
|
-
* returns unified BrainResponse objects.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
import
|
|
14
|
-
import {
|
|
15
|
-
import
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
} from '
|
|
31
|
-
import {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
interface
|
|
49
|
-
|
|
50
|
-
project
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
private
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
private
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
private
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
this.
|
|
83
|
-
this.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
// Process with Phase 12 for proactive recall
|
|
248
|
-
const phase12Result = await phase12.processMessage(
|
|
249
|
-
entities.topic || message,
|
|
250
|
-
project
|
|
251
|
-
)
|
|
252
|
-
|
|
253
|
-
// Build response
|
|
254
|
-
const parts: string[] = [formattedContext]
|
|
255
|
-
|
|
256
|
-
if (phase12Result.recalledMemories?.memories.length) {
|
|
257
|
-
parts.push('\n---\n## Relevant Past Decisions\n')
|
|
258
|
-
for (const mem of phase12Result.recalledMemories.memories) {
|
|
259
|
-
const similarity = Math.round((mem.similarity || 0) * 100)
|
|
260
|
-
const decisionText = mem.decision?.decision || mem.memory?.content || ''
|
|
261
|
-
const safeText = typeof decisionText === 'string' ? decisionText : JSON.stringify(decisionText)
|
|
262
|
-
parts.push(`**[${similarity}%]** ${safeText.slice(0, 200)}`)
|
|
263
|
-
if (mem.decision?.reasoning) {
|
|
264
|
-
const reasoning = typeof mem.decision.reasoning === 'string' ? mem.decision.reasoning : JSON.stringify(mem.decision.reasoning)
|
|
265
|
-
parts.push(` _${reasoning}_`)
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
// If Phase 12 found nothing, do a direct search fallback via SearchEngine
|
|
271
|
-
if (!phase12Result.recalledMemories?.memories.length) {
|
|
272
|
-
try {
|
|
273
|
-
const directResults = await this.searchEngine.enhancedSearch(entities.topic || message, {
|
|
274
|
-
project,
|
|
275
|
-
limit: 5,
|
|
276
|
-
minSimilarity: 0.3
|
|
277
|
-
})
|
|
278
|
-
if (directResults.length > 0) {
|
|
279
|
-
parts.push('\n---\n## Related Memories\n')
|
|
280
|
-
for (const r of directResults) {
|
|
281
|
-
const similarity = Math.round((r.score || 0) * 100)
|
|
282
|
-
const safeContent = typeof r.content === 'string' ? r.content : JSON.stringify(r.content ?? '')
|
|
283
|
-
parts.push(`**[${similarity}%]** ${safeContent.slice(0, 200)}`)
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
} catch {
|
|
287
|
-
// Direct search failed, continue without
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
// Phase 32: Include code file map in session context if code index is available
|
|
292
|
-
try {
|
|
293
|
-
const codeQuery = getCodeQuery()
|
|
294
|
-
if (codeQuery && project) {
|
|
295
|
-
const fileMap = codeQuery.getFileMap(project, 50)
|
|
296
|
-
if (fileMap.length > 0) {
|
|
297
|
-
const formatted = codeQuery.formatFileMap(fileMap)
|
|
298
|
-
parts.push('\n---\n## Code Structure\n')
|
|
299
|
-
parts.push(formatted)
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
} catch {
|
|
303
|
-
// Code intelligence not available
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// C8: Register with episode manager
|
|
307
|
-
this.registerEpisodeMessage(message, project, 'session_start')
|
|
308
|
-
|
|
309
|
-
// C9: Append proactive recommendations
|
|
310
|
-
try {
|
|
311
|
-
const recommendations = await this.searchEngine.getRecommendations(
|
|
312
|
-
entities.topic || message,
|
|
313
|
-
{ project, limit: 3 }
|
|
314
|
-
)
|
|
315
|
-
if (recommendations?.recommendations?.length) {
|
|
316
|
-
parts.push('\n---\n## Proactive Recommendations\n')
|
|
317
|
-
for (const rec of recommendations.recommendations) {
|
|
318
|
-
parts.push(`- **${rec.type}**: ${rec.content?.slice(0, 150) || ''}`)
|
|
319
|
-
if (rec.reasoning) parts.push(` _${rec.reasoning}_`)
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
} catch {
|
|
323
|
-
// Recommendations not available
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
// Phase 25: Query past session summaries for this project
|
|
327
|
-
try {
|
|
328
|
-
let sessionSummaries = await this.searchEngine.plainSearch(
|
|
329
|
-
'session summary',
|
|
330
|
-
{ project: project || undefined, limit: 5, minSimilarity: 0.3 }
|
|
331
|
-
)
|
|
332
|
-
|
|
333
|
-
// Fallback: if plainSearch returns 0, try enhancedSearch with broader query
|
|
334
|
-
if (sessionSummaries.length === 0) {
|
|
335
|
-
sessionSummaries = await this.searchEngine.enhancedSearch(
|
|
336
|
-
`session summary ${project || ''}`.trim(),
|
|
337
|
-
{ project: project || undefined, limit: 5, minSimilarity: 0.2 }
|
|
338
|
-
)
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
let summaryItems = sessionSummaries.filter(r =>
|
|
342
|
-
(r.metadata as Record<string, unknown>)?.tags &&
|
|
343
|
-
Array.isArray((r.metadata as Record<string, unknown>).tags) &&
|
|
344
|
-
((r.metadata as Record<string, unknown>).tags as string[]).includes('session-summary') ||
|
|
345
|
-
r.content.toLowerCase().startsWith('session summary:') ||
|
|
346
|
-
r.content.toLowerCase().includes('worked on')
|
|
347
|
-
)
|
|
348
|
-
|
|
349
|
-
// Sort by date descending (most recent first)
|
|
350
|
-
summaryItems.sort((a, b) => {
|
|
351
|
-
const dateA = a.date || (a.metadata as Record<string, unknown>)?.created_at as string || ''
|
|
352
|
-
const dateB = b.date || (b.metadata as Record<string, unknown>)?.created_at as string || ''
|
|
353
|
-
return dateB.localeCompare(dateA)
|
|
354
|
-
})
|
|
355
|
-
|
|
356
|
-
if (summaryItems.length > 0) {
|
|
357
|
-
parts.push('\n---\n## Past Sessions\n')
|
|
358
|
-
for (const s of summaryItems.slice(0, 3)) {
|
|
359
|
-
const safeContent = typeof s.content === 'string' ? s.content : JSON.stringify(s.content ?? '')
|
|
360
|
-
parts.push(`- ${safeContent.slice(0, 300)}`)
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
} catch {
|
|
364
|
-
// Non-critical — skip if search fails
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
const totalRecalled = phase12Result.recalledMemories?.memories.length || 0
|
|
368
|
-
return {
|
|
369
|
-
action: 'retrieved',
|
|
370
|
-
summary: `Session context for ${project}${totalRecalled ? ` (${totalRecalled} memories)` : ''}`,
|
|
371
|
-
content: parts.join('\n'),
|
|
372
|
-
relevantItems: totalRecalled
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
private async handleContextNeeded(
|
|
377
|
-
message: string,
|
|
378
|
-
project: string | undefined,
|
|
379
|
-
entities: BrainExtractedEntities,
|
|
380
|
-
classification?: ClassificationResult
|
|
381
|
-
): Promise<BrainResponse> {
|
|
382
|
-
if (!isServicesInitialized()) {
|
|
383
|
-
return this.servicesNotReady()
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// Phase 25: Use undefined for search when no project detected (search all projects)
|
|
387
|
-
const searchProject = project || undefined
|
|
388
|
-
const displayProject = project || DEFAULT_PROJECT
|
|
389
|
-
const query = entities.topic || message
|
|
390
|
-
const tiers: TierResults[] = []
|
|
391
|
-
const hasTemporal = classification?.secondary.includes('exploration') ||
|
|
392
|
-
this.classifier.hasTemporalSignal(message.toLowerCase())
|
|
393
|
-
|
|
394
|
-
// Use temporal search if temporal signals detected
|
|
395
|
-
if (hasTemporal) {
|
|
396
|
-
const { results } = await this.searchEngine.temporalSearch(query, { project: searchProject, limit: 5 })
|
|
397
|
-
if (results.length > 0) {
|
|
398
|
-
tiers.push({
|
|
399
|
-
label: 'Memories',
|
|
400
|
-
results: results.map(r => ({
|
|
401
|
-
content: r.content,
|
|
402
|
-
score: r.score,
|
|
403
|
-
source: r.source === 'decision' ? 'Past Decision' : r.source,
|
|
404
|
-
metadata: r.metadata as Record<string, unknown>
|
|
405
|
-
}))
|
|
406
|
-
})
|
|
407
|
-
}
|
|
408
|
-
} else {
|
|
409
|
-
// Standard enhanced search
|
|
410
|
-
const searchResults = await this.searchEngine.enhancedSearch(query, {
|
|
411
|
-
project: searchProject,
|
|
412
|
-
limit: 5,
|
|
413
|
-
minSimilarity: 0.3
|
|
414
|
-
})
|
|
415
|
-
if (searchResults.length > 0) {
|
|
416
|
-
tiers.push({
|
|
417
|
-
label: 'Memories',
|
|
418
|
-
results: searchResults.map(r => ({
|
|
419
|
-
content: r.content,
|
|
420
|
-
score: r.score,
|
|
421
|
-
source: r.source === 'decision' ? 'Past Decision' : r.source,
|
|
422
|
-
metadata: r.metadata as Record<string, unknown>
|
|
423
|
-
}))
|
|
424
|
-
})
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
// Also search patterns
|
|
429
|
-
const patternResults = await this.searchEngine.searchPatterns(query, { project: searchProject, limit: 3 })
|
|
430
|
-
if (patternResults.length > 0) {
|
|
431
|
-
tiers.push({
|
|
432
|
-
label: 'Patterns',
|
|
433
|
-
results: patternResults.map(p => ({
|
|
434
|
-
content: p.content,
|
|
435
|
-
score: p.score,
|
|
436
|
-
source: `Pattern`,
|
|
437
|
-
metadata: p.metadata as Record<string, unknown>
|
|
438
|
-
}))
|
|
439
|
-
})
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
// Phase 32: Code intelligence — query code index for matching symbols/files
|
|
443
|
-
const codeTier = this.queryCodeIntelligence(query, searchProject)
|
|
444
|
-
if (codeTier) {
|
|
445
|
-
tiers.push(codeTier)
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
const response = this.responseFilter.synthesize(tiers, message, displayProject)
|
|
449
|
-
|
|
450
|
-
// P0: When no results found and message looks like a plain statement (not a question),
|
|
451
|
-
// add a hint that the user may have intended to store it
|
|
452
|
-
if (response.action === 'none' && response.relevantItems === 0) {
|
|
453
|
-
const isPlainStatement = message.length > 15 &&
|
|
454
|
-
!message.trim().endsWith('?') &&
|
|
455
|
-
message.split(/\s+/).length > 3
|
|
456
|
-
if (isPlainStatement) {
|
|
457
|
-
response.content += `\n\n**Tip:** If you meant to save this, try:\n- \`brain("Remember: ${message.slice(0, 60)}")\`\n- Or use \`action: "store"\` to force storage: \`brain({ message: "...", action: "store" })\``
|
|
458
|
-
}
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
return response
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
/**
|
|
465
|
-
* Handle explicit "store this" requests — always uses the FULL message as the decision text.
|
|
466
|
-
*/
|
|
467
|
-
private async handleStoreThis(
|
|
468
|
-
message: string,
|
|
469
|
-
project: string | undefined,
|
|
470
|
-
entities: BrainExtractedEntities
|
|
471
|
-
): Promise<BrainResponse> {
|
|
472
|
-
const effectiveProject = project || DEFAULT_PROJECT
|
|
473
|
-
|
|
474
|
-
if (!isServicesInitialized()) {
|
|
475
|
-
return this.servicesNotReady()
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
const memory = getMemoryService()
|
|
479
|
-
const reasoning = entities.reasoning || ''
|
|
480
|
-
const context = entities.topic || message.slice(0, 200)
|
|
481
|
-
|
|
482
|
-
// Phase 30: Optional LLM compression
|
|
483
|
-
const { content: decision, rawContent } = await this.maybeCompress(message, 'store')
|
|
484
|
-
|
|
485
|
-
const decisionId = await timed('brain:store', () => memory.rememberDecision(
|
|
486
|
-
effectiveProject,
|
|
487
|
-
context,
|
|
488
|
-
decision,
|
|
489
|
-
reasoning,
|
|
490
|
-
{
|
|
491
|
-
alternatives: entities.alternatives,
|
|
492
|
-
tags: entities.technologies.length > 0 ? entities.technologies : ['user-stored']
|
|
493
|
-
}
|
|
494
|
-
))
|
|
495
|
-
|
|
496
|
-
// Track for potential update/delete
|
|
497
|
-
this.lastStoredId = decisionId
|
|
498
|
-
this.lastStoredProject = effectiveProject
|
|
499
|
-
|
|
500
|
-
// Bug 4: Track for recency fast path
|
|
501
|
-
this.trackRecentDecision(decision, effectiveProject)
|
|
502
|
-
|
|
503
|
-
// Invalidate cache for this project
|
|
504
|
-
this.searchEngine.invalidateCache(effectiveProject)
|
|
505
|
-
|
|
506
|
-
// Background: vault write + episode linking + code linkage (non-critical)
|
|
507
|
-
setImmediate(() => {
|
|
508
|
-
this.writeToVault(effectiveProject, decision, reasoning, context, entities.alternatives)
|
|
509
|
-
this.linkToActiveEpisode(effectiveProject, decisionId, 'decision')
|
|
510
|
-
this.linkToCodeFiles(decisionId, decision, effectiveProject)
|
|
511
|
-
})
|
|
512
|
-
|
|
513
|
-
const compressedNote = rawContent ? '\n*(compressed)*' : ''
|
|
514
|
-
return {
|
|
515
|
-
action: 'stored',
|
|
516
|
-
summary: `Stored: ${message.slice(0, 60)}`,
|
|
517
|
-
content: `Memory stored (ID: ${decisionId})\n\n**Project:** ${effectiveProject}\n**Content:** ${decision}${reasoning ? `\n**Reasoning:** ${reasoning}` : ''}${compressedNote}`,
|
|
518
|
-
relevantItems: 1
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
private async handleDecisionMade(
|
|
523
|
-
message: string,
|
|
524
|
-
project: string | undefined,
|
|
525
|
-
entities: BrainExtractedEntities
|
|
526
|
-
): Promise<BrainResponse> {
|
|
527
|
-
const effectiveProject = project || DEFAULT_PROJECT
|
|
528
|
-
|
|
529
|
-
if (!isServicesInitialized()) {
|
|
530
|
-
return this.servicesNotReady()
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
const memory = getMemoryService()
|
|
534
|
-
const reasoning = entities.reasoning || ''
|
|
535
|
-
const context = entities.topic || message.slice(0, 200)
|
|
536
|
-
const alternatives = entities.alternatives
|
|
537
|
-
|
|
538
|
-
// Phase 30: Optional LLM compression
|
|
539
|
-
const { content: decision, rawContent } = await this.maybeCompress(message, 'decision')
|
|
540
|
-
|
|
541
|
-
const decisionId = await timed('brain:store', () => memory.rememberDecision(
|
|
542
|
-
effectiveProject,
|
|
543
|
-
context,
|
|
544
|
-
decision,
|
|
545
|
-
reasoning,
|
|
546
|
-
{
|
|
547
|
-
alternatives,
|
|
548
|
-
tags: entities.technologies.length > 0 ? entities.technologies : ['auto-detected']
|
|
549
|
-
}
|
|
550
|
-
))
|
|
551
|
-
|
|
552
|
-
this.lastStoredId = decisionId
|
|
553
|
-
this.lastStoredProject = effectiveProject
|
|
554
|
-
|
|
555
|
-
// Bug 4: Track for recency fast path
|
|
556
|
-
this.trackRecentDecision(decision, effectiveProject)
|
|
557
|
-
|
|
558
|
-
// Invalidate cache
|
|
559
|
-
this.searchEngine.invalidateCache(effectiveProject)
|
|
560
|
-
|
|
561
|
-
// Background: vault write + episode linking + code linkage (non-critical)
|
|
562
|
-
setImmediate(() => {
|
|
563
|
-
this.writeToVault(effectiveProject, decision, reasoning, context, alternatives)
|
|
564
|
-
this.linkToActiveEpisode(effectiveProject, decisionId, 'decision')
|
|
565
|
-
this.linkToCodeFiles(decisionId, decision, effectiveProject)
|
|
566
|
-
})
|
|
567
|
-
|
|
568
|
-
const compressedNote = rawContent ? '\n*(compressed)*' : ''
|
|
569
|
-
return {
|
|
570
|
-
action: 'stored',
|
|
571
|
-
summary: `Stored decision: ${message.slice(0, 60)}`,
|
|
572
|
-
content: `Decision stored (ID: ${decisionId})\n\n**Project:** ${effectiveProject}\n**Decision:** ${decision}\n**Reasoning:** ${reasoning}${compressedNote}`,
|
|
573
|
-
relevantItems: 1
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
private async handlePatternFound(
|
|
578
|
-
message: string,
|
|
579
|
-
project: string | undefined,
|
|
580
|
-
entities: BrainExtractedEntities
|
|
581
|
-
): Promise<BrainResponse> {
|
|
582
|
-
const effectiveProject = project || DEFAULT_PROJECT
|
|
583
|
-
|
|
584
|
-
if (!isServicesInitialized()) {
|
|
585
|
-
return this.servicesNotReady()
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
const memory = getMemoryService()
|
|
589
|
-
const patternType = entities.patternType || 'solution'
|
|
590
|
-
const description = message
|
|
591
|
-
|
|
592
|
-
const patternId = await memory.storePattern({
|
|
593
|
-
project: effectiveProject,
|
|
594
|
-
pattern_type: patternType,
|
|
595
|
-
description,
|
|
596
|
-
confidence: 0.8,
|
|
597
|
-
context: message.slice(0, 300)
|
|
598
|
-
})
|
|
599
|
-
|
|
600
|
-
// Invalidate cache
|
|
601
|
-
this.searchEngine.invalidateCache(effectiveProject)
|
|
602
|
-
|
|
603
|
-
// Link to active episode + code files
|
|
604
|
-
this.linkToActiveEpisode(effectiveProject, patternId, 'pattern')
|
|
605
|
-
this.linkToCodeFiles(patternId, description, effectiveProject)
|
|
606
|
-
|
|
607
|
-
return {
|
|
608
|
-
action: 'stored',
|
|
609
|
-
summary: `Stored ${patternType}: ${description.slice(0, 60)}`,
|
|
610
|
-
content: `Pattern stored (ID: ${patternId})\n\n**Type:** ${patternType}\n**Project:** ${effectiveProject}\n**Description:** ${description}`,
|
|
611
|
-
relevantItems: 1
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
private async handleMistakeLearned(
|
|
616
|
-
message: string,
|
|
617
|
-
project: string | undefined,
|
|
618
|
-
entities: BrainExtractedEntities
|
|
619
|
-
): Promise<BrainResponse> {
|
|
620
|
-
const effectiveProject = project || DEFAULT_PROJECT
|
|
621
|
-
|
|
622
|
-
if (!isServicesInitialized()) {
|
|
623
|
-
return this.servicesNotReady()
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
const memory = getMemoryService()
|
|
627
|
-
const original = entities.original || message
|
|
628
|
-
const correction = entities.correction || ''
|
|
629
|
-
const reasoning = entities.reasoning || 'Lesson learned from experience'
|
|
630
|
-
|
|
631
|
-
const correctionId = await memory.storeCorrection({
|
|
632
|
-
project: effectiveProject,
|
|
633
|
-
original,
|
|
634
|
-
correction: correction || message,
|
|
635
|
-
reasoning,
|
|
636
|
-
context: entities.topic || '',
|
|
637
|
-
confidence: 0.9
|
|
638
|
-
})
|
|
639
|
-
|
|
640
|
-
// Invalidate cache
|
|
641
|
-
this.searchEngine.invalidateCache(effectiveProject)
|
|
642
|
-
|
|
643
|
-
// Link to active episode + code files
|
|
644
|
-
this.linkToActiveEpisode(effectiveProject, correctionId, 'correction')
|
|
645
|
-
this.linkToCodeFiles(correctionId, original, effectiveProject)
|
|
646
|
-
|
|
647
|
-
return {
|
|
648
|
-
action: 'stored',
|
|
649
|
-
summary: `Stored correction: ${original.slice(0, 60)}`,
|
|
650
|
-
content: `Correction stored (ID: ${correctionId})\n\n**Project:** ${effectiveProject}\n**Original:** ${original}\n**Correction:** ${correction || '(see original message)'}`,
|
|
651
|
-
relevantItems: 1
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
private async handleProgressUpdate(
|
|
656
|
-
message: string,
|
|
657
|
-
project: string | undefined,
|
|
658
|
-
entities: BrainExtractedEntities
|
|
659
|
-
): Promise<BrainResponse> {
|
|
660
|
-
const effectiveProject = project || DEFAULT_PROJECT
|
|
661
|
-
|
|
662
|
-
if (!isServicesInitialized()) {
|
|
663
|
-
return this.servicesNotReady()
|
|
664
|
-
}
|
|
665
|
-
|
|
666
|
-
const contextService = getContextService()
|
|
667
|
-
const memory = getMemoryService()
|
|
668
|
-
|
|
669
|
-
const completedTask = entities.completedTask || message
|
|
670
|
-
const nextSteps = entities.nextSteps || 'Continue development'
|
|
671
|
-
|
|
672
|
-
// Store in session context
|
|
673
|
-
try {
|
|
674
|
-
await contextService.progress.addCompletedTask(effectiveProject, {
|
|
675
|
-
id: this.generateTaskId(completedTask),
|
|
676
|
-
title: completedTask,
|
|
677
|
-
status: 'done',
|
|
678
|
-
completedAt: new Date()
|
|
679
|
-
})
|
|
680
|
-
} catch {
|
|
681
|
-
// Progress update failed
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
// Also store as a searchable memory
|
|
685
|
-
try {
|
|
686
|
-
await memory.rememberDecision(
|
|
687
|
-
effectiveProject,
|
|
688
|
-
`Progress update`,
|
|
689
|
-
`Progress: ${completedTask}${nextSteps !== 'Continue development' ? `. Next: ${nextSteps}` : ''}`,
|
|
690
|
-
'Progress tracking',
|
|
691
|
-
{ tags: ['progress', ...entities.technologies] }
|
|
692
|
-
)
|
|
693
|
-
|
|
694
|
-
// Invalidate cache
|
|
695
|
-
this.searchEngine.invalidateCache(effectiveProject)
|
|
696
|
-
} catch {
|
|
697
|
-
// Memory storage failed
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
// Register with episode
|
|
701
|
-
this.registerEpisodeMessage(message, effectiveProject, 'progress_update')
|
|
702
|
-
|
|
703
|
-
return {
|
|
704
|
-
action: 'stored',
|
|
705
|
-
summary: `Progress: ${completedTask.slice(0, 60)}`,
|
|
706
|
-
content: `Progress updated for ${effectiveProject}\n\n**Completed:** ${completedTask}\n**Next:** ${nextSteps}`,
|
|
707
|
-
relevantItems: 1
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
/**
|
|
712
|
-
* List all decisions for a project
|
|
713
|
-
*/
|
|
714
|
-
private async handleListAll(
|
|
715
|
-
_message: string,
|
|
716
|
-
project: string | undefined,
|
|
717
|
-
_entities: BrainExtractedEntities
|
|
718
|
-
): Promise<BrainResponse> {
|
|
719
|
-
if (!isServicesInitialized()) {
|
|
720
|
-
return this.servicesNotReady()
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
const memory = getMemoryService()
|
|
724
|
-
const effectiveProject = project
|
|
725
|
-
|
|
726
|
-
try {
|
|
727
|
-
const decisions = await memory.fetchAllDecisions(effectiveProject)
|
|
728
|
-
const patterns = await memory.fetchAllPatterns(effectiveProject)
|
|
729
|
-
const corrections = await memory.fetchAllCorrections(effectiveProject)
|
|
730
|
-
|
|
731
|
-
const projectLabel = effectiveProject || 'all projects'
|
|
732
|
-
const total = decisions.length + patterns.length + corrections.length
|
|
733
|
-
|
|
734
|
-
if (total === 0) {
|
|
735
|
-
return {
|
|
736
|
-
action: 'none',
|
|
737
|
-
summary: `No memories found for ${projectLabel}`,
|
|
738
|
-
content: `No decisions, patterns, or corrections stored yet for ${projectLabel}.`,
|
|
739
|
-
relevantItems: 0
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
// Phase 27: Use compact format for list_all
|
|
744
|
-
const allItems: any[] = [
|
|
745
|
-
...decisions.map(d => ({
|
|
746
|
-
id: d.id || (d as any).decision_id,
|
|
747
|
-
content: typeof (d.decision || d.document || d.content) === 'string'
|
|
748
|
-
? (d.decision || d.document || d.content)
|
|
749
|
-
: JSON.stringify(d.decision || d.document || d.content || ''),
|
|
750
|
-
category: 'decision',
|
|
751
|
-
project: d.project || effectiveProject || 'general',
|
|
752
|
-
created_at: d.created_at || d.date || '',
|
|
753
|
-
})),
|
|
754
|
-
...patterns.map(p => ({
|
|
755
|
-
id: p.id,
|
|
756
|
-
content: typeof (p.description || p.document || p.content) === 'string'
|
|
757
|
-
? (p.description || p.document || p.content)
|
|
758
|
-
: JSON.stringify(p.description || p.document || p.content || ''),
|
|
759
|
-
category: p.pattern_type || 'pattern',
|
|
760
|
-
project: p.project || effectiveProject || 'general',
|
|
761
|
-
created_at: p.created_at || '',
|
|
762
|
-
})),
|
|
763
|
-
...corrections.map(c => ({
|
|
764
|
-
id: c.id,
|
|
765
|
-
content: typeof (c.original || c.document || c.content) === 'string'
|
|
766
|
-
? (c.original || c.document || c.content)
|
|
767
|
-
: JSON.stringify(c.original || c.document || c.content || ''),
|
|
768
|
-
category: 'correction',
|
|
769
|
-
project: c.project || effectiveProject || 'general',
|
|
770
|
-
created_at: c.created_at || '',
|
|
771
|
-
})),
|
|
772
|
-
]
|
|
773
|
-
|
|
774
|
-
const compactContent = formatCompactResponse(allItems, `all memories for ${projectLabel}`)
|
|
775
|
-
return {
|
|
776
|
-
action: 'retrieved',
|
|
777
|
-
summary: `${total} memories for ${projectLabel}`,
|
|
778
|
-
content: compactContent,
|
|
779
|
-
relevantItems: total
|
|
780
|
-
}
|
|
781
|
-
} catch (error) {
|
|
782
|
-
return {
|
|
783
|
-
action: 'none',
|
|
784
|
-
summary: 'Error listing memories',
|
|
785
|
-
content: `Failed to list memories: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
786
|
-
relevantItems: 0
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
}
|
|
790
|
-
|
|
791
|
-
/**
|
|
792
|
-
* Phase 19 D4: Update with higher similarity threshold
|
|
793
|
-
*/
|
|
794
|
-
private async handleUpdateMemory(
|
|
795
|
-
message: string,
|
|
796
|
-
project: string | undefined,
|
|
797
|
-
entities: BrainExtractedEntities
|
|
798
|
-
): Promise<BrainResponse> {
|
|
799
|
-
const effectiveProject = project || this.lastStoredProject || DEFAULT_PROJECT
|
|
800
|
-
|
|
801
|
-
if (!isServicesInitialized()) {
|
|
802
|
-
return this.servicesNotReady()
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
const memory = getMemoryService()
|
|
806
|
-
const topic = entities.topic || message
|
|
807
|
-
const hasSpecificContent = this.hasDescriptiveContent(message)
|
|
808
|
-
|
|
809
|
-
if (hasSpecificContent) {
|
|
810
|
-
try {
|
|
811
|
-
const results = await memory.searchRaw(topic, {
|
|
812
|
-
project: effectiveProject,
|
|
813
|
-
limit: 1,
|
|
814
|
-
minSimilarity: UPDATE_MIN_SIMILARITY
|
|
815
|
-
})
|
|
816
|
-
|
|
817
|
-
const matchId = results[0]?.memory?.id || results[0]?.decision?.id || results[0]?.id
|
|
818
|
-
const matchSimilarity = results[0]?.similarity || 0
|
|
819
|
-
|
|
820
|
-
if (results.length > 0 && matchId && matchSimilarity >= UPDATE_MIN_SIMILARITY) {
|
|
821
|
-
const oldId = matchId
|
|
822
|
-
const newId = await memory.updateDecision(
|
|
823
|
-
oldId,
|
|
824
|
-
effectiveProject,
|
|
825
|
-
`Updated: ${topic.slice(0, 200)}`,
|
|
826
|
-
message,
|
|
827
|
-
entities.reasoning || 'Updated via brain tool',
|
|
828
|
-
{ tags: entities.technologies.length > 0 ? entities.technologies : ['updated'] }
|
|
829
|
-
)
|
|
830
|
-
|
|
831
|
-
this.lastStoredId = newId
|
|
832
|
-
this.lastStoredProject = effectiveProject
|
|
833
|
-
|
|
834
|
-
// Invalidate cache
|
|
835
|
-
this.searchEngine.invalidateCache(effectiveProject)
|
|
836
|
-
|
|
837
|
-
const oldContent = results[0].decision?.decision || results[0].memory?.content?.slice(0, 100) || results[0].content?.slice(0, 100) || ''
|
|
838
|
-
return {
|
|
839
|
-
action: 'stored',
|
|
840
|
-
summary: `Updated decision`,
|
|
841
|
-
content: `Replaced: "${oldContent.slice(0, 80)}"\n\nWith:\n**New content:** ${message}`,
|
|
842
|
-
relevantItems: 1
|
|
843
|
-
}
|
|
844
|
-
} else if (results.length > 0 && matchSimilarity < UPDATE_MIN_SIMILARITY) {
|
|
845
|
-
// D4: No confident match — store as new instead of updating wrong memory
|
|
846
|
-
return this.storeAsNew(memory, effectiveProject, message, topic, entities, 'No confident match to update (similarity too low)')
|
|
847
|
-
}
|
|
848
|
-
} catch {
|
|
849
|
-
// Search failed, fall through
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
// Generic update — use lastStoredId
|
|
854
|
-
if (this.lastStoredId) {
|
|
855
|
-
const newId = await memory.updateDecision(
|
|
856
|
-
this.lastStoredId,
|
|
857
|
-
effectiveProject,
|
|
858
|
-
`Updated: ${topic.slice(0, 200)}`,
|
|
859
|
-
message,
|
|
860
|
-
entities.reasoning || 'Updated via brain tool',
|
|
861
|
-
{ tags: entities.technologies.length > 0 ? entities.technologies : ['updated'] }
|
|
862
|
-
)
|
|
863
|
-
|
|
864
|
-
this.lastStoredId = newId
|
|
865
|
-
this.lastStoredProject = effectiveProject
|
|
866
|
-
this.searchEngine.invalidateCache(effectiveProject)
|
|
867
|
-
|
|
868
|
-
return {
|
|
869
|
-
action: 'stored',
|
|
870
|
-
summary: `Updated decision`,
|
|
871
|
-
content: `Previous decision replaced with:\n\n**Project:** ${effectiveProject}\n**New content:** ${message}`,
|
|
872
|
-
relevantItems: 1
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
// Fallback: store as new decision
|
|
877
|
-
return this.storeAsNew(memory, effectiveProject, message, topic, entities, 'No matching decision found to update')
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
/**
|
|
881
|
-
* Phase 19 D4: Delete with higher similarity threshold
|
|
882
|
-
*/
|
|
883
|
-
private async handleDeleteMemory(
|
|
884
|
-
message: string,
|
|
885
|
-
project: string | undefined,
|
|
886
|
-
entities: BrainExtractedEntities
|
|
887
|
-
): Promise<BrainResponse> {
|
|
888
|
-
const effectiveProject = project || this.lastStoredProject || DEFAULT_PROJECT
|
|
889
|
-
|
|
890
|
-
if (!isServicesInitialized()) {
|
|
891
|
-
return this.servicesNotReady()
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
const memory = getMemoryService()
|
|
895
|
-
const topic = entities.topic || message
|
|
896
|
-
const hasSpecificContent = this.hasDescriptiveContent(message)
|
|
897
|
-
const lower = message.toLowerCase()
|
|
898
|
-
|
|
899
|
-
// BUG-006 T6: Bulk delete — "delete all memories for project X"
|
|
900
|
-
if (lower.includes('delete all') || lower.includes('remove all') || lower.includes('clear all')) {
|
|
901
|
-
const bulkProject = entities.project || project || this.lastStoredProject
|
|
902
|
-
if (bulkProject) {
|
|
903
|
-
try {
|
|
904
|
-
const decisions = await memory.chroma.store.getDecisionsByProject(bulkProject)
|
|
905
|
-
if (decisions.length === 0) {
|
|
906
|
-
return {
|
|
907
|
-
action: 'none',
|
|
908
|
-
summary: 'No memories found',
|
|
909
|
-
content: `No memories found for project "${bulkProject}".`,
|
|
910
|
-
relevantItems: 0
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
for (const d of decisions) {
|
|
914
|
-
await memory.deleteDecision(d.id)
|
|
915
|
-
}
|
|
916
|
-
this.searchEngine.invalidateCache(bulkProject)
|
|
917
|
-
return {
|
|
918
|
-
action: 'stored',
|
|
919
|
-
summary: `Bulk deleted ${decisions.length} memories`,
|
|
920
|
-
content: `Deleted all ${decisions.length} memories for project "${bulkProject}".`,
|
|
921
|
-
relevantItems: 0
|
|
922
|
-
}
|
|
923
|
-
} catch {
|
|
924
|
-
// Bulk delete failed, fall through to single-item delete
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
if (hasSpecificContent) {
|
|
930
|
-
try {
|
|
931
|
-
const results = await memory.searchRaw(topic, {
|
|
932
|
-
project: effectiveProject,
|
|
933
|
-
limit: 3,
|
|
934
|
-
minSimilarity: DELETE_MIN_SIMILARITY
|
|
935
|
-
})
|
|
936
|
-
|
|
937
|
-
const matchId = results[0]?.memory?.id || results[0]?.decision?.id || results[0]?.id
|
|
938
|
-
const matchSimilarity = results[0]?.similarity || 0
|
|
939
|
-
|
|
940
|
-
if (results.length > 0 && matchId && matchSimilarity >= DELETE_MIN_SIMILARITY) {
|
|
941
|
-
const targetId = matchId
|
|
942
|
-
const content = results[0].decision?.decision || results[0].memory?.content?.slice(0, 100) || results[0].content?.slice(0, 100) || ''
|
|
943
|
-
|
|
944
|
-
await memory.deleteDecision(targetId)
|
|
945
|
-
if (this.lastStoredId === targetId) this.lastStoredId = null
|
|
946
|
-
|
|
947
|
-
// Invalidate cache
|
|
948
|
-
this.searchEngine.invalidateCache(effectiveProject)
|
|
949
|
-
|
|
950
|
-
return {
|
|
951
|
-
action: 'stored',
|
|
952
|
-
summary: `Deleted memory`,
|
|
953
|
-
content: `Deleted: "${content.slice(0, 100)}" (ID: ${targetId})`,
|
|
954
|
-
relevantItems: 0
|
|
955
|
-
}
|
|
956
|
-
} else if (results.length > 0 && matchSimilarity < DELETE_MIN_SIMILARITY) {
|
|
957
|
-
// D4: No confident match — try direct FTS5 content search as fallback
|
|
958
|
-
const memory = getMemoryService()
|
|
959
|
-
if (memory.fts5) {
|
|
960
|
-
const ftsResults = memory.fts5.search(topic, effectiveProject, 3)
|
|
961
|
-
if (ftsResults.length > 0) {
|
|
962
|
-
const ftsTarget = ftsResults[0]
|
|
963
|
-
await memory.deleteDecision(ftsTarget.id)
|
|
964
|
-
if (this.lastStoredId === ftsTarget.id) this.lastStoredId = null
|
|
965
|
-
this.searchEngine.invalidateCache(effectiveProject)
|
|
966
|
-
return {
|
|
967
|
-
action: 'stored',
|
|
968
|
-
summary: `Deleted memory`,
|
|
969
|
-
content: `Deleted: "${ftsTarget.content.slice(0, 100)}" (ID: ${ftsTarget.id})`,
|
|
970
|
-
relevantItems: 0
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
return {
|
|
975
|
-
action: 'none',
|
|
976
|
-
summary: 'No confident match to delete',
|
|
977
|
-
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.`,
|
|
978
|
-
relevantItems: 0
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
} catch {
|
|
982
|
-
// Search failed, fall through
|
|
983
|
-
}
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
// Generic request — use lastStoredId
|
|
987
|
-
if (this.lastStoredId) {
|
|
988
|
-
try {
|
|
989
|
-
await memory.deleteDecision(this.lastStoredId)
|
|
990
|
-
const deletedId = this.lastStoredId
|
|
991
|
-
this.lastStoredId = null
|
|
992
|
-
this.searchEngine.invalidateCache(effectiveProject)
|
|
993
|
-
return {
|
|
994
|
-
action: 'stored',
|
|
995
|
-
summary: `Deleted most recent memory`,
|
|
996
|
-
content: `Deleted memory (ID: ${deletedId})`,
|
|
997
|
-
relevantItems: 0
|
|
998
|
-
}
|
|
999
|
-
} catch {
|
|
1000
|
-
// Deletion failed
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
|
-
return {
|
|
1005
|
-
action: 'none',
|
|
1006
|
-
summary: 'No matching memory found to delete',
|
|
1007
|
-
content: 'Could not find a memory matching your request. Try being more specific about what to delete.',
|
|
1008
|
-
relevantItems: 0
|
|
1009
|
-
}
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
|
-
/**
|
|
1013
|
-
* Phase 19 C7+C10+C11: Enhanced question handler
|
|
1014
|
-
* - Uses SearchEngine for all searches (hybrid, cached)
|
|
1015
|
-
* - Temporal search when temporal signals detected
|
|
1016
|
-
* - ChainRetrieval for complex multi-part questions
|
|
1017
|
-
* - CrossProject patterns for general/unspecified project
|
|
1018
|
-
* - KnowledgeGraph enrichment
|
|
1019
|
-
*/
|
|
1020
|
-
private async handleQuestion(
|
|
1021
|
-
message: string,
|
|
1022
|
-
project: string | undefined,
|
|
1023
|
-
entities: BrainExtractedEntities,
|
|
1024
|
-
classification: ClassificationResult
|
|
1025
|
-
): Promise<BrainResponse> {
|
|
1026
|
-
if (!isServicesInitialized()) {
|
|
1027
|
-
return this.servicesNotReady()
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
// BUG-002: Detect category-based queries and route to fetchAll
|
|
1031
|
-
const categoryIntent = this.detectCategoryIntent(message)
|
|
1032
|
-
if (categoryIntent) {
|
|
1033
|
-
return this.handleCategoryQuery(message, project, categoryIntent)
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
// Phase 25: Use undefined for search (no project filter = search all) when no project detected
|
|
1037
|
-
const searchProject = project || undefined
|
|
1038
|
-
const displayProject = project || DEFAULT_PROJECT
|
|
1039
|
-
const query = entities.topic || message
|
|
1040
|
-
const tiers: TierResults[] = []
|
|
1041
|
-
const hasTemporal = classification.secondary.includes('exploration') ||
|
|
1042
|
-
this.classifier.hasTemporalSignal(message.toLowerCase())
|
|
1043
|
-
|
|
1044
|
-
// C7: Detect multi-part questions for chain retrieval
|
|
1045
|
-
const isComplex = this.isComplexQuestion(message)
|
|
1046
|
-
|
|
1047
|
-
// Main search — temporal or standard
|
|
1048
|
-
let searchResults: NormalizedResult[]
|
|
1049
|
-
if (hasTemporal) {
|
|
1050
|
-
const { results } = await this.searchEngine.temporalSearch(query, { project: searchProject, limit: 5 })
|
|
1051
|
-
searchResults = results
|
|
1052
|
-
} else if (isComplex) {
|
|
1053
|
-
// C7: Try multi-hop chain retrieval
|
|
1054
|
-
const chainResult = await this.searchEngine.chainSearch(query, { project: searchProject })
|
|
1055
|
-
if (chainResult?.allResults?.length) {
|
|
1056
|
-
searchResults = chainResult.allResults.map((r: any) => ({
|
|
1057
|
-
id: r.id || '',
|
|
1058
|
-
content: r.content || '',
|
|
1059
|
-
score: r.similarity || 0,
|
|
1060
|
-
source: 'decision' as const,
|
|
1061
|
-
project: r.metadata?.project || displayProject || '',
|
|
1062
|
-
date: r.metadata?.created_at || '',
|
|
1063
|
-
metadata: r.metadata || {}
|
|
1064
|
-
}))
|
|
1065
|
-
} else {
|
|
1066
|
-
searchResults = await this.searchEngine.enhancedSearch(query, { project: searchProject, limit: 5 })
|
|
1067
|
-
}
|
|
1068
|
-
} else {
|
|
1069
|
-
searchResults = await this.searchEngine.enhancedSearch(query, { project: searchProject, limit: 5 })
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
if (searchResults.length > 0) {
|
|
1073
|
-
tiers.push({
|
|
1074
|
-
label: 'Memories',
|
|
1075
|
-
results: searchResults.map(r => ({
|
|
1076
|
-
content: r.content,
|
|
1077
|
-
score: r.score,
|
|
1078
|
-
source: r.source === 'decision' ? 'Past Decision' : r.source,
|
|
1079
|
-
metadata: {
|
|
1080
|
-
...(r.metadata as Record<string, unknown>),
|
|
1081
|
-
id: r.id,
|
|
1082
|
-
project: r.project,
|
|
1083
|
-
created_at: r.date
|
|
1084
|
-
}
|
|
1085
|
-
}))
|
|
1086
|
-
})
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
// Phase 32: Code intelligence — synchronous SQLite query, runs alongside async searches
|
|
1090
|
-
const codeTier = this.queryCodeIntelligence(query, searchProject)
|
|
1091
|
-
if (codeTier) {
|
|
1092
|
-
tiers.push(codeTier)
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
// Parallel: patterns + corrections + graph
|
|
1096
|
-
const [patternResults, correctionResults, graphResults] = await Promise.all([
|
|
1097
|
-
this.searchEngine.searchPatterns(query, { project: searchProject, limit: 3 }),
|
|
1098
|
-
this.searchEngine.searchCorrections(query, { project: searchProject, limit: 3 }),
|
|
1099
|
-
// C11: KnowledgeGraph enrichment for all questions
|
|
1100
|
-
this.searchEngine.searchGraph(query, 5)
|
|
1101
|
-
])
|
|
1102
|
-
|
|
1103
|
-
if (patternResults.length > 0) {
|
|
1104
|
-
tiers.push({
|
|
1105
|
-
label: 'Patterns',
|
|
1106
|
-
results: patternResults.map(p => ({
|
|
1107
|
-
content: p.content,
|
|
1108
|
-
score: p.score,
|
|
1109
|
-
source: `Pattern`,
|
|
1110
|
-
metadata: {
|
|
1111
|
-
...(p.metadata as Record<string, unknown>),
|
|
1112
|
-
id: p.id,
|
|
1113
|
-
project: p.project,
|
|
1114
|
-
created_at: p.date,
|
|
1115
|
-
category: 'pattern'
|
|
1116
|
-
}
|
|
1117
|
-
}))
|
|
1118
|
-
})
|
|
1119
|
-
}
|
|
1120
|
-
|
|
1121
|
-
if (correctionResults.length > 0) {
|
|
1122
|
-
tiers.push({
|
|
1123
|
-
label: 'Corrections',
|
|
1124
|
-
results: correctionResults.map(c => ({
|
|
1125
|
-
content: c.content,
|
|
1126
|
-
score: c.score,
|
|
1127
|
-
source: 'Lesson Learned',
|
|
1128
|
-
metadata: {
|
|
1129
|
-
...(c.metadata as Record<string, unknown>),
|
|
1130
|
-
id: c.id,
|
|
1131
|
-
project: c.project,
|
|
1132
|
-
created_at: c.date,
|
|
1133
|
-
category: 'correction'
|
|
1134
|
-
}
|
|
1135
|
-
}))
|
|
1136
|
-
})
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
// C11: Add graph results as "Related Concepts"
|
|
1140
|
-
// When a project filter is active, cap graph results lower (0.25) so they don't
|
|
1141
|
-
// outrank project-scoped ChromaDB results. Without a project filter, cap at 0.4.
|
|
1142
|
-
// BUG-006 T4: Also require keyword overlap with query to prevent noise
|
|
1143
|
-
const graphScoreCap = searchProject ? 0.25 : 0.4
|
|
1144
|
-
const queryWordsForGraph = new Set(query.toLowerCase().split(/\s+/).filter(w => w.length > 2))
|
|
1145
|
-
const relevantGraphResults = graphResults.filter(g => {
|
|
1146
|
-
if (g.score < 0.6) return false
|
|
1147
|
-
// Require at least one query keyword to appear in graph content
|
|
1148
|
-
if (queryWordsForGraph.size > 0) {
|
|
1149
|
-
const contentLower = g.content.toLowerCase()
|
|
1150
|
-
const hasOverlap = [...queryWordsForGraph].some(w => contentLower.includes(w))
|
|
1151
|
-
if (!hasOverlap) return false
|
|
1152
|
-
}
|
|
1153
|
-
return true
|
|
1154
|
-
})
|
|
1155
|
-
if (relevantGraphResults.length > 0) {
|
|
1156
|
-
tiers.push({
|
|
1157
|
-
label: 'Related Concepts',
|
|
1158
|
-
results: relevantGraphResults.map(g => ({
|
|
1159
|
-
content: g.content,
|
|
1160
|
-
score: Math.min(g.score, graphScoreCap),
|
|
1161
|
-
source: 'Knowledge Graph',
|
|
1162
|
-
metadata: g.metadata as Record<string, unknown>
|
|
1163
|
-
}))
|
|
1164
|
-
})
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
// C10: CrossProject patterns for general/unspecified project queries (Bug 2: cap to 0.3)
|
|
1168
|
-
if (!project) {
|
|
1169
|
-
try {
|
|
1170
|
-
const crossProject = await this.searchEngine.findCrossProjectPatterns({
|
|
1171
|
-
query,
|
|
1172
|
-
limit: 3
|
|
1173
|
-
})
|
|
1174
|
-
if (crossProject?.patterns?.length) {
|
|
1175
|
-
tiers.push({
|
|
1176
|
-
label: 'Cross-Project Patterns',
|
|
1177
|
-
results: crossProject.patterns.map((p: any) => ({
|
|
1178
|
-
content: `${p.description || ''} (${p.projects?.join(', ') || 'multiple projects'})`,
|
|
1179
|
-
score: Math.min(p.confidence || 0.5, 0.3),
|
|
1180
|
-
source: 'Cross-Project',
|
|
1181
|
-
metadata: {}
|
|
1182
|
-
}))
|
|
1183
|
-
})
|
|
1184
|
-
}
|
|
1185
|
-
} catch {
|
|
1186
|
-
// Cross-project not available
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
// Phase 23b Fix 5: Session recall — "what have we discussed" queries session tracker
|
|
1191
|
-
if (this.isSessionRecallQuery(message)) {
|
|
1192
|
-
try {
|
|
1193
|
-
const tracker = getSessionTracker()
|
|
1194
|
-
if (tracker) {
|
|
1195
|
-
const stats = tracker.getStats()
|
|
1196
|
-
if (stats.totalItems > 0) {
|
|
1197
|
-
tiers.push({
|
|
1198
|
-
label: 'Current Session',
|
|
1199
|
-
results: [{
|
|
1200
|
-
content: `Active sessions: ${stats.activeSessions}, items tracked: ${stats.totalItems}`,
|
|
1201
|
-
score: 0.95,
|
|
1202
|
-
source: 'Session Tracker',
|
|
1203
|
-
metadata: {}
|
|
1204
|
-
}]
|
|
1205
|
-
})
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
} catch {
|
|
1209
|
-
// Session tracker not available
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
// Bug 4: Inject recently stored decisions using keyword overlap as score
|
|
1214
|
-
const recentMatches = this.findRecentDecisions(query, searchProject)
|
|
1215
|
-
// Only include results whose overlap score meets the hard floor (0.35)
|
|
1216
|
-
const qualifiedRecent = recentMatches.filter(d => (d.overlapScore || 0) >= 0.35)
|
|
1217
|
-
if (qualifiedRecent.length > 0) {
|
|
1218
|
-
tiers.unshift({
|
|
1219
|
-
label: 'Recent Decisions',
|
|
1220
|
-
results: qualifiedRecent.map(d => ({
|
|
1221
|
-
content: d.content,
|
|
1222
|
-
score: d.overlapScore || 0,
|
|
1223
|
-
source: 'Recent Decision',
|
|
1224
|
-
metadata: { storedAt: d.storedAt, project: d.project }
|
|
1225
|
-
}))
|
|
1226
|
-
})
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
// Register with episode
|
|
1230
|
-
this.registerEpisodeMessage(message, displayProject, 'question')
|
|
1231
|
-
|
|
1232
|
-
// Phase 27: Use compact format for progressive disclosure
|
|
1233
|
-
// Combine all tier results, filter, then format compactly
|
|
1234
|
-
const allResults: import('./response-filter').FilterableResult[] = []
|
|
1235
|
-
for (const tier of tiers) {
|
|
1236
|
-
allResults.push(...tier.results)
|
|
1237
|
-
}
|
|
1238
|
-
|
|
1239
|
-
if (allResults.length === 0) {
|
|
1240
|
-
return {
|
|
1241
|
-
action: 'none',
|
|
1242
|
-
summary: 'No relevant information found',
|
|
1243
|
-
content: `No results found for: "${message.slice(0, 100)}"`,
|
|
1244
|
-
relevantItems: 0
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
const filtered = this.responseFilter.filter(allResults, message, displayProject)
|
|
1249
|
-
if (filtered.length === 0) {
|
|
1250
|
-
return {
|
|
1251
|
-
action: 'none',
|
|
1252
|
-
summary: 'Results filtered out as noise or irrelevant',
|
|
1253
|
-
content: `No relevant results after filtering for: "${message.slice(0, 100)}"`,
|
|
1254
|
-
relevantItems: 0
|
|
1255
|
-
}
|
|
1256
|
-
}
|
|
1257
|
-
|
|
1258
|
-
// Collect result IDs for detail lookups
|
|
1259
|
-
const resultIds = filtered
|
|
1260
|
-
.map(r => (r.metadata as any)?.id || (r.metadata as any)?.decision_id)
|
|
1261
|
-
.filter(Boolean)
|
|
1262
|
-
|
|
1263
|
-
const compactContent = formatCompactResponse(filtered, message)
|
|
1264
|
-
return {
|
|
1265
|
-
action: 'retrieved',
|
|
1266
|
-
summary: `Found ${filtered.length} result${filtered.length === 1 ? '' : 's'}`,
|
|
1267
|
-
content: compactContent,
|
|
1268
|
-
relevantItems: filtered.length
|
|
1269
|
-
}
|
|
1270
|
-
}
|
|
1271
|
-
|
|
1272
|
-
/**
|
|
1273
|
-
* Phase 19 C6: Enhanced exploration handler
|
|
1274
|
-
* - Timeline for "timeline"/"chronological" queries
|
|
1275
|
-
* - DecisionEvolutionTracker for "how has X changed" queries
|
|
1276
|
-
* - TrendDetector for "trends"/"emerging" queries
|
|
1277
|
-
*/
|
|
1278
|
-
private async handleExploration(
|
|
1279
|
-
message: string,
|
|
1280
|
-
project: string | undefined,
|
|
1281
|
-
entities: BrainExtractedEntities
|
|
1282
|
-
): Promise<BrainResponse> {
|
|
1283
|
-
if (!isServicesInitialized()) {
|
|
1284
|
-
return this.servicesNotReady()
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
// Phase 25: Use undefined for search when no project detected
|
|
1288
|
-
const searchProject = project || undefined
|
|
1289
|
-
const displayProject = project || DEFAULT_PROJECT
|
|
1290
|
-
const query = entities.topic || message
|
|
1291
|
-
const lower = message.toLowerCase()
|
|
1292
|
-
const tiers: TierResults[] = []
|
|
1293
|
-
|
|
1294
|
-
// C6: Timeline queries
|
|
1295
|
-
if (lower.includes('timeline') || lower.includes('chronological') || lower.includes('history of')) {
|
|
1296
|
-
const timeline = await this.searchEngine.buildTimeline({
|
|
1297
|
-
project: searchProject,
|
|
1298
|
-
topic: query,
|
|
1299
|
-
limit: 20
|
|
1300
|
-
})
|
|
1301
|
-
|
|
1302
|
-
if (timeline?.entries?.length) {
|
|
1303
|
-
tiers.push({
|
|
1304
|
-
label: 'Timeline',
|
|
1305
|
-
results: timeline.entries.map((e: any) => ({
|
|
1306
|
-
content: `**${e.date || ''}** — ${e.content || ''}`,
|
|
1307
|
-
score: 0.8,
|
|
1308
|
-
source: `Timeline (${e.type || 'event'})`,
|
|
1309
|
-
metadata: e.metadata || {}
|
|
1310
|
-
}))
|
|
1311
|
-
})
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1314
|
-
|
|
1315
|
-
// C6: Evolution queries
|
|
1316
|
-
if (lower.includes('evolution') || lower.includes('how has') || lower.includes('changed') || lower.includes('evolved')) {
|
|
1317
|
-
const evolution = await this.searchEngine.analyzeEvolution(query, { project: searchProject })
|
|
1318
|
-
if (evolution?.timeline?.length) {
|
|
1319
|
-
const parts = []
|
|
1320
|
-
parts.push(`**Stability:** ${evolution.stability || 'unknown'}`)
|
|
1321
|
-
if (evolution.currentState) parts.push(`**Current:** ${evolution.currentState}`)
|
|
1322
|
-
for (const change of (evolution.changes || []).slice(0, 5)) {
|
|
1323
|
-
parts.push(`- ${change.description || change}`)
|
|
1324
|
-
}
|
|
1325
|
-
|
|
1326
|
-
tiers.push({
|
|
1327
|
-
label: 'Decision Evolution',
|
|
1328
|
-
results: [{
|
|
1329
|
-
content: parts.join('\n'),
|
|
1330
|
-
score: 0.8,
|
|
1331
|
-
source: 'Evolution Analysis',
|
|
1332
|
-
metadata: {}
|
|
1333
|
-
}]
|
|
1334
|
-
})
|
|
1335
|
-
}
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
// C6: Trend queries
|
|
1339
|
-
if (lower.includes('trend') || lower.includes('emerging') || lower.includes('declining')) {
|
|
1340
|
-
const trends = await this.searchEngine.detectTrends({ project: searchProject })
|
|
1341
|
-
if (trends?.topTrends?.length) {
|
|
1342
|
-
tiers.push({
|
|
1343
|
-
label: 'Trends',
|
|
1344
|
-
results: trends.topTrends.slice(0, 5).map((t: any) => ({
|
|
1345
|
-
content: `**${t.term}** — ${t.trend} (${t.occurrences} occurrences, momentum: ${t.momentum || 'unknown'})`,
|
|
1346
|
-
score: t.occurrences > 5 ? 0.9 : 0.7,
|
|
1347
|
-
source: `Trend (${t.trend})`,
|
|
1348
|
-
metadata: {}
|
|
1349
|
-
}))
|
|
1350
|
-
})
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
// Always include graph exploration — cap scores when project filter is active
|
|
1355
|
-
// BUG-006 T4: Also require keyword overlap with query to prevent noise
|
|
1356
|
-
const explorationGraphCap = searchProject ? 0.25 : 0.5
|
|
1357
|
-
const explorationQueryWords = new Set(query.toLowerCase().split(/\s+/).filter(w => w.length > 2))
|
|
1358
|
-
const graphResults = await this.searchEngine.searchGraph(query, 10)
|
|
1359
|
-
const filteredGraphResults = graphResults.filter(g => {
|
|
1360
|
-
if (explorationQueryWords.size > 0) {
|
|
1361
|
-
const contentLower = g.content.toLowerCase()
|
|
1362
|
-
return [...explorationQueryWords].some(w => contentLower.includes(w))
|
|
1363
|
-
}
|
|
1364
|
-
return true
|
|
1365
|
-
})
|
|
1366
|
-
if (filteredGraphResults.length > 0) {
|
|
1367
|
-
tiers.push({
|
|
1368
|
-
label: 'Knowledge Graph',
|
|
1369
|
-
results: filteredGraphResults.map(g => ({
|
|
1370
|
-
content: g.content,
|
|
1371
|
-
score: Math.min(g.score, explorationGraphCap),
|
|
1372
|
-
source: 'Knowledge Graph',
|
|
1373
|
-
metadata: g.metadata as Record<string, unknown>
|
|
1374
|
-
}))
|
|
1375
|
-
})
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
// Issue 9: Cross-project patterns for exploration queries
|
|
1379
|
-
if (!project) {
|
|
1380
|
-
try {
|
|
1381
|
-
const crossProject = await this.searchEngine.findCrossProjectPatterns({
|
|
1382
|
-
query,
|
|
1383
|
-
limit: 3
|
|
1384
|
-
})
|
|
1385
|
-
if (crossProject?.patterns?.length) {
|
|
1386
|
-
tiers.push({
|
|
1387
|
-
label: 'Cross-Project Patterns',
|
|
1388
|
-
results: crossProject.patterns.map((p: any) => ({
|
|
1389
|
-
content: `${p.description || ''} (${p.projects?.join(', ') || 'multiple projects'})`,
|
|
1390
|
-
score: p.confidence || 0.5,
|
|
1391
|
-
source: 'Cross-Project',
|
|
1392
|
-
metadata: {}
|
|
1393
|
-
}))
|
|
1394
|
-
})
|
|
1395
|
-
}
|
|
1396
|
-
} catch {
|
|
1397
|
-
// Cross-project not available
|
|
1398
|
-
}
|
|
1399
|
-
}
|
|
1400
|
-
|
|
1401
|
-
// Also do a basic memory search as fallback
|
|
1402
|
-
const searchResults = await this.searchEngine.enhancedSearch(query, {
|
|
1403
|
-
project: searchProject,
|
|
1404
|
-
limit: 5,
|
|
1405
|
-
minSimilarity: 0.2
|
|
1406
|
-
})
|
|
1407
|
-
if (searchResults.length > 0) {
|
|
1408
|
-
tiers.push({
|
|
1409
|
-
label: 'Memories',
|
|
1410
|
-
results: searchResults.map(r => ({
|
|
1411
|
-
content: r.content,
|
|
1412
|
-
score: r.score,
|
|
1413
|
-
source: r.source === 'decision' ? 'Past Decision' : r.source,
|
|
1414
|
-
metadata: r.metadata as Record<string, unknown>
|
|
1415
|
-
}))
|
|
1416
|
-
})
|
|
1417
|
-
}
|
|
1418
|
-
|
|
1419
|
-
// Phase 32: Code intelligence for exploration queries
|
|
1420
|
-
const codeTier = this.queryCodeIntelligence(query, searchProject)
|
|
1421
|
-
if (codeTier) {
|
|
1422
|
-
tiers.push(codeTier)
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
return this.responseFilter.synthesize(tiers, message, displayProject, 'analyzed')
|
|
1426
|
-
}
|
|
1427
|
-
|
|
1428
|
-
private async handleComparison(
|
|
1429
|
-
message: string,
|
|
1430
|
-
project: string | undefined,
|
|
1431
|
-
entities: BrainExtractedEntities
|
|
1432
|
-
): Promise<BrainResponse> {
|
|
1433
|
-
if (!isServicesInitialized()) {
|
|
1434
|
-
return this.servicesNotReady()
|
|
1435
|
-
}
|
|
1436
|
-
|
|
1437
|
-
// Phase 25: Use undefined for search when no project detected
|
|
1438
|
-
const searchProject = project || undefined
|
|
1439
|
-
const displayProject = project || DEFAULT_PROJECT
|
|
1440
|
-
const query = entities.topic || message
|
|
1441
|
-
const tiers: TierResults[] = []
|
|
1442
|
-
|
|
1443
|
-
// Search for related decisions
|
|
1444
|
-
const searchResults = await this.searchEngine.enhancedSearch(query, {
|
|
1445
|
-
project: searchProject,
|
|
1446
|
-
limit: 5,
|
|
1447
|
-
minSimilarity: 0.2
|
|
1448
|
-
})
|
|
1449
|
-
if (searchResults.length > 0) {
|
|
1450
|
-
tiers.push({
|
|
1451
|
-
label: 'Related Decisions',
|
|
1452
|
-
results: searchResults.map(r => ({
|
|
1453
|
-
content: r.content,
|
|
1454
|
-
score: r.score,
|
|
1455
|
-
source: 'Past Decision',
|
|
1456
|
-
metadata: r.metadata as Record<string, unknown>
|
|
1457
|
-
}))
|
|
1458
|
-
})
|
|
1459
|
-
}
|
|
1460
|
-
|
|
1461
|
-
// Graph search for comparison context
|
|
1462
|
-
const graphResults = await this.searchEngine.searchGraph(query, 5)
|
|
1463
|
-
if (graphResults.length > 0) {
|
|
1464
|
-
tiers.push({
|
|
1465
|
-
label: 'Knowledge Graph',
|
|
1466
|
-
results: graphResults.map(g => ({
|
|
1467
|
-
content: g.content,
|
|
1468
|
-
score: g.score,
|
|
1469
|
-
source: 'Knowledge Graph',
|
|
1470
|
-
metadata: g.metadata as Record<string, unknown>
|
|
1471
|
-
}))
|
|
1472
|
-
})
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
|
-
return this.responseFilter.synthesize(tiers, message, displayProject, 'analyzed')
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
/**
|
|
1479
|
-
* Phase 27: Handle detail requests — "details obs_abc123", "expand <id>"
|
|
1480
|
-
* Looks up a single observation by ID and returns full detail view.
|
|
1481
|
-
*/
|
|
1482
|
-
private async handleDetailRequest(
|
|
1483
|
-
message: string,
|
|
1484
|
-
project: string | undefined,
|
|
1485
|
-
_entities: BrainExtractedEntities
|
|
1486
|
-
): Promise<BrainResponse> {
|
|
1487
|
-
if (!isServicesInitialized()) {
|
|
1488
|
-
return this.servicesNotReady()
|
|
1489
|
-
}
|
|
1490
|
-
|
|
1491
|
-
// Extract ID from message using regex
|
|
1492
|
-
const idMatch = message.match(/\b(obs_\w+|[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}|[a-f0-9]{8,})\b/i)
|
|
1493
|
-
if (!idMatch) {
|
|
1494
|
-
return {
|
|
1495
|
-
action: 'none',
|
|
1496
|
-
summary: 'No observation ID provided',
|
|
1497
|
-
content: 'Please provide an observation ID. Use brain("details {ID}") with an ID from search results.',
|
|
1498
|
-
relevantItems: 0
|
|
1499
|
-
}
|
|
1500
|
-
}
|
|
1501
|
-
|
|
1502
|
-
const id = idMatch[1]
|
|
1503
|
-
const observation = await this.lookupById(id)
|
|
1504
|
-
|
|
1505
|
-
if (!observation) {
|
|
1506
|
-
return {
|
|
1507
|
-
action: 'none',
|
|
1508
|
-
summary: `No observation found: ${id}`,
|
|
1509
|
-
content: `No observation found with ID: ${id}`,
|
|
1510
|
-
relevantItems: 0
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
|
-
// Increment access count
|
|
1515
|
-
this.incrementAccess(id)
|
|
1516
|
-
|
|
1517
|
-
return {
|
|
1518
|
-
action: 'retrieved',
|
|
1519
|
-
summary: `Details for ${id.slice(0, 12)}...`,
|
|
1520
|
-
content: formatDetailResponse(observation),
|
|
1521
|
-
relevantItems: 1
|
|
1522
|
-
}
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
|
-
/**
|
|
1526
|
-
* Phase 27: Handle timeline requests — "timeline for project", "recent activity"
|
|
1527
|
-
* Fetches observations in a time range and groups by day.
|
|
1528
|
-
*/
|
|
1529
|
-
private async handleTimeline(
|
|
1530
|
-
message: string,
|
|
1531
|
-
project: string | undefined,
|
|
1532
|
-
entities: BrainExtractedEntities
|
|
1533
|
-
): Promise<BrainResponse> {
|
|
1534
|
-
if (!isServicesInitialized()) {
|
|
1535
|
-
return this.servicesNotReady()
|
|
1536
|
-
}
|
|
1537
|
-
|
|
1538
|
-
const effectiveProject = entities.project || project
|
|
1539
|
-
const displayProject = effectiveProject || DEFAULT_PROJECT
|
|
1540
|
-
|
|
1541
|
-
// Parse time range from message (default: last 7 days)
|
|
1542
|
-
const now = new Date()
|
|
1543
|
-
let start: Date
|
|
1544
|
-
let daysBack = 7
|
|
1545
|
-
const lower = message.toLowerCase()
|
|
1546
|
-
if (lower.includes('yesterday')) {
|
|
1547
|
-
daysBack = 1
|
|
1548
|
-
start = new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000)
|
|
1549
|
-
} else if (lower.includes('today')) {
|
|
1550
|
-
daysBack = 0
|
|
1551
|
-
// Start of today, not "now"
|
|
1552
|
-
start = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
|
1553
|
-
} else if (lower.includes('last month') || lower.includes('past month')) {
|
|
1554
|
-
daysBack = 30
|
|
1555
|
-
start = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000)
|
|
1556
|
-
} else if (lower.includes('last week') || lower.includes('past week')) {
|
|
1557
|
-
daysBack = 7
|
|
1558
|
-
start = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
|
|
1559
|
-
} else if (lower.includes('last 3 days') || lower.includes('past 3 days')) {
|
|
1560
|
-
daysBack = 3
|
|
1561
|
-
start = new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000)
|
|
1562
|
-
} else {
|
|
1563
|
-
start = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
const observations = await this.fetchRecentObservations(effectiveProject, start, now)
|
|
1567
|
-
if (observations.length === 0) {
|
|
1568
|
-
return {
|
|
1569
|
-
action: 'none',
|
|
1570
|
-
summary: `No activity found for ${displayProject}`,
|
|
1571
|
-
content: `No observations found in the last ${daysBack === 0 ? 'day' : `${daysBack} day(s)`} for ${displayProject}.`,
|
|
1572
|
-
relevantItems: 0
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
|
-
const grouped = groupByDay(observations)
|
|
1577
|
-
const timelineContent = formatTimeline(grouped, displayProject)
|
|
1578
|
-
|
|
1579
|
-
return {
|
|
1580
|
-
action: 'retrieved',
|
|
1581
|
-
summary: `Timeline for ${displayProject} (${observations.length} items)`,
|
|
1582
|
-
content: timelineContent,
|
|
1583
|
-
relevantItems: observations.length
|
|
1584
|
-
}
|
|
1585
|
-
}
|
|
1586
|
-
|
|
1587
|
-
/**
|
|
1588
|
-
* Phase 27: Look up a single observation by ID.
|
|
1589
|
-
* Tries FTS5 first, then falls back to ChromaDB search.
|
|
1590
|
-
*/
|
|
1591
|
-
private async lookupById(id: string): Promise<any | null> {
|
|
1592
|
-
const memory = getMemoryService()
|
|
1593
|
-
|
|
1594
|
-
// Try FTS5 first (fast, direct lookup)
|
|
1595
|
-
if (memory.fts5) {
|
|
1596
|
-
const result = memory.fts5.getById(id)
|
|
1597
|
-
if (result) return result
|
|
1598
|
-
}
|
|
1599
|
-
|
|
1600
|
-
// Fallback: search ChromaDB by ID if available
|
|
1601
|
-
if (memory.isChromaDBEnabled()) {
|
|
1602
|
-
try {
|
|
1603
|
-
const collections = memory.chroma.collections
|
|
1604
|
-
// Try decisions collection
|
|
1605
|
-
const decisions = await collections.getDecisions()
|
|
1606
|
-
if (decisions) {
|
|
1607
|
-
try {
|
|
1608
|
-
const result = await decisions.get({ ids: [id] })
|
|
1609
|
-
if (result?.documents?.[0]) {
|
|
1610
|
-
return {
|
|
1611
|
-
id,
|
|
1612
|
-
content: result.documents[0],
|
|
1613
|
-
metadata: result.metadatas?.[0] || {},
|
|
1614
|
-
category: 'decision',
|
|
1615
|
-
project: (result.metadatas?.[0] as any)?.project || '',
|
|
1616
|
-
created_at: (result.metadatas?.[0] as any)?.created_at || '',
|
|
1617
|
-
reasoning: (result.metadatas?.[0] as any)?.reasoning || null,
|
|
1618
|
-
tags: [],
|
|
1619
|
-
confidence: 0.8
|
|
1620
|
-
}
|
|
1621
|
-
}
|
|
1622
|
-
} catch { /* ID not in this collection */ }
|
|
1623
|
-
}
|
|
1624
|
-
} catch {
|
|
1625
|
-
// ChromaDB lookup failed
|
|
1626
|
-
}
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
return null
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
/**
|
|
1633
|
-
* Phase 27: Fetch recent observations in a date range.
|
|
1634
|
-
* Tries FTS5 first, then falls back to ChromaDB temporal search.
|
|
1635
|
-
*/
|
|
1636
|
-
private async fetchRecentObservations(
|
|
1637
|
-
project: string | undefined,
|
|
1638
|
-
start: Date,
|
|
1639
|
-
end: Date
|
|
1640
|
-
): Promise<any[]> {
|
|
1641
|
-
const memory = getMemoryService()
|
|
1642
|
-
|
|
1643
|
-
// Try FTS5 first (efficient SQL-level date filtering)
|
|
1644
|
-
if (memory.fts5 && project) {
|
|
1645
|
-
return memory.fts5.fetchByTimeRange(project, start, end)
|
|
1646
|
-
}
|
|
1647
|
-
|
|
1648
|
-
// Also try FTS5 without project filter
|
|
1649
|
-
if (memory.fts5 && !project) {
|
|
1650
|
-
// FTS5 fetchByTimeRange requires project, use fetchAll with date filtering
|
|
1651
|
-
try {
|
|
1652
|
-
const all = memory.fts5.search('*', undefined, 100)
|
|
1653
|
-
return all.filter(r => {
|
|
1654
|
-
const date = new Date(r.created_at)
|
|
1655
|
-
if (isNaN(date.getTime())) return false
|
|
1656
|
-
return date >= start && date <= end
|
|
1657
|
-
}).sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())
|
|
1658
|
-
} catch {
|
|
1659
|
-
// FTS5 search failed, fall through
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
|
|
1663
|
-
// Fallback: use memory fetchAll with date filtering
|
|
1664
|
-
try {
|
|
1665
|
-
const allDecisions = await memory.fetchAllDecisions(project)
|
|
1666
|
-
const allPatterns = await memory.fetchAllPatterns(project)
|
|
1667
|
-
const allCorrections = await memory.fetchAllCorrections(project)
|
|
1668
|
-
|
|
1669
|
-
const allItems = [
|
|
1670
|
-
...allDecisions.map((d: any) => ({
|
|
1671
|
-
id: d.id || d.decision_id,
|
|
1672
|
-
content: d.decision || d.document || d.content || '',
|
|
1673
|
-
category: 'decision',
|
|
1674
|
-
project: d.project || project || DEFAULT_PROJECT,
|
|
1675
|
-
created_at: d.created_at || d.date || '',
|
|
1676
|
-
})),
|
|
1677
|
-
...allPatterns.map((p: any) => ({
|
|
1678
|
-
id: p.id,
|
|
1679
|
-
content: p.description || p.document || p.content || '',
|
|
1680
|
-
category: p.pattern_type || 'pattern',
|
|
1681
|
-
project: p.project || project || DEFAULT_PROJECT,
|
|
1682
|
-
created_at: p.created_at || '',
|
|
1683
|
-
})),
|
|
1684
|
-
...allCorrections.map((c: any) => ({
|
|
1685
|
-
id: c.id,
|
|
1686
|
-
content: c.original || c.document || c.content || '',
|
|
1687
|
-
category: 'correction',
|
|
1688
|
-
project: c.project || project || DEFAULT_PROJECT,
|
|
1689
|
-
created_at: c.created_at || '',
|
|
1690
|
-
})),
|
|
1691
|
-
]
|
|
1692
|
-
|
|
1693
|
-
return allItems.filter(item => {
|
|
1694
|
-
if (!item.created_at) return true // include items without dates
|
|
1695
|
-
const date = new Date(item.created_at)
|
|
1696
|
-
if (isNaN(date.getTime())) return true
|
|
1697
|
-
return date >= start && date <= end
|
|
1698
|
-
}).sort((a, b) => {
|
|
1699
|
-
const dateA = new Date(a.created_at || 0).getTime()
|
|
1700
|
-
const dateB = new Date(b.created_at || 0).getTime()
|
|
1701
|
-
return dateB - dateA
|
|
1702
|
-
})
|
|
1703
|
-
} catch {
|
|
1704
|
-
return []
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
/**
|
|
1709
|
-
* Phase 27: Increment access count for an observation.
|
|
1710
|
-
*/
|
|
1711
|
-
private incrementAccess(id: string): void {
|
|
1712
|
-
try {
|
|
1713
|
-
const memory = getMemoryService()
|
|
1714
|
-
if (memory.fts5) {
|
|
1715
|
-
memory.fts5.recordAccess(id)
|
|
1716
|
-
}
|
|
1717
|
-
} catch {
|
|
1718
|
-
// Non-critical
|
|
1719
|
-
}
|
|
1720
|
-
}
|
|
1721
|
-
|
|
1722
|
-
// ===== Helpers =====
|
|
1723
|
-
|
|
1724
|
-
/**
|
|
1725
|
-
* Phase 32: Query code intelligence for symbols and files matching a query.
|
|
1726
|
-
* Returns a TierResults if matches found, null otherwise.
|
|
1727
|
-
* Fast operation — direct SQLite queries, no network calls.
|
|
1728
|
-
*/
|
|
1729
|
-
private queryCodeIntelligence(query: string, project: string | undefined): TierResults | null {
|
|
1730
|
-
try {
|
|
1731
|
-
const codeQuery = getCodeQuery()
|
|
1732
|
-
if (!codeQuery) return null
|
|
1733
|
-
|
|
1734
|
-
// Extract meaningful keywords from query for code search
|
|
1735
|
-
const keywords = this.extractCodeKeywords(query)
|
|
1736
|
-
if (keywords.length === 0) return null
|
|
1737
|
-
|
|
1738
|
-
const results: FilterableResult[] = []
|
|
1739
|
-
|
|
1740
|
-
for (const keyword of keywords) {
|
|
1741
|
-
// Search symbols across all indexed projects (project may not match code index project name)
|
|
1742
|
-
const symbolResults = codeQuery.findSymbols(keyword, project || '', 5)
|
|
1743
|
-
|
|
1744
|
-
// If no results with project filter, try without (code index may use path-based project names)
|
|
1745
|
-
const symbols = symbolResults.length > 0
|
|
1746
|
-
? symbolResults
|
|
1747
|
-
: codeQuery.findSymbols(keyword, '', 5)
|
|
1748
|
-
|
|
1749
|
-
for (const sym of symbols.slice(0, 3)) {
|
|
1750
|
-
const location = sym.lineEnd
|
|
1751
|
-
? `${sym.filePath}:${sym.lineStart}-${sym.lineEnd}`
|
|
1752
|
-
: `${sym.filePath}:${sym.lineStart}`
|
|
1753
|
-
const sigPart = sym.signature ? ` — \`${sym.signature}\`` : ''
|
|
1754
|
-
results.push({
|
|
1755
|
-
content: `**${sym.symbol}** (${sym.type}) at \`${location}\`${sigPart}`,
|
|
1756
|
-
score: sym.confidence * 0.6, // Scale down so memories rank higher
|
|
1757
|
-
source: 'Code Index',
|
|
1758
|
-
metadata: {
|
|
1759
|
-
filePath: sym.filePath,
|
|
1760
|
-
lineStart: sym.lineStart,
|
|
1761
|
-
lineEnd: sym.lineEnd,
|
|
1762
|
-
symbolType: sym.type,
|
|
1763
|
-
}
|
|
1764
|
-
})
|
|
1765
|
-
}
|
|
1766
|
-
|
|
1767
|
-
// Also search files by name/summary
|
|
1768
|
-
const fileResults = codeQuery.findFiles(keyword, project || '')
|
|
1769
|
-
const files = fileResults.length > 0
|
|
1770
|
-
? fileResults
|
|
1771
|
-
: codeQuery.findFiles(keyword, '')
|
|
1772
|
-
|
|
1773
|
-
for (const file of files.slice(0, 3)) {
|
|
1774
|
-
// Skip if we already have symbol results from this file
|
|
1775
|
-
if (results.some(r => (r.metadata as any)?.filePath === file.filePath)) continue
|
|
1776
|
-
const summaryPart = file.summary ? ` — ${file.summary}` : ''
|
|
1777
|
-
results.push({
|
|
1778
|
-
content: `**${file.filePath}** (${file.language || 'unknown'}, ${file.symbolCount} symbols, ${file.lineCount} lines)${summaryPart}`,
|
|
1779
|
-
score: 0.4, // File matches are lower confidence
|
|
1780
|
-
source: 'Code Index',
|
|
1781
|
-
metadata: {
|
|
1782
|
-
filePath: file.filePath,
|
|
1783
|
-
symbolCount: file.symbolCount,
|
|
1784
|
-
}
|
|
1785
|
-
})
|
|
1786
|
-
}
|
|
1787
|
-
}
|
|
1788
|
-
|
|
1789
|
-
// Deduplicate by filePath
|
|
1790
|
-
const seen = new Set<string>()
|
|
1791
|
-
const deduped = results.filter(r => {
|
|
1792
|
-
const fp = (r.metadata as any)?.filePath
|
|
1793
|
-
if (fp && seen.has(fp)) return false
|
|
1794
|
-
if (fp) seen.add(fp)
|
|
1795
|
-
return true
|
|
1796
|
-
})
|
|
1797
|
-
|
|
1798
|
-
if (deduped.length === 0) return null
|
|
1799
|
-
|
|
1800
|
-
return {
|
|
1801
|
-
label: 'Code Intelligence',
|
|
1802
|
-
results: deduped.slice(0, 5)
|
|
1803
|
-
}
|
|
1804
|
-
} catch (error) {
|
|
1805
|
-
this.logger.debug({ error }, 'Code intelligence query failed (non-fatal)')
|
|
1806
|
-
return null
|
|
1807
|
-
}
|
|
1808
|
-
}
|
|
1809
|
-
|
|
1810
|
-
/**
|
|
1811
|
-
* Extract meaningful keywords from a natural language query for code search.
|
|
1812
|
-
* Filters out stop words and common filler, returns words likely to match code symbols.
|
|
1813
|
-
*/
|
|
1814
|
-
private extractCodeKeywords(query: string): string[] {
|
|
1815
|
-
const STOP_WORDS = new Set([
|
|
1816
|
-
'how', 'what', 'where', 'when', 'why', 'who', 'which', 'are', 'is', 'was',
|
|
1817
|
-
'were', 'been', 'being', 'have', 'has', 'had', 'having', 'do', 'does', 'did',
|
|
1818
|
-
'doing', 'will', 'would', 'could', 'should', 'shall', 'can', 'may', 'might',
|
|
1819
|
-
'must', 'the', 'a', 'an', 'and', 'but', 'or', 'nor', 'for', 'yet', 'so',
|
|
1820
|
-
'in', 'on', 'at', 'to', 'from', 'by', 'with', 'about', 'into', 'through',
|
|
1821
|
-
'during', 'before', 'after', 'above', 'below', 'between', 'out', 'off', 'over',
|
|
1822
|
-
'under', 'again', 'further', 'then', 'once', 'here', 'there', 'all', 'each',
|
|
1823
|
-
'every', 'both', 'few', 'more', 'most', 'other', 'some', 'such', 'only', 'own',
|
|
1824
|
-
'same', 'than', 'too', 'very', 'just', 'because', 'not', 'this', 'that', 'these',
|
|
1825
|
-
'those', 'its', 'our', 'their', 'your', 'my', 'me', 'we', 'you', 'they', 'them',
|
|
1826
|
-
'know', 'tell', 'show', 'find', 'get', 'make', 'use', 'using', 'used', 'managing',
|
|
1827
|
-
'managed', 'handle', 'handling', 'handled', 'work', 'working', 'works', 'implement',
|
|
1828
|
-
'implementing', 'implemented', 'doing', 'done', 'project', 'code', 'file', 'function',
|
|
1829
|
-
])
|
|
1830
|
-
|
|
1831
|
-
const words = query.toLowerCase()
|
|
1832
|
-
.replace(/[^a-z0-9\s-_]/g, ' ')
|
|
1833
|
-
.split(/\s+/)
|
|
1834
|
-
.filter(w => w.length > 2 && !STOP_WORDS.has(w))
|
|
1835
|
-
|
|
1836
|
-
// Also try multi-word phrases (camelCase/kebab-case patterns the user might reference)
|
|
1837
|
-
const phrases: string[] = []
|
|
1838
|
-
for (let i = 0; i < words.length - 1; i++) {
|
|
1839
|
-
// Create camelCase combo: "brain router" → "brainRouter"
|
|
1840
|
-
phrases.push(words[i] + words[i + 1].charAt(0).toUpperCase() + words[i + 1].slice(1))
|
|
1841
|
-
}
|
|
1842
|
-
|
|
1843
|
-
// Return unique keywords, single words first then phrases
|
|
1844
|
-
return [...new Set([...words, ...phrases])].slice(0, 5)
|
|
1845
|
-
}
|
|
1846
|
-
|
|
1847
|
-
/**
|
|
1848
|
-
* Phase 29: Link a stored observation to code files (non-blocking, fire-and-forget).
|
|
1849
|
-
*/
|
|
1850
|
-
private linkToCodeFiles(observationId: string, content: string, project: string): void {
|
|
1851
|
-
try {
|
|
1852
|
-
const linker = getCodeLinker()
|
|
1853
|
-
if (linker) {
|
|
1854
|
-
linker.linkObservation(observationId, content, project).catch(err => {
|
|
1855
|
-
this.logger.debug({ err }, 'Code linkage failed (non-fatal)')
|
|
1856
|
-
})
|
|
1857
|
-
}
|
|
1858
|
-
} catch {
|
|
1859
|
-
// Linker not available
|
|
1860
|
-
}
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
|
-
private async writeToVault(
|
|
1864
|
-
project: string,
|
|
1865
|
-
decision: string,
|
|
1866
|
-
reasoning: string,
|
|
1867
|
-
context: string,
|
|
1868
|
-
alternatives?: string
|
|
1869
|
-
): Promise<void> {
|
|
1870
|
-
try {
|
|
1871
|
-
const vault = getVaultService()
|
|
1872
|
-
const projectPaths = vault.getProjectPaths(project)
|
|
1873
|
-
const date = new Date().toISOString().split('T')[0]
|
|
1874
|
-
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`
|
|
1875
|
-
await vault.writer.appendContent(projectPaths.decisions, entry, '\n')
|
|
1876
|
-
} catch {
|
|
1877
|
-
// Vault write failed
|
|
1878
|
-
}
|
|
1879
|
-
}
|
|
1880
|
-
|
|
1881
|
-
/**
|
|
1882
|
-
* Store as a new decision (fallback for update when no match found)
|
|
1883
|
-
*/
|
|
1884
|
-
private async storeAsNew(
|
|
1885
|
-
memory: any,
|
|
1886
|
-
project: string,
|
|
1887
|
-
message: string,
|
|
1888
|
-
topic: string,
|
|
1889
|
-
entities: BrainExtractedEntities,
|
|
1890
|
-
reason: string
|
|
1891
|
-
): Promise<BrainResponse> {
|
|
1892
|
-
const decisionId = await memory.rememberDecision(
|
|
1893
|
-
project,
|
|
1894
|
-
topic.slice(0, 200),
|
|
1895
|
-
message,
|
|
1896
|
-
entities.reasoning || '',
|
|
1897
|
-
{ tags: entities.technologies.length > 0 ? entities.technologies : ['updated'] }
|
|
1898
|
-
)
|
|
1899
|
-
|
|
1900
|
-
this.lastStoredId = decisionId
|
|
1901
|
-
this.lastStoredProject = project
|
|
1902
|
-
this.searchEngine.invalidateCache(project)
|
|
1903
|
-
|
|
1904
|
-
return {
|
|
1905
|
-
action: 'stored',
|
|
1906
|
-
summary: `Stored as new (${reason})`,
|
|
1907
|
-
content: `Stored as new decision (ID: ${decisionId})\n\n**Project:** ${project}\n**Content:** ${message}`,
|
|
1908
|
-
relevantItems: 1
|
|
1909
|
-
}
|
|
1910
|
-
}
|
|
1911
|
-
|
|
1912
|
-
/**
|
|
1913
|
-
* C8: Register a message with the episode manager
|
|
1914
|
-
*/
|
|
1915
|
-
private registerEpisodeMessage(message: string, project?: string, role: string = 'user'): void {
|
|
1916
|
-
try {
|
|
1917
|
-
const episodeManager = getEpisodeService()
|
|
1918
|
-
if (!episodeManager) return
|
|
1919
|
-
episodeManager.processMessage(
|
|
1920
|
-
{ role: role as 'user' | 'assistant' | 'system', content: message, timestamp: new Date().toISOString() },
|
|
1921
|
-
project
|
|
1922
|
-
).catch(() => {})
|
|
1923
|
-
} catch {
|
|
1924
|
-
// Episode manager not available
|
|
1925
|
-
}
|
|
1926
|
-
}
|
|
1927
|
-
|
|
1928
|
-
/**
|
|
1929
|
-
* C8: Link a stored decision/pattern/correction to the active episode
|
|
1930
|
-
*/
|
|
1931
|
-
private linkToActiveEpisode(project: string, id: string, type: 'decision' | 'pattern' | 'correction'): void {
|
|
1932
|
-
try {
|
|
1933
|
-
const episodeManager = getEpisodeService()
|
|
1934
|
-
if (!episodeManager) return
|
|
1935
|
-
const activeEpisode = episodeManager.getActiveEpisode(project)
|
|
1936
|
-
if (!activeEpisode) return
|
|
1937
|
-
|
|
1938
|
-
if (type === 'decision') episodeManager.linkDecision(activeEpisode.id, id)
|
|
1939
|
-
else if (type === 'pattern') episodeManager.linkPattern(activeEpisode.id, id)
|
|
1940
|
-
else if (type === 'correction') episodeManager.linkCorrection(activeEpisode.id, id)
|
|
1941
|
-
} catch {
|
|
1942
|
-
// Episode manager not available
|
|
1943
|
-
}
|
|
1944
|
-
}
|
|
1945
|
-
|
|
1946
|
-
/**
|
|
1947
|
-
* Phase 23b: Detect session recall queries ("what have we discussed", "this session", etc.)
|
|
1948
|
-
*/
|
|
1949
|
-
private isSessionRecallQuery(message: string): boolean {
|
|
1950
|
-
const lower = message.toLowerCase()
|
|
1951
|
-
const SESSION_RECALL_PHRASES = [
|
|
1952
|
-
'what have we discussed',
|
|
1953
|
-
'what did we discuss',
|
|
1954
|
-
'what have we talked about',
|
|
1955
|
-
'what did we talk about',
|
|
1956
|
-
'this session',
|
|
1957
|
-
'current session',
|
|
1958
|
-
'session so far',
|
|
1959
|
-
'what have we done',
|
|
1960
|
-
'what did we do',
|
|
1961
|
-
'session summary',
|
|
1962
|
-
'summarize this session',
|
|
1963
|
-
'recap this session',
|
|
1964
|
-
'what happened this session',
|
|
1965
|
-
'what have we covered'
|
|
1966
|
-
]
|
|
1967
|
-
return SESSION_RECALL_PHRASES.some(p => lower.includes(p))
|
|
1968
|
-
}
|
|
1969
|
-
|
|
1970
|
-
/**
|
|
1971
|
-
* C7: Detect complex multi-part questions
|
|
1972
|
-
*/
|
|
1973
|
-
private isComplexQuestion(message: string): boolean {
|
|
1974
|
-
// Multiple question marks
|
|
1975
|
-
const questionMarks = (message.match(/\?/g) || []).length
|
|
1976
|
-
if (questionMarks >= 2) return true
|
|
1977
|
-
// Very long question (likely multi-part)
|
|
1978
|
-
if (message.length > 200 && message.includes('?')) return true
|
|
1979
|
-
// Multiple clauses with "and" or "also"
|
|
1980
|
-
if (message.includes(' and ') && message.includes('?') && message.length > 100) return true
|
|
1981
|
-
return false
|
|
1982
|
-
}
|
|
1983
|
-
|
|
1984
|
-
private servicesNotReady(): BrainResponse {
|
|
1985
|
-
return {
|
|
1986
|
-
action: 'none',
|
|
1987
|
-
summary: 'Services not initialized',
|
|
1988
|
-
content: 'Claude Brain services are not ready. The server may still be starting up.',
|
|
1989
|
-
relevantItems: 0
|
|
1990
|
-
}
|
|
1991
|
-
}
|
|
1992
|
-
|
|
1993
|
-
/**
|
|
1994
|
-
* Check if a delete/update message has descriptive content beyond just the command words.
|
|
1995
|
-
*/
|
|
1996
|
-
private hasDescriptiveContent(message: string): boolean {
|
|
1997
|
-
const COMMAND_WORDS = [
|
|
1998
|
-
'forget', 'delete', 'remove', 'discard', 'erase', 'undo', 'clear', 'drop',
|
|
1999
|
-
'actually', 'correction', 'update', 'change', 'replace', 'modify', 'revise',
|
|
2000
|
-
'amend', 'override', 'scratch', 'no', 'wait', 'instead',
|
|
2001
|
-
'that', 'this', 'the', 'it', 'about', 'memory', 'decision', 'to', 'a', 'an'
|
|
2002
|
-
]
|
|
2003
|
-
const words = message.toLowerCase().split(/[\s,;:.!?]+/).filter(w => w.length > 1)
|
|
2004
|
-
const meaningfulWords = words.filter(w => !COMMAND_WORDS.includes(w))
|
|
2005
|
-
return meaningfulWords.length >= 2
|
|
2006
|
-
}
|
|
2007
|
-
|
|
2008
|
-
/**
|
|
2009
|
-
* Bug 4: Track a recently stored decision for recency fast path
|
|
2010
|
-
*/
|
|
2011
|
-
private trackRecentDecision(content: string, project: string): void {
|
|
2012
|
-
const now = Date.now()
|
|
2013
|
-
// Prune expired entries
|
|
2014
|
-
this.recentDecisions = this.recentDecisions.filter(
|
|
2015
|
-
d => now - d.storedAt < RECENT_DECISION_WINDOW_MS
|
|
2016
|
-
)
|
|
2017
|
-
this.recentDecisions.push({ content, project, storedAt: now })
|
|
2018
|
-
}
|
|
2019
|
-
|
|
2020
|
-
/**
|
|
2021
|
-
* Bug 4: Find recent decisions matching a query by keyword overlap.
|
|
2022
|
-
* Returns matches with their overlapScore (ratio of query keywords found in content).
|
|
2023
|
-
*/
|
|
2024
|
-
private findRecentDecisions(query: string, project?: string): RecentDecision[] {
|
|
2025
|
-
const now = Date.now()
|
|
2026
|
-
// Prune expired
|
|
2027
|
-
this.recentDecisions = this.recentDecisions.filter(
|
|
2028
|
-
d => now - d.storedAt < RECENT_DECISION_WINDOW_MS
|
|
2029
|
-
)
|
|
2030
|
-
|
|
2031
|
-
const queryWords = new Set(
|
|
2032
|
-
query.toLowerCase().split(/\s+/).filter(w => w.length > 2)
|
|
2033
|
-
)
|
|
2034
|
-
if (queryWords.size === 0) return []
|
|
2035
|
-
|
|
2036
|
-
const matches: RecentDecision[] = []
|
|
2037
|
-
for (const d of this.recentDecisions) {
|
|
2038
|
-
// Project filter: skip if searching a specific project and it doesn't match
|
|
2039
|
-
if (project && d.project !== project) continue
|
|
2040
|
-
const contentWords = new Set(d.content.toLowerCase().split(/\s+/).filter(w => w.length > 2))
|
|
2041
|
-
const overlap = [...queryWords].filter(w => contentWords.has(w)).length
|
|
2042
|
-
if (overlap >= 1) {
|
|
2043
|
-
const overlapScore = overlap / queryWords.size
|
|
2044
|
-
matches.push({ ...d, overlapScore })
|
|
2045
|
-
}
|
|
2046
|
-
}
|
|
2047
|
-
return matches
|
|
2048
|
-
}
|
|
2049
|
-
|
|
2050
|
-
/**
|
|
2051
|
-
* Phase 30: Optionally compress content before storing.
|
|
2052
|
-
* Returns the (possibly compressed) content and original if compressed.
|
|
2053
|
-
*/
|
|
2054
|
-
private async maybeCompress(content: string, category: string): Promise<{ content: string; rawContent?: string }> {
|
|
2055
|
-
if (!this.compressor) return { content }
|
|
2056
|
-
try {
|
|
2057
|
-
const result = await this.compressor.compress(content, category)
|
|
2058
|
-
if (result.compressed) {
|
|
2059
|
-
return { content: result.summary, rawContent: result.original }
|
|
2060
|
-
}
|
|
2061
|
-
} catch {
|
|
2062
|
-
// Compression failed, use original
|
|
2063
|
-
}
|
|
2064
|
-
return { content }
|
|
2065
|
-
}
|
|
2066
|
-
|
|
2067
|
-
/**
|
|
2068
|
-
* BUG-002: Detect category-based intent from question messages.
|
|
2069
|
-
* Returns the category if the user is asking about a specific type of memory.
|
|
2070
|
-
*/
|
|
2071
|
-
private detectCategoryIntent(message: string): 'decision' | 'pattern' | 'correction' | null {
|
|
2072
|
-
const lower = message.toLowerCase()
|
|
2073
|
-
|
|
2074
|
-
// Decision-oriented queries
|
|
2075
|
-
if (
|
|
2076
|
-
lower.includes('what decisions') || lower.includes('my decisions') ||
|
|
2077
|
-
lower.includes('what have i decided') || lower.includes('what did i decide') ||
|
|
2078
|
-
lower.includes('what choices') || lower.includes('decisions i') ||
|
|
2079
|
-
lower.includes('list decisions') || lower.includes('show decisions') ||
|
|
2080
|
-
lower.includes('all decisions') || lower.includes('what did i choose')
|
|
2081
|
-
) {
|
|
2082
|
-
return 'decision'
|
|
2083
|
-
}
|
|
2084
|
-
|
|
2085
|
-
// Pattern-oriented queries
|
|
2086
|
-
if (
|
|
2087
|
-
lower.includes('what patterns') || lower.includes('my patterns') ||
|
|
2088
|
-
lower.includes('best practices') || lower.includes('conventions') ||
|
|
2089
|
-
lower.includes('list patterns') || lower.includes('show patterns') ||
|
|
2090
|
-
lower.includes('all patterns')
|
|
2091
|
-
) {
|
|
2092
|
-
return 'pattern'
|
|
2093
|
-
}
|
|
2094
|
-
|
|
2095
|
-
// Correction-oriented queries
|
|
2096
|
-
if (
|
|
2097
|
-
lower.includes('what mistakes') || lower.includes('my mistakes') ||
|
|
2098
|
-
lower.includes('what bugs') || lower.includes('lessons learned') ||
|
|
2099
|
-
lower.includes('what corrections') || lower.includes('my corrections') ||
|
|
2100
|
-
lower.includes('list corrections') || lower.includes('show corrections') ||
|
|
2101
|
-
lower.includes('all corrections') || lower.includes('what have i fixed') ||
|
|
2102
|
-
lower.includes('what did i fix')
|
|
2103
|
-
) {
|
|
2104
|
-
return 'correction'
|
|
2105
|
-
}
|
|
2106
|
-
|
|
2107
|
-
return null
|
|
2108
|
-
}
|
|
2109
|
-
|
|
2110
|
-
/**
|
|
2111
|
-
* BUG-002: Handle category-scoped queries by fetching all items of that category.
|
|
2112
|
-
*/
|
|
2113
|
-
private async handleCategoryQuery(
|
|
2114
|
-
message: string,
|
|
2115
|
-
project: string | undefined,
|
|
2116
|
-
category: 'decision' | 'pattern' | 'correction'
|
|
2117
|
-
): Promise<BrainResponse> {
|
|
2118
|
-
const memory = getMemoryService()
|
|
2119
|
-
const projectLabel = project || 'all projects'
|
|
2120
|
-
|
|
2121
|
-
let items: any[]
|
|
2122
|
-
switch (category) {
|
|
2123
|
-
case 'decision':
|
|
2124
|
-
items = await memory.fetchAllDecisions(project)
|
|
2125
|
-
break
|
|
2126
|
-
case 'pattern':
|
|
2127
|
-
items = await memory.fetchAllPatterns(project)
|
|
2128
|
-
break
|
|
2129
|
-
case 'correction':
|
|
2130
|
-
items = await memory.fetchAllCorrections(project)
|
|
2131
|
-
break
|
|
2132
|
-
}
|
|
2133
|
-
|
|
2134
|
-
if (items.length === 0) {
|
|
2135
|
-
return {
|
|
2136
|
-
action: 'none',
|
|
2137
|
-
summary: `No ${category}s found`,
|
|
2138
|
-
content: `No ${category}s found for ${projectLabel}.`,
|
|
2139
|
-
relevantItems: 0
|
|
2140
|
-
}
|
|
2141
|
-
}
|
|
2142
|
-
|
|
2143
|
-
// Format as compact items
|
|
2144
|
-
const allItems = items.map(item => ({
|
|
2145
|
-
id: item.id || item.decision_id,
|
|
2146
|
-
content: typeof (item.decision || item.description || item.correction || item.original || item.document || item.content) === 'string'
|
|
2147
|
-
? (item.decision || item.description || item.correction || item.original || item.document || item.content)
|
|
2148
|
-
: JSON.stringify(item.decision || item.description || item.correction || item.original || item.document || item.content || ''),
|
|
2149
|
-
category,
|
|
2150
|
-
project: item.project || project || 'general',
|
|
2151
|
-
created_at: item.created_at || item.date || '',
|
|
2152
|
-
}))
|
|
2153
|
-
|
|
2154
|
-
const compactContent = formatCompactResponse(allItems, `${category}s for ${projectLabel}`)
|
|
2155
|
-
return {
|
|
2156
|
-
action: 'retrieved',
|
|
2157
|
-
summary: `${items.length} ${category}s for ${projectLabel}`,
|
|
2158
|
-
content: compactContent,
|
|
2159
|
-
relevantItems: items.length
|
|
2160
|
-
}
|
|
2161
|
-
}
|
|
2162
|
-
|
|
2163
|
-
private generateTaskId(title: string): string {
|
|
2164
|
-
return title
|
|
2165
|
-
.toLowerCase()
|
|
2166
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
2167
|
-
.replace(/^-+|-+$/g, '')
|
|
2168
|
-
.slice(0, 50)
|
|
2169
|
-
}
|
|
2170
|
-
}
|
|
2171
|
-
|
|
2172
|
-
// Lazy singleton
|
|
2173
|
-
let routerInstance: BrainRouter | null = null
|
|
2174
|
-
|
|
2175
|
-
export function getBrainRouter(logger: Logger): BrainRouter {
|
|
2176
|
-
if (!routerInstance) {
|
|
2177
|
-
routerInstance = new BrainRouter(logger)
|
|
2178
|
-
// SLM Upgrade: Wire inference router if available
|
|
2179
|
-
try {
|
|
2180
|
-
const { getInferenceRouter } = require('@/server/services')
|
|
2181
|
-
const ir = getInferenceRouter()
|
|
2182
|
-
if (ir) routerInstance.setInferenceRouter(ir)
|
|
2183
|
-
} catch {
|
|
2184
|
-
// Services not initialized yet — will use regex fallback
|
|
2185
|
-
}
|
|
2186
|
-
}
|
|
2187
|
-
return routerInstance
|
|
2188
|
-
}
|
|
2189
|
-
|
|
2190
|
-
/** Reset singleton for testing */
|
|
2191
|
-
export function _resetBrainRouterForTesting(): void {
|
|
2192
|
-
routerInstance = null
|
|
2193
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Brain Router
|
|
3
|
+
* Phase 16 + Phase 19: Core orchestrator for the unified brain() tool
|
|
4
|
+
*
|
|
5
|
+
* Routes classified intents to internal service calls and
|
|
6
|
+
* returns unified BrainResponse objects.
|
|
7
|
+
*
|
|
8
|
+
* Task 3.1: Refactored to delegate to domain handler modules.
|
|
9
|
+
* Intent handlers live in src/routing/handlers/.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { Logger } from 'pino'
|
|
13
|
+
import { IntentClassifier } from './intent-classifier'
|
|
14
|
+
import type { InferenceRouter } from '@/intelligence/inference-router'
|
|
15
|
+
import { BrainEntityExtractor } from './entity-extractor'
|
|
16
|
+
import type { BrainResponse } from './response-filter'
|
|
17
|
+
import { SearchEngine } from './search-engine'
|
|
18
|
+
import type { ObservationCompressor } from '@/memory/compression'
|
|
19
|
+
|
|
20
|
+
import type { HandlerContext, RecentDecision } from './handlers/types'
|
|
21
|
+
import { RECENT_DECISION_WINDOW_MS } from './handlers/types'
|
|
22
|
+
|
|
23
|
+
// Domain handlers
|
|
24
|
+
import {
|
|
25
|
+
handleStoreThis,
|
|
26
|
+
handleDecisionMade,
|
|
27
|
+
handlePatternFound,
|
|
28
|
+
handleMistakeLearned,
|
|
29
|
+
handleProgressUpdate
|
|
30
|
+
} from './handlers/memory-handler'
|
|
31
|
+
import {
|
|
32
|
+
handleSessionStart,
|
|
33
|
+
handleContextNeeded,
|
|
34
|
+
handleQuestion,
|
|
35
|
+
handleDetailRequest
|
|
36
|
+
} from './handlers/recall-handler'
|
|
37
|
+
import {
|
|
38
|
+
handleUpdateMemory,
|
|
39
|
+
handleDeleteMemory
|
|
40
|
+
} from './handlers/mutation-handler'
|
|
41
|
+
import {
|
|
42
|
+
handleExploration,
|
|
43
|
+
handleComparison,
|
|
44
|
+
handleListAll,
|
|
45
|
+
handleTimeline
|
|
46
|
+
} from './handlers/exploration-handler'
|
|
47
|
+
|
|
48
|
+
export interface BrainInput {
|
|
49
|
+
message: string
|
|
50
|
+
project?: string
|
|
51
|
+
action?: 'auto' | 'store' | 'recall' | 'update' | 'delete'
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class BrainRouter {
|
|
55
|
+
private classifier: IntentClassifier
|
|
56
|
+
private entityExtractor: BrainEntityExtractor
|
|
57
|
+
private searchEngine: SearchEngine
|
|
58
|
+
private logger: Logger
|
|
59
|
+
|
|
60
|
+
/** SLM Upgrade: Optional inference router for model-based classification */
|
|
61
|
+
private inferenceRouter: InferenceRouter | null = null
|
|
62
|
+
|
|
63
|
+
/** Phase 30: Optional LLM compressor for long observations */
|
|
64
|
+
private compressor: ObservationCompressor | null = null
|
|
65
|
+
|
|
66
|
+
/** Track the most recently stored decision ID for update/delete operations */
|
|
67
|
+
private lastStoredId: string | null = null
|
|
68
|
+
private lastStoredProject: string | null = null
|
|
69
|
+
|
|
70
|
+
/** Bug 4: Recently stored decisions for recency fast path */
|
|
71
|
+
private recentDecisions: RecentDecision[] = []
|
|
72
|
+
|
|
73
|
+
constructor(logger: Logger) {
|
|
74
|
+
this.classifier = new IntentClassifier()
|
|
75
|
+
this.entityExtractor = new BrainEntityExtractor()
|
|
76
|
+
this.searchEngine = new SearchEngine(logger)
|
|
77
|
+
this.logger = logger.child({ component: 'brain-router' })
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** SLM Upgrade: Set the optional inference router for model-based classification */
|
|
81
|
+
setInferenceRouter(router: InferenceRouter): void {
|
|
82
|
+
this.inferenceRouter = router
|
|
83
|
+
this.entityExtractor.setInferenceRouter(router)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Phase 30: Set the optional LLM compressor */
|
|
87
|
+
setCompressor(compressor: ObservationCompressor): void {
|
|
88
|
+
this.compressor = compressor
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Build handler context to pass to domain handlers */
|
|
92
|
+
private buildContext(): HandlerContext {
|
|
93
|
+
return {
|
|
94
|
+
logger: this.logger,
|
|
95
|
+
searchEngine: this.searchEngine,
|
|
96
|
+
classifier: this.classifier,
|
|
97
|
+
compressor: this.compressor,
|
|
98
|
+
lastStoredId: this.lastStoredId,
|
|
99
|
+
lastStoredProject: this.lastStoredProject,
|
|
100
|
+
recentDecisions: this.recentDecisions,
|
|
101
|
+
setLastStored: (id: string, project: string) => {
|
|
102
|
+
this.lastStoredId = id || null
|
|
103
|
+
this.lastStoredProject = project || null
|
|
104
|
+
},
|
|
105
|
+
trackRecentDecision: (content: string, project: string) => {
|
|
106
|
+
const now = Date.now()
|
|
107
|
+
this.recentDecisions = this.recentDecisions.filter(
|
|
108
|
+
d => now - d.storedAt < RECENT_DECISION_WINDOW_MS
|
|
109
|
+
)
|
|
110
|
+
this.recentDecisions.push({ content, project, storedAt: now })
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async route(input: BrainInput): Promise<BrainResponse> {
|
|
116
|
+
const { message, project: inputProject, action } = input
|
|
117
|
+
|
|
118
|
+
// Extract entities (needed for all paths)
|
|
119
|
+
const entities = await this.entityExtractor.extract(message, inputProject)
|
|
120
|
+
const project = entities.project || inputProject
|
|
121
|
+
this.logger.debug({ project, technologies: entities.technologies }, 'Entities extracted')
|
|
122
|
+
|
|
123
|
+
const ctx = this.buildContext()
|
|
124
|
+
|
|
125
|
+
// P1: Explicit action override — bypass intent classifier entirely
|
|
126
|
+
if (action && action !== 'auto') {
|
|
127
|
+
this.logger.debug({ action }, 'Using explicit action override')
|
|
128
|
+
try {
|
|
129
|
+
switch (action) {
|
|
130
|
+
case 'store':
|
|
131
|
+
return handleStoreThis(ctx, message, project, entities)
|
|
132
|
+
case 'recall':
|
|
133
|
+
return handleContextNeeded(ctx, message, project, entities)
|
|
134
|
+
case 'update':
|
|
135
|
+
return handleUpdateMemory(ctx, message, project, entities)
|
|
136
|
+
case 'delete':
|
|
137
|
+
return handleDeleteMemory(ctx, message, project, entities)
|
|
138
|
+
default:
|
|
139
|
+
break
|
|
140
|
+
}
|
|
141
|
+
} catch (error) {
|
|
142
|
+
this.logger.error({ error, action }, 'Router action override error')
|
|
143
|
+
return {
|
|
144
|
+
action: 'none',
|
|
145
|
+
summary: `Error processing ${action} request`,
|
|
146
|
+
content: `Failed to process: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
147
|
+
relevantItems: 0
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Classify intent (SLM: use inference router if available, falls back to regex)
|
|
153
|
+
const classification = this.inferenceRouter
|
|
154
|
+
? await this.inferenceRouter.classifyIntent(message)
|
|
155
|
+
: this.classifier.classify(message)
|
|
156
|
+
this.logger.debug({ intent: classification.primary, confidence: classification.confidence }, 'Intent classified')
|
|
157
|
+
|
|
158
|
+
// Route to handler
|
|
159
|
+
try {
|
|
160
|
+
switch (classification.primary) {
|
|
161
|
+
case 'no_action':
|
|
162
|
+
return { action: 'none', summary: 'No action needed', content: '', relevantItems: 0 }
|
|
163
|
+
|
|
164
|
+
case 'session_start':
|
|
165
|
+
return handleSessionStart(ctx, message, project, entities)
|
|
166
|
+
|
|
167
|
+
case 'context_needed':
|
|
168
|
+
return handleContextNeeded(ctx, message, project, entities, classification)
|
|
169
|
+
|
|
170
|
+
case 'decision_made':
|
|
171
|
+
return handleDecisionMade(ctx, message, project, entities)
|
|
172
|
+
|
|
173
|
+
case 'store_this':
|
|
174
|
+
return handleStoreThis(ctx, message, project, entities)
|
|
175
|
+
|
|
176
|
+
case 'pattern_found':
|
|
177
|
+
return handlePatternFound(ctx, message, project, entities)
|
|
178
|
+
|
|
179
|
+
case 'mistake_learned':
|
|
180
|
+
return handleMistakeLearned(ctx, message, project, entities)
|
|
181
|
+
|
|
182
|
+
case 'progress_update':
|
|
183
|
+
return handleProgressUpdate(ctx, message, project, entities)
|
|
184
|
+
|
|
185
|
+
case 'question':
|
|
186
|
+
return handleQuestion(ctx, message, project, entities, classification)
|
|
187
|
+
|
|
188
|
+
case 'comparison':
|
|
189
|
+
return handleComparison(ctx, message, project, entities)
|
|
190
|
+
|
|
191
|
+
case 'exploration':
|
|
192
|
+
return handleExploration(ctx, message, project, entities)
|
|
193
|
+
|
|
194
|
+
case 'list_all':
|
|
195
|
+
return handleListAll(ctx, message, project, entities)
|
|
196
|
+
|
|
197
|
+
case 'update_memory':
|
|
198
|
+
return handleUpdateMemory(ctx, message, project, entities)
|
|
199
|
+
|
|
200
|
+
case 'delete_memory':
|
|
201
|
+
return handleDeleteMemory(ctx, message, project, entities)
|
|
202
|
+
|
|
203
|
+
case 'detail_request':
|
|
204
|
+
return handleDetailRequest(ctx, message, project, entities)
|
|
205
|
+
|
|
206
|
+
case 'timeline':
|
|
207
|
+
return handleTimeline(ctx, message, project, entities)
|
|
208
|
+
|
|
209
|
+
default:
|
|
210
|
+
return handleContextNeeded(ctx, message, project, entities, classification)
|
|
211
|
+
}
|
|
212
|
+
} catch (error) {
|
|
213
|
+
this.logger.error({ error, intent: classification.primary }, 'Router handler error')
|
|
214
|
+
return {
|
|
215
|
+
action: 'none',
|
|
216
|
+
summary: `Error processing request`,
|
|
217
|
+
content: `Failed to process: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
|
218
|
+
relevantItems: 0
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Lazy singleton
|
|
225
|
+
let routerInstance: BrainRouter | null = null
|
|
226
|
+
|
|
227
|
+
export function getBrainRouter(logger: Logger): BrainRouter {
|
|
228
|
+
if (!routerInstance) {
|
|
229
|
+
routerInstance = new BrainRouter(logger)
|
|
230
|
+
// SLM Upgrade: Wire inference router if available
|
|
231
|
+
try {
|
|
232
|
+
const { getInferenceRouter } = require('@/server/services')
|
|
233
|
+
const ir = getInferenceRouter()
|
|
234
|
+
if (ir) routerInstance.setInferenceRouter(ir)
|
|
235
|
+
} catch {
|
|
236
|
+
// Services not initialized yet — will use regex fallback
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return routerInstance
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Reset singleton for testing */
|
|
243
|
+
export function _resetBrainRouterForTesting(): void {
|
|
244
|
+
routerInstance = null
|
|
245
|
+
}
|