claude-brain 0.4.1 → 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.
@@ -25,12 +25,82 @@ export async function handleAnalyzeDecisionEvolution(
25
25
  const memory = getMemoryService()
26
26
 
27
27
  if (!memory.isChromaDBEnabled()) {
28
- return ResponseFormatter.text(
29
- `No decisions found for topic: "${topic}"\n\n` +
30
- 'Note: ChromaDB is not connected. Decision evolution analysis requires ChromaDB for semantic search. ' +
31
- 'Decisions stored via SQLite fallback are available through recall_similar. ' +
32
- 'To enable evolution tracking, start a ChromaDB server or configure persistent mode.'
33
- )
28
+ // SQLite fallback: use searchRaw for semantic search + chronological analysis
29
+ const results = await memory.searchRaw(topic, {
30
+ project: project_name,
31
+ limit: limit || 20,
32
+ minSimilarity: 0.2
33
+ })
34
+
35
+ if (results.length === 0) {
36
+ return ResponseFormatter.text(`No decisions found for topic: "${topic}"`)
37
+ }
38
+
39
+ // Sort chronologically
40
+ const sorted = results
41
+ .map(r => ({
42
+ date: r.memory?.createdAt?.toISOString?.() || r.memory?.created_at || new Date().toISOString(),
43
+ decision: r.decision?.decision || r.memory?.content || '',
44
+ reasoning: r.decision?.reasoning || '',
45
+ similarity: r.similarity || 0
46
+ }))
47
+ .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
48
+
49
+ // Simple stability assessment based on content similarity between consecutive decisions
50
+ let totalSimilarity = 0
51
+ let comparisons = 0
52
+ const changes: Array<{ daysBetween: number; summary: string }> = []
53
+
54
+ for (let i = 1; i < sorted.length; i++) {
55
+ const prev = sorted[i - 1]
56
+ const curr = sorted[i]
57
+ const daysBetween = Math.round((new Date(curr.date).getTime() - new Date(prev.date).getTime()) / (1000 * 60 * 60 * 24))
58
+
59
+ // Simple Jaccard similarity between word sets
60
+ const prevWords = new Set(prev.decision.toLowerCase().split(/\s+/).filter(w => w.length > 3))
61
+ const currWords = new Set(curr.decision.toLowerCase().split(/\s+/).filter(w => w.length > 3))
62
+ const intersection = new Set([...prevWords].filter(w => currWords.has(w)))
63
+ const union = new Set([...prevWords, ...currWords])
64
+ const jaccard = union.size > 0 ? intersection.size / union.size : 0
65
+ totalSimilarity += jaccard
66
+ comparisons++
67
+
68
+ if (jaccard < 0.5) {
69
+ changes.push({
70
+ daysBetween,
71
+ summary: `Changed from "${prev.decision.slice(0, 60)}..." to "${curr.decision.slice(0, 60)}..."`
72
+ })
73
+ }
74
+ }
75
+
76
+ const avgSimilarity = comparisons > 0 ? totalSimilarity / comparisons : 1
77
+ const stability = avgSimilarity > 0.7 ? 'stable' : avgSimilarity > 0.4 ? 'evolving' : 'volatile'
78
+ const currentState = sorted[sorted.length - 1].decision
79
+
80
+ const parts: string[] = []
81
+ parts.push(`## Decision Evolution: "${topic}" (SQLite)`)
82
+ if (project_name) parts.push(`**Project:** ${project_name}`)
83
+ parts.push(`**Total decisions:** ${sorted.length}`)
84
+ parts.push(`**Stability:** ${stability}`)
85
+ parts.push(`**Current state:** ${currentState.slice(0, 200)}`)
86
+ parts.push('')
87
+
88
+ if (changes.length > 0) {
89
+ parts.push('### Changes Over Time')
90
+ for (const change of changes) {
91
+ parts.push(`- **${change.daysBetween} days apart:** ${change.summary}`)
92
+ }
93
+ parts.push('')
94
+ }
95
+
96
+ parts.push('### Timeline')
97
+ for (const entry of sorted.slice(0, 10)) {
98
+ const date = new Date(entry.date).toLocaleDateString()
99
+ parts.push(`- **${date}:** ${entry.decision.slice(0, 150)}`)
100
+ }
101
+
102
+ const statsLine = formatMemoryStats({ recalled: sorted.length })
103
+ return ResponseFormatter.text(withMemoryIndicator(parts.join('\n'), sorted.length) + '\n\n' + statsLine)
34
104
  }
35
105
 
36
106
  const tracker = new DecisionEvolutionTracker(
@@ -25,12 +25,66 @@ export async function handleDetectTrends(
25
25
  const memory = getMemoryService()
26
26
 
27
27
  if (!memory.isChromaDBEnabled()) {
28
- return ResponseFormatter.text(
29
- 'No decisions found for trend analysis.\n\n' +
30
- 'Note: ChromaDB is not connected. Trend detection requires ChromaDB for semantic search across decisions. ' +
31
- 'Decisions stored via SQLite fallback are available through recall_similar and get_patterns. ' +
32
- 'To enable trend detection, start a ChromaDB server or configure persistent mode.'
33
- )
28
+ // SQLite fallback: frequency-based trend analysis
29
+ const decisions = await memory.fetchAllDecisions(project_name)
30
+
31
+ if (decisions.length === 0) {
32
+ return ResponseFormatter.text('No decisions found for trend analysis.')
33
+ }
34
+
35
+ // Filter by period
36
+ const cutoffDate = new Date()
37
+ cutoffDate.setDate(cutoffDate.getDate() - (period_days || 90))
38
+ const filtered = decisions.filter(d => new Date(d.date) >= cutoffDate)
39
+
40
+ if (filtered.length === 0) {
41
+ return ResponseFormatter.text(`No decisions found in the last ${period_days || 90} days.`)
42
+ }
43
+
44
+ // Extract term frequencies from decision content
45
+ const stopWords = new Set(['the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should', 'may', 'might', 'can', 'shall', 'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by', 'from', 'as', 'into', 'through', 'during', 'before', 'after', 'above', 'below', 'between', 'out', 'off', 'over', 'under', 'again', 'further', 'then', 'once', 'and', 'but', 'or', 'nor', 'not', 'so', 'yet', 'both', 'either', 'neither', 'each', 'every', 'all', 'any', 'few', 'more', 'most', 'other', 'some', 'such', 'no', 'only', 'own', 'same', 'than', 'too', 'very', 'just', 'because', 'if', 'when', 'where', 'how', 'what', 'which', 'who', 'whom', 'this', 'that', 'these', 'those', 'i', 'me', 'my', 'we', 'our', 'you', 'your', 'he', 'him', 'his', 'she', 'her', 'it', 'its', 'they', 'them', 'their', 'use', 'using', 'used', 'decision', 'context', 'reasoning'])
46
+ const termCounts: Record<string, { count: number; projects: Set<string> }> = {}
47
+
48
+ for (const d of filtered) {
49
+ const text = `${d.decision || ''} ${d.context || ''} ${d.reasoning || ''}`.toLowerCase()
50
+ const words = text.match(/\b[a-z][a-z-]{2,}\b/g) || []
51
+ const seen = new Set<string>()
52
+ for (const word of words) {
53
+ if (stopWords.has(word) || seen.has(word)) continue
54
+ seen.add(word)
55
+ if (!termCounts[word]) termCounts[word] = { count: 0, projects: new Set() }
56
+ termCounts[word].count++
57
+ if (d.project) termCounts[word].projects.add(d.project)
58
+ }
59
+ }
60
+
61
+ const minOcc = min_occurrences || 2
62
+ const trends = Object.entries(termCounts)
63
+ .filter(([, v]) => v.count >= minOcc)
64
+ .map(([term, v]) => ({ term, occurrences: v.count, projects: Array.from(v.projects) }))
65
+ .sort((a, b) => b.occurrences - a.occurrences)
66
+ .slice(0, limit || 20)
67
+
68
+ const parts: string[] = []
69
+ parts.push(`## Trend Analysis (SQLite)`)
70
+ if (project_name) parts.push(`**Project:** ${project_name}`)
71
+ parts.push(`**Period:** Last ${period_days || 90} days`)
72
+ parts.push(`**Decisions analyzed:** ${filtered.length}`)
73
+ parts.push('')
74
+
75
+ if (trends.length > 0) {
76
+ parts.push('### Top Terms by Frequency')
77
+ for (const t of trends) {
78
+ parts.push(`- **${t.term}** (${t.occurrences}x, projects: ${t.projects.join(', ')})`)
79
+ }
80
+ } else {
81
+ parts.push('No significant trends found with current filters.')
82
+ }
83
+
84
+ parts.push('\n*Note: SQLite mode provides frequency-based analysis. Enable ChromaDB for momentum and rising/declining classification.*')
85
+
86
+ const statsLine = formatMemoryStats({ recalled: filtered.length })
87
+ return ResponseFormatter.text(withMemoryIndicator(parts.join('\n'), filtered.length) + '\n\n' + statsLine)
34
88
  }
35
89
 
36
90
  const detector = new TrendDetector(logger, memory.chroma.collections)
@@ -25,12 +25,97 @@ export async function handleFindCrossProjectPatterns(
25
25
  const memory = getMemoryService()
26
26
 
27
27
  if (!memory.isChromaDBEnabled()) {
28
- return ResponseFormatter.text(
29
- 'No cross-project patterns found.\n\n' +
30
- 'Note: ChromaDB is not connected. Cross-project pattern analysis requires ChromaDB for semantic search across decisions. ' +
31
- 'Decisions stored via SQLite fallback are available through recall_similar and get_patterns. ' +
32
- 'To enable cross-project analysis, start a ChromaDB server or configure persistent mode.'
33
- )
28
+ // SQLite fallback: fetch all decisions, group by project, find shared terms
29
+ const decisions = await memory.fetchAllDecisions()
30
+
31
+ if (decisions.length === 0) {
32
+ return ResponseFormatter.text('No cross-project patterns found (no decisions in database).')
33
+ }
34
+
35
+ // Group by project
36
+ const byProject: Record<string, any[]> = {}
37
+ for (const d of decisions) {
38
+ const proj = d.project || 'unknown'
39
+ if (!byProject[proj]) byProject[proj] = []
40
+ byProject[proj].push(d)
41
+ }
42
+
43
+ const projectNames = Object.keys(byProject)
44
+ const minProj = min_projects || 2
45
+
46
+ if (projectNames.length < minProj) {
47
+ return ResponseFormatter.text(
48
+ `Only ${projectNames.length} project(s) found (${projectNames.join(', ')}). Need at least ${minProj} for cross-project analysis.`
49
+ )
50
+ }
51
+
52
+ // Extract significant terms per project
53
+ const stopWords = new Set(['the', 'a', 'an', 'is', 'are', 'was', 'were', 'be', 'been', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should', 'to', 'of', 'in', 'for', 'on', 'with', 'at', 'by', 'from', 'as', 'and', 'but', 'or', 'not', 'so', 'if', 'when', 'where', 'how', 'what', 'which', 'who', 'this', 'that', 'use', 'using', 'used', 'decision', 'context', 'reasoning'])
54
+ const termProjects: Record<string, { projects: Set<string>; examples: Array<{ project: string; content: string }> }> = {}
55
+
56
+ for (const [proj, decs] of Object.entries(byProject)) {
57
+ const projectTerms = new Set<string>()
58
+ for (const d of decs) {
59
+ const text = `${d.decision || ''} ${d.context || ''}`.toLowerCase()
60
+ const words = text.match(/\b[a-z][a-z-]{2,}\b/g) || []
61
+ for (const word of words) {
62
+ if (stopWords.has(word)) continue
63
+ if (!projectTerms.has(word)) {
64
+ projectTerms.add(word)
65
+ if (!termProjects[word]) termProjects[word] = { projects: new Set(), examples: [] }
66
+ termProjects[word].projects.add(proj)
67
+ if (termProjects[word].examples.length < 3) {
68
+ termProjects[word].examples.push({ project: proj, content: d.decision?.slice(0, 100) || d.content?.slice(0, 100) || '' })
69
+ }
70
+ }
71
+ }
72
+ }
73
+ }
74
+
75
+ // Filter terms that appear in >= minProj projects
76
+ const crossPatterns = Object.entries(termProjects)
77
+ .filter(([, v]) => v.projects.size >= minProj)
78
+ .map(([term, v]) => ({
79
+ description: term,
80
+ projects: Array.from(v.projects),
81
+ occurrences: v.projects.size,
82
+ confidence: v.projects.size / projectNames.length,
83
+ examples: v.examples
84
+ }))
85
+ .sort((a, b) => b.occurrences - a.occurrences)
86
+ .slice(0, limit || 20)
87
+
88
+ if (crossPatterns.length === 0) {
89
+ return ResponseFormatter.text(
90
+ `No cross-project patterns found (analyzed ${decisions.length} decisions across ${projectNames.length} projects).`
91
+ )
92
+ }
93
+
94
+ const parts: string[] = []
95
+ parts.push(`## Cross-Project Patterns (SQLite)`)
96
+ parts.push(`**Projects analyzed:** ${projectNames.join(', ')}`)
97
+ parts.push(`**Decisions analyzed:** ${decisions.length}`)
98
+ parts.push(`**Shared terms found:** ${crossPatterns.length}`)
99
+ parts.push('')
100
+
101
+ for (const pattern of crossPatterns) {
102
+ const confidence = Math.round(pattern.confidence * 100)
103
+ parts.push(`### ${pattern.description} (${confidence}% confidence)`)
104
+ parts.push(`**Projects:** ${pattern.projects.join(', ')}`)
105
+ parts.push(`**Occurrences:** ${pattern.occurrences} projects`)
106
+ if (pattern.examples.length > 0) {
107
+ parts.push('**Examples:**')
108
+ for (const ex of pattern.examples.slice(0, 3)) {
109
+ parts.push(`- [${ex.project}] ${ex.content}`)
110
+ }
111
+ }
112
+ parts.push('')
113
+ }
114
+
115
+ parts.push('*Note: SQLite mode uses term-frequency analysis. Enable ChromaDB for semantic pattern generalization.*')
116
+
117
+ const statsLine = formatMemoryStats({ patterns: crossPatterns.length })
118
+ return ResponseFormatter.text(withMemoryIndicator(parts.join('\n'), crossPatterns.length) + '\n\n' + statsLine)
34
119
  }
35
120
 
36
121
  const generalizer = new PatternGeneralizer(
@@ -25,21 +25,6 @@ export async function handleGetDecisionTimeline(
25
25
 
26
26
  const memory = getMemoryService()
27
27
 
28
- if (!memory.isChromaDBEnabled()) {
29
- return ResponseFormatter.text(
30
- 'No decisions found for the specified criteria.\n\n' +
31
- 'Note: ChromaDB is not connected. Decision timeline requires ChromaDB for semantic search. ' +
32
- 'Decisions stored via SQLite fallback are available through recall_similar. ' +
33
- 'To enable timeline view, start a ChromaDB server or configure persistent mode.'
34
- )
35
- }
36
-
37
- const timelineBuilder = new TimelineBuilder(
38
- logger,
39
- memory.chroma.collections,
40
- memory.chroma.embeddings
41
- )
42
-
43
28
  // Parse temporal expression if provided
44
29
  let startDate: string | undefined
45
30
  let endDate: string | undefined
@@ -51,6 +36,97 @@ export async function handleGetDecisionTimeline(
51
36
  endDate = parsed.endDate
52
37
  }
53
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
+
54
130
  const timeline = await timelineBuilder.buildTimeline({
55
131
  project: project_name,
56
132
  topic,
@@ -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
 
@@ -25,12 +25,82 @@ export async function handleGetRecommendations(
25
25
  const memory = getMemoryService()
26
26
 
27
27
  if (!memory.isChromaDBEnabled()) {
28
- return ResponseFormatter.text(
29
- `No recommendations found for: "${query}"\n\n` +
30
- 'Note: ChromaDB is not connected. Recommendations require ChromaDB for semantic search across patterns, corrections, and decisions. ' +
31
- 'Decisions stored via SQLite fallback are available through recall_similar, get_patterns, and get_corrections. ' +
32
- 'To enable recommendations, start a ChromaDB server or configure persistent mode.'
33
- )
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)
34
104
  }
35
105
 
36
106
  const recommender = new Recommender(
@@ -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 { 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')