@chrysb/alphaclaw 0.9.18 → 0.9.19

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/README.md CHANGED
@@ -157,6 +157,9 @@ The built-in watchdog monitors gateway health and recovers from failures automat
157
157
  | `WATCHDOG_NOTIFICATIONS_DISABLED` | Optional | Disable watchdog notifications (`true`/`false`) |
158
158
  | `PORT` | Optional | Server port (default `3000`) |
159
159
  | `ALPHACLAW_ROOT_DIR` | Optional | Data directory (default `/data`) |
160
+ | `ALPHACLAW_SKIP_SYSTEM_CRON_INSTALL` | Optional | Skip writes to `/etc/cron.d` while keeping cron config (`true`/`false`); the managed hourly script still exits when sync is disabled |
161
+ | `ALPHACLAW_GIT_SHIM_PATH` | Optional | Install the managed git auth shim at this path and prepend its directory to runtime `PATH` (default `/usr/local/bin/git`) |
162
+ | `ALPHACLAW_GIT_ASKPASS_PATH` | Optional | Install the git askpass helper at this path (default `$TMPDIR/alphaclaw-git-askpass.sh`) |
160
163
  | `TRUST_PROXY_HOPS` | Optional | Trust proxy hop count for correct client IP |
161
164
  | `REMOTE_MCP_URL` | Optional | Upstream remote MCP server URL. When set together with `REMOTE_MCP_API_TOKEN`, AlphaClaw writes a managed `mcp.servers.<name>` entry to `openclaw.json` on every gateway start. |
162
165
  | `REMOTE_MCP_API_TOKEN` | Optional | Bearer token for the remote MCP server. Persisted in `openclaw.json` as the `${REMOTE_MCP_API_TOKEN}` reference, never as plaintext. |
package/bin/alphaclaw.js CHANGED
@@ -6,6 +6,10 @@ const os = require("os");
6
6
  const path = require("path");
7
7
  const { execSync } = require("child_process");
