@chrysb/alphaclaw 0.3.1 → 0.3.2-beta.1
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 +2 -4
- package/lib/public/js/components/watchdog-tab.js +211 -37
- package/lib/public/js/lib/api.js +5 -0
- package/lib/server/alphaclaw-version.js +15 -5
- package/lib/server/routes/watchdog.js +11 -0
- package/lib/server/system-resources.js +149 -0
- package/lib/server/watchdog.js +4 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img width="771" height="339" alt="image" src="https://github.com/user-attachments/assets/
|
|
2
|
+
<img width="771" height="339" alt="image" src="https://github.com/user-attachments/assets/b96b45ab-52f2-4010-bfbe-c640e66b0f36" />
|
|
3
3
|
</p>
|
|
4
4
|
<h1 align="center">AlphaClaw</h1>
|
|
5
5
|
<p align="center">
|
|
@@ -47,9 +47,7 @@
|
|
|
47
47
|
|
|
48
48
|
## No Lock-in. Eject Anytime.
|
|
49
49
|
|
|
50
|
-
AlphaClaw
|
|
51
|
-
|
|
52
|
-
---
|
|
50
|
+
AlphaClaw simply wraps OpenClaw, it's not a dependency. Remove AlphaClaw and your agent keeps running. Nothing proprietary, nothing to migrate.
|
|
53
51
|
|
|
54
52
|
## Quick Start
|
|
55
53
|
|
|
@@ -4,6 +4,7 @@ import htm from "https://esm.sh/htm";
|
|
|
4
4
|
import {
|
|
5
5
|
fetchWatchdogEvents,
|
|
6
6
|
fetchWatchdogLogs,
|
|
7
|
+
fetchWatchdogResources,
|
|
7
8
|
fetchWatchdogSettings,
|
|
8
9
|
updateWatchdogSettings,
|
|
9
10
|
triggerWatchdogRepair,
|
|
@@ -16,9 +17,96 @@ import { showToast } from "./toast.js";
|
|
|
16
17
|
|
|
17
18
|
const html = htm.bind(h);
|
|
18
19
|
|
|
20
|
+
const formatBytes = (bytes) => {
|
|
21
|
+
if (bytes == null) return "—";
|
|
22
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
23
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
|
|
24
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
25
|
+
return `${(bytes / (1024 * 1024)).toFixed(0)} MB`;
|
|
26
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const barColor = (percent) => {
|
|
30
|
+
if (percent == null) return "bg-gray-600";
|
|
31
|
+
if (percent >= 90) return "bg-red-500";
|
|
32
|
+
if (percent >= 70) return "bg-yellow-400";
|
|
33
|
+
return "bg-cyan-400";
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const ResourceBar = ({
|
|
37
|
+
label,
|
|
38
|
+
percent,
|
|
39
|
+
detail,
|
|
40
|
+
segments = null,
|
|
41
|
+
expanded = false,
|
|
42
|
+
onToggle = null,
|
|
43
|
+
}) => html`
|
|
44
|
+
<div
|
|
45
|
+
class=${onToggle ? "cursor-pointer group" : ""}
|
|
46
|
+
onclick=${onToggle || undefined}
|
|
47
|
+
>
|
|
48
|
+
<span
|
|
49
|
+
class=${`text-xs text-gray-400 ${onToggle ? "group-hover:text-gray-200 transition-colors" : ""}`}
|
|
50
|
+
>${label}</span
|
|
51
|
+
>
|
|
52
|
+
<div
|
|
53
|
+
class=${`h-1.5 w-full bg-white/5 rounded-full overflow-hidden mt-1.5 flex ${onToggle ? "group-hover:bg-white/10 transition-colors" : ""}`}
|
|
54
|
+
>
|
|
55
|
+
${expanded && segments
|
|
56
|
+
? segments.map(
|
|
57
|
+
(seg) => html`
|
|
58
|
+
<div
|
|
59
|
+
class="h-full"
|
|
60
|
+
style=${{
|
|
61
|
+
width: `${Math.min(100, seg.percent ?? 0)}%`,
|
|
62
|
+
backgroundColor: seg.color,
|
|
63
|
+
transition:
|
|
64
|
+
"width 0.8s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.5s ease",
|
|
65
|
+
}}
|
|
66
|
+
></div>
|
|
67
|
+
`,
|
|
68
|
+
)
|
|
69
|
+
: html`
|
|
70
|
+
<div
|
|
71
|
+
class=${`h-full rounded-full ${barColor(percent)}`}
|
|
72
|
+
style=${{
|
|
73
|
+
width: `${Math.min(100, percent ?? 0)}%`,
|
|
74
|
+
transition:
|
|
75
|
+
"width 0.8s cubic-bezier(0.4, 0, 0.2, 1), background-color 0.5s ease",
|
|
76
|
+
}}
|
|
77
|
+
></div>
|
|
78
|
+
`}
|
|
79
|
+
</div>
|
|
80
|
+
<div class="flex flex-wrap items-center gap-x-3 mt-2.5">
|
|
81
|
+
<span class="text-xs text-gray-500 font-mono flex-1">${detail}</span>
|
|
82
|
+
${expanded &&
|
|
83
|
+
segments &&
|
|
84
|
+
segments
|
|
85
|
+
.filter((s) => s.label)
|
|
86
|
+
.map(
|
|
87
|
+
(seg) => html`
|
|
88
|
+
<span
|
|
89
|
+
class="inline-flex items-center gap-1 text-xs text-gray-500 font-mono"
|
|
90
|
+
>
|
|
91
|
+
<span
|
|
92
|
+
class="inline-block w-1.5 h-1.5 rounded-full"
|
|
93
|
+
style=${{ backgroundColor: seg.color }}
|
|
94
|
+
></span>
|
|
95
|
+
${seg.label}
|
|
96
|
+
</span>
|
|
97
|
+
`,
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
`;
|
|
102
|
+
|
|
19
103
|
const getIncidentStatusTone = (event) => {
|
|
20
|
-
const eventType = String(event?.eventType || "")
|
|
21
|
-
|
|
104
|
+
const eventType = String(event?.eventType || "")
|
|
105
|
+
.trim()
|
|
106
|
+
.toLowerCase();
|
|
107
|
+
const status = String(event?.status || "")
|
|
108
|
+
.trim()
|
|
109
|
+
.toLowerCase();
|
|
22
110
|
if (status === "failed") {
|
|
23
111
|
return {
|
|
24
112
|
dotClass: "bg-red-500/90",
|
|
@@ -53,6 +141,8 @@ export const WatchdogTab = ({
|
|
|
53
141
|
restartSignal = 0,
|
|
54
142
|
}) => {
|
|
55
143
|
const eventsPoll = usePolling(() => fetchWatchdogEvents(20), 15000);
|
|
144
|
+
const resourcesPoll = usePolling(() => fetchWatchdogResources(), 5000);
|
|
145
|
+
const [memoryExpanded, setMemoryExpanded] = useState(false);
|
|
56
146
|
const [settings, setSettings] = useState({
|
|
57
147
|
autoRepair: false,
|
|
58
148
|
notificationsEnabled: true,
|
|
@@ -66,7 +156,8 @@ export const WatchdogTab = ({
|
|
|
66
156
|
|
|
67
157
|
const currentWatchdogStatus = watchdogStatus || {};
|
|
68
158
|
const events = eventsPoll.data?.events || [];
|
|
69
|
-
const isRepairInProgress =
|
|
159
|
+
const isRepairInProgress =
|
|
160
|
+
repairing || !!currentWatchdogStatus?.operationInProgress;
|
|
70
161
|
|
|
71
162
|
useEffect(() => {
|
|
72
163
|
let active = true;
|
|
@@ -172,7 +263,10 @@ export const WatchdogTab = ({
|
|
|
172
263
|
},
|
|
173
264
|
);
|
|
174
265
|
onRefreshStatuses();
|
|
175
|
-
showToast(
|
|
266
|
+
showToast(
|
|
267
|
+
`Notifications ${nextValue ? "enabled" : "disabled"}`,
|
|
268
|
+
"success",
|
|
269
|
+
);
|
|
176
270
|
} catch (err) {
|
|
177
271
|
showToast(err.message || "Could not update notifications", "error");
|
|
178
272
|
} finally {
|
|
@@ -210,6 +304,80 @@ export const WatchdogTab = ({
|
|
|
210
304
|
repairing=${isRepairInProgress}
|
|
211
305
|
/>
|
|
212
306
|
|
|
307
|
+
${(() => {
|
|
308
|
+
const r = resourcesPoll.data?.resources;
|
|
309
|
+
if (!r) return null;
|
|
310
|
+
return html`
|
|
311
|
+
<div class="bg-surface border border-border rounded-xl p-4">
|
|
312
|
+
${memoryExpanded
|
|
313
|
+
? html`
|
|
314
|
+
<${ResourceBar}
|
|
315
|
+
label="Memory"
|
|
316
|
+
detail=${`${formatBytes(r.memory?.usedBytes)} / ${formatBytes(r.memory?.totalBytes)}`}
|
|
317
|
+
percent=${r.memory?.percent}
|
|
318
|
+
expanded=${true}
|
|
319
|
+
onToggle=${() => setMemoryExpanded(false)}
|
|
320
|
+
segments=${(() => {
|
|
321
|
+
const p = r.processes;
|
|
322
|
+
const total = r.memory?.totalBytes;
|
|
323
|
+
const used = r.memory?.usedBytes;
|
|
324
|
+
if (!p || !total || !used) return null;
|
|
325
|
+
const segs = [];
|
|
326
|
+
let tracked = 0;
|
|
327
|
+
if (p.gateway?.rssBytes != null) {
|
|
328
|
+
tracked += p.gateway.rssBytes;
|
|
329
|
+
segs.push({
|
|
330
|
+
percent: (p.gateway.rssBytes / total) * 100,
|
|
331
|
+
color: "#22d3ee",
|
|
332
|
+
label: `Gateway ${formatBytes(p.gateway.rssBytes)}`,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
if (p.alphaclaw?.rssBytes != null) {
|
|
336
|
+
tracked += p.alphaclaw.rssBytes;
|
|
337
|
+
segs.push({
|
|
338
|
+
percent: (p.alphaclaw.rssBytes / total) * 100,
|
|
339
|
+
color: "#a78bfa",
|
|
340
|
+
label: `AlphaClaw ${formatBytes(p.alphaclaw.rssBytes)}`,
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
const other = Math.max(0, used - tracked);
|
|
344
|
+
if (other > 0) {
|
|
345
|
+
segs.push({
|
|
346
|
+
percent: (other / total) * 100,
|
|
347
|
+
color: "#4b5563",
|
|
348
|
+
label: `Other ${formatBytes(other)}`,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
return segs.length ? segs : null;
|
|
352
|
+
})()}
|
|
353
|
+
/>
|
|
354
|
+
`
|
|
355
|
+
: html`
|
|
356
|
+
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
357
|
+
<${ResourceBar}
|
|
358
|
+
label="Memory"
|
|
359
|
+
percent=${r.memory?.percent}
|
|
360
|
+
detail=${`${formatBytes(r.memory?.usedBytes)} / ${formatBytes(r.memory?.totalBytes)}`}
|
|
361
|
+
onToggle=${() => setMemoryExpanded(true)}
|
|
362
|
+
/>
|
|
363
|
+
<${ResourceBar}
|
|
364
|
+
label="Disk"
|
|
365
|
+
percent=${r.disk?.percent}
|
|
366
|
+
detail=${`${formatBytes(r.disk?.usedBytes)} / ${formatBytes(r.disk?.totalBytes)}`}
|
|
367
|
+
/>
|
|
368
|
+
<${ResourceBar}
|
|
369
|
+
label=${`CPU${r.cpu?.cores ? ` (${r.cpu.cores} core${r.cpu.cores > 1 ? "s" : ""})` : ""}`}
|
|
370
|
+
percent=${r.cpu?.percent}
|
|
371
|
+
detail=${r.cpu?.percent != null
|
|
372
|
+
? `${r.cpu.percent}%`
|
|
373
|
+
: "—"}
|
|
374
|
+
/>
|
|
375
|
+
</div>
|
|
376
|
+
`}
|
|
377
|
+
</div>
|
|
378
|
+
`;
|
|
379
|
+
})()}
|
|
380
|
+
|
|
213
381
|
<div class="bg-surface border border-border rounded-xl p-4">
|
|
214
382
|
<div class="flex items-center justify-between gap-3">
|
|
215
383
|
<div class="inline-flex items-center gap-2 text-xs text-gray-400">
|
|
@@ -229,7 +397,7 @@ export const WatchdogTab = ({
|
|
|
229
397
|
<div class="inline-flex items-center gap-2 text-xs text-gray-400">
|
|
230
398
|
<span>Notifications</span>
|
|
231
399
|
<${InfoTooltip}
|
|
232
|
-
text="Sends
|
|
400
|
+
text="Sends channel notices for watchdog alerts and auto-repair outcomes."
|
|
233
401
|
/>
|
|
234
402
|
</div>
|
|
235
403
|
<${ToggleSwitch}
|
|
@@ -256,7 +424,9 @@ export const WatchdogTab = ({
|
|
|
256
424
|
<pre
|
|
257
425
|
ref=${logsRef}
|
|
258
426
|
class="bg-black/40 border border-border rounded-lg p-3 h-72 overflow-auto text-xs text-gray-300 whitespace-pre-wrap break-words"
|
|
259
|
-
|
|
427
|
+
>
|
|
428
|
+
${loadingLogs ? "Loading logs..." : logs || "No logs yet."}</pre
|
|
429
|
+
>
|
|
260
430
|
</div>
|
|
261
431
|
|
|
262
432
|
<div class="bg-surface border border-border rounded-xl p-4">
|
|
@@ -272,39 +442,43 @@ export const WatchdogTab = ({
|
|
|
272
442
|
<div class="ac-history-list">
|
|
273
443
|
${events.length === 0 &&
|
|
274
444
|
html`<p class="text-xs text-gray-500">No incidents recorded.</p>`}
|
|
275
|
-
${events.map(
|
|
276
|
-
(event)
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
<
|
|
280
|
-
<
|
|
281
|
-
<
|
|
282
|
-
<span class="inline-flex items-center gap-2 min-w-0">
|
|
283
|
-
<span class="ac-history-toggle shrink-0" aria-hidden="true">▸</span>
|
|
284
|
-
<span class="truncate">
|
|
285
|
-
${event.createdAt || ""} · ${event.eventType || "event"} · ${event.status ||
|
|
286
|
-
"unknown"}
|
|
287
|
-
</span>
|
|
288
|
-
</span>
|
|
445
|
+
${events.map((event) => {
|
|
446
|
+
const tone = getIncidentStatusTone(event);
|
|
447
|
+
return html`
|
|
448
|
+
<details class="ac-history-item">
|
|
449
|
+
<summary class="ac-history-summary">
|
|
450
|
+
<div class="ac-history-summary-row">
|
|
451
|
+
<span class="inline-flex items-center gap-2 min-w-0">
|
|
289
452
|
<span
|
|
290
|
-
class
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
453
|
+
class="ac-history-toggle shrink-0"
|
|
454
|
+
aria-hidden="true"
|
|
455
|
+
>▸</span
|
|
456
|
+
>
|
|
457
|
+
<span class="truncate">
|
|
458
|
+
${event.createdAt || ""} · ${event.eventType || "event"}
|
|
459
|
+
· ${event.status || "unknown"}
|
|
460
|
+
</span>
|
|
461
|
+
</span>
|
|
462
|
+
<span
|
|
463
|
+
class=${`h-2.5 w-2.5 shrink-0 rounded-full ${tone.dotClass}`}
|
|
464
|
+
title=${tone.label}
|
|
465
|
+
aria-label=${tone.label}
|
|
466
|
+
></span>
|
|
303
467
|
</div>
|
|
304
|
-
</
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
468
|
+
</summary>
|
|
469
|
+
<div class="ac-history-body text-xs text-gray-400">
|
|
470
|
+
<div>Source: ${event.source || "unknown"}</div>
|
|
471
|
+
<pre
|
|
472
|
+
class="mt-2 bg-black/30 rounded p-2 whitespace-pre-wrap break-words"
|
|
473
|
+
>
|
|
474
|
+
${typeof event.details === "string"
|
|
475
|
+
? event.details
|
|
476
|
+
: JSON.stringify(event.details || {}, null, 2)}</pre
|
|
477
|
+
>
|
|
478
|
+
</div>
|
|
479
|
+
</details>
|
|
480
|
+
`;
|
|
481
|
+
})}
|
|
308
482
|
</div>
|
|
309
483
|
</div>
|
|
310
484
|
</div>
|
package/lib/public/js/lib/api.js
CHANGED
|
@@ -93,6 +93,11 @@ export async function triggerWatchdogRepair() {
|
|
|
93
93
|
return parseJsonOrThrow(res, 'Could not trigger watchdog repair');
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
export async function fetchWatchdogResources() {
|
|
97
|
+
const res = await authFetch('/api/watchdog/resources');
|
|
98
|
+
return parseJsonOrThrow(res, 'Could not load system resources');
|
|
99
|
+
}
|
|
100
|
+
|
|
96
101
|
export async function fetchWatchdogSettings() {
|
|
97
102
|
const res = await authFetch('/api/watchdog/settings');
|
|
98
103
|
return parseJsonOrThrow(res, 'Could not load watchdog settings');
|
|
@@ -10,6 +10,20 @@ const {
|
|
|
10
10
|
kRootDir,
|
|
11
11
|
} = require("./constants");
|
|
12
12
|
|
|
13
|
+
const isNewerVersion = (latest, current) => {
|
|
14
|
+
if (!latest || !current) return false;
|
|
15
|
+
const parse = (v) => {
|
|
16
|
+
const [core] = String(v).replace(/^v/, "").split("-");
|
|
17
|
+
const parts = core.split(".").map(Number);
|
|
18
|
+
return { major: parts[0] || 0, minor: parts[1] || 0, patch: parts[2] || 0 };
|
|
19
|
+
};
|
|
20
|
+
const l = parse(latest);
|
|
21
|
+
const c = parse(current);
|
|
22
|
+
if (l.major !== c.major) return l.major > c.major;
|
|
23
|
+
if (l.minor !== c.minor) return l.minor > c.minor;
|
|
24
|
+
return l.patch > c.patch;
|
|
25
|
+
};
|
|
26
|
+
|
|
13
27
|
const createAlphaclawVersionService = () => {
|
|
14
28
|
let kUpdateStatusCache = {
|
|
15
29
|
latestVersion: null,
|
|
@@ -82,11 +96,7 @@ const createAlphaclawVersionService = () => {
|
|
|
82
96
|
}
|
|
83
97
|
const currentVersion = readAlphaclawVersion();
|
|
84
98
|
const latestVersion = await fetchLatestVersionFromRegistry();
|
|
85
|
-
const hasUpdate =
|
|
86
|
-
currentVersion &&
|
|
87
|
-
latestVersion &&
|
|
88
|
-
latestVersion !== currentVersion
|
|
89
|
-
);
|
|
99
|
+
const hasUpdate = isNewerVersion(latestVersion, currentVersion);
|
|
90
100
|
kUpdateStatusCache = { latestVersion, hasUpdate, fetchedAt: Date.now() };
|
|
91
101
|
if (hasUpdate) {
|
|
92
102
|
console.log(
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
const { getSystemResources } = require("../system-resources");
|
|
2
|
+
|
|
1
3
|
const registerWatchdogRoutes = ({
|
|
2
4
|
app,
|
|
3
5
|
requireAuth,
|
|
@@ -55,6 +57,15 @@ const registerWatchdogRoutes = ({
|
|
|
55
57
|
}
|
|
56
58
|
});
|
|
57
59
|
|
|
60
|
+
app.get("/api/watchdog/resources", requireAuth, (req, res) => {
|
|
61
|
+
try {
|
|
62
|
+
const status = watchdog.getStatus();
|
|
63
|
+
res.json({ ok: true, resources: getSystemResources({ gatewayPid: status.gatewayPid }) });
|
|
64
|
+
} catch (err) {
|
|
65
|
+
res.status(500).json({ ok: false, error: err.message });
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
58
69
|
app.put("/api/watchdog/settings", requireAuth, (req, res) => {
|
|
59
70
|
try {
|
|
60
71
|
const settings = watchdog.updateSettings(req.body || {});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
const os = require("os");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const { execSync } = require("child_process");
|
|
4
|
+
|
|
5
|
+
const readCgroupFile = (filePath) => {
|
|
6
|
+
try {
|
|
7
|
+
return fs.readFileSync(filePath, "utf8").trim();
|
|
8
|
+
} catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const parseCgroupMemory = () => {
|
|
14
|
+
const current = readCgroupFile("/sys/fs/cgroup/memory.current");
|
|
15
|
+
const max = readCgroupFile("/sys/fs/cgroup/memory.max");
|
|
16
|
+
if (!current) return null;
|
|
17
|
+
const usedBytes = Number.parseInt(current, 10);
|
|
18
|
+
if (Number.isNaN(usedBytes)) return null;
|
|
19
|
+
const limitBytes =
|
|
20
|
+
max && max !== "max" ? Number.parseInt(max, 10) : null;
|
|
21
|
+
return {
|
|
22
|
+
usedBytes,
|
|
23
|
+
totalBytes: Number.isNaN(limitBytes) ? null : limitBytes,
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const parseCgroupCpu = () => {
|
|
28
|
+
const stat = readCgroupFile("/sys/fs/cgroup/cpu.stat");
|
|
29
|
+
if (!stat) return null;
|
|
30
|
+
const lines = stat.split("\n");
|
|
31
|
+
const map = {};
|
|
32
|
+
for (const line of lines) {
|
|
33
|
+
const [key, val] = line.split(/\s+/);
|
|
34
|
+
if (key && val) map[key] = Number.parseInt(val, 10);
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
usageUsec: map.usage_usec ?? null,
|
|
38
|
+
userUsec: map.user_usec ?? null,
|
|
39
|
+
systemUsec: map.system_usec ?? null,
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const readProcStatus = (pid) => {
|
|
44
|
+
try {
|
|
45
|
+
const status = fs.readFileSync(`/proc/${pid}/status`, "utf8");
|
|
46
|
+
const vmRss = status.match(/VmRSS:\s+(\d+)\s+kB/);
|
|
47
|
+
return { rssBytes: vmRss ? Number.parseInt(vmRss[1], 10) * 1024 : null };
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const readPsStats = (pid) => {
|
|
54
|
+
try {
|
|
55
|
+
const out = execSync(`ps -o rss=,pcpu= -p ${pid}`, {
|
|
56
|
+
encoding: "utf8",
|
|
57
|
+
timeout: 2000,
|
|
58
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
59
|
+
}).trim();
|
|
60
|
+
const [rss, pcpu] = out.split(/\s+/);
|
|
61
|
+
return {
|
|
62
|
+
rssBytes: rss ? Number.parseInt(rss, 10) * 1024 : null,
|
|
63
|
+
cpuPercent: pcpu ? Number.parseFloat(pcpu) : null,
|
|
64
|
+
};
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const getProcessUsage = (pid) => {
|
|
71
|
+
if (!pid) return null;
|
|
72
|
+
const proc = readProcStatus(pid);
|
|
73
|
+
if (proc) return { rssBytes: proc.rssBytes };
|
|
74
|
+
const ps = readPsStats(pid);
|
|
75
|
+
if (ps) return { rssBytes: ps.rssBytes };
|
|
76
|
+
return null;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
let prevCpuSnapshot = null;
|
|
80
|
+
let prevCpuSnapshotAt = 0;
|
|
81
|
+
|
|
82
|
+
const getSystemResources = ({ gatewayPid = null } = {}) => {
|
|
83
|
+
const cgroupMem = parseCgroupMemory();
|
|
84
|
+
const mem = {
|
|
85
|
+
usedBytes: cgroupMem?.usedBytes ?? process.memoryUsage().rss,
|
|
86
|
+
totalBytes: cgroupMem?.totalBytes ?? os.totalmem(),
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
let diskUsedBytes = null;
|
|
90
|
+
let diskTotalBytes = null;
|
|
91
|
+
try {
|
|
92
|
+
const stat = fs.statfsSync("/");
|
|
93
|
+
diskTotalBytes = stat.bsize * stat.blocks;
|
|
94
|
+
diskUsedBytes = stat.bsize * (stat.blocks - stat.bfree);
|
|
95
|
+
} catch {
|
|
96
|
+
// statfsSync unavailable
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const cgroupCpu = parseCgroupCpu();
|
|
100
|
+
let cpuPercent = null;
|
|
101
|
+
if (cgroupCpu?.usageUsec != null) {
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
if (prevCpuSnapshot && prevCpuSnapshotAt) {
|
|
104
|
+
const elapsedMs = now - prevCpuSnapshotAt;
|
|
105
|
+
if (elapsedMs > 0) {
|
|
106
|
+
const usageDeltaUs = cgroupCpu.usageUsec - prevCpuSnapshot.usageUsec;
|
|
107
|
+
const elapsedUs = elapsedMs * 1000;
|
|
108
|
+
cpuPercent = Math.min(100, Math.max(0, (usageDeltaUs / elapsedUs) * 100));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
prevCpuSnapshot = cgroupCpu;
|
|
112
|
+
prevCpuSnapshotAt = now;
|
|
113
|
+
} else {
|
|
114
|
+
const load = os.loadavg();
|
|
115
|
+
const cpus = os.cpus().length || 1;
|
|
116
|
+
cpuPercent = Math.min(100, Math.max(0, (load[0] / cpus) * 100));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const alphaclawRss = process.memoryUsage().rss;
|
|
120
|
+
const gatewayUsage = getProcessUsage(gatewayPid);
|
|
121
|
+
const gatewayRss = gatewayUsage?.rssBytes ?? null;
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
memory: {
|
|
125
|
+
usedBytes: mem.usedBytes,
|
|
126
|
+
totalBytes: mem.totalBytes,
|
|
127
|
+
percent: mem.totalBytes
|
|
128
|
+
? Math.round((mem.usedBytes / mem.totalBytes) * 1000) / 10
|
|
129
|
+
: null,
|
|
130
|
+
},
|
|
131
|
+
disk: {
|
|
132
|
+
usedBytes: diskUsedBytes,
|
|
133
|
+
totalBytes: diskTotalBytes,
|
|
134
|
+
percent: diskTotalBytes
|
|
135
|
+
? Math.round((diskUsedBytes / diskTotalBytes) * 1000) / 10
|
|
136
|
+
: null,
|
|
137
|
+
},
|
|
138
|
+
cpu: {
|
|
139
|
+
percent: cpuPercent != null ? Math.round(cpuPercent * 10) / 10 : null,
|
|
140
|
+
cores: os.cpus().length,
|
|
141
|
+
},
|
|
142
|
+
processes: {
|
|
143
|
+
alphaclaw: { rssBytes: alphaclawRss },
|
|
144
|
+
gateway: { rssBytes: gatewayRss, pid: gatewayPid },
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
module.exports = { getSystemResources };
|
package/lib/server/watchdog.js
CHANGED
|
@@ -59,6 +59,7 @@ const createWatchdog = ({
|
|
|
59
59
|
notificationsDisabled: isTruthy(process.env.WATCHDOG_NOTIFICATIONS_DISABLED),
|
|
60
60
|
operationInProgress: false,
|
|
61
61
|
gatewayStartedAt: null,
|
|
62
|
+
gatewayPid: null,
|
|
62
63
|
crashRecoveryActive: false,
|
|
63
64
|
expectedRestartInProgress: false,
|
|
64
65
|
expectedRestartUntilMs: 0,
|
|
@@ -502,13 +503,14 @@ const createWatchdog = ({
|
|
|
502
503
|
void restartAfterCrash(correlationId);
|
|
503
504
|
};
|
|
504
505
|
|
|
505
|
-
const onGatewayLaunch = ({ startedAt = Date.now() } = {}) => {
|
|
506
|
+
const onGatewayLaunch = ({ startedAt = Date.now(), pid = null } = {}) => {
|
|
506
507
|
state.lifecycle = "running";
|
|
507
508
|
state.health = "unknown";
|
|
508
509
|
state.crashRecoveryActive = false;
|
|
509
510
|
clearExpectedRestartWindow();
|
|
510
511
|
state.uptimeStartedAt = startedAt;
|
|
511
512
|
state.gatewayStartedAt = startedAt;
|
|
513
|
+
state.gatewayPid = pid;
|
|
512
514
|
startBootstrapHealthChecks();
|
|
513
515
|
};
|
|
514
516
|
|
|
@@ -566,6 +568,7 @@ const createWatchdog = ({
|
|
|
566
568
|
crashLoopThreshold: kWatchdogCrashLoopThreshold,
|
|
567
569
|
crashLoopWindowMs: kWatchdogCrashLoopWindowMs,
|
|
568
570
|
operationInProgress: state.operationInProgress,
|
|
571
|
+
gatewayPid: state.gatewayPid,
|
|
569
572
|
};
|
|
570
573
|
};
|
|
571
574
|
|