/** * write-edit-robustness #4 — checkpoint restore + list routes (coder side). * * Proxied through the apps/server `/api/coder/*` blanket forwarder (no server-side * change needed for new routes). Restore rewinds the session worktree to the * checkpoint's shadow commit, trims the transcript from the anchor message forward, * and resets the agent backend — see services/checkpoints.ts. */ import type { FastifyInstance } from 'fastify'; import type { Sql } from '../db.js'; import { restoreCheckpoint, CheckpointNotFoundError } from '../services/checkpoints.js'; export function registerCheckpointRoutes(app: FastifyInstance, sql: Sql): void { // GET /api/sessions/:sessionId/checkpoints?chat_id= — list a chat's checkpoints // so the frontend can mark which messages have a restore point. When chat_id is // omitted, returns every checkpoint for the session's chats. app.get<{ Params: { sessionId: string }; Querystring: { chat_id?: string } }>( '/api/sessions/:sessionId/checkpoints', async (req, reply) => { const sessionId = req.params.sessionId; const chatId = req.query.chat_id; const session = await sql<{ id: string }[]>`SELECT id FROM sessions WHERE id = ${sessionId}`; if (session.length === 0) { reply.code(404); return { error: 'session not found' }; } // Scope authoritatively through chats.session_id (always set) — NOT the // denormalized checkpoints.session_id (nullable). The chat_id branch must // still be session-gated or it's an IDOR (any session's chat_id reads its // checkpoints). const rows = chatId ? await sql<{ id: string; chat_id: string; message_id: string | null; label: string | null; created_at: Date }[]>` SELECT cp.id, cp.chat_id, cp.message_id, cp.label, cp.created_at FROM checkpoints cp JOIN chats c ON c.id = cp.chat_id WHERE cp.chat_id = ${chatId} AND c.session_id = ${sessionId} ORDER BY cp.created_at ` : await sql<{ id: string; chat_id: string; message_id: string | null; label: string | null; created_at: Date }[]>` SELECT cp.id, cp.chat_id, cp.message_id, cp.label, cp.created_at FROM checkpoints cp JOIN chats c ON c.id = cp.chat_id WHERE c.session_id = ${sessionId} ORDER BY cp.created_at `; return rows; }, ); // POST /api/sessions/:sessionId/checkpoints/:checkpointId/restore — restore. app.post<{ Params: { sessionId: string; checkpointId: string } }>( '/api/sessions/:sessionId/checkpoints/:checkpointId/restore', async (req, reply) => { const { sessionId, checkpointId } = req.params; try { const result = await restoreCheckpoint(sql, checkpointId, { sessionId, log: app.log, }); return result; } catch (err) { if (err instanceof CheckpointNotFoundError) { reply.code(404); return { error: err.message }; } throw err; } }, ); }