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.
@@ -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
- const chromaNote = !memory.isChromaDBEnabled()
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 memory is not enabled. Enable it by setting knowledge.episodic.enabled=true in config.'
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
- const chromaNote = !memory.isChromaDBEnabled()
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 memory is not enabled. Enable it by setting knowledge.episodic.enabled=true in config.'
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
- 'Feedback collection is not enabled. ' +
43
- 'Enable it by setting RETRIEVAL_FEEDBACK_ENABLED=true or retrieval.feedback.enabled=true in config.'
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
- // Calculate and update completion percentage
54
- const progressState = await context.progress.getProgress(project_name)
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
- const chromaNote = !memory.isChromaDBEnabled()
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' })
@@ -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, {