Summary
Reloading a specific plugin through the Dashboard can unexpectedly terminate and reload every registered plugin when the requested plugin name cannot be resolved in star_registry.
The Dashboard sends a non-empty plugin ID, but PluginManager.reload() uses specified_module_path = None for both:
- no plugin was requested (intentional reload-all), and
- a plugin name was requested but no registry entry matched it.
The second case therefore silently falls through to the reload-all branch.
Versions and environment
Observed on:
- AstrBot v4.26.8
- Linux x86_64
- Python 3.12.13
- Source deployment using
python main.py
Still present in:
- latest release v4.27.1 (
ba7b315749081d0b0eea070a9f8642d04d880b3b)
- current
master at bb9b1878474e5ed65667d5c2c6b7fd6efbc51db4 (2026-08-04)
The reload() block is identical in v4.26.8, v4.27.1, and that master revision.
Reproduction steps
A deterministic core-level reproduction is:
-
Register two plugins, for example plugin_target and plugin_other.
-
Call:
await plugin_manager.reload("does_not_exist")
-
Observe that both registered plugins are terminated and unbound, followed by load(None).
The same path is reachable through the Dashboard API when a stale Dashboard card refers to a plugin that is no longer present in star_registry, for example after that plugin failed to load:
POST /api/v1/plugins/reload
Content-Type: application/json
{"plugin_id": "plugin_that_is_not_in_star_registry"}
Expected behavior
reload(None) may intentionally reload all plugins.
reload("known_plugin") should reload only that plugin.
reload("unknown_plugin") should return a plugin-not-found error and must not terminate any plugin.
Actual behavior
reload(None) reloads all plugins.
reload("known_plugin") reloads only that plugin.
reload("unknown_plugin") also reloads all plugins and can return success if the subsequent full load succeeds.
An isolated probe against the actual v4.26.8 implementation produced:
argument=None
terminated=['plugin_target', 'plugin_other']
loaded=[None]
argument='plugin_target'
terminated=['plugin_target']
loaded=['data.plugins.plugin_target.main']
argument='does_not_exist'
terminated=['plugin_target', 'plugin_other']
loaded=[None]
Sanitized logs from an observed Dashboard event
Timestamps are rounded and plugin identifiers are replaced with placeholders:
[18:07:35] AstrBot started.
[18:08:06] Terminating plugin <unrelated_plugin_1> ...
[18:08:06] Terminating plugin <unrelated_plugin_2> ...
[18:08:06] Terminating plugin <unrelated_plugin_3> ...
...
[18:08:12] POST /api/v1/plugins/reload -> 200
This was not a process restart: the AstrBot started message preceded the mass termination by about 30 seconds.
The observed target was a locally modified plugin, so its implementation details and separate load failure are deliberately excluded here. The deterministic core-level reproduction above does not execute any target plugin code: it only shows that an unresolved explicit name enters the reload-all branch.
Call chain and root cause
The Dashboard passes extension.name:
dashboard/src/views/extension/InstalledPluginsTab.vue:377
dashboard/src/api/v1.ts:1244-1247 sends { plugin_id: pluginId }
The API requires a PluginByIdRequest, extracts the non-empty plugin_id, and passes it as name:
astrbot/dashboard/api/plugins.py:837-846
astrbot/dashboard/services/plugin_service.py:154-158
In astrbot/core/star/star_manager.py:1010-1066:
specified_module_path = None
if specified_plugin_name:
for smd in star_registry:
if smd.name == specified_plugin_name:
specified_module_path = smd.module_path
break
if not specified_module_path:
# reload all plugins
...
A requested-but-unmatched name leaves specified_module_path as None, so the code cannot distinguish it from an intentional reload-all call.
The Dashboard sends a non-empty plugin identifier. When that identifier resolves in the runtime registry, only the target reloads. The dangerous behavior occurs when the explicitly supplied identifier is no longer resolvable in the runtime registry.
Relevant source in v4.27.1:
Suggested fix
Keep the two meanings separate before terminating anything:
if specified_plugin_name is None:
# intentional reload-all
...
elif specified_module_path is None:
return False, f"Plugin {specified_plugin_name} not found"
else:
# reload only the resolved plugin
...
Exact error handling should follow the project's conventions. Resolving an accepted directory/root name may also be considered if that is intended API behavior, but an unresolved explicit target should never fall back to reloading all plugins.
Suggested regression tests:
- no name -> all plugins are reloaded;
- known name -> only the target is reloaded;
- unknown name -> failure, with no plugin terminated or unbound;
- Dashboard API with an unknown
plugin_id -> error response, without global reload.
Summary
Reloading a specific plugin through the Dashboard can unexpectedly terminate and reload every registered plugin when the requested plugin name cannot be resolved in
star_registry.The Dashboard sends a non-empty plugin ID, but
PluginManager.reload()usesspecified_module_path = Nonefor both:The second case therefore silently falls through to the reload-all branch.
Versions and environment
Observed on:
python main.pyStill present in:
ba7b315749081d0b0eea070a9f8642d04d880b3b)masteratbb9b1878474e5ed65667d5c2c6b7fd6efbc51db4(2026-08-04)The
reload()block is identical in v4.26.8, v4.27.1, and thatmasterrevision.Reproduction steps
A deterministic core-level reproduction is:
Register two plugins, for example
plugin_targetandplugin_other.Call:
Observe that both registered plugins are terminated and unbound, followed by
load(None).The same path is reachable through the Dashboard API when a stale Dashboard card refers to a plugin that is no longer present in
star_registry, for example after that plugin failed to load:Expected behavior
reload(None)may intentionally reload all plugins.reload("known_plugin")should reload only that plugin.reload("unknown_plugin")should return a plugin-not-found error and must not terminate any plugin.Actual behavior
reload(None)reloads all plugins.reload("known_plugin")reloads only that plugin.reload("unknown_plugin")also reloads all plugins and can return success if the subsequent full load succeeds.An isolated probe against the actual v4.26.8 implementation produced:
Sanitized logs from an observed Dashboard event
Timestamps are rounded and plugin identifiers are replaced with placeholders:
This was not a process restart: the
AstrBot startedmessage preceded the mass termination by about 30 seconds.The observed target was a locally modified plugin, so its implementation details and separate load failure are deliberately excluded here. The deterministic core-level reproduction above does not execute any target plugin code: it only shows that an unresolved explicit name enters the reload-all branch.
Call chain and root cause
The Dashboard passes
extension.name:dashboard/src/views/extension/InstalledPluginsTab.vue:377dashboard/src/api/v1.ts:1244-1247sends{ plugin_id: pluginId }The API requires a
PluginByIdRequest, extracts the non-emptyplugin_id, and passes it asname:astrbot/dashboard/api/plugins.py:837-846astrbot/dashboard/services/plugin_service.py:154-158In
astrbot/core/star/star_manager.py:1010-1066:A requested-but-unmatched name leaves
specified_module_pathasNone, so the code cannot distinguish it from an intentional reload-all call.The Dashboard sends a non-empty plugin identifier. When that identifier resolves in the runtime registry, only the target reloads. The dangerous behavior occurs when the explicitly supplied identifier is no longer resolvable in the runtime registry.
Relevant source in v4.27.1:
Suggested fix
Keep the two meanings separate before terminating anything:
Exact error handling should follow the project's conventions. Resolving an accepted directory/root name may also be considered if that is intended API behavior, but an unresolved explicit target should never fall back to reloading all plugins.
Suggested regression tests:
plugin_id-> error response, without global reload.