@tonyclaw/agent-inspector 2.0.25 → 2.0.26

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 (32) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-DVNvS0GQ.js → CompareDrawer-DBTFzxG5.js} +1 -1
  3. package/.output/public/assets/{ProxyViewerContainer-By3XtQhZ.js → ProxyViewerContainer-ivZk8MgE.js} +4 -4
  4. package/.output/public/assets/{ReplayDialog-Cw6bN-NR.js → ReplayDialog-syW2hDNE.js} +1 -1
  5. package/.output/public/assets/{RequestAnatomy-DnIsirBE.js → RequestAnatomy-BkuRtY9o.js} +1 -1
  6. package/.output/public/assets/{ResponseView-ygsUs7FU.js → ResponseView-BwyvEvoE.js} +1 -1
  7. package/.output/public/assets/{StreamingChunkSequence-DMUn8qXi.js → StreamingChunkSequence-BY4MbPKg.js} +1 -1
  8. package/.output/public/assets/_sessionId-XlPUgIIb.js +1 -0
  9. package/.output/public/assets/index-By11a28-.js +1 -0
  10. package/.output/public/assets/{main-kBQ49n13.js → main-DKRDRBdd.js} +2 -2
  11. package/.output/server/{_sessionId-CYKaI68v.mjs → _sessionId-VDd4N_1B.mjs} +2 -2
  12. package/.output/server/_ssr/{CompareDrawer-CHk4p41X.mjs → CompareDrawer-BQZlxsOC.mjs} +2 -2
  13. package/.output/server/_ssr/{ProxyViewerContainer-Co9ENCKV.mjs → ProxyViewerContainer-njY2oQCc.mjs} +7 -7
  14. package/.output/server/_ssr/{ReplayDialog-BRs3fk8H.mjs → ReplayDialog-B8lkdAIh.mjs} +3 -3
  15. package/.output/server/_ssr/{RequestAnatomy-Cb68EeZz.mjs → RequestAnatomy-CwOTfdwS.mjs} +2 -2
  16. package/.output/server/_ssr/{ResponseView-DGdbfEjs.mjs → ResponseView-3Iz_mZpd.mjs} +2 -2
  17. package/.output/server/_ssr/{StreamingChunkSequence-DptYgirK.mjs → StreamingChunkSequence-hmJcQNW5.mjs} +2 -2
  18. package/.output/server/_ssr/{index-Ef5mCuww.mjs → index-UuTKM4aC.mjs} +2 -2
  19. package/.output/server/_ssr/index.mjs +2 -2
  20. package/.output/server/_ssr/{router-Cvv7SJnd.mjs → router-C7InHrxE.mjs} +351 -9
  21. package/.output/server/{_tanstack-start-manifest_v-DQoXxIFQ.mjs → _tanstack-start-manifest_v-B6yfnMHA.mjs} +1 -1
  22. package/.output/server/index.mjs +54 -54
  23. package/package.json +1 -1
  24. package/src/lib/sessionInfoContract.ts +69 -0
  25. package/src/mcp/server.ts +26 -0
  26. package/src/mcp/toolHandlers.ts +27 -0
  27. package/src/proxy/sessionInfo.ts +222 -0
  28. package/src/proxy/sessionSupervisor.ts +8 -5
  29. package/src/proxy/store.ts +77 -0
  30. package/src/routes/api/sessions.ts +29 -2
  31. package/.output/public/assets/_sessionId-DoqvnFx7.js +0 -1
  32. package/.output/public/assets/index-B0YcFeL-.js +0 -1
