Skip to content

tui: ignore foreign-session events until the session is followed #37792

Open

Description

@kitlangton

Summary

The V2 TUI projects process-global events into local reactive state without first checking whether the client follows that location, Session, or Session family. Activity in an unrelated Session can therefore allocate transcript indexes, mutate stores, and trigger follow-up API reads in every open TUI.

This is a separate client-side failure boundary from the server subscription fan-out in #36443. Server-side scoping should remove most foreign traffic, but the TUI still needs an explicit interest rule so a protocol regression, reconnect, or intentionally global channel cannot silently repopulate unrelated state.

Environment

  • opencode version: 0.0.0-next-15834
  • OS: Darwin 25.5.0, arm64
  • Terminal: WarpTerminal, TERM=xterm-256color, COLORTERM=truecolor
  • Shell: /bin/zsh
  • Install/channel: global Bun install, next
  • Active plugins: three local user plugins; names and paths omitted because they are unrelated to TUI event projection

Reproduction

  1. Keep two V2 TUIs open on unrelated Sessions, ideally in unrelated locations.
  2. Run a tool-heavy turn in Session A while Session B remains idle.
  3. Observe the process-global /api/event stream in both clients.
  4. Inspect Session B's DataProvider state after events for Session A arrive.

In a current production capture with nine TUIs and eight running Sessions, unrelated clients received identical stream-byte deltas. Source inspection at the exact running revision shows that each client dispatches all of those events and that many Session event handlers materialize state by event-owned Session ID.

Expected Behavior

  • A TUI should project Session events only for Sessions it explicitly follows.
  • Following should include the active Session, hydrated Sessions the UI intentionally retains, and explicitly followed child Sessions needed for cross-location subagent UX.
  • An event for an unfollowed Session should not allocate transcript, pending-input, activity, permission, form, or message-index state.
  • When a Session becomes followed later, its authoritative state should hydrate from the Session APIs rather than relying on unrelated historical live events.
  • Location invalidations should refresh only locations already loaded or explicitly followed by the client.

Actual Behavior

ClientProvider opens unscoped api.event.subscribe() and emits every decoded event to local listeners.

DataProvider forwards every non-server.connected event to handleEvent. There is no location or Session-interest check at that boundary.

The message helper always creates both structures for an event-owned Session ID:

function update(sessionID, fn) {
  setStore("session", "message", produce((draft) => {
    fn((draft[sessionID] ??= []), index(sessionID))
  }))
}

See data.tsx:170-178. index(sessionID) also creates and retains a Map for a previously unseen Session at data.tsx:218-224.

Many handlers call this helper without requiring loaded Session info or an existing transcript, including:

  • session.agent.selected
  • session.input.admitted
  • session.instructions.updated
  • session.synthetic
  • shell start/end
  • step start/end/failure
  • text, reasoning, and tool streaming events
  • execution and compaction events

The complete switch begins at data.tsx:296. Other event types create foreign active, pending, input, permission, or form entries, or invalidate and refetch data for an event-owned location.

Additional Context

Why this matters independently of network fan-out

The production incident showed older TUIs at sustained 13-59% CPU, with an earlier 122% spike, and 413 MiB to 2.1 GiB of reported resident memory per client. This issue does not claim that foreign projection alone retains all of that memory. It establishes a deterministic multiplier: every globally delivered event enters every client's reactive graph, and some events create durable local keys for Sessions the client never opened.

Server-side scoping in #36443 is the primary transport fix. A client-side interest gate is still needed as defense in depth and can reduce projection work before the protocol changes land.

Interest semantics to define

The gate should be based on an explicit followed set, not only the currently visible route. V2 intentionally supports child Sessions and cross-location work, so a child followed by an active parent may remain eligible even when its own route is not visible. Conversely, merely receiving a foreign session.created event should not make that Session followed or force an immediate full sync in every TUI.

Acceptance criteria

  • A client hydrated only with Session B receives representative Session A input, step, text, reasoning, tool, execution, permission, form, and compaction events without creating any Session A store or message-index entry.
  • The same events still project for explicitly followed Session A.
  • A followed child Session can project while its parent route is active, including when the child belongs to another location.
  • Transitioning a Session from unfollowed to followed hydrates a complete authoritative snapshot and then applies live events without gaps or duplicates.
  • Foreign location update events do not trigger model, provider, MCP, command, agent, skill, or integration requests unless that location is loaded or followed.
  • Reconnect and duplicate-event tests preserve the same rules.
  • A regression test asserts both state and side effects: no foreign keys, no foreign message index, and no follow-up HTTP request.

Related

No Session IDs, titles, directories, prompts, or event payload contents are included in this report.

Activity

  1. added
    bugSomething isn't working
    perfIndicates a performance issue or need for optimization
    on Jul 19, 2026
  2. self-assigned this
    on Jul 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

2.0bugSomething isn't workingperfIndicates a performance issue or need for optimizationtui

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions