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>
46 lines
1.8 KiB
TypeScript
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 };
|
|
},
|
|
);
|
|
}
|