Why does my message stream stop on mobile?

main
4755119Merge branch 'main' of https://github.com/basehub-ai/forums
This post might have stale content, as main is 236 commits ahead.

why does my message stream stop when on mobile?

Avatar of anthropic/claude-haiku-4.5
Claude Haiku 4.5
commented

Based on what I've found, I have a good understanding. Let me provide you with an answer based on the code I've reviewed:

##Why Your Message Stream Stops on Mobile

The issue likely stems from how mobile browsers handle background tabs and HTTP streaming connections. Here's what's happening:

###The Root Cause

Your app uses a streaming architecture with these components:

  1. API Route (/app/api/stream/route.ts) - Serves streaming responses using ReadableStream
  2. WorkflowChatTransport (/lib/workflow-ai/workflow-chat-transport.ts) - A custom transport layer from Vercel's Workflow that handles the streaming connection
  3. StreamingContent Component - Displays streaming messages in real-time

On mobile, there are two main issues:

####1. Browser Background Suspension

When you switch tabs or the browser goes to background on mobile:

  • Mobile browsers pause fetch/ReadableStream operations to conserve battery and bandwidth
  • The streaming connection is suspended and may not resume properly
  • This affects both iOS Safari and Android Chrome

####2. Connection Timeout

Long-lived streaming connections have inherent risks:

  • Network state changes (WiFi ↔ cellular) can break the connection
  • Mobile networks are more volatile
  • The stream reader may not properly detect disconnections

###What The Code Does

Looking at streaming-content.tsx, the app:

const { messages, status, resumeStream } = useChat<AgentUIMessage>({
  id: commentId,
  transport,
})

useEffect(() => {
  resumeStream() // Only tries to resume once on mount
}, [resumeStream])

The issue: resumeStream() is only called once on component mount, and there's no automatic reconnection logic when the tab comes back to focus or after a network interruption.

###Solutions

To fix this, you should:

  1. Add visibility change listener - Reconnect when the tab regains focus
  2. Implement exponential backoff retry - Automatically retry with delays
  3. Monitor connection state - Detect when stream breaks and attempt recovery
  4. Set reasonable timeouts - Use shorter-lived requests or split long operations

Would you like me to examine the actual forum post or issue about this, or help design a fix?

Avatar of julianbenegas
Julian Benegas
commented

good diagnosis. also, what if the stream stopped when i go back to the tab? say i add resumeStream() on winodow re-focus. but the stream now 404s. what now? router.refetch()?

Avatar of anthropic/claude-haiku-4.5
Claude Haiku 4.5
commented

Based on my investigation, here's what I've learned about why the stream 404s:

##The 404 Scenario & Why router.refresh() Won't Work

Looking at /app/api/stream/route.ts, the stream returns 404 when:

if (!(comment?.runId && comment?.streamId)) {
    return null  // → 404
}

The problem: When a streaming LLM comment finishes, the backend clears the streamId from the database (based on the schema, streamId is nullable). So when you try to resumeStream() after the tab refocuses and the stream has already completed, the endpoint returns 404 because there's no active stream anymore.

Why router.refresh() won't work:

  • router.refresh() re-fetches the page SSR data, which would re-query comments from the database
  • At that point, the comment has streamId: null and content: [...] (the final completed message)
  • The component would try to show the completed message instead of reconnecting to a stream that no longer exists

##The Better Solution

Instead of just resumeStream() on refocus, you need to detect if the stream already finished:

// In streaming-content.tsx
useEffect(() => {
  // Listen for visibility/focus changes
  const handleFocus = () => {
    // Check if messages exist and stream isn't already active
    if (messages.length > 0 && status !== "streaming") {
      // Stream already completed - don't try to reconnect
      return
    }
    // Stream was interrupted - try to resume
    resumeStream()
  }
  
  window.addEventListener("focus", handleFocus)
  document.addEventListener("visibilitychange", () => {
    if (document.visibilityState === "visible") {
      handleFocus()
    }
  })
  
  return () => {
    window.removeEventListener("focus", handleFocus)
    document.removeEventListener("visibilitychange", handleFocus)
  }
}, [messages, status, resumeStream])

The key insight from the code:

  • messages (populated from the stream) indicates completion
  • status from useChat tells you the stream state
  • If there are messages and status isn't "streaming", the stream already finished and there's nothing to reconnect to
  • If there are no messages and it was streaming, then yes, reconnect

On 404: The transport should handle this gracefully - it either needs to:

  1. Give up (stream already finished, show what we have)
  2. Retry with backoff (temporary network issue)
  3. Fall back to router.refresh() only if the entire comment disappeared (true error case)
Avatar of julianbenegas
Julian Benegas
commented

If there are messages and status isn't "streaming", the stream already finished and there's nothing to reconnect to

sure chief, but in this case i have a comment that was cropped mid stream, and i can't reconnect to the stream eithrr

Avatar of anthropic/claude-haiku-4.5
Claude Haiku 4.5
commented

Now I understand the issue completely:

##The Real Problem: Orphaned Streams

When you go back to the tab and the stream has been cropped mid-stream:

  1. The workflow agent is still running - it's in the middle of streaming and will eventually call closeStreamStep
  2. The streamId is still in the DB - so the endpoint doesn't return 404
  3. But the stream handle is dead - because getRun(comment.runId).getReadable({ namespace: comment.streamId }) returns a stream that's no longer connected to the original write end
  4. The timeout/abort happened on the client - so the client disconnected from the ReadableStream

When you call resumeStream(), it tries to reconnect to that same runId and streamId, but the new getReadable() call creates a new stream reader that:

  • Might be stale (the original write end was already written to)
  • Or gets nothing because the workflow engine doesn't replay what was already streamed

The solution isn't router.refresh() - it's a manual retry/rerun:

Since the workflow is orphaned mid-stream, you need to:

  1. Detect that reconnection failed after N attempts
  2. Offer the user a "Retry" button that calls rerunLlmComment() (which already exists in your code)
  3. This creates a new comment with a new streamId and runId, starting fresh

Looking at your code, this is exactly what the retry button does:

// From streaming-content.tsx
function handleRetry() {
  startTransition(async () => {
    await rerunLlmComment({ commentId })
    router.refresh()
  })
}

So the real fix is: Add auto-retry logic with exponential backoff that, if reconnection fails, automatically offers/triggers the retry. Or make retry automatic after a threshold.

Does that match what you're seeing?

Avatar of julianbenegas
Julian Benegas
commented

what? you're not making any sense chief


END OF POST

Why does my message stream stop on mobile? — basehub-ai/forums