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
Multi-instance sidecar setups (e.g. multiple [[sidecar_channels]] of channel_type = "telegram" for several bots) have two observable symptoms that share one root.
1. Cross-bot routing collision in channel_defaults
Every [[sidecar_channels]] block registers its default_agent under the same bare channel_defaults["<channel_type>"] key. The last sidecar to boot overwrites the previous ones and becomes the de-facto router target for every bot — defeating the purpose of per-block default_agent entries.
Daemon log on beta.16 with 6 Telegram bots (boot order in config):
bot-A default agent: agent-A (...) [channel: telegram] ← bare key
bot-B default agent: agent-B (...) [channel: telegram] ← overwrites A
bot-C default agent: agent-C (...) [channel: telegram] ← overwrites B
bot-D default agent: agent-D (...) [channel: telegram] ← overwrites C
bot-E default agent: agent-E (...) [channel: telegram] ← FINAL — answers in every bot
This happens without any user action: every inbound message in bot-A / bot-B / etc. routes to the last-booted agent.
2. /agent cross-bot leak (regression of #5672 Layer B)
When a user runs /agent <name> in bot-A, the override leaks to every other bot for that user. #5688 added a per-bot path (set_user_default_for_channel keyed by "<channel_type>:<account_id>") but the path is dead code under any sidecar adapter because account_id never reaches message.metadata.
17:23 /agent agent-A in bot-A → Now talking to: agent-A
17:24 → agent-A responds in bot-A ✓
18:07 /agent agent-B in bot-B
18:08 → "agent-A is still answering me 🙂" in bot-B ❌ override stuck to user_id, not (user_id, bot)
Root cause — one wiring gap, two manifestations
Both symptoms stem from the daemon never propagating the operator-known SidecarChannelConfig::name as account_id.
for sidecar_config in&sidecar_cfg.sidecar_channels{let adapter = Arc::new(SidecarAdapter::new(sidecar_config, ...));
adapters.push((adapter, sidecar_config.default_agent.clone(),None));// ^^^^// hardcoded — sidecar_config.name not propagated}
The downstream loop computes channel_key from this tuple:
let channel_key = match account_id {Some(aid) => format!("{channel_type}:{aid}"),// qualified per-botNone => channel_type.to_string(),// BARE — last sidecar wins};
router.set_channel_default_with_name(channel_key, agent_id, name);
With account_id always None, every sidecar collides on the bare key → Symptom 1.
Place 2: inbound metadata missing account_id
crates/librefang-channels/src/sidecar.rs (reader loop in spawn_once) injects into metadata: channel_id, platform, sender_username, group_members, group_participants. No account_id — even though the SDK's account_id_cell.get() already holds it from the ready event.
crates/librefang-channels/src/bridge.rs::dispatch_message (fix from #5688) routes /agent correctly only when account_id is in message.metadata:
match message.metadata.get("account_id").and_then(|v| v.as_str()){Some(aid) => router.set_user_default_for_channel(...),// per-bot (correct)None => router.set_user_default(...),// GLOBAL leak path}
With nothing populating that metadata key for sidecar adapters, the Some(aid) arm is dead code → every /agent falls through to global set_user_default((None, peer_id)) → Symptom 2.
Why this didn't surface before sidecar migration
The pre-#5241 in-process Telegram adapter populated account_id from the per-bot config table. After the sidecar migration, the field moved into the adapter trait surface (SidecarAdapter::account_id()) which defaults to None. The Rust Telegram adapter from #5831 doesn't override it, and the daemon registration path doesn't fall back to sidecar_config.name — so the field stays None end-to-end.
Proposed fix (two-line wiring, daemon-side, no SDK change)
Both fixes key off SidecarChannelConfig::name — the operator-provided value already in TOML, identity-stable across supervised restarts.
channel_bridge.rs::start_channel_bridge_with_config — pass Some(sidecar_config.name.clone()) instead of None.
sidecar.rs reader loop — defensive metadata.entry("account_id").or_insert_with(|| ...adapter_name...) next to the other metadata.insert calls.
Local validation (see test patch in comment below) — cargo fmt --all --check clean, cargo clippy -p librefang-api -p librefang-channels --all-targets -- -D warnings clean, live-tested on a 6-bot setup: qualified keys appear in the boot log, every bot answers from its own default_agent, /agent X issued in bot-A no longer leaks to bot-B.
Multiple [[sidecar_channels]] entries with channel_type = "telegram", each with distinct name + default_agent
PUT /api/agents/{id}/channels with bot-name allowlist is rejected — the API expects channel type ("telegram"), not channel name — so per-agent allowlist can't currently scope to a single bot in multi-Telegram setups (separate concern).
Refs: #5672 (original Layer B), #5688 (the fix whose per-bot branch depends on account_id in metadata), #5831 (Rust sidecar adapter), #5241 (sidecar migration that moved account_id into trait surface).
Symptom
Multi-instance sidecar setups (e.g. multiple
[[sidecar_channels]]ofchannel_type = "telegram"for several bots) have two observable symptoms that share one root.1. Cross-bot routing collision in
channel_defaultsEvery
[[sidecar_channels]]block registers itsdefault_agentunder the same barechannel_defaults["<channel_type>"]key. The last sidecar to boot overwrites the previous ones and becomes the de-facto router target for every bot — defeating the purpose of per-blockdefault_agententries.Daemon log on beta.16 with 6 Telegram bots (boot order in config):
This happens without any user action: every inbound message in bot-A / bot-B / etc. routes to the last-booted agent.
2.
/agentcross-bot leak (regression of #5672 Layer B)When a user runs
/agent <name>in bot-A, the override leaks to every other bot for that user. #5688 added a per-bot path (set_user_default_for_channelkeyed by"<channel_type>:<account_id>") but the path is dead code under any sidecar adapter becauseaccount_idnever reachesmessage.metadata.Root cause — one wiring gap, two manifestations
Both symptoms stem from the daemon never propagating the operator-known
SidecarChannelConfig::nameasaccount_id.Place 1: registration uses bare key
crates/librefang-api/src/channel_bridge.rs::start_channel_bridge_with_configThe downstream loop computes
channel_keyfrom this tuple:With
account_idalwaysNone, every sidecar collides on the bare key → Symptom 1.Place 2: inbound metadata missing
account_idcrates/librefang-channels/src/sidecar.rs(reader loop inspawn_once) injects into metadata:channel_id,platform,sender_username,group_members,group_participants. Noaccount_id— even though the SDK'saccount_id_cell.get()already holds it from thereadyevent.crates/librefang-channels/src/bridge.rs::dispatch_message(fix from #5688) routes/agentcorrectly only whenaccount_idis inmessage.metadata:With nothing populating that metadata key for sidecar adapters, the
Some(aid)arm is dead code → every/agentfalls through to globalset_user_default((None, peer_id))→ Symptom 2.Why this didn't surface before sidecar migration
The pre-#5241 in-process Telegram adapter populated
account_idfrom the per-bot config table. After the sidecar migration, the field moved into the adapter trait surface (SidecarAdapter::account_id()) which defaults toNone. The Rust Telegram adapter from #5831 doesn't override it, and the daemon registration path doesn't fall back tosidecar_config.name— so the field staysNoneend-to-end.Proposed fix (two-line wiring, daemon-side, no SDK change)
Both fixes key off
SidecarChannelConfig::name— the operator-provided value already in TOML, identity-stable across supervised restarts.channel_bridge.rs::start_channel_bridge_with_config— passSome(sidecar_config.name.clone())instead ofNone.sidecar.rsreader loop — defensivemetadata.entry("account_id").or_insert_with(|| ...adapter_name...)next to the othermetadata.insertcalls.Local validation (see test patch in comment below) —
cargo fmt --all --checkclean,cargo clippy -p librefang-api -p librefang-channels --all-targets -- -D warningsclean, live-tested on a 6-bot setup: qualified keys appear in the boot log, every bot answers from its owndefault_agent,/agent Xissued in bot-A no longer leaks to bot-B.Setup (for reference)
[[sidecar_channels]]entries withchannel_type = "telegram", each with distinctname+default_agentPUT /api/agents/{id}/channelswith bot-name allowlist is rejected — the API expects channel type ("telegram"), not channel name — so per-agent allowlist can't currently scope to a single bot in multi-Telegram setups (separate concern).Refs: #5672 (original Layer B), #5688 (the fix whose per-bot branch depends on
account_idin metadata), #5831 (Rust sidecar adapter), #5241 (sidecar migration that movedaccount_idinto trait surface).