feat: unify Web UI and TUI conversation stream via v2 protocol#1250
Merged
Conversation
…rotocol
Web UI agent-details and TUI conversation views diverged because each client
stitched REST history with the live WebSocket independently — the TUI dropped
several event types from its backfill and neither client could dedupe across
the history/live boundary. Replaces that with `/v2/stream/{agent_id}`: a
single WebSocket that emits a deterministic snapshot followed by live events,
keyed by a per-agent monotonic `seq`. Both clients migrate; v1 endpoints stay
for one release.
The fix surfaced a real race in the persistence pipeline: persist was
fire-and-forget while broadcast was synchronous, so a client subscribing
between broadcast and DB write would miss the row in its snapshot. Made
`record_and_seq` async and await the insert before returning the seq — the
broadcast now goes out after the row exists, closing the gap.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1250 +/- ##
==========================================
+ Coverage 63.77% 64.03% +0.25%
==========================================
Files 173 173
Lines 7733 7699 -34
Branches 2614 2610 -4
==========================================
- Hits 4932 4930 -2
+ Misses 2780 2748 -32
Partials 21 21
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
…ts on mount Two follow-ups to the v2 stream rollout, both surfacing as "view shows less than the agent has produced": - TUI silently exited its read loop on any WebSocket close or error and had no reconnect logic. Symptom: `Conversation (N)` count freezes and scrolling to the bottom shows nothing new even though the agent is still producing events. Replace the single-pass read with a reconnect loop that tracks the highest `seq` it has observed (from `event` and `snapshot_end` frames) and resubscribes with `since_seq = last_seq` on disconnect. Uses exponential backoff (200 ms → 5 s). - Web UI persisted `last_seq` to sessionStorage, so a fresh visit to an agent detail page resumed from the previous visit's cursor and saw an empty snapshot — no history rendered. Drop the persistence; keep `last_seq` in memory only so the underlying WebSocketManager's auto-reconnects still resume cleanly, but every fresh mount requests the full snapshot. Matches the TUI's behaviour (`conversation_last_seq = 0` on `enter_agent_detail`). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… tail `render_conversation` computed total wrapped rows with a hand-rolled `display_rows` that estimated wrapping as `chars / width`. That ignores ratatui's word-wrap behaviour (and miscounts wide/zero-width characters), so on a long conversation the undercount made `max_scroll` smaller than the true tail. Follow mode then silently parked the viewport above the last events — Conversation (N) kept growing but the bottom row showed an older event. Use `Paragraph::line_count` (gated behind ratatui's `unstable-rendered-line-info` feature, ratatui#293) to get ratatui's own wrapped row count, and drop `display_rows`. The feature is API-stability opt-in only; the underlying call is just the same machinery ratatui uses to render the paragraph. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
/v2/stream/{agent_id}WebSocket that delivers a deterministic snapshot followed by live events, keyed by a new per-agent monotonicseqcolumn.record_and_seqawait the DB insert before broadcast, closing a real race where clients subscribing between broadcast and persist could miss the row in their snapshot.useAgentStream) and TUI (control/stream,control/app) migrate; v1/streamandGET /agents/{id}/conversationstay for one release.Why the views diverged
Both clients already consumed the same orchestrator endpoints, but each stitched REST history with the live WS independently. The TUI fetched history filtered to
output,prompt_sent,tool_use,result— droppingthinking,activity_changed,usage_update,context_clearedfrom backfill while still receiving them live. The Web UI capped at 5000 lines and cached in sessionStorage. Neither client had a sequence number to dedupe across the history/live boundary, so the same conversation could render differently in each app.Wire protocol (
/v2/stream/{agent_id})Client subscribes with
{frame: "subscribe", since_seq: N}. Server then emits:{frame: "snapshot_begin", cursor: N, agent_id: ...}{frame: "event", seq: K, type: "agent:output", ...}(in both snapshot and live phases — identical shape){frame: "snapshot_end", seq: <last replayed>}{frame: "gap", skipped: N, reason: "broadcast_lagged"}on receiver lag{frame: "error", code, message}on bad subscribe / storage failureThe server subscribes to the broadcast channel before the snapshot query so live events arriving during the query are buffered in the receiver and replayed after
snapshot_end, deduped againstlast_replayed.Test plan
cargo test -p orchestrator --test conversation_stream_v2— 6 tests covering monotonic seq, since-window query, snapshot+live correctness, two-client agreement, since_seq resume, migration backfill.cargo test -p orchestrator— 1047 tests pass.cargo clippy --workspace --all-targets -- -D warningsclean.cargo fmt --all -- --checkclean.bunx tsc --noEmitonui/clean.since_seqresume replays only the delta with no duplicates.Notes
agentd-core::config::tests::test_defaults(asserts port 17000, default is now 7000 afterb3aed926 feat: fix port settings) reproduces onmainand is unrelated.seq— additive change, ignored by existing v1 consumers.🤖 Generated with Claude Code