@hienlh/ppm 0.23.20 → 0.23.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/CLAUDE.md +3 -2
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/http-api.md +1 -1
- package/dist/web/assets/{accounts-settings-section-BTga1zwH.js → accounts-settings-section-CiNmIj02.js} +1 -1
- package/dist/web/assets/accounts-settings-section-CiNmIj02.js.br +0 -0
- package/dist/web/assets/accounts-settings-section-CiNmIj02.js.gz +0 -0
- package/dist/web/assets/{agent-session-sheet-Q3kasbrZ.js → agent-session-sheet-DRR36PDA.js} +2 -2
- package/dist/web/assets/agent-session-sheet-DRR36PDA.js.br +0 -0
- package/dist/web/assets/agent-session-sheet-DRR36PDA.js.gz +0 -0
- package/dist/web/assets/{agent-session-window-content-UNuEprIK.js → agent-session-window-content-DMhy16BM.js} +1 -1
- package/dist/web/assets/agent-session-window-content-DMhy16BM.js.br +0 -0
- package/dist/web/assets/agent-session-window-content-DMhy16BM.js.gz +0 -0
- package/dist/web/assets/{ai-resource-editor-BEwA6Do0.js → ai-resource-editor-BG8boPw7.js} +1 -1
- package/dist/web/assets/ai-resource-editor-BG8boPw7.js.br +0 -0
- package/dist/web/assets/ai-resource-editor-BG8boPw7.js.gz +0 -0
- package/dist/web/assets/{ai-settings-section-DVKJPM0n.js → ai-settings-section-DMGM1hYx.js} +1 -1
- package/dist/web/assets/ai-settings-section-DMGM1hYx.js.br +0 -0
- package/dist/web/assets/ai-settings-section-DMGM1hYx.js.gz +0 -0
- package/dist/web/assets/{ai-settings-section-DlfY5lFT.js → ai-settings-section-De5dYWlC.js} +1 -1
- package/dist/web/assets/{android-tab-BNa6R9fV.js → android-tab-lHDxD93G.js} +1 -1
- package/dist/web/assets/android-tab-lHDxD93G.js.br +0 -0
- package/dist/web/assets/android-tab-lHDxD93G.js.gz +0 -0
- package/dist/web/assets/{appearance-settings-section-CjWFwWNn.js → appearance-settings-section-DO-ZyoMj.js} +1 -1
- package/dist/web/assets/appearance-settings-section-DO-ZyoMj.js.br +0 -0
- package/dist/web/assets/appearance-settings-section-DO-ZyoMj.js.gz +0 -0
- package/dist/web/assets/{apps-panel-D4xjeMN9.js → apps-panel-CH-0CHYY.js} +1 -1
- package/dist/web/assets/apps-panel-CH-0CHYY.js.br +0 -0
- package/dist/web/assets/apps-panel-CH-0CHYY.js.gz +0 -0
- package/dist/web/assets/{branch-review-tab-BCW5294d.js → branch-review-tab-CdGtvekz.js} +1 -1
- package/dist/web/assets/branch-review-tab-CdGtvekz.js.br +0 -0
- package/dist/web/assets/branch-review-tab-CdGtvekz.js.gz +0 -0
- package/dist/web/assets/{chat-tab-BG0M1D3d.js → chat-tab-DMqWnFIo.js} +1 -1
- package/dist/web/assets/chat-tab-DMqWnFIo.js.br +0 -0
- package/dist/web/assets/chat-tab-DMqWnFIo.js.gz +0 -0
- package/dist/web/assets/{chat-tab-DsW93LTL.js → chat-tab-DzWpwPFb.js} +4 -4
- package/dist/web/assets/chat-tab-DzWpwPFb.js.br +0 -0
- package/dist/web/assets/chat-tab-DzWpwPFb.js.gz +0 -0
- package/dist/web/assets/{code-editor-AxLIpK-b.js → code-editor-BCv8ONNS.js} +4 -4
- package/dist/web/assets/code-editor-BCv8ONNS.js.br +0 -0
- package/dist/web/assets/code-editor-BCv8ONNS.js.gz +0 -0
- package/dist/web/assets/{codex-reset-credit-button-BnRmkBZu.js → codex-reset-credit-button-CGMbIHak.js} +1 -1
- package/dist/web/assets/codex-reset-credit-button-CGMbIHak.js.br +0 -0
- package/dist/web/assets/codex-reset-credit-button-CGMbIHak.js.gz +0 -0
- package/dist/web/assets/{conflict-editor-BwuNUPAK.js → conflict-editor-3WmzqBlW.js} +1 -1
- package/dist/web/assets/conflict-editor-3WmzqBlW.js.br +0 -0
- package/dist/web/assets/conflict-editor-3WmzqBlW.js.gz +0 -0
- package/dist/web/assets/{connection-form-tab-CM2o80eB.js → connection-form-tab-BoWucvAe.js} +1 -1
- package/dist/web/assets/connection-form-tab-BoWucvAe.js.br +0 -0
- package/dist/web/assets/connection-form-tab-BoWucvAe.js.gz +0 -0
- package/dist/web/assets/{csv-preview-C7ErHm4U.js → csv-preview-BCgGsy7F.js} +1 -1
- package/dist/web/assets/csv-preview-BCgGsy7F.js.br +0 -0
- package/dist/web/assets/csv-preview-BCgGsy7F.js.gz +0 -0
- package/dist/web/assets/{db-tab-parts-C8jbkfxO.js → db-tab-parts-o6QMHBbA.js} +1 -1
- package/dist/web/assets/db-tab-parts-o6QMHBbA.js.br +0 -0
- package/dist/web/assets/db-tab-parts-o6QMHBbA.js.gz +0 -0
- package/dist/web/assets/{design-settings-section-BcGEY4Zf.js → design-settings-section-seo6SdSk.js} +1 -1
- package/dist/web/assets/design-settings-section-seo6SdSk.js.br +0 -0
- package/dist/web/assets/design-settings-section-seo6SdSk.js.gz +0 -0
- package/dist/web/assets/{design-tab-BGPYNFvy.js → design-tab-B-f_b2LM.js} +1 -1
- package/dist/web/assets/design-tab-B-f_b2LM.js.br +0 -0
- package/dist/web/assets/design-tab-B-f_b2LM.js.gz +0 -0
- package/dist/web/assets/{designs-sidebar-panel-BgMj7xRm.js → designs-sidebar-panel-CVT_TQP7.js} +1 -1
- package/dist/web/assets/designs-sidebar-panel-CVT_TQP7.js.br +0 -0
- package/dist/web/assets/designs-sidebar-panel-CVT_TQP7.js.gz +0 -0
- package/dist/web/assets/{diff-viewer-BKJkesHk.js → diff-viewer-y9uBp_8u.js} +1 -1
- package/dist/web/assets/diff-viewer-y9uBp_8u.js.br +0 -0
- package/dist/web/assets/diff-viewer-y9uBp_8u.js.gz +0 -0
- package/dist/web/assets/{docx-preview-DD5oD_nL.js → docx-preview-Dn_x3Inm.js} +1 -1
- package/dist/web/assets/docx-preview-Dn_x3Inm.js.br +0 -0
- package/dist/web/assets/docx-preview-Dn_x3Inm.js.gz +0 -0
- package/dist/web/assets/{editor-dialog--07lKJxl.js → editor-dialog-BwNXSajD.js} +1 -1
- package/dist/web/assets/editor-dialog-BwNXSajD.js.br +0 -0
- package/dist/web/assets/editor-dialog-BwNXSajD.js.gz +0 -0
- package/dist/web/assets/{editor-language-service-51xjO5d3.js → editor-language-service-XA_TnPLN.js} +1 -1
- package/dist/web/assets/editor-language-service-XA_TnPLN.js.br +0 -0
- package/dist/web/assets/editor-language-service-XA_TnPLN.js.gz +0 -0
- package/dist/web/assets/{explorer-body-BXK5SjJQ.js → explorer-body-DukR7zM9.js} +1 -1
- package/dist/web/assets/explorer-body-DukR7zM9.js.br +0 -0
- package/dist/web/assets/explorer-body-DukR7zM9.js.gz +0 -0
- package/dist/web/assets/{explorer-window-content-DKXcCmuX.js → explorer-window-content-BjTZ44S3.js} +1 -1
- package/dist/web/assets/explorer-window-content-BjTZ44S3.js.br +0 -0
- package/dist/web/assets/explorer-window-content-BjTZ44S3.js.gz +0 -0
- package/dist/web/assets/{extension-manager-section-CTpQP6te.js → extension-manager-section-C4WD1BPQ.js} +1 -1
- package/dist/web/assets/extension-manager-section-C4WD1BPQ.js.br +0 -0
- package/dist/web/assets/extension-manager-section-C4WD1BPQ.js.gz +0 -0
- package/dist/web/assets/{extension-webview-COT-yLrM.js → extension-webview-CV2ujflc.js} +1 -1
- package/dist/web/assets/extension-webview-CV2ujflc.js.br +0 -0
- package/dist/web/assets/extension-webview-CV2ujflc.js.gz +0 -0
- package/dist/web/assets/{files-settings-section-CgVR0fqi.js → files-settings-section-hS5ixQ8o.js} +1 -1
- package/dist/web/assets/files-settings-section-hS5ixQ8o.js.br +0 -0
- package/dist/web/assets/files-settings-section-hS5ixQ8o.js.gz +0 -0
- package/dist/web/assets/{general-settings-section-BEMIbE4Y.js → general-settings-section-BnKYY0AA.js} +1 -1
- package/dist/web/assets/general-settings-section-BnKYY0AA.js.br +0 -0
- package/dist/web/assets/general-settings-section-BnKYY0AA.js.gz +0 -0
- package/dist/web/assets/{git-log-panel-Cp2174iI.js → git-log-panel-JiWfR5xv.js} +1 -1
- package/dist/web/assets/git-log-panel-JiWfR5xv.js.br +0 -0
- package/dist/web/assets/git-log-panel-JiWfR5xv.js.gz +0 -0
- package/dist/web/assets/{git-review-tab-CpMJ0qDi.js → git-review-tab-DgvcY__Q.js} +1 -1
- package/dist/web/assets/git-review-tab-DgvcY__Q.js.br +0 -0
- package/dist/web/assets/git-review-tab-DgvcY__Q.js.gz +0 -0
- package/dist/web/assets/{glide-data-grid-BAhwWUhO.js → glide-data-grid-r1MKxDjz.js} +1 -1
- package/dist/web/assets/glide-data-grid-r1MKxDjz.js.br +0 -0
- package/dist/web/assets/glide-data-grid-r1MKxDjz.js.gz +0 -0
- package/dist/web/assets/{grid-save-dialog-DmJhaWOl.js → grid-save-dialog-Fa0yA-Ep.js} +1 -1
- package/dist/web/assets/grid-save-dialog-Fa0yA-Ep.js.br +0 -0
- package/dist/web/assets/grid-save-dialog-Fa0yA-Ep.js.gz +0 -0
- package/dist/web/assets/{group-chat-tab-Bq7-Yegf.js → group-chat-tab-B2Q8yqSX.js} +1 -1
- package/dist/web/assets/group-chat-tab-B2Q8yqSX.js.br +0 -0
- package/dist/web/assets/group-chat-tab-B2Q8yqSX.js.gz +0 -0
- package/dist/web/assets/{impexp-tab-COx3Cvs7.js → impexp-tab-B8Ht4l32.js} +1 -1
- package/dist/web/assets/impexp-tab-B8Ht4l32.js.br +0 -0
- package/dist/web/assets/impexp-tab-B8Ht4l32.js.gz +0 -0
- package/dist/web/assets/{index-BGk8EFQb.js → index-BtAn_krv.js} +5 -5
- package/dist/web/assets/index-BtAn_krv.js.br +0 -0
- package/dist/web/assets/index-BtAn_krv.js.gz +0 -0
- package/dist/web/assets/{jira-watcher-section-BEPbsi62.js → jira-watcher-section--ibKZfBs.js} +1 -1
- package/dist/web/assets/{keybindings-store-DzcSIJUF.js → keybindings-store-Cwxl4tHv.js} +1 -1
- package/dist/web/assets/{keyboard-shortcuts-section-Bqg7AZiZ.js → keyboard-shortcuts-section-aoiLpWwf.js} +1 -1
- package/dist/web/assets/keyboard-shortcuts-section-aoiLpWwf.js.br +0 -0
- package/dist/web/assets/keyboard-shortcuts-section-aoiLpWwf.js.gz +0 -0
- package/dist/web/assets/{language-servers-section-CJ47DSIA.js → language-servers-section-BkGAezo6.js} +2 -2
- package/dist/web/assets/language-servers-section-BkGAezo6.js.br +0 -0
- package/dist/web/assets/language-servers-section-BkGAezo6.js.gz +0 -0
- package/dist/web/assets/{logs-app-BK-n-y_K.js → logs-app-DYgPtmjm.js} +1 -1
- package/dist/web/assets/logs-app-DYgPtmjm.js.br +0 -0
- package/dist/web/assets/logs-app-DYgPtmjm.js.gz +0 -0
- package/dist/web/assets/{logs-tab-B84EZJ1b.js → logs-tab-BbDosK63.js} +1 -1
- package/dist/web/assets/{logs-window-content-B1uN_3pN.js → logs-window-content-CEXAyv-f.js} +1 -1
- package/dist/web/assets/{lsp-client-DHSaAyJW.js → lsp-client-CIcphNmU.js} +1 -1
- package/dist/web/assets/lsp-client-CIcphNmU.js.br +0 -0
- package/dist/web/assets/lsp-client-CIcphNmU.js.gz +0 -0
- package/dist/web/assets/{markdown-renderer-BjDQUqT-.js → markdown-renderer-BNzQeoAO.js} +1 -1
- package/dist/web/assets/{markdown-renderer-CpxdNG0f.js → markdown-renderer-dk1w3goH.js} +1 -1
- package/dist/web/assets/markdown-renderer-dk1w3goH.js.br +0 -0
- package/dist/web/assets/markdown-renderer-dk1w3goH.js.gz +0 -0
- package/dist/web/assets/{mobile-explorer-sheet-BBwXGmXA.js → mobile-explorer-sheet-D4lPZQZc.js} +1 -1
- package/dist/web/assets/mobile-explorer-sheet-D4lPZQZc.js.br +0 -0
- package/dist/web/assets/mobile-explorer-sheet-D4lPZQZc.js.gz +0 -0
- package/dist/web/assets/{new-design-dialog-B_ZOvGhR.js → new-design-dialog-CIq7xspW.js} +1 -1
- package/dist/web/assets/new-design-dialog-CIq7xspW.js.br +0 -0
- package/dist/web/assets/new-design-dialog-CIq7xspW.js.gz +0 -0
- package/dist/web/assets/{notification-store-HuMqfadD.js → notification-store-Rv36noYX.js} +1 -1
- package/dist/web/assets/notifications-settings-section-CoWMtXCC.js +1 -0
- package/dist/web/assets/notifications-settings-section-CoWMtXCC.js.br +0 -0
- package/dist/web/assets/notifications-settings-section-CoWMtXCC.js.gz +0 -0
- package/dist/web/assets/{ppmbot-settings-section-JDvyI-gZ.js → ppmbot-settings-section-Ct0UwA6T.js} +1 -1
- package/dist/web/assets/ppmbot-settings-section-Ct0UwA6T.js.br +0 -0
- package/dist/web/assets/ppmbot-settings-section-Ct0UwA6T.js.gz +0 -0
- package/dist/web/assets/{problems-panel-Bsv_L6T2.js → problems-panel-D-HT-0PE.js} +1 -1
- package/dist/web/assets/problems-panel-D-HT-0PE.js.br +0 -0
- package/dist/web/assets/problems-panel-D-HT-0PE.js.gz +0 -0
- package/dist/web/assets/{process-table-3kZyWOJx.js → process-table-CXN0s4Pq.js} +1 -1
- package/dist/web/assets/process-table-CXN0s4Pq.js.br +0 -0
- package/dist/web/assets/process-table-CXN0s4Pq.js.gz +0 -0
- package/dist/web/assets/{proxy-settings-section-CYcmIp7W.js → proxy-settings-section-e6WWzP8n.js} +1 -1
- package/dist/web/assets/proxy-settings-section-e6WWzP8n.js.br +0 -0
- package/dist/web/assets/proxy-settings-section-e6WWzP8n.js.gz +0 -0
- package/dist/web/assets/{query-tab-CjrWnQUl.js → query-tab-C7C67t8F.js} +1 -1
- package/dist/web/assets/query-tab-C7C67t8F.js.br +0 -0
- package/dist/web/assets/query-tab-C7C67t8F.js.gz +0 -0
- package/dist/web/assets/{remote-access-settings-section-iQkakiVx.js → remote-access-settings-section-gGoATCF9.js} +1 -1
- package/dist/web/assets/remote-access-settings-section-gGoATCF9.js.br +0 -0
- package/dist/web/assets/remote-access-settings-section-gGoATCF9.js.gz +0 -0
- package/dist/web/assets/{remote-desktop-clipboard-notice-CFLnb7ML.js → remote-desktop-clipboard-notice-D5KjvMZx.js} +1 -1
- package/dist/web/assets/remote-desktop-clipboard-notice-D5KjvMZx.js.br +0 -0
- package/dist/web/assets/remote-desktop-clipboard-notice-D5KjvMZx.js.gz +0 -0
- package/dist/web/assets/{remote-desktop-mobile-sheet-C_-_-Nkz.js → remote-desktop-mobile-sheet-CY_DsGXZ.js} +2 -2
- package/dist/web/assets/remote-desktop-mobile-sheet-CY_DsGXZ.js.br +0 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-CY_DsGXZ.js.gz +0 -0
- package/dist/web/assets/{remote-desktop-mobile-view-BnHSrind.js → remote-desktop-mobile-view-DdpI-RF8.js} +1 -1
- package/dist/web/assets/remote-desktop-mobile-view-DdpI-RF8.js.br +0 -0
- package/dist/web/assets/remote-desktop-mobile-view-DdpI-RF8.js.gz +0 -0
- package/dist/web/assets/{remote-desktop-readiness-gate-BGHJ7Sv6.js → remote-desktop-readiness-gate-WIyCCAv5.js} +1 -1
- package/dist/web/assets/remote-desktop-readiness-gate-WIyCCAv5.js.br +0 -0
- package/dist/web/assets/remote-desktop-readiness-gate-WIyCCAv5.js.gz +0 -0
- package/dist/web/assets/{remote-desktop-window-content-BBm1YwM8.js → remote-desktop-window-content-BMORZLZt.js} +1 -1
- package/dist/web/assets/remote-desktop-window-content-BMORZLZt.js.br +0 -0
- package/dist/web/assets/remote-desktop-window-content-BMORZLZt.js.gz +0 -0
- package/dist/web/assets/{rename-object-dialog-BLnF9xyn.js → rename-object-dialog-pIgWe_AL.js} +1 -1
- package/dist/web/assets/rename-object-dialog-pIgWe_AL.js.br +0 -0
- package/dist/web/assets/rename-object-dialog-pIgWe_AL.js.gz +0 -0
- package/dist/web/assets/{review-code-DG7sCIzr.js → review-code-oTNcTYlI.js} +1 -1
- package/dist/web/assets/review-code-oTNcTYlI.js.br +0 -0
- package/dist/web/assets/review-code-oTNcTYlI.js.gz +0 -0
- package/dist/web/assets/{save-as-dialog-KW7cErWh.js → save-as-dialog-CnPLGTaL.js} +1 -1
- package/dist/web/assets/save-as-dialog-CnPLGTaL.js.br +0 -0
- package/dist/web/assets/save-as-dialog-CnPLGTaL.js.gz +0 -0
- package/dist/web/assets/{schedules-settings-section-B4AV4ORv.js → schedules-settings-section-CREqHG_i.js} +1 -1
- package/dist/web/assets/schedules-settings-section-CREqHG_i.js.br +0 -0
- package/dist/web/assets/schedules-settings-section-CREqHG_i.js.gz +0 -0
- package/dist/web/assets/{session-review-tab-C5nPyBzo.js → session-review-tab-nFVJYYJH.js} +1 -1
- package/dist/web/assets/session-review-tab-nFVJYYJH.js.br +0 -0
- package/dist/web/assets/session-review-tab-nFVJYYJH.js.gz +0 -0
- package/dist/web/assets/{settings-body-xWSmRXUD.js → settings-body-DNzH0wOQ.js} +1 -1
- package/dist/web/assets/settings-body-DNzH0wOQ.js.br +0 -0
- package/dist/web/assets/settings-body-DNzH0wOQ.js.gz +0 -0
- package/dist/web/assets/{settings-rows-ChKBHk07.js → settings-rows-D02_4sDY.js} +1 -1
- package/dist/web/assets/settings-rows-D02_4sDY.js.br +0 -0
- package/dist/web/assets/settings-rows-D02_4sDY.js.gz +0 -0
- package/dist/web/assets/settings-section-content-BYW_9mHv.js +1 -0
- package/dist/web/assets/{settings-section-content-DdVdyPVX.js → settings-section-content-DWopMfmq.js} +2 -2
- package/dist/web/assets/settings-section-content-DWopMfmq.js.br +0 -0
- package/dist/web/assets/settings-section-content-DWopMfmq.js.gz +0 -0
- package/dist/web/assets/{settings-tab-CzaBBNoj.js → settings-tab-Cg0v5kO7.js} +1 -1
- package/dist/web/assets/{settings-window-content-DAvWSu_O.js → settings-window-content-CqhwxJJn.js} +1 -1
- package/dist/web/assets/{slash-command-picker-Cn2Fdz2p.js → slash-command-picker-Bcizxvbh.js} +1 -1
- package/dist/web/assets/slash-command-picker-Bcizxvbh.js.br +0 -0
- package/dist/web/assets/slash-command-picker-Bcizxvbh.js.gz +0 -0
- package/dist/web/assets/{sql-object-tab-_mPRCvD9.js → sql-object-tab-b1Z2zVBm.js} +1 -1
- package/dist/web/assets/sql-object-tab-b1Z2zVBm.js.br +0 -0
- package/dist/web/assets/sql-object-tab-b1Z2zVBm.js.gz +0 -0
- package/dist/web/assets/{sqlite-viewer-B8VZt-mA.js → sqlite-viewer-DhPv28tq.js} +1 -1
- package/dist/web/assets/sqlite-viewer-DhPv28tq.js.br +0 -0
- package/dist/web/assets/sqlite-viewer-DhPv28tq.js.gz +0 -0
- package/dist/web/assets/{structure-save-dialog-Lcst7kAl.js → structure-save-dialog-sGUpqs8D.js} +1 -1
- package/dist/web/assets/structure-save-dialog-sGUpqs8D.js.br +0 -0
- package/dist/web/assets/structure-save-dialog-sGUpqs8D.js.gz +0 -0
- package/dist/web/assets/{structure-tab-ljJCzxUe.js → structure-tab-Bo2OmDOc.js} +1 -1
- package/dist/web/assets/structure-tab-Bo2OmDOc.js.br +0 -0
- package/dist/web/assets/structure-tab-Bo2OmDOc.js.gz +0 -0
- package/dist/web/assets/{system-monitor-body-CTi5xUZz.js → system-monitor-body-C475Et9u.js} +2 -2
- package/dist/web/assets/system-monitor-body-C475Et9u.js.br +0 -0
- package/dist/web/assets/system-monitor-body-C475Et9u.js.gz +0 -0
- package/dist/web/assets/{system-monitor-tab-DeL2YoXO.js → system-monitor-tab-Cm52yoW8.js} +1 -1
- package/dist/web/assets/{system-monitor-window-content-DK-rQIZh.js → system-monitor-window-content-YYsGHxZF.js} +1 -1
- package/dist/web/assets/{tab-host-window-content-CG0vPTc_.js → tab-host-window-content-BKI6QoDa.js} +1 -1
- package/dist/web/assets/tab-host-window-content-BKI6QoDa.js.br +0 -0
- package/dist/web/assets/tab-host-window-content-BKI6QoDa.js.gz +0 -0
- package/dist/web/assets/{table-tab-CTZ3nCWV.js → table-tab-D-nKWMJR.js} +1 -1
- package/dist/web/assets/table-tab-D-nKWMJR.js.br +0 -0
- package/dist/web/assets/table-tab-D-nKWMJR.js.gz +0 -0
- package/dist/web/assets/{telegram-bot-token-field-DF7zq7_h.js → telegram-bot-token-field-BeNS2X2k.js} +1 -1
- package/dist/web/assets/telegram-bot-token-field-BeNS2X2k.js.br +0 -0
- package/dist/web/assets/telegram-bot-token-field-BeNS2X2k.js.gz +0 -0
- package/dist/web/assets/{terminal-tab-B_jECWTH.js → terminal-tab-BbP753Ab.js} +1 -1
- package/dist/web/assets/terminal-tab-BbP753Ab.js.br +0 -0
- package/dist/web/assets/terminal-tab-BbP753Ab.js.gz +0 -0
- package/dist/web/assets/{tool-cards-CsD_VXcG.js → tool-cards-Dh8Ekn2W.js} +2 -2
- package/dist/web/assets/tool-cards-Dh8Ekn2W.js.br +0 -0
- package/dist/web/assets/tool-cards-Dh8Ekn2W.js.gz +0 -0
- package/dist/web/assets/{tools-settings-section-DbBr0Hnr.js → tools-settings-section-DGZRIULE.js} +1 -1
- package/dist/web/assets/tools-settings-section-DGZRIULE.js.br +0 -0
- package/dist/web/assets/tools-settings-section-DGZRIULE.js.gz +0 -0
- package/dist/web/assets/{use-inline-blame-BA2mrMxT.js → use-inline-blame-BxwM-Wkj.js} +1 -1
- package/dist/web/assets/use-inline-blame-BxwM-Wkj.js.br +0 -0
- package/dist/web/assets/use-inline-blame-BxwM-Wkj.js.gz +0 -0
- package/dist/web/assets/{use-lsp-ChbrzwvS.js → use-lsp-BqvwRKmM.js} +1 -1
- package/dist/web/assets/use-lsp-BqvwRKmM.js.br +0 -0
- package/dist/web/assets/use-lsp-BqvwRKmM.js.gz +0 -0
- package/dist/web/assets/{use-monaco-theme-C3_wfz5J.js → use-monaco-theme-DU0EpDHH.js} +1 -1
- package/dist/web/assets/{use-process-kill-C-YetnGd.js → use-process-kill-CVN_c4KF.js} +1 -1
- package/dist/web/assets/use-process-kill-CVN_c4KF.js.br +0 -0
- package/dist/web/assets/use-process-kill-CVN_c4KF.js.gz +0 -0
- package/dist/web/assets/{use-sql-schema-info-pKwLD9wk.js → use-sql-schema-info-C0wBmR6-.js} +1 -1
- package/dist/web/assets/use-sql-schema-info-C0wBmR6-.js.br +0 -0
- package/dist/web/assets/use-sql-schema-info-C0wBmR6-.js.gz +0 -0
- package/dist/web/assets/{use-websocket-BFnUq0-x.js → use-websocket-K0OUXbbY.js} +1 -1
- package/dist/web/assets/{web-preview-tab-DGLPuZYM.js → web-preview-tab-DOgLv8Gt.js} +1 -1
- package/dist/web/assets/web-preview-tab-DOgLv8Gt.js.br +0 -0
- package/dist/web/assets/web-preview-tab-DOgLv8Gt.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/deployment-guide.md +39 -0
- package/package.json +1 -1
- package/scripts/mem-diag-report.ts +31 -0
- package/src/providers/registry.ts +10 -1
- package/src/server/index.ts +7 -6
- package/src/server/middleware/access-log.ts +3 -0
- package/src/server/middleware/cors-headers.ts +44 -0
- package/src/server/routes/chat.ts +4 -1
- package/src/server/routes/database-impexp.ts +1 -1
- package/src/server/routes/design-preview.ts +2 -2
- package/src/server/ws/chat.ts +3 -0
- package/src/services/compact-segment.ts +14 -19
- package/src/services/database/grid-export-tickets.ts +6 -2
- package/src/services/database/impexp/readers/file-text.ts +6 -3
- package/src/services/edge-forwarder.ts +12 -0
- package/src/services/file-chunks.ts +39 -0
- package/src/services/file-lines.ts +10 -17
- package/src/services/lsp/lsp-manager.ts +3 -0
- package/src/services/memory-diagnostics-native.ts +110 -0
- package/src/services/memory-diagnostics-report.ts +200 -0
- package/src/services/memory-diagnostics-requests.ts +95 -0
- package/src/services/memory-diagnostics-spawns.ts +242 -0
- package/src/services/memory-diagnostics.ts +121 -23
- package/src/services/memory-watchdog.ts +185 -0
- package/src/services/nested-subagent-spy.ts +3 -0
- package/src/services/process-thread-snapshot.ts +12 -2
- package/src/services/process-tree-memory.ts +161 -0
- package/src/services/read-lines.ts +16 -27
- package/src/services/supervisor.ts +1 -0
- package/src/services/terminal.service.ts +8 -0
- package/src/services/transcript-images-file.ts +7 -11
- package/src/web/components/chat/question-card.tsx +10 -4
- package/src/web/components/settings/notifications-settings-section.tsx +14 -1
- package/src/web/lib/web-push-client.ts +35 -5
- package/src/web/lib/web-push-support.ts +57 -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 +0 -1
- 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
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Counts HTTP requests by route for the memory monitor, so growth can be put next to traffic.
|
|
3
|
+
*
|
|
4
|
+
* The server's 26/10/2026 leak kept about a file's size of native memory per *file response*
|
|
5
|
+
* and per transcript read. Spawn counts could not show that: it rose with requests, not with
|
|
6
|
+
* child processes. A per-route count and the bytes each route sent, logged beside private
|
|
7
|
+
* bytes, is what lets the next reader say "it grows by N MB per `/api/files/raw`".
|
|
8
|
+
*
|
|
9
|
+
* Counters are cumulative; each reader keeps its own previous copy and diffs (`diffRequestCounts`),
|
|
10
|
+
* because the 30-second monitor and the 30-minute summary read the same counters at different rates.
|
|
11
|
+
*
|
|
12
|
+
* A route key is the method plus the path with anything that is data — a project name, an id,
|
|
13
|
+
* a file path, a token — replaced by a placeholder, so the log never carries one.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface RouteCount {
|
|
17
|
+
count: number;
|
|
18
|
+
/** Bytes declared by `Content-Length`; a streamed body has none and is counted in `streamed`. */
|
|
19
|
+
bytes: number;
|
|
20
|
+
streamed: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Segments after which the next one is a name or a token, never a route word. */
|
|
24
|
+
const NAME_BEFORE = new Set(["project", "projects", "session", "sessions", "content", "grid-export", "whep", "designs", "accounts", "connections", "extensions"]);
|
|
25
|
+
/** A segment that reads as a route word rather than data. */
|
|
26
|
+
const ROUTE_WORD = /^[a-z][a-z0-9-]{0,30}$/;
|
|
27
|
+
const MAX_SEGMENTS = 4;
|
|
28
|
+
/** Distinct keys kept; a path pattern we failed to normalise must not grow the map forever. */
|
|
29
|
+
const MAX_KEYS = 500;
|
|
30
|
+
|
|
31
|
+
/** `GET /api/project/ppm/files/raw?path=…` → `GET /api/project/:name/files`. Exported for tests. */
|
|
32
|
+
export function requestRouteKey(method: string, pathname: string): string {
|
|
33
|
+
const segs = pathname.split("/").filter(Boolean);
|
|
34
|
+
if (segs[0] !== "api" && segs[0] !== "ws") {
|
|
35
|
+
const ext = pathname.match(/\.([a-z0-9]{1,8})$/i)?.[1]?.toLowerCase();
|
|
36
|
+
return `${method} static${ext ? ` .${ext}` : ""}`;
|
|
37
|
+
}
|
|
38
|
+
const out: string[] = [];
|
|
39
|
+
for (let i = 0; i < segs.length && out.length < MAX_SEGMENTS; i++) {
|
|
40
|
+
const seg = segs[i]!;
|
|
41
|
+
const prev = segs[i - 1];
|
|
42
|
+
if (prev && NAME_BEFORE.has(prev)) out.push(":name");
|
|
43
|
+
else out.push(ROUTE_WORD.test(seg) ? seg : ":x");
|
|
44
|
+
}
|
|
45
|
+
return `${method} /${out.join("/")}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class RequestTracker {
|
|
49
|
+
private readonly counts = new Map<string, RouteCount>();
|
|
50
|
+
private total = 0;
|
|
51
|
+
|
|
52
|
+
record(method: string, pathname: string, contentLength: string | null): void {
|
|
53
|
+
this.total++;
|
|
54
|
+
const key = requestRouteKey(method, pathname);
|
|
55
|
+
let entry = this.counts.get(key);
|
|
56
|
+
if (!entry) {
|
|
57
|
+
if (this.counts.size >= MAX_KEYS) return;
|
|
58
|
+
entry = { count: 0, bytes: 0, streamed: 0 };
|
|
59
|
+
this.counts.set(key, entry);
|
|
60
|
+
}
|
|
61
|
+
entry.count++;
|
|
62
|
+
const n = contentLength === null ? NaN : Number(contentLength);
|
|
63
|
+
if (Number.isFinite(n)) entry.bytes += n; else entry.streamed++;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A copy of the cumulative counters. */
|
|
67
|
+
read(): { total: number; routes: Map<string, RouteCount> } {
|
|
68
|
+
const routes = new Map<string, RouteCount>();
|
|
69
|
+
for (const [k, v] of this.counts) routes.set(k, { ...v });
|
|
70
|
+
return { total: this.total, routes };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** What happened between two readings: the busiest routes, by count, with MB sent. */
|
|
75
|
+
export function diffRequestCounts(
|
|
76
|
+
prev: ReturnType<RequestTracker["read"]> | undefined,
|
|
77
|
+
now: ReturnType<RequestTracker["read"]>,
|
|
78
|
+
limit: number,
|
|
79
|
+
): { total: number; top: Record<string, { n: number; mb: number; streamed: number }> } {
|
|
80
|
+
const rows: Array<[string, number, number, number]> = [];
|
|
81
|
+
for (const [key, v] of now.routes) {
|
|
82
|
+
const p = prev?.routes.get(key);
|
|
83
|
+
const n = v.count - (p?.count ?? 0);
|
|
84
|
+
if (n <= 0) continue;
|
|
85
|
+
rows.push([key, n, v.bytes - (p?.bytes ?? 0), v.streamed - (p?.streamed ?? 0)]);
|
|
86
|
+
}
|
|
87
|
+
rows.sort((a, b) => b[1] - a[1]);
|
|
88
|
+
const top: Record<string, { n: number; mb: number; streamed: number }> = {};
|
|
89
|
+
for (const [key, n, bytes, streamed] of rows.slice(0, limit)) {
|
|
90
|
+
top[key] = { n, mb: Math.round((bytes / (1024 * 1024)) * 10) / 10, streamed };
|
|
91
|
+
}
|
|
92
|
+
return { total: now.total - (prev?.total ?? 0), top };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const requestTracker = new RequestTracker();
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Counts every child process the server starts, for the memory monitor.
|
|
3
|
+
*
|
|
4
|
+
* A child process is the prime suspect for native growth on Windows: each
|
|
5
|
+
* `powershell.exe` spawn was measured to cost Bun a 32 MiB allocator segment it never
|
|
6
|
+
* returns (see `system-metrics/powershell-session.ts`). A per-interval count by command,
|
|
7
|
+
* logged beside private bytes, is what lets a later reader say "the commit charge climbs
|
|
8
|
+
* by N MB per `git` spawn" instead of guessing.
|
|
9
|
+
*
|
|
10
|
+
* It wraps `Bun.spawn` and `Bun.spawnSync`, which is enough: in Bun, `node:child_process`
|
|
11
|
+
* (`spawn`, `exec`, `execFile`, and their sync forms — what the Claude SDK, simple-git and
|
|
12
|
+
* codex go through) is built on those two globals, so the wrapper sees them too. Only a
|
|
13
|
+
* module that saved its own reference to `Bun.spawn` before the wrapper went in is missed.
|
|
14
|
+
*
|
|
15
|
+
* A label is the program's basename plus at most one more word that is known not to be data:
|
|
16
|
+
* a git subcommand, a script file's name, a runner's own subcommand, or the first program a
|
|
17
|
+
* shell is told to run. Anything else — an inline script, an argument, a word carrying `=`,
|
|
18
|
+
* `$` or quotes — is left out, because an argument can be a path, a prompt or a token.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
interface LiveChild {
|
|
22
|
+
pid: number;
|
|
23
|
+
label: string;
|
|
24
|
+
startedAt: number;
|
|
25
|
+
/** The chat session a Claude/Codex subprocess serves, from its own `--resume`/`--session-id`. */
|
|
26
|
+
session?: string;
|
|
27
|
+
/** Private bytes the last time the tree was read, so an exit can say what the child held. */
|
|
28
|
+
lastPrivateMB?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** What a long-lived child's start and exit look like to whoever logs them. */
|
|
32
|
+
export interface ChildLifecycleEvent {
|
|
33
|
+
event: "start" | "exit";
|
|
34
|
+
pid: number;
|
|
35
|
+
label: string;
|
|
36
|
+
session?: string;
|
|
37
|
+
ageMin?: number;
|
|
38
|
+
lastPrivateMB?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Labels whose start and exit are worth a log line: few, long-lived, and each one is a chat. */
|
|
42
|
+
const LIFECYCLE_LABEL = /^(claude|codex)\b/;
|
|
43
|
+
/** Session ids are UUIDs; anything else after the flag is not taken. */
|
|
44
|
+
const SESSION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
45
|
+
const SESSION_FLAGS = ["--resume", "--session-id"];
|
|
46
|
+
|
|
47
|
+
/** The session a command line resumes or names, when it carries one. Exported for tests. */
|
|
48
|
+
export function sessionOf(argv: readonly unknown[]): string | undefined {
|
|
49
|
+
for (let i = 0; i < argv.length; i++) {
|
|
50
|
+
const a = argv[i];
|
|
51
|
+
if (typeof a !== "string") continue;
|
|
52
|
+
for (const flag of SESSION_FLAGS) {
|
|
53
|
+
const value = a === flag ? argv[i + 1] : a.startsWith(`${flag}=`) ? a.slice(flag.length + 1) : undefined;
|
|
54
|
+
if (typeof value === "string" && SESSION_ID.test(value)) return value;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface SpawnSnapshot {
|
|
61
|
+
/** Spawns since the previous snapshot, by label (sync ones included). */
|
|
62
|
+
spawned: Record<string, number>;
|
|
63
|
+
/** Children still running now, by label. */
|
|
64
|
+
live: Record<string, number>;
|
|
65
|
+
liveTotal: number;
|
|
66
|
+
/** Every spawn since the tracker started. */
|
|
67
|
+
total: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Programs whose own name says nothing: the script or subcommand after them is the label. */
|
|
71
|
+
const RUNNERS = new Set(["node", "bun", "npx", "bunx", "python", "python3", "py", "deno"]);
|
|
72
|
+
/** Programs handed a whole command line as one argument. */
|
|
73
|
+
const SHELLS = new Set(["cmd", "powershell", "pwsh", "sh", "bash", "zsh"]);
|
|
74
|
+
/** A runner flag whose value is code to run: the label stops at the flag. */
|
|
75
|
+
const EVAL_FLAGS = new Set(["-e", "--eval", "-p", "--print", "-c"]);
|
|
76
|
+
/** A runner's own subcommands, which are words and never data. */
|
|
77
|
+
const RUNNER_SUBCOMMANDS = new Set(["run", "x", "exec", "install", "add", "remove", "test", "build", "upgrade"]);
|
|
78
|
+
/** Runner flags that take a separate value, which is not the script. */
|
|
79
|
+
const RUNNER_VALUE_FLAGS = new Set(["-r", "--require", "--import", "--loader", "--cwd", "--env-file", "--config", "-m"]);
|
|
80
|
+
/** The shell flag after which the command line comes. */
|
|
81
|
+
const SHELL_COMMAND_FLAGS = new Set(["-c", "/c", "/k", "-command", "-file"]);
|
|
82
|
+
const SCRIPT_FILE = /\.(c|m)?[jt]sx?$|\.py$|\.ps1$/i;
|
|
83
|
+
/** A word that is plausibly a program or subcommand name and cannot carry an assignment or a quote. */
|
|
84
|
+
const PLAIN_WORD = /^[A-Za-z0-9._\\/:@+-]+$/;
|
|
85
|
+
const GIT_VALUE_FLAGS = new Set(["-c", "-C", "--git-dir", "--work-tree", "--namespace", "--exec-path"]);
|
|
86
|
+
|
|
87
|
+
/** Lowercased basename with the executable extension dropped, cut to 40 characters. */
|
|
88
|
+
function baseName(path: string): string {
|
|
89
|
+
const name = path.split(/[\\/]/).pop() ?? "";
|
|
90
|
+
return name.toLowerCase().replace(/\.(exe|cmd|bat|com)$/, "").slice(0, 40);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function gitLabel(rest: string[]): string {
|
|
94
|
+
for (let i = 0; i < rest.length; i++) {
|
|
95
|
+
const a = rest[i]!;
|
|
96
|
+
if (GIT_VALUE_FLAGS.has(a)) { i++; continue; }
|
|
97
|
+
if (a.startsWith("-")) continue;
|
|
98
|
+
return /^[a-z][a-z0-9-]*$/.test(a) ? `git ${a}` : "git";
|
|
99
|
+
}
|
|
100
|
+
return "git";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function runnerLabel(program: string, rest: string[]): string {
|
|
104
|
+
for (let i = 0; i < rest.length; i++) {
|
|
105
|
+
const a = rest[i]!;
|
|
106
|
+
if (EVAL_FLAGS.has(a)) return `${program} ${a}`;
|
|
107
|
+
if (RUNNER_VALUE_FLAGS.has(a)) { i++; continue; }
|
|
108
|
+
if (a.startsWith("-")) continue;
|
|
109
|
+
if (!PLAIN_WORD.test(a)) return program;
|
|
110
|
+
if (SCRIPT_FILE.test(a)) return `${program} ${baseName(a)}`;
|
|
111
|
+
if (RUNNER_SUBCOMMANDS.has(a)) return `${program} ${a}`;
|
|
112
|
+
// `npx <package>`: the package's own name, its scope and version dropped.
|
|
113
|
+
const pkg = a.match(/^(?:@[\w.-]+\/)?([\w.-]+)(?:@[\w.^~-]+)?$/);
|
|
114
|
+
if ((program === "npx" || program === "bunx") && pkg) return `${program} ${pkg[1]!.toLowerCase().slice(0, 40)}`;
|
|
115
|
+
return program;
|
|
116
|
+
}
|
|
117
|
+
return program;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function shellLabel(program: string, rest: string[]): string {
|
|
121
|
+
const at = rest.findIndex((a) => SHELL_COMMAND_FLAGS.has(a.toLowerCase()));
|
|
122
|
+
const line = at === -1 ? undefined : rest[at + 1];
|
|
123
|
+
if (line === undefined) return program;
|
|
124
|
+
const first = line.trim().split(/\s+/)[0] ?? "";
|
|
125
|
+
if (!first || !PLAIN_WORD.test(first)) return program;
|
|
126
|
+
return `${program} ${baseName(first)}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** A short, argument-free name for a command line. Exported for tests. */
|
|
130
|
+
export function spawnLabel(argv: readonly unknown[]): string {
|
|
131
|
+
const parts = argv.filter((a): a is string => typeof a === "string");
|
|
132
|
+
const [head, ...rest] = parts;
|
|
133
|
+
if (head === undefined) return "unknown";
|
|
134
|
+
const program = baseName(head.replace(/^["']|["']$/g, "")).replace(/[^a-z0-9._+-]/g, "");
|
|
135
|
+
if (!program) return "unknown";
|
|
136
|
+
if (program === "git") return gitLabel(rest);
|
|
137
|
+
if (RUNNERS.has(program)) return runnerLabel(program, rest);
|
|
138
|
+
if (SHELLS.has(program)) return shellLabel(program, rest);
|
|
139
|
+
return program;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** `Bun.spawn(cmd[], opts)` or `Bun.spawn({ cmd, ... })`. */
|
|
143
|
+
function argvOf(args: unknown[]): readonly unknown[] {
|
|
144
|
+
const first = args[0];
|
|
145
|
+
if (Array.isArray(first)) return first;
|
|
146
|
+
if (first && typeof first === "object" && Array.isArray((first as { cmd?: unknown }).cmd)) {
|
|
147
|
+
return (first as { cmd: unknown[] }).cmd;
|
|
148
|
+
}
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export class SpawnTracker {
|
|
153
|
+
/** Keyed by the child object, not its pid: a pid can be reused before the old child's `exited` settles. */
|
|
154
|
+
private readonly live = new Map<object, LiveChild>();
|
|
155
|
+
private interval = new Map<string, number>();
|
|
156
|
+
private readonly totals = new Map<string, number>();
|
|
157
|
+
private total = 0;
|
|
158
|
+
private installed = false;
|
|
159
|
+
/** Set by the memory watchdog to log long-lived children coming and going. */
|
|
160
|
+
onLifecycle?: (e: ChildLifecycleEvent) => void;
|
|
161
|
+
|
|
162
|
+
constructor(private readonly now: () => number = Date.now) {}
|
|
163
|
+
|
|
164
|
+
/** Record one spawn. Exported for tests; `install()` calls it for real spawns. */
|
|
165
|
+
record(label: string, child?: { pid?: number; exited?: Promise<unknown> }, session?: string): void {
|
|
166
|
+
this.total++;
|
|
167
|
+
this.interval.set(label, (this.interval.get(label) ?? 0) + 1);
|
|
168
|
+
this.totals.set(label, (this.totals.get(label) ?? 0) + 1);
|
|
169
|
+
const pid = child?.pid;
|
|
170
|
+
if (!child || typeof pid !== "number" || !child.exited) return;
|
|
171
|
+
const entry: LiveChild = { pid, label, startedAt: this.now(), session };
|
|
172
|
+
this.live.set(child, entry);
|
|
173
|
+
const logged = LIFECYCLE_LABEL.test(label);
|
|
174
|
+
if (logged) this.emit({ event: "start", pid, label, session });
|
|
175
|
+
const done = () => {
|
|
176
|
+
this.live.delete(child);
|
|
177
|
+
if (!logged) return;
|
|
178
|
+
this.emit({
|
|
179
|
+
event: "exit", pid, label, session,
|
|
180
|
+
ageMin: Math.round((this.now() - entry.startedAt) / 60_000),
|
|
181
|
+
lastPrivateMB: entry.lastPrivateMB,
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
child.exited.then(done, done);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private emit(e: ChildLifecycleEvent): void {
|
|
188
|
+
try { this.onLifecycle?.(e); } catch { /* a log line must never break a spawn */ }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** What the tracker knows about a live direct child, by pid. */
|
|
192
|
+
describe(pid: number): LiveChild | undefined {
|
|
193
|
+
for (const c of this.live.values()) if (c.pid === pid) return c;
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Wrap the two globals. Safe to call twice. */
|
|
198
|
+
install(): void {
|
|
199
|
+
if (this.installed) return;
|
|
200
|
+
this.installed = true;
|
|
201
|
+
const bun = Bun as unknown as Record<"spawn" | "spawnSync", (...a: unknown[]) => unknown>;
|
|
202
|
+
const spawn = bun.spawn;
|
|
203
|
+
const spawnSync = bun.spawnSync;
|
|
204
|
+
bun.spawn = (...args: unknown[]) => {
|
|
205
|
+
const child = spawn.apply(Bun, args) as { pid?: number; exited?: Promise<unknown> };
|
|
206
|
+
try {
|
|
207
|
+
const argv = argvOf(args);
|
|
208
|
+
this.record(spawnLabel(argv), child, sessionOf(argv));
|
|
209
|
+
} catch { /* counting must never break a spawn */ }
|
|
210
|
+
return child;
|
|
211
|
+
};
|
|
212
|
+
bun.spawnSync = (...args: unknown[]) => {
|
|
213
|
+
try { this.record(spawnLabel(argvOf(args))); } catch { /* as above */ }
|
|
214
|
+
return spawnSync.apply(Bun, args);
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Counts since the last call, plus what is alive now. Resets the interval counts. */
|
|
219
|
+
snapshot(): SpawnSnapshot {
|
|
220
|
+
const live: Record<string, number> = {};
|
|
221
|
+
for (const c of this.live.values()) live[c.label] = (live[c.label] ?? 0) + 1;
|
|
222
|
+
const spawned = Object.fromEntries(this.interval);
|
|
223
|
+
this.interval = new Map();
|
|
224
|
+
return { spawned, live, liveTotal: this.live.size, total: this.total };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** The longest-running children, for a threshold alert. */
|
|
228
|
+
oldestLive(limit: number): Array<{ pid: number; label: string; ageMin: number }> {
|
|
229
|
+
const t = this.now();
|
|
230
|
+
return [...this.live.values()]
|
|
231
|
+
.sort((a, b) => a.startedAt - b.startedAt)
|
|
232
|
+
.slice(0, limit)
|
|
233
|
+
.map((c) => ({ pid: c.pid, label: c.label, ageMin: Math.round((t - c.startedAt) / 60_000) }));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Spawns by label since the tracker started, most first. */
|
|
237
|
+
topTotals(limit: number): Record<string, number> {
|
|
238
|
+
return Object.fromEntries([...this.totals.entries()].sort((a, b) => b[1] - a[1]).slice(0, limit));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export const spawnTracker = new SpawnTracker();
|
|
@@ -1,19 +1,37 @@
|
|
|
1
|
-
import { appendFileSync, mkdirSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { appendFileSync, mkdirSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { heapStats } from "bun:jsc";
|
|
4
4
|
import { getPpmDir } from "./ppm-dir.ts";
|
|
5
|
+
import { readNativeMemory } from "./memory-diagnostics-native.ts";
|
|
6
|
+
import { spawnTracker } from "./memory-diagnostics-spawns.ts";
|
|
7
|
+
import { diffRequestCounts, requestTracker } from "./memory-diagnostics-requests.ts";
|
|
8
|
+
import { startMemoryWatchdog, summarizeDescendants } from "./memory-watchdog.ts";
|
|
5
9
|
|
|
6
10
|
/**
|
|
7
|
-
*
|
|
11
|
+
* Memory monitoring for each long-lived PPM process (server, supervisor, edge forwarder).
|
|
8
12
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* long-lived state (session maps, buffered events). Comparing RSS against `heapSize` is the
|
|
12
|
-
* first fork in the investigation — RSS climbing with a flat heap is native memory (child
|
|
13
|
-
* process pipes, Bun internals), which a heap snapshot cannot show.
|
|
13
|
+
* Always on: `memory-watchdog.ts`, a summary line in `ppm.log` every 30 minutes plus a WARN
|
|
14
|
+
* at each private-bytes threshold. Opt-in (`PPM_MEM_DIAG=1`): the detailed monitor below.
|
|
14
15
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
16
|
+
* Every interval it appends one JSON line to `<ppm dir>/mem-diag.log` (the supervisor
|
|
17
|
+
* writes `mem-diag-supervisor.log`): RSS, the JS heap, **private bytes** (the commit charge
|
|
18
|
+
* that actually runs a machine out of memory — RSS alone hid a 32 GB leak), kernel handles,
|
|
19
|
+
* the child processes started since the previous line and those still alive, the memory of
|
|
20
|
+
* every process below this one (by program, and by direct child with the chat session it
|
|
21
|
+
* serves), the HTTP routes served since the previous line, the object
|
|
22
|
+
* types that grew most, a few absolute counts of objects that own native resources, and
|
|
23
|
+
* any gauges registered by long-lived state (session maps, buffered events).
|
|
24
|
+
*
|
|
25
|
+
* How to read it: `bun scripts/mem-diag-report.ts` correlates private-byte growth with
|
|
26
|
+
* spawns and gauges. By hand, the first fork is still private vs heap — private climbing
|
|
27
|
+
* with a flat heap is native memory (child process pipes, allocator segments), which a
|
|
28
|
+
* heap snapshot cannot show; then look at what `spawned` and `gauges` did in the lines
|
|
29
|
+
* where `privateDeltaMB` jumps.
|
|
30
|
+
*
|
|
31
|
+
* Two kinds of one-off lines: a V8 heap snapshot the first time the heap crosses each
|
|
32
|
+
* threshold (diffable in Chrome DevTools → Memory → Comparison), and an `alert` line the
|
|
33
|
+
* first time private bytes cross each threshold, carrying the oldest live children and the
|
|
34
|
+
* all-time spawn totals — the state a later reader can no longer reconstruct.
|
|
17
35
|
*/
|
|
18
36
|
|
|
19
37
|
const gauges = new Map<string, () => number>();
|
|
@@ -23,23 +41,59 @@ export function registerMemoryGauge(name: string, read: () => number): void {
|
|
|
23
41
|
gauges.set(name, read);
|
|
24
42
|
}
|
|
25
43
|
|
|
44
|
+
function readGauges(): Record<string, number | string> {
|
|
45
|
+
const values: Record<string, number | string> = {};
|
|
46
|
+
for (const [name, read] of gauges) {
|
|
47
|
+
try { values[name] = read(); } catch (e) { values[name] = `err: ${(e as Error).message}`; }
|
|
48
|
+
}
|
|
49
|
+
return values;
|
|
50
|
+
}
|
|
51
|
+
|
|
26
52
|
const MB = 1024 * 1024;
|
|
27
53
|
const SNAPSHOT_THRESHOLDS_MB = [1024, 2048, 4096, 8192];
|
|
54
|
+
const PRIVATE_ALERT_THRESHOLDS_MB = [2048, 4096, 8192, 16384, 24576, 32768];
|
|
55
|
+
/** Object types that each hold something outside the JS heap. Absolute counts, every line. */
|
|
56
|
+
const NATIVE_OWNER_TYPES = ["Subprocess", "FSWatcher", "SQLStatement", "ArrayBuffer", "ReadableStream", "WebSocket", "ServerWebSocket", "Timeout"];
|
|
57
|
+
/** ~1 KB a line at 30 s is ~3 MB a day; one rotated copy keeps about two weeks. */
|
|
58
|
+
const MAX_LOG_BYTES = 50 * MB;
|
|
59
|
+
|
|
60
|
+
export interface MemoryDiagnosticsOptions {
|
|
61
|
+
/** Which process this is; anything but `server` gets its own log file. */
|
|
62
|
+
role?: "server" | "supervisor" | "edge";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let started = false;
|
|
28
66
|
|
|
29
|
-
export function startMemoryDiagnostics(): void {
|
|
67
|
+
export function startMemoryDiagnostics(opts: MemoryDiagnosticsOptions = {}): void {
|
|
68
|
+
if (started) return;
|
|
69
|
+
started = true;
|
|
70
|
+
const role = opts.role ?? "server";
|
|
71
|
+
spawnTracker.install();
|
|
72
|
+
startMemoryWatchdog(role, readGauges);
|
|
30
73
|
if (process.env.PPM_MEM_DIAG !== "1") return;
|
|
31
74
|
const intervalMs = Math.max(5_000, Number(process.env.PPM_MEM_DIAG_INTERVAL_MS) || 30_000);
|
|
32
75
|
const dir = getPpmDir();
|
|
33
|
-
const logPath = join(dir, "mem-diag.log");
|
|
76
|
+
const logPath = join(dir, role === "server" ? "mem-diag.log" : `mem-diag-${role}.log`);
|
|
34
77
|
const snapDir = join(dir, "heap-snapshots");
|
|
35
78
|
const taken = new Set<number>();
|
|
79
|
+
const alerted = new Set<number>();
|
|
36
80
|
let lastCounts: Record<string, number> = {};
|
|
81
|
+
let lastPrivateMB: number | undefined;
|
|
37
82
|
const startedAt = Date.now();
|
|
83
|
+
let lastRequests = requestTracker.read();
|
|
38
84
|
|
|
39
|
-
const
|
|
85
|
+
const write = (obj: unknown) => {
|
|
86
|
+
try {
|
|
87
|
+
if (statSync(logPath).size > MAX_LOG_BYTES) renameSync(logPath, `${logPath}.1`);
|
|
88
|
+
} catch { /* no file yet */ }
|
|
89
|
+
appendFileSync(logPath, JSON.stringify(obj) + "\n");
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const sample = async () => {
|
|
40
93
|
try {
|
|
41
94
|
const mem = process.memoryUsage();
|
|
42
95
|
const stats = heapStats();
|
|
96
|
+
const native = await readNativeMemory();
|
|
43
97
|
const counts = stats.objectTypeCounts as Record<string, number>;
|
|
44
98
|
const grew = Object.entries(counts)
|
|
45
99
|
.map(([type, n]) => [type, n - (lastCounts[type] ?? 0)] as const)
|
|
@@ -48,32 +102,76 @@ export function startMemoryDiagnostics(): void {
|
|
|
48
102
|
.slice(0, 12);
|
|
49
103
|
lastCounts = counts;
|
|
50
104
|
|
|
51
|
-
const
|
|
52
|
-
for (const [
|
|
53
|
-
try { gaugeValues[name] = read(); } catch (e) { gaugeValues[name] = `err: ${(e as Error).message}`; }
|
|
54
|
-
}
|
|
105
|
+
const nativeOwners: Record<string, number> = {};
|
|
106
|
+
for (const type of NATIVE_OWNER_TYPES) if (counts[type] !== undefined) nativeOwners[type] = counts[type];
|
|
55
107
|
|
|
56
|
-
const
|
|
108
|
+
const spawns = spawnTracker.snapshot();
|
|
109
|
+
const gaugeValues = readGauges();
|
|
110
|
+
const children = await summarizeDescendants(process.pid, 10);
|
|
111
|
+
const requestsNow = requestTracker.read();
|
|
112
|
+
const requests = diffRequestCounts(lastRequests, requestsNow, 10);
|
|
113
|
+
lastRequests = requestsNow;
|
|
114
|
+
const privateDeltaMB = native.privateMB !== undefined && lastPrivateMB !== undefined
|
|
115
|
+
? native.privateMB - lastPrivateMB
|
|
116
|
+
: undefined;
|
|
117
|
+
if (native.privateMB !== undefined) lastPrivateMB = native.privateMB;
|
|
118
|
+
|
|
119
|
+
write({
|
|
57
120
|
t: new Date().toISOString(),
|
|
121
|
+
role,
|
|
122
|
+
pid: process.pid,
|
|
58
123
|
upMin: Math.round((Date.now() - startedAt) / 60_000),
|
|
59
124
|
rssMB: Math.round(mem.rss / MB),
|
|
125
|
+
...native,
|
|
126
|
+
privateDeltaMB,
|
|
60
127
|
heapMB: Math.round(stats.heapSize / MB),
|
|
128
|
+
heapCapacityMB: Math.round(stats.heapCapacity / MB),
|
|
129
|
+
extraMB: Math.round(stats.extraMemorySize / MB),
|
|
61
130
|
externalMB: Math.round(mem.external / MB),
|
|
62
131
|
arrayBuffersMB: Math.round(mem.arrayBuffers / MB),
|
|
63
132
|
objects: stats.objectCount,
|
|
133
|
+
protectedObjects: stats.protectedObjectCount,
|
|
134
|
+
spawned: spawns.spawned,
|
|
135
|
+
liveChildren: spawns.liveTotal,
|
|
136
|
+
liveByCommand: spawns.live,
|
|
137
|
+
spawnTotal: spawns.total,
|
|
138
|
+
children,
|
|
139
|
+
requests,
|
|
140
|
+
nativeOwners,
|
|
64
141
|
grew: Object.fromEntries(grew),
|
|
65
142
|
gauges: gaugeValues,
|
|
66
|
-
};
|
|
67
|
-
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (native.privateMB !== undefined) {
|
|
146
|
+
for (const threshold of PRIVATE_ALERT_THRESHOLDS_MB) {
|
|
147
|
+
if (native.privateMB < threshold || alerted.has(threshold)) continue;
|
|
148
|
+
alerted.add(threshold);
|
|
149
|
+
write({
|
|
150
|
+
t: new Date().toISOString(),
|
|
151
|
+
role,
|
|
152
|
+
pid: process.pid,
|
|
153
|
+
alert: "private-bytes",
|
|
154
|
+
thresholdMB: threshold,
|
|
155
|
+
privateMB: native.privateMB,
|
|
156
|
+
rssMB: Math.round(mem.rss / MB),
|
|
157
|
+
heapMB: Math.round(stats.heapSize / MB),
|
|
158
|
+
handles: native.handles,
|
|
159
|
+
spawnTotals: spawnTracker.topTotals(40),
|
|
160
|
+
oldestLiveChildren: spawnTracker.oldestLive(40),
|
|
161
|
+
children,
|
|
162
|
+
gauges: gaugeValues,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
68
166
|
|
|
69
167
|
const heapMB = stats.heapSize / MB;
|
|
70
168
|
for (const threshold of SNAPSHOT_THRESHOLDS_MB) {
|
|
71
169
|
if (heapMB < threshold || taken.has(threshold)) continue;
|
|
72
170
|
taken.add(threshold);
|
|
73
171
|
mkdirSync(snapDir, { recursive: true });
|
|
74
|
-
const file = join(snapDir, `heap-${threshold}MB-${Date.now()}.heapsnapshot`);
|
|
172
|
+
const file = join(snapDir, `heap-${role}-${threshold}MB-${Date.now()}.heapsnapshot`);
|
|
75
173
|
writeFileSync(file, Bun.generateHeapSnapshot("v8"));
|
|
76
|
-
|
|
174
|
+
write({ t: new Date().toISOString(), role, snapshot: file });
|
|
77
175
|
}
|
|
78
176
|
} catch (e) {
|
|
79
177
|
console.warn(`[mem-diag] sample failed: ${(e as Error).message}`);
|
|
@@ -81,6 +179,6 @@ export function startMemoryDiagnostics(): void {
|
|
|
81
179
|
};
|
|
82
180
|
|
|
83
181
|
console.log(`[mem-diag] sampling every ${intervalMs}ms → ${logPath}`);
|
|
84
|
-
sample();
|
|
85
|
-
setInterval(sample, intervalMs).unref?.();
|
|
182
|
+
void sample();
|
|
183
|
+
setInterval(() => { void sample(); }, intervalMs).unref?.();
|
|
86
184
|
}
|