@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.
Files changed (33) hide show
  1. package/.output/nitro.json +1 -1
  2. package/.output/public/assets/{CompareDrawer-BhXCLr7m.js → CompareDrawer-BpwZCB6M.js} +1 -1
  3. package/.output/public/assets/{ReplayDialog-CzRPSXwa.js → ReplayDialog-Clratkzl.js} +1 -1
  4. package/.output/public/assets/{RequestAnatomy-lMQonao2.js → RequestAnatomy-EtiX0r_G.js} +1 -1
  5. package/.output/public/assets/{ResponseView-Bt0vngo0.js → ResponseView-CJqxo-EN.js} +1 -1
  6. package/.output/public/assets/{StreamingChunkSequence-Dq9XY2E9.js → StreamingChunkSequence-BIbRqQiV.js} +1 -1
  7. package/.output/public/assets/{index-B4nxi_tZ.js → index-B-0F9n1w.js} +8 -8
  8. package/.output/public/assets/{json-viewer-C8ttTXtv.js → json-viewer-D-z1r1Pp.js} +1 -1
  9. package/.output/public/assets/{main-Dgme52Fp.js → main-CZJ63sQh.js} +1 -1
  10. package/.output/server/_ssr/{CompareDrawer-D-Nj8wmx.mjs → CompareDrawer-BJr-913n.mjs} +4 -3
  11. package/.output/server/_ssr/{ReplayDialog-DcucC22E.mjs → ReplayDialog-BwmToGuR.mjs} +5 -4
  12. package/.output/server/_ssr/{RequestAnatomy-aL8GAcW2.mjs → RequestAnatomy-BmMiPRPB.mjs} +3 -2
  13. package/.output/server/_ssr/{ResponseView-BHgpoGaF.mjs → ResponseView-ZB9-8Raw.mjs} +4 -3
  14. package/.output/server/_ssr/{StreamingChunkSequence-DrT7StyS.mjs → StreamingChunkSequence-DWm4CQWC.mjs} +4 -3
  15. package/.output/server/_ssr/{index-nUG0H1oS.mjs → index-C7I_Qgt0.mjs} +18 -20
  16. package/.output/server/_ssr/index.mjs +2 -2
  17. package/.output/server/_ssr/{json-viewer-DLsDT0RE.mjs → json-viewer-D9XETzwp.mjs} +3 -2
  18. package/.output/server/_ssr/{router-DG_jmXCF.mjs → router-711KpGkz.mjs} +647 -98
  19. package/.output/server/{_tanstack-start-manifest_v-D0JtrQPv.mjs → _tanstack-start-manifest_v-noQw0Vmw.mjs} +1 -1
  20. package/.output/server/index.mjs +53 -53
  21. package/package.json +1 -1
  22. package/src/components/proxy-viewer/TurnGroup.tsx +10 -13
  23. package/src/proxy/handler.ts +52 -84
  24. package/src/proxy/logFinalizer.ts +301 -0
  25. package/src/proxy/logFinalizer.worker.ts +24 -0
  26. package/src/proxy/schemas.ts +8 -3
  27. package/src/proxy/sessionProcess.ts +133 -0
  28. package/src/proxy/sessionRuntime.ts +85 -0
  29. package/src/proxy/sessionSupervisor.ts +282 -0
  30. package/src/proxy/sessionWorkerEntry.ts +26 -0
  31. package/src/proxy/store.ts +64 -20
  32. package/src/routes/api/logs.stream.ts +2 -2
  33. package/src/routes/api/sessions.ts +9 -2
@@ -1,14 +1,15 @@
1
1
  import { c as createRouter, a as createRootRoute, b as createFileRoute, l as lazyRouteComponent, O as Outlet, H as HeadContent, S as Scripts } from "../_libs/tanstack__react-router.mjs";
2
2
  import { j as jsxRuntimeExports } from "../_libs/react.mjs";
3
3
  import { S as SWRConfig } from "../_libs/swr.mjs";
4
- import { existsSync, mkdirSync, writeFileSync, renameSync, copyFileSync, unlinkSync, readFileSync } from "node:fs";
4
+ import { existsSync, readFileSync, mkdirSync, writeFileSync, renameSync, copyFileSync, unlinkSync } from "node:fs";
5
5
  import { mkdir, appendFile, readFile, open, readdir, stat, unlink, writeFile } from "node:fs/promises";
6
6
  import { Buffer } from "node:buffer";
7
7
  import path, { join, isAbsolute, dirname } from "node:path";
8
8
  import { C as Conf } from "../_libs/conf.mjs";
9
9
  import { randomUUID } from "crypto";
10
- import { exec } from "node:child_process";
10
+ import { exec, fork } from "node:child_process";
11
11
  import { promisify } from "node:util";
12
+ import { Worker } from "node:worker_threads";
12
13
  import { M as McpServer, W as WebStandardStreamableHTTPServerTransport } from "../_libs/modelcontextprotocol__server.mjs";
13
14
  import { homedir } from "node:os";
14
15
  import { d as object, _ as _enum, b as string, a as array, u as union, n as number, c as boolean, r as record, g as discriminatedUnion, l as literal, h as _null, k as lazy, e as unknown } from "../_libs/zod.mjs";
@@ -70,7 +71,7 @@ function RootDocument({ children }) {
70
71
  ] })
71
72
  ] });
72
73
  }
73
- const $$splitComponentImporter = () => import("./index-nUG0H1oS.mjs").then((n) => n.t);
74
+ const $$splitComponentImporter = () => import("./index-C7I_Qgt0.mjs").then((n) => n.t);
74
75
  const Route$j = createFileRoute("/")({
75
76
  component: lazyRouteComponent($$splitComponentImporter, "component")
76
77
  });
@@ -750,20 +751,24 @@ const LooseRequestSchema = object({
750
751
  model: string().optional(),
751
752
  metadata: object({ user_id: string().optional() }).passthrough().optional()
752
753
  });
