@chrysb/alphaclaw 0.9.16 → 0.9.17

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 (34) hide show
  1. package/lib/public/css/tailwind.generated.css +1 -1
  2. package/lib/public/dist/app.bundle.js +1519 -1459
  3. package/lib/public/js/components/agents-tab/agent-overview/model-card.js +59 -7
  4. package/lib/public/js/components/agents-tab/agent-overview/use-model-card.js +124 -0
  5. package/lib/public/js/components/envars.js +1 -1
  6. package/lib/public/js/components/row-accessory-select.js +52 -0
  7. package/lib/public/js/lib/api.js +7 -0
  8. package/lib/public/js/lib/model-catalog.js +6 -0
  9. package/lib/public/js/lib/model-config.js +12 -7
  10. package/lib/public/js/lib/thinking-levels.js +37 -0
  11. package/lib/server/agents/agents.js +33 -7
  12. package/lib/server/agents/channels.js +4 -2
  13. package/lib/server/chat-ws.js +4 -1
  14. package/lib/server/constants.js +25 -0
  15. package/lib/server/cost-utils.js +2 -0
  16. package/lib/server/db/auth/index.js +147 -0
  17. package/lib/server/db/auth/schema.js +17 -0
  18. package/lib/server/gateway.js +158 -19
  19. package/lib/server/helpers.js +1 -3
  20. package/lib/server/init/register-server-routes.js +37 -18
  21. package/lib/server/init/runtime-init.js +4 -0
  22. package/lib/server/init/server-lifecycle.js +1 -24
  23. package/lib/server/login-throttle.js +242 -60
  24. package/lib/server/model-catalog-bootstrap.json +5 -0
  25. package/lib/server/onboarding/index.js +2 -2
  26. package/lib/server/openclaw-thinking.js +103 -0
  27. package/lib/server/openclaw-version.js +1 -1
  28. package/lib/server/routes/agents.js +10 -3
  29. package/lib/server/routes/models.js +35 -1
  30. package/lib/server/routes/onboarding.js +2 -2
  31. package/lib/server/routes/system.js +2 -2
  32. package/lib/server/usage-tracker-config.js +52 -1
  33. package/lib/server.js +26 -22
  34. package/package.json +2 -2
@@ -3,6 +3,7 @@ import htm from "htm";
3
3
  import { Badge } from "../../badge.js";
4
4
  import { LoadingSpinner } from "../../loading-spinner.js";
5
5
  import { OverflowMenu, OverflowMenuItem } from "../../overflow-menu.js";
