mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage-server
synced 2026-05-18 20:08:19 +00:00
fix(supervisor): sweep stale activeCalls before returning to Live Monitor
Bug 560: Live Call Monitor showed ghost calls with runaway timers when the agent wasn't on a call. Cause — activeCalls Map only added on 'Answered' and deleted on 'Disconnect'; a missed Disconnect (sidecar restart, Ozonetel subscription hiccup, network blip) left the entry lingering forever. getActiveCalls() now sweeps stale entries before returning: - drop if startTime is older than 30 minutes - drop if the mapped agent is currently ready / offline / paused (agent can't be on a call in any of those states) Each sweep logs the reason so we can track how often this fires.
This commit is contained in:
@@ -257,7 +257,52 @@ export class SupervisorService implements OnModuleInit {
|
||||
this.agentStateSubject.next({ agentId, state: 'force-logout' as any, timestamp: new Date().toISOString() });
|
||||
}
|
||||
|
||||
// Max plausible call length before the entry is treated as orphaned.
|
||||
// Real Ozonetel calls cap out far short of this — 30 minutes is a safe
|
||||
// ceiling for a hospital call-center context. If a genuinely longer
|
||||
// call existed, losing it from Live Monitor is preferable to the ghost
|
||||
// state (supervisors lose trust in the dashboard otherwise).
|
||||
private static readonly MAX_ACTIVE_CALL_AGE_MS = 30 * 60 * 1000;
|
||||
|
||||
// Agent states that are incompatible with having an active call. If the
|
||||
// mapped agent is currently in one of these, the activeCalls entry is
|
||||
// definitely stale (e.g. Disconnect webhook was dropped).
|
||||
private static readonly NON_CALL_AGENT_STATES = new Set(['ready', 'offline', 'paused']);
|
||||
|
||||
getActiveCalls(): ActiveCall[] {
|
||||
// Sweep stale entries before returning. The activeCalls Map is a
|
||||
// best-effort in-memory projection of Ozonetel call events — if
|
||||
// Ozonetel drops a Disconnect (network blip, subscription hiccup,
|
||||
// sidecar restart mid-call), the entry lingers forever and the
|
||||
// Live Call Monitor shows a ghost call with a runaway timer.
|
||||
//
|
||||
// Two signals identify staleness:
|
||||
// 1. The associated agent is not in a busy state (ready, offline,
|
||||
// paused — they can't be on a call).
|
||||
// 2. startTime is older than MAX_ACTIVE_CALL_AGE_MS (hard ceiling
|
||||
// regardless of agent-state signal).
|
||||
const now = Date.now();
|
||||
const toDelete: string[] = [];
|
||||
|
||||
for (const [ucid, call] of this.activeCalls.entries()) {
|
||||
const ageMs = now - new Date(call.startTime).getTime();
|
||||
if (isNaN(ageMs)) continue;
|
||||
|
||||
if (ageMs > SupervisorService.MAX_ACTIVE_CALL_AGE_MS) {
|
||||
toDelete.push(ucid);
|
||||
this.logger.warn(`[ACTIVE-CALLS] Sweep: dropping ${ucid} (age ${Math.round(ageMs / 60000)}m, exceeds ${SupervisorService.MAX_ACTIVE_CALL_AGE_MS / 60000}m cap)`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const agentState = this.agentStates.get(call.agentId)?.state;
|
||||
if (agentState && SupervisorService.NON_CALL_AGENT_STATES.has(agentState)) {
|
||||
toDelete.push(ucid);
|
||||
this.logger.warn(`[ACTIVE-CALLS] Sweep: dropping ${ucid} — agent ${call.agentId} is ${agentState}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const ucid of toDelete) this.activeCalls.delete(ucid);
|
||||
|
||||
return Array.from(this.activeCalls.values());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user