fix(session): recover the live stream after a drop or backgrounding - #10
Open
Adam-Dalloul wants to merge 1 commit into
Open
fix(session): recover the live stream after a drop or backgrounding#10Adam-Dalloul wants to merge 1 commit into
Adam-Dalloul wants to merge 1 commit into
Conversation
Two ways an open chat could sit on a frozen shimmer while the server was happily broadcasting the reply, both healed by leaving and re-entering the session. The mid-turn reconnect in consume() only restored the snapshot's pending cards and threw the snapshot's live_message away, so every token the agent produced while the socket was down was lost from the open view. Adopt the rebuilt turn the way consumeReattach does, and rebind the consumer's live turn so later frames land on it. It deliberately does not set liveTurnFromReattach: that flag hides the persisted assistant turns after the last user prompt, and on the send path those are the previous turn's finished reply, since turns is never refetched mid-turn. iOS suspends the app on lock or app switch, killing the socket, and the reconnect backoff is a Task.sleep that cannot progress while suspended, so the budget was spent on attempts that never reached the server and nothing re-attached on return. Latch the background transition and restart recovery with a fresh budget against the same ACP connection, which outlives the WebSocket.
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.
Problem
With a conversation open, sending a message can leave the transcript sitting on the shimmer with nothing streaming in. Leaving the session and re-entering shows the whole reply, so the server had it all along.
Two things combine to produce that:
Task.sleepthat makes no progress while suspended, so the reconnect budget is spent on attempts that never reached the server. Nothing re-attaches on foreground, and the turn stays frozen with no error.consume's.snapshotarm only callsrestorePending. The snapshot'slive_messageis the complete in-flight reply, but it is dropped, so everything the agent produced during the outage stays missing from the open screen.consumeReattachalready handles this correctly withbuildLiveTurn(from:).Change
.snapshoton a mid-turn reconnect now adopts the rebuilt turn (same guard as before, so the initial attach handshake is untouched), andconsumekeeps a mutableliveso later frames land on the turn the transcript is showing rather than an orphan.SessionDetailViewre-attaches on return to the foreground, routed through the existingreconnectStreamrecovery. It is a no-op unless a turn is actually streaming, and it restores the reconnect budget that was burned while suspended..backgroundthen.inactivethen.active.One judgment call worth reviewing
The adoption deliberately does not set
liveTurnFromReattach. On the send pathturnsis the pre-send transcript and is never refetched mid-turn, so it holds no partial copy of this reply to double-render, while the trailing assistant turns thatsuppressInFlightdrops are the previous turn's finished reply. That matches the existing note onbuildPersistedthat suppression is a no-op on the send path.Not included
sinceSeqgap replay is left out on purpose.consume's.replayarm currently discards the events it receives, so requesting a replay would trade a snapshot the client uses for a replay it throws away. Handling.replayinconsumeshould come first, then seq tracking on top.