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 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.2",
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
  );
@@ -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
- const result = await agentPool.launchEphemeralThread(finalPrompt, cwd, timeoutMs);
464
- ctx.log(node.id, `Agent completed: success=${result.success}`);
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;