1use std::io;
2use std::sync::Arc;
3
4use crate::session::task::TaskList;
5use crate::session::types::Session;
6use crate::session::PermissionAuditSeed;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum RetrievalWindowCheckpointOutcome {
15 Committed,
16 Rebased,
17}
18
19#[derive(Debug, Clone)]
23pub struct InheritedChildWait {
24 session_id: String,
25 created_at: chrono::DateTime<chrono::Utc>,
26 wait: crate::WaitingForChildrenState,
27}
28
29impl InheritedChildWait {
30 pub fn capture(session: &Session) -> Option<Self> {
31 let wait = session
32 .agent_runtime_state
33 .as_ref()?
34 .waiting_for_children
35 .as_ref()?;
36 if wait.registered_by_tool_call_id.is_some() {
37 return None;
38 }
39 Some(Self {
40 session_id: session.id.clone(),
41 created_at: session.created_at,
42 wait: wait.clone(),
43 })
44 }
45
46 pub fn session_id(&self) -> &str {
47 &self.session_id
48 }
49
50 pub fn wait(&self) -> &crate::WaitingForChildrenState {
51 &self.wait
52 }
53
54 pub fn validate_session(&self, session: &Session) -> io::Result<()> {
55 if session.id != self.session_id || session.created_at != self.created_at {
56 return Err(io::Error::other(crate::SessionAuthorityConflict(
57 "inherited child wait session changed identity or birth".into(),
58 )));
59 }
60 Ok(())
61 }
62}
63
64#[derive(Debug, Default)]
67pub struct RootInboxAdmission {
68 pub merged: usize,
69 pub committed_messages: Vec<crate::Message>,
70 pub admission_error: Option<String>,
71}
72
73pub fn append_missing_runtime_messages(session: &mut Session, durable: &Session) -> usize {
83 let mut seen = durable
84 .messages
85 .iter()
86 .map(|message| message.id.clone())
87 .collect::<std::collections::HashSet<_>>();
88 let missing = session
89 .messages
90 .iter()
91 .filter(|message| seen.insert(message.id.clone()))
92 .cloned()
93 .collect::<Vec<_>>();
94 let appended = missing.len();
95 session.messages = durable.messages.iter().cloned().chain(missing).collect();
96 session.merge_provider_transcript_from_durable(durable);
100 appended
101}
102
103pub fn merge_session_inbox_admission(session: &mut Session, durable: &Session) {
108 let Some(durable_state) = durable.session_inbox_admission().cloned() else {
109 return;
110 };
111 session
112 .session_inbox_admission_mut()
113 .merge_from(&durable_state);
114}
115
116pub fn restore_missing_admitted_inbox_messages(session: &mut Session, durable: &Session) -> usize {
128 let admission = durable.session_inbox_admission();
129 let mut restored = 0;
130 for (durable_index, message) in durable.messages.iter().enumerate() {
131 let typed_marker = message
132 .metadata
133 .as_ref()
134 .and_then(|metadata| metadata.get("session_message"))
135 .is_some_and(|marker| {
136 marker.get("id").and_then(serde_json::Value::as_str) == Some(message.id.as_str())
137 && marker
138 .get("target_session_id")
139 .and_then(serde_json::Value::as_str)
140 == Some(durable.id.as_str())
141 && crate::SessionMessageId::parse(message.id.clone()).is_ok()
142 });
143 let recent_cursor = admission.is_some_and(|state| state.contains_str(&message.id));
144 if !(typed_marker || recent_cursor)
145 || session
146 .messages
147 .iter()
148 .any(|current| current.id == message.id)
149 {
150 continue;
151 }
152
153 let insertion = durable.messages[..durable_index]
154 .iter()
155 .rev()
156 .find_map(|predecessor| {
157 session
158 .messages
159 .iter()
160 .position(|current| current.id == predecessor.id)
161 .map(|index| index + 1)
162 })
163 .or_else(|| {
164 durable.messages[durable_index + 1..]
165 .iter()
166 .find_map(|successor| {
167 session
168 .messages
169 .iter()
170 .position(|current| current.id == successor.id)
171 })
172 })
173 .unwrap_or(session.messages.len());
174 session.messages.insert(insertion, message.clone());
175 restored += 1;
176 }
177 restored
178}
179
180#[async_trait::async_trait]
187pub trait RuntimeSessionPersistence: Send + Sync {
188 fn inherited_child_wait(&self) -> Option<InheritedChildWait> {
190 None
191 }
192
193 fn bind_inherited_child_wait(
196 &self,
197 _inherited: InheritedChildWait,
198 ) -> io::Result<Arc<dyn RuntimeSessionPersistence>> {
199 Err(io::Error::new(
200 io::ErrorKind::Unsupported,
201 crate::SessionAuthorityConflict(
202 "execution-scoped inherited child wait persistence is unsupported".into(),
203 ),
204 ))
205 }
206
207 fn root_actor_writer(&self) -> Option<crate::RootActorRuntimeWrite> {
210 None
211 }
212
213 fn bind_root_inbox_receipt(
217 &self,
218 _receipt: crate::SessionInboxReceipt,
219 ) -> io::Result<Arc<dyn RuntimeSessionPersistence>> {
220 Err(io::Error::new(
221 io::ErrorKind::Unsupported,
222 "Root receipt binding unsupported",
223 ))
224 }
225
226 fn root_inbox_receipt(&self) -> Option<&crate::SessionInboxReceipt> {
227 None
228 }
229
230 fn root_actor_execution_required(&self, _session: &Session) -> bool {
234 false
235 }
236
237 async fn bind_root_actor_execution(
241 &self,
242 _session: &Session,
243 _run_id: &str,
244 ) -> io::Result<Option<RootActorExecutionBinding>> {
245 Ok(None)
246 }
247
248 async fn save_runtime_session(&self, session: &mut Session) -> io::Result<()>;
250
251 async fn save_finalized_runtime_session(&self, session: &mut Session) -> io::Result<()> {
257 self.save_runtime_session(session).await
258 }
259
260 async fn save_finalized_runtime_with_inherited_child_wait(
264 &self,
265 session: &mut Session,
266 inherited: &crate::session::runtime_state::WaitingForChildrenState,
267 ) -> io::Result<()> {
268 let _ = (session, inherited);
269 Err(io::Error::new(
270 io::ErrorKind::Unsupported,
271 crate::SessionAuthorityConflict(
272 "atomic inherited child wait finalization is unsupported".into(),
273 ),
274 ))
275 }
276
277 async fn seed_runtime_activation(&self, _session: &mut Session) -> io::Result<()> {
290 Err(io::Error::new(
291 io::ErrorKind::Unsupported,
292 "runtime persistence does not support authoritative activation seeding",
293 ))
294 }
295
296 async fn record_permission_posture_activation(
305 &self,
306 _session_id: &str,
307 _expected_audit_revision: Option<u64>,
308 _seed: &PermissionAuditSeed,
309 ) -> io::Result<Option<Session>> {
310 Err(io::Error::new(
311 io::ErrorKind::Unsupported,
312 "runtime persistence does not support atomic permission posture activation",
313 ))
314 }
315
316 async fn save_runtime_control_plane(&self, session: &mut Session) -> io::Result<()> {
329 self.save_runtime_session(session).await
330 }
331
332 async fn load_runtime_control_plane(&self, session_id: &str) -> io::Result<Option<Session>> {
341 self.load_runtime_session(session_id).await
342 }
343
344 async fn update_task_list_control_plane(
356 &self,
357 session_id: &str,
358 task_list: &TaskList,
359 version: &str,
360 ) -> io::Result<bool> {
361 let Some(mut session) = self.load_runtime_session(session_id).await? else {
362 return Ok(false);
363 };
364 session.set_task_list(task_list.clone());
365 session.set_task_list_version_meta(version.to_string());
366 self.save_runtime_control_plane(&mut session).await?;
367 Ok(true)
368 }
369
370 async fn update_task_list_control_plane_if_version(
378 &self,
379 session_id: &str,
380 expected_version: &str,
381 expected_task_list: &TaskList,
382 task_list: &TaskList,
383 version: &str,
384 ) -> io::Result<bool> {
385 let _ = (
386 session_id,
387 expected_version,
388 expected_task_list,
389 task_list,
390 version,
391 );
392 Ok(false)
393 }
394
395 async fn update_task_list_control_planes_if_version(
404 &self,
405 session_id: &str,
406 shared_session_id: &str,
407 expected_version: &str,
408 expected_task_list: &TaskList,
409 task_list: &TaskList,
410 version: &str,
411 ) -> io::Result<bool> {
412 if session_id == shared_session_id {
413 return self
414 .update_task_list_control_plane_if_version(
415 session_id,
416 expected_version,
417 expected_task_list,
418 task_list,
419 version,
420 )
421 .await;
422 }
423 let _ = (
424 session_id,
425 shared_session_id,
426 expected_version,
427 expected_task_list,
428 task_list,
429 version,
430 );
431 Ok(false)
432 }
433
434 async fn checkpoint_runtime_session(&self, session: &mut Session) -> io::Result<()> {
444 if let Some(durable) = self.load_runtime_session(&session.id).await? {
445 session
446 .check_conversation_generation(&durable)
447 .map_err(|error| {
448 io::Error::new(
449 io::ErrorKind::WouldBlock,
450 crate::SessionAuthorityConflict(error.to_string()),
451 )
452 })?;
453 append_missing_runtime_messages(session, &durable);
454 merge_session_inbox_admission(session, &durable);
455 }
456 self.save_runtime_session(session).await
457 }
458
459 async fn claim_scoped_loop_controls(
462 &self,
463 _session: &Session,
464 _inbox: Arc<dyn crate::SessionInboxPort>,
465 _active_run_id: &str,
466 _limit: usize,
467 ) -> io::Result<Vec<crate::ScopedLoopControlClaim>> {
468 Err(io::Error::new(
469 io::ErrorKind::Unsupported,
470 "scoped Loop claims unsupported",
471 ))
472 }
473
474 async fn checkpoint_scoped_loop_control(
477 &self,
478 _session: &mut Session,
479 _bindings: &[crate::ScopedLoopControlBinding],
480 _activation: Arc<dyn crate::SessionActivationPort>,
481 _input: Option<(
482 Arc<dyn crate::SessionInboxPort>,
483 crate::ScopedLoopControlClaim,
484 )>,
485 ) -> io::Result<()> {
486 Err(io::Error::new(
487 io::ErrorKind::Unsupported,
488 "scoped Loop checkpoint unsupported",
489 ))
490 }
491
492 async fn load_loop_control_checkpoint(&self, _session_id: &str) -> io::Result<Option<Session>> {
495 Err(io::Error::new(
496 io::ErrorKind::Unsupported,
497 "strict Loop checkpoint reload unsupported",
498 ))
499 }
500
501 async fn admit_root_inbox(
502 &self,
503 _session: &mut Session,
504 _inbox: Arc<dyn crate::SessionInboxPort>,
505 _active_run_id: Option<&str>,
506 ) -> io::Result<Option<RootInboxAdmission>> {
507 Ok(None)
508 }
509
510 async fn checkpoint_retrieval_window(
527 &self,
528 _expected_base: &Session,
529 _staged: &mut Session,
530 ) -> io::Result<RetrievalWindowCheckpointOutcome> {
531 Err(io::Error::new(
532 io::ErrorKind::Unsupported,
533 "runtime persistence does not support retrieval-window checkpoints",
534 ))
535 }
536
537 async fn checkpoint_prompt_rewrite(
546 &self,
547 _expected_base: &Session,
548 _staged: &mut Session,
549 ) -> io::Result<RetrievalWindowCheckpointOutcome> {
550 Err(io::Error::new(
551 io::ErrorKind::Unsupported,
552 "runtime persistence does not support prompt-rewrite checkpoints",
553 ))
554 }
555
556 async fn checkpoint_manual_archive_rejection(
564 &self,
565 _expected_base: &Session,
566 _staged: &mut Session,
567 ) -> io::Result<RetrievalWindowCheckpointOutcome> {
568 Err(io::Error::new(
569 io::ErrorKind::Unsupported,
570 "runtime persistence does not support manual archive-rejection checkpoints",
571 ))
572 }
573
574 async fn checkpoint_manual_archive_consumption(
582 &self,
583 _expected_base: &Session,
584 _staged: &mut Session,
585 ) -> io::Result<RetrievalWindowCheckpointOutcome> {
586 Err(io::Error::new(
587 io::ErrorKind::Unsupported,
588 "runtime persistence does not support manual archive-consumption checkpoints",
589 ))
590 }
591
592 async fn load_runtime_session(&self, _session_id: &str) -> io::Result<Option<Session>> {
597 Ok(None)
598 }
599
600 async fn load_runtime_admission_snapshot(
606 &self,
607 session_id: &str,
608 ) -> io::Result<Option<Session>> {
609 self.load_runtime_session(session_id).await
610 }
611
612 async fn clear_legacy_pending_messages(
616 &self,
617 session_id: &str,
618 expected: &[serde_json::Value],
619 ) -> io::Result<bool> {
620 let Some(mut latest) = self.load_runtime_session(session_id).await? else {
621 return Ok(false);
622 };
623 if latest.pending_injected_messages().as_deref() != Some(expected) {
624 return Ok(false);
625 }
626 latest.clear_pending_injected_messages();
627 self.save_runtime_session(&mut latest).await?;
628 Ok(true)
629 }
630
631 async fn append_token_usage_record(&self, session_id: &str, json_line: &str) -> io::Result<()> {
637 let _ = (session_id, json_line);
638 Ok(())
639 }
640}
641
642pub struct RootActorExecutionBinding {
646 pub persistence: Arc<dyn RuntimeSessionPersistence>,
647 pub directory: Arc<dyn crate::ActorDirectoryPort>,
648 pub owner: crate::RootActorRuntimeWrite,
649 pub lease_duration: std::time::Duration,
650 abandon: Option<Box<dyn FnOnce() + Send + Sync>>,
651}
652
653impl RootActorExecutionBinding {
654 pub fn new(
655 persistence: Arc<dyn RuntimeSessionPersistence>,
656 directory: Arc<dyn crate::ActorDirectoryPort>,
657 owner: crate::RootActorRuntimeWrite,
658 lease_duration: std::time::Duration,
659 abandon: impl FnOnce() + Send + Sync + 'static,
660 ) -> Self {
661 Self {
662 persistence,
663 directory,
664 owner,
665 lease_duration,
666 abandon: Some(Box::new(abandon)),
667 }
668 }
669
670 pub fn disarm_abandonment(&mut self) {
673 self.abandon.take();
674 }
675}
676
677impl Drop for RootActorExecutionBinding {
678 fn drop(&mut self) {
679 if let Some(abandon) = self.abandon.take() {
680 abandon();
681 }
682 }
683}
684
685#[async_trait::async_trait]
686impl<T: RuntimeSessionPersistence + ?Sized> RuntimeSessionPersistence for Arc<T> {
687 fn inherited_child_wait(&self) -> Option<InheritedChildWait> {
688 (**self).inherited_child_wait()
689 }
690
691 fn bind_inherited_child_wait(
692 &self,
693 inherited: InheritedChildWait,
694 ) -> io::Result<Arc<dyn RuntimeSessionPersistence>> {
695 (**self).bind_inherited_child_wait(inherited)
696 }
697
698 fn root_actor_writer(&self) -> Option<crate::RootActorRuntimeWrite> {
699 (**self).root_actor_writer()
700 }
701
702 fn bind_root_inbox_receipt(
703 &self,
704 receipt: crate::SessionInboxReceipt,
705 ) -> io::Result<Arc<dyn RuntimeSessionPersistence>> {
706 (**self).bind_root_inbox_receipt(receipt)
707 }
708
709 fn root_inbox_receipt(&self) -> Option<&crate::SessionInboxReceipt> {
710 (**self).root_inbox_receipt()
711 }
712
713 fn root_actor_execution_required(&self, session: &Session) -> bool {
714 (**self).root_actor_execution_required(session)
715 }
716
717 async fn bind_root_actor_execution(
718 &self,
719 session: &Session,
720 run_id: &str,
721 ) -> io::Result<Option<RootActorExecutionBinding>> {
722 (**self).bind_root_actor_execution(session, run_id).await
723 }
724 async fn save_runtime_session(&self, session: &mut Session) -> io::Result<()> {
725 (**self).save_runtime_session(session).await
726 }
727
728 async fn save_finalized_runtime_session(&self, session: &mut Session) -> io::Result<()> {
729 (**self).save_finalized_runtime_session(session).await
730 }
731
732 async fn save_finalized_runtime_with_inherited_child_wait(
733 &self,
734 session: &mut Session,
735 inherited: &crate::session::runtime_state::WaitingForChildrenState,
736 ) -> io::Result<()> {
737 (**self)
738 .save_finalized_runtime_with_inherited_child_wait(session, inherited)
739 .await
740 }
741
742 async fn seed_runtime_activation(&self, session: &mut Session) -> io::Result<()> {
743 (**self).seed_runtime_activation(session).await
744 }
745
746 async fn record_permission_posture_activation(
747 &self,
748 session_id: &str,
749 expected_audit_revision: Option<u64>,
750 seed: &PermissionAuditSeed,
751 ) -> io::Result<Option<Session>> {
752 (**self)
753 .record_permission_posture_activation(session_id, expected_audit_revision, seed)
754 .await
755 }
756
757 async fn save_runtime_control_plane(&self, session: &mut Session) -> io::Result<()> {
758 (**self).save_runtime_control_plane(session).await
759 }
760
761 async fn load_runtime_control_plane(&self, session_id: &str) -> io::Result<Option<Session>> {
762 (**self).load_runtime_control_plane(session_id).await
763 }
764
765 async fn update_task_list_control_plane(
766 &self,
767 session_id: &str,
768 task_list: &TaskList,
769 version: &str,
770 ) -> io::Result<bool> {
771 (**self)
772 .update_task_list_control_plane(session_id, task_list, version)
773 .await
774 }
775
776 async fn update_task_list_control_plane_if_version(
777 &self,
778 session_id: &str,
779 expected_version: &str,
780 expected_task_list: &TaskList,
781 task_list: &TaskList,
782 version: &str,
783 ) -> io::Result<bool> {
784 (**self)
785 .update_task_list_control_plane_if_version(
786 session_id,
787 expected_version,
788 expected_task_list,
789 task_list,
790 version,
791 )
792 .await
793 }
794
795 async fn update_task_list_control_planes_if_version(
796 &self,
797 session_id: &str,
798 shared_session_id: &str,
799 expected_version: &str,
800 expected_task_list: &TaskList,
801 task_list: &TaskList,
802 version: &str,
803 ) -> io::Result<bool> {
804 (**self)
805 .update_task_list_control_planes_if_version(
806 session_id,
807 shared_session_id,
808 expected_version,
809 expected_task_list,
810 task_list,
811 version,
812 )
813 .await
814 }
815
816 async fn checkpoint_runtime_session(&self, session: &mut Session) -> io::Result<()> {
817 (**self).checkpoint_runtime_session(session).await
818 }
819
820 async fn claim_scoped_loop_controls(
821 &self,
822 session: &Session,
823 inbox: Arc<dyn crate::SessionInboxPort>,
824 active_run_id: &str,
825 limit: usize,
826 ) -> io::Result<Vec<crate::ScopedLoopControlClaim>> {
827 (**self)
828 .claim_scoped_loop_controls(session, inbox, active_run_id, limit)
829 .await
830 }
831
832 async fn checkpoint_scoped_loop_control(
833 &self,
834 session: &mut Session,
835 bindings: &[crate::ScopedLoopControlBinding],
836 activation: Arc<dyn crate::SessionActivationPort>,
837 input: Option<(
838 Arc<dyn crate::SessionInboxPort>,
839 crate::ScopedLoopControlClaim,
840 )>,
841 ) -> io::Result<()> {
842 (**self)
843 .checkpoint_scoped_loop_control(session, bindings, activation, input)
844 .await
845 }
846
847 async fn load_loop_control_checkpoint(&self, session_id: &str) -> io::Result<Option<Session>> {
848 (**self).load_loop_control_checkpoint(session_id).await
849 }
850
851 async fn admit_root_inbox(
852 &self,
853 session: &mut Session,
854 inbox: Arc<dyn crate::SessionInboxPort>,
855 active_run_id: Option<&str>,
856 ) -> io::Result<Option<RootInboxAdmission>> {
857 (**self)
858 .admit_root_inbox(session, inbox, active_run_id)
859 .await
860 }
861
862 async fn checkpoint_retrieval_window(
863 &self,
864 expected_base: &Session,
865 staged: &mut Session,
866 ) -> io::Result<RetrievalWindowCheckpointOutcome> {
867 (**self)
868 .checkpoint_retrieval_window(expected_base, staged)
869 .await
870 }
871
872 async fn checkpoint_prompt_rewrite(
873 &self,
874 expected_base: &Session,
875 staged: &mut Session,
876 ) -> io::Result<RetrievalWindowCheckpointOutcome> {
877 (**self)
878 .checkpoint_prompt_rewrite(expected_base, staged)
879 .await
880 }
881
882 async fn checkpoint_manual_archive_rejection(
883 &self,
884 expected_base: &Session,
885 staged: &mut Session,
886 ) -> io::Result<RetrievalWindowCheckpointOutcome> {
887 (**self)
888 .checkpoint_manual_archive_rejection(expected_base, staged)
889 .await
890 }
891
892 async fn checkpoint_manual_archive_consumption(
893 &self,
894 expected_base: &Session,
895 staged: &mut Session,
896 ) -> io::Result<RetrievalWindowCheckpointOutcome> {
897 (**self)
898 .checkpoint_manual_archive_consumption(expected_base, staged)
899 .await
900 }
901
902 async fn load_runtime_session(&self, session_id: &str) -> io::Result<Option<Session>> {
903 (**self).load_runtime_session(session_id).await
904 }
905
906 async fn load_runtime_admission_snapshot(
907 &self,
908 session_id: &str,
909 ) -> io::Result<Option<Session>> {
910 (**self).load_runtime_admission_snapshot(session_id).await
911 }
912
913 async fn clear_legacy_pending_messages(
914 &self,
915 session_id: &str,
916 expected: &[serde_json::Value],
917 ) -> io::Result<bool> {
918 (**self)
919 .clear_legacy_pending_messages(session_id, expected)
920 .await
921 }
922
923 async fn append_token_usage_record(&self, session_id: &str, json_line: &str) -> io::Result<()> {
924 (**self)
925 .append_token_usage_record(session_id, json_line)
926 .await
927 }
928}