import * as fs from "node:fs"; import * as path from "node:path"; import { DEFAULT_CACHE, DEFAULT_PATHS } from "../config/defaults.ts"; import type { TeamRunManifest } from "./types.ts"; import { atomicWriteJson } from "./atomic-write.ts"; import { userCrewRoot } from "../utils/paths.ts"; import { isSafePathId } from "../utils/safe-paths.ts"; export interface ActiveRunRegistryEntry { runId: string; cwd: string; stateRoot: string; manifestPath: string; updatedAt: string; } function registryPath(): string { return path.join(userCrewRoot(), DEFAULT_PATHS.state.runsSubdir, "active-run-index.json"); } function registryLockPath(): string { return `${registryPath()}.lock`; } function sleepSync(ms: number): void { try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); } catch { const deadline = Date.now() + ms; while (Date.now() < deadline) { // Best-effort fallback for rare runtimes without Atomics.wait. } } } function lockCreatedAt(raw: string): number | undefined { try { const parsed = JSON.parse(raw) as { createdAt?: unknown }; if (typeof parsed.createdAt !== "string") return undefined; const time = Date.parse(parsed.createdAt); return Number.isNaN(time) ? undefined : time; } catch { return undefined; } } function removeStaleRegistryLock(lockPath: string, staleMs: number): boolean { try { const stat = fs.statSync(lockPath); const createdAt = lockCreatedAt(fs.readFileSync(lockPath, "utf-8")) ?? stat.mtimeMs; if (Date.now() - createdAt <= staleMs) return false; fs.rmSync(lockPath, { force: true }); return true; } catch { return false; } } function withRegistryLock(fn: () => T): T { const filePath = registryLockPath(); const staleMs = 30_000; fs.mkdirSync(path.dirname(filePath), { recursive: true }); let attempt = 0; const deadline = Date.now() + staleMs * 2; while (true) { try { const fd = fs.openSync(filePath, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o644); try { fs.writeSync(fd, JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString() })); } finally { fs.closeSync(fd); } break; } catch (error) { const code = (error as NodeJS.ErrnoException).code; if (code !== "EEXIST") throw error; if (!removeStaleRegistryLock(filePath, staleMs) && Date.now() > deadline) throw new Error("Active-run registry is locked by another operation."); sleepSync(Math.min(250, 25 * 2 ** attempt)); attempt += 1; } } try { return fn(); } finally { try { fs.rmSync(filePath, { force: true }); } catch { // Best-effort cleanup. } } } function normalizeEntry(value: unknown): ActiveRunRegistryEntry | undefined { if (!value || typeof value !== "object" || Array.isArray(value)) return undefined; const record = value as Record; const runId = typeof record.runId === "string" ? record.runId : undefined; const cwd = typeof record.cwd === "string" ? record.cwd : undefined; const stateRoot = typeof record.stateRoot === "string" ? record.stateRoot : undefined; const manifestPath = typeof record.manifestPath === "string" ? record.manifestPath : undefined; const updatedAt = typeof record.updatedAt === "string" ? record.updatedAt : undefined; if (!runId || !isSafePathId(runId) || !cwd || !stateRoot || !manifestPath || !updatedAt) return undefined; if (path.basename(stateRoot) !== runId) return undefined; if (path.resolve(manifestPath) !== path.resolve(path.join(stateRoot, DEFAULT_PATHS.state.manifestFile))) return undefined; return { runId, cwd, stateRoot, manifestPath, updatedAt }; } export function readActiveRunRegistry(maxEntries = DEFAULT_CACHE.manifestMaxEntries): ActiveRunRegistryEntry[] { let parsed: unknown; try { parsed = JSON.parse(fs.readFileSync(registryPath(), "utf-8")); } catch { return []; } const entries = Array.isArray(parsed) ? parsed.map(normalizeEntry).filter((entry): entry is ActiveRunRegistryEntry => entry !== undefined) : []; const byId = new Map(); for (const entry of entries.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt))) { if (!byId.has(entry.runId)) byId.set(entry.runId, entry); } return [...byId.values()].slice(0, Math.max(0, maxEntries)); } function writeEntries(entries: ActiveRunRegistryEntry[]): void { fs.mkdirSync(path.dirname(registryPath()), { recursive: true }); atomicWriteJson(registryPath(), entries.slice(0, DEFAULT_CACHE.manifestMaxEntries)); } export function registerActiveRun(manifest: TeamRunManifest): void { const entry: ActiveRunRegistryEntry = { runId: manifest.runId, cwd: manifest.cwd, stateRoot: manifest.stateRoot, manifestPath: path.join(manifest.stateRoot, DEFAULT_PATHS.state.manifestFile), updatedAt: manifest.updatedAt, }; withRegistryLock(() => { writeEntries([entry, ...readActiveRunRegistry().filter((item) => item.runId !== manifest.runId)]); }); } export function unregisterActiveRun(runId: string): void { if (!isSafePathId(runId)) return; withRegistryLock(() => { writeEntries(readActiveRunRegistry().filter((entry) => entry.runId !== runId)); }); } export function activeRunEntries(): ActiveRunRegistryEntry[] { const entries: ActiveRunRegistryEntry[] = []; for (const entry of readActiveRunRegistry()) { try { if (!fs.existsSync(entry.stateRoot) || !fs.existsSync(entry.manifestPath)) continue; if (fs.lstatSync(entry.stateRoot).isSymbolicLink()) continue; const manifest = JSON.parse(fs.readFileSync(entry.manifestPath, "utf-8")) as { status?: unknown }; if (manifest.status !== "queued" && manifest.status !== "planning" && manifest.status !== "running") continue; entries.push(entry); } catch { // Ignore stale entries; callers filter active status from manifests. } } return entries; } export function activeRunRoots(): string[] { return [...new Set(activeRunEntries().map((entry) => path.dirname(entry.stateRoot)))]; }