@cryptiklemur/lattice 1.41.0 → 1.41.2
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 +1 -1
- package/server/src/project/sdk-bridge.ts +46 -75
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cryptiklemur/lattice",
|
|
3
|
-
"version": "1.41.
|
|
3
|
+
"version": "1.41.2",
|
|
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,7 +4,7 @@ import type { SDKMessage, SDKPartialAssistantMessage, SDKResultMessage, SDKUserM
|
|
|
4
4
|
import type { CanUseTool, PermissionMode, PermissionResult, PermissionUpdate } from "@anthropic-ai/claude-agent-sdk";
|
|
5
5
|
type MessageParam = SDKUserMessage["message"];
|
|
6
6
|
import type { Attachment } from "@lattice/shared";
|
|
7
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync } from "node:fs";
|
|
8
8
|
import { join, resolve } from "node:path";
|
|
9
9
|
import { homedir } from "node:os";
|
|
10
10
|
import { sendTo, broadcast } from "../ws/broadcast";
|
|
@@ -96,22 +96,21 @@ export function addRemoteSessionWatcher(sessionId: string, nodeId: string): void
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
export function getBusyOwner(sessionId: string): "cli" | "lattice" | undefined {
|
|
99
|
-
if (
|
|
100
|
-
return "cli";
|
|
99
|
+
if (activeStreams.has(sessionId)) return "lattice";
|
|
100
|
+
if (isSessionLockedByExternal(sessionId)) return "cli";
|
|
101
|
+
return undefined;
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
// Poll every 3 seconds for external lock changes
|
|
104
105
|
setInterval(function () {
|
|
105
106
|
for (var sessionId of watchedSessions) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
var locked = isSessionLockedByExternal(sessionId);
|
|
107
|
+
var busy = isSessionBusy(sessionId);
|
|
109
108
|
var prev = externalLockState.get(sessionId) ?? false;
|
|
110
109
|
|
|
111
|
-
if (
|
|
112
|
-
externalLockState.set(sessionId,
|
|
113
|
-
var owner =
|
|
114
|
-
broadcast({ type: "session:busy", sessionId, busy:
|
|
110
|
+
if (busy !== prev) {
|
|
111
|
+
externalLockState.set(sessionId, busy);
|
|
112
|
+
var owner = busy ? getBusyOwner(sessionId) : undefined;
|
|
113
|
+
broadcast({ type: "session:busy", sessionId, busy: busy, busyOwner: owner });
|
|
115
114
|
|
|
116
115
|
var watchers = remoteSessionWatchers.get(sessionId);
|
|
117
116
|
if (watchers) {
|
|
@@ -123,7 +122,7 @@ setInterval(function () {
|
|
|
123
122
|
type: "mesh:proxy_response",
|
|
124
123
|
projectSlug: "",
|
|
125
124
|
requestId: "busy-" + sessionId,
|
|
126
|
-
payload: { type: "session:busy", sessionId, busy:
|
|
125
|
+
payload: { type: "session:busy", sessionId, busy: busy, busyOwner: owner },
|
|
127
126
|
}));
|
|
128
127
|
}
|
|
129
128
|
}
|
|
@@ -219,6 +218,7 @@ export function getActiveStreamCount(): number {
|
|
|
219
218
|
* so this ONLY returns true for external CLI instances.
|
|
220
219
|
*/
|
|
221
220
|
export function isSessionBusy(sessionId: string): boolean {
|
|
221
|
+
if (activeStreams.has(sessionId)) return true;
|
|
222
222
|
return isSessionLockedByExternal(sessionId);
|
|
223
223
|
}
|
|
224
224
|
|
|
@@ -227,77 +227,48 @@ export function isSessionBusy(sessionId: string): boolean {
|
|
|
227
227
|
* The SDK spawns child processes (e.g. claude-agent-sdk/cli.js) that hold
|
|
228
228
|
* lock files — those are NOT external.
|
|
229
229
|
*/
|
|
230
|
-
function
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
var
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
return false;
|
|
230
|
+
function isSessionLockedByExternal(sessionId: string): boolean {
|
|
231
|
+
if (activeStreams.has(sessionId)) return false;
|
|
232
|
+
|
|
233
|
+
var config = loadConfig();
|
|
234
|
+
for (var i = 0; i < config.projects.length; i++) {
|
|
235
|
+
var projectPath = config.projects[i].path;
|
|
236
|
+
var hash = projectPath.replace(/\//g, "-");
|
|
237
|
+
var jsonlPath = join(homedir(), ".claude", "projects", hash, sessionId + ".jsonl");
|
|
238
|
+
if (existsSync(jsonlPath)) {
|
|
239
|
+
try {
|
|
240
|
+
var stat = statSync(jsonlPath);
|
|
241
|
+
var mtime = stat.mtimeMs;
|
|
242
|
+
if (Date.now() - mtime < 10000) {
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
} catch {}
|
|
247
246
|
}
|
|
248
247
|
}
|
|
249
|
-
return false;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/**
|
|
253
|
-
* Get PIDs holding the session lock file, excluding Lattice's own process tree.
|
|
254
|
-
* Returns the list of truly external PIDs.
|
|
255
|
-
*/
|
|
256
|
-
function getExternalLockPids(sessionId: string): number[] {
|
|
257
|
-
var lockPath = join(homedir(), ".claude", "tasks", sessionId, ".lock");
|
|
258
|
-
if (!existsSync(lockPath)) return [];
|
|
259
|
-
try {
|
|
260
|
-
var result = Bun.spawnSync(["fuser", lockPath], {
|
|
261
|
-
stderr: "ignore",
|
|
262
|
-
});
|
|
263
|
-
if (result.exitCode !== 0) return [];
|
|
264
|
-
var output = result.stdout.toString().trim();
|
|
265
|
-
var pids = output.split(/\s+/)
|
|
266
|
-
.map(function (s) { return parseInt(s, 10); })
|
|
267
|
-
.filter(function (p) { return !isNaN(p) && !isOwnProcess(p); });
|
|
268
|
-
return pids;
|
|
269
|
-
} catch {
|
|
270
|
-
return [];
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
248
|
|
|
274
|
-
|
|
275
|
-
return getExternalLockPids(sessionId).length > 0;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Get the first external PID holding the session lock file.
|
|
280
|
-
* Used to send SIGINT to stop the external process.
|
|
281
|
-
*/
|
|
282
|
-
function getExternalLockPid(sessionId: string): number | null {
|
|
283
|
-
var pids = getExternalLockPids(sessionId);
|
|
284
|
-
return pids.length > 0 ? pids[0] : null;
|
|
249
|
+
return false;
|
|
285
250
|
}
|
|
286
251
|
|
|
287
|
-
/**
|
|
288
|
-
* Gracefully stop an external Claude Code CLI process controlling a session.
|
|
289
|
-
* Sends SIGINT which triggers Claude Code's graceful shutdown.
|
|
290
|
-
* Returns true if a signal was sent.
|
|
291
|
-
*/
|
|
292
252
|
export function stopExternalSession(sessionId: string): boolean {
|
|
293
|
-
var pid = getExternalLockPid(sessionId);
|
|
294
|
-
if (pid === null) return false;
|
|
295
253
|
try {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
254
|
+
var config = loadConfig();
|
|
255
|
+
for (var i = 0; i < config.projects.length; i++) {
|
|
256
|
+
var hash = config.projects[i].path.replace(/\//g, "-");
|
|
257
|
+
var jsonlPath = join(homedir(), ".claude", "projects", hash, sessionId + ".jsonl");
|
|
258
|
+
if (existsSync(jsonlPath)) {
|
|
259
|
+
var result = Bun.spawnSync(["fuser", jsonlPath], { stderr: "ignore" });
|
|
260
|
+
if (result.exitCode === 0) {
|
|
261
|
+
var output = result.stdout.toString().trim();
|
|
262
|
+
var pids = output.split(/\s+/).map(Number).filter(function (p) { return !isNaN(p) && p !== process.pid; });
|
|
263
|
+
if (pids.length > 0) {
|
|
264
|
+
process.kill(pids[0], "SIGINT");
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
} catch {}
|
|
271
|
+
return false;
|
|
301
272
|
}
|
|
302
273
|
|
|
303
274
|
export function getSessionStreamClientId(sessionId: string): string | undefined {
|