import { createHash } from 'node:crypto'; import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { TaskStatus, isTerminalStatus } from './task-state.js'; import type { TaskState } from './task-state.js'; // --------------------------------------------------------------------------- // Persisted profile state — cooldown / failure tracking for provider profiles // Spec reference: §8.1 // --------------------------------------------------------------------------- export interface PersistedProfileState { id: string; configDir: string; cooldownUntil?: number; failureCount: number; lastFailureReason?: string; } // --------------------------------------------------------------------------- // Top-level persisted state envelope // --------------------------------------------------------------------------- export interface PersistedState { version: 1; tasks: TaskState[]; profiles: PersistedProfileState[]; lastSavedAt: string; } // --------------------------------------------------------------------------- // persistenceDir — deterministic per-workspace directory // --------------------------------------------------------------------------- export function persistenceDir(root: string, cwd: string): string { const hash = createHash('md5').update(cwd).digest('hex'); return join(root, hash); } // --------------------------------------------------------------------------- // saveState — atomic write: write tmp → rename // --------------------------------------------------------------------------- export async function saveState(dir: string, state: PersistedState): Promise { await mkdir(dir, { recursive: true }); const target = join(dir, 'state.json'); const tmp = join(dir, `state.json.tmp.${process.pid}`); await writeFile(tmp, JSON.stringify(state, null, 2), 'utf-8'); await rename(tmp, target); } // --------------------------------------------------------------------------- // loadState — read + parse, null on missing or corrupt // --------------------------------------------------------------------------- export async function loadState(dir: string): Promise { try { const raw = await readFile(join(dir, 'state.json'), 'utf-8'); return JSON.parse(raw) as PersistedState; } catch { return null; } } // --------------------------------------------------------------------------- // applyRecovery — per-task recovery on server restart // Spec reference: §9.2 steps 1–2 // --------------------------------------------------------------------------- export function applyRecovery(task: TaskState, hasCrashRecovery: boolean): TaskState { // 1. Terminal states restore as-is if (isTerminalStatus(task.status)) { return task; } // 2. Adapters without crash recovery → UNKNOWN if (!hasCrashRecovery) { const now = new Date().toISOString(); return { ...task, status: TaskStatus.UNKNOWN, error: `Server restarted; ${task.provider} sessions don't survive restarts`, completedAt: now, // Auto-expire recovered tasks after 60 seconds (vs 300s default) keepAlive: 60_000, output: [ ...task.output, `[recovery] Task was ${task.status} when server restarted; marked UNKNOWN`, ], recovered: true, }; } // 3. Has crash recovery (e.g. Codex) — return as-is; adapter handles externally return task; }