Skip to content

feat(photon): render multiple-choice clarify as a native iMessage poll - #48194

Closed
vaibhavjnf wants to merge 1 commit into
NousResearch:mainfrom
vaibhavjnf:feat/photon-poll-clarify
Closed

feat(photon): render multiple-choice clarify as a native iMessage poll#48194
vaibhavjnf wants to merge 1 commit into
NousResearch:mainfrom
vaibhavjnf:feat/photon-poll-clarify

Conversation

@vaibhavjnf

Copy link
Copy Markdown
Contributor

Summary

On Photon/iMessage, the clarify tool's multiple-choice prompts flattened to a numbered text list — even though iMessage has a native poll bubble and spectrum-ts already exposes it via the poll() content builder. This wires clarify choices to a native iMessage poll: the user taps a choice and the vote resolves the clarify.

Root cause

Two gaps, both directions:

Direction Before
Outbound Sidecar had only /send (text). No way to send a poll → base adapter's numbered-text fallback was used.
Inbound normalizeContent() handled only text/attachment/voice. A poll vote arrived as poll_option and was dropped — surfaced as [Photon content type not handled: poll_option] — so the clarify never resolved.

Reproduced live: sending a poll() to a DM rendered the native bubble; tapping a choice produced exactly the poll_option "content type not handled" marker, and the pending clarify hung to timeout.

Fix (end to end, edge-only)

Sidecar (sidecar/index.mjs)

  • import poll from spectrum-ts
  • add POST /send-pollspace.send(poll(title, ...options))
  • serialize inbound poll_option (vote: chosen title + selected bool) and poll content in normalizeContent()

Adapter (adapter.py)

  • override send_clarify: for choices, send a native poll via _sidecar_send_poll and call mark_awaiting_text so the existing pending-clarify text-intercept resolves it; open-ended clarifies keep the plain-text path
  • inbound poll_option selections dispatch as a plain-text MessageEvent carrying the chosen option (deselections / empty votes dropped)
  • if the poll send fails (older sidecar without /send-poll, or a send error) → fall back to the numbered-text clarify, so nothing regresses on a half-upgraded restart

No new model tool, no new env var, no core change — capability lives at the platform edge, and the vote reuses the existing clarify text-intercept resolution path (no new gateway resolution mechanism).

Test plan

  • tests/plugins/platforms/photon/test_poll_clarify.py (6 tests): inbound vote → choice text; deselection dropped; empty-title vote dropped; send_clarify with choices sends a poll + enables text-capture; open-ended stays text; poll-failure falls back to the text list
  • Full photon suite green (pytest tests/plugins/platforms/photon/)
  • node --check sidecar, py_compile adapter — clean tree, no dependency on local mods
  • Live: native poll bubble renders on real iMessage; tap streams back the poll_option event the new code consumes

Compatibility

Additive (+379/-1). /send-poll is a new route; the adapter degrades to the text-list clarify if the sidecar predates it, and open-ended clarify is unchanged.

Contributed by Vaibhav Sharma (X @vabbyshabby).

The `clarify` tool's multiple-choice prompts flattened to a numbered text
list on Photon/iMessage, even though iMessage has a native poll bubble and
spectrum-ts already exposes it via the `poll()` content builder. Two gaps
caused the flattening:

  * Outbound: the sidecar only had `/send` (text); there was no way to send
    a poll, so the base adapter's numbered-text fallback was used.
  * Inbound: `normalizeContent()` handled only text/attachment/voice, so a
    poll vote (`poll_option`) was dropped on the floor ("[Photon content
    type not handled: poll_option]") and never resolved the clarify.

Fix, end to end:

  * Sidecar: import `poll` from spectrum-ts; add a `/send-poll` route
    (`space.send(poll(title, ...options))`); serialize inbound `poll_option`
    (the vote: chosen title + selected bool) and `poll` content in
    `normalizeContent()`.
  * Adapter: override `send_clarify` — for choices, send a native poll via
    `_sidecar_send_poll` and call `mark_awaiting_text` so the gateway's
    existing pending-clarify text-intercept resolves the answer; open-ended
    clarifies keep the plain-text path. Inbound `poll_option` selections are
    dispatched as a plain-text MessageEvent carrying the chosen option
    (deselections / empty votes are dropped). If the poll send fails (an
    older sidecar without `/send-poll`, or a send error) it falls back to the
    numbered-text clarify, so nothing regresses on a half-upgraded restart.

No new model tool, no new env var, no core change — the capability lives at
the platform edge. The poll vote reuses the existing clarify text-intercept
resolution path, so no new gateway resolution mechanism is introduced.

Tests: tests/plugins/platforms/photon/test_poll_clarify.py — inbound vote ->
choice text, deselection/empty-vote dropped, send_clarify sends a poll +
enables text-capture, open-ended stays text, and poll-failure falls back to
the text list. Full photon suite green.

Contributed by Vaibhav Sharma (X: @vabbyshabby).
@alt-glitch alt-glitch added type/feature New feature or request comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have labels Jun 18, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Related: #43665 (feat(photon): add native poll sending). That PR adds the generic outbound poll primitive (/send-poll sidecar route + send_poll() adapter method) via spectrum-ts poll(). This PR adds the same outbound route plus the layer #43665 lacks: it wires multiple-choice clarify prompts to a native poll AND consumes the inbound poll_option vote to resolve the pending clarify (with a text-list fallback). Complementary rather than duplicate — reviewers may want to reconcile the two /send-poll implementations.

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review Summary

Verdict: Approved

Renders multiple-choice clarify questions as native iMessage polls via spectrum-ts. Selection streams back as a poll_option event resolved as plain text. Graceful fallback to numbered text if native poll fails.

