bosun 0.40.21 → 0.41.1

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 (80) hide show
  1. package/.env.example +8 -0
  2. package/README.md +20 -0
  3. package/agent/agent-custom-tools.mjs +23 -5
  4. package/agent/agent-event-bus.mjs +248 -6
  5. package/agent/agent-pool.mjs +131 -30
  6. package/agent/agent-work-analyzer.mjs +8 -16
  7. package/agent/primary-agent.mjs +81 -7
  8. package/agent/retry-queue.mjs +164 -0
  9. package/bench/swebench/bosun-swebench.mjs +5 -0
  10. package/bosun.config.example.json +25 -0
  11. package/bosun.schema.json +825 -183
  12. package/cli.mjs +267 -8
  13. package/config/config-doctor.mjs +51 -2
  14. package/config/config.mjs +232 -5
  15. package/github/github-auth-manager.mjs +70 -19
  16. package/infra/library-manager.mjs +894 -60
  17. package/infra/monitor.mjs +701 -69
  18. package/infra/runtime-accumulator.mjs +376 -84
  19. package/infra/session-tracker.mjs +95 -28
  20. package/infra/test-runtime.mjs +267 -0
  21. package/lib/codebase-audit.mjs +133 -18
  22. package/package.json +30 -8
  23. package/server/setup-web-server.mjs +29 -1
  24. package/server/ui-server.mjs +1571 -49
  25. package/setup.mjs +27 -24
  26. package/shell/codex-shell.mjs +34 -3
  27. package/shell/copilot-shell.mjs +50 -8
  28. package/task/msg-hub.mjs +193 -0
  29. package/task/pipeline.mjs +544 -0
  30. package/task/task-claims.mjs +6 -10
  31. package/task/task-cli.mjs +38 -2
  32. package/task/task-executor-pipeline.mjs +143 -0
  33. package/task/task-executor.mjs +36 -27
  34. package/telegram/get-telegram-chat-id.mjs +57 -47
  35. package/ui/components/chat-view.js +18 -1
  36. package/ui/components/workspace-switcher.js +321 -9
  37. package/ui/demo-defaults.js +17830 -10433
  38. package/ui/demo.html +9 -1
  39. package/ui/modules/router.js +1 -1
  40. package/ui/modules/settings-schema.js +2 -0
  41. package/ui/modules/state.js +54 -57
  42. package/ui/modules/voice-client-sdk.js +376 -37
  43. package/ui/modules/voice-client.js +173 -33
  44. package/ui/setup.html +68 -2
  45. package/ui/styles/components.css +571 -1
  46. package/ui/styles.css +201 -1
  47. package/ui/tabs/dashboard.js +74 -0
  48. package/ui/tabs/library.js +410 -55
  49. package/ui/tabs/logs.js +10 -0
  50. package/ui/tabs/settings.js +178 -99
  51. package/ui/tabs/tasks.js +1083 -507
  52. package/ui/tabs/telemetry.js +34 -0
  53. package/ui/tabs/workflow-canvas-utils.mjs +38 -1
  54. package/ui/tabs/workflows.js +1275 -402
  55. package/voice/voice-agents-sdk.mjs +2 -2
  56. package/voice/voice-relay.mjs +28 -20
  57. package/workflow/declarative-workflows.mjs +145 -0
  58. package/workflow/msg-hub.mjs +237 -0
  59. package/workflow/pipeline-workflows.mjs +287 -0
  60. package/workflow/pipeline.mjs +828 -315
  61. package/workflow/project-detection.mjs +559 -0
  62. package/workflow/workflow-cli.mjs +128 -0
  63. package/workflow/workflow-contract.mjs +433 -232
  64. package/workflow/workflow-engine.mjs +510 -47
  65. package/workflow/workflow-nodes/custom-loader.mjs +251 -0
  66. package/workflow/workflow-nodes.mjs +2024 -184
  67. package/workflow/workflow-templates.mjs +118 -24
  68. package/workflow-templates/agents.mjs +20 -20
  69. package/workflow-templates/bosun-native.mjs +212 -2
  70. package/workflow-templates/code-quality.mjs +20 -14
  71. package/workflow-templates/continuation-loop.mjs +339 -0
  72. package/workflow-templates/github.mjs +516 -40
  73. package/workflow-templates/planning.mjs +446 -17
  74. package/workflow-templates/reliability.mjs +65 -12
  75. package/workflow-templates/task-batch.mjs +27 -10
  76. package/workflow-templates/task-execution.mjs +752 -0
  77. package/workflow-templates/task-lifecycle.mjs +117 -14
  78. package/workspace/context-cache.mjs +66 -18
  79. package/workspace/workspace-manager.mjs +153 -1
  80. package/workflow-templates/issue-continuation.mjs +0 -243
