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.
@@ -75,10 +75,17 @@ export class Orchestrator {
75
75
  }
76
76
  async handleRequest(input) {
77
77
  throwIfCancelled(input.signal);
78
- const routed = await this.routeRequest(input.request, input.cwd, input.signal, "initial", input.onRouteStart, input.onRouteFallback, input.onRouteProgress);
79
- const roleSelection = input.roleSelection ?? await this.roleConfiguration.selectionForRequest();
80
- const route = routeWithRoleSelection(routed, roleSelection);
81
- input.onRoute?.(route);
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
+ }