1use std::collections::{BTreeSet, HashMap};
7use std::future::Future;
8use std::pin::Pin;
9use std::sync::Arc;
10
11use tokio::sync::{broadcast, mpsc, RwLock};
12use tokio_util::sync::CancellationToken;
13use tracing::Instrument;
14
15use bamboo_agent_core::tools::ToolExecutor;
16use bamboo_agent_core::{AgentError, AgentEvent, Session};
17use bamboo_domain::ReasoningEffort;
18use bamboo_llm::LLMProvider;
19
20use crate::runtime::config::{
21 AuxiliaryModelConfig, BashCompletionSink, BashResumeHook, DisabledFilterResolver, GoldConfig,
22 GuardianConfig, GuardianSpawner, ImageFallbackConfig,
23};
24use crate::runtime::execution::child_completion::ChildCompletion;
25use crate::runtime::execution::event_forwarder::HistoryCommitBarrier;
26use crate::runtime::execution::runner_lifecycle::{
27 finalize_rejected_runner_if_distinct, finalize_runner_exact, reserve_runner_core,
28 reserve_runner_for_registration, ReserveOutcome, RunnerReservation,
29};
30use crate::runtime::execution::runner_state::AgentRunner;
31use crate::runtime::model_roster::ModelRoster;
32use crate::runtime::Agent;
33use crate::runtime::{ExecuteRequest, ExecuteRequestBuilder};
34use crate::session_activation::{
35 SessionActivationRouter, SessionRunRegistration, SessionRunRegistrationError,
36};
37
38pub use crate::session_cache::SessionCache;
39
40enum SessionExecutionActivationOwnership {
41 Unrouted,
43 UnpublishedActivation(Arc<SessionActivationRouter>),
46 RegistrationPending(Arc<SessionActivationRouter>),
50 Registered(Box<SessionRunRegistration>),
53}
54
55pub struct SessionExecutionReservation {
61 session_id: String,
62 run_id: String,
63 cancel_token: CancellationToken,
64 runners: Arc<RwLock<HashMap<String, AgentRunner>>>,
65 activation: SessionExecutionActivationOwnership,
66 root_actor: Option<Box<RootActorExecution>>,
67 child_startup: Option<Box<ChildHostStartup>>,
68 child_closure: Option<tokio::task::JoinHandle<()>>,
69 physical_admission: Option<super::root_execution_lifetime::RootRunAdmission>,
70 execution_persistence: Option<Arc<dyn bamboo_domain::RuntimeSessionPersistence>>,
71 persistence_bound: bool,
72 selected_input: bool,
73 armed: bool,
74}
75
76impl SessionExecutionReservation {
77 pub fn session_id(&self) -> &str {
78 &self.session_id
79 }
80
81 pub fn run_id(&self) -> &str {
82 &self.run_id
83 }
84
85 pub fn cancel_token(&self) -> &CancellationToken {
86 &self.cancel_token
87 }
88
89 pub async fn arm_child_host_startup(
93 &mut self,
94 agent: &Agent,
95 expected: &Session,
96 cache: SessionCache,
97 events: tokio::sync::broadcast::Sender<AgentEvent>,
98 ) -> std::io::Result<()> {
99 self.arm_child_host_startup_inner(agent, expected, cache, Some(events))
100 .await
101 }
102
103 async fn arm_child_host_startup_inner(
104 &mut self,
105 agent: &Agent,
106 expected: &Session,
107 cache: SessionCache,
108 events: Option<tokio::sync::broadcast::Sender<AgentEvent>>,
109 ) -> std::io::Result<()> {
110 if expected.kind != bamboo_domain::SessionKind::Child || expected.id != self.session_id {
111 return Err(std::io::Error::other("Child startup target mismatch"));
112 }
113 if self.child_startup.is_none() {
114 let publication = self
115 .runners
116 .read()
117 .await
118 .get(&self.session_id)
119 .filter(|runner| runner.run_id == self.run_id)
120 .map(|runner| runner.event_publication.clone())
121 .ok_or_else(|| std::io::Error::other("Child startup runner was superseded"))?;
122 let sent = std::sync::atomic::AtomicBool::new(false);
123 self.child_startup = Some(Box::new(ChildHostStartup {
124 storage: agent.storage().clone(),
125 expected: expected.clone(),
126 run_id: self.run_id.clone(),
127 publish: Arc::new(move |saved| {
128 publication.publish(|| {
129 cache.insert(
130 saved.id.clone(),
131 Arc::new(crate::SessionSnapshot::new(saved.clone())),
132 );
133 if !sent.swap(true, std::sync::atomic::Ordering::AcqRel) {
134 if let Some(events) = &events {
135 let _ = events.send(AgentEvent::Error {
136 message: saved.last_run_error().unwrap_or_else(|| {
137 "Child startup failed before execution".into()
138 }),
139 });
140 }
141 }
142 });
143 }),
144 }));
145 }
146 Ok(())
147 }
148
149 pub async fn fail_child_host_startup(&mut self, detail: &str) {
150 if let Some(startup) = self.child_startup.take() {
151 let detail = detail.to_string();
152 self.child_closure = Some(tokio::spawn(async move {
154 startup.close(&detail).await;
155 }));
156 }
157 if let Some(closure) = self.child_closure.as_mut() {
158 let _ = closure.await;
159 }
160 self.child_closure = None;
161 }
162
163 pub async fn bind_root_actor(
166 &mut self,
167 agent: &Agent,
168 session: &Session,
169 ) -> std::io::Result<()> {
170 self.bind_root_actor_inner(agent, session, false).await
171 }
172
173 async fn bind_root_actor_inner(
174 &mut self,
175 agent: &Agent,
176 session: &Session,
177 for_compaction: bool,
178 ) -> std::io::Result<()> {
179 if self.root_actor.is_none() {
180 if self.session_id != session.id {
181 return Err(std::io::Error::new(
182 std::io::ErrorKind::InvalidInput,
183 "Root binding target mismatch",
184 ));
185 }
186 let persistence = agent.persistence();
187 let binding = if for_compaction {
188 persistence
189 .bind_root_actor_compaction(session, &self.run_id)
190 .await?
191 } else {
192 persistence
193 .bind_root_actor_execution(session, &self.run_id)
194 .await?
195 };
196 if let Some(binding) = binding {
197 self.root_actor = Some(Box::new(RootActorExecution::start(binding)));
198 }
199 }
200 self.bind_execution_persistence(agent, session)
201 }
202
203 fn bind_execution_persistence(
204 &mut self,
205 agent: &Agent,
206 session: &Session,
207 ) -> std::io::Result<()> {
208 if self.session_id != session.id {
209 return Err(std::io::Error::other(
210 "execution persistence target mismatch",
211 ));
212 }
213 if self.persistence_bound {
214 return Ok(());
215 }
216 let persistence = self
217 .root_actor
218 .as_ref()
219 .map_or_else(|| agent.persistence().clone(), |owner| owner.persistence());
220 if let Some(inherited) = persistence
221 .inherited_child_wait()
222 .or_else(|| bamboo_domain::InheritedChildWait::capture(session))
223 {
224 inherited.validate_session(session)?;
225 self.execution_persistence = Some(persistence.bind_inherited_child_wait(inherited)?);
226 } else if self.root_actor.is_some() {
227 self.execution_persistence = Some(persistence);
228 }
229 self.persistence_bound = true;
230 Ok(())
231 }
232
233 pub fn execution_persistence(
234 &self,
235 ) -> Option<Arc<dyn bamboo_domain::RuntimeSessionPersistence>> {
236 self.execution_persistence.clone()
237 }
238
239 pub fn bind_root_inbox_receipt(
242 &mut self,
243 receipt: bamboo_domain::SessionInboxReceipt,
244 ) -> std::io::Result<()> {
245 if !self.selected_input || self.root_actor.is_none() || self.cancel_token.is_cancelled() {
246 return Err(std::io::Error::other(
247 "Root receipt requires a live bound reservation",
248 ));
249 }
250 let persistence = self
251 .execution_persistence
252 .as_ref()
253 .ok_or_else(|| std::io::Error::other("Root execution persistence unavailable"))?;
254 self.execution_persistence = Some(persistence.bind_root_inbox_receipt(receipt)?);
255 Ok(())
256 }
257
258 pub async fn retain_selected_run_output(&self) -> std::io::Result<bool> {
261 if !self.selected_input {
262 return Ok(false);
263 }
264 let owner = self
265 .root_actor_writer()
266 .ok_or_else(|| std::io::Error::other("Selected output requires its Root writer"))?;
267 let receipt = self
268 .execution_persistence
269 .as_ref()
270 .and_then(|p| p.root_inbox_receipt())
271 .cloned()
272 .ok_or_else(|| std::io::Error::other("Selected output requires its bound receipt"))?;
273 if owner.fence.actor_id != self.session_id
274 || owner.fence.run_id != self.run_id
275 || self.cancel_token.is_cancelled()
276 {
277 return Err(std::io::Error::other(
278 "Selected output reservation is not live",
279 ));
280 }
281 let runners = self.runners.read().await;
282 let runner = runners
283 .get(&self.session_id)
284 .filter(|r| r.run_id == self.run_id && !r.cancel_token.is_cancelled())
285 .ok_or_else(|| std::io::Error::other("Selected output runner is unavailable"))?;
286 runner
287 .visible_messages
288 .retain_selected_output(&owner, receipt)?;
289 Ok(true)
290 }
291
292 pub fn root_actor_writer(&self) -> Option<bamboo_domain::RootActorRuntimeWrite> {
293 self.root_actor
294 .as_ref()
295 .map(|owner| owner.binding.owner.clone())
296 }
297
298 pub fn queue_root_response_event(&mut self, event: AgentEvent) -> Option<AgentEvent> {
301 let Some(owner) = self.root_actor.as_mut() else {
302 return Some(event);
303 };
304 owner.response_events.push(event);
305 None
306 }
307
308 pub fn take_root_response_events(&mut self) -> Vec<AgentEvent> {
309 self.root_actor
310 .as_mut()
311 .map(|owner| std::mem::take(&mut owner.response_events))
312 .unwrap_or_default()
313 }
314
315 pub async fn finish_root_actor(&mut self, outcome: bamboo_domain::ActorActivationFinish) {
318 if let Some(owner) = self.root_actor.take() {
319 (*owner).finish(outcome).await;
320 }
321 }
322
323 pub(crate) fn from_activation_placeholder(
329 session_id: impl Into<String>,
330 reservation: RunnerReservation,
331 router: Arc<SessionActivationRouter>,
332 runners: Arc<RwLock<HashMap<String, AgentRunner>>>,
333 ) -> Self {
334 Self {
335 session_id: session_id.into(),
336 run_id: reservation.run_id,
337 cancel_token: reservation.cancel_token,
338 runners,
339 activation: SessionExecutionActivationOwnership::UnpublishedActivation(router),
340 root_actor: None,
341 physical_admission: reservation.physical_admission,
342 execution_persistence: None,
343 persistence_bound: false,
344 child_startup: None,
345 child_closure: None,
346 selected_input: false,
347 armed: true,
348 }
349 }
350
351 pub(crate) fn from_pending_registration(
355 session_id: impl Into<String>,
356 reservation: RunnerReservation,
357 router: Option<Arc<SessionActivationRouter>>,
358 runners: Arc<RwLock<HashMap<String, AgentRunner>>>,
359 ) -> Self {
360 let activation = match router {
361 Some(router) => SessionExecutionActivationOwnership::RegistrationPending(router),
362 None => SessionExecutionActivationOwnership::Unrouted,
363 };
364 Self {
365 session_id: session_id.into(),
366 run_id: reservation.run_id,
367 cancel_token: reservation.cancel_token,
368 runners,
369 activation,
370 root_actor: None,
371 physical_admission: reservation.physical_admission,
372 execution_persistence: None,
373 persistence_bound: false,
374 child_startup: None,
375 child_closure: None,
376 selected_input: false,
377 armed: true,
378 }
379 }
380
381 pub(crate) fn mark_activation_published(&mut self) {
384 let activation = std::mem::replace(
385 &mut self.activation,
386 SessionExecutionActivationOwnership::Unrouted,
387 );
388 self.activation = match activation {
389 SessionExecutionActivationOwnership::UnpublishedActivation(router) => {
390 SessionExecutionActivationOwnership::RegistrationPending(router)
391 }
392 other => other,
393 };
394 }
395
396 pub(crate) async fn rollback_unpublished_activation(mut self) {
401 self.armed = false;
402 self.cancel_token.cancel();
403 self.finish_root_actor(bamboo_domain::ActorActivationFinish::Cancelled)
404 .await;
405 let activation = std::mem::replace(
406 &mut self.activation,
407 SessionExecutionActivationOwnership::Unrouted,
408 );
409 debug_assert!(matches!(
410 activation,
411 SessionExecutionActivationOwnership::UnpublishedActivation(_)
412 ));
413 remove_runner_exact(&self.runners, &self.session_id, &self.run_id).await;
414 }
415
416 pub async fn ensure_registered(&mut self) -> Result<(), SessionRunRegistrationError> {
422 let router = match &self.activation {
423 SessionExecutionActivationOwnership::RegistrationPending(router) => router.clone(),
424 SessionExecutionActivationOwnership::UnpublishedActivation(_) => {
425 unreachable!("unpublished activation entered an execution adapter")
426 }
427 SessionExecutionActivationOwnership::Unrouted
428 | SessionExecutionActivationOwnership::Registered(_) => return Ok(()),
429 };
430 match register_reserved_activation(
431 router,
432 &self.runners,
433 &self.session_id,
434 &self.run_id,
435 self.selected_input,
436 )
437 .await
438 {
439 Ok(registration) => {
440 self.activation =
441 SessionExecutionActivationOwnership::Registered(Box::new(registration));
442 Ok(())
443 }
444 Err(error) => {
445 self.disarm_after_registration_rejection(error.existing_run_id());
450 Err(error)
451 }
452 }
453 }
454
455 pub async fn abandon(mut self) {
459 self.armed = false;
460 self.cancel_token.cancel();
461 if let Some(owner) = self.root_actor.take() {
462 (*owner)
463 .finish(bamboo_domain::ActorActivationFinish::Cancelled)
464 .await;
465 }
466 let activation = std::mem::replace(
467 &mut self.activation,
468 SessionExecutionActivationOwnership::Unrouted,
469 );
470 let runners = self.runners.clone();
471 let session_id = self.session_id.clone();
472 let run_id = self.run_id.clone();
473 let cleanup_session_id = session_id.clone();
474 let cleanup_run_id = run_id.clone();
475 let selected_input = self.selected_input;
476 let child_startup = self.child_startup.take();
477 let child_closure = self.child_closure.take();
478 let cleanup = tokio::spawn(async move {
479 if let Some(closure) = child_closure {
480 let _ = closure.await;
481 }
482 if let Some(startup) = child_startup {
483 startup
484 .close("startup reservation abandoned before execution")
485 .await;
486 }
487 cleanup_execution_reservation(
488 activation,
489 runners,
490 cleanup_session_id,
491 cleanup_run_id,
492 selected_input,
493 )
494 .await;
495 });
496 if let Err(error) = cleanup.await {
497 tracing::error!(
498 %session_id,
499 %run_id,
500 %error,
501 "detached execution-reservation cleanup failed"
502 );
503 }
504 }
505
506 fn disarm_after_registration_rejection(&mut self, existing_run_id: &str) {
507 if existing_run_id != self.run_id {
508 self.cancel_token.cancel();
509 }
510 self.armed = false;
511 if let Some(startup) = self.child_startup.take() {
512 if existing_run_id != self.run_id {
513 tokio::spawn(async move {
514 startup
515 .close("startup registration rejected before execution")
516 .await;
517 });
518 }
519 }
520 }
521
522 pub(crate) fn matches_execution_target(
523 &self,
524 session_id: &str,
525 domain_session_id: &str,
526 runners: &Arc<RwLock<HashMap<String, AgentRunner>>>,
527 ) -> bool {
528 self.session_id == session_id
529 && domain_session_id == session_id
530 && Arc::ptr_eq(&self.runners, runners)
531 }
532
533 pub(crate) fn disarm_for_execution(
534 &mut self,
535 ) -> (CancellationToken, Option<SessionRunRegistration>) {
536 self.armed = false;
537 self.child_startup = None;
538 let registration = match std::mem::replace(
539 &mut self.activation,
540 SessionExecutionActivationOwnership::Unrouted,
541 ) {
542 SessionExecutionActivationOwnership::Registered(registration) => Some(*registration),
543 SessionExecutionActivationOwnership::Unrouted => None,
544 SessionExecutionActivationOwnership::UnpublishedActivation(_) => {
545 unreachable!("unpublished activation cannot transfer to execution")
546 }
547 SessionExecutionActivationOwnership::RegistrationPending(_) => {
548 unreachable!("ensure_registered adopts every pending router registration")
549 }
550 };
551 (self.cancel_token.clone(), registration)
552 }
553}
554
555impl Drop for SessionExecutionReservation {
556 fn drop(&mut self) {
557 if !self.armed {
558 return;
559 }
560 self.armed = false;
561 self.cancel_token.cancel();
562 let activation = std::mem::replace(
563 &mut self.activation,
564 SessionExecutionActivationOwnership::Unrouted,
565 );
566 let runners = self.runners.clone();
567 let session_id = self.session_id.clone();
568 let run_id = self.run_id.clone();
569 let selected_input = self.selected_input;
570 let child_startup = self.child_startup.take();
571 let child_closure = self.child_closure.take();
572 if let Ok(runtime) = tokio::runtime::Handle::try_current() {
573 runtime.spawn(async move {
574 if let Some(closure) = child_closure {
575 let _ = closure.await;
576 }
577 if let Some(startup) = child_startup {
578 startup
579 .close("startup reservation dropped before execution")
580 .await;
581 }
582 cleanup_execution_reservation(
583 activation,
584 runners,
585 session_id,
586 run_id,
587 selected_input,
588 )
589 .await;
590 });
591 }
592 }
593}
594
595struct ChildHostStartup {
596 storage: Arc<dyn bamboo_domain::Storage>,
597 expected: Session,
598 run_id: String,
599 publish: bamboo_domain::storage::RootActorRuntimePublisher,
600}
601
602impl ChildHostStartup {
603 async fn close(self, detail: &str) {
604 if let Err(error) = self
605 .storage
606 .fail_child_host_startup(&self.expected, &self.run_id, detail, self.publish)
607 .await
608 {
609 tracing::warn!(session_id = %self.expected.id, run_id = %self.run_id, %error, "Child pre-execution closure unconfirmed");
610 }
611 }
612}
613
614pub struct SessionPreparationReservation {
617 reservation: Option<SessionExecutionReservation>,
618}
619
620impl SessionPreparationReservation {
621 pub async fn bind_root_actor(
622 &mut self,
623 agent: &Agent,
624 session: &Session,
625 ) -> std::io::Result<()> {
626 self.reservation
627 .as_mut()
628 .expect("live preparation")
629 .bind_root_actor(agent, session)
630 .await
631 }
632
633 pub async fn bind_root_actor_compaction(
634 &mut self,
635 agent: &Agent,
636 session: &Session,
637 ) -> std::io::Result<()> {
638 self.reservation
639 .as_mut()
640 .expect("live preparation")
641 .bind_root_actor_inner(agent, session, true)
642 .await
643 }
644
645 pub fn root_actor_writer(&self) -> Option<bamboo_domain::RootActorRuntimeWrite> {
646 self.reservation
647 .as_ref()
648 .and_then(SessionExecutionReservation::root_actor_writer)
649 }
650
651 pub async fn finish(mut self, outcome: bamboo_domain::ActorActivationFinish) {
652 let reservation = self.reservation.take().expect("live preparation");
653 let _ = Self::release(reservation, outcome).await;
654 }
655
656 pub async fn abandon(self) {
657 self.finish(bamboo_domain::ActorActivationFinish::Cancelled)
658 .await;
659 }
660
661 fn release(
662 mut reservation: SessionExecutionReservation,
663 outcome: bamboo_domain::ActorActivationFinish,
664 ) -> tokio::task::JoinHandle<()> {
665 tokio::spawn(async move {
668 reservation.finish_root_actor(outcome).await;
669 reservation.abandon().await;
670 })
671 }
672}
673
674impl Drop for SessionPreparationReservation {
675 fn drop(&mut self) {
676 if let Some(reservation) = self.reservation.take() {
677 if tokio::runtime::Handle::try_current().is_ok() {
678 drop(Self::release(
679 reservation,
680 bamboo_domain::ActorActivationFinish::Cancelled,
681 ));
682 }
683 }
684 }
685}
686
687pub async fn reserve_session_preparation(
690 agent: &Arc<Agent>,
691 runners: &Arc<RwLock<HashMap<String, AgentRunner>>>,
692 session_id: &str,
693) -> std::io::Result<SessionPreparationReservation> {
694 let router = agent.activation_router().cloned().ok_or_else(|| {
695 std::io::Error::new(
696 std::io::ErrorKind::Unsupported,
697 "Preparation requires its activation router",
698 )
699 })?;
700 let busy = || {
701 std::io::Error::new(
702 std::io::ErrorKind::WouldBlock,
703 "Session has an active runner or logical owner",
704 )
705 };
706 if runners.read().await.get(session_id).is_some_and(|runner| {
707 runner.root_reset_admission_reserved()
708 || matches!(
709 runner.status,
710 super::runner_state::AgentStatus::Pending
711 | super::runner_state::AgentStatus::Running
712 )
713 }) {
714 return Err(busy());
715 }
716 let reservation = reserve_logical_session(router, runners, session_id, true)
719 .await
720 .map_err(|_| busy())?;
721 let preparation = SessionPreparationReservation {
722 reservation: Some(reservation),
723 };
724 if runners.read().await.get(session_id).is_some_and(|runner| {
725 runner.root_reset_admission_reserved()
726 || matches!(
727 runner.status,
728 super::runner_state::AgentStatus::Pending
729 | super::runner_state::AgentStatus::Running
730 )
731 }) {
732 preparation.abandon().await;
733 return Err(busy());
734 }
735 Ok(preparation)
736}
737
738async fn reserve_logical_session(
739 router: Arc<SessionActivationRouter>,
740 runners: &Arc<RwLock<HashMap<String, AgentRunner>>>,
741 session_id: &str,
742 selected_input: bool,
743) -> Result<SessionExecutionReservation, SessionRunRegistrationError> {
744 let mut reservation = SessionExecutionReservation {
745 session_id: session_id.to_owned(),
746 run_id: uuid::Uuid::new_v4().to_string(),
747 cancel_token: CancellationToken::new(),
748 runners: runners.clone(),
749 activation: SessionExecutionActivationOwnership::Unrouted,
750 root_actor: None,
751 physical_admission: None,
752 execution_persistence: None,
753 persistence_bound: false,
754 child_startup: None,
755 child_closure: None,
756 selected_input: false,
757 armed: true,
758 };
759 let registration = register_reserved_activation(
760 router,
761 runners,
762 session_id,
763 &reservation.run_id,
764 selected_input,
765 )
766 .await?;
767 reservation.activation =
768 SessionExecutionActivationOwnership::Registered(Box::new(registration));
769 Ok(reservation)
770}
771
772pub enum SessionExecutionReserveOutcome {
774 Reserved(SessionExecutionReservation),
775 AlreadyRunning { run_id: String },
776}
777
778async fn register_reserved_activation(
779 router: Arc<SessionActivationRouter>,
780 runners: &Arc<RwLock<HashMap<String, AgentRunner>>>,
781 session_id: &str,
782 run_id: &str,
783 selected_input: bool,
784) -> Result<SessionRunRegistration, SessionRunRegistrationError> {
785 let mut registration = match router.register_run(session_id, run_id).await {
786 Ok(registration) => registration,
787 Err(error) => {
788 let collision = Err(AgentError::LLM(error.to_string()));
789 finalize_rejected_runner_if_distinct(
790 runners,
791 session_id,
792 error.existing_run_id(),
793 run_id,
794 &collision,
795 )
796 .await;
797 return Err(error);
798 }
799 };
800 if selected_input {
801 registration.restrict_to_selected_input();
802 }
803 let cleanup_runners = runners.clone();
804 let cleanup_session_id = session_id.to_string();
805 let cleanup_run_id = run_id.to_string();
806 registration.set_abort_cleanup(move || async move {
807 let abandoned = Err(AgentError::Cancelled);
808 finalize_runner_exact(
809 &cleanup_runners,
810 &cleanup_session_id,
811 &cleanup_run_id,
812 &abandoned,
813 )
814 .await;
815 });
816 Ok(registration)
817}
818
819async fn cleanup_execution_reservation(
820 activation: SessionExecutionActivationOwnership,
821 runners: Arc<RwLock<HashMap<String, AgentRunner>>>,
822 session_id: String,
823 run_id: String,
824 selected_input: bool,
825) {
826 match activation {
827 SessionExecutionActivationOwnership::Registered(registration) => {
828 (*registration).abandon().await;
829 }
830 SessionExecutionActivationOwnership::RegistrationPending(router) => {
831 match register_reserved_activation(
832 router,
833 &runners,
834 &session_id,
835 &run_id,
836 selected_input,
837 )
838 .await
839 {
840 Ok(registration) => registration.abandon().await,
841 Err(error) => {
842 tracing::debug!(
843 %session_id,
844 %run_id,
845 existing_run_id = %error.existing_run_id(),
846 "abandoned activation placeholder was already adopted or superseded"
847 );
848 }
849 }
850 }
851 SessionExecutionActivationOwnership::UnpublishedActivation(_) => {
852 remove_runner_exact(&runners, &session_id, &run_id).await;
853 }
854 SessionExecutionActivationOwnership::Unrouted => {
855 let abandoned = Err(AgentError::Cancelled);
856 finalize_runner_exact(&runners, &session_id, &run_id, &abandoned).await;
857 }
858 }
859}
860
861async fn remove_runner_exact(
862 runners: &Arc<RwLock<HashMap<String, AgentRunner>>>,
863 session_id: &str,
864 run_id: &str,
865) -> bool {
866 let mut runners = runners.write().await;
867 if runners
868 .get(session_id)
869 .is_some_and(|runner| runner.run_id == run_id)
870 {
871 super::runner_lifecycle::remove_runner_entry(&mut runners, session_id).await;
872 true
873 } else {
874 false
875 }
876}
877
878pub async fn reserve_session_execution(
886 agent: &Arc<Agent>,
887 runners: &Arc<RwLock<HashMap<String, AgentRunner>>>,
888 senders: &Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>>,
889 session_id: &str,
890 event_sender: &broadcast::Sender<AgentEvent>,
891) -> SessionExecutionReserveOutcome {
892 reserve_session_execution_inner(agent, runners, senders, session_id, event_sender, false).await
893}
894
895pub async fn reserve_selected_input_execution(
898 agent: &Arc<Agent>,
899 runners: &Arc<RwLock<HashMap<String, AgentRunner>>>,
900 senders: &Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>>,
901 session_id: &str,
902 event_sender: &broadcast::Sender<AgentEvent>,
903) -> SessionExecutionReserveOutcome {
904 reserve_session_execution_inner(agent, runners, senders, session_id, event_sender, true).await
905}
906
907async fn reserve_session_execution_inner(
908 agent: &Arc<Agent>,
909 runners: &Arc<RwLock<HashMap<String, AgentRunner>>>,
910 senders: &Arc<RwLock<HashMap<String, broadcast::Sender<AgentEvent>>>>,
911 session_id: &str,
912 event_sender: &broadcast::Sender<AgentEvent>,
913 selected_input: bool,
914) -> SessionExecutionReserveOutcome {
915 if let Some(router) = agent.activation_router().cloned() {
916 if let Some(run_id) = runners
917 .read()
918 .await
919 .get(session_id)
920 .filter(|runner| {
921 runner.root_reset_admission_reserved()
922 || matches!(runner.status, super::runner_state::AgentStatus::Running)
923 })
924 .map(|runner| runner.run_id.clone())
925 {
926 return SessionExecutionReserveOutcome::AlreadyRunning { run_id };
927 }
928 let mut reservation =
931 match reserve_logical_session(router, runners, session_id, selected_input).await {
932 Ok(reservation) => reservation,
933 Err(error) => {
934 return SessionExecutionReserveOutcome::AlreadyRunning {
935 run_id: error.existing_run_id().to_owned(),
936 }
937 }
938 };
939 let identity = RunnerReservation {
940 run_id: reservation.run_id.clone(),
941 cancel_token: reservation.cancel_token.clone(),
942 physical_admission: None,
943 };
944 match reserve_runner_for_registration(runners, senders, session_id, event_sender, identity)
945 .await
946 {
947 ReserveOutcome::Reserved(raw) => {
948 reservation.physical_admission = raw.physical_admission;
949 reservation.selected_input = selected_input;
950 return SessionExecutionReserveOutcome::Reserved(reservation);
951 }
952 ReserveOutcome::AlreadyRunning(run_id) => {
953 reservation.abandon().await;
954 return SessionExecutionReserveOutcome::AlreadyRunning { run_id };
955 }
956 }
957 }
958
959 let reservation = match reserve_runner_core(runners, senders, session_id, event_sender).await {
960 ReserveOutcome::AlreadyRunning(run_id) => {
961 return SessionExecutionReserveOutcome::AlreadyRunning { run_id };
962 }
963 ReserveOutcome::Reserved(reservation) => reservation,
964 };
965
966 let execution_reservation = SessionExecutionReservation {
967 session_id: session_id.to_string(),
968 run_id: reservation.run_id,
969 cancel_token: reservation.cancel_token,
970 runners: runners.clone(),
971 activation: SessionExecutionActivationOwnership::Unrouted,
972 root_actor: None,
973 physical_admission: reservation.physical_admission,
974 execution_persistence: None,
975 persistence_bound: false,
976 child_startup: None,
977 child_closure: None,
978 selected_input,
979 armed: true,
980 };
981 SessionExecutionReserveOutcome::Reserved(execution_reservation)
982}
983
984pub fn read_cached_session(cache: &SessionCache, id: &str) -> Option<bamboo_agent_core::Session> {
992 cache
993 .get(id)
994 .map(|e| e.value().clone())
995 .map(|a| a.read().clone())
996}
997
998const SKILL_CONTEXT_START_MARKER: &str = "<!-- BAMBOO_SKILL_CONTEXT_START -->";
999const TOOL_GUIDE_START_MARKER: &str = "<!-- BAMBOO_TOOL_GUIDE_START -->";
1000const EXTERNAL_MEMORY_START_MARKER: &str = "<!-- BAMBOO_EXTERNAL_MEMORY_START -->";
1001const TASK_LIST_START_MARKER: &str = "<!-- BAMBOO_TASK_LIST_START -->";
1002
1003pub struct SessionExecutionOutcome {
1010 pub success: bool,
1012 pub cancelled: bool,
1014 pub error: Option<String>,
1016}
1017
1018impl SessionExecutionOutcome {
1019 fn from_result(result: &Result<(), AgentError>) -> Self {
1020 match result {
1021 Ok(()) => Self {
1022 success: true,
1023 cancelled: false,
1024 error: None,
1025 },
1026 Err(error) => Self {
1027 success: false,
1028 cancelled: error.is_cancelled(),
1029 error: Some(error.to_string()),
1030 },
1031 }
1032 }
1033}
1034
1035pub type SessionCompletionHook = Box<
1045 dyn for<'a> FnOnce(
1046 SessionExecutionOutcome,
1047 &'a mut Session,
1048 ) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>>
1049 + Send,
1050>;
1051
1052pub struct SessionExecutionArgs {
1058 pub agent: Arc<Agent>,
1060 pub session_id: String,
1061 pub session: Session,
1062 pub execution_reservation: SessionExecutionReservation,
1065
1066 pub tools_override: Option<Arc<dyn ToolExecutor>>,
1068 pub provider_override: Option<Arc<dyn LLMProvider>>,
1069 pub model_roster: ModelRoster,
1073 pub reasoning_effort: Option<ReasoningEffort>,
1074 pub reasoning_effort_source: String,
1075 pub auxiliary_model_resolver:
1076 Option<Arc<dyn Fn() -> crate::runtime::config::AuxiliaryModelConfig + Send + Sync>>,
1077 pub disabled_filter_resolver: Option<DisabledFilterResolver>,
1081 pub disabled_tools: Option<BTreeSet<String>>,
1082 pub disabled_skill_ids: Option<BTreeSet<String>>,
1083 pub selected_skill_ids: Option<Vec<String>>,
1084 pub selected_skill_mode: Option<String>,
1085 pub mpsc_tx: mpsc::Sender<AgentEvent>,
1086 pub history_commit_barrier: HistoryCommitBarrier,
1089 pub image_fallback: Option<ImageFallbackConfig>,
1090 pub gold_config: Option<GoldConfig>,
1091 pub guardian_config: Option<GuardianConfig>,
1093 pub guardian_spawner: Option<Arc<dyn GuardianSpawner>>,
1096 pub bash_resume_hook: Option<Arc<dyn BashResumeHook>>,
1098 pub bash_completion_sink: Option<Arc<dyn BashCompletionSink>>,
1100 pub app_data_dir: Option<std::path::PathBuf>,
1101 pub run_budget: Option<bamboo_config::RunBudgetConfig>,
1104
1105 pub runners: Arc<RwLock<HashMap<String, AgentRunner>>>,
1107 pub sessions_cache: SessionCache,
1108
1109 pub on_complete: Option<SessionCompletionHook>,
1112
1113 pub child_completion_handler: Option<Arc<dyn super::ChildCompletionHandler>>,
1125}
1126
1127struct ExecuteRequestParams {
1137 tools: Option<Arc<dyn ToolExecutor>>,
1138 provider_override: Option<Arc<dyn LLMProvider>>,
1139 model_roster: ModelRoster,
1140 reasoning_effort: Option<ReasoningEffort>,
1141 auxiliary_model_resolver: Option<Arc<dyn Fn() -> AuxiliaryModelConfig + Send + Sync>>,
1142 disabled_filter_resolver: Option<DisabledFilterResolver>,
1143 disabled_tools: Option<BTreeSet<String>>,
1144 disabled_skill_ids: Option<BTreeSet<String>>,
1145 selected_skill_ids: Option<Vec<String>>,
1146 selected_skill_mode: Option<String>,
1147 image_fallback: Option<ImageFallbackConfig>,
1148 gold_config: Option<GoldConfig>,
1149 guardian_config: Option<GuardianConfig>,
1150 guardian_spawner: Option<Arc<dyn GuardianSpawner>>,
1151 bash_resume_hook: Option<Arc<dyn BashResumeHook>>,
1152 bash_completion_sink: Option<Arc<dyn BashCompletionSink>>,
1153 app_data_dir: Option<std::path::PathBuf>,
1154 run_budget: Option<bamboo_config::RunBudgetConfig>,
1155}
1156
1157fn build_execute_request(
1164 initial_message: String,
1165 event_tx: mpsc::Sender<AgentEvent>,
1166 cancel_token: CancellationToken,
1167 params: ExecuteRequestParams,
1168) -> ExecuteRequest {
1169 let ExecuteRequestParams {
1170 tools,
1171 provider_override,
1172 model_roster,
1173 reasoning_effort,
1174 auxiliary_model_resolver,
1175 disabled_filter_resolver,
1176 disabled_tools,
1177 disabled_skill_ids,
1178 selected_skill_ids,
1179 selected_skill_mode,
1180 image_fallback,
1181 gold_config,
1182 guardian_config,
1183 guardian_spawner,
1184 bash_resume_hook,
1185 bash_completion_sink,
1186 app_data_dir,
1187 run_budget,
1188 } = params;
1189
1190 let mut builder = ExecuteRequestBuilder::new(initial_message, event_tx, cancel_token)
1191 .model_roster(model_roster)
1192 .gold_config(gold_config)
1193 .guardian_config(guardian_config)
1194 .guardian_spawner(guardian_spawner)
1195 .bash_resume_hook(bash_resume_hook)
1196 .bash_completion_sink(bash_completion_sink);
1197
1198 if let Some(run_budget) = run_budget {
1199 builder = builder.run_budget(run_budget);
1200 }
1201
1202 if let Some(tools) = tools {
1203 builder = builder.tools(tools);
1204 }
1205 if let Some(provider_override) = provider_override {
1206 builder = builder.provider_override(provider_override);
1207 }
1208 if let Some(reasoning_effort) = reasoning_effort {
1209 builder = builder.reasoning_effort(reasoning_effort);
1210 }
1211 if let Some(disabled_filter_resolver) = disabled_filter_resolver {
1212 builder = builder.disabled_filter_resolver(disabled_filter_resolver);
1213 }
1214 if let Some(auxiliary_model_resolver) = auxiliary_model_resolver {
1215 builder = builder.auxiliary_model_resolver(auxiliary_model_resolver);
1216 }
1217 if let Some(disabled_tools) = disabled_tools {
1218 builder = builder.disabled_tools(disabled_tools);
1219 }
1220 if let Some(disabled_skill_ids) = disabled_skill_ids {
1221 builder = builder.disabled_skill_ids(disabled_skill_ids);
1222 }
1223 if let Some(selected_skill_ids) = selected_skill_ids {
1224 builder = builder.selected_skill_ids(selected_skill_ids);
1225 }
1226 if let Some(selected_skill_mode) = selected_skill_mode {
1227 builder = builder.selected_skill_mode(selected_skill_mode);
1228 }
1229 if let Some(image_fallback) = image_fallback {
1230 builder = builder.image_fallback(image_fallback);
1231 }
1232 if let Some(app_data_dir) = app_data_dir {
1233 builder = builder.app_data_dir(app_data_dir);
1234 }
1235
1236 builder.build()
1237}
1238
1239pub fn spawn_session_execution(args: SessionExecutionArgs) {
1248 spawn_session_execution_with_inputs(args, None);
1249}
1250
1251pub fn spawn_session_execution_with_inputs(
1254 args: SessionExecutionArgs,
1255 inputs: Option<crate::runtime::config::UntrustedExecutionInputs>,
1256) {
1257 spawn_session_execution_with_upstream_retry(args, inputs, Default::default());
1258}
1259
1260pub fn spawn_session_execution_with_upstream_retry(
1263 args: SessionExecutionArgs,
1264 inputs: Option<crate::runtime::config::UntrustedExecutionInputs>,
1265 upstream_retry: bamboo_llm::retry::UpstreamRetry,
1266) {
1267 let span_session_id = args.session_id.clone();
1268 let session_span = tracing::info_span!("agent_execution", session_id = %span_session_id);
1269 let is_root = args.session.kind == bamboo_domain::SessionKind::Root;
1270 let outer = if is_root {
1271 match args
1272 .execution_reservation
1273 .physical_admission
1274 .as_ref()
1275 .map(|admission| admission.attach(&args.session))
1276 .transpose()
1277 {
1278 Ok(outer) => outer,
1279 Err(error) => {
1280 tokio::spawn(async move {
1283 let _ = args
1284 .mpsc_tx
1285 .send(AgentEvent::Error {
1286 message: error.to_string(),
1287 })
1288 .await;
1289 args.execution_reservation.abandon().await;
1290 });
1291 return;
1292 }
1293 }
1294 } else {
1295 None
1296 };
1297 let task_outer = outer.clone();
1298
1299 let task = tokio::spawn(
1300 async move {
1301 let SessionExecutionArgs {
1302 agent,
1303 session_id,
1304 mut session,
1305 mut execution_reservation,
1306 tools_override,
1307 provider_override,
1308 model_roster,
1309 reasoning_effort,
1310 reasoning_effort_source,
1311 auxiliary_model_resolver,
1312 disabled_filter_resolver,
1313 disabled_tools,
1314 disabled_skill_ids,
1315 selected_skill_ids,
1316 selected_skill_mode,
1317 mpsc_tx,
1318 mut history_commit_barrier,
1319 image_fallback,
1320 gold_config,
1321 guardian_config,
1322 guardian_spawner,
1323 bash_resume_hook,
1324 bash_completion_sink,
1325 app_data_dir,
1326 run_budget,
1327 runners,
1328 sessions_cache,
1329 on_complete,
1330 child_completion_handler,
1331 } = args;
1332
1333 if !execution_reservation.matches_execution_target(&session_id, &session.id, &runners) {
1334 tracing::error!(
1335 %session_id,
1336 domain_session_id = %session.id,
1337 reservation_session_id = %execution_reservation.session_id(),
1338 run_id = %execution_reservation.run_id(),
1339 same_runner_registry = Arc::ptr_eq(&execution_reservation.runners, &runners),
1340 "refusing mismatched session execution reservation"
1341 );
1342 execution_reservation.abandon().await;
1343 return;
1344 }
1345 if let Err(error) = execution_reservation.ensure_registered().await {
1346 tracing::warn!(
1347 %session_id,
1348 run_id = %execution_reservation.run_id(),
1349 %error,
1350 "session execution could not adopt its router activation owner"
1351 );
1352 return;
1353 }
1354 if execution_reservation.root_actor.is_none()
1355 && agent.persistence().root_actor_execution_required(&session)
1356 {
1357 let message =
1358 "Root execution route requires a bound Actor writer and event handoff";
1359 tracing::warn!(%session_id, "{message}");
1360 let _ = mpsc_tx
1361 .send(AgentEvent::Error {
1362 message: message.into(),
1363 })
1364 .await;
1365 execution_reservation.abandon().await;
1366 return;
1367 }
1368 if let Err(error) = execution_reservation.bind_execution_persistence(&agent, &session) {
1371 let _ = mpsc_tx
1372 .send(AgentEvent::Error {
1373 message: error.to_string(),
1374 })
1375 .await;
1376 execution_reservation.abandon().await;
1377 return;
1378 }
1379 let ownership = match &execution_reservation.activation {
1380 SessionExecutionActivationOwnership::Registered(run) => Some(run.ownership()),
1381 _ => None,
1382 };
1383 let admission_persistence = execution_reservation
1384 .execution_persistence()
1385 .unwrap_or_else(|| agent.persistence().clone());
1386 if let (Some(router), Some(ownership)) = (agent.activation_router(), ownership.as_ref())
1387 {
1388 if !router.bind_run_lifetime(ownership, &session).await {
1389 let _ = mpsc_tx
1390 .send(AgentEvent::Error {
1391 message: "registered run Session lifetime differs".into(),
1392 })
1393 .await;
1394 execution_reservation.abandon().await;
1395 return;
1396 }
1397 }
1398 if let Err(error) = crate::runtime::counted_loop::admit_registered(
1399 &mut session,
1400 ownership.as_ref(),
1401 agent.activation_router(),
1402 admission_persistence.as_ref(),
1403 )
1404 .await
1405 {
1406 let _ = mpsc_tx
1407 .send(AgentEvent::Error {
1408 message: error.to_string(),
1409 })
1410 .await;
1411 execution_reservation.abandon().await;
1412 return;
1413 }
1414 let mut root_actor = execution_reservation.root_actor.take().map(|owner| *owner);
1415 let root_actor_bound = root_actor.is_some();
1416 let persistence = execution_reservation
1417 .execution_persistence()
1418 .unwrap_or_else(|| agent.persistence().clone());
1419 let agent = Arc::new(agent.with_execution_persistence(persistence));
1420 let activation_run_id = execution_reservation.run_id().to_string();
1421 if session.kind == bamboo_agent_core::SessionKind::Child {
1422 if let Err(error) = execution_reservation
1426 .arm_child_host_startup_inner(&agent, &session, sessions_cache.clone(), None)
1427 .await
1428 {
1429 let _ = mpsc_tx
1430 .send(AgentEvent::Error {
1431 message: error.to_string(),
1432 })
1433 .await;
1434 execution_reservation.abandon().await;
1435 return;
1436 }
1437 if let Err(error) = agent
1438 .storage()
1439 .begin_host_run(&session, &activation_run_id)
1440 .await
1441 {
1442 let _ = mpsc_tx
1443 .send(AgentEvent::Error {
1444 message: error.to_string(),
1445 })
1446 .await;
1447 execution_reservation
1448 .fail_child_host_startup(&error.to_string())
1449 .await;
1450 execution_reservation.abandon().await;
1451 return;
1452 }
1453 session.set_host_run_id(&activation_run_id);
1454 session.set_last_run_status("running");
1455 }
1456 let prior_message_ids = session.messages.iter().map(|m| m.id.clone()).collect();
1457 let (cancel_token, mut activation_registration) =
1458 execution_reservation.disarm_for_execution();
1459
1460 let model = model_roster.model.clone().unwrap_or_default();
1464
1465 let initial_message = initial_user_message_for_session(&session);
1466 let selected_skill_ids =
1467 selected_skill_ids.or_else(|| selected_skill_ids_for_session(&session));
1468 let selected_skill_mode =
1469 selected_skill_mode.or_else(|| selected_skill_mode_for_session(&session));
1470
1471 tracing::info!(
1472 "[{}] Using resolved session model: {}, reasoning_effort={}, reasoning_source={}",
1473 session_id,
1474 model,
1475 reasoning_effort
1476 .map(ReasoningEffort::as_str)
1477 .unwrap_or("none"),
1478 reasoning_effort_source
1479 );
1480
1481 crate::session_app::execution_prep::prepare_session_for_execution(
1488 &mut session,
1489 None,
1490 Some(&model),
1491 );
1492
1493 let system_prompt = system_prompt_for_debug_log(&mut session);
1494 if let Some(prompt) = system_prompt.as_ref() {
1495 log_base_system_prompt_snapshot(&session_id, prompt);
1496 }
1497
1498 let inherited_child_wait = agent.persistence().inherited_child_wait();
1502 let execute_request = build_execute_request(
1503 initial_message,
1504 mpsc_tx.clone(),
1505 cancel_token,
1506 ExecuteRequestParams {
1507 tools: tools_override,
1508 provider_override,
1509 model_roster,
1510 reasoning_effort,
1511 auxiliary_model_resolver,
1512 disabled_filter_resolver,
1513 disabled_tools,
1514 disabled_skill_ids,
1515 selected_skill_ids,
1516 selected_skill_mode,
1517 image_fallback,
1518 gold_config,
1519 guardian_config,
1520 guardian_spawner,
1521 bash_resume_hook,
1522 bash_completion_sink,
1523 app_data_dir,
1524 run_budget,
1525 },
1526 );
1527
1528 let result = {
1535 use futures::FutureExt;
1536 match std::panic::AssertUnwindSafe(agent.execute_with_inputs_and_upstream_retry(
1537 &mut session,
1538 execute_request,
1539 inputs,
1540 upstream_retry,
1541 ))
1542 .catch_unwind()
1543 .await
1544 {
1545 Ok(result) => result,
1546 Err(panic) => {
1547 if let Some(outer) = &task_outer {
1548 outer
1549 .unconfirmed("Root loop unwound; physical effects are unconfirmed");
1550 }
1551 let message = panic
1552 .downcast_ref::<&str>()
1553 .map(|s| (*s).to_string())
1554 .or_else(|| panic.downcast_ref::<String>().cloned())
1555 .unwrap_or_else(|| "non-string panic payload".to_string());
1556 tracing::error!(
1557 "[{}] agent execution panicked; finalizing as terminal error: {}",
1558 session_id,
1559 message
1560 );
1561 Err(AgentError::LLM(format!(
1562 "agent execution panicked: {message}"
1563 )))
1564 }
1565 }
1566 };
1567
1568 if let Some(error_event) = terminal_error_event_for_result(&result) {
1570 let _ = mpsc_tx.send(error_event).await;
1571 }
1572
1573 let suspended_non_terminal = result.is_ok() && session.has_runtime_wait();
1590 match &result {
1591 Ok(()) if suspended_non_terminal => {
1592 session.set_last_run_status("suspended");
1593 session.clear_last_run_error();
1594 }
1595 Ok(()) => {
1596 session.set_last_run_status("completed");
1597 session.clear_last_run_error();
1598 }
1599 Err(error) if error.is_cancelled() => {
1600 session.set_last_run_status("cancelled");
1601 session.set_last_run_error(error.to_string());
1602 }
1603 Err(error) => {
1604 session.set_last_run_status("error");
1605 session.set_last_run_error(error.to_string());
1606 }
1607 }
1608
1609 session.synchronize_terminal_runtime_status();
1610
1611 if let Some(on_complete) = on_complete {
1615 on_complete(SessionExecutionOutcome::from_result(&result), &mut session).await;
1616 }
1617
1618 let executed_admitted_generation = session
1622 .session_inbox_admission()
1623 .map_or(0, |state| state.last_admitted_sequence);
1624 let legacy_migration =
1625 crate::runtime::runner::state_bridge::migrate_legacy_pending_only(
1626 &mut session,
1627 Some(agent.storage()),
1628 Some(agent.persistence()),
1629 agent.session_inbox(),
1630 )
1631 .await;
1632 let pending_boundary_generation = session
1633 .session_inbox_admission()
1634 .and_then(|state| state.pending_activation_generation());
1635 let pending_generation = match (
1636 pending_boundary_generation,
1637 legacy_migration.highest_generation,
1638 ) {
1639 (Some(left), Some(right)) => Some(left.max(right)),
1640 (left, right) => left.or(right),
1641 };
1642 if let (Some(generation), Some(router)) =
1643 (pending_generation, agent.activation_router())
1644 {
1645 let activation_ready = if let Some(inbox) = agent.session_inbox() {
1646 match inbox
1647 .mark_activation_eligible(
1648 &session_id,
1649 generation,
1650 bamboo_domain::SessionActivationPolicy::InterruptSpecificWait,
1651 )
1652 .await
1653 {
1654 Ok(()) => true,
1655 Err(error) => {
1656 tracing::error!(
1657 session_id = %session_id,
1658 %error,
1659 "failed to persist unadmitted SessionInbox activation watermark"
1660 );
1661 false
1662 }
1663 }
1664 } else {
1665 false
1666 };
1667 if activation_ready {
1668 if let Err(error) = bamboo_domain::SessionActivationPort::request_activation(
1669 router.as_ref(),
1670 &session_id,
1671 generation,
1672 )
1673 .await
1674 {
1675 tracing::error!(
1676 session_id = %session_id,
1677 %error,
1678 "failed to hand unadmitted SessionInbox generation to activation router"
1679 );
1680 }
1681 }
1682 }
1683
1684 if let Some(registration) = activation_registration.as_mut() {
1688 registration.begin_finalization().await;
1689 }
1690
1691 super::child_completion::ChildCompletionSource::prepare(
1695 &mut session,
1696 &activation_run_id,
1697 &prior_message_ids,
1698 );
1699 let saved = save_finalized_runtime_with_inherited_child_wait(
1700 agent.persistence().as_ref(),
1701 &mut session,
1702 inherited_child_wait
1703 .as_ref()
1704 .map(|inherited| inherited.wait()),
1705 )
1706 .await;
1707 let history_committed = saved.is_ok();
1708 let completion_source =
1709 super::child_completion::ChildCompletionSource::after_final_save(
1710 &session,
1711 history_committed,
1712 );
1713 let completed_at = chrono::Utc::now();
1714 if let Err(error) = &saved {
1715 if let Some(outer) = &task_outer {
1716 outer.unconfirmed(format!(
1717 "Root final persistence outcome unconfirmed: {error}"
1718 ));
1719 }
1720 tracing::warn!("[{}] Failed to save session: {}", session_id, error);
1721 }
1722
1723 if history_committed
1729 && !history_commit_barrier
1730 .send_and_wait(&mpsc_tx, session_id.clone())
1731 .await
1732 {
1733 if let Some(outer) = &task_outer {
1734 outer.unconfirmed("Root history publication barrier unconfirmed");
1735 }
1736 tracing::warn!(
1737 session_id = %session_id,
1738 "history commit barrier could not be published before runner finalization"
1739 );
1740 }
1741
1742 finalize_runner_exact(&runners, &session_id, &activation_run_id, &result).await;
1748
1749 #[cfg(test)]
1750 if let Some(outer) = &task_outer {
1751 outer.pause_before_actor_finish().await;
1752 }
1753
1754 if let Some(owner) = root_actor.take() {
1755 let outcome = if result.as_ref().is_err_and(|error| error.is_cancelled()) {
1756 bamboo_domain::ActorActivationFinish::Cancelled
1757 } else if result.is_err() || !history_committed {
1758 bamboo_domain::ActorActivationFinish::Failed
1759 } else {
1760 bamboo_domain::ActorActivationFinish::Succeeded
1761 };
1762 let finished = owner.finish_observed(outcome).await;
1763 if let Err(error) = &finished {
1764 tracing::warn!(%session_id, %error, "Root Actor closure unconfirmed");
1765 }
1766 if let Some(outer) = &task_outer {
1767 outer.actor_finished(finished);
1768 }
1769 }
1770
1771 let finalization = if let Some(registration) = activation_registration.take() {
1772 registration.finish(executed_admitted_generation).await
1773 } else {
1774 Ok(None)
1775 };
1776 if let Err(error) = finalization {
1777 if let Some(outer) = &task_outer {
1778 outer.unconfirmed(format!(
1779 "Root registration finalization unconfirmed: {error}"
1780 ));
1781 }
1782 tracing::error!(
1786 session_id = %session_id,
1787 %error,
1788 "failed to activate successor for finalization-racing SessionInbox delivery"
1789 );
1790 }
1791
1792 let child_completion = child_completion_handler.filter(|_| {
1801 session.kind == bamboo_agent_core::SessionKind::Child
1802 && session.parent_session_id.is_some()
1803 });
1804 let parent_session_id = session.parent_session_id.clone();
1805 let child_status = session.last_run_status();
1806 let child_error = session.last_run_error();
1807
1808 publish_final_session_cache(&sessions_cache, session, root_actor_bound, &saved);
1809
1810 if let (Some(handler), Some(parent_session_id), Some(status)) =
1811 (child_completion, parent_session_id, child_status)
1812 {
1813 use futures::FutureExt;
1814 let completion = ChildCompletion {
1815 parent_session_id: parent_session_id.clone(),
1816 child_session_id: session_id.clone(),
1817 status,
1818 error: child_error,
1819 completed_at,
1820 source: completion_source,
1821 };
1822 if std::panic::AssertUnwindSafe(handler.on_child_completed(completion))
1823 .catch_unwind()
1824 .await
1825 .is_err()
1826 {
1827 tracing::error!(
1828 %parent_session_id,
1829 child_session_id = %session_id,
1830 "child completion handler panicked on resumed-child terminal"
1831 );
1832 }
1833 }
1834
1835 tracing::info!("[{}] Agent execution completed", session_id);
1836 if let Some(outer) = &task_outer {
1837 outer.tail_completed();
1838 }
1839 }
1840 .instrument(session_span),
1841 );
1842 if let Some(outer) = outer {
1843 outer.retain(task);
1844 } else if is_root {
1845 tokio::spawn(async move {
1848 let _ = task.await;
1849 });
1850 }
1851}
1852
1853async fn save_finalized_runtime_with_inherited_child_wait(
1855 persistence: &dyn bamboo_domain::RuntimeSessionPersistence,
1856 session: &mut Session,
1857 inherited_child_wait: Option<&bamboo_domain::session::runtime_state::WaitingForChildrenState>,
1858) -> std::io::Result<()> {
1859 if let Some(inherited) = inherited_child_wait.filter(|wait| {
1860 session
1861 .agent_runtime_state
1862 .as_ref()
1863 .and_then(|runtime| runtime.waiting_for_children.as_ref())
1864 == Some(*wait)
1865 }) {
1866 persistence
1867 .save_finalized_runtime_with_inherited_child_wait(session, inherited)
1868 .await
1869 } else {
1870 persistence.save_finalized_runtime_session(session).await
1871 }
1872}
1873
1874fn publish_final_session_cache(
1875 cache: &SessionCache,
1876 session: Session,
1877 root_actor_bound: bool,
1878 saved: &std::io::Result<()>,
1879) {
1880 let authority_conflict = saved.as_ref().err().is_some_and(|error| {
1881 error
1882 .get_ref()
1883 .is_some_and(|cause| cause.is::<bamboo_domain::SessionAuthorityConflict>())
1884 });
1885 if !authority_conflict && !root_actor_bound {
1888 cache.insert(
1889 session.id.clone(),
1890 Arc::new(crate::SessionSnapshot::new(session)),
1891 );
1892 }
1893}
1894
1895pub fn log_base_system_prompt_snapshot(session_id: &str, prompt: &str) {
1897 tracing::info!(
1898 "[{}] Base system prompt snapshot: len={} chars, has_skill={}, has_tool_guide={}, has_external_memory={}, has_task_list={}",
1899 session_id,
1900 prompt.len(),
1901 prompt.contains(SKILL_CONTEXT_START_MARKER),
1902 prompt.contains(TOOL_GUIDE_START_MARKER),
1903 prompt.contains(EXTERNAL_MEMORY_START_MARKER),
1904 prompt.contains(TASK_LIST_START_MARKER),
1905 );
1906
1907 tracing::debug!(
1908 "[{}] ========== BASE SYSTEM PROMPT SNAPSHOT ==========",
1909 session_id
1910 );
1911 tracing::debug!("[{}] Snapshot length: {} chars", session_id, prompt.len());
1912 tracing::debug!("[{}] -----------------------------------", session_id);
1913 tracing::debug!("[{}] {}", session_id, prompt);
1914 tracing::debug!(
1915 "[{}] ========== END BASE SYSTEM PROMPT SNAPSHOT ==========",
1916 session_id
1917 );
1918}
1919
1920pub fn terminal_error_event_for_result(result: &Result<(), AgentError>) -> Option<AgentEvent> {
1922 match result {
1923 Ok(_) => None,
1924 Err(error) if error.is_cancelled() => Some(AgentEvent::Error {
1925 message: "Agent execution cancelled by user".to_string(),
1926 }),
1927 Err(error) => Some(AgentEvent::Error {
1928 message: error.to_string(),
1929 }),
1930 }
1931}
1932
1933fn system_prompt_for_session(session: &Session) -> Option<String> {
1936 session
1937 .messages
1938 .iter()
1939 .find(|message| matches!(message.role, bamboo_agent_core::Role::System))
1940 .map(|message| message.content.clone())
1941}
1942
1943fn system_prompt_for_debug_log(session: &mut Session) -> Option<String> {
1947 crate::runtime::runner::session_setup::migrate_legacy_workspace_prompt(session);
1948 system_prompt_for_session(session)
1949}
1950
1951fn initial_user_message_for_session(session: &Session) -> String {
1952 session
1953 .messages
1954 .last()
1955 .filter(|message| matches!(message.role, bamboo_agent_core::Role::User))
1956 .map(|message| message.content.clone())
1957 .unwrap_or_default()
1958}
1959
1960fn selected_skill_ids_for_session(session: &Session) -> Option<Vec<String>> {
1961 session
1962 .metadata
1963 .get("selected_skill_ids")
1964 .and_then(|raw| bamboo_skills::selection::parse_selected_skill_ids_metadata(raw))
1965}
1966
1967fn selected_skill_mode_for_session(session: &Session) -> Option<String> {
1968 let value = session
1969 .metadata
1970 .get("skill_mode")
1971 .or_else(|| session.metadata.get("mode"))?;
1972 let trimmed = value.trim();
1973 if trimmed.is_empty() {
1974 None
1975 } else {
1976 Some(trimmed.to_string())
1977 }
1978}
1979
1980pub(super) struct RootActorExecution {
1981 binding: bamboo_domain::RootActorExecutionBinding,
1982 stop_renewal: CancellationToken,
1983 renewal: Option<tokio::task::JoinHandle<()>>,
1984 response_events: Vec<AgentEvent>,
1985}
1986
1987impl RootActorExecution {
1988 pub(super) fn start(binding: bamboo_domain::RootActorExecutionBinding) -> Self {
1989 let stop_renewal = CancellationToken::new();
1990 let stop = stop_renewal.clone();
1991 let directory = binding.directory.clone();
1992 let fence = binding.owner.fence.clone();
1993 let duration = binding.lease_duration;
1994 let renewal = tokio::spawn(async move {
1995 let mut interval = tokio::time::interval(duration / 3);
1996 interval.tick().await;
1997 loop {
1998 tokio::select! {
1999 _ = stop.cancelled() => break,
2000 _ = interval.tick() => {
2001 let now = chrono::Utc::now();
2002 let expires = now + chrono::Duration::from_std(duration).expect("bounded Root lease");
2003 if let Err(error) = directory.renew_activation(&fence, now, expires).await {
2004 if matches!(error, bamboo_domain::ActorDirectoryError::Storage(_)) {
2012 tracing::warn!(actor_id = %fence.actor_id, run_id = %fence.run_id,
2013 %error, "Root Actor renewal hit transient storage failure; retrying next tick");
2014 continue;
2015 }
2016 tracing::warn!(actor_id = %fence.actor_id, run_id = %fence.run_id,
2017 %error, "Root Actor renewal lost its execution authority");
2018 break;
2019 }
2020 }
2021 }
2022 }
2023 });
2024 Self {
2025 binding,
2026 stop_renewal,
2027 renewal: Some(renewal),
2028 response_events: Vec::new(),
2029 }
2030 }
2031
2032 pub(super) fn persistence(
2033 &self,
2034 ) -> std::sync::Arc<dyn bamboo_domain::RuntimeSessionPersistence> {
2035 self.binding.persistence.clone()
2036 }
2037
2038 pub(super) async fn finish(self, outcome: bamboo_domain::ActorActivationFinish) {
2039 if let Err(error) = self.finish_observed(outcome).await {
2040 tracing::warn!(%error, "Root Actor closure unconfirmed");
2041 }
2042 }
2043
2044 async fn finish_observed(
2045 mut self,
2046 outcome: bamboo_domain::ActorActivationFinish,
2047 ) -> std::io::Result<()> {
2048 self.stop_renewal.cancel();
2049 let renewal = match self.renewal.take() {
2050 Some(renewal) => renewal.await.map_err(std::io::Error::other),
2051 None => Ok(()),
2052 };
2053 let directory = self.binding.directory.clone();
2054 let owner = self.binding.owner.clone();
2055 let publish = self.binding.terminal_publisher.clone();
2056 let finish = tokio::spawn(async move {
2057 directory
2058 .finish_root_activation_observed(&owner, chrono::Utc::now(), outcome, publish)
2059 .await
2060 .map(|_| ())
2061 .map_err(std::io::Error::other)
2062 });
2063 self.binding.disarm_abandonment();
2064 finish.await.map_err(std::io::Error::other)??;
2065 renewal
2066 }
2067}
2068
2069impl Drop for RootActorExecution {
2070 fn drop(&mut self) {
2071 self.stop_renewal.cancel();
2072 }
2075}
2076
2077#[cfg(test)]
2078#[path = "agent_spawn_peer_privacy_tests.rs"]
2079mod peer_privacy_tests;
2080
2081#[cfg(test)]
2082mod reservation_tests {
2083 use super::*;
2084 use crate::runtime::execution::runner_state::AgentStatus;
2085
2086 struct CompletionAtFinalSave {
2087 directory: std::path::PathBuf,
2088 persistence: bamboo_storage::LockedSessionStore,
2089 cleared: std::sync::atomic::AtomicBool,
2090 }
2091
2092 impl CompletionAtFinalSave {
2093 async fn clear_wait(&self, id: &str) {
2094 let mut process = std::process::Command::new(std::env::current_exe().unwrap())
2095 .args(["--exact", "runtime::execution::agent_spawn::reservation_tests::inherited_child_wait_adopts_independent_completion_at_final_save", "--nocapture"])
2096 .env("BAMBOO_FINAL_WAIT_COMPLETION_DIR", &self.directory)
2097 .env("BAMBOO_FINAL_WAIT_COMPLETION_ID", id)
2098 .spawn().unwrap();
2099 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(15);
2100 loop {
2101 if let Some(status) = process.try_wait().unwrap() {
2102 assert!(status.success(), "completion process failed");
2103 break;
2104 }
2105 if std::time::Instant::now() >= deadline {
2106 let _ = process.kill();
2107 let _ = process.wait();
2108 panic!("bounded completion process deadline");
2109 }
2110 std::thread::sleep(std::time::Duration::from_millis(5));
2111 }
2112 self.cleared
2113 .store(true, std::sync::atomic::Ordering::SeqCst);
2114 }
2115 }
2116
2117 #[async_trait::async_trait]
2118 impl bamboo_domain::RuntimeSessionPersistence for CompletionAtFinalSave {
2119 async fn save_runtime_session(&self, session: &mut Session) -> std::io::Result<()> {
2120 self.clear_wait(&session.id).await;
2121 self.persistence.merge_save_finalized_runtime(session).await
2122 }
2123 async fn save_finalized_runtime_with_inherited_child_wait(
2124 &self,
2125 session: &mut Session,
2126 inherited: &bamboo_domain::session::runtime_state::WaitingForChildrenState,
2127 ) -> std::io::Result<()> {
2128 self.clear_wait(&session.id).await;
2129 self.persistence
2130 .merge_save_inherited_child_wait_and_publish(session, inherited, |_, _| {})
2131 .await
2132 }
2133 }
2134
2135 #[tokio::test]
2136 async fn inherited_child_wait_adopts_independent_completion_at_final_save() {
2137 use bamboo_agent_core::storage::Storage;
2138 use bamboo_domain::session::runtime_state::{
2139 AgentRuntimeState, AgentStatusState, ChildWaitPolicy, WaitingForChildrenState,
2140 };
2141 if let Some(directory) = std::env::var_os("BAMBOO_FINAL_WAIT_COMPLETION_DIR") {
2142 let remote = bamboo_storage::SessionStoreV2::new(directory.into())
2143 .await
2144 .unwrap();
2145 let id = std::env::var("BAMBOO_FINAL_WAIT_COMPLETION_ID").unwrap();
2146 let mut latest = remote.load_session(&id).await.unwrap().unwrap();
2147 let runtime = latest.agent_runtime_state.as_mut().unwrap();
2148 runtime.waiting_for_children = None;
2149 runtime.status = AgentStatusState::Idle;
2150 runtime.suspension = None;
2151 latest.metadata.remove("runtime.suspend_reason");
2152 remote.save_runtime_state(&latest).await.unwrap();
2153 return;
2154 }
2155 let temp = tempfile::tempdir().unwrap();
2156 let local = Arc::new(
2157 bamboo_storage::SessionStoreV2::new(temp.path().to_path_buf())
2158 .await
2159 .unwrap(),
2160 );
2161 let mut runner = Session::new("cross-host-final-wait", "model");
2165 let inherited = WaitingForChildrenState::for_children(
2166 vec!["child".into()],
2167 ChildWaitPolicy::All,
2168 chrono::Utc::now(),
2169 );
2170 let mut runtime = AgentRuntimeState::new("run");
2171 runtime.waiting_for_children = Some(inherited.clone());
2172 runtime.status = AgentStatusState::Suspended;
2173 runner.agent_runtime_state = Some(runtime);
2174 runner.metadata.insert(
2175 "runtime.suspend_reason".into(),
2176 "waiting_for_children".into(),
2177 );
2178 runner.set_last_run_status("suspended");
2179 local.save_session(&runner).await.unwrap();
2180 let persistence = CompletionAtFinalSave {
2181 directory: temp.path().to_path_buf(),
2182 persistence: bamboo_storage::LockedSessionStore::new(local.clone()),
2183 cleared: std::sync::atomic::AtomicBool::new(false),
2184 };
2185 save_finalized_runtime_with_inherited_child_wait(
2186 &persistence,
2187 &mut runner,
2188 Some(&inherited),
2189 )
2190 .await
2191 .unwrap();
2192 assert!(persistence
2193 .cleared
2194 .load(std::sync::atomic::Ordering::SeqCst));
2195 let durable = local.load_session(&runner.id).await.unwrap().unwrap();
2196 for session in [&runner, &durable] {
2197 let runtime = session.agent_runtime_state.as_ref().unwrap();
2198 assert!(
2199 runtime.waiting_for_children.is_none(),
2200 "independent completion must not be resurrected"
2201 );
2202 assert_eq!(runtime.status, AgentStatusState::Idle);
2203 assert!(runtime.suspension.is_none());
2204 assert!(!session.metadata.contains_key("runtime.suspend_reason"));
2205 assert_eq!(session.last_run_status().as_deref(), Some("completed"));
2206 }
2207 }
2208
2209 #[tokio::test]
2210 async fn inherited_child_wait_rejects_aba_and_missing_birth_without_cache_publication() {
2211 use bamboo_agent_core::storage::Storage;
2212 use bamboo_domain::session::runtime_state::{
2213 AgentRuntimeState, ChildWaitPolicy, WaitingForChildrenState,
2214 };
2215 use bamboo_storage::{LockedSessionStore, SessionStoreV2};
2216
2217 for missing in [false, true] {
2218 let temp = tempfile::tempdir().unwrap();
2219 let storage = Arc::new(
2220 SessionStoreV2::new(temp.path().to_path_buf())
2221 .await
2222 .unwrap(),
2223 );
2224 let persistence = LockedSessionStore::new(storage.clone());
2225 let cache: SessionCache = Arc::default();
2226 storage
2227 .save_session(&Session::new("root", "model"))
2228 .await
2229 .unwrap();
2230 let mut stale = Session::new("nested-child-parent", "model");
2231 stale.kind = bamboo_domain::SessionKind::Child;
2232 stale.parent_session_id = Some("root".into());
2233 stale.root_session_id = "root".into();
2234 stale.spawn_depth = 1;
2235 let wait = WaitingForChildrenState::for_children(
2236 vec!["grandchild".into()],
2237 ChildWaitPolicy::All,
2238 chrono::Utc::now(),
2239 );
2240 let mut runtime = AgentRuntimeState::new("old-run");
2241 runtime.waiting_for_children = Some(wait.clone());
2242 stale.agent_runtime_state = Some(runtime);
2243 stale.add_message(bamboo_agent_core::Message::assistant("STALE", None));
2244 storage.save_session(&stale).await.unwrap();
2245
2246 let mut replacement = stale.clone();
2247 replacement.created_at += chrono::Duration::seconds(1);
2248 replacement.messages.clear();
2249 replacement.add_message(bamboo_agent_core::Message::assistant("REPLACEMENT", None));
2250 replacement
2251 .agent_runtime_state
2252 .as_mut()
2253 .unwrap()
2254 .waiting_for_children = None;
2255 storage.delete_session(&stale.id).await.unwrap();
2256 if !missing {
2257 storage.save_session(&replacement).await.unwrap();
2258 }
2259 if !missing {
2260 cache.insert(
2261 replacement.id.clone(),
2262 Arc::new(crate::SessionSnapshot::new(replacement.clone())),
2263 );
2264 }
2265
2266 let saved = save_finalized_runtime_with_inherited_child_wait(
2267 &persistence,
2268 &mut stale,
2269 Some(&wait),
2270 )
2271 .await;
2272 assert!(saved.is_err(), "stale incarnation must not be saved");
2273 publish_final_session_cache(&cache, stale, false, &saved);
2276 if missing {
2277 assert!(
2278 read_cached_session(&cache, &replacement.id).is_none(),
2279 "deleted incarnation must not be resurrected in cache"
2280 );
2281 } else {
2282 let cached = read_cached_session(&cache, &replacement.id).unwrap();
2283 assert_eq!(
2284 cached.created_at, replacement.created_at,
2285 "ABA rejection must retain replacement cache birth"
2286 );
2287 assert_eq!(
2288 serde_json::to_value(&cached.messages).unwrap(),
2289 serde_json::to_value(&replacement.messages).unwrap(),
2290 "ABA rejection must retain replacement cache transcript"
2291 );
2292 }
2293 assert!(saved
2294 .as_ref()
2295 .unwrap_err()
2296 .get_ref()
2297 .is_some_and(|cause| cause.is::<bamboo_domain::SessionAuthorityConflict>()));
2298 let durable = storage.load_session(&replacement.id).await.unwrap();
2299 if missing {
2300 assert!(durable.is_none());
2301 } else {
2302 let durable = durable.unwrap();
2303 assert_eq!(durable.created_at, replacement.created_at);
2304 assert_eq!(
2305 serde_json::to_value(&durable.messages).unwrap(),
2306 serde_json::to_value(&replacement.messages).unwrap()
2307 );
2308 }
2309 }
2310 }
2311
2312 #[test]
2313 fn debug_prompt_snapshot_migrates_legacy_host_paths_before_logging() {
2314 let legacy_project = format!(
2315 "{}\nProject ID: legacy-project\nProject path: /private/legacy-project\nProject home: /private/legacy-home\n{}",
2316 crate::runtime::context::PROJECT_CONTEXT_START_MARKER,
2317 crate::runtime::context::PROJECT_CONTEXT_END_MARKER,
2318 );
2319 let legacy_workspace =
2320 crate::runtime::context::build_workspace_prompt_context("/private/legacy-workspace")
2321 .expect("legacy workspace context");
2322 let legacy_instruction = format!(
2323 "{}\nSource: /private/legacy-workspace/AGENTS.md\nlegacy policy\n{}",
2324 crate::runtime::context::instruction::INSTRUCTION_CONTEXT_START_MARKER,
2325 crate::runtime::context::instruction::INSTRUCTION_CONTEXT_END_MARKER,
2326 );
2327 let mut session = Session::new("legacy-debug-log", "model");
2328 session.add_message(bamboo_agent_core::Message::system(format!(
2329 "Base prompt\n\n{legacy_project}\n\n{legacy_workspace}\n\n{legacy_instruction}"
2330 )));
2331
2332 let prompt = system_prompt_for_debug_log(&mut session).expect("normalized System");
2333
2334 assert_eq!(prompt, "Base prompt");
2335 assert_eq!(
2336 session.workspace_path_meta().as_deref(),
2337 Some("/private/legacy-workspace")
2338 );
2339 for private_path in [
2340 "/private/legacy-project",
2341 "/private/legacy-home",
2342 "/private/legacy-workspace",
2343 ] {
2344 assert!(!prompt.contains(private_path));
2345 }
2346 assert!(!prompt.contains(crate::runtime::context::PROJECT_CONTEXT_START_MARKER));
2347 assert!(!prompt.contains(crate::runtime::context::WORKSPACE_CONTEXT_START_MARKER));
2348 assert!(!prompt
2349 .contains(crate::runtime::context::instruction::INSTRUCTION_CONTEXT_START_MARKER));
2350 }
2351
2352 #[test]
2353 fn reservation_target_requires_domain_id_and_exact_runner_registry() {
2354 let runners = Arc::new(RwLock::new(HashMap::new()));
2355 let other_runners = Arc::new(RwLock::new(HashMap::new()));
2356 let mut reservation = SessionExecutionReservation {
2357 session_id: "session-a".to_string(),
2358 run_id: "run-a".to_string(),
2359 cancel_token: CancellationToken::new(),
2360 runners: runners.clone(),
2361 activation: SessionExecutionActivationOwnership::Unrouted,
2362 root_actor: None,
2363 physical_admission: None,
2364 execution_persistence: None,
2365 persistence_bound: false,
2366 child_startup: None,
2367 child_closure: None,
2368 selected_input: false,
2369 armed: true,
2370 };
2371
2372 assert!(reservation.matches_execution_target("session-a", "session-a", &runners));
2373 assert!(!reservation.matches_execution_target("session-b", "session-a", &runners));
2374 assert!(!reservation.matches_execution_target("session-a", "session-b", &runners));
2375 assert!(!reservation.matches_execution_target("session-a", "session-a", &other_runners));
2376 reservation.armed = false;
2377 }
2378
2379 #[tokio::test]
2380 async fn forced_same_run_registration_rejection_never_cancels_live_owner() {
2381 let runners = Arc::new(RwLock::new(HashMap::new()));
2382 let mut runner = AgentRunner::new();
2383 runner.status = AgentStatus::Running;
2384 let run_id = runner.run_id.clone();
2385 let live_cancel_token = runner.cancel_token.clone();
2386 runners.write().await.insert("same-run".to_string(), runner);
2387
2388 let router = SessionActivationRouter::new();
2389 let mut live_registration = router
2390 .register_run("same-run", &run_id)
2391 .await
2392 .expect("first registration owns the run");
2393 let mut rejected = SessionExecutionReservation {
2394 session_id: "same-run".to_string(),
2395 run_id: run_id.clone(),
2396 cancel_token: live_cancel_token.clone(),
2397 runners: runners.clone(),
2398 activation: SessionExecutionActivationOwnership::RegistrationPending(router.clone()),
2399 root_actor: None,
2400 physical_admission: None,
2401 execution_persistence: None,
2402 persistence_bound: false,
2403 child_startup: None,
2404 child_closure: None,
2405 selected_input: false,
2406 armed: true,
2407 };
2408
2409 let error = match rejected.ensure_registered().await {
2410 Ok(()) => panic!("a duplicate registration for the same run must be rejected"),
2411 Err(error) => error,
2412 };
2413 assert_eq!(error.existing_run_id(), run_id);
2414 drop(rejected);
2415
2416 assert!(!live_cancel_token.is_cancelled());
2417 assert!(matches!(
2418 runners
2419 .read()
2420 .await
2421 .get("same-run")
2422 .map(|runner| &runner.status),
2423 Some(AgentStatus::Running)
2424 ));
2425 assert!(router.owns_run("same-run", &run_id).await);
2426 live_registration.begin_finalization().await;
2427 assert_eq!(live_registration.finish(0).await.unwrap(), None);
2428 }
2429}