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
- Re-acquire the execution lease (refresh generation) and reload the task before retrying the write
- Drop the stale write if the task has moved on; re-read current state and re-apply the intent
- 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(¤t_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
- Refresh the execution lease before long-running writes
- Re-read task state after any lease renewal or recovery
- Never cache TaskRecord across generation boundaries
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
- Owner lock was replaced
- Session Runtime ownership changed; reopen the session…
- A saved trace identifier collided. Try saving again.
- a window change is already in progress
- Agent continuation target is outside the active session
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)