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" }
- Send an inbound message into
!room:example.org.
- Expected: routed to
agent-B (the explicit per-room binding).
- 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:
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.
- 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.
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]]whosematch_rulepins apeer_id(a specific room / DM) is then never honored for inbound traffic — every inbound message routes to the instancedefault_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:
!room:example.org.agent-B(the explicit per-room binding).agent-A(the instance default).Root cause
Routing precedence in
resolve_or_fallback(crates/librefang-channels/src/bridge.rs):The instance default (branch 5,
boot.rsseeds it fromsidecar_channels.agent) is consulted before the[[bindings]]chain (branch 6), which the code comments label a "legacy fallback". But a binding that pins a specificpeer_idis 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:AgentRouter::resolve_specific_binding— evaluates only bindings that pin apeer_id; consulted inresolve_or_fallbackbetween 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.Unit tests cover the precedence and the specific-binding selection; existing sticky/override precedence tests are unchanged.