mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-30 05:54:43 +00:00
fix: isolate TUI /new sessions per client
Landed from contributor PR #39238 by @widingmarcus-cyber. Co-authored-by: Marcus Widing <widing.marcus@gmail.com>
This commit is contained in:
@@ -68,6 +68,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/openai-completions stream timeout hardening: ensure runtime undici global dispatchers use extended streaming body/header timeouts (including env-proxy dispatcher mode) before embedded runs, reducing forced mid-stream `terminated` failures on long generations; adds regression coverage for dispatcher selection and idempotent reconfiguration. (#9708) Thanks @scottchguard.
|
||||
- Agents/fallback cooldown probe execution: thread explicit rate-limit cooldown probe intent from model fallback into embedded runner auth-profile selection so same-provider fallback attempts can actually run when all profiles are cooldowned for `rate_limit` (instead of failing pre-run as `No available auth profile`), while preserving default cooldown skip behavior and adding regression tests at both fallback and runner layers. (#13623) Thanks @asfura.
|
||||
- Cron/OpenAI Codex OAuth refresh hardening: when `openai-codex` token refresh fails specifically on account-id extraction, reuse the cached access token instead of failing the run immediately, with regression coverage to keep non-Codex and unrelated refresh failures unchanged. (#36604) Thanks @laulopezreal.
|
||||
- TUI/session isolation for `/new`: make `/new` allocate a unique `tui-<uuid>` session key instead of resetting the shared agent session, so multiple TUI clients on the same agent stop receiving each other’s replies; also sanitize `/new` and `/reset` failure text before rendering in-terminal. Landed from contributor PR #39238 by @widingmarcus-cyber. Thanks @widingmarcus-cyber.
|
||||
- Cron/file permission hardening: enforce owner-only (`0600`) cron store/backup/run-log files and harden cron store + run-log directories to `0700`, including pre-existing directories from older installs. (#36078) Thanks @aerelune.
|
||||
- Gateway/remote WS break-glass hostname support: honor `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1` for `ws://` hostname URLs (not only private IP literals) across onboarding validation and runtime gateway connection checks, while still rejecting public IP literals and non-unicast IPv6 endpoints. (#36930) Thanks @manju-rn.
|
||||
- Routing/binding lookup scalability: pre-index route bindings by channel/account and avoid full binding-list rescans on channel-account cache rollover, preventing multi-second `resolveAgentRoute` stalls in large binding configurations. (#36915) Thanks @songchenghao.
|
||||
|
||||
@@ -7,12 +7,14 @@ type SetActivityStatusMock = ReturnType<typeof vi.fn> & ((text: string) => void)
|
||||
function createHarness(params?: {
|
||||
sendChat?: ReturnType<typeof vi.fn>;
|
||||
resetSession?: ReturnType<typeof vi.fn>;
|
||||
setSession?: ReturnType<typeof vi.fn>;
|
||||
loadHistory?: LoadHistoryMock;
|
||||
setActivityStatus?: SetActivityStatusMock;
|
||||
isConnected?: boolean;
|
||||
}) {
|
||||
const sendChat = params?.sendChat ?? vi.fn().mockResolvedValue({ runId: "r1" });
|
||||
const resetSession = params?.resetSession ?? vi.fn().mockResolvedValue({ ok: true });
|
||||
const setSession = params?.setSession ?? vi.fn().mockResolvedValue(undefined);
|
||||
const addUser = vi.fn();
|
||||
const addSystem = vi.fn();
|
||||
const requestRender = vi.fn();
|
||||
@@ -36,7 +38,7 @@ function createHarness(params?: {
|
||||
closeOverlay: vi.fn(),
|
||||
refreshSessionInfo: vi.fn(),
|
||||
loadHistory,
|
||||
setSession: vi.fn(),
|
||||
setSession,
|
||||
refreshAgents: vi.fn(),
|
||||
abortActive: vi.fn(),
|
||||
setActivityStatus,
|
||||
@@ -51,6 +53,7 @@ function createHarness(params?: {
|
||||
handleCommand,
|
||||
sendChat,
|
||||
resetSession,
|
||||
setSession,
|
||||
addUser,
|
||||
addSystem,
|
||||
requestRender,
|
||||
@@ -104,16 +107,26 @@ describe("tui command handlers", () => {
|
||||
expect(requestRender).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("passes reset reason when handling /new and /reset", async () => {
|
||||
it("creates unique session for /new and resets shared session for /reset", async () => {
|
||||
const loadHistory = vi.fn().mockResolvedValue(undefined);
|
||||
const { handleCommand, resetSession } = createHarness({ loadHistory });
|
||||
const setSessionMock = vi.fn().mockResolvedValue(undefined);
|
||||
const { handleCommand, resetSession } = createHarness({
|
||||
loadHistory,
|
||||
setSession: setSessionMock,
|
||||
});
|
||||
|
||||
await handleCommand("/new");
|
||||
await handleCommand("/reset");
|
||||
|
||||
expect(resetSession).toHaveBeenNthCalledWith(1, "agent:main:main", "new");
|
||||
expect(resetSession).toHaveBeenNthCalledWith(2, "agent:main:main", "reset");
|
||||
expect(loadHistory).toHaveBeenCalledTimes(2);
|
||||
// /new creates a unique session key (isolates TUI client) (#39217)
|
||||
expect(setSessionMock).toHaveBeenCalledTimes(1);
|
||||
expect(setSessionMock).toHaveBeenCalledWith(
|
||||
expect.stringMatching(/^tui-[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/),
|
||||
);
|
||||
// /reset still resets the shared session
|
||||
expect(resetSession).toHaveBeenCalledTimes(1);
|
||||
expect(resetSession).toHaveBeenCalledWith("agent:main:main", "reset");
|
||||
expect(loadHistory).toHaveBeenCalledTimes(1); // /reset calls loadHistory directly; /new does so indirectly via setSession
|
||||
});
|
||||
|
||||
it("reports send failures and marks activity status as error", async () => {
|
||||
@@ -129,6 +142,21 @@ describe("tui command handlers", () => {
|
||||
expect(setActivityStatus).toHaveBeenLastCalledWith("error");
|
||||
});
|
||||
|
||||
it("sanitizes control sequences in /new and /reset failures", async () => {
|
||||
const setSession = vi.fn().mockRejectedValue(new Error("\u001b[31mboom\u001b[0m"));
|
||||
const resetSession = vi.fn().mockRejectedValue(new Error("\u001b[31mboom\u001b[0m"));
|
||||
const { handleCommand, addSystem } = createHarness({
|
||||
setSession,
|
||||
resetSession,
|
||||
});
|
||||
|
||||
await handleCommand("/new");
|
||||
await handleCommand("/reset");
|
||||
|
||||
expect(addSystem).toHaveBeenNthCalledWith(1, "new session failed: Error: boom");
|
||||
expect(addSystem).toHaveBeenNthCalledWith(2, "reset failed: Error: boom");
|
||||
});
|
||||
|
||||
it("reports disconnected status and skips gateway send when offline", async () => {
|
||||
const { handleCommand, sendChat, addUser, addSystem, setActivityStatus } = createHarness({
|
||||
isConnected: false,
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
createSettingsList,
|
||||
} from "./components/selectors.js";
|
||||
import type { GatewayChatClient } from "./gateway-chat.js";
|
||||
import { sanitizeRenderableText } from "./tui-formatters.js";
|
||||
import { formatStatusSummary } from "./tui-status-summary.js";
|
||||
import type {
|
||||
AgentSummary,
|
||||
@@ -423,6 +424,23 @@ export function createCommandHandlers(context: CommandHandlerContext) {
|
||||
}
|
||||
break;
|
||||
case "new":
|
||||
try {
|
||||
// Clear token counts immediately to avoid stale display (#1523)
|
||||
state.sessionInfo.inputTokens = null;
|
||||
state.sessionInfo.outputTokens = null;
|
||||
state.sessionInfo.totalTokens = null;
|
||||
tui.requestRender();
|
||||
|
||||
// Generate unique session key to isolate this TUI client (#39217)
|
||||
// This ensures /new creates a fresh session that doesn't broadcast
|
||||
// to other connected TUI clients sharing the original session key.
|
||||
const uniqueKey = `tui-${randomUUID()}`;
|
||||
await setSession(uniqueKey);
|
||||
chatLog.addSystem(`new session: ${uniqueKey}`);
|
||||
} catch (err) {
|
||||
chatLog.addSystem(`new session failed: ${sanitizeRenderableText(String(err))}`);
|
||||
}
|
||||
break;
|
||||
case "reset":
|
||||
try {
|
||||
// Clear token counts immediately to avoid stale display (#1523)
|
||||
@@ -435,7 +453,7 @@ export function createCommandHandlers(context: CommandHandlerContext) {
|
||||
chatLog.addSystem(`session ${state.currentSessionKey} reset`);
|
||||
await loadHistory();
|
||||
} catch (err) {
|
||||
chatLog.addSystem(`reset failed: ${String(err)}`);
|
||||
chatLog.addSystem(`reset failed: ${sanitizeRenderableText(String(err))}`);
|
||||
}
|
||||
break;
|
||||
case "abort":
|
||||
|
||||
Reference in New Issue
Block a user