bosun 0.28.0 → 0.28.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/.env.example +22 -1
- package/README.md +16 -5
- package/agent-pool.mjs +16 -2
- package/agent-work-analyzer.mjs +419 -0
- package/claude-shell.mjs +12 -2
- package/desktop/launch.mjs +31 -2
- package/desktop/main.mjs +132 -10
- package/desktop/package.json +8 -0
- package/desktop-shortcut.mjs +20 -1
- package/monitor.mjs +151 -0
- package/package.json +3 -2
- package/rotate-agent-logs.sh +8 -7
- package/setup.mjs +759 -59
- package/task-executor.mjs +309 -12
- package/telegram-bot.mjs +419 -5
- package/ui/app.js +2 -0
- package/ui/components/shared.js +31 -2
- package/ui/demo.html +8 -0
- package/ui/modules/icons.js +14 -0
- package/ui/modules/router.js +1 -0
- package/ui/modules/settings-schema.js +11 -1
- package/ui/modules/state.js +59 -0
- package/ui/styles/components.css +60 -8
- package/ui/styles.css +88 -0
- package/ui/tabs/tasks.js +110 -72
- package/ui/tabs/telemetry.js +167 -0
- package/ui-server.mjs +158 -0
- package/workspace-manager.mjs +45 -1
- package/ui/styles/workspace-switcher.css.bak +0 -693
package/desktop/main.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { app, BrowserWindow } from "electron";
|
|
1
|
+
import { app, BrowserWindow, session } from "electron";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
5
|
import { execFileSync, spawn } from "node:child_process";
|
|
6
|
+
import { request as httpRequest } from "node:http";
|
|
7
|
+
import { request as httpsRequest } from "node:https";
|
|
6
8
|
import { homedir } from "node:os";
|
|
7
9
|
|
|
8
10
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -12,9 +14,23 @@ let shuttingDown = false;
|
|
|
12
14
|
let uiServerStarted = false;
|
|
13
15
|
let uiOrigin = null;
|
|
14
16
|
let uiApi = null;
|
|
17
|
+
let runtimeConfigLoaded = false;
|
|
15
18
|
|
|
16
19
|
const DAEMON_PID_FILE = resolve(homedir(), ".cache", "bosun", "daemon.pid");
|
|
17
20
|
|
|
21
|
+
// Local/private-network patterns — TLS cert bypass for the embedded UI server
|
|
22
|
+
const LOCAL_HOSTNAME_RE = [
|
|
23
|
+
/^127\./,
|
|
24
|
+
/^192\.168\./,
|
|
25
|
+
/^10\./,
|
|
26
|
+
/^172\.(1[6-9]|2[0-9]|3[01])\./,
|
|
27
|
+
/^::1$/,
|
|
28
|
+
/^localhost$/i,
|
|
29
|
+
];
|
|
30
|
+
function isLocalHost(hostname) {
|
|
31
|
+
return LOCAL_HOSTNAME_RE.some((re) => re.test(hostname));
|
|
32
|
+
}
|
|
33
|
+
|
|
18
34
|
function parseBoolEnv(value, fallback) {
|
|
19
35
|
if (value === undefined || value === null) return fallback;
|
|
20
36
|
const normalized = String(value).trim().toLowerCase();
|
|
@@ -86,16 +102,81 @@ async function loadBosunModule(file) {
|
|
|
86
102
|
return import(pathToFileURL(modulePath).href);
|
|
87
103
|
}
|
|
88
104
|
|
|
105
|
+
async function loadRuntimeConfig() {
|
|
106
|
+
if (runtimeConfigLoaded) return;
|
|
107
|
+
try {
|
|
108
|
+
const config = await loadBosunModule("config.mjs");
|
|
109
|
+
if (typeof config?.loadConfig === "function") {
|
|
110
|
+
config.loadConfig(["node", "desktop"], { reloadEnv: true });
|
|
111
|
+
}
|
|
112
|
+
} catch (err) {
|
|
113
|
+
console.warn("[desktop] failed to load config env", err?.message || err);
|
|
114
|
+
}
|
|
115
|
+
runtimeConfigLoaded = true;
|
|
116
|
+
}
|
|
117
|
+
|
|
89
118
|
async function loadUiServerModule() {
|
|
90
119
|
if (uiApi) return uiApi;
|
|
91
120
|
uiApi = await loadBosunModule("ui-server.mjs");
|
|
92
121
|
return uiApi;
|
|
93
122
|
}
|
|
94
123
|
|
|
124
|
+
function buildDaemonUiBaseUrl() {
|
|
125
|
+
const rawPort = Number(process.env.TELEGRAM_UI_PORT || "0");
|
|
126
|
+
if (!Number.isFinite(rawPort) || rawPort <= 0) return null;
|
|
127
|
+
const tlsDisabled = parseBoolEnv(process.env.TELEGRAM_UI_TLS_DISABLE, false);
|
|
128
|
+
const protocol = tlsDisabled ? "http" : "https";
|
|
129
|
+
const host =
|
|
130
|
+
process.env.TELEGRAM_UI_DESKTOP_HOST ||
|
|
131
|
+
process.env.TELEGRAM_UI_HOST ||
|
|
132
|
+
"127.0.0.1";
|
|
133
|
+
return `${protocol}://${host}:${rawPort}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function probeUiServer(url) {
|
|
137
|
+
return new Promise((resolve) => {
|
|
138
|
+
try {
|
|
139
|
+
const isHttps = url.startsWith("https://");
|
|
140
|
+
const req = (isHttps ? httpsRequest : httpRequest)(
|
|
141
|
+
`${url}/api/status`,
|
|
142
|
+
{
|
|
143
|
+
method: "GET",
|
|
144
|
+
timeout: 1500,
|
|
145
|
+
rejectUnauthorized: false,
|
|
146
|
+
},
|
|
147
|
+
(res) => {
|
|
148
|
+
res.resume();
|
|
149
|
+
resolve(Boolean(res.statusCode && res.statusCode < 500));
|
|
150
|
+
},
|
|
151
|
+
);
|
|
152
|
+
req.on("error", () => resolve(false));
|
|
153
|
+
req.on("timeout", () => {
|
|
154
|
+
req.destroy();
|
|
155
|
+
resolve(false);
|
|
156
|
+
});
|
|
157
|
+
req.end();
|
|
158
|
+
} catch {
|
|
159
|
+
resolve(false);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function resolveDaemonUiUrl() {
|
|
165
|
+
const useDaemon = parseBoolEnv(
|
|
166
|
+
process.env.BOSUN_DESKTOP_USE_DAEMON_UI,
|
|
167
|
+
true,
|
|
168
|
+
);
|
|
169
|
+
if (!useDaemon) return null;
|
|
170
|
+
const base = buildDaemonUiBaseUrl();
|
|
171
|
+
if (!base) return null;
|
|
172
|
+
const ok = await probeUiServer(base);
|
|
173
|
+
return ok ? base : null;
|
|
174
|
+
}
|
|
175
|
+
|
|
95
176
|
async function ensureDaemonRunning() {
|
|
96
177
|
const autoStart = parseBoolEnv(
|
|
97
178
|
process.env.BOSUN_DESKTOP_AUTO_START_DAEMON,
|
|
98
|
-
|
|
179
|
+
false,
|
|
99
180
|
);
|
|
100
181
|
if (!autoStart) return;
|
|
101
182
|
|
|
@@ -148,6 +229,13 @@ async function startUiServer() {
|
|
|
148
229
|
}
|
|
149
230
|
|
|
150
231
|
async function buildUiUrl() {
|
|
232
|
+
await loadRuntimeConfig();
|
|
233
|
+
const daemonUrl = await resolveDaemonUiUrl();
|
|
234
|
+
if (daemonUrl) {
|
|
235
|
+
uiOrigin = new URL(daemonUrl).origin;
|
|
236
|
+
return daemonUrl;
|
|
237
|
+
}
|
|
238
|
+
await startUiServer();
|
|
151
239
|
const api = await loadUiServerModule();
|
|
152
240
|
const uiServerUrl = api.getTelegramUiUrl();
|
|
153
241
|
if (!uiServerUrl) {
|
|
@@ -164,6 +252,7 @@ async function buildUiUrl() {
|
|
|
164
252
|
|
|
165
253
|
async function createMainWindow() {
|
|
166
254
|
if (mainWindow) return;
|
|
255
|
+
const iconPath = resolveBosunRuntimePath("logo.png");
|
|
167
256
|
|
|
168
257
|
mainWindow = new BrowserWindow({
|
|
169
258
|
width: 1280,
|
|
@@ -171,6 +260,7 @@ async function createMainWindow() {
|
|
|
171
260
|
minWidth: 960,
|
|
172
261
|
minHeight: 640,
|
|
173
262
|
backgroundColor: "#0b0b0c",
|
|
263
|
+
...(existsSync(iconPath) ? { icon: iconPath } : {}),
|
|
174
264
|
show: false,
|
|
175
265
|
webPreferences: {
|
|
176
266
|
contextIsolation: true,
|
|
@@ -194,10 +284,35 @@ async function createMainWindow() {
|
|
|
194
284
|
|
|
195
285
|
async function bootstrap() {
|
|
196
286
|
try {
|
|
287
|
+
if (process.env.ELECTRON_DISABLE_SANDBOX === "1") {
|
|
288
|
+
app.commandLine.appendSwitch("no-sandbox");
|
|
289
|
+
app.commandLine.appendSwitch("disable-gpu-sandbox");
|
|
290
|
+
}
|
|
197
291
|
app.setAppUserModelId("com.virtengine.bosun");
|
|
292
|
+
const iconPath = resolveBosunRuntimePath("logo.png");
|
|
293
|
+
if (existsSync(iconPath)) {
|
|
294
|
+
try {
|
|
295
|
+
app.setIcon(iconPath);
|
|
296
|
+
} catch {
|
|
297
|
+
/* best effort */
|
|
298
|
+
}
|
|
299
|
+
}
|
|
198
300
|
process.chdir(resolveBosunRoot());
|
|
301
|
+
await loadRuntimeConfig();
|
|
302
|
+
|
|
303
|
+
// Bypass TLS verification for the local embedded UI server.
|
|
304
|
+
// setCertificateVerifyProc works at the OpenSSL level — it fires before
|
|
305
|
+
// the higher-level `certificate-error` event and stops the repeated
|
|
306
|
+
// "handshake failed" logs from Chromium's ssl_client_socket_impl.
|
|
307
|
+
session.defaultSession.setCertificateVerifyProc((request, callback) => {
|
|
308
|
+
if (isLocalHost(request.hostname)) {
|
|
309
|
+
callback(0); // 0 = verified OK
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
callback(-3); // -3 = use Chromium default chain verification
|
|
313
|
+
});
|
|
314
|
+
|
|
199
315
|
await ensureDaemonRunning();
|
|
200
|
-
await startUiServer();
|
|
201
316
|
await createMainWindow();
|
|
202
317
|
await maybeAutoUpdate();
|
|
203
318
|
} catch (error) {
|
|
@@ -231,8 +346,10 @@ async function shutdown(reason) {
|
|
|
231
346
|
}
|
|
232
347
|
|
|
233
348
|
try {
|
|
234
|
-
|
|
235
|
-
|
|
349
|
+
if (uiServerStarted) {
|
|
350
|
+
const api = await loadUiServerModule();
|
|
351
|
+
api.stopTelegramUiServer();
|
|
352
|
+
}
|
|
236
353
|
} catch (error) {
|
|
237
354
|
console.error("[desktop] failed to stop ui-server", error);
|
|
238
355
|
}
|
|
@@ -243,7 +360,7 @@ async function shutdown(reason) {
|
|
|
243
360
|
app.on("before-quit", () => {
|
|
244
361
|
shuttingDown = true;
|
|
245
362
|
try {
|
|
246
|
-
if (uiApi?.stopTelegramUiServer) {
|
|
363
|
+
if (uiServerStarted && uiApi?.stopTelegramUiServer) {
|
|
247
364
|
uiApi.stopTelegramUiServer();
|
|
248
365
|
}
|
|
249
366
|
} catch (error) {
|
|
@@ -254,10 +371,15 @@ app.on("before-quit", () => {
|
|
|
254
371
|
app.on(
|
|
255
372
|
"certificate-error",
|
|
256
373
|
(event, _webContents, url, _error, _certificate, callback) => {
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
374
|
+
try {
|
|
375
|
+
const hostname = new URL(url).hostname;
|
|
376
|
+
if ((uiOrigin && url.startsWith(uiOrigin)) || isLocalHost(hostname)) {
|
|
377
|
+
event.preventDefault();
|
|
378
|
+
callback(true);
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
} catch {
|
|
382
|
+
// malformed URL — fall through
|
|
261
383
|
}
|
|
262
384
|
callback(false);
|
|
263
385
|
},
|
package/desktop/package.json
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
"version": "0.1.0",
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
|
+
"author": "VirtEngine Maintainers <hello@virtengine.com>",
|
|
7
|
+
"homepage": "https://bosun.virtengine.com",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/virtengine/bosun.git",
|
|
11
|
+
"directory": "/desktop"
|
|
12
|
+
},
|
|
13
|
+
|
|
6
14
|
"main": "main.mjs",
|
|
7
15
|
"description": "Electron wrapper for the Bosun desktop portal",
|
|
8
16
|
"scripts": {
|
package/desktop-shortcut.mjs
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - Linux: .desktop entry
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { spawnSync } from "node:child_process";
|
|
12
|
+
import { spawnSync, execSync } from "node:child_process";
|
|
13
13
|
import {
|
|
14
14
|
existsSync,
|
|
15
15
|
readFileSync,
|
|
@@ -182,22 +182,41 @@ function installMacShortcut(desktopDir) {
|
|
|
182
182
|
|
|
183
183
|
function installLinuxShortcut(desktopDir) {
|
|
184
184
|
const desktopPath = resolve(desktopDir, `${APP_NAME}.desktop`);
|
|
185
|
+
const appDir = resolve(homedir(), ".local", "share", "applications");
|
|
186
|
+
const appPath = resolve(appDir, `${APP_NAME}.desktop`);
|
|
187
|
+
const iconPath = resolve(__dirname, "logo.png");
|
|
185
188
|
const content = [
|
|
186
189
|
"[Desktop Entry]",
|
|
187
190
|
"Type=Application",
|
|
188
191
|
`Name=${APP_NAME}`,
|
|
189
192
|
"Comment=Bosun Desktop Portal",
|
|
193
|
+
`Icon=${iconPath}`,
|
|
190
194
|
`Exec=${buildShellCommand()}`,
|
|
191
195
|
`Path=${getWorkingDirectory()}`,
|
|
192
196
|
"Terminal=false",
|
|
193
197
|
"StartupNotify=true",
|
|
194
198
|
"Categories=Development;Utility;",
|
|
199
|
+
"NoDisplay=false",
|
|
195
200
|
"",
|
|
196
201
|
].join("\n");
|
|
197
202
|
|
|
198
203
|
try {
|
|
199
204
|
writeFileSync(desktopPath, content, "utf8");
|
|
200
205
|
chmodSync(desktopPath, 0o755);
|
|
206
|
+
mkdirSync(appDir, { recursive: true });
|
|
207
|
+
writeFileSync(appPath, content, "utf8");
|
|
208
|
+
chmodSync(appPath, 0o755);
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
execSync(`gio set "${desktopPath}" metadata::trusted true`, {
|
|
212
|
+
stdio: "ignore",
|
|
213
|
+
});
|
|
214
|
+
execSync(`gio set "${appPath}" metadata::trusted true`, {
|
|
215
|
+
stdio: "ignore",
|
|
216
|
+
});
|
|
217
|
+
} catch {
|
|
218
|
+
/* best effort */
|
|
219
|
+
}
|
|
201
220
|
return {
|
|
202
221
|
success: true,
|
|
203
222
|
method: "Linux desktop entry",
|
package/monitor.mjs
CHANGED
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
initStatusBoard,
|
|
50
50
|
pushStatusBoardUpdate,
|
|
51
51
|
} from "./telegram-bot.mjs";
|
|
52
|
+
import { startAnalyzer, stopAnalyzer } from "./agent-work-analyzer.mjs";
|
|
52
53
|
import { PRCleanupDaemon } from "./pr-cleanup-daemon.mjs";
|
|
53
54
|
import {
|
|
54
55
|
execPrimaryPrompt,
|
|
@@ -227,6 +228,123 @@ const ANOMALY_SIGNAL_PATH = resolve(
|
|
|
227
228
|
"anomaly-signals.json",
|
|
228
229
|
);
|
|
229
230
|
|
|
231
|
+
const AGENT_ALERT_POLL_MS = 10_000;
|
|
232
|
+
let agentWorkAnalyzerActive = false;
|
|
233
|
+
let agentAlertsOffset = 0;
|
|
234
|
+
let agentAlertsTimer = null;
|
|
235
|
+
const agentAlertsDedup = new Map();
|
|
236
|
+
|
|
237
|
+
function getAgentAlertsPath() {
|
|
238
|
+
return resolve(repoRoot, ".cache", "agent-work-logs", "agent-alerts.jsonl");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function rememberAlert(key) {
|
|
242
|
+
agentAlertsDedup.set(key, Date.now());
|
|
243
|
+
if (agentAlertsDedup.size > 200) {
|
|
244
|
+
const sorted = [...agentAlertsDedup.entries()].sort((a, b) => a[1] - b[1]);
|
|
245
|
+
for (const [oldKey] of sorted.slice(0, 100)) {
|
|
246
|
+
agentAlertsDedup.delete(oldKey);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function formatAgentAlert(alert) {
|
|
252
|
+
const severity = String(alert.severity || "medium").toUpperCase();
|
|
253
|
+
const type = alert.type || "alert";
|
|
254
|
+
const lines = [
|
|
255
|
+
`🔎 Agent Analyzer: ${severity} ${type}`,
|
|
256
|
+
`Attempt: ${alert.attempt_id || "unknown"}`,
|
|
257
|
+
];
|
|
258
|
+
if (alert.task_id) lines.push(`Task: ${alert.task_id}`);
|
|
259
|
+
if (alert.executor) lines.push(`Executor: ${alert.executor}`);
|
|
260
|
+
if (alert.recommendation) lines.push(`Recommendation: ${alert.recommendation}`);
|
|
261
|
+
if (alert.error_count) lines.push(`Errors: ${alert.error_count}`);
|
|
262
|
+
if (alert.idle_time_ms) {
|
|
263
|
+
lines.push(`Idle: ${Math.round(alert.idle_time_ms / 1000)}s`);
|
|
264
|
+
}
|
|
265
|
+
if (alert.cost_usd) lines.push(`Cost: $${Number(alert.cost_usd).toFixed(3)}`);
|
|
266
|
+
return lines.join("\n");
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function pollAgentAlerts() {
|
|
270
|
+
if (process.env.VITEST) return;
|
|
271
|
+
const path = getAgentAlertsPath();
|
|
272
|
+
if (!existsSync(path)) return;
|
|
273
|
+
let data;
|
|
274
|
+
try {
|
|
275
|
+
data = await readFile(path, "utf8");
|
|
276
|
+
} catch {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (!data || data.length <= agentAlertsOffset) return;
|
|
280
|
+
const chunk = data.slice(agentAlertsOffset);
|
|
281
|
+
agentAlertsOffset = data.length;
|
|
282
|
+
const lines = chunk.split(/\r?\n/);
|
|
283
|
+
for (const line of lines) {
|
|
284
|
+
const trimmed = line.trim();
|
|
285
|
+
if (!trimmed) continue;
|
|
286
|
+
let alert;
|
|
287
|
+
try {
|
|
288
|
+
alert = JSON.parse(trimmed);
|
|
289
|
+
} catch {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const dedupKey = `${alert.type || "alert"}:${alert.attempt_id || "unknown"}:${alert.timestamp || ""}`;
|
|
293
|
+
if (agentAlertsDedup.has(dedupKey)) continue;
|
|
294
|
+
rememberAlert(dedupKey);
|
|
295
|
+
console.warn(
|
|
296
|
+
`[agent-work-analyzer] ${alert.severity || "medium"} ${alert.type || "alert"} ${alert.attempt_id || ""}`,
|
|
297
|
+
);
|
|
298
|
+
if (
|
|
299
|
+
telegramToken &&
|
|
300
|
+
telegramChatId &&
|
|
301
|
+
process.env.AGENT_ALERTS_NOTIFY === "true"
|
|
302
|
+
) {
|
|
303
|
+
void sendTelegramMessage(formatAgentAlert(alert), {
|
|
304
|
+
dedupKey: `agent-alert:${alert.type || "alert"}:${alert.attempt_id || "unknown"}`,
|
|
305
|
+
}).catch(() => {});
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function startAgentWorkAnalyzer() {
|
|
311
|
+
if (agentWorkAnalyzerActive) return;
|
|
312
|
+
if (process.env.AGENT_WORK_ANALYZER_ENABLED === "false") return;
|
|
313
|
+
try {
|
|
314
|
+
void startAnalyzer();
|
|
315
|
+
agentWorkAnalyzerActive = true;
|
|
316
|
+
console.log("[monitor] agent-work analyzer started");
|
|
317
|
+
} catch (err) {
|
|
318
|
+
console.warn(`[monitor] agent-work analyzer failed to start: ${err.message}`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function stopAgentWorkAnalyzer() {
|
|
323
|
+
if (!agentWorkAnalyzerActive) return;
|
|
324
|
+
try {
|
|
325
|
+
stopAnalyzer();
|
|
326
|
+
} catch {
|
|
327
|
+
/* ignore */
|
|
328
|
+
}
|
|
329
|
+
agentWorkAnalyzerActive = false;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function startAgentAlertTailer() {
|
|
333
|
+
if (agentAlertsTimer) return;
|
|
334
|
+
agentAlertsTimer = setInterval(() => {
|
|
335
|
+
void pollAgentAlerts();
|
|
336
|
+
}, AGENT_ALERT_POLL_MS);
|
|
337
|
+
agentAlertsTimer.unref?.();
|
|
338
|
+
void pollAgentAlerts();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function stopAgentAlertTailer() {
|
|
342
|
+
if (agentAlertsTimer) {
|
|
343
|
+
clearInterval(agentAlertsTimer);
|
|
344
|
+
agentAlertsTimer = null;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
230
348
|
/**
|
|
231
349
|
* Write an anomaly signal to the shared signal file for the orchestrator to pick up.
|
|
232
350
|
* The orchestrator reads this file in Process-AnomalySignals and acts accordingly.
|
|
@@ -1157,6 +1275,8 @@ function restartSelf(reason) {
|
|
|
1157
1275
|
stopTaskPlannerStatusLoop();
|
|
1158
1276
|
stopMonitorMonitorSupervisor({ preserveRunning: true });
|
|
1159
1277
|
stopAutoUpdateLoop();
|
|
1278
|
+
stopAgentAlertTailer();
|
|
1279
|
+
stopAgentWorkAnalyzer();
|
|
1160
1280
|
stopSelfWatcher();
|
|
1161
1281
|
stopWatcher();
|
|
1162
1282
|
stopEnvWatchers();
|
|
@@ -7900,6 +8020,27 @@ async function sendTelegramMessage(text, options = {}) {
|
|
|
7900
8020
|
});
|
|
7901
8021
|
}
|
|
7902
8022
|
|
|
8023
|
+
globalThis.__bosunNotifyAnomaly = (anomaly) => {
|
|
8024
|
+
if (process.env.BOSUN_INTERNAL_ANOMALY_NOTIFY === "false") return;
|
|
8025
|
+
if (!telegramToken || !telegramChatId) return;
|
|
8026
|
+
const icon =
|
|
8027
|
+
anomaly.severity === "CRITICAL"
|
|
8028
|
+
? "🔴"
|
|
8029
|
+
: anomaly.severity === "HIGH"
|
|
8030
|
+
? "🟠"
|
|
8031
|
+
: anomaly.severity === "MEDIUM"
|
|
8032
|
+
? "🟡"
|
|
8033
|
+
: "⚪️";
|
|
8034
|
+
const lines = [
|
|
8035
|
+
`${icon} Internal Anomaly: ${anomaly.type}`,
|
|
8036
|
+
`Attempt: ${anomaly.processId || anomaly.shortId || "unknown"}`,
|
|
8037
|
+
anomaly.message ? `Message: ${anomaly.message}` : null,
|
|
8038
|
+
].filter(Boolean);
|
|
8039
|
+
void sendTelegramMessage(lines.join("\n"), {
|
|
8040
|
+
dedupKey: `internal-anomaly:${anomaly.type}:${anomaly.processId || anomaly.shortId || "unknown"}`,
|
|
8041
|
+
}).catch(() => {});
|
|
8042
|
+
};
|
|
8043
|
+
|
|
7903
8044
|
function enqueueTelegramCommand(handler) {
|
|
7904
8045
|
telegramCommandQueue.push(handler);
|
|
7905
8046
|
void drainTelegramCommandQueue();
|
|
@@ -10775,6 +10916,8 @@ function selfRestartForSourceChange(filename) {
|
|
|
10775
10916
|
stopTaskPlannerStatusLoop();
|
|
10776
10917
|
stopMonitorMonitorSupervisor({ preserveRunning: true });
|
|
10777
10918
|
stopAutoUpdateLoop();
|
|
10919
|
+
stopAgentAlertTailer();
|
|
10920
|
+
stopAgentWorkAnalyzer();
|
|
10778
10921
|
stopSelfWatcher();
|
|
10779
10922
|
stopWatcher();
|
|
10780
10923
|
stopEnvWatchers();
|
|
@@ -11262,6 +11405,8 @@ process.on("SIGINT", async () => {
|
|
|
11262
11405
|
prCleanupDaemon.stop();
|
|
11263
11406
|
}
|
|
11264
11407
|
stopAutoUpdateLoop();
|
|
11408
|
+
stopAgentAlertTailer();
|
|
11409
|
+
stopAgentWorkAnalyzer();
|
|
11265
11410
|
stopSelfWatcher();
|
|
11266
11411
|
stopEnvWatchers();
|
|
11267
11412
|
if (watcher) {
|
|
@@ -11304,6 +11449,8 @@ process.on("exit", () => {
|
|
|
11304
11449
|
stopTaskPlannerStatusLoop();
|
|
11305
11450
|
stopGitHubReconciler();
|
|
11306
11451
|
stopMonitorMonitorSupervisor();
|
|
11452
|
+
stopAgentAlertTailer();
|
|
11453
|
+
stopAgentWorkAnalyzer();
|
|
11307
11454
|
if (vkLogStream) {
|
|
11308
11455
|
vkLogStream.stop();
|
|
11309
11456
|
vkLogStream = null;
|
|
@@ -11323,6 +11470,8 @@ process.on("SIGTERM", async () => {
|
|
|
11323
11470
|
vkLogStream = null;
|
|
11324
11471
|
}
|
|
11325
11472
|
stopAutoUpdateLoop();
|
|
11473
|
+
stopAgentAlertTailer();
|
|
11474
|
+
stopAgentWorkAnalyzer();
|
|
11326
11475
|
stopSelfWatcher();
|
|
11327
11476
|
stopEnvWatchers();
|
|
11328
11477
|
if (watcher) {
|
|
@@ -12225,6 +12374,8 @@ void restoreLiveDigest()
|
|
|
12225
12374
|
.then(() => initStatusBoard().catch(() => {}));
|
|
12226
12375
|
|
|
12227
12376
|
// ── Start long-running devmode monitor-monitor supervisor ───────────────────
|
|
12377
|
+
startAgentWorkAnalyzer();
|
|
12378
|
+
startAgentAlertTailer();
|
|
12228
12379
|
startMonitorMonitorSupervisor();
|
|
12229
12380
|
startTaskPlannerStatusLoop();
|
|
12230
12381
|
restartGitHubReconciler();
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosun",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.2",
|
|
4
4
|
"description": "AI-powered orchestrator supervisor — manages AI agent executors with failover, auto-restarts on failure, analyzes crashes with Codex SDK, creates PRs via Vibe-Kanban API, and sends Telegram notifications. Supports N executors with weighted distribution, multi-repo projects, and auto-setup.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache 2.0",
|
|
7
|
-
"author": "VirtEngine Maintainers <
|
|
7
|
+
"author": "VirtEngine Maintainers <hello@virtengine.com>",
|
|
8
8
|
"homepage": "https://bosun.virtengine.com",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
@@ -189,6 +189,7 @@
|
|
|
189
189
|
"hook-profiles.mjs",
|
|
190
190
|
"agent-hook-bridge.mjs",
|
|
191
191
|
"agent-supervisor.mjs",
|
|
192
|
+
"agent-work-analyzer.mjs",
|
|
192
193
|
"startup-service.mjs",
|
|
193
194
|
"telegram-sentinel.mjs",
|
|
194
195
|
"whatsapp-channel.mjs",
|
package/rotate-agent-logs.sh
CHANGED
|
@@ -15,11 +15,12 @@ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
|
15
15
|
LOG_DIR="$REPO_ROOT/.cache/agent-work-logs"
|
|
16
16
|
ARCHIVE_DIR="$LOG_DIR/archive"
|
|
17
17
|
|
|
18
|
-
# Retention periods
|
|
19
|
-
STREAM_RETENTION_DAYS=30
|
|
20
|
-
ERROR_RETENTION_DAYS=90
|
|
21
|
-
SESSION_RETENTION_COUNT=100
|
|
22
|
-
ARCHIVE_RETENTION_DAYS=180
|
|
18
|
+
# Retention periods (override via env)
|
|
19
|
+
STREAM_RETENTION_DAYS="${AGENT_WORK_STREAM_RETENTION_DAYS:-30}"
|
|
20
|
+
ERROR_RETENTION_DAYS="${AGENT_WORK_ERROR_RETENTION_DAYS:-90}"
|
|
21
|
+
SESSION_RETENTION_COUNT="${AGENT_WORK_SESSION_RETENTION_COUNT:-100}"
|
|
22
|
+
ARCHIVE_RETENTION_DAYS="${AGENT_WORK_ARCHIVE_RETENTION_DAYS:-180}"
|
|
23
|
+
METRICS_ROTATION_ENABLED="${AGENT_WORK_METRICS_ROTATION_ENABLED:-true}"
|
|
23
24
|
|
|
24
25
|
# ── Functions ───────────────────────────────────────────────────────────────
|
|
25
26
|
|
|
@@ -111,10 +112,10 @@ if [ -f "$LOG_DIR/agent-alerts.jsonl" ]; then
|
|
|
111
112
|
rotate_file "$LOG_DIR/agent-alerts.jsonl" "$ALERTS_ARCHIVE" "$STREAM_RETENTION_DAYS"
|
|
112
113
|
fi
|
|
113
114
|
|
|
114
|
-
# Metrics log is kept indefinitely (compressed monthly)
|
|
115
|
+
# Metrics log is kept indefinitely (compressed monthly unless disabled)
|
|
115
116
|
if [ -f "$LOG_DIR/agent-metrics.jsonl" ]; then
|
|
116
117
|
# Only rotate on first day of month
|
|
117
|
-
if [ "$(date +%d)" = "01" ]; then
|
|
118
|
+
if [ "$METRICS_ROTATION_ENABLED" != "false" ] && [ "$(date +%d)" = "01" ]; then
|
|
118
119
|
METRICS_ARCHIVE="agent-metrics-$(date -d 'last month' +%Y%m).jsonl.gz"
|
|
119
120
|
rotate_file "$LOG_DIR/agent-metrics.jsonl" "$METRICS_ARCHIVE" ""
|
|
120
121
|
fi
|