@@ -116,6 +116,9 @@ pub type SharedConfig = Arc<RwLock<Config>>;
116116/// Shared model context, updated on reload.
117117pub type SharedModelCtx = Arc < RwLock < Option < Arc < ModelContext > > > > ;
118118
119+ /// Shared Copilot session, updated when provider changes.
120+ pub type SharedCopilotSession = Arc < RwLock < Option < Arc < CopilotSession > > > > ;
121+
119122/// Shared task manager for first-class task orchestration.
120123pub type SharedTaskManager = Arc < crate :: tasks:: TaskManager > ;
121124
@@ -196,6 +199,79 @@ fn try_import_openclaw_token(vault: &mut SecretsManager) -> Option<CopilotSessio
196199 ) )
197200}
198201
202+ /// Initialize a CopilotSession if the provider requires one.
203+ ///
204+ /// Checks multiple sources in order:
205+ /// 1. Imported session token from vault (GITHUB_COPILOT_SESSION)
206+ /// 2. Fresh token from OpenClaw (~/.openclaw/credentials/github-copilot.token.json)
207+ /// 3. OAuth token from model context
208+ async fn init_copilot_session (
209+ provider : & str ,
210+ api_key : Option < & str > ,
211+ vault : & SharedVault ,
212+ ) -> Option < Arc < CopilotSession > > {
213+ if !crate_providers:: needs_copilot_session ( provider) {
214+ return None ;
215+ }
216+
217+ let mut vault_guard = vault. lock ( ) . await ;
218+ let session_result = vault_guard. get_secret ( "GITHUB_COPILOT_SESSION" , true ) ;
219+
220+ let mut session_from_import = match & session_result {
221+ Ok ( Some ( json_str) ) => {
222+ debug ! ( "Found GITHUB_COPILOT_SESSION in vault" ) ;
223+ serde_json:: from_str :: < serde_json:: Value > ( json_str)
224+ . ok ( )
225+ . and_then ( |json| {
226+ let token = json. get ( "session_token" ) ?. as_str ( ) ?. to_string ( ) ;
227+ let expires_at = json. get ( "expires_at" ) ?. as_i64 ( ) ?;
228+
229+ let now = std:: time:: SystemTime :: now ( )
230+ . duration_since ( std:: time:: UNIX_EPOCH )
231+ . unwrap_or_default ( )
232+ . as_secs ( ) as i64 ;
233+
234+ let remaining = expires_at - now;
235+ debug ! ( remaining_seconds = remaining, "Session expiry check" ) ;
236+
237+ if remaining > 60 {
238+ Some ( CopilotSession :: from_session_token ( token, expires_at) )
239+ } else {
240+ debug ! ( "Session expired or expiring soon" ) ;
241+ None
242+ }
243+ } )
244+ }
245+ Ok ( None ) => {
246+ debug ! ( "GITHUB_COPILOT_SESSION not found in vault" ) ;
247+ None
248+ }
249+ Err ( e) => {
250+ warn ! ( error = %e, "Failed to read GITHUB_COPILOT_SESSION" ) ;
251+ None
252+ }
253+ } ;
254+
255+ // If vault session is expired/missing, try to auto-import from OpenClaw
256+ if session_from_import. is_none ( ) {
257+ if let Some ( session) = try_import_openclaw_token ( & mut vault_guard) {
258+ session_from_import = Some ( session) ;
259+ }
260+ }
261+ drop ( vault_guard) ;
262+
263+ if let Some ( session) = session_from_import {
264+ debug ! ( "Using imported session token" ) ;
265+ Some ( Arc :: new ( session) )
266+ } else if let Some ( oauth) = api_key {
267+ debug ! ( "Falling back to OAuth token" ) ;
268+ Some ( Arc :: new ( CopilotSession :: new ( oauth. to_string ( ) ) ) )
269+ } else {
270+ warn ! ( "No OAuth token available for Copilot provider" ) ;
271+ None
272+ }
273+ }
274+
199275/// Run the gateway WebSocket server.
200276///
201277/// Accepts connections in a loop until the `cancel` token is triggered,
@@ -278,81 +354,14 @@ pub async fn run_gateway(
278354 ( None , None ) => None ,
279355 } ;
280356
281- // If the provider uses Copilot session tokens, check for:
282- // 1. Imported session token (GITHUB_COPILOT_SESSION) - use until expiry
283- // 2. Fresh token from OpenClaw (~/.openclaw/credentials/github-copilot.token.json)
284- // 3. OAuth token (GITHUB_COPILOT_TOKEN) - can refresh sessions
285- let copilot_session: Option < Arc < CopilotSession > > = if model_ctx
286- . as_ref ( )
287- . map ( |ctx| crate_providers:: needs_copilot_session ( & ctx. provider ) )
288- . unwrap_or ( false )
289- {
290- // First check for imported session token in our vault
291- let mut vault_guard = vault. lock ( ) . await ;
292- let session_result = vault_guard. get_secret ( "GITHUB_COPILOT_SESSION" , true ) ;
293-
294- let mut session_from_import = match & session_result {
295- Ok ( Some ( json_str) ) => {
296- debug ! ( "Found GITHUB_COPILOT_SESSION in vault" ) ;
297- serde_json:: from_str :: < serde_json:: Value > ( json_str)
298- . ok ( )
299- . and_then ( |json| {
300- let token = json. get ( "session_token" ) ?. as_str ( ) ?. to_string ( ) ;
301- let expires_at = json. get ( "expires_at" ) ?. as_i64 ( ) ?;
302-
303- let now = std:: time:: SystemTime :: now ( )
304- . duration_since ( std:: time:: UNIX_EPOCH )
305- . unwrap_or_default ( )
306- . as_secs ( ) as i64 ;
307-
308- let remaining = expires_at - now;
309- debug ! ( remaining_seconds = remaining, "Session expiry check" ) ;
310-
311- if remaining > 60 {
312- Some ( CopilotSession :: from_session_token ( token, expires_at) )
313- } else {
314- debug ! ( "Session expired or expiring soon" ) ;
315- None
316- }
317- } )
318- }
319- Ok ( None ) => {
320- debug ! ( "GITHUB_COPILOT_SESSION not found in vault" ) ;
321- None
322- }
323- Err ( e) => {
324- warn ! ( error = %e, "Failed to read GITHUB_COPILOT_SESSION" ) ;
325- None
326- }
327- } ;
328-
329- // If vault session is expired/missing, try to auto-import from OpenClaw
330- if session_from_import. is_none ( ) {
331- if let Some ( session) = try_import_openclaw_token ( & mut vault_guard) {
332- session_from_import = Some ( session) ;
333- }
334- }
335- drop ( vault_guard) ;
336-
337- if let Some ( session) = session_from_import {
338- eprintln ! ( "DEBUG: Using imported session token" ) ;
339- debug ! ( "Using imported session token" ) ;
340- Some ( Arc :: new ( session) )
341- } else {
342- // Fall back to OAuth token
343- if let Some ( oauth) = model_ctx. as_ref ( ) . and_then ( |ctx| ctx. api_key . clone ( ) ) {
344- eprintln ! ( "DEBUG: Falling back to OAuth token" ) ;
345- debug ! ( "Falling back to OAuth token" ) ;
346- Some ( Arc :: new ( CopilotSession :: new ( oauth) ) )
347- } else {
348- eprintln ! ( "DEBUG: No OAuth token available either - copilot_session will be None!" ) ;
349- warn ! ( "No OAuth token available either" ) ;
350- None
351- }
352- }
357+ // Initialize Copilot session if needed (uses the new helper function)
358+ let copilot_session: Option < Arc < CopilotSession > > = if let Some ( ref ctx) = model_ctx {
359+ init_copilot_session ( & ctx. provider , ctx. api_key . as_deref ( ) , & vault) . await
353360 } else {
354361 None
355362 } ;
363+ // Wrap in shared type so it can be updated when models change
364+ let shared_copilot_session: SharedCopilotSession = Arc :: new ( RwLock :: new ( copilot_session) ) ;
356365
357366 let model_ctx = model_ctx. map ( Arc :: new) ;
358367
@@ -386,7 +395,8 @@ pub async fn run_gateway(
386395 let messenger_models = model_registry. clone ( ) ;
387396 let messenger_cancel = cancel. child_token ( ) ;
388397 let mgr_clone = shared_mgr. clone ( ) ;
389- let messenger_copilot = copilot_session. clone ( ) ;
398+ // Read current copilot session from shared state
399+ let messenger_copilot = shared_copilot_session. read ( ) . await . clone ( ) ;
390400
391401 eprintln ! ( "DEBUG: Spawning messenger loop task..." ) ;
392402 tokio:: spawn ( async move {
@@ -436,7 +446,7 @@ pub async fn run_gateway(
436446 let ( stream, peer) = accepted?;
437447 let shared_cfg = shared_config. clone( ) ;
438448 let shared_ctx = shared_model_ctx. clone( ) ;
439- let session_clone = copilot_session . clone( ) ;
449+ let shared_session = shared_copilot_session . clone( ) ;
440450 let vault_clone = vault. clone( ) ;
441451 let skill_clone = skill_mgr. clone( ) ;
442452 let task_mgr_clone = task_mgr. clone( ) ;
@@ -460,7 +470,7 @@ pub async fn run_gateway(
460470
461471 if let Err ( err) = handle_connection(
462472 boxed_stream, peer, shared_cfg, shared_ctx,
463- session_clone , vault_clone, skill_clone, task_mgr_clone,
473+ shared_session , vault_clone, skill_clone, task_mgr_clone,
464474 observer_clone, limiter_clone, child_cancel,
465475 ) . await {
466476 debug!( peer = %peer, error = %err, "Connection error" ) ;
@@ -603,7 +613,7 @@ async fn handle_connection(
603613 peer : SocketAddr ,
604614 shared_config : SharedConfig ,
605615 shared_model_ctx : SharedModelCtx ,
606- copilot_session : Option < Arc < CopilotSession > > ,
616+ shared_copilot_session : SharedCopilotSession ,
607617 vault : SharedVault ,
608618 skill_mgr : SharedSkillManager ,
609619 task_mgr : SharedTaskManager ,
@@ -838,6 +848,9 @@ async fn handle_connection(
838848 . await
839849 . context ( "Failed to send model_connecting status" ) ?;
840850
851+ // Read current copilot session from shared state
852+ let copilot_session = shared_copilot_session. read ( ) . await . clone ( ) ;
853+
841854 match providers:: validate_model_connection (
842855 & http,
843856 & probe_ctx,
@@ -1215,6 +1228,17 @@ async fn handle_connection(
12151228 ( "(none)" . to_string( ) , "(none)" . to_string( ) )
12161229 } ;
12171230
1231+ // Reinitialize Copilot session if the new model needs it
1232+ if let Some ( ref ctx) = new_model_ctx {
1233+ let new_session = init_copilot_session(
1234+ & ctx. provider,
1235+ ctx. api_key. as_deref( ) ,
1236+ & vault,
1237+ ) . await ;
1238+ let mut session = shared_copilot_session. write( ) . await ;
1239+ * session = new_session;
1240+ }
1241+
12181242 {
12191243 let mut cfg = shared_config. write( ) . await ;
12201244 * cfg = new_config;
@@ -1282,6 +1306,8 @@ async fn handle_connection(
12821306
12831307 // Re-read model_ctx from shared state for each dispatch
12841308 let current_model_ctx = shared_model_ctx. read( ) . await . clone( ) ;
1309+ // Re-read copilot session from shared state
1310+ let copilot_session = shared_copilot_session. read( ) . await . clone( ) ;
12851311 let workspace_dir = config. workspace_dir( ) ;
12861312
12871313 // Inject thread context into system prompt if available
0 commit comments