754
+ function extractSessionIdFromHeaders(headers) {
755
+ return headers.get("x-llm-inspector-session-id") ?? headers.get("x-session-affinity");
756
+ }
753
757
  function extractRequestMetadata(body, headers) {
754
- if (body === null) return { model: null, sessionId: null };
758
+ const headerSessionId = extractSessionIdFromHeaders(headers);
759
+ if (body === null) return { model: null, sessionId: headerSessionId };
755
760
  try {
756
761
  const json = JSON.parse(body);
757
762
  const loose = LooseRequestSchema.safeParse(json);
758
763
  if (loose.success) {
759
764
  return {
760
765
  model: loose.data.model ?? null,
761
- sessionId: loose.data.metadata?.user_id ?? headers.get("x-session-affinity") ?? null
766
+ sessionId: loose.data.metadata?.user_id ?? headerSessionId
762
767
  };
763
768
  }
764
769
  } catch {
765
770
  }
766
- return { model: null, sessionId: null };
771
+ return { model: null, sessionId: headerSessionId };
767
772
  }
768
773
  const StreamingChunkSchema = object({
769
774
  index: number(),
@@ -833,6 +838,199 @@ function readChunks(path2) {
833
838
  return null;
834
839
  }
835
840
  }
841
+ const PROVIDER_TEST_SESSION_ID = "provider-test";
842
+ function getRuntimeMode() {
843
+ if (process.env["FINALIZER_USE_WORKER"] === "0") return "in-process";
844
+ const mode = process.env["FINALIZER_RUNTIME"];
845
+ if (mode === "worker") return "worker-thread";
846
+ if (mode === "inline") return "in-process";
847
+ return "child-process";
848
+ }
849
+ const sessions = /* @__PURE__ */ new Map();
850
+ const knownLogSessions = /* @__PURE__ */ new Map();
851
+ const completedLogIds = /* @__PURE__ */ new Set();
852
+ const failedLogIds = /* @__PURE__ */ new Set();
853
+ function normalizeExplicitSessionId(sessionId) {
854
+ if (sessionId === null || sessionId === void 0) return null;
855
+ const trimmed = sessionId.trim();
856
+ return trimmed.length > 0 ? trimmed : null;
857
+ }
858
+ function buildClientSessionId(clientInfo) {
859
+ const projectFolder = normalizeExplicitSessionId(clientInfo.projectFolder);
860
+ if (clientInfo.pid !== null) {
861
+ return {
862
+ id: projectFolder !== null ? `process:${clientInfo.pid}:${projectFolder}` : `process:${clientInfo.pid}`,
863
+ source: "client-process"
864
+ };
865
+ }
866
+ if (clientInfo.port !== null) {
867
+ return {
868
+ id: `connection:${clientInfo.port}`,
869
+ source: "client-connection"
870
+ };
871
+ }
872
+ return { id: null, source: null };
873
+ }
874
+ function resolveSessionIdentity(input) {
875
+ if (input.isTest === true) {
876
+ return { id: PROVIDER_TEST_SESSION_ID, source: "provider-test" };
877
+ }
878
+ const explicit = normalizeExplicitSessionId(input.explicitSessionId);
879
+ if (explicit !== null) {
880
+ return { id: explicit, source: "explicit" };
881
+ }
882
+ if (input.clientInfo !== void 0) {
883
+ return buildClientSessionId(input.clientInfo);
884
+ }
885
+ return { id: null, source: null };
886
+ }
887
+ function getLogSessionIdentity(log) {
888
+ return resolveSessionIdentity({
889
+ explicitSessionId: log.sessionId,
890
+ clientInfo: {
891
+ port: log.clientPort ?? null,
892
+ pid: log.clientPid ?? null,
893
+ cwd: log.clientCwd ?? null,
894
+ projectFolder: log.clientProjectFolder ?? null
895
+ },
896
+ isTest: log.isTest
897
+ });
898
+ }
899
+ function getLogSessionId(log) {
900
+ return getLogSessionIdentity(log).id;
901
+ }
902
+ function getTimestamp(log) {
903
+ return log.timestamp.length > 0 ? log.timestamp : (/* @__PURE__ */ new Date()).toISOString();
904
+ }
905
+ function createSessionRecord(id, source, log) {
906
+ const timestamp = getTimestamp(log);
907
+ return {
908
+ id,
909
+ source,
910
+ runtimeMode: getRuntimeMode(),
911
+ createdAt: timestamp,
912
+ updatedAt: timestamp,
913
+ requestCount: 0,
914
+ activeRequests: 0,
915
+ completedRequests: 0,
916
+ failedRequests: 0,
917
+ queuedTasks: 0,
918
+ runningTasks: 0,
919
+ lastTaskError: null,
920
+ lastLogId: null,
921
+ lastModel: null,
922
+ clientPid: log.clientPid ?? null,
923
+ clientProjectFolder: log.clientProjectFolder ?? null
924
+ };
925
+ }
926
+ function upsertSession(log, sourceOverride) {
927
+ const identity = getLogSessionIdentity(log);
928
+ if (identity.id === null) return null;
929
+ const source = sourceOverride ?? identity.source;
930
+ if (source === null) return null;
931
+ const existing = sessions.get(identity.id);
932
+ const session = existing ?? createSessionRecord(identity.id, source, log);
933
+ session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
934
+ session.lastLogId = log.id;
935
+ session.lastModel = log.model ?? session.lastModel;
936
+ session.clientPid = log.clientPid ?? session.clientPid;
937
+ session.clientProjectFolder = log.clientProjectFolder ?? session.clientProjectFolder;
938
+ sessions.set(identity.id, session);
939
+ return session;
940
+ }
941
+ function rememberLogSession(log) {
942
+ const sessionId = getLogSessionId(log);
943
+ if (sessionId === null) return false;
944
+ const knownSessionId = knownLogSessions.get(log.id);
945
+ if (knownSessionId === sessionId) return false;
946
+ knownLogSessions.set(log.id, sessionId);
947
+ return true;
948
+ }
949
+ function isFailedLog(log) {
950
+ const hasError = log.error !== null && log.error !== void 0 && log.error.length > 0;
951
+ const hasErrorStatus = log.responseStatus !== null && log.responseStatus >= 400;
952
+ return hasError || hasErrorStatus;
953
+ }
954
+ function markSessionStarted(log, sourceOverride) {
955
+ const session = upsertSession(log, sourceOverride);
956
+ if (session === null) return;
957
+ const isNewLog = rememberLogSession(log);
958
+ if (isNewLog) {
959
+ session.requestCount += 1;
960
+ }
961
+ session.activeRequests += 1;
962
+ }
963
+ function markSessionFinished(log, sourceOverride) {
964
+ const session = upsertSession(log, sourceOverride);
965
+ if (session === null) return;
966
+ if (session.activeRequests > 0) {
967
+ session.activeRequests -= 1;
968
+ }
969
+ if (completedLogIds.has(log.id) === false) {
970
+ completedLogIds.add(log.id);
971
+ session.completedRequests += 1;
972
+ }
973
+ if (isFailedLog(log) && failedLogIds.has(log.id) === false) {
974
+ failedLogIds.add(log.id);
975
+ session.failedRequests += 1;
976
+ }
977
+ }
978
+ function markSessionTaskQueued(sessionId) {
979
+ if (sessionId === null) return;
980
+ const session = sessions.get(sessionId);
981
+ if (session === void 0) return;
982
+ session.queuedTasks += 1;
983
+ session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
984
+ }
985
+ function markSessionTaskStarted(sessionId) {
986
+ if (sessionId === null) return;
987
+ const session = sessions.get(sessionId);
988
+ if (session === void 0) return;
989
+ if (session.queuedTasks > 0) {
990
+ session.queuedTasks -= 1;
991
+ }
992
+ session.runningTasks += 1;
993
+ session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
994
+ }
995
+ function markSessionTaskFinished(sessionId, error) {
996
+ if (sessionId === null) return;
997
+ const session = sessions.get(sessionId);
998
+ if (session === void 0) return;
999
+ if (session.runningTasks > 0) {
1000
+ session.runningTasks -= 1;
1001
+ }
1002
+ session.lastTaskError = error;
1003
+ session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1004
+ }
1005
+ function observeSessionLog(log, sourceOverride) {
1006
+ const session = upsertSession(log, sourceOverride);
1007
+ if (session === null) return;
1008
+ const isNewLog = rememberLogSession(log);
1009
+ if (isNewLog) {
1010
+ session.requestCount += 1;
1011
+ }
1012
+ if (log.responseStatus !== null || log.responseText !== null) {
1013
+ markSessionFinished(log, sourceOverride);
1014
+ }
1015
+ }
1016
+ function clearSessionRegistry() {
1017
+ sessions.clear();
1018
+ knownLogSessions.clear();
1019
+ completedLogIds.clear();
1020
+ failedLogIds.clear();
1021
+ }
1022
+ function rebuildSessionRegistry(logs) {
1023
+ clearSessionRegistry();
1024
+ for (const log of logs) {
1025
+ observeSessionLog(log);
1026
+ }
1027
+ }
1028
+ function getSessionIds() {
1029
+ return [...sessions.values()].map((session) => session.id);
1030
+ }
1031
+ function getSessionSnapshots() {
1032
+ return [...sessions.values()].map((session) => ({ ...session }));
1033
+ }
836
1034
  const MAX_MEMORY_CACHE = 100;
837
1035
  const memoryCache = /* @__PURE__ */ new Map();
838
1036
  function evictOldestIfNeeded() {
@@ -842,11 +1040,17 @@ function evictOldestIfNeeded() {
842
1040
  memoryCache.delete(oldestKey);
843
1041
  }
844
1042
  }
1043
+ function normalizeLogSession(log) {
1044
+ const sessionId = getLogSessionId(log);
1045
+ if (sessionId === log.sessionId) return log;
1046
+ return { ...log, sessionId };
1047
+ }
845
1048
  function addToCache(log) {
846
- if (memoryCache.has(log.id)) {
847
- memoryCache.delete(log.id);
1049
+ const cachedLog = normalizeLogSession(log);
1050
+ if (memoryCache.has(cachedLog.id)) {
1051
+ memoryCache.delete(cachedLog.id);
848
1052
  }
849
- memoryCache.set(log.id, log);
1053
+ memoryCache.set(cachedLog.id, cachedLog);
850
1054
  evictOldestIfNeeded();
851
1055
  }
852
1056
  function removeFromCache(id) {
@@ -867,12 +1071,18 @@ async function addTestLogEntry(entry) {
867
1071
  entry.streamingChunks.truncated
868
1072
  );
869
1073
  }
1074
+ const session = resolveSessionIdentity({
1075
+ explicitSessionId: entry.sessionId,
1076
+ isTest: entry.isTest
1077
+ });
870
1078
  const log = {
871
1079
  id,
872
1080
  ...entry,
1081
+ sessionId: session.id,
873
1082
  streamingChunksPath
874
1083
  };
875
1084
  addToCache(log);
1085
+ observeSessionLog(log, session.source);
876
1086
  emitLogUpdate(log);
877
1087
  return log;
878
1088
  }
@@ -880,13 +1090,17 @@ async function createLog(method, path2, requestBody, headers, clientInfo, rawHea
880
1090
  const userAgent = headers.get("user-agent");
881
1091
  const origin = headers.get("origin");
882
1092
  const id = preAcquiredId ?? await getNextLogId();
1093
+ const session = resolveSessionIdentity({
1094
+ explicitSessionId: sessionId,
1095
+ clientInfo
1096
+ });
883
1097
  const log = {
884
1098
  id,
885
1099
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
886
1100
  method,
887
1101
  path: path2,
888
1102
  model,
889
- sessionId,
1103
+ sessionId: session.id,
890
1104
  rawRequestBody: requestBody,
891
1105
  responseStatus: null,
892
1106
  responseText: null,
@@ -913,9 +1127,15 @@ async function createLog(method, path2, requestBody, headers, clientInfo, rawHea
913
1127
  appendLogEntry(log);
914
1128
  await addToIndex(id, logFile, -1, -1);
915
1129
  addToCache(log);
1130
+ markSessionStarted(log, session.source);
916
1131
  emitLogUpdate(log);
917
1132
  return log;
918
1133
  }
1134
+ function finalizeLogUpdate(log) {
1135
+ addToCache(log);
1136
+ markSessionFinished(log);
1137
+ emitLogUpdate(log);
1138
+ }
919
1139
  async function getLogById(id) {
920
1140
  const cached = memoryCache.get(id);
921
1141
  if (cached !== void 0) {
@@ -940,7 +1160,12 @@ async function getLogById(id) {
940
1160
  if (line === "") return null;
941
1161
  const parsed = JSON.parse(line);
942
1162
  const result = CapturedLogSchema.safeParse(parsed);
943
- if (result.success) return result.data;
1163
+ if (result.success) {
1164
+ const log = normalizeLogSession(result.data);
1165
+ addToCache(log);
1166
+ observeSessionLog(log);
1167
+ return log;
1168
+ }
944
1169
  } finally {
945
1170
  await fh.close();
946
1171
  }
@@ -958,9 +1183,11 @@ async function getLogById(id) {
958
1183
  if (desc !== void 0 && typeof desc.value === "number" && desc.value === id) {
959
1184
  const result = CapturedLogSchema.safeParse(parsed);
960
1185
  if (result.success) {
961
- lastMatch = result.data;
1186
+ lastMatch = normalizeLogSession(result.data);
962
1187
  if (result.data.responseStatus !== null) {
963
- return result.data;
1188
+ addToCache(lastMatch);
1189
+ observeSessionLog(lastMatch);
1190
+ return lastMatch;
964
1191
  }
965
1192
  }
966
1193
  }
@@ -968,7 +1195,11 @@ async function getLogById(id) {
968
1195
  } catch {
969
1196
  }
970
1197
  }
971
- if (lastMatch !== null) return lastMatch;
1198
+ if (lastMatch !== null) {
1199
+ addToCache(lastMatch);
1200
+ observeSessionLog(lastMatch);
1201
+ return lastMatch;
1202
+ }
972
1203
  } catch (err) {
973
1204
  logger.error("[store] Failed to read log from disk:", String(err));
974
1205
  }
@@ -976,17 +1207,13 @@ async function getLogById(id) {
976
1207
  }
977
1208
  function getFilteredLogs(sessionId, model) {
978
1209
  return [...memoryCache.values()].filter((l) => {
979
- if (sessionId !== void 0 && l.sessionId !== sessionId) return false;
1210
+ if (sessionId !== void 0 && getLogSessionId(l) !== sessionId) return false;
980
1211
  if (model !== void 0 && l.model !== model) return false;
981
1212
  return true;
982
1213
  });
983
1214
  }
984
1215
  function getSessions() {
985
- const set = /* @__PURE__ */ new Set();
986
- for (const l of memoryCache.values()) {
987
- if (l.sessionId !== null && l.sessionId !== "") set.add(l.sessionId);
988
- }
989
- return [...set];
1216
+ return getSessionIds();
990
1217
  }
991
1218
  function getModels() {
992
1219
  const set = /* @__PURE__ */ new Set();
@@ -998,6 +1225,7 @@ function getModels() {
998
1225
  function clearAllLogs() {
999
1226
  const count = memoryCache.size;
1000
1227
  memoryCache.clear();
1228
+ clearSessionRegistry();
1001
1229
  return { cleared: count };
1002
1230
  }
1003
1231
  function clearLogsByIds(ids) {
@@ -1008,6 +1236,7 @@ function clearLogsByIds(ids) {
1008
1236
  for (const id of unique) {
1009
1237
  removeFromCache(id);
1010
1238
  }
1239
+ rebuildSessionRegistry(memoryCache.values());
1011
1240
  return { cleared: unique.size };
1012
1241
  }
1013
1242
  const sseHandlers = /* @__PURE__ */ new Set();
@@ -2368,6 +2597,350 @@ function stripOpenAIOrphanToolMessages(rawBody) {
2368
2597
  parsed.messages = kept;
2369
2598
  return { body: JSON.stringify(parsed), removed: indices.length, orphanIds };
2370
2599
  }
2600
+ const IDLE_TIMEOUT_MS = Number(process.env["SESSION_PROCESS_IDLE_MS"]) || 5 * 60 * 1e3;
2601
+ const MAX_RESTARTS = 3;
2602
+ const _processes = /* @__PURE__ */ new Map();
2603
+ class SessionProcess {
2604
+ constructor(sessionId) {
2605
+ this.sessionId = sessionId;
2606
+ }
2607
+ child = null;
2608
+ pending = /* @__PURE__ */ new Map();
2609
+ nextId = 0;
2610
+ idleTimer = null;
2611
+ restartCount = 0;
2612
+ destroyed = false;
2613
+ /** Number of outstanding jobs sent to this process. */
2614
+ get pendingCount() {
2615
+ return this.pending.size;
2616
+ }
2617
+ ensureRunning() {
2618
+ if (this.child !== null && this.child.connected) return this.child;
2619
+ const entryPath = new URL("./sessionWorkerEntry.ts", import.meta.url).pathname;
2620
+ const resolvedPath = process.platform === "win32" && entryPath.startsWith("/") ? entryPath.slice(1) : entryPath;
2621
+ this.child = fork(resolvedPath, [], {
2622
+ stdio: ["pipe", "pipe", "pipe", "ipc"]
2623
+ });
2624
+ this.restartCount += 1;
2625
+ this.child.on("message", (msg) => {
2626
+ const pending = this.pending.get(msg.id);
2627
+ if (pending !== void 0) {
2628
+ this.pending.delete(msg.id);
2629
+ pending.resolve(msg.result);
2630
+ }
2631
+ this.resetIdleTimer();
2632
+ });
2633
+ this.child.on("error", (err) => {
2634
+ logger.error(`[sessionProcess] Session ${this.sessionId} process error:`, err.message);
2635
+ });
2636
+ this.child.on("exit", (code, signal) => {
2637
+ const wasConnected = this.child !== null;
2638
+ this.child = null;
2639
+ if (this.destroyed) return;
2640
+ for (const [, pending] of this.pending) {
2641
+ pending.reject(
2642
+ new Error(
2643
+ `Session process exited with code ${code ?? signal}, session=${this.sessionId}`
2644
+ )
2645
+ );
2646
+ }
2647
+ this.pending.clear();
2648
+ if (wasConnected && this.restartCount <= MAX_RESTARTS) {
2649
+ logger.warn(
2650
+ `[sessionProcess] Session ${this.sessionId} worker exited (code=${code ?? signal}), restart ${this.restartCount}/${MAX_RESTARTS}`
2651
+ );
2652
+ }
2653
+ });
2654
+ this.resetIdleTimer();
2655
+ return this.child;
2656
+ }
2657
+ enqueue(job) {
2658
+ return new Promise((resolve, reject) => {
2659
+ const child = this.ensureRunning();
2660
+ const id = String(++this.nextId);
2661
+ this.pending.set(id, { resolve, reject });
2662
+ child.send({ id, job });
2663
+ });
2664
+ }
2665
+ resetIdleTimer() {
2666
+ if (this.idleTimer !== null) clearTimeout(this.idleTimer);
2667
+ this.idleTimer = setTimeout(() => {
2668
+ if (this.pending.size === 0) {
2669
+ this.destroy();
2670
+ }
2671
+ }, IDLE_TIMEOUT_MS);
2672
+ }
2673
+ destroy() {
2674
+ this.destroyed = true;
2675
+ if (this.idleTimer !== null) {
2676
+ clearTimeout(this.idleTimer);
2677
+ this.idleTimer = null;
2678
+ }
2679
+ if (this.child !== null) {
2680
+ this.child.kill();
2681
+ this.child = null;
2682
+ }
2683
+ _processes.delete(this.sessionId);
2684
+ }
2685
+ }
2686
+ function getSessionProcess(sessionId) {
2687
+ const existing = _processes.get(sessionId);
2688
+ if (existing !== void 0) return existing;
2689
+ const sp = new SessionProcess(sessionId);
2690
+ _processes.set(sessionId, sp);
2691
+ return sp;
2692
+ }
2693
+ function buildFileLogEntry(log, upstreamUrl) {
2694
+ return {
2695
+ timestamp: log.timestamp,
2696
+ id: log.id,
2697
+ method: log.method,
2698
+ path: log.path,
2699
+ model: log.model,
2700
+ sessionId: log.sessionId,
2701
+ rawRequestBody: log.rawRequestBody,
2702
+ responseStatus: log.responseStatus,
2703
+ responseText: log.responseText,
2704
+ inputTokens: log.inputTokens,
2705
+ outputTokens: log.outputTokens,
2706
+ cacheCreationInputTokens: log.cacheCreationInputTokens,
2707
+ cacheReadInputTokens: log.cacheReadInputTokens,
2708
+ elapsedMs: log.elapsedMs,
2709
+ streaming: log.streaming,
2710
+ userAgent: log.userAgent,
2711
+ origin: log.origin,
2712
+ upstreamUrl,
2713
+ clientPort: log.clientPort,
2714
+ clientPid: log.clientPid,
2715
+ clientCwd: log.clientCwd,
2716
+ clientProjectFolder: log.clientProjectFolder,
2717
+ streamingChunks: log.streamingChunks,
2718
+ streamingChunksPath: log.streamingChunksPath,
2719
+ error: log.error
2720
+ };
2721
+ }
2722
+ function cloneLog(log) {
2723
+ return { ...log };
2724
+ }
2725
+ function errorMessage$2(err) {
2726
+ return err instanceof Error ? err.message : String(err);
2727
+ }
2728
+ function persistStreamingChunks(log) {
2729
+ if (log.streamingChunks !== void 0 && log.streamingChunks.chunks.length > 0) {
2730
+ const chunkPath = writeChunks(
2731
+ log.id,
2732
+ log.streamingChunks.chunks,
2733
+ log.streamingChunks.truncated
2734
+ );
2735
+ log.streamingChunksPath = chunkPath;
2736
+ }
2737
+ }
2738
+ function finalizeWithError(job, log, fallbackStatus, fallbackResponseText, err) {
2739
+ const message = errorMessage$2(err);
2740
+ logger.error(`[logFinalizer] Failed to finalize log #${log.id}:`, message);
2741
+ log.responseStatus = log.responseStatus ?? fallbackStatus;
2742
+ log.responseText = log.responseText ?? fallbackResponseText;
2743
+ log.elapsedMs = job.elapsedMs;
2744
+ log.error = message;
2745
+ return { log, upstreamUrl: job.upstreamUrl, error: message };
2746
+ }
2747
+ function finalizeNonStreaming(job, log) {
2748
+ const formatHandler = formatForPath(log.path);
2749
+ if (formatHandler === null) {
2750
+ return finalizeWithError(job, log, job.responseStatus, job.responseBody, "Unsupported format");
2751
+ }
2752
+ try {
2753
+ const tokens = formatHandler.extractTokens(job.responseBody);
2754
+ log.elapsedMs = job.elapsedMs;
2755
+ log.responseStatus = job.responseStatus;
2756
+ log.responseText = job.responseBody;
2757
+ log.inputTokens = tokens.inputTokens;
2758
+ log.outputTokens = tokens.outputTokens;
2759
+ log.cacheCreationInputTokens = tokens.cacheCreationInputTokens;
2760
+ log.cacheReadInputTokens = tokens.cacheReadInputTokens;
2761
+ return { log, upstreamUrl: job.upstreamUrl, error: null };
2762
+ } catch (err) {
2763
+ return finalizeWithError(job, log, job.responseStatus, job.responseBody, err);
2764
+ }
2765
+ }
2766
+ function finalizeStreaming(job, log) {
2767
+ const formatHandler = formatForPath(log.path);
2768
+ if (formatHandler === null) {
2769
+ return finalizeWithError(job, log, job.responseStatus, job.rawStream, "Unsupported format");
2770
+ }
2771
+ try {
2772
+ log.elapsedMs = job.elapsedMs;
2773
+ log.responseStatus = job.responseStatus;
2774
+ log.responseText = formatHandler.extractStream(
2775
+ job.rawStream,
2776
+ log,
2777
+ log.model ?? void 0,
2778
+ true
2779
+ );
2780
+ persistStreamingChunks(log);
2781
+ return { log, upstreamUrl: job.upstreamUrl, error: null };
2782
+ } catch (err) {
2783
+ return finalizeWithError(job, log, job.responseStatus, job.rawStream, err);
2784
+ }
2785
+ }
2786
+ function finalizeStreamAbort(job, log) {
2787
+ const formatHandler = formatForPath(log.path);
2788
+ if (formatHandler === null && job.hasChunks) {
2789
+ return finalizeWithError(job, log, 499, "Client aborted", "Unsupported format");
2790
+ }
2791
+ try {
2792
+ log.elapsedMs = job.elapsedMs;
2793
+ if (job.hasChunks && formatHandler !== null) {
2794
+ log.responseText = formatHandler.extractStream(
2795
+ job.rawStream,
2796
+ log,
2797
+ log.model ?? void 0,
2798
+ true
2799
+ );
2800
+ persistStreamingChunks(log);
2801
+ } else {
2802
+ log.responseText = "Client aborted";
2803
+ }
2804
+ return { log, upstreamUrl: job.upstreamUrl, error: "Client aborted" };
2805
+ } catch (err) {
2806
+ return finalizeWithError(job, log, 499, "Client aborted", err);
2807
+ }
2808
+ }
2809
+ function buildFinalizeLogResult(job) {
2810
+ const log = cloneLog(job.log);
2811
+ switch (job.type) {
2812
+ case "non-streaming":
2813
+ return finalizeNonStreaming(job, log);
2814
+ case "streaming":
2815
+ return finalizeStreaming(job, log);
2816
+ case "stream-abort":
2817
+ return finalizeStreamAbort(job, log);
2818
+ }
2819
+ }
2820
+ function commitFinalizeLogResult(result) {
2821
+ appendLogEntry({ ...buildFileLogEntry(result.log, result.upstreamUrl), error: result.error });
2822
+ finalizeLogUpdate(result.log);
2823
+ }
2824
+ const RUNTIME = (() => {
2825
+ if (process.env["FINALIZER_USE_WORKER"] === "0") return "inline";
2826
+ const mode = process.env["FINALIZER_RUNTIME"];
2827
+ if (mode === "worker") return "worker";
2828
+ if (mode === "inline") return "inline";
2829
+ return "process";
2830
+ })();
2831
+ function executeBuildInSessionProcess(job) {
2832
+ const sessionId = job.log.sessionId ?? "__unassigned__";
2833
+ return getSessionProcess(sessionId).enqueue(job).catch((err) => {
2834
+ const message = err instanceof Error ? err.message : String(err);
2835
+ logger.error(
2836
+ `[logFinalizer] Session process failed for log #${job.log.id}, falling back to in-process: ${message}`
2837
+ );
2838
+ return buildFinalizeLogResult(job);
2839
+ });
2840
+ }
2841
+ function resolveBuildPromise(job) {
2842
+ switch (RUNTIME) {
2843
+ case "process":
2844
+ return executeBuildInSessionProcess(job);
2845
+ case "worker":
2846
+ return executeBuildInWorker(job);
2847
+ case "inline":
2848
+ return Promise.resolve(buildFinalizeLogResult(job));
2849
+ }
2850
+ }
2851
+ const WORKER_COUNT = Math.max(1, Number(process.env["FINALIZER_WORKER_COUNT"]) || 4);
2852
+ let _workers = null;
2853
+ const _pending = /* @__PURE__ */ new Map();
2854
+ let _nextWorker = 0;
2855
+ let _jobSeq = 0;
2856
+ function getWorkers() {
2857
+ if (_workers !== null) return _workers;
2858
+ _workers = [];
2859
+ for (let i = 0; i < WORKER_COUNT; i++) {
2860
+ const w = new Worker(new URL("./logFinalizer.worker.ts", import.meta.url));
2861
+ w.on("message", (msg) => {
2862
+ const resolve = _pending.get(msg.id);
2863
+ if (resolve !== void 0) {
2864
+ _pending.delete(msg.id);
2865
+ resolve(msg.result);
2866
+ }
2867
+ });
2868
+ w.on("error", (err) => {
2869
+ logger.error(
2870
+ "[logFinalizer] Worker error:",
2871
+ err instanceof Error ? err.message : String(err)
2872
+ );
2873
+ });
2874
+ _workers.push(w);
2875
+ }
2876
+ return _workers;
2877
+ }
2878
+ function executeBuildInWorker(job) {
2879
+ return new Promise((resolve) => {
2880
+ const list = getWorkers();
2881
+ const idx = _nextWorker % list.length;
2882
+ _nextWorker++;
2883
+ const w = list[idx];
2884
+ if (w === void 0) return;
2885
+ const id = String(++_jobSeq);
2886
+ _pending.set(id, resolve);
2887
+ w.postMessage({ id, job });
2888
+ });
2889
+ }
2890
+ function executeFinalizeLogJob(job) {
2891
+ return resolveBuildPromise(job).then((result) => {
2892
+ commitFinalizeLogResult(result);
2893
+ if (result.error !== null && result.error !== "Client aborted") {
2894
+ return Promise.reject(new Error(result.error));
2895
+ }
2896
+ return void 0;
2897
+ });
2898
+ }
2899
+ const UNASSIGNED_SESSION_QUEUE = "__unassigned__";
2900
+ const queues = /* @__PURE__ */ new Map();
2901
+ function queueKeyForSession(sessionId) {
2902
+ return sessionId ?? UNASSIGNED_SESSION_QUEUE;
2903
+ }
2904
+ function errorMessage$1(err) {
2905
+ return err instanceof Error ? err.message : String(err);
2906
+ }
2907
+ async function runSessionTask(sessionId, taskName, task) {
2908
+ markSessionTaskStarted(sessionId);
2909
+ try {
2910
+ const result = await task();
2911
+ markSessionTaskFinished(sessionId, null);
2912
+ return result;
2913
+ } catch (err) {
2914
+ const message = errorMessage$1(err);
2915
+ logger.error(`[sessionRuntime] ${taskName} failed for session ${sessionId ?? "unassigned"}`);
2916
+ markSessionTaskFinished(sessionId, message);
2917
+ return Promise.reject(err);
2918
+ }
2919
+ }
2920
+ function enqueueSessionTask(sessionId, taskName, task) {
2921
+ const queueKey = queueKeyForSession(sessionId);
2922
+ const previous = queues.get(queueKey) ?? Promise.resolve();
2923
+ markSessionTaskQueued(sessionId);
2924
+ const current = previous.catch(() => void 0).then(() => runSessionTask(sessionId, taskName, task));
2925
+ const settled = current.then(
2926
+ () => void 0,
2927
+ () => void 0
2928
+ );
2929
+ queues.set(queueKey, settled);
2930
+ void settled.then(() => {
2931
+ if (queues.get(queueKey) === settled) {
2932
+ queues.delete(queueKey);
2933
+ }
2934
+ });
2935
+ return current;
2936
+ }
2937
+ function enqueueFinalizeLogJob(job) {
2938
+ return enqueueSessionTask(
2939
+ getLogSessionId(job.log),
2940
+ "finalize-log",
2941
+ () => executeFinalizeLogJob(job)
2942
+ );
2943
+ }
2371
2944
  function describeApiRoute(apiPath) {
2372
2945
  const endpointPath = apiPath.split("?")[0] ?? "";
2373
2946
  const isChatCompletions = endpointPath === PATH_CHAT_COMPLETIONS || endpointPath === PATH_V1_CHAT_COMPLETIONS;
@@ -2430,33 +3003,6 @@ function buildProxyHeaders(originalHeaders) {
2430
3003
  }
2431
3004
  return { headers, rawHeaders };
2432
3005
  }
2433
- function buildFileLogEntry(log, upstreamUrl) {
2434
- return {
2435
- timestamp: log.timestamp,
2436
- id: log.id,
2437
- method: log.method,
2438
- path: log.path,
2439
- model: log.model,
2440
- sessionId: log.sessionId,
2441
- rawRequestBody: log.rawRequestBody,
2442
- responseStatus: log.responseStatus,
2443
- responseText: log.responseText,
2444
- inputTokens: log.inputTokens,
2445
- outputTokens: log.outputTokens,
2446
- elapsedMs: log.elapsedMs,
2447
- streaming: log.streaming,
2448
- userAgent: log.userAgent,
2449
- origin: log.origin,
2450
- upstreamUrl,
2451
- clientPort: log.clientPort,
2452
- clientPid: log.clientPid,
2453
- clientCwd: log.clientCwd,
2454
- clientProjectFolder: log.clientProjectFolder,
2455
- streamingChunks: log.streamingChunks,
2456
- streamingChunksPath: log.streamingChunksPath,
2457
- error: log.error
2458
- };
2459
- }
2460
3006
  function parseRequestPath(req, url) {
2461
3007
  const route = describeApiRoute(getProxyApiPath(url));
2462
3008
  const isPost = req.method === "POST";
@@ -2469,17 +3015,27 @@ function parseRequestPath(req, url) {
2469
3015
  normalizedPath: route.normalizedPath
2470
3016
  };
2471
3017
  }
2472
- function handleNonStreamingResponse(upstreamRes, responseBody, startTime, formatHandler, upstreamUrl, log) {
3018
+ function errorMessage(err) {
3019
+ return err instanceof Error ? err.message : String(err);
3020
+ }
3021
+ function scheduleLogFinalization(job) {
3022
+ void enqueueFinalizeLogJob(job).catch((err) => {
3023
+ logger.error(
3024
+ `[handler] Session finalization task failed for log #${job.log.id}:`,
3025
+ errorMessage(err)
3026
+ );
3027
+ });
3028
+ }
3029
+ function handleNonStreamingResponse(upstreamRes, responseBody, startTime, upstreamUrl, log) {
2473
3030
  const elapsedMs = Date.now() - startTime;
2474
- const tokens = formatHandler.extractTokens(responseBody);
2475
- log.elapsedMs = elapsedMs;
2476
- log.responseStatus = upstreamRes.status;
2477
- log.responseText = responseBody;
2478
- log.inputTokens = tokens.inputTokens;
2479
- log.outputTokens = tokens.outputTokens;
2480
- log.cacheCreationInputTokens = tokens.cacheCreationInputTokens;
2481
- log.cacheReadInputTokens = tokens.cacheReadInputTokens;
2482
- appendLogEntry({ ...buildFileLogEntry(log, upstreamUrl), error: null });
3031
+ scheduleLogFinalization({
3032
+ type: "non-streaming",
3033
+ log,
3034
+ upstreamUrl,
3035
+ elapsedMs,
3036
+ responseStatus: upstreamRes.status,
3037
+ responseBody
3038
+ });
2483
3039
  const responseHeaders = new Headers(upstreamRes.headers);
2484
3040
  responseHeaders.delete(HEADER_CONTENT_ENCODING);
2485
3041
  responseHeaders.delete(HEADER_CONTENT_LENGTH);
@@ -2488,7 +3044,7 @@ function handleNonStreamingResponse(upstreamRes, responseBody, startTime, format
2488
3044
  headers: responseHeaders
2489
3045
  });
2490
3046
  }
2491
- function handleStreamingResponse(upstreamRes, req, startTime, formatHandler, upstreamUrl, log) {
3047
+ function handleStreamingResponse(upstreamRes, req, startTime, upstreamUrl, log) {
2492
3048
  log.streaming = true;
2493
3049
  log.responseStatus = upstreamRes.status;
2494
3050
  const chunks = [];
@@ -2500,18 +3056,15 @@ function handleStreamingResponse(upstreamRes, req, startTime, formatHandler, ups
2500
3056
  },
2501
3057
  flush() {
2502
3058
  const full = chunks.join("");
2503
- log.elapsedMs = Date.now() - startTime;
2504
- log.responseText = formatHandler.extractStream(full, log, log.model ?? void 0, true);
2505
- if (log.streamingChunks && log.streamingChunks.chunks.length > 0) {
2506
- const chunkPath = writeChunks(
2507
- log.id,
2508
- log.streamingChunks.chunks,
2509
- log.streamingChunks.truncated
2510
- );
2511
- log.streamingChunksPath = chunkPath;
2512
- }
2513
- appendLogEntry(buildFileLogEntry(log, upstreamUrl));
2514
- emitLogUpdate(log);
3059
+ const elapsedMs = Date.now() - startTime;
3060
+ scheduleLogFinalization({
3061
+ type: "streaming",
3062
+ log,
3063
+ upstreamUrl,
3064
+ elapsedMs,
3065
+ responseStatus: upstreamRes.status,
3066
+ rawStream: full
3067
+ });
2515
3068
  }
2516
3069
  });
2517
3070
  if (upstreamRes.body === null) {
@@ -2521,23 +3074,17 @@ function handleStreamingResponse(upstreamRes, req, startTime, formatHandler, ups
2521
3074
  req.signal?.addEventListener("abort", () => {
2522
3075
  if (log.responseText === null) {
2523
3076
  logger.info(`[handler] Streaming client aborted: ${log.method} ${log.path}`);
2524
- log.elapsedMs = Date.now() - startTime;
2525
- if (chunks.length > 0) {
2526
- const full = chunks.join("");
2527
- log.responseText = formatHandler.extractStream(full, log, log.model ?? void 0, true);
2528
- if (log.streamingChunks && log.streamingChunks.chunks.length > 0) {
2529
- const chunkPath = writeChunks(
2530
- log.id,
2531
- log.streamingChunks.chunks,
2532
- log.streamingChunks.truncated
2533
- );
2534
- log.streamingChunksPath = chunkPath;
2535
- }
2536
- } else {
2537
- log.responseText = "Client aborted";
2538
- }
2539
- appendLogEntry({ ...buildFileLogEntry(log, upstreamUrl), error: "Client aborted" });
2540
- emitLogUpdate(log);
3077
+ const elapsedMs = Date.now() - startTime;
3078
+ const full = chunks.join("");
3079
+ const hasChunks = chunks.length > 0;
3080
+ scheduleLogFinalization({
3081
+ type: "stream-abort",
3082
+ log,
3083
+ upstreamUrl,
3084
+ elapsedMs,
3085
+ rawStream: full,
3086
+ hasChunks
3087
+ });
2541
3088
  }
2542
3089
  });
2543
3090
  const responseHeaders = new Headers(upstreamRes.headers);
@@ -2625,27 +3172,22 @@ async function handleProxy(req) {
2625
3172
  log.responseStatus = 499;
2626
3173
  log.responseText = "Client aborted";
2627
3174
  appendLogEntry({ ...buildFileLogEntry(log, upstreamUrl), error: "Client aborted" });
3175
+ finalizeLogUpdate(log);
2628
3176
  return new Response("Client aborted", { status: 499 });
2629
3177
  }
2630
3178
  logger.error(`[handler] Proxy error: ${req.method} ${parsed.apiPath}`, String(err));
2631
3179
  log.responseStatus = STATUS_BAD_GATEWAY;
2632
3180
  log.responseText = String(err);
2633
3181
  appendLogEntry({ ...buildFileLogEntry(log, upstreamUrl), error: String(err) });
3182
+ finalizeLogUpdate(log);
2634
3183
  return new Response(`Proxy error: ${err}`, { status: STATUS_BAD_GATEWAY });
2635
3184
  }
2636
3185
  const isStream = upstreamRes.headers.get(HEADER_CONTENT_TYPE)?.includes(CONTENT_TYPE_EVENT_STREAM) ?? false;
2637
3186
  if (!isStream) {
2638
3187
  const responseBody = await upstreamRes.text();
2639
- return handleNonStreamingResponse(
2640
- upstreamRes,
2641
- responseBody,
2642
- startTime,
2643
- formatHandler,
2644
- upstreamUrl,
2645
- log
2646
- );
3188
+ return handleNonStreamingResponse(upstreamRes, responseBody, startTime, upstreamUrl, log);
2647
3189
  }
2648
- return handleStreamingResponse(upstreamRes, req, startTime, formatHandler, upstreamUrl, log);
3190
+ return handleStreamingResponse(upstreamRes, req, startTime, upstreamUrl, log);
2649
3191
  }
2650
3192
  const Route$i = createFileRoute("/proxy/$")({
2651
3193
  server: {
@@ -2662,7 +3204,14 @@ const Route$i = createFileRoute("/proxy/$")({
2662
3204
  const Route$h = createFileRoute("/api/sessions")({
2663
3205
  server: {
2664
3206
  handlers: {
2665
- GET: () => Response.json(getSessions())
3207
+ GET: ({ request }) => {
3208
+ const url = new URL(request.url);
3209
+ const details = url.searchParams.get("details");
3210
+ if (details === "true" || details === "1") {
3211
+ return Response.json(getSessionSnapshots());
3212
+ }
3213
+ return Response.json(getSessions());
3214
+ }
2666
3215
  }
2667
3216
  }
2668
3217
  });
@@ -3832,7 +4381,7 @@ const Route$6 = createFileRoute("/api/logs/stream")({
3832
4381
  let cleanedUp = false;
3833
4382
  const unsubscribe = onLogUpdate((log) => {
3834
4383
  if (controllerRef === null) return;
3835
- if (sessionId !== void 0 && log.sessionId !== sessionId) return;
4384
+ if (sessionId !== void 0 && getLogSessionId(log) !== sessionId) return;
3836
4385
  if (model !== void 0 && log.model !== model) return;
3837
4386
  try {
3838
4387
  const data = `data: ${JSON.stringify({ type: "update", log })}