Skip to content

fix: resolve listed messaging targets consistently#5149

Closed
damianpdr wants to merge 1 commit into
NousResearch:mainfrom
damianpdr:refactor/channel-directory-display-targets
Closed

fix: resolve listed messaging targets consistently#5149
damianpdr wants to merge 1 commit into
NousResearch:mainfrom
damianpdr:refactor/channel-directory-display-targets

Conversation

@damianpdr

Copy link
Copy Markdown
Contributor

Summary

  • extract a shared channel target-label helper so listing and resolution stay in sync
  • let resolve_channel_name() accept the exact labels shown by send_message(action="list")
  • remove the cron-specific suffix stripping workaround and preserve Telegram topic thread IDs after resolution

Why this fixes a real issue

send_message(action="list") shows non-Discord targets with type suffixes like Alice (dm) or Coaching Chat / topic 17585 (group), but resolve_channel_name() previously only matched the raw stored name. Copying a listed target into send_message or cron delivery could fail even though it came from Hermes itself.

Before this patch on main, a direct repro returned None for both:

  • resolve_channel_name("telegram", "Alice (dm)")
  • resolve_channel_name("telegram", "Coaching Chat / topic 17585 (group)")

After this patch, those resolve to the correct IDs, and cron topic deliveries keep the thread id instead of collapsing to a combined chat id string.

Test Plan

  • python -m pytest tests/gateway/test_channel_directory.py tests/tools/test_send_message_tool.py tests/cron/test_scheduler.py::TestResolveDeliveryTarget -q -n 0
  • python -m pytest tests/cron/test_scheduler.py -q -n 0 (still shows the same 5 pre-existing failures already present on origin/main, unrelated to this diff)

Review notes

  • I checked for extra complexity after the refactor and kept it minimal: one shared label helper plus one normalization helper.
  • I also reviewed the cron path for resolved Telegram topic labels and fixed the thread-id preservation gap so the refactor covers the real topic-delivery case too.

@trevorgordon981

Copy link
Copy Markdown

Good fix for a real issue — the list/resolve asymmetry where Hermes emits Alice (dm) but won't accept it as input was a paper cut for agentic flows that copy target labels from send_message(action="list") output.

The structural improvement is solid: _channel_target_name() as the single source of truth for the display label, used by BOTH the listing path AND the resolution path, so they stay in sync by construction. Eliminates the possibility of drift. The removal of the ad-hoc target.rsplit(" (", 1)[0].strip() workaround in cron is also nice cleanup.

One structural concern — reaching into send_message_tool's private API:

from tools.send_message_tool import _parse_target_ref

_parse_target_ref is a leading-underscore private function in send_message_tool.py. After this PR, cron/scheduler.py depends on its signature and return contract. If someone refactors send_message_tool.py (renames, inlines, or changes the return tuple), cron silently breaks — and nothing in the private function's module warns about cross-module dependencies.

Two cleanups:

Option A — promote to public API by dropping the underscore prefix. Signals "other callers depend on this":

# send_message_tool.py
def parse_target_ref(platform_name: str, target_ref: str) -> tuple[Optional[str], Optional[str], bool]:
    """Parse a tool target into chat_id/thread_id and whether it is explicit."""
    ...

Option B — extract to a shared utility (tools/target_refs.py or similar). This is cleaner since _parse_target_ref contains platform-specific regex knowledge that arguably belongs in a platform-refs helper, not the send_message tool module.

Either way, the import from cron stops reaching past the underscore.

Minor observation — the resolution flow re-parses after resolving:

parsed_chat_id, parsed_thread_id, is_explicit = _parse_target_ref(platform_key, rest)
# ... set chat_id, thread_id from parse ...
resolved = resolve_channel_name(platform_key, chat_id)
if resolved:
    parsed_chat_id, parsed_thread_id, resolved_is_explicit = _parse_target_ref(platform_key, resolved)
    if resolved_is_explicit:
        chat_id, thread_id = parsed_chat_id, parsed_thread_id
    else:
        chat_id = resolved

This is correct (the resolver can return -1001:17585 which needs parsing to preserve the thread_id), but the double-parse is a bit tangled. A helper like _coerce_to_chat_thread(platform_key, ref) that encapsulates the parse-or-fallback logic would make the cron site read better:

chat_id, thread_id = _coerce_to_chat_thread(platform_key, rest)
resolved = resolve_channel_name(platform_key, chat_id)
if resolved:
    chat_id, thread_id = _coerce_to_chat_thread(platform_key, resolved)

Ship it — the user-visible behavior is correct, tests exercise the important cases, and the refactor is a net improvement over what was there.

@teknium1

teknium1 commented Apr 5, 2026

Copy link
Copy Markdown
Contributor

Merged via PR #5288 (consolidated bugfix salvage). Your commit(s) were cherry-picked onto current main with your authorship preserved in git log. Thanks @damianpdr for the fix!

@teknium1 teknium1 closed this Apr 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants