/** * Shared SSE parsing for the AI service streams. * * Normalizes CRLF (sse-starlette's wire terminator is \r\n) to LF, then * splits frames on blank lines. Multiple `data:` lines within one frame * are joined with \n per the SSE spec. Frames with no data lines * (keep-alive `: ping` comments) are ignored (G-1). */ export function parseSseEvents(buffer: string): { events: string[]; rest: string } { const normalized = buffer.replace(/\r\n/g, '\n'); const events: string[] = []; const separatorIndex = normalized.lastIndexOf('\n\n'); if (separatorIndex === -1) return { events, rest: normalized }; const complete = normalized.slice(0, separatorIndex); const rest = normalized.slice(separatorIndex + 2); for (const frame of complete.split('\n\n')) { const dataLines = frame .split('\n') .filter((line) => line.startsWith('data:')) .map((line) => line.slice(5).trimStart()); if (dataLines.length === 0) continue; // ping/comment frame — ignore (G-1) events.push(dataLines.join('\n')); } return { events, rest }; }