@cryptiklemur/lattice 1.43.6 → 1.43.7
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/handlers/session.ts +42 -48
- package/server/src/project/session.ts +85 -19
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cryptiklemur/lattice",
|
|
3
|
-
"version": "1.43.
|
|
3
|
+
"version": "1.43.7",
|
|
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>",
|
|
@@ -25,7 +25,7 @@ import {
|
|
|
25
25
|
renameSession,
|
|
26
26
|
} from "../project/session";
|
|
27
27
|
import { getContextBreakdown } from "../project/context-breakdown";
|
|
28
|
-
import { setActiveSession } from "./chat";
|
|
28
|
+
import { setActiveSession, getActiveSession } from "./chat";
|
|
29
29
|
import { setActiveProject } from "./fs";
|
|
30
30
|
import { wasSessionInterrupted, clearInterruptedFlag, isSessionBusy, watchSessionLock, stopExternalSession, getBusyOwner } from "../project/sdk-bridge";
|
|
31
31
|
import { log } from "../logger";
|
|
@@ -82,12 +82,14 @@ registerHandler("session", function (clientId: string, message: ClientMessage) {
|
|
|
82
82
|
|
|
83
83
|
if (message.type === "session:history_page") {
|
|
84
84
|
var pageMsg = message as { type: "session:history_page"; sessionId: string; before: number; limit: number };
|
|
85
|
-
var
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
85
|
+
var activeSession = getActiveSession(clientId);
|
|
86
|
+
void getSessionHistoryPage(pageMsg.sessionId, pageMsg.before, pageMsg.limit, activeSession?.projectSlug).then(function (page) {
|
|
87
|
+
sendTo(clientId, {
|
|
88
|
+
type: "session:history_page_result",
|
|
89
|
+
sessionId: pageMsg.sessionId,
|
|
90
|
+
messages: page.messages,
|
|
91
|
+
hasMore: page.hasMore,
|
|
92
|
+
});
|
|
91
93
|
});
|
|
92
94
|
return;
|
|
93
95
|
}
|
|
@@ -104,50 +106,42 @@ registerHandler("session", function (clientId: string, message: ClientMessage) {
|
|
|
104
106
|
setActiveSession(clientId, activateMsg.projectSlug, activateMsg.sessionId);
|
|
105
107
|
setActiveProject(clientId, activateMsg.projectSlug);
|
|
106
108
|
watchSessionLock(activateMsg.sessionId);
|
|
109
|
+
var activateT0 = Date.now();
|
|
110
|
+
void loadSessionHistory(activateMsg.projectSlug, activateMsg.sessionId).then(function (historyResult) {
|
|
111
|
+
log.session("session:activate total history load: %dms", Date.now() - activateT0);
|
|
112
|
+
var interrupted = wasSessionInterrupted(activateMsg.sessionId);
|
|
113
|
+
if (interrupted) {
|
|
114
|
+
clearInterruptedFlag(activateMsg.sessionId);
|
|
115
|
+
}
|
|
116
|
+
var busy = isSessionBusy(activateMsg.sessionId);
|
|
117
|
+
var busyOwner = busy ? getBusyOwner(activateMsg.sessionId) : undefined;
|
|
118
|
+
sendTo(clientId, {
|
|
119
|
+
type: "session:history",
|
|
120
|
+
projectSlug: activateMsg.projectSlug,
|
|
121
|
+
sessionId: activateMsg.sessionId,
|
|
122
|
+
messages: historyResult.messages,
|
|
123
|
+
title: null,
|
|
124
|
+
interrupted: interrupted || undefined,
|
|
125
|
+
busy: busy || undefined,
|
|
126
|
+
busyOwner: busyOwner,
|
|
127
|
+
totalMessages: historyResult.totalMessages,
|
|
128
|
+
hasMore: historyResult.hasMore,
|
|
129
|
+
});
|
|
130
|
+
}).catch(function (err) {
|
|
131
|
+
log.session("Error sending session history: %O", err);
|
|
132
|
+
sendTo(clientId, { type: "chat:error", message: "Failed to load session history" });
|
|
133
|
+
});
|
|
134
|
+
|
|
107
135
|
void Promise.all([
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}),
|
|
112
|
-
getSessionTitle(activateMsg.projectSlug, activateMsg.sessionId).catch(function (err) {
|
|
113
|
-
log.session("Failed to load session title: %O", err);
|
|
114
|
-
return null;
|
|
115
|
-
}),
|
|
116
|
-
getSessionUsage(activateMsg.projectSlug, activateMsg.sessionId).catch(function (err) {
|
|
117
|
-
log.session("Failed to load session usage: %O", err);
|
|
118
|
-
return null;
|
|
119
|
-
}),
|
|
120
|
-
getContextBreakdown(activateMsg.projectSlug, activateMsg.sessionId).catch(function (err) {
|
|
121
|
-
log.session("Failed to load context breakdown: %O", err);
|
|
122
|
-
return null;
|
|
123
|
-
}),
|
|
136
|
+
getSessionTitle(activateMsg.projectSlug, activateMsg.sessionId).catch(function () { return null; }),
|
|
137
|
+
getSessionUsage(activateMsg.projectSlug, activateMsg.sessionId).catch(function () { return null; }),
|
|
138
|
+
getContextBreakdown(activateMsg.projectSlug, activateMsg.sessionId).catch(function () { return null; }),
|
|
124
139
|
]).then(function (results) {
|
|
125
140
|
try {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
clearInterruptedFlag(activateMsg.sessionId);
|
|
141
|
+
if (results[0]) {
|
|
142
|
+
sendTo(clientId, { type: "session:history", projectSlug: activateMsg.projectSlug, sessionId: activateMsg.sessionId, messages: [], title: results[0] as string });
|
|
129
143
|
}
|
|
130
|
-
var
|
|
131
|
-
var busyOwner = busy ? getBusyOwner(activateMsg.sessionId) : undefined;
|
|
132
|
-
var historyResult = results[0] || { messages: [], totalMessages: 0, hasMore: false };
|
|
133
|
-
sendTo(clientId, {
|
|
134
|
-
type: "session:history",
|
|
135
|
-
projectSlug: activateMsg.projectSlug,
|
|
136
|
-
sessionId: activateMsg.sessionId,
|
|
137
|
-
messages: historyResult.messages,
|
|
138
|
-
title: results[1],
|
|
139
|
-
interrupted: interrupted || undefined,
|
|
140
|
-
busy: busy || undefined,
|
|
141
|
-
busyOwner: busyOwner,
|
|
142
|
-
totalMessages: historyResult.totalMessages,
|
|
143
|
-
hasMore: historyResult.hasMore,
|
|
144
|
-
});
|
|
145
|
-
} catch (err) {
|
|
146
|
-
log.session("Error sending session history: %O", err);
|
|
147
|
-
sendTo(clientId, { type: "chat:error", message: "Failed to load session history" });
|
|
148
|
-
}
|
|
149
|
-
try {
|
|
150
|
-
var usage = results[2];
|
|
144
|
+
var usage = results[1];
|
|
151
145
|
if (usage) {
|
|
152
146
|
sendTo(clientId, {
|
|
153
147
|
type: "chat:context_usage",
|
|
@@ -162,7 +156,7 @@ registerHandler("session", function (clientId: string, message: ClientMessage) {
|
|
|
162
156
|
log.session("Error sending context usage: %O", err);
|
|
163
157
|
}
|
|
164
158
|
try {
|
|
165
|
-
var breakdown = results[
|
|
159
|
+
var breakdown = results[2];
|
|
166
160
|
if (breakdown) {
|
|
167
161
|
sendTo(clientId, {
|
|
168
162
|
type: "chat:context_breakdown",
|
|
@@ -458,45 +458,111 @@ export async function getSessionTitle(projectSlug: string, sessionId: string): P
|
|
|
458
458
|
}
|
|
459
459
|
|
|
460
460
|
var historyCache = new Map<string, { messages: HistoryMessage[]; time: number }>();
|
|
461
|
-
var HISTORY_CACHE_TTL =
|
|
461
|
+
var HISTORY_CACHE_TTL = 60000;
|
|
462
462
|
var INITIAL_MESSAGE_COUNT = 100;
|
|
463
|
+
var TAIL_READ_BYTES = 2 * 1024 * 1024;
|
|
463
464
|
|
|
464
|
-
|
|
465
|
+
function getSessionFilePath(projectSlug: string, sessionId: string): string | null {
|
|
465
466
|
var projectPath = getProjectPath(projectSlug);
|
|
466
|
-
|
|
467
|
+
if (!projectPath) return null;
|
|
468
|
+
var hash = projectPathToHash(projectPath);
|
|
469
|
+
var filePath = join(homedir(), ".claude", "projects", hash, sessionId + ".jsonl");
|
|
470
|
+
return existsSync(filePath) ? filePath : null;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function readTailLines(filePath: string, maxBytes: number): { lines: string[]; isPartial: boolean; fileSize: number } {
|
|
474
|
+
var { openSync, readSync, fstatSync, closeSync } = require("node:fs") as typeof import("node:fs");
|
|
475
|
+
var fd = openSync(filePath, "r");
|
|
476
|
+
var stat = fstatSync(fd);
|
|
477
|
+
var readStart = Math.max(0, stat.size - maxBytes);
|
|
478
|
+
var buf = Buffer.alloc(stat.size - readStart);
|
|
479
|
+
readSync(fd, buf, 0, buf.length, readStart);
|
|
480
|
+
closeSync(fd);
|
|
481
|
+
var text = buf.toString("utf-8");
|
|
482
|
+
if (readStart > 0) {
|
|
483
|
+
var firstNewline = text.indexOf("\n");
|
|
484
|
+
if (firstNewline >= 0) text = text.slice(firstNewline + 1);
|
|
485
|
+
}
|
|
486
|
+
var lines = text.split("\n").filter(function (l) { return l.length > 0; });
|
|
487
|
+
return { lines, isPartial: readStart > 0, fileSize: stat.size };
|
|
488
|
+
}
|
|
467
489
|
|
|
490
|
+
function parseJsonlLines(lines: string[]): SessionMessage[] {
|
|
491
|
+
var results: SessionMessage[] = [];
|
|
492
|
+
for (var i = 0; i < lines.length; i++) {
|
|
493
|
+
try {
|
|
494
|
+
var parsed = JSON.parse(lines[i]);
|
|
495
|
+
if (parsed.type === "user" || parsed.type === "assistant" || parsed.type === "system") {
|
|
496
|
+
results.push(parsed as SessionMessage);
|
|
497
|
+
}
|
|
498
|
+
} catch {}
|
|
499
|
+
}
|
|
500
|
+
return results;
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
export async function loadSessionHistory(projectSlug: string, sessionId: string): Promise<{ messages: HistoryMessage[]; totalMessages: number; hasMore: boolean }> {
|
|
468
504
|
try {
|
|
469
505
|
var t0 = Date.now();
|
|
470
506
|
var cached = historyCache.get(sessionId);
|
|
471
|
-
var allMessages: HistoryMessage[];
|
|
472
|
-
|
|
473
507
|
if (cached && Date.now() - cached.time < HISTORY_CACHE_TTL) {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
508
|
+
var tail = cached.messages.length > INITIAL_MESSAGE_COUNT
|
|
509
|
+
? cached.messages.slice(cached.messages.length - INITIAL_MESSAGE_COUNT)
|
|
510
|
+
: cached.messages;
|
|
511
|
+
log.session("loadSessionHistory %s: %dms (cached, %d total)", sessionId.slice(0, 8), Date.now() - t0, cached.messages.length);
|
|
512
|
+
return { messages: tail, totalMessages: cached.messages.length, hasMore: cached.messages.length > INITIAL_MESSAGE_COUNT };
|
|
479
513
|
}
|
|
480
514
|
|
|
481
|
-
|
|
515
|
+
var filePath = getSessionFilePath(projectSlug, sessionId);
|
|
516
|
+
if (filePath) {
|
|
517
|
+
var tailData = readTailLines(filePath, TAIL_READ_BYTES);
|
|
518
|
+
var tailRaw = parseJsonlLines(tailData.lines);
|
|
519
|
+
var tailMessages = convertSessionMessages(tailRaw);
|
|
520
|
+
var hasMore = tailData.isPartial;
|
|
521
|
+
|
|
522
|
+
log.session("loadSessionHistory %s: %dms (tail read, %d msgs, partial=%s)", sessionId.slice(0, 8), Date.now() - t0, tailMessages.length, hasMore);
|
|
523
|
+
|
|
524
|
+
if (!hasMore) {
|
|
525
|
+
historyCache.set(sessionId, { messages: tailMessages, time: Date.now() });
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
var initialSlice = tailMessages.length > INITIAL_MESSAGE_COUNT
|
|
529
|
+
? tailMessages.slice(tailMessages.length - INITIAL_MESSAGE_COUNT)
|
|
530
|
+
: tailMessages;
|
|
531
|
+
|
|
532
|
+
return { messages: initialSlice, totalMessages: tailMessages.length, hasMore: hasMore };
|
|
533
|
+
}
|
|
482
534
|
|
|
483
|
-
var
|
|
535
|
+
var projectPath = getProjectPath(projectSlug);
|
|
536
|
+
var options = projectPath ? { dir: projectPath } : undefined;
|
|
537
|
+
var rawMessages = await getSessionMessages(sessionId, options);
|
|
538
|
+
var allMessages = convertSessionMessages(rawMessages);
|
|
539
|
+
historyCache.set(sessionId, { messages: allMessages, time: Date.now() });
|
|
540
|
+
log.session("loadSessionHistory %s: %dms (full SDK, %d msgs)", sessionId.slice(0, 8), Date.now() - t0, allMessages.length);
|
|
541
|
+
var tailSlice = allMessages.length > INITIAL_MESSAGE_COUNT
|
|
484
542
|
? allMessages.slice(allMessages.length - INITIAL_MESSAGE_COUNT)
|
|
485
543
|
: allMessages;
|
|
486
|
-
|
|
487
|
-
return {
|
|
488
|
-
messages: tail,
|
|
489
|
-
totalMessages: allMessages.length,
|
|
490
|
-
hasMore: allMessages.length > INITIAL_MESSAGE_COUNT,
|
|
491
|
-
};
|
|
544
|
+
return { messages: tailSlice, totalMessages: allMessages.length, hasMore: allMessages.length > INITIAL_MESSAGE_COUNT };
|
|
492
545
|
} catch (err) {
|
|
493
546
|
log.session("Failed to load session history: %O", err);
|
|
494
547
|
return { messages: [], totalMessages: 0, hasMore: false };
|
|
495
548
|
}
|
|
496
549
|
}
|
|
497
550
|
|
|
498
|
-
export function getSessionHistoryPage(sessionId: string, beforeIndex: number, limit: number): { messages: HistoryMessage[]; hasMore: boolean } {
|
|
551
|
+
export async function getSessionHistoryPage(sessionId: string, beforeIndex: number, limit: number, projectSlug?: string): Promise<{ messages: HistoryMessage[]; hasMore: boolean }> {
|
|
499
552
|
var cached = historyCache.get(sessionId);
|
|
553
|
+
if (!cached && projectSlug) {
|
|
554
|
+
var projectPath = getProjectPath(projectSlug);
|
|
555
|
+
var options = projectPath ? { dir: projectPath } : undefined;
|
|
556
|
+
try {
|
|
557
|
+
var rawMessages = await getSessionMessages(sessionId, options);
|
|
558
|
+
var allMessages = convertSessionMessages(rawMessages);
|
|
559
|
+
historyCache.set(sessionId, { messages: allMessages, time: Date.now() });
|
|
560
|
+
cached = { messages: allMessages, time: Date.now() };
|
|
561
|
+
log.session("getSessionHistoryPage: full load for %s, %d messages", sessionId.slice(0, 8), allMessages.length);
|
|
562
|
+
} catch {
|
|
563
|
+
return { messages: [], hasMore: false };
|
|
564
|
+
}
|
|
565
|
+
}
|
|
500
566
|
if (!cached) return { messages: [], hasMore: false };
|
|
501
567
|
|
|
502
568
|
var endIdx = Math.max(0, beforeIndex);
|