@alfe.ai/gateway 0.10.6 → 0.10.7
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.
- package/dist/health.js +417 -241
- package/dist/node-remediation.js +1 -1
- package/dist/{runtime-process.js → runtime-upgrade-recovery.js} +464 -3
- package/dist/runtime-upgrade.js +327 -18
- package/dist/sentry.js +16 -9
- package/dist/src/index.d.ts +10 -3
- package/dist/src/index.js +1 -1
- package/package.json +4 -4
package/dist/runtime-upgrade.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { S as parseOpenClawVersion, a as recoverInterruptedRuntimeUpgrade, c as wrapInUpgradeScope, d as OPENCLAW_EXTERNAL_SUPERVISOR_ENV, l as writeRuntimeUpgradeMarker, n as clearRuntimeUpgradeMarker, s as upgradeScopeUnitName, t as OPENCLAW_DOCTOR_FIX_ARGS, w as supportsOpenClawNode, x as openClawRequiresRegisterExport, y as OPENCLAW_NODE_REQUIREMENT } from "./runtime-upgrade-recovery.js";
|
|
2
2
|
import { c as captureNodeRemediation, h as logger, i as captureCliFailure } from "./sentry.js";
|
|
3
3
|
import { n as remediateManagedNode, t as NodeRemediationError } from "./node-remediation.js";
|
|
4
4
|
import { mkdir } from "node:fs/promises";
|
|
@@ -6,6 +6,236 @@ import { join } from "node:path";
|
|
|
6
6
|
import { homedir } from "node:os";
|
|
7
7
|
import { execFile } from "node:child_process";
|
|
8
8
|
import { promisify } from "node:util";
|
|
9
|
+
//#region src/openclaw-plugin-compat.ts
|
|
10
|
+
/**
|
|
11
|
+
* Pre-upgrade plugin-export compatibility check for OpenClaw ≥ 2026.8.
|
|
12
|
+
*
|
|
13
|
+
* OpenClaw 2026.8.1 dropped the 2026.6.x `register ?? activate` export alias.
|
|
14
|
+
* An `@alfe.ai/*` plugin installed from an alfe manifest ≤ 1.1.49 only exports
|
|
15
|
+
* `activate`, so after a runtime upgrade it fails to load and `doctor --fix`
|
|
16
|
+
* exits non-zero AFTER migrating state — past the point where `upgradeRuntime`
|
|
17
|
+
* can roll the package back (Sentry AGENT-DAEMON-P/Q/R; Designer Tester went
|
|
18
|
+
* dark with its core plugins unloaded). This check runs BEFORE the gate
|
|
19
|
+
* suspends and before the install, while refusing costs nothing.
|
|
20
|
+
*
|
|
21
|
+
* Mechanism:
|
|
22
|
+
* 1. `openclaw plugins inspect --all --json` (the same structured read the
|
|
23
|
+
* applier batches; 2026.6.11/8.1/9.5 all print `plugin.{id, packageName,
|
|
24
|
+
* enabled, origin, source}`), filtered to enabled, non-bundled
|
|
25
|
+
* `@alfe.ai/*` plugins;
|
|
26
|
+
* 2. ONE child `node` process imports each plugin's entry (`plugin.source`)
|
|
27
|
+
* and resolves its export the way OpenClaw 2026.8+'s
|
|
28
|
+
* `resolvePluginModuleExport` does (unwrap `default`, accept a function or
|
|
29
|
+
* an object with a `register` function, follow `default`/`module`).
|
|
30
|
+
*
|
|
31
|
+
* The child process isolates the daemon from whatever a plugin does at import
|
|
32
|
+
* time (a throw, an open handle); it prints one sentinel-prefixed JSON line and
|
|
33
|
+
* exits. The check never throws: an unreadable inventory or a failed probe is
|
|
34
|
+
* `unknown` and the caller proceeds exactly as before (fail open — the guard
|
|
35
|
+
* targets one proven failure mode and must not become a new way to wedge
|
|
36
|
+
* upgrades). Only a DEFINITE "imported fine, no register" verdict blocks.
|
|
37
|
+
*/
|
|
38
|
+
const INSPECT_TIMEOUT_MS = 12e4;
|
|
39
|
+
const INSPECT_MAX_BUFFER_BYTES = 32 * 1024 * 1024;
|
|
40
|
+
const PROBE_TIMEOUT_MS = 6e4;
|
|
41
|
+
const PROBE_MAX_BUFFER_BYTES = 4 * 1024 * 1024;
|
|
42
|
+
const PLUGIN_EXPORT_PROBE_SENTINEL = "__ALFE_PLUGIN_EXPORT_PROBE__";
|
|
43
|
+
/**
|
|
44
|
+
* Runs as `node --input-type=module -e <script> <json-array-of-paths>`.
|
|
45
|
+
* Mirrors OpenClaw 2026.8.1/9.5 `unwrapDefaultModuleExport` +
|
|
46
|
+
* `resolvePluginModuleExport` (dist `loader-*.mjs`). Verdicts: `register`,
|
|
47
|
+
* `missing`, or `error:<message>`.
|
|
48
|
+
*/
|
|
49
|
+
const PLUGIN_EXPORT_PROBE_SCRIPT = `
|
|
50
|
+
import { pathToFileURL } from 'node:url';
|
|
51
|
+
const unwrap = (value) => {
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
while (value && typeof value === 'object' && 'default' in value && !seen.has(value)) {
|
|
54
|
+
seen.add(value);
|
|
55
|
+
value = value.default;
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
58
|
+
};
|
|
59
|
+
const hasRegister = (moduleExport) => {
|
|
60
|
+
const seen = new Set();
|
|
61
|
+
const candidates = [unwrap(moduleExport), moduleExport];
|
|
62
|
+
for (let i = 0; i < candidates.length && i < 12; i += 1) {
|
|
63
|
+
const resolved = candidates[i];
|
|
64
|
+
if (seen.has(resolved)) continue;
|
|
65
|
+
seen.add(resolved);
|
|
66
|
+
if (typeof resolved === 'function') return true;
|
|
67
|
+
if (resolved && typeof resolved === 'object') {
|
|
68
|
+
if (typeof resolved.register === 'function') return true;
|
|
69
|
+
for (const key of ['default', 'module']) if (key in resolved) candidates.push(resolved[key]);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
};
|
|
74
|
+
const verdicts = {};
|
|
75
|
+
for (const entry of JSON.parse(process.argv[1] ?? '[]')) {
|
|
76
|
+
try {
|
|
77
|
+
const mod = await import(pathToFileURL(entry).href);
|
|
78
|
+
verdicts[entry] = hasRegister(mod) ? 'register' : 'missing';
|
|
79
|
+
} catch (err) {
|
|
80
|
+
verdicts[entry] = 'error:' + String((err && err.message) || err).slice(0, 200);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
process.stdout.write('\\n${PLUGIN_EXPORT_PROBE_SENTINEL}' + JSON.stringify(verdicts) + '\\n');
|
|
84
|
+
process.exit(0);
|
|
85
|
+
`;
|
|
86
|
+
/** Enabled, non-bundled `@alfe.ai/*` plugins with a loadable entry path, or `undefined` for an unexpected payload. */
|
|
87
|
+
function selectAlfePluginRecords(stdout) {
|
|
88
|
+
let parsed;
|
|
89
|
+
try {
|
|
90
|
+
parsed = JSON.parse(stdout.trim());
|
|
91
|
+
} catch {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (!Array.isArray(parsed)) return void 0;
|
|
95
|
+
const records = [];
|
|
96
|
+
for (const entry of parsed) {
|
|
97
|
+
const plugin = entry?.plugin;
|
|
98
|
+
if (!plugin || typeof plugin !== "object") return void 0;
|
|
99
|
+
const id = typeof plugin.id === "string" ? plugin.id : void 0;
|
|
100
|
+
const name = [typeof plugin.packageName === "string" ? plugin.packageName : void 0, id].find((value) => value?.startsWith("@alfe.ai/"));
|
|
101
|
+
if (!id || !name) continue;
|
|
102
|
+
if (plugin.enabled === false || plugin.origin === "bundled") continue;
|
|
103
|
+
if (typeof plugin.source !== "string" || !plugin.source.startsWith("/")) continue;
|
|
104
|
+
const install = entry.install;
|
|
105
|
+
const version = [
|
|
106
|
+
install?.resolvedVersion,
|
|
107
|
+
install?.version,
|
|
108
|
+
plugin.packageVersion,
|
|
109
|
+
plugin.version
|
|
110
|
+
].find((value) => typeof value === "string");
|
|
111
|
+
records.push({
|
|
112
|
+
id: name,
|
|
113
|
+
source: plugin.source,
|
|
114
|
+
...version ? { version } : {}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return records;
|
|
118
|
+
}
|
|
119
|
+
function parseProbeOutput(stdout) {
|
|
120
|
+
const line = stdout.split("\n").reverse().find((candidate) => candidate.startsWith(PLUGIN_EXPORT_PROBE_SENTINEL));
|
|
121
|
+
if (!line) return void 0;
|
|
122
|
+
try {
|
|
123
|
+
const parsed = JSON.parse(line.slice(28));
|
|
124
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
|
|
125
|
+
return parsed;
|
|
126
|
+
} catch {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Find enabled `@alfe.ai/*` plugins that OpenClaw ≥ 2026.8 cannot load because
|
|
132
|
+
* they export `activate` without `register`. Never throws.
|
|
133
|
+
*/
|
|
134
|
+
async function findActivateOnlyAlfePlugins(runCommand, openclawEnv) {
|
|
135
|
+
let records;
|
|
136
|
+
try {
|
|
137
|
+
const { stdout } = await runCommand("openclaw", [
|
|
138
|
+
"plugins",
|
|
139
|
+
"inspect",
|
|
140
|
+
"--all",
|
|
141
|
+
"--json"
|
|
142
|
+
], {
|
|
143
|
+
timeout: INSPECT_TIMEOUT_MS,
|
|
144
|
+
env: openclawEnv,
|
|
145
|
+
maxBuffer: INSPECT_MAX_BUFFER_BYTES
|
|
146
|
+
});
|
|
147
|
+
records = selectAlfePluginRecords(stdout);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
return {
|
|
150
|
+
status: "unknown",
|
|
151
|
+
reason: `plugin inventory unavailable: ${err instanceof Error ? err.message : String(err)}`
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
if (!records) return {
|
|
155
|
+
status: "unknown",
|
|
156
|
+
reason: "plugin inventory had an unexpected shape"
|
|
157
|
+
};
|
|
158
|
+
if (records.length === 0) return {
|
|
159
|
+
status: "checked",
|
|
160
|
+
activateOnly: []
|
|
161
|
+
};
|
|
162
|
+
let verdicts;
|
|
163
|
+
try {
|
|
164
|
+
const { stdout } = await runCommand("node", [
|
|
165
|
+
"--input-type=module",
|
|
166
|
+
"-e",
|
|
167
|
+
PLUGIN_EXPORT_PROBE_SCRIPT,
|
|
168
|
+
JSON.stringify(records.map((r) => r.source))
|
|
169
|
+
], {
|
|
170
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
171
|
+
maxBuffer: PROBE_MAX_BUFFER_BYTES
|
|
172
|
+
});
|
|
173
|
+
verdicts = parseProbeOutput(stdout);
|
|
174
|
+
} catch (err) {
|
|
175
|
+
return {
|
|
176
|
+
status: "unknown",
|
|
177
|
+
reason: `plugin export probe failed: ${err instanceof Error ? err.message : String(err)}`
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
if (!verdicts) return {
|
|
181
|
+
status: "unknown",
|
|
182
|
+
reason: "plugin export probe printed no verdicts"
|
|
183
|
+
};
|
|
184
|
+
const activateOnly = [];
|
|
185
|
+
for (const record of records) {
|
|
186
|
+
const verdict = verdicts[record.source];
|
|
187
|
+
if (verdict === "missing") activateOnly.push({
|
|
188
|
+
id: record.id,
|
|
189
|
+
...record.version ? { version: record.version } : {}
|
|
190
|
+
});
|
|
191
|
+
else if (verdict !== "register") logger.warn({
|
|
192
|
+
plugin: record.id,
|
|
193
|
+
verdict
|
|
194
|
+
}, "Plugin export probe could not classify plugin — not blocking the upgrade");
|
|
195
|
+
}
|
|
196
|
+
return {
|
|
197
|
+
status: "checked",
|
|
198
|
+
activateOnly
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function classifyActivateOnlyPlugins(activateOnly, ownership) {
|
|
202
|
+
if (activateOnly.length === 0) return {
|
|
203
|
+
blocked: [],
|
|
204
|
+
orphans: []
|
|
205
|
+
};
|
|
206
|
+
if (!ownership) return {
|
|
207
|
+
blocked: activateOnly,
|
|
208
|
+
orphans: []
|
|
209
|
+
};
|
|
210
|
+
let owned;
|
|
211
|
+
try {
|
|
212
|
+
owned = ownership.ownedPlugins();
|
|
213
|
+
} catch (err) {
|
|
214
|
+
return {
|
|
215
|
+
blocked: activateOnly,
|
|
216
|
+
orphans: [],
|
|
217
|
+
ownershipError: err instanceof Error ? err.message : String(err)
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const blocked = [];
|
|
221
|
+
const orphans = [];
|
|
222
|
+
for (const plugin of activateOnly) {
|
|
223
|
+
const owner = owned.get(plugin.id);
|
|
224
|
+
if (owner) blocked.push({
|
|
225
|
+
...plugin,
|
|
226
|
+
owner
|
|
227
|
+
});
|
|
228
|
+
else orphans.push(plugin);
|
|
229
|
+
}
|
|
230
|
+
return {
|
|
231
|
+
blocked,
|
|
232
|
+
orphans
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function activateOnlyPluginsMessage(targetVersion, decision) {
|
|
236
|
+
return `Refusing to upgrade OpenClaw to ${targetVersion}: OpenClaw 2026.8+ only loads plugins that export register(), and these enabled Alfe plugins are activate-only builds: ${decision.blocked.map((plugin) => `${plugin.id}${plugin.version ? `@${plugin.version}` : ""}${plugin.owner ? ` (integration "${plugin.owner}")` : ""}`).join(", ")}. Upgrade or reinstall the integration(s) that own them (usually "alfe") so they install current plugin builds, then retry the runtime upgrade. The runtime was not changed.` + (decision.ownershipError ? ` (Plugin ownership could not be read: ${decision.ownershipError}.)` : "");
|
|
237
|
+
}
|
|
238
|
+
//#endregion
|
|
9
239
|
//#region src/runtime-upgrade.ts
|
|
10
240
|
/**
|
|
11
241
|
* Runtime upgrade — safely replace a runtime binary and cycle its child.
|
|
@@ -42,19 +272,6 @@ const OPENCLAW_DOCTOR_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
|
|
|
42
272
|
* runtime regardless of config contents.
|
|
43
273
|
*/
|
|
44
274
|
const OPENCLAW_ADMISSION_PROBE_ARGS = ["config", "file"];
|
|
45
|
-
/**
|
|
46
|
-
* `doctor --fix` runs the state/config migration. On 2026.9.x `--json` puts
|
|
47
|
-
* doctor into READ-ONLY lint mode and it refuses the combination outright
|
|
48
|
-
* (exit 2: "doctor --json runs read-only lint checks and cannot be combined
|
|
49
|
-
* with --repair, --fix, or --force"), so the migration must run without it.
|
|
50
|
-
* The post-upgrade audit is read-only and keeps `--json`.
|
|
51
|
-
*/
|
|
52
|
-
const OPENCLAW_DOCTOR_FIX_ARGS = [
|
|
53
|
-
"doctor",
|
|
54
|
-
"--fix",
|
|
55
|
-
"--non-interactive",
|
|
56
|
-
"--yes"
|
|
57
|
-
];
|
|
58
275
|
const OPENCLAW_DOCTOR_AUDIT_ARGS = [
|
|
59
276
|
"doctor",
|
|
60
277
|
"--post-upgrade",
|
|
@@ -126,6 +343,36 @@ async function upgradeRuntime(runtime, version, runtimeProcess, options = {}) {
|
|
|
126
343
|
error: message
|
|
127
344
|
};
|
|
128
345
|
}
|
|
346
|
+
let orphanPlugins = [];
|
|
347
|
+
if (runtime === "openclaw" && version && openClawRequiresRegisterExport(version)) {
|
|
348
|
+
const found = await findActivateOnlyAlfePlugins(runCommand, openclawEnv);
|
|
349
|
+
if (found.status === "unknown") logger.warn({
|
|
350
|
+
runtime,
|
|
351
|
+
version,
|
|
352
|
+
reason: found.reason
|
|
353
|
+
}, "Plugin export preflight inconclusive — proceeding with the upgrade");
|
|
354
|
+
else if (found.activateOnly.length > 0) {
|
|
355
|
+
const decision = classifyActivateOnlyPlugins(found.activateOnly, options.pluginOwnership);
|
|
356
|
+
if (decision.blocked.length > 0) {
|
|
357
|
+
const message = activateOnlyPluginsMessage(version, decision);
|
|
358
|
+
logger.error({
|
|
359
|
+
runtime,
|
|
360
|
+
version,
|
|
361
|
+
blocked: decision.blocked,
|
|
362
|
+
orphans: decision.orphans
|
|
363
|
+
}, message);
|
|
364
|
+
captureCliFailure("openclaw-upgrade-preflight", new Error(message));
|
|
365
|
+
return {
|
|
366
|
+
success: false,
|
|
367
|
+
runtime,
|
|
368
|
+
version,
|
|
369
|
+
nodeUpgraded: false,
|
|
370
|
+
error: message
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
orphanPlugins = decision.orphans.map((plugin) => plugin.id);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
129
376
|
let previousOpenClawVersion;
|
|
130
377
|
let nodeUpgraded = false;
|
|
131
378
|
if (runtime === "openclaw") try {
|
|
@@ -195,6 +442,32 @@ async function upgradeRuntime(runtime, version, runtimeProcess, options = {}) {
|
|
|
195
442
|
error: message
|
|
196
443
|
};
|
|
197
444
|
}
|
|
445
|
+
const markerPath = options.markerPath;
|
|
446
|
+
const scopeUnits = [];
|
|
447
|
+
/** Wrap a long, state-mutating command in its own scope (records the unit for boot recovery). */
|
|
448
|
+
const isolated = (command, args) => {
|
|
449
|
+
if (!options.isolation) return {
|
|
450
|
+
command,
|
|
451
|
+
args
|
|
452
|
+
};
|
|
453
|
+
const unit = upgradeScopeUnitName(Date.now() + scopeUnits.length);
|
|
454
|
+
scopeUnits.push(unit);
|
|
455
|
+
return wrapInUpgradeScope(options.isolation, unit, command, args);
|
|
456
|
+
};
|
|
457
|
+
const markerState = { keep: false };
|
|
458
|
+
const writeMarker = async (phase) => {
|
|
459
|
+
if (!markerPath || runtime !== "openclaw" || !version) return;
|
|
460
|
+
const marker = {
|
|
461
|
+
runtime: "openclaw",
|
|
462
|
+
phase,
|
|
463
|
+
targetVersion: version,
|
|
464
|
+
...previousOpenClawVersion ? { previousVersion: previousOpenClawVersion } : {},
|
|
465
|
+
startedAt: markerState.current?.startedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
466
|
+
scopeUnits: [...scopeUnits]
|
|
467
|
+
};
|
|
468
|
+
await writeRuntimeUpgradeMarker(markerPath, marker);
|
|
469
|
+
markerState.current = marker;
|
|
470
|
+
};
|
|
198
471
|
logger.info({
|
|
199
472
|
runtime,
|
|
200
473
|
version
|
|
@@ -222,8 +495,10 @@ async function upgradeRuntime(runtime, version, runtimeProcess, options = {}) {
|
|
|
222
495
|
], commandOptions(OPENCLAW_UPGRADE_TIMEOUT_MS));
|
|
223
496
|
if (backup.stdout) logger.info({ backup: backup.stdout.trim() }, "Verified pre-upgrade OpenClaw backup created");
|
|
224
497
|
}
|
|
498
|
+
const install = runtime === "openclaw" ? isolated(cmd.command, cmd.args) : cmd;
|
|
499
|
+
await writeMarker("installing");
|
|
225
500
|
packageInstallAttempted = true;
|
|
226
|
-
const { stdout, stderr } = await runCommand(
|
|
501
|
+
const { stdout, stderr } = await runCommand(install.command, install.args, commandOptions(OPENCLAW_UPGRADE_TIMEOUT_MS));
|
|
227
502
|
if (stdout) logger.debug({ stdout: stdout.trim() }, "runtime upgrade stdout");
|
|
228
503
|
if (stderr) logger.debug({ stderr: stderr.trim() }, "runtime upgrade stderr");
|
|
229
504
|
if (runtime === "openclaw" && version) {
|
|
@@ -231,9 +506,11 @@ async function upgradeRuntime(runtime, version, runtimeProcess, options = {}) {
|
|
|
231
506
|
if (installedVersion !== version) throw new Error(`OpenClaw package verification failed: requested ${version}, resolved ${installedVersion ?? "unknown"}`);
|
|
232
507
|
runtimeChanged = installedVersion !== previousOpenClawVersion;
|
|
233
508
|
await runCommand("openclaw", [...OPENCLAW_ADMISSION_PROBE_ARGS], commandOptions(6e4));
|
|
509
|
+
const doctorCommand = isolated("openclaw", [...OPENCLAW_DOCTOR_FIX_ARGS]);
|
|
510
|
+
await writeMarker("migrating");
|
|
234
511
|
doctorStarted = true;
|
|
235
512
|
try {
|
|
236
|
-
const doctor = await runCommand(
|
|
513
|
+
const doctor = await runCommand(doctorCommand.command, doctorCommand.args, {
|
|
237
514
|
...commandOptions(OPENCLAW_DOCTOR_TIMEOUT_MS),
|
|
238
515
|
maxBuffer: OPENCLAW_DOCTOR_MAX_BUFFER_BYTES
|
|
239
516
|
});
|
|
@@ -256,14 +533,19 @@ async function upgradeRuntime(runtime, version, runtimeProcess, options = {}) {
|
|
|
256
533
|
} catch (err) {
|
|
257
534
|
if (runtime === "openclaw" && packageInstallAttempted && !doctorStarted && previousOpenClawVersion) try {
|
|
258
535
|
logger.warn({ previousOpenClawVersion }, "OpenClaw package replacement failed before state migration — restoring previous package");
|
|
259
|
-
|
|
536
|
+
const restore = isolated("npm", [
|
|
260
537
|
"install",
|
|
261
538
|
"-g",
|
|
262
539
|
`openclaw@${previousOpenClawVersion}`
|
|
263
|
-
]
|
|
540
|
+
]);
|
|
541
|
+
await writeMarker("installing").catch((markerErr) => {
|
|
542
|
+
logger.warn({ err: markerErr instanceof Error ? markerErr.message : String(markerErr) }, "Could not update the runtime upgrade marker for the rollback");
|
|
543
|
+
});
|
|
544
|
+
await runCommand(restore.command, restore.args, commandOptions(OPENCLAW_UPGRADE_TIMEOUT_MS));
|
|
264
545
|
if (parseOpenClawVersion((await runCommand("openclaw", ["--version"], commandOptions(1e4))).stdout) !== previousOpenClawVersion) throw new Error("restored OpenClaw version did not verify");
|
|
265
546
|
runtimeChanged = false;
|
|
266
547
|
} catch (rollbackError) {
|
|
548
|
+
markerState.keep = true;
|
|
267
549
|
logger.error({ err: rollbackError instanceof Error ? rollbackError.message : String(rollbackError) }, "Failed to restore the previous OpenClaw package after upgrade failure");
|
|
268
550
|
}
|
|
269
551
|
throw err;
|
|
@@ -274,6 +556,17 @@ async function upgradeRuntime(runtime, version, runtimeProcess, options = {}) {
|
|
|
274
556
|
gateSuspended = true;
|
|
275
557
|
await options.gate.suspendExclusive();
|
|
276
558
|
} else if (runtimeProcess) await runtimeProcess.stopWhenIdle();
|
|
559
|
+
for (const pkg of orphanPlugins) {
|
|
560
|
+
logger.warn({
|
|
561
|
+
pkg,
|
|
562
|
+
version
|
|
563
|
+
}, "Uninstalling orphaned activate-only Alfe plugin before the OpenClaw upgrade");
|
|
564
|
+
try {
|
|
565
|
+
await options.pluginOwnership?.removePlugin(pkg);
|
|
566
|
+
} catch (err) {
|
|
567
|
+
throw new Error(`Could not uninstall orphaned plugin ${pkg} (activate-only, owned by no integration) before upgrading OpenClaw to ${version ?? "the pin"}: ` + (err instanceof Error ? err.message : String(err)), { cause: err });
|
|
568
|
+
}
|
|
569
|
+
}
|
|
277
570
|
if (runtime === "openclaw" && options.cliLock) await options.cliLock.run(performUpgrade);
|
|
278
571
|
else await performUpgrade();
|
|
279
572
|
logger.info({
|
|
@@ -304,6 +597,22 @@ async function upgradeRuntime(runtime, version, runtimeProcess, options = {}) {
|
|
|
304
597
|
error: message
|
|
305
598
|
};
|
|
306
599
|
} finally {
|
|
600
|
+
if (markerPath && markerState.keep) {
|
|
601
|
+
const repair = () => recoverInterruptedRuntimeUpgrade({
|
|
602
|
+
markerPath,
|
|
603
|
+
...options.isolation ? { isolation: options.isolation } : {},
|
|
604
|
+
...options.runCommand ? { runCommand: options.runCommand } : {}
|
|
605
|
+
});
|
|
606
|
+
try {
|
|
607
|
+
const outcome = options.cliLock ? await options.cliLock.run(repair) : await repair();
|
|
608
|
+
logger.warn({ outcome }, "In-process repair after a failed pre-doctor rollback");
|
|
609
|
+
} catch (repairErr) {
|
|
610
|
+
logger.error({ err: repairErr instanceof Error ? repairErr.message : String(repairErr) }, "In-process repair after a failed pre-doctor rollback threw — marker kept for the next daemon start");
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
if (markerPath && markerState.current && !markerState.keep) await clearRuntimeUpgradeMarker(markerPath).catch((err) => {
|
|
614
|
+
logger.warn({ err: err instanceof Error ? err.message : String(err) }, "Failed to clear the runtime upgrade marker");
|
|
615
|
+
});
|
|
307
616
|
try {
|
|
308
617
|
if (gateSuspended && options.gate) await options.gate.resume();
|
|
309
618
|
else if (!options.gate && runtimeProcess) await runtimeProcess.restart();
|
package/dist/sentry.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import pino from "pino";
|
|
4
3
|
import { mkdirSync } from "node:fs";
|
|
4
|
+
import pino from "pino";
|
|
5
5
|
import { readConfig, resolveConfig } from "@alfe.ai/config";
|
|
6
6
|
//#region src/logger.ts
|
|
7
7
|
/**
|
|
@@ -273,22 +273,29 @@ function setAgentContext(context) {
|
|
|
273
273
|
/**
|
|
274
274
|
* Capture an integration lifecycle failure (install/activate/reinstall/remove)
|
|
275
275
|
* with `{ integration, phase }` tags. Accepts an `Error` (captured with stack)
|
|
276
|
-
* or a plain message string (captured as an error-level message).
|
|
277
|
-
*
|
|
276
|
+
* or a plain message string (captured as an error-level message). Pass
|
|
277
|
+
* `opts.fingerprint` when the message carries variable text that must not
|
|
278
|
+
* split one problem across many Sentry issues. No-op when Sentry is not
|
|
279
|
+
* initialised.
|
|
278
280
|
*/
|
|
279
|
-
function captureIntegrationFailure(integration, phase, cause) {
|
|
281
|
+
function captureIntegrationFailure(integration, phase, cause, opts) {
|
|
280
282
|
if (!sentry) return;
|
|
281
283
|
try {
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
284
|
+
const fingerprint = opts?.fingerprint ? { fingerprint: opts.fingerprint } : {};
|
|
285
|
+
if (cause instanceof Error) sentry.captureException(cause, {
|
|
286
|
+
tags: {
|
|
287
|
+
integration,
|
|
288
|
+
phase
|
|
289
|
+
},
|
|
290
|
+
...fingerprint
|
|
291
|
+
});
|
|
286
292
|
else sentry.captureMessage(String(cause), {
|
|
287
293
|
level: "error",
|
|
288
294
|
tags: {
|
|
289
295
|
integration,
|
|
290
296
|
phase
|
|
291
|
-
}
|
|
297
|
+
},
|
|
298
|
+
...fingerprint
|
|
292
299
|
});
|
|
293
300
|
} catch {}
|
|
294
301
|
}
|
package/dist/src/index.d.ts
CHANGED
|
@@ -46,6 +46,9 @@ declare function startDaemon(): Promise<void>;
|
|
|
46
46
|
* - the daemon `runtime.update` default (this package)
|
|
47
47
|
* - managed provisioning (`services/compute/src/lib/provisioner.ts` installs the
|
|
48
48
|
* Node.js 24 channel before the CLI consumes this pin)
|
|
49
|
+
* - the dashboard/fleet upgrade ceiling: `services/agents` mirrors this literal
|
|
50
|
+
* as `PINNED_OPENCLAW_VERSION` in `version-catalog.service.ts` (its test fails
|
|
51
|
+
* on drift) — bump both in the same change
|
|
49
52
|
*
|
|
50
53
|
* Pinning (rather than floating `npm install -g openclaw` → `latest`) is deliberate:
|
|
51
54
|
* OpenClaw runs on every managed agent, so a `latest` that changes runtime semantics —
|
|
@@ -216,10 +219,14 @@ declare function setAgentContext(context: {
|
|
|
216
219
|
/**
|
|
217
220
|
* Capture an integration lifecycle failure (install/activate/reinstall/remove)
|
|
218
221
|
* with `{ integration, phase }` tags. Accepts an `Error` (captured with stack)
|
|
219
|
-
* or a plain message string (captured as an error-level message).
|
|
220
|
-
*
|
|
222
|
+
* or a plain message string (captured as an error-level message). Pass
|
|
223
|
+
* `opts.fingerprint` when the message carries variable text that must not
|
|
224
|
+
* split one problem across many Sentry issues. No-op when Sentry is not
|
|
225
|
+
* initialised.
|
|
221
226
|
*/
|
|
222
|
-
declare function captureIntegrationFailure(integration: string, phase: string, cause: unknown
|
|
227
|
+
declare function captureIntegrationFailure(integration: string, phase: string, cause: unknown, opts?: {
|
|
228
|
+
fingerprint?: string[];
|
|
229
|
+
}): void;
|
|
223
230
|
/**
|
|
224
231
|
* Capture a generic CLI failure with a `{ context }` tag. The CLI counterpart
|
|
225
232
|
* to `captureIntegrationFailure` — used by `exitWithError` at the handled-error
|
package/dist/src/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { b as PINNED_OPENCLAW_VERSION, v as OPENCLAW_MANAGED_NODE_MAJOR, w as supportsOpenClawNode, y as OPENCLAW_NODE_REQUIREMENT } from "../runtime-upgrade-recovery.js";
|
|
2
2
|
import { _ as loadDaemonConfig, a as installService, c as startService, d as uninstallService, f as PROTOCOL_VERSION, g as fetchAgentConfig, h as SOCKET_PATH, i as checkExistingDaemon, l as stopExistingDaemon, m as PID_PATH, n as queryDaemonHealth, o as isServiceInstalled, p as ALFE_DIR, r as startDaemon, s as restartService, t as formatHealthReport, u as stopService, v as resolveAgentIdentity, y as PINNED_CLAUDE_CODE_HOST_VERSION } from "../health.js";
|
|
3
3
|
import { a as captureFatal, d as flushSentry, f as initAgentSentry, h as logger, i as captureCliFailure, l as captureRuntimeCrash, n as AGENT_MCP_SENTRY_DSN, o as captureIntegrationFailure, p as setAgentContext, r as AGENT_RUNTIME_SENTRY_DSN, s as captureMcpFailure, t as AGENT_DAEMON_SENTRY_DSN, u as captureRuntimeErrorOutput } from "../sentry.js";
|
|
4
4
|
import { n as remediateManagedNode } from "../node-remediation.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alfe.ai/gateway",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.7",
|
|
4
4
|
"description": "Alfe local gateway daemon — persistent control plane for agent integrations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
"smol-toml": ">=1.6.1",
|
|
25
25
|
"ws": "^8.18.0",
|
|
26
26
|
"@alfe.ai/agent-api-client": "^0.19.2",
|
|
27
|
-
"@alfe.ai/ai-proxy-local": "^0.0.17",
|
|
28
27
|
"@alfe.ai/config": "^0.4.1",
|
|
29
28
|
"@alfe.ai/integration-manifest": "^0.4.2",
|
|
30
|
-
"@alfe.ai/integrations": "^0.6.
|
|
31
|
-
"@alfe.ai/mcp-bundler": "^0.4.2"
|
|
29
|
+
"@alfe.ai/integrations": "^0.6.13",
|
|
30
|
+
"@alfe.ai/mcp-bundler": "^0.4.2",
|
|
31
|
+
"@alfe.ai/ai-proxy-local": "^0.0.17"
|
|
32
32
|
},
|
|
33
33
|
"license": "UNLICENSED",
|
|
34
34
|
"homepage": "https://alfe.ai",
|