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
@@ -38,9 +38,11 @@ async function getMcpRegistry() {
38
38
  // ── Configuration ────────────────────────────────────────────────────────────
39
39
 
40
40
  const DEFAULT_TIMEOUT_MS = 60 * 60 * 1000; // 60 min for agentic tasks
41
+ const MAX_TIMER_DELAY_MS = 2_147_483_647; // Node.js timer clamp (2^31 - 1)
41
42
  const STATE_FILE = resolve(__dirname, "..", "logs", "copilot-shell-state.json");
42
43
  const SESSION_LOG_DIR = resolve(__dirname, "..", "logs", "copilot-sessions");
43
44
  const REPO_ROOT = resolveRepoRoot();
45
+ const timeoutNormalizationWarningKey = new Set();
44
46
 
45
47
  // Valid reasoning effort levels for models that support it
46
48
  const VALID_REASONING_EFFORTS = ["low", "medium", "high", "xhigh"];
@@ -200,6 +202,28 @@ function safeStringify(value, maxLen = 8000) {
200
202
  return text;
201
203
  }
202
204
 
205
+ function parsePositiveTimeoutMs(value, fallback = DEFAULT_TIMEOUT_MS) {
206
+ const fallbackValue = Number(fallback);
207
+ if (!Number.isFinite(fallbackValue) || fallbackValue <= 0) {
208
+ throw new Error("parsePositiveTimeoutMs requires a positive finite fallback");
209
+ }
210
+ const parsed = Number(value);
211
+ if (!Number.isFinite(parsed) || parsed <= 0) return fallbackValue;
212
+ return parsed;
213
+ }
214
+
215
+ function normalizeTimeoutMs(value, { fallback = DEFAULT_TIMEOUT_MS, label = "timeoutMs" } = {}) {
216
+ const parsed = parsePositiveTimeoutMs(value, fallback);
217
+ const normalized = Math.min(parsed, MAX_TIMER_DELAY_MS);
218
+ if (normalized !== parsed && !timeoutNormalizationWarningKey.has(label)) {
219
+ timeoutNormalizationWarningKey.add(label);
220
+ console.warn(
221
+ `[copilot-shell] ${label} ${parsed}ms exceeds Node.js timer max; clamped to ${MAX_TIMER_DELAY_MS}ms`,
222
+ );
223
+ }
224
+ return normalized;
225
+ }
226
+
203
227
  function initSessionLog(sessionId, prompt, timeoutMs) {
204
228
  if (!sessionId) return null;
205
229
  try {
@@ -588,8 +612,10 @@ async function ensureClientStarted() {
588
612
  );
589
613
  }
590
614
 
591
- const START_TIMEOUT_MS =
592
- Number(process.env.COPILOT_START_TIMEOUT_MS) || 20_000;
615
+ const START_TIMEOUT_MS = normalizeTimeoutMs(process.env.COPILOT_START_TIMEOUT_MS, {
616
+ fallback: 20_000,
617
+ label: "COPILOT_START_TIMEOUT_MS",
618
+ });
593
619
 
594
620
  await withSanitizedOpenAiEnv(async () => {
595
621
  copilotClient = new Cls(clientOptions);
@@ -859,6 +885,10 @@ export async function execCopilotPrompt(userMessage, options = {}) {
859
885
  persistent = false,
860
886
  mode = null,
861
887
  } = options;
888
+ const normalizedTimeoutMs = normalizeTimeoutMs(timeoutMs, {
889
+ fallback: DEFAULT_TIMEOUT_MS,
890
+ label: "execCopilotPrompt.timeoutMs",
891
+ });
862
892
 
863
893
  if (activeTurn && !options._holdActiveTurn) {
864
894
  return {
@@ -880,7 +910,7 @@ export async function execCopilotPrompt(userMessage, options = {}) {
880
910
 
881
911
  let unsubscribe = null;
882
912
  const session = await getSession();
883
- const logPath = initSessionLog(activeSessionId, userMessage, timeoutMs);
913
+ const logPath = initSessionLog(activeSessionId, userMessage, normalizedTimeoutMs);
884
914
  const items = [];
885
915
  let finalResponse = "";
886
916
  let responseFromMessage = false;
@@ -923,7 +953,7 @@ export async function execCopilotPrompt(userMessage, options = {}) {
923
953
  }
924
954
 
925
955
  const controller = abortController || new AbortController();
926
- const timer = setTimeout(() => controller.abort("timeout"), timeoutMs);
956
+ const timer = setTimeout(() => controller.abort("timeout"), normalizedTimeoutMs);
927
957
 
928
958
  const onAbort = () => {
929
959
  const reason = controller.signal.reason || "user_stop";
@@ -963,7 +993,7 @@ export async function execCopilotPrompt(userMessage, options = {}) {
963
993
 
964
994
  // Pass timeout parameter to sendAndWait to override 60s SDK default
965
995
  const sendPromise = session.sendAndWait
966
- ? sendFn.call(session, { prompt }, timeoutMs)
996
+ ? sendFn.call(session, { prompt }, normalizedTimeoutMs)
967
997
  : sendFn.call(session, { prompt });
968
998
 
969
999
  // If send() returns before idle, wait for session.idle if available
@@ -978,9 +1008,21 @@ export async function execCopilotPrompt(userMessage, options = {}) {
978
1008
  };
979
1009
  const off = session.on ? session.on(idleHandler) : null;
980
1010
  Promise.resolve(sendPromise).catch(reject);
981
- setTimeout(resolve, timeoutMs + 1000);
1011
+ setTimeout(
1012
+ resolve,
1013
+ normalizeTimeoutMs(normalizedTimeoutMs + 1000, {
1014
+ fallback: DEFAULT_TIMEOUT_MS,
1015
+ label: "execCopilotPrompt.idleWaitResolveTimeoutMs",
1016
+ }),
1017
+ );
982
1018
  if (typeof off === "function") {
983
- setTimeout(() => off(), timeoutMs + 2000);
1019
+ setTimeout(
1020
+ () => off(),
1021
+ normalizeTimeoutMs(normalizedTimeoutMs + 2000, {
1022
+ fallback: DEFAULT_TIMEOUT_MS,
1023
+ label: "execCopilotPrompt.idleWaitCleanupTimeoutMs",
1024
+ }),
1025
+ );
984
1026
  }
985
1027
  });
986
1028
  } else {
@@ -1008,7 +1050,7 @@ export async function execCopilotPrompt(userMessage, options = {}) {
1008
1050
  const msg =
1009
1051
  reason === "user_stop"
1010
1052
  ? ":close: Agent stopped by user."
1011
- : `:clock: Agent timed out after ${timeoutMs / 1000}s`;
1053
+ : `:clock: Agent timed out after ${normalizedTimeoutMs / 1000}s`;
1012
1054
  return { finalResponse: msg, items: [], usage: null };
1013
1055
  }
1014
1056
  // ── Transient stream retry ──────────────────────────────────────────────────
@@ -0,0 +1,193 @@
1
+ /**
2
+ * @module task/msg-hub
3
+ * @description Lightweight pub-sub hub for active agent sessions.
4
+ */
5
+
6
+ import { EventEmitter } from "node:events";
7
+ import { randomUUID } from "node:crypto";
8
+
9
+ const SAFE_KEYS = new Set([
10
+ "taskId",
11
+ "title",
12
+ "summary",
13
+ "branch",
14
+ "baseBranch",
15
+ "repoSlug",
16
+ "workspace",
17
+ "repository",
18
+ "paths",
19
+ "files",
20
+ "status",
21
+ "runId",
22
+ "stage",
23
+ ]);
24
+
25
+ function normalizeParticipant(participant, index = 0) {
26
+ if (typeof participant === "string") {
27
+ return { id: participant, name: participant };
28
+ }
29
+ if (participant && typeof participant === "object") {
30
+ const id = String(participant.id || participant.name || `participant-${index + 1}`);
31
+ return {
32
+ ...participant,
33
+ id,
34
+ name: String(participant.name || id),
35
+ };
36
+ }
37
+ throw new TypeError("MsgHub participant must be a string or object");
38
+ }
39
+
40
+ function truncateText(value, maxLength) {
41
+ const text = String(value || "").trim();
42
+ if (text.length <= maxLength) return text;
43
+ return `${text.slice(0, Math.max(0, maxLength - 1))}…`;
44
+ }
45
+
46
+ export function sanitizeMessageReference(message, options = {}) {
47
+ const maxSummaryLength = Number(options.maxSummaryLength || 400);
48
+ if (message == null) return { summary: "" };
49
+
50
+ if (typeof message === "string") {
51
+ return { summary: truncateText(message, maxSummaryLength) };
52
+ }
53
+
54
+ if (Array.isArray(message)) {
55
+ return {
56
+ items: message.slice(0, 20).map((entry) => sanitizeMessageReference(entry, options)),
57
+ };
58
+ }
59
+
60
+ if (typeof message === "object") {
61
+ const sanitized = {};
62
+ for (const [key, value] of Object.entries(message)) {
63
+ if (!SAFE_KEYS.has(key)) continue;
64
+ if (typeof value === "string") {
65
+ sanitized[key] = truncateText(value, maxSummaryLength);
66
+ continue;
67
+ }
68
+ if (Array.isArray(value)) {
69
+ sanitized[key] = value.slice(0, 50).map((entry) => truncateText(entry, maxSummaryLength));
70
+ continue;
71
+ }
72
+ if (value != null && (typeof value === "number" || typeof value === "boolean")) {
73
+ sanitized[key] = value;
74
+ }
75
+ }
76
+ if (Object.keys(sanitized).length > 0) return sanitized;
77
+ }
78
+
79
+ return { summary: truncateText(JSON.stringify(message), maxSummaryLength) };
80
+ }
81
+
82
+ export class MsgHub {
83
+ constructor(participants = [], options = {}) {
84
+ this.options = { ...options };
85
+ this._events = new EventEmitter();
86
+ this._participants = new Map();
87
+ this._queues = new Map();
88
+ this._handlers = new Map();
89
+ this._closed = false;
90
+
91
+ participants.forEach((participant, index) => {
92
+ this.add(participant, index);
93
+ });
94
+ }
95
+
96
+ static async create(participants = [], options = {}) {
97
+ return new MsgHub(participants, options);
98
+ }
99
+
100
+ add(participant, index = this._participants.size) {
101
+ if (this._closed) throw new Error("MsgHub is closed");
102
+ const normalized = normalizeParticipant(participant, index);
103
+ this._participants.set(normalized.id, normalized);
104
+ if (!this._queues.has(normalized.id)) this._queues.set(normalized.id, []);
105
+ if (!this._handlers.has(normalized.id)) this._handlers.set(normalized.id, new Set());
106
+ return normalized;
107
+ }
108
+
109
+ remove(participantOrId) {
110
+ const id = typeof participantOrId === "string" ? participantOrId : participantOrId?.id;
111
+ if (!id) return false;
112
+ const existed = this._participants.delete(id);
113
+ this._queues.delete(id);
114
+ this._handlers.delete(id);
115
+ return existed;
116
+ }
117
+
118
+ has(participantOrId) {
119
+ const id = typeof participantOrId === "string" ? participantOrId : participantOrId?.id;
120
+ return !!id && this._participants.has(id);
121
+ }
122
+
123
+ listParticipants() {
124
+ return Array.from(this._participants.values());
125
+ }
126
+
127
+ subscribe(participantOrId, handler) {
128
+ const id = typeof participantOrId === "string" ? participantOrId : participantOrId?.id;
129
+ if (!id || typeof handler !== "function") {
130
+ throw new TypeError("MsgHub.subscribe requires a participant id and handler");
131
+ }
132
+ if (!this._handlers.has(id)) this._handlers.set(id, new Set());
133
+ this._handlers.get(id).add(handler);
134
+ return () => {
135
+ this._handlers.get(id)?.delete(handler);
136
+ };
137
+ }
138
+
139
+ publish(fromParticipant, message, options = {}) {
140
+ if (this._closed) throw new Error("MsgHub is closed");
141
+ const fromId = typeof fromParticipant === "string" ? fromParticipant : fromParticipant?.id;
142
+ if (!fromId || !this._participants.has(fromId)) {
143
+ throw new Error("MsgHub.publish requires a known sender");
144
+ }
145
+
146
+ const reference = sanitizeMessageReference(message, this.options);
147
+ const deliveries = [];
148
+
149
+ for (const participant of this._participants.values()) {
150
+ if (participant.id === fromId) continue;
151
+ const envelope = {
152
+ id: randomUUID(),
153
+ from: fromId,
154
+ to: participant.id,
155
+ topic: String(options.topic || "reference"),
156
+ createdAt: new Date().toISOString(),
157
+ message: reference,
158
+ };
159
+ this._queues.get(participant.id)?.push(envelope);
160
+ for (const handler of this._handlers.get(participant.id) || []) {
161
+ handler(envelope);
162
+ }
163
+ this._events.emit("message", envelope);
164
+ deliveries.push(envelope);
165
+ }
166
+
167
+ return deliveries;
168
+ }
169
+
170
+ observeOutput(fromParticipant, output, options = {}) {
171
+ const descriptor = output?.descriptor || output?.reference || output?.output || output;
172
+ return this.publish(fromParticipant, descriptor, options);
173
+ }
174
+
175
+ drain(participantOrId) {
176
+ const id = typeof participantOrId === "string" ? participantOrId : participantOrId?.id;
177
+ if (!id) return [];
178
+ const queue = this._queues.get(id) || [];
179
+ this._queues.set(id, []);
180
+ return queue.slice();
181
+ }
182
+
183
+ close() {
184
+ if (this._closed) return;
185
+ this._closed = true;
186
+ this._participants.clear();
187
+ this._queues.clear();
188
+ this._handlers.clear();
189
+ this._events.removeAllListeners();
190
+ }
191
+ }
192
+
193
+ export default MsgHub;