Skip to content

fix: persist [System:] markers with role=system, demote at API call time - #62598

Closed
yingliang-zhang wants to merge 1 commit into
NousResearch:mainfrom
yingliang-zhang:fix/system-marker-role-persistence
Closed

fix: persist [System:] markers with role=system, demote at API call time#62598
yingliang-zhang wants to merge 1 commit into
NousResearch:mainfrom
yingliang-zhang:fix/system-marker-role-persistence

Conversation

@yingliang-zhang

Copy link
Copy Markdown
Contributor

Problem

[System:] messages (model-switch markers, personality pivots, verification nudges, continuation prompts) are persisted as role=user to avoid HTTP 400 on strict OpenAI-compatible providers (#48338). This causes the Desktop to render them as prominent right-aligned user message bubbles, visually pushing assistant responses out of view — users report their "response was washed away by a system message."

Root Cause

PR #54210 (fixing #48338) changed model-switch markers from role=system to role=user to avoid strict providers rejecting mid-array system messages. This conflated the API compatibility layer with the persistence/representation layer: the wire-format constraint was pushed into the DB, where the Desktop reads it as a real user message.

The same role=user pattern was then copied to all 7 [System:] injection points across tui_gateway/server.py and agent/conversation_loop.py.

Fix

Separate the concerns: persist with the correct role=system, demote to role=user only at API call time.

1. Persistence layer — use role=system (4 files, 7 injection points)

File Injection point Change
tui_gateway/server.py:~2440 Model-switch marker (history + DB) usersystem
tui_gateway/server.py:~2448 Model-switch marker (agent DB) usersystem
tui_gateway/server.py:~2454 Model-switch marker (scoped DB) usersystem
tui_gateway/server.py:~4098 Personality change marker usersystem
agent/conversation_loop.py:~1942 Continuation prompt (timeout/truncation) usersystem
agent/conversation_loop.py:~5111 "Continue now" tool-call nudge usersystem
agent/conversation_loop.py:~5184 Verification-stop nudge usersystem
agent/conversation_loop.py:~5242 Pre-verify nudge usersystem

2. API compatibility layer — demote in sanitize_api_messages (1 file)

Added a new step in sanitize_api_messages (agent/agent_runtime_helpers.py) that demotes any system message at position > 0 to role=user on the per-call copy. The stored transcript and Desktop rendering keep the correct system role; only the wire payload is adjusted.

This is the same chokepoint already used for empty tool_calls cleanup and orphaned tool repair — it runs on every API call and only mutates the per-call copy, never the persisted trajectory.

3. Desktop rendering — zero changes needed

The Desktop already has a SystemMessage component (apps/desktop/src/components/assistant-ui/thread/system-message.tsx) that renders role=system messages as small, centered, gray text. The toRuntimeMessage function (chat-runtime.ts:355) already maps role=system to this component. No frontend changes required.

Related

Test

  • 358 tests passed, 0 failed across test_tui_gateway_server.py (316), test_agent_guardrails.py (39), test_verification_stop_caching.py (3)
  • 2 new tests for sanitize_api_messages demotion logic
  • 3 existing tests updated to assert role=system

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review Summary

Automated review by Hermes Agent: No obvious issues found.


Reviewed by Hermes Agent

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Code Review Summary

Automated review by Hermes Agent: No obvious issues found.


Reviewed by Hermes Agent

@alt-glitch alt-glitch added type/bug Something isn't working comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/tui Terminal UI (ui-tui/ + tui_gateway/) sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state P2 Medium — degraded but workaround exists labels Jul 11, 2026
@yingliang-zhang
yingliang-zhang force-pushed the fix/system-marker-role-persistence branch from 9d2f982 to b73a714 Compare July 11, 2026 14:01

@teknium1 teknium1 left a comment

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.

Thanks for separating persisted transcript roles from the strict-provider wire workaround. The current-main premise is real: tui_gateway/server.py:2433-2460 stores model-switch markers as user, and tui_gateway/server.py:4084 does the same for personality pivots.

Problems

  • agent/agent_runtime_helpers.py:2492 demotes every system message after index 0. This also rewrites supported system-role prefill messages: agent/conversation_loop.py:878-881 inserts agent.prefill_messages after the primary system prompt and calls this sanitizer at :900; tests/cron/test_scheduler.py:1864 supplies a system-role prefill example. The new behavior should be limited to Hermes's internal marker records.

Suggested changes

  • Mark internal pivot/nudge messages explicitly and demote only those records at the API boundary.
  • Add a regression test proving a non-marker system prefill remains system while an internal marker is demoted.

Automated hermes-sweeper review.

Comment thread agent/agent_runtime_helpers.py Outdated
@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit labels Jul 11, 2026
@yingliang-zhang
yingliang-zhang force-pushed the fix/system-marker-role-persistence branch 4 times, most recently from 7464aed to 0b06c50 Compare July 20, 2026 18:22
@yingliang-zhang
yingliang-zhang force-pushed the fix/system-marker-role-persistence branch 5 times, most recently from e540f13 to dcf1a01 Compare July 30, 2026 07:07
@yingliang-zhang
yingliang-zhang force-pushed the fix/system-marker-role-persistence branch from dcf1a01 to 6fd61a9 Compare August 4, 2026 02:33
@yingliang-zhang
yingliang-zhang force-pushed the fix/system-marker-role-persistence branch from 6fd61a9 to ab15322 Compare August 7, 2026 04:20
@yingliang-zhang
yingliang-zhang force-pushed the fix/system-marker-role-persistence branch 4 times, most recently from 701674e to c665567 Compare August 10, 2026 04:40
Rebuild on the current upstream snapshot: keep upstream's
conversation-loop refactors, flush-to-DB batch writes, and
personality-marker display_kind tagging, while switching
[System:] markers from role=user to role=system via
make_internal_system_marker. The pre-call sanitizer demotes
tagged markers to role=user for strict-provider compatibility.

Includes test updates: _DB stubs gain append_messages_batch,
personality-marker assertions expect role=system, and the
model-switch marker tests verify the system role + tag.
@yingliang-zhang
yingliang-zhang force-pushed the fix/system-marker-role-persistence branch 2 times, most recently from b232e36 to 447a409 Compare August 12, 2026 05:46
@yingliang-zhang

Copy link
Copy Markdown
Contributor Author

Closing: this branch predates upstream's mid-turn guidelines + display_kind redesign (#78113 / a871948). Against latest main the runner metadata + TUI scaffolding-layer work conflicts architecturally with upstream's rollback/display_kind design, and the abandoned base-insert-in-flight flush direction was validated as unworkable (#80832 replaces it). Not resubmitting in this form.

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

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/tui Terminal UI (ui-tui/ + tui_gateway/) P2 Medium — degraded but workaround exists sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants