import type { ExtensionContext } from "@earendil-works/pi-coding-agent"; // Lazy-loaded: team-tool.ts pulls in entire runtime chain. import type { handleTeamTool as HandleTeamToolFn } from "../extension/team-tool.ts"; import type { MetricRegistry } from "../observability/metric-registry.ts"; async function handleTeamTool( params: Parameters[0], ctx: Parameters[1], ): Promise>> { // LAZY: avoid pulling team-tool.ts (and its entire runtime chain) into module load. const mod = await import("../extension/team-tool.ts"); return mod.handleTeamTool(params, ctx); } import { isToolError, textFromToolResult } from "../extension/tool-result.ts"; import { readCrewAgents } from "../runtime/crew-agent-records.ts"; import { exportDiagnostic } from "../runtime/diagnostic-export.ts"; import type { MailboxDirection, MailboxMessage } from "../state/coordination/mailbox.ts"; import { appendEventBuffered } from "../state/event-log/event-log.ts"; import { loadRunManifestById, saveRunTasks } from "../state/stores/state-store.ts"; import { logInternalError } from "../utils/internal-error.ts"; import { formatCount } from "./format-helpers.ts"; export interface RunActionResult { ok: boolean; message: string; data?: unknown; } function okFromTool(result: Awaited>): RunActionResult { return { ok: !isToolError(result), message: textFromToolResult(result), data: result, }; } function err(error: unknown): RunActionResult { return { ok: false, message: error instanceof Error ? error.message : String(error), }; } async function dispatchApi(ctx: ExtensionContext, runId: string, config: Record): Promise { try { return okFromTool(await handleTeamTool({ action: "api", runId, config }, ctx)); } catch (error) { return err(error); } } function parseMailboxMessages(text: string): MailboxMessage[] { try { const parsed = JSON.parse(text) as unknown; if (!Array.isArray(parsed)) return []; return parsed.filter( (item): item is MailboxMessage => Boolean(item) && typeof item === "object" && !Array.isArray(item) && typeof (item as { id?: unknown }).id === "string", ); } catch { return []; } } export function dispatchMailboxAck(ctx: ExtensionContext, runId: string, messageId: string): Promise { return dispatchApi(ctx, runId, { operation: "ack-message", messageId }); } export function dispatchMailboxNudge(ctx: ExtensionContext, runId: string, agentId: string, message: string): Promise { return dispatchApi(ctx, runId, { operation: "nudge-agent", agentId, message, }); } export function dispatchMailboxCompose( ctx: ExtensionContext, runId: string, payload: { from: string; to: string; body: string; taskId?: string; direction: MailboxDirection; }, ): Promise { return dispatchApi(ctx, runId, { operation: "send-message", ...payload }); } export async function dispatchMailboxAckAll(ctx: ExtensionContext, runId: string): Promise { const listed = await dispatchApi(ctx, runId, { operation: "read-mailbox", direction: "inbox", }); if (!listed.ok) return listed; const messages = parseMailboxMessages(listed.message).filter((message) => message.status !== "acknowledged"); let count = 0; for (const message of messages) { const acked = await dispatchMailboxAck(ctx, runId, message.id); if (!acked.ok) return { ok: false, message: `Acknowledged ${count}/${messages.length}; failed ${message.id}: ${acked.message}`, }; count += 1; } return { ok: true, message: `Acknowledged ${formatCount(count, "message")}.`, data: { count }, }; } export function dispatchHealthRecovery(ctx: ExtensionContext, runId: string): Promise { return dispatchApi(ctx, runId, { operation: "foreground-interrupt", reason: "operator health recovery", }); } export async function dispatchKillStaleWorkers(ctx: ExtensionContext, runId: string): Promise { try { const loaded = loadRunManifestById(ctx.cwd, runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency; if (!loaded) return { ok: false, message: `Run '${runId}' not found.` }; const currentMs = Date.now(); const staleMs = 60_000; const now = new Date(currentMs).toISOString(); let count = 0; const tasks = loaded.tasks.map((task) => { if ((task.status !== "running" && task.status !== "queued") || !task.heartbeat || task.heartbeat.alive === false) return task; const lastSeenMs = Date.parse(task.heartbeat.lastSeenAt); if (!Number.isFinite(lastSeenMs) || currentMs - lastSeenMs <= staleMs) return task; count += 1; return { ...task, heartbeat: { ...task.heartbeat, alive: false, lastSeenAt: now }, }; }); saveRunTasks(loaded.manifest, tasks); appendEventBuffered(loaded.manifest.eventsPath, { type: "worker.kill_stale", runId, message: `Marked ${count} stale worker heartbeat(s) dead.`, data: { count }, }).catch((e) => logInternalError("run_action_dispatcher.buffered", e, "type=worker.kill_stale")); return { ok: true, message: `Marked ${count} stale worker heartbeat(s) dead.`, data: { count }, }; } catch (error) { return err(error); } } export async function dispatchDiagnosticExport( ctx: ExtensionContext, runId: string, options: { registry?: MetricRegistry } = {}, ): Promise { try { const exported = await exportDiagnostic(ctx, runId, options); return { ok: true, message: `Diagnostic exported to ${exported.path}`, data: exported.path, }; } catch (error) { return err(error); } } /** @internal */ function defaultNudgeAgentId(ctx: Pick, runId: string): string | undefined { const loaded = loadRunManifestById(ctx.cwd, runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency; if (!loaded) return undefined; return readCrewAgents(loaded.manifest).find((agent) => agent.status === "running" || agent.status === "queued")?.taskId; }