import type { TeamLogSourceTracker } from '../TeamLogSourceTracker'; import type { TeamChangeEvent } from '@shared/types'; interface TeamAliveProcessesReader { listAliveProcessTeams(): Promise; } interface TeamLogSourceTrackingHandle { enableTracking( teamName: string, consumer: 'stall_monitor' ): Promise<{ projectFingerprint: string | null; logSourceGeneration: string | null }>; disableTracking( teamName: string, consumer: 'stall_monitor' ): Promise<{ projectFingerprint: string | null; logSourceGeneration: string | null }>; } function unrefBackgroundTimer(timer: ReturnType): void { const maybeTimer = timer as { unref?: () => void }; maybeTimer.unref?.(); } export class ActiveTeamRegistry { private readonly activeTeams = new Set(); private reconcileTimer: ReturnType | null = null; constructor( private readonly teamDataService: TeamAliveProcessesReader, private readonly teamLogSourceTracker: Pick< TeamLogSourceTracker, 'enableTracking' | 'disableTracking' > & TeamLogSourceTrackingHandle, private readonly reconcileIntervalMs: number = 5 * 60_000 ) {} noteTeamChange(event: TeamChangeEvent): void { if ( event.type === 'member-spawn' || (event.type === 'lead-activity' && event.detail !== 'offline') ) { if (!this.activeTeams.has(event.teamName)) { this.activeTeams.add(event.teamName); void this.teamLogSourceTracker.enableTracking(event.teamName, 'stall_monitor'); } return; } if (event.type === 'task-log-change' || event.type === 'log-source-change') { if (!this.activeTeams.has(event.teamName)) { return; } } } async listActiveTeams(): Promise { return [...this.activeTeams].sort((left, right) => left.localeCompare(right)); } start(): void { if (this.reconcileTimer) { return; } void this.reconcile(); this.reconcileTimer = setInterval(() => { void this.reconcile(); }, this.reconcileIntervalMs); unrefBackgroundTimer(this.reconcileTimer); } async stop(): Promise { if (this.reconcileTimer) { clearInterval(this.reconcileTimer); this.reconcileTimer = null; } const teamNames = [...this.activeTeams]; this.activeTeams.clear(); await Promise.all( teamNames.map((teamName) => this.teamLogSourceTracker.disableTracking(teamName, 'stall_monitor') ) ); } async reconcile(): Promise { const aliveTeams = await this.teamDataService.listAliveProcessTeams(); const aliveSet = new Set(aliveTeams); for (const teamName of aliveTeams) { if (this.activeTeams.has(teamName)) { continue; } this.activeTeams.add(teamName); await this.teamLogSourceTracker.enableTracking(teamName, 'stall_monitor'); } for (const teamName of [...this.activeTeams]) { if (aliveSet.has(teamName)) { continue; } this.activeTeams.delete(teamName); await this.teamLogSourceTracker.disableTracking(teamName, 'stall_monitor'); } } }