@hienlh/ppm 0.23.20 → 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 (466) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/CLAUDE.md +3 -2
  3. package/assets/skills/ppm/SKILL.md +1 -1
  4. package/assets/skills/ppm/references/http-api.md +1 -1
  5. package/dist/web/assets/{accounts-settings-section-BTga1zwH.js → accounts-settings-section-CiNmIj02.js} +1 -1
  6. package/dist/web/assets/accounts-settings-section-CiNmIj02.js.br +0 -0
  7. package/dist/web/assets/accounts-settings-section-CiNmIj02.js.gz +0 -0
  8. package/dist/web/assets/{agent-session-sheet-Q3kasbrZ.js → agent-session-sheet-DRR36PDA.js} +2 -2
  9. package/dist/web/assets/agent-session-sheet-DRR36PDA.js.br +0 -0
  10. package/dist/web/assets/agent-session-sheet-DRR36PDA.js.gz +0 -0
  11. package/dist/web/assets/{agent-session-window-content-UNuEprIK.js → agent-session-window-content-DMhy16BM.js} +1 -1
  12. package/dist/web/assets/agent-session-window-content-DMhy16BM.js.br +0 -0
  13. package/dist/web/assets/agent-session-window-content-DMhy16BM.js.gz +0 -0
  14. package/dist/web/assets/{ai-resource-editor-BEwA6Do0.js → ai-resource-editor-BG8boPw7.js} +1 -1
  15. package/dist/web/assets/ai-resource-editor-BG8boPw7.js.br +0 -0
  16. package/dist/web/assets/ai-resource-editor-BG8boPw7.js.gz +0 -0
  17. package/dist/web/assets/{ai-settings-section-DVKJPM0n.js → ai-settings-section-DMGM1hYx.js} +1 -1
  18. package/dist/web/assets/ai-settings-section-DMGM1hYx.js.br +0 -0
  19. package/dist/web/assets/ai-settings-section-DMGM1hYx.js.gz +0 -0
  20. package/dist/web/assets/{ai-settings-section-DlfY5lFT.js → ai-settings-section-De5dYWlC.js} +1 -1
  21. package/dist/web/assets/{android-tab-BNa6R9fV.js → android-tab-lHDxD93G.js} +1 -1
  22. package/dist/web/assets/android-tab-lHDxD93G.js.br +0 -0
  23. package/dist/web/assets/android-tab-lHDxD93G.js.gz +0 -0
  24. package/dist/web/assets/{appearance-settings-section-CjWFwWNn.js → appearance-settings-section-DO-ZyoMj.js} +1 -1
  25. package/dist/web/assets/appearance-settings-section-DO-ZyoMj.js.br +0 -0
  26. package/dist/web/assets/appearance-settings-section-DO-ZyoMj.js.gz +0 -0
  27. package/dist/web/assets/{apps-panel-D4xjeMN9.js → apps-panel-CH-0CHYY.js} +1 -1
  28. package/dist/web/assets/apps-panel-CH-0CHYY.js.br +0 -0
  29. package/dist/web/assets/apps-panel-CH-0CHYY.js.gz +0 -0
  30. package/dist/web/assets/{branch-review-tab-BCW5294d.js → branch-review-tab-CdGtvekz.js} +1 -1
  31. package/dist/web/assets/branch-review-tab-CdGtvekz.js.br +0 -0
  32. package/dist/web/assets/branch-review-tab-CdGtvekz.js.gz +0 -0
  33. package/dist/web/assets/{chat-tab-BG0M1D3d.js → chat-tab-DMqWnFIo.js} +1 -1
  34. package/dist/web/assets/chat-tab-DMqWnFIo.js.br +0 -0
  35. package/dist/web/assets/chat-tab-DMqWnFIo.js.gz +0 -0
  36. package/dist/web/assets/{chat-tab-DsW93LTL.js → chat-tab-DzWpwPFb.js} +4 -4
  37. package/dist/web/assets/chat-tab-DzWpwPFb.js.br +0 -0
  38. package/dist/web/assets/chat-tab-DzWpwPFb.js.gz +0 -0
  39. package/dist/web/assets/{code-editor-AxLIpK-b.js → code-editor-BCv8ONNS.js} +4 -4
  40. package/dist/web/assets/code-editor-BCv8ONNS.js.br +0 -0
  41. package/dist/web/assets/code-editor-BCv8ONNS.js.gz +0 -0
  42. package/dist/web/assets/{codex-reset-credit-button-BnRmkBZu.js → codex-reset-credit-button-CGMbIHak.js} +1 -1
  43. package/dist/web/assets/codex-reset-credit-button-CGMbIHak.js.br +0 -0
  44. package/dist/web/assets/codex-reset-credit-button-CGMbIHak.js.gz +0 -0
  45. package/dist/web/assets/{conflict-editor-BwuNUPAK.js → conflict-editor-3WmzqBlW.js} +1 -1
  46. package/dist/web/assets/conflict-editor-3WmzqBlW.js.br +0 -0
  47. package/dist/web/assets/conflict-editor-3WmzqBlW.js.gz +0 -0
  48. package/dist/web/assets/{connection-form-tab-CM2o80eB.js → connection-form-tab-BoWucvAe.js} +1 -1
  49. package/dist/web/assets/connection-form-tab-BoWucvAe.js.br +0 -0
  50. package/dist/web/assets/connection-form-tab-BoWucvAe.js.gz +0 -0
  51. package/dist/web/assets/{csv-preview-C7ErHm4U.js → csv-preview-BCgGsy7F.js} +1 -1
  52. package/dist/web/assets/csv-preview-BCgGsy7F.js.br +0 -0
  53. package/dist/web/assets/csv-preview-BCgGsy7F.js.gz +0 -0
  54. package/dist/web/assets/{db-tab-parts-C8jbkfxO.js → db-tab-parts-o6QMHBbA.js} +1 -1
  55. package/dist/web/assets/db-tab-parts-o6QMHBbA.js.br +0 -0
  56. package/dist/web/assets/db-tab-parts-o6QMHBbA.js.gz +0 -0
  57. package/dist/web/assets/{design-settings-section-BcGEY4Zf.js → design-settings-section-seo6SdSk.js} +1 -1
  58. package/dist/web/assets/design-settings-section-seo6SdSk.js.br +0 -0
  59. package/dist/web/assets/design-settings-section-seo6SdSk.js.gz +0 -0
  60. package/dist/web/assets/{design-tab-BGPYNFvy.js → design-tab-B-f_b2LM.js} +1 -1
  61. package/dist/web/assets/design-tab-B-f_b2LM.js.br +0 -0
  62. package/dist/web/assets/design-tab-B-f_b2LM.js.gz +0 -0
  63. package/dist/web/assets/{designs-sidebar-panel-BgMj7xRm.js → designs-sidebar-panel-CVT_TQP7.js} +1 -1
  64. package/dist/web/assets/designs-sidebar-panel-CVT_TQP7.js.br +0 -0
  65. package/dist/web/assets/designs-sidebar-panel-CVT_TQP7.js.gz +0 -0
  66. package/dist/web/assets/{diff-viewer-BKJkesHk.js → diff-viewer-y9uBp_8u.js} +1 -1
  67. package/dist/web/assets/diff-viewer-y9uBp_8u.js.br +0 -0
  68. package/dist/web/assets/diff-viewer-y9uBp_8u.js.gz +0 -0
  69. package/dist/web/assets/{docx-preview-DD5oD_nL.js → docx-preview-Dn_x3Inm.js} +1 -1
  70. package/dist/web/assets/docx-preview-Dn_x3Inm.js.br +0 -0
  71. package/dist/web/assets/docx-preview-Dn_x3Inm.js.gz +0 -0
  72. package/dist/web/assets/{editor-dialog--07lKJxl.js → editor-dialog-BwNXSajD.js} +1 -1
  73. package/dist/web/assets/editor-dialog-BwNXSajD.js.br +0 -0
  74. package/dist/web/assets/editor-dialog-BwNXSajD.js.gz +0 -0
  75. package/dist/web/assets/{editor-language-service-51xjO5d3.js → editor-language-service-XA_TnPLN.js} +1 -1
  76. package/dist/web/assets/editor-language-service-XA_TnPLN.js.br +0 -0
  77. package/dist/web/assets/editor-language-service-XA_TnPLN.js.gz +0 -0
  78. package/dist/web/assets/{explorer-body-BXK5SjJQ.js → explorer-body-DukR7zM9.js} +1 -1
  79. package/dist/web/assets/explorer-body-DukR7zM9.js.br +0 -0
  80. package/dist/web/assets/explorer-body-DukR7zM9.js.gz +0 -0
  81. package/dist/web/assets/{explorer-window-content-DKXcCmuX.js → explorer-window-content-BjTZ44S3.js} +1 -1
  82. package/dist/web/assets/explorer-window-content-BjTZ44S3.js.br +0 -0
  83. package/dist/web/assets/explorer-window-content-BjTZ44S3.js.gz +0 -0
  84. package/dist/web/assets/{extension-manager-section-CTpQP6te.js → extension-manager-section-C4WD1BPQ.js} +1 -1
  85. package/dist/web/assets/extension-manager-section-C4WD1BPQ.js.br +0 -0
  86. package/dist/web/assets/extension-manager-section-C4WD1BPQ.js.gz +0 -0
  87. package/dist/web/assets/{extension-webview-COT-yLrM.js → extension-webview-CV2ujflc.js} +1 -1
  88. package/dist/web/assets/extension-webview-CV2ujflc.js.br +0 -0
  89. package/dist/web/assets/extension-webview-CV2ujflc.js.gz +0 -0
  90. package/dist/web/assets/{files-settings-section-CgVR0fqi.js → files-settings-section-hS5ixQ8o.js} +1 -1
  91. package/dist/web/assets/files-settings-section-hS5ixQ8o.js.br +0 -0
  92. package/dist/web/assets/files-settings-section-hS5ixQ8o.js.gz +0 -0
  93. package/dist/web/assets/{general-settings-section-BEMIbE4Y.js → general-settings-section-BnKYY0AA.js} +1 -1
  94. package/dist/web/assets/general-settings-section-BnKYY0AA.js.br +0 -0
  95. package/dist/web/assets/general-settings-section-BnKYY0AA.js.gz +0 -0
  96. package/dist/web/assets/{git-log-panel-Cp2174iI.js → git-log-panel-JiWfR5xv.js} +1 -1
  97. package/dist/web/assets/git-log-panel-JiWfR5xv.js.br +0 -0
  98. package/dist/web/assets/git-log-panel-JiWfR5xv.js.gz +0 -0
  99. package/dist/web/assets/{git-review-tab-CpMJ0qDi.js → git-review-tab-DgvcY__Q.js} +1 -1
  100. package/dist/web/assets/git-review-tab-DgvcY__Q.js.br +0 -0
  101. package/dist/web/assets/git-review-tab-DgvcY__Q.js.gz +0 -0
  102. package/dist/web/assets/{glide-data-grid-BAhwWUhO.js → glide-data-grid-r1MKxDjz.js} +1 -1
  103. package/dist/web/assets/glide-data-grid-r1MKxDjz.js.br +0 -0
  104. package/dist/web/assets/glide-data-grid-r1MKxDjz.js.gz +0 -0
  105. package/dist/web/assets/{grid-save-dialog-DmJhaWOl.js → grid-save-dialog-Fa0yA-Ep.js} +1 -1
  106. package/dist/web/assets/grid-save-dialog-Fa0yA-Ep.js.br +0 -0
  107. package/dist/web/assets/grid-save-dialog-Fa0yA-Ep.js.gz +0 -0
  108. package/dist/web/assets/{group-chat-tab-Bq7-Yegf.js → group-chat-tab-B2Q8yqSX.js} +1 -1
  109. package/dist/web/assets/group-chat-tab-B2Q8yqSX.js.br +0 -0
  110. package/dist/web/assets/group-chat-tab-B2Q8yqSX.js.gz +0 -0
  111. package/dist/web/assets/{impexp-tab-COx3Cvs7.js → impexp-tab-B8Ht4l32.js} +1 -1
  112. package/dist/web/assets/impexp-tab-B8Ht4l32.js.br +0 -0
  113. package/dist/web/assets/impexp-tab-B8Ht4l32.js.gz +0 -0
  114. package/dist/web/assets/{index-BGk8EFQb.js → index-BtAn_krv.js} +5 -5
  115. package/dist/web/assets/index-BtAn_krv.js.br +0 -0
  116. package/dist/web/assets/index-BtAn_krv.js.gz +0 -0
  117. package/dist/web/assets/{jira-watcher-section-BEPbsi62.js → jira-watcher-section--ibKZfBs.js} +1 -1
  118. package/dist/web/assets/{keybindings-store-DzcSIJUF.js → keybindings-store-Cwxl4tHv.js} +1 -1
  119. package/dist/web/assets/{keyboard-shortcuts-section-Bqg7AZiZ.js → keyboard-shortcuts-section-aoiLpWwf.js} +1 -1
  120. package/dist/web/assets/keyboard-shortcuts-section-aoiLpWwf.js.br +0 -0
  121. package/dist/web/assets/keyboard-shortcuts-section-aoiLpWwf.js.gz +0 -0
  122. package/dist/web/assets/{language-servers-section-CJ47DSIA.js → language-servers-section-BkGAezo6.js} +2 -2
  123. package/dist/web/assets/language-servers-section-BkGAezo6.js.br +0 -0
  124. package/dist/web/assets/language-servers-section-BkGAezo6.js.gz +0 -0
  125. package/dist/web/assets/{logs-app-BK-n-y_K.js → logs-app-DYgPtmjm.js} +1 -1
  126. package/dist/web/assets/logs-app-DYgPtmjm.js.br +0 -0
  127. package/dist/web/assets/logs-app-DYgPtmjm.js.gz +0 -0
  128. package/dist/web/assets/{logs-tab-B84EZJ1b.js → logs-tab-BbDosK63.js} +1 -1
  129. package/dist/web/assets/{logs-window-content-B1uN_3pN.js → logs-window-content-CEXAyv-f.js} +1 -1
  130. package/dist/web/assets/{lsp-client-DHSaAyJW.js → lsp-client-CIcphNmU.js} +1 -1
  131. package/dist/web/assets/lsp-client-CIcphNmU.js.br +0 -0
  132. package/dist/web/assets/lsp-client-CIcphNmU.js.gz +0 -0
  133. package/dist/web/assets/{markdown-renderer-BjDQUqT-.js → markdown-renderer-BNzQeoAO.js} +1 -1
  134. package/dist/web/assets/{markdown-renderer-CpxdNG0f.js → markdown-renderer-dk1w3goH.js} +1 -1
  135. package/dist/web/assets/markdown-renderer-dk1w3goH.js.br +0 -0
  136. package/dist/web/assets/markdown-renderer-dk1w3goH.js.gz +0 -0
  137. package/dist/web/assets/{mobile-explorer-sheet-BBwXGmXA.js → mobile-explorer-sheet-D4lPZQZc.js} +1 -1
  138. package/dist/web/assets/mobile-explorer-sheet-D4lPZQZc.js.br +0 -0
  139. package/dist/web/assets/mobile-explorer-sheet-D4lPZQZc.js.gz +0 -0
  140. package/dist/web/assets/{new-design-dialog-B_ZOvGhR.js → new-design-dialog-CIq7xspW.js} +1 -1
  141. package/dist/web/assets/new-design-dialog-CIq7xspW.js.br +0 -0
  142. package/dist/web/assets/new-design-dialog-CIq7xspW.js.gz +0 -0
  143. package/dist/web/assets/{notification-store-HuMqfadD.js → notification-store-Rv36noYX.js} +1 -1
  144. package/dist/web/assets/notifications-settings-section-CoWMtXCC.js +1 -0
  145. package/dist/web/assets/notifications-settings-section-CoWMtXCC.js.br +0 -0
  146. package/dist/web/assets/notifications-settings-section-CoWMtXCC.js.gz +0 -0
  147. package/dist/web/assets/{ppmbot-settings-section-JDvyI-gZ.js → ppmbot-settings-section-Ct0UwA6T.js} +1 -1
  148. package/dist/web/assets/ppmbot-settings-section-Ct0UwA6T.js.br +0 -0
  149. package/dist/web/assets/ppmbot-settings-section-Ct0UwA6T.js.gz +0 -0
  150. package/dist/web/assets/{problems-panel-Bsv_L6T2.js → problems-panel-D-HT-0PE.js} +1 -1
  151. package/dist/web/assets/problems-panel-D-HT-0PE.js.br +0 -0
  152. package/dist/web/assets/problems-panel-D-HT-0PE.js.gz +0 -0
  153. package/dist/web/assets/{process-table-3kZyWOJx.js → process-table-CXN0s4Pq.js} +1 -1
  154. package/dist/web/assets/process-table-CXN0s4Pq.js.br +0 -0
  155. package/dist/web/assets/process-table-CXN0s4Pq.js.gz +0 -0
  156. package/dist/web/assets/{proxy-settings-section-CYcmIp7W.js → proxy-settings-section-e6WWzP8n.js} +1 -1
  157. package/dist/web/assets/proxy-settings-section-e6WWzP8n.js.br +0 -0
  158. package/dist/web/assets/proxy-settings-section-e6WWzP8n.js.gz +0 -0
  159. package/dist/web/assets/{query-tab-CjrWnQUl.js → query-tab-C7C67t8F.js} +1 -1
  160. package/dist/web/assets/query-tab-C7C67t8F.js.br +0 -0
  161. package/dist/web/assets/query-tab-C7C67t8F.js.gz +0 -0
  162. package/dist/web/assets/{remote-access-settings-section-iQkakiVx.js → remote-access-settings-section-gGoATCF9.js} +1 -1
  163. package/dist/web/assets/remote-access-settings-section-gGoATCF9.js.br +0 -0
  164. package/dist/web/assets/remote-access-settings-section-gGoATCF9.js.gz +0 -0
  165. package/dist/web/assets/{remote-desktop-clipboard-notice-CFLnb7ML.js → remote-desktop-clipboard-notice-D5KjvMZx.js} +1 -1
  166. package/dist/web/assets/remote-desktop-clipboard-notice-D5KjvMZx.js.br +0 -0
  167. package/dist/web/assets/remote-desktop-clipboard-notice-D5KjvMZx.js.gz +0 -0
  168. package/dist/web/assets/{remote-desktop-mobile-sheet-C_-_-Nkz.js → remote-desktop-mobile-sheet-CY_DsGXZ.js} +2 -2
  169. package/dist/web/assets/remote-desktop-mobile-sheet-CY_DsGXZ.js.br +0 -0
  170. package/dist/web/assets/remote-desktop-mobile-sheet-CY_DsGXZ.js.gz +0 -0
  171. package/dist/web/assets/{remote-desktop-mobile-view-BnHSrind.js → remote-desktop-mobile-view-DdpI-RF8.js} +1 -1
  172. package/dist/web/assets/remote-desktop-mobile-view-DdpI-RF8.js.br +0 -0
  173. package/dist/web/assets/remote-desktop-mobile-view-DdpI-RF8.js.gz +0 -0
  174. package/dist/web/assets/{remote-desktop-readiness-gate-BGHJ7Sv6.js → remote-desktop-readiness-gate-WIyCCAv5.js} +1 -1
  175. package/dist/web/assets/remote-desktop-readiness-gate-WIyCCAv5.js.br +0 -0
  176. package/dist/web/assets/remote-desktop-readiness-gate-WIyCCAv5.js.gz +0 -0
  177. package/dist/web/assets/{remote-desktop-window-content-BBm1YwM8.js → remote-desktop-window-content-BMORZLZt.js} +1 -1
  178. package/dist/web/assets/remote-desktop-window-content-BMORZLZt.js.br +0 -0
  179. package/dist/web/assets/remote-desktop-window-content-BMORZLZt.js.gz +0 -0
  180. package/dist/web/assets/{rename-object-dialog-BLnF9xyn.js → rename-object-dialog-pIgWe_AL.js} +1 -1
  181. package/dist/web/assets/rename-object-dialog-pIgWe_AL.js.br +0 -0
  182. package/dist/web/assets/rename-object-dialog-pIgWe_AL.js.gz +0 -0
  183. package/dist/web/assets/{review-code-DG7sCIzr.js → review-code-oTNcTYlI.js} +1 -1
  184. package/dist/web/assets/review-code-oTNcTYlI.js.br +0 -0
  185. package/dist/web/assets/review-code-oTNcTYlI.js.gz +0 -0
  186. package/dist/web/assets/{save-as-dialog-KW7cErWh.js → save-as-dialog-CnPLGTaL.js} +1 -1
  187. package/dist/web/assets/save-as-dialog-CnPLGTaL.js.br +0 -0
  188. package/dist/web/assets/save-as-dialog-CnPLGTaL.js.gz +0 -0
  189. package/dist/web/assets/{schedules-settings-section-B4AV4ORv.js → schedules-settings-section-CREqHG_i.js} +1 -1
  190. package/dist/web/assets/schedules-settings-section-CREqHG_i.js.br +0 -0
  191. package/dist/web/assets/schedules-settings-section-CREqHG_i.js.gz +0 -0
  192. package/dist/web/assets/{session-review-tab-C5nPyBzo.js → session-review-tab-nFVJYYJH.js} +1 -1
  193. package/dist/web/assets/session-review-tab-nFVJYYJH.js.br +0 -0
  194. package/dist/web/assets/session-review-tab-nFVJYYJH.js.gz +0 -0
  195. package/dist/web/assets/{settings-body-xWSmRXUD.js → settings-body-DNzH0wOQ.js} +1 -1
  196. package/dist/web/assets/settings-body-DNzH0wOQ.js.br +0 -0
  197. package/dist/web/assets/settings-body-DNzH0wOQ.js.gz +0 -0
  198. package/dist/web/assets/{settings-rows-ChKBHk07.js → settings-rows-D02_4sDY.js} +1 -1
  199. package/dist/web/assets/settings-rows-D02_4sDY.js.br +0 -0
  200. package/dist/web/assets/settings-rows-D02_4sDY.js.gz +0 -0
  201. package/dist/web/assets/settings-section-content-BYW_9mHv.js +1 -0
  202. package/dist/web/assets/{settings-section-content-DdVdyPVX.js → settings-section-content-DWopMfmq.js} +2 -2
  203. package/dist/web/assets/settings-section-content-DWopMfmq.js.br +0 -0
  204. package/dist/web/assets/settings-section-content-DWopMfmq.js.gz +0 -0
  205. package/dist/web/assets/{settings-tab-CzaBBNoj.js → settings-tab-Cg0v5kO7.js} +1 -1
  206. package/dist/web/assets/{settings-window-content-DAvWSu_O.js → settings-window-content-CqhwxJJn.js} +1 -1
  207. package/dist/web/assets/{slash-command-picker-Cn2Fdz2p.js → slash-command-picker-Bcizxvbh.js} +1 -1
  208. package/dist/web/assets/slash-command-picker-Bcizxvbh.js.br +0 -0
  209. package/dist/web/assets/slash-command-picker-Bcizxvbh.js.gz +0 -0
  210. package/dist/web/assets/{sql-object-tab-_mPRCvD9.js → sql-object-tab-b1Z2zVBm.js} +1 -1
  211. package/dist/web/assets/sql-object-tab-b1Z2zVBm.js.br +0 -0
  212. package/dist/web/assets/sql-object-tab-b1Z2zVBm.js.gz +0 -0
  213. package/dist/web/assets/{sqlite-viewer-B8VZt-mA.js → sqlite-viewer-DhPv28tq.js} +1 -1
  214. package/dist/web/assets/sqlite-viewer-DhPv28tq.js.br +0 -0
  215. package/dist/web/assets/sqlite-viewer-DhPv28tq.js.gz +0 -0
  216. package/dist/web/assets/{structure-save-dialog-Lcst7kAl.js → structure-save-dialog-sGUpqs8D.js} +1 -1
  217. package/dist/web/assets/structure-save-dialog-sGUpqs8D.js.br +0 -0
  218. package/dist/web/assets/structure-save-dialog-sGUpqs8D.js.gz +0 -0
  219. package/dist/web/assets/{structure-tab-ljJCzxUe.js → structure-tab-Bo2OmDOc.js} +1 -1
  220. package/dist/web/assets/structure-tab-Bo2OmDOc.js.br +0 -0
  221. package/dist/web/assets/structure-tab-Bo2OmDOc.js.gz +0 -0
  222. package/dist/web/assets/{system-monitor-body-CTi5xUZz.js → system-monitor-body-C475Et9u.js} +2 -2
  223. package/dist/web/assets/system-monitor-body-C475Et9u.js.br +0 -0
  224. package/dist/web/assets/system-monitor-body-C475Et9u.js.gz +0 -0
  225. package/dist/web/assets/{system-monitor-tab-DeL2YoXO.js → system-monitor-tab-Cm52yoW8.js} +1 -1
  226. package/dist/web/assets/{system-monitor-window-content-DK-rQIZh.js → system-monitor-window-content-YYsGHxZF.js} +1 -1
  227. package/dist/web/assets/{tab-host-window-content-CG0vPTc_.js → tab-host-window-content-BKI6QoDa.js} +1 -1
  228. package/dist/web/assets/tab-host-window-content-BKI6QoDa.js.br +0 -0
  229. package/dist/web/assets/tab-host-window-content-BKI6QoDa.js.gz +0 -0
  230. package/dist/web/assets/{table-tab-CTZ3nCWV.js → table-tab-D-nKWMJR.js} +1 -1
  231. package/dist/web/assets/table-tab-D-nKWMJR.js.br +0 -0
  232. package/dist/web/assets/table-tab-D-nKWMJR.js.gz +0 -0
  233. package/dist/web/assets/{telegram-bot-token-field-DF7zq7_h.js → telegram-bot-token-field-BeNS2X2k.js} +1 -1
  234. package/dist/web/assets/telegram-bot-token-field-BeNS2X2k.js.br +0 -0
  235. package/dist/web/assets/telegram-bot-token-field-BeNS2X2k.js.gz +0 -0
  236. package/dist/web/assets/{terminal-tab-B_jECWTH.js → terminal-tab-BbP753Ab.js} +1 -1
  237. package/dist/web/assets/terminal-tab-BbP753Ab.js.br +0 -0
  238. package/dist/web/assets/terminal-tab-BbP753Ab.js.gz +0 -0
  239. package/dist/web/assets/{tool-cards-CsD_VXcG.js → tool-cards-Dh8Ekn2W.js} +2 -2
  240. package/dist/web/assets/tool-cards-Dh8Ekn2W.js.br +0 -0
  241. package/dist/web/assets/tool-cards-Dh8Ekn2W.js.gz +0 -0
  242. package/dist/web/assets/{tools-settings-section-DbBr0Hnr.js → tools-settings-section-DGZRIULE.js} +1 -1
  243. package/dist/web/assets/tools-settings-section-DGZRIULE.js.br +0 -0
  244. package/dist/web/assets/tools-settings-section-DGZRIULE.js.gz +0 -0
  245. package/dist/web/assets/{use-inline-blame-BA2mrMxT.js → use-inline-blame-BxwM-Wkj.js} +1 -1
  246. package/dist/web/assets/use-inline-blame-BxwM-Wkj.js.br +0 -0
  247. package/dist/web/assets/use-inline-blame-BxwM-Wkj.js.gz +0 -0
  248. package/dist/web/assets/{use-lsp-ChbrzwvS.js → use-lsp-BqvwRKmM.js} +1 -1
  249. package/dist/web/assets/use-lsp-BqvwRKmM.js.br +0 -0
  250. package/dist/web/assets/use-lsp-BqvwRKmM.js.gz +0 -0
  251. package/dist/web/assets/{use-monaco-theme-C3_wfz5J.js → use-monaco-theme-DU0EpDHH.js} +1 -1
  252. package/dist/web/assets/{use-process-kill-C-YetnGd.js → use-process-kill-CVN_c4KF.js} +1 -1
  253. package/dist/web/assets/use-process-kill-CVN_c4KF.js.br +0 -0
  254. package/dist/web/assets/use-process-kill-CVN_c4KF.js.gz +0 -0
  255. package/dist/web/assets/{use-sql-schema-info-pKwLD9wk.js → use-sql-schema-info-C0wBmR6-.js} +1 -1
  256. package/dist/web/assets/use-sql-schema-info-C0wBmR6-.js.br +0 -0
  257. package/dist/web/assets/use-sql-schema-info-C0wBmR6-.js.gz +0 -0
  258. package/dist/web/assets/{use-websocket-BFnUq0-x.js → use-websocket-K0OUXbbY.js} +1 -1
  259. package/dist/web/assets/{web-preview-tab-DGLPuZYM.js → web-preview-tab-DOgLv8Gt.js} +1 -1
  260. package/dist/web/assets/web-preview-tab-DOgLv8Gt.js.br +0 -0
  261. package/dist/web/assets/web-preview-tab-DOgLv8Gt.js.gz +0 -0
  262. package/dist/web/index.html +1 -1
  263. package/dist/web/index.html.br +0 -0
  264. package/dist/web/index.html.gz +0 -0
  265. package/dist/web/sw.js +1 -1
  266. package/dist/web/sw.js.br +0 -0
  267. package/dist/web/sw.js.gz +0 -0
  268. package/docs/deployment-guide.md +39 -0
  269. package/package.json +1 -1
  270. package/scripts/mem-diag-report.ts +31 -0
  271. package/src/providers/registry.ts +10 -1
  272. package/src/server/index.ts +7 -6
  273. package/src/server/middleware/access-log.ts +3 -0
  274. package/src/server/middleware/cors-headers.ts +44 -0
  275. package/src/server/routes/chat.ts +4 -1
  276. package/src/server/routes/database-impexp.ts +1 -1
  277. package/src/server/routes/design-preview.ts +2 -2
  278. package/src/server/ws/chat.ts +3 -0
  279. package/src/services/compact-segment.ts +14 -19
  280. package/src/services/database/grid-export-tickets.ts +6 -2
  281. package/src/services/database/impexp/readers/file-text.ts +6 -3
  282. package/src/services/edge-forwarder.ts +12 -0
  283. package/src/services/file-chunks.ts +39 -0
  284. package/src/services/file-lines.ts +10 -17
  285. package/src/services/lsp/lsp-manager.ts +3 -0
  286. package/src/services/memory-diagnostics-native.ts +110 -0
  287. package/src/services/memory-diagnostics-report.ts +200 -0
  288. package/src/services/memory-diagnostics-requests.ts +95 -0
  289. package/src/services/memory-diagnostics-spawns.ts +242 -0
  290. package/src/services/memory-diagnostics.ts +121 -23
  291. package/src/services/memory-watchdog.ts +185 -0
  292. package/src/services/nested-subagent-spy.ts +3 -0
  293. package/src/services/process-thread-snapshot.ts +12 -2
  294. package/src/services/process-tree-memory.ts +161 -0
  295. package/src/services/read-lines.ts +16 -27
  296. package/src/services/supervisor.ts +1 -0
  297. package/src/services/terminal.service.ts +8 -0
  298. package/src/services/transcript-images-file.ts +7 -11
  299. package/src/web/components/chat/question-card.tsx +10 -4
  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-BTga1zwH.js.br +0 -0
  304. package/dist/web/assets/accounts-settings-section-BTga1zwH.js.gz +0 -0
  305. package/dist/web/assets/agent-session-sheet-Q3kasbrZ.js.br +0 -0
  306. package/dist/web/assets/agent-session-sheet-Q3kasbrZ.js.gz +0 -0
  307. package/dist/web/assets/agent-session-window-content-UNuEprIK.js.br +0 -0
  308. package/dist/web/assets/agent-session-window-content-UNuEprIK.js.gz +0 -0
  309. package/dist/web/assets/ai-resource-editor-BEwA6Do0.js.br +0 -0
  310. package/dist/web/assets/ai-resource-editor-BEwA6Do0.js.gz +0 -0
  311. package/dist/web/assets/ai-settings-section-DVKJPM0n.js.br +0 -0
  312. package/dist/web/assets/ai-settings-section-DVKJPM0n.js.gz +0 -0
  313. package/dist/web/assets/android-tab-BNa6R9fV.js.br +0 -0
  314. package/dist/web/assets/android-tab-BNa6R9fV.js.gz +0 -0
  315. package/dist/web/assets/appearance-settings-section-CjWFwWNn.js.br +0 -0
  316. package/dist/web/assets/appearance-settings-section-CjWFwWNn.js.gz +0 -0
  317. package/dist/web/assets/apps-panel-D4xjeMN9.js.br +0 -0
  318. package/dist/web/assets/apps-panel-D4xjeMN9.js.gz +0 -0
  319. package/dist/web/assets/branch-review-tab-BCW5294d.js.br +0 -0
  320. package/dist/web/assets/branch-review-tab-BCW5294d.js.gz +0 -0
  321. package/dist/web/assets/chat-tab-BG0M1D3d.js.br +0 -3
  322. package/dist/web/assets/chat-tab-BG0M1D3d.js.gz +0 -0
  323. package/dist/web/assets/chat-tab-DsW93LTL.js.br +0 -0
  324. package/dist/web/assets/chat-tab-DsW93LTL.js.gz +0 -0
  325. package/dist/web/assets/code-editor-AxLIpK-b.js.br +0 -0
  326. package/dist/web/assets/code-editor-AxLIpK-b.js.gz +0 -0
  327. package/dist/web/assets/codex-reset-credit-button-BnRmkBZu.js.br +0 -0
  328. package/dist/web/assets/codex-reset-credit-button-BnRmkBZu.js.gz +0 -0
  329. package/dist/web/assets/conflict-editor-BwuNUPAK.js.br +0 -0
  330. package/dist/web/assets/conflict-editor-BwuNUPAK.js.gz +0 -0
  331. package/dist/web/assets/connection-form-tab-CM2o80eB.js.br +0 -0
  332. package/dist/web/assets/connection-form-tab-CM2o80eB.js.gz +0 -0
  333. package/dist/web/assets/csv-preview-C7ErHm4U.js.br +0 -0
  334. package/dist/web/assets/csv-preview-C7ErHm4U.js.gz +0 -0
  335. package/dist/web/assets/db-tab-parts-C8jbkfxO.js.br +0 -0
  336. package/dist/web/assets/db-tab-parts-C8jbkfxO.js.gz +0 -0
  337. package/dist/web/assets/design-settings-section-BcGEY4Zf.js.br +0 -0
  338. package/dist/web/assets/design-settings-section-BcGEY4Zf.js.gz +0 -0
  339. package/dist/web/assets/design-tab-BGPYNFvy.js.br +0 -0
  340. package/dist/web/assets/design-tab-BGPYNFvy.js.gz +0 -0
  341. package/dist/web/assets/designs-sidebar-panel-BgMj7xRm.js.br +0 -0
  342. package/dist/web/assets/designs-sidebar-panel-BgMj7xRm.js.gz +0 -0
  343. package/dist/web/assets/diff-viewer-BKJkesHk.js.br +0 -0
  344. package/dist/web/assets/diff-viewer-BKJkesHk.js.gz +0 -0
  345. package/dist/web/assets/docx-preview-DD5oD_nL.js.br +0 -0
  346. package/dist/web/assets/docx-preview-DD5oD_nL.js.gz +0 -0
  347. package/dist/web/assets/editor-dialog--07lKJxl.js.br +0 -0
  348. package/dist/web/assets/editor-dialog--07lKJxl.js.gz +0 -0
  349. package/dist/web/assets/editor-language-service-51xjO5d3.js.br +0 -0
  350. package/dist/web/assets/editor-language-service-51xjO5d3.js.gz +0 -0
  351. package/dist/web/assets/explorer-body-BXK5SjJQ.js.br +0 -0
  352. package/dist/web/assets/explorer-body-BXK5SjJQ.js.gz +0 -0
  353. package/dist/web/assets/explorer-window-content-DKXcCmuX.js.br +0 -0
  354. package/dist/web/assets/explorer-window-content-DKXcCmuX.js.gz +0 -0
  355. package/dist/web/assets/extension-manager-section-CTpQP6te.js.br +0 -0
  356. package/dist/web/assets/extension-manager-section-CTpQP6te.js.gz +0 -0
  357. package/dist/web/assets/extension-webview-COT-yLrM.js.br +0 -0
  358. package/dist/web/assets/extension-webview-COT-yLrM.js.gz +0 -0
  359. package/dist/web/assets/files-settings-section-CgVR0fqi.js.br +0 -0
  360. package/dist/web/assets/files-settings-section-CgVR0fqi.js.gz +0 -0
  361. package/dist/web/assets/general-settings-section-BEMIbE4Y.js.br +0 -0
  362. package/dist/web/assets/general-settings-section-BEMIbE4Y.js.gz +0 -0
  363. package/dist/web/assets/git-log-panel-Cp2174iI.js.br +0 -0
  364. package/dist/web/assets/git-log-panel-Cp2174iI.js.gz +0 -0
  365. package/dist/web/assets/git-review-tab-CpMJ0qDi.js.br +0 -0
  366. package/dist/web/assets/git-review-tab-CpMJ0qDi.js.gz +0 -0
  367. package/dist/web/assets/glide-data-grid-BAhwWUhO.js.br +0 -0
  368. package/dist/web/assets/glide-data-grid-BAhwWUhO.js.gz +0 -0
  369. package/dist/web/assets/grid-save-dialog-DmJhaWOl.js.br +0 -0
  370. package/dist/web/assets/grid-save-dialog-DmJhaWOl.js.gz +0 -0
  371. package/dist/web/assets/group-chat-tab-Bq7-Yegf.js.br +0 -0
  372. package/dist/web/assets/group-chat-tab-Bq7-Yegf.js.gz +0 -0
  373. package/dist/web/assets/impexp-tab-COx3Cvs7.js.br +0 -0
  374. package/dist/web/assets/impexp-tab-COx3Cvs7.js.gz +0 -0
  375. package/dist/web/assets/index-BGk8EFQb.js.br +0 -0
  376. package/dist/web/assets/index-BGk8EFQb.js.gz +0 -0
  377. package/dist/web/assets/keyboard-shortcuts-section-Bqg7AZiZ.js.br +0 -0
  378. package/dist/web/assets/keyboard-shortcuts-section-Bqg7AZiZ.js.gz +0 -0
  379. package/dist/web/assets/language-servers-section-CJ47DSIA.js.br +0 -0
  380. package/dist/web/assets/language-servers-section-CJ47DSIA.js.gz +0 -0
  381. package/dist/web/assets/logs-app-BK-n-y_K.js.br +0 -0
  382. package/dist/web/assets/logs-app-BK-n-y_K.js.gz +0 -0
  383. package/dist/web/assets/lsp-client-DHSaAyJW.js.br +0 -0
  384. package/dist/web/assets/lsp-client-DHSaAyJW.js.gz +0 -0
  385. package/dist/web/assets/markdown-renderer-CpxdNG0f.js.br +0 -0
  386. package/dist/web/assets/markdown-renderer-CpxdNG0f.js.gz +0 -0
  387. package/dist/web/assets/mobile-explorer-sheet-BBwXGmXA.js.br +0 -0
  388. package/dist/web/assets/mobile-explorer-sheet-BBwXGmXA.js.gz +0 -0
  389. package/dist/web/assets/new-design-dialog-B_ZOvGhR.js.br +0 -0
  390. package/dist/web/assets/new-design-dialog-B_ZOvGhR.js.gz +0 -0
  391. package/dist/web/assets/notifications-settings-section-D7fIVFHe.js +0 -1
  392. package/dist/web/assets/notifications-settings-section-D7fIVFHe.js.br +0 -0
  393. package/dist/web/assets/notifications-settings-section-D7fIVFHe.js.gz +0 -0
  394. package/dist/web/assets/ppmbot-settings-section-JDvyI-gZ.js.br +0 -0
  395. package/dist/web/assets/ppmbot-settings-section-JDvyI-gZ.js.gz +0 -0
  396. package/dist/web/assets/problems-panel-Bsv_L6T2.js.br +0 -0
  397. package/dist/web/assets/problems-panel-Bsv_L6T2.js.gz +0 -0
  398. package/dist/web/assets/process-table-3kZyWOJx.js.br +0 -0
  399. package/dist/web/assets/process-table-3kZyWOJx.js.gz +0 -0
  400. package/dist/web/assets/proxy-settings-section-CYcmIp7W.js.br +0 -0
  401. package/dist/web/assets/proxy-settings-section-CYcmIp7W.js.gz +0 -0
  402. package/dist/web/assets/query-tab-CjrWnQUl.js.br +0 -0
  403. package/dist/web/assets/query-tab-CjrWnQUl.js.gz +0 -0
  404. package/dist/web/assets/remote-access-settings-section-iQkakiVx.js.br +0 -0
  405. package/dist/web/assets/remote-access-settings-section-iQkakiVx.js.gz +0 -0
  406. package/dist/web/assets/remote-desktop-clipboard-notice-CFLnb7ML.js.br +0 -0
  407. package/dist/web/assets/remote-desktop-clipboard-notice-CFLnb7ML.js.gz +0 -0
  408. package/dist/web/assets/remote-desktop-mobile-sheet-C_-_-Nkz.js.br +0 -0
  409. package/dist/web/assets/remote-desktop-mobile-sheet-C_-_-Nkz.js.gz +0 -0
  410. package/dist/web/assets/remote-desktop-mobile-view-BnHSrind.js.br +0 -0
  411. package/dist/web/assets/remote-desktop-mobile-view-BnHSrind.js.gz +0 -0
  412. package/dist/web/assets/remote-desktop-readiness-gate-BGHJ7Sv6.js.br +0 -0
  413. package/dist/web/assets/remote-desktop-readiness-gate-BGHJ7Sv6.js.gz +0 -0
  414. package/dist/web/assets/remote-desktop-window-content-BBm1YwM8.js.br +0 -0
  415. package/dist/web/assets/remote-desktop-window-content-BBm1YwM8.js.gz +0 -0
  416. package/dist/web/assets/rename-object-dialog-BLnF9xyn.js.br +0 -0
  417. package/dist/web/assets/rename-object-dialog-BLnF9xyn.js.gz +0 -0
  418. package/dist/web/assets/review-code-DG7sCIzr.js.br +0 -0
  419. package/dist/web/assets/review-code-DG7sCIzr.js.gz +0 -0
  420. package/dist/web/assets/save-as-dialog-KW7cErWh.js.br +0 -0
  421. package/dist/web/assets/save-as-dialog-KW7cErWh.js.gz +0 -0
  422. package/dist/web/assets/schedules-settings-section-B4AV4ORv.js.br +0 -0
  423. package/dist/web/assets/schedules-settings-section-B4AV4ORv.js.gz +0 -0
  424. package/dist/web/assets/session-review-tab-C5nPyBzo.js.br +0 -0
  425. package/dist/web/assets/session-review-tab-C5nPyBzo.js.gz +0 -0
  426. package/dist/web/assets/settings-body-xWSmRXUD.js.br +0 -0
  427. package/dist/web/assets/settings-body-xWSmRXUD.js.gz +0 -0
  428. package/dist/web/assets/settings-rows-ChKBHk07.js.br +0 -0
  429. package/dist/web/assets/settings-rows-ChKBHk07.js.gz +0 -0
  430. package/dist/web/assets/settings-section-content-BOh2jaWV.js +0 -1
  431. package/dist/web/assets/settings-section-content-DdVdyPVX.js.br +0 -0
  432. package/dist/web/assets/settings-section-content-DdVdyPVX.js.gz +0 -0
  433. package/dist/web/assets/slash-command-picker-Cn2Fdz2p.js.br +0 -0
  434. package/dist/web/assets/slash-command-picker-Cn2Fdz2p.js.gz +0 -0
  435. package/dist/web/assets/sql-object-tab-_mPRCvD9.js.br +0 -0
  436. package/dist/web/assets/sql-object-tab-_mPRCvD9.js.gz +0 -0
  437. package/dist/web/assets/sqlite-viewer-B8VZt-mA.js.br +0 -0
  438. package/dist/web/assets/sqlite-viewer-B8VZt-mA.js.gz +0 -0
  439. package/dist/web/assets/structure-save-dialog-Lcst7kAl.js.br +0 -0
  440. package/dist/web/assets/structure-save-dialog-Lcst7kAl.js.gz +0 -0
  441. package/dist/web/assets/structure-tab-ljJCzxUe.js.br +0 -0
  442. package/dist/web/assets/structure-tab-ljJCzxUe.js.gz +0 -0
  443. package/dist/web/assets/system-monitor-body-CTi5xUZz.js.br +0 -0
  444. package/dist/web/assets/system-monitor-body-CTi5xUZz.js.gz +0 -0
  445. package/dist/web/assets/tab-host-window-content-CG0vPTc_.js.br +0 -0
  446. package/dist/web/assets/tab-host-window-content-CG0vPTc_.js.gz +0 -0
  447. package/dist/web/assets/table-tab-CTZ3nCWV.js.br +0 -0
  448. package/dist/web/assets/table-tab-CTZ3nCWV.js.gz +0 -0
  449. package/dist/web/assets/telegram-bot-token-field-DF7zq7_h.js.br +0 -0
  450. package/dist/web/assets/telegram-bot-token-field-DF7zq7_h.js.gz +0 -0
  451. package/dist/web/assets/terminal-tab-B_jECWTH.js.br +0 -0
  452. package/dist/web/assets/terminal-tab-B_jECWTH.js.gz +0 -0
  453. package/dist/web/assets/tool-cards-CsD_VXcG.js.br +0 -0
  454. package/dist/web/assets/tool-cards-CsD_VXcG.js.gz +0 -0
  455. package/dist/web/assets/tools-settings-section-DbBr0Hnr.js.br +0 -0
  456. package/dist/web/assets/tools-settings-section-DbBr0Hnr.js.gz +0 -0
  457. package/dist/web/assets/use-inline-blame-BA2mrMxT.js.br +0 -0
  458. package/dist/web/assets/use-inline-blame-BA2mrMxT.js.gz +0 -0
  459. package/dist/web/assets/use-lsp-ChbrzwvS.js.br +0 -0
  460. package/dist/web/assets/use-lsp-ChbrzwvS.js.gz +0 -0
  461. package/dist/web/assets/use-process-kill-C-YetnGd.js.br +0 -0
  462. package/dist/web/assets/use-process-kill-C-YetnGd.js.gz +0 -0
  463. package/dist/web/assets/use-sql-schema-info-pKwLD9wk.js.br +0 -0
  464. package/dist/web/assets/use-sql-schema-info-pKwLD9wk.js.gz +0 -0
  465. package/dist/web/assets/web-preview-tab-DGLPuZYM.js.br +0 -0
  466. package/dist/web/assets/web-preview-tab-DGLPuZYM.js.gz +0 -0
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Always-on memory watchdog: a few lines in `ppm.log`, so that "why is PPM using so much RAM"
3
+ * has an answer in the log a user already has, without anyone having set `PPM_MEM_DIAG`.
4
+ *
5
+ * Every minute it reads this process's private bytes and those of every process below it
6
+ * (one OS call, ~8 ms on Windows). It writes:
7
+ * - a summary line every 30 minutes — this process, its children by program, the biggest child
8
+ * branches with the chat session each one serves, the busiest HTTP routes since the last
9
+ * summary, and the registered gauges (sessions, clients, terminals…). WARN instead of INFO
10
+ * when private bytes grew by a gigabyte or more since the previous summary;
11
+ * - a WARN the first time private bytes (and, for the server, its children's total) cross each
12
+ * threshold, with the same detail;
13
+ * - an INFO when a Claude/Codex subprocess starts or exits, with its pid, its session and what
14
+ * it held at its last reading — the pid in the summary is then traceable to a chat.
15
+ *
16
+ * What it answered on 26/10/2026, after the fact and by hand: the server at 7 GB while its JS
17
+ * heap was 186 MB (a native leak), and 17 Claude subprocesses at ~650 MB each (by design, one
18
+ * per open chat). Both halves are in one summary line now.
19
+ *
20
+ * The detailed per-30-second JSON stays behind `PPM_MEM_DIAG=1` (`memory-diagnostics.ts`).
21
+ */
22
+ import { createLogger } from "./logger.ts";
23
+ import { readNativeMemory } from "./memory-diagnostics-native.ts";
24
+ import { spawnTracker } from "./memory-diagnostics-spawns.ts";
25
+ import { diffRequestCounts, requestTracker } from "./memory-diagnostics-requests.ts";
26
+ import { descendantsOf, readProcessList } from "./process-tree-memory.ts";
27
+ import { heapStats } from "bun:jsc";
28
+
29
+ const log = createLogger("mem");
30
+
31
+ const MB = 1024 * 1024;
32
+ const WATCH_INTERVAL_MS = 60_000;
33
+ const SUMMARY_INTERVAL_MS = 30 * 60_000;
34
+ /** Growth between two summaries that turns the summary into a WARN. */
35
+ const FAST_GROWTH_MB = 1024;
36
+ const SELF_ALERT_MB = [2048, 4096, 8192, 16384, 32768];
37
+ const CHILDREN_ALERT_MB = [8192, 16384, 24576, 32768];
38
+
39
+ export interface ChildBranch {
40
+ pid: number;
41
+ name: string;
42
+ /** The tracker's label for a direct child, when it saw the spawn. */
43
+ label?: string;
44
+ session?: string;
45
+ ageMin?: number;
46
+ /** The child alone. */
47
+ ownMB: number;
48
+ /** The child and everything below it. */
49
+ treeMB: number;
50
+ }
51
+
52
+ export interface TreeSummary {
53
+ count: number;
54
+ privateMB: number;
55
+ byName: Record<string, { n: number; mb: number }>;
56
+ /** Direct children, biggest subtree first. */
57
+ branches: ChildBranch[];
58
+ }
59
+
60
+ /** Read the tree below `rootPid` and fold it by program and by direct child. Undefined when unreadable. */
61
+ export async function summarizeDescendants(rootPid: number, limit: number): Promise<TreeSummary | undefined> {
62
+ const list = await readProcessList(rootPid);
63
+ if (!list) return undefined;
64
+ const all = descendantsOf(rootPid, list);
65
+ const byName: TreeSummary["byName"] = {};
66
+ const branches = new Map<number, ChildBranch>();
67
+ let privateMB = 0;
68
+ for (const d of all) {
69
+ privateMB += d.privateMB;
70
+ const n = (byName[d.name] ??= { n: 0, mb: 0 });
71
+ n.n++;
72
+ n.mb += d.privateMB;
73
+ if (d.depth === 1) {
74
+ const tracked = spawnTracker.describe(d.pid);
75
+ if (tracked) tracked.lastPrivateMB = d.privateMB;
76
+ branches.set(d.pid, {
77
+ pid: d.pid, name: d.name, label: tracked?.label, session: tracked?.session,
78
+ ageMin: tracked ? Math.round((Date.now() - tracked.startedAt) / 60_000) : undefined,
79
+ ownMB: d.privateMB, treeMB: 0,
80
+ });
81
+ }
82
+ }
83
+ for (const d of all) {
84
+ const b = branches.get(d.branchPid);
85
+ if (b) b.treeMB += d.privateMB;
86
+ }
87
+ return {
88
+ count: all.length,
89
+ privateMB,
90
+ byName,
91
+ branches: [...branches.values()].sort((a, b) => b.treeMB - a.treeMB).slice(0, limit),
92
+ };
93
+ }
94
+
95
+ const plural = (n: number, mb: number) => `${n > 1 ? `×${n} ` : ""}${mb}MB`;
96
+
97
+ /** The children half of a log line. Exported for tests. */
98
+ export function describeTree(tree: TreeSummary | undefined): string {
99
+ if (!tree) return "children unreadable";
100
+ if (tree.count === 0) return "no children";
101
+ const names = Object.entries(tree.byName)
102
+ .sort((a, b) => b[1].mb - a[1].mb).slice(0, 6)
103
+ .map(([name, v]) => `${name} ${plural(v.n, v.mb)}`).join(", ");
104
+ const biggest = tree.branches.slice(0, 5).map((b) => {
105
+ const who = b.session ? `session=${b.session.slice(0, 8)}` : b.label ?? b.name;
106
+ const own = b.treeMB !== b.ownMB ? ` (own ${b.ownMB}MB)` : "";
107
+ const age = b.ageMin !== undefined ? ` ${b.ageMin}m` : "";
108
+ return `${b.name} pid=${b.pid} ${who}${age} ${b.treeMB}MB${own}`;
109
+ }).join("; ");
110
+ return `children ${tree.count} = ${tree.privateMB}MB [${names}] biggest: ${biggest}`;
111
+ }
112
+
113
+ function describeRequests(top: Record<string, { n: number; mb: number; streamed: number }>, total: number): string {
114
+ const rows = Object.entries(top).map(([k, v]) => `${k} ×${v.n}${v.mb ? ` ${v.mb}MB` : ""}${v.streamed ? ` (${v.streamed} streamed)` : ""}`);
115
+ return `requests ${total}${rows.length ? `: ${rows.join(", ")}` : ""}`;
116
+ }
117
+
118
+ function describeGauges(gauges: Record<string, number | string>): string {
119
+ const rows = Object.entries(gauges).map(([k, v]) => `${k}=${v}`);
120
+ return rows.length ? `gauges ${rows.join(" ")}` : "";
121
+ }
122
+
123
+ export function startMemoryWatchdog(role: string, readGauges: () => Record<string, number | string>): void {
124
+ const startedAt = Date.now();
125
+ const selfAlerted = new Set<number>();
126
+ const childAlerted = new Set<number>();
127
+ // The first summary comes a minute after start: the baseline every later line is read against.
128
+ let lastSummaryAt = Date.now() - SUMMARY_INTERVAL_MS + WATCH_INTERVAL_MS;
129
+ let lastSummaryPrivateMB: number | undefined;
130
+ let lastRequests = requestTracker.read();
131
+
132
+ spawnTracker.onLifecycle = (e) => {
133
+ const who = `${e.label} pid=${e.pid}${e.session ? ` session=${e.session}` : ""}`;
134
+ if (e.event === "start") log.info(`${who} started`);
135
+ else log.info(`${who} exited after ${e.ageMin}m${e.lastPrivateMB !== undefined ? `, held ${e.lastPrivateMB}MB at its last reading` : ""}`);
136
+ };
137
+
138
+ const tick = async () => {
139
+ try {
140
+ const native = await readNativeMemory();
141
+ const tree = await summarizeDescendants(process.pid, 5);
142
+ const self = native.privateMB;
143
+ const crossedSelf = self === undefined ? [] : SELF_ALERT_MB.filter((t) => self >= t && !selfAlerted.has(t));
144
+ const crossedChildren = role === "server" && tree
145
+ ? CHILDREN_ALERT_MB.filter((t) => tree.privateMB >= t && !childAlerted.has(t))
146
+ : [];
147
+ const summaryDue = Date.now() - lastSummaryAt >= SUMMARY_INTERVAL_MS;
148
+ if (!crossedSelf.length && !crossedChildren.length && !summaryDue) return;
149
+
150
+ const heapMB = Math.round(heapStats().heapSize / MB);
151
+ const rssMB = Math.round(process.memoryUsage().rss / MB);
152
+ const upMin = Math.round((Date.now() - startedAt) / 60_000);
153
+ const head = `${role} pid=${process.pid} up=${upMin}m private=${self ?? "?"}MB heap=${heapMB}MB rss=${rssMB}MB handles=${native.handles ?? "?"}`;
154
+ const tail = [describeTree(tree), describeGauges(readGauges())].filter(Boolean).join(" | ");
155
+
156
+ // One line per crossing, naming the highest threshold passed since the last tick.
157
+ if (crossedSelf.length) {
158
+ for (const t of crossedSelf) selfAlerted.add(t);
159
+ log.warn(`${role} private bytes crossed ${Math.max(...crossedSelf)}MB (heap ${heapMB}MB, so ~${Math.max(0, (self ?? 0) - heapMB)}MB is native) — ${head} | ${tail}`);
160
+ }
161
+ if (crossedChildren.length) {
162
+ for (const t of crossedChildren) childAlerted.add(t);
163
+ log.warn(`${role} children crossed ${Math.max(...crossedChildren)}MB together — ${head} | ${tail}`);
164
+ }
165
+ if (!summaryDue) return;
166
+
167
+ const now = requestTracker.read();
168
+ const requests = diffRequestCounts(lastRequests, now, 6);
169
+ lastRequests = now;
170
+ const minutes = Math.round((Date.now() - lastSummaryAt) / 60_000);
171
+ const growth = self !== undefined && lastSummaryPrivateMB !== undefined ? self - lastSummaryPrivateMB : undefined;
172
+ lastSummaryAt = Date.now();
173
+ if (self !== undefined) lastSummaryPrivateMB = self;
174
+ const growthText = growth === undefined ? "" : ` (${growth >= 0 ? "+" : ""}${growth}MB in ${minutes}m)`;
175
+ const reqText = requests.total ? ` | ${describeRequests(requests.top, requests.total)}` : "";
176
+ const line = `summary ${head}${growthText} | ${tail}${reqText}`;
177
+ if (growth !== undefined && growth >= FAST_GROWTH_MB) log.warn(`${line} — private bytes grew fast`);
178
+ else log.info(line);
179
+ } catch (e) {
180
+ log.debug(`watchdog tick failed: ${(e as Error).message}`);
181
+ }
182
+ };
183
+
184
+ setInterval(() => { void tick(); }, WATCH_INTERVAL_MS).unref?.();
185
+ }
@@ -30,6 +30,7 @@ import type { ChatEvent } from "../types/chat.ts";
30
30
  import { createAgentTranscriptLineParser } from "./subagent-transcript-merger.ts";
31
31
  import { getCachedSubagentGroups } from "./agent-transcript/agent-transcript-index-cache.ts";
32
32
  import { createLogger } from "./logger.ts";
33
+ import { registerMemoryGauge } from "./memory-diagnostics.ts";
33
34
 
34
35
  const log = createLogger("nested-spy");
35
36
 
@@ -207,3 +208,5 @@ function stopAllForSession(sessionId: string): void {
207
208
  }
208
209
 
209
210
  export const nestedSubagentSpy = { startSpy, stopSpy, stopAllForSession };
211
+
212
+ registerMemoryGauge("nestedSubagentSpy.sessions", () => sessions.size);
@@ -109,7 +109,12 @@ const STATUS_INFO_LENGTH_MISMATCH = 0xc0000004 | 0;
109
109
  /** Grows on a length mismatch; a busy machine needs ~1-2 MB. */
110
110
  let bufferSize = 2 * 1024 * 1024;
111
111
 
112
- async function readWindows(pid: number): Promise<ThreadSnapshot | null> {
112
+ /**
113
+ * One `SystemProcessInformation` reading of every process on the machine, or null when it
114
+ * cannot be taken. `base` is the buffer's address: the image names inside it are pointers
115
+ * into the same buffer, so `pointer - base` is their offset in `view`.
116
+ */
117
+ export async function querySystemProcessInformation(): Promise<{ view: DataView; base: bigint } | null> {
113
118
  if (ntdll === undefined) {
114
119
  try {
115
120
  const { dlopen, FFIType } = await import("bun:ffi");
@@ -131,11 +136,16 @@ async function readWindows(pid: number): Promise<ThreadSnapshot | null> {
131
136
  continue;
132
137
  }
133
138
  if (status !== 0) return null;
134
- return parseWindowsProcessInformation(new DataView(buf.buffer), pid);
139
+ return { view: new DataView(buf.buffer), base: BigInt(ptr(buf)) };
135
140
  }
136
141
  return null;
137
142
  }
138
143
 
144
+ async function readWindows(pid: number): Promise<ThreadSnapshot | null> {
145
+ const info = await querySystemProcessInformation();
146
+ return info ? parseWindowsProcessInformation(info.view, pid) : null;
147
+ }
148
+
139
149
  const LINUX_STATES: Record<string, string> = {
140
150
  R: "running", S: "sleeping", D: "disk-wait", T: "stopped", t: "traced", Z: "zombie", X: "dead",
141
151
  I: "idle", W: "paging", P: "parked",
@@ -0,0 +1,161 @@
1
+ /**
2
+ * How much memory every process below this one holds, read from the OS.
3
+ *
4
+ * The monitor's own numbers cover one process, and on 26/10/2026 that was the smaller half:
5
+ * the server held 7 GB while its 17 Claude subprocesses held 11 GB between them, and nothing
6
+ * PPM logged could say so. The spawn tracker only sees direct children, and only the ones
7
+ * started after it was installed; the OS sees the whole tree, grandchildren included (an MCP
8
+ * server under a Claude subprocess, a test runner under a shell).
9
+ *
10
+ * Windows reads every process with one `NtQuerySystemInformation` call (the same one
11
+ * `process-thread-snapshot.ts` makes); Linux walks `/proc`. Other platforms answer null.
12
+ * Nothing here throws.
13
+ */
14
+ import { readdirSync, readFileSync } from "node:fs";
15
+ import { querySystemProcessInformation } from "./process-thread-snapshot.ts";
16
+
17
+ export interface ProcessMemoryEntry {
18
+ pid: number;
19
+ ppid: number;
20
+ /** Image name, lowercased, executable extension dropped. */
21
+ name: string;
22
+ privateMB: number;
23
+ /** When the process started, ms since the epoch on Windows and since boot on Linux: only compared, never shown. */
24
+ startedAt: number;
25
+ }
26
+
27
+ const MB = 1024 * 1024;
28
+
29
+ // SYSTEM_PROCESS_INFORMATION on 64-bit Windows.
30
+ const P_NEXT = 0x00;
31
+ const P_CREATE_TIME = 0x20;
32
+ const P_IMAGE_NAME_LENGTH = 0x38;
33
+ const P_IMAGE_NAME_BUFFER = 0x40;
34
+ const P_PID = 0x50;
35
+ const P_PARENT_PID = 0x58;
36
+ const P_PRIVATE_BYTES = 0xc8;
37
+ const P_SIZE = 0x100;
38
+ /** 100 ns ticks between 1601-01-01 and 1970-01-01. */
39
+ const EPOCH_TICKS = 116_444_736_000_000_000n;
40
+
41
+ export function processName(raw: string): string {
42
+ return raw.toLowerCase().replace(/\.(exe|cmd|bat|com)$/, "").slice(0, 40) || "unknown";
43
+ }
44
+
45
+ /** Every process in a `SystemProcessInformation` buffer. Exported for tests. */
46
+ export function parseWindowsProcessList(view: DataView, base: bigint): ProcessMemoryEntry[] {
47
+ const out: ProcessMemoryEntry[] = [];
48
+ let at = 0;
49
+ for (;;) {
50
+ if (at + P_SIZE > view.byteLength) break;
51
+ const pid = Number(view.getBigUint64(at + P_PID, true));
52
+ let name = pid === 0 ? "idle" : "system";
53
+ const nameBytes = view.getUint16(at + P_IMAGE_NAME_LENGTH, true);
54
+ const namePtr = view.getBigUint64(at + P_IMAGE_NAME_BUFFER, true);
55
+ const offset = Number(namePtr - base);
56
+ if (nameBytes > 0 && offset >= 0 && offset + nameBytes <= view.byteLength) {
57
+ name = new TextDecoder("utf-16le").decode(new Uint8Array(view.buffer, view.byteOffset + offset, nameBytes));
58
+ }
59
+ const created = view.getBigInt64(at + P_CREATE_TIME, true);
60
+ out.push({
61
+ pid,
62
+ ppid: Number(view.getBigUint64(at + P_PARENT_PID, true)),
63
+ name: processName(name),
64
+ privateMB: Math.round(Number(view.getBigUint64(at + P_PRIVATE_BYTES, true)) / MB),
65
+ startedAt: created > EPOCH_TICKS ? Number((created - EPOCH_TICKS) / 10_000n) : 0,
66
+ });
67
+ const next = view.getUint32(at + P_NEXT, true);
68
+ if (next === 0) break;
69
+ at += next;
70
+ }
71
+ return out;
72
+ }
73
+
74
+ /** `/proc/<pid>/stat` → ppid, name and start time (field 22, clock ticks since boot). Exported for tests. */
75
+ export function parseLinuxStatLine(text: string): { ppid: number; name: string; startedAt: number } | null {
76
+ const open = text.indexOf("(");
77
+ const close = text.lastIndexOf(")");
78
+ if (open === -1 || close === -1) return null;
79
+ const f = text.slice(close + 2).trim().split(/\s+/);
80
+ const ppid = Number(f[4 - 3]);
81
+ const startedAt = Number(f[22 - 3]);
82
+ if (!Number.isFinite(ppid)) return null;
83
+ return { ppid, name: processName(text.slice(open + 1, close)), startedAt: Number.isFinite(startedAt) ? startedAt : 0 };
84
+ }
85
+
86
+ function linuxPrivateMB(pid: number): number {
87
+ try {
88
+ const status = readFileSync(`/proc/${pid}/status`, "utf8");
89
+ const kb = (key: string) => Number(status.match(new RegExp(`^${key}:\\s+(\\d+)`, "m"))?.[1] ?? 0);
90
+ return Math.round((kb("RssAnon") + kb("VmSwap")) / 1024);
91
+ } catch {
92
+ return 0;
93
+ }
94
+ }
95
+
96
+ function readLinuxList(rootPid: number): ProcessMemoryEntry[] {
97
+ const all: ProcessMemoryEntry[] = [];
98
+ for (const entry of readdirSync("/proc")) {
99
+ const pid = Number(entry);
100
+ if (!Number.isInteger(pid)) continue;
101
+ try {
102
+ const stat = parseLinuxStatLine(readFileSync(`/proc/${pid}/stat`, "utf8"));
103
+ if (stat) all.push({ pid, ...stat, privateMB: 0 });
104
+ } catch { /* exited while walking */ }
105
+ }
106
+ // Memory only for the tree that is reported: one status read per descendant, not per process.
107
+ const tree = new Set(descendantsOf(rootPid, all).map((d) => d.pid));
108
+ for (const p of all) if (tree.has(p.pid) || p.pid === rootPid) p.privateMB = linuxPrivateMB(p.pid);
109
+ return all;
110
+ }
111
+
112
+ /** Every process the OS lists, or null when this platform cannot be read. */
113
+ export async function readProcessList(rootPid = process.pid, platform: NodeJS.Platform = process.platform): Promise<ProcessMemoryEntry[] | null> {
114
+ try {
115
+ if (platform === "win32") {
116
+ const info = await querySystemProcessInformation();
117
+ return info ? parseWindowsProcessList(info.view, info.base) : null;
118
+ }
119
+ if (platform === "linux") return readLinuxList(rootPid);
120
+ } catch { /* fall through */ }
121
+ return null;
122
+ }
123
+
124
+ export interface Descendant extends ProcessMemoryEntry {
125
+ depth: number;
126
+ /** The direct child of the root this process descends from (itself, at depth 1). */
127
+ branchPid: number;
128
+ }
129
+
130
+ /**
131
+ * Every process below `rootPid`. A process counts as a child only if it started after its
132
+ * parent: Windows reuses pids, so a ppid can name an unrelated process that took a dead
133
+ * parent's number, and without the check that process's whole tree would be claimed.
134
+ */
135
+ export function descendantsOf(rootPid: number, list: ProcessMemoryEntry[]): Descendant[] {
136
+ const byParent = new Map<number, ProcessMemoryEntry[]>();
137
+ const byPid = new Map<number, ProcessMemoryEntry>();
138
+ for (const p of list) {
139
+ byPid.set(p.pid, p);
140
+ if (p.pid === p.ppid) continue;
141
+ const siblings = byParent.get(p.ppid);
142
+ if (siblings) siblings.push(p); else byParent.set(p.ppid, [p]);
143
+ }
144
+ const out: Descendant[] = [];
145
+ const queue: Array<{ p: ProcessMemoryEntry; depth: number; branchPid: number }> = [];
146
+ const root = byPid.get(rootPid);
147
+ for (const c of byParent.get(rootPid) ?? []) {
148
+ if (!root || c.startedAt >= root.startedAt) queue.push({ p: c, depth: 1, branchPid: c.pid });
149
+ }
150
+ const seen = new Set<number>([rootPid]);
151
+ while (queue.length) {
152
+ const { p, depth, branchPid } = queue.shift()!;
153
+ if (seen.has(p.pid)) continue;
154
+ seen.add(p.pid);
155
+ out.push({ ...p, depth, branchPid });
156
+ for (const c of byParent.get(p.pid) ?? []) {
157
+ if (c.startedAt >= p.startedAt) queue.push({ p: c, depth: depth + 1, branchPid });
158
+ }
159
+ }
160
+ return out;
161
+ }
@@ -5,6 +5,7 @@
5
5
  * compaction scan — and having the second import it from the first would make the
6
6
  * two files a cycle.
7
7
  */
8
+ import { readFileChunks } from "./file-chunks.ts";
8
9
 
9
10
  /**
10
11
  * Yield a file's lines without holding the file in memory.
@@ -19,47 +20,35 @@
19
20
  *
20
21
  * `toByte` ends the read there, exclusive. It is meant to be a line start (the
21
22
  * record that begins there is not read) or omitted for the end of the file —
22
- * `compactSegmentWindow` in `compact-segment.ts` is what computes one. It is
23
- * counted here rather than handed to `slice()` as an end, because in Bun 1.3.11
24
- * a sliced file's stream that has to stop well short of EOF never finishes:
25
- * on a 4MB file `slice(0, 259).stream()` hung, `slice(0, 4194303)` did not.
23
+ * `compactSegmentWindow` in `compact-segment.ts` is what computes one.
24
+ *
25
+ * The bytes come from `readFileChunks`, not `Bun.file().stream()`: that stream
26
+ * keeps about the file's size in native memory per full read and never returns
27
+ * it (see `file-chunks.ts`), and its sliced form never finishes when it has to
28
+ * stop well short of EOF (Bun 1.3.11: on a 4MB file `slice(0, 259).stream()`
29
+ * hung).
26
30
  */
27
31
  export async function* readLines(filePath: string, fromByte = 0, toByte?: number): AsyncGenerator<string> {
28
32
  const decoder = new TextDecoder();
29
- // Explicit reader rather than `for await` over the stream: the DOM lib's
30
- // ReadableStream is not typed as async-iterable, and the `finally` is what
31
- // releases it when a caller breaks early on `beforeUuid`.
32
33
  // One byte *before* the offset, so the first line the split produces is
33
34
  // always the tail of the record the offset fell inside — the empty string
34
35
  // when it fell exactly on a newline. That makes the drop below unconditional
35
36
  // instead of a boundary case, and a boundary case here silently costs a whole
36
37
  // record rather than a fragment.
37
- const file = fromByte > 0 ? Bun.file(filePath).slice(fromByte - 1) : Bun.file(filePath);
38
- const reader = file.stream().getReader();
39
38
  // A fragment is not always unparseable — the tail of one record is sometimes
40
39
  // a complete smaller document — so it has to go by position rather than be
41
40
  // left for `JSON.parse` to reject, or it enters as an invented message.
42
41
  let dropPartial = fromByte > 0;
43
- let remaining = toByte === undefined ? Infinity : toByte - Math.max(0, fromByte - 1);
44
42
  let buffered = "";
45
- try {
46
- while (remaining > 0) {
47
- const { done, value } = await reader.read();
48
- if (done) break;
49
- const chunk = value.byteLength > remaining ? value.subarray(0, remaining) : value;
50
- remaining -= chunk.byteLength;
51
- buffered += decoder.decode(chunk, { stream: true });
52
- const lines = buffered.split("\n");
53
- buffered = lines.pop() ?? "";
54
- for (const line of lines) {
55
- if (dropPartial) { dropPartial = false; continue; }
56
- yield line;
57
- }
43
+ // A caller that stops early (`beforeUuid`) ends this loop, which closes the file.
44
+ for await (const chunk of readFileChunks(filePath, { fromByte: Math.max(0, fromByte - 1), toByte })) {
45
+ buffered += decoder.decode(chunk, { stream: true });
46
+ const lines = buffered.split("\n");
47
+ buffered = lines.pop() ?? "";
48
+ for (const line of lines) {
49
+ if (dropPartial) { dropPartial = false; continue; }
50
+ yield line;
58
51
  }
59
- } finally {
60
- // `cancel`, not only `releaseLock`: a stream let go of before its end keeps
61
- // the file open — 2000 early stops left 2000 descriptors behind.
62
- await reader.cancel().catch(() => {});
63
52
  }
64
53
  buffered += decoder.decode();
65
54
  if (buffered && !dropPartial) yield buffered;
@@ -2062,6 +2062,7 @@ export async function runSupervisor(opts: {
2062
2062
  log("INFO", `Supervisor started (PID: ${process.pid}, port: ${opts.port}, share: ${opts.share})`);
2063
2063
  // Before any child is spawned: every child inherits these, the server included.
2064
2064
  await logPriorityRaise("supervisor", (m) => log("WARN", m));
2065
+ (await import("./memory-diagnostics.ts")).startMemoryDiagnostics({ role: "supervisor" });
2065
2066
 
2066
2067
  // ── Systemd self-heal: if the unit file is stale (e.g. still has
2067
2068
  // Restart=on-failure), we were likely spawned by the old Bun.spawn()
@@ -2,6 +2,7 @@ import type { Subprocess, Terminal as BunTerminal } from "bun";
2
2
  import { isUsableTerminalSize } from "../shared/terminal-size";
3
3
  import { withoutAiChatMark } from "./ai-chat-env.ts";
4
4
  import { createLogger } from "./logger.ts";
5
+ import { registerMemoryGauge } from "./memory-diagnostics.ts";
5
6
 
6
7
  /** PTY lifecycle only — never what is typed into a terminal or what it prints. */
7
8
  const log = createLogger("terminal");
@@ -397,3 +398,10 @@ export class TerminalService {
397
398
  }
398
399
 
399
400
  export const terminalService = new TerminalService();
401
+
402
+ registerMemoryGauge("terminal.sessions", () => terminalService["sessions"].size);
403
+ registerMemoryGauge("terminal.bufferedKB", () => {
404
+ let chars = 0;
405
+ for (const b of terminalService["outputBuffers"].values()) chars += b.length;
406
+ return Math.round(chars / 1024);
407
+ });
@@ -7,6 +7,7 @@ import {
7
7
  type StripMode,
8
8
  type TranscriptImageAudit,
9
9
  } from "./transcript-images.ts";
10
+ import { readFileChunks } from "./file-chunks.ts";
10
11
 
11
12
  /**
12
13
  * File-level image auditing and removal that never holds a transcript in memory.
@@ -50,19 +51,14 @@ async function forEachLine(
50
51
  path: string,
51
52
  onLine: (line: string, terminated: boolean) => void,
52
53
  ): Promise<void> {
53
- const reader = Bun.file(path).stream().getReader();
54
54
  const decoder = new TextDecoder();
55
55
  let carry = "";
56
- try {
57
- for (;;) {
58
- const { done, value } = await reader.read();
59
- if (done) break;
60
- const parts = (carry + decoder.decode(value, { stream: true })).split("\n");
61
- carry = parts.pop() ?? "";
62
- for (const line of parts) onLine(line, true);
63
- }
64
- } finally {
65
- reader.releaseLock();
56
+ // Not `Bun.file().stream()`, which keeps about the file's size in native memory per read
57
+ // (see `file-chunks.ts`) — and this runs over every transcript a cleanup visits.
58
+ for await (const value of readFileChunks(path)) {
59
+ const parts = (carry + decoder.decode(value, { stream: true })).split("\n");
60
+ carry = parts.pop() ?? "";
61
+ for (const line of parts) onLine(line, true);
66
62
  }
67
63
  if (carry) onLine(carry, false);
68
64
  }
@@ -134,14 +134,20 @@ function useQuestionKeyboard(config: {
134
134
  if (el) {
135
135
  el.addEventListener("keydown", handleKeyDown);
136
136
  el.setAttribute("tabindex", "0");
137
- // Same reason: asking the main document whether focus is already inside the card answers
138
- // "no" for a card living in another document, and this effect re-runs on every render —
139
- // which would yank focus out of the custom input after each keystroke.
140
- if (config.autoFocus && !el.contains(el.ownerDocument.activeElement)) el.focus();
141
137
  }
142
138
  return () => { el?.removeEventListener("keydown", handleKeyDown); };
143
139
  }, [config, focusedOption]);
144
140
 
141
+ // Focus the card once, when it appears. Doing it on every render took focus back whenever the
142
+ // card re-rendered for any reason, so with two chats side by side each waiting on a question,
143
+ // the other card's chat updating pulled focus out of the card being answered. The owner
144
+ // document is asked rather than the global one, for a card rendered in a picture-in-picture window.
145
+ const autoFocusOnMount = useRef(config.autoFocus);
146
+ useEffect(() => {
147
+ const el = containerRef.current;
148
+ if (autoFocusOnMount.current && el && !el.contains(el.ownerDocument.activeElement)) el.focus();
149
+ }, []);
150
+
145
151
  return { focusedOption, setFocusedOption, containerRef };
146
152
  }
147
153
 
@@ -144,6 +144,8 @@ function PushSection() {
144
144
  const [subscription, setSubscription] = useState<PushSubscription | null>(null);
145
145
  const [loadError, setLoadError] = useState<string | null>(null);
146
146
  const [busy, setBusy] = useState(false);
147
+ const [enabling, setEnabling] = useState(false);
148
+ const [enableError, setEnableError] = useState<string | null>(null);
147
149
  const [testing, setTesting] = useState<string | null>(null);
148
150
  const support = pushSupport();
149
151
 
@@ -174,6 +176,8 @@ function PushSection() {
174
176
  const toggle = async (on: boolean) => {
175
177
  if (!status) return;
176
178
  setBusy(true);
179
+ setEnabling(on);
180
+ setEnableError(null);
177
181
  try {
178
182
  if (on) {
179
183
  await enablePush(status.publicKey);
@@ -182,9 +186,12 @@ function PushSection() {
182
186
  await disablePush();
183
187
  }
184
188
  } catch (e) {
185
- toast.error(on ? "Could not turn push on" : "Could not turn push off", { description: errorText(e) });
189
+ // Kept on screen rather than in a toast: the fix is usually a browser setting to go and change.
190
+ if (on) setEnableError(errorText(e));
191
+ else toast.error("Could not turn push off", { description: errorText(e) });
186
192
  } finally {
187
193
  setBusy(false);
194
+ setEnabling(false);
188
195
  await load();
189
196
  }
190
197
  };
@@ -231,6 +238,12 @@ function PushSection() {
231
238
  />
232
239
  </div>
233
240
  {support.state !== "available" && <p className="text-xs leading-relaxed text-warning">{support.message}</p>}
241
+ {enabling && (
242
+ <p className="text-xs leading-relaxed text-muted-foreground">
243
+ Waiting for the browser's push service. Just after the browser starts, this can take up to a minute.
244
+ </p>
245
+ )}
246
+ {enableError && <p className="break-words text-xs leading-relaxed text-error">{enableError}</p>}
234
247
  {loadError && <p className="text-xs text-error">{loadError}</p>}
235
248
 
236
249
  {status && status.devices.length > 0 && (