Skip to main content

rightkit_agent_engine_protocol/
effect_settlement.rs

1use serde::{Deserialize, Serialize};
2
3/// Durable settlement state for an external effect attempt.
4///
5/// `InterruptedUnknown` is the fail-closed state for a process/network/external
6/// mutation that may have executed but whose result was not durably committed.
7/// Such work must be reconciled explicitly; automatic replay is unsafe.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
9#[serde(rename_all = "snake_case")]
10pub enum ExternalEffectSettlement {
11    NotStarted,
12    Running,
13    Succeeded,
14    Failed,
15    InterruptedUnknown,
16}
17
18impl ExternalEffectSettlement {
19    pub fn may_have_executed(self) -> bool {
20        matches!(
21            self,
22            Self::Running | Self::Succeeded | Self::Failed | Self::InterruptedUnknown
23        )
24    }
25
26    /// Whether recovery may automatically execute the same non-idempotent
27    /// effect again. Only a durably known `NotStarted` attempt is safe by
28    /// default; callers may layer a stronger idempotency contract separately.
29    pub fn allows_automatic_non_idempotent_replay(self) -> bool {
30        matches!(self, Self::NotStarted)
31    }
32
33    pub fn requires_reconciliation(self) -> bool {
34        matches!(self, Self::InterruptedUnknown)
35    }
36}
37
38#[cfg(test)]
39mod tests {
40    use super::*;
41
42    #[test]
43    fn interrupted_unknown_is_never_blindly_replayed() {
44        let state = ExternalEffectSettlement::InterruptedUnknown;
45        assert!(state.may_have_executed());
46        assert!(state.requires_reconciliation());
47        assert!(!state.allows_automatic_non_idempotent_replay());
48    }
49
50    #[test]
51    fn only_known_not_started_is_default_replay_safe() {
52        for state in [
53            ExternalEffectSettlement::Running,
54            ExternalEffectSettlement::Succeeded,
55            ExternalEffectSettlement::Failed,
56            ExternalEffectSettlement::InterruptedUnknown,
57        ] {
58            assert!(!state.allows_automatic_non_idempotent_replay());
59        }
60        assert!(ExternalEffectSettlement::NotStarted.allows_automatic_non_idempotent_replay());
61    }
62}