Skip to content

fix(qqbot): re-evaluate routing bindings per inbound message#73567

Merged
steipete merged 4 commits into
openclaw:mainfrom
statxc:fix/qqbot-dynamic-bindings-69546
May 11, 2026
Merged

fix(qqbot): re-evaluate routing bindings per inbound message#73567
steipete merged 4 commits into
openclaw:mainfrom
statxc:fix/qqbot-dynamic-bindings-69546

Conversation

@statxc

@statxc statxc commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Problem: QQBot routing kept using the cfg snapshot captured at gateway startup, so peer-specific bindings added through the CLI (bindings=2 in openclaw agents list --bindings) were ignored at runtime (bindings=1) until the gateway was restarted.
  • Why it matters: Messages routed to the wrong agent until restart, even though the CLI confirmed the new binding. "Bindings are dynamic" was effectively false for QQBot.
  • What changed: Added a small ActiveCfgProvider that reads the live getRuntimeConfig() from the plugin SDK on every inbound and falls back to the startup snapshot when the runtime registry is unavailable. handleMessage now resolves cfg through the provider, so both buildInboundContext (which feeds runAccessStageresolveAgentRoute) and dispatchOutbound see the latest bindings without restart.
  • What did NOT change (scope boundary): Connection-level cfg in GatewayConnection (lifecycle, not routing) is intentionally untouched. The bridge extensions/qqbot/src/bridge/gateway.ts keeps forwarding the caller's cfg — that value now serves as the engine's fallback when the SDK getter is not yet ready (e.g. very early startup, isolated tests).

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

Root Cause (if applicable)

  • Root cause: extensions/qqbot/src/engine/gateway/gateway.ts captured ctx.cfg once when startGateway was called and reused that exact object reference inside the handleMessage closure for every inbound. The routing resolver in src/routing/resolve-route.ts keys its evaluated-bindings cache with WeakMap<OpenClawConfig, …>, so the same cfg reference always returned the cached result — the new bindings on disk never reached resolveAgentRoute.
  • Missing detection / guardrail: No regression test asserted that the access stage re-evaluates bindings against a fresh cfg between inbounds.
  • Contributing context: Unlike Telegram, the QQBot engine never had a per-event cfg getter. There is no event-driven binding-invalidation surface in core; channels are expected to fetch fresh config per decision (see docs/plugins/sdk-runtime.md).

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file:
    • extensions/qqbot/src/engine/gateway/active-cfg.test.ts — unit tests for the helper (fresh on every call, fallback when fetcher returns undefined or throws, delegation to SDK).
    • extensions/qqbot/src/engine/gateway/stages/access-stage.test.ts — seam test asserting runAccessStage re-evaluates resolveAgentRoute against the cfg supplied on each call (account-only → peer binding flip; never reads from a previous cfg reference).
  • Scenario the test should lock in: Two consecutive runAccessStage calls with cfg-A (account binding only) then cfg-B (peer binding added) must produce different route.agentIds, with resolveAgentRoute receiving each cfg by reference.
  • Why this is the smallest reliable guardrail: The bug surfaces at the exact moment cfg crosses the channel/router boundary; the seam test covers that boundary without spinning up the full inbound pipeline or a live gateway.

User-visible / Behavior Changes

CLI binding edits (peer or account) take effect on the next QQBot inbound message. openclaw gateway restart is no longer required after agents bind add/bind remove. No config knobs added; no defaults changed.

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

The new helper reads the same runtime config that core already exposes through openclaw/plugin-sdk/runtime-config-snapshot. No new I/O, no new privileges.

Repro + Verification

Environment

  • OS: Ubuntu 22.04 (Docker)
  • Runtime/container: Docker (Ubuntu image)
  • Model/provider: modelstudio/qwen3.5-plus (per the issue reporter; the bug is provider-agnostic)
  • Integration/channel: QQBot (C2C)
  • Relevant config (redacted): qqbot.accountId=study; bindings start with one account-level entry, peer entry added mid-run

Steps

  1. Start OpenClaw with QQBot configured and one account-level binding.
  2. Add a peer-specific binding via CLI: openclaw agents bind add ....
  3. Confirm openclaw agents list --bindings shows two bindings.
  4. Send a QQBot C2C message from the targeted peer without restarting the gateway.
  5. Inspect the routing log line.

Expected

  • [routing] resolveAgentRoute ... bindings=2 and matchedBy=binding.peer, routed to the peer-bound agent.

Actual (before fix)

  • [routing] resolveAgentRoute ... bindings=1 and matchedBy=binding.account, routed to the account-level agent until restart.

Actual (after fix)

  • Matches the Expected line above on the very next inbound, no restart needed.

