@hienlh/ppm 0.23.21 → 0.23.23
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 +31 -0
- package/CLAUDE.md +6 -3
- package/README.md +1 -1
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/cli-reference.md +4 -4
- package/assets/skills/ppm/references/http-api.md +2 -1
- package/dist/web/assets/{accounts-settings-section-DD88DRdx.js → accounts-settings-section-Cg-LncY6.js} +1 -1
- package/dist/web/assets/accounts-settings-section-Cg-LncY6.js.br +0 -0
- package/dist/web/assets/accounts-settings-section-Cg-LncY6.js.gz +0 -0
- package/dist/web/assets/{agent-session-sheet-C0PhhLKR.js → agent-session-sheet-DMdV8GOu.js} +2 -2
- package/dist/web/assets/agent-session-sheet-DMdV8GOu.js.br +0 -0
- package/dist/web/assets/agent-session-sheet-DMdV8GOu.js.gz +0 -0
- package/dist/web/assets/{agent-session-window-content-D0XZgh4U.js → agent-session-window-content-CYg58J-V.js} +1 -1
- package/dist/web/assets/agent-session-window-content-CYg58J-V.js.br +0 -0
- package/dist/web/assets/agent-session-window-content-CYg58J-V.js.gz +0 -0
- package/dist/web/assets/{ai-resource-editor-CpfQB6A7.js → ai-resource-editor-BvEnn0Z_.js} +1 -1
- package/dist/web/assets/ai-resource-editor-BvEnn0Z_.js.br +0 -0
- package/dist/web/assets/ai-resource-editor-BvEnn0Z_.js.gz +0 -0
- package/dist/web/assets/{ai-settings-section-DqE67XNC.js → ai-settings-section-Bo-nf2rY.js} +1 -1
- package/dist/web/assets/ai-settings-section-DAqiu_11.js +1 -0
- package/dist/web/assets/ai-settings-section-DAqiu_11.js.br +0 -0
- package/dist/web/assets/ai-settings-section-DAqiu_11.js.gz +0 -0
- package/dist/web/assets/{android-tab-4Z4AgHz1.js → android-tab-C-wmlFc0.js} +1 -1
- package/dist/web/assets/android-tab-C-wmlFc0.js.br +0 -0
- package/dist/web/assets/android-tab-C-wmlFc0.js.gz +0 -0
- package/dist/web/assets/{appearance-settings-section-DjQjiJF7.js → appearance-settings-section-CYqX6BGh.js} +1 -1
- package/dist/web/assets/appearance-settings-section-CYqX6BGh.js.br +0 -0
- package/dist/web/assets/appearance-settings-section-CYqX6BGh.js.gz +0 -0
- package/dist/web/assets/{apps-panel-_lxY9xbc.js → apps-panel-D1VTBxgC.js} +1 -1
- package/dist/web/assets/apps-panel-D1VTBxgC.js.br +0 -0
- package/dist/web/assets/apps-panel-D1VTBxgC.js.gz +0 -0
- package/dist/web/assets/{branch-review-tab-BzrhnFSs.js → branch-review-tab-BG6vxSsC.js} +1 -1
- package/dist/web/assets/branch-review-tab-BG6vxSsC.js.br +0 -0
- package/dist/web/assets/branch-review-tab-BG6vxSsC.js.gz +0 -0
- package/dist/web/assets/chat-tab-BAwGLGyR.js +19 -0
- package/dist/web/assets/chat-tab-BAwGLGyR.js.br +0 -0
- package/dist/web/assets/chat-tab-BAwGLGyR.js.gz +0 -0
- package/dist/web/assets/{chat-tab-BKHepkYi.js → chat-tab-BJ0Mi2sd.js} +1 -1
- package/dist/web/assets/chat-tab-BJ0Mi2sd.js.br +0 -0
- package/dist/web/assets/chat-tab-BJ0Mi2sd.js.gz +0 -0
- package/dist/web/assets/{code-editor-COWmNXO4.js → code-editor-DuwbwTpM.js} +4 -4
- package/dist/web/assets/code-editor-DuwbwTpM.js.br +0 -0
- package/dist/web/assets/code-editor-DuwbwTpM.js.gz +0 -0
- package/dist/web/assets/{codex-reset-credit-button-BHt4yJet.js → codex-reset-credit-button-tIwfnf2G.js} +1 -1
- package/dist/web/assets/codex-reset-credit-button-tIwfnf2G.js.br +0 -0
- package/dist/web/assets/codex-reset-credit-button-tIwfnf2G.js.gz +0 -0
- package/dist/web/assets/{conflict-editor-tj-1gzKr.js → conflict-editor-D2n-hQqs.js} +1 -1
- package/dist/web/assets/conflict-editor-D2n-hQqs.js.br +0 -0
- package/dist/web/assets/conflict-editor-D2n-hQqs.js.gz +0 -0
- package/dist/web/assets/{connection-form-tab-BV2QX8Cf.js → connection-form-tab-DYqPNq_-.js} +1 -1
- package/dist/web/assets/connection-form-tab-DYqPNq_-.js.br +0 -0
- package/dist/web/assets/connection-form-tab-DYqPNq_-.js.gz +0 -0
- package/dist/web/assets/{csv-preview-xhE2esgz.js → csv-preview-Xh3Z6BS5.js} +1 -1
- package/dist/web/assets/csv-preview-Xh3Z6BS5.js.br +0 -0
- package/dist/web/assets/csv-preview-Xh3Z6BS5.js.gz +0 -0
- package/dist/web/assets/{db-tab-parts-C3MsjOID.js → db-tab-parts-Dqnb3GCP.js} +1 -1
- package/dist/web/assets/db-tab-parts-Dqnb3GCP.js.br +0 -0
- package/dist/web/assets/db-tab-parts-Dqnb3GCP.js.gz +0 -0
- package/dist/web/assets/{design-settings-section-C3LxYhIw.js → design-settings-section-DNNWgxF9.js} +1 -1
- package/dist/web/assets/design-settings-section-DNNWgxF9.js.br +0 -0
- package/dist/web/assets/design-settings-section-DNNWgxF9.js.gz +0 -0
- package/dist/web/assets/{design-tab-DYcvjdfb.js → design-tab-BoAdSpZZ.js} +1 -1
- package/dist/web/assets/design-tab-BoAdSpZZ.js.br +0 -0
- package/dist/web/assets/design-tab-BoAdSpZZ.js.gz +0 -0
- package/dist/web/assets/{designs-sidebar-panel-Dpr4PPFT.js → designs-sidebar-panel-BKw7HFAz.js} +1 -1
- package/dist/web/assets/designs-sidebar-panel-BKw7HFAz.js.br +0 -0
- package/dist/web/assets/designs-sidebar-panel-BKw7HFAz.js.gz +0 -0
- package/dist/web/assets/{diff-viewer-Fl_4jqr6.js → diff-viewer-CRmfCAZX.js} +1 -1
- package/dist/web/assets/diff-viewer-CRmfCAZX.js.br +0 -0
- package/dist/web/assets/diff-viewer-CRmfCAZX.js.gz +0 -0
- package/dist/web/assets/{docx-preview-bVE9shPX.js → docx-preview-zIrtL09u.js} +1 -1
- package/dist/web/assets/docx-preview-zIrtL09u.js.br +0 -0
- package/dist/web/assets/docx-preview-zIrtL09u.js.gz +0 -0
- package/dist/web/assets/{editor-dialog-eEOYWdbr.js → editor-dialog-jMYvXVla.js} +1 -1
- package/dist/web/assets/editor-dialog-jMYvXVla.js.br +0 -0
- package/dist/web/assets/editor-dialog-jMYvXVla.js.gz +0 -0
- package/dist/web/assets/{editor-language-service-D1_Ggq4s.js → editor-language-service-Yb-61Bro.js} +1 -1
- package/dist/web/assets/editor-language-service-Yb-61Bro.js.br +0 -0
- package/dist/web/assets/editor-language-service-Yb-61Bro.js.gz +0 -0
- package/dist/web/assets/{explorer-body-C0UOcBPJ.js → explorer-body-C6qKehmd.js} +1 -1
- package/dist/web/assets/explorer-body-C6qKehmd.js.br +0 -0
- package/dist/web/assets/explorer-body-C6qKehmd.js.gz +0 -0
- package/dist/web/assets/{explorer-window-content-Dxee7SuJ.js → explorer-window-content-B9P8grrY.js} +1 -1
- package/dist/web/assets/explorer-window-content-B9P8grrY.js.br +0 -0
- package/dist/web/assets/explorer-window-content-B9P8grrY.js.gz +0 -0
- package/dist/web/assets/{extension-manager-section-D8krnoj8.js → extension-manager-section-C45v1QtG.js} +1 -1
- package/dist/web/assets/extension-manager-section-C45v1QtG.js.br +0 -0
- package/dist/web/assets/extension-manager-section-C45v1QtG.js.gz +0 -0
- package/dist/web/assets/{extension-webview-CtIdV69T.js → extension-webview-DIxM-xkH.js} +1 -1
- package/dist/web/assets/extension-webview-DIxM-xkH.js.br +0 -0
- package/dist/web/assets/extension-webview-DIxM-xkH.js.gz +0 -0
- package/dist/web/assets/{files-settings-section-C8vtarR8.js → files-settings-section-DeVlzKXd.js} +1 -1
- package/dist/web/assets/files-settings-section-DeVlzKXd.js.br +0 -0
- package/dist/web/assets/files-settings-section-DeVlzKXd.js.gz +0 -0
- package/dist/web/assets/format-date-CkyHzZLA.js +1 -0
- package/dist/web/assets/{general-settings-section-Chu8Ut8h.js → general-settings-section-CLVDIEym.js} +1 -1
- package/dist/web/assets/general-settings-section-CLVDIEym.js.br +0 -0
- package/dist/web/assets/general-settings-section-CLVDIEym.js.gz +0 -0
- package/dist/web/assets/{git-log-panel-DG09p6k5.js → git-log-panel-D3Q9NRll.js} +1 -1
- package/dist/web/assets/git-log-panel-D3Q9NRll.js.br +0 -0
- package/dist/web/assets/git-log-panel-D3Q9NRll.js.gz +0 -0
- package/dist/web/assets/{git-review-tab-CSy8DjSC.js → git-review-tab-ORgiYbva.js} +1 -1
- package/dist/web/assets/git-review-tab-ORgiYbva.js.br +0 -0
- package/dist/web/assets/git-review-tab-ORgiYbva.js.gz +0 -0
- package/dist/web/assets/{glide-data-grid-BN1y4BT0.js → glide-data-grid-CtWl1BTk.js} +1 -1
- package/dist/web/assets/glide-data-grid-CtWl1BTk.js.br +0 -0
- package/dist/web/assets/glide-data-grid-CtWl1BTk.js.gz +0 -0
- package/dist/web/assets/{grid-save-dialog-CfSuWw2t.js → grid-save-dialog-eV25Mqpg.js} +1 -1
- package/dist/web/assets/grid-save-dialog-eV25Mqpg.js.br +0 -0
- package/dist/web/assets/grid-save-dialog-eV25Mqpg.js.gz +0 -0
- package/dist/web/assets/{group-chat-tab-B04QMp7A.js → group-chat-tab-CYmbYXN2.js} +1 -1
- package/dist/web/assets/group-chat-tab-CYmbYXN2.js.br +0 -0
- package/dist/web/assets/group-chat-tab-CYmbYXN2.js.gz +0 -0
- package/dist/web/assets/{impexp-tab-BRNouquR.js → impexp-tab-D2OKxpFq.js} +1 -1
- package/dist/web/assets/impexp-tab-D2OKxpFq.js.br +0 -0
- package/dist/web/assets/impexp-tab-D2OKxpFq.js.gz +0 -0
- package/dist/web/assets/{index-BpgSIM5B.js → index-D8QDpdgs.js} +7 -7
- package/dist/web/assets/index-D8QDpdgs.js.br +0 -0
- package/dist/web/assets/index-D8QDpdgs.js.gz +0 -0
- package/dist/web/assets/{jira-watcher-section-CJeS5r6D.js → jira-watcher-section-B3OaMlim.js} +1 -1
- package/dist/web/assets/{keybindings-store-Dvxq-vZD.js → keybindings-store-C_aRhBu4.js} +1 -1
- package/dist/web/assets/{keyboard-shortcuts-section-BKDyZHhK.js → keyboard-shortcuts-section-DXRyKrSy.js} +1 -1
- package/dist/web/assets/keyboard-shortcuts-section-DXRyKrSy.js.br +0 -0
- package/dist/web/assets/keyboard-shortcuts-section-DXRyKrSy.js.gz +0 -0
- package/dist/web/assets/{language-servers-section-CbbkezhA.js → language-servers-section-BTs9iuZc.js} +2 -2
- package/dist/web/assets/language-servers-section-BTs9iuZc.js.br +0 -0
- package/dist/web/assets/language-servers-section-BTs9iuZc.js.gz +0 -0
- package/dist/web/assets/{logs-app-Blr1-v4q.js → logs-app-BlhVTOYN.js} +1 -1
- package/dist/web/assets/logs-app-BlhVTOYN.js.br +0 -0
- package/dist/web/assets/logs-app-BlhVTOYN.js.gz +0 -0
- package/dist/web/assets/{logs-tab-CGPtn2tU.js → logs-tab-kqNARbz0.js} +1 -1
- package/dist/web/assets/{logs-window-content-DAczcNmh.js → logs-window-content-UUCJJCqG.js} +1 -1
- package/dist/web/assets/{lsp-client-Ds3U2O52.js → lsp-client-BTlK_8lb.js} +1 -1
- package/dist/web/assets/lsp-client-BTlK_8lb.js.br +0 -0
- package/dist/web/assets/lsp-client-BTlK_8lb.js.gz +0 -0
- package/dist/web/assets/{markdown-renderer-DbJVwfUH.js → markdown-renderer-ClGxDien.js} +1 -1
- package/dist/web/assets/{markdown-renderer-DzCPadpB.js → markdown-renderer-DpsToFFB.js} +1 -1
- package/dist/web/assets/markdown-renderer-DpsToFFB.js.br +0 -0
- package/dist/web/assets/markdown-renderer-DpsToFFB.js.gz +0 -0
- package/dist/web/assets/{mobile-explorer-sheet-BClCMXb8.js → mobile-explorer-sheet-CybJbScN.js} +1 -1
- package/dist/web/assets/mobile-explorer-sheet-CybJbScN.js.br +0 -0
- package/dist/web/assets/mobile-explorer-sheet-CybJbScN.js.gz +0 -0
- package/dist/web/assets/{new-design-dialog-HRP9H11v.js → new-design-dialog-9WTgcJoj.js} +1 -1
- package/dist/web/assets/new-design-dialog-9WTgcJoj.js.br +0 -0
- package/dist/web/assets/new-design-dialog-9WTgcJoj.js.gz +0 -0
- package/dist/web/assets/{notification-store-BomzYNQj.js → notification-store-CPVq5ec5.js} +1 -1
- package/dist/web/assets/notifications-settings-section-xrYII-pL.js +1 -0
- package/dist/web/assets/notifications-settings-section-xrYII-pL.js.br +0 -0
- package/dist/web/assets/notifications-settings-section-xrYII-pL.js.gz +0 -0
- package/dist/web/assets/{ppmbot-settings-section-BRowIIqc.js → ppmbot-settings-section-Bc46iDP9.js} +1 -1
- package/dist/web/assets/ppmbot-settings-section-Bc46iDP9.js.br +0 -0
- package/dist/web/assets/ppmbot-settings-section-Bc46iDP9.js.gz +0 -0
- package/dist/web/assets/{problems-panel-DdB10Ybd.js → problems-panel-D49-Xkzd.js} +1 -1
- package/dist/web/assets/problems-panel-D49-Xkzd.js.br +0 -0
- package/dist/web/assets/problems-panel-D49-Xkzd.js.gz +0 -0
- package/dist/web/assets/{process-table-CzLaWPe0.js → process-table-NxYTZ0n9.js} +1 -1
- package/dist/web/assets/process-table-NxYTZ0n9.js.br +0 -0
- package/dist/web/assets/process-table-NxYTZ0n9.js.gz +0 -0
- package/dist/web/assets/{proxy-settings-section-BCq7R5DA.js → proxy-settings-section-Exx7cQc4.js} +1 -1
- package/dist/web/assets/proxy-settings-section-Exx7cQc4.js.br +0 -0
- package/dist/web/assets/proxy-settings-section-Exx7cQc4.js.gz +0 -0
- package/dist/web/assets/{query-tab-DGvj_h6l.js → query-tab-BWtJPuxw.js} +1 -1
- package/dist/web/assets/query-tab-BWtJPuxw.js.br +0 -0
- package/dist/web/assets/{query-tab-DGvj_h6l.js.gz → query-tab-BWtJPuxw.js.gz} +0 -0
- package/dist/web/assets/{remote-access-settings-section-BrqOoOyk.js → remote-access-settings-section-C4K__DNW.js} +1 -1
- package/dist/web/assets/remote-access-settings-section-C4K__DNW.js.br +0 -0
- package/dist/web/assets/remote-access-settings-section-C4K__DNW.js.gz +0 -0
- package/dist/web/assets/{remote-desktop-clipboard-notice-5r6UbdlN.js → remote-desktop-clipboard-notice-DQyysBwL.js} +1 -1
- package/dist/web/assets/remote-desktop-clipboard-notice-DQyysBwL.js.br +0 -0
- package/dist/web/assets/remote-desktop-clipboard-notice-DQyysBwL.js.gz +0 -0
- package/dist/web/assets/{remote-desktop-mobile-sheet-B9Q4auqe.js → remote-desktop-mobile-sheet-DZtEFmgf.js} +2 -2
- package/dist/web/assets/remote-desktop-mobile-sheet-DZtEFmgf.js.br +0 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-DZtEFmgf.js.gz +0 -0
- package/dist/web/assets/{remote-desktop-mobile-view-CL-jrkA8.js → remote-desktop-mobile-view-VW7HdPUX.js} +1 -1
- package/dist/web/assets/remote-desktop-mobile-view-VW7HdPUX.js.br +0 -0
- package/dist/web/assets/remote-desktop-mobile-view-VW7HdPUX.js.gz +0 -0
- package/dist/web/assets/{remote-desktop-readiness-gate-DXEf2Jnz.js → remote-desktop-readiness-gate-B3tuP9Kt.js} +1 -1
- package/dist/web/assets/remote-desktop-readiness-gate-B3tuP9Kt.js.br +0 -0
- package/dist/web/assets/remote-desktop-readiness-gate-B3tuP9Kt.js.gz +0 -0
- package/dist/web/assets/{remote-desktop-window-content-20P6-gKB.js → remote-desktop-window-content-n0NoDCaE.js} +1 -1
- package/dist/web/assets/remote-desktop-window-content-n0NoDCaE.js.br +0 -0
- package/dist/web/assets/remote-desktop-window-content-n0NoDCaE.js.gz +0 -0
- package/dist/web/assets/{rename-object-dialog-Bs7pUsUP.js → rename-object-dialog-rLTW4A55.js} +1 -1
- package/dist/web/assets/rename-object-dialog-rLTW4A55.js.br +0 -0
- package/dist/web/assets/rename-object-dialog-rLTW4A55.js.gz +0 -0
- package/dist/web/assets/{review-code-Bjo4sHN9.js → review-code-DNfsaNO1.js} +1 -1
- package/dist/web/assets/review-code-DNfsaNO1.js.br +0 -0
- package/dist/web/assets/review-code-DNfsaNO1.js.gz +0 -0
- package/dist/web/assets/{save-as-dialog-BHx0Wp2B.js → save-as-dialog-BSz8WrNZ.js} +1 -1
- package/dist/web/assets/save-as-dialog-BSz8WrNZ.js.br +0 -0
- package/dist/web/assets/save-as-dialog-BSz8WrNZ.js.gz +0 -0
- package/dist/web/assets/{schedules-settings-section-C6ATYDPK.js → schedules-settings-section-DqUZ-H2K.js} +1 -1
- package/dist/web/assets/schedules-settings-section-DqUZ-H2K.js.br +0 -0
- package/dist/web/assets/schedules-settings-section-DqUZ-H2K.js.gz +0 -0
- package/dist/web/assets/{session-review-tab-BPMUyNTx.js → session-review-tab-DKtrFb9y.js} +1 -1
- package/dist/web/assets/session-review-tab-DKtrFb9y.js.br +0 -0
- package/dist/web/assets/session-review-tab-DKtrFb9y.js.gz +0 -0
- package/dist/web/assets/{settings-body-Dy5D6Vu7.js → settings-body-C42xA9Dw.js} +1 -1
- package/dist/web/assets/settings-body-C42xA9Dw.js.br +0 -0
- package/dist/web/assets/settings-body-C42xA9Dw.js.gz +0 -0
- package/dist/web/assets/{settings-rows-BRoYE2To.js → settings-rows-C7ZatT9O.js} +1 -1
- package/dist/web/assets/settings-rows-C7ZatT9O.js.br +0 -0
- package/dist/web/assets/settings-rows-C7ZatT9O.js.gz +0 -0
- package/dist/web/assets/{settings-section-content-essUViKu.js → settings-section-content-B_iXJpcK.js} +2 -2
- package/dist/web/assets/settings-section-content-B_iXJpcK.js.br +0 -0
- package/dist/web/assets/settings-section-content-B_iXJpcK.js.gz +0 -0
- package/dist/web/assets/settings-section-content-rxfQEQ66.js +1 -0
- package/dist/web/assets/{settings-tab-Di1DUP1j.js → settings-tab-PRw6OpEb.js} +1 -1
- package/dist/web/assets/{settings-window-content-DYvYFPWy.js → settings-window-content-WITQsGfW.js} +1 -1
- package/dist/web/assets/{slash-command-picker-8da30mYP.js → slash-command-picker-DmDs8nOj.js} +1 -1
- package/dist/web/assets/slash-command-picker-DmDs8nOj.js.br +0 -0
- package/dist/web/assets/slash-command-picker-DmDs8nOj.js.gz +0 -0
- package/dist/web/assets/{sql-object-tab-DRIHVXuR.js → sql-object-tab-D6P4hzkb.js} +1 -1
- package/dist/web/assets/sql-object-tab-D6P4hzkb.js.br +0 -0
- package/dist/web/assets/sql-object-tab-D6P4hzkb.js.gz +0 -0
- package/dist/web/assets/{sqlite-viewer-G-Jl2yEr.js → sqlite-viewer-mUitnzHB.js} +1 -1
- package/dist/web/assets/sqlite-viewer-mUitnzHB.js.br +0 -0
- package/dist/web/assets/sqlite-viewer-mUitnzHB.js.gz +0 -0
- package/dist/web/assets/{structure-save-dialog-Cwikz0we.js → structure-save-dialog-CmbNlmBY.js} +1 -1
- package/dist/web/assets/structure-save-dialog-CmbNlmBY.js.br +0 -0
- package/dist/web/assets/structure-save-dialog-CmbNlmBY.js.gz +0 -0
- package/dist/web/assets/{structure-tab-CWEgdZPm.js → structure-tab-DCPNuFTe.js} +1 -1
- package/dist/web/assets/structure-tab-DCPNuFTe.js.br +0 -0
- package/dist/web/assets/structure-tab-DCPNuFTe.js.gz +0 -0
- package/dist/web/assets/{system-monitor-body-BDQm9-wb.js → system-monitor-body-BhxyqSqu.js} +2 -2
- package/dist/web/assets/system-monitor-body-BhxyqSqu.js.br +0 -0
- package/dist/web/assets/system-monitor-body-BhxyqSqu.js.gz +0 -0
- package/dist/web/assets/{system-monitor-tab-BouN21Yh.js → system-monitor-tab-B_miogQI.js} +1 -1
- package/dist/web/assets/{system-monitor-window-content-BRyWBSOb.js → system-monitor-window-content-8IogR6tR.js} +1 -1
- package/dist/web/assets/{tab-host-window-content-CZ55-bTz.js → tab-host-window-content-BWHQjgWZ.js} +1 -1
- package/dist/web/assets/tab-host-window-content-BWHQjgWZ.js.br +0 -0
- package/dist/web/assets/tab-host-window-content-BWHQjgWZ.js.gz +0 -0
- package/dist/web/assets/{table-tab-Bmn4UdBV.js → table-tab-E3YMueu_.js} +1 -1
- package/dist/web/assets/table-tab-E3YMueu_.js.br +0 -0
- package/dist/web/assets/table-tab-E3YMueu_.js.gz +0 -0
- package/dist/web/assets/{telegram-bot-token-field-fdHwRf39.js → telegram-bot-token-field-CwW24m62.js} +1 -1
- package/dist/web/assets/telegram-bot-token-field-CwW24m62.js.br +0 -0
- package/dist/web/assets/telegram-bot-token-field-CwW24m62.js.gz +0 -0
- package/dist/web/assets/{terminal-tab-D-qlZXLK.js → terminal-tab-BAe5cWEL.js} +1 -1
- package/dist/web/assets/terminal-tab-BAe5cWEL.js.br +0 -0
- package/dist/web/assets/terminal-tab-BAe5cWEL.js.gz +0 -0
- package/dist/web/assets/{tool-cards-BdXbQ9Fv.js → tool-cards-DrCiSH2r.js} +2 -2
- package/dist/web/assets/tool-cards-DrCiSH2r.js.br +0 -0
- package/dist/web/assets/tool-cards-DrCiSH2r.js.gz +0 -0
- package/dist/web/assets/{tools-settings-section-CcPaspbR.js → tools-settings-section-S-vBr2af.js} +1 -1
- package/dist/web/assets/tools-settings-section-S-vBr2af.js.br +0 -0
- package/dist/web/assets/tools-settings-section-S-vBr2af.js.gz +0 -0
- package/dist/web/assets/{use-inline-blame-Ck5XoSS3.js → use-inline-blame-BzVaaCCt.js} +1 -1
- package/dist/web/assets/use-inline-blame-BzVaaCCt.js.br +0 -0
- package/dist/web/assets/{use-inline-blame-Ck5XoSS3.js.gz → use-inline-blame-BzVaaCCt.js.gz} +0 -0
- package/dist/web/assets/{use-lsp-CGSqfFBk.js → use-lsp-BwDL9Fqk.js} +1 -1
- package/dist/web/assets/use-lsp-BwDL9Fqk.js.br +0 -0
- package/dist/web/assets/use-lsp-BwDL9Fqk.js.gz +0 -0
- package/dist/web/assets/{use-monaco-theme-BW05eZS4.js → use-monaco-theme-DljSc3Xo.js} +1 -1
- package/dist/web/assets/{use-process-kill-DJ_f7rCO.js → use-process-kill-YDcDqDtG.js} +1 -1
- package/dist/web/assets/use-process-kill-YDcDqDtG.js.br +0 -0
- package/dist/web/assets/use-process-kill-YDcDqDtG.js.gz +0 -0
- package/dist/web/assets/{use-sql-schema-info-wt320fVR.js → use-sql-schema-info-cAI0vn7d.js} +1 -1
- package/dist/web/assets/use-sql-schema-info-cAI0vn7d.js.br +0 -0
- package/dist/web/assets/use-sql-schema-info-cAI0vn7d.js.gz +0 -0
- package/dist/web/assets/{use-websocket-CTwe74Cb.js → use-websocket-Djo_ruX_.js} +1 -1
- package/dist/web/assets/{web-preview-tab-D4XayUEK.js → web-preview-tab-DXy0hRef.js} +1 -1
- package/dist/web/assets/web-preview-tab-DXy0hRef.js.br +0 -0
- package/dist/web/assets/web-preview-tab-DXy0hRef.js.gz +0 -0
- package/dist/web/index.html +2 -2
- 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/architecture/ai-chat-and-providers.md +29 -0
- package/docs/deployment-guide.md +39 -0
- package/docs/lessons-learned.md +14 -4
- package/package.json +1 -1
- package/scripts/mem-diag-report.ts +31 -0
- package/src/providers/claude-agent-sdk-query-options.ts +13 -2
- package/src/providers/claude-agent-sdk.ts +58 -18
- package/src/providers/claude-approval-queue.ts +75 -0
- package/src/providers/claude-effort-status.ts +211 -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 +21 -1
- package/src/server/routes/database-impexp.ts +1 -1
- package/src/server/routes/design-preview.ts +2 -2
- package/src/server/routes/settings.ts +16 -0
- package/src/server/ws/chat-effort-status.ts +73 -0
- package/src/server/ws/chat.ts +33 -3
- 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/shared/effort-status.ts +128 -0
- package/src/types/api.ts +3 -1
- package/src/types/chat.ts +8 -0
- package/src/web/components/chat/chat-scroll-nav.tsx +12 -8
- package/src/web/components/chat/chat-tab.tsx +15 -11
- package/src/web/components/chat/message-action-bar.tsx +19 -4
- package/src/web/components/chat/message-input.tsx +6 -0
- package/src/web/components/chat/message-list.tsx +55 -21
- package/src/web/components/chat/message-user-bubble.tsx +2 -2
- package/src/web/components/chat/model-thinking-selector-helpers.ts +79 -0
- package/src/web/components/chat/model-thinking-selector.tsx +36 -10
- package/src/web/components/chat/session-changes-context.tsx +29 -1
- package/src/web/components/layout/reparenting-tab.tsx +53 -14
- package/src/web/components/settings/ai-settings-section.tsx +32 -0
- package/src/web/components/settings/notifications-settings-section.tsx +14 -1
- package/src/web/hooks/use-chat.ts +16 -0
- package/src/web/hooks/use-effort-status.ts +46 -0
- package/src/web/hooks/use-relative-time-tick.ts +35 -0
- package/src/web/hooks/use-session-file-changes.ts +5 -1
- package/src/web/hooks/use-tab-overflow.ts +26 -13
- package/src/web/lib/format-date.ts +6 -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-DD88DRdx.js.br +0 -0
- package/dist/web/assets/accounts-settings-section-DD88DRdx.js.gz +0 -0
- package/dist/web/assets/agent-session-sheet-C0PhhLKR.js.br +0 -0
- package/dist/web/assets/agent-session-sheet-C0PhhLKR.js.gz +0 -0
- package/dist/web/assets/agent-session-window-content-D0XZgh4U.js.br +0 -0
- package/dist/web/assets/agent-session-window-content-D0XZgh4U.js.gz +0 -0
- package/dist/web/assets/ai-resource-editor-CpfQB6A7.js.br +0 -0
- package/dist/web/assets/ai-resource-editor-CpfQB6A7.js.gz +0 -0
- package/dist/web/assets/ai-settings-section-CQWBwWcM.js +0 -1
- package/dist/web/assets/ai-settings-section-CQWBwWcM.js.br +0 -0
- package/dist/web/assets/ai-settings-section-CQWBwWcM.js.gz +0 -0
- package/dist/web/assets/android-tab-4Z4AgHz1.js.br +0 -0
- package/dist/web/assets/android-tab-4Z4AgHz1.js.gz +0 -0
- package/dist/web/assets/appearance-settings-section-DjQjiJF7.js.br +0 -0
- package/dist/web/assets/appearance-settings-section-DjQjiJF7.js.gz +0 -0
- package/dist/web/assets/apps-panel-_lxY9xbc.js.br +0 -0
- package/dist/web/assets/apps-panel-_lxY9xbc.js.gz +0 -0
- package/dist/web/assets/branch-review-tab-BzrhnFSs.js.br +0 -0
- package/dist/web/assets/branch-review-tab-BzrhnFSs.js.gz +0 -0
- package/dist/web/assets/chat-tab-BKHepkYi.js.br +0 -0
- package/dist/web/assets/chat-tab-BKHepkYi.js.gz +0 -0
- package/dist/web/assets/chat-tab-Cv8gYzcA.js +0 -19
- package/dist/web/assets/chat-tab-Cv8gYzcA.js.br +0 -0
- package/dist/web/assets/chat-tab-Cv8gYzcA.js.gz +0 -0
- package/dist/web/assets/code-editor-COWmNXO4.js.br +0 -0
- package/dist/web/assets/code-editor-COWmNXO4.js.gz +0 -0
- package/dist/web/assets/codex-reset-credit-button-BHt4yJet.js.br +0 -0
- package/dist/web/assets/codex-reset-credit-button-BHt4yJet.js.gz +0 -0
- package/dist/web/assets/conflict-editor-tj-1gzKr.js.br +0 -0
- package/dist/web/assets/conflict-editor-tj-1gzKr.js.gz +0 -0
- package/dist/web/assets/connection-form-tab-BV2QX8Cf.js.br +0 -0
- package/dist/web/assets/connection-form-tab-BV2QX8Cf.js.gz +0 -0
- package/dist/web/assets/csv-preview-xhE2esgz.js.br +0 -0
- package/dist/web/assets/csv-preview-xhE2esgz.js.gz +0 -0
- package/dist/web/assets/db-tab-parts-C3MsjOID.js.br +0 -0
- package/dist/web/assets/db-tab-parts-C3MsjOID.js.gz +0 -0
- package/dist/web/assets/design-settings-section-C3LxYhIw.js.br +0 -0
- package/dist/web/assets/design-settings-section-C3LxYhIw.js.gz +0 -0
- package/dist/web/assets/design-tab-DYcvjdfb.js.br +0 -0
- package/dist/web/assets/design-tab-DYcvjdfb.js.gz +0 -0
- package/dist/web/assets/designs-sidebar-panel-Dpr4PPFT.js.br +0 -0
- package/dist/web/assets/designs-sidebar-panel-Dpr4PPFT.js.gz +0 -0
- package/dist/web/assets/diff-viewer-Fl_4jqr6.js.br +0 -0
- package/dist/web/assets/diff-viewer-Fl_4jqr6.js.gz +0 -0
- package/dist/web/assets/docx-preview-bVE9shPX.js.br +0 -0
- package/dist/web/assets/docx-preview-bVE9shPX.js.gz +0 -0
- package/dist/web/assets/editor-dialog-eEOYWdbr.js.br +0 -0
- package/dist/web/assets/editor-dialog-eEOYWdbr.js.gz +0 -0
- package/dist/web/assets/editor-language-service-D1_Ggq4s.js.br +0 -0
- package/dist/web/assets/editor-language-service-D1_Ggq4s.js.gz +0 -0
- package/dist/web/assets/explorer-body-C0UOcBPJ.js.br +0 -0
- package/dist/web/assets/explorer-body-C0UOcBPJ.js.gz +0 -0
- package/dist/web/assets/explorer-window-content-Dxee7SuJ.js.br +0 -0
- package/dist/web/assets/explorer-window-content-Dxee7SuJ.js.gz +0 -0
- package/dist/web/assets/extension-manager-section-D8krnoj8.js.br +0 -0
- package/dist/web/assets/extension-manager-section-D8krnoj8.js.gz +0 -0
- package/dist/web/assets/extension-webview-CtIdV69T.js.br +0 -0
- package/dist/web/assets/extension-webview-CtIdV69T.js.gz +0 -0
- package/dist/web/assets/files-settings-section-C8vtarR8.js.br +0 -0
- package/dist/web/assets/files-settings-section-C8vtarR8.js.gz +0 -0
- package/dist/web/assets/format-date-CSHaU2wN.js +0 -1
- package/dist/web/assets/general-settings-section-Chu8Ut8h.js.br +0 -0
- package/dist/web/assets/general-settings-section-Chu8Ut8h.js.gz +0 -0
- package/dist/web/assets/git-log-panel-DG09p6k5.js.br +0 -0
- package/dist/web/assets/git-log-panel-DG09p6k5.js.gz +0 -0
- package/dist/web/assets/git-review-tab-CSy8DjSC.js.br +0 -0
- package/dist/web/assets/git-review-tab-CSy8DjSC.js.gz +0 -0
- package/dist/web/assets/glide-data-grid-BN1y4BT0.js.br +0 -0
- package/dist/web/assets/glide-data-grid-BN1y4BT0.js.gz +0 -0
- package/dist/web/assets/grid-save-dialog-CfSuWw2t.js.br +0 -0
- package/dist/web/assets/grid-save-dialog-CfSuWw2t.js.gz +0 -0
- package/dist/web/assets/group-chat-tab-B04QMp7A.js.br +0 -0
- package/dist/web/assets/group-chat-tab-B04QMp7A.js.gz +0 -0
- package/dist/web/assets/impexp-tab-BRNouquR.js.br +0 -0
- package/dist/web/assets/impexp-tab-BRNouquR.js.gz +0 -0
- package/dist/web/assets/index-BpgSIM5B.js.br +0 -0
- package/dist/web/assets/index-BpgSIM5B.js.gz +0 -0
- package/dist/web/assets/keyboard-shortcuts-section-BKDyZHhK.js.br +0 -0
- package/dist/web/assets/keyboard-shortcuts-section-BKDyZHhK.js.gz +0 -0
- package/dist/web/assets/language-servers-section-CbbkezhA.js.br +0 -0
- package/dist/web/assets/language-servers-section-CbbkezhA.js.gz +0 -0
- package/dist/web/assets/logs-app-Blr1-v4q.js.br +0 -0
- package/dist/web/assets/logs-app-Blr1-v4q.js.gz +0 -0
- package/dist/web/assets/lsp-client-Ds3U2O52.js.br +0 -0
- package/dist/web/assets/lsp-client-Ds3U2O52.js.gz +0 -0
- package/dist/web/assets/markdown-renderer-DzCPadpB.js.br +0 -0
- package/dist/web/assets/markdown-renderer-DzCPadpB.js.gz +0 -0
- package/dist/web/assets/mobile-explorer-sheet-BClCMXb8.js.br +0 -0
- package/dist/web/assets/mobile-explorer-sheet-BClCMXb8.js.gz +0 -0
- package/dist/web/assets/new-design-dialog-HRP9H11v.js.br +0 -0
- package/dist/web/assets/new-design-dialog-HRP9H11v.js.gz +0 -0
- package/dist/web/assets/notifications-settings-section-CYTTQaNr.js +0 -1
- package/dist/web/assets/notifications-settings-section-CYTTQaNr.js.br +0 -0
- package/dist/web/assets/notifications-settings-section-CYTTQaNr.js.gz +0 -0
- package/dist/web/assets/ppmbot-settings-section-BRowIIqc.js.br +0 -0
- package/dist/web/assets/ppmbot-settings-section-BRowIIqc.js.gz +0 -0
- package/dist/web/assets/problems-panel-DdB10Ybd.js.br +0 -0
- package/dist/web/assets/problems-panel-DdB10Ybd.js.gz +0 -0
- package/dist/web/assets/process-table-CzLaWPe0.js.br +0 -0
- package/dist/web/assets/process-table-CzLaWPe0.js.gz +0 -0
- package/dist/web/assets/proxy-settings-section-BCq7R5DA.js.br +0 -0
- package/dist/web/assets/proxy-settings-section-BCq7R5DA.js.gz +0 -0
- package/dist/web/assets/query-tab-DGvj_h6l.js.br +0 -0
- package/dist/web/assets/remote-access-settings-section-BrqOoOyk.js.br +0 -0
- package/dist/web/assets/remote-access-settings-section-BrqOoOyk.js.gz +0 -0
- package/dist/web/assets/remote-desktop-clipboard-notice-5r6UbdlN.js.br +0 -0
- package/dist/web/assets/remote-desktop-clipboard-notice-5r6UbdlN.js.gz +0 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-B9Q4auqe.js.br +0 -0
- package/dist/web/assets/remote-desktop-mobile-sheet-B9Q4auqe.js.gz +0 -0
- package/dist/web/assets/remote-desktop-mobile-view-CL-jrkA8.js.br +0 -0
- package/dist/web/assets/remote-desktop-mobile-view-CL-jrkA8.js.gz +0 -0
- package/dist/web/assets/remote-desktop-readiness-gate-DXEf2Jnz.js.br +0 -0
- package/dist/web/assets/remote-desktop-readiness-gate-DXEf2Jnz.js.gz +0 -0
- package/dist/web/assets/remote-desktop-window-content-20P6-gKB.js.br +0 -0
- package/dist/web/assets/remote-desktop-window-content-20P6-gKB.js.gz +0 -0
- package/dist/web/assets/rename-object-dialog-Bs7pUsUP.js.br +0 -0
- package/dist/web/assets/rename-object-dialog-Bs7pUsUP.js.gz +0 -0
- package/dist/web/assets/review-code-Bjo4sHN9.js.br +0 -0
- package/dist/web/assets/review-code-Bjo4sHN9.js.gz +0 -0
- package/dist/web/assets/save-as-dialog-BHx0Wp2B.js.br +0 -0
- package/dist/web/assets/save-as-dialog-BHx0Wp2B.js.gz +0 -0
- package/dist/web/assets/schedules-settings-section-C6ATYDPK.js.br +0 -0
- package/dist/web/assets/schedules-settings-section-C6ATYDPK.js.gz +0 -0
- package/dist/web/assets/session-review-tab-BPMUyNTx.js.br +0 -0
- package/dist/web/assets/session-review-tab-BPMUyNTx.js.gz +0 -0
- package/dist/web/assets/settings-body-Dy5D6Vu7.js.br +0 -0
- package/dist/web/assets/settings-body-Dy5D6Vu7.js.gz +0 -0
- package/dist/web/assets/settings-rows-BRoYE2To.js.br +0 -0
- package/dist/web/assets/settings-rows-BRoYE2To.js.gz +0 -0
- package/dist/web/assets/settings-section-content-C4DX7TYv.js +0 -1
- package/dist/web/assets/settings-section-content-essUViKu.js.br +0 -0
- package/dist/web/assets/settings-section-content-essUViKu.js.gz +0 -0
- package/dist/web/assets/slash-command-picker-8da30mYP.js.br +0 -0
- package/dist/web/assets/slash-command-picker-8da30mYP.js.gz +0 -0
- package/dist/web/assets/sql-object-tab-DRIHVXuR.js.br +0 -0
- package/dist/web/assets/sql-object-tab-DRIHVXuR.js.gz +0 -0
- package/dist/web/assets/sqlite-viewer-G-Jl2yEr.js.br +0 -0
- package/dist/web/assets/sqlite-viewer-G-Jl2yEr.js.gz +0 -0
- package/dist/web/assets/structure-save-dialog-Cwikz0we.js.br +0 -0
- package/dist/web/assets/structure-save-dialog-Cwikz0we.js.gz +0 -0
- package/dist/web/assets/structure-tab-CWEgdZPm.js.br +0 -0
- package/dist/web/assets/structure-tab-CWEgdZPm.js.gz +0 -0
- package/dist/web/assets/system-monitor-body-BDQm9-wb.js.br +0 -0
- package/dist/web/assets/system-monitor-body-BDQm9-wb.js.gz +0 -0
- package/dist/web/assets/tab-host-window-content-CZ55-bTz.js.br +0 -0
- package/dist/web/assets/tab-host-window-content-CZ55-bTz.js.gz +0 -0
- package/dist/web/assets/table-tab-Bmn4UdBV.js.br +0 -0
- package/dist/web/assets/table-tab-Bmn4UdBV.js.gz +0 -0
- package/dist/web/assets/telegram-bot-token-field-fdHwRf39.js.br +0 -0
- package/dist/web/assets/telegram-bot-token-field-fdHwRf39.js.gz +0 -0
- package/dist/web/assets/terminal-tab-D-qlZXLK.js.br +0 -0
- package/dist/web/assets/terminal-tab-D-qlZXLK.js.gz +0 -0
- package/dist/web/assets/tool-cards-BdXbQ9Fv.js.br +0 -0
- package/dist/web/assets/tool-cards-BdXbQ9Fv.js.gz +0 -0
- package/dist/web/assets/tools-settings-section-CcPaspbR.js.br +0 -0
- package/dist/web/assets/tools-settings-section-CcPaspbR.js.gz +0 -0
- package/dist/web/assets/use-inline-blame-Ck5XoSS3.js.br +0 -0
- package/dist/web/assets/use-lsp-CGSqfFBk.js.br +0 -0
- package/dist/web/assets/use-lsp-CGSqfFBk.js.gz +0 -0
- package/dist/web/assets/use-process-kill-DJ_f7rCO.js.br +0 -0
- package/dist/web/assets/use-process-kill-DJ_f7rCO.js.gz +0 -0
- package/dist/web/assets/use-sql-schema-info-wt320fVR.js.br +0 -0
- package/dist/web/assets/use-sql-schema-info-wt320fVR.js.gz +0 -0
- package/dist/web/assets/web-preview-tab-D4XayUEK.js.br +0 -0
- package/dist/web/assets/web-preview-tab-D4XayUEK.js.gz +0 -0
|
@@ -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
|
}
|
|
@@ -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;
|