You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Fixes a TUI recovery hang after the Python gateway crashes or disconnects.
The TUI already attempts to recover automatically:
It detects that the gateway exited.
It starts a replacement gateway.
The replacement sends gateway.ready.
The TUI should receive that event and resume the user's session.
Step 4 is currently broken. Restarting the gateway resets GatewayClient to an unsubscribed state. The replacement gateway's events are buffered, but the React subscription effect only runs when the TUI first mounts—it does not run again after a gateway restart.
As a result, the replacement process starts successfully, but its gateway.ready event is never delivered. The TUI remains stuck on:
gateway exited · recovering session…
The user must restart the entire TUI to recover.
This PR makes GatewayClient remember that the TUI consumer already subscribed. After replacing a gateway transport, it automatically re-arms the existing deferred event drain. The replacement gateway's gateway.ready event can then reach the recovery handler and resume the persisted session.
The initial startup behavior from #36658 remains unchanged: events received before React mounts are still buffered and delivered on a later microtask.
Thanks for the focused recovery regression. The premise remains present on current main: GatewayClient.resetStartupState() clears subscribed at ui-tui/src/gatewayClient.ts:221, so later gateway.ready frames buffer through publish() at ui-tui/src/gatewayClient.ts:170-175. The mounted consumer calls gw.drain() only in its effect at ui-tui/src/app/useMainApp.ts:824-826, while the recovery handler restarts the gateway at ui-tui/src/app/useMainApp.ts:810-815.
The patch preserves the existing generation guard and deferred FIFO drain behavior from ui-tui/src/gatewayClient.ts:618-662, while making that mounted-consumer state survive replacement transport startup. Current main has no subsequent changes to either touched file since the PR base, so this should be a clean salvage.
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
comp/tuiTerminal UI (ui-tui/ + tui_gateway/)P2Medium — degraded but workaround existssweeper:blast-moderateSweeper blast radius: moderate — a subsystem or single platformsweeper:risk-message-deliverySweeper risk: may drop, duplicate, misroute, or suppress messagessweeper:risk-session-stateSweeper risk: may lose/corrupt/mis-associate session or context statetype/bugSomething isn't working
3 participants
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?
Fixes a TUI recovery hang after the Python gateway crashes or disconnects.
The TUI already attempts to recover automatically:
gateway.ready.Step 4 is currently broken. Restarting the gateway resets
GatewayClientto an unsubscribed state. The replacement gateway's events are buffered, but the React subscription effect only runs when the TUI first mounts—it does not run again after a gateway restart.As a result, the replacement process starts successfully, but its
gateway.readyevent is never delivered. The TUI remains stuck on:The user must restart the entire TUI to recover.
This PR makes
GatewayClientremember that the TUI consumer already subscribed. After replacing a gateway transport, it automatically re-arms the existing deferred event drain. The replacement gateway'sgateway.readyevent can then reach the recovery handler and resume the persisted session.The initial startup behavior from #36658 remains unchanged: events received before React mounts are still buffered and delivered on a later microtask.
Related Issue
Fixes #63434.
Related history:
Type of Change
Changes Made
GatewayClientresets for a replacement transport.gateway.readyis deliveredgateway.readyis deliveredsession.infoarrives once and in FIFO orderHow to Test
From
ui-tui:npm run build --prefix packages/hermes-ink npm test -- --run \ src/__tests__/gatewayClient.test.ts \ src/__tests__/gatewayRecovery.test.ts \ src/__tests__/createGatewayEventHandler.test.ts npm run typecheck npx eslint src/gatewayClient.ts src/__tests__/gatewayClient.test.ts npx prettier --check src/gatewayClient.ts src/__tests__/gatewayClient.test.tsResults:
The new regression fails on current
main:Only the original gateway event is delivered. The replacement gateway's events remain buffered.
With this change, the observed order is:
This proves that event delivery survives the transport replacement and preserves FIFO ordering.
Checklist
Code
fix(scope):,feat(scope):, etc.)Documentation & Housekeeping
cli-config.yaml.example— N/A; no configuration changedCONTRIBUTING.mdorAGENTS.md— N/A; no architecture or workflow changedScreenshots / Logs
Not applicable. This transport-lifecycle failure is covered by a deterministic fake-WebSocket regression.