openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
865 lines (864 loc) • 37.8 kB
JavaScript
import { t as createDeferredCore } from "./deferred-D0La5CRk.mjs";
import { o as registerSignalExitGate } from "./signal-exit-barrier-DRkfh-Hg.mjs";
import { h as sleep } from "./utils-DUAwk2p5.mjs";
import { r as resolveStateDir } from "./state-dir-CfAW6S2K.mjs";
import { o as isDefaultInstallIdentity } from "./paths-FklbMVOH.mjs";
import { t as formatErrorMessage } from "./errors-BSdPMFao.mjs";
import { t as formatCliCommand } from "./command-format-Cte1SWM4.mjs";
import { r as createOpenClawDatabaseMaintenanceScope } from "./openclaw-state-db-async-lifecycle-8N3pIeYH.mjs";
import { s as resolveOpenClawStateSqlitePath } from "./openclaw-state-db.paths-BvBEgHyD.mjs";
import { l as resolveGatewayStateOwnerPath, t as GatewayStateOwnerContentionError } from "./gateway-state-owner-duCYcvBp.mjs";
import { r as createSqliteReadOnlyWorkerScope } from "./sqlite-readonly-worker-5mtnBdTa.mjs";
import { n as openDoctorStateSchemaReadAdmission } from "./openclaw-state-db-doctor-schema-CkfI7JQa.mjs";
import { t as DoctorUnreadableStateDatabaseError } from "./state-repair-message-B5Bu99oR.mjs";
import { r as hasCommandProcessCleanupError } from "./exec-result-LNNPagt0.mjs";
import { l as withCommandProcessScope, n as resolveCommandProcessSignal } from "./exec-spawn-DktriUN8.mjs";
import { n as GatewayServiceAuthorityError } from "./service-update-authority-CB0cX2Br.mjs";
import { c as hasGatewayServiceStopUnsafeError, o as findServiceOwnershipRefusal } from "./service-inspection-error-DN1EVxrD.mjs";
import { r as readPackageVersion } from "./package-json-7gGhB8Vf.mjs";
import { n as readBuiltGatewayBuildId } from "./update-git-runtime-BOUI4ctk.mjs";
import { s as closeOpenClawStateDatabaseByPathAsync } from "./openclaw-state-db-cache-BEArcjbO.mjs";
import { n as createUpdateFailureFact } from "./update-failure-facts-ipOD81I6.mjs";
import { n as recordUpdateDoctorRefusal, r as resolveUpdateDoctorGitRecovery, t as formatUpdateDoctorServiceStopRefusal } from "./doctor-update-refusal-Bmut3giK.mjs";
import { c as readStateLeaseProcessOwnerStatus } from "./openclaw-state-lease-store-DU9l42Vs.mjs";
import { o as acquireWithWait } from "./startup-migration-checkpoint-BOGmWYg7.mjs";
import { a as UpdateDoctorError, d as createUpdateDoctorDatabaseWriteCapture, t as DoctorMaintenanceRefusalError } from "./update-doctor-result-T_IRct7a.mjs";
import { n as readGatewayOwnerLease } from "./gateway-owner-lease-90DnCeMV.mjs";
import { n as GatewayLockError, o as readActiveGatewayLockIdentity, r as acquireGatewayLock } from "./gateway-lock-BISyyIm_.mjs";
import { r as inspectUpdateRepairDriverAdmission } from "./update-run-activity-CmOM_-AU.mjs";
import { d as recordUpdateRunRepairContinuation } from "./update-run-ledger-1TfJC2gT.mjs";
import { t as createUpdateRunAdmissionReader } from "./update-run-reader-DrO5nA1i.mjs";
import { i as UPDATE_RUN_ID_ENV } from "./update-control-plane-sentinel-BXGUG6j_.mjs";
import { n as GATEWAY_SERVICE_STOP_TIMEOUT_MS, r as GATEWAY_SHUTDOWN_RESERVE_MS } from "./gateway-shutdown-budget-v-vGFkEN.mjs";
import { n as probePortUsage } from "./ports-probe-i0c0rzCC.mjs";
import { n as resolveGatewayService } from "./service-BF2g9b8b.mjs";
import { t as resolveGatewayRestartDeferralTimeoutMs } from "./restart-budget-BrFMgEHj.mjs";
import { m as resolveUpdatedGatewayRestartPort } from "./update-command-service-plan-D5Hg8gYG.mjs";
import { n as sanitizeDoctorNote } from "./emit-notes-DnxujLhz.mjs";
import { s as inspectGatewayRestart } from "./restart-health-D97bVmr5.mjs";
import { s as TICK_INTERVAL_MS } from "./server-constants-BH_EolwD.mjs";
import { n as readLegacyGatewayLockIdentity, t as assertLegacyGatewayStoppedForMaintenance } from "./gateway-lock-legacy-DQrgp1xw.mjs";
import { c as resolveUpdateParentGatewayActivation, l as shouldManageGatewayService, n as assertDoctorServiceSelection, o as isServiceRepairExternallyManaged } from "./doctor-service-repair-policy-BJrQJIOB.mjs";
import { r as classifyDoctorMaintenanceRefusal, t as assertDoctorMaintenanceInspection } from "./doctor-maintenance-inspection-O3Ryk6ia.mjs";
import { n as resolveDoctorRepairMode, t as isDoctorUpdateRepairMode } from "./doctor-repair-mode-CsL6FPi0.mjs";
import path from "node:path";
//#region src/commands/doctor-agent-lease-refusal.ts
function createDoctorAgentLeaseRefusal(env, cause) {
const message = "Doctor could not enter maintenance. An agent database is in use. Stop other OpenClaw processes using this state, then retry the update.";
return new DoctorMaintenanceRefusalError(message, {
kind: "deferred",
reason: "agent-database-in-use"
}, {
cause,
failureFacts: [createUpdateFailureFact({
check: "doctor",
code: "agent-database-lease-active",
message
}, env)]
});
}
async function preflightExternalDoctorAgentLease(env) {
const { readActiveOpenClawAgentDatabaseLeasesReadOnly } = await import("./openclaw-agent-db-lease-ZMS7u8z0.mjs");
let activeAgentLease = false;
try {
activeAgentLease = readActiveOpenClawAgentDatabaseLeasesReadOnly({ env }, openDoctorStateSchemaReadAdmission).length > 0;
} catch (error) {
if (hasCommandProcessCleanupError(error)) throw error;
}
if (activeAgentLease) throw createDoctorAgentLeaseRefusal(env);
}
/** The caller holds both lifecycle coordinators; this check never opens a state writer. */
async function assertDoctorAgentLeaseAdmission(env) {
const { assertNoOpenClawAgentDatabaseLeasesReadOnly, OpenClawAgentDatabaseLeaseActiveError } = await import("./openclaw-agent-db-lease-ZMS7u8z0.mjs");
try {
assertNoOpenClawAgentDatabaseLeasesReadOnly({ env }, openDoctorStateSchemaReadAdmission);
} catch (error) {
if (error instanceof OpenClawAgentDatabaseLeaseActiveError) throw createDoctorAgentLeaseRefusal(env, error);
const { preflightOpenClawDatabaseSchemas } = await import("./openclaw-database-preflight-DI4G8oRa.mjs");
const unreadable = (await preflightOpenClawDatabaseSchemas({
env,
scope: "state",
openStateSchemaReadAdmission: openDoctorStateSchemaReadAdmission
})).indeterminate.find((database) => database.kind === "state");
if (unreadable) throw new DoctorUnreadableStateDatabaseError(unreadable.path, unreadable.reason);
throw error;
}
}
//#endregion
//#region src/commands/doctor-maintenance-admission.ts
function resolveDoctorUpdateAdmission(env) {
const inheritedRunId = env[UPDATE_RUN_ID_ENV]?.trim();
const readRuns = createUpdateRunAdmissionReader({
active: true,
limit: 100,
includeRunId: inheritedRunId
}, { env }, openDoctorStateSchemaReadAdmission);
const readAdmission = () => {
const runs = readRuns();
const admission = inspectUpdateRepairDriverAdmission(runs, inheritedRunId);
if (admission.kind === "conflict") throw new Error(admission.message);
return admission;
};
const admission = readAdmission();
const continuation = admission.kind === "continuation" ? admission.run : admission.runs.find((run) => run.runId === inheritedRunId);
return {
assertCurrent: () => {
readAdmission();
},
recordContinuation: () => {
readAdmission();
if (continuation?.steps.some((step) => step.step === "finalize:repair-continuation")) recordUpdateRunRepairContinuation(continuation.runId, inheritedRunId, { env });
}
};
}
//#endregion
//#region src/commands/doctor-maintenance-exit.ts
/** Let admitted repair work settle before restoring the service and exiting. */
function holdDoctorMaintenanceExit(reportInterruption) {
const controller = new AbortController();
const finished = createDeferredCore();
finished.promise.catch(() => void 0);
const unregister = registerSignalExitGate(finished.promise, (signal) => {
if (controller.signal.aborted) return;
const message = `Doctor interrupted${signal ? ` by ${signal}` : ""}; cancelling inspections and settling admitted repairs before exit.`;
controller.abort(new Error(message));
try {
reportInterruption?.(message);
} catch {}
});
let active = true;
const release = (failed = false) => {
if (!active) return;
active = false;
unregister();
if (failed) finished.reject(/* @__PURE__ */ new Error("Doctor maintenance did not complete."));
else finished.resolve();
};
return {
signal: controller.signal,
release
};
}
//#endregion
//#region src/commands/doctor-maintenance-stale-service.ts
function doctorGatewayMaintenanceError(params) {
const restart = formatCliCommand("openclaw gateway restart", params.env);
const next = `Run ${formatCliCommand("openclaw gateway status --deep", params.env)}; resolve the reported failure, then ${formatCliCommand("openclaw doctor --fix", params.env)} and ${restart}.`;
const message = `Doctor ${params.phase} failed. ${params.detail} ${next}`;
const failureFacts = [createUpdateFailureFact({
check: params.phase,
code: params.code,
message: params.detail
}, params.env), createUpdateFailureFact({
check: params.phase,
code: "stale-gateway-recovery-command",
message: next
}, params.env)];
return params.phase === "gateway-stop" ? new DoctorMaintenanceRefusalError(message, {
kind: "data-at-risk",
reason: "gateway-state-unverified"
}, {
cause: params.cause,
failureFacts
}) : new UpdateDoctorError(message, failureFacts, { cause: params.cause });
}
/** Identify predecessor code without requiring startup before offline migrations. */
async function inspectStaleDoctorGateway(params) {
const { before, root, env } = params;
const legacy = await readLegacyGatewayLockIdentity(env);
if (!before.running && !legacy) return;
const serviceEnv = before.serviceEnv ?? env;
const service = resolveGatewayService();
const [version, buildId, serviceCommand] = await Promise.all([
readPackageVersion(root),
readBuiltGatewayBuildId(root),
service.readCommand(serviceEnv).catch(() => null)
]);
params.assertCurrent?.();
if ((!version || !buildId) && !legacy) return;
const port = await resolveUpdatedGatewayRestartPort({
serviceEnv,
serviceCommand
});
const health = legacy ? void 0 : await inspectGatewayRestart({
service,
port,
env: serviceEnv,
expectedVersion: version,
expectedBuildId: buildId,
openStateSchemaReadAdmission: openDoctorStateSchemaReadAdmission,
requirePluginHealth: false
});
params.assertCurrent?.();
if (!(legacy || health?.buildIdMismatch?.actual != null || health?.versionMismatch || health?.staleConnection !== void 0)) return;
const pid = legacy?.pid ?? before.servicePid ?? health?.runtime.pid;
if (before.serviceUpdateVerdict?.kind !== "owned" || !before.serviceEnv || !before.running || legacy && (legacy.state !== "alive" || legacy.pid !== before.servicePid)) throw doctorGatewayMaintenanceError({
env,
phase: "gateway-stop",
code: "stale-gateway-service-unverified",
detail: `Gateway PID ${pid ?? "unknown"} is stale, but its managed service ownership is unverified. ${before.blockMessage ?? before.serviceMutationSkipMessage ?? ""}`
});
if (!version || !buildId) throw doctorGatewayMaintenanceError({
env,
phase: "gateway-stop",
code: "stale-gateway-identity-unavailable",
detail: "The installed candidate's build identity is unavailable."
});
return {
version,
buildId,
pid,
port
};
}
async function assertStaleDoctorGatewayStopped(params) {
const runtime = await resolveGatewayService().readRuntime(params.env);
const port = await probePortUsage(params.stale.port);
const legacy = await readLegacyGatewayLockIdentity(params.env);
params.assertCurrent?.();
if (runtime.status !== "stopped" || (runtime.pid ?? 0) > 0 || port !== "free" || legacy) throw new Error(`Stale Gateway PID ${params.stale.pid ?? "unknown"} did not stop: service=${runtime.status}, PID=${runtime.pid ?? "none"}, port ${params.stale.port}=${port}, legacy lock owner=${legacy?.pid ?? "none"}.`);
}
//#endregion
//#region src/commands/doctor-maintenance-foreground.ts
async function acquireDoctorGatewayMaintenanceOwner(databasePath, env, params) {
const updateRepair = isDoctorUpdateRepairMode(resolveDoctorRepairMode(params.options));
let foreground;
let ownerlessDeadlineMs;
return await acquireWithWait({
acquire: async () => {
params.assertCurrent?.();
try {
const owner = await acquireGatewayLock({
env,
role: "sqlite-maintenance",
allowInTests: true,
timeoutMs: 0,
relocatedMaintenanceOwner: params.relocatedMaintenanceOwner
});
if (!owner) throw new Error(`Doctor could not acquire maintenance ownership for ${databasePath}`);
return owner;
} catch (error) {
if (error instanceof GatewayLockError && error.cause instanceof GatewayStateOwnerContentionError) throw error.cause;
throw error;
}
},
shouldRetry: (error) => {
if (!updateRepair || !params.assertCurrent || !(error instanceof GatewayStateOwnerContentionError)) return false;
params.assertCurrent();
const current = readGatewayOwnerLease({
env,
current: true,
openStateSchemaReadAdmission: openDoctorStateSchemaReadAdmission
});
if (!foreground) {
if (!current) {
if (ownerlessDeadlineMs === void 0) {
ownerlessDeadlineMs = performance.now() + GATEWAY_SHUTDOWN_RESERVE_MS;
params.runtime.log("Waiting for Gateway state ownership cleanup to finish.");
}
return performance.now() < ownerlessDeadlineMs;
}
if (ownerlessDeadlineMs !== void 0) return false;
if (current.state !== "live" || current.mode !== "foreground") return false;
foreground = current;
params.runtime.log("Waiting for the previous foreground Gateway to release state.");
} else if (current && (current.owner !== foreground.owner || current.pid !== foreground.pid || current.startedAt !== foreground.startedAt || current.host !== foreground.host || current.mode !== "foreground")) return false;
return readStateLeaseProcessOwnerStatus(foreground) === "live";
},
deadlineMs: Math.min(params.deadlineMs ?? Infinity, performance.now() + TICK_INTERVAL_MS + resolveGatewayRestartDeferralTimeoutMs() + GATEWAY_SERVICE_STOP_TIMEOUT_MS),
pollIntervalMs: 100,
maxPollIntervalMs: 1e3,
sleep: (ms) => sleep(ownerlessDeadlineMs === void 0 ? ms : Math.min(ms, Math.max(0, ownerlessDeadlineMs - performance.now())))
});
}
//#endregion
//#region src/commands/doctor-maintenance-state.ts
/** Database custody can change paths while stopped-service custody remains with Doctor. */
function createDoctorMaintenanceState(options) {
const { params, env, settle } = options;
let resources;
let inspections;
let owner;
let selectedEnv = env;
let captureAdmitted = false;
const capture = createUpdateDoctorDatabaseWriteCapture(params.databaseGenerations, {
env,
root: params.root ?? void 0,
signal: options.signal,
assertCurrent: () => owner.assertCurrent(options.assertCurrent),
warn: options.warn
});
const closeResources = async () => {
const failures = [];
const closeScope = async (current, clear) => {
if (!current) return;
try {
await current.close();
clear();
} catch (error) {
try {
await current.close();
clear();
failures.push(error);
} catch (retryError) {
failures.push(new AggregateError([error, retryError], "Doctor maintenance resource cleanup failed.", { cause: retryError }));
}
}
};
await closeScope(resources, () => {
resources = void 0;
});
await closeScope(inspections, () => {
inspections = void 0;
});
if (failures.length === 1) throw failures[0];
if (failures.length > 1) throw new AggregateError(failures, "Doctor maintenance resource cleanup failed.");
};
const settleCapture = async () => {
if (owner && capture && captureAdmitted) {
await settle(() => capture.settle());
captureAdmitted = false;
}
};
const enterResources = async (acquired) => {
owner = acquired;
try {
acquired.assertCurrent(options.assertCurrent);
resources = createOpenClawDatabaseMaintenanceScope({
schemaMaintenance: true,
assertDatabaseAccess: acquired.assertDatabaseAccess,
assertOwnerCurrent: (access) => {
acquired.assertCurrent(() => {
options.assertCurrent?.();
options.assertReadCurrent();
}, access);
}
});
inspections = createSqliteReadOnlyWorkerScope({
signal: options.signal,
deadlineOwnedByCaller: false
});
} catch (error) {
await acquired.release();
owner = void 0;
throw error;
}
if (capture) {
await settle(() => resources.run(() => capture.admit()));
captureAdmitted = true;
}
};
const state = {
get owner() {
return owner;
},
get resources() {
return resources;
},
get hasOpenResources() {
return Boolean(resources || inspections);
},
get receipt() {
return owner ? void 0 : capture?.receipt;
},
run(operation) {
return resources.run(() => inspections.run(operation));
},
async acquire(relocatedMaintenanceOwner) {
if (resources) return;
const assertCurrent = relocatedMaintenanceOwner ? () => relocatedMaintenanceOwner.assertCurrent(options.assertCurrent) : options.assertCurrent;
assertCurrent?.();
const acquired = await acquireDoctorGatewayMaintenanceOwner(path.resolve(resolveOpenClawStateSqlitePath(selectedEnv)), selectedEnv, {
...params,
assertCurrent,
deadlineMs: options.deadline(),
relocatedMaintenanceOwner
});
await enterResources(acquired);
},
async relocateLegacyRoot() {
const { resolvePendingLegacyStateDirMigrationPaths, prepareLegacyStateDirMigration } = await import("./state-migrations.state-dir-Bj1pCXgi.mjs");
const pending = resolvePendingLegacyStateDirMigrationPaths({ env });
const sourceDir = resolveStateDir(env);
if (!pending || path.resolve(sourceDir) !== path.resolve(pending.source)) return;
const { closeOpenClawAgentDatabasesAsync } = await import("./openclaw-agent-db-lifecycle-DSXFhe4f.mjs");
owner.assertCurrent(options.assertCurrent);
const sourceDatabase = resolveOpenClawStateSqlitePath(env);
await closeResources();
await closeOpenClawAgentDatabasesAsync(sourceDir);
await closeOpenClawStateDatabaseByPathAsync(sourceDatabase);
await settleCapture();
const migration = owner.run(() => {
options.assertCurrent?.();
owner.assertCurrent();
return prepareLegacyStateDirMigration({ env });
});
selectedEnv = {
...env,
OPENCLAW_STATE_DIR: migration?.stateDir ?? sourceDir
};
if (resolveGatewayStateOwnerPath(resolveOpenClawStateSqlitePath(selectedEnv)) !== owner.lockPath) await state.acquire(owner);
else await enterResources(owner);
if (migration) {
const result = await resources.run(() => migration.complete());
for (const change of [...result.changes, ...result.notices ?? []]) params.runtime.log(sanitizeDoctorNote(change));
for (const warning of result.warnings) options.warn(sanitizeDoctorNote(warning));
}
},
async release() {
const failures = [];
try {
await closeResources();
} catch (error) {
if (hasCommandProcessCleanupError(error) || state.hasOpenResources) throw error;
failures.push(error);
}
try {
await settleCapture();
} catch (error) {
if (hasCommandProcessCleanupError(error)) throw error;
failures.push(error);
}
try {
await owner?.release();
owner = void 0;
} catch (error) {
failures.push(error);
}
if (failures.length === 1) throw failures[0];
if (failures.length > 1) throw new AggregateError(failures, "Doctor maintenance state cleanup failed.");
}
};
return state;
}
//#endregion
//#region src/commands/doctor-maintenance.ts
/** Coordinates explicit Doctor repair with the managed Gateway lifecycle. */
async function beginDoctorMaintenance(params) {
if (!(params.options.repair === true || params.options.yes === true)) return;
const env = {
...process.env,
...params.runId ? { [UPDATE_RUN_ID_ENV]: params.runId } : {}
};
const parentActivation = isDoctorUpdateRepairMode(resolveDoctorRepairMode(params.options)) ? resolveUpdateParentGatewayActivation(env) : void 0;
let stopped;
let stopDeadline;
const warnings = [];
const warn = (message) => {
warnings.push(message);
params.runtime.log(message);
};
const failureFacts = [];
let repairStoresMayBeOpen = false;
let retainStoppedInstallation = false;
let inspectingActivation = false;
let staleReplacement;
let assertUpdateAdmissionReadCurrent;
let assertUpdateAdmissionCurrent;
let authorityRefused = false;
const assertAuthority = (assertion) => {
try {
assertion();
} catch (error) {
authorityRefused = true;
throw error;
}
};
const callerAssertCurrent = params.assertCurrent;
const assertCallerCurrent = callerAssertCurrent ? () => assertAuthority(callerAssertCurrent) : void 0;
let cleanupFailure;
const settle = async (operation) => {
if (cleanupFailure) throw cleanupFailure.error;
try {
return await withCommandProcessScope(operation);
} catch (error) {
if (hasCommandProcessCleanupError(error)) cleanupFailure ??= { error };
throw error;
}
};
const acquireStoppedMaintenanceResources = () => acquireWithWait({
acquire: () => state.acquire(),
shouldRetry: (error) => stopped?.stopped === true && error instanceof GatewayStateOwnerContentionError,
deadlineMs: stopDeadline ?? performance.now(),
pollIntervalMs: 250,
maxPollIntervalMs: 2e3
});
const releaseState = async () => {
if (cleanupFailure) throw cleanupFailure.error;
await state.release();
repairStoresMayBeOpen = false;
};
let deferredStateReleaseFailure;
const throwDeferredStateReleaseFailure = () => {
if (deferredStateReleaseFailure !== void 0) throw deferredStateReleaseFailure;
};
const release = async (assertCustody) => {
await settle(async () => {
try {
await releaseState();
} catch (error) {
if (hasCommandProcessCleanupError(error) || state.hasOpenResources) throw error;
deferredStateReleaseFailure ??= error;
}
const recovery = stopped?.windowsTaskAutoStartRecovery;
try {
assertCustody?.();
if (!retainStoppedInstallation) await settle(async () => {
await recovery?.restore(true);
});
} finally {
if (!cleanupFailure) await settle(async () => recovery?.complete(!retainStoppedInstallation));
}
});
};
const finish = async (cfg, assertCustody, writeConfig, assertRestoreAdmission = assertUpdateAdmissionCurrent) => {
await release(assertCustody);
assertCustody?.();
const before = stopped;
const root = params.root;
if (!before?.serviceEnv || !root) return;
if (!before.stopped) {
const verdict = before.serviceUpdateVerdict;
if (verdict?.kind === "owned" && verdict.requiresInstallRootRefresh) {
const { inspectGatewayServiceInstallationDrift } = await import("./service-layout-CelJBVuK.mjs");
const drift = await inspectGatewayServiceInstallationDrift({ packageRootReal: verdict.root }, root);
if (drift) {
const { formatGatewayServiceInstallationDrift } = await import("./shared-dd5uQ7hy.mjs");
const message = formatGatewayServiceInstallationDrift(drift, void 0, before.serviceEnv, {
stopped: true,
port: before.servicePort
});
warn(message);
return;
}
}
if (resolveDoctorRepairMode(params.options).updateInProgress || before.offline !== true || verdict?.kind !== "owned" || verdict.requiresInstallRootRefresh) return;
}
try {
const serviceEnv = before.serviceEnv;
const [{ restoreDoctorGatewayService }, { resolveUpdatedGatewayRestartPort }, { renderRestartDiagnostics, waitForGatewayHealthyRestart }] = await Promise.all([
import("./doctor-maintenance-restoration-CWJFb9va.mjs"),
import("./update-command-service-plan-B0XPv87D.mjs"),
import("./restart-health-BMi3qhn8.mjs")
]);
const { service, state, cfg: restoredConfig } = await restoreDoctorGatewayService({
before,
serviceEnv,
root,
env,
cfg,
writeConfig,
options: params.options,
runtime: params.runtime,
signal: exit.signal,
warnings,
settle,
assertCustody,
assertRestoreAdmission,
assertInstallationAdmission: assertUpdateAdmissionCurrent
});
if (!state) return;
const port = await resolveUpdatedGatewayRestartPort({
config: restoredConfig,
serviceEnv: state.env,
serviceCommand: state.command
});
const health = await settle(() => waitForGatewayHealthyRestart({
service,
port,
env: state.env,
requireRunningService: true,
...staleReplacement ? {
expectedVersion: staleReplacement.version,
expectedBuildId: staleReplacement.buildId,
requirePluginHealth: false
} : {}
}));
if (health.waitOutcome === "still-starting") {
const warning = renderRestartDiagnostics(health).join(" ");
warn(warning);
return;
}
if (!health.healthy) throw doctorGatewayMaintenanceError({
env,
phase: "gateway-restoration",
code: "doctor-gateway-rpc-verification-failed",
detail: `Doctor repaired state, but the managed Gateway did not become ready: ${renderRestartDiagnostics(health).join(" ")}.`
});
} catch (error) {
if (hasCommandProcessCleanupError(error)) {
cleanupFailure ??= { error };
throw error;
}
if (error instanceof GatewayServiceAuthorityError) {
const { formatDaemonServiceInstallCommand } = await import("./shared-dd5uQ7hy.mjs");
const serviceEnv = before.serviceEnv;
const outcome = error.outcome ?? "recovery-pending";
const message = `Doctor could not finish Gateway installation or activation under its maintenance authority (${outcome}). ${outcome === "unchanged" ? "The previous service definition was left unchanged." : outcome === "restored" ? "The previous service definition was restored from its captured backup." : "Restoration was not verified; inspect the current service before replacing it."} Run \`${formatCliCommand("openclaw gateway status --deep", serviceEnv)}\`; after the active maintenance or update finishes, run \`${formatDaemonServiceInstallCommand(serviceEnv, before.servicePort)}\` from the active CLI. Reason: ${error.message}`;
failureFacts.push(createUpdateFailureFact({
check: "gateway-restoration",
code: `${error.code}-${outcome}`,
message
}, serviceEnv));
warnings.push(message);
params.runtime.error(message);
if (outcome === "recovery-pending") throw new UpdateDoctorError(message, failureFacts, { cause: error });
return;
}
if (error instanceof UpdateDoctorError) throw error;
throw doctorGatewayMaintenanceError({
env,
phase: "gateway-restoration",
code: findServiceOwnershipRefusal(error)?.reason ?? "doctor-gateway-restoration-failed",
detail: `The managed Gateway could not be restored after Doctor maintenance: ${String(error)}`,
cause: error
});
}
params.runtime.log("Gateway restarted and verified after Doctor repair.");
if (staleReplacement) {
const warning = `Warning: Replaced stale Gateway PID ${staleReplacement.pid ?? "unknown"} through its service manager; verified ${staleReplacement.version} build ${staleReplacement.buildId} after Doctor maintenance.`;
warn(warning);
}
};
const admitRepair = async () => {
inspectingActivation = false;
await assertLegacyGatewayStoppedForMaintenance(env);
await acquireStoppedMaintenanceResources();
assertUpdateAdmissionCurrent?.();
await assertDoctorAgentLeaseAdmission(env);
repairStoresMayBeOpen = true;
};
let admissionFailureHandled = false;
const failAdmission = async (error, assertStopCustody) => {
admissionFailureHandled = true;
if (hasCommandProcessCleanupError(error)) throw error;
try {
if (stopped?.stopped) {
try {
await acquireStoppedMaintenanceResources();
} catch (ownershipError) {
const warning = ownershipError instanceof GatewayStateOwnerContentionError ? `Warning: The stopped Gateway still holds state ownership after the service stop deadline. Shared-state repair is unsafe while that writer remains active. Restoring its service; run ${formatCliCommand("openclaw gateway status --deep", env)}, then retry ${formatCliCommand("openclaw doctor --fix", env)} after shutdown completes.` : `Warning: Doctor could not reacquire maintenance ownership: ${String(ownershipError)} Restoring its service without repairing shared state.`;
warn(warning);
}
const { readConfigFileSnapshot } = await import("./config/config.js");
await finish((await readConfigFileSnapshot({
skipPluginValidation: true,
observe: false
})).config, assertStopCustody, void 0, assertStopCustody ?? assertUpdateAdmissionCurrent);
throwDeferredStateReleaseFailure();
} else {
await release();
throwDeferredStateReleaseFailure();
}
} catch (restoreError) {
throw new AggregateError([error, restoreError], `${String(error)} ${String(restoreError)}`, { cause: restoreError });
}
resolveCommandProcessSignal()?.throwIfAborted();
if (authorityRefused || error instanceof DoctorUnreadableStateDatabaseError) throw error;
const refusal = error instanceof DoctorMaintenanceRefusalError ? error : new DoctorMaintenanceRefusalError(`Doctor could not enter maintenance. ${String(error)}${hasGatewayServiceStopUnsafeError(error) ? "" : ` Stop the Gateway service and other OpenClaw processes using this state, then run ${formatCliCommand("openclaw doctor --fix", env)} from an independent shell.`}`, classifyDoctorMaintenanceRefusal(error), {
cause: error,
...error instanceof UpdateDoctorError ? { failureFacts: error.failureFacts } : {}
});
const recovery = inspectingActivation ? await resolveUpdateDoctorGitRecovery({ root: params.root }) : void 0;
if (recovery) {
refusal.message += `\n${recovery.message}`;
recordUpdateDoctorRefusal(refusal.message);
}
throw refusal;
};
const exit = holdDoctorMaintenanceExit((message) => {
warnings.push(message);
params.runtime.error(message);
});
const state = createDoctorMaintenanceState({
params,
env,
signal: exit.signal,
deadline: () => stopDeadline,
assertCurrent: assertCallerCurrent,
assertReadCurrent: () => assertUpdateAdmissionReadCurrent?.(),
settle,
warn
});
try {
await settle(async () => {
const externallyManaged = isServiceRepairExternallyManaged();
if (externallyManaged) await preflightExternalDoctorAgentLease(env);
if (params.root && isDefaultInstallIdentity(env) && !externallyManaged && await shouldManageGatewayService(env)) {
const { maybeStopManagedServiceBeforeMutableUpdate } = await import("./update-command-service-maintenance-yw-hzrZK.mjs");
inspectingActivation = true;
const inspection = await maybeStopManagedServiceBeforeMutableUpdate({
updateInstallKind: "package",
root: params.root,
shouldRestart: true,
jsonMode: true,
phase: "inspect"
});
assertDoctorMaintenanceInspection(inspection, env);
if (inspection.serviceUpdateVerdict?.kind !== "absent" && inspection.offline !== true) assertAuthority(() => {
const admitted = resolveDoctorUpdateAdmission(env);
assertUpdateAdmissionReadCurrent = () => assertAuthority(admitted.assertCurrent);
const checkAndRecord = () => assertAuthority(admitted.recordContinuation);
assertUpdateAdmissionCurrent = () => state.owner ? state.owner.run(checkAndRecord) : checkAndRecord();
});
if (inspection.serviceUpdateVerdict?.kind === "owned" && inspection.serviceEnv) assertDoctorServiceSelection(env, inspection.serviceEnv);
staleReplacement = await inspectStaleDoctorGateway({
root: params.root,
env,
before: inspection,
assertCurrent: assertUpdateAdmissionCurrent
});
if (parentActivation !== void 0 && !staleReplacement && inspection.serviceUpdateVerdict?.kind === "owned" && inspection.offline !== true) throw new DoctorMaintenanceRefusalError(`Doctor could not enter maintenance. Error: ${await formatUpdateDoctorServiceStopRefusal(inspection.serviceEnv ?? env)}`, {
kind: "data-at-risk",
reason: "gateway-state-unverified"
});
try {
await state.acquire();
} catch (error) {
if (hasCommandProcessCleanupError(error)) throw error;
const servingOwner = readGatewayOwnerLease({
env,
current: true,
openStateSchemaReadAdmission: openDoctorStateSchemaReadAdmission
});
const legacyGatewayLock = servingOwner ? void 0 : await readActiveGatewayLockIdentity({
env: inspection.serviceEnv ?? env,
requireInspection: true
});
if (!inspection.running || !(servingOwner?.state === "live" && servingOwner.mode === "supervised" || inspection.servicePid !== void 0 && legacyGatewayLock?.pid === inspection.servicePid)) throw error;
}
if (inspection.serviceUpdateVerdict?.kind === "owned") {
inspectingActivation = false;
if (inspection.serviceEnv) assertDoctorServiceSelection(env, inspection.serviceEnv);
if (parentActivation === void 0 || staleReplacement) {
inspection.serviceUpdateVerdict.refreshDefinition = inspection.serviceUpdateVerdict.requiresInstallRootRefresh === true;
const root = params.root;
const { withGatewayServiceOperationLock } = await import("./service-operation-lock-D_K5BO0M.mjs");
await withGatewayServiceOperationLock(inspection.serviceEnv ?? env, async (assertStopCustody) => {
let assertServiceCurrent = assertUpdateAdmissionCurrent;
try {
await settle(async () => {
try {
stopDeadline = performance.now() + GATEWAY_SERVICE_STOP_TIMEOUT_MS;
const stopService = () => maybeStopManagedServiceBeforeMutableUpdate({
updateInstallKind: "package",
root,
shouldRestart: true,
jsonMode: true,
expectedService: inspection,
retainNativeIdentity: true,
assertCurrent: () => assertServiceCurrent?.(),
warn,
onStopped: (before) => {
stopped = before;
}
});
stopped = await (state.resources ? state.resources.run(stopService) : stopService());
assertDoctorMaintenanceInspection(stopped, env);
if (stopped.serviceUpdateVerdict?.kind === "unavailable") warn(stopped.serviceUpdateVerdict.message);
if (staleReplacement && stopped.serviceEnv) await assertStaleDoctorGatewayStopped({
stale: staleReplacement,
env: stopped.serviceEnv,
assertCurrent: assertUpdateAdmissionCurrent
});
} catch (error) {
if (!staleReplacement || hasCommandProcessCleanupError(error)) throw error;
throw doctorGatewayMaintenanceError({
env,
phase: "gateway-stop",
code: "stale-gateway-stop-failed",
detail: String(error),
cause: error
});
}
if (stopped?.stopped) params.runtime.log("Stopped the managed Gateway for Doctor repair.");
await admitRepair();
assertStopCustody();
});
} catch (error) {
assertServiceCurrent = assertStopCustody;
await failAdmission(error, assertStopCustody);
}
});
}
} else if (inspection.serviceUpdateVerdict?.kind === "unavailable") warn(inspection.serviceUpdateVerdict.message);
else if (inspection.serviceUpdateVerdict?.kind !== "absent") params.runtime.log("The stopped Gateway service was left unchanged; repairing Doctor's selected state only.");
}
if (!repairStoresMayBeOpen) await admitRepair();
stopped?.windowsTaskAutoStartRecovery?.beginMutation();
retainStoppedInstallation = stopped?.serviceUpdateVerdict?.kind === "owned" && stopped.serviceUpdateVerdict.requiresInstallRootRefresh === true;
await state.relocateLegacyRoot();
});
} catch (error) {
try {
if (admissionFailureHandled) throw error;
await failAdmission(error);
} finally {
exit.release(true);
}
}
let custody = "held";
const maintenance = {
signal: exit.signal,
warnings,
failureFacts,
get databaseWrites() {
return state.receipt;
},
run: (operation) => state.run(operation),
releaseState: () => settle(releaseState),
async release() {
if (this !== maintenance) throw new Error("Gateway restoration requires its original live maintenance owner.");
custody = "released";
try {
await release();
throwDeferredStateReleaseFailure();
} catch (error) {
exit.release(true);
throw error;
} finally {
exit.release();
}
},
async finish(initialConfig, writeConfig, failure) {
let cfg = initialConfig;
if (cleanupFailure) throw cleanupFailure.error;
const assertCustody = (expected = "restoring") => {
if (this !== maintenance || custody !== expected) throw new Error("Gateway restoration requires its original live maintenance owner.");
};
assertCustody("held");
custody = "restoring";
let failed = failure !== void 0;
try {
if (!cfg && !stopped?.stopped) {
await release(assertCustody);
return;
}
if (!cfg) try {
const { readConfigFileSnapshot } = await import("./config/config.js");
cfg = (await readConfigFileSnapshot({
skipPluginValidation: true,
observe: false
})).config;
} catch (error) {
throw new DoctorMaintenanceRefusalError(`Doctor could not restore the Gateway because persisted repair state is not ready: ${formatErrorMessage(error)}`, {
kind: "data-at-risk",
reason: "incomplete-migration"
}, { cause: error });
}
await finish(cfg, assertCustody, writeConfig);
throwDeferredStateReleaseFailure();
} catch (restoreError) {
failed = true;
if (failure !== void 0) throw new AggregateError([failure, restoreError], `${formatErrorMessage(failure)} ${formatErrorMessage(restoreError)}`, { cause: restoreError });
throw restoreError;
} finally {
custody = "released";
exit.release(failed);
}
}
};
return maintenance;
}
//#endregion
export { beginDoctorMaintenance };