Skip to content

fix(hooks): preserve plugin-registered internal hooks when refreshing file hooks#64340

Closed
anyran wants to merge 2 commits into
openclaw:mainfrom
anyran:fix/dreaming-startup-hook-reconcile
Closed

fix(hooks): preserve plugin-registered internal hooks when refreshing file hooks#64340
anyran wants to merge 2 commits into
openclaw:mainfrom
anyran:fix/dreaming-startup-hook-reconcile

Conversation

@anyran

@anyran anyran commented Apr 10, 2026

Copy link
Copy Markdown

Summary

Fix a bug where clearInternalHooks() wiped plugin-registered internal hooks during startup.

This caused plugin hooks like memory-core's gateway:startup handler to disappear before the startup event fired, which prevented managed dreaming cron reconciliation from running automatically.

Changes

  • separate file-hook and plugin-hook registries
  • make clearInternalHooks() clear only file hooks
  • trigger both file and plugin hook handlers
  • add clearAllInternalHooks() for tests
  • add tests covering plugin hook survival across file-hook refresh

Why this fixes dreaming

memory-core registers its dreaming startup reconcile hook through the plugin hook path.
Previously that hook could be cleared during startup before gateway:startup was triggered, so Memory Dreaming Promotion was never auto-reconciled.
With this change, plugin hooks survive file-hook refresh and the startup reconcile can run correctly.

anyran added 2 commits April 10, 2026 21:49
… clearInternalHooks() wiping plugin-registered handlers

The dreaming startup bug was caused by clearInternalHooks() clearing all registered handlers, including plugin hooks registered during loadOpenClawPlugins(). This function is designed to refresh file-based hooks from ~/.openclaw/hooks/, but it inadvertently wiped plugin hooks that have a different lifecycle.

Changes:
- Add separate pluginHandlers registry for plugin-registered hooks
- registerInternalHook() now accepts source: 'file' | 'plugin' parameter
- clearInternalHooks() only clears file hooks, preserving plugin hooks
- triggerInternalHook() invokes handlers from both registries
- Add clearAllInternalHooks() for tests needing full reset
- Add clearPluginInternalHooks() for plugin cleanup

This ensures memory-core's gateway:startup hook survives the startup sequence and dreaming cron reconciliation runs correctly.
…est isolation

Tests that call clearInternalHooks() now use clearAllInternalHooks() to ensure both file and plugin hooks are cleared between test cases. Also adds new tests to verify plugin hooks survive clearInternalHooks().
@greptile-apps

