specrails-desktop 2.61.0 → 2.63.0
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/cli/dist/specrails-desktop.js +39 -5
- package/client/dist/assets/ActivityFeedPage-Bn2MpG5M.js +1 -0
- package/client/dist/assets/{AgentBrowserCapture-CkNWDZT3.js → AgentBrowserCapture-CHlXWdXq.js} +1 -1
- package/client/dist/assets/AgentModeAnalyticsPane-yj-YCgrK.js +2 -0
- package/client/dist/assets/AgentModeCodePane-Xczcyghb.js +2 -0
- package/client/dist/assets/{AgentModeJobsPane-U1J6PaBo.js → AgentModeJobsPane-DtOW01ln.js} +1 -1
- package/client/dist/assets/AgentsPage-BMfJ4mzU.js +118 -0
- package/client/dist/assets/AnalyticsPage-CGn5cgtc.js +1 -0
- package/client/dist/assets/BarChart-BDMqVFJy.js +55 -0
- package/client/dist/assets/CodePage-C8NIginr.js +3 -0
- package/client/dist/assets/DesktopAnalyticsPage-C87r4y26.js +1 -0
- package/client/dist/assets/{DocsDialog-CSlMPemn.js → DocsDialog-Z50a_KPv.js} +1 -1
- package/client/dist/assets/{DocsPage-BKlYAvWq.js → DocsPage-DIArbEPl.js} +1 -1
- package/client/dist/assets/{ExportDropdown-CweP_1GP.js → ExportDropdown-CJdCqYd7.js} +1 -1
- package/client/dist/assets/IntegrationsPage-EAipiDWm.js +1 -0
- package/client/dist/assets/InteractiveJobComposer-BxLh4me-.js +19 -0
- package/client/dist/assets/{JobDetailModal-DYyGq1Vp.js → JobDetailModal-C7dWoRu6.js} +1 -1
- package/client/dist/assets/JobDetailPage-Ilu294WS.js +1 -0
- package/client/dist/assets/JobsPage-fPq1hefb.js +1 -0
- package/client/dist/assets/LoopBuilderPage-5M7IypEP.js +1 -0
- package/client/dist/assets/{LoopPreviewModal-DkLRfsYX.js → LoopPreviewModal-ChI3NQC7.js} +1 -1
- package/client/dist/assets/{LoopWindowSurface-CuACkXFV.js → LoopWindowSurface-D1WB9i-9.js} +1 -1
- package/client/dist/assets/LoopsPage-CCwc0Biu.js +2 -0
- package/client/dist/assets/MinimizedChatsContext-eH_KaAZ1.js +1 -0
- package/client/dist/assets/PluginsPage-DfDfEfbW.js +2 -0
- package/client/dist/assets/ProjectSettingsDialog-9NHnG8JQ.js +1 -0
- package/client/dist/assets/RepositoryDeliveries-DgzNnJmn.js +5 -0
- package/client/dist/assets/{RepositoryScopeSelector-CeCr6Jz-.js → RepositoryScopeSelector-BSgUqk_u.js} +1 -1
- package/client/dist/assets/ReviewPacketPage-D34bGpSA.js +4 -0
- package/client/dist/assets/{TemplatePreviewModal-B8yd1AGU.js → TemplatePreviewModal-CcfSxWQ7.js} +1 -1
- package/client/dist/assets/TicketDetailModalContext-BoafEcm4.js +16 -0
- package/client/dist/assets/Trans-CVSryAL1.js +1 -0
- package/client/dist/assets/agent-5bSixVzz.js +1 -0
- package/client/dist/assets/agent-BR4EHfhq.js +1 -0
- package/client/dist/assets/agent-C0IKkJP8.js +1 -0
- package/client/dist/assets/agent-C7li9ayC.js +1 -0
- package/client/dist/assets/agent-D0dRmEen.js +1 -0
- package/client/dist/assets/{agent-BjttZLIl.js → agent-DZVGHm7H.js} +1 -1
- package/client/dist/assets/agent-Dip4h2YE.js +1 -0
- package/client/dist/assets/agent-Pp-P-n1p.js +1 -0
- package/client/dist/assets/ban-wqfdzoXt.js +1 -0
- package/client/dist/assets/{button-D1xExSTF.js → button-4FS1ZDTp.js} +1 -1
- package/client/dist/assets/chevron-up-6EFqEHBj.js +1 -0
- package/client/dist/assets/chunk-OB3PAWPO-B1EMk45A.js +3 -0
- package/client/dist/assets/circle-x-D2ttKotB.js +1 -0
- package/client/dist/assets/createLucideIcon-DtODIU64.js +1 -0
- package/client/dist/assets/{dialog-CMgps2MY.js → dialog-DtdR05v1.js} +1 -1
- package/client/dist/assets/download-C9wP_42h.js +1 -0
- package/client/dist/assets/external-link-CZ6Q-v1-.js +1 -0
- package/client/dist/assets/file-text-C0blZHLh.js +1 -0
- package/client/dist/assets/{formatDistanceToNow-DZ3VWsHq.js → formatDistanceToNow-NVM6rK3i.js} +1 -1
- package/client/dist/assets/{getTimezoneOffsetInMilliseconds-9JiGlFrx.js → getTimezoneOffsetInMilliseconds-ZHg7WpdH.js} +1 -1
- package/client/dist/assets/git-branch-yf6LE1Bw.js +1 -0
- package/client/dist/assets/index-Culan6y7.js +83 -0
- package/client/dist/assets/index-D1v33giW.css +2 -0
- package/client/dist/assets/jira-api-Bl8xEIpk.js +1 -0
- package/client/dist/assets/jsx-runtime-BbYvbtY8.js +1 -0
- package/client/dist/assets/layers-BCLTgHXO.js +1 -0
- package/client/dist/assets/{lib-C7Mhm-gT.js → lib-CQJT8S6Q.js} +1 -1
- package/client/dist/assets/{lib-CGdyxIWg.js → lib-DWJfei6y.js} +1 -1
- package/client/dist/assets/loader-circle-BqV9xYGf.js +1 -0
- package/client/dist/assets/{loop-layout-BF8xMiNv.js → loop-layout-CyXj9223.js} +1 -1
- package/client/dist/assets/pencil-CxrRbDwb.js +1 -0
- package/client/dist/assets/plus-fHDdjBk1.js +1 -0
- package/client/dist/assets/{project-repositories-CadLZOnr.js → project-repositories-CvJf1a6H.js} +1 -1
- package/client/dist/assets/react-dom-DDD8FYaU.js +1 -0
- package/client/dist/assets/rotate-ccw-DalJAQQX.js +1 -0
- package/client/dist/assets/save-LmPSfsvZ.js +1 -0
- package/client/dist/assets/search-BDVp3fkH.js +1 -0
- package/client/dist/assets/{settings-Dn-pXukr.js → settings-BO2N-oPf.js} +1 -1
- package/client/dist/assets/{settings-ByVE7jdZ.js → settings-BQChAs_w.js} +1 -1
- package/client/dist/assets/{settings-BBj_kZaD.js → settings-BzmYlcQB.js} +1 -1
- package/client/dist/assets/{settings-COlH_P33.js → settings-CGjXb6TX.js} +1 -1
- package/client/dist/assets/{settings-D77mC_Lb.js → settings-CMxEWY46.js} +1 -1
- package/client/dist/assets/{settings-DZKy7MqN.js → settings-C_dRSgI2.js} +1 -1
- package/client/dist/assets/{settings-B5Hy_Kol.js → settings-Hw7av-B1.js} +1 -1
- package/client/dist/assets/settings-nryN6wuS.js +1 -0
- package/client/dist/assets/sparkles-CCfUdFGY.js +1 -0
- package/client/dist/assets/spending-BfjSJjBd.js +1 -0
- package/client/dist/assets/square-D0JTbRSR.js +1 -0
- package/client/dist/assets/trash-D8zYUmTg.js +1 -0
- package/client/dist/assets/triangle-alert-Cxpn8bqr.js +1 -0
- package/client/dist/assets/upload-CCHvUkRs.js +1 -0
- package/client/dist/assets/{useDesktop-DKNbOF0u.js → useDesktop-C5JVBeur.js} +1 -1
- package/client/dist/assets/{useRuntimeRuns-D3YMw9qx.js → useRuntimeRuns-DohBmeg8.js} +1 -1
- package/client/dist/assets/useSharedWebSocket-CNOvmgTr.js +2 -0
- package/client/dist/assets/{value-s-gzBeyF.js → value-DtOOK0wQ.js} +1 -1
- package/client/dist/assets/x-Bu7X_eR2.js +1 -0
- package/client/dist/assets/zap-bugHyWzQ.js +1 -0
- package/client/dist/index.html +44 -41
- package/docs/ci-cd.md +8 -1
- package/docs/customizing.md +1 -0
- package/docs/getting-started.md +1 -1
- package/docs/guide/de/integrations/6-agent-chat.md +11 -0
- package/docs/guide/de/settings/5-project-environment.md +27 -0
- package/docs/guide/en/integrations/6-agent-chat.md +11 -0
- package/docs/guide/en/settings/5-project-environment.md +27 -0
- package/docs/guide/es/integrations/6-agent-chat.md +11 -0
- package/docs/guide/es/settings/5-project-environment.md +27 -0
- package/docs/guide/fr/integrations/6-agent-chat.md +11 -0
- package/docs/guide/fr/settings/5-project-environment.md +27 -0
- package/docs/guide/it/integrations/6-agent-chat.md +11 -0
- package/docs/guide/it/settings/5-project-environment.md +27 -0
- package/docs/guide/ja/integrations/6-agent-chat.md +11 -0
- package/docs/guide/ja/settings/5-project-environment.md +27 -0
- package/docs/guide/pt/integrations/6-agent-chat.md +11 -0
- package/docs/guide/pt/settings/5-project-environment.md +27 -0
- package/docs/guide/zh/integrations/6-agent-chat.md +11 -0
- package/docs/guide/zh/settings/5-project-environment.md +27 -0
- package/docs/internals/README.md +1 -0
- package/docs/internals/agent-sessions.md +157 -0
- package/docs/internals/api-reference.md +35 -4
- package/docs/internals/configuration.md +80 -5
- package/docs/internals/core-runtime-updates.md +10 -3
- package/docs/internals/operations-runbook.md +11 -0
- package/docs/internals/programmatic-agent-runtime.md +1 -1
- package/docs/internals/safe-pr-review-flow.md +15 -0
- package/docs/internals/source-map.md +42 -1
- package/docs/mcp.md +1 -1
- package/docs/platforms/macos.md +2 -0
- package/docs/platforms/windows-parity.md +4 -4
- package/docs/platforms/windows.md +9 -9
- package/local-runner/dist/specrails-local-runner.js +1 -1
- package/mcp-bridge/dist/specrails-mcp.js +82 -15
- package/package.json +8 -9
- package/server/dist/agent-mcp-config.js +41 -0
- package/server/dist/core-compat.js +2 -0
- package/server/dist/desktop-db.js +101 -4
- package/server/dist/desktop-router.js +31 -2
- package/server/dist/index.js +58 -0
- package/server/dist/mcp/tools/app-settings.js +11 -0
- package/server/dist/mcp/tools/env.js +38 -3
- package/server/dist/mcp/tools/mission.js +45 -2
- package/server/dist/mcp/tools/specs.js +2 -2
- package/server/dist/modules/accounting/runtime/result-event.js +9 -1
- package/server/dist/modules/agent-runtime/runtime/agent-runtime-controls.js +25 -0
- package/server/dist/modules/agent-sessions/adapters/host-process.js +73 -0
- package/server/dist/modules/agent-sessions/adapters/http.js +31 -0
- package/server/dist/modules/agent-sessions/adapters/rpc-client.js +147 -0
- package/server/dist/modules/agent-sessions/application/session-event-pump.js +99 -0
- package/server/dist/modules/agent-sessions/domain/availability.js +25 -0
- package/server/dist/modules/agent-sessions/domain/errors.js +23 -0
- package/server/dist/modules/agent-sessions/domain/host-state.js +80 -0
- package/server/dist/modules/agent-sessions/domain/projection.js +147 -0
- package/server/dist/modules/agent-sessions/domain/protocol.js +59 -0
- package/server/dist/modules/agent-sessions/domain/subagent-policy.js +34 -0
- package/server/dist/modules/agent-sessions/index.js +31 -0
- package/server/dist/modules/agent-sessions/ports.js +2 -0
- package/server/dist/modules/agent-sessions/runtime/core-sessions-availability.js +43 -0
- package/server/dist/modules/agent-sessions/runtime/session-host-registry.js +226 -0
- package/server/dist/modules/agents/runtime/agent-store.js +3 -1
- package/server/dist/modules/execution/runtime/queue-manager.js +8 -0
- package/server/dist/modules/loops/runtime/loop-executors.js +1 -0
- package/server/dist/modules/loops/runtime/loop-implement-recipe.js +15 -4
- package/server/dist/modules/loops/runtime/loop-run-manager.js +11 -0
- package/server/dist/modules/missions/runtime/agent-chat-manager.js +137 -40
- package/server/dist/modules/missions/runtime/agent-chat-router.js +68 -0
- package/server/dist/modules/missions/runtime/agent-operator-prompt.js +8 -9
- package/server/dist/modules/missions/runtime/agent-session-store.js +133 -0
- package/server/dist/modules/missions/runtime/core-session-runner.js +204 -0
- package/server/dist/modules/missions/runtime/mission-core-sessions.js +286 -0
- package/server/dist/modules/missions/runtime/mission-session-projector.js +168 -0
- package/server/dist/modules/project-settings/adapters/http.js +38 -2
- package/server/dist/modules/project-settings/adapters/sqlite.js +30 -2
- package/server/dist/modules/project-settings/domain.js +24 -0
- package/server/dist/modules/project-settings/index.js +2 -1
- package/server/dist/modules/specs/runtime/spec-models.js +1 -1
- package/server/dist/path-resolver.js +135 -20
- package/server/dist/plugins/codex-spawn.js +15 -7
- package/server/dist/project-env.js +265 -17
- package/server/dist/project-registry.js +46 -0
- package/server/dist/project-router-settings.js +24 -1
- package/server/dist/providers/claude-adapter.js +6 -1
- package/server/dist/worktree-node-modules.js +95 -1
- package/client/dist/assets/ActivityFeedPage-Wi5XNgoG.js +0 -1
- package/client/dist/assets/AgentModeAnalyticsPane-CXz2j-Bt.js +0 -2
- package/client/dist/assets/AgentModeCodePane-DnsBPtpS.js +0 -2
- package/client/dist/assets/AgentsPage-CXrqu5vo.js +0 -118
- package/client/dist/assets/AnalyticsPage-Cvtt-LOO.js +0 -1
- package/client/dist/assets/BarChart-nBOMNfiy.js +0 -33
- package/client/dist/assets/CodePage-D35o-MFO.js +0 -3
- package/client/dist/assets/DesktopAnalyticsPage-CsDFy5rp.js +0 -1
- package/client/dist/assets/IntegrationsPage-Bhy58lnQ.js +0 -1
- package/client/dist/assets/InteractiveJobComposer-CFKvbQeJ.js +0 -19
- package/client/dist/assets/JobDetailPage-C6gpdYvj.js +0 -1
- package/client/dist/assets/JobsPage-CgS0pnKd.js +0 -1
- package/client/dist/assets/LoopBuilderPage-jUp20DSZ.js +0 -1
- package/client/dist/assets/LoopsPage-ClSrWzDd.js +0 -2
- package/client/dist/assets/MinimizedChatsContext-CroAs6Kh.js +0 -1
- package/client/dist/assets/PluginsPage-nCKQOwRL.js +0 -2
- package/client/dist/assets/ProjectSettingsDialog-BVDYwllU.js +0 -1
- package/client/dist/assets/RepositoryDeliveries-DJCm1pZr.js +0 -5
- package/client/dist/assets/ReviewPacketPage-kTErrEjx.js +0 -4
- package/client/dist/assets/TicketDetailModalContext-YBJX3RmP.js +0 -16
- package/client/dist/assets/Trans-D5eOjfvG.js +0 -1
- package/client/dist/assets/agent--8QAfoXL.js +0 -1
- package/client/dist/assets/agent-3HxXkIWX.js +0 -1
- package/client/dist/assets/agent-CpFCz32T.js +0 -1
- package/client/dist/assets/agent-DVnw6ybO.js +0 -1
- package/client/dist/assets/agent-Z5s_GTP0.js +0 -1
- package/client/dist/assets/agent-hv57-zyG.js +0 -1
- package/client/dist/assets/agent-xGrZ_aN1.js +0 -1
- package/client/dist/assets/chunk-KS7C4IRE-DEKu087k.js +0 -3
- package/client/dist/assets/circle-x-CMYsN4ZH.js +0 -1
- package/client/dist/assets/createLucideIcon-Bjbbp_KS.js +0 -1
- package/client/dist/assets/download-CFHyyHcw.js +0 -1
- package/client/dist/assets/external-link-D6XxC_Cu.js +0 -1
- package/client/dist/assets/file-text-BcLCfcWZ.js +0 -1
- package/client/dist/assets/git-branch-BRM0F5GN.js +0 -1
- package/client/dist/assets/index-Bfunvi6w.css +0 -2
- package/client/dist/assets/index-lTGJJTuk.js +0 -83
- package/client/dist/assets/jira-api-Bp0zXwjg.js +0 -1
- package/client/dist/assets/jsx-runtime-zu2_FqZY.js +0 -1
- package/client/dist/assets/loader-circle-DcygOQpC.js +0 -1
- package/client/dist/assets/pencil-C9RkrIPZ.js +0 -1
- package/client/dist/assets/plus-CsnjfkmX.js +0 -1
- package/client/dist/assets/react-dom-DiWzBDP9.js +0 -1
- package/client/dist/assets/rotate-ccw-Wh4id13q.js +0 -1
- package/client/dist/assets/save-OsUqVn99.js +0 -1
- package/client/dist/assets/search-Cq_gE3Ik.js +0 -1
- package/client/dist/assets/settings-Dbyd6gmj.js +0 -1
- package/client/dist/assets/sparkles-DrY8hb0X.js +0 -1
- package/client/dist/assets/spending-DQDNtM8q.js +0 -1
- package/client/dist/assets/square-Ci-PFcFe.js +0 -1
- package/client/dist/assets/trash-2-BOU3WdZZ.js +0 -1
- package/client/dist/assets/triangle-alert-H9T4jAmh.js +0 -1
- package/client/dist/assets/upload-DYMjAQ6M.js +0 -1
- package/client/dist/assets/useSharedWebSocket-DaWuiO0S.js +0 -2
- package/client/dist/assets/x-CBMI9ejn.js +0 -1
- package/client/dist/assets/zap-CUOlvtT1.js +0 -1
|
@@ -15,14 +15,14 @@ Installers are published on every release under:
|
|
|
15
15
|
|
|
16
16
|
> 📥 `https://specrails.dev/downloads/specrails-desktop/latest/`
|
|
17
17
|
|
|
18
|
-
Pick the
|
|
18
|
+
Pick the installer that matches your CPU architecture:
|
|
19
19
|
|
|
20
|
-
| Architecture |
|
|
21
|
-
| --- | --- |
|
|
22
|
-
| x64 | `specrails-desktop-<version>-x64-setup.exe` |
|
|
23
|
-
| ARM64 | `specrails-desktop-<version>-arm64-setup.exe` |
|
|
20
|
+
| Architecture | Installer |
|
|
21
|
+
| --- | --- |
|
|
22
|
+
| x64 | `specrails-desktop-<version>-x64-setup.exe` |
|
|
23
|
+
| ARM64 | `specrails-desktop-<version>-arm64-setup.exe` |
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Windows ships only the NSIS installer. For unattended or managed deployment, run it silently with `/S` (optionally `/D=<install dir>` as the last argument). Releases up to 2.62 also published an `.msi`; see [Updates](#updates) for how those installations migrate. The installer provisions Microsoft Edge WebView2 when it is missing, downloading it through a small embedded bootstrapper (Windows 11 and current Windows 10 already include it). Versioned copies live at `downloads/specrails-desktop/v<version>/` for archival and deep-linking.
|
|
26
26
|
|
|
27
27
|
A machine-readable `manifest.json` in `latest/` describes the current release (version, sha256, size) per platform, including `windows-x64` and `windows-arm64` entries — consumers can read it to build download links without hardcoding a version.
|
|
28
28
|
|
|
@@ -59,7 +59,7 @@ When the bundle is present, the Tauri host sets `SPECRAILS_IS_DESKTOP=1` and `SP
|
|
|
59
59
|
|
|
60
60
|
The desktop app self-updates via the Tauri updater plugin. It checks a GitHub Releases `latest.json` endpoint and, on Windows, applies updates with `installMode: "passive"` — the update runs with a minimal progress UI and the app relaunches into the new version.
|
|
61
61
|
|
|
62
|
-
Updates
|
|
62
|
+
Updates use the signed NSIS `-setup.exe` for the same architecture. Installations made from an earlier `.msi` receive that NSIS update too: the installer detects the MSI installation and removes it before installing, so a single installation remains (Windows may ask for administrator approval to remove the machine-wide MSI). The signatures are Tauri/minisign integrity signatures, separate from Authenticode signing. An incomplete installer/signature pair blocks publication.
|
|
63
63
|
|
|
64
64
|
Normal quit and updates request an authenticated graceful shutdown of the owned sidecar first, giving processes and persistent logs time to close before a bounded force-stop fallback.
|
|
65
65
|
|
|
@@ -73,8 +73,8 @@ Reserved paths (`.specrails/profiles/**`, `.claude/agents/custom-*.md`) are pres
|
|
|
73
73
|
|
|
74
74
|
## Uninstall
|
|
75
75
|
|
|
76
|
-
-
|
|
77
|
-
-
|
|
76
|
+
- Use the **Start Menu → Specrails → Uninstall** entry, or *Settings → Apps*.
|
|
77
|
+
- An installation from an earlier `.msi` release that has not updated yet: *Settings → Apps* or `msiexec /x <msi-path>`.
|
|
78
78
|
|
|
79
79
|
## Known limitations
|
|
80
80
|
|
|
@@ -4036,7 +4036,7 @@ var require_core = __commonJS({
|
|
|
4036
4036
|
constructor(opts = {}) {
|
|
4037
4037
|
this.schemas = {};
|
|
4038
4038
|
this.refs = {};
|
|
4039
|
-
this.formats =
|
|
4039
|
+
this.formats = /* @__PURE__ */ Object.create(null);
|
|
4040
4040
|
this._compilations = /* @__PURE__ */ new Set();
|
|
4041
4041
|
this._loading = {};
|
|
4042
4042
|
this._cache = /* @__PURE__ */ new Map();
|
|
@@ -7051,7 +7051,7 @@ function connectBridge(clientFacing, appFacing) {
|
|
|
7051
7051
|
});
|
|
7052
7052
|
}
|
|
7053
7053
|
pending.clear();
|
|
7054
|
-
closeBoth();
|
|
7054
|
+
if (!appFacing.recoverable) closeBoth();
|
|
7055
7055
|
};
|
|
7056
7056
|
}
|
|
7057
7057
|
|
|
@@ -7062,7 +7062,7 @@ function authenticatedFetch(agentHeaders, fetchImpl = fetch) {
|
|
|
7062
7062
|
const token = readMcpToken();
|
|
7063
7063
|
const headers = new Headers(init?.headers);
|
|
7064
7064
|
if (token) headers.set("Authorization", `Bearer ${token}`);
|
|
7065
|
-
for (const [key, value] of Object.entries(agentHeaders)) headers.set(key, value);
|
|
7065
|
+
for (const [key, value] of Object.entries(typeof agentHeaders === "function" ? agentHeaders() : agentHeaders)) headers.set(key, value);
|
|
7066
7066
|
const response = await fetchImpl(input, { ...init, headers });
|
|
7067
7067
|
const refreshed = response.status === 401 ? readMcpToken() : null;
|
|
7068
7068
|
if (refreshed && refreshed !== token) {
|
|
@@ -7073,12 +7073,25 @@ function authenticatedFetch(agentHeaders, fetchImpl = fetch) {
|
|
|
7073
7073
|
return response;
|
|
7074
7074
|
};
|
|
7075
7075
|
}
|
|
7076
|
+
function rejectsStaleSession(err) {
|
|
7077
|
+
const failure = err;
|
|
7078
|
+
if (failure?.code === 404) return true;
|
|
7079
|
+
return failure?.code === 403 && typeof failure.message === "string" && /reinitiali[sz]e/i.test(failure.message);
|
|
7080
|
+
}
|
|
7081
|
+
function isLostStream(error2) {
|
|
7082
|
+
return /SSE stream disconnected|Maximum reconnection attempts|Failed to reconnect/i.test(error2.message);
|
|
7083
|
+
}
|
|
7076
7084
|
var RecoveringHttpTransport = class {
|
|
7077
|
-
constructor(create) {
|
|
7085
|
+
constructor(create, log = () => {
|
|
7086
|
+
}) {
|
|
7078
7087
|
this.create = create;
|
|
7088
|
+
this.log = log;
|
|
7079
7089
|
this.current = this.attach(create());
|
|
7080
7090
|
}
|
|
7081
7091
|
create;
|
|
7092
|
+
log;
|
|
7093
|
+
/** connectBridge keeps the client side open when the app side can recover. */
|
|
7094
|
+
recoverable = true;
|
|
7082
7095
|
onmessage;
|
|
7083
7096
|
onerror;
|
|
7084
7097
|
onclose;
|
|
@@ -7086,12 +7099,19 @@ var RecoveringHttpTransport = class {
|
|
|
7086
7099
|
initializeMessage;
|
|
7087
7100
|
recovery;
|
|
7088
7101
|
closed = false;
|
|
7102
|
+
/** The last recovery failed; the next request must reinitialize first. */
|
|
7103
|
+
stale = false;
|
|
7089
7104
|
attach(transport) {
|
|
7090
7105
|
transport.onmessage = (message, extra) => {
|
|
7091
7106
|
if (transport === this.current && !this.closed) this.onmessage?.(message, extra);
|
|
7092
7107
|
};
|
|
7093
7108
|
transport.onerror = (error2) => {
|
|
7094
|
-
if (transport
|
|
7109
|
+
if (transport !== this.current || this.closed) return;
|
|
7110
|
+
if (isLostStream(error2)) {
|
|
7111
|
+
this.stale = true;
|
|
7112
|
+
this.log("stream-lost", error2.message);
|
|
7113
|
+
}
|
|
7114
|
+
this.onerror?.(error2);
|
|
7095
7115
|
};
|
|
7096
7116
|
transport.onclose = () => {
|
|
7097
7117
|
if (transport === this.current && !this.closed) this.onclose?.();
|
|
@@ -7104,21 +7124,31 @@ var RecoveringHttpTransport = class {
|
|
|
7104
7124
|
async send(message) {
|
|
7105
7125
|
if (this.closed) throw new Error("MCP bridge is closed");
|
|
7106
7126
|
if ("method" in message && message.method === "initialize") this.initializeMessage = message;
|
|
7127
|
+
if (this.stale && this.initializeMessage && message !== this.initializeMessage) this.recovery ??= this.recover("retry after a failed recovery");
|
|
7107
7128
|
if (this.recovery) await this.recovery;
|
|
7108
7129
|
const attempted = this.current;
|
|
7109
7130
|
try {
|
|
7110
7131
|
await attempted.send(message);
|
|
7111
7132
|
} catch (err) {
|
|
7112
|
-
if (err
|
|
7113
|
-
if (attempted === this.current) {
|
|
7114
|
-
this.recovery ??= this.reinitialize().finally(() => {
|
|
7115
|
-
this.recovery = void 0;
|
|
7116
|
-
});
|
|
7117
|
-
}
|
|
7133
|
+
if (!rejectsStaleSession(err) || !this.initializeMessage || message === this.initializeMessage) throw err;
|
|
7134
|
+
if (attempted === this.current) this.recovery ??= this.recover(`session rejected (${err.code})`);
|
|
7118
7135
|
if (this.recovery) await this.recovery;
|
|
7119
7136
|
await this.current.send(message);
|
|
7120
7137
|
}
|
|
7121
7138
|
}
|
|
7139
|
+
recover(reason) {
|
|
7140
|
+
this.log("recover", reason);
|
|
7141
|
+
return this.reinitialize().then(() => {
|
|
7142
|
+
this.stale = false;
|
|
7143
|
+
this.log("recovered");
|
|
7144
|
+
}).catch((error2) => {
|
|
7145
|
+
this.stale = true;
|
|
7146
|
+
this.log("recover-failed", error2 instanceof Error ? error2.message : String(error2));
|
|
7147
|
+
throw error2;
|
|
7148
|
+
}).finally(() => {
|
|
7149
|
+
this.recovery = void 0;
|
|
7150
|
+
});
|
|
7151
|
+
}
|
|
7122
7152
|
async reinitialize() {
|
|
7123
7153
|
const previous = this.current;
|
|
7124
7154
|
const next = this.attach(this.create());
|
|
@@ -7141,9 +7171,6 @@ var RecoveringHttpTransport = class {
|
|
|
7141
7171
|
void next.send({ ...this.initializeMessage, id }).catch(reject);
|
|
7142
7172
|
});
|
|
7143
7173
|
await next.send({ jsonrpc: "2.0", method: "notifications/initialized" });
|
|
7144
|
-
} catch (err) {
|
|
7145
|
-
await this.close();
|
|
7146
|
-
throw err;
|
|
7147
7174
|
} finally {
|
|
7148
7175
|
if (timer) clearTimeout(timer);
|
|
7149
7176
|
next.onmessage = relay;
|
|
@@ -7171,15 +7198,55 @@ var RecoveringHttpTransport = class {
|
|
|
7171
7198
|
};
|
|
7172
7199
|
|
|
7173
7200
|
// mcp-bridge/src/index.ts
|
|
7201
|
+
var import_node_fs = require("node:fs");
|
|
7202
|
+
var import_node_path = require("node:path");
|
|
7203
|
+
var LOG_LIMIT_BYTES = 256 * 1024;
|
|
7204
|
+
function lifecycleLog() {
|
|
7205
|
+
const capabilityFile = process.env.SPECRAILS_AGENT_CAPABILITY_FILE?.trim();
|
|
7206
|
+
if (!capabilityFile) return () => {
|
|
7207
|
+
};
|
|
7208
|
+
const file = (0, import_node_path.join)((0, import_node_path.dirname)(capabilityFile), "bridge.log");
|
|
7209
|
+
return (event, detail) => {
|
|
7210
|
+
try {
|
|
7211
|
+
try {
|
|
7212
|
+
if ((0, import_node_fs.statSync)(file).size > LOG_LIMIT_BYTES) (0, import_node_fs.truncateSync)(file, 0);
|
|
7213
|
+
} catch {
|
|
7214
|
+
}
|
|
7215
|
+
(0, import_node_fs.appendFileSync)(file, `${(/* @__PURE__ */ new Date()).toISOString()} pid=${process.pid} ${event}${detail ? ` ${detail.replace(/\s+/g, " ").slice(0, 500)}` : ""}
|
|
7216
|
+
`, { mode: 384 });
|
|
7217
|
+
} catch {
|
|
7218
|
+
}
|
|
7219
|
+
};
|
|
7220
|
+
}
|
|
7174
7221
|
async function main() {
|
|
7175
|
-
const
|
|
7176
|
-
|
|
7222
|
+
const log = lifecycleLog();
|
|
7223
|
+
log("start", `ppid=${process.ppid}`);
|
|
7224
|
+
for (const signal of ["SIGTERM", "SIGINT", "SIGHUP"]) process.once(signal, () => {
|
|
7225
|
+
log("signal", signal);
|
|
7226
|
+
process.exit(0);
|
|
7227
|
+
});
|
|
7228
|
+
process.on("exit", (code) => log("exit", `code=${code}`));
|
|
7229
|
+
process.stdin.once("end", () => log("stdin-end", "the client closed its side"));
|
|
7230
|
+
agentForwardHeaders();
|
|
7231
|
+
const fetchWithCredentials = authenticatedFetch(() => agentForwardHeaders());
|
|
7232
|
+
const appFacing = new RecoveringHttpTransport(() => new StreamableHTTPClientTransport(appUrl(), { fetch: fetchWithCredentials }), log);
|
|
7177
7233
|
const clientFacing = new StdioServerTransport();
|
|
7178
7234
|
connectBridge(clientFacing, appFacing);
|
|
7235
|
+
const relayError = appFacing.onerror;
|
|
7236
|
+
appFacing.onerror = (error2) => {
|
|
7237
|
+
log("app-error", error2.message);
|
|
7238
|
+
relayError?.(error2);
|
|
7239
|
+
};
|
|
7240
|
+
const relayClose = appFacing.onclose;
|
|
7241
|
+
appFacing.onclose = () => {
|
|
7242
|
+
log("close", "app side closed");
|
|
7243
|
+
relayClose?.();
|
|
7244
|
+
};
|
|
7179
7245
|
await appFacing.start();
|
|
7180
7246
|
await clientFacing.start();
|
|
7181
7247
|
}
|
|
7182
7248
|
main().catch((err) => {
|
|
7249
|
+
lifecycleLog()("fatal", err instanceof Error ? err.message : String(err));
|
|
7183
7250
|
process.stderr.write(`[specrails-mcp] fatal: ${err instanceof Error ? err.message : String(err)}
|
|
7184
7251
|
`);
|
|
7185
7252
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "specrails-desktop",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.63.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
58
58
|
"@tailwindcss/typography": "^0.5.19",
|
|
59
|
-
"ajv": "^8.
|
|
59
|
+
"ajv": "^8.20.0",
|
|
60
60
|
"better-sqlite3": "^12.8.0",
|
|
61
61
|
"chokidar": "^5.0.0",
|
|
62
62
|
"cross-spawn": "^7.0.6",
|
|
@@ -71,29 +71,28 @@
|
|
|
71
71
|
"selfsigned": "^5.5.0",
|
|
72
72
|
"tree-kill": "^1.2.2",
|
|
73
73
|
"werift": "^0.23.0",
|
|
74
|
-
"ws": "^8.
|
|
74
|
+
"ws": "^8.22.0",
|
|
75
75
|
"zod": "^3.25.76"
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
|
+
"@babel/parser": "^7.29.9",
|
|
78
79
|
"@tauri-apps/cli": "^2.0.0",
|
|
79
80
|
"@types/better-sqlite3": "^7.6.0",
|
|
80
|
-
"@types/chokidar": "^1.7.5",
|
|
81
81
|
"@types/cross-spawn": "^6.0.6",
|
|
82
|
-
"@types/diff": "^7.0.2",
|
|
83
82
|
"@types/express": "^5.0.6",
|
|
84
83
|
"@types/js-yaml": "^4.0.9",
|
|
85
84
|
"@types/multer": "^2.1.0",
|
|
86
|
-
"@types/node": "^
|
|
85
|
+
"@types/node": "^26.6.5",
|
|
87
86
|
"@types/pdf-parse": "^1.1.5",
|
|
88
87
|
"@types/supertest": "^7.2.0",
|
|
89
88
|
"@types/ws": "^8.5.0",
|
|
90
|
-
"@vitest/coverage-v8": "^
|
|
89
|
+
"@vitest/coverage-v8": "^5.0.3",
|
|
91
90
|
"concurrently": "^9.2.1",
|
|
92
91
|
"esbuild": "^0.28.1",
|
|
93
92
|
"supertest": "^7.2.2",
|
|
94
93
|
"tsx": "^4.23.0",
|
|
95
|
-
"typescript": "^
|
|
96
|
-
"vitest": "^
|
|
94
|
+
"typescript": "^7.0.2",
|
|
95
|
+
"vitest": "^5.0.3"
|
|
97
96
|
},
|
|
98
97
|
"overrides": {
|
|
99
98
|
"ip": "npm:@eggjs/ip@2.1.0",
|
|
@@ -8,6 +8,8 @@ exports.resolveNodeCommand = resolveNodeCommand;
|
|
|
8
8
|
exports.buildSpecrailsMcpEntry = buildSpecrailsMcpEntry;
|
|
9
9
|
exports.mergeSpecrailsIntoWorkspaceMcp = mergeSpecrailsIntoWorkspaceMcp;
|
|
10
10
|
exports.buildAgentMcpArgs = buildAgentMcpArgs;
|
|
11
|
+
exports.prepareAgentMcpSpec = prepareAgentMcpSpec;
|
|
12
|
+
exports.rotateAgentSessionCapability = rotateAgentSessionCapability;
|
|
11
13
|
exports.removeAgentCapabilityFile = removeAgentCapabilityFile;
|
|
12
14
|
exports.prepareAgentMcp = prepareAgentMcp;
|
|
13
15
|
exports.syncExternalServersIntoJsonFile = syncExternalServersIntoJsonFile;
|
|
@@ -235,6 +237,45 @@ function buildAgentTurnEntry(opts) {
|
|
|
235
237
|
}
|
|
236
238
|
return entry;
|
|
237
239
|
}
|
|
240
|
+
function stringRecord(value) {
|
|
241
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
242
|
+
return undefined;
|
|
243
|
+
const entries = Object.entries(value).filter((entry) => typeof entry[1] === 'string');
|
|
244
|
+
return entries.length ? Object.fromEntries(entries) : undefined;
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* MCP servers for a resident Core agent session. The `specrails` bridge reads
|
|
248
|
+
* the conversation's capability file on every request, and this call
|
|
249
|
+
* atomically (re)writes that file with the CURRENT turn's capability — so a
|
|
250
|
+
* long-lived provider process always acts under the latest turn's tier.
|
|
251
|
+
* External servers are passed through as structured specs.
|
|
252
|
+
*/
|
|
253
|
+
function prepareAgentMcpSpec(opts) {
|
|
254
|
+
const entry = buildAgentTurnEntry({ conversationId: opts.conversationId, port: opts.port, capability: opts.capability });
|
|
255
|
+
if (!entry)
|
|
256
|
+
throw new Error('The bundled Specrails MCP bridge is unavailable.');
|
|
257
|
+
// Specrails authorizes every action against the mission tier; never auto-approve external servers.
|
|
258
|
+
const specs = [{ name: 'specrails', command: entry.command, args: [...entry.args], env: { ...entry.env }, autoApprove: true }];
|
|
259
|
+
for (const server of opts.external ?? []) {
|
|
260
|
+
if (server.name === 'specrails')
|
|
261
|
+
continue;
|
|
262
|
+
const config = server.config;
|
|
263
|
+
if (typeof config.url === 'string') {
|
|
264
|
+
const headers = stringRecord(config.headers);
|
|
265
|
+
specs.push({ name: server.name, url: config.url, ...(headers ? { headers } : {}) });
|
|
266
|
+
}
|
|
267
|
+
else if (typeof config.command === 'string') {
|
|
268
|
+
const env = stringRecord(config.env);
|
|
269
|
+
specs.push({ name: server.name, command: config.command, args: Array.isArray(config.args) ? config.args.filter((arg) => typeof arg === 'string') : [], ...(env ? { env } : {}) });
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return specs;
|
|
273
|
+
}
|
|
274
|
+
/** Rotate the resident session's capability for a new turn (atomic rewrite). */
|
|
275
|
+
function rotateAgentSessionCapability(conversationId, port, capability) {
|
|
276
|
+
if (!buildAgentTurnEntry({ conversationId, port, capability }))
|
|
277
|
+
throw new Error('The bundled Specrails MCP bridge is unavailable.');
|
|
278
|
+
}
|
|
238
279
|
/** Delete the on-disk bearer as soon as its owning agent turn settles. */
|
|
239
280
|
function removeAgentCapabilityFile(conversationId, invocationId) {
|
|
240
281
|
if (!AGENT_CONVERSATION_ID_RE.test(conversationId) ||
|
|
@@ -12,6 +12,7 @@ exports.getCLIStatus = getCLIStatus;
|
|
|
12
12
|
exports.checkCoreCompat = checkCoreCompat;
|
|
13
13
|
exports.coreCompatSupportsProvider = coreCompatSupportsProvider;
|
|
14
14
|
const child_process_1 = require("child_process");
|
|
15
|
+
const agent_sessions_1 = require("./modules/agent-sessions");
|
|
15
16
|
const fs_1 = __importDefault(require("fs"));
|
|
16
17
|
const path_1 = __importDefault(require("path"));
|
|
17
18
|
const setup_manager_1 = require("./setup-manager");
|
|
@@ -264,6 +265,7 @@ async function checkCoreCompat() {
|
|
|
264
265
|
contractFound: true,
|
|
265
266
|
contractSchemaVersion: contract.schemaVersion,
|
|
266
267
|
supportedProviders,
|
|
268
|
+
sessions: contract.agentRuntime?.sessions ? (0, agent_sessions_1.checkSessionContract)(contract.agentRuntime.sessions) : null,
|
|
267
269
|
};
|
|
268
270
|
}
|
|
269
271
|
/** True only when the located Core contract explicitly renders this provider. */
|
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.AGENT_SEARCH_INDEX_DDL = void 0;
|
|
6
|
+
exports.GLOBAL_SUBAGENT_RUNTIME_KEY = exports.GLOBAL_ALLOW_SUBAGENTS_KEY = exports.AGENT_SEARCH_INDEX_DDL = void 0;
|
|
7
7
|
exports.setProjectProvidersMirror = setProjectProvidersMirror;
|
|
8
8
|
exports.getDesktopDbPath = getDesktopDbPath;
|
|
9
9
|
exports.rebuildAgentSearchIndex = rebuildAgentSearchIndex;
|
|
@@ -19,6 +19,9 @@ exports.updateProjectRepository = updateProjectRepository;
|
|
|
19
19
|
exports.removeProjectRepository = removeProjectRepository;
|
|
20
20
|
exports.removeProject = removeProject;
|
|
21
21
|
exports.touchProject = touchProject;
|
|
22
|
+
exports.getGlobalAllowSubagents = getGlobalAllowSubagents;
|
|
23
|
+
exports.getGlobalSubagentRuntime = getGlobalSubagentRuntime;
|
|
24
|
+
exports.setGlobalSubagentRuntime = setGlobalSubagentRuntime;
|
|
22
25
|
exports.getDesktopSetting = getDesktopSetting;
|
|
23
26
|
exports.setDesktopSetting = setDesktopSetting;
|
|
24
27
|
exports.setProjectSetupSession = setProjectSetupSession;
|
|
@@ -39,6 +42,7 @@ exports.getWebhook = getWebhook;
|
|
|
39
42
|
exports.addWebhook = addWebhook;
|
|
40
43
|
exports.updateWebhook = updateWebhook;
|
|
41
44
|
exports.removeWebhook = removeWebhook;
|
|
45
|
+
const project_settings_1 = require("./modules/project-settings");
|
|
42
46
|
const fs_1 = __importDefault(require("fs"));
|
|
43
47
|
const path_1 = __importDefault(require("path"));
|
|
44
48
|
const os_1 = __importDefault(require("os"));
|
|
@@ -683,6 +687,75 @@ function applyDesktopMigrations(db) {
|
|
|
683
687
|
if (!columns.includes('workspace_paths'))
|
|
684
688
|
db.exec('ALTER TABLE project_repositories ADD COLUMN workspace_paths TEXT');
|
|
685
689
|
},
|
|
690
|
+
// 33: core-agent-sessions-host — Desktop's rebuildable projection of Core
|
|
691
|
+
// agent sessions. Core's journal (~/.specrails/sessions/<scope>/) is the
|
|
692
|
+
// source of truth; these rows are re-derivable by replaying it. Cursor and
|
|
693
|
+
// projected rows are always written in one transaction. Column-guarded for
|
|
694
|
+
// migration replay fixtures.
|
|
695
|
+
() => {
|
|
696
|
+
db.exec(`
|
|
697
|
+
CREATE TABLE IF NOT EXISTS agent_session_cursors (
|
|
698
|
+
conversation_id TEXT PRIMARY KEY REFERENCES agent_conversations(id) ON DELETE CASCADE,
|
|
699
|
+
core_session_id TEXT NOT NULL,
|
|
700
|
+
scope TEXT NOT NULL,
|
|
701
|
+
last_seq INTEGER NOT NULL DEFAULT 0,
|
|
702
|
+
resident_phase TEXT NOT NULL DEFAULT 'idle' CHECK (resident_phase IN ('idle', 'turn', 'background')),
|
|
703
|
+
process_alive INTEGER NOT NULL DEFAULT 0,
|
|
704
|
+
live_subagents INTEGER NOT NULL DEFAULT 0,
|
|
705
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
706
|
+
);
|
|
707
|
+
CREATE TABLE IF NOT EXISTS agent_subagents (
|
|
708
|
+
conversation_id TEXT NOT NULL REFERENCES agent_conversations(id) ON DELETE CASCADE,
|
|
709
|
+
subagent_id TEXT NOT NULL,
|
|
710
|
+
parent_id TEXT,
|
|
711
|
+
kind TEXT NOT NULL CHECK (kind IN ('foreground', 'background')),
|
|
712
|
+
agent_type TEXT,
|
|
713
|
+
description TEXT NOT NULL,
|
|
714
|
+
phase TEXT NOT NULL CHECK (phase IN ('running', 'idle', 'failed', 'stopped', 'killed', 'interrupted')),
|
|
715
|
+
reason TEXT,
|
|
716
|
+
restarts INTEGER NOT NULL DEFAULT 0,
|
|
717
|
+
launched_turn_id TEXT,
|
|
718
|
+
started_at TEXT NOT NULL,
|
|
719
|
+
ended_at TEXT,
|
|
720
|
+
usage_json TEXT,
|
|
721
|
+
tool_uses INTEGER,
|
|
722
|
+
duration_ms INTEGER,
|
|
723
|
+
result_summary TEXT,
|
|
724
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
725
|
+
PRIMARY KEY (conversation_id, subagent_id)
|
|
726
|
+
);
|
|
727
|
+
CREATE INDEX IF NOT EXISTS idx_agent_subagents_turn ON agent_subagents(conversation_id, launched_turn_id);
|
|
728
|
+
CREATE TABLE IF NOT EXISTS agent_subagent_events (
|
|
729
|
+
conversation_id TEXT NOT NULL,
|
|
730
|
+
subagent_id TEXT NOT NULL,
|
|
731
|
+
seq INTEGER NOT NULL,
|
|
732
|
+
channel TEXT NOT NULL CHECK (channel IN ('text', 'tool')),
|
|
733
|
+
delta TEXT,
|
|
734
|
+
tool_json TEXT,
|
|
735
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
736
|
+
PRIMARY KEY (conversation_id, subagent_id, seq),
|
|
737
|
+
FOREIGN KEY (conversation_id, subagent_id) REFERENCES agent_subagents(conversation_id, subagent_id) ON DELETE CASCADE
|
|
738
|
+
);
|
|
739
|
+
`);
|
|
740
|
+
const invocationColumns = db.prepare('PRAGMA table_info(agent_invocations)').all().map((c) => c.name);
|
|
741
|
+
if (!invocationColumns.includes('origin'))
|
|
742
|
+
db.exec(`ALTER TABLE agent_invocations ADD COLUMN origin TEXT NOT NULL DEFAULT 'user' CHECK (origin IN ('user', 'subagent', 'system'));`);
|
|
743
|
+
const messageColumns = db.prepare('PRAGMA table_info(agent_messages)').all().map((c) => c.name);
|
|
744
|
+
if (!messageColumns.includes('core_turn_id'))
|
|
745
|
+
db.exec(`ALTER TABLE agent_messages ADD COLUMN core_turn_id TEXT;`);
|
|
746
|
+
if (!messageColumns.includes('turn_origin'))
|
|
747
|
+
db.exec(`ALTER TABLE agent_messages ADD COLUMN turn_origin TEXT CHECK (turn_origin IS NULL OR turn_origin IN ('user', 'subagent', 'system'));`);
|
|
748
|
+
db.exec(`CREATE INDEX IF NOT EXISTS idx_agent_messages_core_turn ON agent_messages(conversation_id, core_turn_id) WHERE core_turn_id IS NOT NULL;`);
|
|
749
|
+
},
|
|
750
|
+
// 34: hybrid sub-agent runtime — sub-agents Core launched on another
|
|
751
|
+
// provider record which driver and model ran them. Column-guarded.
|
|
752
|
+
() => {
|
|
753
|
+
const columns = db.prepare('PRAGMA table_info(agent_subagents)').all().map((c) => c.name);
|
|
754
|
+
if (!columns.includes('delegated_driver'))
|
|
755
|
+
db.exec('ALTER TABLE agent_subagents ADD COLUMN delegated_driver TEXT');
|
|
756
|
+
if (!columns.includes('delegated_model'))
|
|
757
|
+
db.exec('ALTER TABLE agent_subagents ADD COLUMN delegated_model TEXT');
|
|
758
|
+
},
|
|
686
759
|
];
|
|
687
760
|
(0, sqlite_migrations_1.applyNumberedMigrations)(db, migrations);
|
|
688
761
|
}
|
|
@@ -805,6 +878,30 @@ function removeProject(db, id) {
|
|
|
805
878
|
function touchProject(db, id) {
|
|
806
879
|
db.prepare("UPDATE projects SET last_seen_at = datetime('now') WHERE id = ?").run(id);
|
|
807
880
|
}
|
|
881
|
+
/** App-global "Allow sub-agents" for missions without a project (default off). */
|
|
882
|
+
exports.GLOBAL_ALLOW_SUBAGENTS_KEY = 'agent_allow_subagents';
|
|
883
|
+
function getGlobalAllowSubagents(db) {
|
|
884
|
+
return getDesktopSetting(db, exports.GLOBAL_ALLOW_SUBAGENTS_KEY) === 'true';
|
|
885
|
+
}
|
|
886
|
+
/** App-wide "Run sub-agents with" for missions without a project; null = the mission agent's provider. */
|
|
887
|
+
exports.GLOBAL_SUBAGENT_RUNTIME_KEY = 'agent_subagent_runtime';
|
|
888
|
+
function getGlobalSubagentRuntime(db) {
|
|
889
|
+
const raw = getDesktopSetting(db, exports.GLOBAL_SUBAGENT_RUNTIME_KEY);
|
|
890
|
+
if (!raw)
|
|
891
|
+
return null;
|
|
892
|
+
try {
|
|
893
|
+
return (0, project_settings_1.parseSubagentRuntimeSetting)(JSON.parse(raw));
|
|
894
|
+
}
|
|
895
|
+
catch {
|
|
896
|
+
return null;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
function setGlobalSubagentRuntime(db, value) {
|
|
900
|
+
if (value === null)
|
|
901
|
+
db.prepare('DELETE FROM desktop_settings WHERE key = ?').run(exports.GLOBAL_SUBAGENT_RUNTIME_KEY);
|
|
902
|
+
else
|
|
903
|
+
setDesktopSetting(db, exports.GLOBAL_SUBAGENT_RUNTIME_KEY, JSON.stringify(value));
|
|
904
|
+
}
|
|
808
905
|
function getDesktopSetting(db, key) {
|
|
809
906
|
const row = db.prepare('SELECT value FROM desktop_settings WHERE key = ?').get(key);
|
|
810
907
|
return row?.value;
|
|
@@ -828,9 +925,9 @@ function recordAgentInvocation(db, input) {
|
|
|
828
925
|
id, conversation_id, project_id, provider, surface, model, status,
|
|
829
926
|
started_at, finished_at, duration_ms, duration_api_ms,
|
|
830
927
|
tokens_in, tokens_out, tokens_cache_read, tokens_cache_create,
|
|
831
|
-
total_cost_usd, total_cost_usd_estimated, num_turns, session_id
|
|
832
|
-
) VALUES (?, ?, ?, ?, 'agent-chat', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
833
|
-
`).run(input.id, input.conversation_id, input.project_id ?? null, input.provider, input.model ?? null, input.status, input.started_at, input.finished_at ?? null, input.duration_ms ?? null, input.duration_api_ms ?? null, input.tokens_in ?? null, input.tokens_out ?? null, input.tokens_cache_read ?? null, input.tokens_cache_create ?? null, input.total_cost_usd ?? null, input.total_cost_usd_estimated ? 1 : 0, input.num_turns ?? null, input.session_id ?? null);
|
|
928
|
+
total_cost_usd, total_cost_usd_estimated, num_turns, session_id, origin
|
|
929
|
+
) VALUES (?, ?, ?, ?, 'agent-chat', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
930
|
+
`).run(input.id, input.conversation_id, input.project_id ?? null, input.provider, input.model ?? null, input.status, input.started_at, input.finished_at ?? null, input.duration_ms ?? null, input.duration_api_ms ?? null, input.tokens_in ?? null, input.tokens_out ?? null, input.tokens_cache_read ?? null, input.tokens_cache_create ?? null, input.total_cost_usd ?? null, input.total_cost_usd_estimated ? 1 : 0, input.num_turns ?? null, input.session_id ?? null, input.origin ?? 'user');
|
|
834
931
|
}
|
|
835
932
|
/**
|
|
836
933
|
* Sum of all agent-chat turn costs, optionally since an ISO instant (inclusive).
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.createDesktopRouter = createDesktopRouter;
|
|
7
|
+
const project_settings_1 = require("./modules/project-settings");
|
|
7
8
|
const http_1 = require("./modules/subscription-usage/adapters/http");
|
|
8
9
|
const runtime_role_prompts_router_1 = require("./runtime-role-prompts-router");
|
|
9
10
|
const express_1 = require("express");
|
|
@@ -816,11 +817,26 @@ function createDesktopRouter(registry, broadcast, usageService) {
|
|
|
816
817
|
'http://localhost:3000';
|
|
817
818
|
const costAlertThresholdRaw = (0, desktop_db_1.getDesktopSetting)(registry.desktopDb, 'cost_alert_threshold_usd');
|
|
818
819
|
const costAlertThresholdUsd = costAlertThresholdRaw != null ? parseFloat(costAlertThresholdRaw) : null;
|
|
819
|
-
|
|
820
|
+
const allowSubagents = (0, desktop_db_1.getGlobalAllowSubagents)(registry.desktopDb);
|
|
821
|
+
res.json({ port: parseInt(port, 10), specrailsTechUrl, costAlertThresholdUsd, allowSubagents, subagentRuntime: (0, desktop_db_1.getGlobalSubagentRuntime)(registry.desktopDb) });
|
|
820
822
|
});
|
|
821
823
|
// PUT /api/settings — update app-level settings
|
|
822
824
|
router.put('/settings', (req, res) => {
|
|
823
|
-
const { port, specrailsTechUrl, costAlertThresholdUsd } = req.body ?? {};
|
|
825
|
+
const { port, specrailsTechUrl, costAlertThresholdUsd, allowSubagents } = req.body ?? {};
|
|
826
|
+
if (allowSubagents !== undefined && typeof allowSubagents !== 'boolean') {
|
|
827
|
+
res.status(400).json({ error: 'allowSubagents must be a boolean' });
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
let subagentRuntime;
|
|
831
|
+
if (req.body?.subagentRuntime !== undefined) {
|
|
832
|
+
try {
|
|
833
|
+
subagentRuntime = (0, project_settings_1.parseSubagentRuntimeSetting)(req.body.subagentRuntime);
|
|
834
|
+
}
|
|
835
|
+
catch (error) {
|
|
836
|
+
res.status(400).json({ error: error.message });
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
824
840
|
if (port !== undefined) {
|
|
825
841
|
const n = Number(port);
|
|
826
842
|
if (!Number.isInteger(n) || n < 1 || n > 65535) {
|
|
@@ -848,6 +864,19 @@ function createDesktopRouter(registry, broadcast, usageService) {
|
|
|
848
864
|
(0, desktop_db_1.setDesktopSetting)(registry.desktopDb, 'cost_alert_threshold_usd', String(costAlertThresholdUsd));
|
|
849
865
|
}
|
|
850
866
|
}
|
|
867
|
+
if (allowSubagents !== undefined) {
|
|
868
|
+
const previous = (0, desktop_db_1.getGlobalAllowSubagents)(registry.desktopDb);
|
|
869
|
+
(0, desktop_db_1.setDesktopSetting)(registry.desktopDb, desktop_db_1.GLOBAL_ALLOW_SUBAGENTS_KEY, allowSubagents ? 'true' : 'false');
|
|
870
|
+
// Project-less missions re-read their sub-agent policy.
|
|
871
|
+
if (previous !== allowSubagents)
|
|
872
|
+
registry.notifySettingsChanged({ projectId: null });
|
|
873
|
+
}
|
|
874
|
+
if (subagentRuntime !== undefined) {
|
|
875
|
+
const previous = JSON.stringify((0, desktop_db_1.getGlobalSubagentRuntime)(registry.desktopDb));
|
|
876
|
+
(0, desktop_db_1.setGlobalSubagentRuntime)(registry.desktopDb, subagentRuntime);
|
|
877
|
+
if (previous !== JSON.stringify(subagentRuntime))
|
|
878
|
+
registry.notifySettingsChanged({ projectId: null });
|
|
879
|
+
}
|
|
851
880
|
res.json({ ok: true });
|
|
852
881
|
});
|
|
853
882
|
// ─── Budget routes ────────────────────────────────────────────────────────────
|
package/server/dist/index.js
CHANGED
|
@@ -19,6 +19,8 @@ const desktop_router_1 = require("./desktop-router");
|
|
|
19
19
|
const provider_detection_1 = require("./provider-detection");
|
|
20
20
|
const provider_selection_1 = require("./provider-selection");
|
|
21
21
|
const desktop_db_1 = require("./desktop-db");
|
|
22
|
+
const sqlite_1 = require("./modules/project-settings/adapters/sqlite");
|
|
23
|
+
const agent_sessions_1 = require("./modules/agent-sessions");
|
|
22
24
|
const legacy_migration_1 = require("./legacy-migration");
|
|
23
25
|
const framework_reseed_1 = require("./framework-reseed");
|
|
24
26
|
const project_router_1 = require("./project-router");
|
|
@@ -31,6 +33,13 @@ const feature_flags_1 = require("./feature-flags");
|
|
|
31
33
|
const mobile_1 = require("./mobile");
|
|
32
34
|
const mcp_1 = require("./mcp");
|
|
33
35
|
const agent_chat_manager_1 = require("./modules/missions/runtime/agent-chat-manager");
|
|
36
|
+
const mission_core_sessions_1 = require("./modules/missions/runtime/mission-core-sessions");
|
|
37
|
+
const session_host_registry_1 = require("./modules/agent-sessions/runtime/session-host-registry");
|
|
38
|
+
const http_2 = require("./modules/agent-sessions/adapters/http");
|
|
39
|
+
const host_process_1 = require("./modules/agent-sessions/adapters/host-process");
|
|
40
|
+
const core_sessions_availability_1 = require("./modules/agent-sessions/runtime/core-sessions-availability");
|
|
41
|
+
const registry_1 = require("./providers/registry");
|
|
42
|
+
const agent_capability_1 = require("./mcp/agent-capability");
|
|
34
43
|
const agent_chat_router_1 = require("./modules/missions/runtime/agent-chat-router");
|
|
35
44
|
const blueprint_chat_manager_1 = require("./modules/builder/runtime/blueprint-chat-manager");
|
|
36
45
|
const blueprint_router_1 = require("./modules/builder/runtime/blueprint-router");
|
|
@@ -282,6 +291,7 @@ let _registry = null;
|
|
|
282
291
|
let _mobileGateway = null;
|
|
283
292
|
let _mcpManager = null;
|
|
284
293
|
let _agentChatManager = null;
|
|
294
|
+
let _sessionHosts = null;
|
|
285
295
|
let _blueprintChatManager = null;
|
|
286
296
|
let _headroomManager = null;
|
|
287
297
|
server.on('upgrade', (request, socket, head) => {
|
|
@@ -654,10 +664,56 @@ function applyPtyWsRateLimiting(ws) {
|
|
|
654
664
|
});
|
|
655
665
|
const agentChatManager = new agent_chat_manager_1.AgentChatManager(broadcast, registry.desktopDb, port, registry);
|
|
656
666
|
_agentChatManager = agentChatManager;
|
|
667
|
+
// Core agent sessions (core-agent-sessions-host): one supervised Core host per
|
|
668
|
+
// project scope; missions use it when SPECRAILS_CORE_SESSIONS allows and the
|
|
669
|
+
// selected Core advertises `sessions`. Otherwise missions keep legacy runners.
|
|
670
|
+
const sessionHosts = new session_host_registry_1.SessionHostRegistry({
|
|
671
|
+
launcher: new host_process_1.CoreHostLauncher({ host: { name: 'specrails-desktop', version: process.env.npm_package_version ?? 'unknown' } }),
|
|
672
|
+
clock: { now: () => Date.now(), after: (ms, callback) => { const timer = setTimeout(callback, ms); timer.unref?.(); return { cancel: () => clearTimeout(timer) }; } },
|
|
673
|
+
onStatus: (scope, status, detail, code) => {
|
|
674
|
+
console.log(`[agent-sessions] ${scope}: ${status}${detail ? ` (${detail})` : ''}`);
|
|
675
|
+
// App-level event (no projectId): open missions show a notice when their scope degrades.
|
|
676
|
+
broadcast({ type: 'agent_sessions_host', scope, status, detail: detail ?? null, code: code ?? null, timestamp: new Date().toISOString() });
|
|
677
|
+
},
|
|
678
|
+
});
|
|
679
|
+
_sessionHosts = sessionHosts;
|
|
680
|
+
const projectAllowsSubagents = (projectId) => {
|
|
681
|
+
// A pinned project that is not loaded cannot vouch for sub-agents.
|
|
682
|
+
const context = registry.getContext(projectId);
|
|
683
|
+
return context ? (0, sqlite_1.getProjectSettings)(context.db).allowSubagents : false;
|
|
684
|
+
};
|
|
685
|
+
const missionCoreSessions = new mission_core_sessions_1.MissionCoreSessions({
|
|
686
|
+
db: registry.desktopDb,
|
|
687
|
+
registry: sessionHosts,
|
|
688
|
+
port,
|
|
689
|
+
broadcast: (message) => broadcast(message),
|
|
690
|
+
adapterFor: registry_1.getAdapter,
|
|
691
|
+
availability: () => (0, core_sessions_availability_1.coreSessionsAvailability)(),
|
|
692
|
+
projectKey: (projectId) => registry.getProjectRow(projectId)?.slug ?? null,
|
|
693
|
+
revokeCapability: agent_capability_1.revokeAgentCapability,
|
|
694
|
+
// "Allow sub-agents": the project setting decides; missions without a project use the app setting.
|
|
695
|
+
subagentPolicy: (conversation) => (0, agent_sessions_1.resolveSubagentPolicy)({
|
|
696
|
+
surface: 'mission',
|
|
697
|
+
projectAllows: conversation.pinned_project_id ? projectAllowsSubagents(conversation.pinned_project_id) : null,
|
|
698
|
+
globalAllows: (0, desktop_db_1.getGlobalAllowSubagents)(registry.desktopDb),
|
|
699
|
+
}),
|
|
700
|
+
// "Run sub-agents with": the project's choice, or the app-wide one without a project.
|
|
701
|
+
subagentRuntime: (conversation) => {
|
|
702
|
+
if (!conversation.pinned_project_id)
|
|
703
|
+
return (0, desktop_db_1.getGlobalSubagentRuntime)(registry.desktopDb);
|
|
704
|
+
const context = registry.getContext(conversation.pinned_project_id);
|
|
705
|
+
return context ? (0, sqlite_1.getProjectSettings)(context.db).subagentRuntime : null;
|
|
706
|
+
},
|
|
707
|
+
});
|
|
708
|
+
agentChatManager.setCoreSessions(missionCoreSessions);
|
|
709
|
+
registry.onSettingsChanged((scope) => { void missionCoreSessions.refreshSubagentPolicy(scope); });
|
|
710
|
+
registry.onProjectRemoved(({ slug }) => { void sessionHosts.stop(slug); });
|
|
657
711
|
// Publish the instance to the process-wide registry so the rails layer can
|
|
658
712
|
// post PR-decision cards (safe-pr-review-flow). Left null when agent chat is
|
|
659
713
|
// disabled — the rails callers are null-safe and simply skip the card.
|
|
660
714
|
(0, agent_chat_registry_1.setAgentChatManager)((0, agent_chat_router_1.isAgentChatEnabled)() ? agentChatManager : null);
|
|
715
|
+
// Before the agent router: `/api/agent/session-hosts` is not a conversation route.
|
|
716
|
+
app.use('/api/agent/session-hosts', (0, http_2.createSessionHostsRouter)(sessionHosts));
|
|
661
717
|
app.use('/api/agent', (0, agent_chat_router_1.createAgentChatRouter)({ manager: agentChatManager, desktopDb: registry.desktopDb }));
|
|
662
718
|
// Project Builder day-0 chat + orchestrated commit (add-project-builder).
|
|
663
719
|
// App-level like agent chat — a Builder conversation runs before any project
|
|
@@ -790,6 +846,8 @@ async function shutdown() {
|
|
|
790
846
|
Promise.resolve().then(() => _mobileGateway?.stop()),
|
|
791
847
|
Promise.resolve().then(() => _mcpManager?.stop()),
|
|
792
848
|
Promise.resolve().then(() => _agentChatManager?.shutdown()),
|
|
849
|
+
// Core session hosts record running work as interrupted and release their journals.
|
|
850
|
+
Promise.resolve().then(() => _sessionHosts?.stopAll()),
|
|
793
851
|
Promise.resolve().then(() => _blueprintChatManager?.shutdown()),
|
|
794
852
|
]);
|
|
795
853
|
// The shell can exit before its application descendants. Keep the event
|