Skip to main content

starweaver_session/
claim.rs

1//! Durable ownership claims for HITL continuation.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use serde_json::{Map, Value};
6use starweaver_core::{RunId, SessionId};
7
8/// Durable metadata key carrying a typed continuation-effect recovery projection.
9pub const CONTINUATION_EFFECT_METADATA_KEY: &str = "starweaver.continuation.effect";
10
11/// Durable resume-claim phase.
12#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
13#[serde(rename_all = "snake_case")]
14pub enum HitlResumeClaimState {
15    /// Ownership is acquired but no continuation admission exists yet.
16    Preflight,
17    /// A fenced continuation run was admitted, but no hook or tool may execute yet.
18    Admitted,
19    /// Continuation execution may have produced external effects; release is forbidden.
20    Started,
21}
22
23/// Result of atomically aborting a fenced HITL replacement before worker launch.
24#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
25#[serde(rename_all = "snake_case")]
26pub enum HitlResumeAbortOutcome {
27    /// The claim was still admitted, so no approved effect could have run and the source remains waiting.
28    AbortedBeforeEffect,
29    /// The claim was already started; callers must persist fail-closed related-run evidence instead.
30    EffectStarted,
31}
32
33/// Durable effect boundary reached by a continuation.
34#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
35#[serde(rename_all = "snake_case")]
36pub enum ContinuationEffectPhase {
37    /// The effect fence was crossed and an approved hook or tool could have run.
38    Started,
39}
40
41/// Durable outcome classification for a continuation effect boundary.
42#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
43#[serde(rename_all = "snake_case")]
44pub enum ContinuationEffectOutcome {
45    /// Host loss occurred after the effect fence; the approved effect may have happened.
46    Indeterminate,
47}
48
49/// Typed host-visible projection for a continuation whose effect outcome cannot be proven.
50#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
51#[serde(rename_all = "camelCase", deny_unknown_fields)]
52pub struct ContinuationEffectState {
53    /// Last durable effect boundary crossed by the continuation.
54    pub phase: ContinuationEffectPhase,
55    /// Outcome classification exposed for recovery decisions.
56    pub outcome: ContinuationEffectOutcome,
57}
58
59impl ContinuationEffectState {
60    /// Build the fail-closed projection for a started continuation interrupted by host loss.
61    #[must_use]
62    pub const fn indeterminate() -> Self {
63        Self {
64            phase: ContinuationEffectPhase::Started,
65            outcome: ContinuationEffectOutcome::Indeterminate,
66        }
67    }
68
69    /// Insert this projection into durable run metadata.
70    ///
71    /// # Errors
72    ///
73    /// Returns a serialization error when the projection cannot be represented as JSON.
74    pub fn insert_into(&self, metadata: &mut Map<String, Value>) -> Result<(), serde_json::Error> {
75        metadata.insert(
76            CONTINUATION_EFFECT_METADATA_KEY.to_string(),
77            serde_json::to_value(self)?,
78        );
79        Ok(())
80    }
81
82    /// Decode a typed effect projection from durable run metadata.
83    ///
84    /// # Errors
85    ///
86    /// Returns an error when present effect metadata is malformed.
87    pub fn from_metadata(metadata: &Map<String, Value>) -> Result<Option<Self>, serde_json::Error> {
88        metadata
89            .get(CONTINUATION_EFFECT_METADATA_KEY)
90            .map(|value| serde_json::from_value(value.clone()))
91            .transpose()
92    }
93}
94
95/// Exclusive durable claim acquired before a waiting run may execute a continuation.
96///
97/// Claims deliberately have no automatic expiry. A claimant may release a claim only before
98/// continuation admission. Admission advances it to `Admitted`; the store then atomically checks
99/// the live admission fence and advances it to `Started` immediately before any effect. After that
100/// transition the claim is consumed only with terminal source-run evidence. This fails closed
101/// after process loss instead of risking duplicate external tool effects.
102#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
103pub struct HitlResumeClaim {
104    /// Caller-generated unique ownership token.
105    pub claim_id: String,
106    /// Session containing the waiting run.
107    pub session_id: SessionId,
108    /// Waiting source run.
109    pub run_id: RunId,
110    /// Current claim phase.
111    pub state: HitlResumeClaimState,
112    /// Claim creation time.
113    pub created_at: DateTime<Utc>,
114}
115
116impl starweaver_core::VersionedRecord for HitlResumeClaim {
117    const SCHEMA: &'static str = "starweaver.session.hitl_resume_claim";
118}
119
120impl HitlResumeClaim {
121    /// Build a claim for one waiting run.
122    #[must_use]
123    pub const fn new(
124        claim_id: String,
125        session_id: SessionId,
126        run_id: RunId,
127        created_at: DateTime<Utc>,
128    ) -> Self {
129        Self {
130            claim_id,
131            session_id,
132            run_id,
133            state: HitlResumeClaimState::Preflight,
134            created_at,
135        }
136    }
137
138    /// Return whether this is a valid newly acquired preflight claim.
139    #[must_use]
140    pub fn is_valid_preflight(&self) -> bool {
141        self.state == HitlResumeClaimState::Preflight && !self.claim_id.trim().is_empty()
142    }
143}