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() {
|
||||
const {
|
||||
agents, pendingTasks, notifications, connected,
|
||||
agents, pendingTasks, notifications, connected, reconnectAttempt,
|
||||
refreshTrigger, clearNotifications
|
||||
} = useAgentManager();
|
||||
|
||||
@@ -53,7 +53,7 @@ export default function AgentOffice() {
|
||||
|
||||
return (
|
||||
<div className="ao-root">
|
||||
<TopBar connected={connected} />
|
||||
<TopBar connected={connected} reconnectAttempt={reconnectAttempt} />
|
||||
<div className="ao-main">
|
||||
<div className="ao-grid-wrap">
|
||||
<AgentGrid
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
// 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 (
|
||||
<div className="ao-topbar">
|
||||
<div className="ao-topbar-left">
|
||||
<span className="ao-topbar-title">Agent Office</span>
|
||||
<span className={`ao-topbar-status ${connected ? 'connected' : 'disconnected'}`}>
|
||||
● {connected ? 'Connected' : 'Disconnected'}
|
||||
● {statusText}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user