@@ -4,6 +4,7 @@ use crate::config::AgentRoleConfig;
44use crate :: config:: DEFAULT_AGENT_MAX_DEPTH ;
55use crate :: function_tool:: FunctionCallError ;
66use crate :: init_state_db;
7+ use crate :: session:: TurnInput ;
78use crate :: session:: tests:: make_session_and_context;
89use crate :: session_prefix:: format_subagent_notification_message;
910use crate :: thread_manager:: thread_store_from_config;
@@ -92,6 +93,30 @@ fn parse_agent_id(id: &str) -> ThreadId {
9293 ThreadId :: from_string ( id) . expect ( "agent id should be valid" )
9394}
9495
96+ #[ derive( Clone , Copy ) ]
97+ struct MailboxDeliveryTestTask ;
98+
99+ impl crate :: tasks:: SessionTask for MailboxDeliveryTestTask {
100+ fn kind ( & self ) -> crate :: state:: TaskKind {
101+ crate :: state:: TaskKind :: Regular
102+ }
103+
104+ fn span_name ( & self ) -> & ' static str {
105+ "session_task.mailbox_delivery_test"
106+ }
107+
108+ async fn run (
109+ self : Arc < Self > ,
110+ _session : Arc < crate :: tasks:: SessionTaskContext > ,
111+ _ctx : Arc < TurnContext > ,
112+ _input : Vec < TurnInput > ,
113+ cancellation_token : CancellationToken ,
114+ ) -> Option < String > {
115+ cancellation_token. cancelled ( ) . await ;
116+ None
117+ }
118+ }
119+
95120fn thread_manager ( ) -> ThreadManager {
96121 ThreadManager :: with_models_provider_for_tests (
97122 CodexAuth :: from_api_key ( "dummy" ) ,
@@ -294,6 +319,132 @@ async fn spawn_agent_uses_explorer_role_and_preserves_approval_policy() {
294319 assert_eq ! ( snapshot. model_provider_id, "ollama" ) ;
295320}
296321
322+ #[ tokio:: test]
323+ async fn spawn_agent_reopens_mailbox_delivery_for_current_turn ( ) {
324+ let ( mut session, mut turn) = make_session_and_context ( ) . await ;
325+ let manager = thread_manager ( ) ;
326+ let root = manager
327+ . start_thread ( ( * turn. config ) . clone ( ) )
328+ . await
329+ . expect ( "root thread should start" ) ;
330+ session. services . agent_control = manager. agent_control ( ) ;
331+ session. conversation_id = root. thread_id ;
332+ turn. session_source = SessionSource :: SubAgent ( SubAgentSource :: ThreadSpawn {
333+ parent_thread_id : session. conversation_id ,
334+ depth : 0 ,
335+ agent_path : Some ( AgentPath :: root ( ) ) ,
336+ agent_nickname : None ,
337+ agent_role : None ,
338+ } ) ;
339+ let communication = InterAgentCommunication :: new (
340+ AgentPath :: try_from ( "/root/worker" ) . expect ( "worker path should parse" ) ,
341+ AgentPath :: root ( ) ,
342+ Vec :: new ( ) ,
343+ "queued child update" . to_string ( ) ,
344+ /*trigger_turn*/ false ,
345+ ) ;
346+ let session = Arc :: new ( session) ;
347+ let turn = Arc :: new ( turn) ;
348+ session
349+ . spawn_task ( Arc :: clone ( & turn) , Vec :: new ( ) , MailboxDeliveryTestTask )
350+ . await ;
351+ session
352+ . input_queue
353+ . defer_mailbox_delivery_to_next_turn ( & session. active_turn , & turn. sub_id )
354+ . await ;
355+ session
356+ . input_queue
357+ . enqueue_mailbox_communication ( communication. clone ( ) )
358+ . await ;
359+
360+ SpawnAgentHandler :: default ( )
361+ . handle ( invocation (
362+ session. clone ( ) ,
363+ turn. clone ( ) ,
364+ "spawn_agent" ,
365+ function_payload ( json ! ( {
366+ "message" : "inspect this repo" ,
367+ "agent_type" : "explorer"
368+ } ) ) ,
369+ ) )
370+ . await
371+ . expect ( "spawn_agent should succeed" ) ;
372+
373+ assert_eq ! (
374+ session
375+ . input_queue
376+ . get_pending_input( & session. active_turn)
377+ . await ,
378+ vec![ TurnInput :: ResponseInputItem (
379+ communication. to_response_input_item( )
380+ ) ] ,
381+ ) ;
382+ session. abort_all_tasks ( TurnAbortReason :: Replaced ) . await ;
383+ }
384+
385+ #[ tokio:: test]
386+ async fn multi_agent_v2_spawn_agent_reopens_mailbox_delivery_for_current_turn ( ) {
387+ let ( mut session, mut turn) = make_session_and_context ( ) . await ;
388+ let manager = thread_manager ( ) ;
389+ let root = manager
390+ . start_thread ( ( * turn. config ) . clone ( ) )
391+ . await
392+ . expect ( "root thread should start" ) ;
393+ session. services . agent_control = manager. agent_control ( ) ;
394+ session. conversation_id = root. thread_id ;
395+ turn. session_source = SessionSource :: SubAgent ( SubAgentSource :: ThreadSpawn {
396+ parent_thread_id : session. conversation_id ,
397+ depth : 0 ,
398+ agent_path : Some ( AgentPath :: root ( ) ) ,
399+ agent_nickname : None ,
400+ agent_role : None ,
401+ } ) ;
402+ let communication = InterAgentCommunication :: new (
403+ AgentPath :: try_from ( "/root/worker" ) . expect ( "worker path should parse" ) ,
404+ AgentPath :: root ( ) ,
405+ Vec :: new ( ) ,
406+ "queued child update" . to_string ( ) ,
407+ /*trigger_turn*/ false ,
408+ ) ;
409+ let session = Arc :: new ( session) ;
410+ let turn = Arc :: new ( turn) ;
411+ session
412+ . spawn_task ( Arc :: clone ( & turn) , Vec :: new ( ) , MailboxDeliveryTestTask )
413+ . await ;
414+ session
415+ . input_queue
416+ . defer_mailbox_delivery_to_next_turn ( & session. active_turn , & turn. sub_id )
417+ . await ;
418+ session
419+ . input_queue
420+ . enqueue_mailbox_communication ( communication. clone ( ) )
421+ . await ;
422+
423+ SpawnAgentHandlerV2 :: default ( )
424+ . handle ( invocation (
425+ session. clone ( ) ,
426+ turn. clone ( ) ,
427+ "spawn_agent" ,
428+ function_payload ( json ! ( {
429+ "message" : "inspect this repo" ,
430+ "task_name" : "worker"
431+ } ) ) ,
432+ ) )
433+ . await
434+ . expect ( "spawn_agent should succeed" ) ;
435+
436+ assert_eq ! (
437+ session
438+ . input_queue
439+ . get_pending_input( & session. active_turn)
440+ . await ,
441+ vec![ TurnInput :: ResponseInputItem (
442+ communication. to_response_input_item( )
443+ ) ] ,
444+ ) ;
445+ session. abort_all_tasks ( TurnAbortReason :: Replaced ) . await ;
446+ }
447+
297448#[ tokio:: test]
298449async fn spawn_agent_fork_context_rejects_agent_type_override ( ) {
299450 let ( mut session, mut turn) = make_session_and_context ( ) . await ;
@@ -409,8 +560,9 @@ async fn multi_agent_v2_spawn_fork_turns_all_rejects_agent_type_override() {
409560}
410561
411562#[ tokio:: test]
412- async fn multi_agent_v2_spawn_defaults_to_full_fork_and_rejects_child_model_overrides ( ) {
563+ async fn multi_agent_v2_spawn_defaults_to_new_thread_for_agent_type_override ( ) {
413564 let ( mut session, mut turn) = make_session_and_context ( ) . await ;
565+ let role_name = install_role_with_model_override ( & mut turn) . await ;
414566 let manager = thread_manager ( ) ;
415567 let root = manager
416568 . start_thread ( ( * turn. config ) . clone ( ) )
@@ -425,28 +577,45 @@ async fn multi_agent_v2_spawn_defaults_to_full_fork_and_rejects_child_model_over
425577 . expect ( "test config should allow feature update" ) ;
426578 turn. config = Arc :: new ( config) ;
427579
428- let err = SpawnAgentHandlerV2 :: default ( )
580+ let session = Arc :: new ( session) ;
581+ let turn = Arc :: new ( turn) ;
582+ let output = SpawnAgentHandlerV2 :: default ( )
429583 . handle ( invocation (
430- Arc :: new ( session) ,
431- Arc :: new ( turn) ,
584+ session. clone ( ) ,
585+ turn. clone ( ) ,
432586 "spawn_agent" ,
433587 function_payload ( json ! ( {
434588 "message" : "inspect this repo" ,
435- "task_name" : "fork_context_v2" ,
436- "model" : "gpt-5-child-override" ,
437- "reasoning_effort" : "low"
589+ "task_name" : "role_default" ,
590+ "agent_type" : role_name
438591 } ) ) ,
439592 ) )
440593 . await
441- . err ( )
442- . expect ( "default full fork should reject child model overrides" ) ;
443-
444- assert_eq ! (
445- err,
446- FunctionCallError :: RespondToModel (
447- "Full-history forked agents inherit the parent agent type, model, and reasoning effort; omit agent_type, model, and reasoning_effort, or spawn without a full-history fork." . to_string( ) ,
594+ . expect ( "implicit new-thread spawn should allow agent_type overrides" ) ;
595+ let ( content, _) = expect_text_output ( output) ;
596+ let result: serde_json:: Value =
597+ serde_json:: from_str ( & content) . expect ( "spawn_agent result should be json" ) ;
598+ assert_eq ! ( result[ "task_name" ] , "/root/role_default" ) ;
599+ let agent_id = session
600+ . services
601+ . agent_control
602+ . resolve_agent_reference (
603+ session. conversation_id ,
604+ & turn. session_source ,
605+ "role_default" ,
448606 )
449- ) ;
607+ . await
608+ . expect ( "spawned task name should resolve" ) ;
609+ let snapshot = manager
610+ . get_thread ( agent_id)
611+ . await
612+ . expect ( "spawned agent thread should exist" )
613+ . config_snapshot ( )
614+ . await ;
615+
616+ assert_eq ! ( snapshot. model, "gpt-5-role-override" ) ;
617+ assert_eq ! ( snapshot. model_provider_id, "ollama" ) ;
618+ assert_eq ! ( snapshot. reasoning_effort, Some ( ReasoningEffort :: Minimal ) ) ;
450619}
451620
452621#[ tokio:: test]
0 commit comments