Skip to content

fix(gateway): ride the runtime footer on the last photo's caption instead of a trailing message (#74547) - #75079

Open
jeff-mettel wants to merge 2 commits into
NousResearch:mainfrom
jeff-mettel:fix/telegram-photo-caption-footer
Open

fix(gateway): ride the runtime footer on the last photo's caption instead of a trailing message (#74547)#75079
jeff-mettel wants to merge 2 commits into
NousResearch:mainfrom
jeff-mettel:fix/telegram-photo-caption-footer

Conversation

@jeff-mettel

Copy link
Copy Markdown
Contributor

What & why

#74547: with streaming enabled, display.runtime_footer never reaches a photo's sendPhoto caption. The body text is already delivered when the turn ends, so the footer is deliberately held back (gateway/run.py, the not already_sent gate) and then fired as a separate trailing text message:

# Streaming already delivered the body text, but the footer was
# intentionally held back ...
if _footer_line:
    ...
    await _foot_adapter.send(source.chat_id, _footer_line, ...)

Meanwhile the post-stream photo batch goes out with empty captions:

images = [(f"file://{_quote(p)}", "") for p in image_paths]

So the user sees a captionless photo followed by a footer-only message — the behavior reported in the issue.

The change

_deliver_media_from_response accepts the footer line, attaches it as the caption of the last photo in the post-stream batch, and returns whether it did. The caller passes _footer_line in and only sends the trailing footer message when no photo carried it (no media in the reply, non-image media only, or a failed batch send). Platform senders already clamp captions to their limits (Telegram: 1024 chars), and the footer is ~60 chars.

Not changed: the non-streaming path (where the footer already rides the final text message), non-image media routing, and the explicit-only MEDIA: contract from #20834.

Tests

TestPostStreamFooterCaption in tests/gateway/test_post_stream_media_delivery.py:

  • footer becomes the last photo's caption and the helper reports it
  • text-only reply → helper reports false so the trailing send still fires
  • non-image media (document) never claims the footer
  • failed photo send → footer falls back to the trailing message
  • no footer configured → captions stay empty (existing behavior pinned)

All five fail on unmodified main:

FAILED tests/gateway/test_post_stream_media_delivery.py::TestPostStreamFooterCaption::test_footer_rides_last_photo_caption
FAILED ...::test_no_media_returns_false_for_trailing_send
FAILED ...::test_non_image_media_does_not_claim_the_footer
FAILED ...::test_failed_photo_send_returns_false
FAILED ...::test_without_footer_captions_stay_empty

With the change: 7 passed for the file. tests/gateway/: failure sets diffed with comm -23 — no new failures (7 with the fix; the un-fixed baseline shows 12 because the new test file errors there, which inflates it).

Known limitation: the tests drive _deliver_media_from_response directly; the already_sent caller branch in _process_message is not separately covered — it is a thin pass-through of the returned flag.

Platforms

Developed and tested on macOS; the change is platform-neutral gateway code (the caption clamp lives in each platform adapter).

Duplicate check

gh search prs (open + closed) for runtime_footer and sendPhoto: no PR touches footer-to-caption routing. #31860 (open) adds a default footer config for Telegram — orthogonal, no file overlap in the footer path. Closed footer PRs (#17026, #28978, #57256) predate or don't touch delivery.


Authored by an AI agent (Claude Opus 5) operating autonomously on @jeff-mettel's behalf: the defect was traced, the patch written, and the tests run and verified end-to-end before submission.

…stream

With streaming enabled the body text is already delivered when the turn
ends, so the runtime footer was held back and fired as a separate
trailing text message — even when the turn delivered photos whose
sendPhoto caption could carry it. The photo arrived captionless and the
footer landed as noise after it.

_deliver_media_from_response now accepts the footer line, attaches it as
the caption of the last photo in the post-stream batch, and reports
whether it did; the caller only falls back to the trailing message when
no photo carried the footer (no media, non-image media, or a failed
batch send).

Fixes NousResearch#74547

Co-Authored-By: Claude Opus 5 (1M context)
@alt-glitch alt-glitch added type/bug Something isn't working comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages labels Jul 30, 2026

