Skip to content

Fix bash async job notification regression - #2804

Closed
austinywang wants to merge 2 commits into
mainfrom
issue-1565-bash-job-notifications
Closed

austinywang wants to merge 2 commits into
mainfrom
issue-1565-bash-job-notifications

Conversation

@austinywang

@austinywang austinywang commented Apr 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Testing

  • not run locally (per repo policy)
  • bash -n Resources/shell-integration/cmux-bash-integration.bash
  • python3 -m py_compile tests/test_issue_1565_bash_job_notifications.py

Closes #1565


Note

Medium Risk
Medium risk because it changes prompt-time background job launching in the bash integration, which could affect job control behavior across different shell setups. Scope is limited and guarded by a new regression test.

Overview
Fixes a bash integration regression by replacing several async cmux send/relay/background tasks from brace-group + disown to backgrounded subshell launches (and removes disown usage), including cases that track PIDs via $!.

Adds tests/test_issue_1565_bash_job_notifications.py to assert the integration contains no & disown, rejects brace-group async patterns in key functions, and verifies expected counts of subshell-based async launches to prevent the regression from returning.

Reviewed by Cursor Bugbot for commit afe3158. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Restore correct async job notifications in the Bash integration by running background tasks in subshells instead of brace groups with disown. Fixes #1565 and keeps PID tracking using $! where needed.

  • Bug Fixes
    • Replaced { ... } >/dev/null 2>&1 & disown with ( ... ) >/dev/null 2>&1 & across async call sites; removed disown and captured $! for tracked jobs (e.g., PR poll loop, git probe).
    • Added tests/test_issue_1565_bash_job_notifications.py to enforce subshell launches and prevent reintroducing disown.

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

Summary by CodeRabbit

  • Bug Fixes

    • Enhanced bash shell integration to improve stability and reliability of background job management and tracking.
  • Tests

    • Added regression test to validate asynchronous job behavior within bash shell integration and prevent future regressions in job handling.

@vercel

vercel Bot commented Apr 11, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Apr 11, 2026 4:11am

@coderabbitai

coderabbitai Bot commented Apr 11, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR addresses a bash job notification spam issue by refactoring background job invocations in the shell integration script. The changes replace { ... } & disown patterns with ( ... ) & subshells across multiple async functions, eliminating a race condition. A new regression test validates the implementation.

Changes

Cohort / File(s) Summary
Shell Integration Updates
Resources/shell-integration/cmux-bash-integration.bash
Replaced brace-group blocks with explicit disown calls with backgrounded subshells across seven functions: _cmux_relay_rpc_bg, _cmux_report_tty_once, _cmux_report_shell_activity_state, _cmux_ports_kick, _cmux_emit_pr_command_hint, _cmux_start_pr_poll_loop, and async git/PWD reporting in _cmux_prompt_command. PID tracking assignments remain unchanged.
Regression Test
tests/test_issue_1565_bash_job_notifications.py
New test script validating async job implementation by scanning the integration file for disallowed & disown patterns and verifying expected backgrounded subshell counts across target functions using regex-based function body extraction.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • PR #1139: Modifies the same async/background invocation functions (_cmux_start_pr_poll_loop, PR-reporting jobs) by switching from { ... } & disown to ( ... ) & subshells.
  • PR #925: Added explicit disown to the same locations (_cmux_report_tty_once, _cmux_start_pr_poll_loop) that this PR now reverses by switching to backgrounded subshells.
  • PR #2466: Overlapping modifications to cmux-bash-integration.bash covering PR poll loop control and _cmux_ports_kick async background handling patterns.

Poem

🐰 No more noise at every hop,
Subshells backgrounded—jobs won't stop!
Disown be gone, a race won't creep,
Now async tasks run clean and deep.
Bash stays quiet, notifications sleep! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Title check ✅ Passed The title clearly and concisely summarizes the primary change: fixing a bash async job notification regression by switching the implementation approach.
Description check ✅ Passed The description covers the main summary of what changed and why, includes testing methodology, and provides a risk assessment. However, it lacks detail on manual testing verification and omits the requested review bot trigger block.
Linked Issues check ✅ Passed The PR directly addresses all requirements from issue #1565: replaces brace-group + disown pattern with backgrounded subshells across all affected functions, retains PID tracking via $!, and adds a regression test to prevent recurrence.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #1565: modifications to bash integration functions and addition of a focused regression test. No unrelated changes detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-1565-bash-job-notifications

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@greptile-apps

