@hienlh/ppm 0.18.13 → 0.18.15
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/.rdtest2-cf.out +0 -0
- package/CHANGELOG.md +23 -0
- package/CLAUDE.md +3 -0
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/cli-reference.md +18 -0
- package/assets/skills/ppm/references/http-api.md +8 -1
- package/dist/web/assets/{adaptive-context-menu-CbgPYQmj.js → adaptive-context-menu-ChBZWLLq.js} +1 -1
- package/dist/web/assets/{ai-resource-editor-unM2HNmg.js → ai-resource-editor-C8aAY8Po.js} +1 -1
- package/dist/web/assets/{ai-settings-section-CdktdrwF.js → ai-settings-section-ERu1WiZc.js} +1 -1
- package/dist/web/assets/{angular-html-DcpbdcNG.js → angular-html-CXgHIQ2U.js} +1 -1
- package/dist/web/assets/{angular-ts-Bv_WIk34.js → angular-ts-C8renMV9.js} +1 -1
- package/dist/web/assets/api-client-WpZ2FDHg.js +1 -0
- package/dist/web/assets/api-settings-BgSEE6et.js +1 -0
- package/dist/web/assets/{api-settings-BdCtex9i.js → api-settings-R59c66DE.js} +1 -1
- package/dist/web/assets/{api-themes-pRRoLvgD.js → api-themes-DKxrwJda.js} +1 -1
- package/dist/web/assets/{apl-D5_lLszu.js → apl-DkGH8laI.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-CCalSKAt.js +1 -0
- package/dist/web/assets/{astro-CBHwddYL.js → astro-tHUd8Zlg.js} +1 -1
- package/dist/web/assets/{audio-preview-BD1iDovf.js → audio-preview-B_C6Dpvz.js} +1 -1
- package/dist/web/assets/{blade-DGe1d2Pq.js → blade-BbKpjIYK.js} +1 -1
- package/dist/web/assets/{bsl-2lyAzRJA.js → bsl-DA0UYto0.js} +1 -1
- package/dist/web/assets/{button-2WCAS8dI.js → button-DBSpDPS0.js} +1 -1
- package/dist/web/assets/{cairo-DjzZeJrq.js → cairo-79ayucbB.js} +1 -1
- package/dist/web/assets/chat-tab-BraiJsYG.js +15 -0
- package/dist/web/assets/{cobol-cC3XIuAJ.js → cobol-DSS2EOYo.js} +1 -1
- package/dist/web/assets/code-editor-lhmLNhXu.js +10 -0
- package/dist/web/assets/{coffee-4eGboVoU.js → coffee-DvSxq2iD.js} +1 -1
- package/dist/web/assets/{conflict-editor-DdJag4QK.js → conflict-editor-C4VCv51R.js} +1 -1
- package/dist/web/assets/{context-menu-BO1c9jKy.js → context-menu-BpwUny-o.js} +1 -1
- package/dist/web/assets/{cpp-DF9_G-OU.js → cpp-BhWZ-FSn.js} +1 -1
- package/dist/web/assets/{crystal-OavZlzcm.js → crystal-yP2OeF-f.js} +1 -1
- package/dist/web/assets/{csv-preview-DnXwouF3.js → csv-preview-D46W_icb.js} +1 -1
- package/dist/web/assets/{data-grid-overlay-editor-B9Gpjm3l.js → data-grid-overlay-editor-DeGkPYtA.js} +1 -1
- package/dist/web/assets/{database-viewer-Dk8oGyZx.js → database-viewer-DFI4YtrH.js} +1 -1
- package/dist/web/assets/{dialog-B-l2ZxVm.js → dialog-RIpeb87v.js} +1 -1
- package/dist/web/assets/diff-viewer-eaNOLcBL.js +4 -0
- package/dist/web/assets/{docx-preview-DAdHPlhZ.js → docx-preview-Cbj-pOK3.js} +1 -1
- package/dist/web/assets/{edge-TzYh8OlN.js → edge-D3K9cd-p.js} +1 -1
- package/dist/web/assets/{edit-diff-preview-DlwP3UIU.js → edit-diff-preview-DDqy3KLA.js} +1 -1
- package/dist/web/assets/{elixir-Cy3RhKKH.js → elixir-D4PBmsLp.js} +1 -1
- package/dist/web/assets/{elm-BTBZdweK.js → elm-DSpXjuaH.js} +1 -1
- package/dist/web/assets/{erb-dv1mI0y4.js → erb-CaYZMTJW.js} +1 -1
- package/dist/web/assets/{erlang-DjeiDrkl.js → erlang-DX5BFqJj.js} +1 -1
- package/dist/web/assets/{esm-FRwvcesu.js → esm-Cv7HsFnk.js} +1 -1
- package/dist/web/assets/explorer-body-BxbSttfl.js +2 -0
- package/dist/web/assets/explorer-window-content-DUqQCC7A.js +1 -0
- package/dist/web/assets/{extension-store-DUaZINyC.js → extension-store-B1EClUAr.js} +1 -1
- package/dist/web/assets/{extension-webview-W0qRfaAU.js → extension-webview-BaGvDk8v.js} +1 -1
- package/dist/web/assets/{file-store-yReVbR8q.js → file-store-m9crh6uq.js} +1 -1
- package/dist/web/assets/{fortran-fixed-form-BCH9DAIJ.js → fortran-fixed-form-B0bAufUe.js} +1 -1
- package/dist/web/assets/{fsharp-C93NYKYD.js → fsharp-C2AL2b1M.js} +1 -1
- package/dist/web/assets/{gdresource-BcIEE2eX.js → gdresource-DRQsDMNI.js} +1 -1
- package/dist/web/assets/{git-commit-BVjlXEy8.js → git-commit-GtBdnrzk.js} +1 -1
- package/dist/web/assets/{git-log-panel-2_4HuP47.js → git-log-panel-hwV1flSO.js} +1 -1
- package/dist/web/assets/{git-rebase-Bj_21B1V.js → git-rebase-h_0CGA6F.js} +1 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-DAUrJCqi.js +1 -0
- package/dist/web/assets/glide-data-grid-YsN-xm6x.js +138 -0
- package/dist/web/assets/{glimmer-js-C20yj4Zv.js → glimmer-js-Bxwu93bS.js} +1 -1
- package/dist/web/assets/{glimmer-ts-ipbYC7kh.js → glimmer-ts-CIUYdR4E.js} +1 -1
- package/dist/web/assets/{glsl-Dovazu3t.js → glsl-CfxLI5jn.js} +1 -1
- package/dist/web/assets/{graphql-DpMlM7hC.js → graphql-CstqDPde.js} +1 -1
- package/dist/web/assets/group-chat-tab-B7mJO1gG.js +1 -0
- package/dist/web/assets/{hack-gc7LCBuV.js → hack-Bjqnssb5.js} +1 -1
- package/dist/web/assets/{haml-vtlFm9gm.js → haml-lVc-DfbM.js} +1 -1
- package/dist/web/assets/hand-x0ejq3fS.js +1 -0
- package/dist/web/assets/{handlebars-DGKhpCji.js → handlebars-Yy6fPQI_.js} +1 -1
- package/dist/web/assets/{html-derivative-C3oTSSCN.js → html-derivative-CchzCY1z.js} +1 -1
- package/dist/web/assets/{html-DvfhQbXb.js → html-lqtcy6-2.js} +1 -1
- package/dist/web/assets/{http-SG6zLTo7.js → http-ChclRrko.js} +1 -1
- package/dist/web/assets/{hurl-BWXsYlqW.js → hurl-CoOK7sXa.js} +1 -1
- package/dist/web/assets/{hxml-DUdMzaoh.js → hxml-fZSZ7Iyt.js} +1 -1
- package/dist/web/assets/{image-preview-DxgFOV1T.js → image-preview-ClAcqDlL.js} +1 -1
- package/dist/web/assets/index-CtJbMNNI.css +2 -0
- package/dist/web/assets/index-H_gScEOk.js +52 -0
- package/dist/web/assets/info-3K5VOQVL-Cx1ZfJbp.js +1 -0
- package/dist/web/assets/{input-DuAF-Stj.js → input-DXPgUtxi.js} +1 -1
- package/dist/web/assets/{jinja-955w9RKM.js → jinja-Y0w_KBdE.js} +1 -1
- package/dist/web/assets/{jison-WWfqPo50.js → jison-BILj3F2e.js} +1 -1
- package/dist/web/assets/{julia-BFrfnaNl.js → julia-DRfOPDts.js} +1 -1
- package/dist/web/assets/{just-BrZdyR_I.js → just-n3U_oODO.js} +1 -1
- package/dist/web/assets/keybindings-store-BqctreaI.js +1 -0
- package/dist/web/assets/{keybindings-store-0Wqfa7Qp.js → keybindings-store-D-CHdq-G.js} +1 -1
- package/dist/web/assets/keyboard-DBBaoQSu.js +1 -0
- package/dist/web/assets/{latex-CwtPPASD.js → latex-D6gztEOL.js} +1 -1
- package/dist/web/assets/{liquid-CYZuvDDd.js → liquid-DPqCEVb9.js} +1 -1
- package/dist/web/assets/{lua-BV095ZyY.js → lua-I40KPUm-.js} +1 -1
- package/dist/web/assets/markdown-renderer-BwOkiZC1.js +1 -0
- package/dist/web/assets/{markdown-renderer-CnRcKK3d.js → markdown-renderer-oJ9jPdQ2.js} +3 -3
- package/dist/web/assets/{marko-CNeljZ1C.js → marko-DMjsThSW.js} +1 -1
- package/dist/web/assets/{mdc-JfGapShy.js → mdc-CN2alsbo.js} +1 -1
- package/dist/web/assets/{media-url-Bm4PsduE.js → media-url-COUvasjn.js} +1 -1
- package/dist/web/assets/mobile-bottom-sheet-Cj1YBq-c.js +1 -0
- package/dist/web/assets/mobile-explorer-sheet-D1nM0Ik1.js +1 -0
- package/dist/web/assets/{nextflow-DD5C8nqX.js → nextflow-CGa4JeZ6.js} +1 -1
- package/dist/web/assets/{nginx-Ca7B__x_.js → nginx-CmqrIj84.js} +1 -1
- package/dist/web/assets/{nim-B8FogsDw.js → nim-CqhmTeMa.js} +1 -1
- package/dist/web/assets/notification-store-n-XEQ5pH.js +1 -0
- package/dist/web/assets/{number-overlay-editor-DG9xJwEG.js → number-overlay-editor-BEzJ1YeD.js} +1 -1
- package/dist/web/assets/{overview-panel-s53bnl0q.js → overview-panel-Bf9Gn8qV.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-Bqwiy7dz.js +1 -0
- package/dist/web/assets/{panel-store-B4rijFKN.js → panel-store-UWCH6B-_.js} +1 -1
- package/dist/web/assets/{pdf-preview-DYt3aBzM.js → pdf-preview-BMHM5cR2.js} +1 -1
- package/dist/web/assets/{perl-BR78hS8R.js → perl-BOwpb6Tb.js} +1 -1
- package/dist/web/assets/{php-DXBOMC9z.js → php-wT9fVxxu.js} +1 -1
- package/dist/web/assets/pie-UPGHQEXC-BXSJ2God.js +1 -0
- package/dist/web/assets/{postgres-viewer-BvoWSuoZ.js → postgres-viewer-D8rfEY5b.js} +3 -3
- package/dist/web/assets/process-table-CsQ-hDJr.js +1 -0
- package/dist/web/assets/{project-store-BuY7rxn_.js → project-store-CKAOr23O.js} +1 -1
- package/dist/web/assets/{pug-CvTwVVKV.js → pug-DdlNnj-p.js} +1 -1
- package/dist/web/assets/{qml-CPSoZ1-B.js → qml-C5zBdydf.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-D6XrM7HH.js +1 -0
- package/dist/web/assets/{razor-BUA1YKtF.js → razor-DqcJX4iq.js} +1 -1
- package/dist/web/assets/remote-desktop-mobile-sheet-CTiipn4k.js +2 -0
- package/dist/web/assets/remote-desktop-mobile-view-Cf5B0dKo.js +1 -0
- package/dist/web/assets/remote-desktop-stats-overlay-vhJBIW7s.js +1 -0
- package/dist/web/assets/remote-desktop-window-content-wCJUfI6G.js +1 -0
- package/dist/web/assets/{rst-BJsq7ISz.js → rst-DiMiMYUO.js} +1 -1
- package/dist/web/assets/{ruby-DSDMeYf5.js → ruby-DK62iflA.js} +1 -1
- package/dist/web/assets/{run-in-terminal-C9xpskFn.js → run-in-terminal-DJSL6nwZ.js} +1 -1
- package/dist/web/assets/{sas-B1nJOjbN.js → sas-6QPGJRa5.js} +1 -1
- package/dist/web/assets/{scroll-area-DBcG1fQo.js → scroll-area-DXGXeiRz.js} +1 -1
- package/dist/web/assets/{scss-BZFyRiQn.js → scss-DmUqh48_.js} +1 -1
- package/dist/web/assets/{send-to-chat-BIg70MUZ.js → send-to-chat-BOony0sO.js} +1 -1
- package/dist/web/assets/settings-store-BpcY5aYE.js +2 -0
- package/dist/web/assets/settings-tab-BVqRO01j.js +1 -0
- package/dist/web/assets/settings-tab-FH581A_i.js +3 -0
- package/dist/web/assets/{shaderlab-laINRXBo.js → shaderlab-DopFR1mi.js} +1 -1
- package/dist/web/assets/{shellsession-BpOOvWVR.js → shellsession-Bp_TGuXb.js} +1 -1
- package/dist/web/assets/{soy-BW4d91Sa.js → soy-DEB9KEFb.js} +1 -1
- package/dist/web/assets/{sparql-BsvvyoHn.js → sparql-C8lEwO1p.js} +1 -1
- package/dist/web/assets/{sql-query-editor-qQce5QJ6.js → sql-query-editor-MMfk0Kbz.js} +1 -1
- package/dist/web/assets/{sqlite-viewer-yhc0YNvu.js → sqlite-viewer-vglzrgnZ.js} +1 -1
- package/dist/web/assets/{stata-K-dZInUt.js → stata-DVep4apN.js} +1 -1
- package/dist/web/assets/{surrealql-CpNp2sgf.js → surrealql-fSlJApOA.js} +1 -1
- package/dist/web/assets/{svelte-BG6iBA8R.js → svelte-DezWBEDi.js} +1 -1
- package/dist/web/assets/{switch-_NcrktLe.js → switch-DqL_ZjB_.js} +1 -1
- package/dist/web/assets/system-monitor-body-BYUfyF8f.js +2 -0
- package/dist/web/assets/system-monitor-tab-CKGXB8lH.js +1 -0
- package/dist/web/assets/{system-monitor-window-content-CaOhD0AU.js → system-monitor-window-content-CiJ2yIsr.js} +1 -1
- package/dist/web/assets/tab-host-window-content-DYlXR0cg.js +1 -0
- package/dist/web/assets/{tab-store-tFnyPmm4.js → tab-store-D2thn4Vv.js} +1 -1
- package/dist/web/assets/team-member-sheet-Bnygu7Xw.js +2 -0
- package/dist/web/assets/{team-member-window-content-B1jkKkDi.js → team-member-window-content-qrgFTC96.js} +1 -1
- package/dist/web/assets/{templ-gxNKXmt3.js → templ-D0zB42a9.js} +1 -1
- package/dist/web/assets/{terminal-tab-UvwM0sty.js → terminal-tab-D_-R5rGm.js} +3 -3
- package/dist/web/assets/{tex-C0ON27g5.js → tex-BJiPijcH.js} +1 -1
- package/dist/web/assets/{tool-cards-jB7GT-wj.js → tool-cards-BW7Xc20J.js} +4 -4
- package/dist/web/assets/treemap-KZPCXAKY-BobWhcHN.js +1 -0
- package/dist/web/assets/{ts-tags-BX_CTVgQ.js → ts-tags-Dcr4u6y5.js} +1 -1
- package/dist/web/assets/{twig-CwL7XnU0.js → twig-B-SQU0bg.js} +1 -1
- package/dist/web/assets/{use-blob-url-DcRcjNTE.js → use-blob-url-BtPlqwLr.js} +1 -1
- package/dist/web/assets/use-monaco-theme-DQY1ZSaf.js +1 -0
- package/dist/web/assets/{use-open-team-member-1hRsHLKg.js → use-open-team-member-C5LQ_TCM.js} +1 -1
- package/dist/web/assets/use-visual-viewport-pVxLfE5B.js +1 -0
- package/dist/web/assets/{use-websocket-rxJLbFqy.js → use-websocket-l39Odhlp.js} +1 -1
- package/dist/web/assets/{vendor-mermaid-CHTa6D69.js → vendor-mermaid-CtDsSxbM.js} +1 -1
- package/dist/web/assets/{video-preview-DMhnf2Gi.js → video-preview-C5AhkIDl.js} +1 -1
- package/dist/web/assets/{vue-Bykm6wSS.js → vue-Cw10Sfbh.js} +1 -1
- package/dist/web/assets/{vue-html-CETdK_iV.js → vue-html-Cc5C_Z_w.js} +1 -1
- package/dist/web/assets/{vue-vine-DiJb6fO5.js → vue-vine-BuNMlnvu.js} +1 -1
- package/dist/web/assets/window-pip-registry-C7oz3Dfy.js +1 -0
- package/dist/web/assets/window-store-B5ZswdYg.js +1 -0
- package/dist/web/assets/{xml-BNw8J5YW.js → xml-DqT1wkky.js} +1 -1
- package/dist/web/assets/{xsl-CsG9L31b.js → xsl-DLkge3A2.js} +1 -1
- package/dist/web/index.html +45 -42
- package/dist/web/sw.js +1 -1
- package/docs/architecture/ai-chat-and-providers.md +3 -0
- package/docs/architecture/workspace-and-ui.md +8 -0
- package/docs/design-guidelines.md +3 -2
- package/package.json +1 -1
- package/src/cli/commands/backup-cmd.ts +119 -0
- package/src/index.ts +3 -0
- package/src/providers/claude-agent-sdk.ts +29 -1
- package/src/server/index.ts +42 -15
- package/src/server/routes/accounts.ts +30 -2
- package/src/server/routes/chat.ts +8 -1
- package/src/server/routes/named-tunnel.ts +5 -2
- package/src/server/routes/remote-desktop.ts +64 -0
- package/src/server/routes/settings.ts +1 -0
- package/src/server/ws/chat.ts +126 -7
- package/src/server/ws/remote-desktop.ts +86 -0
- package/src/services/account.service.ts +106 -23
- package/src/services/db-backup/db-backup-paths.ts +66 -0
- package/src/services/db-backup/db-backup-restore.ts +105 -0
- package/src/services/db-backup/db-backup-retention.ts +96 -0
- package/src/services/db-backup/db-backup-sync.ts +113 -0
- package/src/services/db-backup/db-backup.service.ts +61 -0
- package/src/services/db.service.ts +29 -1
- package/src/services/fs-credential-path-guard.ts +19 -1
- package/src/services/named-tunnel/cloudflared-login.service.ts +2 -2
- package/src/services/nested-subagent-spy.ts +206 -0
- package/src/services/remote-desktop/access-unit-assembler.ts +90 -0
- package/src/services/remote-desktop/avc1-codec-string.ts +18 -0
- package/src/services/remote-desktop/nal-splitter.ts +100 -0
- package/src/services/remote-desktop/remote-desktop-capture.ts +120 -0
- package/src/services/remote-desktop/remote-desktop-encoder-args.ts +42 -0
- package/src/services/remote-desktop/remote-desktop-flag.ts +10 -0
- package/src/services/remote-desktop/remote-desktop-input.ts +217 -0
- package/src/services/remote-desktop/remote-desktop-nonce.ts +38 -0
- package/src/services/remote-desktop/remote-desktop-session.ts +189 -0
- package/src/services/remote-desktop/remote-desktop-vk-map.ts +41 -0
- package/src/services/subagent-transcript-merger.ts +92 -75
- package/src/services/subprocess-retention.ts +29 -6
- package/src/services/supervisor.ts +58 -0
- package/src/services/team-member-activity/subagent-transcript-index.ts +56 -0
- package/src/types/chat.ts +2 -0
- package/src/web/app.tsx +8 -0
- package/src/web/components/chat/account-dialogs.tsx +11 -4
- package/src/web/components/chat/question-card.tsx +9 -2
- package/src/web/components/chat/usage-badge.tsx +17 -3
- package/src/web/components/floating-window/floating-window.tsx +8 -1
- package/src/web/components/floating-window/open-tab-in-pip.ts +78 -0
- package/src/web/components/floating-window/window-content-registry.ts +2 -0
- package/src/web/components/floating-window/window-pip-registry.ts +46 -0
- package/src/web/components/floating-window/window-store-types.ts +1 -1
- package/src/web/components/layout/mobile-drawer.tsx +38 -27
- package/src/web/components/layout/nav-section-rail.tsx +10 -2
- package/src/web/components/layout/tab-pop-out-menu-item.tsx +25 -2
- package/src/web/components/remote-desktop/open-remote-desktop.ts +26 -0
- package/src/web/components/remote-desktop/remote-desktop-coords.ts +98 -0
- package/src/web/components/remote-desktop/remote-desktop-frame-timestamp.ts +21 -0
- package/src/web/components/remote-desktop/remote-desktop-gesture-classifiers.ts +56 -0
- package/src/web/components/remote-desktop/remote-desktop-mobile-key-bar.tsx +131 -0
- package/src/web/components/remote-desktop/remote-desktop-mobile-sheet.tsx +43 -0
- package/src/web/components/remote-desktop/remote-desktop-mobile-toolbar.tsx +79 -0
- package/src/web/components/remote-desktop/remote-desktop-mobile-view.tsx +190 -0
- package/src/web/components/remote-desktop/remote-desktop-stats-overlay.tsx +54 -0
- package/src/web/components/remote-desktop/remote-desktop-stats.ts +37 -0
- package/src/web/components/remote-desktop/remote-desktop-touch-gesture-tracker.ts +195 -0
- package/src/web/components/remote-desktop/remote-desktop-window-content.tsx +72 -0
- package/src/web/components/remote-desktop/remote-desktop-ws-url.ts +22 -0
- package/src/web/components/remote-desktop/use-h264-canvas-decoder.ts +175 -0
- package/src/web/components/remote-desktop/use-remote-desktop-available.ts +29 -0
- package/src/web/components/remote-desktop/use-remote-desktop-connection.ts +147 -0
- package/src/web/components/remote-desktop/use-remote-desktop-mobile-open-state.ts +18 -0
- package/src/web/components/remote-desktop/use-remote-desktop-touch.ts +224 -0
- package/src/web/components/remote-desktop/use-remote-desktop-virtual-keyboard.ts +50 -0
- package/src/web/components/remote-desktop/use-remote-input-capture.ts +92 -0
- package/src/web/components/tunnels/named-tunnel/named-tunnel-copy.ts +59 -59
- package/src/web/components/tunnels/named-tunnel/named-tunnel-section.tsx +2 -2
- package/src/web/hooks/use-notification-badge.ts +19 -11
- package/src/web/lib/feature-badges.ts +2 -0
- package/src/web/stores/settings-store.ts +13 -0
- package/src/web/stores/streaming-store.ts +21 -2
- package/dist/web/assets/api-client-BogqsFna.js +0 -1
- package/dist/web/assets/api-settings-DfXViPKz.js +0 -1
- package/dist/web/assets/architecture-PBZL5I3N-BzFm9ELu.js +0 -1
- package/dist/web/assets/chat-tab-DzK8LGti.js +0 -15
- package/dist/web/assets/code-editor-CuMfPN9X.js +0 -10
- package/dist/web/assets/diff-viewer-eaUBjLCH.js +0 -4
- package/dist/web/assets/explorer-body-DkCkSVrN.js +0 -2
- package/dist/web/assets/explorer-window-content-MsnTVngu.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-B72vcb2r.js +0 -1
- package/dist/web/assets/glide-data-grid-B0IY6n_Q.js +0 -138
- package/dist/web/assets/group-chat-tab-DRLDHOp6.js +0 -1
- package/dist/web/assets/index-CBn4SOoT.js +0 -52
- package/dist/web/assets/index-DWpNR3nB.css +0 -2
- package/dist/web/assets/info-3K5VOQVL-3irPxRK_.js +0 -1
- package/dist/web/assets/keybindings-store-d54pv2NK.js +0 -1
- package/dist/web/assets/markdown-renderer-BTS8ifdu.js +0 -1
- package/dist/web/assets/mobile-bottom-sheet-M5iZDX7y.js +0 -1
- package/dist/web/assets/mobile-explorer-sheet-BqK2-WuN.js +0 -1
- package/dist/web/assets/notification-store-Yvy1I8gd.js +0 -1
- package/dist/web/assets/packet-RMMSAZCW-Z5jWjFCV.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-Bq7UMi8P.js +0 -1
- package/dist/web/assets/process-table-BYwKigiD.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-BLo4BOTD.js +0 -1
- package/dist/web/assets/settings-store-GV5pkkke.js +0 -2
- package/dist/web/assets/settings-tab-BePvIcjd.js +0 -3
- package/dist/web/assets/settings-tab-Br-lJSPa.js +0 -1
- package/dist/web/assets/system-monitor-body-D8Z5QXBB.js +0 -2
- package/dist/web/assets/system-monitor-tab-BclYXFTe.js +0 -1
- package/dist/web/assets/tab-host-window-content-f_Rc5rJK.js +0 -1
- package/dist/web/assets/team-member-sheet-QDHoYCs0.js +0 -2
- package/dist/web/assets/treemap-KZPCXAKY-dyrE6SW2.js +0 -1
- package/dist/web/assets/use-monaco-theme--9tOOkDA.js +0 -1
- package/dist/web/assets/window-store-CQ1lghvv.js +0 -1
- /package/dist/web/assets/{abap-DGlrX_8G.js → abap-CKgBNk5K.js} +0 -0
- /package/dist/web/assets/{actionscript-3-CUqRtUJE.js → actionscript-3-oARhAY6c.js} +0 -0
- /package/dist/web/assets/{ada-CY3Spa33.js → ada-2vi8ASy3.js} +0 -0
- /package/dist/web/assets/{andromeeda-Lxs2TJn7.js → andromeeda-B3vMjPKd.js} +0 -0
- /package/dist/web/assets/{apache-DfxgGdgM.js → apache-DnIEr338.js} +0 -0
- /package/dist/web/assets/{apex-QVDF1dnI.js → apex-B7Qs5AY5.js} +0 -0
- /package/dist/web/assets/{api-client-oY1tC3tM.js → api-client-BkGKTjsq.js} +0 -0
- /package/dist/web/assets/{applescript-pwJ8QXh-.js → applescript-EbQNUCWb.js} +0 -0
- /package/dist/web/assets/{apply-theme-B_bMVL6a.js → apply-theme-Dd_cqGa-.js} +0 -0
- /package/dist/web/assets/{ara-mPjeX5YX.js → ara-CtHdhFRw.js} +0 -0
- /package/dist/web/assets/{asciidoc-Bf7LsDkI.js → asciidoc-BH6qin_O.js} +0 -0
- /package/dist/web/assets/{asm-rRKC1OUx.js → asm-D1XU1ACk.js} +0 -0
- /package/dist/web/assets/{aurora-x-DwQr0DkG.js → aurora-x-BkhcxkuS.js} +0 -0
- /package/dist/web/assets/{awk-Bpt768fN.js → awk-DijzB6o7.js} +0 -0
- /package/dist/web/assets/{ayu-dark-D9lwQHMs.js → ayu-dark-tysG77WC.js} +0 -0
- /package/dist/web/assets/{ayu-light-B4-ptrU5.js → ayu-light-Dg0WMETC.js} +0 -0
- /package/dist/web/assets/{ayu-mirage-DAyRzH5Q.js → ayu-mirage--RsJCjC4.js} +0 -0
- /package/dist/web/assets/{ballerina-Cgdpj1HC.js → ballerina-DT_5-l82.js} +0 -0
- /package/dist/web/assets/{bat-4FrN36S2.js → bat-BRpDlkv0.js} +0 -0
- /package/dist/web/assets/{beancount-DkcxQER3.js → beancount--8Df-WGG.js} +0 -0
- /package/dist/web/assets/{berry-Dnuut-Ml.js → berry-Bc9fo35J.js} +0 -0
- /package/dist/web/assets/{bibtex-CCDwmbuA.js → bibtex-CwyBpp9X.js} +0 -0
- /package/dist/web/assets/{bicep-BC0ryoGM.js → bicep-DiyH2Bs7.js} +0 -0
- /package/dist/web/assets/{bird2-C50V_A-0.js → bird2-CHgK4a2A.js} +0 -0
- /package/dist/web/assets/{c-Ddorhq0U.js → c-BM1qL6-f.js} +0 -0
- /package/dist/web/assets/{c3-DdIf5r9E.js → c3-D2oOjrP7.js} +0 -0
- /package/dist/web/assets/{cadence-s9pd1Wng.js → cadence-nS45wqF6.js} +0 -0
- /package/dist/web/assets/{can-open-in-ppm-C6hZV1OP.js → can-open-in-ppm-DwP9KU8o.js} +0 -0
- /package/dist/web/assets/{catppuccin-frappe-CzJNTyu-.js → catppuccin-frappe-BpnJHiCj.js} +0 -0
- /package/dist/web/assets/{catppuccin-latte-BNpt-m7s.js → catppuccin-latte-Bb95Wj3h.js} +0 -0
- /package/dist/web/assets/{catppuccin-macchiato-FYGkfsJ5.js → catppuccin-macchiato-Btx8c17W.js} +0 -0
- /package/dist/web/assets/{catppuccin-mocha-it2g5D1q.js → catppuccin-mocha-B4DZOUGI.js} +0 -0
- /package/dist/web/assets/{clarity-BVFBTjMe.js → clarity-1ps69jU6.js} +0 -0
- /package/dist/web/assets/{clipboard-BWznymz7.js → clipboard-CrHX8AYw.js} +0 -0
- /package/dist/web/assets/{clojure-BSYvXMI8.js → clojure-BklC0hKV.js} +0 -0
- /package/dist/web/assets/{cmake-DGEqH-Np.js → cmake-B573LYAQ.js} +0 -0
- /package/dist/web/assets/{codeowners-D8FCOxWg.js → codeowners-BoYeX89M.js} +0 -0
- /package/dist/web/assets/{codeql-COV0eluG.js → codeql-7Dhf5Udk.js} +0 -0
- /package/dist/web/assets/{common-lisp-xHnZUlgt.js → common-lisp-m1nxlthv.js} +0 -0
- /package/dist/web/assets/{coq-Cs_2i3Xg.js → coq-DNhGmd4a.js} +0 -0
- /package/dist/web/assets/{csharp-B1ONB4PT.js → csharp-P-nAurtS.js} +0 -0
- /package/dist/web/assets/{css-BgGQbvrZ.js → css-LYbSeh3-.js} +0 -0
- /package/dist/web/assets/{csv-CNHmQBzs.js → csv-DHZZZm7X.js} +0 -0
- /package/dist/web/assets/{csv-parser-DWR24J17.js → csv-parser-CY9Ggtg5.js} +0 -0
- /package/dist/web/assets/{cue-ZXAKzwIy.js → cue-D8yLUOle.js} +0 -0
- /package/dist/web/assets/{cypher-4PL5bmT5.js → cypher-9cbW8RYA.js} +0 -0
- /package/dist/web/assets/{d-CEsV3RT2.js → d-ZihzhxqI.js} +0 -0
- /package/dist/web/assets/{dark-plus-NtFnYSDz.js → dark-plus-C8xIA1u3.js} +0 -0
- /package/dist/web/assets/{dart-CKHn4mDf.js → dart-CBm6jKc7.js} +0 -0
- /package/dist/web/assets/{data-grid-types-CxU8oo-j.js → data-grid-types-DkEQ-Na9.js} +0 -0
- /package/dist/web/assets/{dax-Cmkj1I_M.js → dax-B3cx6YeV.js} +0 -0
- /package/dist/web/assets/{desktop-DJLalvPQ.js → desktop-C03SpXth.js} +0 -0
- /package/dist/web/assets/{diff-BnS2wpj6.js → diff-D8LnnvFY.js} +0 -0
- /package/dist/web/assets/{dist-uYjs3Wv6.js → dist-C4XoS_zo.js} +0 -0
- /package/dist/web/assets/{dist-CuIdRyUB.js → dist-Do3fljss.js} +0 -0
- /package/dist/web/assets/{docker-CovNy7ml.js → docker-DwFiOQWW.js} +0 -0
- /package/dist/web/assets/{dotenv-Gh4w3I6B.js → dotenv-CVJ_vFSQ.js} +0 -0
- /package/dist/web/assets/{dracula-NF0tAHXl.js → dracula-DMw8UACY.js} +0 -0
- /package/dist/web/assets/{dracula-soft-Cj_qBurK.js → dracula-soft-BnB3nDrA.js} +0 -0
- /package/dist/web/assets/{dream-maker-CRuZPcl7.js → dream-maker-2riXUQXo.js} +0 -0
- /package/dist/web/assets/{emacs-lisp-BACmC9qj.js → emacs-lisp-BQdYaiB5.js} +0 -0
- /package/dist/web/assets/{everforest-dark-Dd9av4QG.js → everforest-dark-C2FPIWdj.js} +0 -0
- /package/dist/web/assets/{everforest-light-C-jsss-B.js → everforest-light-DMOWf9sD.js} +0 -0
- /package/dist/web/assets/{fennel-BJA9XnL8.js → fennel-JtSHp0vW.js} +0 -0
- /package/dist/web/assets/{fish-CKlhNSc2.js → fish-CflDcRfe.js} +0 -0
- /package/dist/web/assets/{fluent-wfglRk5D.js → fluent-D4eu6Vac.js} +0 -0
- /package/dist/web/assets/{format-bytes-utwHMTry.js → format-bytes-DR3228bu.js} +0 -0
- /package/dist/web/assets/{fortran-free-form-BlXCdid3.js → fortran-free-form-hO4RuC9Z.js} +0 -0
- /package/dist/web/assets/{gdscript-BwAVG6Et.js → gdscript-D7n3a8VM.js} +0 -0
- /package/dist/web/assets/{gdshader-DPc_T7ib.js → gdshader-BhUmrlzG.js} +0 -0
- /package/dist/web/assets/{genie-B-zZSLCX.js → genie-VbyftqBp.js} +0 -0
- /package/dist/web/assets/{gherkin-CJ0ajpGs.js → gherkin-BZzhvUUb.js} +0 -0
- /package/dist/web/assets/{github-dark-Dz8ZkzSP.js → github-dark-C4q41woW.js} +0 -0
- /package/dist/web/assets/{github-dark-default-CNUYo_kp.js → github-dark-default-CMNi7dQX.js} +0 -0
- /package/dist/web/assets/{github-dark-dimmed-241z_Ycx.js → github-dark-dimmed-BRf_-LlY.js} +0 -0
- /package/dist/web/assets/{github-dark-high-contrast-7r6yZZYH.js → github-dark-high-contrast-xanKBsRI.js} +0 -0
- /package/dist/web/assets/{github-light-C8gmZn-D.js → github-light-BEQeNQCs.js} +0 -0
- /package/dist/web/assets/{github-light-default-GAEgq1eA.js → github-light-default-ceQrtdXg.js} +0 -0
- /package/dist/web/assets/{github-light-high-contrast-3z91352E.js → github-light-high-contrast-doKbXzgW.js} +0 -0
- /package/dist/web/assets/{gleam-C7vxW5zd.js → gleam-Chzqf4fT.js} +0 -0
- /package/dist/web/assets/{gn-CyJIXwHP.js → gn-CCN_-6fp.js} +0 -0
- /package/dist/web/assets/{gnuplot-CqLbGgPd.js → gnuplot-CIplgwwP.js} +0 -0
- /package/dist/web/assets/{go-DdDcK9aX.js → go-Cjj-9dRs.js} +0 -0
- /package/dist/web/assets/{groovy-UYHggEqH.js → groovy-PqD4K7X1.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-hard-B4ierkJn.js → gruvbox-dark-hard-BAbpUSF-.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-medium-DYwkDSZ6.js → gruvbox-dark-medium-BWcWXDBB.js} +0 -0
- /package/dist/web/assets/{gruvbox-dark-soft-CVL4P-Lz.js → gruvbox-dark-soft-Dkx1okkk.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-hard-BJ0LewUZ.js → gruvbox-light-hard-WVsDK0MM.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-medium-CaqqjeGJ.js → gruvbox-light-medium-tiR9t7gr.js} +0 -0
- /package/dist/web/assets/{gruvbox-light-soft-ByKMoVNr.js → gruvbox-light-soft-HzHohaX6.js} +0 -0
- /package/dist/web/assets/{haskell-BilYRVPp.js → haskell-B1eIZfr6.js} +0 -0
- /package/dist/web/assets/{haxe-Brs9Bj1s.js → haxe-BcDdi5xr.js} +0 -0
- /package/dist/web/assets/{hcl-ClrZpol-.js → hcl-Dq-HKVni.js} +0 -0
- /package/dist/web/assets/{hjson-9PvHnYMX.js → hjson-Dr9D9WVU.js} +0 -0
- /package/dist/web/assets/{hlsl-BbBk4vqV.js → hlsl-Bo8t2FuZ.js} +0 -0
- /package/dist/web/assets/{horizon-E5Py0cny.js → horizon-DWG5IXc-.js} +0 -0
- /package/dist/web/assets/{horizon-bright-DVCsNGEk.js → horizon-bright-DFr9wTTx.js} +0 -0
- /package/dist/web/assets/{houston-JdFEax9Y.js → houston-NAVNhkup.js} +0 -0
- /package/dist/web/assets/{hy-W_W3tLKT.js → hy-DnyvUk3B.js} +0 -0
- /package/dist/web/assets/{image-gallery-5IURd6db.js → image-gallery-DoFdNF9i.js} +0 -0
- /package/dist/web/assets/{imba-CfAqJz7T.js → imba-CPbR5W_A.js} +0 -0
- /package/dist/web/assets/{ini-C_3gvKWs.js → ini-lc8_Lt0y.js} +0 -0
- /package/dist/web/assets/{java-CKqtXZ-j.js → java-B0KCo2vK.js} +0 -0
- /package/dist/web/assets/{javascript-C7gSEfae.js → javascript-YqEbNRpd.js} +0 -0
- /package/dist/web/assets/{json-BoN6e11T.js → json-Co0WtOlh.js} +0 -0
- /package/dist/web/assets/{json5-CgE7C8HW.js → json5-2Jhb0QUN.js} +0 -0
- /package/dist/web/assets/{jsonc-DjyoattY.js → jsonc-e72F11nU.js} +0 -0
- /package/dist/web/assets/{jsonl-lI82wGSw.js → jsonl-Bff8IPye.js} +0 -0
- /package/dist/web/assets/{jsonnet-CszEI8va.js → jsonnet-DRCkb-_H.js} +0 -0
- /package/dist/web/assets/{jssm-CZXDlszb.js → jssm-C6UiwVEm.js} +0 -0
- /package/dist/web/assets/{jsx-BeRz0eEz.js → jsx-u1aZlnfA.js} +0 -0
- /package/dist/web/assets/{kanagawa-dragon-BOmnUiyd.js → kanagawa-dragon-De-RQFV3.js} +0 -0
- /package/dist/web/assets/{kanagawa-lotus-ffU_P5w-.js → kanagawa-lotus-K8elE4QU.js} +0 -0
- /package/dist/web/assets/{kanagawa-wave-DKTJZrMu.js → kanagawa-wave-BrStl_zx.js} +0 -0
- /package/dist/web/assets/{kdl-BWECJveH.js → kdl-CLCfMeZy.js} +0 -0
- /package/dist/web/assets/{kotlin-DpVwBCSt.js → kotlin-DEEC3pNh.js} +0 -0
- /package/dist/web/assets/{kusto-BgVqjUZA.js → kusto-DTqRhCup.js} +0 -0
- /package/dist/web/assets/{laserwave-CEVCi8em.js → laserwave-DsgpUSbA.js} +0 -0
- /package/dist/web/assets/{lean-CGyfRhLJ.js → lean-Dl9MI0Y3.js} +0 -0
- /package/dist/web/assets/{less-B0_H10Ku.js → less-Bg21rao6.js} +0 -0
- /package/dist/web/assets/{lib-hrC3bruJ.js → lib-CXUwxw1M.js} +0 -0
- /package/dist/web/assets/{libesm-DVpFmwJu.js → libesm-CVkncAQz.js} +0 -0
- /package/dist/web/assets/{light-plus-DqzMqPID.js → light-plus-DGNbbK1V.js} +0 -0
- /package/dist/web/assets/{llvm-9ucnfNiF.js → llvm-91_ZX3p-.js} +0 -0
- /package/dist/web/assets/{loader-circle-Dpqcv0eS.js → loader-circle-DouKgIXy.js} +0 -0
- /package/dist/web/assets/{lock-DldbT31x.js → lock-CNxUGpTk.js} +0 -0
- /package/dist/web/assets/{log-m6A2dOSj.js → log-DSXzbq8u.js} +0 -0
- /package/dist/web/assets/{logo-ByPcsC9P.js → logo-BQRNd4tc.js} +0 -0
- /package/dist/web/assets/{luau-DFCo8nBn.js → luau-BE-TMpiZ.js} +0 -0
- /package/dist/web/assets/{make-C90w3MeA.js → make-ePYGKTpc.js} +0 -0
- /package/dist/web/assets/{markdown-DT0uywZG.js → markdown-Cst3wgMW.js} +0 -0
- /package/dist/web/assets/{material-theme-BVUO93tJ.js → material-theme-9XyvnNLb.js} +0 -0
- /package/dist/web/assets/{material-theme-darker-xXgGDwnF.js → material-theme-darker-H1VN4MTS.js} +0 -0
- /package/dist/web/assets/{material-theme-lighter-BjStPzWB.js → material-theme-lighter-DAreBbuF.js} +0 -0
- /package/dist/web/assets/{material-theme-ocean-wFmSiKjy.js → material-theme-ocean-BpTR-3II.js} +0 -0
- /package/dist/web/assets/{material-theme-palenight-e-0CtRoe.js → material-theme-palenight-DUNzX711.js} +0 -0
- /package/dist/web/assets/{matlab-BNaNQAUH.js → matlab-BpYNufBU.js} +0 -0
- /package/dist/web/assets/{mdx-DfpVWAqO.js → mdx-BYjfe1DK.js} +0 -0
- /package/dist/web/assets/{mermaid-Bpq7wFCP.js → mermaid-DPCcIW9B.js} +0 -0
- /package/dist/web/assets/{min-dark-CL2Y0WH6.js → min-dark-DnGOAGCo.js} +0 -0
- /package/dist/web/assets/{min-light-DOgr_IdP.js → min-light-CwV-o31y.js} +0 -0
- /package/dist/web/assets/{mipsasm-C9ie2zlB.js → mipsasm-DdeCPC97.js} +0 -0
- /package/dist/web/assets/{mojo-Md2A2sJy.js → mojo-BmxxZ3iM.js} +0 -0
- /package/dist/web/assets/{monitor-4vWfqzuN.js → monitor-C835ucWH.js} +0 -0
- /package/dist/web/assets/{monokai-nbXIEySB.js → monokai-Cx6cQ-MA.js} +0 -0
- /package/dist/web/assets/{moonbit-CJ-T-s4A.js → moonbit-D2-Gj5vj.js} +0 -0
- /package/dist/web/assets/{move-DwEehLIK.js → move-DSWGoQa8.js} +0 -0
- /package/dist/web/assets/{narrat-CuF4sIKr.js → narrat-D9TnZSSu.js} +0 -0
- /package/dist/web/assets/{nextflow-groovy-1dYhfPfq.js → nextflow-groovy-BbL-8PwD.js} +0 -0
- /package/dist/web/assets/{night-owl-gZS5mfzY.js → night-owl-CZitKm5d.js} +0 -0
- /package/dist/web/assets/{night-owl-light-DDAster5.js → night-owl-light-DJVX1Pno.js} +0 -0
- /package/dist/web/assets/{nix-BlbEYs2c.js → nix-R6hr3oln.js} +0 -0
- /package/dist/web/assets/{nord-BRiPb8Vy.js → nord-ChGHya3l.js} +0 -0
- /package/dist/web/assets/{nushell-DgWicB1i.js → nushell-QQWB-GWx.js} +0 -0
- /package/dist/web/assets/{objective-c-D8mPZ42Y.js → objective-c-C0pZ6y73.js} +0 -0
- /package/dist/web/assets/{objective-cpp-B0pNXzuj.js → objective-cpp-BdzVM31a.js} +0 -0
- /package/dist/web/assets/{ocaml-lQd7aK5d.js → ocaml-D4KktNlS.js} +0 -0
- /package/dist/web/assets/{odin-CxtIOBjm.js → odin-Y-fypmGT.js} +0 -0
- /package/dist/web/assets/{one-dark-pro-T_UFnWfe.js → one-dark-pro-BwecIxQL.js} +0 -0
- /package/dist/web/assets/{one-light-LBVqwyV-.js → one-light-Cz7UVzLU.js} +0 -0
- /package/dist/web/assets/{openscad-C5dnS6J7.js → openscad-DtRe3qYD.js} +0 -0
- /package/dist/web/assets/{pascal-CmWCWQML.js → pascal-Ck6I1fGG.js} +0 -0
- /package/dist/web/assets/{pkl-BWWOtDQk.js → pkl-DdFK94VW.js} +0 -0
- /package/dist/web/assets/{plastic-ekhupckF.js → plastic-BQSAzZwR.js} +0 -0
- /package/dist/web/assets/{play-CpCAoQRZ.js → play-CJlWpVMw.js} +0 -0
- /package/dist/web/assets/{plsql-BPsDHXx4.js → plsql-DjXg_CP0.js} +0 -0
- /package/dist/web/assets/{po-BX2FiuW2.js → po-Bw2SuM4r.js} +0 -0
- /package/dist/web/assets/{poimandres-DzXOdZcN.js → poimandres-YMK6c5VN.js} +0 -0
- /package/dist/web/assets/{polar-DDRXgJzG.js → polar-BMu17-O0.js} +0 -0
- /package/dist/web/assets/{portal-container-context-a399Hd9F.js → portal-container-context-DSGdlXrJ.js} +0 -0
- /package/dist/web/assets/{postcss-BHW6XBhx.js → postcss-DWib10RZ.js} +0 -0
- /package/dist/web/assets/{powerquery-B89c2NBb.js → powerquery-Dht4CK0b.js} +0 -0
- /package/dist/web/assets/{powershell-Dvxrd3Tu.js → powershell-DvrCg8ji.js} +0 -0
- /package/dist/web/assets/{prisma-lMK8EFLU.js → prisma-DbodYvCM.js} +0 -0
- /package/dist/web/assets/{prolog-D0YrtMG4.js → prolog-DZ-m6ysB.js} +0 -0
- /package/dist/web/assets/{proto-J0mM25aw.js → proto-HHpG3tGO.js} +0 -0
- /package/dist/web/assets/{puppet-CUT0xkhh.js → puppet-BljHe_75.js} +0 -0
- /package/dist/web/assets/{purescript-CoJWSGRp.js → purescript-DzCiFR0B.js} +0 -0
- /package/dist/web/assets/{python-BBrmuz6P.js → python-B5BQ27cC.js} +0 -0
- /package/dist/web/assets/{qmldir-CUtzs4Q7.js → qmldir-CYlcXrnn.js} +0 -0
- /package/dist/web/assets/{qss-Dm1RZ8aL.js → qss-Ce7T_TEg.js} +0 -0
- /package/dist/web/assets/{r-BGP7qZTe.js → r-Bku-2Jp_.js} +0 -0
- /package/dist/web/assets/{racket-K_6U1lqJ.js → racket-DtfSL3WR.js} +0 -0
- /package/dist/web/assets/{raku-bNSiZeW_.js → raku-CnDLsBJr.js} +0 -0
- /package/dist/web/assets/{react-X4jUntEf.js → react-Dfio3PfK.js} +0 -0
- /package/dist/web/assets/{red-uVeIEF0f.js → red-vbqcE9b-.js} +0 -0
- /package/dist/web/assets/{refresh-cw-B9iv7csb.js → refresh-cw-84mz8aOE.js} +0 -0
- /package/dist/web/assets/{reg-DSm9GWjh.js → reg-DIDLT8-A.js} +0 -0
- /package/dist/web/assets/{regexp-CljRm_j_.js → regexp-CB4h-VPu.js} +0 -0
- /package/dist/web/assets/{rel-d3JcopZg.js → rel-CiQEnWXa.js} +0 -0
- /package/dist/web/assets/{resolve-theme-DLKmm-lE.js → resolve-theme-DPTQHAy1.js} +0 -0
- /package/dist/web/assets/{riscv-CJmaELjT.js → riscv-R78iq5Rf.js} +0 -0
- /package/dist/web/assets/{ron-D0g9B1rW.js → ron-DhYENxc4.js} +0 -0
- /package/dist/web/assets/{rose-pine-DOrD2BSH.js → rose-pine-5wBeyjfJ.js} +0 -0
- /package/dist/web/assets/{rose-pine-dawn-CzZQ-LCF.js → rose-pine-dawn-FpKtPCuH.js} +0 -0
- /package/dist/web/assets/{rose-pine-moon-CdmsYp7C.js → rose-pine-moon-BwcIYc7C.js} +0 -0
- /package/dist/web/assets/{rosmsg-CmDth6pO.js → rosmsg-DWIGAYMl.js} +0 -0
- /package/dist/web/assets/{rotate-ccw-DnuPzJRE.js → rotate-ccw-CNTk2J7l.js} +0 -0
- /package/dist/web/assets/{rust-DGdP6jiR.js → rust-DU_ofKgt.js} +0 -0
- /package/dist/web/assets/{sass-Dkgt2FKZ.js → sass-U9SUuYEy.js} +0 -0
- /package/dist/web/assets/{save-DxonD4y1.js → save-CtW9ojKk.js} +0 -0
- /package/dist/web/assets/{scala-BQLTON6C.js → scala-op4gjA6j.js} +0 -0
- /package/dist/web/assets/{scheme-DKlxUGCi.js → scheme-tEjkR8rL.js} +0 -0
- /package/dist/web/assets/{sdbl-C3iZJQeZ.js → sdbl-C6SSRmZg.js} +0 -0
- /package/dist/web/assets/{send-B-t5Mz26.js → send-C_sLFt56.js} +0 -0
- /package/dist/web/assets/{shallow-jwPzEunk.js → shallow-C1YoJQA-.js} +0 -0
- /package/dist/web/assets/{shellscript-BRu-ZQAx.js → shellscript-Bf2KiwQj.js} +0 -0
- /package/dist/web/assets/{shield-check-CdTiK1j2.js → shield-check-Du6HKYn9.js} +0 -0
- /package/dist/web/assets/{shield-off-DuetkXKM.js → shield-off-CWKb1MQL.js} +0 -0
- /package/dist/web/assets/{slack-dark-qkm_u2zz.js → slack-dark-oGgCey8r.js} +0 -0
- /package/dist/web/assets/{slack-ochin-BP8_8e4m.js → slack-ochin-CemlB8cE.js} +0 -0
- /package/dist/web/assets/{smalltalk-Bnz4gt07.js → smalltalk-CTE-6jrZ.js} +0 -0
- /package/dist/web/assets/{snazzy-light-fbPZg5io.js → snazzy-light-DoEIGnmz.js} +0 -0
- /package/dist/web/assets/{solarized-dark-CM0vTRoi.js → solarized-dark-CStiJyBg.js} +0 -0
- /package/dist/web/assets/{solarized-light-C4_W9mvs.js → solarized-light-CfuNycS9.js} +0 -0
- /package/dist/web/assets/{solidity-Bz6v1PoD.js → solidity-DL6sO5rd.js} +0 -0
- /package/dist/web/assets/{splunk-DyNxu-0i.js → splunk-yK4ZUFBB.js} +0 -0
- /package/dist/web/assets/{sql-DXRL1gw1.js → sql-Lw03eoN8.js} +0 -0
- /package/dist/web/assets/{ssh-config-2z1QHuOG.js → ssh-config-DeRp25TR.js} +0 -0
- /package/dist/web/assets/{stylus-D4I-VaeY.js → stylus-D8OMsPh-.js} +0 -0
- /package/dist/web/assets/{swift-C_2G-tIc.js → swift-nsl9stPV.js} +0 -0
- /package/dist/web/assets/{synthwave-84-osBjChRP.js → synthwave-84-exAgsgpf.js} +0 -0
- /package/dist/web/assets/{system-verilog-zQP2qPNU.js → system-verilog-DBWf00T6.js} +0 -0
- /package/dist/web/assets/{systemd-UGfjRSBM.js → systemd-nhu27Dw5.js} +0 -0
- /package/dist/web/assets/{table-FLU8ei8b.js → table-GAR4upb6.js} +0 -0
- /package/dist/web/assets/{talonscript-C1XrFPwo.js → talonscript-CNOEcGDc.js} +0 -0
- /package/dist/web/assets/{tasl-B5raVn43.js → tasl-CjYZenSH.js} +0 -0
- /package/dist/web/assets/{tcl-3-vbeMIc.js → tcl-BVA09RDz.js} +0 -0
- /package/dist/web/assets/{terraform-a8VXZDm9.js → terraform-C5EloH1q.js} +0 -0
- /package/dist/web/assets/{text-wrap-B05VGRa-.js → text-wrap-D71Jqz5U.js} +0 -0
- /package/dist/web/assets/{tokyo-night-4SRhf-Hz.js → tokyo-night-D764DdhO.js} +0 -0
- /package/dist/web/assets/{toml-B4N0i8wT.js → toml-CCDYblSB.js} +0 -0
- /package/dist/web/assets/{trash-2-CTXlTflF.js → trash-2-C08fhIg1.js} +0 -0
- /package/dist/web/assets/{triangle-alert-BRq0VAfd.js → triangle-alert-85BQgs3T.js} +0 -0
- /package/dist/web/assets/{tsv-DA5k9Lvw.js → tsv-BZ4sA05e.js} +0 -0
- /package/dist/web/assets/{tsx-D9SZ8owC.js → tsx-DPLBzAt5.js} +0 -0
- /package/dist/web/assets/{turtle-BTHTvyY_.js → turtle-CwGHCq40.js} +0 -0
- /package/dist/web/assets/{typescript-DYzV9p5L.js → typescript-CUH0vBAl.js} +0 -0
- /package/dist/web/assets/{typespec-BC15eh7F.js → typespec-Bq31Vq4u.js} +0 -0
- /package/dist/web/assets/{typst-BJUqzA6D.js → typst-MD9BR9W5.js} +0 -0
- /package/dist/web/assets/{use-is-mobile-DFWViA4S.js → use-is-mobile-DUuki97q.js} +0 -0
- /package/dist/web/assets/{utils-C9OjCrou.js → utils-u2fbYPrY.js} +0 -0
- /package/dist/web/assets/{v-50Lps6oA.js → v-DZV7IxIT.js} +0 -0
- /package/dist/web/assets/{vala-8wiUvvrD.js → vala-urd1zi9S.js} +0 -0
- /package/dist/web/assets/{vb-BlgPLt6B.js → vb-DIZU3zbJ.js} +0 -0
- /package/dist/web/assets/{vendor-xterm-BZOrwF_F.js → vendor-xterm-DCj0zgSC.js} +0 -0
- /package/dist/web/assets/{verilog-CD9pVW5p.js → verilog-Ndz_ciTA.js} +0 -0
- /package/dist/web/assets/{vesper-BrivW4PT.js → vesper-C86M7LJp.js} +0 -0
- /package/dist/web/assets/{vhdl-Bunxfa7N.js → vhdl-_klwDUjd.js} +0 -0
- /package/dist/web/assets/{viml-DndUb2qM.js → viml-DzRXWoQh.js} +0 -0
- /package/dist/web/assets/{vitesse-black-BurZSkxW.js → vitesse-black-TWZ20_3U.js} +0 -0
- /package/dist/web/assets/{vitesse-dark-8UShspzC.js → vitesse-dark-Dl7R6fy9.js} +0 -0
- /package/dist/web/assets/{vitesse-light-DghCAZ90.js → vitesse-light-Cs0Dt2_n.js} +0 -0
- /package/dist/web/assets/{vyper-b-Clr-8u.js → vyper-qnCVj8ia.js} +0 -0
- /package/dist/web/assets/{wasm-CpcKStkw.js → wasm-Ckx9jBPe.js} +0 -0
- /package/dist/web/assets/{wasm-C5v2T06Y.js → wasm-Dy-SJ3W9.js} +0 -0
- /package/dist/web/assets/{wenyan-Dz2srLZR.js → wenyan-DTEy2Lx-.js} +0 -0
- /package/dist/web/assets/{wgsl-CeD1-dRb.js → wgsl-DC7n63Sy.js} +0 -0
- /package/dist/web/assets/{wikitext-CyeS-9FH.js → wikitext-1zFoZiW6.js} +0 -0
- /package/dist/web/assets/{wit-Kr-q-qBQ.js → wit-DtTfCRph.js} +0 -0
- /package/dist/web/assets/{wolfram-BKm8bmln.js → wolfram-B2Bgu1KL.js} +0 -0
- /package/dist/web/assets/{x-YDu4m2Iq.js → x-CeN0Qybc.js} +0 -0
- /package/dist/web/assets/{yaml-2XSxomjM.js → yaml-B-Jxk0IQ.js} +0 -0
- /package/dist/web/assets/{zenscript-CKIS34vf.js → zenscript-BWRfvNER.js} +0 -0
- /package/dist/web/assets/{zig-DdHGNllX.js → zig-CkwuvETm.js} +0 -0
package/src/server/ws/chat.ts
CHANGED
|
@@ -10,11 +10,13 @@ import type { ChatWsClientMessage, SessionPhase } from "../../types/api.ts";
|
|
|
10
10
|
// — a chat socket is not guaranteed to exist now that chat tabs mount lazily.
|
|
11
11
|
import { broadcastGlobalEvent } from "./global.ts";
|
|
12
12
|
import { bashOutputSpy } from "../../services/bash-output-spy.ts";
|
|
13
|
+
import { nestedSubagentSpy } from "../../services/nested-subagent-spy.ts";
|
|
14
|
+
import { resolveSessionDir } from "../../services/subagent-transcript-merger.ts";
|
|
13
15
|
import { backgroundShellRegistry } from "../../services/background-shell-registry.ts";
|
|
14
16
|
import { basename } from "node:path";
|
|
15
17
|
import { configService } from "../../services/config.service.ts";
|
|
16
18
|
import { formatTurnUsageLog } from "../../shared/turn-usage.ts";
|
|
17
|
-
import { isTerminalAgentStatus } from "../../shared/background-agent-status.ts";
|
|
19
|
+
import { isAsyncAgentLaunchAck, isTerminalAgentStatus } from "../../shared/background-agent-status.ts";
|
|
18
20
|
import { cacheReleaseDelayMs, selectWarmIdleEvictions } from "../../services/subprocess-retention.ts";
|
|
19
21
|
|
|
20
22
|
/** Resolve the SESSION's provider config — not the global default provider's.
|
|
@@ -47,10 +49,33 @@ function resolveSessionThinkingEnabled(sessionId: string): boolean {
|
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
const PING_INTERVAL_MS = 15_000; // 15s keepalive
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
/**
|
|
53
|
+
* When an abandoned session's entry is dropped.
|
|
54
|
+
*
|
|
55
|
+
* Deliberately *not* derived from the cache window. This timer also drops `activeSessions`
|
|
56
|
+
* — which carries up to MAX_TURN_EVENTS buffered events per session, tool results included —
|
|
57
|
+
* plus team watchers and the background-shell registry. Stretching it to the cache window
|
|
58
|
+
* held all of that for an hour to protect a subprocess it knows nothing about.
|
|
59
|
+
*
|
|
60
|
+
* It still must not preempt `scheduleSubprocessRelease`, so when that release is pending the
|
|
61
|
+
* timer reschedules itself instead of running. The two stay independent, and the long wait
|
|
62
|
+
* happens only for sessions that actually have a warm subprocess to protect.
|
|
63
|
+
*/
|
|
64
|
+
const CLEANUP_TIMEOUT_MS = 5 * 60_000;
|
|
65
|
+
/**
|
|
66
|
+
* How many clientless sessions may hold a live SDK subprocess at once.
|
|
67
|
+
*
|
|
68
|
+
* This, not the retention window, is what bounds the memory: a longer window keeps these
|
|
69
|
+
* slots filled for longer but never adds a slot. At ~350MB per subprocess, 10 costs ~3.5GB.
|
|
70
|
+
*/
|
|
71
|
+
const MAX_WARM_IDLE_SESSIONS = 10;
|
|
53
72
|
const MAX_TURN_EVENTS = 10_000; // memory safety cap
|
|
73
|
+
/**
|
|
74
|
+
* Share of the turn buffer nested-agent children may take. They are restored
|
|
75
|
+
* from disk on reload anyway; this only keeps a chatty grandchild from evicting
|
|
76
|
+
* the turn's own later events out of a reconnecting client's replay.
|
|
77
|
+
*/
|
|
78
|
+
const MAX_NESTED_TURN_EVENTS = 2_000;
|
|
54
79
|
const BUFFERABLE_TYPES = new Set([
|
|
55
80
|
"text", "thinking", "tool_use", "tool_result",
|
|
56
81
|
"approval_request", "error", "done", "account_info", "account_retry",
|
|
@@ -91,8 +116,10 @@ interface SessionEntry {
|
|
|
91
116
|
lastImplicitTeamProbe?: number;
|
|
92
117
|
/** Compact indicator state — sticky until turn ends or boundary received, synced on reconnect */
|
|
93
118
|
compactStatus?: "compacting" | null;
|
|
94
|
-
/** toolUseIds of Bash calls launched with run_in_background — spy
|
|
119
|
+
/** toolUseIds of Bash/Agent calls launched with run_in_background — their spy outlives the tool_result */
|
|
95
120
|
backgroundToolUseIds?: Set<string>;
|
|
121
|
+
/** Nested-agent children buffered into turnEvents this turn (see MAX_NESTED_TURN_EVENTS) */
|
|
122
|
+
nestedBuffered?: number;
|
|
96
123
|
/** When the last client left, for evicting the least recently used warm subprocess */
|
|
97
124
|
idleSince?: number;
|
|
98
125
|
/** When the last turn completed — the moment this session's prompt cache was last written */
|
|
@@ -116,7 +143,19 @@ function listWarmIdleSessions(): { sessionId: string; entry: SessionEntry; idleS
|
|
|
116
143
|
/** Release a session's subprocess, if it still has one and nobody is using it. */
|
|
117
144
|
function releaseSubprocess(sessionId: string, reason: string, note: string): void {
|
|
118
145
|
const entry = activeSessions.get(sessionId);
|
|
119
|
-
if (!entry
|
|
146
|
+
if (!entry) return;
|
|
147
|
+
// Disarm before deciding anything: past this point the pending release is either being
|
|
148
|
+
// performed or is moot, and a timer left armed is not merely stale. startCleanupTimer
|
|
149
|
+
// reschedules itself while `cacheReleaseTimer` is set, so an entry evicted by
|
|
150
|
+
// enforceWarmIdleCap — which calls straight in here — would outlive its own subprocess by
|
|
151
|
+
// up to the full TTL, holding its turnEvents buffer, ping interval, team watchers and
|
|
152
|
+
// shell registry. That is the accumulation the 5-minute cleanup exists to stop, on the one
|
|
153
|
+
// path that only runs when memory is already tight. Clearing here covers every caller.
|
|
154
|
+
if (entry.cacheReleaseTimer) {
|
|
155
|
+
clearTimeout(entry.cacheReleaseTimer);
|
|
156
|
+
entry.cacheReleaseTimer = undefined;
|
|
157
|
+
}
|
|
158
|
+
if (entry.clients.size > 0 || entry.isStreamingActive) return;
|
|
120
159
|
const provider = providerRegistry.get(entry.providerId);
|
|
121
160
|
if (!provider?.hasStreamingSession?.(sessionId)) return;
|
|
122
161
|
provider.abortQuery?.(sessionId, reason);
|
|
@@ -124,6 +163,30 @@ function releaseSubprocess(sessionId: string, reason: string, note: string): voi
|
|
|
124
163
|
logSessionEvent(sessionId, "INFO", note);
|
|
125
164
|
}
|
|
126
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Drop a session's live subprocess so its next turn is rebuilt from the transcript on disk.
|
|
168
|
+
*
|
|
169
|
+
* Routes that rewrite the JSONL need this. The subprocess holds the conversation in memory,
|
|
170
|
+
* so a rewrite it never learns about simply does not apply: the user strips an oversized
|
|
171
|
+
* image, sends again, and the same image is re-sent from memory and fails identically.
|
|
172
|
+
* `listRunningSessions()` does not cover it — that skips `phase === "idle"`, and a warm idle
|
|
173
|
+
* subprocess is exactly this case. Unlike `releaseSubprocess` it does not require the session
|
|
174
|
+
* to be clientless, because the tab being open is the normal way to reach the strip button.
|
|
175
|
+
*/
|
|
176
|
+
export function dropSubprocessForTranscriptRewrite(sessionId: string): void {
|
|
177
|
+
const entry = activeSessions.get(sessionId);
|
|
178
|
+
if (!entry) return;
|
|
179
|
+
const provider = providerRegistry.get(entry.providerId);
|
|
180
|
+
if (!provider?.hasStreamingSession?.(sessionId)) return;
|
|
181
|
+
provider.abortQuery?.(sessionId, "transcript_rewritten");
|
|
182
|
+
if (entry.cacheReleaseTimer) {
|
|
183
|
+
clearTimeout(entry.cacheReleaseTimer);
|
|
184
|
+
entry.cacheReleaseTimer = undefined;
|
|
185
|
+
}
|
|
186
|
+
console.log(`[chat] session=${sessionId} released subprocess (transcript_rewritten)`);
|
|
187
|
+
logSessionEvent(sessionId, "INFO", "Subprocess released: the transcript was rewritten, so the next turn is rebuilt from disk");
|
|
188
|
+
}
|
|
189
|
+
|
|
127
190
|
/** Tear down the longest-idle subprocesses once too many sessions are holding one. */
|
|
128
191
|
function enforceWarmIdleCap(): void {
|
|
129
192
|
const warmIdle = listWarmIdleSessions();
|
|
@@ -153,7 +216,10 @@ function scheduleSubprocessRelease(sessionId: string): void {
|
|
|
153
216
|
if (!provider?.hasStreamingSession?.(sessionId)) return;
|
|
154
217
|
|
|
155
218
|
const note = "Subprocess released: its prompt cache has expired, so keeping it warm saves nothing";
|
|
156
|
-
|
|
219
|
+
// The window is the provider's to state: an API-key install's cache dies at five minutes,
|
|
220
|
+
// so holding the subprocess for an hour there guards nothing and costs ~350MB.
|
|
221
|
+
const ttlMs = provider.promptCacheTtlMs?.(sessionId);
|
|
222
|
+
const delay = cacheReleaseDelayMs(entry.lastTurnEndedAt, Date.now(), ttlMs);
|
|
157
223
|
if (delay === 0) {
|
|
158
224
|
releaseSubprocess(sessionId, "cache_expired", note);
|
|
159
225
|
return;
|
|
@@ -271,6 +337,24 @@ function bufferAndBroadcast(sessionId: string, event: unknown): void {
|
|
|
271
337
|
broadcast(sessionId, event);
|
|
272
338
|
}
|
|
273
339
|
|
|
340
|
+
/**
|
|
341
|
+
* Emit a nested-agent child read off disk. Buffered for reconnect replay only
|
|
342
|
+
* while its turn is still in flight and under the nested budget; a background
|
|
343
|
+
* agent that outlives the turn streams its children unbuffered, since the next
|
|
344
|
+
* turn's replay is not the place for them and reload restores them from disk.
|
|
345
|
+
*/
|
|
346
|
+
function emitNestedChild(sessionId: string, child: unknown): void {
|
|
347
|
+
const entry = activeSessions.get(sessionId);
|
|
348
|
+
if (!entry) return;
|
|
349
|
+
const inFlight = entry.isStreamingActive && entry.phase !== "idle";
|
|
350
|
+
if (inFlight && (entry.nestedBuffered ?? 0) < MAX_NESTED_TURN_EVENTS) {
|
|
351
|
+
entry.nestedBuffered = (entry.nestedBuffered ?? 0) + 1;
|
|
352
|
+
bufferAndBroadcast(sessionId, child);
|
|
353
|
+
} else {
|
|
354
|
+
broadcast(sessionId, child);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
274
358
|
/** How often a session may stat ~/.claude/teams looking for an implicit team. */
|
|
275
359
|
const IMPLICIT_TEAM_PROBE_INTERVAL_MS = 3_000;
|
|
276
360
|
|
|
@@ -367,6 +451,13 @@ function startCleanupTimer(sessionId: string): void {
|
|
|
367
451
|
entry.cleanupTimer = setTimeout(() => {
|
|
368
452
|
// Double-check: don't kill if streaming started while timer was pending
|
|
369
453
|
if (entry.isStreamingActive) return;
|
|
454
|
+
// A pending release means the subprocess is still worth holding, and dropping the entry
|
|
455
|
+
// takes it with us. Come back after that timer rather than stretching this one.
|
|
456
|
+
if (entry.cacheReleaseTimer) {
|
|
457
|
+
entry.cleanupTimer = undefined;
|
|
458
|
+
startCleanupTimer(sessionId);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
370
461
|
console.log(`[chat] session=${sessionId} cleanup: idle with no FE for ${CLEANUP_TIMEOUT_MS / 1000}s`);
|
|
371
462
|
logSessionEvent(sessionId, "INFO", "Session cleaned up (idle, no FE reconnected)");
|
|
372
463
|
// Backstop for the subprocess: scheduleSubprocessRelease normally gets there first,
|
|
@@ -405,6 +496,7 @@ async function startSessionConsumer(sessionId: string, providerId: string, conte
|
|
|
405
496
|
entry.isStreamingActive = true;
|
|
406
497
|
entry.pendingApprovalEvent = undefined;
|
|
407
498
|
entry.turnEvents = [];
|
|
499
|
+
entry.nestedBuffered = 0;
|
|
408
500
|
setPhase(sessionId, "connecting");
|
|
409
501
|
|
|
410
502
|
let heartbeat: ReturnType<typeof setInterval> | undefined;
|
|
@@ -484,6 +576,9 @@ async function startSessionConsumer(sessionId: string, providerId: string, conte
|
|
|
484
576
|
// the card can settle. Harmless for background bash tasks: no card matches them.
|
|
485
577
|
if (sub === "task_notification" && taskToolUseId && isTerminalAgentStatus(taskStatus)) {
|
|
486
578
|
broadcast(sessionId, { type: "subagent_status", toolUseId: taskToolUseId, status: taskStatus });
|
|
579
|
+
// The agent is done, so its nested workers are too — release the tail.
|
|
580
|
+
nestedSubagentSpy.stopSpy(taskToolUseId);
|
|
581
|
+
entry.backgroundToolUseIds?.delete(taskToolUseId);
|
|
487
582
|
}
|
|
488
583
|
if (taskId) {
|
|
489
584
|
// Ensure the shell is registered even if the spy missed the file (fallback).
|
|
@@ -549,6 +644,18 @@ async function startSessionConsumer(sessionId: string, providerId: string, conte
|
|
|
549
644
|
entry.pendingTeamCreate = ev.toolUseId;
|
|
550
645
|
console.log(`[chat] session=${sessionId} TeamCreate tool_use detected, toolUseId=${ev.toolUseId}`);
|
|
551
646
|
}
|
|
647
|
+
// A session-level Agent card: the SDK streams its agent's own steps, but
|
|
648
|
+
// nothing from agents that agent spawns in turn. Tail those nested
|
|
649
|
+
// transcripts from disk so the card keeps moving instead of freezing on
|
|
650
|
+
// the step that forked them. Claude-SDK-only — the layout is the CLI's.
|
|
651
|
+
if (providerId === "claude" && (ev.tool === "Agent" || ev.tool === "Task") && ev.toolUseId && !ev.parentToolUseId) {
|
|
652
|
+
const sessionDir = resolveSessionDir(sessionId, entry.projectPath);
|
|
653
|
+
if (sessionDir) {
|
|
654
|
+
nestedSubagentSpy.startSpy(sessionId, ev.toolUseId, sessionDir, (events) => {
|
|
655
|
+
for (const child of events) emitNestedChild(sessionId, child);
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
}
|
|
552
659
|
// Start output spy for real-time streaming (Bash on Linux/macOS, PowerShell on Windows).
|
|
553
660
|
// Claude-SDK-only: it tails the SDK's per-tool output file. Other providers
|
|
554
661
|
// (codex/cursor) run commands in their own subprocess with no such file.
|
|
@@ -600,10 +707,18 @@ async function startSessionConsumer(sessionId: string, providerId: string, conte
|
|
|
600
707
|
} else if (evType === "tool_result") {
|
|
601
708
|
logSessionEvent(sessionId, "TOOL_RESULT", `error=${ev.isError ?? false} ${(ev.output ?? "").slice(0, 300)}`);
|
|
602
709
|
console.log(`[chat] session=${sessionId} tool_result: toolUseId=${ev.toolUseId} pendingTeamCreate=${entry.pendingTeamCreate} output=${(ev.output ?? "").slice(0, 200)}`);
|
|
710
|
+
// A backgrounded Agent's tool_result is only a launch ack while the agent
|
|
711
|
+
// runs on — keep its nested spy until the terminal task_notification.
|
|
712
|
+
// Keyed off the ack text: `input.run_in_background` is optional and absent
|
|
713
|
+
// from most recorded calls (see background-agent-status.ts).
|
|
714
|
+
if (ev.toolUseId && !ev.parentToolUseId && isAsyncAgentLaunchAck(ev.output)) {
|
|
715
|
+
(entry.backgroundToolUseIds ??= new Set()).add(ev.toolUseId);
|
|
716
|
+
}
|
|
603
717
|
// Stop bash output spy for this tool — EXCEPT background commands, whose
|
|
604
718
|
// process keeps running after tool_result; keep tailing their .output.
|
|
605
719
|
if (ev.toolUseId && !entry.backgroundToolUseIds?.has(ev.toolUseId)) {
|
|
606
720
|
bashOutputSpy.stopSpy(ev.toolUseId);
|
|
721
|
+
nestedSubagentSpy.stopSpy(ev.toolUseId);
|
|
607
722
|
}
|
|
608
723
|
// Detect team creation from TeamCreate tool_result (legacy explicit teams)
|
|
609
724
|
if (entry.pendingTeamCreate && entry.pendingTeamCreate === ev.toolUseId) {
|
|
@@ -689,6 +804,7 @@ async function startSessionConsumer(sessionId: string, providerId: string, conte
|
|
|
689
804
|
console.log(`[chat] session_migrated: ${sessionId} → ${newId}`);
|
|
690
805
|
// Stop spies tagged with old session ID before re-keying
|
|
691
806
|
bashOutputSpy.stopAllForSession(sessionId);
|
|
807
|
+
nestedSubagentSpy.stopAllForSession(sessionId);
|
|
692
808
|
backgroundShellRegistry.clearSession(sessionId);
|
|
693
809
|
const oldEntry = activeSessions.get(sessionId);
|
|
694
810
|
if (oldEntry) {
|
|
@@ -741,6 +857,9 @@ async function startSessionConsumer(sessionId: string, providerId: string, conte
|
|
|
741
857
|
bufferAndBroadcast(sessionId, { type: "error", message: errMsg });
|
|
742
858
|
} finally {
|
|
743
859
|
if (heartbeat) clearInterval(heartbeat);
|
|
860
|
+
// Drain nested-agent tails while their turn buffer still exists, so the last
|
|
861
|
+
// records land in this turn's replay instead of the head of the next one.
|
|
862
|
+
nestedSubagentSpy.stopAllForSession(sessionId);
|
|
744
863
|
entry.isStreamingActive = false;
|
|
745
864
|
entry.turnEvents = [];
|
|
746
865
|
// Force-clear compact status on stream teardown (error, close, etc.)
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WS handler for `/ws/remote-desktop` — binary out = framed video access units (1-byte
|
|
3
|
+
* key/delta header + Annex-B bytes), text in = JSON control messages.
|
|
4
|
+
*
|
|
5
|
+
* The connection is *not* trusted just because the upgrade succeeded: the coarse PPM auth
|
|
6
|
+
* token already travels as `?token=` on every WS URL (`isWsUpgradeAuthorized`,
|
|
7
|
+
* `src/server/index.ts`) and proves nothing beyond "holds the one reusable app token", so a
|
|
8
|
+
* single-use short-TTL nonce (`POST /api/remote-desktop/session`) must be presented as the
|
|
9
|
+
* client's *first* message — `{type:"auth", nonce}` — before capture or input starts. Kept out
|
|
10
|
+
* of the query string (unlike the coarse token) so it never lands in proxy/tunnel access logs.
|
|
11
|
+
*
|
|
12
|
+
* Both the feature flag and `auth.enabled` are re-checked here even though
|
|
13
|
+
* `src/server/index.ts` already gated the upgrade on them — this handler must never depend on
|
|
14
|
+
* a single call site staying correct as fetch-time routing evolves, and must NOT reuse
|
|
15
|
+
* `isWsUpgradeAuthorized()`, which returns `true` unconditionally when PPM auth is disabled.
|
|
16
|
+
*/
|
|
17
|
+
import { configService } from "../../services/config.service.ts";
|
|
18
|
+
import { isRemoteDesktopEnabled } from "../../services/remote-desktop/remote-desktop-flag.ts";
|
|
19
|
+
import { consumeRemoteDesktopNonce } from "../../services/remote-desktop/remote-desktop-nonce.ts";
|
|
20
|
+
import {
|
|
21
|
+
createRemoteDesktopSession,
|
|
22
|
+
registerRemoteDesktopExitSweep,
|
|
23
|
+
type RemoteDesktopSession,
|
|
24
|
+
type RemoteDesktopSocket,
|
|
25
|
+
} from "../../services/remote-desktop/remote-desktop-session.ts";
|
|
26
|
+
|
|
27
|
+
registerRemoteDesktopExitSweep();
|
|
28
|
+
|
|
29
|
+
interface RemoteDesktopWs {
|
|
30
|
+
data: { type: "remote-desktop"; session?: RemoteDesktopSession; authenticated?: boolean };
|
|
31
|
+
send: (d: string | Uint8Array) => number;
|
|
32
|
+
getBufferedAmount?: () => number;
|
|
33
|
+
close: (code?: number, reason?: string) => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function guardOrClose(ws: RemoteDesktopWs): boolean {
|
|
37
|
+
if (!isRemoteDesktopEnabled()) { ws.close(1008, "remote desktop is disabled"); return false; }
|
|
38
|
+
if (!configService.get("auth").enabled) {
|
|
39
|
+
ws.close(1008, "remote desktop requires PPM authentication to be enabled");
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function authenticateFirstMessage(ws: RemoteDesktopWs, text: string): Promise<void> {
|
|
46
|
+
let parsed: Record<string, unknown>;
|
|
47
|
+
try { parsed = JSON.parse(text); } catch { ws.close(1008, "expected auth message"); return; }
|
|
48
|
+
const nonce = parsed.type === "auth" ? parsed.nonce : undefined;
|
|
49
|
+
if (typeof nonce !== "string" || !consumeRemoteDesktopNonce(nonce)) {
|
|
50
|
+
ws.close(1008, "invalid or expired session nonce");
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
ws.data.authenticated = true;
|
|
54
|
+
try {
|
|
55
|
+
const socket: RemoteDesktopSocket = {
|
|
56
|
+
send: (d) => ws.send(d),
|
|
57
|
+
getBufferedAmount: ws.getBufferedAmount ? () => ws.getBufferedAmount!() : undefined,
|
|
58
|
+
close: (code, reason) => ws.close(code, reason),
|
|
59
|
+
};
|
|
60
|
+
ws.data.session = await createRemoteDesktopSession(socket);
|
|
61
|
+
} catch (e) {
|
|
62
|
+
console.error(`[remote-desktop] failed to start capture: ${(e as Error).message}`);
|
|
63
|
+
ws.send(JSON.stringify({ type: "error", message: (e as Error).message }));
|
|
64
|
+
ws.close(1011, "capture failed to start");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const remoteDesktopWebSocket = {
|
|
69
|
+
open(ws: RemoteDesktopWs) {
|
|
70
|
+
guardOrClose(ws); // nothing else to do until the client's first (auth) message arrives
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
async message(ws: RemoteDesktopWs, msg: string | ArrayBuffer | Uint8Array) {
|
|
74
|
+
if (!guardOrClose(ws)) return;
|
|
75
|
+
const text = typeof msg === "string" ? msg : new TextDecoder().decode(msg as ArrayBuffer);
|
|
76
|
+
if (!ws.data.authenticated) {
|
|
77
|
+
await authenticateFirstMessage(ws, text);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
await ws.data.session?.handleClientMessage(text);
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
close(ws: RemoteDesktopWs) {
|
|
84
|
+
ws.data.session?.close();
|
|
85
|
+
},
|
|
86
|
+
};
|
|
@@ -131,21 +131,36 @@ class AccountService {
|
|
|
131
131
|
* Returns the refreshed account with fresh tokens, or null if refresh failed.
|
|
132
132
|
*/
|
|
133
133
|
async ensureFreshToken(id: string): Promise<AccountWithTokens | null> {
|
|
134
|
+
return (await this.ensureFreshTokenChecked(id)).account;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* `ensureFreshToken`, keeping the reason it failed.
|
|
139
|
+
*
|
|
140
|
+
* `rejected` means the OAuth server turned the refresh token itself down (`invalid_grant`
|
|
141
|
+
* / `invalid_request`): nothing local recovers from that, only a fresh sign-in. Anything
|
|
142
|
+
* else — a network drop, a 429, a decrypt failure — is transient and worth retrying,
|
|
143
|
+
* which is a different sentence to put in front of a user. `ensureFreshToken` collapses
|
|
144
|
+
* the two because its callers are background loops with nobody to tell; the one caller
|
|
145
|
+
* that has a user in front of it (`PATCH /api/accounts/:id`) uses this.
|
|
146
|
+
*/
|
|
147
|
+
async ensureFreshTokenChecked(id: string): Promise<{ account: AccountWithTokens | null; rejected: boolean }> {
|
|
134
148
|
const acc = this.getWithTokens(id);
|
|
135
|
-
if (!acc) return null;
|
|
149
|
+
if (!acc) return { account: null, rejected: false };
|
|
136
150
|
// Only OAuth tokens need refresh
|
|
137
|
-
if (!acc.accessToken.startsWith("sk-ant-oat")) return acc;
|
|
138
|
-
if (!acc.expiresAt) return acc;
|
|
151
|
+
if (!acc.accessToken.startsWith("sk-ant-oat")) return { account: acc, rejected: false };
|
|
152
|
+
if (!acc.expiresAt) return { account: acc, rejected: false };
|
|
139
153
|
const nowS = Math.floor(Date.now() / 1000);
|
|
140
154
|
const REFRESH_BUFFER_S = 3600; // 1 hour — refresh proactively before expiry
|
|
141
|
-
if (acc.expiresAt - nowS > REFRESH_BUFFER_S) return acc; // still fresh
|
|
155
|
+
if (acc.expiresAt - nowS > REFRESH_BUFFER_S) return { account: acc, rejected: false }; // still fresh
|
|
142
156
|
try {
|
|
143
157
|
console.log(`[accounts] Pre-flight refresh for ${acc.email ?? id} (expires in ${acc.expiresAt - nowS}s, buffer=${REFRESH_BUFFER_S}s)`);
|
|
144
158
|
await this.refreshAccessToken(id, false, false, REFRESH_BUFFER_S);
|
|
145
|
-
return this.getWithTokens(id);
|
|
159
|
+
return { account: this.getWithTokens(id), rejected: false };
|
|
146
160
|
} catch (e) {
|
|
147
|
-
|
|
148
|
-
|
|
161
|
+
const msg = (e as Error).message ?? String(e);
|
|
162
|
+
console.error(`[accounts] Pre-flight refresh failed for ${id}: ${msg}`);
|
|
163
|
+
return { account: null, rejected: /invalid_grant|invalid_request/.test(msg) };
|
|
149
164
|
}
|
|
150
165
|
}
|
|
151
166
|
|
|
@@ -183,6 +198,8 @@ class AccountService {
|
|
|
183
198
|
}
|
|
184
199
|
if (params.label) updateAccount(dup.id, { label: params.label });
|
|
185
200
|
if (params.email) updateAccount(dup.id, { email: params.email });
|
|
201
|
+
// After the email lands, or the first re-add of a row that had none logs a UUID.
|
|
202
|
+
this.noteParkSurvivedReAdd(dup.id);
|
|
186
203
|
return this.toAccount(getAccountById(dup.id)!);
|
|
187
204
|
}
|
|
188
205
|
|
|
@@ -284,9 +301,18 @@ class AccountService {
|
|
|
284
301
|
// Check for duplicate — update tokens on existing account
|
|
285
302
|
const dup = this.findDuplicate(email, info.profileData);
|
|
286
303
|
if (dup) {
|
|
287
|
-
|
|
304
|
+
// Mirrors updateTokens(): a fresh token ends a cooldown, but a park is a decision
|
|
305
|
+
// about the rotation and pasting the token in again does not reverse it. This was the
|
|
306
|
+
// one door left that force-enabled, which is how the two paths disagreed.
|
|
307
|
+
const parked = getAccountById(dup.id)?.status === "disabled";
|
|
308
|
+
updateAccount(dup.id, {
|
|
309
|
+
access_token: encrypt(params.apiKey),
|
|
310
|
+
cooldown_until: null,
|
|
311
|
+
...(parked ? {} : { status: "active" as const }),
|
|
312
|
+
});
|
|
288
313
|
if (info.profileData) updateAccount(dup.id, { profile_json: JSON.stringify(info.profileData) });
|
|
289
314
|
if (email) updateAccount(dup.id, { email });
|
|
315
|
+
this.noteParkSurvivedReAdd(dup.id);
|
|
290
316
|
return this.toAccount(getAccountById(dup.id)!);
|
|
291
317
|
}
|
|
292
318
|
|
|
@@ -326,15 +352,39 @@ class AccountService {
|
|
|
326
352
|
}
|
|
327
353
|
|
|
328
354
|
updateTokens(id: string, accessToken: string, refreshToken: string, expiresAt: number): void {
|
|
355
|
+
// A disabled account keeps that status. Usage polling refreshes parked accounts on
|
|
356
|
+
// purpose (a GET costs no quota), and export does the same before writing a backup;
|
|
357
|
+
// activating here put them back in the chat rotation behind the user's back, so the
|
|
358
|
+
// toggle looked like it had been forgotten — most visibly after a restart, which
|
|
359
|
+
// polls immediately. Cooldown still clears for everything else: a fresh token is
|
|
360
|
+
// precisely what ends a cooldown.
|
|
361
|
+
const parked = getAccountById(id)?.status === "disabled";
|
|
329
362
|
updateAccount(id, {
|
|
330
363
|
access_token: encrypt(accessToken),
|
|
331
364
|
refresh_token: encrypt(refreshToken),
|
|
332
365
|
expires_at: expiresAt,
|
|
333
|
-
|
|
366
|
+
// The cooldown clears either way — a fresh token is what ends one, and leaving the
|
|
367
|
+
// timestamp on a parked account would resurrect it the moment the park is lifted.
|
|
334
368
|
cooldown_until: null,
|
|
369
|
+
...(parked ? {} : { status: "active" as const }),
|
|
335
370
|
});
|
|
336
371
|
}
|
|
337
372
|
|
|
373
|
+
/**
|
|
374
|
+
* Both re-add doors land here once the new tokens are stored. Neither lifts a park —
|
|
375
|
+
* disabling is a decision about the rotation, and carrying credentials in does not reverse
|
|
376
|
+
* it — but landing tokens and then showing nothing is exactly what made the OAuth path look
|
|
377
|
+
* like a no-op. Saying so is the whole point; keeping it in one place is what stops the two
|
|
378
|
+
* doors from drifting apart again.
|
|
379
|
+
*/
|
|
380
|
+
private noteParkSurvivedReAdd(id: string): void {
|
|
381
|
+
const row = getAccountById(id);
|
|
382
|
+
if (row?.status !== "disabled") return;
|
|
383
|
+
console.log(
|
|
384
|
+
`[accounts] Tokens updated for ${row.email ?? id} — account stays disabled; enable it in Settings`,
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
338
388
|
setCooldown(id: string, untilMs: number): void {
|
|
339
389
|
updateAccount(id, {
|
|
340
390
|
status: "cooldown",
|
|
@@ -352,7 +402,11 @@ class AccountService {
|
|
|
352
402
|
const acc = this.list().find((a) => a.id === id);
|
|
353
403
|
const nowS = Math.floor(Date.now() / 1000);
|
|
354
404
|
if (acc?.expiresAt && acc.expiresAt < nowS) {
|
|
355
|
-
|
|
405
|
+
// Signing in again refreshes the tokens but no longer re-enables anything, so the
|
|
406
|
+
// text says what actually happens: get a live token in, then flip it back on here.
|
|
407
|
+
throw new Error(
|
|
408
|
+
"Cannot re-enable expired temporary account. Sign in again or import a fresh backup to refresh its tokens, then enable it here.",
|
|
409
|
+
);
|
|
356
410
|
}
|
|
357
411
|
}
|
|
358
412
|
updateAccount(id, { status: "active", cooldown_until: null });
|
|
@@ -552,9 +606,15 @@ class AccountService {
|
|
|
552
606
|
|
|
553
607
|
/**
|
|
554
608
|
* POST the refresh grant, retrying transient failures with backoff.
|
|
555
|
-
* A network blip (e.g. waking from sleep) must not cost a whole refresh cycle
|
|
556
|
-
*
|
|
557
|
-
*
|
|
609
|
+
* A network blip (e.g. waking from sleep) must not cost a whole refresh cycle: this
|
|
610
|
+
* runs at or past access-token expiry, so a dropped attempt leaves the account unable
|
|
611
|
+
* to serve a turn until the next sweep.
|
|
612
|
+
*
|
|
613
|
+
* It is *not* that the refresh token is about to lapse with it. Measured against a
|
|
614
|
+
* live install's history: 324 refreshes, zero `invalid_grant`, and one that succeeded
|
|
615
|
+
* after a 58-hour gap — 50 hours past the 8-hour access-token expiry. Refresh tokens
|
|
616
|
+
* outlive their access tokens by days, which is what makes freezing a parked account
|
|
617
|
+
* (see `startAutoRefresh`) safe rather than a slow way to kill it.
|
|
558
618
|
*/
|
|
559
619
|
private async postRefreshGrant(refreshToken: string, label: string): Promise<Response> {
|
|
560
620
|
const backoffMs = [2_000, 6_000];
|
|
@@ -664,7 +724,8 @@ class AccountService {
|
|
|
664
724
|
* Export accounts backup.
|
|
665
725
|
* @param includeRefreshToken - if true, includes refresh tokens (full transfer).
|
|
666
726
|
* Source keeps its refresh token; it will be auto-cleared if it becomes invalid.
|
|
667
|
-
* Default false = temporary export (access-only, ~1h
|
|
727
|
+
* Default false = temporary export (access-only, ~1h — see `refreshBeforeExport`,
|
|
728
|
+
* which freshens the access token first so the window starts at export time).
|
|
668
729
|
*/
|
|
669
730
|
exportEncrypted(password: string, accountIds?: string[], includeRefreshToken = false): string {
|
|
670
731
|
const rows = accountIds?.length
|
|
@@ -709,14 +770,25 @@ class AccountService {
|
|
|
709
770
|
let accessToken = row.access_token;
|
|
710
771
|
if (!looksEncrypted(accessToken)) accessToken = encrypt(accessToken);
|
|
711
772
|
const refreshToken = looksEncrypted(row.refresh_token) ? row.refresh_token : encrypt(row.refresh_token);
|
|
773
|
+
// An account parked on THIS machine stays parked: the import carries tokens, not
|
|
774
|
+
// the local decision about who is in the rotation. A new account still takes the
|
|
775
|
+
// backup's status below, since there is no local decision to respect. An account
|
|
776
|
+
// the failed-refresh path disabled is recoverable in one click — the import gives
|
|
777
|
+
// it back a live refresh token, which is what setEnabled() checks for.
|
|
778
|
+
const parked = existing.status === "disabled";
|
|
712
779
|
updateAccount(existing.id, {
|
|
713
780
|
access_token: accessToken,
|
|
714
781
|
refresh_token: refreshToken,
|
|
715
782
|
expires_at: row.expires_at,
|
|
716
|
-
status: "active",
|
|
783
|
+
...(parked ? {} : { status: "active" as const }),
|
|
717
784
|
});
|
|
718
785
|
imported++;
|
|
719
|
-
|
|
786
|
+
// The usage poller and export both refresh a parked account, because a token
|
|
787
|
+
// nothing ever refreshes eventually dies. This one is different: it is not
|
|
788
|
+
// keeping the token alive, it is claiming ownership of it — and claiming a token
|
|
789
|
+
// for an account this machine has taken out of the rotation buys nothing here
|
|
790
|
+
// while invalidating the machine still using it. Enabling the account claims it.
|
|
791
|
+
if (!parked) fullTransferIds.push(existing.id);
|
|
720
792
|
console.log(`[accounts] Updated ${row.email ?? existing.id} tokens from import`);
|
|
721
793
|
}
|
|
722
794
|
continue; // skip if import doesn't have refresh token
|
|
@@ -726,6 +798,11 @@ class AccountService {
|
|
|
726
798
|
let accessToken = row.access_token;
|
|
727
799
|
if (!looksEncrypted(accessToken)) accessToken = encrypt(accessToken);
|
|
728
800
|
const refreshToken = hasRefresh ? (looksEncrypted(row.refresh_token) ? row.refresh_token : encrypt(row.refresh_token)) : encrypt("");
|
|
801
|
+
// The column has no CHECK constraint and the blob is user-supplied, so anything
|
|
802
|
+
// outside the three real statuses lands as active. A stray value reads as *on* in
|
|
803
|
+
// the UI (`status !== "disabled"`) while never being selectable for a turn.
|
|
804
|
+
const importedStatus =
|
|
805
|
+
row.status === "disabled" || row.status === "cooldown" ? row.status : "active";
|
|
729
806
|
insertAccount({
|
|
730
807
|
id: row.id,
|
|
731
808
|
label: row.label,
|
|
@@ -733,7 +810,7 @@ class AccountService {
|
|
|
733
810
|
access_token: accessToken,
|
|
734
811
|
refresh_token: refreshToken,
|
|
735
812
|
expires_at: row.expires_at,
|
|
736
|
-
status:
|
|
813
|
+
status: importedStatus,
|
|
737
814
|
cooldown_until: row.cooldown_until,
|
|
738
815
|
priority: row.priority ?? 0,
|
|
739
816
|
total_requests: row.total_requests ?? 0,
|
|
@@ -741,7 +818,7 @@ class AccountService {
|
|
|
741
818
|
profile_json: row.profile_json ?? null,
|
|
742
819
|
});
|
|
743
820
|
imported++;
|
|
744
|
-
if (hasRefresh) fullTransferIds.push(row.id);
|
|
821
|
+
if (hasRefresh && importedStatus !== "disabled") fullTransferIds.push(row.id);
|
|
745
822
|
}
|
|
746
823
|
|
|
747
824
|
// Immediately refresh full-transfer accounts to claim ownership
|
|
@@ -781,11 +858,17 @@ class AccountService {
|
|
|
781
858
|
const accounts = this.list();
|
|
782
859
|
const nowS = Math.floor(Date.now() / 1000);
|
|
783
860
|
for (const acc of accounts) {
|
|
784
|
-
//
|
|
785
|
-
// Refreshing rotates the OAuth refresh token; when
|
|
786
|
-
// shared across machines
|
|
787
|
-
// invalid_grant, killing an account the user only meant to park.
|
|
788
|
-
//
|
|
861
|
+
// Skip disabled accounts: this timer exists to keep the chat rotation supplied and
|
|
862
|
+
// a parked account is not in it. Refreshing rotates the OAuth refresh token; when
|
|
863
|
+
// the same account is shared across machines the rotation races and one side gets
|
|
864
|
+
// invalid_grant, killing an account the user only meant to park.
|
|
865
|
+
//
|
|
866
|
+
// A preference, not a guarantee, and worth knowing before relying on it: the usage
|
|
867
|
+
// poller runs on the same 5-minute cadence with the same 1-hour buffer and does not
|
|
868
|
+
// skip parked accounts — deliberately, because a token nothing ever refreshes does
|
|
869
|
+
// eventually die. So while polling is on, a parked account's token still rotates
|
|
870
|
+
// here-or-there; this skip only declines to be the one that does it. Enabling the
|
|
871
|
+
// account proves the token either way (`PATCH /api/accounts/:id`).
|
|
789
872
|
if (acc.status === "disabled") continue;
|
|
790
873
|
if (!acc.expiresAt) continue;
|
|
791
874
|
if (acc.expiresAt - nowS > REFRESH_BUFFER_S) continue;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { resolve, basename } from "node:path";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { getPpmDir, isIsolatedPpmHome } from "../ppm-dir.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Where database snapshots live, and how their filenames encode when and why
|
|
7
|
+
* they were taken.
|
|
8
|
+
*
|
|
9
|
+
* The directory deliberately sits OUTSIDE `~/.ppm` in production. A snapshot
|
|
10
|
+
* kept next to the database it protects dies with it: the whole point is to
|
|
11
|
+
* survive the case where something wipes or overwrites the PPM directory.
|
|
12
|
+
* Under an isolated `PPM_HOME` (tests) it goes back inside that temp dir, so a
|
|
13
|
+
* test run can never write snapshots into the user's real home.
|
|
14
|
+
*
|
|
15
|
+
* `homedir()` here is a deliberate exception to the getPpmDir()-only rule (see
|
|
16
|
+
* CLAUDE.md "PPM Directory"): the sibling location is the entire reason this
|
|
17
|
+
* directory survives a PPM-directory wipe, so it cannot be derived from
|
|
18
|
+
* `getPpmDir()` in production.
|
|
19
|
+
*/
|
|
20
|
+
export function getBackupsDir(): string {
|
|
21
|
+
return isIsolatedPpmHome()
|
|
22
|
+
? resolve(getPpmDir(), "backups")
|
|
23
|
+
: resolve(homedir(), ".ppm-backups");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Why a snapshot was taken. Encoded in the filename so `ppm restore` can explain each candidate. */
|
|
27
|
+
export type BackupReason = "hourly" | "premigrate" | "start" | "manual";
|
|
28
|
+
|
|
29
|
+
export const BACKUP_REASONS: readonly BackupReason[] = ["hourly", "premigrate", "start", "manual"];
|
|
30
|
+
|
|
31
|
+
/** `2026-09-06T15:46:00.000Z` → `20260906T154600Z` (filename-safe, still sorts chronologically). */
|
|
32
|
+
export function formatBackupStamp(date: Date): string {
|
|
33
|
+
return date.toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Snapshot filename. `dbFile` is the SOURCE database's basename (`ppm.db`,
|
|
38
|
+
* `ppm.dev.db`), kept in the name so a dev-profile run never prunes or
|
|
39
|
+
* restores production snapshots.
|
|
40
|
+
*/
|
|
41
|
+
export function backupFileName(dbFile: string, date: Date, reason: BackupReason): string {
|
|
42
|
+
const stem = basename(dbFile).replace(/\.db$/, "");
|
|
43
|
+
return `${stem}-${formatBackupStamp(date)}-${reason}.db`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ParsedBackupName {
|
|
47
|
+
dbStem: string;
|
|
48
|
+
date: Date;
|
|
49
|
+
reason: BackupReason;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Inverse of `backupFileName`. Returns null for anything this module did not
|
|
54
|
+
* write — including `.partial` files from an interrupted snapshot, which must
|
|
55
|
+
* never be offered as a restore candidate.
|
|
56
|
+
*/
|
|
57
|
+
export function parseBackupName(name: string): ParsedBackupName | null {
|
|
58
|
+
const m = /^(.+)-(\d{8}T\d{6}Z)-([a-z]+)\.db$/.exec(name);
|
|
59
|
+
if (!m) return null;
|
|
60
|
+
const [, dbStem, stamp, reason] = m;
|
|
61
|
+
if (!BACKUP_REASONS.includes(reason as BackupReason)) return null;
|
|
62
|
+
const iso = `${stamp!.slice(0, 4)}-${stamp!.slice(4, 6)}-${stamp!.slice(6, 8)}T${stamp!.slice(9, 11)}:${stamp!.slice(11, 13)}:${stamp!.slice(13, 15)}Z`;
|
|
63
|
+
const date = new Date(iso);
|
|
64
|
+
if (Number.isNaN(date.getTime())) return null;
|
|
65
|
+
return { dbStem: dbStem!, date, reason: reason as BackupReason };
|
|
66
|
+
}
|