// v2.6.10 Phase 3 (server wiring) — fire-and-forget BooCoder close hooks. // // BooCoder (apps/coder, host systemd) added close hooks in // apps/coder/src/routes/lifecycle.ts: // POST /api/chats/:chatId/close — evict the chat's warm (chat,agent) // backends, close its opencode session, // mark agent_sessions closed, and remove // the shared worktree on the last chat. // POST /api/sessions/:sessionId/close — loop the chat-close path for every // chat in the session. // // apps/server (Docker) can't see the host worktree dirs or reach the warm agent // processes, so — exactly like the existing `worktree-risk` guard in // routes/sessions.ts — it signals the coder over HTTP and the coder does the // real teardown. This call is BEST-EFFORT: the coder's idle-pool eviction and // the orphan-worktree reaper backstop a missed/failed call. It MUST NEVER block // or fail the user's delete/archive — hence fire-and-forget with a swallowed // catch. We do not await the returned promise at the call sites. import type { FastifyBaseLogger } from 'fastify'; export type CoderCloseKind = 'chat' | 'session'; function coderOrigin(): string { // Same env + default as routes/sessions.ts' worktree-risk fetch. return process.env.BOOCODER_URL ?? 'http://boocoder:3000'; } /** * Fire-and-forget POST to the BooCoder close hook for a chat or session. * * Resolves to `true` if the coder acknowledged (HTTP 2xx), `false` otherwise * (non-2xx or network error). Callers SHOULD NOT await this — invoke it and * move on. The returned promise never rejects: every failure path is caught, * logged at debug, and folded into a `false` result so an unreachable or * erroring coder can't surface to the user's delete/archive request. */ export async function notifyCoderClose( kind: CoderCloseKind, id: string, log?: Pick, fetcher: typeof fetch = fetch, ): Promise { const segment = kind === 'chat' ? 'chats' : 'sessions'; const url = `${coderOrigin()}/api/${segment}/${id}/close`; try { const res = await fetcher(url, { method: 'POST' }); if (!res.ok) { log?.debug( { kind, id, status: res.status }, 'coder close hook returned non-2xx (best-effort; reaper backstops)', ); return false; } log?.debug({ kind, id }, 'coder close hook acknowledged'); return true; } catch (err) { log?.debug( { kind, id, err: err instanceof Error ? err.message : String(err) }, 'coder close hook unreachable (best-effort; reaper backstops)', ); return false; } }