affaan-m/ECC · error
agent profile inheritance cycle
Error message
agent profile inheritance cycle: {} What it means
This error is raised while resolving an agent profile's inheritance chain: the profile being resolved already appears in the current chain, meaning A inherits from B which (directly or transitively) inherits back to A. The library detects the loop before it would recurse forever, and the message includes the full cycle path (e.g. `a -> b -> c -> a`) so the offending links are visible. It is a guard against malformed profile inheritance graphs.
Solutions
- Break the cycle: edit the config so the inheritance chain is acyclic — the message's path shows exactly which profiles form the loop.
- Remove any self-reference where a profile's `extends`/base field names itself.
- Rename the base profile referenced in the cycle to a genuinely independent root profile, or inline its values into the child.
- Add a startup config lint that runs resolution for every agent profile and fails fast with cycle detection before runtime use.
Example fix
# before (cyclic) [agent_profiles.reviewer] extends = "builder" [agent_profiles.builder] extends = "reviewer" # cycle: reviewer -> builder -> reviewer # after [agent_profiles.reviewer] extends = "builder" [agent_profiles.builder] # no extends; standalone base profile
Defensive patterns
Strategy: validation
Validate before calling
fn profile_graph_is_acyclic(profiles: &HashMap<String, AgentProfile>) -> bool {
fn visit(n: &str, p: &HashMap<String, AgentProfile>, seen: &mut Vec<String>) -> bool {
if seen.contains(&n.to_string()) { return false; }
if let Some(pr) = p.get(n) {
if let Some(base) = &pr.extends {
seen.push(n.to_string());
return visit(base, p, seen);
}
}
true
}
profiles.keys().all(|n| visit(n, profiles, &mut vec![]))
} Try / catch
match config.resolve_agent_profile(name, &mut chain) {
Ok(p) => p,
Err(e) if e.to_string().contains("inheritance cycle") => {
eprintln!("Fix profile extends chain: {e}");
default_profile()
}
Err(e) => return Err(e),
} Prevention
- Keep inheritance shallow (at most 2 levels) so cycles are easy to spot.
- Never point a profile's extends at a profile that transitively extends it; draw the graph when editing.
- Run a startup check that resolves every profile once.
- Use a naming convention (e.g. `base-*` for leaf profiles) to signal which profiles are safe bases.
When it happens
Trigger: Calling profile resolution (`resolve_agent_profile`-style API with a `chain` accumulator) on a name that is already present in `chain`, i.e. the `extends`/`base` references among agent profiles form a directed cycle.
Common situations: Hand-editing config where profile X was changed to extend profile Y and Y was simultaneously repointed to extend X; merging config branches where two profiles each adopted the other as a parent; typos where a profile extends itself (`extends = "itself"`); renaming a base profile so an old `extends` value now points back into the chain.
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
- candidate configuration exceeds 1 MiB
- missing orchestration template variable(s)
- orchestration template
- Unknown agent profile
- worktree_branch_prefix cannot be empty
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/3890510ffdb12957.
Report an issue: GitHub.
Appendix: source
Thrown at ecc2/src/config/mod.rs:473
}
Ok(ResolvedOrchestrationTemplate {
template_name: name.to_string(),
description,
project,
task_group,
steps,
})
}
fn resolve_agent_profile_inner(
&self,
name: &str,
chain: &mut Vec<String>,
) -> Result<ResolvedAgentProfile> {
if chain.iter().any(|existing| existing == name) {
chain.push(name.to_string());
anyhow::bail!("agent profile inheritance cycle: {}", chain.join(" -> "));
}
let profile = self
.agent_profiles
.get(name)
.ok_or_else(|| anyhow::anyhow!("Unknown agent profile: {name}"))?;
chain.push(name.to_string());
let mut resolved = if let Some(parent) = profile.inherits.as_deref() {
self.resolve_agent_profile_inner(parent, chain)?
} else {
ResolvedAgentProfile::default()
};
chain.pop();
resolved.apply(name, profile);
Ok(resolved)
}View on GitHub (pinned to 8321021c54)