fix(tui): isolate /new sessions to prevent cross-client broadcast#39238
fix(tui): isolate /new sessions to prevent cross-client broadcast#39238widingmarcus-cyber wants to merge 4 commits intoopenclaw:mainfrom
Conversation
…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.
|
@greptile-apps please review |
Greptile SummaryThis PR correctly fixes the cross-client broadcast bug by differentiating The only remaining issue is an inconsistency in error handling: the Confidence Score: 4/5
|
src/tui/tui-command-handlers.test.ts
Outdated
| // /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 |
There was a problem hiding this 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:
| 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.
src/tui/tui-command-handlers.ts
Outdated
| // 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)}`; |
There was a problem hiding this 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.
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.There was a problem hiding this comment.
💡 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".
src/tui/tui-command-handlers.ts
Outdated
| const uniqueKey = `tui-${randomUUID().slice(0, 8)}`; | ||
| await setSession(uniqueKey); |
There was a problem hiding this comment.
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
|
@greptile-apps please review Addressed feedback:
|
src/tui/tui-command-handlers.ts
Outdated
| // to other connected TUI clients sharing the original session key. | ||
| const uniqueKey = `tui-${randomUUID()}`; | ||
| await setSession(uniqueKey); | ||
| chatLog.addSystem(`new session: ${state.currentSessionKey}`); |
There was a problem hiding this 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:
| 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.| 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; |
There was a problem hiding this 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();
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
|
@greptile-apps please review Addressed Aisle Security finding:
|
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
|
@greptile-apps please review Addressed 3/5 feedback:
|
There was a problem hiding this comment.
💡 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); |
There was a problem hiding this comment.
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 👍 / 👎.
Landed from contributor PR #39238 by @widingmarcus-cyber. Co-authored-by: Marcus Widing <[email protected]>
|
Landed on What shipped:
Landed commit: Source PR commits: Thanks @widingmarcus-cyber. |
Landed from contributor PR openclaw#39238 by @widingmarcus-cyber. Co-authored-by: Marcus Widing <[email protected]>
Landed from contributor PR openclaw#39238 by @widingmarcus-cyber. Co-authored-by: Marcus Widing <[email protected]>
Landed from contributor PR openclaw#39238 by @widingmarcus-cyber. Co-authored-by: Marcus Widing <[email protected]>
Landed from contributor PR openclaw#39238 by @widingmarcus-cyber. Co-authored-by: Marcus Widing <[email protected]>
Landed from contributor PR openclaw#39238 by @widingmarcus-cyber. Co-authored-by: Marcus Widing <[email protected]>
Landed from contributor PR openclaw#39238 by @widingmarcus-cyber. Co-authored-by: Marcus Widing <[email protected]>
Landed from contributor PR openclaw#39238 by @widingmarcus-cyber. Co-authored-by: Marcus Widing <[email protected]>
Landed from contributor PR openclaw#39238 by @widingmarcus-cyber. Co-authored-by: Marcus Widing <[email protected]> (cherry picked from commit 4600817)
* 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]>
Summary
/new, replies are broadcast to all connected clients because they share the same session key./newand/resetwere usingclient.resetSession()which resets but doesn't change the session key, leaving all TUIs subscribed to the same session./newfrom/reset:/new: Creates a unique session key (tui-{uuid}) to isolate the client/reset: Resets the shared session (existing behavior preserved)Change Type
Scope
Linked Issue/PR
User-visible / Behavior Changes
/newnow creates an isolated session per TUI client instead of resetting the shared session/resetretains existing behavior (resets shared session, messages still broadcast)/newwill no longer see each other's messagesSecurity Impact
Repro + Verification
Steps
openclaw tui, then/agent mainopenclaw tui, then/agent main/newEvidence
creates unique session for /new and resets shared session for /resetHuman Verification
/resetstill works as before (shared session reset)Compatibility / Migration
/newbehavior changes;/resetunchanged)Failure Recovery
/newfailing to create session (test catches this)