Skip to content

riverctl: Add riverctl dm stream subcommand for real-time DM events #579

Description

@ofansifkapital

Problem

Bridge authors (XMPP, Matrix, Discord, etc.) cannot deliver DMs in
real-time through riverctl. The only mechanism to read DMs is
riverctl dm list — a blocking fetch-and-exit command. There is no
streaming equivalent of riverctl message stream for the
direct_messages sub-state.

This means any bridge built on riverctl must either:

  • Poll dm list on a timer (wasteful, adds latency, complexity
    of task lifecycle management)
  • Require the user to manually trigger a fetch (poor UX — the
    user must explicitly "check for DMs" instead of receiving them as
    they arrive)

Current workaround

The riverjabber XMPP gateway (a slidge-based bridge) currently
requires the user to run a check-dms chat command to fetch DMs on
demand. This works but is cumbersome — it feels like "checking your
inbox" rather than chat.

Proposed solution

A riverctl dm stream subcommand that outputs JSONL events when new
DMs arrive in a room, following the same pattern as riverctl message stream:

riverctl dm stream <ROOM_ID> [--subscribe] [--format json] [--initial-messages N]

Output format: JSONL, one event per DM, with fields matching the
existing dm list --format json output:

{
  "counterparty": "MAOKOLK5",
  "counterparty_nickname": "alice",
  "direction": "incoming",
  "body": "hello there",
  "purge_token": "aabbccdd11223344aabbccdd11223344",
  "timestamp": "2026-07-31T14:22:00+00:00",
  "timestamp_unix": 1785511320,
  "is_invitation": false
}

The --initial-messages N flag (matching message stream's
equivalent) would emit the last N DMs on startup, so bridges can
backfill recent history when they connect.

This is a CLI-only change — no protocol or node changes needed

The subscription mechanism in subscribe_and_stream()
(cli/src/api.rs) already receives UpdateNotification events for
the entire room contract state, which includes
direct_messages. On each notification, it re-fetches the full
ChatRoomStateV1 via get_room() — this state already contains
direct_messages. The current code simply processes only
recent_messages from that state and ignores direct_messages.

A dm stream command would use the same ContractRequest::Subscribe
mechanism, the same UpdateNotification handling, and the same
get_room() re-fetch — it would just process
room_state.direct_messages instead of
room_state.recent_messages. The node, the contract, and the
protocol are unaffected.

Impact

Every bridge built on riverctl would benefit. Without this, DMs on
any CLI-bridged client require manual fetches or polling. With it,
DMs flow naturally as they arrive — same experience as the River
webUI, which already gets real-time DM updates via the same
subscription mechanism.

References

  • subscribe_and_stream()cli/src/api.rs:5284 (subscribes to
    room contract, re-fetches full state on each UpdateNotification,
    currently processes only recent_messages)
  • dm list JSON output — cli/src/commands/dm.rs:759-787
  • direct_messages in ChatRoomStateV1
    common/src/room_state/direct_messages.rs

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions