openclaw - ✅(Solved) Fix resolveFirstBoundAccountId should honor guild/team/role constraints on AgentRouteBinding [1 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#67749Fetched 2026-04-17 08:29:29
View on GitHub
Comments
0
Participants
1
Timeline
0
Reactions
0
Participants

resolveFirstBoundAccountId in src/routing/bound-account-read.ts matches AgentRouteBindings by channel, agentId, and (as of #67508) peer.id + peer.kind. It does not honor the remaining constraint fields on AgentBindingMatch:

  • guildId (Discord server)
  • teamId (Slack/Teams workspace)
  • roles (Discord role-based routing)

Root Cause

resolveFirstBoundAccountId in src/routing/bound-account-read.ts matches AgentRouteBindings by channel, agentId, and (as of #67508) peer.id + peer.kind. It does not honor the remaining constraint fields on AgentBindingMatch:

  • guildId (Discord server)
  • teamId (Slack/Teams workspace)
  • roles (Discord role-based routing)

PR fix notes

PR #67508: fix(agents): prefer target agent's bound Matrix account for subagent spawns

Description (problem / solution / changelog)

Summary

Fixes Matrix (and other-channel) subagent spawns inheriting the caller's accountId instead of the target agent's bound account, and hardens resolveFirstBoundAccountId with kind-aware multi-tier matching for peer bindings.

  • src/agents/subagent-spawn.ts: adds resolveRequesterOriginForChild(...) that prefers the target agent's bound account via resolveFirstBoundAccountId({ cfg, channelId, agentId: targetAgentId, peerId, peerKind }) before falling back to the caller's accountId. Same-agent spawns short-circuit the lookup and preserve the caller's account. A new extractRequesterPeer helper peels known delivery-target prefixes (<channel>: namespace, then a generic <word>: loop) from agentTo to produce the raw peer id and — for channels whose plugin does not implement inferTargetChatType — infer peerKind from kind-prefixes (room:/channel:/chat: → channel, group:/team: → group, user:/dm:/pm: → direct) and id-embedded markers (Matrix !/@, IRC #). Id-embedded kind markers take precedence over prefix-derived kind so wrappers like room:@user:server classify correctly.
  • src/routing/bound-account-read.ts: resolveFirstBoundAccountId now accepts optional peerId and peerKind, and uses a four-tier precedence model:
    1. Exact peer id match (with kind cross-check when both sides declare one) wins immediately.
    2. Wildcard peer (peer.id: "*") match — only when the caller supplies a peer AND both sides declare a matching kind (treating group/channel as equivalent, mirroring peerKindMatches in resolve-route.ts).
    3. Channel-only binding (no peer) as the default fallback.
    4. Peerless last-resort fallback (first peer-specific or wildcard binding found) for callers that pass no peerId — preserves the pre-existing first-match semantics for cron delivery resolution.
  • Existing cron delivery-target.ts caller is unaffected: it still passes no peerId/peerKind, and the helper returns the same binding it would have before on realistic configs.

Why

A Matrix-bound parent session spawning a child for another agent could seed deliveryContext.accountId with the caller's account before target bindings were consulted. A child intended to post as the target agent's bound identity could end up posting as the caller — broken speaker identity in shared rooms, nondeterministic debugging, and wrong attribution in multi-agent deployments.

The original resolveFirstBoundAccountId also matched only on channel + agent, so multi-room-bound agents with different accounts per room got "first binding wins" regardless of active room. The peer-id/peer-kind aware matching fixes that while staying backward-compatible for callers that don't pass a peer (cron).

Regression coverage

Twelve unit tests in src/routing/bound-account-read.test.ts (new file) exercise the four-tier lookup: exact peer match, wildcard peer beats channel-only (with caller kind), channel-only beats wildcard (no caller kind), peer-specific fallback for peerless callers, non-matching peer skipped, channel mismatch, kind filtering, kind-unknown wildcard skip, exact id with unknown kind, group/channel equivalence, and the agent-on-different-channel no-match case.

Fifteen lifecycle tests in src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts (eleven new):

  1. Matrix room-bound route uses the target agent's bound account over the caller's.
  2. Peer-specific binding wins over channel-only binding for the same agent.
  3. Non-matching peer falls back to the channel-only binding.
  4. Wildcard peer binding matches any peer and beats channel-only.
  5. Exact peer binding wins over a wildcard peer binding.
  6. Same-agent subagent spawn preserves the caller's account (no re-resolution).
  7. Channel-side prefixes stripped from agentTo before lookup (Matrix room:<id>).
  8. <channel>:<kind>:<id> targets peel both prefixes (LINE line:group:<id>) and pick the group-kinded binding over a conflicting direct-kinded wildcard.
  9. conversation:<id> prefix stripped for Teams-style targets.
  10. Matrix room:@user:server classified as direct, not channel, so direct-peer bindings match.

Validation

  • pnpm build
  • pnpm check ✓ (typecheck, lint, import cycles, boundary checks)
  • pnpm test:changed ✓ (3777 passed on round 1)
  • Targeted vitest across all nine rounds ✓ (27/27 on current HEAD, 12 unit + 15 lifecycle)
  • Live Matrix readback on a downstream deployment (original round-1 fix): five bound target agents posted from their correct sender identities after rebuilt runtime.

Review iterations

This PR went through eight rounds of bot review (Codex + Greptile) before final approval. Commit history reflects each iteration:

CommitAddressed
5d11d7bb1eInitial fix (Matrix bound-account propagation)
5d0027479eCodex P1: wildcard * treated as literal → three-tier precedence
00b393064cCodex+Greptile P1s: same-agent spawn preserve caller; peerless wildcard; peerless peer-specific fallback
f8430fd70bCodex P1: drop peer.kind → kind-aware matching + plugin inference
d662dc529bCodex P1: unnormalized delivery-target prefixes → normalizeRequesterPeerId
3f673354a4Three findings: LINE <channel>:<kind>:<id> parsing; peerless wildcard fallback restored; group/channel kind equivalence
173827d6a9Codex P1: Teams conversation:<id> → generic ^[a-z][a-z0-9_-]*: peeler
249b292111Codex P1: id-embedded kind markers override prefix-derived kind (Matrix room:@user → direct)

Final state: all twelve bot review threads resolved, Codex Didn't find any major issues on the current HEAD.

AI-assisted

Authored with Claude Code (Claude Opus 4.6, 1M-context). Fully tested locally. I've reviewed the change and understand what it does.

Related / prior art

  • #54479 (open, adjacent): seeds subagent deliveryContext from spawn params at the gateway layer — complementary, different layer.
  • #51523 (open, adjacent): Matrix ACP room-binding subagent hooks — complementary.
  • #28399 (closed, prior art): Matrix accountId forwarding in sendMessage.
  • #58228, #60634 (closed, prior art): analogous Feishu fixes for the same bug class on another channel.

Known-red CI

  • Install Smoke is red due to a pre-existing build-system gap (build:docker in package.json doesn't run scripts/write-npm-update-compat-sidecars.ts, unlike build-all.mjs). Root cause documented in #issuecomment-4261219649 with a proposed one-line fix — happy to include here or split out per maintainer preference.
  • Run the GPT-5.4 / Opus 4.6 parity gate against the qa-lab mock is red and looks unrelated to this PR.

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • extensions/discord/src/monitor/agent-components.ts (modified, +1/-0)
  • extensions/discord/src/monitor/message-handler.preflight.ts (modified, +1/-0)
  • extensions/discord/src/monitor/message-handler.preflight.types.ts (modified, +1/-0)
  • extensions/discord/src/monitor/message-handler.process.ts (modified, +2/-0)
  • extensions/discord/src/monitor/native-command-context.test.ts (modified, +3/-0)
  • extensions/discord/src/monitor/native-command-context.ts (modified, +6/-0)
  • extensions/discord/src/monitor/native-command.ts (modified, +2/-0)
  • extensions/msteams/src/monitor-handler/message-handler.authz.test.ts (modified, +1/-0)
  • extensions/msteams/src/monitor-handler/message-handler.ts (modified, +1/-0)
  • extensions/slack/src/monitor/message-handler/prepare.test.ts (modified, +1/-0)
  • extensions/slack/src/monitor/message-handler/prepare.ts (modified, +1/-0)
  • extensions/slack/src/monitor/slash.test.ts (modified, +2/-1)
  • extensions/slack/src/monitor/slash.ts (modified, +1/-0)
  • src/agents/acp-spawn.test.ts (modified, +110/-0)
  • src/agents/acp-spawn.ts (modified, +32/-21)
  • src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts (modified, +393/-0)
  • src/agents/openclaw-tools.ts (modified, +1/-0)
  • src/agents/pi-embedded-runner/run.ts (modified, +1/-0)
  • src/agents/pi-embedded-runner/run/attempt.ts (modified, +1/-0)
  • src/agents/pi-embedded-runner/run/params.ts (modified, +2/-0)
  • src/agents/pi-tools.ts (modified, +3/-0)
  • src/agents/spawn-requester-origin.test.ts (added, +201/-0)
  • src/agents/spawn-requester-origin.ts (added, +117/-0)
  • src/agents/spawned-context.ts (modified, +1/-0)
  • src/agents/subagent-spawn.test.ts (modified, +7/-10)
  • src/agents/subagent-spawn.thread-binding.test.ts (modified, +111/-0)
  • src/agents/subagent-spawn.ts (modified, +14/-7)
  • src/agents/tools/sessions-spawn-tool.ts (modified, +3/-0)
  • src/auto-reply/reply/agent-runner-utils.test.ts (modified, +2/-0)
  • src/auto-reply/reply/agent-runner-utils.ts (modified, +10/-0)
  • src/auto-reply/templating.ts (modified, +2/-0)
  • src/cron/isolated-agent/delivery-target.test.ts (modified, +44/-0)
  • src/routing/binding-scope.ts (added, +59/-0)
  • src/routing/bound-account-read.test.ts (added, +485/-0)
  • src/routing/bound-account-read.ts (modified, +102/-4)
  • src/routing/peer-kind-match.ts (added, +11/-0)
  • src/routing/resolve-route.ts (modified, +9/-31)
RAW_BUFFERClick to expand / collapse

Context

resolveFirstBoundAccountId in src/routing/bound-account-read.ts matches AgentRouteBindings by channel, agentId, and (as of #67508) peer.id + peer.kind. It does not honor the remaining constraint fields on AgentBindingMatch:

  • guildId (Discord server)
  • teamId (Slack/Teams workspace)
  • roles (Discord role-based routing)

Impact

For multi-tenant setups where the same agent has bindings scoped by guild/team/roles with different accounts, the helper picks the first matching binding by declaration order regardless of the caller's actual guild/team/role context. A Discord/Teams user in Guild A can get routed to Guild B's configured account, and cross-agent subagent spawns can emit messages from the wrong tenant identity.

Scope

This gap is pre-existing — the prior sole caller (src/cron/isolated-agent/delivery-target.ts) also ignored these fields. #67508 added a second caller (subagent-spawn), which makes the limitation more visible but does not introduce a regression vs. main.

Prerequisite / dependency

Blocked on #67508 being merged first. The signature of resolveFirstBoundAccountId and the shape of resolveNormalizedBindingMatch are actively changing in #67508 (new peerId/peerKind params, four-tier precedence, kind-safety rules). The exact design of the guild/team/roles fix — which fields to add, how the filtering tiers interact, and how SpawnSubagentContext should expose teamId/roles — depends on the final merged state of #67508.

If #67508 is merged as-is, the fix can extend the existing helper signature. If maintainers ask for #67508 to be split, or restructure the helper differently during review, this follow-up's implementation will need to adjust accordingly. Recommend no work starts on this until #67508 lands (or is clearly going to be reshaped).

What a proper fix needs

(Subject to the final shape of #67508.)

  1. Extend resolveFirstBoundAccountId signature to accept optional guildId, teamId, memberRoleIds.
  2. Extend resolveNormalizedBindingMatch to preserve these fields.
  3. Filter bindings: when both caller and binding declare a constraint, require it to match (parallel to the peer-kind safety in #67508). Role matching should use set-intersection semantics consistent with resolve-route.ts.
  4. Surface teamId / memberRoleIds through SpawnSubagentContext so subagent-spawn.ts can pass them (the current context exposes agentGroupId but not teamId/roles). This is the larger plumbing piece.
  5. Regression tests covering Discord guild-scoped bindings and Slack/Teams workspace-scoped bindings.

References

AI-assisted

Drafted with Claude Code during #67508 review iterations.

extent analysis

TL;DR

Extend the resolveFirstBoundAccountId function to accept and filter by guildId, teamId, and memberRoleIds to ensure correct routing in multi-tenant setups.

Guidance

  • Review the final merged state of #67508 to determine the exact design and implementation of the fix.
  • Extend the resolveFirstBoundAccountId signature to include optional guildId, teamId, and memberRoleIds parameters.
  • Update resolveNormalizedBindingMatch to preserve these fields and filter bindings based on matching constraints.
  • Modify SpawnSubagentContext to expose teamId and memberRoleIds to enable passing them to subagent-spawn.ts.

Example

No code snippet is provided as the implementation details depend on the final shape of #67508.

Notes

The fix is blocked on #67508 being merged, and its implementation may need to be adjusted based on the final merged state.

Recommendation

Apply a workaround by extending the resolveFirstBoundAccountId function to accept and filter by guildId, teamId, and memberRoleIds once #67508 is merged, as this will ensure correct routing in multi-tenant setups.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix resolveFirstBoundAccountId should honor guild/team/role constraints on AgentRouteBinding [1 pull requests, 1 participants]