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
Pending approval/question waits can be silently dropped on reconnect, leaving the turn permanently blocked and the UI appearing stuck — because the two authoritative comments about replay-vs-resync ordering contradict each other, and the real ordering is racy.
// Superseded, not settled: the baseline replay re-sends still-pending requested frames verbatim// (same rpcId), re-minting fresh waits; a stale reference's respond() still reaches the host.this.pending.clear()// ← clears all pending waits on reconnectthis.pendingRev++...awaitthis.open()
// Strict readiness handshake: describe proves unary reachability, onOpen// proves each physical stream is established before any frame —// only then may onConnected fire, so the resync it triggers cannot outrun the// subscribed baseline.
Contradiction: session.ts assumes replay re-sends pending frames afterresync() clears them ("re-minting fresh waits"). connection.ts asserts onConnected fires before any frame — "resync cannot outrun the subscribed baseline". Both cannot be true for the same race.
The mux stream pump (connection.ts:178-192, pumpStream) starts consuming frames as soon as the stream opens — beforePromise.all([describe, streamsOpen]) settles and onConnected fires. The mux stream opens independently; replay frames (including approval/requested, question/requested) can be delivered and processed by SessionManager.handleMuxEnvelope → Session.mint() (creating fresh PendingWaits) beforedescribe returns / streamsOpen resolves. Then onConnected → session.resync() clears this.pending entirely.
Result: a pending approval/question that existed across the disconnect is dropped mid-session. The host never resends it (it already sent it on this connection generation); the user never sees the interaction; the suspended turn blocks forever until a full page refresh.
Trigger
Disconnect/reconnect while an approval or question is pending, with mux replay frames arriving before the describe/onConnected handshake completes (frame-light reconnects, i.e. the common case).
Suggested fix
Before resync() clears pending, have the manager record the request identities (rpcId/approvalId) of the still-pending waits and re-arm replay expectations for those identities after resync — or, better, do not clear pending in resync() at all: let the generation invalidation supersede the pending identities and move the cleanup to the session/subscribed frame (same moment the queue mirror re-baselines), so ordering is guaranteed. Also consolidate the replay-ordering contract into one authoritative comment.
Reviewed at master 47f9438. Web UI runs locally (http://127.0.0.1:3080) — this is a code-path race, not observed live (requires reconnect with pending interaction).
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Summary
Pending approval/question waits can be silently dropped on reconnect, leaving the turn permanently blocked and the UI appearing stuck — because the two authoritative comments about replay-vs-resync ordering contradict each other, and the real ordering is racy.
Evidence
packages/client/runtime/src/client/sessions/session.ts:416-438(resync()):packages/client/connection/src/client/connection.ts:133-137(handshake):Contradiction:
session.tsassumes replay re-sends pending frames afterresync()clears them ("re-minting fresh waits").connection.tsassertsonConnectedfires before any frame — "resync cannot outrun the subscribed baseline". Both cannot be true for the same race.The mux stream pump (
connection.ts:178-192,pumpStream) starts consuming frames as soon as the stream opens — beforePromise.all([describe, streamsOpen])settles andonConnectedfires. The mux stream opens independently; replay frames (includingapproval/requested,question/requested) can be delivered and processed bySessionManager.handleMuxEnvelope→Session.mint()(creating freshPendingWaits) beforedescribereturns /streamsOpenresolves. ThenonConnected→session.resync()clearsthis.pendingentirely.Result: a pending approval/question that existed across the disconnect is dropped mid-session. The host never resends it (it already sent it on this connection generation); the user never sees the interaction; the suspended turn blocks forever until a full page refresh.
Trigger
Disconnect/reconnect while an approval or question is pending, with mux replay frames arriving before the describe/onConnected handshake completes (frame-light reconnects, i.e. the common case).
Suggested fix
Before
resync()clearspending, have the manager record the request identities (rpcId/approvalId) of the still-pending waits and re-arm replay expectations for those identities after resync — or, better, do not clearpendinginresync()at all: let the generation invalidation supersede the pending identities and move the cleanup to thesession/subscribedframe (same moment the queue mirror re-baselines), so ordering is guaranteed. Also consolidate the replay-ordering contract into one authoritative comment.Reviewed at master
47f9438. Web UI runs locally (http://127.0.0.1:3080) — this is a code-path race, not observed live (requires reconnect with pending interaction).All reactions