fix(plugins): preserve channel webhook routes across registry replacements#47676
fix(plugins): preserve channel webhook routes across registry replacements#47676TurboTheTurtle wants to merge 3 commits intoopenclaw:mainfrom
Conversation
…ments Channel plugins (e.g. BlueBubbles) register HTTP webhook routes via requireActivePluginRegistry() during channel startup. However, the per-agent plugin loader calls setActivePluginRegistry() with a fresh registry for each agent, orphaning previously registered httpRoutes. The gateway HTTP handler then reads from the latest registry and finds zero routes, returning 404 for all webhook POSTs. Two changes: 1. setActivePluginRegistry() now carries forward httpRoutes from the previous registry when the new one has none, preserving channel- registered webhook routes across loader cycles. 2. createGatewayPluginRequestHandler() reads routes from requireActivePluginRegistry() (current) instead of the stale snapshot captured at handler creation time. Fixes openclaw#32275
Greptile SummaryThis PR fixes a real bug where channel webhook routes (e.g. BlueBubbles) registered via Key concerns:
Confidence Score: 3/5
Prompt To Fix All With AIThis is a comment left during a code review.
Path: src/plugins/runtime.ts
Line: 34
Comment:
**Caller's registry object is mutated in-place**
`registry.httpRoutes = prevRoutes` directly modifies the object passed by the caller. If the caller (e.g. the per-agent plugin loader) retains a reference to that registry — or if the same registry instance is reused across multiple `setActivePluginRegistry` calls due to the `cacheKey` mechanism — this silent mutation can produce surprising state. The carried-forward routes now live on what the caller believes is a "fresh, empty" registry, which can cause false-positive "route already registered" diagnostics on the next cycle when the channel plugin tries to re-register the same path.
A safer approach avoids mutating the caller's object:
```suggestion
registry = { ...registry, httpRoutes: prevRoutes };
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: src/gateway/server/plugins-http.ts
Line: 67
Comment:
**Dead `registry` parameter in public function signature**
`registry` is renamed to `_registry` and is never referenced inside the function. The parameter is still part of the exported public API (`params: { registry: PluginRegistry; ... }`), which means every call-site is required to pass a registry that has absolutely no effect. This misleads callers into believing the provided registry matters for route matching.
Consider removing the `registry` field from the `params` type and the destructuring entirely, and updating call-sites to stop passing it — or, if backward compatibility is needed for now, at minimum add an inline comment on the type noting that this field is intentionally ignored.
How can I resolve this? If you propose a fix, please make it concise.Last reviewed commit: 521d117 |
src/plugins/runtime.ts
Outdated
| prevRoutes.length > 0 && | ||
| (!registry.httpRoutes || registry.httpRoutes.length === 0) | ||
| ) { | ||
| registry.httpRoutes = prevRoutes; |
There was a problem hiding this comment.
Caller's registry object is mutated in-place
registry.httpRoutes = prevRoutes directly modifies the object passed by the caller. If the caller (e.g. the per-agent plugin loader) retains a reference to that registry — or if the same registry instance is reused across multiple setActivePluginRegistry calls due to the cacheKey mechanism — this silent mutation can produce surprising state. The carried-forward routes now live on what the caller believes is a "fresh, empty" registry, which can cause false-positive "route already registered" diagnostics on the next cycle when the channel plugin tries to re-register the same path.
A safer approach avoids mutating the caller's object:
| registry.httpRoutes = prevRoutes; | |
| registry = { ...registry, httpRoutes: prevRoutes }; |
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/plugins/runtime.ts
Line: 34
Comment:
**Caller's registry object is mutated in-place**
`registry.httpRoutes = prevRoutes` directly modifies the object passed by the caller. If the caller (e.g. the per-agent plugin loader) retains a reference to that registry — or if the same registry instance is reused across multiple `setActivePluginRegistry` calls due to the `cacheKey` mechanism — this silent mutation can produce surprising state. The carried-forward routes now live on what the caller believes is a "fresh, empty" registry, which can cause false-positive "route already registered" diagnostics on the next cycle when the channel plugin tries to re-register the same path.
A safer approach avoids mutating the caller's object:
```suggestion
registry = { ...registry, httpRoutes: prevRoutes };
```
How can I resolve this? If you propose a fix, please make it concise.
src/gateway/server/plugins-http.ts
Outdated
| log: SubsystemLogger; | ||
| }): PluginHttpRequestHandler { | ||
| const { registry, log } = params; | ||
| const { registry: _registry, log } = params; |
There was a problem hiding this comment.
Dead registry parameter in public function signature
registry is renamed to _registry and is never referenced inside the function. The parameter is still part of the exported public API (params: { registry: PluginRegistry; ... }), which means every call-site is required to pass a registry that has absolutely no effect. This misleads callers into believing the provided registry matters for route matching.
Consider removing the registry field from the params type and the destructuring entirely, and updating call-sites to stop passing it — or, if backward compatibility is needed for now, at minimum add an inline comment on the type noting that this field is intentionally ignored.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/server/plugins-http.ts
Line: 67
Comment:
**Dead `registry` parameter in public function signature**
`registry` is renamed to `_registry` and is never referenced inside the function. The parameter is still part of the exported public API (`params: { registry: PluginRegistry; ... }`), which means every call-site is required to pass a registry that has absolutely no effect. This misleads callers into believing the provided registry matters for route matching.
Consider removing the `registry` field from the `params` type and the destructuring entirely, and updating call-sites to stop passing it — or, if backward compatibility is needed for now, at minimum add an inline comment on the type noting that this field is intentionally ignored.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 521d117ccd
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
src/plugins/runtime.ts
Outdated
| prevRoutes && | ||
| prevRoutes.length > 0 && | ||
| (!registry.httpRoutes || registry.httpRoutes.length === 0) | ||
| ) { | ||
| registry.httpRoutes = prevRoutes; |
There was a problem hiding this comment.
Allow clearing plugin HTTP routes on registry replacement
setActivePluginRegistry now unconditionally reuses the previous httpRoutes array whenever the new registry has none, which makes route removal impossible via normal plugin reloads. Since loadOpenClawPlugins() always calls setActivePluginRegistry (src/plugins/loader.ts:627), reloading with a config/workspace that no longer provides HTTP routes will still keep old handlers alive, so disabled/removed webhook endpoints can continue to be served until process restart.
Useful? React with 👍 / 👎.
…eter - Clone registry instead of mutating caller's object in setActivePluginRegistry() to avoid surprising state when the loader retains a reference - Mark registry param as @deprecated in createGatewayPluginRequestHandler() since routes are now read from the active registry at request time
Address Codex review concern about disabled channels retaining stale routes. The carry-forward is safe because channel providers call their unregister() teardown when stopped (e.g. on config reload that disables a channel), which removes their entries from the shared httpRoutes array.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8cbc8db4b9
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| prevRoutes.length > 0 && | ||
| (!registry.httpRoutes || registry.httpRoutes.length === 0) | ||
| ) { | ||
| registry = { ...registry, httpRoutes: prevRoutes }; |
There was a problem hiding this comment.
Clear stale HTTP routes when registry is replaced
This fallback keeps the previous httpRoutes array whenever a replacement registry has none, so unloading/disabling plugins cannot remove their HTTP endpoints during normal reloads. In practice, routes added via api.registerHttpRoute(...) are static plugin-load registrations (for example in extensions/nostr/index.ts) and do not run a channel-level unregister() teardown, so those handlers can remain reachable after the plugin is turned off until the process restarts. Fresh evidence: static plugin routes are registered at load time, not through the dynamic channel lifecycle assumed by this comment.
Useful? React with 👍 / 👎.
|
Thanks for the diagnosis here. I landed a replacement fix in #47902 because the current patch still leaves two correctness gaps:
Closing this as superseded by #47902. |
Problem
Channel plugins (e.g. BlueBubbles) register HTTP webhook routes via
requireActivePluginRegistry()during channel startup. However, the per-agent plugin loader callssetActivePluginRegistry()with a fresh registry for each agent, orphaning previously registeredhttpRoutes. The gateway HTTP handler then reads from the latest registry and finds zero routes, returning 404 for all webhook POSTs.This breaks inbound message delivery for any channel that relies on webhook HTTP routes (BlueBubbles being the primary one).
Root Cause
createGatewayPluginRequestHandler()captures a registry snapshot at creation timemonitorBlueBubblesProvider) registers webhook routes intorequireActivePluginRegistry()setActivePluginRegistry()with a new empty registryFix
Two changes:
setActivePluginRegistry()now carries forwardhttpRoutesfrom the previous registry when the new one has none, preserving channel-registered webhook routes across loader cycles.createGatewayPluginRequestHandler()reads routes fromrequireActivePluginRegistry()(current) instead of the stale snapshot captured at handler creation time.Testing
Fixes #32275