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 append_missing_runtime_messages(session, &durable);
446 merge_session_inbox_admission(session, &durable);
447 }
448 self.save_runtime_session(session).await
449 }
450
451 async fn claim_scoped_loop_controls(
454 &self,
455 _session: &Session,
456 _inbox: Arc<dyn crate::SessionInboxPort>,
457 _active_run_id: &str,
458 _limit: usize,
459 ) -> io::Result<Vec<crate::ScopedLoopControlClaim>> {
460 Err(io::Error::new(
461 io::ErrorKind::Unsupported,
462 "scoped Loop claims unsupported",
463 ))
464 }
465
466 async fn checkpoint_scoped_loop_control(
469 &self,
470 _session: &mut Session,
471 _bindings: &[crate::ScopedLoopControlBinding],
472 _activation: Arc<dyn crate::SessionActivationPort>,
473 _input: Option<(
474 Arc<dyn crate::SessionInboxPort>,
475 crate::ScopedLoopControlClaim,
476 )>,
477 ) -> io::Result<()> {
478 Err(io::Error::new(
479 io::ErrorKind::Unsupported,
480 "scoped Loop checkpoint unsupported",
481 ))
482 }
483
484 async fn load_loop_control_checkpoint(&self, _session_id: &str) -> io::Result<Option<Session>> {
487 Err(io::Error::new(
488 io::ErrorKind::Unsupported,
489 "strict Loop checkpoint reload unsupported",
490 ))
491 }
492
493 async fn admit_root_inbox(
494 &self,
495 _session: &mut Session,
496 _inbox: Arc<dyn crate::SessionInboxPort>,
497 _active_run_id: Option<&str>,
498 ) -> io::Result<Option<RootInboxAdmission>> {
499 Ok(None)
500 }
501
502 async fn checkpoint_retrieval_window(
519 &self,
520 _expected_base: &Session,
521 _staged: &mut Session,
522 ) -> io::Result<RetrievalWindowCheckpointOutcome> {
523 Err(io::Error::new(
524 io::ErrorKind::Unsupported,
525 "runtime persistence does not support retrieval-window checkpoints",
526 ))
527 }
528
529 async fn checkpoint_prompt_rewrite(
538 &self,
539 _expected_base: &Session,
540 _staged: &mut Session,
541 ) -> io::Result<RetrievalWindowCheckpointOutcome> {
542 Err(io::Error::new(
543 io::ErrorKind::Unsupported,
544 "runtime persistence does not support prompt-rewrite checkpoints",
545 ))
546 }
547
548 async fn checkpoint_manual_archive_rejection(
556 &self,
557 _expected_base: &Session,
558 _staged: &mut Session,
559 ) -> io::Result<RetrievalWindowCheckpointOutcome> {
560 Err(io::Error::new(
561 io::ErrorKind::Unsupported,
562 "runtime persistence does not support manual archive-rejection checkpoints",
563 ))
564 }
565
566 async fn checkpoint_manual_archive_consumption(
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 manual archive-consumption checkpoints",
581 ))
582 }
583
584 async fn load_runtime_session(&self, _session_id: &str) -> io::Result<Option<Session>> {
589 Ok(None)
590 }
591
592 async fn load_runtime_admission_snapshot(
598 &self,
599 session_id: &str,
600 ) -> io::Result<Option<Session>> {
601 self.load_runtime_session(session_id).await
602 }
603
604 async fn clear_legacy_pending_messages(
608 &self,
609 session_id: &str,
610 expected: &[serde_json::Value],
611 ) -> io::Result<bool> {
612 let Some(mut latest) = self.load_runtime_session(session_id).await? else {
613 return Ok(false);
614 };
615 if latest.pending_injected_messages().as_deref() != Some(expected) {
616 return Ok(false);
617 }
618 latest.clear_pending_injected_messages();
619 self.save_runtime_session(&mut latest).await?;
620 Ok(true)
621 }
622
623 async fn append_token_usage_record(&self, session_id: &str, json_line: &str) -> io::Result<()> {
629 let _ = (session_id, json_line);
630 Ok(())
631 }
632}
633
634pub struct RootActorExecutionBinding {
638 pub persistence: Arc<dyn RuntimeSessionPersistence>,
639 pub directory: Arc<dyn crate::ActorDirectoryPort>,
640 pub owner: crate::RootActorRuntimeWrite,
641 pub lease_duration: std::time::Duration,
642 abandon: Option<Box<dyn FnOnce() + Send + Sync>>,
643}
644
645impl RootActorExecutionBinding {
646 pub fn new(
647 persistence: Arc<dyn RuntimeSessionPersistence>,
648 directory: Arc<dyn crate::ActorDirectoryPort>,
649 owner: crate::RootActorRuntimeWrite,
650 lease_duration: std::time::Duration,
651 abandon: impl FnOnce() + Send + Sync + 'static,
652 ) -> Self {
653 Self {
654 persistence,
655 directory,
656 owner,
657 lease_duration,
658 abandon: Some(Box::new(abandon)),
659 }
660 }
661
662 pub fn disarm_abandonment(&mut self) {
665 self.abandon.take();
666 }
667}
668
669impl Drop for RootActorExecutionBinding {
670 fn drop(&mut self) {
671 if let Some(abandon) = self.abandon.take() {
672 abandon();
673 }
674 }
675}
676
677#[async_trait::async_trait]
678impl<T: RuntimeSessionPersistence + ?Sized> RuntimeSessionPersistence for Arc<T> {
679 fn inherited_child_wait(&self) -> Option<InheritedChildWait> {
680 (**self).inherited_child_wait()
681 }
682
683 fn bind_inherited_child_wait(
684 &self,
685 inherited: InheritedChildWait,
686 ) -> io::Result<Arc<dyn RuntimeSessionPersistence>> {
687 (**self).bind_inherited_child_wait(inherited)
688 }
689
690 fn root_actor_writer(&self) -> Option<crate::RootActorRuntimeWrite> {
691 (**self).root_actor_writer()
692 }
693
694 fn bind_root_inbox_receipt(
695 &self,
696 receipt: crate::SessionInboxReceipt,
697 ) -> io::Result<Arc<dyn RuntimeSessionPersistence>> {
698 (**self).bind_root_inbox_receipt(receipt)
699 }
700
701 fn root_inbox_receipt(&self) -> Option<&crate::SessionInboxReceipt> {
702 (**self).root_inbox_receipt()
703 }
704
705 fn root_actor_execution_required(&self, session: &Session) -> bool {
706 (**self).root_actor_execution_required(session)
707 }
708
709 async fn bind_root_actor_execution(
710 &self,
711 session: &Session,
712 run_id: &str,
713 ) -> io::Result<Option<RootActorExecutionBinding>> {
714 (**self).bind_root_actor_execution(session, run_id).await
715 }
716 async fn save_runtime_session(&self, session: &mut Session) -> io::Result<()> {
717 (**self).save_runtime_session(session).await
718 }
719
720 async fn save_finalized_runtime_session(&self, session: &mut Session) -> io::Result<()> {
721 (**self).save_finalized_runtime_session(session).await
722 }
723
724 async fn save_finalized_runtime_with_inherited_child_wait(
725 &self,
726 session: &mut Session,
727 inherited: &crate::session::runtime_state::WaitingForChildrenState,
728 ) -> io::Result<()> {
729 (**self)
730 .save_finalized_runtime_with_inherited_child_wait(session, inherited)
731 .await
732 }
733
734 async fn seed_runtime_activation(&self, session: &mut Session) -> io::Result<()> {
735 (**self).seed_runtime_activation(session).await
736 }
737
738 async fn record_permission_posture_activation(
739 &self,
740 session_id: &str,
741 expected_audit_revision: Option<u64>,
742 seed: &PermissionAuditSeed,
743 ) -> io::Result<Option<Session>> {
744 (**self)
745 .record_permission_posture_activation(session_id, expected_audit_revision, seed)
746 .await
747 }
748
749 async fn save_runtime_control_plane(&self, session: &mut Session) -> io::Result<()> {
750 (**self).save_runtime_control_plane(session).await
751 }
752
753 async fn load_runtime_control_plane(&self, session_id: &str) -> io::Result<Option<Session>> {
754 (**self).load_runtime_control_plane(session_id).await
755 }
756
757 async fn update_task_list_control_plane(
758 &self,
759 session_id: &str,
760 task_list: &TaskList,
761 version: &str,
762 ) -> io::Result<bool> {
763 (**self)
764 .update_task_list_control_plane(session_id, task_list, version)
765 .await
766 }
767
768 async fn update_task_list_control_plane_if_version(
769 &self,
770 session_id: &str,
771 expected_version: &str,
772 expected_task_list: &TaskList,
773 task_list: &TaskList,
774 version: &str,
775 ) -> io::Result<bool> {
776 (**self)
777 .update_task_list_control_plane_if_version(
778 session_id,
779 expected_version,
780 expected_task_list,
781 task_list,
782 version,
783 )
784 .await
785 }
786
787 async fn update_task_list_control_planes_if_version(
788 &self,
789 session_id: &str,
790 shared_session_id: &str,
791 expected_version: &str,
792 expected_task_list: &TaskList,
793 task_list: &TaskList,
794 version: &str,
795 ) -> io::Result<bool> {
796 (**self)
797 .update_task_list_control_planes_if_version(
798 session_id,
799 shared_session_id,
800 expected_version,
801 expected_task_list,
802 task_list,
803 version,
804 )
805 .await
806 }
807
808 async fn checkpoint_runtime_session(&self, session: &mut Session) -> io::Result<()> {
809 (**self).checkpoint_runtime_session(session).await
810 }
811
812 async fn claim_scoped_loop_controls(
813 &self,
814 session: &Session,
815 inbox: Arc<dyn crate::SessionInboxPort>,
816 active_run_id: &str,
817 limit: usize,
818 ) -> io::Result<Vec<crate::ScopedLoopControlClaim>> {
819 (**self)
820 .claim_scoped_loop_controls(session, inbox, active_run_id, limit)
821 .await
822 }
823
824 async fn checkpoint_scoped_loop_control(
825 &self,
826 session: &mut Session,
827 bindings: &[crate::ScopedLoopControlBinding],
828 activation: Arc<dyn crate::SessionActivationPort>,
829 input: Option<(
830 Arc<dyn crate::SessionInboxPort>,
831 crate::ScopedLoopControlClaim,
832 )>,
833 ) -> io::Result<()> {
834 (**self)
835 .checkpoint_scoped_loop_control(session, bindings, activation, input)
836 .await
837 }
838
839 async fn load_loop_control_checkpoint(&self, session_id: &str) -> io::Result<Option<Session>> {
840 (**self).load_loop_control_checkpoint(session_id).await
841 }
842
843 async fn admit_root_inbox(
844 &self,
845 session: &mut Session,
846 inbox: Arc<dyn crate::SessionInboxPort>,
847 active_run_id: Option<&str>,
848 ) -> io::Result<Option<RootInboxAdmission>> {
849 (**self)
850 .admit_root_inbox(session, inbox, active_run_id)
851 .await
852 }
853
854 async fn checkpoint_retrieval_window(
855 &self,
856 expected_base: &Session,
857 staged: &mut Session,
858 ) -> io::Result<RetrievalWindowCheckpointOutcome> {
859 (**self)
860 .checkpoint_retrieval_window(expected_base, staged)
861 .await
862 }
863
864 async fn checkpoint_prompt_rewrite(
865 &self,
866 expected_base: &Session,
867 staged: &mut Session,
868 ) -> io::Result<RetrievalWindowCheckpointOutcome> {
869 (**self)
870 .checkpoint_prompt_rewrite(expected_base, staged)
871 .await
872 }
873
874 async fn checkpoint_manual_archive_rejection(
875 &self,
876 expected_base: &Session,
877 staged: &mut Session,
878 ) -> io::Result<RetrievalWindowCheckpointOutcome> {
879 (**self)
880 .checkpoint_manual_archive_rejection(expected_base, staged)
881 .await
882 }
883
884 async fn checkpoint_manual_archive_consumption(
885 &self,
886 expected_base: &Session,
887 staged: &mut Session,
888 ) -> io::Result<RetrievalWindowCheckpointOutcome> {
889 (**self)
890 .checkpoint_manual_archive_consumption(expected_base, staged)
891 .await
892 }
893
894 async fn load_runtime_session(&self, session_id: &str) -> io::Result<Option<Session>> {
895 (**self).load_runtime_session(session_id).await
896 }
897
898 async fn load_runtime_admission_snapshot(
899 &self,
900 session_id: &str,
901 ) -> io::Result<Option<Session>> {
902 (**self).load_runtime_admission_snapshot(session_id).await
903 }
904
905 async fn clear_legacy_pending_messages(
906 &self,
907 session_id: &str,
908 expected: &[serde_json::Value],
909 ) -> io::Result<bool> {
910 (**self)
911 .clear_legacy_pending_messages(session_id, expected)
912 .await
913 }
914
915 async fn append_token_usage_record(&self, session_id: &str, json_line: &str) -> io::Result<()> {
916 (**self)
917 .append_token_usage_record(session_id, json_line)
918 .await
919 }
920}