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

PluginManager supports runtime reload, configuration-driven enable/disable, automatic recovery of failed plugins, health checks, and aggregate metrics.

Reloading a Single Plugin

Reload(ctx, pluginID) re-loads one plugin from disk without restarting xbot:

  1. Deactivates the plugin if it is active (StateDeactivatingDeactivateStateInactive).
  2. Releases OnConfigChanged subscriptions bound to the old plugin context.
  3. Removes the old entry and unregisters all its widgets (widgetRegistry.UnregisterAll).
  4. Re-scans the plugin’s directory (findPluginDir over DefaultPluginDirs + extra dirs) and reloads the manifest (LoadManifest).
  5. Recreates storage (NewFileStorage; falls back to noopStorage on failure) and invalidates the plugin’s config cache.
  6. Builds a fresh PluginEntry (new PluginContext, logger, widget registry) and recreates the runtime via RuntimeFactory.Create.
  7. Re-activates automatically if the manifest declares the onStart activation event.
  8. Emits PluginEventReloaded and writes an AuditReload audit entry.
if err := pm.Reload(ctx, "xbot.genui"); err != nil {
    // manifest / runtime / activation errors
}

Reloading Everything

ReloadAll(ctx) deactivates all plugins, clears the entry map, re-discovers from disk, and re-activates:

  1. Suppresses widget updates for the duration (widgetRegistry.SuppressUpdates) to avoid flooding WebSocket push buffers.
  2. DeactivateAll(ctx) — note this also stops the auto-retry goroutine.
  3. Unregisters all widgets, then replaces the entry map with a fresh one.
  4. Discover(ctx) + ActivateAll(ctx).
  5. Calls registered OnReload callbacks asynchronously (in a goroutine) so slow listeners (e.g. WebSocket widget pushes) cannot block the RPC handler.
pm.OnReload(func() { /* runs after ReloadAll */ })
if err := pm.ReloadAll(ctx); err != nil { /* discover/activate errors */ }

Config Watching

WatchConfig(configPath, interval) polls config.json and reacts to changes in plugins.disabled_plugins:

stop := pm.WatchConfig("/home/user/.xbot/config.json", 30*time.Second)
// ...
close(stop)
  • The interval is clamped to a minimum of 5 seconds.
  • Each tick compares the config file’s modification time; on change it re-reads the file and diffs the plugins.disabled_plugins list against the previous snapshot.
  • Newly disabled plugins are deactivated (StateDeactivatingDeactivateStateInactive) and added to the disabled set.
  • Newly enabled plugins are removed from the disabled set, then either re-activated in place (entry exists, state StateInactive, has onStart) or discovered from disk and activated.

Auto-Retry

SetAutoRetry(enabled, maxRetries) runs a background retry loop for plugins stuck in the error state:

pm.SetAutoRetry(true, 5) // retry up to 5 times per plugin; 0 = unlimited
  • A goroutine (retryLoop) ticks at retryInterval (default 5s; SetRetryInterval exists for tests and is not intended for production).
  • Each tick, retryErrorPlugins scans all entries; error-state plugins whose retryCount is below maxRetries are retried with exponential backoff: 1s * 2^(attempt-1), capped at 30s (retryInitialDelay / retryMaxDelay).
  • A retry sets the entry to StateDiscovered and calls activate. On success the retry counter and lastError are reset, and PluginEventActivated is emitted with {"recovered": true, "attempt": n}; on failure lastError/lastErrorAt are recorded and the plugin’s error callback is invoked via notifyPluginError.

Important: DeactivateAll (and therefore ReloadAll) stops the auto-retry goroutine and sets autoRetry = false. If you activate plugins manually after DeactivateAll, call SetAutoRetry again to restore automatic recovery.

Health Checks

Plugins can implement the optional HealthChecker interface:

type HealthChecker interface {
    HealthCheck(ctx context.Context) error
}
results := pm.HealthCheck(ctx) // map[pluginID]error — nil means healthy

Only ACTIVE plugins are checked; plugins that do not implement HealthChecker are reported as healthy (nil error).

Metrics

Metrics() returns aggregate plugin-system counters:

type PluginMetrics struct {
    TotalPlugins   int   `json:"total_plugins"`
    ActivePlugins  int   `json:"active_plugins"`
    TotalTools     int   `json:"total_tools"`
    TotalHooks     int   `json:"total_hooks"`
    TotalEnrichers int   `json:"total_enrichers"`
    ToolCallCount  int64 `json:"tool_call_count"` // runtime cumulative tool executions
    HookCallCount  int64 `json:"hook_call_count"` // runtime cumulative hook dispatches
}

Tool/hook counts and call counters are aggregated from the PluginContext of ACTIVE plugins only. String() prints a compact summary: PluginManager{total=5, active=3, error=1, disabled=1}.

See Also