1use std::collections::{HashMap, VecDeque};
4use std::sync::Arc;
5use std::sync::Mutex as StdMutex;
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::time::Duration;
8
9use async_trait::async_trait;
10use parking_lot::Mutex as SyncMutex;
11use thiserror::Error;
12use tokio::sync::{Mutex, RwLock};
13use tokio::task::JoinHandle;
14
15use awaken_runtime::{ResolveError, RunActivation};
16use awaken_server_contract::contract::commit_coordinator::CommitCoordinator;
17use awaken_server_contract::contract::event::AgentEvent;
18use awaken_server_contract::contract::event_sink::EventSink;
19use awaken_server_contract::contract::mailbox::{MailboxStore, RunDispatchStatus};
20use awaken_server_contract::contract::message::Message;
21use awaken_server_contract::contract::run::{
22 RunActivationSnapshot, RunInputSnapshot, RunIntent, RunKind, RunOptions, RunTraceContext,
23};
24use awaken_server_contract::contract::staged_commit::{
25 OutboxServerEventPublisher, StagedCommitCoordinator,
26};
27use awaken_server_contract::contract::storage::{RunRecord, StorageError, ThreadRunStore};
28use awaken_server_contract::contract::suspension::{ToolCallOutcome, ToolCallResume};
29use awaken_server_contract::contract::tool_intercept::{AdapterKind, RunMode};
30
31use crate::transport::channel_sink::ReconnectableEventSink;
32
33const INLINE_CLAIM_GUARD_MS: u64 = 60_000;
36#[cfg(not(test))]
37const REMOTE_CANCEL_WAIT_MS: u64 = 5_000;
38#[cfg(test)]
39const REMOTE_CANCEL_WAIT_MS: u64 = 250;
40const REMOTE_CANCEL_POLL_MS: u64 = 25;
41const DISPATCH_SIGNAL_BATCH_DEFAULT: usize = 32;
42const DISPATCH_SIGNAL_EXPIRES_DEFAULT: Duration = Duration::from_millis(500);
43const DISPATCH_SIGNAL_ERROR_DELAY: Duration = Duration::from_millis(250);
44const DISPATCH_SIGNAL_BLOCKED_NACK_BASE_DELAY_DEFAULT: Duration = Duration::from_millis(500);
45const DISPATCH_SIGNAL_BLOCKED_NACK_MAX_DELAY_DEFAULT: Duration = Duration::from_secs(30);
46const DISPATCH_SIGNAL_BATCH_ENV: &str = "AWAKEN_DISPATCH_SIGNAL_BATCH_SIZE";
47const DISPATCH_SIGNAL_EXPIRES_ENV: &str = "AWAKEN_DISPATCH_SIGNAL_FETCH_EXPIRES_MS";
48const DISPATCH_SIGNAL_NACK_BASE_DELAY_ENV: &str = "AWAKEN_DISPATCH_SIGNAL_NACK_BASE_DELAY_MS";
49const DISPATCH_SIGNAL_NACK_MAX_DELAY_ENV: &str = "AWAKEN_DISPATCH_SIGNAL_NACK_MAX_DELAY_MS";
50const DISPATCH_SIGNAL_MAX_CONCURRENT_HANDLERS_DEFAULT: usize = 32;
51const DISPATCH_SIGNAL_MAX_CONCURRENT_HANDLERS_ENV: &str =
52 "AWAKEN_DISPATCH_SIGNAL_MAX_CONCURRENT_HANDLERS";
53const TERMINAL_RECONCILE_BATCH: usize = 100;
54const MAILBOX_DEPTH_STATUSES: [RunDispatchStatus; 6] = [
55 RunDispatchStatus::Queued,
56 RunDispatchStatus::Claimed,
57 RunDispatchStatus::Acked,
58 RunDispatchStatus::Cancelled,
59 RunDispatchStatus::Superseded,
60 RunDispatchStatus::DeadLetter,
61];
62
63pub(crate) const ACTIVE_RUN_CONFLICT_MESSAGE: &str =
65 "thread has an active run; cannot claim inline";
66
67pub(super) fn run_activation_snapshot(
68 request: &RunActivation,
69 persisted_input: RunInputSnapshot,
70 resolution_id: Option<String>,
71) -> RunActivationSnapshot {
72 RunActivationSnapshot {
73 intent: request.intent.clone(),
74 input: persisted_input,
75 options: request.options.clone(),
76 trace: request.trace.clone(),
77 seeded_decisions: request.control.seeded_decisions.clone(),
78 resolution_id,
79 }
80}
81
82#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
87pub(super) struct LegacyRunSnapshotExtras {
88 #[serde(default, skip_serializing_if = "Option::is_none")]
89 overrides: Option<awaken_server_contract::contract::inference::InferenceOverride>,
90 #[serde(default, skip_serializing_if = "Vec::is_empty")]
91 decisions: Vec<(
92 String,
93 awaken_server_contract::contract::suspension::ToolCallResume,
94 )>,
95 #[serde(default, skip_serializing_if = "Vec::is_empty")]
96 frontend_tools: Vec<awaken_server_contract::contract::tool::ToolDescriptor>,
97 #[serde(default, skip_serializing_if = "Option::is_none")]
98 continue_run_id: Option<String>,
99 #[serde(default, skip_serializing_if = "Option::is_none")]
100 run_id_hint: Option<String>,
101 #[serde(default, skip_serializing_if = "Option::is_none")]
102 dispatch_id_hint: Option<String>,
103 #[serde(default, skip_serializing_if = "Option::is_none")]
104 parent_thread_id: Option<String>,
105 #[serde(default, skip_serializing_if = "Option::is_none")]
106 transport_request_id: Option<String>,
107 #[serde(default)]
108 run_mode: RunMode,
109 #[serde(default)]
110 adapter: AdapterKind,
111}
112
113impl LegacyRunSnapshotExtras {
114 #[cfg(test)]
115 fn from_request(request: &awaken_runtime::RunActivation) -> Self {
116 Self {
117 overrides: request.options.overrides.clone(),
118 decisions: request.control.seeded_decisions.clone(),
119 frontend_tools: request.options.frontend_tools.clone(),
120 continue_run_id: request.resume_run_id().map(str::to_owned),
121 run_id_hint: request.persistence.run_id_hint.clone(),
122 dispatch_id_hint: request.persistence.dispatch_id_hint.clone(),
123 parent_thread_id: request.trace.parent_thread_id.clone(),
124 transport_request_id: request.trace.transport_request_id.clone(),
125 run_mode: request.trace.run_mode,
126 adapter: request.trace.adapter,
127 }
128 }
129
130 #[cfg(test)]
131 fn to_value(&self) -> Result<Option<serde_json::Value>, serde_json::Error> {
132 if self.overrides.is_none()
133 && self.decisions.is_empty()
134 && self.frontend_tools.is_empty()
135 && self.continue_run_id.is_none()
136 && self.run_id_hint.is_none()
137 && self.dispatch_id_hint.is_none()
138 && self.parent_thread_id.is_none()
139 && self.transport_request_id.is_none()
140 && self.run_mode == RunMode::Foreground
141 && self.adapter == AdapterKind::Internal
142 {
143 Ok(None)
144 } else {
145 serde_json::to_value(self).map(Some)
146 }
147 }
148
149 fn from_value(value: &serde_json::Value) -> Result<Self, serde_json::Error> {
150 serde_json::from_value(value.clone())
151 }
152
153 #[cfg(test)]
154 fn apply_to(self, mut request: awaken_runtime::RunActivation) -> awaken_runtime::RunActivation {
155 if let Some(ov) = self.overrides {
156 request = request.with_overrides(ov);
157 }
158 if !self.decisions.is_empty() {
159 request = request.with_decisions(self.decisions);
160 }
161 if !self.frontend_tools.is_empty() {
162 request = request.with_frontend_tools(self.frontend_tools);
163 }
164 if let Some(crid) = self.continue_run_id {
165 request = request.with_continue_run_id(crid);
166 }
167 if let Some(run_id_hint) = self.run_id_hint {
168 request = request.with_run_id_hint(run_id_hint);
169 }
170 if let Some(dispatch_id_hint) = self.dispatch_id_hint {
171 request = request.with_dispatch_id_hint(dispatch_id_hint);
172 }
173 if let Some(parent_thread_id) = self.parent_thread_id {
174 request = request.with_parent_thread_id(parent_thread_id);
175 }
176 if let Some(transport_request_id) = self.transport_request_id {
177 request = request.with_transport_request_id(transport_request_id);
178 }
179 request
180 .with_run_mode(self.run_mode)
181 .with_adapter(self.adapter)
182 }
183}
184
185pub(super) struct LegacyRunRequestSnapshotAdapter {
186 pub snapshot: awaken_server_contract::contract::storage::RunRequestSnapshot,
187 pub input: RunInputSnapshot,
188 pub resolution_id: Option<String>,
189 pub thread_id: String,
190 pub agent_id: Option<String>,
191 pub parent_run_id: Option<String>,
192 pub extras: Option<LegacyRunSnapshotExtras>,
193}
194
195impl TryFrom<LegacyRunRequestSnapshotAdapter> for RunActivationSnapshot {
196 type Error = String;
197
198 fn try_from(value: LegacyRunRequestSnapshotAdapter) -> Result<Self, Self::Error> {
199 let mut options = RunOptions {
200 overrides: None,
201 frontend_tools: value.snapshot.frontend_tools,
202 };
203 let mut trace = RunTraceContext {
204 parent_run_id: value.parent_run_id,
205 parent_thread_id: value.snapshot.parent_thread_id,
206 origin: value.snapshot.origin.into(),
207 adapter: AdapterKind::Internal,
208 run_mode: RunMode::Resume,
209 dispatch_id: None,
210 session_id: None,
211 transport_request_id: value.snapshot.transport_request_id,
212 correlation_id: None,
213 };
214 let mut kind = RunKind::NewIntent;
215 let mut seeded_decisions = value
216 .snapshot
217 .decisions
218 .into_iter()
219 .map(|decision| (decision.call_id, decision.resume))
220 .collect::<Vec<_>>();
221
222 if let Some(extras) = value.extras {
223 if let Some(overrides) = extras.overrides {
224 options.overrides = Some(overrides);
225 }
226 if !extras.frontend_tools.is_empty() {
227 options.frontend_tools = extras.frontend_tools;
228 }
229 if !extras.decisions.is_empty() {
230 seeded_decisions = extras.decisions;
231 }
232 if let Some(run_id) = extras.continue_run_id {
233 kind = RunKind::HitlResume { run_id };
234 }
235 if let Some(parent_thread_id) = extras.parent_thread_id {
236 trace.parent_thread_id = Some(parent_thread_id);
237 }
238 if let Some(transport_request_id) = extras.transport_request_id {
239 trace.transport_request_id = Some(transport_request_id);
240 }
241 trace.run_mode = extras.run_mode;
242 trace.adapter = extras.adapter;
243 }
244
245 Ok(RunActivationSnapshot {
246 intent: RunIntent {
247 agent_id: value.agent_id,
248 thread_id: value.thread_id,
249 kind,
250 },
251 input: value.input,
252 options,
253 trace,
254 seeded_decisions,
255 resolution_id: value.resolution_id,
256 })
257 }
258}
259
260pub(super) fn legacy_input_snapshot(
261 run: &RunRecord,
262 snapshot: &awaken_server_contract::contract::storage::RunRequestSnapshot,
263) -> RunInputSnapshot {
264 if let Some(input) = run.input.as_ref() {
265 return RunInputSnapshot {
266 thread_id: input.thread_id.clone(),
267 range: input.range,
268 trigger_message_ids: input.trigger_message_ids.clone(),
269 selected_message_ids: input.selected_message_ids.clone(),
270 context_policy: input.context_policy.clone(),
271 compacted_snapshot_id: input.compacted_snapshot_id.clone(),
272 };
273 }
274 RunInputSnapshot {
275 thread_id: run.thread_id.clone(),
276 range: None,
277 trigger_message_ids: snapshot.input_message_ids.clone(),
278 selected_message_ids: Vec::new(),
279 context_policy: None,
280 compacted_snapshot_id: None,
281 }
282}
283
284pub struct TaskDoneMailboxNotify {
292 mailbox: Arc<Mailbox>,
293 thread_id: String,
294 continue_run_id: Option<String>,
295}
296
297impl TaskDoneMailboxNotify {
298 pub fn new(mailbox: Arc<Mailbox>, thread_id: String, continue_run_id: Option<String>) -> Self {
299 Self {
300 mailbox,
301 thread_id,
302 continue_run_id,
303 }
304 }
305}
306
307impl awaken_runtime::inbox::OnInboxClosed for TaskDoneMailboxNotify {
308 fn closed(&self, message: &serde_json::Value) {
309 let mailbox = self.mailbox.clone();
310 let thread_id = self.thread_id.clone();
311 let continue_run_id = self.continue_run_id.clone();
312 let wake_message = awaken_runtime::inbox::inbox_event_message(message);
313
314 tokio::spawn(async move {
316 let mut request = RunActivation::new(thread_id.clone(), vec![wake_message])
317 .with_origin(awaken_server_contract::contract::storage::RunRequestOrigin::Internal)
318 .with_run_mode(RunMode::InternalWake)
319 .with_adapter(AdapterKind::Internal);
320 if let Some(run_id) = continue_run_id {
321 request = request.with_continue_run_id(run_id);
322 }
323 if let Err(e) = mailbox.submit_background(request).await {
324 tracing::warn!(thread_id, error = %e, "failed to enqueue background task wake dispatch");
325 }
326 });
327 }
328}
329
330#[derive(Debug, Clone)]
334pub struct MailboxSubmitResult {
335 pub dispatch_id: String,
336 pub run_id: String,
337 pub thread_id: String,
338 pub status: MailboxDispatchStatus,
339}
340
341#[derive(Debug, Clone, Copy, PartialEq, Eq)]
343pub enum MailboxDispatchStatus {
344 Running,
346 Queued,
348}
349
350#[derive(Debug, Error)]
352pub enum MailboxError {
353 #[error("validation error: {0}")]
354 Validation(String),
355 #[error("store error: {0}")]
356 Store(#[from] StorageError),
357 #[error("resolution error while {context}: {source}")]
358 Resolution {
359 context: &'static str,
360 source: ResolveError,
361 },
362 #[error("internal error: {0}")]
363 Internal(String),
364 #[error("delivery blocked by barrier: pending '{blocking_pending_id}' must be consumed first")]
369 DeliveryBlockedByBarrier { blocking_pending_id: String },
370}
371
372#[derive(Debug)]
374pub enum MailboxRunOutcome {
375 Completed,
377 TransientError(String),
379 PermanentError(String),
381}
382
383impl MailboxRunOutcome {
384 fn metric_label(&self) -> &'static str {
385 match self {
386 Self::Completed => "completed",
387 Self::TransientError(_) => "transient_error",
388 Self::PermanentError(_) => "permanent_error",
389 }
390 }
391}
392
393#[derive(Debug, Clone)]
395pub struct MailboxConfig {
396 pub lease_ms: u64,
398 pub suspended_lease_ms: u64,
401 pub lease_renewal_interval: Duration,
403 pub sweep_interval: Duration,
405 pub gc_interval: Duration,
407 pub gc_ttl: Duration,
409 pub default_max_attempts: u32,
411 pub default_retry_delay_ms: u64,
413 pub max_retry_delay_ms: u64,
415}
416
417impl Default for MailboxConfig {
418 fn default() -> Self {
419 Self {
420 lease_ms: 30_000,
421 suspended_lease_ms: 600_000,
422 lease_renewal_interval: Duration::from_secs(10),
423 sweep_interval: Duration::from_secs(30),
424 gc_interval: Duration::from_secs(60),
425 gc_ttl: Duration::from_secs(24 * 60 * 60),
426 default_max_attempts: 5,
427 default_retry_delay_ms: 250,
428 max_retry_delay_ms: 30_000,
429 }
430 }
431}
432
433pub type MailboxMaintenanceCallback = Arc<dyn Fn() + Send + Sync + 'static>;
435
436#[derive(Clone)]
438pub struct MailboxStartupRecoveryConfig {
439 pub max_attempts: u32,
442 pub retry_delay: Duration,
444}
445
446impl Default for MailboxStartupRecoveryConfig {
447 fn default() -> Self {
448 Self {
449 max_attempts: 1,
450 retry_delay: Duration::from_millis(250),
451 }
452 }
453}
454
455#[derive(Clone)]
457pub struct MailboxLifecycleConfig {
458 pub startup_delay: Duration,
460 pub startup_recovery: MailboxStartupRecoveryConfig,
462 pub maintenance_callback: Option<MailboxMaintenanceCallback>,
464}
465
466impl Default for MailboxLifecycleConfig {
467 fn default() -> Self {
468 Self {
469 startup_delay: Duration::ZERO,
470 startup_recovery: MailboxStartupRecoveryConfig::default(),
471 maintenance_callback: None,
472 }
473 }
474}
475
476#[derive(Clone)]
481pub struct MailboxLifecycleHandle {
482 tasks: Arc<StdMutex<Option<MailboxLifecycleTasks>>>,
483 transition_lock: Arc<Mutex<()>>,
484}
485
486impl MailboxLifecycleHandle {
487 pub fn abort(&self) {
489 if let Some(tasks) = self.tasks.lock().expect("lifecycle lock poisoned").take() {
490 tasks.abort();
491 }
492 }
493
494 pub async fn shutdown(&self) -> Result<(), MailboxError> {
500 let _transition_guard = self.transition_lock.lock().await;
501 let tasks = self.tasks.lock().expect("lifecycle lock poisoned").take();
502 if let Some(tasks) = tasks {
503 tasks.shutdown().await?;
504 }
505 Ok(())
506 }
507
508 pub fn is_running(&self) -> bool {
510 self.tasks
511 .lock()
512 .expect("lifecycle lock poisoned")
513 .is_some()
514 }
515}
516
517struct MailboxLifecycleTasks {
518 recover_task: Option<JoinHandle<()>>,
519 dispatch_signal_task: Option<JoinHandle<()>>,
520 maintenance_task: JoinHandle<()>,
521}
522
523impl MailboxLifecycleTasks {
524 fn abort(self) {
525 if let Some(task) = self.recover_task {
526 task.abort();
527 }
528 if let Some(task) = self.dispatch_signal_task {
529 task.abort();
530 }
531 self.maintenance_task.abort();
532 }
533
534 async fn shutdown(self) -> Result<(), MailboxError> {
535 if let Some(task) = self.recover_task {
536 task.abort();
537 await_lifecycle_task("mailbox startup recovery", task).await?;
538 }
539 if let Some(task) = self.dispatch_signal_task {
540 task.abort();
541 await_lifecycle_task("mailbox dispatch signal loop", task).await?;
542 }
543 self.maintenance_task.abort();
544 await_lifecycle_task("mailbox maintenance", self.maintenance_task).await
545 }
546}
547
548async fn await_lifecycle_task(name: &str, task: JoinHandle<()>) -> Result<(), MailboxError> {
549 match task.await {
550 Ok(()) => Ok(()),
551 Err(error) if error.is_cancelled() => Ok(()),
552 Err(error) if error.is_panic() => Err(MailboxError::Internal(format!("{name} panicked"))),
553 Err(error) => Err(MailboxError::Internal(format!("{name} failed: {error}"))),
554 }
555}
556
557enum MailboxWorkerStatus {
561 Idle,
562 Claiming,
565 Running {
566 dispatch_id: String,
567 run_id: String,
568 lease_handle: JoinHandle<()>,
569 sink: Arc<ReconnectableEventSink>,
570 },
571}
572
573#[derive(Debug, Clone, Copy, PartialEq, Eq)]
574enum DispatchAttempt {
575 Claimed,
576 Busy,
577 NoEligible,
578 TransientError,
579}
580
581impl DispatchAttempt {
582 fn started_execution(self) -> bool {
583 matches!(self, DispatchAttempt::Claimed)
584 }
585}
586
587struct ThreadContext {
589 messages: Vec<Message>,
590 latest_run: Option<RunRecord>,
591 run_cache: HashMap<String, RunRecord>,
592}
593
594impl ThreadContext {
595 async fn load(run_store: &dyn ThreadRunStore, thread_id: &str) -> Result<Self, MailboxError> {
596 let messages = run_store
597 .load_messages(thread_id)
598 .await?
599 .unwrap_or_default();
600 let latest_run = run_store.latest_run(thread_id).await?;
601 let mut run_cache = HashMap::new();
602 if let Some(ref run) = latest_run {
603 run_cache.insert(run.run_id.clone(), run.clone());
604 }
605 Ok(Self {
606 messages,
607 latest_run,
608 run_cache,
609 })
610 }
611
612 fn get_run(&self, run_id: &str) -> Option<&RunRecord> {
613 self.run_cache.get(run_id)
614 }
615
616 fn apply_checkpoint(&mut self, messages: &[Message], run: &RunRecord) {
617 self.messages = messages.to_vec();
618 self.latest_run = Some(run.clone());
619 self.run_cache.insert(run.run_id.clone(), run.clone());
620 }
621}
622
623struct MailboxWorker {
625 status: MailboxWorkerStatus,
626 thread_ctx: Option<ThreadContext>,
627}
628
629impl Default for MailboxWorker {
630 fn default() -> Self {
631 Self {
632 status: MailboxWorkerStatus::Idle,
633 thread_ctx: None,
634 }
635 }
636}
637
638struct SuspensionAwareSink {
644 inner: Arc<dyn EventSink>,
645 suspended: Arc<AtomicBool>,
646}
647
648#[async_trait]
649impl EventSink for SuspensionAwareSink {
650 async fn emit(&self, event: AgentEvent) {
651 if matches!(
652 &event,
653 AgentEvent::ToolCallDone {
654 outcome: ToolCallOutcome::Suspended,
655 ..
656 }
657 ) {
658 self.suspended.store(true, Ordering::Release);
659 }
660 if matches!(&event, AgentEvent::ToolCallResumed { .. }) {
662 self.suspended.store(false, Ordering::Release);
663 }
664 self.inner.emit(event).await;
665 }
666
667 async fn close(&self) {
668 self.inner.close().await;
669 }
670}
671
672struct ActiveRunGuard;
674
675impl Drop for ActiveRunGuard {
676 fn drop(&mut self) {
677 crate::metrics::dec_active_runs();
678 }
679}
680
681pub struct Mailbox {
690 executor: Arc<dyn RunDispatchExecutor>,
691 store: Arc<dyn MailboxStore>,
692 coordinator: Arc<dyn CommitCoordinator>,
694 staged_coordinator: Option<Arc<dyn StagedCommitCoordinator>>,
698 run_store: Arc<dyn ThreadRunStore>,
701 pending_thread_run_store: Option<Arc<dyn awaken_stores::PendingThreadRunStore>>,
702 consumer_id: String,
703 workers: RwLock<HashMap<String, Arc<SyncMutex<MailboxWorker>>>>,
704 config: MailboxConfig,
705 runtime_event_capture: Option<RuntimeEventCaptureConfig>,
706 server_event_publisher: Option<Arc<dyn OutboxServerEventPublisher>>,
707 server_event_origin: String,
708 lifecycle_tasks: Arc<StdMutex<Option<MailboxLifecycleTasks>>>,
709 lifecycle_start_lock: Arc<Mutex<()>>,
710 thread_append_locks: Box<[Mutex<()>]>,
713 checkpoint_repair_queue: Arc<StdMutex<VecDeque<checkpoint_repair::CheckpointRepairTask>>>,
715 pinned_registry: Option<PinnedRegistrySource>,
723}
724
725#[derive(Clone)]
727struct PinnedRegistrySource {
728 store: Arc<dyn awaken_server_contract::VersionedRegistryStore>,
729 scope: awaken_server_contract::ScopeId,
730}
731
732impl Mailbox {
733 pub fn new(
735 executor: impl IntoDispatchExecutor,
736 store: Arc<dyn MailboxStore>,
737 run_store: Arc<dyn ThreadRunStore>,
738 consumer_id: String,
739 config: MailboxConfig,
740 ) -> Self {
741 Self::try_new(executor, store, run_store, consumer_id, config)
742 .expect("Mailbox requires a CommitCoordinator outside unit-test fallback")
743 }
744
745 pub fn try_new(
753 executor: impl IntoDispatchExecutor,
754 store: Arc<dyn MailboxStore>,
755 run_store: Arc<dyn ThreadRunStore>,
756 consumer_id: String,
757 config: MailboxConfig,
758 ) -> Result<Self, MailboxError> {
759 let executor = executor.into_dispatch_executor();
760 let staged_coordinator = executor.staged_commit_coordinator();
761 let coordinator = if let Some(coordinator) = executor.commit_coordinator() {
762 coordinator
763 } else if cfg!(test) {
764 tracing::warn!(
765 "using unit-test MailboxRunStoreCoordinator fallback; non-test executors must \
766 provide a CommitCoordinator"
767 );
768 Arc::new(MailboxRunStoreCoordinator::new(Arc::clone(&run_store)))
769 as Arc<dyn CommitCoordinator>
770 } else {
771 return Err(MailboxError::Internal(
772 "Mailbox requires a CommitCoordinator; wire a durable \
773 Memory/File/Pg coordinator through the runtime"
774 .to_string(),
775 ));
776 };
777 if let (Some(coordinator_identity), Some(run_store_identity)) = (
778 coordinator.thread_run_storage_identity(),
779 run_store.thread_run_storage_identity(),
780 ) && coordinator_identity != run_store_identity
781 {
782 return Err(MailboxError::Validation(format!(
783 "mailbox run_store must match executor CommitCoordinator thread/run store \
784 (coordinator={coordinator_identity}, run_store={run_store_identity})"
785 )));
786 }
787 Ok(Self {
788 executor,
789 store,
790 coordinator,
791 staged_coordinator,
792 run_store,
793 pending_thread_run_store: None,
794 consumer_id,
795 workers: RwLock::new(HashMap::new()),
796 config,
797 runtime_event_capture: None,
798 server_event_publisher: None,
799 server_event_origin: "mailbox".to_string(),
800 lifecycle_tasks: Arc::new(StdMutex::new(None)),
801 lifecycle_start_lock: Arc::new(Mutex::new(())),
802 thread_append_locks: (0..Self::THREAD_APPEND_STRIPES)
803 .map(|_| Mutex::new(()))
804 .collect(),
805 checkpoint_repair_queue: Arc::new(StdMutex::new(VecDeque::new())),
806 pinned_registry: None,
807 })
808 }
809
810 #[must_use]
816 pub fn with_pinned_registry(
817 self,
818 store: Arc<dyn awaken_server_contract::VersionedRegistryStore>,
819 scope: impl Into<String>,
820 ) -> Self {
821 self.try_with_pinned_registry(store, scope)
822 .expect("pinned registry scope_id must be valid")
823 }
824
825 pub fn try_with_pinned_registry(
826 mut self,
827 store: Arc<dyn awaken_server_contract::VersionedRegistryStore>,
828 scope: impl Into<String>,
829 ) -> Result<Self, awaken_server_contract::ScopeError> {
830 let scope = awaken_server_contract::ScopeId::new(scope.into())?;
831 self.pinned_registry = Some(PinnedRegistrySource { store, scope });
832 Ok(self)
833 }
834
835 async fn materialize_pinned_registry_set(
839 &self,
840 resolution_id: &str,
841 ) -> Result<Option<awaken_runtime::registry::RegistrySet>, MailboxError> {
842 let Some(source) = self.pinned_registry.as_ref() else {
843 return Ok(None);
844 };
845 let snapshot_version = resolution_id.parse::<u64>().map_err(|error| {
846 MailboxError::Internal(format!(
847 "invalid pinned registry resolution id '{resolution_id}': {error}"
848 ))
849 })?;
850 let live = self.executor.live_registry_set().ok_or_else(|| {
851 MailboxError::Internal(
852 "pinned registry materialization requires a live registry snapshot".to_string(),
853 )
854 })?;
855 let materializer = crate::services::frozen_registry::FrozenAgentRegistryMaterializer::new(
856 source.store.clone(),
857 );
858 let frozen = materializer
859 .materialize(awaken_server_contract::VersionSelector::Publication {
860 scope_id: source.scope.as_str().to_string(),
861 snapshot_version,
862 })
863 .await
864 .map_err(|error| {
865 MailboxError::Internal(format!(
866 "failed to materialize pinned registry publication {snapshot_version}: {error}"
867 ))
868 })?;
869 Ok(Some(frozen.to_registry_set(&live)))
870 }
871
872 const THREAD_APPEND_STRIPES: usize = 256;
874
875 const EVENT_CHANNEL_CAPACITY: usize = 256;
877
878 pub fn thread_run_store(&self) -> &Arc<dyn ThreadRunStore> {
883 &self.run_store
884 }
885
886 pub fn coordinator(&self) -> &Arc<dyn CommitCoordinator> {
888 &self.coordinator
889 }
890
891 pub fn send_decision(&self, id: &str, tool_call_id: String, resume: ToolCallResume) -> bool {
896 self.executor.send_decision(id, tool_call_id, resume)
897 }
898}
899
900mod cancel;
901mod checkpoint;
902mod checkpoint_repair;
903mod coordinator_facade;
904mod decision;
905mod dispatch_execution;
906mod helpers;
907mod lifecycle;
908mod live_delivery;
909mod pending_delivery;
910mod runtime_event_capture;
911mod server_event_capture;
912mod signal_loop;
913mod staging_coordinator;
914mod submit;
915
916use self::coordinator_facade::MailboxRunStoreCoordinator;
917use self::{helpers::*, runtime_event_capture::RuntimeEventCaptureConfig};
918pub use crate::run_dispatch::RunDispatchExecutor;
919pub use coordinator_facade::IntoDispatchExecutor;
920#[cfg(test)]
921mod tests;