claude-brain 0.30.2 → 0.30.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (236) hide show
  1. package/README.md +241 -191
  2. package/VERSION +1 -1
  3. package/assets/CLAUDE-unified.md +11 -11
  4. package/assets/CLAUDE.md +29 -29
  5. package/package.json +7 -3
  6. package/packs/backend/node.json +173 -173
  7. package/packs/core/javascript.json +176 -176
  8. package/packs/core/typescript.json +222 -222
  9. package/packs/frontend/react.json +254 -254
  10. package/packs/meta/testing.json +172 -172
  11. package/scripts/postinstall.mjs +531 -531
  12. package/src/automation/decision-detector.ts +452 -452
  13. package/src/automation/phase12-manager.ts +456 -456
  14. package/src/automation/proactive-recall.ts +373 -373
  15. package/src/automation/project-detector.ts +310 -310
  16. package/src/automation/repo-scanner.ts +210 -205
  17. package/src/cli/auto-setup.ts +75 -75
  18. package/src/cli/auto-start.ts +266 -266
  19. package/src/cli/bin.ts +264 -264
  20. package/src/cli/commands/autostart.ts +90 -90
  21. package/src/cli/commands/chroma.ts +578 -577
  22. package/src/cli/commands/export-training.ts +70 -70
  23. package/src/cli/commands/export.ts +130 -130
  24. package/src/cli/commands/git-hook.ts +183 -183
  25. package/src/cli/commands/hooks.ts +217 -217
  26. package/src/cli/commands/init.ts +123 -123
  27. package/src/cli/commands/install-mcp.ts +122 -111
  28. package/src/cli/commands/models.ts +979 -979
  29. package/src/cli/commands/pack.ts +200 -200
  30. package/src/cli/commands/refresh.ts +344 -339
  31. package/src/cli/commands/reindex.ts +120 -120
  32. package/src/cli/commands/serve.ts +466 -463
  33. package/src/cli/commands/start.ts +44 -44
  34. package/src/cli/commands/status.ts +220 -203
  35. package/src/cli/commands/uninstall-mcp.ts +45 -41
  36. package/src/cli/commands/update.ts +130 -124
  37. package/src/cli/migrate-chroma.ts +106 -106
  38. package/src/cli/ui/animations.ts +80 -80
  39. package/src/cli/ui/components.ts +82 -82
  40. package/src/cli/ui/index.ts +4 -4
  41. package/src/cli/ui/logo.ts +36 -36
  42. package/src/cli/ui/theme.ts +55 -55
  43. package/src/code-intelligence/indexer.ts +352 -352
  44. package/src/code-intelligence/linker.ts +178 -178
  45. package/src/code-intelligence/parser.ts +484 -484
  46. package/src/code-intelligence/query.ts +291 -291
  47. package/src/code-intelligence/schema.ts +83 -83
  48. package/src/code-intelligence/types.ts +95 -95
  49. package/src/config/defaults.ts +52 -52
  50. package/src/config/home.ts +56 -56
  51. package/src/config/index.ts +5 -5
  52. package/src/config/loader.ts +192 -192
  53. package/src/config/schema.ts +446 -415
  54. package/src/config/validator.ts +182 -182
  55. package/src/context/assembler.ts +407 -400
  56. package/src/context/index.ts +79 -79
  57. package/src/context/progress-tracker.ts +174 -174
  58. package/src/context/standards-manager.ts +287 -287
  59. package/src/context/validator.ts +58 -58
  60. package/src/diagnostics/index.ts +122 -121
  61. package/src/health/index.ts +233 -232
  62. package/src/hooks/brain-hook.ts +134 -131
  63. package/src/hooks/capture.ts +168 -168
  64. package/src/hooks/claude-code-mastery.md +112 -112
  65. package/src/hooks/context-hook.ts +260 -245
  66. package/src/hooks/deduplicator.ts +72 -72
  67. package/src/hooks/git-capture.ts +109 -109
  68. package/src/hooks/git-hook-installer.ts +211 -207
  69. package/src/hooks/index.ts +20 -20
  70. package/src/hooks/installer.ts +306 -288
  71. package/src/hooks/interceptor-hook.ts +204 -201
  72. package/src/hooks/passive-classifier.ts +397 -397
  73. package/src/hooks/queue.ts +160 -129
  74. package/src/hooks/session-tracker.ts +312 -312
  75. package/src/hooks/types.ts +52 -52
  76. package/src/index.ts +7 -7
  77. package/src/intelligence/cross-project/generalizer.ts +283 -283
  78. package/src/intelligence/cross-project/index.ts +7 -7
  79. package/src/intelligence/hf-downloader.ts +222 -222
  80. package/src/intelligence/hf-manifest.json +78 -78
  81. package/src/intelligence/index.ts +24 -24
  82. package/src/intelligence/inference-router.ts +762 -762
  83. package/src/intelligence/model-manager.ts +263 -245
  84. package/src/intelligence/optimization/index.ts +10 -10
  85. package/src/intelligence/optimization/precompute.ts +202 -202
  86. package/src/intelligence/optimization/semantic-cache.ts +213 -207
  87. package/src/intelligence/prediction/index.ts +7 -7
  88. package/src/intelligence/prediction/recommender.ts +276 -268
  89. package/src/intelligence/reasoning/chain-retrieval.ts +243 -247
  90. package/src/intelligence/reasoning/index.ts +7 -7
  91. package/src/intelligence/temporal/evolution.ts +193 -197
  92. package/src/intelligence/temporal/index.ts +16 -16
  93. package/src/intelligence/temporal/query-processor.ts +190 -190
  94. package/src/intelligence/temporal/timeline.ts +272 -259
  95. package/src/intelligence/temporal/trends.ts +263 -263
  96. package/src/intelligence/tokenizer.ts +118 -118
  97. package/src/knowledge/entity-extractor.ts +447 -443
  98. package/src/knowledge/graph/builder.ts +185 -185
  99. package/src/knowledge/graph/linker.ts +201 -201
  100. package/src/knowledge/graph/memory-graph.ts +359 -359
  101. package/src/knowledge/graph/schema.ts +99 -99
  102. package/src/knowledge/graph/search.ts +166 -166
  103. package/src/knowledge/relationship-extractor.ts +108 -108
  104. package/src/memory/chroma/client.ts +211 -192
  105. package/src/memory/chroma/collection-manager.ts +92 -92
  106. package/src/memory/chroma/config.ts +57 -57
  107. package/src/memory/chroma/embeddings.ts +177 -175
  108. package/src/memory/chroma/index.ts +82 -82
  109. package/src/memory/chroma/migration.ts +270 -270
  110. package/src/memory/chroma/schemas.ts +69 -69
  111. package/src/memory/chroma/search.ts +319 -315
  112. package/src/memory/chroma/store.ts +755 -747
  113. package/src/memory/compression.ts +121 -121
  114. package/src/memory/consolidation/archiver.ts +162 -165
  115. package/src/memory/consolidation/merger.ts +182 -186
  116. package/src/memory/consolidation/scorer.ts +136 -136
  117. package/src/memory/database.ts +9 -0
  118. package/src/memory/dual-write.ts +145 -0
  119. package/src/memory/embeddings.ts +226 -226
  120. package/src/memory/episodic/detector.ts +108 -108
  121. package/src/memory/episodic/manager.ts +347 -351
  122. package/src/memory/episodic/summarizer.ts +179 -179
  123. package/src/memory/episodic/types.ts +52 -52
  124. package/src/memory/fts5-search.ts +692 -633
  125. package/src/memory/index.ts +943 -1060
  126. package/src/memory/migrations/add-fts5.ts +118 -108
  127. package/src/memory/patterns.ts +438 -438
  128. package/src/memory/pruning.ts +60 -60
  129. package/src/memory/schema.ts +88 -88
  130. package/src/memory/store.ts +911 -787
  131. package/src/orchestrator/handlers/decision-handler.ts +204 -204
  132. package/src/packs/index.ts +9 -9
  133. package/src/packs/loader.ts +134 -134
  134. package/src/packs/manager.ts +204 -204
  135. package/src/packs/ranker.ts +78 -78
  136. package/src/packs/types.ts +81 -81
  137. package/src/phase12/index.ts +5 -5
  138. package/src/retrieval/bm25/index.ts +300 -297
  139. package/src/retrieval/bm25/tokenizer.ts +184 -184
  140. package/src/retrieval/feedback/adaptive.ts +221 -221
  141. package/src/retrieval/feedback/index.ts +16 -16
  142. package/src/retrieval/feedback/metrics.ts +221 -221
  143. package/src/retrieval/feedback/store.ts +283 -283
  144. package/src/retrieval/fusion/index.ts +194 -194
  145. package/src/retrieval/fusion/rrf.ts +165 -165
  146. package/src/retrieval/index.ts +12 -12
  147. package/src/retrieval/pipeline.ts +375 -375
  148. package/src/retrieval/query/expander.ts +203 -203
  149. package/src/retrieval/query/index.ts +27 -27
  150. package/src/retrieval/query/intent-classifier.ts +252 -252
  151. package/src/retrieval/query/temporal-parser.ts +295 -295
  152. package/src/retrieval/reranker/index.ts +189 -188
  153. package/src/retrieval/reranker/model.ts +99 -95
  154. package/src/retrieval/service.ts +125 -125
  155. package/src/retrieval/types.ts +162 -162
  156. package/src/routing/entity-extractor.ts +454 -454
  157. package/src/routing/handlers/exploration-handler.ts +369 -0
  158. package/src/routing/handlers/index.ts +19 -0
  159. package/src/routing/handlers/memory-handler.ts +273 -0
  160. package/src/routing/handlers/mutation-handler.ts +241 -0
  161. package/src/routing/handlers/recall-handler.ts +642 -0
  162. package/src/routing/handlers/shared.ts +515 -0
  163. package/src/routing/handlers/types.ts +48 -0
  164. package/src/routing/intent-classifier.ts +552 -552
  165. package/src/routing/response-filter.ts +399 -391
  166. package/src/routing/router.ts +245 -2193
  167. package/src/routing/search-engine.ts +521 -514
  168. package/src/routing/types.ts +104 -94
  169. package/src/scripts/health-check.ts +118 -118
  170. package/src/scripts/setup.ts +122 -122
  171. package/src/server/auto-updater.ts +283 -276
  172. package/src/server/handlers/call-tool.ts +159 -159
  173. package/src/server/handlers/list-tools.ts +35 -35
  174. package/src/server/handlers/tools/auto-remember.ts +165 -165
  175. package/src/server/handlers/tools/brain.ts +86 -86
  176. package/src/server/handlers/tools/create-project.ts +135 -135
  177. package/src/server/handlers/tools/get-code-standards.ts +123 -123
  178. package/src/server/handlers/tools/get-corrections.ts +152 -152
  179. package/src/server/handlers/tools/get-patterns.ts +156 -156
  180. package/src/server/handlers/tools/get-project-context.ts +75 -75
  181. package/src/server/handlers/tools/index.ts +30 -30
  182. package/src/server/handlers/tools/init-project.ts +756 -756
  183. package/src/server/handlers/tools/list-projects.ts +126 -126
  184. package/src/server/handlers/tools/recall-similar.ts +87 -87
  185. package/src/server/handlers/tools/recognize-pattern.ts +132 -132
  186. package/src/server/handlers/tools/record-correction.ts +131 -131
  187. package/src/server/handlers/tools/remember-decision.ts +168 -168
  188. package/src/server/handlers/tools/schemas.ts +179 -179
  189. package/src/server/handlers/tools/search-code.ts +122 -122
  190. package/src/server/handlers/tools/smart-context.ts +146 -146
  191. package/src/server/handlers/tools/update-progress.ts +131 -131
  192. package/src/server/http-api.ts +215 -1229
  193. package/src/server/mcp-proxy.ts +85 -84
  194. package/src/server/mcp-server.ts +285 -284
  195. package/src/server/middleware/auth.ts +39 -0
  196. package/src/server/middleware/error-handler.ts +37 -0
  197. package/src/server/middleware/rate-limit.ts +53 -0
  198. package/src/server/middleware/validate.ts +42 -0
  199. package/src/server/pid-manager.ts +137 -136
  200. package/src/server/providers/resources.ts +581 -581
  201. package/src/server/routes/code.ts +228 -0
  202. package/src/server/routes/context.ts +26 -0
  203. package/src/server/routes/health.ts +19 -0
  204. package/src/server/routes/helpers.ts +100 -0
  205. package/src/server/routes/hooks.ts +197 -0
  206. package/src/server/routes/mcp.ts +47 -0
  207. package/src/server/routes/memory.ts +397 -0
  208. package/src/server/routes/models.ts +96 -0
  209. package/src/server/routes/projects.ts +89 -0
  210. package/src/server/routes/types.ts +21 -0
  211. package/src/server/schemas/api-schemas.ts +202 -0
  212. package/src/server/services.ts +720 -720
  213. package/src/server/utils/memory-indicator.ts +84 -84
  214. package/src/server/utils/response-formatter.ts +129 -129
  215. package/src/server/web-viewer.ts +1145 -1115
  216. package/src/setup/index.ts +38 -38
  217. package/src/tools/registry.ts +115 -115
  218. package/src/tools/schemas.ts +666 -666
  219. package/src/tools/types.ts +412 -412
  220. package/src/training/data-store.ts +320 -298
  221. package/src/training/retrain-pipeline.ts +399 -394
  222. package/src/utils/error-handler.ts +136 -136
  223. package/src/utils/index.ts +58 -58
  224. package/src/utils/kill-port.ts +55 -53
  225. package/src/utils/phase12-helper.ts +56 -56
  226. package/src/utils/safe-path.ts +43 -0
  227. package/src/utils/timing.ts +47 -47
  228. package/src/utils/transaction.ts +63 -63
  229. package/src/vault/index.ts +4 -3
  230. package/src/vault/paths.ts +106 -106
  231. package/src/vault/query.ts +4 -1
  232. package/src/vault/reader.ts +44 -1
  233. package/src/vault/watcher.ts +24 -1
  234. package/src/vault/writer.ts +487 -413
  235. package/skills/persistent-memory/SKILL.md +0 -148
  236. package/skills/persistent-memory/references/tool-reference.md +0 -90
