Repository navigation
fix(agy-browser): remove the handshake dir when no launcher was forked; test the failing-ps branch - #4965
Conversation
…d; test the failing-ps branch A signal between the EXIT trap and the launcher fork ran on_exit with no launcher: it kept the temp dir and said the launcher would read the cancel file, which nothing ever would. $! is set at the fork, before $launcher is, so on_exit reads it: empty means no launcher exists and the dir is removed silently; a forked-but-unreported launcher keeps the current behaviour. Tests: a unit trio sources on_exit and drives its three states (no launcher, dead launcher, live launcher), and a real-Chrome case makes `ps -Ao pgid=,stat=` fail during the exit trap: the owned group is still signalled, never proved gone, and the dir is kept with a message. Reading that ps failure as "gone" turns the case red. Closes #4949 Stand: Echo Act IV Pro Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Coverage Gate✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 90%. Diff CoverageDiff: origin/main...HEAD, staged and unstaged changesNo lines with coverage information in this diff. |
john-the-dev
left a comment
There was a problem hiding this comment.
Reviewed at 5fa139ef3ea45297011f62e6d38ef3b990f6e2f6. This closes both follow-ups from my approval of #4908 (review 5361220778). I checked each by running it, not by reading.
Follow-up 1: signal between trap on_exit EXIT and launcher=$!. Verified.
agy-browser.sh:158-159:launcher="${launcher:-${!:-}}", thenrm -rf "$HS"and return when it is empty. The$!fallback only works if nothing else is backgrounded before the launcher. I grepped the script: the only&job is the launcher fork at:314, and command substitutions and pipelines do not set$!. So an empty$!does mean "no launcher forked", and a set$!with an unset$launchermeans the fork happened but was not yet recorded, which goes down the existing cancel-file path.- rm safety:
:156returns unless${HS:-}is non-empty andsettledis empty, andHSis only evermktemp -doutput (:299, installed before the trap at:300). No path torm -rfof an empty or unscoped value. - Forked path: with a launcher present, the old
[[ -n "${launcher:-}" ]] &&guard is redundant, so callinglauncher_stateunconditionally is equivalent. - Control: the script reverted to
origin/main(f60ac4a) with the PR's test file givesAssertionError: on_exit/none: agy-browser.sh: Chrome's launcher has not reported its process group; it will read .../cancel .... At head it givesPASS 29/29. Theforkedcase setslauncher=''with a livesleep &, so it exercises the$!fallback specifically.
Follow-up 2: failing-ps branch of group_gone. Verified.
- Mutation:
group_gonechanged to|| return 0and[[ -n "$rows" ]] || return 0(ps failure or an empty table read as "gone"), with__pycache__cleared. Result:AssertionError: pstable: an unreadable table was not reported: browser: started on http://127.0.0.1:54193 ..., rc=1. Restored, it passes. - The case runs against the test's FAKE_CHROME process on this Mac (macOS). It did not skip.
Other checks
gh pr diff 4965 | bash scripts/review-checks.sh --diff: PASS (hardcoded-paths, root-artifacts, prose-cap).- CI:
shell tests (lanes)is red, andtsc + tests (clean install)is only its aggregator. The one gating failure istests/watcher-inbox-resolver.test.sh("a fast resolver returns at once ... elapsed 30s of 30"). That is a timing bound in the watcher resolver, and this diff cannot reach it. The same lane passed on main and on the other PR runs today. A rerun is needed before merge. This is not a finding against the diff. - No
license/clastatus is present on this head.
Nits (not blocking)
- Test 27 still runs under the
bash -xdiagnosis plumbing. That was my third nit on #4908; it is out of scope here and still open.
No blockers.
keweichen
left a comment
There was a problem hiding this comment.
REQUEST_CHANGES at exact head 5fa139ef3ea45297011f62e6d38ef3b990f6e2f6.
Blocker — the no-launcher cleanup fails under the shipped macOS Bash
agy-browser.sh ships with #!/bin/bash and set -u, but the new fallback at lines 157-159 is not compatible with macOS /bin/bash 3.2 when no background job exists. That is precisely the pre-launcher state this change is meant to handle.
Direct control on this Mac:
$ /bin/bash --version
GNU bash, version 3.2.57(1)-release
$ /bin/bash -c 'set -u; launcher="${launcher:-${!:-}}"'
/bin/bash: $!: unbound variable
rc=127
The same command succeeds under the PATH-resolved Homebrew Bash 5.2. The new test invokes unqualified bash at the extracted-function probe, so it selects Bash 5.2 on this host and reports 29/29 while missing the production interpreter.
I reran the entire exact-head test with only the outer PATH constrained so bash resolves to the shipped /bin/bash:
AssertionError: on_exit/none:
.../on_exit.sh: line 4: $!: unbound variable
The direct production-path signal repro has the same result: on_exit aborts at line 158, the handshake directory remains, and no cancel file exists. Thus issue #4949's no-launcher leak remains on the primary shipped macOS interpreter.
Use a Bash-3.2-safe way to read an unset $!, and pin the no-launcher case to /bin/bash (ideally also the real trap-before-fork path) so PATH cannot select a newer shell.
Other evidence
The normal PATH run passes 29/29; keeping the head test while pointing SCRIPT at main fails the new on_exit/none arm, so the arm otherwise discriminates the intended old behavior. Diff review checks, git diff --check, and shell syntax pass. Hosted Actions are currently green (Windows skipped), but license/cla is absent rather than green.
Worst case is limited to the opt-in agy-browser start path and a leaked handshake directory when a signal lands before the launcher fork; it does not justify shipping a fix that still fails on the platform interpreter. Changes requested; this head is not ready to merge.
|
@keweichen — confirmed on this Mac too: |
macOS /bin/bash 3.2 rejects ${!:-} under set -u while no job has ever been
started, so the no-launcher exit path aborted on the platform interpreter
and left the handshake dir. on_exit now toggles -u off around the read.
The unit probe runs /bin/bash rather than PATH's bash, which had been
selecting Homebrew's 5.2 and hiding this.
Stand: Echo Act IV Pro
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
@keweichen — fixed at Change. Before/after, the extracted Direct proof of the form: Full suite at |
|
@cla-assistant check |
john-the-dev
left a comment
There was a problem hiding this comment.
Re-approving at 21bc7b763. The delta from 5fa139ef3 is the on_exit read of $!, plus the test now running /bin/bash explicitly.
I verified it on macOS's shipped /bin/bash, GNU bash 3.2.57 (arm64):
- Old expression:
set -u; x="${launcher:-${!:-}}"fails with$!: unbound variablewhen no job has been started. keweichen's finding is confirmed. - New expression, with no job:
set +u; x="${launcher:-$!}"; set -ugives an emptyx, which is the correct "no launcher" path. - New expression after
sleep 1 &: it picks up the pid, as before. tests/claude-gemini-agy-browser.test.pyat head passes 29/29.
Toggling -u only around that single assignment keeps the rest of on_exit strict. Pinning the probe to /bin/bash is the right call, because that is what the #!/bin/bash shebang runs on macOS.
keweichen's CHANGES_REQUESTED at 5fa139ef3 is exactly this /bin/bash 3.2 failure, and this head fixes it. keweichen should still re-review to clear their own change request.
My earlier approval at 5fa139ef3 missed this. The probe ran the bash on PATH (5.x), not the shipped interpreter. Thanks, keweichen, for catching it.
keweichen
left a comment
There was a problem hiding this comment.
APPROVE — exact head 21bc7b76378ce03f2b3ae60fd652e5d0e31fd672 resolves my Bash-3.2 blocker and is ready to merge.
Verified on this Mac’s shipped /bin/bash 3.2.57:
- With no prior job, the new
set +u; launcher="${launcher:-$!}"; set -upath produces an empty launcher and restores nounset; aftersleep &, it captures the exact child PID. - The clean exact-head suite passes
29/29both normally and with PATH constrained so everybashinvocation uses/bin/bash. - Reverting only the production expression to the prior
${!:-}form while retaining the current test makes the full production-shell suite fail aton_exit/nonewith$!: unbound variable. - Extracted actual EXIT/TERM controls remove the no-launcher handshake directory silently while preserving exit status; dead, forked, and live-launcher controls also pass.
The cumulative diff passes bash -n, git diff --check, and review-checks.sh; hosted checks and CLA are green (Windows skipped). Worst-case scope remains the opt-in agy-browser startup cleanup, with no config, migration, or resident-service change.
Non-blocking follow-up: a signal after the child has already written pgid but before the parent records launcher/LSTART can still leave that Chrome alive. I reproduced the same fail-safe survivor on base f60ac4a0, prior head 5fa139ef3, and this head, so this PR neither introduces nor worsens it; it should be tracked separately. The PR body and test docstring also still describe the prior head/27 scenarios, while the exact-head author comment carries the corrected evidence.
What changed, and why
Two follow-ups from rui's approving review of #4908 (issue #4949), in
skills/claude-gemini/scripts/agy-browser.sh:on_exitran with no launcher, kept the handshake dir and said the launcher would read$HS/cancel— nothing ever would.$!is set at the fork, before$launcheris assigned, soon_exitnow reads${launcher:-${!:-}}: empty means no launcher exists, and the dir is removed with no message. A forked-but-not-yet-reported launcher keeps the existing cancel-file path. (${!:-}, not$!: the script runsset -u.)psbranch ofgroup_goneis now tested. No code change there; the branch already returned "not gone" on a failed or emptyps -Ao pgid=,stat=.Before/after
The
on_exitunit trio, extracted from the script and driven in bash (none= no launcher forked):The new real-Chrome
pstablecase (apshook fails exactly-Ao pgid=,stat=while the starter is SIGTERMed under the registry hold): at head the fake Chrome is signalled and exits, the dir is kept, and the exit prints the unproved-gone message. Mutation control —group_gonereturning 0 on a failed or emptyps:Note on the message the case accepts: TERM ends the fake Chrome, so before KILL the dead leader can no longer prove ownership over an unreadable table and
end_groupreturns 2 ("could not prove process group … $HS is kept"); with a TERM-ignoring Chrome it would return 1 ("did not exit"). The case accepts either, and asserts the dir is kept and the group was signalled — the three facts the mutation flips.Tests run at 5fa139e
python3 tests/claude-gemini-agy-browser.test.py→PASS 29/29 agy-browser.sh(was 27).git diff origin/main...HEAD | bash scripts/review-checks.sh→ PASS.Not in this PR
#4915's nits (profile-wide kill on failed start, installer pin, CDP note) — separate.
🤖 Generated with Claude Code