m1k1o/neko · error

cyclical dependency detected

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.

Solutions

  1. Break the cycle in plugin configuration: remove the dependency edge that makes A depend on B while B depends on A
  2. Extract shared logic into a third plugin both can depend on, restoring a DAG
  3. Fix the plugin's dependency declaration (the DependsOn/requirements list) so a plugin never lists something that already depends on it
  4. 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

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


AI-assisted analysis of m1k1o/neko@b0f01cedea (2026-09-01). Data as JSON: /api/errors/bf90b6509300dfac. Report an issue: GitHub.

Appendix: 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, false

View on GitHub (pinned to b0f01cedea)