import type { AgentRecord } from "../shared/types.js"; export type DeliveryCallback = (records: AgentRecord[], partial: boolean) => void; interface AgentGroup { groupId: string; agentIds: Set; completedRecords: Map; timeoutHandle?: ReturnType; delivered: boolean; isStraggler: boolean; } const DEFAULT_TIMEOUT = 30_000; const STRAGGLER_TIMEOUT = 15_000; export class GroupJoinManager { private groups = new Map(); private agentToGroup = new Map(); constructor( private deliverCb: DeliveryCallback, private groupTimeout = DEFAULT_TIMEOUT, ) {} registerGroup(groupId: string, agentIds: string[]): void { const group: AgentGroup = { groupId, agentIds: new Set(agentIds), completedRecords: new Map(), delivered: false, isStraggler: false, }; this.groups.set(groupId, group); for (const id of agentIds) { this.agentToGroup.set(id, groupId); } } onAgentComplete(record: AgentRecord): "delivered" | "held" | "pass" { const groupId = this.agentToGroup.get(record.id); if (!groupId) return "pass"; const group = this.groups.get(groupId); if (!group || group.delivered) return "pass"; group.completedRecords.set(record.id, record); if (group.completedRecords.size >= group.agentIds.size) { this.deliver(group, false); return "delivered"; } if (!group.timeoutHandle) { const timeout = group.isStraggler ? STRAGGLER_TIMEOUT : this.groupTimeout; group.timeoutHandle = setTimeout(() => this.onTimeout(group), timeout); } return "held"; } private onTimeout(group: AgentGroup): void { if (group.delivered) return; group.timeoutHandle = undefined; const remaining = new Set(); for (const id of group.agentIds) { if (!group.completedRecords.has(id)) remaining.add(id); } for (const id of group.completedRecords.keys()) { this.agentToGroup.delete(id); } this.deliverCb([...group.completedRecords.values()], true); group.completedRecords.clear(); group.agentIds = remaining; group.isStraggler = true; } private deliver(group: AgentGroup, partial: boolean): void { if (group.timeoutHandle) { clearTimeout(group.timeoutHandle); group.timeoutHandle = undefined; } group.delivered = true; this.deliverCb([...group.completedRecords.values()], partial); this.cleanupGroup(group.groupId); } private cleanupGroup(groupId: string): void { const group = this.groups.get(groupId); if (!group) return; for (const id of group.agentIds) { this.agentToGroup.delete(id); } this.groups.delete(groupId); } isGrouped(agentId: string): boolean { return this.agentToGroup.has(agentId); } dispose(): void { for (const group of this.groups.values()) { if (group.timeoutHandle) clearTimeout(group.timeoutHandle); } this.groups.clear(); this.agentToGroup.clear(); } }