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, SessionInboxError, SessionInboxPort,
16    SessionInboxReceipt, SessionMessageBody, SessionMessageContent, SessionMessageDeliveryIntent,
17    SessionMessageEnvelope, SessionMessageId, SessionMessageKind, SessionMessageSource,
18    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 = 16 * 1024;
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        )
349        .await
350        .map(SessionPeerDeliveryReceipt::into_legacy)
351    }
352
353    /// Explicit scheduling over the same lifetime-fenced Inbox. Omission on
354    /// the old send_peer API keeps legacy interrupt/activation behavior.
355    pub async fn send_peer_with_intent(
356        &self,
357        caller_session_id: &str,
358        target_session_id: &str,
359        operation_id: &str,
360        message: &str,
361        intent: SessionMessageDeliveryIntent,
362    ) -> Result<SessionPeerDeliveryReceipt, SessionMessengerError> {
363        self.send_peer_delivery(
364            caller_session_id,
365            target_session_id,
366            operation_id,
367            message,
368            Some(intent),
369        )
370        .await
371    }
372
373    /// Active-run-only peer input. The Inbox owns exact retry recovery and
374    /// retains its original authority through the runtime's publication CAS.
375    /// This never cancels a run, requests activation or reconstructs an owner.
376    pub async fn steer_peer(
377        &self,
378        caller_session_id: &str,
379        target_session_id: &str,
380        operation_id: &str,
381        message: &str,
382        expected_run_id: &str,
383    ) -> Result<bamboo_domain::SessionInboxSteerReceipt, SessionMessengerError> {
384        Self::validate_peer_input(operation_id, message)?;
385        if !bamboo_domain::valid_session_run_id(expected_run_id) {
386            return Err(SessionMessengerError::InvalidEnvelope(
387                "invalid expected run id".into(),
388            ));
389        }
390        let (caller, target) = self.peer_pair(caller_session_id, target_session_id).await?;
391        let id = Self::peer_message_id(&caller, &target, operation_id);
392        let envelope = Self::peer_envelope(
393            &caller,
394            &target,
395            operation_id,
396            message,
397            id.clone(),
398            id.to_string(),
399            None,
400        );
401        let source = ActorSession::from_session(&caller)
402            .map_err(|error| SessionMessengerError::InvalidEnvelope(error.to_string()))?;
403        let target = ActorSession::from_session(&target)
404            .map_err(|error| SessionMessengerError::InvalidEnvelope(error.to_string()))?;
405        let started = Instant::now();
406        let receipt = self
407            .inbox
408            .deliver_peer_steer(
409                &envelope,
410                &source,
411                &target,
412                expected_run_id,
413                self.activation.clone(),
414            )
415            .await
416            .map_err(|error| {
417                let error = SessionMessengerError::Inbox(error);
418                self.record_rejection(&error);
419                error
420            })?;
421        self.record_admission(envelope, receipt.delivery.clone(), started);
422        Ok(receipt)
423    }
424
425    async fn send_peer_delivery(
426        &self,
427        caller_session_id: &str,
428        target_session_id: &str,
429        operation_id: &str,
430        message: &str,
431        intent: Option<SessionMessageDeliveryIntent>,
432    ) -> Result<SessionPeerDeliveryReceipt, SessionMessengerError> {
433        Self::validate_peer_input(operation_id, message)?;
434        let (caller, target) = self.peer_pair(caller_session_id, target_session_id).await?;
435        let id = Self::peer_message_id(&caller, &target, operation_id);
436        let envelope = Self::peer_envelope(
437            &caller,
438            &target,
439            operation_id,
440            message,
441            id.clone(),
442            id.to_string(),
443            None,
444        );
445        self.send_peer_envelope(envelope, &caller, &target, None, intent)
446            .await
447    }
448
449    /// Reply only to a canonical received peer envelope in the caller's saved
450    /// transcript. Recipient and thread edges are never supplied by the model.
451    pub async fn reply_peer(
452        &self,
453        caller_session_id: &str,
454        received_message_id: &str,
455        operation_id: &str,
456        message: &str,
457    ) -> Result<SessionMessengerReceipt, SessionMessengerError> {
458        self.reply_peer_delivery(
459            caller_session_id,
460            received_message_id,
461            operation_id,
462            message,
463            None,
464        )
465        .await
466        .map(SessionPeerDeliveryReceipt::into_legacy)
467    }
468
469    /// Derive recipient/thread from the canonical received peer message while
470    /// opting into explicit scheduling. Reply observations never grant authority.
471    pub async fn reply_peer_with_intent(
472        &self,
473        caller_session_id: &str,
474        received_message_id: &str,
475        operation_id: &str,
476        message: &str,
477        intent: SessionMessageDeliveryIntent,
478    ) -> Result<SessionPeerDeliveryReceipt, SessionMessengerError> {
479        self.reply_peer_delivery(
480            caller_session_id,
481            received_message_id,
482            operation_id,
483            message,
484            Some(intent),
485        )
486        .await
487    }
488
489    async fn reply_peer_delivery(
490        &self,
491        caller_session_id: &str,
492        received_message_id: &str,
493        operation_id: &str,
494        message: &str,
495        intent: Option<SessionMessageDeliveryIntent>,
496    ) -> Result<SessionPeerDeliveryReceipt, SessionMessengerError> {
497        Self::validate_peer_input(operation_id, message)?;
498        Self::validate_peer_session_id(caller_session_id)?;
499        let received_id = SessionMessageId::parse(received_message_id)
500            .map_err(|error| SessionMessengerError::InvalidEnvelope(error.to_string()))?;
501        let received = self
502            .load_session(caller_session_id)
503            .await?
504            .ok_or_else(|| SessionMessengerError::SourceNotFound(caller_session_id.into()))?;
505        let original = received
506            .messages
507            .iter()
508            .filter(|message| message.id == received_message_id)
509            .find_map(|message| {
510                let envelope: SessionMessageEnvelope = serde_json::from_value(
511                    message.metadata.as_ref()?.get("session_message")?.clone(),
512                )
513                .ok()?;
514                (envelope.id == received_id
515                    && envelope.kind == SessionMessageKind::PeerMessage
516                    && envelope.target_session_id == caller_session_id
517                    && bamboo_domain::is_matching_session_message(message, &envelope))
518                .then_some(envelope)
519            })
520            .ok_or_else(|| {
521                SessionMessengerError::InvalidEnvelope(
522                    "reply requires a canonical received peer message".into(),
523                )
524            })?;
525        let SessionMessageSource::Session {
526            session_id: target_id,
527        } = &original.source
528        else {
529            return Err(SessionMessengerError::InvalidEnvelope(
530                "reply has no logical sender".into(),
531            ));
532        };
533        let (caller, target) = self.peer_pair(caller_session_id, target_id).await?;
534        let thread = original.thread_id.clone().ok_or_else(|| {
535            SessionMessengerError::InvalidEnvelope("reply requires a threaded peer message".into())
536        })?;
537        let original_operation = original.correlation_id.as_deref().ok_or_else(|| {
538            SessionMessengerError::InvalidEnvelope(
539                "reply requires a peer operation identity".into(),
540            )
541        })?;
542        if original.id != Self::peer_message_id(&target, &caller, original_operation) {
543            return Err(SessionMessengerError::InvalidEnvelope(
544                "received peer message belongs to a different session lifetime".into(),
545            ));
546        }
547        SessionMessageId::parse(&thread)
548            .map_err(|error| SessionMessengerError::InvalidEnvelope(error.to_string()))?;
549        let envelope = Self::peer_envelope(
550            &caller,
551            &target,
552            operation_id,
553            message,
554            Self::peer_message_id(&caller, &target, operation_id),
555            thread,
556            Some(original.id.clone()),
557        );
558        self.send_peer_envelope(envelope, &caller, &target, Some(&original), intent)
559            .await
560    }
561
562    fn validate_peer_input(operation_id: &str, message: &str) -> Result<(), SessionMessengerError> {
563        SessionMessageId::parse(operation_id)
564            .map_err(|error| SessionMessengerError::InvalidEnvelope(error.to_string()))?;
565        if message.trim().is_empty()
566            || message.len() > MAX_PEER_MESSAGE_BYTES
567            || message.contains('\0')
568        {
569            return Err(SessionMessengerError::InvalidEnvelope(format!(
570                "peer message must contain 1..={MAX_PEER_MESSAGE_BYTES} UTF-8 bytes without NUL"
571            )));
572        }
573        Ok(())
574    }
575
576    fn validate_peer_session_id(id: &str) -> Result<(), SessionMessengerError> {
577        bamboo_domain::validate_supervisor_target_id(id)
578            .map_err(|error| SessionMessengerError::InvalidEnvelope(error.into()))?;
579        SessionMessageId::parse(id)
580            .map(|_| ())
581            .map_err(|error| SessionMessengerError::InvalidEnvelope(error.to_string()))
582    }
583
584    fn bounded_peer_label(value: &str, max_bytes: usize) -> String {
585        let mut end = value.len().min(max_bytes);
586        while !value.is_char_boundary(end) {
587            end -= 1;
588        }
589        value[..end].to_owned()
590    }
591
592    async fn peer_member(&self, id: &str, source: bool) -> Result<Session, SessionMessengerError> {
593        Self::validate_peer_session_id(id)?;
594        let session = self
595            .sessions
596            .load_runtime_control_plane(id)
597            .await
598            .map_err(|error| SessionMessengerError::Storage(error.to_string()))?
599            .ok_or_else(|| {
600                if source {
601                    SessionMessengerError::SourceNotFound(id.into())
602                } else {
603                    SessionMessengerError::TargetNotFound(id.into())
604                }
605            })?;
606        let unauthorized = || SessionMessengerError::Unauthorized {
607            source_session_id: id.into(),
608            target: session.root_session_id.clone(),
609        };
610        if session.id != id
611            || session
612                .runtime_metadata
613                .as_ref()
614                .and_then(|meta| meta.project_id.as_ref())
615                .zip(session.metadata.get("project_id"))
616                .is_some_and(|(typed, legacy)| typed != legacy)
617            || bamboo_domain::ActorSession::from_session(&session).is_err()
618        {
619            return Err(unauthorized());
620        }
621        if session.kind == bamboo_domain::SessionKind::Root {
622            return Ok(session);
623        }
624        crate::session_app::child_session::owned_tree::load_owned_descendant(
625            &PeerTreePort(self.sessions.as_ref()),
626            &session.root_session_id,
627            id,
628        )
629        .await
630        .map_err(|_| unauthorized())
631    }
632
633    async fn peer_pair(
634        &self,
635        caller_id: &str,
636        target_id: &str,
637    ) -> Result<(Session, Session), SessionMessengerError> {
638        let caller = self.peer_member(caller_id, true).await?;
639        let target = self.peer_member(target_id, false).await?;
640        if caller_id == target_id
641            || caller.root_session_id != target.root_session_id
642            || caller.project_id_meta() != target.project_id_meta()
643        {
644            return Err(SessionMessengerError::Unauthorized {
645                source_session_id: caller_id.into(),
646                target: target_id.into(),
647            });
648        }
649        Ok((caller, target))
650    }
651
652    fn peer_message_id(caller: &Session, target: &Session, operation_id: &str) -> SessionMessageId {
653        SessionMessageId::stable(
654            "session-peer-v1",
655            &serde_json::json!({
656                "source": caller.id, "source_birth": caller.created_at,
657                "target": target.id, "target_birth": target.created_at,
658                "operation_id": operation_id,
659            }),
660        )
661    }
662
663    #[allow(clippy::too_many_arguments)]
664    fn peer_envelope(
665        caller: &Session,
666        target: &Session,
667        operation_id: &str,
668        message: &str,
669        id: SessionMessageId,
670        thread_id: String,
671        in_reply_to: Option<SessionMessageId>,
672    ) -> SessionMessageEnvelope {
673        SessionMessageEnvelope {
674            id,
675            source: SessionMessageSource::Session {
676                session_id: caller.id.clone(),
677            },
678            target_session_id: target.id.clone(),
679            kind: SessionMessageKind::PeerMessage,
680            body: SessionMessageBody::Content(SessionMessageContent::text(message)),
681            created_at: chrono::Utc::now(),
682            thread_id: Some(thread_id),
683            in_reply_to,
684            attempt: None,
685            correlation_id: Some(operation_id.into()),
686        }
687    }
688
689    async fn send_peer_envelope(
690        &self,
691        envelope: SessionMessageEnvelope,
692        caller: &Session,
693        target: &Session,
694        reply_to: Option<&SessionMessageEnvelope>,
695        intent: Option<SessionMessageDeliveryIntent>,
696    ) -> Result<SessionPeerDeliveryReceipt, SessionMessengerError> {
697        let started = Instant::now();
698        let source = ActorSession::from_session(caller)
699            .map_err(|error| SessionMessengerError::InvalidEnvelope(error.to_string()))?;
700        let target = ActorSession::from_session(target)
701            .map_err(|error| SessionMessengerError::InvalidEnvelope(error.to_string()))?;
702        // These are the same births used for the stable message id. The inbox
703        // must revalidate them under its retained lifecycle authority; a fresh
704        // relationship read before generic delivery would still race rebirth.
705        let result = match intent {
706            Some(intent) => {
707                self.inbox
708                    .deliver_peer_message_with_intent(&envelope, &source, &target, reply_to, intent)
709                    .await
710            }
711            None => {
712                self.inbox
713                    .deliver_peer_message(&envelope, &source, &target, reply_to)
714                    .await
715                    .map(|delivery| bamboo_domain::SessionInboxDeliveryReceipt {
716                        delivery,
717                        // Preserve the historical peer activation contract.
718                        pending: true,
719                    })
720            }
721        };
722        let delivery = match result {
723            Ok(receipt) => receipt,
724            Err(error) => {
725                let error = SessionMessengerError::Inbox(error);
726                self.record_rejection(&error);
727                return Err(error);
728            }
729        };
730        let pending = delivery.pending;
731        let admission = self.record_admission(envelope, delivery.delivery, started);
732        // Exact explicit retries of ACKed/cancelled/dead deliveries cannot
733        // revive input or request work from a fresh router. The Inbox observed
734        // this state while retaining the original admission authority/locks.
735        if !pending {
736            return Ok(SessionPeerDeliveryReceipt {
737                delivery: admission.delivery,
738                activation: None,
739                active_delivery_notified: false,
740            });
741        }
742        if intent == Some(SessionMessageDeliveryIntent::QueueOnly) {
743            let active_delivery_notified = self
744                .activation
745                .notify_active_delivery(&admission.target_session_id, admission.delivery.generation)
746                .await;
747            return Ok(SessionPeerDeliveryReceipt {
748                delivery: admission.delivery,
749                activation: None,
750                active_delivery_notified,
751            });
752        }
753        self.activate_prepared(&admission).await.map(Into::into)
754    }
755
756    /// Original executing Supervisor control over one separately host-granted
757    /// live Root. Enqueue/notification is never a configuration application
758    /// receipt. Reads use a fresh canonical target snapshot under both fences.
759    pub async fn supervisor_loop_control(
760        &self,
761        supervisor: &SupervisorReference,
762        target_session_id: &str,
763        expected_link_revision: Option<u64>,
764        command: &bamboo_domain::LoopControlRequest,
765        receipt_operation_id: Option<&str>,
766    ) -> Result<serde_json::Value, SessionMessengerError> {
767        use bamboo_domain::{LoopControlAction, LoopControlRequest, ScopedLoopControlInput};
768        let invalid = |message: String| SessionMessengerError::InvalidEnvelope(message);
769        bamboo_domain::validate_supervisor_target_id(target_session_id)
770            .map_err(|error| invalid(error.into()))?;
771        bamboo_domain::validate_scoped_loop_control_command(command).map_err(invalid)?;
772        let status = matches!(command, LoopControlRequest::Status);
773        if expected_link_revision == Some(0)
774            || (!status && expected_link_revision.is_none())
775            || (!status && receipt_operation_id.is_some())
776        {
777            return Err(invalid("loop mutations require the exact nonzero link revision; receipt lookup is status-only".into()));
778        }
779        if let Some(operation) = receipt_operation_id {
780            uuid::Uuid::parse_str(operation)
781                .map_err(|_| invalid("receipt operation must be a UUID".into()))?;
782        }
783        let binding = self
784            .activation
785            .observe_supervisor_loop(
786                supervisor,
787                target_session_id,
788                expected_link_revision,
789                LoopControlAction::of(command),
790            )
791            .await?;
792        binding.authorize(command).map_err(invalid)?;
793        let canonical = self
794            .sessions
795            .load_scoped_loop_session(&binding, self.activation.clone())
796            .await
797            .map_err(|error| SessionMessengerError::Storage(error.to_string()))?
798            .ok_or_else(|| SessionMessengerError::TargetNotFound(target_session_id.into()))?;
799        if status {
800            let mut snapshot = serde_json::to_value(
801                crate::runtime::loop_control::inspect_loop(&canonical)
802                    .map_err(|error| SessionMessengerError::Storage(error.to_string()))?,
803            )
804            .map_err(|error| SessionMessengerError::Storage(error.to_string()))?;
805            // Bounded readback belongs to the initiating Supervisor's output
806            // budget. The target's canonical objective bytes remain unchanged.
807            for field in ["objective", "pending_objective"] {
808                let text = snapshot[field].as_str().map(str::to_owned);
809                if let Some(text) = text {
810                    let mut end = text.len().min(1024);
811                    while !text.is_char_boundary(end) {
812                        end -= 1;
813                    }
814                    snapshot[field] = serde_json::Value::String(text[..end].into());
815                    snapshot[format!("{field}_truncated")] =
816                        serde_json::Value::Bool(end < text.len());
817                }
818            }
819            let records = bamboo_domain::scoped_loop_consumptions(&canonical)
820                .map_err(SessionMessengerError::Storage)?;
821            let partial_checkpoint = records.iter().any(|record| {
822                record.binding == binding
823                    && receipt_operation_id.is_none_or(|id| id == record.operation_id)
824                    && !canonical
825                        .session_inbox_admission()
826                        .is_some_and(|cursor| cursor.contains(&record.input_id))
827            });
828            let record = records.into_iter().rev().find(|record| {
829                record.binding == binding
830                    && receipt_operation_id.is_none_or(|id| id == record.operation_id)
831                    && canonical
832                        .session_inbox_admission()
833                        .is_some_and(|cursor| cursor.contains(&record.input_id))
834            });
835            let state = crate::runtime::counted_loop::read_loop_state(&canonical)
836                .map_err(|error| SessionMessengerError::Storage(error.to_string()))?;
837            let objective_applied_actual = record
838                .as_ref()
839                .filter(|record| record.configured)
840                .and_then(|record| record.receipt.as_ref())
841                .zip(state.as_ref())
842                .is_some_and(|(receipt, state)| {
843                    receipt["generation"].as_str() == Some(state.generation.as_str())
844                        && canonical.messages.iter().any(|message| {
845                            message.role == bamboo_domain::Role::User
846                                && message.metadata.as_ref().is_some_and(|metadata| {
847                                    metadata["runtime_kind"].as_str()
848                                        == Some("counted_loop_iteration")
849                                        && metadata["loop_generation"] == receipt["generation"]
850                                        && metadata["objective_revision"]
851                                            == receipt["objective_revision"]
852                                })
853                        })
854                        && state.control.as_ref().is_some_and(|control| {
855                            receipt["accepted_revision"]
856                                .as_u64()
857                                .is_some_and(|revision| control.applied_revision >= revision)
858                                && receipt["objective_revision"]
859                                    .as_u64()
860                                    .is_some_and(|revision| {
861                                        revision > 0
862                                            && control.iteration_objective_revision >= revision
863                                    })
864                        })
865                });
866            return Ok(serde_json::json!({
867                "action":"loop", "command":"status", "target_session_id":target_session_id,
868                "link_revision":binding.link_revision, "owner_run_id":binding.owner_run_id,
869                "execution_id":binding.execution_id, "loop":snapshot,
870                "host_max_iterations":binding.max_iterations, "host_max_total_rounds":binding.max_total_rounds,
871                "configured":record.as_ref().is_some_and(|record|record.configured),
872                "checkpoint_observation":if partial_checkpoint { "partial_or_unknown" } else if record.is_some() { "confirmed" } else { "not_observed" },
873                "operation_receipt":record, "objective_applied_actual":objective_applied_actual,
874                "note":"Configuration receipts are immutable target checkpoints; objective_applied_actual additionally requires its real iteration-admission checkpoint. Neither is a task result."
875            }));
876        }
877        let input = ScopedLoopControlInput {
878            binding,
879            command: command.clone(),
880        };
881        let envelope = input.envelope().map_err(invalid)?;
882        if let Some(record) = bamboo_domain::scoped_loop_consumptions(&canonical)
883            .map_err(SessionMessengerError::Storage)?
884            .into_iter()
885            .find(|record| record.input_id == envelope.id)
886        {
887            if !bamboo_domain::has_scoped_loop_consumption(&canonical, &envelope) {
888                return Err(invalid(
889                    "operation payload conflicts with its immutable target receipt".into(),
890                ));
891            }
892            if canonical
893                .session_inbox_admission()
894                .is_some_and(|cursor| cursor.contains(&record.input_id))
895            {
896                return Ok(serde_json::json!({
897                    "action":"loop", "target_session_id":target_session_id,
898                    "operation_id":input.operation_id(), "receipt_id":envelope.id,
899                    "admitted":true, "replayed":true, "configured":record.configured,
900                    "operation_receipt":record, "objective_applied_actual":false,
901                    "note":"The original configuration receipt is replayed; use status for current objective application."
902                }));
903            }
904            // A runtime sidecar without Main's typed cursor is a partial
905            // publication, not a confirmed application. Exact delivery below
906            // recovers its original queued receipt without claiming success.
907        }
908        let started = Instant::now();
909        let receipt = self
910            .inbox
911            .deliver_supervisor_loop_control(&input.binding, &envelope, self.activation.clone())
912            .await?;
913        self.record_admission(envelope, receipt.delivery.clone(), started);
914        Ok(serde_json::json!({
915            "action":"loop", "target_session_id":target_session_id,
916            "operation_id":input.operation_id(), "receipt_id":receipt.delivery.id,
917            "generation":receipt.delivery.generation, "admitted":true,
918            "replayed":receipt.replayed, "active_delivery_notified":receipt.active_delivery_notified,
919            "configured":false, "target_application_observed":false,
920            "objective_applied_actual":false, "operation_receipt":null,
921            "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."
922        }))
923    }
924
925    /// Follow an already-attached Ordinary Root from the original executing
926    /// Supervisor. The concrete inbox revalidates and retains all authority
927    /// locks through durable admission; a prior inspect_link is never a grant.
928    pub async fn supervisor_followup(
929        &self,
930        supervisor: &SupervisorReference,
931        target_session_id: &str,
932        operation_id: &str,
933        message: &str,
934    ) -> Result<SessionMessengerReceipt, SessionMessengerError> {
935        bamboo_domain::validate_supervisor_target_id(target_session_id)
936            .map_err(|error| SessionMessengerError::InvalidEnvelope(error.to_string()))?;
937        SessionMessageId::parse(operation_id)
938            .map_err(|error| SessionMessengerError::InvalidEnvelope(error.to_string()))?;
939        if message.trim().is_empty() || message.len() > MAX_SUPERVISOR_FOLLOWUP_BYTES {
940            return Err(SessionMessengerError::InvalidEnvelope(format!(
941                "followup message must contain 1..={MAX_SUPERVISOR_FOLLOWUP_BYTES} UTF-8 bytes"
942            )));
943        }
944        let envelope = SessionMessageEnvelope {
945            id: SessionMessageId::stable(
946                "supervisor_followup",
947                &serde_json::json!({
948                    "supervisor": supervisor,
949                    "target_session_id": target_session_id,
950                    "operation_id": operation_id,
951                }),
952            ),
953            source: SessionMessageSource::Session {
954                session_id: supervisor.session_id.clone(),
955            },
956            target_session_id: target_session_id.to_string(),
957            kind: SessionMessageKind::PeerMessage,
958            body: SessionMessageBody::Content(SessionMessageContent::text(message)),
959            created_at: chrono::Utc::now(),
960            thread_id: None,
961            in_reply_to: None,
962            attempt: None,
963            correlation_id: Some(operation_id.to_string()),
964        };
965        let started = Instant::now();
966        let delivery = self
967            .inbox
968            .deliver_supervisor_followup(supervisor, &envelope)
969            .await
970            .map_err(|error| {
971                let error = SessionMessengerError::Inbox(error);
972                self.record_rejection(&error);
973                error
974            })?;
975        let admission = self.record_admission(envelope, delivery, started);
976        // Its specific-message RespectSpecificWait intent was published while
977        // holding Supervisor authority. This grants no staged sibling prefix.
978        self.activate_prepared(&admission).await
979    }
980
981    async fn load_session(&self, id: &str) -> Result<Option<Session>, SessionMessengerError> {
982        self.sessions
983            .load_session(id)
984            .await
985            .map_err(|error| SessionMessengerError::Storage(error.to_string()))
986    }
987
988    fn logical_root(session: &Session) -> &str {
989        if session.root_session_id.trim().is_empty() {
990            &session.id
991        } else {
992            &session.root_session_id
993        }
994    }
995
996    async fn validate_relationship(
997        &self,
998        envelope: &SessionMessageEnvelope,
999    ) -> Result<(), SessionMessengerError> {
1000        envelope
1001            .validate()
1002            .map_err(|error| SessionMessengerError::InvalidEnvelope(error.to_string()))?;
1003        let target = self
1004            .load_session(&envelope.target_session_id)
1005            .await?
1006            .ok_or_else(|| {
1007                SessionMessengerError::TargetNotFound(envelope.target_session_id.clone())
1008            })?;
1009
1010        let SessionMessageSource::Session { session_id } = &envelope.source else {
1011            return Ok(());
1012        };
1013        let source = self
1014            .load_session(session_id)
1015            .await?
1016            .ok_or_else(|| SessionMessengerError::SourceNotFound(session_id.clone()))?;
1017
1018        let same_root = Self::logical_root(&source) == Self::logical_root(&target);
1019        let source_project = source.project_id_meta();
1020        let target_project = target.project_id_meta();
1021        let project_compatible = match (source_project.as_deref(), target_project.as_deref()) {
1022            (Some(left), Some(right)) => left == right,
1023            // Older sessions did not persist project_id. Same-root ancestry is
1024            // the compatibility authority until both sides have migrated.
1025            _ => true,
1026        };
1027        if !same_root || !project_compatible {
1028            return Err(SessionMessengerError::Unauthorized {
1029                source_session_id: source.id,
1030                target: target.id,
1031            });
1032        }
1033        Ok(())
1034    }
1035
1036    fn record_rejection(&self, error: &SessionMessengerError) {
1037        self.metrics.rejected.fetch_add(1, Ordering::Relaxed);
1038        match error {
1039            SessionMessengerError::InvalidEnvelope(_) => {
1040                self.metrics
1041                    .invalid_envelope
1042                    .fetch_add(1, Ordering::Relaxed);
1043            }
1044            SessionMessengerError::Unauthorized { .. }
1045            | SessionMessengerError::SourceNotFound(_)
1046            | SessionMessengerError::TargetNotFound(_) => {
1047                self.metrics.unauthorized.fetch_add(1, Ordering::Relaxed);
1048            }
1049            SessionMessengerError::Inbox(SessionInboxError::AdmissionCancelled)
1050            | SessionMessengerError::Inbox(SessionInboxError::ExpectedRunConflict(_)) => {}
1051            SessionMessengerError::Inbox(SessionInboxError::PayloadTooLarge { .. }) => {
1052                self.metrics
1053                    .payload_too_large
1054                    .fetch_add(1, Ordering::Relaxed);
1055            }
1056            SessionMessengerError::Inbox(SessionInboxError::BacklogFull { .. }) => {
1057                self.metrics.backlog_full.fetch_add(1, Ordering::Relaxed);
1058            }
1059            SessionMessengerError::Inbox(_) | SessionMessengerError::Storage(_) => {
1060                self.metrics.storage_failed.fetch_add(1, Ordering::Relaxed);
1061            }
1062            SessionMessengerError::Activation { .. }
1063            | SessionMessengerError::ActivationEligibility { .. } => {}
1064        }
1065    }
1066
1067    pub async fn admit(
1068        &self,
1069        envelope: SessionMessageEnvelope,
1070    ) -> Result<SessionMessengerAdmission, SessionMessengerError> {
1071        self.admit_with_gate(envelope, None).await
1072    }
1073
1074    pub async fn admit_with_gate(
1075        &self,
1076        envelope: SessionMessageEnvelope,
1077        gate: Option<&bamboo_domain::AdmissionGate>,
1078    ) -> Result<SessionMessengerAdmission, SessionMessengerError> {
1079        self.admit_with_delivery_intent(envelope, gate, None).await
1080    }
1081
1082    /// Commit this specific message's immediate permission together with the
1083    /// envelope. Coordinators continue to use staged [`admit`](Self::admit).
1084    pub async fn admit_with_activation_intent(
1085        &self,
1086        envelope: SessionMessageEnvelope,
1087        policy: SessionActivationPolicy,
1088        gate: Option<&bamboo_domain::AdmissionGate>,
1089    ) -> Result<SessionMessengerAdmission, SessionMessengerError> {
1090        self.admit_with_delivery_intent(envelope, gate, Some(policy))
1091            .await
1092    }
1093
1094    async fn admit_with_delivery_intent(
1095        &self,
1096        envelope: SessionMessageEnvelope,
1097        gate: Option<&bamboo_domain::AdmissionGate>,
1098        policy: Option<SessionActivationPolicy>,
1099    ) -> Result<SessionMessengerAdmission, SessionMessengerError> {
1100        let started = Instant::now();
1101        if let Err(error) = self.validate_relationship(&envelope).await {
1102            self.record_rejection(&error);
1103            tracing::warn!(
1104                message_id = %envelope.id,
1105                target_session_id = %envelope.target_session_id,
1106                error = %error,
1107                "session message rejected"
1108            );
1109            return Err(error);
1110        }
1111
1112        let delivery = match (policy, gate) {
1113            (Some(policy), gate) => {
1114                self.inbox
1115                    .deliver_with_activation_intent(&envelope, policy, gate)
1116                    .await
1117            }
1118            (None, Some(gate)) => self.inbox.deliver_with_gate(&envelope, gate).await,
1119            (None, None) => self.inbox.deliver(&envelope).await,
1120        };
1121        let delivery = match delivery {
1122            Ok(receipt) => receipt,
1123            Err(error) => {
1124                let error = SessionMessengerError::Inbox(error);
1125                self.record_rejection(&error);
1126                return Err(error);
1127            }
1128        };
1129        Ok(self.record_admission(envelope, delivery, started))
1130    }
1131
1132    fn record_admission(
1133        &self,
1134        envelope: SessionMessageEnvelope,
1135        delivery: SessionInboxReceipt,
1136        started: Instant,
1137    ) -> SessionMessengerAdmission {
1138        self.metrics.delivered.fetch_add(1, Ordering::Relaxed);
1139        self.metrics.delivery_latency_micros.fetch_add(
1140            started.elapsed().as_micros().min(u128::from(u64::MAX)) as u64,
1141            Ordering::Relaxed,
1142        );
1143        SessionMessengerAdmission {
1144            envelope_id: envelope.id.as_str().to_string(),
1145            target_session_id: envelope.target_session_id,
1146            delivery,
1147        }
1148    }
1149
1150    pub async fn activate(
1151        &self,
1152        admission: &SessionMessengerAdmission,
1153    ) -> Result<SessionMessengerReceipt, SessionMessengerError> {
1154        self.activate_with_policy(admission, SessionActivationPolicy::InterruptSpecificWait)
1155            .await
1156    }
1157
1158    pub async fn activate_with_policy(
1159        &self,
1160        admission: &SessionMessengerAdmission,
1161        policy: SessionActivationPolicy,
1162    ) -> Result<SessionMessengerReceipt, SessionMessengerError> {
1163        if let Err(error) = self
1164            .inbox
1165            .mark_activation_eligible(
1166                &admission.target_session_id,
1167                admission.delivery.generation,
1168                policy,
1169            )
1170            .await
1171        {
1172            self.metrics
1173                .activation_failed
1174                .fetch_add(1, Ordering::Relaxed);
1175            return Err(SessionMessengerError::ActivationEligibility {
1176                receipt_id: admission.delivery.id.to_string(),
1177                receipt: admission.delivery.clone(),
1178                source: error,
1179            });
1180        }
1181        self.activate_prepared(admission).await
1182    }
1183
1184    /// Request execution for an admission whose activation policy was already
1185    /// durably recorded by a coordinator.
1186    pub async fn activate_prepared(
1187        &self,
1188        admission: &SessionMessengerAdmission,
1189    ) -> Result<SessionMessengerReceipt, SessionMessengerError> {
1190        let activation = match self
1191            .activation
1192            .request_activation(&admission.target_session_id, admission.delivery.generation)
1193            .await
1194        {
1195            Ok(disposition) => disposition,
1196            Err(source) => {
1197                self.metrics
1198                    .activation_failed
1199                    .fetch_add(1, Ordering::Relaxed);
1200                tracing::error!(
1201                    message_id = %admission.envelope_id,
1202                    target_session_id = %admission.target_session_id,
1203                    generation = admission.delivery.generation,
1204                    error = %source,
1205                    "session message durable but activation failed"
1206                );
1207                return Err(SessionMessengerError::Activation {
1208                    receipt_id: admission.delivery.id.to_string(),
1209                    receipt: admission.delivery.clone(),
1210                    source,
1211                });
1212            }
1213        };
1214        match activation {
1215            SessionActivationDisposition::ActiveNotified => {
1216                self.metrics.active_notified.fetch_add(1, Ordering::Relaxed);
1217            }
1218            SessionActivationDisposition::ActivationReserved => {
1219                self.metrics
1220                    .activation_reserved
1221                    .fetch_add(1, Ordering::Relaxed);
1222            }
1223            SessionActivationDisposition::ActivationCoalesced => {
1224                self.metrics
1225                    .activation_coalesced
1226                    .fetch_add(1, Ordering::Relaxed);
1227            }
1228        }
1229        tracing::info!(
1230            message_id = %admission.envelope_id,
1231            target_session_id = %admission.target_session_id,
1232            generation = admission.delivery.generation,
1233            ?activation,
1234            "session message durably delivered"
1235        );
1236        Ok(SessionMessengerReceipt {
1237            delivery: admission.delivery.clone(),
1238            activation,
1239        })
1240    }
1241
1242    /// Durably authorize the admitted queue prefix before a related
1243    /// control-plane transition is committed.
1244    ///
1245    /// Coordinators call this while their specific wait is still armed, then
1246    /// checkpoint the wait clear, and finally call [`activate`](Self::activate).
1247    /// A crash in either gap is recoverable: startup sees the watermark, while
1248    /// the real reservation adapter refuses to run until the specific wait has
1249    /// been durably cleared.
1250    pub async fn prepare_activation(
1251        &self,
1252        admission: &SessionMessengerAdmission,
1253    ) -> Result<(), SessionMessengerError> {
1254        self.inbox
1255            .mark_activation_eligible(
1256                &admission.target_session_id,
1257                admission.delivery.generation,
1258                SessionActivationPolicy::RespectSpecificWait,
1259            )
1260            .await
1261            .map_err(SessionMessengerError::Inbox)
1262    }
1263
1264    /// Authorize one staged delivery, preserving its immutable delivery
1265    /// intent and every sibling's existing execution permission.
1266    pub async fn prepare_message_activation(
1267        &self,
1268        admission: &SessionMessengerAdmission,
1269        policy: SessionActivationPolicy,
1270    ) -> Result<(), SessionMessengerError> {
1271        self.inbox
1272            .mark_message_activation_eligible(
1273                &admission.target_session_id,
1274                &admission.delivery,
1275                policy,
1276            )
1277            .await
1278            .map_err(SessionMessengerError::Inbox)
1279    }
1280
1281    pub async fn send(
1282        &self,
1283        envelope: SessionMessageEnvelope,
1284    ) -> Result<SessionMessengerReceipt, SessionMessengerError> {
1285        self.send_with_gate(envelope, None).await
1286    }
1287
1288    pub async fn send_with_gate(
1289        &self,
1290        envelope: SessionMessageEnvelope,
1291        gate: Option<&bamboo_domain::AdmissionGate>,
1292    ) -> Result<SessionMessengerReceipt, SessionMessengerError> {
1293        let admission = self
1294            .admit_with_activation_intent(
1295                envelope,
1296                SessionActivationPolicy::InterruptSpecificWait,
1297                gate,
1298            )
1299            .await?;
1300        self.activate_prepared(&admission).await
1301    }
1302
1303    pub async fn send_with_policy(
1304        &self,
1305        envelope: SessionMessageEnvelope,
1306        policy: SessionActivationPolicy,
1307    ) -> Result<SessionMessengerReceipt, SessionMessengerError> {
1308        let admission = self
1309            .admit_with_activation_intent(envelope, policy, None)
1310            .await?;
1311        self.activate_prepared(&admission).await
1312    }
1313}
1314
1315#[cfg(test)]
1316mod tests {
1317    use super::*;
1318    use async_trait::async_trait;
1319    use bamboo_domain::{
1320        SessionActivationDisposition, SessionInboxLimits, SessionMessageBody,
1321        SessionMessageContent, SessionMessageId, SessionMessageKind,
1322    };
1323    use bamboo_storage::{FileSessionInbox, SessionStoreV2};
1324    use tempfile::TempDir;
1325
1326    struct RecordingActivation {
1327        calls: tokio::sync::Mutex<Vec<(String, u64)>>,
1328    }
1329
1330    struct FailingActivation;
1331
1332    #[async_trait]
1333    impl SessionActivationPort for FailingActivation {
1334        async fn request_activation(
1335            &self,
1336            _target_session_id: &str,
1337            _inbox_generation: u64,
1338        ) -> Result<SessionActivationDisposition, SessionActivationError> {
1339            Err(SessionActivationError::Internal(
1340                "injected wakeup failure".into(),
1341            ))
1342        }
1343    }
1344
1345    #[tokio::test]
1346    async fn immediate_activation_failure_preserves_receipt_and_restart_permission() {
1347        let (temp, store, recording, _messenger) = fixture().await;
1348        store
1349            .save_session(&Session::new("root", "model"))
1350            .await
1351            .unwrap();
1352        let inbox = Arc::new(FileSessionInbox::new(
1353            store.clone(),
1354            SessionInboxLimits::default(),
1355        ));
1356        let messenger = SessionMessenger::new(store, inbox, Arc::new(FailingActivation));
1357        let envelope = SessionMessageEnvelope::user_input("root", "steer");
1358        let gate = bamboo_domain::AdmissionGate::default();
1359        let receipt = match messenger
1360            .send_with_gate(envelope.clone(), Some(&gate))
1361            .await
1362            .unwrap_err()
1363        {
1364            SessionMessengerError::Activation { receipt, .. } => receipt,
1365            error => panic!("expected durable delivery with failed wakeup: {error}"),
1366        };
1367        assert!(gate.is_committed());
1368        gate.cancel_if_pending();
1369        let restarted_store = Arc::new(SessionStoreV2::new(temp.path().into()).await.unwrap());
1370        let restarted = Arc::new(FileSessionInbox::new(
1371            restarted_store.clone(),
1372            SessionInboxLimits::default(),
1373        ));
1374        let backlog = restarted.inspect("root").await.unwrap();
1375        assert!(backlog.activation_pending());
1376        assert_eq!(backlog.coordinator_generation, 0);
1377        // This is the startup reconciliation request: the durable generation
1378        // comes from this message's intent while the coordinator prefix is 0.
1379        recording
1380            .request_activation("root", backlog.activation_generation)
1381            .await
1382            .unwrap();
1383        let retry = SessionMessenger::new(restarted_store, restarted.clone(), recording.clone());
1384        assert_eq!(
1385            retry
1386                .send_with_gate(envelope, Some(&gate))
1387                .await
1388                .unwrap()
1389                .delivery,
1390            receipt
1391        );
1392        assert_eq!(restarted.inspect("root").await.unwrap().pending, 1);
1393        assert_eq!(messenger.metrics().snapshot().activation_failed, 1);
1394        let claims = restarted.claim("root", 128).await.unwrap();
1395        assert_eq!(claims.len(), 1);
1396        assert_eq!(claims[0].envelope.id, receipt.id);
1397    }
1398
1399    struct FailEligibilityOnce {
1400        inner: Arc<FileSessionInbox>,
1401        fail_next: std::sync::atomic::AtomicBool,
1402    }
1403
1404    #[async_trait]
1405    impl SessionInboxPort for FailEligibilityOnce {
1406        async fn deliver(
1407            &self,
1408            envelope: &SessionMessageEnvelope,
1409        ) -> Result<SessionInboxReceipt, SessionInboxError> {
1410            self.inner.deliver(envelope).await
1411        }
1412
1413        async fn deliver_with_gate(
1414            &self,
1415            envelope: &SessionMessageEnvelope,
1416            gate: &bamboo_domain::AdmissionGate,
1417        ) -> Result<SessionInboxReceipt, SessionInboxError> {
1418            self.inner.deliver_with_gate(envelope, gate).await
1419        }
1420
1421        async fn mark_activation_eligible(
1422            &self,
1423            target_session_id: &str,
1424            generation: u64,
1425            policy: SessionActivationPolicy,
1426        ) -> Result<(), SessionInboxError> {
1427            if self.fail_next.swap(false, Ordering::SeqCst) {
1428                return Err(SessionInboxError::Storage(
1429                    "injected activation watermark failure".into(),
1430                ));
1431            }
1432            self.inner
1433                .mark_activation_eligible(target_session_id, generation, policy)
1434                .await
1435        }
1436
1437        async fn claim(
1438            &self,
1439            target_session_id: &str,
1440            limit: usize,
1441        ) -> Result<Vec<bamboo_domain::SessionInboxClaim>, SessionInboxError> {
1442            self.inner.claim(target_session_id, limit).await
1443        }
1444
1445        async fn was_admitted(
1446            &self,
1447            target_session_id: &str,
1448            id: &SessionMessageId,
1449        ) -> Result<bool, SessionInboxError> {
1450            self.inner.was_admitted(target_session_id, id).await
1451        }
1452
1453        async fn ack(
1454            &self,
1455            target_session_id: &str,
1456            claim: &bamboo_domain::SessionInboxClaim,
1457        ) -> Result<(), SessionInboxError> {
1458            self.inner.ack(target_session_id, claim).await
1459        }
1460
1461        async fn inspect(
1462            &self,
1463            target_session_id: &str,
1464        ) -> Result<bamboo_domain::SessionInboxBacklog, SessionInboxError> {
1465            self.inner.inspect(target_session_id).await
1466        }
1467    }
1468
1469    #[async_trait]
1470    impl SessionActivationPort for RecordingActivation {
1471        async fn request_activation(
1472            &self,
1473            target_session_id: &str,
1474            inbox_generation: u64,
1475        ) -> Result<SessionActivationDisposition, SessionActivationError> {
1476            self.calls
1477                .lock()
1478                .await
1479                .push((target_session_id.to_string(), inbox_generation));
1480            Ok(SessionActivationDisposition::ActivationReserved)
1481        }
1482    }
1483
1484    async fn fixture() -> (
1485        TempDir,
1486        Arc<SessionStoreV2>,
1487        Arc<RecordingActivation>,
1488        SessionMessenger,
1489    ) {
1490        let temp = TempDir::new().unwrap();
1491        let store = Arc::new(
1492            SessionStoreV2::new(temp.path().to_path_buf())
1493                .await
1494                .unwrap(),
1495        );
1496        let activation = Arc::new(RecordingActivation {
1497            calls: tokio::sync::Mutex::new(Vec::new()),
1498        });
1499        let inbox = Arc::new(FileSessionInbox::new(
1500            store.clone(),
1501            SessionInboxLimits::default(),
1502        ));
1503        let messenger = SessionMessenger::new(store.clone(), inbox, activation.clone());
1504        (temp, store, activation, messenger)
1505    }
1506
1507    fn peer(source: &str, target: &str, id: &str) -> SessionMessageEnvelope {
1508        SessionMessageEnvelope {
1509            id: SessionMessageId::parse(id).unwrap(),
1510            source: SessionMessageSource::Session {
1511                session_id: source.to_string(),
1512            },
1513            target_session_id: target.to_string(),
1514            kind: SessionMessageKind::PeerMessage,
1515            body: SessionMessageBody::Content(SessionMessageContent::text("hello")),
1516            created_at: chrono::Utc::now(),
1517            thread_id: None,
1518            in_reply_to: None,
1519            attempt: None,
1520            correlation_id: None,
1521        }
1522    }
1523
1524    #[derive(Default)]
1525    struct PeerMemoryStorage(tokio::sync::RwLock<std::collections::HashMap<String, Session>>);
1526
1527    #[async_trait]
1528    impl Storage for PeerMemoryStorage {
1529        async fn save_session(&self, session: &Session) -> std::io::Result<()> {
1530            self.0
1531                .write()
1532                .await
1533                .insert(session.id.clone(), session.clone());
1534            Ok(())
1535        }
1536
1537        async fn load_session(&self, id: &str) -> std::io::Result<Option<Session>> {
1538            Ok(self.0.read().await.get(id).cloned())
1539        }
1540
1541        async fn delete_session(&self, id: &str) -> std::io::Result<bool> {
1542            Ok(self.0.write().await.remove(id).is_some())
1543        }
1544
1545        async fn list_child_run_statuses(
1546            &self,
1547            parent: &str,
1548        ) -> std::io::Result<Vec<(String, Option<String>)>> {
1549            Ok(self
1550                .0
1551                .read()
1552                .await
1553                .values()
1554                .filter(|session| session.parent_session_id.as_deref() == Some(parent))
1555                .map(|session| (session.id.clone(), session.last_run_status()))
1556                .collect())
1557        }
1558    }
1559
1560    async fn seed_peer_tree(store: &dyn Storage) {
1561        let mut root = Session::new("peer-root", "model");
1562        root.title = "Logical root".into();
1563        root.set_project_id_meta("peer-project");
1564        let left = Session::new_child_of("peer-left", &root, "model", "Left");
1565        let right = Session::new_child_of("peer-right", &root, "model", "Right");
1566        let cousin_left = Session::new_child_of("peer-left-leaf", &left, "model", "Left leaf");
1567        let cousin_right = Session::new_child_of("peer-right-leaf", &right, "model", "Right leaf");
1568        for mut session in [root, left, right, cousin_left, cousin_right] {
1569            session.set_project_id_meta("peer-project");
1570            store.save_session(&session).await.unwrap();
1571        }
1572    }
1573
1574    struct PausedPeerAdmission {
1575        inner: Arc<FileSessionInbox>,
1576        entered: Arc<tokio::sync::Notify>,
1577        release: Arc<tokio::sync::Notify>,
1578    }
1579
1580    #[async_trait]
1581    impl SessionInboxPort for PausedPeerAdmission {
1582        async fn deliver(
1583            &self,
1584            envelope: &SessionMessageEnvelope,
1585        ) -> Result<SessionInboxReceipt, SessionInboxError> {
1586            self.inner.deliver(envelope).await
1587        }
1588
1589        async fn deliver_peer_message(
1590            &self,
1591            envelope: &SessionMessageEnvelope,
1592            source: &ActorSession,
1593            target: &ActorSession,
1594            reply_to: Option<&SessionMessageEnvelope>,
1595        ) -> Result<SessionInboxReceipt, SessionInboxError> {
1596            // The engine has already captured both births and built its id;
1597            // actual filesystem admission has not yet acquired its authority.
1598            self.entered.notify_one();
1599            self.release.notified().await;
1600            self.inner
1601                .deliver_peer_message(envelope, source, target, reply_to)
1602                .await
1603        }
1604
1605        async fn mark_activation_eligible(
1606            &self,
1607            target: &str,
1608            generation: u64,
1609            policy: SessionActivationPolicy,
1610        ) -> Result<(), SessionInboxError> {
1611            self.inner
1612                .mark_activation_eligible(target, generation, policy)
1613                .await
1614        }
1615
1616        async fn claim(
1617            &self,
1618            target: &str,
1619            limit: usize,
1620        ) -> Result<Vec<bamboo_domain::SessionInboxClaim>, SessionInboxError> {
1621            self.inner.claim(target, limit).await
1622        }
1623
1624        async fn was_admitted(
1625            &self,
1626            target: &str,
1627            id: &SessionMessageId,
1628        ) -> Result<bool, SessionInboxError> {
1629            self.inner.was_admitted(target, id).await
1630        }
1631
1632        async fn ack(
1633            &self,
1634            target: &str,
1635            claim: &bamboo_domain::SessionInboxClaim,
1636        ) -> Result<(), SessionInboxError> {
1637            self.inner.ack(target, claim).await
1638        }
1639
1640        async fn inspect(
1641            &self,
1642            target: &str,
1643        ) -> Result<bamboo_domain::SessionInboxBacklog, SessionInboxError> {
1644            self.inner.inspect(target).await
1645        }
1646    }
1647
1648    #[tokio::test]
1649    async fn peer_send_and_reply_reject_either_lifetime_recreated_before_admission() {
1650        for reply in [false, true] {
1651            for replaced_id in ["peer-left", "peer-right"] {
1652                let (_temp, store, activation, messenger) = fixture().await;
1653                seed_peer_tree(store.as_ref()).await;
1654                let received_id = if reply {
1655                    let incoming = messenger
1656                        .send_peer("peer-left", "peer-right", "incoming", "question")
1657                        .await
1658                        .unwrap();
1659                    admit_peer_for_test(store.as_ref(), messenger.inbox().as_ref(), "peer-right")
1660                        .await;
1661                    Some(incoming.delivery.id.to_string())
1662                } else {
1663                    None
1664                };
1665                activation.calls.lock().await.clear();
1666                let (caller, target) = if reply {
1667                    ("peer-right", "peer-left")
1668                } else {
1669                    ("peer-left", "peer-right")
1670                };
1671                let entered = Arc::new(tokio::sync::Notify::new());
1672                let release = Arc::new(tokio::sync::Notify::new());
1673                let inbox = Arc::new(FileSessionInbox::new(
1674                    store.clone(),
1675                    SessionInboxLimits::default(),
1676                ));
1677                let paused = SessionMessenger::new(
1678                    store.clone(),
1679                    Arc::new(PausedPeerAdmission {
1680                        inner: inbox.clone(),
1681                        entered: entered.clone(),
1682                        release: release.clone(),
1683                    }),
1684                    activation.clone(),
1685                );
1686                let sending = tokio::spawn(async move {
1687                    if let Some(id) = received_id {
1688                        paused
1689                            .reply_peer(caller, &id, "racing", "old lifetime")
1690                            .await
1691                    } else {
1692                        paused
1693                            .send_peer(caller, target, "racing", "old lifetime")
1694                            .await
1695                    }
1696                });
1697                tokio::time::timeout(std::time::Duration::from_secs(5), entered.notified())
1698                    .await
1699                    .unwrap();
1700                let old = store.load_session(replaced_id).await.unwrap().unwrap();
1701                assert!(store.delete_session(replaced_id).await.unwrap());
1702                let root = store.load_session("peer-root").await.unwrap().unwrap();
1703                let mut replacement =
1704                    Session::new_child_of(replaced_id, &root, "model", "Replacement");
1705                replacement.created_at = old.created_at + chrono::Duration::seconds(1);
1706                replacement.set_project_id_meta("peer-project");
1707                store.save_session(&replacement).await.unwrap();
1708                release.notify_one();
1709                let result = tokio::time::timeout(std::time::Duration::from_secs(5), sending)
1710                    .await
1711                    .unwrap()
1712                    .unwrap();
1713                assert!(result.is_err(), "reply={reply}, replaced={replaced_id}");
1714                let backlog = inbox.inspect(target).await.unwrap();
1715                assert_eq!(backlog.generation, 0);
1716                assert_eq!(backlog.pending, 0);
1717                assert!(!backlog.activation_pending());
1718                assert!(activation.calls.lock().await.is_empty());
1719                // Same ids, tree and Project are legal for a fresh operation;
1720                // only the stale captured lifetime must fail closed.
1721                let current = messenger
1722                    .send_peer(caller, target, "current-lifetime", "current")
1723                    .await
1724                    .unwrap();
1725                assert_eq!(current.delivery.generation, 1);
1726                assert_eq!(activation.calls.lock().await.len(), 1);
1727            }
1728        }
1729    }
1730
1731    #[tokio::test]
1732    async fn peer_reply_revalidates_received_proof_after_concurrent_clear_or_save() {
1733        for clear in [true, false] {
1734            let (_temp, store, activation, messenger) = fixture().await;
1735            seed_peer_tree(store.as_ref()).await;
1736            let incoming = messenger
1737                .send_peer("peer-left", "peer-right", "incoming", "question")
1738                .await
1739                .unwrap();
1740            admit_peer_for_test(store.as_ref(), messenger.inbox().as_ref(), "peer-right").await;
1741            activation.calls.lock().await.clear();
1742            let before = store.load_session("peer-right").await.unwrap().unwrap();
1743            let received_id = incoming.delivery.id.to_string();
1744            let entered = Arc::new(tokio::sync::Notify::new());
1745            let release = Arc::new(tokio::sync::Notify::new());
1746            let inbox = Arc::new(FileSessionInbox::new(
1747                store.clone(),
1748                SessionInboxLimits::default(),
1749            ));
1750            let paused = SessionMessenger::new(
1751                store.clone(),
1752                Arc::new(PausedPeerAdmission {
1753                    inner: inbox.clone(),
1754                    entered: entered.clone(),
1755                    release: release.clone(),
1756                }),
1757                activation.clone(),
1758            );
1759            let old_id = received_id.clone();
1760            let replying = tokio::spawn(async move {
1761                paused
1762                    .reply_peer("peer-right", &old_id, "racing-proof", "reply")
1763                    .await
1764            });
1765            tokio::time::timeout(std::time::Duration::from_secs(5), entered.notified())
1766                .await
1767                .unwrap();
1768            if clear {
1769                assert!(store.clear_session("peer-right").await.unwrap());
1770            } else {
1771                let mut changed = before.clone();
1772                changed.messages.retain(|message| message.id != received_id);
1773                store.save_session(&changed).await.unwrap();
1774            }
1775            let after = store.load_session("peer-right").await.unwrap().unwrap();
1776            assert_eq!(before.created_at, after.created_at);
1777            assert_eq!(before.root_session_id, after.root_session_id);
1778            assert_eq!(before.project_id_meta(), after.project_id_meta());
1779            assert!(!after
1780                .messages
1781                .iter()
1782                .any(|message| message.id == received_id));
1783            release.notify_one();
1784            let result = tokio::time::timeout(std::time::Duration::from_secs(5), replying)
1785                .await
1786                .unwrap()
1787                .unwrap();
1788            assert!(
1789                result.is_err(),
1790                "clear={clear}: removed proof must not admit a reply"
1791            );
1792            let backlog = inbox.inspect("peer-left").await.unwrap();
1793            assert_eq!(backlog.generation, 0);
1794            assert_eq!(backlog.pending, 0);
1795            assert!(!backlog.activation_pending());
1796            assert!(activation.calls.lock().await.is_empty());
1797
1798            let current = messenger
1799                .send_peer("peer-left", "peer-right", "current-question", "current")
1800                .await
1801                .unwrap();
1802            admit_peer_for_test(store.as_ref(), messenger.inbox().as_ref(), "peer-right").await;
1803            let reply = messenger
1804                .reply_peer(
1805                    "peer-right",
1806                    current.delivery.id.as_str(),
1807                    "racing-proof",
1808                    "reply",
1809                )
1810                .await
1811                .unwrap();
1812            let retry = messenger
1813                .reply_peer(
1814                    "peer-right",
1815                    current.delivery.id.as_str(),
1816                    "racing-proof",
1817                    "reply",
1818                )
1819                .await
1820                .unwrap();
1821            assert_eq!(reply.delivery.generation, 1);
1822            assert_eq!(retry.delivery.generation, reply.delivery.generation);
1823            assert_eq!(retry.delivery.id, reply.delivery.id);
1824            let backlog = inbox.inspect("peer-left").await.unwrap();
1825            assert_eq!(backlog.generation, 1);
1826            assert_eq!(backlog.pending, 1);
1827        }
1828    }
1829
1830    #[tokio::test]
1831    async fn peer_admission_without_integrated_lifetime_fence_is_unsupported() {
1832        let (_temp, store, activation, _messenger) = fixture().await;
1833        seed_peer_tree(store.as_ref()).await;
1834        let inbox = Arc::new(FileSessionInbox::new(
1835            store.clone(),
1836            SessionInboxLimits::default(),
1837        ));
1838        let messenger = SessionMessenger::new(
1839            store,
1840            Arc::new(FailEligibilityOnce {
1841                inner: inbox.clone(),
1842                fail_next: std::sync::atomic::AtomicBool::new(false),
1843            }),
1844            activation.clone(),
1845        );
1846        let error = messenger
1847            .send_peer("peer-left", "peer-right", "unsupported", "message")
1848            .await
1849            .unwrap_err();
1850        assert!(error
1851            .to_string()
1852            .contains("lifetime-fenced peer SessionInbox admission is unsupported"));
1853        for intent in [
1854            SessionMessageDeliveryIntent::QueueOnly,
1855            SessionMessageDeliveryIntent::TriggerTurn,
1856        ] {
1857            assert!(messenger
1858                .send_peer_with_intent(
1859                    "peer-left",
1860                    "peer-right",
1861                    "unsupported-explicit",
1862                    "message",
1863                    intent
1864                )
1865                .await
1866                .unwrap_err()
1867                .to_string()
1868                .contains("explicit peer delivery intent is unsupported"));
1869        }
1870        assert_eq!(inbox.inspect("peer-right").await.unwrap().pending, 0);
1871        assert!(activation.calls.lock().await.is_empty());
1872    }
1873
1874    async fn admit_peer_for_test(
1875        store: &dyn Storage,
1876        inbox: &dyn SessionInboxPort,
1877        target: &str,
1878    ) -> SessionMessageEnvelope {
1879        let claims = inbox.claim(target, 16).await.unwrap();
1880        assert_eq!(claims.len(), 1);
1881        let claim = &claims[0];
1882        let mut session = store.load_session(target).await.unwrap().unwrap();
1883        session.add_message(claim.envelope.to_provider_message().unwrap());
1884        store.save_session(&session).await.unwrap();
1885        inbox.ack(target, claim).await.unwrap();
1886        claim.envelope.clone()
1887    }
1888
1889    #[tokio::test]
1890    async fn peer_api_discovers_and_messages_parent_child_siblings_and_cousins() {
1891        let (_temp, store, activation, messenger) = fixture().await;
1892        seed_peer_tree(store.as_ref()).await;
1893        store
1894            .save_session(&Session::new("foreign-root", "model"))
1895            .await
1896            .unwrap();
1897        let targets = messenger.list_peer_targets("peer-left-leaf").await.unwrap();
1898        assert!(!targets.truncated);
1899        let ids: Vec<_> = targets
1900            .targets
1901            .iter()
1902            .map(|target| target.session_id.as_str())
1903            .collect();
1904        assert_eq!(
1905            ids,
1906            ["peer-root", "peer-left", "peer-right", "peer-right-leaf"]
1907        );
1908        assert_eq!(targets.targets[0].title, "Logical root");
1909        assert!(targets.targets[0].parent_session_id.is_none());
1910        assert!(!serde_json::to_string(&targets)
1911            .unwrap()
1912            .contains("foreign-root"));
1913
1914        for (index, (source, target)) in [
1915            ("peer-root", "peer-left"),
1916            ("peer-left", "peer-root"),
1917            ("peer-left", "peer-right"),
1918            ("peer-left-leaf", "peer-right-leaf"),
1919        ]
1920        .into_iter()
1921        .enumerate()
1922        {
1923            let receipt = messenger
1924                .send_peer(
1925                    source,
1926                    target,
1927                    &format!("operation-{index}"),
1928                    "bounded peer instruction",
1929                )
1930                .await
1931                .unwrap();
1932            let backlog = messenger.inbox().inspect(target).await.unwrap();
1933            assert_eq!(
1934                backlog.coordinator_generation, 0,
1935                "peer permission must not release a prefix"
1936            );
1937            assert!(backlog.interrupt_pending());
1938            let envelope =
1939                admit_peer_for_test(store.as_ref(), messenger.inbox().as_ref(), target).await;
1940            assert_eq!(envelope.id, receipt.delivery.id);
1941            assert_eq!(
1942                envelope.source,
1943                SessionMessageSource::Session {
1944                    session_id: source.into()
1945                }
1946            );
1947            assert_eq!(envelope.thread_id.as_deref(), Some(envelope.id.as_str()));
1948            assert_eq!(
1949                envelope.correlation_id.as_deref(),
1950                Some(format!("operation-{index}").as_str())
1951            );
1952            assert!(envelope.in_reply_to.is_none());
1953        }
1954        assert_eq!(activation.calls.lock().await.len(), 4);
1955    }
1956
1957    #[tokio::test]
1958    async fn peer_api_denies_unknown_deleted_cross_root_and_invalid_project_or_lineage() {
1959        let (_temp, store, activation, messenger) = fixture().await;
1960        seed_peer_tree(store.as_ref()).await;
1961        store
1962            .save_session(&Session::new("foreign-root", "model"))
1963            .await
1964            .unwrap();
1965        for (source, target) in [
1966            ("peer-left", "foreign-root"),
1967            ("unknown", "peer-left"),
1968            ("peer-left", "unknown"),
1969            ("peer-left", "peer-left"),
1970        ] {
1971            assert!(messenger
1972                .send_peer(source, target, "denied-operation", "instruction")
1973                .await
1974                .is_err());
1975        }
1976        store.delete_session("peer-right-leaf").await.unwrap();
1977        assert!(matches!(
1978            messenger
1979                .send_peer("peer-left", "peer-right-leaf", "deleted", "instruction")
1980                .await,
1981            Err(SessionMessengerError::TargetNotFound(_))
1982        ));
1983        assert!(matches!(
1984            messenger
1985                .send_peer(
1986                    "peer-right-leaf",
1987                    "peer-left",
1988                    "deleted-source",
1989                    "instruction"
1990                )
1991                .await,
1992            Err(SessionMessengerError::SourceNotFound(_))
1993        ));
1994
1995        // Invalid canonical snapshots exercise the read boundary without
1996        // relaxing the production store's immutable lineage writer checks.
1997        let memory = Arc::new(PeerMemoryStorage::default());
1998        seed_peer_tree(memory.as_ref()).await;
1999        let strict = SessionMessenger::new(
2000            memory.clone(),
2001            messenger.inbox().clone(),
2002            activation.clone(),
2003        );
2004        let original = memory.load_session("peer-right").await.unwrap().unwrap();
2005        for defect in [
2006            "project",
2007            "missing_project",
2008            "project_mirror",
2009            "parent",
2010            "depth",
2011            "root",
2012            "cycle",
2013        ] {
2014            let mut invalid = original.clone();
2015            match defect {
2016                "project" => invalid.set_project_id_meta("other-project"),
2017                "missing_project" => {
2018                    invalid.metadata.remove("project_id");
2019                    if let Some(metadata) = invalid.runtime_metadata.as_mut() {
2020                        metadata.project_id = None;
2021                    }
2022                }
2023                "project_mirror" => {
2024                    invalid
2025                        .metadata
2026                        .insert("project_id".into(), "other-project".into());
2027                }
2028                "parent" => invalid.parent_session_id = Some("missing-parent".into()),
2029                "depth" => invalid.spawn_depth += 1,
2030                "root" => invalid.root_session_id = "foreign-root".into(),
2031                "cycle" => invalid.parent_session_id = Some("peer-right-leaf".into()),
2032                _ => unreachable!(),
2033            }
2034            memory.save_session(&invalid).await.unwrap();
2035            assert!(
2036                strict
2037                    .send_peer("peer-left", "peer-right", "invalid", "instruction")
2038                    .await
2039                    .is_err(),
2040                "{defect}"
2041            );
2042            let targets = strict.list_peer_targets("peer-left").await.unwrap();
2043            assert!(
2044                !targets
2045                    .targets
2046                    .iter()
2047                    .any(|target| target.session_id == "peer-right"),
2048                "{defect}"
2049            );
2050            memory.save_session(&original).await.unwrap();
2051        }
2052        assert!(activation.calls.lock().await.is_empty());
2053        assert_eq!(
2054            messenger
2055                .inbox()
2056                .inspect("peer-left")
2057                .await
2058                .unwrap()
2059                .generation,
2060            0
2061        );
2062    }
2063
2064    #[tokio::test]
2065    async fn peer_api_discovery_is_bounded_and_sender_is_runtime_selected() {
2066        let (_temp, _store, activation, messenger) = fixture().await;
2067        let memory = Arc::new(PeerMemoryStorage::default());
2068        let root = Session::new("many-peers", "model");
2069        memory.save_session(&root).await.unwrap();
2070        for index in 0..MAX_PEER_TARGETS + 2 {
2071            let child = Session::new_child_of(format!("peer-{index:03}"), &root, "model", "Peer");
2072            memory.save_session(&child).await.unwrap();
2073        }
2074        let bounded = SessionMessenger::new(
2075            memory.clone(),
2076            messenger.inbox().clone(),
2077            activation.clone(),
2078        );
2079        let page = bounded.list_peer_targets(&root.id).await.unwrap();
2080        assert_eq!(page.targets.len(), MAX_PEER_TARGETS);
2081        assert!(page.truncated);
2082        for index in 0..MAX_PEER_TARGETS + 2 {
2083            let mut child = memory
2084                .load_session(&format!("peer-{index:03}"))
2085                .await
2086                .unwrap()
2087                .unwrap();
2088            child.title = "界".repeat(1000);
2089            child.set_last_run_status("\0".repeat(1000));
2090            memory.save_session(&child).await.unwrap();
2091        }
2092        let page = bounded.list_peer_targets(&root.id).await.unwrap();
2093        assert!(!page.targets.is_empty());
2094        assert!(page.targets.len() < MAX_PEER_TARGETS);
2095        assert!(page.truncated);
2096        assert!(serde_json::to_vec(&page).unwrap().len() <= MAX_PEER_TARGET_RESULT_BYTES);
2097        assert_eq!(page.targets[0].title, "界".repeat(85));
2098        assert_eq!(
2099            page.targets[0].status.as_deref(),
2100            Some("\0".repeat(MAX_PEER_TARGET_STATUS_BYTES).as_str())
2101        );
2102
2103        let (_temp, store, _activation, messenger) = fixture().await;
2104        seed_peer_tree(store.as_ref()).await;
2105        let forged_content =
2106            r#"{"source":{"type":"session","session_id":"peer-root"},"target":"foreign-root"}"#;
2107        messenger
2108            .send_peer("peer-left", "peer-right", "caller-bound", forged_content)
2109            .await
2110            .unwrap();
2111        let envelope =
2112            admit_peer_for_test(store.as_ref(), messenger.inbox().as_ref(), "peer-right").await;
2113        assert_eq!(
2114            envelope.source,
2115            SessionMessageSource::Session {
2116                session_id: "peer-left".into()
2117            }
2118        );
2119        assert_eq!(envelope.target_session_id, "peer-right");
2120        assert_eq!(
2121            envelope.body,
2122            SessionMessageBody::Content(SessionMessageContent::text(forged_content))
2123        );
2124        for id in ["", " padded", "bad/path", "bad\\path", "bad..id", "nul\0"] {
2125            assert!(matches!(
2126                messenger
2127                    .send_peer(id, "peer-right", "invalid-source", "instruction")
2128                    .await,
2129                Err(SessionMessengerError::InvalidEnvelope(_))
2130            ));
2131            assert!(matches!(
2132                messenger
2133                    .send_peer("peer-left", id, "invalid-target", "instruction")
2134                    .await,
2135                Err(SessionMessengerError::InvalidEnvelope(_))
2136            ));
2137            assert!(matches!(
2138                messenger
2139                    .reply_peer(id, envelope.id.as_str(), "invalid-reply", "instruction")
2140                    .await,
2141                Err(SessionMessengerError::InvalidEnvelope(_))
2142            ));
2143        }
2144        assert!(matches!(
2145            messenger.list_peer_targets(&"x".repeat(257)).await,
2146            Err(SessionMessengerError::InvalidEnvelope(_))
2147        ));
2148        for (operation, message) in [
2149            ("", "valid"),
2150            (" padded", "valid"),
2151            ("bad..id", "valid"),
2152            ("valid", " "),
2153            ("valid", "nul\0"),
2154        ] {
2155            assert!(messenger
2156                .send_peer("peer-left", "peer-right", operation, message)
2157                .await
2158                .is_err());
2159        }
2160        assert!(messenger
2161            .send_peer(
2162                "peer-left",
2163                "peer-right",
2164                "oversized",
2165                &"x".repeat(MAX_PEER_MESSAGE_BYTES + 1)
2166            )
2167            .await
2168            .is_err());
2169    }
2170
2171    #[tokio::test]
2172    async fn peer_api_retry_dedupes_before_claim_and_after_ack_restart_but_rejects_changed_payload()
2173    {
2174        let (temp, store, activation, messenger) = fixture().await;
2175        seed_peer_tree(store.as_ref()).await;
2176        let first = messenger
2177            .send_peer("peer-left", "peer-right", "stable-operation", "original")
2178            .await
2179            .unwrap();
2180        let retry = messenger
2181            .send_peer("peer-left", "peer-right", "stable-operation", "original")
2182            .await
2183            .unwrap();
2184        assert_eq!(retry.delivery, first.delivery);
2185        assert!(matches!(
2186            messenger
2187                .send_peer("peer-left", "peer-right", "stable-operation", "changed")
2188                .await,
2189            Err(SessionMessengerError::Inbox(
2190                SessionInboxError::InvalidClaim(_)
2191            ))
2192        ));
2193        assert_eq!(
2194            messenger
2195                .inbox()
2196                .inspect("peer-right")
2197                .await
2198                .unwrap()
2199                .generation,
2200            1
2201        );
2202        admit_peer_for_test(store.as_ref(), messenger.inbox().as_ref(), "peer-right").await;
2203        drop(messenger);
2204        drop(store);
2205        let reopened = Arc::new(SessionStoreV2::new(temp.path().into()).await.unwrap());
2206        let inbox = Arc::new(FileSessionInbox::new(
2207            reopened.clone(),
2208            SessionInboxLimits::default(),
2209        ));
2210        let messenger = SessionMessenger::new(reopened, inbox.clone(), activation);
2211        let after_ack = messenger
2212            .send_peer("peer-left", "peer-right", "stable-operation", "original")
2213            .await
2214            .unwrap();
2215        assert_eq!(after_ack.delivery, first.delivery);
2216        assert!(inbox.claim("peer-right", 16).await.unwrap().is_empty());
2217        assert!(matches!(
2218            messenger
2219                .send_peer("peer-left", "peer-right", "stable-operation", "changed")
2220                .await,
2221            Err(SessionMessengerError::Inbox(
2222                SessionInboxError::InvalidClaim(_)
2223            ))
2224        ));
2225        assert_eq!(inbox.inspect("peer-right").await.unwrap().generation, 1);
2226    }
2227
2228    #[tokio::test]
2229    async fn peer_api_reply_recovers_durable_recipient_and_thread_after_restart() {
2230        let (temp, store, activation, messenger) = fixture().await;
2231        seed_peer_tree(store.as_ref()).await;
2232        let first = messenger
2233            .send_peer("peer-left-leaf", "peer-right-leaf", "initial", "question")
2234            .await
2235            .unwrap();
2236        let incoming = admit_peer_for_test(
2237            store.as_ref(),
2238            messenger.inbox().as_ref(),
2239            "peer-right-leaf",
2240        )
2241        .await;
2242        drop(messenger);
2243        drop(store);
2244        let store = Arc::new(SessionStoreV2::new(temp.path().into()).await.unwrap());
2245        let inbox = Arc::new(FileSessionInbox::new(
2246            store.clone(),
2247            SessionInboxLimits::default(),
2248        ));
2249        let messenger = SessionMessenger::new(store.clone(), inbox.clone(), activation);
2250        let reply = messenger
2251            .reply_peer(
2252                "peer-right-leaf",
2253                first.delivery.id.as_str(),
2254                "answer",
2255                "answer text",
2256            )
2257            .await
2258            .unwrap();
2259        let retry = messenger
2260            .reply_peer(
2261                "peer-right-leaf",
2262                first.delivery.id.as_str(),
2263                "answer",
2264                "answer text",
2265            )
2266            .await
2267            .unwrap();
2268        assert_eq!(reply.delivery, retry.delivery);
2269        let response = admit_peer_for_test(store.as_ref(), inbox.as_ref(), "peer-left-leaf").await;
2270        assert_eq!(
2271            response.source,
2272            SessionMessageSource::Session {
2273                session_id: "peer-right-leaf".into()
2274            }
2275        );
2276        assert_eq!(response.in_reply_to, Some(incoming.id.clone()));
2277        assert_eq!(response.thread_id, incoming.thread_id);
2278        assert_eq!(response.correlation_id.as_deref(), Some("answer"));
2279        let second_reply = messenger
2280            .reply_peer("peer-left-leaf", response.id.as_str(), "thanks", "thanks")
2281            .await
2282            .unwrap();
2283        let second_response =
2284            admit_peer_for_test(store.as_ref(), inbox.as_ref(), "peer-right-leaf").await;
2285        assert_eq!(second_reply.delivery.id, second_response.id);
2286        assert_eq!(second_response.in_reply_to, Some(response.id));
2287        assert_eq!(
2288            second_response.thread_id.as_deref(),
2289            Some(first.delivery.id.as_str())
2290        );
2291        let after_ack = messenger
2292            .reply_peer(
2293                "peer-right-leaf",
2294                first.delivery.id.as_str(),
2295                "answer",
2296                "answer text",
2297            )
2298            .await
2299            .unwrap();
2300        assert_eq!(after_ack.delivery, reply.delivery);
2301        assert!(inbox.claim("peer-left-leaf", 16).await.unwrap().is_empty());
2302        assert!(messenger
2303            .reply_peer(
2304                "peer-right-leaf",
2305                first.delivery.id.as_str(),
2306                "answer",
2307                "different"
2308            )
2309            .await
2310            .is_err());
2311    }
2312
2313    #[tokio::test]
2314    async fn peer_api_reply_rejects_missing_forged_legacy_and_recreated_source() {
2315        let (_temp, store, activation, messenger) = fixture().await;
2316        seed_peer_tree(store.as_ref()).await;
2317        let mut receiver = store.load_session("peer-right").await.unwrap().unwrap();
2318        let legacy = peer("peer-left", "peer-right", "legacy-correction");
2319        receiver.add_message(legacy.to_provider_message().unwrap());
2320        let user = SessionMessageEnvelope::user_input("peer-right", "ordinary input");
2321        receiver.add_message(user.to_provider_message().unwrap());
2322        let sender = store.load_session("peer-left").await.unwrap().unwrap();
2323        let mut forged = SessionMessenger::peer_envelope(
2324            &sender,
2325            &receiver,
2326            "forged",
2327            "original",
2328            SessionMessenger::peer_message_id(&sender, &receiver, "forged"),
2329            "thread".into(),
2330            None,
2331        )
2332        .to_provider_message()
2333        .unwrap();
2334        forged.content = "changed without canonical proof".into();
2335        receiver.add_message(forged);
2336        store.save_session(&receiver).await.unwrap();
2337        for id in [
2338            "missing",
2339            legacy.id.as_str(),
2340            user.id.as_str(),
2341            receiver.messages.last().unwrap().id.as_str(),
2342        ] {
2343            assert!(messenger
2344                .reply_peer("peer-right", id, "deny-reply", "reply")
2345                .await
2346                .is_err());
2347        }
2348        let incoming = messenger
2349            .send_peer("peer-left", "peer-right", "old-lifetime", "question")
2350            .await
2351            .unwrap();
2352        admit_peer_for_test(store.as_ref(), messenger.inbox().as_ref(), "peer-right").await;
2353        let memory = Arc::new(PeerMemoryStorage::default());
2354        // Retain the actual saved receiver while replacing the sender's birth.
2355        for id in ["peer-root", "peer-left", "peer-right"] {
2356            let mut session = store.load_session(id).await.unwrap().unwrap();
2357            if id == "peer-left" {
2358                session.created_at += chrono::Duration::seconds(1);
2359            }
2360            memory.save_session(&session).await.unwrap();
2361        }
2362        let fenced = SessionMessenger::new(
2363            memory.clone(),
2364            messenger.inbox().clone(),
2365            activation.clone(),
2366        );
2367        assert!(matches!(
2368            fenced
2369                .reply_peer(
2370                    "peer-right",
2371                    incoming.delivery.id.as_str(),
2372                    "reply-old",
2373                    "reply"
2374                )
2375                .await,
2376            Err(SessionMessengerError::InvalidEnvelope(_))
2377        ));
2378        memory.delete_session("peer-left").await.unwrap();
2379        assert!(matches!(
2380            fenced
2381                .reply_peer(
2382                    "peer-right",
2383                    incoming.delivery.id.as_str(),
2384                    "reply-deleted",
2385                    "reply"
2386                )
2387                .await,
2388            Err(SessionMessengerError::TargetNotFound(_))
2389        ));
2390        assert_eq!(activation.calls.lock().await.len(), 1);
2391    }
2392
2393    #[tokio::test]
2394    async fn same_root_delivery_enqueues_then_requests_activation() {
2395        let (_temp, store, activation, messenger) = fixture().await;
2396        let root = Session::new("root", "model");
2397        let mut child = Session::new("child", "model");
2398        child.kind = bamboo_domain::SessionKind::Child;
2399        child.parent_session_id = Some("root".to_string());
2400        child.root_session_id = "root".to_string();
2401        store.save_session(&root).await.unwrap();
2402        store.save_session(&child).await.unwrap();
2403
2404        let receipt = messenger
2405            .send(peer("root", "child", "msg-1"))
2406            .await
2407            .unwrap();
2408        assert_eq!(receipt.delivery.generation, 1);
2409        assert_eq!(
2410            activation.calls.lock().await.as_slice(),
2411            &[("child".to_string(), 1)]
2412        );
2413        assert_eq!(messenger.metrics().snapshot().delivered, 1);
2414    }
2415
2416    #[tokio::test]
2417    async fn staged_delivery_without_watermark_reports_exact_retry_required() {
2418        let temp = TempDir::new().unwrap();
2419        let store = Arc::new(
2420            SessionStoreV2::new(temp.path().to_path_buf())
2421                .await
2422                .unwrap(),
2423        );
2424        let root = Session::new("root", "model");
2425        let mut child = Session::new("child", "model");
2426        child.kind = bamboo_domain::SessionKind::Child;
2427        child.parent_session_id = Some("root".to_string());
2428        child.root_session_id = "root".to_string();
2429        store.save_session(&root).await.unwrap();
2430        store.save_session(&child).await.unwrap();
2431        let inbox = Arc::new(FailEligibilityOnce {
2432            inner: Arc::new(FileSessionInbox::new(
2433                store.clone(),
2434                SessionInboxLimits::default(),
2435            )),
2436            fail_next: std::sync::atomic::AtomicBool::new(true),
2437        });
2438        let activation = Arc::new(RecordingActivation {
2439            calls: tokio::sync::Mutex::new(Vec::new()),
2440        });
2441        let messenger = SessionMessenger::new(store.clone(), inbox, activation.clone());
2442        let envelope = peer("root", "child", "watermark-failure");
2443        let gate = bamboo_domain::AdmissionGate::default();
2444
2445        let admission = messenger
2446            .admit_with_gate(envelope.clone(), Some(&gate))
2447            .await
2448            .unwrap();
2449        let receipt = match messenger.activate(&admission).await.unwrap_err() {
2450            SessionMessengerError::ActivationEligibility { receipt, .. } => receipt,
2451            error => panic!("expected retry-required classification: {error}"),
2452        };
2453        assert!(gate.is_committed());
2454        assert!(activation.calls.lock().await.is_empty());
2455        let reopened = FileSessionInbox::new(store, SessionInboxLimits::default());
2456        let backlog = reopened.inspect("child").await.unwrap();
2457        assert_eq!(backlog.pending + backlog.claimed, 1);
2458        assert_eq!(backlog.activation_generation, 0);
2459        assert_eq!(backlog.generation, receipt.generation);
2460
2461        let retry = messenger
2462            .admit_with_gate(envelope, Some(&gate))
2463            .await
2464            .unwrap();
2465        let retried = messenger.activate(&retry).await.unwrap();
2466        assert_eq!(retried.delivery, receipt);
2467        assert_eq!(activation.calls.lock().await.len(), 1);
2468        let reopened = reopened.inspect("child").await.unwrap();
2469        assert_eq!(reopened.activation_generation, receipt.generation);
2470        assert_eq!(reopened.pending + reopened.claimed, 1);
2471    }
2472
2473    #[tokio::test]
2474    async fn cross_root_peer_is_rejected_before_enqueue() {
2475        let (_temp, store, activation, messenger) = fixture().await;
2476        store
2477            .save_session(&Session::new("root-a", "model"))
2478            .await
2479            .unwrap();
2480        store
2481            .save_session(&Session::new("root-b", "model"))
2482            .await
2483            .unwrap();
2484
2485        let error = messenger
2486            .send(peer("root-a", "root-b", "msg-2"))
2487            .await
2488            .unwrap_err();
2489        assert!(matches!(error, SessionMessengerError::Unauthorized { .. }));
2490        assert!(activation.calls.lock().await.is_empty());
2491        let metrics = messenger.metrics().snapshot();
2492        assert_eq!(metrics.rejected, 1);
2493        assert_eq!(metrics.unauthorized, 1);
2494        assert_eq!(metrics.payload_too_large, 0);
2495        assert_eq!(metrics.backlog_full, 0);
2496    }
2497
2498    #[tokio::test]
2499    async fn same_root_different_project_peer_is_rejected_before_enqueue() {
2500        let (_temp, store, activation, messenger) = fixture().await;
2501        let mut source = Session::new("project-root", "model");
2502        source.set_project_id_meta("project-a");
2503        let mut target = Session::new("project-child", "model");
2504        target.kind = bamboo_domain::SessionKind::Child;
2505        target.parent_session_id = Some(source.id.clone());
2506        target.root_session_id = source.id.clone();
2507        target.set_project_id_meta("project-b");
2508        store.save_session(&source).await.unwrap();
2509        store.save_session(&target).await.unwrap();
2510
2511        let error = messenger
2512            .send(peer(&source.id, &target.id, "different-project"))
2513            .await
2514            .unwrap_err();
2515        assert!(matches!(error, SessionMessengerError::Unauthorized { .. }));
2516        assert!(activation.calls.lock().await.is_empty());
2517        let metrics = messenger.metrics().snapshot();
2518        assert_eq!(metrics.delivered, 0);
2519        assert_eq!(metrics.rejected, 1);
2520        assert_eq!(metrics.unauthorized, 1);
2521    }
2522
2523    #[tokio::test]
2524    async fn limit_rejections_have_distinct_metrics() {
2525        let temp = TempDir::new().unwrap();
2526        let store = Arc::new(
2527            SessionStoreV2::new(temp.path().to_path_buf())
2528                .await
2529                .unwrap(),
2530        );
2531        store
2532            .save_session(&Session::new("target", "model"))
2533            .await
2534            .unwrap();
2535        let activation = Arc::new(RecordingActivation {
2536            calls: tokio::sync::Mutex::new(Vec::new()),
2537        });
2538        let inbox = Arc::new(FileSessionInbox::new(
2539            store.clone(),
2540            SessionInboxLimits {
2541                max_payload_bytes: 512,
2542                max_backlog: 1,
2543                max_claim_batch: 1,
2544            },
2545        ));
2546        let messenger = SessionMessenger::new(store, inbox, activation.clone());
2547
2548        let oversized = SessionMessageEnvelope::user_input("target", "x".repeat(2048));
2549        assert!(matches!(
2550            messenger.send(oversized).await,
2551            Err(SessionMessengerError::Inbox(
2552                SessionInboxError::PayloadTooLarge { .. }
2553            ))
2554        ));
2555        messenger
2556            .send(SessionMessageEnvelope::user_input("target", "first"))
2557            .await
2558            .unwrap();
2559        assert!(matches!(
2560            messenger
2561                .send(SessionMessageEnvelope::user_input("target", "second"))
2562                .await,
2563            Err(SessionMessengerError::Inbox(
2564                SessionInboxError::BacklogFull { .. }
2565            ))
2566        ));
2567
2568        let metrics = messenger.metrics().snapshot();
2569        assert_eq!(metrics.rejected, 2);
2570        assert_eq!(metrics.payload_too_large, 1);
2571        assert_eq!(metrics.backlog_full, 1);
2572        assert_eq!(activation.calls.lock().await.len(), 1);
2573    }
2574
2575    #[tokio::test]
2576    async fn explicit_peer_intent_keeps_legacy_compatibility_and_fences_changed_retry_after_ack() {
2577        let (_temp, store, activation, messenger) = fixture().await;
2578        seed_peer_tree(store.as_ref()).await;
2579        let receipt = messenger
2580            .send_peer_with_intent(
2581                "peer-left",
2582                "peer-right",
2583                "queue-operation",
2584                "input",
2585                SessionMessageDeliveryIntent::QueueOnly,
2586            )
2587            .await
2588            .unwrap();
2589        assert_eq!(receipt.activation, None);
2590        assert!(!receipt.active_delivery_notified);
2591        assert!(activation.calls.lock().await.is_empty());
2592        let original =
2593            admit_peer_for_test(store.as_ref(), messenger.inbox().as_ref(), "peer-right").await;
2594        assert_eq!(
2595            messenger
2596                .send_peer_with_intent(
2597                    "peer-left",
2598                    "peer-right",
2599                    "queue-operation",
2600                    "input",
2601                    SessionMessageDeliveryIntent::QueueOnly
2602                )
2603                .await
2604                .unwrap()
2605                .delivery,
2606            receipt.delivery
2607        );
2608        assert!(messenger
2609            .send_peer_with_intent(
2610                "peer-left",
2611                "peer-right",
2612                "queue-operation",
2613                "input",
2614                SessionMessageDeliveryIntent::TriggerTurn
2615            )
2616            .await
2617            .is_err());
2618        assert!(activation.calls.lock().await.is_empty());
2619        let reply = messenger
2620            .reply_peer_with_intent(
2621                "peer-right",
2622                original.id.as_str(),
2623                "reply-operation",
2624                "reply",
2625                SessionMessageDeliveryIntent::TriggerTurn,
2626            )
2627            .await
2628            .unwrap();
2629        // This fixture records activation requests as reservations. Active
2630        // owner delivery is covered by the real router's safe-point tests.
2631        assert_eq!(
2632            reply.activation,
2633            Some(SessionActivationDisposition::ActivationReserved)
2634        );
2635        assert_eq!(
2636            activation.calls.lock().await.as_slice(),
2637            &[("peer-left".to_string(), reply.delivery.generation)]
2638        );
2639        let claims = messenger.inbox().claim("peer-left", 8).await.unwrap();
2640        assert_eq!(claims[0].envelope.in_reply_to, Some(original.id));
2641        assert_eq!(
2642            claims[0].activation_policy,
2643            SessionActivationPolicy::RespectSpecificWait
2644        );
2645        messenger
2646            .send_peer("peer-left", "peer-right", "legacy-operation", "legacy")
2647            .await
2648            .unwrap();
2649        let claims = messenger.inbox().claim("peer-right", 8).await.unwrap();
2650        assert_eq!(
2651            claims[0].activation_policy,
2652            SessionActivationPolicy::InterruptSpecificWait
2653        );
2654    }
2655
2656    #[tokio::test]
2657    async fn explicit_peer_intents_deny_foreign_roots_before_admission_or_wake() {
2658        let (_temp, store, activation, messenger) = fixture().await;
2659        seed_peer_tree(store.as_ref()).await;
2660        store
2661            .save_session(&Session::new("foreign-root", "model"))
2662            .await
2663            .unwrap();
2664        for intent in [
2665            SessionMessageDeliveryIntent::QueueOnly,
2666            SessionMessageDeliveryIntent::TriggerTurn,
2667        ] {
2668            assert!(matches!(
2669                messenger
2670                    .send_peer_with_intent("peer-left", "foreign-root", "denied", "input", intent)
2671                    .await,
2672                Err(SessionMessengerError::Unauthorized { .. })
2673            ));
2674        }
2675        assert_eq!(
2676            messenger
2677                .inbox()
2678                .inspect("foreign-root")
2679                .await
2680                .unwrap()
2681                .pending,
2682            0
2683        );
2684        assert!(activation.calls.lock().await.is_empty());
2685    }
2686}