greptile-apps Bot commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a real startup-lifecycle bug: clearInternalHooks() was wiping plugin-registered hooks (e.g., memory-core's gateway:startup dreaming handler) because both file hooks and plugin hooks shared a single registry. The fix separates them into two global-singleton Maps (handlers for file hooks, pluginHandlers for plugin hooks), narrows clearInternalHooks() to file hooks only, adds clearAllInternalHooks() for test isolation, and updates registry.ts to pass { source: \"plugin\" } when registering plugin hooks. The core logic is correct and the changed test files are properly updated.

Confidence Score: 5/5

Safe to merge; the fix is correct and well-tested across the changed files.

The core logic is sound: two separate global-singleton maps, narrowed clearInternalHooks(), correct { source: 'plugin' } wiring in registry.ts, and updated test files. The only finding is a P2 fragility in an untouched file (src/plugins/loader.test.ts) where clearInternalHooks() now has weaker semantics than before — but this is not currently breaking and does not affect the primary fix path.

src/plugins/loader.test.ts — two inline clearInternalHooks() calls should ideally become clearAllInternalHooks() to match the rest of the test suite after this PR's semantic change.

Comments Outside Diff (1)

  1. src/plugins/loader.test.ts, line 1463-1483 (link)

    P2 clearInternalHooks() no longer provides full isolation here

    After this PR, clearInternalHooks() only clears file-based hooks; plugin hooks registered via { source: "plugin" } survive. This test's assertion at line 1481 (getRegisteredEventKeys().toEqual([])) passes today because the only other api.registerHook() call in this file (line 2354, "rejects duplicate plugin registrations") runs after this test. However, if a future test above line 1450 activates a plugin that calls api.registerHook(), those plugin hooks would survive the clearInternalHooks() at line 1463 and break the assertion.

    Consider replacing both clearInternalHooks() calls in this test (and importing clearAllInternalHooks) to match the same update applied to the other hook test files in this PR:

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/plugins/loader.test.ts
    Line: 1463-1483
    
    Comment:
    **`clearInternalHooks()` no longer provides full isolation here**
    
    After this PR, `clearInternalHooks()` only clears file-based hooks; plugin hooks registered via `{ source: "plugin" }` survive. This test's assertion at line 1481 (`getRegisteredEventKeys().toEqual([])`) passes today because the only other `api.registerHook()` call in this file (line 2354, "rejects duplicate plugin registrations") runs *after* this test. However, if a future test above line 1450 activates a plugin that calls `api.registerHook()`, those plugin hooks would survive the `clearInternalHooks()` at line 1463 and break the assertion.
    
    Consider replacing both `clearInternalHooks()` calls in this test (and importing `clearAllInternalHooks`) to match the same update applied to the other hook test files in this PR:
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/plugins/loader.test.ts
Line: 1463-1483

Comment:
**`clearInternalHooks()` no longer provides full isolation here**

After this PR, `clearInternalHooks()` only clears file-based hooks; plugin hooks registered via `{ source: "plugin" }` survive. This test's assertion at line 1481 (`getRegisteredEventKeys().toEqual([])`) passes today because the only other `api.registerHook()` call in this file (line 2354, "rejects duplicate plugin registrations") runs *after* this test. However, if a future test above line 1450 activates a plugin that calls `api.registerHook()`, those plugin hooks would survive the `clearInternalHooks()` at line 1463 and break the assertion.

Consider replacing both `clearInternalHooks()` calls in this test (and importing `clearAllInternalHooks`) to match the same update applied to the other hook test files in this PR:

```suggestion
    clearAllInternalHooks();
    const scoped = loadOpenClawPlugins({
```

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

Reviews (1): Last reviewed commit: "test(hooks): update tests to use clearAl..." | Re-trigger Greptile

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

Copy link
Copy Markdown

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: bbeef4be33

ℹ️ 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".

Comment on lines 292 to 293
export function clearInternalHooks(): void {
handlers.clear();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Clear plugin handlers when resetting internal hooks

clearInternalHooks() now leaves pluginHandlers intact, but the gateway can perform multiple activating plugin loads in one process (initial startup load, then reloadDeferredGatewayPlugins when deferred channel plugins are present). In that flow, plugin hooks are registered again without a plugin-hook reset, so events like gateway:startup execute the same plugin handler more than once, duplicating side effects such as startup reconciliation/scheduling work.

Useful? React with 👍 / 👎.

@steipete

Copy link
Copy Markdown
Contributor

Closing this as implemented after Codex review.

Current main already preserves plugin-registered internal hooks across internal hook refreshes, and it also covers the duplicate-registration risk raised in PR review, so there is no remaining core change for PR #64340 to land.

What I checked:

  • File-hook refresh is targeted, not global: loadInternalHooks() starts by calling resetLoadedInternalHooks(), which unregisters only the handlers tracked in loadedHookRegistrations; it does not clear the full internal-hook registry. (src/hooks/loader.ts:51, 11804a484ded)
  • Current main has an explicit regression test for the reported behavior: src/hooks/loader.test.ts now asserts that a pre-registered gateway:startup hook is still present after loadInternalHooks() runs and still fires once. (src/hooks/loader.test.ts:264, 11804a484ded)
  • Startup still exercises the same sequence safely: Gateway startup loads startup plugins first, then loads internal hooks, then dispatches gateway:startup; combined with the targeted reset above, that preserves plugin hook registrations through startup refresh. (src/gateway/server-startup-post-attach.ts:230, 11804a484ded)
  • Repeated activating loads already replace old plugin hook registrations: Plugin hook registration first unregisters any prior active registrations for the same hook name before registering the new handlers. (src/plugins/registry.ts:514, 11804a484ded)
  • Main covers the PR review’s duplicate-fire concern: src/plugins/loader.test.ts verifies two activating loads of the same plugin still yield exactly one gateway:startup invocation, addressing the review comment that the PR approach could duplicate plugin handlers. (src/plugins/loader.test.ts:2756, 11804a484ded)

So I’m closing this as already implemented rather than keeping a duplicate issue open.

Review notes: reviewed against 11804a484ded; fix evidence: commit 11804a484ded.

@steipete steipete closed this Apr 25, 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.

2 participants