claude-brain 0.3.7 → 0.4.1

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.
@@ -55,6 +55,7 @@ export class MemoryManager {
55
55
  private logger: Logger
56
56
  private initialized: boolean = false
57
57
  private useChromaDB: boolean = true
58
+ private onDecisionStoredCallbacks: ((input: any) => void)[] = []
58
59
 
59
60
  constructor(
60
61
  dbPath: string,
@@ -176,6 +177,24 @@ export class MemoryManager {
176
177
  return this.database.healthCheck()
177
178
  }
178
179
 
180
+ /**
181
+ * Check if ChromaDB backend is enabled and connected
182
+ */
183
+ isChromaDBEnabled(): boolean {
184
+ return this.useChromaDB
185
+ }
186
+
187
+ /**
188
+ * Add a listener that fires when a decision is stored (from any backend)
189
+ */
190
+ addDecisionStoredListener(callback: (input: any) => void): void {
191
+ this.onDecisionStoredCallbacks.push(callback)
192
+ // Also wire to ChromaDB listener if available
193
+ if (this.useChromaDB) {
194
+ this.chroma.store.addDecisionStoredListener(callback)
195
+ }
196
+ }
197
+
179
198
  async rememberDecision(
180
199
  project: string,
181
200
  context: string,
@@ -193,7 +212,7 @@ export class MemoryManager {
193
212
  tags: options?.tags
194
213
  })
195
214
  }
196
- return this.store.storeDecision({
215
+ const id = await this.store.storeDecision({
197
216
  project,
198
217
  context,
199
218
  decision,
@@ -201,6 +220,13 @@ export class MemoryManager {
201
220
  alternatives: options?.alternatives,
202
221
  tags: options?.tags
203
222
  })
223
+ // Notify listeners (e.g., knowledge graph builder) for SQLite path
224
+ for (const cb of this.onDecisionStoredCallbacks) {
225
+ try {
226
+ cb({ project, context, decision, reasoning, alternatives: options?.alternatives, tags: options?.tags, id })
227
+ } catch {}
228
+ }
229
+ return id
204
230
  }
205
231
 
206
232
  /**
@@ -281,7 +307,7 @@ export class MemoryManager {
281
307
  }
282
308
 
283
309
  /**
284
- * Store a pattern in memory
310
+ * Store a pattern in memory — routes to ChromaDB or SQLite
285
311
  */
286
312
  async storePattern(input: {
287
313
  project: string
@@ -291,14 +317,14 @@ export class MemoryManager {
291
317
  confidence: number
292
318
  context?: string
293
319
  }): Promise<string> {
294
- if (!this.useChromaDB) {
295
- throw new Error('Pattern storage requires ChromaDB backend')
320
+ if (this.useChromaDB) {
321
+ return this.chroma.store.storePattern(input)
296
322
  }
297
- return this.chroma.store.storePattern(input)
323
+ return this.store.storePattern(input)
298
324
  }
299
325
 
300
326
  /**
301
- * Store a correction/lesson learned in memory
327
+ * Store a correction/lesson learned routes to ChromaDB or SQLite
302
328
  */
303
329
  async storeCorrection(input: {
304
330
  project: string
@@ -308,14 +334,14 @@ export class MemoryManager {
308
334
  context?: string
309
335
  confidence: number
310
336
  }): Promise<string> {
311
- if (!this.useChromaDB) {
312
- throw new Error('Correction storage requires ChromaDB backend')
337
+ if (this.useChromaDB) {
338
+ return this.chroma.store.storeCorrection(input)
313
339
  }
314
- return this.chroma.store.storeCorrection(input)
340
+ return this.store.storeCorrection(input)
315
341
  }
316
342
 
317
343
  /**
318
- * Get patterns for a project
344
+ * Get patterns for a project — routes to ChromaDB or SQLite
319
345
  */
320
346
  async getPatterns(
321
347
  project?: string,
@@ -324,35 +350,39 @@ export class MemoryManager {
324
350
  limit?: number
325
351
  }
326
352
  ): Promise<any[]> {
327
- if (!this.useChromaDB) {
328
- throw new Error('Pattern retrieval requires ChromaDB backend')
353
+ if (this.useChromaDB) {
354
+ if (project) {
355
+ return this.chroma.store.getPatternsByProject(project, options)
356
+ }
357
+ return this.chroma.store.searchPatterns('', { limit: options?.limit || 10 })
329
358
  }
330
359
  if (project) {
331
- return this.chroma.store.getPatternsByProject(project, options)
360
+ return this.store.getPatternsByProject(project, options)
332
361
  }
333
- // If no project specified, search all patterns
334
- return this.chroma.store.searchPatterns('', { limit: options?.limit || 10 })
362
+ return this.store.searchPatterns('', { limit: options?.limit || 10 })
335
363
  }
336
364
 
337
365
  /**
338
- * Get corrections for a project
366
+ * Get corrections for a project — routes to ChromaDB or SQLite
339
367
  */
340
368
  async getCorrections(
341
369
  project?: string,
342
370
  options?: { limit?: number }
343
371
  ): Promise<any[]> {
344
- if (!this.useChromaDB) {
345
- throw new Error('Correction retrieval requires ChromaDB backend')
372
+ if (this.useChromaDB) {
373
+ if (project) {
374
+ return this.chroma.store.getCorrectionsByProject(project, options?.limit || 10)
375
+ }
376
+ return this.chroma.store.searchCorrections('', { limit: options?.limit || 10 })
346
377
  }
347
378
  if (project) {
348
- return this.chroma.store.getCorrectionsByProject(project, options?.limit || 10)
379
+ return this.store.getCorrectionsByProject(project, options?.limit || 10)
349
380
  }
350
- // If no project specified, search all corrections
351
- return this.chroma.store.searchCorrections('', { limit: options?.limit || 10 })
381
+ return this.store.searchCorrections('', { limit: options?.limit || 10 })
352
382
  }
353
383
 
354
384
  /**
355
- * Search patterns by query
385
+ * Search patterns by query — routes to ChromaDB or SQLite
356
386
  */
357
387
  async searchPatterns(
358
388
  query: string,
@@ -363,14 +393,14 @@ export class MemoryManager {
363
393
  minSimilarity?: number
364
394
  }
365
395
  ): Promise<any[]> {
366
- if (!this.useChromaDB) {
367
- throw new Error('Pattern search requires ChromaDB backend')
396
+ if (this.useChromaDB) {
397
+ return this.chroma.store.searchPatterns(query, options)
368
398
  }
369
- return this.chroma.store.searchPatterns(query, options)
399
+ return this.store.searchPatterns(query, options)
370
400
  }
371
401
 
372
402
  /**
373
- * Search corrections by query
403
+ * Search corrections by query — routes to ChromaDB or SQLite
374
404
  */
375
405
  async searchCorrections(
376
406
  query: string,
@@ -380,10 +410,10 @@ export class MemoryManager {
380
410
  minSimilarity?: number
381
411
  }
382
412
  ): Promise<any[]> {
383
- if (!this.useChromaDB) {
384
- throw new Error('Correction search requires ChromaDB backend')
413
+ if (this.useChromaDB) {
414
+ return this.chroma.store.searchCorrections(query, options)
385
415
  }
386
- return this.chroma.store.searchCorrections(query, options)
416
+ return this.store.searchCorrections(query, options)
387
417
  }
388
418
  }
389
419
 
@@ -41,12 +41,44 @@ CREATE TABLE IF NOT EXISTS decisions (
41
41
  -- Index for decision lookups
42
42
  CREATE INDEX IF NOT EXISTS idx_decisions_memory
43
43
  ON decisions(memory_id);
44
+
45
+ -- Patterns table (Phase 12)
46
+ -- Stores reusable patterns with SQLite fallback
47
+ CREATE TABLE IF NOT EXISTS patterns (
48
+ id TEXT PRIMARY KEY,
49
+ memory_id TEXT NOT NULL,
50
+ project TEXT NOT NULL,
51
+ pattern_type TEXT NOT NULL CHECK(pattern_type IN ('solution','anti-pattern','best-practice','common-issue')),
52
+ description TEXT NOT NULL,
53
+ example TEXT,
54
+ confidence REAL DEFAULT 0.8,
55
+ context TEXT,
56
+ created_at TEXT NOT NULL,
57
+ FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
58
+ );
59
+ CREATE INDEX IF NOT EXISTS idx_patterns_project ON patterns(project);
60
+
61
+ -- Corrections table (Phase 12)
62
+ -- Stores lessons learned with SQLite fallback
63
+ CREATE TABLE IF NOT EXISTS corrections (
64
+ id TEXT PRIMARY KEY,
65
+ memory_id TEXT NOT NULL,
66
+ project TEXT NOT NULL,
67
+ original TEXT NOT NULL,
68
+ correction TEXT NOT NULL,
69
+ reasoning TEXT NOT NULL,
70
+ context TEXT,
71
+ confidence REAL DEFAULT 0.9,
72
+ created_at TEXT NOT NULL,
73
+ FOREIGN KEY (memory_id) REFERENCES memories(id) ON DELETE CASCADE
74
+ );
75
+ CREATE INDEX IF NOT EXISTS idx_corrections_project ON corrections(project);
44
76
  `
45
77
 
46
78
  /**
47
79
  * Schema version for migrations
48
80
  */
49
- export const SCHEMA_VERSION = 1
81
+ export const SCHEMA_VERSION = 2
50
82
 
51
83
  /**
52
84
  * Check if schema needs migration
@@ -17,7 +17,7 @@ import type {
17
17
  DecisionRow
18
18
  } from './types'
19
19
  import { EmbeddingService } from './embeddings'
20
- import { embeddingToBuffer, bufferToEmbedding } from './embedding-utils'
20
+ import { embeddingToBuffer, bufferToEmbedding, cosineSimilarity } from './embedding-utils'
21
21
 
22
22
  export class MemoryStore {
23
23
  private db: Database
@@ -326,6 +326,316 @@ export class MemoryStore {
326
326
  }
327
327
  }
328
328
 
329
+ /**
330
+ * Store a pattern (creates memory + pattern record)
331
+ */
332
+ async storePattern(input: {
333
+ project: string
334
+ pattern_type: 'solution' | 'anti-pattern' | 'best-practice' | 'common-issue'
335
+ description: string
336
+ example?: string
337
+ confidence: number
338
+ context?: string
339
+ }): Promise<string> {
340
+ try {
341
+ const content = `Pattern (${input.pattern_type}): ${input.description}${input.context ? `\nContext: ${input.context}` : ''}${input.example ? `\nExample: ${input.example}` : ''}`
342
+
343
+ const memoryId = await this.storeMemory({
344
+ project: input.project,
345
+ content,
346
+ metadata: {
347
+ type: 'pattern',
348
+ pattern_type: input.pattern_type,
349
+ confidence: input.confidence
350
+ }
351
+ })
352
+
353
+ const patternId = randomUUID()
354
+ const now = new Date().toISOString()
355
+ const stmt = this.db.prepare(`
356
+ INSERT INTO patterns (id, memory_id, project, pattern_type, description, example, confidence, context, created_at)
357
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
358
+ `)
359
+
360
+ stmt.run(
361
+ patternId,
362
+ memoryId,
363
+ input.project,
364
+ input.pattern_type,
365
+ input.description,
366
+ input.example || null,
367
+ input.confidence,
368
+ input.context || null,
369
+ now
370
+ )
371
+
372
+ this.logger.info({ patternId, memoryId, project: input.project }, 'Pattern stored')
373
+ return patternId
374
+ } catch (error) {
375
+ this.logger.error({ error, input }, 'Failed to store pattern')
376
+ throw error
377
+ }
378
+ }
379
+
380
+ /**
381
+ * Store a correction (creates memory + correction record)
382
+ */
383
+ async storeCorrection(input: {
384
+ project: string
385
+ original: string
386
+ correction: string
387
+ reasoning: string
388
+ context?: string
389
+ confidence: number
390
+ }): Promise<string> {
391
+ try {
392
+ const content = `Correction: ${input.correction}\nOriginal: ${input.original}\nReasoning: ${input.reasoning}${input.context ? `\nContext: ${input.context}` : ''}`
393
+
394
+ const memoryId = await this.storeMemory({
395
+ project: input.project,
396
+ content,
397
+ metadata: {
398
+ type: 'correction',
399
+ confidence: input.confidence
400
+ }
401
+ })
402
+
403
+ const correctionId = randomUUID()
404
+ const now = new Date().toISOString()
405
+ const stmt = this.db.prepare(`
406
+ INSERT INTO corrections (id, memory_id, project, original, correction, reasoning, context, confidence, created_at)
407
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
408
+ `)
409
+
410
+ stmt.run(
411
+ correctionId,
412
+ memoryId,
413
+ input.project,
414
+ input.original,
415
+ input.correction,
416
+ input.reasoning,
417
+ input.context || null,
418
+ input.confidence,
419
+ now
420
+ )
421
+
422
+ this.logger.info({ correctionId, memoryId, project: input.project }, 'Correction stored')
423
+ return correctionId
424
+ } catch (error) {
425
+ this.logger.error({ error, input }, 'Failed to store correction')
426
+ throw error
427
+ }
428
+ }
429
+
430
+ /**
431
+ * Get patterns by project with optional type filter
432
+ */
433
+ getPatternsByProject(project: string, options?: {
434
+ pattern_type?: 'solution' | 'anti-pattern' | 'best-practice' | 'common-issue'
435
+ limit?: number
436
+ }): any[] {
437
+ try {
438
+ let sql = 'SELECT * FROM patterns WHERE project = ?'
439
+ const params: any[] = [project]
440
+
441
+ if (options?.pattern_type) {
442
+ sql += ' AND pattern_type = ?'
443
+ params.push(options.pattern_type)
444
+ }
445
+
446
+ sql += ' ORDER BY created_at DESC'
447
+
448
+ if (options?.limit) {
449
+ sql += ' LIMIT ?'
450
+ params.push(options.limit)
451
+ }
452
+
453
+ const stmt = this.db.prepare(sql)
454
+ const rows = stmt.all(...params) as any[]
455
+
456
+ return rows.map(row => ({
457
+ id: row.id,
458
+ description: row.description,
459
+ metadata: {
460
+ project: row.project,
461
+ pattern_type: row.pattern_type,
462
+ example: row.example || '',
463
+ confidence: row.confidence,
464
+ context: row.context || '',
465
+ created_at: row.created_at
466
+ }
467
+ }))
468
+ } catch (error) {
469
+ this.logger.error({ error, project }, 'Failed to get patterns by project')
470
+ return []
471
+ }
472
+ }
473
+
474
+ /**
475
+ * Get corrections by project
476
+ */
477
+ getCorrectionsByProject(project: string, limit: number = 10): any[] {
478
+ try {
479
+ const stmt = this.db.prepare(
480
+ 'SELECT * FROM corrections WHERE project = ? ORDER BY created_at DESC LIMIT ?'
481
+ )
482
+ const rows = stmt.all(project, limit) as any[]
483
+
484
+ return rows.map(row => ({
485
+ id: row.id,
486
+ correction: row.correction,
487
+ metadata: {
488
+ project: row.project,
489
+ original: row.original,
490
+ correction: row.correction,
491
+ reasoning: row.reasoning,
492
+ context: row.context || '',
493
+ confidence: row.confidence,
494
+ created_at: row.created_at
495
+ }
496
+ }))
497
+ } catch (error) {
498
+ this.logger.error({ error, project }, 'Failed to get corrections by project')
499
+ return []
500
+ }
501
+ }
502
+
503
+ /**
504
+ * Search patterns using semantic search on underlying memories
505
+ */
506
+ async searchPatterns(
507
+ query: string,
508
+ options?: {
509
+ project?: string
510
+ pattern_type?: 'solution' | 'anti-pattern' | 'best-practice' | 'common-issue'
511
+ limit?: number
512
+ minSimilarity?: number
513
+ }
514
+ ): Promise<any[]> {
515
+ try {
516
+ // If no query, return all patterns for the project
517
+ if (!query && options?.project) {
518
+ return this.getPatternsByProject(options.project, {
519
+ pattern_type: options?.pattern_type,
520
+ limit: options?.limit
521
+ })
522
+ }
523
+
524
+ // Generate embedding for semantic search
525
+ const queryEmbedding = await this.embeddings.generateEmbedding(query || 'pattern')
526
+
527
+ let sql = `
528
+ SELECT p.*, m.embedding, m.content as memory_content
529
+ FROM patterns p
530
+ JOIN memories m ON p.memory_id = m.id
531
+ WHERE 1=1
532
+ `
533
+ const params: any[] = []
534
+
535
+ if (options?.project) {
536
+ sql += ' AND p.project = ?'
537
+ params.push(options.project)
538
+ }
539
+ if (options?.pattern_type) {
540
+ sql += ' AND p.pattern_type = ?'
541
+ params.push(options.pattern_type)
542
+ }
543
+
544
+ const stmt = this.db.prepare(sql)
545
+ const rows = stmt.all(...params) as any[]
546
+
547
+ const results = rows.map(row => {
548
+ const embedding = bufferToEmbedding(row.embedding)
549
+ const similarity = cosineSimilarity(queryEmbedding, embedding)
550
+ return {
551
+ id: row.id,
552
+ content: row.description,
553
+ metadata: {
554
+ project: row.project,
555
+ pattern_type: row.pattern_type,
556
+ example: row.example || '',
557
+ confidence: row.confidence,
558
+ context: row.context || '',
559
+ created_at: row.created_at
560
+ },
561
+ similarity
562
+ }
563
+ })
564
+ .filter(r => r.similarity >= (options?.minSimilarity || 0.3))
565
+ .sort((a, b) => b.similarity - a.similarity)
566
+ .slice(0, options?.limit || 10)
567
+
568
+ return results
569
+ } catch (error) {
570
+ this.logger.error({ error, query }, 'Pattern search failed')
571
+ return []
572
+ }
573
+ }
574
+
575
+ /**
576
+ * Search corrections using semantic search on underlying memories
577
+ */
578
+ async searchCorrections(
579
+ query: string,
580
+ options?: {
581
+ project?: string
582
+ limit?: number
583
+ minSimilarity?: number
584
+ }
585
+ ): Promise<any[]> {
586
+ try {
587
+ // If no query, return all corrections for the project
588
+ if (!query && options?.project) {
589
+ return this.getCorrectionsByProject(options.project, options?.limit || 10)
590
+ }
591
+
592
+ const queryEmbedding = await this.embeddings.generateEmbedding(query || 'correction')
593
+
594
+ let sql = `
595
+ SELECT c.*, m.embedding, m.content as memory_content
596
+ FROM corrections c
597
+ JOIN memories m ON c.memory_id = m.id
598
+ WHERE 1=1
599
+ `
600
+ const params: any[] = []
601
+
602
+ if (options?.project) {
603
+ sql += ' AND c.project = ?'
604
+ params.push(options.project)
605
+ }
606
+
607
+ const stmt = this.db.prepare(sql)
608
+ const rows = stmt.all(...params) as any[]
609
+
610
+ const results = rows.map(row => {
611
+ const embedding = bufferToEmbedding(row.embedding)
612
+ const similarity = cosineSimilarity(queryEmbedding, embedding)
613
+ return {
614
+ id: row.id,
615
+ content: row.correction,
616
+ metadata: {
617
+ project: row.project,
618
+ original: row.original,
619
+ correction: row.correction,
620
+ reasoning: row.reasoning,
621
+ context: row.context || '',
622
+ confidence: row.confidence,
623
+ created_at: row.created_at
624
+ },
625
+ similarity
626
+ }
627
+ })
628
+ .filter(r => r.similarity >= (options?.minSimilarity || 0.3))
629
+ .sort((a, b) => b.similarity - a.similarity)
630
+ .slice(0, options?.limit || 10)
631
+
632
+ return results
633
+ } catch (error) {
634
+ this.logger.error({ error, query }, 'Correction search failed')
635
+ return []
636
+ }
637
+ }
638
+
329
639
  /**
330
640
  * Convert database row to Decision object
331
641
  */
@@ -23,6 +23,16 @@ export async function handleAnalyzeDecisionEvolution(
23
23
  const { topic, project_name, limit } = input
24
24
 
25
25
  const memory = getMemoryService()
26
+
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
+ )
34
+ }
35
+
26
36
  const tracker = new DecisionEvolutionTracker(
27
37
  logger,
28
38
  memory.chroma.collections,
@@ -23,6 +23,16 @@ export async function handleDetectTrends(
23
23
  const { project_name, period_days, min_occurrences, limit } = input
24
24
 
25
25
  const memory = getMemoryService()
26
+
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
+ )
34
+ }
35
+
26
36
  const detector = new TrendDetector(logger, memory.chroma.collections)
27
37
 
28
38
  const analysis = await detector.detectTrends({
@@ -23,6 +23,16 @@ export async function handleFindCrossProjectPatterns(
23
23
  const { min_projects, limit, query } = input
24
24
 
25
25
  const memory = getMemoryService()
26
+
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
+ )
34
+ }
35
+
26
36
  const generalizer = new PatternGeneralizer(
27
37
  logger,
28
38
  memory.chroma.collections,
@@ -24,6 +24,16 @@ export async function handleGetDecisionTimeline(
24
24
  const { project_name, topic, time_range, limit } = input
25
25
 
26
26
  const memory = getMemoryService()
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
+
27
37
  const timelineBuilder = new TimelineBuilder(
28
38
  logger,
29
39
  memory.chroma.collections,
@@ -23,6 +23,16 @@ 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
+ 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
+ )
34
+ }
35
+
26
36
  const recommender = new Recommender(
27
37
  logger,
28
38
  memory.chroma.collections,
@@ -204,7 +204,7 @@ export const GetRecommendationsSchema = z.object({
204
204
  })
205
205
 
206
206
  export const FindCrossProjectPatternsSchema = z.object({
207
- min_projects: z.number().int().min(2).max(50).optional().default(2),
207
+ min_projects: z.number().int().min(1).max(50).optional().default(2),
208
208
  limit: z.number().int().min(1).max(100).optional().default(20),
209
209
  query: z.string().optional()
210
210
  })
@@ -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) {