@agnishc/edb-bridge 0.12.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,17 @@
1
+ # Changelog
2
+
3
+ All notable changes to `@agnishc/edb-bridge` will be documented in this file.
4
+
5
+ ## [0.12.0] - 2026-05-22
6
+
7
+ ### Added
8
+
9
+ - Initial release
10
+ - Unix socket broker with auto-spawn
11
+ - `ask_supervisor` tool for sub-agents (blocking question to orchestrator)
12
+ - `notify_parent` tool for sub-agents (fire-and-forget progress updates)
13
+ - `answer_subagent` tool for orchestrators (reply to pending sub-agent questions)
14
+ - Internal `pi.events` API: `bridge:ready`, `bridge:task_updated`
15
+ - `PI_BRIDGE_PARENT_SESSION` / `PI_BRIDGE_AGENT_ID` env var support
16
+
17
+ ## [0.10.9] - 2026-05-18
package/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # @agnishc/edb-bridge
2
+
3
+ A Pi CLI extension providing a lightweight inter-session message bus for orchestrator/sub-agent workflows.
4
+
5
+ ## Features
6
+
7
+ - **Unix socket broker** — auto-spawned, zero config
8
+ - **ask_supervisor** — sub-agent blocks until orchestrator answers (10-min timeout)
9
+ - **notify_parent** — sub-agent sends fire-and-forget progress updates to orchestrator
10
+ - **answer_subagent** — orchestrator replies to a pending sub-agent question
11
+ - **Internal pi.events API** — other extensions coordinate via `bridge:ready`, `bridge:task_updated`
12
+
13
+ ## How it works
14
+
15
+ When edb-bridge is installed, each pi session connects to a shared broker process at startup. Sub-agents get `ask_supervisor` and `notify_parent` tools when `PI_BRIDGE_PARENT_SESSION` is set in their environment. The orchestrator uses `answer_subagent` to respond.
16
+
17
+ ## Environment variables (injected by edb-subagents)
18
+
19
+ | Variable | Description |
20
+ |---|---|
21
+ | `PI_BRIDGE_PARENT_SESSION` | Broker session ID of the parent orchestrator |
22
+ | `PI_BRIDGE_AGENT_ID` | edb-subagents agent ID (for routing) |
23
+
24
+ ## Internal pi.events API
25
+
26
+ | Event | Emitted by | Payload |
27
+ |---|---|---|
28
+ | `bridge:ready` | edb-bridge | `{ sessionId: string }` |
29
+ | `bridge:task_updated` | edb-todo | `{ storePath: string }` |
30
+
31
+ ## Install
32
+
33
+ ```bash
34
+ pi install npm:@agnishc/edb-bridge
35
+ ```
36
+
37
+ ## License
38
+
39
+ MIT © Agnish Chakraborty
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@agnishc/edb-bridge",
3
+ "version": "0.12.0",
4
+ "description": "Pi extension: lightweight inter-session message bus for orchestrator/sub-agent workflows",
5
+ "keywords": [
6
+ "pi-package",
7
+ "pi-extension",
8
+ "edb",
9
+ "bridge",
10
+ "ipc"
11
+ ],
12
+ "type": "module",
13
+ "license": "MIT",
14
+ "author": "Agnish Chakraborty",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/agnishcc/pi-extention-monorepo.git",
18
+ "directory": "packages/edb-bridge"
19
+ },
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "scripts": {
24
+ "test": "vitest run"
25
+ },
26
+ "files": [
27
+ "src",
28
+ "README.md",
29
+ "LICENSE",
30
+ "CHANGELOG.md"
31
+ ],
32
+ "pi": {
33
+ "extensions": [
34
+ "./src/index.ts"
35
+ ]
36
+ },
37
+ "peerDependencies": {
38
+ "@earendil-works/pi-coding-agent": "*",
39
+ "@earendil-works/pi-tui": "*",
40
+ "typebox": "*"
41
+ }
42
+ }
package/src/broker.ts ADDED
@@ -0,0 +1,180 @@
1
+ /**
2
+ * edb-bridge broker process.
3
+ *
4
+ * Spawned as a detached child by the first connecting session.
5
+ * Routes messages between connected sessions by broker session ID.
6
+ * Shuts itself down 5s after the last session disconnects.
7
+ *
8
+ * Run: node broker.ts (or tsx broker.ts for development)
9
+ */
10
+
11
+ import { randomUUID } from "node:crypto";
12
+ import { mkdirSync, unlinkSync, writeFileSync } from "node:fs";
13
+ import net from "node:net";
14
+ import { createMessageReader, writeMessage } from "./framing.js";
15
+ import { BRIDGE_DIR, getBrokerPidPath, getBrokerSocketPath } from "./paths.js";
16
+ import type { BridgeMessage, ClientMessage, SessionInfo } from "./types.js";
17
+
18
+ const SOCKET_PATH = getBrokerSocketPath();
19
+ const PID_PATH = getBrokerPidPath();
20
+ const SHUTDOWN_IDLE_MS = 5_000;
21
+
22
+ interface ConnectedSession {
23
+ socket: net.Socket;
24
+ info: SessionInfo;
25
+ }
26
+
27
+ class EdbBridgeBroker {
28
+ private sessions = new Map<string, ConnectedSession>();
29
+ private server: net.Server;
30
+ private shutdownTimer: ReturnType<typeof setTimeout> | null = null;
31
+
32
+ constructor() {
33
+ mkdirSync(BRIDGE_DIR, { recursive: true });
34
+ if (process.platform !== "win32") {
35
+ try {
36
+ unlinkSync(SOCKET_PATH);
37
+ } catch {
38
+ /* stale socket */
39
+ }
40
+ }
41
+ this.server = net.createServer(this.handleConnection.bind(this));
42
+ }
43
+
44
+ start(): void {
45
+ this.server.listen(SOCKET_PATH, () => {
46
+ writeFileSync(PID_PATH, String(process.pid));
47
+ console.log(`edb-bridge broker started (pid: ${process.pid})`);
48
+ });
49
+ process.on("SIGTERM", () => this.shutdown());
50
+ process.on("SIGINT", () => this.shutdown());
51
+ }
52
+
53
+ private handleConnection(socket: net.Socket): void {
54
+ let sessionId: string | null = null;
55
+
56
+ const reader = createMessageReader(
57
+ (msg) =>
58
+ this.handleMessage(socket, msg as ClientMessage, sessionId, (id) => {
59
+ sessionId = id;
60
+ }),
61
+ (err) => socket.destroy(err),
62
+ );
63
+
64
+ socket.on("data", reader);
65
+ socket.on("close", () => {
66
+ if (sessionId) {
67
+ this.sessions.delete(sessionId);
68
+ this.scheduleIdleShutdown();
69
+ }
70
+ });
71
+ socket.on("error", () => {
72
+ /* ignore */
73
+ });
74
+ }
75
+
76
+ private handleMessage(
77
+ socket: net.Socket,
78
+ msg: ClientMessage,
79
+ currentId: string | null,
80
+ setId: (id: string) => void,
81
+ ): void {
82
+ if (!msg || typeof msg !== "object" || !("type" in msg)) {
83
+ throw new Error("invalid message");
84
+ }
85
+
86
+ if (currentId === null && msg.type !== "register") {
87
+ throw new Error(`received '${msg.type}' before register`);
88
+ }
89
+
90
+ switch (msg.type) {
91
+ case "register": {
92
+ if (currentId) throw new Error("duplicate register");
93
+ const id = randomUUID();
94
+ setId(id);
95
+ const info: SessionInfo = { ...msg.session, id };
96
+ this.sessions.set(id, { socket, info });
97
+ if (this.shutdownTimer) {
98
+ clearTimeout(this.shutdownTimer);
99
+ this.shutdownTimer = null;
100
+ }
101
+ writeMessage(socket, { type: "registered", sessionId: id });
102
+ break;
103
+ }
104
+
105
+ case "unregister": {
106
+ if (currentId) {
107
+ this.sessions.delete(currentId);
108
+ this.scheduleIdleShutdown();
109
+ }
110
+ break;
111
+ }
112
+
113
+ case "list": {
114
+ if (typeof msg.requestId !== "string") throw new Error("invalid list");
115
+ const sessions = Array.from(this.sessions.values()).map((s) => s.info);
116
+ writeMessage(socket, { type: "sessions", requestId: msg.requestId, sessions });
117
+ break;
118
+ }
119
+
120
+ case "send": {
121
+ const message = msg.message as BridgeMessage;
122
+ if (!message?.id || typeof msg.to !== "string") {
123
+ writeMessage(socket, {
124
+ type: "delivery_failed",
125
+ messageId: message?.id ?? "",
126
+ reason: "invalid message",
127
+ });
128
+ break;
129
+ }
130
+ const target = this.sessions.get(msg.to);
131
+ if (!target) {
132
+ writeMessage(socket, { type: "delivery_failed", messageId: message.id, reason: "session not found" });
133
+ break;
134
+ }
135
+ const fromSession = this.sessions.get(currentId!);
136
+ if (!fromSession) {
137
+ writeMessage(socket, { type: "delivery_failed", messageId: message.id, reason: "sender not found" });
138
+ break;
139
+ }
140
+ writeMessage(target.socket, { type: "message", from: fromSession.info, message });
141
+ writeMessage(socket, { type: "delivered", messageId: message.id });
142
+ break;
143
+ }
144
+
145
+ default:
146
+ throw new Error(`unknown message type: ${(msg as any).type}`);
147
+ }
148
+ }
149
+
150
+ private scheduleIdleShutdown(): void {
151
+ if (this.shutdownTimer) return;
152
+ this.shutdownTimer = setTimeout(() => {
153
+ this.shutdownTimer = null;
154
+ if (this.sessions.size === 0) {
155
+ console.log("edb-bridge: no sessions, shutting down");
156
+ this.shutdown();
157
+ }
158
+ }, SHUTDOWN_IDLE_MS);
159
+ }
160
+
161
+ private shutdown(): void {
162
+ for (const s of this.sessions.values()) s.socket.end();
163
+ this.sessions.clear();
164
+ if (process.platform !== "win32") {
165
+ try {
166
+ unlinkSync(SOCKET_PATH);
167
+ } catch {
168
+ /* ignore */
169
+ }
170
+ }
171
+ try {
172
+ unlinkSync(PID_PATH);
173
+ } catch {
174
+ /* ignore */
175
+ }
176
+ this.server.close(() => process.exit(0));
177
+ }
178
+ }
179
+
180
+ new EdbBridgeBroker().start();
package/src/client.ts ADDED
@@ -0,0 +1,368 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { EventEmitter } from "node:events";
3
+ import net from "node:net";
4
+ import { createMessageReader, writeMessage } from "./framing.js";
5
+ import { getBrokerSocketPath } from "./paths.js";
6
+ import type { BridgeMessage, BrokerMessage, SessionInfo } from "./types.js";
7
+
8
+ const SOCKET_PATH = getBrokerSocketPath();
9
+
10
+ export interface SendOptions {
11
+ text: string;
12
+ data?: Record<string, unknown>;
13
+ type?: string;
14
+ replyTo?: string;
15
+ expectsReply?: boolean;
16
+ messageId?: string;
17
+ }
18
+
19
+ export interface SendResult {
20
+ id: string;
21
+ delivered: boolean;
22
+ reason?: string;
23
+ }
24
+
25
+ interface PendingSend {
26
+ resolve: (r: SendResult) => void;
27
+ reject: (e: Error) => void;
28
+ }
29
+
30
+ interface PendingList {
31
+ resolve: (sessions: SessionInfo[]) => void;
32
+ reject: (e: Error) => void;
33
+ }
34
+
35
+ function toError(e: unknown): Error {
36
+ return e instanceof Error ? e : new Error(String(e));
37
+ }
38
+
39
+ function isSessionInfo(v: unknown): v is SessionInfo {
40
+ if (!v || typeof v !== "object") return false;
41
+ const s = v as Record<string, unknown>;
42
+ return typeof s.id === "string" && typeof s.cwd === "string";
43
+ }
44
+
45
+ function isBridgeMessage(v: unknown): v is BridgeMessage {
46
+ if (!v || typeof v !== "object") return false;
47
+ const m = v as Record<string, unknown>;
48
+ return (
49
+ typeof m.id === "string" &&
50
+ typeof m.timestamp === "number" &&
51
+ m.content !== null &&
52
+ typeof m.content === "object" &&
53
+ typeof (m.content as any).text === "string"
54
+ );
55
+ }
56
+
57
+ export class BridgeClient extends EventEmitter {
58
+ private socket: net.Socket | null = null;
59
+ private _sessionId: string | null = null;
60
+ private pendingSends = new Map<string, PendingSend>();
61
+ private pendingLists = new Map<string, PendingList>();
62
+ private disconnecting = false;
63
+ private disconnectError: Error | null = null;
64
+
65
+ get sessionId(): string | null {
66
+ return this._sessionId;
67
+ }
68
+
69
+ isConnected(): boolean {
70
+ const s = this.socket;
71
+ return Boolean(s && this._sessionId && !this.disconnecting && !s.destroyed && !s.writableEnded && s.writable);
72
+ }
73
+
74
+ private requireSocket(): net.Socket {
75
+ if (this.disconnecting) throw new Error("disconnecting");
76
+ const s = this.socket;
77
+ if (!s || !this._sessionId) throw new Error("not connected");
78
+ if (s.destroyed || s.writableEnded || !s.writable) throw new Error("disconnected");
79
+ return s;
80
+ }
81
+
82
+ private failPending(err: Error): void {
83
+ for (const p of this.pendingSends.values()) p.reject(err);
84
+ this.pendingSends.clear();
85
+ for (const p of this.pendingLists.values()) p.reject(err);
86
+ this.pendingLists.clear();
87
+ }
88
+
89
+ connect(session: Omit<SessionInfo, "id">): Promise<void> {
90
+ if (this.socket) return Promise.reject(new Error("already connected"));
91
+
92
+ return new Promise((resolve, reject) => {
93
+ const socket = net.connect(SOCKET_PATH);
94
+ this.socket = socket;
95
+ this.disconnectError = null;
96
+ let settled = false;
97
+ let connected = false;
98
+
99
+ const timeout = setTimeout(() => {
100
+ if (!this._sessionId) {
101
+ cleanupAttempt();
102
+ cleanupSocket();
103
+ if (this.socket === socket) this.socket = null;
104
+ socket.destroy();
105
+ reject(new Error("edb-bridge: connection timeout"));
106
+ }
107
+ }, 10_000);
108
+
109
+ const onRegistered = () => {
110
+ settled = true;
111
+ connected = true;
112
+ cleanupAttempt();
113
+ resolve();
114
+ };
115
+
116
+ const onError = (err: Error) => {
117
+ settled = true;
118
+ cleanupAttempt();
119
+ cleanupSocket();
120
+ if (this.socket === socket) this.socket = null;
121
+ socket.destroy();
122
+ reject(err);
123
+ };
124
+
125
+ const onClose = () => {
126
+ const wasConnecting = !settled && !this._sessionId;
127
+ const wasDisconnecting = this.disconnecting;
128
+ const err = this.disconnectError ?? new Error("edb-bridge: disconnected");
129
+ this.disconnecting = false;
130
+ cleanupAttempt();
131
+ cleanupSocket();
132
+ this.failPending(err);
133
+ if (this.socket === socket) this.socket = null;
134
+ this._sessionId = null;
135
+ this.disconnectError = null;
136
+ if (connected && !wasDisconnecting) this.emit("disconnected", err);
137
+ if (wasConnecting) reject(new Error("edb-bridge: connection closed before registration"));
138
+ };
139
+
140
+ const onSocketError = (err: Error) => {
141
+ if (connected) {
142
+ this.disconnectError = err;
143
+ this.emit("error", err);
144
+ }
145
+ };
146
+
147
+ const onReaderError = (err: Error) => {
148
+ if (!connected) {
149
+ onError(err);
150
+ return;
151
+ }
152
+ this.disconnectError = err;
153
+ this.emit("error", err);
154
+ socket.destroy();
155
+ };
156
+
157
+ const reader = createMessageReader((msg) => this.handleBrokerMessage(msg as BrokerMessage), onReaderError);
158
+
159
+ const cleanupAttempt = () => {
160
+ this.off("_registered", onRegistered);
161
+ socket.off("error", onError);
162
+ clearTimeout(timeout);
163
+ };
164
+
165
+ const cleanupSocket = () => {
166
+ socket.off("data", reader);
167
+ socket.off("error", onSocketError);
168
+ socket.off("close", onClose);
169
+ };
170
+
171
+ socket.on("data", reader);
172
+ socket.on("error", onError);
173
+ socket.on("close", onClose);
174
+ socket.on("error", onSocketError);
175
+ this.once("_registered", onRegistered);
176
+
177
+ try {
178
+ writeMessage(socket, { type: "register", session });
179
+ } catch (err) {
180
+ cleanupAttempt();
181
+ cleanupSocket();
182
+ if (this.socket === socket) this.socket = null;
183
+ socket.destroy();
184
+ reject(toError(err));
185
+ }
186
+ });
187
+ }
188
+
189
+ private handleBrokerMessage(msg: BrokerMessage): void {
190
+ if (!msg || typeof msg !== "object" || !("type" in msg)) {
191
+ throw new Error("invalid broker message");
192
+ }
193
+
194
+ switch (msg.type) {
195
+ case "registered": {
196
+ if (this._sessionId !== null) throw new Error("duplicate registered");
197
+ this._sessionId = msg.sessionId;
198
+ this.emit("_registered");
199
+ break;
200
+ }
201
+
202
+ case "sessions": {
203
+ const p = this.pendingLists.get(msg.requestId);
204
+ if (!p) return;
205
+ this.pendingLists.delete(msg.requestId);
206
+ p.resolve(msg.sessions as SessionInfo[]);
207
+ break;
208
+ }
209
+
210
+ case "message": {
211
+ if (!isSessionInfo(msg.from) || !isBridgeMessage(msg.message)) {
212
+ throw new Error("invalid message event");
213
+ }
214
+ this.emit("message", msg.from, msg.message);
215
+ break;
216
+ }
217
+
218
+ case "delivered": {
219
+ const p = this.pendingSends.get(msg.messageId);
220
+ if (!p) return;
221
+ this.pendingSends.delete(msg.messageId);
222
+ p.resolve({ id: msg.messageId, delivered: true });
223
+ break;
224
+ }
225
+
226
+ case "delivery_failed": {
227
+ const p = this.pendingSends.get(msg.messageId);
228
+ if (!p) return;
229
+ this.pendingSends.delete(msg.messageId);
230
+ p.resolve({ id: msg.messageId, delivered: false, reason: msg.reason });
231
+ break;
232
+ }
233
+
234
+ case "session_joined":
235
+ case "session_left":
236
+ // broadcast events — not used currently
237
+ break;
238
+
239
+ default:
240
+ throw new Error(`unknown broker message type: ${(msg as any).type}`);
241
+ }
242
+ }
243
+
244
+ async disconnect(): Promise<void> {
245
+ const socket = this.socket;
246
+ if (!socket) return;
247
+
248
+ this.disconnecting = true;
249
+ this.disconnectError = null;
250
+ this.failPending(new Error("edb-bridge: disconnected"));
251
+
252
+ await new Promise<void>((resolve) => {
253
+ let done = false;
254
+ const finish = () => {
255
+ if (done) return;
256
+ done = true;
257
+ clearTimeout(t);
258
+ socket.off("close", onClose);
259
+ socket.off("error", onError);
260
+ resolve();
261
+ };
262
+ const onClose = () => finish();
263
+ const onError = () => {
264
+ socket.destroy();
265
+ };
266
+ const t = setTimeout(() => {
267
+ socket.destroy();
268
+ }, 2000);
269
+ socket.once("close", onClose);
270
+ socket.once("error", onError);
271
+ try {
272
+ writeMessage(socket, { type: "unregister" });
273
+ socket.end();
274
+ } catch {
275
+ socket.destroy();
276
+ }
277
+ });
278
+ }
279
+
280
+ listSessions(): Promise<SessionInfo[]> {
281
+ let socket: net.Socket;
282
+ try {
283
+ socket = this.requireSocket();
284
+ } catch (e) {
285
+ return Promise.reject(toError(e));
286
+ }
287
+
288
+ return new Promise((resolve, reject) => {
289
+ const requestId = randomUUID();
290
+ let done = false;
291
+ const timeout = setTimeout(() => {
292
+ if (!done) {
293
+ done = true;
294
+ this.pendingLists.delete(requestId);
295
+ reject(new Error("edb-bridge: list sessions timeout"));
296
+ }
297
+ }, 5000);
298
+ this.pendingLists.set(requestId, {
299
+ resolve: (sessions) => {
300
+ clearTimeout(timeout);
301
+ done = true;
302
+ resolve(sessions);
303
+ },
304
+ reject: (err) => {
305
+ clearTimeout(timeout);
306
+ done = true;
307
+ reject(err);
308
+ },
309
+ });
310
+ try {
311
+ writeMessage(socket, { type: "list", requestId });
312
+ } catch (err) {
313
+ clearTimeout(timeout);
314
+ this.pendingLists.delete(requestId);
315
+ reject(toError(err));
316
+ }
317
+ });
318
+ }
319
+
320
+ send(to: string, options: SendOptions): Promise<SendResult> {
321
+ let socket: net.Socket;
322
+ try {
323
+ socket = this.requireSocket();
324
+ } catch (e) {
325
+ return Promise.reject(toError(e));
326
+ }
327
+
328
+ const messageId = options.messageId ?? randomUUID();
329
+ const message: BridgeMessage = {
330
+ id: messageId,
331
+ timestamp: Date.now(),
332
+ replyTo: options.replyTo,
333
+ expectsReply: options.expectsReply,
334
+ type: options.type,
335
+ content: { text: options.text, data: options.data },
336
+ };
337
+
338
+ return new Promise((resolve, reject) => {
339
+ let done = false;
340
+ const timeout = setTimeout(() => {
341
+ if (!done) {
342
+ done = true;
343
+ this.pendingSends.delete(messageId);
344
+ reject(new Error("edb-bridge: send timeout"));
345
+ }
346
+ }, 10_000);
347
+ this.pendingSends.set(messageId, {
348
+ resolve: (r) => {
349
+ clearTimeout(timeout);
350
+ done = true;
351
+ resolve(r);
352
+ },
353
+ reject: (err) => {
354
+ clearTimeout(timeout);
355
+ done = true;
356
+ reject(err);
357
+ },
358
+ });
359
+ try {
360
+ writeMessage(socket, { type: "send", to, message });
361
+ } catch (err) {
362
+ clearTimeout(timeout);
363
+ this.pendingSends.delete(messageId);
364
+ reject(toError(err));
365
+ }
366
+ });
367
+ }
368
+ }
package/src/framing.ts ADDED
@@ -0,0 +1,49 @@
1
+ import type { Socket } from "node:net";
2
+
3
+ /**
4
+ * Write a length-prefixed JSON message to a socket.
5
+ * Format: 4-byte big-endian uint32 length + UTF-8 JSON payload.
6
+ */
7
+ export function writeMessage(socket: Socket, msg: unknown): void {
8
+ const json = JSON.stringify(msg);
9
+ const payload = Buffer.from(json, "utf-8");
10
+ const header = Buffer.alloc(4);
11
+ header.writeUInt32BE(payload.length, 0);
12
+ socket.write(Buffer.concat([header, payload]));
13
+ }
14
+
15
+ /**
16
+ * Create a streaming message reader that handles partial TCP reads.
17
+ * Calls onMessage for each complete framed message.
18
+ * Protocol errors are reported to onError — caller should close the socket.
19
+ */
20
+ export function createMessageReader(onMessage: (msg: unknown) => void, onError: (error: Error) => void) {
21
+ let buffer = Buffer.alloc(0);
22
+
23
+ return (data: Buffer) => {
24
+ buffer = Buffer.concat([buffer, data]);
25
+
26
+ while (buffer.length >= 4) {
27
+ const length = buffer.readUInt32BE(0);
28
+ if (buffer.length < 4 + length) break;
29
+
30
+ const payload = buffer.subarray(4, 4 + length);
31
+ buffer = buffer.subarray(4 + length);
32
+
33
+ let msg: unknown;
34
+ try {
35
+ msg = JSON.parse(payload.toString("utf-8"));
36
+ } catch (err) {
37
+ onError(new Error(`edb-bridge: failed to parse message: ${err}`, { cause: err }));
38
+ return;
39
+ }
40
+
41
+ try {
42
+ onMessage(msg);
43
+ } catch (err) {
44
+ onError(new Error(`edb-bridge: failed to handle message: ${err}`, { cause: err }));
45
+ return;
46
+ }
47
+ }
48
+ };
49
+ }