Skip to content

fix(tui): isolate /new sessions to prevent cross-client broadcast#39238

Closed
widingmarcus-cyber wants to merge 4 commits intoopenclaw:mainfrom
widingmarcus-cyber:fix/tui-new-session-broadcast-39217
Closed

fix(tui): isolate /new sessions to prevent cross-client broadcast#39238
widingmarcus-cyber wants to merge 4 commits intoopenclaw:mainfrom
widingmarcus-cyber:fix/tui-new-session-broadcast-39217

Conversation

@widingmarcus-cyber
Copy link
Copy Markdown
Contributor

Summary

  • Problem: When multiple TUI clients connect to the same agent and one calls /new, replies are broadcast to all connected clients because they share the same session key.
  • Root cause: Both /new and /reset were using client.resetSession() which resets but doesn't change the session key, leaving all TUIs subscribed to the same session.
  • Fix: Differentiate /new from /reset:
    • /new: Creates a unique session key (tui-{uuid}) to isolate the client
    • /reset: Resets the shared session (existing behavior preserved)

Change Type

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • Security hardening
  • Chore/infra

Scope

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

User-visible / Behavior Changes

  • /new now creates an isolated session per TUI client instead of resetting the shared session
  • /reset retains existing behavior (resets shared session, messages still broadcast)
  • TUI clients using /new will no longer see each other's messages

Security Impact

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Repro + Verification

Steps

  1. Open TUI window A: openclaw tui, then /agent main
  2. Open TUI window B: openclaw tui, then /agent main
  3. In window A, type /new
  4. Send a message in window A
  5. Before fix: Reply appears in both windows
  6. After fix: Reply only appears in window A

Evidence

  • Updated test: creates unique session for /new and resets shared session for /reset
  • All 5 tests pass

Human Verification

  • Verified: Test harness updated to support setSession mock
  • Edge cases: /reset still works as before (shared session reset)
  • What I did not verify: Live multi-terminal TUI testing

Compatibility / Migration

  • Backward compatible? Yes (only /new behavior changes; /reset unchanged)
  • Config/env changes? No
  • Migration needed? No

Failure Recovery

  • How to revert: Revert PR commit
  • Known bad symptoms: /new failing to create session (test catches this)

…enclaw#39217)

When multiple TUI clients connect to the same agent and one calls /new,
replies were being broadcast to all connected clients because they
shared the same session key.

This fix differentiates /new from /reset:
- /new: Creates a unique session key (tui-{uuid}) to isolate the client
- /reset: Resets the shared session (existing behavior preserved)

This ensures each TUI client using /new gets its own isolated session
context, preventing unintended message sharing between terminals.
@widingmarcus-cyber
Copy link
Copy Markdown
Contributor Author

@greptile-apps please review

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 7, 2026

Greptile Summary

This PR correctly fixes the cross-client broadcast bug by differentiating /new (creates an isolated tui-{uuid} session via setSession) from /reset (retains shared-session reset via resetSession). The implementation properly addresses all previously flagged concerns: the full UUID is used (no entropy reduction), the chat log message uses the locally-scoped uniqueKey rather than a potentially stale state.currentSessionKey, and the token-clearing block was added to /new to match the /reset handler.

The only remaining issue is an inconsistency in error handling: the /new error path applies sanitizeRenderableText to error strings while the /reset error path does not. These error paths should be consistent in their sanitization approach for robustness.

Confidence Score: 4/5

  • Safe to merge; the fix is logically correct and targeted with no regressions to /reset behavior.
  • All previously flagged issues (truncated UUID, stale session key in log, stale token counts) have been addressed correctly. The remaining issue is a minor stylistic inconsistency in error sanitization between similar error paths—worth fixing for consistency but does not affect functional correctness or security.
  • src/tui/tui-command-handlers.ts: Error sanitization should be applied consistently to /new and /reset error paths.

Comments Outside Diff (1)

  1. src/tui/tui-command-handlers.ts, line 441-456 (link)

    Inconsistent error sanitization between /new and /reset

    The /new error handler wraps the error in sanitizeRenderableText(...), but the /reset handler directly below it does not:

    // /new (line 441)
    chatLog.addSystem(`new session failed: ${sanitizeRenderableText(String(err))}`);
    
    // /reset (line 456)
    chatLog.addSystem(`reset failed: ${String(err)}`);

    If sanitization is needed for error messages rendered in the TUI, the same treatment should be applied consistently to all error paths. Either apply sanitizeRenderableText to the /reset error too, or remove it from /new if it's not needed.

