@@ -100,17 +100,28 @@ pub async fn rpc_handler(State(state): State<AppState>, Json(req): Json<RpcReque
100100pub async fn invoke_method ( state : AppState , method : & str , params : Value ) -> Result < Value , String > {
101101 let result = invoke_method_inner ( state, method, params) . await ;
102102
103- // Session auto-cleanup: If the backend says we're unauthorized,
104- // we should reflect that locally by clearing the stored token.
103+ // Session auto-cleanup: if the backend says we're unauthorized, publish
104+ // a `SessionExpired` event. The credentials subscriber clears the stored
105+ // token, flips the scheduler-gate signed-out override so background
106+ // workers stand down, and (eventually) pushes a sign-out to the UI.
107+ // Centralising via the event bus means 401 detection from any path
108+ // (this one, `llm_provider.api_error`, …) gets the same teardown.
105109 if let Err ( ref msg) = result {
106110 if is_session_expired_error ( msg) {
107111 log:: warn!(
108- "[jsonrpc] backend returned 401 for method '{}' — clearing stored session " ,
112+ "[jsonrpc] backend returned 401 for method '{}' — publishing SessionExpired " ,
109113 method
110114 ) ;
111- if let Ok ( config) = crate :: openhuman:: config:: rpc:: load_config_with_timeout ( ) . await {
112- let _ = crate :: openhuman:: credentials:: rpc:: clear_session ( & config) . await ;
113- }
115+ // Scrub before publishing — subscribers log `reason`, and the
116+ // upstream error string could include API keys / tokens from
117+ // pasted-through provider replies. `sanitize_api_error` runs
118+ // `scrub_secret_patterns` and truncates.
119+ crate :: core:: event_bus:: publish_global (
120+ crate :: core:: event_bus:: DomainEvent :: SessionExpired {
121+ source : format ! ( "jsonrpc.invoke_method:{method}" ) ,
122+ reason : crate :: openhuman:: providers:: ops:: sanitize_api_error ( msg) ,
123+ } ,
124+ ) ;
114125 }
115126 }
116127
@@ -944,6 +955,42 @@ fn register_domain_subscribers(
944955 // (otherwise they fall back to `Policy::Normal` and miss the
945956 // initial throttle decision on battery-powered hosts).
946957 crate :: openhuman:: scheduler_gate:: init_global ( & config) ;
958+
959+ // Seed the scheduler-gate signed-out override from the on-disk
960+ // session. Without this, a sidecar that boots with no stored JWT
961+ // would happily spin up cron / channel loops and fire LLM requests
962+ // that all 401 immediately.
963+ match crate :: api:: jwt:: get_session_token ( & config) {
964+ Ok ( Some ( _) ) => {
965+ crate :: openhuman:: scheduler_gate:: set_signed_out ( false ) ;
966+ }
967+ Ok ( None ) => {
968+ log:: info!(
969+ "[auth] no session token at startup — scheduler gate set to signed_out"
970+ ) ;
971+ crate :: openhuman:: scheduler_gate:: set_signed_out ( true ) ;
972+ }
973+ Err ( err) => {
974+ log:: warn!(
975+ "[auth] failed to read session token at startup ({err}) — assuming signed_out"
976+ ) ;
977+ crate :: openhuman:: scheduler_gate:: set_signed_out ( true ) ;
978+ }
979+ }
980+
981+ // Register the SessionExpired handler before any subscribers that
982+ // might publish 401-derived events, so the very first 401 is
983+ // routed through `clear_session` + the scheduler-gate override.
984+ if let Some ( handle) = crate :: core:: event_bus:: subscribe_global ( Arc :: new (
985+ crate :: openhuman:: credentials:: bus:: SessionExpiredSubscriber :: new ( ) ,
986+ ) ) {
987+ std:: mem:: forget ( handle) ;
988+ } else {
989+ log:: warn!(
990+ "[event_bus] failed to register SessionExpired subscriber — bus not initialized"
991+ ) ;
992+ }
993+
947994 crate :: openhuman:: memory:: tree:: jobs:: start ( config. clone ( ) ) ;
948995
949996 // Restart requests go through a subscriber so every trigger path shares
@@ -970,7 +1017,7 @@ fn register_domain_subscribers(
9701017 crate :: openhuman:: agent:: bus:: register_agent_handlers ( ) ;
9711018
9721019 log:: info!(
973- "[event_bus] domain subscribers registered (webhook, channel, health, conversation, composio, restart, proactive, agent)"
1020+ "[event_bus] domain subscribers registered (webhook, channel, health, conversation, composio, restart, proactive, agent, session_expired )"
9741021 ) ;
9751022 } ) ;
9761023}
0 commit comments