@tonyclaw/agent-inspector 4.0.1 → 4.0.2

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 (373) hide show
  1. package/.output/backend/nitro.json +1 -1
  2. package/.output/cli.js +192 -109
  3. package/.output/server/_ssr/index.mjs +1 -1
  4. package/.output/server/_ssr/{router-CTHqmroT.mjs → router-BBY_zWRK.mjs} +91 -3
  5. package/.output/server/index.mjs +1 -1
  6. package/.output/ui/assets/{CompareDrawer-L3n4J-Or.js → CompareDrawer-rtNElN-B.js} +1 -1
  7. package/.output/ui/assets/{InspectorPet-D5o_3OZS.js → InspectorPet-QaAiuF3d.js} +1 -1
  8. package/.output/ui/assets/{ProxyViewerContainer-Ocagx75S.js → ProxyViewerContainer-6twNZHwl.js} +4 -4
  9. package/.output/ui/assets/{ReplayDialog-Bk7x1h-y.js → ReplayDialog-B7KlziO2.js} +1 -1
  10. package/.output/ui/assets/{RequestAnatomy-U4lzL7Sc.js → RequestAnatomy-2XVFBrW4.js} +1 -1
  11. package/.output/ui/assets/{ResponseView-LOWfvBLK.js → ResponseView-BULgzLVB.js} +1 -1
  12. package/.output/ui/assets/{StreamingChunkSequence-DPfsrrXQ.js → StreamingChunkSequence-DSkvkbns.js} +1 -1
  13. package/.output/ui/assets/{_sessionId-OdX2Jvt1.js → _sessionId-D2dmJi3G.js} +1 -1
  14. package/.output/ui/assets/{_sessionId-0MlWS-CO.js → _sessionId-DcS0s1Mr.js} +1 -1
  15. package/.output/ui/assets/{index-Du4qNOOt.js → index-B0TN1KKO.js} +2 -2
  16. package/.output/ui/assets/{index-gvFj_9K6.js → index-Bva1zHEs.js} +1 -1
  17. package/.output/ui/assets/{index-Ea-c8hPg.js → index-BzBExOxu.js} +1 -1
  18. package/.output/ui/assets/{index-zVVS53Hd.js → index-k6sRQz55.js} +1 -1
  19. package/.output/ui/assets/{json-viewer-C1WAMzg3.js → json-viewer-CWM4tY-j.js} +1 -1
  20. package/.output/ui/assets/{jszip.min-B8AKu2zm.js → jszip.min-BkWvRFlo.js} +1 -1
  21. package/.output/ui/index.html +1 -1
  22. package/package.json +5 -10
  23. package/src/assets/IDE/claudecode-color.svg +0 -1
  24. package/src/assets/IDE/codex-color.svg +0 -1
  25. package/src/assets/IDE/opencode.svg +0 -1
  26. package/src/assets/IDE/xiaomimimo.svg +0 -1
  27. package/src/assets/favicon.svg +0 -21
  28. package/src/assets/logos/alibaba.svg +0 -1
  29. package/src/assets/logos/anthropic.svg +0 -1
  30. package/src/assets/logos/claude-code.svg +0 -4
  31. package/src/assets/logos/deepseek.svg +0 -1
  32. package/src/assets/logos/minimax.jpeg +0 -0
  33. package/src/assets/logos/openai.svg +0 -1
  34. package/src/assets/logos/opencode.svg +0 -4
  35. package/src/assets/logos/qwen.webp +0 -0
  36. package/src/assets/logos/zhipuai.svg +0 -219
  37. package/src/backend/router.tsx +0 -36
  38. package/src/backend/routes/$.ts +0 -26
  39. package/src/backend/routes/__root.ts +0 -4
  40. package/src/backend/routes/api/-instances.ts +0 -7
  41. package/src/backend/routes/api/alerts.summary.ts +0 -28
  42. package/src/backend/routes/api/alerts.ts +0 -70
  43. package/src/backend/routes/api/config.paths.ts +0 -14
  44. package/src/backend/routes/api/config.ts +0 -73
  45. package/src/backend/routes/api/ecosystem.packages.$packageId.help.ts +0 -11
  46. package/src/backend/routes/api/ecosystem.packages.$packageId.install.ts +0 -11
  47. package/src/backend/routes/api/ecosystem.packages.$packageId.runner-presets.ts +0 -11
  48. package/src/backend/routes/api/ecosystem.packages.$packageId.upgrade.ts +0 -11
  49. package/src/backend/routes/api/ecosystem.packages.ts +0 -21
  50. package/src/backend/routes/api/ecosystem.recipes.$recipeId.run.ts +0 -11
  51. package/src/backend/routes/api/ecosystem.tasks.$taskId.ts +0 -16
  52. package/src/backend/routes/api/ecosystem.tasks.ts +0 -12
  53. package/src/backend/routes/api/groups.$groupId.evidence.ts +0 -55
  54. package/src/backend/routes/api/groups.$groupId.sessions.ts +0 -50
  55. package/src/backend/routes/api/groups.$groupId.ts +0 -93
  56. package/src/backend/routes/api/groups.ts +0 -79
  57. package/src/backend/routes/api/health.ts +0 -22
  58. package/src/backend/routes/api/instances.$name.connection.ts +0 -12
  59. package/src/backend/routes/api/instances.$name.restart.ts +0 -23
  60. package/src/backend/routes/api/instances.$name.start.ts +0 -18
  61. package/src/backend/routes/api/instances.$name.stop.ts +0 -23
  62. package/src/backend/routes/api/instances.$name.ts +0 -12
  63. package/src/backend/routes/api/instances.ts +0 -16
  64. package/src/backend/routes/api/knowledge.candidates.$candidateId.promote.ts +0 -32
  65. package/src/backend/routes/api/knowledge.candidates.$candidateId.ts +0 -50
  66. package/src/backend/routes/api/knowledge.candidates.ts +0 -10
  67. package/src/backend/routes/api/knowledge.project-context.ts +0 -29
  68. package/src/backend/routes/api/knowledge.search.ts +0 -62
  69. package/src/backend/routes/api/knowledge.sessions.$sessionId.candidates.ts +0 -26
  70. package/src/backend/routes/api/logs.$id.body.ts +0 -58
  71. package/src/backend/routes/api/logs.$id.chunks.ts +0 -37
  72. package/src/backend/routes/api/logs.$id.replay.ts +0 -297
  73. package/src/backend/routes/api/logs.$id.ts +0 -23
  74. package/src/backend/routes/api/logs.import.ts +0 -39
  75. package/src/backend/routes/api/logs.stream.ts +0 -174
  76. package/src/backend/routes/api/logs.ts +0 -214
  77. package/src/backend/routes/api/mcp.ts +0 -25
  78. package/src/backend/routes/api/models.ts +0 -10
  79. package/src/backend/routes/api/pi-agent.ts +0 -29
  80. package/src/backend/routes/api/providers.$providerId.model-metadata.ts +0 -173
  81. package/src/backend/routes/api/providers.$providerId.test.log.ts +0 -342
  82. package/src/backend/routes/api/providers.$providerId.ts +0 -111
  83. package/src/backend/routes/api/providers.export.ts +0 -44
  84. package/src/backend/routes/api/providers.import.ts +0 -81
  85. package/src/backend/routes/api/providers.scan.ts +0 -28
  86. package/src/backend/routes/api/providers.ts +0 -102
  87. package/src/backend/routes/api/runs.$runId.evidence.ts +0 -55
  88. package/src/backend/routes/api/runs.$runId.ts +0 -50
  89. package/src/backend/routes/api/runs.ts +0 -62
  90. package/src/backend/routes/api/runtime.ts +0 -46
  91. package/src/backend/routes/api/sessions.ts +0 -51
  92. package/src/backend/routes/api/storage.ts +0 -56
  93. package/src/backend/routes/livez.ts +0 -13
  94. package/src/backend/routes/metrics.ts +0 -14
  95. package/src/backend/routes/proxy/$.ts +0 -15
  96. package/src/backend/routes/readyz.ts +0 -18
  97. package/src/backend/start.ts +0 -10
  98. package/src/cli/alias.ts +0 -431
  99. package/src/cli/detect-tools.ts +0 -123
  100. package/src/cli/doctor.ts +0 -836
  101. package/src/cli/instance.ts +0 -1071
  102. package/src/cli/instanceApi.ts +0 -213
  103. package/src/cli/instanceArgs.ts +0 -301
  104. package/src/cli/instanceControl.ts +0 -191
  105. package/src/cli/instanceModel.ts +0 -276
  106. package/src/cli/instanceRegistry.ts +0 -731
  107. package/src/cli/networkHints.ts +0 -170
  108. package/src/cli/onboard.ts +0 -1668
  109. package/src/cli/onboardTarget.ts +0 -228
  110. package/src/cli/rustBackendPackage.ts +0 -458
  111. package/src/cli/startupOutput.ts +0 -16
  112. package/src/cli/templates/codex-skill-onboard.ts +0 -320
  113. package/src/cli/templates/command-onboard.ts +0 -25
  114. package/src/cli/templates/skill-onboard.ts +0 -625
  115. package/src/cli.ts +0 -1433
  116. package/src/components/OnboardingBanner.tsx +0 -115
  117. package/src/components/ProxyViewer.tsx +0 -2259
  118. package/src/components/ProxyViewerContainer.tsx +0 -1076
  119. package/src/components/alerts/AlertsDialog.tsx +0 -615
  120. package/src/components/clients/ClientLogo.tsx +0 -175
  121. package/src/components/ecosystem/AgentLabDialog.tsx +0 -947
  122. package/src/components/errors/SafeErrorBoundary.tsx +0 -201
  123. package/src/components/groups/GroupsDialog.tsx +0 -1304
  124. package/src/components/inspector-pet/InspectorPet.tsx +0 -398
  125. package/src/components/inspector-pet/InspectorPetCrab3D.tsx +0 -455
  126. package/src/components/inspector-pet/inspectorPetLogic.ts +0 -272
  127. package/src/components/pi-agent/PiAgentPanel.tsx +0 -446
  128. package/src/components/pi-agent/piAgentChatLogic.ts +0 -273
  129. package/src/components/providers/ImportWizardDialog.tsx +0 -281
  130. package/src/components/providers/ProviderCard.tsx +0 -347
  131. package/src/components/providers/ProviderForm.tsx +0 -1183
  132. package/src/components/providers/ProviderLogo.tsx +0 -122
  133. package/src/components/providers/ProviderTestResultsView.tsx +0 -734
  134. package/src/components/providers/ProvidersPanel.tsx +0 -714
  135. package/src/components/providers/SettingsDialog.tsx +0 -1042
  136. package/src/components/providers/providerFormCapabilities.ts +0 -36
  137. package/src/components/proxy-viewer/AgentTraceSummary.tsx +0 -902
  138. package/src/components/proxy-viewer/AnswerMarkdown.tsx +0 -51
  139. package/src/components/proxy-viewer/ApplicationBar.tsx +0 -123
  140. package/src/components/proxy-viewer/CompareDrawer.tsx +0 -984
  141. package/src/components/proxy-viewer/ConversationGroup.tsx +0 -208
  142. package/src/components/proxy-viewer/ConversationGroupList.tsx +0 -652
  143. package/src/components/proxy-viewer/ConversationHeader.tsx +0 -429
  144. package/src/components/proxy-viewer/LiveConnectionStatus.tsx +0 -186
  145. package/src/components/proxy-viewer/LogEntry.tsx +0 -1419
  146. package/src/components/proxy-viewer/LogEntryHeader.tsx +0 -596
  147. package/src/components/proxy-viewer/ProviderLogoStack.tsx +0 -57
  148. package/src/components/proxy-viewer/RawExportWarning.tsx +0 -77
  149. package/src/components/proxy-viewer/ReplayDialog.tsx +0 -636
  150. package/src/components/proxy-viewer/RequestToolsPanel.tsx +0 -299
  151. package/src/components/proxy-viewer/ResponseView.tsx +0 -202
  152. package/src/components/proxy-viewer/SessionLoadProgressBar.tsx +0 -61
  153. package/src/components/proxy-viewer/StreamingChunkSequence.tsx +0 -265
  154. package/src/components/proxy-viewer/ThreadConnector.tsx +0 -165
  155. package/src/components/proxy-viewer/ToolTraceEvents.tsx +0 -64
  156. package/src/components/proxy-viewer/TurnGroup.tsx +0 -383
  157. package/src/components/proxy-viewer/TurnGroupList.tsx +0 -156
  158. package/src/components/proxy-viewer/accessibility.ts +0 -8
  159. package/src/components/proxy-viewer/anatomy/RequestAnatomy.tsx +0 -887
  160. package/src/components/proxy-viewer/anatomy/SegmentBar.tsx +0 -222
  161. package/src/components/proxy-viewer/anatomy/contextIntelligence.ts +0 -737
  162. package/src/components/proxy-viewer/anatomy/sessionContextSummary.ts +0 -268
  163. package/src/components/proxy-viewer/anatomy/tokenEstimate.ts +0 -53
  164. package/src/components/proxy-viewer/anatomy/types.ts +0 -41
  165. package/src/components/proxy-viewer/anatomy/useAnatomyJump.ts +0 -114
  166. package/src/components/proxy-viewer/bodyHydration.ts +0 -123
  167. package/src/components/proxy-viewer/cacheTrend.ts +0 -50
  168. package/src/components/proxy-viewer/diff/DiffView.tsx +0 -321
  169. package/src/components/proxy-viewer/diff/computeDiff.ts +0 -178
  170. package/src/components/proxy-viewer/diff/index.ts +0 -3
  171. package/src/components/proxy-viewer/formats/anthropic/ContentBlocks.tsx +0 -194
  172. package/src/components/proxy-viewer/formats/anthropic/ResponseView.tsx +0 -81
  173. package/src/components/proxy-viewer/formats/anthropic/thinkingExtract.ts +0 -21
  174. package/src/components/proxy-viewer/formats/index.tsx +0 -47
  175. package/src/components/proxy-viewer/formats/openai/ResponseView.tsx +0 -442
  176. package/src/components/proxy-viewer/index.ts +0 -9
  177. package/src/components/proxy-viewer/lazy.ts +0 -41
  178. package/src/components/proxy-viewer/liveConnectionState.ts +0 -158
  179. package/src/components/proxy-viewer/log-formats/anthropic.ts +0 -191
  180. package/src/components/proxy-viewer/log-formats/index.ts +0 -23
  181. package/src/components/proxy-viewer/log-formats/openai.ts +0 -256
  182. package/src/components/proxy-viewer/log-formats/types.ts +0 -40
  183. package/src/components/proxy-viewer/log-formats/unknown.ts +0 -18
  184. package/src/components/proxy-viewer/logEntryVisibility.ts +0 -73
  185. package/src/components/proxy-viewer/logFocus.ts +0 -54
  186. package/src/components/proxy-viewer/proxyViewerContainerLogic.ts +0 -124
  187. package/src/components/proxy-viewer/proxyViewerLogic.ts +0 -488
  188. package/src/components/proxy-viewer/replayComparison.ts +0 -131
  189. package/src/components/proxy-viewer/requestDiff.ts +0 -277
  190. package/src/components/proxy-viewer/requestTools.ts +0 -220
  191. package/src/components/proxy-viewer/useCopyFeedback.ts +0 -38
  192. package/src/components/proxy-viewer/useKeyboardNavigation.ts +0 -232
  193. package/src/components/proxy-viewer/viewerState.ts +0 -176
  194. package/src/components/ui/badge.tsx +0 -47
  195. package/src/components/ui/button.tsx +0 -47
  196. package/src/components/ui/collapsible.tsx +0 -21
  197. package/src/components/ui/confirm-dialog.tsx +0 -51
  198. package/src/components/ui/crab-logo.tsx +0 -45
  199. package/src/components/ui/crab-variants.tsx +0 -467
  200. package/src/components/ui/dialog.tsx +0 -130
  201. package/src/components/ui/icon-trigger.ts +0 -2
  202. package/src/components/ui/json-viewer-bulk.ts +0 -132
  203. package/src/components/ui/json-viewer.tsx +0 -508
  204. package/src/components/ui/scroll-area.tsx +0 -54
  205. package/src/components/ui/select.tsx +0 -178
  206. package/src/components/ui/separator.tsx +0 -28
  207. package/src/components/ui/tabs.tsx +0 -88
  208. package/src/components/ui/tooltip.tsx +0 -51
  209. package/src/components/ui/transient-toast.tsx +0 -105
  210. package/src/contracts/anthropic.ts +0 -230
  211. package/src/contracts/index.ts +0 -59
  212. package/src/contracts/json.ts +0 -37
  213. package/src/contracts/log.ts +0 -139
  214. package/src/contracts/openai.ts +0 -267
  215. package/src/index.css +0 -11
  216. package/src/knowledge/candidateStore.ts +0 -94
  217. package/src/knowledge/distiller.ts +0 -98
  218. package/src/knowledge/openclawClient.ts +0 -161
  219. package/src/knowledge/openclawGatewayClient.ts +0 -249
  220. package/src/knowledge/openclawMarkdown.ts +0 -146
  221. package/src/knowledge/redactor.ts +0 -80
  222. package/src/knowledge/types.ts +0 -83
  223. package/src/lib/alertContract.ts +0 -76
  224. package/src/lib/apiClient.ts +0 -166
  225. package/src/lib/apiFormat.ts +0 -29
  226. package/src/lib/backendImplementation.ts +0 -16
  227. package/src/lib/basePath.ts +0 -107
  228. package/src/lib/browserRuntimeContract.ts +0 -124
  229. package/src/lib/browserRuntimeResolver.ts +0 -333
  230. package/src/lib/chunkLoadRecovery.ts +0 -83
  231. package/src/lib/claudeCodeStrip.ts +0 -98
  232. package/src/lib/clipboard.ts +0 -67
  233. package/src/lib/codingAgentEndpoints.ts +0 -54
  234. package/src/lib/ecosystemContract.ts +0 -130
  235. package/src/lib/export-logs.ts +0 -287
  236. package/src/lib/groupContract.ts +0 -166
  237. package/src/lib/instanceContract.ts +0 -239
  238. package/src/lib/jsonFile.ts +0 -46
  239. package/src/lib/logImportContract.ts +0 -12
  240. package/src/lib/managedInstance.ts +0 -112
  241. package/src/lib/mask.ts +0 -4
  242. package/src/lib/objectUtils.ts +0 -22
  243. package/src/lib/piAgentContract.ts +0 -102
  244. package/src/lib/providerContract.ts +0 -125
  245. package/src/lib/providerImportContract.ts +0 -27
  246. package/src/lib/providerModelMetadata.ts +0 -458
  247. package/src/lib/providerTestContract.ts +0 -170
  248. package/src/lib/providerTestPrompt.ts +0 -100
  249. package/src/lib/publicBasePathContext.ts +0 -137
  250. package/src/lib/resourceLimits.ts +0 -345
  251. package/src/lib/runContract.ts +0 -164
  252. package/src/lib/runtimeConfig.ts +0 -43
  253. package/src/lib/safeDiagnostic.ts +0 -38
  254. package/src/lib/safeFetch.ts +0 -508
  255. package/src/lib/serverPort.ts +0 -47
  256. package/src/lib/sessionInfoContract.ts +0 -78
  257. package/src/lib/sessionUrl.ts +0 -46
  258. package/src/lib/ssrfGuard.ts +0 -133
  259. package/src/lib/stopReason.ts +0 -130
  260. package/src/lib/themeMode.ts +0 -41
  261. package/src/lib/timeDisplay.ts +0 -22
  262. package/src/lib/toolTrace.ts +0 -290
  263. package/src/lib/uiScale.ts +0 -117
  264. package/src/lib/unknown.ts +0 -27
  265. package/src/lib/upstreamUrl.ts +0 -150
  266. package/src/lib/useAlerts.ts +0 -82
  267. package/src/lib/useGroupEvidence.ts +0 -37
  268. package/src/lib/useGroups.ts +0 -32
  269. package/src/lib/useOnboarding.ts +0 -93
  270. package/src/lib/useProviders.ts +0 -30
  271. package/src/lib/useStripConfig.ts +0 -154
  272. package/src/lib/useThemeMode.ts +0 -40
  273. package/src/lib/useUiScale.ts +0 -52
  274. package/src/lib/utils.ts +0 -78
  275. package/src/mcp/currentContext.ts +0 -65
  276. package/src/mcp/instanceHandlers.ts +0 -151
  277. package/src/mcp/loopback.ts +0 -88
  278. package/src/mcp/mode.ts +0 -75
  279. package/src/mcp/previewExtractor.ts +0 -233
  280. package/src/mcp/server.ts +0 -2274
  281. package/src/mcp/toolHandlers.ts +0 -1533
  282. package/src/proxy/alerts.ts +0 -664
  283. package/src/proxy/chunkStorage.ts +0 -123
  284. package/src/proxy/config.ts +0 -218
  285. package/src/proxy/constants.ts +0 -66
  286. package/src/proxy/dataDir.ts +0 -147
  287. package/src/proxy/ecosystemExecutionConfirmation.ts +0 -114
  288. package/src/proxy/ecosystemExecutionRoute.ts +0 -116
  289. package/src/proxy/ecosystemRegistry.ts +0 -296
  290. package/src/proxy/ecosystemTasks.ts +0 -415
  291. package/src/proxy/evidenceAnalysis.ts +0 -526
  292. package/src/proxy/evidenceExporter.ts +0 -235
  293. package/src/proxy/formats/anthropic/anthropicProvider.ts +0 -75
  294. package/src/proxy/formats/anthropic/handler.ts +0 -75
  295. package/src/proxy/formats/anthropic/index.ts +0 -13
  296. package/src/proxy/formats/anthropic/register.ts +0 -4
  297. package/src/proxy/formats/anthropic/schemas.ts +0 -6
  298. package/src/proxy/formats/anthropic/stream.ts +0 -205
  299. package/src/proxy/formats/handler.ts +0 -46
  300. package/src/proxy/formats/index.ts +0 -13
  301. package/src/proxy/formats/jsonSchema.ts +0 -2
  302. package/src/proxy/formats/openai/alibabaProvider.ts +0 -34
  303. package/src/proxy/formats/openai/codexMetadata.ts +0 -61
  304. package/src/proxy/formats/openai/handler.ts +0 -133
  305. package/src/proxy/formats/openai/index.ts +0 -33
  306. package/src/proxy/formats/openai/provider.ts +0 -50
  307. package/src/proxy/formats/openai/register.ts +0 -4
  308. package/src/proxy/formats/openai/schemas.ts +0 -25
  309. package/src/proxy/formats/openai/stream.ts +0 -600
  310. package/src/proxy/formats/openai/zhipuProvider.ts +0 -31
  311. package/src/proxy/formats/providerAdapter.ts +0 -50
  312. package/src/proxy/formats/providerRegistry.ts +0 -51
  313. package/src/proxy/formats/providers/index.ts +0 -3
  314. package/src/proxy/formats/registry.ts +0 -65
  315. package/src/proxy/groupEvidenceExporter.ts +0 -371
  316. package/src/proxy/groupStore.ts +0 -336
  317. package/src/proxy/handler.ts +0 -729
  318. package/src/proxy/identityProxy.ts +0 -1842
  319. package/src/proxy/jsonlRecovery.ts +0 -554
  320. package/src/proxy/logBodyChunks.ts +0 -91
  321. package/src/proxy/logFinalizer.ts +0 -777
  322. package/src/proxy/logFinalizer.worker.ts +0 -24
  323. package/src/proxy/logImportUpload.ts +0 -128
  324. package/src/proxy/logImporter.ts +0 -813
  325. package/src/proxy/logIndex.ts +0 -663
  326. package/src/proxy/logSearch.ts +0 -122
  327. package/src/proxy/logger.ts +0 -537
  328. package/src/proxy/openaiOrphanToolStrip.ts +0 -142
  329. package/src/proxy/platformCommands.ts +0 -258
  330. package/src/proxy/privateDataPath.ts +0 -419
  331. package/src/proxy/providerImporters.ts +0 -578
  332. package/src/proxy/providerScanStore.ts +0 -87
  333. package/src/proxy/providerSecretStore.ts +0 -216
  334. package/src/proxy/providers.ts +0 -998
  335. package/src/proxy/rawStreamCapture.ts +0 -342
  336. package/src/proxy/runFailures.ts +0 -94
  337. package/src/proxy/runStore.ts +0 -214
  338. package/src/proxy/runtimeAdmission.ts +0 -55
  339. package/src/proxy/runtimeHealth.ts +0 -221
  340. package/src/proxy/runtimeMetrics.ts +0 -149
  341. package/src/proxy/runtimeShutdown.ts +0 -157
  342. package/src/proxy/schemas.ts +0 -112
  343. package/src/proxy/sessionArchive.ts +0 -479
  344. package/src/proxy/sessionInfo.ts +0 -266
  345. package/src/proxy/sessionProcess.ts +0 -194
  346. package/src/proxy/sessionRuntime.ts +0 -121
  347. package/src/proxy/sessionSupervisor.ts +0 -305
  348. package/src/proxy/sessionWorkerEntry.ts +0 -26
  349. package/src/proxy/shutdownCoordinator.ts +0 -90
  350. package/src/proxy/socketTracker.ts +0 -639
  351. package/src/proxy/sqliteLogIndex.ts +0 -733
  352. package/src/proxy/storageLifecycle.ts +0 -432
  353. package/src/proxy/store.ts +0 -1773
  354. package/src/proxy/streamTiming.ts +0 -31
  355. package/src/proxy/toolSchemaWarnings.ts +0 -529
  356. package/src/proxy/uiAssetServer.ts +0 -106
  357. package/src/proxy/upstream.ts +0 -127
  358. package/src/services/alerts.ts +0 -12
  359. package/src/services/groups.ts +0 -119
  360. package/src/services/piAgent.ts +0 -421
  361. package/src/services/piAgentRetrieval.ts +0 -1180
  362. package/src/services/result.ts +0 -17
  363. package/src/services/runs.ts +0 -76
  364. package/src/ui/bootstrap.ts +0 -167
  365. package/src/ui/index.html +0 -38
  366. package/src/ui/logsStreamClient.ts +0 -20
  367. package/src/ui/main.tsx +0 -13
  368. package/src/ui/public/runtime-config.js +0 -2
  369. package/src/ui/router.tsx +0 -13
  370. package/src/ui/routes/__root.tsx +0 -69
  371. package/src/ui/routes/index.tsx +0 -35
  372. package/src/ui/routes/session/$sessionId.tsx +0 -50
  373. package/src/ui/runtimeDiscovery.ts +0 -50