Last reviewed commit: c8210cb

// /reset still resets the shared session
expect(resetSession).toHaveBeenCalledTimes(1);
expect(resetSession).toHaveBeenCalledWith("agent:main:main", "reset");
expect(loadHistory).toHaveBeenCalledTimes(1); // Only /reset loads history
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misleading test comment about loadHistory

The comment says "Only /reset loads history", but in production setSession (called by /new) internally calls loadHistory() at the end of its implementation (tui-session-actions.ts:367). The assertion is correct for the mocked harness only — the comment should clarify that:

Suggested change
expect(loadHistory).toHaveBeenCalledTimes(1); // Only /reset loads history
expect(loadHistory).toHaveBeenCalledTimes(1); // /new loads history via setSession (mocked here); /reset loads it directly
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/tui/tui-command-handlers.test.ts
Line: 127

Comment:
**Misleading test comment about `loadHistory`**

The comment says "Only `/reset` loads history", but in production `setSession` (called by `/new`) internally calls `loadHistory()` at the end of its implementation (`tui-session-actions.ts:367`). The assertion is correct for the mocked harness only — the comment should clarify that:

```suggestion
    expect(loadHistory).toHaveBeenCalledTimes(1); // /new loads history via setSession (mocked here); /reset loads it directly
```

How can I resolve this? If you propose a fix, please make it concise.

// 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().slice(0, 8)}`;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Truncated UUID reduces session key entropy

.slice(0, 8) keeps only the first segment of the UUID, giving 32 bits of entropy for the generated session key. The whole point of this change is to guarantee per-client isolation — a key collision between two clients would silently re-share a session and reintroduce the broadcast bug. Consider dropping the .slice(0, 8) and using the full UUID to eliminate the risk with no added cost.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/tui/tui-command-handlers.ts
Line: 430

Comment:
**Truncated UUID reduces session key entropy**

`.slice(0, 8)` keeps only the first segment of the UUID, giving 32 bits of entropy for the generated session key. The whole point of this change is to guarantee per-client isolation — a key collision between two clients would silently re-share a session and reintroduce the broadcast bug. Consider dropping the `.slice(0, 8)` and using the full UUID to eliminate the risk with no added cost.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c7ddd8df0e

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +430 to +431
const uniqueKey = `tui-${randomUUID().slice(0, 8)}`;
await setSession(uniqueKey);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reset local session metadata before switching on /new

/new now only calls setSession(uniqueKey), but setSession keeps most state.sessionInfo fields until sessions.list returns an entry; for a brand-new key there is no entry, so applySessionInfo leaves prior model/totalTokens values in place (src/tui/tui-session-actions.ts:164-185 and src/tui/tui-session-actions.ts:241-244). This makes the footer show stale settings immediately after /new and can mislead users about the model/token state of the new conversation.

Useful? React with 👍 / 👎.

Addresses Greptile review feedback:
- Use full UUID instead of truncated 8-char hex to eliminate collision risk
- Clarify test comment about loadHistory behavior
@widingmarcus-cyber
Copy link
Copy Markdown
Contributor Author

@greptile-apps please review

Addressed feedback:

  • Full UUID instead of truncated 8-char hex (eliminates collision risk)
  • Clarified test comment about loadHistory behavior

// to other connected TUI clients sharing the original session key.
const uniqueKey = `tui-${randomUUID()}`;
await setSession(uniqueKey);
chatLog.addSystem(`new session: ${state.currentSessionKey}`);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Log message reads potentially-stale currentSessionKey

chatLog.addSystem is called with state.currentSessionKey after await setSession(uniqueKey). Whether state.currentSessionKey has been updated to uniqueKey by this point depends entirely on the setSession implementation. The test harness mock never updates state, so in tests the message will always show the old key ("agent:main:main"), masking any regression here.

Using the locally-known uniqueKey is unambiguous and consistent regardless of when/whether setSession mutates the state:

Suggested change
chatLog.addSystem(`new session: ${state.currentSessionKey}`);
chatLog.addSystem(`new session: ${uniqueKey}`);
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/tui/tui-command-handlers.ts
Line: 432

Comment:
**Log message reads potentially-stale `currentSessionKey`**

`chatLog.addSystem` is called with `state.currentSessionKey` after `await setSession(uniqueKey)`. Whether `state.currentSessionKey` has been updated to `uniqueKey` by this point depends entirely on the `setSession` implementation. The test harness mock never updates `state`, so in tests the message will always show the old key (`"agent:main:main"`), masking any regression here.

