@tonyclaw/llm-inspector 1.17.1 → 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.
- package/.output/nitro.json +1 -1
- package/.output/public/assets/{CompareDrawer-BhXCLr7m.js → CompareDrawer-BpwZCB6M.js} +1 -1
- package/.output/public/assets/{ReplayDialog-CzRPSXwa.js → ReplayDialog-Clratkzl.js} +1 -1
- package/.output/public/assets/{RequestAnatomy-lMQonao2.js → RequestAnatomy-EtiX0r_G.js} +1 -1
- package/.output/public/assets/{ResponseView-Bt0vngo0.js → ResponseView-CJqxo-EN.js} +1 -1
- package/.output/public/assets/{StreamingChunkSequence-Dq9XY2E9.js → StreamingChunkSequence-BIbRqQiV.js} +1 -1
- package/.output/public/assets/{index-B4nxi_tZ.js → index-B-0F9n1w.js} +8 -8
- package/.output/public/assets/{json-viewer-C8ttTXtv.js → json-viewer-D-z1r1Pp.js} +1 -1
- package/.output/public/assets/{main-Dgme52Fp.js → main-CZJ63sQh.js} +1 -1
- package/.output/server/_ssr/{CompareDrawer-D-Nj8wmx.mjs → CompareDrawer-BJr-913n.mjs} +4 -3
- package/.output/server/_ssr/{ReplayDialog-DcucC22E.mjs → ReplayDialog-BwmToGuR.mjs} +5 -4
- package/.output/server/_ssr/{RequestAnatomy-aL8GAcW2.mjs → RequestAnatomy-BmMiPRPB.mjs} +3 -2
- package/.output/server/_ssr/{ResponseView-BHgpoGaF.mjs → ResponseView-ZB9-8Raw.mjs} +4 -3
- package/.output/server/_ssr/{StreamingChunkSequence-DrT7StyS.mjs → StreamingChunkSequence-DWm4CQWC.mjs} +4 -3
- package/.output/server/_ssr/{index-nUG0H1oS.mjs → index-C7I_Qgt0.mjs} +18 -20
- package/.output/server/_ssr/index.mjs +2 -2
- package/.output/server/_ssr/{json-viewer-DLsDT0RE.mjs → json-viewer-D9XETzwp.mjs} +3 -2
- package/.output/server/_ssr/{router-DG_jmXCF.mjs → router-711KpGkz.mjs} +647 -98
- package/.output/server/{_tanstack-start-manifest_v-D0JtrQPv.mjs → _tanstack-start-manifest_v-noQw0Vmw.mjs} +1 -1
- package/.output/server/index.mjs +53 -53
- package/package.json +1 -1
- package/src/components/proxy-viewer/TurnGroup.tsx +10 -13
- package/src/proxy/handler.ts +52 -84
- package/src/proxy/logFinalizer.ts +301 -0
- package/src/proxy/logFinalizer.worker.ts +24 -0
- package/src/proxy/schemas.ts +8 -3
- package/src/proxy/sessionProcess.ts +133 -0
- package/src/proxy/sessionRuntime.ts +85 -0
- package/src/proxy/sessionSupervisor.ts +282 -0
- package/src/proxy/sessionWorkerEntry.ts +26 -0
- package/src/proxy/store.ts +64 -20
- package/src/routes/api/logs.stream.ts +2 -2
- 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
|
+
});
|
package/src/proxy/store.ts
CHANGED
|
@@ -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(
|
|
40
|
-
memoryCache.delete(
|
|
58
|
+
if (memoryCache.has(cachedLog.id)) {
|
|
59
|
+
memoryCache.delete(cachedLog.id);
|
|
41
60
|
}
|
|
42
|
-
memoryCache.set(
|
|
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)
|
|
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
|
-
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
|
|
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: () =>
|
|
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
|
});
|