Skip to content

fix(chat): don't abort healthy streams during prefill (#6231) - #6232

Open
touzenesmy wants to merge 1 commit into
odysseus-dev:devfrom
touzenesmy:fix/chat-prefill-tab-recovery
Open

fix(chat): don't abort healthy streams during prefill (#6231)#6232
touzenesmy wants to merge 1 commit into
odysseus-dev:devfrom
touzenesmy:fix/chat-prefill-tab-recovery

Conversation

@touzenesmy

@touzenesmy touzenesmy commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Summary

When a stream has not yet emitted its first token, the visibilitychange tab-recovery handler treats "no bytes received since the tab was hidden" as evidence of a frozen connection — but a slow local / multimodal model can legitimately spend minutes pre-filling (image + large agent prompt) before its first chunk, and the main prefill path sends no SSE heartbeat at all, so lastActivity stays frozen. Returning to the tab after 20s+ therefore aborted a perfectly healthy stream with "interrupted after the tab went inactive".

Fix: track firstOutputAt on the stream entry (set on the first data event) and skip tab-recovery until the stream has produced output:

  • no first token yet → normal prefill silence, don't abort;
  • had output then froze → recovery works exactly as before.
    A genuinely dead prefill is still caught by the response timeout, not tab-recovery.

Target branch

  • This PR targets dev, not main. All PRs land in dev; main is curated by the maintainer at each release. If your PR is on main by accident, click "Edit" on this PR and change the base.

Linked Issue

Fixes #6231 — reported there before filing this PR (no existing report existed: searched open and closed issues for the exact error string, 0 results). Repro steps in How to Test below mirror the issue's Steps to Reproduce.

Type of Change

  • Bug fix (non-breaking — fixes a confirmed issue)
  • New feature (non-breaking — adds new behaviour)
  • Breaking change (changes or removes existing behaviour)
  • Refactor / cleanup (behaviour unchanged)
  • Documentation only
  • CI / tooling / configuration

Checklist

How to Test

  1. Repro (without the fix): send a prompt whose prefill takes >20s on a slow/local model — attach an image plus a large prompt. Immediately switch to another browser tab, wait 25s+, return. Before this PR: the stream is killed with "interrupted after the tab went inactive" even though the server is still happily pre-filling. With this PR: the stream continues.
  2. Regression (frozen streams still recover): kill the backend mid-stream after the first token, switch tabs 20s+, return. The tab-recovery path fires exactly as before — the firstOutputAt guard only skips streams that never produced output.
  3. node --check static/js/chat.js passes. (No unit test — this is frontend stream-state logic; the two behavioural paths above cover it.)

Visual / UI changes — REQUIRED if you touched anything that renders

Anything that changes what the UI looks like — buttons, icons, padding, colors, fonts, spacing, layout, CSS, HTML, SVG, or any static/js/ module that draws to the DOM — needs all of the following. PRs that change rendering without these WILL be closed.

  • Screenshot or short clip of the change in the running app, attached below. Mobile screenshot too if the change affects mobile. — Behaviour-only change (stream lifecycle; no DOM/CSS/styling change). Per the template's static/js clause a clip is attached below showing the stream surviving a tab-away during prefill.
  • Style match: the change uses Odysseus's existing visual language. Specifically:
    • Reuse existing CSS variables (--red, --fg, --bg, --card, --border, etc.) — do not introduce new color values, font sizes, or spacing units.
    • Reuse existing button/input/card/border classes. Don't invent parallel styling.
    • No Unicode emoji in UI or code. Use inline SVG (matching the monochrome icon style already in static/index.html) or plain text.
    • Monospaced font (Fira Code) for primary UI text. Don't override.
    • Dark theme is the default; any light-mode work must be wired through the existing theme system, not hard-coded.
  • No new component patterns. If a similar widget already exists in the app, extend it instead of writing a parallel one.
  • I am not an LLM agent submitting a bulk PR. If you are, please open an issue describing the problem first — bulk auto-generated PRs that don't match the project's visual style are closed on sight, even when the underlying fix is correct.

Screenshots / clips

I had a lot of trouble to say below 10Mb

brave_PHihh1TWja.mp4

(clip of the tab-away-during-prefill repro attached before publishing)

The visibilitychange tab-recovery handler treated 'no bytes received'
as a frozen connection. A slow local / multimodal model can spend
minutes pre-filling (image + large agent prompt) before its first token,
and the main prefill path sends no SSE heartbeat, so lastActivity stays
frozen. Returning to the tab after 20s+ aborted a perfectly healthy
stream with 'interrupted after the tab went inactive'.

Track firstOutputAt on the stream entry (set on the first data event)
and skip tab-recovery until the stream has produced output:
- no first token yet -> normal prefill silence, don't abort
- had output then froze -> recovery works exactly as before
A genuinely dead prefill is still handled by the response timeout.
@github-actions github-actions Bot added needs visual evidence UI-sensitive change without an attested screenshot or clip from the running app ready for review Description complete — ready for maintainer review and removed needs visual evidence UI-sensitive change without an attested screenshot or clip from the running app labels Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready for review Description complete — ready for maintainer review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Chat stream aborted during prefill after tab inactivity — tab-recovery misreads prefill silence as a frozen connection

2 participants