Skip to content

fix(client): ignore foreign-session events until session is followed - #56375

Open
dsadawq3 wants to merge 1 commit into
anomalyco:devfrom
dsadawq3:fix/ignore-foreign-session-events
Open

dsadawq3 wants to merge 1 commit into
anomalyco:devfrom
dsadawq3:fix/ignore-foreign-session-events

Conversation

@dsadawq3

@dsadawq3 dsadawq3 commented Oct 17, 2026 •

Copy link
Copy Markdown

Issue for this PR

Closes #37792

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

The process-global SSE event stream (/api/event) broadcasts live events across all sessions and workspaces connected to the server.

In packages/client/src/solid/data.ts, incoming session events were unconditionally projecting into the reactive store. Even for sessions the client never opened, calling message.update allocated empty message arrays in store.session.message and persistent entries in messageIndex. Additionally, foreign session.created, session.execution.*, and foreign location updates triggered redundant background HTTP sync calls (result.session.sync, result.location.*.sync), resulting in unnecessary CPU and memory allocation in multi-session setups.

This PR:

  1. Adds an optional isFollowed?: (sessionID: string) => boolean predicate to CreateDataInput in @opencode/client/solid.
  2. Adds an isFollowed(sessionID) guard in createData that checks config.isFollowed, family root resolution (resolveRoot), and existing loaded transcripts in store.session.message. If config.isFollowed is omitted, it defaults to accepting all sessions for backwards compatibility.
  3. Guards session lifecycle, transcript mutations, execution status, permission/form prompts, and foreign location events so unfollowed sessions do not allocate store keys or fire redundant HTTP synchronization calls.
  4. Connects isFollowed in packages/tui/src/context/data.tsx using useRoute() to check against the active session.
  5. Adds a regression unit test in packages/client/test/solid-data.test.ts verifying foreign events do not create store allocations or trigger sync.

How did you verify your code works?

  • Added ignores foreign session events until the session is followed in packages/client/test/solid-data.test.ts.
  • Ran bun test across packages/client (226 tests passed, 0 failures).
  • Ran tsgo --noEmit in packages/client and tsgo -b in packages/tui (both clean, 0 errors).

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Added by the opencode bot (AI-generated).

Related: #36443, #36441, #53203

Public API: adds optional isFollowed to createData input in @opencode/client/solid (backwards compatible)

@github-actions

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on this. The new test passes, so foreign events no longer allocate store state, but the filtering also drops events the TUI depends on, so this needs changes before it can land.

How I checked: ran solid-data.test.ts (31 pass) and the typecheck scripts in packages/client and packages/tui (clean). I also wrote a scratch test against createData on the base and on this branch: a followed parent session spawns a subagent child, then the child asks for permission and starts executing. On the base, the child joins the parent's family, its permission is stored and it shows as running. With this PR, all of that is dropped. The child's session.created is skipped, so resolveRoot can never link it to the followed parent. In the TUI, subagent permission and form prompts would not appear while you watch the parent session, so the turn can hang. I didn't drive the TUI end to end.

Public API: adds an optional isFollowed?: (sessionID: string) => boolean to CreateDataInput in @opencode/client/solid. This is backwards compatible: omitting it keeps the current behaviour.

Main concerns, details inline:

  • Subagent children of the followed session are never followed (blocking).
  • "Followed" means only the session on screen, but the TUI shows state for other sessions too: background-tab busy/attention/unread, the open menu's "awaiting input" and running indicators (#53435), and the session list. Execution, permission and form events for those sessions are now dropped.
  • Updates to session info that is already loaded (rename, agent/model, moved, revert) are dropped, so titles and other details go stale. Sessions created by other clients are no longer synced into the list. Clearing events (permission.replied, form.replied/cancelled) are dropped too, so a stale prompt can remain after the session becomes followed later.

A safer direction might be to skip only allocation for sessions the store doesn't know yet, and keep applying events for sessions it already holds or that belong to a known family. Alternatively, rely on server-side interest scoping (#36443).

