Skip to content

fix(hooks): preserve plugin-registered hooks across gateway startup#28976

Closed
seilk wants to merge 1 commit intoopenclaw:mainfrom
seilk:fix/plugin-hooks-cleared-on-startup
Closed

fix(hooks): preserve plugin-registered hooks across gateway startup#28976
seilk wants to merge 1 commit intoopenclaw:mainfrom
seilk:fix/plugin-hooks-cleared-on-startup

Conversation

@seilk
Copy link
Copy Markdown
Contributor

@seilk seilk commented Feb 27, 2026

Summary

Fixes #25859

Plugin hooks registered via api.registerHook() during loadGatewayPlugins() were wiped by clearInternalHooks() in startGatewaySidecars(). File-based hooks survived because loadInternalHooks() re-loaded them from disk. Plugin hooks were never restored.

Why it looked intermittent: config file changes trigger plugin re-registration without calling clearInternalHooks(), so hooks came back until the next cold restart. Every restart always killed plugin hooks.

Changes

  • server.impl.ts: Move clearInternalHooks() before loadGatewayPlugins() so plugins register into a clean registry that stays intact
  • server-startup.ts: Remove clearInternalHooks() from startGatewaySidecars()
  • loader.ts: Track file-based handlers and clean them up before reloading (hot-reload safe without touching plugin hooks)
  • loader.test.ts: Two new tests: no duplicate handlers on reload + plugin hook preservation

Same bug as #20541, #26184, #26242

This PR #20541 #26184 #26242
Approach Move clear before plugins + self-tracking loader Move clear before plugins Clear then re-register from registry Separate pluginHandlers Map
Hot-reload safe Yes (loader tracks its own handlers) Needs follow-up Yes Yes (but unregister not synced)
New abstractions None None reregisterPluginInternalHooks() registerPluginHook() + second Map
Tests added 2 (dedup + preserve) 1 (integration) 0 1 (unit)

@openclaw-barnacle openclaw-barnacle bot added gateway Gateway runtime size: S labels Feb 27, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 27, 2026

Greptile Summary

This PR fixes a bug where plugin hooks registered via api.registerHook() were being silently wiped during gateway startup, while file-based hooks survived.

Root cause: clearInternalHooks() was called in startGatewaySidecars() AFTER plugins had already registered their hooks via loadGatewayPlugins(). File-based hooks recovered because loadInternalHooks() re-discovered them from disk, but plugin hooks had no recovery path.

Solution implemented:

  • Moved clearInternalHooks() to run once in server.impl.ts BEFORE loadGatewayPlugins() so plugins register into a clean registry
  • Removed the problematic clearInternalHooks() call from startGatewaySidecars()
  • Made loadInternalHooks() idempotent by tracking file-based handlers in a separate array and selectively unregistering them on reload
  • Added comprehensive tests verifying both deduplication on reload and plugin hook preservation

Key changes:

  • src/gateway/server.impl.ts:389 - Added clearInternalHooks() before plugin loading with clear documentation
  • src/gateway/server-startup.ts - Removed hook clearing and added comment explaining why
  • src/hooks/loader.ts - Added fileRegisteredHandlers tracking array for self-cleaning file-based hooks
  • src/hooks/loader.test.ts - Added two tests covering hot-reload deduplication and plugin hook preservation

Confidence Score: 5/5

  • This PR is safe to merge with high confidence
  • The fix correctly addresses the root cause by reordering operations to preserve plugin hooks. The implementation is clean with self-tracking for file-based hooks, comprehensive test coverage validates the fix, and clear documentation explains the architectural decision. No logical errors, race conditions, or memory leaks were found during analysis.
  • No files require special attention

Last reviewed commit: dbad088

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dbad0888af

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +61 to +64
// Unregister handlers from any previous loadInternalHooks() call (hot-reload safety).
// This only removes file-based hooks, preserving plugin-registered hooks.
for (const { event, handler } of fileRegisteredHandlers) {
unregisterInternalHook(event, handler);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Unregister prior file hooks before disabled-config early return

The new hot-reload cleanup only runs after the hooks.internal.enabled guard, so a second loadInternalHooks() call with hooks disabled leaves previously loaded file handlers registered and still executable. This shows up when configuration transitions from enabled to disabled (or any caller reuses loadInternalHooks for reload semantics): old hooks keep firing even though the new config says internal hooks are off. The unregister pass needs to happen before returning on disabled config.

Useful? React with 👍 / 👎.

@seilk seilk force-pushed the fix/plugin-hooks-cleared-on-startup branch from dbad088 to b868ca6 Compare February 27, 2026 17:07
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b868ca64e1

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 55 to 56
return 0;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Unregister stale file hooks before disabled-config return

loadInternalHooks now tracks file-based handlers for hot reload, but the cleanup runs only after the hooks.internal.enabled check. If hooks were previously loaded and config is then reloaded with enabled: false, this function returns early and leaves those old handlers registered, so file hooks can continue firing even though internal hooks are disabled. Move the unregister pass ahead of this guard so disabling hooks actually removes previously loaded handlers.

Useful? React with 👍 / 👎.

clearInternalHooks() in startGatewaySidecars() was wiping all hooks
including those registered by plugins via api.registerHook() during
loadGatewayPlugins(). File-based hooks survived because loadInternalHooks()
re-discovered them from disk, but plugin hooks were silently lost.

This caused any plugin relying on internal hook events (command:new,
message:received, etc.) to appear registered but never fire after
a gateway restart. The bug appeared intermittent because config file
changes triggered a plugin re-registration path that bypassed the clear.

Fix:
- Move clearInternalHooks() to server.impl.ts, before loadGatewayPlugins()
- Remove clearInternalHooks() from startGatewaySidecars()
- Track file-based handlers in loader.ts for selective cleanup on hot-reload

Fixes openclaw#25859
@steipete
Copy link
Copy Markdown
Contributor

steipete commented Mar 3, 2026

Thanks for the PR! Multiple PRs address the same fix. Keeping #26242 as the earliest submission. Closing to reduce noise. This is an AI-assisted triage review. If we got this wrong, feel free to reopen — happy to revisit.

@steipete steipete closed this Mar 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gateway Gateway runtime size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Plugin api.registerHook("message:received", ...) shows up in openclaw hooks list but never fires (likely cleared at gateway startup)

2 participants