@@ -137,18 +137,67 @@ pub fn user_facing_error(error: &str) -> String {
137137 "Web search is rate limited right now. Wait a moment and try again." . to_string ( )
138138 }
139139 Some ( code) if code == errors:: UNAVAILABLE => SEARCH_EXHAUSTED_MESSAGE . to_string ( ) ,
140+ Some ( code) if code == errors:: PROVIDER_UNAUTHORIZED => {
141+ // The detail names the provider and its HTTP status; the module
142+ // never puts the key or the query in it.
143+ format ! (
144+ "Web search is unavailable: {}. Update the key under Connections → Search, or \
145+ remove it to use managed search.",
146+ code_detail( error, code)
147+ )
148+ }
149+ Some ( code) if code == errors:: BACKEND_UNAUTHORIZED => backend_unauthorized_message ( false ) ,
140150 Some ( code) if code == errors:: INVALID_ARGUMENTS => {
141- let marker = format ! ( "{}{code}: " , errors:: PREFIX ) ;
142- let detail = error
143- . split_once ( marker. as_str ( ) )
144- . map ( |( _, detail) | detail)
145- . unwrap_or ( error) ;
146- format ! ( "The search request was rejected: {detail}" )
151+ format ! (
152+ "The search request was rejected: {}" ,
153+ code_detail( error, code)
154+ )
147155 }
148156 _ => format ! ( "Web search failed: {error}" ) ,
149157 }
150158}
151159
160+ /// The module's message after its `tinysearch.<code>: ` prefix.
161+ fn code_detail < ' a > ( error : & ' a str , code : & str ) -> & ' a str {
162+ let marker = format ! ( "{}{code}: " , errors:: PREFIX ) ;
163+ error
164+ . split_once ( marker. as_str ( ) )
165+ . map ( |( _, detail) | detail)
166+ . unwrap_or ( error)
167+ }
168+
169+ /// What the model is told when the managed backend rejects the credential.
170+ pub ( crate ) fn backend_unauthorized_message ( api_key : bool ) -> String {
171+ if api_key {
172+ "Web search is unavailable: the TinyHumans backend rejected the configured API key. \
173+ Check the key, or add your own provider key under Connections → Search."
174+ . to_string ( )
175+ } else {
176+ "Web search is unavailable: the OpenHuman sign-in has expired. Sign in again to \
177+ continue searching."
178+ . to_string ( )
179+ }
180+ }
181+
182+ /// Recover from the managed backend rejecting the TinyHumans credential.
183+ ///
184+ /// A rejected session is dead for every backend call, not just search, so
185+ /// publish `SessionExpired`: the credentials subscriber clears the token and
186+ /// the UI asks the user to sign in, the same recovery a backend 401 on
187+ /// inference or integrations drives. Before this, a user on their own
188+ /// inference key never saw that prompt and search kept failing on the stale
189+ /// token. A rejected API key has no session to expire, so it never signs the
190+ /// user out (`API_KEY_REJECTED` semantics).
191+ fn on_backend_unauthorized ( tool : & str , api_key : bool ) {
192+ if api_key || crate :: cron:: scheduler_gate:: is_signed_out ( ) {
193+ return ;
194+ }
195+ crate :: core:: bus:: BUS . publish ( crate :: core:: events:: DomainEvent :: SessionExpired {
196+ source : format ! ( "search.{tool}" ) ,
197+ reason : "managed search backend rejected the session (401)" . to_string ( ) ,
198+ } ) ;
199+ }
200+
152201/// The classified error code in a module error message, if any.
153202pub fn error_code ( error : & str ) -> Option < & ' static str > {
154203 errors:: code_of ( error)
@@ -216,8 +265,10 @@ impl Tool for TinySearchTool {
216265 return Ok ( ToolResult :: failed ( SEARCH_EXHAUSTED_MESSAGE . to_string ( ) ) ) ;
217266 }
218267 let subject = super :: render:: subject ( & args) ;
268+ // `limit` is the module's alias for `max_results`.
219269 let max_results = args
220270 . get ( "max_results" )
271+ . or_else ( || args. get ( "limit" ) )
221272 . and_then ( Value :: as_u64)
222273 . map ( |n| n as usize )
223274 . unwrap_or ( config. search . max_results )
@@ -254,6 +305,16 @@ impl Tool for TinySearchTool {
254305 if exhausts_providers ( & error) {
255306 self . mark_exhausted_for ( signature) ;
256307 }
308+ if error_code ( & error) == Some ( errors:: BACKEND_UNAUTHORIZED ) {
309+ let api_key = crate :: security:: credentials:: api_key:: has_api_key ( & config) ;
310+ on_backend_unauthorized ( & self . spec . name , api_key) ;
311+ tracing:: warn!(
312+ tool = %self . spec. name,
313+ api_key,
314+ "[search][tool] managed backend rejected the TinyHumans credential"
315+ ) ;
316+ return Ok ( ToolResult :: error ( backend_unauthorized_message ( api_key) ) ) ;
317+ }
257318 tracing:: warn!(
258319 tool = %self . spec. name,
259320 code = error_code( & error) . unwrap_or( "unclassified" ) ,
0 commit comments