UNPKG

@kontextmind/kxm

Version:

KXM local-first multi-agent orchestration and operator dashboard

233 lines (210 loc) • 10.3 kB
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { kxmResourceIdentifier } from "./project-config.ts"; /** First shape of the roadmap supervisor record. An unknown schema is refused * and left on disk. A later shape migrates in this module the way the Runtime * registry copies schema 1 to 2; this file is not a hub database table. */ export const SUPERVISOR_SCHEMA = "kxm.supervisor.v1"; const TIMESTAMP_RE = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]{1,9})?Z$/; const MERGE_STATE_RE = /^[A-Z][A-Z0-9_]{0,31}$/; const CI_TOKEN_RE = /^[a-z][a-z0-9_-]{0,31}$/; const MAX_BACKOFF_MS = 24 * 60 * 60 * 1000; export interface SupervisorPullRequest { number: number; mergeState: string; seenAt: string; } export interface SupervisorCi { status: string; conclusion: string; seenAt: string; } export interface SupervisorLane { inFlight: boolean; pr?: SupervisorPullRequest; ci?: SupervisorCi; backoffUntil?: string; } export interface SupervisorState { schema: typeof SUPERVISOR_SCHEMA; updatedAt: string; lanes: Record<string, SupervisorLane>; } export interface SupervisorObservation { unit: string; inFlight: boolean; pr?: { number: number; mergeState: string }; ci?: { status: string; conclusion: string }; /** Set when the caller passed a backoff. Zero clears a stored timer. */ backoffMs?: number; } export type SupervisorLoadError = "supervisor_unreadable" | "supervisor_schema_unsupported"; export function supervisorStatePath(stateDir: string): string { return join(stateDir, "supervisor.json"); } export function emptySupervisorState(now: string): SupervisorState { return { schema: SUPERVISOR_SCHEMA, updatedAt: now, lanes: {} }; } export function isSupervisorTimestamp(value: string): boolean { return TIMESTAMP_RE.test(value) && Number.isFinite(Date.parse(value)); } export function resolveSupervisorNow(override: string | undefined, fallback: Date): string | "supervisor_clock_invalid" { if (override === undefined || override.trim() === "") return fallback.toISOString(); const text = override.trim(); return isSupervisorTimestamp(text) ? text : "supervisor_clock_invalid"; } function isPullRequest(value: unknown): value is SupervisorPullRequest { if (!value || typeof value !== "object" || Array.isArray(value)) return false; const record = value as Record<string, unknown>; if (Object.keys(record).some((key) => key !== "number" && key !== "mergeState" && key !== "seenAt")) return false; return Number.isInteger(record.number) && (record.number as number) > 0 && typeof record.mergeState === "string" && MERGE_STATE_RE.test(record.mergeState) && typeof record.seenAt === "string" && isSupervisorTimestamp(record.seenAt); } function isCi(value: unknown): value is SupervisorCi { if (!value || typeof value !== "object" || Array.isArray(value)) return false; const record = value as Record<string, unknown>; if (Object.keys(record).some((key) => key !== "status" && key !== "conclusion" && key !== "seenAt")) return false; return typeof record.status === "string" && CI_TOKEN_RE.test(record.status) && typeof record.conclusion === "string" && CI_TOKEN_RE.test(record.conclusion) && typeof record.seenAt === "string" && isSupervisorTimestamp(record.seenAt); } function isLane(value: unknown): value is SupervisorLane { if (!value || typeof value !== "object" || Array.isArray(value)) return false; const record = value as Record<string, unknown>; if (Object.keys(record).some((key) => key !== "inFlight" && key !== "pr" && key !== "ci" && key !== "backoffUntil")) return false; if (typeof record.inFlight !== "boolean") return false; if (record.pr !== undefined && !isPullRequest(record.pr)) return false; if (record.ci !== undefined && !isCi(record.ci)) return false; if (record.backoffUntil !== undefined && (typeof record.backoffUntil !== "string" || !isSupervisorTimestamp(record.backoffUntil))) return false; return true; } function isState(value: unknown): value is SupervisorState { if (!value || typeof value !== "object" || Array.isArray(value)) return false; const record = value as Record<string, unknown>; if (Object.keys(record).some((key) => key !== "schema" && key !== "updatedAt" && key !== "lanes")) return false; if (record.schema !== SUPERVISOR_SCHEMA) return false; if (typeof record.updatedAt !== "string" || !isSupervisorTimestamp(record.updatedAt)) return false; if (!record.lanes || typeof record.lanes !== "object" || Array.isArray(record.lanes)) return false; for (const [unit, lane] of Object.entries(record.lanes as Record<string, unknown>)) { if (!kxmResourceIdentifier(unit) || !isLane(lane)) return false; } return true; } /** Read `supervisor.json`. A missing file is empty state. A malformed file or * an unknown schema is refused and is not replaced. */ export function loadSupervisorState(stateDir: string, now: string): SupervisorState | SupervisorLoadError { const path = supervisorStatePath(stateDir); if (!existsSync(path)) return emptySupervisorState(now); let parsed: unknown; try { parsed = JSON.parse(readFileSync(path, "utf8")); } catch { return "supervisor_unreadable"; } if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return "supervisor_unreadable"; const schema = (parsed as { schema?: unknown }).schema; if (schema !== SUPERVISOR_SCHEMA) return "supervisor_schema_unsupported"; if (!isState(parsed)) return "supervisor_unreadable"; return parsed; } export function saveSupervisorState(stateDir: string, state: SupervisorState): void { const lanes: Record<string, SupervisorLane> = {}; for (const unit of Object.keys(state.lanes).sort()) { const lane = state.lanes[unit]; if (!lane) continue; const next: SupervisorLane = { inFlight: lane.inFlight }; if (lane.pr) next.pr = { number: lane.pr.number, mergeState: lane.pr.mergeState, seenAt: lane.pr.seenAt }; if (lane.ci) next.ci = { status: lane.ci.status, conclusion: lane.ci.conclusion, seenAt: lane.ci.seenAt }; if (lane.backoffUntil) next.backoffUntil = lane.backoffUntil; lanes[unit] = next; } const body = `${JSON.stringify({ schema: SUPERVISOR_SCHEMA, updatedAt: state.updatedAt, lanes }, null, 2)}\n`; mkdirSync(stateDir, { recursive: true }); const path = supervisorStatePath(stateDir); const temporary = `${path}.tmp`; writeFileSync(temporary, body, { encoding: "utf8", mode: 0o600 }); renameSync(temporary, path); try { chmodSync(path, 0o600); } catch { /* Windows may not expose POSIX modes. */ } } export function applyObservation( state: SupervisorState, observation: SupervisorObservation, now: string, ): SupervisorState | "lane_unit_invalid" | "supervisor_pr_invalid" | "supervisor_ci_invalid" | "supervisor_backoff_invalid" { if (!kxmResourceIdentifier(observation.unit)) return "lane_unit_invalid"; if (!isSupervisorTimestamp(now)) return "supervisor_backoff_invalid"; if (observation.pr) { if (!Number.isInteger(observation.pr.number) || observation.pr.number <= 0 || !MERGE_STATE_RE.test(observation.pr.mergeState)) { return "supervisor_pr_invalid"; } } if (observation.ci) { if (!CI_TOKEN_RE.test(observation.ci.status) || !CI_TOKEN_RE.test(observation.ci.conclusion)) return "supervisor_ci_invalid"; } if (observation.backoffMs !== undefined) { if (!Number.isInteger(observation.backoffMs) || observation.backoffMs < 0 || observation.backoffMs > MAX_BACKOFF_MS) { return "supervisor_backoff_invalid"; } } const prior = state.lanes[observation.unit]; const next: SupervisorLane = { inFlight: observation.inFlight }; if (observation.pr) next.pr = { number: observation.pr.number, mergeState: observation.pr.mergeState, seenAt: now }; else if (prior?.pr) next.pr = { ...prior.pr }; if (observation.ci) next.ci = { status: observation.ci.status, conclusion: observation.ci.conclusion, seenAt: now }; else if (prior?.ci) next.ci = { ...prior.ci }; if (observation.backoffMs === undefined) { if (prior?.backoffUntil) next.backoffUntil = prior.backoffUntil; } else if (observation.backoffMs > 0) { next.backoffUntil = new Date(Date.parse(now) + observation.backoffMs).toISOString(); } return { schema: SUPERVISOR_SCHEMA, updatedAt: now, lanes: { ...state.lanes, [observation.unit]: next }, }; } export interface SupervisorTick { state: SupervisorState; resumed: string[]; waiting: string[]; } /** Keep every lane. A backoff that has elapsed is cleared so the lane can be * acted on again. A backoff still in the future is left untouched. */ export function resumeTick(state: SupervisorState, now: string): SupervisorTick { const nowMs = Date.parse(now); const lanes: Record<string, SupervisorLane> = {}; const resumed: string[] = []; const waiting: string[] = []; for (const unit of Object.keys(state.lanes).sort()) { const lane = state.lanes[unit]; if (!lane) continue; const until = lane.backoffUntil === undefined ? undefined : Date.parse(lane.backoffUntil); const next: SupervisorLane = { inFlight: lane.inFlight }; if (lane.pr) next.pr = { ...lane.pr }; if (lane.ci) next.ci = { ...lane.ci }; if (until !== undefined && until > nowMs && lane.backoffUntil !== undefined) { next.backoffUntil = lane.backoffUntil; waiting.push(unit); } else if (until !== undefined) { resumed.push(unit); } lanes[unit] = next; } return { state: { schema: SUPERVISOR_SCHEMA, updatedAt: now, lanes }, resumed, waiting, }; } export function formatTickLine(now: string, resumed: readonly string[], waiting: readonly string[]): string { const clock = now.slice(11, 16); if (resumed.length === 0 && waiting.length === 0) { return `[supervisor]: no change. No action. - (${clock})`; } const parts: string[] = []; if (resumed.length > 0) parts.push(`resumed ${resumed.join(", ")}`); if (waiting.length > 0) parts.push(`waiting ${waiting.join(", ")}`); const acted = resumed.length > 0 ? `Acted: resume ${resumed.join(", ")}` : "No action"; return `[supervisor]: ${parts.join("; ")}. ${acted}. - (${clock})`; }