@chrysb/alphaclaw 0.9.10 → 0.9.12

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.
@@ -5,6 +5,8 @@ const { getCommandOutputCandidates } = require("./utils/command-output");
5
5
 
6
6
  const kModelCatalogCacheVersion = 1;
7
7
  const kModelCatalogRefreshBackoffMs = 30 * 1000;
8
+ const kModelCatalogLoadTimeoutMs = 120 * 1000;
9
+ const kModelCatalogBootstrapSource = "bootstrap";
8
10
  const kDefaultCachePath = path.join(ALPHACLAW_DIR, "cache", "model-catalog.json");
9
11
 
10
12
  const createResponse = ({
@@ -77,6 +79,7 @@ const createModelCatalogCache = ({
77
79
  parseJsonFromNoisyOutput = () => ({}),
78
80
  normalizeOnboardingModels = (items) => items,
79
81
  readOpenclawVersion = () => null,
82
+ shouldStartDynamicRefresh = () => true,
80
83
  fallbackModels = kFallbackOnboardingModels,
81
84
  cachePath = kDefaultCachePath,
82
85
  refreshBackoffMs = kModelCatalogRefreshBackoffMs,
@@ -117,6 +120,14 @@ const createModelCatalogCache = ({
117
120
 
118
121
  const isRefreshPending = () => !!refreshPromise || !!retryTimer;
119
122
 
123
+ const canStartDynamicRefresh = () => {
124
+ try {
125
+ return shouldStartDynamicRefresh() !== false;
126
+ } catch {
127
+ return false;
128
+ }
129
+ };
130
+
120
131
  const setCacheEntry = (entry, { fresh = false } = {}) => {
121
132
  memoryCache = entry;
122
133
  cacheLoaded = true;
@@ -162,7 +173,7 @@ const createModelCatalogCache = ({
162
173
  try {
163
174
  const output = await shellCmd("openclaw models list --all --json", {
164
175
  env: gatewayEnv(),
165
- timeout: 30000,
176
+ timeout: kModelCatalogLoadTimeoutMs,
166
177
  });
167
178
  models = parseCatalogModelsFromOutput({
168
179
  rawOutput: output,
@@ -205,21 +216,27 @@ const createModelCatalogCache = ({
205
216
  };
206
217
 
207
218
  const scheduleRetry = () => {
208
- if (!memoryCache || retryTimer) return;
219
+ if (!canStartDynamicRefresh()) {
220
+ clearRetryTimer();
221
+ return;
222
+ }
223
+ if (retryTimer) return;
209
224
  const delayMs = Math.max(backoffUntilMs - now(), 0);
210
225
  retryTimer = setTimeoutFn(() => {
211
226
  retryTimer = null;
212
- if (!memoryCache || !cacheIsStale || refreshPromise) return;
227
+ if (!canStartDynamicRefresh()) return;
228
+ if (refreshPromise) return;
229
+ if (memoryCache && !cacheIsStale) return;
213
230
  void startBackgroundRefresh();
214
231
  }, delayMs);
215
232
  if (typeof retryTimer?.unref === "function") retryTimer.unref();
216
233
  };
217
234
 
218
235
  const handleRefreshFailure = (err) => {
236
+ backoffUntilMs = now() + refreshBackoffMs;
237
+ scheduleRetry();
219
238
  if (memoryCache) {
220
239
  cacheIsStale = true;
221
- backoffUntilMs = now() + refreshBackoffMs;
222
- scheduleRetry();
223
240
  logger.error?.(
224
241
  `[models] Failed to refresh cached models: ${err.message || String(err)}`,
225
242
  );
@@ -231,8 +248,11 @@ const createModelCatalogCache = ({
231
248
  };
232
249
 
233
250
  const startBackgroundRefresh = () => {
251
+ if (!canStartDynamicRefresh()) {
252
+ clearRetryTimer();
253
+ return null;
254
+ }
234
255
  readDiskCache();
235
- if (!memoryCache) return null;
236
256
  if (refreshPromise) return refreshPromise;
237
257
  if (retryTimer) return null;
238
258
  if (backoffUntilMs > now()) {
@@ -279,34 +299,25 @@ const createModelCatalogCache = ({
279
299
  });
280
300
  }
281
301
  if (memoryCache) {
282
- startBackgroundRefresh();
302
+ const didStartRefresh = !!startBackgroundRefresh();
283
303
  return createResponse({
284
304
  source: "cache",
285
305
  fetchedAt: memoryCache.fetchedAt,
286
306
  stale: true,
287
- refreshing: isRefreshPending(),
307
+ refreshing:
308
+ canStartDynamicRefresh() && (didStartRefresh || isRefreshPending()),
288
309
  models: memoryCache.models,
289
310
  });
290
311
  }
291
- try {
292
- const freshEntry = await loadFreshCatalog();
293
- return createResponse({
294
- source: "openclaw",
295
- fetchedAt: freshEntry.fetchedAt,
296
- stale: false,
297
- refreshing: false,
298
- models: freshEntry.models,
299
- });
300
- } catch (err) {
301
- handleRefreshFailure(err);
302
- return createResponse({
303
- source: "fallback",
304
- fetchedAt: null,
305
- stale: false,
306
- refreshing: false,
307
- models: fallbackModels,
308
- });
309
- }
312
+ const didStartRefresh = !!startBackgroundRefresh();
313
+ return createResponse({
314
+ source: kModelCatalogBootstrapSource,
315
+ fetchedAt: null,
316
+ stale: true,
317
+ refreshing:
318
+ canStartDynamicRefresh() && (didStartRefresh || isRefreshPending()),
319
+ models: fallbackModels,
320
+ });
310
321
  },
311
322
 
312
323
  markStale() {
@@ -326,5 +337,7 @@ module.exports = {
326
337
  normalizeCacheEntry,
327
338
  kModelCatalogCacheVersion,
328
339
  kModelCatalogRefreshBackoffMs,
340
+ kModelCatalogLoadTimeoutMs,
341
+ kModelCatalogBootstrapSource,
329
342
  kDefaultCachePath,
330
343
  };
@@ -1,5 +1,5 @@
1
1
  const path = require("path");
2
- const { kSetupDir, kRootDir } = require("../constants");
2
+ const { kSetupDir } = require("../constants");
3
3
  const {
4
4
  resolveConfigIncludes,
5
5
  resolveImportedConfigPaths,
@@ -491,14 +491,7 @@ const createOnboardingService = ({
491
491
  await shellCmd(
492
492
  `openclaw onboard ${onboardArgs.map((a) => `"${a}"`).join(" ")}`,
493
493
  {
494
- env: {
495
- ...process.env,
496
- HOME: kRootDir,
497
- OPENCLAW_HOME: kRootDir,
498
- OPENCLAW_CONFIG_PATH: `${OPENCLAW_DIR}/openclaw.json`,
499
- OPENCLAW_STATE_DIR: OPENCLAW_DIR,
500
- XDG_CONFIG_HOME: OPENCLAW_DIR,
501
- },
494
+ env: gatewayEnv(),
502
495
  timeout: 120000,
503
496
  },
504
497
  );
@@ -162,7 +162,10 @@ const getSafeImportedDmPolicy = (channelConfig = {}) => {
162
162
  return channelConfig?.dmPolicy || "pairing";
163
163
  };
164
164
 
165
- const applyFreshOnboardingChannels = ({ cfg, varMap }) => {
165
+ const applyFreshOnboardingChannels = ({
166
+ cfg,
167
+ varMap,
168
+ }) => {
166
169
  if (varMap.TELEGRAM_BOT_TOKEN) {
167
170
  cfg.channels.telegram = {
168
171
  enabled: true,
@@ -214,11 +217,18 @@ const applyFreshOnboardingChannels = ({ cfg, varMap }) => {
214
217
  ensureUsageTrackerPluginEntry(cfg);
215
218
  };
216
219
 
217
- const writeSanitizedOpenclawConfig = ({ fs, openclawDir, varMap }) => {
220
+ const writeSanitizedOpenclawConfig = ({
221
+ fs,
222
+ openclawDir,
223
+ varMap,
224
+ }) => {
218
225
  const configPath = `${openclawDir}/openclaw.json`;
219
226
  const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
220
227
  ensureManagedConfigShell(cfg);
221
- applyFreshOnboardingChannels({ cfg, varMap });
228
+ applyFreshOnboardingChannels({
229
+ cfg,
230
+ varMap,
231
+ });
222
232
 
223
233
  let content = JSON.stringify(cfg, null, 2);
224
234
  const replacements = buildSecretReplacements(varMap, process.env);
@@ -239,7 +249,11 @@ const writeSanitizedOpenclawConfig = ({ fs, openclawDir, varMap }) => {
239
249
  console.log("[onboard] Config sanitized");
240
250
  };
241
251
 
242
- const writeManagedImportOpenclawConfig = ({ fs, openclawDir, varMap }) => {
252
+ const writeManagedImportOpenclawConfig = ({
253
+ fs,
254
+ openclawDir,
255
+ varMap,
256
+ }) => {
243
257
  const configPath = `${openclawDir}/openclaw.json`;
244
258
  const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
245
259
  ensureManagedConfigShell(cfg);
@@ -0,0 +1,55 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { kRootDir } = require("./constants");
4
+
5
+ const kDefaultOpenclawCompileCacheDir = path.join(
6
+ kRootDir,
7
+ "cache",
8
+ "openclaw-compile-cache",
9
+ );
10
+
11
+ const normalizeEnvValue = (value) => String(value || "").trim();
12
+
13
+ const resolveOpenclawCompileCacheDir = (env = process.env) =>
14
+ normalizeEnvValue(env.NODE_COMPILE_CACHE) || kDefaultOpenclawCompileCacheDir;
15
+
16
+ const resolveOpenclawNoRespawn = (env = process.env) =>
17
+ normalizeEnvValue(env.OPENCLAW_NO_RESPAWN) || "1";
18
+
19
+ const withOpenclawStartupEnv = (env = process.env) => ({
20
+ ...env,
21
+ NODE_COMPILE_CACHE: resolveOpenclawCompileCacheDir(env),
22
+ OPENCLAW_NO_RESPAWN: resolveOpenclawNoRespawn(env),
23
+ });
24
+
25
+ const ensureOpenclawStartupEnv = ({
26
+ fsModule = fs,
27
+ env = process.env,
28
+ logger = console,
29
+ } = {}) => {
30
+ const nextEnv = withOpenclawStartupEnv(env);
31
+ try {
32
+ fsModule.mkdirSync(nextEnv.NODE_COMPILE_CACHE, { recursive: true });
33
+ } catch (err) {
34
+ logger?.warn?.(
35
+ `[alphaclaw] OpenClaw compile cache directory unavailable: ${err.message}`,
36
+ );
37
+ }
38
+
39
+ if (!normalizeEnvValue(env.NODE_COMPILE_CACHE)) {
40
+ env.NODE_COMPILE_CACHE = nextEnv.NODE_COMPILE_CACHE;
41
+ }
42
+ if (!normalizeEnvValue(env.OPENCLAW_NO_RESPAWN)) {
43
+ env.OPENCLAW_NO_RESPAWN = nextEnv.OPENCLAW_NO_RESPAWN;
44
+ }
45
+
46
+ return nextEnv;
47
+ };
48
+
49
+ module.exports = {
50
+ kDefaultOpenclawCompileCacheDir,
51
+ ensureOpenclawStartupEnv,
52
+ resolveOpenclawCompileCacheDir,
53
+ resolveOpenclawNoRespawn,
54
+ withOpenclawStartupEnv,
55
+ };
@@ -32,6 +32,7 @@ const registerModelRoutes = ({
32
32
  parseJsonFromNoisyOutput,
33
33
  normalizeOnboardingModels,
34
34
  readOpenclawVersion,
35
+ isOnboarded = () => true,
35
36
  authProfiles,
36
37
  readEnvFile,
37
38
  writeEnvFile,
@@ -42,6 +43,7 @@ const registerModelRoutes = ({
42
43
  parseJsonFromNoisyOutput,
43
44
  normalizeOnboardingModels,
44
45
  readOpenclawVersion,
46
+ shouldStartDynamicRefresh: isOnboarded,
45
47
  fallbackModels: kFallbackOnboardingModels,
46
48
  }),
47
49
  }) => {
@@ -7,6 +7,7 @@ const kUsageTrackerPluginPath = path.resolve(
7
7
  "plugin",
8
8
  "usage-tracker",
9
9
  );
10
+ const kConversationAccessHookPolicyKey = "allowConversationAccess";
10
11
 
11
12
  const ensurePluginsShell = (cfg = {}) => {
12
13
  if (!cfg.plugins || typeof cfg.plugins !== "object") cfg.plugins = {};
@@ -29,19 +30,43 @@ const ensurePluginAllowed = ({ cfg = {}, pluginKey = "" }) => {
29
30
  }
30
31
  };
31
32
 
33
+ const buildUsageTrackerHookPolicy = ({ existingHooks = {} } = {}) => {
34
+ const hooks = {};
35
+ if (typeof existingHooks.allowPromptInjection === "boolean") {
36
+ hooks.allowPromptInjection = existingHooks.allowPromptInjection;
37
+ }
38
+ hooks[kConversationAccessHookPolicyKey] = true;
39
+ return hooks;
40
+ };
41
+
32
42
  const ensureUsageTrackerPluginEntry = (cfg = {}) => {
33
43
  const before = JSON.stringify(cfg);
34
44
  ensurePluginAllowed({ cfg, pluginKey: "usage-tracker" });
35
45
  if (!cfg.plugins.load.paths.includes(kUsageTrackerPluginPath)) {
36
46
  cfg.plugins.load.paths.push(kUsageTrackerPluginPath);
37
47
  }
38
- cfg.plugins.entries["usage-tracker"] = {
39
- ...(cfg.plugins.entries["usage-tracker"] &&
48
+ const existingEntry =
49
+ cfg.plugins.entries["usage-tracker"] &&
40
50
  typeof cfg.plugins.entries["usage-tracker"] === "object"
41
51
  ? cfg.plugins.entries["usage-tracker"]
42
- : {}),
52
+ : {};
53
+ const existingHooks =
54
+ existingEntry.hooks && typeof existingEntry.hooks === "object"
55
+ ? existingEntry.hooks
56
+ : {};
57
+ const hooks = buildUsageTrackerHookPolicy({
58
+ existingHooks,
59
+ });
60
+ const nextEntry = {
61
+ ...existingEntry,
43
62
  enabled: true,
44
63
  };
64
+ if (Object.keys(hooks).length > 0) {
65
+ nextEntry.hooks = hooks;
66
+ } else {
67
+ delete nextEntry.hooks;
68
+ }
69
+ cfg.plugins.entries["usage-tracker"] = nextEntry;
45
70
  return JSON.stringify(cfg) !== before;
46
71
  };
47
72
 
package/lib/server.js CHANGED
@@ -141,12 +141,16 @@ const {
141
141
  const {
142
142
  ensureManagedExecDefaults,
143
143
  } = require("./server/exec-defaults-config");
144
+ const {
145
+ ensureOpenclawStartupEnv,
146
+ } = require("./server/openclaw-runtime-env");
144
147
 
145
148
  const { PORT, kTrustProxyHops, SETUP_API_PREFIXES } = constants;
146
149
 
147
150
  initializeServerRuntime({
148
151
  fs,
149
152
  constants,
153
+ ensureOpenclawStartupEnv,
150
154
  startEnvWatcher,
151
155
  attachGatewaySignalHandlers,
152
156
  cleanupStaleImportTempDirs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.9.10",
3
+ "version": "0.9.12",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -19,14 +19,11 @@
19
19
  },
20
20
  "files": [
21
21
  "bin/",
22
- "lib/",
23
- "patches/",
24
- "scripts/apply-openclaw-patches.js"
22
+ "lib/"
25
23
  ],
26
24
  "scripts": {
27
25
  "start": "node bin/alphaclaw.js start",
28
26
  "build:ui": "node scripts/build-ui.mjs",
29
- "postinstall": "node ./scripts/apply-openclaw-patches.js",
30
27
  "test": "vitest run",
31
28
  "test:watch": "vitest",
32
29
  "test:watchdog": "vitest run tests/server/watchdog.test.js tests/server/watchdog-db.test.js tests/server/routes-watchdog.test.js",
@@ -36,8 +33,7 @@
36
33
  "dependencies": {
37
34
  "express": "^4.21.0",
38
35
  "http-proxy": "^1.18.1",
39
- "openclaw": "2026.4.21",
40
- "patch-package": "^8.0.1",
36
+ "openclaw": "2026.4.24",
41
37
  "ws": "^8.19.0"
42
38
  },
43
39
  "devDependencies": {
@@ -1,13 +0,0 @@
1
- diff --git a/node_modules/openclaw/dist/server.impl-DLF59fRo.js b/node_modules/openclaw/dist/server.impl-DLF59fRo.js
2
- index d6b4a5c..4e76dd9 100644
3
- --- a/node_modules/openclaw/dist/server.impl-DLF59fRo.js
4
- +++ b/node_modules/openclaw/dist/server.impl-DLF59fRo.js
5
- @@ -22187,7 +22187,7 @@ function attachGatewayWsMessageHandler(params) {
6
- close(1008, truncateCloseReason(authMessage));
7
- };
8
- const clearUnboundScopes = () => {
9
- - if (scopes.length > 0) {
10
- + if (scopes.length > 0 && !sharedAuthOk) {
11
- scopes = [];
12
- connectParams.scopes = scopes;
13
- }
@@ -1,99 +0,0 @@
1
- /**
2
- * patch-package resolves paths relative to the npm/yarn project root (where the
3
- * lockfile lives). When this package's postinstall runs, process.cwd() is often
4
- * this package directory, so a plain `patch-package` call treats that as the
5
- * app root and looks for ./node_modules/openclaw under it — but openclaw is
6
- * usually hoisted to the consumer's top-level node_modules.
7
- *
8
- * This script finds the real install root (directory containing a lockfile) and
9
- * runs patch-package there with --patch-dir pointing at our bundled patches/.
10
- */
11
- const { spawnSync } = require("child_process");
12
- const fs = require("fs");
13
- const path = require("path");
14
-
15
- const kAlphaclawRoot = path.join(__dirname, "..");
16
-
17
- const findProjectRootFromOpenclawDir = (openclawDir) => {
18
- let dir = path.resolve(openclawDir);
19
- for (let i = 0; i < 30; i += 1) {
20
- if (
21
- fs.existsSync(path.join(dir, "package-lock.json")) ||
22
- fs.existsSync(path.join(dir, "yarn.lock")) ||
23
- fs.existsSync(path.join(dir, "pnpm-lock.yaml"))
24
- ) {
25
- return dir;
26
- }
27
- const parent = path.dirname(dir);
28
- if (parent === dir) break;
29
- dir = parent;
30
- }
31
- return path.dirname(path.dirname(openclawDir));
32
- };
33
-
34
- const main = () => {
35
- const patchesDir = path.join(kAlphaclawRoot, "patches");
36
- if (!fs.existsSync(patchesDir)) {
37
- return;
38
- }
39
- const hasPatch = fs
40
- .readdirSync(patchesDir)
41
- .some((name) => name.endsWith(".patch"));
42
- if (!hasPatch) {
43
- return;
44
- }
45
-
46
- let openclawMainPath;
47
- try {
48
- openclawMainPath = require.resolve("openclaw", { paths: [kAlphaclawRoot] });
49
- } catch {
50
- return;
51
- }
52
-
53
- const openclawDir = (() => {
54
- let dir = path.dirname(openclawMainPath);
55
- for (let i = 0; i < 8; i += 1) {
56
- const pkgPath = path.join(dir, "package.json");
57
- if (fs.existsSync(pkgPath)) {
58
- try {
59
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
60
- if (pkg.name === "openclaw") return dir;
61
- } catch {
62
- /* continue */
63
- }
64
- }
65
- const parent = path.dirname(dir);
66
- if (parent === dir) break;
67
- dir = parent;
68
- }
69
- return path.dirname(path.dirname(openclawMainPath));
70
- })();
71
- const projectRoot = findProjectRootFromOpenclawDir(openclawDir);
72
-
73
- let relPatchDir = path.relative(projectRoot, patchesDir);
74
- if (relPatchDir.startsWith("..") || path.isAbsolute(relPatchDir)) {
75
- console.error(
76
- "[@chrysb/alphaclaw] patch-package: could not resolve patch dir relative to project root",
77
- );
78
- process.exit(1);
79
- }
80
- relPatchDir = relPatchDir.split(path.sep).join("/");
81
-
82
- const patchPackageMain = require.resolve("patch-package/dist/index.js", {
83
- paths: [kAlphaclawRoot],
84
- });
85
-
86
- const result = spawnSync(
87
- process.execPath,
88
- [patchPackageMain, "--patch-dir", relPatchDir],
89
- { cwd: projectRoot, stdio: "inherit", env: process.env },
90
- );
91
- if (result.error) {
92
- throw result.error;
93
- }
94
- if (result.status !== 0 && result.status !== null) {
95
- process.exit(result.status);
96
- }
97
- };
98
-
99
- main();