Hmbown/CodeWhale · error · anyhow::Error

Task id already exists: {}

Error message

Task id already exists: {}

What it means

Duplicate-id guard in the staged create transaction: the id is rejected if it is already in the in-memory task map, or if tasks/<id>.json exists, or if a staged .<id>.json.pending file exists. The staged extension is deliberately not .json so startup replay ignores interrupted creates; this triple check prevents a retry from overwriting a durable record or resurrecting a half-finished one.

Source

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

            tool_calls: Vec::new(),
            timeline: vec![TaskTimelineEntry {
                timestamp: Utc::now(),
                kind: "queued".to_string(),
                summary: "Task queued".to_string(),
                detail_path: None,
            }],
        };

        {
            let mut state = self.state.lock().await;
            let task_path = self.tasks_dir.join(format!("{}.json", task.id));
            // The staged extension is intentionally not `.json`, so startup
            // replay ignores an interrupted create until the queue write has
            // succeeded and this file is atomically promoted.
            let staged_task_path = self.tasks_dir.join(format!(".{}.json.pending", task.id));
            if state.tasks.contains_key(&task.id) || task_path.exists() || staged_task_path.exists()
            {
                bail!("Task id already exists: {}", task.id);
            }
            let mut next_queue = state.queue.clone();
            next_queue.push_back(task.id.clone());

            // Stage the owner record, then persist its queue membership, then
            // atomically promote it. A crash before promotion leaves either an
            // ignored staged file or a queue entry with no task (which replay
            // drops); a crash after promotion leaves the complete runnable
            // pair. In-memory scheduling is published only after all three.
            write_json_atomic(&staged_task_path, &task)?;
            if let Err(err) = self.persist_queue_locked(&next_queue) {
                if let Err(cleanup_err) = fs::remove_file(&staged_task_path) {
                    tracing::warn!(
                        task_id = %task.id,
                        error = %cleanup_err,
                        "failed to remove ignored staged task after queue write failure"
                    );
                }

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Allocate a fresh id (new UUIDv4-derived task_<16hex>) for every create attempt instead of reusing the preallocated one after a failure.
  2. If a previous attempt definitely failed and left debris, remove tasks/.<id>.json.pending manually, then create with a new id.
  3. Never catch this error and retry unchanged; the id space collision is deterministic until state changes.

Example fix

// before
loop {
    match tm.add_task_with_id(req.clone(), id.clone()).await {
        Ok(t) => break t, // retries the SAME id -> 'Task id already exists'
        Err(_) => continue,
    }
}

// after
loop {
    let id = format!("task_{}", &Uuid::new_v4().simple().to_string()[..16]);
    match tm.add_task_with_id(req.clone(), id).await {
        Ok(t) => break t,
        Err(e) if e.to_string().contains("already exists") => continue,
        Err(e) => return Err(e),
    }
}
Defensive patterns

Strategy: validation

Validate before calling

// Before retrying a create, confirm the id is truly free:
fn task_id_free(tasks_dir: &Path, tasks: &HashMap<String, TaskRecord>, id: &str) -> bool {
    !tasks.contains_key(id)
        && !tasks_dir.join(format!("{id}.json")).exists()
        && !tasks_dir.join(format!(".{id}.json.pending")).exists()
}

Try / catch

match tm.add_task_with_id(req, id).await {
    Err(e) if e.to_string().contains("Task id already exists") => {
        let id = new_task_id(); // regenerate and retry once
        tm.add_task_with_id(req, id).await
    }
    other => other,
}

Prevention

When it happens

Trigger: Calling add_task_with_id twice with the same preallocated id; retrying the register step after a partial failure that left a .pending file; a genuine id collision from a weak external generator.

Common situations: Agent flows that preallocate an id, fail downstream, and retry the whole transaction with the same id; crash-recovery scenarios where the staged file was not cleaned; ids reused across process restarts.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/d12dc282fc032b43. Report an issue: GitHub.