/** * ChunkFactory service - Creates individual chunk objects from messages. * * Responsibilities: * - Build UserChunk from user messages * - Build SystemChunk from command output messages * - Build CompactChunk from summary messages * - Build AIChunk from buffered AI messages * - Calculate timing and metrics for chunks */ import { type EnhancedAIChunk, type EnhancedCompactChunk, type EnhancedSystemChunk, type EnhancedUserChunk, type ParsedMessage, type Process, } from '@main/types'; import { calculateStepContext } from '@main/utils/contextAccumulator'; import { calculateMetrics } from '@main/utils/jsonl'; import { fillTimelineGaps } from '@main/utils/timelineGapFilling'; import { linkProcessesToAIChunk } from './ProcessLinker'; import { extractSemanticStepsFromAIChunk } from './SemanticStepExtractor'; import { buildSemanticStepGroups } from './SemanticStepGrouper'; import { buildToolExecutions } from './ToolExecutionBuilder'; /** * Generate a stable chunk ID based on message UUID. * Using the message UUID ensures IDs are consistent across re-parses. */ function generateStableChunkId(prefix: string, message: ParsedMessage): string { return `${prefix}-${message.uuid}`; } /** * Build a UserChunk from a user message. */ export function buildUserChunk(message: ParsedMessage): EnhancedUserChunk { const id = generateStableChunkId('user', message); const metrics = calculateMetrics([message]); return { id, chunkType: 'user', userMessage: message, startTime: message.timestamp, endTime: message.timestamp, durationMs: 0, metrics, rawMessages: [message], }; } /** * Build a SystemChunk from a command output message. */ export function buildSystemChunk(message: ParsedMessage): EnhancedSystemChunk { const id = generateStableChunkId('system', message); const commandOutput = extractCommandOutput(message); const metrics = calculateMetrics([message]); return { id, chunkType: 'system', message, commandOutput, startTime: message.timestamp, endTime: message.timestamp, durationMs: 0, metrics, rawMessages: [message], }; } /** * Build a CompactChunk from a compact summary message. */ export function buildCompactChunk(message: ParsedMessage): EnhancedCompactChunk { const id = generateStableChunkId('compact', message); const metrics = calculateMetrics([message]); return { id, chunkType: 'compact', message, startTime: message.timestamp, endTime: message.timestamp, durationMs: 0, metrics, rawMessages: [message], }; } /** * Extract command output from tag. */ function extractCommandOutput(message: ParsedMessage): string { const content = typeof message.content === 'string' ? message.content : ''; const match = /([\s\S]*?)<\/local-command-stdout>/.exec(content); const matchStderr = /([\s\S]*?)<\/local-command-stderr>/.exec(content); if (match) { return match[1]; } if (matchStderr) { return matchStderr[1]; } return content; } /** * Build an AIChunk from buffered AI messages. */ export function buildAIChunkFromBuffer( responses: ParsedMessage[], subagents: Process[], allMessages: ParsedMessage[] ): EnhancedAIChunk { // Use first response message's UUID for stable ID const id = responses.length > 0 ? generateStableChunkId('ai', responses[0]) : `ai-empty-${Date.now()}`; // Fallback for edge case const { startTime, endTime, durationMs } = calculateAIChunkTiming(responses); const metrics = calculateMetrics(responses); const toolExecutions = buildToolExecutions(responses); // Collect sidechain messages for this time range const sidechainMessages = collectSidechainMessages(allMessages, startTime, endTime); const chunk: EnhancedAIChunk = { id, chunkType: 'ai', responses, startTime, endTime, durationMs, metrics, processes: [], sidechainMessages, toolExecutions, semanticSteps: [], rawMessages: responses, }; // Link processes to this chunk linkProcessesToAIChunk(chunk, subagents); // Extract semantic steps using the extracted module chunk.semanticSteps = extractSemanticStepsFromAIChunk(chunk); chunk.semanticSteps = fillTimelineGaps({ steps: chunk.semanticSteps, chunkStartTime: chunk.startTime, chunkEndTime: chunk.endTime, }); calculateStepContext(chunk.semanticSteps, chunk.rawMessages); chunk.semanticStepGroups = buildSemanticStepGroups(chunk.semanticSteps); return chunk; } /** * Calculate timing for AI chunks (responses only, no user message). */ function calculateAIChunkTiming(responses: ParsedMessage[]): { startTime: Date; endTime: Date; durationMs: number; } { if (responses.length === 0) { const now = new Date(); return { startTime: now, endTime: now, durationMs: 0 }; } const startTime = responses[0].timestamp; let endTime = startTime; for (const resp of responses) { if (resp.timestamp > endTime) { endTime = resp.timestamp; } } return { startTime, endTime, durationMs: endTime.getTime() - startTime.getTime(), }; } /** * Collect sidechain messages in a time range. */ function collectSidechainMessages( messages: ParsedMessage[], startTime: Date, endTime: Date | undefined ): ParsedMessage[] { return messages.filter((m) => { if (!m.isSidechain) return false; if (m.timestamp < startTime) return false; if (endTime && m.timestamp >= endTime) return false; return true; }); }