fix(tui_gateway): preserve websocket batch order - #69684
Merged
Merged
Conversation
helix4u
marked this pull request as ready for review
July 23, 2026 00:07
The batch-serialization fix routes every send through _safe_send_many; _safe_send became a dead single-line wrapper with no callers. Remove it.
OutThisLife
approved these changes
Jul 23, 2026
OutThisLife
left a comment
Contributor
There was a problem hiding this comment.
Approve. Confirmed the ordering bug on current main: multiple _safe_send_many batches are scheduled as independent tasks with nothing serializing them across the await send_text() yield, so a loop stall interleaves them (A1,B1,B2,A2). The per-transport asyncio.Lock held across the whole batch — plus folding pending tokens + the control frame into one locked batch in write_async — closes both slip-in paths, and the ordering-invariant tests cover them. supplefrog's original #48446 work is preserved. Pushed one tiny follow-up removing the now-dead _safe_send wrapper. Enabling auto-merge.
OutThisLife
enabled auto-merge (squash)
July 23, 2026 04:56
OutThisLife
disabled auto-merge
July 23, 2026 04:58
OutThisLife
enabled auto-merge (squash)
July 23, 2026 04:59
randlee
pushed a commit
to randlee/hermes-agent
that referenced
this pull request
Aug 11, 2026
* fix: serialize TUI gateway websocket sends * fix(tui_gateway): preserve websocket batch order * refactor(tui_gateway): drop unused _safe_send wrapper The batch-serialization fix routes every send through _safe_send_many; _safe_send became a dead single-line wrapper with no callers. Remove it. --------- Co-authored-by: supplefrog <78985073+supplefrog@users.noreply.github.com> Co-authored-by: Brooklyn Nicholson <brooklyn.bb.nicholson@gmail.com>
19 tasks
prmartinow
pushed a commit
to prmartinow/hermes-agent
that referenced
this pull request
Aug 26, 2026
* fix: serialize TUI gateway websocket sends * fix(tui_gateway): preserve websocket batch order * refactor(tui_gateway): drop unused _safe_send wrapper The batch-serialization fix routes every send through _safe_send_many; _safe_send became a dead single-line wrapper with no callers. Remove it. --------- Co-authored-by: supplefrog <78985073+supplefrog@users.noreply.github.com> Co-authored-by: Brooklyn Nicholson <brooklyn.bb.nicholson@gmail.com>
melon-xf
added a commit
to melon-xf/hermes-agent
that referenced
this pull request
Sep 3, 2026
* fix: serialize TUI gateway websocket sends * fix(tui_gateway): preserve websocket batch order * refactor(tui_gateway): drop unused _safe_send wrapper The batch-serialization fix routes every send through _safe_send_many; _safe_send became a dead single-line wrapper with no callers. Remove it. --------- Co-authored-by: supplefrog <78985073+supplefrog@users.noreply.github.com> Co-authored-by: Brooklyn Nicholson <brooklyn.bb.nicholson@gmail.com>
1 task
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.
What does this PR do?
Serializes WebSocket writes per
WSTransportacross whole coalesced batches so streamed Desktop/dashboard frames cannot arrive out of order when the event loop recovers from a long stall.The token buffer's
threading.Lockcurrently preserves buffer-drain and task-creation order, but it does not serialize the asynchronous socket writes. Multiple_safe_send_many()tasks can therefore be in flight together, and each task yields atawait send_text(). A later batch can enter the socket between two frames from an earlier batch. A deterministic reproduction producedA1, B1, B2, A2from logically ordered batches[A1, A2]and[B1, B2].This PR carries forward the per-socket serialization work from #48446 with the original contributor's authorship preserved, then adapts it to the token-batching implementation that landed afterward. It does not reopen the WebSocket-disconnect diagnosis resolved by #55545; this fixes the separate frame-ordering invariant.
Related Issue
Related to #48446 and #55545.
Type of Change
Changes Made
tui_gateway/ws.py: add a per-transport async writer lock and hold it for each complete_safe_send_many()batch.tui_gateway/ws.py: route single-frame sends through the same serialized writer path.tui_gateway/ws.py: send tokens drained bywrite_async()and its current control/RPC frame in one lock acquisition so another batch cannot overtake the current frame.tests/test_tui_gateway_ws.py: retain the original concurrent-send regression and add deterministic cross-batch andwrite_async()ordering coverage.How to Test
tests/test_tui_gateway_ws.pythrough the project test runner.test_ws_transport_preserves_cross_batch_orderblocks batch B until every frame in batch A has been sent.test_ws_write_async_keeps_drained_tokens_with_current_framekeeps the current frame immediately behind the pending tokens it drains.Focused local validation on Windows 11:
python -m py_compile tui_gateway/ws.py tests/test_tui_gateway_ws.pypassed.git diff --checkpassed.Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests passDocumentation & Housekeeping
docs/, docstrings) — N/A, no user-facing contract changedcli-config.yaml.exampleif I added/changed config keys — N/A, no config changedCONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/A, no workflow changedasyncio.LockScreenshots / Logs
Before the fix, the deterministic batch reproduction entered the socket as
A1, B1, B2, A2. With the serialized batch boundary, it enters asA1, A2, B1, B2.