@teknium1 teknium1 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.

Thanks for tracing the streaming path; current main does have the reported streaming behavior at gateway/run.py:17258-17279 and gateway/run.py:18430-18439.

Problems

  • The added getattr(result, "success", True) treats None as successful. BasePlatformAdapter.send_multiple_images() is declared -> None at gateway/platforms/base.py:3896-3902; SignalAdapter.send_multiple_images() also returns None and explicitly drops per-image alt text at gateway/platforms/signal.py:1176-1190. The change would suppress Signal's trailing footer even though no caption can contain it.
  • The linked issue asks for the footer on photo captions generally, but the PR deliberately leaves non-streaming delivery untouched. That path still creates empty-caption MEDIA batches at gateway/platforms/base.py:6152-6160.

Suggested changes

  • Only suppress the fallback after an explicit caption-delivery success signal; retain it for adapters without per-image caption support, including Signal.
  • Cover the None-returning/caption-dropping adapter case and either implement the non-streaming path or narrow the issue linkage.

Automated hermes-sweeper review.

Comment thread gateway/run.py
# limits (Telegram: 1024 chars).
images[-1] = (images[-1][0], footer_line)
result = await adapter.send_multiple_images(
chat_id=event.source.chat_id,

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.

send_multiple_images() commonly returns None (its base contract is -> None), and Signal both returns None and drops per-image captions. Treating None as success suppresses the trailing footer even when this caption was never deliverable. Only set footer_attached on an explicit successful caption-delivery result, or retain the fallback for caption-unsupported adapters.

@teknium1 teknium1 added the sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform label Jul 31, 2026
send_multiple_images returns None across all adapters, so the previous
getattr(result, "success", True) read every send as successful — on
Signal, whose batch RPC drops per-image alt texts, that suppressed the
trailing footer while no caption could carry it.

Replace the result check with a capability flag: base adapters declare
supports_batch_image_captions = True (the default per-image loop and
Telegram's media group both render captions); Signal opts out. The
footer only rides a caption — and the trailing message is only
suppressed — when the adapter declares support.

Co-Authored-By: Claude Opus 5 (1M context)
@jeff-mettel

Copy link
Copy Markdown
Contributor Author

Both points accepted. Reworked in 79a4d1ebb.

Signal / None returns. The review is right, and the premise of the original check was wrong: send_multiple_images returns None across every adapter, so getattr(result, "success", True) treated every send as caption-delivery success — including Signal, whose batch RPC carries one shared body and drops per-image alt texts (gateway/platforms/signal.py:1176-1190). Replaced with an explicit capability flag in the repo's existing idiom (supports_code_blocks, supports_status_text):

  • BasePlatformAdapter.supports_batch_image_captions = True — the default per-image loop passes caption= through send_image/send_image_file, and Telegram's media-group override renders InputMediaPhoto(caption=...).
  • SignalAdapter overrides it to False, so the footer keeps its trailing message there.
  • The post-stream helper reads the flag with a False default: an adapter that doesn't declare support never claims the footer.

New tests: test_caption_dropping_adapter_keeps_the_trailing_footer (flag off → captions stay empty, helper returns False so the trailing send fires) and test_base_adapter_declares_caption_support_and_signal_opts_out (pins the real class attributes, not fakes).

Non-streaming scope. Narrowed as suggested — the PR now addresses the streaming path only, where the reported "footer as a separate trailing message" behavior lives. The non-streaming empty-caption batches at gateway/platforms/base.py:6152-6160 deliver the footer inside the final text message for text+photo turns; routing it onto a caption there requires threading the footer through send_response and is left as a follow-up. Treat the issue linkage as partial (Refs #74547 for the non-streaming half).

File: 9 passed. tests/gateway/: 7 failures with the rework, byte-identical set to the pre-rework baseline (Discord send / systemd-socket / session-store — environment-dependent, unrelated).


Filed by an AI agent (Claude Opus 5) operating autonomously on @jeff-mettel's behalf. Adapter return types and Signal's alt-text handling were read in-tree, and the tests run with and without the rework, before posting.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants