@alfe.ai/gateway 0.10.1 → 0.10.3

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 CHANGED
@@ -1,4 +1,4 @@
1
- import { a as ErrorLineDetector, c as createLogger, i as CaptureThrottle, m as __require, o as STRICT_ERROR_CAPTURE_PROFILE, p as __commonJSMin, r as RuntimeProcess, s as toFingerprintKey, t as DRAIN_GRACE_MS } from "./runtime-process.js";
1
+ import { a as ErrorLineDetector, c as createLogger, f as parseOpenClawVersion, h as __require, i as CaptureThrottle, m as __commonJSMin, n as OPENCLAW_EXTERNAL_SUPERVISOR_ENV, o as STRICT_ERROR_CAPTURE_PROFILE, r as RuntimeProcess, s as toFingerprintKey, t as DRAIN_GRACE_MS } from "./runtime-process.js";
2
2
  import { a as captureFatal, d as flushSentry, f as initAgentSentry, h as logger, o as captureIntegrationFailure, p as setAgentContext, s as captureMcpFailure } from "./sentry.js";
3
3
  import { chmod, mkdir, readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
4
4
  import { dirname, join } from "node:path";
@@ -5858,16 +5858,57 @@ const log$5 = logger.child({ component: "RuntimeGate" });
5858
5858
  var RuntimeGate = class {
5859
5859
  depth = 0;
5860
5860
  wasRunning = false;
5861
+ exclusive = false;
5862
+ /** Brackets parked behind an exclusive hold (or an exclusive request parked behind open brackets). */
5863
+ waiters = [];
5861
5864
  constructor(getRuntime) {
5862
5865
  this.getRuntime = getRuntime;
5863
5866
  }
5867
+ /** True while any bracket (shared or exclusive) is open. */
5868
+ get isSuspended() {
5869
+ return this.depth > 0;
5870
+ }
5871
+ /** True while an exclusive bracket is open. */
5872
+ get isExclusive() {
5873
+ return this.exclusive;
5874
+ }
5875
+ waitFor(ready) {
5876
+ if (ready()) return Promise.resolve();
5877
+ return new Promise((resolve) => {
5878
+ const check = () => {
5879
+ if (ready()) resolve();
5880
+ else this.waiters.push(check);
5881
+ };
5882
+ this.waiters.push(check);
5883
+ });
5884
+ }
5885
+ wakeWaiters() {
5886
+ const pending = this.waiters;
5887
+ this.waiters = [];
5888
+ for (const wake of pending) wake();
5889
+ }
5864
5890
  /**
5865
5891
  * Suspend the runtime child. Only the outermost call (depth 0 → 1) actually
5866
5892
  * stops it; nested calls just bump the refcount. Remembers whether the child
5867
5893
  * was running so `resume()` doesn't spuriously start a runtime that was never
5868
- * up (e.g. autoStartRuntime=false).
5894
+ * up (e.g. autoStartRuntime=false). Waits while an exclusive bracket is open.
5869
5895
  */
5870
5896
  async suspend() {
5897
+ if (this.exclusive) log$5.info("Mutating reconcile parked behind an exclusive runtime hold (upgrade in flight)");
5898
+ while (this.exclusive) await this.waitFor(() => !this.exclusive);
5899
+ await this.enter();
5900
+ }
5901
+ async suspendExclusive() {
5902
+ if (this.exclusive || this.depth !== 0) log$5.info({
5903
+ depth: this.depth,
5904
+ exclusive: this.exclusive
5905
+ }, "Exclusive runtime hold parked behind open brackets (reconcile in flight)");
5906
+ while (this.exclusive || this.depth !== 0) await this.waitFor(() => !this.exclusive && this.depth === 0);
5907
+ log$5.info("Exclusive runtime hold acquired");
5908
+ this.exclusive = true;
5909
+ await this.enter();
5910
+ }
5911
+ async enter() {
5871
5912
  if (this.depth++ > 0) return;
5872
5913
  const rp = this.getRuntime();
5873
5914
  this.wasRunning = rp?.isRunning ?? false;
@@ -5891,6 +5932,8 @@ var RuntimeGate = class {
5891
5932
  rp.resume();
5892
5933
  }
5893
5934
  this.wasRunning = false;
5935
+ if (this.exclusive) this.exclusive = false;
5936
+ this.wakeWaiters();
5894
5937
  return Promise.resolve();
5895
5938
  }
5896
5939
  /**
@@ -5920,6 +5963,9 @@ var NoopRuntimeGate = class {
5920
5963
  suspend() {
5921
5964
  return Promise.resolve();
5922
5965
  }
5966
+ suspendExclusive() {
5967
+ return Promise.resolve();
5968
+ }
5923
5969
  resume() {
5924
5970
  return Promise.resolve();
5925
5971
  }
@@ -7982,6 +8028,7 @@ var IPCServer = class {
7982
8028
  * standalone pieces of `startDaemon()`: version detection, the one-shot
7983
8029
  * persona-file migration, the AI proxy listener, and the IPC server start.
7984
8030
  */
8031
+ const RUNTIME_VERSION_TIMEOUT_MS = 1e4;
7985
8032
  const execFileAsync$2 = promisify(execFile);
7986
8033
  /**
7987
8034
  * Resolve the installed @alfe.ai/cli version.
@@ -8040,9 +8087,24 @@ function resolveRuntimeVersionCommand(runtime) {
8040
8087
  return RUNTIME_VERSION_COMMANDS[runtime];
8041
8088
  }
8042
8089
  /**
8090
+ * Reduce a runtime's `--version` output to the bare version the control plane
8091
+ * compares against catalog versions. Every OpenClaw release prints a branded
8092
+ * line (`OpenClaw 2026.9.5 (ec9c1a1)`); reporting that verbatim hid every
8093
+ * dashboard upgrade affordance for the agents that most needed one. The agents
8094
+ * service also normalises at ingest (it reaches hosts on older daemons); this
8095
+ * is the producer-side half. Falls back to the trimmed output when no version
8096
+ * token is found so an unexpected format is still visible rather than blank.
8097
+ */
8098
+ function normalizeRuntimeVersion(runtime, stdout) {
8099
+ const trimmed = stdout.trim();
8100
+ if (!trimmed) return void 0;
8101
+ if (runtime === "openclaw") return parseOpenClawVersion(trimmed) ?? trimmed;
8102
+ return trimmed;
8103
+ }
8104
+ /**
8043
8105
  * Resolve the installed runtime version by running the per-runtime version
8044
- * command and returning the trimmed stdout. An unknown runtime (no command in
8045
- * the map) returns `undefined` without spawning — it must never throw.
8106
+ * command and normalising its stdout. An unknown runtime (no command in the
8107
+ * map) returns `undefined` without spawning — it must never throw.
8046
8108
  */
8047
8109
  async function getRuntimeVersion(runtime) {
8048
8110
  const cmd = resolveRuntimeVersionCommand(runtime);
@@ -8051,8 +8113,14 @@ async function getRuntimeVersion(runtime) {
8051
8113
  return;
8052
8114
  }
8053
8115
  try {
8054
- const { stdout } = await execFileAsync$2(cmd.command, cmd.args);
8055
- return stdout.trim() || void 0;
8116
+ const { stdout } = await execFileAsync$2(cmd.command, cmd.args, {
8117
+ timeout: RUNTIME_VERSION_TIMEOUT_MS,
8118
+ env: {
8119
+ ...process.env,
8120
+ ...runtime === "openclaw" ? OPENCLAW_EXTERNAL_SUPERVISOR_ENV : {}
8121
+ }
8122
+ });
8123
+ return normalizeRuntimeVersion(runtime, stdout);
8056
8124
  } catch {
8057
8125
  logger.debug({ runtime }, "Could not resolve runtime version");
8058
8126
  return;
@@ -9929,6 +9997,15 @@ async function drainRuntimeBeforeExit(reason) {
9929
9997
  }
9930
9998
  async function executeCloudCommand(command) {
9931
9999
  if (isWorkCommand(command.command)) return executeWorkCommand(command, config.agentId, config.runtime, ipcServer);
10000
+ if ((command.command === "daemon.update" || command.command === "daemon.restart") && upgradingRuntime) return {
10001
+ type: "COMMAND_ACK",
10002
+ commandId: command.commandId,
10003
+ status: "error",
10004
+ result: {
10005
+ code: "RUNTIME_UPGRADE_IN_PROGRESS",
10006
+ message: `${command.command} refused: a runtime upgrade is in progress; retry when it has completed`
10007
+ }
10008
+ };
9932
10009
  if (command.command === "daemon.update") {
9933
10010
  const version = command.payload?.version ?? "latest";
9934
10011
  setTimeout(() => {
@@ -1,4 +1,4 @@
1
- import { f as supportsOpenClawNode, u as OPENCLAW_NODE_REQUIREMENT } from "./runtime-process.js";
1
+ import { p as supportsOpenClawNode, u as OPENCLAW_NODE_REQUIREMENT } from "./runtime-process.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";
@@ -90,6 +90,16 @@ function supportsOpenClawNode(nodeVersion) {
90
90
  if (major === 26) return minor >= 1;
91
91
  return major > 26;
92
92
  }
93
+ /**
94
+ * Extract the semver-like version token printed by `openclaw --version`.
95
+ * Every OpenClaw release prints a branded line (`OpenClaw 2026.6.11 (e085fa1)`,
96
+ * `OpenClaw 2026.9.5 (ec9c1a1)`); this reduces it to the bare token. Requires
97
+ * whitespace delimiters and a four-digit year major, so unexpected formats
98
+ * return `undefined` and callers fall back to the raw line.
99
+ */
100
+ function parseOpenClawVersion(output) {
101
+ return /(?:^|\s)(\d{4}\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)(?:\s|$)/u.exec(output.trim())?.[1];
102
+ }
93
103
  //#endregion
94
104
  //#region ../../node_modules/.pnpm/dotenv@17.2.2/node_modules/dotenv/package.json
95
105
  var require_package = /* @__PURE__ */ __commonJSMin(((exports, module) => {
@@ -15766,4 +15776,4 @@ var RuntimeProcess = class {
15766
15776
  }
15767
15777
  };
15768
15778
  //#endregion
15769
- export { ErrorLineDetector as a, createLogger as c, PINNED_OPENCLAW_VERSION as d, supportsOpenClawNode as f, CaptureThrottle as i, OPENCLAW_MANAGED_NODE_MAJOR as l, __require as m, OPENCLAW_EXTERNAL_SUPERVISOR_ENV as n, STRICT_ERROR_CAPTURE_PROFILE as o, __commonJSMin as p, RuntimeProcess as r, toFingerprintKey as s, DRAIN_GRACE_MS as t, OPENCLAW_NODE_REQUIREMENT as u };
15779
+ 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 };
@@ -1,5 +1,5 @@
1
- import { f as supportsOpenClawNode, n as OPENCLAW_EXTERNAL_SUPERVISOR_ENV, u as OPENCLAW_NODE_REQUIREMENT } from "./runtime-process.js";
2
- import { c as captureNodeRemediation, h as logger } from "./sentry.js";
1
+ import { f as parseOpenClawVersion, n as OPENCLAW_EXTERNAL_SUPERVISOR_ENV, p as supportsOpenClawNode, u as OPENCLAW_NODE_REQUIREMENT } from "./runtime-process.js";
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";
5
5
  import { join } from "node:path";
@@ -27,6 +27,7 @@ import { promisify } from "node:util";
27
27
  const execFileAsync = promisify(execFile);
28
28
  const OPENCLAW_UPGRADE_TIMEOUT_MS = 3e5;
29
29
  const OPENCLAW_DOCTOR_TIMEOUT_MS = 9e5;
30
+ const OPENCLAW_DOCTOR_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
30
31
  /**
31
32
  * A non-diagnostic probe run right after the new package lands. 2026.9.x
32
33
  * classifies `--version`/`--help` as diagnostic and lets them succeed even when
@@ -41,6 +42,31 @@ const OPENCLAW_DOCTOR_TIMEOUT_MS = 9e5;
41
42
  * runtime regardless of config contents.
42
43
  */
43
44
  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
+ const OPENCLAW_DOCTOR_AUDIT_ARGS = [
59
+ "doctor",
60
+ "--post-upgrade",
61
+ "--json"
62
+ ];
63
+ /** Keep the tail of a command's output in an error so a failed migration is diagnosable from the daemon log. */
64
+ const OUTPUT_TAIL_CHARS = 1500;
65
+ const ANSI_ESCAPE = new RegExp(`${String.fromCharCode(27)}\\[[0-9;?]*[A-Za-z]`, "g");
66
+ function outputTail(value) {
67
+ const text = typeof value === "string" ? value.replace(ANSI_ESCAPE, "").trim() : "";
68
+ return text.length > OUTPUT_TAIL_CHARS ? `…${text.slice(-OUTPUT_TAIL_CHARS)}` : text;
69
+ }
44
70
  function resolveUpgradeCommand(runtime, version) {
45
71
  switch (runtime) {
46
72
  case "openclaw":
@@ -70,10 +96,6 @@ function resolveUpgradeCommand(runtime, version) {
70
96
  default: return;
71
97
  }
72
98
  }
73
- /** Extract the semver-like version token printed by `openclaw --version`. */
74
- function parseOpenClawVersion(output) {
75
- return /(?:^|\s)(\d{4}\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)(?:\s|$)/u.exec(output.trim())?.[1];
76
- }
77
99
  async function upgradeRuntime(runtime, version, runtimeProcess, options = {}) {
78
100
  const runCommand = options.runCommand ?? (async (command, args, commandOptions) => {
79
101
  const result = await execFileAsync(command, args, commandOptions);
@@ -210,18 +232,25 @@ async function upgradeRuntime(runtime, version, runtimeProcess, options = {}) {
210
232
  runtimeChanged = installedVersion !== previousOpenClawVersion;
211
233
  await runCommand("openclaw", [...OPENCLAW_ADMISSION_PROBE_ARGS], commandOptions(6e4));
212
234
  doctorStarted = true;
213
- await runCommand("openclaw", [
214
- "doctor",
215
- "--fix",
216
- "--non-interactive",
217
- "--yes",
218
- "--json"
219
- ], commandOptions(OPENCLAW_DOCTOR_TIMEOUT_MS));
220
- const audit = await runCommand("openclaw", [
221
- "doctor",
222
- "--post-upgrade",
223
- "--json"
224
- ], commandOptions(OPENCLAW_DOCTOR_TIMEOUT_MS));
235
+ try {
236
+ const doctor = await runCommand("openclaw", [...OPENCLAW_DOCTOR_FIX_ARGS], {
237
+ ...commandOptions(OPENCLAW_DOCTOR_TIMEOUT_MS),
238
+ maxBuffer: OPENCLAW_DOCTOR_MAX_BUFFER_BYTES
239
+ });
240
+ logger.info({ doctor: outputTail(doctor.stdout) }, "OpenClaw doctor migration completed");
241
+ } catch (err) {
242
+ const failure = err;
243
+ const stdout = outputTail(failure.stdout);
244
+ const stderr = outputTail(failure.stderr);
245
+ logger.error({
246
+ stdout,
247
+ stderr
248
+ }, "OpenClaw doctor migration failed");
249
+ const wrapped = new Error(`openclaw doctor --fix failed: ${failure.message ?? String(err)}` + (stderr ? `\nstderr: ${stderr}` : "") + (stdout ? `\nstdout: ${stdout}` : ""), { cause: err });
250
+ captureCliFailure("openclaw-doctor-migration", wrapped);
251
+ throw wrapped;
252
+ }
253
+ const audit = await runCommand("openclaw", [...OPENCLAW_DOCTOR_AUDIT_ARGS], commandOptions(OPENCLAW_DOCTOR_TIMEOUT_MS));
225
254
  if (audit.stdout) logger.info({ audit: audit.stdout.trim() }, "OpenClaw post-upgrade audit completed");
226
255
  }
227
256
  } catch (err) {
@@ -243,7 +272,7 @@ async function upgradeRuntime(runtime, version, runtimeProcess, options = {}) {
243
272
  try {
244
273
  if (options.gate) {
245
274
  gateSuspended = true;
246
- await options.gate.suspend();
275
+ await options.gate.suspendExclusive();
247
276
  } else if (runtimeProcess) await runtimeProcess.stopWhenIdle();
248
277
  if (runtime === "openclaw" && options.cliLock) await options.cliLock.run(performUpgrade);
249
278
  else await performUpgrade();
@@ -76,6 +76,13 @@ declare const OPENCLAW_MANAGED_NODE_MAJOR = 24;
76
76
  * Pre-release tags are rejected; build metadata (`+…`) is accepted.
77
77
  */
78
78
  declare function supportsOpenClawNode(nodeVersion: string): boolean;
79
+ /**
80
+ * Extract the semver-like version token printed by `openclaw --version`.
81
+ * Every OpenClaw release prints a branded line (`OpenClaw 2026.6.11 (e085fa1)`,
82
+ * `OpenClaw 2026.9.5 (ec9c1a1)`); this reduces it to the bare token. Requires
83
+ * whitespace delimiters and a four-digit year major, so unexpected formats
84
+ * return `undefined` and callers fall back to the raw line.
85
+ */
79
86
  //#endregion
80
87
  //#region src/node-remediation.d.ts
81
88
 
package/dist/src/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { d as PINNED_OPENCLAW_VERSION, f as supportsOpenClawNode, l as OPENCLAW_MANAGED_NODE_MAJOR, u as OPENCLAW_NODE_REQUIREMENT } from "../runtime-process.js";
1
+ import { d as PINNED_OPENCLAW_VERSION, l as OPENCLAW_MANAGED_NODE_MAJOR, p as supportsOpenClawNode, u as OPENCLAW_NODE_REQUIREMENT } from "../runtime-process.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.1",
3
+ "version": "0.10.3",
4
4
  "description": "Alfe local gateway daemon — persistent control plane for agent integrations",
5
5
  "type": "module",
6
6
  "bin": {
@@ -27,7 +27,7 @@
27
27
  "@alfe.ai/ai-proxy-local": "^0.0.17",
28
28
  "@alfe.ai/config": "^0.4.1",
29
29
  "@alfe.ai/integration-manifest": "^0.4.2",
30
- "@alfe.ai/integrations": "^0.6.7",
30
+ "@alfe.ai/integrations": "^0.6.8",
31
31
  "@alfe.ai/mcp-bundler": "^0.4.2"
32
32
  },
33
33
  "license": "UNLICENSED",