agent-comms 1.13.0 → 1.15.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/.claude-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/dist/bridges/claude-code/channel.d.ts.map +1 -1
- package/dist/bridges/claude-code/channel.js +6 -1
- package/dist/bridges/claude-code/channel.js.map +1 -1
- package/dist/bridges/codex/tool.d.ts.map +1 -1
- package/dist/bridges/codex/tool.js +6 -1
- package/dist/bridges/codex/tool.js.map +1 -1
- package/dist/bridges/mcp/index.d.ts.map +1 -1
- package/dist/bridges/mcp/index.js +6 -1
- package/dist/bridges/mcp/index.js.map +1 -1
- package/dist/bridges/opencode/plugin.d.ts.map +1 -1
- package/dist/bridges/opencode/plugin.js +5 -0
- package/dist/bridges/opencode/plugin.js.map +1 -1
- package/dist/bridges/pi/index.d.ts +5 -2
- package/dist/bridges/pi/index.d.ts.map +1 -1
- package/dist/bridges/pi/index.js +125 -20
- package/dist/bridges/pi/index.js.map +1 -1
- package/dist/bridges/user/controller.d.ts +10 -1
- package/dist/bridges/user/controller.d.ts.map +1 -1
- package/dist/bridges/user/controller.js +37 -1
- package/dist/bridges/user/controller.js.map +1 -1
- package/dist/bridges/user/tui.js +4 -0
- package/dist/bridges/user/tui.js.map +1 -1
- package/dist/bridges/user/web/build.d.ts +4 -4
- package/dist/bridges/user/web/build.js +53 -34
- package/dist/bridges/user/web/build.js.map +1 -1
- package/dist/bridges/user/web/frontend/components/App.d.ts +28 -0
- package/dist/bridges/user/web/frontend/components/App.d.ts.map +1 -0
- package/dist/bridges/user/web/frontend/components/App.js +7 -0
- package/dist/bridges/user/web/frontend/components/App.js.map +1 -0
- package/dist/bridges/user/web/frontend/components/ChatArea.d.ts +14 -0
- package/dist/bridges/user/web/frontend/components/ChatArea.d.ts.map +1 -0
- package/dist/bridges/user/web/frontend/components/ChatArea.js +38 -0
- package/dist/bridges/user/web/frontend/components/ChatArea.js.map +1 -0
- package/dist/bridges/user/web/frontend/components/CreateRoomForm.d.ts +11 -0
- package/dist/bridges/user/web/frontend/components/CreateRoomForm.d.ts.map +1 -0
- package/dist/bridges/user/web/frontend/components/CreateRoomForm.js +33 -0
- package/dist/bridges/user/web/frontend/components/CreateRoomForm.js.map +1 -0
- package/dist/bridges/user/web/frontend/components/JoinForm.d.ts +11 -0
- package/dist/bridges/user/web/frontend/components/JoinForm.d.ts.map +1 -0
- package/dist/bridges/user/web/frontend/components/JoinForm.js +28 -0
- package/dist/bridges/user/web/frontend/components/JoinForm.js.map +1 -0
- package/dist/bridges/user/web/frontend/components/Message.d.ts +8 -0
- package/dist/bridges/user/web/frontend/components/Message.d.ts.map +1 -0
- package/dist/bridges/user/web/frontend/components/Message.js +15 -0
- package/dist/bridges/user/web/frontend/components/Message.js.map +1 -0
- package/dist/bridges/user/web/frontend/components/MessageList.d.ts +8 -0
- package/dist/bridges/user/web/frontend/components/MessageList.d.ts.map +1 -0
- package/dist/bridges/user/web/frontend/components/MessageList.js +16 -0
- package/dist/bridges/user/web/frontend/components/MessageList.js.map +1 -0
- package/dist/bridges/user/web/frontend/components/ProjectTree.d.ts +17 -0
- package/dist/bridges/user/web/frontend/components/ProjectTree.d.ts.map +1 -0
- package/dist/bridges/user/web/frontend/components/ProjectTree.js +62 -0
- package/dist/bridges/user/web/frontend/components/ProjectTree.js.map +1 -0
- package/dist/bridges/user/web/frontend/components/RelayPanel.d.ts +15 -0
- package/dist/bridges/user/web/frontend/components/RelayPanel.d.ts.map +1 -0
- package/dist/bridges/user/web/frontend/components/RelayPanel.js +46 -0
- package/dist/bridges/user/web/frontend/components/RelayPanel.js.map +1 -0
- package/dist/bridges/user/web/frontend/components/Sidebar.d.ts +21 -0
- package/dist/bridges/user/web/frontend/components/Sidebar.d.ts.map +1 -0
- package/dist/bridges/user/web/frontend/components/Sidebar.js +37 -0
- package/dist/bridges/user/web/frontend/components/Sidebar.js.map +1 -0
- package/dist/bridges/user/web/frontend/dom.d.ts +12 -22
- package/dist/bridges/user/web/frontend/dom.d.ts.map +1 -1
- package/dist/bridges/user/web/frontend/dom.js +24 -49
- package/dist/bridges/user/web/frontend/dom.js.map +1 -1
- package/dist/bridges/user/web/frontend/input.d.ts +15 -0
- package/dist/bridges/user/web/frontend/input.d.ts.map +1 -1
- package/dist/bridges/user/web/frontend/input.js +39 -1
- package/dist/bridges/user/web/frontend/input.js.map +1 -1
- package/dist/bridges/user/web/frontend/main.d.ts +3 -3
- package/dist/bridges/user/web/frontend/main.d.ts.map +1 -1
- package/dist/bridges/user/web/frontend/main.js +135 -52
- package/dist/bridges/user/web/frontend/main.js.map +1 -1
- package/dist/bridges/user/web/frontend/mesh-client.d.ts +43 -0
- package/dist/bridges/user/web/frontend/mesh-client.d.ts.map +1 -0
- package/dist/bridges/user/web/frontend/mesh-client.js +132 -0
- package/dist/bridges/user/web/frontend/mesh-client.js.map +1 -0
- package/dist/bridges/user/web/frontend/mesh-worker.d.ts +14 -0
- package/dist/bridges/user/web/frontend/mesh-worker.d.ts.map +1 -0
- package/dist/bridges/user/web/frontend/mesh-worker.js +274 -0
- package/dist/bridges/user/web/frontend/mesh-worker.js.map +1 -0
- package/dist/bridges/user/web/frontend/messages.d.ts +17 -0
- package/dist/bridges/user/web/frontend/messages.d.ts.map +1 -0
- package/dist/bridges/user/web/frontend/messages.js +82 -0
- package/dist/bridges/user/web/frontend/messages.js.map +1 -0
- package/dist/bridges/user/web/frontend/project-tree.d.ts +9 -0
- package/dist/bridges/user/web/frontend/project-tree.d.ts.map +1 -0
- package/dist/bridges/user/web/frontend/project-tree.js +173 -0
- package/dist/bridges/user/web/frontend/project-tree.js.map +1 -0
- package/dist/bridges/user/web/frontend/relay-client.d.ts +40 -0
- package/dist/bridges/user/web/frontend/relay-client.d.ts.map +1 -0
- package/dist/bridges/user/web/frontend/relay-client.js +97 -0
- package/dist/bridges/user/web/frontend/relay-client.js.map +1 -0
- package/dist/bridges/user/web/frontend/relay-worker.d.ts +15 -0
- package/dist/bridges/user/web/frontend/relay-worker.d.ts.map +1 -0
- package/dist/bridges/user/web/frontend/relay-worker.js +366 -0
- package/dist/bridges/user/web/frontend/relay-worker.js.map +1 -0
- package/dist/bridges/user/web/frontend/state.d.ts +8 -1
- package/dist/bridges/user/web/frontend/state.d.ts.map +1 -1
- package/dist/bridges/user/web/frontend/state.js +17 -1
- package/dist/bridges/user/web/frontend/state.js.map +1 -1
- package/dist/bridges/user/web/frontend/sw.d.ts +16 -0
- package/dist/bridges/user/web/frontend/sw.d.ts.map +1 -0
- package/dist/bridges/user/web/frontend/sw.js +145 -0
- package/dist/bridges/user/web/frontend/sw.js.map +1 -0
- package/dist/bridges/user/web/frontend/types.d.ts +52 -0
- package/dist/bridges/user/web/frontend/types.d.ts.map +1 -1
- package/dist/bridges/user/web/frontend/url-sync.d.ts +48 -0
- package/dist/bridges/user/web/frontend/url-sync.d.ts.map +1 -0
- package/dist/bridges/user/web/frontend/url-sync.js +74 -0
- package/dist/bridges/user/web/frontend/url-sync.js.map +1 -0
- package/dist/bridges/user/web/server.d.ts +10 -3
- package/dist/bridges/user/web/server.d.ts.map +1 -1
- package/dist/bridges/user/web/server.js +280 -15
- package/dist/bridges/user/web/server.js.map +1 -1
- package/dist/bridges/user/web/test/web-server.integration.test.d.ts +7 -0
- package/dist/bridges/user/web/test/web-server.integration.test.d.ts.map +1 -0
- package/dist/bridges/user/web/test/web-server.integration.test.js +187 -0
- package/dist/bridges/user/web/test/web-server.integration.test.js.map +1 -0
- package/dist/core/bridge.d.ts +43 -0
- package/dist/core/bridge.d.ts.map +1 -1
- package/dist/core/bridge.js +197 -0
- package/dist/core/bridge.js.map +1 -1
- package/dist/core/comms-store.d.ts +22 -1
- package/dist/core/comms-store.d.ts.map +1 -1
- package/dist/core/discovery-mdns.d.ts +33 -0
- package/dist/core/discovery-mdns.d.ts.map +1 -0
- package/dist/core/discovery-mdns.js +182 -0
- package/dist/core/discovery-mdns.js.map +1 -0
- package/dist/core/discovery-tailscale.d.ts +40 -0
- package/dist/core/discovery-tailscale.d.ts.map +1 -0
- package/dist/core/discovery-tailscale.js +158 -0
- package/dist/core/discovery-tailscale.js.map +1 -0
- package/dist/core/discovery.d.ts +82 -0
- package/dist/core/discovery.d.ts.map +1 -0
- package/dist/core/discovery.js +194 -0
- package/dist/core/discovery.js.map +1 -0
- package/dist/core/federation.d.ts +100 -0
- package/dist/core/federation.d.ts.map +1 -0
- package/dist/core/federation.js +411 -0
- package/dist/core/federation.js.map +1 -0
- package/dist/core/identity.d.ts +31 -0
- package/dist/core/identity.d.ts.map +1 -0
- package/dist/core/identity.js +251 -0
- package/dist/core/identity.js.map +1 -0
- package/dist/core/index.d.ts +12 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +7 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/mesh-store.d.ts +75 -26
- package/dist/core/mesh-store.d.ts.map +1 -1
- package/dist/core/mesh-store.js +570 -355
- package/dist/core/mesh-store.js.map +1 -1
- package/dist/core/push-manager.d.ts +41 -0
- package/dist/core/push-manager.d.ts.map +1 -0
- package/dist/core/push-manager.js +60 -0
- package/dist/core/push-manager.js.map +1 -0
- package/dist/core/store.d.ts +21 -1
- package/dist/core/store.d.ts.map +1 -1
- package/dist/core/store.js +44 -0
- package/dist/core/store.js.map +1 -1
- package/dist/core/tcp-transport.d.ts +77 -0
- package/dist/core/tcp-transport.d.ts.map +1 -0
- package/dist/core/tcp-transport.js +596 -0
- package/dist/core/tcp-transport.js.map +1 -0
- package/dist/core/test/bridge.unit.test.d.ts +5 -0
- package/dist/core/test/bridge.unit.test.d.ts.map +1 -0
- package/dist/core/test/bridge.unit.test.js +120 -0
- package/dist/core/test/bridge.unit.test.js.map +1 -0
- package/dist/core/test/identity.unit.test.d.ts +5 -0
- package/dist/core/test/identity.unit.test.d.ts.map +1 -0
- package/dist/core/test/identity.unit.test.js +70 -0
- package/dist/core/test/identity.unit.test.js.map +1 -0
- package/dist/core/tls-transport.d.ts +69 -0
- package/dist/core/tls-transport.d.ts.map +1 -0
- package/dist/core/tls-transport.js +602 -0
- package/dist/core/tls-transport.js.map +1 -0
- package/dist/core/tool.d.ts +23 -2
- package/dist/core/tool.d.ts.map +1 -1
- package/dist/core/tool.js +269 -1
- package/dist/core/tool.js.map +1 -1
- package/dist/core/transport.d.ts +166 -0
- package/dist/core/transport.d.ts.map +1 -0
- package/dist/core/transport.js +20 -0
- package/dist/core/transport.js.map +1 -0
- package/dist/core/types.d.ts +146 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +77 -0
- package/dist/core/types.js.map +1 -1
- package/dist/core/vapid.d.ts +22 -0
- package/dist/core/vapid.d.ts.map +1 -0
- package/dist/core/vapid.js +50 -0
- package/dist/core/vapid.js.map +1 -0
- package/dist/core/web-push.d.ts +37 -0
- package/dist/core/web-push.d.ts.map +1 -0
- package/dist/core/web-push.js +198 -0
- package/dist/core/web-push.js.map +1 -0
- package/dist/core/wire-protocol.d.ts +131 -0
- package/dist/core/wire-protocol.d.ts.map +1 -0
- package/dist/core/wire-protocol.js +54 -0
- package/dist/core/wire-protocol.js.map +1 -0
- package/dist/core/ws-transport.d.ts +71 -0
- package/dist/core/ws-transport.d.ts.map +1 -0
- package/dist/core/ws-transport.js +560 -0
- package/dist/core/ws-transport.js.map +1 -0
- package/dist/test/approval.integration.test.d.ts +9 -0
- package/dist/test/approval.integration.test.d.ts.map +1 -0
- package/dist/test/approval.integration.test.js +361 -0
- package/dist/test/approval.integration.test.js.map +1 -0
- package/dist/test/delivery-receipt.helper.d.ts +16 -0
- package/dist/test/delivery-receipt.helper.d.ts.map +1 -0
- package/dist/test/delivery-receipt.helper.js +409 -0
- package/dist/test/delivery-receipt.helper.js.map +1 -0
- package/dist/test/delivery-receipt.runner.d.ts +13 -0
- package/dist/test/delivery-receipt.runner.d.ts.map +1 -0
- package/dist/test/delivery-receipt.runner.js +51 -0
- package/dist/test/delivery-receipt.runner.js.map +1 -0
- package/dist/test/federation.integration.test.d.ts +15 -0
- package/dist/test/federation.integration.test.d.ts.map +1 -0
- package/dist/test/federation.integration.test.js +218 -0
- package/dist/test/federation.integration.test.js.map +1 -0
- package/dist/test/listener-policy.integration.test.d.ts +9 -0
- package/dist/test/listener-policy.integration.test.d.ts.map +1 -0
- package/dist/test/listener-policy.integration.test.js +239 -0
- package/dist/test/listener-policy.integration.test.js.map +1 -0
- package/dist/test/tls-transport.integration.test.d.ts +8 -0
- package/dist/test/tls-transport.integration.test.d.ts.map +1 -0
- package/dist/test/tls-transport.integration.test.js +158 -0
- package/dist/test/tls-transport.integration.test.js.map +1 -0
- package/dist/test/visibility.integration.test.d.ts +12 -0
- package/dist/test/visibility.integration.test.d.ts.map +1 -0
- package/dist/test/visibility.integration.test.js +181 -0
- package/dist/test/visibility.integration.test.js.map +1 -0
- package/package.json +7 -3
- package/dist/bridges/user/web/frontend/render.d.ts +0 -30
- package/dist/bridges/user/web/frontend/render.d.ts.map +0 -1
- package/dist/bridges/user/web/frontend/render.js +0 -129
- package/dist/bridges/user/web/frontend/render.js.map +0 -1
- package/dist/bridges/user/web/frontend/test/dom.unit.test.d.ts +0 -5
- package/dist/bridges/user/web/frontend/test/dom.unit.test.d.ts.map +0 -1
- package/dist/bridges/user/web/frontend/test/dom.unit.test.js +0 -149
- package/dist/bridges/user/web/frontend/test/dom.unit.test.js.map +0 -1
- package/dist/bridges/user/web/frontend/test/input.unit.test.d.ts +0 -5
- package/dist/bridges/user/web/frontend/test/input.unit.test.d.ts.map +0 -1
- package/dist/bridges/user/web/frontend/test/input.unit.test.js +0 -131
- package/dist/bridges/user/web/frontend/test/input.unit.test.js.map +0 -1
- package/dist/bridges/user/web/frontend/test/render.unit.test.d.ts +0 -5
- package/dist/bridges/user/web/frontend/test/render.unit.test.d.ts.map +0 -1
- package/dist/bridges/user/web/frontend/test/render.unit.test.js +0 -436
- package/dist/bridges/user/web/frontend/test/render.unit.test.js.map +0 -1
- package/dist/bridges/user/web/frontend/test/state.unit.test.d.ts +0 -5
- package/dist/bridges/user/web/frontend/test/state.unit.test.d.ts.map +0 -1
- package/dist/bridges/user/web/frontend/test/state.unit.test.js +0 -119
- package/dist/bridges/user/web/frontend/test/state.unit.test.js.map +0 -1
package/dist/core/mesh-store.js
CHANGED
|
@@ -1,74 +1,30 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MeshStore —
|
|
2
|
+
* MeshStore — transport-agnostic peer mesh for agent communication.
|
|
3
3
|
*
|
|
4
4
|
* Each bridge instance is a peer in the mesh. Peers discover each other
|
|
5
5
|
* via a coordinator (the first instance to bind the well-known port).
|
|
6
6
|
* All state is held in memory and synchronised between peers.
|
|
7
|
-
* Delivery events are pushed directly over
|
|
7
|
+
* Delivery events are pushed directly over the transport — no polling,
|
|
8
|
+
* no filesystem.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
+
* Transport is injected via the constructor. TcpTransport for localhost
|
|
11
|
+
* TCP, TlsTransport for encrypted connections, etc.
|
|
10
12
|
*/
|
|
11
|
-
import * as
|
|
13
|
+
import * as os from "node:os";
|
|
12
14
|
import { nanoid } from "./nanoid.js";
|
|
13
15
|
import { CommsError } from "./store.js";
|
|
16
|
+
import { TcpTransport } from "./tcp-transport.js";
|
|
17
|
+
import { dmKey } from "./wire-protocol.js";
|
|
18
|
+
import { DiscoveryManager } from "./discovery.js";
|
|
19
|
+
import { MdnsDiscoveryBackend } from "./discovery-mdns.js";
|
|
20
|
+
import { TailscaleDiscoveryBackend } from "./discovery-tailscale.js";
|
|
21
|
+
import { FederationManager } from "./federation.js";
|
|
14
22
|
// ---------------------------------------------------------------------------
|
|
15
23
|
// Constants
|
|
16
24
|
// ---------------------------------------------------------------------------
|
|
17
25
|
const DEFAULT_COORDINATOR_PORT = 19876;
|
|
18
26
|
const COORDINATOR_HOST = "127.0.0.1";
|
|
19
27
|
// ---------------------------------------------------------------------------
|
|
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
28
|
// MeshStore
|
|
73
29
|
// ---------------------------------------------------------------------------
|
|
74
30
|
export class MeshStore {
|
|
@@ -81,157 +37,123 @@ export class MeshStore {
|
|
|
81
37
|
dms = new Map();
|
|
82
38
|
deliveryQueues = new Map();
|
|
83
39
|
identityCache = new Map();
|
|
84
|
-
|
|
85
|
-
dataPort = 0;
|
|
86
|
-
coordinatorServer;
|
|
87
|
-
isCoordinator = false;
|
|
88
|
-
peerConnections = new Map();
|
|
89
|
-
/** All sockets accepted by the data server — destroyed on shutdown. */
|
|
90
|
-
dataServerSockets = new Set();
|
|
91
|
-
/** All sockets accepted by the coordinator server — destroyed on shutdown. */
|
|
92
|
-
coordinatorServerSockets = new Set();
|
|
93
|
-
/** Socket connected to the coordinator (client side) — destroyed on shutdown. */
|
|
94
|
-
coordinatorSocket;
|
|
40
|
+
transport;
|
|
95
41
|
peerInfo = new Map();
|
|
96
42
|
staleCheckTimer;
|
|
43
|
+
isShutDown = false;
|
|
44
|
+
pendingMarkReadTimers = [];
|
|
45
|
+
// -- Pending inbound connections awaiting approval --
|
|
46
|
+
pendingInboundConnections = new Map();
|
|
97
47
|
onDelivery;
|
|
48
|
+
/** Fires for every state patch — both locally generated and remote. */
|
|
49
|
+
onPatch;
|
|
50
|
+
/** Serialise the full mesh state for state_sync messages. */
|
|
51
|
+
serialise() {
|
|
52
|
+
return {
|
|
53
|
+
agents: Object.fromEntries(this.agents),
|
|
54
|
+
rooms: Object.fromEntries(this.rooms),
|
|
55
|
+
messages: Object.fromEntries(this.messages),
|
|
56
|
+
dms: Object.fromEntries(this.dms),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
lastLocalDeliveryKey;
|
|
60
|
+
localDeliveryKeys = new Set();
|
|
61
|
+
discovery;
|
|
62
|
+
federation;
|
|
98
63
|
constructor(coordinatorPort = DEFAULT_COORDINATOR_PORT) {
|
|
99
64
|
this.peerId = nanoid(8);
|
|
100
65
|
this.startedAt = new Date().toISOString();
|
|
101
66
|
this.coordinatorPort = coordinatorPort;
|
|
67
|
+
// Wire up transport with this store's event handlers.
|
|
68
|
+
// TcpTransport is the default; callers can replace via setTransport().
|
|
69
|
+
this.transport = new TcpTransport(this.events);
|
|
70
|
+
// Discovery manager — registers available backends
|
|
71
|
+
this.discovery = new DiscoveryManager();
|
|
72
|
+
this.discovery.registerBackend(new MdnsDiscoveryBackend());
|
|
73
|
+
this.discovery.registerBackend(new TailscaleDiscoveryBackend());
|
|
74
|
+
// Federation manager — coordinator-to-coordinator links
|
|
75
|
+
this.federation = new FederationManager(this.peerId, // mesh ID is the coordinator's peer ID
|
|
76
|
+
`mesh-${this.peerId}`, {
|
|
77
|
+
onAgentVisible: (agent) => this.handleFedAgentVisible(agent),
|
|
78
|
+
onAgentGone: (agentId) => this.handleFedAgentGone(agentId),
|
|
79
|
+
onRoomMessage: (roomId, message) => this.handleFedRoomMessage(roomId, message),
|
|
80
|
+
onRoomJoin: (roomId, agentId, agentName) => this.handleFedRoomJoin(roomId, agentId, agentName),
|
|
81
|
+
onRoomLeave: (roomId, agentId) => this.handleFedRoomLeave(roomId, agentId),
|
|
82
|
+
getVisibleAgents: () => this.getVisibleAgentsForFed(),
|
|
83
|
+
getFederatedRoomMemberships: () => this.getFederatedRoomMembershipsForFed(),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/** Replace the transport (e.g. with TlsTransport for encrypted connections). */
|
|
87
|
+
setTransport(transport) {
|
|
88
|
+
this.transport = transport;
|
|
102
89
|
}
|
|
103
90
|
// -----------------------------------------------------------------------
|
|
104
91
|
// Mesh lifecycle
|
|
105
92
|
// -----------------------------------------------------------------------
|
|
106
93
|
async init() {
|
|
107
|
-
await this.startDataServer();
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
this.coordinatorServer?.unref();
|
|
114
|
-
this.coordinatorSocket?.unref();
|
|
115
|
-
}
|
|
116
|
-
startDataServer() {
|
|
117
|
-
return new Promise((resolve, reject) => {
|
|
118
|
-
this.dataServer = net.createServer((socket) => {
|
|
119
|
-
this.handleDataConnection(socket);
|
|
120
|
-
});
|
|
121
|
-
this.dataServer.listen(0, COORDINATOR_HOST, () => {
|
|
122
|
-
const addr = this.dataServer?.address();
|
|
123
|
-
if (addr && typeof addr === "object") {
|
|
124
|
-
this.dataPort = addr.port;
|
|
125
|
-
}
|
|
126
|
-
resolve();
|
|
127
|
-
});
|
|
128
|
-
this.dataServer.on("error", reject);
|
|
94
|
+
await this.transport.startDataServer();
|
|
95
|
+
// Register our own peer info
|
|
96
|
+
this.peerInfo.set(this.peerId, {
|
|
97
|
+
id: this.peerId,
|
|
98
|
+
port: this.transport.dataPort,
|
|
99
|
+
startedAt: this.startedAt,
|
|
129
100
|
});
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
101
|
+
// Try joining an existing mesh; fall back to becoming coordinator.
|
|
102
|
+
// If becomeCoordinator fails with EADDRINUSE (another process won the race),
|
|
103
|
+
// retry connecting — the new coordinator should be ready by now.
|
|
104
|
+
const MAX_RETRIES = 3;
|
|
105
|
+
const RETRY_DELAY_MS = 200;
|
|
106
|
+
let connected = false;
|
|
107
|
+
let lastError;
|
|
108
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
109
|
+
try {
|
|
110
|
+
await this.transport.connectToCoordinator(COORDINATOR_HOST, this.coordinatorPort, this.peerId, this.transport.dataPort);
|
|
111
|
+
connected = true;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
116
|
+
// Only try to become coordinator on the first attempt
|
|
117
|
+
if (attempt === 0) {
|
|
118
|
+
try {
|
|
119
|
+
await this.transport.becomeCoordinator(COORDINATOR_HOST, this.coordinatorPort);
|
|
120
|
+
this.startStaleCheck();
|
|
121
|
+
connected = true;
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
catch (coordErr) {
|
|
125
|
+
const msg = coordErr instanceof Error ? coordErr.message : String(coordErr);
|
|
126
|
+
if (!msg.includes("EADDRINUSE")) {
|
|
127
|
+
throw coordErr;
|
|
156
128
|
}
|
|
129
|
+
// EADDRINUSE — another process became coordinator. Retry connect.
|
|
157
130
|
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
resolve();
|
|
164
|
-
});
|
|
165
|
-
const timer = setTimeout(() => {
|
|
166
|
-
socket.destroy();
|
|
167
|
-
reject(new Error("Coordinator connection timeout"));
|
|
168
|
-
}, 2000);
|
|
169
|
-
socket.on("error", (err) => {
|
|
170
|
-
clearTimeout(timer);
|
|
171
|
-
socket.destroy();
|
|
172
|
-
reject(err);
|
|
173
|
-
});
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
handleCoordinatorResponse(msg) {
|
|
177
|
-
if (msg.method === "peer_list") {
|
|
178
|
-
for (const peer of msg.peers) {
|
|
179
|
-
this.peerInfo.set(peer.id, peer);
|
|
180
|
-
void this.connectToPeerData(peer);
|
|
131
|
+
}
|
|
132
|
+
// Wait before retrying
|
|
133
|
+
if (attempt < MAX_RETRIES - 1) {
|
|
134
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
|
|
135
|
+
}
|
|
181
136
|
}
|
|
182
137
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
void this.connectToPeerData(msg.peer);
|
|
138
|
+
if (!connected) {
|
|
139
|
+
throw new Error(`Failed to join or create mesh on port ${String(this.coordinatorPort)}: ${lastError?.message ?? "unknown error"}`);
|
|
186
140
|
}
|
|
187
|
-
|
|
188
|
-
becomeCoordinator() {
|
|
189
|
-
return new Promise((resolve, reject) => {
|
|
190
|
-
this.coordinatorServer = net.createServer((socket) => {
|
|
191
|
-
this.handleCoordinatorConnection(socket);
|
|
192
|
-
});
|
|
193
|
-
this.coordinatorServer.listen(this.coordinatorPort, COORDINATOR_HOST, () => {
|
|
194
|
-
this.isCoordinator = true;
|
|
195
|
-
this.startStaleCheck();
|
|
196
|
-
this.peerInfo.set(this.peerId, {
|
|
197
|
-
id: this.peerId,
|
|
198
|
-
port: this.dataPort,
|
|
199
|
-
startedAt: this.startedAt,
|
|
200
|
-
});
|
|
201
|
-
resolve();
|
|
202
|
-
});
|
|
203
|
-
this.coordinatorServer.on("error", (err) => {
|
|
204
|
-
const isAddrInUse = err instanceof Error && "code" in err && err.code === "EADDRINUSE";
|
|
205
|
-
if (isAddrInUse) {
|
|
206
|
-
this.coordinatorServer = undefined;
|
|
207
|
-
void this.connectToCoordinator().then(resolve, reject);
|
|
208
|
-
}
|
|
209
|
-
else {
|
|
210
|
-
reject(err instanceof Error ? err : new Error(String(err)));
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
});
|
|
141
|
+
this.transport.unref();
|
|
214
142
|
}
|
|
215
143
|
// -----------------------------------------------------------------------
|
|
216
|
-
//
|
|
144
|
+
// TransportEvents — callbacks from the transport layer
|
|
217
145
|
// -----------------------------------------------------------------------
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
for (const item of items) {
|
|
228
|
-
if (isMeshMessage(item) && item.method === "introduce") {
|
|
229
|
-
void this.handleIntroduction(socket, item);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
});
|
|
146
|
+
handlePeerList(peers) {
|
|
147
|
+
for (const peer of peers) {
|
|
148
|
+
this.peerInfo.set(peer.id, peer);
|
|
149
|
+
void this.transport.connectToPeer(peer, this.peerId);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
handlePeerJoined(peer) {
|
|
153
|
+
this.peerInfo.set(peer.id, peer);
|
|
154
|
+
void this.transport.connectToPeer(peer, this.peerId);
|
|
233
155
|
}
|
|
234
|
-
async handleIntroduction(
|
|
156
|
+
async handleIntroduction(handle, msg) {
|
|
235
157
|
const newPeer = {
|
|
236
158
|
id: msg.peerId,
|
|
237
159
|
port: msg.dataPort,
|
|
@@ -243,45 +165,29 @@ export class MeshStore {
|
|
|
243
165
|
method: "peer_list",
|
|
244
166
|
peers: [...this.peerInfo.values()],
|
|
245
167
|
};
|
|
246
|
-
await
|
|
247
|
-
// Broadcast arrival to all existing
|
|
168
|
+
await this.transport.send(handle, peerList);
|
|
169
|
+
// Broadcast arrival to all existing peers
|
|
248
170
|
const joined = { method: "peer_joined", peer: newPeer };
|
|
249
|
-
await this.
|
|
171
|
+
await this.transport.broadcast(joined);
|
|
250
172
|
// Connect to the new peer's data server
|
|
251
|
-
void this.
|
|
173
|
+
void this.transport.connectToPeer(newPeer, this.peerId);
|
|
252
174
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
remotePeerId = item.peerId;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
if (remotePeerId && !this.peerConnections.has(remotePeerId)) {
|
|
272
|
-
this.peerConnections.set(remotePeerId, { socket, buffer });
|
|
273
|
-
}
|
|
274
|
-
});
|
|
275
|
-
socket.on("close", () => {
|
|
276
|
-
if (remotePeerId)
|
|
277
|
-
this.peerConnections.delete(remotePeerId);
|
|
278
|
-
});
|
|
279
|
-
socket.on("error", () => {
|
|
280
|
-
if (remotePeerId)
|
|
281
|
-
this.peerConnections.delete(remotePeerId);
|
|
282
|
-
});
|
|
175
|
+
async handlePeerConnected(handle, _info) {
|
|
176
|
+
// If we have state and the peer doesn't, send state sync
|
|
177
|
+
if (this.agents.size > 0) {
|
|
178
|
+
const state = {
|
|
179
|
+
agents: Object.fromEntries(this.agents),
|
|
180
|
+
rooms: Object.fromEntries(this.rooms),
|
|
181
|
+
messages: Object.fromEntries(this.messages),
|
|
182
|
+
dms: Object.fromEntries(this.dms),
|
|
183
|
+
};
|
|
184
|
+
await this.transport.send(handle, {
|
|
185
|
+
method: "state_sync",
|
|
186
|
+
state,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
283
189
|
}
|
|
284
|
-
async handleDataMessage(msg) {
|
|
190
|
+
async handleDataMessage(handle, msg) {
|
|
285
191
|
if (msg.method === "state_sync") {
|
|
286
192
|
// Merge — don't replace — so our own state isn't lost
|
|
287
193
|
const incoming = {
|
|
@@ -311,11 +217,153 @@ export class MeshStore {
|
|
|
311
217
|
await this.applyPatch(msg.patch);
|
|
312
218
|
}
|
|
313
219
|
}
|
|
220
|
+
async handleBecomeCoordinator(peerList) {
|
|
221
|
+
// Take over as coordinator using the data server we already have
|
|
222
|
+
await this.transport.becomeCoordinator(COORDINATOR_HOST, this.coordinatorPort);
|
|
223
|
+
this.peerInfo.clear();
|
|
224
|
+
for (const peer of peerList) {
|
|
225
|
+
this.peerInfo.set(peer.id, peer);
|
|
226
|
+
void this.transport.connectToPeer(peer, this.peerId);
|
|
227
|
+
}
|
|
228
|
+
this.startStaleCheck();
|
|
229
|
+
}
|
|
230
|
+
handlePeerDisconnected(handle) {
|
|
231
|
+
this.peerInfo.delete(handle.id);
|
|
232
|
+
}
|
|
233
|
+
// -----------------------------------------------------------------------
|
|
234
|
+
// Connection approval
|
|
235
|
+
// -----------------------------------------------------------------------
|
|
236
|
+
handleConnectionRequest(handle, request) {
|
|
237
|
+
this.pendingInboundConnections.set(handle.id, {
|
|
238
|
+
peerId: request.peerId,
|
|
239
|
+
dataPort: request.dataPort,
|
|
240
|
+
name: request.name,
|
|
241
|
+
fingerprint: request.fingerprint,
|
|
242
|
+
});
|
|
243
|
+
// Deliver connection_request event to the owning agent
|
|
244
|
+
const connectionId = handle.id;
|
|
245
|
+
const event = {
|
|
246
|
+
type: "connection_request",
|
|
247
|
+
connectionId,
|
|
248
|
+
peerId: request.peerId,
|
|
249
|
+
dataPort: request.dataPort,
|
|
250
|
+
name: request.name,
|
|
251
|
+
fingerprint: request.fingerprint,
|
|
252
|
+
};
|
|
253
|
+
const arr = this.deliveryQueues.get(this.peerId) ?? [];
|
|
254
|
+
arr.push(event);
|
|
255
|
+
this.deliveryQueues.set(this.peerId, arr);
|
|
256
|
+
if (this.onDelivery) {
|
|
257
|
+
void this.onDelivery(this.peerId, event);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/** Accept a pending inbound connection. */
|
|
261
|
+
async acceptConnection(connectionId) {
|
|
262
|
+
const pending = this.pendingInboundConnections.get(connectionId);
|
|
263
|
+
if (!pending) {
|
|
264
|
+
throw new Error(`No pending connection ${connectionId}`);
|
|
265
|
+
}
|
|
266
|
+
this.pendingInboundConnections.delete(connectionId);
|
|
267
|
+
const handle = { id: connectionId };
|
|
268
|
+
await this.transport.acceptConnection(handle);
|
|
269
|
+
}
|
|
270
|
+
/** Reject a pending inbound connection. */
|
|
271
|
+
async rejectConnection(connectionId, reason) {
|
|
272
|
+
const pending = this.pendingInboundConnections.get(connectionId);
|
|
273
|
+
if (!pending) {
|
|
274
|
+
throw new Error(`No pending connection ${connectionId}`);
|
|
275
|
+
}
|
|
276
|
+
this.pendingInboundConnections.delete(connectionId);
|
|
277
|
+
const handle = { id: connectionId };
|
|
278
|
+
await this.transport.rejectConnection(handle, reason);
|
|
279
|
+
}
|
|
280
|
+
/** List all pending inbound connections awaiting approval. */
|
|
281
|
+
listPendingConnections() {
|
|
282
|
+
return [...this.pendingInboundConnections.entries()].map(([id, info]) => ({
|
|
283
|
+
connectionId: id,
|
|
284
|
+
...info,
|
|
285
|
+
}));
|
|
286
|
+
}
|
|
287
|
+
/** Initiate an outbound connection to a remote coordinator requiring approval.
|
|
288
|
+
* Fires the connect_request and returns immediately. The connection
|
|
289
|
+
* completes asynchronously when the coordinator accepts or rejects. */
|
|
290
|
+
async connectToRemote(host, port) {
|
|
291
|
+
const agent = this.agents.get(this.peerId);
|
|
292
|
+
// Fire-and-forget: don't await the full approval handshake.
|
|
293
|
+
// The coordinator will either accept (triggering normal introduction flow)
|
|
294
|
+
// or reject (closing the socket). Handle rejection to avoid unhandled rejection.
|
|
295
|
+
this.transport
|
|
296
|
+
.connectToRemote(host, port, this.peerId, this.transport.dataPort, agent?.name ?? "", "")
|
|
297
|
+
.catch((err) => {
|
|
298
|
+
// Rejection is expected when the coordinator denies the connection.
|
|
299
|
+
// Log silently — the calling tool already returned success.
|
|
300
|
+
void err;
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
/** Start only the data server without connecting to a coordinator.
|
|
304
|
+
* Used for testing scenarios where the peer connects via connectToRemote. */
|
|
305
|
+
async startDataServerOnly() {
|
|
306
|
+
await this.transport.startDataServer();
|
|
307
|
+
this.peerInfo.set(this.peerId, {
|
|
308
|
+
id: this.peerId,
|
|
309
|
+
port: this.transport.dataPort,
|
|
310
|
+
startedAt: this.startedAt,
|
|
311
|
+
});
|
|
312
|
+
this.transport.unref();
|
|
313
|
+
}
|
|
314
|
+
// -----------------------------------------------------------------------
|
|
315
|
+
// Transport events accessor (for bridges to wire up)
|
|
316
|
+
// -----------------------------------------------------------------------
|
|
317
|
+
/** Returns the TransportEvents object that bridges should pass to the transport constructor. */
|
|
318
|
+
get events() {
|
|
319
|
+
return {
|
|
320
|
+
onMessage: (handle, msg) => {
|
|
321
|
+
void this.handleDataMessage(handle, msg);
|
|
322
|
+
},
|
|
323
|
+
onPeerConnected: (handle, info) => {
|
|
324
|
+
void this.handlePeerConnected(handle, info);
|
|
325
|
+
},
|
|
326
|
+
onPeerDisconnected: (handle) => {
|
|
327
|
+
this.handlePeerDisconnected(handle);
|
|
328
|
+
},
|
|
329
|
+
onIntroduction: (handle, msg) => {
|
|
330
|
+
void this.handleIntroduction(handle, msg);
|
|
331
|
+
},
|
|
332
|
+
onPeerList: (peers) => {
|
|
333
|
+
this.handlePeerList(peers);
|
|
334
|
+
},
|
|
335
|
+
onPeerJoined: (peer) => {
|
|
336
|
+
this.handlePeerJoined(peer);
|
|
337
|
+
},
|
|
338
|
+
onBecomeCoordinator: (peerList) => {
|
|
339
|
+
void this.handleBecomeCoordinator(peerList);
|
|
340
|
+
},
|
|
341
|
+
onConnectionRequest: (handle, request) => {
|
|
342
|
+
this.handleConnectionRequest(handle, request);
|
|
343
|
+
},
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
// -----------------------------------------------------------------------
|
|
347
|
+
// State patch application
|
|
348
|
+
// -----------------------------------------------------------------------
|
|
314
349
|
async applyPatch(patch) {
|
|
315
350
|
switch (patch.type) {
|
|
316
|
-
case "agent_upsert":
|
|
317
|
-
|
|
351
|
+
case "agent_upsert": {
|
|
352
|
+
// Merge subscribedRooms to avoid losing local room memberships.
|
|
353
|
+
const existingAgent = this.agents.get(patch.agent.id);
|
|
354
|
+
if (existingAgent) {
|
|
355
|
+
const merged = patch.agent;
|
|
356
|
+
for (const r of existingAgent.subscribedRooms) {
|
|
357
|
+
if (!merged.subscribedRooms.includes(r))
|
|
358
|
+
merged.subscribedRooms.push(r);
|
|
359
|
+
}
|
|
360
|
+
this.agents.set(merged.id, merged);
|
|
361
|
+
}
|
|
362
|
+
else {
|
|
363
|
+
this.agents.set(patch.agent.id, patch.agent);
|
|
364
|
+
}
|
|
318
365
|
break;
|
|
366
|
+
}
|
|
319
367
|
case "agent_offline": {
|
|
320
368
|
const agent = this.agents.get(patch.agentId);
|
|
321
369
|
if (agent) {
|
|
@@ -324,9 +372,23 @@ export class MeshStore {
|
|
|
324
372
|
}
|
|
325
373
|
break;
|
|
326
374
|
}
|
|
327
|
-
case "room_upsert":
|
|
328
|
-
|
|
375
|
+
case "room_upsert": {
|
|
376
|
+
// Merge members rather than overwriting — last-write-wins can lose
|
|
377
|
+
// members added locally when a remote patch arrives with a stale list.
|
|
378
|
+
const existing = this.rooms.get(patch.room.id);
|
|
379
|
+
if (existing) {
|
|
380
|
+
const merged = patch.room;
|
|
381
|
+
for (const m of existing.members) {
|
|
382
|
+
if (!merged.members.includes(m))
|
|
383
|
+
merged.members.push(m);
|
|
384
|
+
}
|
|
385
|
+
this.rooms.set(merged.id, merged);
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
this.rooms.set(patch.room.id, patch.room);
|
|
389
|
+
}
|
|
329
390
|
break;
|
|
391
|
+
}
|
|
330
392
|
case "room_delete":
|
|
331
393
|
this.rooms.delete(patch.roomId);
|
|
332
394
|
break;
|
|
@@ -347,14 +409,35 @@ export class MeshStore {
|
|
|
347
409
|
arr.push(patch.event);
|
|
348
410
|
this.deliveryQueues.set(patch.agentId, arr);
|
|
349
411
|
if (patch.agentId === this.peerId && this.onDelivery) {
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
if (
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
412
|
+
// Deduplicate against local deliveries
|
|
413
|
+
const eventKey = JSON.stringify(patch.event);
|
|
414
|
+
if (this.localDeliveryKeys.has(eventKey))
|
|
415
|
+
break;
|
|
416
|
+
this.localDeliveryKeys.add(eventKey);
|
|
417
|
+
if (this.localDeliveryKeys.size > 50) {
|
|
418
|
+
const oldest = this.localDeliveryKeys.values().next().value;
|
|
419
|
+
if (oldest !== undefined)
|
|
420
|
+
this.localDeliveryKeys.delete(oldest);
|
|
357
421
|
}
|
|
422
|
+
void this.onDelivery(patch.agentId, patch.event);
|
|
423
|
+
// Auto-mark read — scheduled as a macrotask to yield to the event
|
|
424
|
+
// loop. Without this yield, the delivery → markRead → broadcast
|
|
425
|
+
// → peer receives → handleDataMessage chain monopolises the
|
|
426
|
+
// microtask queue and starves macrotasks (timers, new connections,
|
|
427
|
+
// sendRoomMessage return values).
|
|
428
|
+
const evt = patch.event;
|
|
429
|
+
const timer = setTimeout(() => {
|
|
430
|
+
if (this.isShutDown)
|
|
431
|
+
return;
|
|
432
|
+
if (evt.type === "room_message") {
|
|
433
|
+
void this.markRead(evt.message.id, this.peerId, evt.message.room);
|
|
434
|
+
}
|
|
435
|
+
else if (evt.type === "dm") {
|
|
436
|
+
void this.markRead(evt.message.id, this.peerId);
|
|
437
|
+
}
|
|
438
|
+
}, 0);
|
|
439
|
+
if (!this.isShutDown)
|
|
440
|
+
this.pendingMarkReadTimers.push(timer);
|
|
358
441
|
}
|
|
359
442
|
break;
|
|
360
443
|
}
|
|
@@ -363,63 +446,18 @@ export class MeshStore {
|
|
|
363
446
|
break;
|
|
364
447
|
}
|
|
365
448
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
return;
|
|
370
|
-
if (this.peerConnections.has(peer.id))
|
|
371
|
-
return;
|
|
372
|
-
return new Promise((resolve) => {
|
|
373
|
-
const socket = net.createConnection({ port: peer.port, host: COORDINATOR_HOST }, () => {
|
|
374
|
-
const buf = new MessageBuffer();
|
|
375
|
-
this.peerConnections.set(peer.id, { socket, buffer: buf });
|
|
376
|
-
// Identify ourselves
|
|
377
|
-
const pong = { method: "pong", peerId: this.peerId };
|
|
378
|
-
socket.write(encode(pong));
|
|
379
|
-
// If we have state and peer doesn't, send state sync
|
|
380
|
-
if (this.agents.size > 0) {
|
|
381
|
-
const state = {
|
|
382
|
-
agents: Object.fromEntries(this.agents),
|
|
383
|
-
rooms: Object.fromEntries(this.rooms),
|
|
384
|
-
messages: Object.fromEntries(this.messages),
|
|
385
|
-
dms: Object.fromEntries(this.dms),
|
|
386
|
-
};
|
|
387
|
-
socket.write(encode({ method: "state_sync", state }));
|
|
388
|
-
}
|
|
389
|
-
// Wire up ongoing message handling
|
|
390
|
-
socket.on("data", (data) => {
|
|
391
|
-
const items = buf.append(data.toString());
|
|
392
|
-
for (const item of items) {
|
|
393
|
-
if (isMeshMessage(item)) {
|
|
394
|
-
void this.handleDataMessage(item);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
});
|
|
398
|
-
resolve();
|
|
399
|
-
});
|
|
400
|
-
socket.on("close", () => this.peerConnections.delete(peer.id));
|
|
401
|
-
socket.on("error", () => {
|
|
402
|
-
this.peerConnections.delete(peer.id);
|
|
403
|
-
socket.destroy();
|
|
404
|
-
resolve();
|
|
405
|
-
});
|
|
406
|
-
});
|
|
449
|
+
if (this.onPatch) {
|
|
450
|
+
await this.onPatch(patch);
|
|
451
|
+
}
|
|
407
452
|
}
|
|
408
453
|
// -----------------------------------------------------------------------
|
|
409
|
-
// Broadcast
|
|
454
|
+
// Broadcast
|
|
410
455
|
// -----------------------------------------------------------------------
|
|
411
|
-
async broadcastToDataConnections(msg) {
|
|
412
|
-
const data = encode(msg);
|
|
413
|
-
const writes = [];
|
|
414
|
-
for (const [, peer] of this.peerConnections) {
|
|
415
|
-
writes.push(writeAsync(peer.socket, data).catch(() => {
|
|
416
|
-
/* broken connection — cleanup handled by close/error listeners */
|
|
417
|
-
}));
|
|
418
|
-
}
|
|
419
|
-
await Promise.all(writes);
|
|
420
|
-
}
|
|
421
456
|
async broadcastPatch(patch) {
|
|
422
|
-
await this.
|
|
457
|
+
await this.transport.broadcast({ method: "state_update", patch });
|
|
458
|
+
if (this.onPatch) {
|
|
459
|
+
await this.onPatch(patch);
|
|
460
|
+
}
|
|
423
461
|
}
|
|
424
462
|
async deliverLocallyAndBroadcast(agentId, event) {
|
|
425
463
|
// Local delivery
|
|
@@ -434,14 +472,34 @@ export class MeshStore {
|
|
|
434
472
|
await this.emitDeliveryStatus(event.message.id, agentId, "delivered");
|
|
435
473
|
}
|
|
436
474
|
if (agentId === this.peerId && this.onDelivery) {
|
|
437
|
-
|
|
438
|
-
//
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
475
|
+
// Deduplicate: skip if this exact event was already delivered locally.
|
|
476
|
+
// The mesh can echo delivery patches through multiple peer paths,
|
|
477
|
+
// causing applyPatch to fire onDelivery for the same event.
|
|
478
|
+
const eventKey = JSON.stringify(event);
|
|
479
|
+
if (this.localDeliveryKeys.has(eventKey))
|
|
480
|
+
return;
|
|
481
|
+
this.localDeliveryKeys.add(eventKey);
|
|
482
|
+
// Prevent unbounded growth — evict oldest when cap reached
|
|
483
|
+
if (this.localDeliveryKeys.size > 50) {
|
|
484
|
+
const oldest = this.localDeliveryKeys.values().next().value;
|
|
485
|
+
if (oldest !== undefined)
|
|
486
|
+
this.localDeliveryKeys.delete(oldest);
|
|
444
487
|
}
|
|
488
|
+
void this.onDelivery(agentId, event);
|
|
489
|
+
// Auto-mark read — scheduled as a macrotask to yield to the event
|
|
490
|
+
// loop (see matching comment in applyPatch for rationale).
|
|
491
|
+
const timer = setTimeout(() => {
|
|
492
|
+
if (this.isShutDown)
|
|
493
|
+
return;
|
|
494
|
+
if (event.type === "room_message") {
|
|
495
|
+
void this.markRead(event.message.id, agentId, event.message.room);
|
|
496
|
+
}
|
|
497
|
+
else if (event.type === "dm") {
|
|
498
|
+
void this.markRead(event.message.id, agentId);
|
|
499
|
+
}
|
|
500
|
+
}, 0);
|
|
501
|
+
if (!this.isShutDown)
|
|
502
|
+
this.pendingMarkReadTimers.push(timer);
|
|
445
503
|
}
|
|
446
504
|
// Remote delivery
|
|
447
505
|
const patch = { type: "delivery", agentId, event };
|
|
@@ -470,6 +528,22 @@ export class MeshStore {
|
|
|
470
528
|
});
|
|
471
529
|
}
|
|
472
530
|
}
|
|
531
|
+
async notifyRoomsOfNameChange(agentId, oldName, newName) {
|
|
532
|
+
const agent = this.agents.get(agentId);
|
|
533
|
+
if (!agent)
|
|
534
|
+
return;
|
|
535
|
+
const event = {
|
|
536
|
+
type: "name_changed",
|
|
537
|
+
agent: agentId,
|
|
538
|
+
oldName,
|
|
539
|
+
newName,
|
|
540
|
+
};
|
|
541
|
+
for (const roomId of agent.subscribedRooms) {
|
|
542
|
+
await this.deliverToRoom(roomId, event, agentId);
|
|
543
|
+
}
|
|
544
|
+
// Also deliver to the agent itself so it sees confirmation
|
|
545
|
+
await this.deliverLocallyAndBroadcast(agentId, event);
|
|
546
|
+
}
|
|
473
547
|
async emitDeliveryStatus(messageId, agentId, status, room) {
|
|
474
548
|
// Find the sender for this message
|
|
475
549
|
const senderId = this.findMessageSender(messageId, room);
|
|
@@ -588,6 +662,10 @@ export class MeshStore {
|
|
|
588
662
|
this.agents.set(id, agent);
|
|
589
663
|
await this.writeIdentity(opts.harness, opts.cwd, id);
|
|
590
664
|
await this.broadcastPatch({ type: "agent_upsert", agent });
|
|
665
|
+
// Broadcast presence to federated links
|
|
666
|
+
if (agent.visibility === "visible") {
|
|
667
|
+
await this.federation.broadcastAgentVisible(agent);
|
|
668
|
+
}
|
|
591
669
|
return agent;
|
|
592
670
|
}
|
|
593
671
|
async getAgent(id) {
|
|
@@ -599,9 +677,13 @@ export class MeshStore {
|
|
|
599
677
|
if (!agent)
|
|
600
678
|
throw new CommsError(`Agent ${id} not found`, "AGENT_NOT_FOUND");
|
|
601
679
|
const oldStatus = agent.status;
|
|
680
|
+
const oldName = agent.name;
|
|
602
681
|
Object.assign(agent, patch);
|
|
603
682
|
this.agents.set(id, agent);
|
|
604
683
|
await this.broadcastPatch({ type: "agent_upsert", agent });
|
|
684
|
+
if (patch.name !== undefined && patch.name !== oldName) {
|
|
685
|
+
await this.notifyRoomsOfNameChange(id, oldName, patch.name);
|
|
686
|
+
}
|
|
605
687
|
if (patch.status && patch.status !== oldStatus) {
|
|
606
688
|
await this.notifyRoomsOfStatus(id, patch.status);
|
|
607
689
|
}
|
|
@@ -619,14 +701,19 @@ export class MeshStore {
|
|
|
619
701
|
}
|
|
620
702
|
async setAgentOffline(id) {
|
|
621
703
|
const agent = this.agents.get(id);
|
|
622
|
-
if (agent)
|
|
623
|
-
|
|
624
|
-
|
|
704
|
+
if (!agent)
|
|
705
|
+
return;
|
|
706
|
+
if (agent.status === "offline")
|
|
707
|
+
return;
|
|
708
|
+
// Only the owning store should broadcast the status change.
|
|
709
|
+
// Other stores learn about it via the agent_offline mesh patch.
|
|
710
|
+
const isOwner = id === this.peerId;
|
|
711
|
+
agent.status = "offline";
|
|
712
|
+
this.agents.set(id, agent);
|
|
713
|
+
if (isOwner) {
|
|
625
714
|
await this.notifyRoomsOfStatus(id, "offline");
|
|
626
715
|
await this.broadcastPatch({ type: "agent_offline", agentId: id });
|
|
627
|
-
|
|
628
|
-
if (this.isCoordinator) {
|
|
629
|
-
await this.handoverCoordinator();
|
|
716
|
+
await this.federation.broadcastAgentGone(id);
|
|
630
717
|
}
|
|
631
718
|
}
|
|
632
719
|
// -----------------------------------------------------------------------
|
|
@@ -645,6 +732,7 @@ export class MeshStore {
|
|
|
645
732
|
description: opts.description,
|
|
646
733
|
members: [opts.owner],
|
|
647
734
|
invited: [],
|
|
735
|
+
federated: opts.federated ?? false,
|
|
648
736
|
};
|
|
649
737
|
this.rooms.set(id, room);
|
|
650
738
|
this.messages.set(id, []);
|
|
@@ -714,6 +802,11 @@ export class MeshStore {
|
|
|
714
802
|
room: roomId,
|
|
715
803
|
agent: agentId,
|
|
716
804
|
}, agentId);
|
|
805
|
+
// Notify federated links if the room is federated
|
|
806
|
+
if (room.federated) {
|
|
807
|
+
const agentName = agent?.name ?? agentId;
|
|
808
|
+
await this.federation.broadcastRoomJoin(roomId, agentId, agentName);
|
|
809
|
+
}
|
|
717
810
|
return room;
|
|
718
811
|
}
|
|
719
812
|
async leaveRoom(roomId, agentId) {
|
|
@@ -734,6 +827,10 @@ export class MeshStore {
|
|
|
734
827
|
room: roomId,
|
|
735
828
|
agent: agentId,
|
|
736
829
|
});
|
|
830
|
+
// Notify federated links if the room is federated
|
|
831
|
+
if (room.federated) {
|
|
832
|
+
await this.federation.broadcastRoomLeave(roomId, agentId);
|
|
833
|
+
}
|
|
737
834
|
if (room.members.length === 0 && room.owner === agentId) {
|
|
738
835
|
await this.destroyRoom(roomId, agentId);
|
|
739
836
|
}
|
|
@@ -829,6 +926,10 @@ export class MeshStore {
|
|
|
829
926
|
arr.push(message);
|
|
830
927
|
this.messages.set(roomId, arr);
|
|
831
928
|
await this.broadcastPatch({ type: "message_add", roomId, message });
|
|
929
|
+
// Forward to federated links if the room is federated
|
|
930
|
+
if (room.federated) {
|
|
931
|
+
await this.federation.forwardRoomMessage(roomId, message);
|
|
932
|
+
}
|
|
832
933
|
for (const memberId of room.members) {
|
|
833
934
|
if (memberId !== from) {
|
|
834
935
|
await this.deliverLocallyAndBroadcast(memberId, {
|
|
@@ -928,11 +1029,27 @@ export class MeshStore {
|
|
|
928
1029
|
await this.notifyRoomsOfStatus(id, "offline");
|
|
929
1030
|
}
|
|
930
1031
|
await this.broadcastPatch({ type: "agent_offline", agentId: id });
|
|
1032
|
+
this.peerInfo.delete(id);
|
|
1033
|
+
}
|
|
1034
|
+
// Also purge long-offline agents to prevent indefinite accumulation
|
|
1035
|
+
const offlineThreshold = Date.now() - 30 * 60 * 1000; // 30 minutes
|
|
1036
|
+
const purgeIds = [];
|
|
1037
|
+
for (const [id, agent] of this.agents) {
|
|
1038
|
+
if (agent.status !== "offline")
|
|
1039
|
+
continue;
|
|
1040
|
+
const startedAt = new Date(agent.startedAt).getTime();
|
|
1041
|
+
if (startedAt < offlineThreshold) {
|
|
1042
|
+
purgeIds.push(id);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
for (const id of purgeIds) {
|
|
1046
|
+
this.agents.delete(id);
|
|
1047
|
+
this.peerInfo.delete(id);
|
|
1048
|
+
this.identityCache.delete(id);
|
|
931
1049
|
}
|
|
932
1050
|
}
|
|
933
1051
|
isProcessAlive(pid) {
|
|
934
1052
|
try {
|
|
935
|
-
// Sending signal 0 doesn't kill the process — it just checks existence
|
|
936
1053
|
process.kill(pid, 0);
|
|
937
1054
|
return true;
|
|
938
1055
|
}
|
|
@@ -941,45 +1058,171 @@ export class MeshStore {
|
|
|
941
1058
|
}
|
|
942
1059
|
}
|
|
943
1060
|
// -----------------------------------------------------------------------
|
|
944
|
-
//
|
|
1061
|
+
// Mesh visibility
|
|
945
1062
|
// -----------------------------------------------------------------------
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1063
|
+
/** Set mesh discovery visibility. Delegates to discovery manager. */
|
|
1064
|
+
async setVisibility(level, adapter) {
|
|
1065
|
+
await this.discovery.setVisibility(level, adapter);
|
|
1066
|
+
}
|
|
1067
|
+
/** Get current mesh discovery visibility. */
|
|
1068
|
+
getVisibility(adapter) {
|
|
1069
|
+
return this.discovery.getVisibility(adapter);
|
|
1070
|
+
}
|
|
1071
|
+
// -----------------------------------------------------------------------
|
|
1072
|
+
// Listener management (coordinator only)
|
|
1073
|
+
// -----------------------------------------------------------------------
|
|
1074
|
+
async addListener(host, port, policy) {
|
|
1075
|
+
function isListenerPolicy(v) {
|
|
1076
|
+
return (v === "full" || v === "observe" || v === "rooms-only" || v === "gateway");
|
|
1077
|
+
}
|
|
1078
|
+
if (!isListenerPolicy(policy)) {
|
|
1079
|
+
throw new CommsError(`Invalid policy "${policy}"`, "INVALID_POLICY");
|
|
1080
|
+
}
|
|
1081
|
+
return this.transport.addListener(host, port, policy);
|
|
1082
|
+
}
|
|
1083
|
+
async removeListener(id) {
|
|
1084
|
+
return this.transport.removeListener(id);
|
|
1085
|
+
}
|
|
1086
|
+
listListeners() {
|
|
1087
|
+
return this.transport.listListeners();
|
|
1088
|
+
}
|
|
1089
|
+
getNetworkInterfaces() {
|
|
1090
|
+
const interfaces = os.networkInterfaces();
|
|
1091
|
+
const result = [];
|
|
1092
|
+
for (const [name, addrs] of Object.entries(interfaces)) {
|
|
1093
|
+
if (addrs === undefined)
|
|
952
1094
|
continue;
|
|
953
|
-
|
|
954
|
-
|
|
1095
|
+
for (const addr of addrs) {
|
|
1096
|
+
result.push({
|
|
1097
|
+
name,
|
|
1098
|
+
address: addr.address,
|
|
1099
|
+
family: addr.family,
|
|
1100
|
+
internal: addr.internal,
|
|
1101
|
+
});
|
|
955
1102
|
}
|
|
956
1103
|
}
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1104
|
+
return result;
|
|
1105
|
+
}
|
|
1106
|
+
// -----------------------------------------------------------------------
|
|
1107
|
+
// Federation (coordinator-to-coordinator)
|
|
1108
|
+
// -----------------------------------------------------------------------
|
|
1109
|
+
async fedConnect(host, port, name) {
|
|
1110
|
+
return this.federation.connect(host, port, name);
|
|
1111
|
+
}
|
|
1112
|
+
async fedDisconnect(linkId) {
|
|
1113
|
+
await this.federation.disconnect(linkId);
|
|
1114
|
+
}
|
|
1115
|
+
fedLinks() {
|
|
1116
|
+
return this.federation.listLinks();
|
|
1117
|
+
}
|
|
1118
|
+
// -----------------------------------------------------------------------
|
|
1119
|
+
// Federation callbacks (inbound from remote meshes)
|
|
1120
|
+
// -----------------------------------------------------------------------
|
|
1121
|
+
async handleFedAgentVisible(agent) {
|
|
1122
|
+
// Store remote agent with a prefixed ID to avoid collisions with local agents
|
|
1123
|
+
const remoteId = `fed:${agent.id}@${agent.harness}`;
|
|
1124
|
+
const remoteAgent = {
|
|
1125
|
+
...agent,
|
|
1126
|
+
id: remoteId,
|
|
1127
|
+
tags: [...agent.tags, "federated"],
|
|
1128
|
+
};
|
|
1129
|
+
this.agents.set(remoteId, remoteAgent);
|
|
1130
|
+
await this.broadcastPatch({ type: "agent_upsert", agent: remoteAgent });
|
|
1131
|
+
}
|
|
1132
|
+
async handleFedAgentGone(agentId) {
|
|
1133
|
+
// The agentId comes from the remote mesh — we need to find the prefixed version
|
|
1134
|
+
const prefix = `fed:${agentId}@`;
|
|
1135
|
+
for (const [localId, agent] of this.agents) {
|
|
1136
|
+
if (localId.startsWith(prefix)) {
|
|
1137
|
+
agent.status = "offline";
|
|
1138
|
+
this.agents.set(localId, agent);
|
|
1139
|
+
await this.broadcastPatch({ type: "agent_offline", agentId: localId });
|
|
1140
|
+
break;
|
|
965
1141
|
}
|
|
966
1142
|
}
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
1143
|
+
}
|
|
1144
|
+
async handleFedRoomMessage(roomId, message) {
|
|
1145
|
+
const room = this.rooms.get(roomId);
|
|
1146
|
+
if (!room?.federated)
|
|
1147
|
+
return;
|
|
1148
|
+
// Store the message locally
|
|
1149
|
+
const arr = this.messages.get(roomId) ?? [];
|
|
1150
|
+
arr.push(message);
|
|
1151
|
+
this.messages.set(roomId, arr);
|
|
1152
|
+
// Deliver to all local room members
|
|
1153
|
+
for (const memberId of room.members) {
|
|
1154
|
+
await this.deliverLocallyAndBroadcast(memberId, {
|
|
1155
|
+
type: "room_message",
|
|
1156
|
+
message,
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
async handleFedRoomJoin(roomId, agentId, _agentName) {
|
|
1161
|
+
const room = this.rooms.get(roomId);
|
|
1162
|
+
if (!room?.federated)
|
|
1163
|
+
return;
|
|
1164
|
+
// Create a shadow agent ID for the remote agent
|
|
1165
|
+
const remoteId = `fed:${agentId}`;
|
|
1166
|
+
if (!room.members.includes(remoteId)) {
|
|
1167
|
+
room.members.push(remoteId);
|
|
1168
|
+
this.rooms.set(roomId, room);
|
|
1169
|
+
await this.broadcastPatch({ type: "room_upsert", room });
|
|
1170
|
+
}
|
|
1171
|
+
// Notify local members
|
|
1172
|
+
await this.deliverToRoom(roomId, {
|
|
1173
|
+
type: "member_joined",
|
|
1174
|
+
room: roomId,
|
|
1175
|
+
agent: remoteId,
|
|
1176
|
+
}, remoteId);
|
|
1177
|
+
}
|
|
1178
|
+
async handleFedRoomLeave(roomId, agentId) {
|
|
1179
|
+
const room = this.rooms.get(roomId);
|
|
1180
|
+
if (!room?.federated)
|
|
1181
|
+
return;
|
|
1182
|
+
const remoteId = `fed:${agentId}`;
|
|
1183
|
+
room.members = room.members.filter((m) => m !== remoteId);
|
|
1184
|
+
this.rooms.set(roomId, room);
|
|
1185
|
+
await this.broadcastPatch({ type: "room_upsert", room });
|
|
1186
|
+
await this.deliverToRoom(roomId, {
|
|
1187
|
+
type: "member_left",
|
|
1188
|
+
room: roomId,
|
|
1189
|
+
agent: remoteId,
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
getVisibleAgentsForFed() {
|
|
1193
|
+
const result = [];
|
|
1194
|
+
for (const agent of this.agents.values()) {
|
|
1195
|
+
// Only broadcast agents that belong to this mesh (not federated)
|
|
1196
|
+
// and are visible
|
|
1197
|
+
if (agent.visibility === "visible" && !agent.id.startsWith("fed:")) {
|
|
1198
|
+
result.push(agent);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
return result;
|
|
1202
|
+
}
|
|
1203
|
+
getFederatedRoomMembershipsForFed() {
|
|
1204
|
+
const result = new Map();
|
|
1205
|
+
for (const [roomId, room] of this.rooms) {
|
|
1206
|
+
if (room.federated) {
|
|
1207
|
+
// Only include local members (not federated ones)
|
|
1208
|
+
const localMembers = room.members.filter((m) => !m.startsWith("fed:"));
|
|
1209
|
+
result.set(roomId, localMembers);
|
|
1210
|
+
}
|
|
972
1211
|
}
|
|
973
|
-
|
|
974
|
-
this.coordinatorServer?.unref();
|
|
975
|
-
this.coordinatorServer?.close();
|
|
976
|
-
this.coordinatorServer = undefined;
|
|
977
|
-
this.isCoordinator = false;
|
|
1212
|
+
return result;
|
|
978
1213
|
}
|
|
979
1214
|
// -----------------------------------------------------------------------
|
|
980
1215
|
// Shutdown
|
|
981
1216
|
// -----------------------------------------------------------------------
|
|
982
1217
|
async shutdown() {
|
|
1218
|
+
this.isShutDown = true;
|
|
1219
|
+
// Clear any pending markRead timers so they don't fire after the
|
|
1220
|
+
// transport is shut down (which would attempt sends on closed sockets)
|
|
1221
|
+
// or keep the event loop alive after process.exit().
|
|
1222
|
+
for (const timer of this.pendingMarkReadTimers) {
|
|
1223
|
+
clearTimeout(timer);
|
|
1224
|
+
}
|
|
1225
|
+
this.pendingMarkReadTimers.length = 0;
|
|
983
1226
|
const agent = this.agents.get(this.peerId);
|
|
984
1227
|
if (agent) {
|
|
985
1228
|
agent.status = "offline";
|
|
@@ -988,37 +1231,9 @@ export class MeshStore {
|
|
|
988
1231
|
agentId: this.peerId,
|
|
989
1232
|
});
|
|
990
1233
|
}
|
|
991
|
-
await this.handoverCoordinator();
|
|
992
|
-
// Destroy the coordinator client socket (not tracked in peerConnections)
|
|
993
|
-
this.coordinatorSocket?.unref();
|
|
994
|
-
this.coordinatorSocket?.destroy();
|
|
995
|
-
this.coordinatorSocket = undefined;
|
|
996
|
-
// Destroy all identified peer connections
|
|
997
|
-
for (const [, peer] of this.peerConnections) {
|
|
998
|
-
peer.socket.unref();
|
|
999
|
-
peer.socket.destroy();
|
|
1000
|
-
}
|
|
1001
|
-
this.peerConnections.clear();
|
|
1002
|
-
// Destroy all data server accepted sockets (including unidentified)
|
|
1003
|
-
for (const socket of this.dataServerSockets) {
|
|
1004
|
-
socket.unref();
|
|
1005
|
-
socket.destroy();
|
|
1006
|
-
}
|
|
1007
|
-
this.dataServerSockets.clear();
|
|
1008
|
-
// Destroy all coordinator server accepted sockets
|
|
1009
|
-
for (const socket of this.coordinatorServerSockets) {
|
|
1010
|
-
socket.unref();
|
|
1011
|
-
socket.destroy();
|
|
1012
|
-
}
|
|
1013
|
-
this.coordinatorServerSockets.clear();
|
|
1014
1234
|
this.stopStaleCheck();
|
|
1015
|
-
|
|
1016
|
-
this.
|
|
1017
|
-
this.dataServer?.close();
|
|
1018
|
-
this.dataServer = undefined;
|
|
1019
|
-
this.coordinatorServer?.unref();
|
|
1020
|
-
this.coordinatorServer?.close();
|
|
1021
|
-
this.coordinatorServer = undefined;
|
|
1235
|
+
await this.federation.shutdown();
|
|
1236
|
+
await this.transport.shutdown();
|
|
1022
1237
|
}
|
|
1023
1238
|
}
|
|
1024
1239
|
//# sourceMappingURL=mesh-store.js.map
|