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
- Check the staged record before replaying: only replay when status == Queued and started_at/thread_id/turn_id are all None
- If the stage already has execution evidence, promote/adopt it through the normal task-adoption path instead of replaying it as new
- Clear stale stage files that belong to tasks already promoted, so recovery does not see them
- 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
- Run recovery under the dispatch lock so promotion and replay cannot race
- Only replay stage files whose status is still Queued
- Clean up promoted stage files promptly after promotion
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
- A pinned task provider requires an explicit model
- err
- first coordination interrupt
- Invalid task execution identity
- Task execution scope is unverified or belongs to another…
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)