UNPKG

@kontextmind/kxm

Version:

KXM local-first multi-agent orchestration and operator dashboard

1,449 lines • 64.2 kB
import { spawn } from "node:child_process";
import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
import { dirname, isAbsolute, join } from "node:path";
import { findKxmRepoRoot } from "./repo-root.ts";
import { loadKxmProject, KxmConfigError, type KxmConfigOptions } from "./project-config.ts";
import {
  KxmRunEventStore,
  KxmRuntimeRegistry,
  projectRuntimeKey,
  readKxmSupervisorRecord,
  runtimeError,
  verifyKxmDriveReceipt,
  kxmRuntimePaths,
  type KxmRuntimePaths,
  type KxmSupervisorRecord,
} from "./runtime-store.ts";
import { openReadOnlyDatabase } from "./sqlite.ts";
import {
  acceptKxmRun,
  cancelKxmRun,
  closeKxmRuntimeContext,
  foldStoredKxmRun,
  openKxmRuntimeContext,
  kxmProjectAdmissionLimits,
  projectKxmRunReadOnly,
  type KxmRuntimeContext,
} from "./runtime-service.ts";
import { createKxmOneShotProducer } from "./oneshot-producer.ts";
import { KxmRunScheduler, createKxmSimulatedProducer, recordDriveReceipt, recoverKxmRun, kxmDrivePollProjection } from "./engine.ts";
import { kxmDriveSession, kxmOpenDriveSessions } from "./runtime-owner.ts";
import { RuntimeHubClient, HubHttpError, type SyncPushResponse } from "./client.ts";
import { CloudTokenError, HubBindingError, loadHubBinding } from "./hub-binding.ts";
import { resolveClientHubAuthToken } from "./hub-env.ts";
import { createLogger } from "./logger.ts";
import type { KxmOutboxRow, KxmOutboxStatus } from "./runtime-store.ts";

/* ------------------------------------------------------------------ *
 * Token management
 * ------------------------------------------------------------------ */

export function kxmSupervisorTokenFile(paths: KxmRuntimePaths): string {
  return join(paths.runtimeDir, "supervisor.token");
}

/** Atomically publish the supervisor token (temp file + rename). */
function publishKxmSupervisorToken(paths: KxmRuntimePaths, token: string): void {
  const file = kxmSupervisorTokenFile(paths);
  mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
  const temp = `${file}.${process.pid}.tmp`;
  writeFileSync(temp, `${token}\n`, { encoding: "utf8", mode: 0o600 });
  try { chmodSync(temp, 0o600); } catch { /* Windows may ignore modes */ }
  renameSync(temp, file);
  try { chmodSync(file, 0o600); } catch { /* Windows may ignore modes */ }
}

export function hashKxmSupervisorToken(token: string): string {
  return `sha256:${createHash("sha256").update(`kxm-runtime-supervisor\0${token}`, "utf8").digest("hex")}`;
}

export function readKxmSupervisorToken(paths: KxmRuntimePaths): string | undefined {
  const file = kxmSupervisorTokenFile(paths);
  const stat = lstatSync(file, { throwIfNoEntry: false });
  if (!stat) return undefined;
  if (stat.isSymbolicLink() || !stat.isFile()) {
    throw runtimeError("runtime_path_invalid", file, "supervisor token file must be a regular file, not a link");
  }
  const token = readFileSync(file, "utf8").trim();
  return token.length >= 32 ? token : undefined;
}

/* ------------------------------------------------------------------ *
 * Supervisor process state + liveness
 * ------------------------------------------------------------------ */

export interface KxmSupervisorStatus {
  running: boolean;
  runtimeId?: string;
  pid?: number;
  port?: number;
  state?: string;
  heartbeatAt?: string;
  startedAt?: string;
}

function processAlive(pid: number): boolean {
  try {
    process.kill(pid, 0);
    return true;
  } catch (error) {
    return (error as NodeJS.ErrnoException).code === "EPERM";
  }
}

const HEARTBEAT_STALE_MS = 15_000;
const SUPERVISOR_ERROR_MAX_AGE_MS = 30_000;

function supervisorErrorFile(paths: KxmRuntimePaths): string {
  return join(paths.runtimeDir, "supervisor.error");
}

function clearSupervisorError(paths: KxmRuntimePaths): void {
  const file = supervisorErrorFile(paths);
  if (existsSync(file)) rmSync(file, { force: true });
}

function recordSupervisorError(paths: KxmRuntimePaths, message: string): void {
  try {
    mkdirSync(paths.runtimeDir, { recursive: true, mode: 0o700 });
    writeFileSync(supervisorErrorFile(paths), `${message}\n`, { encoding: "utf8", mode: 0o600 });
  } catch { /* best effort */ }
}

function readRecentSupervisorError(paths: KxmRuntimePaths): string | undefined {
  const file = supervisorErrorFile(paths);
  const stat = lstatSync(file, { throwIfNoEntry: false });
  if (!stat) return undefined;
  const ageMs = Date.now() - stat.mtimeMs;
  if (ageMs > SUPERVISOR_ERROR_MAX_AGE_MS) return undefined;
  try {
    return readFileSync(file, "utf8").trim();
  } catch {
    return undefined;
  }
}

const SETTLED_RUN_STATUS = new Set(["completed", "failed", "cancelled"]);

/** Same liveness rule as `kxmSupervisorStatus`: running, a fresh heartbeat, and a live pid. */
export function kxmSupervisorRecordIsLive(record: KxmSupervisorRecord | undefined): boolean {
  return supervisorStatusOf(record).running;
}

function unsettledRunIds(context: KxmRuntimeContext): string[] {
  const ids: string[] = [];
  for (const stored of context.eventStore.runsForProject(context.projectId, 10_000)) {
    let status: string = stored.status;
    try {
      status = projectKxmRunReadOnly(context, stored.runId).status;
    } catch {
      ids.push(stored.runId);
      continue;
    }
    if (!SETTLED_RUN_STATUS.has(status)) ids.push(stored.runId);
  }
  return ids;
}

function supervisorStatusOf(record: KxmSupervisorRecord | undefined): KxmSupervisorStatus {
  if (!record) return { running: false };
  // Pid-only liveness is not enough: after a crash the pid may be reused by
  // an unrelated process. A stale heartbeat is authoritative.
  const heartbeatAgeMs = Date.now() - Date.parse(record.heartbeatAt);
  const fresh = Number.isFinite(heartbeatAgeMs) && heartbeatAgeMs < HEARTBEAT_STALE_MS;
  const alive = record.state === "running" && fresh && processAlive(record.pid);
  return {
    running: alive,
    runtimeId: record.runtimeId,
    pid: record.pid,
    port: record.port,
    state: alive ? record.state : "dead",
    heartbeatAt: record.heartbeatAt,
    startedAt: record.startedAt,
  };
}

/**
 * Is the supervisor up? `readOnly` gives the same answer without opening the
 * registry for writing: the ordinary open takes the write lock and, as the last
 * connection, checkpoints the WAL on close. `kxm restore` asks this way, dry run
 * or not, because a project-scoped restore never otherwise touches the registry.
 */
export function kxmSupervisorStatus(paths: KxmRuntimePaths, options: { readOnly?: boolean } = {}): KxmSupervisorStatus {
  if (!existsSync(paths.registryDb)) return { running: false };
  if (options.readOnly) {
    const database = openReadOnlyDatabase(paths.registryDb);
    try {
      database.exec("PRAGMA busy_timeout = 5000");
      return supervisorStatusOf(readKxmSupervisorRecord(database));
    } finally {
      database.close();
    }
  }
  const registry = new KxmRuntimeRegistry(paths.registryDb);
  try {
    return supervisorStatusOf(registry.supervisor());
  } finally {
    registry.close();
  }
}

