Skip to content

fix(history): stop personality marker from swallowing the next real prompt - #74350

Open
JoaoMarcos44 wants to merge 3 commits into
NousResearch:mainfrom
JoaoMarcos44:fix/ia-06-personality-marker-loses-prompt
Open

JoaoMarcos44 wants to merge 3 commits into
NousResearch:mainfrom
JoaoMarcos44:fix/ia-06-personality-marker-loses-prompt

Conversation

@JoaoMarcos44

@JoaoMarcos44 JoaoMarcos44 commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Fixes history: personality-switch marker can merge with the next real prompt and get hidden wholesale, losing it #74315: after a personality switch, the next real user prompt could vanish entirely from hydrated/projected history on reload or resume.
  • The personality-switch marker is now persisted durably (SessionDB.append_message) and identified via structured display_kind="personality_switch" + display_metadata={"marker_text": ...}, mirroring the existing model-switch marker pattern instead of relying on a "[System:" text-prefix convention.
  • The display projection (_history_to_messages) now strips only the marker's own span from a row that alternation-repair merged with a real user turn, instead of dropping the whole merged row.

Root Cause

_apply_personality_to_session in tui_gateway/server.py appended the personality-pivot marker as a plain {"role": "user", "content": marker} dict, mutating only session["history"] in memory — never durably persisted via SessionDB.append_message() (unlike the model-switch marker, see _append_model_switch_marker).

Because the marker was role="user" and the next real prompt is also role="user", alternation-repair (agent/agent_runtime_helpers.py::repair_message_sequence) merges the two consecutive user messages, concatenating the real prompt onto the marker's content (marker_text + "\n\n" + real_text).

The display projection's hide check (_is_display_hidden_marker) then hid the row wholesale because the merged content still started with the literal string "[System:" — with no structured way to tell "this is a bare marker" apart from "this is a marker with a real prompt merged onto its tail". The real prompt the user typed disappeared from every client transcript after reload/resume.

