diff --git a/src/pages/agent-office/AgentOffice.jsx b/src/pages/agent-office/AgentOffice.jsx index 224d6e2..e9e24d4 100644 --- a/src/pages/agent-office/AgentOffice.jsx +++ b/src/pages/agent-office/AgentOffice.jsx @@ -10,7 +10,7 @@ import './AgentOffice.css'; export default function AgentOffice() { const { - agents, pendingTasks, notifications, connected, + agents, pendingTasks, notifications, connected, reconnectAttempt, refreshTrigger, clearNotifications } = useAgentManager(); @@ -53,7 +53,7 @@ export default function AgentOffice() { return (
- +
Agent Office - ● {connected ? 'Connected' : 'Disconnected'} + ● {statusText}
diff --git a/src/pages/agent-office/hooks/useAgentManager.js b/src/pages/agent-office/hooks/useAgentManager.js index 70b35ae..436104b 100644 --- a/src/pages/agent-office/hooks/useAgentManager.js +++ b/src/pages/agent-office/hooks/useAgentManager.js @@ -1,25 +1,34 @@ // src/pages/agent-office/hooks/useAgentManager.js import { useState, useEffect, useRef, useCallback } from 'react'; -const WS_RECONNECT_DELAY = 3000; +// Exponential backoff with cap. 1s → 2s → 4s → 8s → 16s → 30s (cap) +const WS_BACKOFF_BASE_MS = 1000; +const WS_BACKOFF_CAP_MS = 30000; +const WS_BACKOFF_MAX_EXP = 5; export function useAgentManager() { const [agents, setAgents] = useState({}); // { agentId: { state, detail, task_id } } const [pendingTasks, setPendingTasks] = useState([]); // [{id, agent_id, task_type, input_data}] const [notifications, setNotifications] = useState({}); // { agentId: count } const [connected, setConnected] = useState(false); + const [reconnectAttempt, setReconnectAttempt] = useState(0); const [refreshTrigger, setRefreshTrigger] = useState(0); // 탭 데이터 리프레시용 const wsRef = useRef(null); const reconnectRef = useRef(null); const connectRef = useRef(null); + const attemptRef = useRef(0); const connect = useCallback(() => { const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws'; const ws = new WebSocket(`${protocol}://${window.location.host}/api/agent-office/ws`); wsRef.current = ws; - ws.onopen = () => setConnected(true); + ws.onopen = () => { + setConnected(true); + attemptRef.current = 0; + setReconnectAttempt(0); + }; ws.onmessage = (e) => { const msg = JSON.parse(e.data); @@ -69,10 +78,17 @@ export function useAgentManager() { ws.onclose = () => { setConnected(false); - reconnectRef.current = setTimeout(() => connectRef.current?.(), WS_RECONNECT_DELAY); + const exp = Math.min(attemptRef.current, WS_BACKOFF_MAX_EXP); + const delay = Math.min(WS_BACKOFF_BASE_MS * 2 ** exp, WS_BACKOFF_CAP_MS); + attemptRef.current += 1; + setReconnectAttempt(attemptRef.current); + reconnectRef.current = setTimeout(() => connectRef.current?.(), delay); }; - ws.onerror = () => ws.close(); + // onerror fires before onclose; swallow so the browser doesn't print an + // unhandled-error pair for every retry. onclose still runs and schedules + // the next attempt. + ws.onerror = () => {}; }, []); useEffect(() => { @@ -108,6 +124,7 @@ export function useAgentManager() { pendingTasks, notifications, connected, + reconnectAttempt, refreshTrigger, sendCommand, sendApproval,