@@ -0,0 +1,69 @@
1
+ import { z } from "zod";
2
+
3
+ export const SessionTokenUsageSchema = z.object({
4
+ input: z.number().int().nonnegative(),
5
+ output: z.number().int().nonnegative(),
6
+ cacheCreate: z.number().int().nonnegative(),
7
+ cacheRead: z.number().int().nonnegative(),
8
+ total: z.number().int().nonnegative(),
9
+ });
10
+
11
+ export const SessionLogSummarySchema = z.object({
12
+ id: z.number().int().positive(),
13
+ timestamp: z.string(),
14
+ provider: z.string().nullable(),
15
+ model: z.string().nullable(),
16
+ apiFormat: z.enum(["anthropic", "openai", "unknown"]),
17
+ method: z.string(),
18
+ path: z.string(),
19
+ status: z.number().nullable(),
20
+ isStreaming: z.boolean(),
21
+ hasError: z.boolean(),
22
+ error: z.string().nullable(),
23
+ latencyMs: z.number().nullable(),
24
+ tokens: z.object({
25
+ input: z.number().nullable(),
26
+ output: z.number().nullable(),
27
+ cacheCreate: z.number().nullable(),
28
+ cacheRead: z.number().nullable(),
29
+ }),
30
+ sessionId: z.string().nullable(),
31
+ });
32
+
33
+ export const SessionInfoSchema = z.object({
34
+ id: z.string(),
35
+ status: z.enum(["active", "failed", "completed", "empty"]),
36
+ logCoverage: z.enum(["recent", "history"]),
37
+ source: z.enum(["explicit", "client-process", "client-connection", "provider-test"]).nullable(),
38
+ runtimeMode: z.enum(["in-process", "worker-thread", "child-process"]).nullable(),
39
+ createdAt: z.string().nullable(),
40
+ updatedAt: z.string().nullable(),
41
+ requestCount: z.number().int().nonnegative(),
42
+ activeRequests: z.number().int().nonnegative(),
43
+ completedRequests: z.number().int().nonnegative(),
44
+ failedRequests: z.number().int().nonnegative(),
45
+ queuedTasks: z.number().int().nonnegative(),
46
+ runningTasks: z.number().int().nonnegative(),
47
+ lastTaskError: z.string().nullable(),
48
+ logCount: z.number().int().nonnegative(),
49
+ errorCount: z.number().int().nonnegative(),
50
+ models: z.array(z.string()),
51
+ providers: z.array(z.string()),
52
+ tokenUsage: SessionTokenUsageSchema,
53
+ lastLogId: z.number().nullable(),
54
+ lastModel: z.string().nullable(),
55
+ clientPid: z.number().nullable(),
56
+ clientProjectFolder: z.string().nullable(),
57
+ inspectorPath: z.string(),
58
+ inspectorUrl: z.string(),
59
+ apiPath: z.string(),
60
+ apiUrl: z.string(),
61
+ compactLogsPath: z.string(),
62
+ compactLogsUrl: z.string(),
63
+ latestLogLimit: z.number().int().positive(),
64
+ latestLogs: z.array(SessionLogSummarySchema),
65
+ });
66
+
67
+ export type SessionTokenUsage = z.infer<typeof SessionTokenUsageSchema>;
68
+ export type SessionLogSummary = z.infer<typeof SessionLogSummarySchema>;
69
+ export type SessionInfo = z.infer<typeof SessionInfoSchema>;
package/src/mcp/server.ts CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  addProviderImpl,
24
24
  createSessionKnowledgeImpl,
25
25
  getProjectContextImpl,
26
+ getSessionImpl,
26
27
  getLogChunksImpl,
27
28
  getLogImpl,
28
29
  getProviderImpl,
@@ -201,6 +202,30 @@ function registerTools(server: McpServer): void {
201
202
  () => safeCall(() => listSessionsImpl(callApi)),
202
203
  );
203
204
 
