agent-inbox 0.1.7 → 0.1.9

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 (70) hide show
  1. package/CLAUDE.md +44 -7
  2. package/README.md +67 -24
  3. package/dist/federation/connection-manager.d.ts +13 -2
  4. package/dist/federation/connection-manager.d.ts.map +1 -1
  5. package/dist/federation/connection-manager.js +109 -10
  6. package/dist/federation/connection-manager.js.map +1 -1
  7. package/dist/index.d.mts +2 -0
  8. package/dist/index.d.ts +25 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +58 -5
  11. package/dist/index.js.map +1 -1
  12. package/dist/index.mjs +1 -0
  13. package/dist/index.mjs.map +1 -0
  14. package/dist/ipc/ipc-server.d.ts +2 -0
  15. package/dist/ipc/ipc-server.d.ts.map +1 -1
  16. package/dist/ipc/ipc-server.js +48 -0
  17. package/dist/ipc/ipc-server.js.map +1 -1
  18. package/dist/map/map-client.d.ts +100 -0
  19. package/dist/map/map-client.d.ts.map +1 -1
  20. package/dist/map/map-client.js +61 -0
  21. package/dist/map/map-client.js.map +1 -1
  22. package/dist/mcp/mcp-proxy.d.ts +28 -0
  23. package/dist/mcp/mcp-proxy.d.ts.map +1 -0
  24. package/dist/mcp/mcp-proxy.js +280 -0
  25. package/dist/mcp/mcp-proxy.js.map +1 -0
  26. package/dist/mesh/delivery-bridge.d.ts +47 -0
  27. package/dist/mesh/delivery-bridge.d.ts.map +1 -0
  28. package/dist/mesh/delivery-bridge.js +73 -0
  29. package/dist/mesh/delivery-bridge.js.map +1 -0
  30. package/dist/mesh/mesh-connector.d.ts +29 -0
  31. package/dist/mesh/mesh-connector.d.ts.map +1 -0
  32. package/dist/mesh/mesh-connector.js +36 -0
  33. package/dist/mesh/mesh-connector.js.map +1 -0
  34. package/dist/mesh/mesh-transport.d.ts +70 -0
  35. package/dist/mesh/mesh-transport.d.ts.map +1 -0
  36. package/dist/mesh/mesh-transport.js +92 -0
  37. package/dist/mesh/mesh-transport.js.map +1 -0
  38. package/dist/mesh/type-mapper.d.ts +67 -0
  39. package/dist/mesh/type-mapper.d.ts.map +1 -0
  40. package/dist/mesh/type-mapper.js +165 -0
  41. package/dist/mesh/type-mapper.js.map +1 -0
  42. package/dist/types.d.ts +29 -2
  43. package/dist/types.d.ts.map +1 -1
  44. package/docs/CLAUDE-CODE-SWARM-PROPOSAL.md +137 -0
  45. package/package.json +7 -2
  46. package/src/federation/connection-manager.ts +125 -10
  47. package/src/index.ts +96 -5
  48. package/src/ipc/ipc-server.ts +58 -0
  49. package/src/map/map-client.ts +152 -0
  50. package/src/mcp/mcp-proxy.ts +326 -0
  51. package/src/mesh/delivery-bridge.ts +110 -0
  52. package/src/mesh/mesh-connector.ts +41 -0
  53. package/src/mesh/mesh-transport.ts +157 -0
  54. package/src/mesh/type-mapper.ts +239 -0
  55. package/src/types.ts +33 -1
  56. package/test/federation/integration.test.ts +37 -3
  57. package/test/federation/sdk-integration.test.ts +4 -8
  58. package/test/ipc-new-commands.test.ts +200 -0
  59. package/test/mcp-proxy.test.ts +191 -0
  60. package/test/mesh/delivery-bridge.test.ts +178 -0
  61. package/test/mesh/e2e-mesh.test.ts +527 -0
  62. package/test/mesh/e2e-real-meshpeer.test.ts +629 -0
  63. package/test/mesh/federation-mesh.test.ts +269 -0
  64. package/test/mesh/mesh-connector.test.ts +66 -0
  65. package/test/mesh/mesh-transport.test.ts +191 -0
  66. package/test/mesh/meshpeer-integration.test.ts +442 -0
  67. package/test/mesh/mock-mesh.ts +125 -0
  68. package/test/mesh/mock-meshpeer.ts +266 -0
  69. package/test/mesh/type-mapper.test.ts +226 -0
  70. package/docs/PLAN.md +0 -545