greptile-apps Bot commented Apr 11, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes issue #1565 by replacing all { } >/dev/null 2>&1 & disown brace-group async launches in the bash integration with ( ) >/dev/null 2>&1 & backgrounded subshells, and drops disown from both the fire-and-forget and PID-tracked sites.

  • Test quality policy violation: tests/test_issue_1565_bash_job_notifications.py reads the shell integration source file as raw text and checks for string/regex patterns. CLAUDE.md explicitly prohibits this class of test; the file should either be removed (with an explicit note that a behavioral test is impractical) or replaced with a test that runs bash and observes actual output.
  • Missing disown rationale: All eight async launch sites drop disown without an inline comment explaining why ( ) & without disown avoids job-notification output — a brief explanation would prevent a future contributor from re-adding it.

Confidence Score: 4/5

The bash transformation is mechanically correct but the new test directly violates a documented policy and should be removed or replaced before merging.

The P1 finding is a clear test quality policy violation per CLAUDE.md — not a runtime defect in the shell script itself, but a policy breach the team has explicitly called out. The bash change looks structurally sound; the P2 concern about missing disown rationale is non-blocking.

tests/test_issue_1565_bash_job_notifications.py needs to be removed or replaced with a behavioral test.

Important Files Changed

Filename Overview
Resources/shell-integration/cmux-bash-integration.bash Mechanical swap of brace-group + disown to backgrounded subshells across 8 async launch sites; the removal of disown from fire-and-forget sites warrants a comment explaining why bash job notifications are still suppressed.
tests/test_issue_1565_bash_job_notifications.py New regression guard that reads the shell integration source as raw text and grep-tests for code patterns — directly violates the CLAUDE.md test quality policy which prohibits source-text/AST-grep tests.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[PROMPT_COMMAND / hook fires] --> B{async work needed?}
    B -- "before (brace-group)" --> C["{ cmd } >/dev/null 2>&1 &"]
    C --> D["disown (removes from job table)"]
    D --> E[No Done notification]
    B -- "after (subshell)" --> F["( cmd ) >/dev/null 2>&1 &"]
    F --> G[No disown — job stays in table?]
    G --> H{PID tracked?}
    H -- "yes (_CMUX_GIT_JOB_PID, _CMUX_PR_POLL_PID)" --> I["$! captured for kill -0 check"]
    H -- "no (fire-and-forget)" --> J[Completes silently or shows Done notification?]
Loading

Reviews (1): Last reviewed commit: "fix: use subshells for bash async cmux j..." | Re-trigger Greptile

Comment on lines +46 to +67
script_text = INTEGRATION_PATH.read_text(encoding="utf-8")
failures: list[str] = []

if "& disown" in script_text:
failures.append("cmux-bash-integration.bash still contains '& disown'")

for function_name, expected_count in EXPECTED_ASYNC_SUBSHELL_COUNTS.items():
body = extract_function_body(script_text, function_name)
brace_group_hits = len(ASYNC_BRACE_GROUP_RE.findall(body))
if brace_group_hits:
failures.append(
f"{function_name}: found {brace_group_hits} brace-group async launch(es)"
)

if "disown" in body:
failures.append(f"{function_name}: unexpected disown remains in function body")

subshell_hits = len(ASYNC_SUBSHELL_RE.findall(body))
if subshell_hits != expected_count:
failures.append(
f"{function_name}: expected {expected_count} async subshell launch(es), found {subshell_hits}"
)

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.

P1 Violates test quality policy — source-text grep test

This test reads cmux-bash-integration.bash as raw text and checks for string patterns ("& disown") and regex matches on function bodies. CLAUDE.md explicitly prohibits this:

Do not add tests that only verify source code text, method signatures, AST fragments, or grep-style patterns. Tests must verify observable runtime behavior through executable paths.

Per the same policy: "If no meaningful behavioral or artifact-level test is practical, skip the fake regression test and state that explicitly." A behavioural alternative would be to spawn a bash subprocess with the integration sourced, trigger each PROMPT_COMMAND-time function, and assert that no [N]+ Done job-notification lines appear in the output. If that is not practical in CI, the test should be removed rather than replaced by an AST-grep guard.

Context Used: CLAUDE.md (source)

