Skip to content

feat(channels): per-conversation agent routing — extend ThreadOwnership + alias triggering for multi-agent groups #5323

Description

@DaBlitzStein

Context

Builds on top of merged work in this area:

And work that was attempted but never landed:

The gap (multi-agent groups)

When a Telegram (or any) group has more than one LibreFang agent joined, the current code can't route an @agentB mention (or an alias declared by agentB) to agentB if agentB is not the channel's default_agent. Concrete failure modes verified in code, not speculation:

  1. Aliases wiring is single-agent. crates/librefang-api/src/channel_bridge.rs:1934-1969 only loads the default agent's metadata.routing.aliases/weak_aliases into group_trigger_patterns. Aliases of non-default agents joined to the same channel are never merged.
  2. No @agentname → agent_id mapping. crates/librefang-channels/src/telegram.rs:2940-2943, 2983-3014 only sets was_mentioned=true for the bot's own username. Mentioning a specific agent by name is invisible to the bridge.
  3. route_assistant_by_metadata is not consulted from the channel dispatch path. resolve_or_fallback (crates/librefang-channels/src/bridge.rs:2321-2398) walks bindings → direct → user default → channel default → "assistant" → first-available. Per-agent attention scoring exists in kernel but is never called from the channel inbound path. Layer (c) of bug(channels): inbound routing falls to non-deterministic "first available" agent when SidecarChannelConfig has no default + empty channels allowlist accepts all (related #4926) #5294 (non-deterministic "first available") is still alive.
  4. ThreadKey is too coarse for multi-agent + multi-tenant. Today (channel, thread). Without account_id, two Slack workspaces with same thread_ts collide. Without chat_id, two Telegram chats with the same Telegram-side topic id (rare but possible) collide. Without peer_id, two users talking to two different agents in the same topic share one claim. (fix(channels): multi-tenant ThreadKey + sweep_expired wiring (#3414 follow-up) #3419/fix(channels): account_id in ThreadKey + lazy sweep + push bypass note #3420 tried to fix the account_id slice and didn't land.)
  5. DMs and groups without forum topics bypass the registry entirely. crates/librefang-channels/src/thread_ownership.rs:18 comment + bridge.rs:3409-3431 gate. A multi-agent group without active topics has zero stickiness.

End-user impact: in a group with fandangorodelo (default) + ambrogio (joined), saying "oye fandango" routes to fandango (good — #2619). Saying "ambrogio, ayúdame" or @ambrogio … routes to fandango anyway (bad), and the follow-up has no concept of "keep talking to ambrogio" because no claim was ever made for him.

Proposal (high-level — implementation in follow-up PRs)

  1. Resurrect the account_id slice of fix(channels): multi-tenant ThreadKey + sweep_expired wiring (#3414 follow-up) #3419/fix(channels): account_id in ThreadKey + lazy sweep + push bypass note #3420 — extend ThreadKey to { channel, account_id: Option<String>, chat_id: Option<String>, thread: String, peer_id: Option<String> }. thread falls back to chat_id when no forum-topic exists; DMs continue to bypass unless an opt-in flag is set. Restores the multi-tenant safety that feat(channels): thread ownership prevents multi-agent duplicate replies #3414 review-comments asked for.
  2. Multi-agent alias wiring — replace the "only default agent" loop in channel_bridge.rs:1934-1969 with one over every agent whose manifest declares channels.contains(channel_id) (or no allowlist, depending on feat(types,kernel,api): per-agent channel allowlist #4961 outcome). Each agent's aliases land in group_trigger_patterns for that channel; the bridge already maps a triggered alias back to the correct agent via feat(channels): trigger agent on routing aliases in group messages (#2292) #2619 wiring once the lookup exists.
  3. Optional @handle → agent_id resolution — extend check_mention_entities in adapters (Telegram, Discord, Slack) to scan all mention/text_mention entities, lookup against a per-agent metadata.routing.handles: Vec<String> (new convention; ignore if not declared), and stamp metadata[\"addressed_agent_id\"]. was_mentioned semantics preserved for backward compat.
  4. Consult route_assistant_by_metadata from the channel dispatch path — add a KernelHandle method route_assistant_by_metadata_for_channel(channel, text) -> Option<AgentId> and call it from resolve_or_fallback after the existing chain and before the non-deterministic first-available fallback (closes layer (c) of bug(channels): inbound routing falls to non-deterministic "first available" agent when SidecarChannelConfig has no default + empty channels allowlist accepts all (related #4926) #5294).
  5. Configurable knobs[channels.X.overrides] conversation_ownership_ttl_seconds (default 600s), conversation_ownership_include_dms (default false).

All changes preserve existing TTL semantics (refresh-on-holder, re-claim on @-mention) from #3414; only ThreadKey shape changes (plus the new metadata field and the resolver consultation point).

Test plan (will live in follow-up PRs)

Patrón TestServer en crates/librefang-api/tests/ y cargo test -p librefang-channels:

Out of scope (separate issues)

Notes

Plan completo y citas file:line en .omc/plans/per-conversation-routing.md del fork. Disponible para PR atomic splitting según el feedback que reciba este issue.

cc related: #5294 (closed layer a), #4926 (N:M routing), #4961 (channels allowlist).

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/channelsMessaging channel adaptersarea/kernelCore kernel (scheduling, RBAC, workflows)awaiting-responseMaintainer replied — waiting for reporter/contributor feedbackenhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions