You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
bug(channels): inbound routing falls to non-deterministic "first available" agent when SidecarChannelConfig has no default + empty channels allowlist accepts all (related #4926) #5294
After migrating Telegram (and other channels) to sidecar via #5241, the SidecarChannelConfig struct has no default_agent field — only name, command, args, env, channel_type, restart/overflow knobs. The old [channels.telegram] config had default_agent = "X" which used to seed the AgentRouter.channel_defaults at boot. With sidecar, that path is gone.
Result: when an inbound message arrives on a sidecar channel, AgentRouter::resolve_with_context (crates/librefang-channels/src/router.rs:202) returns None because:
No binding for (channel, peer_id) set
No channel_defaults["telegram"]
No default_agent
Then resolve_or_fallback (crates/librefang-channels/src/bridge.rs:2780) tries find_agent_by_name("assistant") → typically None → falls to "first available agent" — which is whichever agent the registry happens to iterate first (non-deterministic, ID/spawn-order-dependent).
Repro
Existing setup: agent fandangorodelo with channels = ["telegram"] in its manifest, configured Telegram via [[sidecar_channels]].
Create a second agent (any purpose, e.g. Profesor) without restricting its channels list (defaults to channels = []).
Send a Telegram message to the bot.
Inbound message routes to Profesor instead of fandangorodelo because (a) router has no channel_defaults["telegram"], (b) router falls to first-available, (c) Profesor was created later but happens to win the iteration order.
Confirmed empirically in our deploy:
DEBUG run_agent_loop: Memory decision: ADD new: Has generated file at `output/horario_definitivo.md`...
agent.name=Profesor agent.id=600770a9-bfbc-5e93-b83d-c9e7729ca55c session.id=9bdaf4de-...
The user wrote to fandangorodelo on Telegram; the loop ran on Profesor. File ended up in ~/.librefang/workspaces/agents/Profesor/output/.
Root cause — 3 layers
Layer A: SidecarChannelConfig (crates/librefang-types/src/config/types.rs:2155) is missing default_agent: Option<String>. The in-process TelegramConfig/DiscordConfig/etc. all have it.
Layer B: channels = [] on an agent means "accepts all channels" (no allowlist filter applied at resolve_or_fallback line 2814). This is opt-out-by-default. Sane default would be opt-in: [] = no inbound channels, agent must explicitly declare "telegram" to receive Telegram traffic.
Layer C: The "first available agent" fallback in resolve_or_fallback (around line 2829) is non-deterministic. When the router resolves nothing, the bridge falls to handle.find_agent_by_name(\"assistant\") and then to the first one in list(). Neither is a clear contract for the operator. A misconfigured deployment silently routes to a wrong agent instead of erroring loud.
Workaround applied locally
POST /api/bindings { agent: \"fandangorodelo\", match_rule: { channel: \"telegram\" } } — adds an explicit binding for all Telegram traffic to fandango. Works without restart. But this is a per-deployment manual step that shouldn't be necessary; operators creating a new agent should not silently break inbound routing for their existing agents.
Proposal
Add default_agent: Option<String> to SidecarChannelConfig so sidecar parity matches in-process channel configs. Wire it into router population at boot (api/src/channel_bridge.rs:3320-3344 already has the loop for in-process channels — add the equivalent for sidecar_channels).
Or, at minimum: when no binding, no channel_default, no default_agent matches, return error loud instead of silently picking first-available. The current behavior is a footgun — every new agent created is a potential routing accident.
Related: #4926 (N:M agent-channel), #4961 (allowlist field — already adds the channels field but doesn't flip the default semantics).
Summary
After migrating Telegram (and other channels) to sidecar via #5241, the
SidecarChannelConfigstruct has nodefault_agentfield — onlyname,command,args,env,channel_type, restart/overflow knobs. The old[channels.telegram]config haddefault_agent = "X"which used to seed theAgentRouter.channel_defaultsat boot. With sidecar, that path is gone.Result: when an inbound message arrives on a sidecar channel,
AgentRouter::resolve_with_context(crates/librefang-channels/src/router.rs:202) returnsNonebecause:(channel, peer_id)setchannel_defaults["telegram"]default_agentThen
resolve_or_fallback(crates/librefang-channels/src/bridge.rs:2780) triesfind_agent_by_name("assistant")→ typicallyNone→ falls to "first available agent" — which is whichever agent the registry happens to iterate first (non-deterministic, ID/spawn-order-dependent).Repro
fandangorodelowithchannels = ["telegram"]in its manifest, configured Telegram via[[sidecar_channels]].Profesor) without restricting itschannelslist (defaults tochannels = []).Profesorinstead offandangorodelobecause (a) router has nochannel_defaults["telegram"], (b) router falls to first-available, (c)Profesorwas created later but happens to win the iteration order.Confirmed empirically in our deploy:
The user wrote to
fandangorodeloon Telegram; the loop ran onProfesor. File ended up in~/.librefang/workspaces/agents/Profesor/output/.Root cause — 3 layers
Layer A:
SidecarChannelConfig(crates/librefang-types/src/config/types.rs:2155) is missingdefault_agent: Option<String>. The in-processTelegramConfig/DiscordConfig/etc. all have it.Layer B:
channels = []on an agent means "accepts all channels" (no allowlist filter applied atresolve_or_fallbackline 2814). This is opt-out-by-default. Sane default would be opt-in:[]= no inbound channels, agent must explicitly declare "telegram" to receive Telegram traffic.Layer C: The "first available agent" fallback in
resolve_or_fallback(around line 2829) is non-deterministic. When the router resolves nothing, the bridge falls tohandle.find_agent_by_name(\"assistant\")and then to the first one inlist(). Neither is a clear contract for the operator. A misconfigured deployment silently routes to a wrong agent instead of erroring loud.Workaround applied locally
POST /api/bindings { agent: \"fandangorodelo\", match_rule: { channel: \"telegram\" } }— adds an explicit binding for all Telegram traffic to fandango. Works without restart. But this is a per-deployment manual step that shouldn't be necessary; operators creating a new agent should not silently break inbound routing for their existing agents.Proposal
default_agent: Option<String>toSidecarChannelConfigso sidecar parity matches in-process channel configs. Wire it into router population at boot (api/src/channel_bridge.rs:3320-3344already has the loop for in-process channels — add the equivalent for sidecar_channels).channels = []semantics to opt-in (breaking): empty allowlist means "no inbound channels" instead of "all channels". Operators upgrading need to add explicit channel lists to existing agents. Provide a one-shot migration helper. Related to feat(channels): N:M agent-channel assignment — replace single default_agent with multi-agent routing #4926 (N:M routing).Related: #4926 (N:M agent-channel), #4961 (allowlist field — already adds the
channelsfield but doesn't flip the default semantics).Logs / DB snapshots available on request.