@alfe.ai/gateway 0.10.6 → 0.10.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- import { p as supportsOpenClawNode, u as OPENCLAW_NODE_REQUIREMENT } from "./runtime-process.js";
1
+ import { w as supportsOpenClawNode, y as OPENCLAW_NODE_REQUIREMENT } from "./runtime-upgrade-recovery.js";
2
2
  import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
3
3
  import { dirname, join } from "node:path";
4
4
  import { homedir } from "node:os";
@@ -1,8 +1,13 @@
1
- import { l as captureRuntimeCrash, u as captureRuntimeErrorOutput } from "./sentry.js";
1
+ import { h as logger$1, i as captureCliFailure, l as captureRuntimeCrash, u as captureRuntimeErrorOutput } from "./sentry.js";
2
2
  import { createRequire } from "node:module";
3
+ import { lstat, mkdir, readFile, readdir, rename, rm, symlink, unlink, writeFile } from "node:fs/promises";
4
+ import { dirname, join, relative } from "node:path";
5
+ import { homedir } from "node:os";
6
+ import { existsSync } from "node:fs";
7
+ import { execFile, spawn } from "node:child_process";
8
+ import { promisify } from "node:util";
3
9
  import pino from "pino";
4
10
  import crypto from "crypto";
5
- import { spawn } from "node:child_process";
6
11
  import stream, { Readable } from "stream";
7
12
  import util, { format } from "util";
8
13
  import http from "http";
@@ -52,6 +57,9 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
52
57
  * - the daemon `runtime.update` default (this package)
53
58
  * - managed provisioning (`services/compute/src/lib/provisioner.ts` installs the
54
59
  * Node.js 24 channel before the CLI consumes this pin)
60
+ * - the dashboard/fleet upgrade ceiling: `services/agents` mirrors this literal
61
+ * as `PINNED_OPENCLAW_VERSION` in `version-catalog.service.ts` (its test fails
62
+ * on drift) — bump both in the same change
55
63
  *
56
64
  * Pinning (rather than floating `npm install -g openclaw` → `latest`) is deliberate:
57
65
  * OpenClaw runs on every managed agent, so a `latest` that changes runtime semantics —
