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    /// Follow an already-attached Ordinary Root from the original executing
1146    /// Supervisor. The concrete inbox revalidates and retains all authority
1147    /// locks through durable admission; a prior inspect_link is never a grant.
1148    pub async fn supervisor_followup(
1149        &self,
1150        supervisor: &SupervisorReference,
1151        target_session_id: &str,
1152        operation_id: &str,
1153        message: &str,
1154    ) -> Result<SessionMessengerReceipt, SessionMessengerError> {
1155        self.supervisor_followup_delivery(
1156            supervisor,
1157            target_session_id,
1158            operation_id,
1159            message,
1160            None,
1161        )
1162        .await
1163    }
1164
1165    /// Canonical sender fallback only. Its Inbox holds a real never-installed
1166    /// namespace guard and the complete original caller through admission.
1167    pub async fn supervisor_followup_observed(
1168        &self,
1169        caller: &SessionCallerReference,
1170        supervisor: &SupervisorReference,
1171        target_session_id: &str,
1172        operation_id: &str,
1173        message: &str,
1174    ) -> Result<SessionMessengerReceipt, SessionMessengerError> {
1175        self.supervisor_followup_delivery(
1176            supervisor,
1177            target_session_id,
1178            operation_id,
1179            message,
1180            Some(caller),
1181        )
1182        .await
1183    }
1184
1185    async fn supervisor_followup_delivery(
1186        &self,
1187        supervisor: &SupervisorReference,
1188        target_session_id: &str,
1189        operation_id: &str,
1190        message: &str,
1191        original_caller: Option<&SessionCallerReference>,
1192    ) -> Result<SessionMessengerReceipt, SessionMessengerError> {
1193        bamboo_domain::validate_supervisor_target_id(target_session_id)
1194            .map_err(|error| SessionMessengerError::InvalidEnvelope(error.to_string()))?;
1195        SessionMessageId::parse(operation_id)
1196            .map_err(|error| SessionMessengerError::InvalidEnvelope(error.to_string()))?;
1197        if message.trim().is_empty() || message.len() > MAX_SUPERVISOR_FOLLOWUP_BYTES {
1198            return Err(SessionMessengerError::InvalidEnvelope(format!(
1199                "followup message must contain 1..={MAX_SUPERVISOR_FOLLOWUP_BYTES} UTF-8 bytes"
1200            )));
1201        }
1202        let envelope = SessionMessageEnvelope {
1203            id: bamboo_domain::supervisor_followup_message_id(
1204                supervisor,
1205                target_session_id,
1206                operation_id,
1207            ),
1208            source: SessionMessageSource::Session {
1209                session_id: supervisor.session_id.clone(),
1210            },
1211            target_session_id: target_session_id.to_string(),
1212            kind: SessionMessageKind::PeerMessage,
1213            body: SessionMessageBody::Content(SessionMessageContent::text(message)),
1214            created_at: chrono::Utc::now(),
1215            thread_id: None,
1216            in_reply_to: None,
1217            attempt: None,
1218            correlation_id: Some(operation_id.to_string()),
1219        };
1220        let started = Instant::now();
1221        let result = match original_caller {
1222            Some(caller) => {
1223                self.inbox
1224                    .deliver_unscoped_supervisor_followup(caller, supervisor, &envelope)
1225                    .await
1226            }
1227            None => {
1228                self.inbox
1229                    .deliver_supervisor_followup(supervisor, &envelope)
1230                    .await
1231            }
1232        };
1233        let delivery = result.map_err(|error| {
1234            let error = SessionMessengerError::Inbox(error);
1235            self.record_rejection(&error);
1236            error
1237        })?;
1238        let admission = self.record_admission(envelope, delivery, started);
1239        // Its specific-message RespectSpecificWait intent was published while
1240        // holding Supervisor authority. This grants no staged sibling prefix.
1241        self.activate_prepared(&admission).await
1242    }
1243
1244    async fn load_session(&self, id: &str) -> Result<Option<Session>, SessionMessengerError> {
1245        self.sessions
1246            .load_session(id)
1247            .await
1248            .map_err(|error| SessionMessengerError::Storage(error.to_string()))
1249    }
1250
1251    fn logical_root(session: &Session) -> &str {
1252        if session.root_session_id.trim().is_empty() {
1253            &session.id
1254        } else {
1255            &session.root_session_id
1256        }
1257    }
1258
1259    async fn validate_relationship(
1260        &self,
1261        envelope: &SessionMessageEnvelope,
1262    ) -> Result<(), SessionMessengerError> {
1263        envelope
1264            .validate()
1265            .map_err(|error| SessionMessengerError::InvalidEnvelope(error.to_string()))?;
1266        let target = self
1267            .load_session(&envelope.target_session_id)
1268            .await?
1269            .ok_or_else(|| {
1270                SessionMessengerError::TargetNotFound(envelope.target_session_id.clone())
1271            })?;
1272
1273        let SessionMessageSource::Session { session_id } = &envelope.source else {
1274            return Ok(());
1275        };
1276        let source = self
1277            .load_session(session_id)
1278            .await?
1279            .ok_or_else(|| SessionMessengerError::SourceNotFound(session_id.clone()))?;
1280
1281        let same_root = Self::logical_root(&source) == Self::logical_root(&target);
1282        let source_project = source.project_id_meta();
1283        let target_project = target.project_id_meta();
1284        let project_compatible = match (source_project.as_deref(), target_project.as_deref()) {
1285            (Some(left), Some(right)) => left == right,
1286            // Older sessions did not persist project_id. Same-root ancestry is
1287            // the compatibility authority until both sides have migrated.
1288            _ => true,
1289        };
1290        if !same_root || !project_compatible {
1291            return Err(SessionMessengerError::Unauthorized {
1292                source_session_id: source.id,
1293                target: target.id,
1294            });
1295        }
1296        Ok(())
1297    }
1298
1299    fn record_rejection(&self, error: &SessionMessengerError) {
1300        self.metrics.rejected.fetch_add(1, Ordering::Relaxed);
1301        match error {
1302            SessionMessengerError::InvalidEnvelope(_) => {
1303                self.metrics
1304                    .invalid_envelope
1305                    .fetch_add(1, Ordering::Relaxed);
1306            }
1307            SessionMessengerError::Unauthorized { .. }
1308            | SessionMessengerError::SourceNotFound(_)
1309            | SessionMessengerError::TargetNotFound(_) => {
1310                self.metrics.unauthorized.fetch_add(1, Ordering::Relaxed);
1311            }
1312            SessionMessengerError::Inbox(SessionInboxError::AdmissionCancelled)
1313            | SessionMessengerError::Inbox(SessionInboxError::ExpectedRunConflict(_)) => {}
1314            SessionMessengerError::Inbox(SessionInboxError::PayloadTooLarge { .. }) => {
1315                self.metrics
1316                    .payload_too_large
1317                    .fetch_add(1, Ordering::Relaxed);
1318            }
1319            SessionMessengerError::Inbox(SessionInboxError::BacklogFull { .. }) => {
1320                self.metrics.backlog_full.fetch_add(1, Ordering::Relaxed);
1321            }
1322            SessionMessengerError::Inbox(_) | SessionMessengerError::Storage(_) => {
1323                self.metrics.storage_failed.fetch_add(1, Ordering::Relaxed);
1324            }
1325            SessionMessengerError::Activation { .. }
1326            | SessionMessengerError::ActivationEligibility { .. } => {}
1327        }
1328    }
1329
1330    pub async fn admit(
1331        &self,
1332        envelope: SessionMessageEnvelope,
1333    ) -> Result<SessionMessengerAdmission, SessionMessengerError> {
1334        self.admit_with_gate(envelope, None).await
1335    }
1336
1337    pub async fn admit_with_gate(
1338        &self,
1339        envelope: SessionMessageEnvelope,
1340        gate: Option<&bamboo_domain::AdmissionGate>,
1341    ) -> Result<SessionMessengerAdmission, SessionMessengerError> {
1342        self.admit_with_delivery_intent(envelope, gate, None).await
1343    }
1344
1345    /// Commit this specific message's immediate permission together with the
1346    /// envelope. Coordinators continue to use staged [`admit`](Self::admit).
1347    pub async fn admit_with_activation_intent(
1348        &self,
1349        envelope: SessionMessageEnvelope,
1350        policy: SessionActivationPolicy,
1351        gate: Option<&bamboo_domain::AdmissionGate>,
1352    ) -> Result<SessionMessengerAdmission, SessionMessengerError> {
1353        self.admit_with_delivery_intent(envelope, gate, Some(policy))
1354            .await
1355    }
1356
1357    async fn admit_with_delivery_intent(
1358        &self,
1359        envelope: SessionMessageEnvelope,
1360        gate: Option<&bamboo_domain::AdmissionGate>,
1361        policy: Option<SessionActivationPolicy>,
1362    ) -> Result<SessionMessengerAdmission, SessionMessengerError> {
1363        let started = Instant::now();
1364        if let Err(error) = self.validate_relationship(&envelope).await {
1365            self.record_rejection(&error);
1366            tracing::warn!(
1367                message_id = %envelope.id,
1368                target_session_id = %envelope.target_session_id,
1369                error = %error,
1370                "session message rejected"
1371            );
1372            return Err(error);
1373        }
1374
1375        let delivery = match (policy, gate) {
1376            (Some(policy), gate) => {
1377                self.inbox
1378                    .deliver_with_activation_intent(&envelope, policy, gate)
1379                    .await
1380            }
1381            (None, Some(gate)) => self.inbox.deliver_with_gate(&envelope, gate).await,
1382            (None, None) => self.inbox.deliver(&envelope).await,
1383        };
1384        let delivery = match delivery {
1385            Ok(receipt) => receipt,
1386            Err(error) => {
1387                let error = SessionMessengerError::Inbox(error);
1388                self.record_rejection(&error);
1389                return Err(error);
1390            }
1391        };
1392        Ok(self.record_admission(envelope, delivery, started))
1393    }
1394
1395    fn record_admission(
1396        &self,
1397        envelope: SessionMessageEnvelope,
1398        delivery: SessionInboxReceipt,
1399        started: Instant,
1400    ) -> SessionMessengerAdmission {
1401        self.metrics.delivered.fetch_add(1, Ordering::Relaxed);
1402        self.metrics.delivery_latency_micros.fetch_add(
1403            started.elapsed().as_micros().min(u128::from(u64::MAX)) as u64,
1404            Ordering::Relaxed,
1405        );
1406        SessionMessengerAdmission {
1407            envelope_id: envelope.id.as_str().to_string(),
1408            target_session_id: envelope.target_session_id,
1409            delivery,
1410        }
1411    }
1412
1413    pub async fn activate(
1414        &self,
1415        admission: &SessionMessengerAdmission,
1416    ) -> Result<SessionMessengerReceipt, SessionMessengerError> {
1417        self.activate_with_policy(admission, SessionActivationPolicy::InterruptSpecificWait)
1418            .await
1419    }
1420
1421    pub async fn activate_with_policy(
1422        &self,
1423        admission: &SessionMessengerAdmission,
1424        policy: SessionActivationPolicy,
1425    ) -> Result<SessionMessengerReceipt, SessionMessengerError> {
1426        if let Err(error) = self
1427            .inbox
1428            .mark_activation_eligible(
1429                &admission.target_session_id,
1430                admission.delivery.generation,
1431                policy,
1432            )
1433            .await
1434        {
1435            self.metrics
1436                .activation_failed
1437                .fetch_add(1, Ordering::Relaxed);
1438            return Err(SessionMessengerError::ActivationEligibility {
1439                receipt_id: admission.delivery.id.to_string(),
1440                receipt: admission.delivery.clone(),
1441                source: error,
1442            });
1443        }
1444        self.activate_prepared(admission).await
1445    }
1446
1447    /// Request execution for an admission whose activation policy was already
1448    /// durably recorded by a coordinator.
1449    pub async fn activate_prepared(
1450        &self,
1451        admission: &SessionMessengerAdmission,
1452    ) -> Result<SessionMessengerReceipt, SessionMessengerError> {
1453        let activation = match self
1454            .activation
1455            .request_activation(&admission.target_session_id, admission.delivery.generation)
1456            .await
1457        {
1458            Ok(disposition) => disposition,
1459            Err(source) => {
1460                self.metrics
1461                    .activation_failed
1462                    .fetch_add(1, Ordering::Relaxed);
1463                tracing::error!(
1464                    message_id = %admission.envelope_id,
1465                    target_session_id = %admission.target_session_id,
1466                    generation = admission.delivery.generation,
1467                    error = %source,
1468                    "session message durable but activation failed"
1469                );
1470                return Err(SessionMessengerError::Activation {
1471                    receipt_id: admission.delivery.id.to_string(),
1472                    receipt: admission.delivery.clone(),
1473                    source,
1474                });
1475            }
1476        };
1477        match activation {
1478            SessionActivationDisposition::ActiveNotified => {
1479                self.metrics.active_notified.fetch_add(1, Ordering::Relaxed);
1480            }
1481            SessionActivationDisposition::ActivationReserved => {
1482                self.metrics
1483                    .activation_reserved
1484                    .fetch_add(1, Ordering::Relaxed);
1485            }
1486            SessionActivationDisposition::ActivationCoalesced => {
1487                self.metrics
1488                    .activation_coalesced
1489                    .fetch_add(1, Ordering::Relaxed);
1490            }
1491        }
1492        tracing::info!(
1493            message_id = %admission.envelope_id,
1494            target_session_id = %admission.target_session_id,
1495            generation = admission.delivery.generation,
1496            ?activation,
1497            "session message durably delivered"
1498        );
1499        Ok(SessionMessengerReceipt {
1500            delivery: admission.delivery.clone(),
1501            activation,
1502        })
1503    }
1504
1505    /// Durably authorize the admitted queue prefix before a related
1506    /// control-plane transition is committed.
1507    ///
1508    /// Coordinators call this while their specific wait is still armed, then
1509    /// checkpoint the wait clear, and finally call [`activate`](Self::activate).
1510    /// A crash in either gap is recoverable: startup sees the watermark, while
1511    /// the real reservation adapter refuses to run until the specific wait has
1512    /// been durably cleared.
1513    pub async fn prepare_activation(
1514        &self,
1515        admission: &SessionMessengerAdmission,
1516    ) -> Result<(), SessionMessengerError> {
1517        self.inbox
1518            .mark_activation_eligible(
1519                &admission.target_session_id,
1520                admission.delivery.generation,
1521                SessionActivationPolicy::RespectSpecificWait,
1522            )
1523            .await
1524            .map_err(SessionMessengerError::Inbox)
1525    }
1526
1527    /// Authorize one staged delivery, preserving its immutable delivery
1528    /// intent and every sibling's existing execution permission.
1529    pub async fn prepare_message_activation(
1530        &self,
1531        admission: &SessionMessengerAdmission,
1532        policy: SessionActivationPolicy,
1533    ) -> Result<(), SessionMessengerError> {
1534        self.inbox
1535            .mark_message_activation_eligible(
1536                &admission.target_session_id,
1537                &admission.delivery,
1538                policy,
1539            )
1540            .await
1541            .map_err(SessionMessengerError::Inbox)
1542    }
1543
1544    pub async fn send(
1545        &self,
1546        envelope: SessionMessageEnvelope,
1547    ) -> Result<SessionMessengerReceipt, SessionMessengerError> {
1548        self.send_with_gate(envelope, None).await
1549    }
1550
1551    pub async fn send_with_gate(
1552        &self,
1553        envelope: SessionMessageEnvelope,
1554        gate: Option<&bamboo_domain::AdmissionGate>,
1555    ) -> Result<SessionMessengerReceipt, SessionMessengerError> {
1556        let admission = self
1557            .admit_with_activation_intent(
1558                envelope,
1559                SessionActivationPolicy::InterruptSpecificWait,
1560                gate,
1561            )
1562            .await?;
1563        self.activate_prepared(&admission).await
1564    }
1565
1566    pub async fn send_with_policy(
1567        &self,
1568        envelope: SessionMessageEnvelope,
1569        policy: SessionActivationPolicy,
1570    ) -> Result<SessionMessengerReceipt, SessionMessengerError> {
1571        let admission = self
1572            .admit_with_activation_intent(envelope, policy, None)
1573            .await?;
1574        self.activate_prepared(&admission).await
1575    }
1576}
1577
1578#[cfg(test)]
1579mod tests {
1580    use super::*;
1581    use async_trait::async_trait;
1582    use bamboo_domain::{
1583        SessionActivationDisposition, SessionInboxLimits, SessionMessageBody,
1584        SessionMessageContent, SessionMessageId, SessionMessageKind,
1585    };
1586    use bamboo_storage::{FileSessionInbox, SessionStoreV2};
1587    use tempfile::TempDir;
1588
1589    struct RecordingActivation {
1590        calls: tokio::sync::Mutex<Vec<(String, u64)>>,
1591    }
1592
1593    struct FailingActivation;
1594
1595    #[async_trait]
1596    impl SessionActivationPort for FailingActivation {
1597        async fn request_activation(
1598            &self,
1599            _target_session_id: &str,
1600            _inbox_generation: u64,
1601        ) -> Result<SessionActivationDisposition, SessionActivationError> {
1602            Err(SessionActivationError::Internal(
1603                "injected wakeup failure".into(),
1604            ))
1605        }
1606    }
1607
1608    #[tokio::test]
1609    async fn immediate_activation_failure_preserves_receipt_and_restart_permission() {
1610        let (temp, store, recording, _messenger) = fixture().await;
1611        store
1612            .save_session(&Session::new("root", "model"))
1613            .await
1614            .unwrap();
1615        let inbox = Arc::new(FileSessionInbox::new(
1616            store.clone(),
1617            SessionInboxLimits::default(),
1618        ));
1619        let messenger = SessionMessenger::new(store, inbox, Arc::new(FailingActivation));
1620        let envelope = SessionMessageEnvelope::user_input("root", "steer");
1621        let gate = bamboo_domain::AdmissionGate::default();
1622        let receipt = match messenger
1623            .send_with_gate(envelope.clone(), Some(&gate))
1624            .await
1625            .unwrap_err()
1626        {
1627            SessionMessengerError::Activation { receipt, .. } => receipt,
1628            error => panic!("expected durable delivery with failed wakeup: {error}"),
1629        };
1630        assert!(gate.is_committed());
1631        gate.cancel_if_pending();
1632        let restarted_store = Arc::new(SessionStoreV2::new(temp.path().into()).await.unwrap());
1633        let restarted = Arc::new(FileSessionInbox::new(
1634            restarted_store.clone(),
1635            SessionInboxLimits::default(),
1636        ));
1637        let backlog = restarted.inspect("root").await.unwrap();
1638        assert!(backlog.activation_pending());
1639        assert_eq!(backlog.coordinator_generation, 0);
1640        // This is the startup reconciliation request: the durable generation
1641        // comes from this message's intent while the coordinator prefix is 0.
1642        recording
1643            .request_activation("root", backlog.activation_generation)
1644            .await
1645            .unwrap();
1646        let retry = SessionMessenger::new(restarted_store, restarted.clone(), recording.clone());
1647        assert_eq!(
1648            retry
1649                .send_with_gate(envelope, Some(&gate))
1650                .await
1651                .unwrap()
1652                .delivery,
1653            receipt
1654        );
1655        assert_eq!(restarted.inspect("root").await.unwrap().pending, 1);
1656        assert_eq!(messenger.metrics().snapshot().activation_failed, 1);
1657        let claims = restarted.claim("root", 128).await.unwrap();
1658        assert_eq!(claims.len(), 1);
1659        assert_eq!(claims[0].envelope.id, receipt.id);
1660    }
1661
1662    struct FailEligibilityOnce {
1663        inner: Arc<FileSessionInbox>,
1664        fail_next: std::sync::atomic::AtomicBool,
1665    }
1666
1667    #[async_trait]
1668    impl SessionInboxPort for FailEligibilityOnce {
1669        async fn deliver(
1670            &self,
1671            envelope: &SessionMessageEnvelope,
1672        ) -> Result<SessionInboxReceipt, SessionInboxError> {
1673            self.inner.deliver(envelope).await
1674        }
1675
1676        async fn deliver_with_gate(
1677            &self,
1678            envelope: &SessionMessageEnvelope,
1679            gate: &bamboo_domain::AdmissionGate,
1680        ) -> Result<SessionInboxReceipt, SessionInboxError> {
1681            self.inner.deliver_with_gate(envelope, gate).await
1682        }
1683
1684        async fn mark_activation_eligible(
1685            &self,
1686            target_session_id: &str,
1687            generation: u64,
1688            policy: SessionActivationPolicy,
1689        ) -> Result<(), SessionInboxError> {
1690            if self.fail_next.swap(false, Ordering::SeqCst) {
1691                return Err(SessionInboxError::Storage(
1692                    "injected activation watermark failure".into(),
1693                ));
1694            }
1695            self.inner
1696                .mark_activation_eligible(target_session_id, generation, policy)
1697                .await
1698        }
1699
1700        async fn claim(
1701            &self,
1702            target_session_id: &str,
1703            limit: usize,
1704        ) -> Result<Vec<bamboo_domain::SessionInboxClaim>, SessionInboxError> {
1705            self.inner.claim(target_session_id, limit).await
1706        }
1707
1708        async fn was_admitted(
1709            &self,
1710            target_session_id: &str,
1711            id: &SessionMessageId,
1712        ) -> Result<bool, SessionInboxError> {
1713            self.inner.was_admitted(target_session_id, id).await
1714        }
1715
1716        async fn ack(
1717            &self,
1718            target_session_id: &str,
1719            claim: &bamboo_domain::SessionInboxClaim,
1720        ) -> Result<(), SessionInboxError> {
1721            self.inner.ack(target_session_id, claim).await
1722        }
1723
1724        async fn inspect(
1725            &self,
1726            target_session_id: &str,
1727        ) -> Result<bamboo_domain::SessionInboxBacklog, SessionInboxError> {
1728            self.inner.inspect(target_session_id).await
1729        }
1730    }
1731
1732    #[async_trait]
1733    impl SessionActivationPort for RecordingActivation {
1734        async fn request_activation(
1735            &self,
1736            target_session_id: &str,
1737            inbox_generation: u64,
1738        ) -> Result<SessionActivationDisposition, SessionActivationError> {
1739            self.calls
1740                .lock()
1741                .await
1742                .push((target_session_id.to_string(), inbox_generation));
1743            Ok(SessionActivationDisposition::ActivationReserved)
1744        }
1745    }
1746
1747    async fn fixture() -> (
1748        TempDir,
1749        Arc<SessionStoreV2>,
1750        Arc<RecordingActivation>,
1751        SessionMessenger,
1752    ) {
1753        let temp = TempDir::new().unwrap();
1754        let store = Arc::new(
1755            SessionStoreV2::new(temp.path().to_path_buf())
1756                .await
1757                .unwrap(),
1758        );
1759        let activation = Arc::new(RecordingActivation {
1760            calls: tokio::sync::Mutex::new(Vec::new()),
1761        });
1762        let inbox = Arc::new(FileSessionInbox::new(
1763            store.clone(),
1764            SessionInboxLimits::default(),
1765        ));
1766        let messenger = SessionMessenger::new(store.clone(), inbox, activation.clone());
1767        (temp, store, activation, messenger)
1768    }
1769
1770    fn peer(source: &str, target: &str, id: &str) -> SessionMessageEnvelope {
1771        SessionMessageEnvelope {
1772            id: SessionMessageId::parse(id).unwrap(),
1773            source: SessionMessageSource::Session {
1774                session_id: source.to_string(),
1775            },
1776            target_session_id: target.to_string(),
1777            kind: SessionMessageKind::PeerMessage,
1778            body: SessionMessageBody::Content(SessionMessageContent::text("hello")),
1779            created_at: chrono::Utc::now(),
1780            thread_id: None,
1781            in_reply_to: None,
1782            attempt: None,
1783            correlation_id: None,
1784        }
1785    }
1786
1787    #[derive(Default)]
1788    struct PeerMemoryStorage(tokio::sync::RwLock<std::collections::HashMap<String, Session>>);
1789
1790    #[async_trait]
1791    impl Storage for PeerMemoryStorage {
1792        async fn save_session(&self, session: &Session) -> std::io::Result<()> {
1793            self.0
1794                .write()
1795                .await
1796                .insert(session.id.clone(), session.clone());
1797            Ok(())
1798        }
1799
1800        async fn load_session(&self, id: &str) -> std::io::Result<Option<Session>> {
1801            Ok(self.0.read().await.get(id).cloned())
1802        }
1803
1804        async fn delete_session(&self, id: &str) -> std::io::Result<bool> {
1805            Ok(self.0.write().await.remove(id).is_some())
1806        }
1807
1808        async fn list_child_run_statuses(
1809            &self,
1810            parent: &str,
1811        ) -> std::io::Result<Vec<(String, Option<String>)>> {
1812            Ok(self
1813                .0
1814                .read()
1815                .await
1816                .values()
1817                .filter(|session| session.parent_session_id.as_deref() == Some(parent))
1818                .map(|session| (session.id.clone(), session.last_run_status()))
1819                .collect())
1820        }
1821    }
1822
1823    async fn seed_peer_tree(store: &dyn Storage) {
1824        let mut root = Session::new("peer-root", "model");
1825        root.title = "Logical root".into();
1826        root.set_project_id_meta("peer-project");
1827        let left = Session::new_child_of("peer-left", &root, "model", "Left");
1828        let right = Session::new_child_of("peer-right", &root, "model", "Right");
1829        let cousin_left = Session::new_child_of("peer-left-leaf", &left, "model", "Left leaf");
1830        let cousin_right = Session::new_child_of("peer-right-leaf", &right, "model", "Right leaf");
1831        for mut session in [root, left, right, cousin_left, cousin_right] {
1832            session.set_project_id_meta("peer-project");
1833            store.save_session(&session).await.unwrap();
1834        }
1835    }
1836
1837    struct PausedPeerAdmission {
1838        inner: Arc<FileSessionInbox>,
1839        entered: Arc<tokio::sync::Notify>,
1840        release: Arc<tokio::sync::Notify>,
1841    }
1842
1843    #[async_trait]
1844    impl SessionInboxPort for PausedPeerAdmission {
1845        async fn deliver(
1846            &self,
1847            envelope: &SessionMessageEnvelope,
1848        ) -> Result<SessionInboxReceipt, SessionInboxError> {
1849            self.inner.deliver(envelope).await
1850        }
1851
1852        async fn deliver_peer_message(
1853            &self,
1854            envelope: &SessionMessageEnvelope,
1855            source: &ActorSession,
1856            target: &ActorSession,
1857            reply_to: Option<&SessionMessageEnvelope>,
1858        ) -> Result<SessionInboxReceipt, SessionInboxError> {
1859            // The engine has already captured both births and built its id;
1860            // actual filesystem admission has not yet acquired its authority.
1861            self.entered.notify_one();
1862            self.release.notified().await;
1863            self.inner
1864                .deliver_peer_message(envelope, source, target, reply_to)
1865                .await
1866        }
1867
1868        async fn mark_activation_eligible(
1869            &self,
1870            target: &str,
1871            generation: u64,
1872            policy: SessionActivationPolicy,
1873        ) -> Result<(), SessionInboxError> {
1874            self.inner
1875                .mark_activation_eligible(target, generation, policy)
1876                .await
1877        }
1878
1879        async fn claim(
1880            &self,
1881            target: &str,
1882            limit: usize,
1883        ) -> Result<Vec<bamboo_domain::SessionInboxClaim>, SessionInboxError> {
1884            self.inner.claim(target, limit).await
1885        }
1886
1887        async fn was_admitted(
1888            &self,
1889            target: &str,
1890            id: &SessionMessageId,
1891        ) -> Result<bool, SessionInboxError> {
1892            self.inner.was_admitted(target, id).await
1893        }
1894
1895        async fn ack(
1896            &self,
1897            target: &str,
1898            claim: &bamboo_domain::SessionInboxClaim,
1899        ) -> Result<(), SessionInboxError> {
1900            self.inner.ack(target, claim).await
1901        }
1902
1903        async fn inspect(
1904            &self,
1905            target: &str,
1906        ) -> Result<bamboo_domain::SessionInboxBacklog, SessionInboxError> {
1907            self.inner.inspect(target).await
1908        }
1909    }
1910
1911    #[tokio::test]
1912    async fn peer_send_and_reply_reject_either_lifetime_recreated_before_admission() {
1913        for reply in [false, true] {
1914            for replaced_id in ["peer-left", "peer-right"] {
1915                let (_temp, store, activation, messenger) = fixture().await;
1916                seed_peer_tree(store.as_ref()).await;
1917                let received_id = if reply {
1918                    let incoming = messenger
1919                        .send_peer("peer-left", "peer-right", "incoming", "question")
1920                        .await
1921                        .unwrap();
1922                    admit_peer_for_test(store.as_ref(), messenger.inbox().as_ref(), "peer-right")
1923                        .await;
1924                    Some(incoming.delivery.id.to_string())
1925                } else {
1926                    None
1927                };
1928                activation.calls.lock().await.clear();
1929                let (caller, target) = if reply {
1930                    ("peer-right", "peer-left")
1931                } else {
1932                    ("peer-left", "peer-right")
1933                };
1934                let entered = Arc::new(tokio::sync::Notify::new());
1935                let release = Arc::new(tokio::sync::Notify::new());
1936                let inbox = Arc::new(FileSessionInbox::new(
1937                    store.clone(),
1938                    SessionInboxLimits::default(),
1939                ));
1940                let paused = SessionMessenger::new(
1941                    store.clone(),
1942                    Arc::new(PausedPeerAdmission {
1943                        inner: inbox.clone(),
1944                        entered: entered.clone(),
1945                        release: release.clone(),
1946                    }),
1947                    activation.clone(),
1948                );
1949                let sending = tokio::spawn(async move {
1950                    if let Some(id) = received_id {
1951                        paused
1952                            .reply_peer(caller, &id, "racing", "old lifetime")
1953                            .await
1954                    } else {
1955                        paused
1956                            .send_peer(caller, target, "racing", "old lifetime")
1957                            .await
1958                    }
1959                });
1960                tokio::time::timeout(std::time::Duration::from_secs(5), entered.notified())
1961                    .await
1962                    .unwrap();
1963                let old = store.load_session(replaced_id).await.unwrap().unwrap();
1964                assert!(store.delete_session(replaced_id).await.unwrap());
1965                let root = store.load_session("peer-root").await.unwrap().unwrap();
1966                let mut replacement =
1967                    Session::new_child_of(replaced_id, &root, "model", "Replacement");
1968                replacement.created_at = old.created_at + chrono::Duration::seconds(1);
1969                replacement.set_project_id_meta("peer-project");
1970                store.save_session(&replacement).await.unwrap();
1971                release.notify_one();
1972                let result = tokio::time::timeout(std::time::Duration::from_secs(5), sending)
1973                    .await
1974                    .unwrap()
1975                    .unwrap();
1976                assert!(result.is_err(), "reply={reply}, replaced={replaced_id}");
1977                let backlog = inbox.inspect(target).await.unwrap();
1978                assert_eq!(backlog.generation, 0);
1979                assert_eq!(backlog.pending, 0);
1980                assert!(!backlog.activation_pending());
1981                assert!(activation.calls.lock().await.is_empty());
1982                // Same ids, tree and Project are legal for a fresh operation;
1983                // only the stale captured lifetime must fail closed.
1984                let current = messenger
1985                    .send_peer(caller, target, "current-lifetime", "current")
1986                    .await
1987                    .unwrap();
1988                assert_eq!(current.delivery.generation, 1);
1989                assert_eq!(activation.calls.lock().await.len(), 1);
1990            }
1991        }
1992    }
1993
1994    #[tokio::test]
1995    async fn peer_reply_revalidates_received_proof_after_concurrent_clear_or_save() {
1996        for clear in [true, false] {
1997            let (_temp, store, activation, messenger) = fixture().await;
1998            seed_peer_tree(store.as_ref()).await;
1999            let incoming = messenger
2000                .send_peer("peer-left", "peer-right", "incoming", "question")
2001                .await
2002                .unwrap();
2003            admit_peer_for_test(store.as_ref(), messenger.inbox().as_ref(), "peer-right").await;
2004            activation.calls.lock().await.clear();
2005            let before = store.load_session("peer-right").await.unwrap().unwrap();
2006            let received_id = incoming.delivery.id.to_string();
2007            let entered = Arc::new(tokio::sync::Notify::new());
2008            let release = Arc::new(tokio::sync::Notify::new());
2009            let inbox = Arc::new(FileSessionInbox::new(
2010                store.clone(),
2011                SessionInboxLimits::default(),
2012            ));
2013            let paused = SessionMessenger::new(
2014                store.clone(),
2015                Arc::new(PausedPeerAdmission {
2016                    inner: inbox.clone(),
2017                    entered: entered.clone(),
2018                    release: release.clone(),
2019                }),
2020                activation.clone(),
2021            );
2022            let old_id = received_id.clone();
2023            let replying = tokio::spawn(async move {
2024                paused
2025                    .reply_peer("peer-right", &old_id, "racing-proof", "reply")
2026                    .await
2027            });
2028            tokio::time::timeout(std::time::Duration::from_secs(5), entered.notified())
2029                .await
2030                .unwrap();
2031            if clear {
2032                assert!(store.clear_session("peer-right").await.unwrap());
2033            } else {
2034                let mut changed = before.clone();
2035                changed.messages.retain(|message| message.id != received_id);
2036                store.save_session(&changed).await.unwrap();
2037            }
2038            let after = store.load_session("peer-right").await.unwrap().unwrap();
2039            assert_eq!(before.created_at, after.created_at);
2040            assert_eq!(before.root_session_id, after.root_session_id);
2041            assert_eq!(before.project_id_meta(), after.project_id_meta());
2042            assert!(!after
2043                .messages
2044                .iter()
2045                .any(|message| message.id == received_id));
2046            release.notify_one();
2047            let result = tokio::time::timeout(std::time::Duration::from_secs(5), replying)
2048                .await
2049                .unwrap()
2050                .unwrap();
2051            assert!(
2052                result.is_err(),
2053                "clear={clear}: removed proof must not admit a reply"
2054            );
2055            let backlog = inbox.inspect("peer-left").await.unwrap();
2056            assert_eq!(backlog.generation, 0);
2057            assert_eq!(backlog.pending, 0);
2058            assert!(!backlog.activation_pending());
2059            assert!(activation.calls.lock().await.is_empty());
2060
2061            let current = messenger
2062                .send_peer("peer-left", "peer-right", "current-question", "current")
2063                .await
2064                .unwrap();
2065            admit_peer_for_test(store.as_ref(), messenger.inbox().as_ref(), "peer-right").await;
2066            let reply = messenger
2067                .reply_peer(
2068                    "peer-right",
2069                    current.delivery.id.as_str(),
2070                    "racing-proof",
2071                    "reply",
2072                )
2073                .await
2074                .unwrap();
2075            let retry = messenger
2076                .reply_peer(
2077                    "peer-right",
2078                    current.delivery.id.as_str(),
2079                    "racing-proof",
2080                    "reply",
2081                )
2082                .await
2083                .unwrap();
2084            assert_eq!(reply.delivery.generation, 1);
2085            assert_eq!(retry.delivery.generation, reply.delivery.generation);
2086            assert_eq!(retry.delivery.id, reply.delivery.id);
2087            let backlog = inbox.inspect("peer-left").await.unwrap();
2088            assert_eq!(backlog.generation, 1);
2089            assert_eq!(backlog.pending, 1);
2090        }
2091    }
2092
2093    #[tokio::test]
2094    async fn peer_admission_without_integrated_lifetime_fence_is_unsupported() {
2095        let (_temp, store, activation, _messenger) = fixture().await;
2096        seed_peer_tree(store.as_ref()).await;
2097        let inbox = Arc::new(FileSessionInbox::new(
2098            store.clone(),
2099            SessionInboxLimits::default(),
2100        ));
2101        let messenger = SessionMessenger::new(
2102            store,
2103            Arc::new(FailEligibilityOnce {
2104                inner: inbox.clone(),
2105                fail_next: std::sync::atomic::AtomicBool::new(false),
2106            }),
2107            activation.clone(),
2108        );
2109        let error = messenger
2110            .send_peer("peer-left", "peer-right", "unsupported", "message")
2111            .await
2112            .unwrap_err();
2113        assert!(error
2114            .to_string()
2115            .contains("lifetime-fenced peer SessionInbox admission is unsupported"));
2116        for intent in [
2117            SessionMessageDeliveryIntent::QueueOnly,
2118            SessionMessageDeliveryIntent::TriggerTurn,
2119        ] {
2120            assert!(messenger
2121                .send_peer_with_intent(
2122                    "peer-left",
2123                    "peer-right",
2124                    "unsupported-explicit",
2125                    "message",
2126                    intent
2127                )
2128                .await
2129                .unwrap_err()
2130                .to_string()
2131                .contains("explicit peer delivery intent is unsupported"));
2132        }
2133        assert_eq!(inbox.inspect("peer-right").await.unwrap().pending, 0);
2134        assert!(activation.calls.lock().await.is_empty());
2135    }
2136
2137    async fn admit_peer_for_test(
2138        store: &dyn Storage,
2139        inbox: &dyn SessionInboxPort,
2140        target: &str,
2141    ) -> SessionMessageEnvelope {
2142        let claims = inbox.claim(target, 16).await.unwrap();
2143        assert_eq!(claims.len(), 1);
2144        let claim = &claims[0];
2145        let mut session = store.load_session(target).await.unwrap().unwrap();
2146        session.add_message(claim.envelope.to_provider_message().unwrap());
2147        store.save_session(&session).await.unwrap();
2148        inbox.ack(target, claim).await.unwrap();
2149        claim.envelope.clone()
2150    }
2151
2152    #[tokio::test]
2153    async fn peer_api_discovers_and_messages_parent_child_siblings_and_cousins() {
2154        let (_temp, store, activation, messenger) = fixture().await;
2155        seed_peer_tree(store.as_ref()).await;
2156        store
2157            .save_session(&Session::new("foreign-root", "model"))
2158            .await
2159            .unwrap();
2160        let targets = messenger.list_peer_targets("peer-left-leaf").await.unwrap();
2161        assert!(!targets.truncated);
2162        let ids: Vec<_> = targets
2163            .targets
2164            .iter()
2165            .map(|target| target.session_id.as_str())
2166            .collect();
2167        assert_eq!(
2168            ids,
2169            ["peer-root", "peer-left", "peer-right", "peer-right-leaf"]
2170        );
2171        assert_eq!(targets.targets[0].title, "Logical root");
2172        assert!(targets.targets[0].parent_session_id.is_none());
2173        assert!(!serde_json::to_string(&targets)
2174            .unwrap()
2175            .contains("foreign-root"));
2176
2177        for (index, (source, target)) in [
2178            ("peer-root", "peer-left"),
2179            ("peer-left", "peer-root"),
2180            ("peer-left", "peer-right"),
2181            ("peer-left-leaf", "peer-right-leaf"),
2182        ]
2183        .into_iter()
2184        .enumerate()
2185        {
2186            let receipt = messenger
2187                .send_peer(
2188                    source,
2189                    target,
2190                    &format!("operation-{index}"),
2191                    "bounded peer instruction",
2192                )
2193                .await
2194                .unwrap();
2195            let backlog = messenger.inbox().inspect(target).await.unwrap();
2196            assert_eq!(
2197                backlog.coordinator_generation, 0,
2198                "peer permission must not release a prefix"
2199            );
2200            assert!(backlog.interrupt_pending());
2201            let envelope =
2202                admit_peer_for_test(store.as_ref(), messenger.inbox().as_ref(), target).await;
2203            assert_eq!(envelope.id, receipt.delivery.id);
2204            assert_eq!(
2205                envelope.source,
2206                SessionMessageSource::Session {
2207                    session_id: source.into()
2208                }
2209            );
2210            assert_eq!(envelope.thread_id.as_deref(), Some(envelope.id.as_str()));
2211            assert_eq!(
2212                envelope.correlation_id.as_deref(),
2213                Some(format!("operation-{index}").as_str())
2214            );
2215            assert!(envelope.in_reply_to.is_none());
2216        }
2217        assert_eq!(activation.calls.lock().await.len(), 4);
2218    }
2219
2220    #[tokio::test]
2221    async fn peer_api_denies_unknown_deleted_cross_root_and_invalid_project_or_lineage() {
2222        let (_temp, store, activation, messenger) = fixture().await;
2223        seed_peer_tree(store.as_ref()).await;
2224        store
2225            .save_session(&Session::new("foreign-root", "model"))
2226            .await
2227            .unwrap();
2228        for (source, target) in [
2229            ("peer-left", "foreign-root"),
2230            ("unknown", "peer-left"),
2231            ("peer-left", "unknown"),
2232            ("peer-left", "peer-left"),
2233        ] {
2234            assert!(messenger
2235                .send_peer(source, target, "denied-operation", "instruction")
2236                .await
2237                .is_err());
2238        }
2239        store.delete_session("peer-right-leaf").await.unwrap();
2240        assert!(matches!(
2241            messenger
2242                .send_peer("peer-left", "peer-right-leaf", "deleted", "instruction")
2243                .await,
2244            Err(SessionMessengerError::TargetNotFound(_))
2245        ));
2246        assert!(matches!(
2247            messenger
2248                .send_peer(
2249                    "peer-right-leaf",
2250                    "peer-left",
2251                    "deleted-source",
2252                    "instruction"
2253                )
2254                .await,
2255            Err(SessionMessengerError::SourceNotFound(_))
2256        ));
2257
2258        // Invalid canonical snapshots exercise the read boundary without
2259        // relaxing the production store's immutable lineage writer checks.
2260        let memory = Arc::new(PeerMemoryStorage::default());
2261        seed_peer_tree(memory.as_ref()).await;
2262        let strict = SessionMessenger::new(
2263            memory.clone(),
2264            messenger.inbox().clone(),
2265            activation.clone(),
2266        );
2267        let original = memory.load_session("peer-right").await.unwrap().unwrap();
2268        for defect in [
2269            "project",
2270            "missing_project",
2271            "project_mirror",
2272            "parent",
2273            "depth",
2274            "root",
2275            "cycle",
2276        ] {
2277            let mut invalid = original.clone();
2278            match defect {
2279                "project" => invalid.set_project_id_meta("other-project"),
2280                "missing_project" => {
2281                    invalid.metadata.remove("project_id");
2282                    if let Some(metadata) = invalid.runtime_metadata.as_mut() {
2283                        metadata.project_id = None;
2284                    }
2285                }
2286                "project_mirror" => {
2287                    invalid
2288                        .metadata
2289                        .insert("project_id".into(), "other-project".into());
2290                }
2291                "parent" => invalid.parent_session_id = Some("missing-parent".into()),
2292                "depth" => invalid.spawn_depth += 1,
2293                "root" => invalid.root_session_id = "foreign-root".into(),
2294                "cycle" => invalid.parent_session_id = Some("peer-right-leaf".into()),
2295                _ => unreachable!(),
2296            }
2297            memory.save_session(&invalid).await.unwrap();
2298            assert!(
2299                strict
2300                    .send_peer("peer-left", "peer-right", "invalid", "instruction")
2301                    .await
2302                    .is_err(),
2303                "{defect}"
2304            );
2305            let targets = strict.list_peer_targets("peer-left").await.unwrap();
2306            assert!(
2307                !targets
2308                    .targets
2309                    .iter()
2310                    .any(|target| target.session_id == "peer-right"),
2311                "{defect}"
2312            );
2313            memory.save_session(&original).await.unwrap();
2314        }
2315        assert!(activation.calls.lock().await.is_empty());
2316        assert_eq!(
2317            messenger
2318                .inbox()
2319                .inspect("peer-left")
2320                .await
2321                .unwrap()
2322                .generation,
2323            0
2324        );
2325    }
2326
2327    #[tokio::test]
2328    async fn peer_api_discovery_is_bounded_and_sender_is_runtime_selected() {
2329        let (_temp, _store, activation, messenger) = fixture().await;
2330        let memory = Arc::new(PeerMemoryStorage::default());
2331        let root = Session::new("many-peers", "model");
2332        memory.save_session(&root).await.unwrap();
2333        for index in 0..MAX_PEER_TARGETS + 2 {
2334            let child = Session::new_child_of(format!("peer-{index:03}"), &root, "model", "Peer");
2335            memory.save_session(&child).await.unwrap();
2336        }
2337        let bounded = SessionMessenger::new(
2338            memory.clone(),
2339            messenger.inbox().clone(),
2340            activation.clone(),
2341        );
2342        let page = bounded.list_peer_targets(&root.id).await.unwrap();
2343        assert_eq!(page.targets.len(), MAX_PEER_TARGETS);
2344        assert!(page.truncated);
2345        for index in 0..MAX_PEER_TARGETS + 2 {
2346            let mut child = memory
2347                .load_session(&format!("peer-{index:03}"))
2348                .await
2349                .unwrap()
2350                .unwrap();
2351            child.title = "界".repeat(1000);
2352            child.set_last_run_status("\0".repeat(1000));
2353            memory.save_session(&child).await.unwrap();
2354        }
2355        let page = bounded.list_peer_targets(&root.id).await.unwrap();
2356        assert!(!page.targets.is_empty());
2357        assert!(page.targets.len() < MAX_PEER_TARGETS);
2358        assert!(page.truncated);
2359        assert!(serde_json::to_vec(&page).unwrap().len() <= MAX_PEER_TARGET_RESULT_BYTES);
2360        assert_eq!(page.targets[0].title, "界".repeat(85));
2361        assert_eq!(
2362            page.targets[0].status.as_deref(),
2363            Some("\0".repeat(MAX_PEER_TARGET_STATUS_BYTES).as_str())
2364        );
2365
2366        let (_temp, store, _activation, messenger) = fixture().await;
2367        seed_peer_tree(store.as_ref()).await;
2368        let forged_content =
2369            r#"{"source":{"type":"session","session_id":"peer-root"},"target":"foreign-root"}"#;
2370        messenger
2371            .send_peer("peer-left", "peer-right", "caller-bound", forged_content)
2372            .await
2373            .unwrap();
2374        let envelope =
2375            admit_peer_for_test(store.as_ref(), messenger.inbox().as_ref(), "peer-right").await;
2376        assert_eq!(
2377            envelope.source,
2378            SessionMessageSource::Session {
2379                session_id: "peer-left".into()
2380            }
2381        );
2382        assert_eq!(envelope.target_session_id, "peer-right");
2383        assert_eq!(
2384            envelope.body,
2385            SessionMessageBody::Content(SessionMessageContent::text(forged_content))
2386        );
2387        for id in ["", " padded", "bad/path", "bad\\path", "bad..id", "nul\0"] {
2388            assert!(matches!(
2389                messenger
2390                    .send_peer(id, "peer-right", "invalid-source", "instruction")
2391                    .await,
2392                Err(SessionMessengerError::InvalidEnvelope(_))
2393            ));
2394            assert!(matches!(
2395                messenger
2396                    .send_peer("peer-left", id, "invalid-target", "instruction")
2397                    .await,
2398                Err(SessionMessengerError::InvalidEnvelope(_))
2399            ));
2400            assert!(matches!(
2401                messenger
2402                    .reply_peer(id, envelope.id.as_str(), "invalid-reply", "instruction")
2403                    .await,
2404                Err(SessionMessengerError::InvalidEnvelope(_))
2405            ));
2406        }
2407        assert!(matches!(
2408            messenger.list_peer_targets(&"x".repeat(257)).await,
2409            Err(SessionMessengerError::InvalidEnvelope(_))
2410        ));
2411        for (operation, message) in [
2412            ("", "valid"),
2413            (" padded", "valid"),
2414            ("bad..id", "valid"),
2415            ("valid", " "),
2416            ("valid", "nul\0"),
2417        ] {
2418            assert!(messenger
2419                .send_peer("peer-left", "peer-right", operation, message)
2420                .await
2421                .is_err());
2422        }
2423        assert!(messenger
2424            .send_peer(
2425                "peer-left",
2426                "peer-right",
2427                "oversized",
2428                &"x".repeat(MAX_PEER_MESSAGE_BYTES + 1)
2429            )
2430            .await
2431            .is_err());
2432    }
2433
2434    #[tokio::test]
2435    async fn peer_api_retry_dedupes_before_claim_and_after_ack_restart_but_rejects_changed_payload()
2436    {
2437        let (temp, store, activation, messenger) = fixture().await;
2438        seed_peer_tree(store.as_ref()).await;
2439        let first = messenger
2440            .send_peer("peer-left", "peer-right", "stable-operation", "original")
2441            .await
2442            .unwrap();
2443        let retry = messenger
2444            .send_peer("peer-left", "peer-right", "stable-operation", "original")
2445            .await
2446            .unwrap();
2447        assert_eq!(retry.delivery, first.delivery);
2448        assert!(matches!(
2449            messenger
2450                .send_peer("peer-left", "peer-right", "stable-operation", "changed")
2451                .await,
2452            Err(SessionMessengerError::Inbox(
2453                SessionInboxError::InvalidClaim(_)
2454            ))
2455        ));
2456        assert_eq!(
2457            messenger
2458                .inbox()
2459                .inspect("peer-right")
2460                .await
2461                .unwrap()
2462                .generation,
2463            1
2464        );
2465        admit_peer_for_test(store.as_ref(), messenger.inbox().as_ref(), "peer-right").await;
2466        drop(messenger);
2467        drop(store);
2468        let reopened = Arc::new(SessionStoreV2::new(temp.path().into()).await.unwrap());
2469        let inbox = Arc::new(FileSessionInbox::new(
2470            reopened.clone(),
2471            SessionInboxLimits::default(),
2472        ));
2473        let messenger = SessionMessenger::new(reopened, inbox.clone(), activation);
2474        let after_ack = messenger
2475            .send_peer("peer-left", "peer-right", "stable-operation", "original")
2476            .await
2477            .unwrap();
2478        assert_eq!(after_ack.delivery, first.delivery);
2479        assert!(inbox.claim("peer-right", 16).await.unwrap().is_empty());
2480        assert!(matches!(
2481            messenger
2482                .send_peer("peer-left", "peer-right", "stable-operation", "changed")
2483                .await,
2484            Err(SessionMessengerError::Inbox(
2485                SessionInboxError::InvalidClaim(_)
2486            ))
2487        ));
2488        assert_eq!(inbox.inspect("peer-right").await.unwrap().generation, 1);
2489    }
2490
2491    #[tokio::test]
2492    async fn peer_api_reply_recovers_durable_recipient_and_thread_after_restart() {
2493        let (temp, store, activation, messenger) = fixture().await;
2494        seed_peer_tree(store.as_ref()).await;
2495        let first = messenger
2496            .send_peer("peer-left-leaf", "peer-right-leaf", "initial", "question")
2497            .await
2498            .unwrap();
2499        let incoming = admit_peer_for_test(
2500            store.as_ref(),
2501            messenger.inbox().as_ref(),
2502            "peer-right-leaf",
2503        )
2504        .await;
2505        drop(messenger);
2506        drop(store);
2507        let store = Arc::new(SessionStoreV2::new(temp.path().into()).await.unwrap());
2508        let inbox = Arc::new(FileSessionInbox::new(
2509            store.clone(),
2510            SessionInboxLimits::default(),
2511        ));
2512        let messenger = SessionMessenger::new(store.clone(), inbox.clone(), activation);
2513        let reply = messenger
2514            .reply_peer(
2515                "peer-right-leaf",
2516                first.delivery.id.as_str(),
2517                "answer",
2518                "answer text",
2519            )
2520            .await
2521            .unwrap();
2522        let retry = messenger
2523            .reply_peer(
2524                "peer-right-leaf",
2525                first.delivery.id.as_str(),
2526                "answer",
2527                "answer text",
2528            )
2529            .await
2530            .unwrap();
2531        assert_eq!(reply.delivery, retry.delivery);
2532        let response = admit_peer_for_test(store.as_ref(), inbox.as_ref(), "peer-left-leaf").await;
2533        assert_eq!(
2534            response.source,
2535            SessionMessageSource::Session {
2536                session_id: "peer-right-leaf".into()
2537            }
2538        );
2539        assert_eq!(response.in_reply_to, Some(incoming.id.clone()));
2540        assert_eq!(response.thread_id, incoming.thread_id);
2541        assert_eq!(response.correlation_id.as_deref(), Some("answer"));
2542        let second_reply = messenger
2543            .reply_peer("peer-left-leaf", response.id.as_str(), "thanks", "thanks")
2544            .await
2545            .unwrap();
2546        let second_response =
2547            admit_peer_for_test(store.as_ref(), inbox.as_ref(), "peer-right-leaf").await;
2548        assert_eq!(second_reply.delivery.id, second_response.id);
2549        assert_eq!(second_response.in_reply_to, Some(response.id));
2550        assert_eq!(
2551            second_response.thread_id.as_deref(),
2552            Some(first.delivery.id.as_str())
2553        );
2554        let after_ack = messenger
2555            .reply_peer(
2556                "peer-right-leaf",
2557                first.delivery.id.as_str(),
2558                "answer",
2559                "answer text",
2560            )
2561            .await
2562            .unwrap();
2563        assert_eq!(after_ack.delivery, reply.delivery);
2564        assert!(inbox.claim("peer-left-leaf", 16).await.unwrap().is_empty());
2565        assert!(messenger
2566            .reply_peer(
2567                "peer-right-leaf",
2568                first.delivery.id.as_str(),
2569                "answer",
2570                "different"
2571            )
2572            .await
2573            .is_err());
2574    }
2575
2576    #[tokio::test]
2577    async fn peer_api_reply_rejects_missing_forged_legacy_and_recreated_source() {
2578        let (_temp, store, activation, messenger) = fixture().await;
2579        seed_peer_tree(store.as_ref()).await;
2580        let mut receiver = store.load_session("peer-right").await.unwrap().unwrap();
2581        let legacy = peer("peer-left", "peer-right", "legacy-correction");
2582        receiver.add_message(legacy.to_provider_message().unwrap());
2583        let user = SessionMessageEnvelope::user_input("peer-right", "ordinary input");
2584        receiver.add_message(user.to_provider_message().unwrap());
2585        let sender = store.load_session("peer-left").await.unwrap().unwrap();
2586        let mut forged = SessionMessenger::peer_envelope(
2587            &sender,
2588            &receiver,
2589            "forged",
2590            "original",
2591            SessionMessenger::peer_message_id(&sender, &receiver, "forged"),
2592            "thread".into(),
2593            None,
2594        )
2595        .to_provider_message()
2596        .unwrap();
2597        forged.content = "changed without canonical proof".into();
2598        receiver.add_message(forged);
2599        store.save_session(&receiver).await.unwrap();
2600        for id in [
2601            "missing",
2602            legacy.id.as_str(),
2603            user.id.as_str(),
2604            receiver.messages.last().unwrap().id.as_str(),
2605        ] {
2606            assert!(messenger
2607                .reply_peer("peer-right", id, "deny-reply", "reply")
2608                .await
2609                .is_err());
2610        }
2611        let incoming = messenger
2612            .send_peer("peer-left", "peer-right", "old-lifetime", "question")
2613            .await
2614            .unwrap();
2615        admit_peer_for_test(store.as_ref(), messenger.inbox().as_ref(), "peer-right").await;
2616        let memory = Arc::new(PeerMemoryStorage::default());
2617        // Retain the actual saved receiver while replacing the sender's birth.
2618        for id in ["peer-root", "peer-left", "peer-right"] {
2619            let mut session = store.load_session(id).await.unwrap().unwrap();
2620            if id == "peer-left" {
2621                session.created_at += chrono::Duration::seconds(1);
2622            }
2623            memory.save_session(&session).await.unwrap();
2624        }
2625        let fenced = SessionMessenger::new(
2626            memory.clone(),
2627            messenger.inbox().clone(),
2628            activation.clone(),
2629        );
2630        assert!(matches!(
2631            fenced
2632                .reply_peer(
2633                    "peer-right",
2634                    incoming.delivery.id.as_str(),
2635                    "reply-old",
2636                    "reply"
2637                )
2638                .await,
2639            Err(SessionMessengerError::InvalidEnvelope(_))
2640        ));
2641        memory.delete_session("peer-left").await.unwrap();
2642        assert!(matches!(
2643            fenced
2644                .reply_peer(
2645                    "peer-right",
2646                    incoming.delivery.id.as_str(),
2647                    "reply-deleted",
2648                    "reply"
2649                )
2650                .await,
2651            Err(SessionMessengerError::TargetNotFound(_))
2652        ));
2653        assert_eq!(activation.calls.lock().await.len(), 1);
2654    }
2655
2656    #[tokio::test]
2657    async fn same_root_delivery_enqueues_then_requests_activation() {
2658        let (_temp, store, activation, messenger) = fixture().await;
2659        let root = Session::new("root", "model");
2660        let mut child = Session::new("child", "model");
2661        child.kind = bamboo_domain::SessionKind::Child;
2662        child.parent_session_id = Some("root".to_string());
2663        child.root_session_id = "root".to_string();
2664        store.save_session(&root).await.unwrap();
2665        store.save_session(&child).await.unwrap();
2666
2667        let receipt = messenger
2668            .send(peer("root", "child", "msg-1"))
2669            .await
2670            .unwrap();
2671        assert_eq!(receipt.delivery.generation, 1);
2672        assert_eq!(
2673            activation.calls.lock().await.as_slice(),
2674            &[("child".to_string(), 1)]
2675        );
2676        assert_eq!(messenger.metrics().snapshot().delivered, 1);
2677    }
2678
2679    #[tokio::test]
2680    async fn staged_delivery_without_watermark_reports_exact_retry_required() {
2681        let temp = TempDir::new().unwrap();
2682        let store = Arc::new(
2683            SessionStoreV2::new(temp.path().to_path_buf())
2684                .await
2685                .unwrap(),
2686        );
2687        let root = Session::new("root", "model");
2688        let mut child = Session::new("child", "model");
2689        child.kind = bamboo_domain::SessionKind::Child;
2690        child.parent_session_id = Some("root".to_string());
2691        child.root_session_id = "root".to_string();
2692        store.save_session(&root).await.unwrap();
2693        store.save_session(&child).await.unwrap();
2694        let inbox = Arc::new(FailEligibilityOnce {
2695            inner: Arc::new(FileSessionInbox::new(
2696                store.clone(),
2697                SessionInboxLimits::default(),
2698            )),
2699            fail_next: std::sync::atomic::AtomicBool::new(true),
2700        });
2701        let activation = Arc::new(RecordingActivation {
2702            calls: tokio::sync::Mutex::new(Vec::new()),
2703        });
2704        let messenger = SessionMessenger::new(store.clone(), inbox, activation.clone());
2705        let envelope = peer("root", "child", "watermark-failure");
2706        let gate = bamboo_domain::AdmissionGate::default();
2707
2708        let admission = messenger
2709            .admit_with_gate(envelope.clone(), Some(&gate))
2710            .await
2711            .unwrap();
2712        let receipt = match messenger.activate(&admission).await.unwrap_err() {
2713            SessionMessengerError::ActivationEligibility { receipt, .. } => receipt,
2714            error => panic!("expected retry-required classification: {error}"),
2715        };
2716        assert!(gate.is_committed());
2717        assert!(activation.calls.lock().await.is_empty());
2718        let reopened = FileSessionInbox::new(store, SessionInboxLimits::default());
2719        let backlog = reopened.inspect("child").await.unwrap();
2720        assert_eq!(backlog.pending + backlog.claimed, 1);
2721        assert_eq!(backlog.activation_generation, 0);
2722        assert_eq!(backlog.generation, receipt.generation);
2723
2724        let retry = messenger
2725            .admit_with_gate(envelope, Some(&gate))
2726            .await
2727            .unwrap();
2728        let retried = messenger.activate(&retry).await.unwrap();
2729        assert_eq!(retried.delivery, receipt);
2730        assert_eq!(activation.calls.lock().await.len(), 1);
2731        let reopened = reopened.inspect("child").await.unwrap();
2732        assert_eq!(reopened.activation_generation, receipt.generation);
2733        assert_eq!(reopened.pending + reopened.claimed, 1);
2734    }
2735
2736    #[tokio::test]
2737    async fn cross_root_peer_is_rejected_before_enqueue() {
2738        let (_temp, store, activation, messenger) = fixture().await;
2739        store
2740            .save_session(&Session::new("root-a", "model"))
2741            .await
2742            .unwrap();
2743        store
2744            .save_session(&Session::new("root-b", "model"))
2745            .await
2746            .unwrap();
2747
2748        let error = messenger
2749            .send(peer("root-a", "root-b", "msg-2"))
2750            .await
2751            .unwrap_err();
2752        assert!(matches!(error, SessionMessengerError::Unauthorized { .. }));
2753        assert!(activation.calls.lock().await.is_empty());
2754        let metrics = messenger.metrics().snapshot();
2755        assert_eq!(metrics.rejected, 1);
2756        assert_eq!(metrics.unauthorized, 1);
2757        assert_eq!(metrics.payload_too_large, 0);
2758        assert_eq!(metrics.backlog_full, 0);
2759    }
2760
2761    #[tokio::test]
2762    async fn same_root_different_project_peer_is_rejected_before_enqueue() {
2763        let (_temp, store, activation, messenger) = fixture().await;
2764        let mut source = Session::new("project-root", "model");
2765        source.set_project_id_meta("project-a");
2766        let mut target = Session::new("project-child", "model");
2767        target.kind = bamboo_domain::SessionKind::Child;
2768        target.parent_session_id = Some(source.id.clone());
2769        target.root_session_id = source.id.clone();
2770        target.set_project_id_meta("project-b");
2771        store.save_session(&source).await.unwrap();
2772        store.save_session(&target).await.unwrap();
2773
2774        let error = messenger
2775            .send(peer(&source.id, &target.id, "different-project"))
2776            .await
2777            .unwrap_err();
2778        assert!(matches!(error, SessionMessengerError::Unauthorized { .. }));
2779        assert!(activation.calls.lock().await.is_empty());
2780        let metrics = messenger.metrics().snapshot();
2781        assert_eq!(metrics.delivered, 0);
2782        assert_eq!(metrics.rejected, 1);
2783        assert_eq!(metrics.unauthorized, 1);
2784    }
2785
2786    #[tokio::test]
2787    async fn limit_rejections_have_distinct_metrics() {
2788        let temp = TempDir::new().unwrap();
2789        let store = Arc::new(
2790            SessionStoreV2::new(temp.path().to_path_buf())
2791                .await
2792                .unwrap(),
2793        );
2794        store
2795            .save_session(&Session::new("target", "model"))
2796            .await
2797            .unwrap();
2798        let activation = Arc::new(RecordingActivation {
2799            calls: tokio::sync::Mutex::new(Vec::new()),
2800        });
2801        let inbox = Arc::new(FileSessionInbox::new(
2802            store.clone(),
2803            SessionInboxLimits {
2804                max_payload_bytes: 512,
2805                max_backlog: 1,
2806                max_claim_batch: 1,
2807            },
2808        ));
2809        let messenger = SessionMessenger::new(store, inbox, activation.clone());
2810
2811        let oversized = SessionMessageEnvelope::user_input("target", "x".repeat(2048));
2812        assert!(matches!(
2813            messenger.send(oversized).await,
2814            Err(SessionMessengerError::Inbox(
2815                SessionInboxError::PayloadTooLarge { .. }
2816            ))
2817        ));
2818        messenger
2819            .send(SessionMessageEnvelope::user_input("target", "first"))
2820            .await
2821            .unwrap();
2822        assert!(matches!(
2823            messenger
2824                .send(SessionMessageEnvelope::user_input("target", "second"))
2825                .await,
2826            Err(SessionMessengerError::Inbox(
2827                SessionInboxError::BacklogFull { .. }
2828            ))
2829        ));
2830
2831        let metrics = messenger.metrics().snapshot();
2832        assert_eq!(metrics.rejected, 2);
2833        assert_eq!(metrics.payload_too_large, 1);
2834        assert_eq!(metrics.backlog_full, 1);
2835        assert_eq!(activation.calls.lock().await.len(), 1);
2836    }
2837
2838    #[tokio::test]
2839    async fn explicit_peer_intent_keeps_legacy_compatibility_and_fences_changed_retry_after_ack() {
2840        let (_temp, store, activation, messenger) = fixture().await;
2841        seed_peer_tree(store.as_ref()).await;
2842        let receipt = messenger
2843            .send_peer_with_intent(
2844                "peer-left",
2845                "peer-right",
2846                "queue-operation",
2847                "input",
2848                SessionMessageDeliveryIntent::QueueOnly,
2849            )
2850            .await
2851            .unwrap();
2852        assert_eq!(receipt.activation, None);
2853        assert!(!receipt.active_delivery_notified);
2854        assert!(activation.calls.lock().await.is_empty());
2855        let original =
2856            admit_peer_for_test(store.as_ref(), messenger.inbox().as_ref(), "peer-right").await;
2857        assert_eq!(
2858            messenger
2859                .send_peer_with_intent(
2860                    "peer-left",
2861                    "peer-right",
2862                    "queue-operation",
2863                    "input",
2864                    SessionMessageDeliveryIntent::QueueOnly
2865                )
2866                .await
2867                .unwrap()
2868                .delivery,
2869            receipt.delivery
2870        );
2871        assert!(messenger
2872            .send_peer_with_intent(
2873                "peer-left",
2874                "peer-right",
2875                "queue-operation",
2876                "input",
2877                SessionMessageDeliveryIntent::TriggerTurn
2878            )
2879            .await
2880            .is_err());
2881        assert!(activation.calls.lock().await.is_empty());
2882        let reply = messenger
2883            .reply_peer_with_intent(
2884                "peer-right",
2885                original.id.as_str(),
2886                "reply-operation",
2887                "reply",
2888                SessionMessageDeliveryIntent::TriggerTurn,
2889            )
2890            .await
2891            .unwrap();
2892        // This fixture records activation requests as reservations. Active
2893        // owner delivery is covered by the real router's safe-point tests.
2894        assert_eq!(
2895            reply.activation,
2896            Some(SessionActivationDisposition::ActivationReserved)
2897        );
2898        assert_eq!(
2899            activation.calls.lock().await.as_slice(),
2900            &[("peer-left".to_string(), reply.delivery.generation)]
2901        );
2902        let claims = messenger.inbox().claim("peer-left", 8).await.unwrap();
2903        assert_eq!(claims[0].envelope.in_reply_to, Some(original.id));
2904        assert_eq!(
2905            claims[0].activation_policy,
2906            SessionActivationPolicy::RespectSpecificWait
2907        );
2908        messenger
2909            .send_peer("peer-left", "peer-right", "legacy-operation", "legacy")
2910            .await
2911            .unwrap();
2912        let claims = messenger.inbox().claim("peer-right", 8).await.unwrap();
2913        assert_eq!(
2914            claims[0].activation_policy,
2915            SessionActivationPolicy::InterruptSpecificWait
2916        );
2917    }
2918
2919    #[tokio::test]
2920    async fn explicit_peer_intents_deny_foreign_roots_before_admission_or_wake() {
2921        let (_temp, store, activation, messenger) = fixture().await;
2922        seed_peer_tree(store.as_ref()).await;
2923        store
2924            .save_session(&Session::new("foreign-root", "model"))
2925            .await
2926            .unwrap();
2927        for intent in [
2928            SessionMessageDeliveryIntent::QueueOnly,
2929            SessionMessageDeliveryIntent::TriggerTurn,
2930        ] {
2931            assert!(matches!(
2932                messenger
2933                    .send_peer_with_intent("peer-left", "foreign-root", "denied", "input", intent)
2934                    .await,
2935                Err(SessionMessengerError::Unauthorized { .. })
2936            ));
2937        }
2938        assert_eq!(
2939            messenger
2940                .inbox()
2941                .inspect("foreign-root")
2942                .await
2943                .unwrap()
2944                .pending,
2945            0
2946        );
2947        assert!(activation.calls.lock().await.is_empty());
2948    }
2949}