@cryptiklemur/lattice 1.41.1 → 1.41.3

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.41.1",
3
+ "version": "1.41.3",
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, readdirSync, readlinkSync } 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";
@@ -227,77 +227,117 @@ 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 isOwnProcess(pid: number): boolean {
231
- var myPid = process.pid;
232
- if (pid === myPid) return true;
233
- // Walk up the process tree to see if pid is a descendant of us
234
- var current = pid;
235
- for (var i = 0; i < 10; i++) {
236
- try {
237
- var stat = readFileSync("/proc/" + current + "/stat", "utf-8");
238
- // Format: pid (comm) state ppid ...
239
- var match = stat.match(/^\d+\s+\([^)]*\)\s+\S+\s+(\d+)/);
240
- if (!match) return false;
241
- var ppid = parseInt(match[1], 10);
242
- if (ppid === myPid) return true;
243
- if (ppid <= 1) return false;
244
- current = ppid;
245
- } catch {
246
- return false;
247
- }
230
+ function getProjectPathForSession(sessionId: string): string | null {
231
+ var config = loadConfig();
232
+ for (var i = 0; i < config.projects.length; i++) {
233
+ var hash = config.projects[i].path.replace(/\//g, "-");
234
+ var jsonlPath = join(homedir(), ".claude", "projects", hash, sessionId + ".jsonl");
235
+ if (existsSync(jsonlPath)) return config.projects[i].path;
248
236
  }
249
- return false;
237
+ return null;
250
238
  }
251
239
 
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 [];
240
+ function getClaudeCliPids(): Array<{ pid: number; cwd: string; cmdline: string[] }> {
241
+ var results: Array<{ pid: number; cwd: string; cmdline: string[] }> = [];
259
242
  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 [];
243
+ var result = Bun.spawnSync(["pgrep", "-x", "claude"], { stderr: "ignore" });
244
+ if (result.exitCode !== 0) return results;
245
+ var pidStrs = result.stdout.toString().trim().split("\n");
246
+ for (var i = 0; i < pidStrs.length; i++) {
247
+ var pid = parseInt(pidStrs[i], 10);
248
+ if (isNaN(pid) || pid === process.pid) continue;
249
+ try {
250
+ var cwd = readlinkSync("/proc/" + pid + "/cwd");
251
+ var cmdline = readFileSync("/proc/" + pid + "/cmdline", "utf-8").split("\0");
252
+ results.push({ pid, cwd, cmdline });
253
+ } catch {}
254
+ }
255
+ } catch {}
256
+ return results;
257
+ }
258
+
259
+ function resolveSessionName(projectPath: string, name: string): string | null {
260
+ var hash = projectPath.replace(/\//g, "-");
261
+ var dir = join(homedir(), ".claude", "projects", hash);
262
+ if (!existsSync(dir)) return null;
263
+
264
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(name)) {
265
+ if (existsSync(join(dir, name + ".jsonl"))) return name;
266
+ }
267
+
268
+ var entries = readdirSync(dir).filter(function (f) { return f.endsWith(".jsonl"); });
269
+ for (var e = 0; e < entries.length; e++) {
270
+ try {
271
+ var result = Bun.spawnSync(["grep", "-m", "1", "custom-title", join(dir, entries[e])], { stdout: "pipe", stderr: "ignore" });
272
+ if (result.exitCode !== 0) continue;
273
+ var line = result.stdout.toString().trim();
274
+ if (!line) continue;
275
+ var parsed = JSON.parse(line);
276
+ if (parsed.type === "custom-title" && parsed.customTitle === name) {
277
+ return entries[e].replace(".jsonl", "");
278
+ }
279
+ } catch {}
271
280
  }
281
+ return null;
272
282
  }
273
283
 
274
- function isSessionLockedByExternal(sessionId: string): boolean {
275
- return getExternalLockPids(sessionId).length > 0;
284
+ function findMostRecentSession(projectPath: string): string | null {
285
+ var hash = projectPath.replace(/\//g, "-");
286
+ var dir = join(homedir(), ".claude", "projects", hash);
287
+ if (!existsSync(dir)) return null;
288
+
289
+ var entries = readdirSync(dir).filter(function (f) { return f.endsWith(".jsonl"); });
290
+ var latest: { id: string; mtime: number } | null = null;
291
+ for (var e = 0; e < entries.length; e++) {
292
+ try {
293
+ var s = statSync(join(dir, entries[e]));
294
+ if (!latest || s.mtimeMs > latest.mtime) {
295
+ latest = { id: entries[e].replace(".jsonl", ""), mtime: s.mtimeMs };
296
+ }
297
+ } catch {}
298
+ }
299
+ if (latest && Date.now() - latest.mtime < 60000) return latest.id;
300
+ return null;
276
301
  }
277
302
 
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;
303
+ function getCliSessionIdForProject(projectPath: string): string | null {
304
+ var cliProcesses = getClaudeCliPids();
305
+ for (var i = 0; i < cliProcesses.length; i++) {
306
+ if (cliProcesses[i].cwd !== projectPath) continue;
307
+
308
+ var cmdline = cliProcesses[i].cmdline;
309
+ var resumeIdx = cmdline.indexOf("--resume");
310
+ if (resumeIdx !== -1 && resumeIdx + 1 < cmdline.length) {
311
+ var sessionName = cmdline[resumeIdx + 1];
312
+ return resolveSessionName(projectPath, sessionName);
313
+ }
314
+
315
+ return findMostRecentSession(projectPath);
316
+ }
317
+ return null;
318
+ }
319
+
320
+ function isSessionLockedByExternal(sessionId: string): boolean {
321
+ if (activeStreams.has(sessionId)) return false;
322
+ var projectPath = getProjectPathForSession(sessionId);
323
+ if (!projectPath) return false;
324
+ var cliSessionId = getCliSessionIdForProject(projectPath);
325
+ return cliSessionId === sessionId;
285
326
  }
286
327
 
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
328
  export function stopExternalSession(sessionId: string): boolean {
293
- var pid = getExternalLockPid(sessionId);
294
- if (pid === null) return false;
295
- try {
296
- process.kill(pid, "SIGINT");
297
- return true;
298
- } catch {
299
- return false;
329
+ var projectPath = getProjectPathForSession(sessionId);
330
+ if (!projectPath) return false;
331
+ var cliProcesses = getClaudeCliPids();
332
+ for (var i = 0; i < cliProcesses.length; i++) {
333
+ if (cliProcesses[i].cwd === projectPath) {
334
+ try {
335
+ process.kill(cliProcesses[i].pid, "SIGINT");
336
+ return true;
337
+ } catch {}
338
+ }
300
339
  }
340
+ return false;
301
341
  }
302
342
 
303
343
  export function getSessionStreamClientId(sessionId: string): string | undefined {