@chrysb/alphaclaw 0.4.6-beta.6 → 0.4.6-beta.7

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.
@@ -0,0 +1,142 @@
1
+ import { h } from "https://esm.sh/preact";
2
+ import { useEffect, useState } from "https://esm.sh/preact/hooks";
3
+ import htm from "https://esm.sh/htm";
4
+ import { ModalShell } from "./modal-shell.js";
5
+ import { ActionButton } from "./action-button.js";
6
+ import { PageHeader } from "./page-header.js";
7
+ import { CloseIcon } from "./icons.js";
8
+ import { useAgentSessions } from "../hooks/useAgentSessions.js";
9
+
10
+ const html = htm.bind(h);
11
+
12
+ export const AgentSendModal = ({
13
+ visible = false,
14
+ title = "Send to agent",
15
+ messageLabel = "Message",
16
+ messageRows = 8,
17
+ initialMessage = "",
18
+ resetKey = "",
19
+ submitLabel = "Send message",
20
+ loadingLabel = "Sending...",
21
+ cancelLabel = "Cancel",
22
+ onClose = () => {},
23
+ onSubmit = async () => true,
24
+ sessionFilter = undefined,
25
+ }) => {
26
+ const {
27
+ sessions,
28
+ selectedSessionKey,
29
+ setSelectedSessionKey,
30
+ selectedSession,
31
+ loading: loadingSessions,
32
+ error: loadError,
33
+ } = useAgentSessions({ enabled: visible, filter: sessionFilter });
34
+ const [messageText, setMessageText] = useState("");
35
+ const [sending, setSending] = useState(false);
36
+
37
+ useEffect(() => {
38
+ if (!visible) return;
39
+ setMessageText(String(initialMessage || ""));
40
+ }, [visible, initialMessage, resetKey]);
41
+
42
+ const handleSend = async () => {
43
+ if (!selectedSession || sending) return;
44
+ const trimmedMessage = String(messageText || "").trim();
45
+ if (!trimmedMessage) return;
46
+ setSending(true);
47
+ try {
48
+ const shouldClose = await onSubmit({
49
+ selectedSession,
50
+ selectedSessionKey,
51
+ message: trimmedMessage,
52
+ });
53
+ if (shouldClose !== false) {
54
+ onClose();
55
+ }
56
+ } finally {
57
+ setSending(false);
58
+ }
59
+ };
60
+
61
+ return html`
62
+ <${ModalShell}
63
+ visible=${visible}
64
+ onClose=${() => {
65
+ if (sending) return;
66
+ onClose();
67
+ }}
68
+ panelClassName="bg-modal border border-border rounded-xl p-5 max-w-lg w-full space-y-4"
69
+ >
70
+ <${PageHeader}
71
+ title=${title}
72
+ actions=${html`
73
+ <button
74
+ type="button"
75
+ onclick=${() => {
76
+ if (sending) return;
77
+ onClose();
78
+ }}
79
+ class="h-8 w-8 inline-flex items-center justify-center rounded-lg ac-btn-secondary"
80
+ aria-label="Close modal"
81
+ >
82
+ <${CloseIcon} className="w-3.5 h-3.5 text-gray-300" />
83
+ </button>
84
+ `}
85
+ />
86
+ <div class="space-y-2">
87
+ <label class="text-xs text-gray-500">Send to session</label>
88
+ <select
89
+ value=${selectedSessionKey}
90
+ onChange=${(event) =>
91
+ setSelectedSessionKey(String(event.currentTarget?.value || ""))}
92
+ disabled=${loadingSessions || sending}
93
+ class="w-full bg-black/30 border border-border rounded-lg px-3 py-2 text-xs text-gray-200 focus:border-gray-500"
94
+ >
95
+ ${sessions.length === 0
96
+ ? html`<option value="">No sessions available</option>`
97
+ : null}
98
+ ${sessions.map(
99
+ (sessionRow) => html`
100
+ <option value=${String(sessionRow?.key || "")}>
101
+ ${String(sessionRow?.label || sessionRow?.key || "Session")}
102
+ </option>
103
+ `,
104
+ )}
105
+ </select>
106
+ ${loadingSessions
107
+ ? html`<div class="text-xs text-gray-500">Loading sessions...</div>`
108
+ : null}
109
+ ${loadError ? html`<div class="text-xs text-red-400">${loadError}</div>` : null}
110
+ </div>
111
+ <div class="space-y-2">
112
+ <label class="text-xs text-gray-500">${messageLabel}</label>
113
+ <textarea
114
+ value=${messageText}
115
+ onInput=${(event) =>
116
+ setMessageText(String(event.currentTarget?.value || ""))}
117
+ disabled=${sending}
118
+ rows=${messageRows}
119
+ class="w-full bg-black/30 border border-border rounded-lg px-3 py-2 text-xs text-gray-200 focus:border-gray-500 font-mono leading-5"
120
+ ></textarea>
121
+ </div>
122
+ <div class="flex items-center justify-end gap-2">
123
+ <${ActionButton}
124
+ onClick=${onClose}
125
+ disabled=${sending}
126
+ tone="secondary"
127
+ size="md"
128
+ idleLabel=${cancelLabel}
129
+ />
130
+ <${ActionButton}
131
+ onClick=${handleSend}
132
+ disabled=${!selectedSession || loadingSessions || !!loadError || !String(messageText || "").trim()}
133
+ loading=${sending}
134
+ tone="primary"
135
+ size="md"
136
+ idleLabel=${submitLabel}
137
+ loadingLabel=${loadingLabel}
138
+ />
139
+ </div>
140
+ </${ModalShell}>
141
+ `;
142
+ };
@@ -1,11 +1,8 @@
1
1
  import { h } from "https://esm.sh/preact";
