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:
@@ -10,7 +10,7 @@ import './AgentOffice.css';
|
|||||||
|
|
||||||
export default function AgentOffice() {
|
export default function AgentOffice() {
|
||||||
const {
|
const {
|
||||||
agents, pendingTasks, notifications, connected,
|
agents, pendingTasks, notifications, connected, reconnectAttempt,
|
||||||
refreshTrigger, clearNotifications
|
refreshTrigger, clearNotifications
|
||||||
} = useAgentManager();
|
} = useAgentManager();
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ export default function AgentOffice() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ao-root">
|
<div className="ao-root">
|
||||||
<TopBar connected={connected} />
|
<TopBar connected={connected} reconnectAttempt={reconnectAttempt} />
|
||||||
<div className="ao-main">
|
<div className="ao-main">
|
||||||
<div className="ao-grid-wrap">
|
<div className="ao-grid-wrap">
|
||||||
<AgentGrid
|
<AgentGrid
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
// src/pages/agent-office/components/TopBar.jsx
|
// src/pages/agent-office/components/TopBar.jsx
|
||||||
export default function TopBar({ connected }) {
|
export default function TopBar({ connected, reconnectAttempt = 0 }) {
|
||||||
|
let statusText;
|
||||||
|
if (connected) {
|
||||||
|
statusText = 'Connected';
|
||||||
|
} else if (reconnectAttempt === 0) {
|
||||||
|
statusText = 'Connecting…';
|
||||||
|
} else {
|
||||||
|
statusText = `Disconnected · 재연결 시도 #${reconnectAttempt}`;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className="ao-topbar">
|
<div className="ao-topbar">
|
||||||
<div className="ao-topbar-left">
|
<div className="ao-topbar-left">
|
||||||
<span className="ao-topbar-title">Agent Office</span>
|
<span className="ao-topbar-title">Agent Office</span>
|
||||||
<span className={`ao-topbar-status ${connected ? 'connected' : 'disconnected'}`}>
|
<span className={`ao-topbar-status ${connected ? 'connected' : 'disconnected'}`}>
|
||||||
● {connected ? 'Connected' : 'Disconnected'}
|
● {statusText}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,25 +1,34 @@
|
|||||||
// src/pages/agent-office/hooks/useAgentManager.js
|
// src/pages/agent-office/hooks/useAgentManager.js
|
||||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
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() {
|
export function useAgentManager() {
|
||||||
const [agents, setAgents] = useState({}); // { agentId: { state, detail, task_id } }
|
const [agents, setAgents] = useState({}); // { agentId: { state, detail, task_id } }
|
||||||
const [pendingTasks, setPendingTasks] = useState([]); // [{id, agent_id, task_type, input_data}]
|
const [pendingTasks, setPendingTasks] = useState([]); // [{id, agent_id, task_type, input_data}]
|
||||||
const [notifications, setNotifications] = useState({}); // { agentId: count }
|
const [notifications, setNotifications] = useState({}); // { agentId: count }
|
||||||
const [connected, setConnected] = useState(false);
|
const [connected, setConnected] = useState(false);
|
||||||
|
const [reconnectAttempt, setReconnectAttempt] = useState(0);
|
||||||
const [refreshTrigger, setRefreshTrigger] = useState(0); // 탭 데이터 리프레시용
|
const [refreshTrigger, setRefreshTrigger] = useState(0); // 탭 데이터 리프레시용
|
||||||
|
|
||||||
const wsRef = useRef(null);
|
const wsRef = useRef(null);
|
||||||
const reconnectRef = useRef(null);
|
const reconnectRef = useRef(null);
|
||||||
const connectRef = useRef(null);
|
const connectRef = useRef(null);
|
||||||
|
const attemptRef = useRef(0);
|
||||||
|
|
||||||
const connect = useCallback(() => {
|
const connect = useCallback(() => {
|
||||||
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
||||||
const ws = new WebSocket(`${protocol}://${window.location.host}/api/agent-office/ws`);
|
const ws = new WebSocket(`${protocol}://${window.location.host}/api/agent-office/ws`);
|
||||||
wsRef.current = ws;
|
wsRef.current = ws;
|
||||||
|
|
||||||
ws.onopen = () => setConnected(true);
|
ws.onopen = () => {
|
||||||
|
setConnected(true);
|
||||||
|
attemptRef.current = 0;
|
||||||
|
setReconnectAttempt(0);
|
||||||
|
};
|
||||||
|
|
||||||
ws.onmessage = (e) => {
|
ws.onmessage = (e) => {
|
||||||
const msg = JSON.parse(e.data);
|
const msg = JSON.parse(e.data);
|
||||||
@@ -69,10 +78,17 @@ export function useAgentManager() {
|
|||||||
|
|
||||||
ws.onclose = () => {
|
ws.onclose = () => {
|
||||||
setConnected(false);
|
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(() => {
|
useEffect(() => {
|
||||||
@@ -108,6 +124,7 @@ export function useAgentManager() {
|
|||||||
pendingTasks,
|
pendingTasks,
|
||||||
notifications,
|
notifications,
|
||||||
connected,
|
connected,
|
||||||
|
reconnectAttempt,
|
||||||
refreshTrigger,
|
refreshTrigger,
|
||||||
sendCommand,
|
sendCommand,
|
||||||
sendApproval,
|
sendApproval,
|
||||||
|
|||||||
Reference in New Issue
Block a user