bosun 0.41.0 → 0.41.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.
Files changed (64) hide show
  1. package/.env.example +8 -0
  2. package/README.md +20 -0
  3. package/agent/agent-event-bus.mjs +248 -6
  4. package/agent/agent-pool.mjs +125 -28
  5. package/agent/agent-work-analyzer.mjs +8 -16
  6. package/agent/retry-queue.mjs +164 -0
  7. package/bosun.config.example.json +25 -0
  8. package/bosun.schema.json +825 -183
  9. package/cli.mjs +59 -5
  10. package/config/config.mjs +130 -3
  11. package/infra/monitor.mjs +693 -67
  12. package/infra/runtime-accumulator.mjs +376 -84
  13. package/infra/session-tracker.mjs +82 -25
  14. package/lib/codebase-audit.mjs +133 -18
  15. package/package.json +23 -4
  16. package/server/setup-web-server.mjs +25 -0
  17. package/server/ui-server.mjs +248 -29
  18. package/setup.mjs +27 -24
  19. package/shell/codex-shell.mjs +34 -3
  20. package/shell/copilot-shell.mjs +50 -8
  21. package/task/msg-hub.mjs +193 -0
  22. package/task/pipeline.mjs +544 -0
  23. package/task/task-cli.mjs +38 -2
  24. package/task/task-executor-pipeline.mjs +143 -0
  25. package/task/task-executor.mjs +36 -27
  26. package/telegram/get-telegram-chat-id.mjs +57 -47
  27. package/ui/components/workspace-switcher.js +7 -7
  28. package/ui/demo-defaults.js +15694 -10573
  29. package/ui/modules/settings-schema.js +2 -0
  30. package/ui/modules/state.js +54 -57
  31. package/ui/modules/voice-client-sdk.js +375 -36
  32. package/ui/modules/voice-client.js +140 -31
  33. package/ui/setup.html +68 -2
  34. package/ui/styles/components.css +57 -0
  35. package/ui/styles.css +201 -1
  36. package/ui/tabs/dashboard.js +74 -0
  37. package/ui/tabs/logs.js +10 -0
  38. package/ui/tabs/settings.js +178 -99
  39. package/ui/tabs/tasks.js +31 -1
  40. package/ui/tabs/telemetry.js +34 -0
  41. package/ui/tabs/workflow-canvas-utils.mjs +8 -1
  42. package/ui/tabs/workflows.js +532 -275
  43. package/voice/voice-agents-sdk.mjs +1 -1
  44. package/voice/voice-relay.mjs +6 -6
  45. package/workflow/declarative-workflows.mjs +145 -0
  46. package/workflow/msg-hub.mjs +237 -0
  47. package/workflow/pipeline-workflows.mjs +287 -0
  48. package/workflow/pipeline.mjs +828 -315
  49. package/workflow/workflow-cli.mjs +128 -0
  50. package/workflow/workflow-engine.mjs +329 -17
  51. package/workflow/workflow-nodes/custom-loader.mjs +250 -0
  52. package/workflow/workflow-nodes.mjs +1955 -223
  53. package/workflow/workflow-templates.mjs +26 -8
  54. package/workflow-templates/agents.mjs +0 -1
  55. package/workflow-templates/bosun-native.mjs +212 -2
  56. package/workflow-templates/continuation-loop.mjs +339 -0
  57. package/workflow-templates/github.mjs +516 -40
  58. package/workflow-templates/planning.mjs +446 -17
  59. package/workflow-templates/reliability.mjs +65 -12
  60. package/workflow-templates/task-batch.mjs +24 -8
  61. package/workflow-templates/task-lifecycle.mjs +83 -6
  62. package/workspace/context-cache.mjs +66 -18
  63. package/workspace/workspace-manager.mjs +2 -1
  64. package/workflow-templates/issue-continuation.mjs +0 -243
@@ -50,6 +50,7 @@ import {
50
50
  execWithRetry,
51
51
  invalidateThread,
52
52
  } from "../agent/agent-pool.mjs";
53
+ import { withTaskLifetimeTotals } from "../infra/runtime-accumulator.mjs";
53
54
  import { resolveAgentPrompts } from "../agent/agent-prompts.mjs";
