Skip to content

fix(render): stop at end of JSON payload instead of waiting for EOF - #39

Merged
leeguooooo merged 1 commit into
leeguooooo:mainfrom
CelChe:fix/stdin-no-eof-hang
Jul 26, 2026
Merged

fix(render): stop at end of JSON payload instead of waiting for EOF#39
leeguooooo merged 1 commit into
leeguooooo:mainfrom
CelChe:fix/stdin-no-eof-hang

Conversation

@CelChe

@CelChe CelChe commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Problem

_consume_stdin() calls sys.stdin.buffer.read(), which returns only at EOF.
EOF requires every write handle on the pipe to close — not just the one
Claude Code wrote through. Claude Code spawns the statusLine via a shell
(bash -c "cs render" on Windows), and a sibling process that inherited the
write handle keeps the pipe open after that shell exits.

So the payload arrives in full, the render never happens, and the process
lives forever. On this machine that is a steady drip of cs render processes
— parent gone, still resident minutes later, roughly one every 20-40s across
concurrent sessions:

16200  age= 281s  parent=27156 GONE
19044  age= 277s  parent=8356  GONE
25480  age= 266s  parent=35288 GONE
34568  age= 226s  parent=27968 GONE

Confirmed directly — write the payload, never close stdin:

before:  stdin never closed -> still alive at 4s; exits the instant stdin closes
after:   stdin never closed -> exit=0 after 1.63s

Fix

The payload is a single JSON document, so read until it parses and stop there
instead of waiting on the pipe. Falls back to reading to EOF when the input
is not JSON, so nothing is truncated. Keeps read() for any stdin
replacement that lacks read1.

Cost: one extra json.loads per chunk. The payload is a couple of KB and
arrives in a single read — ~0.05ms on the render path.

Tests

New tests/test_render_thin_stdin.py: the fake pipe raises if read again
after the payload is complete, which is exactly the call that hangs in
production. Covers single-chunk, split-chunk, non-JSON→EOF, tty, empty, and
session-id routing after the bounded read.

tests/test_daemon.py + the new file: 65 passed, 1 skipped, 1 pre-existing
unrelated failure (test_release_pidfile_leaves_someone_elses_file_alone).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Prevented the status bar from hanging while reading complete JSON input when the input stream remains open.
    • Preserved handling for interactive, empty, unreadable, and non-JSON input.
    • Improved support for JSON data received in multiple chunks.
  • Tests

    • Added coverage for bounded input reads, split payloads, incomplete JSON, and session ID preservation.

`_consume_stdin()` calls `sys.stdin.buffer.read()`, which returns only at
EOF — and EOF requires *every* write handle on the pipe to close, not just
the one Claude Code wrote through. Claude Code spawns the statusLine via a
shell (`bash -c "cs render"`); on Windows a sibling process that inherited
the write handle keeps the pipe open after that shell exits.

Result: the payload has arrived in full, the render never happens, and the
process lives forever. Observed on Windows 11 / v3.32.0 as a steady drip of
`cs render` processes — parent gone, still resident minutes later, one every
20-40s across concurrent sessions:

  16200  age= 281s  parent=27156 GONE
  19044  age= 277s  parent=8356  GONE
  25480  age= 266s  parent=35288 GONE

Confirmed directly: write the payload, never close stdin → the process is
still alive after 4s; close stdin → it exits immediately.

The payload is one JSON document, so read until it parses and stop there.
Falls back to reading to EOF when the input is not JSON, so nothing is
truncated. One extra json.loads per chunk (~0.05ms — the payload is a couple
of KB and arrives in a single read).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

The thin renderer now consumes stdin in chunks and stops when a complete JSON document is available. Tests cover split payloads, non-JSON input, interactive and empty stdin, session extraction, and JSON completeness detection.

Bounded stdin consumption

Layer / File(s) Summary
Chunked JSON-aware stdin reading
src/claude_statusbar/render_thin.py
Adds chunked stdin reads and JSON completeness detection while preserving existing fallback behavior.
Stdin behavior validation
tests/test_render_thin_stdin.py
Adds fake stdin objects and tests for bounded reads, chunk reassembly, fallback cases, session extraction, and completeness checks.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant stdin.buffer
  participant _consume_stdin
  participant _payload_is_complete
  _consume_stdin->>stdin.buffer: read1(_STDIN_CHUNK)
  _consume_stdin->>_payload_is_complete: check accumulated bytes
  _payload_is_complete->>_payload_is_complete: call json.loads
  _payload_is_complete-->>_consume_stdin: complete or incomplete
