Skip to main content

bamboo_engine/
session_messaging.rs

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