agent-comms 1.0.14 → 1.2.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.
Files changed (68) hide show
  1. package/.claude-plugin/marketplace.json +21 -0
  2. package/.claude-plugin/plugin.json +21 -0
  3. package/README.md +83 -22
  4. package/dist/bridges/claude-code/channel.d.ts +1 -0
  5. package/dist/bridges/claude-code/channel.d.ts.map +1 -1
  6. package/dist/bridges/claude-code/channel.js +18 -37
  7. package/dist/bridges/claude-code/channel.js.map +1 -1
  8. package/dist/bridges/codex/tool.d.ts +2 -1
  9. package/dist/bridges/codex/tool.d.ts.map +1 -1
  10. package/dist/bridges/codex/tool.js +24 -19
  11. package/dist/bridges/codex/tool.js.map +1 -1
  12. package/dist/bridges/index.js +2 -2
  13. package/dist/bridges/index.js.map +1 -1
  14. package/dist/bridges/mcp/index.d.ts +11 -0
  15. package/dist/bridges/mcp/index.d.ts.map +1 -0
  16. package/dist/bridges/mcp/index.js +76 -0
  17. package/dist/bridges/mcp/index.js.map +1 -0
  18. package/dist/bridges/opencode/plugin.d.ts +2 -2
  19. package/dist/bridges/opencode/plugin.d.ts.map +1 -1
  20. package/dist/bridges/opencode/plugin.js +22 -20
  21. package/dist/bridges/opencode/plugin.js.map +1 -1
  22. package/dist/bridges/pi/index.d.ts +2 -2
  23. package/dist/bridges/pi/index.d.ts.map +1 -1
  24. package/dist/bridges/pi/index.js +12 -35
  25. package/dist/bridges/pi/index.js.map +1 -1
  26. package/dist/cli.js +4 -78
  27. package/dist/cli.js.map +1 -1
  28. package/dist/core/bridge.d.ts +7 -7
  29. package/dist/core/bridge.d.ts.map +1 -1
  30. package/dist/core/bridge.js +2 -2
  31. package/dist/core/bridge.js.map +1 -1
  32. package/dist/core/comms-store.d.ts +48 -0
  33. package/dist/core/comms-store.d.ts.map +1 -0
  34. package/dist/core/comms-store.js +11 -0
  35. package/dist/core/comms-store.js.map +1 -0
  36. package/dist/core/index.d.ts +6 -4
  37. package/dist/core/index.d.ts.map +1 -1
  38. package/dist/core/index.js +4 -3
  39. package/dist/core/index.js.map +1 -1
  40. package/dist/core/mesh-store.d.ts +88 -0
  41. package/dist/core/mesh-store.d.ts.map +1 -0
  42. package/dist/core/mesh-store.js +790 -0
  43. package/dist/core/mesh-store.js.map +1 -0
  44. package/dist/core/store.d.ts +3 -2
  45. package/dist/core/store.d.ts.map +1 -1
  46. package/dist/core/store.js +20 -20
  47. package/dist/core/store.js.map +1 -1
  48. package/dist/core/tool.d.ts +9 -9
  49. package/dist/core/tool.d.ts.map +1 -1
  50. package/dist/core/tool.js +5 -5
  51. package/dist/core/tool.js.map +1 -1
  52. package/dist/core/types.d.ts +2 -2
  53. package/dist/core/types.d.ts.map +1 -1
  54. package/dist/core/types.js +2 -2
  55. package/dist/core/types.js.map +1 -1
  56. package/dist/test/mesh-e2e.test.d.ts +8 -0
  57. package/dist/test/mesh-e2e.test.d.ts.map +1 -0
  58. package/dist/test/mesh-e2e.test.js +126 -0
  59. package/dist/test/mesh-e2e.test.js.map +1 -0
  60. package/dist/test/mesh-smoke.test.d.ts +9 -0
  61. package/dist/test/mesh-smoke.test.d.ts.map +1 -0
  62. package/dist/test/mesh-smoke.test.js +193 -0
  63. package/dist/test/mesh-smoke.test.js.map +1 -0
  64. package/package.json +17 -4
  65. package/dist/bridges/codex/stop_hook.d.ts +0 -13
  66. package/dist/bridges/codex/stop_hook.d.ts.map +0 -1
  67. package/dist/bridges/codex/stop_hook.js +0 -113
  68. package/dist/bridges/codex/stop_hook.js.map +0 -1
