Skip to content

Commit 28dc53d

Browse files
committed
fix(web): add exponential backoff + max retries to SSE reconnect
SSE reconnect was looping infinitely when the stream kept dropping (e.g. during long Claude thinking pauses). Now uses exponential backoff (2s, 4s, 8s, 16s, 32s) with a max of 5 retries. Counter resets on successful connection.
1 parent c327f0f commit 28dc53d

1 file changed

Lines changed: 20 additions & 1 deletion

File tree

apps/web/src/features/session/ContainerSession.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,14 @@ export default function ContainerSession({
5151

5252
const lineIdRef = useRef(0)
5353
const sseOffsetRef = useRef(0)
54+
const reconnectCountRef = useRef(0)
5455
const eventSourceRef = useRef<EventSource | null>(null)
5556
const mountedRef = useRef(true)
5657
const inputRef = useRef<HTMLInputElement>(null)
5758

59+
const MAX_RECONNECTS = 5
60+
const RECONNECT_BASE_DELAY_MS = 2000
61+
5862
const appendLine = useCallback((text: string, kind?: TerminalLine['kind']) => {
5963
if (!mountedRef.current) return
6064
lineIdRef.current += 1
@@ -73,6 +77,7 @@ export default function ContainerSession({
7377
const abortController = new AbortController()
7478
lineIdRef.current = 0
7579
sseOffsetRef.current = 0
80+
reconnectCountRef.current = 0
7681
setLines([])
7782

7883
async function init() {
@@ -111,6 +116,19 @@ export default function ContainerSession({
111116
}
112117

113118
async function pollSessionStatus(sessionId: string) {
119+
reconnectCountRef.current += 1
120+
if (reconnectCountRef.current > MAX_RECONNECTS) {
121+
setStatus('failed')
122+
setError('Lost connection to session')
123+
appendLine('[error] Lost connection after multiple retries.')
124+
return
125+
}
126+
127+
// Exponential backoff: 2s, 4s, 8s, 16s, 32s
128+
const delay = RECONNECT_BASE_DELAY_MS * Math.pow(2, reconnectCountRef.current - 1)
129+
await new Promise((r) => setTimeout(r, delay))
130+
if (!mountedRef.current) return
131+
114132
try {
115133
const fresh = await getContainerSession(sessionId)
116134
if (!mountedRef.current) return
@@ -121,7 +139,6 @@ export default function ContainerSession({
121139
setError('Container failed')
122140
appendLine('[error] Container failed.')
123141
} else if (fresh.status === 'running') {
124-
appendLine('[reconnecting] Stream dropped, container still running...')
125142
connectStream(sessionId)
126143
}
127144
} catch {
@@ -146,6 +163,8 @@ export default function ContainerSession({
146163
source.addEventListener('open', () => {
147164
if (!mountedRef.current) return
148165
setStatus('running')
166+
// Connection succeeded — reset reconnect counter
167+
reconnectCountRef.current = 0
149168
})
150169

151170
// Default message event — parse Claude Code stream-json

0 commit comments

Comments
 (0)