Skip to content

fix: Emit assistant.message per text block in Claude runtime#1561

Open
knechtionscoding wants to merge 1 commit into
kelos-dev:mainfrom
datagravity-ai:fix/claude-per-block-assistant-message
Open

fix: Emit assistant.message per text block in Claude runtime#1561
knechtionscoding wants to merge 1 commit into
kelos-dev:mainfrom
datagravity-ai:fix/claude-per-block-assistant-message

Conversation

@knechtionscoding

@knechtionscoding knechtionscoding commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

What type of PR is this?

/kind bug

What this PR does / why we need it:

Fixes assistant text blocks concatenating into a single bubble within a turn in
Claude-backed sessions.

The Claude session provider (internal/sessionruntime/claude.go) only emitted
assistant.message via the assembled message envelope, and even then only
when no streaming deltas had occurred (!hadTextDelta). During normal streaming
it never emitted a bubble-closing assistant.message at all, so every text
block in a turn accumulated into one bubble.

Emitting the close events from the assembled envelope wouldn't fix it either:
all deltas arrive before the envelope, producing delta, delta, message, message
instead of the interleaved delta, message, delta, message that clients expect.

This PR:

  • Accumulates streamed text per content-block index and emits assistant.message
    on each content_block_stop, interleaved in the stream where the reducer
    expects it. Non-text blocks (e.g. tool_use) accumulate no text and emit
    nothing. This mirrors the Codex provider, which already emits deltas plus a
    closing message per block.
  • Leaves the assembled-message path unchanged, so the non-streaming fallback
    still works and there is no duplicate emission.
  • Updates the plain terminal (internal/cli/session_terminal.go) to end the
    current streamed line on a mid-stream assistant.message so blocks render
    separately there too. The TUI already handled this via finishStreaming()
    and needed no change.

Which issue(s) this PR is related to:

N/A

Special notes for your reviewer:

Behavior across the three event consumers after this change:

  • gravity-web reducer — receives delta … message … delta … message and
    renders separate bubbles.
  • TUI (session_terminal_tui.go) — already called finishStreaming() on a
    mid-stream assistant.message; it ignores the message text while streaming
    (uses accumulated deltas), so there is no duplication.
  • Plain terminal — now closes the streamed line per block.

Added internal/sessionruntime/claude_test.go covering: two-block separation,
a tool_use block emitting no message, the non-streaming fallback, and
confirmation that the assembled message does not re-emit streamed text.

Note: make verify fails on this branch, but only on pre-existing issues
unrelated to this change — YAML linting of stale .claude/worktrees/ copies and
a generated-files drift check. This is a pure Go-logic change with no
CRD/API/codegen impact. gofmt, go vet, and the internal/sessionruntime and
internal/cli package tests all pass.

Does this PR introduce a user-facing change?

Fixed Claude-backed sessions concatenating multiple assistant text blocks from a single turn into one message; each text block now renders as a separate message.

Summary by cubic

Fixes Claude-backed sessions concatenating multiple assistant text blocks into one message by emitting assistant.message per block, interleaved with deltas. Web and terminal UIs now render separate bubbles/lines for each block.

  • Bug Fixes
    • In internal/sessionruntime/claude.go, accumulate text per content-block index and emit assistant.message on each content_block_stop; non-text blocks emit nothing. The assembled-message path remains unchanged to avoid duplicates and matches the Codex provider’s behavior.
    • Event order is now delta, message, delta, message, so the web reducer renders separate bubbles.
    • In internal/cli/session_terminal.go, close the current streamed line on mid-stream assistant.message so blocks print separately. Added tests for two-block separation, tool_use no-op, non-streaming fallback, and no duplicate emission.

Written for commit b8f56dc. Summary will update on new commits.

Review in cubic

The Claude session provider only emitted assistant.message via the
assembled message envelope, and only when no streaming deltas had
occurred. During normal streaming it never emitted a bubble-closing
event, so every text block in a turn concatenated into one bubble.

Accumulate streamed text per content-block index and emit
assistant.message on each content_block_stop, interleaved with the
deltas. This matches the sequence the web reducer and TUI expect and
mirrors the Codex provider, which already emits a closing message per
block. Update the plain terminal to end the current streamed line on a
mid-stream assistant.message so blocks render separately there too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added kind/bug Categorizes issue or PR as related to a bug needs-triage needs-priority needs-actor needs-release-note Indicates a PR lacks a release-note block labels Jul 24, 2026
@knechtionscoding
knechtionscoding marked this pull request as ready for review July 24, 2026 11:01
@github-actions github-actions Bot added release-note and removed needs-release-note Indicates a PR lacks a release-note block labels Jul 24, 2026

@cubic-dev-ai cubic-dev-ai 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.

No issues found across 3 files

Re-trigger cubic

@gjkim42

gjkim42 commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator

/kelos review

@kelos-bot

kelos-bot Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

🤖 Kelos Task Status

Task kelos-reviewer-issue-comment-125ee52b89ed has succeeded. ✅

@kelos-bot

kelos-bot Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

🤖 Kelos Reviewer Agent @gjkim42

Review Summary

Verdict: REQUEST CHANGES
Overall correctness: patch is incorrect
Scope: Emits one Claude assistant.message boundary per streamed text block and separates those blocks in the plain terminal.

Findings Overview

Priority Count File:Line Summary
P0 0 none
P1 1 internal/sessionruntime/claude_test.go:15 Current-main integration leaves incompatible seenTools map literals
P2 2 internal/sessionruntime/claude.go:571 String concatenation makes streamed-text accumulation quadratic
P2 2 internal/cli/session_terminal.go:208 Plain-terminal block separation is not tested
P3 0 none

Findings

Correctness

  • [P1] internal/sessionruntime/claude_test.go:15 — Update the new test fixtures for the current base before merging. origin/main defines ClaudeProvider.seenTools as map[string]string, but this new file initializes it as map[string]struct{} at lines 15, 49, 73, and 94. Git merges these files textually without a conflict, leaving incompatible composite literals that prevent the internal/sessionruntime test package from compiling.

Performance

  • [P2] internal/sessionruntime/claude.go:571 — Accumulate each block with a mutable buffer rather than p.blockText[event.Index] += event.Delta.Text. Every streamed delta currently reallocates and copies all text accumulated for that block, making total copying quadratic for long Claude responses with thousands of chunks and adding allocation pressure in the single stdout read loop. A strings.Builder or append buffer per block index avoids that growth.

Tests

  • [P2] internal/cli/session_terminal.go:208 — Add a plain-terminal test for the advertised delta, message, delta, message behavior. The new tests cover only Claude event mapping, while the existing terminal test ends its stream with tool.started; therefore removing or breaking this new assistant.message branch would not fail a test. Assert that the two streamed blocks render on separate lines without duplicating their message text.

Key takeaways

  • The per-block event order matches the web reducer and TUI contracts, and the runtime tests cover text blocks, tool blocks, the assembled fallback, and duplicate suppression.
  • The current-base type mismatch is blocking even though Git reports a clean textual merge.

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants