@@ -110,6 +110,7 @@ const TERMINAL_SESSION_STATUSES = new Set(["idle", "completed", "failed", "termi
110110
111111const DEFAULT_POLL_INTERVAL_MS = 2000 ;
112112const DEFAULT_POLL_TIMEOUT_MS = 10 * 60 * 1000 ;
113+ const POLL_INITIAL_INTERVAL_MS = 300 ;
113114
114115export function resolveAgentName ( agents : Record < string , unknown > | undefined , agentName ?: string ) : string {
115116 if ( agentName ) return agentName ;
@@ -255,8 +256,9 @@ export async function collectEventsUntilTerminal(
255256 } = { } ,
256257) : Promise < Omit < CollectedSessionEvents , "eventId" > > {
257258 const start = Date . now ( ) ;
258- const pollIntervalMs = options . pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS ;
259+ const maxPollIntervalMs = options . pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS ;
259260 const pollTimeoutMs = options . pollTimeoutMs ?? DEFAULT_POLL_TIMEOUT_MS ;
261+ let currentIntervalMs = Math . min ( POLL_INITIAL_INTERVAL_MS , maxPollIntervalMs ) ;
260262 let terminalStatus = "idle" ;
261263 let result : ProviderSessionEventList | undefined ;
262264
@@ -274,7 +276,8 @@ export async function collectEventsUntilTerminal(
274276 terminalStatus = terminalEvent . status ;
275277 break ;
276278 }
277- await delay ( pollIntervalMs ) ;
279+ await delay ( currentIntervalMs ) ;
280+ currentIntervalMs = Math . min ( currentIntervalMs * 2 , maxPollIntervalMs ) ;
278281 }
279282 } else {
280283 while ( true ) {
@@ -284,7 +287,8 @@ export async function collectEventsUntilTerminal(
284287 terminalStatus = session . status ;
285288 break ;
286289 }
287- await delay ( pollIntervalMs ) ;
290+ await delay ( currentIntervalMs ) ;
291+ currentIntervalMs = Math . min ( currentIntervalMs * 2 , maxPollIntervalMs ) ;
288292 }
289293
290294 result = await adapter . listSessionEvents ( sessionId , { limit : 100 } ) ;
@@ -537,13 +541,40 @@ function buildAgentNameByRemoteId(ctx: ProjectRuntimeContext, provider: string):
537541}
538542
539543// Qoder: send first (returns event ID), then stream from that ID to avoid missing events.
544+ // When the provider closes the SSE connection mid-turn (e.g. after emitting a
545+ // `session.status_idle` with `stop_reason=requires_action` for tool execution),
546+ // we reconnect automatically until the stream delivers a true terminal status.
540547async function * streamWithResume (
541548 adapter : SessionWorkflowAdapter ,
542549 sessionId : string ,
543550 message : string ,
544551) : AsyncIterable < ProviderSessionEvent > {
545552 const eventId = await adapter . sendSessionMessage ( sessionId , message ) ;
546- yield * adapter . streamSessionEvents ( sessionId , eventId ? { after_id : eventId } : undefined ) ;
553+ let lastEventId : string | undefined = eventId ;
554+ let reachedTerminal = false ;
555+ let reconnectIntervalMs = POLL_INITIAL_INTERVAL_MS ;
556+ const start = Date . now ( ) ;
557+
558+ while ( ! reachedTerminal ) {
559+ assertNotTimedOut ( start , DEFAULT_POLL_TIMEOUT_MS ) ;
560+ for await ( const event of adapter . streamSessionEvents (
561+ sessionId ,
562+ lastEventId ? { after_id : lastEventId } : undefined ,
563+ ) ) {
564+ if ( event . id ) lastEventId = event . id ;
565+ yield event ;
566+ if ( event . type === "status" && isTerminalSessionStatus ( event . status ) ) {
567+ reachedTerminal = true ;
568+ break ;
569+ }
570+ }
571+ if ( ! reachedTerminal ) {
572+ // SSE closed without a terminal event — back off before reconnecting
573+ // using exponential backoff capped at DEFAULT_POLL_INTERVAL_MS.
574+ await delay ( reconnectIntervalMs ) ;
575+ reconnectIntervalMs = Math . min ( reconnectIntervalMs * 2 , DEFAULT_POLL_INTERVAL_MS ) ;
576+ }
577+ }
547578}
548579
549580// Claude/Bailian: connect stream first, then send — provider pushes events immediately on send.
0 commit comments