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}