Skip to main content

runtara_core/persistence/
mod.rs

1//! Persistence interfaces and backends for runtara-core.
2//!
3//! This module defines the persistence abstraction and backend implementations.
4
5pub mod postgres;
6pub mod sqlite;
7
8pub use self::postgres::PostgresPersistence;
9pub use self::sqlite::SqlitePersistence;
10
11use crate::error::CoreError;
12
13/// Instance record from the persistence layer.
14#[derive(Debug, Clone, sqlx::FromRow)]
15pub struct InstanceRecord {
16    /// Unique identifier for the instance.
17    pub instance_id: String,
18    /// Tenant identifier for multi-tenancy isolation.
19    pub tenant_id: String,
20    /// Version of the workflow definition.
21    pub definition_version: i32,
22    /// Current status (pending, running, suspended, completed, failed, cancelled).
23    pub status: String,
24    /// Last checkpoint ID if instance was checkpointed.
25    pub checkpoint_id: Option<String>,
26    /// Current attempt number (for retries).
27    pub attempt: i32,
28    /// Maximum allowed attempts before permanent failure.
29    pub max_attempts: i32,
30    /// When the instance was created.
31    pub created_at: DateTime<Utc>,
32    /// When the instance started running.
33    pub started_at: Option<DateTime<Utc>>,
34    /// When the instance finished (completed, failed, or cancelled).
35    pub finished_at: Option<DateTime<Utc>>,
36    /// Input data provided at launch time.
37    #[sqlx(default)]
38    pub input: Option<Vec<u8>>,
39    /// Output data from successful completion.
40    pub output: Option<Vec<u8>>,
41    /// Error message from failure.
42    pub error: Option<String>,
43    /// When a sleeping instance should be woken.
44    pub sleep_until: Option<DateTime<Utc>>,
45    /// How/why the instance reached its terminal state.
46    #[sqlx(default)]
47    pub termination_reason: Option<String>,
48    /// Process exit code if available.
49    #[sqlx(default)]
50    pub exit_code: Option<i32>,
51}
52
53/// Checkpoint record from the persistence layer.
54#[derive(Debug, Clone, sqlx::FromRow)]
55pub struct CheckpointRecord {
56    /// Database primary key.
57    pub id: i64,
58    /// Instance this checkpoint belongs to.
59    pub instance_id: String,
60    /// Unique checkpoint identifier within the instance.
61    pub checkpoint_id: String,
62    /// Serialized state data.
63    pub state: Vec<u8>,
64    /// When the checkpoint was created.
65    pub created_at: DateTime<Utc>,
66    /// Whether this checkpoint is marked for compensation (saga pattern).
67    #[sqlx(default)]
68    pub is_compensatable: bool,
69    /// Step ID to execute for compensation/rollback.
70    #[sqlx(default)]
71    pub compensation_step_id: Option<String>,
72    /// Serialized data for the compensation step.
73    #[sqlx(default)]
74    pub compensation_data: Option<Vec<u8>>,
75    /// Current state of compensation (none, pending, triggered, completed, failed).
76    #[sqlx(default)]
77    pub compensation_state: Option<String>,
78    /// Order in which to execute compensation (higher = compensate first).
79    #[sqlx(default)]
80    pub compensation_order: i32,
81}
82
83/// Event record from the persistence layer.
84#[derive(Debug, Clone, sqlx::FromRow)]
85pub struct EventRecord {
86    /// Database primary key (None when inserting new events).
87    #[sqlx(default)]
88    pub id: Option<i64>,
89    /// Instance this event belongs to.
90    pub instance_id: String,
91    /// Type of event (heartbeat, completed, failed, suspended, custom).
92    pub event_type: String,
93    /// Associated checkpoint ID if applicable.
94    pub checkpoint_id: Option<String>,
95    /// Optional event payload data.
96    pub payload: Option<Vec<u8>>,
97    /// When the event occurred.
98    pub created_at: DateTime<Utc>,
99    /// Arbitrary subtype for custom events.
100    pub subtype: Option<String>,
101}
102
103/// Signal record from the persistence layer.
104#[derive(Debug, Clone, sqlx::FromRow)]
105pub struct SignalRecord {
106    /// Instance this signal is for.
107    pub instance_id: String,
108    /// Type of signal (cancel, pause, resume).
109    pub signal_type: String,
110    /// Optional signal payload data.
111    pub payload: Option<Vec<u8>>,
112    /// When the signal was created.
113    pub created_at: DateTime<Utc>,
114    /// When the signal was acknowledged by the instance.
115    pub acknowledged_at: Option<DateTime<Utc>>,
116}
117
118/// Pending custom signal scoped to a specific checkpoint.
119#[derive(Debug, Clone, sqlx::FromRow)]
120pub struct CustomSignalRecord {
121    /// Instance this signal is for.
122    pub instance_id: String,
123    /// Target checkpoint/wait key.
124    pub checkpoint_id: String,
125    /// Optional payload.
126    pub payload: Option<Vec<u8>>,
127    /// When the signal was created.
128    pub created_at: DateTime<Utc>,
129}
130
131/// Sort order for event queries.
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
133pub enum EventSortOrder {
134    /// Newest events first (default).
135    #[default]
136    Desc,
137    /// Oldest events first.
138    Asc,
139}
140
141/// Filter options for listing events.
142#[derive(Debug, Clone, Default)]
143pub struct ListEventsFilter {
144    /// Filter by event type (e.g., "custom", "started", "completed").
145    pub event_type: Option<String>,
146    /// Filter by subtype (e.g., "step_debug_start", "step_debug_end", "workflow_log").
147    pub subtype: Option<String>,
148    /// Filter events created at or after this time.
149    pub created_after: Option<DateTime<Utc>>,
150    /// Filter events created before this time.
151    pub created_before: Option<DateTime<Utc>>,
152    /// Full-text search in JSON payload content.
153    pub payload_contains: Option<String>,
154    /// Filter by scope_id in the event payload (for hierarchy filtering).
155    /// When set, only events with matching scope_id in their payload are returned.
156    pub scope_id: Option<String>,
157    /// Filter by parent_scope_id in the event payload (for hierarchy filtering).
158    /// When set, only events with matching parent_scope_id in their payload are returned.
159    /// Use this to get direct children of a scope.
160    pub parent_scope_id: Option<String>,
161    /// When true, only return events that have no parent_scope_id (root-level scopes).
162    /// This is useful for getting top-level execution scopes.
163    pub root_scopes_only: bool,
164    /// Sort order for events by created_at.
165    pub sort_order: EventSortOrder,
166}
167
168// ============================================================================
169// Step Summary Types (for paired step_debug_start/end events)
170// ============================================================================
171
172/// Status of a step execution.
173#[derive(Debug, Clone, Copy, PartialEq, Eq)]
174pub enum StepStatus {
175    /// Step is currently running (has start event, no end event yet).
176    Running,
177    /// Step completed successfully.
178    Completed,
179    /// Step failed with an error.
180    Failed,
181}
182
183/// Summary of a step execution, pairing step_debug_start and step_debug_end events.
184#[derive(Debug, Clone)]
185pub struct StepSummaryRecord {
186    /// Unique step identifier within the instance.
187    pub step_id: String,
188    /// Human-readable step name.
189    pub step_name: Option<String>,
190    /// Step type (e.g., "Agent", "Conditional", "Split").
191    pub step_type: String,
192    /// Current status of the step.
193    pub status: StepStatus,
194    /// When the step started executing.
195    pub started_at: DateTime<Utc>,
196    /// When the step completed (None if still running).
197    pub completed_at: Option<DateTime<Utc>>,
198    /// Duration in milliseconds (None if still running).
199    pub duration_ms: Option<i64>,
200    /// Step inputs from step_debug_start payload.
201    pub inputs: Option<serde_json::Value>,
202    /// Step outputs from step_debug_end payload.
203    pub outputs: Option<serde_json::Value>,
204    /// Error details from step_debug_end payload (if failed).
205    pub error: Option<serde_json::Value>,
206    /// Scope ID for nested execution contexts (Split/While/StartScenario).
207    pub scope_id: Option<String>,
208    /// Parent scope ID for hierarchy.
209    pub parent_scope_id: Option<String>,
210}
211
212/// Filter options for listing step summaries.
213#[derive(Debug, Clone, Default)]
214pub struct ListStepSummariesFilter {
215    /// Sort order for steps by started_at.
216    pub sort_order: EventSortOrder,
217    /// Filter by step status.
218    pub status: Option<StepStatus>,
219    /// Filter by step type (e.g., "Agent", "Conditional").
220    pub step_type: Option<String>,
221    /// Filter by scope_id (for steps within a specific scope).
222    pub scope_id: Option<String>,
223    /// Filter by parent_scope_id (for direct children of a scope).
224    pub parent_scope_id: Option<String>,
225    /// When true, only return steps with no parent_scope_id (root-level steps).
226    pub root_scopes_only: bool,
227}
228
229/// Error history record for structured error tracking.
230#[derive(Debug, Clone, sqlx::FromRow)]
231pub struct ErrorHistoryRecord {
232    /// Database primary key.
233    pub id: i64,
234    /// Instance this error belongs to.
235    pub instance_id: String,
236    /// Checkpoint ID where error occurred (if applicable).
237    pub checkpoint_id: Option<String>,
238    /// Step ID where error occurred (if applicable).
239    pub step_id: Option<String>,
240    /// Machine-readable error code (e.g., "RATE_LIMITED").
241    pub error_code: String,
242    /// Human-readable error message.
243    pub error_message: String,
244    /// Error category (unknown, transient, permanent, business).
245    pub category: String,
246    /// Error severity (info, warning, error, critical).
247    pub severity: String,
248    /// Retry hint (unknown, retry_immediately, retry_with_backoff, retry_after, do_not_retry).
249    pub retry_hint: Option<String>,
250    /// Milliseconds for retry_after hint.
251    pub retry_after_ms: Option<i64>,
252    /// Additional context as JSON.
253    pub attributes: Option<serde_json::Value>,
254    /// Cause error ID for error chains.
255    pub cause_error_id: Option<i64>,
256    /// When the error was recorded.
257    pub created_at: DateTime<Utc>,
258}
259
260/// Compensation log record for audit trail.
261#[derive(Debug, Clone, sqlx::FromRow)]
262pub struct CompensationLogRecord {
263    /// Database primary key.
264    pub id: i64,
265    /// Instance this compensation is for.
266    pub instance_id: String,
267    /// Checkpoint being compensated.
268    pub checkpoint_id: String,
269    /// Step executed for compensation.
270    pub compensation_step_id: String,
271    /// Attempt number (for retries).
272    pub attempt_number: i32,
273    /// When compensation started.
274    pub started_at: DateTime<Utc>,
275    /// When compensation finished (None if still running).
276    pub finished_at: Option<DateTime<Utc>>,
277    /// Whether compensation succeeded.
278    pub success: Option<bool>,
279    /// Error message if compensation failed.
280    pub error_message: Option<String>,
281    /// Reference to error_history entry.
282    pub error_id: Option<i64>,
283}
284
285/// Wake queue entry from the persistence layer.
286#[derive(Debug, Clone, sqlx::FromRow)]
287pub struct WakeEntry {
288    /// Database primary key.
289    pub id: i64,
290    /// Instance to wake.
291    pub instance_id: String,
292    /// Checkpoint to resume from.
293    pub checkpoint_id: String,
294    /// When to wake the instance.
295    pub wake_at: DateTime<Utc>,
296    /// When this wake entry was created.
297    pub created_at: DateTime<Utc>,
298}
299use async_trait::async_trait;
300use chrono::{DateTime, Utc};
301
302/// Persistence interface used by core handlers.
303#[allow(missing_docs)]
304#[async_trait]
305pub trait Persistence: Send + Sync {
306    async fn register_instance(&self, instance_id: &str, tenant_id: &str) -> Result<(), CoreError>;
307
308    async fn get_instance(&self, instance_id: &str) -> Result<Option<InstanceRecord>, CoreError>;
309
310    async fn update_instance_status(
311        &self,
312        instance_id: &str,
313        status: &str,
314        started_at: Option<DateTime<Utc>>,
315    ) -> Result<(), CoreError>;
316
317    async fn update_instance_checkpoint(
318        &self,
319        instance_id: &str,
320        checkpoint_id: &str,
321    ) -> Result<(), CoreError>;
322
323    async fn complete_instance(
324        &self,
325        instance_id: &str,
326        output: Option<&[u8]>,
327        error: Option<&str>,
328    ) -> Result<(), CoreError>;
329
330    /// Complete an instance with extended fields (stderr, checkpoint).
331    ///
332    /// This is an extended version of `complete_instance` that supports additional
333    /// fields needed by the Environment layer. The default implementation calls
334    /// the basic `complete_instance` and ignores the extra fields.
335    ///
336    /// Environment implementations should override this to store stderr and checkpoint.
337    async fn complete_instance_extended(
338        &self,
339        instance_id: &str,
340        status: &str,
341        output: Option<&[u8]>,
342        error: Option<&str>,
343        _stderr: Option<&str>,
344        _checkpoint_id: Option<&str>,
345    ) -> Result<(), CoreError> {
346        // Default: delegate to basic complete_instance, ignoring stderr/checkpoint
347        let error_for_basic = if status == "failed" { error } else { None };
348        let output_for_basic = if status == "completed" { output } else { None };
349        self.complete_instance(instance_id, output_for_basic, error_for_basic)
350            .await
351    }
352
353    /// Complete an instance only if its current status is 'running'.
354    ///
355    /// This prevents race conditions where both Core (via SDK) and Environment
356    /// (via container monitor) try to complete the same instance. Returns true
357    /// if the update was applied, false if skipped.
358    ///
359    /// Default implementation always applies the update (no guard).
360    async fn complete_instance_if_running(
361        &self,
362        instance_id: &str,
363        status: &str,
364        output: Option<&[u8]>,
365        error: Option<&str>,
366        stderr: Option<&str>,
367        checkpoint_id: Option<&str>,
368    ) -> Result<bool, CoreError> {
369        // Default: always apply (returns true)
370        self.complete_instance_extended(instance_id, status, output, error, stderr, checkpoint_id)
371            .await?;
372        Ok(true)
373    }
374
375    /// Complete an instance with termination tracking metadata.
376    ///
377    /// This version includes `termination_reason` and `exit_code` for unambiguous
378    /// identification of how/why the instance terminated.
379    ///
380    /// Default implementation delegates to `complete_instance_extended`, ignoring
381    /// the termination fields.
382    #[allow(clippy::too_many_arguments)]
383    async fn complete_instance_with_termination(
384        &self,
385        instance_id: &str,
386        status: &str,
387        termination_reason: Option<&str>,
388        exit_code: Option<i32>,
389        output: Option<&[u8]>,
390        error: Option<&str>,
391        stderr: Option<&str>,
392        checkpoint_id: Option<&str>,
393    ) -> Result<(), CoreError> {
394        // Default: delegate to extended, ignoring termination fields
395        let _ = termination_reason;
396        let _ = exit_code;
397        self.complete_instance_extended(instance_id, status, output, error, stderr, checkpoint_id)
398            .await
399    }
400
401    /// Complete an instance with termination tracking, only if status is 'running'.
402    ///
403    /// Returns true if the update was applied, false if skipped (instance already
404    /// has a terminal status).
405    #[allow(clippy::too_many_arguments)]
406    async fn complete_instance_with_termination_if_running(
407        &self,
408        instance_id: &str,
409        status: &str,
410        termination_reason: Option<&str>,
411        exit_code: Option<i32>,
412        output: Option<&[u8]>,
413        error: Option<&str>,
414        stderr: Option<&str>,
415        checkpoint_id: Option<&str>,
416    ) -> Result<bool, CoreError> {
417        // Default: always apply (returns true)
418        self.complete_instance_with_termination(
419            instance_id,
420            status,
421            termination_reason,
422            exit_code,
423            output,
424            error,
425            stderr,
426            checkpoint_id,
427        )
428        .await?;
429        Ok(true)
430    }
431
432    /// Update execution metrics for an instance (memory, CPU usage).
433    ///
434    /// This is an environment-specific operation for storing cgroup metrics.
435    /// Core implementations can ignore this (default is no-op).
436    async fn update_instance_metrics(
437        &self,
438        _instance_id: &str,
439        _memory_peak_bytes: Option<u64>,
440        _cpu_usage_usec: Option<u64>,
441    ) -> Result<(), CoreError> {
442        // Default: no-op (Core doesn't track metrics)
443        Ok(())
444    }
445
446    /// Update instance stderr output.
447    ///
448    /// This is an environment-specific operation for storing container stderr.
449    /// Core implementations can ignore this (default is no-op).
450    async fn update_instance_stderr(
451        &self,
452        _instance_id: &str,
453        _stderr: &str,
454    ) -> Result<(), CoreError> {
455        // Default: no-op (Core doesn't track stderr)
456        Ok(())
457    }
458
459    /// Store input data for an instance.
460    ///
461    /// This is an environment-specific operation for storing instance input.
462    /// Core implementations can ignore this (default is no-op).
463    async fn store_instance_input(
464        &self,
465        _instance_id: &str,
466        _input: &[u8],
467    ) -> Result<(), CoreError> {
468        // Default: no-op (Core doesn't store input)
469        Ok(())
470    }
471
472    async fn save_checkpoint(
473        &self,
474        instance_id: &str,
475        checkpoint_id: &str,
476        state: &[u8],
477    ) -> Result<(), CoreError>;
478
479    async fn load_checkpoint(
480        &self,
481        instance_id: &str,
482        checkpoint_id: &str,
483    ) -> Result<Option<CheckpointRecord>, CoreError>;
484
485    async fn list_checkpoints(
486        &self,
487        instance_id: &str,
488        checkpoint_id: Option<&str>,
489        limit: i64,
490        offset: i64,
491        created_after: Option<DateTime<Utc>>,
492        created_before: Option<DateTime<Utc>>,
493    ) -> Result<Vec<CheckpointRecord>, CoreError>;
494
495    async fn count_checkpoints(
496        &self,
497        instance_id: &str,
498        checkpoint_id: Option<&str>,
499        created_after: Option<DateTime<Utc>>,
500        created_before: Option<DateTime<Utc>>,
501    ) -> Result<i64, CoreError>;
502
503    async fn insert_event(&self, event: &EventRecord) -> Result<(), CoreError>;
504
505    async fn insert_signal(
506        &self,
507        instance_id: &str,
508        signal_type: &str,
509        payload: &[u8],
510    ) -> Result<(), CoreError>;
511
512    async fn get_pending_signal(
513        &self,
514        instance_id: &str,
515    ) -> Result<Option<SignalRecord>, CoreError>;
516
517    async fn acknowledge_signal(&self, instance_id: &str) -> Result<(), CoreError>;
518
519    async fn insert_custom_signal(
520        &self,
521        instance_id: &str,
522        checkpoint_id: &str,
523        payload: &[u8],
524    ) -> Result<(), CoreError>;
525
526    async fn take_pending_custom_signal(
527        &self,
528        instance_id: &str,
529        checkpoint_id: &str,
530    ) -> Result<Option<CustomSignalRecord>, CoreError>;
531
532    async fn save_retry_attempt(
533        &self,
534        instance_id: &str,
535        checkpoint_id: &str,
536        attempt: i32,
537        error_message: Option<&str>,
538    ) -> Result<(), CoreError>;
539
540    async fn list_instances(
541        &self,
542        tenant_id: Option<&str>,
543        status: Option<&str>,
544        limit: i64,
545        offset: i64,
546    ) -> Result<Vec<InstanceRecord>, CoreError>;
547
548    async fn health_check_db(&self) -> Result<bool, CoreError>;
549
550    async fn count_active_instances(&self) -> Result<i64, CoreError>;
551
552    /// Set the sleep_until timestamp for an instance.
553    async fn set_instance_sleep(
554        &self,
555        instance_id: &str,
556        sleep_until: DateTime<Utc>,
557    ) -> Result<(), CoreError>;
558
559    /// Clear the sleep_until timestamp for an instance.
560    async fn clear_instance_sleep(&self, instance_id: &str) -> Result<(), CoreError>;
561
562    /// Get instances that are due to wake (sleep_until <= now).
563    async fn get_sleeping_instances_due(
564        &self,
565        limit: i64,
566    ) -> Result<Vec<InstanceRecord>, CoreError>;
567
568    /// List events for an instance with filtering and pagination.
569    ///
570    /// Events are returned in reverse chronological order (newest first).
571    async fn list_events(
572        &self,
573        instance_id: &str,
574        filter: &ListEventsFilter,
575        limit: i64,
576        offset: i64,
577    ) -> Result<Vec<EventRecord>, CoreError>;
578
579    /// Count events for an instance with filtering.
580    async fn count_events(
581        &self,
582        instance_id: &str,
583        filter: &ListEventsFilter,
584    ) -> Result<i64, CoreError>;
585
586    // ========================================================================
587    // Step Summaries (paired step_debug_start/end events)
588    // ========================================================================
589
590    /// List step summaries for an instance, pairing step_debug_start and step_debug_end events.
591    ///
592    /// Returns unified step execution records with status, timing, inputs/outputs.
593    /// Steps are matched by step_id within the same scope context.
594    async fn list_step_summaries(
595        &self,
596        instance_id: &str,
597        filter: &ListStepSummariesFilter,
598        limit: i64,
599        offset: i64,
600    ) -> Result<Vec<StepSummaryRecord>, CoreError>;
601
602    /// Count step summaries for an instance with filtering.
603    async fn count_step_summaries(
604        &self,
605        instance_id: &str,
606        filter: &ListStepSummariesFilter,
607    ) -> Result<i64, CoreError>;
608
609    // ========================================================================
610    // Structured Error Tracking (optional - default implementations no-op)
611    // ========================================================================
612
613    /// Record a structured error in the error history table.
614    ///
615    /// Returns the error ID for chaining or reference.
616    #[allow(clippy::too_many_arguments)]
617    async fn record_error(
618        &self,
619        _instance_id: &str,
620        _checkpoint_id: Option<&str>,
621        _step_id: Option<&str>,
622        _error_code: &str,
623        _error_message: &str,
624        _category: &str,
625        _severity: &str,
626        _retry_hint: Option<&str>,
627        _retry_after_ms: Option<i64>,
628        _attributes: Option<&serde_json::Value>,
629        _cause_error_id: Option<i64>,
630    ) -> Result<i64, CoreError> {
631        // Default: no-op, return 0
632        Ok(0)
633    }
634
635    /// Get the most recent error for an instance.
636    async fn get_last_error(
637        &self,
638        _instance_id: &str,
639    ) -> Result<Option<ErrorHistoryRecord>, CoreError> {
640        // Default: no-op
641        Ok(None)
642    }
643
644    /// List errors for an instance.
645    async fn list_errors(
646        &self,
647        _instance_id: &str,
648        _limit: i64,
649        _offset: i64,
650    ) -> Result<Vec<ErrorHistoryRecord>, CoreError> {
651        // Default: empty list
652        Ok(vec![])
653    }
654
655    // ========================================================================
656    // Compensation Framework (optional - default implementations no-op)
657    // ========================================================================
658
659    /// Mark a checkpoint as compensatable (for saga pattern).
660    async fn register_compensatable_checkpoint(
661        &self,
662        _instance_id: &str,
663        _checkpoint_id: &str,
664        _compensation_step_id: &str,
665        _compensation_data: Option<&[u8]>,
666        _compensation_order: i32,
667    ) -> Result<(), CoreError> {
668        // Default: no-op
669        Ok(())
670    }
671
672    /// Get all compensatable checkpoints for an instance (in reverse order).
673    async fn get_compensatable_checkpoints(
674        &self,
675        _instance_id: &str,
676    ) -> Result<Vec<CheckpointRecord>, CoreError> {
677        // Default: empty list
678        Ok(vec![])
679    }
680
681    /// Update the compensation state of a checkpoint.
682    async fn set_checkpoint_compensation_state(
683        &self,
684        _instance_id: &str,
685        _checkpoint_id: &str,
686        _state: &str,
687    ) -> Result<(), CoreError> {
688        // Default: no-op
689        Ok(())
690    }
691
692    /// Update the instance-level compensation state.
693    async fn set_instance_compensation_state(
694        &self,
695        _instance_id: &str,
696        _state: &str,
697        _reason: Option<&str>,
698    ) -> Result<(), CoreError> {
699        // Default: no-op
700        Ok(())
701    }
702
703    /// Log a compensation attempt.
704    async fn log_compensation_attempt(
705        &self,
706        _instance_id: &str,
707        _checkpoint_id: &str,
708        _compensation_step_id: &str,
709        _success: bool,
710        _error_message: Option<&str>,
711        _error_id: Option<i64>,
712    ) -> Result<(), CoreError> {
713        // Default: no-op
714        Ok(())
715    }
716
717    /// Count pending compensations for an instance.
718    async fn count_pending_compensations(&self, _instance_id: &str) -> Result<i64, CoreError> {
719        // Default: 0
720        Ok(0)
721    }
722
723    /// Check if all compensations for an instance succeeded.
724    async fn all_compensations_succeeded(&self, _instance_id: &str) -> Result<bool, CoreError> {
725        // Default: true (no compensations = all succeeded)
726        Ok(true)
727    }
728
729    // ========================================================================
730    // Data Retention / Cleanup (optional - default implementations no-op)
731    // ========================================================================
732
733    /// Get terminal instance IDs older than the specified timestamp.
734    ///
735    /// Only returns instances with terminal status: completed, failed, cancelled.
736    /// Returns instance IDs ordered by finished_at (oldest first) for batch processing.
737    async fn get_terminal_instances_older_than(
738        &self,
739        _older_than: DateTime<Utc>,
740        _limit: i64,
741    ) -> Result<Vec<String>, CoreError> {
742        // Default: empty list (no cleanup supported)
743        Ok(vec![])
744    }
745
746    /// Delete instances by their IDs.
747    ///
748    /// This deletes from the instances table; child tables with ON DELETE CASCADE
749    /// (checkpoints, events, signals, etc.) are automatically cleaned up.
750    ///
751    /// Environment implementations should override this to clean up environment-specific
752    /// tables (container_registry, container_status, etc.) before calling the parent.
753    ///
754    /// Returns the count of deleted instances.
755    async fn delete_instances_batch(&self, _instance_ids: &[String]) -> Result<u64, CoreError> {
756        // Default: no-op (no deletion supported)
757        Ok(0)
758    }
759}