Comment on lines 379 to +382
payload="$(_cmux_report_tty_payload)"
[[ -n "$payload" ]] || return 0
_CMUX_TTY_REPORTED=1
{
(

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.

P2 Missing disown may re-surface job-notification noise

The entire PR removes disown from every fire-and-forget background launch (_cmux_report_tty_once, _cmux_report_shell_activity_state, _cmux_ports_kick, _cmux_emit_pr_command_hint, and the two sites in _cmux_prompt_command). Without disown, bash keeps these jobs in the job table and prints [N]+ Done ... before the next prompt when each completes — which looks like the same class of notification noise the PR intends to fix.

Subshells (( ) &) and brace-groups ({ } &) are both tracked identically by bash job control; the ( ) form does not suppress notifications by itself. If the fix relies on a specific bash behaviour (e.g. that PROMPT_COMMAND-spawned jobs are not reported, or that subshells in non-interactive child contexts skip job-table registration), a brief comment explaining why disown is no longer needed would prevent a future contributor from re-adding it under the wrong assumption that subshells are self-suppressing.

@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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@tests/test_issue_1565_bash_job_notifications.py`:
- Around line 45-80: The script tests/test_issue_1565_bash_job_notifications.py
defines a main() regression guard and only runs via the if __name__ ==
"__main__" block, so it never executes under CI imports; either add an explicit
CI invocation calling python3 tests/test_issue_1565_bash_job_notifications.py in
the CI job alongside the other explicit scripts, or refactor the file to expose
pytest-discoverable tests by turning main() logic into one or more test_...
functions (keeping the existing helpers like extract_function_body,
EXPECTED_ASYNC_SUBSHELL_COUNTS, ASYNC_BRACE_GROUP_RE, ASYNC_SUBSHELL_RE intact)
so pytest will run the checks automatically.
🪄 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

Run ID: f103f914-a768-499b-91d0-c2c1621bddbe

📥 Commits

Reviewing files that changed from the base of the PR and between 049c653 and afe3158.

📒 Files selected for processing (2)
  • Resources/shell-integration/cmux-bash-integration.bash
  • tests/test_issue_1565_bash_job_notifications.py

Comment on lines +45 to +80
def main() -> int:
script_text = INTEGRATION_PATH.read_text(encoding="utf-8")
failures: list[str] = []

if "& disown" in script_text:
failures.append("cmux-bash-integration.bash still contains '& disown'")

for function_name, expected_count in EXPECTED_ASYNC_SUBSHELL_COUNTS.items():
body = extract_function_body(script_text, function_name)
brace_group_hits = len(ASYNC_BRACE_GROUP_RE.findall(body))
if brace_group_hits:
failures.append(
f"{function_name}: found {brace_group_hits} brace-group async launch(es)"
)

if "disown" in body:
failures.append(f"{function_name}: unexpected disown remains in function body")

subshell_hits = len(ASYNC_SUBSHELL_RE.findall(body))
if subshell_hits != expected_count:
failures.append(
f"{function_name}: expected {expected_count} async subshell launch(es), found {subshell_hits}"
)

if failures:
print("FAIL:")
for failure in failures:
print(failure)
return 1

print("PASS: bash integration async cmux jobs use backgrounded subshells without disown")
return 0


if __name__ == "__main__":
raise SystemExit(main())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Likely test/CI config references =="
fd -HI -t f '(^pyproject\.toml$|^pytest\.ini$|^tox\.ini$|^noxfile\.py$|^Makefile$|^justfile$|\.ya?ml$)' |
while read -r file; do
  rg -n 'test_issue_1565_bash_job_notifications\.py|pytest|unittest|python3 .*test_issue_1565_bash_job_notifications\.py' "$file" || true
done

echo
echo "== Discoverable test symbols in tests/test_issue_1565_bash_job_notifications.py =="
python3 - <<'PY'
import ast
from pathlib import Path

path = Path("tests/test_issue_1565_bash_job_notifications.py")
tree = ast.parse(path.read_text(encoding="utf-8"))
print("top_level_functions:", [n.name for n in tree.body if isinstance(n, ast.FunctionDef)])
print("top_level_classes:", [n.name for n in tree.body if isinstance(n, ast.ClassDef)])
print("pytest_style_tests:", [n.name for n in tree.body if isinstance(n, ast.FunctionDef) and n.name.startswith("test_")])
PY

Repository: manaflow-ai/cmux

Length of output: 282


🏁 Script executed:

fd -t f '\.github/workflows.*\.yml$|\.github/workflows.*\.yaml$' -x cat {}

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

rg -t py 'test_issue_1565_bash_job_notifications' --max-count=20

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

fd -t f 'Makefile|justfile|.github' | head -30

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

git ls-files | head -50

Repository: manaflow-ai/cmux

Length of output: 2021


🏁 Script executed:

cat -n .gitignore 2>/dev/null || echo "No .gitignore found"

Repository: manaflow-ai/cmux

Length of output: 974


🏁 Script executed:

find . -maxdepth 2 -type f \( -name '*.md' -o -name '*.txt' -o -name '*.rst' \) | head -20

Repository: manaflow-ai/cmux

Length of output: 378


🏁 Script executed:

rg 'test.*python|pytest|unittest' --type md --type txt -i | head -30

Repository: manaflow-ai/cmux

Length of output: 739


🏁 Script executed:

cat .github/workflows/ci.yml | head -100

Repository: manaflow-ai/cmux

Length of output: 3004


🏁 Script executed:

cat CLAUDE.md | grep -A 20 -B 5 'test'

Repository: manaflow-ai/cmux

Length of output: 8733


🏁 Script executed:

ls -la tests/ | grep -E '\.py$'

Repository: manaflow-ai/cmux

Length of output: 7512


🏁 Script executed:

cat .github/workflows/ci.yml | grep -A 5 -B 5 'test_issue_1565'

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

grep -r 'test_issue_1565_bash_job_notifications' .github/workflows/

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

grep -r 'python3.*tests/test_' .github/workflows/ | head -20

Repository: manaflow-ai/cmux

Length of output: 479


🏁 Script executed:

cat .github/workflows/ci.yml | tail -150

Repository: manaflow-ai/cmux

Length of output: 6487


🏁 Script executed:

grep -A 30 'workflow-guard-tests' .github/workflows/ci.yml | head -40

Repository: manaflow-ai/cmux

Length of output: 1058


🏁 Script executed:

cat .github/workflows/ci.yml | grep -E 'python3|test_' | head -30

Repository: manaflow-ai/cmux

Length of output: 910


🏁 Script executed:

grep -r 'bash_job_notifications\|1565' .github/workflows/

Repository: manaflow-ai/cmux

Length of output: 42


🏁 Script executed:

cat .github/workflows/ci.yml | wc -l

Repository: manaflow-ai/cmux

Length of output: 63


🏁 Script executed:

cat .github/workflows/ci.yml | grep -E 'test.*\.py|python3' -B 2 -A 2

Repository: manaflow-ai/cmux

Length of output: 921


🏁 Script executed:

cat tests/test_issue_1565_bash_job_notifications.py

Repository: manaflow-ai/cmux

Length of output: 2704


🏁 Script executed:

grep -n 'if __name__' tests/test_issue_1565_bash_job_notifications.py

Repository: manaflow-ai/cmux

Length of output: 89


Add explicit CI invocation for this regression guard.

This file contains proper regression checks but is never executed in CI. The if __name__ == "__main__" block (line 79) only runs when the script is invoked directly; pytest/unittest discovery will import the module but not execute any checks. Without explicit invocation in .github/workflows/ci.yml, the regression guard remains inert.

Either add python3 tests/test_issue_1565_bash_job_notifications.py to the appropriate CI job (alongside test_cli_version_memory_guard.py and test_workspace_churn_up_arrow_lag.py), or reshape the file into pytest-discoverable test_* functions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_issue_1565_bash_job_notifications.py` around lines 45 - 80, The
script tests/test_issue_1565_bash_job_notifications.py defines a main()
regression guard and only runs via the if __name__ == "__main__" block, so it
never executes under CI imports; either add an explicit CI invocation calling
python3 tests/test_issue_1565_bash_job_notifications.py in the CI job alongside
the other explicit scripts, or refactor the file to expose pytest-discoverable
tests by turning main() logic into one or more test_... functions (keeping the
existing helpers like extract_function_body, EXPECTED_ASYNC_SUBSHELL_COUNTS,
ASYNC_BRACE_GROUP_RE, ASYNC_SUBSHELL_RE intact) so pytest will run the checks
automatically.

@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 2 files

@sandipb

sandipb commented May 1, 2026

Copy link
Copy Markdown

Thanks for working on this. Any update to this PR? It is annoying to manually patch cmux after every upgrade manually 😓

This branch was successfully deployed

1 active deployment
Preview — afe3158c Deployed Apr 11, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bash job termination notifications spam every prompt since disown fix (4402a5b0)

4 participants