@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.38.0",
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
  }
@@ -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
 
@@ -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) continue;
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) continue;
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.mesh("Connection timeout for peer: %s", conn.nodeId);
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;
@@ -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
- console.warn("[mesh/proxy] No connection to peer: " + nodeId);
13
- sendTo(clientId, { type: "chat:error", message: "Remote node " + nodeId + " is not connected" });
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
- console.warn("[mesh/proxy] No pending request for id: " + msg.requestId);
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
 
@@ -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.ws("No handler for message type: %s", message.type);
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