Skip to content

0.10.0 follow-up 2: no orphaned inhibitor command on Linux, bounded terminal-route blocking tasks - #6372

Merged
Hmbown merged 2 commits into
mainfrom
claude/trusting-shannon-sgusdh
Sep 21, 2026
Merged

Hmbown merged 2 commits into
mainfrom
claude/trusting-shannon-sgusdh

Conversation

@Hmbown

@Hmbown Hmbown commented Sep 21, 2026

Copy link
Copy Markdown
Owner

Summary

Two findings from the post-merge reviews of #6371, both verified against main b233045.

  • Linux left one sleep infinity behind per interactive turn (Claude Code Review's pre-existing note, confirmed). systemd-inhibit holds the idle lock around a child of its own, and the guard released it with SIGKILL, which is never forwarded to that grandchild: the lock was released, the process was not, and a long session accumulated orphans. The inhibitor's command is now cat reading a pipe the guard holds; dropping the guard closes the pipe, cat exits on EOF and systemd-inhibit follows, with kill_on_drop still sending the release signal at once. macOS caffeinate has no grandchild and is unaffected (it just gains an unused pipe). New Linux test a_released_guard_leaves_no_grandchild_behind lists the inhibitor's children before the drop and asserts none outlives it; where no logind exists the inhibitor exits at once and the list is empty, so the test bites only where an inhibitor really runs.
  • Terminal routes could exhaust the blocking pool (Devin, red, on 0.10.0 follow-up: user-input deadline under the heartbeat, terminal routes off the runtime workers, sleep inhibitor as a tokio child #6371). with_session moved every route onto the blocking pool, but an unbounded number of them: one input write to a child that stopped reading holds the session lock indefinitely, and each further call, including ones whose client has disconnected (a started blocking task cannot be cancelled), would pin another pool thread waiting on that lock until unrelated blocking work stalled. A static 8-permit semaphore now gates the hop; the rest wait asynchronously in the handler, where a disconnect simply drops them. A stuck session can stall terminal routes, never the rest of the runtime.

No-Issue: post-merge review findings on #6371; no tracker issue exists for them.

Testing

CI on dfa9891, all green: Lint ✓, Test (ubuntu-latest) ✓, Test (macos-latest) ✓, Test (windows-latest) ✓, cargo check (aarch64-unknown-linux-ohos) ✓, Mobile runtime smoke ✓, Safety gate ✓, npm wrapper smoke ✓, Version drift ✓, Integrations ✓, VS Code ✓, link ✓, CodeQL (rust/python/js/actions) ✓, GitGuardian ✓.

Local, on this tree (dfa9891), tui lib test binary from cargo rustc -p codewhale-tui --lib --profile test --all-features --locked, sealed HOME, proxy unset:

  • sleep_guard::test result: ok. 3 passed; 0 failed (the_inhibitor_lives_exactly_as_long_as_the_guard, holding_twice_holds_two_independent_inhibitors, a_released_guard_leaves_no_grandchild_behind). This container has systemd-inhibit but no logind, so the inhibitor exits at once and the grandchild list is empty: the new test passes vacuously here and is meaningful only on a host where the inhibitor really runs.

  • runtime_api::tests::terminal_* — 2 passed (the live-session route test drives output/input/resize/kill through the gated helper against a real PTY; unknown session 404).

  • runtime_api::tests::runtime_info_advertises_terminal_capabilities — 1 passed.

  • Non-test cargo check -p codewhale-tui --lib --locked — exit 0, zero warnings (the previous follow-up's -D warnings dead-code trap does not recur).

  • scripts/check-blocking-calls-budget.py — 603 sites across 178 files, within budget; scripts/check-dead-code-budget.py — PASS, 174 attributes, exactly at budget.

  • cargo fmt --all -- --check

  • cargo clippy --workspace --all-targets --all-features --locked (warning-free under the CI allow list) — CI Lint ✓ on dfa9891

  • cargo test --workspace --all-features --locked — CI Test ubuntu ✓ / macos ✓ / windows ✓ on dfa9891

Checklist

  • This PR adds a new layer/module/abstraction — it names or deletes the layer it replaces (no new layer; one static gate inside the existing helper)
  • Updated docs or comments as needed
  • Added or updated tests where relevant
  • Verified TUI behavior manually if UI changes — no UI surface changes
  • Harvested/co-authored credit uses a GitHub numeric noreply address (n/a)

🤖 Generated with Claude Code

https://claude.ai/code/session_0134iUMxmGuXiG1LzPgfZVnv

… a signal

`systemd-inhibit` holds the idle lock around a child of its own, and the
guard released it with SIGKILL — which is never forwarded — so every
interactive turn on Linux left a `sleep infinity` orphan behind once the
inhibitor died (the lock itself was released; the process was not). The
command is now `cat` reading a pipe the guard holds: dropping the guard
closes the pipe, `cat` exits on EOF, and `systemd-inhibit` follows, with
`kill_on_drop` still sending the release signal immediately. A Linux test
lists the inhibitor's children before the drop and asserts none outlives
it; without logind the inhibitor exits at once and the list is empty, so
the test bites where an inhibitor actually runs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134iUMxmGuXiG1LzPgfZVnv
… threads

`with_session` moved every terminal route onto the blocking pool, but an
unbounded number of them: one write to a child that stopped reading holds
the session lock indefinitely, and each further call — including ones
whose client has since disconnected, since a started blocking task cannot
be cancelled — would occupy another pool thread waiting on that lock,
until the runtime's unrelated blocking work stalled too. A static
semaphore now admits eight session touches at a time; the rest wait
asynchronously in the handler, where a disconnect simply drops them.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134iUMxmGuXiG1LzPgfZVnv
@Hmbown
Hmbown marked this pull request as ready for review September 21, 2026 02:23
Copilot AI lite review requested due to automatic review settings September 21, 2026 02:23

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@Hmbown
Hmbown merged commit 3844396 into main Sep 21, 2026
34 of 36 checks passed
@Hmbown
Hmbown deleted the claude/trusting-shannon-sgusdh branch September 21, 2026 02:23
@claude

claude Bot commented Sep 21, 2026

Copy link
Copy Markdown
Contributor

Claude finished @Hmbown's task in 1m 50s —— View job


Review: PR #6372 (already merged into main as 3844396)

  • Gather context (diff, changed files)
  • Understand request (code review of the two changed files)
  • Review crates/tui/src/sleep_guard.rs
  • Review crates/tui/src/runtime_api/terminal.rs
  • Post findings

Note: this sandbox's checkout only contains the merge commit's tree (no reachable history for origin/main or the PR's own parent commits), so I reviewed the full current contents of both changed files rather than a git diff — the module doc comments and PR description let me reconstruct exactly what changed.

sleep_guard.rs — Linux orphan fix

The mechanism checks out. systemd-inhibit --mode=block ... cat blocks on cat's lifetime, and cat's stdin is now the write end of a pipe (Stdio::piped(), spawn, sleep_guard.rs:126-138) that only the guard holds. Dropping SleepGuard drops the Child, which drops its ChildStdin field, closing the pipe; cat sees EOF and exits, and systemd-inhibit (waiting on its own child) follows. kill_on_drop still SIGKILLs systemd-inhibit itself directly, which doesn't reach cat — but that's fine, since the pipe closing is what actually reaps the grandchild, and the direct kill is just belt-and-suspenders for systemd-inhibit.

The new a_released_guard_leaves_no_grandchild_behind test (sleep_guard.rs:188-214) correctly reasons about the vacuous case (no logind → inhibitor exits immediately → empty grandchild list) rather than silently passing for the wrong reason — good.

Verified no unwrap/panic risk: read_to_string on /proc/{pid}/task/{pid}/children uses .unwrap_or_default() (line 200), which also degrades gracefully on kernels without CONFIG_CHECKPOINT_RESTORE where that file doesn't exist.

runtime_api/terminal.rs — bounded blocking-pool gate

ROUTE_GATE (static Semaphore::const_new(8), line 207) is acquired before the work is handed to spawn_blocking (with_session, lines 217-239), and the permit is moved into the blocking closure so it's held for the full duration of the blocking session-lock operation, not just the queueing. This correctly bounds concurrent blocking-pool occupancy from terminal routes to 8, and a caller that disconnects while waiting on .acquire().await drops out without ever consuming a pool thread — matching the stated intent. The AcquireError branch (semaphore never .close()d) is unreachable but handled defensively rather than unwrapped.

This is a narrowly-scoped static gate specific to this route group, not a duplicate of the per-instance Arc<Semaphore> concurrency limiters elsewhere (tool_execution.rs, mcp.rs, client.rs, codemode.rs, workflow/mod.rs) — those bound unrelated resources (shell exec, MCP connects, etc.), so introducing a second one here isn't a rung-2 violation.

Other categories

  • Provider/model/route safety: not applicable — no model/provider selection logic touched.
  • Tests: the existing terminal_routes_serve_a_live_engine_session_over_http and terminal_output_for_an_unknown_session_is_not_found_and_creates_nothing tests still exercise the gated path end-to-end. No test directly drives >8 concurrent callers to prove queuing (would be a flaky/expensive addition), but the semaphore semantics here are standard tokio and don't need it.
  • Security: no new trust-boundary surface; input bounds (TERMINAL_INPUT_MAX_BYTES, dimension clamps, name length) are unchanged by this diff.

No correctness bugs found. Both fixes are well-targeted, minimal, and the reasoning in the module docs matches the actual code.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 21, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-21T02:25:01.342108Z dfa9891 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@devin-ai-integration devin-ai-integration Bot 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.

Devin Review found 1 potential issue.

Devin Review

Comment thread crates/tui/src/sleep_guard.rs
Hmbown added a commit that referenced this pull request Sep 21, 2026
Docs only: the environment guide names `cat` on the guard's pipe as the
inhibitor's command and the pipe closing as the release, matching #6372.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134iUMxmGuXiG1LzPgfZVnv
pull Bot pushed a commit to soitun/CodeWhale that referenced this pull request Sep 21, 2026
Hmbown#6372 replaced the inhibitor's `sleep infinity` with `cat` on a pipe the
guard holds, so the release is the pipe closing rather than a signal; the
environment guide still showed the old command.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0134iUMxmGuXiG1LzPgfZVnv
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.

3 participants