m1k1o/neko · error
cyclical dependency detected: '%s' <-> '%s'
Error message
cyclical dependency detected: '%s' <-> '%s'
What it means
Returned by addPlugin when wiring a plugin's dependencies would create a dependency cycle. The code detects the cycle (dependsOn.findPlugin finds the starting plugin reachable), breaks the link (sets dependsOn.dependsOn = nil and deletes the entry) to keep the graph acyclic, and returns this error. The error names the two plugins forming the cycle.
Source
Thrown at server/internal/plugins/dependency.go:92
plug.plugin = plugin
plug.logger = d.logger
d.deps[pluginName] = plug
dplug, ok := plugin.(types.DependablePlugin)
if !ok {
return nil
}
for _, depName := range dplug.DependsOn() {
dependsOn, ok := d.deps[depName]
if !ok {
dependsOn = &dependency{}
} else if dependsOn.plugin != nil {
// if there is a cyclical dependency, break it and return error
if tdep, ok := dependsOn.findPlugin(pluginName); ok {
dependsOn.dependsOn = nil
delete(d.deps, pluginName)
return fmt.Errorf("cyclical dependency detected: '%s' <-> '%s'", pluginName, tdep.plugin.Name())
}
}
plug.dependsOn = append(plug.dependsOn, dependsOn)
d.deps[depName] = dependsOn
}
return nil
}
func (d *dependiencies) findPlugin(name string) (*dependency, bool) {
for _, dep := range d.deps {
plug, ok := dep.findPlugin(name)
if ok {
return plug, true
}
}
return nil, falseView on GitHub (pinned to b0f01cedea)
Solutions
- Break the cycle in plugin configuration: remove the dependency edge that makes A depend on B while B depends on A
- Extract shared logic into a third plugin both can depend on, restoring a DAG
- Fix the plugin's dependency declaration (the DependsOn/requirements list) so a plugin never lists something that already depends on it
- After this error, note the graph was mutated (cycle broken, entry deleted) — re-initialize the plugin manager before retrying with corrected deps
Example fix
// before pluginA.DependsOn = ["B"] pluginB.DependsOn = ["A"] // cycle: A <-> B // after pluginA.DependsOn = ["B"] pluginB.DependsOn = [] // cycle removed // or: shared plugin C; A -> C, B -> C
Defensive patterns
Strategy: validation
Validate before calling
// before adding, ensure no registered plugin already (transitively) depends on p
func createsCycle(reg map[string]*dependency, p types.Plugin) bool {
var visit func(name string) bool
visit = func(name string) bool {
d, ok := reg[name]
if !ok || d.plugin == nil { return false }
if d.plugin.Name() == p.Name() { return true }
for _, dep := range d.dependsOn {
if visit(dep.Name()) { return true }
}
return false
}
for _, name := range p.Dependencies() {
if visit(name) { return true }
}
return false
} Try / catch
if err := manager.plugins.addPlugin(p); err != nil {
if strings.Contains(err.Error(), "cyclical dependency") {
log.Error().Str("plugin", p.Name()).Err(err).Msg("fix plugin dependency graph")
}
return err
} Prevention
- Keep plugin dependencies a strict DAG; never let a plugin depend on something that depends on it
- Extract shared functionality into a lower-level plugin both depend on
- Test the full dependency graph for cycles in CI before deploying
When it happens
Trigger: Calling addPlugin(p) where p declares (via its dependency list) a dependency on a plugin that transitively already depends on p — e.g. plugin A depends on B, then adding B which depends on A.
Common situations: Refactoring plugins so two features start requiring each other; copy-pasting a DependsOn list that includes the registering plugin itself or its ancestor; config where two plugins mutually list each other as dependencies.
Related errors
- plugin '%s' already added
- unable to unmarshal %s plugin settings from global settings:
- unable to unmarshal %s plugin settings from profile: %w
- not a valid plugin
- failed to add plugin: %w
AI-assisted analysis of m1k1o/neko@b0f01cedea (2026-09-01).
Data as JSON: /api/errors/bf90b6509300dfac.
Report an issue: GitHub.