fix(ui): strip injected inbound metadata from user messages in chat history#21138
fix(ui): strip injected inbound metadata from user messages in chat history#21138Mellowambience wants to merge 1 commit intoopenclaw:mainfrom
Conversation
| previousDefaults?.contextTokens !== params.defaults.contextTokens | ||
| : false; | ||
| if (params.defaults) { | ||
| lastSessionDefaults = params.defaults; | ||
| } | ||
|
|
||
| const entryUpdatedAt = entry?.updatedAt ?? null; | ||
| const currentUpdatedAt = state.sessionInfo.updatedAt ?? null; | ||
| const modelChanged = | ||
| (entry?.modelProvider !== undefined && | ||
| entry.modelProvider !== state.sessionInfo.modelProvider) || | ||
| (entry?.model !== undefined && entry.model !== state.sessionInfo.model); | ||
| if ( | ||
| !params.force && | ||
| entryUpdatedAt !== null && | ||
| currentUpdatedAt !== null && | ||
| entryUpdatedAt < currentUpdatedAt && | ||
| !defaultsChanged && | ||
| !modelChanged | ||
| ) { | ||
| return; | ||
| } | ||
|
|
||
| const next = { ...state.sessionInfo }; | ||
| if (entry?.thinkingLevel !== undefined) { | ||
| next.thinkingLevel = entry.thinkingLevel; | ||
| } | ||
| if (entry?.verboseLevel !== undefined) { | ||
| next.verboseLevel = entry.verboseLevel; | ||
| } | ||
| if (entry?.reasoningLevel !== undefined) { | ||
| next.reasoningLevel = entry.reasoningLevel; | ||
| } | ||
| if (entry?.responseUsage !== undefined) { | ||
| next.responseUsage = entry.responseUsage; | ||
| } | ||
| if (entry?.inputTokens !== undefined) { | ||
| next.inputTokens = entry.inputTokens; | ||
| } | ||
| if (entry?.outputTokens !== undefined) { | ||
| next.outputTokens = entry.outputTokens; | ||
| } | ||
| if (entry?.totalTokens !== undefined) { | ||
| next.totalTokens = entry.totalTokens; | ||
| } | ||
| if (entry?.contextTokens !== undefined || defaults?.contextTokens !== undefined) { | ||
| next.contextTokens = | ||
| entry?.contextTokens ?? defaults?.contextTokens ?? state.sessionInfo.contextTokens; | ||
| } | ||
| if (entry?.displayName !== undefined) { | ||
| next.displayName = entry.displayName; | ||
| } | ||
| if (entry?.updatedAt !== undefined) { | ||
| next.updatedAt = entry.updatedAt; | ||
| } | ||
|
|
||
| const selection = resolveModelSelection(entry); | ||
| if (selection.modelProvider !== undefined) { | ||
| next.modelProvider = selection.modelProvider; | ||
| } | ||
| if (selection.model !== undefined) { | ||
| next.model = selection.model; | ||
| } | ||
|
|
||
| state.sessionInfo = next; | ||
| updateAutocompleteProvider(); | ||
| updateFooter(); | ||
| tui.requestRender(); | ||
| }; | ||
|
|
||
| const runRefreshSessionInfo = async () => { | ||
| try { | ||
| await refreshSessionInfoPromise; | ||
| } finally { | ||
| refreshSessionInfoPromise = null; | ||
| const resolveListAgentId = () => { | ||
| if (state.currentSessionKey === "global" || state.currentSessionKey === "unknown") { | ||
| return undefined; | ||
| } | ||
| const parsed = parseAgentSessionKey(state.currentSessionKey); | ||
| return parsed?.agentId ? normalizeAgentId(parsed.agentId) : state.currentAgentId; | ||
| }; | ||
| const listAgentId = resolveListAgentId(); | ||
| const result = await client.listSessions({ | ||
| includeGlobal: false, | ||
| includeUnknown: false, | ||
| agentId: listAgentId, | ||
| }); | ||
| const normalizeMatchKey = (key: string) => parseAgentSessionKey(key)?.rest ?? key; | ||
| const currentMatchKey = normalizeMatchKey(state.currentSessionKey); | ||
| const entry = result.sessions.find((row) => { | ||
| // Exact match | ||
| if (row.key === state.currentSessionKey) { | ||
| return true; | ||
| } | ||
| // Also match canonical keys like "agent:default:main" against "main" | ||
| return normalizeMatchKey(row.key) === currentMatchKey; | ||
| }); | ||
| if (entry?.key && entry.key !== state.currentSessionKey) { | ||
| updateAgentFromSessionKey(entry.key); |
There was a problem hiding this comment.
Large scope expansion: the PR adds ~200 lines of session management refactoring (applySessionInfo, resolveModelSelection, clearLocalRunIds, model override logic) that aren't mentioned in the PR description. Consider splitting unrelated refactoring into separate PRs for easier review.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/tui/tui-session-actions.ts
Line: 1-255
Comment:
Large scope expansion: the PR adds ~200 lines of session management refactoring (`applySessionInfo`, `resolveModelSelection`, `clearLocalRunIds`, model override logic) that aren't mentioned in the PR description. Consider splitting unrelated refactoring into separate PRs for easier review.
<sub>Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!</sub>
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: e064f922a3
ℹ️ About Codex in GitHub
Your team has set up Codex to 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 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| return { | ||
| modelProvider: state.sessionInfo.modelProvider, | ||
| model: state.sessionInfo.model, | ||
| }; |
There was a problem hiding this comment.
Use session defaults when no model override exists
When a session row has no explicit model fields (common for new sessions or after clearing an override), resolveModelSelection falls back to the previous state.sessionInfo values instead of the latest listSessions defaults. This leaves stale model/provider metadata in the TUI footer and model-dependent UI hints after switching sessions, whereas the old logic recomputed from defaults each refresh.
Useful? React with 👍 / 👎.
| const line = lines[i]; | ||
|
|
||
| // Detect start of a metadata block. | ||
| if (!inMetaBlock && INBOUND_META_SENTINELS.some((s) => line.startsWith(s))) { |
There was a problem hiding this comment.
Restrict metadata stripping to the message prefix
This condition strips any line that starts with a sentinel anywhere in the message body, not just AI-injected prefix blocks. If a user pastes or discusses these headers later in their message, that section (including fenced JSON) is silently removed from rendered history, causing visible content loss in TUI/web views.
Useful? React with 👍 / 👎.
Fixes openclaw#21106 Fixes openclaw#21109 Fixes openclaw#22116 OpenClaw prepends structured metadata blocks (Conversation info, Sender, reply context, etc.) to user message content so the LLM has channel context. These blocks must never render in user-visible UI. Added stripInboundMetadata() utility with zero-alloc fast path. Integrated into TUI history replay and webchat message normalizer. Storage format unchanged — strip is display-only.
e064f92 to
8a3701e
Compare
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
13 similar comments
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
1 similar comment
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
10 similar comments
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
|
Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch. |
Summary
Conversation info (untrusted metadata):,Sender (untrusted metadata):, etc.) are prepended bybuildInboundUserContextPrefixso the LLM can use them as context — but they are AI-facing only and must never render in user-visible UI.stripInboundMetadata()utility + two call-site integrations (TUI history replay + webchat message normalizer).Change Type
Scope
Linked Issues
Fixes #21106
Fixes #21109
Root Cause
buildInboundUserContextPrefixinsrc/auto-reply/reply/inbound-meta.tsprepends structured metadata blocks directly to the stored user messagecontentstring. When session history is replayed:tui-session-actions.ts): callsextractTextFromMessage(message)→chatLog.addUser(text)— no strippingmessage-normalizer.ts): mapsm.content→[{ type: "text", text: m.content }]— no strippingBoth paths render the raw prefix, producing output like:
What did the weather look like yesterday?