async function probeSupervisorWithRetry(port: number, expectedRuntimeId: string, token: string): Promise<boolean> {
  for (let attempt = 0; attempt < 3; attempt += 1) {
    if (await probeSupervisor(port, expectedRuntimeId, token)) return true;
    if (attempt < 2) await new Promise((resolveWait) => setTimeout(resolveWait, 250));
  }
  return false;
}

async function probeSupervisor(port: number, expectedRuntimeId: string, token: string, timeoutMs = 750): Promise<boolean> {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
  try {
    const nonce = randomBytes(16).toString("hex");
    const response = await fetch(`http://127.0.0.1:${port}/healthz?nonce=${nonce}`, { signal: controller.signal });
    if (!response.ok) return false;
    const payload = await response.json() as { runtimeId?: string; tokenProof?: string };
    // The server must prove it knows the current token: an impostor that
    // merely binds the stale port cannot answer the keyed challenge.
    const expectedProof = hashKxmTokenProof(token, nonce);
    return payload.runtimeId === expectedRuntimeId && payload.tokenProof === expectedProof;
  } catch {
    return false;
  } finally {
    clearTimeout(timeout);
  }
}

export function hashKxmTokenProof(token: string, nonce: string): string {
  return createHmac("sha256", token).update(`kxm-runtime-token-proof\0${nonce}`, "utf8").digest("hex");
}

export interface KxmSupervisorHandle {
  runtimeId: string;
  port: number;
  token: string;
  started: boolean;
}

/**
 * Attach to a live supervisor, or return `undefined`. Never starts one.
 *
 * `ensureKxmSupervisor` is for operators: an absent supervisor is a problem to fix, so it
 * spawns. A poller — the portal tenant read, a status screen — has the opposite contract:
 * polling must not conjure a daemon, and "nothing is running" is an answer to report, not
 * a condition to repair. Returns `undefined` for every not-running shape: no claim, a
 * supervisor registered but dead, a missing token file, or a probe that fails the
 * token-proof challenge.
 */
export async function attachKxmSupervisor(
  options: { stateRoot?: string; env?: NodeJS.ProcessEnv } = {},
): Promise<KxmSupervisorHandle | undefined> {
  const paths = kxmRuntimePaths(options.stateRoot !== undefined ? { stateRoot: options.stateRoot } : { ...(options.env ? { env: options.env } : {}) });
  const status = kxmSupervisorStatus(paths);
  if (!status.running || !status.port || !status.runtimeId) return undefined;
  const token = readKxmSupervisorToken(paths);
  if (!token) return undefined;
  if (!(await probeSupervisor(status.port, status.runtimeId, token))) return undefined;
  return { runtimeId: status.runtimeId, port: status.port, token, started: false };
}

/** Ensure a supervisor is running: reuse a live one, otherwise auto-start. */
export async function ensureKxmSupervisor(
  options: { stateRoot?: string; env?: NodeJS.ProcessEnv; spawnImpl?: (scriptPath: string, env: NodeJS.ProcessEnv) => number } = {},
): Promise<KxmSupervisorHandle> {
  const paths = kxmRuntimePaths(options.stateRoot !== undefined ? { stateRoot: options.stateRoot } : { ...(options.env ? { env: options.env } : {}) });
  const status = kxmSupervisorStatus(paths);
  if (status.running && status.port) {
    for (let attempt = 0; attempt < 3; attempt += 1) {
      const token = readKxmSupervisorToken(paths);
      if (token && (await probeSupervisorWithRetry(status.port, status.runtimeId as string, token))) {
        return { runtimeId: status.runtimeId as string, port: status.port, token, started: false };
      }
      // The token may be mid-publish in the claim/rename window; retry shortly.
      if (attempt < 2) await new Promise((resolveWait) => setTimeout(resolveWait, 150));
    }
    // A live, fresh supervisor that cannot be probed is not spawnable:
    // spawning a competitor guarantees a conflict and locks out the state root.
    throw runtimeError("runtime_supervisor_unreachable", paths.registryDb, `runtime supervisor pid ${status.pid} is registered as running but cannot be probed`);
  }

  // Auto-start: claim the singleton, spawn detached, wait for readiness.
  clearSupervisorError(paths);
  const scriptPath = join(findKxmRepoRoot(import.meta.url), "scripts", "kxm-runtime-supervisor.mjs");
  const spawnImpl = options.spawnImpl ?? ((script: string, env: NodeJS.ProcessEnv): number => {
    const child = spawn(process.execPath, [script], {
      detached: true,
      stdio: "ignore",
      env,
      windowsHide: true,
    });
    child.unref();
    if (child.pid === undefined) throw runtimeError("runtime_supervisor_spawn_failed", script, "could not spawn the runtime supervisor");
    return child.pid;
  });
  const pid = spawnImpl(scriptPath, {
    ...process.env,
    KXM_STATE_HOME: paths.stateRoot,
  });

  const deadline = Date.now() + 10_000;
  while (Date.now() < deadline) {
    await new Promise((resolveWait) => setTimeout(resolveWait, 100));
    const next = kxmSupervisorStatus(paths);
    if (next.running && next.port) {
      const token = readKxmSupervisorToken(paths);
      if (token && (await probeSupervisor(next.port, next.runtimeId as string, token))) {
        // Entry status had no live supervisor: this call caused a start
        // (ours or a concurrent winner's), which the probe just confirmed.
        return { runtimeId: next.runtimeId as string, port: next.port, token, started: true };
      }
    }
    const errorDetail = readRecentSupervisorError(paths);
    if (errorDetail) {
      throw runtimeError("runtime_supervisor_start_failed", scriptPath, `runtime supervisor failed at startup: ${errorDetail}`);
    }
  }
  throw runtimeError("runtime_supervisor_start_failed", scriptPath, `runtime supervisor (pid ${pid}) did not become ready in time`);
}

/* ------------------------------------------------------------------ *
 * Supervisor server (runs inside the supervisor process)
 * ------------------------------------------------------------------ */

interface JsonBody {
  [key: string]: unknown;
}

async function readJsonBody(request: IncomingMessage, limit = 64 * 1024): Promise<JsonBody> {
  const chunks: Buffer[] = [];
  let size = 0;
  let tooLarge = false;
  for await (const chunk of request) {
    const buffer = chunk as Buffer;
    size += buffer.byteLength;
    if (size > limit) {
      // Drain the remaining body so the client sees the 400 response instead
      // of a reset connection.
      tooLarge = true;
      continue;
    }
    if (!tooLarge) chunks.push(buffer);
  }
  if (tooLarge) throw runtimeError("runtime_request_too_large", request.url ?? "<request>", "request body exceeds the limit");
  if (chunks.length === 0) return {};
  try {
    const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8")) as unknown;
    if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
      throw runtimeError("runtime_request_invalid", request.url ?? "<request>", "request body must be a JSON object");
    }
    return parsed as JsonBody;
  } catch (error) {
    if (error instanceof KxmConfigError) throw error;
    throw runtimeError("runtime_request_invalid", request.url ?? "<request>", "request body is not valid JSON");
  }
}

function sendJson(response: ServerResponse, status: number, payload: unknown): void {
  const body = JSON.stringify(payload);
  response.writeHead(status, { "content-type": "application/json", "content-length": Buffer.byteLength(body) });
  response.end(body);
}

export const DEFAULT_RUNTIME_STOP_GRACE_MS = 30_000;
/** Documented upper bound: 10 minutes, well under Node's 2^31-1 ms timer range. */
export const MAX_RUNTIME_STOP_GRACE_MS = 600_000;

