1use bamboo_domain::Session;
4use chrono::Utc;
5use serde_json::json;
6
7use super::helpers::{
8 compute_status_guidance, format_child_assignment, map_child_entry, metadata_text,
9 normalize_non_empty_optional, normalize_required_text, render_forked_parent_context,
10 replace_or_append_last_user_message, truncate_after_index, truncate_after_last_user,
11};
12use super::DELEGATION_NOTE;
13use super::{
14 ChildSessionEntry, ChildSessionError, ChildSessionPort, CreateChildInput, CreateChildResult,
15};
16
17pub async fn create_child_action(
18 port: &dyn ChildSessionPort,
19 input: CreateChildInput,
20) -> Result<CreateChildResult, ChildSessionError> {
21 use crate::runner::refresh_prompt_snapshot;
22 use bamboo_agent_core::Message;
23
24 let inherited_project_id =
25 match crate::project_context::ProjectContextResolver::session_project_identity(
26 &input.parent_session,
27 ) {
28 crate::project_context::SessionProjectIdentity::Assigned(project_id) => {
29 Some(project_id)
30 }
31 crate::project_context::SessionProjectIdentity::Unassigned => None,
32 crate::project_context::SessionProjectIdentity::Invalid { raw, message } => {
33 return Err(ChildSessionError::InvalidArguments(format!(
34 "parent session carries an invalid Project identity '{raw}': {message}"
35 )));
36 }
37 };
38 let final_workspace = port
39 .validate_child_workspace(inherited_project_id.as_ref(), &input.workspace)
40 .await?;
41
42 let mut child = Session::new_child_of(
47 input.child_id.clone(),
48 &input.parent_session,
49 input
50 .model_ref_override
51 .as_ref()
52 .map(|model_ref| model_ref.model.clone())
53 .or_else(|| input.model_override.clone())
54 .unwrap_or_else(|| input.parent_session.model.clone()),
55 input.title.clone(),
56 );
57 if let Some(project_id) = inherited_project_id {
60 child.set_project_id_meta(project_id.to_string());
61 }
62
63 if let Some(model_ref) = input.model_ref_override.clone() {
64 child.model_ref = Some(model_ref.clone());
65 child
66 .metadata
67 .insert("provider_name".to_string(), model_ref.provider);
68 } else if let Some(parent_model_ref) = input.parent_session.model_ref.clone() {
69 child.model_ref = Some(parent_model_ref.clone());
70 child.set_provider_name(parent_model_ref.provider);
71 } else if let Some(parent_provider) = input.parent_session.provider_name() {
72 child.set_provider_name(parent_provider);
73 }
74
75 if let Some(effort) = input.reasoning_effort {
79 child.reasoning_effort = Some(effort);
80 }
81
82 if input
87 .parent_session
88 .agent_runtime_state
89 .as_ref()
90 .is_some_and(|state| state.bypass_permissions)
91 {
92 child
93 .agent_runtime_state
94 .get_or_insert_with(bamboo_domain::AgentRuntimeState::default)
95 .bypass_permissions = true;
96 }
97
98 if input
103 .parent_session
104 .agent_runtime_state
105 .as_ref()
106 .is_some_and(|state| state.no_human_approver)
107 {
108 child
109 .agent_runtime_state
110 .get_or_insert_with(bamboo_domain::AgentRuntimeState::default)
111 .no_human_approver = true;
112 }
113
114 let stored_workspace = bamboo_agent_core::workspace_state::set_workspace(
119 &child.id,
120 std::path::PathBuf::from(final_workspace),
121 );
122 child.workspace = Some(stored_workspace.to_string_lossy().to_string());
123
124 child
125 .metadata
126 .insert("spawned_by".to_string(), "SubAgent".to_string());
127 child.set_subagent_type(input.subagent_type.clone());
128 child
129 .metadata
130 .insert("responsibility".to_string(), input.responsibility.clone());
131 child.metadata.insert(
132 "assignment_prompt".to_string(),
133 input.assignment_prompt.clone(),
134 );
135 if input.lifecycle.as_deref() == Some("resident") {
140 child
141 .metadata
142 .insert("lifecycle".to_string(), "resident".to_string());
143 if let Some(name) = input.resident_name.clone().filter(|n| !n.trim().is_empty()) {
144 child.metadata.insert("resident_name".to_string(), name);
145 }
146 child.metadata.insert(
147 "resident_context".to_string(),
148 input
149 .resident_context
150 .clone()
151 .filter(|c| matches!(c.as_str(), "reset" | "accumulate"))
152 .unwrap_or_else(|| "reset".to_string()),
153 );
154 }
155 child.set_last_run_status("pending");
156 child.clear_last_run_error();
157
158 for (key, value) in input.runtime_metadata {
160 child.metadata.insert(key, value);
161 }
162
163 let base_prompt = {
170 let global = crate::prompt_defaults::read_global_default_system_prompt_template();
171 if global.trim().is_empty() {
172 crate::context::DEFAULT_BASE_PROMPT.to_string()
173 } else {
174 global
175 }
176 };
177 let system_prompt = format!("{base_prompt}\n\n{DELEGATION_NOTE}");
178
179 child
180 .metadata
181 .insert("base_system_prompt".to_string(), system_prompt.clone());
182
183 child.add_message(Message::system(&system_prompt));
184
185 if let Some(parent_budget) = input.parent_session.effective_token_budget() {
189 let mut child_budget = parent_budget.clone();
190 child_budget.compression_trigger_percent = 70;
191 child_budget.compression_target_percent = 35;
192 child.token_budget = Some(child_budget);
193 }
194
195 refresh_prompt_snapshot(&mut child);
196 let assignment = format_child_assignment(
197 &input.title,
198 &input.responsibility,
199 &input.subagent_type,
200 &input.assignment_prompt,
201 );
202 let assignment = match input
206 .context_fork
207 .and_then(|n| render_forked_parent_context(&input.parent_session, n))
208 {
209 Some(forked) => format!("{forked}\n\n{assignment}"),
210 None => assignment,
211 };
212 child.add_message(Message::user(assignment));
213
214 if let Some(parent_task_list) = input.parent_session.task_list.clone() {
215 child.set_task_list(parent_task_list);
216 }
217
218 if let Some(ref disabled) = input.disabled_tools {
222 if !disabled.is_empty() {
223 child.metadata.insert(
224 "disabled_tools".to_string(),
225 serde_json::to_string(disabled).unwrap_or_default(),
226 );
227 }
228 }
229
230 let model = child.model.clone();
231 port.save_child_session(&mut child).await?;
232 if input.auto_run {
233 port.enqueue_child_run(&input.parent_session, &child)
234 .await?;
235 }
236
237 Ok(CreateChildResult {
238 child_session_id: child.id,
239 model,
240 })
241}
242
243pub async fn list_children_action(
244 port: &dyn ChildSessionPort,
245 parent_id: &str,
246) -> serde_json::Value {
247 let children = port.list_children(parent_id).await;
248 json!({
249 "parent_session_id": parent_id,
250 "children": children.iter().map(map_child_entry).collect::<Vec<_>>(),
251 "count": children.len(),
252 })
253}
254
255#[derive(Debug, Clone, PartialEq, serde::Serialize)]
258pub struct SessionTreeNode {
259 pub session_id: String,
260 pub title: String,
261 #[serde(skip_serializing_if = "Option::is_none")]
262 pub last_run_status: Option<String>,
263 pub depth: u32,
264 pub children: Vec<SessionTreeNode>,
265}
266
267pub fn assemble_session_tree(
272 root_id: &str,
273 root_title: &str,
274 adjacency: &std::collections::HashMap<String, Vec<ChildSessionEntry>>,
275 max_depth: u32,
276) -> SessionTreeNode {
277 fn build(
278 id: &str,
279 title: &str,
280 status: Option<String>,
281 depth: u32,
282 max_depth: u32,
283 adjacency: &std::collections::HashMap<String, Vec<ChildSessionEntry>>,
284 visited: &mut std::collections::HashSet<String>,
285 ) -> SessionTreeNode {
286 let first_visit = visited.insert(id.to_string());
287 let mut children = Vec::new();
288 if first_visit && depth < max_depth {
289 if let Some(kids) = adjacency.get(id) {
290 for kid in kids {
291 children.push(build(
292 &kid.child_session_id,
293 &kid.title,
294 kid.last_run_status.clone(),
295 depth + 1,
296 max_depth,
297 adjacency,
298 visited,
299 ));
300 }
301 }
302 }
303 SessionTreeNode {
304 session_id: id.to_string(),
305 title: title.to_string(),
306 last_run_status: status,
307 depth,
308 children,
309 }
310 }
311 let mut visited = std::collections::HashSet::new();
312 build(
313 root_id,
314 root_title,
315 None,
316 0,
317 max_depth,
318 adjacency,
319 &mut visited,
320 )
321}
322
323pub async fn build_session_tree_action(
329 port: &dyn ChildSessionPort,
330 root_id: &str,
331 max_depth: u32,
332) -> SessionTreeNode {
333 use std::collections::{HashMap, HashSet, VecDeque};
334 const NODE_CAP: usize = 5000;
335
336 let root_title = port
337 .load_root_session(root_id)
338 .await
339 .map(|s| s.title)
340 .unwrap_or_default();
341
342 let mut adjacency: HashMap<String, Vec<ChildSessionEntry>> = HashMap::new();
343 let mut visited: HashSet<String> = HashSet::new();
344 let mut queue: VecDeque<(String, u32)> = VecDeque::new();
345 queue.push_back((root_id.to_string(), 0));
346
347 while let Some((id, depth)) = queue.pop_front() {
348 if depth >= max_depth || adjacency.len() >= NODE_CAP || !visited.insert(id.clone()) {
349 continue;
350 }
351 let kids = port.list_children(&id).await;
352 for kid in &kids {
353 queue.push_back((kid.child_session_id.clone(), depth + 1));
354 }
355 adjacency.insert(id, kids);
356 }
357
358 assemble_session_tree(root_id, &root_title, &adjacency, max_depth)
359}
360
361pub async fn get_child_action(
362 port: &dyn ChildSessionPort,
363 parent_id: &str,
364 child_session_id: String,
365) -> Result<serde_json::Value, ChildSessionError> {
366 let child = port
367 .load_child_for_parent(parent_id, &child_session_id)
368 .await?;
369
370 let status = metadata_text(&child, "last_run_status");
371 let runner_info = port.get_child_runner_info(&child.id).await;
372
373 Ok(json!({
374 "child_session_id": child.id,
375 "title": child.title,
376 "model": child.model,
377 "pinned": child.pinned,
378 "message_count": child.messages.len(),
379 "is_running": port.is_child_running(&child.id).await,
380 "last_run_status": status,
381 "last_run_error": metadata_text(&child, "last_run_error"),
382 "responsibility": metadata_text(&child, "responsibility"),
383 "subagent_type": metadata_text(&child, "subagent_type"),
384 "prompt": metadata_text(&child, "assignment_prompt"),
385 "latest_user_message": child
386 .messages
387 .iter()
388 .rposition(|message| matches!(message.role, bamboo_agent_core::Role::User))
389 .and_then(|idx| child.messages.get(idx))
390 .map(|message| message.content.clone()),
391 "runtime_kind": metadata_text(&child, "runtime.kind"),
392 "external_protocol": metadata_text(&child, "external.protocol"),
393 "external_agent_id": metadata_text(&child, "external.agent_id"),
394 "a2a_context_id": metadata_text(&child, "a2a.context_id"),
395 "a2a_latest_task_id": metadata_text(&child, "a2a.latest_task_id"),
396 "a2a_last_state": metadata_text(&child, "a2a.last_state"),
397 "runner_started_at": runner_info.as_ref().and_then(|r| r.started_at.map(|t| t.to_rfc3339())),
398 "runner_completed_at": runner_info.as_ref().and_then(|r| r.completed_at.map(|t| t.to_rfc3339())),
399 "last_tool_name": runner_info.as_ref().and_then(|r| r.last_tool_name.clone()),
400 "last_tool_phase": runner_info.as_ref().and_then(|r| r.last_tool_phase.clone()),
401 "last_event_at": runner_info.as_ref().and_then(|r| r.last_event_at.map(|t| t.to_rfc3339())),
402 "round_count": runner_info.as_ref().map(|r| r.round_count).unwrap_or(0),
403 "has_pending_injected_messages": child.has_pending_injected_messages(),
404 "guidance": compute_status_guidance(status.as_deref(), runner_info.as_ref(), child.has_pending_injected_messages()),
405 }))
406}
407
408#[allow(clippy::too_many_arguments)]
409pub async fn update_child_action(
410 port: &dyn ChildSessionPort,
411 parent_id: &str,
412 child_session_id: String,
413 title: Option<String>,
414 responsibility: Option<String>,
415 prompt: Option<String>,
416 subagent_type: Option<String>,
417 reset_after_update: Option<bool>,
418 reasoning_effort: Option<bamboo_domain::ReasoningEffort>,
419) -> Result<serde_json::Value, ChildSessionError> {
420 let mut child = port
421 .load_child_for_parent(parent_id, &child_session_id)
422 .await?;
423
424 let title = normalize_non_empty_optional(title, "title")?;
425 let responsibility = normalize_non_empty_optional(responsibility, "responsibility")?;
426 let prompt = normalize_non_empty_optional(prompt, "prompt")?;
427 let subagent_type = normalize_non_empty_optional(subagent_type, "subagent_type")?;
428
429 let should_refresh_assignment =
430 responsibility.is_some() || prompt.is_some() || subagent_type.is_some();
431
432 if title.is_none() && !should_refresh_assignment && reasoning_effort.is_none() {
433 return Err(ChildSessionError::InvalidArguments(
434 "update requires at least one field: title/responsibility/prompt/subagent_type/reasoning_effort"
435 .to_string(),
436 ));
437 }
438
439 if let Some(effort) = reasoning_effort {
440 child.reasoning_effort = Some(effort);
441 }
442
443 if let Some(title) = title {
444 child.title = title;
445 }
446
447 let mut messages_removed = 0usize;
448
449 if should_refresh_assignment {
450 let effective_responsibility = normalize_required_text(
451 responsibility.or_else(|| metadata_text(&child, "responsibility")),
452 "responsibility",
453 )?;
454 let effective_subagent_type = normalize_required_text(
455 subagent_type.or_else(|| metadata_text(&child, "subagent_type")),
456 "subagent_type",
457 )?;
458 let effective_prompt = normalize_required_text(
459 prompt.or_else(|| metadata_text(&child, "assignment_prompt")),
460 "prompt",
461 )?;
462
463 child.metadata.insert(
464 "responsibility".to_string(),
465 effective_responsibility.clone(),
466 );
467 child
468 .metadata
469 .insert("subagent_type".to_string(), effective_subagent_type.clone());
470 child
471 .metadata
472 .insert("assignment_prompt".to_string(), effective_prompt.clone());
473 child.set_last_run_status("pending");
474 child.clear_last_run_error();
475
476 let assignment = format_child_assignment(
477 &child.title,
478 &effective_responsibility,
479 &effective_subagent_type,
480 &effective_prompt,
481 );
482 let user_index = replace_or_append_last_user_message(&mut child, assignment);
483
484 if reset_after_update.unwrap_or(true) {
485 messages_removed = truncate_after_index(&mut child, user_index);
486 }
487 }
488
489 child.updated_at = Utc::now();
490 port.save_child_session(&mut child).await?;
491
492 Ok(json!({
493 "child_session_id": child.id,
494 "title": child.title,
495 "messages_removed": messages_removed,
496 "last_run_status": metadata_text(&child, "last_run_status"),
497 "note": "Child session updated in place. Use action=run to execute the same child session.",
498 }))
499}
500
501pub async fn run_child_action(
502 port: &dyn ChildSessionPort,
503 parent: &Session,
504 child_session_id: String,
505 reset_to_last_user: Option<bool>,
506) -> Result<serde_json::Value, ChildSessionError> {
507 let mut child = port
508 .load_child_for_parent(&parent.id, &child_session_id)
509 .await?;
510
511 if port.is_child_running(&child.id).await {
512 return Ok(json!({
513 "child_session_id": child.id,
514 "status": "already_running",
515 "note": "Child session is already running.",
516 }));
517 }
518
519 let mut messages_removed = 0usize;
520 if reset_to_last_user.unwrap_or(true) {
521 messages_removed = truncate_after_last_user(&mut child)?;
522 }
523
524 child.set_last_run_status("pending");
525 child.clear_last_run_error();
526 child.updated_at = Utc::now();
527 port.save_child_session(&mut child).await?;
528
529 port.enqueue_child_run(parent, &child).await?;
530
531 Ok(json!({
532 "child_session_id": child.id,
533 "status": "queued",
534 "messages_removed": messages_removed,
535 "note": "Queued existing child session for retry in place.",
536 }))
537}
538
539pub async fn send_message_to_child_action(
540 port: &dyn ChildSessionPort,
541 parent: &Session,
542 child_session_id: String,
543 message: String,
544 auto_run: Option<bool>,
545 interrupt_running: Option<bool>,
546 idempotency_key: Option<&str>,
547) -> Result<serde_json::Value, ChildSessionError> {
548 let mut child = port
549 .load_child_for_parent(&parent.id, &child_session_id)
550 .await?;
551
552 let mut is_running = port.is_child_running(&child.id).await;
553 let should_interrupt = interrupt_running.unwrap_or(false);
554
555 if is_running && should_interrupt {
556 port.cancel_child_run_and_wait(&child.id).await?;
557 child = port
558 .load_child_for_parent(&parent.id, &child_session_id)
559 .await?;
560 is_running = port.is_child_running(&child.id).await;
564 }
565
566 let message = normalize_required_text(Some(message), "message")?;
567
568 let should_auto_run = auto_run.unwrap_or(true);
569 if should_route_child_message_through_inbox(is_running, should_auto_run) {
573 let delivery = port
574 .send_session_message(&parent.id, &child.id, &message, idempotency_key)
575 .await?;
576 let (delivery, activation, status, note, activation_error) = match delivery {
577 super::ChildSessionMessageDelivery::Activated(receipt) => {
578 let (status, note) = match receipt.activation {
579 bamboo_domain::SessionActivationDisposition::ActiveNotified => (
580 "message_delivered_live",
581 "Message durably delivered and the owning active loop was notified.",
582 ),
583 bamboo_domain::SessionActivationDisposition::ActivationReserved => (
584 "queued",
585 "Message durably delivered and exactly one session activation was reserved.",
586 ),
587 bamboo_domain::SessionActivationDisposition::ActivationCoalesced => (
588 "message_queued",
589 "Message durably delivered; activation coalesced with existing session work.",
590 ),
591 };
592 (
593 receipt.delivery,
594 Some(receipt.activation),
595 status,
596 note,
597 None,
598 )
599 }
600 super::ChildSessionMessageDelivery::ActivationPending { delivery, error } => (
601 delivery,
602 None,
603 "activation_pending",
604 "Message is durable; activation is pending and will be retried from the inbox watermark.",
605 Some(error),
606 ),
607 };
608 return Ok(json!({
609 "child_session_id": child.id,
610 "status": status,
611 "auto_run": !matches!(
612 activation,
613 Some(bamboo_domain::SessionActivationDisposition::ActiveNotified)
614 ),
615 "message": message,
616 "message_id": delivery.id.to_string(),
617 "inbox_generation": delivery.generation,
618 "activation_error": activation_error,
619 "message_count": child.messages.len(),
620 "note": note,
621 }));
622 }
623
624 child.add_message(bamboo_agent_core::Message::user(message.clone()));
628 child.set_last_run_status("pending");
629 child.clear_last_run_error();
630 port.save_child_session(&mut child).await?;
631
632 Ok(json!({
633 "child_session_id": child.id,
634 "status": "pending",
635 "auto_run": false,
636 "message": message,
637 "message_count": child.messages.len(),
638 "note": "Follow-up message appended. Use action=run to execute the child session.",
639 }))
640}
641
642fn should_route_child_message_through_inbox(is_running: bool, should_auto_run: bool) -> bool {
643 is_running || should_auto_run
644}
645
646pub async fn cancel_child_action(
647 port: &dyn ChildSessionPort,
648 parent_id: &str,
649 child_session_id: String,
650) -> Result<serde_json::Value, ChildSessionError> {
651 let _ = port
653 .load_child_for_parent(parent_id, &child_session_id)
654 .await?;
655 port.cancel_child_run_and_wait(&child_session_id).await?;
656
657 let mut child = port
662 .load_child_for_parent(parent_id, &child_session_id)
663 .await?;
664 let latest_status = child.last_run_status().unwrap_or_default();
665 if matches!(latest_status.as_str(), "completed" | "error") {
666 return Ok(json!({
667 "child_session_id": child_session_id,
668 "status": latest_status,
669 "note": "Child reached a natural terminal state while the cancel was in flight; its real outcome was kept.",
670 }));
671 }
672 child.set_last_run_status("cancelled");
673 child.set_last_run_error("Cancelled by parent");
674 port.save_child_session(&mut child).await?;
675 Ok(json!({
676 "child_session_id": child_session_id,
677 "status": "cancelled",
678 }))
679}
680
681pub async fn delete_child_action(
682 port: &dyn ChildSessionPort,
683 parent_id: &str,
684 child_session_id: String,
685) -> Result<serde_json::Value, ChildSessionError> {
686 let child = port
688 .load_child_for_parent(parent_id, &child_session_id)
689 .await?;
690 let result = port.delete_child_session(parent_id, &child.id).await?;
691
692 if !result.deleted {
693 return Err(ChildSessionError::Execution(format!(
694 "child session was not deleted: {}",
695 child.id
696 )));
697 }
698
699 Ok(json!({
700 "child_session_id": child.id,
701 "deleted": true,
702 "cancelled_running_child": result.cancelled_running_child,
703 }))
704}
705
706#[cfg(test)]
707mod tree_tests {
708 use super::super::ChildSessionEntry;
709 use super::assemble_session_tree;
710 use std::collections::HashMap;
711
712 fn entry(id: &str, title: &str) -> ChildSessionEntry {
713 ChildSessionEntry {
714 child_session_id: id.to_string(),
715 title: title.to_string(),
716 pinned: false,
717 message_count: 0,
718 updated_at: String::new(),
719 last_run_status: Some("completed".to_string()),
720 last_run_error: None,
721 }
722 }
723
724 #[test]
725 fn assembles_multi_level_tree() {
726 let mut adj: HashMap<String, Vec<ChildSessionEntry>> = HashMap::new();
727 adj.insert(
728 "root".into(),
729 vec![entry("c1", "child 1"), entry("c2", "child 2")],
730 );
731 adj.insert("c1".into(), vec![entry("g1", "grandchild")]);
732
733 let tree = assemble_session_tree("root", "Root", &adj, 8);
734 assert_eq!(tree.session_id, "root");
735 assert_eq!(tree.depth, 0);
736 assert_eq!(tree.children.len(), 2);
737 let c1 = tree.children.iter().find(|n| n.session_id == "c1").unwrap();
738 assert_eq!(c1.depth, 1);
739 assert_eq!(c1.children.len(), 1);
740 assert_eq!(c1.children[0].session_id, "g1");
741 assert_eq!(c1.children[0].depth, 2);
742 let c2 = tree.children.iter().find(|n| n.session_id == "c2").unwrap();
743 assert!(c2.children.is_empty());
744 }
745
746 #[test]
747 fn depth_cap_stops_descent() {
748 let mut adj: HashMap<String, Vec<ChildSessionEntry>> = HashMap::new();
749 adj.insert("root".into(), vec![entry("c1", "c1")]);
750 adj.insert("c1".into(), vec![entry("g1", "g1")]);
751 let tree = assemble_session_tree("root", "Root", &adj, 1);
752 assert_eq!(tree.children.len(), 1);
753 assert!(
754 tree.children[0].children.is_empty(),
755 "depth cap stops expansion at depth 1"
756 );
757 }
758
759 #[test]
760 fn cycle_is_broken_by_first_visit_guard() {
761 let mut adj: HashMap<String, Vec<ChildSessionEntry>> = HashMap::new();
762 adj.insert("a".into(), vec![entry("b", "b")]);
763 adj.insert("b".into(), vec![entry("a", "a")]); let tree = assemble_session_tree("a", "A", &adj, 100);
765 assert_eq!(tree.children.len(), 1);
766 let b = &tree.children[0];
767 assert_eq!(b.session_id, "b");
768 assert_eq!(b.children.len(), 1);
769 let a2 = &b.children[0];
770 assert_eq!(a2.session_id, "a");
771 assert!(a2.children.is_empty(), "cycle must terminate as a leaf");
772 }
773}
774
775#[cfg(test)]
776mod send_message_tests {
777 use super::should_route_child_message_through_inbox;
778
779 #[test]
780 fn running_auto_run_interrupt_matrix_preserves_idle_draft_contract() {
781 for (is_running, auto_run, interrupt_running, expected_inbox) in [
782 (false, false, false, false),
783 (false, false, true, false),
784 (false, true, false, true),
785 (false, true, true, true),
786 (true, false, false, true),
787 (true, false, true, false),
788 (true, true, false, true),
789 (true, true, true, true),
790 ] {
791 let effective_running = is_running && !interrupt_running;
792 assert_eq!(
793 should_route_child_message_through_inbox(effective_running, auto_run),
794 expected_inbox,
795 "is_running={is_running} auto_run={auto_run} interrupt_running={interrupt_running}"
796 );
797 }
798 }
799}