import type { RunProjectionV2, WorkerId } from "../workflow/workflow-types.js"; export interface WorkerNotice { readonly level: "info" | "warning" | "error"; readonly message: string; } export interface CompletionTimerScheduler { schedule(delayMs: number, callback: () => void): { cancel(): void }; } export class WorkerCompletionNotifier { readonly #publish: (notice: WorkerNotice) => void; readonly #timers: CompletionTimerScheduler; readonly #windowMs: number; readonly #states = new Map(); readonly #successful = new Set(); #flush: { cancel(): void } | undefined; constructor(options: { readonly publish: (notice: WorkerNotice) => void; readonly timers?: CompletionTimerScheduler; readonly windowMs?: number; }) { this.#publish = options.publish; this.#timers = options.timers ?? new NodeCompletionTimers(); this.#windowMs = options.windowMs ?? 350; } baseline(run: RunProjectionV2): void { for (const worker of Object.values(run.workers)) { this.#states.set(worker.workerId, worker.state); } } observe(run: RunProjectionV2): void { for (const worker of Object.values(run.workers)) { const previous = this.#states.get(worker.workerId); this.#states.set(worker.workerId, worker.state); if (previous === undefined || previous === worker.state) continue; switch (worker.state) { case "done": this.#successful.add(worker.workerId); this.#scheduleSuccessFlush(); break; case "failed": this.#emit({ level: "error", message: `Worker ${worker.workerId} failed.` }); break; case "blocked": this.#emit({ level: "warning", message: `Worker ${worker.workerId} paused for typed input.` }); break; case "ambiguous": this.#emit({ level: "warning", message: `Worker ${worker.workerId} requires reconciliation.` }); break; default: break; } } } dispose(): void { this.#flush?.cancel(); this.#flush = undefined; this.#successful.clear(); } #scheduleSuccessFlush(): void { if (this.#flush) return; this.#flush = this.#timers.schedule(this.#windowMs, () => { this.#flush = undefined; const ids = [...this.#successful].sort(); this.#successful.clear(); if (ids.length === 0) return; this.#emit({ level: "info", message: ids.length === 1 ? `Worker ${ids[0]} completed.` : `Workers completed: ${ids.join(", ")}.`, }); }); } #emit(notice: WorkerNotice): void { try { this.#publish(notice); } catch { // UI delivery is advisory and cannot invalidate committed ledger truth. } } } class NodeCompletionTimers implements CompletionTimerScheduler { schedule(delayMs: number, callback: () => void): { cancel(): void } { const timer = setTimeout(callback, Math.max(0, delayMs)); timer.unref(); return { cancel: () => clearTimeout(timer) }; } }