Skip to main content

bamboo_domain/session/
persistence.rs

1use std::io;
2use std::sync::Arc;
3
4use crate::session::task::TaskList;
5use crate::session::types::Session;
6use crate::session::PermissionAuditSeed;
7
8/// Result of the retrieval-window-specific execute-boundary checkpoint.
9///
10/// `Rebased` means persistence observed a newer durable transcript, made no
11/// write, and replaced the caller's staged value with a clean reconciled base
12/// that must be planned and prepared again before dispatch.
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum RetrievalWindowCheckpointOutcome {
15    Committed,
16    Rebased,
17}
18
19/// Immutable execution provenance for an already-persisted untagged Child wait.
20/// This capability is captured once from the execution's initial snapshot; it
21/// is not Session metadata and must never be reconstructed at a later save.
22#[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/// Provider-boundary admission through the bound Root writer. None from the
65/// port below means this execution does not use ordinary Root authority.
66#[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
73/// Merge messages from a live runner snapshot into an already-durable
74/// transcript without ever removing or rewriting a durable message.
75///
76/// Runtime sessions are append-oriented, and every newly-created message has a
77/// stable id.  A runner may nevertheless be holding a snapshot that predates a
78/// concurrent append (for example, an injected child-completion message).  A
79/// terminal/error checkpoint must not full-save that stale snapshot: doing so
80/// would shrink the transcript.  Keep the durable ordering and append only the
81/// live messages whose ids are not durable yet.
82pub 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    // Provider-native groups are message-anchored and append-only as well. A
97    // concurrent durable prefix must not be erased by a stale runner save, and
98    // a runner's newly completed group must remain paired with its new message.
99    session.merge_provider_transcript_from_durable(durable);
100    appended
101}
102
103/// Merge the durable SessionInbox admitted-id cursor into a writer snapshot.
104///
105/// Runtime writers can hold a session clone from before another run admitted
106/// an inbox message. No later full save may erase that durable dedupe state.
107pub 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
116/// Restore durable provider messages identified by their typed
117/// `metadata.session_message` marker into a stale writer without preserving
118/// unrelated durable suffixes. The bounded cursor is only a fast recent index;
119/// the transcript marker is the unbounded source of truth after cursor
120/// eviction.
121///
122/// Insertion follows durable transcript neighbors so an admitted user/runtime
123/// message remains ahead of any later assistant output held by the stale
124/// runner. This is narrower than [`append_missing_runtime_messages`], retaining
125/// the historical shrink semantics for unrelated concurrent messages while
126/// making a cursor/tombstone incapable of outliving its transcript entry.
127pub 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/// Port for runtime (non-authoritative) session persistence.
181///
182/// Implementors must:
183/// - Serialize concurrent saves per session ID.
184/// - Merge on-disk authoritative metadata (`title`, `title_generated`, `pinned`, `title_version`,
185///   `metadata_version`) before writing, so UI edits are never clobbered.
186#[async_trait::async_trait]
187pub trait RuntimeSessionPersistence: Send + Sync {
188    /// An execution-private binding, never reconstructed from mutable state.
189    fn inherited_child_wait(&self) -> Option<InheritedChildWait> {
190        None
191    }
192
193    /// Return an execution-private view retaining the captured wait provenance
194    /// through every intermediate save. Unsupported persisters fail closed.
195    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    /// Immutable capability already captured by this concrete execution.
208    /// An unbound Host/default persister never supplies a current owner.
209    fn root_actor_writer(&self) -> Option<crate::RootActorRuntimeWrite> {
210        None
211    }
212
213    /// Whether this Host requires an explicit execution capability for the
214    /// proposed Root. A route without a bound event/persistence handoff must
215    /// reject before claiming or making a provider call.
216    fn root_actor_execution_required(&self, _session: &Session) -> bool {
217        false
218    }
219
220    /// Host execution binding, never a request DTO. Legacy embedders have no
221    /// ActorDirectory; a host that enables one must reject unsupported storage
222    /// before claiming rather than returning an unfenced writer.
223    async fn bind_root_actor_execution(
224        &self,
225        _session: &Session,
226        _run_id: &str,
227    ) -> io::Result<Option<RootActorExecutionBinding>> {
228        Ok(None)
229    }
230
231    /// Persist the session, merging any newer authoritative metadata from disk.
232    async fn save_runtime_session(&self, session: &mut Session) -> io::Result<()>;
233
234    /// Persist a runner's final snapshot. Implementations with a per-session
235    /// lock should reconcile a tool-registered child wait against the latest
236    /// durable wait inside that lock: a child may finish after the runner's
237    /// last read but before this save. The default retains compatibility for
238    /// in-memory and test persisters without that concurrent completion path.
239    async fn save_finalized_runtime_session(&self, session: &mut Session) -> io::Result<()> {
240        self.save_runtime_session(session).await
241    }
242
243    /// Finalize a wait inherited by this execution. Backends must reconcile
244    /// against durable state inside their cross-process write transaction.
245    /// Unsupported adapters fail closed rather than reopen a read/save race.
246    async fn save_finalized_runtime_with_inherited_child_wait(
247        &self,
248        session: &mut Session,
249        inherited: &crate::session::runtime_state::WaitingForChildrenState,
250    ) -> io::Result<()> {
251        let _ = (session, inherited);
252        Err(io::Error::new(
253            io::ErrorKind::Unsupported,
254            crate::SessionAuthorityConflict(
255                "atomic inherited child wait finalization is unsupported".into(),
256            ),
257        ))
258    }
259
260    /// Authoritatively seed one validated actor activation.
261    ///
262    /// Unlike an ordinary runtime save, the incoming RunSpec posture and its
263    /// complete audit record must replace any posture left by a previous warm
264    /// activation. Implementations must still preserve durable SessionInbox
265    /// admission/transcript proof and serialize the operation per session.
266    ///
267    /// There is no safe generic implementation through
268    /// [`Self::save_runtime_session`]: that primitive is explicitly allowed to
269    /// adopt a newer disk posture, which would make warm workers sticky across
270    /// runs. Custom persisters therefore fail closed until they implement this
271    /// authority boundary deliberately.
272    async fn seed_runtime_activation(&self, _session: &mut Session) -> io::Result<()> {
273        Err(io::Error::new(
274            io::ErrorKind::Unsupported,
275            "runtime persistence does not support authoritative activation seeding",
276        ))
277    }
278
279    /// Atomically persist a worker-declared executor mapping for the current
280    /// host-authoritative permission posture.
281    ///
282    /// The caller supplies the audit revision it observed before dispatch.
283    /// Implementations must load and compare that revision while holding the
284    /// per-session lock, reject a concurrent posture update, and allocate a new
285    /// host revision/timestamp themselves. Remote audit clocks are never an
286    /// authority at this boundary.
287    async fn record_permission_posture_activation(
288        &self,
289        _session_id: &str,
290        _expected_audit_revision: Option<u64>,
291        _seed: &PermissionAuditSeed,
292    ) -> io::Result<Option<Session>> {
293        Err(io::Error::new(
294            io::ErrorKind::Unsupported,
295            "runtime persistence does not support atomic permission posture activation",
296        ))
297    }
298
299    /// Persist only the runtime control-plane for a session.
300    ///
301    /// Task lists and other runtime metadata belong to the control-plane and do
302    /// not require rewriting the potentially large message transcript. Built-in
303    /// persistence implementations with a runtime sidecar should override this
304    /// operation with their sidecar-only path. Custom/legacy implementations
305    /// remain source-compatible and safely fall back to the full runtime save.
306    ///
307    /// Callers must not rely on this operation to persist message or
308    /// `model_context_state` changes. The durable ledger is checkpoint-owned;
309    /// sidecar implementations must preserve its latest committed value while
310    /// applying the caller's narrow control-plane mutation.
311    async fn save_runtime_control_plane(&self, session: &mut Session) -> io::Result<()> {
312        self.save_runtime_session(session).await
313    }
314
315    /// Load the representation paired with
316    /// [`save_runtime_control_plane`](Self::save_runtime_control_plane).
317    ///
318    /// Sidecar-capable implementations should return their message-free
319    /// control-plane snapshot. The default deliberately returns the full
320    /// runtime session: when the paired save also falls back to a full save,
321    /// retaining the transcript makes that fallback safe rather than replacing
322    /// durable messages with an empty sidecar-shaped snapshot.
323    async fn load_runtime_control_plane(&self, session_id: &str) -> io::Result<Option<Session>> {
324        self.load_runtime_session(session_id).await
325    }
326
327    /// Atomically update only the shared Task list and its version.
328    ///
329    /// The default is safe for custom/legacy persistence: it loads the full
330    /// runtime session, changes only Task-owned fields, then uses the paired
331    /// control-plane save (which itself defaults to a full save). Returning
332    /// `false` means the implementation could not load the target; callers that
333    /// also hold a [`Storage`](crate::storage::Storage) may retain legacy
334    /// behavior with an explicit full-load/full-save fallback.
335    ///
336    /// Implementations with per-session transactions should override this so
337    /// the load, narrow mutation and save share one critical section.
338    async fn update_task_list_control_plane(
339        &self,
340        session_id: &str,
341        task_list: &TaskList,
342        version: &str,
343    ) -> io::Result<bool> {
344        let Some(mut session) = self.load_runtime_session(session_id).await? else {
345            return Ok(false);
346        };
347        session.set_task_list(task_list.clone());
348        session.set_task_list_version_meta(version.to_string());
349        self.save_runtime_control_plane(&mut session).await?;
350        Ok(true)
351    }
352
353    /// Atomically update Task-owned control-plane fields only when the durable
354    /// Task generation and exact list still match the expected snapshot.
355    ///
356    /// `false` covers an unsupported atomic compare-and-patch, a missing target,
357    /// or a version conflict. Callers must treat it as a stale write and must
358    /// not publish their staged Task state. The default fails closed because a
359    /// load followed by a separately locked save is not an atomic CAS.
360    async fn update_task_list_control_plane_if_version(
361        &self,
362        session_id: &str,
363        expected_version: &str,
364        expected_task_list: &TaskList,
365        task_list: &TaskList,
366        version: &str,
367    ) -> io::Result<bool> {
368        let _ = (
369            session_id,
370            expected_version,
371            expected_task_list,
372            task_list,
373            version,
374        );
375        Ok(false)
376    }
377
378    /// Recoverably compare-and-patch the executing session and its shared root.
379    /// Implementations must validate both generations before either target is
380    /// written and may return `Ok(true)` only after both Task generations are
381    /// durable with no undo record that could later revert them. An error after
382    /// one physical write must restore both originals before returning or retain
383    /// durable recovery state and fail subsequent paired access closed until
384    /// recovery completes. Root-session callers pass the same id twice and
385    /// receive the single-target CAS semantics above.
386    async fn update_task_list_control_planes_if_version(
387        &self,
388        session_id: &str,
389        shared_session_id: &str,
390        expected_version: &str,
391        expected_task_list: &TaskList,
392        task_list: &TaskList,
393        version: &str,
394    ) -> io::Result<bool> {
395        if session_id == shared_session_id {
396            return self
397                .update_task_list_control_plane_if_version(
398                    session_id,
399                    expected_version,
400                    expected_task_list,
401                    task_list,
402                    version,
403                )
404                .await;
405        }
406        let _ = (
407            session_id,
408            shared_session_id,
409            expected_version,
410            expected_task_list,
411            task_list,
412            version,
413        );
414        Ok(false)
415    }
416
417    /// Append-safe checkpoint used at the shared engine execute boundary.
418    ///
419    /// Unlike [`save_runtime_session`](Self::save_runtime_session), this must
420    /// preserve messages that were appended durably by a concurrent writer
421    /// after the runner loaded its snapshot.  Implementations that can provide
422    /// a per-session transaction should override this method and perform the
423    /// load/merge/save under one lock.  The default still reconciles against a
424    /// latest snapshot for lightweight/custom SDK persisters; the built-in
425    /// storage implementation supplies the atomic variant.
426    async fn checkpoint_runtime_session(&self, session: &mut Session) -> io::Result<()> {
427        if let Some(durable) = self.load_runtime_session(&session.id).await? {
428            append_missing_runtime_messages(session, &durable);
429            merge_session_inbox_admission(session, &durable);
430        }
431        self.save_runtime_session(session).await
432    }
433
434    async fn admit_root_inbox(
435        &self,
436        _session: &mut Session,
437        _inbox: Arc<dyn crate::SessionInboxPort>,
438        _active_run_id: Option<&str>,
439    ) -> io::Result<Option<RootInboxAdmission>> {
440        Ok(None)
441    }
442
443    /// Atomically commit a staged retrieval-window transcript rewrite.
444    ///
445    /// `expected_base` is the exact pre-archive Session used for planning.
446    /// Implementations must compare it with the latest durable transcript while
447    /// holding their per-session serialization lock. If a concurrent append or
448    /// rewrite is present, they must perform no save, rebase `staged` onto that
449    /// durable snapshot, and return [`RetrievalWindowCheckpointOutcome::Rebased`].
450    /// Otherwise they must preserve the staged message archive flags and commit
451    /// them with the compression event and model-context reset.
452    ///
453    /// There is no safe fallback through the ordinary append-only checkpoint,
454    /// because that path deliberately restores durable message clones and would
455    /// erase the new archive flags. Custom persisters therefore fail closed
456    /// until they implement this boundary explicitly.
457    async fn checkpoint_retrieval_window(
458        &self,
459        _expected_base: &Session,
460        _staged: &mut Session,
461    ) -> io::Result<RetrievalWindowCheckpointOutcome> {
462        Err(io::Error::new(
463            io::ErrorKind::Unsupported,
464            "runtime persistence does not support retrieval-window checkpoints",
465        ))
466    }
467
468    /// Atomically commit a provider-visible System-prompt rewrite.
469    ///
470    /// This is deliberately separate from both the append-safe runtime
471    /// checkpoint (which must restore durable message content) and the
472    /// retrieval-window archive checkpoint (which requires a new archive
473    /// event). Implementations compare `expected_base` under their per-session
474    /// lock, return `Rebased` without writing on conflict, and accept only the
475    /// bounded prompt rewrite plus its provider/model-context reset.
476    async fn checkpoint_prompt_rewrite(
477        &self,
478        _expected_base: &Session,
479        _staged: &mut Session,
480    ) -> io::Result<RetrievalWindowCheckpointOutcome> {
481        Err(io::Error::new(
482            io::ErrorKind::Unsupported,
483            "runtime persistence does not support prompt-rewrite checkpoints",
484        ))
485    }
486
487    /// Atomically commit one permanently rejected `archive_context` result.
488    ///
489    /// This boundary permits only the correlated Tool result rewrite, the
490    /// bounded consumed/rejection metadata, and the provider/model-context
491    /// reset required to make that rewrite visible. Implementations compare
492    /// `expected_base` under their per-session lock and return `Rebased`
493    /// without writing when the durable transcript changed concurrently.
494    async fn checkpoint_manual_archive_rejection(
495        &self,
496        _expected_base: &Session,
497        _staged: &mut Session,
498    ) -> io::Result<RetrievalWindowCheckpointOutcome> {
499        Err(io::Error::new(
500            io::ErrorKind::Unsupported,
501            "runtime persistence does not support manual archive-rejection checkpoints",
502        ))
503    }
504
505    /// Atomically consume one successful no-op `archive_context` request.
506    ///
507    /// This boundary permits only the correlated consumed-occurrence marker.
508    /// Implementations compare `expected_base` under their per-session lock
509    /// and return `Rebased` without writing when any durable transcript,
510    /// metadata, runtime metadata, or execution-profile field changed. The
511    /// caller must then restage the marker from the returned durable snapshot.
512    async fn checkpoint_manual_archive_consumption(
513        &self,
514        _expected_base: &Session,
515        _staged: &mut Session,
516    ) -> io::Result<RetrievalWindowCheckpointOutcome> {
517        Err(io::Error::new(
518            io::ErrorKind::Unsupported,
519            "runtime persistence does not support manual archive-consumption checkpoints",
520        ))
521    }
522
523    /// Load the latest runtime-visible session snapshot when the persistence
524    /// implementation can coordinate reads. Tools may update a repository-owned
525    /// clone while an agent loop holds its own live Session; the loop uses this
526    /// hook to merge narrowly-scoped tool side effects before its next save.
527    async fn load_runtime_session(&self, _session_id: &str) -> io::Result<Option<Session>> {
528        Ok(None)
529    }
530
531    /// Clear the bounded compatibility queue iff it still equals the entries
532    /// that were durably copied into SessionInbox. Implementations with a
533    /// per-session transaction should override this method.
534    async fn clear_legacy_pending_messages(
535        &self,
536        session_id: &str,
537        expected: &[serde_json::Value],
538    ) -> io::Result<bool> {
539        let Some(mut latest) = self.load_runtime_session(session_id).await? else {
540            return Ok(false);
541        };
542        if latest.pending_injected_messages().as_deref() != Some(expected) {
543            return Ok(false);
544        }
545        latest.clear_pending_injected_messages();
546        self.save_runtime_session(&mut latest).await?;
547        Ok(true)
548    }
549
550    /// Append one JSON-line analysis record to the session's append-only
551    /// token-usage log (see [`Storage::append_token_usage_record`]). Defaults to
552    /// a no-op so non-file-backed persisters are unaffected.
553    ///
554    /// [`Storage::append_token_usage_record`]: crate::storage::Storage::append_token_usage_record
555    async fn append_token_usage_record(&self, session_id: &str, json_line: &str) -> io::Result<()> {
556        let _ = (session_id, json_line);
557        Ok(())
558    }
559}
560
561/// Ownership passed from the canonical repository into one concrete runtime.
562/// The runtime owns renewal and finish; dropping an execution must release its
563/// own fence without ever finishing a replacement owner.
564pub struct RootActorExecutionBinding {
565    pub persistence: Arc<dyn RuntimeSessionPersistence>,
566    pub directory: Arc<dyn crate::ActorDirectoryPort>,
567    pub owner: crate::RootActorRuntimeWrite,
568    pub lease_duration: std::time::Duration,
569    abandon: Option<Box<dyn FnOnce() + Send + Sync>>,
570}
571
572impl RootActorExecutionBinding {
573    pub fn new(
574        persistence: Arc<dyn RuntimeSessionPersistence>,
575        directory: Arc<dyn crate::ActorDirectoryPort>,
576        owner: crate::RootActorRuntimeWrite,
577        lease_duration: std::time::Duration,
578        abandon: impl FnOnce() + Send + Sync + 'static,
579    ) -> Self {
580        Self {
581            persistence,
582            directory,
583            owner,
584            lease_duration,
585            abandon: Some(Box::new(abandon)),
586        }
587    }
588
589    /// The runtime calls this only after handing finish to an owned detached
590    /// job, so cancellation of the waiter cannot strand this execution.
591    pub fn disarm_abandonment(&mut self) {
592        self.abandon.take();
593    }
594}
595
596impl Drop for RootActorExecutionBinding {
597    fn drop(&mut self) {
598        if let Some(abandon) = self.abandon.take() {
599            abandon();
600        }
601    }
602}
603
604#[async_trait::async_trait]
605impl<T: RuntimeSessionPersistence + ?Sized> RuntimeSessionPersistence for Arc<T> {
606    fn inherited_child_wait(&self) -> Option<InheritedChildWait> {
607        (**self).inherited_child_wait()
608    }
609
610    fn bind_inherited_child_wait(
611        &self,
612        inherited: InheritedChildWait,
613    ) -> io::Result<Arc<dyn RuntimeSessionPersistence>> {
614        (**self).bind_inherited_child_wait(inherited)
615    }
616
617    fn root_actor_writer(&self) -> Option<crate::RootActorRuntimeWrite> {
618        (**self).root_actor_writer()
619    }
620
621    fn root_actor_execution_required(&self, session: &Session) -> bool {
622        (**self).root_actor_execution_required(session)
623    }
624
625    async fn bind_root_actor_execution(
626        &self,
627        session: &Session,
628        run_id: &str,
629    ) -> io::Result<Option<RootActorExecutionBinding>> {
630        (**self).bind_root_actor_execution(session, run_id).await
631    }
632    async fn save_runtime_session(&self, session: &mut Session) -> io::Result<()> {
633        (**self).save_runtime_session(session).await
634    }
635
636    async fn save_finalized_runtime_session(&self, session: &mut Session) -> io::Result<()> {
637        (**self).save_finalized_runtime_session(session).await
638    }
639
640    async fn save_finalized_runtime_with_inherited_child_wait(
641        &self,
642        session: &mut Session,
643        inherited: &crate::session::runtime_state::WaitingForChildrenState,
644    ) -> io::Result<()> {
645        (**self)
646            .save_finalized_runtime_with_inherited_child_wait(session, inherited)
647            .await
648    }
649
650    async fn seed_runtime_activation(&self, session: &mut Session) -> io::Result<()> {
651        (**self).seed_runtime_activation(session).await
652    }
653
654    async fn record_permission_posture_activation(
655        &self,
656        session_id: &str,
657        expected_audit_revision: Option<u64>,
658        seed: &PermissionAuditSeed,
659    ) -> io::Result<Option<Session>> {
660        (**self)
661            .record_permission_posture_activation(session_id, expected_audit_revision, seed)
662            .await
663    }
664
665    async fn save_runtime_control_plane(&self, session: &mut Session) -> io::Result<()> {
666        (**self).save_runtime_control_plane(session).await
667    }
668
669    async fn load_runtime_control_plane(&self, session_id: &str) -> io::Result<Option<Session>> {
670        (**self).load_runtime_control_plane(session_id).await
671    }
672
673    async fn update_task_list_control_plane(
674        &self,
675        session_id: &str,
676        task_list: &TaskList,
677        version: &str,
678    ) -> io::Result<bool> {
679        (**self)
680            .update_task_list_control_plane(session_id, task_list, version)
681            .await
682    }
683
684    async fn update_task_list_control_plane_if_version(
685        &self,
686        session_id: &str,
687        expected_version: &str,
688        expected_task_list: &TaskList,
689        task_list: &TaskList,
690        version: &str,
691    ) -> io::Result<bool> {
692        (**self)
693            .update_task_list_control_plane_if_version(
694                session_id,
695                expected_version,
696                expected_task_list,
697                task_list,
698                version,
699            )
700            .await
701    }
702
703    async fn update_task_list_control_planes_if_version(
704        &self,
705        session_id: &str,
706        shared_session_id: &str,
707        expected_version: &str,
708        expected_task_list: &TaskList,
709        task_list: &TaskList,
710        version: &str,
711    ) -> io::Result<bool> {
712        (**self)
713            .update_task_list_control_planes_if_version(
714                session_id,
715                shared_session_id,
716                expected_version,
717                expected_task_list,
718                task_list,
719                version,
720            )
721            .await
722    }
723
724    async fn checkpoint_runtime_session(&self, session: &mut Session) -> io::Result<()> {
725        (**self).checkpoint_runtime_session(session).await
726    }
727
728    async fn admit_root_inbox(
729        &self,
730        session: &mut Session,
731        inbox: Arc<dyn crate::SessionInboxPort>,
732        active_run_id: Option<&str>,
733    ) -> io::Result<Option<RootInboxAdmission>> {
734        (**self)
735            .admit_root_inbox(session, inbox, active_run_id)
736            .await
737    }
738
739    async fn checkpoint_retrieval_window(
740        &self,
741        expected_base: &Session,
742        staged: &mut Session,
743    ) -> io::Result<RetrievalWindowCheckpointOutcome> {
744        (**self)
745            .checkpoint_retrieval_window(expected_base, staged)
746            .await
747    }
748
749    async fn checkpoint_prompt_rewrite(
750        &self,
751        expected_base: &Session,
752        staged: &mut Session,
753    ) -> io::Result<RetrievalWindowCheckpointOutcome> {
754        (**self)
755            .checkpoint_prompt_rewrite(expected_base, staged)
756            .await
757    }
758
759    async fn checkpoint_manual_archive_rejection(
760        &self,
761        expected_base: &Session,
762        staged: &mut Session,
763    ) -> io::Result<RetrievalWindowCheckpointOutcome> {
764        (**self)
765            .checkpoint_manual_archive_rejection(expected_base, staged)
766            .await
767    }
768
769    async fn checkpoint_manual_archive_consumption(
770        &self,
771        expected_base: &Session,
772        staged: &mut Session,
773    ) -> io::Result<RetrievalWindowCheckpointOutcome> {
774        (**self)
775            .checkpoint_manual_archive_consumption(expected_base, staged)
776            .await
777    }
778
779    async fn load_runtime_session(&self, session_id: &str) -> io::Result<Option<Session>> {
780        (**self).load_runtime_session(session_id).await
781    }
782
783    async fn clear_legacy_pending_messages(
784        &self,
785        session_id: &str,
786        expected: &[serde_json::Value],
787    ) -> io::Result<bool> {
788        (**self)
789            .clear_legacy_pending_messages(session_id, expected)
790            .await
791    }
792
793    async fn append_token_usage_record(&self, session_id: &str, json_line: &str) -> io::Result<()> {
794        (**self)
795            .append_token_usage_record(session_id, json_line)
796            .await
797    }
798}