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