feat(bf-52d6): integrate conversation transcript parser with store and web API

Integrate the conversationParser module with the InMemoryEventStore and web server
to provide complete conversation transcript functionality.

Store integration:
- Add conversation session caching with 5-second TTL
- Invalidate cache when conversation events are added
- Add methods: getConversationSessions, getWorkerConversationSessions,
  getBeadConversationSession, getConversationSession,
  getConversationEvents, getWorkerConversationEvents,
  getBeadConversationEvents

Web API integration:
- GET /api/conversations/sessions - Get all conversation sessions
- GET /api/conversations/workers/:workerId - Get sessions for worker
- GET /api/conversations/beads/:beadId - Get session for bead
- GET /api/conversations/:sessionId - Get session by ID
- GET /api/conversations/events - Get all conversation events
- GET /api/conversations/workers/:workerId/events - Get worker events
- GET /api/conversations/beads/:beadId/events - Get bead events

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jedarden 2026-05-08 14:28:34 -04:00
parent 3da90f1be0
commit c627976dbc
2 changed files with 210 additions and 0 deletions

View file

@ -44,6 +44,8 @@ import {
TimelapseOptions,
HeatmapTimelapse,
HeatmapSnapshot,
ConversationSession,
ConversationEvent,
compareEventsBySequence,
} from './types.js';
import { isWorkerStuck } from './tui/utils/stuckDetection.js';
@ -55,6 +57,12 @@ import { WorkerAnalytics, getWorkerAnalytics } from './workerAnalytics.js';
import { CostTracker } from './tui/utils/costTracking.js';
import { SemanticNarrativeGenerator, getSemanticNarrativeManager } from './semanticNarrative.js';
import { HistoricalStore, getHistoricalStore } from './historicalStore.js';
import {
buildConversationSessions,
getWorkerConversationSessions,
getBeadConversationSession,
extractConversationEvents,
} from './conversationParser.js';
/** Time window (in ms) to consider events as concurrent */
const COLLISION_WINDOW_MS = 5000;
@ -157,6 +165,20 @@ export class InMemoryEventStore implements EventStore {
add(event: LogEvent): void {
this.events.push(event);
// Invalidate conversation cache when conversation events are added
const spanName = String(event.span_name || '');
if (
spanName.includes('llm.') ||
spanName.includes('tool.') ||
event.prompt ||
event.response ||
event.tool ||
event.msg.includes('llm.request') ||
event.msg.includes('tool.call')
) {
this.invalidateConversationCache();
}
// Populate secondary index keyed on (worker, sequence)
if (event.sequence != null && event.sequence >= 0) {
this.sequenceIndex.set(`${event.worker}:${event.sequence}`, event);
@ -312,6 +334,9 @@ export class InMemoryEventStore implements EventStore {
clearTimeout(this.batchTimeout);
this.batchTimeout = null;
}
// Clear conversation cache
this.invalidateConversationCache();
}
/**
@ -1938,6 +1963,97 @@ export class InMemoryEventStore implements EventStore {
getSemanticNarrativeManager(): SemanticNarrativeGenerator {
return this.semanticNarrativeManager;
}
// ============================================
// Conversation Transcript Methods
// ============================================
/**
* Cached conversation sessions built from events
*/
private conversationSessions: ConversationSession[] | null = null;
private conversationSessionsCacheTime: number = 0;
private readonly CONVERSATION_CACHE_TTL_MS = 5000; // 5 seconds
/**
* Invalidate the conversation sessions cache
*/
private invalidateConversationCache(): void {
this.conversationSessions = null;
this.conversationSessionsCacheTime = 0;
}
/**
* Build or retrieve cached conversation sessions
*/
private buildConversationSessions(): ConversationSession[] {
const now = Date.now();
// Return cached sessions if still valid
if (
this.conversationSessions &&
(now - this.conversationSessionsCacheTime) < this.CONVERSATION_CACHE_TTL_MS
) {
return this.conversationSessions;
}
// Build new sessions from events
this.conversationSessions = buildConversationSessions(this.events);
this.conversationSessionsCacheTime = now;
return this.conversationSessions;
}
/**
* Get all conversation sessions
*/
getConversationSessions(): ConversationSession[] {
return this.buildConversationSessions();
}
/**
* Get conversation sessions for a specific worker
*/
getWorkerConversationSessions(workerId: string): ConversationSession[] {
return getWorkerConversationSessions(this.events, workerId);
}
/**
* Get conversation session for a specific bead
*/
getBeadConversationSession(beadId: string): ConversationSession | null {
return getBeadConversationSession(this.events, beadId);
}
/**
* Get a specific conversation session by ID
*/
getConversationSession(sessionId: string): ConversationSession | undefined {
const sessions = this.buildConversationSessions();
return sessions.find(s => s.id === sessionId);
}
/**
* Get all conversation events across all sessions
*/
getConversationEvents(): ConversationEvent[] {
return extractConversationEvents(this.events);
}
/**
* Get conversation events for a specific worker
*/
getWorkerConversationEvents(workerId: string): ConversationEvent[] {
const workerEvents = this.events.filter(e => e.worker === workerId);
return extractConversationEvents(workerEvents);
}
/**
* Get conversation events for a specific bead
*/
getBeadConversationEvents(beadId: string): ConversationEvent[] {
const beadEvents = this.events.filter(e => e.bead === beadId);
return extractConversationEvents(beadEvents);
}
}
/**

View file

@ -1166,6 +1166,100 @@ export function createWebServer(options: WebServerOptions): WebServer {
}
});
// ============================================
// Conversation Transcript API
// ============================================
// Get all conversation sessions
app.get('/api/conversations/sessions', (_req: Request, res: Response) => {
try {
const sessions = store.getConversationSessions();
res.json(sessions);
} catch (err) {
console.error('Error getting conversation sessions:', err);
res.status(500).json({ error: 'Failed to get conversation sessions' });
}
});
// Get conversation sessions for a specific worker
app.get('/api/conversations/workers/:workerId', (req: Request, res: Response) => {
try {
const workerId = req.params.workerId as string;
const sessions = store.getWorkerConversationSessions(workerId);
res.json(sessions);
} catch (err) {
console.error('Error getting worker conversation sessions:', err);
res.status(500).json({ error: 'Failed to get worker conversation sessions' });
}
});
// Get conversation session for a specific bead
app.get('/api/conversations/beads/:beadId', (req: Request, res: Response) => {
try {
const beadId = req.params.beadId as string;
const session = store.getBeadConversationSession(beadId);
if (!session) {
res.status(404).json({ error: 'Conversation session not found' });
return;
}
res.json(session);
} catch (err) {
console.error('Error getting bead conversation session:', err);
res.status(500).json({ error: 'Failed to get bead conversation session' });
}
});
// Get a specific conversation session by ID
app.get('/api/conversations/:sessionId', (req: Request, res: Response) => {
try {
const sessionId = req.params.sessionId as string;
const session = store.getConversationSession(sessionId);
if (!session) {
res.status(404).json({ error: 'Conversation session not found' });
return;
}
res.json(session);
} catch (err) {
console.error('Error getting conversation session:', err);
res.status(500).json({ error: 'Failed to get conversation session' });
}
});
// Get all conversation events across all sessions
app.get('/api/conversations/events', (_req: Request, res: Response) => {
try {
const events = store.getConversationEvents();
res.json(events);
} catch (err) {
console.error('Error getting conversation events:', err);
res.status(500).json({ error: 'Failed to get conversation events' });
}
});
// Get conversation events for a specific worker
app.get('/api/conversations/workers/:workerId/events', (req: Request, res: Response) => {
try {
const workerId = req.params.workerId as string;
const events = store.getWorkerConversationEvents(workerId);
res.json(events);
} catch (err) {
console.error('Error getting worker conversation events:', err);
res.status(500).json({ error: 'Failed to get worker conversation events' });
}
});
// Get conversation events for a specific bead
app.get('/api/conversations/beads/:beadId/events', (req: Request, res: Response) => {
try {
const beadId = req.params.beadId as string;
const events = store.getBeadConversationEvents(beadId);
res.json(events);
} catch (err) {
console.error('Error getting bead conversation events:', err);
res.status(500).json({ error: 'Failed to get bead conversation events' });
}
});
// Serve static frontend files
const staticPath = join(__dirname, 'public');
app.use(express.static(staticPath));