@cryptiklemur/lattice 1.38.0 → 1.39.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cryptiklemur/lattice",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.39.0",
|
|
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>",
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ClientMessage, MeshPairMessage, MeshUnpairMessage, NodeInfo } from "@lattice/shared";
|
|
2
|
+
import { log } from "../logger";
|
|
2
3
|
import { registerHandler } from "../ws/router";
|
|
3
4
|
import { sendTo, broadcast } from "../ws/broadcast";
|
|
4
5
|
import { loadConfig } from "../config";
|
|
@@ -122,6 +123,8 @@ export function buildNodesMessage(): NodeInfo[] {
|
|
|
122
123
|
}
|
|
123
124
|
|
|
124
125
|
registerHandler("mesh", function (clientId: string, message: ClientMessage) {
|
|
126
|
+
log.meshHello("mesh message: %s from %s", (message as any).type, clientId.slice(0, 8));
|
|
127
|
+
|
|
125
128
|
if (message.type === "mesh:generate_invite") {
|
|
126
129
|
var genMsg = message as any as { type: "mesh:generate_invite"; address?: string };
|
|
127
130
|
var config = loadConfig();
|
|
@@ -232,14 +235,17 @@ registerHandler("mesh", function (clientId: string, message: ClientMessage) {
|
|
|
232
235
|
var hello = message as any as { type: "mesh:hello"; nodeId: string; name: string; publicKey?: string; token?: string; port?: number; addresses?: string[]; projects: Array<{ slug: string; title: string }> };
|
|
233
236
|
|
|
234
237
|
var knownPeer = hello.nodeId ? getPeer(hello.nodeId) : undefined;
|
|
238
|
+
log.meshHello("mesh:hello from nodeId=%s name=%s known=%s", hello.nodeId?.slice(0, 8), hello.name, !!knownPeer);
|
|
235
239
|
|
|
236
240
|
if (knownPeer) {
|
|
237
241
|
if (knownPeer.publicKey && hello.publicKey && knownPeer.publicKey !== hello.publicKey) {
|
|
242
|
+
log.meshHello(" ✗ public key mismatch for %s", hello.name);
|
|
238
243
|
sendTo(clientId, { type: "mesh:hello_rejected" as any, error: "Public key mismatch — possible impersonation" });
|
|
239
244
|
return;
|
|
240
245
|
}
|
|
241
246
|
|
|
242
247
|
var inboundWs = getClientWebSocket(clientId);
|
|
248
|
+
log.meshHello(" registering inbound connection for %s (ws=%s)", hello.name, !!inboundWs);
|
|
243
249
|
if (inboundWs) {
|
|
244
250
|
registerInboundPeer(hello.nodeId, inboundWs as any);
|
|
245
251
|
}
|
package/server/src/index.ts
CHANGED
|
@@ -224,6 +224,9 @@ function runHelp(): void {
|
|
|
224
224
|
console.log(" Environment:");
|
|
225
225
|
console.log(" LATTICE_HOME Data directory (default: ~/.lattice)");
|
|
226
226
|
console.log(" LATTICE_PORT Server port (default: 7654)");
|
|
227
|
+
console.log(" DEBUG Enable debug logging (e.g. DEBUG=lattice:*)");
|
|
228
|
+
console.log(" Scopes: server,ws,router,mesh,mesh:connect,mesh:hello,");
|
|
229
|
+
console.log(" mesh:proxy,broadcast,chat,session,plugins,update");
|
|
227
230
|
console.log("");
|
|
228
231
|
}
|
|
229
232
|
|
package/server/src/logger.ts
CHANGED
|
@@ -6,7 +6,16 @@ export var log = {
|
|
|
6
6
|
chat: createDebug("lattice:chat"),
|
|
7
7
|
session: createDebug("lattice:session"),
|
|
8
8
|
mesh: createDebug("lattice:mesh"),
|
|
9
|
+
meshConnect: createDebug("lattice:mesh:connect"),
|
|
10
|
+
meshHello: createDebug("lattice:mesh:hello"),
|
|
11
|
+
meshProxy: createDebug("lattice:mesh:proxy"),
|
|
12
|
+
router: createDebug("lattice:router"),
|
|
13
|
+
broadcast: createDebug("lattice:broadcast"),
|
|
9
14
|
auth: createDebug("lattice:auth"),
|
|
10
15
|
fs: createDebug("lattice:fs"),
|
|
11
16
|
analytics: createDebug("lattice:analytics"),
|
|
17
|
+
plugins: createDebug("lattice:plugins"),
|
|
18
|
+
update: createDebug("lattice:update"),
|
|
19
|
+
terminal: createDebug("lattice:terminal"),
|
|
20
|
+
settings: createDebug("lattice:settings"),
|
|
12
21
|
};
|
|
@@ -46,11 +46,18 @@ function reconcilePeers(): void {
|
|
|
46
46
|
var peers = loadPeers();
|
|
47
47
|
for (var i = 0; i < peers.length; i++) {
|
|
48
48
|
var peer = peers[i];
|
|
49
|
-
if (!peer.addresses || peer.addresses.length === 0)
|
|
49
|
+
if (!peer.addresses || peer.addresses.length === 0) {
|
|
50
|
+
log.meshConnect("skip %s — no addresses", peer.name);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
50
53
|
var existing = connections.get(peer.id);
|
|
51
54
|
if (existing && !existing.dead && existing.ws.readyState === WebSocket.OPEN) continue;
|
|
52
|
-
if (existing && !existing.dead && existing.retryTimer !== null)
|
|
55
|
+
if (existing && !existing.dead && existing.retryTimer !== null) {
|
|
56
|
+
log.meshConnect("skip %s — retry pending", peer.name);
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
53
59
|
if (!existing || existing.dead) {
|
|
60
|
+
log.meshConnect("connecting to %s at %s", peer.name, peer.addresses[0]);
|
|
54
61
|
connections.delete(peer.id);
|
|
55
62
|
connectToPeer(peer.id, peer.addresses[0]);
|
|
56
63
|
}
|
|
@@ -103,6 +110,7 @@ function openConnection(conn: PeerConnection, url: string): void {
|
|
|
103
110
|
var circuit = circuitBreakers.get(conn.nodeId);
|
|
104
111
|
if (circuit && circuit.failures >= CIRCUIT_BREAKER_THRESHOLD && !circuit.halfOpen) {
|
|
105
112
|
if (Date.now() < circuit.openUntil) {
|
|
113
|
+
log.meshConnect("circuit breaker open for %s, retry in %dms", conn.nodeId.slice(0, 8), circuit.openUntil - Date.now());
|
|
106
114
|
conn.retryTimer = setTimeout(function () {
|
|
107
115
|
if (conn.dead) return;
|
|
108
116
|
conn.retryTimer = null;
|
|
@@ -114,12 +122,13 @@ function openConnection(conn: PeerConnection, url: string): void {
|
|
|
114
122
|
circuit.halfOpen = true;
|
|
115
123
|
}
|
|
116
124
|
|
|
125
|
+
log.meshConnect("opening WebSocket to %s", url);
|
|
117
126
|
var ws = new WebSocket(url);
|
|
118
127
|
conn.ws = ws;
|
|
119
128
|
|
|
120
129
|
var connectionTimer = setTimeout(function () {
|
|
121
130
|
if (ws.readyState !== WebSocket.OPEN) {
|
|
122
|
-
log.
|
|
131
|
+
log.meshConnect("connection timeout for %s at %s", conn.nodeId.slice(0, 8), url);
|
|
123
132
|
ws.close();
|
|
124
133
|
}
|
|
125
134
|
}, CONNECTION_TIMEOUT_MS);
|
|
@@ -242,8 +251,10 @@ export function getPeerConnection(nodeId: string): WebSocket | undefined {
|
|
|
242
251
|
export function registerInboundPeer(nodeId: string, ws: { send: (data: string) => void; readyState: number }): void {
|
|
243
252
|
var existing = connections.get(nodeId);
|
|
244
253
|
if (existing && !existing.dead && existing.ws.readyState === WebSocket.OPEN) {
|
|
254
|
+
log.meshConnect("inbound peer %s already connected, skipping", nodeId.slice(0, 8));
|
|
245
255
|
return;
|
|
246
256
|
}
|
|
257
|
+
log.meshConnect("registering inbound peer %s", nodeId.slice(0, 8));
|
|
247
258
|
|
|
248
259
|
if (existing) {
|
|
249
260
|
existing.dead = true;
|
package/server/src/mesh/proxy.ts
CHANGED
|
@@ -3,19 +3,22 @@ import type { ClientMessage, MeshProxyRequestMessage, MeshProxyResponseMessage,
|
|
|
3
3
|
import { getPeerConnection } from "./connector";
|
|
4
4
|
import { sendTo, broadcast, registerVirtualClient, removeVirtualClient } from "../ws/broadcast";
|
|
5
5
|
import { routeMessage } from "../ws/router";
|
|
6
|
+
import { log } from "../logger";
|
|
6
7
|
|
|
7
8
|
var pendingRequests = new Map<string, string>();
|
|
8
9
|
|
|
9
10
|
export function proxyToRemoteNode(nodeId: string, projectSlug: string, clientId: string, message: ClientMessage): void {
|
|
11
|
+
log.meshProxy("→ proxy %s to node %s for project %s", (message as any).type, nodeId.slice(0, 8), projectSlug);
|
|
10
12
|
var ws = getPeerConnection(nodeId);
|
|
11
13
|
if (!ws) {
|
|
12
|
-
|
|
13
|
-
sendTo(clientId, { type: "chat:error", message: "Remote node
|
|
14
|
+
log.meshProxy(" ✗ no connection to node %s", nodeId.slice(0, 8));
|
|
15
|
+
sendTo(clientId, { type: "chat:error", message: "Remote node is not connected" });
|
|
14
16
|
return;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
var requestId = randomUUID();
|
|
18
20
|
pendingRequests.set(requestId, clientId);
|
|
21
|
+
log.meshProxy(" envelope requestId=%s", requestId.slice(0, 8));
|
|
19
22
|
|
|
20
23
|
var envelope: MeshProxyRequestMessage = {
|
|
21
24
|
type: "mesh:proxy_request",
|
|
@@ -29,8 +32,10 @@ export function proxyToRemoteNode(nodeId: string, projectSlug: string, clientId:
|
|
|
29
32
|
|
|
30
33
|
export function handleProxyRequest(sourceNodeId: string, msg: MeshProxyRequestMessage): void {
|
|
31
34
|
var proxyClientId = "mesh-proxy:" + sourceNodeId + ":" + msg.requestId;
|
|
35
|
+
log.meshProxy("← proxy_request from %s: %s for %s (reqId=%s)", sourceNodeId.slice(0, 8), (msg.payload as any).type, msg.projectSlug, msg.requestId.slice(0, 8));
|
|
32
36
|
|
|
33
37
|
registerVirtualClient(proxyClientId, function (response: object) {
|
|
38
|
+
log.meshProxy(" → proxy_response %s back to %s", (response as any).type, sourceNodeId.slice(0, 8));
|
|
34
39
|
var ws = getPeerConnection(sourceNodeId);
|
|
35
40
|
if (!ws) {
|
|
36
41
|
console.warn("[mesh/proxy] Cannot send response, no connection to: " + sourceNodeId);
|
|
@@ -53,9 +58,10 @@ export function handleProxyRequest(sourceNodeId: string, msg: MeshProxyRequestMe
|
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
export function handleProxyResponse(msg: MeshProxyResponseMessage): void {
|
|
61
|
+
log.meshProxy("← proxy_response %s (reqId=%s)", (msg.payload as any).type, msg.requestId.slice(0, 8));
|
|
56
62
|
var clientId = pendingRequests.get(msg.requestId);
|
|
57
63
|
if (!clientId) {
|
|
58
|
-
|
|
64
|
+
log.meshProxy(" ✗ no pending request for %s", msg.requestId.slice(0, 8));
|
|
59
65
|
return;
|
|
60
66
|
}
|
|
61
67
|
|
|
@@ -37,6 +37,11 @@ export function sendTo(id: string, message: object): void {
|
|
|
37
37
|
var virtualHandler = virtualSendHandlers.get(id);
|
|
38
38
|
if (virtualHandler) {
|
|
39
39
|
virtualHandler(message);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (id.startsWith("mesh-proxy:")) {
|
|
43
|
+
var { log } = require("../logger");
|
|
44
|
+
log.broadcast(" ✗ sendTo %s but no virtual handler registered (msg=%s)", id.slice(0, 30), (message as any).type);
|
|
40
45
|
}
|
|
41
46
|
}
|
|
42
47
|
|
package/server/src/ws/router.ts
CHANGED
|
@@ -53,6 +53,8 @@ export function getClientRemoteNode(clientId: string): { nodeId: string; project
|
|
|
53
53
|
export function routeMessage(clientId: string, message: ClientMessage): void {
|
|
54
54
|
var prefix = message.type.split(":")[0];
|
|
55
55
|
|
|
56
|
+
log.router("→ %s from client %s (prefix=%s)", message.type, clientId.slice(0, 8), prefix);
|
|
57
|
+
|
|
56
58
|
if (PROXIED_PREFIXES.has(prefix)) {
|
|
57
59
|
var remote = clientRemoteNode.get(clientId);
|
|
58
60
|
|
|
@@ -60,17 +62,21 @@ export function routeMessage(clientId: string, message: ClientMessage): void {
|
|
|
60
62
|
|
|
61
63
|
if (msgSlug) {
|
|
62
64
|
var localProject = getLocalProject(msgSlug);
|
|
65
|
+
log.router(" slug=%s local=%s", msgSlug, localProject);
|
|
63
66
|
if (!localProject) {
|
|
64
67
|
var remoteEntry = getRemoteNodeForProject(msgSlug);
|
|
65
68
|
if (remoteEntry) {
|
|
69
|
+
log.router(" → proxying to remote node %s for project %s", remoteEntry.nodeId.slice(0, 8), msgSlug);
|
|
66
70
|
setClientRemoteNode(clientId, remoteEntry.nodeId, msgSlug);
|
|
67
71
|
proxyMessage(clientId, remoteEntry.nodeId, msgSlug, message);
|
|
68
72
|
return;
|
|
69
73
|
}
|
|
74
|
+
log.router(" ✗ no remote node found for slug %s", msgSlug);
|
|
70
75
|
} else if (message.type === "session:activate" || message.type === "session:list_request") {
|
|
71
76
|
clearClientRemoteNode(clientId);
|
|
72
77
|
}
|
|
73
78
|
} else if (remote) {
|
|
79
|
+
log.router(" → proxying via cached remote node %s", remote.nodeId.slice(0, 8));
|
|
74
80
|
proxyMessage(clientId, remote.nodeId, remote.projectSlug, message);
|
|
75
81
|
return;
|
|
76
82
|
}
|
|
@@ -78,6 +84,7 @@ export function routeMessage(clientId: string, message: ClientMessage): void {
|
|
|
78
84
|
|
|
79
85
|
var handler = handlers.get(prefix);
|
|
80
86
|
if (handler) {
|
|
87
|
+
log.router(" → dispatching to %s handler", prefix);
|
|
81
88
|
try {
|
|
82
89
|
var result = handler(clientId, message);
|
|
83
90
|
if (result && typeof result.then === "function") {
|
|
@@ -94,7 +101,7 @@ export function routeMessage(clientId: string, message: ClientMessage): void {
|
|
|
94
101
|
}
|
|
95
102
|
return;
|
|
96
103
|
}
|
|
97
|
-
log.
|
|
104
|
+
log.router(" ✗ no handler for %s", message.type);
|
|
98
105
|
sendTo(clientId, { type: "error", message: `Unknown message type: ${message.type}` });
|
|
99
106
|
}
|
|
100
107
|
|