From f0b5b61c8e9a1975f3d3eb5911d04fbc3b3a0d8a Mon Sep 17 00:00:00 2001 From: viettranx Date: Wed, 25 Mar 2026 13:40:54 +0700 Subject: [PATCH] fix(chat): isolate WS chat events by channel and session key Add sessionKey to AgentEvent and filter in chat UI so events from Telegram, cron, and other channels don't leak into WS chat page. --- internal/agent/loop.go | 1 + internal/agent/loop_run.go | 1 + internal/agent/loop_types.go | 9 +++++---- ui/web/src/pages/chat/hooks/use-chat-messages.ts | 11 +++++++++++ ui/web/src/types/chat.ts | 2 ++ 5 files changed, 20 insertions(+), 4 deletions(-) diff --git a/internal/agent/loop.go b/internal/agent/loop.go index a334934c..5de90bb1 100644 --- a/internal/agent/loop.go +++ b/internal/agent/loop.go @@ -34,6 +34,7 @@ func (l *Loop) runLoop(ctx context.Context, req RunRequest) (*RunResult, error) event.UserID = req.UserID event.Channel = req.Channel event.ChatID = req.ChatID + event.SessionKey = req.SessionKey event.TenantID = l.tenantID l.emit(event) } diff --git a/internal/agent/loop_run.go b/internal/agent/loop_run.go index 051774f2..03e53756 100644 --- a/internal/agent/loop_run.go +++ b/internal/agent/loop_run.go @@ -29,6 +29,7 @@ func (l *Loop) Run(ctx context.Context, req RunRequest) (*RunResult, error) { event.UserID = req.UserID event.Channel = req.Channel event.ChatID = req.ChatID + event.SessionKey = req.SessionKey event.TenantID = store.TenantIDFromContext(ctx) l.emit(event) } diff --git a/internal/agent/loop_types.go b/internal/agent/loop_types.go index 7ce12bf1..e7765a32 100644 --- a/internal/agent/loop_types.go +++ b/internal/agent/loop_types.go @@ -160,10 +160,11 @@ type AgentEvent struct { TeamTaskID string `json:"teamTaskId,omitempty"` ParentAgentID string `json:"parentAgentId,omitempty"` - // Routing context (helps WS clients filter by user/channel) - UserID string `json:"userId,omitempty"` - Channel string `json:"channel,omitempty"` - ChatID string `json:"chatId,omitempty"` + // Routing context (helps WS clients filter by user/channel/session) + UserID string `json:"userId,omitempty"` + Channel string `json:"channel,omitempty"` + ChatID string `json:"chatId,omitempty"` + SessionKey string `json:"sessionKey,omitempty"` // TenantID scopes this event to a specific tenant for filtering (not serialized). TenantID uuid.UUID `json:"-"` diff --git a/ui/web/src/pages/chat/hooks/use-chat-messages.ts b/ui/web/src/pages/chat/hooks/use-chat-messages.ts index e86c1e1b..26ff3157 100644 --- a/ui/web/src/pages/chat/hooks/use-chat-messages.ts +++ b/ui/web/src/pages/chat/hooks/use-chat-messages.ts @@ -34,6 +34,8 @@ export function useChatMessages(sessionKey: string, agentId: string) { const toolStreamRef = useRef([]); const agentIdRef = useRef(agentId); agentIdRef.current = agentId; + const sessionKeyRef = useRef(sessionKey); + sessionKeyRef.current = sessionKey; const activityRef = useRef(null); const blockRepliesRef = useRef([]); const rafPendingRef = useRef(false); @@ -155,6 +157,15 @@ export function useChatMessages(sessionKey: string, agentId: string) { const event = payload as AgentEventPayload; if (!event) return; + // Only process events from WS channel targeting the active session. + // Delegations/announces (runKind set) are allowed regardless of channel. + if (event.channel && event.channel !== "ws" && !event.runKind) { + return; + } + if (event.sessionKey && event.sessionKey !== sessionKeyRef.current) { + return; + } + // Capture run.started when we are expecting a run for this agent, // OR when an announce run starts (leader summarising team results). if (event.type === "run.started" && event.agentId === agentIdRef.current) { diff --git a/ui/web/src/types/chat.ts b/ui/web/src/types/chat.ts index c720e656..fb82abbb 100644 --- a/ui/web/src/types/chat.ts +++ b/ui/web/src/types/chat.ts @@ -52,6 +52,8 @@ export interface AgentEventPayload { agentId: string; runId: string; runKind?: string; // "delegation" | "announce" — omitted for user-initiated runs + channel?: string; // "ws", "telegram", "cron", etc. + sessionKey?: string; // session this event belongs to payload?: { content?: string; name?: string;