export const SUBAGENT_ASYNC_STARTED = "subagent:async-started"; export const SUBAGENT_ASYNC_COMPLETE = "subagent:async-complete"; export const SUBAGENT_PROCESS_TERMINAL = "subagent:process-terminal"; export const SUBAGENT_RPC_REQUEST = "subagents:rpc:v1:request"; export const SUBAGENT_RPC_REPLY_PREFIX = "subagents:rpc:v1:reply:"; export function runIdFromPayload(payload: unknown): string | undefined { if (!payload || typeof payload !== "object") return undefined; const rec = payload as Record; if (typeof rec.id === "string" && rec.id.trim()) return rec.id; if (typeof rec.runId === "string" && rec.runId.trim()) return rec.runId; return undefined; } export function fleetFromRpcReply(payload: unknown): { totalActive: number; earliestStartedAt?: number } { if (!payload || typeof payload !== "object") return { totalActive: 0 }; const rec = payload as Record; if (rec.success !== true || !rec.data || typeof rec.data !== "object") return { totalActive: 0 }; const data = rec.data as Record; const fleet = data.fleet; if (!fleet || typeof fleet !== "object") return { totalActive: 0 }; const body = fleet as Record; const totalActive = typeof body.totalActive === "number" ? Math.max(0, body.totalActive) : 0; const entries = Array.isArray(body.entries) ? body.entries : []; let earliestStartedAt: number | undefined; for (const entry of entries) { if (!entry || typeof entry !== "object") continue; const startedAt = (entry as { startedAt?: unknown }).startedAt; if (typeof startedAt !== "number" || !Number.isFinite(startedAt)) continue; if (earliestStartedAt === undefined || startedAt < earliestStartedAt) earliestStartedAt = startedAt; } return { totalActive, earliestStartedAt }; } export class ActiveSubagentWatch { private readonly ids = new Set(); private rpcActive = 0; noteStarted(id?: string): void { if (id) this.ids.add(id); } noteComplete(id?: string): void { if (id) this.ids.delete(id); this.rpcActive = Math.max(0, this.rpcActive - 1); if (this.ids.size === 0 && this.rpcActive === 0) return; if (this.ids.size === 0) this.rpcActive = 0; } setRpcActive(count: number): void { this.rpcActive = Math.max(0, count); } get size(): number { return Math.max(this.ids.size, this.rpcActive); } get isActive(): boolean { return this.size > 0; } } type EventBus = { on(event: string, handler: (data: unknown) => unknown): (() => void) | void; emit(event: string, data: unknown): void; }; export function querySubagentFleet( events: EventBus, timeoutMs = 400, ): Promise<{ totalActive: number; earliestStartedAt?: number }> { return new Promise((resolve) => { const requestId = crypto.randomUUID(); const replyEvent = `${SUBAGENT_RPC_REPLY_PREFIX}${requestId}`; let settled = false; let unsubscribe: (() => void) | undefined; const finish = (result: { totalActive: number; earliestStartedAt?: number }) => { if (settled) return; settled = true; clearTimeout(timer); unsubscribe?.(); resolve(result); }; const timer = setTimeout(() => finish({ totalActive: 0 }), timeoutMs); const maybeUnsub = events.on(replyEvent, (reply) => finish(fleetFromRpcReply(reply))); unsubscribe = typeof maybeUnsub === "function" ? maybeUnsub : undefined; try { events.emit(SUBAGENT_RPC_REQUEST, { version: 1, requestId, method: "status", source: { extension: "pi-run-timer" }, }); } catch { finish({ totalActive: 0 }); } }); }