Skip to content

fix: carry forward httpRoutes when plugin registry is replaced#45568

Closed
masatohoshino wants to merge 3 commits intoopenclaw:mainfrom
masatohoshino:fix/carry-forward-http-routes-on-registry-swap
Closed

fix: carry forward httpRoutes when plugin registry is replaced#45568
masatohoshino wants to merge 3 commits intoopenclaw:mainfrom
masatohoshino:fix/carry-forward-http-routes-on-registry-swap

Conversation

@masatohoshino
Copy link
Copy Markdown
Contributor

Summary

Fixes webhook 404s caused by httpRoutes being lost when setActivePluginRegistry replaces the active registry at runtime.

Ref #45445

Root cause

When loadOpenClawPlugins is called at runtime (config schema lookups, channel status probes, etc.), it creates a fresh registry with an empty httpRoutes array and sets it as active. Webhook routes registered by channel plugins (LINE, Google Chat, etc.) on the previous registry are lost, causing 404s.

What changed

  • setActivePluginRegistry in src/plugins/runtime.ts now carries forward httpRoutes from the previous registry when the new registry has an empty httpRoutes array.
  • Carry-forward only triggers when: previous registry exists, is a different object, has routes, and new registry has none.

Tests added

  • Carries forward httpRoutes when new registry has none
  • Does not carry forward when new registry already has its own routes
  • Does not duplicate routes when same registry is set again

Relationship to other PRs

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 14, 2026

Greptile Summary

This PR fixes webhook 404s by carrying forward httpRoutes from the previous plugin registry when setActivePluginRegistry is called with a new, route-less registry. The four-condition guard in runtime.ts is conservative and well-targeted: it only activates when the previous registry exists, is a different object, has routes, and the incoming registry has none — matching exactly the runtime reload scenario described in the root cause.

Key points:

  • The core logic change in runtime.ts is minimal and correct; the [...prev.httpRoutes] shallow copy appropriately isolates the new registry's array from the old one.
  • The fix only carries httpRoutes and intentionally leaves other registry fields (tools, hooks, channels, etc.) alone, which is consistent with the described root cause.
  • The three test cases cover the intended scenarios, but they all share the same globalThis singleton state without a beforeEach reset, making them order-dependent. This is the main improvement needed in the test file.

Confidence Score: 4/5

  • Safe to merge; the logic change is minimal and conservative with no regressions expected.
  • The implementation is a small, well-guarded mutation that precisely addresses the stated root cause. The only concern is test isolation — the test suite mutates shared global state without resetting between cases, which could cause fragile/order-dependent failures as the suite grows. No logic bugs were found in the production code.
  • src/plugins/runtime.test.ts — needs a beforeEach reset to prevent test-order sensitivity.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/plugins/runtime.test.ts
Line: 13-47

Comment:
**Tests share mutable global state across cases**

All three test cases operate against the same `globalThis[Symbol.for("openclaw.pluginRegistryState")]` object, because `setActivePluginRegistry` / `getActivePluginRegistry` read and write from that singleton. The tests happen to pass in the current execution order, but any future re-ordering, parallelism, or new test cases inserted between existing ones could produce unexpected failures because the active registry at the start of each `it` block is whatever was left by the previous one.

Add a `beforeEach` (or `afterEach`) that resets the shared state, for example:

```suggestion
import { describe, it, expect, beforeEach } from "vitest";
import { createEmptyPluginRegistry } from "./registry.js";
import type { PluginHttpRouteRegistration } from "./registry.js";
import { setActivePluginRegistry, getActivePluginRegistry } from "./runtime.js";

const makeRoute = (path: string): PluginHttpRouteRegistration => ({
  path,
  handler: () => {},
  auth: "gateway",
  match: "exact",
});

describe("setActivePluginRegistry", () => {
  beforeEach(() => {
    // Reset the global registry state so each test starts from a clean slate
    setActivePluginRegistry(createEmptyPluginRegistry());
  });
```

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

Last reviewed commit: cbd5ccc

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

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

mekenthompson added a commit to mekenthompson/openclaw that referenced this pull request Mar 16, 2026
Extends the httpRoutes carry-forward from PR openclaw#45568 to also preserve
channel registrations when the plugin registry is replaced at runtime.

Without this, loadOpenClawPlugins() calls triggered by config lookups,
schema validation, or maybeBootstrapChannelPlugin() create a new empty
registry, causing the message tool to fail with 'Unknown channel' errors
because listPluginChannelIds() reads from the now-empty channels array.

