Skip to content

bug(channels): per-room binding shadowed by sidecar default_agent (instance default outranks peer binding) #6257

Description

@neo-wanderer

Summary

When a sidecar channel is configured with a default_agent (e.g. a Matrix or Telegram adapter), that value is seeded as the channel's instance default (channel_instance_defaults). Any explicit per-conversation [[bindings]] whose match_rule pins a peer_id (a specific room / DM) is then never honored for inbound traffic — every inbound message routes to the instance default_agent, regardless of the room it arrived in.

The per-room binding is effectively dead: it matches the peer but is never consulted.

Repro (minimal)

Config:

[[sidecar_channels]]
name = "matrix"
channel_type = "matrix"
default_agent = "agent-A"      # seeds the channel-wide instance default

[[bindings]]
agent = "agent-B"              # a different agent, bound to one specific room
match_rule = { channel = "matrix", peer_id = "!room:example.org" }
  1. Send an inbound message into !room:example.org.
  2. Expected: routed to agent-B (the explicit per-room binding).
  3. Actual: routed to agent-A (the instance default).

Root cause

Routing precedence in resolve_or_fallback (crates/librefang-channels/src/bridge.rs):

3. /agent conversation override
4. sticky holder (#5323)
5. instance default  (resolve_instance_default)   <-- returns agent-A here
6. [[bindings]] router chain (resolve_with_context) <-- never reached

The instance default (branch 5, boot.rs seeds it from sidecar_channels.agent) is consulted before the [[bindings]] chain (branch 6), which the code comments label a "legacy fallback". But a binding that pins a specific peer_id is more specific than a channel-wide default and should win.

There is also a latent secondary bug that surfaces once precedence is fixed: the router's name→id cache used for binding resolution is seeded at channel-bridge startup from kernel.agent_registry().list_arcs() only — i.e. spawned agents. An agent that is defined but spawns lazily (first cron fire), or one reconciled to Suspended after an unclean shutdown, is absent from that registry at bridge-startup, so even when its binding is reached, the name→id lookup misses and it falls through to the default. The cache should also be seeded from the canonical agent-identity registry (every defined agent), independent of spawn state.

Fix

A PR follows from neo-wanderer:fix/inbound-binding-cache-unspawned-agents:

  1. AgentRouter::resolve_specific_binding — evaluates only bindings that pin a peer_id; consulted in resolve_or_fallback between the sticky holder and the instance default. Channel-only bindings stay in the lower-precedence chain, preserving the existing instance-default-beats-channel-default ordering.
  2. Seed the router name cache from the canonical agent-identity registry in addition to the spawned set, so defined-but-unspawned bound agents resolve.

Unit tests cover the precedence and the specific-binding selection; existing sticky/override precedence tests are unchanged.

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/channelsMessaging channel adaptershas-prA pull request has been linked to this issue

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions