import type { FastifyInstance } from 'fastify'; import { z } from 'zod'; import type { Sql } from '../db.js'; import type { Config } from '../config.js'; import type { Broker } from '../services/broker.js'; import type { Session } from '../types/api.js'; import { getSetting } from './settings.js'; const CreateBody = z.object({ name: z.string().min(1).max(200).optional(), model: z.string().min(1).max(200).optional(), system_prompt: z.string().max(8000).optional(), }); const PatchBody = z.object({ name: z.string().min(1).max(200).optional(), model: z.string().min(1).max(200).optional(), system_prompt: z.string().max(8000).optional(), }); async function resolveDefaultModel(sql: Sql, config: Config): Promise { const fromDb = await getSetting(sql, 'default_model'); if (typeof fromDb === 'string' && fromDb.length > 0) return fromDb; return config.DEFAULT_MODEL; } export function registerSessionRoutes( app: FastifyInstance, sql: Sql, config: Config, broker: Broker ): void { app.get<{ Params: { id: string }; Querystring: { status?: string } }>( '/api/projects/:id/sessions', async (req, reply) => { const project = await sql`SELECT id FROM projects WHERE id = ${req.params.id}`; if (project.length === 0) { reply.code(404); return { error: 'project not found' }; } const status = req.query.status === 'archived' ? 'archived' : 'open'; const rows = await sql` SELECT id, project_id, name, model, system_prompt, status, created_at, updated_at FROM sessions WHERE project_id = ${req.params.id} AND status = ${status} ORDER BY updated_at DESC `; return rows; } ); app.post<{ Params: { id: string } }>( '/api/projects/:id/sessions', async (req, reply) => { const parsed = CreateBody.safeParse(req.body ?? {}); if (!parsed.success) { reply.code(400); return { error: 'invalid body', details: parsed.error.flatten() }; } const project = await sql`SELECT id FROM projects WHERE id = ${req.params.id}`; if (project.length === 0) { reply.code(404); return { error: 'project not found' }; } let model = parsed.data.model; if (!model) { const lastUsed = await sql<{ model: string }[]>` SELECT model FROM sessions WHERE project_id = ${req.params.id} ORDER BY created_at DESC LIMIT 1 `; model = lastUsed[0]?.model ?? (await resolveDefaultModel(sql, config)); } const name = parsed.data.name ?? 'New session'; const systemPrompt = parsed.data.system_prompt ?? ''; const row = await sql.begin(async (tx) => { const [session] = await tx` INSERT INTO sessions (project_id, name, model, system_prompt) VALUES (${req.params.id}, ${name}, ${model}, ${systemPrompt}) RETURNING id, project_id, name, model, system_prompt, status, created_at, updated_at `; await tx` INSERT INTO chats (session_id, name, status) VALUES (${session!.id}, NULL, 'open') `; return session!; }); broker.publishUser('default', { type: 'session_created', session: row, project_id: row.project_id, }); reply.code(201); return row; } ); app.get<{ Params: { id: string } }>('/api/sessions/:id', async (req, reply) => { const rows = await sql` SELECT id, project_id, name, model, system_prompt, status, created_at, updated_at FROM sessions WHERE id = ${req.params.id} `; if (rows.length === 0) { reply.code(404); return { error: 'session not found' }; } return rows[0]; }); app.patch<{ Params: { id: string } }>( '/api/sessions/:id', async (req, reply) => { const parsed = PatchBody.safeParse(req.body); if (!parsed.success) { reply.code(400); return { error: 'invalid body', details: parsed.error.flatten() }; } const { name, model, system_prompt } = parsed.data; const rows = await sql` UPDATE sessions SET name = COALESCE(${name ?? null}, name), model = COALESCE(${model ?? null}, model), system_prompt = COALESCE(${system_prompt ?? null}, system_prompt), updated_at = clock_timestamp() WHERE id = ${req.params.id} RETURNING id, project_id, name, model, system_prompt, status, created_at, updated_at `; if (rows.length === 0) { reply.code(404); return { error: 'session not found' }; } const session = rows[0]!; if (name !== undefined) { broker.publishUser('default', { type: 'session_renamed', session_id: session.id, name: session.name, }); } return session; } ); app.post<{ Params: { id: string } }>( '/api/sessions/:id/archive', async (req, reply) => { const rows = await sql<{ id: string; project_id: string }[]>` UPDATE sessions SET status = 'archived', updated_at = clock_timestamp() WHERE id = ${req.params.id} AND status = 'open' RETURNING id, project_id `; if (rows.length === 0) { reply.code(404); return { error: 'session not found or already archived' }; } broker.publishUser('default', { type: 'session_archived', session_id: rows[0]!.id, project_id: rows[0]!.project_id, }); reply.code(204); return null; } ); app.post<{ Params: { id: string } }>( '/api/sessions/:id/unarchive', async (req, reply) => { const rows = await sql` UPDATE sessions SET status = 'open', updated_at = clock_timestamp() WHERE id = ${req.params.id} AND status = 'archived' RETURNING id, project_id, name, model, system_prompt, status, created_at, updated_at `; if (rows.length === 0) { reply.code(404); return { error: 'session not found or not archived' }; } const session = rows[0]!; broker.publishUser('default', { type: 'session_created', session: session, project_id: session.project_id, }); reply.code(200); return session; } ); app.delete<{ Params: { id: string } }>( '/api/sessions/:id', async (req, reply) => { const id = req.params.id; const deleted = await sql<{ project_id: string }[]>` DELETE FROM sessions WHERE id = ${id} RETURNING project_id `; if (deleted.length === 0) { reply.code(404); return { error: 'not found' }; } const project_id = deleted[0]!.project_id; broker.publishUser('default', { type: 'session_deleted', session_id: id, project_id }); reply.code(204); return null; } ); }