Co-authored-by: Claude <[email protected]>
When setActivePluginRegistry swaps in a new registry, dynamically registered
httpRoutes (from LINE, Google Chat, etc.) were lost because the new registry
has an empty httpRoutes array. Now carries forward previous httpRoutes when
the new registry has none, preventing webhook 404s after runtime registry swaps.

Ref openclaw#45445
- Use prev.httpRoutes reference directly instead of [...spread] to preserve
  unregister callback identity (Codex P1 feedback)
- Add beforeEach to reset global registry state between tests (Greptile feedback)
@masatohoshino masatohoshino force-pushed the fix/carry-forward-http-routes-on-registry-swap branch from 11e5507 to ca87dfd Compare March 17, 2026 12:39
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: ca87dfd7eb

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

@masatohoshino
Copy link
Copy Markdown
Contributor Author

Implemented follow-up for Codex P2 (“Restrict route carry-forward to same registry cache key”) in c0e0be617.

What changed

  • src/plugins/runtime.ts

    • setActivePluginRegistry now normalizes and compares cache keys (cacheKey ?? null) before carrying httpRoutes forward.
    • Carry-forward is now limited to same-key registry swaps only.
  • src/plugins/runtime.test.ts

    • Added edge-case tests for key matching/mismatching:
      • same key => carry-forward
      • different key => no carry-forward
      • omitted/undefined key => carry-forward (normalized null)
      • null vs explicit key => no carry-forward
    • Kept test-state reset hardening via before/after cleanup.

Validation

  • pnpm vitest src/plugins/runtime.test.ts passes locally.

I also observed current CI failures in paths outside this PR diff; if they persist after rerun, I can post a per-job non-causality summary.

@masatohoshino
Copy link
Copy Markdown
Contributor Author

CI finished, and I checked the failing jobs against this PR diff.

This PR only changes:

  • src/plugins/runtime.ts
  • src/plugins/runtime.test.ts

Current failures are in unrelated paths, for example:

  • checks (node, extensions, pnpm test:extensions):
    • extensions/feishu/src/client.test.ts
    • extensions/feishu/src/bot.test.ts
    • extensions/google/oauth.test.ts
  • checks (node, channels, pnpm test:channels):
    • extensions/slack/src/monitor/provider.ts
    • src/line/bot-handlers.test.ts
  • checks (node, contracts, pnpm test:contracts):
    • src/plugins/contracts/catalog.contract.test.ts / src/plugins/provider-runtime.test-support.ts
  • checks-windows shards:
    • src/plugins/bundle-mcp.test.ts
    • src/plugin-sdk/index.test.ts

So these failures appear non-causal to this PR’s changes.
Could a maintainer please re-run the failing jobs?

@masatohoshino
Copy link
Copy Markdown
Contributor Author

CI failures on this PR still appear non-causal to this diff. Could a maintainer please re-run only the failing jobs from run https://github.com/openclaw/openclaw/actions/runs/23195583617 ?

Requested re-run targets:

  • checks (node, extensions, pnpm test:extensions)
  • checks (node, channels, pnpm test:channels)
  • checks (node, contracts, pnpm test:contracts)
  • checks-windows (node, test, 1, 6, pnpm test)
  • checks-windows (node, test, 3, 6, pnpm test)
  • checks-windows (node, test, 5, 6, pnpm test)
  • checks-windows (node, test, 6, 6, pnpm test)

I will not add unrelated "main CI test-fix" commits in this PR, per repo policy (r: no-ci-pr).

@masatohoshino
Copy link
Copy Markdown
Contributor Author

Before merge, here is the bug-fix gate checklist from AGENTS.md for this PR:

  • Symptom evidence: webhook routes disappear after plugin registry replacement (404 behavior captured in PR discussion/review thread).
  • Root cause in code: setActivePluginRegistry replaced registry state and could drop/incorrectly carry route state in src/plugins/runtime.ts.
  • Fix touches implicated path: src/plugins/runtime.ts updated carry-forward logic (including cache-key guard).
  • Regression test: src/plugins/runtime.test.ts covers same-key carry-forward / different-key no-carry / keyless normalization cases; local pnpm vitest src/plugins/runtime.test.ts was reported passing in-thread.

Merge-policy note: this PR intentionally avoids unrelated CI-only test-fix commits and keeps scope to the runtime registry bug path.

@masatohoshino
Copy link
Copy Markdown
Contributor Author

Closing this PR because the issue has been resolved upstream.

This was part of the investigation and an alternative fix approach.
Leaving it here for reference. Thanks.

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.

1 participant