From 726ef48c2ac8db3c7484e2a9219b0e072fd5a55a Mon Sep 17 00:00:00 2001 From: Vignesh Natarajan Date: Thu, 5 Mar 2026 22:00:54 -0800 Subject: [PATCH] fix(tui): accept canonical session-key aliases in chat event routing --- CHANGELOG.md | 1 + src/tui/tui-event-handlers.test.ts | 38 ++++++++++++++++++++++++++++++ src/tui/tui-event-handlers.ts | 26 +++++++++++++++++++- 3 files changed, 64 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fac5111ac0d..cf81a987c57 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ Docs: https://docs.openclaw.ai - Gateway/chat streaming tool-boundary text retention: merge assistant delta segments into per-run chat buffers so pre-tool text is preserved in live chat deltas/finals when providers emit post-tool assistant segments as non-prefix snapshots. (#36957) Thanks @Datyedyeguy. - TUI/model indicator freshness: prevent stale session snapshots from overwriting freshly patched model selection (and reset per-session freshness when switching session keys) so `/model` updates reflect immediately instead of lagging by one or more commands. (#21255) Thanks @kowza. - TUI/final-error rendering fallback: when a chat `final` event has no renderable assistant content but includes envelope `errorMessage`, render the formatted error text instead of collapsing to `"(no output)"`, preserving actionable failure context in-session. (#14687) Thanks @Mquarmoc. +- TUI/session-key alias event matching: treat chat events whose session keys are canonical aliases (for example `agent::main` vs `main`) as the same session while preserving cross-agent isolation, so assistant replies no longer disappear or surface in another terminal window due to strict key-form mismatch. (#33937) Thanks @yjh1412. - OpenAI Codex OAuth/login hardening: fail OAuth completion early when the returned token is missing `api.responses.write`, and allow `openclaw models auth login --provider openai-codex` to use the built-in OAuth path even when no provider plugins are installed. (#36660) Thanks @driesvints. - OpenAI Codex OAuth/scope request parity: augment the OAuth authorize URL with required API scopes (`api.responses.write`, `model.request`, `api.model.read`) before browser handoff so OAuth tokens include runtime model/request permissions expected by OpenAI API calls. (#24720) Thanks @Skippy-Gunboat. - Onboarding/API key input hardening: strip non-Latin1 Unicode artifacts from normalized secret input (while preserving Latin-1 content and internal spaces) so malformed copied API keys cannot trigger HTTP header `ByteString` construction crashes; adds regression coverage for shared normalization and MiniMax auth header usage. (#24496) Thanks @fa6maalassaf. diff --git a/src/tui/tui-event-handlers.test.ts b/src/tui/tui-event-handlers.test.ts index f18b2d34656..d976839d466 100644 --- a/src/tui/tui-event-handlers.test.ts +++ b/src/tui/tui-event-handlers.test.ts @@ -193,6 +193,44 @@ describe("tui-event-handlers: handleAgentEvent", () => { expect(chatLog.startTool).toHaveBeenCalledWith("tc1", "exec", undefined); }); + it("accepts chat events when session key is an alias of the active canonical key", () => { + const { state, chatLog, handleChatEvent } = createHandlersHarness({ + state: { + currentSessionKey: "agent:main:main", + activeChatRunId: null, + }, + }); + + handleChatEvent({ + runId: "run-alias", + sessionKey: "main", + state: "delta", + message: { content: "hello" }, + }); + + expect(state.activeChatRunId).toBe("run-alias"); + expect(chatLog.updateAssistant).toHaveBeenCalledWith("hello", "run-alias"); + }); + + it("does not cross-match canonical session keys from different agents", () => { + const { chatLog, handleChatEvent } = createHandlersHarness({ + state: { + currentAgentId: "alpha", + currentSessionKey: "agent:alpha:main", + activeChatRunId: null, + }, + }); + + handleChatEvent({ + runId: "run-other-agent", + sessionKey: "agent:beta:main", + state: "delta", + message: { content: "should be ignored" }, + }); + + expect(chatLog.updateAssistant).not.toHaveBeenCalled(); + }); + it("clears run mapping when the session changes", () => { const { state, chatLog, tui, handleChatEvent, handleAgentEvent } = createHandlersHarness({ state: { activeChatRunId: null }, diff --git a/src/tui/tui-event-handlers.ts b/src/tui/tui-event-handlers.ts index adf304b8e2d..b46a6653f17 100644 --- a/src/tui/tui-event-handlers.ts +++ b/src/tui/tui-event-handlers.ts @@ -1,3 +1,4 @@ +import { parseAgentSessionKey } from "../sessions/session-key-utils.js"; import { asString, extractTextFromMessage, isCommandMessage } from "./tui-formatters.js"; import { TuiStreamAssembler } from "./tui-stream-assembler.js"; import type { AgentEvent, ChatEvent, TuiStateAccess } from "./tui-types.js"; @@ -146,13 +147,36 @@ export function createEventHandlers(context: EventHandlerContext) { void loadHistory?.(); }; + const isSameSessionKey = (left: string | undefined, right: string | undefined): boolean => { + const normalizedLeft = (left ?? "").trim().toLowerCase(); + const normalizedRight = (right ?? "").trim().toLowerCase(); + if (!normalizedLeft || !normalizedRight) { + return false; + } + if (normalizedLeft === normalizedRight) { + return true; + } + const parsedLeft = parseAgentSessionKey(normalizedLeft); + const parsedRight = parseAgentSessionKey(normalizedRight); + if (parsedLeft && parsedRight) { + return parsedLeft.agentId === parsedRight.agentId && parsedLeft.rest === parsedRight.rest; + } + if (parsedLeft) { + return parsedLeft.rest === normalizedRight; + } + if (parsedRight) { + return normalizedLeft === parsedRight.rest; + } + return false; + }; + const handleChatEvent = (payload: unknown) => { if (!payload || typeof payload !== "object") { return; } const evt = payload as ChatEvent; syncSessionKey(); - if (evt.sessionKey !== state.currentSessionKey) { + if (!isSameSessionKey(evt.sessionKey, state.currentSessionKey)) { return; } if (finalizedRuns.has(evt.runId)) {