import { readFile } from "node:fs/promises"; import { join } from "node:path"; import { defaultPiStateDirectory } from "./workspace-paths.js"; import type { WorkerProposalEditableField, WorkerProposalProvenance, WorkerRequest, } from "./workflow-types.js"; export type ConfirmSpawnsPolicy = WorkerProposalProvenance["confirmSpawns"]; export interface SpawnConfirmationConfigOptions { readonly projectDirectory: string; readonly projectTrusted: boolean; readonly userConfigPath?: string; readonly projectConfigPath?: string; } export async function loadConfirmSpawnsPolicy( options: SpawnConfirmationConfigOptions, ): Promise { let policy: ConfirmSpawnsPolicy = "multi-worker-only"; const user = await readOptionalConfig( options.userConfigPath ?? join(defaultPiStateDirectory(), "pi-amq", "config.json"), ); if (user !== undefined) policy = user; if (options.projectTrusted) { const project = await readOptionalConfig( options.projectConfigPath ?? join(options.projectDirectory, ".pi", "pi-amq", "config.json"), ); if (project !== undefined) policy = project; } return policy; } export interface EditedWorkerProposal { readonly request: WorkerRequest; readonly humanEditedFields: readonly WorkerProposalEditableField[]; } export type LaunchPreviewDecision = | { readonly kind: "accepted"; readonly proposals: readonly EditedWorkerProposal[] } | { readonly kind: "cancelled" }; export interface LaunchPreviewSubmission { readonly groupKey: string; readonly policy: ConfirmSpawnsPolicy; readonly tui: boolean; readonly request: WorkerRequest; readonly signal?: AbortSignal; readonly preview: (requests: readonly WorkerRequest[]) => Promise; readonly launch: (accepted: WorkerRequest) => Promise; } export type LaunchPreviewSubmissionResult = | { readonly kind: "launched"; readonly value: T } | { readonly kind: "cancelled"; readonly reason: string }; interface PendingSubmission { readonly request: WorkerRequest; readonly signal: AbortSignal | undefined; readonly preview: LaunchPreviewSubmission["preview"]; readonly launch: LaunchPreviewSubmission["launch"]; readonly resolve: (result: LaunchPreviewSubmissionResult) => void; readonly reject: (error: Error) => void; active: boolean; } interface PendingBatch { readonly policy: ConfirmSpawnsPolicy; readonly tui: boolean; readonly submissions: PendingSubmission[]; readonly timer: ReturnType; } export interface LaunchPreviewCoordinatorOptions { readonly batchWindowMs?: number; } export class LaunchPreviewCoordinator { readonly #batchWindowMs: number; readonly #batches = new Map(); constructor(options: LaunchPreviewCoordinatorOptions = {}) { this.#batchWindowMs = options.batchWindowMs ?? 20; if (!Number.isSafeInteger(this.#batchWindowMs) || this.#batchWindowMs < 0 || this.#batchWindowMs > 1_000) { throw new TypeError("Launch preview batch window must be an integer between 0 and 1000 ms"); } } async submit(submission: LaunchPreviewSubmission): Promise> { if (submission.signal?.aborted) throw abortError(submission.signal); if (submission.policy === "off") { const accepted = withProvenance(submission.request, "off", "skipped-policy-off", []); return { kind: "launched", value: await submission.launch(accepted) }; } return new Promise>((resolve, reject) => { const key = `${submission.groupKey}\0${submission.policy}\0${submission.tui ? "tui" : "headless"}`; let batch = this.#batches.get(key); if (!batch) { const timer = setTimeout(() => void this.#flush(key), this.#batchWindowMs); batch = { policy: submission.policy, tui: submission.tui, submissions: [], timer, }; this.#batches.set(key, batch); } const pending: PendingSubmission = { request: submission.request, signal: submission.signal, preview: submission.preview, launch: submission.launch as (accepted: WorkerRequest) => Promise, resolve: resolve as (result: LaunchPreviewSubmissionResult) => void, reject: (error) => reject(error), active: true, }; batch.submissions.push(pending); submission.signal?.addEventListener("abort", () => { if (!pending.active) return; pending.active = false; pending.reject(abortError(submission.signal!)); }, { once: true }); }); } async #flush(key: string): Promise { const batch = this.#batches.get(key); if (!batch) return; clearTimeout(batch.timer); this.#batches.delete(key); const pending = batch.submissions.filter((submission) => submission.active); if (pending.length === 0) return; if (batch.policy === "multi-worker-only" && pending.length === 1) { const only = pending[0]!; only.active = false; await settleLaunch( only, withProvenance(only.request, batch.policy, "skipped-single-worker", []), ); return; } if (!batch.tui) { const error = new Error( `confirm_spawns=${batch.policy} requires an interactive TUI for this ${pending.length}-worker proposal; no worker was accepted`, ); for (const submission of pending) { submission.active = false; submission.reject(error); } return; } let decision: LaunchPreviewDecision; try { decision = await pending[0]!.preview(pending.map((submission) => submission.request)); } catch (error) { const normalized = error instanceof Error ? error : new Error(String(error)); for (const submission of pending) { if (!submission.active) continue; submission.active = false; submission.reject(normalized); } return; } if (pending.some((submission) => !submission.active)) { for (const submission of pending) { if (!submission.active) continue; submission.active = false; submission.resolve({ kind: "cancelled", reason: "A launch-preview tool call was aborted; no worker contract was accepted or ledgered", }); } return; } if (decision.kind === "cancelled") { for (const submission of pending) { submission.active = false; submission.resolve({ kind: "cancelled", reason: "Launch preview cancelled; no worker contract was accepted or ledgered", }); } return; } if (decision.proposals.length !== pending.length) { const error = new Error("Launch preview returned a different proposal count"); for (const submission of pending) { submission.active = false; submission.reject(error); } return; } const keys = new Set(); for (const proposal of decision.proposals) { if (keys.has(proposal.request.requestKey)) { const error = new Error("Launch preview returned duplicate worker request keys"); for (const submission of pending) { submission.active = false; submission.reject(error); } return; } keys.add(proposal.request.requestKey); } if (pending.some((submission) => !keys.has(submission.request.requestKey))) { const error = new Error("Launch preview changed or dropped a worker request key"); for (const submission of pending) { submission.active = false; submission.reject(error); } return; } for (const submission of pending) submission.active = false; await Promise.all(pending.map(async (submission) => { const proposal = decision.proposals.find( (candidate) => candidate.request.requestKey === submission.request.requestKey, ); if (!proposal) { submission.reject(new Error(`Launch preview dropped ${submission.request.requestKey}`)); return; } const fields = uniqueFields(proposal.humanEditedFields); await settleLaunch( submission, withProvenance(proposal.request, batch.policy, "accepted", fields), ); })); } } async function settleLaunch(submission: PendingSubmission, request: WorkerRequest): Promise { try { submission.resolve({ kind: "launched", value: await submission.launch(request) }); } catch (error) { submission.reject(error instanceof Error ? error : new Error(String(error))); } } function withProvenance( request: WorkerRequest, policy: ConfirmSpawnsPolicy, disposition: WorkerProposalProvenance["disposition"], humanEditedFields: readonly WorkerProposalEditableField[], ): WorkerRequest { return { ...request, proposal: { source: humanEditedFields.length === 0 ? "tool" : "human-edited", confirmSpawns: policy, disposition, humanEditedFields, }, }; } function uniqueFields( fields: readonly WorkerProposalEditableField[], ): readonly WorkerProposalEditableField[] { const supported = new Set(["role", "task", "model", "difficulty", "budget"]); const unique: WorkerProposalEditableField[] = []; for (const field of fields) { if (!supported.has(field)) throw new TypeError(`Unknown edited proposal field ${field}`); if (!unique.includes(field)) unique.push(field); } return unique; } async function readOptionalConfig(path: string): Promise { let raw: string; try { raw = await readFile(path, "utf8"); } catch (error) { if (isMissing(error)) return undefined; throw error; } let value: unknown; try { value = JSON.parse(raw) as unknown; } catch (error) { throw new TypeError(`${path}: invalid JSON: ${error instanceof Error ? error.message : String(error)}`); } if (typeof value !== "object" || value === null || Array.isArray(value)) { throw new TypeError(`${path}: pi-amq config must be an object`); } const policy = (value as Record)["confirm_spawns"]; if (policy === undefined) return undefined; if (policy !== "on" && policy !== "multi-worker-only" && policy !== "off") { throw new TypeError(`${path}: confirm_spawns must be on, multi-worker-only, or off`); } return policy; } function isMissing(error: unknown): boolean { return typeof error === "object" && error !== null && "code" in error && (error as { readonly code?: unknown }).code === "ENOENT"; } function abortError(signal: AbortSignal): Error { return signal.reason instanceof Error ? signal.reason : new Error("Launch preview aborted"); }