Skip to content

Twitch support#1715

Draft
jeff-hykin wants to merge 13 commits intodevfrom
jeff/feat/twitch1
Draft

Twitch support#1715
jeff-hykin wants to merge 13 commits intodevfrom
jeff/feat/twitch1

Conversation

@jeff-hykin
Copy link
Copy Markdown
Member

@jeff-hykin jeff-hykin commented Mar 30, 2026

Draft b/c: probably going to change some file names, might change filtering for the basic module. Need to check the voter de-dup logic (what's the refresh rate)

Problem

To transcend to a higher state of society, dangerous robots need to be controllable by Twitch chat -- the ultimate democratic source of group-intelligence.

Solution

Two new modules:

  1. TwitchChat Module

    • extremely basic: publishes TwitchMessage on raw_messages
    • optional filtered_messages stream, with module config for: filter_is_mod, filter_is_subscriber, filter_author, filter_content
    • has inject_message() for testing.
  2. TwitchVotes

    • extends TwitchChat
    • publishes decisions instead of raw messages (TwitchChoice objects)
    • 4 voting modes: plurality, majority, weighted_recent, runoff. Per-voter de-duplication

New Blueprint unitree-go2-twitch

  • wires votes to Go2 cmd_vel via an inline bridge module.
  • Demo (if you provide twitch auth keys) in examples/twitch_plays/run.py shows the full pipeline

Design decisions:

  • Utils provided for easy inline message filters

Breaking Changes

None

How to Test

Unit tests below, but a full/real test can be run by setting up keys/dimos and then doing python examples/twitch_plays/run.py (see examples/twitch_plays/readme.md for details)

# Unit tests (28 tests, no hardware/credentials needed)
uv run pytest dimos/stream/twitch/test_twitch.py -v --noconftest

# CI checks
bin/ci-check

Contributor License Agreement

  • I have read and approved the CLA.

jeff-hykin and others added 12 commits March 26, 2026 08:17
Viewers vote via chat keywords (!forward, !left, etc.). Winning command
each window is published as Twist on cmd_vel.

Four voting modes:
- plurality: most votes wins (classic Twitch Plays)
- majority: winner needs >50%
- weighted_recent: later votes count more
- runoff: instant runoff if no majority

- dimos/stream/twitch/module.py — TwitchChat module + vote tallying
- Blueprint: unitree-go2-twitch (Go2 + TwitchChat)
- examples/twitch_plays/ — README + standalone runner
- Add .*/native/build(/|$) to mypy exclude pattern to skip CMake
  _deps directories (dimos_lcm-src has invalid Python package name)
- Fix type: ignore comments to cover import-not-found alongside
  import-untyped for twitchio and onnxruntime
- Fix Counter[str] += float type error in weighted_recent tally
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ure, add tests

- Extract _build_twitch_message and _on_message_received hook in TwitchChat
  so TwitchVotes doesn't duplicate badge parsing
- Add threading.Event stop signal to _vote_loop for clean shutdown
- Widen message_to_choice return type to Any (supports lambda short-circuit)
- Add 27 unit tests for TwitchMessage, tally functions, and filter patterns

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- inject_message now calls _on_message_received hook (votes work in local mode)
- find_one uses word-boundary matching (prevents "backwards" matching "back")
- stop() wraps bot close in try/except so cleanup always runs
- Remove config mutation in TwitchVotes.start() (patterns don't affect voting)
- _tally_weighted_recent uses float weights instead of int truncation
- _vote_loop preserves votes arriving after window ends instead of clearing all
- Deduplicate votes per voter per window (prevents spam)
- Compile patterns before local-only early return

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jeff-hykin jeff-hykin marked this pull request as draft March 30, 2026 21:40
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 30, 2026

Greptile Summary

This PR introduces Twitch chat integration for DimOS, adding two new modules (TwitchChat and TwitchVotes), a unitree-go2-twitch blueprint, and a demo that lets Twitch viewers vote to control a Go2 robot. The architecture is clean — TwitchChat reads from Twitch IRC on a daemon thread, publishes TwitchMessage objects to RxPY streams, and TwitchVotes extends it with a windowed vote-tallying loop supporting four modes (plurality, majority, weighted_recent, runoff).

Key findings:

  • P1 — inject_message does not call _on_message_received (dimos/stream/twitch/module.py, line 332): The real-message handler (_handle_message) calls raw_messages.publish → _publish_if_matched → _on_message_received. inject_message omits the last step. Since TwitchVotes._on_message_received is where votes are recorded, using inject_message on a TwitchVotes instance silently records no votes, breaking the documented local-testing workflow.

  • P1 — Per-voter deduplication is only implemented for runoff mode (dimos/stream/twitch/votes.py, lines 666–692): The PR description advertises "Per-voter de-duplication" as a general feature, but _tally_plurality, _tally_majority, and _tally_weighted_recent count every entry in the votes list, allowing one user to spam and dominate results. Only _tally_runoff performs any deduplication (and only in its runoff phase).

  • P2 — is_mod detection relies solely on the moderator badge (module.py, line 298): The IRC mod tag ("1") is the authoritative signal for moderator status and should be checked alongside badges.

  • P2 — Blocking sleep loop in _on_choice without synchronization (unitree_go2_twitch.py, lines 79–97 and run.py, lines 950–968): If two vote-window results arrive close together, two concurrent _on_choice invocations interleave their Twist publishes. A mutex around the execution block would prevent this.

  • P2 — Auto-generated filter pattern does not gate vote counting (votes.py, lines 753–756): The auto-generated prefix pattern (e.g. ^!forward) only controls what reaches filtered_messages; _on_message_received is called for every message unconditionally, so votes are counted regardless of prefix.

Confidence Score: 4/5

  • Safe to merge after the inject_message hook fix and clarification on per-voter deduplication scope.
  • Two P1 findings: inject_message silently drops votes when used on TwitchVotes (breaks the primary documented test path), and per-voter deduplication is absent in three of four voting modes despite being advertised. These should be addressed before merge. All other issues are P2 style/hardening.
  • dimos/stream/twitch/module.py (inject_message bug) and dimos/stream/twitch/votes.py (deduplication gap)

Important Files Changed

Filename Overview
dimos/stream/twitch/module.py New TwitchChat module that reads Twitch IRC chat and publishes TwitchMessage objects; has a P1 bug where inject_message omits the _on_message_received hook call, breaking vote testing in subclasses.
dimos/stream/twitch/votes.py New TwitchVotes module with four tallying modes; per-voter deduplication is only effective in runoff mode — plurality/majority/weighted_recent allow a single user to cast multiple votes per window, contrary to the PR description.
dimos/robot/unitree/go2/blueprints/smart/unitree_go2_twitch.py New blueprint wiring TwitchVotes to Go2 cmd_vel; the _ChoiceToCmdVel bridge uses a blocking sleep loop without synchronization, risking interleaved commands if two choices arrive close together.
dimos/stream/twitch/test_twitch.py 28 unit tests covering tally functions and message filtering; tests are well-structured but don't cover the inject_message → vote recording path (which is where the P1 bug lives).
dimos/robot/all_blueprints.py Registers unitree-go2-twitch blueprint and twitch-chat/twitch-votes modules; straightforward additions.
examples/twitch_plays/run.py Demo script wiring TwitchVotes to Go2 MuJoCo sim; duplicates the _ChoiceToCmdVel class from the blueprint with the same unsynchronized sleep loop.
pyproject.toml Adds native/build directory exclusion to mypy config; unrelated to Twitch feature, minor housekeeping.

Sequence Diagram

sequenceDiagram
    participant TC as TwitchIRC
    participant Bot as _TwitchBot
    participant Chat as TwitchChat
    participant Votes as TwitchVotes
    participant VL as VoteLoop
    participant Bridge as ChoiceToCmdVel
    participant Robot as Go2 cmd_vel

    TC->>Bot: IRC message
    Bot->>Chat: on_message_cb(message)
    Chat->>Chat: _build_twitch_message()
    Chat->>Chat: raw_messages.publish(msg)
    Chat->>Chat: _publish_if_matched(msg)
    Chat->>Votes: _on_message_received(msg)
    Votes->>Votes: message_to_choice(msg, choices)
    Votes->>Votes: append to _votes deque

    Note over VL: every vote_window_seconds
    VL->>Votes: acquire _votes_lock
    VL->>VL: _tally(votes, mode)
    VL->>Bridge: chat_vote_choice.publish(TwitchChoice)
    Bridge->>Robot: cmd_vel.publish(Twist) x10
    Bridge->>Robot: cmd_vel.publish(Twist zero)
Loading

Comments Outside Diff (5)

  1. dimos/stream/twitch/votes.py, line 666-670 (link)

    P1 Per-voter deduplication not implemented in plurality/majority/weighted_recent modes

    The PR description advertises "Per-voter de-duplication" as a general feature, but only _tally_runoff performs any deduplication (and only in its runoff phase). _tally_plurality, _tally_majority, and _tally_weighted_recent all count every entry in the votes list, allowing a single Twitch user to spam votes and skew results.

    For example, in plurality mode a single user can send "forward" 50 times in the window and dominate the outcome.

    If per-voter deduplication is desired across all modes, the de-duplication should happen before calling the tally functions, e.g.:

    def _deduplicate(votes: list[tuple[str, float, str]]) -> list[tuple[str, float, str]]:
        """Keep only the last vote per voter."""
        seen: dict[str, tuple[str, float, str]] = {}
        for v in votes:
            seen[v[2]] = v  # overwrite with latest
        return list(seen.values())

    Then call votes = _deduplicate(votes) before tallying.

  2. dimos/stream/twitch/module.py, line 297-298 (link)

    P2 is_mod detection relies only on badges, missing the IRC mod tag

    The Twitch IRC protocol provides a dedicated mod tag (value "1") that is more authoritative for moderator status than the moderator badge entry. Badge display can be inconsistent (e.g., if the badges tag is absent for some reason). Twitchio also exposes message.author.is_mod directly.

  3. dimos/robot/unitree/go2/blueprints/smart/unitree_go2_twitch.py, line 79-97 (link)

    P2 Blocking sleep loop in _on_choice without synchronization

    _on_choice is called from a subscription callback and blocks the calling thread for command_duration seconds (default 1 second) using a while/sleep loop. If two TwitchChoice events arrive close together (e.g., if the vote loop fires slightly late), two concurrent invocations of _on_choice can execute simultaneously, resulting in interleaved Twist publishes from two goroutines — confusing robot behavior.

    Consider adding a lock, or cancelling the current command before starting a new one:

    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self._cmd_lock = threading.Lock()
    
    def _on_choice(self, choice: TwitchChoice) -> None:
        with self._cmd_lock:
            ...

    The same pattern exists in examples/twitch_plays/run.py at ChoiceToCmdVel._on_choice (lines 950–968).

  4. dimos/stream/twitch/votes.py, line 753-756 (link)

    P2 Auto-generated filter pattern doesn't gate vote counting

    The auto-generated pattern (requiring the bot prefix !) is applied to filtered_messages only. _on_message_received is called for every message via _handle_message, so a user typing bare "forward" (no prefix) will still have their vote counted even though the message is blocked from filtered_messages.

    This creates a discrepancy: messages silently get voted on without any feedback to downstream filtered_messages subscribers. Consider documenting this behaviour clearly, or routing _on_message_received to only run after the filter check if prefix-gating of votes is the intended design.

  5. dimos/stream/twitch/module.py, line 332-336 (link)

    P1 inject_message skips _on_message_received, breaking vote testing

    inject_message calls raw_messages.publish and _publish_if_matched but does not call self._on_message_received(msg). Compare with _handle_message (lines 302–306), which calls all three.

    TwitchVotes overrides _on_message_received to record votes. This means calling inject_message on a TwitchVotes instance will publish to streams but no vote is recorded — silently breaking the primary stated testing method described in the README. The fix is to add self._on_message_received(msg) as the last line of inject_message.

Reviews (1): Last reviewed commit: "cleanup" | Re-trigger Greptile

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.

1 participant