@hienlh/ppm 0.23.21 → 0.23.22

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 (463) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/CLAUDE.md +3 -2
  3. package/assets/skills/ppm/SKILL.md +1 -1
  4. package/assets/skills/ppm/references/cli-reference.md +4 -4
  5. package/assets/skills/ppm/references/http-api.md +1 -1
  6. package/dist/web/assets/{accounts-settings-section-DD88DRdx.js → accounts-settings-section-CiNmIj02.js} +1 -1
  7. package/dist/web/assets/accounts-settings-section-CiNmIj02.js.br +0 -0
  8. package/dist/web/assets/accounts-settings-section-CiNmIj02.js.gz +0 -0
  9. package/dist/web/assets/{agent-session-sheet-C0PhhLKR.js → agent-session-sheet-DRR36PDA.js} +2 -2
  10. package/dist/web/assets/agent-session-sheet-DRR36PDA.js.br +0 -0
  11. package/dist/web/assets/agent-session-sheet-DRR36PDA.js.gz +0 -0
  12. package/dist/web/assets/{agent-session-window-content-D0XZgh4U.js → agent-session-window-content-DMhy16BM.js} +1 -1
  13. package/dist/web/assets/agent-session-window-content-DMhy16BM.js.br +0 -0
  14. package/dist/web/assets/agent-session-window-content-DMhy16BM.js.gz +0 -0
  15. package/dist/web/assets/{ai-resource-editor-CpfQB6A7.js → ai-resource-editor-BG8boPw7.js} +1 -1
  16. package/dist/web/assets/ai-resource-editor-BG8boPw7.js.br +0 -0
  17. package/dist/web/assets/ai-resource-editor-BG8boPw7.js.gz +0 -0
  18. package/dist/web/assets/{ai-settings-section-CQWBwWcM.js → ai-settings-section-DMGM1hYx.js} +1 -1
  19. package/dist/web/assets/ai-settings-section-DMGM1hYx.js.br +0 -0
  20. package/dist/web/assets/ai-settings-section-DMGM1hYx.js.gz +0 -0
  21. package/dist/web/assets/{ai-settings-section-DqE67XNC.js → ai-settings-section-De5dYWlC.js} +1 -1
  22. package/dist/web/assets/{android-tab-4Z4AgHz1.js → android-tab-lHDxD93G.js} +1 -1
  23. package/dist/web/assets/android-tab-lHDxD93G.js.br +0 -0
  24. package/dist/web/assets/android-tab-lHDxD93G.js.gz +0 -0
  25. package/dist/web/assets/{appearance-settings-section-DjQjiJF7.js → appearance-settings-section-DO-ZyoMj.js} +1 -1
  26. package/dist/web/assets/appearance-settings-section-DO-ZyoMj.js.br +0 -0
  27. package/dist/web/assets/appearance-settings-section-DO-ZyoMj.js.gz +0 -0
  28. package/dist/web/assets/{apps-panel-_lxY9xbc.js → apps-panel-CH-0CHYY.js} +1 -1
  29. package/dist/web/assets/apps-panel-CH-0CHYY.js.br +0 -0
  30. package/dist/web/assets/apps-panel-CH-0CHYY.js.gz +0 -0
  31. package/dist/web/assets/{branch-review-tab-BzrhnFSs.js → branch-review-tab-CdGtvekz.js} +1 -1
  32. package/dist/web/assets/branch-review-tab-CdGtvekz.js.br +0 -0
  33. package/dist/web/assets/branch-review-tab-CdGtvekz.js.gz +0 -0
  34. package/dist/web/assets/{chat-tab-BKHepkYi.js → chat-tab-DMqWnFIo.js} +1 -1
  35. package/dist/web/assets/chat-tab-DMqWnFIo.js.br +0 -0
  36. package/dist/web/assets/chat-tab-DMqWnFIo.js.gz +0 -0
  37. package/dist/web/assets/{chat-tab-Cv8gYzcA.js → chat-tab-DzWpwPFb.js} +3 -3
  38. package/dist/web/assets/chat-tab-DzWpwPFb.js.br +0 -0
  39. package/dist/web/assets/chat-tab-DzWpwPFb.js.gz +0 -0
  40. package/dist/web/assets/{code-editor-COWmNXO4.js → code-editor-BCv8ONNS.js} +4 -4
  41. package/dist/web/assets/code-editor-BCv8ONNS.js.br +0 -0
  42. package/dist/web/assets/code-editor-BCv8ONNS.js.gz +0 -0
  43. package/dist/web/assets/{codex-reset-credit-button-BHt4yJet.js → codex-reset-credit-button-CGMbIHak.js} +1 -1
  44. package/dist/web/assets/codex-reset-credit-button-CGMbIHak.js.br +0 -0
  45. package/dist/web/assets/codex-reset-credit-button-CGMbIHak.js.gz +0 -0
  46. package/dist/web/assets/{conflict-editor-tj-1gzKr.js → conflict-editor-3WmzqBlW.js} +1 -1
  47. package/dist/web/assets/conflict-editor-3WmzqBlW.js.br +0 -0
  48. package/dist/web/assets/conflict-editor-3WmzqBlW.js.gz +0 -0
  49. package/dist/web/assets/{connection-form-tab-BV2QX8Cf.js → connection-form-tab-BoWucvAe.js} +1 -1
  50. package/dist/web/assets/connection-form-tab-BoWucvAe.js.br +0 -0
  51. package/dist/web/assets/connection-form-tab-BoWucvAe.js.gz +0 -0
  52. package/dist/web/assets/{csv-preview-xhE2esgz.js → csv-preview-BCgGsy7F.js} +1 -1
  53. package/dist/web/assets/csv-preview-BCgGsy7F.js.br +0 -0
  54. package/dist/web/assets/csv-preview-BCgGsy7F.js.gz +0 -0
  55. package/dist/web/assets/{db-tab-parts-C3MsjOID.js → db-tab-parts-o6QMHBbA.js} +1 -1
  56. package/dist/web/assets/db-tab-parts-o6QMHBbA.js.br +0 -0
  57. package/dist/web/assets/db-tab-parts-o6QMHBbA.js.gz +0 -0
  58. package/dist/web/assets/{design-settings-section-C3LxYhIw.js → design-settings-section-seo6SdSk.js} +1 -1
  59. package/dist/web/assets/design-settings-section-seo6SdSk.js.br +0 -0
  60. package/dist/web/assets/design-settings-section-seo6SdSk.js.gz +0 -0
  61. package/dist/web/assets/{design-tab-DYcvjdfb.js → design-tab-B-f_b2LM.js} +1 -1
  62. package/dist/web/assets/design-tab-B-f_b2LM.js.br +0 -0
  63. package/dist/web/assets/design-tab-B-f_b2LM.js.gz +0 -0
  64. package/dist/web/assets/{designs-sidebar-panel-Dpr4PPFT.js → designs-sidebar-panel-CVT_TQP7.js} +1 -1
  65. package/dist/web/assets/designs-sidebar-panel-CVT_TQP7.js.br +0 -0
  66. package/dist/web/assets/designs-sidebar-panel-CVT_TQP7.js.gz +0 -0
  67. package/dist/web/assets/{diff-viewer-Fl_4jqr6.js → diff-viewer-y9uBp_8u.js} +1 -1
  68. package/dist/web/assets/diff-viewer-y9uBp_8u.js.br +0 -0
  69. package/dist/web/assets/diff-viewer-y9uBp_8u.js.gz +0 -0
  70. package/dist/web/assets/{docx-preview-bVE9shPX.js → docx-preview-Dn_x3Inm.js} +1 -1
  71. package/dist/web/assets/docx-preview-Dn_x3Inm.js.br +0 -0
  72. package/dist/web/assets/docx-preview-Dn_x3Inm.js.gz +0 -0
  73. package/dist/web/assets/{editor-dialog-eEOYWdbr.js → editor-dialog-BwNXSajD.js} +1 -1
  74. package/dist/web/assets/editor-dialog-BwNXSajD.js.br +0 -0
  75. package/dist/web/assets/editor-dialog-BwNXSajD.js.gz +0 -0
  76. package/dist/web/assets/{editor-language-service-D1_Ggq4s.js → editor-language-service-XA_TnPLN.js} +1 -1
  77. package/dist/web/assets/editor-language-service-XA_TnPLN.js.br +0 -0
  78. package/dist/web/assets/editor-language-service-XA_TnPLN.js.gz +0 -0
  79. package/dist/web/assets/{explorer-body-C0UOcBPJ.js → explorer-body-DukR7zM9.js} +1 -1
  80. package/dist/web/assets/explorer-body-DukR7zM9.js.br +0 -0
  81. package/dist/web/assets/explorer-body-DukR7zM9.js.gz +0 -0
  82. package/dist/web/assets/{explorer-window-content-Dxee7SuJ.js → explorer-window-content-BjTZ44S3.js} +1 -1
  83. package/dist/web/assets/explorer-window-content-BjTZ44S3.js.br +0 -0
  84. package/dist/web/assets/explorer-window-content-BjTZ44S3.js.gz +0 -0
  85. package/dist/web/assets/{extension-manager-section-D8krnoj8.js → extension-manager-section-C4WD1BPQ.js} +1 -1
  86. package/dist/web/assets/extension-manager-section-C4WD1BPQ.js.br +0 -0
  87. package/dist/web/assets/extension-manager-section-C4WD1BPQ.js.gz +0 -0
  88. package/dist/web/assets/{extension-webview-CtIdV69T.js → extension-webview-CV2ujflc.js} +1 -1
  89. package/dist/web/assets/extension-webview-CV2ujflc.js.br +0 -0
  90. package/dist/web/assets/extension-webview-CV2ujflc.js.gz +0 -0
  91. package/dist/web/assets/{files-settings-section-C8vtarR8.js → files-settings-section-hS5ixQ8o.js} +1 -1
  92. package/dist/web/assets/files-settings-section-hS5ixQ8o.js.br +0 -0
  93. package/dist/web/assets/files-settings-section-hS5ixQ8o.js.gz +0 -0
  94. package/dist/web/assets/{general-settings-section-Chu8Ut8h.js → general-settings-section-BnKYY0AA.js} +1 -1
  95. package/dist/web/assets/general-settings-section-BnKYY0AA.js.br +0 -0
  96. package/dist/web/assets/general-settings-section-BnKYY0AA.js.gz +0 -0
  97. package/dist/web/assets/{git-log-panel-DG09p6k5.js → git-log-panel-JiWfR5xv.js} +1 -1
  98. package/dist/web/assets/git-log-panel-JiWfR5xv.js.br +0 -0
  99. package/dist/web/assets/git-log-panel-JiWfR5xv.js.gz +0 -0
  100. package/dist/web/assets/{git-review-tab-CSy8DjSC.js → git-review-tab-DgvcY__Q.js} +1 -1
  101. package/dist/web/assets/git-review-tab-DgvcY__Q.js.br +0 -0
  102. package/dist/web/assets/git-review-tab-DgvcY__Q.js.gz +0 -0
  103. package/dist/web/assets/{glide-data-grid-BN1y4BT0.js → glide-data-grid-r1MKxDjz.js} +1 -1
  104. package/dist/web/assets/glide-data-grid-r1MKxDjz.js.br +0 -0
  105. package/dist/web/assets/glide-data-grid-r1MKxDjz.js.gz +0 -0
  106. package/dist/web/assets/{grid-save-dialog-CfSuWw2t.js → grid-save-dialog-Fa0yA-Ep.js} +1 -1
  107. package/dist/web/assets/grid-save-dialog-Fa0yA-Ep.js.br +0 -0
  108. package/dist/web/assets/grid-save-dialog-Fa0yA-Ep.js.gz +0 -0
  109. package/dist/web/assets/{group-chat-tab-B04QMp7A.js → group-chat-tab-B2Q8yqSX.js} +1 -1
  110. package/dist/web/assets/group-chat-tab-B2Q8yqSX.js.br +0 -0
  111. package/dist/web/assets/group-chat-tab-B2Q8yqSX.js.gz +0 -0
  112. package/dist/web/assets/{impexp-tab-BRNouquR.js → impexp-tab-B8Ht4l32.js} +1 -1
  113. package/dist/web/assets/impexp-tab-B8Ht4l32.js.br +0 -0
  114. package/dist/web/assets/impexp-tab-B8Ht4l32.js.gz +0 -0
  115. package/dist/web/assets/{index-BpgSIM5B.js → index-BtAn_krv.js} +5 -5
  116. package/dist/web/assets/index-BtAn_krv.js.br +0 -0
  117. package/dist/web/assets/index-BtAn_krv.js.gz +0 -0
  118. package/dist/web/assets/{jira-watcher-section-CJeS5r6D.js → jira-watcher-section--ibKZfBs.js} +1 -1
  119. package/dist/web/assets/{keybindings-store-Dvxq-vZD.js → keybindings-store-Cwxl4tHv.js} +1 -1
  120. package/dist/web/assets/{keyboard-shortcuts-section-BKDyZHhK.js → keyboard-shortcuts-section-aoiLpWwf.js} +1 -1
  121. package/dist/web/assets/keyboard-shortcuts-section-aoiLpWwf.js.br +0 -0
  122. package/dist/web/assets/keyboard-shortcuts-section-aoiLpWwf.js.gz +0 -0
  123. package/dist/web/assets/{language-servers-section-CbbkezhA.js → language-servers-section-BkGAezo6.js} +2 -2
  124. package/dist/web/assets/language-servers-section-BkGAezo6.js.br +0 -0
  125. package/dist/web/assets/language-servers-section-BkGAezo6.js.gz +0 -0
  126. package/dist/web/assets/{logs-app-Blr1-v4q.js → logs-app-DYgPtmjm.js} +1 -1
  127. package/dist/web/assets/logs-app-DYgPtmjm.js.br +0 -0
  128. package/dist/web/assets/logs-app-DYgPtmjm.js.gz +0 -0
  129. package/dist/web/assets/{logs-tab-CGPtn2tU.js → logs-tab-BbDosK63.js} +1 -1
  130. package/dist/web/assets/{logs-window-content-DAczcNmh.js → logs-window-content-CEXAyv-f.js} +1 -1
  131. package/dist/web/assets/{lsp-client-Ds3U2O52.js → lsp-client-CIcphNmU.js} +1 -1
  132. package/dist/web/assets/lsp-client-CIcphNmU.js.br +0 -0
  133. package/dist/web/assets/lsp-client-CIcphNmU.js.gz +0 -0
  134. package/dist/web/assets/{markdown-renderer-DbJVwfUH.js → markdown-renderer-BNzQeoAO.js} +1 -1
  135. package/dist/web/assets/{markdown-renderer-DzCPadpB.js → markdown-renderer-dk1w3goH.js} +1 -1
  136. package/dist/web/assets/markdown-renderer-dk1w3goH.js.br +0 -0
  137. package/dist/web/assets/markdown-renderer-dk1w3goH.js.gz +0 -0
  138. package/dist/web/assets/{mobile-explorer-sheet-BClCMXb8.js → mobile-explorer-sheet-D4lPZQZc.js} +1 -1
  139. package/dist/web/assets/mobile-explorer-sheet-D4lPZQZc.js.br +0 -0
  140. package/dist/web/assets/mobile-explorer-sheet-D4lPZQZc.js.gz +0 -0
  141. package/dist/web/assets/{new-design-dialog-HRP9H11v.js → new-design-dialog-CIq7xspW.js} +1 -1
  142. package/dist/web/assets/new-design-dialog-CIq7xspW.js.br +0 -0
  143. package/dist/web/assets/new-design-dialog-CIq7xspW.js.gz +0 -0
  144. package/dist/web/assets/{notification-store-BomzYNQj.js → notification-store-Rv36noYX.js} +1 -1
  145. package/dist/web/assets/notifications-settings-section-CoWMtXCC.js +1 -0
  146. package/dist/web/assets/notifications-settings-section-CoWMtXCC.js.br +0 -0
  147. package/dist/web/assets/notifications-settings-section-CoWMtXCC.js.gz +0 -0
  148. package/dist/web/assets/{ppmbot-settings-section-BRowIIqc.js → ppmbot-settings-section-Ct0UwA6T.js} +1 -1
  149. package/dist/web/assets/ppmbot-settings-section-Ct0UwA6T.js.br +0 -0
  150. package/dist/web/assets/ppmbot-settings-section-Ct0UwA6T.js.gz +0 -0
  151. package/dist/web/assets/{problems-panel-DdB10Ybd.js → problems-panel-D-HT-0PE.js} +1 -1
  152. package/dist/web/assets/problems-panel-D-HT-0PE.js.br +0 -0
  153. package/dist/web/assets/problems-panel-D-HT-0PE.js.gz +0 -0
  154. package/dist/web/assets/{process-table-CzLaWPe0.js → process-table-CXN0s4Pq.js} +1 -1
  155. package/dist/web/assets/process-table-CXN0s4Pq.js.br +0 -0
  156. package/dist/web/assets/process-table-CXN0s4Pq.js.gz +0 -0
  157. package/dist/web/assets/{proxy-settings-section-BCq7R5DA.js → proxy-settings-section-e6WWzP8n.js} +1 -1
  158. package/dist/web/assets/proxy-settings-section-e6WWzP8n.js.br +0 -0
  159. package/dist/web/assets/proxy-settings-section-e6WWzP8n.js.gz +0 -0
  160. package/dist/web/assets/{query-tab-DGvj_h6l.js → query-tab-C7C67t8F.js} +1 -1
  161. package/dist/web/assets/query-tab-C7C67t8F.js.br +0 -0
  162. package/dist/web/assets/{query-tab-DGvj_h6l.js.gz → query-tab-C7C67t8F.js.gz} +0 -0
  163. package/dist/web/assets/{remote-access-settings-section-BrqOoOyk.js → remote-access-settings-section-gGoATCF9.js} +1 -1
  164. package/dist/web/assets/remote-access-settings-section-gGoATCF9.js.br +0 -0
  165. package/dist/web/assets/remote-access-settings-section-gGoATCF9.js.gz +0 -0
  166. package/dist/web/assets/{remote-desktop-clipboard-notice-5r6UbdlN.js → remote-desktop-clipboard-notice-D5KjvMZx.js} +1 -1
  167. package/dist/web/assets/remote-desktop-clipboard-notice-D5KjvMZx.js.br +0 -0
  168. package/dist/web/assets/remote-desktop-clipboard-notice-D5KjvMZx.js.gz +0 -0
  169. package/dist/web/assets/{remote-desktop-mobile-sheet-B9Q4auqe.js → remote-desktop-mobile-sheet-CY_DsGXZ.js} +2 -2
  170. package/dist/web/assets/remote-desktop-mobile-sheet-CY_DsGXZ.js.br +0 -0
  171. package/dist/web/assets/remote-desktop-mobile-sheet-CY_DsGXZ.js.gz +0 -0
  172. package/dist/web/assets/{remote-desktop-mobile-view-CL-jrkA8.js → remote-desktop-mobile-view-DdpI-RF8.js} +1 -1
  173. package/dist/web/assets/remote-desktop-mobile-view-DdpI-RF8.js.br +0 -0
  174. package/dist/web/assets/remote-desktop-mobile-view-DdpI-RF8.js.gz +0 -0
  175. package/dist/web/assets/{remote-desktop-readiness-gate-DXEf2Jnz.js → remote-desktop-readiness-gate-WIyCCAv5.js} +1 -1
  176. package/dist/web/assets/remote-desktop-readiness-gate-WIyCCAv5.js.br +0 -0
  177. package/dist/web/assets/remote-desktop-readiness-gate-WIyCCAv5.js.gz +0 -0
  178. package/dist/web/assets/{remote-desktop-window-content-20P6-gKB.js → remote-desktop-window-content-BMORZLZt.js} +1 -1
  179. package/dist/web/assets/remote-desktop-window-content-BMORZLZt.js.br +0 -0
  180. package/dist/web/assets/remote-desktop-window-content-BMORZLZt.js.gz +0 -0
  181. package/dist/web/assets/{rename-object-dialog-Bs7pUsUP.js → rename-object-dialog-pIgWe_AL.js} +1 -1
  182. package/dist/web/assets/rename-object-dialog-pIgWe_AL.js.br +0 -0
  183. package/dist/web/assets/rename-object-dialog-pIgWe_AL.js.gz +0 -0
  184. package/dist/web/assets/{review-code-Bjo4sHN9.js → review-code-oTNcTYlI.js} +1 -1
  185. package/dist/web/assets/review-code-oTNcTYlI.js.br +0 -0
  186. package/dist/web/assets/review-code-oTNcTYlI.js.gz +0 -0
  187. package/dist/web/assets/{save-as-dialog-BHx0Wp2B.js → save-as-dialog-CnPLGTaL.js} +1 -1
  188. package/dist/web/assets/save-as-dialog-CnPLGTaL.js.br +0 -0
  189. package/dist/web/assets/save-as-dialog-CnPLGTaL.js.gz +0 -0
  190. package/dist/web/assets/{schedules-settings-section-C6ATYDPK.js → schedules-settings-section-CREqHG_i.js} +1 -1
  191. package/dist/web/assets/schedules-settings-section-CREqHG_i.js.br +0 -0
  192. package/dist/web/assets/schedules-settings-section-CREqHG_i.js.gz +0 -0
  193. package/dist/web/assets/{session-review-tab-BPMUyNTx.js → session-review-tab-nFVJYYJH.js} +1 -1
  194. package/dist/web/assets/session-review-tab-nFVJYYJH.js.br +0 -0
  195. package/dist/web/assets/session-review-tab-nFVJYYJH.js.gz +0 -0
  196. package/dist/web/assets/{settings-body-Dy5D6Vu7.js → settings-body-DNzH0wOQ.js} +1 -1
  197. package/dist/web/assets/settings-body-DNzH0wOQ.js.br +0 -0
  198. package/dist/web/assets/settings-body-DNzH0wOQ.js.gz +0 -0
  199. package/dist/web/assets/{settings-rows-BRoYE2To.js → settings-rows-D02_4sDY.js} +1 -1
  200. package/dist/web/assets/settings-rows-D02_4sDY.js.br +0 -0
  201. package/dist/web/assets/settings-rows-D02_4sDY.js.gz +0 -0
  202. package/dist/web/assets/settings-section-content-BYW_9mHv.js +1 -0
  203. package/dist/web/assets/{settings-section-content-essUViKu.js → settings-section-content-DWopMfmq.js} +2 -2
  204. package/dist/web/assets/settings-section-content-DWopMfmq.js.br +0 -0
  205. package/dist/web/assets/settings-section-content-DWopMfmq.js.gz +0 -0
  206. package/dist/web/assets/{settings-tab-Di1DUP1j.js → settings-tab-Cg0v5kO7.js} +1 -1
  207. package/dist/web/assets/{settings-window-content-DYvYFPWy.js → settings-window-content-CqhwxJJn.js} +1 -1
  208. package/dist/web/assets/{slash-command-picker-8da30mYP.js → slash-command-picker-Bcizxvbh.js} +1 -1
  209. package/dist/web/assets/slash-command-picker-Bcizxvbh.js.br +0 -0
  210. package/dist/web/assets/slash-command-picker-Bcizxvbh.js.gz +0 -0
  211. package/dist/web/assets/{sql-object-tab-DRIHVXuR.js → sql-object-tab-b1Z2zVBm.js} +1 -1
  212. package/dist/web/assets/sql-object-tab-b1Z2zVBm.js.br +0 -0
  213. package/dist/web/assets/sql-object-tab-b1Z2zVBm.js.gz +0 -0
  214. package/dist/web/assets/{sqlite-viewer-G-Jl2yEr.js → sqlite-viewer-DhPv28tq.js} +1 -1
  215. package/dist/web/assets/sqlite-viewer-DhPv28tq.js.br +0 -0
  216. package/dist/web/assets/sqlite-viewer-DhPv28tq.js.gz +0 -0
  217. package/dist/web/assets/{structure-save-dialog-Cwikz0we.js → structure-save-dialog-sGUpqs8D.js} +1 -1
  218. package/dist/web/assets/structure-save-dialog-sGUpqs8D.js.br +0 -0
  219. package/dist/web/assets/structure-save-dialog-sGUpqs8D.js.gz +0 -0
  220. package/dist/web/assets/{structure-tab-CWEgdZPm.js → structure-tab-Bo2OmDOc.js} +1 -1
  221. package/dist/web/assets/structure-tab-Bo2OmDOc.js.br +0 -0
  222. package/dist/web/assets/structure-tab-Bo2OmDOc.js.gz +0 -0
  223. package/dist/web/assets/{system-monitor-body-BDQm9-wb.js → system-monitor-body-C475Et9u.js} +2 -2
  224. package/dist/web/assets/system-monitor-body-C475Et9u.js.br +0 -0
  225. package/dist/web/assets/{system-monitor-body-BDQm9-wb.js.gz → system-monitor-body-C475Et9u.js.gz} +0 -0
  226. package/dist/web/assets/{system-monitor-tab-BouN21Yh.js → system-monitor-tab-Cm52yoW8.js} +1 -1
  227. package/dist/web/assets/{system-monitor-window-content-BRyWBSOb.js → system-monitor-window-content-YYsGHxZF.js} +1 -1
  228. package/dist/web/assets/{tab-host-window-content-CZ55-bTz.js → tab-host-window-content-BKI6QoDa.js} +1 -1
  229. package/dist/web/assets/tab-host-window-content-BKI6QoDa.js.br +0 -0
  230. package/dist/web/assets/tab-host-window-content-BKI6QoDa.js.gz +0 -0
  231. package/dist/web/assets/{table-tab-Bmn4UdBV.js → table-tab-D-nKWMJR.js} +1 -1
  232. package/dist/web/assets/table-tab-D-nKWMJR.js.br +0 -0
  233. package/dist/web/assets/table-tab-D-nKWMJR.js.gz +0 -0
  234. package/dist/web/assets/{telegram-bot-token-field-fdHwRf39.js → telegram-bot-token-field-BeNS2X2k.js} +1 -1
  235. package/dist/web/assets/telegram-bot-token-field-BeNS2X2k.js.br +0 -0
  236. package/dist/web/assets/telegram-bot-token-field-BeNS2X2k.js.gz +0 -0
  237. package/dist/web/assets/{terminal-tab-D-qlZXLK.js → terminal-tab-BbP753Ab.js} +1 -1
  238. package/dist/web/assets/terminal-tab-BbP753Ab.js.br +0 -0
  239. package/dist/web/assets/terminal-tab-BbP753Ab.js.gz +0 -0
  240. package/dist/web/assets/{tool-cards-BdXbQ9Fv.js → tool-cards-Dh8Ekn2W.js} +2 -2
  241. package/dist/web/assets/tool-cards-Dh8Ekn2W.js.br +0 -0
  242. package/dist/web/assets/tool-cards-Dh8Ekn2W.js.gz +0 -0
  243. package/dist/web/assets/{tools-settings-section-CcPaspbR.js → tools-settings-section-DGZRIULE.js} +1 -1
  244. package/dist/web/assets/tools-settings-section-DGZRIULE.js.br +0 -0
  245. package/dist/web/assets/tools-settings-section-DGZRIULE.js.gz +0 -0
  246. package/dist/web/assets/{use-inline-blame-Ck5XoSS3.js → use-inline-blame-BxwM-Wkj.js} +1 -1
  247. package/dist/web/assets/use-inline-blame-BxwM-Wkj.js.br +0 -0
  248. package/dist/web/assets/use-inline-blame-BxwM-Wkj.js.gz +0 -0
  249. package/dist/web/assets/{use-lsp-CGSqfFBk.js → use-lsp-BqvwRKmM.js} +1 -1
  250. package/dist/web/assets/use-lsp-BqvwRKmM.js.br +0 -0
  251. package/dist/web/assets/use-lsp-BqvwRKmM.js.gz +0 -0
  252. package/dist/web/assets/{use-monaco-theme-BW05eZS4.js → use-monaco-theme-DU0EpDHH.js} +1 -1
  253. package/dist/web/assets/{use-process-kill-DJ_f7rCO.js → use-process-kill-CVN_c4KF.js} +1 -1
  254. package/dist/web/assets/use-process-kill-CVN_c4KF.js.br +0 -0
  255. package/dist/web/assets/use-process-kill-CVN_c4KF.js.gz +0 -0
  256. package/dist/web/assets/{use-sql-schema-info-wt320fVR.js → use-sql-schema-info-C0wBmR6-.js} +1 -1
  257. package/dist/web/assets/use-sql-schema-info-C0wBmR6-.js.br +0 -0
  258. package/dist/web/assets/use-sql-schema-info-C0wBmR6-.js.gz +0 -0
  259. package/dist/web/assets/{use-websocket-CTwe74Cb.js → use-websocket-K0OUXbbY.js} +1 -1
  260. package/dist/web/assets/{web-preview-tab-D4XayUEK.js → web-preview-tab-DOgLv8Gt.js} +1 -1
  261. package/dist/web/assets/{web-preview-tab-D4XayUEK.js.br → web-preview-tab-DOgLv8Gt.js.br} +0 -0
  262. package/dist/web/assets/web-preview-tab-DOgLv8Gt.js.gz +0 -0
  263. package/dist/web/index.html +1 -1
  264. package/dist/web/index.html.br +0 -0
  265. package/dist/web/index.html.gz +0 -0
  266. package/dist/web/sw.js +1 -1
  267. package/dist/web/sw.js.br +0 -0
  268. package/dist/web/sw.js.gz +0 -0
  269. package/docs/deployment-guide.md +39 -0
  270. package/package.json +1 -1
  271. package/scripts/mem-diag-report.ts +31 -0
  272. package/src/providers/registry.ts +10 -1
  273. package/src/server/index.ts +7 -6
  274. package/src/server/middleware/access-log.ts +3 -0
  275. package/src/server/middleware/cors-headers.ts +44 -0
  276. package/src/server/routes/chat.ts +4 -1
  277. package/src/server/routes/database-impexp.ts +1 -1
  278. package/src/server/routes/design-preview.ts +2 -2
  279. package/src/server/ws/chat.ts +3 -0
  280. package/src/services/compact-segment.ts +14 -19
  281. package/src/services/database/grid-export-tickets.ts +6 -2
  282. package/src/services/database/impexp/readers/file-text.ts +6 -3
  283. package/src/services/edge-forwarder.ts +12 -0
  284. package/src/services/file-chunks.ts +39 -0
  285. package/src/services/file-lines.ts +10 -17
  286. package/src/services/lsp/lsp-manager.ts +3 -0
  287. package/src/services/memory-diagnostics-native.ts +110 -0
  288. package/src/services/memory-diagnostics-report.ts +200 -0
  289. package/src/services/memory-diagnostics-requests.ts +95 -0
  290. package/src/services/memory-diagnostics-spawns.ts +242 -0
  291. package/src/services/memory-diagnostics.ts +121 -23
  292. package/src/services/memory-watchdog.ts +185 -0
  293. package/src/services/nested-subagent-spy.ts +3 -0
  294. package/src/services/process-thread-snapshot.ts +12 -2
  295. package/src/services/process-tree-memory.ts +161 -0
  296. package/src/services/read-lines.ts +16 -27
  297. package/src/services/supervisor.ts +1 -0
  298. package/src/services/terminal.service.ts +8 -0
  299. package/src/services/transcript-images-file.ts +7 -11
  300. package/src/web/components/settings/notifications-settings-section.tsx +14 -1
  301. package/src/web/lib/web-push-client.ts +35 -5
  302. package/src/web/lib/web-push-support.ts +57 -4
  303. package/dist/web/assets/accounts-settings-section-DD88DRdx.js.br +0 -0
  304. package/dist/web/assets/accounts-settings-section-DD88DRdx.js.gz +0 -0
  305. package/dist/web/assets/agent-session-sheet-C0PhhLKR.js.br +0 -0
  306. package/dist/web/assets/agent-session-sheet-C0PhhLKR.js.gz +0 -0
  307. package/dist/web/assets/agent-session-window-content-D0XZgh4U.js.br +0 -0
  308. package/dist/web/assets/agent-session-window-content-D0XZgh4U.js.gz +0 -0
  309. package/dist/web/assets/ai-resource-editor-CpfQB6A7.js.br +0 -0
  310. package/dist/web/assets/ai-resource-editor-CpfQB6A7.js.gz +0 -0
  311. package/dist/web/assets/ai-settings-section-CQWBwWcM.js.br +0 -0
  312. package/dist/web/assets/ai-settings-section-CQWBwWcM.js.gz +0 -0
  313. package/dist/web/assets/android-tab-4Z4AgHz1.js.br +0 -0
  314. package/dist/web/assets/android-tab-4Z4AgHz1.js.gz +0 -0
  315. package/dist/web/assets/appearance-settings-section-DjQjiJF7.js.br +0 -0
  316. package/dist/web/assets/appearance-settings-section-DjQjiJF7.js.gz +0 -0
  317. package/dist/web/assets/apps-panel-_lxY9xbc.js.br +0 -0
  318. package/dist/web/assets/apps-panel-_lxY9xbc.js.gz +0 -0
  319. package/dist/web/assets/branch-review-tab-BzrhnFSs.js.br +0 -0
  320. package/dist/web/assets/branch-review-tab-BzrhnFSs.js.gz +0 -0
  321. package/dist/web/assets/chat-tab-BKHepkYi.js.br +0 -0
  322. package/dist/web/assets/chat-tab-BKHepkYi.js.gz +0 -0
  323. package/dist/web/assets/chat-tab-Cv8gYzcA.js.br +0 -0
  324. package/dist/web/assets/chat-tab-Cv8gYzcA.js.gz +0 -0
  325. package/dist/web/assets/code-editor-COWmNXO4.js.br +0 -0
  326. package/dist/web/assets/code-editor-COWmNXO4.js.gz +0 -0
  327. package/dist/web/assets/codex-reset-credit-button-BHt4yJet.js.br +0 -0
  328. package/dist/web/assets/codex-reset-credit-button-BHt4yJet.js.gz +0 -0
  329. package/dist/web/assets/conflict-editor-tj-1gzKr.js.br +0 -0
  330. package/dist/web/assets/conflict-editor-tj-1gzKr.js.gz +0 -0
  331. package/dist/web/assets/connection-form-tab-BV2QX8Cf.js.br +0 -0
  332. package/dist/web/assets/connection-form-tab-BV2QX8Cf.js.gz +0 -0
  333. package/dist/web/assets/csv-preview-xhE2esgz.js.br +0 -0
  334. package/dist/web/assets/csv-preview-xhE2esgz.js.gz +0 -0
  335. package/dist/web/assets/db-tab-parts-C3MsjOID.js.br +0 -0
  336. package/dist/web/assets/db-tab-parts-C3MsjOID.js.gz +0 -0
  337. package/dist/web/assets/design-settings-section-C3LxYhIw.js.br +0 -0
  338. package/dist/web/assets/design-settings-section-C3LxYhIw.js.gz +0 -0
  339. package/dist/web/assets/design-tab-DYcvjdfb.js.br +0 -0
  340. package/dist/web/assets/design-tab-DYcvjdfb.js.gz +0 -0
  341. package/dist/web/assets/designs-sidebar-panel-Dpr4PPFT.js.br +0 -0
  342. package/dist/web/assets/designs-sidebar-panel-Dpr4PPFT.js.gz +0 -0
  343. package/dist/web/assets/diff-viewer-Fl_4jqr6.js.br +0 -0
  344. package/dist/web/assets/diff-viewer-Fl_4jqr6.js.gz +0 -0
  345. package/dist/web/assets/docx-preview-bVE9shPX.js.br +0 -0
  346. package/dist/web/assets/docx-preview-bVE9shPX.js.gz +0 -0
  347. package/dist/web/assets/editor-dialog-eEOYWdbr.js.br +0 -0
  348. package/dist/web/assets/editor-dialog-eEOYWdbr.js.gz +0 -0
  349. package/dist/web/assets/editor-language-service-D1_Ggq4s.js.br +0 -0
  350. package/dist/web/assets/editor-language-service-D1_Ggq4s.js.gz +0 -0
  351. package/dist/web/assets/explorer-body-C0UOcBPJ.js.br +0 -0
  352. package/dist/web/assets/explorer-body-C0UOcBPJ.js.gz +0 -0
  353. package/dist/web/assets/explorer-window-content-Dxee7SuJ.js.br +0 -0
  354. package/dist/web/assets/explorer-window-content-Dxee7SuJ.js.gz +0 -0
  355. package/dist/web/assets/extension-manager-section-D8krnoj8.js.br +0 -0
  356. package/dist/web/assets/extension-manager-section-D8krnoj8.js.gz +0 -0
  357. package/dist/web/assets/extension-webview-CtIdV69T.js.br +0 -0
  358. package/dist/web/assets/extension-webview-CtIdV69T.js.gz +0 -0
  359. package/dist/web/assets/files-settings-section-C8vtarR8.js.br +0 -0
  360. package/dist/web/assets/files-settings-section-C8vtarR8.js.gz +0 -0
  361. package/dist/web/assets/general-settings-section-Chu8Ut8h.js.br +0 -0
  362. package/dist/web/assets/general-settings-section-Chu8Ut8h.js.gz +0 -0
  363. package/dist/web/assets/git-log-panel-DG09p6k5.js.br +0 -0
  364. package/dist/web/assets/git-log-panel-DG09p6k5.js.gz +0 -0
  365. package/dist/web/assets/git-review-tab-CSy8DjSC.js.br +0 -0
  366. package/dist/web/assets/git-review-tab-CSy8DjSC.js.gz +0 -0
  367. package/dist/web/assets/glide-data-grid-BN1y4BT0.js.br +0 -0
  368. package/dist/web/assets/glide-data-grid-BN1y4BT0.js.gz +0 -0
  369. package/dist/web/assets/grid-save-dialog-CfSuWw2t.js.br +0 -0
  370. package/dist/web/assets/grid-save-dialog-CfSuWw2t.js.gz +0 -0
  371. package/dist/web/assets/group-chat-tab-B04QMp7A.js.br +0 -0
  372. package/dist/web/assets/group-chat-tab-B04QMp7A.js.gz +0 -0
  373. package/dist/web/assets/impexp-tab-BRNouquR.js.br +0 -0
  374. package/dist/web/assets/impexp-tab-BRNouquR.js.gz +0 -0
  375. package/dist/web/assets/index-BpgSIM5B.js.br +0 -0
  376. package/dist/web/assets/index-BpgSIM5B.js.gz +0 -0
  377. package/dist/web/assets/keyboard-shortcuts-section-BKDyZHhK.js.br +0 -0
  378. package/dist/web/assets/keyboard-shortcuts-section-BKDyZHhK.js.gz +0 -0
  379. package/dist/web/assets/language-servers-section-CbbkezhA.js.br +0 -0
  380. package/dist/web/assets/language-servers-section-CbbkezhA.js.gz +0 -0
  381. package/dist/web/assets/logs-app-Blr1-v4q.js.br +0 -0
  382. package/dist/web/assets/logs-app-Blr1-v4q.js.gz +0 -0
  383. package/dist/web/assets/lsp-client-Ds3U2O52.js.br +0 -0
  384. package/dist/web/assets/lsp-client-Ds3U2O52.js.gz +0 -0
  385. package/dist/web/assets/markdown-renderer-DzCPadpB.js.br +0 -0
  386. package/dist/web/assets/markdown-renderer-DzCPadpB.js.gz +0 -0
  387. package/dist/web/assets/mobile-explorer-sheet-BClCMXb8.js.br +0 -0
  388. package/dist/web/assets/mobile-explorer-sheet-BClCMXb8.js.gz +0 -0
  389. package/dist/web/assets/new-design-dialog-HRP9H11v.js.br +0 -0
  390. package/dist/web/assets/new-design-dialog-HRP9H11v.js.gz +0 -0
  391. package/dist/web/assets/notifications-settings-section-CYTTQaNr.js +0 -1
  392. package/dist/web/assets/notifications-settings-section-CYTTQaNr.js.br +0 -0
  393. package/dist/web/assets/notifications-settings-section-CYTTQaNr.js.gz +0 -0
  394. package/dist/web/assets/ppmbot-settings-section-BRowIIqc.js.br +0 -0
  395. package/dist/web/assets/ppmbot-settings-section-BRowIIqc.js.gz +0 -0
  396. package/dist/web/assets/problems-panel-DdB10Ybd.js.br +0 -0
  397. package/dist/web/assets/problems-panel-DdB10Ybd.js.gz +0 -0
  398. package/dist/web/assets/process-table-CzLaWPe0.js.br +0 -0
  399. package/dist/web/assets/process-table-CzLaWPe0.js.gz +0 -0
  400. package/dist/web/assets/proxy-settings-section-BCq7R5DA.js.br +0 -0
  401. package/dist/web/assets/proxy-settings-section-BCq7R5DA.js.gz +0 -0
  402. package/dist/web/assets/query-tab-DGvj_h6l.js.br +0 -0
  403. package/dist/web/assets/remote-access-settings-section-BrqOoOyk.js.br +0 -0
  404. package/dist/web/assets/remote-access-settings-section-BrqOoOyk.js.gz +0 -0
  405. package/dist/web/assets/remote-desktop-clipboard-notice-5r6UbdlN.js.br +0 -0
  406. package/dist/web/assets/remote-desktop-clipboard-notice-5r6UbdlN.js.gz +0 -0
  407. package/dist/web/assets/remote-desktop-mobile-sheet-B9Q4auqe.js.br +0 -0
  408. package/dist/web/assets/remote-desktop-mobile-sheet-B9Q4auqe.js.gz +0 -0
  409. package/dist/web/assets/remote-desktop-mobile-view-CL-jrkA8.js.br +0 -0
  410. package/dist/web/assets/remote-desktop-mobile-view-CL-jrkA8.js.gz +0 -0
  411. package/dist/web/assets/remote-desktop-readiness-gate-DXEf2Jnz.js.br +0 -0
  412. package/dist/web/assets/remote-desktop-readiness-gate-DXEf2Jnz.js.gz +0 -0
  413. package/dist/web/assets/remote-desktop-window-content-20P6-gKB.js.br +0 -0
  414. package/dist/web/assets/remote-desktop-window-content-20P6-gKB.js.gz +0 -0
  415. package/dist/web/assets/rename-object-dialog-Bs7pUsUP.js.br +0 -0
  416. package/dist/web/assets/rename-object-dialog-Bs7pUsUP.js.gz +0 -0
  417. package/dist/web/assets/review-code-Bjo4sHN9.js.br +0 -0
  418. package/dist/web/assets/review-code-Bjo4sHN9.js.gz +0 -0
  419. package/dist/web/assets/save-as-dialog-BHx0Wp2B.js.br +0 -0
  420. package/dist/web/assets/save-as-dialog-BHx0Wp2B.js.gz +0 -0
  421. package/dist/web/assets/schedules-settings-section-C6ATYDPK.js.br +0 -0
  422. package/dist/web/assets/schedules-settings-section-C6ATYDPK.js.gz +0 -0
  423. package/dist/web/assets/session-review-tab-BPMUyNTx.js.br +0 -0
  424. package/dist/web/assets/session-review-tab-BPMUyNTx.js.gz +0 -0
  425. package/dist/web/assets/settings-body-Dy5D6Vu7.js.br +0 -0
  426. package/dist/web/assets/settings-body-Dy5D6Vu7.js.gz +0 -0
  427. package/dist/web/assets/settings-rows-BRoYE2To.js.br +0 -0
  428. package/dist/web/assets/settings-rows-BRoYE2To.js.gz +0 -0
  429. package/dist/web/assets/settings-section-content-C4DX7TYv.js +0 -1
  430. package/dist/web/assets/settings-section-content-essUViKu.js.br +0 -0
  431. package/dist/web/assets/settings-section-content-essUViKu.js.gz +0 -0
  432. package/dist/web/assets/slash-command-picker-8da30mYP.js.br +0 -0
  433. package/dist/web/assets/slash-command-picker-8da30mYP.js.gz +0 -0
  434. package/dist/web/assets/sql-object-tab-DRIHVXuR.js.br +0 -0
  435. package/dist/web/assets/sql-object-tab-DRIHVXuR.js.gz +0 -0
  436. package/dist/web/assets/sqlite-viewer-G-Jl2yEr.js.br +0 -0
  437. package/dist/web/assets/sqlite-viewer-G-Jl2yEr.js.gz +0 -0
  438. package/dist/web/assets/structure-save-dialog-Cwikz0we.js.br +0 -0
  439. package/dist/web/assets/structure-save-dialog-Cwikz0we.js.gz +0 -0
  440. package/dist/web/assets/structure-tab-CWEgdZPm.js.br +0 -0
  441. package/dist/web/assets/structure-tab-CWEgdZPm.js.gz +0 -0
  442. package/dist/web/assets/system-monitor-body-BDQm9-wb.js.br +0 -0
  443. package/dist/web/assets/tab-host-window-content-CZ55-bTz.js.br +0 -0
  444. package/dist/web/assets/tab-host-window-content-CZ55-bTz.js.gz +0 -0
  445. package/dist/web/assets/table-tab-Bmn4UdBV.js.br +0 -0
  446. package/dist/web/assets/table-tab-Bmn4UdBV.js.gz +0 -0
  447. package/dist/web/assets/telegram-bot-token-field-fdHwRf39.js.br +0 -0
  448. package/dist/web/assets/telegram-bot-token-field-fdHwRf39.js.gz +0 -0
  449. package/dist/web/assets/terminal-tab-D-qlZXLK.js.br +0 -0
  450. package/dist/web/assets/terminal-tab-D-qlZXLK.js.gz +0 -0
  451. package/dist/web/assets/tool-cards-BdXbQ9Fv.js.br +0 -0
  452. package/dist/web/assets/tool-cards-BdXbQ9Fv.js.gz +0 -0
  453. package/dist/web/assets/tools-settings-section-CcPaspbR.js.br +0 -0
  454. package/dist/web/assets/tools-settings-section-CcPaspbR.js.gz +0 -0
  455. package/dist/web/assets/use-inline-blame-Ck5XoSS3.js.br +0 -0
  456. package/dist/web/assets/use-inline-blame-Ck5XoSS3.js.gz +0 -0
  457. package/dist/web/assets/use-lsp-CGSqfFBk.js.br +0 -0
  458. package/dist/web/assets/use-lsp-CGSqfFBk.js.gz +0 -0
  459. package/dist/web/assets/use-process-kill-DJ_f7rCO.js.br +0 -0
  460. package/dist/web/assets/use-process-kill-DJ_f7rCO.js.gz +0 -0
  461. package/dist/web/assets/use-sql-schema-info-wt320fVR.js.br +0 -0
  462. package/dist/web/assets/use-sql-schema-info-wt320fVR.js.gz +0 -0
  463. package/dist/web/assets/web-preview-tab-D4XayUEK.js.gz +0 -0
