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;