rho-coding-agent 2.16.0

A fast Rust agent harness with a small footprint and opinionated defaults
//! Destructive plan/run mutations for the durable workflow store.
//!
//! Inventory (`store_inventory.rs`) is projection only. Delete and path-guarded
//! removal live here next to the other durable mutation entry points.

use std::path::Path;

use fs2::FileExt;

use super::{delete_child_directory, run_relative, WorkflowStore};
use crate::workflow::{PlanId, RunId, WorkflowError, WorkflowResult};

impl WorkflowStore {
    /// Deletes one plan directory. Runs keep their copied graph, so resume still works.
    pub(crate) fn delete_plan(&self, id: PlanId) -> WorkflowResult<()> {
        // Confirm the plan directory is a real store entry before removal.
        let _ = self.read_plan_inventory(id)?;
        delete_child_directory(&self.layout.plans(), &self.layout.plan(id))
    }

    /// Deletes one run directory.
    ///
    /// Holds the exclusive writer lock across a rename of the run ID path so
    /// another process cannot `lock_run` and drive the tree while it is removed.
    /// Live (`Running` / `Cancelling`) runs are refused under that lock, except
    /// read-only legacy runs: nothing can cancel or resume them, so a free lock
    /// means no owner remains.
    pub(crate) fn delete_run(&self, id: RunId) -> WorkflowResult<()> {
        let lock = self
            .root
            .open_private_file(&run_relative(id, Path::new("mutation.lock")), true)?;
        lock.try_lock_exclusive()
            .map_err(|error| WorkflowError::Corrupt {
                path: self.layout.run_lock(id),
                reason: format!("run already has an active writer: {error}"),
            })?;

        // The store owns the live-run deletion policy; check it under the lock.
        let run = self.read_run_inventory(id)?;
        // NEXT_MAJOR(rho-coding-agent): drop the legacy exemption with version 1 run support.
        if run.lifecycle.is_live() && run.access == super::RecordAccess::Executable {
            let _ = lock.unlock();
            return Err(WorkflowError::LiveRun {
                id,
                lifecycle: run.lifecycle,
            });
        }

        // Move the run ID path aside so lock_run cannot attach by the original path.
        // Unix can rename under the open lock handle; Windows cannot rename a
        // directory while any file inside it is open, so drop the handle first.
        // After unlock, a concurrent lock_run either loses the path (rename won)
        // or keeps open handles that make rename fail closed.
        let trash_name = format!(".trash-run-{id}-{}", uuid::Uuid::new_v4());
        let run_path = self.layout.run(id);
        let trash_path = self.layout.runs().join(&trash_name);
        #[cfg(windows)]
        {
            let _ = lock.unlock();
            drop(lock);
            std::fs::rename(&run_path, &trash_path).map_err(WorkflowError::Io)?;
        }
        #[cfg(not(windows))]
        {
            if let Err(error) = std::fs::rename(&run_path, &trash_path) {
                let _ = lock.unlock();
                return Err(WorkflowError::Io(error));
            }
            let _ = lock.unlock();
            drop(lock);
        }
        delete_child_directory(&self.layout.runs(), &trash_path)
    }
}