claude-brain 0.14.2 → 0.14.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +191 -191
- package/VERSION +1 -1
- package/assets/CLAUDE-unified.md +11 -11
- package/assets/CLAUDE.md +11 -11
- package/bunfig.toml +8 -8
- package/package.json +80 -80
- package/packs/backend/node.json +173 -173
- package/packs/core/javascript.json +176 -176
- package/packs/core/typescript.json +222 -222
- package/packs/frontend/react.json +254 -254
- package/packs/meta/testing.json +172 -172
- package/src/automation/auto-context.ts +240 -240
- package/src/automation/decision-detector.ts +452 -452
- package/src/automation/index.ts +11 -11
- package/src/automation/phase12-manager.ts +456 -456
- package/src/automation/proactive-recall.ts +373 -373
- package/src/automation/project-detector.ts +310 -310
- package/src/automation/repo-scanner.ts +205 -205
- package/src/cli/auto-setup.ts +82 -82
- package/src/cli/bin.ts +202 -202
- package/src/cli/commands/chroma.ts +573 -573
- package/src/cli/commands/git-hook.ts +189 -189
- package/src/cli/commands/hooks.ts +213 -213
- package/src/cli/commands/init.ts +122 -122
- package/src/cli/commands/install-mcp.ts +92 -92
- package/src/cli/commands/pack.ts +197 -197
- package/src/cli/commands/serve.ts +167 -167
- package/src/cli/commands/start.ts +42 -42
- package/src/cli/commands/uninstall-mcp.ts +41 -41
- package/src/cli/commands/update.ts +121 -121
- package/src/cli/diagnose.ts +4 -4
- package/src/cli/health-check.ts +4 -4
- package/src/cli/migrate-chroma.ts +106 -106
- package/src/cli/setup.ts +4 -4
- package/src/cli/ui/animations.ts +80 -80
- package/src/cli/ui/components.ts +82 -82
- package/src/cli/ui/index.ts +4 -4
- package/src/cli/ui/logo.ts +36 -36
- package/src/cli/ui/theme.ts +55 -55
- package/src/config/defaults.ts +50 -50
- package/src/config/home.ts +55 -55
- package/src/config/index.ts +7 -7
- package/src/config/loader.ts +166 -166
- package/src/config/migration.ts +76 -76
- package/src/config/schema.ts +360 -360
- package/src/config/validator.ts +184 -184
- package/src/config/watcher.ts +86 -86
- package/src/context/assembler.ts +398 -398
- package/src/context/cache-manager.ts +101 -101
- package/src/context/formatter.ts +84 -84
- package/src/context/hierarchy.ts +85 -85
- package/src/context/index.ts +83 -83
- package/src/context/progress-tracker.ts +174 -174
- package/src/context/standards-manager.ts +287 -287
- package/src/context/types.ts +252 -252
- package/src/context/validator.ts +58 -58
- package/src/diagnostics/index.ts +123 -123
- package/src/health/index.ts +229 -229
- package/src/hooks/brain-hook.ts +112 -112
- package/src/hooks/capture.ts +168 -168
- package/src/hooks/deduplicator.ts +72 -72
- package/src/hooks/git-capture.ts +109 -109
- package/src/hooks/git-hook-installer.ts +207 -207
- package/src/hooks/index.ts +20 -20
- package/src/hooks/installer.ts +191 -194
- package/src/hooks/passive-classifier.ts +366 -366
- package/src/hooks/queue.ts +129 -129
- package/src/hooks/session-tracker.ts +275 -275
- package/src/hooks/types.ts +47 -47
- package/src/index.ts +7 -7
- package/src/intelligence/cross-project/affinity.ts +162 -162
- package/src/intelligence/cross-project/generalizer.ts +283 -283
- package/src/intelligence/cross-project/index.ts +13 -13
- package/src/intelligence/cross-project/transfer.ts +201 -201
- package/src/intelligence/index.ts +24 -24
- package/src/intelligence/optimization/index.ts +10 -10
- package/src/intelligence/optimization/precompute.ts +202 -202
- package/src/intelligence/optimization/semantic-cache.ts +207 -207
- package/src/intelligence/prediction/context-anticipator.ts +198 -198
- package/src/intelligence/prediction/decision-predictor.ts +184 -184
- package/src/intelligence/prediction/index.ts +13 -13
- package/src/intelligence/prediction/recommender.ts +268 -268
- package/src/intelligence/reasoning/chain-retrieval.ts +247 -247
- package/src/intelligence/reasoning/counterfactual.ts +248 -248
- package/src/intelligence/reasoning/index.ts +13 -13
- package/src/intelligence/reasoning/synthesizer.ts +169 -169
- package/src/intelligence/temporal/evolution.ts +197 -197
- package/src/intelligence/temporal/index.ts +16 -16
- package/src/intelligence/temporal/query-processor.ts +190 -190
- package/src/intelligence/temporal/timeline.ts +259 -259
- package/src/intelligence/temporal/trends.ts +263 -263
- package/src/knowledge/entity-extractor.ts +416 -416
- package/src/knowledge/graph/builder.ts +185 -185
- package/src/knowledge/graph/linker.ts +201 -201
- package/src/knowledge/graph/memory-graph.ts +359 -359
- package/src/knowledge/graph/schema.ts +99 -99
- package/src/knowledge/graph/search.ts +168 -168
- package/src/knowledge/relationship-extractor.ts +108 -108
- package/src/memory/chroma/client.ts +174 -174
- package/src/memory/chroma/collection-manager.ts +94 -94
- package/src/memory/chroma/config.ts +57 -57
- package/src/memory/chroma/embeddings.ts +153 -153
- package/src/memory/chroma/index.ts +82 -82
- package/src/memory/chroma/migration.ts +270 -270
- package/src/memory/chroma/schemas.ts +69 -69
- package/src/memory/chroma/search.ts +315 -315
- package/src/memory/chroma/store.ts +741 -741
- package/src/memory/consolidation/archiver.ts +164 -164
- package/src/memory/consolidation/merger.ts +186 -186
- package/src/memory/consolidation/scorer.ts +138 -138
- package/src/memory/context-builder.ts +236 -236
- package/src/memory/database.ts +169 -169
- package/src/memory/embedding-utils.ts +156 -156
- package/src/memory/embeddings.ts +226 -226
- package/src/memory/episodic/detector.ts +108 -108
- package/src/memory/episodic/manager.ts +351 -351
- package/src/memory/episodic/summarizer.ts +179 -179
- package/src/memory/episodic/types.ts +52 -52
- package/src/memory/index.ts +582 -582
- package/src/memory/knowledge-extractor.ts +455 -455
- package/src/memory/learning.ts +378 -378
- package/src/memory/patterns.ts +396 -396
- package/src/memory/schema.ts +88 -88
- package/src/memory/search.ts +309 -309
- package/src/memory/store.ts +787 -787
- package/src/memory/types.ts +121 -121
- package/src/orchestrator/coordinator.ts +272 -272
- package/src/orchestrator/decision-logger.ts +228 -228
- package/src/orchestrator/event-emitter.ts +198 -198
- package/src/orchestrator/event-queue.ts +184 -184
- package/src/orchestrator/handlers/base-handler.ts +70 -70
- package/src/orchestrator/handlers/context-handler.ts +73 -73
- package/src/orchestrator/handlers/decision-handler.ts +204 -204
- package/src/orchestrator/handlers/index.ts +10 -10
- package/src/orchestrator/handlers/status-handler.ts +131 -131
- package/src/orchestrator/handlers/task-handler.ts +171 -171
- package/src/orchestrator/index.ts +275 -275
- package/src/orchestrator/task-parser.ts +284 -284
- package/src/orchestrator/types.ts +98 -98
- package/src/packs/index.ts +9 -9
- package/src/packs/loader.ts +134 -134
- package/src/packs/manager.ts +204 -204
- package/src/packs/ranker.ts +78 -78
- package/src/packs/types.ts +81 -81
- package/src/phase12/index.ts +5 -5
- package/src/retrieval/bm25/index.ts +300 -300
- package/src/retrieval/bm25/tokenizer.ts +184 -184
- package/src/retrieval/feedback/adaptive.ts +223 -223
- package/src/retrieval/feedback/index.ts +16 -16
- package/src/retrieval/feedback/metrics.ts +223 -223
- package/src/retrieval/feedback/store.ts +283 -283
- package/src/retrieval/fusion/index.ts +194 -194
- package/src/retrieval/fusion/rrf.ts +163 -163
- package/src/retrieval/index.ts +12 -12
- package/src/retrieval/pipeline.ts +375 -375
- package/src/retrieval/query/expander.ts +198 -198
- package/src/retrieval/query/index.ts +27 -27
- package/src/retrieval/query/intent-classifier.ts +236 -236
- package/src/retrieval/query/temporal-parser.ts +295 -295
- package/src/retrieval/reranker/index.ts +188 -188
- package/src/retrieval/reranker/model.ts +95 -95
- package/src/retrieval/service.ts +125 -125
- package/src/retrieval/types.ts +162 -162
- package/src/routing/entity-extractor.ts +428 -428
- package/src/routing/intent-classifier.ts +436 -436
- package/src/routing/response-filter.ts +258 -254
- package/src/routing/router.ts +1322 -1314
- package/src/routing/search-engine.ts +475 -475
- package/src/routing/types.ts +94 -84
- package/src/scripts/health-check.ts +118 -118
- package/src/scripts/setup.ts +122 -122
- package/src/server/handlers/call-tool.ts +156 -156
- package/src/server/handlers/index.ts +9 -9
- package/src/server/handlers/list-tools.ts +35 -35
- package/src/server/handlers/tools/analyze-decision-evolution.ts +151 -151
- package/src/server/handlers/tools/auto-remember.ts +200 -200
- package/src/server/handlers/tools/brain.ts +85 -85
- package/src/server/handlers/tools/create-project.ts +135 -135
- package/src/server/handlers/tools/detect-trends.ts +144 -144
- package/src/server/handlers/tools/find-cross-project-patterns.ts +168 -168
- package/src/server/handlers/tools/get-activity-log.ts +194 -194
- package/src/server/handlers/tools/get-code-standards.ts +124 -124
- package/src/server/handlers/tools/get-corrections.ts +154 -154
- package/src/server/handlers/tools/get-decision-timeline.ts +172 -172
- package/src/server/handlers/tools/get-episode.ts +103 -103
- package/src/server/handlers/tools/get-patterns.ts +158 -158
- package/src/server/handlers/tools/get-phase12-status.ts +63 -63
- package/src/server/handlers/tools/get-project-context.ts +75 -75
- package/src/server/handlers/tools/get-recommendations.ts +145 -145
- package/src/server/handlers/tools/index.ts +31 -31
- package/src/server/handlers/tools/init-project.ts +757 -757
- package/src/server/handlers/tools/list-episodes.ts +90 -90
- package/src/server/handlers/tools/list-projects.ts +125 -125
- package/src/server/handlers/tools/rate-memory.ts +101 -101
- package/src/server/handlers/tools/recall-similar.ts +87 -87
- package/src/server/handlers/tools/recognize-pattern.ts +126 -126
- package/src/server/handlers/tools/record-correction.ts +125 -125
- package/src/server/handlers/tools/remember-decision.ts +153 -153
- package/src/server/handlers/tools/schemas.ts +253 -253
- package/src/server/handlers/tools/search-knowledge-graph.ts +102 -102
- package/src/server/handlers/tools/smart-context.ts +146 -146
- package/src/server/handlers/tools/update-progress.ts +131 -131
- package/src/server/handlers/tools/what-if-analysis.ts +135 -135
- package/src/server/http-api.ts +693 -693
- package/src/server/index.ts +40 -40
- package/src/server/mcp-server.ts +283 -283
- package/src/server/providers/index.ts +7 -7
- package/src/server/providers/prompts.ts +327 -327
- package/src/server/providers/resources.ts +622 -622
- package/src/server/services.ts +468 -468
- package/src/server/types.ts +39 -39
- package/src/server/utils/error-handler.ts +155 -155
- package/src/server/utils/index.ts +13 -13
- package/src/server/utils/memory-indicator.ts +83 -83
- package/src/server/utils/request-context.ts +122 -122
- package/src/server/utils/response-formatter.ts +129 -124
- package/src/server/utils/validators.ts +210 -210
- package/src/setup/index.ts +48 -48
- package/src/setup/wizard.ts +461 -461
- package/src/tools/index.ts +24 -24
- package/src/tools/registry.ts +115 -115
- package/src/tools/schemas.test.ts +30 -30
- package/src/tools/schemas.ts +617 -617
- package/src/tools/types.ts +412 -412
- package/src/utils/circuit-breaker.ts +130 -130
- package/src/utils/cleanup.ts +34 -34
- package/src/utils/error-handler.ts +132 -132
- package/src/utils/error-messages.ts +60 -60
- package/src/utils/fallback.ts +45 -45
- package/src/utils/index.ts +54 -54
- package/src/utils/logger-utils.ts +80 -80
- package/src/utils/logger.ts +88 -88
- package/src/utils/phase12-helper.ts +56 -56
- package/src/utils/retry.ts +94 -94
- package/src/utils/timing.ts +47 -47
- package/src/utils/transaction.ts +63 -63
- package/src/vault/frontmatter.ts +264 -264
- package/src/vault/index.ts +318 -318
- package/src/vault/paths.ts +106 -106
- package/src/vault/query.ts +422 -422
- package/src/vault/reader.ts +264 -264
- package/src/vault/templates.ts +186 -186
- package/src/vault/types.ts +73 -73
- package/src/vault/watcher.ts +277 -277
- package/src/vault/writer.ts +413 -413
- package/tsconfig.json +30 -30
|
@@ -1,351 +1,351 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Episode Manager
|
|
3
|
-
* Manages episode lifecycle with ChromaDB persistence
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { randomUUID } from 'crypto'
|
|
7
|
-
import type { Logger } from 'pino'
|
|
8
|
-
import type { CollectionManager } from '../chroma/collection-manager'
|
|
9
|
-
import type { EmbeddingProvider } from '../chroma/embeddings'
|
|
10
|
-
import type { Episode, EpisodeMessage, EpisodeMetadata } from './types'
|
|
11
|
-
import { SessionDetector, type DetectorOptions } from './detector'
|
|
12
|
-
import { ExtractiveSummarizer } from './summarizer'
|
|
13
|
-
|
|
14
|
-
export class EpisodeManager {
|
|
15
|
-
private logger: Logger
|
|
16
|
-
private collections: CollectionManager
|
|
17
|
-
private embeddings?: EmbeddingProvider
|
|
18
|
-
private detector: SessionDetector
|
|
19
|
-
private summarizer: ExtractiveSummarizer
|
|
20
|
-
private activeEpisodes: Map<string, Episode> = new Map() // project → episode
|
|
21
|
-
|
|
22
|
-
constructor(
|
|
23
|
-
logger: Logger,
|
|
24
|
-
collections: CollectionManager,
|
|
25
|
-
embeddings?: EmbeddingProvider,
|
|
26
|
-
detectorOptions?: DetectorOptions
|
|
27
|
-
) {
|
|
28
|
-
this.logger = logger.child({ component: 'episode-manager' })
|
|
29
|
-
this.collections = collections
|
|
30
|
-
this.embeddings = embeddings
|
|
31
|
-
this.detector = new SessionDetector(detectorOptions)
|
|
32
|
-
this.summarizer = new ExtractiveSummarizer()
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async startEpisode(project?: string): Promise<Episode> {
|
|
36
|
-
const now = new Date().toISOString()
|
|
37
|
-
const episode: Episode = {
|
|
38
|
-
id: randomUUID(),
|
|
39
|
-
project,
|
|
40
|
-
status: 'active',
|
|
41
|
-
started_at: now,
|
|
42
|
-
messages: [],
|
|
43
|
-
related_decisions: [],
|
|
44
|
-
related_patterns: [],
|
|
45
|
-
related_corrections: [],
|
|
46
|
-
token_count: 0,
|
|
47
|
-
message_count: 0
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const key = project || '__global__'
|
|
51
|
-
this.activeEpisodes.set(key, episode)
|
|
52
|
-
this.logger.info({ episodeId: episode.id, project }, 'Episode started')
|
|
53
|
-
return episode
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async endEpisode(episodeId: string): Promise<Episode | null> {
|
|
57
|
-
// Find the episode
|
|
58
|
-
let episode: Episode | null = null
|
|
59
|
-
let key: string | null = null
|
|
60
|
-
|
|
61
|
-
for (const [k, ep] of this.activeEpisodes) {
|
|
62
|
-
if (ep.id === episodeId) {
|
|
63
|
-
episode = ep
|
|
64
|
-
key = k
|
|
65
|
-
break
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (!episode || !key) {
|
|
70
|
-
this.logger.warn({ episodeId }, 'Episode not found in active episodes')
|
|
71
|
-
return null
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
episode.status = 'completed'
|
|
75
|
-
episode.ended_at = new Date().toISOString()
|
|
76
|
-
|
|
77
|
-
// Generate summary
|
|
78
|
-
if (episode.messages.length > 0) {
|
|
79
|
-
episode.summary = this.summarizer.summarize(episode)
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Persist to ChromaDB
|
|
83
|
-
await this.persistEpisode(episode)
|
|
84
|
-
|
|
85
|
-
this.activeEpisodes.delete(key)
|
|
86
|
-
this.logger.info({ episodeId, messageCount: episode.message_count }, 'Episode ended')
|
|
87
|
-
return episode
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async addMessage(episodeId: string, message: EpisodeMessage): Promise<void> {
|
|
91
|
-
for (const episode of this.activeEpisodes.values()) {
|
|
92
|
-
if (episode.id === episodeId) {
|
|
93
|
-
episode.messages.push(message)
|
|
94
|
-
episode.message_count++
|
|
95
|
-
episode.token_count += message.token_estimate || Math.ceil(message.content.length / 4)
|
|
96
|
-
return
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
this.logger.warn({ episodeId }, 'Episode not found for adding message')
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
async processMessage(
|
|
104
|
-
message: EpisodeMessage,
|
|
105
|
-
project?: string
|
|
106
|
-
): Promise<{ episode: Episode; classification: string }> {
|
|
107
|
-
const key = project || '__global__'
|
|
108
|
-
const activeEpisode = this.activeEpisodes.get(key)
|
|
109
|
-
|
|
110
|
-
const lastActivity = activeEpisode
|
|
111
|
-
? (activeEpisode.messages.length > 0
|
|
112
|
-
? activeEpisode.messages[activeEpisode.messages.length - 1].timestamp
|
|
113
|
-
: activeEpisode.started_at)
|
|
114
|
-
: undefined
|
|
115
|
-
|
|
116
|
-
const recentMessages = activeEpisode
|
|
117
|
-
? activeEpisode.messages.slice(-5)
|
|
118
|
-
: undefined
|
|
119
|
-
|
|
120
|
-
const classification = this.detector.classifyMessage(message, lastActivity, recentMessages)
|
|
121
|
-
|
|
122
|
-
if (classification === 'new_session') {
|
|
123
|
-
// End current episode if exists
|
|
124
|
-
if (activeEpisode) {
|
|
125
|
-
await this.endEpisode(activeEpisode.id)
|
|
126
|
-
}
|
|
127
|
-
// Start new episode
|
|
128
|
-
const episode = await this.startEpisode(project)
|
|
129
|
-
await this.addMessage(episode.id, message)
|
|
130
|
-
return { episode, classification }
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// continuation or topic_shift: add to current episode
|
|
134
|
-
if (activeEpisode) {
|
|
135
|
-
await this.addMessage(activeEpisode.id, message)
|
|
136
|
-
return { episode: activeEpisode, classification }
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// No active episode, start new one
|
|
140
|
-
const episode = await this.startEpisode(project)
|
|
141
|
-
await this.addMessage(episode.id, message)
|
|
142
|
-
return { episode, classification: 'new_session' }
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
getActiveEpisode(project?: string): Episode | undefined {
|
|
146
|
-
const key = project || '__global__'
|
|
147
|
-
return this.activeEpisodes.get(key)
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
async getRecentEpisodes(project?: string, limit: number = 10): Promise<Episode[]> {
|
|
151
|
-
try {
|
|
152
|
-
const collection = await this.collections.getEpisodes()
|
|
153
|
-
|
|
154
|
-
const where: any = {}
|
|
155
|
-
if (project) {
|
|
156
|
-
where.project = { $eq: project }
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const results = await collection.get({
|
|
160
|
-
where: Object.keys(where).length > 0 ? where : undefined,
|
|
161
|
-
include: ['documents', 'metadatas'],
|
|
162
|
-
limit
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
return results.ids.map((id, i) => {
|
|
166
|
-
const metadata = results.metadatas?.[i] as Record<string, any>
|
|
167
|
-
return this.metadataToEpisode(id, metadata, results.documents?.[i] as string)
|
|
168
|
-
}).sort((a, b) => b.started_at.localeCompare(a.started_at))
|
|
169
|
-
|
|
170
|
-
} catch (error) {
|
|
171
|
-
this.logger.error({ error }, 'Failed to get recent episodes')
|
|
172
|
-
return []
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
async searchEpisodes(query: string, project?: string, limit: number = 5): Promise<Episode[]> {
|
|
177
|
-
try {
|
|
178
|
-
const collection = await this.collections.getEpisodes()
|
|
179
|
-
|
|
180
|
-
const where: any = project ? { project: { $eq: project } } : undefined
|
|
181
|
-
|
|
182
|
-
let results: any
|
|
183
|
-
|
|
184
|
-
if (this.embeddings) {
|
|
185
|
-
const embedding = await this.embeddings.generate(query)
|
|
186
|
-
results = await collection.query({
|
|
187
|
-
queryEmbeddings: [embedding],
|
|
188
|
-
nResults: limit,
|
|
189
|
-
where,
|
|
190
|
-
include: ['documents', 'metadatas', 'distances']
|
|
191
|
-
})
|
|
192
|
-
} else {
|
|
193
|
-
results = await collection.query({
|
|
194
|
-
queryTexts: [query],
|
|
195
|
-
nResults: limit,
|
|
196
|
-
where,
|
|
197
|
-
include: ['documents', 'metadatas', 'distances']
|
|
198
|
-
})
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
if (!results.ids || results.ids[0]?.length === 0) return []
|
|
202
|
-
|
|
203
|
-
return results.ids[0].map((id: string, i: number) => {
|
|
204
|
-
const metadata = results.metadatas?.[0]?.[i] as Record<string, any>
|
|
205
|
-
return this.metadataToEpisode(id, metadata, results.documents?.[0]?.[i] as string)
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
} catch (error) {
|
|
209
|
-
this.logger.error({ error, query }, 'Failed to search episodes')
|
|
210
|
-
return []
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
async getEpisode(episodeId: string): Promise<Episode | null> {
|
|
215
|
-
// Check active episodes first
|
|
216
|
-
for (const episode of this.activeEpisodes.values()) {
|
|
217
|
-
if (episode.id === episodeId) return episode
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Check ChromaDB
|
|
221
|
-
try {
|
|
222
|
-
const collection = await this.collections.getEpisodes()
|
|
223
|
-
const result = await collection.get({
|
|
224
|
-
ids: [episodeId],
|
|
225
|
-
include: ['documents', 'metadatas']
|
|
226
|
-
})
|
|
227
|
-
|
|
228
|
-
if (result.ids.length === 0) return null
|
|
229
|
-
|
|
230
|
-
const metadata = result.metadatas?.[0] as Record<string, any>
|
|
231
|
-
return this.metadataToEpisode(episodeId, metadata, result.documents?.[0] as string)
|
|
232
|
-
} catch (error) {
|
|
233
|
-
this.logger.error({ error, episodeId }, 'Failed to get episode')
|
|
234
|
-
return null
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
linkDecision(episodeId: string, decisionId: string): void {
|
|
239
|
-
for (const episode of this.activeEpisodes.values()) {
|
|
240
|
-
if (episode.id === episodeId) {
|
|
241
|
-
if (!episode.related_decisions.includes(decisionId)) {
|
|
242
|
-
episode.related_decisions.push(decisionId)
|
|
243
|
-
}
|
|
244
|
-
return
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
linkPattern(episodeId: string, patternId: string): void {
|
|
250
|
-
for (const episode of this.activeEpisodes.values()) {
|
|
251
|
-
if (episode.id === episodeId) {
|
|
252
|
-
if (!episode.related_patterns.includes(patternId)) {
|
|
253
|
-
episode.related_patterns.push(patternId)
|
|
254
|
-
}
|
|
255
|
-
return
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
linkCorrection(episodeId: string, correctionId: string): void {
|
|
261
|
-
for (const episode of this.activeEpisodes.values()) {
|
|
262
|
-
if (episode.id === episodeId) {
|
|
263
|
-
if (!episode.related_corrections.includes(correctionId)) {
|
|
264
|
-
episode.related_corrections.push(correctionId)
|
|
265
|
-
}
|
|
266
|
-
return
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* Remove a decision reference from all active episodes.
|
|
273
|
-
* Called when a decision is deleted to prevent stale references.
|
|
274
|
-
*/
|
|
275
|
-
unlinkDecision(decisionId: string): void {
|
|
276
|
-
for (const episode of this.activeEpisodes.values()) {
|
|
277
|
-
const idx = episode.related_decisions.indexOf(decisionId)
|
|
278
|
-
if (idx !== -1) {
|
|
279
|
-
episode.related_decisions.splice(idx, 1)
|
|
280
|
-
this.logger.debug(
|
|
281
|
-
{ episodeId: episode.id, decisionId },
|
|
282
|
-
'Decision unlinked from episode'
|
|
283
|
-
)
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
private async persistEpisode(episode: Episode): Promise<void> {
|
|
289
|
-
try {
|
|
290
|
-
const collection = await this.collections.getEpisodes()
|
|
291
|
-
|
|
292
|
-
const summaryText = episode.summary
|
|
293
|
-
? `${episode.summary.brief}\n\nTopics: ${episode.summary.key_topics.join(', ')}\n\n${episode.summary.detailed}`
|
|
294
|
-
: `Episode with ${episode.message_count} messages`
|
|
295
|
-
|
|
296
|
-
const metadata: Record<string, any> = {
|
|
297
|
-
episode_id: episode.id,
|
|
298
|
-
project: episode.project || '',
|
|
299
|
-
status: episode.status,
|
|
300
|
-
started_at: episode.started_at,
|
|
301
|
-
ended_at: episode.ended_at || '',
|
|
302
|
-
message_count: episode.message_count,
|
|
303
|
-
token_count: episode.token_count,
|
|
304
|
-
key_topics: episode.summary?.key_topics.join(',') || '',
|
|
305
|
-
brief_summary: episode.summary?.brief || '',
|
|
306
|
-
related_decisions: episode.related_decisions.join(','),
|
|
307
|
-
related_patterns: episode.related_patterns.join(','),
|
|
308
|
-
related_corrections: episode.related_corrections.join(',')
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const embeddings = this.embeddings
|
|
312
|
-
? [await this.embeddings.generate(summaryText)]
|
|
313
|
-
: undefined
|
|
314
|
-
|
|
315
|
-
await collection.add({
|
|
316
|
-
ids: [episode.id],
|
|
317
|
-
documents: [summaryText],
|
|
318
|
-
metadatas: [metadata],
|
|
319
|
-
...(embeddings ? { embeddings } : {})
|
|
320
|
-
})
|
|
321
|
-
|
|
322
|
-
this.logger.debug({ episodeId: episode.id }, 'Episode persisted to ChromaDB')
|
|
323
|
-
} catch (error) {
|
|
324
|
-
this.logger.error({ error, episodeId: episode.id }, 'Failed to persist episode')
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
private metadataToEpisode(id: string, metadata: Record<string, any>, document?: string): Episode {
|
|
329
|
-
return {
|
|
330
|
-
id,
|
|
331
|
-
project: metadata.project || undefined,
|
|
332
|
-
status: metadata.status || 'completed',
|
|
333
|
-
started_at: metadata.started_at || '',
|
|
334
|
-
ended_at: metadata.ended_at || undefined,
|
|
335
|
-
messages: [],
|
|
336
|
-
summary: metadata.brief_summary ? {
|
|
337
|
-
brief: metadata.brief_summary,
|
|
338
|
-
detailed: document || metadata.brief_summary,
|
|
339
|
-
key_topics: (metadata.key_topics || '').split(',').filter(Boolean),
|
|
340
|
-
decisions_made: [],
|
|
341
|
-
unresolved_questions: [],
|
|
342
|
-
entity_count: 0
|
|
343
|
-
} : undefined,
|
|
344
|
-
related_decisions: (metadata.related_decisions || '').split(',').filter(Boolean),
|
|
345
|
-
related_patterns: (metadata.related_patterns || '').split(',').filter(Boolean),
|
|
346
|
-
related_corrections: (metadata.related_corrections || '').split(',').filter(Boolean),
|
|
347
|
-
token_count: metadata.token_count || 0,
|
|
348
|
-
message_count: metadata.message_count || 0
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Episode Manager
|
|
3
|
+
* Manages episode lifecycle with ChromaDB persistence
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { randomUUID } from 'crypto'
|
|
7
|
+
import type { Logger } from 'pino'
|
|
8
|
+
import type { CollectionManager } from '../chroma/collection-manager'
|
|
9
|
+
import type { EmbeddingProvider } from '../chroma/embeddings'
|
|
10
|
+
import type { Episode, EpisodeMessage, EpisodeMetadata } from './types'
|
|
11
|
+
import { SessionDetector, type DetectorOptions } from './detector'
|
|
12
|
+
import { ExtractiveSummarizer } from './summarizer'
|
|
13
|
+
|
|
14
|
+
export class EpisodeManager {
|
|
15
|
+
private logger: Logger
|
|
16
|
+
private collections: CollectionManager
|
|
17
|
+
private embeddings?: EmbeddingProvider
|
|
18
|
+
private detector: SessionDetector
|
|
19
|
+
private summarizer: ExtractiveSummarizer
|
|
20
|
+
private activeEpisodes: Map<string, Episode> = new Map() // project → episode
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
logger: Logger,
|
|
24
|
+
collections: CollectionManager,
|
|
25
|
+
embeddings?: EmbeddingProvider,
|
|
26
|
+
detectorOptions?: DetectorOptions
|
|
27
|
+
) {
|
|
28
|
+
this.logger = logger.child({ component: 'episode-manager' })
|
|
29
|
+
this.collections = collections
|
|
30
|
+
this.embeddings = embeddings
|
|
31
|
+
this.detector = new SessionDetector(detectorOptions)
|
|
32
|
+
this.summarizer = new ExtractiveSummarizer()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async startEpisode(project?: string): Promise<Episode> {
|
|
36
|
+
const now = new Date().toISOString()
|
|
37
|
+
const episode: Episode = {
|
|
38
|
+
id: randomUUID(),
|
|
39
|
+
project,
|
|
40
|
+
status: 'active',
|
|
41
|
+
started_at: now,
|
|
42
|
+
messages: [],
|
|
43
|
+
related_decisions: [],
|
|
44
|
+
related_patterns: [],
|
|
45
|
+
related_corrections: [],
|
|
46
|
+
token_count: 0,
|
|
47
|
+
message_count: 0
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const key = project || '__global__'
|
|
51
|
+
this.activeEpisodes.set(key, episode)
|
|
52
|
+
this.logger.info({ episodeId: episode.id, project }, 'Episode started')
|
|
53
|
+
return episode
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async endEpisode(episodeId: string): Promise<Episode | null> {
|
|
57
|
+
// Find the episode
|
|
58
|
+
let episode: Episode | null = null
|
|
59
|
+
let key: string | null = null
|
|
60
|
+
|
|
61
|
+
for (const [k, ep] of this.activeEpisodes) {
|
|
62
|
+
if (ep.id === episodeId) {
|
|
63
|
+
episode = ep
|
|
64
|
+
key = k
|
|
65
|
+
break
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!episode || !key) {
|
|
70
|
+
this.logger.warn({ episodeId }, 'Episode not found in active episodes')
|
|
71
|
+
return null
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
episode.status = 'completed'
|
|
75
|
+
episode.ended_at = new Date().toISOString()
|
|
76
|
+
|
|
77
|
+
// Generate summary
|
|
78
|
+
if (episode.messages.length > 0) {
|
|
79
|
+
episode.summary = this.summarizer.summarize(episode)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Persist to ChromaDB
|
|
83
|
+
await this.persistEpisode(episode)
|
|
84
|
+
|
|
85
|
+
this.activeEpisodes.delete(key)
|
|
86
|
+
this.logger.info({ episodeId, messageCount: episode.message_count }, 'Episode ended')
|
|
87
|
+
return episode
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async addMessage(episodeId: string, message: EpisodeMessage): Promise<void> {
|
|
91
|
+
for (const episode of this.activeEpisodes.values()) {
|
|
92
|
+
if (episode.id === episodeId) {
|
|
93
|
+
episode.messages.push(message)
|
|
94
|
+
episode.message_count++
|
|
95
|
+
episode.token_count += message.token_estimate || Math.ceil(message.content.length / 4)
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
this.logger.warn({ episodeId }, 'Episode not found for adding message')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async processMessage(
|
|
104
|
+
message: EpisodeMessage,
|
|
105
|
+
project?: string
|
|
106
|
+
): Promise<{ episode: Episode; classification: string }> {
|
|
107
|
+
const key = project || '__global__'
|
|
108
|
+
const activeEpisode = this.activeEpisodes.get(key)
|
|
109
|
+
|
|
110
|
+
const lastActivity = activeEpisode
|
|
111
|
+
? (activeEpisode.messages.length > 0
|
|
112
|
+
? activeEpisode.messages[activeEpisode.messages.length - 1].timestamp
|
|
113
|
+
: activeEpisode.started_at)
|
|
114
|
+
: undefined
|
|
115
|
+
|
|
116
|
+
const recentMessages = activeEpisode
|
|
117
|
+
? activeEpisode.messages.slice(-5)
|
|
118
|
+
: undefined
|
|
119
|
+
|
|
120
|
+
const classification = this.detector.classifyMessage(message, lastActivity, recentMessages)
|
|
121
|
+
|
|
122
|
+
if (classification === 'new_session') {
|
|
123
|
+
// End current episode if exists
|
|
124
|
+
if (activeEpisode) {
|
|
125
|
+
await this.endEpisode(activeEpisode.id)
|
|
126
|
+
}
|
|
127
|
+
// Start new episode
|
|
128
|
+
const episode = await this.startEpisode(project)
|
|
129
|
+
await this.addMessage(episode.id, message)
|
|
130
|
+
return { episode, classification }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// continuation or topic_shift: add to current episode
|
|
134
|
+
if (activeEpisode) {
|
|
135
|
+
await this.addMessage(activeEpisode.id, message)
|
|
136
|
+
return { episode: activeEpisode, classification }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// No active episode, start new one
|
|
140
|
+
const episode = await this.startEpisode(project)
|
|
141
|
+
await this.addMessage(episode.id, message)
|
|
142
|
+
return { episode, classification: 'new_session' }
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
getActiveEpisode(project?: string): Episode | undefined {
|
|
146
|
+
const key = project || '__global__'
|
|
147
|
+
return this.activeEpisodes.get(key)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async getRecentEpisodes(project?: string, limit: number = 10): Promise<Episode[]> {
|
|
151
|
+
try {
|
|
152
|
+
const collection = await this.collections.getEpisodes()
|
|
153
|
+
|
|
154
|
+
const where: any = {}
|
|
155
|
+
if (project) {
|
|
156
|
+
where.project = { $eq: project }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const results = await collection.get({
|
|
160
|
+
where: Object.keys(where).length > 0 ? where : undefined,
|
|
161
|
+
include: ['documents', 'metadatas'],
|
|
162
|
+
limit
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
return results.ids.map((id, i) => {
|
|
166
|
+
const metadata = results.metadatas?.[i] as Record<string, any>
|
|
167
|
+
return this.metadataToEpisode(id, metadata, results.documents?.[i] as string)
|
|
168
|
+
}).sort((a, b) => b.started_at.localeCompare(a.started_at))
|
|
169
|
+
|
|
170
|
+
} catch (error) {
|
|
171
|
+
this.logger.error({ error }, 'Failed to get recent episodes')
|
|
172
|
+
return []
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async searchEpisodes(query: string, project?: string, limit: number = 5): Promise<Episode[]> {
|
|
177
|
+
try {
|
|
178
|
+
const collection = await this.collections.getEpisodes()
|
|
179
|
+
|
|
180
|
+
const where: any = project ? { project: { $eq: project } } : undefined
|
|
181
|
+
|
|
182
|
+
let results: any
|
|
183
|
+
|
|
184
|
+
if (this.embeddings) {
|
|
185
|
+
const embedding = await this.embeddings.generate(query)
|
|
186
|
+
results = await collection.query({
|
|
187
|
+
queryEmbeddings: [embedding],
|
|
188
|
+
nResults: limit,
|
|
189
|
+
where,
|
|
190
|
+
include: ['documents', 'metadatas', 'distances']
|
|
191
|
+
})
|
|
192
|
+
} else {
|
|
193
|
+
results = await collection.query({
|
|
194
|
+
queryTexts: [query],
|
|
195
|
+
nResults: limit,
|
|
196
|
+
where,
|
|
197
|
+
include: ['documents', 'metadatas', 'distances']
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!results.ids || results.ids[0]?.length === 0) return []
|
|
202
|
+
|
|
203
|
+
return results.ids[0].map((id: string, i: number) => {
|
|
204
|
+
const metadata = results.metadatas?.[0]?.[i] as Record<string, any>
|
|
205
|
+
return this.metadataToEpisode(id, metadata, results.documents?.[0]?.[i] as string)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
} catch (error) {
|
|
209
|
+
this.logger.error({ error, query }, 'Failed to search episodes')
|
|
210
|
+
return []
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async getEpisode(episodeId: string): Promise<Episode | null> {
|
|
215
|
+
// Check active episodes first
|
|
216
|
+
for (const episode of this.activeEpisodes.values()) {
|
|
217
|
+
if (episode.id === episodeId) return episode
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Check ChromaDB
|
|
221
|
+
try {
|
|
222
|
+
const collection = await this.collections.getEpisodes()
|
|
223
|
+
const result = await collection.get({
|
|
224
|
+
ids: [episodeId],
|
|
225
|
+
include: ['documents', 'metadatas']
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
if (result.ids.length === 0) return null
|
|
229
|
+
|
|
230
|
+
const metadata = result.metadatas?.[0] as Record<string, any>
|
|
231
|
+
return this.metadataToEpisode(episodeId, metadata, result.documents?.[0] as string)
|
|
232
|
+
} catch (error) {
|
|
233
|
+
this.logger.error({ error, episodeId }, 'Failed to get episode')
|
|
234
|
+
return null
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
linkDecision(episodeId: string, decisionId: string): void {
|
|
239
|
+
for (const episode of this.activeEpisodes.values()) {
|
|
240
|
+
if (episode.id === episodeId) {
|
|
241
|
+
if (!episode.related_decisions.includes(decisionId)) {
|
|
242
|
+
episode.related_decisions.push(decisionId)
|
|
243
|
+
}
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
linkPattern(episodeId: string, patternId: string): void {
|
|
250
|
+
for (const episode of this.activeEpisodes.values()) {
|
|
251
|
+
if (episode.id === episodeId) {
|
|
252
|
+
if (!episode.related_patterns.includes(patternId)) {
|
|
253
|
+
episode.related_patterns.push(patternId)
|
|
254
|
+
}
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
linkCorrection(episodeId: string, correctionId: string): void {
|
|
261
|
+
for (const episode of this.activeEpisodes.values()) {
|
|
262
|
+
if (episode.id === episodeId) {
|
|
263
|
+
if (!episode.related_corrections.includes(correctionId)) {
|
|
264
|
+
episode.related_corrections.push(correctionId)
|
|
265
|
+
}
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Remove a decision reference from all active episodes.
|
|
273
|
+
* Called when a decision is deleted to prevent stale references.
|
|
274
|
+
*/
|
|
275
|
+
unlinkDecision(decisionId: string): void {
|
|
276
|
+
for (const episode of this.activeEpisodes.values()) {
|
|
277
|
+
const idx = episode.related_decisions.indexOf(decisionId)
|
|
278
|
+
if (idx !== -1) {
|
|
279
|
+
episode.related_decisions.splice(idx, 1)
|
|
280
|
+
this.logger.debug(
|
|
281
|
+
{ episodeId: episode.id, decisionId },
|
|
282
|
+
'Decision unlinked from episode'
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private async persistEpisode(episode: Episode): Promise<void> {
|
|
289
|
+
try {
|
|
290
|
+
const collection = await this.collections.getEpisodes()
|
|
291
|
+
|
|
292
|
+
const summaryText = episode.summary
|
|
293
|
+
? `${episode.summary.brief}\n\nTopics: ${episode.summary.key_topics.join(', ')}\n\n${episode.summary.detailed}`
|
|
294
|
+
: `Episode with ${episode.message_count} messages`
|
|
295
|
+
|
|
296
|
+
const metadata: Record<string, any> = {
|
|
297
|
+
episode_id: episode.id,
|
|
298
|
+
project: episode.project || '',
|
|
299
|
+
status: episode.status,
|
|
300
|
+
started_at: episode.started_at,
|
|
301
|
+
ended_at: episode.ended_at || '',
|
|
302
|
+
message_count: episode.message_count,
|
|
303
|
+
token_count: episode.token_count,
|
|
304
|
+
key_topics: episode.summary?.key_topics.join(',') || '',
|
|
305
|
+
brief_summary: episode.summary?.brief || '',
|
|
306
|
+
related_decisions: episode.related_decisions.join(','),
|
|
307
|
+
related_patterns: episode.related_patterns.join(','),
|
|
308
|
+
related_corrections: episode.related_corrections.join(',')
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const embeddings = this.embeddings
|
|
312
|
+
? [await this.embeddings.generate(summaryText)]
|
|
313
|
+
: undefined
|
|
314
|
+
|
|
315
|
+
await collection.add({
|
|
316
|
+
ids: [episode.id],
|
|
317
|
+
documents: [summaryText],
|
|
318
|
+
metadatas: [metadata],
|
|
319
|
+
...(embeddings ? { embeddings } : {})
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
this.logger.debug({ episodeId: episode.id }, 'Episode persisted to ChromaDB')
|
|
323
|
+
} catch (error) {
|
|
324
|
+
this.logger.error({ error, episodeId: episode.id }, 'Failed to persist episode')
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
private metadataToEpisode(id: string, metadata: Record<string, any>, document?: string): Episode {
|
|
329
|
+
return {
|
|
330
|
+
id,
|
|
331
|
+
project: metadata.project || undefined,
|
|
332
|
+
status: metadata.status || 'completed',
|
|
333
|
+
started_at: metadata.started_at || '',
|
|
334
|
+
ended_at: metadata.ended_at || undefined,
|
|
335
|
+
messages: [],
|
|
336
|
+
summary: metadata.brief_summary ? {
|
|
337
|
+
brief: metadata.brief_summary,
|
|
338
|
+
detailed: document || metadata.brief_summary,
|
|
339
|
+
key_topics: (metadata.key_topics || '').split(',').filter(Boolean),
|
|
340
|
+
decisions_made: [],
|
|
341
|
+
unresolved_questions: [],
|
|
342
|
+
entity_count: 0
|
|
343
|
+
} : undefined,
|
|
344
|
+
related_decisions: (metadata.related_decisions || '').split(',').filter(Boolean),
|
|
345
|
+
related_patterns: (metadata.related_patterns || '').split(',').filter(Boolean),
|
|
346
|
+
related_corrections: (metadata.related_corrections || '').split(',').filter(Boolean),
|
|
347
|
+
token_count: metadata.token_count || 0,
|
|
348
|
+
message_count: metadata.message_count || 0
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|