Skip to main content

glass/browser/session/workflow/
checkpoint.rs

1use super::*;
2/// Bounded, deterministic workflow checkpoint. Input values and page content
3/// are intentionally excluded; only route identity and step state are kept.
4#[derive(Debug, Clone, Serialize, Deserialize)]
5#[serde(rename_all = "camelCase")]
6pub struct WorkflowCheckpoint {
7    pub schema_version: u8,
8    #[serde(default)]
9    pub run_id: String,
10    pub workflow_name: String,
11    pub workflow_version: String,
12    pub definition_hash: String,
13    pub status: WorkflowRunStatus,
14    pub next_step_index: usize,
15    pub steps: Vec<WorkflowCheckpointStep>,
16    pub page: WorkflowCheckpointPage,
17}
18
19/// Redacted state for one checkpointed workflow step.
20#[derive(Debug, Clone, Serialize, Deserialize)]
21#[serde(rename_all = "camelCase")]
22pub struct WorkflowCheckpointStep {
23    pub id: String,
24    pub state: WorkflowStepState,
25    pub attempts: u32,
26    #[serde(default, skip_serializing_if = "Vec::is_empty")]
27    pub history: Vec<WorkflowStepState>,
28    #[serde(default, skip_serializing_if = "Vec::is_empty")]
29    pub execution_ids: Vec<String>,
30    #[serde(default)]
31    pub dispatch_acknowledged: bool,
32    #[serde(default)]
33    pub effect_observed: bool,
34    #[serde(default)]
35    pub postcondition_verified: bool,
36    #[serde(default)]
37    pub retry_safe: bool,
38    #[serde(default, skip_serializing_if = "Option::is_none")]
39    pub previous_revision: Option<u64>,
40    #[serde(default, skip_serializing_if = "Option::is_none")]
41    pub current_revision: Option<u64>,
42    #[serde(default, skip_serializing_if = "Option::is_none")]
43    pub branch_decision: Option<WorkflowBranchDecision>,
44    #[serde(default, skip_serializing_if = "Option::is_none")]
45    pub intent_evidence: Option<WorkflowIntentEvidence>,
46}
47
48/// Bounded page identity used to reject unsafe resume attempts.
49#[derive(Debug, Clone, Serialize, Deserialize)]
50#[serde(rename_all = "camelCase")]
51pub struct WorkflowCheckpointPage {
52    pub target_id: String,
53    pub frame_id: String,
54    pub url: String,
55    pub title: String,
56    pub revision: u64,
57}
58
59/// Safe next action after a checkpoint has been reconciled with the live page.
60#[derive(Debug, Clone, Serialize, Deserialize)]
61#[serde(rename_all = "camelCase")]
62pub struct WorkflowResumePlan {
63    pub workflow_name: String,
64    pub workflow_version: String,
65    pub next_step_index: usize,
66    pub current_revision: u64,
67    pub reconciled: bool,
68}
69
70/// Reason a workflow checkpoint cannot be resumed safely.
71#[derive(Debug, Clone, Serialize)]
72#[serde(tag = "kind", rename_all = "snake_case")]
73pub enum WorkflowResumeError {
74    SchemaVersionMismatch {
75        expected: u8,
76        found: u8,
77    },
78    DefinitionMismatch,
79    RouteChanged,
80    InvalidState {
81        step_id: String,
82        state: WorkflowStepState,
83    },
84    CheckpointTooLarge,
85    CheckpointShape(String),
86}
87
88impl fmt::Display for WorkflowResumeError {
89    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
90        match self {
91            Self::SchemaVersionMismatch { expected, found } => {
92                write!(
93                    formatter,
94                    "workflow checkpoint schema mismatch: expected {expected}, found {found}"
95                )
96            }
97            Self::DefinitionMismatch => {
98                formatter.write_str("workflow definition does not match checkpoint")
99            }
100            Self::RouteChanged => {
101                formatter.write_str("workflow checkpoint route or target changed")
102            }
103            Self::InvalidState { step_id, state } => {
104                write!(
105                    formatter,
106                    "workflow step {step_id:?} cannot be resumed from {state:?}"
107                )
108            }
109            Self::CheckpointTooLarge => {
110                formatter.write_str("workflow checkpoint exceeds the 8 KiB limit")
111            }
112            Self::CheckpointShape(message) => {
113                write!(formatter, "invalid workflow checkpoint: {message}")
114            }
115        }
116    }
117}
118
119impl std::error::Error for WorkflowResumeError {}