@tonyclaw/agent-inspector 3.0.44 → 3.0.46

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 (347) hide show
  1. package/.output/{nitro.json → backend/nitro.json} +3 -3
  2. package/.output/backend-public/assets/index-D4LX-sw-.js +11 -0
  3. package/.output/cli.js +22707 -17099
  4. package/.output/server/_libs/extend.mjs +4 -8
  5. package/.output/server/_libs/jszip.mjs +9 -35
  6. package/.output/server/_libs/modelcontextprotocol__core.mjs +25 -25
  7. package/.output/server/_libs/modelcontextprotocol__server.mjs +2 -2
  8. package/.output/server/_libs/react-dom.mjs +2 -3
  9. package/.output/server/_libs/react.mjs +1 -27
  10. package/.output/server/_libs/tanstack__react-router.mjs +1 -329
  11. package/.output/server/_libs/tanstack__router-core.mjs +42 -100
  12. package/.output/server/_libs/use-sync-external-store.mjs +1 -64
  13. package/.output/server/_libs/ws.mjs +1 -1
  14. package/.output/server/_libs/zod.mjs +6 -6
  15. package/.output/server/_ssr/index.mjs +5 -4
  16. package/.output/server/_ssr/publicBasePathContext-IH2ilOfu.mjs +176 -0
  17. package/.output/server/_ssr/{router-DWcUFPR3.mjs → router-D18yUq36.mjs} +7877 -5597
  18. package/.output/server/_ssr/start-COQu_AgD.mjs +57 -0
  19. package/.output/server/_tanstack-start-manifest_v-DnbdNeun.mjs +4 -0
  20. package/.output/server/index.mjs +5 -145
  21. package/.output/{public/assets/CompareDrawer-DQHjxFO6.js → ui/assets/CompareDrawer-DAUuIJ6G.js} +1 -1
  22. package/.output/{public/assets/InspectorPet-D1A4505B.js → ui/assets/InspectorPet-BRBVjOWI.js} +1 -1
  23. package/.output/ui/assets/ProxyViewerContainer-D7Sq0ctc.js +59 -0
  24. package/.output/{public/assets/ReplayDialog-C5vmbecv.js → ui/assets/ReplayDialog-nDLsjOhs.js} +1 -1
  25. package/.output/{public/assets/RequestAnatomy-CPGNB8lk.js → ui/assets/RequestAnatomy-zd4BDgL0.js} +1 -1
  26. package/.output/ui/assets/ResponseView-AdIXGcSn.js +2 -0
  27. package/.output/ui/assets/StreamingChunkSequence-CesTz8He.js +1 -0
  28. package/.output/{public/assets/_sessionId-CDhRgzf6.js → ui/assets/_sessionId-BAfzrhSU.js} +1 -1
  29. package/.output/{public/assets/_sessionId-mw3aA8Bp.js → ui/assets/_sessionId-QnBomgPD.js} +1 -1
  30. package/.output/ui/assets/favicon-DYEkHSNl.svg +21 -0
  31. package/.output/{public/assets/index-B2eoeT-Z.js → ui/assets/index-BcsdxBAd.js} +1 -1
  32. package/.output/ui/assets/index-BvXp42al.css +1 -0
  33. package/.output/{public/assets/index-CX7_rf7o.js → ui/assets/index-D0rtCN9V.js} +1 -1
  34. package/.output/ui/assets/index-DHpwr08Z.js +73 -0
  35. package/.output/{public/assets/index-BfRhTvOd.js → ui/assets/index-dNuk2dsU.js} +1 -1
  36. package/.output/ui/assets/json-viewer-BvFHglMb.js +1 -0
  37. package/.output/{public/assets/jszip.min-C9rCFoU3.js → ui/assets/jszip.min-B5Z7gNZB.js} +1 -1
  38. package/.output/ui/index.html +229 -0
  39. package/.output/ui/runtime-config.js +2 -0
  40. package/.output/workers/logFinalizer.worker.js +983 -414
  41. package/.output/workers/sessionWorkerEntry.js +983 -414
  42. package/README.md +363 -35
  43. package/docs/instance-control.openapi.yaml +361 -0
  44. package/package.json +28 -5
  45. package/scripts/generate-theme-scales.mjs +3 -3
  46. package/src/{router.tsx → backend/router.tsx} +3 -15
  47. package/src/backend/routes/$.ts +26 -0
  48. package/src/backend/routes/__root.ts +4 -0
  49. package/src/backend/routes/api/-instances.ts +109 -0
  50. package/src/{routes → backend/routes}/api/alerts.summary.ts +3 -3
  51. package/src/{routes → backend/routes}/api/alerts.ts +4 -4
  52. package/src/{routes → backend/routes}/api/config.paths.ts +1 -1
  53. package/src/{routes → backend/routes}/api/config.ts +11 -2
  54. package/src/{routes → backend/routes}/api/ecosystem.packages.$packageId.help.ts +1 -1
  55. package/src/{routes → backend/routes}/api/ecosystem.packages.$packageId.install.ts +1 -1
  56. package/src/{routes → backend/routes}/api/ecosystem.packages.$packageId.runner-presets.ts +1 -1
  57. package/src/{routes → backend/routes}/api/ecosystem.packages.$packageId.upgrade.ts +1 -1
  58. package/src/{routes → backend/routes}/api/ecosystem.packages.ts +5 -1
  59. package/src/{routes → backend/routes}/api/ecosystem.recipes.$recipeId.run.ts +1 -1
  60. package/src/{routes → backend/routes}/api/ecosystem.tasks.$taskId.ts +1 -1
  61. package/src/{routes → backend/routes}/api/ecosystem.tasks.ts +1 -1
  62. package/src/{routes → backend/routes}/api/groups.$groupId.evidence.ts +2 -2
  63. package/src/{routes → backend/routes}/api/groups.$groupId.sessions.ts +2 -2
  64. package/src/{routes → backend/routes}/api/groups.$groupId.ts +2 -2
  65. package/src/{routes → backend/routes}/api/groups.ts +2 -2
  66. package/src/{routes → backend/routes}/api/health.ts +3 -3
  67. package/src/backend/routes/api/instances.$name.connection.ts +12 -0
  68. package/src/backend/routes/api/instances.$name.restart.ts +23 -0
  69. package/src/backend/routes/api/instances.$name.start.ts +18 -0
  70. package/src/backend/routes/api/instances.$name.stop.ts +23 -0
  71. package/src/backend/routes/api/instances.$name.ts +12 -0
  72. package/src/backend/routes/api/instances.ts +16 -0
  73. package/src/{routes → backend/routes}/api/knowledge.candidates.$candidateId.promote.ts +2 -2
  74. package/src/{routes → backend/routes}/api/knowledge.candidates.$candidateId.ts +2 -2
  75. package/src/{routes → backend/routes}/api/knowledge.candidates.ts +1 -1
  76. package/src/{routes → backend/routes}/api/knowledge.project-context.ts +2 -2
  77. package/src/{routes → backend/routes}/api/knowledge.search.ts +2 -2
  78. package/src/{routes → backend/routes}/api/knowledge.sessions.$sessionId.candidates.ts +3 -3
  79. package/src/{routes → backend/routes}/api/logs.$id.body.ts +6 -6
  80. package/src/{routes → backend/routes}/api/logs.$id.chunks.ts +5 -4
  81. package/src/{routes → backend/routes}/api/logs.$id.replay.ts +12 -11
  82. package/src/{routes → backend/routes}/api/logs.$id.ts +4 -3
  83. package/src/{routes → backend/routes}/api/logs.import.ts +3 -3
  84. package/src/{routes → backend/routes}/api/logs.stream.ts +11 -7
  85. package/src/{routes → backend/routes}/api/logs.ts +17 -9
  86. package/src/{routes → backend/routes}/api/mcp.ts +1 -1
  87. package/src/{routes → backend/routes}/api/models.ts +1 -1
  88. package/src/{routes → backend/routes}/api/pi-agent.ts +2 -2
  89. package/src/{routes → backend/routes}/api/providers.$providerId.model-metadata.ts +5 -5
  90. package/src/{routes → backend/routes}/api/providers.$providerId.test.log.ts +7 -7
  91. package/src/{routes → backend/routes}/api/providers.$providerId.ts +4 -4
  92. package/src/{routes → backend/routes}/api/providers.export.ts +1 -1
  93. package/src/{routes → backend/routes}/api/providers.import.ts +3 -3
  94. package/src/{routes → backend/routes}/api/providers.scan.ts +2 -2
  95. package/src/{routes → backend/routes}/api/providers.ts +9 -4
  96. package/src/{routes → backend/routes}/api/runs.$runId.evidence.ts +2 -2
  97. package/src/{routes → backend/routes}/api/runs.$runId.ts +2 -2
  98. package/src/{routes → backend/routes}/api/runs.ts +4 -4
  99. package/src/backend/routes/api/runtime.ts +46 -0
  100. package/src/{routes → backend/routes}/api/sessions.ts +3 -3
  101. package/src/{routes → backend/routes}/proxy/$.ts +1 -1
  102. package/src/{routes → backend/routes}/readyz.ts +1 -1
  103. package/src/backend/start.ts +10 -0
  104. package/src/cli/alias.ts +431 -0
  105. package/src/cli/doctor.ts +32 -9
  106. package/src/cli/instance.ts +1050 -0
  107. package/src/cli/instanceArgs.ts +285 -0
  108. package/src/cli/instanceControl.ts +181 -0
  109. package/src/cli/instanceModel.ts +267 -0
  110. package/src/cli/instanceRegistry.ts +731 -0
  111. package/src/cli/networkHints.ts +27 -14
  112. package/src/cli/onboard.ts +190 -35
  113. package/src/cli/onboardTarget.ts +228 -0
  114. package/src/cli/templates/codex-skill-onboard.ts +109 -31
  115. package/src/cli/templates/skill-onboard.ts +140 -70
  116. package/src/cli.ts +338 -77
  117. package/src/components/ProxyViewer.tsx +20 -10
  118. package/src/components/ProxyViewerContainer.tsx +12 -11
  119. package/src/components/pi-agent/PiAgentPanel.tsx +2 -2
  120. package/src/components/providers/ProviderCard.tsx +1 -1
  121. package/src/components/providers/ProviderTestResultsView.tsx +1 -1
  122. package/src/components/providers/SettingsDialog.tsx +160 -140
  123. package/src/components/proxy-viewer/ApplicationBar.tsx +2 -1
  124. package/src/components/proxy-viewer/LogEntry.tsx +1 -1
  125. package/src/components/proxy-viewer/log-formats/index.ts +1 -1
  126. package/src/contracts/index.ts +5 -0
  127. package/src/contracts/log.ts +33 -3
  128. package/src/knowledge/types.ts +3 -2
  129. package/src/lib/alertContract.ts +2 -2
  130. package/src/lib/apiClient.ts +30 -7
  131. package/src/lib/apiFormat.ts +29 -0
  132. package/src/lib/basePath.ts +31 -7
  133. package/src/lib/browserRuntimeContract.ts +124 -0
  134. package/src/lib/browserRuntimeResolver.ts +244 -0
  135. package/src/{proxy → lib}/claudeCodeStrip.ts +6 -7
  136. package/src/lib/codingAgentEndpoints.ts +54 -0
  137. package/src/lib/instanceContract.ts +236 -0
  138. package/src/lib/managedInstance.ts +104 -0
  139. package/src/lib/piAgentContract.ts +3 -2
  140. package/src/lib/publicBasePathContext.ts +137 -0
  141. package/src/lib/resourceLimits.ts +2 -0
  142. package/src/lib/runContract.ts +3 -3
  143. package/src/lib/sessionInfoContract.ts +3 -2
  144. package/src/mcp/instanceHandlers.ts +150 -0
  145. package/src/mcp/loopback.ts +16 -5
  146. package/src/mcp/mode.ts +25 -0
  147. package/src/mcp/server.ts +274 -39
  148. package/src/mcp/toolHandlers.ts +2 -2
  149. package/src/proxy/config.ts +25 -30
  150. package/src/proxy/dataDir.ts +43 -7
  151. package/src/proxy/ecosystemTasks.ts +161 -70
  152. package/src/proxy/formats/registry.ts +6 -21
  153. package/src/proxy/handler.ts +129 -29
  154. package/src/proxy/identityProxy.ts +1049 -50
  155. package/src/proxy/jsonlRecovery.ts +554 -0
  156. package/src/proxy/logFinalizer.ts +25 -10
  157. package/src/proxy/logImporter.ts +2 -1
  158. package/src/proxy/logIndex.ts +191 -95
  159. package/src/proxy/logger.ts +77 -14
  160. package/src/proxy/platformCommands.ts +41 -0
  161. package/src/proxy/privateDataPath.ts +310 -88
  162. package/src/proxy/rawStreamCapture.ts +194 -13
  163. package/src/proxy/runtimeAdmission.ts +5 -2
  164. package/src/proxy/runtimeHealth.ts +14 -1
  165. package/src/proxy/runtimeShutdown.ts +84 -2
  166. package/src/proxy/schemas.ts +7 -1
  167. package/src/proxy/sessionArchive.ts +78 -14
  168. package/src/proxy/sessionInfo.ts +5 -4
  169. package/src/proxy/sqliteLogIndex.ts +65 -2
  170. package/src/proxy/store.ts +141 -34
  171. package/src/proxy/uiAssetServer.ts +106 -0
  172. package/src/proxy/upstream.ts +29 -2
  173. package/src/ui/bootstrap.ts +167 -0
  174. package/src/ui/index.html +38 -0
  175. package/src/ui/logsStreamClient.ts +20 -0
  176. package/src/ui/main.tsx +13 -0
  177. package/src/ui/public/runtime-config.js +2 -0
  178. package/src/ui/router.tsx +13 -0
  179. package/src/ui/routes/__root.tsx +69 -0
  180. package/src/{routes → ui/routes}/index.tsx +2 -3
  181. package/src/{routes → ui/routes}/session/$sessionId.tsx +5 -11
  182. package/src/ui/runtimeDiscovery.ts +50 -0
  183. package/styles/globals.css +1 -0
  184. package/.output/public/assets/ProxyViewerContainer-BhpIgfBi.js +0 -126
  185. package/.output/public/assets/ResponseView-DUfrajCm.js +0 -2
  186. package/.output/public/assets/StreamingChunkSequence-nbeCQqOz.js +0 -1
  187. package/.output/public/assets/index-BWjeqxbk.css +0 -1
  188. package/.output/public/assets/index-C6qZwYlR.js +0 -70
  189. package/.output/public/assets/json-viewer-BLmF1sOZ.js +0 -1
  190. package/.output/server/_libs/@radix-ui/react-accessible-icon+[...].mjs +0 -1
  191. package/.output/server/_libs/@radix-ui/react-dismissable-layer+[...].mjs +0 -344
  192. package/.output/server/_libs/@radix-ui/react-navigation-menu+[...].mjs +0 -2
  193. package/.output/server/_libs/@radix-ui/react-one-time-password-field+[...].mjs +0 -2
  194. package/.output/server/_libs/@radix-ui/react-password-toggle-field+[...].mjs +0 -2
  195. package/.output/server/_libs/@radix-ui/react-use-callback-ref+[...].mjs +0 -11
  196. package/.output/server/_libs/@radix-ui/react-use-controllable-state+[...].mjs +0 -69
  197. package/.output/server/_libs/@radix-ui/react-use-effect-event+[...].mjs +0 -1
  198. package/.output/server/_libs/@radix-ui/react-use-is-hydrated+[...].mjs +0 -28
  199. package/.output/server/_libs/@radix-ui/react-use-layout-effect+[...].mjs +0 -6
  200. package/.output/server/_libs/@radix-ui/react-visually-hidden+[...].mjs +0 -34
  201. package/.output/server/_libs/aria-hidden.mjs +0 -122
  202. package/.output/server/_libs/bail.mjs +0 -8
  203. package/.output/server/_libs/character-entities.mjs +0 -2130
  204. package/.output/server/_libs/class-variance-authority.mjs +0 -44
  205. package/.output/server/_libs/clsx.mjs +0 -16
  206. package/.output/server/_libs/comma-separated-tokens.mjs +0 -10
  207. package/.output/server/_libs/decode-named-character-reference+[...].mjs +0 -8
  208. package/.output/server/_libs/dequal.mjs +0 -27
  209. package/.output/server/_libs/detect-node-es.mjs +0 -1
  210. package/.output/server/_libs/devlop.mjs +0 -8
  211. package/.output/server/_libs/diff.mjs +0 -320
  212. package/.output/server/_libs/estree-util-is-identifier-name.mjs +0 -11
  213. package/.output/server/_libs/floating-ui__core.mjs +0 -725
  214. package/.output/server/_libs/floating-ui__dom.mjs +0 -622
  215. package/.output/server/_libs/floating-ui__react-dom.mjs +0 -292
  216. package/.output/server/_libs/floating-ui__utils.mjs +0 -320
  217. package/.output/server/_libs/get-nonce.mjs +0 -9
  218. package/.output/server/_libs/hast-util-to-jsx-runtime.mjs +0 -388
  219. package/.output/server/_libs/hast-util-whitespace.mjs +0 -10
  220. package/.output/server/_libs/html-url-attributes.mjs +0 -26
  221. package/.output/server/_libs/inline-style-parser.mjs +0 -142
  222. package/.output/server/_libs/is-plain-obj.mjs +0 -10
  223. package/.output/server/_libs/lucide-react.mjs +0 -1027
  224. package/.output/server/_libs/mdast-util-from-markdown.mjs +0 -717
  225. package/.output/server/_libs/mdast-util-to-hast.mjs +0 -710
  226. package/.output/server/_libs/mdast-util-to-string.mjs +0 -38
  227. package/.output/server/_libs/micromark-core-commonmark.mjs +0 -2259
  228. package/.output/server/_libs/micromark-factory-destination.mjs +0 -94
  229. package/.output/server/_libs/micromark-factory-label.mjs +0 -63
  230. package/.output/server/_libs/micromark-factory-space.mjs +0 -24
  231. package/.output/server/_libs/micromark-factory-title.mjs +0 -65
  232. package/.output/server/_libs/micromark-factory-whitespace.mjs +0 -22
  233. package/.output/server/_libs/micromark-util-character.mjs +0 -44
  234. package/.output/server/_libs/micromark-util-chunked.mjs +0 -36
  235. package/.output/server/_libs/micromark-util-classify-character+[...].mjs +0 -12
  236. package/.output/server/_libs/micromark-util-combine-extensions+[...].mjs +0 -41
  237. package/.output/server/_libs/micromark-util-decode-numeric-character-reference+[...].mjs +0 -19
  238. package/.output/server/_libs/micromark-util-decode-string.mjs +0 -21
  239. package/.output/server/_libs/micromark-util-encode.mjs +0 -1
  240. package/.output/server/_libs/micromark-util-html-tag-name.mjs +0 -69
  241. package/.output/server/_libs/micromark-util-normalize-identifier+[...].mjs +0 -6
  242. package/.output/server/_libs/micromark-util-resolve-all.mjs +0 -15
  243. package/.output/server/_libs/micromark-util-sanitize-uri.mjs +0 -41
  244. package/.output/server/_libs/micromark-util-subtokenize.mjs +0 -346
  245. package/.output/server/_libs/micromark.mjs +0 -906
  246. package/.output/server/_libs/property-information.mjs +0 -1209
  247. package/.output/server/_libs/radix-ui.mjs +0 -1
  248. package/.output/server/_libs/radix-ui__number.mjs +0 -6
  249. package/.output/server/_libs/radix-ui__primitive.mjs +0 -11
  250. package/.output/server/_libs/radix-ui__react-accordion.mjs +0 -1
  251. package/.output/server/_libs/radix-ui__react-alert-dialog.mjs +0 -1
  252. package/.output/server/_libs/radix-ui__react-arrow.mjs +0 -23
  253. package/.output/server/_libs/radix-ui__react-aspect-ratio.mjs +0 -1
  254. package/.output/server/_libs/radix-ui__react-avatar.mjs +0 -1
  255. package/.output/server/_libs/radix-ui__react-checkbox.mjs +0 -1
  256. package/.output/server/_libs/radix-ui__react-collapsible.mjs +0 -144
  257. package/.output/server/_libs/radix-ui__react-collection.mjs +0 -69
  258. package/.output/server/_libs/radix-ui__react-compose-refs.mjs +0 -38
  259. package/.output/server/_libs/radix-ui__react-context-menu.mjs +0 -1
  260. package/.output/server/_libs/radix-ui__react-context.mjs +0 -64
  261. package/.output/server/_libs/radix-ui__react-dialog.mjs +0 -280
  262. package/.output/server/_libs/radix-ui__react-direction.mjs +0 -9
  263. package/.output/server/_libs/radix-ui__react-dropdown-menu.mjs +0 -1
  264. package/.output/server/_libs/radix-ui__react-focus-guards.mjs +0 -39
  265. package/.output/server/_libs/radix-ui__react-focus-scope.mjs +0 -210
  266. package/.output/server/_libs/radix-ui__react-form.mjs +0 -1
  267. package/.output/server/_libs/radix-ui__react-hover-card.mjs +0 -1
  268. package/.output/server/_libs/radix-ui__react-id.mjs +0 -14
  269. package/.output/server/_libs/radix-ui__react-label.mjs +0 -1
  270. package/.output/server/_libs/radix-ui__react-menu.mjs +0 -1
  271. package/.output/server/_libs/radix-ui__react-menubar.mjs +0 -1
  272. package/.output/server/_libs/radix-ui__react-popover.mjs +0 -1
  273. package/.output/server/_libs/radix-ui__react-popper.mjs +0 -340
  274. package/.output/server/_libs/radix-ui__react-portal.mjs +0 -16
  275. package/.output/server/_libs/radix-ui__react-presence.mjs +0 -173
  276. package/.output/server/_libs/radix-ui__react-primitive.mjs +0 -42
  277. package/.output/server/_libs/radix-ui__react-progress.mjs +0 -1
  278. package/.output/server/_libs/radix-ui__react-radio-group.mjs +0 -1
  279. package/.output/server/_libs/radix-ui__react-roving-focus.mjs +0 -235
  280. package/.output/server/_libs/radix-ui__react-scroll-area.mjs +0 -731
  281. package/.output/server/_libs/radix-ui__react-select.mjs +0 -1207
  282. package/.output/server/_libs/radix-ui__react-separator.mjs +0 -28
  283. package/.output/server/_libs/radix-ui__react-slider.mjs +0 -1
  284. package/.output/server/_libs/radix-ui__react-slot.mjs +0 -132
  285. package/.output/server/_libs/radix-ui__react-switch.mjs +0 -1
  286. package/.output/server/_libs/radix-ui__react-tabs.mjs +0 -194
  287. package/.output/server/_libs/radix-ui__react-toast.mjs +0 -2
  288. package/.output/server/_libs/radix-ui__react-toggle-group.mjs +0 -1
  289. package/.output/server/_libs/radix-ui__react-toggle.mjs +0 -1
  290. package/.output/server/_libs/radix-ui__react-toolbar.mjs +0 -1
  291. package/.output/server/_libs/radix-ui__react-tooltip.mjs +0 -498
  292. package/.output/server/_libs/radix-ui__react-use-previous.mjs +0 -14
  293. package/.output/server/_libs/radix-ui__react-use-size.mjs +0 -39
  294. package/.output/server/_libs/react-markdown.mjs +0 -235
  295. package/.output/server/_libs/react-remove-scroll-bar.mjs +0 -82
  296. package/.output/server/_libs/react-remove-scroll.mjs +0 -328
  297. package/.output/server/_libs/react-style-singleton.mjs +0 -69
  298. package/.output/server/_libs/remark-parse.mjs +0 -19
  299. package/.output/server/_libs/remark-rehype.mjs +0 -21
  300. package/.output/server/_libs/space-separated-tokens.mjs +0 -6
  301. package/.output/server/_libs/style-to-js.mjs +0 -72
  302. package/.output/server/_libs/style-to-object.mjs +0 -38
  303. package/.output/server/_libs/swr.mjs +0 -947
  304. package/.output/server/_libs/tailwind-merge.mjs +0 -3255
  305. package/.output/server/_libs/tanstack__react-virtual.mjs +0 -130
  306. package/.output/server/_libs/tanstack__virtual-core.mjs +0 -1319
  307. package/.output/server/_libs/three.mjs +0 -26016
  308. package/.output/server/_libs/trim-lines.mjs +0 -41
  309. package/.output/server/_libs/trough.mjs +0 -85
  310. package/.output/server/_libs/tslib.mjs +0 -1
  311. package/.output/server/_libs/ungap__structured-clone.mjs +0 -212
  312. package/.output/server/_libs/unified.mjs +0 -661
  313. package/.output/server/_libs/unist-util-is.mjs +0 -100
  314. package/.output/server/_libs/unist-util-position.mjs +0 -27
  315. package/.output/server/_libs/unist-util-stringify-position.mjs +0 -27
  316. package/.output/server/_libs/unist-util-visit-parents.mjs +0 -82
  317. package/.output/server/_libs/unist-util-visit.mjs +0 -24
  318. package/.output/server/_libs/use-callback-ref.mjs +0 -66
  319. package/.output/server/_libs/use-sidecar.mjs +0 -106
  320. package/.output/server/_libs/vfile-message.mjs +0 -138
  321. package/.output/server/_libs/vfile.mjs +0 -467
  322. package/.output/server/_sessionId-E_Dmopc-.mjs +0 -144
  323. package/.output/server/_sessionId-U54BAqdp.mjs +0 -82
  324. package/.output/server/_ssr/CompareDrawer-CksatPed.mjs +0 -1058
  325. package/.output/server/_ssr/InspectorPet-DNc4GQ7z.mjs +0 -936
  326. package/.output/server/_ssr/ProxyViewerContainer-B02Ey0p_.mjs +0 -17607
  327. package/.output/server/_ssr/ReplayDialog-DdMvHJF1.mjs +0 -723
  328. package/.output/server/_ssr/RequestAnatomy--vh2Z8__.mjs +0 -835
  329. package/.output/server/_ssr/ResponseView-Dhxc-cKC.mjs +0 -755
  330. package/.output/server/_ssr/StreamingChunkSequence-BG0EQmaD.mjs +0 -272
  331. package/.output/server/_ssr/index-D0At9nfd.mjs +0 -82
  332. package/.output/server/_ssr/index-lcfulIXa.mjs +0 -133
  333. package/.output/server/_ssr/json-viewer-My1J1_5R.mjs +0 -481
  334. package/.output/server/_ssr/start-HYkvq4Ni.mjs +0 -4
  335. package/.output/server/_tanstack-start-manifest_v-B2chR8ef.mjs +0 -4
  336. package/.output/server/node_modules/tslib/modules/index.js +0 -70
  337. package/.output/server/node_modules/tslib/modules/package.json +0 -3
  338. package/.output/server/node_modules/tslib/package.json +0 -47
  339. package/.output/server/node_modules/tslib/tslib.js +0 -484
  340. package/.output/server/package.json +0 -9
  341. package/src/routes/__root.tsx +0 -170
  342. /package/.output/{public → ui}/assets/agent-inspector-58K1_MsC.ico +0 -0
  343. /package/.output/{public → ui}/assets/alibaba-TTwafVwX.svg +0 -0
  344. /package/.output/{public → ui}/assets/minimax-BPMzvuL-.jpeg +0 -0
  345. /package/.output/{public → ui}/assets/qwen-mMn3f5ul.webp +0 -0
  346. /package/.output/{public → ui}/assets/zhipuai-BPNAnxo-.svg +0 -0
  347. /package/src/{routes → backend/routes}/livez.ts +0 -0
