[eric] agents: an outage parks the turn and resumes when the provider answers; OAuth connects survive a backend restart

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014wtspwSFzZmjCx9UNPAorQ
This commit is contained in:
ciregenz
2026-08-20 08:39:41 -07:00
co-authored by Claude Opus 5
parent d5934b1da1
commit 58e376496e
14 changed files with 652 additions and 4 deletions
+23
View File
@@ -118,6 +118,8 @@ export interface AgentSession {
framework_overhead_tokens?: number;
context_overflow?: { reason: string; message: string; at: string } | null;
rate_limited?: { retry_after_s: number | null; at: string } | null;
// Parked waiting for the connection back; unlike the pills above this can last minutes, so the UI has to say so.
reconnect_wait?: { retry_in_s: number | null; attempt: number | null; at: string } | null;
provider_retrying?: { attempt: number | null; delay_ms: number | null; at: string } | null;
context_recovered?: { at: string } | null;
// Set when a view-builder turn installed/changed deps, so the app card does a HARD reload (Vite restart) at turn-finish instead of the soft one. Reset when the next turn starts.
@@ -1051,6 +1053,25 @@ const agentsSlice = createSlice({
if (session) session.rate_limited = null;
},
setReconnectWait(
state,
action: PayloadAction<{ sessionId: string; retryInS: number | null; attempt: number | null }>
) {
const session = state.sessions[action.payload.sessionId];
if (session) {
session.reconnect_wait = {
retry_in_s: action.payload.retryInS,
attempt: action.payload.attempt,
at: new Date().toISOString(),
};
}
},
clearReconnectWait(state, action: PayloadAction<{ sessionId: string }>) {
const session = state.sessions[action.payload.sessionId];
if (session) session.reconnect_wait = null;
},
setAppDepsChanged(state, action: PayloadAction<{ sessionId: string }>) {
const session = state.sessions[action.payload.sessionId];
if (session) session.app_deps_changed = true;
@@ -1543,6 +1564,8 @@ export const {
setContextOverflow,
setRateLimited,
clearRateLimited,
setReconnectWait,
clearReconnectWait,
setProviderRetrying,
clearProviderRetrying,
setContextRecovered,
@@ -14,6 +14,8 @@ import {
updateSessionContext,
setContextOverflow,
setRateLimited,
setReconnectWait,
clearReconnectWait,
setProviderRetrying,
setContextRecovered,
setAppDepsChanged,
@@ -474,6 +476,8 @@ class WebSocketManager {
}
if (data.status === 'running' && session_id) {
store.dispatch(trackAgentNotification(session_id));
// The parked-for-reconnect pill describes a wait that just ended; leaving it up outlives the recovery it was announcing.
store.dispatch(clearReconnectWait({ sessionId: session_id }));
}
// Native OS notification when an agent finishes while the user is elsewhere: workflows already had this; long chat tasks deserve the same "it's done" tap on both platforms. Sub-agents stay silent (their parent's finish is the story).
if (data.status === 'completed' && session_id && document.hidden) {
@@ -715,6 +719,17 @@ class WebSocketManager {
}
break;
case 'agent:reconnect_wait':
// The turn is PARKED, not over: it retries itself on a widening schedule, and an agent that looks idle for fifteen minutes reads as broken.
if (session_id) {
store.dispatch(setReconnectWait({
sessionId: session_id,
retryInS: typeof data.retry_in_s === 'number' ? data.retry_in_s : null,
attempt: typeof data.attempt === 'number' ? data.attempt : null,
}));
}
break;
case 'agent:context_recovered':
// The backend hit a context-overflow crash mid-turn, rebuilt from its local copy, and retried on its own. Transient muted pill so the recovery is visible without reading like an error.
if (session_id) {