From 3bb431592590815088571dee6758b5db11829b21 Mon Sep 17 00:00:00 2001 From: saridsa2 Date: Mon, 20 Apr 2026 10:27:40 +0530 Subject: [PATCH] =?UTF-8?q?fix:=20persist=20LOGIN=20events=20for=20session?= =?UTF-8?q?=20rollup=20=E2=80=94=20fixes=20zero=20dashboard=20metrics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit History event persistence was gated behind if(mapped), but login returns null for state (UI waits for release). LOGIN events were never written to AgentEvent table → rollup computed 0 for loginDuration → idle/pause/ wrap all zero. Moved history persistence outside the state mapping gate. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/supervisor/supervisor.service.ts | 38 +++++++++++++++------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/src/supervisor/supervisor.service.ts b/src/supervisor/supervisor.service.ts index b81967e..83bb6b3 100644 --- a/src/supervisor/supervisor.service.ts +++ b/src/supervisor/supervisor.service.ts @@ -176,26 +176,30 @@ export class SupervisorService implements OnModuleInit { const priorState = this.agentStates.get(agentId)?.state; const mapped = this.mapOzonetelAction(action, eventData, pauseReason); + + // Persist to AgentEvent table regardless of state mapping. + // login returns null for state (UI waits for release/ready) but + // the history pipeline needs LOGIN to compute loginDuration. + const historyEventType = this.mapToHistoryEventType(action, priorState); + if (historyEventType) { + const resolvedPauseReason = (pauseReason || eventData || '') || null; + this.logger.log(`[AGENT-HISTORY] ${agentId} action=${action} → eventType=${historyEventType} priorState=${priorState ?? 'none'} mapped=${mapped ?? 'null'}`); + this.history.persistAgentEvent({ + ozonetelAgentId: agentId, + eventType: historyEventType, + eventAt: this.parseOzonetelTime(eventTime), + pauseReason: historyEventType === 'PAUSE' ? resolvedPauseReason : null, + }).catch((err) => { + this.logger.warn(`[AGENT-HISTORY] Failed to persist ${historyEventType} for ${agentId}: ${err?.message ?? err}`); + }); + } else { + this.logger.log(`[AGENT-HISTORY] ${agentId} action=${action} → no history event (priorState=${priorState ?? 'none'} mapped=${mapped ?? 'null'})`); + } + if (mapped) { this.agentStates.set(agentId, { state: mapped, timestamp: eventTime }); this.agentStateSubject.next({ agentId, state: mapped, timestamp: eventTime }); - this.logger.log(`[AGENT-STATE] Emitted: ${agentId} → ${mapped}`); - - // Persist to AgentEvent table. CALL_START/CALL_END are - // handled in handleCallEvent (they arrive via a separate - // Ozonetel webhook). Everything else is captured here. - // Pass priorState so 'release' → RESUME / ACW_END / READY can - // be disambiguated for the session rollup. - const historyEventType = this.mapToHistoryEventType(action, priorState); - if (historyEventType) { - const resolvedPauseReason = (pauseReason || eventData || '') || null; - this.history.persistAgentEvent({ - ozonetelAgentId: agentId, - eventType: historyEventType, - eventAt: this.parseOzonetelTime(eventTime), - pauseReason: historyEventType === 'PAUSE' ? resolvedPauseReason : null, - }).catch(() => {}); - } + this.logger.log(`[AGENT-STATE] ${agentId} ${priorState ?? 'none'} → ${mapped} (action=${action})`); // Layer 3: ACW auto-dispose safety net if (mapped === 'acw') {