export function runtimeStopGraceMs(env: NodeJS.ProcessEnv = process.env): number {
  const raw = env.KXM_RUNTIME_STOP_GRACE_MS;
  if (raw === undefined || raw.trim() === "") return DEFAULT_RUNTIME_STOP_GRACE_MS;
  const parsed = Number(raw.trim());
  if (
    !Number.isFinite(parsed)
    || !Number.isInteger(parsed)
    || parsed < 0
    || parsed > MAX_RUNTIME_STOP_GRACE_MS
  ) {
    process.stderr.write(
      `KXM_RUNTIME_STOP_GRACE_MS must be an integer between 0 and ${MAX_RUNTIME_STOP_GRACE_MS} ms (10 minutes); using default ${DEFAULT_RUNTIME_STOP_GRACE_MS}\n`,
    );
    return DEFAULT_RUNTIME_STOP_GRACE_MS;
  }
  return parsed;
}

async function waitForDriveSessions(settled: Promise<unknown>[], graceMs: number): Promise<void> {
  if (settled.length === 0) return;
  let timeout: ReturnType<typeof setTimeout> | undefined;
  try {
    await Promise.race([
      Promise.allSettled(settled),
      new Promise<void>((resolve) => {
        timeout = setTimeout(resolve, graceMs);
        timeout.unref?.();
      }),
    ]);
  } finally {
    if (timeout !== undefined) clearTimeout(timeout);
  }
}

async function driveSessionStillPending(settled: Promise<unknown>): Promise<boolean> {
  let pending = true;
  void settled.then(
    () => { pending = false; },
    () => { pending = false; },
  );
  await Promise.resolve();
  return pending;
}

/* ------------------------------------------------------------------ *
 * Runtime → hub sync (P5): outbound only
 * ------------------------------------------------------------------ */

export const DEFAULT_RUNTIME_SYNC_INTERVAL_MS = 10_000;
const MIN_RUNTIME_SYNC_INTERVAL_MS = 250;
const MAX_RUNTIME_SYNC_INTERVAL_MS = 60_000;
const OUTBOX_PUSH_BATCH = 32;
/** Hub body ceiling is 256 KiB; leave headroom for the JSON envelope. */
const OUTBOX_PUSH_BATCH_BYTES = 200_000;
/** How long the sync tick stops knocking on a hub that keeps failing. */
const RUNTIME_SYNC_MAX_BACKOFF_MS = 5 * 60_000;

/** How often the supervisor heartbeats and pushes its outbox. Keep it well
 * under the hub's presence lease (30 s by default). */
export function runtimeSyncIntervalMs(env: NodeJS.ProcessEnv = process.env): number {
  const raw = env.KXM_RUNTIME_SYNC_INTERVAL_MS?.trim();
  if (!raw) return DEFAULT_RUNTIME_SYNC_INTERVAL_MS;
  const parsed = Number(raw);
  if (!Number.isInteger(parsed)) return DEFAULT_RUNTIME_SYNC_INTERVAL_MS;
  return Math.min(MAX_RUNTIME_SYNC_INTERVAL_MS, Math.max(MIN_RUNTIME_SYNC_INTERVAL_MS, parsed));
}

export interface KxmOutboxRefusal {
  runId: string;
  sequence: number;
  code: string;
}

export interface KxmOutboxSyncResult {
  /** Rows carried to the hub in this pass, including rows later refused. */
  pushed: number;
  /** Rows the hub accepted or already held — the cursor advanced. */
  acked: number;
  /** Rows the hub refused in a way re-sending cannot change. */
  refused: number;
  /** Per-row refusals, oldest first, carrying the hub's own code. */
  refusals: KxmOutboxRefusal[];
  /** Rows the hub gave no answer for. Left pending, never acked. */
  unconfirmed: number;
  /** A transport failure ended the pass; the rest of the queue stays pending. */
  blocked: boolean;
  /** Why the pass was blocked, already reduced to an operator-safe line. */
  blockedReason?: string;
}

/** A hub answer that will not change if the same bytes are sent again. */
function isDurableRefusal(outcome: string): boolean {
  return outcome === "conflict" || outcome === "rejected";
}

/** An HTTP refusal that says "this batch is too big", not "try again later". */
function isOversizeRefusal(error: unknown): boolean {
  return error instanceof HubHttpError
    && (error.statusCode === 413 || error.code === "sync_batch_too_large" || error.code === "payload_too_large");
}

/**
 * Push every pending outbox row to the bound hub in outbox order.
 *
 * Accepted and duplicate rows are acked. A row the hub **durably** refuses — a
 * sequence already used with other bytes, a project id another hub project
 * claimed, an event that fails the sync schema — is refused locally too: it
 * leaves the pending queue with the hub's code recorded, because re-sending the
 * same bytes can only re-raise the same alert. That is the difference between a
 * retry and a stampede, and it is what keeps one unreachable row from parking
 * every row behind it.
 *
 * A transport failure marks the pass `blocked` and returns: the rows stay
 * pending for a real retry, with the failure carried back to the caller so it
 * can be logged and backed off instead of swallowed.
 */
export async function syncKxmOutbox(
  eventStore: KxmRunEventStore,
  client: RuntimeHubClient,
  options: { now?: () => string; batchSize?: number } = {},
): Promise<KxmOutboxSyncResult> {
  const now = options.now ?? (() => new Date().toISOString());
  const batchSize = Math.max(1, options.batchSize ?? OUTBOX_PUSH_BATCH);
  const result: KxmOutboxSyncResult = { pushed: 0, acked: 0, refused: 0, refusals: [], unconfirmed: 0, blocked: false };

  const refuse = (row: KxmOutboxRow, code: string): void => {
    if (eventStore.refuseOutbox([{ seq: row.seq, code }], now()) === 0) return;
    result.refused += 1;
    result.refusals.push({ runId: row.runId, sequence: row.sequence, code });
  };

  /**
   * Deliver one batch. Returns false only for a transient failure, where every
   * row of the batch stays pending. One result per event, in order, is the
   * hub's contract, so the answer for a row is found at its index and then
   * verified against the row it claims to describe.
   */
  const deliver = async (batch: readonly KxmOutboxRow[]): Promise<boolean> => {
    const payload: unknown[] = [];
    const sendable: KxmOutboxRow[] = [];
    for (const row of batch) {
      try {
        payload.push(JSON.parse(row.syncEvent) as unknown);
        sendable.push(row);
      } catch {
        // Written by the transform, so unparseable bytes are local corruption —
        // durable, visible, and never a reason to stop the queue behind it.
        refuse(row, "sync_row_unreadable");
      }
    }
    if (sendable.length === 0) return true;
    eventStore.markOutboxAttempted(sendable.map((row) => row.seq), now());
    let response: SyncPushResponse;
    try {
      response = await client.pushSyncEvents(payload);
    } catch (error) {
      if (isOversizeRefusal(error)) {
        if (sendable.length > 1) {
          // Isolate the offender one row at a time so the batch never becomes
          // a permanent head-of-line block.
          for (const row of sendable) {
            if (!(await deliver([row]))) return false;
          }
          return true;
        }
        refuse(sendable[0]!, "sync_row_too_large");
        return true;
      }
      result.blockedReason = syncFailureText(error);
      return false;
    }
    result.pushed += sendable.length;
    const acked: number[] = [];
    sendable.forEach((row, index) => {
      const entry = response.results[index];
      const describesRow = entry !== undefined
        && (entry.runId === undefined || entry.runId === row.runId)
        && (entry.sequence === undefined || entry.sequence === row.sequence);
      if (!entry || !describesRow) {
        result.unconfirmed += 1;
        return;
      }
      if (entry.outcome === "accepted" || entry.outcome === "duplicate") acked.push(row.seq);
      else if (isDurableRefusal(entry.outcome)) refuse(row, entry.code ?? `sync_${entry.outcome}`);
      else result.unconfirmed += 1;
    });
    result.acked += eventStore.ackOutbox(acked, now());
    return true;
  };

  let afterSeq = 0;
  for (;;) {
    const rows = eventStore.pendingOutbox(batchSize, afterSeq);
    if (rows.length === 0) return result;
    // Batch by serialized byte size, not just count: the hub rejects requests
    // over its body ceiling (HTTP 413), and retrying the same oversized batch
    // would permanently block the queue. Trim to the byte budget and leave the
    // rest for the next iteration.
    let byteBudget = OUTBOX_PUSH_BATCH_BYTES;
    let sendCount = 0;
    for (const row of rows) {
      const rowBytes = Buffer.byteLength(row.syncEvent, "utf8") + 64; // JSON overhead
      if (sendCount > 0 && byteBudget - rowBytes < 0) break;
      byteBudget -= rowBytes;
      sendCount += 1;
    }
    const batch = rows.slice(0, Math.max(1, sendCount));
    afterSeq = batch[batch.length - 1]!.seq;
    if (!(await deliver(batch))) {
      result.blocked = true;
      return result;
    }
  }
}

