Skip to content

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

Closed
Sid-Qin wants to merge 2 commits intoopenclaw:mainfrom
Sid-Qin:fix/plugin-register-hook-cleared-25859
Closed

fix(hooks): preserve plugin-registered hooks across gateway startup reload#26242
Sid-Qin wants to merge 2 commits intoopenclaw:mainfrom
Sid-Qin:fix/plugin-register-hook-cleared-25859

Conversation

@Sid-Qin
Copy link
Copy Markdown
Contributor

@Sid-Qin Sid-Qin commented Feb 25, 2026

Summary

  • Problem: Plugin hooks registered via api.registerHook("message:received", handler) appear in openclaw hooks list but never fire. The handler is registered during plugin load, then wiped by clearInternalHooks() during gateway sidecar startup.
  • Why it matters: Any plugin relying on api.registerHook() for internal hooks (e.g., message:received, session:new) is silently broken — the hook appears registered but never triggers.
  • What changed: Introduced registerPluginHook() that tracks plugin-sourced handlers separately. clearInternalHooks() now restores plugin handlers after clearing, so they survive the gateway startup reload cycle.
  • What did NOT change (scope boundary): api.on() (typed plugin hooks) is unaffected — it uses a separate registry. File-based HOOK.md hooks are still cleared and reloaded as before.

Change Type (select all)

  • Bug fix

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution

Linked Issue/PR

User-visible / Behavior Changes

Plugin hooks registered via api.registerHook() now correctly fire on matching events. Previously they were silently cleared during gateway startup.

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Repro + Verification

Environment

  • OS: Any
  • Runtime/container: Node.js
  • Model/provider: N/A
  • Integration/channel: Any (plugin system)

Steps

  1. Create a plugin that calls api.registerHook("message:received", handler)
  2. Restart gateway
  3. Send a message

Expected

  • Plugin hook handler fires and logs output

Actual (before fix)

  • Hook appears in openclaw hooks list but handler never fires

Evidence

  • Failing test/log before + passing after
  • New test "should preserve plugin-registered handlers" in internal-hooks.test.ts
  • All 30 internal-hooks tests pass; all 162 plugin tests pass

Human Verification (required)

  • Verified scenarios: Plugin hooks survive clearInternalHooks(); regular hooks are still cleared; plugin hooks fire after gateway startup reload
  • Edge cases checked: Multiple events on same key; mixed plugin and config hooks on same event key
  • What you did not verify: Live plugin with gateway (requires running gateway with custom plugin)

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No

Failure Recovery (if this breaks)

  • How to disable/revert this change quickly: Revert to previous commit
  • Files/config to restore: src/hooks/internal-hooks.ts, src/plugins/registry.ts
  • Known bad symptoms reviewers should watch for: Plugin hooks firing when they shouldn't

Risks and Mitigations

  • Risk: Plugin hooks accumulate across multiple hot-reloads if clearInternalHooks is called without a full process restart.
    • Mitigation: Plugin hooks are only registered once during loadGatewayPlugins() at process start. Hot-reload scenarios re-run the full startup sequence including plugin loading.

Greptile Summary

Fixes plugin hooks being silently cleared during gateway startup by introducing separate tracking for plugin-registered handlers.

Key changes:

  • Added pluginHandlers map to track plugin-sourced hook handlers separately from config-based hooks
  • registerPluginHook() registers handlers in both handlers and pluginHandlers maps
  • clearInternalHooks() now preserves plugin handlers by restoring them after clearing
  • Updated plugin registry to use registerPluginHook() instead of registerInternalHook()

Issue found:

  • unregisterInternalHook() doesn't clean up the pluginHandlers map, which could cause unregistered plugin hooks to be incorrectly restored on the next clearInternalHooks() call

Confidence Score: 4/5

  • This PR is safe to merge with one logical issue that should be fixed
  • The implementation correctly solves the stated problem (plugin hooks being cleared during gateway startup) with proper test coverage. However, there's a logical issue where unregisterInternalHook() doesn't clean up the pluginHandlers map, which could cause unregistered plugin hooks to be incorrectly restored. This is unlikely to be hit in practice since plugins are loaded once and not dynamically unloaded, but should be fixed for correctness.
  • Pay attention to src/hooks/internal-hooks.ts - the unregisterInternalHook function needs to also clean up the pluginHandlers map

Last reviewed commit: ee2530d

…eload

Plugins register internal hooks during loadGatewayPlugins(), but
startGatewaySidecars() later calls clearInternalHooks() to reload
file-based hook definitions, which wiped plugin handlers.  Hooks
registered via api.registerHook() appeared in `openclaw hooks list`
but never fired.

Introduce registerPluginHook() that tracks handlers separately.
clearInternalHooks() now restores plugin handlers after clearing,
so they survive the gateway startup reload cycle.

Closes openclaw#25859

Co-authored-by: Cursor <[email protected]>
Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

3 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 25, 2026

Additional Comments (1)

src/hooks/internal-hooks.ts
unregisterInternalHook doesn't remove handlers from the pluginHandlers map. If a plugin hook is unregistered, it will be removed from handlers but restored on the next clearInternalHooks() call.

export function unregisterInternalHook(eventKey: string, handler: InternalHookHandler): void {
  const eventHandlers = handlers.get(eventKey);
  if (!eventHandlers) {
    return;
  }

  const index = eventHandlers.indexOf(handler);
  if (index !== -1) {
    eventHandlers.splice(index, 1);
  }

  // Also remove from pluginHandlers if present
  const pluginEventHandlers = pluginHandlers.get(eventKey);
  if (pluginEventHandlers) {
    const pluginIndex = pluginEventHandlers.indexOf(handler);
    if (pluginIndex !== -1) {
      pluginEventHandlers.splice(pluginIndex, 1);
    }
    if (pluginEventHandlers.length === 0) {
      pluginHandlers.delete(eventKey);
    }
  }

  // Clean up empty handler arrays
  if (eventHandlers.length === 0) {
    handlers.delete(eventKey);
  }
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/hooks/internal-hooks.ts
Line: 163-178

Comment:
`unregisterInternalHook` doesn't remove handlers from the `pluginHandlers` map. If a plugin hook is unregistered, it will be removed from `handlers` but restored on the next `clearInternalHooks()` call.

```suggestion
export function unregisterInternalHook(eventKey: string, handler: InternalHookHandler): void {
  const eventHandlers = handlers.get(eventKey);
  if (!eventHandlers) {
    return;
  }

  const index = eventHandlers.indexOf(handler);
  if (index !== -1) {
    eventHandlers.splice(index, 1);
  }

  // Also remove from pluginHandlers if present
  const pluginEventHandlers = pluginHandlers.get(eventKey);
  if (pluginEventHandlers) {
    const pluginIndex = pluginEventHandlers.indexOf(handler);
    if (pluginIndex !== -1) {
      pluginEventHandlers.splice(pluginIndex, 1);
    }
    if (pluginEventHandlers.length === 0) {
      pluginHandlers.delete(eventKey);
    }
  }

  // Clean up empty handler arrays
  if (eventHandlers.length === 0) {
    handlers.delete(eventKey);
  }
}
```

How can I resolve this? If you propose a fix, please make it concise.

@steipete
Copy link
Copy Markdown
Contributor

steipete commented Mar 3, 2026

Closing as duplicate of #29515.

Same startup hook-preservation fix path; consolidating review and merge discussion into one PR.

@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

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