Skip to main content
xbot
Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Toggle Dark/Light/Auto mode Back to homepage
Edit page

Understand the plugin lifecycle: discovery, activation, deactivation, and everything in between.

Lifecycle States

Discovered → Activating → Active → Deactivating → Inactive
                ↓                          ↑
              Error ←──────────────────────┘
StateConstantDescription
DiscoveredStateDiscoveredManifest loaded, not yet activated
ActivatingStateActivatingActivate() in progress
ActiveStateActivePlugin is running and contributing
InactiveStateInactiveDisabled by user or config
ErrorStateErrorActivation failed or runtime error

Discovery

PluginManager.Discover() scans plugin directories:

  1. Scans ~/.xbot/plugins/ and ~/.xbot/plugins/builtin/
  2. For each subdirectory, loads plugin.json via LoadManifest()
  3. Validates the manifest (ID format, version, runtime type)
  4. Creates a runtime instance via RuntimeFactory.Create()
  5. Resolves dependency activation order (topological sort)

Disabled plugins (listed in config.jsonplugins.disabled_plugins) stay in the entries map as StateInactive — they’re visible in the plugin panel but not activated.

Activation Events

The activation_events field in the manifest controls when a plugin activates:

EventDescription
onStartActivate when xbot starts (default if empty)
onTool:<name>Activate when tool <name> is first called
onHook:<event>Activate when hook event <event> fires
onCommand:<cmd>Activate when command /<cmd> is invoked

Lazy activation: plugins with onTool/onHook/onCommand events are only activated when the event first occurs, saving resources.

Activation Process

When a plugin activates:

  1. State transition: StateDiscoveredStateActivating
  2. Context creation: A PluginContext is created with the plugin’s declared permissions
  3. Activate(ctx) called: The plugin registers its capabilities:
    • ctx.RegisterTool(tool) — Register tools
    • ctx.OnPreToolUse(matcher, handler) — Subscribe to hooks
    • ctx.ContributeUI(widgetID, zone, widget, priority) — Register widgets
    • ctx.Subscribe(topic, handler) — Subscribe to event bus
    • ctx.ScheduleCron(spec) — Schedule cron jobs
  4. Wiring: Capabilities are wired into xbot’s registries
  5. State transition: StateActivatingStateActive

If Activate() returns an error or panics:

  • State → StateError
  • Error is logged
  • Auto-retry may attempt reactivation (if enabled)

Deactivation

Deactivation occurs when:

  • xbot shuts down
  • User disables the plugin via config
  • WatchConfig detects the plugin was added to disabled_plugins
  • Plugin is reloaded

The Deactivate(ctx) method is called, which should:

  • Clean up resources (goroutines, file handles, network connections)
  • Unregister tools, hooks, and widgets (handled automatically by the PluginManager)
  • Flush any pending data to storage

Auto-Retry

Failed plugins can automatically retry activation with exponential backoff:

  • Initial delay: 1 second
  • Maximum delay: 30 seconds
  • Backoff multiplier: 2x
  • Enabled via SetAutoRetry(true, maxRetries)

DeactivateAll() cancels the retry context. If you call activate() manually after DeactivateAll(), you must re-enable auto-retry.

Hot Reload

WatchConfig polls config.json every 30 seconds (configurable, minimum 5s):

  1. Compares plugins.disabled_plugins lists
  2. Reactively deactivates newly disabled plugins
  3. Reactively activates newly enabled plugins

Returns a stop channel for graceful shutdown.

Plugin Entry States

type PluginState int

const (
    StateDiscovered PluginState = iota  // Manifest loaded
    StateActivating                      // Activate() in progress
    StateActive                          // Running
    StateInactive                        // Disabled
    StateError                           // Failed
)

State Transitions in Code

// Discovery → Discovered
entry := pm.newEntry(manifest, pluginDir, plugin)
entry.State = StateDiscovered

// Activation → Activating → Active
entry.State = StateActivating
if err := plugin.Activate(entry.Context); err != nil {
    entry.State = StateError
} else {
    entry.State = StateActive
}

// Deactivation → Inactive
plugin.Deactivate(entry.Context)
entry.State = StateInactive

See Also