Hmbown/CodeWhale · error

Task execution ownership changed; refusing a stale write

Error message

Task execution ownership changed; refusing a stale write

What it means

Before persisting a change to a running task, the manager verifies the task is still owned by this execution: its execution_scope matches this manager's scope, its execution_generation matches the current lease generation, and its status is Running. If any changed (e.g. another generation took over, or the task stopped), a stale write would clobber newer state, so it refuses.

Solutions

  1. Re-acquire the execution lease (refresh generation) and reload the task before retrying the write
  2. Drop the stale write if the task has moved on; re-read current state and re-apply the intent
  3. Check task.status == Running and matching scope/generation before issuing updates

Example fix

// before: blind update
manager.update_task_output(id, output).await?; // may be refused
// after: verify ownership first
if let Some(t) = manager.get_task(id) {
    if t.status == TaskStatus::Running { manager.update_task_output(id, output).await?; }
}
Defensive patterns

Strategy: validation

Validate before calling

if task.status != TaskStatus::Running
    || task.execution_scope.as_deref() != Some(current_scope)
    || task.execution_generation.as_deref() != Some(&current_generation) {
    return Err(anyhow!("ownership lost; skip stale write"));
}

Type guard

fn owns_execution(task: &TaskRecord, scope: &str, generation: &str) -> bool {
    task.status == TaskStatus::Running
        && task.execution_scope.as_deref() == Some(scope)
        && task.execution_generation.as_deref() == Some(generation)
}

Try / catch

match manager.update_task(id, change).await {
    Err(e) if e.to_string().contains("refusing a stale write") => {
        // reload current state and re-apply the change under fresh ownership
        let fresh = manager.get_task(id).context("task gone")?;
        manager.update_task(&fresh.id, change).await?;
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling persist_changed_task_locked (or any path that calls require_execution_owner) for a task whose execution_scope/execution_generation no longer matches the current lease, or whose status is no longer Running.

Common situations: A lease renewal lapsed and another process advanced the generation; the task was cancelled or completed concurrently; recovery reassigned the task to a different execution scope and an old writer flushes late.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


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

Appendix: source

Thrown at crates/tui/src/task_manager.rs:2996

        for (id, events) in &state.pending_events {
            let task = state
                .tasks
                .get_mut(id)
                .context("Pending task disappeared")?;
            self.require_execution_owner(task)?;
            for event in events {
                self.apply_event_to_task(task, event.clone())?;
            }
        }
        Ok(())
    }

    fn require_execution_owner(&self, task: &TaskRecord) -> Result<()> {
        if task.execution_scope.as_deref() != Some(self.execution_scope())
            || task.execution_generation.as_deref() != Some(&self.execution_lease.generation)
            || task.status != TaskStatus::Running
        {
            bail!("Task execution ownership changed; refusing a stale write");
        }
        Ok(())
    }

    fn persist_changed_task_locked(&self, state: &mut ManagerState, id: &str) -> Result<()> {
        let task = state.tasks.get(id).context("Changed task is missing")?;
        self.persist_task_locked(task)?;
        state.pending_events.remove(id);
        Ok(())
    }

    fn recover_dead_executions_locked(&self, state: &mut ManagerState) -> Result<()> {
        for task in state.tasks.values_mut() {
            // Unknown legacy ownership is preserved, never guessed from the
            // visibility owner, model spelling, or current process defaults.
            if task.status != TaskStatus::Running {
                continue;
            }

View on GitHub (pinned to 73e0f67d83)