Skip to main content

bamboo_engine/
session_messaging.rs

1//! Internal logical-session messaging service.
2//!
3//! This module deliberately has no HTTP concepts. User transports, tools,
4//! actor adapters, the SDK, and runtime coordinators all submit the same typed
5//! envelope addressed by stable [`Session::id`](bamboo_domain::Session).
6
7use std::collections::{HashSet, VecDeque};
8use std::sync::atomic::{AtomicU64, Ordering};
9use std::sync::Arc;
10use std::time::Instant;
11
12use bamboo_agent_core::storage::Storage;
13use bamboo_domain::{
14    ActorSession, Session, SessionActivationDisposition, SessionActivationError,
15    SessionActivationPolicy, SessionActivationPort, SessionCallerReference, SessionInboxError,
16    SessionInboxPort, SessionInboxReceipt, SessionMessageBody, SessionMessageContent,
17    SessionMessageDeliveryIntent, SessionMessageEnvelope, SessionMessageId, SessionMessageKind,
18    SessionMessageSource, SupervisorReference,
19};
20
21/// A followup is one bounded peer instruction, not a context export or file transfer.
22pub const MAX_SUPERVISOR_FOLLOWUP_BYTES: usize = 16 * 1024;
23
24pub const MAX_PEER_MESSAGE_BYTES: usize = bamboo_domain::MAX_SESSION_PEER_MESSAGE_BYTES;
25pub const MAX_PEER_TARGETS: usize = 128;
26pub const MAX_PEER_TARGET_RESULT_BYTES: usize = 32 * 1024;
27const MAX_PEER_TARGET_TITLE_BYTES: usize = 256;
28const MAX_PEER_TARGET_STATUS_BYTES: usize = 64;
29const MAX_PEER_TARGET_SCAN: usize = 1024;
30
31/// Logical discovery data only. A target observation is never delivery authority.
32#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
33pub struct SessionPeerTarget {
34    pub session_id: String,
35    pub parent_session_id: Option<String>,
36    pub kind: bamboo_domain::SessionKind,
37    pub title: String,
38    pub status: Option<String>,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
42pub struct SessionPeerTargets {
43    pub targets: Vec<SessionPeerTarget>,
44    pub truncated: bool,
45}
46
47struct PeerTreePort<'a>(&'a dyn Storage);
48
49#[async_trait::async_trait]
50impl crate::session_app::child_session::owned_tree::OwnedTreePort for PeerTreePort<'_> {
51    async fn load(
52        &self,
53        id: &str,
54    ) -> Result<Session, crate::session_app::child_session::owned_tree::OwnedTreeError> {
55        use crate::session_app::child_session::owned_tree::OwnedTreeError;
56        bamboo_domain::validate_supervisor_target_id(id)
57            .map_err(|_| OwnedTreeError::InvalidLineage)?;
58        self.0
59            .load_runtime_control_plane(id)
60            .await
61            .map_err(|_| OwnedTreeError::InvalidLineage)?
62            .ok_or(OwnedTreeError::InvalidLineage)
63    }
64
65    async fn child_ids(
66        &self,
67        parent_id: &str,
68    ) -> Result<Vec<String>, crate::session_app::child_session::owned_tree::OwnedTreeError> {
69        use crate::session_app::child_session::owned_tree::OwnedTreeError;
70        self.0
71            .list_child_run_statuses(parent_id)
72            .await
73            .map(|children| children.into_iter().map(|(id, _)| id).collect())
74            .map_err(|_| OwnedTreeError::InvalidLineage)
75    }
76}
77
78/// Observable counters for the internal delivery plane.
79#[derive(Debug, Default)]
80pub struct SessionMessagingMetrics {
81    delivered: AtomicU64,
82    rejected: AtomicU64,
83    invalid_envelope: AtomicU64,
84    unauthorized: AtomicU64,
85    payload_too_large: AtomicU64,
86    backlog_full: AtomicU64,
87    storage_failed: AtomicU64,
88    activation_failed: AtomicU64,
89    active_notified: AtomicU64,
90    activation_reserved: AtomicU64,
91    activation_coalesced: AtomicU64,
92    delivery_latency_micros: AtomicU64,
93}
94
95#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
96pub struct SessionMessagingMetricsSnapshot {
97    pub delivered: u64,
98    pub rejected: u64,
99    pub invalid_envelope: u64,
100    pub unauthorized: u64,
101    pub payload_too_large: u64,
102    pub backlog_full: u64,
103    pub storage_failed: u64,
104    pub activation_failed: u64,
105    pub active_notified: u64,
106    pub activation_reserved: u64,
107    pub activation_coalesced: u64,
108    pub delivery_latency_micros: u64,
109}
110
111impl SessionMessagingMetrics {
112    pub fn snapshot(&self) -> SessionMessagingMetricsSnapshot {
113        SessionMessagingMetricsSnapshot {
114            delivered: self.delivered.load(Ordering::Relaxed),
115            rejected: self.rejected.load(Ordering::Relaxed),
116            invalid_envelope: self.invalid_envelope.load(Ordering::Relaxed),
117            unauthorized: self.unauthorized.load(Ordering::Relaxed),
118            payload_too_large: self.payload_too_large.load(Ordering::Relaxed),
119            backlog_full: self.backlog_full.load(Ordering::Relaxed),
120            storage_failed: self.storage_failed.load(Ordering::Relaxed),
121            activation_failed: self.activation_failed.load(Ordering::Relaxed),
122            active_notified: self.active_notified.load(Ordering::Relaxed),
123            activation_reserved: self.activation_reserved.load(Ordering::Relaxed),
124            activation_coalesced: self.activation_coalesced.load(Ordering::Relaxed),
125            delivery_latency_micros: self.delivery_latency_micros.load(Ordering::Relaxed),
126        }
127    }
128}
129
130#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct SessionMessengerReceipt {
132    pub delivery: SessionInboxReceipt,
133    pub activation: SessionActivationDisposition,
134}
135
136/// A durable peer delivery receipt. No field proves context consumption,
137/// a terminal run, or an answer. None means activation was not requested.
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct SessionPeerDeliveryReceipt {
140    pub delivery: SessionInboxReceipt,
141    pub activation: Option<SessionActivationDisposition>,
142    pub active_delivery_notified: bool,
143}
144
145impl From<SessionMessengerReceipt> for SessionPeerDeliveryReceipt {
146    fn from(receipt: SessionMessengerReceipt) -> Self {
147        Self {
148            delivery: receipt.delivery,
149            activation: Some(receipt.activation),
150            active_delivery_notified: false,
151        }
152    }
153}
154
155impl SessionPeerDeliveryReceipt {
156    fn into_legacy(self) -> SessionMessengerReceipt {
157        SessionMessengerReceipt {
158            delivery: self.delivery,
159            activation: self
160                .activation
161                .expect("legacy peer delivery requests activation"),
162        }
163    }
164}
165
166/// Durable admission separated from activation so coordinators can commit
167/// related control-plane state between those phases without opening a race.
168#[derive(Debug, Clone, PartialEq, Eq)]
169pub struct SessionMessengerAdmission {
170    pub envelope_id: String,
171    pub target_session_id: String,
172    pub delivery: SessionInboxReceipt,
173}
174
175#[derive(Debug, thiserror::Error)]
176pub enum SessionMessengerError {
177    #[error("invalid session message: {0}")]
178    InvalidEnvelope(String),
179    #[error("session message source not found: {0}")]
180    SourceNotFound(String),
181    #[error("session message target not found: {0}")]
182    TargetNotFound(String),
183    #[error("session {source_session_id} is not authorized to message session {target}")]
184    Unauthorized {
185        source_session_id: String,
186        target: String,
187    },
188    #[error(transparent)]
189    Inbox(#[from] SessionInboxError),
190    /// The enqueue is already durable when this error is returned. The receipt
191    /// lets the caller distinguish it from a delivery failure and retry
192    /// activation without inventing a second message id.
193    #[error("message {receipt_id} was durably delivered but activation failed: {source}")]
194    Activation {
195        receipt_id: String,
196        receipt: SessionInboxReceipt,
197        #[source]
198        source: SessionActivationError,
199    },
200    /// Staged delivery is durable, but its explicit activation watermark did
201    /// not persist. Immediate delivery instead binds intent at publication.
202    /// Startup cannot infer permission to run from admission alone; a caller
203    /// must retry this exact envelope until the watermark is committed.
204    #[error(
205        "message {receipt_id} was delivered but activation eligibility was not persisted: {source}"
206    )]
207    ActivationEligibility {
208        receipt_id: String,
209        receipt: SessionInboxReceipt,
210        #[source]
211        source: SessionInboxError,
212    },
213    #[error("session store failure: {0}")]
214    Storage(String),
215}
216
217/// Validates logical identity, atomically enqueues, then requests one runtime
218/// activation. The service owns no process-global registry.
219pub struct SessionMessenger {
220    sessions: Arc<dyn Storage>,
221    inbox: Arc<dyn SessionInboxPort>,
222    activation: Arc<dyn SessionActivationPort>,
223    metrics: Arc<SessionMessagingMetrics>,
224}
225
226impl SessionMessenger {
227    pub fn new(
228        sessions: Arc<dyn Storage>,
229        inbox: Arc<dyn SessionInboxPort>,
230        activation: Arc<dyn SessionActivationPort>,
231    ) -> Self {
232        Self {
233            sessions,
234            inbox,
235            activation,
236            metrics: Arc::new(SessionMessagingMetrics::default()),
237        }
238    }
239
240    pub fn inbox(&self) -> &Arc<dyn SessionInboxPort> {
241        &self.inbox
242    }
243
244    pub fn activation(&self) -> &Arc<dyn SessionActivationPort> {
245        &self.activation
246    }
247
248    pub fn metrics(&self) -> &Arc<SessionMessagingMetrics> {
249        &self.metrics
250    }
251
252    /// Discover bounded logical peers in the caller's current canonical tree.
253    /// `caller_session_id` comes from the authenticated runtime context, never
254    /// from model arguments. Every candidate is revalidated before disclosure.
255    pub async fn list_peer_targets(
256        &self,
257        caller_session_id: &str,
258    ) -> Result<SessionPeerTargets, SessionMessengerError> {
259        let caller = self.peer_member(caller_session_id, true).await?;
260        let mut pending = VecDeque::from([(caller.root_session_id.clone(), None)]);
261        let mut seen = HashSet::new();
262        let mut result = SessionPeerTargets {
263            targets: Vec::new(),
264            truncated: false,
265        };
266        // Account for JSON escaping, separators, and the complete wrapper,
267        // rather than treating raw display-string lengths as an output bound.
268        let mut result_bytes = serde_json::to_vec(&result)
269            .map_err(|error| SessionMessengerError::InvalidEnvelope(error.to_string()))?
270            .len();
271        while let Some((id, indexed_parent)) = pending.pop_front() {
272            if !seen.insert(id.clone()) {
273                continue;
274            }
275            let current = match self.peer_member(&id, false).await {
276                Ok(current)
277                    if current.root_session_id == caller.root_session_id
278                        && current.project_id_meta() == caller.project_id_meta()
279                        && current.parent_session_id == indexed_parent =>
280                {
281                    current
282                }
283                Ok(_)
284                | Err(SessionMessengerError::TargetNotFound(_))
285                | Err(SessionMessengerError::Unauthorized { .. })
286                | Err(SessionMessengerError::InvalidEnvelope(_)) => continue,
287                Err(error) => return Err(error),
288            };
289            if id != caller_session_id {
290                if result.targets.len() == MAX_PEER_TARGETS {
291                    result.truncated = true;
292                    break;
293                }
294                let target = SessionPeerTarget {
295                    session_id: current.id.clone(),
296                    parent_session_id: current.parent_session_id.clone(),
297                    kind: current.kind,
298                    title: Self::bounded_peer_label(&current.title, MAX_PEER_TARGET_TITLE_BYTES),
299                    status: current.last_run_status().map(|status| {
300                        Self::bounded_peer_label(&status, MAX_PEER_TARGET_STATUS_BYTES)
301                    }),
302                };
303                let target_bytes = serde_json::to_vec(&target)
304                    .map_err(|error| SessionMessengerError::InvalidEnvelope(error.to_string()))?
305                    .len()
306                    + usize::from(!result.targets.is_empty());
307                if result_bytes.saturating_add(target_bytes) > MAX_PEER_TARGET_RESULT_BYTES {
308                    result.truncated = true;
309                    break;
310                }
311                result_bytes += target_bytes;
312                result.targets.push(target);
313            }
314            let mut children = self
315                .sessions
316                .list_child_run_statuses(&id)
317                .await
318                .map_err(|error| SessionMessengerError::Storage(error.to_string()))?;
319            children.sort_by(|left, right| left.0.cmp(&right.0));
320            let budget = MAX_PEER_TARGET_SCAN.saturating_sub(seen.len() + pending.len());
321            result.truncated |= children.len() > budget;
322            pending.extend(
323                children
324                    .into_iter()
325                    .take(budget)
326                    .map(|(child, _)| (child, Some(id.clone()))),
327            );
328        }
329        Ok(result)
330    }
331
332    /// Send an asynchronous peer instruction. Source identity and association
333    /// fields are constructed by the runtime; callers provide no envelope.
334    /// Reuse the same operation id and message after an uncertain result.
335    pub async fn send_peer(
336        &self,
337        caller_session_id: &str,
338        target_session_id: &str,
339        operation_id: &str,
340        message: &str,
341    ) -> Result<SessionMessengerReceipt, SessionMessengerError> {
342        self.send_peer_delivery(
343            caller_session_id,
344            target_session_id,
345            operation_id,
346            message,
347            None,
348            None,
349        )
350        .await
351        .map(SessionPeerDeliveryReceipt::into_legacy)
352    }
353
354    /// Explicit scheduling over the same lifetime-fenced Inbox. Omission on
355    /// the old send_peer API keeps legacy interrupt/activation behavior.
356    pub async fn send_peer_with_intent(
357        &self,
358        caller_session_id: &str,
359        target_session_id: &str,
360        operation_id: &str,
361        message: &str,
362        intent: SessionMessageDeliveryIntent,
363    ) -> Result<SessionPeerDeliveryReceipt, SessionMessengerError> {
364        self.send_peer_delivery(
365            caller_session_id,
366            target_session_id,
367            operation_id,
368            message,
369            Some(intent),
370            None,
371        )
372        .await
373    }
374
375    /// Canonical compatibility send from the original executing caller. The
376    /// final Inbox retains a never-installed Host namespace plus the existing
377    /// peer lifecycle/Project authority. An installed/revoked scope refuses
378    /// this fallback; use the explicitly scoped sender for that namespace.
379    pub async fn send_peer_observed(
380        &self,
381        caller: &SessionCallerReference,
382        target_session_id: &str,
383        operation_id: &str,
384        message: &str,
385        intent: Option<SessionMessageDeliveryIntent>,
386    ) -> Result<SessionPeerDeliveryReceipt, SessionMessengerError> {
387        self.send_peer_delivery(
388            &caller.session_id,
389            target_session_id,
390            operation_id,
391            message,
392            intent,
393            Some(caller),
394        )
395        .await
396    }
397
398    /// One explicitly installed SendPeer grant, including independent Roots.
399    /// No inspect right or public target read is needed to build this bounded
400    /// envelope. The Inbox validates the original caller and token at its final
401    /// retained authority boundary; these binding bytes are observations only.
402    pub async fn send_host_scoped_peer(
403        &self,
404        token: &bamboo_domain::HostSessionScopeToken,
405        caller: &SessionCallerReference,
406        target_session_id: &str,
407        operation_id: &str,
408        message: &str,
409        intent: SessionMessageDeliveryIntent,
410    ) -> Result<SessionPeerDeliveryReceipt, SessionMessengerError> {
411        Self::validate_peer_input(operation_id, message)?;
412        Self::validate_peer_session_id(target_session_id)?;
413        let unauthorized = || SessionMessengerError::Unauthorized {
414            source_session_id: caller.session_id.clone(),
415            target: target_session_id.into(),
416        };
417        if !token.binding().caller.identity.matches_caller(caller) {
418            return Err(unauthorized());
419        }
420        let target = &token
421            .binding()
422            .target(target_session_id, bamboo_domain::HostSessionRight::SendPeer)
423            .map_err(|_| unauthorized())?
424            .observation
425            .identity;
426        let identity = bamboo_domain::HostSessionPeerIdentity::for_caller(
427            caller,
428            &target.session_id,
429            target.created_at,
430            operation_id,
431        );
432        let envelope = Self::peer_envelope_for_ids(
433            &caller.session_id,
434            &target.session_id,
435            operation_id,
436            message,
437            identity.message_id,
438            identity.thread_id,
439            None,
440        );
441        let started = Instant::now();
442        let delivery = self
443            .inbox
444            .deliver_host_scoped_peer_message(token, caller, &envelope, intent)
445            .await
446            .map_err(|error| {
447                let error = SessionMessengerError::Inbox(error);
448                self.record_rejection(&error);
449                error
450            })?;
451        self.finish_peer_delivery(envelope, delivery, started, Some(intent))
452            .await
453    }
454
455    /// Admit the first input for one explicitly authorized Root creation. This
456    /// is not the SendPeer port: no target grant or relationship is inferred.
457    /// A known durable receipt survives a later activation failure unchanged.
458    pub async fn send_host_root_first_input(
459        &self,
460        token: &bamboo_domain::HostSessionScopeToken,
461        caller: &SessionCallerReference,
462        operation_id: uuid::Uuid,
463        reserved_root: &Session,
464        message: &str,
465    ) -> Result<SessionPeerDeliveryReceipt, SessionMessengerError> {
466        let envelope =
467            Self::root_first_input_envelope(caller, operation_id, reserved_root, message)?;
468        let started = Instant::now();
469        let delivery = self
470            .inbox
471            .deliver_host_root_first_input(token, caller, operation_id, reserved_root, &envelope)
472            .await
473            .map_err(|error| {
474                let error = SessionMessengerError::Inbox(error);
475                self.record_rejection(&error);
476                error
477            })?;
478        self.finish_peer_delivery(
479            envelope,
480            delivery,
481            started,
482            Some(SessionMessageDeliveryIntent::TriggerTurn),
483        )
484        .await
485    }
486
487    /// Canonical generic-caller Supervisor creation keeps its old wire but
488    /// uses current pending state, unlike the independent legacy followup API.
489    pub async fn supervisor_root_first_input_observed(
490        &self,
491        caller: &SessionCallerReference,
492        supervisor: &SupervisorReference,
493        operation_id: uuid::Uuid,
494        reserved_root: &Session,
495        message: &str,
496    ) -> Result<SessionPeerDeliveryReceipt, SessionMessengerError> {
497        let envelope =
498            Self::root_first_input_envelope(caller, operation_id, reserved_root, message)?;
499        let started = Instant::now();
500        let delivery = self
501            .inbox
502            .deliver_unscoped_supervisor_root_first_input(
503                caller,
504                supervisor,
505                operation_id,
506                reserved_root,
507                &envelope,
508            )
509            .await
510            .map_err(|error| {
511                let error = SessionMessengerError::Inbox(error);
512                self.record_rejection(&error);
513                error
514            })?;
515        self.finish_peer_delivery(
516            envelope,
517            delivery,
518            started,
519            Some(SessionMessageDeliveryIntent::TriggerTurn),
520        )
521        .await
522    }
523
524    fn root_first_input_envelope(
525        caller: &SessionCallerReference,
526        operation_id: uuid::Uuid,
527        reserved_root: &Session,
528        message: &str,
529    ) -> Result<SessionMessageEnvelope, SessionMessengerError> {
530        let operation = operation_id.to_string();
531        Self::validate_peer_input(&operation, message)?;
532        let (id, thread) =
533            bamboo_domain::host_root_first_input_identity(caller, operation_id, reserved_root)
534                .map_err(|error| SessionMessengerError::InvalidEnvelope(error.into()))?;
535        Ok(Self::peer_envelope_for_ids(
536            &caller.session_id,
537            &reserved_root.id,
538            &operation,
539            message,
540            id,
541            thread,
542            None,
543        ))
544    }
545
546    /// Active-run-only peer input. The Inbox owns exact retry recovery and
547    /// retains its original authority through the runtime's publication CAS.
548    /// This never cancels a run, requests activation or reconstructs an owner.
549    pub async fn steer_peer(
550        &self,
551        caller_session_id: &str,
552        target_session_id: &str,
553        operation_id: &str,
554        message: &str,
555        expected_run_id: &str,
556    ) -> Result<bamboo_domain::SessionInboxSteerReceipt, SessionMessengerError> {
557        Self::validate_peer_input(operation_id, message)?;
558        if !bamboo_domain::valid_session_run_id(expected_run_id) {
559            return Err(SessionMessengerError::InvalidEnvelope(
560                "invalid expected run id".into(),
561            ));
562        }
563        let (caller, target) = self.peer_pair(caller_session_id, target_session_id).await?;
564        let id = Self::peer_message_id(&caller, &target, operation_id);
565        let envelope = Self::peer_envelope(
566            &caller,
567            &target,
568            operation_id,
569            message,
570            id.clone(),
571            id.to_string(),
572            None,
573        );
574        let source = ActorSession::from_session(&caller)
575            .map_err(|error| SessionMessengerError::InvalidEnvelope(error.to_string()))?;
576        let target = ActorSession::from_session(&target)
577            .map_err(|error| SessionMessengerError::InvalidEnvelope(error.to_string()))?;
578        let started = Instant::now();
579        let receipt = self
580            .inbox
581            .deliver_peer_steer(
582                &envelope,
583                &source,
584                &target,
585                expected_run_id,
586                self.activation.clone(),
587            )
588            .await
589            .map_err(|error| {
590                let error = SessionMessengerError::Inbox(error);
591                self.record_rejection(&error);
592                error
593            })?;
594        self.record_admission(envelope, receipt.delivery.clone(), started);
595        Ok(receipt)
596    }
597
598    async fn send_peer_delivery(
599        &self,
600        caller_session_id: &str,
601        target_session_id: &str,
602        operation_id: &str,
603        message: &str,
604        intent: Option<SessionMessageDeliveryIntent>,
605        original_caller: Option<&SessionCallerReference>,
606    ) -> Result<SessionPeerDeliveryReceipt, SessionMessengerError> {
607        Self::validate_peer_input(operation_id, message)?;
608        let (caller, target) = self.peer_pair(caller_session_id, target_session_id).await?;
609        if original_caller.is_some_and(|original| !original.matches_session(&caller)) {
610            return Err(SessionMessengerError::Unauthorized {
611                source_session_id: caller_session_id.into(),
612                target: target_session_id.into(),
613            });
614        }
615        let id = Self::peer_message_id(&caller, &target, operation_id);
616        let envelope = Self::peer_envelope(
617            &caller,
618            &target,
619            operation_id,
620            message,
621            id.clone(),
622            id.to_string(),
623            None,
624        );
625        self.send_peer_envelope(envelope, &caller, &target, None, intent, original_caller)
626            .await
627    }
628
629    /// Reply only to a canonical received peer envelope in the caller's saved
630    /// transcript. Recipient and thread edges are never supplied by the model.
631    pub async fn reply_peer(
632        &self,
633        caller_session_id: &str,
634        received_message_id: &str,
635        operation_id: &str,
636        message: &str,
637    ) -> Result<SessionMessengerReceipt, SessionMessengerError> {
638        self.reply_peer_delivery(
639            caller_session_id,
640            received_message_id,
641            operation_id,
642            message,
643            None,
644        )
645        .await
646        .map(SessionPeerDeliveryReceipt::into_legacy)
647    }
648
649    /// Derive recipient/thread from the canonical received peer message while
650    /// opting into explicit scheduling. Reply observations never grant authority.
651    pub async fn reply_peer_with_intent(
652        &self,
653        caller_session_id: &str,
654        received_message_id: &str,
655        operation_id: &str,
656        message: &str,
657        intent: SessionMessageDeliveryIntent,
658    ) -> Result<SessionPeerDeliveryReceipt, SessionMessengerError> {
659        self.reply_peer_delivery(
660            caller_session_id,
661            received_message_id,
662            operation_id,
663            message,
664            Some(intent),
665        )
666        .await
667    }
668
669    async fn reply_peer_delivery(
670        &self,
671        caller_session_id: &str,
672        received_message_id: &str,
673        operation_id: &str,
674        message: &str,
675        intent: Option<SessionMessageDeliveryIntent>,
676    ) -> Result<SessionPeerDeliveryReceipt, SessionMessengerError> {
677        Self::validate_peer_input(operation_id, message)?;
678        Self::validate_peer_session_id(caller_session_id)?;
679        let received_id = SessionMessageId::parse(received_message_id)
680            .map_err(|error| SessionMessengerError::InvalidEnvelope(error.to_string()))?;
681        let received = self
682            .load_session(caller_session_id)
683            .await?
684            .ok_or_else(|| SessionMessengerError::SourceNotFound(caller_session_id.into()))?;
685        let original = received
686            .messages
687            .iter()
688            .filter(|message| message.id == received_message_id)
689            .find_map(|message| {
690                let envelope: SessionMessageEnvelope = serde_json::from_value(
691                    message.metadata.as_ref()?.get("session_message")?.clone(),
692                )
693                .ok()?;
694                (envelope.id == received_id
695                    && envelope.kind == SessionMessageKind::PeerMessage
696                    && envelope.target_session_id == caller_session_id
697                    && bamboo_domain::is_matching_session_message(message, &envelope))
698                .then_some(envelope)
699            })
700            .ok_or_else(|| {
701                SessionMessengerError::InvalidEnvelope(
702                    "reply requires a canonical received peer message".into(),
703                )
704            })?;
705        let SessionMessageSource::Session {
706            session_id: target_id,
707        } = &original.source
708        else {
709            return Err(SessionMessengerError::InvalidEnvelope(
710                "reply has no logical sender".into(),
711            ));
712        };
713        let (caller, target) = self.peer_pair(caller_session_id, target_id).await?;
714        let thread = original.thread_id.clone().ok_or_else(|| {
715            SessionMessengerError::InvalidEnvelope("reply requires a threaded peer message".into())
716        })?;
717        let original_operation = original.correlation_id.as_deref().ok_or_else(|| {
718            SessionMessengerError::InvalidEnvelope(
719                "reply requires a peer operation identity".into(),
720            )
721        })?;
722        if original.id != Self::peer_message_id(&target, &caller, original_operation) {
723            return Err(SessionMessengerError::InvalidEnvelope(
724                "received peer message belongs to a different session lifetime".into(),
725            ));
726        }
727        SessionMessageId::parse(&thread)
728            .map_err(|error| SessionMessengerError::InvalidEnvelope(error.to_string()))?;
729        let envelope = Self::peer_envelope(
730            &caller,
731            &target,
732            operation_id,
733            message,
734            Self::peer_message_id(&caller, &target, operation_id),
735            thread,
736            Some(original.id.clone()),
737        );
738        self.send_peer_envelope(envelope, &caller, &target, Some(&original), intent, None)
739            .await
740    }
741
742    fn validate_peer_input(operation_id: &str, message: &str) -> Result<(), SessionMessengerError> {
743        SessionMessageId::parse(operation_id)
744            .map_err(|error| SessionMessengerError::InvalidEnvelope(error.to_string()))?;
745        if message.trim().is_empty()
746            || message.len() > MAX_PEER_MESSAGE_BYTES
747            || message.contains('\0')
748        {
749            return Err(SessionMessengerError::InvalidEnvelope(format!(
750                "peer message must contain 1..={MAX_PEER_MESSAGE_BYTES} UTF-8 bytes without NUL"
751            )));
752        }
753        Ok(())
754    }
755
756    fn validate_peer_session_id(id: &str) -> Result<(), SessionMessengerError> {
757        bamboo_domain::validate_supervisor_target_id(id)
758            .map_err(|error| SessionMessengerError::InvalidEnvelope(error.into()))?;
759        SessionMessageId::parse(id)
760            .map(|_| ())
761            .map_err(|error| SessionMessengerError::InvalidEnvelope(error.to_string()))
762    }
763
764    fn bounded_peer_label(value: &str, max_bytes: usize) -> String {
765        let mut end = value.len().min(max_bytes);
766        while !value.is_char_boundary(end) {
767            end -= 1;
768        }
769        value[..end].to_owned()
770    }
771
772    async fn peer_member(&self, id: &str, source: bool) -> Result<Session, SessionMessengerError> {
773        Self::validate_peer_session_id(id)?;
774        let session = self
775            .sessions
776            .load_runtime_control_plane(id)
777            .await
778            .map_err(|error| SessionMessengerError::Storage(error.to_string()))?
779            .ok_or_else(|| {
780                if source {
781                    SessionMessengerError::SourceNotFound(id.into())
782                } else {
783                    SessionMessengerError::TargetNotFound(id.into())
784                }
785            })?;
786        let unauthorized = || SessionMessengerError::Unauthorized {
787            source_session_id: id.into(),
788            target: session.root_session_id.clone(),
789        };
790        if session.id != id
791            || session
792                .runtime_metadata
793                .as_ref()
794                .and_then(|meta| meta.project_id.as_ref())
795                .zip(session.metadata.get("project_id"))
796                .is_some_and(|(typed, legacy)| typed != legacy)
797            || bamboo_domain::ActorSession::from_session(&session).is_err()
798        {
799            return Err(unauthorized());
800        }
801        if session.kind == bamboo_domain::SessionKind::Root {
802            return Ok(session);
803        }
804        crate::session_app::child_session::owned_tree::load_owned_descendant(
805            &PeerTreePort(self.sessions.as_ref()),
806            &session.root_session_id,
807            id,
808        )
809        .await
810        .map_err(|_| unauthorized())
811    }
812
813    async fn peer_pair(
814        &self,
815        caller_id: &str,
816        target_id: &str,
817    ) -> Result<(Session, Session), SessionMessengerError> {
818        let caller = self.peer_member(caller_id, true).await?;
819        let target = self.peer_member(target_id, false).await?;
820        if caller_id == target_id
821            || caller.root_session_id != target.root_session_id
822            || caller.project_id_meta() != target.project_id_meta()
823        {
824            return Err(SessionMessengerError::Unauthorized {
825                source_session_id: caller_id.into(),
826                target: target_id.into(),
827            });
828        }
829        Ok((caller, target))
830    }
831
832    fn peer_message_id(caller: &Session, target: &Session, operation_id: &str) -> SessionMessageId {
833        bamboo_domain::session_peer_message_id(
834            &caller.id,
835            caller.created_at,
836            &target.id,
837            target.created_at,
838            operation_id,
839        )
840    }
841
842    #[allow(clippy::too_many_arguments)]
843    fn peer_envelope(
844        caller: &Session,
845        target: &Session,
846        operation_id: &str,
847        message: &str,
848        id: SessionMessageId,
849        thread_id: String,
850        in_reply_to: Option<SessionMessageId>,
851    ) -> SessionMessageEnvelope {
852        Self::peer_envelope_for_ids(
853            &caller.id,
854            &target.id,
855            operation_id,
856            message,
857            id,
858            Some(thread_id),
859            in_reply_to,
860        )
861    }
862
863    #[allow(clippy::too_many_arguments)]
864    fn peer_envelope_for_ids(
865        caller_id: &str,
866        target_id: &str,
867        operation_id: &str,
868        message: &str,
869        id: SessionMessageId,
870        thread_id: Option<String>,
871        in_reply_to: Option<SessionMessageId>,
872    ) -> SessionMessageEnvelope {
873        SessionMessageEnvelope {
874            id,
875            source: SessionMessageSource::Session {
876                session_id: caller_id.into(),
877            },
878            target_session_id: target_id.into(),
879            kind: SessionMessageKind::PeerMessage,
880            body: SessionMessageBody::Content(SessionMessageContent::text(message)),
881            created_at: chrono::Utc::now(),
882            thread_id,
883            in_reply_to,
884            attempt: None,
885            correlation_id: Some(operation_id.into()),
886        }
887    }
888
889    async fn send_peer_envelope(
890        &self,
891        envelope: SessionMessageEnvelope,
892        caller: &Session,
893        target: &Session,
894        reply_to: Option<&SessionMessageEnvelope>,
895        intent: Option<SessionMessageDeliveryIntent>,
896        original_caller: Option<&SessionCallerReference>,
897    ) -> Result<SessionPeerDeliveryReceipt, SessionMessengerError> {
898        let started = Instant::now();
899        let source = ActorSession::from_session(caller)
900            .map_err(|error| SessionMessengerError::InvalidEnvelope(error.to_string()))?;
901        let target = ActorSession::from_session(target)
902            .map_err(|error| SessionMessengerError::InvalidEnvelope(error.to_string()))?;
903        // These are the same births used for the stable message id. The inbox
904        // must revalidate them under its retained lifecycle authority; a fresh
905        // relationship read before generic delivery would still race rebirth.
906        let result = if let Some(original) = original_caller {
907            self.inbox
908                .deliver_unscoped_peer_message(original, &envelope, &source, &target, intent)
909                .await
910        } else {
911            match intent {
912                Some(intent) => {
913                    self.inbox
914                        .deliver_peer_message_with_intent(
915                            &envelope, &source, &target, reply_to, intent,
916                        )
917                        .await
918                }
919                None => {
920                    self.inbox
921                        .deliver_peer_message(&envelope, &source, &target, reply_to)
922                        .await
923                        .map(|delivery| bamboo_domain::SessionInboxDeliveryReceipt {
924                            delivery,
925                            // Preserve the historical peer activation contract.
926                            pending: true,
927                        })
928                }
929            }
930        };
931        let delivery = match result {
932            Ok(receipt) => receipt,
933            Err(error) => {
934                let error = SessionMessengerError::Inbox(error);
935                self.record_rejection(&error);
936                return Err(error);
937            }
938        };
939        self.finish_peer_delivery(envelope, delivery, started, intent)
940            .await
941    }
942
943    async fn finish_peer_delivery(
944        &self,
945        envelope: SessionMessageEnvelope,
946        delivery: bamboo_domain::SessionInboxDeliveryReceipt,
947        started: Instant,
948        intent: Option<SessionMessageDeliveryIntent>,
949    ) -> Result<SessionPeerDeliveryReceipt, SessionMessengerError> {
950        let pending = delivery.pending;
951        let admission = self.record_admission(envelope, delivery.delivery, started);
952        // Exact explicit retries of ACKed/cancelled/dead deliveries cannot
953        // revive input or request work from a fresh router. The Inbox observed
954        // this state while retaining the original admission authority/locks.
955        if !pending {
956            return Ok(SessionPeerDeliveryReceipt {
957                delivery: admission.delivery,
958                activation: None,
959                active_delivery_notified: false,
960            });
961        }
962        if intent == Some(SessionMessageDeliveryIntent::QueueOnly) {
963            let active_delivery_notified = self
964                .activation
965                .notify_active_delivery(&admission.target_session_id, admission.delivery.generation)
966                .await;
967            return Ok(SessionPeerDeliveryReceipt {
968                delivery: admission.delivery,
969                activation: None,
970                active_delivery_notified,
971            });
972        }
973        self.activate_prepared(&admission).await.map(Into::into)
974    }
975
976    /// Original executing Supervisor control over one separately host-granted
977    /// live Root. Enqueue/notification is never a configuration application
978    /// receipt. Reads use a fresh canonical target snapshot under both fences.
979    pub async fn supervisor_loop_control(
980        &self,
981        supervisor: &SupervisorReference,
982        target_session_id: &str,
983        expected_link_revision: Option<u64>,
984        command: &bamboo_domain::LoopControlRequest,
985        receipt_operation_id: Option<&str>,
986    ) -> Result<serde_json::Value, SessionMessengerError> {
987        use bamboo_domain::{LoopControlAction, LoopControlRequest, ScopedLoopControlInput};
988        let invalid = |message: String| SessionMessengerError::InvalidEnvelope(message);
989        bamboo_domain::validate_supervisor_target_id(target_session_id)
990            .map_err(|error| invalid(error.into()))?;
991        bamboo_domain::validate_scoped_loop_control_command(command).map_err(invalid)?;
992        let status = matches!(command, LoopControlRequest::Status);
993        if expected_link_revision == Some(0)
994            || (!status && expected_link_revision.is_none())
995            || (!status && receipt_operation_id.is_some())
996        {
997            return Err(invalid("loop mutations require the exact nonzero link revision; receipt lookup is status-only".into()));
998        }
999        if let Some(operation) = receipt_operation_id {
1000            uuid::Uuid::parse_str(operation)
1001                .map_err(|_| invalid("receipt operation must be a UUID".into()))?;
1002        }
1003        let binding = self
1004            .activation
1005            .observe_supervisor_loop(
1006                supervisor,
1007                target_session_id,
1008                expected_link_revision,
1009                LoopControlAction::of(command),
1010            )
1011            .await?;
1012        binding.authorize(command).map_err(invalid)?;
1013        let canonical = self
1014            .sessions
1015            .load_scoped_loop_session(&binding, self.activation.clone())
1016            .await
1017            .map_err(|error| SessionMessengerError::Storage(error.to_string()))?
1018            .ok_or_else(|| SessionMessengerError::TargetNotFound(target_session_id.into()))?;
1019        if status {
1020            let mut snapshot = serde_json::to_value(
1021                crate::runtime::loop_control::inspect_loop(&canonical)
1022                    .map_err(|error| SessionMessengerError::Storage(error.to_string()))?,
1023            )
1024            .map_err(|error| SessionMessengerError::Storage(error.to_string()))?;
1025            // Bounded readback belongs to the initiating Supervisor's output
1026            // budget. The target's canonical objective bytes remain unchanged.
1027            for field in ["objective", "pending_objective"] {
1028                let text = snapshot[field].as_str().map(str::to_owned);
1029                if let Some(text) = text {
1030                    let mut end = text.len().min(1024);
1031                    while !text.is_char_boundary(end) {
1032                        end -= 1;
1033                    }
1034                    snapshot[field] = serde_json::Value::String(text[..end].into());
1035                    snapshot[format!("{field}_truncated")] =
1036                        serde_json::Value::Bool(end < text.len());
1037                }
1038            }
1039            let records = bamboo_domain::scoped_loop_consumptions(&canonical)
1040                .map_err(SessionMessengerError::Storage)?;
1041            let partial_checkpoint = records.iter().any(|record| {
1042                record.binding == binding
1043                    && receipt_operation_id.is_none_or(|id| id == record.operation_id)
1044                    && !canonical
1045                        .session_inbox_admission()
1046                        .is_some_and(|cursor| cursor.contains(&record.input_id))
1047            });
1048            let record = records.into_iter().rev().find(|record| {
1049                record.binding == binding
1050                    && receipt_operation_id.is_none_or(|id| id == record.operation_id)
1051                    && canonical
1052                        .session_inbox_admission()
1053                        .is_some_and(|cursor| cursor.contains(&record.input_id))
1054            });
1055            let state = crate::runtime::counted_loop::read_loop_state(&canonical)
1056                .map_err(|error| SessionMessengerError::Storage(error.to_string()))?;
1057            let objective_applied_actual = record
1058                .as_ref()
1059                .filter(|record| record.configured)
1060                .and_then(|record| record.receipt.as_ref())
1061                .zip(state.as_ref())
1062                .is_some_and(|(receipt, state)| {
1063                    receipt["generation"].as_str() == Some(state.generation.as_str())
1064                        && canonical.messages.iter().any(|message| {
1065                            message.role == bamboo_domain::Role::User
1066                                && message.metadata.as_ref().is_some_and(|metadata| {
1067                                    metadata["runtime_kind"].as_str()
1068                                        == Some("counted_loop_iteration")
1069                                        && metadata["loop_generation"] == receipt["generation"]
1070                                        && metadata["objective_revision"]
1071                                            == receipt["objective_revision"]
1072                                })
1073                        })
1074                        && state.control.as_ref().is_some_and(|control| {
1075                            receipt["accepted_revision"]
1076                                .as_u64()
1077                                .is_some_and(|revision| control.applied_revision >= revision)
1078                                && receipt["objective_revision"]
1079                                    .as_u64()
1080                                    .is_some_and(|revision| {
1081                                        revision > 0
1082                                            && control.iteration_objective_revision >= revision
1083                                    })
1084                        })
1085                });
1086            return Ok(serde_json::json!({
1087                "action":"loop", "command":"status", "target_session_id":target_session_id,
1088                "link_revision":binding.link_revision, "owner_run_id":binding.owner_run_id,
1089                "execution_id":binding.execution_id, "loop":snapshot,
1090                "host_max_iterations":binding.max_iterations, "host_max_total_rounds":binding.max_total_rounds,
1091                "configured":record.as_ref().is_some_and(|record|record.configured),
1092                "checkpoint_observation":if partial_checkpoint { "partial_or_unknown" } else if record.is_some() { "confirmed" } else { "not_observed" },
1093                "operation_receipt":record, "objective_applied_actual":objective_applied_actual,
1094                "note":"Configuration receipts are immutable target checkpoints; objective_applied_actual additionally requires its real iteration-admission checkpoint. Neither is a task result."
1095            }));
1096        }
1097        let input = ScopedLoopControlInput {
1098            binding,
1099            command: command.clone(),
1100        };
1101        let envelope = input.envelope().map_err(invalid)?;
1102        if let Some(record) = bamboo_domain::scoped_loop_consumptions(&canonical)
1103            .map_err(SessionMessengerError::Storage)?
1104            .into_iter()
1105            .find(|record| record.input_id == envelope.id)
1106        {
1107            if !bamboo_domain::has_scoped_loop_consumption(&canonical, &envelope) {
1108                return Err(invalid(
1109                    "operation payload conflicts with its immutable target receipt".into(),
1110                ));
1111            }
1112            if canonical
1113                .session_inbox_admission()
1114                .is_some_and(|cursor| cursor.contains(&record.input_id))
1115            {
1116                return Ok(serde_json::json!({
1117                    "action":"loop", "target_session_id":target_session_id,
1118                    "operation_id":input.operation_id(), "receipt_id":envelope.id,
1119                    "admitted":true, "replayed":true, "configured":record.configured,
1120                    "operation_receipt":record, "objective_applied_actual":false,
1121                    "note":"The original configuration receipt is replayed; use status for current objective application."
1122                }));
1123            }
1124            // A runtime sidecar without Main's typed cursor is a partial
1125            // publication, not a confirmed application. Exact delivery below
1126            // recovers its original queued receipt without claiming success.
1127        }
1128        let started = Instant::now();
1129        let receipt = self
1130            .inbox
1131            .deliver_supervisor_loop_control(&input.binding, &envelope, self.activation.clone())
1132            .await?;
1133        self.record_admission(envelope, receipt.delivery.clone(), started);
1134        Ok(serde_json::json!({
1135            "action":"loop", "target_session_id":target_session_id,
1136            "operation_id":input.operation_id(), "receipt_id":receipt.delivery.id,
1137            "generation":receipt.delivery.generation, "admitted":true,
1138            "replayed":receipt.replayed, "active_delivery_notified":receipt.active_delivery_notified,
1139            "configured":false, "target_application_observed":false,
1140            "objective_applied_actual":false, "operation_receipt":null,
1141            "note":"Transport admission is historical on replay; the input may no longer be pending. Target application remains unobserved here; query status with receipt_operation_id. This input never starts a successor or answers a wait."
1142        }))
1143    }
1144
1145    /// Current-user management always retains the immutable selected target
1146    /// and full original caller through the existing physical Inbox writer.
1147    pub async fn supervisor_current_user_followup(
1148        &self,
1149        caller: &SessionCallerReference,
1150        supervisor: &SupervisorReference,
1151        target: &bamboo_domain::SupervisorCurrentUserTarget,
1152        operation_id: &str,
1153        message: &str,
1154    ) -> Result<SessionMessengerReceipt, SessionMessengerError> {
1155        self.supervisor_followup_with_material_notifications(
1156            caller,
1157            supervisor,
1158            target,
1159            operation_id,
1160            message,
1161            false,
1162        )
1163        .await
1164    }
1165
1166    pub async fn supervisor_followup_with_material_notifications(
1167        &self,
1168        caller: &SessionCallerReference,
1169        supervisor: &SupervisorReference,
1170        target: &bamboo_domain::SupervisorCurrentUserTarget,
1171        operation_id: &str,
1172        message: &str,
1173        notify_on_material_event: bool,
1174    ) -> Result<SessionMessengerReceipt, SessionMessengerError> {
1175        Self::validate_peer_input(operation_id, message)?;
1176        let envelope = SessionMessageEnvelope {
1177            id: bamboo_domain::supervisor_current_user_message_id(supervisor, target, operation_id),
1178            source: SessionMessageSource::Session {
1179                session_id: caller.session_id.clone(),
1180            },
1181            target_session_id: target.session_id.clone(),
1182            kind: SessionMessageKind::PeerMessage,
1183            body: SessionMessageBody::Content(SessionMessageContent::text(message)),
1184            created_at: chrono::Utc::now(),
1185            thread_id: None,
1186            in_reply_to: None,
1187            attempt: None,
1188            correlation_id: Some(operation_id.to_string()),
1189        };
1190        let started = Instant::now();
1191        let delivery = self
1192            .inbox
1193            .deliver_supervisor_followup_with_material_notifications(
1194                caller,
1195                supervisor,
1196                target,
1197                &envelope,
1198                notify_on_material_event,
1199            )
1200            .await
1201            .map_err(SessionMessengerError::Inbox)?;
1202        let admission = self.record_admission(envelope, delivery, started);
1203        self.activate_prepared(&admission).await
1204    }
1205
1206    /// Follow an originally observed Session from the canonical Supervisor.
1207    /// Retain its birth across retries. The concrete Inbox revalidates all
1208    /// authority under physical locks; a fresh ID lookup cannot replace it.
1209    pub async fn supervisor_followup(
1210        &self,
1211        supervisor: &SupervisorReference,
1212        target: &Session,
1213        operation_id: &str,
1214        message: &str,
1215    ) -> Result<SessionMessengerReceipt, SessionMessengerError> {
1216        self.supervisor_followup_delivery(
1217            supervisor,
1218            &target.id,
1219            target.created_at,
1220            operation_id,
1221            message,
1222            None,
1223        )
1224        .await
1225    }
1226
1227    /// Canonical sender fallback only. Its Inbox holds a real never-installed
1228    /// namespace guard and the complete original caller through admission.
1229    pub async fn supervisor_followup_observed(
1230        &self,
1231        caller: &SessionCallerReference,
1232        supervisor: &SupervisorReference,
1233        target: &Session,
1234        operation_id: &str,
1235        message: &str,
1236    ) -> Result<SessionMessengerReceipt, SessionMessengerError> {
1237        self.supervisor_followup_delivery(
1238            supervisor,
1239            &target.id,
1240            target.created_at,
1241            operation_id,
1242            message,
1243            Some(caller),
1244        )
1245        .await
1246    }
1247
1248    async fn supervisor_followup_delivery(
1249        &self,
1250        supervisor: &SupervisorReference,
1251        target_session_id: &str,
1252        target_created_at: chrono::DateTime<chrono::Utc>,
1253        operation_id: &str,
1254        message: &str,
1255        original_caller: Option<&SessionCallerReference>,
1256    ) -> Result<SessionMessengerReceipt, SessionMessengerError> {
1257        bamboo_domain::validate_supervisor_target_id(target_session_id)
1258            .map_err(|error| SessionMessengerError::InvalidEnvelope(error.to_string()))?;
1259        SessionMessageId::parse(operation_id)
1260            .map_err(|error| SessionMessengerError::InvalidEnvelope(error.to_string()))?;
1261        if message.trim().is_empty() || message.len() > MAX_SUPERVISOR_FOLLOWUP_BYTES {
1262            return Err(SessionMessengerError::InvalidEnvelope(format!(
1263                "followup message must contain 1..={MAX_SUPERVISOR_FOLLOWUP_BYTES} UTF-8 bytes"
1264            )));
1265        }
1266        let envelope = SessionMessageEnvelope {
1267            id: bamboo_domain::supervisor_followup_message_id(
1268                supervisor,
1269                target_session_id,
1270                operation_id,
1271            ),
1272            source: SessionMessageSource::Session {
1273                session_id: supervisor.session_id.clone(),
1274            },
1275            target_session_id: target_session_id.to_string(),
1276            kind: SessionMessageKind::PeerMessage,
1277            body: SessionMessageBody::Content(SessionMessageContent::text(message)),
1278            created_at: chrono::Utc::now(),
1279            thread_id: None,
1280            in_reply_to: None,
1281            attempt: None,
1282            correlation_id: Some(operation_id.to_string()),
1283        };
1284        let started = Instant::now();
1285        let result = match original_caller {
1286            Some(caller) => {
1287                self.inbox
1288                    .deliver_unscoped_supervisor_followup(
1289                        caller,
1290                        supervisor,
1291                        target_created_at,
1292                        &envelope,
1293                    )
1294                    .await
1295            }
1296            None => {
1297                self.inbox
1298                    .deliver_supervisor_followup(supervisor, target_created_at, &envelope)
1299                    .await
1300            }
1301        };
1302        let delivery = result.map_err(|error| {
1303            let error = SessionMessengerError::Inbox(error);
1304            self.record_rejection(&error);
1305            error
1306        })?;
1307        let admission = self.record_admission(envelope, delivery, started);
1308        // Its specific-message RespectSpecificWait intent was published while
1309        // holding Supervisor authority. This grants no staged sibling prefix.
1310        self.activate_prepared(&admission).await
1311    }
1312
1313    async fn load_session(&self, id: &str) -> Result<Option<Session>, SessionMessengerError> {
1314        self.sessions
1315            .load_session(id)
1316            .await
1317            .map_err(|error| SessionMessengerError::Storage(error.to_string()))
1318    }
1319
1320    fn logical_root(session: &Session) -> &str {
1321        if session.root_session_id.trim().is_empty() {
1322            &session.id
1323        } else {
1324            &session.root_session_id
1325        }
1326    }
1327
1328    async fn validate_relationship(
1329        &self,
1330        envelope: &SessionMessageEnvelope,
1331    ) -> Result<(), SessionMessengerError> {
1332        envelope
1333            .validate()
1334            .map_err(|error| SessionMessengerError::InvalidEnvelope(error.to_string()))?;
1335        let target = self
1336            .load_session(&envelope.target_session_id)
1337            .await?
1338            .ok_or_else(|| {
1339                SessionMessengerError::TargetNotFound(envelope.target_session_id.clone())
1340            })?;
1341
1342        let SessionMessageSource::Session { session_id } = &envelope.source else {
1343            return Ok(());
1344        };
1345        let source = self
1346            .load_session(session_id)
1347            .await?
1348            .ok_or_else(|| SessionMessengerError::SourceNotFound(session_id.clone()))?;
1349
1350        let same_root = Self::logical_root(&source) == Self::logical_root(&target);
1351        let source_project = source.project_id_meta();
1352        let target_project = target.project_id_meta();
1353        let project_compatible = match (source_project.as_deref(), target_project.as_deref()) {
1354            (Some(left), Some(right)) => left == right,
1355            // Older sessions did not persist project_id. Same-root ancestry is
1356            // the compatibility authority until both sides have migrated.
1357            _ => true,
1358        };
1359        if !same_root || !project_compatible {
1360            return Err(SessionMessengerError::Unauthorized {
1361                source_session_id: source.id,
1362                target: target.id,
1363            });
1364        }
1365        Ok(())
1366    }
1367
1368    fn record_rejection(&self, error: &SessionMessengerError) {
1369        self.metrics.rejected.fetch_add(1, Ordering::Relaxed);
1370        match error {
1371            SessionMessengerError::InvalidEnvelope(_) => {
1372                self.metrics
1373                    .invalid_envelope
1374                    .fetch_add(1, Ordering::Relaxed);
1375            }
1376            SessionMessengerError::Unauthorized { .. }
1377            | SessionMessengerError::SourceNotFound(_)
1378            | SessionMessengerError::TargetNotFound(_) => {
1379                self.metrics.unauthorized.fetch_add(1, Ordering::Relaxed);
1380            }
1381            SessionMessengerError::Inbox(SessionInboxError::AdmissionCancelled)
1382            | SessionMessengerError::Inbox(SessionInboxError::ExpectedRunConflict(_)) => {}
1383            SessionMessengerError::Inbox(SessionInboxError::PayloadTooLarge { .. }) => {
1384                self.metrics
1385                    .payload_too_large
1386                    .fetch_add(1, Ordering::Relaxed);
1387            }
1388            SessionMessengerError::Inbox(SessionInboxError::BacklogFull { .. }) => {
1389                self.metrics.backlog_full.fetch_add(1, Ordering::Relaxed);
1390            }
1391            SessionMessengerError::Inbox(_) | SessionMessengerError::Storage(_) => {
1392                self.metrics.storage_failed.fetch_add(1, Ordering::Relaxed);
1393            }
1394            SessionMessengerError::Activation { .. }
1395            | SessionMessengerError::ActivationEligibility { .. } => {}
1396        }
1397    }
1398
1399    pub async fn admit(
1400        &self,
1401        envelope: SessionMessageEnvelope,
1402    ) -> Result<SessionMessengerAdmission, SessionMessengerError> {
1403        self.admit_with_gate(envelope, None).await
1404    }
1405
1406    pub(crate) fn supports_runtime_target_lifetime(&self) -> bool {
1407        self.inbox.supports_runtime_target_lifetime()
1408    }
1409
1410    pub(crate) async fn admit_runtime_for_target(
1411        &self,
1412        envelope: SessionMessageEnvelope,
1413        target_created_at: chrono::DateTime<chrono::Utc>,
1414    ) -> Result<SessionMessengerAdmission, SessionMessengerError> {
1415        let started = Instant::now();
1416        let result = async {
1417            self.validate_relationship(&envelope).await?;
1418            self.inbox
1419                .deliver_runtime_for_target(&envelope, target_created_at)
1420                .await
1421                .map_err(SessionMessengerError::Inbox)
1422        }
1423        .await;
1424        match result {
1425            Ok(receipt) => Ok(self.record_admission(envelope, receipt, started)),
1426            Err(error) => {
1427                self.record_rejection(&error);
1428                Err(error)
1429            }
1430        }
1431    }
1432
1433    pub async fn admit_with_gate(
1434        &self,
1435        envelope: SessionMessageEnvelope,
1436        gate: Option<&bamboo_domain::AdmissionGate>,
1437    ) -> Result<SessionMessengerAdmission, SessionMessengerError> {
1438        self.admit_with_delivery_intent(envelope, gate, None).await
1439    }
1440
1441    /// Commit this specific message's immediate permission together with the
1442    /// envelope. Coordinators continue to use staged [`admit`](Self::admit).
1443    pub async fn admit_with_activation_intent(
1444        &self,
1445        envelope: SessionMessageEnvelope,
1446        policy: SessionActivationPolicy,
1447        gate: Option<&bamboo_domain::AdmissionGate>,
1448    ) -> Result<SessionMessengerAdmission, SessionMessengerError> {
1449        self.admit_with_delivery_intent(envelope, gate, Some(policy))
1450            .await
1451    }
1452
1453    async fn admit_with_delivery_intent(
1454        &self,
1455        envelope: SessionMessageEnvelope,
1456        gate: Option<&bamboo_domain::AdmissionGate>,
1457        policy: Option<SessionActivationPolicy>,
1458    ) -> Result<SessionMessengerAdmission, SessionMessengerError> {
1459        let started = Instant::now();
1460        if let Err(error) = self.validate_relationship(&envelope).await {
1461            self.record_rejection(&error);
1462            tracing::warn!(
1463                message_id = %envelope.id,
1464                target_session_id = %envelope.target_session_id,
1465                error = %error,
1466                "session message rejected"
1467            );
1468            return Err(error);
1469        }
1470
1471        let delivery = match (policy, gate) {
1472            (Some(policy), gate) => {
1473                self.inbox
1474                    .deliver_with_activation_intent(&envelope, policy, gate)
1475                    .await
1476            }
1477            (None, Some(gate)) => self.inbox.deliver_with_gate(&envelope, gate).await,
1478            (None, None) => self.inbox.deliver(&envelope).await,
1479        };
1480        let delivery = match delivery {
1481            Ok(receipt) => receipt,
1482            Err(error) => {
1483                let error = SessionMessengerError::Inbox(error);
1484                self.record_rejection(&error);
1485                return Err(error);
1486            }
1487        };
1488        Ok(self.record_admission(envelope, delivery, started))
1489    }
1490
1491    fn record_admission(
1492        &self,
1493        envelope: SessionMessageEnvelope,
1494        delivery: SessionInboxReceipt,
1495        started: Instant,
1496    ) -> SessionMessengerAdmission {
1497        self.metrics.delivered.fetch_add(1, Ordering::Relaxed);
1498        self.metrics.delivery_latency_micros.fetch_add(
1499            started.elapsed().as_micros().min(u128::from(u64::MAX)) as u64,
1500            Ordering::Relaxed,
1501        );
1502        SessionMessengerAdmission {
1503            envelope_id: envelope.id.as_str().to_string(),
1504            target_session_id: envelope.target_session_id,
1505            delivery,
1506        }
1507    }
1508
1509    pub async fn activate(
1510        &self,
1511        admission: &SessionMessengerAdmission,
1512    ) -> Result<SessionMessengerReceipt, SessionMessengerError> {
1513        self.activate_with_policy(admission, SessionActivationPolicy::InterruptSpecificWait)
1514            .await
1515    }
1516
1517    pub async fn activate_with_policy(
1518        &self,
1519        admission: &SessionMessengerAdmission,
1520        policy: SessionActivationPolicy,
1521    ) -> Result<SessionMessengerReceipt, SessionMessengerError> {
1522        if let Err(error) = self
1523            .inbox
1524            .mark_activation_eligible(
1525                &admission.target_session_id,
1526                admission.delivery.generation,
1527                policy,
1528            )
1529            .await
1530        {
1531            self.metrics
1532                .activation_failed
1533                .fetch_add(1, Ordering::Relaxed);
1534            return Err(SessionMessengerError::ActivationEligibility {
1535                receipt_id: admission.delivery.id.to_string(),
1536                receipt: admission.delivery.clone(),
1537                source: error,
1538            });
1539        }
1540        self.activate_prepared(admission).await
1541    }
1542
1543    /// Request execution for an admission whose activation policy was already
1544    /// durably recorded by a coordinator.
1545    pub async fn activate_prepared(
1546        &self,
1547        admission: &SessionMessengerAdmission,
1548    ) -> Result<SessionMessengerReceipt, SessionMessengerError> {
1549        let activation = match self
1550            .activation
1551            .request_activation(&admission.target_session_id, admission.delivery.generation)
1552            .await
1553        {
1554            Ok(disposition) => disposition,
1555            Err(source) => {
1556                self.metrics
1557                    .activation_failed
1558                    .fetch_add(1, Ordering::Relaxed);
1559                tracing::error!(
1560                    message_id = %admission.envelope_id,
1561                    target_session_id = %admission.target_session_id,
1562                    generation = admission.delivery.generation,
1563                    error = %source,
1564                    "session message durable but activation failed"
1565                );
1566                return Err(SessionMessengerError::Activation {
1567                    receipt_id: admission.delivery.id.to_string(),
1568                    receipt: admission.delivery.clone(),
1569                    source,
1570                });
1571            }
1572        };
1573        match activation {
1574            SessionActivationDisposition::ActiveNotified => {
1575                self.metrics.active_notified.fetch_add(1, Ordering::Relaxed);
1576            }
1577            SessionActivationDisposition::ActivationReserved => {
1578                self.metrics
1579                    .activation_reserved
1580                    .fetch_add(1, Ordering::Relaxed);
1581            }
1582            SessionActivationDisposition::ActivationCoalesced => {
1583                self.metrics
1584                    .activation_coalesced
1585                    .fetch_add(1, Ordering::Relaxed);
1586            }
1587        }
1588        tracing::info!(
1589            message_id = %admission.envelope_id,
1590            target_session_id = %admission.target_session_id,
1591            generation = admission.delivery.generation,
1592            ?activation,
1593            "session message durably delivered"
1594        );
1595        Ok(SessionMessengerReceipt {
1596            delivery: admission.delivery.clone(),
1597            activation,
1598        })
1599    }
1600
1601    /// Durably authorize the admitted queue prefix before a related
1602    /// control-plane transition is committed.
1603    ///
1604    /// Coordinators call this while their specific wait is still armed, then
1605    /// checkpoint the wait clear, and finally call [`activate`](Self::activate).
1606    /// A crash in either gap is recoverable: startup sees the watermark, while
1607    /// the real reservation adapter refuses to run until the specific wait has
1608    /// been durably cleared.
1609    pub async fn prepare_activation(
1610        &self,
1611        admission: &SessionMessengerAdmission,
1612    ) -> Result<(), SessionMessengerError> {
1613        self.inbox
1614            .mark_activation_eligible(
1615                &admission.target_session_id,
1616                admission.delivery.generation,
1617                SessionActivationPolicy::RespectSpecificWait,
1618            )
1619            .await
1620            .map_err(SessionMessengerError::Inbox)
1621    }
1622
1623    /// Authorize one staged delivery, preserving its immutable delivery
1624    /// intent and every sibling's existing execution permission.
1625    pub async fn prepare_message_activation(
1626        &self,
1627        admission: &SessionMessengerAdmission,
1628        policy: SessionActivationPolicy,
1629    ) -> Result<(), SessionMessengerError> {
1630        self.inbox
1631            .mark_message_activation_eligible(
1632                &admission.target_session_id,
1633                &admission.delivery,
1634                policy,
1635            )
1636            .await
1637            .map_err(SessionMessengerError::Inbox)
1638    }
1639
1640    pub async fn send(
1641        &self,
1642        envelope: SessionMessageEnvelope,
1643    ) -> Result<SessionMessengerReceipt, SessionMessengerError> {
1644        self.send_with_gate(envelope, None).await
1645    }
1646
1647    pub async fn send_with_gate(
1648        &self,
1649        envelope: SessionMessageEnvelope,
1650        gate: Option<&bamboo_domain::AdmissionGate>,
1651    ) -> Result<SessionMessengerReceipt, SessionMessengerError> {
1652        let admission = self
1653            .admit_with_activation_intent(
1654                envelope,
1655                SessionActivationPolicy::InterruptSpecificWait,
1656                gate,
1657            )
1658            .await?;
1659        self.activate_prepared(&admission).await
1660    }
1661
1662    pub async fn send_with_policy(
1663        &self,
1664        envelope: SessionMessageEnvelope,
1665        policy: SessionActivationPolicy,
1666    ) -> Result<SessionMessengerReceipt, SessionMessengerError> {
1667        let admission = self
1668            .admit_with_activation_intent(envelope, policy, None)
1669            .await?;
1670        self.activate_prepared(&admission).await
1671    }
1672}
1673
1674#[cfg(test)]
1675mod tests {
1676    use super::*;
1677    use async_trait::async_trait;
1678    use bamboo_domain::{
1679        SessionActivationDisposition, SessionInboxLimits, SessionMessageBody,
1680        SessionMessageContent, SessionMessageId, SessionMessageKind,
1681    };
1682    use bamboo_storage::{FileSessionInbox, SessionStoreV2};
1683    use chrono::Utc;
1684    use std::time::Duration;
1685    use tempfile::TempDir;
1686
1687    struct RecordingActivation {
1688        calls: tokio::sync::Mutex<Vec<(String, u64)>>,
1689    }
1690
1691    struct FailingActivation;
1692
1693    #[async_trait]
1694    impl SessionActivationPort for FailingActivation {
1695        async fn request_activation(
1696            &self,
1697            _target_session_id: &str,
1698            _inbox_generation: u64,
1699        ) -> Result<SessionActivationDisposition, SessionActivationError> {
1700            Err(SessionActivationError::Internal(
1701                "injected wakeup failure".into(),
1702            ))
1703        }
1704    }
1705
1706    #[tokio::test]
1707    async fn immediate_activation_failure_preserves_receipt_and_restart_permission() {
1708        let (temp, store, recording, _messenger) = fixture().await;
1709        store
1710            .save_session(&Session::new("root", "model"))
1711            .await
1712            .unwrap();
1713        let inbox = Arc::new(FileSessionInbox::new(
1714            store.clone(),
1715            SessionInboxLimits::default(),
1716        ));
1717        let messenger = SessionMessenger::new(store, inbox, Arc::new(FailingActivation));
1718        let envelope = SessionMessageEnvelope::user_input("root", "steer");
1719        let gate = bamboo_domain::AdmissionGate::default();
1720        let receipt = match messenger
1721            .send_with_gate(envelope.clone(), Some(&gate))
1722            .await
1723            .unwrap_err()
1724        {
1725            SessionMessengerError::Activation { receipt, .. } => receipt,
1726            error => panic!("expected durable delivery with failed wakeup: {error}"),
1727        };
1728        assert!(gate.is_committed());
1729        gate.cancel_if_pending();
1730        let restarted_store = Arc::new(SessionStoreV2::new(temp.path().into()).await.unwrap());
1731        let restarted = Arc::new(FileSessionInbox::new(
1732            restarted_store.clone(),
1733            SessionInboxLimits::default(),
1734        ));
1735        let backlog = restarted.inspect("root").await.unwrap();
1736        assert!(backlog.activation_pending());
1737        assert_eq!(backlog.coordinator_generation, 0);
1738        // This is the startup reconciliation request: the durable generation
1739        // comes from this message's intent while the coordinator prefix is 0.
1740        recording
1741            .request_activation("root", backlog.activation_generation)
1742            .await
1743            .unwrap();
1744        let retry = SessionMessenger::new(restarted_store, restarted.clone(), recording.clone());
1745        assert_eq!(
1746            retry
1747                .send_with_gate(envelope, Some(&gate))
1748                .await
1749                .unwrap()
1750                .delivery,
1751            receipt
1752        );
1753        assert_eq!(restarted.inspect("root").await.unwrap().pending, 1);
1754        assert_eq!(messenger.metrics().snapshot().activation_failed, 1);
1755        let claims = restarted.claim("root", 128).await.unwrap();
1756        assert_eq!(claims.len(), 1);
1757        assert_eq!(claims[0].envelope.id, receipt.id);
1758    }
1759
1760    struct FailEligibilityOnce {
1761        inner: Arc<FileSessionInbox>,
1762        fail_next: std::sync::atomic::AtomicBool,
1763    }
1764
1765    #[async_trait]
1766    impl SessionInboxPort for FailEligibilityOnce {
1767        async fn deliver(
1768            &self,
1769            envelope: &SessionMessageEnvelope,
1770        ) -> Result<SessionInboxReceipt, SessionInboxError> {
1771            self.inner.deliver(envelope).await
1772        }
1773
1774        async fn deliver_with_gate(
1775            &self,
1776            envelope: &SessionMessageEnvelope,
1777            gate: &bamboo_domain::AdmissionGate,
1778        ) -> Result<SessionInboxReceipt, SessionInboxError> {
1779            self.inner.deliver_with_gate(envelope, gate).await
1780        }
1781
1782        async fn mark_activation_eligible(
1783            &self,
1784            target_session_id: &str,
1785            generation: u64,
1786            policy: SessionActivationPolicy,
1787        ) -> Result<(), SessionInboxError> {
1788            if self.fail_next.swap(false, Ordering::SeqCst) {
1789                return Err(SessionInboxError::Storage(
1790                    "injected activation watermark failure".into(),
1791                ));
1792            }
1793            self.inner
1794                .mark_activation_eligible(target_session_id, generation, policy)
1795                .await
1796        }
1797
1798        async fn claim(
1799            &self,
1800            target_session_id: &str,
1801            limit: usize,
1802        ) -> Result<Vec<bamboo_domain::SessionInboxClaim>, SessionInboxError> {
1803            self.inner.claim(target_session_id, limit).await
1804        }
1805
1806        async fn was_admitted(
1807            &self,
1808            target_session_id: &str,
1809            id: &SessionMessageId,
1810        ) -> Result<bool, SessionInboxError> {
1811            self.inner.was_admitted(target_session_id, id).await
1812        }
1813
1814        async fn ack(
1815            &self,
1816            target_session_id: &str,
1817            claim: &bamboo_domain::SessionInboxClaim,
1818        ) -> Result<(), SessionInboxError> {
1819            self.inner.ack(target_session_id, claim).await
1820        }
1821
1822        async fn inspect(
1823            &self,
1824            target_session_id: &str,
1825        ) -> Result<bamboo_domain::SessionInboxBacklog, SessionInboxError> {
1826            self.inner.inspect(target_session_id).await
1827        }
1828    }
1829
1830    #[async_trait]
1831    impl SessionActivationPort for RecordingActivation {
1832        async fn request_activation(
1833            &self,
1834            target_session_id: &str,
1835            inbox_generation: u64,
1836        ) -> Result<SessionActivationDisposition, SessionActivationError> {
1837            self.calls
1838                .lock()
1839                .await
1840                .push((target_session_id.to_string(), inbox_generation));
1841            Ok(SessionActivationDisposition::ActivationReserved)
1842        }
1843    }
1844
1845    async fn fixture() -> (
1846        TempDir,
1847        Arc<SessionStoreV2>,
1848        Arc<RecordingActivation>,
1849        SessionMessenger,
1850    ) {
1851        let temp = TempDir::new().unwrap();
1852        let store = Arc::new(
1853            SessionStoreV2::new(temp.path().to_path_buf())
1854                .await
1855                .unwrap(),
1856        );
1857        let activation = Arc::new(RecordingActivation {
1858            calls: tokio::sync::Mutex::new(Vec::new()),
1859        });
1860        let inbox = Arc::new(FileSessionInbox::new(
1861            store.clone(),
1862            SessionInboxLimits::default(),
1863        ));
1864        let messenger = SessionMessenger::new(store.clone(), inbox, activation.clone());
1865        (temp, store, activation, messenger)
1866    }
1867
1868    fn peer(source: &str, target: &str, id: &str) -> SessionMessageEnvelope {
1869        SessionMessageEnvelope {
1870            id: SessionMessageId::parse(id).unwrap(),
1871            source: SessionMessageSource::Session {
1872                session_id: source.to_string(),
1873            },
1874            target_session_id: target.to_string(),
1875            kind: SessionMessageKind::PeerMessage,
1876            body: SessionMessageBody::Content(SessionMessageContent::text("hello")),
1877            created_at: chrono::Utc::now(),
1878            thread_id: None,
1879            in_reply_to: None,
1880            attempt: None,
1881            correlation_id: None,
1882        }
1883    }
1884
1885    #[derive(Default)]
1886    struct PeerMemoryStorage(tokio::sync::RwLock<std::collections::HashMap<String, Session>>);
1887
1888    #[async_trait]
1889    impl Storage for PeerMemoryStorage {
1890        async fn save_session(&self, session: &Session) -> std::io::Result<()> {
1891            self.0
1892                .write()
1893                .await
1894                .insert(session.id.clone(), session.clone());
1895            Ok(())
1896        }
1897
1898        async fn load_session(&self, id: &str) -> std::io::Result<Option<Session>> {
1899            Ok(self.0.read().await.get(id).cloned())
1900        }
1901
1902        async fn delete_session(&self, id: &str) -> std::io::Result<bool> {
1903            Ok(self.0.write().await.remove(id).is_some())
1904        }
1905
1906        async fn list_child_run_statuses(
1907            &self,
1908            parent: &str,
1909        ) -> std::io::Result<Vec<(String, Option<String>)>> {
1910            Ok(self
1911                .0
1912                .read()
1913                .await
1914                .values()
1915                .filter(|session| session.parent_session_id.as_deref() == Some(parent))
1916                .map(|session| (session.id.clone(), session.last_run_status()))
1917                .collect())
1918        }
1919    }
1920
1921    async fn seed_peer_tree(store: &dyn Storage) {
1922        let mut root = Session::new("peer-root", "model");
1923        root.title = "Logical root".into();
1924        root.set_project_id_meta("peer-project");
1925        let left = Session::new_child_of("peer-left", &root, "model", "Left");
1926        let right = Session::new_child_of("peer-right", &root, "model", "Right");
1927        let cousin_left = Session::new_child_of("peer-left-leaf", &left, "model", "Left leaf");
1928        let cousin_right = Session::new_child_of("peer-right-leaf", &right, "model", "Right leaf");
1929        for mut session in [root, left, right, cousin_left, cousin_right] {
1930            session.set_project_id_meta("peer-project");
1931            store.save_session(&session).await.unwrap();
1932        }
1933    }
1934
1935    struct PausedPeerAdmission {
1936        inner: Arc<FileSessionInbox>,
1937        entered: Arc<tokio::sync::Notify>,
1938        release: Arc<tokio::sync::Notify>,
1939    }
1940
1941    #[async_trait]
1942    impl SessionInboxPort for PausedPeerAdmission {
1943        fn supports_runtime_target_lifetime(&self) -> bool {
1944            true
1945        }
1946
1947        async fn deliver_runtime_for_target(
1948            &self,
1949            envelope: &SessionMessageEnvelope,
1950            target_created_at: chrono::DateTime<Utc>,
1951        ) -> Result<SessionInboxReceipt, SessionInboxError> {
1952            self.entered.notify_one();
1953            self.release.notified().await;
1954            self.inner
1955                .deliver_runtime_for_target(envelope, target_created_at)
1956                .await
1957        }
1958
1959        async fn deliver(
1960            &self,
1961            envelope: &SessionMessageEnvelope,
1962        ) -> Result<SessionInboxReceipt, SessionInboxError> {
1963            self.inner.deliver(envelope).await
1964        }
1965
1966        async fn deliver_peer_message(
1967            &self,
1968            envelope: &SessionMessageEnvelope,
1969            source: &ActorSession,
1970            target: &ActorSession,
1971            reply_to: Option<&SessionMessageEnvelope>,
1972        ) -> Result<SessionInboxReceipt, SessionInboxError> {
1973            // The engine has already captured both births and built its id;
1974            // actual filesystem admission has not yet acquired its authority.
1975            self.entered.notify_one();
1976            self.release.notified().await;
1977            self.inner
1978                .deliver_peer_message(envelope, source, target, reply_to)
1979                .await
1980        }
1981
1982        async fn mark_activation_eligible(
1983            &self,
1984            target: &str,
1985            generation: u64,
1986            policy: SessionActivationPolicy,
1987        ) -> Result<(), SessionInboxError> {
1988            self.inner
1989                .mark_activation_eligible(target, generation, policy)
1990                .await
1991        }
1992
1993        async fn claim(
1994            &self,
1995            target: &str,
1996            limit: usize,
1997        ) -> Result<Vec<bamboo_domain::SessionInboxClaim>, SessionInboxError> {
1998            self.inner.claim(target, limit).await
1999        }
2000
2001        async fn was_admitted(
2002            &self,
2003            target: &str,
2004            id: &SessionMessageId,
2005        ) -> Result<bool, SessionInboxError> {
2006            self.inner.was_admitted(target, id).await
2007        }
2008
2009        async fn ack(
2010            &self,
2011            target: &str,
2012            claim: &bamboo_domain::SessionInboxClaim,
2013        ) -> Result<(), SessionInboxError> {
2014            self.inner.ack(target, claim).await
2015        }
2016
2017        async fn inspect(
2018            &self,
2019            target: &str,
2020        ) -> Result<bamboo_domain::SessionInboxBacklog, SessionInboxError> {
2021            self.inner.inspect(target).await
2022        }
2023    }
2024
2025    #[tokio::test]
2026    async fn peer_send_and_reply_reject_either_lifetime_recreated_before_admission() {
2027        for reply in [false, true] {
2028            for replaced_id in ["peer-left", "peer-right"] {
2029                let (_temp, store, activation, messenger) = fixture().await;
2030                seed_peer_tree(store.as_ref()).await;
2031                let received_id = if reply {
2032                    let incoming = messenger
2033                        .send_peer("peer-left", "peer-right", "incoming", "question")
2034                        .await
2035                        .unwrap();
2036                    admit_peer_for_test(store.as_ref(), messenger.inbox().as_ref(), "peer-right")
2037                        .await;
2038                    Some(incoming.delivery.id.to_string())
2039                } else {
2040                    None
2041                };
2042                activation.calls.lock().await.clear();
2043                let (caller, target) = if reply {
2044                    ("peer-right", "peer-left")
2045                } else {
2046                    ("peer-left", "peer-right")
2047                };
2048                let entered = Arc::new(tokio::sync::Notify::new());
2049                let release = Arc::new(tokio::sync::Notify::new());
2050                let inbox = Arc::new(FileSessionInbox::new(
2051                    store.clone(),
2052                    SessionInboxLimits::default(),
2053                ));
2054                let paused = SessionMessenger::new(
2055                    store.clone(),
2056                    Arc::new(PausedPeerAdmission {
2057                        inner: inbox.clone(),
2058                        entered: entered.clone(),
2059                        release: release.clone(),
2060                    }),
2061                    activation.clone(),
2062                );
2063                let sending = tokio::spawn(async move {
2064                    if let Some(id) = received_id {
2065                        paused
2066                            .reply_peer(caller, &id, "racing", "old lifetime")
2067                            .await
2068                    } else {
2069                        paused
2070                            .send_peer(caller, target, "racing", "old lifetime")
2071                            .await
2072                    }
2073                });
2074                tokio::time::timeout(std::time::Duration::from_secs(5), entered.notified())
2075                    .await
2076                    .unwrap();
2077                let old = store.load_session(replaced_id).await.unwrap().unwrap();
2078                assert!(store.delete_session(replaced_id).await.unwrap());
2079                let root = store.load_session("peer-root").await.unwrap().unwrap();
2080                let mut replacement =
2081                    Session::new_child_of(replaced_id, &root, "model", "Replacement");
2082                replacement.created_at = old.created_at + chrono::Duration::seconds(1);
2083                replacement.set_project_id_meta("peer-project");
2084                store.save_session(&replacement).await.unwrap();
2085                release.notify_one();
2086                let result = tokio::time::timeout(std::time::Duration::from_secs(5), sending)
2087                    .await
2088                    .unwrap()
2089                    .unwrap();
2090                assert!(result.is_err(), "reply={reply}, replaced={replaced_id}");
2091                let backlog = inbox.inspect(target).await.unwrap();
2092                assert_eq!(backlog.generation, 0);
2093                assert_eq!(backlog.pending, 0);
2094                assert!(!backlog.activation_pending());
2095                assert!(activation.calls.lock().await.is_empty());
2096                // Same ids, tree and Project are legal for a fresh operation;
2097                // only the stale captured lifetime must fail closed.
2098                let current = messenger
2099                    .send_peer(caller, target, "current-lifetime", "current")
2100                    .await
2101                    .unwrap();
2102                assert_eq!(current.delivery.generation, 1);
2103                assert_eq!(activation.calls.lock().await.len(), 1);
2104            }
2105        }
2106    }
2107
2108    #[tokio::test]
2109    async fn background_exec_admission_rechecks_target_birth_at_actual_inbox_publication() {
2110        let (_temp, store, activation, _) = fixture().await;
2111        let old = Session::new("runtime-completion-target", "model");
2112        store.save_session(&old).await.unwrap();
2113        let entered = Arc::new(tokio::sync::Notify::new());
2114        let release = Arc::new(tokio::sync::Notify::new());
2115        let inbox = Arc::new(FileSessionInbox::new(
2116            store.clone(),
2117            SessionInboxLimits::default(),
2118        ));
2119        let paused = SessionMessenger::new(
2120            store.clone(),
2121            Arc::new(PausedPeerAdmission {
2122                inner: inbox.clone(),
2123                entered: entered.clone(),
2124                release: release.clone(),
2125            }),
2126            activation.clone(),
2127        );
2128        let mut envelope = SessionMessageEnvelope::user_input(&old.id, "unused");
2129        envelope.source = SessionMessageSource::Runtime {
2130            subsystem: "background_exec".into(),
2131        };
2132        envelope.kind = bamboo_domain::SessionMessageKind::RuntimeInstruction;
2133        envelope.body = bamboo_domain::SessionMessageBody::RuntimeInstruction(
2134            bamboo_domain::SessionRuntimeInstruction {
2135                instruction: "background_exec_completed".into(),
2136                content: Some(bamboo_domain::SessionMessageContent::text(
2137                    "old command result",
2138                )),
2139                data: None,
2140                provider_message: None,
2141            },
2142        );
2143        let birth = old.created_at;
2144        let sending = tokio::spawn(async move {
2145            match paused.admit_runtime_for_target(envelope, birth).await {
2146                Ok(admission) => {
2147                    paused.prepare_activation(&admission).await.unwrap();
2148                    paused.activate_prepared(&admission).await.unwrap();
2149                    true
2150                }
2151                Err(_) => false,
2152            }
2153        });
2154        tokio::time::timeout(Duration::from_secs(5), entered.notified())
2155            .await
2156            .unwrap();
2157        store.delete_session(&old.id).await.unwrap();
2158        let replacement = store.recreate_root_session(&old.id, "model").await.unwrap();
2159        assert_ne!(replacement.created_at, birth);
2160        release.notify_one();
2161        assert!(!tokio::time::timeout(Duration::from_secs(5), sending)
2162            .await
2163            .unwrap()
2164            .unwrap());
2165        assert_eq!(inbox.inspect(&old.id).await.unwrap().pending, 0);
2166        assert!(activation.calls.lock().await.is_empty());
2167    }
2168
2169    #[tokio::test]
2170    async fn background_exec_old_receipt_cannot_authorize_a_recreated_sessions_staged_message() {
2171        let (_temp, store, activation, messenger) = fixture().await;
2172        let old = Session::new("runtime-activation-target", "model");
2173        store.save_session(&old).await.unwrap();
2174        let mut envelope = SessionMessageEnvelope::user_input(&old.id, "unused");
2175        envelope.source = SessionMessageSource::Runtime {
2176            subsystem: "background_exec".into(),
2177        };
2178        envelope.kind = SessionMessageKind::RuntimeInstruction;
2179        envelope.body =
2180            SessionMessageBody::RuntimeInstruction(bamboo_domain::SessionRuntimeInstruction {
2181                instruction: "background_exec_completed".into(),
2182                content: Some(SessionMessageContent::text("old result")),
2183                data: None,
2184                provider_message: None,
2185            });
2186        let admission = messenger
2187            .admit_runtime_for_target(envelope, old.created_at)
2188            .await
2189            .unwrap();
2190        store.delete_session(&old.id).await.unwrap();
2191        let replacement = store.recreate_root_session(&old.id, "model").await.unwrap();
2192        assert_ne!(replacement.created_at, old.created_at);
2193        let staged = messenger
2194            .admit(SessionMessageEnvelope::user_input(
2195                &old.id,
2196                "new staged input",
2197            ))
2198            .await
2199            .unwrap();
2200        assert_eq!(admission.delivery.generation, staged.delivery.generation);
2201        assert_ne!(admission.delivery.id, staged.delivery.id);
2202        assert!(messenger
2203            .prepare_message_activation(&admission, SessionActivationPolicy::RespectSpecificWait)
2204            .await
2205            .is_err());
2206        assert!(!messenger
2207            .inbox()
2208            .inspect(&old.id)
2209            .await
2210            .unwrap()
2211            .activation_pending());
2212        assert!(activation.calls.lock().await.is_empty());
2213    }
2214
2215    #[tokio::test]
2216    async fn peer_reply_revalidates_received_proof_after_concurrent_clear_or_save() {
2217        for clear in [true, false] {
2218            let (_temp, store, activation, messenger) = fixture().await;
2219            seed_peer_tree(store.as_ref()).await;
2220            let incoming = messenger
2221                .send_peer("peer-left", "peer-right", "incoming", "question")
2222                .await
2223                .unwrap();
2224            admit_peer_for_test(store.as_ref(), messenger.inbox().as_ref(), "peer-right").await;
2225            activation.calls.lock().await.clear();
2226            let before = store.load_session("peer-right").await.unwrap().unwrap();
2227            let received_id = incoming.delivery.id.to_string();
2228            let entered = Arc::new(tokio::sync::Notify::new());
2229            let release = Arc::new(tokio::sync::Notify::new());
2230            let inbox = Arc::new(FileSessionInbox::new(
2231                store.clone(),
2232                SessionInboxLimits::default(),
2233            ));
2234            let paused = SessionMessenger::new(
2235                store.clone(),
2236                Arc::new(PausedPeerAdmission {
2237                    inner: inbox.clone(),
2238                    entered: entered.clone(),
2239                    release: release.clone(),
2240                }),
2241                activation.clone(),
2242            );
2243            let old_id = received_id.clone();
2244            let replying = tokio::spawn(async move {
2245                paused
2246                    .reply_peer("peer-right", &old_id, "racing-proof", "reply")
2247                    .await
2248            });
2249            tokio::time::timeout(std::time::Duration::from_secs(5), entered.notified())
2250                .await
2251                .unwrap();
2252            if clear {
2253                assert!(store.clear_session("peer-right").await.unwrap());
2254            } else {
2255                let mut changed = before.clone();
2256                changed.messages.retain(|message| message.id != received_id);
2257                store.save_session(&changed).await.unwrap();
2258            }
2259            let after = store.load_session("peer-right").await.unwrap().unwrap();
2260            assert_eq!(before.created_at, after.created_at);
2261            assert_eq!(before.root_session_id, after.root_session_id);
2262            assert_eq!(before.project_id_meta(), after.project_id_meta());
2263            assert!(!after
2264                .messages
2265                .iter()
2266                .any(|message| message.id == received_id));
2267            release.notify_one();
2268            let result = tokio::time::timeout(std::time::Duration::from_secs(5), replying)
2269                .await
2270                .unwrap()
2271                .unwrap();
2272            assert!(
2273                result.is_err(),
2274                "clear={clear}: removed proof must not admit a reply"
2275            );
2276            let backlog = inbox.inspect("peer-left").await.unwrap();
2277            assert_eq!(backlog.generation, 0);
2278            assert_eq!(backlog.pending, 0);
2279            assert!(!backlog.activation_pending());
2280            assert!(activation.calls.lock().await.is_empty());
2281
2282            let current = messenger
2283                .send_peer("peer-left", "peer-right", "current-question", "current")
2284                .await
2285                .unwrap();
2286            admit_peer_for_test(store.as_ref(), messenger.inbox().as_ref(), "peer-right").await;
2287            let reply = messenger
2288                .reply_peer(
2289                    "peer-right",
2290                    current.delivery.id.as_str(),
2291                    "racing-proof",
2292                    "reply",
2293                )
2294                .await
2295                .unwrap();
2296            let retry = messenger
2297                .reply_peer(
2298                    "peer-right",
2299                    current.delivery.id.as_str(),
2300                    "racing-proof",
2301                    "reply",
2302                )
2303                .await
2304                .unwrap();
2305            assert_eq!(reply.delivery.generation, 1);
2306            assert_eq!(retry.delivery.generation, reply.delivery.generation);
2307            assert_eq!(retry.delivery.id, reply.delivery.id);
2308            let backlog = inbox.inspect("peer-left").await.unwrap();
2309            assert_eq!(backlog.generation, 1);
2310            assert_eq!(backlog.pending, 1);
2311        }
2312    }
2313
2314    #[tokio::test]
2315    async fn peer_admission_without_integrated_lifetime_fence_is_unsupported() {
2316        let (_temp, store, activation, _messenger) = fixture().await;
2317        seed_peer_tree(store.as_ref()).await;
2318        let inbox = Arc::new(FileSessionInbox::new(
2319            store.clone(),
2320            SessionInboxLimits::default(),
2321        ));
2322        let messenger = SessionMessenger::new(
2323            store,
2324            Arc::new(FailEligibilityOnce {
2325                inner: inbox.clone(),
2326                fail_next: std::sync::atomic::AtomicBool::new(false),
2327            }),
2328            activation.clone(),
2329        );
2330        let error = messenger
2331            .send_peer("peer-left", "peer-right", "unsupported", "message")
2332            .await
2333            .unwrap_err();
2334        assert!(error
2335            .to_string()
2336            .contains("lifetime-fenced peer SessionInbox admission is unsupported"));
2337        for intent in [
2338            SessionMessageDeliveryIntent::QueueOnly,
2339            SessionMessageDeliveryIntent::TriggerTurn,
2340        ] {
2341            assert!(messenger
2342                .send_peer_with_intent(
2343                    "peer-left",
2344                    "peer-right",
2345                    "unsupported-explicit",
2346                    "message",
2347                    intent
2348                )
2349                .await
2350                .unwrap_err()
2351                .to_string()
2352                .contains("explicit peer delivery intent is unsupported"));
2353        }
2354        assert_eq!(inbox.inspect("peer-right").await.unwrap().pending, 0);
2355        assert!(activation.calls.lock().await.is_empty());
2356    }
2357
2358    async fn admit_peer_for_test(
2359        store: &dyn Storage,
2360        inbox: &dyn SessionInboxPort,
2361        target: &str,
2362    ) -> SessionMessageEnvelope {
2363        let claims = inbox.claim(target, 16).await.unwrap();
2364        assert_eq!(claims.len(), 1);
2365        let claim = &claims[0];
2366        let mut session = store.load_session(target).await.unwrap().unwrap();
2367        session.add_message(claim.envelope.to_provider_message().unwrap());
2368        store.save_session(&session).await.unwrap();
2369        inbox.ack(target, claim).await.unwrap();
2370        claim.envelope.clone()
2371    }
2372
2373    #[tokio::test]
2374    async fn peer_api_discovers_and_messages_parent_child_siblings_and_cousins() {
2375        let (_temp, store, activation, messenger) = fixture().await;
2376        seed_peer_tree(store.as_ref()).await;
2377        store
2378            .save_session(&Session::new("foreign-root", "model"))
2379            .await
2380            .unwrap();
2381        let targets = messenger.list_peer_targets("peer-left-leaf").await.unwrap();
2382        assert!(!targets.truncated);
2383        let ids: Vec<_> = targets
2384            .targets
2385            .iter()
2386            .map(|target| target.session_id.as_str())
2387            .collect();
2388        assert_eq!(
2389            ids,
2390            ["peer-root", "peer-left", "peer-right", "peer-right-leaf"]
2391        );
2392        assert_eq!(targets.targets[0].title, "Logical root");
2393        assert!(targets.targets[0].parent_session_id.is_none());
2394        assert!(!serde_json::to_string(&targets)
2395            .unwrap()
2396            .contains("foreign-root"));
2397
2398        for (index, (source, target)) in [
2399            ("peer-root", "peer-left"),
2400            ("peer-left", "peer-root"),
2401            ("peer-left", "peer-right"),
2402            ("peer-left-leaf", "peer-right-leaf"),
2403        ]
2404        .into_iter()
2405        .enumerate()
2406        {
2407            let receipt = messenger
2408                .send_peer(
2409                    source,
2410                    target,
2411                    &format!("operation-{index}"),
2412                    "bounded peer instruction",
2413                )
2414                .await
2415                .unwrap();
2416            let backlog = messenger.inbox().inspect(target).await.unwrap();
2417            assert_eq!(
2418                backlog.coordinator_generation, 0,
2419                "peer permission must not release a prefix"
2420            );
2421            assert!(backlog.interrupt_pending());
2422            let envelope =
2423                admit_peer_for_test(store.as_ref(), messenger.inbox().as_ref(), target).await;
2424            assert_eq!(envelope.id, receipt.delivery.id);
2425            assert_eq!(
2426                envelope.source,
2427                SessionMessageSource::Session {
2428                    session_id: source.into()
2429                }
2430            );
2431            assert_eq!(envelope.thread_id.as_deref(), Some(envelope.id.as_str()));
2432            assert_eq!(
2433                envelope.correlation_id.as_deref(),
2434                Some(format!("operation-{index}").as_str())
2435            );
2436            assert!(envelope.in_reply_to.is_none());
2437        }
2438        assert_eq!(activation.calls.lock().await.len(), 4);
2439    }
2440
2441    #[tokio::test]
2442    async fn peer_api_denies_unknown_deleted_cross_root_and_invalid_project_or_lineage() {
2443        let (_temp, store, activation, messenger) = fixture().await;
2444        seed_peer_tree(store.as_ref()).await;
2445        store
2446            .save_session(&Session::new("foreign-root", "model"))
2447            .await
2448            .unwrap();
2449        for (source, target) in [
2450            ("peer-left", "foreign-root"),
2451            ("unknown", "peer-left"),
2452            ("peer-left", "unknown"),
2453            ("peer-left", "peer-left"),
2454        ] {
2455            assert!(messenger
2456                .send_peer(source, target, "denied-operation", "instruction")
2457                .await
2458                .is_err());
2459        }
2460        store.delete_session("peer-right-leaf").await.unwrap();
2461        assert!(matches!(
2462            messenger
2463                .send_peer("peer-left", "peer-right-leaf", "deleted", "instruction")
2464                .await,
2465            Err(SessionMessengerError::TargetNotFound(_))
2466        ));
2467        assert!(matches!(
2468            messenger
2469                .send_peer(
2470                    "peer-right-leaf",
2471                    "peer-left",
2472                    "deleted-source",
2473                    "instruction"
2474                )
2475                .await,
2476            Err(SessionMessengerError::SourceNotFound(_))
2477        ));
2478
2479        // Invalid canonical snapshots exercise the read boundary without
2480        // relaxing the production store's immutable lineage writer checks.
2481        let memory = Arc::new(PeerMemoryStorage::default());
2482        seed_peer_tree(memory.as_ref()).await;
2483        let strict = SessionMessenger::new(
2484            memory.clone(),
2485            messenger.inbox().clone(),
2486            activation.clone(),
2487        );
2488        let original = memory.load_session("peer-right").await.unwrap().unwrap();
2489        for defect in [
2490            "project",
2491            "missing_project",
2492            "project_mirror",
2493            "parent",
2494            "depth",
2495            "root",
2496            "cycle",
2497        ] {
2498            let mut invalid = original.clone();
2499            match defect {
2500                "project" => invalid.set_project_id_meta("other-project"),
2501                "missing_project" => {
2502                    invalid.metadata.remove("project_id");
2503                    if let Some(metadata) = invalid.runtime_metadata.as_mut() {
2504                        metadata.project_id = None;
2505                    }
2506                }
2507                "project_mirror" => {
2508                    invalid
2509                        .metadata
2510                        .insert("project_id".into(), "other-project".into());
2511                }
2512                "parent" => invalid.parent_session_id = Some("missing-parent".into()),
2513                "depth" => invalid.spawn_depth += 1,
2514                "root" => invalid.root_session_id = "foreign-root".into(),
2515                "cycle" => invalid.parent_session_id = Some("peer-right-leaf".into()),
2516                _ => unreachable!(),
2517            }
2518            memory.save_session(&invalid).await.unwrap();
2519            assert!(
2520                strict
2521                    .send_peer("peer-left", "peer-right", "invalid", "instruction")
2522                    .await
2523                    .is_err(),
2524                "{defect}"
2525            );
2526            let targets = strict.list_peer_targets("peer-left").await.unwrap();
2527            assert!(
2528                !targets
2529                    .targets
2530                    .iter()
2531                    .any(|target| target.session_id == "peer-right"),
2532                "{defect}"
2533            );
2534            memory.save_session(&original).await.unwrap();
2535        }
2536        assert!(activation.calls.lock().await.is_empty());
2537        assert_eq!(
2538            messenger
2539                .inbox()
2540                .inspect("peer-left")
2541                .await
2542                .unwrap()
2543                .generation,
2544            0
2545        );
2546    }
2547
2548    #[tokio::test]
2549    async fn peer_api_discovery_is_bounded_and_sender_is_runtime_selected() {
2550        let (_temp, _store, activation, messenger) = fixture().await;
2551        let memory = Arc::new(PeerMemoryStorage::default());
2552        let root = Session::new("many-peers", "model");
2553        memory.save_session(&root).await.unwrap();
2554        for index in 0..MAX_PEER_TARGETS + 2 {
2555            let child = Session::new_child_of(format!("peer-{index:03}"), &root, "model", "Peer");
2556            memory.save_session(&child).await.unwrap();
2557        }
2558        let bounded = SessionMessenger::new(
2559            memory.clone(),
2560            messenger.inbox().clone(),
2561            activation.clone(),
2562        );
2563        let page = bounded.list_peer_targets(&root.id).await.unwrap();
2564        assert_eq!(page.targets.len(), MAX_PEER_TARGETS);
2565        assert!(page.truncated);
2566        for index in 0..MAX_PEER_TARGETS + 2 {
2567            let mut child = memory
2568                .load_session(&format!("peer-{index:03}"))
2569                .await
2570                .unwrap()
2571                .unwrap();
2572            child.title = "界".repeat(1000);
2573            child.set_last_run_status("\0".repeat(1000));
2574            memory.save_session(&child).await.unwrap();
2575        }
2576        let page = bounded.list_peer_targets(&root.id).await.unwrap();
2577        assert!(!page.targets.is_empty());
2578        assert!(page.targets.len() < MAX_PEER_TARGETS);
2579        assert!(page.truncated);
2580        assert!(serde_json::to_vec(&page).unwrap().len() <= MAX_PEER_TARGET_RESULT_BYTES);
2581        assert_eq!(page.targets[0].title, "界".repeat(85));
2582        assert_eq!(
2583            page.targets[0].status.as_deref(),
2584            Some("\0".repeat(MAX_PEER_TARGET_STATUS_BYTES).as_str())
2585        );
2586
2587        let (_temp, store, _activation, messenger) = fixture().await;
2588        seed_peer_tree(store.as_ref()).await;
2589        let forged_content =
2590            r#"{"source":{"type":"session","session_id":"peer-root"},"target":"foreign-root"}"#;
2591        messenger
2592            .send_peer("peer-left", "peer-right", "caller-bound", forged_content)
2593            .await
2594            .unwrap();
2595        let envelope =
2596            admit_peer_for_test(store.as_ref(), messenger.inbox().as_ref(), "peer-right").await;
2597        assert_eq!(
2598            envelope.source,
2599            SessionMessageSource::Session {
2600                session_id: "peer-left".into()
2601            }
2602        );
2603        assert_eq!(envelope.target_session_id, "peer-right");
2604        assert_eq!(
2605            envelope.body,
2606            SessionMessageBody::Content(SessionMessageContent::text(forged_content))
2607        );
2608        for id in ["", " padded", "bad/path", "bad\\path", "bad..id", "nul\0"] {
2609            assert!(matches!(
2610                messenger
2611                    .send_peer(id, "peer-right", "invalid-source", "instruction")
2612                    .await,
2613                Err(SessionMessengerError::InvalidEnvelope(_))
2614            ));
2615            assert!(matches!(
2616                messenger
2617                    .send_peer("peer-left", id, "invalid-target", "instruction")
2618                    .await,
2619                Err(SessionMessengerError::InvalidEnvelope(_))
2620            ));
2621            assert!(matches!(
2622                messenger
2623                    .reply_peer(id, envelope.id.as_str(), "invalid-reply", "instruction")
2624                    .await,
2625                Err(SessionMessengerError::InvalidEnvelope(_))
2626            ));
2627        }
2628        assert!(matches!(
2629            messenger.list_peer_targets(&"x".repeat(257)).await,
2630            Err(SessionMessengerError::InvalidEnvelope(_))
2631        ));
2632        for (operation, message) in [
2633            ("", "valid"),
2634            (" padded", "valid"),
2635            ("bad..id", "valid"),
2636            ("valid", " "),
2637            ("valid", "nul\0"),
2638        ] {
2639            assert!(messenger
2640                .send_peer("peer-left", "peer-right", operation, message)
2641                .await
2642                .is_err());
2643        }
2644        assert!(messenger
2645            .send_peer(
2646                "peer-left",
2647                "peer-right",
2648                "oversized",
2649                &"x".repeat(MAX_PEER_MESSAGE_BYTES + 1)
2650            )
2651            .await
2652            .is_err());
2653    }
2654
2655    #[tokio::test]
2656    async fn peer_api_retry_dedupes_before_claim_and_after_ack_restart_but_rejects_changed_payload()
2657    {
2658        let (temp, store, activation, messenger) = fixture().await;
2659        seed_peer_tree(store.as_ref()).await;
2660        let first = messenger
2661            .send_peer("peer-left", "peer-right", "stable-operation", "original")
2662            .await
2663            .unwrap();
2664        let retry = messenger
2665            .send_peer("peer-left", "peer-right", "stable-operation", "original")
2666            .await
2667            .unwrap();
2668        assert_eq!(retry.delivery, first.delivery);
2669        assert!(matches!(
2670            messenger
2671                .send_peer("peer-left", "peer-right", "stable-operation", "changed")
2672                .await,
2673            Err(SessionMessengerError::Inbox(
2674                SessionInboxError::InvalidClaim(_)
2675            ))
2676        ));
2677        assert_eq!(
2678            messenger
2679                .inbox()
2680                .inspect("peer-right")
2681                .await
2682                .unwrap()
2683                .generation,
2684            1
2685        );
2686        admit_peer_for_test(store.as_ref(), messenger.inbox().as_ref(), "peer-right").await;
2687        drop(messenger);
2688        drop(store);
2689        let reopened = Arc::new(SessionStoreV2::new(temp.path().into()).await.unwrap());
2690        let inbox = Arc::new(FileSessionInbox::new(
2691            reopened.clone(),
2692            SessionInboxLimits::default(),
2693        ));
2694        let messenger = SessionMessenger::new(reopened, inbox.clone(), activation);
2695        let after_ack = messenger
2696            .send_peer("peer-left", "peer-right", "stable-operation", "original")
2697            .await
2698            .unwrap();
2699        assert_eq!(after_ack.delivery, first.delivery);
2700        assert!(inbox.claim("peer-right", 16).await.unwrap().is_empty());
2701        assert!(matches!(
2702            messenger
2703                .send_peer("peer-left", "peer-right", "stable-operation", "changed")
2704                .await,
2705            Err(SessionMessengerError::Inbox(
2706                SessionInboxError::InvalidClaim(_)
2707            ))
2708        ));
2709        assert_eq!(inbox.inspect("peer-right").await.unwrap().generation, 1);
2710    }
2711
2712    #[tokio::test]
2713    async fn peer_api_reply_recovers_durable_recipient_and_thread_after_restart() {
2714        let (temp, store, activation, messenger) = fixture().await;
2715        seed_peer_tree(store.as_ref()).await;
2716        let first = messenger
2717            .send_peer("peer-left-leaf", "peer-right-leaf", "initial", "question")
2718            .await
2719            .unwrap();
2720        let incoming = admit_peer_for_test(
2721            store.as_ref(),
2722            messenger.inbox().as_ref(),
2723            "peer-right-leaf",
2724        )
2725        .await;
2726        drop(messenger);
2727        drop(store);
2728        let store = Arc::new(SessionStoreV2::new(temp.path().into()).await.unwrap());
2729        let inbox = Arc::new(FileSessionInbox::new(
2730            store.clone(),
2731            SessionInboxLimits::default(),
2732        ));
2733        let messenger = SessionMessenger::new(store.clone(), inbox.clone(), activation);
2734        let reply = messenger
2735            .reply_peer(
2736                "peer-right-leaf",
2737                first.delivery.id.as_str(),
2738                "answer",
2739                "answer text",
2740            )
2741            .await
2742            .unwrap();
2743        let retry = messenger
2744            .reply_peer(
2745                "peer-right-leaf",
2746                first.delivery.id.as_str(),
2747                "answer",
2748                "answer text",
2749            )
2750            .await
2751            .unwrap();
2752        assert_eq!(reply.delivery, retry.delivery);
2753        let response = admit_peer_for_test(store.as_ref(), inbox.as_ref(), "peer-left-leaf").await;
2754        assert_eq!(
2755            response.source,
2756            SessionMessageSource::Session {
2757                session_id: "peer-right-leaf".into()
2758            }
2759        );
2760        assert_eq!(response.in_reply_to, Some(incoming.id.clone()));
2761        assert_eq!(response.thread_id, incoming.thread_id);
2762        assert_eq!(response.correlation_id.as_deref(), Some("answer"));
2763        let second_reply = messenger
2764            .reply_peer("peer-left-leaf", response.id.as_str(), "thanks", "thanks")
2765            .await
2766            .unwrap();
2767        let second_response =
2768            admit_peer_for_test(store.as_ref(), inbox.as_ref(), "peer-right-leaf").await;
2769        assert_eq!(second_reply.delivery.id, second_response.id);
2770        assert_eq!(second_response.in_reply_to, Some(response.id));
2771        assert_eq!(
2772            second_response.thread_id.as_deref(),
2773            Some(first.delivery.id.as_str())
2774        );
2775        let after_ack = messenger
2776            .reply_peer(
2777                "peer-right-leaf",
2778                first.delivery.id.as_str(),
2779                "answer",
2780                "answer text",
2781            )
2782            .await
2783            .unwrap();
2784        assert_eq!(after_ack.delivery, reply.delivery);
2785        assert!(inbox.claim("peer-left-leaf", 16).await.unwrap().is_empty());
2786        assert!(messenger
2787            .reply_peer(
2788                "peer-right-leaf",
2789                first.delivery.id.as_str(),
2790                "answer",
2791                "different"
2792            )
2793            .await
2794            .is_err());
2795    }
2796
2797    #[tokio::test]
2798    async fn peer_api_reply_rejects_missing_forged_legacy_and_recreated_source() {
2799        let (_temp, store, activation, messenger) = fixture().await;
2800        seed_peer_tree(store.as_ref()).await;
2801        let mut receiver = store.load_session("peer-right").await.unwrap().unwrap();
2802        let legacy = peer("peer-left", "peer-right", "legacy-correction");
2803        receiver.add_message(legacy.to_provider_message().unwrap());
2804        let user = SessionMessageEnvelope::user_input("peer-right", "ordinary input");
2805        receiver.add_message(user.to_provider_message().unwrap());
2806        let sender = store.load_session("peer-left").await.unwrap().unwrap();
2807        let mut forged = SessionMessenger::peer_envelope(
2808            &sender,
2809            &receiver,
2810            "forged",
2811            "original",
2812            SessionMessenger::peer_message_id(&sender, &receiver, "forged"),
2813            "thread".into(),
2814            None,
2815        )
2816        .to_provider_message()
2817        .unwrap();
2818        forged.content = "changed without canonical proof".into();
2819        receiver.add_message(forged);
2820        store.save_session(&receiver).await.unwrap();
2821        for id in [
2822            "missing",
2823            legacy.id.as_str(),
2824            user.id.as_str(),
2825            receiver.messages.last().unwrap().id.as_str(),
2826        ] {
2827            assert!(messenger
2828                .reply_peer("peer-right", id, "deny-reply", "reply")
2829                .await
2830                .is_err());
2831        }
2832        let incoming = messenger
2833            .send_peer("peer-left", "peer-right", "old-lifetime", "question")
2834            .await
2835            .unwrap();
2836        admit_peer_for_test(store.as_ref(), messenger.inbox().as_ref(), "peer-right").await;
2837        let memory = Arc::new(PeerMemoryStorage::default());
2838        // Retain the actual saved receiver while replacing the sender's birth.
2839        for id in ["peer-root", "peer-left", "peer-right"] {
2840            let mut session = store.load_session(id).await.unwrap().unwrap();
2841            if id == "peer-left" {
2842                session.created_at += chrono::Duration::seconds(1);
2843            }
2844            memory.save_session(&session).await.unwrap();
2845        }
2846        let fenced = SessionMessenger::new(
2847            memory.clone(),
2848            messenger.inbox().clone(),
2849            activation.clone(),
2850        );
2851        assert!(matches!(
2852            fenced
2853                .reply_peer(
2854                    "peer-right",
2855                    incoming.delivery.id.as_str(),
2856                    "reply-old",
2857                    "reply"
2858                )
2859                .await,
2860            Err(SessionMessengerError::InvalidEnvelope(_))
2861        ));
2862        memory.delete_session("peer-left").await.unwrap();
2863        assert!(matches!(
2864            fenced
2865                .reply_peer(
2866                    "peer-right",
2867                    incoming.delivery.id.as_str(),
2868                    "reply-deleted",
2869                    "reply"
2870                )
2871                .await,
2872            Err(SessionMessengerError::TargetNotFound(_))
2873        ));
2874        assert_eq!(activation.calls.lock().await.len(), 1);
2875    }
2876
2877    #[tokio::test]
2878    async fn same_root_delivery_enqueues_then_requests_activation() {
2879        let (_temp, store, activation, messenger) = fixture().await;
2880        let root = Session::new("root", "model");
2881        let mut child = Session::new("child", "model");
2882        child.kind = bamboo_domain::SessionKind::Child;
2883        child.parent_session_id = Some("root".to_string());
2884        child.root_session_id = "root".to_string();
2885        store.save_session(&root).await.unwrap();
2886        store.save_session(&child).await.unwrap();
2887
2888        let receipt = messenger
2889            .send(peer("root", "child", "msg-1"))
2890            .await
2891            .unwrap();
2892        assert_eq!(receipt.delivery.generation, 1);
2893        assert_eq!(
2894            activation.calls.lock().await.as_slice(),
2895            &[("child".to_string(), 1)]
2896        );
2897        assert_eq!(messenger.metrics().snapshot().delivered, 1);
2898    }
2899
2900    #[tokio::test]
2901    async fn staged_delivery_without_watermark_reports_exact_retry_required() {
2902        let temp = TempDir::new().unwrap();
2903        let store = Arc::new(
2904            SessionStoreV2::new(temp.path().to_path_buf())
2905                .await
2906                .unwrap(),
2907        );
2908        let root = Session::new("root", "model");
2909        let mut child = Session::new("child", "model");
2910        child.kind = bamboo_domain::SessionKind::Child;
2911        child.parent_session_id = Some("root".to_string());
2912        child.root_session_id = "root".to_string();
2913        store.save_session(&root).await.unwrap();
2914        store.save_session(&child).await.unwrap();
2915        let inbox = Arc::new(FailEligibilityOnce {
2916            inner: Arc::new(FileSessionInbox::new(
2917                store.clone(),
2918                SessionInboxLimits::default(),
2919            )),
2920            fail_next: std::sync::atomic::AtomicBool::new(true),
2921        });
2922        let activation = Arc::new(RecordingActivation {
2923            calls: tokio::sync::Mutex::new(Vec::new()),
2924        });
2925        let messenger = SessionMessenger::new(store.clone(), inbox, activation.clone());
2926        let envelope = peer("root", "child", "watermark-failure");
2927        let gate = bamboo_domain::AdmissionGate::default();
2928
2929        let admission = messenger
2930            .admit_with_gate(envelope.clone(), Some(&gate))
2931            .await
2932            .unwrap();
2933        let receipt = match messenger.activate(&admission).await.unwrap_err() {
2934            SessionMessengerError::ActivationEligibility { receipt, .. } => receipt,
2935            error => panic!("expected retry-required classification: {error}"),
2936        };
2937        assert!(gate.is_committed());
2938        assert!(activation.calls.lock().await.is_empty());
2939        let reopened = FileSessionInbox::new(store, SessionInboxLimits::default());
2940        let backlog = reopened.inspect("child").await.unwrap();
2941        assert_eq!(backlog.pending + backlog.claimed, 1);
2942        assert_eq!(backlog.activation_generation, 0);
2943        assert_eq!(backlog.generation, receipt.generation);
2944
2945        let retry = messenger
2946            .admit_with_gate(envelope, Some(&gate))
2947            .await
2948            .unwrap();
2949        let retried = messenger.activate(&retry).await.unwrap();
2950        assert_eq!(retried.delivery, receipt);
2951        assert_eq!(activation.calls.lock().await.len(), 1);
2952        let reopened = reopened.inspect("child").await.unwrap();
2953        assert_eq!(reopened.activation_generation, receipt.generation);
2954        assert_eq!(reopened.pending + reopened.claimed, 1);
2955    }
2956
2957    #[tokio::test]
2958    async fn cross_root_peer_is_rejected_before_enqueue() {
2959        let (_temp, store, activation, messenger) = fixture().await;
2960        store
2961            .save_session(&Session::new("root-a", "model"))
2962            .await
2963            .unwrap();
2964        store
2965            .save_session(&Session::new("root-b", "model"))
2966            .await
2967            .unwrap();
2968
2969        let error = messenger
2970            .send(peer("root-a", "root-b", "msg-2"))
2971            .await
2972            .unwrap_err();
2973        assert!(matches!(error, SessionMessengerError::Unauthorized { .. }));
2974        assert!(activation.calls.lock().await.is_empty());
2975        let metrics = messenger.metrics().snapshot();
2976        assert_eq!(metrics.rejected, 1);
2977        assert_eq!(metrics.unauthorized, 1);
2978        assert_eq!(metrics.payload_too_large, 0);
2979        assert_eq!(metrics.backlog_full, 0);
2980    }
2981
2982    #[tokio::test]
2983    async fn same_root_different_project_peer_is_rejected_before_enqueue() {
2984        let (_temp, store, activation, messenger) = fixture().await;
2985        let mut source = Session::new("project-root", "model");
2986        source.set_project_id_meta("project-a");
2987        let mut target = Session::new("project-child", "model");
2988        target.kind = bamboo_domain::SessionKind::Child;
2989        target.parent_session_id = Some(source.id.clone());
2990        target.root_session_id = source.id.clone();
2991        target.set_project_id_meta("project-b");
2992        store.save_session(&source).await.unwrap();
2993        store.save_session(&target).await.unwrap();
2994
2995        let error = messenger
2996            .send(peer(&source.id, &target.id, "different-project"))
2997            .await
2998            .unwrap_err();
2999        assert!(matches!(error, SessionMessengerError::Unauthorized { .. }));
3000        assert!(activation.calls.lock().await.is_empty());
3001        let metrics = messenger.metrics().snapshot();
3002        assert_eq!(metrics.delivered, 0);
3003        assert_eq!(metrics.rejected, 1);
3004        assert_eq!(metrics.unauthorized, 1);
3005    }
3006
3007    #[tokio::test]
3008    async fn limit_rejections_have_distinct_metrics() {
3009        let temp = TempDir::new().unwrap();
3010        let store = Arc::new(
3011            SessionStoreV2::new(temp.path().to_path_buf())
3012                .await
3013                .unwrap(),
3014        );
3015        store
3016            .save_session(&Session::new("target", "model"))
3017            .await
3018            .unwrap();
3019        let activation = Arc::new(RecordingActivation {
3020            calls: tokio::sync::Mutex::new(Vec::new()),
3021        });
3022        let inbox = Arc::new(FileSessionInbox::new(
3023            store.clone(),
3024            SessionInboxLimits {
3025                max_payload_bytes: 512,
3026                max_backlog: 1,
3027                max_claim_batch: 1,
3028            },
3029        ));
3030        let messenger = SessionMessenger::new(store, inbox, activation.clone());
3031
3032        let oversized = SessionMessageEnvelope::user_input("target", "x".repeat(2048));
3033        assert!(matches!(
3034            messenger.send(oversized).await,
3035            Err(SessionMessengerError::Inbox(
3036                SessionInboxError::PayloadTooLarge { .. }
3037            ))
3038        ));
3039        messenger
3040            .send(SessionMessageEnvelope::user_input("target", "first"))
3041            .await
3042            .unwrap();
3043        assert!(matches!(
3044            messenger
3045                .send(SessionMessageEnvelope::user_input("target", "second"))
3046                .await,
3047            Err(SessionMessengerError::Inbox(
3048                SessionInboxError::BacklogFull { .. }
3049            ))
3050        ));
3051
3052        let metrics = messenger.metrics().snapshot();
3053        assert_eq!(metrics.rejected, 2);
3054        assert_eq!(metrics.payload_too_large, 1);
3055        assert_eq!(metrics.backlog_full, 1);
3056        assert_eq!(activation.calls.lock().await.len(), 1);
3057    }
3058
3059    #[tokio::test]
3060    async fn explicit_peer_intent_keeps_legacy_compatibility_and_fences_changed_retry_after_ack() {
3061        let (_temp, store, activation, messenger) = fixture().await;
3062        seed_peer_tree(store.as_ref()).await;
3063        let receipt = messenger
3064            .send_peer_with_intent(
3065                "peer-left",
3066                "peer-right",
3067                "queue-operation",
3068                "input",
3069                SessionMessageDeliveryIntent::QueueOnly,
3070            )
3071            .await
3072            .unwrap();
3073        assert_eq!(receipt.activation, None);
3074        assert!(!receipt.active_delivery_notified);
3075        assert!(activation.calls.lock().await.is_empty());
3076        let original =
3077            admit_peer_for_test(store.as_ref(), messenger.inbox().as_ref(), "peer-right").await;
3078        assert_eq!(
3079            messenger
3080                .send_peer_with_intent(
3081                    "peer-left",
3082                    "peer-right",
3083                    "queue-operation",
3084                    "input",
3085                    SessionMessageDeliveryIntent::QueueOnly
3086                )
3087                .await
3088                .unwrap()
3089                .delivery,
3090            receipt.delivery
3091        );
3092        assert!(messenger
3093            .send_peer_with_intent(
3094                "peer-left",
3095                "peer-right",
3096                "queue-operation",
3097                "input",
3098                SessionMessageDeliveryIntent::TriggerTurn
3099            )
3100            .await
3101            .is_err());
3102        assert!(activation.calls.lock().await.is_empty());
3103        let reply = messenger
3104            .reply_peer_with_intent(
3105                "peer-right",
3106                original.id.as_str(),
3107                "reply-operation",
3108                "reply",
3109                SessionMessageDeliveryIntent::TriggerTurn,
3110            )
3111            .await
3112            .unwrap();
3113        // This fixture records activation requests as reservations. Active
3114        // owner delivery is covered by the real router's safe-point tests.
3115        assert_eq!(
3116            reply.activation,
3117            Some(SessionActivationDisposition::ActivationReserved)
3118        );
3119        assert_eq!(
3120            activation.calls.lock().await.as_slice(),
3121            &[("peer-left".to_string(), reply.delivery.generation)]
3122        );
3123        let claims = messenger.inbox().claim("peer-left", 8).await.unwrap();
3124        assert_eq!(claims[0].envelope.in_reply_to, Some(original.id));
3125        assert_eq!(
3126            claims[0].activation_policy,
3127            SessionActivationPolicy::RespectSpecificWait
3128        );
3129        messenger
3130            .send_peer("peer-left", "peer-right", "legacy-operation", "legacy")
3131            .await
3132            .unwrap();
3133        let claims = messenger.inbox().claim("peer-right", 8).await.unwrap();
3134        assert_eq!(
3135            claims[0].activation_policy,
3136            SessionActivationPolicy::InterruptSpecificWait
3137        );
3138    }
3139
3140    #[tokio::test]
3141    async fn explicit_peer_intents_deny_foreign_roots_before_admission_or_wake() {
3142        let (_temp, store, activation, messenger) = fixture().await;
3143        seed_peer_tree(store.as_ref()).await;
3144        store
3145            .save_session(&Session::new("foreign-root", "model"))
3146            .await
3147            .unwrap();
3148        for intent in [
3149            SessionMessageDeliveryIntent::QueueOnly,
3150            SessionMessageDeliveryIntent::TriggerTurn,
3151        ] {
3152            assert!(matches!(
3153                messenger
3154                    .send_peer_with_intent("peer-left", "foreign-root", "denied", "input", intent)
3155                    .await,
3156                Err(SessionMessengerError::Unauthorized { .. })
3157            ));
3158        }
3159        assert_eq!(
3160            messenger
3161                .inbox()
3162                .inspect("foreign-root")
3163                .await
3164                .unwrap()
3165                .pending,
3166            0
3167        );
3168        assert!(activation.calls.lock().await.is_empty());
3169    }
3170}