Skip to main content

starweaver_session/store/
contract.rs

1//! Durable session store contract.
2
3use async_trait::async_trait;
4use starweaver_context::{AgentCheckpoint, ResumableState};
5use starweaver_core::{RunId, SessionId};
6use starweaver_stream::{AgentStreamRecord, ReplayEvent};
7
8use crate::{
9    AcquireBackgroundSubagentContinuation, AcquireRunAdmission, AdmitRunControl,
10    BackgroundSubagentContinuationReceipt, BackgroundSubagentRecord,
11    DurableBackgroundSubagentDeliveryClaim, DurableBackgroundSubagentDeliveryRelease,
12    DurableControlReceipt, DurableRunControlIntent, DurableRunControlStatus, RunAdmissionLease,
13    RunAdmissionReceipt, SessionContinuationFence, UpdateManagedSession,
14    approval::{ApprovalRecord, DeferredToolRecord},
15    claim::HitlResumeClaim,
16    error::{SessionStoreError, SessionStoreResult},
17    evidence::RunEvidenceCommit,
18    host_events::{
19        DurableHostEventClass, DurableHostEventPage, DurableHostEventQuery, DurableHostEventRecord,
20        DurableHostEventScope, PendingHostEventPublication,
21    },
22    publication::{PendingStreamPublication, StreamPublicationTarget},
23    records::{
24        EnvironmentStateRef, RunRecord, RunStatus, RunTerminalProjection, SessionRecord,
25        SessionStatus, StreamCursorRef,
26    },
27    resume::SessionResumeSnapshot,
28    trace::{CompactRunTrace, CompactSessionTrace},
29};
30
31fn management_unsupported<T>() -> SessionStoreResult<T> {
32    Err(SessionStoreError::Failed(
33        "session store does not support agent session management".to_string(),
34    ))
35}
36
37/// Query filters for listing sessions.
38#[derive(Clone, Debug, Default, Eq, PartialEq)]
39pub struct SessionFilter {
40    /// Required session status.
41    pub status: Option<SessionStatus>,
42    /// Required profile name.
43    pub profile: Option<String>,
44    /// Required workspace identifier or path.
45    pub workspace: Option<String>,
46    /// Maximum number of sessions returned.
47    pub limit: Option<usize>,
48}
49
50/// Maximum number of records returned by one stable keyset page.
51pub const MAX_STABLE_PAGE_SIZE: usize = 200;
52
53/// Stable key identifying one session's position in the updated-time ordering.
54#[derive(Clone, Debug, Eq, PartialEq)]
55pub struct SessionPageKey {
56    /// Last update time of the session at the page boundary.
57    pub updated_at: chrono::DateTime<chrono::Utc>,
58    /// Stable session identity used to break equal-timestamp ties.
59    pub session_id: SessionId,
60}
61
62impl SessionPageKey {
63    /// Build a key from one returned session.
64    #[must_use]
65    pub fn from_session(session: &SessionRecord) -> Self {
66        Self {
67            updated_at: session.updated_at,
68            session_id: session.session_id.clone(),
69        }
70    }
71}
72
73/// Bounded keyset query over sessions ordered by update time and stable identity descending.
74#[derive(Clone, Debug, Eq, PartialEq)]
75pub struct SessionPageQuery {
76    after: Option<SessionPageKey>,
77    limit: usize,
78}
79
80impl SessionPageQuery {
81    /// Build a validated session-page query.
82    ///
83    /// # Errors
84    ///
85    /// Returns an error unless `limit` is between 1 and [`MAX_STABLE_PAGE_SIZE`].
86    pub fn new(after: Option<SessionPageKey>, limit: usize) -> SessionStoreResult<Self> {
87        validate_page_limit(limit)?;
88        Ok(Self { after, limit })
89    }
90
91    /// Return the exclusive page boundary.
92    #[must_use]
93    pub const fn after(&self) -> Option<&SessionPageKey> {
94        self.after.as_ref()
95    }
96
97    /// Return the validated page size.
98    #[must_use]
99    pub const fn limit(&self) -> usize {
100        self.limit
101    }
102}
103
104/// One stable page of durable sessions.
105#[derive(Clone, Debug, Eq, PartialEq)]
106pub struct SessionPage {
107    /// Sessions in `updated_at DESC, session_id DESC` order.
108    pub sessions: Vec<SessionRecord>,
109    /// Last returned key, or the requested start key when the page is empty.
110    pub next_key: Option<SessionPageKey>,
111    /// Whether another record exists after `next_key`.
112    pub has_more: bool,
113}
114
115/// Stable key identifying one HITL record's position in updated-time ordering.
116#[derive(Clone, Debug, Eq, PartialEq)]
117pub struct InteractionPageKey {
118    /// Last update time of the interaction at the page boundary.
119    pub updated_at: chrono::DateTime<chrono::Utc>,
120    /// Approval or deferred-tool identity used to break equal-timestamp ties.
121    pub interaction_id: String,
122}
123
124/// Bounded keyset query over approval or deferred-tool records.
125#[derive(Clone, Debug, Eq, PartialEq)]
126pub struct InteractionPageQuery {
127    session_id: Option<SessionId>,
128    run_id: Option<RunId>,
129    after: Option<InteractionPageKey>,
130    limit: usize,
131}
132
133impl InteractionPageQuery {
134    /// Build a validated interaction-page query.
135    ///
136    /// # Errors
137    ///
138    /// Returns an error unless `limit` is between 1 and [`MAX_STABLE_PAGE_SIZE`].
139    pub fn new(
140        session_id: Option<SessionId>,
141        run_id: Option<RunId>,
142        after: Option<InteractionPageKey>,
143        limit: usize,
144    ) -> SessionStoreResult<Self> {
145        validate_page_limit(limit)?;
146        Ok(Self {
147            session_id,
148            run_id,
149            after,
150            limit,
151        })
152    }
153
154    /// Return the optional owning-session filter.
155    #[must_use]
156    pub const fn session_id(&self) -> Option<&SessionId> {
157        self.session_id.as_ref()
158    }
159
160    /// Return the optional owning-run filter.
161    #[must_use]
162    pub const fn run_id(&self) -> Option<&RunId> {
163        self.run_id.as_ref()
164    }
165
166    /// Return the exclusive page boundary.
167    #[must_use]
168    pub const fn after(&self) -> Option<&InteractionPageKey> {
169        self.after.as_ref()
170    }
171
172    /// Return the validated page size.
173    #[must_use]
174    pub const fn limit(&self) -> usize {
175        self.limit
176    }
177}
178
179/// One stable page of approval or deferred-tool records.
180#[derive(Clone, Debug, Eq, PartialEq)]
181pub struct InteractionPage<T> {
182    /// Records in `updated_at DESC, stable_id DESC` order.
183    pub records: Vec<T>,
184    /// Last returned key, or the requested start key when the page is empty.
185    pub next_key: Option<InteractionPageKey>,
186    /// Whether another record exists after `next_key`.
187    pub has_more: bool,
188}
189
190fn validate_page_limit(limit: usize) -> SessionStoreResult<()> {
191    if !(1..=MAX_STABLE_PAGE_SIZE).contains(&limit) {
192        return Err(SessionStoreError::Failed(format!(
193            "stable page limit must be between 1 and {MAX_STABLE_PAGE_SIZE}"
194        )));
195    }
196    Ok(())
197}
198
199/// Durable session store contract.
200#[async_trait]
201pub trait SessionStore: Send + Sync {
202    /// Atomically persist a complete run evidence bundle.
203    ///
204    /// Implementations must leave either the complete previous state or the complete committed
205    /// state visible. One successful logical commit advances each changed existing run exactly
206    /// once (new runs begin at revision one), and atomically enqueues authoritative run/output
207    /// host publications. Identical retries are idempotent and conflicting retries must fail.
208    async fn commit_run_evidence(&self, commit: RunEvidenceCommit)
209    -> SessionStoreResult<RunRecord>;
210
211    /// Atomically persist a complete run evidence bundle under an active admission lease.
212    ///
213    /// Implementations must validate the admission identity and fencing generation in the same
214    /// transaction as the evidence write. Exact evidence retries may succeed after release, but
215    /// a new or conflicting write from an expired or stale owner must fail.
216    async fn commit_run_evidence_fenced(
217        &self,
218        _lease: &RunAdmissionLease,
219        _commit: RunEvidenceCommit,
220    ) -> SessionStoreResult<RunRecord> {
221        management_unsupported()
222    }
223
224    /// Atomically append a replay-event batch under an active admission lease.
225    ///
226    /// Every event must use the run-local scope for the admitted run. Implementations must
227    /// validate the active admission identity, host, target, generation, and expiry in the same
228    /// transaction as the inserts. Exact retries are idempotent; a different event at an occupied
229    /// sequence conflicts and leaves the entire batch unchanged.
230    async fn append_replay_events_fenced(
231        &self,
232        _lease: &RunAdmissionLease,
233        _events: Vec<ReplayEvent>,
234    ) -> SessionStoreResult<()> {
235        management_unsupported()
236    }
237
238    /// Atomically bootstrap missing session/run records and persist one runtime checkpoint.
239    ///
240    /// This is the executor write path. Implementations must not expose a session or run without
241    /// the checkpoint when the operation fails. Exact checkpoint retries are idempotent and
242    /// conflicting retries fail.
243    async fn commit_checkpoint(
244        &self,
245        session_id: &SessionId,
246        checkpoint: AgentCheckpoint,
247    ) -> SessionStoreResult<()>;
248
249    /// Persist one runtime checkpoint under an active admission lease.
250    ///
251    /// The store must validate the lease in the same transaction as a new checkpoint write.
252    async fn commit_checkpoint_fenced(
253        &self,
254        _lease: &RunAdmissionLease,
255        _checkpoint: AgentCheckpoint,
256    ) -> SessionStoreResult<()> {
257        management_unsupported()
258    }
259
260    /// Acquire exclusive ownership of a waiting run before any continuation side effect.
261    ///
262    /// A run may have at most one active claim. Claims are consumed by the related-run update in
263    /// [`Self::commit_run_evidence`].
264    async fn claim_hitl_resume(&self, _claim: HitlResumeClaim) -> SessionStoreResult<()> {
265        Err(SessionStoreError::Failed(
266            "session store does not support exclusive HITL resume claims".to_string(),
267        ))
268    }
269
270    /// Atomically authorize the first HITL continuation effect under a live admission lease.
271    ///
272    /// Stores must verify the exact current lease, its expiry, the target run's source binding,
273    /// and the matching `Admitted` claim in one operation before advancing the claim to `Started`.
274    async fn start_hitl_resume_effect(
275        &self,
276        _lease: &RunAdmissionLease,
277        _source_run_id: &RunId,
278        _claim_id: &str,
279    ) -> SessionStoreResult<()> {
280        management_unsupported()
281    }
282
283    /// Atomically abort a pre-worker waiting-run replacement under its live admission lease.
284    ///
285    /// An `Admitted` claim proves no approved effect can have run, so this terminalizes only the
286    /// replacement and consumes the claim while leaving the source waiting. `Started` is reported
287    /// without mutation: callers must instead persist fail-closed related-run evidence.
288    async fn abort_admitted_hitl_resume(
289        &self,
290        _lease: &RunAdmissionLease,
291        _source_run_id: &RunId,
292        _claim_id: &str,
293        _output_preview: &str,
294    ) -> SessionStoreResult<crate::HitlResumeAbortOutcome> {
295        management_unsupported()
296    }
297
298    /// Mark a non-admitted claim started immediately before the first continuation hook or tool executes.
299    async fn mark_hitl_resume_started(
300        &self,
301        _session_id: &SessionId,
302        _run_id: &RunId,
303        _claim_id: &str,
304    ) -> SessionStoreResult<()> {
305        Err(SessionStoreError::Failed(
306            "session store does not support exclusive HITL resume claims".to_string(),
307        ))
308    }
309
310    /// Release a preflight claim. Stores must reject release after execution has started.
311    async fn release_hitl_resume_claim(
312        &self,
313        _session_id: &SessionId,
314        _run_id: &RunId,
315        _claim_id: &str,
316    ) -> SessionStoreResult<()> {
317        Err(SessionStoreError::Failed(
318            "session store does not support exclusive HITL resume claims".to_string(),
319        ))
320    }
321
322    /// List transactionally enqueued stream publications still awaiting at least one sink.
323    async fn pending_stream_publications(
324        &self,
325        _session_id: &SessionId,
326    ) -> SessionStoreResult<Vec<PendingStreamPublication>> {
327        Err(SessionStoreError::Failed(
328            "session store does not support transactional stream publication".to_string(),
329        ))
330    }
331
332    /// Acknowledge one sink only after its complete idempotent delivery succeeds.
333    async fn acknowledge_stream_publication(
334        &self,
335        _publication_id: &str,
336        _target: StreamPublicationTarget,
337    ) -> SessionStoreResult<()> {
338        Err(SessionStoreError::Failed(
339            "session store does not support transactional stream publication".to_string(),
340        ))
341    }
342
343    /// Atomically enqueue view-independent host-event publications.
344    ///
345    /// Exact retries are idempotent. Reusing a publication key or event identity with different
346    /// evidence must fail without inserting any member of the batch.
347    async fn enqueue_host_event_publications(
348        &self,
349        _publications: Vec<PendingHostEventPublication>,
350    ) -> SessionStoreResult<()> {
351        management_unsupported()
352    }
353
354    /// List a bounded oldest-first batch of host-event outbox entries.
355    async fn pending_host_event_publications(
356        &self,
357        _limit: usize,
358    ) -> SessionStoreResult<Vec<PendingHostEventPublication>> {
359        management_unsupported()
360    }
361
362    /// Idempotently materialize a bounded outbox batch into the canonical durable event log.
363    ///
364    /// Position allocation, exact-conflict checks, durable inserts, and outbox deletion occur in
365    /// one transaction. Returned records are durable before this method completes.
366    async fn materialize_host_event_publications(
367        &self,
368        _limit: usize,
369    ) -> SessionStoreResult<Vec<DurableHostEventRecord>> {
370        management_unsupported()
371    }
372
373    /// Replay a bounded, class-filtered durable host-event page.
374    async fn replay_host_events(
375        &self,
376        _query: DurableHostEventQuery,
377    ) -> SessionStoreResult<DurableHostEventPage> {
378        management_unsupported()
379    }
380
381    /// Capture the latest eligible durable backend position for one scope and class set.
382    async fn host_event_fence(
383        &self,
384        _scope: &DurableHostEventScope,
385        _event_classes: &[DurableHostEventClass],
386    ) -> SessionStoreResult<Option<u64>> {
387        management_unsupported()
388    }
389
390    /// Atomically create a session and bind an idempotency key to a normalized fingerprint.
391    async fn create_session_idempotent(
392        &self,
393        _session: SessionRecord,
394        _idempotency_key: &str,
395        _command_fingerprint: &str,
396    ) -> SessionStoreResult<SessionRecord> {
397        management_unsupported()
398    }
399
400    /// Atomically create a session, bind its idempotency receipt, and enqueue host events.
401    ///
402    /// Exact retries validate and deduplicate the supplied publications. A fingerprint or event
403    /// conflict leaves the session, receipt, and outbox unchanged.
404    async fn create_session_idempotent_with_host_events(
405        &self,
406        _session: SessionRecord,
407        _idempotency_key: &str,
408        _command_fingerprint: &str,
409        _publications: Vec<PendingHostEventPublication>,
410    ) -> SessionStoreResult<SessionRecord> {
411        management_unsupported()
412    }
413
414    /// Read a session mutation receipt without creating or changing durable state.
415    async fn load_session_mutation_receipt(
416        &self,
417        _namespace_id: &str,
418        _idempotency_key: &str,
419        _command_fingerprint: &str,
420    ) -> SessionStoreResult<Option<SessionRecord>> {
421        management_unsupported()
422    }
423
424    /// Apply an allowlisted session patch with expected-revision and idempotency checks.
425    async fn update_managed_session(
426        &self,
427        _command: UpdateManagedSession,
428        _command_fingerprint: &str,
429    ) -> SessionStoreResult<SessionRecord> {
430        management_unsupported()
431    }
432
433    /// Apply an allowlisted session patch and enqueue host events in the same atomic mutation.
434    async fn update_managed_session_with_host_events(
435        &self,
436        _command: UpdateManagedSession,
437        _command_fingerprint: &str,
438        _publications: Vec<PendingHostEventPublication>,
439    ) -> SessionStoreResult<SessionRecord> {
440        management_unsupported()
441    }
442
443    /// Acquire a deletion fence that blocks run, continuation, and delegation admission.
444    async fn acquire_session_deletion_fence(
445        &self,
446        _session_id: &SessionId,
447        _expected_revision: u64,
448        _fence_id: &str,
449        _requested_by: &str,
450        _idempotency_key: &str,
451        _command_fingerprint: &str,
452    ) -> SessionStoreResult<SessionRecord> {
453        management_unsupported()
454    }
455
456    /// Complete a fenced session tombstone. This never purges retained evidence.
457    async fn tombstone_session(
458        &self,
459        _session_id: &SessionId,
460        _fence_id: &str,
461    ) -> SessionStoreResult<SessionRecord> {
462        management_unsupported()
463    }
464
465    /// Complete a fenced tombstone while replacing its idempotency receipt and enqueuing events.
466    ///
467    /// The key and fingerprint must match the receipt written when the deletion fence was
468    /// acquired. Exact retries are idempotent and validate the same event publications.
469    #[allow(clippy::too_many_arguments)]
470    async fn tombstone_session_idempotent_with_host_events(
471        &self,
472        _session_id: &SessionId,
473        _fence_id: &str,
474        _idempotency_key: &str,
475        _command_fingerprint: &str,
476        _publications: Vec<PendingHostEventPublication>,
477    ) -> SessionStoreResult<SessionRecord> {
478        management_unsupported()
479    }
480
481    /// Load the deletion/continuation fence used by async supervisors before side effects.
482    async fn session_continuation_fence(
483        &self,
484        _namespace_id: &str,
485        _session_id: &SessionId,
486    ) -> SessionStoreResult<SessionContinuationFence> {
487        management_unsupported()
488    }
489
490    /// Atomically persist a queued run and acquire the session's single active lease.
491    async fn acquire_run_admission(
492        &self,
493        _request: AcquireRunAdmission,
494    ) -> SessionStoreResult<RunAdmissionReceipt> {
495        management_unsupported()
496    }
497
498    /// Read an admission idempotency receipt without creating or changing durable state.
499    async fn load_run_admission_receipt(
500        &self,
501        _namespace_id: &str,
502        _idempotency_key: &str,
503        _command_fingerprint: &str,
504    ) -> SessionStoreResult<Option<RunAdmissionReceipt>> {
505        management_unsupported()
506    }
507
508    /// Extend a lease only for its current host and fencing generation.
509    async fn heartbeat_run_admission(
510        &self,
511        _lease: &RunAdmissionLease,
512        _lease_expires_at: chrono::DateTime<chrono::Utc>,
513    ) -> SessionStoreResult<RunAdmissionLease> {
514        management_unsupported()
515    }
516
517    /// Release a lease after terminal run durability; stale generations cannot release a new owner.
518    async fn release_run_admission(&self, _lease: &RunAdmissionLease) -> SessionStoreResult<()> {
519        management_unsupported()
520    }
521
522    /// Update one admitted run to an active status while validating its lease atomically.
523    ///
524    /// Terminal transitions must use [`Self::finalize_run_admission`] so status, output, and
525    /// diagnostics cannot be persisted independently.
526    async fn update_run_status_fenced(
527        &self,
528        _lease: &RunAdmissionLease,
529        _status: RunStatus,
530        _output_preview: Option<String>,
531    ) -> SessionStoreResult<RunRecord> {
532        management_unsupported()
533    }
534
535    /// Atomically persist a non-active status and release its matching admission lease.
536    ///
537    /// If complete terminal evidence was already committed under the active lease, that evidence
538    /// is authoritative: finalization releases only the matching lease and ignores a differing
539    /// process-local fallback outcome. An exact retry after a successful commit is idempotent. A
540    /// stale owner cannot overwrite a different terminal result or release a newer generation.
541    async fn finalize_run_admission(
542        &self,
543        _lease: &RunAdmissionLease,
544        _terminal: RunTerminalProjection,
545    ) -> SessionStoreResult<RunRecord> {
546        management_unsupported()
547    }
548
549    /// Load durable admission truth for a composite target.
550    async fn load_run_admission(
551        &self,
552        _target: &crate::ManagedRunTarget,
553    ) -> SessionStoreResult<Option<RunAdmissionLease>> {
554        management_unsupported()
555    }
556
557    /// Deterministically terminalize expired active leases owned by prior host instances.
558    ///
559    /// When an expired lease belongs to a waiting-HITL replacement whose source still waits, the
560    /// replacement and source must both become cancelled, the exact started source claim must be
561    /// validated and consumed, the admission must be removed, and session active-run state must
562    /// be cleared as one atomic operation. Any mismatch fails closed without exposing a partial
563    /// transition. Ordinary expired admissions retain the same replacement-only terminalization.
564    async fn reconcile_expired_run_admissions(
565        &self,
566        _namespace_id: &str,
567        _now: chrono::DateTime<chrono::Utc>,
568    ) -> SessionStoreResult<Vec<crate::ManagedRunTarget>> {
569        management_unsupported()
570    }
571
572    /// Atomically reserve an authority/key/fingerprint-bound receipt and durable effect intent.
573    ///
574    /// Implementations must validate the exact live admission lease in the same transaction as
575    /// both records. Exact retries return the original intent; any identity or payload mismatch
576    /// fails without changing the receipt or inbox. No runtime effect may occur before success.
577    async fn admit_run_control(
578        &self,
579        _request: AdmitRunControl,
580    ) -> SessionStoreResult<DurableRunControlIntent> {
581        management_unsupported()
582    }
583
584    /// Load one durable control effect by target and deterministic operation id.
585    async fn load_run_control_intent(
586        &self,
587        _target: &crate::ManagedRunTarget,
588        _operation_id: &str,
589    ) -> SessionStoreResult<Option<DurableRunControlIntent>> {
590        management_unsupported()
591    }
592
593    /// List a bounded oldest-first control inbox for recovery and delivery.
594    async fn list_run_control_intents(
595        &self,
596        _target: &crate::ManagedRunTarget,
597        _statuses: &[DurableRunControlStatus],
598        _limit: usize,
599    ) -> SessionStoreResult<Vec<DurableRunControlIntent>> {
600        management_unsupported()
601    }
602
603    /// Monotonically acknowledge runtime delivery or consumption under the exact live lease.
604    ///
605    /// The store validates admission id, host, target, generation, and expiry in the same
606    /// transaction as the state update. Exact state retries are idempotent.
607    async fn advance_run_control_intent(
608        &self,
609        _lease: &RunAdmissionLease,
610        _operation_id: &str,
611        _expected: DurableRunControlStatus,
612        _next: DurableRunControlStatus,
613        _occurred_at: chrono::DateTime<chrono::Utc>,
614    ) -> SessionStoreResult<DurableRunControlIntent> {
615        management_unsupported()
616    }
617
618    /// Mark a pending, delivered, or consumed intent reconciled during terminal/stale recovery.
619    async fn reconcile_run_control_intent(
620        &self,
621        _target: &crate::ManagedRunTarget,
622        _operation_id: &str,
623        _occurred_at: chrono::DateTime<chrono::Utc>,
624    ) -> SessionStoreResult<DurableRunControlIntent> {
625        management_unsupported()
626    }
627
628    /// Load a durable control receipt by composite target and idempotency key.
629    async fn load_control_receipt(
630        &self,
631        _target: &crate::ManagedRunTarget,
632        _idempotency_key: &str,
633    ) -> SessionStoreResult<Option<DurableControlReceipt>> {
634        management_unsupported()
635    }
636
637    /// Reserve or replay a durable fenced control receipt.
638    async fn reserve_control_receipt(
639        &self,
640        _receipt: DurableControlReceipt,
641    ) -> SessionStoreResult<DurableControlReceipt> {
642        management_unsupported()
643    }
644
645    /// Record the final accepted/failed effect state for a reserved receipt.
646    async fn update_control_receipt_state(
647        &self,
648        _receipt_id: &str,
649        _state: &str,
650    ) -> SessionStoreResult<DurableControlReceipt> {
651        management_unsupported()
652    }
653
654    /// Wait for store-owned background-subagent operations whose caller futures may have ended.
655    ///
656    /// Implementations that detach non-cancellable database or network work must retain and drain
657    /// that work here. Cancellation-safe implementations must explicitly return success; the
658    /// default fails closed so a store cannot accidentally claim a complete shutdown guarantee.
659    async fn drain_background_subagent_operations(&self) -> SessionStoreResult<()> {
660        management_unsupported()
661    }
662
663    /// Idempotently persist one accepted durable background-subagent attempt.
664    async fn record_background_subagent_acceptance(
665        &self,
666        _record: BackgroundSubagentRecord,
667    ) -> SessionStoreResult<BackgroundSubagentRecord> {
668        management_unsupported()
669    }
670
671    /// Persist a monotonic non-terminal lifecycle transition or child-run correlation.
672    async fn update_background_subagent_execution(
673        &self,
674        _record: BackgroundSubagentRecord,
675    ) -> SessionStoreResult<BackgroundSubagentRecord> {
676        management_unsupported()
677    }
678
679    /// Extend an active background execution lease for its current fenced owner.
680    async fn heartbeat_background_subagent(
681        &self,
682        _attempt_id: &starweaver_core::SubagentAttemptId,
683        _host_instance_id: &str,
684        _fencing_generation: u64,
685        _lease_expires_at: chrono::DateTime<chrono::Utc>,
686    ) -> SessionStoreResult<BackgroundSubagentRecord> {
687        management_unsupported()
688    }
689
690    /// Atomically persist terminal evidence and its optional oversized-result artifact.
691    async fn commit_background_subagent_terminal(
692        &self,
693        commit: crate::BackgroundSubagentTerminalCommit,
694    ) -> SessionStoreResult<BackgroundSubagentRecord> {
695        if commit.artifact.is_some() {
696            return management_unsupported();
697        }
698        self.record_background_subagent_terminal(commit.record)
699            .await
700    }
701
702    /// Load one retained background-result artifact by stable reference.
703    async fn load_background_subagent_artifact(
704        &self,
705        _artifact_ref: &str,
706    ) -> SessionStoreResult<crate::BackgroundSubagentArtifact> {
707        management_unsupported()
708    }
709
710    /// Expire retained background-result content while preserving minimal audit evidence.
711    async fn expire_background_subagent_retention(
712        &self,
713        _namespace_id: &str,
714        _now: chrono::DateTime<chrono::Utc>,
715        _limit: usize,
716    ) -> SessionStoreResult<Vec<BackgroundSubagentRecord>> {
717        management_unsupported()
718    }
719
720    /// Idempotently persist immutable terminal outcome before delivery becomes claimable.
721    async fn record_background_subagent_terminal(
722        &self,
723        _record: BackgroundSubagentRecord,
724    ) -> SessionStoreResult<BackgroundSubagentRecord> {
725        management_unsupported()
726    }
727
728    /// Load one durable background attempt by globally unique attempt identity.
729    async fn load_background_subagent(
730        &self,
731        _attempt_id: &starweaver_core::SubagentAttemptId,
732    ) -> SessionStoreResult<BackgroundSubagentRecord> {
733        management_unsupported()
734    }
735
736    /// List bounded durable background attempts in one host namespace and optional session.
737    async fn list_background_subagents(
738        &self,
739        _namespace_id: &str,
740        _session_id: Option<&SessionId>,
741        _limit: usize,
742    ) -> SessionStoreResult<Vec<BackgroundSubagentRecord>> {
743        management_unsupported()
744    }
745
746    /// List terminal results still awaiting or holding logical delivery ownership.
747    async fn list_pending_background_subagents(
748        &self,
749        _namespace_id: &str,
750        _session_id: Option<&SessionId>,
751        _limit: usize,
752    ) -> SessionStoreResult<Vec<BackgroundSubagentRecord>> {
753        management_unsupported()
754    }
755
756    /// Atomically claim one terminal result, allowing exact-claim idempotent replay.
757    async fn claim_background_subagent_delivery(
758        &self,
759        _attempt_id: &starweaver_core::SubagentAttemptId,
760        _claim: DurableBackgroundSubagentDeliveryClaim,
761    ) -> SessionStoreResult<BackgroundSubagentRecord> {
762        management_unsupported()
763    }
764
765    /// Acknowledge one matching claim as logically delivered.
766    async fn acknowledge_background_subagent_delivery(
767        &self,
768        _attempt_id: &starweaver_core::SubagentAttemptId,
769        _claim_id: &str,
770    ) -> SessionStoreResult<BackgroundSubagentRecord> {
771        management_unsupported()
772    }
773
774    /// Release one matching claim with a durable retry or consumer-termination disposition.
775    async fn release_background_subagent_delivery(
776        &self,
777        _attempt_id: &starweaver_core::SubagentAttemptId,
778        _claim_id: &str,
779        _release: DurableBackgroundSubagentDeliveryRelease,
780    ) -> SessionStoreResult<BackgroundSubagentRecord> {
781        management_unsupported()
782    }
783
784    /// Atomically admit a continuation run and consume its background result exactly once.
785    async fn acquire_background_subagent_continuation(
786        &self,
787        _request: AcquireBackgroundSubagentContinuation,
788    ) -> SessionStoreResult<BackgroundSubagentContinuationReceipt> {
789        management_unsupported()
790    }
791
792    /// Classify lost in-process executions and reclaim expired delivery claims after restart.
793    async fn reconcile_background_subagents(
794        &self,
795        _namespace_id: &str,
796        _now: chrono::DateTime<chrono::Utc>,
797    ) -> SessionStoreResult<Vec<BackgroundSubagentRecord>> {
798        management_unsupported()
799    }
800
801    /// Save a session record.
802    ///
803    /// A new record retains its supplied creation and update evidence; updating an existing
804    /// record assigns the store's current update time and advances its revision.
805    async fn save_session(&self, session: SessionRecord) -> SessionStoreResult<()>;
806
807    /// Load a session record.
808    async fn load_session(&self, session_id: &SessionId) -> SessionStoreResult<SessionRecord>;
809
810    /// List sessions by optional filter.
811    async fn list_sessions(&self, filter: SessionFilter) -> SessionStoreResult<Vec<SessionRecord>>;
812
813    /// Return one storage-bounded stable keyset page of sessions.
814    async fn list_session_page(&self, query: SessionPageQuery) -> SessionStoreResult<SessionPage>;
815
816    /// Update session status.
817    async fn update_session_status(
818        &self,
819        session_id: &SessionId,
820        status: SessionStatus,
821    ) -> SessionStoreResult<()>;
822
823    /// Save a context state snapshot for a session.
824    async fn save_context_state(
825        &self,
826        session_id: &SessionId,
827        state: ResumableState,
828    ) -> SessionStoreResult<()>;
829
830    /// Save an environment state reference for a session.
831    async fn save_environment_state(
832        &self,
833        session_id: &SessionId,
834        environment_state: EnvironmentStateRef,
835    ) -> SessionStoreResult<()>;
836
837    /// Append or replace a run record.
838    ///
839    /// A zero `sequence_no` requests atomic session-local allocation. Replacing an existing run
840    /// must preserve its assigned sequence; an explicit attempt to change that sequence fails.
841    async fn append_run(&self, run: RunRecord) -> SessionStoreResult<()>;
842
843    /// Load a run record.
844    async fn load_run(
845        &self,
846        session_id: &SessionId,
847        run_id: &RunId,
848    ) -> SessionStoreResult<RunRecord>;
849
850    /// List runs for a session.
851    async fn list_runs(&self, session_id: &SessionId) -> SessionStoreResult<Vec<RunRecord>>;
852
853    /// Update run status and optional output preview through the legacy low-level path.
854    ///
855    /// Implementations synthesize a generic durable diagnostic for failed and cancelled writes.
856    /// Admission-owned terminal transitions must use [`Self::finalize_run_admission`].
857    async fn update_run_status(
858        &self,
859        session_id: &SessionId,
860        run_id: &RunId,
861        status: RunStatus,
862        output_preview: Option<String>,
863    ) -> SessionStoreResult<()>;
864
865    /// Append a full runtime checkpoint.
866    async fn append_checkpoint(
867        &self,
868        session_id: &SessionId,
869        checkpoint: AgentCheckpoint,
870    ) -> SessionStoreResult<()>;
871
872    /// Load checkpoints for a run in insertion order.
873    async fn load_checkpoints(
874        &self,
875        session_id: &SessionId,
876        run_id: &RunId,
877    ) -> SessionStoreResult<Vec<AgentCheckpoint>>;
878
879    /// Load the latest checkpoint for a run.
880    async fn latest_checkpoint(
881        &self,
882        session_id: &SessionId,
883        run_id: &RunId,
884    ) -> SessionStoreResult<Option<AgentCheckpoint>> {
885        let checkpoints = self.load_checkpoints(session_id, run_id).await?;
886        Ok(checkpoints.into_iter().last())
887    }
888
889    /// Append runtime stream records used as resume evidence.
890    async fn append_stream_records(
891        &self,
892        session_id: &SessionId,
893        run_id: &RunId,
894        records: Vec<AgentStreamRecord>,
895    ) -> SessionStoreResult<()>;
896
897    /// Replay runtime stream records for a run.
898    async fn replay_stream_records(
899        &self,
900        session_id: &SessionId,
901        run_id: &RunId,
902    ) -> SessionStoreResult<Vec<AgentStreamRecord>>;
903
904    /// Replay runtime stream records after a sequence cursor.
905    async fn replay_stream_records_after(
906        &self,
907        session_id: &SessionId,
908        run_id: &RunId,
909        after_sequence: Option<usize>,
910    ) -> SessionStoreResult<Vec<AgentStreamRecord>> {
911        let records = self.replay_stream_records(session_id, run_id).await?;
912        Ok(records
913            .into_iter()
914            .filter(|record| after_sequence.is_none_or(|cursor| record.sequence > cursor))
915            .collect())
916    }
917
918    /// Store a stream cursor reference for a run and session.
919    async fn save_stream_cursor(
920        &self,
921        session_id: &SessionId,
922        run_id: &RunId,
923        cursor: StreamCursorRef,
924    ) -> SessionStoreResult<()>;
925
926    /// Append an approval record.
927    async fn append_approval(&self, approval: ApprovalRecord) -> SessionStoreResult<()>;
928
929    /// Load approval records for a run.
930    async fn load_approvals(
931        &self,
932        session_id: &SessionId,
933        run_id: &RunId,
934    ) -> SessionStoreResult<Vec<ApprovalRecord>>;
935
936    /// Append a deferred tool record.
937    async fn append_deferred_tool(&self, record: DeferredToolRecord) -> SessionStoreResult<()>;
938
939    /// Load deferred tool records for a run.
940    async fn load_deferred_tools(
941        &self,
942        session_id: &SessionId,
943        run_id: &RunId,
944    ) -> SessionStoreResult<Vec<DeferredToolRecord>>;
945
946    /// Load a resume snapshot from session, checkpoint, and stream evidence.
947    async fn resume_snapshot(
948        &self,
949        _session_id: &SessionId,
950        _run_id: &RunId,
951    ) -> SessionStoreResult<SessionResumeSnapshot> {
952        Err(SessionStoreError::Failed(
953            "session store does not support per-run resume snapshots".to_string(),
954        ))
955    }
956
957    /// Load and side-effect-free prepare one host-neutral continuation package.
958    ///
959    /// Implementations should normally rely on this default so every product applies the same
960    /// snapshot identity and waiting-HITL evidence validation before admission or claim changes.
961    async fn prepare_continuation(
962        &self,
963        session_id: &SessionId,
964        run_id: &RunId,
965        mode: crate::ContinuationPreparationMode,
966    ) -> SessionStoreResult<crate::PreparedContinuation> {
967        let snapshot = self.resume_snapshot(session_id, run_id).await?;
968        match mode {
969            crate::ContinuationPreparationMode::Ordinary => {
970                crate::PreparedContinuation::ordinary(snapshot)
971            }
972            crate::ContinuationPreparationMode::WaitingHitl => {
973                crate::PreparedContinuation::waiting_hitl(snapshot)
974            }
975        }
976        .map_err(|error| SessionStoreError::Conflict(error.to_string()))
977    }
978
979    /// Return compact run trace projection.
980    async fn compact_run_trace(
981        &self,
982        session_id: &SessionId,
983        run_id: &RunId,
984    ) -> SessionStoreResult<CompactRunTrace>;
985
986    /// Return compact session trace projection.
987    async fn compact_session_trace(
988        &self,
989        session_id: &SessionId,
990    ) -> SessionStoreResult<CompactSessionTrace>;
991}