/** Where this Runtime reports one project, or nothing when no hub is bound. */
function runtimeHubClientFor(
  context: KxmRuntimeContext,
  env: NodeJS.ProcessEnv,
  onBindingWarning?: (message: string) => void,
): RuntimeHubClient | undefined {
  let recordUrl: string | undefined;
  try {
    const loaded = loadHubBinding(env);
    if (loaded.warning) onBindingWarning?.(loaded.warning);
    recordUrl = loaded.record?.url;
  } catch (error) {
    if (!(error instanceof HubBindingError)) throw error;
    onBindingWarning?.(`${error.message}; falling back to the local hub so sync can continue`);
  }
  const serverUrl = env.KXM_SERVER_URL?.trim() || recordUrl;
  if (!serverUrl) return undefined;
  // Use the context's actual projectId — the same identity sync events carry — so the
  // ops snapshot can join runs to their home Runtime. The npm package name (what an
  // earlier build sent) never matches project.yaml's prj_* id: presence lands under one
  // label and runs under the other, and the hub pins the project id to the first label
  // it ever saw, which refuses every later push. See the supervisor sync gate.
  const project = context.projectId;
  const authToken = resolveClientHubAuthToken(env, project);
  return new RuntimeHubClient({
    serverUrl,
    project,
    runtimeId: context.homeRuntimeId,
    ...(authToken ? { authToken } : {}),
  });
}

/* ------------------------------------------------------------------ *
 * Sync status: what the tick saw, readable without opening SQLite
 * ------------------------------------------------------------------ */

/**
 * One project's outbound sync state as the supervisor last observed it.
 *
 * `state` is the answer to "is this Runtime talking to the hub?":
 * `ok` (rows are acking), `no_hub` (nothing bound — keeping rows locally is the
 * design), `blocked` (a transport or credential failure; retryable, backing
 * off), or `refusing` (the hub answered durably; an operator has to change
 * something). `blocked` and `refusing` are the two states that mean run facts
 * are not reaching the hub.
 */
export interface KxmProjectSyncStatus {
  projectId: string;
  projectRoot: string;
  homeRuntimeId: string;
  state: "ok" | "no_hub" | "blocked" | "refusing";
  outbox: KxmOutboxStatus;
  consecutiveFailures: number;
  /**
   * False when the counts above are zeros because the store could not be opened
   * at all — a refused schema is not an empty outbox, and reporting the two the
   * same way is how a blocked project reads as "nothing to do".
   */
  storeReadable?: boolean;
  hubUrl?: string;
  lastCompletedAt?: string;
  lastPushed?: number;
  lastAcked?: number;
  lastRefused?: number;
  lastUnconfirmed?: number;
  lastError?: string;
  nextAttemptAt?: string;
}

/** Exponential backoff on consecutive stalls, capped so a hub outage heals by itself. */
function syncBackoffMs(consecutiveStalls: number, intervalMs: number): number {
  if (consecutiveStalls <= 0) return 0;
  return Math.min(RUNTIME_SYNC_MAX_BACKOFF_MS, intervalMs * 2 ** Math.min(5, consecutiveStalls - 1));
}

/** A failure worth showing an operator: bounded, and never the bearer token. */
function syncFailureText(error: unknown): string {
  const code = error instanceof HubHttpError ? `${error.statusCode} ${error.code ?? "error"}` : undefined;
  const message = error instanceof Error ? error.message : String(error);
  return `${code ? `${code}: ` : ""}${message}`.replace(/(kxm_[A-Za-z0-9_]+|[A-Za-z0-9._-]{40,})/g, "[redacted]").slice(0, 300);
}

function runtimeSupervisorLogFile(paths: KxmRuntimePaths): string {
  return join(paths.runtimeDir, "logs", "kxm-runtime.jsonl");
}

export interface KxmRuntimeSupervisor {
  server: Server;
  port: number;
  runtimeId: string;
  stop: () => Promise<void>;
}

export async function startKxmRuntimeSupervisor(
  options: { stateRoot?: string; port?: number; now?: () => string; env?: NodeJS.ProcessEnv } = {},
): Promise<KxmRuntimeSupervisor> {
  const now = options.now ?? (() => new Date().toISOString());
  const env = options.env ?? process.env;
  const paths = kxmRuntimePaths(options.stateRoot !== undefined ? { stateRoot: options.stateRoot } : {});
  try {
    return await startKxmRuntimeSupervisorInner(paths, options.port, now, env);
  } catch (error) {
    // Startup failures are recorded so auto-start clients see the real cause
    // instead of a bare timeout. Conflict is not an error: the winner is
    // probed and reused instead.
    if (!(error instanceof KxmConfigError && error.issues.some((issue) => issue.code === "runtime_supervisor_conflict"))) {
      recordSupervisorError(paths, (error as Error).message);
    }
    throw error;
  }
}