@@ -0,0 +1,790 @@
1
+ /**
2
+ * MeshStore — TCP localhost peer mesh for agent communication.
3
+ *
4
+ * Each bridge instance is a peer in the mesh. Peers discover each other
5
+ * via a coordinator (the first instance to bind the well-known port).
6
+ * All state is held in memory and synchronised between peers.
7
+ * Delivery events are pushed directly over TCP — no polling, no filesystem.
8
+ *
9
+ * Falls back to FileStore if the mesh is unavailable.
10
+ */
11
+ import * as net from "node:net";
12
+ import { nanoid } from "./nanoid.js";
13
+ import { CommsError } from "./store.js";
14
+ // ---------------------------------------------------------------------------
15
+ // Constants
16
+ // ---------------------------------------------------------------------------
17
+ const DEFAULT_COORDINATOR_PORT = 19876;
18
+ const COORDINATOR_HOST = "127.0.0.1";
19
+ // ---------------------------------------------------------------------------
20
+ // Framing
21
+ // ---------------------------------------------------------------------------
22
+ function encode(msg) {
23
+ return JSON.stringify(msg) + "\n";
24
+ }
25
+ class MessageBuffer {
26
+ buffer = "";
27
+ append(data) {
28
+ this.buffer += data;
29
+ const results = [];
30
+ let idx = this.buffer.indexOf("\n");
31
+ while (idx !== -1) {
32
+ const line = this.buffer.slice(0, idx);
33
+ this.buffer = this.buffer.slice(idx + 1);
34
+ if (line.length > 0) {
35
+ try {
36
+ results.push(JSON.parse(line));
37
+ }
38
+ catch {
39
+ /* skip malformed lines */
40
+ }
41
+ }
42
+ idx = this.buffer.indexOf("\n");
43
+ }
44
+ return results;
45
+ }
46
+ }
47
+ function isMeshMessage(value) {
48
+ if (typeof value !== "object" || value === null)
49
+ return false;
50
+ if (!("method" in value))
51
+ return false;
52
+ return typeof value.method === "string";
53
+ }
54
+ function dmKey(a, b) {
55
+ const sorted = [a, b].sort();
56
+ return `${sorted[0] ?? a}--${sorted[1] ?? b}`;
57
+ }
58
+ // ---------------------------------------------------------------------------
59
+ // Async socket write
60
+ // ---------------------------------------------------------------------------
61
+ function writeAsync(socket, data) {
62
+ return new Promise((resolve, reject) => {
63
+ socket.write(data, "utf-8", (err) => {
64
+ if (err)
65
+ reject(err);
66
+ else
67
+ resolve();
68
+ });
69
+ });
70
+ }
71
+ // ---------------------------------------------------------------------------
72
+ // MeshStore
73
+ // ---------------------------------------------------------------------------
74
+ export class MeshStore {
75
+ peerId;
76
+ startedAt;
77
+ coordinatorPort;
78
+ agents = new Map();
79
+ rooms = new Map();
80
+ messages = new Map();
81
+ dms = new Map();
82
+ deliveryQueues = new Map();
83
+ identityCache = new Map();
84
+ dataServer;
85
+ dataPort = 0;
86
+ coordinatorServer;
87
+ isCoordinator = false;
88
+ peerConnections = new Map();
89
+ peerInfo = new Map();
90
+ staleCheckTimer;
91
+ onDelivery;
92
+ constructor(coordinatorPort = DEFAULT_COORDINATOR_PORT) {
93
+ this.peerId = nanoid(8);
94
+ this.startedAt = new Date().toISOString();
95
+ this.coordinatorPort = coordinatorPort;
96
+ }
97
+ // -----------------------------------------------------------------------
98
+ // Mesh lifecycle
99
+ // -----------------------------------------------------------------------
100
+ async init() {
101
+ await this.startDataServer();
102
+ await this.tryJoinMesh();
103
+ }
104
+ startDataServer() {
105
+ return new Promise((resolve, reject) => {
106
+ this.dataServer = net.createServer((socket) => {
107
+ this.handleDataConnection(socket);
108
+ });
109
+ this.dataServer.listen(0, COORDINATOR_HOST, () => {
110
+ const addr = this.dataServer?.address();
111
+ if (addr && typeof addr === "object") {
112
+ this.dataPort = addr.port;
113
+ }
114
+ resolve();
115
+ });
116
+ this.dataServer.on("error", reject);
117
+ });
118
+ }
119
+ async tryJoinMesh() {
120
+ try {
121
+ await this.connectToCoordinator();
122
+ }
123
+ catch {
124
+ await this.becomeCoordinator();
125
+ }
126
+ }
127
+ connectToCoordinator() {
128
+ return new Promise((resolve, reject) => {
129
+ const socket = net.createConnection({ port: this.coordinatorPort, host: COORDINATOR_HOST }, () => {
130
+ const intro = {
131
+ method: "introduce",
132
+ peerId: this.peerId,
133
+ dataPort: this.dataPort,
134
+ };
135
+ socket.write(encode(intro));
136
+ // Read the peer_list response
137
+ const buf = new MessageBuffer();
138
+ socket.on("data", (data) => {
139
+ const items = buf.append(data.toString());
140
+ for (const item of items) {
141
+ if (isMeshMessage(item)) {
142
+ this.handleCoordinatorResponse(item);
143
+ }
144
+ }
145
+ });
146
+ socket.on("error", () => {
147
+ /* ignore late errors */
148
+ });
149
+ resolve();
150
+ });
151
+ const timer = setTimeout(() => {
152
+ socket.destroy();
153
+ reject(new Error("Coordinator connection timeout"));
154
+ }, 2000);
155
+ socket.on("error", (err) => {
156
+ clearTimeout(timer);
157
+ reject(err);
158
+ });
159
+ });
160
+ }
161
+ handleCoordinatorResponse(msg) {
162
+ if (msg.method === "peer_list") {
163
+ for (const peer of msg.peers) {
164
+ this.peerInfo.set(peer.id, peer);
165
+ void this.connectToPeerData(peer);
166
+ }
167
+ }
168
+ else if (msg.method === "peer_joined") {
169
+ this.peerInfo.set(msg.peer.id, msg.peer);
170
+ void this.connectToPeerData(msg.peer);
171
+ }
172
+ }
173
+ becomeCoordinator() {
174
+ return new Promise((resolve, reject) => {
175
+ this.coordinatorServer = net.createServer((socket) => {
176
+ this.handleCoordinatorConnection(socket);
177
+ });
178
+ this.coordinatorServer.listen(this.coordinatorPort, COORDINATOR_HOST, () => {
179
+ this.isCoordinator = true;
180
+ this.startStaleCheck();
181
+ this.peerInfo.set(this.peerId, {
182
+ id: this.peerId,
183
+ port: this.dataPort,
184
+ startedAt: this.startedAt,
185
+ });
186
+ resolve();
187
+ });
188
+ this.coordinatorServer.on("error", (err) => {
189
+ const isAddrInUse = err instanceof Error && "code" in err && err.code === "EADDRINUSE";
190
+ if (isAddrInUse) {
191
+ this.coordinatorServer = undefined;
192
+ void this.connectToCoordinator().then(resolve, reject);
193
+ }
194
+ else {
195
+ reject(err instanceof Error ? err : new Error(String(err)));
196
+ }
197
+ });
198
+ });
199
+ }
200
+ // -----------------------------------------------------------------------
201
+ // Coordinator protocol
202
+ // -----------------------------------------------------------------------
203
+ handleCoordinatorConnection(socket) {
204
+ const buffer = new MessageBuffer();
205
+ socket.on("data", (data) => {
206
+ const items = buffer.append(data.toString());
207
+ for (const item of items) {
208
+ if (isMeshMessage(item) && item.method === "introduce") {
209
+ void this.handleIntroduction(socket, item);
210
+ }
211
+ }
212
+ });
213
+ }
214
+ async handleIntroduction(socket, msg) {
215
+ const newPeer = {
216
+ id: msg.peerId,
217
+ port: msg.dataPort,
218
+ startedAt: new Date().toISOString(),
219
+ };
220
+ this.peerInfo.set(msg.peerId, newPeer);
221
+ // Send full peer list to the new peer
222
+ const peerList = {
223
+ method: "peer_list",
224
+ peers: [...this.peerInfo.values()],
225
+ };
226
+ await writeAsync(socket, encode(peerList));
227
+ // Broadcast arrival to all existing data connections
228
+ const joined = { method: "peer_joined", peer: newPeer };
229
+ await this.broadcastToDataConnections(joined);
230
+ // Connect to the new peer's data server
231
+ void this.connectToPeerData(newPeer);
232
+ }
233
+ // -----------------------------------------------------------------------
234
+ // Data connections
235
+ // -----------------------------------------------------------------------
236
+ handleDataConnection(socket) {
237
+ const buffer = new MessageBuffer();
238
+ let remotePeerId;
239
+ socket.on("data", (data) => {
240
+ const items = buffer.append(data.toString());
241
+ for (const item of items) {
242
+ if (isMeshMessage(item)) {
243
+ this.handleDataMessage(item);
244
+ if (item.method === "pong") {
245
+ remotePeerId = item.peerId;
246
+ }
247
+ }
248
+ }
249
+ if (remotePeerId && !this.peerConnections.has(remotePeerId)) {
250
+ this.peerConnections.set(remotePeerId, { socket, buffer });
251
+ }
252
+ });
253
+ socket.on("close", () => {
254
+ if (remotePeerId)
255
+ this.peerConnections.delete(remotePeerId);
256
+ });
257
+ socket.on("error", () => {
258
+ if (remotePeerId)
259
+ this.peerConnections.delete(remotePeerId);
260
+ });
261
+ }
262
+ handleDataMessage(msg) {
263
+ if (msg.method === "state_sync") {
264
+ // Merge — don't replace — so our own state isn't lost
265
+ const incoming = {
266
+ agents: new Map(Object.entries(msg.state.agents)),
267
+ rooms: new Map(Object.entries(msg.state.rooms)),
268
+ messages: new Map(Object.entries(msg.state.messages)),
269
+ dms: new Map(Object.entries(msg.state.dms)),
270
+ };
271
+ for (const [id, agent] of incoming.agents) {
272
+ if (!this.agents.has(id))
273
+ this.agents.set(id, agent);
274
+ }
275
+ for (const [id, room] of incoming.rooms) {
276
+ if (!this.rooms.has(id))
277
+ this.rooms.set(id, room);
278
+ }
279
+ for (const [id, msgs] of incoming.messages) {
280
+ if (!this.messages.has(id))
281
+ this.messages.set(id, msgs);
282
+ }
283
+ for (const [id, dmMsgs] of incoming.dms) {
284
+ if (!this.dms.has(id))
285
+ this.dms.set(id, dmMsgs);
286
+ }
287
+ }
288
+ else if (msg.method === "state_update") {
289
+ this.applyPatch(msg.patch);
290
+ }
291
+ }
292
+ applyPatch(patch) {
293
+ switch (patch.type) {
294
+ case "agent_upsert":
295
+ this.agents.set(patch.agent.id, patch.agent);
296
+ break;
297
+ case "agent_offline": {
298
+ const agent = this.agents.get(patch.agentId);
299
+ if (agent) {
300
+ agent.status = "offline";
301
+ this.agents.set(patch.agentId, agent);
302
+ }
303
+ break;
304
+ }
305
+ case "room_upsert":
306
+ this.rooms.set(patch.room.id, patch.room);
307
+ break;
308
+ case "room_delete":
309
+ this.rooms.delete(patch.roomId);
310
+ break;
311
+ case "message_add": {
312
+ const arr = this.messages.get(patch.roomId) ?? [];
313
+ arr.push(patch.message);
314
+ this.messages.set(patch.roomId, arr);
315
+ break;
316
+ }
317
+ case "dm_add": {
318
+ const arr = this.dms.get(patch.key) ?? [];
319
+ arr.push(patch.message);
320
+ this.dms.set(patch.key, arr);
321
+ break;
322
+ }
323
+ case "delivery": {
324
+ const arr = this.deliveryQueues.get(patch.agentId) ?? [];
325
+ arr.push(patch.event);
326
+ this.deliveryQueues.set(patch.agentId, arr);
327
+ if (patch.agentId === this.peerId && this.onDelivery) {
328
+ void this.onDelivery(patch.agentId, patch.event);
329
+ }
330
+ break;
331
+ }
332
+ }
333
+ }
334
+ async connectToPeerData(peer) {
335
+ if (peer.id === this.peerId)
336
+ return;
337
+ if (this.peerConnections.has(peer.id))
338
+ return;
339
+ return new Promise((resolve) => {
340
+ const socket = net.createConnection({ port: peer.port, host: COORDINATOR_HOST }, () => {
341
+ const buf = new MessageBuffer();
342
+ this.peerConnections.set(peer.id, { socket, buffer: buf });
343
+ // Identify ourselves
344
+ const pong = { method: "pong", peerId: this.peerId };
345
+ socket.write(encode(pong));
346
+ // If we have state and peer doesn't, send state sync
347
+ if (this.agents.size > 0) {
348
+ const state = {
349
+ agents: Object.fromEntries(this.agents),
350
+ rooms: Object.fromEntries(this.rooms),
351
+ messages: Object.fromEntries(this.messages),
352
+ dms: Object.fromEntries(this.dms),
353
+ };
354
+ socket.write(encode({ method: "state_sync", state }));
355
+ }
356
+ // Wire up ongoing message handling
357
+ socket.on("data", (data) => {
358
+ const items = buf.append(data.toString());
359
+ for (const item of items) {
360
+ if (isMeshMessage(item)) {
361
+ this.handleDataMessage(item);
362
+ }
363
+ }
364
+ });
365
+ resolve();
366
+ });
367
+ socket.on("close", () => this.peerConnections.delete(peer.id));
368
+ socket.on("error", () => {
369
+ this.peerConnections.delete(peer.id);
370
+ resolve();
371
+ });
372
+ });
373
+ }
374
+ // -----------------------------------------------------------------------
375
+ // Broadcast (async — writes to TCP sockets)
376
+ // -----------------------------------------------------------------------
377
+ async broadcastToDataConnections(msg) {
378
+ const data = encode(msg);
379
+ const writes = [];
380
+ for (const [, peer] of this.peerConnections) {
381
+ writes.push(writeAsync(peer.socket, data).catch(() => {
382
+ /* broken connection — cleanup handled by close/error listeners */
383
+ }));
384
+ }
385
+ await Promise.all(writes);
386
+ }
387
+ async broadcastPatch(patch) {
388
+ await this.broadcastToDataConnections({ method: "state_update", patch });
389
+ }
390
+ async deliverLocallyAndBroadcast(agentId, event) {
391
+ // Local delivery
392
+ const arr = this.deliveryQueues.get(agentId) ?? [];
393
+ arr.push(event);
394
+ this.deliveryQueues.set(agentId, arr);
395
+ if (agentId === this.peerId && this.onDelivery) {
396
+ void this.onDelivery(agentId, event);
397
+ }
398
+ // Remote delivery
399
+ const patch = { type: "delivery", agentId, event };
400
+ await this.broadcastPatch(patch);
401
+ }
402
+ // -----------------------------------------------------------------------
403
+ // CommsStore — Identity
404
+ // -----------------------------------------------------------------------
405
+ async readIdentity(harness, cwd) {
406
+ await Promise.resolve();
407
+ return this.identityCache.get(`${harness}--${cwd}`);
408
+ }
409
+ async writeIdentity(harness, cwd, id) {
410
+ await Promise.resolve();
411
+ this.identityCache.set(`${harness}--${cwd}`, { id });
412
+ }
413
+ // -----------------------------------------------------------------------
414
+ // CommsStore — Agent registry
415
+ // -----------------------------------------------------------------------
416
+ async registerAgent(opts) {
417
+ const existing = await this.readIdentity(opts.harness, opts.cwd);
418
+ if (existing) {
419
+ return this.updateAgent(existing.id, {
420
+ name: opts.name,
421
+ visibility: opts.visibility,
422
+ tags: opts.tags,
423
+ status: "active",
424
+ pid: opts.pid,
425
+ });
426
+ }
427
+ const id = this.peerId;
428
+ const agent = {
429
+ id,
430
+ name: opts.name,
431
+ harness: opts.harness,
432
+ pid: opts.pid,
433
+ startedAt: this.startedAt,
434
+ visibility: opts.visibility,
435
+ status: "active",
436
+ tags: opts.tags,
437
+ subscribedRooms: [],
438
+ };
439
+ this.agents.set(id, agent);
440
+ await this.writeIdentity(opts.harness, opts.cwd, id);
441
+ await this.broadcastPatch({ type: "agent_upsert", agent });
442
+ return agent;
443
+ }
444
+ async getAgent(id) {
445
+ await Promise.resolve();
446
+ return this.agents.get(id);
447
+ }
448
+ async updateAgent(id, patch) {
449
+ const agent = this.agents.get(id);
450
+ if (!agent)
451
+ throw new CommsError(`Agent ${id} not found`, "AGENT_NOT_FOUND");
452
+ Object.assign(agent, patch);
453
+ this.agents.set(id, agent);
454
+ await this.broadcastPatch({ type: "agent_upsert", agent });
455
+ return agent;
456
+ }
457
+ async listAgents(requesterId) {
458
+ await Promise.resolve();
459
+ const result = [];
460
+ for (const agent of this.agents.values()) {
461
+ if (agent.visibility === "ghost" && agent.id !== requesterId)
462
+ continue;
463
+ result.push(agent);
464
+ }
465
+ return result;
466
+ }
467
+ async setAgentOffline(id) {
468
+ const agent = this.agents.get(id);
469
+ if (agent) {
470
+ agent.status = "offline";
471
+ this.agents.set(id, agent);
472
+ await this.broadcastPatch({ type: "agent_offline", agentId: id });
473
+ }
474
+ if (this.isCoordinator) {
475
+ await this.handoverCoordinator();
476
+ }
477
+ }
478
+ // -----------------------------------------------------------------------
479
+ // CommsStore — Rooms
480
+ // -----------------------------------------------------------------------
481
+ async createRoom(opts) {
482
+ const id = opts.type === "secret" ? `_${opts.name}` : opts.name;
483
+ if (this.rooms.has(id))
484
+ throw new CommsError(`Room ${id} already exists`, "ROOM_EXISTS");
485
+ const room = {
486
+ id,
487
+ name: opts.name,
488
+ type: opts.type,
489
+ owner: opts.owner,
490
+ createdAt: new Date().toISOString(),
491
+ description: opts.description,
492
+ members: [opts.owner],
493
+ invited: [],
494
+ };
495
+ this.rooms.set(id, room);
496
+ this.messages.set(id, []);
497
+ await this.broadcastPatch({ type: "room_upsert", room });
498
+ return room;
499
+ }
500
+ async getRoom(id) {
501
+ await Promise.resolve();
502
+ return this.rooms.get(id);
503
+ }
504
+ async listRooms(requesterId) {
505
+ await Promise.resolve();
506
+ const result = [];
507
+ for (const room of this.rooms.values()) {
508
+ if (room.type === "secret" && !room.members.includes(requesterId))
509
+ continue;
510
+ result.push(room);
511
+ }
512
+ return result;
513
+ }
514
+ async joinRoom(roomId, agentId) {
515
+ const room = this.rooms.get(roomId);
516
+ if (!room)
517
+ throw new CommsError(`Room ${roomId} not found`, "ROOM_NOT_FOUND");
518
+ if (room.type === "public") {
519
+ if (!room.members.includes(agentId))
520
+ room.members.push(agentId);
521
+ }
522
+ else {
523
+ if (!room.invited.includes(agentId) &&
524
+ room.owner !== agentId &&
525
+ !room.members.includes(agentId)) {
526
+ throw new CommsError(`Not invited to room ${roomId}`, "NOT_INVITED");
527
+ }
528
+ room.invited = room.invited.filter((id) => id !== agentId);
529
+ if (!room.members.includes(agentId))
530
+ room.members.push(agentId);
531
+ }
532
+ this.rooms.set(roomId, room);
533
+ const agent = this.agents.get(agentId);
534
+ if (agent && !agent.subscribedRooms.includes(roomId)) {
535
+ agent.subscribedRooms.push(roomId);
536
+ this.agents.set(agentId, agent);
537
+ await this.broadcastPatch({ type: "agent_upsert", agent });
538
+ }
539
+ await this.broadcastPatch({ type: "room_upsert", room });
540
+ await this.deliverLocallyAndBroadcast(roomId, {
541
+ type: "member_joined",
542
+ room: roomId,
543
+ agent: agentId,
544
+ });
545
+ return room;
546
+ }
547
+ async leaveRoom(roomId, agentId) {
548
+ const room = this.rooms.get(roomId);
549
+ if (!room)
550
+ throw new CommsError(`Room ${roomId} not found`, "ROOM_NOT_FOUND");
551
+ room.members = room.members.filter((id) => id !== agentId);
552
+ this.rooms.set(roomId, room);
553
+ const agent = this.agents.get(agentId);
554
+ if (agent) {
555
+ agent.subscribedRooms = agent.subscribedRooms.filter((id) => id !== roomId);
556
+ this.agents.set(agentId, agent);
557
+ await this.broadcastPatch({ type: "agent_upsert", agent });
558
+ }
559
+ await this.broadcastPatch({ type: "room_upsert", room });
560
+ await this.deliverLocallyAndBroadcast(roomId, {
561
+ type: "member_left",
562
+ room: roomId,
563
+ agent: agentId,
564
+ });
565
+ if (room.members.length === 0 && room.owner === agentId) {
566
+ await this.destroyRoom(roomId, agentId);
567
+ }
568
+ }
569
+ async inviteToRoom(roomId, targetId, inviterId) {
570
+ const room = this.rooms.get(roomId);
571
+ if (!room)
572
+ throw new CommsError(`Room ${roomId} not found`, "ROOM_NOT_FOUND");
573
+ if (room.owner !== inviterId)
574
+ throw new CommsError("Only the room owner can invite", "NOT_OWNER");
575
+ if (!room.invited.includes(targetId) && !room.members.includes(targetId)) {
576
+ room.invited.push(targetId);
577
+ }
578
+ this.rooms.set(roomId, room);
579
+ await this.broadcastPatch({ type: "room_upsert", room });
580
+ await this.deliverLocallyAndBroadcast(targetId, {
581
+ type: "room_invite",
582
+ room: roomId,
583
+ from: inviterId,
584
+ });
585
+ }
586
+ async kickFromRoom(roomId, targetId, kickerId) {
587
+ const room = this.rooms.get(roomId);
588
+ if (!room)
589
+ throw new CommsError(`Room ${roomId} not found`, "ROOM_NOT_FOUND");
590
+ if (room.owner !== kickerId)
591
+ throw new CommsError("Only the room owner can kick", "NOT_OWNER");
592
+ room.members = room.members.filter((id) => id !== targetId);
593
+ room.invited = room.invited.filter((id) => id !== targetId);
594
+ this.rooms.set(roomId, room);
595
+ await this.broadcastPatch({ type: "room_upsert", room });
596
+ }
597
+ async destroyRoom(roomId, agentId) {
598
+ const room = this.rooms.get(roomId);
599
+ if (!room)
600
+ throw new CommsError(`Room ${roomId} not found`, "ROOM_NOT_FOUND");
601
+ if (room.owner !== agentId)
602
+ throw new CommsError("Only the room owner can destroy", "NOT_OWNER");
603
+ for (const memberId of room.members) {
604
+ const member = this.agents.get(memberId);
605
+ if (member) {
606
+ member.subscribedRooms = member.subscribedRooms.filter((id) => id !== roomId);
607
+ this.agents.set(memberId, member);
608
+ await this.broadcastPatch({ type: "agent_upsert", agent: member });
609
+ }
610
+ }
611
+ this.rooms.delete(roomId);
612
+ this.messages.delete(roomId);
613
+ await this.broadcastPatch({ type: "room_delete", roomId });
614
+ }
615
+ // -----------------------------------------------------------------------
616
+ // CommsStore — Messages
617
+ // -----------------------------------------------------------------------
618
+ async sendRoomMessage(roomId, from, content, replyTo) {
619
+ const room = this.rooms.get(roomId);
620
+ if (!room)
621
+ throw new CommsError(`Room ${roomId} not found`, "ROOM_NOT_FOUND");
622
+ if (!room.members.includes(from))
623
+ throw new CommsError(`Not a member of ${roomId}`, "NOT_MEMBER");
624
+ const id = `${String(Date.now())}-${nanoid(6)}`;
625
+ const message = {
626
+ id,
627
+ from,
628
+ room: roomId,
629
+ content,
630
+ timestamp: new Date().toISOString(),
631
+ replyTo,
632
+ };
633
+ const arr = this.messages.get(roomId) ?? [];
634
+ arr.push(message);
635
+ this.messages.set(roomId, arr);
636
+ await this.broadcastPatch({ type: "message_add", roomId, message });
637
+ for (const memberId of room.members) {
638
+ if (memberId !== from) {
639
+ await this.deliverLocallyAndBroadcast(memberId, {
640
+ type: "room_message",
641
+ message,
642
+ });
643
+ }
644
+ }
645
+ return message;
646
+ }
647
+ async readRoomMessages(roomId, since) {
648
+ await Promise.resolve();
649
+ const arr = this.messages.get(roomId) ?? [];
650
+ if (!since)
651
+ return [...arr];
652
+ return arr.filter((m) => m.timestamp > since);
653
+ }
654
+ // -----------------------------------------------------------------------
655
+ // CommsStore — DMs
656
+ // -----------------------------------------------------------------------
657
+ async sendDm(from, to, content) {
658
+ if (to !== from) {
659
+ const recipient = this.agents.get(to);
660
+ if (!recipient)
661
+ throw new CommsError(`Agent ${to} not found`, "AGENT_NOT_FOUND");
662
+ if (recipient.visibility === "ghost")
663
+ throw new CommsError(`Cannot DM agent ${to}`, "AGENT_NOT_FOUND");
664
+ }
665
+ const id = `${String(Date.now())}-${nanoid(6)}`;
666
+ const message = {
667
+ id,
668
+ from,
669
+ to,
670
+ content,
671
+ timestamp: new Date().toISOString(),
672
+ };
673
+ const key = dmKey(from, to);
674
+ const arr = this.dms.get(key) ?? [];
675
+ arr.push(message);
676
+ this.dms.set(key, arr);
677
+ await this.broadcastPatch({ type: "dm_add", key, message });
678
+ await this.deliverLocallyAndBroadcast(to, { type: "dm", message });
679
+ return message;
680
+ }
681
+ // -----------------------------------------------------------------------
682
+ // CommsStore — Delivery
683
+ // -----------------------------------------------------------------------
684
+ async deliver(agentId, event) {
685
+ await this.deliverLocallyAndBroadcast(agentId, event);
686
+ }
687
+ async drainDelivery(agentId) {
688
+ await Promise.resolve();
689
+ const events = this.deliveryQueues.get(agentId) ?? [];
690
+ this.deliveryQueues.set(agentId, []);
691
+ return events;
692
+ }
693
+ // -----------------------------------------------------------------------
694
+ // Stale agent cleanup (coordinator only)
695
+ // -----------------------------------------------------------------------
696
+ startStaleCheck() {
697
+ if (this.staleCheckTimer)
698
+ return;
699
+ this.staleCheckTimer = setInterval(() => {
700
+ void this.probeStaleAgents();
701
+ }, 5000);
702
+ }
703
+ stopStaleCheck() {
704
+ if (this.staleCheckTimer) {
705
+ clearInterval(this.staleCheckTimer);
706
+ this.staleCheckTimer = undefined;
707
+ }
708
+ }
709
+ async probeStaleAgents() {
710
+ const deadIds = [];
711
+ for (const [id, agent] of this.agents) {
712
+ if (agent.status !== "active")
713
+ continue;
714
+ if (!this.isProcessAlive(agent.pid)) {
715
+ deadIds.push(id);
716
+ }
717
+ }
718
+ for (const id of deadIds) {
719
+ const agent = this.agents.get(id);
720
+ if (agent) {
721
+ agent.status = "offline";
722
+ this.agents.set(id, agent);
723
+ }
724
+ await this.broadcastPatch({ type: "agent_offline", agentId: id });
725
+ }
726
+ }
727
+ isProcessAlive(pid) {
728
+ try {
729
+ // Sending signal 0 doesn't kill the process — it just checks existence
730
+ process.kill(pid, 0);
731
+ return true;
732
+ }
733
+ catch {
734
+ return false;
735
+ }
736
+ }
737
+ // -----------------------------------------------------------------------
738
+ // Coordinator handover
739
+ // -----------------------------------------------------------------------
740
+ async handoverCoordinator() {
741
+ if (!this.isCoordinator)
742
+ return;
743
+ let successor;
744
+ for (const [id, info] of this.peerInfo) {
745
+ if (id === this.peerId)
746
+ continue;
747
+ if (!successor || info.startedAt < successor.startedAt) {
748
+ successor = info;
749
+ }
750
+ }
751
+ if (!successor)
752
+ return;
753
+ const peer = this.peerConnections.get(successor.id);
754
+ if (peer) {
755
+ const msg = {
756
+ method: "become_coordinator",
757
+ peerList: [...this.peerInfo.values()].filter((p) => p.id !== this.peerId),
758
+ };
759
+ await writeAsync(peer.socket, encode(msg));
760
+ }
761
+ this.stopStaleCheck();
762
+ this.coordinatorServer?.close();
763
+ this.coordinatorServer = undefined;
764
+ this.isCoordinator = false;
765
+ }
766
+ // -----------------------------------------------------------------------
767
+ // Shutdown
768
+ // -----------------------------------------------------------------------
769
+ async shutdown() {
770
+ const agent = this.agents.get(this.peerId);
771
+ if (agent) {
772
+ agent.status = "offline";
773
+ await this.broadcastPatch({
774
+ type: "agent_offline",
775
+ agentId: this.peerId,
776
+ });
777
+ }
778
+ await this.handoverCoordinator();
779
+ for (const [, peer] of this.peerConnections) {
780
+ peer.socket.destroy();
781
+ }
782
+ this.peerConnections.clear();
783
+ this.stopStaleCheck();
784
+ this.dataServer?.close();
785
+ this.dataServer = undefined;
786
+ this.coordinatorServer?.close();
787
+ this.coordinatorServer = undefined;
788
+ }
789
+ }
790
+ //# sourceMappingURL=mesh-store.js.map