Skip to content

fix(session): recover the live stream after a drop or backgrounding - #10

Open
Adam-Dalloul wants to merge 1 commit into
xintaofei:mainfrom
Adam-Dalloul:fix/live-stream-recovery
Open

fix(session): recover the live stream after a drop or backgrounding#10
Adam-Dalloul wants to merge 1 commit into
xintaofei:mainfrom
Adam-Dalloul:fix/live-stream-recovery

Conversation

@Adam-Dalloul

Copy link
Copy Markdown

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:

  1. iOS suspends the app on screen lock or an app switch, which kills the event socket mid-turn. The silent reconnect backoff is a Task.sleep that 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.
  2. Even when a reconnect does land, consume's .snapshot arm only calls restorePending. The snapshot's live_message is the complete in-flight reply, but it is dropped, so everything the agent produced during the outage stays missing from the open screen. consumeReattach already handles this correctly with buildLiveTurn(from:).

Change

  • .snapshot on a mid-turn reconnect now adopts the rebuilt turn (same guard as before, so the initial attach handshake is untouched), and consume keeps a mutable live so later frames land on the turn the transcript is showing rather than an orphan.
  • SessionDetailView re-attaches on return to the foreground, routed through the existing reconnectStream recovery. It is a no-op unless a turn is actually streaming, and it restores the reconnect budget that was burned while suspended.
  • A latch is used rather than comparing against the previous phase, because coming back can report .background then .inactive then .active.

One judgment call worth reviewing

The adoption deliberately does not set liveTurnFromReattach. On the send path turns is 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 that suppressInFlight drops are the previous turn's finished reply. That matches the existing note on buildPersisted that suppression is a no-op on the send path.

Not included

sinceSeq gap replay is left out on purpose. consume's .replay arm currently discards the events it receives, so requesting a replay would trade a snapshot the client uses for a replay it throws away. Handling .replay in consume should come first, then seq tracking on top.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant