@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.
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-DVNvS0GQ.js → CompareDrawer-DBTFzxG5.js} +1 -1
- package/.output/public/assets/{ProxyViewerContainer-By3XtQhZ.js → ProxyViewerContainer-ivZk8MgE.js} +4 -4
- package/.output/public/assets/{ReplayDialog-Cw6bN-NR.js → ReplayDialog-syW2hDNE.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-DnIsirBE.js → RequestAnatomy-BkuRtY9o.js} +1 -1
- package/.output/public/assets/{ResponseView-ygsUs7FU.js → ResponseView-BwyvEvoE.js} +1 -1
- package/.output/public/assets/{StreamingChunkSequence-DMUn8qXi.js → StreamingChunkSequence-BY4MbPKg.js} +1 -1
- package/.output/public/assets/_sessionId-XlPUgIIb.js +1 -0
- package/.output/public/assets/index-By11a28-.js +1 -0
- package/.output/public/assets/{main-kBQ49n13.js → main-DKRDRBdd.js} +2 -2
- package/.output/server/{_sessionId-CYKaI68v.mjs → _sessionId-VDd4N_1B.mjs} +2 -2
- package/.output/server/_ssr/{CompareDrawer-CHk4p41X.mjs → CompareDrawer-BQZlxsOC.mjs} +2 -2
- package/.output/server/_ssr/{ProxyViewerContainer-Co9ENCKV.mjs → ProxyViewerContainer-njY2oQCc.mjs} +7 -7
- package/.output/server/_ssr/{ReplayDialog-BRs3fk8H.mjs → ReplayDialog-B8lkdAIh.mjs} +3 -3
- package/.output/server/_ssr/{RequestAnatomy-Cb68EeZz.mjs → RequestAnatomy-CwOTfdwS.mjs} +2 -2
- package/.output/server/_ssr/{ResponseView-DGdbfEjs.mjs → ResponseView-3Iz_mZpd.mjs} +2 -2
- package/.output/server/_ssr/{StreamingChunkSequence-DptYgirK.mjs → StreamingChunkSequence-hmJcQNW5.mjs} +2 -2
- package/.output/server/_ssr/{index-Ef5mCuww.mjs → index-UuTKM4aC.mjs} +2 -2
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{router-Cvv7SJnd.mjs → router-C7InHrxE.mjs} +351 -9
- package/.output/server/{_tanstack-start-manifest_v-DQoXxIFQ.mjs → _tanstack-start-manifest_v-B6yfnMHA.mjs} +1 -1
- package/.output/server/index.mjs +54 -54
- package/package.json +1 -1
- package/src/lib/sessionInfoContract.ts +69 -0
- package/src/mcp/server.ts +26 -0
- package/src/mcp/toolHandlers.ts +27 -0
- package/src/proxy/sessionInfo.ts +222 -0
- package/src/proxy/sessionSupervisor.ts +8 -5
- package/src/proxy/store.ts +77 -0
- package/src/routes/api/sessions.ts +29 -2
- package/.output/public/assets/_sessionId-DoqvnFx7.js +0 -1
- 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",
|
package/src/mcp/toolHandlers.ts
CHANGED
|
@@ -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.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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;
|
package/src/proxy/store.ts
CHANGED
|
@@ -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};
|