Skip to main content

bamboo_engine/
session_repository_parent_outcome.rs

1//! Host-only adapter for the existing direct-parent outcome CAS.
2//! Reuse an actual execution's immutable capability, or own one short control
3//! execution through the same ActorDirectory protocol while the Root is idle.
4
5use super::*;
6use bamboo_domain::{
7    ActorActivationFinish, ActorActivationStatus, RootActorExecutionBinding,
8    RuntimeSessionPersistence,
9};
10
11pub struct ParentOutcomeRoute {
12    directory: Option<std::sync::Weak<dyn bamboo_domain::ActorDirectoryPort>>,
13    writers: std::sync::Weak<
14        dashmap::DashMap<String, std::sync::Weak<bamboo_domain::RootActorRuntimeWrite>>,
15    >,
16}
17
18impl ParentOutcomeRoute {
19    /// Keep the original Host route in deadline jobs without retaining its
20    /// lifetime. Rebuilding a repository must not erase the named writer gate.
21    pub fn rebind(&self, mut repository: SessionRepository) -> Option<SessionRepository> {
22        if let Some(directory) = &self.directory {
23            repository.root_actor_directory = Some(directory.upgrade()?);
24            repository.root_actor_writers = self.writers.upgrade()?;
25        }
26        Some(repository)
27    }
28}
29
30pub struct ParentOutcomeWriter {
31    repository: SessionRepository,
32    execution: Option<RootActorExecutionBinding>,
33}
34
35impl ParentOutcomeWriter {
36    pub fn repository(&self) -> &SessionRepository {
37        &self.repository
38    }
39
40    /// Only the control execution created here is finished. A borrowed
41    /// provider execution retains its own renewal and finalization lifetime.
42    pub async fn finish(mut self) -> std::io::Result<()> {
43        let Some(mut execution) = self.execution.take() else {
44            return Ok(());
45        };
46        execution
47            .directory
48            .finish_activation(
49                &execution.owner.fence,
50                chrono::Utc::now(),
51                ActorActivationFinish::Succeeded,
52            )
53            .await
54            .map_err(root_actor_binding_error)?;
55        execution.disarm_abandonment();
56        Ok(())
57    }
58}
59
60impl SessionRepository {
61    pub fn downgrade_parent_outcome_route(&self) -> ParentOutcomeRoute {
62        ParentOutcomeRoute {
63            directory: self.root_actor_directory.as_ref().map(Arc::downgrade),
64            writers: Arc::downgrade(&self.root_actor_writers),
65        }
66    }
67    /// A named adapter for trusted Host parent-request projections. Default
68    /// saves retain their rejection boundary; request identity, lineage,
69    /// deadline and single-winner validation remain in the original CAS.
70    pub async fn bind_parent_outcome_writer(
71        &self,
72        session_id: &str,
73    ) -> std::io::Result<ParentOutcomeWriter> {
74        let session = self
75            .storage
76            .load_session(session_id)
77            .await?
78            .ok_or_else(|| root_actor_binding_error("Parent outcome target is missing"))?;
79        if let Some((_, owner)) = &self.root_actor_owner {
80            if owner.fence.actor_id != session_id || owner.created_at != session.created_at {
81                return Err(root_actor_binding_error(
82                    "Parent outcome execution target changed",
83                ));
84            }
85            return Ok(ParentOutcomeWriter {
86                repository: self.clone(),
87                execution: None,
88            });
89        }
90        if !self.root_actor_execution_required(&session) {
91            return Ok(ParentOutcomeWriter {
92                repository: self.clone(),
93                execution: None,
94            });
95        }
96        if let Some(origin) = bamboo_agent_core::tools::context::root_actor_tool_writer() {
97            let owner = origin.ok_or_else(|| {
98                root_actor_binding_error("Parent reply has no Root execution capability")
99            })?;
100            if owner.fence.actor_id != session_id || owner.created_at != session.created_at {
101                return Err(root_actor_binding_error(
102                    "Parent reply execution target changed",
103                ));
104            }
105            return Ok(ParentOutcomeWriter {
106                repository: self.bind_root_response_writer(owner)?,
107                execution: None,
108            });
109        }
110        let directory = self.root_actor_directory.as_ref().ok_or_else(|| {
111            root_actor_binding_error("Parent outcome requires its Host Actor directory")
112        })?;
113        let active_owner = self
114            .root_actor_writers
115            .get(session_id)
116            .and_then(|entry| entry.value().upgrade());
117        if let Some(owner) = active_owner {
118            let current = directory
119                .inspect_actor(session_id)
120                .await
121                .map_err(root_actor_binding_error)?;
122            if current.activation.is_some_and(|activation| {
123                activation.fence() == owner.fence
124                    && activation.status == ActorActivationStatus::Running
125                    && activation.lease_expires_at > chrono::Utc::now()
126            }) {
127                // Observation only rejects a captured capability. Final Main
128                // and cache publication recheck it under the physical guards.
129                return Ok(ParentOutcomeWriter {
130                    repository: self.bind_root_response_writer((*owner).clone())?,
131                    execution: None,
132                });
133            }
134        }
135        // A different live Host owner is rejected by claim_activation. Never
136        // borrow its fence or force replacement. Cancellation drops this exact
137        // binding; filesystem jobs retain the existing physical guards.
138        let execution = self
139            .bind_root_actor_execution(
140                &session,
141                &format!("parent-outcome-{}", uuid::Uuid::new_v4()),
142            )
143            .await?
144            .ok_or_else(|| root_actor_binding_error("Parent outcome Root binding is absent"))?;
145        let repository = self.bind_root_response_writer(execution.owner.clone())?;
146        Ok(ParentOutcomeWriter {
147            repository,
148            execution: Some(execution),
149        })
150    }
151}