Files
helix-engage/src/state/sip-manager.ts
saridsa2 cb4894ddc3 feat: Global E2E tests, multi-agent fixes, SIP agent tracing
- 13 Global Hospital smoke tests (CC Agent + Supervisor)
- Auto-unlock agent session in test setup via maint API
- agent-status-toggle sends agentId from localStorage (was missing)
- maint-otp-modal injects agentId from localStorage into all calls
- SIP manager logs agent identity on connect/disconnect/state changes
- seed-data.ts: added CC agent + marketing users, idempotent member
  creation, cleanup phase before seeding
- .gitignore: exclude test-results/ and playwright-report/

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 12:12:22 +05:30

118 lines
3.8 KiB
TypeScript

import { SIPClient } from '@/lib/sip-client';
import type { SIPConfig, ConnectionStatus, CallState } from '@/types/sip';
// Singleton SIP client — survives React StrictMode remounts
let sipClient: SIPClient | null = null;
let connected = false;
let outboundPending = false;
let outboundActive = false;
let activeAgentId: string | null = null;
type StateUpdater = {
setConnectionStatus: (status: ConnectionStatus) => void;
setCallState: (state: CallState) => void;
setCallerNumber: (number: string | null) => void;
setCallUcid: (ucid: string | null) => void;
};
let stateUpdater: StateUpdater | null = null;
export function registerSipStateUpdater(updater: StateUpdater) {
stateUpdater = updater;
}
export function setOutboundPending(pending: boolean) {
outboundPending = pending;
}
export function isOutboundPending(): boolean {
return outboundPending;
}
export function connectSip(config: SIPConfig): void {
if (connected || sipClient?.isRegistered() || sipClient?.isConnected()) {
return;
}
if (!config.wsServer || !config.uri) {
console.warn('SIP config incomplete — wsServer and uri required');
return;
}
if (sipClient) {
sipClient.disconnect();
}
// Resolve agent identity for logging
try {
const agentCfg = JSON.parse(localStorage.getItem('helix_agent_config') ?? '{}');
activeAgentId = agentCfg.ozonetelAgentId ?? null;
const ext = config.uri?.match(/sip:(\d+)@/)?.[1] ?? 'unknown';
console.log(`[SIP] Connecting agent=${activeAgentId} ext=${ext} ws=${config.wsServer}`);
} catch {
console.log(`[SIP] Connecting uri=${config.uri}`);
}
connected = true;
stateUpdater?.setConnectionStatus('connecting');
sipClient = new SIPClient(
config,
(status) => stateUpdater?.setConnectionStatus(status),
(state, number, ucid) => {
// Auto-answer SIP when it's a bridge from our outbound call
if (state === 'ringing-in' && outboundPending) {
outboundPending = false;
outboundActive = true;
console.log('[SIP-MGR] Outbound bridge detected — auto-answering');
setTimeout(() => {
sipClient?.answer();
setTimeout(() => stateUpdater?.setCallState('active'), 300);
}, 500);
if (ucid) stateUpdater?.setCallUcid(ucid);
return;
}
console.log(`[SIP] ${activeAgentId} | state=${state} | caller=${number ?? 'none'} | ucid=${ucid ?? 'none'} | outbound=${outboundActive}`);
stateUpdater?.setCallState(state);
if (!outboundActive && number !== undefined) {
stateUpdater?.setCallerNumber(number ?? null);
}
if (ucid) stateUpdater?.setCallUcid(ucid);
if (state === 'ended' || state === 'failed') {
outboundActive = false;
outboundPending = false;
}
},
);
sipClient.connect();
}
export function disconnectSip(force = false): void {
// Guard: don't disconnect SIP during an active or pending call
// unless explicitly forced (e.g., logout, page unload).
// This prevents React re-render cycles from killing the
// SIP WebSocket mid-dial.
if (!force && (outboundPending || outboundActive)) {
console.log('[SIP-MGR] Disconnect blocked — call in progress');
return;
}
console.log(`[SIP] Disconnecting agent=${activeAgentId}` + (force ? ' (forced)' : ''));
sipClient?.disconnect();
sipClient = null;
connected = false;
outboundPending = false;
outboundActive = false;
activeAgentId = null;
stateUpdater?.setConnectionStatus('disconnected');
stateUpdater?.setCallUcid(null);
}
export function getSipClient(): SIPClient | null {
return sipClient;
}