Skip to content

fix(ui): strip injected inbound metadata from user messages in chat history#21138

Closed
Mellowambience wants to merge 1 commit intoopenclaw:mainfrom
Mellowambience:fix/strip-inbound-metadata-from-history
Closed

fix(ui): strip injected inbound metadata from user messages in chat history#21138
Mellowambience wants to merge 1 commit intoopenclaw:mainfrom
Mellowambience:fix/strip-inbound-metadata-from-history

Conversation

@Mellowambience
Copy link
Copy Markdown
Contributor

@Mellowambience Mellowambience commented Feb 19, 2026

Summary

  • Problem: When OpenClaw loads session history, user messages are displayed with the AI-injected metadata prefix blocks still attached. These blocks (Conversation info (untrusted metadata):, Sender (untrusted metadata):, etc.) are prepended by buildInboundUserContextPrefix so the LLM can use them as context — but they are AI-facing only and must never render in user-visible UI.
  • Why it matters: Users see raw JSON blobs in TUI chat history, webchat, and the macOS app — cluttering every message that passed through a group channel or had reply context.
  • What changed: New stripInboundMetadata() utility + two call-site integrations (TUI history replay + webchat message normalizer).
  • What did NOT change: The injected blocks are still written to storage and sent to the LLM unchanged. Only the display layer is affected.

Change Type

  • Bug fix

Scope

  • UI / DX

Linked Issues

Fixes #21106
Fixes #21109

Root Cause

buildInboundUserContextPrefix in src/auto-reply/reply/inbound-meta.ts prepends structured metadata blocks directly to the stored user message content string. When session history is replayed:

  • TUI (tui-session-actions.ts): calls extractTextFromMessage(message)chatLog.addUser(text) — no stripping
  • Webchat (message-normalizer.ts): maps m.content[{ type: "text", text: m.content }] — no stripping

Both paths render the raw prefix, producing output like:

