specrails-desktop 2.36.0 → 2.42.2
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/README.md +94 -247
- package/client/dist/assets/ActivityFeedPage-DoEMumQS.js +1 -0
- package/client/dist/assets/AgentBrowserCapture-DIh4K0gh.js +1 -0
- package/client/dist/assets/AgentModeAnalyticsPane-mbq4WEoO.js +2 -0
- package/client/dist/assets/AgentModeCodePane-CKh7zuA4.js +2 -0
- package/client/dist/assets/AgentModeJobsPane-BDokPiLJ.js +1 -0
- package/client/dist/assets/AgentsPage-BFVAZbBj.js +87 -0
- package/client/dist/assets/AnalyticsPage-3Y9LuWwz.js +1 -0
- package/client/dist/assets/CodePage-C9KyuV_C.js +3 -0
- package/client/dist/assets/DesktopAnalyticsPage-Bhi54bXQ.js +1 -0
- package/client/dist/assets/{DocsDialog-C76VdBJC.js → DocsDialog-Dj6mt4ga.js} +2 -2
- package/client/dist/assets/{DocsPage-ziMlj6B6.js → DocsPage-24yWglNI.js} +1 -1
- package/client/dist/assets/ExportDropdown-ophapHrT.js +1 -0
- package/client/dist/assets/InteractiveJobComposer-C1ucmxQh.js +19 -0
- package/client/dist/assets/JobDetailModal-DNWmgVHD.js +1 -0
- package/client/dist/assets/JobDetailPage-BcajZyuu.js +1 -0
- package/client/dist/assets/JobsPage-B9nTZw6e.js +1 -0
- package/client/dist/assets/LoopBuilderPage-Oc48iOFW.js +7 -0
- package/client/dist/assets/LoopPreviewModal-ae0Fr6SU.js +1 -0
- package/client/dist/assets/LoopsPage-CUEqzUBI.js +1 -0
- package/client/dist/assets/MinimizedChatsContext-Deg39peL.js +1 -0
- package/client/dist/assets/PluginsPage-C1xtARIx.js +2 -0
- package/client/dist/assets/ProjectSettingsDialog-JZzrXFBN.js +1 -0
- package/client/dist/assets/RepositoryDeliveries-CSDsiVUz.js +5 -0
- package/client/dist/assets/RepositoryScopeSelector-D-RCaknl.js +1 -0
- package/client/dist/assets/ReviewPacketPage-x5EfGTiL.js +1 -0
- package/client/dist/assets/TemplatePreviewModal-BSyuZxGq.js +1 -0
- package/client/dist/assets/TicketDetailModalContext-DqyF0so-.js +14 -0
- package/client/dist/assets/Trans-SnMX4MhW.js +1 -0
- package/client/dist/assets/agent-BdT0SB1D.js +1 -0
- package/client/dist/assets/agent-Bh9K7iEt.js +1 -0
- package/client/dist/assets/agent-C4ibdFk9.js +1 -0
- package/client/dist/assets/agent-CSFY22Zo.js +1 -0
- package/client/dist/assets/agent-DAnqxUn8.js +1 -0
- package/client/dist/assets/agent-DTQiu0Mi.js +1 -0
- package/client/dist/assets/agent-T54WuR_Y.js +1 -0
- package/client/dist/assets/agent-i75WDZOA.js +1 -0
- package/client/dist/assets/browser-05vRDg_y.js +1 -0
- package/client/dist/assets/browser-Bulb-cB1.js +1 -0
- package/client/dist/assets/browser-C7f-Jsg1.js +1 -0
- package/client/dist/assets/browser-CG0PE1j_.js +1 -0
- package/client/dist/assets/browser-CMzaqAjo.js +1 -0
- package/client/dist/assets/browser-DMajYdNX.js +1 -0
- package/client/dist/assets/browser-DxcOHrdP.js +1 -0
- package/client/dist/assets/browser-ObE7xVo3.js +1 -0
- package/client/dist/assets/builder-765NeYTp.js +1 -0
- package/client/dist/assets/builder-8MG3V6ea.js +1 -0
- package/client/dist/assets/builder-B-1UBs5K.js +1 -0
- package/client/dist/assets/builder-BTiGnPT0.js +1 -0
- package/client/dist/assets/builder-Cx9Rz-7u.js +1 -0
- package/client/dist/assets/builder-DKFMELBl.js +1 -0
- package/client/dist/assets/builder-OcmKrC0h.js +1 -0
- package/client/dist/assets/builder-P9qVBXFZ.js +1 -0
- package/client/dist/assets/{button-CJqFnUOL.js → button-DCBqEaZl.js} +1 -1
- package/client/dist/assets/{code-CkUuW4m9.js → code-B37GRg9C.js} +1 -1
- package/client/dist/assets/{code-B0O8-p8k.js → code-BBOEZbf9.js} +1 -1
- package/client/dist/assets/{code-B1VLZDhS.js → code-BRm-jcyx.js} +1 -1
- package/client/dist/assets/{code-Bpj4Ddml.js → code-CF7w62CR.js} +1 -1
- package/client/dist/assets/{code-BykBZ5-e.js → code-CLuvJt2r.js} +1 -1
- package/client/dist/assets/{code-B86IMAfz.js → code-D0RCm59f.js} +1 -1
- package/client/dist/assets/code-DaZ-ZwCT.js +1 -0
- package/client/dist/assets/{code-cAqaqMTx.js → code-iIHkGaCL.js} +1 -1
- package/client/dist/assets/commands-0XjAftla.js +1 -0
- package/client/dist/assets/commands-BAWSYHjQ.js +1 -0
- package/client/dist/assets/commands-BBwowgiE.js +1 -0
- package/client/dist/assets/commands-BacjIZLN.js +1 -0
- package/client/dist/assets/commands-Bs62IAZ3.js +1 -0
- package/client/dist/assets/commands-CLPdh-Hh.js +1 -0
- package/client/dist/assets/commands-DlbFzgrp.js +1 -0
- package/client/dist/assets/commands-LjPSsi5F.js +1 -0
- package/client/dist/assets/common-B5AZNYMH.js +1 -0
- package/client/dist/assets/common-BFZ0c_vN.js +1 -0
- package/client/dist/assets/common-BrlwEnL7.js +1 -0
- package/client/dist/assets/common-BttQx3z4.js +1 -0
- package/client/dist/assets/common-CM65k_-e.js +1 -0
- package/client/dist/assets/common-D4mvFFlu.js +1 -0
- package/client/dist/assets/common-TZ9Ny96S.js +1 -0
- package/client/dist/assets/common-nFpSF4GC.js +1 -0
- package/client/dist/assets/dashboard-B14uYXkG.js +1 -0
- package/client/dist/assets/dashboard-BK_mez-z.js +1 -0
- package/client/dist/assets/dashboard-BfBir_6o.js +1 -0
- package/client/dist/assets/dashboard-Bt0cCzeG.js +1 -0
- package/client/dist/assets/dashboard-BwkjxI6H.js +1 -0
- package/client/dist/assets/{dashboard-C8WOEiPu.js → dashboard-C3bDKf7s.js} +1 -1
- package/client/dist/assets/dashboard-CdvjGfog.js +1 -0
- package/client/dist/assets/dashboard-fRrP0Kv0.js +1 -0
- package/client/dist/assets/dialog-SRw01_QU.js +45 -0
- package/client/dist/assets/dist-js-CJ_2XX1Y.js +1 -0
- package/client/dist/assets/formatDistanceToNow-DSj5DprF.js +1 -0
- package/client/dist/assets/getTimezoneOffsetInMilliseconds-YhD8rIEs.js +1 -0
- package/client/dist/assets/index-DwAmL7m5.css +2 -0
- package/client/dist/assets/index-EzrkUb8B.js +76 -0
- package/client/dist/assets/{jira-api-B1-MUehg.js → jira-api-CSCqGr-Q.js} +1 -1
- package/client/dist/assets/{jobs-CN_zv4Gr.js → jobs-B8rrzfyZ.js} +1 -1
- package/client/dist/assets/{jobs-DUlWSYq2.js → jobs-BS6RTurf.js} +1 -1
- package/client/dist/assets/{jobs-Bpks6doU.js → jobs-BTT0Bsks.js} +1 -1
- package/client/dist/assets/{jobs-Bgq-PZkC.js → jobs-CezU69AE.js} +1 -1
- package/client/dist/assets/{jobs-SwYS2M-B.js → jobs-ClFJae0s.js} +1 -1
- package/client/dist/assets/{jobs-BAls3bWq.js → jobs-DJSFIFVy.js} +1 -1
- package/client/dist/assets/{jobs-DhxKgWFq.js → jobs-DhgGr8vm.js} +1 -1
- package/client/dist/assets/{jobs-B_TeSQxb.js → jobs-DzMjUcaN.js} +1 -1
- package/client/dist/assets/mcp-6rMoZfEO.js +1 -0
- package/client/dist/assets/mcp-BD8sL5mP.js +1 -0
- package/client/dist/assets/mcp-BkcA3I5s.js +1 -0
- package/client/dist/assets/mcp-CCpSelOh.js +1 -0
- package/client/dist/assets/mcp-CUAN6ORK.js +1 -0
- package/client/dist/assets/mcp-CqbRxRTE.js +1 -0
- package/client/dist/assets/mcp-DP2YfVhO.js +1 -0
- package/client/dist/assets/mcp-DUVQamDz.js +1 -0
- package/client/dist/assets/narration-B-ynWTpn.js +1 -0
- package/client/dist/assets/narration-Bj_laZwg.js +1 -0
- package/client/dist/assets/narration-BxHpmMv_.js +1 -0
- package/client/dist/assets/narration-C1Px0-0j.js +1 -0
- package/client/dist/assets/narration-CAXhGbzM.js +1 -0
- package/client/dist/assets/narration-DQ1l_Kq6.js +1 -0
- package/client/dist/assets/narration-XISXwqZd.js +1 -0
- package/client/dist/assets/narration-kwzfglQo.js +1 -0
- package/client/dist/assets/nav-1-Hh2kOz.js +1 -0
- package/client/dist/assets/nav-B34QkESm.js +1 -0
- package/client/dist/assets/nav-Bap_UXAq.js +1 -0
- package/client/dist/assets/{nav-pYuXq7sf.js → nav-BwxXIB24.js} +1 -1
- package/client/dist/assets/nav-Ch-zHBG5.js +1 -0
- package/client/dist/assets/nav-CygC_WN-.js +1 -0
- package/client/dist/assets/{nav-iwjCg5FR.js → nav-KnnLQtOQ.js} +1 -1
- package/client/dist/assets/nav-m00uxYUG.js +1 -0
- package/client/dist/assets/packet-BQ2tXfJI.js +1 -0
- package/client/dist/assets/packet-BdpOJ1Cy.js +1 -0
- package/client/dist/assets/packet-C3Ievb9R.js +1 -0
- package/client/dist/assets/packet-C4zJIG0Y.js +1 -0
- package/client/dist/assets/packet-Cx-zRuWn.js +1 -0
- package/client/dist/assets/packet-DUTGX52M.js +1 -0
- package/client/dist/assets/packet-DhsKtUPB.js +1 -0
- package/client/dist/assets/packet-z3LJ8Zyy.js +1 -0
- package/client/dist/assets/project-repositories-DYhTvwQ1.js +1 -0
- package/client/dist/assets/provider-capabilities-B-wOHfLP.js +1 -0
- package/client/dist/assets/settings-BBt2xtDQ.js +1 -0
- package/client/dist/assets/settings-BJB9RKJj.js +1 -0
- package/client/dist/assets/settings-CHwQR2rB.js +1 -0
- package/client/dist/assets/settings-CrMi3kSi.js +1 -0
- package/client/dist/assets/settings-DKyhDGD1.js +1 -0
- package/client/dist/assets/settings-Deupji9c.js +1 -0
- package/client/dist/assets/settings-DqRMmwAM.js +1 -0
- package/client/dist/assets/settings-J4rj6vo-.js +1 -0
- package/client/dist/assets/setup-2xGgu4Sk.js +1 -0
- package/client/dist/assets/setup-Ba11l2JA.js +1 -0
- package/client/dist/assets/setup-BzW4kMok.js +1 -0
- package/client/dist/assets/setup-COzwYhkJ.js +1 -0
- package/client/dist/assets/setup-CRVULhtF.js +1 -0
- package/client/dist/assets/setup-DgvlxwpE.js +1 -0
- package/client/dist/assets/setup-Dw_wYeEY.js +1 -0
- package/client/dist/assets/setup-ZPtEeJPW.js +1 -0
- package/client/dist/assets/spending-BqbdPm_4.js +1 -0
- package/client/dist/assets/square-Ci-PFcFe.js +1 -0
- package/client/dist/assets/{terminal-BFRZnMPP.js → terminal-BAzpHtpY.js} +1 -1
- package/client/dist/assets/{terminal-IahE4LW9.js → terminal-C7dHeR3x.js} +1 -1
- package/client/dist/assets/{terminal-tITa81iz.js → terminal-CMla5Qsz.js} +1 -1
- package/client/dist/assets/{terminal-BZiIu3rf.js → terminal-CdNXH-n9.js} +1 -1
- package/client/dist/assets/{terminal-ScjyXNxQ.js → terminal-Cw5bZsmV.js} +1 -1
- package/client/dist/assets/{terminal-C2NAd7BW.js → terminal-DFUP4eNv.js} +1 -1
- package/client/dist/assets/{terminal-d7GadXbN.js → terminal-DGqljeK_.js} +1 -1
- package/client/dist/assets/{terminal-QpXZU2Bn.js → terminal-cU70NodO.js} +1 -1
- package/client/dist/assets/useDesktop-RSMTyy7V.js +1 -0
- package/client/dist/assets/useSharedWebSocket-BOdS5_Iz.js +2 -0
- package/client/dist/index.html +34 -35
- package/docs/README.md +3 -0
- package/docs/agent-live-steering.md +57 -0
- package/docs/ci-cd.md +57 -0
- package/docs/code-explorer.md +48 -0
- package/docs/codex.md +12 -1
- package/docs/features/detachable-mission-windows.md +59 -0
- package/docs/guide/de/integrations/5-mcp-server.md +13 -9
- package/docs/guide/de/integrations/6-agent-chat.md +1 -0
- package/docs/guide/en/integrations/5-mcp-server.md +13 -9
- package/docs/guide/en/integrations/6-agent-chat.md +1 -0
- package/docs/guide/es/integrations/5-mcp-server.md +13 -9
- package/docs/guide/es/integrations/6-agent-chat.md +1 -0
- package/docs/guide/fr/integrations/5-mcp-server.md +13 -9
- package/docs/guide/fr/integrations/6-agent-chat.md +1 -0
- package/docs/guide/it/integrations/5-mcp-server.md +13 -9
- package/docs/guide/it/integrations/6-agent-chat.md +1 -0
- package/docs/guide/ja/integrations/5-mcp-server.md +13 -9
- package/docs/guide/ja/integrations/6-agent-chat.md +1 -0
- package/docs/guide/pt/integrations/5-mcp-server.md +13 -9
- package/docs/guide/pt/integrations/6-agent-chat.md +1 -0
- package/docs/guide/zh/integrations/5-mcp-server.md +13 -9
- package/docs/guide/zh/integrations/6-agent-chat.md +1 -0
- package/docs/internals/README.md +1 -0
- package/docs/internals/browser-capture-performance.md +55 -3
- package/docs/internals/browser-login-popups.md +45 -0
- package/docs/internals/browser-native-retina-audit.md +39 -0
- package/docs/internals/ci-cd-audit.md +39 -0
- package/docs/internals/core-runtime-updates.md +68 -0
- package/docs/internals/embedded-browser-native-webview-evaluation.md +2 -0
- package/docs/internals/implementation-reliability-audit.md +93 -0
- package/docs/internals/interactive-jobs.md +110 -6
- package/docs/internals/mcp-mission-audit.md +76 -0
- package/docs/internals/project-builder.md +404 -42
- package/docs/internals/safe-pr-review-flow.md +52 -10
- package/docs/internals/startup-recovery-audit.md +58 -0
- package/docs/mcp.md +74 -41
- package/docs/mission-processes.md +59 -0
- package/docs/multi-repo-projects.md +51 -0
- package/docs/platforms/windows-parity.md +70 -0
- package/docs/platforms/windows.md +13 -11
- package/docs/running-pipelines.md +14 -0
- package/mcp-bridge/dist/specrails-mcp.js +7186 -0
- package/package.json +9 -4
- package/server/dist/agent-chat-manager.js +537 -68
- package/server/dist/agent-chat-router.js +87 -6
- package/server/dist/agent-context-resolver.js +129 -25
- package/server/dist/agent-defaults.js +19 -11
- package/server/dist/agent-input-store.js +206 -0
- package/server/dist/agent-mcp-config.js +54 -15
- package/server/dist/agent-operator-prompt.js +255 -65
- package/server/dist/agent-spec-framing.js +202 -0
- package/server/dist/agent-steering.js +218 -0
- package/server/dist/agent-store.js +153 -0
- package/server/dist/api-not-found.js +8 -0
- package/server/dist/background-process-control.js +164 -0
- package/server/dist/background-process-service.js +61 -0
- package/server/dist/background-process-store.js +242 -0
- package/server/dist/background-windows-bootstrap.js +238 -0
- package/server/dist/blueprint-chat-manager.js +476 -62
- package/server/dist/blueprint-commit.js +21 -1
- package/server/dist/blueprint-draft-parser.js +122 -17
- package/server/dist/blueprint-generation.js +238 -0
- package/server/dist/blueprint-operator-prompt.js +91 -63
- package/server/dist/blueprint-router.js +55 -3
- package/server/dist/blueprint-spec-fixtures.js +85 -0
- package/server/dist/blueprint-spec-quality.js +57 -16
- package/server/dist/blueprint-store.js +92 -1
- package/server/dist/browser-capture-manager.js +86 -30
- package/server/dist/browser-playwright.js +168 -45
- package/server/dist/browser-viewport.js +26 -0
- package/server/dist/chat-manager.js +23 -15
- package/server/dist/chromium-archive.cjs +83 -0
- package/server/dist/chromium-resolver.js +72 -64
- package/server/dist/code-activity.js +145 -0
- package/server/dist/code-explorer-router.js +714 -114
- package/server/dist/core-compat.js +28 -1
- package/server/dist/core-execution.js +111 -0
- package/server/dist/core-package.js +4 -16
- package/server/dist/core-runtime.js +202 -0
- package/server/dist/core-update-manager.js +161 -60
- package/server/dist/core-update-state.js +29 -0
- package/server/dist/db.js +66 -35
- package/server/dist/delivery-evidence.js +142 -39
- package/server/dist/desktop-db.js +239 -43
- package/server/dist/desktop-router.js +109 -9
- package/server/dist/dev-ports.js +8 -3
- package/server/dist/file-provenance.js +34 -22
- package/server/dist/file-story-manager.js +62 -64
- package/server/dist/file-story.js +118 -35
- package/server/dist/file-summary-generator.js +46 -5
- package/server/dist/file-summary-manager.js +355 -106
- package/server/dist/framework-manager.js +98 -110
- package/server/dist/framework-reseed.js +76 -10
- package/server/dist/git-diagnostics.js +2 -2
- package/server/dist/headroom-manager.js +38 -3
- package/server/dist/host-control.js +52 -0
- package/server/dist/index.js +43 -2
- package/server/dist/interactive-job-session.js +149 -16
- package/server/dist/internal-api.js +48 -0
- package/server/dist/jira/jira-materializer.js +1 -0
- package/server/dist/json-tolerant.js +174 -0
- package/server/dist/legacy-migration.js +4 -2
- package/server/dist/loop-command-catalog.js +83 -21
- package/server/dist/loop-constants.js +6 -1
- package/server/dist/loop-decider.js +11 -1
- package/server/dist/loop-executors.js +194 -45
- package/server/dist/loop-factory.js +11 -4
- package/server/dist/loop-graph.js +90 -4
- package/server/dist/loop-run-manager.js +301 -69
- package/server/dist/loop-shell-invocation.js +21 -0
- package/server/dist/loop-step-idle.js +55 -0
- package/server/dist/loop-templates.js +31 -7
- package/server/dist/mcp/agent-capability.js +37 -4
- package/server/dist/mcp/guide.js +149 -36
- package/server/dist/mcp/mcp-admin-router.js +20 -4
- package/server/dist/mcp/mcp-server.js +89 -43
- package/server/dist/mcp/mcp-tiers.js +6 -5
- package/server/dist/mcp/mcp-token.js +3 -1
- package/server/dist/mcp/resources.js +5 -4
- package/server/dist/mcp/tools/catalog.js +4 -0
- package/server/dist/mcp/tools/code.js +125 -14
- package/server/dist/mcp/tools/context.js +153 -0
- package/server/dist/mcp/tools/git.js +15 -5
- package/server/dist/mcp/tools/jobs.js +84 -71
- package/server/dist/mcp/tools/loops.js +17 -6
- package/server/dist/mcp/tools/meta.js +74 -27
- package/server/dist/mcp/tools/mission.js +29 -0
- package/server/dist/mcp/tools/projects.js +22 -6
- package/server/dist/mcp/tools/rails.js +48 -11
- package/server/dist/mcp/tools/specs.js +86 -14
- package/server/dist/mcp/tools/support.js +1 -1
- package/server/dist/mcp/tools/types.js +117 -43
- package/server/dist/mcp/tools/watch.js +144 -51
- package/server/dist/milestone-chain-store.js +164 -0
- package/server/dist/milestone-chain.js +487 -0
- package/server/dist/milestone-progress.js +348 -0
- package/server/dist/mobile/mobile-devices.js +10 -7
- package/server/dist/mobile/mobile-gateway.js +2 -0
- package/server/dist/mobile/mobile-missions.js +214 -0
- package/server/dist/mobile/mobile-redact.js +17 -3
- package/server/dist/mobile/mobile-router.js +46 -4
- package/server/dist/mobile/mobile-ws.js +49 -4
- package/server/dist/multi-repo-bases.js +56 -0
- package/server/dist/multi-repo-checkout.js +101 -0
- package/server/dist/multi-repo-delivery.js +100 -0
- package/server/dist/multi-repo-execution-store.js +65 -0
- package/server/dist/multi-repo-execution.js +453 -0
- package/server/dist/offline-assemble.js +10 -8
- package/server/dist/openspec-runtime-plugin-commands.json +18 -0
- package/server/dist/openspec-runtime-plugin.js +120 -0
- package/server/dist/plugins/codex-spawn.js +49 -0
- package/server/dist/plugins/serena/manifest.js +2 -2
- package/server/dist/plugins/serena/templates/instructions.md +16 -0
- package/server/dist/pr-publisher.js +1 -1
- package/server/dist/profiles-router.js +3 -18
- package/server/dist/project-code-discovery.js +142 -0
- package/server/dist/project-git.js +19 -21
- package/server/dist/project-registry.js +272 -20
- package/server/dist/project-repositories.js +157 -0
- package/server/dist/project-repository-provenance.js +31 -0
- package/server/dist/project-router-background-processes.js +105 -0
- package/server/dist/project-router-git.js +75 -16
- package/server/dist/project-router-jobs.js +2 -126
- package/server/dist/project-router-loop-runs.js +72 -2
- package/server/dist/project-router-tickets.js +53 -8
- package/server/dist/project-router.js +239 -38
- package/server/dist/provider-detection.js +18 -6
- package/server/dist/provider-limit.js +64 -0
- package/server/dist/providers/claude-adapter.js +26 -1
- package/server/dist/providers/claude-background-tasks.js +18 -0
- package/server/dist/providers/claude-live-session.js +383 -0
- package/server/dist/providers/codex-adapter.js +19 -0
- package/server/dist/providers/codex-live-session.js +545 -0
- package/server/dist/providers/gemini-adapter.js +1 -0
- package/server/dist/providers/live-session-types.js +12 -0
- package/server/dist/providers/live-session.js +13 -0
- package/server/dist/providers/terminal-result.js +24 -0
- package/server/dist/queue-manager.js +2 -0
- package/server/dist/rail-isolated-launch.js +141 -16
- package/server/dist/rail-pr-decision.js +364 -99
- package/server/dist/rail-pr-store.js +24 -10
- package/server/dist/rail-worktree-release.js +34 -2
- package/server/dist/rail-worktrees-store.js +4 -4
- package/server/dist/rails-router.js +249 -55
- package/server/dist/repo-lock.js +92 -17
- package/server/dist/review-packet.js +23 -13
- package/server/dist/schemas/file-summary.v1.json +59 -59
- package/server/dist/schemas/profile.v1.json +137 -137
- package/server/dist/setup-manager.js +52 -8
- package/server/dist/shell-integration/bash-shim.bash +45 -0
- package/server/dist/shell-integration/fish-shim.fish +36 -0
- package/server/dist/shell-integration/powershell-shim.ps1 +48 -0
- package/server/dist/shell-integration/zsh-shim.zsh +47 -0
- package/server/dist/smash-runner.js +1 -0
- package/server/dist/spawn-lifecycle.js +44 -27
- package/server/dist/spec-contract-prompt.js +143 -0
- package/server/dist/spec-draft-parser.js +6 -1
- package/server/dist/spec-models.js +1 -0
- package/server/dist/stuck-run-detector.js +1 -0
- package/server/dist/terminal-manager.js +21 -1
- package/server/dist/terminal-shell-integration.js +4 -1
- package/server/dist/transient-children.js +619 -230
- package/server/dist/util/cli-prompt.js +78 -5
- package/server/dist/util/sqlite-migrations.js +20 -0
- package/server/dist/util/win-spawn.js +18 -3
- package/server/dist/util/windows-provider-shell.js +29 -0
- package/server/dist/verification-sentinel.js +14 -0
- package/server/dist/vitest-setup.js +2 -0
- package/server/dist/windows-job-supervisor.js +133 -0
- package/server/dist/workspace-manager.js +9 -30
- package/server/dist/worktree-manager.js +17 -2
- package/server/dist/worktree-overlay.js +3 -1
- package/client/dist/assets/ActivityFeedPage-tAQcWs1R.js +0 -1
- package/client/dist/assets/AgentBrowserCapture-BX8BNae5.js +0 -1
- package/client/dist/assets/AgentModeAnalyticsPane-BVAqCL0K.js +0 -2
- package/client/dist/assets/AgentModeCodePane-9XfqQc9c.js +0 -2
- package/client/dist/assets/AgentModeJobsPane-BCjLsPhc.js +0 -1
- package/client/dist/assets/AgentsPage-C8J_42sm.js +0 -87
- package/client/dist/assets/AnalyticsPage-BtQuyfJm.js +0 -1
- package/client/dist/assets/CodePage-BZ2MwdCu.js +0 -2
- package/client/dist/assets/DesktopAnalyticsPage-Bh_29V4g.js +0 -1
- package/client/dist/assets/ExportDropdown-BrtfWzEG.js +0 -1
- package/client/dist/assets/InteractiveJobComposer-BCZiSfzc.js +0 -19
- package/client/dist/assets/JobDetailModal-NAJmuYIT.js +0 -1
- package/client/dist/assets/JobDetailPage-fE0ZycUX.js +0 -1
- package/client/dist/assets/JobsPage-z5M_HVJG.js +0 -1
- package/client/dist/assets/LoopBuilderPage-j1FQS47c.js +0 -7
- package/client/dist/assets/LoopPreviewModal-DhDJjdCe.js +0 -1
- package/client/dist/assets/LoopsPage-Ll7zORZb.js +0 -1
- package/client/dist/assets/MinimizedChatsContext-Bd3OqR_S.js +0 -1
- package/client/dist/assets/PluginsPage-DN4kGvJt.js +0 -2
- package/client/dist/assets/ProjectSettingsDialog-OgsgxjzF.js +0 -1
- package/client/dist/assets/ReviewPacketPage-DFog4sXn.js +0 -1
- package/client/dist/assets/TemplatePreviewModal-L2XxqiOD.js +0 -1
- package/client/dist/assets/TicketDetailModal-CsF-AWZX.js +0 -14
- package/client/dist/assets/Trans-Bsz9_gyq.js +0 -1
- package/client/dist/assets/agent-BQgKv_2F.js +0 -1
- package/client/dist/assets/agent-C1NRDJqL.js +0 -1
- package/client/dist/assets/agent-CLNleiy6.js +0 -1
- package/client/dist/assets/agent-CmceArkQ.js +0 -1
- package/client/dist/assets/agent-D3UxQ3SR.js +0 -1
- package/client/dist/assets/agent-DB1JeE64.js +0 -1
- package/client/dist/assets/agent-DXkHcQ-D.js +0 -1
- package/client/dist/assets/agent-DjJJJ2M8.js +0 -1
- package/client/dist/assets/auth-D_5bQv5W.js +0 -1
- package/client/dist/assets/ban-E4EaZe3a.js +0 -1
- package/client/dist/assets/bot-DzuuuC5R.js +0 -1
- package/client/dist/assets/browser-BgqHDMXP.js +0 -1
- package/client/dist/assets/browser-CsKJtrpv.js +0 -1
- package/client/dist/assets/browser-Cu58EpZU.js +0 -1
- package/client/dist/assets/browser-CvXOJm-j.js +0 -1
- package/client/dist/assets/browser-DOrFh1c7.js +0 -1
- package/client/dist/assets/browser-DQv_Jo7x.js +0 -1
- package/client/dist/assets/browser-LPgxvJu2.js +0 -1
- package/client/dist/assets/browser-eL-3MIEI.js +0 -1
- package/client/dist/assets/builder-0IWtosUm.js +0 -1
- package/client/dist/assets/builder-Bf2N6XWn.js +0 -1
- package/client/dist/assets/builder-BnqaaBkT.js +0 -1
- package/client/dist/assets/builder-BxxfGgAO.js +0 -1
- package/client/dist/assets/builder-C5IIgWsq.js +0 -1
- package/client/dist/assets/builder-CHuahItu.js +0 -1
- package/client/dist/assets/builder-CjWosXL_.js +0 -1
- package/client/dist/assets/builder-CpCIOoKj.js +0 -1
- package/client/dist/assets/code-CHPkOkDS.js +0 -1
- package/client/dist/assets/commands-6tYQih5m.js +0 -1
- package/client/dist/assets/commands-BK2QinXN.js +0 -1
- package/client/dist/assets/commands-C2Nca76h.js +0 -1
- package/client/dist/assets/commands-CKdLihNX.js +0 -1
- package/client/dist/assets/commands-CYIBTAlc.js +0 -1
- package/client/dist/assets/commands-CjUtYJIM.js +0 -1
- package/client/dist/assets/commands-Cq_u_8y9.js +0 -1
- package/client/dist/assets/commands-D_ZHYQug.js +0 -1
- package/client/dist/assets/common-6MLbD-jM.js +0 -1
- package/client/dist/assets/common-BKGEu4rm.js +0 -1
- package/client/dist/assets/common-BNbnv8cE.js +0 -1
- package/client/dist/assets/common-BqGWtDbt.js +0 -1
- package/client/dist/assets/common-DBtTzeYm.js +0 -1
- package/client/dist/assets/common-DEW3Nr8n.js +0 -1
- package/client/dist/assets/common-DHsv9N5d.js +0 -1
- package/client/dist/assets/common-mkrEcW-Y.js +0 -1
- package/client/dist/assets/dashboard-B9GhC5ku.js +0 -1
- package/client/dist/assets/dashboard-CBnv6zOI.js +0 -1
- package/client/dist/assets/dashboard-CaP0fU87.js +0 -1
- package/client/dist/assets/dashboard-D4ZZ4x84.js +0 -1
- package/client/dist/assets/dashboard-DmuTQBX8.js +0 -1
- package/client/dist/assets/dashboard-DynLYj3d.js +0 -1
- package/client/dist/assets/dashboard-zj4gGFJ1.js +0 -1
- package/client/dist/assets/dialog-CdWCb02j.js +0 -45
- package/client/dist/assets/dist-bVqfYmby.js +0 -1
- package/client/dist/assets/dist-js-CWeUdf5Z.js +0 -1
- package/client/dist/assets/formatDistanceToNow-DQCvMgUc.js +0 -1
- package/client/dist/assets/getTimezoneOffsetInMilliseconds-C16cxSe1.js +0 -1
- package/client/dist/assets/git-refresh-wTW0rO0Z.js +0 -1
- package/client/dist/assets/i18n-hnX2DPmo.js +0 -2
- package/client/dist/assets/index-DIhMEAVw.js +0 -66
- package/client/dist/assets/index-DwygcU7s.css +0 -2
- package/client/dist/assets/mcp-B8X8wBLu.js +0 -1
- package/client/dist/assets/mcp-C2AojZZN.js +0 -1
- package/client/dist/assets/mcp-D8BT78_L.js +0 -1
- package/client/dist/assets/mcp-Df5RWBNk.js +0 -1
- package/client/dist/assets/mcp-Dj7BgMQW.js +0 -1
- package/client/dist/assets/mcp-Q7iT7rcz.js +0 -1
- package/client/dist/assets/mcp-YlvNvJbW.js +0 -1
- package/client/dist/assets/mcp-mKFpTSRb.js +0 -1
- package/client/dist/assets/narration-4Z2VUhZw.js +0 -1
- package/client/dist/assets/narration-BGuiEDj3.js +0 -1
- package/client/dist/assets/narration-BuSoqRmv.js +0 -1
- package/client/dist/assets/narration-CB9t29fy.js +0 -1
- package/client/dist/assets/narration-CU3x8FHD.js +0 -1
- package/client/dist/assets/narration-CWeXhfP5.js +0 -1
- package/client/dist/assets/narration-DRaDnhT5.js +0 -1
- package/client/dist/assets/narration-DtEbQgCw.js +0 -1
- package/client/dist/assets/nav-ApJVg3xu.js +0 -1
- package/client/dist/assets/nav-BKNky1Bg.js +0 -1
- package/client/dist/assets/nav-C6vWWKYx.js +0 -1
- package/client/dist/assets/nav-CXJ6Aw89.js +0 -1
- package/client/dist/assets/nav-CsmFBIDf.js +0 -1
- package/client/dist/assets/nav-DjkmQJjd.js +0 -1
- package/client/dist/assets/packet-B1IxaL5f.js +0 -1
- package/client/dist/assets/packet-Bh5kkbiB.js +0 -1
- package/client/dist/assets/packet-By8fqYxW.js +0 -1
- package/client/dist/assets/packet-CZdvA44E.js +0 -1
- package/client/dist/assets/packet-CrjHRQCP.js +0 -1
- package/client/dist/assets/packet-Dwi2pTtx.js +0 -1
- package/client/dist/assets/packet-_-ZIa0IR.js +0 -1
- package/client/dist/assets/packet-n0W6E1Hn.js +0 -1
- package/client/dist/assets/play-DNlrawbS.js +0 -1
- package/client/dist/assets/provider-capabilities-Doej9lfa.js +0 -1
- package/client/dist/assets/settings-Bd0x4TBa.js +0 -1
- package/client/dist/assets/settings-C1-7cXzC.js +0 -1
- package/client/dist/assets/settings-CAFaT38b.js +0 -1
- package/client/dist/assets/settings-Ct3v1nW2.js +0 -1
- package/client/dist/assets/settings-DHh7_IF5.js +0 -1
- package/client/dist/assets/settings-DQXAmdcv.js +0 -1
- package/client/dist/assets/settings-guzlhpup.js +0 -1
- package/client/dist/assets/settings-sZfTPMEH.js +0 -1
- package/client/dist/assets/setup-3jWaoVUO.js +0 -1
- package/client/dist/assets/setup-BjgRneb2.js +0 -1
- package/client/dist/assets/setup-BzFIjv-f.js +0 -1
- package/client/dist/assets/setup-C4VfQsxE.js +0 -1
- package/client/dist/assets/setup-DfUkpDbN.js +0 -1
- package/client/dist/assets/setup-ECe90Pmu.js +0 -1
- package/client/dist/assets/setup-dlqC4Jmm.js +0 -1
- package/client/dist/assets/setup-jwfLMiM2.js +0 -1
- package/client/dist/assets/spending-EC9xqio-.js +0 -1
- package/client/dist/assets/useDesktop-BQVt8lFd.js +0 -1
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.BACKGROUND_LOG_RETENTION_MS = void 0;
|
|
3
|
+
exports.BACKGROUND_PERSISTENCE_FLUSH_MS = exports.BACKGROUND_LOG_RETENTION_MS = void 0;
|
|
4
|
+
exports.initializeBackgroundProcessPersistence = initializeBackgroundProcessPersistence;
|
|
5
|
+
exports.flushBackgroundProcessPersistence = flushBackgroundProcessPersistence;
|
|
6
|
+
exports.closeBackgroundProcessPersistence = closeBackgroundProcessPersistence;
|
|
7
|
+
exports.purgeBackgroundProcessHistory = purgeBackgroundProcessHistory;
|
|
7
8
|
exports.trackTransientChild = trackTransientChild;
|
|
8
9
|
exports.killTransientChildren = killTransientChildren;
|
|
9
10
|
exports.startBackgroundProcess = startBackgroundProcess;
|
|
@@ -13,62 +14,238 @@ exports.listBackgroundProcesses = listBackgroundProcesses;
|
|
|
13
14
|
exports.killBackgroundProcess = killBackgroundProcess;
|
|
14
15
|
exports.killOwnedBackgroundProcess = killOwnedBackgroundProcess;
|
|
15
16
|
exports.killBackgroundProcessesForChat = killBackgroundProcessesForChat;
|
|
16
|
-
|
|
17
|
+
exports.awaitBackgroundProcessesStopped = awaitBackgroundProcessesStopped;
|
|
18
|
+
const win_spawn_1 = require("./util/win-spawn");
|
|
17
19
|
const child_process_1 = require("child_process");
|
|
18
|
-
const
|
|
20
|
+
const crypto_1 = require("crypto");
|
|
21
|
+
const string_decoder_1 = require("string_decoder");
|
|
19
22
|
const process_admission_1 = require("./process-admission");
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
* it survived project removal (burning AI-CLI spend) until the watchdog fired.
|
|
26
|
-
*
|
|
27
|
-
* Children registered here are tree-killed when their project is removed or the
|
|
28
|
-
* app shuts down. Self-cleaning: each child is auto-unregistered on `close`.
|
|
29
|
-
*/
|
|
23
|
+
const background_process_control_1 = require("./background-process-control");
|
|
24
|
+
const win_spawn_2 = require("./util/win-spawn");
|
|
25
|
+
const background_process_store_1 = require("./background-process-store");
|
|
26
|
+
const background_windows_bootstrap_1 = require("./background-windows-bootstrap");
|
|
27
|
+
/** Fire-and-forget children not already owned by a long-lived provider manager. */
|
|
30
28
|
const byProject = new Map();
|
|
31
29
|
const backgroundByPid = new Map();
|
|
32
|
-
const
|
|
33
|
-
const backgroundTerminal = new Set(['exited', 'killed', 'failed']);
|
|
30
|
+
const finishedBackgroundById = new Map();
|
|
31
|
+
const backgroundTerminal = new Set(['exited', 'killed', 'failed', 'interrupted']);
|
|
34
32
|
const BACKGROUND_KILL_GRACE_MS = 2500;
|
|
35
|
-
const
|
|
33
|
+
const BACKGROUND_STOP_CONFIRM_MS = 2500;
|
|
34
|
+
const BACKGROUND_OUTPUT_MAX_LINE_CHARS = 4000;
|
|
36
35
|
const BACKGROUND_OUTPUT_MAX_LINES_PER_SECOND = 20;
|
|
37
|
-
const BACKGROUND_LOG_MAX_LINES =
|
|
36
|
+
const BACKGROUND_LOG_MAX_LINES = 2000;
|
|
37
|
+
const BACKGROUND_FINISHED_MAX = 32;
|
|
38
38
|
exports.BACKGROUND_LOG_RETENTION_MS = 10 * 60 * 1000;
|
|
39
|
+
exports.BACKGROUND_PERSISTENCE_FLUSH_MS = 250;
|
|
40
|
+
const pendingPersistence = new Map();
|
|
41
|
+
let backgroundStore;
|
|
42
|
+
let persistenceAttempted = false;
|
|
43
|
+
let persistenceError;
|
|
44
|
+
let persistenceTimer;
|
|
45
|
+
let lastPersistenceAttempt = 0;
|
|
46
|
+
let persistenceRetryDelay = exports.BACKGROUND_PERSISTENCE_FLUSH_MS;
|
|
47
|
+
let persistenceFile;
|
|
48
|
+
let persistenceClosed = false;
|
|
49
|
+
let lastHistoryOpenAttempt = 0;
|
|
50
|
+
function ensureBackgroundProcessStore() {
|
|
51
|
+
if (backgroundStore || !persistenceAttempted)
|
|
52
|
+
return backgroundStore;
|
|
53
|
+
if (!persistenceClosed && persistenceFile && Date.now() - lastHistoryOpenAttempt >= 1000) {
|
|
54
|
+
try {
|
|
55
|
+
initializeBackgroundProcessPersistence(persistenceFile);
|
|
56
|
+
}
|
|
57
|
+
catch { /* report the retained open failure below */ }
|
|
58
|
+
}
|
|
59
|
+
if (!backgroundStore)
|
|
60
|
+
throw new Error(`Background process history is unavailable: ${persistenceClosed ? 'the store is closed' : persistenceError ?? 'the store could not be opened'}. Retry after the storage problem is resolved.`);
|
|
61
|
+
return backgroundStore;
|
|
62
|
+
}
|
|
63
|
+
function schedulePersistenceFlush(delay) {
|
|
64
|
+
if (persistenceTimer || !pendingPersistence.size || !backgroundStore)
|
|
65
|
+
return;
|
|
66
|
+
persistenceTimer = setTimeout(() => {
|
|
67
|
+
persistenceTimer = undefined;
|
|
68
|
+
try {
|
|
69
|
+
flushBackgroundProcessPersistence();
|
|
70
|
+
}
|
|
71
|
+
catch { /* failure schedules a bounded retry even for a quiet process */ }
|
|
72
|
+
}, delay);
|
|
73
|
+
persistenceTimer.unref?.();
|
|
74
|
+
}
|
|
75
|
+
function initializeBackgroundProcessPersistence(file) {
|
|
76
|
+
if (backgroundStore) {
|
|
77
|
+
flushBackgroundProcessPersistence();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
persistenceFile = file;
|
|
81
|
+
persistenceClosed = false;
|
|
82
|
+
lastHistoryOpenAttempt = Date.now();
|
|
83
|
+
persistenceAttempted = true;
|
|
84
|
+
try {
|
|
85
|
+
backgroundStore = new background_process_store_1.BackgroundProcessStore(file);
|
|
86
|
+
persistenceError = undefined;
|
|
87
|
+
for (const record of backgroundByPid.values()) {
|
|
88
|
+
for (const line of record.outputLines)
|
|
89
|
+
queuePersistence(record, line);
|
|
90
|
+
queuePersistence(record);
|
|
91
|
+
}
|
|
92
|
+
flushBackgroundProcessPersistence();
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
persistenceError = error instanceof Error ? error.message : String(error);
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function queuePersistence(record, line) {
|
|
100
|
+
if (!backgroundStore || record.skipPersistence)
|
|
101
|
+
return;
|
|
102
|
+
let pending = pendingPersistence.get(record.process.processId);
|
|
103
|
+
if (!pending) {
|
|
104
|
+
pending = { record, lines: new Map() };
|
|
105
|
+
pendingPersistence.set(record.process.processId, pending);
|
|
106
|
+
}
|
|
107
|
+
if (line)
|
|
108
|
+
pending.lines.set(line.sequence, cloneLogLine(line));
|
|
109
|
+
if (pending.lines.size >= BACKGROUND_LOG_MAX_LINES) {
|
|
110
|
+
if (!persistenceError || Date.now() - lastPersistenceAttempt >= exports.BACKGROUND_PERSISTENCE_FLUSH_MS) {
|
|
111
|
+
try {
|
|
112
|
+
flushBackgroundProcessPersistence();
|
|
113
|
+
}
|
|
114
|
+
catch { /* failure stays visible and the buffer remains bounded */ }
|
|
115
|
+
}
|
|
116
|
+
while (pending.lines.size > BACKGROUND_LOG_MAX_LINES)
|
|
117
|
+
pending.lines.delete(pending.lines.keys().next().value);
|
|
118
|
+
}
|
|
119
|
+
schedulePersistenceFlush(persistenceError ? persistenceRetryDelay : exports.BACKGROUND_PERSISTENCE_FLUSH_MS);
|
|
120
|
+
}
|
|
121
|
+
/** One transaction and one bulk line statement per batch, including upserts
|
|
122
|
+
* for partial lines. A crash can lose at most the last 250ms of normal output. */
|
|
123
|
+
function flushBackgroundProcessPersistence() {
|
|
124
|
+
if (persistenceTimer) {
|
|
125
|
+
clearTimeout(persistenceTimer);
|
|
126
|
+
persistenceTimer = undefined;
|
|
127
|
+
}
|
|
128
|
+
if (!backgroundStore) {
|
|
129
|
+
if (persistenceError)
|
|
130
|
+
throw new Error(persistenceError);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
lastPersistenceAttempt = Date.now();
|
|
134
|
+
let failure;
|
|
135
|
+
for (const [processId, pending] of pendingPersistence) {
|
|
136
|
+
if (pending.record.skipPersistence) {
|
|
137
|
+
pendingPersistence.delete(processId);
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const record = pending.record;
|
|
141
|
+
try {
|
|
142
|
+
const { persistenceError: previousError, ...process } = record.process;
|
|
143
|
+
backgroundStore.write({ process, lines: [...pending.lines.values()], nextSequence: record.nextSequence, clipped: record.clippedOutput });
|
|
144
|
+
pendingPersistence.delete(processId);
|
|
145
|
+
delete record.process.persistenceError;
|
|
146
|
+
const retained = finishedBackgroundById.get(processId);
|
|
147
|
+
if (retained)
|
|
148
|
+
delete retained.process.persistenceError;
|
|
149
|
+
if (previousError)
|
|
150
|
+
safelyNotify(() => record.hooks.onUpdated?.(cloneProcess(record.process)));
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
failure = error;
|
|
154
|
+
const message = `Background history could not be saved: ${error instanceof Error ? error.message : String(error)}`;
|
|
155
|
+
if (record.process.persistenceError !== message) {
|
|
156
|
+
record.process.persistenceError = message;
|
|
157
|
+
const retained = finishedBackgroundById.get(processId);
|
|
158
|
+
if (retained)
|
|
159
|
+
retained.process.persistenceError = message;
|
|
160
|
+
safelyNotify(() => record.hooks.onUpdated?.(cloneProcess(record.process)));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
persistenceError = failure ? (failure instanceof Error ? failure.message : String(failure)) : undefined;
|
|
165
|
+
if (failure) {
|
|
166
|
+
schedulePersistenceFlush(persistenceRetryDelay);
|
|
167
|
+
persistenceRetryDelay = Math.min(5000, persistenceRetryDelay * 2);
|
|
168
|
+
throw failure;
|
|
169
|
+
}
|
|
170
|
+
persistenceRetryDelay = exports.BACKGROUND_PERSISTENCE_FLUSH_MS;
|
|
171
|
+
}
|
|
172
|
+
function closeBackgroundProcessPersistence() {
|
|
173
|
+
persistenceClosed = true;
|
|
174
|
+
try {
|
|
175
|
+
flushBackgroundProcessPersistence();
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
if (persistenceTimer) {
|
|
179
|
+
clearTimeout(persistenceTimer);
|
|
180
|
+
persistenceTimer = undefined;
|
|
181
|
+
}
|
|
182
|
+
const store = backgroundStore;
|
|
183
|
+
backgroundStore = undefined;
|
|
184
|
+
store?.close();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function purgeBackgroundProcessHistory(filter) {
|
|
188
|
+
if (!filter.chatId && !filter.projectId)
|
|
189
|
+
throw new Error('A conversation or project is required to purge background history.');
|
|
190
|
+
// Do not discard pending data or acknowledge deletion while its durable
|
|
191
|
+
// store is inaccessible. A subsequent explicit retry can reopen it.
|
|
192
|
+
ensureBackgroundProcessStore()?.purge(filter);
|
|
193
|
+
const matches = (process) => (!filter.chatId || process.chatId === filter.chatId) && (!filter.projectId || process.projectId === filter.projectId);
|
|
194
|
+
for (const record of backgroundByPid.values())
|
|
195
|
+
if (matches(record.process)) {
|
|
196
|
+
record.skipPersistence = true;
|
|
197
|
+
pendingPersistence.delete(record.process.processId);
|
|
198
|
+
}
|
|
199
|
+
for (const [id, record] of finishedBackgroundById)
|
|
200
|
+
if (matches(record.process)) {
|
|
201
|
+
clearTimeout(record.cleanupTimer);
|
|
202
|
+
finishedBackgroundById.delete(id);
|
|
203
|
+
pendingPersistence.delete(id);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
39
206
|
function treeKillSafe(pid, signal) {
|
|
40
207
|
try {
|
|
41
|
-
(0,
|
|
208
|
+
(0, win_spawn_1.treeKillSafe)(pid, signal, () => undefined);
|
|
42
209
|
}
|
|
43
|
-
catch { /*
|
|
210
|
+
catch { /* generic provider owners handle their own terminal state */ }
|
|
44
211
|
}
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
212
|
+
function isCurrent(record) { return backgroundByPid.get(record.process.pid) === record; }
|
|
213
|
+
function cloneProcess(value) { return { ...value }; }
|
|
214
|
+
function cloneLogLine(value) { return { ...value }; }
|
|
215
|
+
function safelyNotify(callback) {
|
|
216
|
+
try {
|
|
217
|
+
callback?.();
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
console.warn('[background-process] notification failed:', error instanceof Error ? error.message : String(error));
|
|
221
|
+
}
|
|
53
222
|
}
|
|
54
|
-
function
|
|
55
|
-
|
|
223
|
+
function notifyUpdated(record) {
|
|
224
|
+
queuePersistence(record);
|
|
225
|
+
try {
|
|
226
|
+
flushBackgroundProcessPersistence();
|
|
227
|
+
}
|
|
228
|
+
catch { /* process control must continue when history persistence fails */ }
|
|
229
|
+
safelyNotify(() => record.hooks.onUpdated?.(cloneProcess(record.process)));
|
|
56
230
|
}
|
|
57
|
-
function
|
|
58
|
-
|
|
231
|
+
function reportError(record, error) {
|
|
232
|
+
if (!isCurrent(record))
|
|
233
|
+
return;
|
|
234
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
235
|
+
if (record.process.error === message)
|
|
236
|
+
return;
|
|
237
|
+
record.process.error = message;
|
|
238
|
+
notifyUpdated(record);
|
|
59
239
|
}
|
|
60
|
-
/** Track a child under a projectId;
|
|
240
|
+
/** Track a provider child under a projectId; its own manager retains lifecycle ownership. */
|
|
61
241
|
function trackTransientChild(projectId, child) {
|
|
62
242
|
try {
|
|
63
243
|
(0, process_admission_1.assertProcessAdmission)(projectId);
|
|
64
244
|
}
|
|
65
|
-
catch (
|
|
66
|
-
// A continuation may have spawned immediately before observing the closed
|
|
67
|
-
// project epoch. Never leave that child unowned simply because registration
|
|
68
|
-
// lost the teardown race.
|
|
245
|
+
catch (error) {
|
|
69
246
|
if (child.pid)
|
|
70
247
|
treeKillSafe(child.pid, 'SIGTERM');
|
|
71
|
-
throw
|
|
248
|
+
throw error;
|
|
72
249
|
}
|
|
73
250
|
let set = byProject.get(projectId);
|
|
74
251
|
if (!set) {
|
|
@@ -77,247 +254,459 @@ function trackTransientChild(projectId, child) {
|
|
|
77
254
|
}
|
|
78
255
|
set.add(child);
|
|
79
256
|
const drop = () => {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
byProject.delete(projectId);
|
|
85
|
-
}
|
|
257
|
+
const current = byProject.get(projectId);
|
|
258
|
+
current?.delete(child);
|
|
259
|
+
if (current?.size === 0)
|
|
260
|
+
byProject.delete(projectId);
|
|
86
261
|
};
|
|
87
262
|
child.once('close', drop);
|
|
88
263
|
child.once('error', drop);
|
|
89
264
|
}
|
|
90
|
-
/**
|
|
265
|
+
/** Background apps retain their group owner until exit is confirmed. */
|
|
91
266
|
function killTransientChildren(projectId) {
|
|
92
|
-
const
|
|
93
|
-
if (
|
|
94
|
-
for (const child of
|
|
95
|
-
if (child.pid)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
267
|
+
const children = byProject.get(projectId);
|
|
268
|
+
if (children)
|
|
269
|
+
for (const child of children) {
|
|
270
|
+
if (!child.pid || backgroundByPid.get(child.pid)?.child === child)
|
|
271
|
+
continue;
|
|
272
|
+
let alive = true;
|
|
273
|
+
treeKillSafe(child.pid, 'SIGTERM');
|
|
274
|
+
const timer = setTimeout(() => { if (alive)
|
|
275
|
+
treeKillSafe(child.pid, 'SIGKILL'); }, BACKGROUND_KILL_GRACE_MS);
|
|
276
|
+
timer.unref?.();
|
|
277
|
+
const clear = () => { alive = false; clearTimeout(timer); };
|
|
278
|
+
child.once('close', clear);
|
|
279
|
+
child.once('error', clear);
|
|
104
280
|
}
|
|
105
281
|
byProject.delete(projectId);
|
|
106
|
-
for (const
|
|
107
|
-
if (record.process.projectId
|
|
108
|
-
|
|
109
|
-
markTerminal(record, 'killed', null, 'SIGTERM');
|
|
110
|
-
terminateBackgroundRecord(record);
|
|
282
|
+
for (const record of backgroundByPid.values()) {
|
|
283
|
+
if (record.process.projectId === projectId)
|
|
284
|
+
terminateBackgroundRecord(record);
|
|
111
285
|
}
|
|
112
286
|
}
|
|
113
|
-
function startBackgroundProcess(command, cwd, chatId, projectId, hooks) {
|
|
287
|
+
function startBackgroundProcess(command, cwd, chatId, projectId, hooks = {}, metadata = {}) {
|
|
114
288
|
(0, process_admission_1.assertProcessAdmission)(projectId);
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
289
|
+
ensureBackgroundProcessStore();
|
|
290
|
+
if (persistenceError)
|
|
291
|
+
flushBackgroundProcessPersistence();
|
|
292
|
+
const startedAt = Date.now();
|
|
293
|
+
const windowsBootstrap = process.platform === 'win32' ? (0, background_windows_bootstrap_1.spawnWindowsBackgroundBootstrap)(command, cwd) : undefined;
|
|
294
|
+
const child = windowsBootstrap?.child ?? (0, child_process_1.spawn)(command, {
|
|
295
|
+
cwd, shell: true, detached: true, stdio: ['ignore', 'pipe', 'pipe'],
|
|
296
|
+
...(process.platform === 'win32' ? { env: (0, win_spawn_2.windowsSpawnEnv)(), windowsHide: true } : {}),
|
|
120
297
|
});
|
|
121
298
|
if (!child.pid) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
command
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const staleFinished = finishedBackgroundByPid.get(child.pid);
|
|
134
|
-
if (staleFinished) {
|
|
135
|
-
clearTimeout(staleFinished.cleanupTimer);
|
|
136
|
-
finishedBackgroundByPid.delete(child.pid);
|
|
299
|
+
// spawn emits error asynchronously even when it returns no pid.
|
|
300
|
+
child.once('error', () => { });
|
|
301
|
+
child.stdout?.destroy();
|
|
302
|
+
child.stderr?.destroy();
|
|
303
|
+
throw new Error('Background process failed to start; check its command and working directory.');
|
|
304
|
+
}
|
|
305
|
+
const previous = backgroundByPid.get(child.pid);
|
|
306
|
+
if (previous) {
|
|
307
|
+
// Defensive against a reused pid in stale registry state: never let old
|
|
308
|
+
// timers, child events or UI requests operate on the replacement.
|
|
309
|
+
finishRecord(previous, 'failed', 'The operating system reused this process ID.');
|
|
137
310
|
}
|
|
138
311
|
const record = {
|
|
139
|
-
child,
|
|
140
|
-
process,
|
|
141
|
-
|
|
142
|
-
outputLines: [],
|
|
143
|
-
|
|
312
|
+
child, control: windowsBootstrap?.control ?? (0, background_process_control_1.createBackgroundProcessControl)(child, startedAt), hooks, windowsBootstrap,
|
|
313
|
+
process: { processId: (0, crypto_1.randomUUID)(), pid: child.pid, command, cwd, startedAt, status: windowsBootstrap ? 'starting' : 'running', chatId, projectId, ...metadata },
|
|
314
|
+
stoppingAttempt: 0, stopAttemptActive: false, rootClosed: false, rootExited: false, userStopRequested: false, groupGone: false, exitCode: null, exitSignal: null,
|
|
315
|
+
outputLines: [], outputBySequence: new Map(), nextSequence: 0, droppedOutputLines: 0, clippedOutput: false,
|
|
316
|
+
flushOutput: [], outputWindowStart: Date.now(), outputEventsInWindow: 0,
|
|
144
317
|
};
|
|
145
318
|
backgroundByPid.set(child.pid, record);
|
|
146
|
-
|
|
147
|
-
attachOutput(child, '
|
|
148
|
-
|
|
149
|
-
child.once('
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
319
|
+
attachOutput(child.stdout, 'stdout', record);
|
|
320
|
+
attachOutput(child.stderr, 'stderr', record);
|
|
321
|
+
child.once('error', error => { reportError(record, error); void probeRecord(record); });
|
|
322
|
+
child.once('exit', (code, signal) => {
|
|
323
|
+
record.rootExited = true;
|
|
324
|
+
record.exitCode = code;
|
|
325
|
+
record.exitSignal = signal;
|
|
326
|
+
void probeRecord(record);
|
|
153
327
|
});
|
|
154
328
|
child.once('close', (code, signal) => {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
329
|
+
record.rootExited = true;
|
|
330
|
+
record.rootClosed = true;
|
|
331
|
+
record.exitCode = code;
|
|
332
|
+
record.exitSignal = signal;
|
|
333
|
+
record.flushOutput.forEach(flush => flush());
|
|
334
|
+
void probeRecord(record);
|
|
159
335
|
});
|
|
160
|
-
record
|
|
161
|
-
|
|
336
|
+
queuePersistence(record);
|
|
337
|
+
try {
|
|
338
|
+
flushBackgroundProcessPersistence();
|
|
339
|
+
}
|
|
340
|
+
catch (error) {
|
|
341
|
+
terminateBackgroundRecord(record);
|
|
342
|
+
throw error;
|
|
343
|
+
}
|
|
344
|
+
safelyNotify(() => hooks.onStarted?.(cloneProcess(record.process)));
|
|
345
|
+
if (windowsBootstrap) {
|
|
346
|
+
void record.control.ready?.then(() => {
|
|
347
|
+
if (!isCurrent(record) || record.process.status !== 'starting' || record.rootExited) {
|
|
348
|
+
windowsBootstrap.cancel();
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
windowsBootstrap.start();
|
|
352
|
+
record.process.status = 'running';
|
|
353
|
+
queuePersistence(record);
|
|
354
|
+
safelyNotify(() => hooks.onUpdated?.(cloneProcess(record.process)));
|
|
355
|
+
}).catch(error => {
|
|
356
|
+
if (!isCurrent(record))
|
|
357
|
+
return;
|
|
358
|
+
reportError(record, error);
|
|
359
|
+
if (windowsBootstrap.hasLaunched())
|
|
360
|
+
terminateBackgroundRecord(record, true);
|
|
361
|
+
else
|
|
362
|
+
windowsBootstrap.cancel();
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
scheduleProbe(record);
|
|
366
|
+
return cloneProcess(record.process);
|
|
367
|
+
}
|
|
368
|
+
function scheduleProbe(record) {
|
|
369
|
+
if (!isCurrent(record) || record.probeTimer)
|
|
370
|
+
return;
|
|
371
|
+
const delay = record.process.status === 'stopping' && process.platform !== 'win32' ? 100 : 1000;
|
|
372
|
+
record.probeTimer = setTimeout(() => { record.probeTimer = undefined; void probeRecord(record); }, delay);
|
|
373
|
+
record.probeTimer.unref?.();
|
|
374
|
+
}
|
|
375
|
+
function probeRecord(record) {
|
|
376
|
+
if (!isCurrent(record))
|
|
377
|
+
return Promise.resolve();
|
|
378
|
+
if (record.probing)
|
|
379
|
+
return record.probing;
|
|
380
|
+
const pending = Promise.resolve().then(async () => {
|
|
381
|
+
try {
|
|
382
|
+
// Before admission the bootstrap has no descendants. Its own child handle
|
|
383
|
+
// can confirm exit even if Windows identity discovery failed or was cancelled.
|
|
384
|
+
if (record.windowsBootstrap && !record.windowsBootstrap.hasLaunched() && record.rootClosed)
|
|
385
|
+
record.groupGone = true;
|
|
386
|
+
if (!record.groupGone)
|
|
387
|
+
record.groupGone = !(await record.control.isAlive());
|
|
388
|
+
if (!isCurrent(record))
|
|
389
|
+
return;
|
|
390
|
+
if (record.groupGone && record.rootClosed) {
|
|
391
|
+
const containmentFailure = record.control.terminalFailure?.();
|
|
392
|
+
finishRecord(record, containmentFailure ? 'failed' : record.userStopRequested ? 'killed' : record.exitCode === 0 ? 'exited' : 'failed', containmentFailure);
|
|
393
|
+
}
|
|
394
|
+
else if (record.rootExited && !record.groupGone && record.process.status !== 'stopping') {
|
|
395
|
+
// A wrapper that exits must not leave an unowned app behind. Clean up
|
|
396
|
+
// its group, retaining the wrapper's original success/failure outcome.
|
|
397
|
+
terminateBackgroundRecord(record, true);
|
|
398
|
+
}
|
|
399
|
+
else if (record.forceKillAt !== undefined && Date.now() - record.forceKillAt >= BACKGROUND_STOP_CONFIRM_MS && !record.groupGone) {
|
|
400
|
+
record.stopAttemptActive = false;
|
|
401
|
+
reportError(record, new Error('The process group has not stopped after the force-stop request. Retry stopping it.'));
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
catch (error) {
|
|
405
|
+
record.stopAttemptActive = false;
|
|
406
|
+
reportError(record, error);
|
|
407
|
+
}
|
|
408
|
+
}).finally(() => { if (record.probing === pending)
|
|
409
|
+
record.probing = undefined; scheduleProbe(record); });
|
|
410
|
+
record.probing = pending;
|
|
411
|
+
return pending;
|
|
412
|
+
}
|
|
413
|
+
/** Stop is idempotent while an attempt is progressing. Failed attempts remain
|
|
414
|
+
* visible and may be retried; terminal state requires both OS and child close. */
|
|
415
|
+
function terminateBackgroundRecord(record, automatic = false) {
|
|
416
|
+
record.windowsBootstrap?.cancel();
|
|
417
|
+
if (!automatic)
|
|
418
|
+
record.userStopRequested = true;
|
|
419
|
+
if (!isCurrent(record) || record.stopAttemptActive)
|
|
420
|
+
return;
|
|
421
|
+
record.stopAttemptActive = true;
|
|
422
|
+
const attempt = ++record.stoppingAttempt;
|
|
423
|
+
record.process.status = 'stopping';
|
|
424
|
+
record.process.stopRequestedAt ??= Date.now();
|
|
425
|
+
delete record.process.error;
|
|
426
|
+
record.forceKillAt = undefined;
|
|
427
|
+
if (record.killTimer)
|
|
428
|
+
clearTimeout(record.killTimer);
|
|
429
|
+
notifyUpdated(record);
|
|
430
|
+
const signal = async (value) => {
|
|
431
|
+
if (!isCurrent(record) || record.stoppingAttempt !== attempt || record.groupGone)
|
|
432
|
+
return;
|
|
433
|
+
try {
|
|
434
|
+
await record.control.terminate(value);
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
if (record.stoppingAttempt === attempt) {
|
|
438
|
+
record.stopAttemptActive = false;
|
|
439
|
+
reportError(record, error);
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
await probeRecord(record);
|
|
443
|
+
};
|
|
444
|
+
void signal('SIGTERM');
|
|
445
|
+
record.killTimer = setTimeout(() => {
|
|
446
|
+
record.killTimer = undefined;
|
|
447
|
+
if (!isCurrent(record) || record.stoppingAttempt !== attempt || record.groupGone)
|
|
448
|
+
return;
|
|
449
|
+
record.forceKillAt = Date.now();
|
|
450
|
+
void signal('SIGKILL');
|
|
451
|
+
}, BACKGROUND_KILL_GRACE_MS);
|
|
452
|
+
// Keep the escalation armed even after the shell's close event. Shutdown
|
|
453
|
+
// additionally awaits the drain below before exiting the server process.
|
|
454
|
+
record.killTimer.unref?.();
|
|
162
455
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
456
|
+
/** Incremental bounded terminal-text capture. StringDecoder keeps split UTF-8
|
|
457
|
+
* intact; escape state is finite even for unterminated OSC/CSI sequences. */
|
|
458
|
+
function attachOutput(stream, source, record) {
|
|
459
|
+
if (!stream)
|
|
460
|
+
return;
|
|
461
|
+
const decoder = new string_decoder_1.StringDecoder('utf8');
|
|
462
|
+
let text = '', clipped = false, pendingCR = false, ended = false;
|
|
463
|
+
let escape = 'plain';
|
|
464
|
+
let row;
|
|
465
|
+
const publish = (partial) => {
|
|
466
|
+
if (!text && !row && partial)
|
|
172
467
|
return;
|
|
468
|
+
if (!row || !record.outputBySequence.has(row.sequence)) {
|
|
469
|
+
row = { sequence: ++record.nextSequence, at: Date.now(), source, line: '', partial };
|
|
470
|
+
record.outputLines.push(row);
|
|
471
|
+
record.outputBySequence.set(row.sequence, row);
|
|
472
|
+
if (record.outputLines.length > BACKGROUND_LOG_MAX_LINES) {
|
|
473
|
+
const removed = record.outputLines.shift();
|
|
474
|
+
record.outputBySequence.delete(removed.sequence);
|
|
475
|
+
record.droppedOutputLines += 1;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
row.line = clipped ? `${text.slice(0, BACKGROUND_OUTPUT_MAX_LINE_CHARS - 1).replace(/[\uD800-\uDBFF]$/, '')}…` : text;
|
|
479
|
+
row.partial = partial;
|
|
480
|
+
if (clipped)
|
|
481
|
+
record.clippedOutput = true;
|
|
482
|
+
queuePersistence(record, row);
|
|
173
483
|
const now = Date.now();
|
|
174
|
-
if (now -
|
|
175
|
-
|
|
176
|
-
|
|
484
|
+
if (now - record.outputWindowStart >= 1000) {
|
|
485
|
+
record.outputWindowStart = now;
|
|
486
|
+
record.outputEventsInWindow = 0;
|
|
487
|
+
}
|
|
488
|
+
// Live notifications are hints; sequence gaps are recoverable from the
|
|
489
|
+
// bounded authoritative snapshot. Never drop lines from that snapshot.
|
|
490
|
+
if (record.outputEventsInWindow < BACKGROUND_OUTPUT_MAX_LINES_PER_SECOND) {
|
|
491
|
+
record.outputEventsInWindow += 1;
|
|
492
|
+
safelyNotify(() => record.hooks.onOutput?.({ ...cloneLogLine(row), processId: record.process.processId, pid: record.process.pid, chatId: record.process.chatId, projectId: record.process.projectId }));
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
const consume = (chunk) => {
|
|
496
|
+
for (const character of chunk) {
|
|
497
|
+
if (escape === 'escape') {
|
|
498
|
+
escape = character === '[' ? 'csi' : [']', 'P', '^', '_'].includes(character) ? 'osc' : 'plain';
|
|
499
|
+
continue;
|
|
500
|
+
}
|
|
501
|
+
if (escape === 'csi') {
|
|
502
|
+
if (character >= '@' && character <= '~')
|
|
503
|
+
escape = 'plain';
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
if (escape === 'osc') {
|
|
507
|
+
if (character === '\u0007')
|
|
508
|
+
escape = 'plain';
|
|
509
|
+
else if (character === '\u001b')
|
|
510
|
+
escape = 'oscEscape';
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
if (escape === 'oscEscape') {
|
|
514
|
+
escape = character === '\\' ? 'plain' : 'osc';
|
|
515
|
+
continue;
|
|
516
|
+
}
|
|
517
|
+
if (character === '\u001b') {
|
|
518
|
+
escape = 'escape';
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
if (character === '\u009b') {
|
|
522
|
+
escape = 'csi';
|
|
523
|
+
continue;
|
|
524
|
+
}
|
|
525
|
+
if (pendingCR) {
|
|
526
|
+
pendingCR = false;
|
|
527
|
+
if (character !== '\n') {
|
|
528
|
+
text = '';
|
|
529
|
+
clipped = false;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
if (character === '\r') {
|
|
533
|
+
pendingCR = true;
|
|
534
|
+
continue;
|
|
535
|
+
}
|
|
536
|
+
if (character === '\n') {
|
|
537
|
+
publish(false);
|
|
538
|
+
text = '';
|
|
539
|
+
clipped = false;
|
|
540
|
+
row = undefined;
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
if (character === '\b') {
|
|
544
|
+
text = Array.from(text).slice(0, -1).join('');
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
if ((character < ' ' && character !== '\t') || character === '\u007f' || (character >= '\u0080' && character <= '\u009f'))
|
|
548
|
+
continue;
|
|
549
|
+
if (text.length + character.length <= BACKGROUND_OUTPUT_MAX_LINE_CHARS)
|
|
550
|
+
text += character;
|
|
551
|
+
else
|
|
552
|
+
clipped = true;
|
|
177
553
|
}
|
|
178
|
-
|
|
554
|
+
publish(true);
|
|
555
|
+
};
|
|
556
|
+
stream.on('data', (chunk) => { if (!ended)
|
|
557
|
+
consume(typeof chunk === 'string' ? chunk : decoder.write(chunk)); });
|
|
558
|
+
const finish = () => {
|
|
559
|
+
if (ended)
|
|
179
560
|
return;
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
561
|
+
consume(decoder.end());
|
|
562
|
+
ended = true;
|
|
563
|
+
if (row || text)
|
|
564
|
+
publish(false);
|
|
565
|
+
};
|
|
566
|
+
stream.once('end', finish);
|
|
567
|
+
stream.once('close', finish);
|
|
568
|
+
stream.once('error', error => { reportError(record, error); finish(); });
|
|
569
|
+
record.flushOutput.push(finish);
|
|
189
570
|
}
|
|
190
|
-
function
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
record.
|
|
195
|
-
|
|
571
|
+
function finishRecord(record, status, error) {
|
|
572
|
+
if (!isCurrent(record))
|
|
573
|
+
return;
|
|
574
|
+
if (record.killTimer)
|
|
575
|
+
clearTimeout(record.killTimer);
|
|
576
|
+
if (record.probeTimer)
|
|
577
|
+
clearTimeout(record.probeTimer);
|
|
578
|
+
record.process.status = status;
|
|
579
|
+
record.process.endedAt = Date.now();
|
|
580
|
+
record.process.exitCode = record.exitCode;
|
|
581
|
+
record.process.signal = record.exitSignal;
|
|
582
|
+
if (error)
|
|
583
|
+
record.process.error = error;
|
|
584
|
+
record.flushOutput.forEach(flush => flush());
|
|
585
|
+
queuePersistence(record);
|
|
586
|
+
try {
|
|
587
|
+
flushBackgroundProcessPersistence();
|
|
196
588
|
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (record.
|
|
200
|
-
|
|
201
|
-
if (existing) {
|
|
202
|
-
existing.process = cloneProcess(record.process);
|
|
203
|
-
existing.outputLines = record.outputLines.map(cloneLogLine);
|
|
204
|
-
existing.droppedOutputLines = record.droppedOutputLines;
|
|
205
|
-
}
|
|
589
|
+
catch { /* retain the pending batch and visible persistenceError for a later retry */ }
|
|
590
|
+
backgroundByPid.delete(record.process.pid);
|
|
591
|
+
if (record.skipPersistence) {
|
|
592
|
+
safelyNotify(() => record.hooks.onExited?.(cloneProcess(record.process)));
|
|
206
593
|
return;
|
|
207
594
|
}
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
if (previous)
|
|
211
|
-
clearTimeout(previous.cleanupTimer);
|
|
212
|
-
const cleanupTimer = setTimeout(() => {
|
|
213
|
-
finishedBackgroundByPid.delete(record.process.pid);
|
|
214
|
-
}, exports.BACKGROUND_LOG_RETENTION_MS);
|
|
595
|
+
const processId = record.process.processId;
|
|
596
|
+
const cleanupTimer = setTimeout(() => { finishedBackgroundById.delete(processId); }, exports.BACKGROUND_LOG_RETENTION_MS);
|
|
215
597
|
cleanupTimer.unref?.();
|
|
216
|
-
|
|
217
|
-
process: cloneProcess(record.process),
|
|
218
|
-
|
|
219
|
-
droppedOutputLines: record.droppedOutputLines,
|
|
220
|
-
cleanupTimer,
|
|
598
|
+
finishedBackgroundById.set(processId, {
|
|
599
|
+
process: cloneProcess(record.process), outputLines: record.outputLines.map(cloneLogLine),
|
|
600
|
+
nextSequence: record.nextSequence, droppedOutputLines: record.droppedOutputLines, clippedOutput: record.clippedOutput, cleanupTimer,
|
|
221
601
|
});
|
|
602
|
+
while (finishedBackgroundById.size > BACKGROUND_FINISHED_MAX) {
|
|
603
|
+
const oldest = finishedBackgroundById.entries().next().value;
|
|
604
|
+
clearTimeout(oldest[1].cleanupTimer);
|
|
605
|
+
finishedBackgroundById.delete(oldest[0]);
|
|
606
|
+
}
|
|
607
|
+
safelyNotify(() => record.hooks.onExited?.(cloneProcess(record.process)));
|
|
222
608
|
}
|
|
223
|
-
function
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
record.
|
|
232
|
-
record.hooks.onExited?.(cloneProcess(record.process));
|
|
609
|
+
function findRecord(pid, processId) {
|
|
610
|
+
const live = backgroundByPid.get(pid);
|
|
611
|
+
if (live && (!processId || live.process.processId === processId))
|
|
612
|
+
return live;
|
|
613
|
+
if (processId) {
|
|
614
|
+
const finished = finishedBackgroundById.get(processId);
|
|
615
|
+
return finished?.process.pid === pid ? finished : undefined;
|
|
616
|
+
}
|
|
617
|
+
return [...finishedBackgroundById.values()].reverse().find(record => record.process.pid === pid);
|
|
233
618
|
}
|
|
234
|
-
function getBackgroundProcess(pid) {
|
|
235
|
-
const record =
|
|
236
|
-
|
|
237
|
-
return cloneProcess(record.process);
|
|
238
|
-
const finished = finishedBackgroundByPid.get(pid);
|
|
239
|
-
return finished ? cloneProcess(finished.process) : null;
|
|
619
|
+
function getBackgroundProcess(pid, processId) {
|
|
620
|
+
const record = findRecord(pid, processId);
|
|
621
|
+
return record ? cloneProcess(record.process) : ensureBackgroundProcessStore()?.get(pid, processId) ?? null;
|
|
240
622
|
}
|
|
241
623
|
function getBackgroundProcessLogs(pid, filter = {}) {
|
|
242
|
-
const
|
|
243
|
-
const record = live
|
|
244
|
-
? {
|
|
245
|
-
process: live.process,
|
|
246
|
-
outputLines: live.outputLines,
|
|
247
|
-
droppedOutputLines: live.droppedOutputLines,
|
|
248
|
-
}
|
|
249
|
-
: finishedBackgroundByPid.get(pid);
|
|
624
|
+
const record = findRecord(pid, filter.processId);
|
|
250
625
|
if (!record)
|
|
626
|
+
return ensureBackgroundProcessStore()?.logs(pid, filter) ?? null;
|
|
627
|
+
if ((filter.projectId && record.process.projectId !== filter.projectId) || (filter.chatId && record.process.chatId !== filter.chatId))
|
|
251
628
|
return null;
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
const
|
|
262
|
-
const
|
|
629
|
+
const maxLines = backgroundStore ? background_process_store_1.BACKGROUND_HISTORY_MAX_LINES : BACKGROUND_LOG_MAX_LINES;
|
|
630
|
+
const limit = typeof filter.limit === 'number' && Number.isFinite(filter.limit) ? Math.max(1, Math.min(maxLines, Math.floor(filter.limit))) : BACKGROUND_LOG_MAX_LINES;
|
|
631
|
+
let durable = null;
|
|
632
|
+
try {
|
|
633
|
+
durable = ensureBackgroundProcessStore()?.logs(pid, { ...filter, processId: record.process.processId, limit }) ?? null;
|
|
634
|
+
}
|
|
635
|
+
catch (error) {
|
|
636
|
+
record.process.persistenceError = `Background history could not be read: ${error instanceof Error ? error.message : String(error)}`;
|
|
637
|
+
}
|
|
638
|
+
const combined = new Map((durable?.lines ?? []).map(line => [line.sequence, line]));
|
|
639
|
+
for (const line of record.outputLines)
|
|
640
|
+
combined.set(line.sequence, line);
|
|
641
|
+
for (const line of pendingPersistence.get(record.process.processId)?.lines.values() ?? [])
|
|
642
|
+
combined.set(line.sequence, line);
|
|
643
|
+
const lines = [...combined.values()].sort((a, b) => a.sequence - b.sequence).slice(-limit).map(cloneLogLine);
|
|
644
|
+
const nextSequence = Math.max(record.nextSequence, durable?.nextSequence ?? 0);
|
|
645
|
+
const droppedLines = Math.max(0, nextSequence - lines.length);
|
|
263
646
|
return {
|
|
264
|
-
process: cloneProcess(record.process),
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
droppedLines,
|
|
268
|
-
maxLines: BACKGROUND_LOG_MAX_LINES,
|
|
269
|
-
maxLineChars: BACKGROUND_OUTPUT_MAX_LINE_CHARS,
|
|
270
|
-
retentionMs: exports.BACKGROUND_LOG_RETENTION_MS,
|
|
647
|
+
process: cloneProcess(record.process), lines, nextSequence,
|
|
648
|
+
truncated: droppedLines > 0 || record.clippedOutput || (durable?.truncated ?? false), droppedLines,
|
|
649
|
+
maxLines, maxLineChars: BACKGROUND_OUTPUT_MAX_LINE_CHARS, retentionMs: backgroundStore ? background_process_store_1.BACKGROUND_HISTORY_RETENTION_MS : exports.BACKGROUND_LOG_RETENTION_MS,
|
|
271
650
|
};
|
|
272
651
|
}
|
|
273
652
|
function listBackgroundProcesses(filter = {}) {
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
if (filter.projectId
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
continue;
|
|
281
|
-
if (backgroundTerminal.has(process.status))
|
|
282
|
-
continue;
|
|
283
|
-
processes.push(cloneProcess(process));
|
|
284
|
-
}
|
|
285
|
-
return processes;
|
|
653
|
+
const records = [...backgroundByPid.values(), ...(filter.includeFinished ? finishedBackgroundById.values() : [])];
|
|
654
|
+
const combined = new Map((ensureBackgroundProcessStore()?.list(filter) ?? []).map(process => [process.processId, process]));
|
|
655
|
+
for (const record of records)
|
|
656
|
+
if ((!filter.projectId || record.process.projectId === filter.projectId) && (!filter.chatId || record.process.chatId === filter.chatId))
|
|
657
|
+
combined.set(record.process.processId, cloneProcess(record.process));
|
|
658
|
+
return [...combined.values()].sort((a, b) => a.startedAt - b.startedAt);
|
|
286
659
|
}
|
|
287
|
-
function killBackgroundProcess(pid) {
|
|
660
|
+
function killBackgroundProcess(pid, processId) {
|
|
288
661
|
const record = backgroundByPid.get(pid);
|
|
289
|
-
if (!record)
|
|
290
|
-
|
|
291
|
-
record.process.status = 'killed';
|
|
292
|
-
terminateBackgroundRecord(record);
|
|
662
|
+
if (record && (!processId || processId === record.process.processId))
|
|
663
|
+
terminateBackgroundRecord(record);
|
|
293
664
|
}
|
|
294
665
|
function killOwnedBackgroundProcess(pid, owner) {
|
|
295
|
-
const record =
|
|
666
|
+
const record = findRecord(pid, owner.processId);
|
|
296
667
|
if (!record)
|
|
668
|
+
return false; // historical rows never regain operating-system control
|
|
669
|
+
if (!record || record.process.projectId !== owner.projectId || record.process.chatId !== owner.chatId)
|
|
297
670
|
return false;
|
|
298
|
-
if (record.process.
|
|
299
|
-
|
|
300
|
-
if (record.process.chatId !== owner.chatId)
|
|
301
|
-
return false;
|
|
302
|
-
killBackgroundProcess(pid);
|
|
671
|
+
if (!backgroundTerminal.has(record.process.status))
|
|
672
|
+
killBackgroundProcess(pid, record.process.processId);
|
|
303
673
|
return true;
|
|
304
674
|
}
|
|
305
|
-
function terminateBackgroundRecord(record) {
|
|
306
|
-
if (record.killTimer)
|
|
307
|
-
clearTimeout(record.killTimer);
|
|
308
|
-
record.killTimer = treeKillWithEscalation(record.process.pid);
|
|
309
|
-
}
|
|
310
675
|
function killBackgroundProcessesForChat(chatId, projectId) {
|
|
311
|
-
let
|
|
676
|
+
let count = 0;
|
|
312
677
|
for (const record of backgroundByPid.values()) {
|
|
313
|
-
if (record.process.chatId !== chatId)
|
|
314
|
-
continue;
|
|
315
|
-
if (projectId && record.process.projectId !== projectId)
|
|
678
|
+
if (record.process.chatId !== chatId || (projectId && record.process.projectId !== projectId))
|
|
316
679
|
continue;
|
|
317
|
-
record.process.status = 'killed';
|
|
318
|
-
markTerminal(record, 'killed', null, 'SIGTERM');
|
|
319
680
|
terminateBackgroundRecord(record);
|
|
320
|
-
|
|
681
|
+
count += 1;
|
|
682
|
+
}
|
|
683
|
+
return count;
|
|
684
|
+
}
|
|
685
|
+
/** Shutdown requests stop for all owned apps and keeps the event loop alive
|
|
686
|
+
* through escalation. Returns unresolved processes rather than claiming exit. */
|
|
687
|
+
async function awaitBackgroundProcessesStopped(timeoutMs = 6000) {
|
|
688
|
+
for (const record of backgroundByPid.values())
|
|
689
|
+
terminateBackgroundRecord(record);
|
|
690
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
691
|
+
while (backgroundByPid.size > 0 && Date.now() < deadline) {
|
|
692
|
+
let deadlineTimer;
|
|
693
|
+
try {
|
|
694
|
+
await Promise.race([
|
|
695
|
+
Promise.all([...backgroundByPid.values()].map(probeRecord)),
|
|
696
|
+
new Promise(resolve => { deadlineTimer = setTimeout(resolve, Math.max(0, deadline - Date.now())); }),
|
|
697
|
+
]);
|
|
698
|
+
}
|
|
699
|
+
finally {
|
|
700
|
+
if (deadlineTimer)
|
|
701
|
+
clearTimeout(deadlineTimer);
|
|
702
|
+
}
|
|
703
|
+
if (backgroundByPid.size === 0 || Date.now() >= deadline)
|
|
704
|
+
break;
|
|
705
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(50, Math.max(0, deadline - Date.now()))));
|
|
706
|
+
}
|
|
707
|
+
try {
|
|
708
|
+
flushBackgroundProcessPersistence();
|
|
321
709
|
}
|
|
322
|
-
|
|
710
|
+
catch { /* storage failure must not prevent control/shutdown; close reports it to the app */ }
|
|
711
|
+
return [...backgroundByPid.values()].map(record => cloneProcess(record.process));
|
|
323
712
|
}
|