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
|
@@ -36,9 +36,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.FileSummaryManager = exports.SUMMARY_MAX_LENGTH = void 0;
|
|
39
|
+
exports.FileSummaryManager = exports.WATCH_DEBOUNCE_MS = exports.SUMMARY_MAX_LENGTH = exports.CURRENT_PROMPT_VERSION = void 0;
|
|
40
40
|
exports.isValidSummaryPayload = isValidSummaryPayload;
|
|
41
|
+
exports.isSummaryMetadataStale = isSummaryMetadataStale;
|
|
41
42
|
exports.summariesDir = summariesDir;
|
|
43
|
+
exports.repositorySummaryRoot = repositorySummaryRoot;
|
|
42
44
|
exports.pathHash = pathHash;
|
|
43
45
|
exports.summaryFilePath = summaryFilePath;
|
|
44
46
|
exports.computeFileHash = computeFileHash;
|
|
@@ -46,6 +48,7 @@ exports.readSummary = readSummary;
|
|
|
46
48
|
exports.writeSummary = writeSummary;
|
|
47
49
|
exports.ensureGitignoreLine = ensureGitignoreLine;
|
|
48
50
|
exports.sweepOrphans = sweepOrphans;
|
|
51
|
+
exports.resolveWatchEngine = resolveWatchEngine;
|
|
49
52
|
exports.__resetDesktopSummaryStateForTests = __resetDesktopSummaryStateForTests;
|
|
50
53
|
const crypto_1 = require("crypto");
|
|
51
54
|
const fs = __importStar(require("fs"));
|
|
@@ -58,7 +61,7 @@ const file_summary_v1_json_1 = __importDefault(require("./schemas/file-summary.v
|
|
|
58
61
|
const SUMMARIES_REL = path.join('.specrails', 'file-summaries');
|
|
59
62
|
// Current summary prompt version. Bump when buildSystemPrompt changes materially
|
|
60
63
|
// so existing summaries are treated as stale and regenerated on next request.
|
|
61
|
-
|
|
64
|
+
exports.CURRENT_PROMPT_VERSION = 2;
|
|
62
65
|
// Defensive bound on the per-job counter map so it cannot grow without limit
|
|
63
66
|
// across a long-lived app session (the per-job cap is best-effort).
|
|
64
67
|
const MAX_JOB_COUNTERS = 2000;
|
|
@@ -97,17 +100,61 @@ function getSummaryValidator() {
|
|
|
97
100
|
/** True when `payload` conforms to file-summary.v1.json (with the maxLength
|
|
98
101
|
* bound on `summary`). Exported so callers/tests can pre-validate. */
|
|
99
102
|
function isValidSummaryPayload(payload) {
|
|
100
|
-
|
|
103
|
+
if (getSummaryValidator()(payload) !== true)
|
|
104
|
+
return false;
|
|
105
|
+
const summary = payload;
|
|
106
|
+
return summary.summary.trim().length > 0 && Number.isFinite(Date.parse(summary.generatedAt));
|
|
107
|
+
}
|
|
108
|
+
/** Legacy caches remain readable, but new prompts/language require regeneration. */
|
|
109
|
+
function isSummaryMetadataStale(summary, language) {
|
|
110
|
+
return summary.language !== language || summary.generatedBy.promptVersion !== exports.CURRENT_PROMPT_VERSION;
|
|
101
111
|
}
|
|
102
112
|
function summariesDir(projectPath) {
|
|
103
113
|
return path.join(projectPath, SUMMARIES_REL);
|
|
104
114
|
}
|
|
115
|
+
/** Secondary artifacts belong to the logical project's workspace, never to a
|
|
116
|
+
* member's own Core registry/backlog. The primary keeps its historical layout. */
|
|
117
|
+
function repositorySummaryRoot(artifactRoot, repository) {
|
|
118
|
+
return repository.isPrimary ? artifactRoot : path.join(artifactRoot, '.specrails', 'repository-context', pathHash(repository.id));
|
|
119
|
+
}
|
|
120
|
+
function repositoryKey(projectId, repositoryId) {
|
|
121
|
+
return JSON.stringify([projectId, repositoryId ?? null]);
|
|
122
|
+
}
|
|
105
123
|
function pathHash(relPath) {
|
|
106
124
|
return (0, crypto_1.createHash)('sha256').update(Buffer.from(relPath, 'utf8')).digest('hex');
|
|
107
125
|
}
|
|
108
126
|
function summaryFilePath(projectPath, relPath) {
|
|
109
127
|
return path.join(summariesDir(projectPath), `${pathHash(relPath)}.json`);
|
|
110
128
|
}
|
|
129
|
+
/** Revalidate the queued target and bound reads before handing any bytes to AI. */
|
|
130
|
+
function readSourceSnapshot(projectPath, relPath) {
|
|
131
|
+
const root = fs.realpathSync(projectPath);
|
|
132
|
+
const absolute = fs.realpathSync(path.resolve(root, relPath));
|
|
133
|
+
const relative = path.relative(root, absolute);
|
|
134
|
+
if (!relative || relative === '..' || relative.startsWith('..' + path.sep) || path.isAbsolute(relative))
|
|
135
|
+
throw new Error('Source escapes repository');
|
|
136
|
+
const fd = fs.openSync(absolute, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0));
|
|
137
|
+
try {
|
|
138
|
+
const maximum = 2 * 1024 * 1024;
|
|
139
|
+
const metadata = fs.fstatSync(fd);
|
|
140
|
+
if (!metadata.isFile() || metadata.size > maximum)
|
|
141
|
+
throw new Error('Source is not a supported file');
|
|
142
|
+
const bytes = Buffer.alloc(Math.min(maximum + 1, metadata.size + 1));
|
|
143
|
+
let length = 0;
|
|
144
|
+
while (length < bytes.length) {
|
|
145
|
+
const read = fs.readSync(fd, bytes, length, bytes.length - length, null);
|
|
146
|
+
if (!read)
|
|
147
|
+
break;
|
|
148
|
+
length += read;
|
|
149
|
+
}
|
|
150
|
+
if (length > metadata.size || length > maximum || bytes.subarray(0, Math.min(length, 8192)).includes(0))
|
|
151
|
+
throw new Error('Source changed size or is binary');
|
|
152
|
+
return bytes.subarray(0, length);
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
fs.closeSync(fd);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
111
158
|
async function computeFileHash(absolutePath) {
|
|
112
159
|
// Use streaming hash so very large files do not balloon memory.
|
|
113
160
|
return await new Promise((resolve, reject) => {
|
|
@@ -121,12 +168,15 @@ async function computeFileHash(absolutePath) {
|
|
|
121
168
|
function readSummary(projectPath, relPath) {
|
|
122
169
|
const file = summaryFilePath(projectPath, relPath);
|
|
123
170
|
try {
|
|
171
|
+
const metadata = fs.lstatSync(file);
|
|
172
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.size > 128 * 1024)
|
|
173
|
+
return null;
|
|
124
174
|
const raw = fs.readFileSync(file, 'utf8');
|
|
125
175
|
const parsed = JSON.parse(raw);
|
|
126
176
|
// Validate against file-summary.v1.json. A corrupt / hand-edited /
|
|
127
177
|
// cross-version / oversized summary is treated as ABSENT (null) instead of
|
|
128
178
|
// being trusted and surfaced verbatim — so the next request regenerates it.
|
|
129
|
-
if (!isValidSummaryPayload(parsed))
|
|
179
|
+
if (!isValidSummaryPayload(parsed) || parsed.path !== relPath)
|
|
130
180
|
return null;
|
|
131
181
|
return parsed;
|
|
132
182
|
}
|
|
@@ -142,7 +192,7 @@ function writeSummary(summaryRoot, relPath, payload,
|
|
|
142
192
|
appendGitignore = true) {
|
|
143
193
|
// Reject a non-conformant payload before it ever lands on disk so the
|
|
144
194
|
// documented "schema validated" invariant is real, not aspirational.
|
|
145
|
-
if (!isValidSummaryPayload(payload)) {
|
|
195
|
+
if (!isValidSummaryPayload(payload) || payload.path !== relPath) {
|
|
146
196
|
throw new Error('writeSummary: payload failed file-summary.v1 schema validation');
|
|
147
197
|
}
|
|
148
198
|
const dir = summariesDir(summaryRoot);
|
|
@@ -225,6 +275,36 @@ sourceRoot) {
|
|
|
225
275
|
}
|
|
226
276
|
return { deleted, remaining };
|
|
227
277
|
}
|
|
278
|
+
/** Trailing debounce applied to source-change events before the stale check. */
|
|
279
|
+
exports.WATCH_DEBOUNCE_MS = 200;
|
|
280
|
+
/** Errno codes meaning the watcher is consuming a process-wide resource it can
|
|
281
|
+
* no longer get (kqueue file descriptors on macOS, inotify watches on Linux).
|
|
282
|
+
* Keeping such a watcher alive keeps the leak — and, before this guard, an
|
|
283
|
+
* unhandled `error` event on the recursive watcher took the whole server down
|
|
284
|
+
* (`Error: EMFILE: too many open files, watch`). Close + degrade instead. */
|
|
285
|
+
const WATCHER_EXHAUSTION_CODES = new Set(['EMFILE', 'ENFILE', 'ENOSPC']);
|
|
286
|
+
/**
|
|
287
|
+
* Pick the source-watch engine for a platform.
|
|
288
|
+
*
|
|
289
|
+
* macOS and Windows get the kernel's own recursive watch —
|
|
290
|
+
* `fs.watch(dir, { recursive: true })` = FSEvents / ReadDirectoryChangesW —
|
|
291
|
+
* which costs ONE handle for the whole tree. chokidar ≥ 4 dropped fsevents and
|
|
292
|
+
* watches every file with its own `fs.watch`; on macOS that is one kqueue fd
|
|
293
|
+
* PER FILE, so any repo past the ~10k-fd limit blew the process up with
|
|
294
|
+
* `EMFILE: too many open files, watch`. Linux keeps chokidar: inotify watches
|
|
295
|
+
* are not fds, and chokidar's `ignored` pruning keeps `node_modules` / build
|
|
296
|
+
* trees out of the watch set (Node's own recursive implementation on Linux
|
|
297
|
+
* walks and watches everything, `node_modules` included).
|
|
298
|
+
*/
|
|
299
|
+
function resolveWatchEngine(platform = process.platform) {
|
|
300
|
+
return platform === 'darwin' || platform === 'win32' ? 'native' : 'chokidar';
|
|
301
|
+
}
|
|
302
|
+
function errnoCode(err) {
|
|
303
|
+
return err?.code;
|
|
304
|
+
}
|
|
305
|
+
function errMessage(err) {
|
|
306
|
+
return err instanceof Error ? err.message : String(err);
|
|
307
|
+
}
|
|
228
308
|
// App-wide concurrency is shared across EVERY FileSummaryManager instance.
|
|
229
309
|
// Production constructs one manager per project (each with its own db/broadcast/
|
|
230
310
|
// generate), so a per-instance counter would only ever cap per-project. This
|
|
@@ -271,9 +351,9 @@ class FileSummaryManager {
|
|
|
271
351
|
_disposed = false;
|
|
272
352
|
// Per-project SUPERSET of relPaths that have a summary on disk. Seeded from
|
|
273
353
|
// the summaries dir at attachWatcher and only ever added to (on write), so a
|
|
274
|
-
// path absent from the set provably has no summary — letting the
|
|
275
|
-
//
|
|
276
|
-
//
|
|
354
|
+
// path absent from the set provably has no summary — letting the watcher's
|
|
355
|
+
// change funnel skip the disk hits for the common no-summary file. A stale
|
|
356
|
+
// entry (after sweep) just costs one harmless failed read.
|
|
277
357
|
knownSummaries = new Map();
|
|
278
358
|
// Tracks pending generation promises so flush() can await them in tests.
|
|
279
359
|
pending = new Set();
|
|
@@ -289,23 +369,26 @@ class FileSummaryManager {
|
|
|
289
369
|
// NOT async: returning the in-flight promise verbatim is what makes the dedupe
|
|
290
370
|
// a true coalesce (the second caller gets the SAME promise, not a wrapper).
|
|
291
371
|
enqueue(req) {
|
|
372
|
+
if (this._disposed)
|
|
373
|
+
return Promise.resolve('skipped:not-found');
|
|
292
374
|
// Per-(project,relPath) in-flight dedupe: a second enqueue for the same file
|
|
293
375
|
// while one is still running coalesces onto the first promise, so the
|
|
294
376
|
// provider is spawned once and ai_invocations is billed once (fixes
|
|
295
377
|
// concurrent-regenerate double-billing across tabs/clients).
|
|
296
|
-
const dedupeKey =
|
|
378
|
+
const dedupeKey = JSON.stringify([req.projectId, req.repositoryId ?? null, req.relPath]);
|
|
297
379
|
const existing = this.inFlightByKey.get(dedupeKey);
|
|
298
380
|
if (existing)
|
|
299
381
|
return existing;
|
|
300
|
-
const p = this._enqueueInner(req)
|
|
301
|
-
this.inFlightByKey.set(dedupeKey, p);
|
|
302
|
-
void p.catch(() => undefined).finally(() => {
|
|
382
|
+
const p = this._enqueueInner(req).finally(() => {
|
|
303
383
|
if (this.inFlightByKey.get(dedupeKey) === p)
|
|
304
384
|
this.inFlightByKey.delete(dedupeKey);
|
|
305
385
|
});
|
|
386
|
+
this.inFlightByKey.set(dedupeKey, p);
|
|
306
387
|
return p;
|
|
307
388
|
}
|
|
308
389
|
async _enqueueInner(req) {
|
|
390
|
+
if (this._disposed)
|
|
391
|
+
return 'skipped:not-found';
|
|
309
392
|
const absolutePath = path.join(req.projectPath, req.relPath);
|
|
310
393
|
// Step 1: file readability check.
|
|
311
394
|
let newHash;
|
|
@@ -316,8 +399,12 @@ class FileSummaryManager {
|
|
|
316
399
|
return 'skipped:not-found';
|
|
317
400
|
}
|
|
318
401
|
newHash = await computeFileHash(absolutePath);
|
|
402
|
+
if (this._disposed)
|
|
403
|
+
return 'skipped:not-found';
|
|
319
404
|
}
|
|
320
405
|
catch {
|
|
406
|
+
if (this._disposed)
|
|
407
|
+
return 'skipped:not-found';
|
|
321
408
|
this.emitSkipped(req, 'not-found');
|
|
322
409
|
return 'skipped:not-found';
|
|
323
410
|
}
|
|
@@ -333,8 +420,8 @@ class FileSummaryManager {
|
|
|
333
420
|
existing &&
|
|
334
421
|
existing.fileHash === newHash &&
|
|
335
422
|
existing.language === currentLang &&
|
|
336
|
-
existing.generatedBy?.promptVersion === CURRENT_PROMPT_VERSION) {
|
|
337
|
-
this.deps.broadcast(buildSummaryUpdated(req.projectId, existing, false));
|
|
423
|
+
existing.generatedBy?.promptVersion === exports.CURRENT_PROMPT_VERSION) {
|
|
424
|
+
this.deps.broadcast(buildSummaryUpdated(req.projectId, existing, false, req.repositoryId));
|
|
338
425
|
return 'skipped:hash';
|
|
339
426
|
}
|
|
340
427
|
// Step 3: per-job cap — PRE-CHECK ONLY. The counter is incremented in pump()
|
|
@@ -361,21 +448,28 @@ class FileSummaryManager {
|
|
|
361
448
|
return 'skipped:budget';
|
|
362
449
|
}
|
|
363
450
|
}
|
|
364
|
-
//
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
451
|
+
// Hash was only an admission/cache optimization. The worker snapshots fresh
|
|
452
|
+
// bytes and hashes exactly those bytes when its queue slot actually starts.
|
|
453
|
+
return this.queueTask(req, (signal) => this.runOne(req, signal), (reason) => this.emitSkipped(req, reason));
|
|
454
|
+
}
|
|
455
|
+
getLanguage() { return this.deps.language?.() ?? 'en'; }
|
|
456
|
+
/** Stories share this manager's project/app concurrency, spend reservations
|
|
457
|
+
* and disposal. Only the task itself records its billable usage. */
|
|
458
|
+
scheduleTask(req, task) {
|
|
459
|
+
return this.queueTask(req, async (signal) => await task(signal) ? 'enqueued' : 'failed', () => { });
|
|
460
|
+
}
|
|
461
|
+
queueTask(req, run, skipped) {
|
|
462
|
+
if (this._disposed)
|
|
463
|
+
return Promise.resolve('skipped:not-found');
|
|
464
|
+
const queue = this.queues.get(req.projectId) ?? [];
|
|
465
|
+
// Bound retained request/context memory even when provider capacity stalls.
|
|
466
|
+
if (queue.length >= 200)
|
|
467
|
+
return Promise.resolve('failed');
|
|
468
|
+
return new Promise((resolve, reject) => {
|
|
469
|
+
queue.push({ req: { ...req }, enqueuedAt: (this.deps.now ?? Date.now)(), run, skipped, resolve, reject });
|
|
375
470
|
this.queues.set(req.projectId, queue);
|
|
376
471
|
this.pump(req.projectId);
|
|
377
472
|
});
|
|
378
|
-
return result;
|
|
379
473
|
}
|
|
380
474
|
// Recorded month-to-date spend PLUS the optimistically-reserved spend of
|
|
381
475
|
// generations already in flight for this project. Both budget gates read this
|
|
@@ -408,8 +502,7 @@ class FileSummaryManager {
|
|
|
408
502
|
// TTL drop before starting. Distinct 'skipped:ttl' (not 'skipped:hash') so
|
|
409
503
|
// the regenerate route can tell the user it was dropped, not silently 202.
|
|
410
504
|
if (now - entry.enqueuedAt > this.queueTtlMs) {
|
|
411
|
-
this.
|
|
412
|
-
entry.resolve('skipped:ttl');
|
|
505
|
+
this.skipEntry(entry, 'ttl');
|
|
413
506
|
continue;
|
|
414
507
|
}
|
|
415
508
|
// Budget re-check at dequeue: an entry that crossed the monthly cap while
|
|
@@ -421,8 +514,7 @@ class FileSummaryManager {
|
|
|
421
514
|
const spend = this.effectiveSpend(entry.req.projectId);
|
|
422
515
|
const budget = this.deps.monthlyBudgetUsd();
|
|
423
516
|
if (spend >= budget) {
|
|
424
|
-
this.
|
|
425
|
-
entry.resolve('skipped:budget');
|
|
517
|
+
this.skipEntry(entry, 'budget');
|
|
426
518
|
continue;
|
|
427
519
|
}
|
|
428
520
|
}
|
|
@@ -432,8 +524,7 @@ class FileSummaryManager {
|
|
|
432
524
|
if (entry.req.jobId) {
|
|
433
525
|
const count = this.jobCounter.get(entry.req.jobId) ?? 0;
|
|
434
526
|
if (count >= this.perJobCap) {
|
|
435
|
-
this.
|
|
436
|
-
entry.resolve('skipped:per-job-cap');
|
|
527
|
+
this.skipEntry(entry, 'per-job-cap');
|
|
437
528
|
continue;
|
|
438
529
|
}
|
|
439
530
|
if (!this.jobCounter.has(entry.req.jobId) && this.jobCounter.size >= this.maxJobCounters) {
|
|
@@ -452,7 +543,8 @@ class FileSummaryManager {
|
|
|
452
543
|
if (!entry.req.overrideBudget)
|
|
453
544
|
this.reserveSpend(projectId);
|
|
454
545
|
const reservedSpend = !entry.req.overrideBudget;
|
|
455
|
-
const p = this.
|
|
546
|
+
const p = this.runEntry(entry)
|
|
547
|
+
.then(entry.resolve)
|
|
456
548
|
.catch((err) => entry.reject(err))
|
|
457
549
|
.finally(() => {
|
|
458
550
|
if (reservedSpend)
|
|
@@ -479,22 +571,37 @@ class FileSummaryManager {
|
|
|
479
571
|
for (const pid of [...this.queues.keys()])
|
|
480
572
|
this.pump(pid);
|
|
481
573
|
}
|
|
482
|
-
|
|
483
|
-
|
|
574
|
+
skipEntry(entry, reason) {
|
|
575
|
+
try {
|
|
576
|
+
entry.skipped(reason);
|
|
577
|
+
}
|
|
578
|
+
catch { /* a closed WS transport cannot strand a request */ }
|
|
579
|
+
entry.resolve(`skipped:${reason}`);
|
|
580
|
+
}
|
|
581
|
+
async runEntry(entry) {
|
|
582
|
+
const controller = new AbortController();
|
|
583
|
+
this.activeControllers.add(controller);
|
|
584
|
+
try {
|
|
585
|
+
const result = await entry.run(controller.signal);
|
|
586
|
+
return controller.signal.aborted ? 'failed' : result;
|
|
587
|
+
}
|
|
588
|
+
finally {
|
|
589
|
+
this.activeControllers.delete(controller);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
async runOne(req, signal) {
|
|
484
593
|
const absolutePath = path.join(req.projectPath, req.relPath);
|
|
485
594
|
const startedIso = new Date((this.deps.now ?? Date.now)()).toISOString();
|
|
486
595
|
let contents;
|
|
487
596
|
let fileHash;
|
|
488
597
|
try {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
fileHash = entry.newHash || (await computeFileHash(absolutePath));
|
|
598
|
+
const snapshot = readSourceSnapshot(req.projectPath, req.relPath);
|
|
599
|
+
contents = snapshot.toString('utf8');
|
|
600
|
+
fileHash = (0, crypto_1.createHash)('sha256').update(snapshot).digest('hex');
|
|
493
601
|
}
|
|
494
602
|
catch {
|
|
495
603
|
this.emitSkipped(req, 'not-found');
|
|
496
|
-
|
|
497
|
-
return;
|
|
604
|
+
return 'skipped:not-found';
|
|
498
605
|
}
|
|
499
606
|
const tokens = Math.ceil(contents.length / TOKEN_CHARS_PER_TOKEN);
|
|
500
607
|
let truncated = false;
|
|
@@ -506,20 +613,18 @@ class FileSummaryManager {
|
|
|
506
613
|
promptContents = head + TRUNCATE_MARKER + tail;
|
|
507
614
|
}
|
|
508
615
|
const lang = (this.deps.language?.() ?? 'en');
|
|
509
|
-
const controller = new AbortController();
|
|
510
|
-
this.activeControllers.add(controller);
|
|
511
616
|
try {
|
|
512
617
|
const out = await this.deps.generate({
|
|
513
618
|
relPath: req.relPath,
|
|
619
|
+
repositoryId: req.repositoryId,
|
|
514
620
|
contents: promptContents,
|
|
515
621
|
truncated,
|
|
516
622
|
language: lang,
|
|
517
|
-
},
|
|
623
|
+
}, signal);
|
|
518
624
|
// The project may have been removed (and its DB closed) while the provider
|
|
519
625
|
// ran. Skip all DB/disk/broadcast work so we never touch a closed handle.
|
|
520
626
|
if (this._disposed) {
|
|
521
|
-
|
|
522
|
-
return;
|
|
627
|
+
return 'failed';
|
|
523
628
|
}
|
|
524
629
|
// Cap a runaway LLM summary at the schema bound so writeSummary's
|
|
525
630
|
// validation never rejects a real generation (it would otherwise throw and
|
|
@@ -532,7 +637,7 @@ class FileSummaryManager {
|
|
|
532
637
|
summary: boundedSummary,
|
|
533
638
|
language: lang,
|
|
534
639
|
generatedAt: new Date((this.deps.now ?? Date.now)()).toISOString(),
|
|
535
|
-
generatedBy: { model: out.model, promptVersion: CURRENT_PROMPT_VERSION, truncated },
|
|
640
|
+
generatedBy: { model: out.model, promptVersion: exports.CURRENT_PROMPT_VERSION, truncated },
|
|
536
641
|
triggeredBy: req.triggeredBy,
|
|
537
642
|
};
|
|
538
643
|
// Relocate-artifacts: summaries OUTPUT to the workspace when relocated
|
|
@@ -541,7 +646,7 @@ class FileSummaryManager {
|
|
|
541
646
|
const summaryRoot = req.summaryRoot ?? req.projectPath;
|
|
542
647
|
writeSummary(summaryRoot, req.relPath, payload, summaryRoot === req.projectPath);
|
|
543
648
|
// Keep the watcher's negative-cache a correct superset.
|
|
544
|
-
this.knownSummaries.get(req.projectId)?.add(req.relPath);
|
|
649
|
+
this.knownSummaries.get(repositoryKey(req.projectId, req.repositoryId))?.add(req.relPath);
|
|
545
650
|
try {
|
|
546
651
|
(0, ai_invocations_1.recordInvocation)(this.deps.db, {
|
|
547
652
|
id: (0, crypto_1.randomUUID)(),
|
|
@@ -570,15 +675,23 @@ class FileSummaryManager {
|
|
|
570
675
|
// spending under-counts with no trace.
|
|
571
676
|
console.error('[file-summary] recordInvocation (success) failed:', err);
|
|
572
677
|
}
|
|
573
|
-
|
|
678
|
+
let stale = isSummaryMetadataStale(payload, this.getLanguage());
|
|
679
|
+
try {
|
|
680
|
+
stale ||= await computeFileHash(absolutePath) !== fileHash;
|
|
681
|
+
}
|
|
682
|
+
catch {
|
|
683
|
+
stale = true;
|
|
684
|
+
}
|
|
685
|
+
if (this._disposed || signal.aborted)
|
|
686
|
+
return 'failed';
|
|
687
|
+
this.deps.broadcast(buildSummaryUpdated(req.projectId, payload, stale, req.repositoryId));
|
|
574
688
|
this.deps.broadcast({ type: 'spending.invalidated', projectId: req.projectId });
|
|
575
|
-
|
|
689
|
+
return 'enqueued';
|
|
576
690
|
}
|
|
577
691
|
catch (err) {
|
|
578
692
|
// Disposed mid-flight (project removed) — DB is closed; skip all writes.
|
|
579
693
|
if (this._disposed) {
|
|
580
|
-
|
|
581
|
-
return;
|
|
694
|
+
return 'failed';
|
|
582
695
|
}
|
|
583
696
|
const reason = err instanceof Error ? err.message : String(err);
|
|
584
697
|
// A timeout/abort kills the child AFTER the provider may have billed tokens.
|
|
@@ -616,25 +729,23 @@ class FileSummaryManager {
|
|
|
616
729
|
const failedMsg = {
|
|
617
730
|
type: 'file.summary_failed',
|
|
618
731
|
projectId: req.projectId,
|
|
732
|
+
...(req.repositoryId ? { repositoryId: req.repositoryId } : {}),
|
|
619
733
|
path: req.relPath,
|
|
620
734
|
reason,
|
|
621
735
|
};
|
|
622
736
|
this.deps.broadcast(failedMsg);
|
|
623
737
|
// Resolve with 'failed' (not 'enqueued') so a caller awaiting enqueue()
|
|
624
738
|
// can distinguish a failed generation from a successful one.
|
|
625
|
-
|
|
626
|
-
}
|
|
627
|
-
finally {
|
|
628
|
-
this.activeControllers.delete(controller);
|
|
739
|
+
return 'failed';
|
|
629
740
|
}
|
|
630
741
|
}
|
|
631
742
|
/** `summaryRoot` (relocate-artifacts) is where the summary JSON lives — the
|
|
632
743
|
* workspace when relocated, else === projectPath. */
|
|
633
|
-
markStale(projectPath, projectId, relPath, summaryRoot) {
|
|
744
|
+
markStale(projectPath, projectId, relPath, summaryRoot, repositoryId) {
|
|
634
745
|
const existing = readSummary(summaryRoot ?? projectPath, relPath);
|
|
635
746
|
if (!existing)
|
|
636
747
|
return;
|
|
637
|
-
this.deps.broadcast(buildSummaryUpdated(projectId, existing, true));
|
|
748
|
+
this.deps.broadcast(buildSummaryUpdated(projectId, existing, true, repositoryId));
|
|
638
749
|
}
|
|
639
750
|
/**
|
|
640
751
|
* Watch the repo SOURCE tree (`projectPath`) for edits and mark the
|
|
@@ -642,28 +753,87 @@ class FileSummaryManager {
|
|
|
642
753
|
* summary JSON lives (workspace when relocated) — source is watched at
|
|
643
754
|
* `projectPath`, summaries are scanned/swept/marked at `summaryRoot`. When
|
|
644
755
|
* omitted `summaryRoot` defaults to `projectPath` (legacy, byte-identical).
|
|
756
|
+
*
|
|
757
|
+
* Never throws and never lets the watcher take the process down: a watcher
|
|
758
|
+
* that cannot start (or later dies of fd/inotify exhaustion) leaves the
|
|
759
|
+
* project attached in `degraded` status, so later Code-Explorer requests do
|
|
760
|
+
* not retry a doomed recursive watch on every hit.
|
|
645
761
|
*/
|
|
646
|
-
attachWatcher(projectId, projectPath, summaryRoot) {
|
|
647
|
-
if (this.
|
|
762
|
+
attachWatcher(projectId, projectPath, summaryRoot, repositoryId) {
|
|
763
|
+
if (this._disposed)
|
|
648
764
|
return;
|
|
765
|
+
const key = repositoryKey(projectId, repositoryId);
|
|
766
|
+
const previous = this.watchers.get(key);
|
|
767
|
+
if (previous) {
|
|
768
|
+
// ReadDirectoryChangesW dies with EPERM when its root is removed. An
|
|
769
|
+
// explicit explorer request may reattach after it reappears, with a
|
|
770
|
+
// cooldown and a hard limit; resource exhaustion never retries here.
|
|
771
|
+
if (previous.status !== 'degraded' || previous.retryAfter === undefined ||
|
|
772
|
+
Date.now() < previous.retryAfter || (previous.restarts ?? 0) >= 3)
|
|
773
|
+
return;
|
|
774
|
+
try {
|
|
775
|
+
if (!fs.statSync(projectPath).isDirectory())
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
catch {
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
this.closeWatcherState(previous);
|
|
782
|
+
}
|
|
649
783
|
const sumRoot = summaryRoot ?? projectPath;
|
|
650
|
-
this.knownSummaries.set(
|
|
784
|
+
this.knownSummaries.set(key, this.scanKnownSummaries(sumRoot));
|
|
651
785
|
// Reclaim summary JSON files whose source file was renamed/deleted since the
|
|
652
786
|
// last session. Runs once per project per session (attachWatcher is
|
|
653
|
-
// idempotent) and is capped at 200/pass inside sweepOrphans. The
|
|
654
|
-
//
|
|
787
|
+
// idempotent) and is capped at 200/pass inside sweepOrphans. The watcher
|
|
788
|
+
// only marks stale, never deletes, so this is the only reaper.
|
|
655
789
|
// sweepOrphans resolves source files against the repo, so it must know both:
|
|
656
790
|
// summaries under sumRoot, source under projectPath.
|
|
657
791
|
try {
|
|
658
792
|
sweepOrphans(sumRoot, undefined, projectPath);
|
|
659
793
|
}
|
|
660
794
|
catch { /* best effort */ }
|
|
661
|
-
//
|
|
662
|
-
//
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
795
|
+
// Registered BEFORE the watch is created so a failed start still counts as
|
|
796
|
+
// attached (degraded) — idempotency is what stops a retry storm.
|
|
797
|
+
const state = { projectPath, status: 'degraded', close: () => { }, timers: new Map(), restarts: previous ? (previous.restarts ?? 0) + 1 : 0 };
|
|
798
|
+
this.watchers.set(key, state);
|
|
799
|
+
const engine = this.deps.watchEngine ?? resolveWatchEngine();
|
|
800
|
+
const onChange = (raw) => this.onSourceChanged(projectId, state, raw, sumRoot, repositoryId);
|
|
801
|
+
const onError = (err) => this.onWatcherError(projectId, state, err);
|
|
802
|
+
try {
|
|
803
|
+
state.close = engine === 'native'
|
|
804
|
+
? this.startNativeWatcher(projectPath, onChange, onError)
|
|
805
|
+
: this.startChokidarWatcher(projectPath, onChange, onError);
|
|
806
|
+
state.status = engine;
|
|
807
|
+
}
|
|
808
|
+
catch (err) {
|
|
809
|
+
this.degradeWatcher(projectId, state, err, 'start');
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
/** Current watcher status for a project, or null when never attached. */
|
|
813
|
+
watcherStatus(projectId, repositoryId) {
|
|
814
|
+
return this.watchers.get(repositoryKey(projectId, repositoryId))?.status ?? null;
|
|
815
|
+
}
|
|
816
|
+
/** One kernel-level recursive watch for the whole tree (FSEvents on macOS,
|
|
817
|
+
* ReadDirectoryChangesW on Windows). `filename` arrives relative to the
|
|
818
|
+
* watched root. `persistent: false` — the HTTP server keeps the process
|
|
819
|
+
* alive; a watcher never should. */
|
|
820
|
+
startNativeWatcher(projectPath, onChange, onError) {
|
|
821
|
+
const watch = this.deps.fsWatch ?? fs.watch;
|
|
822
|
+
const watcher = watch(projectPath, { recursive: true, persistent: false }, (_event, filename) => onChange(filename));
|
|
823
|
+
watcher.on('error', onError);
|
|
824
|
+
return () => { try {
|
|
825
|
+
watcher.close();
|
|
826
|
+
}
|
|
827
|
+
catch { /* best effort */ } };
|
|
828
|
+
}
|
|
829
|
+
/** chokidar (Linux): one inotify watch per file/dir, so the build/dep trees
|
|
830
|
+
* MUST be pruned — a Rust `target/` or `node_modules` alone is tens of
|
|
831
|
+
* thousands of watches. The predicate is tested against each path relative
|
|
832
|
+
* to the project root so a dot-segment in the absolute prefix (the user's
|
|
833
|
+
* home dir) can't false-positive. */
|
|
834
|
+
startChokidarWatcher(projectPath, onChange, onError) {
|
|
666
835
|
const watcher = chokidar_1.default.watch(projectPath, {
|
|
836
|
+
followSymlinks: false,
|
|
667
837
|
ignored: (p) => {
|
|
668
838
|
const rel = path.relative(projectPath, p);
|
|
669
839
|
if (!rel || rel.startsWith('..'))
|
|
@@ -671,25 +841,105 @@ class FileSummaryManager {
|
|
|
671
841
|
return (0, build_dirs_1.isInBuildDir)(rel);
|
|
672
842
|
},
|
|
673
843
|
ignoreInitial: true,
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
});
|
|
678
|
-
watcher.on('change', (changed) => {
|
|
679
|
-
// The summary store keys off POSIX forward-slash relpaths everywhere
|
|
680
|
-
// (the REST normalizeRel, pathHash, knownSummaries seeding). On Windows
|
|
681
|
-
// path.relative yields backslashes, so normalize before lookup/markStale —
|
|
682
|
-
// otherwise `known.has(rel)` always misses and edits never mark stale.
|
|
683
|
-
const rel = path.relative(projectPath, changed).split(path.sep).join('/');
|
|
684
|
-
if (!rel || rel.startsWith('..'))
|
|
685
|
-
return;
|
|
686
|
-
// Skip the readSummary disk hit when this file provably has no summary.
|
|
687
|
-
const known = this.knownSummaries.get(projectId);
|
|
688
|
-
if (known && !known.has(rel))
|
|
689
|
-
return;
|
|
690
|
-
this.markStale(projectPath, projectId, rel, sumRoot);
|
|
844
|
+
// A dir we cannot read is not a reason to lose the whole watcher.
|
|
845
|
+
ignorePermissionErrors: true,
|
|
846
|
+
persistent: false,
|
|
691
847
|
});
|
|
692
|
-
|
|
848
|
+
watcher.on('change', (changed) => onChange(changed));
|
|
849
|
+
watcher.on('unlink', (removed) => onChange(removed));
|
|
850
|
+
watcher.on('error', onError);
|
|
851
|
+
return () => { void watcher.close().catch(() => { }); };
|
|
852
|
+
}
|
|
853
|
+
/** Shared change funnel for both engines: normalise to a POSIX relpath, drop
|
|
854
|
+
* build/dep/dot trees and files that provably have no summary, then debounce
|
|
855
|
+
* per path before the (hash-checked) stale mark. */
|
|
856
|
+
onSourceChanged(projectId, state, raw, sumRoot, repositoryId) {
|
|
857
|
+
if (raw == null || this._disposed || state.status === 'degraded')
|
|
858
|
+
return;
|
|
859
|
+
const name = typeof raw === 'string' ? raw : raw.toString();
|
|
860
|
+
// Native engines report paths relative to the watched root (Windows with
|
|
861
|
+
// backslashes); chokidar reports absolute paths. Meet in the middle.
|
|
862
|
+
const relRaw = path.isAbsolute(name) ? path.relative(state.projectPath, name) : name;
|
|
863
|
+
// The summary store keys off POSIX forward-slash relpaths everywhere (the
|
|
864
|
+
// REST normalizeRel, pathHash, knownSummaries seeding) — normalise before
|
|
865
|
+
// lookup/markStale, otherwise `known.has(rel)` always misses on Windows.
|
|
866
|
+
const rel = relRaw.split(path.sep).join('/');
|
|
867
|
+
if (!rel || rel.startsWith('..') || path.isAbsolute(rel))
|
|
868
|
+
return;
|
|
869
|
+
if ((0, build_dirs_1.isInBuildDir)(rel))
|
|
870
|
+
return;
|
|
871
|
+
// Skip the disk hits when this file provably has no summary.
|
|
872
|
+
const known = this.knownSummaries.get(repositoryKey(projectId, repositoryId));
|
|
873
|
+
if (known && !known.has(rel))
|
|
874
|
+
return;
|
|
875
|
+
const pending = state.timers.get(rel);
|
|
876
|
+
if (pending)
|
|
877
|
+
clearTimeout(pending);
|
|
878
|
+
const timer = setTimeout(() => {
|
|
879
|
+
state.timers.delete(rel);
|
|
880
|
+
void this.markStaleIfChanged(state.projectPath, projectId, rel, sumRoot, repositoryId);
|
|
881
|
+
}, exports.WATCH_DEBOUNCE_MS);
|
|
882
|
+
timer.unref?.();
|
|
883
|
+
state.timers.set(rel, timer);
|
|
884
|
+
}
|
|
885
|
+
/** Watcher-side markStale: broadcast ONLY when the source really differs from
|
|
886
|
+
* what the summary was generated against. A save that leaves the bytes
|
|
887
|
+
* unchanged, an editor's temp-file dance, or a replayed/coalesced kernel
|
|
888
|
+
* event must not flag a still-valid summary. A source that vanished IS
|
|
889
|
+
* stale — the summary describes a file that is gone. */
|
|
890
|
+
async markStaleIfChanged(projectPath, projectId, rel, sumRoot, repositoryId) {
|
|
891
|
+
const existing = readSummary(sumRoot, rel);
|
|
892
|
+
if (!existing)
|
|
893
|
+
return;
|
|
894
|
+
let hash = null;
|
|
895
|
+
try {
|
|
896
|
+
hash = await computeFileHash(path.join(projectPath, rel));
|
|
897
|
+
}
|
|
898
|
+
catch {
|
|
899
|
+
hash = null;
|
|
900
|
+
}
|
|
901
|
+
if (hash !== null && hash === existing.fileHash)
|
|
902
|
+
return;
|
|
903
|
+
if (this._disposed)
|
|
904
|
+
return;
|
|
905
|
+
this.deps.broadcast(buildSummaryUpdated(projectId, existing, true, repositoryId));
|
|
906
|
+
}
|
|
907
|
+
onWatcherError(projectId, state, err) {
|
|
908
|
+
const code = errnoCode(err);
|
|
909
|
+
if (state.status === 'native' && (code === 'EPERM' || code === 'EACCES')) {
|
|
910
|
+
state.retryAfter = Date.now() + 30_000;
|
|
911
|
+
this.degradeWatcher(projectId, state, err, 'runtime');
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
if (code && !WATCHER_EXHAUSTION_CODES.has(code) && state.status !== 'degraded') {
|
|
915
|
+
// Transient and local (a dir vanished mid-scan, a permission hiccup on
|
|
916
|
+
// one subtree): keep watching, just log.
|
|
917
|
+
console.warn('[file-summary] watcher error', JSON.stringify({ projectId, code, error: errMessage(err) }));
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
this.degradeWatcher(projectId, state, err, 'runtime');
|
|
921
|
+
}
|
|
922
|
+
/** Release the watcher (and, with it, every fd/watch it holds) and park the
|
|
923
|
+
* project in degraded mode. Logged once; only a terminated native root
|
|
924
|
+
* watcher may be reattached by a later explicit request after cooldown. */
|
|
925
|
+
degradeWatcher(projectId, state, err, phase) {
|
|
926
|
+
const alreadyDegraded = state.status === 'degraded' && phase === 'runtime';
|
|
927
|
+
state.status = 'degraded';
|
|
928
|
+
this.closeWatcherState(state);
|
|
929
|
+
if (alreadyDegraded)
|
|
930
|
+
return;
|
|
931
|
+
console.warn('[file-summary] watcher degraded — edits will not mark summaries stale until the next explicit read/regenerate', JSON.stringify({ projectId, projectPath: state.projectPath, phase, code: errnoCode(err) ?? null, error: errMessage(err) }));
|
|
932
|
+
}
|
|
933
|
+
closeWatcherState(state) {
|
|
934
|
+
const close = state.close;
|
|
935
|
+
state.close = () => { };
|
|
936
|
+
try {
|
|
937
|
+
close();
|
|
938
|
+
}
|
|
939
|
+
catch { /* best effort */ }
|
|
940
|
+
for (const timer of state.timers.values())
|
|
941
|
+
clearTimeout(timer);
|
|
942
|
+
state.timers.clear();
|
|
693
943
|
}
|
|
694
944
|
/** Read the relPaths of every summary on disk into a set (one-time at attach). */
|
|
695
945
|
scanKnownSummaries(projectPath) {
|
|
@@ -714,13 +964,14 @@ class FileSummaryManager {
|
|
|
714
964
|
}
|
|
715
965
|
return set;
|
|
716
966
|
}
|
|
717
|
-
detachWatcher(projectId) {
|
|
718
|
-
const
|
|
967
|
+
detachWatcher(projectId, repositoryId) {
|
|
968
|
+
const key = repositoryKey(projectId, repositoryId);
|
|
969
|
+
const state = this.watchers.get(key);
|
|
719
970
|
if (!state)
|
|
720
971
|
return;
|
|
721
|
-
|
|
722
|
-
this.watchers.delete(
|
|
723
|
-
this.knownSummaries.delete(
|
|
972
|
+
this.closeWatcherState(state);
|
|
973
|
+
this.watchers.delete(key);
|
|
974
|
+
this.knownSummaries.delete(key);
|
|
724
975
|
}
|
|
725
976
|
/** Full teardown for a single manager: stop accepting work, abort any in-flight
|
|
726
977
|
* provider child (so it is not orphaned past project removal), reject queued
|
|
@@ -743,34 +994,30 @@ class FileSummaryManager {
|
|
|
743
994
|
// Reject still-queued entries so awaiting callers settle (skipped, not hung).
|
|
744
995
|
for (const [, queue] of this.queues) {
|
|
745
996
|
for (const entry of queue) {
|
|
746
|
-
|
|
747
|
-
this.emitSkipped(entry.req, 'not-found');
|
|
748
|
-
entry.resolve('skipped:not-found');
|
|
749
|
-
}
|
|
750
|
-
catch { /* best effort */ }
|
|
997
|
+
this.skipEntry(entry, 'not-found');
|
|
751
998
|
}
|
|
752
999
|
}
|
|
753
1000
|
this.queues.clear();
|
|
754
1001
|
this.inFlightByKey.clear();
|
|
755
1002
|
this.pendingSpend.clear();
|
|
756
|
-
for (const [, state] of this.watchers)
|
|
757
|
-
|
|
758
|
-
void state.watcher.close();
|
|
759
|
-
}
|
|
760
|
-
catch { /* best effort */ }
|
|
761
|
-
}
|
|
1003
|
+
for (const [, state] of this.watchers)
|
|
1004
|
+
this.closeWatcherState(state);
|
|
762
1005
|
this.watchers.clear();
|
|
763
1006
|
this.knownSummaries.clear();
|
|
764
1007
|
}
|
|
765
|
-
/** Close every watcher. Called on graceful server shutdown so
|
|
766
|
-
* underlying
|
|
1008
|
+
/** Close every watcher. Called on graceful server shutdown so the
|
|
1009
|
+
* underlying FSEvents/ReadDirectoryChangesW/inotify handles are released,
|
|
1010
|
+
* never leaked. */
|
|
767
1011
|
disposeAll() {
|
|
768
1012
|
this.dispose();
|
|
769
1013
|
}
|
|
770
1014
|
async flush() {
|
|
771
1015
|
// Drain until no pending work remains.
|
|
772
1016
|
while (this.pending.size > 0 || this.hasQueued()) {
|
|
773
|
-
|
|
1017
|
+
if (this.pending.size > 0)
|
|
1018
|
+
await Promise.allSettled(Array.from(this.pending));
|
|
1019
|
+
else
|
|
1020
|
+
await new Promise(resolve => setTimeout(resolve, 0)); // let another manager release the shared slot
|
|
774
1021
|
}
|
|
775
1022
|
}
|
|
776
1023
|
hasQueued() {
|
|
@@ -783,6 +1030,7 @@ class FileSummaryManager {
|
|
|
783
1030
|
const msg = {
|
|
784
1031
|
type: 'file.summary_skipped',
|
|
785
1032
|
projectId: req.projectId,
|
|
1033
|
+
...(req.repositoryId ? { repositoryId: req.repositoryId } : {}),
|
|
786
1034
|
path: req.relPath,
|
|
787
1035
|
reason,
|
|
788
1036
|
};
|
|
@@ -790,10 +1038,11 @@ class FileSummaryManager {
|
|
|
790
1038
|
}
|
|
791
1039
|
}
|
|
792
1040
|
exports.FileSummaryManager = FileSummaryManager;
|
|
793
|
-
function buildSummaryUpdated(projectId, payload, stale) {
|
|
1041
|
+
function buildSummaryUpdated(projectId, payload, stale, repositoryId) {
|
|
794
1042
|
const msg = {
|
|
795
1043
|
type: 'file.summary_updated',
|
|
796
1044
|
projectId,
|
|
1045
|
+
...(repositoryId ? { repositoryId } : {}),
|
|
797
1046
|
path: payload.path,
|
|
798
1047
|
summaryAvailable: true,
|
|
799
1048
|
stale,
|