diff --git a/apps/desktop/src/app/session/hooks/use-message-stream.ts b/apps/desktop/src/app/session/hooks/use-message-stream.ts index fe89c8b5055..382a2cd7f37 100644 --- a/apps/desktop/src/app/session/hooks/use-message-stream.ts +++ b/apps/desktop/src/app/session/hooks/use-message-stream.ts @@ -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 diff --git a/apps/desktop/src/lib/gateway-events.test.ts b/apps/desktop/src/lib/gateway-events.test.ts new file mode 100644 index 00000000000..ad118beb680 --- /dev/null +++ b/apps/desktop/src/lib/gateway-events.test.ts @@ -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) + }) +}) diff --git a/apps/desktop/src/lib/gateway-events.ts b/apps/desktop/src/lib/gateway-events.ts index fe6b1a0f78b..0da4a8683cc 100644 --- a/apps/desktop/src/lib/gateway-events.ts +++ b/apps/desktop/src/lib/gateway-events.ts @@ -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 { return payload && typeof payload === 'object' ? (payload as Record) : {} } +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