Skip to content

fix(claude-code): stop emitting assistant roles on CC stdin (#5711) - #5816

Open
ntdatt812 wants to merge 1 commit into
tinyhumansai:mainfrom
ntdatt812:fix/5711-claude-code-assistant-role
Open

fix(claude-code): stop emitting assistant roles on CC stdin (#5711)#5816
ntdatt812 wants to merge 1 commit into
tinyhumansai:mainfrom
ntdatt812:fix/5711-claude-code-assistant-role

Conversation

@ntdatt812

@ntdatt812 ntdatt812 commented Aug 27, 2026

Copy link
Copy Markdown

Closes #5711.

Reproduced first-hand

Against Claude Code CLI 2.1.221 (newer than the 2.1.207 in the report), Windows 11, using the issue's minimal input:

$ claude -p --input-format stream-json --output-format stream-json --verbose --model haiku < assistant-role.jsonl
Error: Expected message role 'user', got 'assistant'
EXIT=1

Not one non-hook stdout event was produced — the turn died before any model invocation, which matches the report. The same three lines with every message.role set to "user" exit 0 and produce a normal response, so the constraint is on the inner role, not the type: "user" envelope.

The fix

Prior turns are folded into one labelled transcript row carried as role: "user"; the trailing user turn follows verbatim:

{"type":"user","message":{"role":"user","content":[{"type":"text","text":"Earlier conversation, for context only — do not answer it again:\n\nUser: first user\nAssistant: prior assistant"}]}}
{"type":"user","message":{"role":"user","content":[{"type":"text","text":"<the actual prompt>"}]}}

I piped that exact payload to the real CLI: exit 0, model responded.

The issue offers two strategies; this is "serialize prior transcript context into a user message" rather than "send only the trailing user turn", because the latter silently discards the conversation the user can see on screen.

Why labelled, rather than rewriting each turn into its own bare user row: without labels the model receives several consecutive user messages and can read its own past replies as fresh instructions. Keeping the latest turn as its own verbatim row also means the actual prompt is never reworded — only the context around it is reshaped.

One case the issue does not mention but this repo hits: a history ending on an assistant turn, which is exactly what switching an existing thread to this provider produces. It is now treated as all context and no prompt, instead of sending the assistant's own words to the model as if the user had typed them.

Verification

  • 6 tests in input_builder, 3 of which fail against the previous implementation (new_session_never_emits_an_assistant_role, new_session_carries_prior_turns_as_one_labelled_transcript, a_history_ending_on_an_assistant_turn_is_all_context) — verified by restoring the old builder with the new tests in place: 3 passed; 3 failed.
  • cargo test --lib claude_code — 45 passed.
  • cargo fmt --all — clean.
  • End-to-end against the live CLI as above.

One test helper, assert_every_row_is_a_user_role, parses each emitted line and asserts the invariant directly, so any future row-shaping change is held to the schema rather than to a string match.

Note on --append-system-prompt

The system row is still filtered out and still rides --append-system-prompt; a test pins that it does not leak into the transcript block.

Summary by CodeRabbit

  • Bug Fixes
    • Improved conversation handling when sending prompts to Claude Code.
    • Preserved the latest user message as the active prompt while including earlier conversation context in a consolidated transcript.
    • Ensured submitted conversation entries use a consistent user role for more reliable processing.

…nsai#5711)

Starting a new Claude Code session from an existing conversation died before
the model ran. `build_stdin` wrapped every history row as `type: "user"` but
preserved `message.role: "assistant"` for prior assistant turns, and the CLI
validates that inner role:

    Error: Expected message role 'user', got 'assistant'   (exit 1)

Reproduced first-hand against Claude Code CLI 2.1.221 on Windows 11 with the
issue's minimal input: exit 1, and not one non-hook stdout event, so the turn
died before any model invocation. The same three lines with every role set to
`user` exit 0 and produce a normal response.

Prior turns are now folded into a single labelled transcript row carried as
`role: "user"`, and the trailing user turn is sent verbatim after it:

    {"type":"user","message":{"role":"user","content":[{"type":"text",
      "text":"Earlier conversation, for context only — do not answer it
              again:\n\nUser: …\nAssistant: …"}]}}
    {"type":"user","message":{"role":"user","content":[{"type":"text",
      "text":"<the actual prompt>"}]}}

Piped that exact payload to the real CLI: exit 0, model responded.

Why a labelled transcript rather than rewriting each turn into its own `user`
row: without the labels the model receives several consecutive user messages
and can read its own past replies as fresh instructions. Keeping the latest
turn as its own verbatim row means the actual prompt is never reworded.

A conversation ending on an assistant turn — which is exactly what switching an
existing thread to this provider produces — is treated as all context and no
prompt, rather than sending the assistant's own words as the user's.

Tests: 6 in `input_builder`, 3 of which fail against the previous
implementation. `cargo test --lib claude_code` 45 passed.
@ntdatt812
ntdatt812 requested a review from a team August 27, 2026 01:43

@tinysweeper tinysweeper Bot 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.

tinysweeper found nothing blocking. Approving.

$0.0000 · 0 in / 0 out

@tinysweeper tinysweeper Bot added the priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect. label Aug 27, 2026
@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0599fd41-e579-43a8-84fc-e83643f1f708

📥 Commits

Reviewing files that changed from the base of the PR and between 5630b00 and 1357acc.

📒 Files selected for processing (1)
  • src/openhuman/inference/provider/claude_code/input_builder.rs

Included review availability: Your plan provides up to 10 included reviews per hour; 7 remain after this review.


📝 Walkthrough

Walkthrough

build_stdin now emits only user roles. Prior conversation turns become one labelled transcript row, while a trailing user turn remains verbatim. Tests cover new-session and resume behavior, including histories ending with assistant turns.

Changes

Claude Code input handling

Layer / File(s) Summary
User-role input policy
src/openhuman/inference/provider/claude_code/input_builder.rs
build_stdin separates the latest user turn from prior context. Helper functions emit user rows and render earlier turns as labelled transcript context.
Input behavior regression coverage
src/openhuman/inference/provider/claude_code/input_builder.rs
Tests verify user-only roles, transcript folding, assistant-ending histories, single-turn input, and resume behavior.

Estimated code review effort: 2 (Simple) | ~15 minutes

Merge Risk: ⚪ Minimal · up to 1357a

The change reshapes Claude Code input so prior context remains available while every emitted message uses the accepted user role; no actionable merge-blocking risk remains after normal checks and review.

Suggested reviewers: al629176, codeghost21, giri-aayush

Poem

A rabbit checks each role in line,

“User” rows now hop along just fine.
Old turns curl into context’s trail,
The latest prompt stays crisp and pale.
Claude’s input gate lets the session start,
With carrots, tests, and a tidy chart.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the primary change: preventing unsupported assistant roles in Claude Code stdin.
Linked Issues check ✅ Passed The changes satisfy issue #5711. They emit only user-role stdin messages, preserve prior turns in a labelled transcript, keep the trailing user turn separate for new sessions, handle assistant-ending …
Out of Scope Changes check ✅ Passed The changes are within scope for issue #5711. The helper functions, test updates, system-message handling, and formatting support the Claude Code stdin compatibility fix.
Docstring Coverage ✅ Passed Docstring coverage is 81.82% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 11 functions across 1 files.
Full details: Linked Issues check

Explanation

The changes satisfy issue #5711. They emit only user-role stdin messages, preserve prior turns in a labelled transcript, keep the trailing user turn separate for new sessions, handle assistant-ending histories as context, and add regression coverage.

Warning

Your free Security trial is over. An organization admin can activate billing to continue.


Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1357accc30

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +55 to +56
if let Some(transcript) = render_transcript(history) {
push_json_line(&mut out, &user_row(&transcript));

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid emitting context when no prompt is present

When a new-session history ends with an assistant turn, latest is empty but this still emits the transcript as a role=user row. Because the driver aborts only when build_stdin returns no bytes, Claude treats that context-only row as a new prompt and can generate an unsolicited duplicate response—the exact provider-switch scenario covered by the new test—despite the comment claiming there is no fresh instruction. Require a trailing user turn before emitting anything.

Useful? React with 👍 / 👎.

Comment on lines +82 to +87
fn render_transcript(history: &[&ChatMessage]) -> Option<String> {
let mut body = String::new();
for msg in history {
let speaker = match msg.role.as_str() {
"user" => "User",
"assistant" => "Assistant",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Move transcript replay into the provider dialect

This introduces a second transcript-replay implementation that independently decides which roles survive and how turns are serialized. That can drift from the active tool dialect—for example, its handling of assistant/tool exchanges—and produce malformed next iterations when the dialect's tool grammar changes. The repository contract explicitly requires transcript replay and tool-call formatting to remain together in tinyagents::harness::tool_calling::dialect, so this Claude-specific behavior should be added or delegated there instead of implemented in the transport builder.

AGENTS.md reference: AGENTS.md:L487-L494

Useful? React with 👍 / 👎.

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

Labels

priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Claude Code rejects assistant history when starting a new CLI session

1 participant