bosun 0.35.1 → 0.35.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/monitor.mjs CHANGED
@@ -724,7 +724,7 @@ async function pollAgentAlerts() {
724
724
  telegramChatId &&
725
725
  process.env.AGENT_ALERTS_NOTIFY === "true"
726
726
  ) {
727
- runDetached("agent-alerts:telegram", () =>
727
+ runDetached("agent-alerts:notify", () =>
728
728
  sendTelegramMessage(formatAgentAlert(alert), {
729
729
  dedupKey: `agent-alert:${alert.type || "alert"}:${alert.attempt_id || "unknown"}`,
730
730
  }),
@@ -780,10 +780,10 @@ function startAgentAlertTailer() {
780
780
  if (agentAlertsTimer) return;
781
781
  loadAgentAlertsState();
782
782
  agentAlertsTimer = setInterval(() => {
783
- runDetached("agent-alerts:poll", pollAgentAlerts);
783
+ runDetached("agent-alerts:poll-interval", pollAgentAlerts);
784
784
  }, AGENT_ALERT_POLL_MS);
785
785
  agentAlertsTimer.unref?.();
786
- runDetached("agent-alerts:poll", pollAgentAlerts);
786
+ runDetached("agent-alerts:poll-startup", pollAgentAlerts);
787
787
  }
788
788
 
789
789
  function stopAgentAlertTailer() {
@@ -2681,9 +2681,7 @@ async function handleMonitorFailure(reason, err) {
2681
2681
  // Ensure we retry after safe-mode window if still running.
2682
2682
  if (!shuttingDown) {
2683
2683
  setTimeout(() => {
2684
- if (!shuttingDown) {
2685
- runDetached("start-process:hard-cap-retry", startProcess);
2686
- }
2684
+ if (!shuttingDown) void startProcess();
2687
2685
  }, pauseMs + 1000);
2688
2686
  }
2689
2687
  return;
@@ -2790,19 +2788,25 @@ function runGuarded(reason, fn) {
2790
2788
  reportGuardedFailure(reason, err);
2791
2789
  }
2792
2790
  }
2791
+
2793
2792
  function runDetached(label, promiseOrFn) {
2794
- const logFailure = (err) => {
2795
- const message = formatMonitorError(err);
2796
- console.warn(`[monitor] detached ${label} failed: ${message}`);
2797
- };
2793
+ if (shuttingDown) return;
2798
2794
  try {
2799
- const result =
2795
+ const pending =
2800
2796
  typeof promiseOrFn === "function" ? promiseOrFn() : promiseOrFn;
2801
- if (result && typeof result.then === "function") {
2802
- result.catch((err) => logFailure(err));
2797
+ if (pending && typeof pending.then === "function") {
2798
+ pending.catch((err) => {
2799
+ const error = err instanceof Error ? err : new Error(formatMonitorError(err));
2800
+ console.warn(
2801
+ `[monitor] detached task failed (${label}): ${error.stack || error.message}`,
2802
+ );
2803
+ });
2803
2804
  }
2804
2805
  } catch (err) {
2805
- logFailure(err);
2806
+ const error = err instanceof Error ? err : new Error(formatMonitorError(err));
2807
+ console.warn(
2808
+ `[monitor] detached task failed (${label}): ${error.stack || error.message}`,
2809
+ );
2806
2810
  }
2807
2811
  }
2808
2812
 
@@ -3185,19 +3189,17 @@ function notifyVkError(line) {
3185
3189
  ]
3186
3190
  .filter(Boolean)
3187
3191
  .join("\n");
3188
- runDetached("vk-error:telegram", () =>
3192
+ runDetached("vk-error:notify", () =>
3189
3193
  sendTelegramMessage(message, { parseMode: "HTML" }),
3190
3194
  );
3191
- runDetached("vk-recovery:notify", () => triggerVibeKanbanRecovery(line));
3195
+ runDetached("vk-error:trigger-recovery", () => triggerVibeKanbanRecovery(line));
3192
3196
  }
3193
3197
 
3194
3198
  function notifyCodexTrigger(context) {
3195
3199
  if (!telegramToken || !telegramChatId) {
3196
3200
  return;
3197
3201
  }
3198
- runDetached("codex-trigger:telegram", () =>
3199
- sendTelegramMessage(`Codex triggered: ${context}`),
3200
- );
3202
+ void sendTelegramMessage(`Codex triggered: ${context}`);
3201
3203
  }
3202
3204
 
3203
3205
  async function runCodexRecovery(reason) {
@@ -3431,10 +3433,7 @@ function scheduleVibeKanbanRestart() {
3431
3433
  console.log(
3432
3434
  `[monitor] restarting vibe-kanban in ${delay}ms (attempt ${vkRestartCount}/${vkMaxRestarts})`,
3433
3435
  );
3434
- setTimeout(
3435
- () => runDetached("vk-restart:scheduled", startVibeKanbanProcess),
3436
- delay,
3437
- );
3436
+ setTimeout(() => void startVibeKanbanProcess(), delay);
3438
3437
  }
3439
3438
 
3440
3439
  async function canConnectTcp(host, port, timeoutMs = 1200) {
@@ -3532,7 +3531,7 @@ function restartVibeKanbanProcess() {
3532
3531
  /* best effort */
3533
3532
  }
3534
3533
  } else {
3535
- runDetached("vk-restart:manual", startVibeKanbanProcess);
3534
+ void startVibeKanbanProcess();
3536
3535
  }
3537
3536
  }
3538
3537
 
@@ -3747,9 +3746,7 @@ function ensureVkLogStream() {
3747
3746
  }
3748
3747
 
3749
3748
  // Discover any active sessions immediately and keep polling for new sessions
3750
- runDetached("vk-session-discovery:startup", () =>
3751
- refreshVkSessionStreams("startup"),
3752
- );
3749
+ void refreshVkSessionStreams("startup");
3753
3750
  ensureVkSessionDiscoveryLoop();
3754
3751
  }
3755
3752
 
@@ -3757,9 +3754,7 @@ function ensureVkSessionDiscoveryLoop() {
3757
3754
  if (vkSessionDiscoveryTimer) return;
3758
3755
  if (!Number.isFinite(vkEnsureIntervalMs) || vkEnsureIntervalMs <= 0) return;
3759
3756
  vkSessionDiscoveryTimer = setInterval(() => {
3760
- runDetached("vk-session-discovery:periodic", () =>
3761
- refreshVkSessionStreams("periodic"),
3762
- );
3757
+ void refreshVkSessionStreams("periodic");
3763
3758
  }, vkEnsureIntervalMs);
3764
3759
  }
3765
3760
 
@@ -3941,9 +3936,7 @@ async function triggerVibeKanbanRecovery(reason) {
3941
3936
  const notice = codexEnabled
3942
3937
  ? `Codex recovery triggered: vibe-kanban unreachable. Attempting restart. (${link})`
3943
3938
  : `Vibe-kanban recovery triggered (Codex disabled). Attempting restart. (${link})`;
3944
- runDetached("vk-recovery-notify", () =>
3945
- sendTelegramMessage(notice, { parseMode: "HTML" }),
3946
- );
3939
+ void sendTelegramMessage(notice, { parseMode: "HTML" });
3947
3940
  }
3948
3941
  await runCodexRecovery(reason || "vibe-kanban unreachable");
3949
3942
  restartVibeKanbanProcess();
@@ -4053,7 +4046,7 @@ async function fetchVk(path, opts = {}) {
4053
4046
  if (shouldLogVkWarning("network-error")) {
4054
4047
  console.warn(`[monitor] fetchVk ${method} ${path} error: ${msg}`);
4055
4048
  }
4056
- runDetached("vk-recovery:network", () =>
4049
+ runDetached("fetchVk:network-recovery", () =>
4057
4050
  triggerVibeKanbanRecovery(
4058
4051
  `fetchVk ${method} ${path} network error: ${msg}`,
4059
4052
  ),
@@ -4074,7 +4067,7 @@ async function fetchVk(path, opts = {}) {
4074
4067
  `[monitor] fetchVk ${method} ${path} error: invalid response object (res=${!!res}, res.ok=${res?.ok})`,
4075
4068
  );
4076
4069
  }
4077
- runDetached("vk-recovery:invalid-response", () =>
4070
+ runDetached("fetchVk:invalid-response-recovery", () =>
4078
4071
  triggerVibeKanbanRecovery(
4079
4072
  `fetchVk ${method} ${path} invalid response object`,
4080
4073
  ),
@@ -4091,7 +4084,7 @@ async function fetchVk(path, opts = {}) {
4091
4084
  );
4092
4085
  }
4093
4086
  if (res.status >= 500) {
4094
- runDetached("vk-recovery:http", () =>
4087
+ runDetached("fetchVk:http-5xx-recovery", () =>
4095
4088
  triggerVibeKanbanRecovery(
4096
4089
  `fetchVk ${method} ${path} HTTP ${res.status}`,
4097
4090
  ),
@@ -4132,7 +4125,7 @@ async function fetchVk(path, opts = {}) {
4132
4125
  );
4133
4126
  }
4134
4127
  }
4135
- runDetached("vk-recovery:non-json", () =>
4128
+ runDetached("fetchVk:non-json-recovery", () =>
4136
4129
  triggerVibeKanbanRecovery(
4137
4130
  `fetchVk ${method} ${path} non-JSON response`,
4138
4131
  ),
@@ -8094,12 +8087,10 @@ async function runMergeStrategyAnalysis(ctx, opts = {}) {
8094
8087
  // Re-run analysis after the wait period
8095
8088
  setTimeout(
8096
8089
  () => {
8097
- runDetached("merge-strategy:wait-recheck", () =>
8098
- runMergeStrategyAnalysis({
8099
- ...ctx,
8100
- ciStatus: "re-check",
8101
- }),
8102
- );
8090
+ void runMergeStrategyAnalysis({
8091
+ ...ctx,
8092
+ ciStatus: "re-check",
8093
+ });
8103
8094
  },
8104
8095
  (execResult.waitSeconds || 300) * 1000,
8105
8096
  );
@@ -8406,12 +8397,10 @@ async function actOnAssessment(ctx, decision) {
8406
8397
  const waitSec = decision.waitSeconds || 300;
8407
8398
  console.log(`[${tag}] → wait ${waitSec}s`);
8408
8399
  setTimeout(() => {
8409
- runDetached("task-assessment:recheck", () =>
8410
- runTaskAssessment({
8411
- ...ctx,
8412
- trigger: "reassessment",
8413
- }),
8414
- );
8400
+ void runTaskAssessment({
8401
+ ...ctx,
8402
+ trigger: "reassessment",
8403
+ });
8415
8404
  }, waitSec * 1000);
8416
8405
  break;
8417
8406
  }
@@ -10379,7 +10368,7 @@ globalThis.__bosunNotifyAnomaly = (anomaly) => {
10379
10368
 
10380
10369
  function enqueueTelegramCommand(handler) {
10381
10370
  telegramCommandQueue.push(handler);
10382
- void drainTelegramCommandQueue();
10371
+ runDetached("telegram-commands:drain", drainTelegramCommandQueue);
10383
10372
  }
10384
10373
 
10385
10374
  function drainTelegramCommandQueue() {
@@ -10851,7 +10840,6 @@ async function startTelegramNotifier() {
10851
10840
  await checkStatusMilestones();
10852
10841
  };
10853
10842
 
10854
-
10855
10843
  // Suppress "Notifier started" message on rapid restarts (e.g. code-change restarts).
10856
10844
  // If the last start was <60s ago, skip the notification — just log locally.
10857
10845
  const lastStartPath = resolve(
@@ -10876,16 +10864,18 @@ async function startTelegramNotifier() {
10876
10864
  `[monitor] notifier restarted (suppressed telegram notification — rapid restart)`,
10877
10865
  );
10878
10866
  } else {
10879
- runDetached("telegram-notifier:startup", () =>
10867
+ runDetached("telegram-notifier:startup-message", () =>
10880
10868
  sendTelegramMessage(`${projectName} Orchestrator Notifier started.`),
10881
10869
  );
10882
10870
  }
10883
- telegramNotifierTimeout = setTimeout(() => {
10884
- runDetached("telegram-notifier:tick", sendUpdate);
10885
- }, intervalMs);
10886
- telegramNotifierInterval = setInterval(() => {
10887
- runDetached("telegram-notifier:tick", sendUpdate);
10888
- }, intervalMs);
10871
+ telegramNotifierTimeout = setTimeout(
10872
+ () => runDetached("telegram-notifier:timeout-update", sendUpdate),
10873
+ intervalMs,
10874
+ );
10875
+ telegramNotifierInterval = setInterval(
10876
+ () => runDetached("telegram-notifier:interval-update", sendUpdate),
10877
+ intervalMs,
10878
+ );
10889
10879
  }
10890
10880
 
10891
10881
  async function checkStatusMilestones() {
@@ -13921,9 +13911,7 @@ async function startProcess() {
13921
13911
  if (!shuttingDown) {
13922
13912
  const retryMs = Math.max(5_000, restartDelayMs || 0);
13923
13913
  safeSetTimeout("startProcess-retry", () => {
13924
- if (!shuttingDown) {
13925
- return startProcess();
13926
- }
13914
+ if (!shuttingDown) void startProcess();
13927
13915
  }, retryMs);
13928
13916
  }
13929
13917
  }
@@ -16057,10 +16045,7 @@ if (isExecutorDisabled()) {
16057
16045
  if (projectId) {
16058
16046
  syncEngine = createSyncEngine({
16059
16047
  projectId,
16060
- syncIntervalMs: Math.max(
16061
- 60_000,
16062
- Number(process.env.KANBAN_SYNC_INTERVAL_MS) || 5 * 60 * 1000,
16063
- ), // default 5 min (env: KANBAN_SYNC_INTERVAL_MS)
16048
+ syncIntervalMs: 60_000, // 1 minute
16064
16049
  syncPolicy: kanbanConfig?.syncPolicy || "internal-primary",
16065
16050
  sendTelegram:
16066
16051
  telegramToken && telegramChatId
@@ -16080,7 +16065,7 @@ if (isExecutorDisabled()) {
16080
16065
  });
16081
16066
  syncEngine.start();
16082
16067
  console.log(
16083
- `[monitor] sync engine started (interval: ${Math.max(60_000, Number(process.env.KANBAN_SYNC_INTERVAL_MS) || 5 * 60 * 1000) / 1000}s, backend=${activeKanbanBackend}, policy=${kanbanConfig?.syncPolicy || "internal-primary"}, project=${projectId})`,
16068
+ `[monitor] sync engine started (interval: 60s, backend=${activeKanbanBackend}, policy=${kanbanConfig?.syncPolicy || "internal-primary"}, project=${projectId})`,
16084
16069
  );
16085
16070
  } else {
16086
16071
  console.log(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosun",
3
- "version": "0.35.1",
3
+ "version": "0.35.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",
package/telegram-bot.mjs CHANGED
@@ -935,7 +935,30 @@ function getBrowserUiUrl() {
935
935
  return appendTokenToUrl(base, token) || base;
936
936
  }
937
937
 
938
- function getBrowserUiUrlOptions() {
938
+ function isTelegramInlineButtonUrlAllowed(inputUrl) {
939
+ try {
940
+ const parsed = new URL(String(inputUrl || "").trim());
941
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
942
+ return false;
943
+ }
944
+ const host = String(parsed.hostname || "").toLowerCase();
945
+ if (!host) return false;
946
+ // Telegram rejects localhost/loopback URLs for inline keyboard URL buttons.
947
+ if (
948
+ host === "localhost" ||
949
+ host === "127.0.0.1" ||
950
+ host === "::1" ||
951
+ host === "[::1]"
952
+ ) {
953
+ return false;
954
+ }
955
+ return true;
956
+ } catch {
957
+ return false;
958
+ }
959
+ }
960
+
961
+ function getBrowserUiUrlOptions({ forTelegramButtons = true } = {}) {
939
962
  const base = String(telegramUiUrl || "").trim();
940
963
  if (!base) return [];
941
964
 
@@ -949,6 +972,9 @@ function getBrowserUiUrlOptions() {
949
972
  if (!url) return;
950
973
  if (seen.has(url)) return;
951
974
  seen.add(url);
975
+ if (forTelegramButtons && !isTelegramInlineButtonUrlAllowed(url)) {
976
+ return;
977
+ }
952
978
  options.push({ label, url });
953
979
  };
954
980