import type { FastifyInstance } from 'fastify'; import type { Sql } from '../db.js'; import type { Broker } from '../services/broker.js'; import type { Message } from '../types/api.js'; export function registerWebSocket( app: FastifyInstance, sql: Sql, broker: Broker ): void { app.get<{ Params: { id: string } }>( '/api/ws/sessions/:id', { websocket: true }, async (socket, req) => { const sessionId = req.params.id; const session = await sql<{ id: string }[]>`SELECT id FROM sessions WHERE id = ${sessionId}`; if (session.length === 0) { socket.send(JSON.stringify({ type: 'error', error: 'session not found' })); socket.close(1008, 'session not found'); return; } // v1.11: snapshot includes compaction fields so MessageBubble can // render the SummaryCard for summary=true rows on first connect. // v1.13.1-B: reads tool_calls/tool_results via the parts-merged view. const messages = await sql` SELECT id, session_id, chat_id, role, content, kind, tool_calls, tool_results, status, last_seq, tokens_used, ctx_used, ctx_max, started_at, finished_at, created_at, metadata, summary, tail_start_id, compacted_at FROM messages_with_parts WHERE session_id = ${sessionId} ORDER BY created_at ASC, id ASC `; socket.send(JSON.stringify({ type: 'snapshot', messages })); const unsubscribe = broker.subscribe(sessionId, (frame) => { if (socket.readyState !== socket.OPEN) return; try { socket.send(JSON.stringify(frame)); } catch (err) { app.log.warn({ err, sessionId }, 'ws send failed'); } }); socket.on('close', () => unsubscribe()); socket.on('error', () => unsubscribe()); } ); app.get('/api/ws/user', { websocket: true }, async (socket) => { const user = 'default'; const unsubscribe = broker.subscribeUser(user, (frame) => { if (socket.readyState !== socket.OPEN) return; try { socket.send(JSON.stringify(frame)); } catch (err) { app.log.warn({ err, user }, 'user ws send failed'); } }); socket.on('close', () => unsubscribe()); socket.on('error', () => unsubscribe()); }); }