Evidence

  • Failing test/log before + passing after
    • pnpm test extensions/qqbot/src/engine/gateway/active-cfg.test.ts extensions/qqbot/src/engine/gateway/stages/access-stage.test.ts9 / 9 pass on the merged tree.
    • Full QQBot suite (pnpm test extensions/qqbot): 396 / 396 pass.
  • Trace/log snippets — quoted in the issue body ([Bug]: QQBot channel does not apply updated bindings until gateway restart #69546)
  • Screenshot/recording
  • Perf numbers (if relevant) — N/A; one extra call to getRuntimeConfig() per inbound (a registry lookup).

Human Verification (required)

  • Verified scenarios:
    • Helper returns the freshly fetched value on every call (unit test).
    • Helper falls back to the startup snapshot when the SDK getter returns undefined or throws (unit test).
    • runAccessStage re-routes when a new cfg with an additional peer binding is supplied (seam test).
    • Full extensions/qqbot Vitest suite passes locally.
    • pnpm tsgo:extensions clean; oxfmt --check clean; oxlint clean on touched files.
  • Edge cases checked:
    • SDK getter throws → fallback used (no inbound crash).
    • SDK getter returns undefined → fallback used.
    • Provider called repeatedly → no caching, fetcher called every time.
  • What I did not verify:
    • Live end-to-end with a real QQBot account (no QQBot test bot configured locally). Behavior is verified via seam test that drives resolveAgentRoute with the same shape the framework injects.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

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

The bridge-level cfg is still accepted and now serves as the fallback for the engine's per-event lookup, so existing callers (including third-party drivers of coreStartGateway) keep working without changes.

@greptile-apps

greptile-apps Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes stale routing in the QQBot engine by introducing an ActiveCfgProvider that calls getRuntimeConfig() on each inbound message instead of reusing the ctx.cfg snapshot captured at gateway startup. Both buildInboundContext and dispatchOutbound now receive the same freshly resolved config per message, matching the pattern already used by the Telegram integration. The ui/src/ui/app-view-state.ts change is an unrelated import reorder.

Confidence Score: 5/5

Safe to merge — targeted, well-tested fix with no new I/O, no auth changes, and clean fallback behavior.

The change is minimal and focused: one new helper module, one call-site update, and two test files. The fallback-to-snapshot path is covered by tests, the no-caching invariant is explicitly asserted, and the seam test locks in the cross-cfg routing behavior that was the root cause. No P0 or P1 findings.

No files require special attention.

Reviews (1): Last reviewed commit: "Merge branch 'main' of https://github.co..." | Re-trigger Greptile

@clawsweeper

clawsweeper Bot commented Apr 28, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge.

Summary
The branch adds a QQBot active runtime-config provider, uses it for each inbound routing/outbound dispatch decision, adds QQBot regression tests, adjusts one Codex test cast, and records the fix in the changelog.

Reproducibility: yes. The linked bug gives concrete QQBot C2C steps and before/after routing logs, and current main still passes startup ctx.cfg into the route resolver; I did not run a live QQBot account in this read-only review.

Real behavior proof
Needs real behavior proof before merge: Needs real behavior proof before merge: the PR body explicitly says no live QQBot account was used after the fix, so the contributor should add a redacted runtime log, terminal output, screenshot, recording, or linked artifact and update the PR body to trigger re-review.

Next step before merge
This needs contributor or maintainer follow-up for real QQBot runtime proof; I found no narrow code defect for an automated repair branch.

Security
Cleared: The diff uses an existing plugin SDK runtime-config helper and adds tests/changelog only, with no dependency, workflow, secret, permission, package-resolution, or new network-call change.

Review details

Best possible solution:

Land the QQBot-side per-inbound active runtime config lookup after redacted live QQBot/log proof is added; the fix should remain localized to the QQBot gateway boundary.

Do we have a high-confidence way to reproduce the issue?

Yes. The linked bug gives concrete QQBot C2C steps and before/after routing logs, and current main still passes startup ctx.cfg into the route resolver; I did not run a live QQBot account in this read-only review.

Is this the best way to solve the issue?

Mostly yes. Resolving active runtime config at the QQBot inbound boundary matches the SDK runtime contract and Telegram's existing pattern; the remaining blocker is real behavior proof, not a different code direction.

Acceptance criteria:

  • pnpm test extensions/qqbot/src/engine/gateway/active-cfg.test.ts extensions/qqbot/src/engine/gateway/stages/access-stage.test.ts
  • pnpm test extensions/qqbot
  • pnpm tsgo:extensions

What I checked:

  • Current main still uses startup config for QQBot inbound routing: The QQBot gateway message handler passes the long-lived startup ctx.cfg into buildInboundContext, and later passes the same startup config into dispatchOutbound, so current main still has the stale-config routing path this PR targets. (extensions/qqbot/src/engine/gateway/gateway.ts:113, 9d89a3be6028)
  • Access stage makes the gateway config choice decisive: runAccessStage forwards the injected cfg directly to runtime.channel.routing.resolveAgentRoute, so the gateway handler's config object determines which bindings are evaluated for QQBot inbounds. (extensions/qqbot/src/engine/gateway/stages/access-stage.ts:28, 9d89a3be6028)
  • Route caches are keyed by config object: The generic route resolver stores evaluated bindings and resolved routes in WeakMaps keyed by OpenClawConfig, matching the reported failure mode when a stale startup config object is reused after bindings change. (src/routing/resolve-route.ts:204, 9d89a3be6028)
  • Bindings are intended to hot-apply: The gateway reload plan classifies the top-level bindings prefix as kind: "none", supporting the reporter's expectation that binding edits should not require a gateway restart. (src/gateway/config-reload-plan.ts:114, 9d89a3be6028)
  • Runtime config contract supports this boundary: The plugin runtime docs say long-lived handlers may use the current process snapshot when config can change after handler creation, and provider/channel execution paths must use the active runtime config snapshot. Public docs: docs/plugins/sdk-runtime.md. (docs/plugins/sdk-runtime.md:32, 9d89a3be6028)
  • PR wires fresh config into the QQBot inbound boundary: The PR diff creates an active config provider, resolves activeCfg once per inbound, and passes that value to both buildInboundContext and dispatchOutbound. (extensions/qqbot/src/engine/gateway/gateway.ts:116, cbc1c57da442)

Likely related people:

  • cxyhhhhh: GitHub commit history shows this person authored the major QQBot group/C2C architecture refactor that extracted the inbound pipeline and access stage involved in the stale config path. (role: QQBot gateway history contributor; confidence: high; commits: 5ccf179a34a9; files: extensions/qqbot/src/engine/gateway/gateway.ts, extensions/qqbot/src/engine/gateway/stages/access-stage.ts, extensions/qqbot/src/bridge/gateway.ts)
  • sliverp: GitHub commit history attributes the original QQBot channel plugin addition to this handle, and the PR timeline shows this person assigned themselves on the current review thread. (role: original QQBot channel introducer and assigned area reviewer; confidence: medium; commits: bf6f506dfae6; files: extensions/qqbot, docs/channels/qqbot.md, .github/labeler.yml)

Remaining risk / open question:

  • No redacted live QQBot after-fix log, terminal output, screenshot, recording, or linked artifact has been provided yet; tests alone do not prove the contributor's real runtime setup.

Codex review notes: model gpt-5.5, reasoning high; reviewed against 0b79d7cb3a04.

@statxc
statxc force-pushed the fix/qqbot-dynamic-bindings-69546 branch from c0c5380 to b6455d6 Compare April 29, 2026 17:57
@openclaw-barnacle openclaw-barnacle Bot removed the app: web-ui App: web-ui label Apr 29, 2026
@statxc
statxc force-pushed the fix/qqbot-dynamic-bindings-69546 branch from b6455d6 to bb53bef Compare April 29, 2026 19:35
@sliverp sliverp self-assigned this May 8, 2026
@statxc

statxc commented May 8, 2026

Copy link
Copy Markdown
Contributor Author

Hi @sliverp Thanks for assigning. Could you let me know what I should do more?

@openclaw-barnacle openclaw-barnacle Bot added the triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. label May 10, 2026
@statxc
statxc force-pushed the fix/qqbot-dynamic-bindings-69546 branch from 13fc546 to cbc1c57 Compare May 10, 2026 18:24
@steipete
steipete force-pushed the fix/qqbot-dynamic-bindings-69546 branch from cbc1c57 to edbe524 Compare May 11, 2026 13:11
steipete added a commit to statxc/openclaw that referenced this pull request May 11, 2026
@steipete steipete added proof: override Maintainer override for the external PR real behavior proof gate. and removed triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels May 11, 2026
statxc and others added 4 commits May 11, 2026 14:14
QQBot's gateway captured `ctx.cfg` once at startup and reused that
reference for every inbound, so peer-specific bindings added via the
CLI were ignored until the gateway restarted (the routing resolver
caches evaluated bindings keyed by the cfg object reference).

Add a small `ActiveCfgProvider` that reads `getRuntimeConfig()` from
the plugin SDK on every event and falls back to the startup snapshot
when the runtime registry is not populated, mirroring Telegram's
per-event lookup pattern. Wire it into `handleMessage` so both the
inbound pipeline and outbound dispatch run against the live config.

Fixes openclaw#69546.
@steipete
steipete force-pushed the fix/qqbot-dynamic-bindings-69546 branch from edbe524 to e6a87cb Compare May 11, 2026 13:15
@steipete
steipete merged commit 97bf9bf into openclaw:main May 11, 2026
93 of 94 checks passed
@steipete

Copy link
Copy Markdown
Contributor

Landed via rebase onto main.

Known CI note: the failing checks-node-agentic-plugin-sdk lane was unrelated to this PR (src/plugin-sdk/channel-streaming.test.ts; no PR diff in src/plugin-sdk/channel-streaming*) and latest main already had a CI failure in that area (run https://github.com/openclaw/openclaw/actions/runs/25672134482).

Thanks @statxc!

@statxc

statxc commented May 11, 2026

Copy link
Copy Markdown
Contributor Author

Thanks @steipete @sliverp. I will keep contributing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: qqbot extensions: codex proof: override Maintainer override for the external PR real behavior proof gate. size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: QQBot channel does not apply updated bindings until gateway restart

3 participants