1use 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 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}