← Files RiqorARCHIVED FILE

hooks/state.ts

13.8 KB · Oct 3, 2026 · 06:31 UTC

↓ Download file

import { createHash, randomUUID } from "node:crypto";
import { chmod, lstat, mkdir, readdir, readFile, rename, rm, rmdir, writeFile } from "node:fs/promises";
import { join } from "node:path";

export type MutationKind = "code" | "docs" | "config" | "unknown";
export type VerificationScope = "code" | "docs";

type TurnState = {
  version: 1;
  mutationKind: MutationKind;
  mutatedAt: number;
  verifiedAt?: number;
  blockedOnce: boolean;
};

const mutationKinds = new Set<MutationKind>(["code", "docs", "config", "unknown"]);
const keyPattern = /^[a-f0-9]{64}$/;
const lockRetryMs = 5;
const lockAttempts = 40;

export function turnKey(input: Record<string, unknown>) {
  return createHash("sha256")
    .update(`${String(input.session_id ?? "unknown")}\0${String(input.turn_id ?? "unknown")}`)
    .digest("hex");
}

function statePath(dataDir: string, key: string) {
  if (!keyPattern.test(key)) throw new Error("invalid state key");
  return join(dataDir, `${key}.json`);
}

async function secureDataDir(dataDir: string) {
  await mkdir(dataDir, { recursive: true, mode: 0o700 });
  const info = await lstat(dataDir);
  if (!info.isDirectory() || info.isSymbolicLink()) throw new Error("PLUGIN_DATA must be a real directory");
  await chmod(dataDir, 0o700);
}

function lockPath(dataDir: string, key: string) {
  if (!keyPattern.test(key)) throw new Error("invalid state key");
  return join(dataDir, `.${key}.lock`);
}

const delay = (milliseconds: number) => new Promise((resolve) => setTimeout(resolve, milliseconds));
const lockLeaseMs = 60_000;
const lockHardExpiryMs = 24 * 60 * 60 * 1000;

type LockOwner = Readonly<{
  version: 1;
  token: string;
  pid: number;
  createdAt: number;
}>;

type TurnLock = Readonly<{ path: string; ownerPath: string; token: string }>;

function parseLockOwner(contents: string): LockOwner | undefined {
  try {
    const owner = JSON.parse(contents) as Partial<LockOwner>;
    if (owner.version !== 1) return undefined;
    if (typeof owner.token !== "string" || owner.token.length < 1 || owner.token.length > 128) return undefined;
    if (!Number.isInteger(owner.pid) || Number(owner.pid) <= 0) return undefined;
    if (!validTime(owner.createdAt)) return undefined;
    return owner as LockOwner;
  } catch {
    return undefined;
  }
}

async function readLockOwner(path: string) {
  try {
    const info = await lstat(path);
    if (!info.isFile() || info.isSymbolicLink() || info.size > 512) return undefined;
    return parseLockOwner(await readFile(path, "utf8"));
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
    throw error;
  }
}

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

async function recoverAbandonedLock(path: string) {
  let directoryInfo;
  try {
    directoryInfo = await lstat(path);
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code === "ENOENT") return true;
    throw error;
  }
  if (!directoryInfo.isDirectory() || directoryInfo.isSymbolicLink()) return false;

  const ownerPath = join(path, "owner.json");
  const observedOwner = await readLockOwner(ownerPath);
  const observedAt = observedOwner?.createdAt ?? directoryInfo.mtimeMs;
  const observedAge = Date.now() - observedAt;
  if (observedAge <= lockLeaseMs) return false;
  if (observedOwner && observedAge <= lockHardExpiryMs && processIsAlive(observedOwner.pid)) return false;

  const recoveryPath = join(path, ".recovery.json");
  const recoveryToken = randomUUID();
  const staleMarker = await readLockOwner(recoveryPath);
  if (staleMarker) {
    const markerAge = Date.now() - staleMarker.createdAt;
    if (markerAge > lockLeaseMs && (markerAge > lockHardExpiryMs || !processIsAlive(staleMarker.pid))) {
      const confirmedMarker = await readLockOwner(recoveryPath);
      if (confirmedMarker?.token === staleMarker.token) await rm(recoveryPath, { force: true });
    }
  }
  try {
    await writeFile(recoveryPath, `${JSON.stringify({ version: 1, token: recoveryToken, pid: process.pid, createdAt: Date.now() })}
`, {
      encoding: "utf8",
      flag: "wx",
      mode: 0o600,
    });
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code === "EEXIST") return false;
    if ((error as NodeJS.ErrnoException).code === "ENOENT") return true;
    throw error;
  }

  try {
    const currentOwner = await readLockOwner(ownerPath);
    if (observedOwner && currentOwner?.token !== observedOwner.token) return false;
    if (currentOwner) {
      const currentAge = Date.now() - currentOwner.createdAt;
      if (currentAge <= lockLeaseMs) return false;
      if (currentAge <= lockHardExpiryMs && processIsAlive(currentOwner.pid)) return false;
    }
    await rm(ownerPath, { force: true });
  } finally {
    const recovery = await readLockOwner(recoveryPath);
    if (recovery?.token === recoveryToken) await rm(recoveryPath, { force: true });
  }

  try {
    await rmdir(path);
    return true;
  } catch (error) {
    const code = (error as NodeJS.ErrnoException).code;
    if (code === "ENOENT") return true;
    if (code === "ENOTEMPTY" || code === "EEXIST") return false;
    throw error;
  }
}