Looks Good

  • Clean feature implementation with proper error handling
  • Well-documented code with clear architectural comments
  • Fallback path is well-thought-out
  • No debug or diagnostic artifacts
  • Appropriate logging for production

Reviewed by Hermes Agent

@vaibhavjnf

Copy link
Copy Markdown
Contributor Author

Cant wait to become a hermes contributor 😭

@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the focused edge-platform implementation. The current main premise is valid: Photon still inherits the numbered clarify fallback in gateway/platforms/base.py:3149-3166, and its inbound dispatcher has no poll_option handling (plugins/platforms/photon/adapter.py:700-789).

Problems

  • tests/plugins/platforms/photon/test_poll_clarify.py:25-33 replaces handle_message, so the tests do not cover the actual pending-clarify interception path in gateway/run.py:9076-9107. This feature changes that message-resolution chain; add an integration-style test that registers a pending clarify and verifies a poll vote resolves it through the normal gateway path.
  • The branch predates current Photon sidecar changes. Main now pins spectrum-ts 8.0.0 at plugins/platforms/photon/sidecar/package.json:13 (commit 4345b3e76), so validate the poll export and poll_option payload against that SDK during salvage.
  • The new sidecar poll normalization still falls into the adapter's unsupported-content fallback at plugins/platforms/photon/adapter.py:787-789; either define its adapter behavior or omit that serialization.

Suggested changes

Automated hermes-sweeper review.

@teknium1 teknium1 added sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 14, 2026
teknium1 added a commit that referenced this pull request Jul 28, 2026
… clarify

Follow-up to the #48194 pick: it was written before #43665 landed and
re-added its own /send-poll sidecar route and poll import. Collapse the
duplicates:

- keep #43665's /send-poll route (>=2 trimmed string options) as the
  single sidecar implementation; drop #48194's variant
- drop the duplicated poll import in the sidecar destructure
- make adapter.send_poll() a thin wrapper over _sidecar_send_poll(), the
  one /send-poll client (shared with the poll-backed clarify path), and
  align its validation to the sidecar's >=2-options contract
teknium1 added a commit that referenced this pull request Jul 29, 2026
… clarify

Follow-up to the #48194 pick: it was written before #43665 landed and
re-added its own /send-poll sidecar route and poll import. Collapse the
duplicates:

- keep #43665's /send-poll route (>=2 trimmed string options) as the
  single sidecar implementation; drop #48194's variant
- drop the duplicated poll import in the sidecar destructure
- make adapter.send_poll() a thin wrapper over _sidecar_send_poll(), the
  one /send-poll client (shared with the poll-backed clarify path), and
  align its validation to the sidecar's >=2-options contract
teknium1 added a commit that referenced this pull request Jul 29, 2026
… clarify

Follow-up to the #48194 pick: it was written before #43665 landed and
re-added its own /send-poll sidecar route and poll import. Collapse the
duplicates:

- keep #43665's /send-poll route (>=2 trimmed string options) as the
  single sidecar implementation; drop #48194's variant
- drop the duplicated poll import in the sidecar destructure
- make adapter.send_poll() a thin wrapper over _sidecar_send_poll(), the
  one /send-poll client (shared with the poll-backed clarify path), and
  align its validation to the sidecar's >=2-options contract
teknium1 added a commit that referenced this pull request Jul 29, 2026
… clarify

Follow-up to the #48194 pick: it was written before #43665 landed and
re-added its own /send-poll sidecar route and poll import. Collapse the
duplicates:

- keep #43665's /send-poll route (>=2 trimmed string options) as the
  single sidecar implementation; drop #48194's variant
- drop the duplicated poll import in the sidecar destructure
- make adapter.send_poll() a thin wrapper over _sidecar_send_poll(), the
  one /send-poll client (shared with the poll-backed clarify path), and
  align its validation to the sidecar's >=2-options contract
@teknium1

Copy link
Copy Markdown
Contributor

Merged via #73614 — cherry-picked with authorship preserved; a maintainer follow-up deduplicated the /send-poll route in favor of #43665's primitive (earliest submission) and tightened option validation to the sidecar contract. The clarify→poll→vote-resolution loop is a genuinely great UX idea.

@teknium1 teknium1 closed this Jul 29, 2026
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
 for poll clarify

Follow-up to the NousResearch#48194 pick: it was written before NousResearch#43665 landed and
re-added its own /send-poll sidecar route and poll import. Collapse the
duplicates:

- keep NousResearch#43665's /send-poll route (>=2 trimmed string options) as the
  single sidecar implementation; drop NousResearch#48194's variant
- drop the duplicated poll import in the sidecar destructure
- make adapter.send_poll() a thin wrapper over _sidecar_send_poll(), the
  one /send-poll client (shared with the poll-backed clarify path), and
  align its validation to the sidecar's >=2-options contract
33hodl pushed a commit to 33hodl/hermes-agent that referenced this pull request Aug 12, 2026
 for poll clarify

Follow-up to the NousResearch#48194 pick: it was written before NousResearch#43665 landed and
re-added its own /send-poll sidecar route and poll import. Collapse the
duplicates:

- keep NousResearch#43665's /send-poll route (>=2 trimmed string options) as the
  single sidecar implementation; drop NousResearch#48194's variant
- drop the duplicated poll import in the sidecar destructure
- make adapter.send_poll() a thin wrapper over _sidecar_send_poll(), the
  one /send-poll client (shared with the poll-backed clarify path), and
  align its validation to the sidecar's >=2-options contract
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-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades 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.

4 participants