fix(desktop): require session ids for scoped gateway events (#42178)

* fix(desktop): require session ids for scoped gateway events

Drop unscoped stream, tool, and subagent events in the desktop renderer so async activity cannot attach to whichever chat is currently focused.

* fix(desktop): preserve unscoped session info events

Keep session.info out of the scoped-event drop list so global desktop runtime broadcasts still initialize UI state before a session is active.
This commit is contained in:
brooklyn! 2026-06-08 11:50:48 -05:00 committed by GitHub
parent a77efada5f
commit de80d28f38
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 52 additions and 0 deletions

View file

@ -14,6 +14,7 @@ import {
upsertToolPart
} from '@/lib/chat-messages'
import { coerceGatewayText, coerceThinkingText, normalizePersonalityValue } from '@/lib/chat-runtime'
import { gatewayEventRequiresSessionId } from '@/lib/gateway-events'
import { triggerHaptic } from '@/lib/haptics'
import { isProviderSetupErrorMessage } from '@/lib/provider-setup-errors'
import { setClarifyRequest } from '@/store/clarify'
@ -613,6 +614,9 @@ export function useMessageStream({
(event: RpcEvent) => {
const payload = event.payload as GatewayEventPayload | undefined
const explicitSid = event.session_id || ''
if (!explicitSid && gatewayEventRequiresSessionId(event.type)) {
return
}
const sessionId = explicitSid || activeSessionIdRef.current
const isActiveEvent = !!sessionId && sessionId === activeSessionIdRef.current

View file

@ -0,0 +1,19 @@
import { describe, expect, it } from 'vitest'
import { gatewayEventRequiresSessionId } from './gateway-events'
describe('gateway event routing', () => {
it('requires explicit session ids for async session-scoped events', () => {
expect(gatewayEventRequiresSessionId('message.delta')).toBe(true)
expect(gatewayEventRequiresSessionId('tool.start')).toBe(true)
expect(gatewayEventRequiresSessionId('subagent.progress')).toBe(true)
expect(gatewayEventRequiresSessionId('approval.request')).toBe(true)
})
it('allows global events to remain unscoped', () => {
expect(gatewayEventRequiresSessionId('gateway.ready')).toBe(false)
expect(gatewayEventRequiresSessionId('preview.restart.progress')).toBe(false)
expect(gatewayEventRequiresSessionId('session.info')).toBe(false)
expect(gatewayEventRequiresSessionId(undefined)).toBe(false)
})
})

View file

@ -7,10 +7,39 @@ interface RpcEventLike {
type?: string
}
const SESSION_SCOPED_EVENT_TYPES = new Set([
'approval.request',
'clarify.request',
'error',
'message.complete',
'message.delta',
'message.start',
'reasoning.available',
'reasoning.delta',
'secret.request',
'status.update',
'subagent.complete',
'subagent.progress',
'subagent.spawn_requested',
'subagent.start',
'subagent.thinking',
'subagent.tool',
'sudo.request',
'thinking.delta'
])
function asRecord(payload: unknown): Record<string, unknown> {
return payload && typeof payload === 'object' ? (payload as Record<string, unknown>) : {}
}
export function gatewayEventRequiresSessionId(eventType: string | undefined): boolean {
if (!eventType) {
return false
}
return SESSION_SCOPED_EVENT_TYPES.has(eventType) || eventType.startsWith('tool.')
}
export function gatewayEventCompletedFileDiff(event: RpcEventLike): boolean {
if (event.type !== 'tool.complete') {
return false