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
|
@@ -291,7 +291,7 @@ export async function createRealtimeSession(agent, provider, config = {}, option
|
|
|
291
291
|
turnDetection: {
|
|
292
292
|
type: turnDetection,
|
|
293
293
|
...(turnDetection === "server_vad"
|
|
294
|
-
? { threshold: 0.
|
|
294
|
+
? { threshold: 0.75, prefix_padding_ms: 500, silence_duration_ms: 1200 }
|
|
295
295
|
: {}),
|
|
296
296
|
...(turnDetection === "semantic_vad"
|
|
297
297
|
? { eagerness: "low" }
|
package/voice/voice-relay.mjs
CHANGED
|
@@ -1236,16 +1236,16 @@ async function createOpenAIEphemeralToken(cfg, toolDefinitions = [], callContext
|
|
|
1236
1236
|
turn_detection: {
|
|
1237
1237
|
type: cfg.turnDetection,
|
|
1238
1238
|
...(cfg.turnDetection === "server_vad" ? {
|
|
1239
|
-
threshold: 0.
|
|
1239
|
+
threshold: 0.75,
|
|
1240
1240
|
prefix_padding_ms: 500,
|
|
1241
1241
|
silence_duration_ms: 1800,
|
|
1242
1242
|
create_response: true,
|
|
1243
|
-
interrupt_response:
|
|
1243
|
+
interrupt_response: false,
|
|
1244
1244
|
} : {}),
|
|
1245
1245
|
...(cfg.turnDetection === "semantic_vad" ? {
|
|
1246
1246
|
eagerness: "low",
|
|
1247
1247
|
create_response: true,
|
|
1248
|
-
interrupt_response:
|
|
1248
|
+
interrupt_response: false,
|
|
1249
1249
|
} : {}),
|
|
1250
1250
|
},
|
|
1251
1251
|
...(transcriptionEnabled ? { input_audio_transcription: { model: transcriptionModel } } : {}),
|
|
@@ -1335,16 +1335,16 @@ async function createAzureEphemeralToken(cfg, toolDefinitions = [], callContext
|
|
|
1335
1335
|
turn_detection: {
|
|
1336
1336
|
type: cfg.turnDetection,
|
|
1337
1337
|
...(cfg.turnDetection === "server_vad" ? {
|
|
1338
|
-
threshold: 0.
|
|
1338
|
+
threshold: 0.75,
|
|
1339
1339
|
prefix_padding_ms: 500,
|
|
1340
1340
|
silence_duration_ms: 1800,
|
|
1341
1341
|
create_response: true,
|
|
1342
|
-
interrupt_response:
|
|
1342
|
+
interrupt_response: false,
|
|
1343
1343
|
} : {}),
|
|
1344
1344
|
...(cfg.turnDetection === "semantic_vad" ? {
|
|
1345
1345
|
eagerness: "low",
|
|
1346
1346
|
create_response: true,
|
|
1347
|
-
interrupt_response:
|
|
1347
|
+
interrupt_response: false,
|
|
1348
1348
|
} : {}),
|
|
1349
1349
|
},
|
|
1350
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;
|