@inspectr/mcplab 1.28.4 → 1.29.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 +52 -5
- package/dist/app/assets/index-256LN2cO.css +2 -0
- package/dist/app/assets/index-DVIaEnjQ.js +243 -0
- package/dist/app/assets/sse-queue-worker-Iwmqd2oW.js +1 -0
- package/dist/app/index.html +2 -2
- package/dist/app-server/app-context.d.ts +75 -115
- package/dist/app-server/app-context.d.ts.map +1 -1
- package/dist/app-server/app-dist.d.ts +3 -3
- package/dist/app-server/app-dist.js.map +1 -1
- package/dist/app-server/app-run-artifacts.d.ts +4 -0
- package/dist/app-server/app-run-artifacts.d.ts.map +1 -0
- package/dist/app-server/app-run-artifacts.js +66 -0
- package/dist/app-server/app-run-artifacts.js.map +1 -0
- package/dist/app-server/assistant-common.d.ts +27 -37
- package/dist/app-server/assistant-common.d.ts.map +1 -1
- package/dist/app-server/assistant-common.js +110 -1
- package/dist/app-server/assistant-common.js.map +1 -1
- package/dist/app-server/assistant-events.d.ts +8 -28
- package/dist/app-server/browser-launch.d.ts +3 -3
- package/dist/app-server/browser-launch.js.map +1 -1
- package/dist/app-server/browser-provider-learning.d.ts +26 -0
- package/dist/app-server/browser-provider-learning.d.ts.map +1 -0
- package/dist/app-server/browser-provider-learning.js +353 -0
- package/dist/app-server/browser-provider-learning.js.map +1 -0
- package/dist/app-server/config-store.d.ts +13 -21
- package/dist/app-server/dev-mcp.d.ts +2 -5
- package/dist/app-server/evals-routes.d.ts +8 -18
- package/dist/app-server/evaluation-group-results.d.ts +11 -0
- package/dist/app-server/evaluation-group-results.d.ts.map +1 -0
- package/dist/app-server/evaluation-group-results.js +46 -0
- package/dist/app-server/evaluation-group-results.js.map +1 -0
- package/dist/app-server/evaluation-journal-projection.d.ts +9 -0
- package/dist/app-server/evaluation-journal-projection.d.ts.map +1 -0
- package/dist/app-server/evaluation-journal-projection.js +76 -0
- package/dist/app-server/evaluation-journal-projection.js.map +1 -0
- package/dist/app-server/evaluation-journal-writer.d.ts +23 -0
- package/dist/app-server/evaluation-journal-writer.d.ts.map +1 -0
- package/dist/app-server/evaluation-journal-writer.js +41 -0
- package/dist/app-server/evaluation-journal-writer.js.map +1 -0
- package/dist/app-server/evaluation-result-projection.d.ts +12 -0
- package/dist/app-server/evaluation-result-projection.d.ts.map +1 -0
- package/dist/app-server/evaluation-result-projection.js +107 -0
- package/dist/app-server/evaluation-result-projection.js.map +1 -0
- package/dist/app-server/execution-journal.d.ts +13 -0
- package/dist/app-server/execution-journal.d.ts.map +1 -0
- package/dist/app-server/execution-journal.js +39 -0
- package/dist/app-server/execution-journal.js.map +1 -0
- package/dist/app-server/http.d.ts +1 -1
- package/dist/app-server/index.d.ts +1 -1
- package/dist/app-server/inspectr-session-manager.d.ts +45 -0
- package/dist/app-server/inspectr-session-manager.d.ts.map +1 -0
- package/dist/app-server/inspectr-session-manager.js +203 -0
- package/dist/app-server/inspectr-session-manager.js.map +1 -0
- package/dist/app-server/jobs.d.ts +6 -9
- package/dist/app-server/libraries-store.d.ts +26 -10
- package/dist/app-server/libraries-store.d.ts.map +1 -1
- package/dist/app-server/libraries-store.js +130 -3
- package/dist/app-server/libraries-store.js.map +1 -1
- package/dist/app-server/live-tests-routes.d.ts +17 -0
- package/dist/app-server/live-tests-routes.d.ts.map +1 -0
- package/dist/app-server/live-tests-routes.js +82 -0
- package/dist/app-server/live-tests-routes.js.map +1 -0
- package/dist/app-server/live-tests.d.ts +81 -0
- package/dist/app-server/live-tests.d.ts.map +1 -0
- package/dist/app-server/live-tests.js +272 -0
- package/dist/app-server/live-tests.js.map +1 -0
- package/dist/app-server/markdown-reports.d.ts +7 -7
- package/dist/app-server/oauth-debugger-domain.d.ts +183 -207
- package/dist/app-server/oauth-debugger-domain.js.map +1 -1
- package/dist/app-server/oauth-debugger.d.ts +10 -17
- package/dist/app-server/oauth-debugger.js +1 -1
- package/dist/app-server/oauth-debugger.js.map +1 -1
- package/dist/app-server/oauth-runtime-domain.d.ts +42 -53
- package/dist/app-server/oauth-runtime-domain.js.map +1 -1
- package/dist/app-server/oauth-runtime-routes.d.ts +12 -12
- package/dist/app-server/oauth-session-manager.d.ts +44 -50
- package/dist/app-server/oauth-session-manager.d.ts.map +1 -1
- package/dist/app-server/oauth-session-manager.js.map +1 -1
- package/dist/app-server/provider-models.d.ts +5 -5
- package/dist/app-server/result-assistant-domain.d.ts +66 -82
- package/dist/app-server/result-assistant-domain.d.ts.map +1 -1
- package/dist/app-server/result-assistant-domain.js.map +1 -1
- package/dist/app-server/result-assistant-tools.d.ts +5 -5
- package/dist/app-server/result-assistant.d.ts +9 -21
- package/dist/app-server/result-assistant.js.map +1 -1
- package/dist/app-server/router.d.ts +9 -1
- package/dist/app-server/router.d.ts.map +1 -1
- package/dist/app-server/router.js +328 -10
- package/dist/app-server/router.js.map +1 -1
- package/dist/app-server/rover-connection.d.ts +48 -0
- package/dist/app-server/rover-connection.d.ts.map +1 -0
- package/dist/app-server/rover-connection.js +172 -0
- package/dist/app-server/rover-connection.js.map +1 -0
- package/dist/app-server/run-agent-selection.d.ts +2 -5
- package/dist/app-server/run-queue-domain.d.ts +89 -65
- package/dist/app-server/run-queue-domain.d.ts.map +1 -1
- package/dist/app-server/run-queue-domain.js +723 -9
- package/dist/app-server/run-queue-domain.js.map +1 -1
- package/dist/app-server/run-queue-events.d.ts +3 -10
- package/dist/app-server/run-queue-events.d.ts.map +1 -1
- package/dist/app-server/run-queue-events.js +114 -14
- package/dist/app-server/run-queue-events.js.map +1 -1
- package/dist/app-server/run-queue-executor.d.ts +33 -47
- package/dist/app-server/run-queue-executor.d.ts.map +1 -1
- package/dist/app-server/run-queue-executor.js +85 -15
- package/dist/app-server/run-queue-executor.js.map +1 -1
- package/dist/app-server/run-queue-state.d.ts +81 -47
- package/dist/app-server/run-queue-state.d.ts.map +1 -1
- package/dist/app-server/run-queue-state.js.map +1 -1
- package/dist/app-server/runs-routes.d.ts +13 -35
- package/dist/app-server/runs-routes.d.ts.map +1 -1
- package/dist/app-server/runs-routes.js +182 -16
- package/dist/app-server/runs-routes.js.map +1 -1
- package/dist/app-server/runs-routes.test-helpers.d.ts +42 -38
- package/dist/app-server/runs-routes.test-helpers.d.ts.map +1 -1
- package/dist/app-server/runs-routes.test-helpers.js +4 -1
- package/dist/app-server/runs-routes.test-helpers.js.map +1 -1
- package/dist/app-server/runs-store.d.ts +60 -43
- package/dist/app-server/runs-store.d.ts.map +1 -1
- package/dist/app-server/runs-store.js +157 -70
- package/dist/app-server/runs-store.js.map +1 -1
- package/dist/app-server/scenario-assistant-domain.d.ts +131 -157
- package/dist/app-server/scenario-assistant-domain.d.ts.map +1 -1
- package/dist/app-server/scenario-assistant-domain.js +7 -15
- package/dist/app-server/scenario-assistant-domain.js.map +1 -1
- package/dist/app-server/scenario-assistant.d.ts +10 -26
- package/dist/app-server/scenario-assistant.js.map +1 -1
- package/dist/app-server/settings-store.d.ts +4 -4
- package/dist/app-server/static-serving.d.ts +7 -13
- package/dist/app-server/store-utils.d.ts +1 -1
- package/dist/app-server/tool-analysis-domain.d.ts +1 -2
- package/dist/app-server/tool-analysis-domain.d.ts.map +1 -1
- package/dist/app-server/tool-analysis-domain.js +2 -59
- package/dist/app-server/tool-analysis-domain.js.map +1 -1
- package/dist/app-server/tool-analysis-storage.d.ts +20 -34
- package/dist/app-server/tool-analysis.d.ts +10 -19
- package/dist/app-server/types.d.ts +25 -25
- package/dist/app-server/version-info.d.ts +6 -6
- package/dist/app-server/version-info.js.map +1 -1
- package/dist/app-server/workspace-paths.d.ts +2 -1
- package/dist/app-server/workspace-paths.d.ts.map +1 -1
- package/dist/app-server/workspace-paths.js +5 -1
- package/dist/app-server/workspace-paths.js.map +1 -1
- package/dist/cli-branding.d.ts +1 -1
- package/dist/cli-branding.js.map +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/eval-config-files.d.ts +5 -8
- package/dist/interactive-helpers.d.ts +11 -15
- package/dist/migrate-utils.d.ts +1 -1
- package/dist/results/context.d.ts +1 -1
- package/dist/results/format.d.ts +7 -14
- package/dist/results/indexer.d.ts +2 -9
- package/dist/results/search.d.ts +1 -1
- package/dist/results/types.d.ts +2 -11
- package/dist/run-interactive.d.ts +21 -23
- package/dist/test-results-fixture.d.ts +4 -4
- package/package.json +8 -6
- package/dist/app/assets/index-DZoA-93O.js +0 -258
- package/dist/app/assets/index-aPA0WpCV.css +0 -1
- package/dist/app/assets/sse-queue-worker-BMo6aPaB.js +0 -1
|
@@ -1,22 +1,195 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { join } from 'node:path';
|
|
2
3
|
import { emitQueueEvent, buildQueueState, closeJobClients } from './run-queue-events.js';
|
|
3
4
|
import { admitQueuedJob, executeRunJob } from './run-queue-executor.js';
|
|
4
5
|
import { createRunQueueState, currentWorkerUsage } from './run-queue-state.js';
|
|
6
|
+
import { ROVER_LEASE_REQUEUE_REASONS, ROVER_LEASE_TERMINAL_REASONS, upsertQueueChildProgress } from '@inspectr/mcplab-core';
|
|
7
|
+
import { appendExecutionEvent, readExecutionEvents } from './execution-journal.js';
|
|
8
|
+
import { recordEvaluationTerminalExecution } from './evaluation-journal-writer.js';
|
|
9
|
+
import { projectEvaluationJournal } from './evaluation-journal-projection.js';
|
|
5
10
|
export function createRunQueueService(params) {
|
|
6
11
|
const jobs = params.jobs ?? new Map();
|
|
7
12
|
const state = params.state ?? createRunQueueState(params.settings.defaultQueueWorkers);
|
|
8
13
|
const { settings, oauthSessionManager, deps } = params;
|
|
14
|
+
const leaseTimers = new Map();
|
|
15
|
+
let rejectedRoverRegistration;
|
|
16
|
+
function markRoverStarted(job) {
|
|
17
|
+
deps.addJobEvent(job, {
|
|
18
|
+
type: 'started',
|
|
19
|
+
ts: new Date().toISOString(),
|
|
20
|
+
payload: {
|
|
21
|
+
executionType: 'rover',
|
|
22
|
+
roverAgent: job.runParams.roverAgent,
|
|
23
|
+
agents: job.runParams.requestedAgents ?? null
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
function appendRoverTraceEvent(job, message, provider) {
|
|
28
|
+
if (!job?.runParams.evaluationRunId)
|
|
29
|
+
return;
|
|
30
|
+
const evaluationRunId = job.runParams.evaluationRunId;
|
|
31
|
+
const details = Object.fromEntries(Object.entries(message).filter(([key, value]) => !['type', 'prompt', 'text', 'response', 'result', 'content'].includes(key) &&
|
|
32
|
+
(typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean')));
|
|
33
|
+
appendExecutionEvent(join(settings.runsDir, evaluationRunId), {
|
|
34
|
+
eventId: `rover-${job.id}-${randomUUID()}`,
|
|
35
|
+
type: 'rover_event',
|
|
36
|
+
ts: new Date().toISOString(),
|
|
37
|
+
evaluationRunId,
|
|
38
|
+
executionId: job.id,
|
|
39
|
+
roverType: message.type,
|
|
40
|
+
provider,
|
|
41
|
+
...details
|
|
42
|
+
});
|
|
43
|
+
}
|
|
9
44
|
function emit() {
|
|
10
45
|
emitQueueEvent(jobs, state, deps.sendSseEvent);
|
|
11
46
|
}
|
|
47
|
+
function scheduleLeaseExpiry(job) {
|
|
48
|
+
const lease = job.roverLease;
|
|
49
|
+
if (!lease)
|
|
50
|
+
return;
|
|
51
|
+
const existing = leaseTimers.get(job.id);
|
|
52
|
+
if (existing)
|
|
53
|
+
clearTimeout(existing);
|
|
54
|
+
const delay = Math.max(0, Date.parse(lease.expiresAt) - Date.now());
|
|
55
|
+
leaseTimers.set(job.id, setTimeout(() => {
|
|
56
|
+
leaseTimers.delete(job.id);
|
|
57
|
+
const current = jobs.get(job.id)?.roverLease;
|
|
58
|
+
if (!current ||
|
|
59
|
+
current.leaseId !== lease.leaseId ||
|
|
60
|
+
Date.parse(current.expiresAt) > Date.now())
|
|
61
|
+
return;
|
|
62
|
+
if (job.status === 'running' || job.status === 'paused_rover') {
|
|
63
|
+
finalizeRoverJob(job, { kind: 'requeue', reason: 'lease_expired' });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
deps.addJobEvent(job, {
|
|
67
|
+
type: 'log',
|
|
68
|
+
ts: new Date().toISOString(),
|
|
69
|
+
payload: { message: 'Rover assignment lease expired and was requeued.' }
|
|
70
|
+
});
|
|
71
|
+
emit();
|
|
72
|
+
if (job.runParams.executionType === 'rover')
|
|
73
|
+
params.onRoverJobReleased?.(job.runParams.roverAgent.provider, job.id);
|
|
74
|
+
}, delay));
|
|
75
|
+
}
|
|
12
76
|
function addStopEvent(job, message) {
|
|
13
77
|
deps.addJobEvent(job, {
|
|
14
78
|
type: 'error',
|
|
15
79
|
ts: new Date().toISOString(),
|
|
16
|
-
payload: { message }
|
|
80
|
+
payload: { message, status: 'stopped' }
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function markEvaluationStopped(job, reason) {
|
|
84
|
+
const evaluationRunId = job.runParams.evaluationRunId;
|
|
85
|
+
if (!evaluationRunId)
|
|
86
|
+
return;
|
|
87
|
+
appendExecutionEvent(join(settings.runsDir, evaluationRunId), {
|
|
88
|
+
eventId: `evaluation-stopped-${job.id}`,
|
|
89
|
+
type: 'evaluation_stopped',
|
|
90
|
+
ts: new Date().toISOString(),
|
|
91
|
+
executionId: job.id,
|
|
92
|
+
evaluationRunId,
|
|
93
|
+
reason
|
|
94
|
+
});
|
|
95
|
+
projectEvaluationJournal({
|
|
96
|
+
runsDir: settings.runsDir,
|
|
97
|
+
evaluationRunId,
|
|
98
|
+
evaluationName: job.runParams.evaluationName,
|
|
99
|
+
runNote: job.runParams.runNote,
|
|
100
|
+
executionStatus: 'stopped'
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function recordTerminalExecution(job, status, reason) {
|
|
104
|
+
const evaluationRunId = job.runParams.evaluationRunId;
|
|
105
|
+
if (!evaluationRunId)
|
|
106
|
+
return;
|
|
107
|
+
recordEvaluationTerminalExecution({
|
|
108
|
+
runsDir: settings.runsDir,
|
|
109
|
+
evaluationRunId,
|
|
110
|
+
evaluationName: job.runParams.evaluationName,
|
|
111
|
+
runNote: job.runParams.runNote,
|
|
112
|
+
executionId: job.id,
|
|
113
|
+
status,
|
|
114
|
+
reason
|
|
17
115
|
});
|
|
18
116
|
}
|
|
117
|
+
function clearRoverLease(job) {
|
|
118
|
+
const timer = leaseTimers.get(job.id);
|
|
119
|
+
if (timer)
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
leaseTimers.delete(job.id);
|
|
122
|
+
job.roverLease = undefined;
|
|
123
|
+
}
|
|
124
|
+
function settleUnfinishedChildren(job, status) {
|
|
125
|
+
job.childProgress = job.childProgress?.map((child) => child.status === 'queued' || child.status === 'running' ? { ...child, status } : child);
|
|
126
|
+
}
|
|
127
|
+
function finalizeRoverJob(job, disposition) {
|
|
128
|
+
if (job.runParams.executionType !== 'rover')
|
|
129
|
+
return false;
|
|
130
|
+
state.activeJobIds.delete(job.id);
|
|
131
|
+
state.admittingJobIds.delete(job.id);
|
|
132
|
+
state.blockedJobIds.delete(job.id);
|
|
133
|
+
clearRoverLease(job);
|
|
134
|
+
const queueIndex = state.queue.indexOf(job.id);
|
|
135
|
+
if (queueIndex !== -1)
|
|
136
|
+
state.queue.splice(queueIndex, 1);
|
|
137
|
+
if (disposition.kind === 'requeue') {
|
|
138
|
+
job.status = 'waiting_for_rover';
|
|
139
|
+
state.queue.unshift(job.id);
|
|
140
|
+
deps.addJobEvent(job, {
|
|
141
|
+
type: 'log',
|
|
142
|
+
ts: new Date().toISOString(),
|
|
143
|
+
payload: { message: `Rover assignment requeued: ${disposition.reason}` }
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
else if (disposition.kind === 'completed') {
|
|
147
|
+
settleUnfinishedChildren(job, disposition.payload?.outcome === 'error'
|
|
148
|
+
? 'error'
|
|
149
|
+
: disposition.payload?.outcome === 'incomplete'
|
|
150
|
+
? 'stopped'
|
|
151
|
+
: 'completed');
|
|
152
|
+
job.status = 'completed';
|
|
153
|
+
deps.addJobEvent(job, {
|
|
154
|
+
type: 'completed',
|
|
155
|
+
ts: new Date().toISOString(),
|
|
156
|
+
payload: { ...disposition.payload, executionType: 'rover' }
|
|
157
|
+
});
|
|
158
|
+
closeJobClients(job);
|
|
159
|
+
}
|
|
160
|
+
else if (disposition.kind === 'stopped') {
|
|
161
|
+
settleUnfinishedChildren(job, 'stopped');
|
|
162
|
+
job.status = 'stopped';
|
|
163
|
+
addStopEvent(job, disposition.reason);
|
|
164
|
+
markEvaluationStopped(job, disposition.reason);
|
|
165
|
+
closeJobClients(job);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
settleUnfinishedChildren(job, 'error');
|
|
169
|
+
job.status = 'error';
|
|
170
|
+
deps.addJobEvent(job, {
|
|
171
|
+
type: 'error',
|
|
172
|
+
ts: new Date().toISOString(),
|
|
173
|
+
payload: { message: `Rover assignment failed: ${disposition.reason}` }
|
|
174
|
+
});
|
|
175
|
+
recordTerminalExecution(job, 'error', disposition.reason);
|
|
176
|
+
closeJobClients(job);
|
|
177
|
+
}
|
|
178
|
+
emit();
|
|
179
|
+
if (disposition.kind !== 'completed')
|
|
180
|
+
params.onRoverJobReleased?.(job.runParams.roverAgent.provider, job.id);
|
|
181
|
+
// Wake API runs after the caller has offered the next Rover assignment.
|
|
182
|
+
// That assignment must count against the shared worker limit before we claim another job.
|
|
183
|
+
queueMicrotask(() => {
|
|
184
|
+
void advance({ emitWhenIdle: true });
|
|
185
|
+
});
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
19
188
|
function stopQueuedJob(job, message = 'Run stopped before it started') {
|
|
189
|
+
if (job.runParams.executionType === 'rover') {
|
|
190
|
+
clearRoverLease(job);
|
|
191
|
+
settleUnfinishedChildren(job, 'stopped');
|
|
192
|
+
}
|
|
20
193
|
const idx = state.queue.indexOf(job.id);
|
|
21
194
|
if (idx !== -1)
|
|
22
195
|
state.queue.splice(idx, 1);
|
|
@@ -24,6 +197,7 @@ export function createRunQueueService(params) {
|
|
|
24
197
|
state.blockedJobIds.delete(job.id);
|
|
25
198
|
job.status = 'stopped';
|
|
26
199
|
addStopEvent(job, message);
|
|
200
|
+
markEvaluationStopped(job, message);
|
|
27
201
|
closeJobClients(job);
|
|
28
202
|
}
|
|
29
203
|
function shouldStartQueuedJobImmediately(jobId) {
|
|
@@ -70,6 +244,7 @@ export function createRunQueueService(params) {
|
|
|
70
244
|
}
|
|
71
245
|
if (job.status !== 'stopped') {
|
|
72
246
|
job.status = 'error';
|
|
247
|
+
settleUnfinishedChildren(job, 'error');
|
|
73
248
|
deps.addJobEvent(job, {
|
|
74
249
|
type: 'error',
|
|
75
250
|
ts: new Date().toISOString(),
|
|
@@ -105,6 +280,10 @@ export function createRunQueueService(params) {
|
|
|
105
280
|
}
|
|
106
281
|
state.blockedJobIds.delete(job.id);
|
|
107
282
|
job.status = outcome.status;
|
|
283
|
+
if (outcome.status === 'error' || outcome.status === 'stopped') {
|
|
284
|
+
settleUnfinishedChildren(job, outcome.status);
|
|
285
|
+
recordTerminalExecution(job, outcome.status, outcome.status === 'stopped' ? 'Run stopped by user' : 'Run failed');
|
|
286
|
+
}
|
|
108
287
|
closeJobClients(job);
|
|
109
288
|
emit();
|
|
110
289
|
pruneOldJobs();
|
|
@@ -187,6 +366,7 @@ export function createRunQueueService(params) {
|
|
|
187
366
|
state.queue.splice(queueIndex, 1);
|
|
188
367
|
}
|
|
189
368
|
job.status = 'running';
|
|
369
|
+
job.roverProgress = { completed: 0, total: job.runParams.roverScenarios?.length ?? 0 };
|
|
190
370
|
state.activeJobIds.add(job.id);
|
|
191
371
|
deps.addJobEvent(job, {
|
|
192
372
|
type: 'started',
|
|
@@ -238,12 +418,17 @@ export function createRunQueueService(params) {
|
|
|
238
418
|
index -= 1;
|
|
239
419
|
continue;
|
|
240
420
|
}
|
|
241
|
-
if (nextJob.status !== 'queued' &&
|
|
421
|
+
if (nextJob.status !== 'queued' &&
|
|
422
|
+
nextJob.status !== 'blocked_auth' &&
|
|
423
|
+
nextJob.status !== 'waiting_for_rover' &&
|
|
424
|
+
nextJob.status !== 'paused_rover') {
|
|
242
425
|
state.queue.splice(index, 1);
|
|
243
426
|
queueMutated = true;
|
|
244
427
|
index -= 1;
|
|
245
428
|
continue;
|
|
246
429
|
}
|
|
430
|
+
if (nextJob.status === 'waiting_for_rover' || nextJob.status === 'paused_rover')
|
|
431
|
+
continue;
|
|
247
432
|
if (nextJob.status === 'blocked_auth' && !allowBlockedAuth) {
|
|
248
433
|
continue;
|
|
249
434
|
}
|
|
@@ -290,24 +475,54 @@ export function createRunQueueService(params) {
|
|
|
290
475
|
state,
|
|
291
476
|
enqueueRun(runParams, options) {
|
|
292
477
|
const jobId = `run-${Date.now()}-${randomUUID().slice(0, 8)}`;
|
|
478
|
+
const isRover = runParams.executionType === 'rover';
|
|
293
479
|
const job = {
|
|
294
480
|
id: jobId,
|
|
295
|
-
status: 'queued',
|
|
481
|
+
status: isRover ? 'waiting_for_rover' : 'queued',
|
|
296
482
|
events: [],
|
|
297
483
|
clients: new Set(),
|
|
298
484
|
abortController: new AbortController(),
|
|
299
|
-
runParams
|
|
485
|
+
runParams,
|
|
486
|
+
...(runParams.executionType === 'rover'
|
|
487
|
+
? {
|
|
488
|
+
childProgress: (runParams.roverScenarios ?? []).map((scenario) => ({
|
|
489
|
+
scenarioId: scenario.id,
|
|
490
|
+
agentName: runParams.roverAgent.name,
|
|
491
|
+
completed: 0,
|
|
492
|
+
total: 1,
|
|
493
|
+
status: 'queued'
|
|
494
|
+
}))
|
|
495
|
+
}
|
|
496
|
+
: {})
|
|
300
497
|
};
|
|
301
498
|
jobs.set(jobId, job);
|
|
499
|
+
if (runParams.evaluationRunId) {
|
|
500
|
+
const journalDir = join(settings.runsDir, runParams.evaluationRunId);
|
|
501
|
+
if (!readExecutionEvents(journalDir).some((event) => event.type === 'evaluation_started')) {
|
|
502
|
+
appendExecutionEvent(journalDir, {
|
|
503
|
+
eventId: `evaluation-started-${runParams.evaluationRunId}`,
|
|
504
|
+
type: 'evaluation_started',
|
|
505
|
+
ts: new Date().toISOString(),
|
|
506
|
+
evaluationRunId: runParams.evaluationRunId,
|
|
507
|
+
evaluationName: runParams.evaluationName,
|
|
508
|
+
newConversationBeforeStart: runParams.newConversationBeforeStart,
|
|
509
|
+
newConversationBetweenScenarios: runParams.newConversationBetweenScenarios
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
}
|
|
302
513
|
state.queue.push(jobId);
|
|
303
514
|
const queuedPosition = state.queue.length;
|
|
304
|
-
const shouldAttemptAdvance = currentWorkerUsage(state) < state.queueWorkerCount;
|
|
515
|
+
const shouldAttemptAdvance = !isRover && currentWorkerUsage(state) < state.queueWorkerCount;
|
|
305
516
|
const shouldStartImmediately = shouldStartQueuedJobImmediately(jobId);
|
|
306
|
-
if (
|
|
517
|
+
if (isRover && params.assignRoverJob) {
|
|
518
|
+
params.assignRoverJob(runParams.roverAgent?.provider ?? 'claude');
|
|
519
|
+
}
|
|
520
|
+
if (!shouldStartImmediately || isRover) {
|
|
307
521
|
deps.addJobEvent(job, {
|
|
308
522
|
type: 'queued',
|
|
309
523
|
ts: new Date().toISOString(),
|
|
310
524
|
payload: {
|
|
525
|
+
evaluationRunId: runParams.evaluationRunId,
|
|
311
526
|
configPath: runParams.configPath,
|
|
312
527
|
runsPerScenario: runParams.runsPerScenario,
|
|
313
528
|
scenarioId: runParams.scenarioId ?? null,
|
|
@@ -316,7 +531,9 @@ export function createRunQueueService(params) {
|
|
|
316
531
|
runNote: runParams.runNote ?? null,
|
|
317
532
|
serverOverrideAll: runParams.serverOverrideAll ?? null,
|
|
318
533
|
scenarioServerOverrides: runParams.scenarioServerOverrides ?? null,
|
|
319
|
-
position: queuedPosition
|
|
534
|
+
position: queuedPosition,
|
|
535
|
+
executionType: runParams.executionType ?? 'mcplab',
|
|
536
|
+
roverAgent: runParams.roverAgent
|
|
320
537
|
}
|
|
321
538
|
});
|
|
322
539
|
emit();
|
|
@@ -332,7 +549,10 @@ export function createRunQueueService(params) {
|
|
|
332
549
|
const job = jobs.get(jobId);
|
|
333
550
|
if (!job)
|
|
334
551
|
return null;
|
|
335
|
-
if (job.status === 'queued' ||
|
|
552
|
+
if (job.status === 'queued' ||
|
|
553
|
+
job.status === 'blocked_auth' ||
|
|
554
|
+
job.status === 'waiting_for_rover' ||
|
|
555
|
+
job.status === 'paused_rover') {
|
|
336
556
|
stopQueuedJob(job);
|
|
337
557
|
void advance({ emitWhenIdle: true, hostHeader: options?.hostHeader });
|
|
338
558
|
return { ok: true, status: 'stopped' };
|
|
@@ -341,9 +561,62 @@ export function createRunQueueService(params) {
|
|
|
341
561
|
return { ok: true, status: job.status };
|
|
342
562
|
}
|
|
343
563
|
job.abortController.abort();
|
|
564
|
+
if (job.runParams.executionType === 'rover') {
|
|
565
|
+
params.sendRoverMessage?.({ type: 'stop', jobId: job.id });
|
|
566
|
+
finalizeRoverJob(job, { kind: 'stopped', reason: 'Run stopped by user' });
|
|
567
|
+
return { ok: true, status: 'stopped' };
|
|
568
|
+
}
|
|
569
|
+
state.activeJobIds.delete(jobId);
|
|
344
570
|
job.status = 'stopped';
|
|
571
|
+
markEvaluationStopped(job, 'Run stopped by user');
|
|
345
572
|
return { ok: true, status: 'stopped' };
|
|
346
573
|
},
|
|
574
|
+
stopEvaluationRun(evaluationRunId, options) {
|
|
575
|
+
const jobsForEvaluation = Array.from(jobs.values()).filter((job) => job.runParams.evaluationRunId === evaluationRunId);
|
|
576
|
+
if (jobsForEvaluation.length === 0)
|
|
577
|
+
return null;
|
|
578
|
+
let stopped = 0;
|
|
579
|
+
for (const job of jobsForEvaluation) {
|
|
580
|
+
if (['queued', 'blocked_auth', 'waiting_for_rover', 'paused_rover', 'running'].includes(job.status)) {
|
|
581
|
+
if (this.stopJob(job.id, options)?.status === 'stopped')
|
|
582
|
+
stopped += 1;
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
if (jobsForEvaluation.every((job) => ['completed', 'error', 'stopped'].includes(jobs.get(job.id)?.status ?? job.status))) {
|
|
586
|
+
for (const job of jobsForEvaluation)
|
|
587
|
+
jobs.delete(job.id);
|
|
588
|
+
}
|
|
589
|
+
void advance({ emitWhenIdle: true, hostHeader: options?.hostHeader });
|
|
590
|
+
return { ok: true, stopped };
|
|
591
|
+
},
|
|
592
|
+
removeEvaluationRun(evaluationRunId, options) {
|
|
593
|
+
const jobsForEvaluation = Array.from(jobs.values()).filter((job) => job.runParams.evaluationRunId === evaluationRunId);
|
|
594
|
+
if (jobsForEvaluation.length === 0)
|
|
595
|
+
return null;
|
|
596
|
+
const terminalEvaluation = jobsForEvaluation.every((job) => ['completed', 'error', 'stopped'].includes(job.status));
|
|
597
|
+
if (!terminalEvaluation &&
|
|
598
|
+
jobsForEvaluation.some((job) => job.status !== 'queued' &&
|
|
599
|
+
job.status !== 'blocked_auth' &&
|
|
600
|
+
job.status !== 'waiting_for_rover' &&
|
|
601
|
+
job.status !== 'stopped')) {
|
|
602
|
+
return {
|
|
603
|
+
error: 'Evaluation run has already started. Use the stop action instead.',
|
|
604
|
+
statusCode: 400
|
|
605
|
+
};
|
|
606
|
+
}
|
|
607
|
+
let removed = 0;
|
|
608
|
+
for (const job of jobsForEvaluation) {
|
|
609
|
+
if (terminalEvaluation || job.status === 'stopped') {
|
|
610
|
+
jobs.delete(job.id);
|
|
611
|
+
removed += 1;
|
|
612
|
+
}
|
|
613
|
+
else if (this.removeQueuedJob(job.id, options)) {
|
|
614
|
+
removed += 1;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
void advance({ emitWhenIdle: true, hostHeader: options?.hostHeader });
|
|
618
|
+
return { ok: true, removed };
|
|
619
|
+
},
|
|
347
620
|
removeQueuedJob(jobId, options) {
|
|
348
621
|
const job = jobs.get(jobId);
|
|
349
622
|
if (!job)
|
|
@@ -354,7 +627,9 @@ export function createRunQueueService(params) {
|
|
|
354
627
|
statusCode: 400
|
|
355
628
|
};
|
|
356
629
|
}
|
|
357
|
-
if (job.status !== 'queued' &&
|
|
630
|
+
if (job.status !== 'queued' &&
|
|
631
|
+
job.status !== 'blocked_auth' &&
|
|
632
|
+
job.status !== 'waiting_for_rover') {
|
|
358
633
|
return { error: 'Job is not queued', statusCode: 404 };
|
|
359
634
|
}
|
|
360
635
|
stopQueuedJob(job, 'Removed from queue by user');
|
|
@@ -367,6 +642,21 @@ export function createRunQueueService(params) {
|
|
|
367
642
|
getQueueState() {
|
|
368
643
|
return buildQueueState(jobs, state);
|
|
369
644
|
},
|
|
645
|
+
getWaitingRoverJobs(provider) {
|
|
646
|
+
return state.queue.flatMap((jobId, index) => {
|
|
647
|
+
const job = jobs.get(jobId);
|
|
648
|
+
if (job?.status !== 'waiting_for_rover' || job.runParams.roverAgent?.provider !== provider)
|
|
649
|
+
return [];
|
|
650
|
+
return [
|
|
651
|
+
{
|
|
652
|
+
jobId: job.id,
|
|
653
|
+
evaluationName: job.runParams.evaluationName,
|
|
654
|
+
provider,
|
|
655
|
+
position: index + 1
|
|
656
|
+
}
|
|
657
|
+
];
|
|
658
|
+
});
|
|
659
|
+
},
|
|
370
660
|
subscribeQueue(req, res) {
|
|
371
661
|
if ('flushHeaders' in res && typeof res.flushHeaders === 'function') {
|
|
372
662
|
res.flushHeaders();
|
|
@@ -390,6 +680,430 @@ export function createRunQueueService(params) {
|
|
|
390
680
|
for (const client of state.clients)
|
|
391
681
|
client.end();
|
|
392
682
|
state.clients.clear();
|
|
683
|
+
},
|
|
684
|
+
assignRoverJob(provider, send, worker) {
|
|
685
|
+
if (worker && worker.provider !== provider)
|
|
686
|
+
return null;
|
|
687
|
+
// A page mismatch invalidates this registration. Wait for a fresh tab
|
|
688
|
+
// registration or reconnect instead of offering the same rejected job in a loop.
|
|
689
|
+
if (worker &&
|
|
690
|
+
rejectedRoverRegistration?.connectionId === worker.connectionId &&
|
|
691
|
+
rejectedRoverRegistration.version === (worker.registrationVersion ?? 0))
|
|
692
|
+
return null;
|
|
693
|
+
const roverAlreadyBusy = Array.from(state.activeJobIds).some((id) => jobs.get(id)?.runParams.executionType === 'rover');
|
|
694
|
+
if (roverAlreadyBusy)
|
|
695
|
+
return null;
|
|
696
|
+
const job = state.queue
|
|
697
|
+
.map((id) => jobs.get(id))
|
|
698
|
+
.find((candidate) => candidate?.status === 'waiting_for_rover' &&
|
|
699
|
+
candidate.runParams.roverAgent?.provider === provider &&
|
|
700
|
+
(candidate.runParams.roverAgent.providerRevision === undefined ||
|
|
701
|
+
worker?.providerRevision === undefined ||
|
|
702
|
+
candidate.runParams.roverAgent.providerRevision === worker.providerRevision));
|
|
703
|
+
if (!job)
|
|
704
|
+
return null;
|
|
705
|
+
const assignment = {
|
|
706
|
+
type: 'assignment',
|
|
707
|
+
jobId: job.id,
|
|
708
|
+
evaluationRunId: job.runParams.evaluationRunId,
|
|
709
|
+
configPath: job.runParams.configPath,
|
|
710
|
+
evaluationName: job.runParams.evaluationName,
|
|
711
|
+
agentName: job.runParams.roverAgent.name,
|
|
712
|
+
agent: job.runParams.roverAgent,
|
|
713
|
+
scenarios: job.runParams.roverScenarios ?? [],
|
|
714
|
+
newConversationBeforeStart: job.runParams.roverNewConversationBeforeStart !== false,
|
|
715
|
+
newConversationBetweenScenarios: job.runParams.roverNewConversationBetweenScenarios !== false
|
|
716
|
+
};
|
|
717
|
+
const leaseCapable = worker?.capabilities?.includes('assignment_lease') === true;
|
|
718
|
+
if (leaseCapable && worker) {
|
|
719
|
+
const now = new Date();
|
|
720
|
+
const leaseId = randomUUID();
|
|
721
|
+
const expiresAt = new Date(now.getTime() + 30_000).toISOString();
|
|
722
|
+
job.roverLease = {
|
|
723
|
+
leaseId,
|
|
724
|
+
connectionId: worker.connectionId,
|
|
725
|
+
registrationVersion: worker.registrationVersion,
|
|
726
|
+
state: 'offered',
|
|
727
|
+
expiresAt,
|
|
728
|
+
offeredAt: now.toISOString()
|
|
729
|
+
};
|
|
730
|
+
assignment.leaseId = leaseId;
|
|
731
|
+
assignment.leaseExpiresAt = expiresAt;
|
|
732
|
+
scheduleLeaseExpiry(job);
|
|
733
|
+
}
|
|
734
|
+
const index = state.queue.indexOf(job.id);
|
|
735
|
+
if (index !== -1)
|
|
736
|
+
state.queue.splice(index, 1);
|
|
737
|
+
job.status = 'running';
|
|
738
|
+
state.activeJobIds.add(job.id);
|
|
739
|
+
if (!send(assignment)) {
|
|
740
|
+
state.activeJobIds.delete(job.id);
|
|
741
|
+
clearRoverLease(job);
|
|
742
|
+
job.status = 'waiting_for_rover';
|
|
743
|
+
if (index !== -1)
|
|
744
|
+
state.queue.splice(index, 0, job.id);
|
|
745
|
+
return null;
|
|
746
|
+
}
|
|
747
|
+
appendRoverTraceEvent(job, assignment, provider);
|
|
748
|
+
if (!leaseCapable)
|
|
749
|
+
markRoverStarted(job);
|
|
750
|
+
emit();
|
|
751
|
+
return job;
|
|
752
|
+
},
|
|
753
|
+
rebindRoverLeases(provider, worker) {
|
|
754
|
+
const now = Date.now();
|
|
755
|
+
for (const job of jobs.values()) {
|
|
756
|
+
if (job.runParams.executionType !== 'rover' ||
|
|
757
|
+
job.runParams.roverAgent.provider !== provider ||
|
|
758
|
+
!job.roverLease ||
|
|
759
|
+
job.status === 'completed' ||
|
|
760
|
+
job.status === 'stopped' ||
|
|
761
|
+
job.status === 'error')
|
|
762
|
+
continue;
|
|
763
|
+
job.roverLease.connectionId = worker.connectionId;
|
|
764
|
+
job.roverLease.registrationVersion = worker.registrationVersion;
|
|
765
|
+
job.roverLease.expiresAt = new Date(now + 30_000).toISOString();
|
|
766
|
+
scheduleLeaseExpiry(job);
|
|
767
|
+
}
|
|
768
|
+
},
|
|
769
|
+
pauseRoverJob(jobId, connectionId) {
|
|
770
|
+
const job = jobs.get(jobId);
|
|
771
|
+
if (!job || job.runParams.executionType !== 'rover' || job.status !== 'running')
|
|
772
|
+
return;
|
|
773
|
+
if (connectionId && job.roverLease && job.roverLease.connectionId !== connectionId)
|
|
774
|
+
return;
|
|
775
|
+
state.activeJobIds.delete(jobId);
|
|
776
|
+
job.status = 'paused_rover';
|
|
777
|
+
state.queue.unshift(jobId);
|
|
778
|
+
deps.addJobEvent(job, {
|
|
779
|
+
type: 'log',
|
|
780
|
+
ts: new Date().toISOString(),
|
|
781
|
+
payload: { message: 'Rover disconnected. Job paused until Rover reconnects.' }
|
|
782
|
+
});
|
|
783
|
+
emit();
|
|
784
|
+
},
|
|
785
|
+
resumeRoverJob(jobId) {
|
|
786
|
+
const job = jobs.get(jobId);
|
|
787
|
+
if (!job || job.runParams.executionType !== 'rover' || job.status !== 'paused_rover')
|
|
788
|
+
return false;
|
|
789
|
+
job.status = 'waiting_for_rover';
|
|
790
|
+
if (!state.queue.includes(jobId))
|
|
791
|
+
state.queue.push(jobId);
|
|
792
|
+
deps.addJobEvent(job, {
|
|
793
|
+
type: 'log',
|
|
794
|
+
ts: new Date().toISOString(),
|
|
795
|
+
payload: { message: 'Rover job resumed and waiting for Rover connection.' }
|
|
796
|
+
});
|
|
797
|
+
emit();
|
|
798
|
+
params.assignRoverJob?.(job.runParams.roverAgent.provider);
|
|
799
|
+
return true;
|
|
800
|
+
},
|
|
801
|
+
completeRoverJob(jobId, payload = {}) {
|
|
802
|
+
const job = jobs.get(jobId);
|
|
803
|
+
if (!job ||
|
|
804
|
+
job.runParams.executionType !== 'rover' ||
|
|
805
|
+
(job.status !== 'running' && job.status !== 'paused_rover'))
|
|
806
|
+
return;
|
|
807
|
+
finalizeRoverJob(job, { kind: 'completed', payload });
|
|
808
|
+
},
|
|
809
|
+
stopRoverScenario(jobId, scenarioId) {
|
|
810
|
+
const job = jobs.get(jobId);
|
|
811
|
+
if (!job || job.runParams.executionType !== 'rover')
|
|
812
|
+
return null;
|
|
813
|
+
if (!scenarioId.trim())
|
|
814
|
+
return { error: 'Scenario ID is required', statusCode: 400 };
|
|
815
|
+
const child = (job.childProgress ?? []).find((entry) => entry.scenarioId === scenarioId);
|
|
816
|
+
if (child?.status === 'completed' || child?.status === 'stopped') {
|
|
817
|
+
return { ok: true, status: 'stopped' };
|
|
818
|
+
}
|
|
819
|
+
if (job.status === 'queued' ||
|
|
820
|
+
job.status === 'waiting_for_rover' ||
|
|
821
|
+
job.status === 'paused_rover') {
|
|
822
|
+
// Revoke paused assignments before editing their scenario list. Reconnection
|
|
823
|
+
// must offer the remaining work, never resume a lease containing cancelled work.
|
|
824
|
+
if (!child)
|
|
825
|
+
return { error: 'Scenario not found', statusCode: 404 };
|
|
826
|
+
if (job.status === 'paused_rover') {
|
|
827
|
+
clearRoverLease(job);
|
|
828
|
+
job.status = 'waiting_for_rover';
|
|
829
|
+
}
|
|
830
|
+
job.runParams.roverScenarios = job.runParams.roverScenarios?.filter((scenario) => scenario.id !== scenarioId &&
|
|
831
|
+
!(job.childProgress ?? []).some((entry) => entry.scenarioId === scenario.id &&
|
|
832
|
+
['completed', 'error', 'stopped'].includes(entry.status)));
|
|
833
|
+
}
|
|
834
|
+
else {
|
|
835
|
+
const sent = params.sendRoverMessage?.({
|
|
836
|
+
type: 'stop_scenario',
|
|
837
|
+
jobId,
|
|
838
|
+
scenarioId
|
|
839
|
+
});
|
|
840
|
+
if (sent === false)
|
|
841
|
+
return { error: 'Rover is not connected', statusCode: 409 };
|
|
842
|
+
}
|
|
843
|
+
const progress = job.childProgress ?? [];
|
|
844
|
+
const next = {
|
|
845
|
+
scenarioId,
|
|
846
|
+
agentName: job.runParams.roverAgent.name,
|
|
847
|
+
completed: child?.completed ?? 0,
|
|
848
|
+
total: child?.total ?? 1,
|
|
849
|
+
status: 'stopped'
|
|
850
|
+
};
|
|
851
|
+
job.childProgress = upsertQueueChildProgress(progress, { ...next });
|
|
852
|
+
const children = job.childProgress ?? [];
|
|
853
|
+
const aggregateTerminal = children.length > 0 &&
|
|
854
|
+
children.every((entry) => ['completed', 'error', 'stopped'].includes(entry.status));
|
|
855
|
+
if (aggregateTerminal) {
|
|
856
|
+
finalizeRoverJob(job, { kind: 'stopped', reason: 'Run stopped by user' });
|
|
857
|
+
return { ok: true, status: 'stopped' };
|
|
858
|
+
}
|
|
859
|
+
emit();
|
|
860
|
+
return { ok: true, status: 'stopped' };
|
|
861
|
+
},
|
|
862
|
+
stopScenario(jobId, scenarioId, agentName) {
|
|
863
|
+
const job = jobs.get(jobId);
|
|
864
|
+
if (!job || !scenarioId.trim())
|
|
865
|
+
return null;
|
|
866
|
+
const matches = (job.childProgress ?? []).filter((entry) => entry.scenarioId === scenarioId);
|
|
867
|
+
if (matches.length > 1 && !agentName) {
|
|
868
|
+
return {
|
|
869
|
+
error: 'Agent name is required when multiple agents run this scenario',
|
|
870
|
+
statusCode: 400
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
const child = matches.find((entry) => !agentName || entry.agentName === agentName);
|
|
874
|
+
if (matches.length > 0 && !child)
|
|
875
|
+
return { error: 'Scenario-agent child not found', statusCode: 404 };
|
|
876
|
+
if (child?.status === 'completed' || child?.status === 'stopped') {
|
|
877
|
+
return { ok: true, status: 'stopped' };
|
|
878
|
+
}
|
|
879
|
+
const resolvedAgentName = child?.agentName ??
|
|
880
|
+
(job.runParams.executionType === 'rover' ? job.runParams.roverAgent.name : undefined);
|
|
881
|
+
if (job.runParams.executionType === 'rover') {
|
|
882
|
+
return this.stopRoverScenario(jobId, scenarioId);
|
|
883
|
+
}
|
|
884
|
+
if (!resolvedAgentName)
|
|
885
|
+
return { error: 'Scenario is not initialized', statusCode: 409 };
|
|
886
|
+
const controller = job.childAbortControllers?.get(`${scenarioId}:${resolvedAgentName}`);
|
|
887
|
+
if (!controller)
|
|
888
|
+
return { error: 'Scenario is not running', statusCode: 409 };
|
|
889
|
+
controller.abort();
|
|
890
|
+
const progress = job.childProgress ?? [];
|
|
891
|
+
job.childProgress = upsertQueueChildProgress(progress, {
|
|
892
|
+
...child,
|
|
893
|
+
status: 'stopped'
|
|
894
|
+
});
|
|
895
|
+
emit();
|
|
896
|
+
return { ok: true, status: 'stopped' };
|
|
897
|
+
},
|
|
898
|
+
handleRoverMessage(message, provider, send, worker) {
|
|
899
|
+
const messageJob = typeof message.jobId === 'string' ? jobs.get(message.jobId) : undefined;
|
|
900
|
+
if (typeof message.jobId === 'string' && typeof message.leaseId === 'string') {
|
|
901
|
+
const knownJob = jobs.get(message.jobId);
|
|
902
|
+
const knownLease = knownJob?.roverLease;
|
|
903
|
+
if (!knownJob ||
|
|
904
|
+
!knownLease ||
|
|
905
|
+
knownLease.leaseId !== message.leaseId ||
|
|
906
|
+
(worker && knownLease.connectionId !== worker.connectionId)) {
|
|
907
|
+
send({
|
|
908
|
+
type: 'lease_unknown',
|
|
909
|
+
jobId: message.jobId,
|
|
910
|
+
leaseId: message.leaseId,
|
|
911
|
+
reason: 'unknown_lease'
|
|
912
|
+
});
|
|
913
|
+
return null;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
if (messageJob?.roverLease &&
|
|
917
|
+
(message.leaseId !== messageJob.roverLease.leaseId ||
|
|
918
|
+
(worker && messageJob.roverLease.connectionId !== worker.connectionId)))
|
|
919
|
+
return null;
|
|
920
|
+
appendRoverTraceEvent(messageJob, message, provider);
|
|
921
|
+
if ((message.type === 'assignment_accept' ||
|
|
922
|
+
message.type === 'assignment_reject' ||
|
|
923
|
+
message.type === 'lease_renew' ||
|
|
924
|
+
message.type === 'lease_release') &&
|
|
925
|
+
typeof message.jobId === 'string' &&
|
|
926
|
+
typeof message.leaseId === 'string') {
|
|
927
|
+
const job = jobs.get(message.jobId);
|
|
928
|
+
const lease = job?.roverLease;
|
|
929
|
+
if (!job ||
|
|
930
|
+
!lease ||
|
|
931
|
+
lease.leaseId !== message.leaseId ||
|
|
932
|
+
(worker && lease.connectionId !== worker.connectionId))
|
|
933
|
+
return null;
|
|
934
|
+
if (message.type === 'assignment_accept' && lease.state === 'offered') {
|
|
935
|
+
lease.state = 'accepted';
|
|
936
|
+
lease.acceptedAt = new Date().toISOString();
|
|
937
|
+
if (typeof message.tabId === 'number')
|
|
938
|
+
lease.tabId = message.tabId;
|
|
939
|
+
scheduleLeaseExpiry(job);
|
|
940
|
+
markRoverStarted(job);
|
|
941
|
+
emit();
|
|
942
|
+
return null;
|
|
943
|
+
}
|
|
944
|
+
if (message.type === 'lease_renew' && lease.state !== 'offered') {
|
|
945
|
+
const requested = Date.parse(String(message.leaseExpiresAt ?? ''));
|
|
946
|
+
if (!Number.isFinite(requested) || requested <= Date.now())
|
|
947
|
+
return null;
|
|
948
|
+
lease.expiresAt = new Date(Math.min(requested, Date.now() + 30_000)).toISOString();
|
|
949
|
+
scheduleLeaseExpiry(job);
|
|
950
|
+
emit();
|
|
951
|
+
return null;
|
|
952
|
+
}
|
|
953
|
+
if (message.type === 'assignment_reject' && lease.state === 'offered') {
|
|
954
|
+
const detail = typeof message.message === 'string' ? message.message.trim() : '';
|
|
955
|
+
const providerUnavailable = message.reason === 'provider_mismatch' || message.reason === 'provider_unavailable';
|
|
956
|
+
if (providerUnavailable && message.retryable !== false) {
|
|
957
|
+
rejectedRoverRegistration = {
|
|
958
|
+
connectionId: lease.connectionId,
|
|
959
|
+
version: lease.registrationVersion ?? 0
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
// A missing provider capability needs a configuration change, not another offer.
|
|
963
|
+
// Older Rover versions omit retryable, so preserve their requeue behavior.
|
|
964
|
+
finalizeRoverJob(job, {
|
|
965
|
+
kind: message.retryable === false ? 'error' : 'requeue',
|
|
966
|
+
reason: `assignment_rejected:${String(message.reason ?? 'unknown')}${detail ? `. ${detail}` : ''}${providerUnavailable && message.retryable !== false ? `. Waiting for Rover to confirm provider ${job.runParams.roverAgent?.provider}. Open its browser agent tab.` : ''}`
|
|
967
|
+
});
|
|
968
|
+
return null;
|
|
969
|
+
}
|
|
970
|
+
if (message.type === 'lease_release') {
|
|
971
|
+
const reason = String(message.reason ?? 'error');
|
|
972
|
+
const shouldRequeue = ROVER_LEASE_REQUEUE_REASONS.includes(reason) ||
|
|
973
|
+
(reason === 'error' && job.status === 'running');
|
|
974
|
+
if (shouldRequeue && job.status === 'running')
|
|
975
|
+
finalizeRoverJob(job, { kind: 'requeue', reason });
|
|
976
|
+
else if (ROVER_LEASE_TERMINAL_REASONS.includes(reason))
|
|
977
|
+
finalizeRoverJob(job, { kind: 'error', reason });
|
|
978
|
+
send({
|
|
979
|
+
type: 'lease_action_ack',
|
|
980
|
+
jobId: job.id,
|
|
981
|
+
leaseId: message.leaseId,
|
|
982
|
+
action: 'release'
|
|
983
|
+
});
|
|
984
|
+
return null;
|
|
985
|
+
}
|
|
986
|
+
return null;
|
|
987
|
+
}
|
|
988
|
+
if (typeof message.jobId === 'string' && typeof message.leaseId === 'string') {
|
|
989
|
+
const leasedJob = jobs.get(message.jobId);
|
|
990
|
+
if (leasedJob?.roverLease && leasedJob.roverLease.leaseId !== message.leaseId)
|
|
991
|
+
return null;
|
|
992
|
+
if (leasedJob?.roverLease?.state === 'accepted' &&
|
|
993
|
+
(message.type === 'progress' ||
|
|
994
|
+
message.type === 'stage' ||
|
|
995
|
+
(message.type === 'scenario_status' && message.status === 'running')) &&
|
|
996
|
+
(!worker || leasedJob.roverLease.connectionId === worker.connectionId)) {
|
|
997
|
+
leasedJob.roverLease.state = 'running';
|
|
998
|
+
emit();
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
if (message.type === 'scenario_status' && typeof message.jobId === 'string') {
|
|
1002
|
+
const job = jobs.get(message.jobId);
|
|
1003
|
+
const roverAgent = job?.runParams.executionType === 'rover' ? job.runParams.roverAgent : undefined;
|
|
1004
|
+
if (job?.runParams.executionType === 'rover' &&
|
|
1005
|
+
roverAgent &&
|
|
1006
|
+
typeof message.scenarioId === 'string') {
|
|
1007
|
+
const status = String(message.status ?? 'running');
|
|
1008
|
+
const allowed = new Set(['queued', 'running', 'completed', 'error', 'stopped']);
|
|
1009
|
+
if (!allowed.has(status))
|
|
1010
|
+
return null;
|
|
1011
|
+
const existing = job.childProgress ?? [];
|
|
1012
|
+
const current = existing.find((child) => child.scenarioId === message.scenarioId && child.agentName === roverAgent.name);
|
|
1013
|
+
const next = {
|
|
1014
|
+
scenarioId: message.scenarioId,
|
|
1015
|
+
agentName: roverAgent.name,
|
|
1016
|
+
completed: Math.max(0, Number(message.completed ?? 0)),
|
|
1017
|
+
total: Math.max(1, Number(message.total ?? 1)),
|
|
1018
|
+
status: status,
|
|
1019
|
+
...(typeof message.lastDurationMs === 'number'
|
|
1020
|
+
? { lastDurationMs: Math.max(0, message.lastDurationMs) }
|
|
1021
|
+
: {}),
|
|
1022
|
+
...(typeof message.error === 'string' ? { error: message.error } : {})
|
|
1023
|
+
};
|
|
1024
|
+
const terminal = new Set(['completed', 'error', 'stopped']);
|
|
1025
|
+
if (current && terminal.has(current.status) && !terminal.has(next.status)) {
|
|
1026
|
+
return null;
|
|
1027
|
+
}
|
|
1028
|
+
if (current && next.completed < current.completed)
|
|
1029
|
+
return null;
|
|
1030
|
+
job.childProgress = upsertQueueChildProgress(existing, next);
|
|
1031
|
+
emit();
|
|
1032
|
+
}
|
|
1033
|
+
return null;
|
|
1034
|
+
}
|
|
1035
|
+
if (message.type === 'stage' && typeof message.jobId === 'string') {
|
|
1036
|
+
const job = jobs.get(message.jobId);
|
|
1037
|
+
const stage = typeof message.stage === 'string' ? message.stage : 'progress';
|
|
1038
|
+
const scenario = typeof message.scenarioId === 'string' ? ` (${message.scenarioId})` : '';
|
|
1039
|
+
const labels = {
|
|
1040
|
+
prompt_sent: 'Prompt sent to Rover',
|
|
1041
|
+
waiting_for_response: 'Waiting for the agent response',
|
|
1042
|
+
response_captured: 'Response captured from Rover',
|
|
1043
|
+
evaluating: 'Evaluating response in MCPLab',
|
|
1044
|
+
persisted: 'Result persisted in MCPLab'
|
|
1045
|
+
};
|
|
1046
|
+
if (job?.runParams.executionType === 'rover') {
|
|
1047
|
+
deps.addJobEvent(job, {
|
|
1048
|
+
type: 'log',
|
|
1049
|
+
ts: new Date().toISOString(),
|
|
1050
|
+
payload: {
|
|
1051
|
+
message: `${labels[stage] ?? String(message.message ?? 'Rover progress')}${scenario} [agent=${job.runParams.executionType === 'rover' ? job.runParams.roverAgent.name : 'unknown'}]`
|
|
1052
|
+
}
|
|
1053
|
+
});
|
|
1054
|
+
emit();
|
|
1055
|
+
}
|
|
1056
|
+
return null;
|
|
1057
|
+
}
|
|
1058
|
+
if (message.type === 'progress' && typeof message.jobId === 'string') {
|
|
1059
|
+
const job = jobs.get(message.jobId);
|
|
1060
|
+
if (job?.runParams.executionType === 'rover') {
|
|
1061
|
+
if (typeof message.completed === 'number' && typeof message.total === 'number') {
|
|
1062
|
+
const total = Math.max(0, message.total);
|
|
1063
|
+
job.roverProgress = {
|
|
1064
|
+
completed: Math.max(0, Math.min(message.completed, total)),
|
|
1065
|
+
total,
|
|
1066
|
+
...(typeof message.currentScenarioId === 'string'
|
|
1067
|
+
? { currentScenarioId: message.currentScenarioId }
|
|
1068
|
+
: {}),
|
|
1069
|
+
...(typeof message.lastDurationMs === 'number'
|
|
1070
|
+
? { lastDurationMs: Math.max(0, message.lastDurationMs) }
|
|
1071
|
+
: {}),
|
|
1072
|
+
...(typeof message.error === 'string' ? { error: message.error } : {})
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
deps.addJobEvent(job, {
|
|
1076
|
+
type: 'log',
|
|
1077
|
+
ts: new Date().toISOString(),
|
|
1078
|
+
payload: {
|
|
1079
|
+
message: String(message.message ??
|
|
1080
|
+
(message.error ? `Rover error: ${message.error}` : 'Rover progress'))
|
|
1081
|
+
}
|
|
1082
|
+
});
|
|
1083
|
+
if (typeof message.error === 'string' && job.status === 'running' && !job.roverLease) {
|
|
1084
|
+
finalizeRoverJob(job, { kind: 'error', reason: message.error });
|
|
1085
|
+
}
|
|
1086
|
+
emit();
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
if (message.type !== 'complete' || typeof message.jobId !== 'string')
|
|
1090
|
+
return null;
|
|
1091
|
+
const completedJob = jobs.get(message.jobId);
|
|
1092
|
+
this.completeRoverJob(message.jobId, {
|
|
1093
|
+
runId: completedJob?.runParams.evaluationRunId ?? message.runId,
|
|
1094
|
+
outcome: message.outcome,
|
|
1095
|
+
provider
|
|
1096
|
+
});
|
|
1097
|
+
if (typeof message.leaseId === 'string') {
|
|
1098
|
+
send({
|
|
1099
|
+
type: 'lease_action_ack',
|
|
1100
|
+
jobId: message.jobId,
|
|
1101
|
+
leaseId: message.leaseId,
|
|
1102
|
+
action: 'complete'
|
|
1103
|
+
});
|
|
1104
|
+
}
|
|
1105
|
+
const next = this.assignRoverJob(provider, send, worker);
|
|
1106
|
+
return next?.id ?? null;
|
|
393
1107
|
}
|
|
394
1108
|
};
|
|
395
1109
|
}
|