6
+ import { RowAccessorySelect } from "../../row-accessory-select.js";
6
7
  import {
7
8
  getModelDisplayLabel,
8
9
  SearchableModelPicker,
@@ -22,16 +23,25 @@ export const AgentModelCard = ({
22
23
  canEditModel,
23
24
  effectiveModel,
24
25
  effectiveModelEntry,
26
+ formatInheritedThinkingLabel,
25
27
  handleClearModelOverride,
26
28
  handleSelectModel,
29
+ handleSelectThinkingDefault,
27
30
  hasDistinctModelOverride,
31
+ hasDistinctThinkingOverride,
32
+ inheritedThinkingDefault,
28
33
  loading,
29
34
  menuOpen,
30
35
  modelEntries,
31
36
  popularModels,
32
37
  remainingModelOptions,
33
38
  setMenuOpen,
39
+ showThinkingSelect,
40
+ thinkingOptionsLoading,
41
+ thinkingSelectOptions,
42
+ thinkingSelectValue,
34
43
  updatingModel,
44
+ updatingThinking,
35
45
  } = useModelCard({
36
46
  agent,
37
47
  onUpdateAgent,
@@ -63,7 +73,19 @@ export const AgentModelCard = ({
63
73
  handleClearModelOverride();
64
74
  }}
65
75
  >
66
- Inherit from defaults
76
+ Inherit model from defaults
77
+ </${OverflowMenuItem}>
78
+ `
79
+ : null}
80
+ ${hasDistinctThinkingOverride
81
+ ? html`
82
+ <${OverflowMenuItem}
83
+ onClick=${() => {
84
+ setMenuOpen(false);
85
+ handleSelectThinkingDefault("");
86
+ }}
87
+ >
88
+ Inherit thinking from defaults
67
89
  </${OverflowMenuItem}>
68
90
  `
69
91
  : null}
@@ -93,17 +115,20 @@ export const AgentModelCard = ({
93
115
  </p>`
94
116
  : html`
95
117
  <div class="space-y-1">
96
- ${modelEntries.map(
97
- (entry) => html`
118
+ ${modelEntries.map((entry) => {
119
+ const isPrimary = entry.key === effectiveModel;
120
+ const showThinkingPicker =
121
+ isPrimary && showThinkingSelect && !thinkingOptionsLoading;
122
+ return html`
98
123
  <div
99
124
  key=${entry.key}
100
- class="flex items-center justify-between py-1"
125
+ class="flex items-center justify-between gap-3 py-1"
101
126
  >
102
127
  <div class="flex items-center gap-2 min-w-0">
103
128
  <span class="text-sm text-body truncate">
104
129
  ${getModelDisplayLabel(entry)}
105
130
  </span>
106
- ${entry.key === effectiveModel
131
+ ${isPrimary
107
132
  ? html`<${Badge} tone="cyan">Primary</${Badge}>`
108
133
  : html`
109
134
  <button
@@ -115,9 +140,36 @@ export const AgentModelCard = ({
115
140
  </button>
116
141
  `}
117
142
  </div>
143
+ ${showThinkingPicker
144
+ ? html`
145
+ <${RowAccessorySelect}
146
+ ariaLabel="Agent thinking level"
147
+ title="Agent thinking level"
148
+ value=${thinkingSelectValue}
149
+ disabled=${saving ||
150
+ updatingModel ||
151
+ updatingThinking ||
152
+ !canEditModel}
153
+ onChange=${handleSelectThinkingDefault}
154
+ >
155
+ <option value="">
156
+ ${formatInheritedThinkingLabel(
157
+ inheritedThinkingDefault,
158
+ )}
159
+ </option>
160
+ ${thinkingSelectOptions.map(
161
+ (option) => html`
162
+ <option value=${option.value}>
163
+ ${option.label}
164
+ </option>
165
+ `,
166
+ )}
167
+ </${RowAccessorySelect}>
168
+ `
169
+ : null}
118
170
  </div>
119
- `,
120
- )}
171
+ `;
172
+ })}
121
173
  </div>
122
174
  `}
123
175
  ${loading
@@ -1,4 +1,10 @@
1
1
  import { useEffect, useMemo, useState } from "preact/hooks";
2
+ import { fetchThinkingOptions } from "../../../lib/api.js";
3
+ import {
4
+ formatInheritedThinkingLabel,
5
+ formatThinkingLevelLabel,
6
+ shouldShowThinkingLevelSelect,
7
+ } from "../../../lib/thinking-levels.js";
2
8
  import { useModels } from "../../models-tab/use-models.js";
3
9
  import {
4
10
  buildProviderHasAuth,
@@ -25,7 +31,14 @@ export const useModelCard = ({
25
31
  onUpdateAgent = async () => {},
26
32
  }) => {
27
33
  const [updatingModel, setUpdatingModel] = useState(false);
34
+ const [updatingThinking, setUpdatingThinking] = useState(false);
28
35
  const [menuOpen, setMenuOpen] = useState(false);
36
+ const [thinkingOptions, setThinkingOptions] = useState({
37
+ levels: [],
38
+ inheritedDefault: "off",
39
+ modelDefault: "off",
40
+ });
41
+ const [thinkingOptionsLoading, setThinkingOptionsLoading] = useState(false);
29
42
  const {
30
43
  catalog,
31
44
  primary: defaultPrimaryModel,
@@ -41,6 +54,45 @@ export const useModelCard = ({
41
54
  const hasDistinctModelOverride =
42
55
  !!explicitModel &&
43
56
  String(explicitModel).trim() !== String(defaultPrimaryModel || "").trim();
57
+ const explicitThinkingDefault = String(agent.thinkingDefault || "").trim();
58
+ const inheritedThinkingDefault = String(
59
+ thinkingOptions.inheritedDefault || thinkingOptions.modelDefault || "off",
60
+ ).trim();
61
+ const hasDistinctThinkingOverride =
62
+ !!explicitThinkingDefault &&
63
+ explicitThinkingDefault !== inheritedThinkingDefault;
64
+ const showThinkingSelect = shouldShowThinkingLevelSelect(
65
+ thinkingOptions.levels,
66
+ );
67
+
68
+ useEffect(() => {
69
+ const modelKey = String(effectiveModel || "").trim();
70
+ if (!modelKey.includes("/")) {
71
+ setThinkingOptions({
72
+ levels: [],
73
+ inheritedDefault: "off",
74
+ modelDefault: "off",
75
+ });
76
+ return undefined;
77
+ }
78
+ let cancelled = false;
79
+ setThinkingOptionsLoading(true);
80
+ fetchThinkingOptions(modelKey)
81
+ .then((payload) => {
82
+ if (cancelled || !payload?.ok) return;
83
+ setThinkingOptions({
84
+ levels: Array.isArray(payload.levels) ? payload.levels : [],
85
+ inheritedDefault: String(payload.inheritedDefault || "off").trim(),
86
+ modelDefault: String(payload.modelDefault || "off").trim(),
87
+ });
88
+ })
89
+ .finally(() => {
90
+ if (!cancelled) setThinkingOptionsLoading(false);
91
+ });
92
+ return () => {
93
+ cancelled = true;
94
+ };
95
+ }, [effectiveModel]);
44
96
 
45
97
  const providerHasAuth = useMemo(
46
98
  () => buildProviderHasAuth({ authProfiles, codexStatus }),
@@ -149,6 +201,69 @@ export const useModelCard = ({
149
201
  }
150
202
  };
151
203
 
204
+ const handleSelectThinkingDefault = async (nextValue) => {
205
+ const normalizedValue = String(nextValue || "").trim();
206
+ const isInherit = !normalizedValue;
207
+ if (isInherit) {
208
+ if (!hasDistinctThinkingOverride) return;
209
+ setUpdatingThinking(true);
210
+ try {
211
+ await onUpdateAgent(
212
+ String(agent.id || "").trim(),
213
+ { thinkingDefault: null },
214
+ "Agent thinking level reset to default",
215
+ );
216
+ } finally {
217
+ setUpdatingThinking(false);
218
+ }
219
+ return;
220
+ }
221
+ if (normalizedValue === explicitThinkingDefault) return;
222
+ setUpdatingThinking(true);
223
+ try {
224
+ await onUpdateAgent(
225
+ String(agent.id || "").trim(),
226
+ { thinkingDefault: normalizedValue },
227
+ "Agent thinking level updated",
228
+ );
229
+ } finally {
230
+ setUpdatingThinking(false);
231
+ }
232
+ };
233
+
234
+ const thinkingSelectValue = hasDistinctThinkingOverride
235
+ ? explicitThinkingDefault
236
+ : "";
237
+ const thinkingSelectOptions = useMemo(() => {
238
+ const seen = new Set();
239
+ const options = [];
240
+ const addOption = (value, label) => {
241
+ const normalizedValue = String(value || "").trim();
242
+ if (!normalizedValue || seen.has(normalizedValue)) return;
243
+ seen.add(normalizedValue);
244
+ options.push({
245
+ value: normalizedValue,
246
+ label: String(label || formatThinkingLevelLabel(normalizedValue)).trim(),
247
+ });
248
+ };
249
+ for (const entry of thinkingOptions.levels) {
250
+ addOption(
251
+ entry?.id,
252
+ formatThinkingLevelLabel(entry?.label || entry?.id),
253
+ );
254
+ }
255
+ if (
256
+ explicitThinkingDefault &&
257
+ !seen.has(explicitThinkingDefault)
258
+ ) {
259
+ addOption(
260
+ explicitThinkingDefault,
261
+ `${formatThinkingLevelLabel(explicitThinkingDefault)} (custom)`,
262
+ );
263
+ }
264
+ return options;
265
+ }, [explicitThinkingDefault, thinkingOptions.levels]);
266
+
152
267
  return {
153
268
  authorizedModelOptions,
154
269
  canEditModel: modelsReady && !loadingModels,
@@ -156,13 +271,22 @@ export const useModelCard = ({
156
271
  effectiveModelEntry,
157
272
  handleClearModelOverride,
158
273
  handleSelectModel,
274
+ handleSelectThinkingDefault,
159
275
  hasDistinctModelOverride,
276
+ hasDistinctThinkingOverride,
277
+ inheritedThinkingDefault,
160
278
  loading: !modelsReady || loadingModels,
161
279
  menuOpen,
162
280
  modelEntries,
163
281
  popularModels,
164
282
  remainingModelOptions,
165
283
  setMenuOpen,
284
+ showThinkingSelect,
285
+ thinkingOptionsLoading,
286
+ thinkingSelectOptions,
287
+ thinkingSelectValue,
288
+ formatInheritedThinkingLabel,
166
289
  updatingModel,
290
+ updatingThinking,
167
291
  };
168
292
  };
@@ -58,7 +58,7 @@ const normalizeEnvVarKey = (raw) =>
58
58
  .toUpperCase()
59
59
  .replace(/[^A-Z0-9_]/g, "_");
60
60
  const kManagedChannelTokenPattern =
61
- /^(?:TELEGRAM_BOT_TOKEN|DISCORD_BOT_TOKEN|SLACK_BOT_TOKEN|SLACK_APP_TOKEN)(?:_[A-Z0-9_]+)?$/;
61
+ /^(?:TELEGRAM_BOT_TOKEN|DISCORD_BOT_TOKEN|SLACK_BOT_TOKEN|SLACK_APP_TOKEN|WHATSAPP_OWNER_NUMBER)(?:_[A-Z0-9_]+)?$/;
62
62
  const stripSurroundingQuotes = (raw) => {
63
63
  const value = String(raw || "").trim();
64
64
  if (value.length < 2) return value;
@@ -0,0 +1,52 @@
1
+ import { h } from "preact";
2
+ import htm from "htm";
3
+
4
+ const html = htm.bind(h);
5
+
6
+ const RowAccessoryChevron = () => html`
7
+ <svg
8
+ width="14"
9
+ height="14"
10
+ viewBox="0 0 16 16"
11
+ fill="none"
12
+ class="text-fg-dim"
13
+ aria-hidden="true"
14
+ >
15
+ <path
16
+ d="M3.5 6L8 10.5L12.5 6"
17
+ stroke="currentColor"
18
+ stroke-width="2"
19
+ stroke-linecap="round"
20
+ stroke-linejoin="round"
21
+ />
22
+ </svg>
23
+ `;
24
+
25
+ export const RowAccessorySelect = ({
26
+ ariaLabel = "",
27
+ title = "",
28
+ value = "",
29
+ disabled = false,
30
+ onChange = () => {},
31
+ children = null,
32
+ }) => html`
33
+ <label
34
+ class=${`relative inline-flex shrink-0 items-center justify-end max-w-[12rem] min-w-[5.5rem] ${disabled
35
+ ? "opacity-50 cursor-not-allowed"
36
+ : "cursor-pointer"}`}
37
+ >
38
+ <select
39
+ aria-label=${ariaLabel}
40
+ title=${title || ariaLabel}
41
+ value=${value}
42
+ disabled=${disabled}
43
+ onInput=${(event) => onChange(String(event.currentTarget?.value ?? ""))}
44
+ class="appearance-none bg-transparent border-0 py-0 pl-0 pr-5 w-full text-right text-xs text-fg-muted hover:text-body cursor-pointer focus:outline-none focus-visible:ring-1 focus-visible:ring-border rounded disabled:cursor-not-allowed truncate"
45
+ >
46
+ ${children}
47
+ </select>
48
+ <span class="pointer-events-none absolute right-0 top-1/2 -translate-y-1/2">
49
+ <${RowAccessoryChevron} />
50
+ </span>
51
+ </label>
52
+ `;
@@ -857,6 +857,13 @@ export const fetchModelStatus = async () => {
857
857
  return res.json();
858
858
  };
859
859
 
860
+ export const fetchThinkingOptions = async (modelKey) => {
861
+ const normalized = String(modelKey || "").trim();
862
+ const qs = new URLSearchParams({ modelKey: normalized });
863
+ const res = await authFetch(`/api/models/thinking-options?${qs.toString()}`);
864
+ return res.json();
865
+ };
866
+
860
867
  export const setPrimaryModel = async (modelKey) => {
861
868
  const res = await authFetch("/api/models/set", {
862
869
  method: "POST",
@@ -4,6 +4,7 @@ import { getFeaturedModels } from "./model-config.js";
4
4
 
5
5
  export const kModelCatalogCacheKey = "/api/models";
6
6
  export const kModelCatalogPollIntervalMs = 3000;
7
+ export const kDefaultOnboardingModelKey = "anthropic/claude-opus-4-8";
7
8
 
8
9
  export const getModelCatalogModels = (payload) =>
9
10
  Array.isArray(payload?.models) ? payload.models : [];
@@ -26,6 +27,11 @@ export const getInitialOnboardingModelKey = ({
26
27
  } = {}) => {
27
28
  const normalizedCurrent = String(currentModelKey || "").trim();
28
29
  if (normalizedCurrent) return normalizedCurrent;
30
+ const catalogHasKey = (key) =>
31
+ catalog.some((model) => String(model?.key || "") === key);
32
+ if (catalogHasKey(kDefaultOnboardingModelKey)) {
33
+ return kDefaultOnboardingModelKey;
34
+ }
29
35
  const featuredModels = getFeaturedModels(catalog);
30
36
  return String(featuredModels[0]?.key || catalog[0]?.key || "");
31
37
  };
@@ -9,6 +9,10 @@ export const getAuthProviderFromModelProvider = (provider) => {
9
9
  };
10
10
 
11
11
  export const kFeaturedModelDefs = [
12
+ {
13
+ label: "Opus 4.8",
14
+ preferredKeys: ["anthropic/claude-opus-4-8"],
15
+ },
12
16
  {
13
17
  label: "Opus 4.7",
14
18
  preferredKeys: ["anthropic/claude-opus-4-7"],
@@ -58,13 +62,14 @@ export const kProviderAuthFields = {
58
62
  linkText: "Get key",
59
63
  placeholder: "sk-ant-...",
60
64
  },
61
- {
62
- key: "ANTHROPIC_TOKEN",
63
- label: "Anthropic Setup Token",
64
- hint: "From claude setup-token (uses your Claude subscription)",
65
- linkText: "Get token",
66
- placeholder: "Token...",
67
- },
65
+ // Temporarily hidden — setup-token flow is not supported in onboarding yet.
66
+ // {
67
+ // key: "ANTHROPIC_TOKEN",
68
+ // label: "Anthropic Setup Token",
69
+ // hint: "From claude setup-token (uses your Claude subscription)",
70
+ // linkText: "Get token",
71
+ // placeholder: "Token...",
72
+ // },
68
73
  ],
69
74
  openai: [
70
75
  {
@@ -0,0 +1,37 @@
1
+ const kThinkingLevelLabelOverrides = {
2
+ off: "Off",
3
+ on: "On",
4
+ minimal: "Minimal",
5
+ low: "Low",
6
+ medium: "Medium",
7
+ high: "High",
8
+ adaptive: "Adaptive",
9
+ xhigh: "Extra high",
10
+ max: "Maximum",
11
+ };
12
+
13
+ export const formatThinkingLevelLabel = (levelId = "") => {
14
+ const normalized = String(levelId || "").trim().toLowerCase();
15
+ if (!normalized) return "";
16
+ if (kThinkingLevelLabelOverrides[normalized]) {
17
+ return kThinkingLevelLabelOverrides[normalized];
18
+ }
19
+ return normalized
20
+ .split(/[-_]/g)
21
+ .filter(Boolean)
22
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
23
+ .join(" ");
24
+ };
25
+
26
+ export const formatInheritedThinkingLabel = (levelId = "") => {
27
+ const label = formatThinkingLevelLabel(levelId);
28
+ return label ? `Inherited: ${label}` : "Inherited";
29
+ };
30
+
31
+ export const shouldShowThinkingLevelSelect = (levels = []) => {
32
+ const normalized = (Array.isArray(levels) ? levels : [])
33
+ .map((entry) => String(entry?.id || entry || "").trim().toLowerCase())
34
+ .filter(Boolean);
35
+ if (normalized.length === 0) return false;
36
+ return !(normalized.length === 1 && normalized[0] === "off");
37
+ };
@@ -1,4 +1,5 @@
1
1
  const path = require("path");
2
+ const { normalizeThinkingDefaultValue } = require("../openclaw-thinking");
2
3
 
3
4
  const {
4
5
  kDefaultAgentId,
@@ -42,11 +43,25 @@ const toReadableAgent = (agent = {}) => ({
42
43
  });
43
44
 
44
45
  const createAgentsDomain = ({ fsImpl, OPENCLAW_DIR }) => {
45
- const listAgents = () => {
46
- const cfg = withNormalizedAgentsConfig({
46
+ const readAgentsConfig = () =>
47
+ withNormalizedAgentsConfig({
47
48
  OPENCLAW_DIR,
48
49
  cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
49
50
  });
51
+
52
+ const getAgentDefaults = () => {
53
+ const cfg = readAgentsConfig();
54
+ const thinkingDefault = cfg.agents?.defaults?.thinkingDefault;
55
+ return {
56
+ thinkingDefault:
57
+ typeof thinkingDefault === "string" && thinkingDefault.trim()
58
+ ? thinkingDefault.trim()
59
+ : null,
60
+ };
61
+ };
62
+
63
+ const listAgents = () => {
64
+ const cfg = readAgentsConfig();
50
65
  return (cfg.agents?.list || []).map((entry) => toReadableAgent(entry));
51
66
  };
52
67
 
@@ -131,12 +146,9 @@ const createAgentsDomain = ({ fsImpl, OPENCLAW_DIR }) => {
131
146
  return toReadableAgent(nextAgent);
132
147
  };
133
148
 
134
- const updateAgent = (agentId, patch = {}) => {
149
+ const updateAgent = async (agentId, patch = {}) => {
135
150
  const normalized = String(agentId || "").trim();
136
- const cfg = withNormalizedAgentsConfig({
137
- OPENCLAW_DIR,
138
- cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
139
- });
151
+ const cfg = readAgentsConfig();
140
152
  const index = cfg.agents.list.findIndex((entry) => entry.id === normalized);
141
153
  if (index < 0) throw new Error(`Agent "${normalized}" not found`);
142
154
  const current = cfg.agents.list[index];
@@ -188,6 +200,19 @@ const createAgentsDomain = ({ fsImpl, OPENCLAW_DIR }) => {
188
200
  delete next.tools;
189
201
  }
190
202
  }
203
+ if (patch.thinkingDefault !== undefined) {
204
+ if (patch.thinkingDefault === null) {
205
+ delete next.thinkingDefault;
206
+ } else {
207
+ const normalizedThinking = await normalizeThinkingDefaultValue(
208
+ patch.thinkingDefault,
209
+ );
210
+ if (!normalizedThinking) {
211
+ throw new Error("Invalid thinkingDefault value");
212
+ }
213
+ next.thinkingDefault = normalizedThinking;
214
+ }
215
+ }
191
216
  cfg.agents.list[index] = next;
192
217
  saveConfig({ fsImpl, OPENCLAW_DIR, config: cfg });
193
218
  return toReadableAgent(next);
@@ -253,6 +278,7 @@ const createAgentsDomain = ({ fsImpl, OPENCLAW_DIR }) => {
253
278
  return {
254
279
  listAgents,
255
280
  getAgent,
281
+ getAgentDefaults,
256
282
  getAgentWorkspaceSize,
257
283
  createAgent,
258
284
  updateAgent,
@@ -270,7 +270,7 @@ const createChannelsDomain = ({
270
270
 
271
271
  const previousConfig = cloneJson(cfg);
272
272
  try {
273
- onProgress({ phase: "restarting", label: "Rebooting..." });
273
+ onProgress({ phase: "configuring", label: "Configuring..." });
274
274
  writeEnvFile(nextEnvVars);
275
275
  reloadEnv();
276
276
  assertActiveChannelTokenEnvVars({
@@ -280,7 +280,6 @@ const createChannelsDomain = ({
280
280
  }),
281
281
  envVars: nextEnvVars,
282
282
  });
283
- await restartGateway();
284
283
  const pluginEnabledCfg = withNormalizedAgentsConfig({
285
284
  OPENCLAW_DIR,
286
285
  cfg: loadConfig({ fsImpl, OPENCLAW_DIR }),
@@ -374,6 +373,8 @@ const createChannelsDomain = ({
374
373
  "Could not bind channel account",
375
374
  );
376
375
  }
376
+ onProgress({ phase: "restarting", label: "Rebooting..." });
377
+ await restartGateway();
377
378
  } catch (error) {
378
379
  try {
379
380
  await clawCmd(
@@ -760,6 +761,7 @@ const createChannelsDomain = ({
760
761
  }
761
762
 
762
763
  cleanupChannelAccountPairingFiles({ provider, accountId });
764
+ await restartGateway();
763
765
  return { ok: true };
764
766
  }
765
767
 
@@ -6,7 +6,7 @@ const kEnvRefPattern = /^\$\{([A-Z0-9_]+)\}$/i;
6
6
  const kConnectTimeoutMs = 8000;
7
7
  const kHistoryTimeoutMs = 12000;
8
8
  const kGatewayReqTimeoutMs = 15000;
9
- const kGatewayProtocolVersion = 3;
9
+ const kGatewayProtocolVersion = 4;
10
10
  // Gateway method auth (see OpenClaw method-scopes): chat.history needs operator.read;
11
11
  // chat.send / chat.abort need operator.write. Align with CLI_DEFAULT_OPERATOR_SCOPES plus admin.
12
12
  const kGatewayChatBridgeScopes = [
@@ -283,6 +283,9 @@ const sanitizeError = (error) => {
283
283
  ) {
284
284
  return "Gateway authentication failed. Verify OPENCLAW_GATEWAY_TOKEN matches the gateway.";
285
285
  }
286
+ if (lower.includes("protocol mismatch")) {
287
+ return "Chat cannot connect to the gateway (protocol version mismatch). Update AlphaClaw to match your OpenClaw version.";
288
+ }
286
289
  if (lower.includes("method not found") || lower.includes("unknown method")) {
287
290
  return "This gateway build does not support chat APIs. Update OpenClaw.";
288
291
  }
@@ -54,6 +54,22 @@ const kLoginMaxLockMs = parsePositiveInt(
54
54
  process.env.LOGIN_RATE_MAX_LOCK_MS,
55
55
  15 * 60 * 1000,
56
56
  );
57
+ const kLoginGlobalWindowMs = parsePositiveInt(
58
+ process.env.LOGIN_RATE_GLOBAL_WINDOW_MS,
59
+ kLoginWindowMs,
60
+ );
61
+ const kLoginGlobalMaxAttempts = parsePositiveInt(
62
+ process.env.LOGIN_RATE_GLOBAL_MAX_ATTEMPTS,
63
+ Math.max(kLoginMaxAttempts * 5, 25),
64
+ );
65
+ const kLoginGlobalBaseLockMs = parsePositiveInt(
66
+ process.env.LOGIN_RATE_GLOBAL_BASE_LOCK_MS,
67
+ kLoginBaseLockMs,
68
+ );
69
+ const kLoginGlobalMaxLockMs = parsePositiveInt(
70
+ process.env.LOGIN_RATE_GLOBAL_MAX_LOCK_MS,
71
+ kLoginMaxLockMs,
72
+ );
57
73
  const kLoginCleanupIntervalMs = parsePositiveInt(
58
74
  process.env.LOGIN_RATE_CLEANUP_INTERVAL_MS,
59
75
  60 * 1000,
@@ -92,6 +108,11 @@ const kOnboardingModelProviders = new Set([
92
108
  "vllm",
93
109
  ]);
94
110
  const kMinimalFallbackOnboardingModels = [
111
+ {
112
+ key: "anthropic/claude-opus-4-8",
113
+ provider: "anthropic",
114
+ label: "Claude Opus 4.8",
115
+ },
95
116
  {
96
117
  key: "anthropic/claude-opus-4-7",
97
118
  provider: "anthropic",
@@ -445,6 +466,10 @@ module.exports = {
445
466
  kLoginMaxAttempts,
446
467
  kLoginBaseLockMs,
447
468
  kLoginMaxLockMs,
469
+ kLoginGlobalWindowMs,
470
+ kLoginGlobalMaxAttempts,
471
+ kLoginGlobalBaseLockMs,
472
+ kLoginGlobalMaxLockMs,
448
473
  kLoginCleanupIntervalMs,
449
474
  kLoginStateTtlMs,
450
475
  kOnboardingModelProviders,
@@ -13,6 +13,8 @@ const kClaudeOpus47Pricing = {
13
13
  };
14
14
 
15
15
  const kGlobalModelPricing = {
16
+ "claude-opus-4-8": kClaudeOpus47Pricing,
17
+ "claude-opus-4.8": kClaudeOpus47Pricing,
16
18
  "claude-opus-4-7": kClaudeOpus47Pricing,
17
19
  "claude-opus-4.7": kClaudeOpus47Pricing,
18
20
  "claude-opus-4-6": {