@tonyclaw/llm-inspector 1.17.0 → 1.18.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.
Files changed (34) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-C4fie5g5.js → CompareDrawer-BpwZCB6M.js} +1 -1
  3. package/.output/public/assets/{ReplayDialog-Dme5uOR9.js → ReplayDialog-Clratkzl.js} +1 -1
  4. package/.output/public/assets/{RequestAnatomy-ChBLDNFH.js → RequestAnatomy-EtiX0r_G.js} +1 -1
  5. package/.output/public/assets/{ResponseView-wGeqBzVU.js → ResponseView-CJqxo-EN.js} +1 -1
  6. package/.output/public/assets/{StreamingChunkSequence-zeJZQLqT.js → StreamingChunkSequence-BIbRqQiV.js} +1 -1
  7. package/.output/public/assets/{index-DpbutOvo.js → index-B-0F9n1w.js} +17 -17
  8. package/.output/public/assets/{json-viewer-BV-WUszW.js → json-viewer-D-z1r1Pp.js} +1 -1
  9. package/.output/public/assets/{main-DRu10KNQ.js → main-CZJ63sQh.js} +1 -1
  10. package/.output/server/_ssr/{CompareDrawer-C4-CQL5w.mjs → CompareDrawer-BJr-913n.mjs} +4 -3
  11. package/.output/server/_ssr/{ReplayDialog-BTb1Bam8.mjs → ReplayDialog-BwmToGuR.mjs} +5 -4
  12. package/.output/server/_ssr/{RequestAnatomy-CZFV1IvL.mjs → RequestAnatomy-BmMiPRPB.mjs} +3 -2
  13. package/.output/server/_ssr/{ResponseView-CTZekh65.mjs → ResponseView-ZB9-8Raw.mjs} +4 -3
  14. package/.output/server/_ssr/{StreamingChunkSequence-C38Ynabd.mjs → StreamingChunkSequence-DWm4CQWC.mjs} +4 -3
  15. package/.output/server/_ssr/{index-Cnu-QzAy.mjs → index-C7I_Qgt0.mjs} +24 -36
  16. package/.output/server/_ssr/index.mjs +2 -2
  17. package/.output/server/_ssr/{json-viewer-DROqpjS9.mjs → json-viewer-D9XETzwp.mjs} +3 -2
  18. package/.output/server/_ssr/{router-pP4GCTQx.mjs → router-711KpGkz.mjs} +647 -98
  19. package/.output/server/{_tanstack-start-manifest_v-CphS4rZd.mjs → _tanstack-start-manifest_v-noQw0Vmw.mjs} +1 -1
  20. package/.output/server/index.mjs +50 -50
  21. package/package.json +1 -1
  22. package/src/components/proxy-viewer/LogEntryHeader.tsx +5 -18
  23. package/src/components/proxy-viewer/TurnGroup.tsx +10 -13
  24. package/src/proxy/handler.ts +52 -84
  25. package/src/proxy/logFinalizer.ts +301 -0
  26. package/src/proxy/logFinalizer.worker.ts +24 -0
  27. package/src/proxy/schemas.ts +8 -3
  28. package/src/proxy/sessionProcess.ts +133 -0
  29. package/src/proxy/sessionRuntime.ts +85 -0
  30. package/src/proxy/sessionSupervisor.ts +282 -0
  31. package/src/proxy/sessionWorkerEntry.ts +26 -0
  32. package/src/proxy/store.ts +64 -20
  33. package/src/routes/api/logs.stream.ts +2 -2
  34. package/src/routes/api/sessions.ts +9 -2
