claude-brain 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/VERSION +1 -1
- package/package.json +1 -1
- package/src/cli/auto-setup.ts +8 -0
- package/src/cli/bin.ts +19 -5
- package/src/cli/commands/chroma.ts +537 -0
- package/src/cli/commands/serve.ts +6 -0
- package/src/cli/commands/start.ts +42 -0
- package/src/config/defaults.ts +1 -1
- package/src/config/schema.ts +1 -1
- package/src/memory/chroma/client.ts +8 -4
- package/src/memory/chroma/config.ts +4 -0
- package/src/memory/index.ts +91 -0
- package/src/memory/store.ts +136 -4
- package/src/server/handlers/tools/analyze-decision-evolution.ts +81 -4
- package/src/server/handlers/tools/detect-trends.ts +65 -4
- package/src/server/handlers/tools/find-cross-project-patterns.ts +95 -0
- package/src/server/handlers/tools/get-decision-timeline.ts +92 -9
- package/src/server/handlers/tools/get-episode.ts +11 -1
- package/src/server/handlers/tools/get-recommendations.ts +81 -4
- package/src/server/handlers/tools/list-episodes.ts +11 -1
- package/src/server/handlers/tools/rate-memory.ts +8 -2
- package/src/server/handlers/tools/search-knowledge-graph.ts +14 -1
- package/src/server/handlers/tools/smart-context.ts +23 -1
- package/src/server/handlers/tools/update-progress.ts +18 -13
- package/src/server/handlers/tools/what-if-analysis.ts +63 -4
- package/src/server/services.ts +12 -8
- package/src/setup/wizard.ts +81 -11
|
@@ -24,11 +24,6 @@ export async function handleGetDecisionTimeline(
|
|
|
24
24
|
const { project_name, topic, time_range, limit } = input
|
|
25
25
|
|
|
26
26
|
const memory = getMemoryService()
|
|
27
|
-
const timelineBuilder = new TimelineBuilder(
|
|
28
|
-
logger,
|
|
29
|
-
memory.chroma.collections,
|
|
30
|
-
memory.chroma.embeddings
|
|
31
|
-
)
|
|
32
27
|
|
|
33
28
|
// Parse temporal expression if provided
|
|
34
29
|
let startDate: string | undefined
|
|
@@ -41,6 +36,97 @@ export async function handleGetDecisionTimeline(
|
|
|
41
36
|
endDate = parsed.endDate
|
|
42
37
|
}
|
|
43
38
|
|
|
39
|
+
if (!memory.isChromaDBEnabled()) {
|
|
40
|
+
// SQLite fallback: fetch all data and build timeline
|
|
41
|
+
const [decisions, patterns, corrections] = await Promise.all([
|
|
42
|
+
memory.fetchAllDecisions(project_name),
|
|
43
|
+
memory.fetchAllPatterns(project_name),
|
|
44
|
+
memory.fetchAllCorrections(project_name)
|
|
45
|
+
])
|
|
46
|
+
|
|
47
|
+
// Combine into timeline entries
|
|
48
|
+
let entries: Array<{ date: string; type: string; content: string; reasoning?: string }> = []
|
|
49
|
+
|
|
50
|
+
for (const d of decisions) {
|
|
51
|
+
entries.push({
|
|
52
|
+
date: d.date,
|
|
53
|
+
type: 'decision',
|
|
54
|
+
content: d.decision || d.content,
|
|
55
|
+
reasoning: d.reasoning
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
for (const p of patterns) {
|
|
59
|
+
entries.push({
|
|
60
|
+
date: p.date,
|
|
61
|
+
type: 'pattern',
|
|
62
|
+
content: p.description || p.content
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
for (const c of corrections) {
|
|
66
|
+
entries.push({
|
|
67
|
+
date: c.date,
|
|
68
|
+
type: 'correction',
|
|
69
|
+
content: c.correction || c.content,
|
|
70
|
+
reasoning: c.reasoning
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Filter by topic (substring match)
|
|
75
|
+
if (topic) {
|
|
76
|
+
const topicLower = topic.toLowerCase()
|
|
77
|
+
entries = entries.filter(e =>
|
|
78
|
+
e.content.toLowerCase().includes(topicLower) ||
|
|
79
|
+
(e.reasoning && e.reasoning.toLowerCase().includes(topicLower))
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Filter by date range
|
|
84
|
+
if (startDate) {
|
|
85
|
+
entries = entries.filter(e => e.date >= startDate!)
|
|
86
|
+
}
|
|
87
|
+
if (endDate) {
|
|
88
|
+
entries = entries.filter(e => e.date <= endDate!)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Sort chronologically and limit
|
|
92
|
+
entries.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
|
|
93
|
+
const limited = entries.slice(0, limit || 20)
|
|
94
|
+
|
|
95
|
+
if (limited.length === 0) {
|
|
96
|
+
return ResponseFormatter.text('No decisions found for the specified criteria.')
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const parts: string[] = []
|
|
100
|
+
parts.push(`## Decision Timeline (SQLite)`)
|
|
101
|
+
if (project_name) parts.push(`**Project:** ${project_name}`)
|
|
102
|
+
if (topic) parts.push(`**Topic:** ${topic}`)
|
|
103
|
+
if (limited.length > 0) {
|
|
104
|
+
parts.push(`**Period:** ${new Date(limited[0].date).toLocaleDateString()} - ${new Date(limited[limited.length - 1].date).toLocaleDateString()}`)
|
|
105
|
+
}
|
|
106
|
+
parts.push(`**Total entries:** ${limited.length}`)
|
|
107
|
+
parts.push('')
|
|
108
|
+
|
|
109
|
+
for (const entry of limited) {
|
|
110
|
+
const date = new Date(entry.date).toLocaleDateString()
|
|
111
|
+
const icon = entry.type === 'decision' ? '📋' : entry.type === 'pattern' ? '🔄' : '⚠️'
|
|
112
|
+
parts.push(`### ${icon} ${date} [${entry.type}]`)
|
|
113
|
+
parts.push(entry.content.slice(0, 200))
|
|
114
|
+
if (entry.reasoning) {
|
|
115
|
+
parts.push(`*Reasoning:* ${entry.reasoning.slice(0, 100)}`)
|
|
116
|
+
}
|
|
117
|
+
parts.push('')
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const statsLine = formatMemoryStats({ recalled: limited.length })
|
|
121
|
+
return ResponseFormatter.text(withMemoryIndicator(parts.join('\n'), limited.length) + '\n\n' + statsLine)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const timelineBuilder = new TimelineBuilder(
|
|
125
|
+
logger,
|
|
126
|
+
memory.chroma.collections,
|
|
127
|
+
memory.chroma.embeddings
|
|
128
|
+
)
|
|
129
|
+
|
|
44
130
|
const timeline = await timelineBuilder.buildTimeline({
|
|
45
131
|
project: project_name,
|
|
46
132
|
topic,
|
|
@@ -50,10 +136,7 @@ export async function handleGetDecisionTimeline(
|
|
|
50
136
|
})
|
|
51
137
|
|
|
52
138
|
if (timeline.entries.length === 0) {
|
|
53
|
-
|
|
54
|
-
? '\n\nNote: ChromaDB is not connected. Decision timeline requires ChromaDB for semantic search. Start a ChromaDB server or switch to persistent mode.'
|
|
55
|
-
: ''
|
|
56
|
-
return ResponseFormatter.text('No decisions found for the specified criteria.' + chromaNote)
|
|
139
|
+
return ResponseFormatter.text('No decisions found for the specified criteria.')
|
|
57
140
|
}
|
|
58
141
|
|
|
59
142
|
// Format timeline
|
|
@@ -32,7 +32,17 @@ export async function handleGetEpisode(
|
|
|
32
32
|
const episodeService = getEpisodeService()
|
|
33
33
|
if (!episodeService) {
|
|
34
34
|
return ResponseFormatter.text(
|
|
35
|
-
'Episodic
|
|
35
|
+
'## Episodic Memory Not Available\n\n' +
|
|
36
|
+
'Episodic memory requires ChromaDB to track conversation sessions.\n\n' +
|
|
37
|
+
'**To enable:**\n' +
|
|
38
|
+
'- Start a ChromaDB server: `chroma run --path /path/to/data`\n' +
|
|
39
|
+
'- Set `CHROMA_URL=http://localhost:8000` in your environment\n' +
|
|
40
|
+
'- Set `knowledge.episodic.enabled=true` in config\n\n' +
|
|
41
|
+
'**Alternative tools that work without ChromaDB:**\n' +
|
|
42
|
+
'- `recall_similar` — search past decisions by semantic similarity\n' +
|
|
43
|
+
'- `get_patterns` — retrieve recognized patterns\n' +
|
|
44
|
+
'- `get_corrections` — retrieve lessons learned\n' +
|
|
45
|
+
'- `smart_context` — get full project context with memory recall'
|
|
36
46
|
)
|
|
37
47
|
}
|
|
38
48
|
|
|
@@ -23,6 +23,86 @@ export async function handleGetRecommendations(
|
|
|
23
23
|
const { query, project_name, limit } = input
|
|
24
24
|
|
|
25
25
|
const memory = getMemoryService()
|
|
26
|
+
|
|
27
|
+
if (!memory.isChromaDBEnabled()) {
|
|
28
|
+
// SQLite fallback: combine searchRaw + searchPatterns + searchCorrections
|
|
29
|
+
const [rawResults, patternResults, correctionResults] = await Promise.all([
|
|
30
|
+
memory.searchRaw(query, { project: project_name, limit: limit || 5, minSimilarity: 0.2 }),
|
|
31
|
+
memory.searchPatterns(query, { project: project_name, limit: limit || 5, minSimilarity: 0.2 }),
|
|
32
|
+
memory.searchCorrections(query, { project: project_name, limit: limit || 5, minSimilarity: 0.2 })
|
|
33
|
+
])
|
|
34
|
+
|
|
35
|
+
// Combine and format as recommendations
|
|
36
|
+
const recommendations: Array<{ type: string; source: string; content: string; reasoning: string; confidence: number }> = []
|
|
37
|
+
|
|
38
|
+
// Corrections get highest priority (lessons learned)
|
|
39
|
+
for (const c of correctionResults) {
|
|
40
|
+
recommendations.push({
|
|
41
|
+
type: 'correction',
|
|
42
|
+
source: 'Past Correction',
|
|
43
|
+
content: `Original: ${c.metadata?.original || ''}\nCorrection: ${c.metadata?.correction || c.content || ''}`,
|
|
44
|
+
reasoning: c.metadata?.reasoning || 'Based on previous lesson learned',
|
|
45
|
+
confidence: c.similarity || c.metadata?.confidence || 0.5
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Patterns
|
|
50
|
+
for (const p of patternResults) {
|
|
51
|
+
recommendations.push({
|
|
52
|
+
type: p.metadata?.pattern_type || 'pattern',
|
|
53
|
+
source: `Pattern (${p.metadata?.pattern_type || 'general'})`,
|
|
54
|
+
content: p.metadata?.description || p.content || '',
|
|
55
|
+
reasoning: p.metadata?.context || 'Based on recognized pattern',
|
|
56
|
+
confidence: p.similarity || p.metadata?.confidence || 0.5
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Decisions
|
|
61
|
+
for (const r of rawResults) {
|
|
62
|
+
recommendations.push({
|
|
63
|
+
type: 'decision',
|
|
64
|
+
source: 'Past Decision',
|
|
65
|
+
content: r.decision?.decision || r.memory?.content?.slice(0, 300) || '',
|
|
66
|
+
reasoning: r.decision?.reasoning || 'Based on similar past decision',
|
|
67
|
+
confidence: r.similarity || 0.5
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Sort by confidence, deduplicate, and limit
|
|
72
|
+
const seen = new Set<string>()
|
|
73
|
+
const deduped = recommendations.filter(r => {
|
|
74
|
+
const key = r.content.slice(0, 50)
|
|
75
|
+
if (seen.has(key)) return false
|
|
76
|
+
seen.add(key)
|
|
77
|
+
return true
|
|
78
|
+
})
|
|
79
|
+
.sort((a, b) => b.confidence - a.confidence)
|
|
80
|
+
.slice(0, limit || 5)
|
|
81
|
+
|
|
82
|
+
if (deduped.length === 0) {
|
|
83
|
+
return ResponseFormatter.text(`No recommendations found for: "${query}"`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const parts: string[] = []
|
|
87
|
+
parts.push(`## Recommendations (SQLite)`)
|
|
88
|
+
parts.push(`**Query:** ${query}`)
|
|
89
|
+
if (project_name) parts.push(`**Project:** ${project_name}`)
|
|
90
|
+
parts.push(`**Found:** ${deduped.length}`)
|
|
91
|
+
parts.push('')
|
|
92
|
+
|
|
93
|
+
for (const rec of deduped) {
|
|
94
|
+
const confidence = Math.round(rec.confidence * 100)
|
|
95
|
+
const icon = rec.type === 'correction' ? '⚠️' : rec.type === 'best-practice' ? '✅' : rec.type === 'pattern' ? '🔄' : '📋'
|
|
96
|
+
parts.push(`### ${icon} ${rec.source} (${confidence}% confidence)`)
|
|
97
|
+
parts.push(rec.content.slice(0, 300))
|
|
98
|
+
parts.push(`*${rec.reasoning}*`)
|
|
99
|
+
parts.push('')
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const statsLine = formatMemoryStats({ recalled: deduped.length })
|
|
103
|
+
return ResponseFormatter.text(withMemoryIndicator(parts.join('\n'), deduped.length) + '\n\n' + statsLine)
|
|
104
|
+
}
|
|
105
|
+
|
|
26
106
|
const recommender = new Recommender(
|
|
27
107
|
logger,
|
|
28
108
|
memory.chroma.collections,
|
|
@@ -35,10 +115,7 @@ export async function handleGetRecommendations(
|
|
|
35
115
|
})
|
|
36
116
|
|
|
37
117
|
if (result.recommendations.length === 0) {
|
|
38
|
-
|
|
39
|
-
? '\n\nNote: ChromaDB is not connected. Recommendations require ChromaDB for semantic search across patterns, corrections, and decisions. Start a ChromaDB server or switch to persistent mode.'
|
|
40
|
-
: ''
|
|
41
|
-
return ResponseFormatter.text(`No recommendations found for: "${query}"` + chromaNote)
|
|
118
|
+
return ResponseFormatter.text(`No recommendations found for: "${query}"`)
|
|
42
119
|
}
|
|
43
120
|
|
|
44
121
|
const parts: string[] = []
|
|
@@ -32,7 +32,17 @@ export async function handleListEpisodes(
|
|
|
32
32
|
const episodeService = getEpisodeService()
|
|
33
33
|
if (!episodeService) {
|
|
34
34
|
return ResponseFormatter.text(
|
|
35
|
-
'Episodic
|
|
35
|
+
'## Episodic Memory Not Available\n\n' +
|
|
36
|
+
'Episodic memory requires ChromaDB to track conversation sessions.\n\n' +
|
|
37
|
+
'**To enable:**\n' +
|
|
38
|
+
'- Start a ChromaDB server: `chroma run --path /path/to/data`\n' +
|
|
39
|
+
'- Set `CHROMA_URL=http://localhost:8000` in your environment\n' +
|
|
40
|
+
'- Set `knowledge.episodic.enabled=true` in config\n\n' +
|
|
41
|
+
'**Alternative tools that work without ChromaDB:**\n' +
|
|
42
|
+
'- `recall_similar` — search past decisions by semantic similarity\n' +
|
|
43
|
+
'- `get_patterns` — retrieve recognized patterns\n' +
|
|
44
|
+
'- `get_corrections` — retrieve lessons learned\n' +
|
|
45
|
+
'- `smart_context` — get full project context with memory recall'
|
|
36
46
|
)
|
|
37
47
|
}
|
|
38
48
|
|
|
@@ -39,8 +39,14 @@ export async function handleRateMemory(
|
|
|
39
39
|
// Check if feedback is enabled
|
|
40
40
|
if (!retrieval) {
|
|
41
41
|
return ResponseFormatter.text(
|
|
42
|
-
'
|
|
43
|
-
'
|
|
42
|
+
'## Memory Feedback Not Available\n\n' +
|
|
43
|
+
'Memory rating requires the retrieval feedback system, which depends on ChromaDB.\n\n' +
|
|
44
|
+
'**To enable:**\n' +
|
|
45
|
+
'- Start a ChromaDB server: `chroma run --path /path/to/data`\n' +
|
|
46
|
+
'- Set `CHROMA_URL=http://localhost:8000` in your environment\n' +
|
|
47
|
+
'- Set `RETRIEVAL_FEEDBACK_ENABLED=true` or `retrieval.feedback.enabled=true` in config\n\n' +
|
|
48
|
+
'**Note:** Your rating was not lost — the memory system will still function without feedback. ' +
|
|
49
|
+
'Feedback helps improve retrieval quality over time by learning which memories are most useful.'
|
|
44
50
|
)
|
|
45
51
|
}
|
|
46
52
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { Logger } from 'pino'
|
|
7
7
|
import type { ToolResponse } from '@/tools/types'
|
|
8
|
-
import { getKnowledgeGraphService, isServicesInitialized } from '@/server/services'
|
|
8
|
+
import { getKnowledgeGraphService, getMemoryService, isServicesInitialized } from '@/server/services'
|
|
9
9
|
import { ToolValidator } from '@/server/utils/validators'
|
|
10
10
|
import { ResponseFormatter } from '@/server/utils/response-formatter'
|
|
11
11
|
import { ErrorHandler } from '@/server/utils/error-handler'
|
|
@@ -72,6 +72,19 @@ export async function handleSearchKnowledgeGraph(
|
|
|
72
72
|
edgeCount: result.edges.length
|
|
73
73
|
}, 'Knowledge graph search complete')
|
|
74
74
|
|
|
75
|
+
// If graph is empty and ChromaDB is not connected, add guidance
|
|
76
|
+
if (result.nodes.length === 0 && result.edges.length === 0) {
|
|
77
|
+
const memory = getMemoryService()
|
|
78
|
+
if (!memory.isChromaDBEnabled()) {
|
|
79
|
+
return ResponseFormatter.text(
|
|
80
|
+
'Knowledge graph is empty (0 nodes, 0 edges).\n\n' +
|
|
81
|
+
'Note: ChromaDB is not connected. The knowledge graph is populated from stored decisions. ' +
|
|
82
|
+
'New decisions stored via SQLite will populate the graph going forward. ' +
|
|
83
|
+
'To migrate existing decisions into the graph, start a ChromaDB server or configure persistent mode.'
|
|
84
|
+
)
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
75
88
|
return ResponseFormatter.json(response, 'Knowledge Graph Search Results')
|
|
76
89
|
|
|
77
90
|
} catch (error) {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type { Logger } from 'pino'
|
|
7
7
|
import type { ToolResponse } from '@/tools/types'
|
|
8
|
-
import { getContextService, getPhase12Service, isServicesInitialized } from '@/server/services'
|
|
8
|
+
import { getContextService, getPhase12Service, getMemoryService, isServicesInitialized } from '@/server/services'
|
|
9
9
|
import { ToolValidator } from '@/server/utils/validators'
|
|
10
10
|
import { ResponseFormatter } from '@/server/utils/response-formatter'
|
|
11
11
|
import { withMemoryIndicator, formatMemoryStats } from '@/server/utils/memory-indicator'
|
|
@@ -52,6 +52,28 @@ export async function handleSmartContext(
|
|
|
52
52
|
// Process with Phase 12 for proactive recall
|
|
53
53
|
const phase12Result = await phase12.processMessage(current_task, project_name)
|
|
54
54
|
|
|
55
|
+
// If Phase12's gated recall found nothing, do direct search (like recall_similar)
|
|
56
|
+
if (!phase12Result.recalledMemories || phase12Result.recalledMemories.memories.length === 0) {
|
|
57
|
+
try {
|
|
58
|
+
const memory = getMemoryService()
|
|
59
|
+
const directRecall = await memory.searchRaw(current_task, {
|
|
60
|
+
project: project_name,
|
|
61
|
+
limit: 5,
|
|
62
|
+
minSimilarity: min_similarity || 0.3
|
|
63
|
+
})
|
|
64
|
+
if (directRecall.length > 0) {
|
|
65
|
+
phase12Result.recalledMemories = {
|
|
66
|
+
query: current_task,
|
|
67
|
+
memories: directRecall,
|
|
68
|
+
relevanceScore: directRecall.reduce((sum: number, m: any) => sum + (m.similarity || 0), 0) / directRecall.length,
|
|
69
|
+
triggeredBy: ['direct-search-fallback']
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
} catch (e) {
|
|
73
|
+
logger.debug({ error: e }, 'Direct recall fallback failed, continuing without')
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
55
77
|
// Format base context
|
|
56
78
|
const formattedContext = contextService.formatter.format(context)
|
|
57
79
|
|
|
@@ -43,6 +43,7 @@ export async function handleUpdateProgress(
|
|
|
43
43
|
|
|
44
44
|
const taskId = generateTaskId(completed_task)
|
|
45
45
|
|
|
46
|
+
// Step 1: Append completed task to progress file
|
|
46
47
|
await context.progress.addCompletedTask(project_name, {
|
|
47
48
|
id: taskId,
|
|
48
49
|
title: completed_task,
|
|
@@ -50,19 +51,8 @@ export async function handleUpdateProgress(
|
|
|
50
51
|
completedAt: new Date()
|
|
51
52
|
})
|
|
52
53
|
|
|
53
|
-
//
|
|
54
|
-
const
|
|
55
|
-
const totalTasks = progressState.completedTasks.length + progressState.currentTasks.length
|
|
56
|
-
const completionPercentage = totalTasks > 0
|
|
57
|
-
? Math.round((progressState.completedTasks.length / totalTasks) * 100)
|
|
58
|
-
: 0
|
|
59
|
-
|
|
60
|
-
await context.progress.updateProgress(project_name, {
|
|
61
|
-
completionPercentage,
|
|
62
|
-
currentPhase: completionPercentage >= 100 ? 'complete' : 'active'
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
const progressFile = await vault.reader.readMarkdownFile(projectPaths.progress)
|
|
54
|
+
// Step 2: Read file bypassing cache, update next steps, write content
|
|
55
|
+
const progressFile = await vault.reader.readMarkdownFile(projectPaths.progress, false)
|
|
66
56
|
const updatedContent = updateNextStepsSection(progressFile.content, next_steps)
|
|
67
57
|
|
|
68
58
|
await vault.writer.writeMarkdownFile(
|
|
@@ -72,11 +62,26 @@ export async function handleUpdateProgress(
|
|
|
72
62
|
true
|
|
73
63
|
)
|
|
74
64
|
|
|
65
|
+
// Step 3: Notes appended if present
|
|
75
66
|
if (notes) {
|
|
76
67
|
const notesEntry = `\n## Notes (${new Date().toLocaleDateString()})\n${notes}\n`
|
|
77
68
|
await vault.writer.appendContent(projectPaths.progress, notesEntry)
|
|
78
69
|
}
|
|
79
70
|
|
|
71
|
+
// Step 4: Calculate and update frontmatter LAST (so nothing overwrites it)
|
|
72
|
+
const progressState = await context.progress.getProgress(project_name)
|
|
73
|
+
const totalTasks = progressState.completedTasks.length + progressState.currentTasks.length
|
|
74
|
+
const completionPercentage = totalTasks > 0
|
|
75
|
+
? Math.round((progressState.completedTasks.length / totalTasks) * 100)
|
|
76
|
+
: 0
|
|
77
|
+
|
|
78
|
+
await context.progress.updateProgress(project_name, {
|
|
79
|
+
completionPercentage,
|
|
80
|
+
currentPhase: completionPercentage >= 100 ? 'complete' : 'active'
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// Step 5: Invalidate cache so subsequent reads get fresh data
|
|
84
|
+
vault.reader.invalidateCache(projectPaths.progress)
|
|
80
85
|
context.invalidateContext(project_name)
|
|
81
86
|
|
|
82
87
|
logger.info({ projectName: project_name, taskId }, 'Progress updated successfully')
|
|
@@ -23,6 +23,68 @@ export async function handleWhatIfAnalysis(
|
|
|
23
23
|
const { change, project_name, max_results } = input
|
|
24
24
|
|
|
25
25
|
const memory = getMemoryService()
|
|
26
|
+
|
|
27
|
+
if (!memory.isChromaDBEnabled()) {
|
|
28
|
+
// SQLite fallback: use searchRaw for semantic search + knowledge graph if available
|
|
29
|
+
const results = await memory.searchRaw(change, {
|
|
30
|
+
project: project_name,
|
|
31
|
+
limit: max_results || 10,
|
|
32
|
+
minSimilarity: 0.2
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const parts: string[] = []
|
|
36
|
+
parts.push(`## What-If: "${change}" (SQLite)`)
|
|
37
|
+
parts.push('')
|
|
38
|
+
|
|
39
|
+
if (results.length === 0) {
|
|
40
|
+
parts.push('No related decisions found to assess impact.')
|
|
41
|
+
return ResponseFormatter.text(parts.join('\n'))
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Assess impact based on similarity
|
|
45
|
+
const affectedDecisions = results.map(r => {
|
|
46
|
+
const similarity = r.similarity || 0
|
|
47
|
+
const impact = similarity > 0.7 ? 'high' : similarity > 0.4 ? 'medium' : 'low'
|
|
48
|
+
return {
|
|
49
|
+
decision: r.decision?.decision || r.memory?.content?.slice(0, 150) || '',
|
|
50
|
+
impact,
|
|
51
|
+
similarity,
|
|
52
|
+
reason: `${Math.round(similarity * 100)}% similarity — ${r.decision?.reasoning?.slice(0, 100) || 'related context found'}`
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
parts.push(`Analyzed ${results.length} related decisions.\n`)
|
|
57
|
+
|
|
58
|
+
parts.push('### Affected Decisions')
|
|
59
|
+
for (const d of affectedDecisions) {
|
|
60
|
+
const icon = d.impact === 'high' ? '🔴' : d.impact === 'medium' ? '🟡' : '🟢'
|
|
61
|
+
parts.push(`${icon} **[${d.impact}]** ${d.decision}`)
|
|
62
|
+
parts.push(` *${d.reason}*`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Try knowledge graph if available
|
|
66
|
+
try {
|
|
67
|
+
const kgService = getKnowledgeGraphService()
|
|
68
|
+
if (kgService?.graph) {
|
|
69
|
+
const graphResults = await kgService.graph.search(change, { limit: 5 })
|
|
70
|
+
if (graphResults && graphResults.length > 0) {
|
|
71
|
+
const technologies = graphResults
|
|
72
|
+
.filter((n: any) => n.type === 'technology')
|
|
73
|
+
.map((n: any) => n.name || n.label)
|
|
74
|
+
if (technologies.length > 0) {
|
|
75
|
+
parts.push(`\n### Connected Technologies`)
|
|
76
|
+
parts.push(technologies.map((t: string) => `- ${t}`).join('\n'))
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// Knowledge graph not available, skip
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const content = parts.join('\n')
|
|
85
|
+
return ResponseFormatter.text(withMemoryIndicator(content, results.length))
|
|
86
|
+
}
|
|
87
|
+
|
|
26
88
|
const kgService = getKnowledgeGraphService()
|
|
27
89
|
const graph = kgService?.graph || null
|
|
28
90
|
|
|
@@ -63,10 +125,7 @@ export async function handleWhatIfAnalysis(
|
|
|
63
125
|
return ResponseFormatter.text(withMemoryIndicator(content, totalAffected))
|
|
64
126
|
}
|
|
65
127
|
|
|
66
|
-
|
|
67
|
-
? '\n\nNote: ChromaDB is not connected. What-if analysis requires ChromaDB for semantic search across decisions. Start a ChromaDB server or switch to persistent mode.'
|
|
68
|
-
: ''
|
|
69
|
-
return ResponseFormatter.text(content + chromaNote)
|
|
128
|
+
return ResponseFormatter.text(content)
|
|
70
129
|
|
|
71
130
|
} catch (error) {
|
|
72
131
|
ErrorHandler.logError(logger, error, { tool: 'what_if_analysis' })
|
package/src/server/services.ts
CHANGED
|
@@ -112,9 +112,9 @@ export async function initializeServices(config: Config, logger: Logger): Promis
|
|
|
112
112
|
await phase12.initialize()
|
|
113
113
|
serviceLogger.info('Phase 12 service initialized')
|
|
114
114
|
|
|
115
|
-
// Initialize Retrieval Service (Phase 13)
|
|
115
|
+
// Initialize Retrieval Service (Phase 13) — requires ChromaDB
|
|
116
116
|
let retrieval: RetrievalService | null = null
|
|
117
|
-
if (config.retrieval?.feedback?.enabled || config.retrieval?.enabled) {
|
|
117
|
+
if ((config.retrieval?.feedback?.enabled || config.retrieval?.enabled) && memory.isChromaDBEnabled()) {
|
|
118
118
|
retrieval = new RetrievalService(
|
|
119
119
|
logger,
|
|
120
120
|
memory.chroma.collections,
|
|
@@ -123,6 +123,8 @@ export async function initializeServices(config: Config, logger: Logger): Promis
|
|
|
123
123
|
)
|
|
124
124
|
await retrieval.initialize()
|
|
125
125
|
serviceLogger.info('Retrieval service initialized')
|
|
126
|
+
} else if (config.retrieval?.enabled && !memory.isChromaDBEnabled()) {
|
|
127
|
+
serviceLogger.warn('Retrieval service requires ChromaDB, skipping initialization')
|
|
126
128
|
}
|
|
127
129
|
|
|
128
130
|
// Initialize Knowledge Graph Service (Phase 14)
|
|
@@ -146,14 +148,16 @@ export async function initializeServices(config: Config, logger: Logger): Promis
|
|
|
146
148
|
|
|
147
149
|
knowledgeGraph = { graph, search, builder, linker }
|
|
148
150
|
|
|
149
|
-
// Migrate existing decisions if graph is empty
|
|
150
|
-
if (graph.getNodeCount() === 0) {
|
|
151
|
+
// Migrate existing decisions if graph is empty (requires ChromaDB)
|
|
152
|
+
if (graph.getNodeCount() === 0 && memory.isChromaDBEnabled()) {
|
|
151
153
|
serviceLogger.info('Empty graph detected, migrating existing decisions...')
|
|
152
154
|
const migrationResult = await builder.migrateExistingDecisions(memory.chroma.collections)
|
|
153
155
|
serviceLogger.info(
|
|
154
156
|
{ processed: migrationResult.processed, errors: migrationResult.errors },
|
|
155
157
|
'Initial graph migration complete'
|
|
156
158
|
)
|
|
159
|
+
} else if (graph.getNodeCount() === 0) {
|
|
160
|
+
serviceLogger.info('Empty graph detected, but ChromaDB unavailable — graph will populate as new decisions are stored')
|
|
157
161
|
}
|
|
158
162
|
|
|
159
163
|
// Hook builder into decision storage for real-time graph population
|
|
@@ -171,9 +175,9 @@ export async function initializeServices(config: Config, logger: Logger): Promis
|
|
|
171
175
|
}
|
|
172
176
|
}
|
|
173
177
|
|
|
174
|
-
// Initialize Episode Manager (Phase 14)
|
|
178
|
+
// Initialize Episode Manager (Phase 14) — requires ChromaDB
|
|
175
179
|
let episodeManager: EpisodeManager | null = null
|
|
176
|
-
if (config.knowledge?.episodic?.enabled !== false) {
|
|
180
|
+
if (config.knowledge?.episodic?.enabled !== false && memory.isChromaDBEnabled()) {
|
|
177
181
|
try {
|
|
178
182
|
episodeManager = new EpisodeManager(
|
|
179
183
|
logger,
|
|
@@ -187,10 +191,10 @@ export async function initializeServices(config: Config, logger: Logger): Promis
|
|
|
187
191
|
}
|
|
188
192
|
}
|
|
189
193
|
|
|
190
|
-
// Initialize Semantic Cache & Precompute (Phase 15)
|
|
194
|
+
// Initialize Semantic Cache & Precompute (Phase 15) — requires ChromaDB
|
|
191
195
|
let semanticCache: SemanticCache | null = null
|
|
192
196
|
let precompute: PrecomputeEngine | null = null
|
|
193
|
-
if (config.advancedIntelligence?.enabled !== false && config.advancedIntelligence?.cache?.enabled !== false) {
|
|
197
|
+
if (config.advancedIntelligence?.enabled !== false && config.advancedIntelligence?.cache?.enabled !== false && memory.isChromaDBEnabled()) {
|
|
194
198
|
try {
|
|
195
199
|
const cacheConfig = config.advancedIntelligence?.cache || {}
|
|
196
200
|
semanticCache = new SemanticCache(logger, {
|