@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
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns `mem-diag.log` into a short answer to "what was growing, and what went with it".
|
|
3
|
+
*
|
|
4
|
+
* Pure: takes the log's text, returns lines to print. `scripts/mem-diag-report.ts` is the
|
|
5
|
+
* command around it.
|
|
6
|
+
*
|
|
7
|
+
* The attribution is deliberately simple. Within one process run, every interval has a
|
|
8
|
+
* private-byte delta and a count of spawns per command. For each command, a slope through
|
|
9
|
+
* the origin (`Σ count·delta / Σ count²`) estimates MB per spawn, and the correlation says
|
|
10
|
+
* whether the two actually move together. It cannot separate two commands that always run
|
|
11
|
+
* together, and a leak not tied to a spawn shows up as growth no command explains — which is
|
|
12
|
+
* then the cue to look at the gauges and `nativeOwners` trends printed below it.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface DiagLine {
|
|
16
|
+
t: string;
|
|
17
|
+
role?: string;
|
|
18
|
+
pid?: number;
|
|
19
|
+
upMin?: number;
|
|
20
|
+
rssMB?: number;
|
|
21
|
+
privateMB?: number;
|
|
22
|
+
privateDeltaMB?: number;
|
|
23
|
+
heapMB?: number;
|
|
24
|
+
handles?: number;
|
|
25
|
+
spawned?: Record<string, number>;
|
|
26
|
+
liveChildren?: number;
|
|
27
|
+
children?: { count: number; privateMB: number; byName?: Record<string, { n: number; mb: number }> };
|
|
28
|
+
requests?: { total: number; top: Record<string, { n: number; mb: number }> };
|
|
29
|
+
nativeOwners?: Record<string, number>;
|
|
30
|
+
gauges?: Record<string, number | string>;
|
|
31
|
+
alert?: string;
|
|
32
|
+
snapshot?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CommandAttribution {
|
|
36
|
+
label: string;
|
|
37
|
+
spawns: number;
|
|
38
|
+
mbPerSpawn: number;
|
|
39
|
+
correlation: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Parse the log, skipping torn or foreign lines. */
|
|
43
|
+
export function parseDiagLog(text: string): DiagLine[] {
|
|
44
|
+
const out: DiagLine[] = [];
|
|
45
|
+
for (const raw of text.split("\n")) {
|
|
46
|
+
if (!raw.trim()) continue;
|
|
47
|
+
try {
|
|
48
|
+
const obj = JSON.parse(raw) as DiagLine;
|
|
49
|
+
if (typeof obj.t === "string") out.push(obj);
|
|
50
|
+
} catch { /* a line cut by a crash mid-write */ }
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Split samples into process runs: a new pid, or uptime going backwards, starts a run. */
|
|
56
|
+
export function splitRuns(lines: DiagLine[]): DiagLine[][] {
|
|
57
|
+
const runs: DiagLine[][] = [];
|
|
58
|
+
let current: DiagLine[] = [];
|
|
59
|
+
for (const l of lines) {
|
|
60
|
+
if (l.alert || l.snapshot || l.rssMB === undefined) continue;
|
|
61
|
+
const prev = current[current.length - 1];
|
|
62
|
+
const restarted = prev && ((l.pid !== undefined && prev.pid !== undefined && l.pid !== prev.pid)
|
|
63
|
+
|| (l.upMin !== undefined && prev.upMin !== undefined && l.upMin < prev.upMin));
|
|
64
|
+
if (restarted) { runs.push(current); current = []; }
|
|
65
|
+
current.push(l);
|
|
66
|
+
}
|
|
67
|
+
if (current.length) runs.push(current);
|
|
68
|
+
return runs;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** MB-per-spawn and correlation per command over one run's intervals. */
|
|
72
|
+
export function attributeGrowth(run: DiagLine[], minSpawns = 5): CommandAttribution[] {
|
|
73
|
+
const samples = run.filter((l) => typeof l.privateDeltaMB === "number" && l.spawned);
|
|
74
|
+
const labels = new Set<string>();
|
|
75
|
+
for (const s of samples) for (const k of Object.keys(s.spawned!)) labels.add(k);
|
|
76
|
+
const deltas = samples.map((s) => s.privateDeltaMB!);
|
|
77
|
+
const out: CommandAttribution[] = [];
|
|
78
|
+
for (const label of labels) {
|
|
79
|
+
const counts = samples.map((s) => s.spawned![label] ?? 0);
|
|
80
|
+
const spawns = counts.reduce((a, b) => a + b, 0);
|
|
81
|
+
if (spawns < minSpawns) continue;
|
|
82
|
+
let cd = 0, cc = 0;
|
|
83
|
+
counts.forEach((c, i) => { cd += c * (deltas[i] ?? 0); cc += c * c; });
|
|
84
|
+
out.push({ label, spawns, mbPerSpawn: cc ? cd / cc : 0, correlation: pearson(counts, deltas) });
|
|
85
|
+
}
|
|
86
|
+
return out.sort((a, b) => b.mbPerSpawn * b.spawns - a.mbPerSpawn * a.spawns);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function pearson(x: number[], y: number[]): number {
|
|
90
|
+
const n = x.length;
|
|
91
|
+
if (n < 3) return 0;
|
|
92
|
+
const mx = x.reduce((a, b) => a + b, 0) / n;
|
|
93
|
+
const my = y.reduce((a, b) => a + b, 0) / n;
|
|
94
|
+
let sxy = 0, sxx = 0, syy = 0;
|
|
95
|
+
x.forEach((xi, i) => {
|
|
96
|
+
const dx = xi - mx, dy = (y[i] ?? 0) - my;
|
|
97
|
+
sxy += dx * dy; sxx += dx * dx; syy += dy * dy;
|
|
98
|
+
});
|
|
99
|
+
return sxx && syy ? sxy / Math.sqrt(sxx * syy) : 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Numeric series that grew most between a run's first and last sample. */
|
|
103
|
+
function trends(first0: DiagLine, last0: DiagLine, pick: (l: DiagLine) => Record<string, number | string> | undefined): Array<[string, number, number]> {
|
|
104
|
+
const first = pick(first0) ?? {};
|
|
105
|
+
const last = pick(last0) ?? {};
|
|
106
|
+
const rows: Array<[string, number, number]> = [];
|
|
107
|
+
for (const [k, v] of Object.entries(last)) {
|
|
108
|
+
const a = Number(first[k] ?? 0), b = Number(v);
|
|
109
|
+
if (Number.isFinite(a) && Number.isFinite(b) && b !== a) rows.push([k, a, b]);
|
|
110
|
+
}
|
|
111
|
+
return rows.sort((p, q) => (q[2] - q[1]) - (p[2] - p[1]));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const fmt = (n: number | undefined, unit = "") => (n === undefined ? "—" : `${Math.round(n).toLocaleString("en-US")}${unit}`);
|
|
115
|
+
|
|
116
|
+
/** The whole report as printable lines. `lastRuns` limits output to the most recent runs. */
|
|
117
|
+
export function buildReport(text: string, lastRuns = 3): string[] {
|
|
118
|
+
const lines = parseDiagLog(text);
|
|
119
|
+
const out: string[] = [];
|
|
120
|
+
const runs = splitRuns(lines).slice(-lastRuns);
|
|
121
|
+
if (runs.length === 0) return ["No samples in the log. Is PPM_MEM_DIAG=1 set for the process?"];
|
|
122
|
+
|
|
123
|
+
for (const run of runs) {
|
|
124
|
+
const a = run[0], z = run[run.length - 1];
|
|
125
|
+
if (!a || !z) continue;
|
|
126
|
+
const hours = Math.max(1 / 60, (Date.parse(z.t) - Date.parse(a.t)) / 3_600_000);
|
|
127
|
+
out.push("", `== ${a.role ?? "server"} pid=${a.pid ?? "?"} ${a.t} → ${z.t} (${hours.toFixed(1)} h, ${run.length} samples)`);
|
|
128
|
+
if (a.privateMB === undefined) {
|
|
129
|
+
out.push(" This run predates private-byte sampling: only RSS/heap are available.");
|
|
130
|
+
} else {
|
|
131
|
+
const rate = ((z.privateMB ?? 0) - (a.privateMB ?? 0)) / hours;
|
|
132
|
+
out.push(` private ${fmt(a.privateMB)} → ${fmt(z.privateMB)} MB (${rate >= 0 ? "+" : ""}${fmt(rate)} MB/h)`);
|
|
133
|
+
// Growth the JS heap accounts for is a heap-snapshot problem, not a native one.
|
|
134
|
+
const heapGrowth = (z.heapMB ?? 0) - (a.heapMB ?? 0);
|
|
135
|
+
const privateGrowth = (z.privateMB ?? 0) - (a.privateMB ?? 0);
|
|
136
|
+
if (privateGrowth > 0) out.push(` JS heap explains ${fmt(Math.max(0, heapGrowth))} MB of that ${fmt(privateGrowth)} MB; the rest is native`);
|
|
137
|
+
}
|
|
138
|
+
out.push(` rss ${fmt(a.rssMB)} → ${fmt(z.rssMB)} MB heap ${fmt(a.heapMB)} → ${fmt(z.heapMB)} MB handles ${fmt(a.handles)} → ${fmt(z.handles)} live children ${fmt(a.liveChildren)} → ${fmt(z.liveChildren)}`);
|
|
139
|
+
|
|
140
|
+
if (a.children || z.children) {
|
|
141
|
+
const names = Object.entries(z.children?.byName ?? {}).sort((p, q) => q[1].mb - p[1].mb).slice(0, 5)
|
|
142
|
+
.map(([k, v]) => `${k}×${v.n} ${fmt(v.mb)} MB`).join(", ");
|
|
143
|
+
out.push(` children private ${fmt(a.children?.privateMB)} → ${fmt(z.children?.privateMB)} MB (${fmt(z.children?.count)} processes${names ? `: ${names}` : ""})`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const routes = new Map<string, { n: number; mb: number }>();
|
|
147
|
+
for (const l of run) {
|
|
148
|
+
for (const [k, v] of Object.entries(l.requests?.top ?? {})) {
|
|
149
|
+
const r = routes.get(k) ?? { n: 0, mb: 0 };
|
|
150
|
+
r.n += v.n;
|
|
151
|
+
r.mb += v.mb;
|
|
152
|
+
routes.set(k, r);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
if (routes.size) {
|
|
156
|
+
out.push(" Busiest routes over the run (each sample keeps its top 10):");
|
|
157
|
+
for (const [k, v] of [...routes].sort((p, q) => q[1].n - p[1].n).slice(0, 8)) {
|
|
158
|
+
out.push(` ${k.padEnd(44)} ${String(v.n).padStart(7)} requests ${v.mb.toFixed(1).padStart(8)} MB sent`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const attribution = attributeGrowth(run);
|
|
163
|
+
if (attribution.length) {
|
|
164
|
+
out.push(" Spawns vs private growth (slope through origin; correlation -1..1):");
|
|
165
|
+
for (const c of attribution.slice(0, 12)) {
|
|
166
|
+
out.push(` ${c.label.padEnd(32)} ${String(c.spawns).padStart(6)} spawns ${c.mbPerSpawn.toFixed(1).padStart(7)} MB/spawn r=${c.correlation.toFixed(2)}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const jumps = run
|
|
171
|
+
.filter((l) => (l.privateDeltaMB ?? 0) > 0)
|
|
172
|
+
.sort((p, q) => (q.privateDeltaMB ?? 0) - (p.privateDeltaMB ?? 0))
|
|
173
|
+
.slice(0, 8);
|
|
174
|
+
if (jumps.length) {
|
|
175
|
+
out.push(" Largest single-interval jumps:");
|
|
176
|
+
for (const j of jumps) {
|
|
177
|
+
const spawned = Object.entries(j.spawned ?? {}).sort((p, q) => q[1] - p[1]).slice(0, 5).map(([k, v]) => `${k}×${v}`).join(", ");
|
|
178
|
+
out.push(` ${j.t} +${fmt(j.privateDeltaMB)} MB spawned: ${spawned || "none"}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const gaugeTrends = trends(a, z, (l) => l.gauges).slice(0, 10);
|
|
183
|
+
if (gaugeTrends.length) {
|
|
184
|
+
out.push(" Gauges (first → last):");
|
|
185
|
+
for (const [k, x, y] of gaugeTrends) out.push(` ${k.padEnd(32)} ${fmt(x)} → ${fmt(y)}`);
|
|
186
|
+
}
|
|
187
|
+
const ownerTrends = trends(a, z, (l) => l.nativeOwners).slice(0, 8);
|
|
188
|
+
if (ownerTrends.length) {
|
|
189
|
+
out.push(" Objects owning native memory (first → last):");
|
|
190
|
+
for (const [k, x, y] of ownerTrends) out.push(` ${k.padEnd(32)} ${fmt(x)} → ${fmt(y)}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const alerts = lines.filter((l) => l.alert);
|
|
195
|
+
if (alerts.length) {
|
|
196
|
+
out.push("", "Threshold alerts (full detail is in the log line):");
|
|
197
|
+
for (const al of alerts.slice(-10)) out.push(` ${al.t} ${al.role ?? "server"} pid=${al.pid} ${al.alert} ${fmt((al as { privateMB?: number }).privateMB)} MB`);
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Counts HTTP requests by route for the memory monitor, so growth can be put next to traffic.
|
|
3
|
+
*
|
|
4
|
+
* The server's 26/10/2026 leak kept about a file's size of native memory per *file response*
|
|
5
|
+
* and per transcript read. Spawn counts could not show that: it rose with requests, not with
|
|
6
|
+
* child processes. A per-route count and the bytes each route sent, logged beside private
|
|
7
|
+
* bytes, is what lets the next reader say "it grows by N MB per `/api/files/raw`".
|
|
8
|
+
*
|
|
9
|
+
* Counters are cumulative; each reader keeps its own previous copy and diffs (`diffRequestCounts`),
|
|
10
|
+
* because the 30-second monitor and the 30-minute summary read the same counters at different rates.
|
|
11
|
+
*
|
|
12
|
+
* A route key is the method plus the path with anything that is data — a project name, an id,
|
|
13
|
+
* a file path, a token — replaced by a placeholder, so the log never carries one.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export interface RouteCount {
|
|
17
|
+
count: number;
|
|
18
|
+
/** Bytes declared by `Content-Length`; a streamed body has none and is counted in `streamed`. */
|
|
19
|
+
bytes: number;
|
|
20
|
+
streamed: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Segments after which the next one is a name or a token, never a route word. */
|
|
24
|
+
const NAME_BEFORE = new Set(["project", "projects", "session", "sessions", "content", "grid-export", "whep", "designs", "accounts", "connections", "extensions"]);
|
|
25
|
+
/** A segment that reads as a route word rather than data. */
|
|
26
|
+
const ROUTE_WORD = /^[a-z][a-z0-9-]{0,30}$/;
|
|
27
|
+
const MAX_SEGMENTS = 4;
|
|
28
|
+
/** Distinct keys kept; a path pattern we failed to normalise must not grow the map forever. */
|
|
29
|
+
const MAX_KEYS = 500;
|
|
30
|
+
|
|
31
|
+
/** `GET /api/project/ppm/files/raw?path=…` → `GET /api/project/:name/files`. Exported for tests. */
|
|
32
|
+
export function requestRouteKey(method: string, pathname: string): string {
|
|
33
|
+
const segs = pathname.split("/").filter(Boolean);
|
|
34
|
+
if (segs[0] !== "api" && segs[0] !== "ws") {
|
|
35
|
+
const ext = pathname.match(/\.([a-z0-9]{1,8})$/i)?.[1]?.toLowerCase();
|
|
36
|
+
return `${method} static${ext ? ` .${ext}` : ""}`;
|
|
37
|
+
}
|
|
38
|
+
const out: string[] = [];
|
|
39
|
+
for (let i = 0; i < segs.length && out.length < MAX_SEGMENTS; i++) {
|
|
40
|
+
const seg = segs[i]!;
|
|
41
|
+
const prev = segs[i - 1];
|
|
42
|
+
if (prev && NAME_BEFORE.has(prev)) out.push(":name");
|
|
43
|
+
else out.push(ROUTE_WORD.test(seg) ? seg : ":x");
|
|
44
|
+
}
|
|
45
|
+
return `${method} /${out.join("/")}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class RequestTracker {
|
|
49
|
+
private readonly counts = new Map<string, RouteCount>();
|
|
50
|
+
private total = 0;
|
|
51
|
+
|
|
52
|
+
record(method: string, pathname: string, contentLength: string | null): void {
|
|
53
|
+
this.total++;
|
|
54
|
+
const key = requestRouteKey(method, pathname);
|
|
55
|
+
let entry = this.counts.get(key);
|
|
56
|
+
if (!entry) {
|
|
57
|
+
if (this.counts.size >= MAX_KEYS) return;
|
|
58
|
+
entry = { count: 0, bytes: 0, streamed: 0 };
|
|
59
|
+
this.counts.set(key, entry);
|
|
60
|
+
}
|
|
61
|
+
entry.count++;
|
|
62
|
+
const n = contentLength === null ? NaN : Number(contentLength);
|
|
63
|
+
if (Number.isFinite(n)) entry.bytes += n; else entry.streamed++;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** A copy of the cumulative counters. */
|
|
67
|
+
read(): { total: number; routes: Map<string, RouteCount> } {
|
|
68
|
+
const routes = new Map<string, RouteCount>();
|
|
69
|
+
for (const [k, v] of this.counts) routes.set(k, { ...v });
|
|
70
|
+
return { total: this.total, routes };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** What happened between two readings: the busiest routes, by count, with MB sent. */
|
|
75
|
+
export function diffRequestCounts(
|
|
76
|
+
prev: ReturnType<RequestTracker["read"]> | undefined,
|
|
77
|
+
now: ReturnType<RequestTracker["read"]>,
|
|
78
|
+
limit: number,
|
|
79
|
+
): { total: number; top: Record<string, { n: number; mb: number; streamed: number }> } {
|
|
80
|
+
const rows: Array<[string, number, number, number]> = [];
|
|
81
|
+
for (const [key, v] of now.routes) {
|
|
82
|
+
const p = prev?.routes.get(key);
|
|
83
|
+
const n = v.count - (p?.count ?? 0);
|
|
84
|
+
if (n <= 0) continue;
|
|
85
|
+
rows.push([key, n, v.bytes - (p?.bytes ?? 0), v.streamed - (p?.streamed ?? 0)]);
|
|
86
|
+
}
|
|
87
|
+
rows.sort((a, b) => b[1] - a[1]);
|
|
88
|
+
const top: Record<string, { n: number; mb: number; streamed: number }> = {};
|
|
89
|
+
for (const [key, n, bytes, streamed] of rows.slice(0, limit)) {
|
|
90
|
+
top[key] = { n, mb: Math.round((bytes / (1024 * 1024)) * 10) / 10, streamed };
|
|
91
|
+
}
|
|
92
|
+
return { total: now.total - (prev?.total ?? 0), top };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const requestTracker = new RequestTracker();
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Counts every child process the server starts, for the memory monitor.
|
|
3
|
+
*
|
|
4
|
+
* A child process is the prime suspect for native growth on Windows: each
|
|
5
|
+
* `powershell.exe` spawn was measured to cost Bun a 32 MiB allocator segment it never
|
|
6
|
+
* returns (see `system-metrics/powershell-session.ts`). A per-interval count by command,
|
|
7
|
+
* logged beside private bytes, is what lets a later reader say "the commit charge climbs
|
|
8
|
+
* by N MB per `git` spawn" instead of guessing.
|
|
9
|
+
*
|
|
10
|
+
* It wraps `Bun.spawn` and `Bun.spawnSync`, which is enough: in Bun, `node:child_process`
|
|
11
|
+
* (`spawn`, `exec`, `execFile`, and their sync forms — what the Claude SDK, simple-git and
|
|
12
|
+
* codex go through) is built on those two globals, so the wrapper sees them too. Only a
|
|
13
|
+
* module that saved its own reference to `Bun.spawn` before the wrapper went in is missed.
|
|
14
|
+
*
|
|
15
|
+
* A label is the program's basename plus at most one more word that is known not to be data:
|
|
16
|
+
* a git subcommand, a script file's name, a runner's own subcommand, or the first program a
|
|
17
|
+
* shell is told to run. Anything else — an inline script, an argument, a word carrying `=`,
|
|
18
|
+
* `$` or quotes — is left out, because an argument can be a path, a prompt or a token.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
interface LiveChild {
|
|
22
|
+
pid: number;
|
|
23
|
+
label: string;
|
|
24
|
+
startedAt: number;
|
|
25
|
+
/** The chat session a Claude/Codex subprocess serves, from its own `--resume`/`--session-id`. */
|
|
26
|
+
session?: string;
|
|
27
|
+
/** Private bytes the last time the tree was read, so an exit can say what the child held. */
|
|
28
|
+
lastPrivateMB?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** What a long-lived child's start and exit look like to whoever logs them. */
|
|
32
|
+
export interface ChildLifecycleEvent {
|
|
33
|
+
event: "start" | "exit";
|
|
34
|
+
pid: number;
|
|
35
|
+
label: string;
|
|
36
|
+
session?: string;
|
|
37
|
+
ageMin?: number;
|
|
38
|
+
lastPrivateMB?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Labels whose start and exit are worth a log line: few, long-lived, and each one is a chat. */
|
|
42
|
+
const LIFECYCLE_LABEL = /^(claude|codex)\b/;
|
|
43
|
+
/** Session ids are UUIDs; anything else after the flag is not taken. */
|
|
44
|
+
const SESSION_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
45
|
+
const SESSION_FLAGS = ["--resume", "--session-id"];
|
|
46
|
+
|
|
47
|
+
/** The session a command line resumes or names, when it carries one. Exported for tests. */
|
|
48
|
+
export function sessionOf(argv: readonly unknown[]): string | undefined {
|
|
49
|
+
for (let i = 0; i < argv.length; i++) {
|
|
50
|
+
const a = argv[i];
|
|
51
|
+
if (typeof a !== "string") continue;
|
|
52
|
+
for (const flag of SESSION_FLAGS) {
|
|
53
|
+
const value = a === flag ? argv[i + 1] : a.startsWith(`${flag}=`) ? a.slice(flag.length + 1) : undefined;
|
|
54
|
+
if (typeof value === "string" && SESSION_ID.test(value)) return value;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface SpawnSnapshot {
|
|
61
|
+
/** Spawns since the previous snapshot, by label (sync ones included). */
|
|
62
|
+
spawned: Record<string, number>;
|
|
63
|
+
/** Children still running now, by label. */
|
|
64
|
+
live: Record<string, number>;
|
|
65
|
+
liveTotal: number;
|
|
66
|
+
/** Every spawn since the tracker started. */
|
|
67
|
+
total: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Programs whose own name says nothing: the script or subcommand after them is the label. */
|
|
71
|
+
const RUNNERS = new Set(["node", "bun", "npx", "bunx", "python", "python3", "py", "deno"]);
|
|
72
|
+
/** Programs handed a whole command line as one argument. */
|
|
73
|
+
const SHELLS = new Set(["cmd", "powershell", "pwsh", "sh", "bash", "zsh"]);
|
|
74
|
+
/** A runner flag whose value is code to run: the label stops at the flag. */
|
|
75
|
+
const EVAL_FLAGS = new Set(["-e", "--eval", "-p", "--print", "-c"]);
|
|
76
|
+
/** A runner's own subcommands, which are words and never data. */
|
|
77
|
+
const RUNNER_SUBCOMMANDS = new Set(["run", "x", "exec", "install", "add", "remove", "test", "build", "upgrade"]);
|
|
78
|
+
/** Runner flags that take a separate value, which is not the script. */
|
|
79
|
+
const RUNNER_VALUE_FLAGS = new Set(["-r", "--require", "--import", "--loader", "--cwd", "--env-file", "--config", "-m"]);
|
|
80
|
+
/** The shell flag after which the command line comes. */
|
|
81
|
+
const SHELL_COMMAND_FLAGS = new Set(["-c", "/c", "/k", "-command", "-file"]);
|
|
82
|
+
const SCRIPT_FILE = /\.(c|m)?[jt]sx?$|\.py$|\.ps1$/i;
|
|
83
|
+
/** A word that is plausibly a program or subcommand name and cannot carry an assignment or a quote. */
|
|
84
|
+
const PLAIN_WORD = /^[A-Za-z0-9._\\/:@+-]+$/;
|
|
85
|
+
const GIT_VALUE_FLAGS = new Set(["-c", "-C", "--git-dir", "--work-tree", "--namespace", "--exec-path"]);
|
|
86
|
+
|
|
87
|
+
/** Lowercased basename with the executable extension dropped, cut to 40 characters. */
|
|
88
|
+
function baseName(path: string): string {
|
|
89
|
+
const name = path.split(/[\\/]/).pop() ?? "";
|
|
90
|
+
return name.toLowerCase().replace(/\.(exe|cmd|bat|com)$/, "").slice(0, 40);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function gitLabel(rest: string[]): string {
|
|
94
|
+
for (let i = 0; i < rest.length; i++) {
|
|
95
|
+
const a = rest[i]!;
|
|
96
|
+
if (GIT_VALUE_FLAGS.has(a)) { i++; continue; }
|
|
97
|
+
if (a.startsWith("-")) continue;
|
|
98
|
+
return /^[a-z][a-z0-9-]*$/.test(a) ? `git ${a}` : "git";
|
|
99
|
+
}
|
|
100
|
+
return "git";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function runnerLabel(program: string, rest: string[]): string {
|
|
104
|
+
for (let i = 0; i < rest.length; i++) {
|
|
105
|
+
const a = rest[i]!;
|
|
106
|
+
if (EVAL_FLAGS.has(a)) return `${program} ${a}`;
|
|
107
|
+
if (RUNNER_VALUE_FLAGS.has(a)) { i++; continue; }
|
|
108
|
+
if (a.startsWith("-")) continue;
|
|
109
|
+
if (!PLAIN_WORD.test(a)) return program;
|
|
110
|
+
if (SCRIPT_FILE.test(a)) return `${program} ${baseName(a)}`;
|
|
111
|
+
if (RUNNER_SUBCOMMANDS.has(a)) return `${program} ${a}`;
|
|
112
|
+
// `npx <package>`: the package's own name, its scope and version dropped.
|
|
113
|
+
const pkg = a.match(/^(?:@[\w.-]+\/)?([\w.-]+)(?:@[\w.^~-]+)?$/);
|
|
114
|
+
if ((program === "npx" || program === "bunx") && pkg) return `${program} ${pkg[1]!.toLowerCase().slice(0, 40)}`;
|
|
115
|
+
return program;
|
|
116
|
+
}
|
|
117
|
+
return program;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function shellLabel(program: string, rest: string[]): string {
|
|
121
|
+
const at = rest.findIndex((a) => SHELL_COMMAND_FLAGS.has(a.toLowerCase()));
|
|
122
|
+
const line = at === -1 ? undefined : rest[at + 1];
|
|
123
|
+
if (line === undefined) return program;
|
|
124
|
+
const first = line.trim().split(/\s+/)[0] ?? "";
|
|
125
|
+
if (!first || !PLAIN_WORD.test(first)) return program;
|
|
126
|
+
return `${program} ${baseName(first)}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** A short, argument-free name for a command line. Exported for tests. */
|
|
130
|
+
export function spawnLabel(argv: readonly unknown[]): string {
|
|
131
|
+
const parts = argv.filter((a): a is string => typeof a === "string");
|
|
132
|
+
const [head, ...rest] = parts;
|
|
133
|
+
if (head === undefined) return "unknown";
|
|
134
|
+
const program = baseName(head.replace(/^["']|["']$/g, "")).replace(/[^a-z0-9._+-]/g, "");
|
|
135
|
+
if (!program) return "unknown";
|
|
136
|
+
if (program === "git") return gitLabel(rest);
|
|
137
|
+
if (RUNNERS.has(program)) return runnerLabel(program, rest);
|
|
138
|
+
if (SHELLS.has(program)) return shellLabel(program, rest);
|
|
139
|
+
return program;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** `Bun.spawn(cmd[], opts)` or `Bun.spawn({ cmd, ... })`. */
|
|
143
|
+
function argvOf(args: unknown[]): readonly unknown[] {
|
|
144
|
+
const first = args[0];
|
|
145
|
+
if (Array.isArray(first)) return first;
|
|
146
|
+
if (first && typeof first === "object" && Array.isArray((first as { cmd?: unknown }).cmd)) {
|
|
147
|
+
return (first as { cmd: unknown[] }).cmd;
|
|
148
|
+
}
|
|
149
|
+
return [];
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export class SpawnTracker {
|
|
153
|
+
/** Keyed by the child object, not its pid: a pid can be reused before the old child's `exited` settles. */
|
|
154
|
+
private readonly live = new Map<object, LiveChild>();
|
|
155
|
+
private interval = new Map<string, number>();
|
|
156
|
+
private readonly totals = new Map<string, number>();
|
|
157
|
+
private total = 0;
|
|
158
|
+
private installed = false;
|
|
159
|
+
/** Set by the memory watchdog to log long-lived children coming and going. */
|
|
160
|
+
onLifecycle?: (e: ChildLifecycleEvent) => void;
|
|
161
|
+
|
|
162
|
+
constructor(private readonly now: () => number = Date.now) {}
|
|
163
|
+
|
|
164
|
+
/** Record one spawn. Exported for tests; `install()` calls it for real spawns. */
|
|
165
|
+
record(label: string, child?: { pid?: number; exited?: Promise<unknown> }, session?: string): void {
|
|
166
|
+
this.total++;
|
|
167
|
+
this.interval.set(label, (this.interval.get(label) ?? 0) + 1);
|
|
168
|
+
this.totals.set(label, (this.totals.get(label) ?? 0) + 1);
|
|
169
|
+
const pid = child?.pid;
|
|
170
|
+
if (!child || typeof pid !== "number" || !child.exited) return;
|
|
171
|
+
const entry: LiveChild = { pid, label, startedAt: this.now(), session };
|
|
172
|
+
this.live.set(child, entry);
|
|
173
|
+
const logged = LIFECYCLE_LABEL.test(label);
|
|
174
|
+
if (logged) this.emit({ event: "start", pid, label, session });
|
|
175
|
+
const done = () => {
|
|
176
|
+
this.live.delete(child);
|
|
177
|
+
if (!logged) return;
|
|
178
|
+
this.emit({
|
|
179
|
+
event: "exit", pid, label, session,
|
|
180
|
+
ageMin: Math.round((this.now() - entry.startedAt) / 60_000),
|
|
181
|
+
lastPrivateMB: entry.lastPrivateMB,
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
child.exited.then(done, done);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private emit(e: ChildLifecycleEvent): void {
|
|
188
|
+
try { this.onLifecycle?.(e); } catch { /* a log line must never break a spawn */ }
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** What the tracker knows about a live direct child, by pid. */
|
|
192
|
+
describe(pid: number): LiveChild | undefined {
|
|
193
|
+
for (const c of this.live.values()) if (c.pid === pid) return c;
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Wrap the two globals. Safe to call twice. */
|
|
198
|
+
install(): void {
|
|
199
|
+
if (this.installed) return;
|
|
200
|
+
this.installed = true;
|
|
201
|
+
const bun = Bun as unknown as Record<"spawn" | "spawnSync", (...a: unknown[]) => unknown>;
|
|
202
|
+
const spawn = bun.spawn;
|
|
203
|
+
const spawnSync = bun.spawnSync;
|
|
204
|
+
bun.spawn = (...args: unknown[]) => {
|
|
205
|
+
const child = spawn.apply(Bun, args) as { pid?: number; exited?: Promise<unknown> };
|
|
206
|
+
try {
|
|
207
|
+
const argv = argvOf(args);
|
|
208
|
+
this.record(spawnLabel(argv), child, sessionOf(argv));
|
|
209
|
+
} catch { /* counting must never break a spawn */ }
|
|
210
|
+
return child;
|
|
211
|
+
};
|
|
212
|
+
bun.spawnSync = (...args: unknown[]) => {
|
|
213
|
+
try { this.record(spawnLabel(argvOf(args))); } catch { /* as above */ }
|
|
214
|
+
return spawnSync.apply(Bun, args);
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Counts since the last call, plus what is alive now. Resets the interval counts. */
|
|
219
|
+
snapshot(): SpawnSnapshot {
|
|
220
|
+
const live: Record<string, number> = {};
|
|
221
|
+
for (const c of this.live.values()) live[c.label] = (live[c.label] ?? 0) + 1;
|
|
222
|
+
const spawned = Object.fromEntries(this.interval);
|
|
223
|
+
this.interval = new Map();
|
|
224
|
+
return { spawned, live, liveTotal: this.live.size, total: this.total };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** The longest-running children, for a threshold alert. */
|
|
228
|
+
oldestLive(limit: number): Array<{ pid: number; label: string; ageMin: number }> {
|
|
229
|
+
const t = this.now();
|
|
230
|
+
return [...this.live.values()]
|
|
231
|
+
.sort((a, b) => a.startedAt - b.startedAt)
|
|
232
|
+
.slice(0, limit)
|
|
233
|
+
.map((c) => ({ pid: c.pid, label: c.label, ageMin: Math.round((t - c.startedAt) / 60_000) }));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Spawns by label since the tracker started, most first. */
|
|
237
|
+
topTotals(limit: number): Record<string, number> {
|
|
238
|
+
return Object.fromEntries([...this.totals.entries()].sort((a, b) => b[1] - a[1]).slice(0, limit));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export const spawnTracker = new SpawnTracker();
|