You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The original root-cause framing in this issue is incorrect. After tracing the live 2026.5.28 runtime more carefully, the actual mechanism is:
getRuntimeConfig(options) is a literal alias for loadConfig(options). loadConfig by default returns loadPinnedRuntimeConfig(...) — the same pinned OpenClawConfig reference. Only options.pin === false bypasses the pin, but the public channel-plugin API (core.config.loadConfig) does not expose options in its type signature.
The only code path that replaces the snapshot is applySnapshot in server-reload-handlers-*.js:152, which has an early return when isNoopReloadPlan(plan) && !followUp.requiresRestart. Because bindings/agents/routing are all kind: "none" in BASE_RELOAD_RULES_TAIL, any CLI binding-only edit produces a noop plan → applySnapshot returns → setRuntimeConfigSnapshot() is never called.
src/routing/resolve-route.ts caches by cfg object identity (WeakMap<OpenClawConfig, ...>), so as long as the snapshot ref doesn't change, routing returns the same evaluated bindings forever.
Implication: the per-inbound getRuntimeConfig() pattern documented in #90852 (now closed) does not actually refresh routing in current runtime. Telegram/qqbot in-tree consumers are also effectively no-ops under this code path unless they explicitly pass { pin: false } (which would force a fresh disk read + parse + validate on every inbound — not a desirable hot-path cost). The qqbot fix #73567 most likely worked on the version it was written against and silently regressed when loadPinnedRuntimeConfig was introduced — worth maintainer verification.
Real fix surface — one of:
A. Unconditional snapshot swap in applySnapshot when changedPaths is non-empty, regardless of isNoopReloadPlan. Snapshot ref always advances; routing WeakMap always misses; no new reload actions triggered. Smallest, safest core change.
B. New ReloadRule kind "swap-snapshot-only" (or similar) for bindings/agents/routing. Same effect as A but more explicit — the planner classifies, applySnapshot honors a "swap but no action" plan.
C. (workaround, not a fix) Channel plugins watch openclaw.json mtime themselves and force loadConfig({ pin: false }) — penalizes hot path, only kicks the problem down the road.
Happy to draft a PR for direction A or B once a maintainer signals which they prefer. I won't repeat the docs-PR mistake — any next PR will be source-level and gated on maintainer ack first.
Original (now superseded) analysis below for context.
Summary
The "channel plugins must call getRuntimeConfig() per-inbound to pick up bindings[] edits without a gateway restart" contract is real, intentional, and load-bearing — but it is currently undocumented, and every channel plugin re-discovers it the hard way by shipping a regression first.
Problem to solve
src/gateway/config-reload-plan.ts:120-140 declares bindings, agents, and routing as kind: "none":
This is by design (PR #7747 attempted to flip them to kind: "hot" and was closed; issue #27706 proposed a binding-only hot-reload API and was closed-as-stale). The expected design is that channel plugins re-evaluate routing per inbound message using a fresh cfg reference from openclaw/plugin-sdk/runtime-config-snapshot.getRuntimeConfig(), so that src/routing/resolve-route.ts:205WeakMap<OpenClawConfig, EvaluatedBindingsCache> misses on the new ref and re-reads bindings[].
But this contract is not written down anywhere a channel author would find it. As a result every channel plugin currently in the tree has had to re-discover it independently:
Plugin
Status
Discovery cost
extensions/telegram
Inlined the pattern from the start (bot-handlers.runtime.ts:967cfg: telegramDeps.getRuntimeConfig() // "Fresh config for bindings lookup")
Discovered implicitly during initial author
extensions/qqbot
Shipped without it, regressed in production, fixed by PR #73567 (merged 2026-05-11) — added ActiveCfgProvider (52-line helper)
Shipped a regression, then ~80-line fix
A third-party channel plugin we maintain
Has the same bug right now (we hit it this week)
Will repeat the same ~80-line fix
This is three plugins independently re-rolling the same ActiveCfgProvider because the contract is implicit. Anyone who writes a fourth channel plugin will repeat the cycle.
Proposed solution
We see two reasonable directions and would like maintainer guidance before sending a PR. Both can ship; they're not mutually exclusive.
Option A — Docs only (lowest risk, fastest)
Add a short "Routing & live bindings" section to the channel plugin author guide (e.g. docs/plugins/sdk-runtime.md or a new docs/plugins/channel-routing.md) stating the contract:
Channel plugins must call getRuntimeConfig() from openclaw/plugin-sdk/runtime-config-snapshot on every inbound message and pass the returned cfg to resolveAgentRoute. Do not capture or memoize the cfg reference handed to start() — doing so will cause CLI binding edits (openclaw agents bind) to be ignored until the gateway is restarted, because bindings is kind: "none" in the reload plan by design (see config-reload-plan.ts).
Cross-link from config-reload-plan.ts:120-140 comments back to the doc, and from resolve-route.ts:205 WeakMap declaration ("cache key is cfg reference; stale refs return stale routes by construction").
This is 100% within the "Refactor-only PRs are not accepted" exception because it's a docs PR addressing real plugin author confusion.
Option B — Centralize the helper in plugin SDK (eliminates duplicate code)
Export a thin createActiveCfgProvider({ fallback }) from openclaw/plugin-sdk/runtime-config-snapshot (or a sibling module). Pattern is verbatim what qqbot PR #73567 added to extensions/qqbot/src/engine/gateway/active-cfg.ts:
Then deprecate the inlined copies in telegram + qqbot in follow-ups, on the plugin authors' own schedule. New channel plugins get the helper for free.
This is a small additive SDK change with one preexisting in-tree consumer (qqbot) plus matching telegram pattern — it doesn't introduce a new architecture, just names a pattern that already exists.
Leave it as-is and let each channel plugin re-discover — current status. Three plugins, three independent fixes, the third still broken. New channel plugins (Mattermost, MS Teams, IRC, etc. all on the maintainer roster) will repeat.
Impact
Affected: every current and future channel plugin author who needs to support openclaw agents bind taking effect without a gateway restart
Frequency: once per plugin author lifetime; reliably reproducible
Consequence: each affected plugin ships a regression to production, gets a bug report, then writes ~80 lines of duplicate code. With Option A this becomes "read the docs"; with Option B it becomes "import a helper"
We're a third-party channel plugin team that just hit this. We'd like to contribute the fix upstream rather than just patching our own plugin. Per CONTRIBUTING.md ("New features / architecture → Start a GitHub Issue first"), we're opening this issue before drafting any PR.
Happy to send the PR for whichever direction you prefer (A, B, or both) once a maintainer signals which is in scope. If you'd rather we don't, that's fine too — we'll just patch around it on our side and move on. Tagging gateway-area maintainers per CONTRIBUTING.md: @frankekn (Gateway/Channels), @joshavant (Gateway), @cpojer (JS Infra) — apologies if this is not the right escalation path.
Real environment we tested in:
OpenClaw: latest main (commit c1ddfccf65c9cf3708771b356090b7331c543f58 at time of investigation)
OS: macOS 15.x (Darwin 25.3.0)
Channel triggering the discovery: a third-party channel plugin we maintain
Reproducer: openclaw agents add foo --workspace ~/.openclaw/workspaces/foo --non-interactive → openclaw config patch --stdin writing channels.<our-channel>.accounts.<botUid> → openclaw agents bind --agent foo --bind <our-channel>:<botUid> → DM the bot without restarting → routes to agent:main instead of foo. Confirmed by stepping through resolveAgentRoute and observing the WeakMap hit returning the pre-bind evaluated bindings.
Summary
The "channel plugins must call
getRuntimeConfig()per-inbound to pick upbindings[]edits without a gateway restart" contract is real, intentional, and load-bearing — but it is currently undocumented, and every channel plugin re-discovers it the hard way by shipping a regression first.Problem to solve
src/gateway/config-reload-plan.ts:120-140declaresbindings,agents, androutingaskind: "none":This is by design (PR #7747 attempted to flip them to
kind: "hot"and was closed; issue #27706 proposed a binding-only hot-reload API and was closed-as-stale). The expected design is that channel plugins re-evaluate routing per inbound message using a fresh cfg reference fromopenclaw/plugin-sdk/runtime-config-snapshot.getRuntimeConfig(), so thatsrc/routing/resolve-route.ts:205WeakMap<OpenClawConfig, EvaluatedBindingsCache>misses on the new ref and re-readsbindings[].But this contract is not written down anywhere a channel author would find it. As a result every channel plugin currently in the tree has had to re-discover it independently:
extensions/telegrambot-handlers.runtime.ts:967cfg: telegramDeps.getRuntimeConfig()// "Fresh config for bindings lookup")extensions/qqbotActiveCfgProvider(52-line helper)This is three plugins independently re-rolling the same
ActiveCfgProviderbecause the contract is implicit. Anyone who writes a fourth channel plugin will repeat the cycle.Proposed solution
We see two reasonable directions and would like maintainer guidance before sending a PR. Both can ship; they're not mutually exclusive.
Option A — Docs only (lowest risk, fastest)
Add a short "Routing & live bindings" section to the channel plugin author guide (e.g.
docs/plugins/sdk-runtime.mdor a newdocs/plugins/channel-routing.md) stating the contract:Cross-link from
config-reload-plan.ts:120-140comments back to the doc, and fromresolve-route.ts:205WeakMap declaration ("cache key is cfg reference; stale refs return stale routes by construction").This is 100% within the "Refactor-only PRs are not accepted" exception because it's a docs PR addressing real plugin author confusion.
Option B — Centralize the helper in plugin SDK (eliminates duplicate code)
Export a thin
createActiveCfgProvider({ fallback })fromopenclaw/plugin-sdk/runtime-config-snapshot(or a sibling module). Pattern is verbatim what qqbot PR #73567 added toextensions/qqbot/src/engine/gateway/active-cfg.ts:Then deprecate the inlined copies in telegram + qqbot in follow-ups, on the plugin authors' own schedule. New channel plugins get the helper for free.
This is a small additive SDK change with one preexisting in-tree consumer (qqbot) plus matching telegram pattern — it doesn't introduce a new architecture, just names a pattern that already exists.
Alternatives considered
Flip
bindings/routingtokind: "hot"in the reload plan — already tried in PR Gateway: add zero-latency hot-reload for agent bindings #7747, closed-as-stale. Per-event re-resolve is the chosen direction; we are not asking to revisit this.A
config.reload-bindingsadmin RPC — issue Feature: Hot-reload bindings without gateway restart (SIGUSR2 / config.reload-bindings) #27706, closed-as-stale. Same reason.Leave it as-is and let each channel plugin re-discover — current status. Three plugins, three independent fixes, the third still broken. New channel plugins (Mattermost, MS Teams, IRC, etc. all on the maintainer roster) will repeat.
Impact
openclaw agents bindtaking effect without a gateway restartopenclaw gateway restart, which kills active sessions and reconnects providers (issue Gateway: account-scoped channel config changes restart the entire channel and can disrupt active traffic #43935 documents this cost)Evidence/examples
kind: "none"by design:config-reload-plan.ts:120-140resolve-route.ts:205-217extensions/telegram/src/bot-handlers.runtime.ts:967(cfg: telegramDeps.getRuntimeConfig()// "Fresh config for bindings lookup")extensions/qqbot/src/engine/gateway/active-cfg.ts, references issue [Bug]: QQBot channel does not apply updated bindings until gateway restart #69546Additional information
We're a third-party channel plugin team that just hit this. We'd like to contribute the fix upstream rather than just patching our own plugin. Per CONTRIBUTING.md ("New features / architecture → Start a GitHub Issue first"), we're opening this issue before drafting any PR.
Happy to send the PR for whichever direction you prefer (A, B, or both) once a maintainer signals which is in scope. If you'd rather we don't, that's fine too — we'll just patch around it on our side and move on. Tagging gateway-area maintainers per CONTRIBUTING.md: @frankekn (Gateway/Channels), @joshavant (Gateway), @cpojer (JS Infra) — apologies if this is not the right escalation path.
Real environment we tested in:
main(commitc1ddfccf65c9cf3708771b356090b7331c543f58at time of investigation)openclaw agents add foo --workspace ~/.openclaw/workspaces/foo --non-interactive→openclaw config patch --stdinwritingchannels.<our-channel>.accounts.<botUid>→openclaw agents bind --agent foo --bind <our-channel>:<botUid>→ DM the bot without restarting → routes toagent:maininstead offoo. Confirmed by stepping throughresolveAgentRouteand observing the WeakMap hit returning the pre-bind evaluated bindings.