Fix

  1. Durable persistence: _apply_personality_to_session now calls SessionDB.append_message(..., display_kind="personality_switch", display_metadata={"marker_text": marker}), following the exact pattern already used by _append_model_switch_marker (same _ensure_session_db_row / _session_db fallback, wrapped the same way).
  2. Structured identification instead of a text-prefix convention: the marker row now carries display_kind="personality_switch" and display_metadata={"marker_text": ...}. Alternation-repair's merge only rewrites contentdisplay_kind/display_metadata survive the merge untouched, so both the merge and the projection can recognize the marker unambiguously.
  3. Strip only the marker's own span: a new _bookkeeping_marker_span() helper in tui_gateway/server.py compares a row's content against its recorded display_metadata["marker_text"]. If they're equal, the row is a bare marker and is hidden entirely (unchanged behavior). If content starts with marker_text + "\n\n", only that prefix is stripped — the remainder (the real user prompt) is kept and rendered as an ordinary message, with the marker's display_kind/display_metadata suppressed since the visible content is no longer the marker.
  4. Rows without a recorded marker_text (legacy rows persisted before this fix, or the model-switch marker which doesn't yet carry display_metadata) fall back to the previous "[System:" text-prefix hiding — no behavior change for those paths.

Diff is scoped to the personality-marker path (tui_gateway/server.py); agent_runtime_helpers.py's alternation-repair merge logic is unchanged (confirmed it already leaves display_kind/display_metadata untouched on merge).

Test Plan

  • Added test_personality_marker_survives_alternation_repair_merge (tests/test_tui_gateway_server.py) — reproduces the exact chain from the issue: personality marker inserted with structured metadata → merged with the next real user turn via repair_message_sequence → projected via _history_to_messages → asserts the real prompt text is present and visible, and that no literal "[System:" text leaks into the projected transcript.
  • Added test_history_to_messages_hides_bare_personality_marker_without_merge — confirms a marker with nothing merged onto it is still hidden entirely, matching the model-switch marker's existing contract.
  • Ran pytest tests/test_tui_gateway_server.py -k "personality_marker or hides_gateway_system_markers or hides_bare_personality_marker or config_set_personality" — 5 passed.
  • Ran the full tests/test_tui_gateway_server.py suite (493 passed, 2 pre-existing unrelated failures — test_persist_model_switch_preserves_sibling_model_keys / test_persist_model_switch_clears_stale_base_url — confirmed failing identically on unmodified main, unrelated to this change).
  • Ran tests/run_agent/test_message_sequence_repair.py (43 passed), tests/gateway/test_session.py + tests/cli/test_personality_none.py (164 passed), tests/hermes_state/test_restore_alternation_repair.py + tests/agent/test_synthetic_turn_display_kind.py (7 passed) — no regressions.

Closes #74315

Infographic

Personality marker swallow fix — Greek/Aegis theme

…ed real prompts

The personality-switch marker was appended as an in-memory-only role=user
message identified purely by a "[System:" text prefix, unlike the
model-switch marker which is durably persisted via SessionDB.append_message.
When alternation-repair merged the marker with the next real user turn
(both role=user), the merged blob still started with "[System:" and the
display projection dropped the whole row, silently swallowing the real
prompt.

Persist the marker durably and identify it via display_kind="personality_switch"
plus display_metadata={"marker_text": ...}, mirroring the model-switch
marker's pattern. Both survive alternation-repair's merge (which only
rewrites `content`), so the hide-projection can now strip just the marker's
own span from a merged row instead of dropping the whole thing.

Closes NousResearch#74315
@alt-glitch alt-glitch added type/bug Something isn't working comp/tui Terminal UI (ui-tui/ + tui_gateway/) P2 Medium — degraded but workaround exists sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state labels Jul 29, 2026

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for tracing the marker/alternation/display interaction; the current-main premise is valid: tui_gateway/server.py:5556-5558 creates the consecutive user marker, agent/agent_runtime_helpers.py:681-709 merges it, and tui_gateway/server.py:6615-6617 hides the merged [System: row.

Problems

  • The new display_kind="personality_switch" at tui_gateway/server.py:5516 survives that merge because repair rewrites only content (agent/agent_runtime_helpers.py:692-709). The merged row is then excluded from every current real-user predicate: /undo at tui_gateway/methods_session.py:2307, /retry at tui_gateway/methods_tools.py:721, and rewind ordinal selection at tui_gateway/methods_prompt.py:168. The projection suppresses the display kind, but the live session history does not.

Suggested changes

  • Preserve a distinct merged-marker discriminator while making the merged row count as a real user turn for those paths, and add regression coverage for undo, retry, and rewind after the merge.

Automated hermes-sweeper review.

Comment thread tui_gateway/server.py
@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 30, 2026
@JoaoMarcos44
JoaoMarcos44 requested a review from teknium1 July 31, 2026 02:20
…y/rewind

teknium's review on NousResearch#74350: repair_message_sequence's consecutive-user
merge only rewrites content, so a real prompt merged onto a
personality/model-switch marker tail still carries the marker's
display_kind. undo, retry, and rewind ordinal selection all treat
role=user with no display_kind as the definition of a real turn, so
they silently skipped this visible prompt.

Pass 2 of repair_message_sequence now stamps merged_real_turn=True on a
bookkeeping-marker row when a distinct real turn gets merged onto it.
The four role=user and not display_kind predicates in
tui_gateway/server.py (session.undo, prompt.submit rewind ordinal,
retry, rollback.restore) now also accept merged_real_turn rows as real.

Added regression coverage for undo, retry, and rewind after the merge.
@JoaoMarcos44

Copy link
Copy Markdown
Contributor Author

Thanks for tracing the marker/alternation/display interaction; the current-main premise is valid: tui_gateway/server.py:5556-5558 creates the consecutive user marker, agent/agent_runtime_helpers.py:681-709 merges it, and tui_gateway/server.py:6615-6617 hides the merged [System: row.

Problems

  • The new display_kind="personality_switch" at tui_gateway/server.py:5516 survives that merge because repair rewrites only content (agent/agent_runtime_helpers.py:692-709). The merged row is then excluded from every current real-user predicate: /undo at tui_gateway/methods_session.py:2307, /retry at tui_gateway/methods_tools.py:721, and rewind ordinal selection at tui_gateway/methods_prompt.py:168. The projection suppresses the display kind, but the live session history does not.

Suggested changes

  • Preserve a distinct merged-marker discriminator while making the merged row count as a real user turn for those paths, and add regression coverage for undo, retry, and rewind after the merge.

Automated hermes-sweeper review.

Done

Resolves conflicts from main's split of tui_gateway/server.py into
methods_session.py/methods_prompt.py/methods_tools.py: took main's
side for the moved RPC handlers (session.undo, prompt.submit,
command.dispatch, rollback.*) and reapplied the merged_real_turn
predicate fix (this branch's last commit) to its new locations:

- methods_session.py: session.undo
- methods_prompt.py: prompt.submit rewind-ordinal selection
- methods_tools.py: /retry and rollback.restore

tests/tui_gateway/test_protocol.py: main dropped the old
command.dispatch retry/steer/skill/bundle test block wholesale
(no replacement found elsewhere); kept only this branch's new
regression test (test_command_dispatch_retry_resends_prompt_merged_onto_marker),
which still exercises the live command.dispatch/retry codepath.

_apply_personality_to_session and _bookkeeping_marker_span (the
original PR fix) did not conflict — main hadn't touched that code.

This branch has not been deployed

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

Labels

comp/tui Terminal UI (ui-tui/ + tui_gateway/) P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform 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.

history: personality-switch marker can merge with the next real prompt and get hidden wholesale, losing it

3 participants