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