@@ -1,1060 +1,943 @@
1
- /**
2
- * Memory System - Main Module
3
- * Phase 3: Memory and Embedding System
4
- *
5
- * Unified memory system manager that combines all components
6
- */
7
-
8
- import { randomUUID } from 'crypto'
9
- import type { Logger } from 'pino'
10
- import { MemoryDatabase } from './database'
11
- import { EmbeddingService } from './embeddings'
12
- import { MemoryStore } from './store'
13
- import { SemanticSearch } from './search'
14
- import { MemoryContextBuilder } from './context-builder'
15
- import type { MemorySystemStats } from './types'
16
- import { ChromaManager, DEFAULT_CHROMA_CONFIG, getChromaConfigFromEnv, ChromaMigration, type MigrationOptions } from './chroma'
17
- import { FTS5Search } from './fts5-search'
18
- import { addFTS5Tables } from './migrations/add-fts5'
19
-
20
- // Re-export all types and classes for external use
21
- export * from './types'
22
- export { MemoryDatabase } from './database'
23
- export { EmbeddingService } from './embeddings'
24
- export { MemoryStore } from './store'
25
- export { SemanticSearch } from './search'
26
- export { MemoryContextBuilder, type ContextOptions } from './context-builder'
27
- export {
28
- embeddingToBuffer,
29
- bufferToEmbedding,
30
- normalizeEmbedding,
31
- euclideanDistance,
32
- cosineSimilarity,
33
- dotProduct,
34
- magnitude,
35
- averageEmbeddings,
36
- topKSimilar
37
- } from './embedding-utils'
38
-
39
- // Phase 12: Advanced Memory Features
40
- export { PatternRecognizer, type Pattern } from './patterns'
41
- export { LearningSystem, type Correction, type Preference, type LearningInsights } from './learning'
42
- export { KnowledgeExtractor, type ExtractedKnowledge, type ExtractionResult } from './knowledge-extractor'
43
-
44
- // Phase 26: FTS5 Search
45
- export { FTS5Search } from './fts5-search'
46
- export type { ObservationCategory, NewObservation, ObservationResult, ScoredResult } from './fts5-search'
47
-
48
- /**
49
- * Unified memory system manager
50
- * Combines database, embeddings, store, search, and context building
51
- */
52
- export class MemoryManager {
53
- readonly database: MemoryDatabase
54
- readonly embeddings: EmbeddingService
55
- readonly contextBuilder: MemoryContextBuilder
56
- readonly chroma: ChromaManager
57
-
58
- // Phase 26: FTS5 search (always available, no external deps)
59
- private _fts5: FTS5Search | null = null
60
-
61
- // Store and search are initialized after database is ready
62
- private _store: MemoryStore | null = null
63
- private _search: SemanticSearch | null = null
64
-
65
- private logger: Logger
66
- private initialized: boolean = false
67
- private useChromaDB: boolean = true
68
- private onDecisionStoredCallbacks: ((input: any) => void)[] = []
69
- private onDecisionDeletedCallbacks: ((id: string) => void)[] = []
70
-
71
- constructor(
72
- dbPath: string,
73
- logger: Logger,
74
- useChromaDB: boolean = true,
75
- chromaConfig?: any,
76
- customEmbeddings?: EmbeddingService
77
- ) {
78
- this.logger = logger.child({ component: 'memory-manager' })
79
- this.useChromaDB = useChromaDB
80
-
81
- this.database = new MemoryDatabase(dbPath, logger)
82
- this.embeddings = customEmbeddings || new EmbeddingService(logger)
83
- this.contextBuilder = new MemoryContextBuilder(logger)
84
-
85
- const envConfig = getChromaConfigFromEnv()
86
- const config = { ...DEFAULT_CHROMA_CONFIG, ...envConfig, ...chromaConfig }
87
- this.chroma = new ChromaManager(logger, config)
88
- }
89
-
90
- /**
91
- * Initialize memory system
92
- * Must be called before using store or search
93
- */
94
- async initialize(): Promise<void> {
95
- if (this.initialized) {
96
- this.logger.warn('Memory system already initialized')
97
- return
98
- }
99
-
100
- try {
101
- this.logger.info('Initializing memory system...')
102
-
103
- await this.database.initialize()
104
-
105
- await this.embeddings.initialize()
106
-
107
- const db = this.database.getDb()
108
- this._store = new MemoryStore(db, this.embeddings, this.logger)
109
- this._search = new SemanticSearch(db, this.embeddings, this.logger)
110
-
111
- // Phase 26: Always initialize FTS5 (just SQLite, no external deps)
112
- try {
113
- addFTS5Tables(db)
114
- this._fts5 = new FTS5Search(db, this.logger)
115
- this.logger.info('FTS5 search initialized')
116
-
117
- // Phase 26b: Backfill embeddings for existing observations (async, non-blocking)
118
- if (this.embeddings.isReady()) {
119
- this.backfillEmbeddings().then(count => {
120
- if (count > 0) {
121
- this.logger.info({ count }, 'Backfilled embeddings for existing observations')
122
- }
123
- }).catch(() => {
124
- // Non-critical, log handled in backfillEmbeddings
125
- })
126
- }
127
- } catch (error) {
128
- this.logger.warn({ error }, 'Failed to initialize FTS5, continuing without it')
129
- }
130
-
131
- if (this.useChromaDB) {
132
- try {
133
- await this.chroma.initialize()
134
- this.logger.info('ChromaDB backend initialized successfully')
135
- } catch (error) {
136
- this.logger.warn({ error }, 'Failed to initialize ChromaDB, falling back to SQLite backend')
137
- this.useChromaDB = false
138
- }
139
- }
140
-
141
- this.initialized = true
142
- this.logger.info('Memory system initialized successfully')
143
- } catch (error) {
144
- this.logger.error({ error }, 'Failed to initialize memory system')
145
- throw error
146
- }
147
- }
148
-
149
- /**
150
- * Get the memory store (throws if not initialized)
151
- */
152
- get store(): MemoryStore {
153
- if (!this._store) {
154
- throw new Error('Memory system not initialized. Call initialize() first.')
155
- }
156
- return this._store
157
- }
158
-
159
- /**
160
- * Get the semantic search engine (throws if not initialized)
161
- */
162
- get search(): SemanticSearch {
163
- if (!this._search) {
164
- throw new Error('Memory system not initialized. Call initialize() first.')
165
- }
166
- return this._search
167
- }
168
-
169
- /**
170
- * Get the FTS5 search engine (null if not initialized)
171
- */
172
- get fts5(): FTS5Search | null {
173
- return this._fts5
174
- }
175
-
176
- /**
177
- * Check if memory system is initialized
178
- */
179
- isInitialized(): boolean {
180
- return this.initialized
181
- }
182
-
183
- /**
184
- * Generate and store an embedding for an observation (fire-and-forget).
185
- * Runs asynchronously to avoid blocking the store operation.
186
- */
187
- private storeObservationEmbedding(observationId: string, text: string): void {
188
- if (!this._fts5 || !this.embeddings.isReady()) return
189
-
190
- // Fire-and-forget: don't block the store operation
191
- this.embeddings.generateEmbedding(text).then(embedding => {
192
- this._fts5?.storeEmbedding(observationId, embedding)
193
- }).catch(error => {
194
- this.logger.debug({ error, observationId }, 'Failed to store observation embedding')
195
- })
196
- }
197
-
198
- /**
199
- * Backfill embeddings for existing observations that don't have them.
200
- * Call this after initialization to migrate existing data.
201
- */
202
- async backfillEmbeddings(batchSize: number = 50): Promise<number> {
203
- if (!this._fts5 || !this.embeddings.isReady()) return 0
204
- return this._fts5.backfillEmbeddings(this.embeddings, batchSize)
205
- }
206
-
207
- close(): void {
208
- if (this.useChromaDB) {
209
- this.chroma.close()
210
- }
211
- this.database.close()
212
- this._store = null
213
- this._search = null
214
- this.initialized = false
215
- this.logger.info('Memory system closed')
216
- }
217
-
218
- /**
219
- * Get system statistics
220
- */
221
- getStats(): MemorySystemStats {
222
- if (!this.initialized) {
223
- throw new Error('Memory system not initialized')
224
- }
225
-
226
- return {
227
- database: this.database.getStats(),
228
- embeddings: this.embeddings.getCacheStats()
229
- }
230
- }
231
-
232
- /**
233
- * Health check
234
- */
235
- async healthCheck(): Promise<boolean> {
236
- if (!this.initialized) {
237
- return false
238
- }
239
- return this.database.healthCheck()
240
- }
241
-
242
- /**
243
- * Check if ChromaDB backend is enabled and connected
244
- */
245
- isChromaDBEnabled(): boolean {
246
- return this.useChromaDB
247
- }
248
-
249
- /**
250
- * Add a listener that fires when a decision is stored (from any backend)
251
- */
252
- addDecisionStoredListener(callback: (input: any) => void): () => void {
253
- this.onDecisionStoredCallbacks.push(callback)
254
- let chromaUnsub: (() => void) | undefined
255
- if (this.useChromaDB) {
256
- chromaUnsub = this.chroma.store.addDecisionStoredListener(callback)
257
- }
258
- return () => {
259
- const idx = this.onDecisionStoredCallbacks.indexOf(callback)
260
- if (idx >= 0) this.onDecisionStoredCallbacks.splice(idx, 1)
261
- chromaUnsub?.()
262
- }
263
- }
264
-
265
- /**
266
- * Add a listener that fires when a decision is deleted
267
- */
268
- addDecisionDeletedListener(callback: (id: string) => void): () => void {
269
- this.onDecisionDeletedCallbacks.push(callback)
270
- return () => {
271
- const idx = this.onDecisionDeletedCallbacks.indexOf(callback)
272
- if (idx >= 0) this.onDecisionDeletedCallbacks.splice(idx, 1)
273
- }
274
- }
275
-
276
- async rememberDecision(
277
- project: string,
278
- context: string,
279
- decision: string,
280
- reasoning: string,
281
- options?: { alternatives?: string; tags?: string[] }
282
- ): Promise<string> {
283
- // BUG-001 fix: Generate a single shared ID for all backends
284
- const sharedId = randomUUID()
285
-
286
- // Phase 26: Always store in FTS5 if available
287
- let fts5Id: string | undefined
288
- if (this._fts5) {
289
- try {
290
- // Check for duplicates first
291
- const dup = this._fts5.searchForDuplicates(decision, project)
292
- if (dup) {
293
- this.logger.info({ existingId: dup.id, score: dup.score }, 'FTS5 skipping duplicate decision')
294
- return dup.id
295
- }
296
-
297
- fts5Id = this._fts5.store({
298
- project,
299
- category: 'decision',
300
- content: decision,
301
- reasoning,
302
- context,
303
- tags: options?.tags
304
- }, sharedId)
305
-
306
- // Store embedding for semantic search
307
- this.storeObservationEmbedding(fts5Id, [decision, reasoning, context].filter(Boolean).join(' '))
308
- } catch (error) {
309
- this.logger.warn({ error }, 'FTS5 store failed, continuing with other backends')
310
- }
311
- }
312
-
313
- // Store in ChromaDB if available
314
- if (this.useChromaDB) {
315
- try {
316
- const chromaId = await this.chroma.store.storeDecision({
317
- id: sharedId,
318
- project,
319
- context,
320
- decision,
321
- reasoning,
322
- alternatives: options?.alternatives,
323
- tags: options?.tags
324
- })
325
- return fts5Id || chromaId
326
- } catch (error) {
327
- this.logger.warn({ error }, 'ChromaDB store failed')
328
- if (fts5Id) return fts5Id
329
- }
330
- }
331
-
332
- // Fallback to legacy SQLite store if no FTS5 and no ChromaDB
333
- if (!fts5Id) {
334
- const id = await this.store.storeDecision({
335
- project,
336
- context,
337
- decision,
338
- reasoning,
339
- alternatives: options?.alternatives,
340
- tags: options?.tags
341
- })
342
- fts5Id = id
343
- }
344
-
345
- // Notify listeners (e.g., knowledge graph builder)
346
- for (const cb of this.onDecisionStoredCallbacks) {
347
- try {
348
- cb({ project, context, decision, reasoning, alternatives: options?.alternatives, tags: options?.tags, id: fts5Id })
349
- } catch {}
350
- }
351
- return fts5Id!
352
- }
353
-
354
- /**
355
- * Get raw search results - uses FTS5 as primary, embeddings as semantic fallback
356
- * Use this for internal operations that need raw results
357
- */
358
- async searchRaw(
359
- query: string,
360
- options?: { project?: string; limit?: number; minSimilarity?: number }
361
- ): Promise<any[]> {
362
- const limit = options?.limit || 5
363
- const LOW_CONFIDENCE_THRESHOLD = 0.4
364
-
365
- // Phase 26: Try FTS5 first (always available)
366
- if (this._fts5) {
367
- const ftsResults = this._fts5.searchWithConfidence(query, options?.project, limit)
368
-
369
- // Check if FTS5 returned good results
370
- const hasGoodResults = ftsResults.length > 0 &&
371
- ftsResults.some(r => r.score >= LOW_CONFIDENCE_THRESHOLD)
372
-
373
- if (hasGoodResults) {
374
- return this.transformFtsResults(ftsResults)
375
- }
376
-
377
- // Phase 26b: FTS5 returned nothing or low-confidence — try semantic search
378
- if (this.embeddings.isReady() && this._fts5.hasEmbeddings()) {
379
- try {
380
- const queryEmbedding = await this.embeddings.generateEmbedding(query)
381
- const semanticResults = await this._fts5.semanticSearch(
382
- queryEmbedding,
383
- options?.project,
384
- limit,
385
- options?.minSimilarity || 0.3
386
- )
387
-
388
- if (semanticResults.length > 0) {
389
- this.logger.debug(
390
- { query, ftsCount: ftsResults.length, semanticCount: semanticResults.length },
391
- 'Semantic search found results where FTS5 did not'
392
- )
393
-
394
- // If we had some low-confidence FTS5 results, merge with semantic results
395
- if (ftsResults.length > 0) {
396
- return this.mergeSearchResults(ftsResults, semanticResults, limit)
397
- }
398
-
399
- return this.transformFtsResults(semanticResults)
400
- }
401
- } catch (error) {
402
- this.logger.warn({ error }, 'Semantic search fallback failed')
403
- }
404
- }
405
-
406
- // Return whatever FTS5 found (even if low confidence)
407
- if (ftsResults.length > 0) {
408
- return this.transformFtsResults(ftsResults)
409
- }
410
- }
411
-
412
- // Fallback: Try ChromaDB if available
413
- if (this.useChromaDB) {
414
- const chromaResults = await this.chroma.search.searchDecisions(query, {
415
- project: options?.project,
416
- limit,
417
- minSimilarity: options?.minSimilarity || 0.5
418
- })
419
- return chromaResults.map(r => {
420
- const memoryContent = typeof r.content === 'string' ? r.content : JSON.stringify(r.content)
421
- const decisionObj = r.metadata.decision ? {
422
- id: r.id,
423
- project: r.metadata.project || options?.project || 'unknown',
424
- context: r.metadata.context || '',
425
- decision: r.metadata.decision || memoryContent,
426
- reasoning: r.metadata.reasoning || '',
427
- alternatives: r.metadata.alternatives_considered || '',
428
- tags: r.metadata.tags || [],
429
- outcome: r.metadata.outcome,
430
- createdAt: r.metadata.created_at ? new Date(r.metadata.created_at) : new Date()
431
- } : undefined
432
-
433
- return {
434
- id: r.id,
435
- content: decisionObj ? decisionObj.decision : memoryContent,
436
- memory: {
437
- id: r.id,
438
- project: r.metadata.project || options?.project || 'unknown',
439
- content: memoryContent,
440
- createdAt: r.metadata.created_at ? new Date(r.metadata.created_at) : new Date(),
441
- metadata: r.metadata
442
- },
443
- similarity: r.similarity,
444
- decision: decisionObj,
445
- metadata: r.metadata
446
- }
447
- })
448
- }
449
-
450
- // Final fallback: legacy SQLite search
451
- return await this.search.search(query, {
452
- project: options?.project,
453
- limit,
454
- minSimilarity: options?.minSimilarity || 0.5
455
- })
456
- }
457
-
458
- /**
459
- * Transform FTS5/semantic ScoredResults to the expected MemorySearchResult structure
460
- */
461
- private transformFtsResults(results: import('./fts5-search').ScoredResult[]): any[] {
462
- return results.map(r => ({
463
- id: r.id,
464
- content: r.content,
465
- memory: {
466
- id: r.id,
467
- project: r.project,
468
- content: r.content,
469
- createdAt: new Date(r.created_at),
470
- metadata: {
471
- project: r.project,
472
- category: r.category,
473
- context: r.context || '',
474
- reasoning: r.reasoning || '',
475
- tags: r.tags,
476
- created_at: r.created_at
477
- }
478
- },
479
- similarity: r.score,
480
- decision: r.category === 'decision' ? {
481
- id: r.id,
482
- project: r.project,
483
- context: r.context || '',
484
- decision: r.content,
485
- reasoning: r.reasoning || '',
486
- alternatives: '',
487
- tags: r.tags,
488
- createdAt: new Date(r.created_at)
489
- } : undefined,
490
- metadata: {
491
- project: r.project,
492
- category: r.category,
493
- context: r.context || '',
494
- reasoning: r.reasoning || '',
495
- tags: r.tags,
496
- created_at: r.created_at
497
- }
498
- }))
499
- }
500
-
501
- /**
502
- * Merge FTS5 and semantic search results, deduplicating by ID.
503
- * Semantic results get a small boost since they matched conceptually.
504
- */
505
- private mergeSearchResults(
506
- ftsResults: import('./fts5-search').ScoredResult[],
507
- semanticResults: import('./fts5-search').ScoredResult[],
508
- limit: number
509
- ): any[] {
510
- const seen = new Set<string>()
511
- const merged: import('./fts5-search').ScoredResult[] = []
512
-
513
- // Add semantic results first (they matched conceptually when FTS5 didn't)
514
- for (const r of semanticResults) {
515
- if (!seen.has(r.id)) {
516
- seen.add(r.id)
517
- merged.push(r)
518
- }
519
- }
520
-
521
- // Then add FTS5 results that aren't already included
522
- for (const r of ftsResults) {
523
- if (!seen.has(r.id)) {
524
- seen.add(r.id)
525
- merged.push(r)
526
- }
527
- }
528
-
529
- // Sort by score descending and limit
530
- merged.sort((a, b) => b.score - a.score)
531
- return this.transformFtsResults(merged.slice(0, limit))
532
- }
533
-
534
- async recallSimilar(
535
- query: string,
536
- options?: { project?: string; limit?: number; minSimilarity?: number }
537
- ): Promise<string> {
538
- const results = await this.searchRaw(query, options)
539
- return this.contextBuilder.buildDecisionContext(results)
540
- }
541
-
542
- async getRecommendations(
543
- currentContext: string,
544
- project: string,
545
- limit: number = 3
546
- ): Promise<string> {
547
- const results = await this.search.getRecommendations(currentContext, project, limit)
548
- return this.contextBuilder.buildRecommendationContext(results)
549
- }
550
-
551
- async migrateToChromaDB(options: MigrationOptions = {}): Promise<any> {
552
- if (!this.useChromaDB) {
553
- throw new Error('ChromaDB is not enabled')
554
- }
555
-
556
- const migration = new ChromaMigration(
557
- this.logger,
558
- this.database.getDb(),
559
- this.chroma.store,
560
- this.chroma.collections
561
- )
562
-
563
- return migration.migrate(options)
564
- }
565
-
566
- /**
567
- * Store a pattern in memory — dual-writes to FTS5 + ChromaDB/SQLite
568
- */
569
- async storePattern(input: {
570
- project: string
571
- pattern_type: 'solution' | 'anti-pattern' | 'best-practice' | 'common-issue'
572
- description: string
573
- example?: string
574
- confidence: number
575
- context?: string
576
- source?: string
577
- }): Promise<string> {
578
- // BUG-001 fix: Generate a single shared ID for all backends
579
- const sharedId = randomUUID()
580
-
581
- // Phase 26: Dual-write to FTS5
582
- let fts5Id: string | undefined
583
- if (this._fts5) {
584
- try {
585
- fts5Id = this._fts5.store({
586
- project: input.project,
587
- category: 'pattern',
588
- content: input.description,
589
- context: input.context,
590
- confidence: input.confidence,
591
- source: input.source
592
- }, sharedId)
593
-
594
- // Store embedding for semantic search
595
- this.storeObservationEmbedding(fts5Id, [input.description, input.context].filter(Boolean).join(' '))
596
- } catch (error) {
597
- this.logger.warn({ error }, 'FTS5 pattern store failed')
598
- }
599
- }
600
-
601
- if (this.useChromaDB) {
602
- try {
603
- const chromaId = await this.chroma.store.storePattern({ ...input, id: sharedId })
604
- return fts5Id || chromaId
605
- } catch (error) {
606
- this.logger.warn({ error }, 'ChromaDB pattern store failed')
607
- if (fts5Id) return fts5Id
608
- }
609
- }
610
-
611
- if (!fts5Id) {
612
- return this.store.storePattern(input)
613
- }
614
- return fts5Id
615
- }
616
-
617
- /**
618
- * Store a correction/lesson learned — dual-writes to FTS5 + ChromaDB/SQLite
619
- */
620
- async storeCorrection(input: {
621
- project: string
622
- original: string
623
- correction: string
624
- reasoning: string
625
- context?: string
626
- confidence: number
627
- }): Promise<string> {
628
- // BUG-001 fix: Generate a single shared ID for all backends
629
- const sharedId = randomUUID()
630
-
631
- // Phase 26: Dual-write to FTS5
632
- let fts5Id: string | undefined
633
- if (this._fts5) {
634
- try {
635
- fts5Id = this._fts5.store({
636
- project: input.project,
637
- category: 'correction',
638
- content: input.correction,
639
- reasoning: input.reasoning,
640
- context: input.context,
641
- confidence: input.confidence
642
- }, sharedId)
643
-
644
- // Store embedding for semantic search
645
- this.storeObservationEmbedding(fts5Id, [input.correction, input.reasoning, input.context].filter(Boolean).join(' '))
646
- } catch (error) {
647
- this.logger.warn({ error }, 'FTS5 correction store failed')
648
- }
649
- }
650
-
651
- if (this.useChromaDB) {
652
- try {
653
- const chromaId = await this.chroma.store.storeCorrection({ ...input, id: sharedId })
654
- return fts5Id || chromaId
655
- } catch (error) {
656
- this.logger.warn({ error }, 'ChromaDB correction store failed')
657
- if (fts5Id) return fts5Id
658
- }
659
- }
660
-
661
- if (!fts5Id) {
662
- return this.store.storeCorrection(input)
663
- }
664
- return fts5Id
665
- }
666
-
667
- /**
668
- * Get patterns for a project — routes to FTS5, ChromaDB, or legacy SQLite
669
- */
670
- async getPatterns(
671
- project?: string,
672
- options?: {
673
- pattern_type?: 'solution' | 'anti-pattern' | 'best-practice' | 'common-issue'
674
- limit?: number
675
- }
676
- ): Promise<any[]> {
677
- // Phase 26: Try FTS5 first (BUG-002: works with or without project)
678
- if (this._fts5) {
679
- const results = this._fts5.fetchAll(project, 'pattern')
680
- if (results.length > 0) {
681
- return results.map(r => ({
682
- id: r.id,
683
- description: r.content,
684
- metadata: {
685
- project: r.project,
686
- pattern_type: r.category,
687
- confidence: r.confidence,
688
- context: r.context || '',
689
- created_at: r.created_at
690
- }
691
- }))
692
- }
693
- }
694
-
695
- if (this.useChromaDB) {
696
- if (project) {
697
- return this.chroma.store.getPatternsByProject(project, options)
698
- }
699
- return this.chroma.store.searchPatterns('', { limit: options?.limit || 10 })
700
- }
701
- if (project) {
702
- return this.store.getPatternsByProject(project, options)
703
- }
704
- return this.store.searchPatterns('', { limit: options?.limit || 10 })
705
- }
706
-
707
- /**
708
- * Get corrections for a project — routes to FTS5, ChromaDB, or legacy SQLite
709
- */
710
- async getCorrections(
711
- project?: string,
712
- options?: { limit?: number }
713
- ): Promise<any[]> {
714
- // Phase 26: Try FTS5 first (BUG-002: works with or without project)
715
- if (this._fts5) {
716
- const results = this._fts5.fetchAll(project, 'correction')
717
- if (results.length > 0) {
718
- return results.map(r => ({
719
- id: r.id,
720
- correction: r.content,
721
- metadata: {
722
- project: r.project,
723
- reasoning: r.reasoning || '',
724
- context: r.context || '',
725
- confidence: r.confidence,
726
- created_at: r.created_at
727
- }
728
- }))
729
- }
730
- }
731
-
732
- if (this.useChromaDB) {
733
- if (project) {
734
- return this.chroma.store.getCorrectionsByProject(project, options?.limit || 10)
735
- }
736
- return this.chroma.store.searchCorrections('', { limit: options?.limit || 10 })
737
- }
738
- if (project) {
739
- return this.store.getCorrectionsByProject(project, options?.limit || 10)
740
- }
741
- return this.store.searchCorrections('', { limit: options?.limit || 10 })
742
- }
743
-
744
- /**
745
- * Fetch all decisions with content — routes to FTS5, ChromaDB, or legacy SQLite
746
- * Used by analytical tools that need bulk access to decision data
747
- */
748
- async fetchAllDecisions(project?: string): Promise<any[]> {
749
- // Phase 26: Try FTS5 first (BUG-002: works with or without project)
750
- if (this._fts5) {
751
- const results = this._fts5.fetchAll(project, 'decision')
752
- if (results.length > 0) {
753
- return results.map(r => ({
754
- id: r.id,
755
- content: r.content,
756
- date: r.created_at,
757
- project: r.project,
758
- context: r.context || '',
759
- decision: r.content,
760
- reasoning: r.reasoning || '',
761
- alternatives: '',
762
- tags: r.tags
763
- }))
764
- }
765
- }
766
-
767
- if (this.useChromaDB) {
768
- try {
769
- const collection = await this.chroma.collections.getDecisions()
770
- const results = await collection.get({
771
- where: project ? { project } : undefined
772
- })
773
- if (results && results.ids) {
774
- return results.ids.map((id: string, i: number) => ({
775
- id,
776
- content: results.documents?.[i] || '',
777
- date: results.metadatas?.[i]?.created_at || new Date().toISOString(),
778
- project: results.metadatas?.[i]?.project || project || 'unknown',
779
- context: results.metadatas?.[i]?.context || '',
780
- decision: results.metadatas?.[i]?.decision || results.documents?.[i] || '',
781
- reasoning: results.metadatas?.[i]?.reasoning || '',
782
- alternatives: results.metadatas?.[i]?.alternatives_considered || '',
783
- tags: results.metadatas?.[i]?.tags || []
784
- }))
785
- }
786
- return []
787
- } catch (error) {
788
- this.logger.warn({ error }, 'ChromaDB fetchAllDecisions failed, falling back to SQLite')
789
- }
790
- }
791
- return this.store.getAllDecisionsWithContent(project)
792
- }
793
-
794
- /**
795
- * Fetch all patterns with content — routes to FTS5, ChromaDB, or legacy SQLite
796
- */
797
- async fetchAllPatterns(project?: string): Promise<any[]> {
798
- // Phase 26: Try FTS5 first (BUG-002: works with or without project)
799
- if (this._fts5) {
800
- const results = this._fts5.fetchAll(project, 'pattern')
801
- if (results.length > 0) {
802
- return results.map(r => ({
803
- id: r.id,
804
- content: r.content,
805
- date: r.created_at,
806
- project: r.project,
807
- pattern_type: '',
808
- description: r.content,
809
- example: '',
810
- confidence: r.confidence,
811
- context: r.context || ''
812
- }))
813
- }
814
- }
815
-
816
- if (this.useChromaDB) {
817
- try {
818
- const collection = await this.chroma.collections.getPatterns()
819
- const results = await collection.get({
820
- where: project ? { project } : undefined
821
- })
822
- if (results && results.ids) {
823
- return results.ids.map((id: string, i: number) => ({
824
- id,
825
- content: results.documents?.[i] || '',
826
- date: results.metadatas?.[i]?.created_at || new Date().toISOString(),
827
- project: results.metadatas?.[i]?.project || project || 'unknown',
828
- pattern_type: results.metadatas?.[i]?.pattern_type || '',
829
- description: results.metadatas?.[i]?.description || results.documents?.[i] || '',
830
- example: results.metadatas?.[i]?.example || '',
831
- confidence: results.metadatas?.[i]?.confidence || 0,
832
- context: results.metadatas?.[i]?.context || ''
833
- }))
834
- }
835
- return []
836
- } catch (error) {
837
- this.logger.warn({ error }, 'ChromaDB fetchAllPatterns failed, falling back to SQLite')
838
- }
839
- }
840
- return this.store.getAllPatternsWithContent(project)
841
- }
842
-
843
- /**
844
- * Fetch all corrections with content — routes to FTS5, ChromaDB, or legacy SQLite
845
- */
846
- async fetchAllCorrections(project?: string): Promise<any[]> {
847
- // Phase 26: Try FTS5 first (BUG-002: works with or without project)
848
- if (this._fts5) {
849
- const results = this._fts5.fetchAll(project, 'correction')
850
- if (results.length > 0) {
851
- return results.map(r => ({
852
- id: r.id,
853
- content: r.content,
854
- date: r.created_at,
855
- project: r.project,
856
- original: '',
857
- correction: r.content,
858
- reasoning: r.reasoning || '',
859
- context: r.context || '',
860
- confidence: r.confidence
861
- }))
862
- }
863
- }
864
-
865
- if (this.useChromaDB) {
866
- try {
867
- const collection = await this.chroma.collections.getCorrections()
868
- const results = await collection.get({
869
- where: project ? { project } : undefined
870
- })
871
- if (results && results.ids) {
872
- return results.ids.map((id: string, i: number) => ({
873
- id,
874
- content: results.documents?.[i] || '',
875
- date: results.metadatas?.[i]?.created_at || new Date().toISOString(),
876
- project: results.metadatas?.[i]?.project || project || 'unknown',
877
- original: results.metadatas?.[i]?.original || '',
878
- correction: results.metadatas?.[i]?.correction || results.documents?.[i] || '',
879
- reasoning: results.metadatas?.[i]?.reasoning || '',
880
- context: results.metadatas?.[i]?.context || '',
881
- confidence: results.metadatas?.[i]?.confidence || 0
882
- }))
883
- }
884
- return []
885
- } catch (error) {
886
- this.logger.warn({ error }, 'ChromaDB fetchAllCorrections failed, falling back to SQLite')
887
- }
888
- }
889
- return this.store.getAllCorrectionsWithContent(project)
890
- }
891
-
892
- /**
893
- * Search patterns by query — routes to FTS5, ChromaDB, or legacy SQLite
894
- */
895
- async searchPatterns(
896
- query: string,
897
- options?: {
898
- project?: string
899
- pattern_type?: 'solution' | 'anti-pattern' | 'best-practice' | 'common-issue'
900
- limit?: number
901
- minSimilarity?: number
902
- }
903
- ): Promise<any[]> {
904
- // Phase 26: Try FTS5 first
905
- if (this._fts5 && query) {
906
- const results = this._fts5.searchWithConfidence(query, options?.project, options?.limit || 10)
907
- const patterns = results.filter(r => r.category === 'pattern')
908
- if (patterns.length > 0) {
909
- return patterns.map(r => ({
910
- id: r.id,
911
- content: r.content,
912
- metadata: {
913
- project: r.project,
914
- pattern_type: '',
915
- description: r.content,
916
- confidence: r.confidence,
917
- context: r.context || '',
918
- created_at: r.created_at
919
- },
920
- similarity: r.score
921
- }))
922
- }
923
- }
924
-
925
- if (this.useChromaDB) {
926
- return this.chroma.store.searchPatterns(query, options)
927
- }
928
- return this.store.searchPatterns(query, options)
929
- }
930
-
931
- /**
932
- * Search corrections by query routes to FTS5, ChromaDB, or legacy SQLite
933
- */
934
- async searchCorrections(
935
- query: string,
936
- options?: {
937
- project?: string
938
- limit?: number
939
- minSimilarity?: number
940
- }
941
- ): Promise<any[]> {
942
- // Phase 26: Try FTS5 first
943
- if (this._fts5 && query) {
944
- const results = this._fts5.searchWithConfidence(query, options?.project, options?.limit || 10)
945
- const corrections = results.filter(r => r.category === 'correction')
946
- if (corrections.length > 0) {
947
- return corrections.map(r => ({
948
- id: r.id,
949
- content: r.content,
950
- metadata: {
951
- project: r.project,
952
- reasoning: r.reasoning || '',
953
- context: r.context || '',
954
- confidence: r.confidence,
955
- created_at: r.created_at
956
- },
957
- similarity: r.score
958
- }))
959
- }
960
- }
961
-
962
- if (this.useChromaDB) {
963
- return this.chroma.store.searchCorrections(query, options)
964
- }
965
- return this.store.searchCorrections(query, options)
966
- }
967
-
968
- /**
969
- * Delete a decision by ID — removes from FTS5 + embeddings + ChromaDB/SQLite
970
- */
971
- async deleteDecision(id: string): Promise<void> {
972
- // Phase 26: Delete from FTS5 and embeddings
973
- if (this._fts5) {
974
- try {
975
- this._fts5.delete(id)
976
- this._fts5.deleteEmbedding(id)
977
- } catch (error) {
978
- this.logger.warn({ error, id }, 'FTS5 delete failed')
979
- }
980
- }
981
-
982
- if (this.useChromaDB) {
983
- await this.chroma.store.deleteDecision(id)
984
- } else {
985
- this.store.deleteMemory(id)
986
- }
987
-
988
- // Notify listeners (e.g., knowledge graph builder) about deletion
989
- for (const cb of this.onDecisionDeletedCallbacks) {
990
- try {
991
- cb(id)
992
- } catch {}
993
- }
994
- }
995
-
996
- /**
997
- * Update a decision by deleting the old one and storing a new version.
998
- * Phase 20: Ensures both ChromaDB and knowledge graph are atomically updated.
999
- */
1000
- async updateDecision(
1001
- oldId: string,
1002
- project: string,
1003
- context: string,
1004
- decision: string,
1005
- reasoning: string,
1006
- options?: { alternatives?: string; tags?: string[] }
1007
- ): Promise<string> {
1008
- // BUG-001: True in-place update using FTS5 (preserves original ID)
1009
- if (this._fts5) {
1010
- try {
1011
- this._fts5.update(oldId, {
1012
- content: decision,
1013
- reasoning,
1014
- context,
1015
- tags: options?.tags
1016
- })
1017
- this.logger.debug({ oldId }, 'Decision updated in-place via FTS5')
1018
-
1019
- // Also update ChromaDB if available (best-effort)
1020
- if (this.useChromaDB) {
1021
- try {
1022
- await this.chroma.store.deleteDecision(oldId)
1023
- await this.chroma.store.storeDecision({
1024
- project,
1025
- context,
1026
- decision,
1027
- reasoning,
1028
- alternatives: options?.alternatives,
1029
- tags: options?.tags
1030
- })
1031
- } catch {
1032
- // ChromaDB sync failed, FTS5 is source of truth
1033
- }
1034
- }
1035
-
1036
- return oldId // SAME ID preserved
1037
- } catch (error) {
1038
- this.logger.warn({ error, oldId }, 'FTS5 in-place update failed, falling back to delete+store')
1039
- }
1040
- }
1041
-
1042
- // Fallback: delete + store (legacy behavior for non-FTS5 backends)
1043
- try {
1044
- await this.deleteDecision(oldId)
1045
- this.logger.debug({ oldId }, 'Old decision deleted for update')
1046
- } catch (error) {
1047
- this.logger.warn({ error, oldId }, 'Failed to delete old decision during update, storing new version anyway')
1048
- }
1049
- const newId = await this.rememberDecision(project, context, decision, reasoning, options)
1050
- this.logger.debug({ oldId, newId }, 'Decision updated: old deleted, new stored')
1051
- return newId
1052
- }
1053
- }
1054
-
1055
- /**
1056
- * Create a memory manager instance
1057
- */
1058
- export function createMemoryManager(dbPath: string, logger: Logger): MemoryManager {
1059
- return new MemoryManager(dbPath, logger)
1060
- }
1
+ /**
2
+ * Memory System - Main Module
3
+ * Phase 3: Memory and Embedding System
4
+ *
5
+ * Unified memory system manager that combines all components
6
+ */
7
+
8
+ import { randomUUID } from 'crypto'
9
+ import type { Logger } from 'pino'
10
+ import { MemoryDatabase } from './database'
11
+ import { EmbeddingService } from './embeddings'
12
+ import { MemoryStore } from './store'
13
+ import { SemanticSearch } from './search'
14
+ import { MemoryContextBuilder } from './context-builder'
15
+ import type { MemorySystemStats } from './types'
16
+ import { ChromaManager, DEFAULT_CHROMA_CONFIG, getChromaConfigFromEnv, ChromaMigration, type MigrationOptions } from './chroma'
17
+ import { FTS5Search } from './fts5-search'
18
+ import { addFTS5Tables } from './migrations/add-fts5'
19
+ import { DualWriteManager } from './dual-write'
20
+
21
+ // Re-export all types and classes for external use
22
+ export * from './types'
23
+ export { MemoryDatabase } from './database'
24
+ export { EmbeddingService } from './embeddings'
25
+ export { MemoryStore } from './store'
26
+ export { SemanticSearch } from './search'
27
+ export { MemoryContextBuilder, type ContextOptions } from './context-builder'
28
+ export {
29
+ embeddingToBuffer,
30
+ bufferToEmbedding,
31
+ normalizeEmbedding,
32
+ euclideanDistance,
33
+ cosineSimilarity,
34
+ dotProduct,
35
+ magnitude,
36
+ averageEmbeddings,
37
+ topKSimilar
38
+ } from './embedding-utils'
39
+
40
+ // Phase 12: Advanced Memory Features
41
+ export { PatternRecognizer, type Pattern } from './patterns'
42
+ export { LearningSystem, type Correction, type Preference, type LearningInsights } from './learning'
43
+ export { KnowledgeExtractor, type ExtractedKnowledge, type ExtractionResult } from './knowledge-extractor'
44
+
45
+ // Phase 26: FTS5 Search
46
+ export { FTS5Search } from './fts5-search'
47
+ export type { ObservationCategory, NewObservation, ObservationResult, ScoredResult } from './fts5-search'
48
+
49
+ /**
50
+ * Unified memory system manager
51
+ * Combines database, embeddings, store, search, and context building
52
+ */
53
+ export class MemoryManager {
54
+ readonly database: MemoryDatabase
55
+ readonly embeddings: EmbeddingService
56
+ readonly contextBuilder: MemoryContextBuilder
57
+ readonly chroma: ChromaManager
58
+
59
+ // Phase 26: FTS5 search (always available, no external deps)
60
+ private _fts5: FTS5Search | null = null
61
+
62
+ // Store and search are initialized after database is ready
63
+ private _store: MemoryStore | null = null
64
+ private _search: SemanticSearch | null = null
65
+
66
+ private logger: Logger
67
+ private initialized: boolean = false
68
+ private useChromaDB: boolean = true
69
+ private onDecisionStoredCallbacks: ((input: Record<string, unknown>) => void)[] = []
70
+ private onDecisionDeletedCallbacks: ((id: string) => void)[] = []
71
+
72
+ // Task 3.5: Extracted dual-write logic
73
+ private dualWrite: DualWriteManager
74
+
75
+ constructor(
76
+ dbPath: string,
77
+ logger: Logger,
78
+ useChromaDB: boolean = true,
79
+ chromaConfig?: Record<string, unknown>,
80
+ customEmbeddings?: EmbeddingService
81
+ ) {
82
+ this.logger = logger.child({ component: 'memory-manager' })
83
+ this.useChromaDB = useChromaDB
84
+
85
+ this.database = new MemoryDatabase(dbPath, logger)
86
+ this.embeddings = customEmbeddings || new EmbeddingService(logger)
87
+ this.contextBuilder = new MemoryContextBuilder(logger)
88
+
89
+ const envConfig = getChromaConfigFromEnv()
90
+ const config = { ...DEFAULT_CHROMA_CONFIG, ...envConfig, ...chromaConfig }
91
+ this.chroma = new ChromaManager(logger, config)
92
+
93
+ this.dualWrite = new DualWriteManager(
94
+ this.logger,
95
+ () => this._fts5,
96
+ () => ({ manager: this.chroma, enabled: this.useChromaDB }),
97
+ () => this.embeddings
98
+ )
99
+ }
100
+
101
+ /**
102
+ * Initialize memory system
103
+ * Must be called before using store or search
104
+ */
105
+ async initialize(): Promise<void> {
106
+ if (this.initialized) {
107
+ this.logger.warn('Memory system already initialized')
108
+ return
109
+ }
110
+
111
+ try {
112
+ this.logger.info('Initializing memory system...')
113
+
114
+ await this.database.initialize()
115
+
116
+ await this.embeddings.initialize()
117
+
118
+ const db = this.database.getDb()
119
+ this._store = new MemoryStore(db, this.embeddings, this.logger)
120
+ this._search = new SemanticSearch(db, this.embeddings, this.logger)
121
+
122
+ // Phase 26: Always initialize FTS5 (just SQLite, no external deps)
123
+ try {
124
+ addFTS5Tables(db)
125
+ this._fts5 = new FTS5Search(db, this.logger)
126
+ this.logger.info('FTS5 search initialized')
127
+
128
+ // Phase 26b: Backfill embeddings for existing observations (async, non-blocking)
129
+ if (this.embeddings.isReady()) {
130
+ this.backfillEmbeddings().then(count => {
131
+ if (count > 0) {
132
+ this.logger.info({ count }, 'Backfilled embeddings for existing observations')
133
+ }
134
+ }).catch((error) => {
135
+ this.logger.warn({ error }, 'Background embedding backfill failed')
136
+ })
137
+ }
138
+ } catch (error) {
139
+ this.logger.warn({ error }, 'Failed to initialize FTS5, continuing without it')
140
+ }
141
+
142
+ if (this.useChromaDB) {
143
+ try {
144
+ await this.chroma.initialize()
145
+ this.logger.info('ChromaDB backend initialized successfully')
146
+ } catch (error) {
147
+ this.logger.warn({ error }, 'Failed to initialize ChromaDB, falling back to SQLite backend')
148
+ this.useChromaDB = false
149
+ }
150
+ }
151
+
152
+ this.initialized = true
153
+ this.logger.info('Memory system initialized successfully')
154
+ } catch (error) {
155
+ this.logger.error({ error }, 'Failed to initialize memory system')
156
+ throw error
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Get the memory store (throws if not initialized)
162
+ */
163
+ get store(): MemoryStore {
164
+ if (!this._store) {
165
+ throw new Error('Memory system not initialized. Call initialize() first.')
166
+ }
167
+ return this._store
168
+ }
169
+
170
+ /**
171
+ * Get the semantic search engine (throws if not initialized)
172
+ */
173
+ get search(): SemanticSearch {
174
+ if (!this._search) {
175
+ throw new Error('Memory system not initialized. Call initialize() first.')
176
+ }
177
+ return this._search
178
+ }
179
+
180
+ /**
181
+ * Get the FTS5 search engine (null if not initialized)
182
+ */
183
+ get fts5(): FTS5Search | null {
184
+ return this._fts5
185
+ }
186
+
187
+ /**
188
+ * Check if memory system is initialized
189
+ */
190
+ isInitialized(): boolean {
191
+ return this.initialized
192
+ }
193
+
194
+ /**
195
+ * Backfill embeddings for existing observations that don't have them.
196
+ * Call this after initialization to migrate existing data.
197
+ */
198
+ async backfillEmbeddings(batchSize: number = 50): Promise<number> {
199
+ if (!this._fts5 || !this.embeddings.isReady()) return 0
200
+ return this._fts5.backfillEmbeddings(this.embeddings, batchSize)
201
+ }
202
+
203
+ close(): void {
204
+ if (this.useChromaDB) {
205
+ this.chroma.close()
206
+ }
207
+ this.database.close()
208
+ this._store = null
209
+ this._search = null
210
+ this.initialized = false
211
+ this.logger.info('Memory system closed')
212
+ }
213
+
214
+ /**
215
+ * Get system statistics
216
+ */
217
+ getStats(): MemorySystemStats {
218
+ if (!this.initialized) {
219
+ throw new Error('Memory system not initialized')
220
+ }
221
+
222
+ return {
223
+ database: this.database.getStats(),
224
+ embeddings: this.embeddings.getCacheStats()
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Health check
230
+ */
231
+ async healthCheck(): Promise<boolean> {
232
+ if (!this.initialized) {
233
+ return false
234
+ }
235
+ return this.database.healthCheck()
236
+ }
237
+
238
+ /**
239
+ * Check if ChromaDB backend is enabled and connected
240
+ */
241
+ isChromaDBEnabled(): boolean {
242
+ return this.useChromaDB
243
+ }
244
+
245
+ /**
246
+ * Add a listener that fires when a decision is stored (from any backend)
247
+ */
248
+ addDecisionStoredListener(callback: (input: Record<string, unknown>) => void): () => void {
249
+ this.onDecisionStoredCallbacks.push(callback)
250
+ let chromaUnsub: (() => void) | undefined
251
+ if (this.useChromaDB) {
252
+ chromaUnsub = this.chroma.store.addDecisionStoredListener(callback)
253
+ }
254
+ return () => {
255
+ const idx = this.onDecisionStoredCallbacks.indexOf(callback)
256
+ if (idx >= 0) this.onDecisionStoredCallbacks.splice(idx, 1)
257
+ chromaUnsub?.()
258
+ }
259
+ }
260
+
261
+ /**
262
+ * Add a listener that fires when a decision is deleted
263
+ */
264
+ addDecisionDeletedListener(callback: (id: string) => void): () => void {
265
+ this.onDecisionDeletedCallbacks.push(callback)
266
+ return () => {
267
+ const idx = this.onDecisionDeletedCallbacks.indexOf(callback)
268
+ if (idx >= 0) this.onDecisionDeletedCallbacks.splice(idx, 1)
269
+ }
270
+ }
271
+
272
+ async rememberDecision(
273
+ project: string,
274
+ context: string,
275
+ decision: string,
276
+ reasoning: string,
277
+ options?: { alternatives?: string; tags?: string[] }
278
+ ): Promise<string> {
279
+ const sharedId = randomUUID()
280
+
281
+ // rememberDecision has a special duplicate check before the standard dual-write
282
+ if (this._fts5) {
283
+ const dup = this._fts5.searchForDuplicates(decision, project)
284
+ if (dup) {
285
+ this.logger.info({ existingId: dup.id, score: dup.score }, 'FTS5 skipping duplicate decision')
286
+ return dup.id
287
+ }
288
+ }
289
+
290
+ const id = await this.dualWrite.store({
291
+ sharedId,
292
+ fts5Fn: (fts5, id) => fts5.store({
293
+ project,
294
+ category: 'decision',
295
+ content: decision,
296
+ reasoning,
297
+ context,
298
+ tags: options?.tags
299
+ }, id),
300
+ chromaFn: async (chroma, id) => chroma.store.storeDecision({
301
+ id,
302
+ project,
303
+ context,
304
+ decision,
305
+ reasoning,
306
+ alternatives: options?.alternatives,
307
+ tags: options?.tags
308
+ }),
309
+ sqliteFn: () => this.store.storeDecision({
310
+ project,
311
+ context,
312
+ decision,
313
+ reasoning,
314
+ alternatives: options?.alternatives,
315
+ tags: options?.tags
316
+ }),
317
+ embeddingText: [decision, reasoning, context].filter(Boolean).join(' ')
318
+ })
319
+
320
+ // Notify listeners (e.g., knowledge graph builder)
321
+ for (const cb of this.onDecisionStoredCallbacks) {
322
+ try {
323
+ cb({ project, context, decision, reasoning, alternatives: options?.alternatives, tags: options?.tags, id })
324
+ } catch {}
325
+ }
326
+ return id
327
+ }
328
+
329
+ /**
330
+ * Get raw search results - uses FTS5 as primary, embeddings as semantic fallback
331
+ * Use this for internal operations that need raw results
332
+ */
333
+ async searchRaw(
334
+ query: string,
335
+ options?: { project?: string; limit?: number; minSimilarity?: number }
336
+ ): Promise<Record<string, unknown>[]> {
337
+ const limit = options?.limit || 5
338
+ const LOW_CONFIDENCE_THRESHOLD = 0.25
339
+
340
+ // Phase 26: Try FTS5 first (always available)
341
+ if (this._fts5) {
342
+ const ftsResults = this._fts5.searchWithConfidence(query, options?.project, limit)
343
+
344
+ // Check if FTS5 returned good results
345
+ const hasGoodResults = ftsResults.length > 0 &&
346
+ ftsResults.some(r => r.score >= LOW_CONFIDENCE_THRESHOLD)
347
+
348
+ if (hasGoodResults) {
349
+ return this.transformFtsResults(ftsResults)
350
+ }
351
+
352
+ // Phase 26b: FTS5 returned nothing or low-confidence — try semantic search
353
+ if (this.embeddings.isReady() && this._fts5.hasEmbeddings()) {
354
+ try {
355
+ const queryEmbedding = await this.embeddings.generateEmbedding(query)
356
+ const semanticResults = await this._fts5.semanticSearch(
357
+ queryEmbedding,
358
+ options?.project,
359
+ limit,
360
+ options?.minSimilarity || 0.3
361
+ )
362
+
363
+ if (semanticResults.length > 0) {
364
+ this.logger.debug(
365
+ { query, ftsCount: ftsResults.length, semanticCount: semanticResults.length },
366
+ 'Semantic search found results where FTS5 did not'
367
+ )
368
+
369
+ // If we had some low-confidence FTS5 results, merge with semantic results
370
+ if (ftsResults.length > 0) {
371
+ return this.mergeSearchResults(ftsResults, semanticResults, limit)
372
+ }
373
+
374
+ return this.transformFtsResults(semanticResults)
375
+ }
376
+ } catch (error) {
377
+ this.logger.warn({ error }, 'Semantic search fallback failed')
378
+ }
379
+ }
380
+
381
+ // Return whatever FTS5 found (even if low confidence)
382
+ if (ftsResults.length > 0) {
383
+ return this.transformFtsResults(ftsResults)
384
+ }
385
+ }
386
+
387
+ // Fallback: Try ChromaDB if available
388
+ if (this.useChromaDB) {
389
+ const chromaResults = await this.chroma.search.searchDecisions(query, {
390
+ project: options?.project,
391
+ limit,
392
+ minSimilarity: options?.minSimilarity || 0.3
393
+ })
394
+ return chromaResults.map(r => {
395
+ const memoryContent = typeof r.content === 'string' ? r.content : JSON.stringify(r.content)
396
+ const decisionObj = r.metadata.decision ? {
397
+ id: r.id,
398
+ project: r.metadata.project || options?.project || 'unknown',
399
+ context: r.metadata.context || '',
400
+ decision: r.metadata.decision || memoryContent,
401
+ reasoning: r.metadata.reasoning || '',
402
+ alternatives: r.metadata.alternatives_considered || '',
403
+ tags: r.metadata.tags || [],
404
+ outcome: r.metadata.outcome,
405
+ createdAt: r.metadata.created_at ? new Date(r.metadata.created_at) : new Date()
406
+ } : undefined
407
+
408
+ return {
409
+ id: r.id,
410
+ content: decisionObj ? decisionObj.decision : memoryContent,
411
+ memory: {
412
+ id: r.id,
413
+ project: r.metadata.project || options?.project || 'unknown',
414
+ content: memoryContent,
415
+ createdAt: r.metadata.created_at ? new Date(r.metadata.created_at) : new Date(),
416
+ metadata: r.metadata
417
+ },
418
+ similarity: r.similarity,
419
+ decision: decisionObj,
420
+ metadata: r.metadata
421
+ }
422
+ })
423
+ }
424
+
425
+ // Final fallback: legacy SQLite search
426
+ return await this.search.search(query, {
427
+ project: options?.project,
428
+ limit,
429
+ minSimilarity: options?.minSimilarity || 0.3
430
+ })
431
+ }
432
+
433
+ /**
434
+ * Transform FTS5/semantic ScoredResults to the expected MemorySearchResult structure
435
+ */
436
+ private transformFtsResults(results: import('./fts5-search').ScoredResult[]): Record<string, unknown>[] {
437
+ return results.map(r => ({
438
+ id: r.id,
439
+ content: r.content,
440
+ memory: {
441
+ id: r.id,
442
+ project: r.project,
443
+ content: r.content,
444
+ createdAt: new Date(r.created_at),
445
+ metadata: {
446
+ project: r.project,
447
+ category: r.category,
448
+ context: r.context || '',
449
+ reasoning: r.reasoning || '',
450
+ tags: r.tags,
451
+ created_at: r.created_at
452
+ }
453
+ },
454
+ similarity: r.score,
455
+ decision: r.category === 'decision' ? {
456
+ id: r.id,
457
+ project: r.project,
458
+ context: r.context || '',
459
+ decision: r.content,
460
+ reasoning: r.reasoning || '',
461
+ alternatives: '',
462
+ tags: r.tags,
463
+ createdAt: new Date(r.created_at)
464
+ } : undefined,
465
+ metadata: {
466
+ project: r.project,
467
+ category: r.category,
468
+ context: r.context || '',
469
+ reasoning: r.reasoning || '',
470
+ tags: r.tags,
471
+ created_at: r.created_at
472
+ }
473
+ }))
474
+ }
475
+
476
+ /**
477
+ * Merge FTS5 and semantic search results, deduplicating by ID.
478
+ * Semantic results get a small boost since they matched conceptually.
479
+ */
480
+ private mergeSearchResults(
481
+ ftsResults: import('./fts5-search').ScoredResult[],
482
+ semanticResults: import('./fts5-search').ScoredResult[],
483
+ limit: number
484
+ ): Record<string, unknown>[] {
485
+ const seen = new Set<string>()
486
+ const merged: import('./fts5-search').ScoredResult[] = []
487
+
488
+ // Add semantic results first (they matched conceptually when FTS5 didn't)
489
+ for (const r of semanticResults) {
490
+ if (!seen.has(r.id)) {
491
+ seen.add(r.id)
492
+ merged.push(r)
493
+ }
494
+ }
495
+
496
+ // Then add FTS5 results that aren't already included
497
+ for (const r of ftsResults) {
498
+ if (!seen.has(r.id)) {
499
+ seen.add(r.id)
500
+ merged.push(r)
501
+ }
502
+ }
503
+
504
+ // Sort by score descending and limit
505
+ merged.sort((a, b) => b.score - a.score)
506
+ return this.transformFtsResults(merged.slice(0, limit))
507
+ }
508
+
509
+ async recallSimilar(
510
+ query: string,
511
+ options?: { project?: string; limit?: number; minSimilarity?: number }
512
+ ): Promise<string> {
513
+ const results = await this.searchRaw(query, options)
514
+ return this.contextBuilder.buildDecisionContext(results)
515
+ }
516
+
517
+ async getRecommendations(
518
+ currentContext: string,
519
+ project: string,
520
+ limit: number = 3
521
+ ): Promise<string> {
522
+ const results = await this.search.getRecommendations(currentContext, project, limit)
523
+ return this.contextBuilder.buildRecommendationContext(results)
524
+ }
525
+
526
+ async migrateToChromaDB(options: MigrationOptions = {}): Promise<unknown> {
527
+ if (!this.useChromaDB) {
528
+ throw new Error('ChromaDB is not enabled')
529
+ }
530
+
531
+ const migration = new ChromaMigration(
532
+ this.logger,
533
+ this.database.getDb(),
534
+ this.chroma.store,
535
+ this.chroma.collections
536
+ )
537
+
538
+ return migration.migrate(options)
539
+ }
540
+
541
+ /**
542
+ * Store a pattern in memory — dual-writes to FTS5 + ChromaDB/SQLite
543
+ */
544
+ async storePattern(input: {
545
+ project: string
546
+ pattern_type: 'solution' | 'anti-pattern' | 'best-practice' | 'common-issue'
547
+ description: string
548
+ example?: string
549
+ confidence: number
550
+ context?: string
551
+ source?: string
552
+ }): Promise<string> {
553
+ const sharedId = randomUUID()
554
+
555
+ return this.dualWrite.store({
556
+ sharedId,
557
+ fts5Fn: (fts5, id) => fts5.store({
558
+ project: input.project,
559
+ category: 'pattern',
560
+ content: input.description,
561
+ context: input.context,
562
+ confidence: input.confidence,
563
+ source: input.source
564
+ }, id),
565
+ chromaFn: async (chroma, id) => chroma.store.storePattern({ ...input, id }),
566
+ sqliteFn: () => this.store.storePattern(input),
567
+ embeddingText: [input.description, input.context].filter(Boolean).join(' ')
568
+ })
569
+ }
570
+
571
+ /**
572
+ * Store a correction/lesson learned — dual-writes to FTS5 + ChromaDB/SQLite
573
+ */
574
+ async storeCorrection(input: {
575
+ project: string
576
+ original: string
577
+ correction: string
578
+ reasoning: string
579
+ context?: string
580
+ confidence: number
581
+ }): Promise<string> {
582
+ const sharedId = randomUUID()
583
+
584
+ return this.dualWrite.store({
585
+ sharedId,
586
+ fts5Fn: (fts5, id) => fts5.store({
587
+ project: input.project,
588
+ category: 'correction',
589
+ content: input.correction,
590
+ reasoning: input.reasoning,
591
+ context: input.context,
592
+ confidence: input.confidence
593
+ }, id),
594
+ chromaFn: async (chroma, id) => chroma.store.storeCorrection({ ...input, id }),
595
+ sqliteFn: () => this.store.storeCorrection(input),
596
+ embeddingText: [input.correction, input.reasoning, input.context].filter(Boolean).join(' ')
597
+ })
598
+ }
599
+
600
+ /**
601
+ * Get patterns for a project — routes to FTS5, ChromaDB, or legacy SQLite
602
+ */
603
+ async getPatterns(
604
+ project?: string,
605
+ options?: {
606
+ pattern_type?: 'solution' | 'anti-pattern' | 'best-practice' | 'common-issue'
607
+ limit?: number
608
+ }
609
+ ): Promise<Record<string, unknown>[]> {
610
+ return this.dualWrite.fetch({
611
+ fts5Fn: (fts5) => fts5.fetchAll(project, 'pattern').map(r => ({
612
+ id: r.id,
613
+ description: r.content,
614
+ metadata: {
615
+ project: r.project,
616
+ pattern_type: r.category,
617
+ confidence: r.confidence,
618
+ context: r.context || '',
619
+ created_at: r.created_at
620
+ }
621
+ })),
622
+ chromaFn: async (chroma) => {
623
+ if (project) {
624
+ return chroma.store.getPatternsByProject(project, options)
625
+ }
626
+ return chroma.store.searchPatterns('', { limit: options?.limit || 10 })
627
+ },
628
+ sqliteFn: () => {
629
+ if (project) {
630
+ return this.store.getPatternsByProject(project, options)
631
+ }
632
+ return this.store.searchPatterns('', { limit: options?.limit || 10 })
633
+ }
634
+ })
635
+ }
636
+
637
+ /**
638
+ * Get corrections for a project — routes to FTS5, ChromaDB, or legacy SQLite
639
+ */
640
+ async getCorrections(
641
+ project?: string,
642
+ options?: { limit?: number }
643
+ ): Promise<Record<string, unknown>[]> {
644
+ return this.dualWrite.fetch({
645
+ fts5Fn: (fts5) => fts5.fetchAll(project, 'correction').map(r => ({
646
+ id: r.id,
647
+ correction: r.content,
648
+ metadata: {
649
+ project: r.project,
650
+ reasoning: r.reasoning || '',
651
+ context: r.context || '',
652
+ confidence: r.confidence,
653
+ created_at: r.created_at
654
+ }
655
+ })),
656
+ chromaFn: async (chroma) => {
657
+ if (project) {
658
+ return chroma.store.getCorrectionsByProject(project, options?.limit || 10)
659
+ }
660
+ return chroma.store.searchCorrections('', { limit: options?.limit || 10 })
661
+ },
662
+ sqliteFn: () => {
663
+ if (project) {
664
+ return this.store.getCorrectionsByProject(project, options?.limit || 10)
665
+ }
666
+ return this.store.searchCorrections('', { limit: options?.limit || 10 })
667
+ }
668
+ })
669
+ }
670
+
671
+ /**
672
+ * Fetch all decisions with content — routes to FTS5, ChromaDB, or legacy SQLite
673
+ * Used by analytical tools that need bulk access to decision data
674
+ */
675
+ async fetchAllDecisions(project?: string): Promise<Record<string, unknown>[]> {
676
+ return this.dualWrite.fetch({
677
+ fts5Fn: (fts5) => fts5.fetchAll(project, 'decision').map(r => ({
678
+ id: r.id,
679
+ content: r.content,
680
+ date: r.created_at,
681
+ project: r.project,
682
+ context: r.context || '',
683
+ decision: r.content,
684
+ reasoning: r.reasoning || '',
685
+ alternatives: '',
686
+ tags: r.tags
687
+ })),
688
+ chromaFn: async (chroma) => {
689
+ const collection = await chroma.collections.getDecisions()
690
+ const results = await collection.get({
691
+ where: project ? { project } : undefined
692
+ })
693
+ if (results && results.ids) {
694
+ return results.ids.map((id: string, i: number) => ({
695
+ id,
696
+ content: results.documents?.[i] || '',
697
+ date: results.metadatas?.[i]?.created_at || new Date().toISOString(),
698
+ project: results.metadatas?.[i]?.project || project || 'unknown',
699
+ context: results.metadatas?.[i]?.context || '',
700
+ decision: results.metadatas?.[i]?.decision || results.documents?.[i] || '',
701
+ reasoning: results.metadatas?.[i]?.reasoning || '',
702
+ alternatives: results.metadatas?.[i]?.alternatives_considered || '',
703
+ tags: results.metadatas?.[i]?.tags || []
704
+ }))
705
+ }
706
+ return []
707
+ },
708
+ sqliteFn: () => this.store.getAllDecisionsWithContent(project)
709
+ })
710
+ }
711
+
712
+ /**
713
+ * Fetch all patterns with content — routes to FTS5, ChromaDB, or legacy SQLite
714
+ */
715
+ async fetchAllPatterns(project?: string): Promise<Record<string, unknown>[]> {
716
+ return this.dualWrite.fetch({
717
+ fts5Fn: (fts5) => fts5.fetchAll(project, 'pattern').map(r => ({
718
+ id: r.id,
719
+ content: r.content,
720
+ date: r.created_at,
721
+ project: r.project,
722
+ pattern_type: '',
723
+ description: r.content,
724
+ example: '',
725
+ confidence: r.confidence,
726
+ context: r.context || ''
727
+ })),
728
+ chromaFn: async (chroma) => {
729
+ const collection = await chroma.collections.getPatterns()
730
+ const results = await collection.get({
731
+ where: project ? { project } : undefined
732
+ })
733
+ if (results && results.ids) {
734
+ return results.ids.map((id: string, i: number) => ({
735
+ id,
736
+ content: results.documents?.[i] || '',
737
+ date: results.metadatas?.[i]?.created_at || new Date().toISOString(),
738
+ project: results.metadatas?.[i]?.project || project || 'unknown',
739
+ pattern_type: results.metadatas?.[i]?.pattern_type || '',
740
+ description: results.metadatas?.[i]?.description || results.documents?.[i] || '',
741
+ example: results.metadatas?.[i]?.example || '',
742
+ confidence: results.metadatas?.[i]?.confidence || 0,
743
+ context: results.metadatas?.[i]?.context || ''
744
+ }))
745
+ }
746
+ return []
747
+ },
748
+ sqliteFn: () => this.store.getAllPatternsWithContent(project)
749
+ })
750
+ }
751
+
752
+ /**
753
+ * Fetch all corrections with content — routes to FTS5, ChromaDB, or legacy SQLite
754
+ */
755
+ async fetchAllCorrections(project?: string): Promise<Record<string, unknown>[]> {
756
+ return this.dualWrite.fetch({
757
+ fts5Fn: (fts5) => fts5.fetchAll(project, 'correction').map(r => ({
758
+ id: r.id,
759
+ content: r.content,
760
+ date: r.created_at,
761
+ project: r.project,
762
+ original: '',
763
+ correction: r.content,
764
+ reasoning: r.reasoning || '',
765
+ context: r.context || '',
766
+ confidence: r.confidence
767
+ })),
768
+ chromaFn: async (chroma) => {
769
+ const collection = await chroma.collections.getCorrections()
770
+ const results = await collection.get({
771
+ where: project ? { project } : undefined
772
+ })
773
+ if (results && results.ids) {
774
+ return results.ids.map((id: string, i: number) => ({
775
+ id,
776
+ content: results.documents?.[i] || '',
777
+ date: results.metadatas?.[i]?.created_at || new Date().toISOString(),
778
+ project: results.metadatas?.[i]?.project || project || 'unknown',
779
+ original: results.metadatas?.[i]?.original || '',
780
+ correction: results.metadatas?.[i]?.correction || results.documents?.[i] || '',
781
+ reasoning: results.metadatas?.[i]?.reasoning || '',
782
+ context: results.metadatas?.[i]?.context || '',
783
+ confidence: results.metadatas?.[i]?.confidence || 0
784
+ }))
785
+ }
786
+ return []
787
+ },
788
+ sqliteFn: () => this.store.getAllCorrectionsWithContent(project)
789
+ })
790
+ }
791
+
792
+ /**
793
+ * Search patterns by query — routes to FTS5, ChromaDB, or legacy SQLite
794
+ */
795
+ async searchPatterns(
796
+ query: string,
797
+ options?: {
798
+ project?: string
799
+ pattern_type?: 'solution' | 'anti-pattern' | 'best-practice' | 'common-issue'
800
+ limit?: number
801
+ minSimilarity?: number
802
+ }
803
+ ): Promise<Record<string, unknown>[]> {
804
+ return this.dualWrite.fetch({
805
+ fts5Fn: query ? (fts5) => {
806
+ const results = fts5.searchWithConfidence(query, options?.project, options?.limit || 10)
807
+ return results.filter(r => r.category === 'pattern').map(r => ({
808
+ id: r.id,
809
+ content: r.content,
810
+ metadata: {
811
+ project: r.project,
812
+ pattern_type: '',
813
+ description: r.content,
814
+ confidence: r.confidence,
815
+ context: r.context || '',
816
+ created_at: r.created_at
817
+ },
818
+ similarity: r.score
819
+ }))
820
+ } : undefined,
821
+ chromaFn: async (chroma) => chroma.store.searchPatterns(query, options),
822
+ sqliteFn: () => this.store.searchPatterns(query, options)
823
+ })
824
+ }
825
+
826
+ /**
827
+ * Search corrections by query — routes to FTS5, ChromaDB, or legacy SQLite
828
+ */
829
+ async searchCorrections(
830
+ query: string,
831
+ options?: {
832
+ project?: string
833
+ limit?: number
834
+ minSimilarity?: number
835
+ }
836
+ ): Promise<Record<string, unknown>[]> {
837
+ return this.dualWrite.fetch({
838
+ fts5Fn: query ? (fts5) => {
839
+ const results = fts5.searchWithConfidence(query, options?.project, options?.limit || 10)
840
+ return results.filter(r => r.category === 'correction').map(r => ({
841
+ id: r.id,
842
+ content: r.content,
843
+ metadata: {
844
+ project: r.project,
845
+ reasoning: r.reasoning || '',
846
+ context: r.context || '',
847
+ confidence: r.confidence,
848
+ created_at: r.created_at
849
+ },
850
+ similarity: r.score
851
+ }))
852
+ } : undefined,
853
+ chromaFn: async (chroma) => chroma.store.searchCorrections(query, options),
854
+ sqliteFn: () => this.store.searchCorrections(query, options)
855
+ })
856
+ }
857
+
858
+ /**
859
+ * Delete a decision by ID — removes from FTS5 + embeddings + ChromaDB/SQLite
860
+ */
861
+ async deleteDecision(id: string): Promise<void> {
862
+ await this.dualWrite.delete(id, {
863
+ fts5Fn: (fts5) => {
864
+ fts5.delete(id)
865
+ fts5.deleteEmbedding(id)
866
+ },
867
+ chromaFn: async (chroma) => chroma.store.deleteDecision(id),
868
+ sqliteFn: () => this.store.deleteMemory(id)
869
+ })
870
+
871
+ // Notify listeners (e.g., knowledge graph builder) about deletion
872
+ for (const cb of this.onDecisionDeletedCallbacks) {
873
+ try {
874
+ cb(id)
875
+ } catch {}
876
+ }
877
+ }
878
+
879
+ /**
880
+ * Update a decision by deleting the old one and storing a new version.
881
+ * Phase 20: Ensures both ChromaDB and knowledge graph are atomically updated.
882
+ */
883
+ async updateDecision(
884
+ oldId: string,
885
+ project: string,
886
+ context: string,
887
+ decision: string,
888
+ reasoning: string,
889
+ options?: { alternatives?: string; tags?: string[] }
890
+ ): Promise<string> {
891
+ // BUG-001: True in-place update using FTS5 (preserves original ID)
892
+ if (this._fts5) {
893
+ try {
894
+ this._fts5.update(oldId, {
895
+ content: decision,
896
+ reasoning,
897
+ context,
898
+ tags: options?.tags
899
+ })
900
+ this.logger.debug({ oldId }, 'Decision updated in-place via FTS5')
901
+
902
+ // Also update ChromaDB if available (best-effort)
903
+ if (this.useChromaDB) {
904
+ try {
905
+ await this.chroma.store.deleteDecision(oldId)
906
+ await this.chroma.store.storeDecision({
907
+ project,
908
+ context,
909
+ decision,
910
+ reasoning,
911
+ alternatives: options?.alternatives,
912
+ tags: options?.tags
913
+ })
914
+ } catch {
915
+ // ChromaDB sync failed, FTS5 is source of truth
916
+ }
917
+ }
918
+
919
+ return oldId // SAME ID preserved
920
+ } catch (error) {
921
+ this.logger.warn({ error, oldId }, 'FTS5 in-place update failed, falling back to delete+store')
922
+ }
923
+ }
924
+
925
+ // Fallback: delete + store (legacy behavior for non-FTS5 backends)
926
+ try {
927
+ await this.deleteDecision(oldId)
928
+ this.logger.debug({ oldId }, 'Old decision deleted for update')
929
+ } catch (error) {
930
+ this.logger.warn({ error, oldId }, 'Failed to delete old decision during update, storing new version anyway')
931
+ }
932
+ const newId = await this.rememberDecision(project, context, decision, reasoning, options)
933
+ this.logger.debug({ oldId, newId }, 'Decision updated: old deleted, new stored')
934
+ return newId
935
+ }
936
+ }
937
+
938
+ /**
939
+ * Create a memory manager instance
940
+ */
941
+ export function createMemoryManager(dbPath: string, logger: Logger): MemoryManager {
942
+ return new MemoryManager(dbPath, logger)
943
+ }