summaryrefslogtreecommitdiff
path: root/makima/src/db/models.rs
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-02-01 01:31:04 +0000
committerGitHub <noreply@github.com>2026-02-01 01:31:04 +0000
commit65eebd078af712d004a5a9e28863a16df30792a6 (patch)
tree3a9457f8e2bcfb0a85a7177d55686ec41bebcf89 /makima/src/db/models.rs
parent15d680a8a3c22be03a8faacd7bd43214e62a37f4 (diff)
parent5055b3f06d8027870b64abd84d9d3875070372e0 (diff)
downloadsoryu-65eebd078af712d004a5a9e28863a16df30792a6.tar.gz
soryu-65eebd078af712d004a5a9e28863a16df30792a6.zip
Merge pull request #55 from soryu-co/makima/contract-management-phase3
feat: Implement Phase 3 - Supervisor Resilience and State Management
Diffstat (limited to 'makima/src/db/models.rs')
-rw-r--r--makima/src/db/models.rs379
1 files changed, 379 insertions, 0 deletions
diff --git a/makima/src/db/models.rs b/makima/src/db/models.rs
index 636d81a..abdcce6 100644
--- a/makima/src/db/models.rs
+++ b/makima/src/db/models.rs
@@ -1971,6 +1971,50 @@ pub struct SupervisorState {
pub last_activity: DateTime<Utc>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
+ /// Current supervisor state (initializing, idle, working, waiting_for_user, etc.)
+ pub state: String,
+ /// Human-readable description of current activity
+ pub current_activity: Option<String>,
+ /// Progress percentage (0-100)
+ pub progress: i32,
+ /// Error message when state is failed or blocked
+ pub error_message: Option<String>,
+ /// Tasks spawned by this supervisor
+ #[sqlx(try_from = "Vec<Uuid>")]
+ pub spawned_task_ids: Vec<Uuid>,
+ /// Pending questions awaiting user response
+ #[sqlx(json)]
+ pub pending_questions: serde_json::Value,
+ /// Number of times this supervisor has been restored
+ pub restoration_count: i32,
+ /// Timestamp of last restoration
+ pub last_restored_at: Option<DateTime<Utc>>,
+ /// Source of last restoration (daemon_restart, task_reassignment, manual)
+ pub restoration_source: Option<String>,
+}
+
+/// Pending question structure for supervisor state
+#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct PendingQuestion {
+ /// Unique question ID
+ pub id: Uuid,
+ /// The question text
+ pub question: String,
+ /// Optional choices (empty for free-form)
+ #[serde(default)]
+ pub choices: Vec<String>,
+ /// Optional context
+ pub context: Option<String>,
+ /// Question type: general, phase_confirmation, contract_complete
+ #[serde(default = "default_question_type")]
+ pub question_type: String,
+ /// When the question was asked
+ pub asked_at: DateTime<Utc>,
+}
+
+fn default_question_type() -> String {
+ "general".to_string()
}
/// Request to update supervisor state
@@ -1983,6 +2027,64 @@ pub struct UpdateSupervisorStateRequest {
pub pending_task_ids: Option<Vec<Uuid>>,
/// Current contract phase
pub phase: Option<String>,
+ /// Current supervisor state
+ pub state: Option<String>,
+ /// Current activity description
+ pub current_activity: Option<String>,
+ /// Progress percentage
+ pub progress: Option<i32>,
+ /// Error message
+ pub error_message: Option<String>,
+ /// Spawned task IDs
+ pub spawned_task_ids: Option<Vec<Uuid>>,
+ /// Pending questions
+ pub pending_questions: Option<serde_json::Value>,
+}
+
+/// Restoration context returned when restoring a supervisor
+#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct SupervisorRestorationContext {
+ /// Whether restoration was successful
+ pub success: bool,
+ /// Previous state before restoration
+ pub previous_state: SupervisorStateEnum,
+ /// Restored conversation history
+ pub conversation_history: serde_json::Value,
+ /// Pending questions that need re-delivery
+ pub pending_questions: Vec<PendingQuestion>,
+ /// Tasks still being waited on
+ pub waiting_task_ids: Vec<Uuid>,
+ /// Spawned tasks to check status of
+ pub spawned_task_ids: Vec<Uuid>,
+ /// Restoration count (incremented)
+ pub restoration_count: i32,
+ /// Context message for Claude
+ pub restoration_context_message: String,
+ /// Any warnings during restoration
+ pub warnings: Vec<String>,
+}
+
+/// Validation result for supervisor state consistency
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct StateValidationResult {
+ pub is_valid: bool,
+ pub issues: Vec<String>,
+ /// Suggested recovery action
+ pub recovery_action: StateRecoveryAction,
+}
+
+/// Action to take when state validation fails
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub enum StateRecoveryAction {
+ /// State is valid, proceed with restoration
+ Proceed,
+ /// Start from last checkpoint
+ UseCheckpoint,
+ /// Start fresh
+ StartFresh,
+ /// Manual intervention required
+ ManualIntervention,
}
// ============================================================================
@@ -2339,6 +2441,111 @@ pub struct CheckpointPatchInfo {
// Red Team Types
// ============================================================================
+// =============================================================================
+// Supervisor State and Heartbeat Types
+// =============================================================================
+
+/// Supervisor state for contract supervisor tasks.
+/// Captures detailed activity state for monitoring and restoration.
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum SupervisorStateEnum {
+ /// Supervisor is starting up
+ Initializing,
+ /// Supervisor is idle, waiting for work
+ Idle,
+ /// Supervisor is actively working
+ Working,
+ /// Supervisor is waiting for user input/confirmation
+ WaitingForUser,
+ /// Supervisor is waiting for spawned tasks to complete
+ WaitingForTasks,
+ /// Supervisor is blocked (external dependency, error, etc.)
+ Blocked,
+ /// Supervisor has completed its contract
+ Completed,
+ /// Supervisor has failed
+ Failed,
+ /// Supervisor was interrupted (daemon crash, etc.)
+ Interrupted,
+}
+
+impl Default for SupervisorStateEnum {
+ fn default() -> Self {
+ SupervisorStateEnum::Initializing
+ }
+}
+
+impl std::fmt::Display for SupervisorStateEnum {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ match self {
+ SupervisorStateEnum::Initializing => write!(f, "initializing"),
+ SupervisorStateEnum::Idle => write!(f, "idle"),
+ SupervisorStateEnum::Working => write!(f, "working"),
+ SupervisorStateEnum::WaitingForUser => write!(f, "waiting_for_user"),
+ SupervisorStateEnum::WaitingForTasks => write!(f, "waiting_for_tasks"),
+ SupervisorStateEnum::Blocked => write!(f, "blocked"),
+ SupervisorStateEnum::Completed => write!(f, "completed"),
+ SupervisorStateEnum::Failed => write!(f, "failed"),
+ SupervisorStateEnum::Interrupted => write!(f, "interrupted"),
+ }
+ }
+}
+
+impl std::str::FromStr for SupervisorStateEnum {
+ type Err = String;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match s.to_lowercase().as_str() {
+ "initializing" => Ok(SupervisorStateEnum::Initializing),
+ "idle" => Ok(SupervisorStateEnum::Idle),
+ "working" => Ok(SupervisorStateEnum::Working),
+ "waiting_for_user" | "waitingforuser" => Ok(SupervisorStateEnum::WaitingForUser),
+ "waiting_for_tasks" | "waitingfortasks" => Ok(SupervisorStateEnum::WaitingForTasks),
+ "blocked" => Ok(SupervisorStateEnum::Blocked),
+ "completed" => Ok(SupervisorStateEnum::Completed),
+ "failed" => Ok(SupervisorStateEnum::Failed),
+ "interrupted" => Ok(SupervisorStateEnum::Interrupted),
+ _ => Err(format!("Unknown supervisor state: {}", s)),
+ }
+ }
+}
+
+/// Enhanced heartbeat record for supervisor task monitoring.
+/// Stored in the database for historical analysis and dead supervisor detection.
+#[derive(Debug, Clone, FromRow, Serialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct SupervisorHeartbeatRecord {
+ pub id: Uuid,
+ pub supervisor_task_id: Uuid,
+ pub contract_id: Uuid,
+ pub state: String,
+ pub phase: String,
+ pub current_activity: Option<String>,
+ pub progress: i32,
+ #[sqlx(try_from = "Vec<Uuid>")]
+ pub pending_task_ids: Vec<Uuid>,
+ pub timestamp: DateTime<Utc>,
+}
+
+/// Request payload for sending a supervisor heartbeat.
+#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct SupervisorHeartbeatRequest {
+ pub task_id: Uuid,
+ pub contract_id: Uuid,
+ pub state: SupervisorStateEnum,
+ pub phase: String,
+ pub current_activity: Option<String>,
+ /// Progress percentage (0-100)
+ pub progress: u8,
+ pub pending_task_ids: Vec<Uuid>,
+}
+
+// =============================================================================
+// Red Team Types
+// =============================================================================
+
/// Red Team notification record
#[derive(Debug, Clone, FromRow, Serialize, ToSchema)]
#[serde(rename_all = "camelCase")]
@@ -2395,3 +2602,175 @@ impl std::str::FromStr for NotificationSeverity {
}
}
}
+
+// ============================================================================
+// Supervisor Status API Types
+// ============================================================================
+
+/// Response for supervisor status endpoint
+#[derive(Debug, Clone, Serialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct SupervisorStatusResponse {
+ /// The supervisor task ID
+ pub task_id: Uuid,
+ /// Current supervisor state (from supervisor_states table)
+ pub state: String,
+ /// Current contract phase
+ pub phase: String,
+ /// Description of current activity (from task progress_summary)
+ pub current_activity: Option<String>,
+ /// Progress percentage (0-100)
+ pub progress: Option<u8>,
+ /// When the supervisor last updated its state
+ pub last_heartbeat: DateTime<Utc>,
+ /// Task IDs the supervisor is currently waiting on
+ pub pending_task_ids: Vec<Uuid>,
+ /// Whether the supervisor is currently running
+ pub is_running: bool,
+}
+
+/// Individual heartbeat entry for history
+#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct SupervisorHeartbeatEntry {
+ /// Timestamp of this heartbeat
+ pub timestamp: DateTime<Utc>,
+ /// Supervisor state at this time
+ pub state: String,
+ /// Activity description at this time
+ pub activity: Option<String>,
+ /// Progress at this time
+ pub progress: Option<u8>,
+ /// Contract phase at this time
+ pub phase: String,
+ /// Pending task IDs at this time
+ pub pending_task_ids: Vec<Uuid>,
+}
+
+/// Response for supervisor heartbeat history endpoint
+#[derive(Debug, Clone, Serialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct SupervisorHeartbeatHistoryResponse {
+ /// List of heartbeat entries
+ pub heartbeats: Vec<SupervisorHeartbeatEntry>,
+ /// Total count of heartbeats (for pagination)
+ pub total: i64,
+}
+
+/// Response for supervisor sync endpoint
+#[derive(Debug, Clone, Serialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct SupervisorSyncResponse {
+ /// Whether the sync was successful
+ pub synced: bool,
+ /// Current supervisor state after sync
+ pub state: String,
+ /// Optional message about the sync result
+ pub message: Option<String>,
+}
+
+/// Query parameters for heartbeat history endpoint
+#[derive(Debug, Deserialize, ToSchema)]
+#[serde(rename_all = "camelCase")]
+pub struct HeartbeatHistoryQuery {
+ /// Maximum number of heartbeats to return (default: 10)
+ pub limit: Option<i32>,
+ /// Offset for pagination (default: 0)
+ pub offset: Option<i32>,
+}
+
+// =============================================================================
+// Unit Tests
+// =============================================================================
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use uuid::Uuid;
+
+ #[test]
+ fn test_supervisor_state_enum_display() {
+ assert_eq!(SupervisorStateEnum::Initializing.to_string(), "initializing");
+ assert_eq!(SupervisorStateEnum::Idle.to_string(), "idle");
+ assert_eq!(SupervisorStateEnum::Working.to_string(), "working");
+ assert_eq!(SupervisorStateEnum::WaitingForUser.to_string(), "waiting_for_user");
+ assert_eq!(SupervisorStateEnum::WaitingForTasks.to_string(), "waiting_for_tasks");
+ assert_eq!(SupervisorStateEnum::Blocked.to_string(), "blocked");
+ assert_eq!(SupervisorStateEnum::Completed.to_string(), "completed");
+ assert_eq!(SupervisorStateEnum::Failed.to_string(), "failed");
+ assert_eq!(SupervisorStateEnum::Interrupted.to_string(), "interrupted");
+ }
+
+ #[test]
+ fn test_supervisor_state_enum_from_str() {
+ // Standard lowercase
+ assert_eq!("initializing".parse::<SupervisorStateEnum>().unwrap(), SupervisorStateEnum::Initializing);
+ assert_eq!("idle".parse::<SupervisorStateEnum>().unwrap(), SupervisorStateEnum::Idle);
+ assert_eq!("working".parse::<SupervisorStateEnum>().unwrap(), SupervisorStateEnum::Working);
+ assert_eq!("waiting_for_user".parse::<SupervisorStateEnum>().unwrap(), SupervisorStateEnum::WaitingForUser);
+ assert_eq!("waiting_for_tasks".parse::<SupervisorStateEnum>().unwrap(), SupervisorStateEnum::WaitingForTasks);
+ assert_eq!("blocked".parse::<SupervisorStateEnum>().unwrap(), SupervisorStateEnum::Blocked);
+ assert_eq!("completed".parse::<SupervisorStateEnum>().unwrap(), SupervisorStateEnum::Completed);
+ assert_eq!("failed".parse::<SupervisorStateEnum>().unwrap(), SupervisorStateEnum::Failed);
+ assert_eq!("interrupted".parse::<SupervisorStateEnum>().unwrap(), SupervisorStateEnum::Interrupted);
+
+ // Case insensitive
+ assert_eq!("WORKING".parse::<SupervisorStateEnum>().unwrap(), SupervisorStateEnum::Working);
+ assert_eq!("Working".parse::<SupervisorStateEnum>().unwrap(), SupervisorStateEnum::Working);
+
+ // Alternative formats
+ assert_eq!("waitingforuser".parse::<SupervisorStateEnum>().unwrap(), SupervisorStateEnum::WaitingForUser);
+ assert_eq!("waitingfortasks".parse::<SupervisorStateEnum>().unwrap(), SupervisorStateEnum::WaitingForTasks);
+
+ // Invalid state
+ assert!("invalid_state".parse::<SupervisorStateEnum>().is_err());
+ }
+
+ #[test]
+ fn test_supervisor_state_enum_serialization() {
+ // Test JSON serialization
+ let state = SupervisorStateEnum::Working;
+ let json = serde_json::to_string(&state).unwrap();
+ assert_eq!(json, "\"working\"");
+
+ // Test JSON deserialization
+ let deserialized: SupervisorStateEnum = serde_json::from_str("\"working\"").unwrap();
+ assert_eq!(deserialized, SupervisorStateEnum::Working);
+
+ // Test underscore variants
+ let json = "\"waiting_for_user\"";
+ let deserialized: SupervisorStateEnum = serde_json::from_str(json).unwrap();
+ assert_eq!(deserialized, SupervisorStateEnum::WaitingForUser);
+ }
+
+ #[test]
+ fn test_supervisor_state_enum_default() {
+ let default_state = SupervisorStateEnum::default();
+ assert_eq!(default_state, SupervisorStateEnum::Initializing);
+ }
+
+ #[test]
+ fn test_supervisor_heartbeat_request_serialization() {
+ let request = SupervisorHeartbeatRequest {
+ task_id: Uuid::nil(),
+ contract_id: Uuid::nil(),
+ state: SupervisorStateEnum::Working,
+ phase: "execute".to_string(),
+ current_activity: Some("Implementing feature".to_string()),
+ progress: 50,
+ pending_task_ids: vec![Uuid::nil()],
+ };
+
+ let json = serde_json::to_string(&request).unwrap();
+ assert!(json.contains("\"state\":\"working\""));
+ assert!(json.contains("\"phase\":\"execute\""));
+ assert!(json.contains("\"progress\":50"));
+ assert!(json.contains("\"currentActivity\":\"Implementing feature\""));
+
+ // Test deserialization
+ let deserialized: SupervisorHeartbeatRequest = serde_json::from_str(&json).unwrap();
+ assert_eq!(deserialized.state, SupervisorStateEnum::Working);
+ assert_eq!(deserialized.phase, "execute");
+ assert_eq!(deserialized.progress, 50);
+ }
+}