@@ -0,0 +1,44 @@
1
+ import type { MiddlewareHandler } from "hono";
2
+
3
+ /**
4
+ * The app-wide CORS headers, written onto the handler's own response AFTER it is made.
5
+ *
6
+ * Hono's `cors()` sets its headers on `c.res` before `next()`, which makes Hono build a
7
+ * placeholder response; when the handler then returns its own, the `c.res` setter rebuilds it
8
+ * as `new Response(res.body, res)` to merge the two. Reading `.body` turns a `Bun.file(...)`
9
+ * body into Bun's file stream — the one that keeps about the file's size in native memory per
10
+ * response and never returns it (see `services/file-chunks.ts`). Measured on Bun 1.3.10
11
+ * (Windows), a 23 MB file: 2.9 MB per response bare, 30.2 MB behind `cors()`, and the
12
+ * response lost its `Content-Length` (sent chunked). Every static asset, chat image and
13
+ * download went through that.
14
+ *
15
+ * Same answer as `cors()` with its defaults (`origin: "*"`), with one difference: a route that
16
+ * sets `Access-Control-Allow-Origin` itself keeps it (`cors()` overwrote it, which is why the
17
+ * design preview had to re-assert `null`).
18
+ */
19
+ const ALLOW_METHODS = "GET,HEAD,PUT,POST,DELETE,PATCH";
20
+
21
+ export const corsHeaders: MiddlewareHandler = async (c, next) => {
22
+ if (c.req.method === "OPTIONS") {
23
+ // A preflight never reaches a handler, so answering it here rebuilds nothing.
24
+ const headers = new Headers({ "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": ALLOW_METHODS });
25
+ const requested = c.req.header("Access-Control-Request-Headers");
26
+ if (requested) {
27
+ headers.set("Access-Control-Allow-Headers", requested.split(/\s*,\s*/).join(","));
28
+ headers.append("Vary", "Access-Control-Request-Headers");
29
+ }
30
+ return new Response(null, { status: 204, statusText: "No Content", headers });
31
+ }
32
+
33
+ await next();
34
+ if (c.res.headers.has("Access-Control-Allow-Origin")) return;
35
+ try {
36
+ c.res.headers.set("Access-Control-Allow-Origin", "*");
37
+ } catch {
38
+ // A response whose headers are immutable (one passed through from `fetch`) is rebuilt;
39
+ // its body is a stream already, so there is no file to turn into one.
40
+ const res = new Response(c.res.body, c.res);
41
+ res.headers.set("Access-Control-Allow-Origin", "*");
42
+ c.res = res;
43
+ }
44
+ };
@@ -1277,7 +1277,10 @@ chatRoutes.get("/uploads/:filename", async (c) => {
1277
1277
  if (c.req.header("if-none-match") === etag) {
1278
1278
  return new Response(null, { status: 304, headers: { ETag: etag } });
1279
1279
  }
1280
- return new Response(file.stream(), {
1280
+ // The file itself, not `file.stream()`: that stream keeps about the file's size in native
1281
+ // memory per response and never returns it (see `file-chunks.ts`), and chat images are
1282
+ // served every time a tab opens without them cached.
1283
+ return new Response(file, {
1281
1284
  headers: {
1282
1285
  "Content-Type": file.type || "application/octet-stream",
1283
1286
  "Cache-Control": "public, max-age=31536000, immutable",
@@ -310,7 +310,7 @@ impExpJobRoutes.post("/jobs/:id/download", async (c) => {
310
310
  const ticket = issueExportTicket({
311
311
  fileName: file.name,
312
312
  contentType: file.format === "zip" ? "application/zip" : CONTENT_TYPES[file.format],
313
- open: () => Bun.file(file.path).stream(),
313
+ open: () => Bun.file(file.path),
314
314
  abandon: () => {},
315
315
  });
316
316
  return c.json(ok<GridExportTicket>({ ticket, fileName: file.name }));
@@ -51,8 +51,8 @@ export function createDesignPreviewRoutes(now: () => number = Date.now) {
51
51
  const api = new Hono();
52
52
  const content = new Hono();
53
53
 
54
- // The app's global `cors()` answers `*`, and Hono copies headers set before a handler over
55
- // the handler's own response, so the value set below would be lost. Re-assert it after.
54
+ // Every response here must say `null`, error pages included; asserted after the handler so
55
+ // it holds whatever an inner layer set. The app's CORS layer leaves an existing value alone.
56
56
  content.use("*", async (c, next) => {
57
57
  await next();
58
58
  c.res.headers.set("Access-Control-Allow-Origin", "null");
@@ -1196,6 +1196,9 @@ async function startSessionConsumer(sessionId: string, providerId: string, conte
1196
1196
  const stopped = turnStop ? describeTurnStop(turnStop) : null;
1197
1197
 
1198
1198
  const finalText = entry.finalText;
1199
+ // The consumer outlives the turn (Claude pushes a follow-up into the same stream), so
1200
+ // the next turn's answer must not start with this one's.
1201
+ entry.finalText = undefined;
1199
1202
  import("../../services/notification.service.ts").then(({ notificationService }) => {
1200
1203
  const project = entry.projectName || "Project";
1201
1204
  const session = chatService.getSession(sessionId);
@@ -19,6 +19,7 @@
19
19
  import { statSync } from "node:fs";
20
20
  import type { ChatMessage } from "../types/chat.ts";
21
21
  import { parseJsonlTranscript } from "./jsonl-transcript-parser.ts";
22
+ import { readFileChunks } from "./file-chunks.ts";
22
23
 
23
24
  /**
24
25
  * How many bytes one expand may parse.
@@ -90,26 +91,20 @@ export async function compactSegmentWindow(filePath: string, beforeUuid?: string
90
91
  // instead would copy a long record once per chunk it spans.
91
92
  const pending: Uint8Array[] = [];
92
93
  let at = 0; // file offset of the first pending byte
93
- const reader = Bun.file(filePath).stream().getReader();
94
- try {
95
- for (;;) {
96
- const { done, value } = await reader.read();
97
- if (done) break;
98
- const lastNewline = value.lastIndexOf(NEWLINE);
99
- if (lastNewline === -1) {
100
- pending.push(value);
101
- continue;
102
- }
103
- const lines = Buffer.concat([...pending, value.subarray(0, lastNewline + 1)]);
104
- const stop = scan(lines, at);
105
- if (stop !== undefined) return window(stop);
106
- at += lines.length;
107
- pending.length = 0;
108
- pending.push(value.subarray(lastNewline + 1));
94
+ // Not `Bun.file().stream()`, which keeps about the file's size in native memory per read
95
+ // (see `file-chunks.ts`). Returning mid-loop closes the file.
96
+ for await (const value of readFileChunks(filePath)) {
97
+ const lastNewline = value.lastIndexOf(NEWLINE);
98
+ if (lastNewline === -1) {
99
+ pending.push(value);
100
+ continue;
109
101
  }
110
- } finally {
111
- // Not only `releaseLock`: a stream let go of before its end keeps the file open.
112
- await reader.cancel().catch(() => {});
102
+ const lines = Buffer.concat([...pending, value.subarray(0, lastNewline + 1)]);
103
+ const stop = scan(lines, at);
104
+ if (stop !== undefined) return window(stop);
105
+ at += lines.length;
106
+ pending.length = 0;
107
+ pending.push(value.subarray(lastNewline + 1));
113
108
  }
114
109
  // A last line with no newline yet is still a record the full parse would read.
115
110
  const stop = scan(Buffer.concat(pending), at);
@@ -15,8 +15,12 @@ import { GRID_EXPORT_TICKET_TTL_MS } from "../../shared/db-grid-export.ts";
15
15
  export interface ExportDownload {
16
16
  fileName: string;
17
17
  contentType: string;
18
- /** The file, read from the database as it is sent. */
19
- open(): ReadableStream<Uint8Array>;
18
+ /**
19
+ * The file, read from the database as it is sent — or, for a file already on disk, the file
20
+ * itself: a `Bun.file` body is sent by Bun directly, where `Bun.file().stream()` keeps about the
21
+ * file's size in native memory per download (see `file-chunks.ts`).
22
+ */
23
+ open(): ReadableStream<Uint8Array> | Blob;
20
24
  /** Lets go of the rows and the connection, when the download never starts. */
21
25
  abandon(): void;
22
26
  }
@@ -3,6 +3,7 @@
3
3
  * order mark, or UTF-16 with one — what PPM's "CSV for Excel" writes. Bytes that are not text in
4
4
  * that encoding fail the read: turned into "�" they would be imported as a different value.
5
5
  */
6
+ import { readFileChunks } from "../../../file-chunks.ts";
6
7
 
7
8
  /** The encoding the first bytes name, and how many of them the mark takes. */
8
9
  function encodingOf(head: Uint8Array): { encoding: "utf-8" | "utf-16le" | "utf-16be"; skip: number } {
@@ -30,7 +31,9 @@ function notText(encoding: string): FileTextError {
30
31
 
31
32
  /** The text of the file at `path`, in pieces as it is read. Stop ends it between two pieces. */
32
33
  export async function* fileText(path: string, signal?: AbortSignal): AsyncGenerator<string> {
33
- const reader = Bun.file(path).stream().getReader();
34
+ // Not `Bun.file().stream()`, which keeps about the file's size in native memory per read
35
+ // (see `file-chunks.ts`) — an import file can be hundreds of MB.
36
+ const chunks = readFileChunks(path);
34
37
  let decoder: TextDecoder | null = null;
35
38
  let encoding = "utf-8";
36
39
  let head: Uint8Array = new Uint8Array(0);
@@ -55,7 +58,7 @@ export async function* fileText(path: string, signal?: AbortSignal): AsyncGenera
55
58
  sliceStart = performance.now();
56
59
  }
57
60
  signal?.throwIfAborted();
58
- const { done, value } = await reader.read();
61
+ const { done, value } = await chunks.next();
59
62
  if (done) break;
60
63
  let bytes: Uint8Array = value;
61
64
  if (!decoder) {
@@ -75,6 +78,6 @@ export async function* fileText(path: string, signal?: AbortSignal): AsyncGenera
75
78
  if (rest) yield rest;
76
79
  } finally {
77
80
  // Read to its end or not, the file is closed.
78
- await reader.cancel().catch(() => {});
81
+ await chunks.return(undefined).catch(() => {});
79
82
  }
80
83
  }
@@ -21,6 +21,7 @@
21
21
  import net from "node:net";
22
22
  import { resolveTargetPort, _resetTargetCache } from "./edge-target-resolver.ts";
23
23
  import { createLogger, installFileLogSink } from "./logger.ts";
24
+ import { registerMemoryGauge, startMemoryDiagnostics } from "./memory-diagnostics.ts";
24
25
 
25
26
  const log = createLogger("edge");
26
27
 
@@ -42,6 +43,12 @@ const CONNECT_WINDOW_MS = 5000;
42
43
 
43
44
  const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
44
45
 
46
+ /** Client connections open right now, and ever: the memory watchdog's view of the edge's load. */
47
+ let openConnections = 0;
48
+ let totalConnections = 0;
49
+ registerMemoryGauge("edge.openConnections", () => openConnections);
50
+ registerMemoryGauge("edge.totalConnections", () => totalConnections);
51
+
45
52
  /** At most one warning a minute about connections dropped for want of a server. */
46
53
  const DROP_WARN_INTERVAL_MS = 60_000;
47
54
  let droppedSinceWarning = 0;
@@ -201,6 +208,9 @@ export function startEdgeForwarder(
201
208
  // allowHalfOpen keeps the other direction alive when one side sends FIN —
202
209
  // long-lived chat WS and SSE streams depend on it.
203
210
  const server = net.createServer({ allowHalfOpen: true }, (client) => {
211
+ openConnections++;
212
+ totalConnections++;
213
+ client.once("close", () => { openConnections--; });
204
214
  void handleConnection(client, connectWindowMs);
205
215
  });
206
216
  server.once("error", reject);
@@ -218,6 +228,8 @@ if (process.argv.includes("__edge__")) {
218
228
  // Lines go to ppm.log with a timestamp and level like every other process's. The
219
229
  // supervisor hands the log over as stderr, so the stderr echo is skipped (see logger.ts).
220
230
  installFileLogSink({ echo: "stderr" });
231
+ // Reads memory through the OS only: it spawns nothing, so the invariant above holds.
232
+ startMemoryDiagnostics({ role: "edge" });
221
233
  const idx = process.argv.indexOf("__edge__");
222
234
  const publicPort = parseInt(process.argv[idx + 1] ?? "", 10);
223
235
  const host = process.argv[idx + 2] ?? "0.0.0.0";
@@ -0,0 +1,39 @@
1
+ import { open } from "node:fs/promises";
2
+
3
+ /**
4
+ * A file's bytes, a piece at a time, through `node:fs/promises` — never `Bun.file().stream()`.
5
+ *
6
+ * On Bun 1.3.10 (Windows), every full read through `Bun.file(path).stream()` keeps roughly the
7
+ * file's size in native memory that is never handed back: a 30 MB transcript read 40 times took
8
+ * a process from 377 MB to 1,949 MB private, 38 MB a read, with the JS heap back at 18 MB. A
9
+ * chat tab opening reads the whole transcript this way (`readCompactions`), so a server whose
10
+ * tabs reconnected all day reached 25 GB committed and the machine ran out of memory. The same
11
+ * 40 reads through a `FileHandle` stayed flat (0.1 MB a read); so did `createReadStream` and
12
+ * `readSync`. Measured as private bytes (commit charge), which RSS and the heap both hide.
13
+ *
14
+ * Each chunk is a fresh buffer, so a caller may keep it after asking for the next one. Breaking
15
+ * out of the loop closes the file (the generator's `finally`), so an early stop leaks no handle.
16
+ *
17
+ * `fromByte` is inclusive, `toByte` exclusive; both are absolute offsets.
18
+ */
19
+ export async function* readFileChunks(
20
+ filePath: string,
21
+ opts: { fromByte?: number; toByte?: number; chunkBytes?: number } = {},
22
+ ): AsyncGenerator<Uint8Array> {
23
+ const chunkBytes = opts.chunkBytes ?? 512 * 1024;
24
+ let position = Math.max(0, opts.fromByte ?? 0);
25
+ const end = opts.toByte ?? Infinity;
26
+ const handle = await open(filePath, "r");
27
+ try {
28
+ while (position < end) {
29
+ const want = Math.min(chunkBytes, end - position);
30
+ const buf = new Uint8Array(want);
31
+ const { bytesRead } = await handle.read(buf, 0, want, position);
32
+ if (bytesRead === 0) break;
33
+ position += bytesRead;
34
+ yield bytesRead === want ? buf : buf.subarray(0, bytesRead);
35
+ }
36
+ } finally {
37
+ await handle.close().catch(() => {});
38
+ }
39
+ }
@@ -15,6 +15,7 @@
15
15
  * Neither cost was bounded: both grow with the file, and both were paid on the
16
16
  * one thread that also serves every other request.
17
17
  */
18
+ import { readFileChunks } from "./file-chunks.ts";
18
19
 
19
20
  /** How much of the end of a file is read to find the last few lines. */
20
21
  export const DEFAULT_TAIL_BYTES = 64 * 1024;
@@ -89,24 +90,16 @@ export async function countLines(filePath: string): Promise<number> {
89
90
  let count = 0;
90
91
  let lastByte = -1;
91
92
  let sinceYield = 0;
92
- // `getReader()` rather than `for await`: Bun's ReadableStream is async
93
- // iterable at runtime, but the DOM typings it is declared against do not say
94
- // so, and a cast to paper over that would outlive the reason for it.
95
- const reader = Bun.file(filePath).stream().getReader();
96
- try {
97
- for (;;) {
98
- const { done, value } = await reader.read();
99
- if (done || !value) break;
100
- for (let i = 0; i < value.length; i++) if (value[i] === 0x0a) count++;
101
- if (value.length > 0) lastByte = value[value.length - 1]!;
102
- sinceYield += value.length;
103
- if (sinceYield >= YIELD_EVERY_BYTES) {
104
- sinceYield = 0;
105
- await new Promise<void>((r) => setTimeout(r, 0));
106
- }
93
+ // Not `Bun.file().stream()`, which keeps about the file's size in native
94
+ // memory per read and never returns it (see `file-chunks.ts`).
95
+ for await (const value of readFileChunks(filePath)) {
96
+ for (let i = 0; i < value.length; i++) if (value[i] === 0x0a) count++;
97
+ if (value.length > 0) lastByte = value[value.length - 1]!;
98
+ sinceYield += value.length;
99
+ if (sinceYield >= YIELD_EVERY_BYTES) {
100
+ sinceYield = 0;
101
+ await new Promise<void>((r) => setTimeout(r, 0));
107
102
  }
108
- } finally {
109
- reader.releaseLock();
110
103
  }
111
104
  if (lastByte === -1) return 0; // empty file
112
105
  if (lastByte !== 0x0a) count++; // last line carries no newline
@@ -20,6 +20,7 @@ import { resolveBunPath } from "../autostart-generator.ts";
20
20
  import { createLogger } from "../logger.ts";
21
21
  import { canInstall, lspInstallDir, rustupServerPath } from "./lsp-install.ts";
22
22
  import { LspSession, type LspSessionState } from "./lsp-session.ts";
23
+ import { registerMemoryGauge } from "../memory-diagnostics.ts";
23
24
  import {
24
25
  LANGUAGE_SERVERS,
25
26
  ancestorDirs,
@@ -595,3 +596,5 @@ export class LspManager {
595
596
  }
596
597
 
597
598
  export const lspManager = new LspManager();
599
+
600
+ registerMemoryGauge("lsp.servers", () => lspManager["entries"].size);
@@ -0,0 +1,110 @@
1
+ import { readdirSync, readFileSync } from "node:fs";
2
+
3
+ /**
4
+ * What the OS says this process holds, beyond what `process.memoryUsage()` reports.
5
+ *
6
+ * `rss` is only what is resident right now. A leak of *committed* memory — an allocator
7
+ * segment reserved and never handed back, a pipe buffer per child — can sit at 30 GB of
8
+ * private bytes while RSS reads 1–2 GB, because Windows trims the working set and pages
9
+ * the rest out. That is exactly how the server's leak looked on 20/10/2026: mem-diag said
10
+ * 1 GB, Windows' low-memory detector named the same pid at 32 GB, and every new child
11
+ * process died of memory exhaustion. Private bytes are the number that runs the machine
12
+ * out of commit, so that is the one recorded here.
13
+ *
14
+ * Every field is `undefined` when this platform cannot measure it, never `0`.
15
+ */
16
+ export interface NativeMemory {
17
+ /** Windows `PrivateUsage` (commit charge); Linux `RssAnon + VmSwap`. */
18
+ privateMB?: number;
19
+ /** Windows `PeakWorkingSetSize`; Linux `VmHWM`. */
20
+ peakRssMB?: number;
21
+ /** Linux `VmSize` — address space, mostly reservations. */
22
+ virtualMB?: number;
23
+ /** Windows kernel handles; Linux open file descriptors. Climbs with leaked pipes and children. */
24
+ handles?: number;
25
+ /** Linux only: `Threads` in `/proc/self/status`. */
26
+ threads?: number;
27
+ }
28
+
29
+ const MB = 1024 * 1024;
30
+
31
+ type Win32 = {
32
+ K32GetProcessMemoryInfo: (process: bigint, counters: number, size: number) => number;
33
+ GetProcessHandleCount: (process: bigint, count: number) => number;
34
+ };
35
+
36
+ /** `(HANDLE)-1`, the pseudo handle for the calling process. A bigint, so it crosses FFI unrounded. */
37
+ const CURRENT_PROCESS = -1n;
38
+ /** `sizeof(PROCESS_MEMORY_COUNTERS_EX)` on 64-bit Windows: two DWORDs then nine SIZE_Ts. */
39
+ const COUNTERS_SIZE = 80;
40
+ const OFFSET_PEAK_WORKING_SET = 8;
41
+ const OFFSET_PRIVATE_USAGE = 72;
42
+
43
+ let win32: Win32 | null | undefined;
44
+
45
+ async function loadWin32(): Promise<Win32 | null> {
46
+ if (win32 !== undefined) return win32;
47
+ try {
48
+ const { dlopen, FFIType } = await import("bun:ffi");
49
+ win32 = dlopen("kernel32.dll", {
50
+ K32GetProcessMemoryInfo: { args: [FFIType.i64, FFIType.ptr, FFIType.u32], returns: FFIType.i32 },
51
+ GetProcessHandleCount: { args: [FFIType.i64, FFIType.ptr], returns: FFIType.i32 },
52
+ }).symbols as unknown as Win32;
53
+ } catch {
54
+ win32 = null;
55
+ }
56
+ return win32;
57
+ }
58
+
59
+ async function readWindows(): Promise<NativeMemory> {
60
+ const lib = await loadWin32();
61
+ if (!lib) return {};
62
+ const { ptr } = await import("bun:ffi");
63
+ const out: NativeMemory = {};
64
+
65
+ const counters = new Uint8Array(COUNTERS_SIZE);
66
+ const view = new DataView(counters.buffer);
67
+ view.setUint32(0, COUNTERS_SIZE, true);
68
+ if (lib.K32GetProcessMemoryInfo(CURRENT_PROCESS, ptr(counters), COUNTERS_SIZE) !== 0) {
69
+ out.privateMB = Math.round(Number(view.getBigUint64(OFFSET_PRIVATE_USAGE, true)) / MB);
70
+ out.peakRssMB = Math.round(Number(view.getBigUint64(OFFSET_PEAK_WORKING_SET, true)) / MB);
71
+ }
72
+
73
+ const handleCount = new Uint32Array(1);
74
+ if (lib.GetProcessHandleCount(CURRENT_PROCESS, ptr(handleCount)) !== 0) out.handles = handleCount[0];
75
+ return out;
76
+ }
77
+
78
+ /** Parse the `kB` lines of `/proc/self/status` that matter here. Exported for tests. */
79
+ export function parseProcStatus(text: string): NativeMemory {
80
+ const kb = (key: string): number | undefined => {
81
+ const m = text.match(new RegExp(`^${key}:\\s+(\\d+)\\s+kB`, "m"));
82
+ return m ? Number(m[1]) : undefined;
83
+ };
84
+ const anon = kb("RssAnon");
85
+ const swap = kb("VmSwap");
86
+ const hwm = kb("VmHWM");
87
+ const size = kb("VmSize");
88
+ const threads = text.match(/^Threads:\s+(\d+)/m);
89
+ return {
90
+ privateMB: anon === undefined ? undefined : Math.round((anon + (swap ?? 0)) / 1024),
91
+ peakRssMB: hwm === undefined ? undefined : Math.round(hwm / 1024),
92
+ virtualMB: size === undefined ? undefined : Math.round(size / 1024),
93
+ threads: threads ? Number(threads[1]) : undefined,
94
+ };
95
+ }
96
+
97
+ function readLinux(): NativeMemory {
98
+ const out = parseProcStatus(readFileSync("/proc/self/status", "utf8"));
99
+ try { out.handles = readdirSync("/proc/self/fd").length; } catch { /* fd count is optional */ }
100
+ return out;
101
+ }
102
+
103
+ /** One reading of this process's native memory. Never throws; an unreadable field is left out. */
104
+ export async function readNativeMemory(platform: NodeJS.Platform = process.platform): Promise<NativeMemory> {
105
+ try {
106
+ if (platform === "win32") return await readWindows();
107
+ if (platform === "linux") return readLinux();
108
+ } catch { /* fall through: no native reading this tick */ }
109
+ return {};
110
+ }
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Turns `mem-diag.log` into a short answer to "what was growing, and what went with it".
3
+ *
4
+ * Pure: takes the log's text, returns lines to print. `scripts/mem-diag-report.ts` is the
5
+ * command around it.
6
+ *
7
+ * The attribution is deliberately simple. Within one process run, every interval has a
8
+ * private-byte delta and a count of spawns per command. For each command, a slope through
9
+ * the origin (`Σ count·delta / Σ count²`) estimates MB per spawn, and the correlation says
10
+ * whether the two actually move together. It cannot separate two commands that always run
11
+ * together, and a leak not tied to a spawn shows up as growth no command explains — which is
12
+ * then the cue to look at the gauges and `nativeOwners` trends printed below it.
13
+ */
14
+
15
+ export interface DiagLine {
16
+ t: string;
17
+ role?: string;
18
+ pid?: number;
19
+ upMin?: number;
20
+ rssMB?: number;
21
+ privateMB?: number;
22
+ privateDeltaMB?: number;
23
+ heapMB?: number;
24
+ handles?: number;
25
+ spawned?: Record<string, number>;
26
+ liveChildren?: number;
27
+ children?: { count: number; privateMB: number; byName?: Record<string, { n: number; mb: number }> };
28
+ requests?: { total: number; top: Record<string, { n: number; mb: number }> };
29
+ nativeOwners?: Record<string, number>;
30
+ gauges?: Record<string, number | string>;
31
+ alert?: string;
32
+ snapshot?: string;
33
+ }
34
+
35
+ export interface CommandAttribution {
36
+ label: string;
37
+ spawns: number;
38
+ mbPerSpawn: number;
39
+ correlation: number;
40
+ }
41
+
42
+ /** Parse the log, skipping torn or foreign lines. */
43
+ export function parseDiagLog(text: string): DiagLine[] {
44
+ const out: DiagLine[] = [];
45
+ for (const raw of text.split("\n")) {
46
+ if (!raw.trim()) continue;
47
+ try {
48
+ const obj = JSON.parse(raw) as DiagLine;
49
+ if (typeof obj.t === "string") out.push(obj);
50
+ } catch { /* a line cut by a crash mid-write */ }
51
+ }
52
+ return out;
53
+ }
54
+
55
+ /** Split samples into process runs: a new pid, or uptime going backwards, starts a run. */
56
+ export function splitRuns(lines: DiagLine[]): DiagLine[][] {
57
+ const runs: DiagLine[][] = [];
58
+ let current: DiagLine[] = [];
59
+ for (const l of lines) {
60
+ if (l.alert || l.snapshot || l.rssMB === undefined) continue;
61
+ const prev = current[current.length - 1];
62
+ const restarted = prev && ((l.pid !== undefined && prev.pid !== undefined && l.pid !== prev.pid)
63
+ || (l.upMin !== undefined && prev.upMin !== undefined && l.upMin < prev.upMin));
64
+ if (restarted) { runs.push(current); current = []; }
65
+ current.push(l);
66
+ }
67
+ if (current.length) runs.push(current);
68
+ return runs;
69
+ }
70
+
71
+ /** MB-per-spawn and correlation per command over one run's intervals. */
72
+ export function attributeGrowth(run: DiagLine[], minSpawns = 5): CommandAttribution[] {
73
+ const samples = run.filter((l) => typeof l.privateDeltaMB === "number" && l.spawned);
74
+ const labels = new Set<string>();
75
+ for (const s of samples) for (const k of Object.keys(s.spawned!)) labels.add(k);
76
+ const deltas = samples.map((s) => s.privateDeltaMB!);
77
+ const out: CommandAttribution[] = [];
78
+ for (const label of labels) {
79
+ const counts = samples.map((s) => s.spawned![label] ?? 0);
80
+ const spawns = counts.reduce((a, b) => a + b, 0);
81
+ if (spawns < minSpawns) continue;
82
+ let cd = 0, cc = 0;
83
+ counts.forEach((c, i) => { cd += c * (deltas[i] ?? 0); cc += c * c; });
84
+ out.push({ label, spawns, mbPerSpawn: cc ? cd / cc : 0, correlation: pearson(counts, deltas) });
85
+ }
86
+ return out.sort((a, b) => b.mbPerSpawn * b.spawns - a.mbPerSpawn * a.spawns);
87
+ }
88
+
89
+ function pearson(x: number[], y: number[]): number {
90
+ const n = x.length;
91
+ if (n < 3) return 0;
92
+ const mx = x.reduce((a, b) => a + b, 0) / n;
93
+ const my = y.reduce((a, b) => a + b, 0) / n;
94
+ let sxy = 0, sxx = 0, syy = 0;
95
+ x.forEach((xi, i) => {
96
+ const dx = xi - mx, dy = (y[i] ?? 0) - my;
97
+ sxy += dx * dy; sxx += dx * dx; syy += dy * dy;
98
+ });
99
+ return sxx && syy ? sxy / Math.sqrt(sxx * syy) : 0;
100
+ }
101
+
102
+ /** Numeric series that grew most between a run's first and last sample. */
103
+ function trends(first0: DiagLine, last0: DiagLine, pick: (l: DiagLine) => Record<string, number | string> | undefined): Array<[string, number, number]> {
104
+ const first = pick(first0) ?? {};
105
+ const last = pick(last0) ?? {};
106
+ const rows: Array<[string, number, number]> = [];
107
+ for (const [k, v] of Object.entries(last)) {
108
+ const a = Number(first[k] ?? 0), b = Number(v);
109
+ if (Number.isFinite(a) && Number.isFinite(b) && b !== a) rows.push([k, a, b]);
110
+ }
111
+ return rows.sort((p, q) => (q[2] - q[1]) - (p[2] - p[1]));
112
+ }
113
+
114
+ const fmt = (n: number | undefined, unit = "") => (n === undefined ? "—" : `${Math.round(n).toLocaleString("en-US")}${unit}`);
115
+
116
+ /** The whole report as printable lines. `lastRuns` limits output to the most recent runs. */
117
+ export function buildReport(text: string, lastRuns = 3): string[] {
118
+ const lines = parseDiagLog(text);
119
+ const out: string[] = [];
120
+ const runs = splitRuns(lines).slice(-lastRuns);
121
+ if (runs.length === 0) return ["No samples in the log. Is PPM_MEM_DIAG=1 set for the process?"];
122
+
123
+ for (const run of runs) {
124
+ const a = run[0], z = run[run.length - 1];
125
+ if (!a || !z) continue;
126
+ const hours = Math.max(1 / 60, (Date.parse(z.t) - Date.parse(a.t)) / 3_600_000);
127
+ out.push("", `== ${a.role ?? "server"} pid=${a.pid ?? "?"} ${a.t} → ${z.t} (${hours.toFixed(1)} h, ${run.length} samples)`);
128
+ if (a.privateMB === undefined) {
129
+ out.push(" This run predates private-byte sampling: only RSS/heap are available.");
130
+ } else {
131
+ const rate = ((z.privateMB ?? 0) - (a.privateMB ?? 0)) / hours;
132
+ out.push(` private ${fmt(a.privateMB)} → ${fmt(z.privateMB)} MB (${rate >= 0 ? "+" : ""}${fmt(rate)} MB/h)`);
133
+ // Growth the JS heap accounts for is a heap-snapshot problem, not a native one.
134
+ const heapGrowth = (z.heapMB ?? 0) - (a.heapMB ?? 0);
135
+ const privateGrowth = (z.privateMB ?? 0) - (a.privateMB ?? 0);
136
+ if (privateGrowth > 0) out.push(` JS heap explains ${fmt(Math.max(0, heapGrowth))} MB of that ${fmt(privateGrowth)} MB; the rest is native`);
137
+ }
138
+ out.push(` rss ${fmt(a.rssMB)} → ${fmt(z.rssMB)} MB heap ${fmt(a.heapMB)} → ${fmt(z.heapMB)} MB handles ${fmt(a.handles)} → ${fmt(z.handles)} live children ${fmt(a.liveChildren)} → ${fmt(z.liveChildren)}`);
139
+
140
+ if (a.children || z.children) {
141
+ const names = Object.entries(z.children?.byName ?? {}).sort((p, q) => q[1].mb - p[1].mb).slice(0, 5)
142
+ .map(([k, v]) => `${k}×${v.n} ${fmt(v.mb)} MB`).join(", ");
143
+ out.push(` children private ${fmt(a.children?.privateMB)} → ${fmt(z.children?.privateMB)} MB (${fmt(z.children?.count)} processes${names ? `: ${names}` : ""})`);
144
+ }
145
+
146
+ const routes = new Map<string, { n: number; mb: number }>();
147
+ for (const l of run) {
148
+ for (const [k, v] of Object.entries(l.requests?.top ?? {})) {
149
+ const r = routes.get(k) ?? { n: 0, mb: 0 };
150
+ r.n += v.n;
151
+ r.mb += v.mb;
152
+ routes.set(k, r);
153
+ }
154
+ }
155
+ if (routes.size) {
156
+ out.push(" Busiest routes over the run (each sample keeps its top 10):");
157
+ for (const [k, v] of [...routes].sort((p, q) => q[1].n - p[1].n).slice(0, 8)) {
158
+ out.push(` ${k.padEnd(44)} ${String(v.n).padStart(7)} requests ${v.mb.toFixed(1).padStart(8)} MB sent`);
159
+ }
160
+ }
161
+
162
+ const attribution = attributeGrowth(run);
163
+ if (attribution.length) {
164
+ out.push(" Spawns vs private growth (slope through origin; correlation -1..1):");
165
+ for (const c of attribution.slice(0, 12)) {
166
+ out.push(` ${c.label.padEnd(32)} ${String(c.spawns).padStart(6)} spawns ${c.mbPerSpawn.toFixed(1).padStart(7)} MB/spawn r=${c.correlation.toFixed(2)}`);
167
+ }
168
+ }
169
+
170
+ const jumps = run
171
+ .filter((l) => (l.privateDeltaMB ?? 0) > 0)
172
+ .sort((p, q) => (q.privateDeltaMB ?? 0) - (p.privateDeltaMB ?? 0))
173
+ .slice(0, 8);
174
+ if (jumps.length) {
175
+ out.push(" Largest single-interval jumps:");
176
+ for (const j of jumps) {
177
+ const spawned = Object.entries(j.spawned ?? {}).sort((p, q) => q[1] - p[1]).slice(0, 5).map(([k, v]) => `${k}×${v}`).join(", ");
178
+ out.push(` ${j.t} +${fmt(j.privateDeltaMB)} MB spawned: ${spawned || "none"}`);
179
+ }
180
+ }
181
+
182
+ const gaugeTrends = trends(a, z, (l) => l.gauges).slice(0, 10);
183
+ if (gaugeTrends.length) {
184
+ out.push(" Gauges (first → last):");
185
+ for (const [k, x, y] of gaugeTrends) out.push(` ${k.padEnd(32)} ${fmt(x)} → ${fmt(y)}`);
186
+ }
187
+ const ownerTrends = trends(a, z, (l) => l.nativeOwners).slice(0, 8);
188
+ if (ownerTrends.length) {
189
+ out.push(" Objects owning native memory (first → last):");
190
+ for (const [k, x, y] of ownerTrends) out.push(` ${k.padEnd(32)} ${fmt(x)} → ${fmt(y)}`);
191
+ }
192
+ }
193
+
194
+ const alerts = lines.filter((l) => l.alert);
195
+ if (alerts.length) {
196
+ out.push("", "Threshold alerts (full detail is in the log line):");
197
+ for (const al of alerts.slice(-10)) out.push(` ${al.t} ${al.role ?? "server"} pid=${al.pid} ${al.alert} ${fmt((al as { privateMB?: number }).privateMB)} MB`);
198
+ }
199
+ return out;
200
+ }