@@ -0,0 +1,282 @@
1
+ import type { CapturedLog } from "./schemas";
2
+
3
+ export const PROVIDER_TEST_SESSION_ID = "provider-test";
4
+
5
+ export type SessionClientInfo = {
6
+ port: number | null;
7
+ pid: number | null;
8
+ cwd: string | null;
9
+ projectFolder: string | null;
10
+ };
11
+
12
+ export type SessionSource = "explicit" | "client-process" | "client-connection" | "provider-test";
13
+
14
+ export type SessionIdentity = {
15
+ id: string | null;
16
+ source: SessionSource | null;
17
+ };
18
+
19
+ export type SessionRuntimeMode = "in-process" | "worker-thread" | "child-process";
20
+
21
+ function getRuntimeMode(): SessionRuntimeMode {
22
+ if (process.env["FINALIZER_USE_WORKER"] === "0") return "in-process";
23
+ const mode = process.env["FINALIZER_RUNTIME"];
24
+ if (mode === "worker") return "worker-thread";
25
+ if (mode === "inline") return "in-process";
26
+ // Default: per-session child process
27
+ return "child-process";
28
+ }
29
+
30
+ export type SessionSnapshot = {
31
+ id: string;
32
+ source: SessionSource;
33
+ runtimeMode: SessionRuntimeMode;
34
+ createdAt: string;
35
+ updatedAt: string;
36
+ requestCount: number;
37
+ activeRequests: number;
38
+ completedRequests: number;
39
+ failedRequests: number;
40
+ queuedTasks: number;
41
+ runningTasks: number;
42
+ lastTaskError: string | null;
43
+ lastLogId: number | null;
44
+ lastModel: string | null;
45
+ clientPid: number | null;
46
+ clientProjectFolder: string | null;
47
+ };
48
+
49
+ type SessionRecord = SessionSnapshot;
50
+
51
+ const sessions = new Map<string, SessionRecord>();
52
+ const knownLogSessions = new Map<number, string>();
53
+ const completedLogIds = new Set<number>();
54
+ const failedLogIds = new Set<number>();
55
+
56
+ function normalizeExplicitSessionId(sessionId: string | null | undefined): string | null {
57
+ if (sessionId === null || sessionId === undefined) return null;
58
+ const trimmed = sessionId.trim();
59
+ return trimmed.length > 0 ? trimmed : null;
60
+ }
61
+
62
+ function buildClientSessionId(clientInfo: SessionClientInfo): SessionIdentity {
63
+ const projectFolder = normalizeExplicitSessionId(clientInfo.projectFolder);
64
+
65
+ if (clientInfo.pid !== null) {
66
+ return {
67
+ id:
68
+ projectFolder !== null
69
+ ? `process:${clientInfo.pid}:${projectFolder}`
70
+ : `process:${clientInfo.pid}`,
71
+ source: "client-process",
72
+ };
73
+ }
74
+
75
+ if (clientInfo.port !== null) {
76
+ return {
77
+ id: `connection:${clientInfo.port}`,
78
+ source: "client-connection",
79
+ };
80
+ }
81
+
82
+ return { id: null, source: null };
83
+ }
84
+
85
+ export function resolveSessionIdentity(input: {
86
+ explicitSessionId: string | null;
87
+ clientInfo?: SessionClientInfo;
88
+ isTest?: boolean;
89
+ }): SessionIdentity {
90
+ if (input.isTest === true) {
91
+ return { id: PROVIDER_TEST_SESSION_ID, source: "provider-test" };
92
+ }
93
+
94
+ const explicit = normalizeExplicitSessionId(input.explicitSessionId);
95
+ if (explicit !== null) {
96
+ return { id: explicit, source: "explicit" };
97
+ }
98
+
99
+ if (input.clientInfo !== undefined) {
100
+ return buildClientSessionId(input.clientInfo);
101
+ }
102
+
103
+ return { id: null, source: null };
104
+ }
105
+
106
+ export function getLogSessionIdentity(log: CapturedLog): SessionIdentity {
107
+ return resolveSessionIdentity({
108
+ explicitSessionId: log.sessionId,
109
+ clientInfo: {
110
+ port: log.clientPort ?? null,
111
+ pid: log.clientPid ?? null,
112
+ cwd: log.clientCwd ?? null,
113
+ projectFolder: log.clientProjectFolder ?? null,
114
+ },
115
+ isTest: log.isTest,
116
+ });
117
+ }
118
+
119
+ export function getLogSessionId(log: CapturedLog): string | null {
120
+ return getLogSessionIdentity(log).id;
121
+ }
122
+
123
+ function getTimestamp(log: CapturedLog): string {
124
+ return log.timestamp.length > 0 ? log.timestamp : new Date().toISOString();
125
+ }
126
+
127
+ function createSessionRecord(id: string, source: SessionSource, log: CapturedLog): SessionRecord {
128
+ const timestamp = getTimestamp(log);
129
+ return {
130
+ id,
131
+ source,
132
+ runtimeMode: getRuntimeMode(),
133
+ createdAt: timestamp,
134
+ updatedAt: timestamp,
135
+ requestCount: 0,
136
+ activeRequests: 0,
137
+ completedRequests: 0,
138
+ failedRequests: 0,
139
+ queuedTasks: 0,
140
+ runningTasks: 0,
141
+ lastTaskError: null,
142
+ lastLogId: null,
143
+ lastModel: null,
144
+ clientPid: log.clientPid ?? null,
145
+ clientProjectFolder: log.clientProjectFolder ?? null,
146
+ };
147
+ }
148
+
149
+ function upsertSession(
150
+ log: CapturedLog,
151
+ sourceOverride?: SessionSource | null,
152
+ ): SessionRecord | null {
153
+ const identity = getLogSessionIdentity(log);
154
+ if (identity.id === null) return null;
155
+
156
+ const source = sourceOverride ?? identity.source;
157
+ if (source === null) return null;
158
+
159
+ const existing = sessions.get(identity.id);
160
+ const session = existing ?? createSessionRecord(identity.id, source, log);
161
+ session.updatedAt = new Date().toISOString();
162
+ session.lastLogId = log.id;
163
+ session.lastModel = log.model ?? session.lastModel;
164
+ session.clientPid = log.clientPid ?? session.clientPid;
165
+ session.clientProjectFolder = log.clientProjectFolder ?? session.clientProjectFolder;
166
+
167
+ sessions.set(identity.id, session);
168
+ return session;
169
+ }
170
+
171
+ function rememberLogSession(log: CapturedLog): boolean {
172
+ const sessionId = getLogSessionId(log);
173
+ if (sessionId === null) return false;
174
+
175
+ const knownSessionId = knownLogSessions.get(log.id);
176
+ if (knownSessionId === sessionId) return false;
177
+
178
+ knownLogSessions.set(log.id, sessionId);
179
+ return true;
180
+ }
181
+
182
+ function isFailedLog(log: CapturedLog): boolean {
183
+ const hasError = log.error !== null && log.error !== undefined && log.error.length > 0;
184
+ const hasErrorStatus = log.responseStatus !== null && log.responseStatus >= 400;
185
+ return hasError || hasErrorStatus;
186
+ }
187
+
188
+ export function markSessionStarted(log: CapturedLog, sourceOverride?: SessionSource | null): void {
189
+ const session = upsertSession(log, sourceOverride);
190
+ if (session === null) return;
191
+
192
+ const isNewLog = rememberLogSession(log);
193
+ if (isNewLog) {
194
+ session.requestCount += 1;
195
+ }
196
+ session.activeRequests += 1;
197
+ }
198
+
199
+ export function markSessionFinished(log: CapturedLog, sourceOverride?: SessionSource | null): void {
200
+ const session = upsertSession(log, sourceOverride);
201
+ if (session === null) return;
202
+
203
+ if (session.activeRequests > 0) {
204
+ session.activeRequests -= 1;
205
+ }
206
+
207
+ if (completedLogIds.has(log.id) === false) {
208
+ completedLogIds.add(log.id);
209
+ session.completedRequests += 1;
210
+ }
211
+
212
+ if (isFailedLog(log) && failedLogIds.has(log.id) === false) {
213
+ failedLogIds.add(log.id);
214
+ session.failedRequests += 1;
215
+ }
216
+ }
217
+
218
+ export function markSessionTaskQueued(sessionId: string | null): void {
219
+ if (sessionId === null) return;
220
+ const session = sessions.get(sessionId);
221
+ if (session === undefined) return;
222
+ session.queuedTasks += 1;
223
+ session.updatedAt = new Date().toISOString();
224
+ }
225
+
226
+ export function markSessionTaskStarted(sessionId: string | null): void {
227
+ if (sessionId === null) return;
228
+ const session = sessions.get(sessionId);
229
+ if (session === undefined) return;
230
+ if (session.queuedTasks > 0) {
231
+ session.queuedTasks -= 1;
232
+ }
233
+ session.runningTasks += 1;
234
+ session.updatedAt = new Date().toISOString();
235
+ }
236
+
237
+ export function markSessionTaskFinished(sessionId: string | null, error: string | null): void {
238
+ if (sessionId === null) return;
239
+ const session = sessions.get(sessionId);
240
+ if (session === undefined) return;
241
+ if (session.runningTasks > 0) {
242
+ session.runningTasks -= 1;
243
+ }
244
+ session.lastTaskError = error;
245
+ session.updatedAt = new Date().toISOString();
246
+ }
247
+
248
+ export function observeSessionLog(log: CapturedLog, sourceOverride?: SessionSource | null): void {
249
+ const session = upsertSession(log, sourceOverride);
250
+ if (session === null) return;
251
+
252
+ const isNewLog = rememberLogSession(log);
253
+ if (isNewLog) {
254
+ session.requestCount += 1;
255
+ }
256
+
257
+ if (log.responseStatus !== null || log.responseText !== null) {
258
+ markSessionFinished(log, sourceOverride);
259
+ }
260
+ }
261
+
262
+ export function clearSessionRegistry(): void {
263
+ sessions.clear();
264
+ knownLogSessions.clear();
265
+ completedLogIds.clear();
266
+ failedLogIds.clear();
267
+ }
268
+
269
+ export function rebuildSessionRegistry(logs: Iterable<CapturedLog>): void {
270
+ clearSessionRegistry();
271
+ for (const log of logs) {
272
+ observeSessionLog(log);
273
+ }
274
+ }
275
+
276
+ export function getSessionIds(): string[] {
277
+ return [...sessions.values()].map((session) => session.id);
278
+ }
279
+
280
+ export function getSessionSnapshots(): SessionSnapshot[] {
281
+ return [...sessions.values()].map((session) => ({ ...session }));
282
+ }
@@ -0,0 +1,26 @@
1
+ import {
2
+ buildFinalizeLogResult,
3
+ type FinalizeLogJob,
4
+ type FinalizeLogResult,
5
+ } from "./logFinalizer";
6
+
7
+ // Child-process entry point for session finalization.
8
+ // Communicates with the parent via IPC (process.send / process.on("message")).
9
+ // Each job is { id: string; job: FinalizeLogJob }, each response is { id: string; result: FinalizeLogResult }.
10
+
11
+ process.on("message", (msg: { id: string; job: FinalizeLogJob }) => {
12
+ let result: FinalizeLogResult;
13
+ try {
14
+ result = buildFinalizeLogResult(msg.job);
15
+ } catch (err) {
16
+ const message = err instanceof Error ? err.message : String(err);
17
+ result = {
18
+ log: msg.job.log,
19
+ upstreamUrl: msg.job.upstreamUrl,
20
+ error: message,
21
+ };
22
+ }
23
+ if (process.send !== undefined) {
24
+ process.send({ id: msg.id, result });
25
+ }
26
+ });
@@ -16,9 +16,21 @@ import {
16
16
  import { writeChunks } from "./chunkStorage";
17
17
  import type { CapturedLog } from "./schemas";
18
18
  import { CapturedLogSchema } from "./schemas";
19
+ import {
20
+ clearSessionRegistry,
21
+ getLogSessionId,
22
+ getSessionIds,
23
+ markSessionFinished,
24
+ markSessionStarted,
25
+ observeSessionLog,
26
+ rebuildSessionRegistry,
27
+ resolveSessionIdentity,
28
+ type SessionClientInfo,
29
+ } from "./sessionSupervisor";
19
30
 
20
31
  export type { CapturedLog };
21
32
  export { getNextLogId };
33
+ export { getLogSessionId, getSessionSnapshots } from "./sessionSupervisor";
22
34
 
23
35
  const MAX_MEMORY_CACHE = 100;
24
36
 
@@ -34,12 +46,19 @@ function evictOldestIfNeeded(): void {
34
46
  }
35
47
  }
