Summary
When an agent calls the native channel_send tool, the outbound message currently has no path back into the session that will receive the user's reply. If Agent A sends a message into a channel and the user replies, the reply lands in the session of whichever agent owns the inbound channel binding (Agent B), not in Agent A's session. Agent B has no record of what Agent A pushed, so it cannot respond with context.
Current behavior
tool_channel_send (crates/librefang-runtime/src/tool_runner.rs:4745) executes the send and returns a ToolResult containing only an acknowledgement string (e.g. "Message sent to {recipient} via {channel}"). The message body itself is not persisted anywhere in any agent's session history.
- Inbound channel messages are routed by
SessionId::for_channel(agent_id, "channel:chat_id") (crates/librefang-kernel/src/kernel/messaging.rs:1764), where agent_id is the agent bound to the channel — not the agent that called channel_send.
- Per-adapter self-echo filters (Messenger, Feishu, etc.) drop reflections of the bot's own send by matching the bot's platform user ID, so the outbound never re-enters as an inbound message.
Net effect: in any deployment with more than one agent (e.g. one cron/automation agent plus one chat-handling agent on the same channel), outbound sends from non-channel-owner agents become invisible to the agent that will eventually process the user's reply.
Proposed change
When channel_send is invoked, in addition to recording the tool result on the calling agent, write a synthetic turn into the inbound-routing session — i.e. SessionId::for_channel(channel_owner_agent_id, "channel:chat_id") — containing the actual outbound message body and an attribution to the originating agent.
Sketch:
- In
tool_channel_send, after a successful send, resolve the channel-owning agent for (channel, chat_id) using the existing inbound routing logic.
- If that agent differs from the caller (or always, for consistency), append a message to the channel-owning agent's session for that chat. A
system role with structured content seems safest — e.g. { from_agent_id, channel, chat_id, body, sent_at } — to avoid colliding with strict alternating-role drivers.
- No new response is triggered on the receiving session; the mirrored turn is context only. The channel-owner agent reacts on the next genuine inbound user message, which now has full context.
Why this approach
- Preserves the current "one session per (channel-owner, channel, chat_id)" invariant — no addressing scheme changes for callers.
- Surgical: localized to
tool_channel_send and a small kernel helper to resolve the channel-binding owner.
- Backwards-compatible: callers that don't care still get the existing tool result on their own session; channel-owner agents simply gain context they didn't have before.
- Composable with future enhancements (e.g. an optional
on_behalf_of_session parameter for explicit routing, or correlation tokens for platform-native reply threading) without conflicting with them.
Alternatives considered
- Caller specifies target session (
on_behalf_of_session: Uuid): more explicit but requires every caller — including cron-scheduled agents with no session reference — to know the routing key.
- Per-(channel, chat_id) shared transcript: cleanest model conceptually (one timeline, multiple agent speakers), but a much larger refactor of session loading and history trimming.
- Correlation tokens + platform reply primitives: useful when a chat hosts multiple parallel threads, but only meaningful on platforms with reply-to support (Telegram, Slack threads, Discord); does not help on flat channels.
The proposal above is intended as the smallest change that closes the gap; the alternatives can be layered on later if needed.
Open questions
- Should the mirrored turn be written unconditionally, or only when
caller_agent_id != channel_owner_agent_id? Unconditional is simpler and lets the calling agent see its own outbound as context too if it ever processes inbound on the same channel.
- Role choice:
system, assistant, or a new content-block kind? system avoids alternating-role driver issues but may be surfaced oddly in dashboards.
- Should the mirror also fire when
channel_send fails partway (delivered to platform but ack lost)? Probably yes — the user may still see and reply to the message.
- Does the existing per-adapter self-echo filter need to be aware of mirrored turns to avoid double-counting if a platform later does reflect the outbound back as inbound?
Summary
When an agent calls the native
channel_sendtool, the outbound message currently has no path back into the session that will receive the user's reply. If Agent A sends a message into a channel and the user replies, the reply lands in the session of whichever agent owns the inbound channel binding (Agent B), not in Agent A's session. Agent B has no record of what Agent A pushed, so it cannot respond with context.Current behavior
tool_channel_send(crates/librefang-runtime/src/tool_runner.rs:4745) executes the send and returns aToolResultcontaining only an acknowledgement string (e.g."Message sent to {recipient} via {channel}"). The message body itself is not persisted anywhere in any agent's session history.SessionId::for_channel(agent_id, "channel:chat_id")(crates/librefang-kernel/src/kernel/messaging.rs:1764), whereagent_idis the agent bound to the channel — not the agent that calledchannel_send.Net effect: in any deployment with more than one agent (e.g. one cron/automation agent plus one chat-handling agent on the same channel), outbound sends from non-channel-owner agents become invisible to the agent that will eventually process the user's reply.
Proposed change
When
channel_sendis invoked, in addition to recording the tool result on the calling agent, write a synthetic turn into the inbound-routing session — i.e.SessionId::for_channel(channel_owner_agent_id, "channel:chat_id")— containing the actual outbound message body and an attribution to the originating agent.Sketch:
tool_channel_send, after a successful send, resolve the channel-owning agent for(channel, chat_id)using the existing inbound routing logic.systemrole with structured content seems safest — e.g.{ from_agent_id, channel, chat_id, body, sent_at }— to avoid colliding with strict alternating-role drivers.Why this approach
tool_channel_sendand a small kernel helper to resolve the channel-binding owner.on_behalf_of_sessionparameter for explicit routing, or correlation tokens for platform-native reply threading) without conflicting with them.Alternatives considered
on_behalf_of_session: Uuid): more explicit but requires every caller — including cron-scheduled agents with no session reference — to know the routing key.The proposal above is intended as the smallest change that closes the gap; the alternatives can be layered on later if needed.
Open questions
caller_agent_id != channel_owner_agent_id? Unconditional is simpler and lets the calling agent see its own outbound as context too if it ever processes inbound on the same channel.system,assistant, or a new content-block kind?systemavoids alternating-role driver issues but may be surfaced oddly in dashboards.channel_sendfails partway (delivered to platform but ack lost)? Probably yes — the user may still see and reply to the message.