Ctrl K
ComunidadGuías…

bin/daemon/generation.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/** Validate an installed replacement before releasing the daemon to its recorded supervisor. */
import { spawn } from "node:child_process";
import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { updateActivationBlocked } from "@cotal-ai/workspace";
import { globalConfigDir } from "@cotal-ai/core";
import { fileURLToPath } from "node:url";

export function installedGenerationVersion(): string {
  const pkg = JSON.parse(readFileSync(packageFile(), "utf8"));
  if (typeof pkg.version !== "string" || !pkg.version) throw new Error("the daemon's CLI version is not identifiable");
  return pkg.version;
}

export class ReplacedDaemonGeneration extends Error {
  constructor(readonly loaded: string, readonly installed: string) {
    super(`daemon loaded CLI ${loaded}, but the installed CLI is ${installed}; refusing to launch a hosted Manager from the replaced installation`);
    this.name = "ReplacedDaemonGeneration";
  }
}

export async function reconcileHostedConnectors(version: string): Promise<void> {
  if (updateActivationBlocked()) throw new Error("an update is in progress, retry in a minute");
  const installed = installedGenerationVersion();
  if (installed !== version) throw new ReplacedDaemonGeneration(version, installed);
  try {
    const stamp = JSON.parse(readFileSync(join(globalConfigDir(), "seed", "stamp.json"), "utf8"));
    if (stamp?.generation === version) return;
  } catch (error) {
    if (!(error instanceof SyntaxError) && (error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
  }
  await reconcileInstalledGeneration();
}

function packageFile(): string {
  let dir = dirname(fileURLToPath(import.meta.url));
  for (let i = 0; i < 4; i++, dir = dirname(dir)) {
    const path = join(dir, "package.json");
    try { if (JSON.parse(readFileSync(path, "utf8")).name === "cotal-ai") return path; }
    catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
  }
  throw new Error("the daemon's CLI package is not identifiable");
}

export async function reconcileInstalledGeneration(): Promise<void> {
  const file = packageFile();
  await new Promise<void>((resolve, reject) => {
    const child = spawn(process.execPath, [join(dirname(file), "dist", "cotal.js"), "ext", "seed"], { env: { ...process.env, COTAL_SKIP_CONNECTOR_SEED: "1" }, stdio: ["ignore", "ignore", "pipe"], windowsHide: true });
    let error = "";
    child.stderr.setEncoding("utf8");
    child.stderr.on("data", (chunk: string) => { error = (error + chunk).slice(-1500); });
    const timer = setTimeout(() => child.kill("SIGKILL"), 120_000);
    child.once("error", (cause) => { clearTimeout(timer); reject(cause); });
    child.once("close", (code) => {
      clearTimeout(timer);
      if (code === 0) resolve();
      else reject(new Error(`replacement connector reconciliation failed (exit ${code}): ${error.trim()}; run \`cotal ext seed --repair\` with the installed CLI`));
    });
  });
}

/** Poll cheap package metadata. Expensive validation and activation attempts back off on failure. */
export function watchDaemonGeneration(version: string, changed: (version: string) => Promise<void>, report: (line: string) => void, pending: (version: string | undefined, reason?: string) => void = () => {}, activate = true): () => void {
  const file = packageFile();
  let busy = false, closed = false, candidate: string | undefined;
  let retryAt = 0, delay = 5000, lastReport = "";
  const announce = (line: string) => { if (line !== lastReport) { lastReport = line; report(line); } };
  const failed = (next: string, reason: string) => {
    pending(next, reason);
    announce(`daemon: installed CLI ${next} activation pending: ${reason}; current generation remains loaded and will retry automatically`);
    retryAt = Date.now() + delay;
    delay = Math.min(300_000, delay * 2);
    busy = false;
  };
  const timer = setInterval(() => {
    if (busy || closed) return;
    try { if (updateActivationBlocked()) return; } catch { return; }
    let next: string;
    try {
      const pkg = JSON.parse(readFileSync(file, "utf8"));
      if (pkg.name !== "cotal-ai" || typeof pkg.version !== "string") return;
      next = pkg.version;
    } catch { return; }
    if (next === version) { candidate = undefined; retryAt = 0; delay = 5000; pending(undefined); return; }
    if (candidate !== next) {
      candidate = next; retryAt = 0; delay = 5000;
      const reason = activate ? "verifying the installed entry" : "no verified native launch marker; automatic activation is unavailable; use the installed CLI to restart this daemon";
      pending(next, reason);
      announce(`daemon: installed CLI generation changed to ${next}; ${reason}`);
      return;
    }
    if (!activate || Date.now() < retryAt) return;
    busy = true;
    const child = spawn(process.execPath, [join(dirname(file), "dist", "cotal.js"), "--version"], { env: { ...process.env, COTAL_SKIP_CONNECTOR_SEED: "1" }, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
    let output = "", settled = false;
    child.stdout.on("data", (chunk) => { if (output.length < 1024) output += String(chunk); });
    child.stderr.resume();
    const timeout = setTimeout(() => child.kill("SIGKILL"), 5000);
    child.once("error", () => { clearTimeout(timeout); settled = true; if (!closed) failed(next, "the installed entry could not start"); });
    child.once("close", (code) => {
      clearTimeout(timeout);
      if (settled || closed) return;
      if (code !== 0 || output.split(/\r?\n/, 1)[0] !== `cotal-ai ${next}`) { failed(next, `entry verification did not confirm its version (exit ${code})`); return; }
      try { if (updateActivationBlocked()) { busy = false; return; } } catch { busy = false; return; }
      void changed(next).catch((error: unknown) => { if (!closed) failed(next, error instanceof Error ? error.message : "the replacement could not be activated"); });
    });
  }, 1000);
  timer.unref();
  return () => { closed = true; clearInterval(timer); };
}