36
48
 
49
+ function normalizeLogSession(log: CapturedLog): CapturedLog {
50
+ const sessionId = getLogSessionId(log);
51
+ if (sessionId === log.sessionId) return log;
52
+ return { ...log, sessionId };
53
+ }
54
+
37
55
  function addToCache(log: CapturedLog): void {
56
+ const cachedLog = normalizeLogSession(log);
38
57
  // If updating an existing entry, remove first to reset insertion order
39
- if (memoryCache.has(log.id)) {
40
- memoryCache.delete(log.id);
58
+ if (memoryCache.has(cachedLog.id)) {
59
+ memoryCache.delete(cachedLog.id);
41
60
  }
42
- memoryCache.set(log.id, log);
61
+ memoryCache.set(cachedLog.id, cachedLog);
43
62
  evictOldestIfNeeded();
44
63
  }
45
64
 
@@ -70,22 +89,24 @@ export async function addTestLogEntry(entry: Omit<CapturedLog, "id">): Promise<C
70
89
  );
71
90
  }
72
91
 
92
+ const session = resolveSessionIdentity({
93
+ explicitSessionId: entry.sessionId,
94
+ isTest: entry.isTest,
95
+ });
96
+
73
97
  const log: CapturedLog = {
74
98
  id,
75
99
  ...entry,
100
+ sessionId: session.id,
76
101
  streamingChunksPath,
77
102
  };
