{"record":{"id":"d12dc282fc032b43","repo":"Hmbown/CodeWhale","slug":"task-id-already-exists","errorCode":null,"errorMessage":"Task id already exists: {}","messagePattern":"Task id already exists: (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/task_manager.rs","lineNumber":1299,"sourceCode":"            tool_calls: Vec::new(),\n            timeline: vec![TaskTimelineEntry {\n                timestamp: Utc::now(),\n                kind: \"queued\".to_string(),\n                summary: \"Task queued\".to_string(),\n                detail_path: None,\n            }],\n        };\n\n        {\n            let mut state = self.state.lock().await;\n            let task_path = self.tasks_dir.join(format!(\"{}.json\", task.id));\n            // The staged extension is intentionally not `.json`, so startup\n            // replay ignores an interrupted create until the queue write has\n            // succeeded and this file is atomically promoted.\n            let staged_task_path = self.tasks_dir.join(format!(\".{}.json.pending\", task.id));\n            if state.tasks.contains_key(&task.id) || task_path.exists() || staged_task_path.exists()\n            {\n                bail!(\"Task id already exists: {}\", task.id);\n            }\n            let mut next_queue = state.queue.clone();\n            next_queue.push_back(task.id.clone());\n\n            // Stage the owner record, then persist its queue membership, then\n            // atomically promote it. A crash before promotion leaves either an\n            // ignored staged file or a queue entry with no task (which replay\n            // drops); a crash after promotion leaves the complete runnable\n            // pair. In-memory scheduling is published only after all three.\n            write_json_atomic(&staged_task_path, &task)?;\n            if let Err(err) = self.persist_queue_locked(&next_queue) {\n                if let Err(cleanup_err) = fs::remove_file(&staged_task_path) {\n                    tracing::warn!(\n                        task_id = %task.id,\n                        error = %cleanup_err,\n                        \"failed to remove ignored staged task after queue write failure\"\n                    );\n                }","sourceCodeStart":1281,"sourceCodeEnd":1317,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/task_manager.rs#L1281-L1317","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Allocate a fresh id (new UUIDv4-derived task_<16hex>) for every create attempt instead of reusing the preallocated one after a failure.","If a previous attempt definitely failed and left debris, remove tasks/.<id>.json.pending manually, then create with a new id.","Never catch this error and retry unchanged; the id space collision is deterministic until state changes."],"exampleFix":"// before\nloop {\n    match tm.add_task_with_id(req.clone(), id.clone()).await {\n        Ok(t) => break t, // retries the SAME id -> 'Task id already exists'\n        Err(_) => continue,\n    }\n}\n\n// after\nloop {\n    let id = format!(\"task_{}\", &Uuid::new_v4().simple().to_string()[..16]);\n    match tm.add_task_with_id(req.clone(), id).await {\n        Ok(t) => break t,\n        Err(e) if e.to_string().contains(\"already exists\") => continue,\n        Err(e) => return Err(e),\n    }\n}","handlingStrategy":"validation","validationCode":"// Before retrying a create, confirm the id is truly free:\nfn task_id_free(tasks_dir: &Path, tasks: &HashMap<String, TaskRecord>, id: &str) -> bool {\n    !tasks.contains_key(id)\n        && !tasks_dir.join(format!(\"{id}.json\")).exists()\n        && !tasks_dir.join(format!(\".{id}.json.pending\")).exists()\n}","typeGuard":null,"tryCatchPattern":"match tm.add_task_with_id(req, id).await {\n    Err(e) if e.to_string().contains(\"Task id already exists\") => {\n        let id = new_task_id(); // regenerate and retry once\n        tm.add_task_with_id(req, id).await\n    }\n    other => other,\n}","preventionTips":["Mint a fresh id per create attempt; never reuse a preallocated id after any failure.","After crashes, inspect tasks_dir for .pending debris before re-registering ids.","Keep one writer per tasks_dir to avoid cross-process duplicates."],"tags":["task","duplicate","idempotency","rust"],"backgroundTag":"duplicate-key-conflict","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}