Skip to content

Bug: Stream data gets GC'd while user is still on the session page #379

@Sshrimp

Description

@Sshrimp

Bug: Stream data gets GC'd while user is still on the session page

Description

When a stream completes (or is stopped/errored) in a chat session, the scheduleGC function sets a 50-minute timer to delete the stream from the in-memory Map. However, this happens regardless of whether the user is still viewing the session page.

As a result, if the user stays on the same session page after a stream finishes and then tries to interact with it (e.g., re-subscribing via subscribe()), the stream data may have already been garbage-collected, causing the UI to lose the cached streaming state.

Root Cause

In src/lib/stream-session-manager.ts, the scheduleGC function only checks two conditions before deleting a stream entry:

  1. The stream reference still matches the one stored in the map (current === stream)
  2. The stream is no longer in 'active' phase

It does not check whether the session is the one the user is currently viewing.

// src/lib/stream-session-manager.ts
function scheduleGC(stream: ActiveStream) {
  if (stream.gcTimer) clearTimeout(stream.gcTimer);
  stream.gcTimer = setTimeout(() => {
    const map = getStreamsMap();
    const current = map.get(stream.sessionId);
    if (current === stream && current.snapshot.phase !== 'active') {
      map.delete(stream.sessionId); // ← deletes even while user is on this page
    }
  }, GC_DELAY_MS);
}

Expected Behavior

The stream entry for the currently active session should not be deleted by GC while the user is still on that session page. GC should only clean up the stream after the user navigates away from the page (i.e., the session becomes truly inactive).

Proposed Fix

Introduce a module-level activeSessionId variable and three helper functions:

  • setActiveSessionId(id: string) — called on page mount
  • getActiveSessionId(): string | null — getter
  • clearActiveSessionId() — called on page unmount

Then update scheduleGC to skip deletion when stream.sessionId === activeSessionId.

A useEffect in src/app/chat/[id]/page.tsx calls setActiveSessionId(id) on mount and clearActiveSessionId() on unmount:

useEffect(() => {
  setActiveSessionId(id);
  return () => {
    clearActiveSessionId();
  };
}, [id]);

Files Involved

File Change
src/lib/stream-session-manager.ts Add activeSessionId state + getter/setter, update scheduleGC
src/app/chat/[id]/page.tsx Call setActiveSessionId/clearActiveSessionId on mount/unmount

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions