agent-comms 1.14.0 → 1.15.1
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.map +1 -1
- package/dist/bridges/pi/index.js +39 -17
- package/dist/bridges/pi/index.js.map +1 -1
- package/dist/bridges/user/controller.d.ts +9 -1
- package/dist/bridges/user/controller.d.ts.map +1 -1
- package/dist/bridges/user/controller.js +25 -1
- package/dist/bridges/user/controller.js.map +1 -1
- package/dist/bridges/user/tui.js +2 -0
- package/dist/bridges/user/tui.js.map +1 -1
- package/dist/bridges/user/web/build.d.ts +2 -0
- package/dist/bridges/user/web/build.d.ts.map +1 -1
- package/dist/bridges/user/web/build.js +44 -0
- package/dist/bridges/user/web/build.js.map +1 -1
- package/dist/bridges/user/web/frontend/components/App.d.ts +5 -0
- package/dist/bridges/user/web/frontend/components/App.d.ts.map +1 -1
- package/dist/bridges/user/web/frontend/components/App.js +1 -1
- package/dist/bridges/user/web/frontend/components/App.js.map +1 -1
- package/dist/bridges/user/web/frontend/components/ProjectTree.js +6 -3
- package/dist/bridges/user/web/frontend/components/ProjectTree.js.map +1 -1
- 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 +7 -2
- package/dist/bridges/user/web/frontend/components/Sidebar.d.ts.map +1 -1
- package/dist/bridges/user/web/frontend/components/Sidebar.js +15 -9
- package/dist/bridges/user/web/frontend/components/Sidebar.js.map +1 -1
- package/dist/bridges/user/web/frontend/input.js +1 -1
- package/dist/bridges/user/web/frontend/input.js.map +1 -1
- package/dist/bridges/user/web/frontend/main.js +73 -5
- 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/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/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/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 +254 -12
- package/dist/bridges/user/web/server.js.map +1 -1
- package/dist/core/bridge.d.ts +28 -0
- package/dist/core/bridge.d.ts.map +1 -1
- package/dist/core/bridge.js +137 -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 +11 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +6 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/mesh-store.d.ts +72 -26
- package/dist/core/mesh-store.d.ts.map +1 -1
- package/dist/core/mesh-store.js +467 -352
- 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/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 +136 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +71 -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 +4 -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,159 +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
|
+
}
|
|
98
59
|
lastLocalDeliveryKey;
|
|
99
60
|
localDeliveryKeys = new Set();
|
|
61
|
+
discovery;
|
|
62
|
+
federation;
|
|
100
63
|
constructor(coordinatorPort = DEFAULT_COORDINATOR_PORT) {
|
|
101
64
|
this.peerId = nanoid(8);
|
|
102
65
|
this.startedAt = new Date().toISOString();
|
|
103
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;
|
|
104
89
|
}
|
|
105
90
|
// -----------------------------------------------------------------------
|
|
106
91
|
// Mesh lifecycle
|
|
107
92
|
// -----------------------------------------------------------------------
|
|
108
93
|
async init() {
|
|
109
|
-
await this.startDataServer();
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
this.coordinatorServer?.unref();
|
|
116
|
-
this.coordinatorSocket?.unref();
|
|
117
|
-
}
|
|
118
|
-
startDataServer() {
|
|
119
|
-
return new Promise((resolve, reject) => {
|
|
120
|
-
this.dataServer = net.createServer((socket) => {
|
|
121
|
-
this.handleDataConnection(socket);
|
|
122
|
-
});
|
|
123
|
-
this.dataServer.listen(0, COORDINATOR_HOST, () => {
|
|
124
|
-
const addr = this.dataServer?.address();
|
|
125
|
-
if (addr && typeof addr === "object") {
|
|
126
|
-
this.dataPort = addr.port;
|
|
127
|
-
}
|
|
128
|
-
resolve();
|
|
129
|
-
});
|
|
130
|
-
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,
|
|
131
100
|
});
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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;
|
|
158
128
|
}
|
|
129
|
+
// EADDRINUSE — another process became coordinator. Retry connect.
|
|
159
130
|
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
resolve();
|
|
166
|
-
});
|
|
167
|
-
const timer = setTimeout(() => {
|
|
168
|
-
socket.destroy();
|
|
169
|
-
reject(new Error("Coordinator connection timeout"));
|
|
170
|
-
}, 2000);
|
|
171
|
-
socket.on("error", (err) => {
|
|
172
|
-
clearTimeout(timer);
|
|
173
|
-
socket.destroy();
|
|
174
|
-
reject(err);
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
handleCoordinatorResponse(msg) {
|
|
179
|
-
if (msg.method === "peer_list") {
|
|
180
|
-
for (const peer of msg.peers) {
|
|
181
|
-
this.peerInfo.set(peer.id, peer);
|
|
182
|
-
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
|
+
}
|
|
183
136
|
}
|
|
184
137
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
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"}`);
|
|
188
140
|
}
|
|
189
|
-
|
|
190
|
-
becomeCoordinator() {
|
|
191
|
-
return new Promise((resolve, reject) => {
|
|
192
|
-
this.coordinatorServer = net.createServer((socket) => {
|
|
193
|
-
this.handleCoordinatorConnection(socket);
|
|
194
|
-
});
|
|
195
|
-
this.coordinatorServer.listen(this.coordinatorPort, COORDINATOR_HOST, () => {
|
|
196
|
-
this.isCoordinator = true;
|
|
197
|
-
this.startStaleCheck();
|
|
198
|
-
this.peerInfo.set(this.peerId, {
|
|
199
|
-
id: this.peerId,
|
|
200
|
-
port: this.dataPort,
|
|
201
|
-
startedAt: this.startedAt,
|
|
202
|
-
});
|
|
203
|
-
resolve();
|
|
204
|
-
});
|
|
205
|
-
this.coordinatorServer.on("error", (err) => {
|
|
206
|
-
const isAddrInUse = err instanceof Error && "code" in err && err.code === "EADDRINUSE";
|
|
207
|
-
if (isAddrInUse) {
|
|
208
|
-
this.coordinatorServer = undefined;
|
|
209
|
-
void this.connectToCoordinator().then(resolve, reject);
|
|
210
|
-
}
|
|
211
|
-
else {
|
|
212
|
-
reject(err instanceof Error ? err : new Error(String(err)));
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
});
|
|
141
|
+
this.transport.unref();
|
|
216
142
|
}
|
|
217
143
|
// -----------------------------------------------------------------------
|
|
218
|
-
//
|
|
144
|
+
// TransportEvents — callbacks from the transport layer
|
|
219
145
|
// -----------------------------------------------------------------------
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
for (const item of items) {
|
|
230
|
-
if (isMeshMessage(item) && item.method === "introduce") {
|
|
231
|
-
void this.handleIntroduction(socket, item);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
});
|
|
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);
|
|
235
155
|
}
|
|
236
|
-
async handleIntroduction(
|
|
156
|
+
async handleIntroduction(handle, msg) {
|
|
237
157
|
const newPeer = {
|
|
238
158
|
id: msg.peerId,
|
|
239
159
|
port: msg.dataPort,
|
|
@@ -245,45 +165,29 @@ export class MeshStore {
|
|
|
245
165
|
method: "peer_list",
|
|
246
166
|
peers: [...this.peerInfo.values()],
|
|
247
167
|
};
|
|
248
|
-
await
|
|
249
|
-
// Broadcast arrival to all existing
|
|
168
|
+
await this.transport.send(handle, peerList);
|
|
169
|
+
// Broadcast arrival to all existing peers
|
|
250
170
|
const joined = { method: "peer_joined", peer: newPeer };
|
|
251
|
-
await this.
|
|
171
|
+
await this.transport.broadcast(joined);
|
|
252
172
|
// Connect to the new peer's data server
|
|
253
|
-
void this.
|
|
173
|
+
void this.transport.connectToPeer(newPeer, this.peerId);
|
|
254
174
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
remotePeerId = item.peerId;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
if (remotePeerId && !this.peerConnections.has(remotePeerId)) {
|
|
274
|
-
this.peerConnections.set(remotePeerId, { socket, buffer });
|
|
275
|
-
}
|
|
276
|
-
});
|
|
277
|
-
socket.on("close", () => {
|
|
278
|
-
if (remotePeerId)
|
|
279
|
-
this.peerConnections.delete(remotePeerId);
|
|
280
|
-
});
|
|
281
|
-
socket.on("error", () => {
|
|
282
|
-
if (remotePeerId)
|
|
283
|
-
this.peerConnections.delete(remotePeerId);
|
|
284
|
-
});
|
|
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
|
+
}
|
|
285
189
|
}
|
|
286
|
-
async handleDataMessage(msg) {
|
|
190
|
+
async handleDataMessage(handle, msg) {
|
|
287
191
|
if (msg.method === "state_sync") {
|
|
288
192
|
// Merge — don't replace — so our own state isn't lost
|
|
289
193
|
const incoming = {
|
|
@@ -313,6 +217,135 @@ export class MeshStore {
|
|
|
313
217
|
await this.applyPatch(msg.patch);
|
|
314
218
|
}
|
|
315
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
|
+
// -----------------------------------------------------------------------
|
|
316
349
|
async applyPatch(patch) {
|
|
317
350
|
switch (patch.type) {
|
|
318
351
|
case "agent_upsert": {
|
|
@@ -387,13 +420,24 @@ export class MeshStore {
|
|
|
387
420
|
this.localDeliveryKeys.delete(oldest);
|
|
388
421
|
}
|
|
389
422
|
void this.onDelivery(patch.agentId, patch.event);
|
|
390
|
-
// Auto-mark read —
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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);
|
|
397
441
|
}
|
|
398
442
|
break;
|
|
399
443
|
}
|
|
@@ -402,63 +446,18 @@ export class MeshStore {
|
|
|
402
446
|
break;
|
|
403
447
|
}
|
|
404
448
|
}
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
return;
|
|
409
|
-
if (this.peerConnections.has(peer.id))
|
|
410
|
-
return;
|
|
411
|
-
return new Promise((resolve) => {
|
|
412
|
-
const socket = net.createConnection({ port: peer.port, host: COORDINATOR_HOST }, () => {
|
|
413
|
-
const buf = new MessageBuffer();
|
|
414
|
-
this.peerConnections.set(peer.id, { socket, buffer: buf });
|
|
415
|
-
// Identify ourselves
|
|
416
|
-
const pong = { method: "pong", peerId: this.peerId };
|
|
417
|
-
socket.write(encode(pong));
|
|
418
|
-
// If we have state and peer doesn't, send state sync
|
|
419
|
-
if (this.agents.size > 0) {
|
|
420
|
-
const state = {
|
|
421
|
-
agents: Object.fromEntries(this.agents),
|
|
422
|
-
rooms: Object.fromEntries(this.rooms),
|
|
423
|
-
messages: Object.fromEntries(this.messages),
|
|
424
|
-
dms: Object.fromEntries(this.dms),
|
|
425
|
-
};
|
|
426
|
-
socket.write(encode({ method: "state_sync", state }));
|
|
427
|
-
}
|
|
428
|
-
// Wire up ongoing message handling
|
|
429
|
-
socket.on("data", (data) => {
|
|
430
|
-
const items = buf.append(data.toString());
|
|
431
|
-
for (const item of items) {
|
|
432
|
-
if (isMeshMessage(item)) {
|
|
433
|
-
void this.handleDataMessage(item);
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
});
|
|
437
|
-
resolve();
|
|
438
|
-
});
|
|
439
|
-
socket.on("close", () => this.peerConnections.delete(peer.id));
|
|
440
|
-
socket.on("error", () => {
|
|
441
|
-
this.peerConnections.delete(peer.id);
|
|
442
|
-
socket.destroy();
|
|
443
|
-
resolve();
|
|
444
|
-
});
|
|
445
|
-
});
|
|
449
|
+
if (this.onPatch) {
|
|
450
|
+
await this.onPatch(patch);
|
|
451
|
+
}
|
|
446
452
|
}
|
|
447
453
|
// -----------------------------------------------------------------------
|
|
448
|
-
// Broadcast
|
|
454
|
+
// Broadcast
|
|
449
455
|
// -----------------------------------------------------------------------
|
|
450
|
-
async broadcastToDataConnections(msg) {
|
|
451
|
-
const data = encode(msg);
|
|
452
|
-
const writes = [];
|
|
453
|
-
for (const [, peer] of this.peerConnections) {
|
|
454
|
-
writes.push(writeAsync(peer.socket, data).catch(() => {
|
|
455
|
-
/* broken connection — cleanup handled by close/error listeners */
|
|
456
|
-
}));
|
|
457
|
-
}
|
|
458
|
-
await Promise.all(writes);
|
|
459
|
-
}
|
|
460
456
|
async broadcastPatch(patch) {
|
|
461
|
-
await this.
|
|
457
|
+
await this.transport.broadcast({ method: "state_update", patch });
|
|
458
|
+
if (this.onPatch) {
|
|
459
|
+
await this.onPatch(patch);
|
|
460
|
+
}
|
|
462
461
|
}
|
|
463
462
|
async deliverLocallyAndBroadcast(agentId, event) {
|
|
464
463
|
// Local delivery
|
|
@@ -487,13 +486,20 @@ export class MeshStore {
|
|
|
487
486
|
this.localDeliveryKeys.delete(oldest);
|
|
488
487
|
}
|
|
489
488
|
void this.onDelivery(agentId, event);
|
|
490
|
-
// Auto-mark read —
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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);
|
|
497
503
|
}
|
|
498
504
|
// Remote delivery
|
|
499
505
|
const patch = { type: "delivery", agentId, event };
|
|
@@ -656,6 +662,10 @@ export class MeshStore {
|
|
|
656
662
|
this.agents.set(id, agent);
|
|
657
663
|
await this.writeIdentity(opts.harness, opts.cwd, id);
|
|
658
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
|
+
}
|
|
659
669
|
return agent;
|
|
660
670
|
}
|
|
661
671
|
async getAgent(id) {
|
|
@@ -703,9 +713,7 @@ export class MeshStore {
|
|
|
703
713
|
if (isOwner) {
|
|
704
714
|
await this.notifyRoomsOfStatus(id, "offline");
|
|
705
715
|
await this.broadcastPatch({ type: "agent_offline", agentId: id });
|
|
706
|
-
|
|
707
|
-
if (this.isCoordinator) {
|
|
708
|
-
await this.handoverCoordinator();
|
|
716
|
+
await this.federation.broadcastAgentGone(id);
|
|
709
717
|
}
|
|
710
718
|
}
|
|
711
719
|
// -----------------------------------------------------------------------
|
|
@@ -724,6 +732,7 @@ export class MeshStore {
|
|
|
724
732
|
description: opts.description,
|
|
725
733
|
members: [opts.owner],
|
|
726
734
|
invited: [],
|
|
735
|
+
federated: opts.federated ?? false,
|
|
727
736
|
};
|
|
728
737
|
this.rooms.set(id, room);
|
|
729
738
|
this.messages.set(id, []);
|
|
@@ -793,6 +802,11 @@ export class MeshStore {
|
|
|
793
802
|
room: roomId,
|
|
794
803
|
agent: agentId,
|
|
795
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
|
+
}
|
|
796
810
|
return room;
|
|
797
811
|
}
|
|
798
812
|
async leaveRoom(roomId, agentId) {
|
|
@@ -813,6 +827,10 @@ export class MeshStore {
|
|
|
813
827
|
room: roomId,
|
|
814
828
|
agent: agentId,
|
|
815
829
|
});
|
|
830
|
+
// Notify federated links if the room is federated
|
|
831
|
+
if (room.federated) {
|
|
832
|
+
await this.federation.broadcastRoomLeave(roomId, agentId);
|
|
833
|
+
}
|
|
816
834
|
if (room.members.length === 0 && room.owner === agentId) {
|
|
817
835
|
await this.destroyRoom(roomId, agentId);
|
|
818
836
|
}
|
|
@@ -908,6 +926,10 @@ export class MeshStore {
|
|
|
908
926
|
arr.push(message);
|
|
909
927
|
this.messages.set(roomId, arr);
|
|
910
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
|
+
}
|
|
911
933
|
for (const memberId of room.members) {
|
|
912
934
|
if (memberId !== from) {
|
|
913
935
|
await this.deliverLocallyAndBroadcast(memberId, {
|
|
@@ -1007,9 +1029,7 @@ export class MeshStore {
|
|
|
1007
1029
|
await this.notifyRoomsOfStatus(id, "offline");
|
|
1008
1030
|
}
|
|
1009
1031
|
await this.broadcastPatch({ type: "agent_offline", agentId: id });
|
|
1010
|
-
// Clean up stale peer from coordinator's registry
|
|
1011
1032
|
this.peerInfo.delete(id);
|
|
1012
|
-
this.peerConnections.delete(id);
|
|
1013
1033
|
}
|
|
1014
1034
|
// Also purge long-offline agents to prevent indefinite accumulation
|
|
1015
1035
|
const offlineThreshold = Date.now() - 30 * 60 * 1000; // 30 minutes
|
|
@@ -1018,7 +1038,6 @@ export class MeshStore {
|
|
|
1018
1038
|
if (agent.status !== "offline")
|
|
1019
1039
|
continue;
|
|
1020
1040
|
const startedAt = new Date(agent.startedAt).getTime();
|
|
1021
|
-
// If the agent went offline long ago, purge it
|
|
1022
1041
|
if (startedAt < offlineThreshold) {
|
|
1023
1042
|
purgeIds.push(id);
|
|
1024
1043
|
}
|
|
@@ -1026,13 +1045,11 @@ export class MeshStore {
|
|
|
1026
1045
|
for (const id of purgeIds) {
|
|
1027
1046
|
this.agents.delete(id);
|
|
1028
1047
|
this.peerInfo.delete(id);
|
|
1029
|
-
this.peerConnections.delete(id);
|
|
1030
1048
|
this.identityCache.delete(id);
|
|
1031
1049
|
}
|
|
1032
1050
|
}
|
|
1033
1051
|
isProcessAlive(pid) {
|
|
1034
1052
|
try {
|
|
1035
|
-
// Sending signal 0 doesn't kill the process — it just checks existence
|
|
1036
1053
|
process.kill(pid, 0);
|
|
1037
1054
|
return true;
|
|
1038
1055
|
}
|
|
@@ -1041,45 +1058,171 @@ export class MeshStore {
|
|
|
1041
1058
|
}
|
|
1042
1059
|
}
|
|
1043
1060
|
// -----------------------------------------------------------------------
|
|
1044
|
-
//
|
|
1061
|
+
// Mesh visibility
|
|
1045
1062
|
// -----------------------------------------------------------------------
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
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)
|
|
1052
1094
|
continue;
|
|
1053
|
-
|
|
1054
|
-
|
|
1095
|
+
for (const addr of addrs) {
|
|
1096
|
+
result.push({
|
|
1097
|
+
name,
|
|
1098
|
+
address: addr.address,
|
|
1099
|
+
family: addr.family,
|
|
1100
|
+
internal: addr.internal,
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
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;
|
|
1055
1141
|
}
|
|
1056
1142
|
}
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
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);
|
|
1065
1199
|
}
|
|
1066
1200
|
}
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
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
|
+
}
|
|
1072
1211
|
}
|
|
1073
|
-
|
|
1074
|
-
this.coordinatorServer?.unref();
|
|
1075
|
-
this.coordinatorServer?.close();
|
|
1076
|
-
this.coordinatorServer = undefined;
|
|
1077
|
-
this.isCoordinator = false;
|
|
1212
|
+
return result;
|
|
1078
1213
|
}
|
|
1079
1214
|
// -----------------------------------------------------------------------
|
|
1080
1215
|
// Shutdown
|
|
1081
1216
|
// -----------------------------------------------------------------------
|
|
1082
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;
|
|
1083
1226
|
const agent = this.agents.get(this.peerId);
|
|
1084
1227
|
if (agent) {
|
|
1085
1228
|
agent.status = "offline";
|
|
@@ -1088,37 +1231,9 @@ export class MeshStore {
|
|
|
1088
1231
|
agentId: this.peerId,
|
|
1089
1232
|
});
|
|
1090
1233
|
}
|
|
1091
|
-
await this.handoverCoordinator();
|
|
1092
|
-
// Destroy the coordinator client socket (not tracked in peerConnections)
|
|
1093
|
-
this.coordinatorSocket?.unref();
|
|
1094
|
-
this.coordinatorSocket?.destroy();
|
|
1095
|
-
this.coordinatorSocket = undefined;
|
|
1096
|
-
// Destroy all identified peer connections
|
|
1097
|
-
for (const [, peer] of this.peerConnections) {
|
|
1098
|
-
peer.socket.unref();
|
|
1099
|
-
peer.socket.destroy();
|
|
1100
|
-
}
|
|
1101
|
-
this.peerConnections.clear();
|
|
1102
|
-
// Destroy all data server accepted sockets (including unidentified)
|
|
1103
|
-
for (const socket of this.dataServerSockets) {
|
|
1104
|
-
socket.unref();
|
|
1105
|
-
socket.destroy();
|
|
1106
|
-
}
|
|
1107
|
-
this.dataServerSockets.clear();
|
|
1108
|
-
// Destroy all coordinator server accepted sockets
|
|
1109
|
-
for (const socket of this.coordinatorServerSockets) {
|
|
1110
|
-
socket.unref();
|
|
1111
|
-
socket.destroy();
|
|
1112
|
-
}
|
|
1113
|
-
this.coordinatorServerSockets.clear();
|
|
1114
1234
|
this.stopStaleCheck();
|
|
1115
|
-
|
|
1116
|
-
this.
|
|
1117
|
-
this.dataServer?.close();
|
|
1118
|
-
this.dataServer = undefined;
|
|
1119
|
-
this.coordinatorServer?.unref();
|
|
1120
|
-
this.coordinatorServer?.close();
|
|
1121
|
-
this.coordinatorServer = undefined;
|
|
1235
|
+
await this.federation.shutdown();
|
|
1236
|
+
await this.transport.shutdown();
|
|
1122
1237
|
}
|
|
1123
1238
|
}
|
|
1124
1239
|
//# sourceMappingURL=mesh-store.js.map
|