@chrysb/alphaclaw 0.9.28 → 0.9.30

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.
Files changed (33) hide show
  1. package/README.md +1 -1
  2. package/bin/alphaclaw.js +34 -0
  3. package/lib/node-runtime.js +38 -0
  4. package/lib/public/dist/app.bundle.js +1027 -1020
  5. package/lib/public/js/components/gateway.js +7 -2
  6. package/lib/public/js/components/models-tab/model-picker.js +5 -2
  7. package/lib/public/js/components/models-tab/use-models.js +4 -1
  8. package/lib/public/js/components/nodes-tab/setup-wizard/index.js +3 -1
  9. package/lib/public/js/components/onboarding/welcome-config.js +6 -2
  10. package/lib/public/js/components/onboarding/welcome-setup-step.js +58 -4
  11. package/lib/public/js/components/welcome/use-welcome.js +18 -8
  12. package/lib/public/js/lib/api.js +5 -0
  13. package/lib/public/js/lib/model-config.js +77 -8
  14. package/lib/public/js/lib/thinking-levels.js +1 -0
  15. package/lib/server/agents/shared.js +2 -0
  16. package/lib/server/auth-profiles.js +8 -1
  17. package/lib/server/codex-runtime-config.js +51 -0
  18. package/lib/server/constants.js +18 -0
  19. package/lib/server/cost-utils.js +18 -0
  20. package/lib/server/helpers.js +18 -2
  21. package/lib/server/model-catalog-cache.js +2 -2
  22. package/lib/server/onboarding/index.js +11 -0
  23. package/lib/server/onboarding/openclaw.js +4 -0
  24. package/lib/server/onboarding/validation.js +17 -3
  25. package/lib/server/openclaw-codex-migration.js +61 -17
  26. package/lib/server/openclaw-thinking.js +33 -27
  27. package/lib/server/openclaw-version.js +7 -0
  28. package/lib/server/plugin-config.js +22 -0
  29. package/lib/server/routes/models.js +4 -0
  30. package/lib/server/routes/onboarding.js +33 -0
  31. package/lib/server/usage-tracker-config.js +3 -21
  32. package/lib/server/watchdog.js +43 -0
  33. package/package.json +3 -3
@@ -2,6 +2,17 @@ const { getEnvVarForApiKeyProvider } = require("../auth-profiles");
2
2
 
3
3
  const kAnthropicSetupTokenPrefix = "sk-ant-oat01-";
4
4
  const kAnthropicApiKeyPrefix = "sk-ant-api";
5
+ const kCanonicalCodexOauthModelKeys = new Set([
6
+ "openai/gpt-5.4-mini",
7
+ "openai/gpt-5.5",
8
+ "openai/gpt-5.6-sol",
9
+ "openai/gpt-5.6-terra",
10
+ "openai/gpt-5.6-luna",
11
+ ]);
12
+
13
+ const usesCodexOauth = (modelKey, provider) =>
14
+ provider === "openai-codex" ||
15
+ kCanonicalCodexOauthModelKeys.has(String(modelKey || "").trim());
5
16
 
