@chrysb/alphaclaw 0.9.29 → 0.9.31

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.
@@ -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({
@@ -1,6 +1,7 @@
1
1
  const runOnboardedBootSequence = ({
2
2
  ensureManagedExecDefaults,
3
3
  ensureUsageTrackerPluginConfig,
4
+ ensureWebhookMappingIds,
4
5
  doSyncPromptFiles,
5
6
  reloadEnv,
6
7
  syncChannelConfig,
@@ -25,6 +26,18 @@ const runOnboardedBootSequence = ({
25
26
  `[alphaclaw] Failed to ensure usage-tracker plugin config on boot: ${error.message}`,
26
27
  );
27
28
  }
29
+ try {
30
+ const result = ensureWebhookMappingIds();
31
+ if (result?.changed) {
32
+ console.log(
33
+ `[alphaclaw] Added IDs to webhook mappings: ${result.updatedIds.join(", ")}`,
34
+ );
35
+ }
36
+ } catch (error) {
37
+ console.error(
38
+ `[alphaclaw] Failed to ensure webhook mapping IDs on boot: ${error.message}`,
39
+ );
40
+ }
28
41
  doSyncPromptFiles();
29
42
  reloadEnv();
30
43
  syncChannelConfig(readEnvFile());
@@ -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;
@@ -1,4 +1,8 @@
1
1
  const path = require("path");
2
+ const {
3
+ readOpenclawConfig,
4
+ resolveOpenclawConfigPath,
5
+ } = require("./openclaw-config");
2
6
 
3
7
  const kNamePattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
4
8
  const kTransformsDir = "hooks/transforms";
@@ -12,11 +16,16 @@ const kManagedWebhookConfigs = [
12
16
  ];
13
17
 
14
18
  const getConfigPath = ({ OPENCLAW_DIR }) =>
15
- path.join(OPENCLAW_DIR, "openclaw.json");
19
+ resolveOpenclawConfigPath({ openclawDir: OPENCLAW_DIR });
16
20
 
17
21
  const readConfig = ({ fs, constants }) => {
18
22
  const configPath = getConfigPath(constants);
19
- const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
23
+ const cfg = readOpenclawConfig({
24
+ fsModule: fs,
25
+ openclawDir: constants.OPENCLAW_DIR,
26
+ fallback: null,
27
+ });
28
+ if (!cfg) throw new Error("Could not read openclaw.json");
20
29
  return { cfg, configPath };
21
30
  };
22
31
 
@@ -78,6 +87,32 @@ const ensureHooksRoot = (cfg) => {
78
87
 
79
88
  const getMappingHookName = (mapping) =>
80
89
  String(mapping?.match?.path || "").trim();
90
+ const normalizeMappingIdPart = (value) =>
91
+ String(value || "")
92
+ .trim()
93
+ .toLowerCase()
94
+ .replace(/[^a-z0-9]+/g, "-")
95
+ .replace(/-+/g, "-")
96
+ .replace(/^-|-$/g, "");
97
+ const getMappingIdBase = (mapping, index) => {
98
+ const hookPath = normalizeMappingIdPart(getMappingHookName(mapping));
99
+ if (hookPath) return hookPath;
100
+ const name = normalizeMappingIdPart(mapping?.name);
101
+ if (name) return name;
102
+ const source = normalizeMappingIdPart(mapping?.match?.source);
103
+ if (source) return `source-${source}`;
104
+ return `mapping-${index + 1}`;
105
+ };
106
+ const reserveUniqueMappingId = (baseId, usedIds) => {
107
+ let candidate = baseId;
108
+ let suffix = 2;
109
+ while (usedIds.has(candidate)) {
110
+ candidate = `${baseId}-${suffix}`;
111
+ suffix += 1;
112
+ }
113
+ usedIds.add(candidate);
114
+ return candidate;
115
+ };
81
116
  const isWebhookMapping = (mapping) => !!getMappingHookName(mapping);
82
117
  const findMappingIndexByName = (mappings, name) =>
83
118
  mappings.findIndex((mapping) => getMappingHookName(mapping) === name);
@@ -142,6 +177,35 @@ const normalizeMappingTransformModules = (mappings) => {
142
177
  return changed;
143
178
  };
144
179
 
180
+ const ensureWebhookMappingIds = ({ fs, constants }) => {
181
+ const { cfg, configPath } = readConfig({ fs, constants });
182
+ const mappings = Array.isArray(cfg?.hooks?.mappings)
183
+ ? cfg.hooks.mappings
184
+ : [];
185
+ const usedIds = new Set(
186
+ mappings
187
+ .map((mapping) => String(mapping?.id || "").trim())
188
+ .filter(Boolean),
189
+ );
190
+ const updatedIds = [];
191
+ for (const [index, mapping] of mappings.entries()) {
192
+ if (!mapping || typeof mapping !== "object" || Array.isArray(mapping)) {
193
+ continue;
194
+ }
195
+ if (String(mapping?.id || "").trim()) continue;
196
+ const id = reserveUniqueMappingId(
197
+ getMappingIdBase(mapping, index),
198
+ usedIds,
199
+ );
200
+ mapping.id = id;
201
+ updatedIds.push(id);
202
+ }
203
+ if (updatedIds.length > 0) {
204
+ writeConfig({ fs, configPath, cfg });
205
+ }
206
+ return { changed: updatedIds.length > 0, updatedIds };
207
+ };
208
+
145
209
  const buildDefaultTransformSource = (name) => {
146
210
  return [
147
211
  "export default async function transform(payload, context) {",
@@ -188,6 +252,7 @@ const ensureWebhookMapping = ({ cfg, name, mapping = {} }) => {
188
252
  const normalizedModulesChanged = normalizeMappingTransformModules(mappings);
189
253
  const index = findMappingIndexByName(mappings, webhookName);
190
254
  const defaults = {
255
+ id: webhookName,
191
256
  match: { path: webhookName },
192
257
  action: "agent",
193
258
  name: webhookName,
@@ -198,6 +263,7 @@ const ensureWebhookMapping = ({ cfg, name, mapping = {} }) => {
198
263
  mappings.push({
199
264
  ...defaults,
200
265
  ...mapping,
266
+ id: webhookName,
201
267
  match: { ...defaults.match, ...(mapping.match || {}) },
202
268
  transform: { ...defaults.transform, ...(mapping.transform || {}) },
203
269
  });
@@ -207,6 +273,7 @@ const ensureWebhookMapping = ({ cfg, name, mapping = {} }) => {
207
273
  const next = {
208
274
  ...current,
209
275
  ...mapping,
276
+ id: webhookName,
210
277
  match: {
211
278
  ...(current.match || {}),
212
279
  ...(mapping.match || {}),
@@ -436,6 +503,7 @@ const updateWebhookDestination = ({ fs, constants, name, destination = null }) =
436
503
  });
437
504
  const next = {
438
505
  ...current,
506
+ id: webhookName,
439
507
  deliver: true,
440
508
  channel:
441
509
  String(normalizedDestination?.channel || "").trim() ||
@@ -499,6 +567,7 @@ const deleteWebhook = ({ fs, constants, name, deleteTransformDir = false }) => {
499
567
  };
500
568
 
501
569
  module.exports = {
570
+ ensureWebhookMappingIds,
502
571
  listWebhooks,
503
572
  getWebhookDetail,
504
573
  createWebhook,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.9.29",
3
+ "version": "0.9.31",
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
  }