Hmbown/CodeWhale · error

A cancelled Operation cannot be edited.

Error message

A cancelled Operation cannot be edited.

What it means

apply_operate_patch refuses to mutate an Operation whose status is Cancelled. Cancellation is terminal: once an Operation is cancelled its recorded fields are frozen so history stays consistent, and any patch (direction, leadPlan, etc.) is rejected.

Solutions

  1. Check `op.status` before editing and skip cancelled operations
  2. Create a new Operation instead of editing the cancelled one
  3. If the operation should not have been cancelled, restart it as a new operation rather than un-cancelling

Example fix

// before
apply_operate_patch(&mut op, &patch)?;
// after
if op.status != OperateStatus::Cancelled {
    apply_operate_patch(&mut op, &patch)?;
}
Defensive patterns

Strategy: validation

Validate before calling

fn editable(op: &Operation) -> bool { op.status != OperateStatus::Cancelled }

Try / catch

match apply_operate_patch(&mut op, &patch) { Err(e) if e.to_string().contains("cannot be edited") => eprintln!("operation was cancelled; create a new one"), other => other?, }

Prevention

When it happens

Trigger: Calling apply_operate_patch (or an edit API that routes to it) with a JSON patch targeting an Operation with status == Cancelled.

Common situations: UI or automation still holds a stale handle to an operation the user cancelled; a retry/update job firing after cancellation; bulk edits iterating over operations including cancelled ones.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/1f6e834bdc900fb1. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/operate.rs:1091

            Ok(())
        })?;
        if let Some(op) = attached {
            return Ok(op);
        }
    }
    start_operation(
        store,
        workspace,
        direction,
        burn_usd_per_hour,
        credentials_present,
        lead_model,
    )
}

pub fn apply_operate_patch(op: &mut Operation, patch: &serde_json::Value) -> Result<()> {
    if op.status == OperateStatus::Cancelled {
        anyhow::bail!("A cancelled Operation cannot be edited.");
    }
    if let Some(direction) = patch.get("direction") {
        let next = normalize_direction(direction.as_str().map(str::to_string).unwrap_or_default());
        if patch.get("leadPlan").is_none() && next != op.direction {
            // A changed direction supersedes the recorded lead plan: workers
            // must stop executing slices derived from the old direction. The
            // operation idles `awaiting_lead_plan` until the lead re-plans
            // (same patch may instead carry an explicit replacement plan).
            op.lead_plan = None;
        }
        op.direction = next;
    }
    if patch.get("burnRate").is_some() {
        op.burn_rate = parse_burn_rate(patch.get("burnRate"))?;
    }
    if let Some(plan) = patch.get("leadPlan") {
        op.lead_plan = if plan.is_null() {
            None

View on GitHub (pinned to 73e0f67d83)