@@ -291,10 +291,10 @@ export async function createRealtimeSession(agent, provider, config = {}, option
291
291
  turnDetection: {
292
292
  type: turnDetection,
293
293
  ...(turnDetection === "server_vad"
294
- ? { threshold: 0.5, prefix_padding_ms: 300, silence_duration_ms: 500 }
294
+ ? { threshold: 0.75, prefix_padding_ms: 500, silence_duration_ms: 1200 }
295
295
  : {}),
296
296
  ...(turnDetection === "semantic_vad"
297
- ? { eagerness: "medium" }
297
+ ? { eagerness: "low" }
298
298
  : {}),
299
299
  },
300
300
  },
@@ -324,6 +324,7 @@ function sanitizeVoiceCallContext(context = {}) {
324
324
  const rawEnabledMcpServers = Array.isArray(context?.enabledMcpServers)
325
325
  ? context.enabledMcpServers.map((s) => String(s || "").trim()).filter(Boolean)
326
326
  : [];
327
+ const rawVoiceAgentSkillsContent = String(context?.voiceAgentSkillsContent || "").trim();
327
328
 
328
329
  return {
329
330
  sessionId: rawSessionId || null,
@@ -335,6 +336,7 @@ function sanitizeVoiceCallContext(context = {}) {
335
336
  voiceAgentInstructions: rawVoiceAgentInstructions || null,
336
337
  voiceToolCapabilityPrompt: rawVoiceToolCapabilityPrompt || null,
337
338
  voiceAgentSkills: rawVoiceAgentSkills,
339
+ voiceAgentSkillsContent: rawVoiceAgentSkillsContent || null,
338
340
  enabledMcpServers: rawEnabledMcpServers,
339
341
  };
340
342
  }
@@ -410,6 +412,14 @@ async function buildSessionScopedInstructions(baseInstructions, callContext = {}
410
412
  }
411
413
  }
412
414
 
