{"record":{"id":"240bdb16da62a322","repo":"tinyhumansai/openhuman","slug":"dependency-cycle-detected","errorCode":null,"errorMessage":"dependency cycle detected","messagePattern":"dependency cycle detected","errorType":"validation","errorClass":"TeamError","httpStatus":null,"severity":"error","filePath":"src/openhuman/agent/orchestration/agent_teams/ops.rs","lineNumber":416,"sourceCode":"    existing: &[AgentTeamTask],\n) -> Result<()> {\n    let known: HashSet<&str> = existing.iter().map(|t| t.id.as_str()).collect();\n\n    for dep in depends_on {\n        if dep == new_task_id {\n            return Err(anyhow!(TeamError::SelfDependency {\n                task_id: new_task_id.to_string(),\n            }));\n        }\n        if !known.contains(dep.as_str()) {\n            return Err(anyhow!(TeamError::UnknownDependency {\n                depends_on: dep.clone(),\n            }));\n        }\n    }\n\n    if has_task_cycle(new_task_id, depends_on, existing) {\n        return Err(anyhow!(TeamError::CyclicDependency));\n    }\n\n    Ok(())\n}\n\n/// Kahn's-algorithm cycle check over the task dependency graph (existing tasks\n/// plus the candidate new task). Edge `dep -> task` means `task` depends on\n/// `dep`. Edges pointing at unknown ids are ignored here (rejected separately).\nfn has_task_cycle(\n    new_task_id: &str,\n    new_depends_on: &[String],\n    existing: &[AgentTeamTask],\n) -> bool {\n    // Node set: every existing task id plus the new one.\n    let mut nodes: HashSet<&str> = existing.iter().map(|t| t.id.as_str()).collect();\n    nodes.insert(new_task_id);\n\n    let mut indegree: HashMap<&str, usize> = nodes.iter().map(|&n| (n, 0)).collect();","sourceCodeStart":398,"sourceCodeEnd":434,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/a221052e0df5b1f7598fceba7329fd1af95d6699/src/openhuman/agent/orchestration/agent_teams/ops.rs#L398-L434","documentation":"Thrown by agent_teams::ops::validate_dependencies when has_task_cycle — a Kahn's-algorithm topological sort over the existing tasks plus the candidate new task — cannot exhaust the graph, i.e. the combined dependency graph contains a cycle. It wraps TeamError::CyclicDependency and fires after self-dep and unknown-dep checks pass.","triggerScenarios":"Assigning a task whose depends_on closes a loop: the new task depends on an existing task that (transitively) depends back on it, or the stored existing graph already contains a cycle that the new edges complete/expose. Requires the dep ids to all be known and non-self, otherwise the earlier checks fire instead.","commonSituations":"LLM-generated plans adding back-references; two tasks authored to wait on each other; a previously corrupted ledger where tasks were inserted with cyclic deps bypassing validation.","solutions":["Map the existing team's task graph (list tasks + their deps) and remove the back-edge before assigning","Break the loop by making one direction a separate follow-up task with no reverse dep","If the existing graph itself is cyclic (data corruption), rebuild the team's tasks"],"exampleFix":"// before\n// taskB already depends on taskA\nagent_teams::ops::assign_task(&config, &team, \"taskA-v2\", member, &[\"taskB\".into()])?; // closes cycle A->B->A\n\n// after — cut the back-edge before adding the new node\nlet deps: Vec<String> = depends_on\n    .into_iter()\n    .filter(|d| !task_transitively_depends_on(&config, &team, d, &new_task_id))\n    .collect();\nagent_teams::ops::assign_task(&config, &team, &new_task_id, member, &deps)?;","handlingStrategy":"validation","validationCode":"// Client-side Kahn check mirroring has_task_cycle: build edges dep -> task over\n// existing tasks plus the candidate, then ensure a full topological order exists.\nfn would_cycle(new_id: &str, new_deps: &[String], existing: &[AgentTeamTask]) -> bool {\n    let mut indeg: HashMap<&str, usize> = HashMap::new();\n    let mut edges: HashMap<&str, Vec<&str>> = HashMap::new();\n    let ids = existing.iter().map(|t| t.id.as_str()).chain([new_id]);\n    for id in ids { indeg.entry(id).or_insert(0); }\n    for t in existing { for d in &t.depends_on { edges.entry(d.as_str()).or_default().push(t.id.as_str()); *indeg.get_mut(t.id.as_str()).unwrap() += 1; } }\n    for d in new_deps { edges.entry(d.as_str()).or_default().push(new_id); *indeg.get_mut(new_id).unwrap() += 1; }\n    let mut q: Vec<&str> = indeg.iter().filter(|(_, &d)| d == 0).map(|(k, _)| *k).collect();\n    let mut seen = 0;\n    while let Some(n) = q.pop() { seen += 1; for &m in edges.get(n).into_iter().flatten() { if let Some(d) = indeg.get_mut(m) { *d -= 1; if *d == 0 { q.push(m); } } } }\n    seen != indeg.len()\n}","typeGuard":null,"tryCatchPattern":"match agent_teams::ops::assign_task(&config, &team_id, &task_id, member, &depends_on) {\n    Ok(_) => Ok(()),\n    Err(e) if e.to_string().contains(\"dependency cycle\") => {\n        // drop the back-edge and re-submit, or split the mutually-dependent work\n        assign_without_back_edge(&config, &team_id, &task_id, member, &depends_on)\n    }\n    Err(e) => Err(e),\n}","preventionTips":["Model the task graph (DAG) in the caller and reject back-edges at authoring time","If two tasks genuinely wait on each other, merge them or introduce an intermediate task","Audit the stored task graph for pre-existing cycles when this error appears with surprising input"],"tags":["agent-teams","tasks","dependencies","cycle-detection"],"backgroundTag":null,"analyzedSha":"a221052e0df5b1f7598fceba7329fd1af95d6699","analyzedAt":"2026-08-16T12:47:06.542Z","schemaVersion":2},"datasetVersion":"2026-08-16T13:17:31.715Z"}