@@ -0,0 +1,326 @@
1
+ /**
2
+ * mcp-proxy.ts — MCP server that proxies all tools to an existing inbox IPC socket.
3
+ *
4
+ * Instead of creating its own storage/router, this connects to a running
5
+ * agent-inbox IPC server (e.g., the sidecar's inbox instance) and translates
6
+ * MCP tool calls into IPC commands.
7
+ *
8
+ * This ensures a single source of truth for messages, agents, and routing.
9
+ */
10
+
11
+ import * as net from "node:net";
12
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
14
+ import { z } from "zod";
15
+ import type { IpcResponse } from "../types.js";
16
+
17
+ const IPC_TIMEOUT_MS = 5000;
18
+ const CONNECT_RETRY_MS = 500;
19
+ const CONNECT_MAX_RETRIES = 10;
20
+
21
+ export class InboxMcpProxy {
22
+ private mcp: McpServer;
23
+
24
+ constructor(
25
+ private socketPath: string,
26
+ private defaultAgentId: string = "anonymous",
27
+ private defaultScope: string = "default"
28
+ ) {
29
+ this.mcp = new McpServer({
30
+ name: "agent-inbox",
31
+ version: "0.1.0",
32
+ });
33
+
34
+ this.registerTools();
35
+ }
36
+
37
+ /**
38
+ * Send an IPC command to the inbox socket and return the response.
39
+ * Retries connection if socket is not yet available.
40
+ */
41
+ private async sendIpc(command: Record<string, unknown>): Promise<IpcResponse> {
42
+ let lastError: Error | null = null;
43
+
44
+ for (let attempt = 0; attempt < CONNECT_MAX_RETRIES; attempt++) {
45
+ try {
46
+ return await this.sendIpcOnce(command);
47
+ } catch (err) {
48
+ lastError = err instanceof Error ? err : new Error(String(err));
49
+ // Only retry on connection errors (socket not ready yet)
50
+ if (lastError.message.includes("ENOENT") || lastError.message.includes("ECONNREFUSED")) {
51
+ if (attempt < CONNECT_MAX_RETRIES - 1) {
52
+ await new Promise((r) => setTimeout(r, CONNECT_RETRY_MS));
53
+ continue;
54
+ }
55
+ }
56
+ break;
57
+ }
58
+ }
59
+
60
+ return { ok: false, error: `Inbox unavailable: ${lastError?.message ?? "unknown error"}` };
61
+ }
62
+
63
+ private sendIpcOnce(command: Record<string, unknown>): Promise<IpcResponse> {
64
+ return new Promise((resolve, reject) => {
65
+ const client = net.createConnection(this.socketPath);
66
+ let buffer = "";
67
+ let settled = false;
68
+
69
+ const timer = setTimeout(() => {
70
+ if (!settled) {
71
+ settled = true;
72
+ client.destroy();
73
+ reject(new Error("IPC timeout"));
74
+ }
75
+ }, IPC_TIMEOUT_MS);
76
+
77
+ client.on("connect", () => {
78
+ client.write(JSON.stringify(command) + "\n");
79
+ });
80
+
81
+ client.on("data", (data) => {
82
+ buffer += data.toString();
83
+ const newlineIdx = buffer.indexOf("\n");
84
+ if (newlineIdx !== -1) {
85
+ clearTimeout(timer);
86
+ settled = true;
87
+ const line = buffer.slice(0, newlineIdx).trim();
88
+ client.destroy();
89
+ try {
90
+ resolve(JSON.parse(line) as IpcResponse);
91
+ } catch {
92
+ reject(new Error("Invalid IPC response"));
93
+ }
94
+ }
95
+ });
96
+
97
+ client.on("error", (err) => {
98
+ if (!settled) {
99
+ clearTimeout(timer);
100
+ settled = true;
101
+ reject(err);
102
+ }
103
+ });
104
+ });
105
+ }
106
+
107
+ private registerTools(): void {
108
+ this.mcp.tool(
109
+ "send_message",
110
+ "Send a message to one or more agents. Supports replies (inReplyTo), threading (threadTag), and federated addressing (agent@system).",
111
+ {
112
+ to: z
113
+ .union([z.string(), z.array(z.string())])
114
+ .describe(
115
+ "Recipient agent ID(s). Use 'agent@system' for federated addressing."
116
+ ),
117
+ body: z
118
+ .string()
119
+ .optional()
120
+ .describe("Plain text message body (shorthand for content)"),
121
+ content: z
122
+ .object({ type: z.string() })
123
+ .passthrough()
124
+ .optional()
125
+ .describe("Structured message content"),
126
+ from: z
127
+ .string()
128
+ .optional()
129
+ .describe("Sender agent ID (defaults to caller)"),
130
+ threadTag: z
131
+ .string()
132
+ .optional()
133
+ .describe("Thread tag for grouping related messages"),
134
+ inReplyTo: z
135
+ .string()
136
+ .optional()
137
+ .describe("Message ID this is a reply to"),
138
+ importance: z
139
+ .enum(["low", "normal", "high", "urgent"])
140
+ .optional()
141
+ .describe("Message importance level"),
142
+ subject: z.string().optional().describe("Message subject"),
143
+ },
144
+ async ({ to, body, content, from, threadTag, inReplyTo, importance, subject }) => {
145
+ const payload = content ?? body ?? "";
146
+ const senderId = from ?? this.defaultAgentId;
147
+ const resp = await this.sendIpc({
148
+ action: "send",
149
+ from: senderId,
150
+ to,
151
+ payload,
152
+ threadTag,
153
+ inReplyTo,
154
+ importance,
155
+ subject,
156
+ });
157
+ return {
158
+ content: [
159
+ {
160
+ type: "text" as const,
161
+ text: JSON.stringify(
162
+ resp.ok
163
+ ? { ok: true, messageId: resp.messageId }
164
+ : { ok: false, error: resp.error }
165
+ ),
166
+ },
167
+ ],
168
+ };
169
+ }
170
+ );
171
+
172
+ this.mcp.tool(
173
+ "check_inbox",
174
+ "Check inbox for messages addressed to an agent. Auto-registers the agent if not already registered.",
175
+ {
176
+ agentId: z.string().describe("Agent ID to check inbox for"),
177
+ unreadOnly: z
178
+ .boolean()
179
+ .optional()
180
+ .describe("Only return unread messages (default true)"),
181
+ limit: z
182
+ .number()
183
+ .optional()
184
+ .describe("Max messages to return"),
185
+ },
186
+ async ({ agentId, unreadOnly, limit }) => {
187
+ const resp = await this.sendIpc({
188
+ action: "check_inbox",
189
+ agentId,
190
+ unreadOnly: unreadOnly ?? true,
191
+ clear: true, // Mark as read after retrieval
192
+ });
193
+
194
+ if (!resp.ok) {
195
+ return {
196
+ content: [
197
+ { type: "text" as const, text: JSON.stringify({ ok: false, error: resp.error }) },
198
+ ],
199
+ };
200
+ }
201
+
202
+ let messages = resp.messages ?? [];
203
+ if (limit && messages.length > limit) {
204
+ messages = messages.slice(0, limit);
205
+ }
206
+
207
+ return {
208
+ content: [
209
+ {
210
+ type: "text" as const,
211
+ text: JSON.stringify({
212
+ count: messages.length,
213
+ messages: messages.map((m) => ({
214
+ id: m.id,
215
+ from: m.sender_id,
216
+ subject: m.subject,
217
+ content: m.content,
218
+ threadTag: m.thread_tag,
219
+ importance: m.importance,
220
+ createdAt: m.created_at,
221
+ inReplyTo: m.in_reply_to,
222
+ })),
223
+ }),
224
+ },
225
+ ],
226
+ };
227
+ }
228
+ );
229
+
230
+ this.mcp.tool(
231
+ "read_thread",
232
+ "Read all messages in a thread (by thread_tag)",
233
+ {
234
+ threadTag: z.string().describe("Thread tag to read"),
235
+ scope: z
236
+ .string()
237
+ .optional()
238
+ .describe("Scope (defaults to 'default')"),
239
+ },
240
+ async ({ threadTag, scope }) => {
241
+ const resp = await this.sendIpc({
242
+ action: "read_thread",
243
+ threadTag,
244
+ scope: scope ?? this.defaultScope,
245
+ });
246
+
247
+ if (!resp.ok) {
248
+ return {
249
+ content: [
250
+ { type: "text" as const, text: JSON.stringify({ ok: false, error: resp.error }) },
251
+ ],
252
+ };
253
+ }
254
+
255
+ const messages = resp.messages ?? [];
256
+ return {
257
+ content: [
258
+ {
259
+ type: "text" as const,
260
+ text: JSON.stringify({
261
+ threadTag,
262
+ count: messages.length,
263
+ messages: messages.map((m) => ({
264
+ id: m.id,
265
+ from: m.sender_id,
266
+ content: m.content,
267
+ createdAt: m.created_at,
268
+ inReplyTo: m.in_reply_to,
269
+ })),
270
+ }),
271
+ },
272
+ ],
273
+ };
274
+ }
275
+ );
276
+
277
+ this.mcp.tool(
278
+ "list_agents",
279
+ "List agents registered in the inbox (local and optionally federated)",
280
+ {
281
+ scope: z.string().optional().describe("Filter by scope"),
282
+ includeFederated: z
283
+ .boolean()
284
+ .optional()
285
+ .describe("Include agents known from federation routing table"),
286
+ },
287
+ async ({ scope, includeFederated }) => {
288
+ const resp = await this.sendIpc({
289
+ action: "list_agents",
290
+ scope,
291
+ includeFederated,
292
+ });
293
+
294
+ if (!resp.ok) {
295
+ return {
296
+ content: [
297
+ { type: "text" as const, text: JSON.stringify({ ok: false, error: resp.error }) },
298
+ ],
299
+ };
300
+ }
301
+
302
+ return {
303
+ content: [
304
+ {
305
+ type: "text" as const,
306
+ text: JSON.stringify({
307
+ count: resp.count ?? resp.agents?.length ?? 0,
308
+ agents: resp.agents ?? [],
309
+ }),
310
+ },
311
+ ],
312
+ };
313
+ }
314
+ );
315
+ }
316
+
317
+ async start(): Promise<void> {
318
+ const transport = new StdioServerTransport();
319
+ await this.mcp.connect(transport);
320
+ }
321
+
322
+ /** Expose for testing */
323
+ get server(): McpServer {
324
+ return this.mcp;
325
+ }
326
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * DeliveryHandler bridge: connects agentic-mesh's MapServer message routing
3
+ * to agent-inbox's storage layer.
4
+ *
5
+ * When MapServer's MessageRouter resolves a message to a local agent,
6
+ * it calls `deliverToAgent()`. This bridge translates the MAP Message
7
+ * into an agent-inbox Message and stores it, triggering the full inbox
8
+ * pipeline (traceability, push notifications, etc.).
9
+ *
10
+ * For `forwardToPeer()` and `routeToFederation()`, we delegate back to
11
+ * the previous (default) handler so MeshPeer's own transport handles it.
12
+ */
13
+
14
+ import { EventEmitter } from "node:events";
15
+ import type { Storage } from "../storage/interface.js";
16
+ import { mapMessageToInbox } from "./type-mapper.js";
17
+ import type { MapMessage } from "./type-mapper.js";
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // DeliveryHandler interface (matches agentic-mesh v0.2.0)
21
+ // ---------------------------------------------------------------------------
22
+
23
+ /**
24
+ * Subset of agentic-mesh's DeliveryHandler interface.
25
+ * We define it here to avoid importing the full package at compile time.
26
+ */
27
+ export interface MeshDeliveryHandler {
28
+ deliverToAgent(agentId: string, message: MapMessage): Promise<boolean>;
29
+ forwardToPeer(
30
+ peerId: string,
31
+ agentIds: string[],
32
+ message: MapMessage
33
+ ): Promise<boolean>;
34
+ routeToFederation?(
35
+ systemId: string,
36
+ agentIds: string[],
37
+ message: MapMessage
38
+ ): Promise<boolean>;
39
+ }
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // DeliveryBridge
43
+ // ---------------------------------------------------------------------------
44
+
45
+ export class DeliveryBridge implements MeshDeliveryHandler {
46
+ constructor(
47
+ private storage: Storage,
48
+ private events: EventEmitter,
49
+ private scope: string = "default",
50
+ private previousHandler?: MeshDeliveryHandler
51
+ ) {}
52
+
53
+ /**
54
+ * Called by MapServer's MessageRouter when a message is resolved
55
+ * to a local agent. Translates and stores in agent-inbox.
56
+ */
57
+ async deliverToAgent(agentId: string, message: MapMessage): Promise<boolean> {
58
+ try {
59
+ const inboxMsg = mapMessageToInbox(message, this.scope);
60
+
61
+ // Ensure the target agent is in the recipients list
62
+ const hasAgent = inboxMsg.recipients.some(
63
+ (r) => r.agent_id === agentId
64
+ );
65
+ if (!hasAgent) {
66
+ inboxMsg.recipients.push({
67
+ agent_id: agentId,
68
+ kind: "to",
69
+ delivered_at: new Date().toISOString(),
70
+ });
71
+ }
72
+
73
+ this.storage.putMessage(inboxMsg);
74
+ this.events.emit("message.created", inboxMsg);
75
+ return true;
76
+ } catch {
77
+ return false;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Forward to a connected peer. Delegates to the previous handler
83
+ * (MeshPeer's default transport handles actual peer forwarding).
84
+ */
85
+ async forwardToPeer(
86
+ peerId: string,
87
+ agentIds: string[],
88
+ message: MapMessage
89
+ ): Promise<boolean> {
90
+ if (this.previousHandler) {
91
+ return this.previousHandler.forwardToPeer(peerId, agentIds, message);
92
+ }
93
+ return false;
94
+ }
95
+
96
+ /**
97
+ * Route to a federated system. Delegates to the previous handler
98
+ * (FederationGateway handles cross-mesh routing).
99
+ */
100
+ async routeToFederation(
101
+ systemId: string,
102
+ agentIds: string[],
103
+ message: MapMessage
104
+ ): Promise<boolean> {
105
+ if (this.previousHandler?.routeToFederation) {
106
+ return this.previousHandler.routeToFederation(systemId, agentIds, message);
107
+ }
108
+ return false;
109
+ }
110
+ }
@@ -0,0 +1,41 @@
1
+ import type { MapAgentConnectionClass, MapConnection } from "../map/map-client.js";
2
+ import type { MeshContextLike } from "./mesh-transport.js";
3
+ import { MeshTransport, DEFAULT_CHANNEL_NAME } from "./mesh-transport.js";
4
+
5
+ /**
6
+ * Factory for creating MeshTransport connections to remote peers.
7
+ *
8
+ * Implements MapAgentConnectionClass so it can be injected into
9
+ * ConnectionManager alongside (or instead of) the MAP SDK class.
10
+ *
11
+ * Usage:
12
+ * ```ts
13
+ * const connector = new MeshConnector(mesh, "my-peer-id");
14
+ * const conn = await connector.connect("remote-peer-id");
15
+ * ```
16
+ */
17
+ export class MeshConnector implements MapAgentConnectionClass {
18
+ constructor(
19
+ private mesh: MeshContextLike,
20
+ private localPeerId: string,
21
+ private channelName: string = DEFAULT_CHANNEL_NAME
22
+ ) {}
23
+
24
+ /**
25
+ * Create a MeshTransport connection to a remote peer.
26
+ *
27
+ * The `server` parameter is the remote peer ID (not a URL).
28
+ * The `opts` parameter is ignored — mesh peers don't need
29
+ * MAP handshake parameters.
30
+ */
31
+ async connect(server: string, _opts?: unknown): Promise<MapConnection> {
32
+ const transport = new MeshTransport(
33
+ this.mesh,
34
+ this.localPeerId,
35
+ server,
36
+ this.channelName
37
+ );
38
+ await transport.open();
39
+ return transport;
40
+ }
41
+ }
@@ -0,0 +1,157 @@
1
+ import type {
2
+ MapConnection,
3
+ IncomingMapMessage,
4
+ } from "../map/map-client.js";
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Minimal type stubs for agentic-mesh.
8
+ // We only reference the subset of MeshContext / IMessageChannel that
9
+ // MeshTransport actually uses, so agent-inbox compiles without importing
10
+ // the full agentic-mesh package (it's an optional peer dep).
11
+ // ---------------------------------------------------------------------------
12
+
13
+ /** Subset of agentic-mesh PeerInfo used by MeshTransport. */
14
+ export interface MeshPeerInfo {
15
+ id: string;
16
+ name?: string;
17
+ groups: string[];
18
+ }
19
+
20
+ /** Subset of agentic-mesh IMessageChannel<T>. */
21
+ export interface MeshChannel<T> {
22
+ readonly name: string;
23
+ readonly isOpen: boolean;
24
+ open(): Promise<void>;
25
+ close(): Promise<void>;
26
+ send(peerId: string, message: T): boolean;
27
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
28
+ on(event: string | symbol, listener: (...args: any[]) => void): this;
29
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
+ off(event: string | symbol, listener: (...args: any[]) => void): this;
31
+ }
32
+
33
+ /** Subset of agentic-mesh MeshContext used by MeshTransport. */
34
+ export interface MeshContextLike {
35
+ createChannel<T>(name: string): MeshChannel<T>;
36
+ _getPeerId(): string;
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Wire format
41
+ // ---------------------------------------------------------------------------
42
+
43
+ /**
44
+ * Payload exchanged over the mesh channel between agent-inbox instances.
45
+ * Flat structure that maps 1-to-1 with IncomingMapMessage.
46
+ */
47
+ export interface InboxWireMessage {
48
+ to?: { agentId?: string; scope?: string };
49
+ payload: unknown;
50
+ from: string;
51
+ timestamp?: string;
52
+ meta?: Record<string, unknown>;
53
+ }
54
+
55
+ // ---------------------------------------------------------------------------
56
+ // MeshTransport
57
+ // ---------------------------------------------------------------------------
58
+
59
+ /** Default channel name used for agent-inbox federation traffic.
60
+ * Uses the "proto:" prefix to distinguish protocol channels from
61
+ * application channels on the same mesh. */
62
+ export const DEFAULT_CHANNEL_NAME = "proto:agent-inbox";
63
+
64
+ /**
65
+ * Implements the MapConnection interface over an agentic-mesh MessageChannel.
66
+ *
67
+ * Each instance represents a connection to a single remote peer. Multiple
68
+ * MeshTransport instances share the same underlying channel (one channel
69
+ * per mesh, not per peer) and filter incoming messages by remotePeerId.
70
+ */
71
+ export class MeshTransport implements MapConnection {
72
+ private channel: MeshChannel<InboxWireMessage>;
73
+ private handlers: ((msg: IncomingMapMessage) => void)[] = [];
74
+ private messageListener: ((message: InboxWireMessage, from: MeshPeerInfo) => void) | null = null;
75
+ private closed = false;
76
+ readonly systemName: string;
77
+
78
+ constructor(
79
+ private mesh: MeshContextLike,
80
+ private localPeerId: string,
81
+ private remotePeerId: string,
82
+ channelName: string = DEFAULT_CHANNEL_NAME
83
+ ) {
84
+ this.systemName = remotePeerId;
85
+ this.channel = mesh.createChannel<InboxWireMessage>(channelName);
86
+ }
87
+
88
+ /**
89
+ * Open the underlying channel and start listening for messages.
90
+ * Must be called before send() or onMessage().
91
+ */
92
+ async open(): Promise<void> {
93
+ if (!this.channel.isOpen) {
94
+ await this.channel.open();
95
+ }
96
+ this.messageListener = (message: InboxWireMessage, from: MeshPeerInfo) => {
97
+ if (this.closed) return;
98
+ // Only process messages from our remote peer
99
+ if (from.id !== this.remotePeerId) return;
100
+ const incoming = wireToIncoming(message);
101
+ for (const handler of this.handlers) {
102
+ handler(incoming);
103
+ }
104
+ };
105
+ this.channel.on("message", this.messageListener);
106
+ }
107
+
108
+ async send(
109
+ to: { agentId?: string; scope?: string },
110
+ payload: unknown,
111
+ meta?: Record<string, unknown>
112
+ ): Promise<void> {
113
+ if (this.closed) {
114
+ throw new Error("MeshTransport is closed");
115
+ }
116
+ const wire: InboxWireMessage = {
117
+ to,
118
+ payload,
119
+ from: this.localPeerId,
120
+ timestamp: new Date().toISOString(),
121
+ meta,
122
+ };
123
+ const sent = this.channel.send(this.remotePeerId, wire);
124
+ if (!sent) {
125
+ throw new Error(
126
+ `Failed to send to peer "${this.remotePeerId}" — peer may be offline`
127
+ );
128
+ }
129
+ }
130
+
131
+ onMessage(handler: (msg: IncomingMapMessage) => void): void {
132
+ this.handlers.push(handler);
133
+ }
134
+
135
+ async disconnect(): Promise<void> {
136
+ this.closed = true;
137
+ if (this.messageListener) {
138
+ this.channel.off("message", this.messageListener);
139
+ this.messageListener = null;
140
+ }
141
+ this.handlers = [];
142
+ }
143
+ }
144
+
145
+ // ---------------------------------------------------------------------------
146
+ // Helpers
147
+ // ---------------------------------------------------------------------------
148
+
149
+ function wireToIncoming(wire: InboxWireMessage): IncomingMapMessage {
150
+ return {
151
+ from: wire.from,
152
+ to: wire.to,
153
+ payload: wire.payload,
154
+ timestamp: wire.timestamp,
155
+ meta: wire.meta,
156
+ };
157
+ }