From b713f00bf90b9696f60e5146e6cb854aaeebb506 Mon Sep 17 00:00:00 2001 From: gahusb Date: Mon, 18 May 2026 08:25:18 +0900 Subject: [PATCH] feat(agent-office): WS reconnect exponential backoff + status detail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace fixed 3s reconnect with exponential backoff (1s/2s/4s/8s/16s/30s, capped). Reduces console noise when upstream WebSocket is blocked (e.g. DSM reverse proxy without WS upgrade headers). - ws.onerror swallowed (onclose still schedules reconnect) so the browser stops printing an unhandled-error pair per attempt. - Expose reconnectAttempt in hook; TopBar shows 'Connecting…' pre-first-attempt and 'Disconnected · 재연결 시도 #N' after. Root cause of WS failure is upstream (curl proves the endpoint itself is fine — see DSM reverse proxy WebSocket headers). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/pages/agent-office/AgentOffice.jsx | 4 +-- src/pages/agent-office/components/TopBar.jsx | 12 +++++++-- .../agent-office/hooks/useAgentManager.js | 25 ++++++++++++++++--- 3 files changed, 33 insertions(+), 8 deletions(-) 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,