Files
boocode/apps/coder/src/routes/worktree-safety.ts
indifferentketchup cb1846c0d5 feat(coder): re-key agent_sessions to (chat_id, agent) + worktrees table (P1.5-b)
The tab (a chat) is the context unit: two opencode tabs in one session are two independent agent contexts sharing one worktree. agent_sessions re-keys from (session_id, agent) to (chat_id, agent) — chat_id FK ON DELETE CASCADE (closing a tab ends its context); worktree_id and session_id become informational SET NULL columns. New worktrees table (one-per-session, survives session delete via session_id SET NULL) supersedes session_worktrees, which is defanged (CASCADE dropped) not yet removed. chat_id is threaded end-to-end: tasks.chat_id added, written by the coder message + skills routes from the frontend tab, read by runOpenCodeServerTask which falls back to resolve-or-create a chat for session-less creators (arena/MCP/new_task/generic) so ensureSession never gets a null key. Idempotent migration with a backfill-verify gate (0-row assertion after the test session was deleted). config_hash fingerprint logic preserved; one-worktree-per-session unchanged; runExternalAgent untouched. Column rename worktree_path -> path repointed at all five readers (server delete-guard, risk/stash endpoints, ensureSessionWorktree). Supersedes the earlier (worktree_id) draft.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 00:04:35 +00:00

46 lines
1.8 KiB
TypeScript

/**
* Session-delete work-loss guard (coder side).
*
* Session delete itself lives in apps/server (Docker), which CANNOT see the
* host worktree dirs (/tmp/booworktrees) or run git on them. Only BooCoder
* (host systemd) can. So the server's DELETE route calls these endpoints
* pre-delete to learn whether a session's worktree holds work at risk, and to
* stash it. The server owns the gate; coder owns the git truth.
*/
import type { FastifyInstance } from 'fastify';
import type { Sql } from '../db.js';
import { checkWorktreeWorkAtRisk, stashWorktree } from '../services/worktrees.js';
export function registerWorktreeSafetyRoutes(app: FastifyInstance, sql: Sql): void {
// GET risk for a session's worktree(s). One row per session today (PK on
// session_id); the loop already handles the Phase-1.5 multi-worktree case.
app.get<{ Params: { sessionId: string } }>(
'/api/sessions/:sessionId/worktree-risk',
async (req) => {
const rows = await sql<{ worktree_path: string }[]>`
SELECT path AS worktree_path FROM worktrees WHERE session_id = ${req.params.sessionId}
`;
const reports = [];
for (const row of rows) {
reports.push(await checkWorktreeWorkAtRisk(row.worktree_path));
}
return { reports };
},
);
// Stash a session's worktree(s) — clears the dirty risk; recoverable.
app.post<{ Params: { sessionId: string } }>(
'/api/sessions/:sessionId/worktree-stash',
async (req) => {
const rows = await sql<{ worktree_path: string }[]>`
SELECT path AS worktree_path FROM worktrees WHERE session_id = ${req.params.sessionId}
`;
const results = [];
for (const row of rows) {
results.push({ worktreePath: row.worktree_path, ...(await stashWorktree(row.worktree_path)) });
}
return { results };
},
);
}