@inspectr/mcplab 1.28.5 → 1.30.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.
Files changed (181) hide show
  1. package/README.md +74 -6
  2. package/dist/app/assets/index-8I9ZyPIK.js +243 -0
  3. package/dist/app/assets/index-jX7LoIQP.css +2 -0
  4. package/dist/app/assets/sse-queue-worker-Iwmqd2oW.js +1 -0
  5. package/dist/app/index.html +2 -2
  6. package/dist/app-server/app-context.d.ts +75 -115
  7. package/dist/app-server/app-context.d.ts.map +1 -1
  8. package/dist/app-server/app-dist.d.ts +3 -3
  9. package/dist/app-server/app-dist.js.map +1 -1
  10. package/dist/app-server/app-run-artifacts.d.ts +4 -0
  11. package/dist/app-server/app-run-artifacts.d.ts.map +1 -0
  12. package/dist/app-server/app-run-artifacts.js +66 -0
  13. package/dist/app-server/app-run-artifacts.js.map +1 -0
  14. package/dist/app-server/assistant-common.d.ts +27 -37
  15. package/dist/app-server/assistant-common.d.ts.map +1 -1
  16. package/dist/app-server/assistant-common.js +110 -1
  17. package/dist/app-server/assistant-common.js.map +1 -1
  18. package/dist/app-server/assistant-events.d.ts +8 -28
  19. package/dist/app-server/browser-launch.d.ts +3 -3
  20. package/dist/app-server/browser-launch.js.map +1 -1
  21. package/dist/app-server/browser-provider-learning.d.ts +26 -0
  22. package/dist/app-server/browser-provider-learning.d.ts.map +1 -0
  23. package/dist/app-server/browser-provider-learning.js +353 -0
  24. package/dist/app-server/browser-provider-learning.js.map +1 -0
  25. package/dist/app-server/config-store.d.ts +13 -21
  26. package/dist/app-server/connection-test-routes.d.ts +8 -0
  27. package/dist/app-server/connection-test-routes.d.ts.map +1 -0
  28. package/dist/app-server/connection-test-routes.js +34 -0
  29. package/dist/app-server/connection-test-routes.js.map +1 -0
  30. package/dist/app-server/decision-routes.d.ts +8 -0
  31. package/dist/app-server/decision-routes.d.ts.map +1 -0
  32. package/dist/app-server/decision-routes.js +27 -0
  33. package/dist/app-server/decision-routes.js.map +1 -0
  34. package/dist/app-server/dev-mcp.d.ts +2 -5
  35. package/dist/app-server/evals-routes.d.ts +8 -18
  36. package/dist/app-server/evaluation-group-results.d.ts +11 -0
  37. package/dist/app-server/evaluation-group-results.d.ts.map +1 -0
  38. package/dist/app-server/evaluation-group-results.js +46 -0
  39. package/dist/app-server/evaluation-group-results.js.map +1 -0
  40. package/dist/app-server/evaluation-journal-projection.d.ts +9 -0
  41. package/dist/app-server/evaluation-journal-projection.d.ts.map +1 -0
  42. package/dist/app-server/evaluation-journal-projection.js +76 -0
  43. package/dist/app-server/evaluation-journal-projection.js.map +1 -0
  44. package/dist/app-server/evaluation-journal-writer.d.ts +23 -0
  45. package/dist/app-server/evaluation-journal-writer.d.ts.map +1 -0
  46. package/dist/app-server/evaluation-journal-writer.js +41 -0
  47. package/dist/app-server/evaluation-journal-writer.js.map +1 -0
  48. package/dist/app-server/evaluation-result-projection.d.ts +12 -0
  49. package/dist/app-server/evaluation-result-projection.d.ts.map +1 -0
  50. package/dist/app-server/evaluation-result-projection.js +107 -0
  51. package/dist/app-server/evaluation-result-projection.js.map +1 -0
  52. package/dist/app-server/execution-journal.d.ts +13 -0
  53. package/dist/app-server/execution-journal.d.ts.map +1 -0
  54. package/dist/app-server/execution-journal.js +39 -0
  55. package/dist/app-server/execution-journal.js.map +1 -0
  56. package/dist/app-server/http.d.ts +1 -1
  57. package/dist/app-server/index.d.ts +1 -1
  58. package/dist/app-server/inspectr-session-manager.d.ts +45 -0
  59. package/dist/app-server/inspectr-session-manager.d.ts.map +1 -0
  60. package/dist/app-server/inspectr-session-manager.js +203 -0
  61. package/dist/app-server/inspectr-session-manager.js.map +1 -0
  62. package/dist/app-server/jobs.d.ts +6 -9
  63. package/dist/app-server/libraries-store.d.ts +26 -10
  64. package/dist/app-server/libraries-store.d.ts.map +1 -1
  65. package/dist/app-server/libraries-store.js +130 -3
  66. package/dist/app-server/libraries-store.js.map +1 -1
  67. package/dist/app-server/live-tests-routes.d.ts +17 -0
  68. package/dist/app-server/live-tests-routes.d.ts.map +1 -0
  69. package/dist/app-server/live-tests-routes.js +82 -0
  70. package/dist/app-server/live-tests-routes.js.map +1 -0
  71. package/dist/app-server/live-tests.d.ts +78 -0
  72. package/dist/app-server/live-tests.d.ts.map +1 -0
  73. package/dist/app-server/live-tests.js +285 -0
  74. package/dist/app-server/live-tests.js.map +1 -0
  75. package/dist/app-server/markdown-reports.d.ts +7 -7
  76. package/dist/app-server/oauth-debugger-domain.d.ts +183 -207
  77. package/dist/app-server/oauth-debugger-domain.js.map +1 -1
  78. package/dist/app-server/oauth-debugger.d.ts +10 -17
  79. package/dist/app-server/oauth-debugger.js +1 -1
  80. package/dist/app-server/oauth-debugger.js.map +1 -1
  81. package/dist/app-server/oauth-runtime-domain.d.ts +42 -53
  82. package/dist/app-server/oauth-runtime-domain.js.map +1 -1
  83. package/dist/app-server/oauth-runtime-routes.d.ts +12 -12
  84. package/dist/app-server/oauth-session-manager.d.ts +44 -50
  85. package/dist/app-server/oauth-session-manager.d.ts.map +1 -1
  86. package/dist/app-server/oauth-session-manager.js.map +1 -1
  87. package/dist/app-server/provider-models.d.ts +2 -7
  88. package/dist/app-server/provider-models.d.ts.map +1 -1
  89. package/dist/app-server/provider-models.js +11 -12
  90. package/dist/app-server/provider-models.js.map +1 -1
  91. package/dist/app-server/request-origin.d.ts +6 -0
  92. package/dist/app-server/request-origin.d.ts.map +1 -0
  93. package/dist/app-server/request-origin.js +61 -0
  94. package/dist/app-server/request-origin.js.map +1 -0
  95. package/dist/app-server/result-assistant-domain.d.ts +66 -82
  96. package/dist/app-server/result-assistant-domain.d.ts.map +1 -1
  97. package/dist/app-server/result-assistant-domain.js.map +1 -1
  98. package/dist/app-server/result-assistant-tools.d.ts +5 -5
  99. package/dist/app-server/result-assistant.d.ts +9 -21
  100. package/dist/app-server/result-assistant.js.map +1 -1
  101. package/dist/app-server/router.d.ts +7 -1
  102. package/dist/app-server/router.d.ts.map +1 -1
  103. package/dist/app-server/router.js +375 -57
  104. package/dist/app-server/router.js.map +1 -1
  105. package/dist/app-server/rover-connection.d.ts +48 -0
  106. package/dist/app-server/rover-connection.d.ts.map +1 -0
  107. package/dist/app-server/rover-connection.js +172 -0
  108. package/dist/app-server/rover-connection.js.map +1 -0
  109. package/dist/app-server/run-agent-selection.d.ts +2 -5
  110. package/dist/app-server/run-queue-domain.d.ts +89 -65
  111. package/dist/app-server/run-queue-domain.d.ts.map +1 -1
  112. package/dist/app-server/run-queue-domain.js +723 -9
  113. package/dist/app-server/run-queue-domain.js.map +1 -1
  114. package/dist/app-server/run-queue-events.d.ts +3 -10
  115. package/dist/app-server/run-queue-events.d.ts.map +1 -1
  116. package/dist/app-server/run-queue-events.js +114 -14
  117. package/dist/app-server/run-queue-events.js.map +1 -1
  118. package/dist/app-server/run-queue-executor.d.ts +22 -48
  119. package/dist/app-server/run-queue-executor.d.ts.map +1 -1
  120. package/dist/app-server/run-queue-executor.js +91 -30
  121. package/dist/app-server/run-queue-executor.js.map +1 -1
  122. package/dist/app-server/run-queue-state.d.ts +81 -47
  123. package/dist/app-server/run-queue-state.d.ts.map +1 -1
  124. package/dist/app-server/run-queue-state.js.map +1 -1
  125. package/dist/app-server/runs-routes.d.ts +13 -35
  126. package/dist/app-server/runs-routes.d.ts.map +1 -1
  127. package/dist/app-server/runs-routes.js +196 -29
  128. package/dist/app-server/runs-routes.js.map +1 -1
  129. package/dist/app-server/runs-routes.test-helpers.d.ts +42 -38
  130. package/dist/app-server/runs-routes.test-helpers.d.ts.map +1 -1
  131. package/dist/app-server/runs-routes.test-helpers.js +4 -1
  132. package/dist/app-server/runs-routes.test-helpers.js.map +1 -1
  133. package/dist/app-server/runs-store.d.ts +60 -43
  134. package/dist/app-server/runs-store.d.ts.map +1 -1
  135. package/dist/app-server/runs-store.js +157 -70
  136. package/dist/app-server/runs-store.js.map +1 -1
  137. package/dist/app-server/scenario-assistant-domain.d.ts +131 -157
  138. package/dist/app-server/scenario-assistant-domain.d.ts.map +1 -1
  139. package/dist/app-server/scenario-assistant-domain.js +7 -15
  140. package/dist/app-server/scenario-assistant-domain.js.map +1 -1
  141. package/dist/app-server/scenario-assistant.d.ts +10 -26
  142. package/dist/app-server/scenario-assistant.js.map +1 -1
  143. package/dist/app-server/settings-store.d.ts +4 -8
  144. package/dist/app-server/settings-store.d.ts.map +1 -1
  145. package/dist/app-server/settings-store.js +33 -15
  146. package/dist/app-server/settings-store.js.map +1 -1
  147. package/dist/app-server/static-serving.d.ts +7 -13
  148. package/dist/app-server/store-utils.d.ts +1 -1
  149. package/dist/app-server/tool-analysis-domain.d.ts +167 -183
  150. package/dist/app-server/tool-analysis-domain.d.ts.map +1 -1
  151. package/dist/app-server/tool-analysis-domain.js +2 -59
  152. package/dist/app-server/tool-analysis-domain.js.map +1 -1
  153. package/dist/app-server/tool-analysis-storage.d.ts +20 -34
  154. package/dist/app-server/tool-analysis.d.ts +10 -19
  155. package/dist/app-server/types.d.ts +26 -27
  156. package/dist/app-server/types.d.ts.map +1 -1
  157. package/dist/app-server/version-info.d.ts +6 -6
  158. package/dist/app-server/version-info.js.map +1 -1
  159. package/dist/app-server/workspace-paths.d.ts +2 -1
  160. package/dist/app-server/workspace-paths.d.ts.map +1 -1
  161. package/dist/app-server/workspace-paths.js +5 -1
  162. package/dist/app-server/workspace-paths.js.map +1 -1
  163. package/dist/cli-branding.d.ts +1 -1
  164. package/dist/cli-branding.js.map +1 -1
  165. package/dist/cli.d.ts +1 -1
  166. package/dist/cli.js +5 -6
  167. package/dist/cli.js.map +1 -1
  168. package/dist/eval-config-files.d.ts +5 -8
  169. package/dist/interactive-helpers.d.ts +11 -15
  170. package/dist/migrate-utils.d.ts +1 -1
  171. package/dist/results/context.d.ts +1 -1
  172. package/dist/results/format.d.ts +7 -14
  173. package/dist/results/indexer.d.ts +2 -9
  174. package/dist/results/search.d.ts +1 -1
  175. package/dist/results/types.d.ts +2 -11
  176. package/dist/run-interactive.d.ts +21 -23
  177. package/dist/test-results-fixture.d.ts +4 -4
  178. package/package.json +8 -6
  179. package/dist/app/assets/index-Bxq-wlTN.css +0 -1
  180. package/dist/app/assets/index-j_NpCqyK.js +0 -258
  181. 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' && nextJob.status !== 'blocked_auth') {
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 (!shouldStartImmediately) {
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' || job.status === 'blocked_auth') {
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' && job.status !== 'blocked_auth') {
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
  }