Related: #36443, #36441, #53203

let activeUpdates: Map<string, DataSessionStatus | undefined> | undefined
const pendingUpdates = new Map<string, Map<string, SessionInboxInfo | SessionInbox.Delivery | undefined>>()

function isFollowed(sessionID: string): boolean {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This decides whether to follow a session from config.isFollowed, from resolveRoot and from loaded transcripts. A new subagent child has no info loaded yet, so resolveRoot(child) === child, and its session.created is now skipped (line 688), so its info is never loaded. Every later event for the child is dropped, including permission.asked. I reproduced this with a test against createData: on this branch the child's permission and running status are lost, while on the base they are stored. In the TUI, a subagent's permission prompt wouldn't appear while the parent session is on screen.

)
return
}
case "session.renamed": {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This guard (and those on agent.selected, model.selected, moved and revert.*) drops updates even when store.session.info[sessionID] is already loaded. Sessions shown in tabs, the session list or the open menu then keep stale titles and other details. These handlers already do nothing for unknown sessions, so they don't need the follow check.

setStore("project", "info", event.data.id, reconcile(event.data))
return
case "session.created":
case "session.created": {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skipping the sync for sessions this client didn't create also stops new sessions from other clients appearing in the session list, which reads data.session from the store. And it is what cuts subagent children off from their followed parent.

@@ -1203,17 +1242,20 @@
if (event.data.inputID) compacting.get(event.data.sessionID)?.observed.add(event.data.inputID)
return
case "permission.asked":

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

session-tabs.tsx (attention), dialog-open.tsx (awaiting input, #53435) and the session list (running indicator) read permissions, forms and status for sessions other than the one on screen. With the TUI predicate, these events are dropped for any session not opened in this process. The matching permission.replied/form.replied guards also mean a prompt resolved elsewhere stays in the store if it was loaded earlier through a permission.sync.

event: client.event,
connection: client.connection,
directory: props.directory,
isFollowed: (sessionID) => (route ? route.data.type === "session" && route.data.sessionID === sessionID : true),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This treats only the session currently on screen as followed. Its subagents, background tabs and sessions listed in the open menu or session list are not covered. The predicate needs to include at least the route session's family and the open tabs, or the filtering in createData needs to be limited to allocation for sessions it doesn't know yet.

@dsadawq3
dsadawq3 force-pushed the fix/ignore-foreign-session-events branch from 53dba41 to 75df496 Compare October 17, 2026 11:04
@dsadawq3

Copy link
Copy Markdown
Author

Updated and pushed commit 75df496 to address the review feedback:

  1. Restored unconditional session.created sync: session.created now unconditionally invalidates and syncs SessionInfo. New sessions from other clients appear in the session list, and subagent children immediately load their parentID and register in store.session.family[root], allowing subagent family checks and
    esolveRoot(child) to work seamlessly.
  2. Metadata updates for known sessions: Removed !isFollowed checks from �gent.selected, model.selected,
    enamed, moved, �iewed, and
    evert.*. These handlers already check if (store.session.info[id]), safely updating open tabs, the session list, and dialogs without allocating state for unknown sessions.
  3. Running indicators & attention badges: Updated setSessionActive, permission.asked, and orm.created to allow updates for any session already present in store.session.info (if (!store.session.info[id] && !isFollowed(id)) return/break). Background tabs and sessions in the session list retain active execution badges and permission prompts.
  4. Resolution cleanup: Removed !isFollowed guards on permission.replied, orm.replied, and orm.cancelled so resolved prompts are always cleaned up even if resolved elsewhere.
  5. Focused transcript isolation: The follow guard is now strictly limited to transcript allocation (store.session.message and messageIndex via message.update, session.step.started, session.synthetic, session.shell., session.instructions.updated, session.inbox.enqueued, session.compaction.).
  6. Regression test: Updated solid-data.test.ts to verify foreign session transcript events do not allocate messages, while subagent child sessions correctly sync parentID, resolve to the followed root, and retain permission requests.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant