@chrysb/alphaclaw 0.8.3-beta.0 → 0.8.3-beta.2
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 +1 -1
- package/lib/public/dist/app.bundle.js +1045 -1045
- package/lib/public/js/components/routes/chat-route.js +4 -4
- package/lib/public/js/hooks/use-browse-navigation.js +4 -4
- package/lib/server/chat-ws.js +40 -3
- package/lib/server/constants.js +4 -0
- package/lib/server/openclaw-version.js +2 -1
- package/lib/server/routes/proxy.js +2 -2
- package/lib/server/slack-api.js +211 -2
- package/lib/server/watchdog-notify.js +52 -2
- package/lib/server/watchdog.js +9 -4
- package/package.json +8 -5
- package/patches/openclaw+2026.3.28.patch +13 -0
- /package/lib/public/dist/chunks/{addon-fit-W4YZGRNV.js → addon-fit-4LH2IIZ4.js} +0 -0
- /package/lib/public/dist/chunks/{xterm-KOX4YMOF.js → xterm-DK3X7FZB.js} +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { h } from "
|
|
1
|
+
import { h } from "preact";
|
|
2
2
|
import {
|
|
3
3
|
useCallback,
|
|
4
4
|
useEffect,
|
|
5
5
|
useMemo,
|
|
6
6
|
useRef,
|
|
7
7
|
useState,
|
|
8
|
-
} from "
|
|
9
|
-
import htm from "
|
|
10
|
-
import { marked } from "
|
|
8
|
+
} from "preact/hooks";
|
|
9
|
+
import htm from "htm";
|
|
10
|
+
import { marked } from "marked";
|
|
11
11
|
import { authFetch } from "../../lib/api.js";
|
|
12
12
|
import { kChatSessionDraftsStorageKey } from "../../lib/storage-keys.js";
|
|
13
13
|
import { showToast } from "../toast.js";
|
|
@@ -56,12 +56,13 @@ export const useBrowseNavigation = ({
|
|
|
56
56
|
location,
|
|
57
57
|
});
|
|
58
58
|
|
|
59
|
+
// Derive sidebar tab only from `location`. Avoid optimistic setSidebarTab + this effect
|
|
60
|
+
// fighting (e.g. chat tab selected while hash is still /general → pane never mounts).
|
|
59
61
|
useEffect(() => {
|
|
60
|
-
setSidebarTab((
|
|
62
|
+
setSidebarTab(() => {
|
|
61
63
|
if (location.startsWith("/browse")) return "browse";
|
|
62
64
|
if (location.startsWith("/chat")) return "chat";
|
|
63
|
-
|
|
64
|
-
return currentTab;
|
|
65
|
+
return "menu";
|
|
65
66
|
});
|
|
66
67
|
}, [location]);
|
|
67
68
|
|
|
@@ -148,7 +149,6 @@ export const useBrowseNavigation = ({
|
|
|
148
149
|
}, [browsePreviewPath, onCloseMobileSidebar, selectedBrowsePath, setLocation]);
|
|
149
150
|
|
|
150
151
|
const handleSelectSidebarTab = useCallback((nextTab) => {
|
|
151
|
-
setSidebarTab(nextTab);
|
|
152
152
|
if (nextTab === "menu" && location.startsWith("/browse")) {
|
|
153
153
|
setBrowsePreviewPath("");
|
|
154
154
|
setLocation(lastMenuRoute || `/${kDefaultUiTab}`);
|
package/lib/server/chat-ws.js
CHANGED
|
@@ -7,7 +7,15 @@ const kConnectTimeoutMs = 8000;
|
|
|
7
7
|
const kHistoryTimeoutMs = 12000;
|
|
8
8
|
const kGatewayReqTimeoutMs = 15000;
|
|
9
9
|
const kGatewayProtocolVersion = 3;
|
|
10
|
-
|
|
10
|
+
// Gateway method auth (see OpenClaw method-scopes): chat.history needs operator.read;
|
|
11
|
+
// chat.send / chat.abort need operator.write. Align with CLI_DEFAULT_OPERATOR_SCOPES plus admin.
|
|
12
|
+
const kGatewayChatBridgeScopes = [
|
|
13
|
+
"operator.admin",
|
|
14
|
+
"operator.read",
|
|
15
|
+
"operator.write",
|
|
16
|
+
"operator.approvals",
|
|
17
|
+
"operator.pairing",
|
|
18
|
+
];
|
|
11
19
|
|
|
12
20
|
const collectHistoryTextFragments = (value) => {
|
|
13
21
|
if (typeof value === "string") {
|
|
@@ -252,9 +260,35 @@ const resolveSessionKeyFromPayload = (payload = {}) =>
|
|
|
252
260
|
|
|
253
261
|
const sanitizeError = (error) => {
|
|
254
262
|
const message = error instanceof Error ? error.message : String(error || "");
|
|
255
|
-
|
|
263
|
+
const lower = message.toLowerCase();
|
|
264
|
+
console.error(`[alphaclaw] chat websocket handler error: ${message}`);
|
|
265
|
+
if (lower.includes("not connected")) {
|
|
256
266
|
return "Agent runtime is not connected right now.";
|
|
257
267
|
}
|
|
268
|
+
if (
|
|
269
|
+
lower.includes("gateway is not connected") ||
|
|
270
|
+
lower.includes("econnrefused") ||
|
|
271
|
+
lower.includes("connect failed")
|
|
272
|
+
) {
|
|
273
|
+
return "Could not connect to the OpenClaw gateway. Check that the gateway is running and reachable.";
|
|
274
|
+
}
|
|
275
|
+
if (lower.includes("timed out") || lower.includes("timeout")) {
|
|
276
|
+
return "The gateway did not respond in time. Try again after the gateway finishes starting.";
|
|
277
|
+
}
|
|
278
|
+
if (
|
|
279
|
+
lower.includes("auth") ||
|
|
280
|
+
lower.includes("token") ||
|
|
281
|
+
lower.includes("unauthorized") ||
|
|
282
|
+
lower.includes("forbidden")
|
|
283
|
+
) {
|
|
284
|
+
return "Gateway authentication failed. Verify OPENCLAW_GATEWAY_TOKEN matches the gateway.";
|
|
285
|
+
}
|
|
286
|
+
if (lower.includes("method not found") || lower.includes("unknown method")) {
|
|
287
|
+
return "This gateway build does not support chat APIs. Update OpenClaw.";
|
|
288
|
+
}
|
|
289
|
+
if (lower.includes("gateway request failed")) {
|
|
290
|
+
return "The gateway could not start this chat run. Check gateway logs.";
|
|
291
|
+
}
|
|
258
292
|
return "Something went wrong. Please try again.";
|
|
259
293
|
};
|
|
260
294
|
|
|
@@ -522,7 +556,7 @@ const createChatWsService = ({
|
|
|
522
556
|
mode: "backend",
|
|
523
557
|
},
|
|
524
558
|
role: "operator",
|
|
525
|
-
scopes:
|
|
559
|
+
scopes: kGatewayChatBridgeScopes,
|
|
526
560
|
caps: [],
|
|
527
561
|
commands: [],
|
|
528
562
|
permissions: {},
|
|
@@ -733,9 +767,12 @@ const createChatWsService = ({
|
|
|
733
767
|
}
|
|
734
768
|
};
|
|
735
769
|
run().catch((err) => {
|
|
770
|
+
const sessionKey = String(payload?.sessionKey || "").trim();
|
|
736
771
|
sendJson(ws, {
|
|
737
772
|
type: "error",
|
|
738
773
|
message: sanitizeError(err),
|
|
774
|
+
...(sessionKey ? { sessionKey } : {}),
|
|
775
|
+
messageId: crypto.randomUUID(),
|
|
739
776
|
});
|
|
740
777
|
});
|
|
741
778
|
});
|
package/lib/server/constants.js
CHANGED
|
@@ -135,6 +135,8 @@ const kFallbackOnboardingModels = [
|
|
|
135
135
|
|
|
136
136
|
const kVersionCacheTtlMs = 60 * 1000;
|
|
137
137
|
const kLatestVersionCacheTtlMs = 10 * 60 * 1000;
|
|
138
|
+
/** `cp` of a full openclaw npm tree into /app/node_modules can exceed 60s on slow volumes. */
|
|
139
|
+
const kOpenclawUpdateCopyTimeoutMs = 5 * 60 * 1000;
|
|
138
140
|
const kOpenclawRegistryUrl = "https://registry.npmjs.org/openclaw";
|
|
139
141
|
const kAlphaclawRegistryUrl = "https://registry.npmjs.org/@chrysb%2falphaclaw";
|
|
140
142
|
const kAlphaclawGithubReleasesBaseUrl =
|
|
@@ -370,6 +372,7 @@ const SETUP_API_PREFIXES = [
|
|
|
370
372
|
"/api/codex",
|
|
371
373
|
"/api/models",
|
|
372
374
|
"/api/browse",
|
|
375
|
+
"/api/chat",
|
|
373
376
|
"/api/gateway",
|
|
374
377
|
"/api/restart-status",
|
|
375
378
|
"/api/onboard",
|
|
@@ -425,6 +428,7 @@ module.exports = {
|
|
|
425
428
|
kFallbackOnboardingModels,
|
|
426
429
|
kVersionCacheTtlMs,
|
|
427
430
|
kLatestVersionCacheTtlMs,
|
|
431
|
+
kOpenclawUpdateCopyTimeoutMs,
|
|
428
432
|
kOpenclawRegistryUrl,
|
|
429
433
|
kAlphaclawRegistryUrl,
|
|
430
434
|
kAlphaclawGithubReleasesBaseUrl,
|
|
@@ -6,6 +6,7 @@ const {
|
|
|
6
6
|
kVersionCacheTtlMs,
|
|
7
7
|
kLatestVersionCacheTtlMs,
|
|
8
8
|
kNpmPackageRoot,
|
|
9
|
+
kOpenclawUpdateCopyTimeoutMs,
|
|
9
10
|
} = require("./constants");
|
|
10
11
|
const { normalizeOpenclawVersion } = require("./helpers");
|
|
11
12
|
const { parseJsonObjectFromNoisyOutput } = require("./utils/json");
|
|
@@ -174,7 +175,7 @@ const createOpenclawVersionService = ({
|
|
|
174
175
|
const dest = path.join(installDir, "node_modules");
|
|
175
176
|
exec(
|
|
176
177
|
`cp -af "${src}/." "${dest}/"`,
|
|
177
|
-
{ timeout:
|
|
178
|
+
{ timeout: kOpenclawUpdateCopyTimeoutMs },
|
|
178
179
|
(cpErr) => {
|
|
179
180
|
cleanup();
|
|
180
181
|
if (cpErr) {
|
|
@@ -29,8 +29,8 @@ const registerProxyRoutes = ({
|
|
|
29
29
|
app.all(kHooksPathPattern, webhookMiddleware);
|
|
30
30
|
app.all(kWebhookPathPattern, webhookMiddleware);
|
|
31
31
|
|
|
32
|
-
app.all(kApiPathPattern, (req, res) => {
|
|
33
|
-
if (SETUP_API_PREFIXES.some((p) => req.path.startsWith(p))) return;
|
|
32
|
+
app.all(kApiPathPattern, (req, res, next) => {
|
|
33
|
+
if (SETUP_API_PREFIXES.some((p) => req.path.startsWith(p))) return next();
|
|
34
34
|
proxy.web(req, res, { target: getGatewayUrl() });
|
|
35
35
|
});
|
|
36
36
|
};
|
package/lib/server/slack-api.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
const kSlackApiBase = "https://slack.com/api";
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const { Readable } = require("stream");
|
|
4
|
+
const { Blob } = require("buffer");
|
|
2
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Create Slack API client with enhanced features:
|
|
8
|
+
* - Threading support
|
|
9
|
+
* - Reactions
|
|
10
|
+
* - File uploads
|
|
11
|
+
* - Backward compatible with existing code
|
|
12
|
+
*/
|
|
3
13
|
const createSlackApi = (getToken) => {
|
|
4
14
|
const call = async (method, body = {}) => {
|
|
5
15
|
const token = typeof getToken === "function" ? getToken() : getToken;
|
|
@@ -24,14 +34,213 @@ const createSlackApi = (getToken) => {
|
|
|
24
34
|
return data;
|
|
25
35
|
};
|
|
26
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Convert various file input types to Buffer
|
|
39
|
+
*/
|
|
40
|
+
const toBuffer = async (content) => {
|
|
41
|
+
if (Buffer.isBuffer(content)) {
|
|
42
|
+
return content;
|
|
43
|
+
} else if (content instanceof Readable) {
|
|
44
|
+
const chunks = [];
|
|
45
|
+
for await (const chunk of content) {
|
|
46
|
+
chunks.push(chunk);
|
|
47
|
+
}
|
|
48
|
+
return Buffer.concat(chunks);
|
|
49
|
+
} else if (typeof content === "string" && fs.existsSync(content)) {
|
|
50
|
+
return fs.readFileSync(content);
|
|
51
|
+
} else {
|
|
52
|
+
throw new Error("Invalid file content: must be Buffer, Stream, or file path");
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Verify Slack credentials
|
|
58
|
+
*/
|
|
27
59
|
const authTest = () => call("auth.test");
|
|
28
60
|
|
|
29
|
-
|
|
30
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Send a message to a channel or DM
|
|
63
|
+
* @param {string} channel - Channel ID or user ID
|
|
64
|
+
* @param {string} text - Message text
|
|
65
|
+
* @param {object} opts - Options
|
|
66
|
+
* @param {string} opts.thread_ts - Thread timestamp (for threaded replies)
|
|
67
|
+
* @param {boolean} opts.reply_broadcast - Also send to channel (when in thread)
|
|
68
|
+
* @param {boolean} opts.mrkdwn - Enable Slack markdown formatting (default: true)
|
|
69
|
+
* @returns {Promise<object>} Response with ts (message timestamp)
|
|
70
|
+
*/
|
|
71
|
+
const postMessage = (channel, text, opts = {}) => {
|
|
72
|
+
const payload = {
|
|
73
|
+
channel,
|
|
74
|
+
text: String(text || ""),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Threading support
|
|
78
|
+
if (opts.thread_ts) {
|
|
79
|
+
payload.thread_ts = opts.thread_ts;
|
|
80
|
+
}
|
|
81
|
+
if (opts.reply_broadcast) {
|
|
82
|
+
payload.reply_broadcast = true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Formatting
|
|
86
|
+
if (opts.mrkdwn !== false) {
|
|
87
|
+
payload.mrkdwn = true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return call("chat.postMessage", payload);
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Post a message in a thread (convenience wrapper)
|
|
95
|
+
* @param {string} channel - Channel ID
|
|
96
|
+
* @param {string} threadTs - Thread timestamp
|
|
97
|
+
* @param {string} text - Message text
|
|
98
|
+
* @param {object} opts - Additional options (reply_broadcast, etc.)
|
|
99
|
+
*/
|
|
100
|
+
const postMessageInThread = (channel, threadTs, text, opts = {}) => {
|
|
101
|
+
return postMessage(channel, text, { ...opts, thread_ts: threadTs });
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Add a reaction emoji to a message
|
|
106
|
+
* @param {string} channel - Channel ID
|
|
107
|
+
* @param {string} timestamp - Message timestamp
|
|
108
|
+
* @param {string} emoji - Emoji name (without colons, e.g., "white_check_mark")
|
|
109
|
+
*/
|
|
110
|
+
const addReaction = (channel, timestamp, emoji) => {
|
|
111
|
+
// Remove colons if user included them
|
|
112
|
+
const cleanEmoji = String(emoji || "").replace(/^:|:$/g, "");
|
|
113
|
+
return call("reactions.add", {
|
|
114
|
+
channel,
|
|
115
|
+
timestamp,
|
|
116
|
+
name: cleanEmoji,
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Remove a reaction emoji from a message
|
|
122
|
+
* @param {string} channel - Channel ID
|
|
123
|
+
* @param {string} timestamp - Message timestamp
|
|
124
|
+
* @param {string} emoji - Emoji name (without colons)
|
|
125
|
+
*/
|
|
126
|
+
const removeReaction = (channel, timestamp, emoji) => {
|
|
127
|
+
const cleanEmoji = String(emoji || "").replace(/^:|:$/g, "");
|
|
128
|
+
return call("reactions.remove", {
|
|
129
|
+
channel,
|
|
130
|
+
timestamp,
|
|
131
|
+
name: cleanEmoji,
|
|
132
|
+
});
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Upload a file to Slack using the 3-step external upload flow
|
|
137
|
+
* @param {string|string[]} channels - Channel ID(s) to share file in
|
|
138
|
+
* @param {Buffer|Stream|string} fileContent - File content (Buffer, Stream, or file path)
|
|
139
|
+
* @param {object} opts - Options
|
|
140
|
+
* @param {string} opts.filename - Filename
|
|
141
|
+
* @param {string} opts.title - File title
|
|
142
|
+
* @param {string} opts.initial_comment - Comment to add with file
|
|
143
|
+
* @param {string} opts.thread_ts - Thread timestamp (upload to thread)
|
|
144
|
+
* @param {string} opts.contentType - MIME type
|
|
145
|
+
* @returns {Promise<object>} Upload response with file info
|
|
146
|
+
*/
|
|
147
|
+
const uploadFile = async (channels, fileContent, opts = {}) => {
|
|
148
|
+
const filename = opts.filename || "file";
|
|
149
|
+
const buffer = await toBuffer(fileContent);
|
|
150
|
+
const filesize = buffer.length;
|
|
151
|
+
|
|
152
|
+
// Step 1: Get upload URL
|
|
153
|
+
const uploadInfo = await call("files.getUploadURLExternal", {
|
|
154
|
+
filename,
|
|
155
|
+
length: filesize,
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const { upload_url, file_id } = uploadInfo;
|
|
159
|
+
|
|
160
|
+
// Step 2: Upload file to the external URL (raw POST, no auth)
|
|
161
|
+
const uploadRes = await fetch(upload_url, {
|
|
162
|
+
method: "POST",
|
|
163
|
+
headers: {
|
|
164
|
+
"Content-Type": opts.contentType || "application/octet-stream",
|
|
165
|
+
},
|
|
166
|
+
body: buffer,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (!uploadRes.ok) {
|
|
170
|
+
throw new Error(`File upload to external URL failed: HTTP ${uploadRes.status}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Step 3: Complete the upload and share to channel(s)
|
|
174
|
+
const completePayload = {
|
|
175
|
+
files: [
|
|
176
|
+
{
|
|
177
|
+
id: file_id,
|
|
178
|
+
title: opts.title || filename,
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
// Handle single channel vs multiple channels
|
|
184
|
+
if (channels) {
|
|
185
|
+
if (Array.isArray(channels)) {
|
|
186
|
+
completePayload.channel_id = channels[0]; // Primary channel
|
|
187
|
+
if (channels.length > 1) {
|
|
188
|
+
throw new Error("Multi-channel upload not supported with external upload flow. Use channel_id for one channel.");
|
|
189
|
+
}
|
|
190
|
+
} else {
|
|
191
|
+
completePayload.channel_id = channels;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (opts.initial_comment) {
|
|
196
|
+
completePayload.initial_comment = opts.initial_comment;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (opts.thread_ts) {
|
|
200
|
+
completePayload.thread_ts = opts.thread_ts;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return call("files.completeUploadExternal", completePayload);
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Upload text as a code snippet with syntax highlighting
|
|
208
|
+
* @param {string|string[]} channels - Channel ID(s)
|
|
209
|
+
* @param {string} content - Text content
|
|
210
|
+
* @param {object} opts - Options
|
|
211
|
+
* @param {string} opts.filename - Filename (affects syntax highlighting, e.g., "code.js")
|
|
212
|
+
* @param {string} opts.title - Snippet title
|
|
213
|
+
* @param {string} opts.filetype - File type for syntax highlighting (e.g., "javascript")
|
|
214
|
+
* @param {string} opts.initial_comment - Comment
|
|
215
|
+
* @param {string} opts.thread_ts - Thread timestamp
|
|
216
|
+
*/
|
|
217
|
+
const uploadTextSnippet = (channels, content, opts = {}) => {
|
|
218
|
+
const buffer = Buffer.from(String(content || ""), "utf8");
|
|
219
|
+
|
|
220
|
+
// Detect language from filename if provided
|
|
221
|
+
let filename = opts.filename || "snippet.txt";
|
|
222
|
+
if (opts.filetype) {
|
|
223
|
+
const ext = opts.filetype.replace(/^\./, "");
|
|
224
|
+
if (!filename.includes(".")) {
|
|
225
|
+
filename = `snippet.${ext}`;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return uploadFile(channels, buffer, {
|
|
230
|
+
...opts,
|
|
231
|
+
filename,
|
|
232
|
+
contentType: "text/plain",
|
|
233
|
+
});
|
|
234
|
+
};
|
|
31
235
|
|
|
32
236
|
return {
|
|
33
237
|
authTest,
|
|
34
238
|
postMessage,
|
|
239
|
+
postMessageInThread,
|
|
240
|
+
addReaction,
|
|
241
|
+
removeReaction,
|
|
242
|
+
uploadFile,
|
|
243
|
+
uploadTextSnippet,
|
|
35
244
|
};
|
|
36
245
|
};
|
|
37
246
|
|
|
@@ -36,8 +36,14 @@ const getPairedIds = (channel) => {
|
|
|
36
36
|
const formatDiscordMessage = (message) =>
|
|
37
37
|
String(message || "").replace(/(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g, "**$1**");
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Track thread state for Slack notifications
|
|
41
|
+
* Key: userId, Value: { threadTs, lastEvent }
|
|
42
|
+
*/
|
|
43
|
+
const slackThreads = new Map();
|
|
44
|
+
|
|
39
45
|
const createWatchdogNotifier = ({ telegramApi, discordApi, slackApi }) => {
|
|
40
|
-
const notify = async (message) => {
|
|
46
|
+
const notify = async (message, opts = {}) => {
|
|
41
47
|
const summary = {
|
|
42
48
|
telegram: { sent: 0, failed: 0, skipped: false, targets: 0 },
|
|
43
49
|
discord: { sent: 0, failed: 0, skipped: false, targets: 0 },
|
|
@@ -78,14 +84,58 @@ const createWatchdogNotifier = ({ telegramApi, discordApi, slackApi }) => {
|
|
|
78
84
|
}
|
|
79
85
|
}
|
|
80
86
|
|
|
87
|
+
// Enhanced Slack notifications with threading and reactions
|
|
81
88
|
const slackTargets = getPairedIds("slack");
|
|
82
89
|
summary.slack.targets = slackTargets.length;
|
|
83
90
|
if (!slackApi?.postMessage || !process.env.SLACK_BOT_TOKEN || slackTargets.length === 0) {
|
|
84
91
|
summary.slack.skipped = true;
|
|
85
92
|
} else {
|
|
93
|
+
const eventType = opts.eventType || "info"; // crash, recovery, health, info
|
|
94
|
+
|
|
86
95
|
for (const userId of slackTargets) {
|
|
87
96
|
try {
|
|
88
|
-
|
|
97
|
+
let threadTs = null;
|
|
98
|
+
let shouldCreateNewThread = true;
|
|
99
|
+
|
|
100
|
+
// Check if we have an active thread for this user
|
|
101
|
+
const existingThread = slackThreads.get(userId);
|
|
102
|
+
if (existingThread && existingThread.lastEvent === "crash" && eventType === "recovery") {
|
|
103
|
+
// Recovery message goes in the crash thread
|
|
104
|
+
threadTs = existingThread.threadTs;
|
|
105
|
+
shouldCreateNewThread = false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Send message (in thread if continuing conversation)
|
|
109
|
+
const result = await slackApi.postMessage(userId, String(message || ""), {
|
|
110
|
+
thread_ts: threadTs,
|
|
111
|
+
mrkdwn: true,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Store thread for future related messages
|
|
115
|
+
if (shouldCreateNewThread && result.ts) {
|
|
116
|
+
slackThreads.set(userId, {
|
|
117
|
+
threadTs: result.ts,
|
|
118
|
+
lastEvent: eventType,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Add reactions based on event type
|
|
123
|
+
// Use result.channel (the actual conversation/DM ID) instead of userId
|
|
124
|
+
if (result.ts && result.channel && slackApi.addReaction) {
|
|
125
|
+
try {
|
|
126
|
+
if (eventType === "crash") {
|
|
127
|
+
await slackApi.addReaction(result.channel, result.ts, "x");
|
|
128
|
+
} else if (eventType === "recovery") {
|
|
129
|
+
await slackApi.addReaction(result.channel, result.ts, "white_check_mark");
|
|
130
|
+
} else if (eventType === "health") {
|
|
131
|
+
await slackApi.addReaction(result.channel, result.ts, "heart");
|
|
132
|
+
}
|
|
133
|
+
} catch (reactionErr) {
|
|
134
|
+
// Reactions are nice-to-have, don't fail the whole notification
|
|
135
|
+
console.error(`[watchdog] slack reaction failed for ${userId}: ${reactionErr.message}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
89
139
|
summary.slack.sent += 1;
|
|
90
140
|
} catch (err) {
|
|
91
141
|
summary.slack.failed += 1;
|
package/lib/server/watchdog.js
CHANGED
|
@@ -184,12 +184,12 @@ const createWatchdog = ({
|
|
|
184
184
|
}
|
|
185
185
|
};
|
|
186
186
|
|
|
187
|
-
const notify = async (message, correlationId = "") => {
|
|
187
|
+
const notify = async (message, correlationId = "", eventType = "info") => {
|
|
188
188
|
if (state.notificationsDisabled) {
|
|
189
189
|
return { ok: false, skipped: true, reason: "notifications_disabled" };
|
|
190
190
|
}
|
|
191
191
|
if (!notifier?.notify) return { ok: false, reason: "notifier_unavailable" };
|
|
192
|
-
const result = await notifier.notify(message);
|
|
192
|
+
const result = await notifier.notify(message, { eventType });
|
|
193
193
|
logEvent(
|
|
194
194
|
"notification",
|
|
195
195
|
"watchdog",
|
|
@@ -204,9 +204,10 @@ const createWatchdog = ({
|
|
|
204
204
|
notificationKey,
|
|
205
205
|
message,
|
|
206
206
|
correlationId = "",
|
|
207
|
+
eventType = "info",
|
|
207
208
|
) => {
|
|
208
209
|
const key = String(notificationKey || "").trim();
|
|
209
|
-
if (!key) return notify(message, correlationId);
|
|
210
|
+
if (!key) return notify(message, correlationId, eventType);
|
|
210
211
|
if (sentIncidentNotifications.has(key)) {
|
|
211
212
|
return {
|
|
212
213
|
ok: false,
|
|
@@ -214,7 +215,7 @@ const createWatchdog = ({
|
|
|
214
215
|
reason: "incident_notification_already_sent",
|
|
215
216
|
};
|
|
216
217
|
}
|
|
217
|
-
const result = await notify(message, correlationId);
|
|
218
|
+
const result = await notify(message, correlationId, eventType);
|
|
218
219
|
if (result?.ok || result?.skipped) {
|
|
219
220
|
sentIncidentNotifications.add(key);
|
|
220
221
|
}
|
|
@@ -273,6 +274,7 @@ const createWatchdog = ({
|
|
|
273
274
|
...(attempts > 0 ? [`Attempt count: ${attempts}`] : []),
|
|
274
275
|
].join("\n"),
|
|
275
276
|
correlationId,
|
|
277
|
+
ok && verifiedHealthy ? "recovery" : "crash",
|
|
276
278
|
);
|
|
277
279
|
};
|
|
278
280
|
|
|
@@ -468,6 +470,7 @@ const createWatchdog = ({
|
|
|
468
470
|
withViewLogsSuffix("Auto-repair paused until manual action."),
|
|
469
471
|
].join("\n"),
|
|
470
472
|
correlationId,
|
|
473
|
+
"crash",
|
|
471
474
|
);
|
|
472
475
|
}
|
|
473
476
|
return { ok: false, result };
|
|
@@ -543,6 +546,7 @@ const createWatchdog = ({
|
|
|
543
546
|
withViewLogsSuffix("🟢 Gateway healthy again"),
|
|
544
547
|
].join("\n"),
|
|
545
548
|
correlationId,
|
|
549
|
+
"recovery",
|
|
546
550
|
);
|
|
547
551
|
}
|
|
548
552
|
state.pendingRecoveryNoticeSource = "";
|
|
@@ -766,6 +770,7 @@ const createWatchdog = ({
|
|
|
766
770
|
: ["Auto-restart paused; manual action required."]),
|
|
767
771
|
].join("\n"),
|
|
768
772
|
correlationId,
|
|
773
|
+
"crash",
|
|
769
774
|
);
|
|
770
775
|
if (state.autoRepair) {
|
|
771
776
|
void runRepair({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chrysb/alphaclaw",
|
|
3
|
-
"version": "0.8.3-beta.
|
|
3
|
+
"version": "0.8.3-beta.2",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -19,11 +19,13 @@
|
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
21
|
"bin/",
|
|
22
|
-
"lib/"
|
|
22
|
+
"lib/",
|
|
23
|
+
"patches/"
|
|
23
24
|
],
|
|
24
25
|
"scripts": {
|
|
25
26
|
"start": "node bin/alphaclaw.js start",
|
|
26
27
|
"build:ui": "node scripts/build-ui.mjs",
|
|
28
|
+
"postinstall": "patch-package",
|
|
27
29
|
"test": "vitest run",
|
|
28
30
|
"test:watch": "vitest",
|
|
29
31
|
"test:watchdog": "vitest run tests/server/watchdog.test.js tests/server/watchdog-db.test.js tests/server/routes-watchdog.test.js",
|
|
@@ -33,13 +35,14 @@
|
|
|
33
35
|
"dependencies": {
|
|
34
36
|
"express": "^4.21.0",
|
|
35
37
|
"http-proxy": "^1.18.1",
|
|
36
|
-
"openclaw": "2026.3.
|
|
38
|
+
"openclaw": "^2026.3.28",
|
|
39
|
+
"patch-package": "^8.0.1",
|
|
37
40
|
"ws": "^8.19.0"
|
|
38
41
|
},
|
|
39
42
|
"devDependencies": {
|
|
43
|
+
"@vitest/coverage-v8": "^4.0.18",
|
|
40
44
|
"@xterm/addon-fit": "^0.10.0",
|
|
41
45
|
"@xterm/xterm": "^5.5.0",
|
|
42
|
-
"@vitest/coverage-v8": "^4.0.18",
|
|
43
46
|
"chart.js": "^4.5.1",
|
|
44
47
|
"esbuild": "^0.25.9",
|
|
45
48
|
"htm": "^3.1.1",
|
|
@@ -51,6 +54,6 @@
|
|
|
51
54
|
"wouter-preact": "^3.7.1"
|
|
52
55
|
},
|
|
53
56
|
"engines": {
|
|
54
|
-
"node": ">=22.
|
|
57
|
+
"node": ">=22.14.0"
|
|
55
58
|
}
|
|
56
59
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
diff --git a/node_modules/openclaw/dist/gateway-cli-DlnlX7IW.js b/node_modules/openclaw/dist/gateway-cli-DlnlX7IW.js
|
|
2
|
+
index ca48b932..c12478c4 100644
|
|
3
|
+
--- a/node_modules/openclaw/dist/gateway-cli-DlnlX7IW.js
|
|
4
|
+
+++ b/node_modules/openclaw/dist/gateway-cli-DlnlX7IW.js
|
|
5
|
+
@@ -25935,7 +25935,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
|
+
}
|
|
File without changes
|
|
File without changes
|