Using the locally-known `uniqueKey` is unambiguous and consistent regardless of when/whether `setSession` mutates the state:

```suggestion
          chatLog.addSystem(`new session: ${uniqueKey}`);
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 425 to +436
case "new":
try {
// 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: ${state.currentSessionKey}`);
} catch (err) {
chatLog.addSystem(`new session failed: ${String(err)}`);
}
break;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale token counts after /new

The /reset handler explicitly zeroes out state.sessionInfo.inputTokens, state.sessionInfo.outputTokens, and state.sessionInfo.totalTokens, then calls tui.requestRender() before awaiting the network call (see the // Clear token counts immediately to avoid stale display (#1523) block). The /new handler has no equivalent cleanup, so the status bar will continue to display the old session's token counts until the first reply arrives in the newly-isolated session.

Consider adding the same token-clearing block at the top of the "new" case:

state.sessionInfo.inputTokens = null;
state.sessionInfo.outputTokens = null;
state.sessionInfo.totalTokens = null;
tui.requestRender();
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/tui/tui-command-handlers.ts
Line: 425-436

Comment:
**Stale token counts after `/new`**

The `/reset` handler explicitly zeroes out `state.sessionInfo.inputTokens`, `state.sessionInfo.outputTokens`, and `state.sessionInfo.totalTokens`, then calls `tui.requestRender()` before awaiting the network call (see the `// Clear token counts immediately to avoid stale display (#1523)` block). The `/new` handler has no equivalent cleanup, so the status bar will continue to display the old session's token counts until the first reply arrives in the newly-isolated session.

Consider adding the same token-clearing block at the top of the `"new"` case:
```
state.sessionInfo.inputTokens = null;
state.sessionInfo.outputTokens = null;
state.sessionInfo.totalTokens = null;
tui.requestRender();
```

How can I resolve this? If you propose a fix, please make it concise.

…-150)

Addresses Aisle Security analysis:
- Error messages from gateway could contain ANSI/OSC/control sequences
- Using sanitizeRenderableText() neutralizes terminal escape sequences
@widingmarcus-cyber
Copy link
Copy Markdown
Contributor Author

@greptile-apps please review

Addressed Aisle Security finding:

  • Added sanitizeRenderableText() to neutralize terminal escape sequences in error messages (CWE-150)