2
- import { useEffect, useState } from "https://esm.sh/preact/hooks";
3
2
  import htm from "https://esm.sh/htm";
4
- import { ModalShell } from "../modal-shell.js";
5
- import { ActionButton } from "../action-button.js";
6
3
  import { sendDoctorCardFix, updateDoctorCardStatus } from "../../lib/api.js";
7
4
  import { showToast } from "../toast.js";
8
- import { useAgentSessions } from "../../hooks/useAgentSessions.js";
5
+ import { AgentSendModal } from "../agent-send-modal.js";
9
6
 
10
7
  const html = htm.bind(h);
11
8
 
@@ -15,33 +12,15 @@ export const DoctorFixCardModal = ({
15
12
  onClose = () => {},
16
13
  onComplete = () => {},
17
14
  }) => {
18
- const {
19
- sessions,
20
- selectedSessionKey,
21
- setSelectedSessionKey,
22
- selectedSession,
23
- loading: loadingSessions,
24
- error: loadError,
25
- } = useAgentSessions({ enabled: visible });
26
-
27
- const [sending, setSending] = useState(false);
28
- const [promptText, setPromptText] = useState("");
29
-
30
- useEffect(() => {
31
- if (!visible) return;
32
- setPromptText(String(card?.fixPrompt || ""));
33
- }, [visible, card?.fixPrompt, card?.id]);
34
-
35
- const handleSend = async () => {
36
- if (!card?.id || sending) return;
15
+ const handleSend = async ({ selectedSession, message }) => {
16
+ if (!card?.id) return false;
37
17
  try {
38
- setSending(true);
39
18
  await sendDoctorCardFix({
40
19
  cardId: card.id,
41
20
  sessionId: selectedSession?.sessionId || "",
42
21
  replyChannel: selectedSession?.replyChannel || "",
43
22
  replyTo: selectedSession?.replyTo || "",
44
- prompt: promptText,
23
+ prompt: message,
45
24
  });
46
25
  try {
47
26
  await updateDoctorCardStatus({ cardId: card.id, status: "fixed" });
@@ -57,77 +36,24 @@ export const DoctorFixCardModal = ({
57
36
  );
58
37
  }
59
38
  await onComplete();
60
- onClose();
39
+ return true;
61
40
  } catch (error) {
62
41
  showToast(error.message || "Could not send Doctor fix request", "error");
63
- } finally {
64
- setSending(false);
42
+ return false;
65
43
  }
66
44
  };
67
45
 
68
46
  return html`
69
- <${ModalShell}
47
+ <${AgentSendModal}
70
48
  visible=${visible}
49
+ title="Ask agent to fix"
50
+ messageLabel="Instructions"
51
+ initialMessage=${String(card?.fixPrompt || "")}
52
+ resetKey=${String(card?.id || "")}
53
+ submitLabel="Send fix request"
54
+ loadingLabel="Sending..."
71
55
  onClose=${onClose}
72
- panelClassName="bg-modal border border-border rounded-xl p-5 max-w-lg w-full space-y-4"
73
- >
74
- <div class="space-y-1">
75
- <h2 class="text-base font-semibold">Ask agent to fix</h2>
76
- <p class="text-xs text-gray-400">
77
- Send this Doctor finding to one of your agent sessions as a focused fix request.
78
- </p>
79
- </div>
80
- <div class="space-y-2">
81
- <label class="text-xs text-gray-500">Send to session</label>
82
- <select
83
- value=${selectedSessionKey}
84
- onChange=${(event) => setSelectedSessionKey(String(event.currentTarget?.value || ""))}
85
- disabled=${loadingSessions || sending}
86
- class="w-full bg-black/30 border border-border rounded-lg px-3 py-2 text-xs text-gray-200 focus:border-gray-500"
87
- >
88
- ${sessions.map(
89
- (sessionRow) => html`
90
- <option value=${String(sessionRow?.key || "")}>
91
- ${String(sessionRow?.label || sessionRow?.key || "Session")}
92
- </option>
93
- `,
94
- )}
95
- </select>
96
- ${
97
- loadingSessions
98
- ? html`<div class="text-xs text-gray-500">Loading sessions...</div>`
99
- : null
100
- }
101
- ${loadError ? html`<div class="text-xs text-red-400">${loadError}</div>` : null}
102
- </div>
103
- <div class="space-y-2">
104
- <label class="text-xs text-gray-500">Instructions</label>
105
- <textarea
106
- value=${promptText}
107
- onInput=${(event) => setPromptText(String(event.currentTarget?.value || ""))}
108
- disabled=${sending}
109
- rows="8"
110
- class="w-full bg-black/30 border border-border rounded-lg px-3 py-2 text-xs text-gray-200 focus:border-gray-500 font-mono leading-5"
111
- ></textarea>
112
- </div>
113
- <div class="flex items-center justify-end gap-2">
114
- <${ActionButton}
115
- onClick=${onClose}
116
- disabled=${sending}
117
- tone="secondary"
118
- size="md"
119
- idleLabel="Cancel"
120
- />
121
- <${ActionButton}
122
- onClick=${handleSend}
123
- disabled=${!selectedSession || loadingSessions || !!loadError || !String(promptText || "").trim()}
124
- loading=${sending}
125
- tone="primary"
126
- size="md"
127
- idleLabel="Send fix request"
128
- loadingLabel="Sending..."
129
- />
130
- </div>
131
- </${ModalShell}>
56
+ onSubmit=${handleSend}
57
+ />
132
58
  `;
133
59
  };
@@ -326,6 +326,7 @@ export const Gateway = ({
326
326
  const showInspectButton = watchdogHealth === "degraded" && !!onOpenWatchdog;
327
327
  const showRepairButton =
328
328
  isRepairInProgress ||
329
+ (watchdogStatus?.health === "degraded" && !onOpenWatchdog) ||
329
330
  watchdogStatus?.lifecycle === "crash_loop" ||
330
331
  watchdogStatus?.health === "unhealthy" ||
331
332
  watchdogStatus?.health === "crashed";
@@ -81,6 +81,7 @@ export const GmailSetupWizard = ({
81
81
  }) => {
82
82
  const [step, setStep] = useState(0);
83
83
  const [projectIdInput, setProjectIdInput] = useState("");
84
+ const [editingProjectId, setEditingProjectId] = useState(false);
84
85
  const [localError, setLocalError] = useState("");
85
86
  const [projectIdResolved, setProjectIdResolved] = useState(false);
86
87
  const [watchEnabled, setWatchEnabled] = useState(false);
@@ -100,6 +101,7 @@ export const GmailSetupWizard = ({
100
101
  setStep(0);
101
102
  setLocalError("");
102
103
  setProjectIdInput("");
104
+ setEditingProjectId(false);
103
105
  setProjectIdResolved(false);
104
106
  setWatchEnabled(false);
105
107
  setSendingToAgent(false);
@@ -110,10 +112,11 @@ export const GmailSetupWizard = ({
110
112
  const hasProjectIdFromConfig = Boolean(
111
113
  String(clientConfig?.projectId || "").trim() || commands,
112
114
  );
113
- const needsProjectId = !hasProjectIdFromConfig && !projectIdResolved;
115
+ const needsProjectId =
116
+ editingProjectId || (!hasProjectIdFromConfig && !projectIdResolved);
114
117
  const detectedProjectId =
115
- String(clientConfig?.projectId || "").trim() ||
116
118
  String(projectIdInput || "").trim() ||
119
+ String(clientConfig?.projectId || "").trim() ||
117
120
  "<project-id>";
118
121
  const client =
119
122
  String(account?.client || clientConfig?.client || "default").trim() ||
@@ -135,6 +138,13 @@ export const GmailSetupWizard = ({
135
138
  showToast("Could not copy text", "error");
136
139
  }, []);
137
140
 
141
+ const handleChangeProjectId = useCallback(() => {
142
+ setLocalError("");
143
+ setProjectIdInput(String(clientConfig?.projectId || "").trim());
144
+ setProjectIdResolved(false);
145
+ setEditingProjectId(true);
146
+ }, [clientConfig?.projectId]);
147
+
138
148
  const handleFinish = async () => {
139
149
  try {
140
150
  setLocalError("");
@@ -159,6 +169,7 @@ export const GmailSetupWizard = ({
159
169
  client,
160
170
  projectId: String(projectIdInput || "").trim(),
161
171
  });
172
+ setEditingProjectId(false);
162
173
  setProjectIdResolved(true);
163
174
  } catch (err) {
164
175
  setLocalError(err.message || "Could not save project id");
@@ -230,7 +241,9 @@ export const GmailSetupWizard = ({
230
241
  <div
231
242
  class="rounded-lg border border-border bg-black/20 p-3 space-y-2"
232
243
  >
233
- <div class="text-sm">Project ID required</div>
244
+ <div class="text-sm">
245
+ ${editingProjectId ? "Change project ID" : "Project ID required"}
246
+ </div>
234
247
  <div class="text-xs text-gray-500">
235
248
  Find it in the${" "}
236
249
  <a
@@ -257,6 +270,9 @@ export const GmailSetupWizard = ({
257
270
  !needsProjectId && step === 0
258
271
  ? html`
259
272
  <div class="space-y-1">
273
+ <div class="text-xs text-gray-500">
274
+ Using project <code>${detectedProjectId}</code>.
275
+ </div>
260
276
  <div class="text-xs text-gray-500">
261
277
  If <code>gcloud</code> is not installed on your computer,
262
278
  follow the official install guide:${" "}
@@ -372,7 +388,15 @@ export const GmailSetupWizard = ({
372
388
  }
373
389
  <div class="grid grid-cols-2 gap-2 pt-2">
374
390
  ${step === 0
375
- ? html`<div></div>`
391
+ ? html`${!needsProjectId
392
+ ? html`<button
393
+ type="button"
394
+ onclick=${handleChangeProjectId}
395
+ class="justify-self-start text-xs px-2 py-1 rounded-lg ac-btn-ghost"
396
+ >
397
+ Change project ID
398
+ </button>`
399
+ : html`<div></div>`}`
376
400
  : html`<${ActionButton}
377
401
  onClick=${() => setStep((prev) => Math.max(prev - 1, 0))}
378
402
  disabled=${saving}
@@ -6,8 +6,10 @@ import {
6
6
  createWebhook,
7
7
  deleteWebhook,
8
8
  fetchWebhookDetail,
9
+ fetchWebhookRequest,
9
10
  fetchWebhookRequests,
10
11
  fetchWebhooks,
12
+ sendAgentMessage,
11
13
  } from "../lib/api.js";
12
14
  import {
13
15
  formatLocaleDateTime,
@@ -17,6 +19,7 @@ import { showToast } from "./toast.js";
17
19
  import { PageHeader } from "./page-header.js";
18
20
  import { ConfirmDialog } from "./confirm-dialog.js";
19
21
  import { ActionButton } from "./action-button.js";
22
+ import { AgentSendModal } from "./agent-send-modal.js";
20
23
  import { ModalShell } from "./modal-shell.js";
21
24
  import { Badge } from "./badge.js";
22
25
  import { CloseIcon } from "./icons.js";
@@ -82,6 +85,40 @@ const jsonPretty = (value) => {
82
85
  }
83
86
  };
84
87
 
88
+ const buildWebhookDebugMessage = ({
89
+ hookName = "",
90
+ webhook = null,
91
+ request = null,
92
+ }) => {
93
+ const hookPath =
94
+ String(webhook?.path || "").trim() ||
95
+ (hookName ? `/hooks/${hookName}` : "/hooks/unknown");
96
+ const gatewayStatus =
97
+ request?.gatewayStatus == null ? "n/a" : String(request.gatewayStatus);
98
+ return [
99
+ "Investigate this failed webhook request and share findings before fixing anything.",
100
+ "Reply with your diagnosis first, including the likely root cause, any relevant risks, and what you would change if I approve a fix.",
101
+ "",
102
+ `Webhook: ${hookPath}`,
103
+ `Request ID: ${String(request?.id || "unknown")}`,
104
+ `Time: ${String(request?.createdAt || "unknown")}`,
105
+ `Method: ${String(request?.method || "unknown")}`,
106
+ `Source IP: ${String(request?.sourceIp || "unknown")}`,
107
+ `Gateway status: ${gatewayStatus}`,
108
+ `Transform path: ${String(webhook?.transformPath || "unknown")}`,
109
+ `Payload truncated: ${request?.payloadTruncated ? "yes" : "no"}`,
110
+ "",
111
+ "Headers:",
112
+ jsonPretty(request?.headers),
113
+ "",
114
+ "Payload:",
115
+ jsonPretty(request?.payload),
116
+ "",
117
+ "Gateway response:",
118
+ jsonPretty(request?.gatewayBody),
119
+ ].join("\n");
120
+ };
121
+
85
122
  const CreateWebhookModal = ({
86
123
  visible,
87
124
  name,
@@ -197,6 +234,8 @@ export const Webhooks = ({
197
234
  const [expandedRows, setExpandedRows] = useState(() => new Set());
198
235
  const [sendingTestWebhook, setSendingTestWebhook] = useState(false);
199
236
  const [replayingRequestId, setReplayingRequestId] = useState(null);
237
+ const [debugLoadingRequestId, setDebugLoadingRequestId] = useState(null);
238
+ const [debugRequest, setDebugRequest] = useState(null);
200
239
 
201
240
  const listPoll = usePolling(fetchWebhooks, 15000);
202
241
  const webhooks = listPoll.data?.webhooks || [];
@@ -496,6 +535,31 @@ export const Webhooks = ({
496
535
  }, []);
497
536
 
498
537
  const isListLoading = !listPoll.data && !listPoll.error;
538
+ const debugAgentMessage = useMemo(
539
+ () =>
540
+ buildWebhookDebugMessage({
541
+ hookName: selectedHookName,
542
+ webhook: selectedWebhook,
543
+ request: debugRequest,
544
+ }),
545
+ [debugRequest, selectedHookName, selectedWebhook],
546
+ );
547
+
548
+ const handleAskAgentToDebug = useCallback(
549
+ async (item) => {
550
+ if (!selectedHookName || !item?.id || debugLoadingRequestId === item.id) return;
551
+ try {
552
+ setDebugLoadingRequestId(item.id);
553
+ const data = await fetchWebhookRequest(selectedHookName, item.id);
554
+ setDebugRequest(data?.request || item);
555
+ } catch (err) {
556
+ showToast(err.message || "Could not load webhook request details", "error");
557
+ } finally {
558
+ setDebugLoadingRequestId(null);
559
+ }
560
+ },
561
+ [debugLoadingRequestId, selectedHookName],
562
+ );
499
563
 
500
564
  return html`
501
565
  <div class="space-y-4">
@@ -841,7 +905,7 @@ export const Webhooks = ({
841
905
  >
842
906
  ${jsonPretty(item.headers)}</pre
843
907
  >
844
- <div class="mt-2 flex justify-start">
908
+ <div class="mt-2 flex justify-start gap-2">
845
909
  <button
846
910
  class="h-7 text-xs px-2.5 rounded-lg ac-btn-secondary"
847
911
  onclick=${() =>
@@ -905,7 +969,7 @@ ${jsonPretty(item.payload)}</pre
905
969
  >
906
970
  ${jsonPretty(item.gatewayBody)}</pre
907
971
  >
908
- <div class="mt-2 flex justify-start">
972
+ <div class="mt-2 flex justify-start gap-2">
909
973
  <button
910
974
  class="h-7 text-xs px-2.5 rounded-lg ac-btn-secondary"
911
975
  onclick=${() =>
@@ -916,6 +980,19 @@ ${jsonPretty(item.gatewayBody)}</pre
916
980
  >
917
981
  Copy
918
982
  </button>
983
+ ${item.status === "error"
984
+ ? html`<${ActionButton}
985
+ onClick=${() =>
986
+ handleAskAgentToDebug(item)}
987
+ loading=${debugLoadingRequestId ===
988
+ item.id}
989
+ tone="primary"
990
+ size="sm"
991
+ idleLabel="Ask agent to debug"
992
+ loadingLabel="Loading..."
993
+ className="h-7 px-2.5"
994
+ />`
995
+ : null}
919
996
  </div>
920
997
  </div>
921
998
  </div>
@@ -1058,6 +1135,30 @@ ${jsonPretty(item.gatewayBody)}</pre
1058
1135
  }}
1059
1136
  onConfirm=${handleDeleteConfirmed}
1060
1137
  />
1138
+ <${AgentSendModal}
1139
+ visible=${!!debugRequest}
1140
+ title="Ask agent to debug"
1141
+ messageLabel="Debug request"
1142
+ messageRows=${12}
1143
+ initialMessage=${debugAgentMessage}
1144
+ resetKey=${String(debugRequest?.id || "")}
1145
+ submitLabel="Send debug request"
1146
+ loadingLabel="Sending..."
1147
+ onClose=${() => setDebugRequest(null)}
1148
+ onSubmit=${async ({ selectedSessionKey, message }) => {
1149
+ try {
1150
+ await sendAgentMessage({
1151
+ message,
1152
+ sessionKey: selectedSessionKey,
1153
+ });
1154
+ showToast("Debug request sent to agent", "success");
1155
+ return true;
1156
+ } catch (err) {
1157
+ showToast(err.message || "Could not send debug request", "error");
1158
+ return false;
1159
+ }
1160
+ }}
1161
+ />
1061
1162
  </div>
1062
1163
  `;
1063
1164
  };
@@ -6,6 +6,7 @@ const { parsePositiveInt } = require("./utils/number");
6
6
  // Portable root directory: --root-dir flag sets ALPHACLAW_ROOT_DIR before require
7
7
  const kRootDir =
8
8
  process.env.ALPHACLAW_ROOT_DIR || path.join(os.homedir(), ".alphaclaw");
9
+ const ALPHACLAW_DIR = kRootDir;
9
10
  const kPackageRoot = path.resolve(__dirname, "..");
10
11
  const kNpmPackageRoot = path.resolve(kPackageRoot, "..");
11
12
  const kSetupDir = path.join(kPackageRoot, "setup");
@@ -18,6 +19,8 @@ const OPENCLAW_DIR = path.join(kRootDir, ".openclaw");
18
19
  const GATEWAY_TOKEN = process.env.OPENCLAW_GATEWAY_TOKEN || "";
19
20
  const ENV_FILE_PATH = path.join(kRootDir, ".env");
20
21
  const WORKSPACE_DIR = path.join(OPENCLAW_DIR, "workspace");
22
+ const kOnboardingMarkerPath = path.join(ALPHACLAW_DIR, "onboarded.json");
23
+ const kControlUiSkillPath = path.join(OPENCLAW_DIR, "skills", "control-ui", "SKILL.md");
21
24
  const AUTH_PROFILES_PATH = path.join(
22
25
  OPENCLAW_DIR,
23
26
  "agents",
@@ -346,6 +349,7 @@ const SETUP_API_PREFIXES = [
346
349
  ];
347
350
 
348
351
  module.exports = {
352
+ ALPHACLAW_DIR,
349
353
  kRootDir,
350
354
  kPackageRoot,
351
355
  kNpmPackageRoot,
@@ -358,6 +362,8 @@ module.exports = {
358
362
  GATEWAY_TOKEN,
359
363
  ENV_FILE_PATH,
360
364
  WORKSPACE_DIR,
365
+ kOnboardingMarkerPath,
366
+ kControlUiSkillPath,
361
367
  AUTH_PROFILES_PATH,
362
368
  CODEX_PROFILE_ID,
363
369
  CODEX_OAUTH_CLIENT_ID,
@@ -1,3 +1,4 @@
1
+ const path = require("path");
1
2
  const { spawn, execSync } = require("child_process");
2
3
  const fs = require("fs");
3
4
  const net = require("net");
@@ -5,7 +6,9 @@ const {
5
6
  OPENCLAW_DIR,
6
7
  GATEWAY_HOST,
7
8
  GATEWAY_PORT,
9
+ kControlUiSkillPath,
8
10
  kChannelDefs,
11
+ kOnboardingMarkerPath,
9
12
  kRootDir,
10
13
  } = require("./constants");
11
14
 
@@ -45,7 +48,7 @@ const gatewayEnv = () => ({
45
48
  XDG_CONFIG_HOME: OPENCLAW_DIR,
46
49
  });
47
50
 
48
- const isOnboarded = () => {
51
+ const hasOnboardingModelConfig = () => {
49
52
  const configPath = `${OPENCLAW_DIR}/openclaw.json`;
50
53
  if (!fs.existsSync(configPath)) return false;
51
54
  try {
@@ -59,6 +62,43 @@ const isOnboarded = () => {
59
62
  }
60
63
  };
61
64
 
65
+ const hasLegacyOnboardingArtifacts = () => fs.existsSync(kControlUiSkillPath);
66
+
67
+ const writeOnboardingMarker = (reason) => {
68
+ try {
69
+ fs.mkdirSync(path.dirname(kOnboardingMarkerPath), { recursive: true });
70
+ fs.writeFileSync(
71
+ kOnboardingMarkerPath,
72
+ JSON.stringify(
73
+ {
74
+ onboarded: true,
75
+ reason: String(reason || "unknown"),
76
+ markedAt: new Date().toISOString(),
77
+ },
78
+ null,
79
+ 2,
80
+ ),
81
+ );
82
+ return true;
83
+ } catch (err) {
84
+ console.error(`[alphaclaw] Failed to write onboarding marker: ${err.message}`);
85
+ return false;
86
+ }
87
+ };
88
+
89
+ const isOnboarded = () => {
90
+ if (fs.existsSync(kOnboardingMarkerPath)) return true;
91
+ if (hasOnboardingModelConfig()) {
92
+ writeOnboardingMarker("config_primary_model");
93
+ return true;
94
+ }
95
+ if (hasLegacyOnboardingArtifacts()) {
96
+ writeOnboardingMarker("legacy_artifact_backfill");
97
+ return true;
98
+ }
99
+ return false;
100
+ };
101
+
62
102
  const isGatewayRunning = () =>
63
103
  new Promise((resolve) => {
64
104
  const sock = net.createConnection(GATEWAY_PORT, GATEWAY_HOST);
@@ -70,19 +70,22 @@ const ensureTopicPathForClient = ({
70
70
  const normalizedClient = String(client || "default").trim() || "default";
71
71
  const push = getGmailPushConfig(state);
72
72
  const existingTopic = String(push.topics?.[normalizedClient] || "").trim();
73
- if (existingTopic) {
73
+ const requestedProjectId = String(projectIdOverride || "").trim();
74
+ const existingProjectId = parseProjectIdFromTopicPath(existingTopic);
75
+ if (existingTopic && (!requestedProjectId || requestedProjectId === existingProjectId)) {
74
76
  return { state, topicPath: existingTopic };
75
77
  }
76
78
  const credentials = readGoogleCredentials(normalizedClient);
77
79
  const projectId =
78
- String(projectIdOverride || "").trim() ||
80
+ requestedProjectId ||
79
81
  String(credentials?.projectId || "").trim();
80
82
  if (!projectId) {
81
83
  throw new Error(
82
84
  `Could not detect GCP project_id for client "${normalizedClient}". Save Google credentials first.`,
83
85
  );
84
86
  }
85
- const topicName = createTopicNameForClient(normalizedClient);
87
+ const topicName =
88
+ parseTopicName(existingTopic) || createTopicNameForClient(normalizedClient);
86
89
  const topicPath = `projects/${projectId}/topics/${topicName}`;
87
90
  const updated = setGmailPushConfig({
88
91
  state,
@@ -34,7 +34,7 @@ const createOnboardingService = ({
34
34
  getBaseUrl,
35
35
  startGateway,
36
36
  }) => {
37
- const { OPENCLAW_DIR, WORKSPACE_DIR } = constants;
37
+ const { OPENCLAW_DIR, WORKSPACE_DIR, kOnboardingMarkerPath } = constants;
38
38
 
39
39
  const verifyGithubSetup = async ({
40
40
  githubRepoInput,
@@ -160,6 +160,19 @@ const createOnboardingService = ({
160
160
 
161
161
  installHourlyGitSyncScript({ fs, openclawDir: OPENCLAW_DIR });
162
162
  await installHourlyGitSyncCron({ fs, openclawDir: OPENCLAW_DIR });
163
+ fs.mkdirSync(path.dirname(kOnboardingMarkerPath), { recursive: true });
164
+ fs.writeFileSync(
165
+ kOnboardingMarkerPath,
166
+ JSON.stringify(
167
+ {
168
+ onboarded: true,
169
+ reason: "onboarding_complete",
170
+ markedAt: new Date().toISOString(),
171
+ },
172
+ null,
173
+ 2,
174
+ ),
175
+ );
163
176
 
164
177
  try {
165
178
  await shellCmd(`alphaclaw git-sync -m "initial setup"`, {
@@ -17,7 +17,6 @@ const registerSystemRoutes = ({
17
17
  alphaclawVersionService,
18
18
  clawCmd,
19
19
  restartGateway,
20
- onExpectedGatewayRestart,
21
20
  OPENCLAW_DIR,
22
21
  restartRequiredState,
23
22
  topicRegistry,
@@ -59,7 +58,9 @@ const registerSystemRoutes = ({
59
58
  const parseJsonFromStdout = (stdout) => {
60
59
  const raw = String(stdout || "").trim();
61
60
  if (!raw) return null;
62
- const candidateStarts = [raw.indexOf("{"), raw.indexOf("[")].filter((idx) => idx >= 0);
61
+ const candidateStarts = [raw.indexOf("{"), raw.indexOf("[")].filter(
62
+ (idx) => idx >= 0,
63
+ );
63
64
  for (const start of candidateStarts) {
64
65
  const candidate = raw.slice(start);
65
66
  try {
@@ -75,7 +76,9 @@ const registerSystemRoutes = ({
75
76
  if (telegramMatch) {
76
77
  return `Telegram ${telegramMatch[1]}`;
77
78
  }
78
- const telegramTopicMatch = key.match(/:telegram:group:([^:]+):topic:([^:]+)$/);
79
+ const telegramTopicMatch = key.match(
80
+ /:telegram:group:([^:]+):topic:([^:]+)$/,
81
+ );
79
82
  if (telegramTopicMatch) {
80
83
  const [, groupId, topicId] = telegramTopicMatch;
81
84
  let groupEntry = null;
@@ -83,7 +86,9 @@ const registerSystemRoutes = ({
83
86
  groupEntry = topicRegistry?.getGroup?.(groupId) || null;
84
87
  } catch {}
85
88
  const groupName = String(groupEntry?.name || "").trim();
86
- const topicName = String(groupEntry?.topics?.[topicId]?.name || "").trim();
89
+ const topicName = String(
90
+ groupEntry?.topics?.[topicId]?.name || "",
91
+ ).trim();
87
92
  if (groupName && topicName) return `Telegram ${groupName} · ${topicName}`;
88
93
  if (topicName) return `Telegram Topic ${topicName}`;
89
94
  return `Telegram Topic ${topicId}`;
@@ -266,13 +271,19 @@ const registerSystemRoutes = ({
266
271
  );
267
272
  const hiddenKnownVarKeys = new Set(
268
273
  kKnownVars
269
- .filter((def) => !isReservedUserEnvVar(def.key) && !isVisibleInEnvars(def))
274
+ .filter(
275
+ (def) => !isReservedUserEnvVar(def.key) && !isVisibleInEnvars(def),
276
+ )
270
277
  .map((def) => def.key),
271
278
  );
272
279
  const existingHiddenKnownVars = readEnvFile().filter((v) =>
273
280
  hiddenKnownVarKeys.has(v.key),
274
281
  );
275
- const nextEnvVars = [...filtered, ...existingHiddenKnownVars, ...existingLockedVars];
282
+ const nextEnvVars = [
283
+ ...filtered,
284
+ ...existingHiddenKnownVars,
285
+ ...existingLockedVars,
286
+ ];
276
287
  syncChannelConfig(nextEnvVars, "remove");
277
288
  writeEnvFile(nextEnvVars);
278
289
  const changed = reloadEnv();
@@ -402,12 +413,15 @@ const registerSystemRoutes = ({
402
413
  let selectedSession = null;
403
414
  try {
404
415
  const sessions = await listSendableAgentSessions();
405
- selectedSession = sessions.find((sessionRow) => sessionRow.key === sessionKey) || null;
416
+ selectedSession =
417
+ sessions.find((sessionRow) => sessionRow.key === sessionKey) || null;
406
418
  } catch (err) {
407
419
  return res.status(502).json({ ok: false, error: err.message });
408
420
  }
409
421
  if (!selectedSession) {
410
- return res.status(400).json({ ok: false, error: "Selected session was not found" });
422
+ return res
423
+ .status(400)
424
+ .json({ ok: false, error: "Selected session was not found" });
411
425
  }
412
426
  if (selectedSession.replyChannel && selectedSession.replyTo) {
413
427
  command +=
@@ -421,7 +435,10 @@ const registerSystemRoutes = ({
421
435
  if (!result.ok) {
422
436
  return res
423
437
  .status(502)
424
- .json({ ok: false, error: result.stderr || "Could not send message to agent" });
438
+ .json({
439
+ ok: false,
440
+ error: result.stderr || "Could not send message to agent",
441
+ });
425
442
  }
426
443
  return res.json({ ok: true, stdout: result.stdout || "" });
427
444
  });
@@ -458,9 +475,6 @@ const registerSystemRoutes = ({
458
475
  }
459
476
  restartRequiredState.markRestartInProgress();
460
477
  try {
461
- if (typeof onExpectedGatewayRestart === "function") {
462
- onExpectedGatewayRestart();
463
- }
464
478
  restartGateway();
465
479
  envRestartPending = false;
466
480
  restartRequiredState.clearRequired();
@@ -9,7 +9,7 @@ const {
9
9
 
10
10
  const kHealthStartupGraceMs = 30 * 1000;
11
11
  const kBootstrapHealthCheckMs = 5 * 1000;
12
- const kExpectedRestartWindowMs = 45 * 1000;
12
+ const kExpectedRestartWindowMs = 15 * 1000;
13
13
 
14
14
  const isTruthy = (value) =>
15
15
  ["1", "true", "yes", "on"].includes(String(value || "").trim().toLowerCase());
@@ -506,7 +506,7 @@ const createWatchdog = ({
506
506
  const onGatewayExit = ({ code, signal, expectedExit = false, stderrTail = [] } = {}) => {
507
507
  const correlationId = createCorrelationId();
508
508
  clearDegradedHealthCheckTimer();
509
- if (expectedExit) {
509
+ if (expectedExit && (code == null || code === 0)) {
510
510
  state.lifecycle = "restarting";
511
511
  state.health = "unknown";
512
512
  state.crashRecoveryActive = false;
package/lib/server.js CHANGED
@@ -242,7 +242,6 @@ registerSystemRoutes({
242
242
  alphaclawVersionService,
243
243
  clawCmd,
244
244
  restartGateway,
245
- onExpectedGatewayRestart: () => watchdog.onExpectedRestart(),
246
245
  OPENCLAW_DIR: constants.OPENCLAW_DIR,
247
246
  restartRequiredState,
248
247
  topicRegistry,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.4.6-beta.6",
3
+ "version": "0.4.6-beta.7",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },