@hienlh/ppm 0.23.19 → 0.23.21

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 (441) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/CLAUDE.md +2 -0
  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-BTga1zwH.js → accounts-settings-section-DD88DRdx.js} +1 -1
  7. package/dist/web/assets/accounts-settings-section-DD88DRdx.js.br +0 -0
  8. package/dist/web/assets/accounts-settings-section-DD88DRdx.js.gz +0 -0
  9. package/dist/web/assets/{agent-session-sheet-Q3kasbrZ.js → agent-session-sheet-C0PhhLKR.js} +2 -2
  10. package/dist/web/assets/agent-session-sheet-C0PhhLKR.js.br +0 -0
  11. package/dist/web/assets/agent-session-sheet-C0PhhLKR.js.gz +0 -0
  12. package/dist/web/assets/{agent-session-window-content-UNuEprIK.js → agent-session-window-content-D0XZgh4U.js} +1 -1
  13. package/dist/web/assets/agent-session-window-content-D0XZgh4U.js.br +0 -0
  14. package/dist/web/assets/agent-session-window-content-D0XZgh4U.js.gz +0 -0
  15. package/dist/web/assets/{ai-resource-editor-BEwA6Do0.js → ai-resource-editor-CpfQB6A7.js} +1 -1
  16. package/dist/web/assets/ai-resource-editor-CpfQB6A7.js.br +0 -0
  17. package/dist/web/assets/ai-resource-editor-CpfQB6A7.js.gz +0 -0
  18. package/dist/web/assets/{ai-settings-section-DVKJPM0n.js → ai-settings-section-CQWBwWcM.js} +1 -1
  19. package/dist/web/assets/ai-settings-section-CQWBwWcM.js.br +0 -0
  20. package/dist/web/assets/ai-settings-section-CQWBwWcM.js.gz +0 -0
  21. package/dist/web/assets/{ai-settings-section-DlfY5lFT.js → ai-settings-section-DqE67XNC.js} +1 -1
  22. package/dist/web/assets/{android-tab-BNa6R9fV.js → android-tab-4Z4AgHz1.js} +1 -1
  23. package/dist/web/assets/android-tab-4Z4AgHz1.js.br +0 -0
  24. package/dist/web/assets/android-tab-4Z4AgHz1.js.gz +0 -0
  25. package/dist/web/assets/{appearance-settings-section-CjWFwWNn.js → appearance-settings-section-DjQjiJF7.js} +1 -1
  26. package/dist/web/assets/appearance-settings-section-DjQjiJF7.js.br +0 -0
  27. package/dist/web/assets/appearance-settings-section-DjQjiJF7.js.gz +0 -0
  28. package/dist/web/assets/{apps-panel-D4xjeMN9.js → apps-panel-_lxY9xbc.js} +1 -1
  29. package/dist/web/assets/apps-panel-_lxY9xbc.js.br +0 -0
  30. package/dist/web/assets/apps-panel-_lxY9xbc.js.gz +0 -0
  31. package/dist/web/assets/{branch-review-tab-BCW5294d.js → branch-review-tab-BzrhnFSs.js} +1 -1
  32. package/dist/web/assets/branch-review-tab-BzrhnFSs.js.br +0 -0
  33. package/dist/web/assets/branch-review-tab-BzrhnFSs.js.gz +0 -0
  34. package/dist/web/assets/{chat-tab-BG0M1D3d.js → chat-tab-BKHepkYi.js} +1 -1
  35. package/dist/web/assets/chat-tab-BKHepkYi.js.br +0 -0
  36. package/dist/web/assets/chat-tab-BKHepkYi.js.gz +0 -0
  37. package/dist/web/assets/{chat-tab-DsW93LTL.js → chat-tab-Cv8gYzcA.js} +4 -4
  38. package/dist/web/assets/chat-tab-Cv8gYzcA.js.br +0 -0
  39. package/dist/web/assets/chat-tab-Cv8gYzcA.js.gz +0 -0
  40. package/dist/web/assets/{code-editor-AxLIpK-b.js → code-editor-COWmNXO4.js} +4 -4
  41. package/dist/web/assets/code-editor-COWmNXO4.js.br +0 -0
  42. package/dist/web/assets/code-editor-COWmNXO4.js.gz +0 -0
  43. package/dist/web/assets/{codex-reset-credit-button-BnRmkBZu.js → codex-reset-credit-button-BHt4yJet.js} +1 -1
  44. package/dist/web/assets/codex-reset-credit-button-BHt4yJet.js.br +0 -0
  45. package/dist/web/assets/codex-reset-credit-button-BHt4yJet.js.gz +0 -0
  46. package/dist/web/assets/{conflict-editor-BwuNUPAK.js → conflict-editor-tj-1gzKr.js} +1 -1
  47. package/dist/web/assets/conflict-editor-tj-1gzKr.js.br +0 -0
  48. package/dist/web/assets/conflict-editor-tj-1gzKr.js.gz +0 -0
  49. package/dist/web/assets/{connection-form-tab-CM2o80eB.js → connection-form-tab-BV2QX8Cf.js} +1 -1
  50. package/dist/web/assets/connection-form-tab-BV2QX8Cf.js.br +0 -0
  51. package/dist/web/assets/connection-form-tab-BV2QX8Cf.js.gz +0 -0
  52. package/dist/web/assets/{csv-preview-C7ErHm4U.js → csv-preview-xhE2esgz.js} +1 -1
  53. package/dist/web/assets/csv-preview-xhE2esgz.js.br +0 -0
  54. package/dist/web/assets/csv-preview-xhE2esgz.js.gz +0 -0
  55. package/dist/web/assets/{db-tab-parts-C8jbkfxO.js → db-tab-parts-C3MsjOID.js} +1 -1
  56. package/dist/web/assets/db-tab-parts-C3MsjOID.js.br +0 -0
  57. package/dist/web/assets/db-tab-parts-C3MsjOID.js.gz +0 -0
  58. package/dist/web/assets/{design-settings-section-BcGEY4Zf.js → design-settings-section-C3LxYhIw.js} +1 -1
  59. package/dist/web/assets/design-settings-section-C3LxYhIw.js.br +0 -0
  60. package/dist/web/assets/design-settings-section-C3LxYhIw.js.gz +0 -0
  61. package/dist/web/assets/{design-tab-BGPYNFvy.js → design-tab-DYcvjdfb.js} +1 -1
  62. package/dist/web/assets/design-tab-DYcvjdfb.js.br +0 -0
  63. package/dist/web/assets/design-tab-DYcvjdfb.js.gz +0 -0
  64. package/dist/web/assets/{designs-sidebar-panel-BgMj7xRm.js → designs-sidebar-panel-Dpr4PPFT.js} +1 -1
  65. package/dist/web/assets/designs-sidebar-panel-Dpr4PPFT.js.br +0 -0
  66. package/dist/web/assets/designs-sidebar-panel-Dpr4PPFT.js.gz +0 -0
  67. package/dist/web/assets/{diff-viewer-BKJkesHk.js → diff-viewer-Fl_4jqr6.js} +1 -1
  68. package/dist/web/assets/diff-viewer-Fl_4jqr6.js.br +0 -0
  69. package/dist/web/assets/diff-viewer-Fl_4jqr6.js.gz +0 -0
  70. package/dist/web/assets/{docx-preview-DD5oD_nL.js → docx-preview-bVE9shPX.js} +1 -1
  71. package/dist/web/assets/docx-preview-bVE9shPX.js.br +0 -0
  72. package/dist/web/assets/docx-preview-bVE9shPX.js.gz +0 -0
  73. package/dist/web/assets/{editor-dialog--07lKJxl.js → editor-dialog-eEOYWdbr.js} +1 -1
  74. package/dist/web/assets/editor-dialog-eEOYWdbr.js.br +0 -0
  75. package/dist/web/assets/editor-dialog-eEOYWdbr.js.gz +0 -0
  76. package/dist/web/assets/{editor-language-service-51xjO5d3.js → editor-language-service-D1_Ggq4s.js} +1 -1
  77. package/dist/web/assets/editor-language-service-D1_Ggq4s.js.br +0 -0
  78. package/dist/web/assets/editor-language-service-D1_Ggq4s.js.gz +0 -0
  79. package/dist/web/assets/{explorer-body-BXK5SjJQ.js → explorer-body-C0UOcBPJ.js} +1 -1
  80. package/dist/web/assets/explorer-body-C0UOcBPJ.js.br +0 -0
  81. package/dist/web/assets/explorer-body-C0UOcBPJ.js.gz +0 -0
  82. package/dist/web/assets/{explorer-window-content-DKXcCmuX.js → explorer-window-content-Dxee7SuJ.js} +1 -1
  83. package/dist/web/assets/explorer-window-content-Dxee7SuJ.js.br +0 -0
  84. package/dist/web/assets/explorer-window-content-Dxee7SuJ.js.gz +0 -0
  85. package/dist/web/assets/{extension-manager-section-CTpQP6te.js → extension-manager-section-D8krnoj8.js} +1 -1
  86. package/dist/web/assets/extension-manager-section-D8krnoj8.js.br +0 -0
  87. package/dist/web/assets/extension-manager-section-D8krnoj8.js.gz +0 -0
  88. package/dist/web/assets/{extension-webview-COT-yLrM.js → extension-webview-CtIdV69T.js} +1 -1
  89. package/dist/web/assets/extension-webview-CtIdV69T.js.br +0 -0
  90. package/dist/web/assets/extension-webview-CtIdV69T.js.gz +0 -0
  91. package/dist/web/assets/{files-settings-section-CgVR0fqi.js → files-settings-section-C8vtarR8.js} +1 -1
  92. package/dist/web/assets/files-settings-section-C8vtarR8.js.br +0 -0
  93. package/dist/web/assets/files-settings-section-C8vtarR8.js.gz +0 -0
  94. package/dist/web/assets/{general-settings-section-BEMIbE4Y.js → general-settings-section-Chu8Ut8h.js} +1 -1
  95. package/dist/web/assets/general-settings-section-Chu8Ut8h.js.br +0 -0
  96. package/dist/web/assets/general-settings-section-Chu8Ut8h.js.gz +0 -0
  97. package/dist/web/assets/{git-log-panel-Cp2174iI.js → git-log-panel-DG09p6k5.js} +1 -1
  98. package/dist/web/assets/git-log-panel-DG09p6k5.js.br +0 -0
  99. package/dist/web/assets/git-log-panel-DG09p6k5.js.gz +0 -0
  100. package/dist/web/assets/{git-review-tab-CpMJ0qDi.js → git-review-tab-CSy8DjSC.js} +1 -1
  101. package/dist/web/assets/git-review-tab-CSy8DjSC.js.br +0 -0
  102. package/dist/web/assets/git-review-tab-CSy8DjSC.js.gz +0 -0
  103. package/dist/web/assets/{glide-data-grid-BAhwWUhO.js → glide-data-grid-BN1y4BT0.js} +1 -1
  104. package/dist/web/assets/glide-data-grid-BN1y4BT0.js.br +0 -0
  105. package/dist/web/assets/glide-data-grid-BN1y4BT0.js.gz +0 -0
  106. package/dist/web/assets/{grid-save-dialog-DmJhaWOl.js → grid-save-dialog-CfSuWw2t.js} +1 -1
  107. package/dist/web/assets/grid-save-dialog-CfSuWw2t.js.br +0 -0
  108. package/dist/web/assets/grid-save-dialog-CfSuWw2t.js.gz +0 -0
  109. package/dist/web/assets/{group-chat-tab-Bq7-Yegf.js → group-chat-tab-B04QMp7A.js} +1 -1
  110. package/dist/web/assets/group-chat-tab-B04QMp7A.js.br +0 -0
  111. package/dist/web/assets/group-chat-tab-B04QMp7A.js.gz +0 -0
  112. package/dist/web/assets/{impexp-tab-COx3Cvs7.js → impexp-tab-BRNouquR.js} +1 -1
  113. package/dist/web/assets/impexp-tab-BRNouquR.js.br +0 -0
  114. package/dist/web/assets/impexp-tab-BRNouquR.js.gz +0 -0
  115. package/dist/web/assets/{index-BGk8EFQb.js → index-BpgSIM5B.js} +5 -5
  116. package/dist/web/assets/index-BpgSIM5B.js.br +0 -0
  117. package/dist/web/assets/index-BpgSIM5B.js.gz +0 -0
  118. package/dist/web/assets/{jira-watcher-section-BEPbsi62.js → jira-watcher-section-CJeS5r6D.js} +1 -1
  119. package/dist/web/assets/{keybindings-store-DzcSIJUF.js → keybindings-store-Dvxq-vZD.js} +1 -1
  120. package/dist/web/assets/{keyboard-shortcuts-section-Bqg7AZiZ.js → keyboard-shortcuts-section-BKDyZHhK.js} +1 -1
  121. package/dist/web/assets/keyboard-shortcuts-section-BKDyZHhK.js.br +0 -0
  122. package/dist/web/assets/keyboard-shortcuts-section-BKDyZHhK.js.gz +0 -0
  123. package/dist/web/assets/{language-servers-section-CJ47DSIA.js → language-servers-section-CbbkezhA.js} +2 -2
  124. package/dist/web/assets/language-servers-section-CbbkezhA.js.br +0 -0
  125. package/dist/web/assets/language-servers-section-CbbkezhA.js.gz +0 -0
  126. package/dist/web/assets/{logs-app-BK-n-y_K.js → logs-app-Blr1-v4q.js} +1 -1
  127. package/dist/web/assets/logs-app-Blr1-v4q.js.br +0 -0
  128. package/dist/web/assets/logs-app-Blr1-v4q.js.gz +0 -0
  129. package/dist/web/assets/{logs-tab-B84EZJ1b.js → logs-tab-CGPtn2tU.js} +1 -1
  130. package/dist/web/assets/{logs-window-content-B1uN_3pN.js → logs-window-content-DAczcNmh.js} +1 -1
  131. package/dist/web/assets/{lsp-client-DHSaAyJW.js → lsp-client-Ds3U2O52.js} +1 -1
  132. package/dist/web/assets/lsp-client-Ds3U2O52.js.br +0 -0
  133. package/dist/web/assets/lsp-client-Ds3U2O52.js.gz +0 -0
  134. package/dist/web/assets/{markdown-renderer-BjDQUqT-.js → markdown-renderer-DbJVwfUH.js} +1 -1
  135. package/dist/web/assets/{markdown-renderer-CpxdNG0f.js → markdown-renderer-DzCPadpB.js} +1 -1
  136. package/dist/web/assets/markdown-renderer-DzCPadpB.js.br +0 -0
  137. package/dist/web/assets/markdown-renderer-DzCPadpB.js.gz +0 -0
  138. package/dist/web/assets/{mobile-explorer-sheet-BBwXGmXA.js → mobile-explorer-sheet-BClCMXb8.js} +1 -1
  139. package/dist/web/assets/mobile-explorer-sheet-BClCMXb8.js.br +0 -0
  140. package/dist/web/assets/mobile-explorer-sheet-BClCMXb8.js.gz +0 -0
  141. package/dist/web/assets/{new-design-dialog-B_ZOvGhR.js → new-design-dialog-HRP9H11v.js} +1 -1
  142. package/dist/web/assets/new-design-dialog-HRP9H11v.js.br +0 -0
  143. package/dist/web/assets/new-design-dialog-HRP9H11v.js.gz +0 -0
  144. package/dist/web/assets/{notification-store-HuMqfadD.js → notification-store-BomzYNQj.js} +1 -1
  145. package/dist/web/assets/{notifications-settings-section-D7fIVFHe.js → notifications-settings-section-CYTTQaNr.js} +1 -1
  146. package/dist/web/assets/notifications-settings-section-CYTTQaNr.js.br +0 -0
  147. package/dist/web/assets/notifications-settings-section-CYTTQaNr.js.gz +0 -0
  148. package/dist/web/assets/{ppmbot-settings-section-JDvyI-gZ.js → ppmbot-settings-section-BRowIIqc.js} +1 -1
  149. package/dist/web/assets/ppmbot-settings-section-BRowIIqc.js.br +0 -0
  150. package/dist/web/assets/ppmbot-settings-section-BRowIIqc.js.gz +0 -0
  151. package/dist/web/assets/{problems-panel-Bsv_L6T2.js → problems-panel-DdB10Ybd.js} +1 -1
  152. package/dist/web/assets/problems-panel-DdB10Ybd.js.br +0 -0
  153. package/dist/web/assets/problems-panel-DdB10Ybd.js.gz +0 -0
  154. package/dist/web/assets/{process-table-3kZyWOJx.js → process-table-CzLaWPe0.js} +1 -1
  155. package/dist/web/assets/process-table-CzLaWPe0.js.br +0 -0
  156. package/dist/web/assets/process-table-CzLaWPe0.js.gz +0 -0
  157. package/dist/web/assets/{proxy-settings-section-CYcmIp7W.js → proxy-settings-section-BCq7R5DA.js} +1 -1
  158. package/dist/web/assets/proxy-settings-section-BCq7R5DA.js.br +0 -0
  159. package/dist/web/assets/proxy-settings-section-BCq7R5DA.js.gz +0 -0
  160. package/dist/web/assets/{query-tab-CjrWnQUl.js → query-tab-DGvj_h6l.js} +1 -1
  161. package/dist/web/assets/query-tab-DGvj_h6l.js.br +0 -0
  162. package/dist/web/assets/query-tab-DGvj_h6l.js.gz +0 -0
  163. package/dist/web/assets/{remote-access-settings-section-iQkakiVx.js → remote-access-settings-section-BrqOoOyk.js} +1 -1
  164. package/dist/web/assets/remote-access-settings-section-BrqOoOyk.js.br +0 -0
  165. package/dist/web/assets/remote-access-settings-section-BrqOoOyk.js.gz +0 -0
  166. package/dist/web/assets/{remote-desktop-clipboard-notice-CFLnb7ML.js → remote-desktop-clipboard-notice-5r6UbdlN.js} +1 -1
  167. package/dist/web/assets/remote-desktop-clipboard-notice-5r6UbdlN.js.br +0 -0
  168. package/dist/web/assets/remote-desktop-clipboard-notice-5r6UbdlN.js.gz +0 -0
  169. package/dist/web/assets/{remote-desktop-mobile-sheet-C_-_-Nkz.js → remote-desktop-mobile-sheet-B9Q4auqe.js} +2 -2
  170. package/dist/web/assets/remote-desktop-mobile-sheet-B9Q4auqe.js.br +0 -0
  171. package/dist/web/assets/remote-desktop-mobile-sheet-B9Q4auqe.js.gz +0 -0
  172. package/dist/web/assets/{remote-desktop-mobile-view-BnHSrind.js → remote-desktop-mobile-view-CL-jrkA8.js} +1 -1
  173. package/dist/web/assets/remote-desktop-mobile-view-CL-jrkA8.js.br +0 -0
  174. package/dist/web/assets/remote-desktop-mobile-view-CL-jrkA8.js.gz +0 -0
  175. package/dist/web/assets/{remote-desktop-readiness-gate-BGHJ7Sv6.js → remote-desktop-readiness-gate-DXEf2Jnz.js} +1 -1
  176. package/dist/web/assets/remote-desktop-readiness-gate-DXEf2Jnz.js.br +0 -0
  177. package/dist/web/assets/remote-desktop-readiness-gate-DXEf2Jnz.js.gz +0 -0
  178. package/dist/web/assets/{remote-desktop-window-content-BBm1YwM8.js → remote-desktop-window-content-20P6-gKB.js} +1 -1
  179. package/dist/web/assets/remote-desktop-window-content-20P6-gKB.js.br +0 -0
  180. package/dist/web/assets/remote-desktop-window-content-20P6-gKB.js.gz +0 -0
  181. package/dist/web/assets/{rename-object-dialog-BLnF9xyn.js → rename-object-dialog-Bs7pUsUP.js} +1 -1
  182. package/dist/web/assets/rename-object-dialog-Bs7pUsUP.js.br +0 -0
  183. package/dist/web/assets/rename-object-dialog-Bs7pUsUP.js.gz +0 -0
  184. package/dist/web/assets/{review-code-DG7sCIzr.js → review-code-Bjo4sHN9.js} +1 -1
  185. package/dist/web/assets/review-code-Bjo4sHN9.js.br +0 -0
  186. package/dist/web/assets/review-code-Bjo4sHN9.js.gz +0 -0
  187. package/dist/web/assets/{save-as-dialog-KW7cErWh.js → save-as-dialog-BHx0Wp2B.js} +1 -1
  188. package/dist/web/assets/save-as-dialog-BHx0Wp2B.js.br +0 -0
  189. package/dist/web/assets/save-as-dialog-BHx0Wp2B.js.gz +0 -0
  190. package/dist/web/assets/{schedules-settings-section-B4AV4ORv.js → schedules-settings-section-C6ATYDPK.js} +1 -1
  191. package/dist/web/assets/schedules-settings-section-C6ATYDPK.js.br +0 -0
  192. package/dist/web/assets/schedules-settings-section-C6ATYDPK.js.gz +0 -0
  193. package/dist/web/assets/{session-review-tab-C5nPyBzo.js → session-review-tab-BPMUyNTx.js} +1 -1
  194. package/dist/web/assets/session-review-tab-BPMUyNTx.js.br +0 -0
  195. package/dist/web/assets/session-review-tab-BPMUyNTx.js.gz +0 -0
  196. package/dist/web/assets/{settings-body-xWSmRXUD.js → settings-body-Dy5D6Vu7.js} +1 -1
  197. package/dist/web/assets/settings-body-Dy5D6Vu7.js.br +0 -0
  198. package/dist/web/assets/settings-body-Dy5D6Vu7.js.gz +0 -0
  199. package/dist/web/assets/{settings-rows-ChKBHk07.js → settings-rows-BRoYE2To.js} +1 -1
  200. package/dist/web/assets/settings-rows-BRoYE2To.js.br +0 -0
  201. package/dist/web/assets/settings-rows-BRoYE2To.js.gz +0 -0
  202. package/dist/web/assets/settings-section-content-C4DX7TYv.js +1 -0
  203. package/dist/web/assets/{settings-section-content-DdVdyPVX.js → settings-section-content-essUViKu.js} +2 -2
  204. package/dist/web/assets/settings-section-content-essUViKu.js.br +0 -0
  205. package/dist/web/assets/settings-section-content-essUViKu.js.gz +0 -0
  206. package/dist/web/assets/{settings-tab-CzaBBNoj.js → settings-tab-Di1DUP1j.js} +1 -1
  207. package/dist/web/assets/{settings-window-content-DAvWSu_O.js → settings-window-content-DYvYFPWy.js} +1 -1
  208. package/dist/web/assets/{slash-command-picker-Cn2Fdz2p.js → slash-command-picker-8da30mYP.js} +1 -1
  209. package/dist/web/assets/slash-command-picker-8da30mYP.js.br +0 -0
  210. package/dist/web/assets/slash-command-picker-8da30mYP.js.gz +0 -0
  211. package/dist/web/assets/{sql-object-tab-_mPRCvD9.js → sql-object-tab-DRIHVXuR.js} +1 -1
  212. package/dist/web/assets/sql-object-tab-DRIHVXuR.js.br +0 -0
  213. package/dist/web/assets/sql-object-tab-DRIHVXuR.js.gz +0 -0
  214. package/dist/web/assets/{sqlite-viewer-B8VZt-mA.js → sqlite-viewer-G-Jl2yEr.js} +1 -1
  215. package/dist/web/assets/sqlite-viewer-G-Jl2yEr.js.br +0 -0
  216. package/dist/web/assets/sqlite-viewer-G-Jl2yEr.js.gz +0 -0
  217. package/dist/web/assets/{structure-save-dialog-Lcst7kAl.js → structure-save-dialog-Cwikz0we.js} +1 -1
  218. package/dist/web/assets/structure-save-dialog-Cwikz0we.js.br +0 -0
  219. package/dist/web/assets/structure-save-dialog-Cwikz0we.js.gz +0 -0
  220. package/dist/web/assets/{structure-tab-ljJCzxUe.js → structure-tab-CWEgdZPm.js} +1 -1
  221. package/dist/web/assets/structure-tab-CWEgdZPm.js.br +0 -0
  222. package/dist/web/assets/structure-tab-CWEgdZPm.js.gz +0 -0
  223. package/dist/web/assets/{system-monitor-body-CTi5xUZz.js → system-monitor-body-BDQm9-wb.js} +2 -2
  224. package/dist/web/assets/system-monitor-body-BDQm9-wb.js.br +0 -0
  225. package/dist/web/assets/system-monitor-body-BDQm9-wb.js.gz +0 -0
  226. package/dist/web/assets/{system-monitor-tab-DeL2YoXO.js → system-monitor-tab-BouN21Yh.js} +1 -1
  227. package/dist/web/assets/{system-monitor-window-content-DK-rQIZh.js → system-monitor-window-content-BRyWBSOb.js} +1 -1
  228. package/dist/web/assets/{tab-host-window-content-CG0vPTc_.js → tab-host-window-content-CZ55-bTz.js} +1 -1
  229. package/dist/web/assets/tab-host-window-content-CZ55-bTz.js.br +0 -0
  230. package/dist/web/assets/tab-host-window-content-CZ55-bTz.js.gz +0 -0
  231. package/dist/web/assets/{table-tab-CTZ3nCWV.js → table-tab-Bmn4UdBV.js} +1 -1
  232. package/dist/web/assets/table-tab-Bmn4UdBV.js.br +0 -0
  233. package/dist/web/assets/table-tab-Bmn4UdBV.js.gz +0 -0
  234. package/dist/web/assets/{telegram-bot-token-field-DF7zq7_h.js → telegram-bot-token-field-fdHwRf39.js} +1 -1
  235. package/dist/web/assets/telegram-bot-token-field-fdHwRf39.js.br +0 -0
  236. package/dist/web/assets/telegram-bot-token-field-fdHwRf39.js.gz +0 -0
  237. package/dist/web/assets/{terminal-tab-B_jECWTH.js → terminal-tab-D-qlZXLK.js} +1 -1
  238. package/dist/web/assets/terminal-tab-D-qlZXLK.js.br +0 -0
  239. package/dist/web/assets/terminal-tab-D-qlZXLK.js.gz +0 -0
  240. package/dist/web/assets/{tool-cards-CsD_VXcG.js → tool-cards-BdXbQ9Fv.js} +2 -2
  241. package/dist/web/assets/tool-cards-BdXbQ9Fv.js.br +0 -0
  242. package/dist/web/assets/tool-cards-BdXbQ9Fv.js.gz +0 -0
  243. package/dist/web/assets/{tools-settings-section-DbBr0Hnr.js → tools-settings-section-CcPaspbR.js} +1 -1
  244. package/dist/web/assets/tools-settings-section-CcPaspbR.js.br +0 -0
  245. package/dist/web/assets/tools-settings-section-CcPaspbR.js.gz +0 -0
  246. package/dist/web/assets/{use-inline-blame-BA2mrMxT.js → use-inline-blame-Ck5XoSS3.js} +1 -1
  247. package/dist/web/assets/use-inline-blame-Ck5XoSS3.js.br +0 -0
  248. package/dist/web/assets/use-inline-blame-Ck5XoSS3.js.gz +0 -0
  249. package/dist/web/assets/{use-lsp-ChbrzwvS.js → use-lsp-CGSqfFBk.js} +1 -1
  250. package/dist/web/assets/use-lsp-CGSqfFBk.js.br +0 -0
  251. package/dist/web/assets/use-lsp-CGSqfFBk.js.gz +0 -0
  252. package/dist/web/assets/{use-monaco-theme-C3_wfz5J.js → use-monaco-theme-BW05eZS4.js} +1 -1
  253. package/dist/web/assets/{use-process-kill-C-YetnGd.js → use-process-kill-DJ_f7rCO.js} +1 -1
  254. package/dist/web/assets/use-process-kill-DJ_f7rCO.js.br +0 -0
  255. package/dist/web/assets/use-process-kill-DJ_f7rCO.js.gz +0 -0
  256. package/dist/web/assets/{use-sql-schema-info-pKwLD9wk.js → use-sql-schema-info-wt320fVR.js} +1 -1
  257. package/dist/web/assets/use-sql-schema-info-wt320fVR.js.br +0 -0
  258. package/dist/web/assets/use-sql-schema-info-wt320fVR.js.gz +0 -0
  259. package/dist/web/assets/{use-websocket-BFnUq0-x.js → use-websocket-CTwe74Cb.js} +1 -1
  260. package/dist/web/assets/{web-preview-tab-DGLPuZYM.js → web-preview-tab-D4XayUEK.js} +1 -1
  261. package/dist/web/assets/web-preview-tab-D4XayUEK.js.br +0 -0
  262. package/dist/web/assets/web-preview-tab-D4XayUEK.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/lessons-learned.md +30 -0
  270. package/package.json +1 -1
  271. package/src/server/index.ts +8 -0
  272. package/src/services/autostart-generator.ts +9 -1
  273. package/src/services/event-loop-lag.ts +44 -11
  274. package/src/services/machine-cpu.ts +40 -0
  275. package/src/services/process-priority.ts +153 -0
  276. package/src/services/process-thread-snapshot.ts +193 -0
  277. package/src/services/supervisor.ts +44 -2
  278. package/src/web/components/chat/question-card.tsx +10 -4
  279. package/dist/web/assets/accounts-settings-section-BTga1zwH.js.br +0 -0
  280. package/dist/web/assets/accounts-settings-section-BTga1zwH.js.gz +0 -0
  281. package/dist/web/assets/agent-session-sheet-Q3kasbrZ.js.br +0 -0
  282. package/dist/web/assets/agent-session-sheet-Q3kasbrZ.js.gz +0 -0
  283. package/dist/web/assets/agent-session-window-content-UNuEprIK.js.br +0 -0
  284. package/dist/web/assets/agent-session-window-content-UNuEprIK.js.gz +0 -0
  285. package/dist/web/assets/ai-resource-editor-BEwA6Do0.js.br +0 -0
  286. package/dist/web/assets/ai-resource-editor-BEwA6Do0.js.gz +0 -0
  287. package/dist/web/assets/ai-settings-section-DVKJPM0n.js.br +0 -0
  288. package/dist/web/assets/ai-settings-section-DVKJPM0n.js.gz +0 -0
  289. package/dist/web/assets/android-tab-BNa6R9fV.js.br +0 -0
  290. package/dist/web/assets/android-tab-BNa6R9fV.js.gz +0 -0
  291. package/dist/web/assets/appearance-settings-section-CjWFwWNn.js.br +0 -0
  292. package/dist/web/assets/appearance-settings-section-CjWFwWNn.js.gz +0 -0
  293. package/dist/web/assets/apps-panel-D4xjeMN9.js.br +0 -0
  294. package/dist/web/assets/apps-panel-D4xjeMN9.js.gz +0 -0
  295. package/dist/web/assets/branch-review-tab-BCW5294d.js.br +0 -0
  296. package/dist/web/assets/branch-review-tab-BCW5294d.js.gz +0 -0
  297. package/dist/web/assets/chat-tab-BG0M1D3d.js.br +0 -3
  298. package/dist/web/assets/chat-tab-BG0M1D3d.js.gz +0 -0
  299. package/dist/web/assets/chat-tab-DsW93LTL.js.br +0 -0
  300. package/dist/web/assets/chat-tab-DsW93LTL.js.gz +0 -0
  301. package/dist/web/assets/code-editor-AxLIpK-b.js.br +0 -0
  302. package/dist/web/assets/code-editor-AxLIpK-b.js.gz +0 -0
  303. package/dist/web/assets/codex-reset-credit-button-BnRmkBZu.js.br +0 -0
  304. package/dist/web/assets/codex-reset-credit-button-BnRmkBZu.js.gz +0 -0
  305. package/dist/web/assets/conflict-editor-BwuNUPAK.js.br +0 -0
  306. package/dist/web/assets/conflict-editor-BwuNUPAK.js.gz +0 -0
  307. package/dist/web/assets/connection-form-tab-CM2o80eB.js.br +0 -0
  308. package/dist/web/assets/connection-form-tab-CM2o80eB.js.gz +0 -0
  309. package/dist/web/assets/csv-preview-C7ErHm4U.js.br +0 -0
  310. package/dist/web/assets/csv-preview-C7ErHm4U.js.gz +0 -0
  311. package/dist/web/assets/db-tab-parts-C8jbkfxO.js.br +0 -0
  312. package/dist/web/assets/db-tab-parts-C8jbkfxO.js.gz +0 -0
  313. package/dist/web/assets/design-settings-section-BcGEY4Zf.js.br +0 -0
  314. package/dist/web/assets/design-settings-section-BcGEY4Zf.js.gz +0 -0
  315. package/dist/web/assets/design-tab-BGPYNFvy.js.br +0 -0
  316. package/dist/web/assets/design-tab-BGPYNFvy.js.gz +0 -0
  317. package/dist/web/assets/designs-sidebar-panel-BgMj7xRm.js.br +0 -0
  318. package/dist/web/assets/designs-sidebar-panel-BgMj7xRm.js.gz +0 -0
  319. package/dist/web/assets/diff-viewer-BKJkesHk.js.br +0 -0
  320. package/dist/web/assets/diff-viewer-BKJkesHk.js.gz +0 -0
  321. package/dist/web/assets/docx-preview-DD5oD_nL.js.br +0 -0
  322. package/dist/web/assets/docx-preview-DD5oD_nL.js.gz +0 -0
  323. package/dist/web/assets/editor-dialog--07lKJxl.js.br +0 -0
  324. package/dist/web/assets/editor-dialog--07lKJxl.js.gz +0 -0
  325. package/dist/web/assets/editor-language-service-51xjO5d3.js.br +0 -0
  326. package/dist/web/assets/editor-language-service-51xjO5d3.js.gz +0 -0
  327. package/dist/web/assets/explorer-body-BXK5SjJQ.js.br +0 -0
  328. package/dist/web/assets/explorer-body-BXK5SjJQ.js.gz +0 -0
  329. package/dist/web/assets/explorer-window-content-DKXcCmuX.js.br +0 -0
  330. package/dist/web/assets/explorer-window-content-DKXcCmuX.js.gz +0 -0
  331. package/dist/web/assets/extension-manager-section-CTpQP6te.js.br +0 -0
  332. package/dist/web/assets/extension-manager-section-CTpQP6te.js.gz +0 -0
  333. package/dist/web/assets/extension-webview-COT-yLrM.js.br +0 -0
  334. package/dist/web/assets/extension-webview-COT-yLrM.js.gz +0 -0
  335. package/dist/web/assets/files-settings-section-CgVR0fqi.js.br +0 -0
  336. package/dist/web/assets/files-settings-section-CgVR0fqi.js.gz +0 -0
  337. package/dist/web/assets/general-settings-section-BEMIbE4Y.js.br +0 -0
  338. package/dist/web/assets/general-settings-section-BEMIbE4Y.js.gz +0 -0
  339. package/dist/web/assets/git-log-panel-Cp2174iI.js.br +0 -0
  340. package/dist/web/assets/git-log-panel-Cp2174iI.js.gz +0 -0
  341. package/dist/web/assets/git-review-tab-CpMJ0qDi.js.br +0 -0
  342. package/dist/web/assets/git-review-tab-CpMJ0qDi.js.gz +0 -0
  343. package/dist/web/assets/glide-data-grid-BAhwWUhO.js.br +0 -0
  344. package/dist/web/assets/glide-data-grid-BAhwWUhO.js.gz +0 -0
  345. package/dist/web/assets/grid-save-dialog-DmJhaWOl.js.br +0 -0
  346. package/dist/web/assets/grid-save-dialog-DmJhaWOl.js.gz +0 -0
  347. package/dist/web/assets/group-chat-tab-Bq7-Yegf.js.br +0 -0
  348. package/dist/web/assets/group-chat-tab-Bq7-Yegf.js.gz +0 -0
  349. package/dist/web/assets/impexp-tab-COx3Cvs7.js.br +0 -0
  350. package/dist/web/assets/impexp-tab-COx3Cvs7.js.gz +0 -0
  351. package/dist/web/assets/index-BGk8EFQb.js.br +0 -0
  352. package/dist/web/assets/index-BGk8EFQb.js.gz +0 -0
  353. package/dist/web/assets/keyboard-shortcuts-section-Bqg7AZiZ.js.br +0 -0
  354. package/dist/web/assets/keyboard-shortcuts-section-Bqg7AZiZ.js.gz +0 -0
  355. package/dist/web/assets/language-servers-section-CJ47DSIA.js.br +0 -0
  356. package/dist/web/assets/language-servers-section-CJ47DSIA.js.gz +0 -0
  357. package/dist/web/assets/logs-app-BK-n-y_K.js.br +0 -0
  358. package/dist/web/assets/logs-app-BK-n-y_K.js.gz +0 -0
  359. package/dist/web/assets/lsp-client-DHSaAyJW.js.br +0 -0
  360. package/dist/web/assets/lsp-client-DHSaAyJW.js.gz +0 -0
  361. package/dist/web/assets/markdown-renderer-CpxdNG0f.js.br +0 -0
  362. package/dist/web/assets/markdown-renderer-CpxdNG0f.js.gz +0 -0
  363. package/dist/web/assets/mobile-explorer-sheet-BBwXGmXA.js.br +0 -0
  364. package/dist/web/assets/mobile-explorer-sheet-BBwXGmXA.js.gz +0 -0
  365. package/dist/web/assets/new-design-dialog-B_ZOvGhR.js.br +0 -0
  366. package/dist/web/assets/new-design-dialog-B_ZOvGhR.js.gz +0 -0
  367. package/dist/web/assets/notifications-settings-section-D7fIVFHe.js.br +0 -0
  368. package/dist/web/assets/notifications-settings-section-D7fIVFHe.js.gz +0 -0
  369. package/dist/web/assets/ppmbot-settings-section-JDvyI-gZ.js.br +0 -0
  370. package/dist/web/assets/ppmbot-settings-section-JDvyI-gZ.js.gz +0 -0
  371. package/dist/web/assets/problems-panel-Bsv_L6T2.js.br +0 -0
  372. package/dist/web/assets/problems-panel-Bsv_L6T2.js.gz +0 -0
  373. package/dist/web/assets/process-table-3kZyWOJx.js.br +0 -0
  374. package/dist/web/assets/process-table-3kZyWOJx.js.gz +0 -0
  375. package/dist/web/assets/proxy-settings-section-CYcmIp7W.js.br +0 -0
  376. package/dist/web/assets/proxy-settings-section-CYcmIp7W.js.gz +0 -0
  377. package/dist/web/assets/query-tab-CjrWnQUl.js.br +0 -0
  378. package/dist/web/assets/query-tab-CjrWnQUl.js.gz +0 -0
  379. package/dist/web/assets/remote-access-settings-section-iQkakiVx.js.br +0 -0
  380. package/dist/web/assets/remote-access-settings-section-iQkakiVx.js.gz +0 -0
  381. package/dist/web/assets/remote-desktop-clipboard-notice-CFLnb7ML.js.br +0 -0
  382. package/dist/web/assets/remote-desktop-clipboard-notice-CFLnb7ML.js.gz +0 -0
  383. package/dist/web/assets/remote-desktop-mobile-sheet-C_-_-Nkz.js.br +0 -0
  384. package/dist/web/assets/remote-desktop-mobile-sheet-C_-_-Nkz.js.gz +0 -0
  385. package/dist/web/assets/remote-desktop-mobile-view-BnHSrind.js.br +0 -0
  386. package/dist/web/assets/remote-desktop-mobile-view-BnHSrind.js.gz +0 -0
  387. package/dist/web/assets/remote-desktop-readiness-gate-BGHJ7Sv6.js.br +0 -0
  388. package/dist/web/assets/remote-desktop-readiness-gate-BGHJ7Sv6.js.gz +0 -0
  389. package/dist/web/assets/remote-desktop-window-content-BBm1YwM8.js.br +0 -0
  390. package/dist/web/assets/remote-desktop-window-content-BBm1YwM8.js.gz +0 -0
  391. package/dist/web/assets/rename-object-dialog-BLnF9xyn.js.br +0 -0
  392. package/dist/web/assets/rename-object-dialog-BLnF9xyn.js.gz +0 -0
  393. package/dist/web/assets/review-code-DG7sCIzr.js.br +0 -0
  394. package/dist/web/assets/review-code-DG7sCIzr.js.gz +0 -0
  395. package/dist/web/assets/save-as-dialog-KW7cErWh.js.br +0 -0
  396. package/dist/web/assets/save-as-dialog-KW7cErWh.js.gz +0 -0
  397. package/dist/web/assets/schedules-settings-section-B4AV4ORv.js.br +0 -0
  398. package/dist/web/assets/schedules-settings-section-B4AV4ORv.js.gz +0 -0
  399. package/dist/web/assets/session-review-tab-C5nPyBzo.js.br +0 -0
  400. package/dist/web/assets/session-review-tab-C5nPyBzo.js.gz +0 -0
  401. package/dist/web/assets/settings-body-xWSmRXUD.js.br +0 -0
  402. package/dist/web/assets/settings-body-xWSmRXUD.js.gz +0 -0
  403. package/dist/web/assets/settings-rows-ChKBHk07.js.br +0 -0
  404. package/dist/web/assets/settings-rows-ChKBHk07.js.gz +0 -0
  405. package/dist/web/assets/settings-section-content-BOh2jaWV.js +0 -1
  406. package/dist/web/assets/settings-section-content-DdVdyPVX.js.br +0 -0
  407. package/dist/web/assets/settings-section-content-DdVdyPVX.js.gz +0 -0
  408. package/dist/web/assets/slash-command-picker-Cn2Fdz2p.js.br +0 -0
  409. package/dist/web/assets/slash-command-picker-Cn2Fdz2p.js.gz +0 -0
  410. package/dist/web/assets/sql-object-tab-_mPRCvD9.js.br +0 -0
  411. package/dist/web/assets/sql-object-tab-_mPRCvD9.js.gz +0 -0
  412. package/dist/web/assets/sqlite-viewer-B8VZt-mA.js.br +0 -0
  413. package/dist/web/assets/sqlite-viewer-B8VZt-mA.js.gz +0 -0
  414. package/dist/web/assets/structure-save-dialog-Lcst7kAl.js.br +0 -0
  415. package/dist/web/assets/structure-save-dialog-Lcst7kAl.js.gz +0 -0
  416. package/dist/web/assets/structure-tab-ljJCzxUe.js.br +0 -0
  417. package/dist/web/assets/structure-tab-ljJCzxUe.js.gz +0 -0
  418. package/dist/web/assets/system-monitor-body-CTi5xUZz.js.br +0 -0
  419. package/dist/web/assets/system-monitor-body-CTi5xUZz.js.gz +0 -0
  420. package/dist/web/assets/tab-host-window-content-CG0vPTc_.js.br +0 -0
  421. package/dist/web/assets/tab-host-window-content-CG0vPTc_.js.gz +0 -0
  422. package/dist/web/assets/table-tab-CTZ3nCWV.js.br +0 -0
  423. package/dist/web/assets/table-tab-CTZ3nCWV.js.gz +0 -0
  424. package/dist/web/assets/telegram-bot-token-field-DF7zq7_h.js.br +0 -0
  425. package/dist/web/assets/telegram-bot-token-field-DF7zq7_h.js.gz +0 -0
  426. package/dist/web/assets/terminal-tab-B_jECWTH.js.br +0 -0
  427. package/dist/web/assets/terminal-tab-B_jECWTH.js.gz +0 -0
  428. package/dist/web/assets/tool-cards-CsD_VXcG.js.br +0 -0
  429. package/dist/web/assets/tool-cards-CsD_VXcG.js.gz +0 -0
  430. package/dist/web/assets/tools-settings-section-DbBr0Hnr.js.br +0 -0
  431. package/dist/web/assets/tools-settings-section-DbBr0Hnr.js.gz +0 -0
  432. package/dist/web/assets/use-inline-blame-BA2mrMxT.js.br +0 -0
  433. package/dist/web/assets/use-inline-blame-BA2mrMxT.js.gz +0 -0
  434. package/dist/web/assets/use-lsp-ChbrzwvS.js.br +0 -0
  435. package/dist/web/assets/use-lsp-ChbrzwvS.js.gz +0 -0
  436. package/dist/web/assets/use-process-kill-C-YetnGd.js.br +0 -0
  437. package/dist/web/assets/use-process-kill-C-YetnGd.js.gz +0 -0
  438. package/dist/web/assets/use-sql-schema-info-pKwLD9wk.js.br +0 -0
  439. package/dist/web/assets/use-sql-schema-info-pKwLD9wk.js.gz +0 -0
  440. package/dist/web/assets/web-preview-tab-DGLPuZYM.js.br +0 -0
  441. package/dist/web/assets/web-preview-tab-DGLPuZYM.js.gz +0 -0
