feat(agent-office): WS reconnect exponential backoff + status detail

- 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) <noreply@anthropic.com>
This commit is contained in:
2026-05-18 08:25:18 +09:00
parent 0dce449124
commit b713f00bf9
3 changed files with 33 additions and 8 deletions

View File

@@ -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,