bin/daemon/update-entry.ts
Rama main · réplica de lectura del repositorio original.
/** The binary composes machine-daemon custody with the stock package updater. */
import { existsSync } from "node:fs";
import { registry, type Command } from "@cotal-ai/core";
import { update, updateRuntime } from "@cotal-ai/cli/update";
import { withIdpSessionDir, withRemoteExchangeRetry } from "@cotal-ai/auth";
import { CommandFailure, homeCotalDir, readUpdateRecovery, claimUpdateClosureAccess, resolveControlTarget, shellPath } from "@cotal-ai/workspace";
import { askManager } from "@cotal-ai/workspace/control";
import { daemonPaths, EndpointRefusal } from "./endpoint.js";
import { managerViaDaemon, queryDaemon } from "./client.js";
import { installedDaemonIdentity, restartInstalled, unitReceiptFile } from "./install.js";
import { excludeLifetime, processBirth, readLifetimeRecord } from "./lifetime.js";
import { waitForInstalledDaemonGeneration } from "./update-generation.js";
const command = registry.resolve<Command>("command", "update");
registry.unregister("command", "update");
const composed: Command = { ...command, async run(args) {
let hostedSpaces: string[] = [];
let daemonObserved = false;
let preparedRestart = false;
let absentRecoveryHost = false;
await update(args, { ...updateRuntime, preparedRestart: () => preparedRestart, async prepareReplacement() {
if (process.env.COTAL_AUTO_UPDATE !== "1" || !daemonObserved) return async () => {};
const paths = daemonPaths();
try { await managerViaDaemon({ paths }, { op: "managers-prepare-restart" }); }
catch (error) { throw new Error(`daemon unavailable or refused update preparation: ${error instanceof Error ? error.message : String(error)}`); }
preparedRestart = true;
return async () => {
try {
const current = await queryDaemon({ paths });
if (current.ok && current.result.version === updateRuntime.version()) await managerViaDaemon({ paths }, { op: "managers-abort-restart" });
} catch (error) {
if (!(error instanceof EndpointRefusal) || !["NOT_RUNNING", "PEER_NOT_LOCK_OWNER"].includes(error.code)) throw error;
}
};
}, waitForInstalledGeneration: async version => {
if (absentRecoveryHost) return;
await waitForInstalledDaemonGeneration(version);
}, reportRunningManager: (flags) => withRemoteExchangeRetry(async () => {
const recovery = readUpdateRecovery();
if (recovery?.preparedRestart && process.env.COTAL_UPDATE_PARENT === String(process.ppid) && process.env.COTAL_UPDATE_TARGET_VERSION === updateRuntime.version() && recovery?.owner?.pid === process.ppid && recovery.owner.nonce === process.env.COTAL_UPDATE_CLOSURE_NONCE && recovery.owner.start === process.env.COTAL_UPDATE_CLOSURE_START) {
const release = claimUpdateClosureAccess();
release();
const paths = daemonPaths();
const lifetime = readLifetimeRecord(paths.lock);
let absent = !existsSync(paths.lock) || (lifetime?.start !== undefined && processBirth(lifetime.pid) !== lifetime.start);
if (!lifetime && !absent) {
const exclusion = excludeLifetime(paths.lock);
exclusion.release();
absent = true;
}
if (absent) {
absentRecoveryHost = recovery.phase === "restoring";
console.log("hosted Manager continuity: daemon lifetime proves no live host");
return "none";
}
const observed = await queryDaemon({ paths });
const evidence = observed.ok ? observed.result.update?.restartEvidence : undefined;
if (!evidence?.prepared || evidence.managers.some(row => !row.fenced || row.seats !== 0)) throw new Error("prepared update admission is no longer fenced and empty");
console.log("hosted Manager continuity: daemon verified prepared fences and zero seats");
return "none";
}
let managers;
try {
const reply = await queryDaemon({ paths: daemonPaths() });
if (!reply.ok) throw new Error(`machine daemon custody is unreadable: ${reply.error.message}`);
daemonObserved = true;
managers = reply.result.managers ?? [];
} catch (error) {
if (error instanceof EndpointRefusal && ["NOT_RUNNING", "NO_RUNTIME_DIR"].includes(error.code)) return updateRuntime.reportRunningManager(flags);
throw error;
}
// Updating this installation restarts the machine daemon, including spaces not selected by flags.
for (const manager of managers) {
if (manager.pid === undefined) console.log(`hosted Manager ${manager.space}: ${manager.state}; no live child, skipped`);
}
const active = managers.filter((manager) => manager.pid !== undefined);
hostedSpaces = active.map((manager) => manager.space);
if (!active.length) return updateRuntime.reportRunningManager(flags);
for (const manager of active) {
// A failed earlier observation is not proof that a retained child's endpoint cannot answer.
// Keep the fresh, instance-pinned custody check below as the authority for update admission.
if (!manager.instanceId) throw new CommandFailure("refused", "LIFECYCLE_BLOCKED", `hosted Manager for ${manager.space} is ${manager.state}; its custody must answer before update`, { remedy: `run \`cotal daemon service-disconnect --space ${manager.space}\`, correct its sign-in or connection, then run \`cotal update\` again` });
const reply = await withIdpSessionDir(homeCotalDir(), async () => {
const target = await resolveControlTarget({ space: manager.space }, "control-caller-privileged", manager.instanceId, { onRefusal: "throw", readOnly: true });
return askManager({ versionSkew: (text: string) => text, roster: async () => [] }, manager.space, target.server, "managerStatus", undefined, target.auth, "owner", 3000, { instanceId: manager.instanceId });
});
if (!reply.ok) throw new Error(reply.error ?? "hosted Manager status did not answer");
const status = reply.data as { agentCount?: number; custody?: string; stopImpact?: { agents: { name: string }[] } };
if (status.custody === "legacy" && status.agentCount !== 0) {
const names = status.stopImpact?.agents.map((agent) => agent.name);
throw new CommandFailure("refused", "LIFECYCLE_BLOCKED", `hosted Manager ${manager.instanceId} cannot preserve running agents on this computer during update${names?.length ? ` (${names.join(", ")})` : ""}; restarting it would end them`, { remedy: `save their work; stop each with \`cotal stop <name> --on ${manager.instanceId} --space ${shellPath(manager.space)}\`, then rerun the same update command. To list agents, use \`cotal ps --on ${manager.instanceId} --space ${shellPath(manager.space)}\` (requires admin scope)` });
}
console.log(`hosted Manager ${manager.space}: instance ${manager.instanceId} answered; ${status.agentCount ?? "unknown"} seat(s)`);
}
// The stock resolver still inspects other locally hosted spaces when one is not selected.
if (typeof flags.space !== "string") {
const { readMeshRegistry } = await import("@cotal-ai/workspace");
for (const entry of readMeshRegistry().entries) {
if (active.some((manager) => manager.space === entry.space)) continue;
if (await updateRuntime.reportRunningManager({ ...flags, space: entry.space }) === "legacy") return "legacy";
}
}
return "none";
}) });
if (args.values.check === true || args.values.auto || args.values.notify || process.exitCode || !daemonObserved) return;
// The daemon started this update and restarts itself once this process has exited. A restart
// asked for from here would stop this process with the rest of the service.
if (process.env.COTAL_AUTO_UPDATE === "1") return;
const paths = daemonPaths();
const current = await queryDaemon({ paths });
if (!current.ok) throw new Error("the installed daemon did not answer after update");
if (!existsSync(unitReceiptFile(paths))) {
console.log(`machine daemon: PID ${current.result.pid} runs Cotal ${current.result.version}; no recorded user service, left running`);
return;
}
const installation = installedDaemonIdentity(paths);
if (!installation.same) {
console.log(`machine daemon: ${installation.installed} runs Cotal ${current.result.version}; another installation was left running`);
return;
}
if (current.result.version !== updateRuntime.version()) {
const hosted = (current.result.managers ?? []).some((manager) => manager.pid !== undefined);
if (hosted) await managerViaDaemon({ paths }, { op: "managers-prepare-restart" });
try { await restartInstalled(paths, undefined, false, updateRuntime.version(), detail => console.log(`machine daemon: ${detail}`)); }
catch (error) {
if (hosted && process.platform !== "win32") {
try { await managerViaDaemon({ paths }, { op: "managers-abort-restart" }); }
catch { /* The native restart failure remains the primary outcome. */ }
}
throw error;
}
}
const deadline = Date.now() + 120_000;
for (;;) {
try {
const reply = await queryDaemon({ paths });
if (reply.ok && reply.result.version === updateRuntime.version() && hostedSpaces.every((space) => reply.result.managers?.some((manager) => manager.space === space && manager.state === "answering"))) return;
} catch (error) { if (!(error instanceof EndpointRefusal) || !["NOT_RUNNING", "PEER_NOT_LOCK_OWNER"].includes(error.code)) throw error; }
if (Date.now() >= deadline) throw new CommandFailure("refused", "LIFECYCLE_BLOCKED", "the updated daemon did not restore its hosted Managers before the deadline", { remedy: "read \`cotal daemon status\` and the recorded daemon user service" });
await new Promise((resolve) => setTimeout(resolve, 250));
}
} };
registry.register(composed);