Addresses Greptile 3/5 feedback:
- Add token-count reset to /new for UX parity with /reset (openclaw#1523)
- Use uniqueKey directly instead of state.currentSessionKey to avoid
  fragile dependency on setSession's internal state-mutation order
@widingmarcus-cyber
Copy link
Copy Markdown
Contributor Author

@greptile-apps please review

Addressed 3/5 feedback:

  • Added token-count cleanup to /new for UX parity with /reset
  • Use uniqueKey directly in log instead of state.currentSessionKey

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c8210cba2a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

// 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);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Abort in-flight run before switching to /new session

When /new runs during an active request, this path switches the client to a new session key without resetting or aborting the previous session run. setSession clears activeChatRunId, and after the key switch the old run’s final events are ignored by session-key filtering, so the previous run continues in the background while the UI can remain stuck in a busy state (waiting/streaming) with no way to reconcile that run from the new session.

Useful? React with 👍 / 👎.

steipete added a commit that referenced this pull request Mar 8, 2026
Landed from contributor PR #39238 by @widingmarcus-cyber.

Co-authored-by: Marcus Widing <[email protected]>
@steipete
Copy link
Copy Markdown
Contributor

steipete commented Mar 8, 2026

Landed on main after rebasing onto the latest origin/main.

What shipped:

  • /new now allocates a unique tui-<uuid> session key per client instead of resetting the shared agent session
  • /reset keeps the existing shared-session reset behavior
  • added regression coverage for the split /new vs /reset behavior and failure sanitization
  • sanitized /reset failure text too so both paths render safely in-terminal
  • updated CHANGELOG.md
  • ran pnpm lint, pnpm build, and pnpm test

Landed commit:

Source PR commits:

Thanks @widingmarcus-cyber.

@steipete steipete closed this Mar 8, 2026
mcaxtr pushed a commit to mcaxtr/openclaw that referenced this pull request Mar 8, 2026
Landed from contributor PR openclaw#39238 by @widingmarcus-cyber.

Co-authored-by: Marcus Widing <[email protected]>
Saitop pushed a commit to NomiciAI/openclaw that referenced this pull request Mar 8, 2026
Landed from contributor PR openclaw#39238 by @widingmarcus-cyber.

Co-authored-by: Marcus Widing <[email protected]>
GordonSH-oss pushed a commit to GordonSH-oss/openclaw that referenced this pull request Mar 9, 2026
Landed from contributor PR openclaw#39238 by @widingmarcus-cyber.

Co-authored-by: Marcus Widing <[email protected]>
jenawant pushed a commit to jenawant/openclaw that referenced this pull request Mar 10, 2026
Landed from contributor PR openclaw#39238 by @widingmarcus-cyber.

Co-authored-by: Marcus Widing <[email protected]>
dhoman pushed a commit to dhoman/chrono-claw that referenced this pull request Mar 11, 2026
Landed from contributor PR openclaw#39238 by @widingmarcus-cyber.

Co-authored-by: Marcus Widing <[email protected]>
senw-developers pushed a commit to senw-developers/va-openclaw that referenced this pull request Mar 17, 2026
Landed from contributor PR openclaw#39238 by @widingmarcus-cyber.

Co-authored-by: Marcus Widing <[email protected]>
V-Gutierrez pushed a commit to V-Gutierrez/openclaw-vendor that referenced this pull request Mar 17, 2026
Landed from contributor PR openclaw#39238 by @widingmarcus-cyber.

Co-authored-by: Marcus Widing <[email protected]>
alexey-pelykh pushed a commit to remoteclaw/remoteclaw that referenced this pull request Mar 22, 2026
Landed from contributor PR openclaw#39238 by @widingmarcus-cyber.

Co-authored-by: Marcus Widing <[email protected]>
(cherry picked from commit 4600817)
alexey-pelykh added a commit to remoteclaw/remoteclaw that referenced this pull request Mar 22, 2026
* refactor(discord): extract route resolution helpers

(cherry picked from commit c1d07b0)

* refactor(discord): extract native command context builder

(cherry picked from commit 9d10697)

* refactor(discord): extract native command session targets

(cherry picked from commit 8f719e5)

* fix(discord): default missing native command args

(cherry picked from commit eb9e78d)

* refactor(discord): extract inbound context helpers

(cherry picked from commit 547436b)

* refactor(discord): compose native command routes

(cherry picked from commit 6016e22)

* fix(telegram): honor commands.allowFrom in native command auth (openclaw#39310)

* telegram: honor commands.allowFrom in native auth

* test(telegram): cover native commands.allowFrom precedence

* changelog: note telegram native commands allowFrom fix

* Update CHANGELOG.md

* telegram: preserve group policy in native command auth

* test(telegram): keep commands.allowFrom under group gating

(cherry picked from commit c22a445)

* Discord: fix native command context test args

(cherry picked from commit ad80ecd)

* refactor(routing): centralize inbound last-route policy

(cherry picked from commit 6a8081a)

* refactor(telegram): centralize text parsing helpers

(cherry picked from commit e705627)

* refactor(telegram): split bot message context helpers

(cherry picked from commit c2e1ae6)

* fix: isolate TUI /new sessions per client

Landed from contributor PR openclaw#39238 by @widingmarcus-cyber.

Co-authored-by: Marcus Widing <[email protected]>
(cherry picked from commit 4600817)

* TUI: type setSession test mocks

(cherry picked from commit 6cb889d)

* fix(telegram): restore DM draft streaming

(cherry picked from commit e45fcc5)

* fix(ci): pin multi-arch docker base digests

(cherry picked from commit 5759b93)

* fix: reject launchd pid sentinel values

Landed from contributor PR openclaw#39281 by @mvanhorn.

Co-authored-by: Matt Van Horn <[email protected]>
(cherry picked from commit 7f44bc5)

* refactor: register gateway service adapters

(cherry picked from commit bd41326)

* fix: resolve cherry-pick type errors (rebrand, gutted modules, lastRoutePolicy)

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

* fix: resolve cherry-pick test failures (gutted Dockerfiles, rebrand INSTALL_BROWSER, preflight stub)

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>

---------

Co-authored-by: Peter Steinberger <[email protected]>
Co-authored-by: Vincent Koc <[email protected]>
Co-authored-by: Marcus Widing <[email protected]>
Co-authored-by: Ayaan Zaidi <[email protected]>
Co-authored-by: Matt Van Horn <[email protected]>
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] TUI: /new session replies broadcast to all connected TUI clients for the same agent

2 participants