205
+ server.registerTool(
206
+ "inspector_get_session",
207
+ {
208
+ title: "Get Inspector session info",
209
+ description:
210
+ "Returns a lightweight, evidence-friendly summary for one Inspector session: clickable inspectorUrl, compact logs URL, status, request/error counts, token totals, models/providers, and the latest compact log summaries. Defaults to recent in-memory/cache data for speed; set includeHistory=true only when you explicitly want to scan persisted jsonl history. It avoids raw request/response bodies so it is safe to use in evaluation artifacts.",
211
+ inputSchema: z.object({
212
+ sessionId: z.string().min(1).describe("The Inspector session id."),
213
+ includeHistory: z
214
+ .boolean()
215
+ .optional()
216
+ .describe("Scan persisted jsonl history for this session. Slower; default false."),
217
+ latestLogLimit: z
218
+ .number()
219
+ .int()
220
+ .positive()
221
+ .max(50)
222
+ .optional()
223
+ .describe("How many latest compact log summaries to include. Max 50, default 10."),
224
+ }),
225
+ },
226
+ (args) => safeCall(() => getSessionImpl(callApi, args)),
227
+ );
228
+
204
229
  server.registerTool(
205
230
  "inspector_list_models",
206
231
  {
@@ -389,6 +414,7 @@ export const TOOL_NAMES = [
389
414
  "inspector_get_log",
390
415
  "inspector_get_log_chunks",
391
416
  "inspector_list_sessions",
417
+ "inspector_get_session",
392
418
  "inspector_list_models",
393
419
  "inspector_list_providers",
394
420
  "inspector_get_provider",
@@ -17,6 +17,7 @@ import { Buffer } from "node:buffer";
17
17
  import { z } from "zod";
18
18
  import { LoopbackTimeoutError, type CallApiOptions } from "./loopback";
19
19
  import { extractLastUserMessagePreview, extractResponsePreview } from "./previewExtractor";
20
+ import { SessionInfoSchema } from "../lib/sessionInfoContract";
20
21
  import { CapturedLogSchema, type CapturedLog } from "../proxy/schemas";
21
22
 
22
23
  /** Minimal callApi contract: same shape as the real `callApi` in loopback.ts. */
@@ -191,6 +192,32 @@ export async function listSessionsImpl(callApi: CallApiFn): Promise<ToolResult>
191
192
  return textJson(await res.json());
192
193
  }
193
194
 
195
+ export type GetSessionArgs = {
196
+ sessionId: string;
197
+ includeHistory?: boolean;
198
+ latestLogLimit?: number;
199
+ };
200
+
201
+ export async function getSessionImpl(
202
+ callApi: CallApiFn,
203
+ args: GetSessionArgs,
204
+ ): Promise<ToolResult> {
205
+ const params = new URLSearchParams({ sessionId: args.sessionId });
206
+ if (args.latestLogLimit !== undefined) {
207
+ params.set("limit", String(args.latestLogLimit));
208
+ }
209
+ if (args.includeHistory === true) {
210
+ params.set("includeHistory", "1");
211
+ }
212
+ const path = `/api/sessions?${params.toString()}`;
213
+ const res = await callApi(path);
214
+ if (!res.ok) return toolError(`GET ${path} returned ${res.status}`);
215
+ const rawBody: unknown = await res.json();
216
+ const parsed = SessionInfoSchema.safeParse(rawBody);
217
+ if (!parsed.success) return toolError("GET /api/sessions returned an unparseable session info");
218
+ return textJson(parsed.data);
219
+ }
220
+
194
221
  export async function listModelsImpl(callApi: CallApiFn): Promise<ToolResult> {
195
222
  const res = await callApi("/api/models");
196
223
  if (!res.ok) return toolError(`GET /api/models returned ${res.status}`);
@@ -0,0 +1,222 @@
1
+ import type { SessionInfo, SessionLogSummary, SessionTokenUsage } from "../lib/sessionInfoContract";
2
+ import { getSessionPath } from "../lib/sessionUrl";
3
+ import type { CapturedLog } from "./schemas";
4
+ import type { SessionSnapshot } from "./sessionSupervisor";
5
+
6
+ export const SESSION_INFO_DEFAULT_LATEST_LOG_LIMIT = 10;
7
+ export const SESSION_INFO_MAX_LATEST_LOG_LIMIT = 50;
8
+
9
+ type SessionStatus = SessionInfo["status"];
10
+
11
+ type BuildSessionInfoInput = {
12
+ sessionId: string;
13
+ baseUrl: string;
14
+ snapshot: SessionSnapshot | null;
15
+ summaries: readonly SessionLogSummary[];
16
+ logCoverage: SessionInfo["logCoverage"];
17
+ latestLogLimit?: number;
18
+ };
19
+
20
+ export function clampSessionInfoLogLimit(input: number | undefined): number {
21
+ if (input === undefined) return SESSION_INFO_DEFAULT_LATEST_LOG_LIMIT;
22
+ if (!Number.isFinite(input) || input < 1) return 1;
23
+ return Math.min(Math.floor(input), SESSION_INFO_MAX_LATEST_LOG_LIMIT);
24
+ }
25
+
26
+ function normalizeString(value: string | null | undefined): string | null {
27
+ if (value === null || value === undefined || value.length === 0) return null;
28
+ return value;
29
+ }
30
+
31
+ function truncateError(value: string | null | undefined): string | null {
32
+ const normalized = normalizeString(value);
33
+ if (normalized === null) return null;
34
+ if (normalized.length <= 500) return normalized;
35
+ return `${normalized.slice(0, 500)}...`;
36
+ }
37
+
38
+ function hasLogError(log: CapturedLog): boolean {
39
+ const error = normalizeString(log.error);
40
+ if (error !== null) return true;
41
+ return log.responseStatus !== null && log.responseStatus >= 400;
42
+ }
43
+
44
+ export function buildSessionLogSummary(log: CapturedLog): SessionLogSummary {
45
+ return {
46
+ id: log.id,
47
+ timestamp: log.timestamp,
48
+ provider: normalizeString(log.providerName),
49
+ model: normalizeString(log.model),
50
+ apiFormat: log.apiFormat,
51
+ method: log.method,
52
+ path: log.path,
53
+ status: log.responseStatus,
54
+ isStreaming: log.streaming,
55
+ hasError: hasLogError(log),
56
+ error: truncateError(log.error),
57
+ latencyMs: log.elapsedMs,
58
+ tokens: {
59
+ input: log.inputTokens,
60
+ output: log.outputTokens,
61
+ cacheCreate: log.cacheCreationInputTokens,
62
+ cacheRead: log.cacheReadInputTokens,
63
+ },
64
+ sessionId: normalizeString(log.sessionId),
65
+ };
66
+ }
67
+
68
+ function nonNegativeInteger(value: number | null): number {
69
+ if (value === null || !Number.isFinite(value) || value < 0) return 0;
70
+ return Math.floor(value);
71
+ }
72
+
73
+ function addTokenUsage(total: SessionTokenUsage, summary: SessionLogSummary): SessionTokenUsage {
74
+ const input = nonNegativeInteger(summary.tokens.input);
75
+ const output = nonNegativeInteger(summary.tokens.output);
76
+ const cacheCreate = nonNegativeInteger(summary.tokens.cacheCreate);
77
+ const cacheRead = nonNegativeInteger(summary.tokens.cacheRead);
78
+ return {
79
+ input: total.input + input,
80
+ output: total.output + output,
81
+ cacheCreate: total.cacheCreate + cacheCreate,
82
+ cacheRead: total.cacheRead + cacheRead,
83
+ total: total.total + input + output + cacheCreate + cacheRead,
84
+ };
85
+ }
86
+
87
+ function buildRelativePath(pathname: string, params: URLSearchParams): string {
88
+ const query = params.toString();
89
+ return query.length > 0 ? `${pathname}?${query}` : pathname;
90
+ }
91
+
92
+ function buildAbsoluteUrl(baseUrl: string, path: string): string {
93
+ try {
94
+ return new URL(path, baseUrl).toString();
95
+ } catch {
96
+ return path;
97
+ }
98
+ }
99
+
100
+ function buildApiPath(sessionId: string): string {
101
+ return buildRelativePath("/api/sessions", new URLSearchParams({ sessionId }));
102
+ }
103
+
104
+ function buildCompactLogsPath(sessionId: string): string {
105
+ return buildRelativePath("/api/logs", new URLSearchParams({ compact: "1", sessionId }));
106
+ }
107
+
108
+ function collectSortedValues(values: readonly (string | null)[]): string[] {
109
+ const set = new Set<string>();
110
+ for (const value of values) {
111
+ if (value !== null && value.length > 0) {
112
+ set.add(value);
113
+ }
114
+ }
115
+ return [...set].sort();
116
+ }
117
+
118
+ function countCompletedLogs(summaries: readonly SessionLogSummary[]): number {
119
+ let count = 0;
120
+ for (const summary of summaries) {
121
+ if (summary.status !== null || summary.latencyMs !== null) {
122
+ count += 1;
123
+ }
124
+ }
125
+ return count;
126
+ }
127
+
128
+ function countErrorLogs(summaries: readonly SessionLogSummary[]): number {
129
+ let count = 0;
130
+ for (const summary of summaries) {
131
+ if (summary.hasError) {
132
+ count += 1;
133
+ }
134
+ }
135
+ return count;
136
+ }
137
+
138
+ function deriveSessionStatus(input: {
139
+ activeRequests: number;
140
+ runningTasks: number;
141
+ failedRequests: number;
142
+ errorCount: number;
143
+ lastTaskError: string | null;
144
+ completedRequests: number;
145
+ logCount: number;
146
+ }): SessionStatus {
147
+ if (input.activeRequests > 0 || input.runningTasks > 0) return "active";
148
+ if (input.failedRequests > 0 || input.errorCount > 0 || input.lastTaskError !== null) {
149
+ return "failed";
150
+ }
151
+ if (input.completedRequests > 0 || input.logCount > 0) return "completed";
152
+ return "empty";
153
+ }
154
+
155
+ export function buildSessionInfo(input: BuildSessionInfoInput): SessionInfo {
156
+ const latestLogLimit = clampSessionInfoLogLimit(input.latestLogLimit);
157
+ const summaries = [...input.summaries].sort((left, right) => right.id - left.id);
158
+ const latestLog = summaries[0] ?? null;
159
+ const oldestLog = summaries.length > 0 ? summaries[summaries.length - 1] : null;
160
+ const logCount =
161
+ input.logCoverage === "history"
162
+ ? summaries.length
163
+ : Math.max(input.snapshot?.requestCount ?? 0, summaries.length);
164
+ const errorCount = Math.max(countErrorLogs(summaries), input.snapshot?.failedRequests ?? 0);
165
+ const completedLogCount = countCompletedLogs(summaries);
166
+ const tokenUsage = summaries.reduce<SessionTokenUsage>(
167
+ (total, summary) => addTokenUsage(total, summary),
168
+ { input: 0, output: 0, cacheCreate: 0, cacheRead: 0, total: 0 },
169
+ );
170
+ const requestCount = Math.max(input.snapshot?.requestCount ?? 0, logCount);
171
+ const activeRequests = input.snapshot?.activeRequests ?? 0;
172
+ const completedRequests = Math.max(input.snapshot?.completedRequests ?? 0, completedLogCount);
173
+ const failedRequests = Math.max(input.snapshot?.failedRequests ?? 0, errorCount);
174
+ const queuedTasks = input.snapshot?.queuedTasks ?? 0;
175
+ const runningTasks = input.snapshot?.runningTasks ?? 0;
176
+ const lastTaskError = normalizeString(input.snapshot?.lastTaskError);
177
+ const inspectorPath = getSessionPath(input.sessionId);
178
+ const apiPath = buildApiPath(input.sessionId);
179
+ const compactLogsPath = buildCompactLogsPath(input.sessionId);
180
+
181
+ return {
182
+ id: input.sessionId,
183
+ status: deriveSessionStatus({
184
+ activeRequests,
185
+ runningTasks,
186
+ failedRequests,
187
+ errorCount,
188
+ lastTaskError,
189
+ completedRequests,
190
+ logCount,
191
+ }),
192
+ logCoverage: input.logCoverage,
193
+ source: input.snapshot?.source ?? null,
194
+ runtimeMode: input.snapshot?.runtimeMode ?? null,
195
+ createdAt: input.snapshot?.createdAt ?? oldestLog?.timestamp ?? null,
196
+ updatedAt: input.snapshot?.updatedAt ?? latestLog?.timestamp ?? null,
197
+ requestCount,
198
+ activeRequests,
199
+ completedRequests,
200
+ failedRequests,
201
+ queuedTasks,
202
+ runningTasks,
203
+ lastTaskError,
204
+ logCount,
205
+ errorCount,
206
+ models: collectSortedValues(summaries.map((summary) => summary.model)),
207
+ providers: collectSortedValues(summaries.map((summary) => summary.provider)),
208
+ tokenUsage,
209
+ lastLogId: input.snapshot?.lastLogId ?? latestLog?.id ?? null,
210
+ lastModel: normalizeString(input.snapshot?.lastModel) ?? latestLog?.model ?? null,
211
+ clientPid: input.snapshot?.clientPid ?? null,
212
+ clientProjectFolder: normalizeString(input.snapshot?.clientProjectFolder),
213
+ inspectorPath,
214
+ inspectorUrl: buildAbsoluteUrl(input.baseUrl, inspectorPath),
215
+ apiPath,
216
+ apiUrl: buildAbsoluteUrl(input.baseUrl, apiPath),
217
+ compactLogsPath,
218
+ compactLogsUrl: buildAbsoluteUrl(input.baseUrl, compactLogsPath),
219
+ latestLogLimit,
220
+ latestLogs: summaries.slice(0, latestLogLimit),
221
+ };
222
+ }
@@ -159,11 +159,14 @@ function upsertSession(
159
159
 
160
160
  const existing = sessions.get(identity.id);
161
161
  const session = existing ?? createSessionRecord(identity.id, source, log);
162
- session.updatedAt = new Date().toISOString();
163
- session.lastLogId = log.id;
164
- session.lastModel = log.model ?? session.lastModel;
165
- session.clientPid = log.clientPid ?? session.clientPid;
166
- session.clientProjectFolder = log.clientProjectFolder ?? session.clientProjectFolder;
162
+ const isSameOrNewerLog = session.lastLogId === null || log.id >= session.lastLogId;
163
+ if (isSameOrNewerLog) {
164
+ session.updatedAt = new Date().toISOString();
165
+ session.lastLogId = log.id;
166
+ session.lastModel = log.model ?? session.lastModel;
167
+ session.clientPid = log.clientPid ?? session.clientPid;
168
+ session.clientProjectFolder = log.clientProjectFolder ?? session.clientProjectFolder;
169
+ }
167
170
 
168
171
  sessions.set(identity.id, session);
169
172
  return session;
@@ -30,11 +30,14 @@ import {
30
30
  type LogIndexEntry,
31
31
  } from "./logIndex";
32
32
  import { writeChunks } from "./chunkStorage";
33
+ import type { SessionInfo, SessionLogSummary } from "../lib/sessionInfoContract";
34
+ import { buildSessionInfo, buildSessionLogSummary } from "./sessionInfo";
33
35
  import type { CapturedLog } from "./schemas";
34
36
  import { CapturedLogSchema } from "./schemas";
35
37
  import {
36
38
  clearSessionRegistry,
37
39
  getLogSessionId,
40
+ getSessionSnapshots,
38
41
  getSessionIds,
39
42
  markSessionFinished,
40
43
  markSessionStarted,
@@ -42,6 +45,7 @@ import {
42
45
  rebuildSessionRegistry,
43
46
  resolveSessionIdentity,
44
47
  type SessionClientInfo,
48
+ type SessionSnapshot,
45
49
  } from "./sessionSupervisor";
46
50
 
47
51
  export type { CapturedLog };
@@ -87,6 +91,12 @@ export type ListLogsPageResult = {
87
91
  limit: number;
88
92
  };
89
93
 
94
+ export type GetSessionInfoOptions = {
95
+ baseUrl: string;
96
+ includeHistory?: boolean;
97
+ latestLogLimit?: number;
98
+ };
99
+
90
100
  // Memory cache: id -> CapturedLog (recent logs only, FIFO eviction)
91
101
  const memoryCache: Map<number, CapturedLog> = new Map();
92
102
 
@@ -506,6 +516,73 @@ export async function listLogsPage(options: ListLogsPageOptions): Promise<ListLo
506
516
  };
507
517
  }
508
518
 
519
+ function findSessionSnapshot(sessionId: string): SessionSnapshot | null {
520
+ for (const snapshot of getSessionSnapshots()) {
521
+ if (snapshot.id === sessionId) return snapshot;
522
+ }
523
+ return null;
524
+ }
525
+
526
+ async function collectSessionLogSummaries(
527
+ sessionId: string,
528
+ includeHistory: boolean,
529
+ ): Promise<SessionLogSummary[]> {
530
+ const summariesById = new Map<number, SessionLogSummary>();
531
+ const observeMatchingLog = (log: CapturedLog): void => {
532
+ if (!matchesLogFilters(log, sessionId, undefined)) return;
533
+ summariesById.set(log.id, buildSessionLogSummary(log));
534
+ };
535
+
536
+ if (includeHistory) {
537
+ await visitPersistedLogs((log) => {
538
+ observeSessionLog(log);
539
+ observeMatchingLog(log);
540
+ });
541
+ }
542
+
543
+ for (const log of memoryCache.values()) {
544
+ observeMatchingLog(log);
545
+ }
546
+
547
+ return [...summariesById.values()];
548
+ }
549
+
550
+ async function includeSnapshotLastLogSummary(
551
+ sessionId: string,
552
+ snapshot: SessionSnapshot | null,
553
+ summaries: SessionLogSummary[],
554
+ ): Promise<SessionLogSummary[]> {
555
+ if (snapshot === null || snapshot.lastLogId === null) return summaries;
556
+ for (const summary of summaries) {
557
+ if (summary.id === snapshot.lastLogId) return summaries;
558
+ }
559
+
560
+ const log = await getLogById(snapshot.lastLogId);
561
+ if (log === null || !matchesLogFilters(log, sessionId, undefined)) return summaries;
562
+ return [...summaries, buildSessionLogSummary(log)];
563
+ }
564
+
565
+ export async function getSessionInfo(
566
+ sessionId: string,
567
+ options: GetSessionInfoOptions,
568
+ ): Promise<SessionInfo | null> {
569
+ const includeHistory = options.includeHistory === true;
570
+ const collectedSummaries = await collectSessionLogSummaries(sessionId, includeHistory);
571
+ const snapshot = findSessionSnapshot(sessionId);
572
+ const summaries = includeHistory
573
+ ? collectedSummaries
574
+ : await includeSnapshotLastLogSummary(sessionId, snapshot, collectedSummaries);
575
+ if (snapshot === null && summaries.length === 0) return null;
576
+ return buildSessionInfo({
577
+ sessionId,
578
+ baseUrl: options.baseUrl,
579
+ snapshot,
580
+ summaries,
581
+ logCoverage: includeHistory ? "history" : "recent",
582
+ latestLogLimit: options.latestLogLimit,
583
+ });
584
+ }
585
+
509
586
  export function getSessions(): string[] {
510
587
  return getSessionIds();
511
588
  }
@@ -1,11 +1,38 @@
1
1
  import { createFileRoute } from "@tanstack/react-router";
2
- import { getSessionSnapshots, getSessions } from "../../proxy/store";
2
+ import { getSessionInfo, getSessionSnapshots, getSessions } from "../../proxy/store";
3
+
4
+ function parsePositiveInt(value: string | null): number | undefined {
5
+ if (value === null) return undefined;
6
+ const parsed = Number(value);
7
+ if (!Number.isInteger(parsed) || parsed < 1) return undefined;
8
+ return parsed;
9
+ }
10
+
11
+ function parseBooleanFlag(value: string | null): boolean {
12
+ return value === "1" || value === "true";
13
+ }
3
14
 
4
15
  export const Route = createFileRoute("/api/sessions")({
5
16
  server: {
6
17
  handlers: {
7
- GET: ({ request }: { request: Request }) => {
18
+ GET: async ({ request }: { request: Request }) => {
8
19
  const url = new URL(request.url);
20
+ const sessionId = url.searchParams.get("sessionId");
21
+ if (sessionId !== null) {
22
+ if (sessionId.length === 0) {
23
+ return Response.json({ error: "sessionId is required" }, { status: 400 });
24
+ }
25
+ const info = await getSessionInfo(sessionId, {
26
+ baseUrl: url.origin,
27
+ includeHistory: parseBooleanFlag(url.searchParams.get("includeHistory")),
28
+ latestLogLimit: parsePositiveInt(url.searchParams.get("limit")),
29
+ });
30
+ if (info === null) {
31
+ return Response.json({ error: "Session not found" }, { status: 404 });
32
+ }
33
+ return Response.json(info);
34
+ }
35
+
9
36
  const details = url.searchParams.get("details");
10
37
  if (details === "true" || details === "1") {
11
38
  return Response.json(getSessionSnapshots());
@@ -1 +0,0 @@
1
- import{R as s,j as e}from"./main-kBQ49n13.js";import{P as i}from"./ProxyViewerContainer-By3XtQhZ.js";function t(){const{sessionId:o}=s.useParams();return e.jsx(i,{initialSessionId:o},o)}export{t as component};
@@ -1 +0,0 @@
1
- import{P as o}from"./ProxyViewerContainer-By3XtQhZ.js";import"./main-kBQ49n13.js";const r=o;export{r as component};