@@ -9,7 +9,7 @@
9
9
  * a busy instance against 0.12-0.65ms on an idle one of the same binary), but
10
10
  * from outside it names no culprit.
11
11
  *
12
- * A late tick means one of two things, and they are fixed in different places:
12
+ * A late tick means one of three things, and they are fixed in different places:
13
13
  *
14
14
  * - **self** — the loop was busy with our own synchronous work, or with GC.
15
15
  * Worth hunting: a blocking call, a whole-file read, a synchronous DB write.
@@ -18,10 +18,17 @@
18
18
  * and one `claude` process per session. Nothing in this repository fixes
19
19
  * that; moving work to another thread least of all, since the CPU was never
20
20
  * ours to move it to.
21
+ * - **blocked** — we got little CPU while the machine had plenty to spare, so
22
+ * the thread was *waiting*: on a page-in, a disk read, a lock, a synchronous
23
+ * child process. This is what every 20-60s stall on 26/10/2026 turned out to
24
+ * be — the main thread in `PageIn` waits with the machine at 13% CPU, because
25
+ * the process ran at memory priority 2 and I/O priority Low (see
26
+ * `process-priority.ts`). They were logged as "starved" until this cause existed,
27
+ * which pointed at the one explanation that was wrong.
21
28
  *
22
- * Sampling `process.cpuUsage()` across the same gap tells them apart: time we
23
- * burned is ours, time we did not is somebody else's. Without that, "the server
24
- * stalled" is a fact with two opposite conclusions.
29
+ * Sampling `process.cpuUsage()` across the same gap separates our work from not
30
+ * ours; the machine's CPU across it separates starved from blocked. Without both,
31
+ * "the server stalled" is a fact with opposite conclusions.
25
32
  *