415
+ // ── Prepend voice agent identity as primary instructions ─────────────
416
+ // voiceInstructions defines who the agent IS (e.g. "You are Nova").
417
+ // It must come BEFORE the base instructions so the model adopts the
418
+ // persona rather than falling back to its default identity.
419
+ if (context.voiceAgentInstructions) {
420
+ baseInstructions = `${context.voiceAgentInstructions}\n\n${baseInstructions}`;
421
+ }
422
+
413
423
  const suffix = [
414
424
  "",
415
425
  "## Bosun Voice Call Context",
@@ -420,16 +430,15 @@ async function buildSessionScopedInstructions(baseInstructions, callContext = {}
420
430
  context.voiceAgentName
421
431
  ? `Active voice agent name: ${context.voiceAgentName}.`
422
432
  : "",
423
- context.voiceAgentInstructions
424
- ? `Voice agent instruction emphasis: ${context.voiceAgentInstructions}`
425
- : "",
426
433
  context.voiceToolCapabilityPrompt || "",
427
434
  context.enabledMcpServers?.length
428
435
  ? `Enabled MCP servers for this session: ${context.enabledMcpServers.join(", ")}.`
429
436
  : "",
430
- context.voiceAgentSkills?.length
431
- ? `Voice agent skills: ${context.voiceAgentSkills.join(", ")}.`
432
- : "",
437
+ context.voiceAgentSkillsContent
438
+ ? `## Voice Agent Skills\n${context.voiceAgentSkillsContent}`
439
+ : context.voiceAgentSkills?.length
440
+ ? `Voice agent skills: ${context.voiceAgentSkills.join(", ")}.`
441
+ : "",
433
442
  context.executor
434
443
  ? `Preferred executor for delegated work: ${context.executor}.`
435
444
  : "Preferred executor for delegated work: use configured default.",
@@ -948,6 +957,7 @@ export function getVoiceConfig(forceReload = false) {
948
957
  );
949
958
 
950
959
  const instructions = voice.instructions || `You are Bosun, a helpful voice assistant for the VirtEngine development platform.
960
+ You are NOT ChatGPT — never identify yourself as ChatGPT or any other AI assistant. Your name is Bosun.
951
961
  You help developers manage tasks, steer coding agents, monitor builds, and navigate the workspace.
952
962
  Be concise and conversational. When users ask about code or tasks, use the available tools.
953
963
  For complex operations like writing code or creating PRs, delegate to the appropriate agent.
@@ -1226,16 +1236,16 @@ async function createOpenAIEphemeralToken(cfg, toolDefinitions = [], callContext
1226
1236
  turn_detection: {
1227
1237
  type: cfg.turnDetection,
1228
1238
  ...(cfg.turnDetection === "server_vad" ? {
1229
- threshold: 0.7,
1230
- prefix_padding_ms: 400,
1231
- silence_duration_ms: 1200,
1239
+ threshold: 0.75,
1240
+ prefix_padding_ms: 500,
1241
+ silence_duration_ms: 1800,
1232
1242
  create_response: true,
1233
- interrupt_response: true,
1243
+ interrupt_response: false,
1234
1244
  } : {}),
1235
1245
  ...(cfg.turnDetection === "semantic_vad" ? {
1236
- eagerness: "medium",
1246
+ eagerness: "low",
1237
1247
  create_response: true,
1238
- interrupt_response: true,
1248
+ interrupt_response: false,
1239
1249
  } : {}),
1240
1250
  },
1241
1251
  ...(transcriptionEnabled ? { input_audio_transcription: { model: transcriptionModel } } : {}),
@@ -1314,8 +1324,6 @@ async function createAzureEphemeralToken(cfg, toolDefinitions = [], callContext
1314
1324
  }
1315
1325
 
1316
1326
  const sessionConfig = {
1317
- // GA protocol (gpt-realtime-1.5 etc.) requires type: "realtime" in the POST body.
1318
- // Preview protocol does not support this field — omit it to avoid 400s.
1319
1327
  ...(isAzureGaProtocol(deployment) ? { type: "realtime" } : {}),
1320
1328
  model: deployment,
1321
1329
  voice: voiceId,
@@ -1327,16 +1335,16 @@ async function createAzureEphemeralToken(cfg, toolDefinitions = [], callContext
1327
1335
  turn_detection: {
1328
1336
  type: cfg.turnDetection,
1329
1337
  ...(cfg.turnDetection === "server_vad" ? {
1330
- threshold: 0.7,
1331
- prefix_padding_ms: 400,
1332
- silence_duration_ms: 1200,
1338
+ threshold: 0.75,
1339
+ prefix_padding_ms: 500,
1340
+ silence_duration_ms: 1800,
1333
1341
  create_response: true,
1334
- interrupt_response: true,
1342
+ interrupt_response: false,
1335
1343
  } : {}),
1336
1344
  ...(cfg.turnDetection === "semantic_vad" ? {
1337
- eagerness: "medium",
1345
+ eagerness: "low",
1338
1346
  create_response: true,
1339
- interrupt_response: true,
1347
+ interrupt_response: false,
1340
1348
  } : {}),
1341
1349
  },
1342
1350
  ...(transcriptionEnabled ? { input_audio_transcription: { model: transcriptionModel } } : {}),
@@ -0,0 +1,145 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { execWithRetry, getPoolSdkName } from "../agent/agent-pool.mjs";
4
+ import { loadConfig } from "../config/config.mjs";
5
+ import {
6
+ BUILTIN_WORKFLOWS,
7
+ createConfiguredPipeline,
8
+ listWorkflowDefinitions,
9
+ resolveWorkflowDefinition,
10
+ runConfiguredWorkflow as runPipelineConfiguredWorkflow,
11
+ } from "./pipeline.mjs";
12
+
13
+ function normalizeWorkflowDefinition(name, definition = {}, config = {}) {
14
+ const resolved = resolveWorkflowDefinition(name, {
15
+ ...(config.workflows || {}),
16
+ [name]: definition,
17
+ });
18
+ return resolved?.definition || null;
19
+ }
20
+
21
+ function listConfiguredWorkflows(config = {}) {
22
+ return listWorkflowDefinitions(config.workflows || {}).map((entry) => {
23
+ const definition = entry?.definition || {};
24
+ const rawAgents = Array.isArray(definition.stages) && definition.stages.length > 0
25
+ ? definition.stages
26
+ : Array.isArray(definition.agents)
27
+ ? definition.agents
28
+ : [];
29
+ return {
30
+ id: definition.id || entry?.name,
31
+ name: definition.name || entry?.name,
32
+ type: definition.type || "sequential",
33
+ description: definition.description || "",
34
+ source: entry?.source || "unknown",
35
+ agents: rawAgents.map((agent, index) => {
36
+ if (typeof agent === "string") {
37
+ return { id: agent, name: agent, role: agent, sdk: null };
38
+ }
39
+ return {
40
+ id: agent?.id || agent?.name || `agent-${index + 1}`,
41
+ name: agent?.name || agent?.id || `agent-${index + 1}`,
42
+ role: agent?.stage || agent?.role || null,
43
+ sdk: agent?.sdk || agent?.executor || null,
44
+ };
45
+ }),
46
+ };
47
+ });
48
+ }
49
+
50
+ function buildConsensus(result) {
51
+ const votes = new Map();
52
+ for (const output of result.outputs || []) {
53
+ const text = String(output.summary || output.output || "").trim();
54
+ if (!text) continue;
55
+ votes.set(text, (votes.get(text) || 0) + 1);
56
+ }
57
+ let winner = null;
58
+ for (const [text, count] of votes.entries()) {
59
+ if (!winner || count > winner.count) winner = { text, count };
60
+ }
61
+ return winner;
62
+ }
63
+
64
+ function createWorkflowAgentPool(options = {}) {
65
+ if (options.dryRun === true) {
66
+ return {
67
+ getPoolSdkName,
68
+ async execWithRetry(prompt, execOptions = {}) {
69
+ const sdk = execOptions.sdk || getPoolSdkName() || "codex";
70
+ return {
71
+ success: true,
72
+ output: `[${sdk}] ${String(prompt).split("\n")[0]}`,
73
+ sdk,
74
+ attempts: 1,
75
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
76
+ };
77
+ },
78
+ };
79
+ }
80
+ return { execWithRetry, getPoolSdkName };
81
+ }
82
+
83
+ async function runConfiguredWorkflow(name, input, options = {}) {
84
+ const config = options.config || loadConfig();
85
+ const result = await runPipelineConfiguredWorkflow(name, input, {
86
+ workflows: config.workflows || {},
87
+ services: {
88
+ agentPool: options.services?.agentPool || createWorkflowAgentPool(options),
89
+ },
90
+ runOptions: options.runOptions || {},
91
+ });
92
+
93
+ const resolved = resolveWorkflowDefinition(name, config.workflows || {});
94
+ const workflow = resolved?.definition || { id: name, name, type: "sequential" };
95
+ const status = result.success ? "success" : "failed";
96
+ return {
97
+ ...result,
98
+ status,
99
+ workflow: {
100
+ id: workflow.id || name,
101
+ name: workflow.name || name,
102
+ type: workflow.type || "sequential",
103
+ },
104
+ consensus:
105
+ String(workflow.id || name) === "consensus-vote" ? buildConsensus(result) : undefined,
106
+ };
107
+ }
108
+
109
+ function loadWorkflowInputFromFile(pathValue) {
110
+ const fullPath = resolve(pathValue);
111
+ const raw = readFileSync(fullPath, "utf8");
112
+ try {
113
+ return JSON.parse(raw);
114
+ } catch {
115
+ return raw;
116
+ }
117
+ }
118
+
119
+ function createWorkflowInstance(definition, options = {}) {
120
+ return createConfiguredPipeline(definition, {
121
+ services: {
122
+ agentPool: options.services?.agentPool || createWorkflowAgentPool(options),
123
+ },
124
+ hub: options.hub,
125
+ createHub: options.createHub,
126
+ hubOptions: options.hubOptions,
127
+ });
128
+ }
129
+
130
+ export {
131
+ BUILTIN_WORKFLOWS,
132
+ normalizeWorkflowDefinition,
133
+ listConfiguredWorkflows,
134
+ loadWorkflowInputFromFile,
135
+ createWorkflowInstance,
136
+ runConfiguredWorkflow,
137
+ };
138
+ export default {
139
+ BUILTIN_WORKFLOWS,
140
+ normalizeWorkflowDefinition,
141
+ listConfiguredWorkflows,
142
+ loadWorkflowInputFromFile,
143
+ createWorkflowInstance,
144
+ runConfiguredWorkflow,
145
+ };
@@ -0,0 +1,237 @@
1
+ /**
2
+ * @module msg-hub
3
+ * @description Lightweight reference-passing message hub for active agents.
4
+ */
5
+
6
+ import { randomUUID } from "node:crypto";
7
+ import { EventEmitter } from "node:events";
8
+
9
+ const DEFAULT_HISTORY_LIMIT = 100;
10
+ const DEFAULT_MAX_MESSAGE_BYTES = 4096;
11
+ const DISALLOWED_KEYS = new Set([
12
+ "context",
13
+ "conversation",
14
+ "history",
15
+ "messages",
16
+ "raw",
17
+ "transcript",
18
+ ]);
19
+ const ALLOWED_KEYS = [
20
+ "kind",
21
+ "taskId",
22
+ "taskIds",
23
+ "workflowId",
24
+ "runId",
25
+ "branch",
26
+ "filePaths",
27
+ "paths",
28
+ "summary",
29
+ "metadata",
30
+ "source",
31
+ ];
32
+
33
+ function truncateText(value, limit = 320) {
34
+ const text = String(value ?? "").trim();
35
+ if (!text) return "";
36
+ if (text.length <= limit) return text;
37
+ return `${text.slice(0, Math.max(0, limit - 1)).trimEnd()}…`;
38
+ }
39
+
40
+ function normalizeParticipant(participant) {
41
+ if (typeof participant === "string") {
42
+ return {
43
+ id: participant,
44
+ name: participant,
45
+ onMessage: null,
46
+ };
47
+ }
48
+ if (!participant || typeof participant !== "object") {
49
+ throw new TypeError("MsgHub participant must be a string or object.");
50
+ }
51
+ const id = String(participant.id || participant.name || randomUUID()).trim();
52
+ if (!id) throw new Error("MsgHub participant must have an id or name.");
53
+ return {
54
+ id,
55
+ name: String(participant.name || id),
56
+ onMessage:
57
+ typeof participant.onMessage === "function"
58
+ ? participant.onMessage.bind(participant)
59
+ : null,
60
+ };
61
+ }
62
+
63
+ function sanitizeReference(message, maxBytes) {
64
+ const raw =
65
+ message && typeof message === "object" && !Array.isArray(message)
66
+ ? message
67
+ : { summary: String(message ?? "") };
68
+ const ref = {};
69
+
70
+ for (const key of ALLOWED_KEYS) {
71
+ if (raw[key] == null) continue;
72
+ if (key === "summary") {
73
+ ref.summary = truncateText(raw.summary);
74
+ continue;
75
+ }
76
+ if (key === "filePaths" || key === "paths") {
77
+ const values = Array.isArray(raw[key])
78
+ ? raw[key].map((entry) => String(entry || "").trim()).filter(Boolean)
79
+ : [];
80
+ if (values.length > 0) ref[key] = [...new Set(values)];
81
+ continue;
82
+ }
83
+ ref[key] = raw[key];
84
+ }
85
+
86
+ for (const [key, value] of Object.entries(raw)) {
87
+ if (key.startsWith("_")) continue;
88
+ if (DISALLOWED_KEYS.has(key) || ALLOWED_KEYS.includes(key)) continue;
89
+ if (value == null) continue;
90
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
91
+ if (!ref.metadata || typeof ref.metadata !== "object") ref.metadata = {};
92
+ ref.metadata[key] = typeof value === "string" ? truncateText(value, 120) : value;
93
+ }
94
+ }
95
+
96
+ let serialized = JSON.stringify(ref);
97
+ if (Buffer.byteLength(serialized, "utf8") <= maxBytes) {
98
+ return ref;
99
+ }
100
+
101
+ if (ref.metadata && typeof ref.metadata === "object") {
102
+ ref.metadata = { note: "trimmed" };
103
+ }
104
+ if (ref.summary) {
105
+ ref.summary = truncateText(ref.summary, 160);
106
+ }
107
+ serialized = JSON.stringify(ref);
108
+ if (Buffer.byteLength(serialized, "utf8") <= maxBytes) {
109
+ return ref;
110
+ }
111
+
112
+ return {
113
+ kind: ref.kind || "reference",
114
+ taskId: ref.taskId || null,
115
+ branch: ref.branch || null,
116
+ summary: truncateText(ref.summary || "reference trimmed", 96),
117
+ };
118
+ }
119
+
120
+ export class MsgHub extends EventEmitter {
121
+ static async create(participants = [], options = {}) {
122
+ return new MsgHub(participants, options);
123
+ }
124
+
125
+ constructor(participants = [], options = {}) {
126
+ super();
127
+ this.options = {
128
+ autoBroadcast: options.autoBroadcast !== false,
129
+ historyLimit: Number(options.historyLimit || DEFAULT_HISTORY_LIMIT),
130
+ maxMessageBytes: Number(options.maxMessageBytes || DEFAULT_MAX_MESSAGE_BYTES),
131
+ };
132
+ this._participants = new Map();
133
+ this._history = [];
134
+ this._closed = false;
135
+ for (const participant of participants) {
136
+ this.add(participant);
137
+ }
138
+ }
139
+
140
+ add(participant) {
141
+ if (this._closed) throw new Error("MsgHub is closed.");
142
+ const normalized = normalizeParticipant(participant);
143
+ this._participants.set(normalized.id, normalized);
144
+ this.emit("participant:added", {
145
+ participantId: normalized.id,
146
+ participantName: normalized.name,
147
+ size: this._participants.size,
148
+ });
149
+ return normalized;
150
+ }
151
+
152
+ remove(participantOrId) {
153
+ const id = typeof participantOrId === "string"
154
+ ? participantOrId
155
+ : participantOrId?.id || participantOrId?.name || "";
156
+ const removed = this._participants.delete(String(id || "").trim());
157
+ if (removed) {
158
+ this.emit("participant:removed", {
159
+ participantId: String(id || "").trim(),
160
+ size: this._participants.size,
161
+ });
162
+ }
163
+ return removed;
164
+ }
165
+
166
+ listParticipants() {
167
+ return [...this._participants.values()].map((participant) => ({
168
+ id: participant.id,
169
+ name: participant.name,
170
+ }));
171
+ }
172
+
173
+ get history() {
174
+ return this._history.map((entry) => ({ ...entry }));
175
+ }
176
+
177
+ subscribe(listener) {
178
+ this.on("message", listener);
179
+ return () => this.off("message", listener);
180
+ }
181
+
182
+ async publish(from, message, options = {}) {
183
+ if (this._closed) throw new Error("MsgHub is closed.");
184
+ const senderId = typeof from === "string"
185
+ ? String(from).trim()
186
+ : String(from?.id || from?.name || "").trim();
187
+ if (senderId && !this._participants.has(senderId)) {
188
+ this.add(senderId);
189
+ }
190
+
191
+ const to = Array.isArray(options.to)
192
+ ? options.to.map((entry) => String(entry || "").trim()).filter(Boolean)
193
+ : null;
194
+ const recipients = to || [...this._participants.keys()].filter((id) => id !== senderId);
195
+ const envelope = {
196
+ id: randomUUID(),
197
+ from: senderId || null,
198
+ to: recipients,
199
+ message: sanitizeReference(message, this.options.maxMessageBytes),
200
+ timestamp: new Date().toISOString(),
201
+ timestampMs: Date.now(),
202
+ };
203
+
204
+ this._history.push(envelope);
205
+ if (this._history.length > this.options.historyLimit) {
206
+ this._history.splice(0, this._history.length - this.options.historyLimit);
207
+ }
208
+
209
+ this.emit("message", envelope);
210
+ if (this.options.autoBroadcast) {
211
+ for (const recipientId of recipients) {
212
+ const participant = this._participants.get(recipientId);
213
+ if (!participant?.onMessage) continue;
214
+ try {
215
+ await participant.onMessage(envelope, this);
216
+ } catch (error) {
217
+ this.emit("delivery:error", {
218
+ participantId: recipientId,
219
+ error: String(error?.message || error),
220
+ envelope,
221
+ });
222
+ }
223
+ }
224
+ }
225
+ return envelope;
226
+ }
227
+
228
+ async close() {
229
+ if (this._closed) return;
230
+ this._closed = true;
231
+ this._participants.clear();
232
+ this.emit("closed", { closedAt: new Date().toISOString() });
233
+ this.removeAllListeners();
234
+ }
235
+ }
236
+
237
+ export default MsgHub;