async function acquireTurnLock(dataDir: string, key: string): Promise<TurnLock> {
  await secureDataDir(dataDir);
  const path = lockPath(dataDir, key);
  const ownerPath = join(path, "owner.json");
  const token = randomUUID();
  for (let attempt = 0; attempt < lockAttempts; attempt += 1) {
    try {
      await mkdir(path, { mode: 0o700 });
      try {
        await writeFile(ownerPath, `${JSON.stringify({ version: 1, token, pid: process.pid, createdAt: Date.now() })}
`, {
          encoding: "utf8",
          flag: "wx",
          mode: 0o600,
        });
      } catch (error) {
        await rm(ownerPath, { force: true });
        await rmdir(path).catch(() => undefined);
        throw error;
      }
      return { path, ownerPath, token };
    } catch (error) {
      const code = (error as NodeJS.ErrnoException).code;
      if (code !== "EEXIST") throw error;
      await recoverAbandonedLock(path);
      await delay(lockRetryMs);
    }
  }
  throw new Error("timed out acquiring turn state lock");
}

async function releaseTurnLock(lock: TurnLock) {
  const owner = await readLockOwner(lock.ownerPath);
  if (owner?.token !== lock.token) return;
  await rm(lock.ownerPath, { force: true });
  try {
    await rmdir(lock.path);
  } catch (error) {
    const code = (error as NodeJS.ErrnoException).code;
    if (code !== "ENOENT" && code !== "ENOTEMPTY" && code !== "EEXIST") throw error;
  }
}

async function withTurnLock<T>(dataDir: string, key: string, operation: () => Promise<T>) {
  const lock = await acquireTurnLock(dataDir, key);
  try {
    return await operation();
  } finally {
    await releaseTurnLock(lock);
  }
}

function validTime(value: unknown): value is number {
  return typeof value === "number" && Number.isFinite(value) && value >= 0;
}

function parseState(contents: string): TurnState | undefined {
  try {
    const candidate = JSON.parse(contents) as Partial<TurnState>;
    if (candidate.version !== 1) return undefined;
    if (!mutationKinds.has(candidate.mutationKind as MutationKind)) return undefined;
    if (!validTime(candidate.mutatedAt)) return undefined;
    if (candidate.verifiedAt !== undefined && !validTime(candidate.verifiedAt)) return undefined;
    if (typeof candidate.blockedOnce !== "boolean") return undefined;
    return candidate as TurnState;
  } catch {
    return undefined;
  }
}

async function readState(dataDir: string, key: string) {
  const path = statePath(dataDir, key);
  try {
    const info = await lstat(path);
    if (!info.isFile() || info.isSymbolicLink() || info.size > 512) {
      await rm(path, { force: true });
      return undefined;
    }
    const state = parseState(await readFile(path, "utf8"));
    if (!state) await rm(path, { force: true });
    return state;
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
    throw error;
  }
}

