Skip to content

feat(photon): markdown rendering, emoji reactions, sidecar lifecycle fixes, telemetry toggle (salvage #44486)#44713

Merged
teknium1 merged 9 commits into
mainfrom
hermes/hermes-1071f263
Jun 12, 2026
Merged

feat(photon): markdown rendering, emoji reactions, sidecar lifecycle fixes, telemetry toggle (salvage #44486)#44713
teknium1 merged 9 commits into
mainfrom
hermes/hermes-1071f263

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

Summary

Salvage of #44486 by @underthestars-zhy onto current main: Photon (iMessage via Spectrum) markdown rendering, emoji tapback reactions, sidecar orphan reaping + stdin-EOF lifecycle, spectrum-ts 3.1.0 pin, and a telemetry opt-in toggle — plus a follow-up commit adding the unreact action the adapter shipped but the tool didn't expose.

Changes

  • plugins/platforms/photon/: adapter (+399), sidecar (+214), CLI telemetry toggle, plugin.yaml env vars, README
  • tools/send_message_tool.py: action=react/unreact with emoji/message_id params (extends the existing gated tool, no new tool)
  • tests: 89 photon tests (markdown, reactions, sidecar lifecycle) + 6 new tool-level react/unreact dispatch tests

Validation

Result
tests/plugins/platforms/photon/ 89 passed
tests/tools/test_send_message_react.py (new) 6 passed
tests/tools/test_send_message_target_parse.py passed

Contributor commits cherry-picked with authorship preserved — rebase-merge this PR. Closes #44486.

Infographic

photon-imessage-upgrade

underthestars-zhy and others added 9 commits June 11, 2026 23:34
…eactions

Pin spectrum-ts to exactly 3.0.0 (was ^1.18.0 plus an `npm install
spectrum-ts@latest` on every setup) so breaking SDK majors can't take
down fresh installs silently; `hermes photon setup` now runs `npm ci`.
Upgrade procedure documented in the README.

Migrate resolveSpace to the v3 namespace API: `im.space.create(phone)`
for DMs and `im.space.get(id)` for everything else — group spaces are
now rehydratable from their persisted id after a sidecar restart, which
v1 could not do.

Markdown: replies go out via the v3 `markdown()` builder (iMessage
renders natively; other Spectrum platforms degrade to plain text).
`PHOTON_MARKDOWN=false` reverts to the stripped plain-text path.

Reactions, behind PHOTON_REACTIONS (default off): lifecycle tapbacks
(👀 while processing, 👍/👎 on completion) via new sidecar /react and
/unreact endpoints with per-target reaction-handle tracking, and user
tapbacks on bot-sent messages routed to the agent as synthetic
`reaction:added:<emoji>` events.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…port

A hard gateway exit (crash, SIGKILL, supervisor restart) left the
detached Node sidecar running with a token the next gateway run doesn't
know, so it could never be told to /shutdown. Every replacement spawn
then died on EADDRINUSE, failing each 30→300s reconnect attempt while
the orphan kept consuming the inbound gRPC stream.

Two layers:
- Lifetime binding: the adapter now holds the sidecar's stdin as a
  pipe, and the sidecar (PHOTON_SIDECAR_WATCH_STDIN=1) shuts down on
  stdin EOF — fired by the OS on any parent death, including SIGKILL.
- Startup reaping: before spawning, the adapter probes the port and
  terminates a stale listener, but only after verifying its command
  line is a Photon sidecar; a foreign listener raises a clear error
  instead of being signalled.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Prevents "Future attached to a different loop" errors when
_sidecar_call is invoked from a worker thread via _run_async in
send_message_tool. The persistent _http_client remains in use for
the inbound streaming loop, which always runs on the gateway's loop.
Add `action='react'` to `send_message` tool and expose `add_reaction`/
`remove_reaction` on the Photon adapter.

- Track latest inbound message id per chat (`_last_inbound_by_chat`,
  bounded to 200 entries) so the agent can react without threading
  message ids through tool calls
- New `add_reaction`/`remove_reaction` public methods on PhotonAdapter;
  unlike the lifecycle tapbacks, these are not gated by PHOTON_REACTIONS
- `send_message` gains `action='react'` with `emoji` and optional
  `message_id` params; resolves target via existing channel-directory
  and home-channel logic; requires a live gateway adapter
Inbound events key the tracker by the DM chat GUID (any;-;+1555...),
but home-channel react calls address the same space by bare E.164 —
normalize both to the phone so add_reaction's last-inbound default
resolves regardless of which form the caller uses (mirrors the
sidecar's phoneTargetFromSpaceId).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…atch tests

Follow-up for salvaged PR #44486: the adapter shipped remove_reaction but
the tool only exposed 'react'. Generalize _handle_react(remove=) and add
tool-level dispatch tests for react/unreact (missing from the original PR).
@teknium1 teknium1 requested a review from a team June 12, 2026 06:39
@github-actions

Copy link
Copy Markdown
Contributor

🔎 Lint report: hermes/hermes-1071f263 vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 10852 on HEAD, 10847 on base (🆕 +5)

🆕 New issues (6):

Rule Count
invalid-assignment 3
unresolved-import 3
First entries
tests/plugins/platforms/photon/test_markdown.py:34: [invalid-assignment] invalid-assignment: Object of type `def _fake_call(path: str, body: dict[str, Any]) -> CoroutineType[Any, Any, dict[str, Any]]` is not assignable to attribute `_sidecar_call` of type `def _sidecar_call(self, path: str, body: dict[str, Any]) -> CoroutineType[Any, Any, dict[str, Any]]`
tests/plugins/platforms/photon/test_sidecar_lifecycle.py:14: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/plugins/platforms/photon/test_reactions.py:38: [invalid-assignment] invalid-assignment: Object of type `def _fake_call(path: str, body: dict[str, Any]) -> CoroutineType[Any, Any, dict[str, Any]]` is not assignable to attribute `_sidecar_call` of type `def _sidecar_call(self, path: str, body: dict[str, Any]) -> CoroutineType[Any, Any, dict[str, Any]]`
tests/plugins/platforms/photon/test_reactions.py:13: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`
tests/plugins/platforms/photon/test_reactions.py:133: [invalid-assignment] invalid-assignment: Object of type `def _boom(path: str, body: dict[str, Any]) -> CoroutineType[Any, Any, dict[str, Any]]` is not assignable to attribute `_sidecar_call` of type `def _sidecar_call(self, path: str, body: dict[str, Any]) -> CoroutineType[Any, Any, dict[str, Any]]`
tests/plugins/platforms/photon/test_markdown.py:11: [unresolved-import] unresolved-import: Cannot resolve imported module `pytest`

✅ Fixed issues (1):

Rule Count
invalid-assignment 1
First entries
plugins/platforms/photon/adapter.py:148: [invalid-assignment] invalid-assignment: Invalid subscript assignment with key of type `Literal["home_channel"]` and value of type `dict[str, str]` on object of type `dict[str, str]`

Unchanged: 5686 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/plugins Plugin system and bundled plugins platform/sms SMS (Twilio) adapter tool/web Web search and extraction labels Jun 12, 2026
@teknium1 teknium1 merged commit 05470aa into main Jun 12, 2026
35 of 36 checks passed
@teknium1 teknium1 deleted the hermes/hermes-1071f263 branch June 12, 2026 08:07
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 platform/sms SMS (Twilio) adapter tool/web Web search and extraction type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants