ruvnet / ruvnet/ruflo

ruvocal: streamed answers freeze mid-render in hidden tabs (throttled smoothing timers + rAF-only DOM flush)

Open
#3,002 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
72.8k
Forks
8.6k
Avg merge
2d 23h
Merged PRs (30d)
82

Description

Summary

In the ruvocal web UI, a long streamed answer freezes mid-render whenever the browser tab is hidden (user switches tab/app while the answer generates). The server finishes and logs [mcp] completion stream closed + final answer emitted, but the page keeps showing "Stop generating" with the text cut off mid-table, and crawls at ~1 word/second — effectively forever for a long answer. Reloading shows the full message (it persisted fine), so users read this as a hang when it's a client render bug.

Environment

  • ruflo 3.38.2 (npm), ruvocal from ruflo/src/ruvocal, production build (vite build, adapter-node)
  • Chrome (macOS), any model/endpoint — reproduced with both an OpenAI-compatible shim and Ollama passthrough

Repro

  1. Start ruvocal, open a conversation, ask for a long answer (~800+ words, include a markdown table so the cutoff is obvious).
  2. While it streams, switch to another tab/app (i.e. document.visibilityState === "hidden").
  3. Wait for the server log to print [mcp] completion stream closed.
  4. Page still shows "Stop generating"; rendered text grows at ~5.5 chars/sec (measured: document.body.innerText.length grew 33 chars over 6 s, minutes after stream close).

Root cause — two compounding hidden-tab defects

1. src/lib/utils/messageUpdates.tssmoothStreamUpdates paces output with setTimeout-based sleeps of 5–80 ms per word chunk. Chrome clamps timers in hidden tabs to ≥1 s, so every chunk costs ≥1 s → a 20k-char answer takes ~1 hour instead of seconds. (The backlog-pressure fast path doesn't rescue it: while generating, the backlog is consumed one ≥1 s sleep at a time, so pressure never builds enough to zero the delays.)

2. src/routes/conversation/[id]/+page.sveltescheduleFrameFlush flushes buffered tokens to the DOM only via requestAnimationFrame — and rAF never fires in hidden tabs, so even drained tokens don't reach the page.

Fix (verified locally)

Both changes are hidden-tab-scoped; visible-tab smoothing behavior is unchanged and all 14 existing messageUpdates.spec.ts cases still pass.

// messageUpdates.ts — consumer loop, before sleeping:
const pageHidden =
    typeof document !== "undefined" && document.visibilityState === "hidden";
if (delayMs > 0 && !pageHidden) {
    await sleep(delayMs);
}
// +page.svelte — scheduleFrameFlush: always arm a timeout fallback beside rAF
const flush = () => {
    if (!frameFlushScheduled) return; // idempotent: rAF + timeout may both fire
    frameFlushScheduled = false;
    flushBuffer(new Date());
};
if (typeof requestAnimationFrame === "function") {
    requestAnimationFrame(flush);
}
setTimeout(flush, 300);

After the fix, the same repro (tab hidden the entire run, 6.6k-token answer) renders completely within seconds of stream close, with the composer state correctly reset.

Happy to open a PR with these two changes if useful.

Related

  • Filed the same day as #2990 (HTTP transport residuals); same local install.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with src/lib/utils/messageUpdates.ts and the smoothStreamUpdates consumer loop, then inspect scheduleFrameFlush in src/routes/conversation/[id]/+page.svelte. Run the existing messageUpdates.spec.ts cases and reproduce with document.visibilityState set to hidden. Done means long hidden-tab streams render completely and the composer leaves the “Stop generating” state after completion, without changing visible-tab smoothing.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
frontend, performance
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.