Hmbown/CodeWhale · error

Unpromoted task stage contains execution evidence; refusing…

Error message

Unpromoted task stage contains execution evidence; refusing to replay it

What it means

When replaying a staged (unpromoted) task during recovery, the manager refuses if the staged record already shows execution evidence: a status other than Queued, or a set started_at, thread_id, or turn_id. Replaying such a stage would duplicate or corrupt a task that was already (partially) executed, so the admission is aborted instead.

Solutions

  1. Check the staged record before replaying: only replay when status == Queued and started_at/thread_id/turn_id are all None
  2. If the stage already has execution evidence, promote/adopt it through the normal task-adoption path instead of replaying it as new
  3. Clear stale stage files that belong to tasks already promoted, so recovery does not see them
  4. Serialize recovery with the worker's dispatch lock so a concurrently promoted task is not staged-replayed

Example fix

// before
mgr.recover_staged_task(&task_id, &request).await?;
// after
if let Some(staged) = mgr.read_staged_task(&task_id)? {
    if staged.status == TaskStatus::Queued
        && staged.started_at.is_none()
        && staged.thread_id.is_none()
        && staged.turn_id.is_none()
    {
        mgr.recover_staged_task(&task_id, &request).await?;
    }
}
Defensive patterns

Strategy: validation

Validate before calling

fn replayable(st: &TaskRecord) -> bool { st.status == TaskStatus::Queued && st.started_at.is_none() && st.thread_id.is_none() && st.turn_id.is_none() }

Try / catch

match replay_result { Err(e) if e.to_string().contains("execution evidence") => adopt_existing_task_instead()?, r => r?, }

Prevention

When it happens

Trigger: Calling the recovery/promotion path (staged-task replay via admit_task_record with recover_stage=true, e.g. startup replay or a recover_staged_task-style call) on a stage whose TaskStatus is not Queued or that already has started_at/thread_id/turn_id populated.

Common situations: Re-running startup recovery while a worker already promoted and began the task (crash between promotion and cleanup); replaying the same stage file twice; copying a tasks directory that contains post-promotion stages.

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/04c26690c13ccde6. Report an issue: GitHub.

Appendix: source

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

    pub(crate) async fn recover_task_admission(
        &self,
        request: NewTaskRequest,
        task_id: String,
    ) -> Result<TaskRecord> {
        validate_preallocated_task_id(&task_id)?;
        if let Some(task) = self.read_bound_task(&task_id)? {
            validate_bound_task_request(&task, &request)?;
            return Ok(task);
        }
        let staged_path = self.tasks_dir.join(format!(".{task_id}.json.pending"));
        let admission = if let Some(staged) = read_bound_task_file(&staged_path, &task_id)? {
            validate_bound_task_request(&staged, &request)?;
            if staged.status != TaskStatus::Queued
                || staged.started_at.is_some()
                || staged.thread_id.is_some()
                || staged.turn_id.is_some()
            {
                bail!("Unpromoted task stage contains execution evidence; refusing to replay it");
            }
            // The original resolved settings remain durable in this stage
            // until promotion, including if recovery itself is interrupted.
            self.admit_task_record(staged, true).await
        } else {
            self.add_task_with_id(request.clone(), task_id.clone())
                .await
        };
        match admission {
            Ok(task) => Ok(task),
            Err(error) => {
                // Preserve a task promoted before a torn response; do not
                // replace its identity or convert accepted work into a retry.
                if let Some(task) = self.read_bound_task(&task_id)? {
                    validate_bound_task_request(&task, &request)?;
                    Ok(task)
                } else {
                    Err(error)

View on GitHub (pinned to 73e0f67d83)