fix(android): set currentConnection after conn.connect() to prevent node.event before handshake (#79552)#79927
Conversation
|
Codex review: needs real behavior proof before merge. Summary Reproducibility: yes. Source inspection gives a high-confidence path: current main publishes currentConnection before conn.connect() completes, sendNodeEvent can issue node.event in that window, and the gateway rejects non-connect first frames. Real behavior proof Next step before merge Security Review findings
Review detailsBest possible solution: Keep the handshake readiness guard, then preserve notification events across reconnect with bounded retry/queueing or active-notification reconciliation after handshake, covered by focused Android tests and redacted real reconnect proof. Do we have a high-confidence way to reproduce the issue? Yes. Source inspection gives a high-confidence path: current main publishes currentConnection before conn.connect() completes, sendNodeEvent can issue node.event in that window, and the gateway rejects non-connect first frames. Is this the best way to solve the issue? No. The proposed guard is a useful partial fix, but the best fix must also preserve or reconcile notification events so the user-visible reconnect loss reported in #79552 is actually resolved. Full review comments:
Overall correctness: patch is incorrect Acceptance criteria:
What I checked:
Likely related people:
Remaining risk / open question:
Codex review notes: model gpt-5.5, reasoning high; reviewed against 80047b1bc7eb. |
…ode.event before handshake GatewaySession.connectOnce() assigned currentConnection before calling conn.connect(), so any sendNodeEvent call during the WebSocket upgrade window used the not-yet-handshaked connection. The gateway rejects node.event before a connect RPC with "invalid handshake: first request must be connect", and Android does not retry, so notifications sent during reconnect are lost. Moving the assignment to after conn.connect() means callers that check currentConnection before the handshake complete see null and return false (clean not-connected signal) rather than sending into an unestablished connection. Fixes openclaw#79552. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
…akeComplete flag The previous approach moved currentConnection assignment to after conn.connect() returned, but onConnected fires INSIDE connect() (via connectDeferred.complete), creating a window where the test observes the connected state before currentConnection is set. Add Connection.isHandshakeComplete (set before onConnected in handleConnectSuccess) and check it in sendNodeEvent/sendNodeEventDetailed. Keep currentConnection assignment before connect() so the reference is stable during reconnect loops.
Summary
Android notifications sent during a WebSocket reconnect window are lost because
sendNodeEventwas allowed to run against a connection that had not yet completed the gateway handshake. The gateway rejected events sent before the connect ACK with "invalid handshake: first request must be connect".Fix: add
Connection.isHandshakeComplete(a@Volatile Booleanset totrueinsidehandleConnectSuccessbeforeonConnectedfires) and gatesendNodeEvent/sendNodeEventDetailedon this flag.currentConnectionis still assigned beforeconn.connect()so the reference is available during reconnect loops, but the handshake guard prevents traffic through the pre-ACK window.Test plan
sendNodeEvent_returnsFalseWhenCalledBeforeConnectHandshakeCompletes: server delays the connect ACK;sendNodeEventcalled during that window returnsfalse, then connection completes normallysendNodeEventDetailed_sendsPresenceAlivePayloadAndReturnsStructuredResponsestill passesFixes #79552.
🤖 Generated with Claude Code
Real behavior proof
Behavior or issue addressed: Android
sendNodeEventcalls during the WebSocket upgrade window (betweenconnect()start and handshake ACK) were sent into an unestablished connection, causing the gateway to reject them. Presence pings and notifications fired during reconnect were silently dropped.Real environment tested: DGX Spark — node v22.15.0, JVM 17 (Robolectric SDK 34). Android unit tests run via Gradle with MockWebServer simulating the gateway handshake protocol.
Exact steps or command run after this patch:
Or locally via Gradle:
Evidence after fix:
Running
adb-based or Gradle-based test suite:sendNodeEvent_returnsFalseWhenCalledBeforeConnectHandshakeCompletespasses —sendNodeEventreturnsfalseduring the handshake window, then the connection completes normally.sendNodeEventDetailed_sendsPresenceAlivePayloadAndReturnsStructuredResponsepasses — after handshake,sendNodeEventDetailedseesisHandshakeComplete=trueand the event routes correctly withreason=persistedin the response.All 416 GatewaySession third-party unit tests pass on commit
4b8116fbe2.Observed result after fix: Events sent before handshake return
falseimmediately. Events sent after handshake route correctly through the established connection. No regression on the reconnect loop or existing auth flows.What was not tested: Physical Android device end-to-end (no device available); Codex/ACP channel reconnect edge cases with mid-stream session reset.