@@ -1,1773 +0,0 @@
1
- import {
2
- existsSync,
3
- createReadStream,
4
- createWriteStream,
5
- readdirSync,
6
- statSync,
7
- type WriteStream,
8
- } from "node:fs";
9
- import { open, unlink, rename } from "node:fs/promises";
10
- import { readdir as readdirCallback } from "node:fs/promises";
11
- import { createInterface } from "node:readline";
12
- import { Buffer } from "node:buffer";
13
- import { dirname, join } from "node:path";
14
- import {
15
- appendLogEntry,
16
- resolveLogDir,
17
- logger,
18
- runWithFlushedLogWriteLock,
19
- runWithLogWriteLock,
20
- } from "./logger";
21
- import { getChunksDir } from "./chunkStorage";
22
- import {
23
- addToIndex,
24
- createLogIndexEntryMetadata,
25
- findInIndex,
26
- flushIndex,
27
- getNextLogId,
28
- getCurrentLogFile,
29
- listFilteredIndexEntries,
30
- listIndexEntries,
31
- rebuildIndex,
32
- updateLogIndexEntryMetadata,
33
- type LogIndexEntry,
34
- type LogIndexSummary,
35
- } from "./logIndex";
36
- import { writeChunks } from "./chunkStorage";
37
- import { closeSqliteLogIndex, listSqliteLogCursorEntries } from "./sqliteLogIndex";
38
- import type { SessionInfo, SessionLogSummary } from "../lib/sessionInfoContract";
39
- import { extractToolTraceEvents } from "../lib/toolTrace";
40
- import { extractStopReason } from "../lib/stopReason";
41
- import { textByteLength } from "../lib/unknown";
42
- import { buildSessionInfo, buildSessionLogSummary } from "./sessionInfo";
43
- import type { CapturedLog } from "./schemas";
44
- import { CapturedLogSchema } from "./schemas";
45
- import { apiFormatForPath } from "./formats/registry";
46
- import type { ImportedLogDraft } from "./logImporter";
47
- import {
48
- archiveSessionLogs,
49
- clearSessionArchives,
50
- deleteArchivedLogsByIds,
51
- findArchivedLogById,
52
- getSessionArchiveRoot,
53
- listArchivedSessionLogs,
54
- listArchivedSessionIds,
55
- } from "./sessionArchive";
56
- import {
57
- ensurePrivateDirectorySync,
58
- PRIVATE_FILE_MODE,
59
- securePrivateFileSync,
60
- } from "./privateDataPath";
61
- import { recoverCapturedLogStorage } from "./jsonlRecovery";
62
- import {
63
- clearSessionRegistry,
64
- getLogSessionId,
65
- getSessionSnapshots,
66
- getSessionIds,
67
- markSessionFinished,
68
- markSessionStarted,
69
- observeSessionLog,
70
- rebuildSessionRegistry,
71
- resolveSessionIdentity,
72
- type SessionClientInfo,
73
- type SessionSnapshot,
74
- } from "./sessionSupervisor";
75
- import { installStorageLifecycleMaintenance, runStorageLifecycleGc } from "./storageLifecycle";
76
-
77
- export type { CapturedLog };
78
- export { getNextLogId };
79
- export { getLogSessionId, getSessionSnapshots } from "./sessionSupervisor";
80
-
81
- const MAX_MEMORY_CACHE = 100;
82
-
83
- export type LogStorageStats = {
84
- memoryCount: number;
85
- logDir: string;
86
- logFileCount: number;
87
- logBytes: number;
88
- chunkDir: string;
89
- chunkFileCount: number;
90
- chunkBytes: number;
91
- sessionArchiveDir: string;
92
- sessionArchiveFileCount: number;
93
- sessionArchiveBytes: number;
94
- };
95
-
96
- export type ClearPersistedLogStorageResult = {
97
- cleared: number;
98
- logFilesDeleted: number;
99
- chunkFilesDeleted: number;
100
- sessionArchiveFilesDeleted: number;
101
- };
102
-
103
- export type ClearPersistedLogsByIdsResult = {
104
- cleared: number;
105
- logFilesRewritten: number;
106
- chunkFilesDeleted: number;
107
- sessionArchiveLogsDeleted: number;
108
- };
109
-
110
- export type ListLogsPageOptions = {
111
- sessionId?: string;
112
- model?: string;
113
- clientPid?: number;
114
- offset: number;
115
- limit: number;
116
- includeBodies?: boolean;
117
- };
118
-
119
- export type ListLogsPageResult = {
120
- logs: CapturedLog[];
121
- total: number;
122
- offset: number;
123
- limit: number;
124
- };
125
-
126
- export type ListLogsCursorPageOptions = {
127
- sessionId?: string;
128
- model?: string;
129
- clientPid?: number;
130
- limit: number;
131
- includeBodies?: boolean;
132
- beforeLogId?: number;
133
- afterLogId?: number;
134
- anchor?: "newest" | "oldest";
135
- };
136
-
137
- export type ListLogsCursorPageResult = {
138
- logs: CapturedLog[];
139
- total: number;
140
- limit: number;
141
- hasOlder: boolean;
142
- hasNewer: boolean;
143
- oldestLogId: number | null;
144
- newestLogId: number | null;
145
- };
146
-
147
- export type GetSessionInfoOptions = {
148
- baseUrl: string;
149
- includeHistory?: boolean;
150
- latestLogLimit?: number;
151
- };
152
-
153
- // Memory cache: id -> CapturedLog (recent logs only, FIFO eviction)
154
- const memoryCache: Map<number, CapturedLog> = new Map();
155
- const MAX_CURSOR_PAGE_INDEXES = 16;
156
-
157
- type LogCursorPageIndex = {
158
- key: string;
159
- sessionId?: string;
160
- model?: string;
161
- clientPid?: number;
162
- ids: number[];
163
- logsById: Map<number, CapturedLog>;
164
- entriesById: Map<number, LogIndexEntry>;
165
- };
166
-
167
- const logCursorPageIndexes: Map<string, LogCursorPageIndex> = new Map();
168
- let logCursorPageIndexBuildCount = 0;
169
- let sessionArchiveQueue: Promise<void> = Promise.resolve();
170
- const SESSION_ARCHIVE_BATCH_SIZE = 128;
171
- const SESSION_ARCHIVE_FLUSH_DELAY_MS = 10;
172
- let sessionArchiveBuffer: CapturedLog[] = [];
173
- let sessionArchiveFlushTimeout: ReturnType<typeof setTimeout> | null = null;
174
-
175
- function drainSessionArchiveBuffer(): CapturedLog[] {
176
- const logs = sessionArchiveBuffer;
177
- sessionArchiveBuffer = [];
178
- return logs;
179
- }
180
-
181
- function queueSessionArchiveFlush(): void {
182
- const logs = drainSessionArchiveBuffer();
183
- if (logs.length === 0) return;
184
- sessionArchiveQueue = sessionArchiveQueue
185
- .catch(() => undefined)
186
- .then(async () => {
187
- await archiveSessionLogs(logs);
188
- })
189
- .catch((err: unknown) => {
190
- logger.warn("[store] Failed to archive session logs:", String(err));
191
- });
192
- }
193
-
194
- function cancelSessionArchiveFlushTimer(): void {
195
- if (sessionArchiveFlushTimeout === null) return;
196
- clearTimeout(sessionArchiveFlushTimeout);
197
- sessionArchiveFlushTimeout = null;
198
- }
199
-
200
- function scheduleSessionArchiveFlush(): void {
201
- if (sessionArchiveFlushTimeout !== null) return;
202
- sessionArchiveFlushTimeout = setTimeout(() => {
203
- sessionArchiveFlushTimeout = null;
204
- queueSessionArchiveFlush();
205
- }, SESSION_ARCHIVE_FLUSH_DELAY_MS);
206
- }
207
-
208
- function cursorPageIndexKey(
209
- options: Pick<ListLogsCursorPageOptions, "sessionId" | "model" | "clientPid">,
210
- ): string {
211
- return JSON.stringify({
212
- sessionId: options.sessionId ?? null,
213
- model: options.model ?? null,
214
- clientPid: options.clientPid ?? null,
215
- });
216
- }
217
-
218
- function matchesCursorIndexFilters(log: CapturedLog, index: LogCursorPageIndex): boolean {
219
- if (index.sessionId !== undefined && getLogSessionId(log) !== index.sessionId) return false;
220
- if (index.model !== undefined && log.model !== index.model) return false;
221
- if (index.clientPid !== undefined && log.clientPid !== index.clientPid) return false;
222
- return true;
223
- }
224
-
225
- function insertSortedLogId(ids: number[], id: number): void {
226
- if (ids.includes(id)) return;
227
- const insertionIndex = ids.findIndex((candidate) => candidate > id);
228
- if (insertionIndex === -1) {
229
- ids.push(id);
230
- return;
231
- }
232
- ids.splice(insertionIndex, 0, id);
233
- }
234
-
235
- function upsertLogCursorPageIndexes(log: CapturedLog): void {
236
- for (const index of logCursorPageIndexes.values()) {
237
- if (!matchesCursorIndexFilters(log, index)) {
238
- if (index.logsById.delete(log.id)) {
239
- index.ids = index.ids.filter((id) => id !== log.id);
240
- }
241
- index.entriesById.delete(log.id);
242
- continue;
243
- }
244
- index.logsById.set(log.id, log);
245
- insertSortedLogId(index.ids, log.id);
246
- }
247
- }
248
-
249
- function removeLogsFromCursorPageIndexes(ids: ReadonlySet<number>): void {
250
- if (ids.size === 0) return;
251
- for (const index of logCursorPageIndexes.values()) {
252
- for (const id of ids) {
253
- index.logsById.delete(id);
254
- index.entriesById.delete(id);
255
- }
256
- index.ids = index.ids.filter((id) => !ids.has(id));
257
- }
258
- }
259
-
260
- function clearLogCursorPageIndexes(): void {
261
- logCursorPageIndexes.clear();
262
- logCursorPageIndexBuildCount = 0;
263
- }
264
-
265
- function evictOldestIfNeeded(): void {
266
- while (memoryCache.size > MAX_MEMORY_CACHE) {
267
- // Map preserves insertion order; first key = oldest
268
- const oldestKey = memoryCache.keys().next().value;
269
- if (oldestKey === undefined) break;
270
- memoryCache.delete(oldestKey);
271
- }
272
- }
273
-
274
- function normalizeLog(log: CapturedLog): CapturedLog {
275
- const sessionId = getLogSessionId(log);
276
- const pathFormat = apiFormatForPath(log.path);
277
- const apiFormat = pathFormat === "unknown" ? log.apiFormat : pathFormat;
278
- const captureIncomplete = log.captureIncomplete ?? false;
279
- const droppedChunks = log.droppedChunks ?? 0;
280
- const droppedBytes = log.droppedBytes ?? 0;
281
- const captureIncompleteReason = log.captureIncompleteReason ?? null;
282
- const stopReason = log.stopReason ?? extractStopReason(log);
283
- if (
284
- sessionId === log.sessionId &&
285
- apiFormat === log.apiFormat &&
286
- log.captureIncomplete !== undefined &&
287
- log.droppedChunks !== undefined &&
288
- log.droppedBytes !== undefined &&
289
- log.captureIncompleteReason !== undefined &&
290
- log.stopReason !== undefined
291
- ) {
292
- return log;
293
- }
294
- return {
295
- ...log,
296
- sessionId,
297
- apiFormat,
298
- captureIncomplete,
299
- droppedChunks,
300
- droppedBytes,
301
- captureIncompleteReason,
302
- stopReason,
303
- };
304
- }
305
-
306
- function addToCache(log: CapturedLog): void {
307
- const cachedLog = normalizeLog(log);
308
- // If updating an existing entry, remove first to reset insertion order
309
- if (memoryCache.has(cachedLog.id)) {
310
- memoryCache.delete(cachedLog.id);
311
- }
312
- memoryCache.set(cachedLog.id, cachedLog);
313
- upsertLogCursorPageIndexes(cachedLog);
314
- evictOldestIfNeeded();
315
- }
316
-
317
- function archiveLogSnapshot(log: CapturedLog): void {
318
- sessionArchiveBuffer.push(normalizeLog(log));
319
- if (sessionArchiveBuffer.length >= SESSION_ARCHIVE_BATCH_SIZE) {
320
- cancelSessionArchiveFlushTimer();
321
- queueSessionArchiveFlush();
322
- } else if (sessionArchiveBuffer.length === 1) {
323
- scheduleSessionArchiveFlush();
324
- }
325
- }
326
-
327
- export async function flushSessionArchiveWrites(): Promise<void> {
328
- cancelSessionArchiveFlushTimer();
329
- if (sessionArchiveBuffer.length > 0) queueSessionArchiveFlush();
330
- await sessionArchiveQueue;
331
- if (sessionArchiveBuffer.length > 0) await flushSessionArchiveWrites();
332
- }
333
-
334
- function removeFromCache(id: number): void {
335
- memoryCache.delete(id);
336
- removeLogsFromCursorPageIndexes(new Set([id]));
337
- }
338
-
339
- export function compactLogForList(log: CapturedLog): CapturedLog {
340
- return {
341
- ...log,
342
- rawRequestBody: null,
343
- responseText: null,
344
- rawHeaders: undefined,
345
- headers: undefined,
346
- streamingChunks: undefined,
347
- stopReason: log.stopReason ?? extractStopReason(log),
348
- toolTraceEvents: log.toolTraceEvents ?? extractToolTraceEvents(log),
349
- rawRequestBodyBytes: log.rawRequestBodyBytes ?? textByteLength(log.rawRequestBody),
350
- responseTextBytes: log.responseTextBytes ?? textByteLength(log.responseText),
351
- bodyContentMode: "compact",
352
- };
353
- }
354
-
355
- function compactLogFromIndexSummary(summary: LogIndexSummary): CapturedLog {
356
- return normalizeLog({
357
- id: summary.id,
358
- timestamp: summary.timestamp,
359
- method: summary.method,
360
- path: summary.path,
361
- model: summary.model,
362
- sessionId: summary.sessionId,
363
- rawRequestBody: null,
364
- responseStatus: summary.responseStatus,
365
- responseText: null,
366
- inputTokens: summary.inputTokens,
367
- outputTokens: summary.outputTokens,
368
- cacheCreationInputTokens: summary.cacheCreationInputTokens,
369
- cacheReadInputTokens: summary.cacheReadInputTokens,
370
- elapsedMs: summary.elapsedMs,
371
- firstChunkMs: summary.firstChunkMs,
372
- totalStreamMs: summary.totalStreamMs,
373
- tokensPerSecond: summary.tokensPerSecond,
374
- streaming: summary.streaming,
375
- userAgent: summary.userAgent,
376
- origin: summary.origin,
377
- apiFormat: summary.apiFormat,
378
- isTest: summary.isTest,
379
- replayOfLogId: summary.replayOfLogId,
380
- providerName: summary.providerName,
381
- clientPort: summary.clientPort,
382
- clientPid: summary.clientPid,
383
- clientCwd: summary.clientCwd,
384
- clientProjectFolder: summary.clientProjectFolder,
385
- streamingChunksPath: summary.streamingChunksPath,
386
- rawRequestBodyBytes: summary.rawRequestBodyBytes,
387
- responseTextBytes: summary.responseTextBytes,
388
- bodyContentMode: "compact",
389
- captureIncomplete: summary.captureIncomplete,
390
- droppedChunks: summary.droppedChunks,
391
- droppedBytes: summary.droppedBytes,
392
- captureIncompleteReason: summary.captureIncompleteReason,
393
- warnings: summary.warnings,
394
- stopReason: summary.stopReason,
395
- toolTraceEvents: summary.toolTraceEvents,
396
- error: summary.error,
397
- });
398
- }
399
-
400
- function prepareLogForList(log: CapturedLog, includeBodies: boolean): CapturedLog {
401
- return includeBodies ? log : compactLogForList(log);
402
- }
403
-
404
- /**
405
- * Add a test log entry to the in-memory store and persistent log file.
406
- * This is used by the provider test endpoint to seed provider-test sessions.
407
- */
408
- export async function addTestLogEntry(entry: Omit<CapturedLog, "id">): Promise<CapturedLog> {
409
- const id = await getNextLogId();
410
-
411
- // Persist streaming chunks to disk if present
412
- let streamingChunksPath: string | null = null;
413
- if (entry.streamingChunks !== undefined && entry.streamingChunks.chunks.length > 0) {
414
- streamingChunksPath = writeChunks(
415
- id,
416
- entry.streamingChunks.chunks,
417
- entry.streamingChunks.truncated,
418
- );
419
- }
420
-
421
- const session = resolveSessionIdentity({
422
- explicitSessionId: entry.sessionId,
423
- isTest: entry.isTest,
424
- });
425
-
426
- const log: CapturedLog = normalizeLog({
427
- id,
428
- ...entry,
429
- sessionId: session.id,
430
- streamingChunksPath,
431
- });
432
-
433
- await runWithLogWriteLock(async () => {
434
- const logFile = getCurrentLogFile();
435
- appendLogEntry(log);
436
- await addToIndex(id, logFile, -1, -1, createLogIndexEntryMetadata(log));
437
- });
438
-
439
- addToCache(log);
440
- archiveLogSnapshot(log);
441
- observeSessionLog(log, session.source);
442
- emitLogUpdate(log);
443
- return log;
444
- }
445
-
446
- export async function importCapturedLogs(
447
- entries: readonly ImportedLogDraft[],
448
- sessionId: string,
449
- ): Promise<CapturedLog[]> {
450
- const imported: CapturedLog[] = [];
451
- for (const entry of entries) {
452
- const log = await addTestLogEntry({
453
- ...entry,
454
- sessionId,
455
- isTest: false,
456
- replayOfLogId: null,
457
- });
458
- imported.push(log);
459
- }
460
- return imported;
461
- }
462
-
463
- type ClientInfo = SessionClientInfo;
464
-
465
- export async function createLog(
466
- method: string,
467
- path: string,
468
- requestBody: string | null,
469
- headers: Headers,
470
- clientInfo?: ClientInfo,
471
- rawHeaders?: Record<string, string>,
472
- upstreamHeaders?: Record<string, string>,
473
- apiFormat: "anthropic" | "openai" | "unknown" = "unknown",
474
- model: string | null = null,
475
- sessionId: string | null = null,
476
- /** Pre-acquired log id; if omitted, `getNextLogId` is awaited internally. */
477
- preAcquiredId?: number,
478
- ): Promise<CapturedLog> {
479
- const userAgent = headers.get("user-agent");
480
- const origin = headers.get("origin");
481
-
482
- const id = preAcquiredId ?? (await getNextLogId());
483
- const session = resolveSessionIdentity({
484
- explicitSessionId: sessionId,
485
- clientInfo,
486
- });
487
-
488
- const log: CapturedLog = normalizeLog({
489
- id,
490
- timestamp: new Date().toISOString(),
491
- method,
492
- path,
493
- model,
494
- sessionId: session.id,
495
- rawRequestBody: requestBody,
496
- responseStatus: null,
497
- responseText: null,
498
- inputTokens: null,
499
- outputTokens: null,
500
- cacheCreationInputTokens: null,
501
- cacheReadInputTokens: null,
502
- elapsedMs: null,
503
- firstChunkMs: null,
504
- totalStreamMs: null,
505
- tokensPerSecond: null,
506
- streaming: false,
507
- userAgent,
508
- origin,
509
- rawHeaders: rawHeaders,
510
- headers: upstreamHeaders,
511
- apiFormat,
512
- isTest: false,
513
- providerName: null,
514
- clientPort: clientInfo?.port ?? null,
515
- clientPid: clientInfo?.pid ?? null,
516
- clientCwd: clientInfo?.cwd ?? null,
517
- clientProjectFolder: clientInfo?.projectFolder ?? null,
518
- streamingChunksPath: null,
519
- captureIncomplete: false,
520
- droppedChunks: 0,
521
- droppedBytes: 0,
522
- captureIncompleteReason: null,
523
- });
524
-
525
- // Write to disk and update index
526
- await runWithLogWriteLock(async () => {
527
- const logFile = getCurrentLogFile();
528
- appendLogEntry(log);
529
- await addToIndex(id, logFile, -1, -1, createLogIndexEntryMetadata(log));
530
- });
531
-
532
- // Add to memory cache
533
- addToCache(log);
534
- archiveLogSnapshot(log);
535
- markSessionStarted(log, session.source);
536
- emitLogUpdate(log);
537
-
538
- return log;
539
- }
540
-
541
- export function finalizeLogUpdate(log: CapturedLog): void {
542
- addToCache(log);
543
- archiveLogSnapshot(log);
544
- markSessionFinished(log);
545
- emitLogUpdate(log);
546
- }
547
-
548
- /** Persist the terminal cache/session state and refresh both JSON and SQLite index summaries. */
549
- export async function persistFinalizedLogUpdate(log: CapturedLog): Promise<void> {
550
- finalizeLogUpdate(log);
551
- await updateLogIndexEntryMetadata(log);
552
- }
553
-
554
- async function getArchivedLogByIdForStore(id: number): Promise<CapturedLog | null> {
555
- await flushSessionArchiveWrites();
556
- const archivedLog = await findArchivedLogById(id);
557
- if (archivedLog === null) return null;
558
- const normalized = normalizeLog(archivedLog);
559
- addToCache(normalized);
560
- observeSessionLog(normalized);
561
- return normalized;
562
- }
563
-
564
- /**
565
- * Get a log by ID, checking memory cache first then disk.
566
- * Uses byte-offset index for efficient single-line reads.
567
- */
568
- export async function getLogById(id: number): Promise<CapturedLog | null> {
569
- // Check memory cache first
570
- const cached = memoryCache.get(id);
571
- if (cached !== undefined) {
572
- return cached;
573
- }
574
-
575
- // Look up in index and load from disk
576
- const entry = await findInIndex(id);
577
- if (entry === null) {
578
- return await getArchivedLogByIdForStore(id);
579
- }
580
-
581
- // Load from disk using byte-offset index
582
- try {
583
- const filePath = join(resolveLogDir(), entry.file);
584
- if (!existsSync(filePath)) {
585
- return await getArchivedLogByIdForStore(id);
586
- }
587
-
588
- // Use byte offset for direct read when available
589
- if (entry.byteOffset >= 0 && entry.byteLength > 0) {
590
- const fh = await open(filePath, "r");
591
- try {
592
- const buffer = Buffer.alloc(entry.byteLength);
593
- const { bytesRead } = await fh.read(buffer, 0, entry.byteLength, entry.byteOffset);
594
- if (bytesRead === 0) return null;
595
- const line = buffer.toString("utf-8", 0, bytesRead).trimEnd();
596
- if (line === "") return null;
597
- const parsed: unknown = JSON.parse(line);
598
- const result = CapturedLogSchema.safeParse(parsed);
599
- if (result.success) {
600
- const log = normalizeLog(result.data);
601
- addToCache(log);
602
- observeSessionLog(log);
603
- return log;
604
- }
605
- } finally {
606
- await fh.close();
607
- }
608
- return null;
609
- }
610
-
611
- const lastMatch = await scanLogFileForId(filePath, id);
612
- if (lastMatch !== null) {
613
- addToCache(lastMatch);
614
- observeSessionLog(lastMatch);
615
- return lastMatch;
616
- }
617
- } catch (err) {
618
- logger.error("[store] Failed to read log from disk:", String(err));
619
- }
620
-
621
- return await getArchivedLogByIdForStore(id);
622
- }
623
-
624
- async function scanLogFileForId(filePath: string, id: number): Promise<CapturedLog | null> {
625
- let lastMatch: CapturedLog | null = null;
626
- const fileStream = createInterface({
627
- input: createReadStream(filePath),
628
- crlfDelay: Infinity,
629
- });
630
-
631
- let lineNumber = 0;
632
- for await (const line of fileStream) {
633
- lineNumber += 1;
634
- if (line.trim() === "") continue;
635
- let parsed: unknown;
636
- try {
637
- parsed = JSON.parse(line);
638
- } catch (error) {
639
- return await Promise.reject(
640
- new Error(
641
- `Malformed JSONL record in ${filePath} at line ${String(lineNumber)}: ${String(error)}`,
642
- ),
643
- );
644
- }
645
- const result = CapturedLogSchema.safeParse(parsed);
646
- if (!result.success) {
647
- return await Promise.reject(
648
- new Error(`Invalid captured-log record in ${filePath} at line ${String(lineNumber)}`),
649
- );
650
- }
651
- if (result.data.id === id) {
652
- lastMatch = normalizeLog(result.data);
653
- if (result.data.responseStatus !== null) {
654
- return lastMatch;
655
- }
656
- }
657
- }
658
-
659
- return lastMatch;
660
- }
661
-
662
- export function getFilteredLogs(
663
- sessionId?: string,
664
- model?: string,
665
- clientPid?: number,
666
- ): CapturedLog[] {
667
- // Cache maintains insertion order (sorted by ID since logs are added in ID order)
668
- // Use spread instead of Array.from for slightly better performance
669
- return [...memoryCache.values()].filter((l) => {
670
- return matchesLogFilters(l, sessionId, model, clientPid);
671
- });
672
- }
673
-
674
- function matchesLogFilters(
675
- log: CapturedLog,
676
- sessionId?: string,
677
- model?: string,
678
- clientPid?: number,
679
- ): boolean {
680
- if (sessionId !== undefined && getLogSessionId(log) !== sessionId) return false;
681
- if (model !== undefined && log.model !== model) return false;
682
- if (clientPid !== undefined && log.clientPid !== clientPid) return false;
683
- return true;
684
- }
685
-
686
- type PersistedLogVisitor = (log: CapturedLog) => void;
687
-
688
- async function visitPersistedLogs(visitor: PersistedLogVisitor): Promise<void> {
689
- const logDir = resolveLogDir();
690
- if (!existsSync(logDir)) return;
691
-
692
- const recovery = await recoverCapturedLogStorage(logDir);
693
- if (recovery.level === "unavailable") {
694
- return await Promise.reject(
695
- new Error(recovery.diagnostic ?? "Captured-log recovery blocked persisted-log reads"),
696
- );
697
- }
698
-
699
- try {
700
- const entries = await readdirCallback(logDir);
701
- const files = entries.filter((file) => file.endsWith(".jsonl")).sort();
702
- for (const file of files) {
703
- await visitPersistedLogFile(join(logDir, file), visitor);
704
- }
705
- } catch (err) {
706
- logger.error("[store] Failed to list persisted logs:", String(err));
707
- return await Promise.reject(
708
- err instanceof Error ? err : new Error(`Failed to list persisted logs: ${String(err)}`),
709
- );
710
- }
711
- }
712
-
713
- async function visitPersistedLogFile(
714
- filePath: string,
715
- visitor: PersistedLogVisitor,
716
- ): Promise<void> {
717
- if (!existsSync(filePath)) return;
718
-
719
- try {
720
- const fileStream = createInterface({
721
- input: createReadStream(filePath),
722
- crlfDelay: Infinity,
723
- });
724
-
725
- let lineNumber = 0;
726
- for await (const line of fileStream) {
727
- lineNumber += 1;
728
- if (line.trim() === "") continue;
729
- let parsed: unknown;
730
- try {
731
- parsed = JSON.parse(line);
732
- } catch (error) {
733
- return await Promise.reject(
734
- new Error(
735
- `Malformed JSONL record in ${filePath} at line ${String(lineNumber)}: ${String(error)}`,
736
- ),
737
- );
738
- }
739
- const result = CapturedLogSchema.safeParse(parsed);
740
- if (!result.success) {
741
- return await Promise.reject(
742
- new Error(`Invalid captured-log record in ${filePath} at line ${String(lineNumber)}`),
743
- );
744
- }
745
- const log = normalizeLog(result.data);
746
- visitor(log);
747
- }
748
- } catch (err) {
749
- logger.error("[store] Failed to read persisted log file:", filePath, String(err));
750
- return await Promise.reject(
751
- err instanceof Error
752
- ? err
753
- : new Error(`Failed to read persisted log file ${filePath}: ${String(err)}`),
754
- );
755
- }
756
- }
757
-
758
- async function visitArchivedSessionLogs(
759
- sessionId: string | undefined,
760
- visitor: PersistedLogVisitor,
761
- ): Promise<void> {
762
- if (sessionId === undefined) return;
763
- await flushSessionArchiveWrites();
764
- const logs = await listArchivedSessionLogs(sessionId);
765
- for (const archivedLog of logs) {
766
- const log = normalizeLog(archivedLog);
767
- observeSessionLog(log);
768
- visitor(log);
769
- }
770
- }
771
-
772
- function canUseIndexedListPage(options: ListLogsPageOptions): boolean {
773
- return (
774
- options.sessionId === undefined &&
775
- options.model === undefined &&
776
- options.clientPid === undefined
777
- );
778
- }
779
-
780
- function hasUsableIndexOffset(entry: LogIndexEntry): boolean {
781
- return entry.byteOffset >= 0 && entry.byteLength > 0;
782
- }
783
-
784
- async function listLogsPageFromIndex(
785
- options: ListLogsPageOptions,
786
- allowRebuild: boolean,
787
- ): Promise<ListLogsPageResult | null> {
788
- if (!canUseIndexedListPage(options)) return null;
789
-
790
- let entries = await listIndexEntries();
791
- if (entries.length === 0 && allowRebuild) {
792
- await rebuildIndex();
793
- entries = await listIndexEntries();
794
- }
795
-
796
- const pageStart = options.offset;
797
- const pageEnd = options.offset + options.limit;
798
- const pageEntries = entries.slice(pageStart, pageEnd);
799
- const hasIncompleteOffset = pageEntries.some((entry) => !hasUsableIndexOffset(entry));
800
-
801
- if (hasIncompleteOffset) {
802
- if (!allowRebuild) return null;
803
- await rebuildIndex();
804
- return await listLogsPageFromIndex(options, false);
805
- }
806
-
807
- const logs: CapturedLog[] = [];
808
- const includeBodies = options.includeBodies !== false;
809
- for (const entry of pageEntries) {
810
- const log = await getLogById(entry.id);
811
- if (log === null) return null;
812
- logs.push(prepareLogForList(log, includeBodies));
813
- }
814
-
815
- return {
816
- logs,
817
- total: entries.length,
818
- offset: options.offset,
819
- limit: options.limit,
820
- };
821
- }
822
-
823
- export async function listLogsPage(options: ListLogsPageOptions): Promise<ListLogsPageResult> {
824
- const indexedPage = await listLogsPageFromIndex(options, true);
825
- if (indexedPage !== null) return indexedPage;
826
-
827
- const seenIds = new Set<number>();
828
- const pageById = new Map<number, CapturedLog>();
829
- const pageStart = options.offset;
830
- const pageEnd = options.offset + options.limit;
831
- const includeBodies = options.includeBodies !== false;
832
- let total = 0;
833
-
834
- const visitLog = (log: CapturedLog): void => {
835
- if (!matchesLogFilters(log, options.sessionId, options.model, options.clientPid)) return;
836
-
837
- if (!seenIds.has(log.id)) {
838
- seenIds.add(log.id);
839
- const position = total;
840
- total += 1;
841
- if (position >= pageStart && position < pageEnd) {
842
- pageById.set(log.id, prepareLogForList(log, includeBodies));
843
- }
844
- return;
845
- }
846
-
847
- if (pageById.has(log.id)) {
848
- pageById.set(log.id, prepareLogForList(log, includeBodies));
849
- }
850
- };
851
-
852
- await visitPersistedLogs((log) => {
853
- observeSessionLog(log);
854
- visitLog(log);
855
- });
856
-
857
- await visitArchivedSessionLogs(options.sessionId, visitLog);
858
-
859
- for (const log of memoryCache.values()) {
860
- visitLog(log);
861
- }
862
-
863
- const logs = [...pageById.values()].sort((left, right) => left.id - right.id);
864
-
865
- return {
866
- logs,
867
- total,
868
- offset: options.offset,
869
- limit: options.limit,
870
- };
871
- }
872
-
873
- function lowerBoundLogId(ids: readonly number[], target: number): number {
874
- let low = 0;
875
- let high = ids.length;
876
- while (low < high) {
877
- const middle = Math.floor((low + high) / 2);
878
- const id = ids[middle];
879
- if (id === undefined || id >= target) {
880
- high = middle;
881
- continue;
882
- }
883
- low = middle + 1;
884
- }
885
- return low;
886
- }
887
-
888
- function upperBoundLogId(ids: readonly number[], target: number): number {
889
- let low = 0;
890
- let high = ids.length;
891
- while (low < high) {
892
- const middle = Math.floor((low + high) / 2);
893
- const id = ids[middle];
894
- if (id === undefined || id > target) {
895
- high = middle;
896
- continue;
897
- }
898
- low = middle + 1;
899
- }
900
- return low;
901
- }
902
-
903
- function selectCursorPageLogs(
904
- ids: readonly number[],
905
- options: ListLogsCursorPageOptions,
906
- ): number[] {
907
- const windowStart =
908
- options.afterLogId === undefined ? 0 : upperBoundLogId(ids, options.afterLogId);
909
- const windowEnd =
910
- options.beforeLogId === undefined ? ids.length : lowerBoundLogId(ids, options.beforeLogId);
911
- if (windowStart >= windowEnd) return [];
912
-
913
- if (options.afterLogId !== undefined || options.anchor === "oldest") {
914
- return ids.slice(windowStart, Math.min(windowEnd, windowStart + options.limit));
915
- }
916
- return ids.slice(Math.max(windowStart, windowEnd - options.limit), windowEnd);
917
- }
918
-
919
- function rememberLogCursorPageIndex(index: LogCursorPageIndex): LogCursorPageIndex {
920
- if (logCursorPageIndexes.has(index.key)) {
921
- logCursorPageIndexes.delete(index.key);
922
- }
923
- logCursorPageIndexes.set(index.key, index);
924
- while (logCursorPageIndexes.size > MAX_CURSOR_PAGE_INDEXES) {
925
- const oldestKey = logCursorPageIndexes.keys().next().value;
926
- if (oldestKey === undefined) break;
927
- logCursorPageIndexes.delete(oldestKey);
928
- }
929
- return index;
930
- }
931
-
932
- function hasNullableStringField(entry: LogIndexEntry, field: "sessionId" | "model"): boolean {
933
- const desc = Object.getOwnPropertyDescriptor(entry, field);
934
- if (desc === undefined) return false;
935
- return typeof desc.value === "string" || desc.value === null;
936
- }
937
-
938
- function hasLogIndexFilterMetadata(entry: LogIndexEntry): boolean {
939
- return hasNullableStringField(entry, "sessionId") && hasNullableStringField(entry, "model");
940
- }
941
-
942
- function hasLogIndexSummary(entry: LogIndexEntry): boolean {
943
- return entry.summary !== undefined;
944
- }
945
-
946
- function matchesLogIndexEntryFilters(
947
- entry: LogIndexEntry,
948
- sessionId: string | undefined,
949
- model: string | undefined,
950
- clientPid: number | undefined,
951
- ): boolean {
952
- if (sessionId !== undefined && entry.sessionId !== sessionId) return false;
953
- if (model !== undefined && entry.model !== model) return false;
954
- if (clientPid !== undefined && entry.summary?.clientPid !== clientPid) return false;
955
- return true;
956
- }
957
-
958
- async function buildLogCursorPageIndexFromLogIndex(
959
- options: Pick<ListLogsCursorPageOptions, "sessionId" | "model" | "clientPid">,
960
- allowRebuild: boolean,
961
- ): Promise<LogCursorPageIndex | null> {
962
- let entries = await listSqliteLogCursorEntries(options);
963
- if (entries === null) {
964
- if (options.clientPid !== undefined) return null;
965
- entries = await listFilteredIndexEntries(options);
966
- }
967
- if (entries.length === 0 && allowRebuild) {
968
- await rebuildIndex();
969
- entries = await listSqliteLogCursorEntries(options);
970
- if (entries === null) {
971
- if (options.clientPid !== undefined) return null;
972
- entries = await listFilteredIndexEntries(options);
973
- }
974
- }
975
-
976
- if (entries.some((entry) => !hasLogIndexFilterMetadata(entry))) {
977
- if (!allowRebuild) return null;
978
- await rebuildIndex();
979
- return await buildLogCursorPageIndexFromLogIndex(options, false);
980
- }
981
-
982
- const matchingEntries = entries.filter((entry) =>
983
- matchesLogIndexEntryFilters(entry, options.sessionId, options.model, options.clientPid),
984
- );
985
-
986
- if (matchingEntries.some((entry) => !hasLogIndexSummary(entry))) {
987
- if (!allowRebuild) return null;
988
- await rebuildIndex();
989
- return await buildLogCursorPageIndexFromLogIndex(options, false);
990
- }
991
-
992
- if (matchingEntries.some((entry) => !hasUsableIndexOffset(entry) && !memoryCache.has(entry.id))) {
993
- if (!allowRebuild) return null;
994
- await rebuildIndex();
995
- return await buildLogCursorPageIndexFromLogIndex(options, false);
996
- }
997
-
998
- const idsById = new Set<number>();
999
- const entriesById = new Map<number, LogIndexEntry>();
1000
- for (const entry of matchingEntries) {
1001
- idsById.add(entry.id);
1002
- entriesById.set(entry.id, entry);
1003
- }
1004
-
1005
- const logsById = new Map<number, CapturedLog>();
1006
- for (const log of memoryCache.values()) {
1007
- if (!matchesLogFilters(log, options.sessionId, options.model, options.clientPid)) continue;
1008
- idsById.add(log.id);
1009
- logsById.set(log.id, log);
1010
- }
1011
-
1012
- if (options.sessionId !== undefined) {
1013
- await flushSessionArchiveWrites();
1014
- const archivedLogs = await listArchivedSessionLogs(options.sessionId);
1015
- for (const archivedLog of archivedLogs) {
1016
- const log = normalizeLog(archivedLog);
1017
- if (!matchesLogFilters(log, options.sessionId, options.model, options.clientPid)) continue;
1018
- observeSessionLog(log);
1019
- idsById.add(log.id);
1020
- logsById.set(log.id, log);
1021
- }
1022
- }
1023
-
1024
- return {
1025
- key: cursorPageIndexKey(options),
1026
- sessionId: options.sessionId,
1027
- model: options.model,
1028
- clientPid: options.clientPid,
1029
- ids: [...idsById].sort((left, right) => left - right),
1030
- logsById,
1031
- entriesById,
1032
- };
1033
- }
1034
-
1035
- async function buildLogCursorPageIndex(
1036
- options: Pick<ListLogsCursorPageOptions, "sessionId" | "model" | "clientPid">,
1037
- ): Promise<LogCursorPageIndex> {
1038
- logCursorPageIndexBuildCount += 1;
1039
- const indexed = await buildLogCursorPageIndexFromLogIndex(options, true);
1040
- if (indexed !== null) return indexed;
1041
-
1042
- const key = cursorPageIndexKey(options);
1043
- const logsById = new Map<number, CapturedLog>();
1044
-
1045
- const visitLog = (log: CapturedLog): void => {
1046
- if (!matchesLogFilters(log, options.sessionId, options.model, options.clientPid)) return;
1047
- logsById.set(log.id, log);
1048
- };
1049
-
1050
- await visitPersistedLogs((log) => {
1051
- observeSessionLog(log);
1052
- visitLog(log);
1053
- });
1054
-
1055
- await visitArchivedSessionLogs(options.sessionId, visitLog);
1056
-
1057
- for (const log of memoryCache.values()) {
1058
- visitLog(log);
1059
- }
1060
-
1061
- return {
1062
- key,
1063
- sessionId: options.sessionId,
1064
- model: options.model,
1065
- clientPid: options.clientPid,
1066
- ids: [...logsById.keys()].sort((left, right) => left - right),
1067
- logsById,
1068
- entriesById: new Map<number, LogIndexEntry>(),
1069
- };
1070
- }
1071
-
1072
- async function getLogCursorPageIndex(
1073
- options: Pick<ListLogsCursorPageOptions, "sessionId" | "model" | "clientPid">,
1074
- ): Promise<LogCursorPageIndex> {
1075
- const key = cursorPageIndexKey(options);
1076
- const cached = logCursorPageIndexes.get(key);
1077
- if (cached !== undefined) {
1078
- logCursorPageIndexes.delete(key);
1079
- logCursorPageIndexes.set(key, cached);
1080
- return cached;
1081
- }
1082
- return rememberLogCursorPageIndex(await buildLogCursorPageIndex(options));
1083
- }
1084
-
1085
- async function hydrateCursorPageLogs(
1086
- index: LogCursorPageIndex,
1087
- pageIds: readonly number[],
1088
- ): Promise<CapturedLog[]> {
1089
- const logs: CapturedLog[] = [];
1090
- for (const id of pageIds) {
1091
- const cached = index.logsById.get(id);
1092
- if (cached !== undefined) {
1093
- logs.push(cached);
1094
- continue;
1095
- }
1096
-
1097
- const log = await getLogById(id);
1098
- if (log === null) continue;
1099
- index.logsById.set(id, log);
1100
- logs.push(log);
1101
- }
1102
- return logs;
1103
- }
1104
-
1105
- async function hydrateCompactCursorPageLogs(
1106
- index: LogCursorPageIndex,
1107
- pageIds: readonly number[],
1108
- ): Promise<CapturedLog[]> {
1109
- const logs: CapturedLog[] = [];
1110
- for (const id of pageIds) {
1111
- const cached = index.logsById.get(id);
1112
- if (cached !== undefined) {
1113
- logs.push(compactLogForList(cached));
1114
- continue;
1115
- }
1116
-
1117
- const entry = index.entriesById.get(id);
1118
- if (entry !== undefined && entry.summary !== undefined) {
1119
- logs.push(compactLogFromIndexSummary(entry.summary));
1120
- continue;
1121
- }
1122
-
1123
- const log = await getLogById(id);
1124
- if (log === null) continue;
1125
- index.logsById.set(id, log);
1126
- logs.push(compactLogForList(log));
1127
- }
1128
- return logs;
1129
- }
1130
-
1131
- export async function listLogsCursorPage(
1132
- options: ListLogsCursorPageOptions,
1133
- ): Promise<ListLogsCursorPageResult> {
1134
- const includeBodies = options.includeBodies !== false;
1135
- const index = await getLogCursorPageIndex(options);
1136
- const pageIds = selectCursorPageLogs(index.ids, options);
1137
- const pageLogs = includeBodies
1138
- ? await hydrateCursorPageLogs(index, pageIds)
1139
- : await hydrateCompactCursorPageLogs(index, pageIds);
1140
- const oldestLogId = pageIds[0] ?? null;
1141
- const newestLogId = pageIds.at(-1) ?? null;
1142
- const oldestIndex = oldestLogId === null ? -1 : index.ids.indexOf(oldestLogId);
1143
- const newestIndex = newestLogId === null ? -1 : index.ids.indexOf(newestLogId);
1144
-
1145
- return {
1146
- logs: pageLogs.map((log) => prepareLogForList(log, includeBodies)),
1147
- total: index.ids.length,
1148
- limit: options.limit,
1149
- hasOlder: oldestIndex > 0,
1150
- hasNewer: newestIndex >= 0 && newestIndex < index.ids.length - 1,
1151
- oldestLogId,
1152
- newestLogId,
1153
- };
1154
- }
1155
-
1156
- function findSessionSnapshot(sessionId: string): SessionSnapshot | null {
1157
- for (const snapshot of getSessionSnapshots()) {
1158
- if (snapshot.id === sessionId) return snapshot;
1159
- }
1160
- return null;
1161
- }
1162
-
1163
- async function collectSessionLogSummaries(
1164
- sessionId: string,
1165
- includeHistory: boolean,
1166
- ): Promise<SessionLogSummary[]> {
1167
- const summariesById = new Map<number, SessionLogSummary>();
1168
- const observeMatchingLog = (log: CapturedLog): void => {
1169
- if (!matchesLogFilters(log, sessionId, undefined)) return;
1170
- summariesById.set(log.id, buildSessionLogSummary(log));
1171
- };
1172
-
1173
- const indexedSummaries = await collectIndexedSessionLogSummaries(sessionId);
1174
- for (const summary of indexedSummaries) {
1175
- summariesById.set(summary.id, summary);
1176
- }
1177
-
1178
- await flushSessionArchiveWrites();
1179
- const archivedLogs = await listArchivedSessionLogs(sessionId);
1180
- for (const archivedLog of archivedLogs) {
1181
- const log = normalizeLog(archivedLog);
1182
- observeSessionLog(log);
1183
- if (!summariesById.has(log.id)) {
1184
- summariesById.set(log.id, buildSessionLogSummary(log));
1185
- }
1186
- }
1187
-
1188
- if (includeHistory && summariesById.size === 0) {
1189
- await visitPersistedLogs((log) => {
1190
- observeSessionLog(log);
1191
- observeMatchingLog(log);
1192
- });
1193
- }
1194
-
1195
- for (const log of memoryCache.values()) {
1196
- observeMatchingLog(log);
1197
- }
1198
-
1199
- return [...summariesById.values()];
1200
- }
1201
-
1202
- async function collectIndexedSessionLogSummaries(sessionId: string): Promise<SessionLogSummary[]> {
1203
- let entries = await listFilteredIndexEntries({ sessionId });
1204
- if (entries.length === 0) return [];
1205
-
1206
- const needsMetadataRebuild = entries.some((entry) => !hasLogIndexFilterMetadata(entry));
1207
- const needsSummaryRebuild = entries.some((entry) => {
1208
- if (!matchesLogIndexEntryFilters(entry, sessionId, undefined, undefined)) return false;
1209
- return !hasLogIndexSummary(entry);
1210
- });
1211
-
1212
- if (needsMetadataRebuild || needsSummaryRebuild) {
1213
- await rebuildIndex();
1214
- entries = await listFilteredIndexEntries({ sessionId });
1215
- }
1216
-
1217
- const summaries: SessionLogSummary[] = [];
1218
- for (const entry of entries) {
1219
- if (!matchesLogIndexEntryFilters(entry, sessionId, undefined, undefined)) continue;
1220
- if (entry.summary === undefined) continue;
1221
- summaries.push(buildSessionLogSummary(compactLogFromIndexSummary(entry.summary)));
1222
- }
1223
- return summaries;
1224
- }
1225
-
1226
- async function includeSnapshotLastLogSummary(
1227
- sessionId: string,
1228
- snapshot: SessionSnapshot | null,
1229
- summaries: SessionLogSummary[],
1230
- ): Promise<SessionLogSummary[]> {
1231
- if (snapshot === null || snapshot.lastLogId === null) return summaries;
1232
- for (const summary of summaries) {
1233
- if (summary.id === snapshot.lastLogId) return summaries;
1234
- }
1235
-
1236
- const log = await getLogById(snapshot.lastLogId);
1237
- if (log === null || !matchesLogFilters(log, sessionId, undefined)) return summaries;
1238
- return [...summaries, buildSessionLogSummary(log)];
1239
- }
1240
-
1241
- export async function getSessionInfo(
1242
- sessionId: string,
1243
- options: GetSessionInfoOptions,
1244
- ): Promise<SessionInfo | null> {
1245
- const includeHistory = options.includeHistory === true;
1246
- const collectedSummaries = await collectSessionLogSummaries(sessionId, includeHistory);
1247
- const snapshot = findSessionSnapshot(sessionId);
1248
- const summaries = includeHistory
1249
- ? collectedSummaries
1250
- : await includeSnapshotLastLogSummary(sessionId, snapshot, collectedSummaries);
1251
- if (snapshot === null && summaries.length === 0) return null;
1252
- return buildSessionInfo({
1253
- sessionId,
1254
- baseUrl: options.baseUrl,
1255
- snapshot,
1256
- summaries,
1257
- logCoverage: includeHistory ? "history" : "recent",
1258
- latestLogLimit: options.latestLogLimit,
1259
- });
1260
- }
1261
-
1262
- export function getSessions(): string[] {
1263
- return getSessionIds();
1264
- }
1265
-
1266
- export async function listSessions(): Promise<string[]> {
1267
- await flushSessionArchiveWrites();
1268
- const sessionIds = new Set<string>(getSessionIds());
1269
- for (const sessionId of await listArchivedSessionIds()) {
1270
- sessionIds.add(sessionId);
1271
- }
1272
- return [...sessionIds].sort();
1273
- }
1274
-
1275
- export function getModels(): string[] {
1276
- const set = new Set<string>();
1277
- for (const l of memoryCache.values()) {
1278
- if (l.model !== null && l.model !== "") set.add(l.model);
1279
- }
1280
- return [...set];
1281
- }
1282
-
1283
- export function getReplayLogsForSource(sourceLogId: number): CapturedLog[] {
1284
- return [...memoryCache.values()].filter((log) => log.replayOfLogId === sourceLogId);
1285
- }
1286
-
1287
- function collectDirectoryStats(
1288
- dir: string,
1289
- shouldCount: (fileName: string) => boolean,
1290
- ): { fileCount: number; bytes: number } {
1291
- if (!existsSync(dir)) return { fileCount: 0, bytes: 0 };
1292
- let fileCount = 0;
1293
- let bytes = 0;
1294
- const visit = (currentDir: string, depth: number): void => {
1295
- if (depth > 8) return;
1296
- let entries: string[];
1297
- try {
1298
- entries = readdirSync(currentDir);
1299
- } catch {
1300
- return;
1301
- }
1302
- for (const entry of entries) {
1303
- const path = join(currentDir, entry);
1304
- try {
1305
- const stats = statSync(path);
1306
- if (stats.isDirectory()) {
1307
- visit(path, depth + 1);
1308
- } else if (shouldCount(entry)) {
1309
- fileCount += 1;
1310
- bytes += stats.size;
1311
- }
1312
- } catch {
1313
- // Ignore files that disappear while stats are being collected.
1314
- }
1315
- }
1316
- };
1317
- visit(dir, 0);
1318
- return { fileCount, bytes };
1319
- }
1320
-
1321
- export function getLogStorageStats(): LogStorageStats {
1322
- const logDir = resolveLogDir();
1323
- const chunkDir = getChunksDir();
1324
- const sessionArchiveDir = getSessionArchiveRoot();
1325
- const logStats = collectDirectoryStats(logDir, (fileName) => fileName.endsWith(".jsonl"));
1326
- const chunkStats = collectDirectoryStats(chunkDir, (fileName) => fileName.endsWith(".json"));
1327
- const sessionArchiveStats = collectDirectoryStats(
1328
- sessionArchiveDir,
1329
- (fileName) => fileName === "session.sqlite" || fileName.startsWith("session.sqlite-"),
1330
- );
1331
- return {
1332
- memoryCount: memoryCache.size,
1333
- logDir,
1334
- logFileCount: logStats.fileCount,
1335
- logBytes: logStats.bytes,
1336
- chunkDir,
1337
- chunkFileCount: chunkStats.fileCount,
1338
- chunkBytes: chunkStats.bytes,
1339
- sessionArchiveDir,
1340
- sessionArchiveFileCount: sessionArchiveStats.fileCount,
1341
- sessionArchiveBytes: sessionArchiveStats.bytes,
1342
- };
1343
- }
1344
-
1345
- /**
1346
- * Load recent completed log entries from all log files into the memory cache.
1347
- * Called on server startup so the frontend shows existing logs.
1348
- *
1349
- * Strategy: stream files forward, keep the LAST completed entry per ID,
1350
- * then only retain the newest MAX_MEMORY_CACHE entries in cache.
1351
- */
1352
- export async function loadLogsIntoMemory(): Promise<void> {
1353
- try {
1354
- const gcResult = runStorageLifecycleGc();
1355
- if (gcResult.filesDeleted > 0 || gcResult.errors.length > 0) {
1356
- logger.info(
1357
- "[store] Storage lifecycle GC completed:",
1358
- `${gcResult.filesDeleted} files deleted, ${gcResult.bytesDeleted} bytes reclaimed,`,
1359
- `${gcResult.errors.length} errors`,
1360
- );
1361
- }
1362
- } catch (err) {
1363
- logger.warn("[store] Storage lifecycle GC failed:", String(err));
1364
- }
1365
- installStorageLifecycleMaintenance();
1366
-
1367
- const logDir = resolveLogDir();
1368
- if (!existsSync(logDir)) return;
1369
-
1370
- const recentCompletedById = new Map<number, CapturedLog>();
1371
- await visitPersistedLogs((log) => {
1372
- rememberRecentCompletedLog(log, recentCompletedById);
1373
- });
1374
-
1375
- // Sort by id descending, keep only MAX_MEMORY_CACHE newest completed entries
1376
- const sorted = [...recentCompletedById.values()].sort((a, b) => b.id - a.id);
1377
- for (const entry of sorted) {
1378
- addToCache(entry);
1379
- }
1380
-
1381
- rebuildSessionRegistry(memoryCache.values());
1382
- clearLogCursorPageIndexes();
1383
- }
1384
-
1385
- function rememberRecentCompletedLog(
1386
- log: CapturedLog,
1387
- recentCompletedById: Map<number, CapturedLog>,
1388
- ): void {
1389
- if (log.responseStatus === null) return;
1390
- recentCompletedById.set(log.id, log);
1391
- while (recentCompletedById.size > MAX_MEMORY_CACHE) {
1392
- const oldestId = findSmallestLogId(recentCompletedById);
1393
- if (oldestId === null) return;
1394
- recentCompletedById.delete(oldestId);
1395
- }
1396
- }
1397
-
1398
- function findSmallestLogId(logsById: ReadonlyMap<number, CapturedLog>): number | null {
1399
- let smallestId: number | null = null;
1400
- for (const id of logsById.keys()) {
1401
- if (smallestId === null || id < smallestId) {
1402
- smallestId = id;
1403
- }
1404
- }
1405
- return smallestId;
1406
- }
1407
-
1408
- export function clearAllLogs(): { cleared: number } {
1409
- const count = memoryCache.size;
1410
- memoryCache.clear();
1411
- clearLogCursorPageIndexes();
1412
- clearSessionRegistry();
1413
- closeSqliteLogIndex();
1414
- return { cleared: count };
1415
- }
1416
-
1417
- async function deleteMatchingFiles(
1418
- dir: string,
1419
- shouldDelete: (fileName: string) => boolean,
1420
- ): Promise<number> {
1421
- if (!existsSync(dir)) return 0;
1422
-
1423
- let deleted = 0;
1424
- let entries: string[];
1425
- try {
1426
- entries = await readdirCallback(dir);
1427
- } catch (err) {
1428
- logger.warn("[store] Failed to list log storage directory:", String(err));
1429
- return deleted;
1430
- }
1431
-
1432
- for (const entry of entries) {
1433
- if (!shouldDelete(entry)) continue;
1434
- try {
1435
- await unlink(join(dir, entry));
1436
- deleted += 1;
1437
- } catch (err) {
1438
- logger.warn("[store] Failed to delete log storage file:", entry, String(err));
1439
- }
1440
- }
1441
- return deleted;
1442
- }
1443
-
1444
- export async function clearPersistedLogStorage(): Promise<ClearPersistedLogStorageResult> {
1445
- return await runWithFlushedLogWriteLock(async () => {
1446
- await flushIndex();
1447
- await flushSessionArchiveWrites();
1448
-
1449
- const result = clearAllLogs();
1450
- const [logFilesDeleted, chunkFilesDeleted, sessionArchiveFilesDeleted] = await Promise.all([
1451
- deleteMatchingFiles(
1452
- resolveLogDir(),
1453
- (fileName) =>
1454
- fileName.endsWith(".jsonl") ||
1455
- fileName === "logs.idx" ||
1456
- fileName === "inspector.sqlite" ||
1457
- fileName.startsWith("inspector.sqlite-"),
1458
- ),
1459
- deleteMatchingFiles(
1460
- getChunksDir(),
1461
- (fileName) =>
1462
- fileName.endsWith(".json") || (fileName.startsWith(".") && fileName.endsWith(".tmp")),
1463
- ),
1464
- clearSessionArchives(),
1465
- ]);
1466
-
1467
- await rebuildIndex();
1468
-
1469
- return {
1470
- cleared: result.cleared,
1471
- logFilesDeleted,
1472
- chunkFilesDeleted,
1473
- sessionArchiveFilesDeleted,
1474
- };
1475
- });
1476
- }
1477
-
1478
- function readLogIdFromLine(line: string): number | null {
1479
- if (line.trim() === "") return null;
1480
- try {
1481
- const parsed: unknown = JSON.parse(line);
1482
- if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return null;
1483
- const idDesc = Object.getOwnPropertyDescriptor(parsed, "id");
1484
- if (idDesc === undefined || typeof idDesc.value !== "number") return null;
1485
- return idDesc.value;
1486
- } catch {
1487
- return null;
1488
- }
1489
- }
1490
-
1491
- type RewriteLogFileResult = {
1492
- rewritten: boolean;
1493
- removedIds: Set<number>;
1494
- };
1495
-
1496
- function tempRewritePath(filePath: string): string {
1497
- return `${filePath}.rewrite-${String(process.pid)}-${String(Date.now())}.tmp`;
1498
- }
1499
-
1500
- async function removeTempFile(filePath: string): Promise<void> {
1501
- try {
1502
- await unlink(filePath);
1503
- } catch {
1504
- // Best-effort cleanup for temp rewrite files.
1505
- }
1506
- }
1507
-
1508
- function waitForWriterDrain(writer: WriteStream): Promise<boolean> {
1509
- return new Promise((resolve) => {
1510
- const cleanup = (): void => {
1511
- writer.off("drain", onDrain);
1512
- writer.off("error", onError);
1513
- };
1514
- const onDrain = (): void => {
1515
- cleanup();
1516
- resolve(true);
1517
- };
1518
- const onError = (): void => {
1519
- cleanup();
1520
- resolve(false);
1521
- };
1522
- writer.once("drain", onDrain);
1523
- writer.once("error", onError);
1524
- });
1525
- }
1526
-
1527
- async function writeKeptLogLine(writer: WriteStream, line: string): Promise<boolean> {
1528
- if (writer.write(`${line}\n`, "utf-8")) return true;
1529
- return await waitForWriterDrain(writer);
1530
- }
1531
-
1532
- function finishWriter(writer: WriteStream): Promise<boolean> {
1533
- return new Promise((resolve) => {
1534
- const cleanup = (): void => {
1535
- writer.off("finish", onFinish);
1536
- writer.off("error", onError);
1537
- };
1538
- const onFinish = (): void => {
1539
- cleanup();
1540
- resolve(true);
1541
- };
1542
- const onError = (): void => {
1543
- cleanup();
1544
- resolve(false);
1545
- };
1546
- writer.once("finish", onFinish);
1547
- writer.once("error", onError);
1548
- writer.end();
1549
- });
1550
- }
1551
-
1552
- async function rewriteLogFileWithoutIds(
1553
- filePath: string,
1554
- ids: ReadonlySet<number>,
1555
- ): Promise<RewriteLogFileResult> {
1556
- const tempPath = tempRewritePath(filePath);
1557
- if (!ensurePrivateDirectorySync(dirname(filePath))) {
1558
- return { rewritten: false, removedIds: new Set() };
1559
- }
1560
- const writer = createWriteStream(tempPath, {
1561
- encoding: "utf-8",
1562
- mode: PRIVATE_FILE_MODE,
1563
- });
1564
- const removedIds = new Set<number>();
1565
- let keptLineCount = 0;
1566
- let writeOk = true;
1567
-
1568
- try {
1569
- const fileStream = createInterface({
1570
- input: createReadStream(filePath),
1571
- crlfDelay: Infinity,
1572
- });
1573
-
1574
- for await (const line of fileStream) {
1575
- if (line === "") continue;
1576
- const id = readLogIdFromLine(line);
1577
- if (id !== null && ids.has(id)) {
1578
- removedIds.add(id);
1579
- } else {
1580
- keptLineCount += 1;
1581
- writeOk = await writeKeptLogLine(writer, line);
1582
- if (!writeOk) break;
1583
- }
1584
- }
1585
- } catch (err) {
1586
- logger.warn("[store] Failed to stream log file for deletion:", filePath, String(err));
1587
- writeOk = false;
1588
- }
1589
-
1590
- if (!(await finishWriter(writer)) || !writeOk) {
1591
- await removeTempFile(tempPath);
1592
- return { rewritten: false, removedIds: new Set() };
1593
- }
1594
-
1595
- if (!securePrivateFileSync(tempPath)) {
1596
- await removeTempFile(tempPath);
1597
- return { rewritten: false, removedIds: new Set() };
1598
- }
1599
-
1600
- if (removedIds.size === 0) {
1601
- await removeTempFile(tempPath);
1602
- return { rewritten: false, removedIds };
1603
- }
1604
-
1605
- try {
1606
- if (keptLineCount === 0) {
1607
- await removeTempFile(tempPath);
1608
- await unlink(filePath);
1609
- } else {
1610
- await rename(tempPath, filePath);
1611
- }
1612
- } catch (err) {
1613
- await removeTempFile(tempPath);
1614
- logger.warn("[store] Failed to rewrite log file after deletion:", filePath, String(err));
1615
- return { rewritten: false, removedIds: new Set() };
1616
- }
1617
-
1618
- return { rewritten: true, removedIds };
1619
- }
1620
-
1621
- type RewriteLogFilesResult = {
1622
- logFilesRewritten: number;
1623
- removedIds: Set<number>;
1624
- };
1625
-
1626
- async function rewriteLogFilesWithoutIds(ids: ReadonlySet<number>): Promise<RewriteLogFilesResult> {
1627
- const logDir = resolveLogDir();
1628
- if (!existsSync(logDir)) return { logFilesRewritten: 0, removedIds: new Set() };
1629
-
1630
- let entries: string[];
1631
- try {
1632
- entries = await readdirCallback(logDir);
1633
- } catch (err) {
1634
- logger.warn("[store] Failed to list log directory for deletion:", String(err));
1635
- return { logFilesRewritten: 0, removedIds: new Set() };
1636
- }
1637
-
1638
- let logFilesRewritten = 0;
1639
- const removedIds = new Set<number>();
1640
- for (const entry of entries) {
1641
- if (!entry.endsWith(".jsonl")) continue;
1642
- const result = await rewriteLogFileWithoutIds(join(logDir, entry), ids);
1643
- if (result.rewritten) logFilesRewritten += 1;
1644
- for (const removedId of result.removedIds) {
1645
- removedIds.add(removedId);
1646
- }
1647
- }
1648
- return { logFilesRewritten, removedIds };
1649
- }
1650
-
1651
- async function deleteChunkFilesByIds(ids: ReadonlySet<number>): Promise<number> {
1652
- const chunkDir = getChunksDir();
1653
- if (!existsSync(chunkDir)) return 0;
1654
-
1655
- let deleted = 0;
1656
- for (const id of ids) {
1657
- const candidates = [`${String(id)}.json`, `.${String(id)}.tmp`];
1658
- for (const candidate of candidates) {
1659
- try {
1660
- await unlink(join(chunkDir, candidate));
1661
- deleted += 1;
1662
- } catch {
1663
- // Missing chunk files are expected for non-streaming logs.
1664
- }
1665
- }
1666
- }
1667
- return deleted;
1668
- }
1669
-
1670
- export async function clearPersistedLogsByIds(
1671
- ids: readonly number[],
1672
- ): Promise<ClearPersistedLogsByIdsResult> {
1673
- const uniqueIds = new Set(ids);
1674
- if (uniqueIds.size === 0) {
1675
- return {
1676
- cleared: 0,
1677
- logFilesRewritten: 0,
1678
- chunkFilesDeleted: 0,
1679
- sessionArchiveLogsDeleted: 0,
1680
- };
1681
- }
1682
-
1683
- return await runWithFlushedLogWriteLock(async () => {
1684
- await flushIndex();
1685
- await flushSessionArchiveWrites();
1686
-
1687
- const memoryRemovedIds = new Set<number>();
1688
- for (const id of uniqueIds) {
1689
- if (memoryCache.has(id)) memoryRemovedIds.add(id);
1690
- }
1691
- const result = clearLogsByIds([...uniqueIds]);
1692
- const [rewriteResult, chunkFilesDeleted, archivedRemovedIds] = await Promise.all([
1693
- rewriteLogFilesWithoutIds(uniqueIds),
1694
- deleteChunkFilesByIds(uniqueIds),
1695
- deleteArchivedLogsByIds(uniqueIds),
1696
- ]);
1697
-
1698
- await rebuildIndex();
1699
- clearLogCursorPageIndexes();
1700
-
1701
- const clearedIds = new Set<number>(rewriteResult.removedIds);
1702
- if (result.cleared > 0) {
1703
- for (const id of memoryRemovedIds) {
1704
- clearedIds.add(id);
1705
- }
1706
- }
1707
- for (const id of archivedRemovedIds) {
1708
- clearedIds.add(id);
1709
- }
1710
-
1711
- return {
1712
- cleared: clearedIds.size,
1713
- logFilesRewritten: rewriteResult.logFilesRewritten,
1714
- chunkFilesDeleted,
1715
- sessionArchiveLogsDeleted: archivedRemovedIds.size,
1716
- };
1717
- });
1718
- }
1719
-
1720
- /**
1721
- * Remove a specific set of log IDs from the in-memory cache. Returns the
1722
- * number of logs actually removed. IDs not present in the cache are ignored.
1723
- * This is used by the per-conversation-group "Clear" button in the UI.
1724
- */
1725
- export function clearLogsByIds(ids: readonly number[]): { cleared: number } {
1726
- const unique = new Set<number>();
1727
- for (const id of ids) {
1728
- if (memoryCache.has(id)) unique.add(id);
1729
- }
1730
- for (const id of unique) {
1731
- removeFromCache(id);
1732
- }
1733
- removeLogsFromCursorPageIndexes(unique);
1734
- rebuildSessionRegistry(memoryCache.values());
1735
- return { cleared: unique.size };
1736
- }
1737
-
1738
- export function _getLogCursorPageIndexStatsForTests(): {
1739
- indexCount: number;
1740
- buildCount: number;
1741
- } {
1742
- return {
1743
- indexCount: logCursorPageIndexes.size,
1744
- buildCount: logCursorPageIndexBuildCount,
1745
- };
1746
- }
1747
-
1748
- // SSE event system for real-time log updates
1749
- type LogUpdateHandler = (log: CapturedLog) => void;
1750
- const sseHandlers: Set<LogUpdateHandler> = new Set();
1751
-
1752
- export function onLogUpdate(handler: LogUpdateHandler): () => void {
1753
- sseHandlers.add(handler);
1754
- return () => {
1755
- sseHandlers.delete(handler);
1756
- };
1757
- }
1758
-
1759
- export function emitLogUpdate(log: CapturedLog): void {
1760
- const failedHandlers: LogUpdateHandler[] = [];
1761
- for (const handler of sseHandlers) {
1762
- try {
1763
- handler(log);
1764
- } catch {
1765
- // Collect failed handlers to remove after iteration
1766
- failedHandlers.push(handler);
1767
- }
1768
- }
1769
- // Remove failed handlers after iteration to avoid Set mutation during for...of
1770
- for (const handler of failedHandlers) {
1771
- sseHandlers.delete(handler);
1772
- }
1773
- }