specrails-desktop 2.44.2 → 2.45.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/README.md +2 -0
- package/client/dist/assets/{ActivityFeedPage-CL1BUXoj.js → ActivityFeedPage-D3SQklu5.js} +1 -1
- package/client/dist/assets/AgentBrowserCapture-DNGxE33d.js +1 -0
- package/client/dist/assets/AgentModeAnalyticsPane-BUvZNoj0.js +2 -0
- package/client/dist/assets/AgentModeCodePane-YW717UvJ.js +2 -0
- package/client/dist/assets/{AgentModeJobsPane-C7ELTPC1.js → AgentModeJobsPane-Cxg_eNmd.js} +1 -1
- package/client/dist/assets/AgentsPage-B_DCByok.js +87 -0
- package/client/dist/assets/{AnalyticsPage-DTsg-N5z.js → AnalyticsPage-BqV6mM2O.js} +1 -1
- package/client/dist/assets/CodePage-PyZKCj1B.js +3 -0
- package/client/dist/assets/{DesktopAnalyticsPage-Y0Z1ryQr.js → DesktopAnalyticsPage-D6WNax3K.js} +1 -1
- package/client/dist/assets/{DocsDialog-KeCOxvGH.js → DocsDialog-68jKUH9f.js} +1 -1
- package/client/dist/assets/{DocsPage-Bt1PgYPO.js → DocsPage-BoGJgRpm.js} +1 -1
- package/client/dist/assets/{ExportDropdown-DczgU4at.js → ExportDropdown-B2C_XLGp.js} +1 -1
- package/client/dist/assets/InteractiveJobComposer-l8q-r5Yy.js +19 -0
- package/client/dist/assets/{JobDetailModal-DKA_DUHe.js → JobDetailModal-C-X2GI9j.js} +1 -1
- package/client/dist/assets/JobDetailPage-C6Jdm7S9.js +1 -0
- package/client/dist/assets/{JobsPage-Bwoe-Fb0.js → JobsPage-DycM6Vgp.js} +1 -1
- package/client/dist/assets/{LoopBuilderPage-CqC4hbXb.js → LoopBuilderPage-D21GxFmL.js} +2 -2
- package/client/dist/assets/{LoopPreviewModal-T0zuFv6y.js → LoopPreviewModal-Bo3fYFxN.js} +1 -1
- package/client/dist/assets/{LoopsPage-DrQGcZv2.js → LoopsPage-BcrBw1cg.js} +1 -1
- package/client/dist/assets/{MinimizedChatsContext-C843Y13h.js → MinimizedChatsContext-Dsv2eHf_.js} +1 -1
- package/client/dist/assets/PluginsPage-BbyTmSLx.js +2 -0
- package/client/dist/assets/ProjectSettingsDialog-B3lEXLv7.js +1 -0
- package/client/dist/assets/{RepositoryDeliveries-bgIHE8ep.js → RepositoryDeliveries-ok_Wo65C.js} +1 -1
- package/client/dist/assets/{RepositoryScopeSelector-CZ_U_R3U.js → RepositoryScopeSelector-DU6QwNaS.js} +1 -1
- package/client/dist/assets/{ReviewPacketPage-DbKO6pT8.js → ReviewPacketPage-BBCeHtgd.js} +1 -1
- package/client/dist/assets/TemplatePreviewModal-CfNmxS6O.js +1 -0
- package/client/dist/assets/{TicketDetailModalContext-BbRA6cb7.js → TicketDetailModalContext-DW3eQ0I7.js} +1 -1
- package/client/dist/assets/{Trans-mjFZpHw1.js → Trans-BHPMWkOw.js} +1 -1
- package/client/dist/assets/agentRuntime-14faDjWM.js +1 -0
- package/client/dist/assets/agentRuntime-BIokojkJ.js +1 -0
- package/client/dist/assets/agentRuntime-Bu4xEcRw.js +1 -0
- package/client/dist/assets/agentRuntime-Cz0ASixB.js +1 -0
- package/client/dist/assets/agentRuntime-DESdGXLV.js +1 -0
- package/client/dist/assets/agentRuntime-DhX5Xy5S.js +1 -0
- package/client/dist/assets/agentRuntime-Dpbh_gLp.js +1 -0
- package/client/dist/assets/agentRuntime-jS2lplFm.js +1 -0
- package/client/dist/assets/{formatDistanceToNow-BIL-HxGy.js → formatDistanceToNow-Bq8iptrG.js} +1 -1
- package/client/dist/assets/{getTimezoneOffsetInMilliseconds-Cs9z2sdO.js → getTimezoneOffsetInMilliseconds-BVFky3l_.js} +1 -1
- package/client/dist/assets/index-CBLYwPw6.css +2 -0
- package/client/dist/assets/index-DOKftX7y.js +76 -0
- package/client/dist/assets/{jira-api-B2BBZQTx.js → jira-api-zrRdZFBB.js} +1 -1
- package/client/dist/assets/{narration-C1Px0-0j.js → narration-BA8Yw5ZL.js} +1 -1
- package/client/dist/assets/{narration-DQ1l_Kq6.js → narration-BuMFUS7Q.js} +1 -1
- package/client/dist/assets/{narration-kwzfglQo.js → narration-Bv_iQQG5.js} +1 -1
- package/client/dist/assets/{narration-B-ynWTpn.js → narration-C6Ost9nL.js} +1 -1
- package/client/dist/assets/{narration-CAXhGbzM.js → narration-CIbjo2pA.js} +1 -1
- package/client/dist/assets/{narration-BxHpmMv_.js → narration-CNJ2-2-a.js} +1 -1
- package/client/dist/assets/{narration-Bj_laZwg.js → narration-OzsPJCXM.js} +1 -1
- package/client/dist/assets/{narration-XISXwqZd.js → narration-zCdgiuSU.js} +1 -1
- package/client/dist/assets/{project-repositories-xSqimlaj.js → project-repositories-CFRaB9UB.js} +1 -1
- package/client/dist/assets/{settings-BLOsrXEw.js → settings-CNiERE-R.js} +1 -1
- package/client/dist/assets/{settings-NSWQjY5B.js → settings-ChOCVjX_.js} +1 -1
- package/client/dist/assets/{settings-C6s9PtRD.js → settings-CrtCs5-B.js} +1 -1
- package/client/dist/assets/{settings-DPjNiUsS.js → settings-DJ2jl3hJ.js} +1 -1
- package/client/dist/assets/{settings-FIqwhTup.js → settings-DVRVybR_.js} +1 -1
- package/client/dist/assets/{settings-CCNZN5Q-.js → settings-DzXZCb1h.js} +1 -1
- package/client/dist/assets/{settings-2Yx_9yJM.js → settings-OOyDrZQo.js} +1 -1
- package/client/dist/assets/{settings-CpKwTew1.js → settings-a7NIODme.js} +1 -1
- package/client/dist/assets/{spending-DXOcv1tn.js → spending-lTZ-bm_y.js} +1 -1
- package/client/dist/assets/{useDesktop-gVGoWWiY.js → useDesktop-Duv5ddAa.js} +1 -1
- package/client/dist/assets/useSharedWebSocket-5mj2SMKn.js +2 -0
- package/client/dist/index.html +17 -16
- package/docs/README.md +1 -0
- package/docs/internals/README.md +2 -0
- package/docs/internals/agent-runtime-framework-evaluation.md +208 -0
- package/docs/internals/programmatic-agent-runtime-validation.md +72 -0
- package/docs/internals/programmatic-agent-runtime.md +129 -0
- package/package.json +1 -1
- package/server/dist/agent-runtime-accounting.js +73 -0
- package/server/dist/agent-runtime-bridge.js +183 -0
- package/server/dist/agent-runtime-controls-router.js +44 -0
- package/server/dist/agent-runtime-controls.js +277 -0
- package/server/dist/agent-runtime-loader.js +85 -0
- package/server/dist/agent-runtime-paths.js +12 -0
- package/server/dist/agent-runtime-settings-router.js +59 -0
- package/server/dist/agent-runtime-settings.js +135 -0
- package/server/dist/agent-runtime-verification-suggestions.js +119 -0
- package/server/dist/core-execution.js +17 -8
- package/server/dist/core-node-runtime.js +12 -0
- package/server/dist/loop-executors.js +42 -2
- package/server/dist/loop-factory.js +1 -1
- package/server/dist/loop-run-manager.js +60 -26
- package/server/dist/project-registry.js +9 -0
- package/server/dist/project-router-settings.js +4 -0
- package/server/dist/rail-isolated-launch.js +18 -0
- package/server/dist/rails-router.js +1 -0
- package/server/dist/schemas/agent-runtime.schema.json +55 -0
- package/client/dist/assets/AgentBrowserCapture-BQem50z4.js +0 -1
- package/client/dist/assets/AgentModeAnalyticsPane-BC_xbT_l.js +0 -2
- package/client/dist/assets/AgentModeCodePane-DK-q3K6Y.js +0 -2
- package/client/dist/assets/AgentsPage-DbLezyI6.js +0 -87
- package/client/dist/assets/CodePage-DwgbeqQ_.js +0 -3
- package/client/dist/assets/InteractiveJobComposer-BcR6xlHU.js +0 -19
- package/client/dist/assets/JobDetailPage-vaOcE-cI.js +0 -1
- package/client/dist/assets/PluginsPage-CTUHX_Lu.js +0 -2
- package/client/dist/assets/ProjectSettingsDialog-DeL8md1s.js +0 -1
- package/client/dist/assets/TemplatePreviewModal-_CgCCV7L.js +0 -1
- package/client/dist/assets/index-Bfj8HvX1.css +0 -2
- package/client/dist/assets/index-UnaJTMIy.js +0 -76
- package/client/dist/assets/useSharedWebSocket-DbGL_YOt.js +0 -2
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RUNTIME_HOST_ENV_KEYS = void 0;
|
|
4
|
+
exports.selectAgentRuntime = selectAgentRuntime;
|
|
5
|
+
exports.runtimeChangeName = runtimeChangeName;
|
|
6
|
+
exports.runAgentRuntimeInvocation = runAgentRuntimeInvocation;
|
|
7
|
+
const node_child_process_1 = require("node:child_process");
|
|
8
|
+
const node_crypto_1 = require("node:crypto");
|
|
9
|
+
const node_fs_1 = require("node:fs");
|
|
10
|
+
const node_path_1 = require("node:path");
|
|
11
|
+
const node_readline_1 = require("node:readline");
|
|
12
|
+
const agent_runtime_loader_1 = require("./agent-runtime-loader");
|
|
13
|
+
const agent_runtime_settings_1 = require("./agent-runtime-settings");
|
|
14
|
+
const core_node_runtime_1 = require("./core-node-runtime");
|
|
15
|
+
const win_spawn_1 = require("./util/win-spawn");
|
|
16
|
+
/** The stored admission wins over configuration changes for an existing run. */
|
|
17
|
+
function selectAgentRuntime(configPath, contextPath) {
|
|
18
|
+
if (contextPath && (0, node_fs_1.existsSync)((0, node_path_1.join)((0, node_path_1.dirname)(contextPath), 'agent-runtime-request.json')))
|
|
19
|
+
return true;
|
|
20
|
+
if (!(0, node_fs_1.existsSync)(configPath))
|
|
21
|
+
return false;
|
|
22
|
+
return (0, agent_runtime_settings_1.validateAgentRuntimeConfig)(JSON.parse((0, node_fs_1.readFileSync)(configPath, 'utf8'))).enabled;
|
|
23
|
+
}
|
|
24
|
+
function runtimeChangeName(runId) {
|
|
25
|
+
return 'runtime-' + (0, node_crypto_1.createHash)('sha256').update(runId).digest('hex').slice(0, 20);
|
|
26
|
+
}
|
|
27
|
+
exports.RUNTIME_HOST_ENV_KEYS = [
|
|
28
|
+
'SPECRAILS_REPO_DIR', 'SPECRAILS_GIT_AUTO', 'SPECRAILS_REPO_MAP_PATH',
|
|
29
|
+
'SPECRAILS_EXECUTION_CONTEXT', 'SPECRAILS_EXECUTION_MANIFEST', 'SPECRAILS_REPOSITORIES',
|
|
30
|
+
'SPECRAILS_WORKSPACE_DIR', 'SPECRAILS_TICKETS_PATH', 'SPECRAILS_STATE_DIR',
|
|
31
|
+
'SPECRAILS_BACKLOG_CONFIG_PATH', 'SPECRAILS_PROFILES_DIR',
|
|
32
|
+
'SPECRAILS_PROFILE_PATH',
|
|
33
|
+
];
|
|
34
|
+
function saveHostContext(options) {
|
|
35
|
+
const file = (0, node_path_1.join)((0, node_path_1.dirname)(options.contextPath), 'desktop-runtime-host.json');
|
|
36
|
+
const serialized = JSON.stringify({ schemaVersion: 1, cwd: (0, node_fs_1.realpathSync)(options.cwd), env: Object.fromEntries(exports.RUNTIME_HOST_ENV_KEYS.filter(key => options.env[key] !== undefined).map(key => [key, options.env[key]])) }, null, 2) + '\n';
|
|
37
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(file), { recursive: true, mode: 0o700 });
|
|
38
|
+
try {
|
|
39
|
+
(0, node_fs_1.writeFileSync)(file, serialized, { flag: 'wx', mode: 0o600 });
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (error.code !== 'EEXIST')
|
|
43
|
+
throw error;
|
|
44
|
+
if ((0, node_fs_1.readFileSync)(file, 'utf8') !== serialized)
|
|
45
|
+
throw new Error('Desktop runtime scope changed; resume using its frozen host context');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Core owns the complete agent workflow in one managed process. The existing
|
|
49
|
+
* rail's cancellation and worktree ownership remain in Desktop. */
|
|
50
|
+
async function runAgentRuntimeInvocation(options) {
|
|
51
|
+
const cli = (0, agent_runtime_loader_1.findCoreAgentRuntimeCli)();
|
|
52
|
+
if (!cli)
|
|
53
|
+
throw new Error('Programmatic agent runtime is enabled but its Core CLI is unavailable. Build or bundle the compatible Core runtime.');
|
|
54
|
+
if (!options.resume && (!options.configPath || !options.change))
|
|
55
|
+
throw new Error('New programmatic runs require configuration and a change name');
|
|
56
|
+
const admittedContext = JSON.parse((0, node_fs_1.readFileSync)(options.contextPath, 'utf8'));
|
|
57
|
+
if (typeof admittedContext.runId !== 'string')
|
|
58
|
+
throw new Error('Core context is missing its run identity');
|
|
59
|
+
if (!options.resume)
|
|
60
|
+
saveHostContext(options);
|
|
61
|
+
const args = [cli, options.resume ? 'resume' : 'run', '--context', options.contextPath];
|
|
62
|
+
if (!options.resume)
|
|
63
|
+
args.push('--config', options.configPath, '--change', options.change);
|
|
64
|
+
for (const [flag, values] of [['approve', options.approve], ['recover', options.recover], ['invalidate', options.invalidate]]) {
|
|
65
|
+
if (values?.length)
|
|
66
|
+
args.push('--' + flag, values.join(','));
|
|
67
|
+
}
|
|
68
|
+
if (options.answer !== undefined) {
|
|
69
|
+
if (!options.resume)
|
|
70
|
+
throw new Error('Answers apply to runtime resume');
|
|
71
|
+
args.push('--answer', options.answer);
|
|
72
|
+
}
|
|
73
|
+
const started = Date.now();
|
|
74
|
+
return new Promise((resolve) => {
|
|
75
|
+
let result;
|
|
76
|
+
let stderr = '', summary = '', invalidProtocol = false, timedOut = false;
|
|
77
|
+
let timer;
|
|
78
|
+
const child = (0, node_child_process_1.spawn)((0, core_node_runtime_1.resolveCoreNodeRuntime)(), args, {
|
|
79
|
+
cwd: options.cwd, env: (0, win_spawn_1.windowsSpawnEnv)(options.env), shell: false, windowsHide: true,
|
|
80
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
81
|
+
});
|
|
82
|
+
// Always leave a terminal result, including synchronous observer failures.
|
|
83
|
+
let observerError;
|
|
84
|
+
try {
|
|
85
|
+
options.onSpawn?.(child);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
observerError = error instanceof Error ? error.message : String(error);
|
|
89
|
+
if (child.pid)
|
|
90
|
+
(0, win_spawn_1.treeKillSafe)(child.pid, 'SIGKILL');
|
|
91
|
+
}
|
|
92
|
+
const observe = (callback) => { try {
|
|
93
|
+
callback?.();
|
|
94
|
+
}
|
|
95
|
+
catch { /* Logging cannot replay a workflow. */ } };
|
|
96
|
+
const lines = (0, node_readline_1.createInterface)({ input: child.stdout });
|
|
97
|
+
lines.on('line', line => {
|
|
98
|
+
if (line.length > 2_000_000) {
|
|
99
|
+
invalidProtocol = true;
|
|
100
|
+
if (child.pid)
|
|
101
|
+
(0, win_spawn_1.treeKillSafe)(child.pid, 'SIGKILL');
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
let event;
|
|
105
|
+
try {
|
|
106
|
+
event = JSON.parse(line);
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
invalidProtocol = true;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (!event || typeof event !== 'object') {
|
|
113
|
+
invalidProtocol = true;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
observe(() => options.onRawLine?.(line));
|
|
117
|
+
if (event.type === 'runtime-result') {
|
|
118
|
+
if (result)
|
|
119
|
+
invalidProtocol = true;
|
|
120
|
+
result = event;
|
|
121
|
+
}
|
|
122
|
+
else if (event.type === 'workflow-event') {
|
|
123
|
+
const payload = event.event;
|
|
124
|
+
if (payload?.type)
|
|
125
|
+
observe(() => options.onLine?.(`[runtime] ${payload.type}${payload.stepId ? ': ' + payload.stepId : ''}${payload.message ? ' — ' + payload.message : ''}\n`));
|
|
126
|
+
}
|
|
127
|
+
else if (event.type === 'agent-event') {
|
|
128
|
+
const payload = event.event;
|
|
129
|
+
const role = typeof event.role === 'string' ? event.role : 'agent';
|
|
130
|
+
if (payload?.kind === 'text' && typeof payload.text === 'string') {
|
|
131
|
+
summary = (summary + payload.text).slice(-32_000);
|
|
132
|
+
observe(() => options.onLine?.(payload.text + '\n'));
|
|
133
|
+
}
|
|
134
|
+
else if (payload?.kind === 'tool-start' && typeof payload.tool === 'string') {
|
|
135
|
+
// Live tool activity keeps a long developer turn from looking hung.
|
|
136
|
+
observe(() => options.onLine?.(`[${role}] ${payload.tool}${payload.detail ? ' ' + payload.detail : ''}\n`));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else if (event.type === 'verification-output' && typeof event.text === 'string') {
|
|
140
|
+
observe(() => options.onLine?.(String(event.text)));
|
|
141
|
+
}
|
|
142
|
+
else if (event.type === 'span') {
|
|
143
|
+
// Trace spans are telemetry; the raw line is already recorded for diagnostics.
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
child.stderr?.on('data', (chunk) => {
|
|
147
|
+
stderr = (stderr + chunk.toString('utf8')).slice(-32_000);
|
|
148
|
+
observe(() => options.onLine?.(chunk.toString('utf8'), 'stderr'));
|
|
149
|
+
});
|
|
150
|
+
child.on('error', error => { stderr = error.message; });
|
|
151
|
+
if (options.timeoutMs && options.timeoutMs > 0) {
|
|
152
|
+
timer = setTimeout(() => {
|
|
153
|
+
timedOut = true;
|
|
154
|
+
if (child.pid)
|
|
155
|
+
(0, win_spawn_1.treeKillSafe)(child.pid, 'SIGKILL');
|
|
156
|
+
}, options.timeoutMs);
|
|
157
|
+
timer.unref?.();
|
|
158
|
+
}
|
|
159
|
+
child.on('close', code => {
|
|
160
|
+
if (timer)
|
|
161
|
+
clearTimeout(timer);
|
|
162
|
+
lines.close();
|
|
163
|
+
const usage = result?.invocationUsage;
|
|
164
|
+
const known = (value) => typeof value === 'number' && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
165
|
+
const cost = known(usage?.costUsd), tokensIn = known(usage?.inputTokens), tokensOut = known(usage?.outputTokens);
|
|
166
|
+
if (result?.status === 'succeeded' && result.runId !== admittedContext.runId)
|
|
167
|
+
invalidProtocol = true;
|
|
168
|
+
const failed = code !== 0 || result?.status !== 'succeeded' || invalidProtocol || timedOut || Boolean(observerError);
|
|
169
|
+
const errorText = observerError ?? (timedOut ? 'Programmatic workflow timed out; inspect its checkpoint before recovery'
|
|
170
|
+
: invalidProtocol ? 'Core returned an invalid runtime event stream'
|
|
171
|
+
: result?.error ?? (result?.status === 'paused' ? (typeof result.pendingQuestion?.question === 'string' && result.pendingQuestion.question.trim()
|
|
172
|
+
? `Workflow awaits an answer in Agent Runtime settings: ${result.pendingQuestion.question.trim().slice(0, 500)}`
|
|
173
|
+
: 'Workflow awaits approval in Agent Runtime settings')
|
|
174
|
+
: failed ? stderr || 'Core exited without a successful programmatic workflow result' : undefined));
|
|
175
|
+
resolve({
|
|
176
|
+
text: summary || (failed ? errorText ?? '' : 'Programmatic implementation verified and archived.'),
|
|
177
|
+
provider: 'agent-runtime', model: 'per-role', failed, errorText,
|
|
178
|
+
cost, tokensIn, tokensOut, tokens: tokensIn === undefined || tokensOut === undefined ? undefined : tokensIn + tokensOut,
|
|
179
|
+
estimated: cost === undefined, durationMs: Date.now() - started,
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.shutdownAgentRuntimeControls = shutdownAgentRuntimeControls;
|
|
4
|
+
exports.registerAgentRuntimeControlRoutes = registerAgentRuntimeControlRoutes;
|
|
5
|
+
const agent_runtime_controls_1 = require("./agent-runtime-controls");
|
|
6
|
+
const controllers = new WeakMap();
|
|
7
|
+
function shutdownAgentRuntimeControls(context) { controllers.get(context)?.shutdown(); }
|
|
8
|
+
function registerAgentRuntimeControlRoutes({ router, ctx }) {
|
|
9
|
+
function controls(request) {
|
|
10
|
+
const context = ctx(request);
|
|
11
|
+
let controller = controllers.get(context);
|
|
12
|
+
if (!controller) {
|
|
13
|
+
controller = new agent_runtime_controls_1.AgentRuntimeControls(context);
|
|
14
|
+
controllers.set(context, controller);
|
|
15
|
+
}
|
|
16
|
+
return controller;
|
|
17
|
+
}
|
|
18
|
+
router.get('/:projectId/agent-runtime/runs', async (req, res) => {
|
|
19
|
+
try {
|
|
20
|
+
res.json({ runs: await controls(req).list() });
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
res.status(500).json({ error: 'runtime_status_failed', message: 'Could not read runtime executions' });
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
router.post('/:projectId/agent-runtime/runs/:runId/resume', async (req, res) => {
|
|
27
|
+
try {
|
|
28
|
+
await controls(req).resume(String(req.params.runId), (0, agent_runtime_controls_1.validateRuntimeResumeInput)(req.body));
|
|
29
|
+
res.status(202).json({ accepted: true });
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
res.status(error instanceof agent_runtime_controls_1.RuntimeControlError ? error.statusCode : 500).json({ error: error instanceof agent_runtime_controls_1.RuntimeControlError ? error.code : 'runtime_resume_failed', message: error instanceof agent_runtime_controls_1.RuntimeControlError ? error.message : 'Could not resume runtime execution' });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
router.post('/:projectId/agent-runtime/runs/:runId/cancel', (req, res) => {
|
|
36
|
+
try {
|
|
37
|
+
controls(req).cancel(String(req.params.runId));
|
|
38
|
+
res.status(202).json({ accepted: true });
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
res.status(error instanceof agent_runtime_controls_1.RuntimeControlError ? error.statusCode : 500).json({ error: error instanceof agent_runtime_controls_1.RuntimeControlError ? error.code : 'runtime_cancel_failed', message: error instanceof agent_runtime_controls_1.RuntimeControlError ? error.message : 'Could not cancel runtime execution' });
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.AgentRuntimeControls = exports.RuntimeControlError = void 0;
|
|
7
|
+
exports.validateRuntimeResumeInput = validateRuntimeResumeInput;
|
|
8
|
+
exports.readAgentRuntimeStatus = readAgentRuntimeStatus;
|
|
9
|
+
const node_child_process_1 = require("node:child_process");
|
|
10
|
+
const node_crypto_1 = require("node:crypto");
|
|
11
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
const node_util_1 = require("node:util");
|
|
14
|
+
const agent_runtime_loader_1 = require("./agent-runtime-loader");
|
|
15
|
+
const agent_runtime_bridge_1 = require("./agent-runtime-bridge");
|
|
16
|
+
const core_node_runtime_1 = require("./core-node-runtime");
|
|
17
|
+
const win_spawn_1 = require("./util/win-spawn");
|
|
18
|
+
const workspace_resolution_1 = require("./workspace-resolution");
|
|
19
|
+
const loop_runs_store_1 = require("./loop-runs-store");
|
|
20
|
+
const multi_repo_execution_store_1 = require("./multi-repo-execution-store");
|
|
21
|
+
const db_1 = require("./db");
|
|
22
|
+
const loop_run_manager_1 = require("./loop-run-manager");
|
|
23
|
+
const SAFE_ID = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
24
|
+
const STEP_IDS = ['architect', 'developer', 'verify', 'reviewer', 'archive'];
|
|
25
|
+
const ANSWER_LIMIT = 20_000;
|
|
26
|
+
class RuntimeControlError extends Error {
|
|
27
|
+
statusCode;
|
|
28
|
+
code;
|
|
29
|
+
constructor(statusCode, code, message) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.statusCode = statusCode;
|
|
32
|
+
this.code = code;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
exports.RuntimeControlError = RuntimeControlError;
|
|
36
|
+
function validateRuntimeResumeInput(input) {
|
|
37
|
+
if (!input || typeof input !== 'object' || Array.isArray(input))
|
|
38
|
+
throw new RuntimeControlError(400, 'invalid_resume_request', 'Resume request must be an object');
|
|
39
|
+
const body = input;
|
|
40
|
+
for (const [key, value] of Object.entries(body)) {
|
|
41
|
+
if (key === 'answer') {
|
|
42
|
+
if (typeof value !== 'string' || !value.trim() || value.length > ANSWER_LIMIT)
|
|
43
|
+
throw new RuntimeControlError(400, 'invalid_resume_request', `An answer must be a nonempty string of at most ${ANSWER_LIMIT} characters`);
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (!['approve', 'recover', 'invalidate'].includes(key) || !Array.isArray(value) || value.length > STEP_IDS.length || !value.every((id) => typeof id === 'string' && STEP_IDS.includes(id)) || new Set(value).size !== value.length)
|
|
47
|
+
throw new RuntimeControlError(400, 'invalid_resume_request', 'Only approve, recover and invalidate arrays of known phase IDs are allowed');
|
|
48
|
+
}
|
|
49
|
+
return body;
|
|
50
|
+
}
|
|
51
|
+
/** A question stays pending until Core records its answer. */
|
|
52
|
+
function openQuestion(state) {
|
|
53
|
+
return state.pendingQuestion && state.pendingQuestion.answeredAt === undefined ? state.pendingQuestion : undefined;
|
|
54
|
+
}
|
|
55
|
+
/** Status is a read-only Core CLI operation. It never invokes a provider. */
|
|
56
|
+
async function readAgentRuntimeStatus(contextPath, cwd, env) {
|
|
57
|
+
const cli = (0, agent_runtime_loader_1.findCoreAgentRuntimeCli)();
|
|
58
|
+
if (!cli)
|
|
59
|
+
throw new RuntimeControlError(503, 'runtime_unavailable', 'Update Core to inspect or resume agent runtime executions');
|
|
60
|
+
const { stdout } = await (0, node_util_1.promisify)(node_child_process_1.execFile)((0, core_node_runtime_1.resolveCoreNodeRuntime)(), [cli, 'status', '--context', contextPath, '--compact'], {
|
|
61
|
+
cwd, env: (0, win_spawn_1.windowsSpawnEnv)(env), windowsHide: true, timeout: 15000, maxBuffer: 4 * 1024 * 1024, encoding: 'utf8',
|
|
62
|
+
});
|
|
63
|
+
const result = JSON.parse(stdout);
|
|
64
|
+
if (result.type !== 'runtime-status' || result.state === undefined)
|
|
65
|
+
throw new Error('Core returned an invalid runtime status');
|
|
66
|
+
return result.state;
|
|
67
|
+
}
|
|
68
|
+
/** One controller per ProjectContext; Core's durable lease remains the final
|
|
69
|
+
* cross-process guard. Resume never constructs a new context or worktree. */
|
|
70
|
+
class AgentRuntimeControls {
|
|
71
|
+
ctx;
|
|
72
|
+
dependencies;
|
|
73
|
+
active = new Map();
|
|
74
|
+
errors = new Map();
|
|
75
|
+
statusCache = new Map();
|
|
76
|
+
disposed = false;
|
|
77
|
+
constructor(ctx, dependencies = { status: readAgentRuntimeStatus, execute: agent_runtime_bridge_1.runAgentRuntimeInvocation, kill: win_spawn_1.treeKillSafe }) {
|
|
78
|
+
this.ctx = ctx;
|
|
79
|
+
this.dependencies = dependencies;
|
|
80
|
+
}
|
|
81
|
+
context(runId) {
|
|
82
|
+
if (!SAFE_ID.test(runId))
|
|
83
|
+
throw new RuntimeControlError(400, 'invalid_run_id', 'Invalid runtime run ID');
|
|
84
|
+
const execution = (0, workspace_resolution_1.resolveProjectExecution)(this.ctx.project);
|
|
85
|
+
const backlogRoot = node_fs_1.default.realpathSync(node_path_1.default.dirname(execution.specrailsDir));
|
|
86
|
+
const directory = node_path_1.default.join(execution.specrailsDir, 'pipeline', runId);
|
|
87
|
+
const file = node_path_1.default.join(directory, 'desktop-context.json');
|
|
88
|
+
if (!node_fs_1.default.existsSync(file))
|
|
89
|
+
throw new RuntimeControlError(404, 'runtime_run_not_found', 'Runtime execution context is unavailable');
|
|
90
|
+
if (node_fs_1.default.realpathSync(file) !== node_path_1.default.join(node_fs_1.default.realpathSync(execution.specrailsDir), 'pipeline', runId, 'desktop-context.json'))
|
|
91
|
+
throw new RuntimeControlError(409, 'runtime_scope_changed', 'Runtime context path no longer matches its project');
|
|
92
|
+
const frozen = JSON.parse(node_fs_1.default.readFileSync(file, 'utf8'));
|
|
93
|
+
try {
|
|
94
|
+
if (frozen.runId !== runId || node_fs_1.default.realpathSync(frozen.backlogRoot) !== backlogRoot || !Array.isArray(frozen.repositories) || !frozen.repositories.length || !frozen.repositories.some((repository) => repository.path === frozen.artifactRoot))
|
|
95
|
+
throw new Error();
|
|
96
|
+
for (const root of [frozen.artifactRoot, ...frozen.repositories.map((repository) => repository.path)]) {
|
|
97
|
+
if (!node_path_1.default.isAbsolute(root) || node_fs_1.default.realpathSync(root) !== root || !node_fs_1.default.statSync(root).isDirectory())
|
|
98
|
+
throw new Error();
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
throw new RuntimeControlError(409, 'runtime_scope_unavailable', 'The original runtime worktree or project scope is unavailable. Start a new implementation.');
|
|
103
|
+
}
|
|
104
|
+
const stored = this.ctx.db.prepare('SELECT execution_manifest FROM loop_runs WHERE id = ?').get(runId);
|
|
105
|
+
const manifest = (0, multi_repo_execution_store_1.readExecutionManifest)(stored?.execution_manifest);
|
|
106
|
+
if (manifest && (manifest.projectId !== this.ctx.project.id || manifest.repositories.length !== frozen.repositories.length || !manifest.repositories.every((repository) => frozen.repositories.some((item) => item.id === repository.repositoryId && item.path === repository.worktreePath))))
|
|
107
|
+
throw new RuntimeControlError(409, 'runtime_scope_changed', 'The saved worktree manifest differs from the frozen runtime scope');
|
|
108
|
+
const hostFile = node_path_1.default.join(directory, 'desktop-runtime-host.json');
|
|
109
|
+
if (!node_fs_1.default.existsSync(hostFile))
|
|
110
|
+
throw new RuntimeControlError(409, 'runtime_host_unavailable', 'The original runtime host settings are missing. Start a new implementation.');
|
|
111
|
+
const host = JSON.parse(node_fs_1.default.readFileSync(hostFile, 'utf8'));
|
|
112
|
+
if (host.schemaVersion !== 1 || typeof host.cwd !== 'string' || ![frozen.backlogRoot, ...frozen.repositories.map((repository) => repository.path)].includes(host.cwd) || !host.env || typeof host.env !== 'object' || Array.isArray(host.env) || Object.entries(host.env).some(([key, value]) => !agent_runtime_bridge_1.RUNTIME_HOST_ENV_KEYS.includes(key) || typeof value !== 'string'))
|
|
113
|
+
throw new RuntimeControlError(409, 'runtime_host_invalid', 'The saved runtime host settings are invalid');
|
|
114
|
+
const env = { ...(0, workspace_resolution_1.resolveLoopBaseEnv)(this.ctx.project) };
|
|
115
|
+
for (const key of agent_runtime_bridge_1.RUNTIME_HOST_ENV_KEYS)
|
|
116
|
+
delete env[key];
|
|
117
|
+
Object.assign(env, host.env, { SPECRAILS_EXECUTION_CONTEXT: file });
|
|
118
|
+
return { file, frozen, cwd: host.cwd, env };
|
|
119
|
+
}
|
|
120
|
+
async list() {
|
|
121
|
+
const directory = node_path_1.default.join((0, workspace_resolution_1.resolveProjectExecution)(this.ctx.project).specrailsDir, 'pipeline');
|
|
122
|
+
if (!node_fs_1.default.existsSync(directory))
|
|
123
|
+
return [];
|
|
124
|
+
const entries = node_fs_1.default.readdirSync(directory, { withFileTypes: true }).filter((entry) => entry.isDirectory() && SAFE_ID.test(entry.name) && node_fs_1.default.existsSync(node_path_1.default.join(directory, entry.name, 'agent-runtime-request.json')))
|
|
125
|
+
.sort((a, b) => node_fs_1.default.statSync(node_path_1.default.join(directory, b.name)).mtimeMs - node_fs_1.default.statSync(node_path_1.default.join(directory, a.name)).mtimeMs).slice(0, 20);
|
|
126
|
+
const runs = [];
|
|
127
|
+
// Bounded sequential reads avoid spawning one Node process per historical run at once.
|
|
128
|
+
for (const entry of entries)
|
|
129
|
+
runs.push(await this.summary(entry.name));
|
|
130
|
+
return runs;
|
|
131
|
+
}
|
|
132
|
+
async summary(runId) {
|
|
133
|
+
try {
|
|
134
|
+
const { file, cwd, env } = this.context(runId);
|
|
135
|
+
// Core owns checkpoint parsing. Its atomic file revision is only a cache
|
|
136
|
+
// key, keeping idle settings panes from spawning CLI processes repeatedly.
|
|
137
|
+
const checkpoint = node_path_1.default.join(node_path_1.default.dirname(file), 'agent-workflow', runId, 'checkpoint.json');
|
|
138
|
+
const stat = node_fs_1.default.existsSync(checkpoint) ? node_fs_1.default.statSync(checkpoint) : null;
|
|
139
|
+
const fingerprint = stat ? `${stat.mtimeMs}:${stat.size}` : null;
|
|
140
|
+
const cached = this.statusCache.get(runId);
|
|
141
|
+
const state = fingerprint && cached?.fingerprint === fingerprint ? cached.state : await this.dependencies.status(file, cwd, env);
|
|
142
|
+
if (!state || state.runId !== runId)
|
|
143
|
+
throw new Error('No saved workflow state is available');
|
|
144
|
+
if (fingerprint)
|
|
145
|
+
this.statusCache.set(runId, { fingerprint, state });
|
|
146
|
+
const parent = (0, loop_runs_store_1.getLoopRun)(this.ctx.db, runId);
|
|
147
|
+
const active = this.active.has(runId) || parent?.status === 'running' || parent?.status === 'paused';
|
|
148
|
+
const recoverableSteps = Object.entries(state.steps).filter(([, step]) => ['running', 'interrupted'].includes(step.status)).map(([id]) => id);
|
|
149
|
+
return { runId, status: state.status === 'running' && !active ? 'interrupted' : state.status, nextStep: state.nextStep,
|
|
150
|
+
updatedAt: state.updatedAt, error: this.errors.get(runId) ?? state.error, pendingApproval: state.pendingApproval,
|
|
151
|
+
traceId: state.traceId, pendingQuestion: openQuestion(state),
|
|
152
|
+
recoverableSteps, active, canCancel: this.active.has(runId), canResume: !active && parent?.status === 'completed' && state.status !== 'succeeded' };
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
return { runId, status: 'unavailable', nextStep: null, active: this.active.has(runId), canResume: false, canCancel: this.active.has(runId), recoverableSteps: [], error: error instanceof RuntimeControlError ? error.message : 'Could not inspect the saved runtime execution' };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
async resume(runId, input) {
|
|
159
|
+
if (this.disposed)
|
|
160
|
+
throw new RuntimeControlError(503, 'runtime_shutting_down', 'Project runtime is shutting down');
|
|
161
|
+
const parent = (0, loop_runs_store_1.getLoopRun)(this.ctx.db, runId);
|
|
162
|
+
if (!parent || parent.status !== 'completed' || this.active.has(runId))
|
|
163
|
+
throw new RuntimeControlError(409, 'runtime_run_active', 'Wait for the original Desktop execution to settle before resuming');
|
|
164
|
+
const context = this.context(runId);
|
|
165
|
+
// Reserve before awaiting status so concurrent resume requests cannot race.
|
|
166
|
+
const active = { cancelled: false };
|
|
167
|
+
this.active.set(runId, active);
|
|
168
|
+
try {
|
|
169
|
+
// Accounting from a prior continuation must settle even when Core now
|
|
170
|
+
// reports success and correctly refuses any further execution.
|
|
171
|
+
(0, loop_run_manager_1.recoverOrphanLoopStepAccounting)(this.ctx.db, new Date().toISOString(), runId);
|
|
172
|
+
const state = await this.dependencies.status(context.file, context.cwd, context.env);
|
|
173
|
+
if (!state || state.runId !== runId || state.status === 'succeeded')
|
|
174
|
+
throw new RuntimeControlError(409, 'runtime_not_resumable', 'This execution has no resumable workflow');
|
|
175
|
+
// A pending question resumes the architect only with the operator's answer.
|
|
176
|
+
if (openQuestion(state) && !input.answer)
|
|
177
|
+
throw new RuntimeControlError(400, 'answer_required', 'This execution is waiting for an answer to the architect\'s question');
|
|
178
|
+
this.errors.delete(runId);
|
|
179
|
+
this.statusCache.delete(runId);
|
|
180
|
+
const startedAt = new Date().toISOString();
|
|
181
|
+
const invocationId = (0, node_crypto_1.randomUUID)();
|
|
182
|
+
const stepKey = `runtime-resume:${invocationId}`;
|
|
183
|
+
// Both live settlement and restart use the existing loop ledger.
|
|
184
|
+
const previous = (0, loop_runs_store_1.getLoopRun)(this.ctx.db, runId);
|
|
185
|
+
let sequence = this.ctx.db.prepare('SELECT COALESCE(MAX(seq), -1) AS seq FROM events WHERE job_id = ?').get(runId).seq;
|
|
186
|
+
const startedAtMs = Date.parse(startedAt);
|
|
187
|
+
let ticketIds = [];
|
|
188
|
+
try {
|
|
189
|
+
const ids = JSON.parse(previous.ticket_ids_json ?? '[]');
|
|
190
|
+
if (Array.isArray(ids))
|
|
191
|
+
ticketIds = ids.filter((id) => typeof id === 'number' && Number.isSafeInteger(id));
|
|
192
|
+
}
|
|
193
|
+
catch { /* Older loop records can lack a ticket scope. */ }
|
|
194
|
+
(0, loop_runs_store_1.stageLoopStepRecovery)(this.ctx.db, {
|
|
195
|
+
version: 1, runId, stepKey, invocationId, projectId: this.ctx.project.id,
|
|
196
|
+
provider: 'agent-runtime', model: 'per-role', surfaceRefId: stepKey,
|
|
197
|
+
ticketIds, startedAt, baseline: (0, loop_runs_store_1.readLoopJobUsage)(this.ctx.db, runId),
|
|
198
|
+
completedEventSeq: sequence, providerCostBaseline: 0, providerTurnsBaseline: 0,
|
|
199
|
+
loopDurationBaseline: previous.total_duration_ms, completedDurationMs: 0,
|
|
200
|
+
iterationCount: previous.iteration_count, activeTurnStartedAtMs: startedAtMs, lastActivityAtMs: startedAtMs,
|
|
201
|
+
});
|
|
202
|
+
const logLine = (line, source = 'stdout') => {
|
|
203
|
+
if (this.disposed)
|
|
204
|
+
return;
|
|
205
|
+
try {
|
|
206
|
+
(0, db_1.appendEvent)(this.ctx.db, runId, ++sequence, { event_type: 'log', source, payload: JSON.stringify({ line: line.replace(/\n$/, '') }) });
|
|
207
|
+
}
|
|
208
|
+
catch { /* events table best-effort */ }
|
|
209
|
+
};
|
|
210
|
+
const requested = [...(input.approve?.length ? ['approve ' + input.approve.join(',')] : []), ...(input.recover?.length ? ['recover ' + input.recover.join(',')] : []), ...(input.invalidate?.length ? ['invalidate ' + input.invalidate.join(',')] : []), ...(input.answer ? ['answered question'] : [])];
|
|
211
|
+
logLine(`[runtime] continuation started from phase ${state.nextStep ?? 'end'}${requested.length ? ' (' + requested.join('; ') + ')' : ''}; the original worktree and frozen scope are reused`);
|
|
212
|
+
void this.dependencies.execute({
|
|
213
|
+
contextPath: context.file, cwd: context.cwd, env: context.env, resume: true, ...input,
|
|
214
|
+
// Readable progress lands in the same job log as the original rail run.
|
|
215
|
+
onLine: logLine,
|
|
216
|
+
onRawLine: (line) => {
|
|
217
|
+
if (this.disposed)
|
|
218
|
+
return;
|
|
219
|
+
let eventType = 'agent-runtime';
|
|
220
|
+
try {
|
|
221
|
+
const parsed = JSON.parse(line);
|
|
222
|
+
if (typeof parsed.type === 'string')
|
|
223
|
+
eventType = parsed.type;
|
|
224
|
+
}
|
|
225
|
+
catch { /* Keep raw diagnostics available to recovery. */ }
|
|
226
|
+
this.ctx.db.transaction(() => {
|
|
227
|
+
(0, db_1.appendEvent)(this.ctx.db, runId, ++sequence, { event_type: eventType, source: 'stdout', payload: line });
|
|
228
|
+
(0, loop_runs_store_1.updateLoopStepActivityCheckpoint)(this.ctx.db, runId, stepKey, undefined, Date.now());
|
|
229
|
+
})();
|
|
230
|
+
},
|
|
231
|
+
onSpawn: (child) => {
|
|
232
|
+
active.child = child;
|
|
233
|
+
if (this.disposed && child.pid)
|
|
234
|
+
this.dependencies.kill(child.pid, 'SIGKILL');
|
|
235
|
+
else if (active.cancelled)
|
|
236
|
+
this.cancel(runId);
|
|
237
|
+
},
|
|
238
|
+
}).then((result) => {
|
|
239
|
+
if (this.disposed)
|
|
240
|
+
return;
|
|
241
|
+
logLine(result.failed ? `[runtime] continuation ended: ${result.errorText ?? 'failed'}` : '[runtime] continuation completed', result.failed ? 'stderr' : 'stdout');
|
|
242
|
+
(0, loop_runs_store_1.setLoopStepSettledResult)(this.ctx.db, runId, stepKey, { ...result, provider: 'agent-runtime', model: 'per-role', failed: active.cancelled || result.failed });
|
|
243
|
+
(0, loop_run_manager_1.recoverOrphanLoopStepAccounting)(this.ctx.db, new Date().toISOString(), runId);
|
|
244
|
+
if (result.errorText)
|
|
245
|
+
this.errors.set(runId, result.errorText);
|
|
246
|
+
}).catch(() => { this.errors.set(runId, 'Runtime continuation failed. Inspect the saved phase state before retrying.'); })
|
|
247
|
+
.finally(() => { clearTimeout(active.forceKillTimer); this.active.delete(runId); });
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
this.active.delete(runId);
|
|
251
|
+
throw error;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
cancel(runId) {
|
|
255
|
+
const active = this.active.get(runId);
|
|
256
|
+
if (!active)
|
|
257
|
+
throw new RuntimeControlError(409, 'runtime_not_active', 'Only a continuation started here can be cancelled here; stop active rail runs from their job controls');
|
|
258
|
+
active.cancelled = true;
|
|
259
|
+
if (active.child?.pid) {
|
|
260
|
+
this.dependencies.kill(active.child.pid, this.disposed ? 'SIGKILL' : 'SIGTERM');
|
|
261
|
+
if (!this.disposed && !active.forceKillTimer) {
|
|
262
|
+
const pid = active.child.pid;
|
|
263
|
+
active.forceKillTimer = setTimeout(() => {
|
|
264
|
+
if (this.active.get(runId) === active)
|
|
265
|
+
this.dependencies.kill(pid, 'SIGKILL');
|
|
266
|
+
}, 3000);
|
|
267
|
+
active.forceKillTimer.unref?.();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
shutdown() {
|
|
272
|
+
this.disposed = true;
|
|
273
|
+
for (const runId of this.active.keys())
|
|
274
|
+
this.cancel(runId);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
exports.AgentRuntimeControls = AgentRuntimeControls;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.findCoreAgentRuntimeEntry = findCoreAgentRuntimeEntry;
|
|
4
|
+
exports.resetCoreAgentRuntimeApiCache = resetCoreAgentRuntimeApiCache;
|
|
5
|
+
exports.loadCoreAgentRuntime = loadCoreAgentRuntime;
|
|
6
|
+
exports.findCoreAgentRuntimeCli = findCoreAgentRuntimeCli;
|
|
7
|
+
const node_fs_1 = require("node:fs");
|
|
8
|
+
const node_child_process_1 = require("node:child_process");
|
|
9
|
+
const node_module_1 = require("node:module");
|
|
10
|
+
const node_path_1 = require("node:path");
|
|
11
|
+
const core_runtime_1 = require("./core-runtime");
|
|
12
|
+
const core_node_runtime_1 = require("./core-node-runtime");
|
|
13
|
+
const win_spawn_1 = require("./util/win-spawn");
|
|
14
|
+
/** An explicit or bundled installation is authoritative: never silently replace
|
|
15
|
+
* a broken production bundle with a developer's checkout. */
|
|
16
|
+
function findCoreAgentRuntimeEntry() {
|
|
17
|
+
const explicit = process.env.SPECRAILS_CORE_RUNTIME_PATH;
|
|
18
|
+
if (explicit) {
|
|
19
|
+
const entry = (0, node_path_1.resolve)(explicit);
|
|
20
|
+
return (0, node_fs_1.existsSync)(entry) ? entry : null;
|
|
21
|
+
}
|
|
22
|
+
const selected = (0, core_runtime_1.getCoreRuntimeStatus)();
|
|
23
|
+
if (selected.error)
|
|
24
|
+
return null;
|
|
25
|
+
if (selected.runtime) {
|
|
26
|
+
const entry = (0, node_path_1.join)(selected.runtime.root, 'dist', 'agent-runtime', 'index.js');
|
|
27
|
+
if ((0, node_fs_1.existsSync)(entry))
|
|
28
|
+
return entry;
|
|
29
|
+
// Match Desktop's activated Core package, including managed upgrades. A
|
|
30
|
+
// missing API must never load an older bundle behind an active framework.
|
|
31
|
+
if (process.env.NODE_ENV === 'production' || ['managed', 'override', 'bundled'].includes(selected.runtime.source))
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
return (0, node_module_1.createRequire)((0, node_path_1.join)(process.cwd(), 'package.json')).resolve('specrails-core/agent-runtime');
|
|
36
|
+
}
|
|
37
|
+
catch { /* A source checkout can use the sibling Core build. */ }
|
|
38
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
39
|
+
const entry = (0, node_path_1.resolve)(process.cwd(), '..', 'specrails-core', 'dist', 'agent-runtime', 'index.js');
|
|
40
|
+
if ((0, node_fs_1.existsSync)(entry))
|
|
41
|
+
return entry;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
/** The api probe is stable for a given CLI file; repeated settings saves need not respawn it.
|
|
46
|
+
* Validation stays uncached because its answer depends on the submitted configuration. */
|
|
47
|
+
const apiProbeCache = new Map();
|
|
48
|
+
function resetCoreAgentRuntimeApiCache() { apiProbeCache.clear(); }
|
|
49
|
+
async function loadCoreAgentRuntime() {
|
|
50
|
+
const cli = findCoreAgentRuntimeCli();
|
|
51
|
+
if (!cli)
|
|
52
|
+
throw new Error('Programmatic agent runtime is unavailable. Build or bundle a compatible specrails-core release.');
|
|
53
|
+
// Core is ESM. Desktop ships both CJS and a pkg sidecar; importing external
|
|
54
|
+
// ESM from either depends on host-loader details. The bundled ordinary Node
|
|
55
|
+
// process is the same portable boundary used for execution and inspection.
|
|
56
|
+
const invoke = (args, input) => (0, node_child_process_1.execFileSync)((0, core_node_runtime_1.resolveCoreNodeRuntime)(), [cli, ...args], {
|
|
57
|
+
cwd: (0, node_path_1.dirname)(cli), env: (0, win_spawn_1.windowsSpawnEnv)(process.env), input, encoding: 'utf8',
|
|
58
|
+
timeout: 15_000, maxBuffer: 2 * 1024 * 1024, windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'],
|
|
59
|
+
});
|
|
60
|
+
const stat = (0, node_fs_1.statSync)(cli);
|
|
61
|
+
const cached = apiProbeCache.get(cli);
|
|
62
|
+
if (!cached || cached.mtimeMs !== stat.mtimeMs || cached.size !== stat.size) {
|
|
63
|
+
const api = JSON.parse(invoke(['api']));
|
|
64
|
+
if (api.type !== 'runtime-api' || api.apiVersion !== 1) {
|
|
65
|
+
throw new Error('Incompatible Core agent runtime API; expected version 1. Update the paired Core bundle.');
|
|
66
|
+
}
|
|
67
|
+
apiProbeCache.set(cli, { mtimeMs: stat.mtimeMs, size: stat.size });
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
RUNTIME_API_VERSION: 1,
|
|
71
|
+
validateRuntimeConfig(input) {
|
|
72
|
+
const result = JSON.parse(invoke(['validate', '--stdin'], JSON.stringify(input)));
|
|
73
|
+
if (result.type !== 'runtime-config-valid')
|
|
74
|
+
throw new Error('Core did not validate the runtime configuration');
|
|
75
|
+
return input;
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function findCoreAgentRuntimeCli() {
|
|
80
|
+
const entry = findCoreAgentRuntimeEntry();
|
|
81
|
+
if (!entry)
|
|
82
|
+
return null;
|
|
83
|
+
const cli = (0, node_path_1.join)((0, node_path_1.dirname)(entry), 'cli.js');
|
|
84
|
+
return (0, node_fs_1.existsSync)(cli) ? cli : null;
|
|
85
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hasAgentRuntimeRequest = hasAgentRuntimeRequest;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const node_path_1 = require("node:path");
|
|
6
|
+
const workspace_resolution_1 = require("./workspace-resolution");
|
|
7
|
+
function hasAgentRuntimeRequest(project, runId) {
|
|
8
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(runId))
|
|
9
|
+
return false;
|
|
10
|
+
const file = (0, node_path_1.join)((0, workspace_resolution_1.resolveProjectExecution)(project).specrailsDir, 'pipeline', runId, 'agent-runtime-request.json');
|
|
11
|
+
return (0, node_fs_1.existsSync)(file);
|
|
12
|
+
}
|