Skip to content

feat(photon): support rich link previews - #54646

Closed
huntsyea wants to merge 1 commit into
NousResearch:mainfrom
huntsyea:feat/photon-richlink-previews
Closed

feat(photon): support rich link previews#54646
huntsyea wants to merge 1 commit into
NousResearch:mainfrom
huntsyea:feat/photon-richlink-previews

Conversation

@huntsyea

@huntsyea huntsyea commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

What changed

Adds full Photon rich-link support for Hermes:

  • routes URL-only outbound Photon replies through the sidecar's /send-richlink endpoint, which wraps Spectrum richlink(url) so iMessage can render native preview cards
  • falls back to plain /send text if richlink sending is unsupported or fails
  • preserves inbound richlink URL text, and preserves title / summary metadata when Spectrum has already exposed those as plain fields
  • coalesces iMessage .pluginPayloadAttachment preview artwork that arrives immediately after a URL/richlink so the agent receives one link message instead of a follow-up attachment prompt
  • updates Photon docs and focused regression coverage

Why

Fixes #50336.

This also supersedes/extends #50337. That PR handles the inbound richlink placeholder bug; this branch includes compatible inbound handling and adds outbound richlink sending plus preview-art coalescing. Unlike #50337's original sidecar approach, this branch does not call lazy title() / summary() accessors in the inbound stream. Photon docs describe those as lazy metadata accessors, and webhook docs intentionally keep richlink deliveries URL-only to avoid adding URL fetch latency or attack surface to delivery.

Validation against Photon docs

Checked current Photon docs:

  • https://photon.codes/docs/llms.txt lists Spectrum rich links and webhooks docs.
  • https://photon.codes/docs/spectrum-ts/content/rich-links.md documents richlink(url) for outbound URL preview cards and lazy title(), summary(), cover() metadata accessors.
  • https://photon.codes/docs/webhooks/overview.md says webhook richlink deliveries ship the url only and omit resolved title/summary/cover metadata.

Implementation follows that split: outbound uses richlink(url); inbound preserves URL and any already-materialized title/summary fields without resolving lazy accessors itself.

Tests

  • uv run --with pytest==9.0.2 --with pytest-asyncio==1.3.0 python -m pytest tests/plugins/platforms/photon -q — 133 passed
  • node --check plugins/platforms/photon/sidecar/index.mjs
  • git diff --check

I also ran two reviews. One implementation review found no blocking issues.

One docs/API review flagged lazy inbound metadata resolution as risky; this was updated to avoid resolving title() / summary() in the sidecar and only preserve plain fields when present.

@alt-glitch alt-glitch added type/feature New feature or request comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages labels Jun 29, 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 extending the Photon sidecar with an outbound rich-link path and preserving the v8-compatible inbound behavior.

Problems

  • plugins/platforms/photon/adapter.py:1529 removes current main's return result after exhausted retryable attempts (plugins/platforms/photon/adapter.py:1450-1455). The new fallthrough performs another plain-text _sidecar_send() for every exhausted retryable send, including ordinary text. A timeout can occur after space.send() accepted the earlier message, so that extra POST can duplicate a delivered response. The richlink-specific fallback is already handled by _sidecar_send() at PR lines 1565-1574.

Suggested changes

  • Restore the exhausted-retry return for ordinary sends and add a regression test asserting that an exhausted retryable non-URL response makes no extra send.

Automated hermes-sweeper review.

max_retries, error_str,
)
return result
# Fall through to the plain-text fallback below. For URL-only

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.

Please retain the existing return result for exhausted retryable failures. This fallthrough runs for ordinary text as well as URLs and makes another /send after the retry budget; an HTTP timeout can occur after space.send() accepted a prior attempt, so that extra POST can duplicate a delivered reply. Richlink-to-plain fallback is already handled inside _sidecar_send().

@teknium1 teknium1 added the sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform label Jul 15, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Merged via #73614 — cherry-picked with authorship preserved. Rich links + preview-artwork coalescing shipped; the preview-suppression logic composed cleanly with the FFFC wait machinery on main.

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

Labels

comp/plugins Plugin system and bundled plugins 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/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Photon adapter does not handle iMessage rich links — shows '[Photon content type not handled: richlink]'

3 participants