6
17
  const validateAnthropicCredentialShape = (varMap) => {
7
18
  const anthropicToken = String(varMap.ANTHROPIC_TOKEN || "").trim();
@@ -71,7 +82,10 @@ const validateOnboardingInput = ({ vars, modelKey, resolveModelProvider, hasCode
71
82
  if (!anthropicValidation.ok) return anthropicValidation;
72
83
  const githubToken = String(varMap.GITHUB_TOKEN || "");
73
84
  const githubRepoInput = String(varMap.GITHUB_WORKSPACE_REPO || "").trim();
74
- const selectedProvider = resolveModelProvider(modelKey);
85
+ const resolvedProvider = resolveModelProvider(modelKey);
86
+ const selectedProvider = usesCodexOauth(modelKey, resolvedProvider)
87
+ ? "openai-codex"
88
+ : resolvedProvider;
75
89
  const hasCodexOauth = hasCodexOauthProfile();
76
90
  const hasAnyAi = !!(
77
91
  varMap.ANTHROPIC_API_KEY ||
@@ -82,7 +96,7 @@ const validateOnboardingInput = ({ vars, modelKey, resolveModelProvider, hasCode
82
96
  );
83
97
  const hasAi = (() => {
84
98
  if (selectedProvider === "openai-codex") {
85
- return hasCodexOauth;
99
+ return hasCodexOauth || !!String(varMap.OPENAI_API_KEY || "").trim();
86
100
  }
87
101
  if (selectedProvider === "anthropic") {
88
102
  return !!(varMap.ANTHROPIC_API_KEY || varMap.ANTHROPIC_TOKEN);
@@ -101,7 +115,7 @@ const validateOnboardingInput = ({ vars, modelKey, resolveModelProvider, hasCode
101
115
  return {
102
116
  ok: false,
103
117
  status: 400,
104
- error: "Connect OpenAI Codex OAuth before continuing",
118
+ error: "Connect OpenAI Codex OAuth or add an OpenAI API key before continuing",
105
119
  };
106
120
  }
107
121
  return {
@@ -3,14 +3,36 @@ const path = require("path");
3
3
  const { pathToFileURL } = require("url");
4
4
  const { DatabaseSync } = require("node:sqlite");
5
5
 
6
- const findOpenclawDistModule = (prefix) => {
6
+ const findOpenclawDistModules = (prefix) => {
7
7
  const entryPath = require.resolve("openclaw");
8
8
  const distDir = path.dirname(entryPath);
9
- const filename = fs
9
+ const filenames = fs
10
10
  .readdirSync(distDir)
11
- .find((name) => name.startsWith(`${prefix}-`) && name.endsWith(".js"));
12
- if (!filename) throw new Error(`OpenClaw migration module not found: ${prefix}`);
13
- return path.join(distDir, filename);
11
+ .filter((name) => name.startsWith(`${prefix}-`) && name.endsWith(".js"));
12
+ if (filenames.length === 0) {
13
+ throw new Error(`OpenClaw migration module not found: ${prefix}`);
14
+ }
15
+ return filenames.map((filename) => path.join(distDir, filename));
16
+ };
17
+
18
+ const loadOpenclawMigrationApi = async ({ prefix, functionNames }) => {
19
+ const api = {};
20
+ for (const modulePath of findOpenclawDistModules(prefix)) {
21
+ const mod = await import(pathToFileURL(modulePath).href);
22
+ for (const candidate of Object.values(mod)) {
23
+ if (typeof candidate !== "function") continue;
24
+ if (functionNames.includes(candidate.name) && !api[candidate.name]) {
25
+ api[candidate.name] = candidate;
26
+ }
27
+ }
28
+ }
29
+ const missing = functionNames.filter((name) => typeof api[name] !== "function");
30
+ if (missing.length > 0) {
31
+ throw new Error(
32
+ `OpenClaw migration exports not found for ${prefix}: ${missing.join(", ")}`,
33
+ );
34
+ }
35
+ return api;
14
36
  };
15
37
 
16
38
  const writeConfig = (configPath, cfg) => {
@@ -73,33 +95,55 @@ const migrateLegacyCodexState = async ({
73
95
  }
74
96
 
75
97
  const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
76
- const routeModule = await import(
77
- pathToFileURL(findOpenclawDistModule("codex-route-warnings")).href
78
- );
79
- const authModule = await import(
80
- pathToFileURL(findOpenclawDistModule("doctor-auth-flat-profiles")).href
81
- );
98
+ const routeApi = await loadOpenclawMigrationApi({
99
+ prefix: "codex-route-warnings",
100
+ functionNames: [
101
+ "maybeRepairCodexRoutes",
102
+ "maybeRepairCodexSessionRoutes",
103
+ ],
104
+ });
105
+ const authApi = await loadOpenclawMigrationApi({
106
+ prefix: "doctor-auth-flat-profiles",
107
+ functionNames: [
108
+ "collectOpenAICodexAuthProfileStoreIdMap",
109
+ "maybeRepairOpenAICodexAuthConfig",
110
+ "maybeRepairOpenAICodexAuthProfileStores",
111
+ "maybeMigrateAuthProfileJsonStoresToSqlite",
112
+ ],
113
+ });
82
114
 
83
115
  const changes = [];
84
116
  const warnings = [];
85
- const routeRepair = routeModule.r({ cfg, env, shouldRepair: true });
117
+ const routeRepair = routeApi.maybeRepairCodexRoutes({
118
+ cfg,
119
+ env,
120
+ shouldRepair: true,
121
+ });
86
122
  let nextCfg = routeRepair.cfg;
87
123
  changes.push(...routeRepair.changes);
88
124
  warnings.push(...routeRepair.warnings);
89
125
 
90
- const profileIdMap = authModule.t({ cfg: nextCfg, env });
91
- const configAuthRepair = authModule.a(nextCfg, { profileIdMap });
126
+ const profileIdMap = authApi.collectOpenAICodexAuthProfileStoreIdMap({
127
+ cfg: nextCfg,
128
+ env,
129
+ });
130
+ const configAuthRepair = authApi.maybeRepairOpenAICodexAuthConfig(nextCfg, {
131
+ profileIdMap,
132
+ });
92
133
  nextCfg = configAuthRepair.config;
93
134
  changes.push(...configAuthRepair.changes);
94
135
  warnings.push(...configAuthRepair.warnings);
95
136
 
96
137
  if (changes.length > 0) writeConfig(configPath, nextCfg);
97
138
 
98
- const storeRepair = await authModule.o({ cfg: nextCfg, env });
139
+ const storeRepair = await authApi.maybeRepairOpenAICodexAuthProfileStores({
140
+ cfg: nextCfg,
141
+ env,
142
+ });
99
143
  changes.push(...storeRepair.changes);
100
144
  warnings.push(...storeRepair.warnings);
101
145
 
102
- const sqliteMigration = await authModule.n({
146
+ const sqliteMigration = await authApi.maybeMigrateAuthProfileJsonStoresToSqlite({
103
147
  cfg: nextCfg,
104
148
  env,
105
149
  prompter: { confirmAutoFix: async () => true },
@@ -113,7 +157,7 @@ const migrateLegacyCodexState = async ({
113
157
  writeConfig(configPath, nextCfg);
114
158
  }
115
159
 
116
- const sessionRepair = await routeModule.i({
160
+ const sessionRepair = await routeApi.maybeRepairCodexSessionRoutes({
117
161
  cfg: nextCfg,
118
162
  env,
119
163
  shouldRepair: true,
@@ -27,29 +27,28 @@ const loadThinkingModule = async () => {
27
27
  return thinkingModulePromise;
28
28
  };
29
29
 
30
- const isNormalizeThinkLevel = (candidate) => {
31
- if (typeof candidate !== "function") return false;
32
- try {
33
- return (
34
- candidate("off") === "off" &&
35
- candidate("low") === "low" &&
36
- candidate("high") === "high"
37
- );
38
- } catch {
39
- return false;
30
+ const normalizeThinkingLevel = (raw) => {
31
+ const key = String(raw || "").trim().toLowerCase();
32
+ if (!key) return null;
33
+ const collapsed = key.replace(/[\s_-]+/g, "");
34
+ if (collapsed === "adaptive" || collapsed === "auto") return "adaptive";
35
+ if (collapsed === "max") return "max";
36
+ if (collapsed === "ultra") return "ultra";
37
+ if (collapsed === "xhigh" || collapsed === "extrahigh") return "xhigh";
38
+ if (key === "off") return "off";
39
+ if (["on", "enable", "enabled"].includes(key)) return "low";
40
+ if (["min", "minimal"].includes(key)) return "minimal";
41
+ if (["low", "thinkhard", "think-hard", "think_hard"].includes(key)) {
42
+ return "low";
40
43
  }
41
- };
42
-
43
- const resolveNormalizeThinkLevel = (mod) => {
44
- const candidates = [
45
- mod.normalizeThinkLevel,
46
- mod.f,
47
- mod.p,
48
- ...Object.values(mod),
49
- ];
50
- const match = candidates.find(isNormalizeThinkLevel);
51
- if (!match) throw new Error("OpenClaw normalizeThinkLevel export not found");
52
- return match;
44
+ if (["mid", "med", "medium", "thinkharder", "think-harder", "harder"].includes(key)) {
45
+ return "medium";
46
+ }
47
+ if (["high", "ultrathink", "thinkhardest", "highest"].includes(key)) {
48
+ return "high";
49
+ }
50
+ if (key === "think") return "minimal";
51
+ return null;
53
52
  };
54
53
 
55
54
  const splitModelKey = (modelKey = "") => {
@@ -80,13 +79,13 @@ const resolveThinkingApi = async () => {
80
79
  return {
81
80
  listThinkingLevelOptions: mod.listThinkingLevelOptions || mod.i,
82
81
  resolveThinkingDefaultForModel: mod.resolveThinkingDefaultForModel || mod.s,
83
- normalizeThinkLevel: resolveNormalizeThinkLevel(mod),
84
82
  };
85
83
  };
86
84
 
87
85
  const resolveThinkingOptionsForModel = async ({
88
86
  modelKey = "",
89
87
  catalog = [],
88
+ agentRuntime = "",
90
89
  } = {}) => {
91
90
  const { provider, model } = splitModelKey(modelKey);
92
91
  if (!provider || !model) {
@@ -96,12 +95,20 @@ const resolveThinkingOptionsForModel = async ({
96
95
  };
97
96
  }
98
97
  const api = await resolveThinkingApi();
99
- const levels = api.listThinkingLevelOptions(provider, model, catalog) || [];
98
+ const normalizedAgentRuntime = String(agentRuntime || "").trim() || undefined;
99
+ const levels =
100
+ api.listThinkingLevelOptions(
101
+ provider,
102
+ model,
103
+ catalog,
104
+ normalizedAgentRuntime,
105
+ ) || [];
100
106
  const modelDefault =
101
107
  api.resolveThinkingDefaultForModel({
102
108
  provider,
103
109
  model,
104
110
  catalog,
111
+ agentRuntime: normalizedAgentRuntime,
105
112
  }) || "off";
106
113
  return {
107
114
  levels: levels.map((entry) => ({
@@ -114,14 +121,13 @@ const resolveThinkingOptionsForModel = async ({
114
121
 
115
122
  const normalizeThinkingDefaultValue = async (raw) => {
116
123
  if (raw === null || raw === undefined || raw === "") return null;
117
- const api = await resolveThinkingApi();
118
- const normalized = api.normalizeThinkLevel(String(raw || "").trim());
119
- return normalized || null;
124
+ return normalizeThinkingLevel(raw);
120
125
  };
121
126
 
122
127
  module.exports = {
123
128
  buildCatalogEntry,
124
129
  loadThinkingModule,
130
+ normalizeThinkingLevel,
125
131
  normalizeThinkingDefaultValue,
126
132
  resolveThinkingOptionsForModel,
127
133
  splitModelKey,
@@ -10,6 +10,7 @@ const {
10
10
  } = require("./constants");
11
11
  const { normalizeOpenclawVersion } = require("./helpers");
12
12
  const { parseJsonObjectFromNoisyOutput } = require("./utils/json");
13
+ const { assertSupportedNodeVersion } = require("../node-runtime");
13
14
 
14
15
  const createOpenclawVersionService = ({
15
16
  gatewayEnv,
@@ -124,6 +125,12 @@ const createOpenclawVersionService = ({
124
125
  // Copying individual files (cp -af) avoids the rename syscall entirely.
125
126
  const installLatestOpenclaw = () =>
126
127
  new Promise((resolve, reject) => {
128
+ try {
129
+ assertSupportedNodeVersion();
130
+ } catch (error) {
131
+ reject(error);
132
+ return;
133
+ }
127
134
  const installDir = findInstallDir();
128
135
  const tmpDir = fs.mkdtempSync(
129
136
  path.join(os.tmpdir(), "openclaw-update-"),
@@ -0,0 +1,22 @@
1
+ const ensurePluginsShell = (cfg = {}) => {
2
+ if (!cfg.plugins || typeof cfg.plugins !== "object") cfg.plugins = {};
3
+ if (!Array.isArray(cfg.plugins.allow)) cfg.plugins.allow = [];
4
+ if (!cfg.plugins.load || typeof cfg.plugins.load !== "object") {
5
+ cfg.plugins.load = {};
6
+ }
7
+ if (!Array.isArray(cfg.plugins.load.paths)) cfg.plugins.load.paths = [];
8
+ if (!cfg.plugins.entries || typeof cfg.plugins.entries !== "object") {
9
+ cfg.plugins.entries = {};
10
+ }
11
+ };
12
+
13
+ const ensurePluginAllowed = ({ cfg = {}, pluginKey = "" }) => {
14
+ const normalizedPluginKey = String(pluginKey || "").trim();
15
+ if (!normalizedPluginKey) return;
16
+ ensurePluginsShell(cfg);
17
+ if (!cfg.plugins.allow.includes(normalizedPluginKey)) {
18
+ cfg.plugins.allow.push(normalizedPluginKey);
19
+ }
20
+ };
21
+
22
+ module.exports = { ensurePluginsShell, ensurePluginAllowed };
@@ -197,8 +197,12 @@ const registerModelRoutes = ({
197
197
  typeof cfg.agents?.defaults?.thinkingDefault === "string"
198
198
  ? cfg.agents.defaults.thinkingDefault.trim()
199
199
  : null;
200
+ const agentRuntime = String(
201
+ cfg.agents?.defaults?.models?.[modelKey]?.agentRuntime?.id || "",
202
+ ).trim();
200
203
  const { levels, modelDefault } = await resolveThinkingOptionsForModel({
201
204
  modelKey,
205
+ agentRuntime,
202
206
  });
203
207
  const inheritedDefault = globalThinkingDefault || modelDefault || "off";
204
208
  return res.json({
@@ -95,6 +95,28 @@ const registerOnboardingRoutes = ({
95
95
  getBaseUrl,
96
96
  runOnboardedBootSequence,
97
97
  }) => {
98
+ const kOnboardingProgressMessages = {
99
+ creating_repo: "Creating repo...",
100
+ running_openclaw_onboard: "Running openclaw onboard...",
101
+ initial_git_push: "Initial git commit/push...",
102
+ starting_gateway: "Starting gateway...",
103
+ };
104
+ let onboardingProgress = {
105
+ active: false,
106
+ stage: "idle",
107
+ message: "",
108
+ updatedAt: null,
109
+ };
110
+ const setOnboardingProgress = (stage, { active = true } = {}) => {
111
+ const normalizedStage = String(stage || "").trim();
112
+ onboardingProgress = {
113
+ active,
114
+ stage: normalizedStage,
115
+ message: kOnboardingProgressMessages[normalizedStage] || "",
116
+ updatedAt: new Date().toISOString(),
117
+ };
118
+ };
119
+
98
120
  // Keep mutating onboarding routes marker-gated so in-progress imports
99
121
  // can promote files before the final completion marker is written.
100
122
  const hasExplicitOnboardingMarker = () =>
@@ -157,21 +179,32 @@ const registerOnboardingRoutes = ({
157
179
  res.json({ onboarded: hasExplicitOnboardingMarker() });
158
180
  });
159
181
 
182
+ app.get("/api/onboard/progress", (req, res) => {
183
+ res.json(onboardingProgress);
184
+ });
185
+
160
186
  app.post("/api/onboard", async (req, res) => {
161
187
  if (hasExplicitOnboardingMarker())
162
188
  return res.json({ ok: false, error: "Already onboarded" });
163
189
 
164
190
  try {
191
+ setOnboardingProgress("creating_repo");
165
192
  const { vars, modelKey, importMode } = req.body;
166
193
  const result = await onboardingService.completeOnboarding({
167
194
  req,
168
195
  vars,
169
196
  modelKey,
170
197
  importMode: !!importMode,
198
+ onProgress: (stage) => setOnboardingProgress(stage),
171
199
  });
200
+ setOnboardingProgress(
201
+ result.body?.ok ? "starting_gateway" : "failed",
202
+ { active: false },
203
+ );
172
204
  res.status(result.status).json(result.body);
173
205
  } catch (err) {
174
206
  console.error("[onboard] Error:", err);
207
+ setOnboardingProgress("failed", { active: false });
175
208
  res.status(500).json({ ok: false, error: sanitizeOnboardingError(err) });
176
209
  }
177
210
  });
@@ -3,6 +3,8 @@ const { readOpenclawConfig, writeOpenclawConfig } = require("./openclaw-config")
3
3
  const {
4
4
  migrateLegacyTelegramStreamingConfig,
5
5
  } = require("./openclaw-config-migrations");
6
+ const { ensureCodexRuntimePlugin } = require("./codex-runtime-config");
7
+ const { ensurePluginsShell, ensurePluginAllowed } = require("./plugin-config");
6
8
 
7
9
  const kUsageTrackerPluginPath = path.resolve(
8
10
  __dirname,
@@ -14,27 +16,6 @@ const kConversationAccessHookPolicyKey = "allowConversationAccess";
14
16
  const kChannelPluginIds = ["telegram", "discord", "slack", "whatsapp"];
15
17
  const kDefaultDiscordGroupPolicy = "disabled";
16
18
 
17
- const ensurePluginsShell = (cfg = {}) => {
18
- if (!cfg.plugins || typeof cfg.plugins !== "object") cfg.plugins = {};
19
- if (!Array.isArray(cfg.plugins.allow)) cfg.plugins.allow = [];
20
- if (!cfg.plugins.load || typeof cfg.plugins.load !== "object") {
21
- cfg.plugins.load = {};
22
- }
23
- if (!Array.isArray(cfg.plugins.load.paths)) cfg.plugins.load.paths = [];
24
- if (!cfg.plugins.entries || typeof cfg.plugins.entries !== "object") {
25
- cfg.plugins.entries = {};
26
- }
27
- };
28
-
29
- const ensurePluginAllowed = ({ cfg = {}, pluginKey = "" }) => {
30
- const normalizedPluginKey = String(pluginKey || "").trim();
31
- if (!normalizedPluginKey) return;
32
- ensurePluginsShell(cfg);
33
- if (!cfg.plugins.allow.includes(normalizedPluginKey)) {
34
- cfg.plugins.allow.push(normalizedPluginKey);
35
- }
36
- };
37
-
38
19
  const buildUsageTrackerHookPolicy = ({ existingHooks = {} } = {}) => {
39
20
  const hooks = {};
40
21
  if (typeof existingHooks.allowPromptInjection === "boolean") {
@@ -115,6 +96,7 @@ const reconcileEnabledChannelPlugins = (cfg = {}) => {
115
96
 
116
97
  const reconcileManagedPluginConfig = (cfg = {}) => {
117
98
  let changed = ensureUsageTrackerPluginEntry(cfg);
99
+ if (ensureCodexRuntimePlugin(cfg)) changed = true;
118
100
  if (reconcileEnabledChannelPlugins(cfg)) changed = true;
119
101
  if (reconcileDiscordGroupPolicy(cfg)) changed = true;
120
102
  return changed;
@@ -63,6 +63,7 @@ const createWatchdog = ({
63
63
  pendingRecoveryNoticeSource: "",
64
64
  awaitingAutoRepairRecovery: false,
65
65
  startupConsecutiveHealthFailures: 0,
66
+ configurationErrorActive: false,
66
67
  };
67
68
  let healthTimer = null;
68
69
  let bootstrapHealthTimer = null;
@@ -90,6 +91,7 @@ const createWatchdog = ({
90
91
 
91
92
  const scheduleDegradedHealthCheck = () => {
92
93
  if (degradedHealthTimer) return;
94
+ if (state.configurationErrorActive) return;
93
95
  if (state.health !== "degraded" || state.lifecycle !== "running") return;
94
96
  degradedHealthTimer = setTimeout(async () => {
95
97
  degradedHealthTimer = null;
@@ -380,6 +382,9 @@ const createWatchdog = ({
380
382
  };
381
383
 
382
384
  const runRepair = async ({ source, correlationId, force = false }) => {
385
+ if (state.configurationErrorActive && !force) {
386
+ return { ok: false, skipped: true, reason: "configuration_error" };
387
+ }
383
388
  if (!force && !state.autoRepair) {
384
389
  return { ok: false, skipped: true, reason: "auto_repair_disabled" };
385
390
  }
@@ -389,10 +394,16 @@ const createWatchdog = ({
389
394
  if (state.operationInProgress) {
390
395
  return { ok: false, skipped: true, reason: "operation_in_progress" };
391
396
  }
397
+ if (force) {
398
+ state.configurationErrorActive = false;
399
+ }
392
400
 
393
401
  state.operationInProgress = true;
394
402
  try {
395
403
  const result = await clawCmd("doctor --fix --yes", { quiet: true });
404
+ if (state.configurationErrorActive && !force) {
405
+ return { ok: false, skipped: true, reason: "configuration_error" };
406
+ }
396
407
  const ok = !!result?.ok;
397
408
  logEvent("repair", source, ok ? "ok" : "failed", result, correlationId);
398
409
  if (ok) {
@@ -484,6 +495,7 @@ const createWatchdog = ({
484
495
  source = "health_timer",
485
496
  allowAutoRepair = true,
486
497
  } = {}) => {
498
+ if (state.configurationErrorActive) return false;
487
499
  if (
488
500
  state.expectedRestartInProgress &&
489
501
  Date.now() >= state.expectedRestartUntilMs
@@ -495,6 +507,9 @@ const createWatchdog = ({
495
507
  const correlationId = createCorrelationId();
496
508
  state.lastHealthCheckAt = new Date().toISOString();
497
509
  const parsed = await probeGatewayHealth();
510
+ // The gateway may exit with EX_CONFIG while a probe is in flight. Keep the
511
+ // latched configuration-error state from being overwritten by that result.
512
+ if (state.configurationErrorActive) return false;
498
513
  const staleAfterRestart =
499
514
  gatewayStartedAtAtStart != null &&
500
515
  state.gatewayStartedAt != null &&
@@ -727,6 +742,33 @@ const createWatchdog = ({
727
742
  return;
728
743
  }
729
744
 
745
+ if (code === 78) {
746
+ state.configurationErrorActive = true;
747
+ state.lifecycle = "configuration_error";
748
+ state.health = "unhealthy";
749
+ state.uptimeStartedAt = null;
750
+ state.crashRecoveryActive = false;
751
+ state.startupConsecutiveHealthFailures = 0;
752
+ logEvent(
753
+ "config_error",
754
+ "exit_event",
755
+ "failed",
756
+ { code, signal: signal ?? null, stderrTail },
757
+ correlationId,
758
+ );
759
+ void notifyOncePerIncident(
760
+ "gateway_config_error",
761
+ [
762
+ "🐺 *AlphaClaw Watchdog*",
763
+ withViewLogsSuffix("🔴 Gateway configuration invalid"),
764
+ "OpenClaw stopped with `EX_CONFIG`; automatic restart is paused.",
765
+ ].join("\n"),
766
+ correlationId,
767
+ "config_error",
768
+ );
769
+ return;
770
+ }
771
+
730
772
  state.lifecycle = "crashed";
731
773
  state.health = "unhealthy";
732
774
  state.uptimeStartedAt = null;
@@ -787,6 +829,7 @@ const createWatchdog = ({
787
829
 
788
830
  const onGatewayLaunch = ({ startedAt = Date.now(), pid = null } = {}) => {
789
831
  clearDegradedHealthCheckTimer();
832
+ state.configurationErrorActive = false;
790
833
  state.lifecycle = "running";
791
834
  state.health = "unknown";
792
835
  state.startupConsecutiveHealthFailures = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.9.28",
3
+ "version": "0.9.30",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -33,7 +33,7 @@
33
33
  "dependencies": {
34
34
  "express": "^4.21.0",
35
35
  "http-proxy": "^1.18.1",
36
- "openclaw": "2026.6.11",
36
+ "openclaw": "2026.7.1",
37
37
  "ws": "^8.19.0"
38
38
  },
39
39
  "devDependencies": {
@@ -51,6 +51,6 @@
51
51
  "wouter-preact": "^3.7.1"
52
52
  },
53
53
  "engines": {
54
- "node": ">=22.19.0"
54
+ "node": ">=22.22.3 <23 || >=24.15.0 <25 || >=25.9.0"
55
55
  }
56
56
  }