openclaw - ✅(Solved) Fix [Bug]: Startup context prelude and system event blocks bleed into Control UI chat as visible user message text [1 pull requests, 1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#68094Fetched 2026-04-18 05:53:58
View on GitHub
Comments
0
Participants
1
Timeline
4
Reactions
0
Author
Participants
Timeline (top)
referenced ×3cross-referenced ×1

When a new session starts (/new or /reset), the startup context prelude (daily memory notes, persona instructions, etc.) is concatenated directly into the text body of the first user message turn. Similarly, system events — including heartbeat wakes — are prepended into the user message body of whichever turn they fire during.

The Control UI renders all user turns in full with no filtering, so both the startup prelude and heartbeat event content are displayed verbatim as chat bubbles, as if the user typed them.

Error Message

Control UI filteringap() in control-ui/assets/index-*.js only filters messages matching /^\s*NO_REPLY\s*$/ (assistant role) or a specific synthetic tool error string. There is no filtering for internal runtime content embedded in user turns.

Root Cause

Root cause (traced in source)

Fix Action

Fixed

PR fix notes

PR #68125: fix: Control UI runtime content filtering (#68094)

Description (problem / solution / changelog)

Fix: Control UI Runtime Content Filtering

Problem Statement

When a new session starts (/new or /reset), the startup context prelude (daily memory notes, persona instructions) is concatenated directly into the user message body. Similarly, system events (heartbeat wakes, cron triggers) are prepended to user messages.

The Control UI renders all user turns in full with no filtering, causing runtime-injected content to appear as visible chat bubbles — as if the user typed them.

Impact: Confusing UX — users see internal runtime metadata displayed as their own messages.

Solution

Two-layer defense-in-depth filtering at the display boundary:

Layer 1: Gateway Stripping (Primary)

stripRuntimeInjectedContent() in src/gateway/server-methods/chat.ts:

  • Detects and strips startup context blocks ([Startup context loaded by runtime])
  • Detects and strips system event lines (System: [timestamp] / System (untrusted): [timestamp])
  • Removes messages entirely if empty after stripping
  • Handles all three message formats: content array, content string, text field
  • Preserves non-runtime text blocks in multi-block messages (e.g., image + text)
  • Preserves legitimate whitespace when no runtime content is present
  • Anchors startup context detection to leading position only (prevents false positives)

Layer 2: UI Filtering (Defense-in-Depth)

shouldHideHistoryMessage() in ui/src/ui/controllers/chat.ts:

  • Detects both startup context prefix and system event patterns
  • Filters runtime-injected messages client-side if gateway stripping fails
  • Matches existing NO_REPLY filtering pattern

Architecture

User Message Flow:
  Session Start → Startup Context Injected → Stored in Transcript
  chat.history Request → stripRuntimeInjectedContent() → Stripped Message
  Control UI → shouldHideHistoryMessage() → Clean Display

Key Design Decisions:

  • No changes to message storage or prompt pipeline (model still receives full content)
  • Content-pattern detection at display boundary only
  • Backward compatible (old sessions without injected content pass through unchanged)
  • Reference equality preserved when no changes needed (avoids unnecessary re-renders)

Changes

FileRoleDescription
src/gateway/server-methods/chat.tsGateway strippingRuntime content detection and stripping with block-index tracking
src/gateway/server-methods/chat.runtime-inject-strip.test.tsGateway tests17 test cases covering all stripping scenarios
ui/src/ui/controllers/chat.tsUI filteringDefense-in-depth detection for both startup context and system events
ui/src/ui/controllers/chat.test.tsUI testsRegression tests for system event filtering and edge cases

Test Coverage

Gateway Tests (17 tests)

  • ✅ Returns messages unchanged when no runtime content present
  • ✅ Removes user message that is entirely startup context prelude
  • ✅ Removes user message that is startup context + bare reset prompt
  • ✅ Strips system event lines from beginning, keeps user text
  • ✅ Strips untrusted system event lines
  • ✅ Removes message entirely when only system event lines
  • ✅ Handles string content field (not array)
  • ✅ Handles text field (not content)
  • ✅ Does not modify assistant messages
  • ✅ Does not modify user messages without runtime patterns
  • ✅ Returns same reference when nothing changes (performance)
  • ✅ Strips combined startup context + system events
  • ✅ Preserves non-runtime text blocks in content arrays
  • ✅ Preserves leading/trailing whitespace when no runtime content present
  • ✅ Does not strip startup context prefix when it appears mid-message
  • ✅ Strips startup context prefix only when at leading position
  • ✅ Does not strip startup context prefix with leading whitespace

UI Tests (7 tests)

  • ✅ Filters user messages that are entirely startup context prelude
  • ✅ Keeps user messages that do not start with startup context prefix
  • ✅ Filters user messages that are only system event lines
  • ✅ Filters user messages with untrusted system event lines
  • ✅ Keeps user messages that start with system event but have user text
  • ✅ Existing NO_REPLY filtering still works
  • ✅ Existing synthetic tool result filtering still works

Verification

# Gateway tests
npx vitest run src/gateway/server-methods/chat.runtime-inject-strip.test.ts
# Result: 17/17 PASS

# UI tests
npx vitest run ui/src/ui/controllers/chat.test.ts
# Result: 76/76 PASS

Edge Cases Addressed

Edge CaseHandling
Multi-text-block messages (text + image + text)Only strips the specific text block containing runtime content; preserves all other blocks
User types [Startup context loaded by runtime] in normal textAnchored to leading position — mid-message occurrences are never stripped
Indented/whitespace-heavy messages without runtime contentNo trimming applied — whitespace preserved exactly as-is
System-event-only messages bypassing gatewayUI layer catches with matching SYSTEM_EVENT_LINE_RE regex
Mixed system events + user textSystem event lines stripped, user text preserved

Outcomes

Before:

  • Startup context prelude visible as user message bubble
  • System event blocks visible as user text
  • Multi-text-block messages could be corrupted during stripping
  • Legitimate whitespace could be trimmed from normal messages

After:

  • Runtime-injected content invisible in Control UI
  • Clean chat history showing only user-typed messages
  • Multi-block messages preserved correctly
  • Whitespace integrity maintained
  • Model still receives full injected content (no functional changes to prompt pipeline)
  • Defense-in-depth: gateway strips, UI filters as backup

Fixes

Closes #68094

Changed files

  • src/gateway/server-methods/chat.runtime-inject-strip.test.ts (added, +259/-0)
  • src/gateway/server-methods/chat.ts (modified, +170/-1)
  • ui/src/ui/controllers/chat.test.ts (modified, +126/-0)
  • ui/src/ui/controllers/chat.ts (modified, +41/-1)

Code Example

const baseBodyForPrompt = isBareSessionReset
  ? [startupContextPrelude, baseBodyFinal].filter(Boolean).join("\n\n")
  : ...
RAW_BUFFERClick to expand / collapse

Bug type

Behavior bug (incorrect output/state without crash)

Summary

When a new session starts (/new or /reset), the startup context prelude (daily memory notes, persona instructions, etc.) is concatenated directly into the text body of the first user message turn. Similarly, system events — including heartbeat wakes — are prepended into the user message body of whichever turn they fire during.

The Control UI renders all user turns in full with no filtering, so both the startup prelude and heartbeat event content are displayed verbatim as chat bubbles, as if the user typed them.

Steps to reproduce

  1. Configure an agent with startupContext enabled (default) and daily memory files present
  2. Open the Control UI and start a new session (/new)
  3. Observe: the startup context prelude ("[Startup context loaded by runtime]", daily memory notes block, etc.) appears as a visible user message in the chat
  4. Trigger or wait for a heartbeat/system event mid-session
  5. Observe: the System: [timestamp] ... block appears as part of the next visible user turn

Root cause (traced in source)

Startup contextbuildSessionStartupContextPrelude() in startup-context-DVdfqRzB.js returns a string that is concatenated directly into baseBodyForPrompt (the user message body) in get-reply-XW5nFnK2.js:

const baseBodyForPrompt = isBareSessionReset
  ? [startupContextPrelude, baseBodyFinal].filter(Boolean).join("\n\n")
  : ...

System eventsdrainFormattedSystemEvents() in session-system-events-D8ngkWKO.js formats queued events as System: [timestamp] ... lines, which are then prepended to the user message body via buildReplyPromptBodies().

Control UI filteringap() in control-ui/assets/index-*.js only filters messages matching /^\s*NO_REPLY\s*$/ (assistant role) or a specific synthetic tool error string. There is no filtering for internal runtime content embedded in user turns.

Expected behaviour

Internal runtime content (startup context prelude and system event blocks) should not be visible in the Control UI chat pane. They are metadata injected for the model's benefit, not user input.

Suggested fix

One of:

  • Tag messages containing injected runtime content with metadata (e.g. kind: "runtime-inject" or hidden: true) so the UI can filter them out
  • Strip injected blocks from message text before returning history via chat.history
  • Move runtime-injected content to a dedicated non-user role that the UI already filters

Version

2026.4.15 (Linux, x64, node v22.22.0)

Additional context

Both issues share the same root cause and should ideally be fixed together. The NO_REPLY filtering precedent in the Control UI shows the pattern already exists — it just needs extending to cover runtime-injected content in user turns.

extent analysis

TL;DR

Tag messages containing injected runtime content with metadata to enable UI filtering.

Guidance

  • Identify the functions buildSessionStartupContextPrelude() and drainFormattedSystemEvents() as the sources of injected runtime content.
  • Modify these functions to include a metadata tag (e.g., kind: "runtime-inject" or hidden: true) in the messages they generate.
  • Update the Control UI filtering logic in ap() to exclude messages with this metadata tag.
  • Consider moving runtime-injected content to a dedicated non-user role that the UI already filters, as an alternative solution.

Example

// Example of adding metadata to startup context prelude
const baseBodyForPrompt = isBareSessionReset
  ? [{ text: startupContextPrelude, kind: "runtime-inject" }, baseBodyFinal].filter(Boolean)
  : ...

Notes

The suggested fix assumes that the Control UI can be modified to filter out messages based on metadata tags. If this is not possible, an alternative approach may be needed.

Recommendation

Apply workaround: Tag messages containing injected runtime content with metadata to enable UI filtering. This approach is preferred because it is a targeted solution that addresses the root cause of the issue without requiring significant changes to the existing codebase.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

openclaw - ✅(Solved) Fix [Bug]: Startup context prelude and system event blocks bleed into Control UI chat as visible user message text [1 pull requests, 1 participants]