@@ -32,6 +32,7 @@ use std::sync::atomic::AtomicU64;
3232use std:: sync:: atomic:: Ordering ;
3333
3434use crate :: agent_identity:: AgentIdentityManager ;
35+ use crate :: agent_identity:: AgentTaskRuntimeMismatch ;
3536use crate :: agent_identity:: RegisteredAgentTask ;
3637use codex_api:: ApiError ;
3738use codex_api:: CompactClient as ApiCompactClient ;
@@ -159,7 +160,7 @@ struct ModelClientState {
159160 include_timing_metrics : bool ,
160161 beta_features_header : Option < String > ,
161162 disable_websockets : AtomicBool ,
162- cached_websocket_session : StdMutex < WebsocketSession > ,
163+ cached_websocket_session : StdMutex < CachedWebsocketSession > ,
163164}
164165
165166/// Resolved API client setup for a single request attempt.
@@ -244,6 +245,12 @@ struct WebsocketSession {
244245 connection_reused : StdMutex < bool > ,
245246}
246247
248+ #[ derive( Debug , Default ) ]
249+ struct CachedWebsocketSession {
250+ agent_task : Option < RegisteredAgentTask > ,
251+ websocket_session : WebsocketSession ,
252+ }
253+
247254impl WebsocketSession {
248255 fn set_connection_reused ( & self , connection_reused : bool ) {
249256 * self
@@ -360,7 +367,7 @@ impl ModelClient {
360367 include_timing_metrics,
361368 beta_features_header,
362369 disable_websockets : AtomicBool :: new ( false ) ,
363- cached_websocket_session : StdMutex :: new ( WebsocketSession :: default ( ) ) ,
370+ cached_websocket_session : StdMutex :: new ( CachedWebsocketSession :: default ( ) ) ,
364371 } ) ,
365372 }
366373 }
@@ -377,18 +384,15 @@ impl ModelClient {
377384 & self ,
378385 agent_task : Option < RegisteredAgentTask > ,
379386 ) -> ModelClientSession {
380- let cache_websocket_session_on_drop = agent_task. is_none ( ) ;
381- let websocket_session = if agent_task. is_some ( ) {
382- drop ( self . take_cached_websocket_session ( ) ) ;
383- WebsocketSession :: default ( )
384- } else {
385- self . take_cached_websocket_session ( )
386- } ;
387+ // WebSocket auth is bound to the task that opened the connection. Reuse only when the
388+ // cached connection was created for the same task, and drop mismatched taskless/task-scoped
389+ // sessions rather than mixing auth contexts.
390+ let websocket_session = self . take_cached_websocket_session ( agent_task. as_ref ( ) ) ;
387391 ModelClientSession {
388392 client : self . clone ( ) ,
389393 websocket_session,
390394 agent_task,
391- cache_websocket_session_on_drop,
395+ cache_websocket_session_on_drop : true ,
392396 turn_state : Arc :: new ( OnceLock :: new ( ) ) ,
393397 }
394398 }
@@ -401,12 +405,12 @@ impl ModelClient {
401405 self . state
402406 . window_generation
403407 . store ( window_generation, Ordering :: Relaxed ) ;
404- self . store_cached_websocket_session ( WebsocketSession :: default ( ) ) ;
408+ self . clear_cached_websocket_session ( ) ;
405409 }
406410
407411 pub ( crate ) fn advance_window_generation ( & self ) {
408412 self . state . window_generation . fetch_add ( 1 , Ordering :: Relaxed ) ;
409- self . store_cached_websocket_session ( WebsocketSession :: default ( ) ) ;
413+ self . clear_cached_websocket_session ( ) ;
410414 }
411415
412416 fn current_window_id ( & self ) -> String {
@@ -415,21 +419,44 @@ impl ModelClient {
415419 format ! ( "{conversation_id}:{window_generation}" )
416420 }
417421
418- fn take_cached_websocket_session ( & self ) -> WebsocketSession {
422+ fn take_cached_websocket_session (
423+ & self ,
424+ agent_task : Option < & RegisteredAgentTask > ,
425+ ) -> WebsocketSession {
419426 let mut cached_websocket_session = self
420427 . state
421428 . cached_websocket_session
422429 . lock ( )
423430 . unwrap_or_else ( std:: sync:: PoisonError :: into_inner) ;
424- std:: mem:: take ( & mut * cached_websocket_session)
431+ if cached_websocket_session. agent_task . as_ref ( ) == agent_task {
432+ return std:: mem:: take ( & mut * cached_websocket_session) . websocket_session ;
433+ }
434+
435+ * cached_websocket_session = CachedWebsocketSession :: default ( ) ;
436+ WebsocketSession :: default ( )
437+ }
438+
439+ fn store_cached_websocket_session (
440+ & self ,
441+ agent_task : Option < RegisteredAgentTask > ,
442+ websocket_session : WebsocketSession ,
443+ ) {
444+ * self
445+ . state
446+ . cached_websocket_session
447+ . lock ( )
448+ . unwrap_or_else ( std:: sync:: PoisonError :: into_inner) = CachedWebsocketSession {
449+ agent_task,
450+ websocket_session,
451+ } ;
425452 }
426453
427- fn store_cached_websocket_session ( & self , websocket_session : WebsocketSession ) {
454+ fn clear_cached_websocket_session ( & self ) {
428455 * self
429456 . state
430457 . cached_websocket_session
431458 . lock ( )
432- . unwrap_or_else ( std:: sync:: PoisonError :: into_inner) = websocket_session ;
459+ . unwrap_or_else ( std:: sync:: PoisonError :: into_inner) = CachedWebsocketSession :: default ( ) ;
433460 }
434461
435462 pub ( crate ) fn force_http_fallback (
@@ -449,7 +476,7 @@ impl ModelClient {
449476 ) ;
450477 }
451478
452- self . store_cached_websocket_session ( WebsocketSession :: default ( ) ) ;
479+ self . clear_cached_websocket_session ( ) ;
453480 activated
454481 }
455482
@@ -727,6 +754,15 @@ impl ModelClient {
727754 . authorization_header_for_task ( agent_task)
728755 . await
729756 . map_err ( |err| {
757+ if let Some ( mismatch) = err. downcast_ref :: < AgentTaskRuntimeMismatch > ( ) {
758+ debug ! (
759+ agent_runtime_id = %mismatch. agent_runtime_id,
760+ task_id = %mismatch. task_id,
761+ stored_agent_runtime_id = %mismatch. stored_agent_runtime_id,
762+ "agent task no longer matches stored identity"
763+ ) ;
764+ return CodexErr :: AgentTaskStale ;
765+ }
730766 CodexErr :: Stream (
731767 format ! ( "failed to build agent assertion authorization: {err}" ) ,
732768 None ,
@@ -883,12 +919,16 @@ impl Drop for ModelClientSession {
883919 let websocket_session = std:: mem:: take ( & mut self . websocket_session ) ;
884920 if self . cache_websocket_session_on_drop {
885921 self . client
886- . store_cached_websocket_session ( websocket_session) ;
922+ . store_cached_websocket_session ( self . agent_task . clone ( ) , websocket_session) ;
887923 }
888924 }
889925}
890926
891927impl ModelClientSession {
928+ pub ( crate ) fn agent_task ( & self ) -> Option < & RegisteredAgentTask > {
929+ self . agent_task . as_ref ( )
930+ }
931+
892932 pub ( crate ) fn disable_cached_websocket_session_on_drop ( & mut self ) {
893933 self . cache_websocket_session_on_drop = false ;
894934 }
0 commit comments