fix(plugins): preserve plugin-registered internal hooks across gateway startup#29515
fix(plugins): preserve plugin-registered internal hooks across gateway startup#29515carbaj03 wants to merge 2 commits intoopenclaw:mainfrom
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 84415f5a9a
ℹ️ 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".
| for (const hook of params.pluginRegistry.hooks) { | ||
| if (hook.handler) { | ||
| for (const event of hook.events) { | ||
| registerInternalHook(event, hook.handler); |
There was a problem hiding this comment.
Preserve register:false when restoring plugin hooks
This loop re-registers every plugin hook that has a stored handler, but api.registerHook(..., { register: false }) is explicitly supported in src/plugins/registry.ts (the initial registration path returns early when opts?.register === false). Because the register intent is not stored and checked here, hooks that were intentionally metadata-only become active after startup, so their handlers run unexpectedly once clearInternalHooks() has happened and this restore block executes.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Fixed in 33742f6. Hooks registered with opts.register = false now store handler: undefined in the registry, so the re-registration loop in startGatewaySidecars skips them (the existing if (hook.handler) guard handles it). Added a dedicated test case for this edge case.
Greptile SummaryThis PR fixes a real lifecycle bug where Key issues:
Confidence Score: 3/5
Last reviewed commit: 84415f5 |
| describe("plugin hook re-registration after clear", () => { | ||
| it("should survive clearInternalHooks when re-registered from registry", async () => { | ||
| const handler = vi.fn(); | ||
|
|
||
| // Simulate plugin registering a hook during loadGatewayPlugins | ||
| registerInternalHook("agent:bootstrap", handler); | ||
|
|
||
| // Simulate startGatewaySidecars clearing all hooks | ||
| clearInternalHooks(); | ||
|
|
||
| // Handler should be gone | ||
| const event = createInternalHookEvent("agent", "bootstrap", "test-session", { | ||
| workspaceDir: "/tmp", | ||
| bootstrapFiles: [], | ||
| }); | ||
| await triggerInternalHook(event); | ||
| expect(handler).not.toHaveBeenCalled(); | ||
|
|
||
| // Simulate re-registration from plugin registry (the fix) | ||
| registerInternalHook("agent:bootstrap", handler); | ||
|
|
||
| // Now handler should fire | ||
| await triggerInternalHook(event); | ||
| expect(handler).toHaveBeenCalledOnce(); | ||
| expect(handler).toHaveBeenCalledWith(event); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Test exercises raw primitives, not the full re-registration path
The test directly calls registerInternalHook / clearInternalHooks / registerInternalHook again — essentially verifying that the underlying hook map works, which was already covered by earlier tests. It doesn't exercise the actual code path introduced in this PR (startGatewaySidecars iterating pluginRegistry.hooks and calling registerInternalHook).
As a result, the opts.register = false edge-case described in the sibling comment is untested, and any regression in the re-registration loop in server-startup.ts would go undetected by this suite. Consider adding an integration-level test that goes through the createPluginRegistry → registerHook → re-registration loop, or at minimum add a case where handler is undefined (as it would be for opts.register = false hooks) and confirm it is skipped.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/hooks/internal-hooks.test.ts
Line: 459-485
Comment:
**Test exercises raw primitives, not the full re-registration path**
The test directly calls `registerInternalHook` / `clearInternalHooks` / `registerInternalHook` again — essentially verifying that the underlying hook map works, which was already covered by earlier tests. It doesn't exercise the actual code path introduced in this PR (`startGatewaySidecars` iterating `pluginRegistry.hooks` and calling `registerInternalHook`).
As a result, the `opts.register = false` edge-case described in the sibling comment is untested, and any regression in the re-registration loop in `server-startup.ts` would go undetected by this suite. Consider adding an integration-level test that goes through the `createPluginRegistry` → `registerHook` → re-registration loop, or at minimum add a case where `handler` is `undefined` (as it would be for `opts.register = false` hooks) and confirm it is skipped.
How can I resolve this? If you propose a fix, please make it concise.There was a problem hiding this comment.
Addressed in 33742f6. Added a test case that simulates the re-registration loop with a handler: undefined hook (as produced by register: false) alongside a real handler, and verifies only the real handler fires. The registry fix ensures handler is stored as undefined when register: false, so metadata-only hooks stay inactive.
Additional Comments (1)
The The fix is to only store const shouldRegister = hookSystemEnabled && opts?.register !== false;
registry.hooks.push({
pluginId: record.id,
entry: hookEntry,
events: normalizedEvents,
source: record.source,
handler: shouldRegister ? handler : undefined,
});Alternatively, store Prompt To Fix With AIThis is a comment left during a code review.
Path: src/plugins/registry.ts
Line: 254-260
Comment:
**`handler` stored unconditionally, ignoring `opts?.register === false`**
The `handler` is pushed into `registry.hooks` unconditionally, even when the early-return guard at line 263 would have prevented registration (`!hookSystemEnabled || opts?.register === false`). This means that during re-registration in `startGatewaySidecars`, any hook a plugin author explicitly opted out of via `opts.register = false` will be silently re-registered anyway — violating their stated intent.
The fix is to only store `handler` when registration was actually intended:
```ts
const shouldRegister = hookSystemEnabled && opts?.register !== false;
registry.hooks.push({
pluginId: record.id,
entry: hookEntry,
events: normalizedEvents,
source: record.source,
handler: shouldRegister ? handler : undefined,
});
```
Alternatively, store `shouldRegister` as a boolean field in `PluginHookRegistration` and check it in `startGatewaySidecars`.
How can I resolve this? If you propose a fix, please make it concise. |
|
This pull request has been automatically marked as stale due to inactivity. |
Workaround for OpenClaw Gateway startup order issue. Problem: api.registerHook() registers hooks before clearInternalHooks() runs, causing plugin hooks to be cleared during Gateway startup. Solution: Register the hook in service.start() by directly accessing globalThis.__openclaw_internal_hook_handlers__, which runs after clearInternalHooks() has completed. This workaround will be obsolete once openclaw/openclaw#29515 is merged and released.
Workaround for OpenClaw Gateway startup order issue. Problem: api.registerHook() registers hooks before clearInternalHooks() runs, causing plugin hooks to be cleared during Gateway startup. Solution: Register the hook in service.start() by directly accessing globalThis.__openclaw_internal_hook_handlers__, which runs after clearInternalHooks() has completed. This workaround will be obsolete once openclaw/openclaw#29515 is merged and released.
|
Still active. All bot review concerns addressed (see inline replies). CI is green. Ready for maintainer review. |
|
👋 Friendly ping — this PR has been green for over a week now and is ready for review. We're preparing to launch our app and this fix is in the critical path. Would really appreciate a review when you get a chance. Thanks! |
|
CI is green, all bot review concerns addressed. This fixes a real lifecycle bug: Minimal fix: store the handler reference in the plugin registry and re-register after the clear. 3 files, 2 tests. Happy to address any feedback. |
|
@vincentkoc would you have a chance to take a look when you get a moment? |
|
@joshavant @altaywtf — friendly ping, this PR is CI-green and ready for review. We're preparing for our app launch and this fix is in the critical path. Would appreciate a look when you get a chance! |
33742f6 to
f260f1d
Compare
|
@tyler6204 @vincentkoc — this is a small fix (86 lines, 3 files) for a plugin lifecycle bug: CI is green, tests included. Would really appreciate a review! |
|
Hi @vincentkoc and @steipete — gentle ping on this one. This PR preserves plugin-registered internal hooks across gateway restarts (fixes hook loss on reload). All CI checks pass. Would love a human reviewer when you have a moment. Thanks! 🙏 |
|
Bumping this for visibility — this PR and #31828 (canvas broadcast to all connected nodes) are a pair, both green with 23/23 CI checks passing. supersantux expressed interest on Discord today and mentioned he'd follow up. Both are small and non-breaking — would really appreciate a maintainer pass when you get a chance. 🙏 |
…y startup clearInternalHooks() in startGatewaySidecars wiped plugin hooks registered during loadGatewayPlugins. Store the handler function in PluginHookRegistration and re-register plugin hooks after loadInternalHooks(). Co-Authored-By: Claude Opus 4.6 <[email protected]>
Hooks registered with opts.register=false are metadata-only. Previously the handler was stored unconditionally, causing these hooks to become active after clearInternalHooks() re-registration in startGatewaySidecars. Co-Authored-By: Claude Opus 4.6 <[email protected]>
f260f1d to
34c62d8
Compare
|
Closing — stale with merge conflicts and no reviews. Will re-submit on a fresh branch if still needed. |
Summary
agent:bootstrap) being wiped during gateway startup:clearInternalHooks()instartGatewaySidecarsdestroyed hooks registered by plugins duringloadGatewayPluginsPluginHookRegistrationso it survives the clear, then re-register plugin hooks afterloadInternalHooks()api.registerHook("agent:bootstrap", handler)— the only way to inject content into the system prompt once per sessionTest plan
pnpm tsgo— no new type errorspnpm vitest run src/hooks/internal-hooks.test.ts— 30/30 pass (includes new test)registerHook("agent:bootstrap")→ "re-registered 1 plugin hook" in logs🤖 Generated with Claude Code