Skip to content

feat(cli): add buzz feed watch for streaming activity events - #3562

Open
mvanhorn wants to merge 1 commit into
block:mainfrom
mvanhorn:fix/buzz-cli-feed-watch-stream
Open

feat(cli): add buzz feed watch for streaming activity events#3562
mvanhorn wants to merge 1 commit into
block:mainfrom
mvanhorn:fix/buzz-cli-feed-watch-stream

Conversation

@mvanhorn

@mvanhorn mvanhorn commented Jul 29, 2026

Copy link
Copy Markdown

Summary

buzz feed watch streaming activity events as NDJSON

Simulated demo. Terminal frames are verbatim captured output — the opening is this branch's parent commit answering unrecognized subcommand 'watch'. The event-delivery diagram is labelled illustrative — not a live capture because no relay was run, and the closing frame states that the receive loop has no automated coverage yet.

FeedCmd has exactly one variant, Get, which issues a single REST query and returns. An agent that wants to react to mentions has to poll buzz feed get --since <ts> on a timer, which costs a latency floor equal to the poll interval, delivers duplicates at window boundaries that the caller has to dedupe by event id, and pushes cursor management onto the caller.

The pieces for a subscription are already in the tree and unused. buzz-ws-client is already a buzz-cli dependency ("WebSocket client — ephemeral event publish") and already exposes connect_authenticated, send_raw, next_event, disconnect, and a RelayMessage enum covering Event / Eose / Closed / Notice / Auth / Count. Today the CLI uses exactly one of these, for one-shot ephemeral publish. The subscription half of the same client was never wired up.

buzz feed watch authenticates, sends a NIP-01 REQ with the same filter cmd_get_feed builds, and prints one JSON event per line, flushing immediately:

buzz feed watch [--types <csv>] [--since <unix>] [--channel <UUID>] [--idle-timeout <secs>]
  • Eose is silent (it only marks the end of backfill).
  • Notices and subscription closures go to stderr, so stdout stays pure NDJSON for a jq --unbuffered consumer.
  • An Auth challenge triggers re-authentication.
  • Ctrl-C closes the subscription and disconnects cleanly.
  • --since replays backfill first and then stays live, so an agent restarting after a crash resumes from its last seen timestamp in one command.

NDJSON rather than a JSON array because the stream never terminates, so the array would never close. buzz feed get keeps its existing array output untouched.

parse_feed_types and build_feed_filter are extracted from cmd_get_feed so get and watch reject identical bad input and select identical events. tokio gains the signal and time features for ctrl_c and the idle deadline.

Related issue

None found. Searched open issues and PRs for feed watch, stream subscribe cli — zero hits. #944 (feat(agent): SSE streaming for LLM completions, closed unmerged) is LLM token streaming inside buzz-agent and shares no surface with a relay subscription.

Every comparable platform ships a listen primitive: Slack Socket Mode, mmctl websocket, Zulip's call_on_each_message.

Testing

cargo test -p buzz-cli — 258 passed, 0 failed. New unit tests in commands::feed::tests, no relay required:

  • every valid feed type accepted; an unknown one rejected with CliError::Usage; surrounding whitespace trimmed (parity with today's feed get behavior)
  • the no-flags filter is asserted equal to what feed get sent before this change (regression guard)
  • optional fields (since, #h, feed_types, limit) appear only when supplied
  • subscription ids stay inside the NIP-01 1–64 char bound and do not repeat

just fmt and cargo clippy -p buzz-cli --all-targets -- -D warnings are both clean.

Local verification:

$ buzz feed --help
Commands:
  get    Get recent activity feed entries
  watch  Stream activity feed entries as they arrive (NDJSON, one event per line)

$ buzz feed watch --types bogus
{"error":"user_error","message":"invalid feed type \"bogus\" — must be one of: mentions, needs_action, activity, agent_activity","retryable":false}

Input validation returns before any relay connection is attempted.

Three details in the receive loop are worth a reviewer's attention, since they are the parts a stub test would not have caught:

  1. Only WsClientError::Timeout counts as an idle tick. Any other error — a closed socket, a transport failure — returns Err rather than being retried, so a dropped connection surfaces instead of spinning.
  2. The idle deadline is its own select! branch rather than a check after next_event returns, because next_event answers relay Pings internally and restarts its own timeout; a chatty relay would otherwise keep it pending past the deadline forever. The deadline resets only on a delivered Event.
  3. Re-authentication races both Ctrl-C and the same absolute deadline, so a relay that issues AUTH and withholds OK cannot swallow a SIGINT or stretch --idle-timeout.

The receive loop itself has no automated coverage — that needs a stub WebSocket server. Cargo.toml already carries axum as a dev-dependency for "minimal HTTP test server for retry/policy integration tests", so a tests/feed_watch.rs driving a stub relay is the shape to follow if you would like that before merge. Happy to add it.

No UI change. crates/buzz-cli/README.md documents the subcommand and the NDJSON contract.


AI was used for assistance. I wrote, ran, and reviewed the final code and tests.

FeedCmd had exactly one variant, Get, which issues a single REST query and
returns. An agent that wants to react to mentions had to poll feed get on a
timer and dedupe across window boundaries, paying a latency floor equal to the
poll interval and carrying its own cursor.

buzz-ws-client is already a buzz-cli dependency and already exposes everything
a subscription needs (connect_authenticated, send_raw, next_event, disconnect,
and a RelayMessage enum). Only the one-shot publish half was being used.

Add buzz feed watch: authenticate, send a NIP-01 REQ with the same filter
cmd_get_feed builds, then print one JSON event per line and flush. Eose is
silent, notices and closures go to stderr, and an Auth challenge triggers
re-authentication. Ctrl-C closes the subscription and disconnects cleanly.

NDJSON rather than a JSON array because the stream never terminates, so the
array would never close. feed get keeps its array output unchanged.

parse_feed_types and build_feed_filter are extracted so get and watch validate
identical input and select identical events; a regression test pins the
no-flags filter to what feed get sent before.

tokio gains the signal feature for ctrl_c.

Signed-off-by: Matt Van Horn <mvanhorn@gmail.com>
@mvanhorn
mvanhorn requested a review from a team as a code owner July 29, 2026 15:13
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