@cryptiklemur/lattice 1.46.6 → 1.47.0
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/client/src/components/chat/ChatView.tsx +7 -72
- package/client/src/components/chat/ElicitationCard.tsx +235 -0
- package/client/src/components/chat/Message.tsx +16 -0
- package/client/src/components/settings/BudgetSettings.tsx +6 -2
- package/client/src/components/sidebar/UserIsland.tsx +141 -14
- package/client/src/components/ui/SaveFooter.tsx +28 -3
- package/client/src/hooks/useSession.ts +89 -42
- package/client/src/hooks/useWebSocket.ts +1 -1
- package/client/src/stores/session.ts +64 -16
- package/package.json +1 -1
- package/server/src/daemon.ts +25 -8
- package/server/src/handlers/chat.ts +12 -1
- package/server/src/handlers/session.ts +1 -15
- package/server/src/handlers/settings.ts +3 -0
- package/server/src/index.ts +3 -0
- package/server/src/project/sdk-bridge.ts +119 -166
- package/server/src/project/session.ts +36 -7
- package/server/src/project/warmup.ts +232 -0
- package/shared/src/messages.ts +49 -11
- package/shared/src/models.ts +7 -1
|
@@ -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
|
|
7
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } 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";
|
|
@@ -15,6 +15,21 @@ import { guessContextWindow, getSessionTitle, renameSession, listSessions, inval
|
|
|
15
15
|
import { getLatticeHome, loadConfig } from "../config";
|
|
16
16
|
import { log } from "../logger";
|
|
17
17
|
import { getDailySpend, invalidateDailySpendCache } from "../analytics/engine";
|
|
18
|
+
import { getWarmupModels } from "./warmup";
|
|
19
|
+
import { execSync } from "node:child_process";
|
|
20
|
+
|
|
21
|
+
var claudeExePath: string | null = null;
|
|
22
|
+
|
|
23
|
+
function getClaudeExecutablePath(): string {
|
|
24
|
+
if (claudeExePath) return claudeExePath;
|
|
25
|
+
try {
|
|
26
|
+
claudeExePath = execSync("which claude", { encoding: "utf-8" }).trim();
|
|
27
|
+
log.chat("Using system claude: %s", claudeExePath);
|
|
28
|
+
} catch {
|
|
29
|
+
claudeExePath = "claude";
|
|
30
|
+
}
|
|
31
|
+
return claudeExePath;
|
|
32
|
+
}
|
|
18
33
|
|
|
19
34
|
interface PendingPermission {
|
|
20
35
|
resolve: (result: PermissionResult) => void;
|
|
@@ -28,6 +43,15 @@ interface PendingPermission {
|
|
|
28
43
|
}
|
|
29
44
|
|
|
30
45
|
var pendingPermissions = new Map<string, PendingPermission>();
|
|
46
|
+
|
|
47
|
+
interface PendingElicitation {
|
|
48
|
+
resolve: (result: { action: "accept" | "decline"; content?: Record<string, unknown> }) => void;
|
|
49
|
+
clientId: string;
|
|
50
|
+
sessionId: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
var pendingElicitations = new Map<string, PendingElicitation>();
|
|
54
|
+
|
|
31
55
|
var autoApprovedTools = new Map<string, Set<string>>();
|
|
32
56
|
var sessionPermissionOverrides = new Map<string, PermissionMode>();
|
|
33
57
|
|
|
@@ -49,87 +73,15 @@ export interface ModelEntry {
|
|
|
49
73
|
displayName: string;
|
|
50
74
|
}
|
|
51
75
|
|
|
52
|
-
var KNOWN_MODELS: ModelEntry[] = [
|
|
53
|
-
{ value: "default", displayName: "Default" },
|
|
54
|
-
{ value: "opus", displayName: "Opus" },
|
|
55
|
-
{ value: "sonnet", displayName: "Sonnet" },
|
|
56
|
-
{ value: "haiku", displayName: "Haiku" },
|
|
57
|
-
];
|
|
58
|
-
|
|
59
76
|
export function getAvailableModels(): ModelEntry[] {
|
|
60
|
-
return
|
|
77
|
+
return getWarmupModels();
|
|
61
78
|
}
|
|
62
79
|
|
|
63
80
|
var activeStreams = new Map<string, Query>();
|
|
64
81
|
var pendingStreams = new Set<string>();
|
|
65
|
-
var streamMetadata = new Map<string, { projectSlug: string; clientId: string; startedAt: number }>();
|
|
82
|
+
var streamMetadata = new Map<string, { projectSlug: string; clientId: string; startedAt: number; abortController?: AbortController }>();
|
|
66
83
|
var interruptedSessions = new Set<string>();
|
|
67
84
|
|
|
68
|
-
// Track external lock state so we only broadcast on changes
|
|
69
|
-
var externalLockState = new Map<string, boolean>();
|
|
70
|
-
var watchedSessions = new Set<string>();
|
|
71
|
-
var remoteSessionWatchers = new Map<string, Set<string>>();
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Start polling a session's lock file for external CLI usage.
|
|
75
|
-
* Called when a client activates a session.
|
|
76
|
-
*/
|
|
77
|
-
export function watchSessionLock(sessionId: string): void {
|
|
78
|
-
watchedSessions.add(sessionId);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Stop polling a session's lock file.
|
|
83
|
-
*/
|
|
84
|
-
export function unwatchSessionLock(sessionId: string): void {
|
|
85
|
-
watchedSessions.delete(sessionId);
|
|
86
|
-
externalLockState.delete(sessionId);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
export function addRemoteSessionWatcher(sessionId: string, nodeId: string): void {
|
|
90
|
-
var watchers = remoteSessionWatchers.get(sessionId);
|
|
91
|
-
if (!watchers) {
|
|
92
|
-
watchers = new Set();
|
|
93
|
-
remoteSessionWatchers.set(sessionId, watchers);
|
|
94
|
-
}
|
|
95
|
-
watchers.add(nodeId);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export function getBusyOwner(sessionId: string): "cli" | "lattice" | undefined {
|
|
99
|
-
if (activeStreams.has(sessionId)) return "lattice";
|
|
100
|
-
if (isSessionLockedByExternal(sessionId)) return "cli";
|
|
101
|
-
return undefined;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// Poll every 3 seconds for external lock changes
|
|
105
|
-
setInterval(function () {
|
|
106
|
-
for (var sessionId of watchedSessions) {
|
|
107
|
-
var busy = isSessionBusy(sessionId);
|
|
108
|
-
var prev = externalLockState.get(sessionId) ?? false;
|
|
109
|
-
|
|
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 });
|
|
114
|
-
|
|
115
|
-
var watchers = remoteSessionWatchers.get(sessionId);
|
|
116
|
-
if (watchers) {
|
|
117
|
-
var { getPeerConnection } = require("../mesh/connector") as typeof import("../mesh/connector");
|
|
118
|
-
for (var nodeId of watchers) {
|
|
119
|
-
var peerWs = getPeerConnection(nodeId);
|
|
120
|
-
if (peerWs) {
|
|
121
|
-
peerWs.send(JSON.stringify({
|
|
122
|
-
type: "mesh:proxy_response",
|
|
123
|
-
projectSlug: "",
|
|
124
|
-
requestId: "busy-" + sessionId,
|
|
125
|
-
payload: { type: "session:busy", sessionId, busy: busy, busyOwner: owner },
|
|
126
|
-
}));
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
}, 3000);
|
|
133
85
|
|
|
134
86
|
function getStreamStatePath(): string {
|
|
135
87
|
return join(getLatticeHome(), "active-streams.json");
|
|
@@ -191,6 +143,30 @@ export function cleanupClientPermissions(clientId: string): void {
|
|
|
191
143
|
}
|
|
192
144
|
}
|
|
193
145
|
|
|
146
|
+
export function getPendingElicitation(requestId: string): PendingElicitation | undefined {
|
|
147
|
+
return pendingElicitations.get(requestId);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function resolveElicitation(requestId: string, result: { action: "accept" | "decline"; content?: Record<string, unknown> }): void {
|
|
151
|
+
var pending = pendingElicitations.get(requestId);
|
|
152
|
+
if (!pending) return;
|
|
153
|
+
pendingElicitations.delete(requestId);
|
|
154
|
+
pending.resolve(result);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function cleanupClientElicitations(clientId: string): void {
|
|
158
|
+
var toRemove: string[] = [];
|
|
159
|
+
pendingElicitations.forEach(function (entry, requestId) {
|
|
160
|
+
if (entry.clientId === clientId) {
|
|
161
|
+
toRemove.push(requestId);
|
|
162
|
+
entry.resolve({ action: "decline" });
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
for (var i = 0; i < toRemove.length; i++) {
|
|
166
|
+
pendingElicitations.delete(toRemove[i]);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
194
170
|
export function addAutoApprovedTool(sessionId: string, toolName: string): void {
|
|
195
171
|
var tools = autoApprovedTools.get(sessionId);
|
|
196
172
|
if (!tools) {
|
|
@@ -212,99 +188,14 @@ export function getActiveStreamCount(): number {
|
|
|
212
188
|
return activeStreams.size;
|
|
213
189
|
}
|
|
214
190
|
|
|
215
|
-
export function
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
for (var [sessionId] of activeStreams) {
|
|
221
|
-
if (existsSync(join(dir, sessionId + ".jsonl"))) count++;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (cliActiveProjects.has(projectPath)) count++;
|
|
225
|
-
|
|
191
|
+
export function getActiveStreamCountForProject(projectSlug: string): number {
|
|
192
|
+
let count = 0;
|
|
193
|
+
streamMetadata.forEach(function (meta) {
|
|
194
|
+
if (meta.projectSlug === projectSlug) count++;
|
|
195
|
+
});
|
|
226
196
|
return count;
|
|
227
197
|
}
|
|
228
198
|
|
|
229
|
-
/**
|
|
230
|
-
* Check if a session is controlled by an external process (not Lattice).
|
|
231
|
-
* Lattice's own active streams are handled by isProcessing on the client,
|
|
232
|
-
* so this ONLY returns true for external CLI instances.
|
|
233
|
-
*/
|
|
234
|
-
export function isSessionBusy(sessionId: string): boolean {
|
|
235
|
-
if (activeStreams.has(sessionId)) return true;
|
|
236
|
-
return isSessionLockedByExternal(sessionId);
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/**
|
|
240
|
-
* Check if a PID is the Lattice daemon or one of its child processes.
|
|
241
|
-
* The SDK spawns child processes (e.g. claude-agent-sdk/cli.js) that hold
|
|
242
|
-
* lock files — those are NOT external.
|
|
243
|
-
*/
|
|
244
|
-
var cliActiveProjects = new Set<string>();
|
|
245
|
-
|
|
246
|
-
function refreshCliDetection(): void {
|
|
247
|
-
var newActive = new Set<string>();
|
|
248
|
-
var cliPids = getClaudeCliPidsAsync();
|
|
249
|
-
for (var i = 0; i < cliPids.length; i++) {
|
|
250
|
-
newActive.add(cliPids[i].cwd);
|
|
251
|
-
}
|
|
252
|
-
cliActiveProjects = newActive;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function getClaudeCliPidsAsync(): Array<{ pid: number; cwd: string; cmdline: string[] }> {
|
|
256
|
-
var results: Array<{ pid: number; cwd: string; cmdline: string[] }> = [];
|
|
257
|
-
try {
|
|
258
|
-
var procEntries = readdirSync("/proc").filter(function (e) { return /^\d+$/.test(e); });
|
|
259
|
-
for (var i = 0; i < procEntries.length; i++) {
|
|
260
|
-
var pid = parseInt(procEntries[i], 10);
|
|
261
|
-
if (pid === process.pid) continue;
|
|
262
|
-
try {
|
|
263
|
-
var cmdline = readFileSync("/proc/" + pid + "/cmdline", "utf-8").split("\0");
|
|
264
|
-
var exe = cmdline[0] || "";
|
|
265
|
-
if (!exe.endsWith("/claude") && exe !== "claude") continue;
|
|
266
|
-
var cwd = readlinkSync("/proc/" + pid + "/cwd");
|
|
267
|
-
results.push({ pid, cwd, cmdline });
|
|
268
|
-
} catch {}
|
|
269
|
-
}
|
|
270
|
-
} catch {}
|
|
271
|
-
return results;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
setInterval(refreshCliDetection, 10000);
|
|
275
|
-
|
|
276
|
-
function getProjectPathForSession(sessionId: string): string | null {
|
|
277
|
-
var config = loadConfig();
|
|
278
|
-
for (var i = 0; i < config.projects.length; i++) {
|
|
279
|
-
var hash = config.projects[i].path.replace(/\//g, "-");
|
|
280
|
-
var jsonlPath = join(homedir(), ".claude", "projects", hash, sessionId + ".jsonl");
|
|
281
|
-
if (existsSync(jsonlPath)) return config.projects[i].path;
|
|
282
|
-
}
|
|
283
|
-
return null;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
function isSessionLockedByExternal(sessionId: string): boolean {
|
|
288
|
-
if (activeStreams.has(sessionId)) return false;
|
|
289
|
-
var projectPath = getProjectPathForSession(sessionId);
|
|
290
|
-
if (!projectPath) return false;
|
|
291
|
-
return cliActiveProjects.has(projectPath);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
export function stopExternalSession(sessionId: string): boolean {
|
|
295
|
-
var projectPath = getProjectPathForSession(sessionId);
|
|
296
|
-
if (!projectPath) return false;
|
|
297
|
-
var pids = getClaudeCliPidsAsync();
|
|
298
|
-
for (var i = 0; i < pids.length; i++) {
|
|
299
|
-
if (pids[i].cwd === projectPath) {
|
|
300
|
-
try {
|
|
301
|
-
process.kill(pids[i].pid, "SIGINT");
|
|
302
|
-
return true;
|
|
303
|
-
} catch {}
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
return false;
|
|
307
|
-
}
|
|
308
199
|
|
|
309
200
|
export function getSessionStreamClientId(sessionId: string): string | undefined {
|
|
310
201
|
var meta = streamMetadata.get(sessionId);
|
|
@@ -481,18 +372,55 @@ export function startChatStream(options: ChatStreamOptions): void {
|
|
|
481
372
|
} catch {}
|
|
482
373
|
}
|
|
483
374
|
|
|
375
|
+
var abortController = new AbortController();
|
|
376
|
+
|
|
484
377
|
var queryOptions: Parameters<typeof query>[0]["options"] = {
|
|
485
378
|
cwd,
|
|
486
379
|
permissionMode: effectiveMode,
|
|
487
380
|
promptSuggestions: true,
|
|
381
|
+
settingSources: ["user", "project", "local"],
|
|
382
|
+
includePartialMessages: true,
|
|
383
|
+
enableFileCheckpointing: true,
|
|
384
|
+
agentProgressSummaries: true,
|
|
385
|
+
abortController,
|
|
386
|
+
pathToClaudeCodeExecutable: getClaudeExecutablePath(),
|
|
488
387
|
additionalDirectories: savedAdditionalDirs.length > 0 ? savedAdditionalDirs : undefined,
|
|
489
388
|
mcpServers: Object.keys(mcpServers).length > 0 ? mcpServers as Record<string, any> : undefined,
|
|
389
|
+
stderr: function (data: string) {
|
|
390
|
+
if (data.includes("error") || data.includes("Error") || data.includes("credit") || data.includes("Credit") || data.includes("billing") || data.includes("auth")) {
|
|
391
|
+
log.chat("SDK stderr: %s", data.trim());
|
|
392
|
+
}
|
|
393
|
+
},
|
|
490
394
|
};
|
|
491
395
|
|
|
492
396
|
(queryOptions as any).toolConfig = {
|
|
493
397
|
askUserQuestion: { previewFormat: "html" },
|
|
494
398
|
};
|
|
495
399
|
|
|
400
|
+
queryOptions.onElicitation = function (request: any, opts: any) {
|
|
401
|
+
return new Promise(function (resolve) {
|
|
402
|
+
var requestId = crypto.randomUUID();
|
|
403
|
+
pendingElicitations.set(requestId, { resolve, clientId, sessionId });
|
|
404
|
+
sendTo(clientId, {
|
|
405
|
+
type: "chat:elicitation_request",
|
|
406
|
+
requestId,
|
|
407
|
+
serverName: request.serverName || "MCP Server",
|
|
408
|
+
message: request.message || "",
|
|
409
|
+
mode: request.mode || "form",
|
|
410
|
+
url: request.url || null,
|
|
411
|
+
requestedSchema: request.requestedSchema || null,
|
|
412
|
+
});
|
|
413
|
+
if (opts && opts.signal) {
|
|
414
|
+
opts.signal.addEventListener("abort", function () {
|
|
415
|
+
if (pendingElicitations.has(requestId)) {
|
|
416
|
+
pendingElicitations.delete(requestId);
|
|
417
|
+
resolve({ action: "decline" });
|
|
418
|
+
}
|
|
419
|
+
}, { once: true });
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
} as any;
|
|
423
|
+
|
|
496
424
|
queryOptions.canUseTool = function (toolName, input, options) {
|
|
497
425
|
var approved = autoApprovedTools.get(sessionId);
|
|
498
426
|
if (approved && approved.has(toolName)) {
|
|
@@ -713,7 +641,7 @@ export function startChatStream(options: ChatStreamOptions): void {
|
|
|
713
641
|
var stream = query({ prompt: queryPrompt, options: queryOptions });
|
|
714
642
|
pendingStreams.delete(sessionId);
|
|
715
643
|
activeStreams.set(sessionId, stream);
|
|
716
|
-
streamMetadata.set(sessionId, { projectSlug, clientId, startedAt: Date.now() });
|
|
644
|
+
streamMetadata.set(sessionId, { projectSlug, clientId, startedAt: Date.now(), abortController });
|
|
717
645
|
persistStreamState();
|
|
718
646
|
broadcast({ type: "session:busy", sessionId, busy: true }, clientId);
|
|
719
647
|
|
|
@@ -743,6 +671,15 @@ export function startChatStream(options: ChatStreamOptions): void {
|
|
|
743
671
|
});
|
|
744
672
|
toCleanup.forEach(function (reqId) { pendingPermissions.delete(reqId); });
|
|
745
673
|
|
|
674
|
+
var elicitToCleanup: string[] = [];
|
|
675
|
+
pendingElicitations.forEach(function (entry, reqId) {
|
|
676
|
+
if (entry.sessionId === sessionId) {
|
|
677
|
+
elicitToCleanup.push(reqId);
|
|
678
|
+
entry.resolve({ action: "decline" });
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
elicitToCleanup.forEach(function (reqId) { pendingElicitations.delete(reqId); });
|
|
682
|
+
|
|
746
683
|
autoApprovedTools.delete(sessionId);
|
|
747
684
|
sessionPermissionOverrides.delete(sessionId);
|
|
748
685
|
|
|
@@ -906,6 +843,22 @@ export function startChatStream(options: ChatStreamOptions): void {
|
|
|
906
843
|
return;
|
|
907
844
|
}
|
|
908
845
|
|
|
846
|
+
if (msg.type === "rate_limit_event") {
|
|
847
|
+
var rlMsg = msg as { type: string; rate_limit_info: { status: string; utilization?: number; resetsAt?: number; rateLimitType?: string; overageStatus?: string; overageResetsAt?: number; isUsingOverage?: boolean } };
|
|
848
|
+
var rli = rlMsg.rate_limit_info;
|
|
849
|
+
sendTo(clientId, {
|
|
850
|
+
type: "chat:rate_limit",
|
|
851
|
+
status: rli.status,
|
|
852
|
+
utilization: rli.utilization,
|
|
853
|
+
resetsAt: rli.resetsAt,
|
|
854
|
+
rateLimitType: rli.rateLimitType,
|
|
855
|
+
overageStatus: rli.overageStatus,
|
|
856
|
+
overageResetsAt: rli.overageResetsAt,
|
|
857
|
+
isUsingOverage: rli.isUsingOverage,
|
|
858
|
+
} as any);
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
|
|
909
862
|
if (msg.type === "prompt_suggestion") {
|
|
910
863
|
var suggestion = (msg as { type: string; suggestion: string }).suggestion;
|
|
911
864
|
if (suggestion) {
|
|
@@ -465,8 +465,30 @@ export async function getSessionTitle(projectSlug: string, sessionId: string): P
|
|
|
465
465
|
return "Untitled";
|
|
466
466
|
}
|
|
467
467
|
|
|
468
|
-
var historyCache = new Map<string, { messages: HistoryMessage[];
|
|
469
|
-
var
|
|
468
|
+
var historyCache = new Map<string, { messages: HistoryMessage[]; title: string | null }>();
|
|
469
|
+
var historyCacheOrder: string[] = [];
|
|
470
|
+
var MAX_HISTORY_CACHE = 50;
|
|
471
|
+
|
|
472
|
+
function touchCache(sessionId: string): void {
|
|
473
|
+
var idx = historyCacheOrder.indexOf(sessionId);
|
|
474
|
+
if (idx >= 0) historyCacheOrder.splice(idx, 1);
|
|
475
|
+
historyCacheOrder.push(sessionId);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function evictOldest(): void {
|
|
479
|
+
while (historyCacheOrder.length > MAX_HISTORY_CACHE) {
|
|
480
|
+
var oldest = historyCacheOrder.shift();
|
|
481
|
+
if (oldest) historyCache.delete(oldest);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export function appendToHistoryCache(sessionId: string, message: HistoryMessage): void {
|
|
486
|
+
var cached = historyCache.get(sessionId);
|
|
487
|
+
if (!cached) return;
|
|
488
|
+
cached.messages.push(message);
|
|
489
|
+
touchCache(sessionId);
|
|
490
|
+
}
|
|
491
|
+
|
|
470
492
|
var INITIAL_MESSAGE_COUNT = 100;
|
|
471
493
|
var TAIL_READ_BYTES = 2 * 1024 * 1024;
|
|
472
494
|
|
|
@@ -512,7 +534,8 @@ export async function loadSessionHistory(projectSlug: string, sessionId: string)
|
|
|
512
534
|
try {
|
|
513
535
|
var t0 = Date.now();
|
|
514
536
|
var cached = historyCache.get(sessionId);
|
|
515
|
-
if (cached
|
|
537
|
+
if (cached) {
|
|
538
|
+
touchCache(sessionId);
|
|
516
539
|
var tail = cached.messages.length > INITIAL_MESSAGE_COUNT
|
|
517
540
|
? cached.messages.slice(cached.messages.length - INITIAL_MESSAGE_COUNT)
|
|
518
541
|
: cached.messages;
|
|
@@ -530,7 +553,9 @@ export async function loadSessionHistory(projectSlug: string, sessionId: string)
|
|
|
530
553
|
log.session("loadSessionHistory %s: %dms (tail read, %d msgs, partial=%s)", sessionId.slice(0, 8), Date.now() - t0, tailMessages.length, hasMore);
|
|
531
554
|
|
|
532
555
|
if (!hasMore) {
|
|
533
|
-
historyCache.set(sessionId, { messages: tailMessages,
|
|
556
|
+
historyCache.set(sessionId, { messages: tailMessages, title: null });
|
|
557
|
+
touchCache(sessionId);
|
|
558
|
+
evictOldest();
|
|
534
559
|
}
|
|
535
560
|
|
|
536
561
|
var initialSlice = tailMessages.length > INITIAL_MESSAGE_COUNT
|
|
@@ -544,7 +569,9 @@ export async function loadSessionHistory(projectSlug: string, sessionId: string)
|
|
|
544
569
|
var options = projectPath ? { dir: projectPath } : undefined;
|
|
545
570
|
var rawMessages = await getSessionMessages(sessionId, options);
|
|
546
571
|
var allMessages = convertSessionMessages(rawMessages);
|
|
547
|
-
historyCache.set(sessionId, { messages: allMessages,
|
|
572
|
+
historyCache.set(sessionId, { messages: allMessages, title: null });
|
|
573
|
+
touchCache(sessionId);
|
|
574
|
+
evictOldest();
|
|
548
575
|
log.session("loadSessionHistory %s: %dms (full SDK, %d msgs)", sessionId.slice(0, 8), Date.now() - t0, allMessages.length);
|
|
549
576
|
var tailSlice = allMessages.length > INITIAL_MESSAGE_COUNT
|
|
550
577
|
? allMessages.slice(allMessages.length - INITIAL_MESSAGE_COUNT)
|
|
@@ -564,8 +591,10 @@ export async function getSessionHistoryPage(sessionId: string, beforeIndex: numb
|
|
|
564
591
|
try {
|
|
565
592
|
var rawMessages = await getSessionMessages(sessionId, options);
|
|
566
593
|
var allMessages = convertSessionMessages(rawMessages);
|
|
567
|
-
historyCache.set(sessionId, { messages: allMessages,
|
|
568
|
-
|
|
594
|
+
historyCache.set(sessionId, { messages: allMessages, title: null });
|
|
595
|
+
touchCache(sessionId);
|
|
596
|
+
evictOldest();
|
|
597
|
+
cached = historyCache.get(sessionId)!;
|
|
569
598
|
log.session("getSessionHistoryPage: full load for %s, %d messages", sessionId.slice(0, 8), allMessages.length);
|
|
570
599
|
} catch {
|
|
571
600
|
return { messages: [], hasMore: false };
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import type { AccountInfo } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
3
|
+
import { broadcast } from "../ws/broadcast";
|
|
4
|
+
import { log } from "../logger";
|
|
5
|
+
import { loadConfig } from "../config";
|
|
6
|
+
import { listSessions, loadSessionHistory } from "./session";
|
|
7
|
+
import { execSync } from "node:child_process";
|
|
8
|
+
|
|
9
|
+
var claudeExePath: string | null = null;
|
|
10
|
+
|
|
11
|
+
function getClaudeExecutablePath(): string {
|
|
12
|
+
if (claudeExePath) return claudeExePath;
|
|
13
|
+
try {
|
|
14
|
+
claudeExePath = execSync("which claude", { encoding: "utf-8" }).trim();
|
|
15
|
+
} catch {
|
|
16
|
+
claudeExePath = "claude";
|
|
17
|
+
}
|
|
18
|
+
return claudeExePath;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ModelEntry {
|
|
22
|
+
value: string;
|
|
23
|
+
displayName: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
var KNOWN_MODELS: ModelEntry[] = [
|
|
27
|
+
{ value: "default", displayName: "Default" },
|
|
28
|
+
{ value: "opus", displayName: "Opus" },
|
|
29
|
+
{ value: "sonnet", displayName: "Sonnet" },
|
|
30
|
+
{ value: "haiku", displayName: "Haiku" },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
var warmupModels: ModelEntry[] = [];
|
|
34
|
+
var warmupSlashCommands: string[] = [];
|
|
35
|
+
var warmupAccountInfo: AccountInfo | null = null;
|
|
36
|
+
var warmupComplete = false;
|
|
37
|
+
|
|
38
|
+
function ensureKnownModels(sdkModels: ModelEntry[]): ModelEntry[] {
|
|
39
|
+
var seen = new Set<string>();
|
|
40
|
+
for (var i = 0; i < sdkModels.length; i++) {
|
|
41
|
+
seen.add(sdkModels[i].value);
|
|
42
|
+
}
|
|
43
|
+
var result = sdkModels.slice();
|
|
44
|
+
for (var j = 0; j < KNOWN_MODELS.length; j++) {
|
|
45
|
+
if (!seen.has(KNOWN_MODELS[j].value)) {
|
|
46
|
+
result.push(KNOWN_MODELS[j]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function runWarmup(cwd: string): Promise<void> {
|
|
53
|
+
log.server("SDK warmup starting (cwd: %s)...", cwd);
|
|
54
|
+
try {
|
|
55
|
+
var ac = new AbortController();
|
|
56
|
+
var ended = false;
|
|
57
|
+
|
|
58
|
+
var mq = {
|
|
59
|
+
[Symbol.asyncIterator]: function () {
|
|
60
|
+
return {
|
|
61
|
+
next: function () {
|
|
62
|
+
if (ended) return Promise.resolve({ value: undefined as any, done: true });
|
|
63
|
+
ended = true;
|
|
64
|
+
return Promise.resolve({
|
|
65
|
+
value: {
|
|
66
|
+
type: "user" as const,
|
|
67
|
+
message: { role: "user" as const, content: [{ type: "text" as const, text: "hi" }] },
|
|
68
|
+
parent_tool_use_id: null,
|
|
69
|
+
session_id: "warmup",
|
|
70
|
+
},
|
|
71
|
+
done: false,
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
var stream = query({
|
|
79
|
+
prompt: mq as any,
|
|
80
|
+
options: {
|
|
81
|
+
cwd,
|
|
82
|
+
settingSources: ["user", "project", "local"],
|
|
83
|
+
abortController: ac,
|
|
84
|
+
permissionMode: "plan",
|
|
85
|
+
pathToClaudeCodeExecutable: getClaudeExecutablePath(),
|
|
86
|
+
stderr: function (data: string) {
|
|
87
|
+
log.server("Warmup stderr: %s", data.trim());
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
var gotInit = false;
|
|
93
|
+
for await (var msg of stream) {
|
|
94
|
+
if (msg.type === "system") {
|
|
95
|
+
var sysMsg = msg as any;
|
|
96
|
+
if (sysMsg.subtype === "init") {
|
|
97
|
+
gotInit = true;
|
|
98
|
+
if (sysMsg.slash_commands) {
|
|
99
|
+
warmupSlashCommands = sysMsg.slash_commands;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
var models = await stream.supportedModels();
|
|
104
|
+
warmupModels = ensureKnownModels((models || []) as ModelEntry[]);
|
|
105
|
+
} catch {
|
|
106
|
+
warmupModels = KNOWN_MODELS.slice();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
warmupAccountInfo = await stream.accountInfo();
|
|
111
|
+
} catch {}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (msg.type === "rate_limit_event") {
|
|
116
|
+
var rlMsg = msg as { type: string; rate_limit_info: { status: string; utilization?: number; resetsAt?: number; rateLimitType?: string; overageStatus?: string; overageResetsAt?: number; isUsingOverage?: boolean } };
|
|
117
|
+
var rli = rlMsg.rate_limit_info;
|
|
118
|
+
broadcast({
|
|
119
|
+
type: "chat:rate_limit",
|
|
120
|
+
status: rli.status,
|
|
121
|
+
utilization: rli.utilization,
|
|
122
|
+
resetsAt: rli.resetsAt,
|
|
123
|
+
rateLimitType: rli.rateLimitType,
|
|
124
|
+
overageStatus: rli.overageStatus,
|
|
125
|
+
overageResetsAt: rli.overageResetsAt,
|
|
126
|
+
isUsingOverage: rli.isUsingOverage,
|
|
127
|
+
} as any);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (msg.type === "result" && gotInit) {
|
|
131
|
+
ac.abort();
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
warmupComplete = true;
|
|
137
|
+
log.server("SDK warmup complete: %d models, %d commands, auth=%s",
|
|
138
|
+
warmupModels.length, warmupSlashCommands.length,
|
|
139
|
+
warmupAccountInfo?.apiKeySource || "unknown");
|
|
140
|
+
if (warmupAccountInfo) {
|
|
141
|
+
log.server("Account: email=%s org=%s subscription=%s provider=%s source=%s",
|
|
142
|
+
warmupAccountInfo.email || "none",
|
|
143
|
+
warmupAccountInfo.organization || "none",
|
|
144
|
+
warmupAccountInfo.subscriptionType || "none",
|
|
145
|
+
warmupAccountInfo.apiProvider || "none",
|
|
146
|
+
warmupAccountInfo.apiKeySource || "none");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
broadcast({
|
|
150
|
+
type: "warmup:models",
|
|
151
|
+
models: warmupModels,
|
|
152
|
+
} as any);
|
|
153
|
+
|
|
154
|
+
if (warmupAccountInfo) {
|
|
155
|
+
broadcast({
|
|
156
|
+
type: "warmup:account",
|
|
157
|
+
email: warmupAccountInfo.email,
|
|
158
|
+
organization: warmupAccountInfo.organization,
|
|
159
|
+
subscriptionType: warmupAccountInfo.subscriptionType,
|
|
160
|
+
apiKeySource: warmupAccountInfo.apiKeySource,
|
|
161
|
+
apiProvider: warmupAccountInfo.apiProvider,
|
|
162
|
+
} as any);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
void warmupProjectData();
|
|
166
|
+
} catch (err) {
|
|
167
|
+
if (err && (err as any).name !== "AbortError" && !(err instanceof Error && err.message.includes("aborted"))) {
|
|
168
|
+
log.server("SDK warmup failed: %O", err);
|
|
169
|
+
}
|
|
170
|
+
warmupModels = KNOWN_MODELS.slice();
|
|
171
|
+
warmupComplete = true;
|
|
172
|
+
void warmupProjectData();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function warmupProjectData(): Promise<void> {
|
|
177
|
+
var t0 = Date.now();
|
|
178
|
+
var config = loadConfig();
|
|
179
|
+
var projects = config.projects;
|
|
180
|
+
if (projects.length === 0) return;
|
|
181
|
+
|
|
182
|
+
log.server("Data warmup: pre-caching %d project(s)...", projects.length);
|
|
183
|
+
|
|
184
|
+
var totalSessions = 0;
|
|
185
|
+
var recentSessionIds: Array<{ projectSlug: string; sessionId: string }> = [];
|
|
186
|
+
|
|
187
|
+
for (var i = 0; i < projects.length; i++) {
|
|
188
|
+
try {
|
|
189
|
+
var result = await listSessions(projects[i].slug, { limit: 40 });
|
|
190
|
+
totalSessions += result.sessions.length;
|
|
191
|
+
broadcast({
|
|
192
|
+
type: "session:list",
|
|
193
|
+
projectSlug: projects[i].slug,
|
|
194
|
+
sessions: result.sessions,
|
|
195
|
+
totalCount: result.totalCount,
|
|
196
|
+
} as any);
|
|
197
|
+
if (result.sessions.length > 0) {
|
|
198
|
+
recentSessionIds.push({
|
|
199
|
+
projectSlug: projects[i].slug,
|
|
200
|
+
sessionId: result.sessions[0].id,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
} catch {}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
log.server("Data warmup: cached %d sessions across %d projects (%dms)", totalSessions, projects.length, Date.now() - t0);
|
|
207
|
+
|
|
208
|
+
for (var j = 0; j < recentSessionIds.length; j++) {
|
|
209
|
+
try {
|
|
210
|
+
await loadSessionHistory(recentSessionIds[j].projectSlug, recentSessionIds[j].sessionId);
|
|
211
|
+
} catch {}
|
|
212
|
+
}
|
|
213
|
+
if (recentSessionIds.length > 0) {
|
|
214
|
+
log.server("Data warmup: pre-cached history for %d recent sessions (%dms)", recentSessionIds.length, Date.now() - t0);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function getWarmupModels(): ModelEntry[] {
|
|
219
|
+
return warmupModels.length > 0 ? warmupModels : KNOWN_MODELS.slice();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function getWarmupSlashCommands(): string[] {
|
|
223
|
+
return warmupSlashCommands;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function getWarmupAccountInfo(): AccountInfo | null {
|
|
227
|
+
return warmupAccountInfo;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function isWarmupComplete(): boolean {
|
|
231
|
+
return warmupComplete;
|
|
232
|
+
}
|