@@ -100,6 +108,67 @@ function supportsOpenClawNode(nodeVersion) {
100
108
  function parseOpenClawVersion(output) {
101
109
  return /(?:^|\s)(\d{4}\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)(?:\s|$)/u.exec(output.trim())?.[1];
102
110
  }
111
+ /**
112
+ * Compare two EXACT OpenClaw `YYYY.M.P` versions numerically. Anything else —
113
+ * a pre-release/build suffix (`2026.9.5-rc.2`, `2026.9.5+build`), a fourth
114
+ * segment (`2026.9.5.1`), a branded `--version` line — is `undefined`, never
115
+ * "equal": callers treat `undefined` as "not a validated release".
116
+ */
117
+ function compareOpenClawVersions(a, b) {
118
+ const parse = (value) => {
119
+ const match = /^(\d{4})\.(\d+)\.(\d+)$/u.exec(value.trim());
120
+ return match ? [
121
+ Number(match[1]),
122
+ Number(match[2]),
123
+ Number(match[3])
124
+ ] : void 0;
125
+ };
126
+ const left = parse(a);
127
+ const right = parse(b);
128
+ if (!left || !right) return void 0;
129
+ for (let i = 0; i < 3; i++) if (left[i] !== right[i]) return left[i] - right[i];
130
+ return 0;
131
+ }
132
+ /**
133
+ * OpenClaw 2026.8.1 dropped the 2026.6.x `register ?? activate` plugin-export
134
+ * alias: from 2026.8 a plugin whose default export has no `register` function
135
+ * fails to load ("missing register/activate export") and `doctor --fix` exits
136
+ * non-zero. `@alfe.ai/*` plugins export `register` from alfe manifest 1.1.50
137
+ * (#373); anything installed from an older manifest is activate-only.
138
+ */
139
+ const OPENCLAW_REGISTER_EXPORT_REQUIRED_FROM = "2026.8.0";
140
+ /** Whether `version` is an OpenClaw that only loads `register`-exporting plugins. */
141
+ function openClawRequiresRegisterExport(version) {
142
+ if (!version) return false;
143
+ const cmp = compareOpenClawVersions(version, OPENCLAW_REGISTER_EXPORT_REQUIRED_FROM);
144
+ return cmp !== void 0 && cmp >= 0;
145
+ }
146
+ /**
147
+ * The OpenClaw version a `runtime.update` actually installs. The pin is the
148
+ * ceiling: a requested version above it (the dashboard offered npm `latest`,
149
+ * 2026.9.7, to Dana on 2026-10-02) or an unparseable one is clamped to the pin,
150
+ * because an unvalidated runtime is a fleet outage with no Alfe deploy. Versions
151
+ * at or below the pin pass through (operator-chosen rollback targets).
152
+ */
153
+ function resolveOpenClawUpgradeTarget(requested) {
154
+ if (!requested) return {
155
+ version: PINNED_OPENCLAW_VERSION,
156
+ clamped: false
157
+ };
158
+ const cmp = compareOpenClawVersions(requested, PINNED_OPENCLAW_VERSION);
159
+ if (cmp === void 0 || cmp > 0) return {
160
+ version: PINNED_OPENCLAW_VERSION,
161
+ clamped: true
162
+ };
163
+ if (cmp === 0) return {
164
+ version: PINNED_OPENCLAW_VERSION,
165
+ clamped: false
166
+ };
167
+ return {
168
+ version: requested.trim(),
169
+ clamped: false
170
+ };
171
+ }
103
172
  //#endregion
104
173
  //#region ../../node_modules/.pnpm/dotenv@17.2.2/node_modules/dotenv/package.json
105
174
  var require_package = /* @__PURE__ */ __commonJSMin(((exports, module) => {
@@ -15832,4 +15901,396 @@ var RuntimeProcess = class {
15832
15901
  }
15833
15902
  };
15834
15903
  //#endregion
15835
- export { ErrorLineDetector as a, createLogger as c, PINNED_OPENCLAW_VERSION as d, parseOpenClawVersion as f, __require as h, CaptureThrottle as i, OPENCLAW_MANAGED_NODE_MAJOR as l, __commonJSMin as m, OPENCLAW_EXTERNAL_SUPERVISOR_ENV as n, STRICT_ERROR_CAPTURE_PROFILE as o, supportsOpenClawNode as p, RuntimeProcess as r, toFingerprintKey as s, DRAIN_GRACE_MS as t, OPENCLAW_NODE_REQUIREMENT as u };
15904
+ //#region src/runtime-upgrade-recovery.ts
15905
+ /**
15906
+ * Surviving an OS-level restart of the daemon during an OpenClaw upgrade.
15907
+ *
15908
+ * Incident (Dana, prod, 2026-10-02): `apt-daily-upgrade` upgraded libssl and
15909
+ * needrestart restarted `alfe-gateway` while the daemon's `npm install -g
15910
+ * openclaw@…` was mid-reify. systemd SIGKILLed everything in the unit's cgroup,
15911
+ * npm included, leaving a partial `/usr/lib/node_modules/openclaw` (no
15912
+ * `openclaw.mjs`), the intact old tree retired to `.openclaw-<hash>` and the bin
15913
+ * symlink retired to `/usr/bin/.openclaw-<hash>`. The restarted daemon had no
15914
+ * record that an upgrade had been running, so the agent stayed without a
15915
+ * runtime (and still reported `running`).
15916
+ *
15917
+ * Two defences live here:
15918
+ * 1. **Cgroup isolation** — on a systemd host the long, state-mutating
15919
+ * commands (npm install, doctor --fix) run inside a transient scope
15920
+ * (`systemd-run --scope`), so stopping/restarting the daemon's unit no
15921
+ * longer kills them. `--scope` executes the command as the caller's own
15922
+ * child (stdio, exit code and environment are unchanged), it only moves it
15923
+ * into its own cgroup. Off systemd (self-hosted macOS, containers) the
15924
+ * commands run directly, as before.
15925
+ * 2. **Durable marker + boot recovery** — `~/.alfe/runtime-upgrade.json` is
15926
+ * written before the package is replaced and removed when the transaction
15927
+ * finishes. A marker found at boot means the daemon died mid-upgrade:
15928
+ * - phase `installing` (doctor never started, state not migrated): put the
15929
+ * PREVIOUS version back — first by restoring npm's retired
15930
+ * `.openclaw-*` sibling (no network), else `npm install -g
15931
+ * openclaw@<previous>` (or the pin when the previous is unknown);
15932
+ * - phase `migrating` (doctor started; rollback is unsafe): make sure the
15933
+ * TARGET package is installed and re-run the idempotent doctor.
15934
+ * Recovery runs under the exclusive RuntimeGate + CLI lock before the first
15935
+ * runtime start, is bounded to {@link MAX_RECOVERY_ATTEMPTS} boots, and is
15936
+ * reported to Sentry on failure.
15937
+ */
15938
+ const execFileAsync = promisify(execFile);
15939
+ const SYSTEMD_RUN_PATHS = ["/usr/bin/systemd-run", "/bin/systemd-run"];
15940
+ /**
15941
+ * Whether this daemon runs as a systemd service that can start transient
15942
+ * scopes. `INVOCATION_ID` is set by systemd for every unit it starts, so its
15943
+ * absence means "not under systemd" (an interactive/self-hosted daemon, a
15944
+ * container) and the commands run directly.
15945
+ */
15946
+ function detectUpgradeIsolation(env = process.env, uid = process.getuid?.(), hasSystemdRun = () => SYSTEMD_RUN_PATHS.some((path) => existsSync(path)), platform = process.platform) {
15947
+ if (platform !== "linux") return void 0;
15948
+ if (!env.INVOCATION_ID || !hasSystemdRun()) return void 0;
15949
+ if (uid === 0) return { manager: "system" };
15950
+ return env.XDG_RUNTIME_DIR ? { manager: "user" } : void 0;
15951
+ }
15952
+ function upgradeScopeUnitName(now = Date.now()) {
15953
+ return `alfe-openclaw-upgrade-${String(now)}`;
15954
+ }
15955
+ /** Wrap a command so it runs in its own transient scope (own cgroup). */
15956
+ function wrapInUpgradeScope(isolation, unit, command, args) {
15957
+ if (!isolation) return {
15958
+ command,
15959
+ args
15960
+ };
15961
+ return {
15962
+ command: "systemd-run",
15963
+ args: [
15964
+ ...isolation.manager === "user" ? ["--user"] : [],
15965
+ "--scope",
15966
+ "--quiet",
15967
+ "--collect",
15968
+ `--unit=${unit}`,
15969
+ "--",
15970
+ command,
15971
+ ...args
15972
+ ]
15973
+ };
15974
+ }
15975
+ function defaultRuntimeUpgradeMarkerPath() {
15976
+ return join(homedir(), ".alfe", "runtime-upgrade.json");
15977
+ }
15978
+ async function writeRuntimeUpgradeMarker(path, marker) {
15979
+ await mkdir(dirname(path), {
15980
+ recursive: true,
15981
+ mode: 448
15982
+ });
15983
+ const tmp = `${path}.${String(process.pid)}.tmp`;
15984
+ await writeFile(tmp, JSON.stringify(marker, null, 2) + "\n", { mode: 384 });
15985
+ await rename(tmp, path);
15986
+ }
15987
+ async function readRuntimeUpgradeMarker(path) {
15988
+ let raw;
15989
+ try {
15990
+ raw = await readFile(path, "utf8");
15991
+ } catch (err) {
15992
+ if (err.code === "ENOENT") return void 0;
15993
+ throw err;
15994
+ }
15995
+ const parsed = JSON.parse(raw);
15996
+ if (parsed?.runtime !== "openclaw" || parsed.phase !== "installing" && parsed.phase !== "migrating" || typeof parsed.targetVersion !== "string") throw new Error(`Runtime upgrade marker ${path} is malformed`);
15997
+ return parsed;
15998
+ }
15999
+ async function clearRuntimeUpgradeMarker(path) {
16000
+ await rm(path, { force: true });
16001
+ }
16002
+ const NPM_INSTALL_TIMEOUT_MS = 3e5;
16003
+ const DOCTOR_TIMEOUT_MS = 9e5;
16004
+ const DOCTOR_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
16005
+ const SCOPE_WAIT_POLL_MS = 5e3;
16006
+ const SCOPE_WAIT_MAX_MS = 20 * 6e4;
16007
+ /**
16008
+ * `doctor --fix` runs the state/config migration. On 2026.9.x `--json` puts
16009
+ * doctor into READ-ONLY lint mode and it refuses the combination outright
16010
+ * (exit 2: "doctor --json runs read-only lint checks and cannot be combined
16011
+ * with --repair, --fix, or --force"), so the migration must run without it.
16012
+ */
16013
+ const OPENCLAW_DOCTOR_FIX_ARGS = [
16014
+ "doctor",
16015
+ "--fix",
16016
+ "--non-interactive",
16017
+ "--yes"
16018
+ ];
16019
+ async function installedOpenClawVersion(run, env) {
16020
+ try {
16021
+ const { stdout } = await run("openclaw", ["--version"], {
16022
+ timeout: 3e4,
16023
+ env
16024
+ });
16025
+ return parseOpenClawVersion(stdout);
16026
+ } catch {
16027
+ return;
16028
+ }
16029
+ }
16030
+ /** Wait for the interrupted upgrade's scopes to finish so recovery never races a still-running npm/doctor. */
16031
+ async function waitForScopes(run, isolation, units, sleep, now) {
16032
+ if (!isolation || units.length === 0) return;
16033
+ const deadline = now() + SCOPE_WAIT_MAX_MS;
16034
+ for (const unit of units) for (;;) {
16035
+ let active = false;
16036
+ try {
16037
+ await run("systemctl", [
16038
+ ...isolation.manager === "user" ? ["--user"] : [],
16039
+ "is-active",
16040
+ "--quiet",
16041
+ `${unit}.scope`
16042
+ ], { timeout: 1e4 });
16043
+ active = true;
16044
+ } catch {
16045
+ active = false;
16046
+ }
16047
+ if (!active) break;
16048
+ if (now() >= deadline) throw new Error(`upgrade scope ${unit}.scope is still running after ${String(SCOPE_WAIT_MAX_MS / 6e4)} min`);
16049
+ logger$1.info({ unit }, "Interrupted runtime upgrade still running in its scope — waiting");
16050
+ await sleep(SCOPE_WAIT_POLL_MS);
16051
+ }
16052
+ }
16053
+ /**
16054
+ * Put npm's retired `.openclaw-*` tree for `version` back in place, with its
16055
+ * bin link. npm's reify renames the live package to `.<name>-<rand>` before
16056
+ * extracting and only deletes it on success, so a killed reify leaves the old
16057
+ * tree intact next to the partial one. Returns true when `openclaw --version`
16058
+ * then reports `version`.
16059
+ */
16060
+ async function restoreRetiredOpenClawTree(run, env, version) {
16061
+ const root = (await run("npm", ["root", "-g"], {
16062
+ timeout: 3e4,
16063
+ env
16064
+ })).stdout.trim();
16065
+ const prefix = (await run("npm", ["prefix", "-g"], {
16066
+ timeout: 3e4,
16067
+ env
16068
+ })).stdout.trim();
16069
+ if (!root.startsWith("/") || !prefix.startsWith("/")) return false;
16070
+ let candidate;
16071
+ for (const entry of await readdir(root).catch(() => [])) {
16072
+ if (!entry.startsWith(".openclaw-")) continue;
16073
+ const dir = join(root, entry);
16074
+ try {
16075
+ const pkg = JSON.parse(await readFile(join(dir, "package.json"), "utf8"));
16076
+ if (pkg.name !== "openclaw" || pkg.version !== version) continue;
16077
+ const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.openclaw;
16078
+ if (typeof bin !== "string" || !existsSync(join(dir, bin))) continue;
16079
+ candidate = {
16080
+ dir,
16081
+ bin
16082
+ };
16083
+ break;
16084
+ } catch {
16085
+ continue;
16086
+ }
16087
+ }
16088
+ if (!candidate) return false;
16089
+ const live = join(root, "openclaw");
16090
+ if (existsSync(live)) {
16091
+ const aside = join(root, `.openclaw-alfe-partial-${String(Date.now())}`);
16092
+ await rename(live, aside);
16093
+ await rm(aside, {
16094
+ recursive: true,
16095
+ force: true
16096
+ });
16097
+ }
16098
+ await rename(candidate.dir, live);
16099
+ const binLink = join(prefix, "bin", "openclaw");
16100
+ await lstat(binLink).then(() => unlink(binLink), () => void 0);
16101
+ await symlink(relative(dirname(binLink), join(live, candidate.bin)), binLink);
16102
+ logger$1.info({
16103
+ restored: candidate.dir,
16104
+ version
16105
+ }, "Restored the retired OpenClaw package tree");
16106
+ return await installedOpenClawVersion(run, env) === version;
16107
+ }
16108
+ /**
16109
+ * Finish or undo an OpenClaw upgrade the daemon did not live to complete.
16110
+ * Never throws; the caller holds the exclusive RuntimeGate and the CLI lock.
16111
+ */
16112
+ async function recoverInterruptedRuntimeUpgrade(options = {}) {
16113
+ const markerPath = options.markerPath ?? defaultRuntimeUpgradeMarkerPath();
16114
+ const run = options.runCommand ?? (async (command, args, commandOptions) => {
16115
+ const result = await execFileAsync(command, args, commandOptions);
16116
+ return {
16117
+ stdout: result.stdout,
16118
+ stderr: result.stderr
16119
+ };
16120
+ });
16121
+ const env = {
16122
+ ...process.env,
16123
+ ...OPENCLAW_EXTERNAL_SUPERVISOR_ENV
16124
+ };
16125
+ const sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
16126
+ const now = options.now ?? Date.now;
16127
+ let marker;
16128
+ try {
16129
+ marker = await readRuntimeUpgradeMarker(markerPath);
16130
+ } catch (err) {
16131
+ captureCliFailure("openclaw-upgrade-recovery", err);
16132
+ logger$1.error({ err: err instanceof Error ? err.message : String(err) }, "Discarding unreadable runtime upgrade marker");
16133
+ await clearRuntimeUpgradeMarker(markerPath).catch(() => void 0);
16134
+ return { status: "none" };
16135
+ }
16136
+ if (!marker) return { status: "none" };
16137
+ const attempts = (marker.recoveryAttempts ?? 0) + 1;
16138
+ const phase = marker.phase;
16139
+ if (attempts > 3) {
16140
+ const error = `Gave up recovering the interrupted OpenClaw upgrade to ${marker.targetVersion} after ${String(3)} boots`;
16141
+ captureCliFailure("openclaw-upgrade-recovery", new Error(error));
16142
+ logger$1.error({ marker }, error);
16143
+ await clearRuntimeUpgradeMarker(markerPath).catch(() => void 0);
16144
+ return {
16145
+ status: "failed",
16146
+ phase,
16147
+ error,
16148
+ gaveUp: true
16149
+ };
16150
+ }
16151
+ try {
16152
+ await writeRuntimeUpgradeMarker(markerPath, {
16153
+ ...marker,
16154
+ recoveryAttempts: attempts
16155
+ });
16156
+ } catch (err) {
16157
+ const error = `Could not record the recovery attempt in ${markerPath}: ${err instanceof Error ? err.message : String(err)}`;
16158
+ captureCliFailure("openclaw-upgrade-recovery", new Error(error, { cause: err }));
16159
+ logger$1.error({ err: error }, "Not recovering the interrupted OpenClaw upgrade — marker is not writable");
16160
+ return {
16161
+ status: "failed",
16162
+ phase,
16163
+ error,
16164
+ gaveUp: false
16165
+ };
16166
+ }
16167
+ logger$1.warn({
16168
+ marker,
16169
+ attempt: attempts
16170
+ }, "Found an interrupted OpenClaw runtime upgrade — recovering before the runtime starts");
16171
+ let scopeSequence = 0;
16172
+ const scoped = (command, args) => {
16173
+ scopeSequence += 1;
16174
+ return wrapInUpgradeScope(options.isolation, `${upgradeScopeUnitName(now())}-recovery-${String(scopeSequence)}`, command, args);
16175
+ };
16176
+ try {
16177
+ await waitForScopes(run, options.isolation, marker.scopeUnits ?? [], sleep, now);
16178
+ const installed = await installedOpenClawVersion(run, env);
16179
+ const target = marker.previousVersion;
16180
+ if (phase === "installing" && target !== void 0 && installed !== marker.targetVersion) {
16181
+ let method = "already-in-place";
16182
+ if (installed !== target) {
16183
+ if (await restoreRetiredOpenClawTree(run, env, target).catch((err) => {
16184
+ logger$1.warn({ err: err instanceof Error ? err.message : String(err) }, "Restoring the retired OpenClaw tree failed — reinstalling");
16185
+ return false;
16186
+ })) method = "restored-retired-tree";
16187
+ else {
16188
+ const install = scoped("npm", [
16189
+ "install",
16190
+ "-g",
16191
+ `openclaw@${target}`
16192
+ ]);
16193
+ await run(install.command, install.args, {
16194
+ timeout: NPM_INSTALL_TIMEOUT_MS,
16195
+ env
16196
+ });
16197
+ method = "reinstalled";
16198
+ }
16199
+ const verified = await installedOpenClawVersion(run, env);
16200
+ if (verified !== target) throw new Error(`OpenClaw did not verify after recovery: expected ${target}, resolved ${verified ?? "nothing"}`);
16201
+ }
16202
+ await clearRuntimeUpgradeMarker(markerPath);
16203
+ logger$1.info({
16204
+ version: target,
16205
+ method
16206
+ }, "Recovered interrupted OpenClaw upgrade (pre-migration): previous runtime restored");
16207
+ return {
16208
+ status: "recovered",
16209
+ phase,
16210
+ version: target,
16211
+ method
16212
+ };
16213
+ }
16214
+ let method = phase === "migrating" ? "doctor-rerun" : "rolled-forward";
16215
+ if (installed !== marker.targetVersion) {
16216
+ const install = scoped("npm", [
16217
+ "install",
16218
+ "-g",
16219
+ `openclaw@${marker.targetVersion}`
16220
+ ]);
16221
+ await run(install.command, install.args, {
16222
+ timeout: NPM_INSTALL_TIMEOUT_MS,
16223
+ env
16224
+ });
16225
+ const verified = await installedOpenClawVersion(run, env);
16226
+ if (verified !== marker.targetVersion) throw new Error(`OpenClaw did not verify after recovery: expected ${marker.targetVersion}, resolved ${verified ?? "nothing"}`);
16227
+ method = `${method}-after-reinstall`;
16228
+ }
16229
+ const doctor = scoped("openclaw", [...OPENCLAW_DOCTOR_FIX_ARGS]);
16230
+ await run(doctor.command, doctor.args, {
16231
+ timeout: DOCTOR_TIMEOUT_MS,
16232
+ env,
16233
+ maxBuffer: DOCTOR_MAX_BUFFER_BYTES
16234
+ });
16235
+ await clearRuntimeUpgradeMarker(markerPath);
16236
+ logger$1.info({
16237
+ version: marker.targetVersion,
16238
+ method
16239
+ }, "Recovered interrupted OpenClaw upgrade (migration completed)");
16240
+ return {
16241
+ status: "recovered",
16242
+ phase,
16243
+ version: marker.targetVersion,
16244
+ method
16245
+ };
16246
+ } catch (err) {
16247
+ const error = err instanceof Error ? err.message : String(err);
16248
+ captureCliFailure("openclaw-upgrade-recovery", err instanceof Error ? err : new Error(error));
16249
+ logger$1.error({
16250
+ err: error,
16251
+ phase,
16252
+ attempt: attempts
16253
+ }, "Recovering the interrupted OpenClaw upgrade failed — will retry on the next daemon start");
16254
+ return {
16255
+ status: "failed",
16256
+ phase,
16257
+ error,
16258
+ gaveUp: false
16259
+ };
16260
+ }
16261
+ }
16262
+ /**
16263
+ * Start boot-time recovery when a marker exists. Returns `undefined` (and does
16264
+ * nothing) without one. Otherwise the exclusive RuntimeGate hold is RESERVED
16265
+ * SYNCHRONOUSLY before this returns: `RuntimeGate.suspendExclusive` sets its
16266
+ * exclusive flag and depth before its first await, so a `requestStart()` issued
16267
+ * right after this call folds into our `resume()` instead of spawning a runtime
16268
+ * whose binary may be missing. Order: gate, then CLI lock (the documented
16269
+ * lock-ordering invariant). The returned promise never rejects.
16270
+ */
16271
+ function startBootUpgradeRecovery(options) {
16272
+ if (!existsSync(options.markerPath)) return void 0;
16273
+ const reservation = options.gate.suspendExclusive();
16274
+ return (async () => {
16275
+ try {
16276
+ await reservation;
16277
+ const outcome = await options.cliLock.run(() => options.recover(options.markerPath));
16278
+ logger$1.info({ outcome }, "Interrupted runtime upgrade recovery finished");
16279
+ if (outcome.status === "recovered") options.onRecovered?.(outcome.version);
16280
+ return outcome;
16281
+ } catch (err) {
16282
+ const error = err instanceof Error ? err.message : String(err);
16283
+ logger$1.error({ err: error }, "Interrupted runtime upgrade recovery threw");
16284
+ return {
16285
+ status: "failed",
16286
+ phase: "installing",
16287
+ error,
16288
+ gaveUp: false
16289
+ };
16290
+ } finally {
16291
+ await options.gate.resume();
16292
+ }
16293
+ })();
16294
+ }
16295
+ //#endregion
16296
+ export { resolveOpenClawUpgradeTarget as C, __require as E, parseOpenClawVersion as S, __commonJSMin as T, createLogger as _, recoverInterruptedRuntimeUpgrade as a, PINNED_OPENCLAW_VERSION as b, wrapInUpgradeScope as c, OPENCLAW_EXTERNAL_SUPERVISOR_ENV as d, RuntimeProcess as f, toFingerprintKey as g, STRICT_ERROR_CAPTURE_PROFILE as h, detectUpgradeIsolation as i, writeRuntimeUpgradeMarker as l, ErrorLineDetector as m, clearRuntimeUpgradeMarker as n, startBootUpgradeRecovery as o, CaptureThrottle as p, defaultRuntimeUpgradeMarkerPath as r, upgradeScopeUnitName as s, OPENCLAW_DOCTOR_FIX_ARGS as t, DRAIN_GRACE_MS as u, OPENCLAW_MANAGED_NODE_MAJOR as v, supportsOpenClawNode as w, openClawRequiresRegisterExport as x, OPENCLAW_NODE_REQUIREMENT as y };