async function startKxmRuntimeSupervisorInner(
  paths: KxmRuntimePaths,
  requestedPortOption: number | undefined,
  now: () => string,
  env: NodeJS.ProcessEnv,
): Promise<KxmRuntimeSupervisor> {
  mkdirSync(paths.runtimeDir, { recursive: true, mode: 0o700 });
  mkdirSync(paths.projectsDir, { recursive: true, mode: 0o700 });
  // The supervisor is spawned detached with stdio ignored, so this file is its
  // only durable voice. Without it, a sync path that refuses every row is
  // indistinguishable from a sync path that never ran.
  const logger = createLogger({
    component: "runtime",
    path: runtimeSupervisorLogFile(paths),
    stdout: false,
    maxBytes: 2 * 1024 * 1024,
  });
  // The token is generated in memory and written only after the supervisor
  // singleton is owned: a losing supervisor can never overwrite a winner's
  // token and lock every client out.
  const token = randomBytes(32).toString("hex");
  const tokenHash = hashKxmSupervisorToken(token);
  const registry = new KxmRuntimeRegistry(paths.registryDb);

  const runtimeId = `rtm_${createHash("sha256").update(`${paths.stateRoot}\0${process.pid}\0${now()}\0${randomBytes(16).toString("hex")}`, "utf8").digest("hex").slice(0, 24)}`;
  const requestedPort = requestedPortOption ?? 0;
  let activeRuntimeId = runtimeId;

  const contexts = new Map<string, KxmRuntimeContext>();
  // Declared with the contexts rather than with the timer that fills it: the
  // listener is up before the tick is armed, and `GET /v1/sync/status` must be
  // answerable in that window rather than reach for a binding that is not there
  // yet.
  const syncStatuses = new Map<string, KxmProjectSyncStatus>();
  const registerSyncCredentials = (context: KxmRuntimeContext): void => {
    // Register credentials on the store's redactor at context creation —
    // BEFORE any event can be appended — so the very first outbox row is
    // already scrubbed. Registering on the sync tick leaves a window where
    // appended events retain credentials.
    try {
      const hubToken = resolveClientHubAuthToken(env, context.projectId);
      if (hubToken) context.eventStore.syncRedactor.register(hubToken);
    } catch (error) {
      // A missing cloud token must not stop the local Runtime from opening.
      // The sync tick records the same failure as blocked.
      if (!(error instanceof CloudTokenError)) throw error;
    }
    for (const key of Object.keys(env)) {
      if ((key.startsWith("KXM_") && (key.endsWith("_TOKEN") || key.endsWith("_KEY"))) || key.endsWith("_API_KEY") || key.endsWith("_SECRET")) {
        const value = env[key]?.trim();
        if (value) context.eventStore.syncRedactor.register(value);
      }
    }
  };

  const contextFor = (projectRoot: string): KxmRuntimeContext => {
    if (!isAbsolute(projectRoot)) {
      throw runtimeError("runtime_request_invalid", "projectRoot", "projectRoot must be an absolute path");
    }
    const key = projectRuntimeKey(projectRoot);
    const existing = contexts.get(key);
    if (existing) return existing;
    const context = openKxmRuntimeContext(projectRoot, { homeRuntimeId: activeRuntimeId, stateRoot: paths.stateRoot, logger });
    registerSyncCredentials(context);
    contexts.set(key, context);
    return context;
  };

  const requireAuth = (request: IncomingMessage): void => {
    const header = request.headers.authorization ?? "";
    const presented = header.startsWith("Bearer ") ? header.slice("Bearer ".length) : "";
    const presentedHash = presented.length > 0 ? Buffer.from(hashKxmSupervisorToken(presented), "utf8") : Buffer.alloc(0);
    const expectedHash = Buffer.from(tokenHash, "utf8");
    if (presentedHash.length !== expectedHash.length || !timingSafeEqual(presentedHash, expectedHash)) {
      throw runtimeError("runtime_auth_failed", request.url ?? "<request>", "missing or invalid supervisor token");
    }
  };

  const server = createServer((request, response) => {
    void (async () => {
      try {
        const url = new URL(request.url ?? "/", "http://127.0.0.1");
        if (request.method === "GET" && url.pathname === "/healthz") {
          const nonce = url.searchParams.get("nonce") ?? "";
          sendJson(response, 200, {
            ok: true,
            runtimeId: activeRuntimeId,
            state: "running",
            // Proof of token knowledge: an impostor that merely binds the
            // stale port cannot answer the keyed challenge.
            tokenProof: nonce.length > 0 ? hashKxmTokenProof(token, nonce) : undefined,
          });
          return;
        }
        requireAuth(request);

        if (request.method === "POST" && url.pathname === "/v1/shutdown") {
          sendJson(response, 200, { ok: true, stopping: true });
          setImmediate(() => { void stop(); });
          return;
        }

        // What the sync loop last saw, per project this Runtime owns. This is the
        // surface that makes "the supervisor is running clean" mean something:
        // pending, acked and refused rows, the hub's refusal codes, and when the
        // next attempt is due.
        if (request.method === "GET" && url.pathname === "/v1/sync/status") {
          const projectRoot = url.searchParams.get("projectRoot");
          const statuses = [...syncStatuses.values()]
            .filter((status) => projectRoot === null || status.projectRoot === projectRoot)
            .sort((left, right) => left.projectId.localeCompare(right.projectId));
          sendJson(response, 200, { ok: true, runtimeId: activeRuntimeId, projects: statuses });
          return;
        }

        // Operator-only revive: a durably refused row stays refused until someone
        // says the hub-side state has been corrected. The Runtime never decides on
        // its own that a refusal has become retryable, because the refusal is
        // usually the hub holding a claim this Runtime cannot see.
        if (request.method === "POST" && url.pathname === "/v1/sync/retry") {
          const body = await readJsonBody(request);
          const projectRoot = typeof body.projectRoot === "string" ? body.projectRoot : "";
          const context = contextFor(projectRoot);
          const key = projectRuntimeKey(context.projectRoot);
          const retried = context.eventStore.retryRefusedOutbox();
          const status = syncStatuses.get(key);
          // Clear the backoff gate so the rows go out on the next tick, and let
          // that tick re-derive the state from what the hub actually says.
          if (status) {
            const cleared: KxmProjectSyncStatus = { ...status, outbox: context.eventStore.outboxStatus(), consecutiveFailures: 0 };
            delete (cleared as Partial<KxmProjectSyncStatus>).nextAttemptAt;
            syncStatuses.set(key, cleared);
          }
          logger({ event: "runtime_sync_retry", projectId: context.projectId, retried });
          sendJson(response, 200, { ok: true, projectId: context.projectId, retried, outbox: context.eventStore.outboxStatus() });
          return;
        }

        if (request.method === "POST" && url.pathname === "/v1/projects/unregister") {
          const body = await readJsonBody(request);
          const projectRoot = typeof body.projectRoot === "string" ? body.projectRoot : "";
          const force = body.force === true;
          if (!projectRoot || !isAbsolute(projectRoot)) {
            sendJson(response, 400, { ok: false, error: "runtime_request_invalid", message: "projectRoot must be an absolute path" });
            return;
          }
          if (!force) {
            const key = projectRuntimeKey(projectRoot);
            const existing = contexts.get(key);
            let openRunIds: string[] = [];
            if (existing) {
              openRunIds = unsettledRunIds(existing);
            } else {
              const registration = registry.projectByRoot(projectRoot);
              const eventsPath = join(paths.projectsDir, registration?.projectKey ?? projectRuntimeKey(projectRoot), "run-events.db");
              if (registration && existsSync(eventsPath)) {
                const store = new KxmRunEventStore(eventsPath);
                try {
                  openRunIds = store.runsForProject(registration.projectId, 10_000)
                    .filter((run) => !SETTLED_RUN_STATUS.has(run.status))
                    .map((run) => run.runId);
                } finally {
                  store.close();
                }
              }
            }
            if (openRunIds.length > 0) {
              sendJson(response, 409, {
                ok: false,
                error: "runtime_project_busy",
                message: `unsettled run ${openRunIds.join(", ")}`,
                runIds: openRunIds,
              });
              return;
            }
            const laneCount = registry.homeLaneCount(projectRoot);
            if (laneCount > 0) {
              sendJson(response, 409, {
                ok: false,
                error: "runtime_project_has_lanes",
                message: `home root still has ${laneCount} lane(s)`,
              });
              return;
            }
          }
          const key = projectRuntimeKey(projectRoot);
          const existing = contexts.get(key);
          const unregistered = registry.unregisterProject(projectRoot);
          if (existing) {
            contexts.delete(key);
            syncStatuses.delete(key);
            closeKxmRuntimeContext(existing);
          }
          sendJson(response, 200, { ok: true, unregistered });
          return;
        }

        if (request.method === "POST" && url.pathname === "/v1/runs") {
          const body = await readJsonBody(request);
          const projectRoot = typeof body.projectRoot === "string" ? body.projectRoot : "";
          const workflowId = typeof body.workflowId === "string" ? body.workflowId : "";
          const prompt = typeof body.prompt === "string" ? body.prompt : "";
          if (!projectRoot || !workflowId) {
            sendJson(response, 400, { ok: false, error: "runtime_request_invalid", message: "projectRoot and workflowId are required" });
            return;
          }
          const context = contextFor(projectRoot);
          const bundle = loadKxmProject(projectRoot, {});
          const acceptance = acceptKxmRun(context, bundle, {
            ...(typeof body.commandId === "string" && body.commandId.length > 0 ? { commandId: body.commandId } : {}),
            workflowId,
            prompt,
          });
          sendJson(response, 200, {
            ok: true,
            idempotent: acceptance.idempotent,
            run: acceptance.run,
            event: acceptance.event,
          });
          return;
        }

        const runMatch = /^\/v1\/runs\/([A-Za-z0-9_-]+)(?:\/(events|cancel|signal|wait|drive))?$/.exec(url.pathname);
        if (runMatch) {
          const runId = runMatch[1] as string;
          const sub = runMatch[2];
          const projectRoot = url.searchParams.get("projectRoot") ?? "";
          if (!projectRoot) {
            sendJson(response, 400, { ok: false, error: "runtime_request_invalid", message: "projectRoot query parameter is required" });
            return;
          }
          const context = contextFor(projectRoot);
          const bundle = loadKxmProject(projectRoot, {});
          if (request.method === "GET" && !sub) {
            const projected = projectKxmRunReadOnly(context, runId);
            const folded = foldStoredKxmRun(context, projected);
            const drive = kxmDrivePollProjection(context, runId, folded);
            sendJson(response, 200, { ok: true, run: projected, ...(drive !== undefined ? { drive } : {}) });
            return;
          }
          if (request.method === "POST" && sub === "drive") {
            const body = await readJsonBody(request);
            if (body.mode !== "simulated" && body.mode !== "live") {
              sendJson(response, 400, { ok: false, error: "runtime_request_invalid", message: "drive mode must be simulated or live" });
              return;
            }
            const run = context.eventStore.run(runId);
            if (!run) throw runtimeError("run_unknown", runId, "run not found");

            const delayMs = typeof body.delayMs === "number" && body.delayMs > 0 ? body.delayMs : 0;
            const createProducer = () => body.mode === "live"
              ? createKxmOneShotProducer({
                  projectRoot,
                  timeoutMs: kxmProjectAdmissionLimits(bundle).agentStepTimeoutMs,
                })
              : createKxmSimulatedProducer(async () => {
                  if (delayMs > 0) {
                    await new Promise((r) => setTimeout(r, delayMs));
                  }
                  return { outcome: "passed" };
                });

            let session: Awaited<ReturnType<KxmRunScheduler["openDriveSession"]>> | undefined;
            let earlyError: unknown;
            try {
              const scheduler = KxmRunScheduler.for(context, bundle);
              const opening = scheduler.openDriveSession(runId, {
                mode: body.mode,
                allowLimits: false,
                liveMode: body.mode === "live",
                createProducer,
              });
              opening.catch((err) => { earlyError = err; });
              session = await opening;
            } catch (err) {
              earlyError = err;
            }

            if (earlyError) {
              const errStr = String(earlyError);
              if (errStr.includes("run_handoff_required")) {
                const handoff = (earlyError as { handoff?: unknown }).handoff;
                sendJson(response, 409, { ok: false, error: "run_handoff_required", ...(handoff !== undefined ? { handoff } : {}) });
                return;
              }
              if (errStr.includes("run_busy") || errStr.includes("scheduler_policy_conflict")) {
                sendJson(response, 409, { ok: false, error: "run_busy", message: `run ${runId} is already admitted or queued` });
                return;
              }
              throw earlyError;
            }

            void session!.settled.catch(() => {
              // Post-202 drive failures stay on the session; keep the request 202.
            });

            if (response.writableEnded) return;
            sendJson(response, 202, {
              ok: true,
              status: "accepted",
              runId,
              driveId: session!.driveId,
              poll: `/v1/runs/${runId}`,
              mode: body.mode,
            });
            return;
          }

          if (request.method === "GET" && sub === "events") {
            const after = Number.parseInt(url.searchParams.get("after") ?? "0", 10);
            const events = context.eventStore.events(runId, Number.isFinite(after) && after >= 0 ? after : 0, 500);
            sendJson(response, 200, { ok: true, events });
            return;
          }

          if (request.method === "GET" && sub === "drive") {
            const run = context.eventStore.run(runId);
            if (!run) throw runtimeError("run_unknown", runId, "run not found");

            const activeSession = kxmDriveSession(context.eventStore.path, runId);
            const session = activeSession
              ? {
                  driveId: activeSession.driveId,
                  mode: activeSession.mode,
                  openedAt: activeSession.openedAt,
                  deadlineAt: activeSession.deadlineAt ?? null,
                }
              : null;
            const receipts = context.eventStore.driveReceiptsForRun(runId, 20);
            sendJson(response, 200, { ok: true, session, receipts });
            return;
          }
          if (request.method === "POST" && sub === "cancel") {
            const body = await readJsonBody(request);
            const result = cancelKxmRun(context, runId, {
              ...(typeof body.commandId === "string" && body.commandId.length > 0 ? { commandId: body.commandId } : {}),
            });
            sendJson(response, 200, { ok: true, idempotent: result.idempotent, run: result.run, events: result.events });
            return;
          }
          if (request.method === "POST" && sub === "signal") {
            const body = await readJsonBody(request);
            const run = context.eventStore.run(runId);
            if (!run) throw runtimeError("run_unknown", runId, "run not found");
            const state = foldStoredKxmRun(context, run);
            let unblocked = false;
            if (state.status === "blocked_uncertain" || state.status === "cancelling" || state.currentStep?.effectState === "blocked_uncertain") {
              const action = body.action === "cancel" || body.action === "fail" || body.action === "retry" || body.action === "unblock" ? body.action : "unblock";
              const rec = recoverKxmRun(context, runId, {
                action,
                reason: typeof body.summary === "string" ? body.summary : `signal_${body.signalKey ?? "callback"}`,
              });
              unblocked = rec.unblocked;
            }
            sendJson(response, 200, { ok: true, runId, signalKey: body.signalKey, status: body.status, unblocked });
            return;
          }
          if (request.method === "POST" && sub === "wait") {
            const body = await readJsonBody(request);
            const run = context.eventStore.run(runId);
            if (!run) throw runtimeError("run_unknown", runId, "run not found");
            sendJson(response, 200, { ok: true, runId, stageId: body.stageId, signalKey: body.signalKey, waiting: true });
            return;
          }
        }

        const driveMatch = /^\/v1\/drives\/([A-Za-z0-9_-]+)$/.exec(url.pathname);
        if (request.method === "GET" && driveMatch) {
          const driveId = driveMatch[1] as string;
          const projectRoot = url.searchParams.get("projectRoot") ?? "";
          if (!projectRoot) {
            sendJson(response, 400, { ok: false, error: "runtime_request_invalid", message: "projectRoot query parameter is required" });
            return;
          }
          const context = contextFor(projectRoot);

          let receipt;
          try {
            receipt = context.eventStore.driveReceipt(driveId);
          } catch {
            sendJson(response, 200, { ok: true, receipt: null, verified: false, divergence: "receipt unreadable" });
            return;
          }

          if (!receipt) {
            sendJson(response, 404, { ok: false, error: "drive_receipt_missing", message: "receipt not found" });
            return;
          }

          const runId = receipt.runId;
          const stored = context.eventStore.run(runId);
          if (!stored) {
            sendJson(response, 200, { ok: true, receipt, verified: false, divergence: "run missing" });
            return;
          }
          const state = foldStoredKxmRun(context, stored);
          const events = context.eventStore.events(runId, 0, 1_000_000);
          const checked = verifyKxmDriveReceipt(receipt, events, state.status, { runId, driveId });
          sendJson(response, 200, {
            ok: true,
            receipt,
            verified: checked.verified,
            ...(checked.divergence !== undefined ? { divergence: checked.divergence } : {}),
          });
          return;
        }

        const projectRunsMatch = /^\/v1\/projects\/(prj_[A-Za-z0-9_-]+)\/runs$/.exec(url.pathname);
        if (request.method === "GET" && projectRunsMatch) {
          const projectRoot = url.searchParams.get("projectRoot") ?? "";
          if (!projectRoot) {
            sendJson(response, 400, { ok: false, error: "runtime_request_invalid", message: "projectRoot query parameter is required" });
            return;
          }
          const context = contextFor(projectRoot);
          const requestedProjectId = projectRunsMatch[1] as string;
          if (requestedProjectId !== context.projectId) {
            sendJson(response, 400, { ok: false, error: "runtime_request_invalid", message: `project ${requestedProjectId} is not the bound project ${context.projectId}` });
            return;
          }
          // The stored `runs` row is a cache, not the state: per-run reads fold the event
          // log (`projectKxmRunReadOnly`) precisely because the row can be stale. A listing
          // that returned raw rows would let every consumer — including the portal's
          // tenant read — present cached status as authoritative. Folding replays each
          // run's events; workflows are transition-bounded, so this stays cheap at the
          // default 50-run cap (`limit` may ask for up to 10000). A run that refuses to fold is returned with its cached row plus
          // `projectionError`, so one corrupt run cannot make the listing lie by omission.
          const limitText = url.searchParams.get("limit");
          const parsedLimit = limitText === null ? 50 : Number(limitText);
          const limit = Number.isInteger(parsedLimit) && parsedLimit > 0 ? Math.min(parsedLimit, 10_000) : 50;
          const runs = context.eventStore.runsForProject(requestedProjectId, limit).map((stored) => {
            try {
              return projectKxmRunReadOnly(context, stored.runId);
            } catch (error) {
              return {
                ...stored,
                projectionError: error instanceof KxmConfigError
                  ? (error.issues[0]?.code ?? "runtime_projection_failed")
                  : "runtime_projection_failed",
              };
            }
          });
          sendJson(response, 200, { ok: true, runs });
          return;
        }

        sendJson(response, 404, { ok: false, error: "runtime_not_found" });
      } catch (error) {
        if (error instanceof KxmConfigError) {
          const code = error.issues[0]?.code ?? "runtime_error";
          const status = code === "runtime_auth_failed" ? 401 : code === "run_unknown" ? 404 : 400;
          sendJson(response, status, { ok: false, error: code, issues: error.issues });
          return;
        }
        sendJson(response, 500, { ok: false, error: "runtime_internal" });
      }
    })();
  });

  const port = await new Promise<number>((resolvePort, rejectPort) => {
    server.once("error", rejectPort);
    server.listen(requestedPort, "127.0.0.1", () => {
      const address = server.address();
      if (!address || typeof address !== "object") {
        rejectPort(new Error("runtime supervisor did not bind"));
        return;
      }
      resolvePort(address.port);
    });
  });

  // Claim or take over the supervisor singleton.
  const claim = registry.claimSupervisor({ runtimeId, pid: process.pid, port, tokenHash, now: now() });
  if (!claim.claimed) {
    const existing = claim.record;
    // Only a genuinely live supervisor (running, fresh heartbeat, live pid)
    // conflicts. A cleanly stopped or stale supervisor may be replaced.
    const heartbeatAgeMs = Date.now() - Date.parse(existing.heartbeatAt);
    const fresh = Number.isFinite(heartbeatAgeMs) && heartbeatAgeMs < HEARTBEAT_STALE_MS;
    if (existing.state === "running" && fresh && processAlive(existing.pid)) {
      registry.close();
      server.close();
      throw runtimeError("runtime_supervisor_conflict", paths.registryDb, `runtime supervisor is already managed by PID ${existing.pid}`);
    }
    try {
      const takeover = registry.takeoverSupervisor({ runtimeId, pid: process.pid, port, tokenHash, now: now(), observedDeadPid: existing.pid, observedHeartbeatAt: existing.heartbeatAt });
      activeRuntimeId = takeover.runtimeId;
    } catch (error) {
      registry.close();
      server.close();
      throw error;
    }
  }

  // Ownership is established: publish the token atomically so concurrent
  // readers never see a truncated file, and clear any stale startup error.
  try {
    publishKxmSupervisorToken(paths, token);
    clearSupervisorError(paths);
  } catch (error) {
    registry.close();
    server.close();
    throw error;
  }

  const heartbeat = setInterval(() => {
    try {
      const changes = registry.heartbeat(process.pid, now());
      if (changes === 0 && !stopping) {
        // The singleton was taken over by another process: this supervisor is
        // superseded and must not linger as an unreachable zombie.
        void stop().finally(() => process.exit(0));
      }
    } catch { /* registry closed during shutdown */ }
  }, 1000);
  heartbeat.unref();

  // Outbound only: the supervisor pulls nothing and exposes nothing to the hub.
  // A tick that finds no bound hub, no credential or an unreachable hub does
  // not fail a run; outbox rows stay pending and local execution never waits on
  // it. "Never waits" is not "never says anything": every tick records what it
  // saw into the sync status this process serves at `GET /v1/sync/status`, and
  // a transport failure backs off instead of knocking every interval.
  //
  // Restart recovery: contexts are only populated on demand (a project request
  // opens one), so a restarted supervisor would see an empty map and silently
  // stop syncing every registered project's pending outbox rows. Reopen the
  // projects this Runtime owns before the first tick, and remember the ones that
  // will not open — a refused schema and an empty outbox are different answers,
  // and the status surface has to tell them apart.
  const reopenRegisteredProjects = (): void => {
    for (const reg of registry.projectsForRuntime(activeRuntimeId)) {
      const key = projectRuntimeKey(reg.projectRoot);
      if (contexts.has(key)) continue;
      try {
        contextFor(reg.projectRoot);
        syncStatuses.delete(key);
      } catch (error) {
        // A project whose checkout has moved, was deleted, or carries a store
        // this build refuses to open. Its outbox rows stay pending and its
        // presence expires, which the ops snapshot shows as orphaned — but the
        // reason is reported here, on the Runtime, where the operator looks.
        const reason = syncFailureText(error);
        const prior = syncStatuses.get(key);
        // Retried on every tick so a repaired checkout or a retired store heals
        // without a restart — which means this line is logged on a change of
        // state only, never once per tick.
        if (prior === undefined || prior.lastError !== reason) {
          logger.warn({
            event: "runtime_sync_context_unavailable",
            projectId: reg.projectId,
            message: `cannot reopen ${reg.projectRoot}: its outbox rows stay pending`,
            reason,
          });
        }
        syncStatuses.set(key, {
          projectId: reg.projectId,
          projectRoot: reg.projectRoot,
          homeRuntimeId: activeRuntimeId,
          state: "blocked",
          outbox: { pending: 0, acked: 0, refused: 0, refusals: [] },
          storeReadable: false,
          consecutiveFailures: 1,
          lastError: reason,
        });
      }
    }
  };
  reopenRegisteredProjects();

  const recordSyncStatus = (context: KxmRuntimeContext, next: KxmProjectSyncStatus): void => {
    const key = projectRuntimeKey(context.projectRoot);
    const previous = syncStatuses.get(key);
    syncStatuses.set(key, next);
    const changed = previous === undefined || previous.state !== next.state
      || (next.lastError !== undefined && previous?.lastError !== next.lastError)
      || JSON.stringify(next.outbox.refusals) !== JSON.stringify(previous?.outbox.refusals ?? []);
    if (!changed) return;
    // One line per state change — never one per tick, which is how a stalled
    // sync turns its own log into noise. `refusing` and `blocked` are the two
    // states that mean "run facts are not reaching the hub", so they are the
    // states an operator must be able to see without opening SQLite.
    const stalledState = next.state === "blocked" || next.state === "refusing";
    const entry = {
      event: stalledState ? "runtime_sync_stalled" : "runtime_sync_state",
      projectId: next.projectId,
      runtimeId: next.homeRuntimeId,
      state: next.state,
      pending: next.outbox.pending,
      acked: next.outbox.acked,
      refused: next.outbox.refused,
      ...(next.outbox.refusals.length > 0 ? { refusals: next.outbox.refusals } : {}),
      ...(next.hubUrl ? { hubUrl: next.hubUrl } : {}),
      ...(next.lastError ? { reason: next.lastError } : {}),
      ...(next.nextAttemptAt ? { nextAttemptAt: next.nextAttemptAt } : {}),
    };
    if (stalledState) logger.warn(entry);
    else logger.info(entry);
  };

  let syncing = false;
  let bindingWarning: string | undefined;
  const syncTimer = setInterval(() => {
    if (syncing || stopping) return;
    syncing = true;
    void (async () => {
      // A project that could not open last time gets another attempt: the fix on
      // disk (retired store, restored backup, moved checkout) is not followed by
      // a restart, and a blocked state that only clears on restart is its own
      // kind of silence.
      reopenRegisteredProjects();
      for (const context of [...contexts.values()]) {
        const key = projectRuntimeKey(context.projectRoot);
        const prior = syncStatuses.get(key);
        if (prior?.nextAttemptAt && Date.parse(prior.nextAttemptAt) > Date.parse(now())) continue;
        const base = {
          projectId: context.projectId,
          projectRoot: context.projectRoot,
          homeRuntimeId: context.homeRuntimeId,
          outbox: context.eventStore.outboxStatus(),
          consecutiveFailures: 0,
        };
        try {
          const client = runtimeHubClientFor(context, env, (message) => {
            if (bindingWarning === message) return;
            bindingWarning = message;
            logger.warn({ event: "hub_binding_fallback", message });
          });
          if (!client) {
            // Not a failure: a box with no bound hub simply keeps its rows.
            recordSyncStatus(context, { ...base, state: "no_hub" });
            continue;
          }
          await client.heartbeat();
          const pass = await syncKxmOutbox(context.eventStore, client, { now });
          const outbox = context.eventStore.outboxStatus();
          // A refused row is out of the pending queue, so there is nothing left to
          // stampede over: back off only for failures a later pass could actually
          // fix. The `refusing` state persists for as long as the store holds
          // refusals, which is the part an operator has to see.
          const failures = pass.blocked || pass.unconfirmed > 0 ? (prior?.consecutiveFailures ?? 0) + 1 : 0;
          const delayMs = syncBackoffMs(failures, runtimeSyncIntervalMs(env));
          recordSyncStatus(context, {
            ...base,
            hubUrl: client.options.serverUrl,
            outbox,
            state: pass.blocked ? "blocked" : (outbox.refused > 0 || pass.unconfirmed > 0) ? "refusing" : "ok",
            lastCompletedAt: now(),
            lastPushed: pass.pushed,
            lastAcked: pass.acked,
            lastRefused: pass.refused,
            lastUnconfirmed: pass.unconfirmed,
            consecutiveFailures: failures,
            ...(pass.blockedReason !== undefined ? { lastError: pass.blockedReason } : {}),
            ...(delayMs > 0 ? { nextAttemptAt: new Date(Date.parse(now()) + delayMs).toISOString() } : {}),
          });
        } catch (error) {
          // The hub is unreachable, unauthenticated or answering garbage. The
          // rows are still pending, so this is retryable — but it is never
          // silently retried: the reason is recorded and the tick backs off.
          const failures = (prior?.consecutiveFailures ?? 0) + 1;
          const delayMs = syncBackoffMs(failures, runtimeSyncIntervalMs(env));
          recordSyncStatus(context, {
            ...base,
            state: "blocked",
            consecutiveFailures: failures,
            lastError: syncFailureText(error),
            nextAttemptAt: new Date(Date.parse(now()) + delayMs).toISOString(),
          });
        }
      }
    })().finally(() => { syncing = false; });
  }, runtimeSyncIntervalMs(env));
  syncTimer.unref();

  let stopping = false;
  const stop = async (): Promise<void> => {
    if (stopping) return;
    stopping = true;
    clearInterval(heartbeat);
    clearInterval(syncTimer);
    try { registry.markStopping(process.pid, now()); } catch { /* best effort */ }
    const openSessions = [...contexts.values()].flatMap((context) => (
      kxmOpenDriveSessions(context.eventStore.path).map((session) => ({ context, session }))
    ));
    for (const { context, session } of openSessions) {
      try {
        cancelKxmRun(context, session.runId, { reason: "runtime_shutdown" });
      } catch {
        // Terminal or already cancelling runs still release on session settle.
      }
      try {
        session.controller.abort();
      } catch {
        // Session abort is best-effort after cancel is recorded.
      }
    }
    await waitForDriveSessions(openSessions.map(({ session }) => session.settled), runtimeStopGraceMs());
    for (const { context, session } of openSessions) {
      if (!(await driveSessionStillPending(session.settled))) continue;
      recordDriveReceipt(context, session, {
        kind: "unsettled",
        reason: "runtime_shutdown_grace_expired",
        producerId: session.producerId,
        producerClosed: false,
      });
    }
    for (const context of contexts.values()) closeKxmRuntimeContext(context);
    contexts.clear();
    logger.close();
    const closed = new Promise<void>((resolveStop) => server.close(() => resolveStop()));
    server.closeIdleConnections?.();
    await closed;
    try { registry.markStopped(process.pid, now()); } catch { /* best effort */ }
    registry.close();
  };

  process.once("SIGINT", () => { void stop().finally(() => process.exit(0)); });
  process.once("SIGTERM", () => { void stop().finally(() => process.exit(0)); });

  return { server, port, runtimeId: activeRuntimeId, stop };
}

