@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
|
-
},
|
|
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.
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
269
|
-
var
|
|
270
|
-
|
|
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
|
|
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
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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")))
|
|
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
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
367
|
-
for (var i = 0; i <
|
|
368
|
-
if (
|
|
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(
|
|
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
|
-
|
|
1036
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|