Skip to main content

vv_agent/runtime/
state_v2.rs

1//! Checkpoint v2 state and store contract.
2
3use std::collections::BTreeMap;
4
5use serde::{Deserialize, Serialize};
6use serde_json::{Map, Value};
7
8use crate::budget::BudgetUsageSnapshot;
9use crate::checkpoint::{
10    canonical_json_bytes, validate_checkpoint_key, validate_extension_namespace, validate_sha256,
11    CheckpointError, CheckpointResult, CheckpointStatus, ClaimMode, EventCursor, OperationKind,
12    OperationState, ToolIdempotency, MAX_EXTENSION_ENTRY_BYTES, MAX_WIRE_INTEGER,
13};
14use crate::types::{CycleRecord, Message};
15
16mod transitions;
17mod validation;
18
19pub use transitions::*;
20pub use validation::*;
21use validation::{optional_string, required_string, required_u64, validate_json};
22
23pub const CHECKPOINT_V2_SCHEMA: &str = crate::checkpoint::CHECKPOINT_V2_SCHEMA;
24pub const RUN_DEFINITION_SCHEMA: &str = crate::checkpoint::RUN_DEFINITION_SCHEMA;
25
26#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub struct OperationError {
28    pub code: String,
29    pub message: String,
30    #[serde(default)]
31    pub retryable: bool,
32}
33
34impl OperationError {
35    pub fn new(code: impl Into<String>, message: impl Into<String>, retryable: bool) -> Self {
36        Self {
37            code: code.into(),
38            message: message.into(),
39            retryable,
40        }
41    }
42
43    pub fn validate(&self) -> CheckpointResult<()> {
44        if self.code.trim().is_empty() || self.message.trim().is_empty() {
45            return Err(CheckpointError::new(
46                "operation_error_invalid",
47                "operation error code and message must be non-empty",
48            ));
49        }
50        Ok(())
51    }
52
53    pub fn to_value(&self) -> Value {
54        serde_json::json!({
55            "code": self.code,
56            "message": self.message,
57            "retryable": self.retryable,
58        })
59    }
60
61    pub fn from_value(value: &Value) -> CheckpointResult<Self> {
62        let object = value.as_object().ok_or_else(|| {
63            CheckpointError::new(
64                "operation_error_invalid",
65                "operation error must be an object",
66            )
67        })?;
68        let error = Self {
69            code: required_string(object, "code", "operation_error_invalid")?.to_string(),
70            message: required_string(object, "message", "operation_error_invalid")?.to_string(),
71            retryable: object
72                .get("retryable")
73                .and_then(Value::as_bool)
74                .ok_or_else(|| {
75                    CheckpointError::new(
76                        "operation_error_invalid",
77                        "operation error retryable must be a boolean",
78                    )
79                })?,
80        };
81        error.validate()?;
82        Ok(error)
83    }
84}
85
86#[derive(Debug, Clone, PartialEq)]
87pub struct OperationJournalEntry {
88    pub kind: OperationKind,
89    pub operation_id: String,
90    pub cycle_index: u64,
91    pub attempt: u64,
92    pub state: OperationState,
93    pub request_digest: String,
94    pub idempotency_key: Option<String>,
95    pub response: Option<Value>,
96    pub error: Option<OperationError>,
97    pub tool_call_id: Option<String>,
98    pub tool_name: Option<String>,
99    pub arguments: Option<Map<String, Value>>,
100    pub idempotency_support: Option<ToolIdempotency>,
101    pub result: Option<Value>,
102}
103
104impl OperationJournalEntry {
105    pub fn model(
106        operation_id: impl Into<String>,
107        cycle_index: u64,
108        attempt: u64,
109        request_digest: impl Into<String>,
110        idempotency_key: Option<String>,
111    ) -> Self {
112        Self {
113            kind: OperationKind::Model,
114            operation_id: operation_id.into(),
115            cycle_index,
116            attempt,
117            state: OperationState::Planned,
118            request_digest: request_digest.into(),
119            idempotency_key,
120            response: None,
121            error: None,
122            tool_call_id: None,
123            tool_name: None,
124            arguments: None,
125            idempotency_support: None,
126            result: None,
127        }
128    }
129
130    #[allow(clippy::too_many_arguments)]
131    pub fn tool(
132        operation_id: impl Into<String>,
133        cycle_index: u64,
134        attempt: u64,
135        request_digest: impl Into<String>,
136        tool_call_id: impl Into<String>,
137        tool_name: impl Into<String>,
138        arguments: Map<String, Value>,
139        idempotency_key: impl Into<String>,
140        idempotency_support: ToolIdempotency,
141    ) -> Self {
142        Self {
143            kind: OperationKind::Tool,
144            operation_id: operation_id.into(),
145            cycle_index,
146            attempt,
147            state: OperationState::Planned,
148            request_digest: request_digest.into(),
149            idempotency_key: Some(idempotency_key.into()),
150            response: None,
151            error: None,
152            tool_call_id: Some(tool_call_id.into()),
153            tool_name: Some(tool_name.into()),
154            arguments: Some(arguments),
155            idempotency_support: Some(idempotency_support),
156            result: None,
157        }
158    }
159
160    pub fn validate(&self) -> CheckpointResult<()> {
161        if self.operation_id.trim().is_empty() {
162            return Err(CheckpointError::new(
163                "operation_id_invalid",
164                "operation_id must be non-empty",
165            ));
166        }
167        if self.cycle_index == 0 || self.cycle_index > MAX_WIRE_INTEGER {
168            return Err(CheckpointError::new(
169                "operation_cycle_invalid",
170                "operation cycle_index must be positive and JSON-safe",
171            ));
172        }
173        if self.attempt == 0 || self.attempt > MAX_WIRE_INTEGER {
174            return Err(CheckpointError::new(
175                "operation_attempt_invalid",
176                "operation attempt must be positive and JSON-safe",
177            ));
178        }
179        validate_sha256(&self.request_digest, "operation request_digest").map_err(|_| {
180            CheckpointError::new(
181                "operation_request_digest_invalid",
182                "operation request_digest must be lowercase SHA-256",
183            )
184        })?;
185        if let Some(response) = &self.response {
186            validate_json(response, "operation response")?;
187        }
188        if let Some(result) = &self.result {
189            validate_json(result, "operation result")?;
190        }
191        if let Some(error) = &self.error {
192            error.validate()?;
193        }
194        match self.kind {
195            OperationKind::Model => {
196                if self.tool_call_id.is_some()
197                    || self.tool_name.is_some()
198                    || self.arguments.is_some()
199                    || self.idempotency_support.is_some()
200                    || self.result.is_some()
201                {
202                    return Err(CheckpointError::new(
203                        "operation_kind_fields_invalid",
204                        "model journal entries cannot contain tool fields",
205                    ));
206                }
207            }
208            OperationKind::Tool => {
209                if self.tool_call_id.as_deref().is_none_or(str::is_empty)
210                    || self.tool_name.as_deref().is_none_or(str::is_empty)
211                    || self.idempotency_key.as_deref().is_none_or(str::is_empty)
212                    || self.arguments.is_none()
213                    || self.idempotency_support.is_none()
214                {
215                    return Err(CheckpointError::new(
216                        "tool_idempotency_key_required",
217                        "tool journal entries require call, arguments, idempotency key, and support",
218                    ));
219                }
220                if self.response.is_some() {
221                    return Err(CheckpointError::new(
222                        "operation_kind_fields_invalid",
223                        "tool journal entries cannot contain model responses",
224                    ));
225                }
226                validate_json(
227                    &Value::Object(self.arguments.clone().expect("checked above")),
228                    "tool journal arguments",
229                )?;
230            }
231        }
232        match self.state {
233            OperationState::Succeeded => {
234                let receipt = match self.kind {
235                    OperationKind::Model => self.response.is_some(),
236                    OperationKind::Tool => self.result.is_some(),
237                };
238                if !receipt || self.error.is_some() {
239                    return Err(CheckpointError::new(
240                        "operation_receipt_required",
241                        "succeeded operation requires one success receipt",
242                    ));
243                }
244            }
245            OperationState::Failed => {
246                if self.error.is_none() || self.response.is_some() || self.result.is_some() {
247                    return Err(CheckpointError::new(
248                        "operation_error_required",
249                        "failed operation requires one typed error",
250                    ));
251                }
252            }
253            OperationState::Planned | OperationState::Started | OperationState::Ambiguous => {
254                if self.response.is_some() || self.result.is_some() || self.error.is_some() {
255                    return Err(CheckpointError::new(
256                        "operation_receipt_unexpected",
257                        "non-terminal operation cannot contain a receipt",
258                    ));
259                }
260            }
261        }
262        Ok(())
263    }
264
265    pub fn to_value(&self) -> Value {
266        let mut object = Map::new();
267        object.insert("kind".to_string(), serde_json::json!(self.kind));
268        object.insert(
269            "operation_id".to_string(),
270            Value::String(self.operation_id.clone()),
271        );
272        object.insert("cycle_index".to_string(), Value::from(self.cycle_index));
273        object.insert("attempt".to_string(), Value::from(self.attempt));
274        object.insert("state".to_string(), serde_json::json!(self.state));
275        object.insert(
276            "request_digest".to_string(),
277            Value::String(self.request_digest.clone()),
278        );
279        object.insert(
280            "idempotency_key".to_string(),
281            self.idempotency_key
282                .clone()
283                .map_or(Value::Null, Value::String),
284        );
285        match self.kind {
286            OperationKind::Model => {
287                object.insert(
288                    "response".to_string(),
289                    self.response.clone().unwrap_or(Value::Null),
290                );
291            }
292            OperationKind::Tool => {
293                object.insert(
294                    "tool_call_id".to_string(),
295                    self.tool_call_id.clone().map_or(Value::Null, Value::String),
296                );
297                object.insert(
298                    "tool_name".to_string(),
299                    self.tool_name.clone().map_or(Value::Null, Value::String),
300                );
301                object.insert(
302                    "arguments".to_string(),
303                    self.arguments.clone().map_or(Value::Null, Value::Object),
304                );
305                object.insert(
306                    "idempotency_support".to_string(),
307                    self.idempotency_support
308                        .map_or(Value::Null, |support| serde_json::json!(support)),
309                );
310                object.insert(
311                    "result".to_string(),
312                    self.result.clone().unwrap_or(Value::Null),
313                );
314            }
315        }
316        object.insert(
317            "error".to_string(),
318            self.error
319                .as_ref()
320                .map(OperationError::to_value)
321                .unwrap_or(Value::Null),
322        );
323        Value::Object(object)
324    }
325
326    pub fn from_value(value: &Value) -> CheckpointResult<Self> {
327        let object = value.as_object().ok_or_else(|| {
328            CheckpointError::new(
329                "operation_journal_invalid",
330                "operation journal entry must be an object",
331            )
332        })?;
333        let kind: OperationKind =
334            serde_json::from_value(object.get("kind").cloned().ok_or_else(|| {
335                CheckpointError::new("operation_journal_invalid", "kind missing")
336            })?)
337            .map_err(|_| {
338                CheckpointError::new("operation_journal_invalid", "unknown operation kind")
339            })?;
340        let state: OperationState =
341            serde_json::from_value(object.get("state").cloned().ok_or_else(|| {
342                CheckpointError::new("operation_journal_invalid", "state missing")
343            })?)
344            .map_err(|_| {
345                CheckpointError::new("operation_journal_invalid", "unknown operation state")
346            })?;
347        let entry = Self {
348            kind,
349            operation_id: required_string(object, "operation_id", "operation_journal_invalid")?
350                .to_string(),
351            cycle_index: required_u64(object, "cycle_index", "operation_cycle_invalid")?,
352            attempt: required_u64(object, "attempt", "operation_attempt_invalid")?,
353            state,
354            request_digest: required_string(
355                object,
356                "request_digest",
357                "operation_request_digest_invalid",
358            )?
359            .to_string(),
360            idempotency_key: optional_string(object, "idempotency_key")?,
361            response: object
362                .get("response")
363                .filter(|value| !value.is_null())
364                .cloned(),
365            error: object
366                .get("error")
367                .filter(|value| !value.is_null())
368                .map(OperationError::from_value)
369                .transpose()?,
370            tool_call_id: optional_string(object, "tool_call_id")?,
371            tool_name: optional_string(object, "tool_name")?,
372            arguments: object
373                .get("arguments")
374                .filter(|value| !value.is_null())
375                .map(|value| {
376                    value.as_object().cloned().ok_or_else(|| {
377                        CheckpointError::new(
378                            "operation_kind_fields_invalid",
379                            "tool arguments must be an object",
380                        )
381                    })
382                })
383                .transpose()?,
384            idempotency_support: object
385                .get("idempotency_support")
386                .filter(|value| !value.is_null())
387                .cloned()
388                .map(|value| {
389                    serde_json::from_value(value).map_err(|_| {
390                        CheckpointError::new(
391                            "operation_kind_fields_invalid",
392                            "invalid tool idempotency support",
393                        )
394                    })
395                })
396                .transpose()?,
397            result: object
398                .get("result")
399                .filter(|value| !value.is_null())
400                .cloned(),
401        };
402        entry.validate()?;
403        Ok(entry)
404    }
405
406    pub fn transition_to(&mut self, next: OperationState) -> CheckpointResult<()> {
407        let allowed = matches!(
408            (self.state, next),
409            (OperationState::Planned, OperationState::Failed)
410                | (OperationState::Planned, OperationState::Started)
411                | (OperationState::Started, OperationState::Succeeded)
412                | (OperationState::Started, OperationState::Failed)
413                | (OperationState::Started, OperationState::Ambiguous)
414                | (OperationState::Ambiguous, OperationState::Planned)
415                | (OperationState::Ambiguous, OperationState::Succeeded)
416                | (OperationState::Ambiguous, OperationState::Failed)
417        );
418        if !allowed {
419            return Err(CheckpointError::new(
420                "operation_transition_invalid",
421                format!("cannot transition {:?} to {:?}", self.state, next),
422            ));
423        }
424        self.state = next;
425        self.validate()
426    }
427
428    pub fn retry(&mut self) -> CheckpointResult<()> {
429        if self.state != OperationState::Ambiguous {
430            return Err(CheckpointError::new(
431                "operation_transition_invalid",
432                "only ambiguous operations can be retried",
433            ));
434        }
435        self.attempt = self
436            .attempt
437            .checked_add(1)
438            .ok_or_else(|| CheckpointError::new("operation_attempt_invalid", "attempt overflow"))?;
439        self.state = OperationState::Planned;
440        self.validate()
441    }
442
443    pub fn mark_ambiguous(&mut self) -> CheckpointResult<()> {
444        self.transition_to(OperationState::Ambiguous)
445    }
446
447    pub fn verify_request(&self, request: &Value) -> CheckpointResult<()> {
448        let digest = crate::checkpoint::operation_request_digest(self.kind, request)?;
449        if digest != self.request_digest {
450            return Err(CheckpointError::new(
451                "checkpoint_journal_integrity_mismatch",
452                "operation request does not match the durable request_digest",
453            ));
454        }
455        Ok(())
456    }
457}
458
459#[derive(Debug, Clone, PartialEq)]
460pub struct ExtensionStateEntry {
461    pub version: String,
462    pub required: bool,
463    pub state: Value,
464}
465
466impl ExtensionStateEntry {
467    pub fn validate(&self) -> CheckpointResult<()> {
468        if self.version.trim().is_empty() {
469            return Err(CheckpointError::new(
470                "checkpoint_extension_state_invalid",
471                "extension version must be non-empty",
472            ));
473        }
474        validate_json(&self.state, "extension state")
475    }
476
477    pub fn to_value(&self) -> Value {
478        serde_json::json!({
479            "version": self.version,
480            "required": self.required,
481            "state": self.state,
482        })
483    }
484
485    pub fn from_value(value: &Value) -> CheckpointResult<Self> {
486        let object = value.as_object().ok_or_else(|| {
487            CheckpointError::new(
488                "checkpoint_extension_state_invalid",
489                "extension state entry must be an object",
490            )
491        })?;
492        let entry = Self {
493            version: required_string(object, "version", "checkpoint_extension_state_invalid")?
494                .to_string(),
495            required: object
496                .get("required")
497                .and_then(Value::as_bool)
498                .ok_or_else(|| {
499                    CheckpointError::new(
500                        "checkpoint_extension_state_invalid",
501                        "extension required must be a boolean",
502                    )
503                })?,
504            state: object.get("state").cloned().ok_or_else(|| {
505                CheckpointError::new(
506                    "checkpoint_extension_state_invalid",
507                    "extension state is required",
508                )
509            })?,
510        };
511        entry.validate()?;
512        Ok(entry)
513    }
514}
515
516#[derive(Debug, Clone, PartialEq)]
517pub struct EventOutboxEntry {
518    pub event_id: String,
519    pub payload_digest: String,
520    pub state: String,
521    pub event: Value,
522    pub cursor: Option<Value>,
523}
524
525impl EventOutboxEntry {
526    pub fn validate(&self) -> CheckpointResult<()> {
527        if self.event_id.trim().is_empty() {
528            return Err(CheckpointError::new(
529                "checkpoint_event_outbox_invalid",
530                "event_id must be non-empty",
531            ));
532        }
533        validate_sha256(&self.payload_digest, "event_outbox.payload_digest")?;
534        if self.state != "pending" && self.state != "delivered" {
535            return Err(CheckpointError::new(
536                "checkpoint_event_outbox_invalid",
537                "outbox state must be pending or delivered",
538            ));
539        }
540        if !self.event.is_object() {
541            return Err(CheckpointError::new(
542                "checkpoint_event_outbox_invalid",
543                "outbox event must be an object",
544            ));
545        }
546        if self.state == "pending" && self.cursor.is_some()
547            || self.state == "delivered" && self.cursor.is_none()
548        {
549            return Err(CheckpointError::new(
550                "checkpoint_event_outbox_invalid",
551                "pending entries have no cursor and delivered entries have one",
552            ));
553        }
554        if let Some(cursor) = &self.cursor {
555            validate_json(cursor, "event outbox cursor")?;
556        }
557        Ok(())
558    }
559
560    /// Verify the identity before delivering an outbox entry. A codec read
561    /// intentionally only validates the wire shape so older producers with a
562    /// deferred receipt can still be inspected and migrated explicitly.
563    pub fn verify_payload(&self) -> CheckpointResult<()> {
564        self.validate()?;
565        let digest = crate::checkpoint::event_payload_digest(&self.event)?;
566        if digest != self.payload_digest {
567            return Err(CheckpointError::new(
568                "event_identity_conflict",
569                "outbox payload digest does not match event",
570            ));
571        }
572        Ok(())
573    }
574
575    pub fn pending(event_id: impl Into<String>, event: Value) -> CheckpointResult<Self> {
576        let payload_digest = crate::checkpoint::event_payload_digest(&event)?;
577        let entry = Self {
578            event_id: event_id.into(),
579            payload_digest,
580            state: "pending".to_string(),
581            event,
582            cursor: None,
583        };
584        entry.validate()?;
585        Ok(entry)
586    }
587
588    pub fn to_value(&self) -> Value {
589        serde_json::json!({
590            "event_id": self.event_id,
591            "payload_digest": self.payload_digest,
592            "state": self.state,
593            "event": self.event,
594            "cursor": self.cursor,
595        })
596    }
597
598    pub fn from_value(value: &Value) -> CheckpointResult<Self> {
599        let object = value.as_object().ok_or_else(|| {
600            CheckpointError::new(
601                "checkpoint_event_outbox_invalid",
602                "outbox entry must be an object",
603            )
604        })?;
605        let entry = Self {
606            event_id: required_string(object, "event_id", "checkpoint_event_outbox_invalid")?
607                .to_string(),
608            payload_digest: required_string(
609                object,
610                "payload_digest",
611                "checkpoint_event_outbox_invalid",
612            )?
613            .to_string(),
614            state: required_string(object, "state", "checkpoint_event_outbox_invalid")?.to_string(),
615            event: object.get("event").cloned().ok_or_else(|| {
616                CheckpointError::new(
617                    "checkpoint_event_outbox_invalid",
618                    "outbox event is required",
619                )
620            })?,
621            cursor: object
622                .get("cursor")
623                .filter(|value| !value.is_null())
624                .cloned(),
625        };
626        entry.validate()?;
627        Ok(entry)
628    }
629}
630
631#[derive(Debug, Clone, PartialEq)]
632pub struct CheckpointV2 {
633    pub schema_version: String,
634    pub run_definition_schema: String,
635    pub run_definition: Value,
636    pub checkpoint_key: String,
637    pub task_id: String,
638    pub root_run_id: String,
639    pub trace_id: String,
640    pub run_definition_digest: String,
641    pub resume_attempt: u64,
642    pub cycle_index: u64,
643    pub status: CheckpointStatus,
644    pub messages: Vec<Message>,
645    pub cycles: Vec<CycleRecord>,
646    pub shared_state: BTreeMap<String, Value>,
647    pub budget_usage: Option<BudgetUsageSnapshot>,
648    pub event_cursor: Option<EventCursor>,
649    pub event_outbox: Vec<EventOutboxEntry>,
650    pub extension_state: BTreeMap<String, ExtensionStateEntry>,
651    pub model_call_journal: Vec<OperationJournalEntry>,
652    pub tool_journal: Vec<OperationJournalEntry>,
653    pub revision: u64,
654    pub claim_token: Option<String>,
655    pub claimed_cycle: Option<u64>,
656    pub lease_expires_at_ms: Option<u64>,
657    pub terminal_result: Option<Value>,
658    pub terminal_acknowledged: bool,
659    pub unknown_fields: BTreeMap<String, Value>,
660}
661
662impl Default for CheckpointV2 {
663    fn default() -> Self {
664        Self {
665            schema_version: CHECKPOINT_V2_SCHEMA.to_string(),
666            run_definition_schema: RUN_DEFINITION_SCHEMA.to_string(),
667            run_definition: Value::Object(Map::new()),
668            checkpoint_key: String::new(),
669            task_id: String::new(),
670            root_run_id: String::new(),
671            trace_id: String::new(),
672            run_definition_digest: String::new(),
673            resume_attempt: 1,
674            cycle_index: 0,
675            status: CheckpointStatus::Running,
676            messages: Vec::new(),
677            cycles: Vec::new(),
678            shared_state: BTreeMap::new(),
679            budget_usage: None,
680            event_cursor: None,
681            event_outbox: Vec::new(),
682            extension_state: BTreeMap::new(),
683            model_call_journal: Vec::new(),
684            tool_journal: Vec::new(),
685            revision: 0,
686            claim_token: None,
687            claimed_cycle: None,
688            lease_expires_at_ms: None,
689            terminal_result: None,
690            terminal_acknowledged: false,
691            unknown_fields: BTreeMap::new(),
692        }
693    }
694}
695
696impl CheckpointV2 {
697    pub fn validate(&self) -> CheckpointResult<()> {
698        validate_checkpoint_v2(self)
699    }
700
701    pub fn active_cycle(&self) -> CheckpointResult<u64> {
702        self.claimed_cycle
703            .or_else(|| self.cycle_index.checked_add(1))
704            .ok_or_else(|| {
705                CheckpointError::new("checkpoint_cycle_invalid", "active cycle overflow")
706            })
707            .and_then(|cycle| {
708                if cycle == 0 || cycle > MAX_WIRE_INTEGER {
709                    Err(CheckpointError::new(
710                        "checkpoint_cycle_invalid",
711                        "active cycle is outside the JSON-safe range",
712                    ))
713                } else {
714                    Ok(cycle)
715                }
716            })
717    }
718
719    pub fn has_ambiguous_operation(&self) -> bool {
720        self.model_call_journal
721            .iter()
722            .chain(self.tool_journal.iter())
723            .any(|entry| entry.state == OperationState::Ambiguous)
724    }
725
726    pub fn is_operator_abort_terminal(&self) -> bool {
727        let Some(result) = self.terminal_result.as_ref().and_then(Value::as_object) else {
728            return false;
729        };
730        result
731            .get("error_code")
732            .and_then(Value::as_str)
733            .is_some_and(|code| code == "operator_abort_with_unknown_outcome")
734            || result
735                .get("resume_observation")
736                .is_some_and(|value| !value.is_null())
737    }
738}
739
740pub trait CheckpointStoreV2: Send + Sync {
741    fn create_checkpoint_v2(&self, checkpoint: CheckpointV2) -> CheckpointResult<bool>;
742    fn load_checkpoint_v2(&self, checkpoint_key: &str) -> CheckpointResult<Option<CheckpointV2>>;
743    fn claim_checkpoint_v2(
744        &self,
745        checkpoint_key: &str,
746        cycle_index: u64,
747        claim_token: &str,
748        lease_expires_at_ms: u64,
749        now_ms: u64,
750        claim_mode: ClaimMode,
751    ) -> CheckpointResult<Option<CheckpointV2>>;
752    fn progress_checkpoint_v2(
753        &self,
754        checkpoint: CheckpointV2,
755        claim_token: &str,
756        expected_revision: u64,
757    ) -> CheckpointResult<bool>;
758    fn suspend_checkpoint_v2(
759        &self,
760        checkpoint: CheckpointV2,
761        claim_token: &str,
762        expected_revision: u64,
763    ) -> CheckpointResult<bool>;
764    fn commit_checkpoint_v2(
765        &self,
766        checkpoint: CheckpointV2,
767        claim_token: &str,
768        expected_revision: u64,
769    ) -> CheckpointResult<bool>;
770    fn finalize_claimed_v2(
771        &self,
772        checkpoint: CheckpointV2,
773        claim_token: &str,
774        expected_revision: u64,
775    ) -> CheckpointResult<bool>;
776    fn finalize_checkpoint_v2(
777        &self,
778        checkpoint: CheckpointV2,
779        expected_revision: u64,
780    ) -> CheckpointResult<bool>;
781    fn renew_checkpoint_claim_v2(
782        &self,
783        checkpoint_key: &str,
784        claim_token: &str,
785        lease_expires_at_ms: u64,
786        now_ms: u64,
787    ) -> CheckpointResult<bool>;
788    fn record_event_delivery_v2(
789        &self,
790        checkpoint_key: &str,
791        claim_token: Option<&str>,
792        expected_revision: u64,
793        event_id: &str,
794        payload_digest: &str,
795        cursor: EventCursor,
796    ) -> CheckpointResult<bool>;
797    fn acknowledge_terminal_v2(
798        &self,
799        checkpoint_key: &str,
800        expected_revision: u64,
801    ) -> CheckpointResult<bool>;
802    fn delete_checkpoint_v2(&self, checkpoint_key: &str) -> CheckpointResult<()>;
803    fn list_checkpoints_v2(&self) -> CheckpointResult<Vec<String>>;
804
805    fn claim_checkpoint_v2_continue(
806        &self,
807        checkpoint_key: &str,
808        cycle_index: u64,
809        claim_token: &str,
810        lease_expires_at_ms: u64,
811        now_ms: u64,
812    ) -> CheckpointResult<Option<CheckpointV2>> {
813        self.claim_checkpoint_v2(
814            checkpoint_key,
815            cycle_index,
816            claim_token,
817            lease_expires_at_ms,
818            now_ms,
819            ClaimMode::Continue,
820        )
821    }
822
823    fn claim_checkpoint_v2_recovery(
824        &self,
825        checkpoint_key: &str,
826        cycle_index: u64,
827        claim_token: &str,
828        lease_expires_at_ms: u64,
829        now_ms: u64,
830    ) -> CheckpointResult<Option<CheckpointV2>> {
831        self.claim_checkpoint_v2(
832            checkpoint_key,
833            cycle_index,
834            claim_token,
835            lease_expires_at_ms,
836            now_ms,
837            ClaimMode::Recovery,
838        )
839    }
840}