openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
528 lines (527 loc) • 23.5 kB
JavaScript
import { r as resolveOpenClawPackageRootSync } from "../openclaw-root-DWMGzHgM.mjs";
import { r as defaultRuntime } from "../runtime-BC29JSZp.mjs";
import { t as finalizeActiveDebugProxyCaptures } from "../runtime-cleanup-D3XlaCOY.mjs";
import { o as withCliProcessScope, r as retainCliProcessJobUntilExit } from "../runtime-cleanup-scope-BdzwA5yn.mjs";
import { r as resolveEnvironmentValue } from "../process-env-DlZFJzq6.mjs";
import { a as createSqliteLifecycleAggregateError } from "../sqlite-lifecycle-errors-DuOygjcn.mjs";
import "../openclaw-state-db-contract-CQDemVBb.mjs";
import { i as routeLogsToStderr } from "../console-cjqeMbm6.mjs";
import { a as POST_CORE_UPDATE_RESULT_PATH_ENV } from "../update-post-core-context-B2x24New.mjs";
import { n as openDoctorStateSchemaReadAdmission } from "../openclaw-state-db-doctor-schema-CkfI7JQa.mjs";
import { r as hasCommandProcessCleanupError } from "../exec-result-LNNPagt0.mjs";
import { t as resolveUpdateInstallRoot } from "../update-install-root-DH-onosx.mjs";
import { a as closeOpenClawStateDatabaseAsync } from "../openclaw-state-db-cache-BEArcjbO.mjs";
import "../openclaw-agent-db-contract-ClUH-Vmp.mjs";
import "../openclaw-state-db-VZFEEleX.mjs";
import { b as writeUpdatePostInstallDoctorResult, i as UPDATE_POST_INSTALL_DOCTOR_RESULT_PATH_ENV, v as recordUpdateDoctorConfigWriteRefusal } from "../update-doctor-result-T_IRct7a.mjs";
import { n as readGatewayOwnerLease } from "../gateway-owner-lease-90DnCeMV.mjs";
import { f as recordUpdateRunStep, n as adoptUpdateRun } from "../update-run-ledger-1TfJC2gT.mjs";
import { i as getUpdateRun } from "../update-run-reader-DrO5nA1i.mjs";
import { n as createManagedUpdateRequesterAuthority, r as createManagedUpdateRequesterContinuationAuthority, t as UpdateRequesterRevokedError } from "../update-requester-authority-CCs7D0Vp.mjs";
import { n as withDelegatedUpdateCommandExecutor, t as withUpdateCommandExecutor } from "../update-command-executor-TUUoFiFs.mjs";
import "../update-control-plane-sentinel-BXGUG6j_.mjs";
import { v as POST_CORE_EXECUTOR_CAPABILITY, y as POST_CORE_MUTATION_PROTOCOL } from "../package-update-activation-CeJy3CZW.mjs";
import { d as isOmittedUpdateTimeout, l as writePostCoreUpdateFailureFile, t as finishUpdate } from "../update-command-post-update-C34zOPO1.mjs";
import "../gateway-shutdown-budget-v-vGFkEN.mjs";
import { a as createWindowsTaskAutoStartRecovery, n as maybeStopManagedServiceBeforeMutableUpdate, t as createWindowsTaskAutoStartGuard } from "../update-command-service-maintenance-bmxRr-nv.mjs";
import { c as formatUpdateFinalizationError, n as UpdateCommandFailure } from "../update-command-result-BV528p_F.mjs";
import { l as withUpdateCommandTerminalResult } from "../update-command-terminal-c5LuZQ0S.mjs";
import { t as resolveUpdateFinalizationTimeoutMs } from "../update-finalization-budget-CXL950z_.mjs";
import path from "node:path";
import fs from "node:fs/promises";
//#region src/cli/update-cli/update-command-legacy-service-stop.ts
/**
* A legacy updater (through 2026.9.5) inspects the managed Gateway service with
* its own manager adapter before handing finalization to this candidate. When
* that inspection is unavailable it transfers an unstopped, still-running
* service, so the candidate's Doctor cannot enter maintenance: the predecessor
* Gateway keeps gateway-lifecycle and, being supervised, never releases it on
* its own. The candidate must inspect and stop that service itself.
*/
function needsCandidateManagedServiceStop(params) {
const transferred = params.preManagedServiceStop;
return params.shouldRestart && params.mode !== "unknown" && transferred?.serviceUpdateVerdict?.kind === "unavailable" && !transferred.stopped && !transferred.inspected && params.windowsTaskAutoStartSuspended !== true;
}
//#endregion
//#region src/infra/update-candidate-predecessor-stop.ts
/**
* Ledger receipt the delegated Doctor records at the native stop boundary.
* The `finalize:` prefix keeps it through count eviction in both this codec and
* the published parents'; the identity lives in the key because those parents
* strip `detail` from retained steps under the byte limit.
*/
const RECEIPT_PREFIX = "finalize:predecessor-stop:";
function serviceIdentity(state, stoppedAtMs) {
const verdict = state.serviceUpdateVerdict;
return {
...state.servicePid !== void 0 ? { pid: state.servicePid } : {},
...verdict && "fingerprint" in verdict ? { fingerprint: verdict.fingerprint } : {},
...state.serviceManagerUid !== void 0 ? { managerUid: state.serviceManagerUid } : {},
stoppedAtMs
};
}
function encodeReceipt(identity) {
const field = (value) => value === void 0 ? "-" : String(value).replaceAll(":", "_");
return `${RECEIPT_PREFIX}${identity.stoppedAtMs}:${field(identity.managerUid)}:${field(identity.pid)}:${field(identity.fingerprint)}`;
}
function decodeReceipt(step) {
if (!step.startsWith(RECEIPT_PREFIX)) return;
const [stoppedAt, managerUid, pid, fingerprint] = step.slice(26).split(":");
const stoppedAtMs = Number(stoppedAt);
if (!Number.isFinite(stoppedAtMs)) return;
const number = (value) => value === void 0 || value === "-" || !/^\d+$/.test(value) ? void 0 : Number(value);
const identity = { stoppedAtMs };
const parsedPid = number(pid);
const parsedUid = number(managerUid);
if (parsedPid !== void 0) identity.pid = parsedPid;
if (parsedUid !== void 0) identity.managerUid = parsedUid;
if (fingerprint && fingerprint !== "-") identity.fingerprint = fingerprint;
return identity;
}
function readDoctorStop(runId, ledger) {
for (const step of getUpdateRun(runId, ledger)?.steps ?? []) {
if (step.status !== "completed") continue;
const identity = decodeReceipt(step.step);
if (identity) return identity;
}
}
/**
* A legacy updater (through 2026.9.5) that could not inspect the managed
* service leaves the predecessor Gateway running and then delegates Doctor to
* this candidate. That supervised owner keeps gateway-lifecycle until its
* service manager stops it, so Doctor can never enter maintenance. Stop it
* here with this candidate's adapter and record the stopped service's identity
* at the mutation boundary; finalization restarts the updated service.
*/
async function stopSupervisedPredecessorGateway(input, params) {
if (!input.repair || process.platform === "win32") return false;
let owner;
try {
owner = readGatewayOwnerLease({
env: process.env,
current: true,
openStateSchemaReadAdmission: openDoctorStateSchemaReadAdmission
});
} catch {
return false;
}
if (owner?.state !== "live" || owner.mode !== "supervised") return false;
params.assertCurrent();
let recorded = false;
const record = (state) => {
if (recorded) return;
recorded = true;
const stoppedAtMs = state.stoppedAtMs ?? Date.now();
recordUpdateRunStep(input.runId, {
step: encodeReceipt(serviceIdentity(state, stoppedAtMs)),
status: "completed",
endedAtMs: stoppedAtMs
});
};
try {
const state = await maybeStopManagedServiceBeforeMutableUpdate({
updateInstallKind: "package",
root: params.root,
shouldRestart: true,
jsonMode: true,
phase: "prepare",
timeoutMs: params.timeoutMs ?? 33e4,
onStopped: record,
assertCurrent: params.assertCurrent,
warn: params.warn
});
if (state.stopped) record(state);
} catch (error) {
if (!recorded) throw error;
params.warn(`Predecessor Gateway stop reported an error after its native mutation: ${String(error)}`);
}
return recorded;
}
/**
* Finalization: adopt a stop the delegated Doctor recorded for this run, or
* perform the candidate's own stop when a legacy parent transferred an
* uninspected service. A recorded stop is adopted only after a mutation-free
* inspection reports the same service identity, and a Gateway the candidate
* itself stopped is never left down under --no-restart.
*/
async function adoptCandidateManagedServiceStop(params) {
const unchanged = {
stopped: params.transferred,
restartRequired: false
};
if (process.platform === "win32") return unchanged;
const startedAt = Date.now();
const stopStep = (advisory) => ({
name: "managed-service",
command: "stop managed gateway service before Doctor (candidate inspection)",
cwd: params.root,
durationMs: Date.now() - startedAt,
exitCode: 0,
...advisory ? { advisory: {
kind: "recoverable-maintenance",
message: advisory
} } : {}
});
const updateInstallKind = params.mode === "git" ? "git" : "package";
const doctorStop = readDoctorStop(params.runId, params.ledger);
if (doctorStop) {
const inspected = await maybeStopManagedServiceBeforeMutableUpdate({
updateInstallKind,
root: params.root,
shouldRestart: true,
jsonMode: true,
timeoutMs: params.timeoutMs,
phase: "inspect",
assertCurrent: params.assertCurrent
});
if (!inspected.inspected) return unchanged;
const current = serviceIdentity(inspected, doctorStop.stoppedAtMs);
if (current.fingerprint !== doctorStop.fingerprint || current.managerUid !== doctorStop.managerUid) {
params.onStep({
...stopStep("The Gateway service was replaced after update Doctor stopped its predecessor; the recorded stop was not adopted and the current service was left untouched."),
exitCode: 1
});
return unchanged;
}
if (inspected.running) return {
stopped: inspected,
restartRequired: false
};
const restartRequired = !params.shouldRestart;
params.onStep(stopStep(restartRequired ? "The previous Gateway had to be stopped for update Doctor maintenance; it is restarted on the updated installation despite --no-restart." : void 0));
return {
stopped: {
...inspected,
stopped: true,
stoppedAtMs: doctorStop.stoppedAtMs
},
restartRequired
};
}
if (!needsCandidateManagedServiceStop({
...params,
preManagedServiceStop: params.transferred
})) return unchanged;
let stopped = params.transferred;
const state = await maybeStopManagedServiceBeforeMutableUpdate({
updateInstallKind,
root: params.root,
shouldRestart: true,
jsonMode: true,
timeoutMs: params.timeoutMs,
phase: "prepare",
onStopped: (current) => {
stopped = current;
},
assertCurrent: params.assertCurrent
});
if (state.inspected || state.stopped) stopped = state;
if (stopped?.stopped) params.onStep(stopStep());
return {
stopped,
restartRequired: false
};
}
//#endregion
//#region src/infra/update-migrated-finalize.worker.ts
async function finalizeMigratedUpdate() {
if (process.argv[2] === "--check") {
const { finishUpdateRun } = await import("../cli/daemon-cli.js");
routeLogsToStderr();
if (typeof finishUpdateRun !== "function") throw new Error("Update recovery writer is unavailable.");
process.stdout.write(JSON.stringify({
executorDelegation: "pid-start-v1",
postCoreExecutor: POST_CORE_EXECUTOR_CAPABILITY,
mutationProtocol: POST_CORE_MUTATION_PROTOCOL,
retainedOwnerBinding: true,
doctorConfigWrites: "pid-start-v1",
gatewayRestartCompletion: true,
state: 19,
agent: 24
}));
return;
}
await withCliProcessScope(retainCliProcessJobUntilExit);
const chunks = [];
for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
const text = Buffer.concat(chunks).toString("utf8");
if (process.argv[2] === "--post-core") return await runDelegatedPostCore(JSON.parse(text));
if (process.argv[2] === "--doctor") return await runDelegatedDoctor(JSON.parse(text));
const input = JSON.parse(text);
if (input.params.opts.json) routeLogsToStderr();
if (input.recoveryHandoff) throw new Error("Full-state checkpoint recovery is deferred; retained state was left unchanged.");
const omittedOperatorTimeout = isOmittedUpdateTimeout(input.params.opts.timeout, input);
if (omittedOperatorTimeout) input.params.opts.timeout = void 0;
const activationTimeoutMs = input.params.opts.run?.activationTimeoutMs ?? (omittedOperatorTimeout ? void 0 : await resolveUpdateFinalizationTimeoutMs(input.params.updateStepTimeoutMs, {
env: input.params.ownedManagedUpdateEnv ?? input.params.opts.run?.env,
databases: input.params.schemaVersions,
pluginCount: Object.keys(input.params.preUpdatePluginInstallRecords).length
}));
let publishedRecord;
const finalized = await withUpdateCommandTerminalResult(async (registerRun) => {
if (input.executor) return await withDelegatedUpdateCommandExecutor(input.executor, input.params.opts.run?.runId ?? "", input.params.result.root ?? input.params.root, async (fence) => finalizeInput(input, fence, registerRun), activationTimeoutMs === void 0 ? void 0 : { activationTimeoutMs });
const admissionEnv = input.params.ownedManagedUpdateEnv ?? input.params.opts.run?.env;
if (!admissionEnv) throw new Error("Grantless finalization requires its captured update environment.");
const meta = input.params.controlPlaneUpdateSentinelMeta;
const runId = input.params.opts.run?.runId ?? "";
const scratch = path.dirname(input.resultPath);
const legacyManagedParent = input.params.result.before?.version === "2026.9.3" && admissionEnv.OPENCLAW_UPDATE_RUN_HANDOFF === "1" && admissionEnv.OPENCLAW_UPDATE_RUN_ID === runId && meta?.runId === runId && meta.handoffId && meta.root && meta.root === resolveUpdateInstallRoot(input.params.result.root ?? input.params.root) && path.basename(scratch).startsWith("openclaw-update-migrated-") && path.basename(input.resultPath) === "result.json" && [
"TMPDIR",
"TMP",
"TEMP"
].every((name) => resolveEnvironmentValue(process.env, name) === scratch) ? {
runId,
handoffId: meta.handoffId,
root: meta.root
} : void 0;
for (const name of [
"TMPDIR",
"TMP",
"TEMP"
]) {
const value = resolveEnvironmentValue(admissionEnv, name);
if (value === void 0) delete process.env[name];
else process.env[name] = value;
}
return await withUpdateCommandExecutor(runId, async (executor) => {
const fence = await executor.enter(input.params.result.root ?? input.params.root, { activationTimeoutMs });
return await finalizeInput(input, fence, registerRun);
}, legacyManagedParent ? { legacyManagedParent } : void 0);
}, {
...input.params.opts,
onTerminalRecord: (record) => {
publishedRecord = record;
}
});
const terminal = publishedRecord ?? getUpdateRun(finalized.run.runId, { env: finalized.run.env });
const gatewayRestartPending = finalized.run.completionOwner === "gateway-restart" && finalized.run.gatewayRestartRequired === true && finalized.result.status === "ok" && terminal?.status === "running" && terminal.phase === "restarting";
if (!terminal || terminal.runId !== finalized.run.runId || terminal.status === "running" && !gatewayRestartPending) throw new Error("Update finalization left the update run nonterminal.");
const response = {
result: finalized.result,
exitCode: finalized.exitCode,
candidateStartAttempted: finalized.candidateStartAttempted || gatewayRestartPending,
...gatewayRestartPending ? { restartRunId: terminal.runId } : { terminalRunId: terminal.runId },
executorDelegation: "pid-start-v1",
automaticTriage: finalized.automaticTriage
};
await fs.writeFile(input.resultPath, JSON.stringify(response), { mode: 384 });
}
async function runDelegatedPostCore(input) {
const resultPath = process.env[POST_CORE_UPDATE_RESULT_PATH_ENV]?.trim();
const executingRoot = resolveOpenClawPackageRootSync({ moduleUrl: import.meta.url });
if (!resultPath || !input.executor || process.env["OPENCLAW_UPDATE_POST_CORE"] !== "1" || process.env["OPENCLAW_UPDATE_RUN_ID"] !== input.runId || !executingRoot || resolveUpdateInstallRoot(executingRoot) !== resolveUpdateInstallRoot(input.root)) throw new Error("Post-core update does not match its delegated run and installed runtime.");
if (input.opts.json) routeLogsToStderr();
process.env[POST_CORE_UPDATE_RESULT_PATH_ENV] = `${resultPath}.pending`;
try {
await withDelegatedUpdateCommandExecutor(input.executor, input.runId, input.root, async (fence) => {
const requesterAuthority = input.requester?.authorizationSource?.startsWith("profile:") ? await createManagedUpdateRequesterContinuationAuthority(input.requester, {
runId: input.runId,
executor: fence
}) : input.requester ? await createManagedUpdateRequesterAuthority(input.requester) : void 0;
const { updateCommand } = await import("../update-command-CIc4izHy.mjs");
fence.assertCurrent();
if (requesterAuthority?.isCurrent() === false) throw new UpdateRequesterRevokedError();
await updateCommand({
...input.opts,
run: {
runId: input.runId,
env: { ...process.env },
executorFence: fence,
...requesterAuthority ? { requesterAuthority } : {}
}
});
});
await fs.rename(`${resultPath}.pending`, resultPath);
} catch (error) {
if (!hasCommandProcessCleanupError(error)) await writePostCoreUpdateFailureFile(resultPath, error);
throw error;
}
}
async function runDelegatedDoctor(input) {
const resultPath = process.env[UPDATE_POST_INSTALL_DOCTOR_RESULT_PATH_ENV]?.trim();
if (!resultPath || !input.executor) throw new Error("Update Doctor requires its delegated executor and result path.");
await withDelegatedUpdateCommandExecutor(input.executor, input.runId, input.root, async (fence, commandAuthority) => {
const requester = input.requester?.authorizationSource?.startsWith("profile:") ? await createManagedUpdateRequesterContinuationAuthority(input.requester, {
runId: input.runId,
executor: fence
}) : input.requester ? await createManagedUpdateRequesterAuthority(input.requester) : void 0;
const assertCurrent = () => {
try {
fence.assertCurrent();
if (requester?.isCurrent() === false) throw new UpdateRequesterRevokedError();
} catch (error) {
recordUpdateDoctorConfigWriteRefusal({
reason: error instanceof UpdateRequesterRevokedError ? error.code : "authority-check-failed",
message: formatUpdateFinalizationError(error),
keys: []
});
throw error;
}
};
try {
assertCurrent();
} catch (error) {
if (!(error instanceof UpdateRequesterRevokedError)) throw error;
fence.assertCurrent();
await writeUpdatePostInstallDoctorResult({
resultPath,
result: {
status: "error",
configWriteRefusal: {
reason: error.code,
message: error.message,
keys: []
}
}
});
process.exitCode = 1;
return;
}
const { runDoctorHealthFlow } = await import("../doctor-health-D0SRVqcY.mjs");
assertCurrent();
await stopSupervisedPredecessorGateway(input, {
root: input.root,
assertCurrent,
warn: (message) => process.stderr.write(`${message}\n`)
});
assertCurrent();
await runDoctorHealthFlow({
...defaultRuntime,
exit: (code) => {
process.exitCode = code;
}
}, {
repair: input.repair,
nonInteractive: true,
...input.yes !== void 0 ? { yes: input.yes } : {},
...input.workspaceSuggestions !== void 0 ? { workspaceSuggestions: input.workspaceSuggestions } : {}
}, {
inputHash: input.configInputHash,
assertCurrent,
commandAuthority,
...input.databaseGenerations ? { databaseGenerations: input.databaseGenerations } : {},
...input.postCoreSchemaRepair === true ? { postCoreSchemaRepair: {
runId: input.runId,
assertCurrent
} } : {}
});
});
}
async function finalizeInput(input, executorFence, registerRun) {
const transferredRun = input.params.opts.run;
if (!transferredRun || "executorFence" in transferredRun || !input.recoveryHandoff && input.params.rollbackBlockedReason !== "state-migrated-no-rollback" && input.params.rollbackBlockedReason !== "rollback-state-unverified") throw new Error("Update finalization requires its migrated update run.");
const { requesterAuthority: descriptor, ...runIdentity } = transferredRun;
executorFence.assertCurrent();
adoptUpdateRun(runIdentity.runId, { env: runIdentity.env });
const run = {
...runIdentity,
executorFence,
...descriptor ? { requesterAuthority: descriptor.requester.authorizationSource?.startsWith("profile:") ? await createManagedUpdateRequesterContinuationAuthority(descriptor.requester, {
runId: runIdentity.runId,
executor: executorFence
}, runIdentity.env) : await createManagedUpdateRequesterAuthority(descriptor.requester, runIdentity.env) } : {}
};
executorFence.assertCurrent();
registerRun(run);
for (const step of input.bufferedSteps) {
executorFence.assertCurrent();
recordUpdateRunStep(run.runId, step, { env: run.env });
}
const { stopped, restartRequired } = await adoptCandidateManagedServiceStop({
transferred: input.params.preManagedServiceStop,
shouldRestart: input.params.shouldRestart,
mode: input.params.result.mode,
windowsTaskAutoStartSuspended: input.windowsTaskAutoStartSuspended,
runId: run.runId,
ledger: { env: run.env },
root: input.params.result.root ?? input.params.root,
timeoutMs: input.params.updateStepTimeoutMs,
assertCurrent: () => {
executorFence.assertCurrent();
if (run.requesterAuthority?.isCurrent() === false) throw new UpdateRequesterRevokedError();
},
onStep: (step) => input.params.result.steps.push(step)
});
if (restartRequired) input.params.shouldRestart = true;
if (input.windowsTaskAutoStartSuspended && !stopped?.serviceEnv) throw new Error("Transferred Windows task suspension is missing its stopped service owner.");
const windowsRecovery = input.windowsTaskAutoStartSuspended && stopped?.serviceEnv ? createWindowsTaskAutoStartRecovery({
serviceEnv: stopped.serviceEnv,
updateRun: run,
alreadySuspended: true,
assertCurrentService: createWindowsTaskAutoStartGuard({
root: input.params.result.root ?? input.params.root,
before: stopped,
timeoutMs: input.params.updateStepTimeoutMs
}),
assertCurrent: () => {
run.executorFence?.assertCurrent();
if (getUpdateRun(run.runId, { env: run.env })?.status !== "running") throw new Error("Update run no longer owns Windows task activation.");
}
}) : void 0;
let result;
let exitCode = 0;
let candidateStartAttempted = false;
let automaticTriage;
try {
result = await finishUpdate({
...input.params,
result: {
...input.params.result,
runId: run.runId
},
opts: {
...input.params.opts,
run
},
...stopped ? { preManagedServiceStop: {
...stopped,
windowsTaskAutoStartRecovery: windowsRecovery
} } : {}
}, {
candidateRuntime: true,
onGatewayStartAttempted: () => {
candidateStartAttempted = true;
}
});
} catch (error) {
if (!(error instanceof UpdateCommandFailure)) throw error;
result = error.result;
exitCode = error.exitCode;
automaticTriage = error.automaticTriage;
} finally {
await windowsRecovery?.complete(result?.status === "ok" || result?.recovery?.serviceRestartSafe === true && result.recovery.service === "healthy");
}
executorFence.assertCurrent();
return {
run,
result,
exitCode,
automaticTriage,
candidateStartAttempted
};
}
(async () => {
const errors = [];
try {
await finalizeMigratedUpdate();
} catch (error) {
errors.push(error);
}
try {
await finalizeActiveDebugProxyCaptures();
} catch (error) {
errors.push(error);
}
try {
await closeOpenClawStateDatabaseAsync();
} catch (error) {
errors.push(error);
}
if (errors.length === 1) throw errors[0];
if (errors.length > 1) throw createSqliteLifecycleAggregateError(errors, "Update finalization and resource cleanup failed.", errors[0]);
})().catch((error) => {
process.stderr.write(`${formatUpdateFinalizationError(error)}\n`);
process.exitCode = 1;
});
//#endregion
export {};