54
55
  import {
55
56
  listActiveWorktrees,
@@ -140,6 +141,12 @@ import {
140
141
  addSessionEventListener,
141
142
  } from "../infra/session-tracker.mjs";
142
143
  import { ensureTestRuntimeSandbox } from "../infra/test-runtime.mjs";
144
+ import {
145
+ addSessionAccumulationListener,
146
+ getCompletedSessions,
147
+ getRuntimeStats,
148
+ getTaskLifetimeTotals,
149
+ } from "../infra/runtime-accumulator.mjs";
143
150
  import {
144
151
  collectDiffStats,
145
152
  getCompactDiffSummary,
@@ -3462,6 +3469,7 @@ const DEFAULT_AUTO_OPEN_COOLDOWN_MS = 12 * 60 * 60 * 1000; // 12h
3462
3469
  const DEFAULT_SESSION_TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
3463
3470
  const wsClients = new Set();
3464
3471
  let sessionListenerAttached = false;
3472
+ let sessionAccumulatorListenerAttached = false;
3465
3473
  /** @type {ReturnType<typeof setInterval>|null} */
3466
3474
  let wsHeartbeatTimer = null;
3467
3475
  const WORKFLOW_WS_BATCH_MS = 80;
@@ -7128,6 +7136,20 @@ async function reconcileTaskAfterDispatchAttempt({
7128
7136
  return persistTaskStatusForExecution(adapter, taskId, targetStatus, source);
7129
7137
  }
7130
7138
 
7139
+ function enrichTaskLifetimeTotals(task) {
7140
+ if (!task || typeof task !== "object") return task;
7141
+ const taskId = String(task?.id || task?.taskId || "").trim();
7142
+ const lifetimeTotals = taskId ? getTaskLifetimeTotals(taskId) : null;
7143
+ return {
7144
+ ...task,
7145
+ lifetimeTotals,
7146
+ meta: {
7147
+ ...(task.meta || {}),
7148
+ lifetimeTotals,
7149
+ },
7150
+ };
7151
+ }
7152
+
7131
7153
  function buildTaskRuntimeSnapshot(task) {
7132
7154
  const runtimeExecutor = uiDeps.getInternalExecutor?.() || null;
7133
7155
  const status = runtimeExecutor?.getStatus?.() || {};
@@ -7146,6 +7168,7 @@ function buildTaskRuntimeSnapshot(task) {
7146
7168
  taskId,
7147
7169
  taskStatus: task?.status || null,
7148
7170
  statusLabel: "Live execution",
7171
+ lifetimeTotals: task?.lifetimeTotals || task?.meta?.lifetimeTotals || null,
7149
7172
  slot: {
7150
7173
  taskId,
7151
7174
  branch: slot?.branch || slot?.branchName || null,
@@ -7169,6 +7192,7 @@ function buildTaskRuntimeSnapshot(task) {
7169
7192
  taskStatus: task?.status || null,
7170
7193
  statusLabel: "Queued for execution",
7171
7194
  reason: "no_free_slots",
7195
+ lifetimeTotals: task?.lifetimeTotals || task?.meta?.lifetimeTotals || null,
7172
7196
  };
7173
7197
  }
7174
7198
  if (normalizedStatus === "inprogress") {
@@ -7179,6 +7203,7 @@ function buildTaskRuntimeSnapshot(task) {
7179
7203
  taskStatus: task?.status || null,
7180
7204
  statusLabel: "No live execution detected",
7181
7205
  reason: "no_active_executor_slot",
7206
+ lifetimeTotals: task?.lifetimeTotals || task?.meta?.lifetimeTotals || null,
7182
7207
  };
7183
7208
  }
7184
7209
  if (normalizedStatus === "inreview") {
@@ -7188,6 +7213,7 @@ function buildTaskRuntimeSnapshot(task) {
7188
7213
  taskId,
7189
7214
  taskStatus: task?.status || null,
7190
7215
  statusLabel: "Awaiting review",
7216
+ lifetimeTotals: task?.lifetimeTotals || task?.meta?.lifetimeTotals || null,
7191
7217
  };
7192
7218
  }
7193
7219
  return {
@@ -7196,17 +7222,19 @@ function buildTaskRuntimeSnapshot(task) {
7196
7222
  taskId,
7197
7223
  taskStatus: task?.status || null,
7198
7224
  statusLabel: "No active execution",
7225
+ lifetimeTotals: task?.lifetimeTotals || task?.meta?.lifetimeTotals || null,
7199
7226
  };
7200
7227
  }
7201
7228
 
7202
7229
  function withTaskRuntimeSnapshot(task) {
7203
7230
  if (!task || typeof task !== "object") return task;
7204
- const runtimeSnapshot = buildTaskRuntimeSnapshot(task);
7231
+ const withLifetimeTotals = enrichTaskLifetimeTotals(task);
7232
+ const runtimeSnapshot = buildTaskRuntimeSnapshot(withLifetimeTotals);
7205
7233
  return {
7206
- ...task,
7234
+ ...withLifetimeTotals,
7207
7235
  runtimeSnapshot,
7208
7236
  meta: {
7209
- ...(task.meta || {}),
7237
+ ...(withLifetimeTotals.meta || {}),
7210
7238
  runtimeSnapshot,
7211
7239
  },
7212
7240
  };
@@ -8040,10 +8068,12 @@ async function collectUiStats() {
8040
8068
  } catch { /* best effort */ }
8041
8069
  }
8042
8070
 
8071
+ const runtimeStats = getRuntimeStats();
8072
+
8043
8073
  return {
8044
8074
  uptimeMs: process.uptime() * 1000,
8045
- runtimeMs: globalThis.__bosun_runtimeMs || 0,
8046
- totalCostUsd: globalThis.__bosun_totalCostUsd || 0,
8075
+ runtimeMs: runtimeStats.runtimeMs || 0,
8076
+ totalCostUsd: runtimeStats.totalCostUsd || 0,
8047
8077
  totalSessions: sessionStats.total,
8048
8078
  activeSessions: sessionStats.active,
8049
8079
  completedSessions: sessionStats.completed,
@@ -8055,7 +8085,24 @@ async function collectUiStats() {
8055
8085
  queuedTasks: taskStats.queued,
8056
8086
  activeSlots: orchestratorStatus?.active_slots || "0/0",
8057
8087
  executorMode: orchestratorStatus?.executor_mode || "unknown",
8058
- retryQueue: globalThis.__bosun_setRetryQueueData ? _retryQueue : { count: 0, items: [] },
8088
+ retryQueue: (() => {
8089
+ const bus = _resolveEventBus();
8090
+ if (bus && typeof bus.getRetryQueue === "function") {
8091
+ try {
8092
+ const snapshot = bus.getRetryQueue();
8093
+ if (snapshot && typeof snapshot === "object") return snapshot;
8094
+ } catch {
8095
+ /* best effort */
8096
+ }
8097
+ }
8098
+ return globalThis.__bosun_setRetryQueueData
8099
+ ? _retryQueue
8100
+ : {
8101
+ count: 0,
8102
+ items: [],
8103
+ stats: { totalRetriesToday: 0, peakRetryDepth: 0, exhaustedTaskIds: [] },
8104
+ };
8105
+ })(),
8059
8106
  workflows: {
8060
8107
  active: globalThis.__bosun_activeWorkflows || [],
8061
8108
  total: globalThis.__bosun_totalWorkflows || 0,
@@ -8111,20 +8158,30 @@ async function listDirFilesWithMtime(dir, predicate = () => true) {
8111
8158
  return entries.filter(Boolean);
8112
8159
  }
8113
8160
 
8114
- async function resolvePreferredSystemLogPath() {
8161
+ const SYSTEM_LOG_PRIORITY = Object.freeze({
8162
+ "monitor.log": 300,
8163
+ "monitor-error.log": 250,
8164
+ "daemon.log": 200,
8165
+ });
8166
+
8167
+ async function listPreferredSystemLogEntries(limit = 4) {
8115
8168
  const rootLogEntries = await listDirFilesWithMtime(
8116
8169
  logsDir,
8117
8170
  (name) => name.endsWith(".log"),
8118
8171
  );
8119
- const nonDaemonEntries = rootLogEntries.filter((entry) => entry.name !== "daemon.log");
8120
-
8121
- const preferredEntries = [...nonDaemonEntries].sort(
8122
- (a, b) => b.mtimeMs - a.mtimeMs,
8123
- );
8124
- if (preferredEntries.length > 0) return preferredEntries[0].path;
8172
+ return rootLogEntries
8173
+ .sort((a, b) => {
8174
+ const priorityDelta =
8175
+ (SYSTEM_LOG_PRIORITY[b.name] || 0) - (SYSTEM_LOG_PRIORITY[a.name] || 0);
8176
+ if (priorityDelta !== 0) return priorityDelta;
8177
+ return b.mtimeMs - a.mtimeMs;
8178
+ })
8179
+ .slice(0, Math.max(1, limit));
8180
+ }
8125
8181
 
8126
- const daemonEntry = rootLogEntries.find((entry) => entry.name === "daemon.log");
8127
- return daemonEntry ? daemonEntry.path : null;
8182
+ async function resolvePreferredSystemLogPath() {
8183
+ const preferredEntries = await listPreferredSystemLogEntries(1);
8184
+ return preferredEntries[0]?.path || null;
8128
8185
  }
8129
8186
 
8130
8187
  /**
@@ -9302,10 +9359,70 @@ function withTaskMetadataTopLevel(task) {
9302
9359
  }
9303
9360
 
9304
9361
  async function getLatestLogTail(lineCount) {
9305
- const logPath = await resolvePreferredSystemLogPath();
9306
- if (!logPath) return { file: null, lines: [] };
9307
- const tail = await tailFile(logPath, lineCount);
9308
- return { file: basename(logPath), lines: tail.lines || [] };
9362
+ return getMergedSystemLogTail(lineCount);
9363
+ }
9364
+
9365
+ function parseSystemLogTimestamp(line) {
9366
+ const match = String(line || "").match(/^\s*(\d{4}-\d{2}-\d{2}T[^\s]+)/);
9367
+ if (!match?.[1]) return Number.NaN;
9368
+ const parsed = Date.parse(match[1]);
9369
+ return Number.isFinite(parsed) ? parsed : Number.NaN;
9370
+ }
9371
+
9372
+ async function getMergedSystemLogTail(
9373
+ lineCount,
9374
+ {
9375
+ fileLimit = 4,
9376
+ maxBytesPerFile = 350_000,
9377
+ } = {},
9378
+ ) {
9379
+ const entries = await listPreferredSystemLogEntries(fileLimit);
9380
+ if (!entries.length) {
9381
+ return { file: null, files: [], lines: [], truncated: false };
9382
+ }
9383
+
9384
+ const perFileLineBudget = Math.max(lineCount * 2, 160);
9385
+ const tails = await Promise.all(
9386
+ entries.map((entry) => tailFile(entry.path, perFileLineBudget, maxBytesPerFile).catch(() => null)),
9387
+ );
9388
+
9389
+ const merged = [];
9390
+ let truncated = false;
9391
+ tails.forEach((tail, sourceIndex) => {
9392
+ if (!tail) return;
9393
+ truncated = truncated || tail.truncated === true;
9394
+ const lines = Array.isArray(tail.lines) ? tail.lines : [];
9395
+ lines.forEach((line, lineIndex) => {
9396
+ merged.push({
9397
+ line,
9398
+ timestamp: parseSystemLogTimestamp(line),
9399
+ sourceIndex,
9400
+ lineIndex,
9401
+ });
9402
+ });
9403
+ });
9404
+
9405
+ merged.sort((a, b) => {
9406
+ const aHasTs = Number.isFinite(a.timestamp);
9407
+ const bHasTs = Number.isFinite(b.timestamp);
9408
+ if (aHasTs && bHasTs && a.timestamp !== b.timestamp) {
9409
+ return a.timestamp - b.timestamp;
9410
+ }
9411
+ if (aHasTs !== bHasTs) {
9412
+ return aHasTs ? -1 : 1;
9413
+ }
9414
+ if (a.sourceIndex !== b.sourceIndex) {
9415
+ return a.sourceIndex - b.sourceIndex;
9416
+ }
9417
+ return a.lineIndex - b.lineIndex;
9418
+ });
9419
+
9420
+ return {
9421
+ file: entries[0]?.name || null,
9422
+ files: entries.map((entry) => entry.name),
9423
+ lines: merged.slice(-lineCount).map((entry) => entry.line),
9424
+ truncated,
9425
+ };
9309
9426
  }
9310
9427
 
9311
9428
  async function tailFile(filePath, lineCount, maxBytes = 1_000_000) {
@@ -13494,7 +13611,24 @@ async function handleApi(req, res, url) {
13494
13611
  const logDir = resolveAgentWorkLogDir();
13495
13612
  const metricsPath = resolve(logDir, "agent-metrics.jsonl");
13496
13613
  const metrics = await readJsonlTail(metricsPath, 3000);
13497
- const summary = summarizeTelemetry(metrics, days);
13614
+ const summary = summarizeTelemetry(metrics, days) || {};
13615
+ const runtimeStats = getRuntimeStats();
13616
+ const completedSessions = getCompletedSessions(10_000);
13617
+ const lifetimeTotals = completedSessions.reduce(
13618
+ (acc, session) => {
13619
+ acc.attemptsCount += 1;
13620
+ acc.tokenCount += Number(session?.tokenCount || 0);
13621
+ acc.inputTokens += Number(session?.inputTokens || 0);
13622
+ acc.outputTokens += Number(session?.outputTokens || 0);
13623
+ return acc;
13624
+ },
13625
+ { attemptsCount: 0, tokenCount: 0, inputTokens: 0, outputTokens: 0 },
13626
+ );
13627
+ summary.runtimeMs = Number(runtimeStats?.runtimeMs || 0);
13628
+ summary.lifetimeTotals = {
13629
+ ...lifetimeTotals,
13630
+ durationMs: summary.runtimeMs,
13631
+ };
13498
13632
  jsonResponse(res, 200, { ok: true, data: summary });
13499
13633
  } catch (err) {
13500
13634
  jsonResponse(res, 500, { ok: false, error: err.message });
@@ -14586,12 +14720,25 @@ async function handleApi(req, res, url) {
14586
14720
  }
14587
14721
  const wfMod = wfCtx.wfMod;
14588
14722
  const types = wfMod.listNodeTypes();
14589
- jsonResponse(res, 200, { ok: true, nodeTypes: types.map(nt => ({
14590
- type: nt.type,
14591
- category: nt.type.split(".")[0],
14592
- description: nt.description || "",
14593
- schema: nt.schema || {},
14594
- })) });
14723
+ jsonResponse(res, 200, { ok: true, nodeTypes: types.map((nt) => {
14724
+ const rawPorts = nt?.ports && typeof nt.ports === "object" ? nt.ports : {};
14725
+ const ports = {
14726
+ inputs: Array.isArray(rawPorts.inputs) ? rawPorts.inputs : [],
14727
+ outputs: Array.isArray(rawPorts.outputs) ? rawPorts.outputs : [],
14728
+ };
14729
+ return {
14730
+ type: nt.type,
14731
+ category: nt.type.split(".")[0],
14732
+ description: nt.description || "",
14733
+ schema: nt.schema || {},
14734
+ source: nt.source || "builtin",
14735
+ badge: nt.badge || null,
14736
+ isCustom: nt.isCustom === true,
14737
+ filePath: nt.filePath || null,
14738
+ ports,
14739
+ ui: nt?.ui && typeof nt.ui === "object" ? nt.ui : {},
14740
+ };
14741
+ }) });
14595
14742
  } catch (err) {
14596
14743
  jsonResponse(res, 500, { ok: false, error: err.message });
14597
14744
  }
@@ -15469,6 +15616,14 @@ async function handleApi(req, res, url) {
15469
15616
  `[telegram-ui] failed to retry task ${taskId}: ${error.message}`,
15470
15617
  );
15471
15618
  });
15619
+ const bus = _resolveEventBus();
15620
+ if (bus && typeof bus.clearRetryQueueTask === "function") {
15621
+ try {
15622
+ bus.clearRetryQueueTask(taskId, "manual-retry-now");
15623
+ } catch {
15624
+ /* best effort */
15625
+ }
15626
+ }
15472
15627
  jsonResponse(res, 200, { ok: true, taskId });
15473
15628
  broadcastUiEvent(
15474
15629
  ["tasks", "overview", "executor", "agents"],
@@ -15484,14 +15639,45 @@ async function handleApi(req, res, url) {
15484
15639
  // ── GET /api/retry-queue ───────────────────────────────────────────
15485
15640
  if (path === "/api/retry-queue" && req.method === "GET") {
15486
15641
  try {
15487
- const retryQueue = globalThis.__bosun_setRetryQueueData ? _retryQueue : { count: 0, items: [] };
15642
+ const bus = _resolveEventBus();
15643
+ let retryQueue = null;
15644
+ if (bus && typeof bus.getRetryQueue === "function") {
15645
+ try {
15646
+ const snapshot = bus.getRetryQueue();
15647
+ if (snapshot && typeof snapshot === "object") {
15648
+ retryQueue = snapshot;
15649
+ }
15650
+ } catch {
15651
+ /* best effort */
15652
+ }
15653
+ }
15654
+ if (!retryQueue) {
15655
+ retryQueue = globalThis.__bosun_setRetryQueueData
15656
+ ? _retryQueue
15657
+ : {
15658
+ count: 0,
15659
+ items: [],
15660
+ stats: { totalRetriesToday: 0, peakRetryDepth: 0, exhaustedTaskIds: [] },
15661
+ };
15662
+ }
15488
15663
  jsonResponse(res, 200, {
15489
15664
  ok: true,
15490
15665
  count: retryQueue.count || 0,
15491
15666
  items: retryQueue.items || [],
15667
+ stats: retryQueue.stats || {
15668
+ totalRetriesToday: 0,
15669
+ peakRetryDepth: 0,
15670
+ exhaustedTaskIds: [],
15671
+ },
15492
15672
  });
15493
15673
  } catch (err) {
15494
- jsonResponse(res, 500, { ok: false, error: err.message, count: 0, items: [] });
15674
+ jsonResponse(res, 500, {
15675
+ ok: false,
15676
+ error: err.message,
15677
+ count: 0,
15678
+ items: [],
15679
+ stats: { totalRetriesToday: 0, peakRetryDepth: 0, exhaustedTaskIds: [] },
15680
+ });
15495
15681
  }
15496
15682
  return;
15497
15683
  }
@@ -18029,6 +18215,17 @@ export async function startTelegramUiServer(options = {}) {
18029
18215
  broadcastSessionMessage(payload);
18030
18216
  });
18031
18217
  }
18218
+ if (!sessionAccumulatorListenerAttached) {
18219
+ sessionAccumulatorListenerAttached = true;
18220
+ addSessionAccumulationListener((payload) => {
18221
+ broadcastUiEvent(["tasks", "overview", "telemetry", "sessions"], "invalidate", {
18222
+ reason: "session-accumulated",
18223
+ taskId: payload?.taskId || null,
18224
+ totals: payload?.totals || null,
18225
+ type: payload?.type || "session-accumulated",
18226
+ });
18227
+ });
18228
+ }
18032
18229
 
18033
18230
  // Periodic stats broadcast for TUI
18034
18231
  let statsBroadcastInterval = null;
@@ -18050,7 +18247,29 @@ export async function startTelegramUiServer(options = {}) {
18050
18247
  // Retry queue tracking
18051
18248
  let _retryQueue = { count: 0, items: [] };
18052
18249
  function setRetryQueueData(data) {
18053
- _retryQueue = data || { count: 0, items: [] };
18250
+ const normalized = data && typeof data === "object" ? data : {};
18251
+ _retryQueue = {
18252
+ count: Number(normalized.count || 0),
18253
+ items: Array.isArray(normalized.items) ? normalized.items : [],
18254
+ stats: normalized.stats && typeof normalized.stats === "object"
18255
+ ? {
18256
+ totalRetriesToday: Number(normalized.stats.totalRetriesToday || 0),
18257
+ peakRetryDepth: Number(normalized.stats.peakRetryDepth || 0),
18258
+ exhaustedTaskIds: Array.isArray(normalized.stats.exhaustedTaskIds)
18259
+ ? normalized.stats.exhaustedTaskIds
18260
+ : [],
18261
+ }
18262
+ : {
18263
+ totalRetriesToday: 0,
18264
+ peakRetryDepth: 0,
18265
+ exhaustedTaskIds: [],
18266
+ },
18267
+ };
18268
+ broadcastUiEvent(["retry-queue", "overview", "telemetry", "tasks"], "retry-queue-updated", _retryQueue);
18269
+ broadcastUiEvent(["overview", "telemetry", "retry-queue"], "invalidate", {
18270
+ reason: "retry-queue-updated",
18271
+ count: _retryQueue.count,
18272
+ });
18054
18273
  }
18055
18274
  globalThis.__bosun_setRetryQueueData = setRetryQueueData;
18056
18275
 
package/setup.mjs CHANGED
@@ -60,6 +60,7 @@ import {
60
60
  resolveWorkflowTemplateIds,
61
61
  normalizeTemplateOverridesById,
62
62
  } from "./workflow/workflow-templates.mjs";
63
+ import { discoverTelegramChats } from "./telegram/get-telegram-chat-id.mjs";
63
64
 
64
65
  const __dirname = dirname(fileURLToPath(import.meta.url));
65
66
 
@@ -89,6 +90,14 @@ function getVersion() {
89
90
  }
90
91
  }
91
92
 
93
+ function formatTelegramChatChoice(chat) {
94
+ const parts = [String(chat.id)];
95
+ if (chat.type) parts.push(chat.type);
96
+ if (chat.username) parts.push(`@${chat.username}`);
97
+ if (chat.title) parts.push(chat.title);
98
+ return parts.join(" · ");
99
+ }
100
+
92
101
  function hasSetupMarkers(dir) {
93
102
  const markers = [
94
103
  ".env",
@@ -3883,32 +3892,26 @@ async function main() {
3883
3892
  // Try to fetch chat ID from Telegram API
3884
3893
  info("Fetching your chat ID from Telegram...");
3885
3894
  try {
3886
- const response = await fetch(
3887
- `https://api.telegram.org/bot${env.TELEGRAM_BOT_TOKEN}/getUpdates`,
3888
- );
3889
- const data = await response.json();
3890
-
3891
- if (data.ok && data.result && data.result.length > 0) {
3892
- // Find the most recent message
3893
- const latestMessage = data.result[data.result.length - 1];
3894
- const chatId = latestMessage?.message?.chat?.id;
3895
-
3896
- if (chatId) {
3897
- env.TELEGRAM_CHAT_ID = String(chatId);
3898
- info(`✓ Found your chat ID: ${chatId}`);
3899
- console.log();
3900
- } else {
3901
- warn(
3902
- "Couldn't find a chat ID. Make sure you sent a message to your bot.",
3903
- );
3904
- env.TELEGRAM_CHAT_ID = await prompt.ask(
3905
- "Enter chat ID manually",
3906
- "",
3907
- );
3908
- }
3895
+ const discovery = await discoverTelegramChats(env.TELEGRAM_BOT_TOKEN);
3896
+
3897
+ if (discovery.chats.length === 1) {
3898
+ env.TELEGRAM_CHAT_ID = String(discovery.chats[0].id);
3899
+ info(`✓ Found your chat ID: ${env.TELEGRAM_CHAT_ID}`);
3900
+ console.log();
3901
+ } else if (discovery.chats.length > 1) {
3902
+ const selectedIdx = await prompt.choose(
3903
+ "Select the chat Bosun should use:",
3904
+ discovery.chats.map(formatTelegramChatChoice),
3905
+ 0,
3906
+ );
3907
+ const selectedChat = discovery.chats[selectedIdx];
3908
+ env.TELEGRAM_CHAT_ID = String(selectedChat.id);
3909
+ info(`✓ Selected chat ID: ${env.TELEGRAM_CHAT_ID}`);
3910
+ console.log();
3909
3911
  } else {
3910
3912
  warn(
3911
- "No messages found. Make sure you sent a message to your bot first.",
3913
+ discovery.message ||
3914
+ "Couldn't find a chat ID. Make sure you sent a message to your bot.",
3912
3915
  );
3913
3916
  console.log(
3914
3917
  chalk.dim(
@@ -31,10 +31,12 @@ const __dirname = resolve(fileURLToPath(new URL(".", import.meta.url)));
31
31
  // ── Configuration ────────────────────────────────────────────────────────────
32
32
 
33
33
  const DEFAULT_TIMEOUT_MS = 60 * 60 * 1000; // 60 min for agentic tasks (matches Azure stream timeout)
34
+ const MAX_TIMER_DELAY_MS = 2_147_483_647; // Node.js timer clamp (2^31 - 1)
34
35
  // MAX_STREAM_RETRIES, isTransientStreamError, streamRetryDelay ← imported from ./stream-resilience.mjs
35
36
  const STATE_FILE = resolve(__dirname, "..", "logs", "codex-shell-state.json");
36
37
  const SESSIONS_DIR = resolve(__dirname, "..", "logs", "sessions");
37
38
  const MAX_PERSISTENT_TURNS = 50;
39
+ const timeoutNormalizationWarningKey = new Set();
38
40
 
39
41
  // ── Payload safety ────────────────────────────────────────────────────────────
40
42
  // The Codex API rejects JSON bodies with malformed or oversized strings.
@@ -54,6 +56,28 @@ function parseBoundedNumber(value, fallback, min, max) {
54
56
  return Math.min(Math.max(Math.trunc(num), min), max);
55
57
  }
56
58
 
59
+ function parsePositiveTimeoutMs(value, fallback = DEFAULT_TIMEOUT_MS) {
60
+ const fallbackValue = Number(fallback);
61
+ if (!Number.isFinite(fallbackValue) || fallbackValue <= 0) {
62
+ throw new Error("parsePositiveTimeoutMs requires a positive finite fallback");
63
+ }
64
+ const parsed = Number(value);
65
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallbackValue;
66
+ return parsed;
67
+ }
68
+
69
+ function normalizeTimeoutMs(value, { fallback = DEFAULT_TIMEOUT_MS, label = "timeoutMs" } = {}) {
70
+ const parsed = parsePositiveTimeoutMs(value, fallback);
71
+ const normalized = Math.min(parsed, MAX_TIMER_DELAY_MS);
72
+ if (normalized !== parsed && !timeoutNormalizationWarningKey.has(label)) {
73
+ timeoutNormalizationWarningKey.add(label);
74
+ console.warn(
75
+ `[codex-shell] ${label} ${parsed}ms exceeds Node.js timer max; clamped to ${MAX_TIMER_DELAY_MS}ms`,
76
+ );
77
+ }
78
+ return normalized;
79
+ }
80
+
57
81
  function getInternalExecutorStreamConfig() {
58
82
  try {
59
83
  const cfg = loadConfig();
@@ -692,6 +716,10 @@ export async function execCodexPrompt(userMessage, options = {}) {
692
716
  mode = null,
693
717
  cwd = null,
694
718
  } = options;
719
+ const normalizedTimeoutMs = normalizeTimeoutMs(timeoutMs, {
720
+ fallback: DEFAULT_TIMEOUT_MS,
721
+ label: "execCodexPrompt.timeoutMs",
722
+ });
695
723
 
696
724
  agentSdk = resolveAgentSdkConfig({ reload: true });
697
725
  if (agentSdk.primary !== "codex") {
@@ -714,7 +742,7 @@ export async function execCodexPrompt(userMessage, options = {}) {
714
742
  activeTurn = true;
715
743
 
716
744
  try {
717
- const streamSafety = resolveCodexStreamSafety(timeoutMs);
745
+ const streamSafety = resolveCodexStreamSafety(normalizedTimeoutMs);
718
746
  const requestedWorkingDirectory = normalizeWorkingDirectory(cwd);
719
747
 
720
748
  if (!persistent) {
@@ -811,7 +839,10 @@ export async function execCodexPrompt(userMessage, options = {}) {
811
839
  // immediately fail. The total wall-clock budget is still bounded by the
812
840
  // outer timeoutMs passed in.
813
841
  const controller = abortController || new AbortController();
814
- const timer = setTimeout(() => controller.abort("timeout"), timeoutMs);
842
+ const timer = setTimeout(
843
+ () => controller.abort("timeout"),
844
+ normalizedTimeoutMs,
845
+ );
815
846
 
816
847
  try {
817
848
  // Use runStreamed for real-time event streaming
@@ -939,7 +970,7 @@ export async function execCodexPrompt(userMessage, options = {}) {
939
970
  const msg =
940
971
  reason === "user_stop"
941
972
  ? ":close: Agent stopped by user."
942
- : `:clock: Agent timed out after ${timeoutMs / 1000}s`;
973
+ : `:clock: Agent timed out after ${normalizedTimeoutMs / 1000}s`;
943
974
  return { finalResponse: msg, items: [], usage: null };
944
975
  }
945
976
  }