PluginManager supports runtime reload, configuration-driven enable/disable, automatic recovery of failed plugins, health checks, and aggregate metrics.
Reload(ctx, pluginID) re-loads one plugin from disk without restarting xbot:
- Deactivates the plugin if it is active (
StateDeactivating→Deactivate→StateInactive). - Releases
OnConfigChangedsubscriptions bound to the old plugin context. - Removes the old entry and unregisters all its widgets (
widgetRegistry.UnregisterAll). - Re-scans the plugin’s directory (
findPluginDiroverDefaultPluginDirs+ extra dirs) and reloads the manifest (LoadManifest). - Recreates storage (
NewFileStorage; falls back tonoopStorageon failure) and invalidates the plugin’s config cache. - Builds a fresh
PluginEntry(newPluginContext, logger, widget registry) and recreates the runtime viaRuntimeFactory.Create. - Re-activates automatically if the manifest declares the
onStartactivation event. - Emits
PluginEventReloadedand writes anAuditReloadaudit entry.
if err := pm.Reload(ctx, "xbot.genui"); err != nil {
// manifest / runtime / activation errors
}
ReloadAll(ctx) deactivates all plugins, clears the entry map, re-discovers from disk, and re-activates:
- Suppresses widget updates for the duration (
widgetRegistry.SuppressUpdates) to avoid flooding WebSocket push buffers. DeactivateAll(ctx)— note this also stops the auto-retry goroutine.- Unregisters all widgets, then replaces the entry map with a fresh one.
Discover(ctx)+ActivateAll(ctx).- Calls registered
OnReloadcallbacks 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 */ }
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_pluginslist against the previous snapshot. - Newly disabled plugins are deactivated (
StateDeactivating→Deactivate→StateInactive) and added to thedisabledset. - Newly enabled plugins are removed from the disabled set, then either re-activated in place (entry exists, state
StateInactive, hasonStart) or discovered from disk and activated.
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 atretryInterval(default 5s;SetRetryIntervalexists for tests and is not intended for production). - Each tick,
retryErrorPluginsscans all entries; error-state plugins whoseretryCountis belowmaxRetriesare retried with exponential backoff:1s * 2^(attempt-1), capped at 30s (retryInitialDelay/retryMaxDelay). - A retry sets the entry to
StateDiscoveredand callsactivate. On success the retry counter andlastErrorare reset, andPluginEventActivatedis emitted with{"recovered": true, "attempt": n}; on failurelastError/lastErrorAtare recorded and the plugin’s error callback is invoked vianotifyPluginError.
Important:
DeactivateAll(and thereforeReloadAll) stops the auto-retry goroutine and setsautoRetry = false. If you activate plugins manually afterDeactivateAll, callSetAutoRetryagain to restore automatic recovery.
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() 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}.
- Plugin Lifecycle — activation states and events
- Logging & Audit — reload operations are audited
- Configuration — hot reload of plugin config