diff options
| author | soryu <soryu@soryu.co> | 2026-01-11 05:52:14 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-15 00:21:16 +0000 |
| commit | 87044a747b47bd83249d61a45842c7f7b2eae56d (patch) | |
| tree | ef2000ce79ffcc2723ef841acef5aa1deb1d5378 /makima/daemon/src/ws | |
| parent | 077820c4167c168072d217a1b01df840463a12a8 (diff) | |
| download | soryu-87044a747b47bd83249d61a45842c7f7b2eae56d.tar.gz soryu-87044a747b47bd83249d61a45842c7f7b2eae56d.zip | |
Contract system
Diffstat (limited to 'makima/daemon/src/ws')
| -rw-r--r-- | makima/daemon/src/ws/client.rs | 290 | ||||
| -rw-r--r-- | makima/daemon/src/ws/mod.rs | 7 | ||||
| -rw-r--r-- | makima/daemon/src/ws/protocol.rs | 511 |
3 files changed, 0 insertions, 808 deletions
diff --git a/makima/daemon/src/ws/client.rs b/makima/daemon/src/ws/client.rs deleted file mode 100644 index ba1263f..0000000 --- a/makima/daemon/src/ws/client.rs +++ /dev/null @@ -1,290 +0,0 @@ -//! WebSocket client for connecting to the makima server. - -use std::sync::Arc; -use std::time::Duration; - -use backoff::backoff::Backoff; -use backoff::ExponentialBackoff; -use futures::{SinkExt, StreamExt}; -use tokio::sync::{mpsc, RwLock}; -use tokio_tungstenite::{connect_async, tungstenite::{client::IntoClientRequest, Message}}; -use uuid::Uuid; - -use super::protocol::{DaemonCommand, DaemonMessage}; -use crate::config::ServerConfig; -use crate::error::{DaemonError, Result}; - -/// WebSocket client state. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ConnectionState { - /// Not connected to server. - Disconnected, - /// Currently connecting. - Connecting, - /// Connected and authenticated. - Connected, - /// Connection failed, will retry. - Reconnecting, - /// Permanently failed (e.g., auth failure). - Failed, -} - -/// WebSocket client for daemon-server communication. -pub struct WsClient { - config: ServerConfig, - machine_id: String, - hostname: String, - max_concurrent_tasks: i32, - state: Arc<RwLock<ConnectionState>>, - daemon_id: Arc<RwLock<Option<Uuid>>>, - /// Channel to receive messages to send to server. - outgoing_rx: mpsc::Receiver<DaemonMessage>, - /// Sender for outgoing messages (clone this to send messages). - outgoing_tx: mpsc::Sender<DaemonMessage>, - /// Channel to send received commands to the task manager. - incoming_tx: mpsc::Sender<DaemonCommand>, -} - -impl WsClient { - /// Create a new WebSocket client. - pub fn new( - config: ServerConfig, - machine_id: String, - hostname: String, - max_concurrent_tasks: i32, - incoming_tx: mpsc::Sender<DaemonCommand>, - ) -> Self { - let (outgoing_tx, outgoing_rx) = mpsc::channel(256); - - Self { - config, - machine_id, - hostname, - max_concurrent_tasks, - state: Arc::new(RwLock::new(ConnectionState::Disconnected)), - daemon_id: Arc::new(RwLock::new(None)), - outgoing_rx, - outgoing_tx, - incoming_tx, - } - } - - /// Get a sender for outgoing messages. - pub fn sender(&self) -> mpsc::Sender<DaemonMessage> { - self.outgoing_tx.clone() - } - - /// Get current connection state. - pub async fn state(&self) -> ConnectionState { - *self.state.read().await - } - - /// Get daemon ID if authenticated. - pub async fn daemon_id(&self) -> Option<Uuid> { - *self.daemon_id.read().await - } - - /// Run the WebSocket client with automatic reconnection. - pub async fn run(&mut self) -> Result<()> { - let mut backoff = ExponentialBackoff { - initial_interval: Duration::from_secs(self.config.reconnect_interval_secs), - max_interval: Duration::from_secs(60), - max_elapsed_time: if self.config.max_reconnect_attempts > 0 { - Some(Duration::from_secs( - self.config.reconnect_interval_secs * self.config.max_reconnect_attempts as u64 * 10, - )) - } else { - None // Infinite retries - }, - ..Default::default() - }; - - loop { - *self.state.write().await = ConnectionState::Connecting; - tracing::info!("Connecting to server: {}", self.config.url); - - match self.connect_and_run().await { - Ok(()) => { - // Clean shutdown - tracing::info!("WebSocket connection closed cleanly"); - break; - } - Err(DaemonError::AuthFailed(msg)) => { - tracing::error!("Authentication failed: {}", msg); - *self.state.write().await = ConnectionState::Failed; - return Err(DaemonError::AuthFailed(msg)); - } - Err(e) => { - tracing::warn!("Connection error: {}", e); - *self.state.write().await = ConnectionState::Reconnecting; - - if let Some(delay) = backoff.next_backoff() { - tracing::info!("Reconnecting in {:?}...", delay); - tokio::time::sleep(delay).await; - } else { - tracing::error!("Max reconnection attempts reached"); - *self.state.write().await = ConnectionState::Failed; - return Err(DaemonError::ConnectionLost); - } - } - } - } - - Ok(()) - } - - /// Connect to server and run the message loop. - async fn connect_and_run(&mut self) -> Result<()> { - // Build WebSocket URL - let ws_url = format!("{}/api/v1/mesh/daemons/connect", self.config.url); - tracing::debug!("Connecting to WebSocket: {}", ws_url); - - // Build request with API key header - let mut request = ws_url.into_client_request()?; - request.headers_mut().insert( - "x-makima-api-key", - self.config.api_key.parse().map_err(|_| { - DaemonError::AuthFailed("Invalid API key format".into()) - })?, - ); - - // Connect with API key in headers - let (ws_stream, _response) = connect_async(request).await?; - let (mut write, mut read) = ws_stream.split(); - - // Send daemon info after connection (server authenticated us via header) - let info_msg = DaemonMessage::authenticate( - &self.config.api_key, - &self.machine_id, - &self.hostname, - self.max_concurrent_tasks, - ); - let info_json = serde_json::to_string(&info_msg)?; - write.send(Message::Text(info_json)).await?; - - // Wait for authentication response - let auth_response = read - .next() - .await - .ok_or(DaemonError::ConnectionLost)??; - - let auth_text = match auth_response { - Message::Text(text) => text, - Message::Close(_) => return Err(DaemonError::ConnectionLost), - _ => return Err(DaemonError::AuthFailed("Unexpected response type".into())), - }; - - let command: DaemonCommand = serde_json::from_str(&auth_text)?; - match command { - DaemonCommand::Authenticated { daemon_id } => { - tracing::info!("Authenticated with daemon ID: {}", daemon_id); - *self.daemon_id.write().await = Some(daemon_id); - *self.state.write().await = ConnectionState::Connected; - - // Send daemon directories info to server - let working_directory = std::env::current_dir() - .map(|p| p.to_string_lossy().to_string()) - .unwrap_or_else(|_| ".".to_string()); - let home_directory = dirs::home_dir() - .map(|h| h.join(".makima").join("home")) - .unwrap_or_else(|| std::path::PathBuf::from("~/.makima/home")); - // Create home directory if it doesn't exist - if let Err(e) = std::fs::create_dir_all(&home_directory) { - tracing::warn!("Failed to create home directory {:?}: {}", home_directory, e); - } - let home_directory_str = home_directory.to_string_lossy().to_string(); - let worktrees_directory = dirs::home_dir() - .map(|h| h.join(".makima").join("worktrees").to_string_lossy().to_string()) - .unwrap_or_else(|| "~/.makima/worktrees".to_string()); - - let dirs_msg = DaemonMessage::DaemonDirectories { - working_directory, - home_directory: home_directory_str, - worktrees_directory, - }; - let dirs_json = serde_json::to_string(&dirs_msg)?; - write.send(Message::Text(dirs_json)).await?; - tracing::info!("Sent daemon directories info to server"); - } - DaemonCommand::Error { code, message } => { - return Err(DaemonError::AuthFailed(format!("{}: {}", code, message))); - } - _ => { - return Err(DaemonError::AuthFailed( - "Unexpected response to authentication".into(), - )); - } - } - - // Start main message loop - let heartbeat_interval = Duration::from_secs(self.config.heartbeat_interval_secs); - let mut heartbeat_timer = tokio::time::interval(heartbeat_interval); - - loop { - tokio::select! { - // Handle incoming server commands - msg = read.next() => { - match msg { - Some(Ok(Message::Text(text))) => { - tracing::info!("Received WebSocket message: {} bytes", text.len()); - match serde_json::from_str::<DaemonCommand>(&text) { - Ok(command) => { - tracing::info!("Parsed command: {:?}", command); - tracing::info!("Sending command to task manager channel..."); - if self.incoming_tx.send(command).await.is_err() { - tracing::warn!("Command receiver dropped, shutting down"); - break; - } - tracing::info!("Command sent to task manager successfully"); - } - Err(e) => { - tracing::warn!("Failed to parse server message: {}", e); - tracing::debug!("Raw message: {}", text); - } - } - } - Some(Ok(Message::Ping(data))) => { - write.send(Message::Pong(data)).await?; - } - Some(Ok(Message::Close(_))) | None => { - tracing::info!("Server closed connection"); - return Err(DaemonError::ConnectionLost); - } - Some(Err(e)) => { - tracing::warn!("WebSocket error: {}", e); - return Err(e.into()); - } - _ => {} - } - } - - // Handle outgoing messages - msg = self.outgoing_rx.recv() => { - match msg { - Some(message) => { - let json = serde_json::to_string(&message)?; - tracing::trace!("Sending message: {}", json); - write.send(Message::Text(json)).await?; - } - None => { - // Sender dropped, shutdown - tracing::info!("Outgoing channel closed, shutting down"); - break; - } - } - } - - // Send heartbeat - _ = heartbeat_timer.tick() => { - // Get active task IDs from task manager - // For now, send empty list - will be connected to task manager - let heartbeat = DaemonMessage::heartbeat(vec![]); - let json = serde_json::to_string(&heartbeat)?; - write.send(Message::Text(json)).await?; - } - } - } - - Ok(()) - } -} diff --git a/makima/daemon/src/ws/mod.rs b/makima/daemon/src/ws/mod.rs deleted file mode 100644 index 5a0e9d1..0000000 --- a/makima/daemon/src/ws/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! WebSocket client and protocol types for daemon-server communication. - -pub mod client; -pub mod protocol; - -pub use client::{ConnectionState, WsClient}; -pub use protocol::{BranchInfo, DaemonCommand, DaemonMessage}; diff --git a/makima/daemon/src/ws/protocol.rs b/makima/daemon/src/ws/protocol.rs deleted file mode 100644 index 7c2ad6d..0000000 --- a/makima/daemon/src/ws/protocol.rs +++ /dev/null @@ -1,511 +0,0 @@ -//! Protocol types for daemon-server communication. -//! -//! These types mirror the server's protocol exactly for compatibility. - -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -/// Message from daemon to server. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "camelCase")] -pub enum DaemonMessage { - /// Authentication request (first message required). - Authenticate { - #[serde(rename = "apiKey")] - api_key: String, - #[serde(rename = "machineId")] - machine_id: String, - hostname: String, - #[serde(rename = "maxConcurrentTasks")] - max_concurrent_tasks: i32, - }, - - /// Periodic heartbeat with current status. - Heartbeat { - #[serde(rename = "activeTasks")] - active_tasks: Vec<Uuid>, - }, - - /// Task output streaming (stdout/stderr from Claude Code). - TaskOutput { - #[serde(rename = "taskId")] - task_id: Uuid, - output: String, - #[serde(rename = "isPartial")] - is_partial: bool, - }, - - /// Task status change notification. - TaskStatusChange { - #[serde(rename = "taskId")] - task_id: Uuid, - #[serde(rename = "oldStatus")] - old_status: String, - #[serde(rename = "newStatus")] - new_status: String, - }, - - /// Task progress update with summary. - TaskProgress { - #[serde(rename = "taskId")] - task_id: Uuid, - summary: String, - }, - - /// Task completion notification. - TaskComplete { - #[serde(rename = "taskId")] - task_id: Uuid, - success: bool, - error: Option<String>, - }, - - /// Register a tool key for orchestrator API access. - RegisterToolKey { - #[serde(rename = "taskId")] - task_id: Uuid, - /// The API key for this orchestrator to use when calling mesh endpoints. - key: String, - }, - - /// Revoke a tool key when task completes. - RevokeToolKey { - #[serde(rename = "taskId")] - task_id: Uuid, - }, - - // ========================================================================= - // Merge Response Messages (sent by daemon after processing merge commands) - // ========================================================================= - - /// Response to ListBranches command. - BranchList { - #[serde(rename = "taskId")] - task_id: Uuid, - branches: Vec<BranchInfo>, - }, - - /// Response to MergeStatus command. - MergeStatusResponse { - #[serde(rename = "taskId")] - task_id: Uuid, - #[serde(rename = "inProgress")] - in_progress: bool, - #[serde(rename = "sourceBranch")] - source_branch: Option<String>, - #[serde(rename = "conflictedFiles")] - conflicted_files: Vec<String>, - }, - - /// Response to merge operations (MergeStart, MergeResolve, MergeCommit, MergeAbort). - MergeResult { - #[serde(rename = "taskId")] - task_id: Uuid, - success: bool, - message: String, - #[serde(rename = "commitSha")] - commit_sha: Option<String>, - /// Present only when conflicts occurred. - conflicts: Option<Vec<String>>, - }, - - /// Response to CheckMergeComplete command. - MergeCompleteCheck { - #[serde(rename = "taskId")] - task_id: Uuid, - #[serde(rename = "canComplete")] - can_complete: bool, - #[serde(rename = "unmergedBranches")] - unmerged_branches: Vec<String>, - #[serde(rename = "mergedCount")] - merged_count: u32, - #[serde(rename = "skippedCount")] - skipped_count: u32, - }, - - // ========================================================================= - // Completion Action Response Messages - // ========================================================================= - - /// Response to RetryCompletionAction command. - CompletionActionResult { - #[serde(rename = "taskId")] - task_id: Uuid, - success: bool, - message: String, - /// PR URL if action was "pr" and successful. - #[serde(rename = "prUrl")] - pr_url: Option<String>, - }, - - /// Report daemon's available directories for task output. - DaemonDirectories { - /// Current working directory of the daemon. - #[serde(rename = "workingDirectory")] - working_directory: String, - /// Path to ~/.makima/home directory (for cloning completed work). - #[serde(rename = "homeDirectory")] - home_directory: String, - /// Path to worktrees directory (~/.makima/worktrees). - #[serde(rename = "worktreesDirectory")] - worktrees_directory: String, - }, - - /// Response to CloneWorktree command. - CloneWorktreeResult { - #[serde(rename = "taskId")] - task_id: Uuid, - success: bool, - message: String, - /// The path where the worktree was cloned. - #[serde(rename = "targetDir")] - target_dir: Option<String>, - }, - - /// Response to CheckTargetExists command. - CheckTargetExistsResult { - #[serde(rename = "taskId")] - task_id: Uuid, - /// Whether the target directory exists. - exists: bool, - /// The path that was checked. - #[serde(rename = "targetDir")] - target_dir: String, - }, -} - -/// Information about a branch (used in BranchList message). -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct BranchInfo { - /// Full branch name. - pub name: String, - /// Task ID extracted from branch name (if parseable). - #[serde(rename = "taskId")] - pub task_id: Option<Uuid>, - /// Whether this branch has been merged. - #[serde(rename = "isMerged")] - pub is_merged: bool, - /// Short SHA of the last commit. - #[serde(rename = "lastCommit")] - pub last_commit: String, - /// Subject line of the last commit. - #[serde(rename = "lastCommitMessage")] - pub last_commit_message: String, -} - -/// Command from server to daemon. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "camelCase")] -pub enum DaemonCommand { - /// Confirm successful authentication. - Authenticated { - #[serde(rename = "daemonId")] - daemon_id: Uuid, - }, - - /// Spawn a new task in a container. - SpawnTask { - #[serde(rename = "taskId")] - task_id: Uuid, - /// Human-readable task name (used for commit messages). - #[serde(rename = "taskName")] - task_name: String, - plan: String, - #[serde(rename = "repoUrl")] - repo_url: Option<String>, - #[serde(rename = "baseBranch")] - base_branch: Option<String>, - /// Target branch to merge into (used for completion actions). - #[serde(rename = "targetBranch")] - target_branch: Option<String>, - /// Parent task ID if this is a subtask. - #[serde(rename = "parentTaskId")] - parent_task_id: Option<Uuid>, - /// Depth in task hierarchy (0=top-level, 1=subtask, 2=sub-subtask). - depth: i32, - /// Whether this task should run as an orchestrator (true if depth==0 and has subtasks). - #[serde(rename = "isOrchestrator")] - is_orchestrator: bool, - /// Path to user's local repository (outside ~/.makima) for completion actions. - #[serde(rename = "targetRepoPath")] - target_repo_path: Option<String>, - /// Action on completion: "none", "branch", "merge", "pr". - #[serde(rename = "completionAction")] - completion_action: Option<String>, - /// Task ID to continue from (copy worktree from this task). - #[serde(rename = "continueFromTaskId")] - continue_from_task_id: Option<Uuid>, - /// Files to copy from parent task's worktree. - #[serde(rename = "copyFiles")] - copy_files: Option<Vec<String>>, - }, - - /// Pause a running task. - PauseTask { - #[serde(rename = "taskId")] - task_id: Uuid, - }, - - /// Resume a paused task. - ResumeTask { - #[serde(rename = "taskId")] - task_id: Uuid, - }, - - /// Interrupt a task (gracefully or forced). - InterruptTask { - #[serde(rename = "taskId")] - task_id: Uuid, - graceful: bool, - }, - - /// Send a message to a running task. - SendMessage { - #[serde(rename = "taskId")] - task_id: Uuid, - message: String, - }, - - /// Inject context about sibling task progress. - InjectSiblingContext { - #[serde(rename = "taskId")] - task_id: Uuid, - #[serde(rename = "siblingTaskId")] - sibling_task_id: Uuid, - #[serde(rename = "siblingName")] - sibling_name: String, - #[serde(rename = "siblingStatus")] - sibling_status: String, - #[serde(rename = "progressSummary")] - progress_summary: Option<String>, - #[serde(rename = "changedFiles")] - changed_files: Vec<String>, - }, - - // ========================================================================= - // Merge Commands (for orchestrators to merge subtask branches) - // ========================================================================= - - /// List all subtask branches for a task. - ListBranches { - #[serde(rename = "taskId")] - task_id: Uuid, - }, - - /// Start merging a subtask branch. - MergeStart { - #[serde(rename = "taskId")] - task_id: Uuid, - #[serde(rename = "sourceBranch")] - source_branch: String, - }, - - /// Get current merge status. - MergeStatus { - #[serde(rename = "taskId")] - task_id: Uuid, - }, - - /// Resolve a merge conflict. - MergeResolve { - #[serde(rename = "taskId")] - task_id: Uuid, - file: String, - /// "ours" or "theirs" - strategy: String, - }, - - /// Commit the current merge. - MergeCommit { - #[serde(rename = "taskId")] - task_id: Uuid, - message: String, - }, - - /// Abort the current merge. - MergeAbort { - #[serde(rename = "taskId")] - task_id: Uuid, - }, - - /// Skip merging a subtask branch (mark as intentionally not merged). - MergeSkip { - #[serde(rename = "taskId")] - task_id: Uuid, - #[serde(rename = "subtaskId")] - subtask_id: Uuid, - reason: String, - }, - - /// Check if all subtask branches have been merged or skipped (completion gate). - CheckMergeComplete { - #[serde(rename = "taskId")] - task_id: Uuid, - }, - - // ========================================================================= - // Completion Action Commands - // ========================================================================= - - /// Retry a completion action for a completed task. - RetryCompletionAction { - #[serde(rename = "taskId")] - task_id: Uuid, - /// Human-readable task name (used for commit messages). - #[serde(rename = "taskName")] - task_name: String, - /// The action to execute: "branch", "merge", or "pr". - action: String, - /// Path to the target repository. - #[serde(rename = "targetRepoPath")] - target_repo_path: String, - /// Target branch to merge into (for merge/pr actions). - #[serde(rename = "targetBranch")] - target_branch: Option<String>, - }, - - /// Clone worktree to a target directory. - CloneWorktree { - #[serde(rename = "taskId")] - task_id: Uuid, - /// Path to the target directory. - #[serde(rename = "targetDir")] - target_dir: String, - }, - - /// Check if a target directory exists. - CheckTargetExists { - #[serde(rename = "taskId")] - task_id: Uuid, - /// Path to check. - #[serde(rename = "targetDir")] - target_dir: String, - }, - - /// Error response. - Error { - code: String, - message: String, - }, -} - -impl DaemonMessage { - /// Create an authentication message. - pub fn authenticate( - api_key: &str, - machine_id: &str, - hostname: &str, - max_concurrent_tasks: i32, - ) -> Self { - Self::Authenticate { - api_key: api_key.to_string(), - machine_id: machine_id.to_string(), - hostname: hostname.to_string(), - max_concurrent_tasks, - } - } - - /// Create a heartbeat message. - pub fn heartbeat(active_tasks: Vec<Uuid>) -> Self { - Self::Heartbeat { active_tasks } - } - - /// Create a task output message. - pub fn task_output(task_id: Uuid, output: String, is_partial: bool) -> Self { - Self::TaskOutput { - task_id, - output, - is_partial, - } - } - - /// Create a task status change message. - pub fn task_status_change(task_id: Uuid, old_status: &str, new_status: &str) -> Self { - Self::TaskStatusChange { - task_id, - old_status: old_status.to_string(), - new_status: new_status.to_string(), - } - } - - /// Create a task progress message. - pub fn task_progress(task_id: Uuid, summary: String) -> Self { - Self::TaskProgress { task_id, summary } - } - - /// Create a task complete message. - pub fn task_complete(task_id: Uuid, success: bool, error: Option<String>) -> Self { - Self::TaskComplete { - task_id, - success, - error, - } - } - - /// Create a register tool key message. - pub fn register_tool_key(task_id: Uuid, key: String) -> Self { - Self::RegisterToolKey { task_id, key } - } - - /// Create a revoke tool key message. - pub fn revoke_tool_key(task_id: Uuid) -> Self { - Self::RevokeToolKey { task_id } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_daemon_message_serialization() { - let msg = DaemonMessage::authenticate("key123", "machine-abc", "worker-1", 4); - let json = serde_json::to_string(&msg).unwrap(); - assert!(json.contains("\"type\":\"authenticate\"")); - assert!(json.contains("\"apiKey\":\"key123\"")); - assert!(json.contains("\"machineId\":\"machine-abc\"")); - } - - #[test] - fn test_daemon_command_deserialization() { - let json = r#"{"type":"spawnTask","taskId":"550e8400-e29b-41d4-a716-446655440000","plan":"Build the feature","repoUrl":"https://github.com/test/repo","baseBranch":"main","parentTaskId":null,"depth":0,"isOrchestrator":false}"#; - let cmd: DaemonCommand = serde_json::from_str(json).unwrap(); - match cmd { - DaemonCommand::SpawnTask { - plan, - repo_url, - base_branch, - parent_task_id, - depth, - is_orchestrator, - .. - } => { - assert_eq!(plan, "Build the feature"); - assert_eq!(repo_url, Some("https://github.com/test/repo".to_string())); - assert_eq!(base_branch, Some("main".to_string())); - assert_eq!(parent_task_id, None); - assert_eq!(depth, 0); - assert!(!is_orchestrator); - } - _ => panic!("Expected SpawnTask"), - } - } - - #[test] - fn test_orchestrator_spawn_deserialization() { - let json = r#"{"type":"spawnTask","taskId":"550e8400-e29b-41d4-a716-446655440000","plan":"Coordinate subtasks","repoUrl":"https://github.com/test/repo","baseBranch":"main","parentTaskId":null,"depth":0,"isOrchestrator":true}"#; - let cmd: DaemonCommand = serde_json::from_str(json).unwrap(); - match cmd { - DaemonCommand::SpawnTask { - is_orchestrator, - depth, - .. - } => { - assert!(is_orchestrator); - assert_eq!(depth, 0); - } - _ => panic!("Expected SpawnTask"), - } - } -} |
