@tonyclaw/agent-inspector 3.0.45 → 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 (338) 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 +2326 -516
  4. package/.output/server/_libs/extend.mjs +4 -8
  5. package/.output/server/_libs/jszip.mjs +9 -35
  6. package/.output/server/_libs/react-dom.mjs +2 -3
  7. package/.output/server/_libs/react.mjs +1 -27
  8. package/.output/server/_libs/tanstack__react-router.mjs +1 -329
  9. package/.output/server/_libs/tanstack__router-core.mjs +42 -100
  10. package/.output/server/_libs/use-sync-external-store.mjs +1 -64
  11. package/.output/server/_libs/ws.mjs +1 -1
  12. package/.output/server/_ssr/index.mjs +4 -4
  13. package/.output/server/_ssr/{publicBasePathContext-5264L3rX.mjs → publicBasePathContext-IH2ilOfu.mjs} +52 -24
  14. package/.output/server/_ssr/{router-BJ_ltLim.mjs → router-D18yUq36.mjs} +19770 -19447
  15. package/.output/server/_ssr/{start-C6MjPjxp.mjs → start-COQu_AgD.mjs} +1 -1
  16. package/.output/server/_tanstack-start-manifest_v-DnbdNeun.mjs +4 -0
  17. package/.output/server/index.mjs +5 -145
  18. package/.output/{public/assets/CompareDrawer-B3_TLTJh.js → ui/assets/CompareDrawer-DAUuIJ6G.js} +1 -1
  19. package/.output/{public/assets/InspectorPet-Cpn6hv4P.js → ui/assets/InspectorPet-BRBVjOWI.js} +1 -1
  20. package/.output/ui/assets/ProxyViewerContainer-D7Sq0ctc.js +59 -0
  21. package/.output/{public/assets/ReplayDialog-B3EKVdJk.js → ui/assets/ReplayDialog-nDLsjOhs.js} +1 -1
  22. package/.output/{public/assets/RequestAnatomy-COOQjyUC.js → ui/assets/RequestAnatomy-zd4BDgL0.js} +1 -1
  23. package/.output/ui/assets/ResponseView-AdIXGcSn.js +2 -0
  24. package/.output/ui/assets/StreamingChunkSequence-CesTz8He.js +1 -0
  25. package/.output/{public/assets/_sessionId-zbgYl7ZJ.js → ui/assets/_sessionId-BAfzrhSU.js} +1 -1
  26. package/.output/{public/assets/_sessionId-BJlM3nmt.js → ui/assets/_sessionId-QnBomgPD.js} +1 -1
  27. package/.output/ui/assets/favicon-DYEkHSNl.svg +21 -0
  28. package/.output/{public/assets/index-BIK4bPTv.js → ui/assets/index-BcsdxBAd.js} +1 -1
  29. package/.output/ui/assets/index-BvXp42al.css +1 -0
  30. package/.output/{public/assets/index-D2Qw6Ri2.js → ui/assets/index-D0rtCN9V.js} +1 -1
  31. package/.output/ui/assets/index-DHpwr08Z.js +73 -0
  32. package/.output/{public/assets/index-xF_NY8Yb.js → ui/assets/index-dNuk2dsU.js} +5 -5
  33. package/.output/ui/assets/json-viewer-BvFHglMb.js +1 -0
  34. package/.output/{public/assets/jszip.min-B_rfAUCe.js → ui/assets/jszip.min-B5Z7gNZB.js} +1 -1
  35. package/.output/ui/index.html +229 -0
  36. package/.output/ui/runtime-config.js +2 -0
  37. package/.output/workers/logFinalizer.worker.js +979 -414
  38. package/.output/workers/sessionWorkerEntry.js +979 -414
  39. package/README.md +175 -43
  40. package/docs/instance-control.openapi.yaml +59 -4
  41. package/package.json +25 -5
  42. package/scripts/generate-theme-scales.mjs +3 -3
  43. package/src/{router.tsx → backend/router.tsx} +3 -15
  44. package/src/backend/routes/$.ts +26 -0
  45. package/src/backend/routes/__root.ts +4 -0
  46. package/src/{routes → backend/routes}/api/-instances.ts +3 -3
  47. package/src/{routes → backend/routes}/api/alerts.summary.ts +3 -3
  48. package/src/{routes → backend/routes}/api/alerts.ts +4 -4
  49. package/src/{routes → backend/routes}/api/config.paths.ts +1 -1
  50. package/src/{routes → backend/routes}/api/config.ts +11 -2
  51. package/src/{routes → backend/routes}/api/ecosystem.packages.$packageId.help.ts +1 -1
  52. package/src/{routes → backend/routes}/api/ecosystem.packages.$packageId.install.ts +1 -1
  53. package/src/{routes → backend/routes}/api/ecosystem.packages.$packageId.runner-presets.ts +1 -1
  54. package/src/{routes → backend/routes}/api/ecosystem.packages.$packageId.upgrade.ts +1 -1
  55. package/src/{routes → backend/routes}/api/ecosystem.packages.ts +5 -1
  56. package/src/{routes → backend/routes}/api/ecosystem.recipes.$recipeId.run.ts +1 -1
  57. package/src/{routes → backend/routes}/api/ecosystem.tasks.$taskId.ts +1 -1
  58. package/src/{routes → backend/routes}/api/ecosystem.tasks.ts +1 -1
  59. package/src/{routes → backend/routes}/api/groups.$groupId.evidence.ts +2 -2
  60. package/src/{routes → backend/routes}/api/groups.$groupId.sessions.ts +2 -2
  61. package/src/{routes → backend/routes}/api/groups.$groupId.ts +2 -2
  62. package/src/{routes → backend/routes}/api/groups.ts +2 -2
  63. package/src/{routes → backend/routes}/api/health.ts +3 -3
  64. package/src/{routes → backend/routes}/api/instances.$name.connection.ts +1 -1
  65. package/src/{routes → backend/routes}/api/instances.$name.restart.ts +2 -2
  66. package/src/{routes → backend/routes}/api/instances.$name.start.ts +2 -2
  67. package/src/{routes → backend/routes}/api/instances.$name.stop.ts +2 -2
  68. package/src/{routes → backend/routes}/api/instances.$name.ts +1 -1
  69. package/src/{routes → backend/routes}/api/instances.ts +1 -1
  70. package/src/{routes → backend/routes}/api/knowledge.candidates.$candidateId.promote.ts +2 -2
  71. package/src/{routes → backend/routes}/api/knowledge.candidates.$candidateId.ts +2 -2
  72. package/src/{routes → backend/routes}/api/knowledge.candidates.ts +1 -1
  73. package/src/{routes → backend/routes}/api/knowledge.project-context.ts +2 -2
  74. package/src/{routes → backend/routes}/api/knowledge.search.ts +2 -2
  75. package/src/{routes → backend/routes}/api/knowledge.sessions.$sessionId.candidates.ts +3 -3
  76. package/src/{routes → backend/routes}/api/logs.$id.body.ts +6 -6
  77. package/src/{routes → backend/routes}/api/logs.$id.chunks.ts +5 -4
  78. package/src/{routes → backend/routes}/api/logs.$id.replay.ts +12 -11
  79. package/src/{routes → backend/routes}/api/logs.$id.ts +4 -3
  80. package/src/{routes → backend/routes}/api/logs.import.ts +3 -3
  81. package/src/{routes → backend/routes}/api/logs.stream.ts +11 -7
  82. package/src/{routes → backend/routes}/api/logs.ts +17 -9
  83. package/src/{routes → backend/routes}/api/mcp.ts +1 -1
  84. package/src/{routes → backend/routes}/api/models.ts +1 -1
  85. package/src/{routes → backend/routes}/api/pi-agent.ts +2 -2
  86. package/src/{routes → backend/routes}/api/providers.$providerId.model-metadata.ts +5 -5
  87. package/src/{routes → backend/routes}/api/providers.$providerId.test.log.ts +7 -7
  88. package/src/{routes → backend/routes}/api/providers.$providerId.ts +4 -4
  89. package/src/{routes → backend/routes}/api/providers.export.ts +1 -1
  90. package/src/{routes → backend/routes}/api/providers.import.ts +3 -3
  91. package/src/{routes → backend/routes}/api/providers.scan.ts +2 -2
  92. package/src/{routes → backend/routes}/api/providers.ts +9 -4
  93. package/src/{routes → backend/routes}/api/runs.$runId.evidence.ts +2 -2
  94. package/src/{routes → backend/routes}/api/runs.$runId.ts +2 -2
  95. package/src/{routes → backend/routes}/api/runs.ts +4 -4
  96. package/src/backend/routes/api/runtime.ts +46 -0
  97. package/src/{routes → backend/routes}/api/sessions.ts +3 -3
  98. package/src/{routes → backend/routes}/proxy/$.ts +1 -1
  99. package/src/{routes → backend/routes}/readyz.ts +1 -1
  100. package/src/{start.ts → backend/start.ts} +1 -1
  101. package/src/cli/alias.ts +2 -11
  102. package/src/cli/doctor.ts +32 -9
  103. package/src/cli/instance.ts +26 -13
  104. package/src/cli/instanceArgs.ts +30 -11
  105. package/src/cli/instanceControl.ts +0 -2
  106. package/src/cli/instanceModel.ts +9 -17
  107. package/src/cli/instanceRegistry.ts +0 -9
  108. package/src/cli/networkHints.ts +27 -14
  109. package/src/cli/onboard.ts +69 -2
  110. package/src/cli/onboardTarget.ts +89 -40
  111. package/src/cli/templates/codex-skill-onboard.ts +44 -14
  112. package/src/cli/templates/skill-onboard.ts +64 -28
  113. package/src/cli.ts +184 -61
  114. package/src/components/ProxyViewer.tsx +11 -9
  115. package/src/components/ProxyViewerContainer.tsx +12 -11
  116. package/src/components/pi-agent/PiAgentPanel.tsx +2 -2
  117. package/src/components/providers/ProviderCard.tsx +1 -1
  118. package/src/components/providers/ProviderTestResultsView.tsx +1 -1
  119. package/src/components/providers/SettingsDialog.tsx +159 -144
  120. package/src/components/proxy-viewer/ApplicationBar.tsx +2 -1
  121. package/src/components/proxy-viewer/LogEntry.tsx +1 -1
  122. package/src/components/proxy-viewer/log-formats/index.ts +1 -1
  123. package/src/contracts/index.ts +5 -0
  124. package/src/contracts/log.ts +33 -3
  125. package/src/knowledge/types.ts +3 -2
  126. package/src/lib/alertContract.ts +2 -2
  127. package/src/lib/apiClient.ts +30 -7
  128. package/src/lib/apiFormat.ts +29 -0
  129. package/src/lib/basePath.ts +29 -11
  130. package/src/lib/browserRuntimeContract.ts +124 -0
  131. package/src/lib/browserRuntimeResolver.ts +244 -0
  132. package/src/{proxy → lib}/claudeCodeStrip.ts +6 -7
  133. package/src/lib/codingAgentEndpoints.ts +38 -0
  134. package/src/lib/instanceContract.ts +27 -11
  135. package/src/lib/piAgentContract.ts +3 -2
  136. package/src/lib/publicBasePathContext.ts +40 -10
  137. package/src/lib/resourceLimits.ts +2 -0
  138. package/src/lib/runContract.ts +3 -3
  139. package/src/lib/sessionInfoContract.ts +3 -2
  140. package/src/mcp/instanceHandlers.ts +1 -0
  141. package/src/mcp/server.ts +19 -24
  142. package/src/proxy/config.ts +25 -30
  143. package/src/proxy/dataDir.ts +43 -7
  144. package/src/proxy/ecosystemTasks.ts +161 -70
  145. package/src/proxy/formats/registry.ts +6 -21
  146. package/src/proxy/handler.ts +129 -29
  147. package/src/proxy/identityProxy.ts +419 -39
  148. package/src/proxy/jsonlRecovery.ts +554 -0
  149. package/src/proxy/logFinalizer.ts +25 -10
  150. package/src/proxy/logImporter.ts +2 -1
  151. package/src/proxy/logIndex.ts +191 -95
  152. package/src/proxy/logger.ts +77 -14
  153. package/src/proxy/platformCommands.ts +41 -0
  154. package/src/proxy/privateDataPath.ts +310 -88
  155. package/src/proxy/rawStreamCapture.ts +194 -13
  156. package/src/proxy/runtimeAdmission.ts +5 -2
  157. package/src/proxy/runtimeHealth.ts +14 -1
  158. package/src/proxy/runtimeShutdown.ts +11 -1
  159. package/src/proxy/schemas.ts +7 -1
  160. package/src/proxy/sessionArchive.ts +78 -14
  161. package/src/proxy/sqliteLogIndex.ts +65 -2
  162. package/src/proxy/store.ts +141 -34
  163. package/src/proxy/uiAssetServer.ts +106 -0
  164. package/src/proxy/upstream.ts +29 -2
  165. package/src/ui/bootstrap.ts +167 -0
  166. package/src/ui/index.html +38 -0
  167. package/src/ui/logsStreamClient.ts +20 -0
  168. package/src/ui/main.tsx +13 -0
  169. package/src/ui/public/runtime-config.js +2 -0
  170. package/src/ui/router.tsx +13 -0
  171. package/src/ui/routes/__root.tsx +69 -0
  172. package/src/{routes → ui/routes}/index.tsx +2 -3
  173. package/src/{routes → ui/routes}/session/$sessionId.tsx +5 -11
  174. package/src/ui/runtimeDiscovery.ts +50 -0
  175. package/styles/globals.css +1 -0
  176. package/.output/public/assets/ProxyViewerContainer-BEAhuGOJ.js +0 -126
  177. package/.output/public/assets/ResponseView-DBCjkwye.js +0 -2
  178. package/.output/public/assets/StreamingChunkSequence-CuXhDgMo.js +0 -1
  179. package/.output/public/assets/index-BZo_8koU.js +0 -70
  180. package/.output/public/assets/index-ZWschkCh.css +0 -1
  181. package/.output/public/assets/json-viewer-B-Ao6pwW.js +0 -1
  182. package/.output/server/_libs/@radix-ui/react-accessible-icon+[...].mjs +0 -1
  183. package/.output/server/_libs/@radix-ui/react-dismissable-layer+[...].mjs +0 -344
  184. package/.output/server/_libs/@radix-ui/react-navigation-menu+[...].mjs +0 -2
  185. package/.output/server/_libs/@radix-ui/react-one-time-password-field+[...].mjs +0 -2
  186. package/.output/server/_libs/@radix-ui/react-password-toggle-field+[...].mjs +0 -2
  187. package/.output/server/_libs/@radix-ui/react-use-callback-ref+[...].mjs +0 -11
  188. package/.output/server/_libs/@radix-ui/react-use-controllable-state+[...].mjs +0 -69
  189. package/.output/server/_libs/@radix-ui/react-use-effect-event+[...].mjs +0 -1
  190. package/.output/server/_libs/@radix-ui/react-use-is-hydrated+[...].mjs +0 -28
  191. package/.output/server/_libs/@radix-ui/react-use-layout-effect+[...].mjs +0 -6
  192. package/.output/server/_libs/@radix-ui/react-visually-hidden+[...].mjs +0 -34
  193. package/.output/server/_libs/aria-hidden.mjs +0 -122
  194. package/.output/server/_libs/bail.mjs +0 -8
  195. package/.output/server/_libs/character-entities.mjs +0 -2130
  196. package/.output/server/_libs/class-variance-authority.mjs +0 -44
  197. package/.output/server/_libs/clsx.mjs +0 -16
  198. package/.output/server/_libs/comma-separated-tokens.mjs +0 -10
  199. package/.output/server/_libs/decode-named-character-reference+[...].mjs +0 -8
  200. package/.output/server/_libs/dequal.mjs +0 -27
  201. package/.output/server/_libs/detect-node-es.mjs +0 -1
  202. package/.output/server/_libs/devlop.mjs +0 -8
  203. package/.output/server/_libs/diff.mjs +0 -320
  204. package/.output/server/_libs/estree-util-is-identifier-name.mjs +0 -11
  205. package/.output/server/_libs/floating-ui__core.mjs +0 -725
  206. package/.output/server/_libs/floating-ui__dom.mjs +0 -622
  207. package/.output/server/_libs/floating-ui__react-dom.mjs +0 -292
  208. package/.output/server/_libs/floating-ui__utils.mjs +0 -320
  209. package/.output/server/_libs/get-nonce.mjs +0 -9
  210. package/.output/server/_libs/hast-util-to-jsx-runtime.mjs +0 -388
  211. package/.output/server/_libs/hast-util-whitespace.mjs +0 -10
  212. package/.output/server/_libs/html-url-attributes.mjs +0 -26
  213. package/.output/server/_libs/inline-style-parser.mjs +0 -142
  214. package/.output/server/_libs/is-plain-obj.mjs +0 -10
  215. package/.output/server/_libs/lucide-react.mjs +0 -1027
  216. package/.output/server/_libs/mdast-util-from-markdown.mjs +0 -717
  217. package/.output/server/_libs/mdast-util-to-hast.mjs +0 -710
  218. package/.output/server/_libs/mdast-util-to-string.mjs +0 -38
  219. package/.output/server/_libs/micromark-core-commonmark.mjs +0 -2259
  220. package/.output/server/_libs/micromark-factory-destination.mjs +0 -94
  221. package/.output/server/_libs/micromark-factory-label.mjs +0 -63
  222. package/.output/server/_libs/micromark-factory-space.mjs +0 -24
  223. package/.output/server/_libs/micromark-factory-title.mjs +0 -65
  224. package/.output/server/_libs/micromark-factory-whitespace.mjs +0 -22
  225. package/.output/server/_libs/micromark-util-character.mjs +0 -44
  226. package/.output/server/_libs/micromark-util-chunked.mjs +0 -36
  227. package/.output/server/_libs/micromark-util-classify-character+[...].mjs +0 -12
  228. package/.output/server/_libs/micromark-util-combine-extensions+[...].mjs +0 -41
  229. package/.output/server/_libs/micromark-util-decode-numeric-character-reference+[...].mjs +0 -19
  230. package/.output/server/_libs/micromark-util-decode-string.mjs +0 -21
  231. package/.output/server/_libs/micromark-util-encode.mjs +0 -1
  232. package/.output/server/_libs/micromark-util-html-tag-name.mjs +0 -69
  233. package/.output/server/_libs/micromark-util-normalize-identifier+[...].mjs +0 -6
  234. package/.output/server/_libs/micromark-util-resolve-all.mjs +0 -15
  235. package/.output/server/_libs/micromark-util-sanitize-uri.mjs +0 -41
  236. package/.output/server/_libs/micromark-util-subtokenize.mjs +0 -346
  237. package/.output/server/_libs/micromark.mjs +0 -906
  238. package/.output/server/_libs/property-information.mjs +0 -1209
  239. package/.output/server/_libs/radix-ui.mjs +0 -1
  240. package/.output/server/_libs/radix-ui__number.mjs +0 -6
  241. package/.output/server/_libs/radix-ui__primitive.mjs +0 -11
  242. package/.output/server/_libs/radix-ui__react-accordion.mjs +0 -1
  243. package/.output/server/_libs/radix-ui__react-alert-dialog.mjs +0 -1
  244. package/.output/server/_libs/radix-ui__react-arrow.mjs +0 -23
  245. package/.output/server/_libs/radix-ui__react-aspect-ratio.mjs +0 -1
  246. package/.output/server/_libs/radix-ui__react-avatar.mjs +0 -1
  247. package/.output/server/_libs/radix-ui__react-checkbox.mjs +0 -1
  248. package/.output/server/_libs/radix-ui__react-collapsible.mjs +0 -144
  249. package/.output/server/_libs/radix-ui__react-collection.mjs +0 -69
  250. package/.output/server/_libs/radix-ui__react-compose-refs.mjs +0 -38
  251. package/.output/server/_libs/radix-ui__react-context-menu.mjs +0 -1
  252. package/.output/server/_libs/radix-ui__react-context.mjs +0 -64
  253. package/.output/server/_libs/radix-ui__react-dialog.mjs +0 -280
  254. package/.output/server/_libs/radix-ui__react-direction.mjs +0 -9
  255. package/.output/server/_libs/radix-ui__react-dropdown-menu.mjs +0 -1
  256. package/.output/server/_libs/radix-ui__react-focus-guards.mjs +0 -39
  257. package/.output/server/_libs/radix-ui__react-focus-scope.mjs +0 -210
  258. package/.output/server/_libs/radix-ui__react-form.mjs +0 -1
  259. package/.output/server/_libs/radix-ui__react-hover-card.mjs +0 -1
  260. package/.output/server/_libs/radix-ui__react-id.mjs +0 -14
  261. package/.output/server/_libs/radix-ui__react-label.mjs +0 -1
  262. package/.output/server/_libs/radix-ui__react-menu.mjs +0 -1
  263. package/.output/server/_libs/radix-ui__react-menubar.mjs +0 -1
  264. package/.output/server/_libs/radix-ui__react-popover.mjs +0 -1
  265. package/.output/server/_libs/radix-ui__react-popper.mjs +0 -340
  266. package/.output/server/_libs/radix-ui__react-portal.mjs +0 -16
  267. package/.output/server/_libs/radix-ui__react-presence.mjs +0 -173
  268. package/.output/server/_libs/radix-ui__react-primitive.mjs +0 -42
  269. package/.output/server/_libs/radix-ui__react-progress.mjs +0 -1
  270. package/.output/server/_libs/radix-ui__react-radio-group.mjs +0 -1
  271. package/.output/server/_libs/radix-ui__react-roving-focus.mjs +0 -235
  272. package/.output/server/_libs/radix-ui__react-scroll-area.mjs +0 -731
  273. package/.output/server/_libs/radix-ui__react-select.mjs +0 -1207
  274. package/.output/server/_libs/radix-ui__react-separator.mjs +0 -28
  275. package/.output/server/_libs/radix-ui__react-slider.mjs +0 -1
  276. package/.output/server/_libs/radix-ui__react-slot.mjs +0 -132
  277. package/.output/server/_libs/radix-ui__react-switch.mjs +0 -1
  278. package/.output/server/_libs/radix-ui__react-tabs.mjs +0 -194
  279. package/.output/server/_libs/radix-ui__react-toast.mjs +0 -2
  280. package/.output/server/_libs/radix-ui__react-toggle-group.mjs +0 -1
  281. package/.output/server/_libs/radix-ui__react-toggle.mjs +0 -1
  282. package/.output/server/_libs/radix-ui__react-toolbar.mjs +0 -1
  283. package/.output/server/_libs/radix-ui__react-tooltip.mjs +0 -498
  284. package/.output/server/_libs/radix-ui__react-use-previous.mjs +0 -14
  285. package/.output/server/_libs/radix-ui__react-use-size.mjs +0 -39
  286. package/.output/server/_libs/react-markdown.mjs +0 -235
  287. package/.output/server/_libs/react-remove-scroll-bar.mjs +0 -82
  288. package/.output/server/_libs/react-remove-scroll.mjs +0 -328
  289. package/.output/server/_libs/react-style-singleton.mjs +0 -69
  290. package/.output/server/_libs/remark-parse.mjs +0 -19
  291. package/.output/server/_libs/remark-rehype.mjs +0 -21
  292. package/.output/server/_libs/space-separated-tokens.mjs +0 -6
  293. package/.output/server/_libs/style-to-js.mjs +0 -72
  294. package/.output/server/_libs/style-to-object.mjs +0 -38
  295. package/.output/server/_libs/swr.mjs +0 -947
  296. package/.output/server/_libs/tailwind-merge.mjs +0 -3255
  297. package/.output/server/_libs/tanstack__react-virtual.mjs +0 -130
  298. package/.output/server/_libs/tanstack__virtual-core.mjs +0 -1319
  299. package/.output/server/_libs/three.mjs +0 -26016
  300. package/.output/server/_libs/trim-lines.mjs +0 -41
  301. package/.output/server/_libs/trough.mjs +0 -85
  302. package/.output/server/_libs/tslib.mjs +0 -1
  303. package/.output/server/_libs/ungap__structured-clone.mjs +0 -212
  304. package/.output/server/_libs/unified.mjs +0 -661
  305. package/.output/server/_libs/unist-util-is.mjs +0 -100
  306. package/.output/server/_libs/unist-util-position.mjs +0 -27
  307. package/.output/server/_libs/unist-util-stringify-position.mjs +0 -27
  308. package/.output/server/_libs/unist-util-visit-parents.mjs +0 -82
  309. package/.output/server/_libs/unist-util-visit.mjs +0 -24
  310. package/.output/server/_libs/use-callback-ref.mjs +0 -66
  311. package/.output/server/_libs/use-sidecar.mjs +0 -106
  312. package/.output/server/_libs/vfile-message.mjs +0 -138
  313. package/.output/server/_libs/vfile.mjs +0 -467
  314. package/.output/server/_sessionId-CJ99gZFV.mjs +0 -84
  315. package/.output/server/_sessionId-DEjScBpK.mjs +0 -146
  316. package/.output/server/_ssr/CompareDrawer-DtOIMeQB.mjs +0 -1060
  317. package/.output/server/_ssr/InspectorPet-CXc4oCBQ.mjs +0 -938
  318. package/.output/server/_ssr/ProxyViewerContainer-DVPlwfkt.mjs +0 -17632
  319. package/.output/server/_ssr/ReplayDialog-BQEA95qC.mjs +0 -725
  320. package/.output/server/_ssr/RequestAnatomy-BMyd8B7Q.mjs +0 -837
  321. package/.output/server/_ssr/ResponseView-qrTaEIM8.mjs +0 -757
  322. package/.output/server/_ssr/StreamingChunkSequence-BFTL8mIy.mjs +0 -274
  323. package/.output/server/_ssr/index-Bcj1Hcy4.mjs +0 -135
  324. package/.output/server/_ssr/index-DW-M7XUG.mjs +0 -84
  325. package/.output/server/_ssr/json-viewer-DiI0Yjsn.mjs +0 -483
  326. package/.output/server/_tanstack-start-manifest_v-C6pXXX9R.mjs +0 -4
  327. package/.output/server/node_modules/tslib/modules/index.js +0 -70
  328. package/.output/server/node_modules/tslib/modules/package.json +0 -3
  329. package/.output/server/node_modules/tslib/package.json +0 -47
  330. package/.output/server/node_modules/tslib/tslib.js +0 -484
  331. package/.output/server/package.json +0 -9
  332. package/src/routes/__root.tsx +0 -170
  333. /package/.output/{public → ui}/assets/agent-inspector-58K1_MsC.ico +0 -0
  334. /package/.output/{public → ui}/assets/alibaba-TTwafVwX.svg +0 -0
  335. /package/.output/{public → ui}/assets/minimax-BPMzvuL-.jpeg +0 -0
  336. /package/.output/{public → ui}/assets/qwen-mMn3f5ul.webp +0 -0
  337. /package/.output/{public → ui}/assets/zhipuai-BPNAnxo-.svg +0 -0
  338. /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,