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
|
@@ -23,6 +23,7 @@ function appTools() {
|
|
|
23
23
|
description: 'Read or update app-level Specrails settings (theme, language, daily budget, cost-alert threshold, Code Explorer summary language + monthly summary budget). ' +
|
|
24
24
|
'Actions: get, set, runtime_providers.list (connections + live status), runtime_providers.test (probe an OpenAI-compatible baseUrl without saving), runtime_providers.save (replace the connections list — local AI engines re-register live). ' +
|
|
25
25
|
'The MCP enable flag and permission tiers are read-only here (user-controlled in the app). ' +
|
|
26
|
+
'"Allow sub-agents" and "Run sub-agents with" are also read-only here (an agent must not grant itself sub-agents): get reports allowSubagentsWithoutProject and subagentRuntimeWithoutProject, and with projectId the project settings (null runtime = same as the mission agent). ' +
|
|
26
27
|
'dailyBudgetUsd here is APP-WIDE (all projects combined); per-project caps live in specrails_analytics(budget_set).',
|
|
27
28
|
hintTier: 'read',
|
|
28
29
|
tier: (args) => (args.action === 'set' || args.action === 'runtime_providers.save' ? 'write' : 'read'),
|
|
@@ -43,6 +44,7 @@ function appTools() {
|
|
|
43
44
|
.optional()
|
|
44
45
|
.describe('APP-WIDE daily budget cap USD across all projects; null clears it. Per-project caps live in specrails_analytics(budget_set).'),
|
|
45
46
|
costAlertThresholdUsd: zod_1.z.number().min(0).nullable().optional(),
|
|
47
|
+
projectId: zod_1.z.string().optional().describe('get: also report this project\'s "Allow sub-agents" setting'),
|
|
46
48
|
summaryLanguage: zod_1.z
|
|
47
49
|
.enum(['en', 'es'])
|
|
48
50
|
.optional()
|
|
@@ -83,6 +85,11 @@ function appTools() {
|
|
|
83
85
|
// Code Explorer settings (same keys + defaults as GET /api/code-explorer-settings)
|
|
84
86
|
summaryLanguage: (0, desktop_db_1.getDesktopSetting)(db, 'summary_language') === 'es' ? 'es' : 'en',
|
|
85
87
|
summaryMonthlyBudgetUsd: nonNegNum((0, desktop_db_1.getDesktopSetting)(db, 'summary_monthly_budget_usd')) ?? 5.0,
|
|
88
|
+
allowSubagentsWithoutProject: (0, desktop_db_1.getGlobalAllowSubagents)(db),
|
|
89
|
+
subagentRuntimeWithoutProject: (0, desktop_db_1.getGlobalSubagentRuntime)(db),
|
|
90
|
+
...(typeof args.projectId === 'string' && args.projectId
|
|
91
|
+
? await projectSubagentSettings(ctx, args.projectId)
|
|
92
|
+
: {}),
|
|
86
93
|
mcp: {
|
|
87
94
|
enabled: (0, mcp_tiers_1.isMcpEnabled)(db),
|
|
88
95
|
tierWrite: (0, mcp_tiers_1.isTierEnabled)(db, 'write'),
|
|
@@ -161,3 +168,7 @@ function writeNumOrClear(db, key, value) {
|
|
|
161
168
|
else
|
|
162
169
|
(0, desktop_db_1.setDesktopSetting)(db, key, String(value));
|
|
163
170
|
}
|
|
171
|
+
async function projectSubagentSettings(ctx, projectId) {
|
|
172
|
+
const settings = (await (0, types_1.apiCall)(ctx, 'GET', `/projects/${encodeURIComponent(projectId)}/settings`));
|
|
173
|
+
return { projectAllowSubagents: settings.allowSubagents === true, projectSubagentRuntime: settings.subagentRuntime ?? null };
|
|
174
|
+
}
|
|
@@ -15,6 +15,26 @@ function uniq(names) {
|
|
|
15
15
|
}
|
|
16
16
|
return out;
|
|
17
17
|
}
|
|
18
|
+
/** Names and states only: rebuild each record field by field so nothing but
|
|
19
|
+
* the documented, value-free fields can ever reach an MCP client. */
|
|
20
|
+
function sanitizeStatus(raw) {
|
|
21
|
+
const report = (raw && typeof raw === 'object' ? raw : {});
|
|
22
|
+
const names = Array.isArray(report.names) ? report.names : [];
|
|
23
|
+
return {
|
|
24
|
+
checking: report.checking === true,
|
|
25
|
+
loginShellRecovery: report.loginShellRecovery !== false,
|
|
26
|
+
timeoutMs: typeof report.timeoutMs === 'number' ? report.timeoutMs : null,
|
|
27
|
+
statuses: names
|
|
28
|
+
.filter((entry) => typeof entry?.name === 'string' && typeof entry?.status === 'string')
|
|
29
|
+
.map((entry) => ({
|
|
30
|
+
name: entry.name,
|
|
31
|
+
status: entry.status,
|
|
32
|
+
shell: typeof entry.shell === 'string' ? entry.shell : null,
|
|
33
|
+
checkedAt: typeof entry.checkedAt === 'string' ? entry.checkedAt : null,
|
|
34
|
+
exitCode: typeof entry.exitCode === 'number' ? entry.exitCode : null,
|
|
35
|
+
})),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
18
38
|
function envTools() {
|
|
19
39
|
return [
|
|
20
40
|
{
|
|
@@ -22,11 +42,12 @@ function envTools() {
|
|
|
22
42
|
title: 'Project worktree environment',
|
|
23
43
|
description: 'Read, scan, and configure the per-project environment variable NAME allowlist used for rail jobs and isolated loop worktrees. ' +
|
|
24
44
|
'Values are never read or stored by this tool; runs read them from the server environment and can recover configured names from the login shell when the desktop process is missing them. scan inspects safe repo config files such as package.json, .npmrc, .yarnrc.yml and pnpm-workspace.yaml for ${VAR} / process.env.VAR references and private scoped package auth hints. ' +
|
|
25
|
-
'
|
|
45
|
+
'Each configured name reports a value-free resolution status: inherited, recovered (from the login shell), not-defined, probe-timeout or probe-failed. ' +
|
|
46
|
+
'Actions: get (read current names and their statuses), status (statuses only), recheck (probe the login shell now, then return statuses), scan (discover candidates), set (write names), auto_configure (scan then merge discovered names into settings).',
|
|
26
47
|
hintTier: 'read',
|
|
27
48
|
tier: (args) => (args.action === 'set' || args.action === 'auto_configure' ? 'write' : 'read'),
|
|
28
49
|
inputSchema: {
|
|
29
|
-
action: zod_1.z.enum(['get', 'scan', 'set', 'auto_configure']).describe('Operation'),
|
|
50
|
+
action: zod_1.z.enum(['get', 'status', 'recheck', 'scan', 'set', 'auto_configure']).describe('Operation'),
|
|
30
51
|
projectId: zod_1.z.string().optional().describe('Project id (defaults to the active project)'),
|
|
31
52
|
names: zod_1.z.array(zod_1.z.string()).optional().describe('set: env var names to configure'),
|
|
32
53
|
merge: zod_1.z.boolean().optional().describe('set: merge with existing names instead of replacing them (default true)'),
|
|
@@ -36,7 +57,21 @@ function envTools() {
|
|
|
36
57
|
const base = (0, types_1.projectPath)(ctx, args.projectId);
|
|
37
58
|
if (action === 'get') {
|
|
38
59
|
const settings = await (0, types_1.apiCall)(ctx, 'GET', `${base}/settings`);
|
|
39
|
-
|
|
60
|
+
const names = Array.isArray(settings.worktreeEnvPassthrough) ? settings.worktreeEnvPassthrough : [];
|
|
61
|
+
// Statuses are best-effort: an older server without the route still answers names.
|
|
62
|
+
try {
|
|
63
|
+
const { statuses } = sanitizeStatus(await (0, types_1.apiCall)(ctx, 'GET', `${base}/env-passthrough/status`));
|
|
64
|
+
return { names, statuses };
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return { names };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (action === 'status') {
|
|
71
|
+
return sanitizeStatus(await (0, types_1.apiCall)(ctx, 'GET', `${base}/env-passthrough/status`));
|
|
72
|
+
}
|
|
73
|
+
if (action === 'recheck') {
|
|
74
|
+
return sanitizeStatus(await (0, types_1.apiCall)(ctx, 'POST', `${base}/env-passthrough/recheck`));
|
|
40
75
|
}
|
|
41
76
|
if (action === 'scan') {
|
|
42
77
|
const project = (0, types_1.requireProject)(ctx, args.projectId).project;
|
|
@@ -2,18 +2,30 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.missionTools = missionTools;
|
|
4
4
|
const zod_1 = require("zod");
|
|
5
|
+
const agent_chat_registry_1 = require("../../modules/missions/runtime/agent-chat-registry");
|
|
5
6
|
function missionTools() {
|
|
6
7
|
return [{
|
|
7
8
|
name: 'specrails_mission',
|
|
8
9
|
title: 'Mission user updates',
|
|
9
|
-
description: 'First-party mission only: acknowledge authenticated user updates received during the current running turn. For MCP mission_user_updates, read every block, replan, and acknowledge_updates with the exact latest revision in a separate call before invoking other tools. For the initial Mission input ID or native user messages carrying queueId, acknowledge_inputs with those inputIds only after reading them; this records a read receipt and does not release the MCP revision gate. Does not execute, retry or undo any action and cannot change permissions, provider or project pin.'
|
|
10
|
+
description: 'First-party mission only: acknowledge authenticated user updates received during the current running turn. For MCP mission_user_updates, read every block, replan, and acknowledge_updates with the exact latest revision in a separate call before invoking other tools. For the initial Mission input ID or native user messages carrying queueId, acknowledge_inputs with those inputIds only after reading them; this records a read receipt and does not release the MCP revision gate. Does not execute, retry or undo any action and cannot change permissions, provider or project pin. ' +
|
|
11
|
+
'When the mission says Specrails launches its sub-agents: subagent_start (description, instructions, optional contextTurns) delegates a task to a sub-agent on the configured provider; subagent_wait returns finished results (results you never wait for arrive later as a message); subagent_list shows them; subagent_stop stops them. These actions are refused in missions whose agent launches its own sub-agents.',
|
|
12
|
+
// Delegation is gated by the project's "Allow sub-agents" / "Run sub-agents with" settings, like native sub-agents.
|
|
10
13
|
tier: 'read',
|
|
11
14
|
inputSchema: {
|
|
12
|
-
action: zod_1.z.enum(['acknowledge_updates', 'acknowledge_inputs']),
|
|
15
|
+
action: zod_1.z.enum(['acknowledge_updates', 'acknowledge_inputs', 'subagent_start', 'subagent_wait', 'subagent_list', 'subagent_stop']),
|
|
16
|
+
description: zod_1.z.string().min(1).max(200).optional().describe('subagent_start: a short name for the task, shown to the user'),
|
|
17
|
+
instructions: zod_1.z.string().min(1).max(64 * 1024).optional().describe('subagent_start: the complete, self-contained task for the sub-agent'),
|
|
18
|
+
agentType: zod_1.z.string().min(1).max(100).optional().describe('subagent_start: optional label for the kind of work (e.g. "review")'),
|
|
19
|
+
contextTurns: zod_1.z.number().int().min(0).max(10).optional().describe('subagent_start: how many recent turns of this conversation to hand over'),
|
|
20
|
+
subagentIds: zod_1.z.array(zod_1.z.string().min(1).max(200)).max(50).optional().describe('subagent_wait / subagent_stop: limit to these sub-agents (default: all of this mission)'),
|
|
21
|
+
timeoutSeconds: zod_1.z.number().int().min(0).max(300).optional().describe('subagent_wait: how long to wait (default 60, max 300); call again to keep waiting'),
|
|
13
22
|
revision: zod_1.z.number().int().positive().max(Number.MAX_SAFE_INTEGER).optional().describe('Required for acknowledge_updates: exact latest revision delivered in mission_user_updates. Never guess; an older acknowledgement cannot release newer messages.'),
|
|
14
23
|
inputIds: zod_1.z.array(zod_1.z.string().min(1).max(200)).min(1).max(50).optional().describe('Required for acknowledge_inputs: exact initial Mission input ID or queueId values from native user messages you have read in this invocation.'),
|
|
15
24
|
},
|
|
16
25
|
handler: (ctx, args) => {
|
|
26
|
+
// Delegation actions are async; the acknowledge actions stay synchronous (callers rely on sync throws).
|
|
27
|
+
if (typeof args.action === 'string' && args.action.startsWith('subagent_'))
|
|
28
|
+
return delegationAction(ctx, args);
|
|
17
29
|
if (args.action === 'acknowledge_inputs') {
|
|
18
30
|
if (!ctx.firstPartyAgent || !ctx.acknowledgeAgentInputsRead)
|
|
19
31
|
throw new Error('This action requires an active first-party mission turn.');
|
|
@@ -27,3 +39,34 @@ function missionTools() {
|
|
|
27
39
|
},
|
|
28
40
|
}];
|
|
29
41
|
}
|
|
42
|
+
async function delegationAction(ctx, args) {
|
|
43
|
+
if (!ctx.firstPartyAgent || !ctx.originConversationId)
|
|
44
|
+
throw new Error('This action requires an active first-party mission turn.');
|
|
45
|
+
const manager = (0, agent_chat_registry_1.getAgentChatManager)();
|
|
46
|
+
if (!manager)
|
|
47
|
+
throw new Error('Missions are not available.');
|
|
48
|
+
const conversationId = ctx.originConversationId;
|
|
49
|
+
const ids = Array.isArray(args.subagentIds) ? args.subagentIds : undefined;
|
|
50
|
+
switch (args.action) {
|
|
51
|
+
case 'subagent_start': {
|
|
52
|
+
if (typeof args.description !== 'string' || typeof args.instructions !== 'string')
|
|
53
|
+
throw new Error('subagent_start needs a description and instructions.');
|
|
54
|
+
return manager.delegateSubagent(conversationId, {
|
|
55
|
+
description: args.description, prompt: args.instructions,
|
|
56
|
+
...(typeof args.agentType === 'string' ? { agentType: args.agentType } : {}),
|
|
57
|
+
...(typeof args.contextTurns === 'number' ? { contextTurns: args.contextTurns } : {}),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
case 'subagent_wait':
|
|
61
|
+
return manager.waitSubagents(conversationId, ids, (typeof args.timeoutSeconds === 'number' ? args.timeoutSeconds : 60) * 1000);
|
|
62
|
+
case 'subagent_stop':
|
|
63
|
+
return { stopped: await manager.stopSubagents(conversationId, ids) };
|
|
64
|
+
case 'subagent_list':
|
|
65
|
+
return {
|
|
66
|
+
subagents: (manager.sessionState(conversationId)?.subagents ?? [])
|
|
67
|
+
.filter((node) => node.delegated)
|
|
68
|
+
.map((node) => ({ subagentId: node.subagentId, description: node.description, phase: node.phase, provider: node.delegated.driver, model: node.delegated.model, result: node.resultSummary })),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
throw new Error(`Unknown action: ${String(args.action)}`);
|
|
72
|
+
}
|
|
@@ -57,7 +57,7 @@ function specsTools() {
|
|
|
57
57
|
'assignee, prerequisites, metadata. THE canonical way to persist a spec refined in conversation — do NOT route ' +
|
|
58
58
|
'a finished spec through create/generate, which re-generate the content with AI and lossily rewrite it. ' +
|
|
59
59
|
'Agent-authored inserts (use b) are enriched with a Contract Layer BY DEFAULT via one short background AI pass ' +
|
|
60
|
-
'— pass contractRefine: false
|
|
60
|
+
'— never ask the user about it; pass contractRefine: false only when they asked for no contract layer unprompted), ' +
|
|
61
61
|
'generate (same as create but takes a pre-formed idea string; both accept contextScope/attachments/createLocal), ' +
|
|
62
62
|
'ai_edit (ai-spawn, AI-edit a ticket title+description), ' +
|
|
63
63
|
'cancel_ai_edit (abort an in-flight ai-edit), contract_refine (ai-spawn, append/re-fire a Contract Layer — Claude or Codex; ' +
|
|
@@ -172,7 +172,7 @@ function specsTools() {
|
|
|
172
172
|
contractRefine: zod_1.z
|
|
173
173
|
.boolean()
|
|
174
174
|
.optional()
|
|
175
|
-
.describe('create/generate/commit_draft: enrich the spec with a Contract Layer post-persist (Claude or Codex). DEFAULTS TO TRUE — pass false
|
|
175
|
+
.describe('create/generate/commit_draft: enrich the spec with a Contract Layer post-persist (Claude or Codex). DEFAULTS TO TRUE and is never asked about — pass false only when the user asked for no contract layer unprompted'),
|
|
176
176
|
attachmentIds: zod_1.z.array(zod_1.z.string()).optional().describe('create/generate/ai_edit: attachment ids (create/generate require pendingSpecId)'),
|
|
177
177
|
// ── ai_edit ──────────────────────────────────────────────────────
|
|
178
178
|
instructions: zod_1.z.string().optional().describe('ai_edit: natural-language editing instructions (required)'),
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
// Spec: openspec/changes/add-multi-provider-support/specs/project-spending/spec.md
|
|
22
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
23
|
exports.finaliseInvocationResult = finaliseInvocationResult;
|
|
24
|
+
exports.finaliseNormalisedResult = finaliseNormalisedResult;
|
|
24
25
|
exports.normaliseResultEvent = normaliseResultEvent;
|
|
25
26
|
const pricing_1 = require("./pricing");
|
|
26
27
|
const local_adapter_1 = require("../../../providers/local-adapter");
|
|
@@ -30,7 +31,14 @@ const local_adapter_1 = require("../../../providers/local-adapter");
|
|
|
30
31
|
* pricing-table fallback when the adapter does not report `total_cost_usd`.
|
|
31
32
|
*/
|
|
32
33
|
function finaliseInvocationResult(adapter, events, opts = {}) {
|
|
33
|
-
|
|
34
|
+
return finaliseNormalisedResult(adapter, adapter.extractResult(events), opts);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Same finalisation for a result that is already normalised (e.g. a turn
|
|
38
|
+
* reported by Core's session runtime): model/duration stamping and the
|
|
39
|
+
* native-cost-absent estimation rules, with identical null semantics.
|
|
40
|
+
*/
|
|
41
|
+
function finaliseNormalisedResult(adapter, result, opts = {}) {
|
|
34
42
|
const cloned = { ...result };
|
|
35
43
|
// Stamp the model from the caller when the adapter did not derive one. The
|
|
36
44
|
// adapter contract permits leaving `model` undefined; the manager knows the
|
|
@@ -373,9 +373,34 @@ class AgentRuntimeControls {
|
|
|
373
373
|
this.statusCache.delete(runId);
|
|
374
374
|
}
|
|
375
375
|
}
|
|
376
|
+
/** The error a run logged when it failed before any Core step ran; null otherwise. */
|
|
377
|
+
launchFailure(runId) {
|
|
378
|
+
const run = this.ctx.db.prepare('SELECT final_outcome, iteration_count, core_revision FROM loop_runs WHERE id = ?').get(runId);
|
|
379
|
+
if (!run || run.final_outcome !== 'failed' || run.iteration_count || run.core_revision != null)
|
|
380
|
+
return null;
|
|
381
|
+
const rows = this.ctx.db.prepare("SELECT payload FROM events WHERE job_id = ? AND event_type = 'log' ORDER BY seq DESC LIMIT 20").all(runId);
|
|
382
|
+
for (const row of rows) {
|
|
383
|
+
try {
|
|
384
|
+
const line = JSON.parse(row.payload).line;
|
|
385
|
+
if (typeof line === 'string' && /^error:/i.test(line.trim()))
|
|
386
|
+
return line.trim().replace(/^error:\s*/i, '').slice(0, 2000);
|
|
387
|
+
}
|
|
388
|
+
catch { /* not a log line */ }
|
|
389
|
+
}
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
376
392
|
/** Read-only recovery assessment. canResume is admission, not a repair claim. */
|
|
377
393
|
async diagnose(runId) {
|
|
378
394
|
const summary = await this.summary(runId);
|
|
395
|
+
// Rejected before Core started (e.g. an unsupported model/effort): no runtime
|
|
396
|
+
// scope was ever written, so the launch error is the whole diagnosis.
|
|
397
|
+
const launchError = summary.status === 'unavailable' ? this.launchFailure(runId) : null;
|
|
398
|
+
if (launchError)
|
|
399
|
+
return {
|
|
400
|
+
runId, summary: { ...summary, error: launchError }, recommendation: 'fix_launch_precondition',
|
|
401
|
+
reason: 'The run was rejected before Core started, so there is no runtime state or evidence. Fix the reported precondition (or change the launch settings) before launching again; Resume is not possible.',
|
|
402
|
+
launchError, repeatFailureCount: null, recentFailures: [],
|
|
403
|
+
};
|
|
379
404
|
if (summary.historical || summary.status === 'unavailable')
|
|
380
405
|
return {
|
|
381
406
|
runId, summary, recommendation: 'inspect_saved_evidence',
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CoreHostLauncher = void 0;
|
|
4
|
+
const node_child_process_1 = require("node:child_process");
|
|
5
|
+
const agent_runtime_loader_1 = require("../../agent-runtime/runtime/agent-runtime-loader");
|
|
6
|
+
const core_node_runtime_1 = require("../../../core-node-runtime");
|
|
7
|
+
const win_spawn_1 = require("../../../util/win-spawn");
|
|
8
|
+
const errors_1 = require("../domain/errors");
|
|
9
|
+
const protocol_1 = require("../domain/protocol");
|
|
10
|
+
const rpc_client_1 = require("./rpc-client");
|
|
11
|
+
/**
|
|
12
|
+
* Launches `node <core>/cli.js host --stdio --scope <scope>` through the same
|
|
13
|
+
* Core and Node resolution as implementation runs (bundled Node under pkg,
|
|
14
|
+
* Windows-safe environment), then performs the protocol handshake.
|
|
15
|
+
*/
|
|
16
|
+
class CoreHostLauncher {
|
|
17
|
+
options;
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
this.options = options;
|
|
20
|
+
}
|
|
21
|
+
async launch(scope) {
|
|
22
|
+
const cli = (this.options.cli ?? agent_runtime_loader_1.findCoreAgentRuntimeCli)();
|
|
23
|
+
if (!cli)
|
|
24
|
+
throw new errors_1.SessionRequestError('The selected Core runtime does not include the agent runtime CLI', { code: 'driver_unavailable', retryable: false });
|
|
25
|
+
const env = (0, win_spawn_1.windowsSpawnEnv)(this.options.env ?? process.env);
|
|
26
|
+
const args = [cli, 'host', '--stdio', '--scope', scope];
|
|
27
|
+
const command = (this.options.node ?? core_node_runtime_1.resolveCoreNodeRuntime)();
|
|
28
|
+
const child = (this.options.spawnProcess ?? ((cmd, argv, opts) => (0, node_child_process_1.spawn)(cmd, argv, { env: opts.env, shell: false, windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'] })))(command, args, { env });
|
|
29
|
+
if (!child.stdin || !child.stdout)
|
|
30
|
+
throw new errors_1.SessionRequestError('Session host has no stdio pipes', { code: 'internal', retryable: true });
|
|
31
|
+
let stderr = '';
|
|
32
|
+
child.stderr?.on('data', (chunk) => {
|
|
33
|
+
const text = chunk.toString('utf8');
|
|
34
|
+
stderr = (stderr + text).slice(-8_192);
|
|
35
|
+
this.options.onStderr?.(scope, text);
|
|
36
|
+
});
|
|
37
|
+
const exited = new Promise((resolve) => {
|
|
38
|
+
child.once('exit', (code) => resolve(code));
|
|
39
|
+
child.once('error', () => resolve(null));
|
|
40
|
+
});
|
|
41
|
+
const grace = this.options.shutdownGraceMs ?? 10_000;
|
|
42
|
+
const client = new rpc_client_1.StdioSessionHostClient({ stdin: child.stdin, stdout: child.stdout }, {
|
|
43
|
+
...(this.options.requestTimeoutMs !== undefined ? { timeoutMs: this.options.requestTimeoutMs } : {}),
|
|
44
|
+
onClose: async () => {
|
|
45
|
+
// Ask for a graceful stop (interruptions recorded, lease released), then make sure the tree is gone.
|
|
46
|
+
await Promise.race([client.request('host.shutdown', { graceMs: grace }).catch(() => undefined), exited]);
|
|
47
|
+
const forced = setTimeout(() => { if (child.pid)
|
|
48
|
+
(0, win_spawn_1.treeKillSafe)(child.pid, 'SIGKILL'); }, grace);
|
|
49
|
+
await exited;
|
|
50
|
+
clearTimeout(forced);
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
void exited.then((code) => client.markClosed(`Session host exited (code ${code ?? 'none'})${stderr ? `: ${stderr.trim().split('\n').at(-1)}` : ''}`));
|
|
54
|
+
let initialize;
|
|
55
|
+
try {
|
|
56
|
+
initialize = await client.request('initialize', {
|
|
57
|
+
protocolVersions: [...protocol_1.SUPPORTED_PROTOCOL_VERSIONS],
|
|
58
|
+
host: this.options.host ?? { name: 'specrails-desktop', version: 'dev' },
|
|
59
|
+
scope,
|
|
60
|
+
});
|
|
61
|
+
if (initialize.capabilities?.sessions !== 1)
|
|
62
|
+
throw new errors_1.SessionRequestError('Core session host does not advertise sessions', { code: 'protocol_mismatch', retryable: false });
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (child.pid)
|
|
66
|
+
(0, win_spawn_1.treeKillSafe)(child.pid, 'SIGKILL');
|
|
67
|
+
client.markClosed('Session host failed to initialize');
|
|
68
|
+
throw error;
|
|
69
|
+
}
|
|
70
|
+
return Object.assign(client, { initialize, pid: child.pid });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
exports.CoreHostLauncher = CoreHostLauncher;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createSessionHostsRouter = createSessionHostsRouter;
|
|
4
|
+
const express_1 = require("express");
|
|
5
|
+
const SCOPE_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
6
|
+
/**
|
|
7
|
+
* Inbound adapter: Core session host status per scope and manual retry of a
|
|
8
|
+
* degraded scope (after the cause, e.g. another Desktop instance, is gone).
|
|
9
|
+
*/
|
|
10
|
+
function createSessionHostsRouter(control) {
|
|
11
|
+
const router = (0, express_1.Router)();
|
|
12
|
+
router.get('/', (_req, res) => {
|
|
13
|
+
res.json({ hosts: control.hosts() });
|
|
14
|
+
});
|
|
15
|
+
router.post('/:scope/retry', async (req, res) => {
|
|
16
|
+
const scope = String(req.params.scope);
|
|
17
|
+
if (!SCOPE_RE.test(scope)) {
|
|
18
|
+
res.status(400).json({ error: 'Invalid scope' });
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
await control.retry(scope);
|
|
23
|
+
res.json({ host: control.hosts().find((host) => host.scope === scope) ?? { scope, status: 'absent', detail: null, code: null } });
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
// The retry ran; the scope reports why it is still unavailable.
|
|
27
|
+
res.status(502).json({ error: error instanceof Error ? error.message : 'Retry failed', host: control.hosts().find((host) => host.scope === scope) ?? null });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
return router;
|
|
31
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.StdioSessionHostClient = exports.MAX_RESPONSE_BYTES = void 0;
|
|
4
|
+
const node_string_decoder_1 = require("node:string_decoder");
|
|
5
|
+
const errors_1 = require("../domain/errors");
|
|
6
|
+
/** Longest accepted response line from the host. */
|
|
7
|
+
exports.MAX_RESPONSE_BYTES = 16 * 1024 * 1024;
|
|
8
|
+
/**
|
|
9
|
+
* JSON-RPC 2.0 client for the Core session host over NDJSON stdio.
|
|
10
|
+
* Requests time out; every pending request is rejected when the connection
|
|
11
|
+
* ends; notifications fan out to subscribers (session.event, session.lagged,
|
|
12
|
+
* host.leaseLost).
|
|
13
|
+
*/
|
|
14
|
+
class StdioSessionHostClient {
|
|
15
|
+
transport;
|
|
16
|
+
options;
|
|
17
|
+
nextId = 1;
|
|
18
|
+
pending = new Map();
|
|
19
|
+
eventListeners = new Set();
|
|
20
|
+
lagListeners = new Set();
|
|
21
|
+
closeListeners = new Set();
|
|
22
|
+
closedReason = null;
|
|
23
|
+
closedCode = null;
|
|
24
|
+
constructor(transport, options = {}) {
|
|
25
|
+
this.transport = transport;
|
|
26
|
+
this.options = options;
|
|
27
|
+
const decoder = new node_string_decoder_1.StringDecoder('utf8');
|
|
28
|
+
let buffer = '';
|
|
29
|
+
transport.stdout.on('data', (chunk) => {
|
|
30
|
+
buffer += typeof chunk === 'string' ? chunk : decoder.write(chunk);
|
|
31
|
+
for (;;) {
|
|
32
|
+
const newline = buffer.indexOf('\n');
|
|
33
|
+
if (newline < 0)
|
|
34
|
+
break;
|
|
35
|
+
const line = buffer.slice(0, newline).trim();
|
|
36
|
+
buffer = buffer.slice(newline + 1);
|
|
37
|
+
if (line)
|
|
38
|
+
this.receive(line);
|
|
39
|
+
}
|
|
40
|
+
if (Buffer.byteLength(buffer) > exports.MAX_RESPONSE_BYTES)
|
|
41
|
+
this.markClosed('Session host sent an oversized frame');
|
|
42
|
+
});
|
|
43
|
+
transport.stdout.on('end', () => this.markClosed('Session host closed its output'));
|
|
44
|
+
transport.stdin.on('error', () => this.markClosed('Session host input closed'));
|
|
45
|
+
}
|
|
46
|
+
get closed() {
|
|
47
|
+
return this.closedReason !== null;
|
|
48
|
+
}
|
|
49
|
+
request(method, params = {}) {
|
|
50
|
+
if (this.closedReason)
|
|
51
|
+
return Promise.reject(new errors_1.SessionRequestError(this.closedReason, this.closedCode ? { code: this.closedCode, retryable: false } : { code: 'host_unavailable', retryable: true }));
|
|
52
|
+
const id = this.nextId++;
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
const timer = setTimeout(() => {
|
|
55
|
+
this.pending.delete(id);
|
|
56
|
+
reject(new errors_1.SessionRequestError(`Session host did not answer ${method} in time`, { code: 'host_timeout', retryable: true }));
|
|
57
|
+
}, this.options.timeoutMs ?? 60_000);
|
|
58
|
+
timer.unref?.();
|
|
59
|
+
this.pending.set(id, { method, resolve: resolve, reject, timer });
|
|
60
|
+
this.transport.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', id, method, params })}\n`);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
onEvent(listener) {
|
|
64
|
+
this.eventListeners.add(listener);
|
|
65
|
+
return () => { this.eventListeners.delete(listener); };
|
|
66
|
+
}
|
|
67
|
+
onLagged(listener) {
|
|
68
|
+
this.lagListeners.add(listener);
|
|
69
|
+
return () => { this.lagListeners.delete(listener); };
|
|
70
|
+
}
|
|
71
|
+
onClose(listener) {
|
|
72
|
+
if (this.closedReason) {
|
|
73
|
+
listener(this.closedReason, this.closedCode ?? undefined);
|
|
74
|
+
return () => { };
|
|
75
|
+
}
|
|
76
|
+
this.closeListeners.add(listener);
|
|
77
|
+
return () => { this.closeListeners.delete(listener); };
|
|
78
|
+
}
|
|
79
|
+
async close() {
|
|
80
|
+
if (this.closedReason)
|
|
81
|
+
return;
|
|
82
|
+
await this.options.onClose?.();
|
|
83
|
+
this.markClosed('Session host closed');
|
|
84
|
+
}
|
|
85
|
+
/** The process ended (called by the launcher). A known `code` (e.g. `journal_locked`) travels to pending requests and close listeners. */
|
|
86
|
+
markClosed(reason, code) {
|
|
87
|
+
if (this.closedReason)
|
|
88
|
+
return;
|
|
89
|
+
this.closedReason = reason;
|
|
90
|
+
this.closedCode = code ?? null;
|
|
91
|
+
for (const [id, request] of this.pending) {
|
|
92
|
+
clearTimeout(request.timer);
|
|
93
|
+
request.reject(new errors_1.SessionRequestError(reason, code ? { code, retryable: false } : { code: 'host_unavailable', retryable: true }));
|
|
94
|
+
this.pending.delete(id);
|
|
95
|
+
}
|
|
96
|
+
for (const listener of this.closeListeners)
|
|
97
|
+
listener(reason, code);
|
|
98
|
+
this.closeListeners.clear();
|
|
99
|
+
}
|
|
100
|
+
receive(line) {
|
|
101
|
+
let message;
|
|
102
|
+
try {
|
|
103
|
+
message = JSON.parse(line);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (typeof message.method === 'string') {
|
|
109
|
+
const params = (message.params ?? {});
|
|
110
|
+
if (message.method === 'session.event') {
|
|
111
|
+
const envelope = { sessionId: String(params.sessionId), seq: Number(params.seq), event: params.event };
|
|
112
|
+
for (const listener of this.eventListeners)
|
|
113
|
+
listener(envelope);
|
|
114
|
+
}
|
|
115
|
+
else if (message.method === 'session.lagged') {
|
|
116
|
+
for (const listener of this.lagListeners)
|
|
117
|
+
listener(String(params.sessionId), Number(params.deliveredSeq));
|
|
118
|
+
}
|
|
119
|
+
else if (message.method === 'host.leaseLost') {
|
|
120
|
+
this.markClosed('Another session host took over this scope', 'journal_locked');
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
// The host could not start and printed its fatal result (e.g. another host owns the journal).
|
|
125
|
+
if (message.type === 'runtime-result' && message.status === 'failed') {
|
|
126
|
+
const error = message.error;
|
|
127
|
+
const detail = typeof error === 'object' && error !== null ? error : { message: error };
|
|
128
|
+
this.markClosed(typeof detail.message === 'string' ? detail.message : 'The session host failed to start', typeof detail.code === 'string' ? detail.code : undefined);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (typeof message.id !== 'number')
|
|
132
|
+
return;
|
|
133
|
+
const request = this.pending.get(message.id);
|
|
134
|
+
if (!request)
|
|
135
|
+
return;
|
|
136
|
+
this.pending.delete(message.id);
|
|
137
|
+
clearTimeout(request.timer);
|
|
138
|
+
if (message.error && typeof message.error === 'object') {
|
|
139
|
+
const error = message.error;
|
|
140
|
+
request.reject(new errors_1.SessionRequestError(error.message ?? `Session host rejected ${request.method}`, error.data ?? { code: 'internal', retryable: false }, error.code));
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
request.resolve(message.result ?? {});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
exports.StdioSessionHostClient = StdioSessionHostClient;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SessionEventPump = void 0;
|
|
4
|
+
const projection_1 = require("../domain/projection");
|
|
5
|
+
const PAGE = 500;
|
|
6
|
+
/**
|
|
7
|
+
* Applies one Core session's committed events to a surface sink exactly once
|
|
8
|
+
* and in order. Duplicates are ignored, gaps are filled from the host's
|
|
9
|
+
* journal (`session.events`), and `attach` rebuilds the in-memory projection
|
|
10
|
+
* by replaying up to the sink's cursor without re-applying anything.
|
|
11
|
+
*/
|
|
12
|
+
class SessionEventPump {
|
|
13
|
+
sessionId;
|
|
14
|
+
client;
|
|
15
|
+
sink;
|
|
16
|
+
listener;
|
|
17
|
+
state;
|
|
18
|
+
chain = Promise.resolve();
|
|
19
|
+
detached = false;
|
|
20
|
+
constructor(sessionId, client, sink, listener = () => { }) {
|
|
21
|
+
this.sessionId = sessionId;
|
|
22
|
+
this.client = client;
|
|
23
|
+
this.sink = sink;
|
|
24
|
+
this.listener = listener;
|
|
25
|
+
this.state = (0, projection_1.initialProjection)(sessionId);
|
|
26
|
+
}
|
|
27
|
+
get projection() {
|
|
28
|
+
return this.state;
|
|
29
|
+
}
|
|
30
|
+
/** Rebuild state up to the sink cursor, then apply everything after it. */
|
|
31
|
+
attach() {
|
|
32
|
+
return this.enqueue(async () => {
|
|
33
|
+
const cursor = this.sink.cursor(this.sessionId);
|
|
34
|
+
this.state = (0, projection_1.initialProjection)(this.sessionId);
|
|
35
|
+
await this.fetchFrom(0, cursor);
|
|
36
|
+
await this.fetchFrom(this.state.lastSeq);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/** A live notification. */
|
|
40
|
+
push(envelope) {
|
|
41
|
+
if (envelope.sessionId !== this.sessionId)
|
|
42
|
+
return Promise.resolve();
|
|
43
|
+
return this.enqueue(async () => {
|
|
44
|
+
const result = (0, projection_1.reduceEnvelope)(this.state, envelope);
|
|
45
|
+
if (result.status === 'duplicate')
|
|
46
|
+
return;
|
|
47
|
+
if (result.status === 'gap') {
|
|
48
|
+
await this.fetchFrom(this.state.lastSeq);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
this.commit(envelope.seq, result.state, result.ops);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
/** The host dropped queued notifications; catch up from the journal. */
|
|
55
|
+
catchUp() {
|
|
56
|
+
return this.enqueue(() => this.fetchFrom(this.state.lastSeq));
|
|
57
|
+
}
|
|
58
|
+
detach() {
|
|
59
|
+
this.detached = true;
|
|
60
|
+
}
|
|
61
|
+
/** Page through the journal; events up to `rebuildUntil` only rebuild state. */
|
|
62
|
+
async fetchFrom(afterSeq, rebuildUntil = 0) {
|
|
63
|
+
let cursor = afterSeq;
|
|
64
|
+
for (;;) {
|
|
65
|
+
if (this.detached)
|
|
66
|
+
return;
|
|
67
|
+
if (rebuildUntil > 0 && cursor >= rebuildUntil)
|
|
68
|
+
return;
|
|
69
|
+
const page = await this.client.request('session.events', { sessionId: this.sessionId, afterSeq: cursor, limit: PAGE });
|
|
70
|
+
for (const envelope of page.events) {
|
|
71
|
+
const result = (0, projection_1.reduceEnvelope)(this.state, envelope);
|
|
72
|
+
if (result.status !== 'applied')
|
|
73
|
+
continue;
|
|
74
|
+
if (envelope.seq <= rebuildUntil)
|
|
75
|
+
this.state = result.state;
|
|
76
|
+
else
|
|
77
|
+
this.commit(envelope.seq, result.state, result.ops);
|
|
78
|
+
cursor = envelope.seq;
|
|
79
|
+
if (rebuildUntil > 0 && cursor >= rebuildUntil)
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (!page.hasMore || page.events.length === 0)
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
commit(seq, state, ops) {
|
|
87
|
+
// The sink owns the transaction (ops + cursor); state advances only after it committed.
|
|
88
|
+
this.sink.apply(this.sessionId, seq, ops);
|
|
89
|
+
this.state = state;
|
|
90
|
+
if (ops.length > 0)
|
|
91
|
+
this.listener(this.sessionId, ops, state);
|
|
92
|
+
}
|
|
93
|
+
enqueue(task) {
|
|
94
|
+
const run = this.chain.then(task);
|
|
95
|
+
this.chain = run.catch(() => undefined);
|
|
96
|
+
return run;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
exports.SessionEventPump = SessionEventPump;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DEFAULT_CORE_SESSIONS_FLAG = void 0;
|
|
4
|
+
exports.parseCoreSessionsFlag = parseCoreSessionsFlag;
|
|
5
|
+
exports.resolveCoreSessionsAvailability = resolveCoreSessionsAvailability;
|
|
6
|
+
exports.DEFAULT_CORE_SESSIONS_FLAG = 'auto';
|
|
7
|
+
function parseCoreSessionsFlag(raw) {
|
|
8
|
+
const value = raw?.trim().toLowerCase();
|
|
9
|
+
if (value === 'on' || value === '1' || value === 'true')
|
|
10
|
+
return 'on';
|
|
11
|
+
if (value === 'auto')
|
|
12
|
+
return 'auto';
|
|
13
|
+
if (value === 'off' || value === '0' || value === 'false')
|
|
14
|
+
return 'off';
|
|
15
|
+
return exports.DEFAULT_CORE_SESSIONS_FLAG;
|
|
16
|
+
}
|
|
17
|
+
function resolveCoreSessionsAvailability(flag, capabilities, contractCompatible = true) {
|
|
18
|
+
if (flag === 'off')
|
|
19
|
+
return { enabled: false, flag, reason: 'Core sessions are disabled (SPECRAILS_CORE_SESSIONS=off)' };
|
|
20
|
+
if (capabilities?.sessions !== 1)
|
|
21
|
+
return { enabled: false, flag, reason: 'The selected Core does not advertise the sessions capability; using legacy transports' };
|
|
22
|
+
if (!contractCompatible)
|
|
23
|
+
return { enabled: false, flag, reason: 'The selected Core session contract is not compatible with this Desktop; using legacy transports' };
|
|
24
|
+
return { enabled: true, flag, reason: flag === 'on' ? 'Core sessions forced on' : 'Core sessions available' };
|
|
25
|
+
}
|