fix: open terminals no longer delay gateway updates (#121601)

* fix(gateway): allow updates with open terminals

* fix(updater): support terminal policy upgrades

* chore(plugin-sdk): refresh API baseline
This commit is contained in:
Peter Steinberger
2026-08-11 09:37:58 -07:00
committed by GitHub
parent b45ddf2754
commit 1dcac5b15f
34 changed files with 506 additions and 53 deletions
@@ -262,6 +262,47 @@ function gatewayCliOperation(args) {
return "gateway.cli";
}
function isLegacyGatewaySuspendPrepareParamsError(error) {
if (
!(error instanceof UpdateCommandError) ||
error.operation !== "gateway.suspend.prepare" ||
error.status !== 1
) {
return false;
}
const cause = ownDataProperty(error, "cause");
const stdout = ownDataProperty(cause, "stdout");
if (typeof stdout !== "string" || !stdout.trim()) {
return false;
}
let payload;
try {
payload = JSON.parse(stdout.trim());
} catch {
return false;
}
if (
typeof payload !== "object" ||
payload === null ||
Array.isArray(payload) ||
Object.keys(payload).length !== 2 ||
payload.ok !== false
) {
return false;
}
const requestError = payload.error;
return (
typeof requestError === "object" &&
requestError !== null &&
!Array.isArray(requestError) &&
Object.keys(requestError).length === 4 &&
requestError.type === "gateway_request_error" &&
requestError.code === "INVALID_REQUEST" &&
requestError.message === "invalid gateway.suspend.prepare params" &&
requestError.retryable === false
);
}
async function runUpdateCommand(runCommand, operation, command, args, checkout, options) {
try {
return await runCommand(command, args, checkout, options);
@@ -1845,8 +1886,8 @@ export function runBuiltGatewayCall(checkout, method, params, deployment) {
/**
* @param {string} checkout
* @param {(checkout: string, method: string, params: { requestId: string }, deployment: GatewayDeploymentRef | null) => string} [callGateway]
* @param {GatewayDeploymentRef | null} [deployment]
* @param {(checkout: string, method: string, params: { requestId: string, terminalPolicy?: "terminate" }, deployment: GatewayCliDeployment | null) => string} [callGateway]
* @param {GatewayCliDeployment | null} [deployment]
*/
export function prepareGatewaySuspension(
checkout,
@@ -1854,11 +1895,20 @@ export function prepareGatewaySuspension(
deployment = null,
) {
const requestId = `openclaw-live-updater-${randomUUID()}`;
const callPrepare = (params) =>
JSON.parse(callGateway(checkout, "gateway.suspend.prepare", params, deployment));
let result;
try {
result = JSON.parse(
callGateway(checkout, "gateway.suspend.prepare", { requestId }, deployment),
);
try {
result = callPrepare({ requestId, terminalPolicy: "terminate" });
} catch (error) {
if (!isLegacyGatewaySuspendPrepareParamsError(error)) {
throw error;
}
// Older closed schemas reject the new field before acquiring a lease.
// Retry once with preserve semantics so mixed-version updates remain safe.
result = callPrepare({ requestId });
}
} catch (error) {
throw new UpdateInvariantError(
"gateway_suspend_prepare_failed",
@@ -2747,7 +2797,7 @@ function defaultSleep(ms) {
}
/**
* @param {(command: string, args: string[], checkout: string, options?: Record<string, unknown>) => unknown | Promise<unknown>} runCommand
* @param {(command: string, args: string[], checkout: string, options?: Record<string, unknown>) => void | Promise<void>} runCommand
* @param {string} checkout
* @param {string} expectedSha
* @param {(ms: number) => void | Promise<void>} [sleep]
@@ -2786,7 +2836,7 @@ export async function verifyGatewayReadiness(
for (let attempt = 1; attempt <= GATEWAY_READINESS_ATTEMPTS; attempt += 1) {
try {
if (deployment) {
markGatewayMilestones(timing, await probeMilestones(deployment), timestampAt(now));
markGatewayMilestones(timing, probeMilestones(deployment), timestampAt(now));
}
const deepRpcReadyAt = await verifyGatewayDeepRpc(
runCommand,
@@ -2799,7 +2849,7 @@ export async function verifyGatewayReadiness(
if (deployment) {
markGatewayMilestones(
timing,
await probeMilestones(deployment),
probeMilestones(deployment),
timestampAt(now),
deepRpcReadyAt,
);