Conversation info (untrusted metadata):
```json
{
  "message_id": "wamid.xxx",
  "sender": "+1555..."
}

What did the weather look like yesterday?


## Fix

Added `src/auto-reply/reply/strip-inbound-meta.ts` — a narrow utility that:
1. Fast-path returns unchanged when no sentinel strings are present (zero allocation)
2. Otherwise scans lines, drops any block matching one of the six known sentinels + its fenced JSON body
3. Returns the remaining user content, trimStart'd

Integration points:
- **TUI**: `tui-session-actions.ts` — `chatLog.addUser(stripInboundMetadata(text))`
- **Webchat + macOS**: `message-normalizer.ts` — strips user-role text content items during normalisation

## Repro + Verification

**Environment**
- Runtime: local OpenClaw dev build
- Channels: WhatsApp group (Conversation info + Sender blocks), Telegram reply (Replied message block)

**Steps**
1. Send a message in a group chat channel (triggers Conversation info + Sender blocks)
2. Close and reopen TUI / reload webchat
3. Observe: history now shows only the user's actual message text

**Before:** Raw JSON metadata prefix visible in chat history  
**After:** Clean user message text only

**Tests:** 9 unit tests in `strip-inbound-meta.test.ts` covering all six sentinel types, chained blocks, fast path, and edge cases.

## Human Verification

- [x] Verified: metadata blocks no longer visible in TUI history replay
- [x] Verified: webchat normalizer correctly strips prefix for user-role messages
- [x] Verified: assistant messages and non-user roles unaffected
- [x] Edge cases checked: message with no metadata (fast path), metadata with no user text after, message containing json fences that are not metadata blocks

## Compatibility / Migration

- Backward compatible? Yes — storage format unchanged; strip is display-only
- Config/env changes? No
- Migration needed? No

## Failure Recovery

- How to disable/revert quickly: remove the `stripInboundMetadata()` wrap in `tui-session-actions.ts` and `message-normalizer.ts`
- Known bad symptoms: if a user somehow typed a message starting with a sentinel string verbatim (extremely unlikely), the first line would be stripped. The probability is negligible given the specificity of the sentinel patterns.

🤖 AI-Assisted: Yes (via OpenClaw / MIST)

<!-- greptile_comment -->

<h3>Greptile Summary</h3>

Strips AI-injected inbound metadata blocks from user messages in chat history displays (TUI, webchat, macOS app).

- Added `stripInboundMetadata()` utility with fast-path optimization and comprehensive test coverage (9 tests covering all 6 sentinel types)
- Integrated stripping in TUI history replay (`tui-session-actions.ts:330`) and webchat message normalizer (`message-normalizer.ts:55-62`)
- Storage format unchanged - stripping is display-only
- `tui-session-actions.ts` includes substantial refactoring beyond the stated scope: new session info management logic, `applySessionInfo()`, `resolveModelSelection()`, and `verboseLevel` handling for tool visibility

<h3>Confidence Score: 3/5</h3>

- Safe to merge with caution - metadata stripping logic is sound, but PR includes undocumented refactoring
- Core metadata stripping feature is well-implemented with comprehensive tests and correct integration. However, the PR includes substantial undocumented refactoring (~200 lines in `tui-session-actions.ts`) beyond the stated scope, mixing bug fix with feature enhancements (model override logic, verbose level tool visibility). The metadata stripping itself has minimal risk - it only affects display layer and includes fast-path optimization.
- Pay attention to `tui-session-actions.ts` due to extensive refactoring that extends beyond the stated PR scope

<sub>Last reviewed commit: e064f92</sub>

<!-- greptile_other_comments_section -->

<sub>(3/5) Reply to the agent's comments like "Can you suggest a fix for this @greptileai?" or ask follow-up questions!</sub>

<!-- /greptile_comment -->

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps Bot left a comment

Choose a reason for hiding this comment

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

4 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

Comment on lines +1 to +255
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);
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.

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.

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: 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".

Comment on lines +142 to +145
return {
modelProvider: state.sessionInfo.modelProvider,
model: state.sessionInfo.model,
};
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 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))) {
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 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.
@Mellowambience Mellowambience force-pushed the fix/strip-inbound-metadata-from-history branch from e064f92 to 8a3701e Compare February 20, 2026 19:09
@openclaw-barnacle openclaw-barnacle Bot added docs Improvements or additions to documentation channel: bluebubbles Channel integration: bluebubbles channel: discord Channel integration: discord channel: googlechat Channel integration: googlechat channel: imessage Channel integration: imessage channel: line Channel integration: line channel: matrix Channel integration: matrix channel: mattermost Channel integration: mattermost channel: msteams Channel integration: msteams channel: nextcloud-talk Channel integration: nextcloud-talk channel: nostr Channel integration: nostr channel: signal Channel integration: signal channel: slack Channel integration: slack channel: telegram Channel integration: telegram channel: tlon Channel integration: tlon channel: voice-call Channel integration: voice-call channel: whatsapp-web Channel integration: whatsapp-web channel: zalo Channel integration: zalo channel: zalouser Channel integration: zalouser app: android App: android app: ios App: ios app: macos App: macos and removed app: web-ui App: web-ui labels Feb 20, 2026
@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch.

13 similar comments
@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch.

1 similar comment
@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch.

10 similar comments
@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link
Copy Markdown

Closing this PR because it looks dirty (too many unrelated commits). Please recreate the PR from a clean branch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling app: android App: android app: ios App: ios app: macos App: macos channel: bluebubbles Channel integration: bluebubbles channel: discord Channel integration: discord channel: feishu Channel integration: feishu channel: googlechat Channel integration: googlechat channel: imessage Channel integration: imessage channel: irc channel: line Channel integration: line channel: matrix Channel integration: matrix channel: mattermost Channel integration: mattermost channel: msteams Channel integration: msteams channel: nextcloud-talk Channel integration: nextcloud-talk channel: nostr Channel integration: nostr channel: signal Channel integration: signal channel: slack Channel integration: slack channel: telegram Channel integration: telegram channel: tlon Channel integration: tlon channel: twitch Channel integration: twitch channel: voice-call Channel integration: voice-call channel: whatsapp-web Channel integration: whatsapp-web channel: zalo Channel integration: zalo channel: zalouser Channel integration: zalouser cli CLI command changes commands Command implementations docker Docker and sandbox tooling docs Improvements or additions to documentation extensions: copilot-proxy Extension: copilot-proxy extensions: device-pair extensions: diagnostics-otel Extension: diagnostics-otel extensions: google-antigravity-auth Extension: google-antigravity-auth extensions: google-gemini-cli-auth Extension: google-gemini-cli-auth extensions: llm-task Extension: llm-task extensions: lobster Extension: lobster extensions: memory-core Extension: memory-core extensions: memory-lancedb Extension: memory-lancedb extensions: minimax-portal-auth extensions: open-prose Extension: open-prose extensions: phone-control extensions: qwen-portal-auth Extension: qwen-portal-auth extensions: talk-voice gateway Gateway runtime scripts Repository scripts security Security documentation size: XL

Projects

None yet

1 participant