Skip to content

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

Closed
lukeboyett wants to merge 2 commits intomainfrom
fix/matrix-bound-account-subagent-spawn
Closed

fix(agents): prefer target agent's bound Matrix account for subagent spawns#1
lukeboyett wants to merge 2 commits intomainfrom
fix/matrix-bound-account-subagent-spawn

Conversation

@lukeboyett
Copy link
Copy Markdown
Owner

Summary

Fixes Matrix subagent spawns inheriting the caller's accountId instead of the target agent's room-bound account.

  • src/agents/subagent-spawn.ts: adds resolveRequesterOriginForChild(...) that prefers the target agent's bound account via resolveFirstBoundAccountId({ cfg, channelId, agentId: targetAgentId, peerId: requesterTo }) before falling back to the caller's accountId.
  • src/routing/bound-account-read.ts: resolveFirstBoundAccountId now accepts an optional peerId and uses two-tier matching — peer-specific bindings win immediately, channel-only bindings are kept as fallback, non-matching peer bindings are skipped.
  • Preserves caller-fallback behavior and preserves existing channel-only behavior for callers that don't pass peerId (e.g. src/cron/isolated-agent/delivery-target.ts is unaffected).

Why

Before this fix, 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 Matrix identity could end up posting as the caller.

The original resolveFirstBoundAccountId also matched only on channel + agent, so multi-room-bound agents could get the "first binding wins" regardless of which room the spawn was happening in. The two-tier matching fixes that while staying backward-compatible for callers that don't pass a peer.

Regression coverage

src/agents/openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts:

  • sessions_spawn uses the target agent's bound account for a Matrix room-bound route — a room-bound target binding beats the caller's account.
  • sessions_spawn prefers peer-specific binding over channel-only binding — two bindings for the same agent (channel-only default, peer-specific for room A); spawning in room A returns the peer-specific account.
  • sessions_spawn falls back to channel-only binding when peer does not match — same two bindings; spawning in a non-matching room returns the channel-only default.

Existing caller-fallback coverage is provided by sessions_spawn announces with requester accountId.

Validation

  • pnpm build
  • pnpm check ✓ (typecheck, lint, import cycles, boundary checks; also re-run by the pre-commit hook)
  • pnpm test:changed ✓ 3777/3777 (360s)
  • Targeted lifecycle vitest ✓ 8/8
  • Live Matrix readback on a downstream deployment: five bound target agents posted from their correct sender identities after rebuilt runtime.

AI-assisted

Authored with Claude Code (Claude Opus 4.6, 1M-context). Fully tested locally (typecheck + lint + targeted vitest + pnpm test:changed + live Matrix validation). I've reviewed the change and understand what it does.

Related / prior art

@codex review

…spawns

A Matrix-bound parent session spawning a subagent for another agent could
seed the child's deliveryContext.accountId with the caller's account before
target bindings were consulted, causing child posts to come from the wrong
Matrix identity.

- src/agents/subagent-spawn.ts: add resolveRequesterOriginForChild(...) that
  prefers the target agent's bound account via resolveFirstBoundAccountId
  before falling back to the caller's accountId.
- src/routing/bound-account-read.ts: resolveFirstBoundAccountId now accepts
  an optional peerId and uses two-tier matching — peer-specific bindings win
  immediately, channel-only bindings are kept as fallback, non-matching peer
  bindings are skipped. The existing cron delivery-target caller is
  unaffected (it does not pass peerId, so it gets channel-only matching as
  before).

Regression coverage in openclaw-tools.subagents.sessions-spawn.lifecycle.test.ts:
- Matrix room-bound route uses the target agent's bound account over the
  caller's account.
- Peer-specific binding wins over channel-only binding for the same agent.
- Non-matching peer falls back to the channel-only binding.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@lukeboyett
Copy link
Copy Markdown
Owner Author

@codex review

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: 5d11d7bb1e

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

Comment thread src/routing/bound-account-read.ts
…nt lookup

`resolveFirstBoundAccountId` previously compared `resolved.peerId` as a
literal string, so a binding with `match.peer.id: "*"` would never match
real peer IDs and would get skipped. That could make child spawn identity
selection fall through to a channel-only or requester account instead of
the target agent's wildcard-bound account.

Align the helper's three-tier precedence with `resolve-route.ts`:
1. Exact peer match wins immediately.
2. Wildcard peer match (`peer.id: "*"`) wins over channel-only.
3. Channel-only binding is the final fallback.

Addresses Codex review comment on PR #1.

Regression coverage:
- Wildcard peer binding matches a caller with any peer and beats a
  channel-only binding for the same agent.
- Exact peer binding still wins over a wildcard binding when both exist.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@lukeboyett
Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🚀

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

@lukeboyett
Copy link
Copy Markdown
Owner Author

Review consensus reached. Codex approved after one P1 fix (wildcard peer binding handling). Squashed to a single commit and opened upstream as openclaw#67508. Closing this fork PR as the review record.

@lukeboyett lukeboyett closed this Apr 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant