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 fn supports_bound_startup_session(&self) -> bool {
253 false
254 }
255
256 async fn save_bound_startup_session(
260 &self,
261 _session: &mut Session,
262 _expected: &Session,
263 ) -> io::Result<()> {
264 Err(io::Error::new(
265 io::ErrorKind::Unsupported,
266 crate::SessionAuthorityConflict(
267 "conditional bound startup persistence is unsupported".into(),
268 ),
269 ))
270 }
271
272 async fn save_finalized_runtime_session(&self, session: &mut Session) -> io::Result<()> {
278 self.save_runtime_session(session).await
279 }
280
281 async fn save_finalized_runtime_with_inherited_child_wait(
285 &self,
286 session: &mut Session,
287 inherited: &crate::session::runtime_state::WaitingForChildrenState,
288 ) -> io::Result<()> {
289 let _ = (session, inherited);
290 Err(io::Error::new(
291 io::ErrorKind::Unsupported,
292 crate::SessionAuthorityConflict(
293 "atomic inherited child wait finalization is unsupported".into(),
294 ),
295 ))
296 }
297
298 async fn seed_runtime_activation(&self, _session: &mut Session) -> io::Result<()> {
311 Err(io::Error::new(
312 io::ErrorKind::Unsupported,
313 "runtime persistence does not support authoritative activation seeding",
314 ))
315 }
316
317 async fn record_permission_posture_activation(
326 &self,
327 _session_id: &str,
328 _expected_audit_revision: Option<u64>,
329 _seed: &PermissionAuditSeed,
330 ) -> io::Result<Option<Session>> {
331 Err(io::Error::new(
332 io::ErrorKind::Unsupported,
333 "runtime persistence does not support atomic permission posture activation",
334 ))
335 }
336
337 async fn save_runtime_control_plane(&self, session: &mut Session) -> io::Result<()> {
350 self.save_runtime_session(session).await
351 }
352
353 async fn load_runtime_control_plane(&self, session_id: &str) -> io::Result<Option<Session>> {
362 self.load_runtime_session(session_id).await
363 }
364
365 async fn update_task_list_control_plane(
377 &self,
378 session_id: &str,
379 task_list: &TaskList,
380 version: &str,
381 ) -> io::Result<bool> {
382 let Some(mut session) = self.load_runtime_session(session_id).await? else {
383 return Ok(false);
384 };
385 session.set_task_list(task_list.clone());
386 session.set_task_list_version_meta(version.to_string());
387 self.save_runtime_control_plane(&mut session).await?;
388 Ok(true)
389 }
390
391 async fn update_task_list_control_plane_if_version(
399 &self,
400 session_id: &str,
401 expected_version: &str,
402 expected_task_list: &TaskList,
403 task_list: &TaskList,
404 version: &str,
405 ) -> io::Result<bool> {
406 let _ = (
407 session_id,
408 expected_version,
409 expected_task_list,
410 task_list,
411 version,
412 );
413 Ok(false)
414 }
415
416 async fn update_task_list_control_planes_if_version(
425 &self,
426 session_id: &str,
427 shared_session_id: &str,
428 expected_version: &str,
429 expected_task_list: &TaskList,
430 task_list: &TaskList,
431 version: &str,
432 ) -> io::Result<bool> {
433 if session_id == shared_session_id {
434 return self
435 .update_task_list_control_plane_if_version(
436 session_id,
437 expected_version,
438 expected_task_list,
439 task_list,
440 version,
441 )
442 .await;
443 }
444 let _ = (
445 session_id,
446 shared_session_id,
447 expected_version,
448 expected_task_list,
449 task_list,
450 version,
451 );
452 Ok(false)
453 }
454
455 async fn checkpoint_runtime_session(&self, session: &mut Session) -> io::Result<()> {
465 if let Some(durable) = self.load_runtime_session(&session.id).await? {
466 session
467 .check_conversation_generation(&durable)
468 .map_err(|error| {
469 io::Error::new(
470 io::ErrorKind::WouldBlock,
471 crate::SessionAuthorityConflict(error.to_string()),
472 )
473 })?;
474 append_missing_runtime_messages(session, &durable);
475 merge_session_inbox_admission(session, &durable);
476 }
477 self.save_runtime_session(session).await
478 }
479
480 async fn checkpoint_live_bootstrap(
483 &self,
484 _session: &Session,
485 _inbox: Arc<dyn crate::SessionInboxPort>,
486 _gate: crate::AdmissionGate,
487 ) -> io::Result<crate::LiveBootstrapProof> {
488 Err(io::Error::new(
489 io::ErrorKind::Unsupported,
490 "live bootstrap checkpoint unsupported",
491 ))
492 }
493
494 async fn confirm_live_bootstrap(
495 &self,
496 _proof: &crate::LiveBootstrapProof,
497 _inbox: Arc<dyn crate::SessionInboxPort>,
498 _publish: crate::RootActorRuntimeEventPublisher,
499 ) -> io::Result<()> {
500 Err(io::Error::new(
501 io::ErrorKind::Unsupported,
502 "live bootstrap confirmation unsupported",
503 ))
504 }
505
506 async fn claim_scoped_loop_controls(
509 &self,
510 _session: &Session,
511 _inbox: Arc<dyn crate::SessionInboxPort>,
512 _active_run_id: &str,
513 _limit: usize,
514 ) -> io::Result<Vec<crate::ScopedLoopControlClaim>> {
515 Err(io::Error::new(
516 io::ErrorKind::Unsupported,
517 "scoped Loop claims unsupported",
518 ))
519 }
520
521 async fn checkpoint_scoped_loop_control(
524 &self,
525 _session: &mut Session,
526 _bindings: &[crate::ScopedLoopControlBinding],
527 _activation: Arc<dyn crate::SessionActivationPort>,
528 _input: Option<(
529 Arc<dyn crate::SessionInboxPort>,
530 crate::ScopedLoopControlClaim,
531 )>,
532 ) -> io::Result<()> {
533 Err(io::Error::new(
534 io::ErrorKind::Unsupported,
535 "scoped Loop checkpoint unsupported",
536 ))
537 }
538
539 async fn load_loop_control_checkpoint(&self, _session_id: &str) -> io::Result<Option<Session>> {
542 Err(io::Error::new(
543 io::ErrorKind::Unsupported,
544 "strict Loop checkpoint reload unsupported",
545 ))
546 }
547
548 async fn admit_root_inbox(
549 &self,
550 _session: &mut Session,
551 _inbox: Arc<dyn crate::SessionInboxPort>,
552 _active_run_id: Option<&str>,
553 ) -> io::Result<Option<RootInboxAdmission>> {
554 Ok(None)
555 }
556
557 async fn checkpoint_retrieval_window(
574 &self,
575 _expected_base: &Session,
576 _staged: &mut Session,
577 ) -> io::Result<RetrievalWindowCheckpointOutcome> {
578 Err(io::Error::new(
579 io::ErrorKind::Unsupported,
580 "runtime persistence does not support retrieval-window checkpoints",
581 ))
582 }
583
584 async fn checkpoint_prompt_rewrite(
593 &self,
594 _expected_base: &Session,
595 _staged: &mut Session,
596 ) -> io::Result<RetrievalWindowCheckpointOutcome> {
597 Err(io::Error::new(
598 io::ErrorKind::Unsupported,
599 "runtime persistence does not support prompt-rewrite checkpoints",
600 ))
601 }
602
603 async fn checkpoint_manual_archive_rejection(
611 &self,
612 _expected_base: &Session,
613 _staged: &mut Session,
614 ) -> io::Result<RetrievalWindowCheckpointOutcome> {
615 Err(io::Error::new(
616 io::ErrorKind::Unsupported,
617 "runtime persistence does not support manual archive-rejection checkpoints",
618 ))
619 }
620
621 async fn checkpoint_manual_archive_consumption(
629 &self,
630 _expected_base: &Session,
631 _staged: &mut Session,
632 ) -> io::Result<RetrievalWindowCheckpointOutcome> {
633 Err(io::Error::new(
634 io::ErrorKind::Unsupported,
635 "runtime persistence does not support manual archive-consumption checkpoints",
636 ))
637 }
638
639 async fn load_runtime_session(&self, _session_id: &str) -> io::Result<Option<Session>> {
644 Ok(None)
645 }
646
647 async fn load_runtime_admission_snapshot(
653 &self,
654 session_id: &str,
655 ) -> io::Result<Option<Session>> {
656 self.load_runtime_session(session_id).await
657 }
658
659 async fn clear_legacy_pending_messages(
663 &self,
664 session_id: &str,
665 expected: &[serde_json::Value],
666 ) -> io::Result<bool> {
667 let Some(mut latest) = self.load_runtime_session(session_id).await? else {
668 return Ok(false);
669 };
670 if latest.pending_injected_messages().as_deref() != Some(expected) {
671 return Ok(false);
672 }
673 latest.clear_pending_injected_messages();
674 self.save_runtime_session(&mut latest).await?;
675 Ok(true)
676 }
677
678 async fn append_token_usage_record(&self, session_id: &str, json_line: &str) -> io::Result<()> {
684 let _ = (session_id, json_line);
685 Ok(())
686 }
687}
688
689pub struct RootActorExecutionBinding {
693 pub persistence: Arc<dyn RuntimeSessionPersistence>,
694 pub directory: Arc<dyn crate::ActorDirectoryPort>,
695 pub owner: crate::RootActorRuntimeWrite,
696 pub lease_duration: std::time::Duration,
697 abandon: Option<Box<dyn FnOnce() + Send + Sync>>,
698}
699
700impl RootActorExecutionBinding {
701 pub fn new(
702 persistence: Arc<dyn RuntimeSessionPersistence>,
703 directory: Arc<dyn crate::ActorDirectoryPort>,
704 owner: crate::RootActorRuntimeWrite,
705 lease_duration: std::time::Duration,
706 abandon: impl FnOnce() + Send + Sync + 'static,
707 ) -> Self {
708 Self {
709 persistence,
710 directory,
711 owner,
712 lease_duration,
713 abandon: Some(Box::new(abandon)),
714 }
715 }
716
717 pub fn disarm_abandonment(&mut self) {
720 self.abandon.take();
721 }
722}
723
724impl Drop for RootActorExecutionBinding {
725 fn drop(&mut self) {
726 if let Some(abandon) = self.abandon.take() {
727 abandon();
728 }
729 }
730}
731
732#[async_trait::async_trait]
733impl<T: RuntimeSessionPersistence + ?Sized> RuntimeSessionPersistence for Arc<T> {
734 fn inherited_child_wait(&self) -> Option<InheritedChildWait> {
735 (**self).inherited_child_wait()
736 }
737
738 fn bind_inherited_child_wait(
739 &self,
740 inherited: InheritedChildWait,
741 ) -> io::Result<Arc<dyn RuntimeSessionPersistence>> {
742 (**self).bind_inherited_child_wait(inherited)
743 }
744
745 fn root_actor_writer(&self) -> Option<crate::RootActorRuntimeWrite> {
746 (**self).root_actor_writer()
747 }
748
749 fn bind_root_inbox_receipt(
750 &self,
751 receipt: crate::SessionInboxReceipt,
752 ) -> io::Result<Arc<dyn RuntimeSessionPersistence>> {
753 (**self).bind_root_inbox_receipt(receipt)
754 }
755
756 fn root_inbox_receipt(&self) -> Option<&crate::SessionInboxReceipt> {
757 (**self).root_inbox_receipt()
758 }
759
760 fn root_actor_execution_required(&self, session: &Session) -> bool {
761 (**self).root_actor_execution_required(session)
762 }
763
764 async fn bind_root_actor_execution(
765 &self,
766 session: &Session,
767 run_id: &str,
768 ) -> io::Result<Option<RootActorExecutionBinding>> {
769 (**self).bind_root_actor_execution(session, run_id).await
770 }
771 async fn save_runtime_session(&self, session: &mut Session) -> io::Result<()> {
772 (**self).save_runtime_session(session).await
773 }
774
775 fn supports_bound_startup_session(&self) -> bool {
776 (**self).supports_bound_startup_session()
777 }
778
779 async fn save_bound_startup_session(
780 &self,
781 session: &mut Session,
782 expected: &Session,
783 ) -> io::Result<()> {
784 (**self).save_bound_startup_session(session, expected).await
785 }
786
787 async fn save_finalized_runtime_session(&self, session: &mut Session) -> io::Result<()> {
788 (**self).save_finalized_runtime_session(session).await
789 }
790
791 async fn save_finalized_runtime_with_inherited_child_wait(
792 &self,
793 session: &mut Session,
794 inherited: &crate::session::runtime_state::WaitingForChildrenState,
795 ) -> io::Result<()> {
796 (**self)
797 .save_finalized_runtime_with_inherited_child_wait(session, inherited)
798 .await
799 }
800
801 async fn seed_runtime_activation(&self, session: &mut Session) -> io::Result<()> {
802 (**self).seed_runtime_activation(session).await
803 }
804
805 async fn record_permission_posture_activation(
806 &self,
807 session_id: &str,
808 expected_audit_revision: Option<u64>,
809 seed: &PermissionAuditSeed,
810 ) -> io::Result<Option<Session>> {
811 (**self)
812 .record_permission_posture_activation(session_id, expected_audit_revision, seed)
813 .await
814 }
815
816 async fn save_runtime_control_plane(&self, session: &mut Session) -> io::Result<()> {
817 (**self).save_runtime_control_plane(session).await
818 }
819
820 async fn load_runtime_control_plane(&self, session_id: &str) -> io::Result<Option<Session>> {
821 (**self).load_runtime_control_plane(session_id).await
822 }
823
824 async fn update_task_list_control_plane(
825 &self,
826 session_id: &str,
827 task_list: &TaskList,
828 version: &str,
829 ) -> io::Result<bool> {
830 (**self)
831 .update_task_list_control_plane(session_id, task_list, version)
832 .await
833 }
834
835 async fn update_task_list_control_plane_if_version(
836 &self,
837 session_id: &str,
838 expected_version: &str,
839 expected_task_list: &TaskList,
840 task_list: &TaskList,
841 version: &str,
842 ) -> io::Result<bool> {
843 (**self)
844 .update_task_list_control_plane_if_version(
845 session_id,
846 expected_version,
847 expected_task_list,
848 task_list,
849 version,
850 )
851 .await
852 }
853
854 async fn update_task_list_control_planes_if_version(
855 &self,
856 session_id: &str,
857 shared_session_id: &str,
858 expected_version: &str,
859 expected_task_list: &TaskList,
860 task_list: &TaskList,
861 version: &str,
862 ) -> io::Result<bool> {
863 (**self)
864 .update_task_list_control_planes_if_version(
865 session_id,
866 shared_session_id,
867 expected_version,
868 expected_task_list,
869 task_list,
870 version,
871 )
872 .await
873 }
874
875 async fn checkpoint_runtime_session(&self, session: &mut Session) -> io::Result<()> {
876 (**self).checkpoint_runtime_session(session).await
877 }
878
879 async fn checkpoint_live_bootstrap(
880 &self,
881 session: &Session,
882 inbox: Arc<dyn crate::SessionInboxPort>,
883 gate: crate::AdmissionGate,
884 ) -> io::Result<crate::LiveBootstrapProof> {
885 (**self)
886 .checkpoint_live_bootstrap(session, inbox, gate)
887 .await
888 }
889
890 async fn confirm_live_bootstrap(
891 &self,
892 proof: &crate::LiveBootstrapProof,
893 inbox: Arc<dyn crate::SessionInboxPort>,
894 publish: crate::RootActorRuntimeEventPublisher,
895 ) -> io::Result<()> {
896 (**self).confirm_live_bootstrap(proof, inbox, publish).await
897 }
898
899 async fn claim_scoped_loop_controls(
900 &self,
901 session: &Session,
902 inbox: Arc<dyn crate::SessionInboxPort>,
903 active_run_id: &str,
904 limit: usize,
905 ) -> io::Result<Vec<crate::ScopedLoopControlClaim>> {
906 (**self)
907 .claim_scoped_loop_controls(session, inbox, active_run_id, limit)
908 .await
909 }
910
911 async fn checkpoint_scoped_loop_control(
912 &self,
913 session: &mut Session,
914 bindings: &[crate::ScopedLoopControlBinding],
915 activation: Arc<dyn crate::SessionActivationPort>,
916 input: Option<(
917 Arc<dyn crate::SessionInboxPort>,
918 crate::ScopedLoopControlClaim,
919 )>,
920 ) -> io::Result<()> {
921 (**self)
922 .checkpoint_scoped_loop_control(session, bindings, activation, input)
923 .await
924 }
925
926 async fn load_loop_control_checkpoint(&self, session_id: &str) -> io::Result<Option<Session>> {
927 (**self).load_loop_control_checkpoint(session_id).await
928 }
929
930 async fn admit_root_inbox(
931 &self,
932 session: &mut Session,
933 inbox: Arc<dyn crate::SessionInboxPort>,
934 active_run_id: Option<&str>,
935 ) -> io::Result<Option<RootInboxAdmission>> {
936 (**self)
937 .admit_root_inbox(session, inbox, active_run_id)
938 .await
939 }
940
941 async fn checkpoint_retrieval_window(
942 &self,
943 expected_base: &Session,
944 staged: &mut Session,
945 ) -> io::Result<RetrievalWindowCheckpointOutcome> {
946 (**self)
947 .checkpoint_retrieval_window(expected_base, staged)
948 .await
949 }
950
951 async fn checkpoint_prompt_rewrite(
952 &self,
953 expected_base: &Session,
954 staged: &mut Session,
955 ) -> io::Result<RetrievalWindowCheckpointOutcome> {
956 (**self)
957 .checkpoint_prompt_rewrite(expected_base, staged)
958 .await
959 }
960
961 async fn checkpoint_manual_archive_rejection(
962 &self,
963 expected_base: &Session,
964 staged: &mut Session,
965 ) -> io::Result<RetrievalWindowCheckpointOutcome> {
966 (**self)
967 .checkpoint_manual_archive_rejection(expected_base, staged)
968 .await
969 }
970
971 async fn checkpoint_manual_archive_consumption(
972 &self,
973 expected_base: &Session,
974 staged: &mut Session,
975 ) -> io::Result<RetrievalWindowCheckpointOutcome> {
976 (**self)
977 .checkpoint_manual_archive_consumption(expected_base, staged)
978 .await
979 }
980
981 async fn load_runtime_session(&self, session_id: &str) -> io::Result<Option<Session>> {
982 (**self).load_runtime_session(session_id).await
983 }
984
985 async fn load_runtime_admission_snapshot(
986 &self,
987 session_id: &str,
988 ) -> io::Result<Option<Session>> {
989 (**self).load_runtime_admission_snapshot(session_id).await
990 }
991
992 async fn clear_legacy_pending_messages(
993 &self,
994 session_id: &str,
995 expected: &[serde_json::Value],
996 ) -> io::Result<bool> {
997 (**self)
998 .clear_legacy_pending_messages(session_id, expected)
999 .await
1000 }
1001
1002 async fn append_token_usage_record(&self, session_id: &str, json_line: &str) -> io::Result<()> {
1003 (**self)
1004 .append_token_usage_record(session_id, json_line)
1005 .await
1006 }
1007}