Loading

Suggested reviewers: leeguooooo

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: stopping stdin reads once the JSON payload is complete instead of waiting for EOF.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/test_render_thin_stdin.py (1)

27-44: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing coverage for the no-read1 fallback path.

The PR objectives call out that "stdin replacements without read1 remain supported," but no test exercises render_thin.py's read_some is None branch (the old blocking-read() path) or the except (OSError, AttributeError) branch. Given _FakeBuffer already exists, a variant without read1 would close this gap cheaply.

As per coding guidelines, "Add targeted pytest tests around core.main, calculation helpers, and file parsing when modifying logic."

class _FakeBufferNoRead1:
    """Stands in for stdin replacements that only implement read()."""

    def read(self):
        return PAYLOAD


def test_falls_back_to_read_when_read1_is_absent(monkeypatch):
    stdin = _FakeStdin([PAYLOAD])
    stdin.buffer = _FakeBufferNoRead1()
    monkeypatch.setattr(sys, "stdin", stdin)

    assert render_thin._consume_stdin() == PAYLOAD
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_render_thin_stdin.py` around lines 27 - 44, Add targeted pytest
coverage for render_thin._consume_stdin when stdin.buffer lacks read1: introduce
a fake buffer implementing only read() and assert the payload is returned. Also
exercise the (OSError, AttributeError) fallback path if needed to cover both
supported fallback branches, while preserving the existing _FakeBuffer behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/claude_statusbar/render_thin.py`:
- Around line 190-196: Update _payload_is_complete to catch RecursionError
alongside ValueError from json.loads, returning False so deeply nested JSON
follows the same incomplete/malformed-payload fallback path without propagating
through _consume_stdin or render.

---

Nitpick comments:
In `@tests/test_render_thin_stdin.py`:
- Around line 27-44: Add targeted pytest coverage for render_thin._consume_stdin
when stdin.buffer lacks read1: introduce a fake buffer implementing only read()
and assert the payload is returned. Also exercise the (OSError, AttributeError)
fallback path if needed to cover both supported fallback branches, while
preserving the existing _FakeBuffer behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8faea5aa-9d1b-4a2d-9adc-402041c5c5d4

📥 Commits

Reviewing files that changed from the base of the PR and between b33bb9e and 23ac655.

📒 Files selected for processing (2)
  • src/claude_statusbar/render_thin.py
  • tests/test_render_thin_stdin.py

Comment on lines +190 to +196
def _payload_is_complete(buf: bytearray) -> bool:
"""True once `buf` holds a whole JSON document."""
try:
json.loads(buf.decode("utf-8", errors="replace"))
except ValueError: # JSONDecodeError ⊂ ValueError
return False
return True

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

_payload_is_complete doesn't catch RecursionError from deeply-nested JSON.

json.loads raises RecursionError (a RuntimeError, not ValueError) on pathologically nested documents. That exception isn't caught here, nor by _consume_stdin's except (OSError, AttributeError): (Line 237), so it propagates out of _consume_stdin() and crashes render() entirely instead of falling back gracefully like malformed JSON does.

🛡️ Proposed fix
 def _payload_is_complete(buf: bytearray) -> bool:
     """True once `buf` holds a whole JSON document."""
     try:
         json.loads(buf.decode("utf-8", errors="replace"))
-    except ValueError:  # JSONDecodeError ⊂ ValueError
+    except (ValueError, RecursionError):  # JSONDecodeError ⊂ ValueError
         return False
     return True
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def _payload_is_complete(buf: bytearray) -> bool:
"""True once `buf` holds a whole JSON document."""
try:
json.loads(buf.decode("utf-8", errors="replace"))
except ValueError: # JSONDecodeError ⊂ ValueError
return False
return True
def _payload_is_complete(buf: bytearray) -> bool:
"""True once `buf` holds a whole JSON document."""
try:
json.loads(buf.decode("utf-8", errors="replace"))
except (ValueError, RecursionError): # JSONDecodeError ⊂ ValueError
return False
return True
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/claude_statusbar/render_thin.py` around lines 190 - 196, Update
_payload_is_complete to catch RecursionError alongside ValueError from
json.loads, returning False so deeply nested JSON follows the same
incomplete/malformed-payload fallback path without propagating through
_consume_stdin or render.

@leeguooooo
leeguooooo merged commit 0190a54 into leeguooooo:main Jul 26, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants