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.
- package/.env.example +8 -0
- package/README.md +20 -0
- package/agent/agent-event-bus.mjs +248 -6
- package/agent/agent-pool.mjs +125 -28
- package/agent/agent-work-analyzer.mjs +8 -16
- package/agent/retry-queue.mjs +164 -0
- package/bosun.config.example.json +25 -0
- package/bosun.schema.json +825 -183
- package/cli.mjs +59 -5
- package/config/config.mjs +130 -3
- package/infra/monitor.mjs +693 -67
- package/infra/runtime-accumulator.mjs +376 -84
- package/infra/session-tracker.mjs +82 -25
- package/lib/codebase-audit.mjs +133 -18
- package/package.json +23 -4
- package/server/setup-web-server.mjs +25 -0
- package/server/ui-server.mjs +248 -29
- package/setup.mjs +27 -24
- package/shell/codex-shell.mjs +34 -3
- package/shell/copilot-shell.mjs +50 -8
- package/task/msg-hub.mjs +193 -0
- package/task/pipeline.mjs +544 -0
- package/task/task-cli.mjs +38 -2
- package/task/task-executor-pipeline.mjs +143 -0
- package/task/task-executor.mjs +36 -27
- package/telegram/get-telegram-chat-id.mjs +57 -47
- package/ui/components/workspace-switcher.js +7 -7
- package/ui/demo-defaults.js +15694 -10573
- package/ui/modules/settings-schema.js +2 -0
- package/ui/modules/state.js +54 -57
- package/ui/modules/voice-client-sdk.js +375 -36
- package/ui/modules/voice-client.js +140 -31
- package/ui/setup.html +68 -2
- package/ui/styles/components.css +57 -0
- package/ui/styles.css +201 -1
- package/ui/tabs/dashboard.js +74 -0
- package/ui/tabs/logs.js +10 -0
- package/ui/tabs/settings.js +178 -99
- package/ui/tabs/tasks.js +31 -1
- package/ui/tabs/telemetry.js +34 -0
- package/ui/tabs/workflow-canvas-utils.mjs +8 -1
- package/ui/tabs/workflows.js +532 -275
- package/voice/voice-agents-sdk.mjs +1 -1
- package/voice/voice-relay.mjs +6 -6
- package/workflow/declarative-workflows.mjs +145 -0
- package/workflow/msg-hub.mjs +237 -0
- package/workflow/pipeline-workflows.mjs +287 -0
- package/workflow/pipeline.mjs +828 -315
- package/workflow/workflow-cli.mjs +128 -0
- package/workflow/workflow-engine.mjs +329 -17
- package/workflow/workflow-nodes/custom-loader.mjs +250 -0
- package/workflow/workflow-nodes.mjs +1955 -223
- package/workflow/workflow-templates.mjs +26 -8
- package/workflow-templates/agents.mjs +0 -1
- package/workflow-templates/bosun-native.mjs +212 -2
- package/workflow-templates/continuation-loop.mjs +339 -0
- package/workflow-templates/github.mjs +516 -40
- package/workflow-templates/planning.mjs +446 -17
- package/workflow-templates/reliability.mjs +65 -12
- package/workflow-templates/task-batch.mjs +24 -8
- package/workflow-templates/task-lifecycle.mjs +83 -6
- package/workspace/context-cache.mjs +66 -18
- package/workspace/workspace-manager.mjs +2 -1
- package/workflow-templates/issue-continuation.mjs +0 -243
package/shell/copilot-shell.mjs
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
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"),
|
|
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 },
|
|
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(
|
|
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(
|
|
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 ${
|
|
1053
|
+
: `:clock: Agent timed out after ${normalizedTimeoutMs / 1000}s`;
|
|
1012
1054
|
return { finalResponse: msg, items: [], usage: null };
|
|
1013
1055
|
}
|
|
1014
1056
|
// ── Transient stream retry ──────────────────────────────────────────────────
|
package/task/msg-hub.mjs
ADDED
|
@@ -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;
|