bosun 0.34.2 → 0.34.3
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/cli.mjs +121 -1
- package/package.json +1 -1
- package/telegram-bot.mjs +5 -0
- package/ui-server.mjs +1 -1
- package/workflow-nodes.mjs +109 -2
package/cli.mjs
CHANGED
|
@@ -81,6 +81,7 @@ function showHelp() {
|
|
|
81
81
|
--no-auto-update Disable background auto-update polling
|
|
82
82
|
--daemon, -d Run as a background daemon (detached, with PID file)
|
|
83
83
|
--stop-daemon Stop a running daemon process
|
|
84
|
+
--terminate Hard-stop all bosun processes (daemon + monitor + companions)
|
|
84
85
|
--daemon-status Check if daemon is running
|
|
85
86
|
|
|
86
87
|
ORCHESTRATOR
|
|
@@ -674,6 +675,122 @@ function daemonStatus() {
|
|
|
674
675
|
process.exit(0);
|
|
675
676
|
}
|
|
676
677
|
|
|
678
|
+
function findAllBosunProcessPids() {
|
|
679
|
+
const patterns = [
|
|
680
|
+
"cli.mjs",
|
|
681
|
+
"monitor.mjs",
|
|
682
|
+
"telegram-bot.mjs",
|
|
683
|
+
"telegram-sentinel.mjs",
|
|
684
|
+
"ui-server.mjs",
|
|
685
|
+
];
|
|
686
|
+
const joined = patterns.join("|");
|
|
687
|
+
if (process.platform === "win32") {
|
|
688
|
+
try {
|
|
689
|
+
const out = execFileSync(
|
|
690
|
+
"powershell",
|
|
691
|
+
[
|
|
692
|
+
"-NoProfile",
|
|
693
|
+
"-Command",
|
|
694
|
+
`Get-CimInstance Win32_Process | Where-Object { $_.Name -match '^node(\\.exe)?$' -and $_.CommandLine -match '${joined.replace(/\|/g, "|")}' } | Select-Object -ExpandProperty ProcessId`,
|
|
695
|
+
],
|
|
696
|
+
{ encoding: "utf8", stdio: ["pipe", "pipe", "pipe"], timeout: 4000 },
|
|
697
|
+
).trim();
|
|
698
|
+
if (!out) return [];
|
|
699
|
+
return out
|
|
700
|
+
.split(/\r?\n/)
|
|
701
|
+
.map((s) => Number.parseInt(String(s).trim(), 10))
|
|
702
|
+
.filter((pid) => Number.isFinite(pid) && pid > 0 && pid !== process.pid);
|
|
703
|
+
} catch {
|
|
704
|
+
return [];
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
try {
|
|
708
|
+
const out = execFileSync("pgrep", ["-f", joined], {
|
|
709
|
+
encoding: "utf8",
|
|
710
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
711
|
+
timeout: 4000,
|
|
712
|
+
}).trim();
|
|
713
|
+
if (!out) return [];
|
|
714
|
+
return out
|
|
715
|
+
.split(/\r?\n/)
|
|
716
|
+
.map((s) => Number.parseInt(String(s).trim(), 10))
|
|
717
|
+
.filter((pid) => Number.isFinite(pid) && pid > 0 && pid !== process.pid);
|
|
718
|
+
} catch {
|
|
719
|
+
return [];
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function removeKnownPidFiles() {
|
|
724
|
+
const pidFiles = [
|
|
725
|
+
DAEMON_PID_FILE,
|
|
726
|
+
PID_FILE,
|
|
727
|
+
SENTINEL_PID_FILE,
|
|
728
|
+
SENTINEL_PID_FILE_LEGACY,
|
|
729
|
+
resolve(__dirname, "..", ".cache", "bosun.pid"),
|
|
730
|
+
resolve(process.cwd(), ".cache", "bosun.pid"),
|
|
731
|
+
];
|
|
732
|
+
for (const pidFile of pidFiles) {
|
|
733
|
+
try {
|
|
734
|
+
if (existsSync(pidFile)) unlinkSync(pidFile);
|
|
735
|
+
} catch {
|
|
736
|
+
/* best effort */
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
function terminateBosun() {
|
|
742
|
+
const tracked = [
|
|
743
|
+
getDaemonPid(),
|
|
744
|
+
readAlivePid(PID_FILE),
|
|
745
|
+
readAlivePid(SENTINEL_PID_FILE),
|
|
746
|
+
readAlivePid(SENTINEL_PID_FILE_LEGACY),
|
|
747
|
+
].filter((pid) => Number.isFinite(pid) && pid > 0);
|
|
748
|
+
const ghosts = findGhostDaemonPids();
|
|
749
|
+
const scanned = findAllBosunProcessPids();
|
|
750
|
+
const allPids = Array.from(new Set([...tracked, ...ghosts, ...scanned])).filter(
|
|
751
|
+
(pid) => pid !== process.pid,
|
|
752
|
+
);
|
|
753
|
+
if (allPids.length === 0) {
|
|
754
|
+
removeKnownPidFiles();
|
|
755
|
+
console.log(" No running bosun processes found.");
|
|
756
|
+
process.exit(0);
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
console.log(` Terminating ${allPids.length} bosun process(es): ${allPids.join(", ")}`);
|
|
761
|
+
for (const pid of allPids) {
|
|
762
|
+
try {
|
|
763
|
+
process.kill(pid, "SIGTERM");
|
|
764
|
+
} catch {
|
|
765
|
+
/* already dead */
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const deadline = Date.now() + 5000;
|
|
770
|
+
let alive = allPids.filter((pid) => isProcessAlive(pid));
|
|
771
|
+
while (alive.length > 0 && Date.now() < deadline) {
|
|
772
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 200);
|
|
773
|
+
alive = alive.filter((pid) => isProcessAlive(pid));
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
for (const pid of alive) {
|
|
777
|
+
try {
|
|
778
|
+
process.kill(pid, "SIGKILL");
|
|
779
|
+
} catch {
|
|
780
|
+
/* already dead */
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
removeKnownPidFiles();
|
|
784
|
+
const killed = allPids.length - alive.length;
|
|
785
|
+
console.log(` ✓ Terminated ${killed}/${allPids.length} process(es).`);
|
|
786
|
+
if (alive.length > 0) {
|
|
787
|
+
console.log(` ⚠️ Still alive: ${alive.join(", ")}`);
|
|
788
|
+
process.exit(1);
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
process.exit(0);
|
|
792
|
+
}
|
|
793
|
+
|
|
677
794
|
async function main() {
|
|
678
795
|
// Apply legacy CODEX_MONITOR_* → BOSUN_* env aliases before any config ops
|
|
679
796
|
applyAllCompatibility();
|
|
@@ -793,6 +910,10 @@ async function main() {
|
|
|
793
910
|
stopDaemon();
|
|
794
911
|
return;
|
|
795
912
|
}
|
|
913
|
+
if (args.includes("--terminate")) {
|
|
914
|
+
terminateBosun();
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
796
917
|
if (args.includes("--daemon-status")) {
|
|
797
918
|
daemonStatus();
|
|
798
919
|
return;
|
|
@@ -1500,4 +1621,3 @@ main().catch(async (err) => {
|
|
|
1500
1621
|
await sendCrashNotification(1, null).catch(() => {});
|
|
1501
1622
|
process.exit(1);
|
|
1502
1623
|
});
|
|
1503
|
-
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosun",
|
|
3
|
-
"version": "0.34.
|
|
3
|
+
"version": "0.34.3",
|
|
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",
|
package/telegram-bot.mjs
CHANGED
|
@@ -10439,6 +10439,11 @@ export async function startTelegramBot() {
|
|
|
10439
10439
|
if (miniAppEnabled || miniAppPort > 0) {
|
|
10440
10440
|
try {
|
|
10441
10441
|
await startTelegramUiServer({
|
|
10442
|
+
// Background monitor/bot runtime should not keep opening browser tabs.
|
|
10443
|
+
// Set BOSUN_UI_AUTO_OPEN_BROWSER=1 to opt-in.
|
|
10444
|
+
skipAutoOpen: !["1", "true", "yes", "on"].includes(
|
|
10445
|
+
String(process.env.BOSUN_UI_AUTO_OPEN_BROWSER || "").toLowerCase(),
|
|
10446
|
+
),
|
|
10442
10447
|
dependencies: {
|
|
10443
10448
|
execPrimaryPrompt,
|
|
10444
10449
|
getInternalExecutor: _getInternalExecutor,
|
package/ui-server.mjs
CHANGED
|
@@ -6136,7 +6136,7 @@ async function handleApi(req, res, url) {
|
|
|
6136
6136
|
jsonResponse(res, 404, { ok: false, error: "Task not found." });
|
|
6137
6137
|
return;
|
|
6138
6138
|
}
|
|
6139
|
-
executor.executeTask(task).catch((error) => {
|
|
6139
|
+
executor.executeTask(task, { force: true }).catch((error) => {
|
|
6140
6140
|
console.warn(
|
|
6141
6141
|
`[telegram-ui] dispatch failed for ${taskId}: ${error.message}`,
|
|
6142
6142
|
);
|
package/workflow-nodes.mjs
CHANGED
|
@@ -26,6 +26,76 @@ import { randomUUID } from "node:crypto";
|
|
|
26
26
|
const TAG = "[workflow-nodes]";
|
|
27
27
|
const PORTABLE_WORKTREE_COUNT_COMMAND = "node -e \"const cp=require('node:child_process');const wt=cp.execSync('git worktree list --porcelain',{encoding:'utf8'});const count=(wt.match(/^worktree /gm)||[]).length;process.stdout.write(String(count)+'\\\\n');\"";
|
|
28
28
|
const PORTABLE_PRUNE_AND_COUNT_WORKTREES_COMMAND = "node -e \"const cp=require('node:child_process');cp.execSync('git worktree prune',{stdio:'ignore'});const wt=cp.execSync('git worktree list --porcelain',{encoding:'utf8'});const count=(wt.match(/^worktree /gm)||[]).length;process.stdout.write(String(count)+'\\\\n');\"";
|
|
29
|
+
const WORKFLOW_AGENT_HEARTBEAT_MS = (() => {
|
|
30
|
+
const raw = Number(process.env.WORKFLOW_AGENT_HEARTBEAT_MS || 30000);
|
|
31
|
+
if (!Number.isFinite(raw)) return 30000;
|
|
32
|
+
return Math.max(5000, Math.min(120000, Math.trunc(raw)));
|
|
33
|
+
})();
|
|
34
|
+
|
|
35
|
+
function trimLogText(value, max = 180) {
|
|
36
|
+
const text = String(value || "").replace(/\s+/g, " ").trim();
|
|
37
|
+
if (!text) return "";
|
|
38
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function summarizeAgentStreamEvent(event) {
|
|
42
|
+
const type = String(event?.type || "").trim();
|
|
43
|
+
if (!type) return "";
|
|
44
|
+
|
|
45
|
+
if (
|
|
46
|
+
type === "response.output_text.delta" ||
|
|
47
|
+
type === "response.output_text.done" ||
|
|
48
|
+
type === "item.updated"
|
|
49
|
+
) {
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (type === "tool_call") {
|
|
54
|
+
return `Tool call: ${event?.tool_name || event?.data?.tool_name || "unknown"}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (type === "tool_result") {
|
|
58
|
+
const name = event?.tool_name || event?.data?.tool_name || "unknown";
|
|
59
|
+
return `Tool result: ${name}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (type === "error") {
|
|
63
|
+
return `Agent error: ${trimLogText(event?.error || event?.message || "unknown error", 220)}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const messageText = trimLogText(
|
|
67
|
+
event?.message?.content ||
|
|
68
|
+
event?.message?.text ||
|
|
69
|
+
event?.content ||
|
|
70
|
+
event?.text ||
|
|
71
|
+
event?.data?.content ||
|
|
72
|
+
event?.data?.text ||
|
|
73
|
+
"",
|
|
74
|
+
220,
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
if (messageText) {
|
|
78
|
+
if (
|
|
79
|
+
type === "agent_message" ||
|
|
80
|
+
type === "assistant_message" ||
|
|
81
|
+
type === "message" ||
|
|
82
|
+
type === "item.completed"
|
|
83
|
+
) {
|
|
84
|
+
return `Agent: ${messageText}`;
|
|
85
|
+
}
|
|
86
|
+
return `${type}: ${messageText}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (
|
|
90
|
+
type === "turn.complete" ||
|
|
91
|
+
type === "session.completed" ||
|
|
92
|
+
type === "response.completed"
|
|
93
|
+
) {
|
|
94
|
+
return `Agent event: ${type}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return "";
|
|
98
|
+
}
|
|
29
99
|
|
|
30
100
|
function normalizeLegacyWorkflowCommand(command) {
|
|
31
101
|
let normalized = String(command || "");
|
|
@@ -460,8 +530,45 @@ registerNodeType("action.run_agent", {
|
|
|
460
530
|
// Use the engine's service injection to call agent pool
|
|
461
531
|
const agentPool = engine.services?.agentPool;
|
|
462
532
|
if (agentPool?.launchEphemeralThread) {
|
|
463
|
-
|
|
464
|
-
|
|
533
|
+
let streamEventCount = 0;
|
|
534
|
+
let lastStreamLog = "";
|
|
535
|
+
const startedAt = Date.now();
|
|
536
|
+
const launchExtra = {};
|
|
537
|
+
if (sdk && sdk !== "auto") launchExtra.sdk = sdk;
|
|
538
|
+
|
|
539
|
+
launchExtra.onEvent = (event) => {
|
|
540
|
+
try {
|
|
541
|
+
const line = summarizeAgentStreamEvent(event);
|
|
542
|
+
if (!line || line === lastStreamLog) return;
|
|
543
|
+
lastStreamLog = line;
|
|
544
|
+
streamEventCount += 1;
|
|
545
|
+
ctx.log(node.id, line);
|
|
546
|
+
} catch {
|
|
547
|
+
// Stream callbacks must never crash workflow execution.
|
|
548
|
+
}
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
const heartbeat = setInterval(() => {
|
|
552
|
+
const elapsedSec = Math.max(1, Math.round((Date.now() - startedAt) / 1000));
|
|
553
|
+
ctx.log(node.id, `Agent still running (${elapsedSec}s elapsed)`);
|
|
554
|
+
}, WORKFLOW_AGENT_HEARTBEAT_MS);
|
|
555
|
+
|
|
556
|
+
let result;
|
|
557
|
+
try {
|
|
558
|
+
result = await agentPool.launchEphemeralThread(
|
|
559
|
+
finalPrompt,
|
|
560
|
+
cwd,
|
|
561
|
+
timeoutMs,
|
|
562
|
+
launchExtra,
|
|
563
|
+
);
|
|
564
|
+
} finally {
|
|
565
|
+
clearInterval(heartbeat);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
ctx.log(
|
|
569
|
+
node.id,
|
|
570
|
+
`Agent completed: success=${result.success} streamEvents=${streamEventCount}`,
|
|
571
|
+
);
|
|
465
572
|
|
|
466
573
|
// Propagate session/thread IDs for downstream chaining
|
|
467
574
|
const threadId = result.threadId || result.sessionId || null;
|