26
33
  * The monitor costs one timer and two counter reads per tick. It holds its
27
34
  * samples in memory, and only a stall of `LOG_STALL_MS` or more also reaches
@@ -31,6 +38,7 @@
31
38
  * left nothing behind once the process restarted.
32
39
  */
33
40
  import { createLogger } from "./logger.ts";
41
+ import { machineBusyFraction, readMachineCpuTimes, type MachineCpuTimes } from "./machine-cpu.ts";
34
42
 
35
43
  const log = createLogger("event-loop");
36
44
 
@@ -55,7 +63,13 @@ export const SELF_RATIO = 0.7;
55
63
  /** CPU/wall at or below this: the process was not running for most of the gap. */
56
64
  export const STARVED_RATIO = 0.25;
57
65
 
58
- export type LagCause = "self" | "starved" | "mixed";
66
+ /**
67
+ * Machine-wide busy share at or above which a process not running counts as starved.
68
+ * Below it there were free cores, so not running means the thread was waiting.
69
+ */
70
+ export const MACHINE_BUSY_FRACTION = 0.8;
71
+
72
+ export type LagCause = "self" | "starved" | "blocked" | "mixed";
59
73
 
60
74
  export interface LagSample {
61
75
  /** Epoch ms at which the late tick finally ran. */
@@ -66,6 +80,8 @@ export interface LagSample {
66
80
  cpuMs: number;
67
81
  /** Resident set at that moment, to spot a stall that is really GC pressure. */
68
82
  rssMb: number;
83
+ /** Share of every core on the machine that was busy across the gap, 0-100; absent when unknown. */
84
+ machineCpuPct?: number;
69
85
  cause: LagCause;
70
86
  }
71
87
 
@@ -96,12 +112,17 @@ export interface LagReport {
96
112
  * The ratio can exceed 1: `cpuUsage` counts every thread, and Bun runs file
97
113
  * system work on a pool, so a gap spent in parallel IO bills more CPU than wall
98
114
  * time passed. That is still our own work, which is what the answer says.
115
+ *
116
+ * Without a machine reading a gap we spent off the CPU stays "starved", the only
117
+ * answer there was before the machine was measured.
99
118
  */
100
- export function classifyLag(lagMs: number, cpuMs: number): LagCause {
119
+ export function classifyLag(lagMs: number, cpuMs: number, machineBusy?: number | null): LagCause {
101
120
  if (lagMs <= 0) return "self";
102
121
  const ratio = cpuMs / lagMs;
103
122
  if (ratio >= SELF_RATIO) return "self";
104
- if (ratio <= STARVED_RATIO) return "starved";
123
+ if (ratio <= STARVED_RATIO) {
124
+ return machineBusy != null && machineBusy < MACHINE_BUSY_FRACTION ? "blocked" : "starved";
125
+ }
105
126
  return "mixed";
106
127
  }
107
128
 
@@ -116,18 +137,22 @@ export function sampleFromTick(args: {
116
137
  elapsedMs: number;
117
138
  cpuMicros: number;
118
139
  rssBytes: number;
140
+ /** Machine-wide busy share across the same gap, 0-1, when it was measured. */
141
+ machineBusy?: number | null;
119
142
  thresholdMs?: number;
120
143
  }): LagSample | null {
121
144
  const lagMs = args.elapsedMs - TICK_MS;
122
145
  if (lagMs < (args.thresholdMs ?? REPORT_THRESHOLD_MS)) return null;
123
146
  const cpuMs = args.cpuMicros / 1000;
124
- return {
147
+ const sample: LagSample = {
125
148
  at: args.now,
126
149
  lagMs: Math.round(lagMs),
127
150
  cpuMs: Math.round(cpuMs),
128
151
  rssMb: Math.round(args.rssBytes / 1048576),
129
- cause: classifyLag(lagMs, cpuMs),
152
+ cause: classifyLag(lagMs, cpuMs, args.machineBusy),
130
153
  };
154
+ if (args.machineBusy != null) sample.machineCpuPct = Math.round(args.machineBusy * 100);
155
+ return sample;
131
156
  }
132
157
 
133
158
  // ─── Monitor ───────────────────────────────────────────────────────────
@@ -137,6 +162,7 @@ let samples: LagSample[] = [];
137
162
  let startedAt = 0;
138
163
  let lastAt = 0;
139
164
  let lastCpu: { user: number; system: number } | null = null;
165
+ let lastMachine: MachineCpuTimes | null = null;
140
166
  let ticks = 0;
141
167
  let stalls = 0;
142
168
  let blockedMs = 0;
@@ -156,7 +182,8 @@ export function logLongStall(sample: LagSample): void {
156
182
  return;
157
183
  }
158
184
  const more = longStallsNotLogged ? ` (+${longStallsNotLogged} more of ${LOG_STALL_MS}ms+ since the last one logged)` : "";
159
- log.warn(`Event loop stalled ${sample.lagMs}ms (cause=${sample.cause}, cpu=${sample.cpuMs}ms, rss=${sample.rssMb}MB)${more}`);
185
+ const machine = sample.machineCpuPct === undefined ? "" : `, machineCpu=${sample.machineCpuPct}%`;
186
+ log.warn(`Event loop stalled ${sample.lagMs}ms (cause=${sample.cause}, cpu=${sample.cpuMs}ms${machine}, rss=${sample.rssMb}MB)${more}`);
160
187
  lastStallLoggedAt = sample.at;
161
188
  longStallsNotLogged = 0;
162
189
  }
@@ -168,8 +195,11 @@ function tick(): void {
168
195
  const cpuMicros = lastCpu
169
196
  ? cpu.user - lastCpu.user + (cpu.system - lastCpu.system)
170
197
  : 0;
198
+ const machine = readMachineCpuTimes();
199
+ const machineBusy = machineBusyFraction(lastMachine, machine);
171
200
  lastAt = now;
172
201
  lastCpu = cpu;
202
+ lastMachine = machine;
173
203
  ticks++;
174
204
 
175
205
  const sample = sampleFromTick({
@@ -177,6 +207,7 @@ function tick(): void {
177
207
  elapsedMs,
178
208
  cpuMicros,
179
209
  rssBytes: process.memoryUsage.rss(),
210
+ machineBusy,
180
211
  });
181
212
  if (!sample) return;
182
213
 
@@ -194,6 +225,7 @@ export function startLagMonitor(): void {
194
225
  startedAt = Date.now();
195
226
  lastAt = startedAt;
196
227
  lastCpu = process.cpuUsage();
228
+ lastMachine = readMachineCpuTimes();
197
229
  ticks = 0;
198
230
  stalls = 0;
199
231
  blockedMs = 0;
@@ -211,11 +243,12 @@ export function stopLagMonitor(): void {
211
243
  clearInterval(timer);
212
244
  timer = null;
213
245
  lastCpu = null;
246
+ lastMachine = null;
214
247
  }
215
248
 
216
249
  export function lagReport(): LagReport {
217
250
  const uptimeMs = startedAt ? Date.now() - startedAt : 0;
218
- const byCause: Record<LagCause, number> = { self: 0, starved: 0, mixed: 0 };
251
+ const byCause: Record<LagCause, number> = { self: 0, starved: 0, blocked: 0, mixed: 0 };
219
252
  for (const s of samples) byCause[s.cause]++;
220
253
  return {
221
254
  running: timer !== null,
@@ -0,0 +1,40 @@
1
+ /**
2
+ * How busy the whole machine's CPUs were between two readings.
3
+ *
4
+ * A process that got little CPU during a stall was either *starved* (every core was
5
+ * busy with someone else) or *blocked* (cores were free, and it was waiting on a disk
6
+ * read, a page-in, a lock or a child process). Its own CPU time cannot tell the two
7
+ * apart; the machine's can.
8
+ */
9
+ import { cpus } from "node:os";
10
+
11
+ export interface MachineCpuTimes {
12
+ /** Sum over every core of time spent not idle, in ms. */
13
+ busy: number;
14
+ /** Sum over every core of all time, in ms. */
15
+ total: number;
16
+ }
17
+
18
+ export function readMachineCpuTimes(): MachineCpuTimes | null {
19
+ try {
20
+ let busy = 0;
21
+ let total = 0;
22
+ for (const c of cpus()) {
23
+ const t = c.times;
24
+ const all = t.user + t.nice + t.sys + t.idle + t.irq;
25
+ total += all;
26
+ busy += all - t.idle;
27
+ }
28
+ return total > 0 ? { busy, total } : null;
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+
34
+ /** Share of all cores that were busy between `a` and `b`, 0-1; null when it cannot be told. */
35
+ export function machineBusyFraction(a: MachineCpuTimes | null, b: MachineCpuTimes | null): number | null {
36
+ if (!a || !b) return null;
37
+ const total = b.total - a.total;
38
+ if (total <= 0) return null;
39
+ return Math.min(1, Math.max(0, (b.busy - a.busy) / total));
40
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * This process's CPU, memory and I/O priority on Windows, and raising them back to normal.
3
+ *
4
+ * A process started by a Task Scheduler task at the default priority 7 — which is how
5
+ * PPM's autostart task was registered until it was changed to 5 — runs below normal on
6
+ * CPU, at memory priority 2 instead of 5, and at I/O priority Low instead of Normal.
7
+ * Every child inherits that, the server included. Windows takes pages from a
8
+ * low-memory-priority process first, and reading them back happens at that process's
9
+ * I/O priority. Measured on 26/10/2026: the server's main thread sat in `PageIn` waits
10
+ * for 23s on an idle NVMe disk with the machine at 13% CPU, and stalls like it, 20-60s
11
+ * long and several an hour, were what the supervisor's health check kept killing it for.
12
+ * Raising the three priorities to normal on the running process needs no privilege.
13
+ *
14
+ * Only ever raises: a priority someone set higher on purpose is left alone.
15
+ * Every read is `undefined` when it fails, and nothing here throws.
16
+ */
17
+
18
+ export interface ProcessPriority {
19
+ /** Priority class name, e.g. "normal", "below-normal". */
20
+ cpu?: string;
21
+ /** Memory priority, 1 (very low) to 5 (normal). */
22
+ memory?: number;
23
+ /** I/O priority, 0 (very low) to 2 (normal); 3 is high. */
24
+ io?: number;
25
+ }
26
+
27
+ const CLASS_NAMES: Record<number, string> = {
28
+ 0x40: "idle",
29
+ 0x4000: "below-normal",
30
+ 0x20: "normal",
31
+ 0x8000: "above-normal",
32
+ 0x80: "high",
33
+ 0x100: "realtime",
34
+ };
35
+ const NORMAL_PRIORITY_CLASS = 0x20;
36
+ /** Classes below normal, the only ones this raises. */
37
+ const LOW_CLASSES = new Set(["idle", "below-normal"]);
38
+ export const MEMORY_PRIORITY_NORMAL = 5;
39
+ export const IO_PRIORITY_NORMAL = 2;
40
+
41
+ /** `GetProcessInformation` / `SetProcessInformation` class. */
42
+ const PROCESS_MEMORY_PRIORITY = 0;
43
+ /** `NtQueryInformationProcess` / `NtSetInformationProcess` class `ProcessIoPriority`. */
44
+ const PROCESS_IO_PRIORITY = 33;
45
+ /** `(HANDLE)-1`, the pseudo handle for the calling process. */
46
+ const CURRENT_PROCESS = -1n;
47
+
48
+ /** What to change to bring `p` up to normal; an empty object when nothing is below it. Exported for tests. */
49
+ export function priorityRaisePlan(p: ProcessPriority): { cpu?: true; memory?: true; io?: true } {
50
+ const plan: { cpu?: true; memory?: true; io?: true } = {};
51
+ if (p.cpu !== undefined && LOW_CLASSES.has(p.cpu)) plan.cpu = true;
52
+ if (p.memory !== undefined && p.memory < MEMORY_PRIORITY_NORMAL) plan.memory = true;
53
+ if (p.io !== undefined && p.io < IO_PRIORITY_NORMAL) plan.io = true;
54
+ return plan;
55
+ }
56
+
57
+ /** "cpu=below-normal memory=2/5 io=1/2", for one log line. */
58
+ export function describePriority(p: ProcessPriority): string {
59
+ const v = (x: unknown) => (x === undefined ? "?" : String(x));
60
+ return `cpu=${v(p.cpu)} memory=${v(p.memory)}/${MEMORY_PRIORITY_NORMAL} io=${v(p.io)}/${IO_PRIORITY_NORMAL}`;
61
+ }
62
+
63
+ type Kernel32 = {
64
+ GetPriorityClass: (process: bigint) => number;
65
+ SetPriorityClass: (process: bigint, cls: number) => number;
66
+ GetProcessInformation: (process: bigint, cls: number, info: number, size: number) => number;
67
+ SetProcessInformation: (process: bigint, cls: number, info: number, size: number) => number;
68
+ };
69
+ type Ntdll = {
70
+ NtQueryInformationProcess: (process: bigint, cls: number, info: number, size: number, ret: number | null) => number;
71
+ NtSetInformationProcess: (process: bigint, cls: number, info: number, size: number) => number;
72
+ };
73
+
74
+ let libs: { k32: Kernel32; nt: Ntdll } | null | undefined;
75
+
76
+ async function load(): Promise<{ k32: Kernel32; nt: Ntdll } | null> {
77
+ if (libs !== undefined) return libs;
78
+ try {
79
+ const { dlopen, FFIType } = await import("bun:ffi");
80
+ const k32 = dlopen("kernel32.dll", {
81
+ GetPriorityClass: { args: [FFIType.i64], returns: FFIType.u32 },
82
+ SetPriorityClass: { args: [FFIType.i64, FFIType.u32], returns: FFIType.i32 },
83
+ GetProcessInformation: { args: [FFIType.i64, FFIType.i32, FFIType.ptr, FFIType.u32], returns: FFIType.i32 },
84
+ SetProcessInformation: { args: [FFIType.i64, FFIType.i32, FFIType.ptr, FFIType.u32], returns: FFIType.i32 },
85
+ }).symbols as unknown as Kernel32;
86
+ const nt = dlopen("ntdll.dll", {
87
+ NtQueryInformationProcess: { args: [FFIType.i64, FFIType.i32, FFIType.ptr, FFIType.u32, FFIType.ptr], returns: FFIType.i32 },
88
+ NtSetInformationProcess: { args: [FFIType.i64, FFIType.i32, FFIType.ptr, FFIType.u32], returns: FFIType.i32 },
89
+ }).symbols as unknown as Ntdll;
90
+ libs = { k32, nt };
91
+ } catch {
92
+ libs = null;
93
+ }
94
+ return libs;
95
+ }
96
+
97
+ /** This process's priorities. Empty off Windows or when the calls are unavailable. */
98
+ export async function readProcessPriority(platform: NodeJS.Platform = process.platform): Promise<ProcessPriority> {
99
+ if (platform !== "win32") return {};
100
+ try {
101
+ const l = await load();
102
+ if (!l) return {};
103
+ const { ptr } = await import("bun:ffi");
104
+ const out: ProcessPriority = {};
105
+ const cls = l.k32.GetPriorityClass(CURRENT_PROCESS);
106
+ if (cls) out.cpu = CLASS_NAMES[cls] ?? `0x${cls.toString(16)}`;
107
+ const mem = new Uint32Array(1);
108
+ if (l.k32.GetProcessInformation(CURRENT_PROCESS, PROCESS_MEMORY_PRIORITY, ptr(mem), 4) !== 0) out.memory = mem[0];
109
+ const io = new Uint32Array(1);
110
+ if (l.nt.NtQueryInformationProcess(CURRENT_PROCESS, PROCESS_IO_PRIORITY, ptr(io), 4, null) === 0) out.io = io[0];
111
+ return out;
112
+ } catch {
113
+ return {};
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Raise whatever is below normal to normal. Returns the reading before and after, so the
119
+ * caller can log what it found — a below-normal reading is the evidence, whether or not
120
+ * the raise worked.
121
+ */
122
+ export async function raiseToNormalPriority(
123
+ platform: NodeJS.Platform = process.platform,
124
+ ): Promise<{ before: ProcessPriority; after: ProcessPriority; changed: boolean }> {
125
+ const before = await readProcessPriority(platform);
126
+ const plan = priorityRaisePlan(before);
127
+ if (!plan.cpu && !plan.memory && !plan.io) return { before, after: before, changed: false };
128
+ try {
129
+ const l = await load();
130
+ if (!l) return { before, after: before, changed: false };
131
+ const { ptr } = await import("bun:ffi");
132
+ if (plan.cpu) l.k32.SetPriorityClass(CURRENT_PROCESS, NORMAL_PRIORITY_CLASS);
133
+ if (plan.memory) {
134
+ const mem = new Uint32Array([MEMORY_PRIORITY_NORMAL]);
135
+ l.k32.SetProcessInformation(CURRENT_PROCESS, PROCESS_MEMORY_PRIORITY, ptr(mem), 4);
136
+ }
137
+ if (plan.io) {
138
+ const io = new Uint32Array([IO_PRIORITY_NORMAL]);
139
+ l.nt.NtSetInformationProcess(CURRENT_PROCESS, PROCESS_IO_PRIORITY, ptr(io), 4);
140
+ }
141
+ } catch { /* the reading after says what took */ }
142
+ return { before, after: await readProcessPriority(platform), changed: true };
143
+ }
144
+
145
+ /**
146
+ * Raise to normal and log it — only when something was below normal, since that reading
147
+ * is the one worth keeping: it says the process ran demoted until this moment.
148
+ */
149
+ export async function logPriorityRaise(role: string, warn: (message: string) => void): Promise<void> {
150
+ const { before, after, changed } = await raiseToNormalPriority();
151
+ if (!changed) return;
152
+ warn(`${role} started below normal priority (${describePriority(before)}), raised to ${describePriority(after)}`);
153
+ }
@@ -0,0 +1,193 @@
1
+ /**
2
+ * What another process's main thread is doing right now, read from outside it.
3
+ *
4
+ * When the server stops answering, nothing inside it can say why: its event loop is
5
+ * the thing that stopped. The supervisor can still look, and one reading taken while
6
+ * the health probe is timing out names the wait — `WrPageIn` (memory being read back
7
+ * from disk), `Executive` (a kernel wait such as a synchronous file read), `WrQueue`
8
+ * (idle, so the loop is fine and the probe was slow elsewhere), or running (our own
9
+ * synchronous work). That one word is what located the 20-60s stalls of 26/10/2026,
10
+ * which were invisible to every log PPM kept: an outside watcher found the main
11
+ * thread in `PageIn` for 23s with the machine at 13% CPU.
12
+ *
13
+ * Also returned: the process's hard page faults and CPU time, so two readings give a
14
+ * rate. Windows reads all of it with one `NtQuerySystemInformation` call; Linux reads
15
+ * `/proc/<pid>/stat` and the main task's `wchan`. Every field is `undefined` when it
16
+ * cannot be read, and nothing here throws.
17
+ */
18
+ import { readFileSync } from "node:fs";
19
+
20
+ export interface ThreadSnapshot {
21
+ /** Main thread scheduler state: "running", "waiting", "ready", … (Linux: R, S, D, … spelled out). */
22
+ state?: string;
23
+ /** Windows `KWAIT_REASON` name while waiting; Linux `wchan` (the kernel function it sleeps in). */
24
+ waitReason?: string;
25
+ threads?: number;
26
+ /** Page faults that went to disk, since the process started. */
27
+ hardFaults?: number;
28
+ /** User + kernel CPU of the whole process since it started, in ms. */
29
+ cpuMs?: number;
30
+ }
31
+
32
+ const THREAD_STATES = [
33
+ "initialized", "ready", "running", "standby", "terminated", "waiting", "transition",
34
+ "deferred-ready", "gate-wait", "waiting-for-outswap",
35
+ ];
36
+
37
+ /** `KWAIT_REASON`, in order. */
38
+ const WAIT_REASONS = [
39
+ "Executive", "FreePage", "PageIn", "PoolAllocation", "DelayExecution", "Suspended", "UserRequest",
40
+ "WrExecutive", "WrFreePage", "WrPageIn", "WrPoolAllocation", "WrDelayExecution", "WrSuspended",
41
+ "WrUserRequest", "WrSpare0", "WrQueue", "WrLpcReceive", "WrLpcReply", "WrVirtualMemory", "WrPageOut",
42
+ "WrRendezvous", "WrKeyedEvent", "WrTerminated", "WrProcessInSwap", "WrCpuRateControl",
43
+ "WrCalloutStack", "WrKernel", "WrResource", "WrPushLock", "WrMutex", "WrQuantumEnd",
44
+ "WrDispatchInt", "WrPreempted", "WrYieldExecution", "WrFastMutex", "WrGuardedMutex", "WrRundown",
45
+ "WrAlertByThreadId", "WrDeferredPreempt", "WrPhysicalFault",
46
+ ];
47
+
48
+ // SYSTEM_PROCESS_INFORMATION on 64-bit Windows.
49
+ const P_NEXT = 0x00;
50
+ const P_THREADS = 0x04;
51
+ const P_HARD_FAULTS = 0x10;
52
+ const P_USER_TIME = 0x28;
53
+ const P_KERNEL_TIME = 0x30;
54
+ const P_PID = 0x50;
55
+ const P_SIZE = 0x100;
56
+ // SYSTEM_THREAD_INFORMATION, which follow it one after another.
57
+ const T_CREATE_TIME = 0x10;
58
+ const T_STATE = 0x44;
59
+ const T_WAIT_REASON = 0x48;
60
+ const T_SIZE = 0x50;
61
+ /** Process and thread times are in 100 ns units. */
62
+ const TICKS_PER_MS = 10_000n;
63
+
64
+ /**
65
+ * Find `pid` in a `SystemProcessInformation` buffer and describe its main thread — the
66
+ * one created first. Exported for tests.
67
+ */
68
+ export function parseWindowsProcessInformation(view: DataView, pid: number): ThreadSnapshot | null {
69
+ let at = 0;
70
+ for (;;) {
71
+ if (at + P_SIZE > view.byteLength) return null;
72
+ if (Number(view.getBigUint64(at + P_PID, true)) === pid) {
73
+ const threads = view.getUint32(at + P_THREADS, true);
74
+ const cpu = view.getBigInt64(at + P_USER_TIME, true) + view.getBigInt64(at + P_KERNEL_TIME, true);
75
+ const out: ThreadSnapshot = {
76
+ threads,
77
+ hardFaults: view.getUint32(at + P_HARD_FAULTS, true),
78
+ cpuMs: Number(cpu / TICKS_PER_MS),
79
+ };
80
+ let main = -1;
81
+ let mainCreated = 0n;
82
+ for (let i = 0; i < threads; i++) {
83
+ const t = at + P_SIZE + i * T_SIZE;
84
+ if (t + T_SIZE > view.byteLength) break;
85
+ const created = view.getBigInt64(t + T_CREATE_TIME, true);
86
+ if (main === -1 || created < mainCreated) { main = t; mainCreated = created; }
87
+ }
88
+ if (main !== -1) {
89
+ const state = view.getUint32(main + T_STATE, true);
90
+ out.state = THREAD_STATES[state] ?? `state-${state}`;
91
+ if (out.state === "waiting") {
92
+ const reason = view.getUint32(main + T_WAIT_REASON, true);
93
+ out.waitReason = WAIT_REASONS[reason] ?? `reason-${reason}`;
94
+ }
95
+ }
96
+ return out;
97
+ }
98
+ const next = view.getUint32(at + P_NEXT, true);
99
+ if (next === 0) return null;
100
+ at += next;
101
+ }
102
+ }
103
+
104
+ type Ntdll = { NtQuerySystemInformation: (cls: number, buf: number, len: number, ret: number) => number };
105
+ let ntdll: Ntdll | null | undefined;
106
+
107
+ const SYSTEM_PROCESS_INFORMATION = 5;
108
+ const STATUS_INFO_LENGTH_MISMATCH = 0xc0000004 | 0;
109
+ /** Grows on a length mismatch; a busy machine needs ~1-2 MB. */
110
+ let bufferSize = 2 * 1024 * 1024;
111
+
112
+ async function readWindows(pid: number): Promise<ThreadSnapshot | null> {
113
+ if (ntdll === undefined) {
114
+ try {
115
+ const { dlopen, FFIType } = await import("bun:ffi");
116
+ ntdll = dlopen("ntdll.dll", {
117
+ NtQuerySystemInformation: { args: [FFIType.i32, FFIType.ptr, FFIType.u32, FFIType.ptr], returns: FFIType.i32 },
118
+ }).symbols as unknown as Ntdll;
119
+ } catch {
120
+ ntdll = null;
121
+ }
122
+ }
123
+ if (!ntdll) return null;
124
+ const { ptr } = await import("bun:ffi");
125
+ for (let attempt = 0; attempt < 4; attempt++) {
126
+ const buf = new Uint8Array(bufferSize);
127
+ const needed = new Uint32Array(1);
128
+ const status = ntdll.NtQuerySystemInformation(SYSTEM_PROCESS_INFORMATION, ptr(buf), buf.byteLength, ptr(needed));
129
+ if (status === STATUS_INFO_LENGTH_MISMATCH) {
130
+ bufferSize = Math.max(bufferSize * 2, (needed[0] ?? 0) + 256 * 1024);
131
+ continue;
132
+ }
133
+ if (status !== 0) return null;
134
+ return parseWindowsProcessInformation(new DataView(buf.buffer), pid);
135
+ }
136
+ return null;
137
+ }
138
+
139
+ const LINUX_STATES: Record<string, string> = {
140
+ R: "running", S: "sleeping", D: "disk-wait", T: "stopped", t: "traced", Z: "zombie", X: "dead",
141
+ I: "idle", W: "paging", P: "parked",
142
+ };
143
+
144
+ /** `/proc/<pid>/stat`: state, threads, major faults, CPU. Exported for tests. */
145
+ export function parseLinuxStat(text: string, clockTicksPerSecond = 100): ThreadSnapshot | null {
146
+ // The command name is in parentheses and may itself hold spaces or parentheses.
147
+ const close = text.lastIndexOf(")");
148
+ if (close === -1) return null;
149
+ const f = text.slice(close + 2).trim().split(/\s+/);
150
+ // f[0] is field 3 (state); field N is f[N - 3].
151
+ const state = f[0];
152
+ const majflt = Number(f[12 - 3]);
153
+ const utime = Number(f[14 - 3]);
154
+ const stime = Number(f[15 - 3]);
155
+ const threads = Number(f[20 - 3]);
156
+ const out: ThreadSnapshot = {};
157
+ if (state) out.state = LINUX_STATES[state] ?? state;
158
+ if (Number.isFinite(majflt)) out.hardFaults = majflt;
159
+ if (Number.isFinite(utime) && Number.isFinite(stime)) out.cpuMs = Math.round(((utime + stime) * 1000) / clockTicksPerSecond);
160
+ if (Number.isFinite(threads)) out.threads = threads;
161
+ return out;
162
+ }
163
+
164
+ function readLinux(pid: number): ThreadSnapshot | null {
165
+ const out = parseLinuxStat(readFileSync(`/proc/${pid}/stat`, "utf8"));
166
+ if (!out) return null;
167
+ // The process's own stat reports the main task's state; wchan says where it sleeps.
168
+ try {
169
+ const wchan = readFileSync(`/proc/${pid}/task/${pid}/wchan`, "utf8").trim();
170
+ if (wchan && wchan !== "0") out.waitReason = wchan;
171
+ } catch { /* wchan is optional, and hidden on some kernels */ }
172
+ return out;
173
+ }
174
+
175
+ /** One reading of `pid`. Null when the platform cannot be read or the process is gone. */
176
+ export async function readThreadSnapshot(pid: number, platform: NodeJS.Platform = process.platform): Promise<ThreadSnapshot | null> {
177
+ try {
178
+ if (platform === "win32") return await readWindows(pid);
179
+ if (platform === "linux") return readLinux(pid);
180
+ } catch { /* fall through */ }
181
+ return null;
182
+ }
183
+
184
+ /** "main thread waiting/WrPageIn, hard faults +38, cpu +120ms" — relative to `previous` when given. */
185
+ export function describeThreadSnapshot(now: ThreadSnapshot, previous?: ThreadSnapshot | null): string {
186
+ const parts: string[] = [];
187
+ if (now.state) parts.push(`main thread ${now.state}${now.waitReason ? `/${now.waitReason}` : ""}`);
188
+ if (now.hardFaults !== undefined) {
189
+ parts.push(previous?.hardFaults !== undefined ? `hard faults +${now.hardFaults - previous.hardFaults}` : `hard faults ${now.hardFaults}`);
190
+ }
191
+ if (now.cpuMs !== undefined && previous?.cpuMs !== undefined) parts.push(`server cpu +${now.cpuMs - previous.cpuMs}ms`);
192
+ return parts.join(", ");
193
+ }
@@ -41,6 +41,9 @@ import {
41
41
  } from "./edge-target-resolver.ts";
42
42
  import { EDGE_PRIOR_DEATHS_ENV } from "./edge-forwarder.ts";
43
43
  import { PLIST_LABEL } from "./autostart-generator.ts";
44
+ import { logPriorityRaise } from "./process-priority.ts";
45
+ import { readThreadSnapshot, describeThreadSnapshot, type ThreadSnapshot } from "./process-thread-snapshot.ts";
46
+ import { readMachineCpuTimes, machineBusyFraction, type MachineCpuTimes } from "./machine-cpu.ts";
44
47
 
45
48
  // ─── Constants ─────────────────────────────────────────────────────────
46
49
  const MAX_RESTARTS = 10;
@@ -1193,6 +1196,41 @@ async function runDbBackupTick(reason: "hourly" | "start"): Promise<void> {
1193
1196
  }
1194
1197
 
1195
1198
  // ─── Health checks ─────────────────────────────────────────────────────
1199
+
1200
+ /**
1201
+ * The server and the machine as of the last probe, so a failed probe can report rates
1202
+ * over the 30s before it rather than totals since boot.
1203
+ */
1204
+ let serverBaseline: { pid: number; thread: ThreadSnapshot | null; machine: MachineCpuTimes | null } | null = null;
1205
+
1206
+ async function recordServerBaseline(pid: number | undefined): Promise<void> {
1207
+ if (!pid) return;
1208
+ serverBaseline = { pid, thread: await readThreadSnapshot(pid), machine: readMachineCpuTimes() };
1209
+ }
1210
+
1211
+ /**
1212
+ * What the server is doing while it does not answer: its main thread's wait, the
1213
+ * page faults that went to disk and its CPU since the last probe, and how busy the
1214
+ * machine was. A failed probe used to say only "timeout", which left a 60s stall with
1215
+ * no cause on record once the server was restarted.
1216
+ */
1217
+ async function describeUnresponsiveServer(pid: number | undefined): Promise<string> {
1218
+ if (!pid) return "";
1219
+ try {
1220
+ const thread = await readThreadSnapshot(pid);
1221
+ const machine = readMachineCpuTimes();
1222
+ const before = serverBaseline?.pid === pid ? serverBaseline : null;
1223
+ const parts: string[] = [];
1224
+ if (thread) parts.push(describeThreadSnapshot(thread, before?.thread));
1225
+ const busy = machineBusyFraction(before?.machine ?? null, machine);
1226
+ if (busy !== null) parts.push(`machine cpu ${Math.round(busy * 100)}%`);
1227
+ serverBaseline = { pid, thread, machine };
1228
+ return parts.filter(Boolean).join(", ");
1229
+ } catch {
1230
+ return "";
1231
+ }
1232
+ }
1233
+
1196
1234
  function startServerHealthCheck() {
1197
1235
  healthTimer = setInterval(async () => {
1198
1236
  if (shuttingDown || getState() === "stopped") return;
@@ -1232,14 +1270,16 @@ function startServerHealthCheck() {
1232
1270
  const res = await fetch(`http://127.0.0.1:${checkPort}/api/health`, {
1233
1271
  signal: AbortSignal.timeout(5000),
1234
1272
  });
1235
- if (res.ok) { healthFailCount = 0; return; }
1273
+ if (res.ok) { healthFailCount = 0; await recordServerBaseline(serverChild?.pid); return; }
1236
1274
  lastProbe = `HTTP ${res.status}`;
1237
1275
  } catch (e) {
1238
1276
  const err = e as { name?: string; code?: string; message?: string } | null;
1239
1277
  lastProbe = err?.name === "TimeoutError" ? "timeout after 5s" : err?.code ?? err?.message ?? String(e);
1240
1278
  }
1241
1279
  healthFailCount++;
1242
- log("DEBUG", `Server health probe on port ${checkPort} failed (${healthFailCount}/${SERVER_HEALTH_FAIL_THRESHOLD}): ${lastProbe}`);
1280
+ // Read while the server is still not answering: the main thread's wait is the cause.
1281
+ const waitingOn = await describeUnresponsiveServer(serverChild?.pid);
1282
+ log("WARN", `Server health probe on port ${checkPort} failed (${healthFailCount}/${SERVER_HEALTH_FAIL_THRESHOLD}): ${lastProbe}${waitingOn ? ` — ${waitingOn}` : ""}`);
1243
1283
  if (healthFailCount >= SERVER_HEALTH_FAIL_THRESHOLD && serverChild) {
1244
1284
  log("ERROR", `Server unresponsive for ${healthFailCount}×${SERVER_HEALTH_INTERVAL_MS / 1000}s (last probe: ${lastProbe}) — killing PID ${serverChild.pid}`);
1245
1285
  const pid = serverChild.pid;
@@ -2020,6 +2060,8 @@ export async function runSupervisor(opts: {
2020
2060
  const logFd = openSync(logFile(), "a");
2021
2061
  startLogRotation();
2022
2062
  log("INFO", `Supervisor started (PID: ${process.pid}, port: ${opts.port}, share: ${opts.share})`);
2063
+ // Before any child is spawned: every child inherits these, the server included.
2064
+ await logPriorityRaise("supervisor", (m) => log("WARN", m));
2023
2065
 
2024
2066
  // ── Systemd self-heal: if the unit file is stale (e.g. still has
2025
2067
  // Restart=on-failure), we were likely spawned by the old Bun.spawn()
@@ -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