async function writeState(dataDir: string, key: string, state: TurnState) {
  await secureDataDir(dataDir);
  const path = statePath(dataDir, key);
  const temporary = join(dataDir, `.${key}.${randomUUID()}.tmp`);
  try {
    await writeFile(temporary, `${JSON.stringify(state)}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
    await rename(temporary, path);
  } finally {
    await rm(temporary, { force: true });
  }
}

export async function markRuntimeSeen(dataDir: string, now = Date.now()) {
  await secureDataDir(dataDir);
  const path = join(dataDir, "runtime.json");
  const temporary = join(dataDir, `.runtime.${randomUUID()}.tmp`);
  try {
    await writeFile(temporary, `${JSON.stringify({ version: 1, event: "SessionStart", lastSeenAt: now })}\n`, {
      encoding: "utf8",
      flag: "wx",
      mode: 0o600,
    });
    await rename(temporary, path);
  } finally {
    await rm(temporary, { force: true });
  }
}

export async function recordMutation(dataDir: string, key: string, mutationKind: MutationKind, now = Date.now()) {
  if (!mutationKinds.has(mutationKind)) throw new Error("invalid mutation kind");
  await withTurnLock(dataDir, key, () =>
    writeState(dataDir, key, { version: 1, mutationKind, mutatedAt: now, blockedOnce: false })
  );
}

function scopeCovers(mutationKind: MutationKind, scope: VerificationScope) {
  return scope === "code" || mutationKind === "docs";
}

export async function recordVerification(
  dataDir: string,
  key: string,
  now = Date.now(),
  scope: VerificationScope = "code",
) {
  await withTurnLock(dataDir, key, async () => {
    const current = await readState(dataDir, key);
    if (!current || !scopeCovers(current.mutationKind, scope)) return;
    await writeState(dataDir, key, { ...current, verifiedAt: now, blockedOnce: false });
  });
}

async function clearTurnUnlocked(dataDir: string, key: string) {
  await rm(statePath(dataDir, key), { force: true });
}

export async function consumeEvidenceGate(dataDir: string, key: string) {
  return withTurnLock(dataDir, key, async () => {
    const current = await readState(dataDir, key);
    if (!current) return { pending: false } as const;
    if (current.verifiedAt !== undefined && current.verifiedAt >= current.mutatedAt) {
      await clearTurnUnlocked(dataDir, key);
      return { pending: false } as const;
    }
    if (!current.blockedOnce) {
      await writeState(dataDir, key, { ...current, blockedOnce: true });
      return { pending: true, firstBlock: true, mutationKind: current.mutationKind } as const;
    }
    await clearTurnUnlocked(dataDir, key);
    return { pending: true, firstBlock: false, mutationKind: current.mutationKind } as const;
  });
}

export async function clearTurn(dataDir: string, key: string) {
  await withTurnLock(dataDir, key, () => clearTurnUnlocked(dataDir, key));
}

type PruneCandidate = Readonly<{ key: string; path: string; modifiedAt: number }>;

type LockAttempt<T> = Readonly<{ acquired: true; value: T }> | Readonly<{ acquired: false }>;

async function tryWithTurnLock<T>(dataDir: string, key: string, operation: () => Promise<T>): Promise<LockAttempt<T>> {
  try {
    return { acquired: true, value: await withTurnLock(dataDir, key, operation) };
  } catch (error) {
    if (error instanceof Error && error.message === "timed out acquiring turn state lock") return { acquired: false };
    throw error;
  }
}

async function inspectPruneCandidateUnlocked(
  dataDir: string,
  key: string,
  removeInvalid: boolean,
): Promise<PruneCandidate | undefined> {
  const path = statePath(dataDir, key);
  try {
    const info = await lstat(path);
    if (!info.isFile() || info.isSymbolicLink() || info.size > 512) {
      if (removeInvalid) await rm(path, { force: true });
      return undefined;
    }
    const state = parseState(await readFile(path, "utf8"));
    if (!state) {
      if (removeInvalid) await rm(path, { force: true });
      return undefined;
    }
    return {
      key,
      path,
      modifiedAt: Math.max(state.mutatedAt, state.verifiedAt ?? 0, info.mtimeMs),
    };
  } catch (error) {
    if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
    throw error;
  }
}

export async function pruneState(
  dataDir: string,
  now = Date.now(),
  limits: { maxAgeMs?: number; maxFiles?: number } = {},
) {
  await secureDataDir(dataDir);
  const maxAgeMs = Math.max(0, limits.maxAgeMs ?? 14 * 24 * 60 * 60 * 1000);
  const maxFiles = Math.max(0, Math.floor(limits.maxFiles ?? 256));
  const candidates: PruneCandidate[] = [];

  for (const name of await readdir(dataDir)) {
    const match = name.match(/^([a-f0-9]{64})\.json$/);
    if (!match) continue;
    const key = match[1]!;
    const attempt = await tryWithTurnLock(dataDir, key, async () => {
      const current = await inspectPruneCandidateUnlocked(dataDir, key, true);
      if (!current) return undefined;
      if (now - current.modifiedAt > maxAgeMs) {
        await rm(current.path, { force: true });
        return undefined;
      }
      return current;
    });
    if (attempt.acquired && attempt.value) candidates.push(attempt.value);
  }

  candidates.sort((left, right) => right.modifiedAt - left.modifiedAt || left.key.localeCompare(right.key));
  let survivorRank = 0;
  for (const candidate of candidates) {
    const attempt = await tryWithTurnLock(dataDir, candidate.key, async () => {
      const current = await inspectPruneCandidateUnlocked(dataDir, candidate.key, true);
      if (!current) return false;
      if (now - current.modifiedAt > maxAgeMs) {
        await rm(current.path, { force: true });
        return true;
      }
      if (current.modifiedAt !== candidate.modifiedAt) {
        survivorRank += 1;
        return false;
      }
      if (survivorRank < maxFiles) {
        survivorRank += 1;
        return false;
      }
      await rm(current.path, { force: true });
      return true;
    });
    if (!attempt.acquired) survivorRank += 1;
  }
}

SHA-256: 9d5c09cd7b9c30ce4ff50c66fe1b57b7e9421d697aeeb44603031cd4c0cdac7f