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 /// Execution-private input restriction. A selector carries no authority;
214 /// only a persister already bound to a real Root writer may install it.
215 /// Unsupported hosts fail closed. This introduces no durable state.
216 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 /// Whether this Host requires an explicit execution capability for the
231 /// proposed Root. A route without a bound event/persistence handoff must
232 /// reject before claiming or making a provider call.
233 fn root_actor_execution_required(&self, _session: &Session) -> bool {
234 false
235 }
236
237 /// Host execution binding, never a request DTO. Legacy embedders have no
238 /// ActorDirectory; a host that enables one must reject unsupported storage
239 /// before claiming rather than returning an unfenced writer.
240 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 /// Persist the session, merging any newer authoritative metadata from disk.
249 async fn save_runtime_session(&self, session: &mut Session) -> io::Result<()>;
250
251 /// Persist a runner's final snapshot. Implementations with a per-session
252 /// lock should reconcile a tool-registered child wait against the latest
253 /// durable wait inside that lock: a child may finish after the runner's
254 /// last read but before this save. The default retains compatibility for
255 /// in-memory and test persisters without that concurrent completion path.
256 async fn save_finalized_runtime_session(&self, session: &mut Session) -> io::Result<()> {
257 self.save_runtime_session(session).await
258 }
259
260 /// Finalize a wait inherited by this execution. Backends must reconcile
261 /// against durable state inside their cross-process write transaction.
262 /// Unsupported adapters fail closed rather than reopen a read/save race.
263 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 /// Authoritatively seed one validated actor activation.
278 ///
279 /// Unlike an ordinary runtime save, the incoming RunSpec posture and its
280 /// complete audit record must replace any posture left by a previous warm
281 /// activation. Implementations must still preserve durable SessionInbox
282 /// admission/transcript proof and serialize the operation per session.
283 ///
284 /// There is no safe generic implementation through
285 /// [`Self::save_runtime_session`]: that primitive is explicitly allowed to
286 /// adopt a newer disk posture, which would make warm workers sticky across
287 /// runs. Custom persisters therefore fail closed until they implement this
288 /// authority boundary deliberately.
289 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 /// Atomically persist a worker-declared executor mapping for the current
297 /// host-authoritative permission posture.
298 ///
299 /// The caller supplies the audit revision it observed before dispatch.
300 /// Implementations must load and compare that revision while holding the
301 /// per-session lock, reject a concurrent posture update, and allocate a new
302 /// host revision/timestamp themselves. Remote audit clocks are never an
303 /// authority at this boundary.
304 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 /// Persist only the runtime control-plane for a session.
317 ///
318 /// Task lists and other runtime metadata belong to the control-plane and do
319 /// not require rewriting the potentially large message transcript. Built-in
320 /// persistence implementations with a runtime sidecar should override this
321 /// operation with their sidecar-only path. Custom/legacy implementations
322 /// remain source-compatible and safely fall back to the full runtime save.
323 ///
324 /// Callers must not rely on this operation to persist message or
325 /// `model_context_state` changes. The durable ledger is checkpoint-owned;
326 /// sidecar implementations must preserve its latest committed value while
327 /// applying the caller's narrow control-plane mutation.
328 async fn save_runtime_control_plane(&self, session: &mut Session) -> io::Result<()> {
329 self.save_runtime_session(session).await
330 }
331
332 /// Load the representation paired with
333 /// [`save_runtime_control_plane`](Self::save_runtime_control_plane).
334 ///
335 /// Sidecar-capable implementations should return their message-free
336 /// control-plane snapshot. The default deliberately returns the full
337 /// runtime session: when the paired save also falls back to a full save,
338 /// retaining the transcript makes that fallback safe rather than replacing
339 /// durable messages with an empty sidecar-shaped snapshot.
340 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 /// Atomically update only the shared Task list and its version.
345 ///
346 /// The default is safe for custom/legacy persistence: it loads the full
347 /// runtime session, changes only Task-owned fields, then uses the paired
348 /// control-plane save (which itself defaults to a full save). Returning
349 /// `false` means the implementation could not load the target; callers that
350 /// also hold a [`Storage`](crate::storage::Storage) may retain legacy
351 /// behavior with an explicit full-load/full-save fallback.
352 ///
353 /// Implementations with per-session transactions should override this so
354 /// the load, narrow mutation and save share one critical section.
355 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 /// Atomically update Task-owned control-plane fields only when the durable
371 /// Task generation and exact list still match the expected snapshot.
372 ///
373 /// `false` covers an unsupported atomic compare-and-patch, a missing target,
374 /// or a version conflict. Callers must treat it as a stale write and must
375 /// not publish their staged Task state. The default fails closed because a
376 /// load followed by a separately locked save is not an atomic CAS.
377 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 /// Recoverably compare-and-patch the executing session and its shared root.
396 /// Implementations must validate both generations before either target is
397 /// written and may return `Ok(true)` only after both Task generations are
398 /// durable with no undo record that could later revert them. An error after
399 /// one physical write must restore both originals before returning or retain
400 /// durable recovery state and fail subsequent paired access closed until
401 /// recovery completes. Root-session callers pass the same id twice and
402 /// receive the single-target CAS semantics above.
403 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 /// Append-safe checkpoint used at the shared engine execute boundary.
435 ///
436 /// Unlike [`save_runtime_session`](Self::save_runtime_session), this must
437 /// preserve messages that were appended durably by a concurrent writer
438 /// after the runner loaded its snapshot. Implementations that can provide
439 /// a per-session transaction should override this method and perform the
440 /// load/merge/save under one lock. The default still reconciles against a
441 /// latest snapshot for lightweight/custom SDK persisters; the built-in
442 /// storage implementation supplies the atomic variant.
443 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 admit_root_inbox(
452 &self,
453 _session: &mut Session,
454 _inbox: Arc<dyn crate::SessionInboxPort>,
455 _active_run_id: Option<&str>,
456 ) -> io::Result<Option<RootInboxAdmission>> {
457 Ok(None)
458 }
459
460 /// Atomically commit a staged archive transcript rewrite.
461 ///
462 /// `expected_base` is the exact pre-archive Session used for planning.
463 /// Implementations must compare it with the latest durable transcript while
464 /// holding their per-session serialization lock. If a concurrent append or
465 /// rewrite is present, they must perform no save, rebase `staged` onto that
466 /// durable snapshot, and return [`RetrievalWindowCheckpointOutcome::Rebased`].
467 /// Otherwise they must preserve the staged message archive flags and commit
468 /// them with the compression event and model-context reset. The built-in
469 /// implementation also accepts a MessageSummary archive through this same
470 /// boundary, preserving raw history while replacing its derived summary.
471 ///
472 /// There is no safe fallback through the ordinary append-only checkpoint,
473 /// because that path deliberately restores durable message clones and would
474 /// erase the new archive flags. Custom persisters therefore fail closed
475 /// until they implement this boundary explicitly.
476 async fn checkpoint_retrieval_window(
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 retrieval-window checkpoints",
484 ))
485 }
486
487 /// Atomically commit a provider-visible System-prompt rewrite.
488 ///
489 /// This is deliberately separate from both the append-safe runtime
490 /// checkpoint (which must restore durable message content) and the
491 /// retrieval-window archive checkpoint (which requires a new archive
492 /// event). Implementations compare `expected_base` under their per-session
493 /// lock, return `Rebased` without writing on conflict, and accept only the
494 /// bounded prompt rewrite plus its provider/model-context reset.
495 async fn checkpoint_prompt_rewrite(
496 &self,
497 _expected_base: &Session,
498 _staged: &mut Session,
499 ) -> io::Result<RetrievalWindowCheckpointOutcome> {
500 Err(io::Error::new(
501 io::ErrorKind::Unsupported,
502 "runtime persistence does not support prompt-rewrite checkpoints",
503 ))
504 }
505
506 /// Atomically commit one permanently rejected `archive_context` result.
507 ///
508 /// This boundary permits only the correlated Tool result rewrite, the
509 /// bounded consumed/rejection metadata, and the provider/model-context
510 /// reset required to make that rewrite visible. Implementations compare
511 /// `expected_base` under their per-session lock and return `Rebased`
512 /// without writing when the durable transcript changed concurrently.
513 async fn checkpoint_manual_archive_rejection(
514 &self,
515 _expected_base: &Session,
516 _staged: &mut Session,
517 ) -> io::Result<RetrievalWindowCheckpointOutcome> {
518 Err(io::Error::new(
519 io::ErrorKind::Unsupported,
520 "runtime persistence does not support manual archive-rejection checkpoints",
521 ))
522 }
523
524 /// Atomically consume one successful no-op `archive_context` request.
525 ///
526 /// This boundary permits only the correlated consumed-occurrence marker.
527 /// Implementations compare `expected_base` under their per-session lock
528 /// and return `Rebased` without writing when any durable transcript,
529 /// metadata, runtime metadata, or execution-profile field changed. The
530 /// caller must then restage the marker from the returned durable snapshot.
531 async fn checkpoint_manual_archive_consumption(
532 &self,
533 _expected_base: &Session,
534 _staged: &mut Session,
535 ) -> io::Result<RetrievalWindowCheckpointOutcome> {
536 Err(io::Error::new(
537 io::ErrorKind::Unsupported,
538 "runtime persistence does not support manual archive-consumption checkpoints",
539 ))
540 }
541
542 /// Load the latest runtime-visible session snapshot when the persistence
543 /// implementation can coordinate reads. Tools may update a repository-owned
544 /// clone while an agent loop holds its own live Session; the loop uses this
545 /// hook to merge narrowly-scoped tool side effects before its next save.
546 async fn load_runtime_session(&self, _session_id: &str) -> io::Result<Option<Session>> {
547 Ok(None)
548 }
549
550 /// Read a canonical snapshot for execution admission without publishing or
551 /// trusting a cached clone. Preserve the same bound-reader checks as the
552 /// runtime loader. A persister with a cache must override this operation to
553 /// read its durable source (or return Unsupported); the default serves
554 /// uncached/legacy loaders. This read never grants execution/write authority.
555 async fn load_runtime_admission_snapshot(
556 &self,
557 session_id: &str,
558 ) -> io::Result<Option<Session>> {
559 self.load_runtime_session(session_id).await
560 }
561
562 /// Clear the bounded compatibility queue iff it still equals the entries
563 /// that were durably copied into SessionInbox. Implementations with a
564 /// per-session transaction should override this method.
565 async fn clear_legacy_pending_messages(
566 &self,
567 session_id: &str,
568 expected: &[serde_json::Value],
569 ) -> io::Result<bool> {
570 let Some(mut latest) = self.load_runtime_session(session_id).await? else {
571 return Ok(false);
572 };
573 if latest.pending_injected_messages().as_deref() != Some(expected) {
574 return Ok(false);
575 }
576 latest.clear_pending_injected_messages();
577 self.save_runtime_session(&mut latest).await?;
578 Ok(true)
579 }
580
581 /// Append one JSON-line analysis record to the session's append-only
582 /// token-usage log (see [`Storage::append_token_usage_record`]). Defaults to
583 /// a no-op so non-file-backed persisters are unaffected.
584 ///
585 /// [`Storage::append_token_usage_record`]: crate::storage::Storage::append_token_usage_record
586 async fn append_token_usage_record(&self, session_id: &str, json_line: &str) -> io::Result<()> {
587 let _ = (session_id, json_line);
588 Ok(())
589 }
590}
591
592/// Ownership passed from the canonical repository into one concrete runtime.
593/// The runtime owns renewal and finish; dropping an execution must release its
594/// own fence without ever finishing a replacement owner.
595pub struct RootActorExecutionBinding {
596 pub persistence: Arc<dyn RuntimeSessionPersistence>,
597 pub directory: Arc<dyn crate::ActorDirectoryPort>,
598 pub owner: crate::RootActorRuntimeWrite,
599 pub lease_duration: std::time::Duration,
600 abandon: Option<Box<dyn FnOnce() + Send + Sync>>,
601}
602
603impl RootActorExecutionBinding {
604 pub fn new(
605 persistence: Arc<dyn RuntimeSessionPersistence>,
606 directory: Arc<dyn crate::ActorDirectoryPort>,
607 owner: crate::RootActorRuntimeWrite,
608 lease_duration: std::time::Duration,
609 abandon: impl FnOnce() + Send + Sync + 'static,
610 ) -> Self {
611 Self {
612 persistence,
613 directory,
614 owner,
615 lease_duration,
616 abandon: Some(Box::new(abandon)),
617 }
618 }
619
620 /// The runtime calls this only after handing finish to an owned detached
621 /// job, so cancellation of the waiter cannot strand this execution.
622 pub fn disarm_abandonment(&mut self) {
623 self.abandon.take();
624 }
625}
626
627impl Drop for RootActorExecutionBinding {
628 fn drop(&mut self) {
629 if let Some(abandon) = self.abandon.take() {
630 abandon();
631 }
632 }
633}
634
635#[async_trait::async_trait]
636impl<T: RuntimeSessionPersistence + ?Sized> RuntimeSessionPersistence for Arc<T> {
637 fn inherited_child_wait(&self) -> Option<InheritedChildWait> {
638 (**self).inherited_child_wait()
639 }
640
641 fn bind_inherited_child_wait(
642 &self,
643 inherited: InheritedChildWait,
644 ) -> io::Result<Arc<dyn RuntimeSessionPersistence>> {
645 (**self).bind_inherited_child_wait(inherited)
646 }
647
648 fn root_actor_writer(&self) -> Option<crate::RootActorRuntimeWrite> {
649 (**self).root_actor_writer()
650 }
651
652 fn bind_root_inbox_receipt(
653 &self,
654 receipt: crate::SessionInboxReceipt,
655 ) -> io::Result<Arc<dyn RuntimeSessionPersistence>> {
656 (**self).bind_root_inbox_receipt(receipt)
657 }
658
659 fn root_inbox_receipt(&self) -> Option<&crate::SessionInboxReceipt> {
660 (**self).root_inbox_receipt()
661 }
662
663 fn root_actor_execution_required(&self, session: &Session) -> bool {
664 (**self).root_actor_execution_required(session)
665 }
666
667 async fn bind_root_actor_execution(
668 &self,
669 session: &Session,
670 run_id: &str,
671 ) -> io::Result<Option<RootActorExecutionBinding>> {
672 (**self).bind_root_actor_execution(session, run_id).await
673 }
674 async fn save_runtime_session(&self, session: &mut Session) -> io::Result<()> {
675 (**self).save_runtime_session(session).await
676 }
677
678 async fn save_finalized_runtime_session(&self, session: &mut Session) -> io::Result<()> {
679 (**self).save_finalized_runtime_session(session).await
680 }
681
682 async fn save_finalized_runtime_with_inherited_child_wait(
683 &self,
684 session: &mut Session,
685 inherited: &crate::session::runtime_state::WaitingForChildrenState,
686 ) -> io::Result<()> {
687 (**self)
688 .save_finalized_runtime_with_inherited_child_wait(session, inherited)
689 .await
690 }
691
692 async fn seed_runtime_activation(&self, session: &mut Session) -> io::Result<()> {
693 (**self).seed_runtime_activation(session).await
694 }
695
696 async fn record_permission_posture_activation(
697 &self,
698 session_id: &str,
699 expected_audit_revision: Option<u64>,
700 seed: &PermissionAuditSeed,
701 ) -> io::Result<Option<Session>> {
702 (**self)
703 .record_permission_posture_activation(session_id, expected_audit_revision, seed)
704 .await
705 }
706
707 async fn save_runtime_control_plane(&self, session: &mut Session) -> io::Result<()> {
708 (**self).save_runtime_control_plane(session).await
709 }
710
711 async fn load_runtime_control_plane(&self, session_id: &str) -> io::Result<Option<Session>> {
712 (**self).load_runtime_control_plane(session_id).await
713 }
714
715 async fn update_task_list_control_plane(
716 &self,
717 session_id: &str,
718 task_list: &TaskList,
719 version: &str,
720 ) -> io::Result<bool> {
721 (**self)
722 .update_task_list_control_plane(session_id, task_list, version)
723 .await
724 }
725
726 async fn update_task_list_control_plane_if_version(
727 &self,
728 session_id: &str,
729 expected_version: &str,
730 expected_task_list: &TaskList,
731 task_list: &TaskList,
732 version: &str,
733 ) -> io::Result<bool> {
734 (**self)
735 .update_task_list_control_plane_if_version(
736 session_id,
737 expected_version,
738 expected_task_list,
739 task_list,
740 version,
741 )
742 .await
743 }
744
745 async fn update_task_list_control_planes_if_version(
746 &self,
747 session_id: &str,
748 shared_session_id: &str,
749 expected_version: &str,
750 expected_task_list: &TaskList,
751 task_list: &TaskList,
752 version: &str,
753 ) -> io::Result<bool> {
754 (**self)
755 .update_task_list_control_planes_if_version(
756 session_id,
757 shared_session_id,
758 expected_version,
759 expected_task_list,
760 task_list,
761 version,
762 )
763 .await
764 }
765
766 async fn checkpoint_runtime_session(&self, session: &mut Session) -> io::Result<()> {
767 (**self).checkpoint_runtime_session(session).await
768 }
769
770 async fn admit_root_inbox(
771 &self,
772 session: &mut Session,
773 inbox: Arc<dyn crate::SessionInboxPort>,
774 active_run_id: Option<&str>,
775 ) -> io::Result<Option<RootInboxAdmission>> {
776 (**self)
777 .admit_root_inbox(session, inbox, active_run_id)
778 .await
779 }
780
781 async fn checkpoint_retrieval_window(
782 &self,
783 expected_base: &Session,
784 staged: &mut Session,
785 ) -> io::Result<RetrievalWindowCheckpointOutcome> {
786 (**self)
787 .checkpoint_retrieval_window(expected_base, staged)
788 .await
789 }
790
791 async fn checkpoint_prompt_rewrite(
792 &self,
793 expected_base: &Session,
794 staged: &mut Session,
795 ) -> io::Result<RetrievalWindowCheckpointOutcome> {
796 (**self)
797 .checkpoint_prompt_rewrite(expected_base, staged)
798 .await
799 }
800
801 async fn checkpoint_manual_archive_rejection(
802 &self,
803 expected_base: &Session,
804 staged: &mut Session,
805 ) -> io::Result<RetrievalWindowCheckpointOutcome> {
806 (**self)
807 .checkpoint_manual_archive_rejection(expected_base, staged)
808 .await
809 }
810
811 async fn checkpoint_manual_archive_consumption(
812 &self,
813 expected_base: &Session,
814 staged: &mut Session,
815 ) -> io::Result<RetrievalWindowCheckpointOutcome> {
816 (**self)
817 .checkpoint_manual_archive_consumption(expected_base, staged)
818 .await
819 }
820
821 async fn load_runtime_session(&self, session_id: &str) -> io::Result<Option<Session>> {
822 (**self).load_runtime_session(session_id).await
823 }
824
825 async fn load_runtime_admission_snapshot(
826 &self,
827 session_id: &str,
828 ) -> io::Result<Option<Session>> {
829 (**self).load_runtime_admission_snapshot(session_id).await
830 }
831
832 async fn clear_legacy_pending_messages(
833 &self,
834 session_id: &str,
835 expected: &[serde_json::Value],
836 ) -> io::Result<bool> {
837 (**self)
838 .clear_legacy_pending_messages(session_id, expected)
839 .await
840 }
841
842 async fn append_token_usage_record(&self, session_id: &str, json_line: &str) -> io::Result<()> {
843 (**self)
844 .append_token_usage_record(session_id, json_line)
845 .await
846 }
847}