@@ -1519,7 +1519,7 @@ fn spawn_main_chat_reasoning_turn(
15191519 Arc :: ptr_eq ( stored, & task_token_arc)
15201520 } ) ;
15211521
1522- let next_inbound = pending_inbound. get ( & task_chat_id) . and_then ( |r| {
1522+ let mut next_inbound = pending_inbound. get ( & task_chat_id) . and_then ( |r| {
15231523 let mut g = match r. lock ( ) {
15241524 Ok ( guard) => guard,
15251525 Err ( poisoned) => {
@@ -1536,10 +1536,30 @@ fn spawn_main_chat_reasoning_turn(
15361536 g. pop_front ( )
15371537 } ) ;
15381538
1539- if let Some ( mut next_inbound) = next_inbound {
1540- match ensure_run_id ( & mut next_inbound) {
1539+ while let Some ( mut inbound) = next_inbound {
1540+ let next_from_queue = || {
1541+ pending_inbound. get ( & task_chat_id) . and_then ( |r| {
1542+ let mut g = match r. lock ( ) {
1543+ Ok ( guard) => guard,
1544+ Err ( poisoned) => {
1545+ let _ = logger_tx. send ( BusMessage :: Log (
1546+ LogEvent :: warn (
1547+ "AgentLogic" ,
1548+ "pending_inbound mutex poisoned after reasoning turn; recovering queue." ,
1549+ )
1550+ . with_chat_id ( & task_chat_id) ,
1551+ ) ) ;
1552+ poisoned. into_inner ( )
1553+ }
1554+ } ;
1555+ g. pop_front ( )
1556+ } )
1557+ } ;
1558+
1559+ match ensure_run_id ( & mut inbound) {
15411560 Ok ( next_run_id) => {
1542- spawn_main_chat_reasoning_turn ( args_for_chain, next_inbound, next_run_id) ;
1561+ spawn_main_chat_reasoning_turn ( args_for_chain, inbound, next_run_id) ;
1562+ break ;
15431563 }
15441564 Err ( error) => {
15451565 let _ = logger_tx. send ( BusMessage :: Log (
@@ -1549,6 +1569,7 @@ fn spawn_main_chat_reasoning_turn(
15491569 )
15501570 . with_chat_id ( & task_chat_id) ,
15511571 ) ) ;
1572+ next_inbound = next_from_queue ( ) ;
15521573 }
15531574 }
15541575 }
@@ -1980,7 +2001,26 @@ impl ActorLogic<BusMessage> for AgentLogic {
19802001 return Ok ( None ) ;
19812002 }
19822003 BusMessage :: Inbound ( mut inbound) => {
1983- let run_id = ensure_run_id ( & mut inbound) . map_err ( ActorError :: from) ?;
2004+ let run_id = match ensure_run_id ( & mut inbound) {
2005+ Ok ( run_id) => run_id,
2006+ Err ( error) => {
2007+ let _ = self . logger_tx . send ( BusMessage :: Log (
2008+ LogEvent :: error (
2009+ & self . name ,
2010+ & format ! ( "Rejecting inbound message: {}" , error) ,
2011+ )
2012+ . with_chat_id ( & inbound. chat_id ) ,
2013+ ) ) ;
2014+ let notice = crate :: channels:: terminal:: build_channel_error_notice (
2015+ & inbound. channel ,
2016+ & inbound. chat_id ,
2017+ inbound. thread_id . as_deref ( ) ,
2018+ & error,
2019+ ) ;
2020+ let _ = self . outbound_tx . send ( BusMessage :: Outbound ( notice) ) . await ;
2021+ return Ok ( None ) ;
2022+ }
2023+ } ;
19842024 let chat_id = inbound. chat_id . clone ( ) ;
19852025 let session_key = inbound. clarification_session_key ( ) ;
19862026 if self
@@ -3973,6 +4013,7 @@ mod tests {
39734013 Router ,
39744014 } ;
39754015 use serde_json:: Value ;
4016+ use std:: collections:: VecDeque ;
39764017 use std:: path:: PathBuf ;
39774018 use std:: sync:: atomic:: { AtomicU64 , AtomicUsize , Ordering } ;
39784019 use std:: sync:: { Arc , Mutex } ;
@@ -4869,6 +4910,91 @@ mod tests {
48694910 ) ;
48704911 }
48714912
4913+ #[ tokio:: test]
4914+ async fn invalid_tauri_inbound_is_rejected_without_stopping_the_actor ( ) {
4915+ let provider = RespondingProvider {
4916+ tag : "done" . to_string ( ) ,
4917+ } ;
4918+ let ( mut agent, mut outbound_rx) = build_agent_with_provider ( Box :: new ( provider) ) ;
4919+ let mut invalid = test_inbound ( "invalid-run-id" , "hello" ) ;
4920+ invalid. channel = "tauri" . to_string ( ) ;
4921+
4922+ assert ! ( matches!(
4923+ agent. process( BusMessage :: Inbound ( invalid) ) . await ,
4924+ Ok ( None )
4925+ ) ) ;
4926+ assert ! ( matches!(
4927+ outbound_rx. recv( ) . await ,
4928+ Some ( BusMessage :: Outbound ( _) )
4929+ ) ) ;
4930+
4931+ let mut valid = test_inbound ( "valid-after-rejection" , "hello" ) ;
4932+ valid. channel = "tauri" . to_string ( ) ;
4933+ valid. metadata . insert (
4934+ METADATA_RUN_ID . to_string ( ) ,
4935+ serde_json:: json!( "valid-run-id" ) ,
4936+ ) ;
4937+ agent
4938+ . process ( BusMessage :: Inbound ( valid) )
4939+ . await
4940+ . expect ( "actor remains usable after rejecting malformed inbound" ) ;
4941+ assert ! ( matches!(
4942+ tokio:: time:: timeout( Duration :: from_secs( 2 ) , outbound_rx. recv( ) ) . await ,
4943+ Ok ( Some ( BusMessage :: RunLifecycle (
4944+ RunLifecycleEvent :: Started { .. }
4945+ ) ) )
4946+ ) ) ;
4947+ }
4948+
4949+ #[ tokio:: test]
4950+ async fn invalid_queued_inbound_does_not_strand_following_valid_message ( ) {
4951+ let ( unblock_tx, unblock_rx) = tokio:: sync:: oneshot:: channel ( ) ;
4952+ let calls = Arc :: new ( AtomicUsize :: new ( 0 ) ) ;
4953+ let provider = GateFirstChatProvider {
4954+ calls : calls. clone ( ) ,
4955+ first_unblock : Arc :: new ( tokio:: sync:: Mutex :: new ( Some ( unblock_rx) ) ) ,
4956+ } ;
4957+ let ( mut agent, _outbound_rx) = build_agent_with_provider ( Box :: new ( provider) ) ;
4958+ let chat_id = "skip-invalid-queued" ;
4959+ agent
4960+ . process ( BusMessage :: Inbound ( test_inbound ( chat_id, "first" ) ) )
4961+ . await
4962+ . expect ( "start first turn" ) ;
4963+ for _ in 0 ..200 {
4964+ if calls. load ( Ordering :: SeqCst ) == 1 {
4965+ break ;
4966+ }
4967+ tokio:: time:: sleep ( Duration :: from_millis ( 5 ) ) . await ;
4968+ }
4969+ assert_eq ! ( calls. load( Ordering :: SeqCst ) , 1 ) ;
4970+
4971+ let mut invalid = test_inbound ( chat_id, "invalid queued" ) ;
4972+ invalid. channel = "tauri" . to_string ( ) ;
4973+ let mut valid = test_inbound ( chat_id, "valid queued" ) ;
4974+ valid. channel = "tauri" . to_string ( ) ;
4975+ valid. metadata . insert (
4976+ METADATA_RUN_ID . to_string ( ) ,
4977+ serde_json:: json!( "queued-run-id" ) ,
4978+ ) ;
4979+ agent. pending_inbound . insert (
4980+ chat_id. to_string ( ) ,
4981+ Mutex :: new ( VecDeque :: from ( [ invalid, valid] ) ) ,
4982+ ) ;
4983+
4984+ unblock_tx. send ( ( ) ) . expect ( "unblock first turn" ) ;
4985+ for _ in 0 ..400 {
4986+ if calls. load ( Ordering :: SeqCst ) == 2 {
4987+ break ;
4988+ }
4989+ tokio:: time:: sleep ( Duration :: from_millis ( 5 ) ) . await ;
4990+ }
4991+ assert_eq ! (
4992+ calls. load( Ordering :: SeqCst ) ,
4993+ 2 ,
4994+ "the valid queued message must run after the invalid item is dropped"
4995+ ) ;
4996+ }
4997+
48724998 #[ tokio:: test]
48734999 async fn started_lifecycle_event_preserves_caller_run_id ( ) {
48745000 let provider = RespondingProvider {
0 commit comments