@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.
- package/CHANGELOG.md +13 -0
- package/CLAUDE.md +2 -0
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/cli-reference.md +4 -4
- package/assets/skills/ppm/references/http-api.md +1 -1
- package/dist/web/assets/{accounts-settings-section-BTga1zwH.js → accounts-settings-section-DD88DRdx.js} +1 -1
- package/dist/web/assets/accounts-settings-section-DD88DRdx.js.br +0 -0
- package/dist/web/assets/accounts-settings-section-DD88DRdx.js.gz +0 -0
- package/dist/web/assets/{agent-session-sheet-Q3kasbrZ.js → agent-session-sheet-C0PhhLKR.js} +2 -2
- package/dist/web/assets/agent-session-sheet-C0PhhLKR.js.br +0 -0
- package/dist/web/assets/agent-session-sheet-C0PhhLKR.js.gz +0 -0
- package/dist/web/assets/{agent-session-window-content-UNuEprIK.js → agent-session-window-content-D0XZgh4U.js} +1 -1
- package/dist/web/assets/agent-session-window-content-D0XZgh4U.js.br +0 -0
- package/dist/web/assets/agent-session-window-content-D0XZgh4U.js.gz +0 -0
- package/dist/web/assets/{ai-resource-editor-BEwA6Do0.js → ai-resource-editor-CpfQB6A7.js} +1 -1
- package/dist/web/assets/ai-resource-editor-CpfQB6A7.js.br +0 -0
- package/dist/web/assets/ai-resource-editor-CpfQB6A7.js.gz +0 -0
- package/dist/web/assets/{ai-settings-section-DVKJPM0n.js → ai-settings-section-CQWBwWcM.js} +1 -1
- package/dist/web/assets/ai-settings-section-CQWBwWcM.js.br +0 -0
- package/dist/web/assets/ai-settings-section-CQWBwWcM.js.gz +0 -0
- package/dist/web/assets/{ai-settings-section-DlfY5lFT.js → ai-settings-section-DqE67XNC.js} +1 -1
- package/dist/web/assets/{android-tab-BNa6R9fV.js → android-tab-4Z4AgHz1.js} +1 -1
- package/dist/web/assets/android-tab-4Z4AgHz1.js.br +0 -0
- package/dist/web/assets/android-tab-4Z4AgHz1.js.gz +0 -0
- package/dist/web/assets/{appearance-settings-section-CjWFwWNn.js → appearance-settings-section-DjQjiJF7.js} +1 -1
- package/dist/web/assets/appearance-settings-section-DjQjiJF7.js.br +0 -0
- package/dist/web/assets/appearance-settings-section-DjQjiJF7.js.gz +0 -0
- package/dist/web/assets/{apps-panel-D4xjeMN9.js → apps-panel-_lxY9xbc.js} +1 -1
- package/dist/web/assets/apps-panel-_lxY9xbc.js.br +0 -0
- package/dist/web/assets/apps-panel-_lxY9xbc.js.gz +0 -0
- package/dist/web/assets/{branch-review-tab-BCW5294d.js → branch-review-tab-BzrhnFSs.js} +1 -1
- package/dist/web/assets/branch-review-tab-BzrhnFSs.js.br +0 -0
- package/dist/web/assets/branch-review-tab-BzrhnFSs.js.gz +0 -0
- package/dist/web/assets/{chat-tab-BG0M1D3d.js → chat-tab-BKHepkYi.js} +1 -1
- package/dist/web/assets/chat-tab-BKHepkYi.js.br +0 -0
- package/dist/web/assets/chat-tab-BKHepkYi.js.gz +0 -0
- package/dist/web/assets/{chat-tab-DsW93LTL.js → chat-tab-Cv8gYzcA.js} +4 -4
- package/dist/web/assets/chat-tab-Cv8gYzcA.js.br +0 -0
- package/dist/web/assets/chat-tab-Cv8gYzcA.js.gz +0 -0
- package/dist/web/assets/{code-editor-AxLIpK-b.js → code-editor-COWmNXO4.js} +4 -4
- package/dist/web/assets/code-editor-COWmNXO4.js.br +0 -0
- package/dist/web/assets/code-editor-COWmNXO4.js.gz +0 -0
- package/dist/web/assets/{codex-reset-credit-button-BnRmkBZu.js → codex-reset-credit-button-BHt4yJet.js} +1 -1
- package/dist/web/assets/codex-reset-credit-button-BHt4yJet.js.br +0 -0
- package/dist/web/assets/codex-reset-credit-button-BHt4yJet.js.gz +0 -0
- package/dist/web/assets/{conflict-editor-BwuNUPAK.js → conflict-editor-tj-1gzKr.js} +1 -1
- package/dist/web/assets/conflict-editor-tj-1gzKr.js.br +0 -0
- package/dist/web/assets/conflict-editor-tj-1gzKr.js.gz +0 -0
- package/dist/web/assets/{connection-form-tab-CM2o80eB.js → connection-form-tab-BV2QX8Cf.js} +1 -1
- package/dist/web/assets/connection-form-tab-BV2QX8Cf.js.br +0 -0
- package/dist/web/assets/connection-form-tab-BV2QX8Cf.js.gz +0 -0
- package/dist/web/assets/{csv-preview-C7ErHm4U.js → csv-preview-xhE2esgz.js} +1 -1
- package/dist/web/assets/csv-preview-xhE2esgz.js.br +0 -0
- package/dist/web/assets/csv-preview-xhE2esgz.js.gz +0 -0
- package/dist/web/assets/{db-tab-parts-C8jbkfxO.js → db-tab-parts-C3MsjOID.js} +1 -1
- package/dist/web/assets/db-tab-parts-C3MsjOID.js.br +0 -0
- package/dist/web/assets/db-tab-parts-C3MsjOID.js.gz +0 -0
- package/dist/web/assets/{design-settings-section-BcGEY4Zf.js → design-settings-section-C3LxYhIw.js} +1 -1
- package/dist/web/assets/design-settings-section-C3LxYhIw.js.br +0 -0
- package/dist/web/assets/design-settings-section-C3LxYhIw.js.gz +0 -0
- package/dist/web/assets/{design-tab-BGPYNFvy.js → design-tab-DYcvjdfb.js} +1 -1
- package/dist/web/assets/design-tab-DYcvjdfb.js.br +0 -0
- package/dist/web/assets/design-tab-DYcvjdfb.js.gz +0 -0
- package/dist/web/assets/{designs-sidebar-panel-BgMj7xRm.js → designs-sidebar-panel-Dpr4PPFT.js} +1 -1
- package/dist/web/assets/designs-sidebar-panel-Dpr4PPFT.js.br +0 -0
- package/dist/web/assets/designs-sidebar-panel-Dpr4PPFT.js.gz +0 -0
- package/dist/web/assets/{diff-viewer-BKJkesHk.js → diff-viewer-Fl_4jqr6.js} +1 -1
- package/dist/web/assets/diff-viewer-Fl_4jqr6.js.br +0 -0
- package/dist/web/assets/diff-viewer-Fl_4jqr6.js.gz +0 -0
- package/dist/web/assets/{docx-preview-DD5oD_nL.js → docx-preview-bVE9shPX.js} +1 -1
- package/dist/web/assets/docx-preview-bVE9shPX.js.br +0 -0
- package/dist/web/assets/docx-preview-bVE9shPX.js.gz +0 -0
- package/dist/web/assets/{editor-dialog--07lKJxl.js → editor-dialog-eEOYWdbr.js} +1 -1
- package/dist/web/assets/editor-dialog-eEOYWdbr.js.br +0 -0
- package/dist/web/assets/editor-dialog-eEOYWdbr.js.gz +0 -0
- package/dist/web/assets/{editor-language-service-51xjO5d3.js → editor-language-service-D1_Ggq4s.js} +1 -1
- package/dist/web/assets/editor-language-service-D1_Ggq4s.js.br +0 -0
- package/dist/web/assets/editor-language-service-D1_Ggq4s.js.gz +0 -0
- package/dist/web/assets/{explorer-body-BXK5SjJQ.js → explorer-body-C0UOcBPJ.js} +1 -1
- package/dist/web/assets/explorer-body-C0UOcBPJ.js.br +0 -0
- package/dist/web/assets/explorer-body-C0UOcBPJ.js.gz +0 -0
- package/dist/web/assets/{explorer-window-content-DKXcCmuX.js → explorer-window-content-Dxee7SuJ.js} +1 -1
- package/dist/web/assets/explorer-window-content-Dxee7SuJ.js.br +0 -0
- package/dist/web/assets/explorer-window-content-Dxee7SuJ.js.gz +0 -0
- package/dist/web/assets/{extension-manager-section-CTpQP6te.js → extension-manager-section-D8krnoj8.js} +1 -1
- package/dist/web/assets/extension-manager-section-D8krnoj8.js.br +0 -0
- package/dist/web/assets/extension-manager-section-D8krnoj8.js.gz +0 -0
- package/dist/web/assets/{extension-webview-COT-yLrM.js → extension-webview-CtIdV69T.js} +1 -1
- package/dist/web/assets/extension-webview-CtIdV69T.js.br +0 -0
- package/dist/web/assets/extension-webview-CtIdV69T.js.gz +0 -0
- package/dist/web/assets/{files-settings-section-CgVR0fqi.js → files-settings-section-C8vtarR8.js} +1 -1
- package/dist/web/assets/files-settings-section-C8vtarR8.js.br +0 -0
- package/dist/web/assets/files-settings-section-C8vtarR8.js.gz +0 -0
- package/dist/web/assets/{general-settings-section-BEMIbE4Y.js → general-settings-section-Chu8Ut8h.js} +1 -1
- package/dist/web/assets/general-settings-section-Chu8Ut8h.js.br +0 -0
- package/dist/web/assets/general-settings-section-Chu8Ut8h.js.gz +0 -0
- package/dist/web/assets/{git-log-panel-Cp2174iI.js → git-log-panel-DG09p6k5.js} +1 -1
- package/dist/web/assets/git-log-panel-DG09p6k5.js.br +0 -0
- package/dist/web/assets/git-log-panel-DG09p6k5.js.gz +0 -0
- package/dist/web/assets/{git-review-tab-CpMJ0qDi.js → git-review-tab-CSy8DjSC.js} +1 -1
- package/dist/web/assets/git-review-tab-CSy8DjSC.js.br +0 -0
- package/dist/web/assets/git-review-tab-CSy8DjSC.js.gz +0 -0
- package/dist/web/assets/{glide-data-grid-BAhwWUhO.js → glide-data-grid-BN1y4BT0.js} +1 -1
- package/dist/web/assets/glide-data-grid-BN1y4BT0.js.br +0 -0
- package/dist/web/assets/glide-data-grid-BN1y4BT0.js.gz +0 -0
- package/dist/web/assets/{grid-save-dialog-DmJhaWOl.js → grid-save-dialog-CfSuWw2t.js} +1 -1
- package/dist/web/assets/grid-save-dialog-CfSuWw2t.js.br +0 -0
- package/dist/web/assets/grid-save-dialog-CfSuWw2t.js.gz +0 -0
- package/dist/web/assets/{group-chat-tab-Bq7-Yegf.js → group-chat-tab-B04QMp7A.js} +1 -1
- package/dist/web/assets/group-chat-tab-B04QMp7A.js.br +0 -0
- package/dist/web/assets/group-chat-tab-B04QMp7A.js.gz +0 -0
- package/dist/web/assets/{impexp-tab-COx3Cvs7.js → impexp-tab-BRNouquR.js} +1 -1
- package/dist/web/assets/impexp-tab-BRNouquR.js.br +0 -0
- package/dist/web/assets/impexp-tab-BRNouquR.js.gz +0 -0
- package/dist/web/assets/{index-BGk8EFQb.js → index-BpgSIM5B.js} +5 -5
- package/dist/web/assets/index-BpgSIM5B.js.br +0 -0
- package/dist/web/assets/index-BpgSIM5B.js.gz +0 -0
- package/dist/web/assets/{jira-watcher-section-BEPbsi62.js → jira-watcher-section-CJeS5r6D.js} +1 -1
- package/dist/web/assets/{keybindings-store-DzcSIJUF.js → keybindings-store-Dvxq-vZD.js} +1 -1
- package/dist/web/assets/{keyboard-shortcuts-section-Bqg7AZiZ.js → keyboard-shortcuts-section-BKDyZHhK.js} +1 -1
- package/dist/web/assets/keyboard-shortcuts-section-BKDyZHhK.js.br +0 -0
- package/dist/web/assets/keyboard-shortcuts-section-BKDyZHhK.js.gz +0 -0
- package/dist/web/assets/{language-servers-section-CJ47DSIA.js → language-servers-section-CbbkezhA.js} +2 -2
- package/dist/web/assets/language-servers-section-CbbkezhA.js.br +0 -0
- package/dist/web/assets/language-servers-section-CbbkezhA.js.gz +0 -0
- package/dist/web/assets/{logs-app-BK-n-y_K.js → logs-app-Blr1-v4q.js} +1 -1
- package/dist/web/assets/logs-app-Blr1-v4q.js.br +0 -0
- package/dist/web/assets/logs-app-Blr1-v4q.js.gz +0 -0
- package/dist/web/assets/{logs-tab-B84EZJ1b.js → logs-tab-CGPtn2tU.js} +1 -1
- package/dist/web/assets/{logs-window-content-B1uN_3pN.js → logs-window-content-DAczcNmh.js} +1 -1
- package/dist/web/assets/{lsp-client-DHSaAyJW.js → lsp-client-Ds3U2O52.js} +1 -1
- package/dist/web/assets/lsp-client-Ds3U2O52.js.br +0 -0
- package/dist/web/assets/lsp-client-Ds3U2O52.js.gz +0 -0
- package/dist/web/assets/{markdown-renderer-BjDQUqT-.js → markdown-renderer-DbJVwfUH.js} +1 -1
- package/dist/web/assets/{markdown-renderer-CpxdNG0f.js → markdown-renderer-DzCPadpB.js} +1 -1
- package/dist/web/assets/markdown-renderer-DzCPadpB.js.br +0 -0
- package/dist/web/assets/markdown-renderer-DzCPadpB.js.gz +0 -0
- package/dist/web/assets/{mobile-explorer-sheet-BBwXGmXA.js → mobile-explorer-sheet-BClCMXb8.js} +1 -1
- package/dist/web/assets/mobile-explorer-sheet-BClCMXb8.js.br +0 -0
- package/dist/web/assets/mobile-explorer-sheet-BClCMXb8.js.gz +0 -0
- package/dist/web/assets/{new-design-dialog-B_ZOvGhR.js → new-design-dialog-HRP9H11v.js} +1 -1
- package/dist/web/assets/new-design-dialog-HRP9H11v.js.br +0 -0
- package/dist/web/assets/new-design-dialog-HRP9H11v.js.gz +0 -0
- package/dist/web/assets/{notification-store-HuMqfadD.js → notification-store-BomzYNQj.js} +1 -1
- package/dist/web/assets/{notifications-settings-section-D7fIVFHe.js → notifications-settings-section-CYTTQaNr.js} +1 -1
- package/dist/web/assets/notifications-settings-section-CYTTQaNr.js.br +0 -0
- package/dist/web/assets/notifications-settings-section-CYTTQaNr.js.gz +0 -0
- package/dist/web/assets/{ppmbot-settings-section-JDvyI-gZ.js → ppmbot-settings-section-BRowIIqc.js} +1 -1
- package/dist/web/assets/ppmbot-settings-section-BRowIIqc.js.br +0 -0
- package/dist/web/assets/ppmbot-settings-section-BRowIIqc.js.gz +0 -0
- package/dist/web/assets/{problems-panel-Bsv_L6T2.js → problems-panel-DdB10Ybd.js} +1 -1
- package/dist/web/assets/problems-panel-DdB10Ybd.js.br +0 -0
- package/dist/web/assets/problems-panel-DdB10Ybd.js.gz +0 -0
- package/dist/web/assets/{process-table-3kZyWOJx.js → process-table-CzLaWPe0.js} +1 -1
- package/dist/web/assets/process-table-CzLaWPe0.js.br +0 -0
- package/dist/web/assets/process-table-CzLaWPe0.js.gz +0 -0
- package/dist/web/assets/{proxy-settings-section-CYcmIp7W.js → proxy-settings-section-BCq7R5DA.js} +1 -1
- package/dist/web/assets/proxy-settings-section-BCq7R5DA.js.br +0 -0
- package/dist/web/assets/proxy-settings-section-BCq7R5DA.js.gz +0 -0
- package/dist/web/assets/{query-tab-CjrWnQUl.js → query-tab-DGvj_h6l.js} +1 -1
- package/dist/web/assets/query-tab-DGvj_h6l.js.br +0 -0
- package/dist/web/assets/query-tab-DGvj_h6l.js.gz +0 -0
- package/dist/web/assets/{remote-access-settings-section-iQkakiVx.js → remote-access-settings-section-BrqOoOyk.js} +1 -1
- package/dist/web/assets/remote-access-settings-section-BrqOoOyk.js.br +0 -0
- package/dist/web/assets/remote-access-settings-section-BrqOoOyk.js.gz +0 -0
- package/dist/web/assets/{remote-desktop-clipboard-notice-CFLnb7ML.js → remote-desktop-clipboard-notice-5r6UbdlN.js} +1 -1
- package/dist/web/assets/remote-desktop-clipboard-notice-5r6UbdlN.js.br +0 -0
- package/dist/web/assets/remote-desktop-clipboard-notice-5r6UbdlN.js.gz +0 -0
- package/dist/web/assets/{remote-desktop-mobile-sheet-C_-_-Nkz.js → remote-desktop-mobile-sheet-B9Q4auqe.js} +2 -2
- package/dist/web/assets/remote-desktop-mobile-sheet-B9Q4auqe.js.br +0 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-B9Q4auqe.js.gz +0 -0
- package/dist/web/assets/{remote-desktop-mobile-view-BnHSrind.js → remote-desktop-mobile-view-CL-jrkA8.js} +1 -1
- package/dist/web/assets/remote-desktop-mobile-view-CL-jrkA8.js.br +0 -0
- package/dist/web/assets/remote-desktop-mobile-view-CL-jrkA8.js.gz +0 -0
- package/dist/web/assets/{remote-desktop-readiness-gate-BGHJ7Sv6.js → remote-desktop-readiness-gate-DXEf2Jnz.js} +1 -1
- package/dist/web/assets/remote-desktop-readiness-gate-DXEf2Jnz.js.br +0 -0
- package/dist/web/assets/remote-desktop-readiness-gate-DXEf2Jnz.js.gz +0 -0
- package/dist/web/assets/{remote-desktop-window-content-BBm1YwM8.js → remote-desktop-window-content-20P6-gKB.js} +1 -1
- package/dist/web/assets/remote-desktop-window-content-20P6-gKB.js.br +0 -0
- package/dist/web/assets/remote-desktop-window-content-20P6-gKB.js.gz +0 -0
- package/dist/web/assets/{rename-object-dialog-BLnF9xyn.js → rename-object-dialog-Bs7pUsUP.js} +1 -1
- package/dist/web/assets/rename-object-dialog-Bs7pUsUP.js.br +0 -0
- package/dist/web/assets/rename-object-dialog-Bs7pUsUP.js.gz +0 -0
- package/dist/web/assets/{review-code-DG7sCIzr.js → review-code-Bjo4sHN9.js} +1 -1
- package/dist/web/assets/review-code-Bjo4sHN9.js.br +0 -0
- package/dist/web/assets/review-code-Bjo4sHN9.js.gz +0 -0
- package/dist/web/assets/{save-as-dialog-KW7cErWh.js → save-as-dialog-BHx0Wp2B.js} +1 -1
- package/dist/web/assets/save-as-dialog-BHx0Wp2B.js.br +0 -0
- package/dist/web/assets/save-as-dialog-BHx0Wp2B.js.gz +0 -0
- package/dist/web/assets/{schedules-settings-section-B4AV4ORv.js → schedules-settings-section-C6ATYDPK.js} +1 -1
- package/dist/web/assets/schedules-settings-section-C6ATYDPK.js.br +0 -0
- package/dist/web/assets/schedules-settings-section-C6ATYDPK.js.gz +0 -0
- package/dist/web/assets/{session-review-tab-C5nPyBzo.js → session-review-tab-BPMUyNTx.js} +1 -1
- package/dist/web/assets/session-review-tab-BPMUyNTx.js.br +0 -0
- package/dist/web/assets/session-review-tab-BPMUyNTx.js.gz +0 -0
- package/dist/web/assets/{settings-body-xWSmRXUD.js → settings-body-Dy5D6Vu7.js} +1 -1
- package/dist/web/assets/settings-body-Dy5D6Vu7.js.br +0 -0
- package/dist/web/assets/settings-body-Dy5D6Vu7.js.gz +0 -0
- package/dist/web/assets/{settings-rows-ChKBHk07.js → settings-rows-BRoYE2To.js} +1 -1
- package/dist/web/assets/settings-rows-BRoYE2To.js.br +0 -0
- package/dist/web/assets/settings-rows-BRoYE2To.js.gz +0 -0
- package/dist/web/assets/settings-section-content-C4DX7TYv.js +1 -0
- package/dist/web/assets/{settings-section-content-DdVdyPVX.js → settings-section-content-essUViKu.js} +2 -2
- package/dist/web/assets/settings-section-content-essUViKu.js.br +0 -0
- package/dist/web/assets/settings-section-content-essUViKu.js.gz +0 -0
- package/dist/web/assets/{settings-tab-CzaBBNoj.js → settings-tab-Di1DUP1j.js} +1 -1
- package/dist/web/assets/{settings-window-content-DAvWSu_O.js → settings-window-content-DYvYFPWy.js} +1 -1
- package/dist/web/assets/{slash-command-picker-Cn2Fdz2p.js → slash-command-picker-8da30mYP.js} +1 -1
- package/dist/web/assets/slash-command-picker-8da30mYP.js.br +0 -0
- package/dist/web/assets/slash-command-picker-8da30mYP.js.gz +0 -0
- package/dist/web/assets/{sql-object-tab-_mPRCvD9.js → sql-object-tab-DRIHVXuR.js} +1 -1
- package/dist/web/assets/sql-object-tab-DRIHVXuR.js.br +0 -0
- package/dist/web/assets/sql-object-tab-DRIHVXuR.js.gz +0 -0
- package/dist/web/assets/{sqlite-viewer-B8VZt-mA.js → sqlite-viewer-G-Jl2yEr.js} +1 -1
- package/dist/web/assets/sqlite-viewer-G-Jl2yEr.js.br +0 -0
- package/dist/web/assets/sqlite-viewer-G-Jl2yEr.js.gz +0 -0
- package/dist/web/assets/{structure-save-dialog-Lcst7kAl.js → structure-save-dialog-Cwikz0we.js} +1 -1
- package/dist/web/assets/structure-save-dialog-Cwikz0we.js.br +0 -0
- package/dist/web/assets/structure-save-dialog-Cwikz0we.js.gz +0 -0
- package/dist/web/assets/{structure-tab-ljJCzxUe.js → structure-tab-CWEgdZPm.js} +1 -1
- package/dist/web/assets/structure-tab-CWEgdZPm.js.br +0 -0
- package/dist/web/assets/structure-tab-CWEgdZPm.js.gz +0 -0
- package/dist/web/assets/{system-monitor-body-CTi5xUZz.js → system-monitor-body-BDQm9-wb.js} +2 -2
- package/dist/web/assets/system-monitor-body-BDQm9-wb.js.br +0 -0
- package/dist/web/assets/system-monitor-body-BDQm9-wb.js.gz +0 -0
- package/dist/web/assets/{system-monitor-tab-DeL2YoXO.js → system-monitor-tab-BouN21Yh.js} +1 -1
- package/dist/web/assets/{system-monitor-window-content-DK-rQIZh.js → system-monitor-window-content-BRyWBSOb.js} +1 -1
- package/dist/web/assets/{tab-host-window-content-CG0vPTc_.js → tab-host-window-content-CZ55-bTz.js} +1 -1
- package/dist/web/assets/tab-host-window-content-CZ55-bTz.js.br +0 -0
- package/dist/web/assets/tab-host-window-content-CZ55-bTz.js.gz +0 -0
- package/dist/web/assets/{table-tab-CTZ3nCWV.js → table-tab-Bmn4UdBV.js} +1 -1
- package/dist/web/assets/table-tab-Bmn4UdBV.js.br +0 -0
- package/dist/web/assets/table-tab-Bmn4UdBV.js.gz +0 -0
- package/dist/web/assets/{telegram-bot-token-field-DF7zq7_h.js → telegram-bot-token-field-fdHwRf39.js} +1 -1
- package/dist/web/assets/telegram-bot-token-field-fdHwRf39.js.br +0 -0
- package/dist/web/assets/telegram-bot-token-field-fdHwRf39.js.gz +0 -0
- package/dist/web/assets/{terminal-tab-B_jECWTH.js → terminal-tab-D-qlZXLK.js} +1 -1
- package/dist/web/assets/terminal-tab-D-qlZXLK.js.br +0 -0
- package/dist/web/assets/terminal-tab-D-qlZXLK.js.gz +0 -0
- package/dist/web/assets/{tool-cards-CsD_VXcG.js → tool-cards-BdXbQ9Fv.js} +2 -2
- package/dist/web/assets/tool-cards-BdXbQ9Fv.js.br +0 -0
- package/dist/web/assets/tool-cards-BdXbQ9Fv.js.gz +0 -0
- package/dist/web/assets/{tools-settings-section-DbBr0Hnr.js → tools-settings-section-CcPaspbR.js} +1 -1
- package/dist/web/assets/tools-settings-section-CcPaspbR.js.br +0 -0
- package/dist/web/assets/tools-settings-section-CcPaspbR.js.gz +0 -0
- package/dist/web/assets/{use-inline-blame-BA2mrMxT.js → use-inline-blame-Ck5XoSS3.js} +1 -1
- package/dist/web/assets/use-inline-blame-Ck5XoSS3.js.br +0 -0
- package/dist/web/assets/use-inline-blame-Ck5XoSS3.js.gz +0 -0
- package/dist/web/assets/{use-lsp-ChbrzwvS.js → use-lsp-CGSqfFBk.js} +1 -1
- package/dist/web/assets/use-lsp-CGSqfFBk.js.br +0 -0
- package/dist/web/assets/use-lsp-CGSqfFBk.js.gz +0 -0
- package/dist/web/assets/{use-monaco-theme-C3_wfz5J.js → use-monaco-theme-BW05eZS4.js} +1 -1
- package/dist/web/assets/{use-process-kill-C-YetnGd.js → use-process-kill-DJ_f7rCO.js} +1 -1
- package/dist/web/assets/use-process-kill-DJ_f7rCO.js.br +0 -0
- package/dist/web/assets/use-process-kill-DJ_f7rCO.js.gz +0 -0
- package/dist/web/assets/{use-sql-schema-info-pKwLD9wk.js → use-sql-schema-info-wt320fVR.js} +1 -1
- package/dist/web/assets/use-sql-schema-info-wt320fVR.js.br +0 -0
- package/dist/web/assets/use-sql-schema-info-wt320fVR.js.gz +0 -0
- package/dist/web/assets/{use-websocket-BFnUq0-x.js → use-websocket-CTwe74Cb.js} +1 -1
- package/dist/web/assets/{web-preview-tab-DGLPuZYM.js → web-preview-tab-D4XayUEK.js} +1 -1
- package/dist/web/assets/web-preview-tab-D4XayUEK.js.br +0 -0
- package/dist/web/assets/web-preview-tab-D4XayUEK.js.gz +0 -0
- package/dist/web/index.html +1 -1
- package/dist/web/index.html.br +0 -0
- package/dist/web/index.html.gz +0 -0
- package/dist/web/sw.js +1 -1
- package/dist/web/sw.js.br +0 -0
- package/dist/web/sw.js.gz +0 -0
- package/docs/lessons-learned.md +30 -0
- package/package.json +1 -1
- package/src/server/index.ts +8 -0
- package/src/services/autostart-generator.ts +9 -1
- package/src/services/event-loop-lag.ts +44 -11
- package/src/services/machine-cpu.ts +40 -0
- package/src/services/process-priority.ts +153 -0
- package/src/services/process-thread-snapshot.ts +193 -0
- package/src/services/supervisor.ts +44 -2
- package/src/web/components/chat/question-card.tsx +10 -4
- package/dist/web/assets/accounts-settings-section-BTga1zwH.js.br +0 -0
- package/dist/web/assets/accounts-settings-section-BTga1zwH.js.gz +0 -0
- package/dist/web/assets/agent-session-sheet-Q3kasbrZ.js.br +0 -0
- package/dist/web/assets/agent-session-sheet-Q3kasbrZ.js.gz +0 -0
- package/dist/web/assets/agent-session-window-content-UNuEprIK.js.br +0 -0
- package/dist/web/assets/agent-session-window-content-UNuEprIK.js.gz +0 -0
- package/dist/web/assets/ai-resource-editor-BEwA6Do0.js.br +0 -0
- package/dist/web/assets/ai-resource-editor-BEwA6Do0.js.gz +0 -0
- package/dist/web/assets/ai-settings-section-DVKJPM0n.js.br +0 -0
- package/dist/web/assets/ai-settings-section-DVKJPM0n.js.gz +0 -0
- package/dist/web/assets/android-tab-BNa6R9fV.js.br +0 -0
- package/dist/web/assets/android-tab-BNa6R9fV.js.gz +0 -0
- package/dist/web/assets/appearance-settings-section-CjWFwWNn.js.br +0 -0
- package/dist/web/assets/appearance-settings-section-CjWFwWNn.js.gz +0 -0
- package/dist/web/assets/apps-panel-D4xjeMN9.js.br +0 -0
- package/dist/web/assets/apps-panel-D4xjeMN9.js.gz +0 -0
- package/dist/web/assets/branch-review-tab-BCW5294d.js.br +0 -0
- package/dist/web/assets/branch-review-tab-BCW5294d.js.gz +0 -0
- package/dist/web/assets/chat-tab-BG0M1D3d.js.br +0 -3
- package/dist/web/assets/chat-tab-BG0M1D3d.js.gz +0 -0
- package/dist/web/assets/chat-tab-DsW93LTL.js.br +0 -0
- package/dist/web/assets/chat-tab-DsW93LTL.js.gz +0 -0
- package/dist/web/assets/code-editor-AxLIpK-b.js.br +0 -0
- package/dist/web/assets/code-editor-AxLIpK-b.js.gz +0 -0
- package/dist/web/assets/codex-reset-credit-button-BnRmkBZu.js.br +0 -0
- package/dist/web/assets/codex-reset-credit-button-BnRmkBZu.js.gz +0 -0
- package/dist/web/assets/conflict-editor-BwuNUPAK.js.br +0 -0
- package/dist/web/assets/conflict-editor-BwuNUPAK.js.gz +0 -0
- package/dist/web/assets/connection-form-tab-CM2o80eB.js.br +0 -0
- package/dist/web/assets/connection-form-tab-CM2o80eB.js.gz +0 -0
- package/dist/web/assets/csv-preview-C7ErHm4U.js.br +0 -0
- package/dist/web/assets/csv-preview-C7ErHm4U.js.gz +0 -0
- package/dist/web/assets/db-tab-parts-C8jbkfxO.js.br +0 -0
- package/dist/web/assets/db-tab-parts-C8jbkfxO.js.gz +0 -0
- package/dist/web/assets/design-settings-section-BcGEY4Zf.js.br +0 -0
- package/dist/web/assets/design-settings-section-BcGEY4Zf.js.gz +0 -0
- package/dist/web/assets/design-tab-BGPYNFvy.js.br +0 -0
- package/dist/web/assets/design-tab-BGPYNFvy.js.gz +0 -0
- package/dist/web/assets/designs-sidebar-panel-BgMj7xRm.js.br +0 -0
- package/dist/web/assets/designs-sidebar-panel-BgMj7xRm.js.gz +0 -0
- package/dist/web/assets/diff-viewer-BKJkesHk.js.br +0 -0
- package/dist/web/assets/diff-viewer-BKJkesHk.js.gz +0 -0
- package/dist/web/assets/docx-preview-DD5oD_nL.js.br +0 -0
- package/dist/web/assets/docx-preview-DD5oD_nL.js.gz +0 -0
- package/dist/web/assets/editor-dialog--07lKJxl.js.br +0 -0
- package/dist/web/assets/editor-dialog--07lKJxl.js.gz +0 -0
- package/dist/web/assets/editor-language-service-51xjO5d3.js.br +0 -0
- package/dist/web/assets/editor-language-service-51xjO5d3.js.gz +0 -0
- package/dist/web/assets/explorer-body-BXK5SjJQ.js.br +0 -0
- package/dist/web/assets/explorer-body-BXK5SjJQ.js.gz +0 -0
- package/dist/web/assets/explorer-window-content-DKXcCmuX.js.br +0 -0
- package/dist/web/assets/explorer-window-content-DKXcCmuX.js.gz +0 -0
- package/dist/web/assets/extension-manager-section-CTpQP6te.js.br +0 -0
- package/dist/web/assets/extension-manager-section-CTpQP6te.js.gz +0 -0
- package/dist/web/assets/extension-webview-COT-yLrM.js.br +0 -0
- package/dist/web/assets/extension-webview-COT-yLrM.js.gz +0 -0
- package/dist/web/assets/files-settings-section-CgVR0fqi.js.br +0 -0
- package/dist/web/assets/files-settings-section-CgVR0fqi.js.gz +0 -0
- package/dist/web/assets/general-settings-section-BEMIbE4Y.js.br +0 -0
- package/dist/web/assets/general-settings-section-BEMIbE4Y.js.gz +0 -0
- package/dist/web/assets/git-log-panel-Cp2174iI.js.br +0 -0
- package/dist/web/assets/git-log-panel-Cp2174iI.js.gz +0 -0
- package/dist/web/assets/git-review-tab-CpMJ0qDi.js.br +0 -0
- package/dist/web/assets/git-review-tab-CpMJ0qDi.js.gz +0 -0
- package/dist/web/assets/glide-data-grid-BAhwWUhO.js.br +0 -0
- package/dist/web/assets/glide-data-grid-BAhwWUhO.js.gz +0 -0
- package/dist/web/assets/grid-save-dialog-DmJhaWOl.js.br +0 -0
- package/dist/web/assets/grid-save-dialog-DmJhaWOl.js.gz +0 -0
- package/dist/web/assets/group-chat-tab-Bq7-Yegf.js.br +0 -0
- package/dist/web/assets/group-chat-tab-Bq7-Yegf.js.gz +0 -0
- package/dist/web/assets/impexp-tab-COx3Cvs7.js.br +0 -0
- package/dist/web/assets/impexp-tab-COx3Cvs7.js.gz +0 -0
- package/dist/web/assets/index-BGk8EFQb.js.br +0 -0
- package/dist/web/assets/index-BGk8EFQb.js.gz +0 -0
- package/dist/web/assets/keyboard-shortcuts-section-Bqg7AZiZ.js.br +0 -0
- package/dist/web/assets/keyboard-shortcuts-section-Bqg7AZiZ.js.gz +0 -0
- package/dist/web/assets/language-servers-section-CJ47DSIA.js.br +0 -0
- package/dist/web/assets/language-servers-section-CJ47DSIA.js.gz +0 -0
- package/dist/web/assets/logs-app-BK-n-y_K.js.br +0 -0
- package/dist/web/assets/logs-app-BK-n-y_K.js.gz +0 -0
- package/dist/web/assets/lsp-client-DHSaAyJW.js.br +0 -0
- package/dist/web/assets/lsp-client-DHSaAyJW.js.gz +0 -0
- package/dist/web/assets/markdown-renderer-CpxdNG0f.js.br +0 -0
- package/dist/web/assets/markdown-renderer-CpxdNG0f.js.gz +0 -0
- package/dist/web/assets/mobile-explorer-sheet-BBwXGmXA.js.br +0 -0
- package/dist/web/assets/mobile-explorer-sheet-BBwXGmXA.js.gz +0 -0
- package/dist/web/assets/new-design-dialog-B_ZOvGhR.js.br +0 -0
- package/dist/web/assets/new-design-dialog-B_ZOvGhR.js.gz +0 -0
- package/dist/web/assets/notifications-settings-section-D7fIVFHe.js.br +0 -0
- package/dist/web/assets/notifications-settings-section-D7fIVFHe.js.gz +0 -0
- package/dist/web/assets/ppmbot-settings-section-JDvyI-gZ.js.br +0 -0
- package/dist/web/assets/ppmbot-settings-section-JDvyI-gZ.js.gz +0 -0
- package/dist/web/assets/problems-panel-Bsv_L6T2.js.br +0 -0
- package/dist/web/assets/problems-panel-Bsv_L6T2.js.gz +0 -0
- package/dist/web/assets/process-table-3kZyWOJx.js.br +0 -0
- package/dist/web/assets/process-table-3kZyWOJx.js.gz +0 -0
- package/dist/web/assets/proxy-settings-section-CYcmIp7W.js.br +0 -0
- package/dist/web/assets/proxy-settings-section-CYcmIp7W.js.gz +0 -0
- package/dist/web/assets/query-tab-CjrWnQUl.js.br +0 -0
- package/dist/web/assets/query-tab-CjrWnQUl.js.gz +0 -0
- package/dist/web/assets/remote-access-settings-section-iQkakiVx.js.br +0 -0
- package/dist/web/assets/remote-access-settings-section-iQkakiVx.js.gz +0 -0
- package/dist/web/assets/remote-desktop-clipboard-notice-CFLnb7ML.js.br +0 -0
- package/dist/web/assets/remote-desktop-clipboard-notice-CFLnb7ML.js.gz +0 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-C_-_-Nkz.js.br +0 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-C_-_-Nkz.js.gz +0 -0
- package/dist/web/assets/remote-desktop-mobile-view-BnHSrind.js.br +0 -0
- package/dist/web/assets/remote-desktop-mobile-view-BnHSrind.js.gz +0 -0
- package/dist/web/assets/remote-desktop-readiness-gate-BGHJ7Sv6.js.br +0 -0
- package/dist/web/assets/remote-desktop-readiness-gate-BGHJ7Sv6.js.gz +0 -0
- package/dist/web/assets/remote-desktop-window-content-BBm1YwM8.js.br +0 -0
- package/dist/web/assets/remote-desktop-window-content-BBm1YwM8.js.gz +0 -0
- package/dist/web/assets/rename-object-dialog-BLnF9xyn.js.br +0 -0
- package/dist/web/assets/rename-object-dialog-BLnF9xyn.js.gz +0 -0
- package/dist/web/assets/review-code-DG7sCIzr.js.br +0 -0
- package/dist/web/assets/review-code-DG7sCIzr.js.gz +0 -0
- package/dist/web/assets/save-as-dialog-KW7cErWh.js.br +0 -0
- package/dist/web/assets/save-as-dialog-KW7cErWh.js.gz +0 -0
- package/dist/web/assets/schedules-settings-section-B4AV4ORv.js.br +0 -0
- package/dist/web/assets/schedules-settings-section-B4AV4ORv.js.gz +0 -0
- package/dist/web/assets/session-review-tab-C5nPyBzo.js.br +0 -0
- package/dist/web/assets/session-review-tab-C5nPyBzo.js.gz +0 -0
- package/dist/web/assets/settings-body-xWSmRXUD.js.br +0 -0
- package/dist/web/assets/settings-body-xWSmRXUD.js.gz +0 -0
- package/dist/web/assets/settings-rows-ChKBHk07.js.br +0 -0
- package/dist/web/assets/settings-rows-ChKBHk07.js.gz +0 -0
- package/dist/web/assets/settings-section-content-BOh2jaWV.js +0 -1
- package/dist/web/assets/settings-section-content-DdVdyPVX.js.br +0 -0
- package/dist/web/assets/settings-section-content-DdVdyPVX.js.gz +0 -0
- package/dist/web/assets/slash-command-picker-Cn2Fdz2p.js.br +0 -0
- package/dist/web/assets/slash-command-picker-Cn2Fdz2p.js.gz +0 -0
- package/dist/web/assets/sql-object-tab-_mPRCvD9.js.br +0 -0
- package/dist/web/assets/sql-object-tab-_mPRCvD9.js.gz +0 -0
- package/dist/web/assets/sqlite-viewer-B8VZt-mA.js.br +0 -0
- package/dist/web/assets/sqlite-viewer-B8VZt-mA.js.gz +0 -0
- package/dist/web/assets/structure-save-dialog-Lcst7kAl.js.br +0 -0
- package/dist/web/assets/structure-save-dialog-Lcst7kAl.js.gz +0 -0
- package/dist/web/assets/structure-tab-ljJCzxUe.js.br +0 -0
- package/dist/web/assets/structure-tab-ljJCzxUe.js.gz +0 -0
- package/dist/web/assets/system-monitor-body-CTi5xUZz.js.br +0 -0
- package/dist/web/assets/system-monitor-body-CTi5xUZz.js.gz +0 -0
- package/dist/web/assets/tab-host-window-content-CG0vPTc_.js.br +0 -0
- package/dist/web/assets/tab-host-window-content-CG0vPTc_.js.gz +0 -0
- package/dist/web/assets/table-tab-CTZ3nCWV.js.br +0 -0
- package/dist/web/assets/table-tab-CTZ3nCWV.js.gz +0 -0
- package/dist/web/assets/telegram-bot-token-field-DF7zq7_h.js.br +0 -0
- package/dist/web/assets/telegram-bot-token-field-DF7zq7_h.js.gz +0 -0
- package/dist/web/assets/terminal-tab-B_jECWTH.js.br +0 -0
- package/dist/web/assets/terminal-tab-B_jECWTH.js.gz +0 -0
- package/dist/web/assets/tool-cards-CsD_VXcG.js.br +0 -0
- package/dist/web/assets/tool-cards-CsD_VXcG.js.gz +0 -0
- package/dist/web/assets/tools-settings-section-DbBr0Hnr.js.br +0 -0
- package/dist/web/assets/tools-settings-section-DbBr0Hnr.js.gz +0 -0
- package/dist/web/assets/use-inline-blame-BA2mrMxT.js.br +0 -0
- package/dist/web/assets/use-inline-blame-BA2mrMxT.js.gz +0 -0
- package/dist/web/assets/use-lsp-ChbrzwvS.js.br +0 -0
- package/dist/web/assets/use-lsp-ChbrzwvS.js.gz +0 -0
- package/dist/web/assets/use-process-kill-C-YetnGd.js.br +0 -0
- package/dist/web/assets/use-process-kill-C-YetnGd.js.gz +0 -0
- package/dist/web/assets/use-sql-schema-info-pKwLD9wk.js.br +0 -0
- package/dist/web/assets/use-sql-schema-info-pKwLD9wk.js.gz +0 -0
- package/dist/web/assets/web-preview-tab-DGLPuZYM.js.br +0 -0
- 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
|
|
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
|
|
23
|
-
*
|
|
24
|
-
* stalled" is a fact with
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|