@cryptiklemur/lattice 1.43.0 → 1.43.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.
@@ -295,7 +295,7 @@ export function SessionList(props: SessionListProps) {
295
295
  if (props.projectSlug && ws.status === "connected") {
296
296
  sendRef.current({ type: "session:list_request", projectSlug: props.projectSlug, offset: 0, limit: PAGE_SIZE });
297
297
  }
298
- }, 10000);
298
+ }, 30000);
299
299
  return function () { clearInterval(interval); };
300
300
  }
301
301
  }, [props.projectSlug, ws.status]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cryptiklemur/lattice",
3
- "version": "1.43.0",
3
+ "version": "1.43.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>",
@@ -3,6 +3,7 @@ import type {
3
3
  SessionActivateMessage,
4
4
  SessionCreateMessage,
5
5
  SessionDeleteMessage,
6
+ SessionSummary,
6
7
  SessionListRequestMessage,
7
8
  SessionPreviewRequestMessage,
8
9
  SessionRenameMessage,
@@ -18,6 +19,7 @@ import {
18
19
  getSessionTitle,
19
20
  getSessionUsage,
20
21
  listSessions,
22
+ invalidateSessionCache,
21
23
  loadSessionHistory,
22
24
  renameSession,
23
25
  } from "../project/session";
@@ -30,16 +32,14 @@ import { log } from "../logger";
30
32
  registerHandler("session", function (clientId: string, message: ClientMessage) {
31
33
  if (message.type === "session:list_request") {
32
34
  var listReqMsg = message as SessionListRequestMessage;
33
- void listSessions(listReqMsg.projectSlug).then(function (sessions) {
34
- var offset = listReqMsg.offset || 0;
35
- var limit = listReqMsg.limit || 0;
36
- var totalCount = sessions.length;
37
- var sliced = limit > 0 ? sessions.slice(offset, offset + limit) : sessions;
35
+ var offset = listReqMsg.offset || 0;
36
+ var limit = listReqMsg.limit || 0;
37
+ void listSessions(listReqMsg.projectSlug, { offset, limit }).then(function (result) {
38
38
  sendTo(clientId, {
39
39
  type: "session:list",
40
40
  projectSlug: listReqMsg.projectSlug,
41
- sessions: sliced,
42
- totalCount,
41
+ sessions: result.sessions,
42
+ totalCount: result.totalCount,
43
43
  offset,
44
44
  });
45
45
  });
@@ -59,16 +59,16 @@ registerHandler("session", function (clientId: string, message: ClientMessage) {
59
59
  if (message.type === "session:list_all_request") {
60
60
  var config = loadConfig();
61
61
  var allPromises = config.projects.map(function (p: typeof config.projects[number]) {
62
- return listSessions(p.slug);
62
+ return listSessions(p.slug, { limit: 20 });
63
63
  });
64
64
  void Promise.all(allPromises).then(function (results) {
65
- var merged: typeof results[0] = [];
65
+ var merged: SessionSummary[] = [];
66
66
  for (var i = 0; i < results.length; i++) {
67
- for (var j = 0; j < results[i].length; j++) {
68
- merged.push(results[i][j]);
67
+ for (var j = 0; j < results[i].sessions.length; j++) {
68
+ merged.push(results[i].sessions[j]);
69
69
  }
70
70
  }
71
- merged.sort(function (a: typeof merged[number], b: typeof merged[number]) { return b.updatedAt - a.updatedAt; });
71
+ merged.sort(function (a, b) { return b.updatedAt - a.updatedAt; });
72
72
  sendTo(clientId, {
73
73
  type: "session:list_all",
74
74
  sessions: merged.slice(0, 20),
@@ -171,11 +171,13 @@ registerHandler("session", function (clientId: string, message: ClientMessage) {
171
171
  return;
172
172
  }
173
173
  void renameSession(projectSlug, renameMsg.sessionId, renameMsg.title).then(function () {
174
- void listSessions(projectSlug).then(function (sessions) {
174
+ invalidateSessionCache(projectSlug);
175
+ void listSessions(projectSlug, { limit: 40 }).then(function (result) {
175
176
  sendTo(clientId, {
176
177
  type: "session:list",
177
178
  projectSlug,
178
- sessions,
179
+ sessions: result.sessions,
180
+ totalCount: result.totalCount,
179
181
  });
180
182
  });
181
183
  });
@@ -191,11 +193,13 @@ registerHandler("session", function (clientId: string, message: ClientMessage) {
191
193
  return;
192
194
  }
193
195
  void deleteSession(deleteProjectSlug, deleteMsg.sessionId).then(function () {
194
- void listSessions(deleteProjectSlug).then(function (sessions) {
196
+ invalidateSessionCache(deleteProjectSlug);
197
+ void listSessions(deleteProjectSlug, { limit: 40 }).then(function (result) {
195
198
  sendTo(clientId, {
196
199
  type: "session:list",
197
200
  projectSlug: deleteProjectSlug,
198
- sessions,
201
+ sessions: result.sessions,
202
+ totalCount: result.totalCount,
199
203
  });
200
204
  });
201
205
  });
@@ -11,7 +11,7 @@ import { sendTo, broadcast } from "../ws/broadcast";
11
11
  import { syncSessionToPeers } from "../mesh/session-sync";
12
12
  import { resolveSkillContent } from "../handlers/skills";
13
13
  import { getPluginMcpServers } from "../handlers/plugins";
14
- import { guessContextWindow, getSessionTitle, renameSession, listSessions } from "./session";
14
+ import { guessContextWindow, getSessionTitle, renameSession, listSessions, invalidateSessionCache } from "./session";
15
15
  import { getLatticeHome, loadConfig } from "../config";
16
16
  import { log } from "../logger";
17
17
  import { getDailySpend, invalidateDailySpendCache } from "../analytics/engine";
@@ -221,32 +221,11 @@ export function getActiveSessionCountForProject(projectPath: string): number {
221
221
  if (existsSync(join(dir, sessionId + ".jsonl"))) count++;
222
222
  }
223
223
 
224
- for (var [sessionId2] of streamMetadata) {
225
- void sessionId2;
226
- }
227
-
228
- if (isClaudeCliRunningInProject(projectPath)) count++;
224
+ if (cliSessionsByProject.get(projectPath) !== null && cliSessionsByProject.get(projectPath) !== undefined) count++;
229
225
 
230
226
  return count;
231
227
  }
232
228
 
233
- function isClaudeCliRunningInProject(projectPath: string): boolean {
234
- try {
235
- var result = Bun.spawnSync(["pgrep", "-x", "claude"], { stderr: "ignore" });
236
- if (result.exitCode !== 0) return false;
237
- var pids = result.stdout.toString().trim().split("\n");
238
- for (var i = 0; i < pids.length; i++) {
239
- var pid = parseInt(pids[i], 10);
240
- if (isNaN(pid) || pid === process.pid) continue;
241
- try {
242
- var cwd = readlinkSync("/proc/" + pid + "/cwd");
243
- if (cwd === projectPath) return true;
244
- } catch {}
245
- }
246
- } catch {}
247
- return false;
248
- }
249
-
250
229
  /**
251
230
  * Check if a session is controlled by an external process (not Lattice).
252
231
  * Lattice's own active streams are handled by isProcessing on the client,
@@ -262,28 +241,42 @@ export function isSessionBusy(sessionId: string): boolean {
262
241
  * The SDK spawns child processes (e.g. claude-agent-sdk/cli.js) that hold
263
242
  * lock files — those are NOT external.
264
243
  */
265
- function getProjectPathForSession(sessionId: string): string | null {
244
+ var cliSessionsByProject = new Map<string, string | null>();
245
+ var sessionNameCache = new Map<string, string>();
246
+
247
+ function refreshCliDetection(): void {
266
248
  var config = loadConfig();
249
+ var cliPids = getClaudeCliPidsAsync();
267
250
  for (var i = 0; i < config.projects.length; i++) {
268
- var hash = config.projects[i].path.replace(/\//g, "-");
269
- var jsonlPath = join(homedir(), ".claude", "projects", hash, sessionId + ".jsonl");
270
- if (existsSync(jsonlPath)) return config.projects[i].path;
251
+ var projectPath = config.projects[i].path;
252
+ var found: string | null = null;
253
+ for (var j = 0; j < cliPids.length; j++) {
254
+ if (cliPids[j].cwd !== projectPath) continue;
255
+ var cmdline = cliPids[j].cmdline;
256
+ var resumeIdx = cmdline.indexOf("--resume");
257
+ if (resumeIdx !== -1 && resumeIdx + 1 < cmdline.length) {
258
+ found = resolveSessionName(projectPath, cmdline[resumeIdx + 1]);
259
+ } else {
260
+ found = findMostRecentSession(projectPath);
261
+ }
262
+ break;
263
+ }
264
+ cliSessionsByProject.set(projectPath, found);
271
265
  }
272
- return null;
273
266
  }
274
267
 
275
- function getClaudeCliPids(): Array<{ pid: number; cwd: string; cmdline: string[] }> {
268
+ function getClaudeCliPidsAsync(): Array<{ pid: number; cwd: string; cmdline: string[] }> {
276
269
  var results: Array<{ pid: number; cwd: string; cmdline: string[] }> = [];
277
270
  try {
278
- var result = Bun.spawnSync(["pgrep", "-x", "claude"], { stderr: "ignore" });
279
- if (result.exitCode !== 0) return results;
280
- var pidStrs = result.stdout.toString().trim().split("\n");
281
- for (var i = 0; i < pidStrs.length; i++) {
282
- var pid = parseInt(pidStrs[i], 10);
283
- if (isNaN(pid) || pid === process.pid) continue;
271
+ var procEntries = readdirSync("/proc").filter(function (e) { return /^\d+$/.test(e); });
272
+ for (var i = 0; i < procEntries.length; i++) {
273
+ var pid = parseInt(procEntries[i], 10);
274
+ if (pid === process.pid) continue;
284
275
  try {
285
- var cwd = readlinkSync("/proc/" + pid + "/cwd");
286
276
  var cmdline = readFileSync("/proc/" + pid + "/cmdline", "utf-8").split("\0");
277
+ var exe = cmdline[0] || "";
278
+ if (!exe.endsWith("/claude") && exe !== "claude") continue;
279
+ var cwd = readlinkSync("/proc/" + pid + "/cwd");
287
280
  results.push({ pid, cwd, cmdline });
288
281
  } catch {}
289
282
  }
@@ -291,25 +284,50 @@ function getClaudeCliPids(): Array<{ pid: number; cwd: string; cmdline: string[]
291
284
  return results;
292
285
  }
293
286
 
287
+ setInterval(refreshCliDetection, 5000);
288
+ setTimeout(refreshCliDetection, 1000);
289
+
290
+ function getProjectPathForSession(sessionId: string): string | null {
291
+ var config = loadConfig();
292
+ for (var i = 0; i < config.projects.length; i++) {
293
+ var hash = config.projects[i].path.replace(/\//g, "-");
294
+ var jsonlPath = join(homedir(), ".claude", "projects", hash, sessionId + ".jsonl");
295
+ if (existsSync(jsonlPath)) return config.projects[i].path;
296
+ }
297
+ return null;
298
+ }
299
+
300
+
294
301
  function resolveSessionName(projectPath: string, name: string): string | null {
302
+ var cacheKey = projectPath + ":" + name;
303
+ var cached = sessionNameCache.get(cacheKey);
304
+ if (cached) return cached;
305
+
295
306
  var hash = projectPath.replace(/\//g, "-");
296
307
  var dir = join(homedir(), ".claude", "projects", hash);
297
308
  if (!existsSync(dir)) return null;
298
309
 
299
310
  if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(name)) {
300
- if (existsSync(join(dir, name + ".jsonl"))) return name;
311
+ if (existsSync(join(dir, name + ".jsonl"))) {
312
+ sessionNameCache.set(cacheKey, name);
313
+ return name;
314
+ }
301
315
  }
302
316
 
303
317
  var entries = readdirSync(dir).filter(function (f) { return f.endsWith(".jsonl"); });
304
318
  for (var e = 0; e < entries.length; e++) {
305
319
  try {
306
- var result = Bun.spawnSync(["grep", "-m", "1", "custom-title", join(dir, entries[e])], { stdout: "pipe", stderr: "ignore" });
307
- if (result.exitCode !== 0) continue;
308
- var line = result.stdout.toString().trim();
309
- if (!line) continue;
320
+ var content = readFileSync(join(dir, entries[e]), "utf-8");
321
+ var titleIdx = content.indexOf('"custom-title"');
322
+ if (titleIdx === -1) continue;
323
+ var lineStart = content.lastIndexOf("\n", titleIdx) + 1;
324
+ var lineEnd = content.indexOf("\n", titleIdx);
325
+ var line = content.slice(lineStart, lineEnd === -1 ? undefined : lineEnd);
310
326
  var parsed = JSON.parse(line);
311
327
  if (parsed.type === "custom-title" && parsed.customTitle === name) {
312
- return entries[e].replace(".jsonl", "");
328
+ var id = entries[e].replace(".jsonl", "");
329
+ sessionNameCache.set(cacheKey, id);
330
+ return id;
313
331
  }
314
332
  } catch {}
315
333
  }
@@ -335,39 +353,22 @@ function findMostRecentSession(projectPath: string): string | null {
335
353
  return null;
336
354
  }
337
355
 
338
- function getCliSessionIdForProject(projectPath: string): string | null {
339
- var cliProcesses = getClaudeCliPids();
340
- for (var i = 0; i < cliProcesses.length; i++) {
341
- if (cliProcesses[i].cwd !== projectPath) continue;
342
-
343
- var cmdline = cliProcesses[i].cmdline;
344
- var resumeIdx = cmdline.indexOf("--resume");
345
- if (resumeIdx !== -1 && resumeIdx + 1 < cmdline.length) {
346
- var sessionName = cmdline[resumeIdx + 1];
347
- return resolveSessionName(projectPath, sessionName);
348
- }
349
-
350
- return findMostRecentSession(projectPath);
351
- }
352
- return null;
353
- }
354
356
 
355
357
  function isSessionLockedByExternal(sessionId: string): boolean {
356
358
  if (activeStreams.has(sessionId)) return false;
357
359
  var projectPath = getProjectPathForSession(sessionId);
358
360
  if (!projectPath) return false;
359
- var cliSessionId = getCliSessionIdForProject(projectPath);
360
- return cliSessionId === sessionId;
361
+ return cliSessionsByProject.get(projectPath) === sessionId;
361
362
  }
362
363
 
363
364
  export function stopExternalSession(sessionId: string): boolean {
364
365
  var projectPath = getProjectPathForSession(sessionId);
365
366
  if (!projectPath) return false;
366
- var cliProcesses = getClaudeCliPids();
367
- for (var i = 0; i < cliProcesses.length; i++) {
368
- if (cliProcesses[i].cwd === projectPath) {
367
+ var pids = getClaudeCliPidsAsync();
368
+ for (var i = 0; i < pids.length; i++) {
369
+ if (pids[i].cwd === projectPath) {
369
370
  try {
370
- process.kill(cliProcesses[i].pid, "SIGINT");
371
+ process.kill(pids[i].pid, "SIGINT");
371
372
  return true;
372
373
  } catch {}
373
374
  }
@@ -1032,8 +1033,9 @@ export function startChatStream(options: ChatStreamOptions): void {
1032
1033
  void renameSession(projectSlug, sessionId, newTitle).then(function (ok) {
1033
1034
  if (!ok) return;
1034
1035
  log.session("Auto-titled session %s: %s", sessionId, newTitle);
1035
- void listSessions(projectSlug).then(function (sessions) {
1036
- broadcast({ type: "session:list", projectSlug, sessions });
1036
+ invalidateSessionCache(projectSlug);
1037
+ void listSessions(projectSlug, { limit: 40 }).then(function (result) {
1038
+ broadcast({ type: "session:list", projectSlug, sessions: result.sessions, totalCount: result.totalCount });
1037
1039
  });
1038
1040
  });
1039
1041
  });
@@ -404,25 +404,47 @@ export async function getSessionPreview(projectSlug: string, sessionId: string):
404
404
  }
405
405
  }
406
406
 
407
- export async function listSessions(projectSlug: string): Promise<SessionSummary[]> {
407
+ var sessionListCache = new Map<string, { sessions: SessionSummary[]; time: number }>();
408
+ var SESSION_CACHE_TTL = 5000;
409
+
410
+ export async function listSessions(projectSlug: string, options?: { offset?: number; limit?: number; noCache?: boolean }): Promise<{ sessions: SessionSummary[]; totalCount: number }> {
408
411
  var projectPath = getProjectPath(projectSlug);
409
412
  if (!projectPath) {
410
- return [];
413
+ return { sessions: [], totalCount: 0 };
414
+ }
415
+
416
+ var cached = sessionListCache.get(projectSlug);
417
+ if (cached && !options?.noCache && Date.now() - cached.time < SESSION_CACHE_TTL) {
418
+ var offset = options?.offset ?? 0;
419
+ var limit = options?.limit ?? 0;
420
+ var sliced = limit > 0 ? cached.sessions.slice(offset, offset + limit) : cached.sessions;
421
+ return { sessions: sliced, totalCount: cached.sessions.length };
411
422
  }
412
423
 
413
424
  try {
425
+ var sdkT0 = Date.now();
414
426
  var sdkSessions = await sdkListSessions({ dir: projectPath });
427
+ log.session("sdkListSessions for %s: %dms (%d sessions)", projectSlug, Date.now() - sdkT0, sdkSessions.length);
415
428
  var summaries = sdkSessions.map(function (s) {
416
429
  return mapSDKSession(s, projectSlug);
417
430
  });
418
431
  summaries.sort(function (a, b) { return b.updatedAt - a.updatedAt; });
419
- return summaries;
432
+ sessionListCache.set(projectSlug, { sessions: summaries, time: Date.now() });
433
+
434
+ var offset2 = options?.offset ?? 0;
435
+ var limit2 = options?.limit ?? 0;
436
+ var sliced2 = limit2 > 0 ? summaries.slice(offset2, offset2 + limit2) : summaries;
437
+ return { sessions: sliced2, totalCount: summaries.length };
420
438
  } catch (err) {
421
439
  log.session("Failed to list SDK sessions: %O", err);
422
- return [];
440
+ return { sessions: [], totalCount: 0 };
423
441
  }
424
442
  }
425
443
 
444
+ export function invalidateSessionCache(projectSlug: string): void {
445
+ sessionListCache.delete(projectSlug);
446
+ }
447
+
426
448
  export async function getSessionTitle(projectSlug: string, sessionId: string): Promise<string> {
427
449
  var projectPath = getProjectPath(projectSlug);
428
450
  var options = projectPath ? { dir: projectPath } : undefined;