@@ -1,15 +1,18 @@
1
- import { readFile, writeFile, stat, readdir } from "node:fs/promises";
2
- import { createReadStream, existsSync } from "node:fs";
1
+ import { randomUUID } from "node:crypto";
2
+ import { writeFile, readdir, rename, unlink, open } from "node:fs/promises";
3
+ import { constants, existsSync } from "node:fs";
3
4
  import { join, dirname } from "node:path";
4
- import { createInterface } from "node:readline";
5
5
  import { Buffer } from "node:buffer";
6
6
  import { resolveLogDir, logger } from "./logger";
7
- import type { ApiFormat, CapturedLog } from "./schemas";
7
+ import {
8
+ CapturedLogSchema,
9
+ type ApiFormat,
10
+ type CaptureIncompleteReason,
11
+ type CapturedLog,
12
+ } from "./schemas";
8
13
  import { textByteLength } from "../lib/unknown";
9
14
  import {
10
- findSqliteLogIndexEntry,
11
15
  getSqliteLogIndexMaxId,
12
- listSqliteLogIndexEntries,
13
16
  replaceSqliteLogIndexEntries,
14
17
  syncSqliteLogIndexEntries,
15
18
  upsertSqliteLogIndexEntry,
@@ -19,6 +22,8 @@ import {
19
22
  PRIVATE_FILE_MODE,
20
23
  securePrivateFileSync,
21
24
  } from "./privateDataPath";
25
+ import { recoverCapturedLogStorage } from "./jsonlRecovery";
26
+ import { getSessionArchiveMaxLogId } from "./sessionArchive";
22
27
 
23
28
  export type LogIndexSummary = {
24
29
  id: number;
@@ -50,6 +55,10 @@ export type LogIndexSummary = {
50
55
  streamingChunksPath: string | null;
51
56
  rawRequestBodyBytes: number | null;
52
57
  responseTextBytes: number | null;
58
+ captureIncomplete: boolean;
59
+ droppedChunks: number;
60
+ droppedBytes: number;
61
+ captureIncompleteReason: CaptureIncompleteReason | null;
53
62
  warnings?: string[];
54
63
  error: string | null;
55
64
  };
@@ -78,6 +87,7 @@ type LogIndex = {
78
87
 
79
88
  const INDEX_VERSION = 1;
80
89
  const INDEX_FILE = "logs.idx";
90
+ const INDEX_READ_CHUNK_BYTES = 64 * 1024;
81
91
 
82
92
  export { resolveLogDir as getLogDir };
83
93
 
@@ -86,6 +96,7 @@ function getIndexPath(): string {
86
96
  }
87
97
 
88
98
  let cachedIndex: LogIndex | null = null;
99
+ let allocatedMaxId = 0;
89
100
  let sqliteBackfillScheduledMaxId = 0;
90
101
 
91
102
  export function createLogIndexEntryMetadata(log: CapturedLog): LogIndexEntryMetadata {
@@ -122,6 +133,10 @@ export function createLogIndexEntryMetadata(log: CapturedLog): LogIndexEntryMeta
122
133
  streamingChunksPath: log.streamingChunksPath ?? null,
123
134
  rawRequestBodyBytes: log.rawRequestBodyBytes ?? textByteLength(log.rawRequestBody),
124
135
  responseTextBytes: log.responseTextBytes ?? textByteLength(log.responseText),
136
+ captureIncomplete: log.captureIncomplete ?? false,
137
+ droppedChunks: log.droppedChunks ?? 0,
138
+ droppedBytes: log.droppedBytes ?? 0,
139
+ captureIncompleteReason: log.captureIncompleteReason ?? null,
125
140
  warnings: log.warnings,
126
141
  error: log.error ?? null,
127
142
  },
@@ -136,45 +151,11 @@ function createEmptyIndex(): LogIndex {
136
151
  };
137
152
  }
138
153
 
139
- function isLogIndex(obj: unknown): obj is LogIndex {
140
- if (typeof obj !== "object" || obj === null || Array.isArray(obj)) return false;
141
- const versionDesc = Object.getOwnPropertyDescriptor(obj, "version");
142
- const entriesDesc = Object.getOwnPropertyDescriptor(obj, "entries");
143
- const maxIdDesc = Object.getOwnPropertyDescriptor(obj, "maxId");
144
- return (
145
- versionDesc !== undefined &&
146
- typeof versionDesc.value === "number" &&
147
- entriesDesc !== undefined &&
148
- typeof entriesDesc.value === "object" &&
149
- entriesDesc.value !== null &&
150
- maxIdDesc !== undefined &&
151
- typeof maxIdDesc.value === "number"
152
- );
153
- }
154
-
155
154
  export async function loadIndex(): Promise<LogIndex> {
156
155
  if (cachedIndex !== null) return cachedIndex;
157
-
158
- const indexPath = getIndexPath();
159
- if (!existsSync(indexPath)) {
160
- cachedIndex = createEmptyIndex();
161
- return cachedIndex;
162
- }
163
-
164
- try {
165
- const content = await readFile(indexPath, "utf-8");
166
- const parsed: unknown = JSON.parse(content);
167
- if (isLogIndex(parsed)) {
168
- cachedIndex = parsed;
169
- } else {
170
- cachedIndex = createEmptyIndex();
171
- }
172
- return cachedIndex;
173
- } catch (err) {
174
- logger.error("[logIndex] Failed to load index:", String(err));
175
- cachedIndex = createEmptyIndex();
176
- return cachedIndex;
177
- }
156
+ // logs.idx and inspector.sqlite are projections only. A cold process always rebuilds from
157
+ // authoritative JSONL rather than allowing a stale, conflicting, or higher derived row to win.
158
+ return await rebuildIndex();
178
159
  }
179
160
 
180
161
  export async function saveIndex(index: LogIndex): Promise<void> {
@@ -186,15 +167,23 @@ export async function saveIndex(index: LogIndex): Promise<void> {
186
167
  return;
187
168
  }
188
169
 
170
+ const temporaryPath = `${indexPath}.${String(process.pid)}.${randomUUID()}.tmp`;
189
171
  try {
190
- await writeFile(indexPath, JSON.stringify(index), {
172
+ await writeFile(temporaryPath, JSON.stringify(index), {
191
173
  encoding: "utf-8",
192
174
  mode: PRIVATE_FILE_MODE,
193
175
  });
176
+ if (!securePrivateFileSync(temporaryPath)) {
177
+ await unlink(temporaryPath).catch(() => undefined);
178
+ logger.error("[logIndex] Failed to secure temporary index file:", temporaryPath);
179
+ return;
180
+ }
181
+ await rename(temporaryPath, indexPath);
194
182
  if (!securePrivateFileSync(indexPath)) {
195
183
  logger.error("[logIndex] Failed to secure index file:", indexPath);
196
184
  }
197
185
  } catch (err) {
186
+ await unlink(temporaryPath).catch(() => undefined);
198
187
  logger.error("[logIndex] Failed to save index:", String(err));
199
188
  }
200
189
  }
@@ -220,6 +209,25 @@ export async function addToIndex(
220
209
  scheduleIndexFlush();
221
210
  }
222
211
 
212
+ /**
213
+ * Refresh the searchable summary after a request reaches its terminal state. The initial index
214
+ * entry is created before the upstream call starts, so leaving it untouched would make compact
215
+ * reads (and the SQLite sidecar) report the pre-request placeholder after finalization.
216
+ */
217
+ export async function updateLogIndexEntryMetadata(log: CapturedLog): Promise<void> {
218
+ const index = await loadIndex();
219
+ const existing = index.entries[log.id];
220
+ if (existing === undefined) return;
221
+
222
+ const updated: LogIndexEntry = {
223
+ ...existing,
224
+ ...createLogIndexEntryMetadata(log),
225
+ };
226
+ index.entries[log.id] = updated;
227
+ await upsertSqliteLogIndexEntry(updated);
228
+ scheduleIndexFlush();
229
+ }
230
+
223
231
  // Batch writes: collect pending flushes and write once
224
232
  let indexFlushScheduled = false;
225
233
  let indexFlushTimeout: ReturnType<typeof setTimeout> | null = null;
@@ -250,9 +258,6 @@ export async function flushIndex(): Promise<void> {
250
258
  }
251
259
 
252
260
  export async function findInIndex(id: number): Promise<LogIndexEntry | null> {
253
- const sqliteEntry = await findSqliteLogIndexEntry(id);
254
- if (sqliteEntry !== null) return sqliteEntry;
255
-
256
261
  const index = await loadIndex();
257
262
  return index.entries[id] ?? null;
258
263
  }
@@ -260,10 +265,6 @@ export async function findInIndex(id: number): Promise<LogIndexEntry | null> {
260
265
  export async function listIndexEntries(): Promise<LogIndexEntry[]> {
261
266
  const index = await loadIndex();
262
267
  const sqliteMaxId = await getSqliteLogIndexMaxId();
263
- if (sqliteMaxId !== null && sqliteMaxId >= index.maxId) {
264
- const sqliteEntries = await listSqliteLogIndexEntries();
265
- if (sqliteEntries !== null) return sqliteEntries;
266
- }
267
268
  maybeBackfillSqliteIndex(index, sqliteMaxId);
268
269
  return Object.values(index.entries).sort((left, right) => left.id - right.id);
269
270
  }
@@ -277,12 +278,7 @@ export async function listFilteredIndexEntries({
277
278
  }): Promise<LogIndexEntry[]> {
278
279
  const index = await loadIndex();
279
280
  const sqliteMaxId = await getSqliteLogIndexMaxId();
280
- if (sqliteMaxId !== null && sqliteMaxId >= index.maxId) {
281
- const sqliteEntries = await listSqliteLogIndexEntries({ sessionId, model });
282
- if (sqliteEntries !== null) return sqliteEntries;
283
- }
284
281
  maybeBackfillSqliteIndex(index, sqliteMaxId);
285
-
286
282
  return Object.values(index.entries)
287
283
  .sort((left, right) => left.id - right.id)
288
284
  .filter((entry) => {
@@ -311,6 +307,11 @@ type FileIndexResult = {
311
307
  maxId: number;
312
308
  };
313
309
 
310
+ type ParsedIndexRecord =
311
+ | { status: "blank" }
312
+ | { status: "log"; log: CapturedLog }
313
+ | { status: "invalid"; message: string };
314
+
314
315
  function readStringOrNullField(entry: object, field: string): string | null {
315
316
  const desc = Object.getOwnPropertyDescriptor(entry, field);
316
317
  if (desc === undefined) return null;
@@ -349,6 +350,22 @@ function readApiFormatField(entry: object): ApiFormat {
349
350
  }
350
351
  }
351
352
 
353
+ function readCaptureIncompleteReasonField(entry: object): CaptureIncompleteReason | null {
354
+ const value = readStringOrNullField(entry, "captureIncompleteReason");
355
+ switch (value) {
356
+ case "capacity-exceeded":
357
+ case "pipeline-closed":
358
+ case "write-failed":
359
+ case "finalize-failed":
360
+ case "upstream-stream-error":
361
+ case "shutdown":
362
+ return value;
363
+ case null:
364
+ default:
365
+ return null;
366
+ }
367
+ }
368
+
352
369
  function readStringArrayField(entry: object, field: string): string[] | undefined {
353
370
  const desc = Object.getOwnPropertyDescriptor(entry, field);
354
371
  if (desc === undefined || !Array.isArray(desc.value)) return undefined;
@@ -398,58 +415,138 @@ function readLogIndexSummary(entry: object, id: number): LogIndexSummary {
398
415
  streamingChunksPath: readStringOrNullField(entry, "streamingChunksPath"),
399
416
  rawRequestBodyBytes: readTextByteLength(entry, "rawRequestBodyBytes", "rawRequestBody"),
400
417
  responseTextBytes: readTextByteLength(entry, "responseTextBytes", "responseText"),
418
+ captureIncomplete: readBooleanField(entry, "captureIncomplete"),
419
+ droppedChunks: readNumberOrNullField(entry, "droppedChunks") ?? 0,
420
+ droppedBytes: readNumberOrNullField(entry, "droppedBytes") ?? 0,
421
+ captureIncompleteReason: readCaptureIncompleteReasonField(entry),
401
422
  warnings: readStringArrayField(entry, "warnings"),
402
423
  error: readStringOrNullField(entry, "error"),
403
424
  };
404
425
  }
405
426
 
406
- async function indexFile(filePath: string, file: string): Promise<FileIndexResult> {
407
- const entries: Record<number, LogIndexEntry> = {};
408
- let maxId = 0;
409
- let byteOffset = 0;
427
+ function parseIndexRecord(record: Buffer, file: string, byteOffset: number): ParsedIndexRecord {
428
+ const content =
429
+ record.length > 0 && record[record.length - 1] === 0x0d ? record.subarray(0, -1) : record;
430
+ const line = content.toString("utf-8");
431
+ if (line.trim() === "") return { status: "blank" };
410
432
 
411
- const fileStream = createInterface({
412
- input: createReadStream(filePath),
413
- crlfDelay: Infinity,
414
- });
433
+ let parsed: unknown;
434
+ try {
435
+ parsed = JSON.parse(line);
436
+ } catch (error) {
437
+ return {
438
+ status: "invalid",
439
+ message: `Malformed JSONL record in ${file} at byte ${String(byteOffset)}: ${String(error)}`,
440
+ };
441
+ }
442
+ const validated = CapturedLogSchema.safeParse(parsed);
443
+ return validated.success
444
+ ? { status: "log", log: validated.data }
445
+ : {
446
+ status: "invalid",
447
+ message: `Invalid captured-log record in ${file} at byte ${String(byteOffset)}`,
448
+ };
449
+ }
415
450
 
416
- for await (const line of fileStream) {
417
- const lineBytes = Buffer.byteLength(line, "utf-8") + 1;
418
- if (line.trim() !== "") {
419
- try {
420
- const entry: unknown = JSON.parse(line);
421
- if (typeof entry === "object" && entry !== null && !Array.isArray(entry)) {
422
- const idDesc = Object.getOwnPropertyDescriptor(entry, "id");
423
- if (idDesc !== undefined && typeof idDesc.value === "number") {
424
- const entryId = idDesc.value;
425
- maxId = Math.max(maxId, entryId);
426
- entries[entryId] = {
427
- id: entryId,
428
- file,
429
- byteOffset,
430
- byteLength: lineBytes,
431
- sessionId: readStringOrNullField(entry, "sessionId"),
432
- model: readStringOrNullField(entry, "model"),
433
- summary: readLogIndexSummary(entry, entryId),
434
- };
435
- }
436
- }
437
- } catch {
438
- // Skip malformed lines
439
- }
451
+ function addIndexedRecord(
452
+ result: FileIndexResult,
453
+ parsed: ParsedIndexRecord,
454
+ file: string,
455
+ byteOffset: number,
456
+ byteLength: number,
457
+ ): Error | null {
458
+ switch (parsed.status) {
459
+ case "blank":
460
+ return null;
461
+ case "invalid":
462
+ return new Error(parsed.message);
463
+ case "log": {
464
+ const entry = parsed.log;
465
+ result.maxId = Math.max(result.maxId, entry.id);
466
+ result.entries[entry.id] = {
467
+ id: entry.id,
468
+ file,
469
+ byteOffset,
470
+ byteLength,
471
+ sessionId: entry.sessionId,
472
+ model: entry.model,
473
+ summary: readLogIndexSummary(entry, entry.id),
474
+ };
475
+ return null;
440
476
  }
441
- byteOffset += lineBytes;
442
477
  }
478
+ }
479
+
480
+ async function indexFile(filePath: string, file: string): Promise<FileIndexResult> {
481
+ const result: FileIndexResult = { entries: {}, maxId: 0 };
482
+ let handle: Awaited<ReturnType<typeof open>> | null = null;
483
+ try {
484
+ handle = await open(filePath, constants.O_RDONLY | constants.O_NOFOLLOW);
485
+ const fileStats = await handle.stat();
486
+ if (!fileStats.isFile()) {
487
+ return await Promise.reject(new Error(`Captured-log ledger is not a regular file: ${file}`));
488
+ }
489
+
490
+ const readBuffer = Buffer.alloc(INDEX_READ_CHUNK_BYTES);
491
+ let position = 0;
492
+ let pending = Buffer.alloc(0);
493
+ let pendingOffset = 0;
494
+ while (position < fileStats.size) {
495
+ const length = Math.min(INDEX_READ_CHUNK_BYTES, fileStats.size - position);
496
+ const read = await handle.read(readBuffer, 0, length, position);
497
+ if (read.bytesRead === 0) {
498
+ return await Promise.reject(
499
+ new Error(`Captured-log ledger changed while rebuilding index: ${file}`),
500
+ );
501
+ }
502
+ const chunk = readBuffer.subarray(0, read.bytesRead);
503
+ const combined = pending.length === 0 ? Buffer.from(chunk) : Buffer.concat([pending, chunk]);
504
+ let consumed = 0;
505
+ let newline = combined.indexOf(0x0a, consumed);
506
+ while (newline !== -1) {
507
+ const record = combined.subarray(consumed, newline);
508
+ const byteOffset = pendingOffset + consumed;
509
+ const byteLength = record.length + 1;
510
+ const error = addIndexedRecord(
511
+ result,
512
+ parseIndexRecord(record, file, byteOffset),
513
+ file,
514
+ byteOffset,
515
+ byteLength,
516
+ );
517
+ if (error !== null) return await Promise.reject(error);
518
+ consumed = newline + 1;
519
+ newline = combined.indexOf(0x0a, consumed);
520
+ }
521
+ pending = Buffer.from(combined.subarray(consumed));
522
+ pendingOffset += consumed;
523
+ position += read.bytesRead;
524
+ }
443
525
 
444
- return { entries, maxId };
526
+ if (pending.length > 0) {
527
+ return await Promise.reject(
528
+ new Error(`Captured-log ledger ended without recovery delimiter: ${file}`),
529
+ );
530
+ }
531
+ return result;
532
+ } finally {
533
+ await handle?.close().catch(() => undefined);
534
+ }
445
535
  }
446
536
 
447
537
  export async function rebuildIndex(): Promise<LogIndex> {
448
538
  const logDir = resolveLogDir();
449
539
  const newIndex = createEmptyIndex();
540
+ const recovery = await recoverCapturedLogStorage(logDir);
541
+ if (recovery.level === "unavailable") {
542
+ return await Promise.reject(
543
+ new Error(recovery.diagnostic ?? "Captured-log recovery blocked index rebuild"),
544
+ );
545
+ }
450
546
 
451
547
  if (!existsSync(logDir)) {
452
548
  cachedIndex = newIndex;
549
+ allocatedMaxId = 0;
453
550
  await saveIndex(newIndex);
454
551
  await replaceSqliteLogIndexEntries([]);
455
552
  return newIndex;
@@ -470,6 +567,7 @@ export async function rebuildIndex(): Promise<LogIndex> {
470
567
  }
471
568
 
472
569
  cachedIndex = newIndex;
570
+ allocatedMaxId = Math.max(allocatedMaxId, newIndex.maxId);
473
571
  await saveIndex(newIndex);
474
572
  await replaceSqliteLogIndexEntries(Object.values(newIndex.entries));
475
573
  return newIndex;
@@ -494,11 +592,9 @@ async function withIdLock<T>(fn: () => Promise<T>): Promise<T> {
494
592
  export async function getNextLogId(): Promise<number> {
495
593
  return withIdLock(async () => {
496
594
  const index = await loadIndex();
497
- const sqliteMaxId = await getSqliteLogIndexMaxId();
498
- const nextId = Math.max(index.maxId, sqliteMaxId ?? 0) + 1;
499
- index.maxId = nextId;
500
- // Synchronously update the index in memory (disk write is deferred via batching)
501
- cachedIndex = index;
595
+ const archiveMaxId = await getSessionArchiveMaxLogId();
596
+ const nextId = Math.max(index.maxId, archiveMaxId, allocatedMaxId) + 1;
597
+ allocatedMaxId = nextId;
502
598
  return nextId;
503
599
  });
504
600
  }
@@ -1,5 +1,5 @@
1
1
  import { Buffer } from "node:buffer";
2
- import { appendFileSync } from "node:fs";
2
+ import { closeSync, constants, fsyncSync, openSync, writeSync } from "node:fs";
3
3
  import { appendFile, readdir, rename, stat, unlink } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { getDataDir } from "./dataDir";
@@ -9,6 +9,12 @@ import {
9
9
  securePrivateFileSync,
10
10
  securePrivateTreeSync,
11
11
  } from "./privateDataPath";
12
+ import {
13
+ canAppendCapturedLogFileOnExit,
14
+ markCapturedLogFileAppended,
15
+ recoverCapturedLogFileBeforeAppend,
16
+ recoverCapturedLogStorage,
17
+ } from "./jsonlRecovery";
12
18
 
13
19
  const DAY_MS = 24 * 60 * 60 * 1000;
14
20
  const DEFAULT_RETENTION_DAYS = 7;
@@ -236,17 +242,25 @@ export async function initLogger(): Promise<void> {
236
242
  } else if (!securePrivateTreeSync(dir)) {
237
243
  writeStderrFallback("[logger] Failed to secure existing log files:", dir);
238
244
  } else {
239
- const entries = await readdir(dir);
240
- for (const entry of entries) {
241
- if (!entry.endsWith(".jsonl")) continue;
242
- const fullPath = path.join(dir, entry);
243
- try {
244
- const s = await stat(fullPath);
245
- if (s.mtimeMs < cutoff) {
246
- await unlink(fullPath);
245
+ const recovery = await recoverCapturedLogStorage(dir);
246
+ if (recovery.level === "unavailable") {
247
+ writeStderrFallback(
248
+ "[logger] Captured-log recovery blocked readiness:",
249
+ recovery.diagnostic ?? "unknown recovery failure",
250
+ );
251
+ } else {
252
+ const entries = await readdir(dir);
253
+ for (const entry of entries) {
254
+ if (!entry.endsWith(".jsonl")) continue;
255
+ const fullPath = path.join(dir, entry);
256
+ try {
257
+ const s = await stat(fullPath);
258
+ if (s.mtimeMs < cutoff) {
259
+ await unlink(fullPath);
260
+ }
261
+ } catch {
262
+ // Skip files that can't be stat - not critical
247
263
  }
248
- } catch {
249
- // Skip files that can't be stat - not critical
250
264
  }
251
265
  }
252
266
  }
@@ -354,9 +368,29 @@ export const logger = {
354
368
 
355
369
  // Write buffer for batching async writes
356
370
  const MAX_BUFFER_SIZE = 1000;
371
+ const BUFFER_FLUSH_DELAY_MS = 10;
357
372
  let writeBuffer: string[] = [];
358
373
  let writeQueue: Promise<void> = Promise.resolve();
359
374
  let logWriteLock: Promise<void> = Promise.resolve();
375
+ let scheduledBufferFlush: ReturnType<typeof setTimeout> | null = null;
376
+
377
+ function queueBufferedFlush(): void {
378
+ writeQueue = writeQueue.then(() => runWithLogWriteLock(flushWriteBufferUnlocked));
379
+ }
380
+
381
+ function cancelScheduledBufferFlush(): void {
382
+ if (scheduledBufferFlush === null) return;
383
+ clearTimeout(scheduledBufferFlush);
384
+ scheduledBufferFlush = null;
385
+ }
386
+
387
+ function scheduleBufferedFlush(): void {
388
+ if (scheduledBufferFlush !== null) return;
389
+ scheduledBufferFlush = setTimeout(() => {
390
+ scheduledBufferFlush = null;
391
+ queueBufferedFlush();
392
+ }, BUFFER_FLUSH_DELAY_MS);
393
+ }
360
394
 
361
395
  export async function runWithLogWriteLock<T>(fn: () => Promise<T>): Promise<T> {
362
396
  const previousLock = logWriteLock;
@@ -393,10 +427,20 @@ async function flushWriteBufferUnlocked(): Promise<void> {
393
427
  writeStderrFallback("[logger] Failed to secure captured-log directory:", filePath);
394
428
  return;
395
429
  }
430
+ const recovery = await recoverCapturedLogFileBeforeAppend(resolveLogDir(), filePath);
431
+ if (recovery.level === "unavailable") {
432
+ writeBuffer.unshift(toWrite);
433
+ writeStderrFallback(
434
+ "[logger] Refused captured-log append until recovery succeeds:",
435
+ recovery.diagnostic ?? "unknown recovery failure",
436
+ );
437
+ return;
438
+ }
396
439
  await appendFile(filePath, toWrite, { encoding: "utf-8", mode: PRIVATE_FILE_MODE });
397
440
  if (!securePrivateFileSync(filePath)) {
398
441
  writeStderrFallback("[logger] Failed to secure captured-log file:", filePath);
399
442
  }
443
+ await markCapturedLogFileAppended(resolveLogDir(), filePath);
400
444
  } catch (err) {
401
445
  // On failure, re-queue the data so it is not lost
402
446
  writeBuffer.unshift(toWrite);
@@ -408,9 +452,10 @@ export function appendLogEntry(entry: Record<string, unknown>): void {
408
452
  const line = JSON.stringify(entry) + "\n";
409
453
  writeBuffer.push(line);
410
454
  if (writeBuffer.length >= MAX_BUFFER_SIZE) {
411
- writeQueue = writeQueue.then(() => runWithLogWriteLock(flushWriteBufferUnlocked));
455
+ cancelScheduledBufferFlush();
456
+ queueBufferedFlush();
412
457
  } else if (writeBuffer.length === 1) {
413
- writeQueue = writeQueue.then(() => runWithLogWriteLock(flushWriteBufferUnlocked));
458
+ scheduleBufferedFlush();
414
459
  }
415
460
  }
416
461
 
@@ -419,6 +464,7 @@ export function appendLogEntry(entry: Record<string, unknown>): void {
419
464
  * to avoid losing unflushed data.
420
465
  */
421
466
  export async function flushLogBuffer(): Promise<void> {
467
+ cancelScheduledBufferFlush();
422
468
  await writeQueue;
423
469
  if (writeBuffer.length > 0) {
424
470
  await runWithLogWriteLock(flushWriteBufferUnlocked);
@@ -441,7 +487,24 @@ process.on("exit", () => {
441
487
  // Synchronous write required in 'exit' handler (async I/O not allowed)
442
488
  const filePath = getLogFilePath();
443
489
  if (!ensurePrivateDirectorySync(path.dirname(filePath))) return;
444
- appendFileSync(filePath, toWrite, { encoding: "utf-8", mode: PRIVATE_FILE_MODE });
490
+ if (!canAppendCapturedLogFileOnExit(resolveLogDir(), filePath)) return;
491
+ const descriptor = openSync(
492
+ filePath,
493
+ constants.O_WRONLY | constants.O_APPEND | constants.O_NOFOLLOW,
494
+ PRIVATE_FILE_MODE,
495
+ );
496
+ try {
497
+ const payload = Buffer.from(toWrite, "utf-8");
498
+ let written = 0;
499
+ while (written < payload.length) {
500
+ const writeCount = writeSync(descriptor, payload, written, payload.length - written);
501
+ if (writeCount === 0) return;
502
+ written += writeCount;
503
+ }
504
+ fsyncSync(descriptor);
505
+ } finally {
506
+ closeSync(descriptor);
507
+ }
445
508
  void securePrivateFileSync(filePath);
446
509
  } catch {
447
510
  // Best-effort: nothing we can do in an exit handler
@@ -205,6 +205,47 @@ export function killPid(pid: number, platform: PlatformName = process.platform):
205
205
  }
206
206
  }
207
207
 
208
+ export type ProcessTreeSignal = "SIGTERM" | "SIGKILL";
209
+
210
+ /**
211
+ * Signal a child process and every descendant created in its process tree.
212
+ *
213
+ * Unix callers must spawn the root with `detached: true`, which makes its PID the process-group
214
+ * ID. Windows has no equivalent portable Node signal, so taskkill owns descendant traversal.
215
+ */
216
+ export function signalProcessTree(
217
+ pid: number,
218
+ signal: ProcessTreeSignal,
219
+ platform: PlatformName = process.platform,
220
+ ): boolean {
221
+ if (!Number.isInteger(pid) || pid <= 0) return false;
222
+ try {
223
+ if (platform === "win32") {
224
+ const args = ["/PID", String(pid), "/T"];
225
+ if (signal === "SIGKILL") args.push("/F");
226
+ execFileSync("taskkill.exe", args, {
227
+ encoding: "utf8",
228
+ timeout: 5000,
229
+ windowsHide: true,
230
+ stdio: ["ignore", "pipe", "ignore"],
231
+ });
232
+ return true;
233
+ }
234
+ process.kill(-pid, signal);
235
+ return true;
236
+ } catch {
237
+ // Killing only the Windows root would orphan descendants and could make the runtime report a
238
+ // false clean shutdown. Let the caller retry taskkill with /F instead.
239
+ if (platform === "win32") return false;
240
+ try {
241
+ process.kill(pid, signal);
242
+ return true;
243
+ } catch {
244
+ return false;
245
+ }
246
+ }
247
+ }
248
+
208
249
  export const platformCommandParsersForTests = {
209
250
  parseUnixLsofPids,
210
251
  parseUnixSsPids,