From 74110b4d7205ed0d1df3c8119fcb3821353fb477 Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Fri, 4 Sep 2026 14:01:01 -0400 Subject: [PATCH] The reconnect loop does not reproduce; log who falls behind Measured against a session streaming 20 events a second. Reopening one with an anchor 1,800 events back connects 87-119 events behind, under CATCH_UP_LIMIT's 200 -- the restore is two requests now, the opening page and one span covering the whole distance to the anchor, where the report was written against a restore that took thirteen. Three provocations, no repeat connection between them: that reopen, twenty seconds backgrounded while 415 events were produced, and a reset forced by dropping the limit to 5, where the app cleared, refilled and carried on without reconnecting. So the TODO entry goes. What stays is the instrument that answered it: every SSE subscriber now logs at debug the cursor it arrived with and whether it was continued or reset, since nothing else could say. The app sees a window arrive and cannot tell how far it had fallen. Co-Authored-By: Claude Opus 5 --- AGENTS.md | 19 +++++++++++++++++++ TODO.md | 13 ------------- server/src/routes.rs | 13 ++++++++++++- 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 103ada8..1f49443 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -541,6 +541,25 @@ first if a remote spawn ever mangles an argument. `pm grant ... POST_NOTIFICATIONS`. And a saved scroll anchor is per session id, so the only way two builds start a scroll from the same place is a *fresh session for each*. +- **A phone that falls behind the stream is answered with `reset`, and + `RUST_LOG=ai_server=debug` says when.** Every SSE subscriber logs the + cursor it arrived with and whether it was continued or reset + (`stream backlog:` in `send_backlog`), which is the only place that + question is answerable: the app sees a window arrive and cannot tell how + far it had fallen, and a reset is the one thing that makes its screen jump + to the newest end. Measured 2026-09-04 against a session streaming at 20 + events a second: reopening one with an anchor 1,800 events back connects + **87-119 events behind**, well under `CATCH_UP_LIMIT`'s 200, because the + restore is two requests -- the opening page, then one span covering the + whole distance to the anchor. So the reset path is not reachable by + reopening a session, and **to exercise it at all you have to lower + `CATCH_UP_LIMIT`** in a throwaway server build; at 5 the app takes the + reset on a live connection, clears, refills and carries on without + reconnecting. Worth knowing alongside it: **the session screen's stream + survives backgrounding here** -- 20 seconds at the launcher while 415 + events were produced brought no reconnect at all -- which is not what the + comment above that loop expects, and is most likely this emulator being + headless rather than the phone's behaviour. - **`ai-server --delay MS` holds every response back.** Over the tunnel a phone's requests take tens to hundreds of milliseconds, and several faults live entirely in what the app does *while* one is outstanding. On diff --git a/TODO.md b/TODO.md index 5155c84..1f9dcde 100644 --- a/TODO.md +++ b/TODO.md @@ -16,19 +16,6 @@ one in place when it turns out to need a decision. would show exactly as "sometimes". Confirming it means driving a real stream-json session and sending it messages in both states. -## App — reconnect - -- [ ] Restarting the app onto a session with a saved anchor, while a long - reply was streaming, left it reconnecting every 1.5s - (`RECONNECT_DELAY_MS`) with the spinner up until the server was - restarted. `events?after=N` more than `CATCH_UP_LIMIT` (200) behind - answers `reset` plus the newest 200 *raw* deltas -- a window starting - mid-message -- and the reset clears `items`, which is the state the - restore loop then pages against. **May already be fixed:** the - restore's one-event-per-request bug was part of what made it so - visible and has since been fixed, so the first thing to find out is - whether this survives that. Found 2026-09-03. - ## Session settings - [ ] Autocompact belongs in session settings; empty disables it, which is the diff --git a/server/src/routes.rs b/server/src/routes.rs index f2b5c51..6cbdf15 100644 --- a/server/src/routes.rs +++ b/server/src/routes.rs @@ -1788,9 +1788,20 @@ async fn stream_session( /// and local; revisit if daily use produces transcripts where this shows /// (phase 6 territory). async fn send_backlog(transcript: &Path, last: &mut u64, tx: &mpsc::Sender) -> bool { + let cursor = *last; let entries = match catch_up(transcript, *last, CATCH_UP_LIMIT) { - Ok(CatchUp::Continue(entries)) => entries, + Ok(CatchUp::Continue(entries)) => { + // The pair of them at debug, because "was this subscriber reset, + // and how far behind was it" is a question about a phone that + // nothing else here can answer -- the app sees a window arrive + // and cannot tell how far it had fallen, and a reset is the one + // thing that makes its screen jump. `RUST_LOG=ai_server=debug`, + // beside the transcript pages. + tracing::debug!(cursor, sent = entries.len(), "stream backlog: continue"); + entries + } Ok(CatchUp::Restart(entries)) => { + tracing::debug!(cursor, sent = entries.len(), "stream backlog: reset"); if tx.send(SseEvent::default().event("reset")).await.is_err() { return false; }