78
103
  addToCache(log);
104
+ observeSessionLog(log, session.source);
79
105
  emitLogUpdate(log);
80
106
  return log;
81
107
  }
82
108
 
83
- type ClientInfo = {
84
- port: number | null;
85
- pid: number | null;
86
- cwd: string | null;
87
- projectFolder: string | null;
88
- };
109
+ type ClientInfo = SessionClientInfo;
89
110
 
90
111
  export async function createLog(
91
112
  method: string,
@@ -105,13 +126,18 @@ export async function createLog(
105
126
  const origin = headers.get("origin");
106
127
 
107
128
  const id = preAcquiredId ?? (await getNextLogId());
129
+ const session = resolveSessionIdentity({
130
+ explicitSessionId: sessionId,
131
+ clientInfo,
132
+ });
133
+
108
134
  const log: CapturedLog = {
109
135
  id,
110
136
  timestamp: new Date().toISOString(),
111
137
  method,
112
138
  path,
113
139
  model,
114
- sessionId,
140
+ sessionId: session.id,
115
141
  rawRequestBody: requestBody,
116
142
  responseStatus: null,
117
143
  responseText: null,
@@ -142,11 +168,18 @@ export async function createLog(
142
168
 
143
169
  // Add to memory cache
144
170
  addToCache(log);
171
+ markSessionStarted(log, session.source);
145
172
  emitLogUpdate(log);
146
173
 
147
174
  return log;
148
175
  }
149
176
 
177
+ export function finalizeLogUpdate(log: CapturedLog): void {
178
+ addToCache(log);
179
+ markSessionFinished(log);
180
+ emitLogUpdate(log);
181
+ }
182
+
150
183
  /**
151
184
  * Get a log by ID, checking memory cache first then disk.
152
185
  * Uses byte-offset index for efficient single-line reads.
@@ -182,7 +215,12 @@ export async function getLogById(id: number): Promise<CapturedLog | null> {
182
215
  if (line === "") return null;
183
216
  const parsed: unknown = JSON.parse(line);
184
217
  const result = CapturedLogSchema.safeParse(parsed);
185
- if (result.success) return result.data;
218
+ if (result.success) {
219
+ const log = normalizeLogSession(result.data);
220
+ addToCache(log);
221
+ observeSessionLog(log);
222
+ return log;
223
+ }
186
224
  } finally {
187
225
  await fh.close();
188
226
  }
@@ -203,9 +241,11 @@ export async function getLogById(id: number): Promise<CapturedLog | null> {
203
241
  if (desc !== undefined && typeof desc.value === "number" && desc.value === id) {
204
242
  const result = CapturedLogSchema.safeParse(parsed);
205
243
  if (result.success) {
206
- lastMatch = result.data;
244
+ lastMatch = normalizeLogSession(result.data);
207
245
  if (result.data.responseStatus !== null) {
208
- return result.data;
246
+ addToCache(lastMatch);
247
+ observeSessionLog(lastMatch);
248
+ return lastMatch;
209
249
  }
210
250
  }
211
251
  }
@@ -214,7 +254,11 @@ export async function getLogById(id: number): Promise<CapturedLog | null> {
214
254
  // Skip malformed lines
215
255
  }
216
256
  }
217
- if (lastMatch !== null) return lastMatch;
257
+ if (lastMatch !== null) {
258
+ addToCache(lastMatch);
259
+ observeSessionLog(lastMatch);
260
+ return lastMatch;
261
+ }
218
262
  } catch (err) {
219
263
  logger.error("[store] Failed to read log from disk:", String(err));
220
264
  }
@@ -226,18 +270,14 @@ export function getFilteredLogs(sessionId?: string, model?: string): CapturedLog
226
270
  // Cache maintains insertion order (sorted by ID since logs are added in ID order)
227
271
  // Use spread instead of Array.from for slightly better performance
228
272
  return [...memoryCache.values()].filter((l) => {
229
- if (sessionId !== undefined && l.sessionId !== sessionId) return false;
273
+ if (sessionId !== undefined && getLogSessionId(l) !== sessionId) return false;
230
274
  if (model !== undefined && l.model !== model) return false;
231
275
  return true;
232
276
  });
233
277
  }
234
278
 
235
279
  export function getSessions(): string[] {
236
- const set = new Set<string>();
237
- for (const l of memoryCache.values()) {
238
- if (l.sessionId !== null && l.sessionId !== "") set.add(l.sessionId);
239
- }
240
- return [...set];
280
+ return getSessionIds();
241
281
  }
242
282
 
243
283
  export function getModels(): string[] {
@@ -279,6 +319,8 @@ export async function loadLogsIntoMemory(): Promise<void> {
279
319
  const entry = sorted[i];
280
320
  if (entry !== undefined) addToCache(entry);
281
321
  }
322
+
323
+ rebuildSessionRegistry(memoryCache.values());
282
324
  }
283
325
 
284
326
  async function loadLogFile(
@@ -321,6 +363,7 @@ async function loadLogFile(
321
363
  export function clearAllLogs(): { cleared: number } {
322
364
  const count = memoryCache.size;
323
365
  memoryCache.clear();
366
+ clearSessionRegistry();
324
367
  return { cleared: count };
325
368
  }
326
369
 
@@ -337,6 +380,7 @@ export function clearLogsByIds(ids: readonly number[]): { cleared: number } {
337
380
  for (const id of unique) {
338
381
  removeFromCache(id);
339
382
  }
383
+ rebuildSessionRegistry(memoryCache.values());
340
384
  return { cleared: unique.size };
341
385
  }
342
386
 
@@ -1,5 +1,5 @@
1
1
  import { createFileRoute } from "@tanstack/react-router";
2
- import { onLogUpdate, getFilteredLogs } from "../../proxy/store";
2
+ import { onLogUpdate, getFilteredLogs, getLogSessionId } from "../../proxy/store";
3
3
  import { type CapturedLog } from "../../proxy/schemas";
4
4
 
5
5
  export const Route = createFileRoute("/api/logs/stream")({
@@ -16,7 +16,7 @@ export const Route = createFileRoute("/api/logs/stream")({
16
16
  const unsubscribe = onLogUpdate((log: CapturedLog) => {
17
17
  if (controllerRef === null) return;
18
18
  // Filter by session/model if specified
19
- if (sessionId !== undefined && log.sessionId !== sessionId) return;
19
+ if (sessionId !== undefined && getLogSessionId(log) !== sessionId) return;
20
20
  if (model !== undefined && log.model !== model) return;
21
21
  try {
22
22
  const data = `data: ${JSON.stringify({ type: "update", log })}\n\n`;
@@ -1,10 +1,17 @@
1
1
  import { createFileRoute } from "@tanstack/react-router";
2
- import { getSessions } from "../../proxy/store";
2
+ import { getSessionSnapshots, getSessions } from "../../proxy/store";
3
3
 
4
4
  export const Route = createFileRoute("/api/sessions")({
5
5
  server: {
6
6
  handlers: {
7
- GET: () => Response.json(getSessions()),
7
+ GET: ({ request }: { request: Request }) => {
8
+ const url = new URL(request.url);
9
+ const details = url.searchParams.get("details");
10
+ if (details === "true" || details === "1") {
11
+ return Response.json(getSessionSnapshots());
12
+ }
13
+ return Response.json(getSessions());
14
+ },
8
15
  },
9
16
  },
10
17
  });