Skip to content

fix(runtime): channel_send without recipient replies to the group, not the speaker#6261

Merged
houko merged 1 commit into
librefang:mainfrom
neo-wanderer:fix/channel-send-reply-to-group-room
Jun 21, 2026
Merged

fix(runtime): channel_send without recipient replies to the group, not the speaker#6261
houko merged 1 commit into
librefang:mainfrom
neo-wanderer:fix/channel-send-reply-to-group-room

Conversation

@neo-wanderer

Copy link
Copy Markdown
Contributor

Problem

A channel_send call without an explicit recipient is meant to reply to the current conversation. In a group/room it instead targeted the speaker rather than the room.

tool_channel_send (crates/librefang-runtime/src/tool_runner/channel.rs) auto-filled the recipient from sender_id (the individual who spoke), while the #6117 cross-chat dispatch guard a few lines below computed its expected_chat from sender_chat_id first (sender_chat_id.filter(non-empty).or(sender_id)). The two fallbacks disagreed.

In a group the speaker's id is not a conversation, so the send routed to it as if it were one. Concretely on Matrix a no-recipient file send resolved to the speaker MXID @user:hs instead of the room !room:hs; the homeserver rejected the PUT m.room.message with:

403 M_FORBIDDEN: User @bot:hs not in room @user:hs

The sidecar Send command is fire-and-forget (no Response.error correlation — the known long-term gap noted in sdk/python/librefang/sidecar/runtime.py), so that 403 surfaced only on the sidecar's stderr. The tool returned File '…' sent to … via <channel> and the agent reported success while nothing was delivered.

The explicit-recipient path was unaffected — the guard correctly rejects a mismatched explicit recipient; only the implicit (auto-fill) path resolved a target the guard never validated.

Fix

Resolve the send target and the guard's expected_chat through one shared helper, resolve_send_target(explicit, sender_chat_id, sender_id):

  • explicit recipient wins;
  • else the conversation id (sender_chat_id — room / group / chat);
  • else sender_id (DM, where the chat id coincides with the sender or none is stamped).

Because the auto-fill now returns exactly what the guard treats as canonical, a no-recipient send can no longer resolve a chat the guard would reject. DM replies are unchanged (they fall through to sender_id). Out-of-band callers (cron / trigger, no identity stamped) still surface the existing "Missing 'recipient'" error.

This is purely the recipient-resolution defect. The separate fire-and-forget "phantom success" gap (a failed sidecar send still reports success) needs the Response.error protocol change already tracked in runtime.py and is not in scope here.

Changes

  • resolve_send_target helper unifies the send fallback with the guard's expected_chat.
  • Updated the stale comment that claimed the auto-filled recipient "targets the current chat by construction" (untrue for groups before this fix).

Verification

  • cargo check -p librefang-runtime --lib — clean.
  • cargo clippy -p librefang-runtime --lib — no warnings.
  • cargo fmt -p librefang-runtime -- --check — clean.
  • cargo test -p librefang-runtime --lib channel::tests — 5/5 pass:
    • auto_fill_replies_to_group_not_speaker (the regression: room over speaker),
    • auto_fill_falls_back_to_sender_for_dm,
    • explicit_recipient_wins,
    • auto_fill_matches_guard_expected_chat (send target ≡ guard expectation),
    • no_identity_resolves_none.

@github-actions github-actions Bot added size/M 50-249 lines changed area/docs Documentation and guides area/runtime Agent loop, LLM drivers, WASM sandbox labels Jun 21, 2026
@neo-wanderer
neo-wanderer force-pushed the fix/channel-send-reply-to-group-room branch from d0d64ea to d90c568 Compare June 21, 2026 03:17
@github-actions github-actions Bot added has-conflicts PR has merge conflicts that need resolution ready-for-review PR is ready for maintainer review and removed has-conflicts PR has merge conflicts that need resolution labels Jun 21, 2026
A no-recipient channel_send auto-filled the recipient from sender_id
(the individual who spoke) instead of sender_chat_id (the room / group).
In a group the send then targeted the speaker's user id as if it were a
conversation — e.g. a Matrix file send routed to @user:hs rather than the
room !room:hs, which the homeserver rejected with 403 not in room.
That failure was visible only on the sidecar's stderr, so the tool still
reported success and the agent believed the message was delivered.

The librefang#6117 cross-chat guard already computed the correct target
(sender_chat_id first, sender_id only as a DM fallback) but the send
fallback disagreed, using the raw sender_id. Unify both through a shared
resolve_send_target helper so the auto-filled target is exactly the
expected_chat the guard validates — a no-recipient send can no longer
resolve a chat the guard would reject.

Adds unit tests for the group, DM, explicit-recipient, guard-parity, and
no-identity cases.
@houko
houko force-pushed the fix/channel-send-reply-to-group-room branch from d90c568 to 7e85ffd Compare June 21, 2026 10:38
@houko

houko commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Rebased onto main to clear the .secrets.baseline conflict. #6262 replaced detect-secrets with gitleaks and deleted that baseline file, so the conflict resolves by dropping it — your code and CHANGELOG entry are otherwise untouched. gitleaks scan passes on the rebased tree. Please git fetch + reset your local branch since it was force-updated.

@houko houko left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM (rebased onto current main to clear the .secrets.baseline conflict; reviewed independently). Correct fix: a no-recipient channel_send in a group was auto-filling the recipient from sender_id (the speaker) instead of sender_chat_id (the room), so sends routed to the speaker's user id and silently failed (e.g. Matrix 403 not-in-room). The PR unifies the auto-fill and the #6117 guard's expected_chat through one resolve_send_target helper so the two fallbacks can't disagree, with 5 new unit tests. CI is fully green.

@houko
houko merged commit 91aa92c into librefang:main Jun 21, 2026
31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/docs Documentation and guides area/runtime Agent loop, LLM drivers, WASM sandbox ready-for-review PR is ready for maintainer review size/M 50-249 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants