@cryptiklemur/lattice 1.31.1 → 1.32.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.
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useCallback, memo } from "react";
|
|
2
|
-
import { Plus, CircleDot, Circle } from "lucide-react";
|
|
2
|
+
import { Plus, CircleDot, Circle, RefreshCw, Loader2 } from "lucide-react";
|
|
3
3
|
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
4
4
|
import { useMesh } from "../../hooks/useMesh";
|
|
5
5
|
import { PairingDialog } from "../mesh/PairingDialog";
|
|
@@ -8,10 +8,12 @@ import type { NodeInfo } from "@lattice/shared";
|
|
|
8
8
|
interface NodeRowProps {
|
|
9
9
|
node: NodeInfo;
|
|
10
10
|
onUnpair: (nodeId: string) => void;
|
|
11
|
+
onReconnect: (nodeId: string) => void;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
function NodeRow(props: NodeRowProps) {
|
|
14
15
|
var [confirming, setConfirming] = useState(false);
|
|
16
|
+
var [reconnecting, setReconnecting] = useState(false);
|
|
15
17
|
|
|
16
18
|
function handleUnpair() {
|
|
17
19
|
if (!confirming) {
|
|
@@ -56,6 +58,27 @@ function NodeRow(props: NodeRowProps) {
|
|
|
56
58
|
|
|
57
59
|
{!props.node.isLocal && (
|
|
58
60
|
<div className="flex gap-1.5 flex-shrink-0">
|
|
61
|
+
{!props.node.online && (
|
|
62
|
+
reconnecting ? (
|
|
63
|
+
<span className="flex items-center gap-1 text-[11px] text-base-content/40">
|
|
64
|
+
<Loader2 size={10} className="animate-spin" />
|
|
65
|
+
Connecting...
|
|
66
|
+
</span>
|
|
67
|
+
) : (
|
|
68
|
+
<button
|
|
69
|
+
onClick={function () {
|
|
70
|
+
setReconnecting(true);
|
|
71
|
+
props.onReconnect(props.node.id);
|
|
72
|
+
setTimeout(function () { setReconnecting(false); }, 5000);
|
|
73
|
+
}}
|
|
74
|
+
className="btn btn-ghost btn-xs border border-base-content/20 hover:btn-info hover:border-info gap-1"
|
|
75
|
+
title="Attempt to reconnect"
|
|
76
|
+
>
|
|
77
|
+
<RefreshCw size={10} />
|
|
78
|
+
Reconnect
|
|
79
|
+
</button>
|
|
80
|
+
)
|
|
81
|
+
)}
|
|
59
82
|
{confirming ? (
|
|
60
83
|
<>
|
|
61
84
|
<button
|
|
@@ -96,6 +119,10 @@ export function MeshStatus() {
|
|
|
96
119
|
ws.send({ type: "mesh:unpair", nodeId });
|
|
97
120
|
}
|
|
98
121
|
|
|
122
|
+
function handleReconnect(nodeId: string) {
|
|
123
|
+
ws.send({ type: "mesh:reconnect", nodeId } as any);
|
|
124
|
+
}
|
|
125
|
+
|
|
99
126
|
var localNode = nodes.find(function (n) { return n.isLocal; });
|
|
100
127
|
var remoteNodes = nodes.filter(function (n) { return !n.isLocal; });
|
|
101
128
|
|
|
@@ -106,7 +133,7 @@ export function MeshStatus() {
|
|
|
106
133
|
This Node
|
|
107
134
|
</div>
|
|
108
135
|
{localNode ? (
|
|
109
|
-
<NodeRow node={localNode} onUnpair={handleUnpair} />
|
|
136
|
+
<NodeRow node={localNode} onUnpair={handleUnpair} onReconnect={handleReconnect} />
|
|
110
137
|
) : (
|
|
111
138
|
<div className="text-[12px] text-base-content/40 italic">
|
|
112
139
|
Waiting for node info...
|
|
@@ -139,6 +166,7 @@ export function MeshStatus() {
|
|
|
139
166
|
key={node.id}
|
|
140
167
|
node={node}
|
|
141
168
|
onUnpair={handleUnpair}
|
|
169
|
+
onReconnect={handleReconnect}
|
|
142
170
|
/>
|
|
143
171
|
);
|
|
144
172
|
})
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cryptiklemur/lattice",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.32.1",
|
|
4
4
|
"description": "Multi-machine agentic dashboard for Claude Code. Monitor sessions, manage MCP servers and skills, orchestrate across mesh-networked nodes.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Aaron Scherer <me@aaronscherer.me>",
|
|
@@ -4,8 +4,8 @@ import { sendTo, broadcast } from "../ws/broadcast";
|
|
|
4
4
|
import { loadConfig } from "../config";
|
|
5
5
|
import { loadOrCreateIdentity } from "../identity";
|
|
6
6
|
import { generateInviteCode, parseInviteCode, validatePairingToken, consumePairingToken } from "../mesh/pairing";
|
|
7
|
-
import { addPeer, removePeer, loadPeers } from "../mesh/peers";
|
|
8
|
-
import { getConnectedPeerIds, connectToPeer } from "../mesh/connector";
|
|
7
|
+
import { addPeer, removePeer, loadPeers, getPeer } from "../mesh/peers";
|
|
8
|
+
import { getConnectedPeerIds, connectToPeer, reconnectPeer } from "../mesh/connector";
|
|
9
9
|
import type { PeerInfo } from "@lattice/shared";
|
|
10
10
|
import { networkInterfaces } from "node:os";
|
|
11
11
|
|
|
@@ -172,6 +172,20 @@ registerHandler("mesh", function (clientId: string, message: ClientMessage) {
|
|
|
172
172
|
if ((message as any).type === "mesh:hello") {
|
|
173
173
|
var hello = message as any as { type: "mesh:hello"; nodeId: string; name: string; token?: string; port?: number; addresses?: string[]; projects: Array<{ slug: string; title: string }> };
|
|
174
174
|
|
|
175
|
+
var knownPeer = hello.nodeId ? getPeer(hello.nodeId) : undefined;
|
|
176
|
+
|
|
177
|
+
if (knownPeer) {
|
|
178
|
+
var identity = loadOrCreateIdentity();
|
|
179
|
+
sendTo(clientId, {
|
|
180
|
+
type: "mesh:hello" as any,
|
|
181
|
+
nodeId: identity.id,
|
|
182
|
+
name: loadConfig().name,
|
|
183
|
+
projects: [],
|
|
184
|
+
});
|
|
185
|
+
broadcast({ type: "mesh:nodes", nodes: buildNodesMessage() });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
175
189
|
if (!hello.token || !validatePairingToken(hello.token)) {
|
|
176
190
|
sendTo(clientId, { type: "mesh:hello_rejected" as any, error: "Invalid or expired invite code" });
|
|
177
191
|
return;
|
|
@@ -193,10 +207,10 @@ registerHandler("mesh", function (clientId: string, message: ClientMessage) {
|
|
|
193
207
|
connectToPeer(peer.id, peerAddresses[0]);
|
|
194
208
|
}
|
|
195
209
|
|
|
196
|
-
var
|
|
210
|
+
var identity2 = loadOrCreateIdentity();
|
|
197
211
|
sendTo(clientId, {
|
|
198
212
|
type: "mesh:hello" as any,
|
|
199
|
-
nodeId:
|
|
213
|
+
nodeId: identity2.id,
|
|
200
214
|
name: loadConfig().name,
|
|
201
215
|
projects: [],
|
|
202
216
|
});
|
|
@@ -205,6 +219,15 @@ registerHandler("mesh", function (clientId: string, message: ClientMessage) {
|
|
|
205
219
|
return;
|
|
206
220
|
}
|
|
207
221
|
|
|
222
|
+
if (message.type === "mesh:reconnect") {
|
|
223
|
+
var reconnectMsg = message as { type: "mesh:reconnect"; nodeId: string };
|
|
224
|
+
reconnectPeer(reconnectMsg.nodeId);
|
|
225
|
+
setTimeout(function () {
|
|
226
|
+
broadcast({ type: "mesh:nodes", nodes: buildNodesMessage() });
|
|
227
|
+
}, 2000);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
208
231
|
if (message.type === "mesh:unpair") {
|
|
209
232
|
var unpairMsg = message as MeshUnpairMessage;
|
|
210
233
|
var removed = removePeer(unpairMsg.nodeId);
|
|
@@ -35,10 +35,25 @@ interface CircuitState {
|
|
|
35
35
|
|
|
36
36
|
var circuitBreakers = new Map<string, CircuitState>();
|
|
37
37
|
|
|
38
|
+
var RECONNECT_INTERVAL_MS = 15000;
|
|
39
|
+
|
|
38
40
|
export function startMeshConnections(): void {
|
|
41
|
+
reconcilePeers();
|
|
42
|
+
setInterval(reconcilePeers, RECONNECT_INTERVAL_MS);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function reconcilePeers(): void {
|
|
39
46
|
var peers = loadPeers();
|
|
40
47
|
for (var i = 0; i < peers.length; i++) {
|
|
41
|
-
|
|
48
|
+
var peer = peers[i];
|
|
49
|
+
if (!peer.addresses || peer.addresses.length === 0) continue;
|
|
50
|
+
var existing = connections.get(peer.id);
|
|
51
|
+
if (existing && !existing.dead && existing.ws.readyState === WebSocket.OPEN) continue;
|
|
52
|
+
if (existing && !existing.dead && existing.retryTimer !== null) continue;
|
|
53
|
+
if (!existing || existing.dead) {
|
|
54
|
+
connections.delete(peer.id);
|
|
55
|
+
connectToPeer(peer.id, peer.addresses[0]);
|
|
56
|
+
}
|
|
42
57
|
}
|
|
43
58
|
}
|
|
44
59
|
|
|
@@ -223,6 +238,26 @@ export function getPeerConnection(nodeId: string): WebSocket | undefined {
|
|
|
223
238
|
return conn.ws;
|
|
224
239
|
}
|
|
225
240
|
|
|
241
|
+
export function reconnectPeer(nodeId: string): void {
|
|
242
|
+
var existing = connections.get(nodeId);
|
|
243
|
+
if (existing) {
|
|
244
|
+
existing.dead = true;
|
|
245
|
+
if (existing.retryTimer !== null) {
|
|
246
|
+
clearTimeout(existing.retryTimer);
|
|
247
|
+
existing.retryTimer = null;
|
|
248
|
+
}
|
|
249
|
+
existing.ws.close();
|
|
250
|
+
connections.delete(nodeId);
|
|
251
|
+
}
|
|
252
|
+
circuitBreakers.delete(nodeId);
|
|
253
|
+
|
|
254
|
+
var peers = loadPeers();
|
|
255
|
+
var peer = peers.find(function (p) { return p.id === nodeId; });
|
|
256
|
+
if (peer && peer.addresses && peer.addresses.length > 0) {
|
|
257
|
+
connectToPeer(nodeId, peer.addresses[0]);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
226
261
|
export function getConnectedPeerIds(): string[] {
|
|
227
262
|
var ids: string[] = [];
|
|
228
263
|
for (var [nodeId, conn] of connections) {
|
package/shared/src/messages.ts
CHANGED
|
@@ -218,6 +218,11 @@ export interface MeshUnpairMessage {
|
|
|
218
218
|
nodeId: string;
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
export interface MeshReconnectMessage {
|
|
222
|
+
type: "mesh:reconnect";
|
|
223
|
+
nodeId: string;
|
|
224
|
+
}
|
|
225
|
+
|
|
221
226
|
export interface LoopStartMessage {
|
|
222
227
|
type: "loop:start";
|
|
223
228
|
projectSlug: string;
|
|
@@ -537,6 +542,7 @@ export type ClientMessage =
|
|
|
537
542
|
| MeshPairMessage
|
|
538
543
|
| MeshGenerateInviteMessage
|
|
539
544
|
| MeshUnpairMessage
|
|
545
|
+
| MeshReconnectMessage
|
|
540
546
|
| LoopStartMessage
|
|
541
547
|
| LoopStopMessage
|
|
542
548
|
| LoopStatusRequestMessage
|