@cryptiklemur/lattice 1.46.7 → 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.
@@ -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, statSync, readdirSync, readlinkSync } from "node:fs";
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 KNOWN_MODELS.slice();
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 getActiveSessionCountForProject(projectPath: string): number {
216
- var count = 0;
217
- var hash = projectPath.replace(/\//g, "-");
218
- var dir = join(homedir(), ".claude", "projects", hash);
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[]; time: number }>();
469
- var HISTORY_CACHE_TTL = 60000;
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 && Date.now() - cached.time < HISTORY_CACHE_TTL) {
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, time: Date.now() });
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, time: Date.now() });
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, time: Date.now() });
568
- cached = { messages: allMessages, time: Date.now() };
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
+ }