parallel-codex-tui 0.3.1 → 0.4.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 +19 -8
- package/dist/cli.js +22 -2
- package/dist/core/session-index.js +123 -10
- package/dist/core/session-manager.js +15 -4
- package/dist/core/task-report.js +510 -0
- package/dist/core/task-search.js +142 -0
- package/dist/orchestrator/orchestrator.js +26 -4
- package/dist/supervisor/client.js +442 -0
- package/dist/supervisor/protocol.js +177 -0
- package/dist/supervisor/runner.js +238 -0
- package/dist/supervisor/store.js +213 -0
- package/dist/tui/App.js +169 -36
- package/dist/tui/AppShell.js +7 -3
- package/dist/tui/InputBar.js +20 -6
- package/dist/tui/TaskSessionsView.js +30 -9
- package/dist/version.js +1 -1
- package/dist/workers/mock-adapter.js +18 -0
- package/package.json +1 -1
|
@@ -75,10 +75,17 @@ export class Orchestrator {
|
|
|
75
75
|
}
|
|
76
76
|
async handleRequest(input) {
|
|
77
77
|
throwIfCancelled(input.signal);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
let roleSelection;
|
|
79
|
+
let route;
|
|
80
|
+
if (input.route) {
|
|
81
|
+
roleSelection = input.roleSelection ?? await this.roleConfiguration.selectionForRequest();
|
|
82
|
+
route = routeWithRoleSelection(input.route, roleSelection);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
const routed = await this.routeInitialRequest(input);
|
|
86
|
+
roleSelection = routed.roleSelection;
|
|
87
|
+
route = routed.route;
|
|
88
|
+
}
|
|
82
89
|
throwIfCancelled(input.signal);
|
|
83
90
|
const workers = [];
|
|
84
91
|
const executionInput = { ...input, roleSelection };
|
|
@@ -115,6 +122,21 @@ export class Orchestrator {
|
|
|
115
122
|
await this.roleConfiguration.writeTurnSelection(turn.dir, roleSelectionWithEngines(route, roleSelection));
|
|
116
123
|
return this.withTaskRunLease(task, () => this.runInitialTask(executionInput, task, route, turn, workers));
|
|
117
124
|
}
|
|
125
|
+
async routeInitialRequest(input) {
|
|
126
|
+
throwIfCancelled(input.signal);
|
|
127
|
+
const routed = await this.routeRequest(input.request, input.cwd, input.signal, "initial", input.onRouteStart, input.onRouteFallback, input.onRouteProgress);
|
|
128
|
+
const roleSelection = input.roleSelection ?? await this.roleConfiguration.selectionForRequest();
|
|
129
|
+
const route = routeWithRoleSelection(routed, roleSelection);
|
|
130
|
+
input.onRoute?.(route);
|
|
131
|
+
throwIfCancelled(input.signal);
|
|
132
|
+
return {
|
|
133
|
+
mode: route.mode,
|
|
134
|
+
taskId: null,
|
|
135
|
+
reason: route.reason,
|
|
136
|
+
route,
|
|
137
|
+
roleSelection
|
|
138
|
+
};
|
|
139
|
+
}
|
|
118
140
|
async handleTaskTurn(input) {
|
|
119
141
|
throwIfCancelled(input.signal);
|
|
120
142
|
const task = this.sessions.taskFromId(input.taskId);
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { closeSync, openSync } from "node:fs";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { asSupervisorRunResult, supervisorEventPayload } from "./protocol.js";
|
|
6
|
+
import { acknowledgeSupervisorRun, appendSupervisorCommand, claimSupervisorController, createSupervisorRun, createSupervisorRunId, listSupervisorRuns, readSupervisorEvents, readSupervisorRunState, supervisorRunIsAcknowledged, supervisorRunIsTerminal, supervisorRunProcessIsActive, writeSupervisorRunState } from "./store.js";
|
|
7
|
+
const RUN_POLL_MS = 100;
|
|
8
|
+
const QUEUED_START_GRACE_MS = 3000;
|
|
9
|
+
export class SupervisorDetachedError extends Error {
|
|
10
|
+
constructor(message = "Background run detached") {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "SupervisorDetachedError";
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function isSupervisorDetachedError(error) {
|
|
16
|
+
return error instanceof Error && error.name === "SupervisorDetachedError";
|
|
17
|
+
}
|
|
18
|
+
export class SupervisorOrchestrator {
|
|
19
|
+
options;
|
|
20
|
+
persistsRunResults = true;
|
|
21
|
+
launch;
|
|
22
|
+
now;
|
|
23
|
+
recoverableRun = null;
|
|
24
|
+
currentRun = null;
|
|
25
|
+
settledRun = null;
|
|
26
|
+
detaching = false;
|
|
27
|
+
backgroundRunListeners = new Set();
|
|
28
|
+
constructor(options) {
|
|
29
|
+
this.options = options;
|
|
30
|
+
this.launch = options.launch ?? launchSupervisorProcess;
|
|
31
|
+
this.now = options.now ?? (() => new Date());
|
|
32
|
+
}
|
|
33
|
+
static async open(options) {
|
|
34
|
+
const client = new SupervisorOrchestrator(options);
|
|
35
|
+
client.recoverableRun = await client.findRecoverableRun();
|
|
36
|
+
return client;
|
|
37
|
+
}
|
|
38
|
+
async handleRequest(input) {
|
|
39
|
+
const routed = await this.options.delegate.routeInitialRequest(input);
|
|
40
|
+
return this.startRun({
|
|
41
|
+
kind: "handle-request",
|
|
42
|
+
request: input.request,
|
|
43
|
+
cwd: input.cwd,
|
|
44
|
+
route: routed.route,
|
|
45
|
+
role_selection: routed.roleSelection
|
|
46
|
+
}, input);
|
|
47
|
+
}
|
|
48
|
+
async handleTaskTurn(input) {
|
|
49
|
+
return this.startRun({
|
|
50
|
+
kind: "handle-task-turn",
|
|
51
|
+
request: input.request,
|
|
52
|
+
cwd: input.cwd,
|
|
53
|
+
task_id: input.taskId,
|
|
54
|
+
...(input.route ? { route: input.route } : {}),
|
|
55
|
+
...(input.roleSelection ? { role_selection: input.roleSelection } : {})
|
|
56
|
+
}, input);
|
|
57
|
+
}
|
|
58
|
+
async answerTaskQuestion(input) {
|
|
59
|
+
return this.startRun({
|
|
60
|
+
kind: "answer-task-question",
|
|
61
|
+
request: input.request,
|
|
62
|
+
cwd: input.cwd,
|
|
63
|
+
task_id: input.taskId,
|
|
64
|
+
...(input.route ? { route: input.route } : {}),
|
|
65
|
+
...(input.roleSelection ? { role_selection: input.roleSelection } : {})
|
|
66
|
+
}, input);
|
|
67
|
+
}
|
|
68
|
+
async retryTask(input) {
|
|
69
|
+
return this.startRun({
|
|
70
|
+
kind: "retry-task",
|
|
71
|
+
cwd: input.cwd,
|
|
72
|
+
task_id: input.taskId
|
|
73
|
+
}, input);
|
|
74
|
+
}
|
|
75
|
+
async resumeFeature(input) {
|
|
76
|
+
return this.startRun({
|
|
77
|
+
kind: "resume-feature",
|
|
78
|
+
cwd: input.cwd,
|
|
79
|
+
task_id: input.taskId,
|
|
80
|
+
feature_id: input.featureId
|
|
81
|
+
}, input);
|
|
82
|
+
}
|
|
83
|
+
async restorePendingRun(input) {
|
|
84
|
+
this.detaching = input.signal?.aborted ? this.detaching : false;
|
|
85
|
+
const pending = this.recoverableRun ?? await this.findRecoverableRun();
|
|
86
|
+
this.recoverableRun = null;
|
|
87
|
+
if (!pending) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const controller = await claimSupervisorController(pending.files);
|
|
91
|
+
return this.watchRun({
|
|
92
|
+
files: pending.files,
|
|
93
|
+
controller,
|
|
94
|
+
detached: this.detaching,
|
|
95
|
+
cancelSent: false
|
|
96
|
+
}, input);
|
|
97
|
+
}
|
|
98
|
+
detachBackgroundRuns() {
|
|
99
|
+
this.detaching = true;
|
|
100
|
+
const current = this.currentRun;
|
|
101
|
+
if (!current) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
current.detached = true;
|
|
105
|
+
void current.controller?.release();
|
|
106
|
+
current.controller = null;
|
|
107
|
+
this.notifyBackgroundRunState();
|
|
108
|
+
}
|
|
109
|
+
backgroundRunAttached() {
|
|
110
|
+
return this.currentRun !== null;
|
|
111
|
+
}
|
|
112
|
+
backgroundRunControllable() {
|
|
113
|
+
return this.currentRun?.controller !== null && this.currentRun?.controller !== undefined;
|
|
114
|
+
}
|
|
115
|
+
subscribeBackgroundRunState(listener) {
|
|
116
|
+
this.backgroundRunListeners.add(listener);
|
|
117
|
+
return () => this.backgroundRunListeners.delete(listener);
|
|
118
|
+
}
|
|
119
|
+
async acknowledgeBackgroundRun() {
|
|
120
|
+
if (!this.settledRun) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const files = this.settledRun;
|
|
124
|
+
this.settledRun = null;
|
|
125
|
+
await acknowledgeSupervisorRun(files);
|
|
126
|
+
}
|
|
127
|
+
async cancelFeature(taskId, featureId) {
|
|
128
|
+
if (!this.currentRun) {
|
|
129
|
+
return this.options.delegate.cancelFeature(taskId, featureId);
|
|
130
|
+
}
|
|
131
|
+
if (!this.currentRun.controller) {
|
|
132
|
+
return { requested: false, featureId };
|
|
133
|
+
}
|
|
134
|
+
await appendSupervisorCommand(this.currentRun.files, {
|
|
135
|
+
version: 1,
|
|
136
|
+
id: randomUUID(),
|
|
137
|
+
at: this.now().toISOString(),
|
|
138
|
+
type: "cancel-feature",
|
|
139
|
+
task_id: taskId,
|
|
140
|
+
feature_id: featureId
|
|
141
|
+
});
|
|
142
|
+
return { requested: true, featureId };
|
|
143
|
+
}
|
|
144
|
+
async pauseFeature(taskId, featureId) {
|
|
145
|
+
if (!this.currentRun) {
|
|
146
|
+
return this.options.delegate.pauseFeature(taskId, featureId);
|
|
147
|
+
}
|
|
148
|
+
if (!this.currentRun.controller) {
|
|
149
|
+
return { requested: false, featureId };
|
|
150
|
+
}
|
|
151
|
+
await appendSupervisorCommand(this.currentRun.files, {
|
|
152
|
+
version: 1,
|
|
153
|
+
id: randomUUID(),
|
|
154
|
+
at: this.now().toISOString(),
|
|
155
|
+
type: "pause-feature",
|
|
156
|
+
task_id: taskId,
|
|
157
|
+
feature_id: featureId
|
|
158
|
+
});
|
|
159
|
+
return { requested: true, featureId };
|
|
160
|
+
}
|
|
161
|
+
routeTaskFollowUp(input) {
|
|
162
|
+
return this.options.delegate.routeTaskFollowUp(input);
|
|
163
|
+
}
|
|
164
|
+
canRetryTask(taskId) {
|
|
165
|
+
return this.options.delegate.canRetryTask(taskId);
|
|
166
|
+
}
|
|
167
|
+
listTaskWorkers(taskId) {
|
|
168
|
+
return this.options.delegate.listTaskWorkers(taskId);
|
|
169
|
+
}
|
|
170
|
+
reassignFeature(input) {
|
|
171
|
+
return this.options.delegate.reassignFeature(input);
|
|
172
|
+
}
|
|
173
|
+
roleConfigurationSnapshot(taskId) {
|
|
174
|
+
return this.options.delegate.roleConfigurationSnapshot(taskId);
|
|
175
|
+
}
|
|
176
|
+
updateRoleConfiguration(input) {
|
|
177
|
+
return this.options.delegate.updateRoleConfiguration(input);
|
|
178
|
+
}
|
|
179
|
+
clearRoleConfiguration(scope, taskId) {
|
|
180
|
+
return this.options.delegate.clearRoleConfiguration(scope, taskId);
|
|
181
|
+
}
|
|
182
|
+
validateRoleConfiguration(roles) {
|
|
183
|
+
return this.options.delegate.validateRoleConfiguration(roles);
|
|
184
|
+
}
|
|
185
|
+
async startRun(input, callbacks) {
|
|
186
|
+
this.detaching = callbacks.signal?.aborted ? this.detaching : false;
|
|
187
|
+
if (this.currentRun) {
|
|
188
|
+
throw new Error("A Supervisor run is already attached in this TUI.");
|
|
189
|
+
}
|
|
190
|
+
const active = (await this.reconcileRuns()).filter(({ state }) => !supervisorRunIsTerminal(state));
|
|
191
|
+
if (active.length > 0) {
|
|
192
|
+
throw new Error("A background run is already active in this workspace. Reopen the TUI to attach to it.");
|
|
193
|
+
}
|
|
194
|
+
const createdAt = this.now().toISOString();
|
|
195
|
+
const request = {
|
|
196
|
+
...input,
|
|
197
|
+
version: 1,
|
|
198
|
+
run_id: createSupervisorRunId(this.now()),
|
|
199
|
+
app_root: this.options.appRoot,
|
|
200
|
+
workspace_root: this.options.workspaceRoot,
|
|
201
|
+
data_dir: this.options.dataDir,
|
|
202
|
+
created_at: createdAt
|
|
203
|
+
};
|
|
204
|
+
const files = await createSupervisorRun(this.options.workspaceRoot, this.options.dataDir, request);
|
|
205
|
+
const controller = await claimSupervisorController(files);
|
|
206
|
+
if (!controller) {
|
|
207
|
+
throw new Error("The new Supervisor run was claimed by another TUI before it could start.");
|
|
208
|
+
}
|
|
209
|
+
const watched = {
|
|
210
|
+
files,
|
|
211
|
+
controller,
|
|
212
|
+
detached: this.detaching,
|
|
213
|
+
cancelSent: false
|
|
214
|
+
};
|
|
215
|
+
this.currentRun = watched;
|
|
216
|
+
this.notifyBackgroundRunState();
|
|
217
|
+
try {
|
|
218
|
+
await this.launch(files, request);
|
|
219
|
+
}
|
|
220
|
+
catch (error) {
|
|
221
|
+
if (this.currentRun === watched) {
|
|
222
|
+
this.currentRun = null;
|
|
223
|
+
this.notifyBackgroundRunState();
|
|
224
|
+
}
|
|
225
|
+
await controller.release();
|
|
226
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
227
|
+
const failedAt = this.now().toISOString();
|
|
228
|
+
const state = await readSupervisorRunState(files);
|
|
229
|
+
await writeSupervisorRunState(files, {
|
|
230
|
+
...state,
|
|
231
|
+
status: "failed",
|
|
232
|
+
updated_at: failedAt,
|
|
233
|
+
finished_at: failedAt,
|
|
234
|
+
error: message
|
|
235
|
+
});
|
|
236
|
+
await this.options.sessions.appendChatMessage({ from: "system", text: message });
|
|
237
|
+
this.settledRun = files;
|
|
238
|
+
throw error;
|
|
239
|
+
}
|
|
240
|
+
return this.watchRun(watched, callbacks);
|
|
241
|
+
}
|
|
242
|
+
async watchRun(watched, callbacks) {
|
|
243
|
+
this.currentRun = watched;
|
|
244
|
+
this.notifyBackgroundRunState();
|
|
245
|
+
let nextEventSequence = 0;
|
|
246
|
+
try {
|
|
247
|
+
while (true) {
|
|
248
|
+
if (watched.detached) {
|
|
249
|
+
throw new SupervisorDetachedError();
|
|
250
|
+
}
|
|
251
|
+
if (!watched.controller) {
|
|
252
|
+
watched.controller = await claimSupervisorController(watched.files);
|
|
253
|
+
if (watched.controller) {
|
|
254
|
+
this.notifyBackgroundRunState();
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const events = await readSupervisorEvents(watched.files);
|
|
258
|
+
for (const event of events) {
|
|
259
|
+
if (event.sequence < nextEventSequence) {
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
nextEventSequence = event.sequence + 1;
|
|
263
|
+
dispatchSupervisorEvent(event, callbacks);
|
|
264
|
+
}
|
|
265
|
+
const state = await readSupervisorRunState(watched.files);
|
|
266
|
+
if (supervisorRunIsTerminal(state)) {
|
|
267
|
+
this.settledRun = watched.files;
|
|
268
|
+
if (state.status === "completed" && state.result) {
|
|
269
|
+
return asSupervisorRunResult(state.result);
|
|
270
|
+
}
|
|
271
|
+
const error = new Error(state.error || `Supervisor run ${state.status}`);
|
|
272
|
+
if (state.status === "cancelled") {
|
|
273
|
+
error.name = "AbortError";
|
|
274
|
+
}
|
|
275
|
+
throw error;
|
|
276
|
+
}
|
|
277
|
+
if (watched.controller && !(await supervisorRunProcessIsActive(state))) {
|
|
278
|
+
const ageMs = this.now().getTime() - Date.parse(state.created_at);
|
|
279
|
+
if (state.status !== "queued" || ageMs >= QUEUED_START_GRACE_MS) {
|
|
280
|
+
const failedAt = this.now().toISOString();
|
|
281
|
+
const summary = `Supervisor exited unexpectedly while ${state.kind} was running.`;
|
|
282
|
+
await writeSupervisorRunState(watched.files, {
|
|
283
|
+
...state,
|
|
284
|
+
status: "failed",
|
|
285
|
+
updated_at: failedAt,
|
|
286
|
+
finished_at: failedAt,
|
|
287
|
+
error: summary
|
|
288
|
+
});
|
|
289
|
+
await this.options.sessions.appendChatMessage({
|
|
290
|
+
from: "system",
|
|
291
|
+
text: summary,
|
|
292
|
+
taskId: state.task_id ?? undefined
|
|
293
|
+
});
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (callbacks.signal?.aborted && !watched.cancelSent) {
|
|
298
|
+
if (!watched.controller) {
|
|
299
|
+
throw new SupervisorDetachedError("Observer detached; the background run is still active");
|
|
300
|
+
}
|
|
301
|
+
watched.cancelSent = true;
|
|
302
|
+
await appendSupervisorCommand(watched.files, {
|
|
303
|
+
version: 1,
|
|
304
|
+
id: randomUUID(),
|
|
305
|
+
at: this.now().toISOString(),
|
|
306
|
+
type: "cancel-run"
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
await delay(RUN_POLL_MS);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
finally {
|
|
313
|
+
if (this.currentRun === watched) {
|
|
314
|
+
this.currentRun = null;
|
|
315
|
+
}
|
|
316
|
+
await watched.controller?.release();
|
|
317
|
+
watched.controller = null;
|
|
318
|
+
this.notifyBackgroundRunState();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
notifyBackgroundRunState() {
|
|
322
|
+
for (const listener of this.backgroundRunListeners) {
|
|
323
|
+
listener();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
async findRecoverableRun() {
|
|
327
|
+
const runs = await this.reconcileRuns();
|
|
328
|
+
const active = runs.filter(({ state }) => !supervisorRunIsTerminal(state));
|
|
329
|
+
if (active.length > 0) {
|
|
330
|
+
return active.at(-1) ?? null;
|
|
331
|
+
}
|
|
332
|
+
const unacknowledged = [];
|
|
333
|
+
for (const run of runs) {
|
|
334
|
+
if (supervisorRunIsTerminal(run.state) && !(await supervisorRunIsAcknowledged(run.files))) {
|
|
335
|
+
unacknowledged.push(run);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return unacknowledged.at(-1) ?? null;
|
|
339
|
+
}
|
|
340
|
+
async reconcileRuns() {
|
|
341
|
+
const runs = await listSupervisorRuns(this.options.workspaceRoot, this.options.dataDir);
|
|
342
|
+
for (const run of runs) {
|
|
343
|
+
if (supervisorRunIsTerminal(run.state) || await supervisorRunProcessIsActive(run.state)) {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
const ageMs = this.now().getTime() - Date.parse(run.state.created_at);
|
|
347
|
+
if (run.state.status === "queued" && ageMs < QUEUED_START_GRACE_MS) {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
const failedAt = this.now().toISOString();
|
|
351
|
+
const summary = `Supervisor exited unexpectedly while ${run.state.kind} was running.`;
|
|
352
|
+
run.state = {
|
|
353
|
+
...run.state,
|
|
354
|
+
status: "failed",
|
|
355
|
+
updated_at: failedAt,
|
|
356
|
+
finished_at: failedAt,
|
|
357
|
+
error: summary
|
|
358
|
+
};
|
|
359
|
+
await writeSupervisorRunState(run.files, run.state);
|
|
360
|
+
await this.options.sessions.appendChatMessage({
|
|
361
|
+
from: "system",
|
|
362
|
+
text: summary,
|
|
363
|
+
taskId: run.state.task_id ?? undefined
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
return runs;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function dispatchSupervisorEvent(event, callbacks) {
|
|
370
|
+
const payload = supervisorEventPayload(event);
|
|
371
|
+
switch (event.type) {
|
|
372
|
+
case "route-start":
|
|
373
|
+
callbacks.onRouteStart?.(payload);
|
|
374
|
+
break;
|
|
375
|
+
case "route-progress":
|
|
376
|
+
callbacks.onRouteProgress?.(payload);
|
|
377
|
+
break;
|
|
378
|
+
case "route":
|
|
379
|
+
callbacks.onRoute?.(payload);
|
|
380
|
+
break;
|
|
381
|
+
case "status":
|
|
382
|
+
callbacks.onStatus?.(payload);
|
|
383
|
+
break;
|
|
384
|
+
case "worker":
|
|
385
|
+
callbacks.onWorker?.(payload);
|
|
386
|
+
break;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
async function launchSupervisorProcess(files, request) {
|
|
390
|
+
const entrypoint = process.argv[1] ? resolve(process.argv[1]) : "";
|
|
391
|
+
if (!entrypoint) {
|
|
392
|
+
throw new Error("Cannot locate the parallel-codex-tui CLI entrypoint for Supervisor launch.");
|
|
393
|
+
}
|
|
394
|
+
const errorLog = openSync(join(files.dir, "supervisor.log"), "a");
|
|
395
|
+
const child = spawn(process.execPath, [...process.execArgv, entrypoint], {
|
|
396
|
+
cwd: process.cwd(),
|
|
397
|
+
detached: true,
|
|
398
|
+
env: {
|
|
399
|
+
...process.env,
|
|
400
|
+
PCT_SUPERVISOR_RUN_DIR: files.dir
|
|
401
|
+
},
|
|
402
|
+
stdio: ["ignore", "ignore", errorLog]
|
|
403
|
+
});
|
|
404
|
+
try {
|
|
405
|
+
await new Promise((resolve, reject) => {
|
|
406
|
+
child.once("spawn", resolve);
|
|
407
|
+
child.once("error", reject);
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
finally {
|
|
411
|
+
closeSync(errorLog);
|
|
412
|
+
}
|
|
413
|
+
let exitCode;
|
|
414
|
+
const onExit = (code) => {
|
|
415
|
+
exitCode = code;
|
|
416
|
+
};
|
|
417
|
+
child.once("exit", onExit);
|
|
418
|
+
const deadline = Date.now() + 5000;
|
|
419
|
+
while (Date.now() < deadline) {
|
|
420
|
+
const state = await readSupervisorRunState(files);
|
|
421
|
+
if (state.status !== "queued") {
|
|
422
|
+
child.off("exit", onExit);
|
|
423
|
+
child.unref();
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (exitCode !== undefined) {
|
|
427
|
+
throw new Error(`Supervisor process exited before startup (code ${exitCode ?? "signal"}).`);
|
|
428
|
+
}
|
|
429
|
+
await delay(25);
|
|
430
|
+
}
|
|
431
|
+
child.off("exit", onExit);
|
|
432
|
+
try {
|
|
433
|
+
child.kill("SIGTERM");
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
// The startup failure below remains the useful error when the child already exited.
|
|
437
|
+
}
|
|
438
|
+
throw new Error("Supervisor process did not publish startup state within 5s.");
|
|
439
|
+
}
|
|
440
|
+
function delay(ms) {
|
|
441
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
442
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { EngineNameSchema, RouteDecisionSchema, TaskIdSchema, WorkerModelNameSchema, WorkerRoleSchema, WorkerStatusSchema } from "../domain/schemas.js";
|
|
3
|
+
const RoleExecutionTargetSchema = z.object({
|
|
4
|
+
engine: EngineNameSchema,
|
|
5
|
+
model: WorkerModelNameSchema
|
|
6
|
+
}).strict();
|
|
7
|
+
const RoleExecutionSelectionSchema = z.object({
|
|
8
|
+
main: RoleExecutionTargetSchema,
|
|
9
|
+
judge: RoleExecutionTargetSchema,
|
|
10
|
+
actor: RoleExecutionTargetSchema,
|
|
11
|
+
critic: RoleExecutionTargetSchema
|
|
12
|
+
}).strict();
|
|
13
|
+
const SupervisorRequestBaseSchema = z.object({
|
|
14
|
+
version: z.literal(1),
|
|
15
|
+
run_id: z.string().min(1),
|
|
16
|
+
app_root: z.string().min(1),
|
|
17
|
+
workspace_root: z.string().min(1),
|
|
18
|
+
data_dir: z.string().min(1),
|
|
19
|
+
created_at: z.string().datetime()
|
|
20
|
+
});
|
|
21
|
+
export const SupervisorRunRequestSchema = z.discriminatedUnion("kind", [
|
|
22
|
+
SupervisorRequestBaseSchema.extend({
|
|
23
|
+
kind: z.literal("handle-request"),
|
|
24
|
+
request: z.string().min(1),
|
|
25
|
+
cwd: z.string().min(1),
|
|
26
|
+
route: RouteDecisionSchema.optional(),
|
|
27
|
+
role_selection: RoleExecutionSelectionSchema.optional()
|
|
28
|
+
}).strict(),
|
|
29
|
+
SupervisorRequestBaseSchema.extend({
|
|
30
|
+
kind: z.literal("handle-task-turn"),
|
|
31
|
+
request: z.string().min(1),
|
|
32
|
+
cwd: z.string().min(1),
|
|
33
|
+
task_id: TaskIdSchema,
|
|
34
|
+
route: RouteDecisionSchema.optional(),
|
|
35
|
+
role_selection: RoleExecutionSelectionSchema.optional()
|
|
36
|
+
}).strict(),
|
|
37
|
+
SupervisorRequestBaseSchema.extend({
|
|
38
|
+
kind: z.literal("answer-task-question"),
|
|
39
|
+
request: z.string().min(1),
|
|
40
|
+
cwd: z.string().min(1),
|
|
41
|
+
task_id: TaskIdSchema,
|
|
42
|
+
route: RouteDecisionSchema.optional(),
|
|
43
|
+
role_selection: RoleExecutionSelectionSchema.optional()
|
|
44
|
+
}).strict(),
|
|
45
|
+
SupervisorRequestBaseSchema.extend({
|
|
46
|
+
kind: z.literal("retry-task"),
|
|
47
|
+
cwd: z.string().min(1),
|
|
48
|
+
task_id: TaskIdSchema
|
|
49
|
+
}).strict(),
|
|
50
|
+
SupervisorRequestBaseSchema.extend({
|
|
51
|
+
kind: z.literal("resume-feature"),
|
|
52
|
+
cwd: z.string().min(1),
|
|
53
|
+
task_id: TaskIdSchema,
|
|
54
|
+
feature_id: z.string().min(1)
|
|
55
|
+
}).strict()
|
|
56
|
+
]);
|
|
57
|
+
export const SupervisorRunStatusSchema = z.enum([
|
|
58
|
+
"queued",
|
|
59
|
+
"running",
|
|
60
|
+
"cancelling",
|
|
61
|
+
"completed",
|
|
62
|
+
"failed",
|
|
63
|
+
"cancelled"
|
|
64
|
+
]);
|
|
65
|
+
const WorkerLogRefSchema = z.object({
|
|
66
|
+
id: z.string().min(1),
|
|
67
|
+
featureId: z.string().min(1).optional(),
|
|
68
|
+
role: WorkerRoleSchema,
|
|
69
|
+
engine: EngineNameSchema,
|
|
70
|
+
label: z.string(),
|
|
71
|
+
logPath: z.string().min(1),
|
|
72
|
+
statusPath: z.string().min(1),
|
|
73
|
+
runtimeStatus: WorkerStatusSchema.optional()
|
|
74
|
+
}).strict();
|
|
75
|
+
export const SupervisorRunResultSchema = z.object({
|
|
76
|
+
mode: z.enum(["simple", "complex"]),
|
|
77
|
+
taskId: TaskIdSchema.nullable(),
|
|
78
|
+
summary: z.string(),
|
|
79
|
+
workers: z.array(WorkerLogRefSchema)
|
|
80
|
+
}).strict();
|
|
81
|
+
export const SupervisorRunStateSchema = z.object({
|
|
82
|
+
version: z.literal(1),
|
|
83
|
+
run_id: z.string().min(1),
|
|
84
|
+
kind: z.enum([
|
|
85
|
+
"handle-request",
|
|
86
|
+
"handle-task-turn",
|
|
87
|
+
"answer-task-question",
|
|
88
|
+
"retry-task",
|
|
89
|
+
"resume-feature"
|
|
90
|
+
]),
|
|
91
|
+
status: SupervisorRunStatusSchema,
|
|
92
|
+
app_root: z.string().min(1),
|
|
93
|
+
workspace_root: z.string().min(1),
|
|
94
|
+
created_at: z.string().datetime(),
|
|
95
|
+
updated_at: z.string().datetime(),
|
|
96
|
+
started_at: z.string().datetime().optional(),
|
|
97
|
+
finished_at: z.string().datetime().optional(),
|
|
98
|
+
task_id: TaskIdSchema.nullable().optional(),
|
|
99
|
+
pid: z.number().int().positive().optional(),
|
|
100
|
+
process_start_token: z.string().min(1).optional(),
|
|
101
|
+
result: SupervisorRunResultSchema.optional(),
|
|
102
|
+
error: z.string().optional()
|
|
103
|
+
}).strict();
|
|
104
|
+
export const SupervisorEventTypeSchema = z.enum([
|
|
105
|
+
"route-start",
|
|
106
|
+
"route-progress",
|
|
107
|
+
"route",
|
|
108
|
+
"status",
|
|
109
|
+
"worker"
|
|
110
|
+
]);
|
|
111
|
+
export const SupervisorRunEventSchema = z.object({
|
|
112
|
+
version: z.literal(1),
|
|
113
|
+
sequence: z.number().int().nonnegative(),
|
|
114
|
+
at: z.string().datetime(),
|
|
115
|
+
type: SupervisorEventTypeSchema,
|
|
116
|
+
payload: z.unknown()
|
|
117
|
+
}).strict();
|
|
118
|
+
export const SupervisorCommandSchema = z.discriminatedUnion("type", [
|
|
119
|
+
z.object({
|
|
120
|
+
version: z.literal(1),
|
|
121
|
+
id: z.string().min(1),
|
|
122
|
+
at: z.string().datetime(),
|
|
123
|
+
type: z.literal("cancel-run")
|
|
124
|
+
}).strict(),
|
|
125
|
+
z.object({
|
|
126
|
+
version: z.literal(1),
|
|
127
|
+
id: z.string().min(1),
|
|
128
|
+
at: z.string().datetime(),
|
|
129
|
+
type: z.literal("cancel-feature"),
|
|
130
|
+
task_id: TaskIdSchema,
|
|
131
|
+
feature_id: z.string().min(1)
|
|
132
|
+
}).strict(),
|
|
133
|
+
z.object({
|
|
134
|
+
version: z.literal(1),
|
|
135
|
+
id: z.string().min(1),
|
|
136
|
+
at: z.string().datetime(),
|
|
137
|
+
type: z.literal("pause-feature"),
|
|
138
|
+
task_id: TaskIdSchema,
|
|
139
|
+
feature_id: z.string().min(1)
|
|
140
|
+
}).strict()
|
|
141
|
+
]);
|
|
142
|
+
export const SupervisorControllerSchema = z.object({
|
|
143
|
+
version: z.literal(1),
|
|
144
|
+
controller_id: z.string().min(1),
|
|
145
|
+
pid: z.number().int().positive(),
|
|
146
|
+
acquired_at: z.string().datetime(),
|
|
147
|
+
process_start_token: z.string().min(1).optional()
|
|
148
|
+
}).strict();
|
|
149
|
+
export function supervisorEventPayload(event) {
|
|
150
|
+
switch (event.type) {
|
|
151
|
+
case "route":
|
|
152
|
+
return RouteDecisionSchema.parse(event.payload);
|
|
153
|
+
case "worker":
|
|
154
|
+
return WorkerLogRefSchema.parse(event.payload);
|
|
155
|
+
case "route-progress":
|
|
156
|
+
return z.object({ phase: z.enum([
|
|
157
|
+
"dispatching",
|
|
158
|
+
"starting",
|
|
159
|
+
"retrying",
|
|
160
|
+
"waiting-output",
|
|
161
|
+
"receiving-stderr",
|
|
162
|
+
"receiving-response",
|
|
163
|
+
"parsing",
|
|
164
|
+
"stopping"
|
|
165
|
+
]) }).parse(event.payload);
|
|
166
|
+
case "route-start":
|
|
167
|
+
return event.payload;
|
|
168
|
+
case "status":
|
|
169
|
+
return event.payload;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
export function asSupervisorRunResult(value) {
|
|
173
|
+
return SupervisorRunResultSchema.parse(value);
|
|
174
|
+
}
|
|
175
|
+
export function roleSelectionFromRequest(request) {
|
|
176
|
+
return "role_selection" in request ? request.role_selection : undefined;
|
|
177
|
+
}
|