@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,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Always-on memory watchdog: a few lines in `ppm.log`, so that "why is PPM using so much RAM"
|
|
3
|
+
* has an answer in the log a user already has, without anyone having set `PPM_MEM_DIAG`.
|
|
4
|
+
*
|
|
5
|
+
* Every minute it reads this process's private bytes and those of every process below it
|
|
6
|
+
* (one OS call, ~8 ms on Windows). It writes:
|
|
7
|
+
* - a summary line every 30 minutes — this process, its children by program, the biggest child
|
|
8
|
+
* branches with the chat session each one serves, the busiest HTTP routes since the last
|
|
9
|
+
* summary, and the registered gauges (sessions, clients, terminals…). WARN instead of INFO
|
|
10
|
+
* when private bytes grew by a gigabyte or more since the previous summary;
|
|
11
|
+
* - a WARN the first time private bytes (and, for the server, its children's total) cross each
|
|
12
|
+
* threshold, with the same detail;
|
|
13
|
+
* - an INFO when a Claude/Codex subprocess starts or exits, with its pid, its session and what
|
|
14
|
+
* it held at its last reading — the pid in the summary is then traceable to a chat.
|
|
15
|
+
*
|
|
16
|
+
* What it answered on 26/10/2026, after the fact and by hand: the server at 7 GB while its JS
|
|
17
|
+
* heap was 186 MB (a native leak), and 17 Claude subprocesses at ~650 MB each (by design, one
|
|
18
|
+
* per open chat). Both halves are in one summary line now.
|
|
19
|
+
*
|
|
20
|
+
* The detailed per-30-second JSON stays behind `PPM_MEM_DIAG=1` (`memory-diagnostics.ts`).
|
|
21
|
+
*/
|
|
22
|
+
import { createLogger } from "./logger.ts";
|
|
23
|
+
import { readNativeMemory } from "./memory-diagnostics-native.ts";
|
|
24
|
+
import { spawnTracker } from "./memory-diagnostics-spawns.ts";
|
|
25
|
+
import { diffRequestCounts, requestTracker } from "./memory-diagnostics-requests.ts";
|
|
26
|
+
import { descendantsOf, readProcessList } from "./process-tree-memory.ts";
|
|
27
|
+
import { heapStats } from "bun:jsc";
|
|
28
|
+
|
|
29
|
+
const log = createLogger("mem");
|
|
30
|
+
|
|
31
|
+
const MB = 1024 * 1024;
|
|
32
|
+
const WATCH_INTERVAL_MS = 60_000;
|
|
33
|
+
const SUMMARY_INTERVAL_MS = 30 * 60_000;
|
|
34
|
+
/** Growth between two summaries that turns the summary into a WARN. */
|
|
35
|
+
const FAST_GROWTH_MB = 1024;
|
|
36
|
+
const SELF_ALERT_MB = [2048, 4096, 8192, 16384, 32768];
|
|
37
|
+
const CHILDREN_ALERT_MB = [8192, 16384, 24576, 32768];
|
|
38
|
+
|
|
39
|
+
export interface ChildBranch {
|
|
40
|
+
pid: number;
|
|
41
|
+
name: string;
|
|
42
|
+
/** The tracker's label for a direct child, when it saw the spawn. */
|
|
43
|
+
label?: string;
|
|
44
|
+
session?: string;
|
|
45
|
+
ageMin?: number;
|
|
46
|
+
/** The child alone. */
|
|
47
|
+
ownMB: number;
|
|
48
|
+
/** The child and everything below it. */
|
|
49
|
+
treeMB: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface TreeSummary {
|
|
53
|
+
count: number;
|
|
54
|
+
privateMB: number;
|
|
55
|
+
byName: Record<string, { n: number; mb: number }>;
|
|
56
|
+
/** Direct children, biggest subtree first. */
|
|
57
|
+
branches: ChildBranch[];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Read the tree below `rootPid` and fold it by program and by direct child. Undefined when unreadable. */
|
|
61
|
+
export async function summarizeDescendants(rootPid: number, limit: number): Promise<TreeSummary | undefined> {
|
|
62
|
+
const list = await readProcessList(rootPid);
|
|
63
|
+
if (!list) return undefined;
|
|
64
|
+
const all = descendantsOf(rootPid, list);
|
|
65
|
+
const byName: TreeSummary["byName"] = {};
|
|
66
|
+
const branches = new Map<number, ChildBranch>();
|
|
67
|
+
let privateMB = 0;
|
|
68
|
+
for (const d of all) {
|
|
69
|
+
privateMB += d.privateMB;
|
|
70
|
+
const n = (byName[d.name] ??= { n: 0, mb: 0 });
|
|
71
|
+
n.n++;
|
|
72
|
+
n.mb += d.privateMB;
|
|
73
|
+
if (d.depth === 1) {
|
|
74
|
+
const tracked = spawnTracker.describe(d.pid);
|
|
75
|
+
if (tracked) tracked.lastPrivateMB = d.privateMB;
|
|
76
|
+
branches.set(d.pid, {
|
|
77
|
+
pid: d.pid, name: d.name, label: tracked?.label, session: tracked?.session,
|
|
78
|
+
ageMin: tracked ? Math.round((Date.now() - tracked.startedAt) / 60_000) : undefined,
|
|
79
|
+
ownMB: d.privateMB, treeMB: 0,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
for (const d of all) {
|
|
84
|
+
const b = branches.get(d.branchPid);
|
|
85
|
+
if (b) b.treeMB += d.privateMB;
|
|
86
|
+
}
|
|
87
|
+
return {
|
|
88
|
+
count: all.length,
|
|
89
|
+
privateMB,
|
|
90
|
+
byName,
|
|
91
|
+
branches: [...branches.values()].sort((a, b) => b.treeMB - a.treeMB).slice(0, limit),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const plural = (n: number, mb: number) => `${n > 1 ? `×${n} ` : ""}${mb}MB`;
|
|
96
|
+
|
|
97
|
+
/** The children half of a log line. Exported for tests. */
|
|
98
|
+
export function describeTree(tree: TreeSummary | undefined): string {
|
|
99
|
+
if (!tree) return "children unreadable";
|
|
100
|
+
if (tree.count === 0) return "no children";
|
|
101
|
+
const names = Object.entries(tree.byName)
|
|
102
|
+
.sort((a, b) => b[1].mb - a[1].mb).slice(0, 6)
|
|
103
|
+
.map(([name, v]) => `${name} ${plural(v.n, v.mb)}`).join(", ");
|
|
104
|
+
const biggest = tree.branches.slice(0, 5).map((b) => {
|
|
105
|
+
const who = b.session ? `session=${b.session.slice(0, 8)}` : b.label ?? b.name;
|
|
106
|
+
const own = b.treeMB !== b.ownMB ? ` (own ${b.ownMB}MB)` : "";
|
|
107
|
+
const age = b.ageMin !== undefined ? ` ${b.ageMin}m` : "";
|
|
108
|
+
return `${b.name} pid=${b.pid} ${who}${age} ${b.treeMB}MB${own}`;
|
|
109
|
+
}).join("; ");
|
|
110
|
+
return `children ${tree.count} = ${tree.privateMB}MB [${names}] biggest: ${biggest}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function describeRequests(top: Record<string, { n: number; mb: number; streamed: number }>, total: number): string {
|
|
114
|
+
const rows = Object.entries(top).map(([k, v]) => `${k} ×${v.n}${v.mb ? ` ${v.mb}MB` : ""}${v.streamed ? ` (${v.streamed} streamed)` : ""}`);
|
|
115
|
+
return `requests ${total}${rows.length ? `: ${rows.join(", ")}` : ""}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function describeGauges(gauges: Record<string, number | string>): string {
|
|
119
|
+
const rows = Object.entries(gauges).map(([k, v]) => `${k}=${v}`);
|
|
120
|
+
return rows.length ? `gauges ${rows.join(" ")}` : "";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function startMemoryWatchdog(role: string, readGauges: () => Record<string, number | string>): void {
|
|
124
|
+
const startedAt = Date.now();
|
|
125
|
+
const selfAlerted = new Set<number>();
|
|
126
|
+
const childAlerted = new Set<number>();
|
|
127
|
+
// The first summary comes a minute after start: the baseline every later line is read against.
|
|
128
|
+
let lastSummaryAt = Date.now() - SUMMARY_INTERVAL_MS + WATCH_INTERVAL_MS;
|
|
129
|
+
let lastSummaryPrivateMB: number | undefined;
|
|
130
|
+
let lastRequests = requestTracker.read();
|
|
131
|
+
|
|
132
|
+
spawnTracker.onLifecycle = (e) => {
|
|
133
|
+
const who = `${e.label} pid=${e.pid}${e.session ? ` session=${e.session}` : ""}`;
|
|
134
|
+
if (e.event === "start") log.info(`${who} started`);
|
|
135
|
+
else log.info(`${who} exited after ${e.ageMin}m${e.lastPrivateMB !== undefined ? `, held ${e.lastPrivateMB}MB at its last reading` : ""}`);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const tick = async () => {
|
|
139
|
+
try {
|
|
140
|
+
const native = await readNativeMemory();
|
|
141
|
+
const tree = await summarizeDescendants(process.pid, 5);
|
|
142
|
+
const self = native.privateMB;
|
|
143
|
+
const crossedSelf = self === undefined ? [] : SELF_ALERT_MB.filter((t) => self >= t && !selfAlerted.has(t));
|
|
144
|
+
const crossedChildren = role === "server" && tree
|
|
145
|
+
? CHILDREN_ALERT_MB.filter((t) => tree.privateMB >= t && !childAlerted.has(t))
|
|
146
|
+
: [];
|
|
147
|
+
const summaryDue = Date.now() - lastSummaryAt >= SUMMARY_INTERVAL_MS;
|
|
148
|
+
if (!crossedSelf.length && !crossedChildren.length && !summaryDue) return;
|
|
149
|
+
|
|
150
|
+
const heapMB = Math.round(heapStats().heapSize / MB);
|
|
151
|
+
const rssMB = Math.round(process.memoryUsage().rss / MB);
|
|
152
|
+
const upMin = Math.round((Date.now() - startedAt) / 60_000);
|
|
153
|
+
const head = `${role} pid=${process.pid} up=${upMin}m private=${self ?? "?"}MB heap=${heapMB}MB rss=${rssMB}MB handles=${native.handles ?? "?"}`;
|
|
154
|
+
const tail = [describeTree(tree), describeGauges(readGauges())].filter(Boolean).join(" | ");
|
|
155
|
+
|
|
156
|
+
// One line per crossing, naming the highest threshold passed since the last tick.
|
|
157
|
+
if (crossedSelf.length) {
|
|
158
|
+
for (const t of crossedSelf) selfAlerted.add(t);
|
|
159
|
+
log.warn(`${role} private bytes crossed ${Math.max(...crossedSelf)}MB (heap ${heapMB}MB, so ~${Math.max(0, (self ?? 0) - heapMB)}MB is native) — ${head} | ${tail}`);
|
|
160
|
+
}
|
|
161
|
+
if (crossedChildren.length) {
|
|
162
|
+
for (const t of crossedChildren) childAlerted.add(t);
|
|
163
|
+
log.warn(`${role} children crossed ${Math.max(...crossedChildren)}MB together — ${head} | ${tail}`);
|
|
164
|
+
}
|
|
165
|
+
if (!summaryDue) return;
|
|
166
|
+
|
|
167
|
+
const now = requestTracker.read();
|
|
168
|
+
const requests = diffRequestCounts(lastRequests, now, 6);
|
|
169
|
+
lastRequests = now;
|
|
170
|
+
const minutes = Math.round((Date.now() - lastSummaryAt) / 60_000);
|
|
171
|
+
const growth = self !== undefined && lastSummaryPrivateMB !== undefined ? self - lastSummaryPrivateMB : undefined;
|
|
172
|
+
lastSummaryAt = Date.now();
|
|
173
|
+
if (self !== undefined) lastSummaryPrivateMB = self;
|
|
174
|
+
const growthText = growth === undefined ? "" : ` (${growth >= 0 ? "+" : ""}${growth}MB in ${minutes}m)`;
|
|
175
|
+
const reqText = requests.total ? ` | ${describeRequests(requests.top, requests.total)}` : "";
|
|
176
|
+
const line = `summary ${head}${growthText} | ${tail}${reqText}`;
|
|
177
|
+
if (growth !== undefined && growth >= FAST_GROWTH_MB) log.warn(`${line} — private bytes grew fast`);
|
|
178
|
+
else log.info(line);
|
|
179
|
+
} catch (e) {
|
|
180
|
+
log.debug(`watchdog tick failed: ${(e as Error).message}`);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
setInterval(() => { void tick(); }, WATCH_INTERVAL_MS).unref?.();
|
|
185
|
+
}
|
|
@@ -30,6 +30,7 @@ import type { ChatEvent } from "../types/chat.ts";
|
|
|
30
30
|
import { createAgentTranscriptLineParser } from "./subagent-transcript-merger.ts";
|
|
31
31
|
import { getCachedSubagentGroups } from "./agent-transcript/agent-transcript-index-cache.ts";
|
|
32
32
|
import { createLogger } from "./logger.ts";
|
|
33
|
+
import { registerMemoryGauge } from "./memory-diagnostics.ts";
|
|
33
34
|
|
|
34
35
|
const log = createLogger("nested-spy");
|
|
35
36
|
|
|
@@ -207,3 +208,5 @@ function stopAllForSession(sessionId: string): void {
|
|
|
207
208
|
}
|
|
208
209
|
|
|
209
210
|
export const nestedSubagentSpy = { startSpy, stopSpy, stopAllForSession };
|
|
211
|
+
|
|
212
|
+
registerMemoryGauge("nestedSubagentSpy.sessions", () => sessions.size);
|
|
@@ -109,7 +109,12 @@ const STATUS_INFO_LENGTH_MISMATCH = 0xc0000004 | 0;
|
|
|
109
109
|
/** Grows on a length mismatch; a busy machine needs ~1-2 MB. */
|
|
110
110
|
let bufferSize = 2 * 1024 * 1024;
|
|
111
111
|
|
|
112
|
-
|
|
112
|
+
/**
|
|
113
|
+
* One `SystemProcessInformation` reading of every process on the machine, or null when it
|
|
114
|
+
* cannot be taken. `base` is the buffer's address: the image names inside it are pointers
|
|
115
|
+
* into the same buffer, so `pointer - base` is their offset in `view`.
|
|
116
|
+
*/
|
|
117
|
+
export async function querySystemProcessInformation(): Promise<{ view: DataView; base: bigint } | null> {
|
|
113
118
|
if (ntdll === undefined) {
|
|
114
119
|
try {
|
|
115
120
|
const { dlopen, FFIType } = await import("bun:ffi");
|
|
@@ -131,11 +136,16 @@ async function readWindows(pid: number): Promise<ThreadSnapshot | null> {
|
|
|
131
136
|
continue;
|
|
132
137
|
}
|
|
133
138
|
if (status !== 0) return null;
|
|
134
|
-
return
|
|
139
|
+
return { view: new DataView(buf.buffer), base: BigInt(ptr(buf)) };
|
|
135
140
|
}
|
|
136
141
|
return null;
|
|
137
142
|
}
|
|
138
143
|
|
|
144
|
+
async function readWindows(pid: number): Promise<ThreadSnapshot | null> {
|
|
145
|
+
const info = await querySystemProcessInformation();
|
|
146
|
+
return info ? parseWindowsProcessInformation(info.view, pid) : null;
|
|
147
|
+
}
|
|
148
|
+
|
|
139
149
|
const LINUX_STATES: Record<string, string> = {
|
|
140
150
|
R: "running", S: "sleeping", D: "disk-wait", T: "stopped", t: "traced", Z: "zombie", X: "dead",
|
|
141
151
|
I: "idle", W: "paging", P: "parked",
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How much memory every process below this one holds, read from the OS.
|
|
3
|
+
*
|
|
4
|
+
* The monitor's own numbers cover one process, and on 26/10/2026 that was the smaller half:
|
|
5
|
+
* the server held 7 GB while its 17 Claude subprocesses held 11 GB between them, and nothing
|
|
6
|
+
* PPM logged could say so. The spawn tracker only sees direct children, and only the ones
|
|
7
|
+
* started after it was installed; the OS sees the whole tree, grandchildren included (an MCP
|
|
8
|
+
* server under a Claude subprocess, a test runner under a shell).
|
|
9
|
+
*
|
|
10
|
+
* Windows reads every process with one `NtQuerySystemInformation` call (the same one
|
|
11
|
+
* `process-thread-snapshot.ts` makes); Linux walks `/proc`. Other platforms answer null.
|
|
12
|
+
* Nothing here throws.
|
|
13
|
+
*/
|
|
14
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
15
|
+
import { querySystemProcessInformation } from "./process-thread-snapshot.ts";
|
|
16
|
+
|
|
17
|
+
export interface ProcessMemoryEntry {
|
|
18
|
+
pid: number;
|
|
19
|
+
ppid: number;
|
|
20
|
+
/** Image name, lowercased, executable extension dropped. */
|
|
21
|
+
name: string;
|
|
22
|
+
privateMB: number;
|
|
23
|
+
/** When the process started, ms since the epoch on Windows and since boot on Linux: only compared, never shown. */
|
|
24
|
+
startedAt: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const MB = 1024 * 1024;
|
|
28
|
+
|
|
29
|
+
// SYSTEM_PROCESS_INFORMATION on 64-bit Windows.
|
|
30
|
+
const P_NEXT = 0x00;
|
|
31
|
+
const P_CREATE_TIME = 0x20;
|
|
32
|
+
const P_IMAGE_NAME_LENGTH = 0x38;
|
|
33
|
+
const P_IMAGE_NAME_BUFFER = 0x40;
|
|
34
|
+
const P_PID = 0x50;
|
|
35
|
+
const P_PARENT_PID = 0x58;
|
|
36
|
+
const P_PRIVATE_BYTES = 0xc8;
|
|
37
|
+
const P_SIZE = 0x100;
|
|
38
|
+
/** 100 ns ticks between 1601-01-01 and 1970-01-01. */
|
|
39
|
+
const EPOCH_TICKS = 116_444_736_000_000_000n;
|
|
40
|
+
|
|
41
|
+
export function processName(raw: string): string {
|
|
42
|
+
return raw.toLowerCase().replace(/\.(exe|cmd|bat|com)$/, "").slice(0, 40) || "unknown";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Every process in a `SystemProcessInformation` buffer. Exported for tests. */
|
|
46
|
+
export function parseWindowsProcessList(view: DataView, base: bigint): ProcessMemoryEntry[] {
|
|
47
|
+
const out: ProcessMemoryEntry[] = [];
|
|
48
|
+
let at = 0;
|
|
49
|
+
for (;;) {
|
|
50
|
+
if (at + P_SIZE > view.byteLength) break;
|
|
51
|
+
const pid = Number(view.getBigUint64(at + P_PID, true));
|
|
52
|
+
let name = pid === 0 ? "idle" : "system";
|
|
53
|
+
const nameBytes = view.getUint16(at + P_IMAGE_NAME_LENGTH, true);
|
|
54
|
+
const namePtr = view.getBigUint64(at + P_IMAGE_NAME_BUFFER, true);
|
|
55
|
+
const offset = Number(namePtr - base);
|
|
56
|
+
if (nameBytes > 0 && offset >= 0 && offset + nameBytes <= view.byteLength) {
|
|
57
|
+
name = new TextDecoder("utf-16le").decode(new Uint8Array(view.buffer, view.byteOffset + offset, nameBytes));
|
|
58
|
+
}
|
|
59
|
+
const created = view.getBigInt64(at + P_CREATE_TIME, true);
|
|
60
|
+
out.push({
|
|
61
|
+
pid,
|
|
62
|
+
ppid: Number(view.getBigUint64(at + P_PARENT_PID, true)),
|
|
63
|
+
name: processName(name),
|
|
64
|
+
privateMB: Math.round(Number(view.getBigUint64(at + P_PRIVATE_BYTES, true)) / MB),
|
|
65
|
+
startedAt: created > EPOCH_TICKS ? Number((created - EPOCH_TICKS) / 10_000n) : 0,
|
|
66
|
+
});
|
|
67
|
+
const next = view.getUint32(at + P_NEXT, true);
|
|
68
|
+
if (next === 0) break;
|
|
69
|
+
at += next;
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** `/proc/<pid>/stat` → ppid, name and start time (field 22, clock ticks since boot). Exported for tests. */
|
|
75
|
+
export function parseLinuxStatLine(text: string): { ppid: number; name: string; startedAt: number } | null {
|
|
76
|
+
const open = text.indexOf("(");
|
|
77
|
+
const close = text.lastIndexOf(")");
|
|
78
|
+
if (open === -1 || close === -1) return null;
|
|
79
|
+
const f = text.slice(close + 2).trim().split(/\s+/);
|
|
80
|
+
const ppid = Number(f[4 - 3]);
|
|
81
|
+
const startedAt = Number(f[22 - 3]);
|
|
82
|
+
if (!Number.isFinite(ppid)) return null;
|
|
83
|
+
return { ppid, name: processName(text.slice(open + 1, close)), startedAt: Number.isFinite(startedAt) ? startedAt : 0 };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function linuxPrivateMB(pid: number): number {
|
|
87
|
+
try {
|
|
88
|
+
const status = readFileSync(`/proc/${pid}/status`, "utf8");
|
|
89
|
+
const kb = (key: string) => Number(status.match(new RegExp(`^${key}:\\s+(\\d+)`, "m"))?.[1] ?? 0);
|
|
90
|
+
return Math.round((kb("RssAnon") + kb("VmSwap")) / 1024);
|
|
91
|
+
} catch {
|
|
92
|
+
return 0;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function readLinuxList(rootPid: number): ProcessMemoryEntry[] {
|
|
97
|
+
const all: ProcessMemoryEntry[] = [];
|
|
98
|
+
for (const entry of readdirSync("/proc")) {
|
|
99
|
+
const pid = Number(entry);
|
|
100
|
+
if (!Number.isInteger(pid)) continue;
|
|
101
|
+
try {
|
|
102
|
+
const stat = parseLinuxStatLine(readFileSync(`/proc/${pid}/stat`, "utf8"));
|
|
103
|
+
if (stat) all.push({ pid, ...stat, privateMB: 0 });
|
|
104
|
+
} catch { /* exited while walking */ }
|
|
105
|
+
}
|
|
106
|
+
// Memory only for the tree that is reported: one status read per descendant, not per process.
|
|
107
|
+
const tree = new Set(descendantsOf(rootPid, all).map((d) => d.pid));
|
|
108
|
+
for (const p of all) if (tree.has(p.pid) || p.pid === rootPid) p.privateMB = linuxPrivateMB(p.pid);
|
|
109
|
+
return all;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Every process the OS lists, or null when this platform cannot be read. */
|
|
113
|
+
export async function readProcessList(rootPid = process.pid, platform: NodeJS.Platform = process.platform): Promise<ProcessMemoryEntry[] | null> {
|
|
114
|
+
try {
|
|
115
|
+
if (platform === "win32") {
|
|
116
|
+
const info = await querySystemProcessInformation();
|
|
117
|
+
return info ? parseWindowsProcessList(info.view, info.base) : null;
|
|
118
|
+
}
|
|
119
|
+
if (platform === "linux") return readLinuxList(rootPid);
|
|
120
|
+
} catch { /* fall through */ }
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface Descendant extends ProcessMemoryEntry {
|
|
125
|
+
depth: number;
|
|
126
|
+
/** The direct child of the root this process descends from (itself, at depth 1). */
|
|
127
|
+
branchPid: number;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Every process below `rootPid`. A process counts as a child only if it started after its
|
|
132
|
+
* parent: Windows reuses pids, so a ppid can name an unrelated process that took a dead
|
|
133
|
+
* parent's number, and without the check that process's whole tree would be claimed.
|
|
134
|
+
*/
|
|
135
|
+
export function descendantsOf(rootPid: number, list: ProcessMemoryEntry[]): Descendant[] {
|
|
136
|
+
const byParent = new Map<number, ProcessMemoryEntry[]>();
|
|
137
|
+
const byPid = new Map<number, ProcessMemoryEntry>();
|
|
138
|
+
for (const p of list) {
|
|
139
|
+
byPid.set(p.pid, p);
|
|
140
|
+
if (p.pid === p.ppid) continue;
|
|
141
|
+
const siblings = byParent.get(p.ppid);
|
|
142
|
+
if (siblings) siblings.push(p); else byParent.set(p.ppid, [p]);
|
|
143
|
+
}
|
|
144
|
+
const out: Descendant[] = [];
|
|
145
|
+
const queue: Array<{ p: ProcessMemoryEntry; depth: number; branchPid: number }> = [];
|
|
146
|
+
const root = byPid.get(rootPid);
|
|
147
|
+
for (const c of byParent.get(rootPid) ?? []) {
|
|
148
|
+
if (!root || c.startedAt >= root.startedAt) queue.push({ p: c, depth: 1, branchPid: c.pid });
|
|
149
|
+
}
|
|
150
|
+
const seen = new Set<number>([rootPid]);
|
|
151
|
+
while (queue.length) {
|
|
152
|
+
const { p, depth, branchPid } = queue.shift()!;
|
|
153
|
+
if (seen.has(p.pid)) continue;
|
|
154
|
+
seen.add(p.pid);
|
|
155
|
+
out.push({ ...p, depth, branchPid });
|
|
156
|
+
for (const c of byParent.get(p.pid) ?? []) {
|
|
157
|
+
if (c.startedAt >= p.startedAt) queue.push({ p: c, depth: depth + 1, branchPid });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* compaction scan — and having the second import it from the first would make the
|
|
6
6
|
* two files a cycle.
|
|
7
7
|
*/
|
|
8
|
+
import { readFileChunks } from "./file-chunks.ts";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Yield a file's lines without holding the file in memory.
|
|
@@ -19,47 +20,35 @@
|
|
|
19
20
|
*
|
|
20
21
|
* `toByte` ends the read there, exclusive. It is meant to be a line start (the
|
|
21
22
|
* record that begins there is not read) or omitted for the end of the file —
|
|
22
|
-
* `compactSegmentWindow` in `compact-segment.ts` is what computes one.
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* `compactSegmentWindow` in `compact-segment.ts` is what computes one.
|
|
24
|
+
*
|
|
25
|
+
* The bytes come from `readFileChunks`, not `Bun.file().stream()`: that stream
|
|
26
|
+
* keeps about the file's size in native memory per full read and never returns
|
|
27
|
+
* it (see `file-chunks.ts`), and its sliced form never finishes when it has to
|
|
28
|
+
* stop well short of EOF (Bun 1.3.11: on a 4MB file `slice(0, 259).stream()`
|
|
29
|
+
* hung).
|
|
26
30
|
*/
|
|
27
31
|
export async function* readLines(filePath: string, fromByte = 0, toByte?: number): AsyncGenerator<string> {
|
|
28
32
|
const decoder = new TextDecoder();
|
|
29
|
-
// Explicit reader rather than `for await` over the stream: the DOM lib's
|
|
30
|
-
// ReadableStream is not typed as async-iterable, and the `finally` is what
|
|
31
|
-
// releases it when a caller breaks early on `beforeUuid`.
|
|
32
33
|
// One byte *before* the offset, so the first line the split produces is
|
|
33
34
|
// always the tail of the record the offset fell inside — the empty string
|
|
34
35
|
// when it fell exactly on a newline. That makes the drop below unconditional
|
|
35
36
|
// instead of a boundary case, and a boundary case here silently costs a whole
|
|
36
37
|
// record rather than a fragment.
|
|
37
|
-
const file = fromByte > 0 ? Bun.file(filePath).slice(fromByte - 1) : Bun.file(filePath);
|
|
38
|
-
const reader = file.stream().getReader();
|
|
39
38
|
// A fragment is not always unparseable — the tail of one record is sometimes
|
|
40
39
|
// a complete smaller document — so it has to go by position rather than be
|
|
41
40
|
// left for `JSON.parse` to reject, or it enters as an invented message.
|
|
42
41
|
let dropPartial = fromByte > 0;
|
|
43
|
-
let remaining = toByte === undefined ? Infinity : toByte - Math.max(0, fromByte - 1);
|
|
44
42
|
let buffered = "";
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
buffered = lines.pop() ?? "";
|
|
54
|
-
for (const line of lines) {
|
|
55
|
-
if (dropPartial) { dropPartial = false; continue; }
|
|
56
|
-
yield line;
|
|
57
|
-
}
|
|
43
|
+
// A caller that stops early (`beforeUuid`) ends this loop, which closes the file.
|
|
44
|
+
for await (const chunk of readFileChunks(filePath, { fromByte: Math.max(0, fromByte - 1), toByte })) {
|
|
45
|
+
buffered += decoder.decode(chunk, { stream: true });
|
|
46
|
+
const lines = buffered.split("\n");
|
|
47
|
+
buffered = lines.pop() ?? "";
|
|
48
|
+
for (const line of lines) {
|
|
49
|
+
if (dropPartial) { dropPartial = false; continue; }
|
|
50
|
+
yield line;
|
|
58
51
|
}
|
|
59
|
-
} finally {
|
|
60
|
-
// `cancel`, not only `releaseLock`: a stream let go of before its end keeps
|
|
61
|
-
// the file open — 2000 early stops left 2000 descriptors behind.
|
|
62
|
-
await reader.cancel().catch(() => {});
|
|
63
52
|
}
|
|
64
53
|
buffered += decoder.decode();
|
|
65
54
|
if (buffered && !dropPartial) yield buffered;
|
|
@@ -2062,6 +2062,7 @@ export async function runSupervisor(opts: {
|
|
|
2062
2062
|
log("INFO", `Supervisor started (PID: ${process.pid}, port: ${opts.port}, share: ${opts.share})`);
|
|
2063
2063
|
// Before any child is spawned: every child inherits these, the server included.
|
|
2064
2064
|
await logPriorityRaise("supervisor", (m) => log("WARN", m));
|
|
2065
|
+
(await import("./memory-diagnostics.ts")).startMemoryDiagnostics({ role: "supervisor" });
|
|
2065
2066
|
|
|
2066
2067
|
// ── Systemd self-heal: if the unit file is stale (e.g. still has
|
|
2067
2068
|
// Restart=on-failure), we were likely spawned by the old Bun.spawn()
|
|
@@ -2,6 +2,7 @@ import type { Subprocess, Terminal as BunTerminal } from "bun";
|
|
|
2
2
|
import { isUsableTerminalSize } from "../shared/terminal-size";
|
|
3
3
|
import { withoutAiChatMark } from "./ai-chat-env.ts";
|
|
4
4
|
import { createLogger } from "./logger.ts";
|
|
5
|
+
import { registerMemoryGauge } from "./memory-diagnostics.ts";
|
|
5
6
|
|
|
6
7
|
/** PTY lifecycle only — never what is typed into a terminal or what it prints. */
|
|
7
8
|
const log = createLogger("terminal");
|
|
@@ -397,3 +398,10 @@ export class TerminalService {
|
|
|
397
398
|
}
|
|
398
399
|
|
|
399
400
|
export const terminalService = new TerminalService();
|
|
401
|
+
|
|
402
|
+
registerMemoryGauge("terminal.sessions", () => terminalService["sessions"].size);
|
|
403
|
+
registerMemoryGauge("terminal.bufferedKB", () => {
|
|
404
|
+
let chars = 0;
|
|
405
|
+
for (const b of terminalService["outputBuffers"].values()) chars += b.length;
|
|
406
|
+
return Math.round(chars / 1024);
|
|
407
|
+
});
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type StripMode,
|
|
8
8
|
type TranscriptImageAudit,
|
|
9
9
|
} from "./transcript-images.ts";
|
|
10
|
+
import { readFileChunks } from "./file-chunks.ts";
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* File-level image auditing and removal that never holds a transcript in memory.
|
|
@@ -50,19 +51,14 @@ async function forEachLine(
|
|
|
50
51
|
path: string,
|
|
51
52
|
onLine: (line: string, terminated: boolean) => void,
|
|
52
53
|
): Promise<void> {
|
|
53
|
-
const reader = Bun.file(path).stream().getReader();
|
|
54
54
|
const decoder = new TextDecoder();
|
|
55
55
|
let carry = "";
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
for (const line of parts) onLine(line, true);
|
|
63
|
-
}
|
|
64
|
-
} finally {
|
|
65
|
-
reader.releaseLock();
|
|
56
|
+
// Not `Bun.file().stream()`, which keeps about the file's size in native memory per read
|
|
57
|
+
// (see `file-chunks.ts`) — and this runs over every transcript a cleanup visits.
|
|
58
|
+
for await (const value of readFileChunks(path)) {
|
|
59
|
+
const parts = (carry + decoder.decode(value, { stream: true })).split("\n");
|
|
60
|
+
carry = parts.pop() ?? "";
|
|
61
|
+
for (const line of parts) onLine(line, true);
|
|
66
62
|
}
|
|
67
63
|
if (carry) onLine(carry, false);
|
|
68
64
|
}
|
|
@@ -134,14 +134,20 @@ function useQuestionKeyboard(config: {
|
|
|
134
134
|
if (el) {
|
|
135
135
|
el.addEventListener("keydown", handleKeyDown);
|
|
136
136
|
el.setAttribute("tabindex", "0");
|
|
137
|
-
// Same reason: asking the main document whether focus is already inside the card answers
|
|
138
|
-
// "no" for a card living in another document, and this effect re-runs on every render —
|
|
139
|
-
// which would yank focus out of the custom input after each keystroke.
|
|
140
|
-
if (config.autoFocus && !el.contains(el.ownerDocument.activeElement)) el.focus();
|
|
141
137
|
}
|
|
142
138
|
return () => { el?.removeEventListener("keydown", handleKeyDown); };
|
|
143
139
|
}, [config, focusedOption]);
|
|
144
140
|
|
|
141
|
+
// Focus the card once, when it appears. Doing it on every render took focus back whenever the
|
|
142
|
+
// card re-rendered for any reason, so with two chats side by side each waiting on a question,
|
|
143
|
+
// the other card's chat updating pulled focus out of the card being answered. The owner
|
|
144
|
+
// document is asked rather than the global one, for a card rendered in a picture-in-picture window.
|
|
145
|
+
const autoFocusOnMount = useRef(config.autoFocus);
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
const el = containerRef.current;
|
|
148
|
+
if (autoFocusOnMount.current && el && !el.contains(el.ownerDocument.activeElement)) el.focus();
|
|
149
|
+
}, []);
|
|
150
|
+
|
|
145
151
|
return { focusedOption, setFocusedOption, containerRef };
|
|
146
152
|
}
|
|
147
153
|
|
|
@@ -144,6 +144,8 @@ function PushSection() {
|
|
|
144
144
|
const [subscription, setSubscription] = useState<PushSubscription | null>(null);
|
|
145
145
|
const [loadError, setLoadError] = useState<string | null>(null);
|
|
146
146
|
const [busy, setBusy] = useState(false);
|
|
147
|
+
const [enabling, setEnabling] = useState(false);
|
|
148
|
+
const [enableError, setEnableError] = useState<string | null>(null);
|
|
147
149
|
const [testing, setTesting] = useState<string | null>(null);
|
|
148
150
|
const support = pushSupport();
|
|
149
151
|
|
|
@@ -174,6 +176,8 @@ function PushSection() {
|
|
|
174
176
|
const toggle = async (on: boolean) => {
|
|
175
177
|
if (!status) return;
|
|
176
178
|
setBusy(true);
|
|
179
|
+
setEnabling(on);
|
|
180
|
+
setEnableError(null);
|
|
177
181
|
try {
|
|
178
182
|
if (on) {
|
|
179
183
|
await enablePush(status.publicKey);
|
|
@@ -182,9 +186,12 @@ function PushSection() {
|
|
|
182
186
|
await disablePush();
|
|
183
187
|
}
|
|
184
188
|
} catch (e) {
|
|
185
|
-
|
|
189
|
+
// Kept on screen rather than in a toast: the fix is usually a browser setting to go and change.
|
|
190
|
+
if (on) setEnableError(errorText(e));
|
|
191
|
+
else toast.error("Could not turn push off", { description: errorText(e) });
|
|
186
192
|
} finally {
|
|
187
193
|
setBusy(false);
|
|
194
|
+
setEnabling(false);
|
|
188
195
|
await load();
|
|
189
196
|
}
|
|
190
197
|
};
|
|
@@ -231,6 +238,12 @@ function PushSection() {
|
|
|
231
238
|
/>
|
|
232
239
|
</div>
|
|
233
240
|
{support.state !== "available" && <p className="text-xs leading-relaxed text-warning">{support.message}</p>}
|
|
241
|
+
{enabling && (
|
|
242
|
+
<p className="text-xs leading-relaxed text-muted-foreground">
|
|
243
|
+
Waiting for the browser's push service. Just after the browser starts, this can take up to a minute.
|
|
244
|
+
</p>
|
|
245
|
+
)}
|
|
246
|
+
{enableError && <p className="break-words text-xs leading-relaxed text-error">{enableError}</p>}
|
|
234
247
|
{loadError && <p className="text-xs text-error">{loadError}</p>}
|
|
235
248
|
|
|
236
249
|
{status && status.devices.length > 0 && (
|