/* ------------------------------------------------------------------ *
 * Thin client used by the CLI
 * ------------------------------------------------------------------ */

export async function kxmRuntimeRequest(
  handle: KxmSupervisorHandle,
  method: "GET" | "POST",
  path: string,
  body?: unknown,
): Promise<Record<string, unknown>> {
  const response = await fetch(`http://127.0.0.1:${handle.port}${path}`, {
    method,
    headers: {
      authorization: `Bearer ${handle.token}`,
      ...(body !== undefined ? { "content-type": "application/json" } : {}),
    },
    ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
  });
  const payload = await response.json() as Record<string, unknown>;
  if (!response.ok) {
    const code = typeof payload.error === "string" ? payload.error : "runtime_request_failed";
    const message = typeof payload.message === "string" ? payload.message : `runtime request failed with HTTP ${response.status}`;
    throw runtimeError(code, path, `${message}${handoffSuffix(payload.handoff)}`);
  }
  return payload;
}

const HANDOFF_TEXT_MAX = 200;

/** Why the Runtime handed a run off, from a `run_handoff_required` body: the
 * reason, field and detail, each capped so a refusal stays one readable line. */
function handoffSuffix(handoff: unknown): string {
  if (!handoff || typeof handoff !== "object" || Array.isArray(handoff)) return "";
  const parts: string[] = [];
  for (const key of ["reason", "field", "detail"] as const) {
    const value = (handoff as Record<string, unknown>)[key];
    if (typeof value === "string" && value.length > 0) parts.push(`${key} ${value.slice(0, HANDOFF_TEXT_MAX)}`);
  }
  return parts.length > 0 ? ` (handoff ${parts.join("; ")})` : "";
}