8
8
  const {
9
+ shouldSkipSystemCronInstall,
10
+ resolveGitAskPassPath,
11
+ resolveGitShimPath,
12
+ prependGitShimDirToPath,
9
13
  normalizeGitSyncFilePath,
10
14
  validateGitSyncFilePath,
11
15
  resolveRealGitPath,
@@ -16,6 +20,9 @@ const {
16
20
  restoreMissingOpenclawConfigFromRemote,
17
21
  } = require("../lib/cli/openclaw-config-restore");
18
22
  const { buildSecretReplacements } = require("../lib/server/helpers");
23
+ const {
24
+ migrateLegacyTelegramStreamingConfig,
25
+ } = require("../lib/server/openclaw-config-migrations");
19
26
  const {
20
27
  migrateManagedInternalFiles,
21
28
  } = require("../lib/server/internal-files-migration");
@@ -278,7 +285,7 @@ const runGitSync = () => {
278
285
  }
279
286
 
280
287
  const realGitPath = resolveRealGitPath({
281
- shimPath: "/usr/local/bin/git",
288
+ shimPath: resolveGitShimPath(),
282
289
  });
283
290
  if (!realGitPath) {
284
291
  console.error(
@@ -631,7 +638,11 @@ if (fs.existsSync(hourlyGitSyncPath)) {
631
638
  }
632
639
 
633
640
  const cronFilePath = "/etc/cron.d/openclaw-hourly-sync";
634
- if (cronEnabled) {
641
+ if (shouldSkipSystemCronInstall()) {
642
+ console.log(
643
+ "[alphaclaw] System cron setup skipped by ALPHACLAW_SKIP_SYSTEM_CRON_INSTALL",
644
+ );
645
+ } else if (cronEnabled) {
635
646
  const cronContent = [
636
647
  "SHELL=/bin/bash",
637
648
  "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
@@ -732,7 +743,10 @@ if (fs.existsSync(configPath)) {
732
743
  if (!cfg.plugins.load) cfg.plugins.load = {};
733
744
  if (!Array.isArray(cfg.plugins.load.paths)) cfg.plugins.load.paths = [];
734
745
  if (!cfg.plugins.entries) cfg.plugins.entries = {};
735
- let changed = false;
746
+ let changed = migrateLegacyTelegramStreamingConfig(cfg);
747
+ if (changed) {
748
+ console.log("[alphaclaw] Migrated legacy Telegram streaming config");
749
+ }
736
750
 
737
751
  if (process.env.TELEGRAM_BOT_TOKEN && !cfg.channels.telegram) {
738
752
  cfg.channels.telegram = {
@@ -819,11 +833,17 @@ try {
819
833
 
820
834
  try {
821
835
  const gitAskPassSrc = path.join(__dirname, "..", "lib", "scripts", "git-askpass");
822
- const gitAskPassDest = "/tmp/alphaclaw-git-askpass.sh";
836
+ const gitAskPassDest = resolveGitAskPassPath({
837
+ tmpDir: os.tmpdir(),
838
+ });
823
839
  const gitShimTemplatePath = path.join(__dirname, "..", "lib", "scripts", "git");
824
- const gitShimDest = "/usr/local/bin/git";
840
+ const gitShimDest = resolveGitShimPath();
841
+ process.env.PATH = prependGitShimDirToPath({
842
+ shimPath: gitShimDest,
843
+ });
825
844
 
826
845
  if (fs.existsSync(gitAskPassSrc)) {
846
+ fs.mkdirSync(path.dirname(gitAskPassDest), { recursive: true });
827
847
  fs.copyFileSync(gitAskPassSrc, gitAskPassDest);
828
848
  fs.chmodSync(gitAskPassDest, 0o755);
829
849
  }
@@ -837,7 +857,9 @@ try {
837
857
  const gitShimTemplate = fs.readFileSync(gitShimTemplatePath, "utf8");
838
858
  const gitShimContent = gitShimTemplate
839
859
  .replace("@@REAL_GIT@@", realGitPath)
840
- .replace("@@OPENCLAW_REPO_ROOT@@", openclawDir);
860
+ .replace("@@OPENCLAW_REPO_ROOT@@", openclawDir)
861
+ .replace("@@ASKPASS_PATH@@", gitAskPassDest);
862
+ fs.mkdirSync(path.dirname(gitShimDest), { recursive: true });
841
863
  fs.writeFileSync(gitShimDest, gitShimContent, { mode: 0o755 });
842
864
  console.log("[alphaclaw] git auth shim installed");
843
865
  }
@@ -2,6 +2,43 @@ const fs = require("fs");
2
2
  const path = require("path");
3
3
  const { execSync } = require("child_process");
4
4
 
5
+ const isEnabledEnvFlag = (value) =>
6
+ ["1", "true", "yes", "on"].includes(
7
+ String(value || "").trim().toLowerCase(),
8
+ );
9
+
10
+ const shouldSkipSystemCronInstall = ({ env = process.env } = {}) =>
11
+ isEnabledEnvFlag(env.ALPHACLAW_SKIP_SYSTEM_CRON_INSTALL);
12
+
13
+ const resolveGitAskPassPath = ({ env = process.env, tmpDir = "/tmp" } = {}) => {
14
+ const explicitPath = String(env.ALPHACLAW_GIT_ASKPASS_PATH || "").trim();
15
+ if (explicitPath) return explicitPath;
16
+
17
+ const runtimeTmpDir =
18
+ String(env.TMPDIR || "").trim() || String(tmpDir || "").trim() || "/tmp";
19
+ return path.join(runtimeTmpDir, "alphaclaw-git-askpass.sh");
20
+ };
21
+
22
+ const resolveGitShimPath = ({ env = process.env } = {}) => {
23
+ const explicitPath = String(env.ALPHACLAW_GIT_SHIM_PATH || "").trim();
24
+ return explicitPath || "/usr/local/bin/git";
25
+ };
26
+
27
+ const prependGitShimDirToPath = ({
28
+ env = process.env,
29
+ shimPath = resolveGitShimPath({ env }),
30
+ } = {}) => {
31
+ if (!String(env.ALPHACLAW_GIT_SHIM_PATH || "").trim()) {
32
+ return env.PATH || process.env.PATH || "";
33
+ }
34
+ const shimDir = path.dirname(String(shimPath || "").trim());
35
+ if (!shimDir || shimDir === ".") return env.PATH || process.env.PATH || "";
36
+ const pathEntries = String(env.PATH || process.env.PATH || "")
37
+ .split(path.delimiter)
38
+ .filter((entry) => entry && entry !== shimDir);
39
+ return [shimDir, ...pathEntries].join(path.delimiter);
40
+ };
41
+
5
42
  const normalizeGitSyncFilePath = (requestedFilePath) => {
6
43
  const rawPath = String(requestedFilePath || "").trim();
7
44
  if (!rawPath) return "";
@@ -90,6 +127,10 @@ const shouldRefreshHourlyGitSyncScript = ({
90
127
  };
91
128
 
92
129
  module.exports = {
130
+ shouldSkipSystemCronInstall,
131
+ resolveGitAskPassPath,
132
+ resolveGitShimPath,
133
+ prependGitShimDirToPath,
93
134
  normalizeGitSyncFilePath,
94
135
  validateGitSyncFilePath,
95
136
  resolveRealGitPath,
@@ -27,7 +27,7 @@ const resolveCurrentBranch = ({ execSyncImpl, openclawDir }) => {
27
27
 
28
28
  const createGitEnv = ({ fsModule, osModule, env, processId }) => {
29
29
  const githubToken = String(env.GITHUB_TOKEN || "").trim();
30
- const gitEnv = { ...env };
30
+ const gitEnv = { ...env, PATH: env.PATH || process.env.PATH };
31
31
  if (!githubToken) {
32
32
  return { gitEnv, askPassPath: "" };
33
33
  }
package/lib/scripts/git CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  REAL_GIT_HINT="@@REAL_GIT@@"
6
6
  OPENCLAW_REPO_ROOT="@@OPENCLAW_REPO_ROOT@@"
7
- ASKPASS_PATH="/tmp/alphaclaw-git-askpass.sh"
7
+ ASKPASS_PATH="@@ASKPASS_PATH@@"
8
8
 
9
9
  same_path() {
10
10
  local left_path right_path
@@ -505,12 +505,16 @@ const readPairedCountsByAccount = ({
505
505
  } catch {}
506
506
 
507
507
  for (const accountId of counts.keys()) {
508
- if (String(channelId || "").trim() === "whatsapp") continue;
509
508
  const accountConfig =
510
509
  accountId === "default" &&
511
510
  !(config.accounts && typeof config.accounts === "object")
512
511
  ? config
513
512
  : config.accounts?.[accountId] || {};
513
+ if (String(channelId || "").trim() === "whatsapp") {
514
+ if (!hasSavedWhatsAppCredentials({ fsImpl, OPENCLAW_DIR, accountId })) {
515
+ continue;
516
+ }
517
+ }
514
518
  const inlineAllowFrom = accountConfig?.allowFrom;
515
519
  if (!Array.isArray(inlineAllowFrom)) continue;
516
520
  counts.set(
@@ -616,7 +620,15 @@ const listConfiguredChannelAccounts = ({ fsImpl, OPENCLAW_DIR, cfg }) => {
616
620
  status:
617
621
  Number(pairedCounts.get(accountId) || 0) > 0
618
622
  ? "paired"
619
- : "configured",
623
+ : hasImplicitWhatsAppSelfPairing({
624
+ fsImpl,
625
+ OPENCLAW_DIR,
626
+ channelId,
627
+ accountId,
628
+ accountConfig,
629
+ })
630
+ ? "paired"
631
+ : "configured",
620
632
  };
621
633
  }),
622
634
  };
@@ -764,6 +776,10 @@ module.exports = {
764
776
  resolveCredentialsDirPath,
765
777
  resolveWhatsAppCredentialCandidatePaths,
766
778
  hasSavedWhatsAppCredentials,
779
+ normalizeChannelAccountId,
780
+ resolveCredentialPairingAccountId,
781
+ hasImplicitWhatsAppSelfPairing,
782
+ readPairedCountsByAccount,
767
783
  resolveAgentWorkspacePath,
768
784
  loadConfig,
769
785
  saveConfig,
package/lib/server/env.js CHANGED
@@ -1,21 +1,48 @@
1
1
  const fs = require("fs");
2
2
  const { ENV_FILE_PATH, kKnownVars } = require("./constants");
3
3
 
4
+ const kSensitiveEnvKeyPattern = /token|key|password/i;
5
+ const kEnvWatchDebounceMs = 250;
6
+ let envWatchDebounceTimer = null;
7
+ let lastLoadedEnvSignature = null;
8
+ let pendingSelfWriteSignature = null;
9
+
10
+ const normalizeEnvVars = (vars) => {
11
+ const byKey = new Map();
12
+ for (const entry of vars || []) {
13
+ const key = String(entry?.key || "").trim();
14
+ if (!key) continue;
15
+ if (byKey.has(key)) byKey.delete(key);
16
+ byKey.set(key, {
17
+ key,
18
+ value: String(entry?.value || ""),
19
+ });
20
+ }
21
+ return Array.from(byKey.values());
22
+ };
23
+
24
+ const buildEnvSignature = (vars) =>
25
+ JSON.stringify(normalizeEnvVars(vars).map(({ key, value }) => [key, value]));
26
+
27
+ const readRawEnvFile = () => {
28
+ const content = fs.readFileSync(ENV_FILE_PATH, "utf8");
29
+ const vars = [];
30
+ for (const line of content.split("\n")) {
31
+ const trimmed = line.trim();
32
+ if (!trimmed || trimmed.startsWith("#")) continue;
33
+ const eqIdx = trimmed.indexOf("=");
34
+ if (eqIdx === -1) continue;
35
+ vars.push({
36
+ key: trimmed.slice(0, eqIdx).trim(),
37
+ value: trimmed.slice(eqIdx + 1),
38
+ });
39
+ }
40
+ return vars;
41
+ };
42
+
4
43
  const readEnvFile = () => {
5
44
  try {
6
- const content = fs.readFileSync(ENV_FILE_PATH, "utf8");
7
- const vars = [];
8
- for (const line of content.split("\n")) {
9
- const trimmed = line.trim();
10
- if (!trimmed || trimmed.startsWith("#")) continue;
11
- const eqIdx = trimmed.indexOf("=");
12
- if (eqIdx === -1) continue;
13
- vars.push({
14
- key: trimmed.slice(0, eqIdx),
15
- value: trimmed.slice(eqIdx + 1),
16
- });
17
- }
18
- return vars;
45
+ return normalizeEnvVars(readRawEnvFile());
19
46
  } catch {
20
47
  return [];
21
48
  }
@@ -23,22 +50,24 @@ const readEnvFile = () => {
23
50
 
24
51
  const writeEnvFile = (vars) => {
25
52
  const lines = [];
26
- for (const { key, value } of vars || []) {
27
- if (!key) continue;
53
+ const normalizedVars = normalizeEnvVars(vars);
54
+ for (const { key, value } of normalizedVars) {
28
55
  lines.push(`${key}=${String(value || "")}`);
29
56
  }
30
57
  fs.writeFileSync(ENV_FILE_PATH, lines.join("\n"));
58
+ pendingSelfWriteSignature = buildEnvSignature(normalizedVars);
31
59
  };
32
60
 
33
61
  const reloadEnv = () => {
34
62
  const vars = readEnvFile();
63
+ const signature = buildEnvSignature(vars);
35
64
  const fileKeys = new Set(vars.map((v) => v.key));
36
65
  let changed = false;
37
66
 
38
67
  for (const { key, value } of vars) {
39
68
  if (value && value !== process.env[key]) {
40
69
  console.log(
41
- `[alphaclaw] Env updated: ${key}=${key.toLowerCase().includes("token") || key.toLowerCase().includes("key") || key.toLowerCase().includes("password") ? "***" : value}`,
70
+ `[alphaclaw] Env updated: ${key}=${kSensitiveEnvKeyPattern.test(key) ? "***" : value}`,
42
71
  );
43
72
  process.env[key] = value;
44
73
  changed = true;
@@ -58,21 +87,43 @@ const reloadEnv = () => {
58
87
  }
59
88
  }
60
89
 
90
+ lastLoadedEnvSignature = signature;
61
91
  return changed;
62
92
  };
63
93
 
94
+ const readEnvFileSignature = () => {
95
+ try {
96
+ return buildEnvSignature(readRawEnvFile());
97
+ } catch {
98
+ return null;
99
+ }
100
+ };
101
+
64
102
  const startEnvWatcher = () => {
65
103
  try {
66
104
  fs.watchFile(ENV_FILE_PATH, { interval: 2000 }, () => {
67
- console.log(
68
- `[alphaclaw] ${ENV_FILE_PATH} changed externally, reloading...`,
69
- );
70
- reloadEnv();
105
+ if (envWatchDebounceTimer) clearTimeout(envWatchDebounceTimer);
106
+ envWatchDebounceTimer = setTimeout(() => {
107
+ envWatchDebounceTimer = null;
108
+ const signature = readEnvFileSignature();
109
+ if (signature && signature === pendingSelfWriteSignature) {
110
+ pendingSelfWriteSignature = null;
111
+ lastLoadedEnvSignature = signature;
112
+ return;
113
+ }
114
+ pendingSelfWriteSignature = null;
115
+ if (signature && signature === lastLoadedEnvSignature) return;
116
+ console.log(
117
+ `[alphaclaw] ${ENV_FILE_PATH} changed externally, reloading...`,
118
+ );
119
+ reloadEnv();
120
+ }, kEnvWatchDebounceMs);
71
121
  });
72
122
  } catch {}
73
123
  };
74
124
 
75
125
  module.exports = {
126
+ normalizeEnvVars,
76
127
  readEnvFile,
77
128
  writeEnvFile,
78
129
  reloadEnv,
@@ -11,6 +11,10 @@ const {
11
11
  kOnboardingMarkerPath,
12
12
  kRootDir,
13
13
  } = require("./constants");
14
+ const {
15
+ normalizeChannelAccountId,
16
+ readPairedCountsByAccount,
17
+ } = require("./agents/shared");
14
18
  const { withOpenclawStartupEnv } = require("./openclaw-runtime-env");
15
19
  const { isOpenAiCompatApiEnabled } = require("./alphaclaw-config");
16
20
 
@@ -203,18 +207,6 @@ const getGatewayPort = () => {
203
207
 
204
208
  const getGatewayUrl = () => `http://${GATEWAY_HOST}:${getGatewayPort()}`;
205
209
 
206
- const normalizeChannelAccountId = (value) => String(value || "").trim() || "default";
207
-
208
- const resolveCredentialPairingAccountId = ({ channel, fileName }) => {
209
- const prefix = `${String(channel || "").trim()}-`;
210
- const suffix = "-allowFrom.json";
211
- if (!String(fileName || "").startsWith(prefix) || !String(fileName || "").endsWith(suffix)) {
212
- return "";
213
- }
214
- const rawAccountId = String(fileName || "").slice(prefix.length, -suffix.length);
215
- return normalizeChannelAccountId(rawAccountId);
216
- };
217
-
218
210
  const isGatewayRunning = () =>
219
211
  new Promise((resolve) => {
220
212
  const sock = net.createConnection(getGatewayPort(), GATEWAY_HOST);
@@ -781,32 +773,6 @@ const getChannelStatus = () => {
781
773
  );
782
774
  const credDir = `${OPENCLAW_DIR}/credentials`;
783
775
  const channels = {};
784
- const hasImplicitWhatsAppSelfPairing = ({ accountId, accountConfig }) => {
785
- if (!accountConfig || typeof accountConfig !== "object") return false;
786
- if (accountConfig.selfChatMode === false) return false;
787
- if (String(accountConfig.dmPolicy || "").trim().toLowerCase() === "disabled") {
788
- return false;
789
- }
790
- const candidatePaths = [
791
- `${credDir}/whatsapp/${accountId}/creds.json`,
792
- ...(accountId === "default" ? [`${credDir}/creds.json`] : []),
793
- ];
794
- const matches = candidatePaths.map((targetPath) => {
795
- try {
796
- return {
797
- path: targetPath,
798
- exists: !!String(fs.readFileSync(targetPath, "utf8") || "").trim(),
799
- };
800
- } catch (error) {
801
- return {
802
- path: targetPath,
803
- exists: false,
804
- error: String(error?.message || error || "read failed"),
805
- };
806
- }
807
- });
808
- return matches.some((entry) => entry.exists);
809
- };
810
776
 
811
777
  for (const ch of Object.keys(kChannelDefs)) {
812
778
  const channelConfig =
@@ -836,55 +802,14 @@ const getChannelStatus = () => {
836
802
  });
837
803
  if (!hasConfiguredToken) continue;
838
804
 
839
- const pairedByAccount = new Map(
840
- Array.from(configuredAccountIds).map((accountId) => [accountId, 0]),
841
- );
842
- try {
843
- if (ch !== "whatsapp") {
844
- const files = fs
845
- .readdirSync(credDir)
846
- .filter(
847
- (f) => f.startsWith(`${ch}-`) && f.endsWith("-allowFrom.json"),
848
- );
849
- for (const file of files) {
850
- const accountId = resolveCredentialPairingAccountId({
851
- channel: ch,
852
- fileName: file,
853
- });
854
- if (!accountId || !configuredAccountIds.has(accountId)) continue;
855
- const data = JSON.parse(
856
- fs.readFileSync(`${credDir}/${file}`, "utf8"),
857
- );
858
- const nextCount =
859
- Number(pairedByAccount.get(accountId) || 0)
860
- + (Array.isArray(data.allowFrom) ? data.allowFrom.length : 0);
861
- pairedByAccount.set(accountId, nextCount);
862
- }
863
- }
864
- } catch {}
865
- for (const [accountId, accountConfig] of accountEntries) {
866
- if (ch === "whatsapp") continue;
867
- const inlineAllowFrom = accountConfig?.allowFrom;
868
- if (!Array.isArray(inlineAllowFrom)) continue;
869
- const normalizedAccountId = normalizeChannelAccountId(accountId);
870
- const nextCount =
871
- Number(pairedByAccount.get(normalizedAccountId) || 0) + inlineAllowFrom.length;
872
- pairedByAccount.set(normalizedAccountId, nextCount);
873
- }
874
- if (ch === "whatsapp") {
875
- for (const [accountId, accountConfig] of accountEntries) {
876
- const normalizedAccountId = normalizeChannelAccountId(accountId);
877
- if (Number(pairedByAccount.get(normalizedAccountId) || 0) > 0) continue;
878
- if (
879
- hasImplicitWhatsAppSelfPairing({
880
- accountId: normalizedAccountId,
881
- accountConfig,
882
- })
883
- ) {
884
- pairedByAccount.set(normalizedAccountId, 1);
885
- }
886
- }
887
- }
805
+ const pairedByAccount = readPairedCountsByAccount({
806
+ fsImpl: fs,
807
+ OPENCLAW_DIR,
808
+ channelId: ch,
809
+ accountIds: Array.from(configuredAccountIds),
810
+ config: channelConfig,
811
+ });
812
+
888
813
  const accounts = Object.fromEntries(
889
814
  Array.from(pairedByAccount.entries()).map(([accountId, paired]) => [
890
815
  accountId,
@@ -1,6 +1,7 @@
1
1
  const path = require("path");
2
2
  const { kSetupDir } = require("../constants");
3
3
  const { buildManagedPaths } = require("../internal-files-migration");
4
+ const { shouldSkipSystemCronInstall } = require("../../cli/git-runtime");
4
5
 
5
6
  const kHourlyGitSyncTemplatePath = path.join(kSetupDir, "hourly-git-sync.sh");
6
7
  const kSystemCronPath = "/etc/cron.d/openclaw-hourly-sync";
@@ -37,6 +38,13 @@ const installHourlyGitSyncCron = async ({ fs, openclawDir }) => {
37
38
  fs.mkdirSync(configDir, { recursive: true });
38
39
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
39
40
 
41
+ if (shouldSkipSystemCronInstall()) {
42
+ console.log(
43
+ "[onboard] System cron install skipped by ALPHACLAW_SKIP_SYSTEM_CRON_INSTALL",
44
+ );
45
+ return false;
46
+ }
47
+
40
48
  const cronContent = buildSystemCronFile({
41
49
  schedule: config.schedule,
42
50
  scriptPath: hourlyGitSyncPath,
@@ -0,0 +1,77 @@
1
+ const isRecord = (value) =>
2
+ !!value && typeof value === "object" && !Array.isArray(value);
3
+
4
+ const parseStreamingMode = (value) => {
5
+ if (typeof value === "boolean") return value ? "partial" : "off";
6
+ const normalized = String(value || "").trim().toLowerCase();
7
+ return ["off", "partial", "block", "progress"].includes(normalized)
8
+ ? normalized
9
+ : null;
10
+ };
11
+
12
+ const migrateLegacyTelegramStreamingEntry = (entry) => {
13
+ if (!isRecord(entry)) return false;
14
+
15
+ const legacyStreaming = entry.streaming;
16
+ const hasLegacyStreaming =
17
+ typeof legacyStreaming === "boolean" || typeof legacyStreaming === "string";
18
+ const hasLegacyFields =
19
+ entry.streamMode !== undefined ||
20
+ entry.chunkMode !== undefined ||
21
+ entry.blockStreaming !== undefined ||
22
+ entry.blockStreamingCoalesce !== undefined ||
23
+ entry.draftChunk !== undefined;
24
+ if (!hasLegacyStreaming && !hasLegacyFields) return false;
25
+
26
+ const streaming = isRecord(legacyStreaming) ? { ...legacyStreaming } : {};
27
+ const resolvedMode =
28
+ parseStreamingMode(isRecord(legacyStreaming) ? legacyStreaming.mode : legacyStreaming) ||
29
+ parseStreamingMode(entry.streamMode) ||
30
+ "partial";
31
+ if (streaming.mode === undefined) streaming.mode = resolvedMode;
32
+
33
+ if (entry.chunkMode !== undefined && streaming.chunkMode === undefined) {
34
+ streaming.chunkMode = entry.chunkMode;
35
+ }
36
+ if (entry.draftChunk !== undefined) {
37
+ const preview = isRecord(streaming.preview) ? { ...streaming.preview } : {};
38
+ if (preview.chunk === undefined) preview.chunk = entry.draftChunk;
39
+ streaming.preview = preview;
40
+ }
41
+ if (entry.blockStreaming !== undefined || entry.blockStreamingCoalesce !== undefined) {
42
+ const block = isRecord(streaming.block) ? { ...streaming.block } : {};
43
+ if (entry.blockStreaming !== undefined && block.enabled === undefined) {
44
+ block.enabled = entry.blockStreaming;
45
+ }
46
+ if (entry.blockStreamingCoalesce !== undefined && block.coalesce === undefined) {
47
+ block.coalesce = entry.blockStreamingCoalesce;
48
+ }
49
+ streaming.block = block;
50
+ }
51
+
52
+ entry.streaming = streaming;
53
+ delete entry.streamMode;
54
+ delete entry.chunkMode;
55
+ delete entry.blockStreaming;
56
+ delete entry.blockStreamingCoalesce;
57
+ delete entry.draftChunk;
58
+ return true;
59
+ };
60
+
61
+ const migrateLegacyTelegramStreamingConfig = (cfg = {}) => {
62
+ const telegram = cfg.channels?.telegram;
63
+ if (!isRecord(telegram)) return false;
64
+
65
+ let changed = migrateLegacyTelegramStreamingEntry(telegram);
66
+ if (isRecord(telegram.accounts)) {
67
+ for (const account of Object.values(telegram.accounts)) {
68
+ if (migrateLegacyTelegramStreamingEntry(account)) changed = true;
69
+ }
70
+ }
71
+ return changed;
72
+ };
73
+
74
+ module.exports = {
75
+ migrateLegacyTelegramStreamingConfig,
76
+ migrateLegacyTelegramStreamingEntry,
77
+ };
@@ -27,6 +27,31 @@ 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;
40
+ }
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;
53
+ };
54
+
30
55
  const splitModelKey = (modelKey = "") => {
31
56
  const normalized = String(modelKey || "").trim();
32
57
  const slashIndex = normalized.indexOf("/");
@@ -55,7 +80,7 @@ const resolveThinkingApi = async () => {
55
80
  return {
56
81
  listThinkingLevelOptions: mod.listThinkingLevelOptions || mod.i,
57
82
  resolveThinkingDefaultForModel: mod.resolveThinkingDefaultForModel || mod.s,
58
- normalizeThinkLevel: mod.normalizeThinkLevel || mod.p,
83
+ normalizeThinkLevel: resolveNormalizeThinkLevel(mod),
59
84
  };
60
85
  };
61
86
 
@@ -1,5 +1,6 @@
1
1
  const { buildManagedPaths } = require("../internal-files-migration");
2
2
  const { readOpenclawConfig } = require("../openclaw-config");
3
+ const { shouldSkipSystemCronInstall } = require("../../cli/git-runtime");
3
4
  const {
4
5
  readAlphaclawConfig,
5
6
  updateOpenAiCompatApiFeature,
@@ -388,6 +389,9 @@ const registerSystemRoutes = ({
388
389
  kSystemCronConfigPath,
389
390
  JSON.stringify(nextConfig, null, 2),
390
391
  );
392
+ if (shouldSkipSystemCronInstall()) {
393
+ return getSystemCronStatus();
394
+ }
391
395
  if (nextConfig.enabled) {
392
396
  fs.writeFileSync(
393
397
  kSystemCronPath,
@@ -1,5 +1,8 @@
1
1
  const path = require("path");
2
2
  const { readOpenclawConfig, writeOpenclawConfig } = require("./openclaw-config");
3
+ const {
4
+ migrateLegacyTelegramStreamingConfig,
5
+ } = require("./openclaw-config-migrations");
3
6
 
4
7
  const kUsageTrackerPluginPath = path.resolve(
5
8
  __dirname,
@@ -123,7 +126,8 @@ const ensureUsageTrackerPluginConfig = ({ fsModule, openclawDir }) => {
123
126
  openclawDir,
124
127
  fallback: {},
125
128
  });
126
- const changed = reconcileManagedPluginConfig(cfg);
129
+ const migrated = migrateLegacyTelegramStreamingConfig(cfg);
130
+ const changed = reconcileManagedPluginConfig(cfg) || migrated;
127
131
  if (!changed) return false;
128
132
  writeOpenclawConfig({
129
133
  fsModule,
@@ -12,6 +12,23 @@ if [[ -f "$REPO/.env" ]]; then
12
12
  set +a
13
13
  fi
14
14
 
15
+ if [[ -f "$REPO/cron/system-sync.json" ]]; then
16
+ if node - "$REPO/cron/system-sync.json" <<'NODE'
17
+ const fs = require('fs');
18
+ const file = process.argv[2];
19
+ try {
20
+ const config = JSON.parse(fs.readFileSync(file, 'utf8'));
21
+ process.exit(config && config.enabled === false ? 0 : 1);
22
+ } catch {
23
+ process.exit(1);
24
+ }
25
+ NODE
26
+ then
27
+ echo "hourly-git-sync: disabled by cron/system-sync.json"
28
+ exit 0
29
+ fi
30
+ fi
31
+
15
32
  # Drop cron scheduler runtime-only churn when it is metadata/timestamp-only.
16
33
  maybe_restore_if_runtime_only() {
17
34
  local file="$1"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.9.18",
3
+ "version": "0.9.19",
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.5.28",
36
+ "openclaw": "2026.6.11",
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.14.0"
54
+ "node": ">=22.19.0"
55
55
  }
56
56
  }