Skip to content

fix(gateway): pivot personality on the next turn, not the one after - #73708

Open
keniniclaw-design wants to merge 2 commits into
NousResearch:mainfrom
keniniclaw-design:fix/personality-pivot-note
Open

fix(gateway): pivot personality on the next turn, not the one after#73708
keniniclaw-design wants to merge 2 commits into
NousResearch:mainfrom
keniniclaw-design:fix/personality-pivot-note

Conversation

@keniniclaw-design

Copy link
Copy Markdown

/personality wrote config.yaml and set _ephemeral_system_prompt correctly, but the model kept replying in the previous persona's voice for one more turn: with a populated transcript, prior assistant turns act as style exemplars and outweigh the system prompt.

Only visible on the second and later swaps in a process. On a cold gateway the history is empty, nothing to imitate, and the swap looks correct — which is why a restart appears to fix it.

tui_gateway._apply_personality_to_session already handles this by injecting a pivot marker, and documents the cause: "without this, LLMs tend to continue the tone established by earlier messages in the transcript". The gateway path had no equivalent.

Stage a note keyed by session_key and prepend it to the next user message, mirroring _pending_model_notes rather than writing into the transcript — history rows need display_kind bookkeeping, and an untagged marker is the class of bug that broke /retry.

Registered in _CONVERSATION_SCOPED_STATE so a staged-but-unconsumed note cannot leak across a reset or resume into a new conversation.

What does this PR do?

Related Issue

Fixes #

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

How to Test

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform:

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

For New Skills

  • This skill is broadly useful to most users (if bundled) — see Contributing Guide
  • SKILL.md follows the standard format (frontmatter, trigger conditions, steps, pitfalls)
  • No external dependencies that aren't already available (prefer stdlib, curl, existing Hermes tools)
  • I've tested the skill end-to-end: hermes --toolsets skills -q "Use the X skill to do Y"

Screenshots / Logs

/personality wrote config.yaml and set _ephemeral_system_prompt
correctly, but the model kept replying in the previous persona's
voice for one more turn: with a populated transcript, prior assistant
turns act as style exemplars and outweigh the system prompt.

Only visible on the second and later swaps in a process. On a cold
gateway the history is empty, nothing to imitate, and the swap looks
correct — which is why a restart appears to fix it.

tui_gateway._apply_personality_to_session already handles this by
injecting a pivot marker, and documents the cause: "without this,
LLMs tend to continue the tone established by earlier messages in
the transcript". The gateway path had no equivalent.

Stage a note keyed by session_key and prepend it to the next user
message, mirroring _pending_model_notes rather than writing into the
transcript — history rows need display_kind bookkeeping, and an
untagged marker is the class of bug that broke /retry.

Registered in _CONVERSATION_SCOPED_STATE so a staged-but-unconsumed
note cannot leak across a reset or resume into a new conversation.
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages 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
Contributor

Choose a reason for hiding this comment

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

Thanks for identifying a real gateway/TUI parity gap. Current main's gateway /personality path still only changes _ephemeral_system_prompt (gateway/slash_commands.py:2532-2549), while the TUI adds an explicit pivot marker (tui_gateway/server.py:5543-5558).

Problems

  • The submitted _pending_personality_notes dictionary needs reworking for current main. Conversation-scoped state is now structural in ConversationState (gateway/session_state.py:90-128) and boundary clearing goes through GatewayRunner._clear_conversation_scope() (gateway/run.py:21849-21863).
  • The note-consumption hunk predates the TurnRunner extraction; current model-note consumption is in TurnRunner.run_sync (gateway/run.py:4896-4900).
  • tests/gateway/test_personality_pivot_note.py verifies staging and /new, but does not exercise the subsequent agent turn or verify one-shot model delivery and clean persisted user text.

Suggested changes

  • Port the staged pivot into ConversationState and consume it in TurnRunner.run_sync beside the existing model-switch note path.
  • Add an end-to-end gateway-turn regression asserting the pivot reaches the model-facing payload once without becoming a visible/persisted user message.

Automated hermes-sweeper review.

Comment thread gateway/run.py
@@ -2311,6 +2311,7 @@ def _own_policy_open_startup_violation(config) -> Optional[str]:
"_session_reasoning_overrides",

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.

Current main has moved conversation-scoped state into gateway/session_state.py:ConversationState, whose clear() is invoked by every boundary. Please port this pivot state there rather than adding another runner-level dictionary to the legacy registry.

@teknium1 teknium1 added the sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users label Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages 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.

3 participants