fix(acp): only signal the agent's process group while the agent is alive (#271) - #284
Open
orveth wants to merge 1 commit into
Open
fix(acp): only signal the agent's process group while the agent is alive (#271)#284orveth wants to merge 1 commit into
orveth wants to merge 1 commit into
Conversation
…ive (#271) `AcpDriver::shutdown` sent `kill -TERM -<pid>` unconditionally. That group TERM exists for a harness that spawned children of its own, and while the agent is alive it is exactly right: the agent is its own process-group leader, so the signal reaches that subtree and nothing else. Once the agent has exited it is neither safe nor useful. Its group has no members left to signal, and its pid -- which is also its pgid -- is held only by the unreaped zombie that the `wait()` two lines below releases for reuse. A group-directed signal in that state cannot reach the agent, and can only ever reach a group that inherited the number. Measured on tests/acp_concurrency.rs, whose stub agent exits on its own after reporting end_turn: at teardown its process group is already EMPTY. So the unguarded TERM was firing at a memberless group on every run, discarding the resulting ESRCH, on a CI runner, milliseconds before `wait()` freed that pid. The liveness check is therefore the substance of signalling here rather than a politeness -- it is what keeps a teardown signal inside the subtree it was aimed at. This does NOT claim to close #271. The runner-side kill does not reproduce locally, so the causal chain is unproven. What it does is remove the one construct in the teardown path capable of reaching outside its intended subtree, which stands on its own terms. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Narrow fix in the #271 teardown path. This does not claim to close #271 — the runner-side kill does not reproduce locally, so the causal chain is unproven. What it does is remove the one construct in that path capable of reaching outside the subtree it was aimed at.
What changed
AcpDriver::shutdownsentkill -TERM -<pid>unconditionally. It now sends it only whenchild.try_wait()reports the agent is still running.Why — measured, not reasoned
I instrumented
shutdown()locally and rantests/acp_concurrency.rs, logging what the group TERM was about to reach:Three findings, in order of importance:
end_turn, so by teardown its process group has no members. The TERM was firing at a memberless group on every run, discarding the resultingESRCHvialet _ =.target_pid_pgid == target_pid⇒process_group(0)works; the agent really is its own group leader, so the TERM is correctly scoped while the agent is alive.test_pgiddiffers from both targets ⇒ this is not signalling the test's own group.The instrument was validated before I trusted it.
ps -g <pgid>genuinely selects by process group on this box — positive control: run against my own shell's pgid it lists that shell plus its children. (ps --pgidis not supported by thispsat all, so the naive "correct" flag would have returned nothing and looked like the same answer for the wrong reason.) The empty result is a real measurement, not a silent flag error.The reasoning that follows
An empty group is not reassurance — it is the risk condition. The agent's pid is its pgid, and it is held only by an unreaped zombie that the
wait()two lines below releases for reuse. So at the moment of the unguarded TERM the signal: cannot reach the agent (gone), has no legitimate recipient (group empty), and is aimed at a number about to be recycled — on a CI runner, under heavy process churn.That makes the liveness check the substance of signalling here rather than a politeness. It is what keeps a teardown signal inside the subtree it was aimed at.
Why
try_wait()rather than a pid guardA
pid > 0guard againstkill -TERM -0— which would signal the signaller's entire process group, and on a CI runner that includes the runner service — is unreachable by construction:Child::id()never returns 0 for a spawned child. It would be a guard that can never fire, and therefore one with no positive control.try_wait()guards the condition that actually occurs and is measurable, andacp_concurrency.rsexercises it on every single run because its stub always exits first.Verification
cargo test -p mobee-core --features acp --test acp_concurrency→ 1 passed, 2.01s. The feat(seller): homogeneous multi-slot execution (reserve-at-claim) [REVIEW — do not merge] #223 concurrency guarantee is unaffected.try_wait()returnsOk(None), the group TERM fires exactly as before, so a real harness's spawned children are still reaped.Scope
The timing evidence for #271 (kill lands ~20ms past the 2.01s pass ⇒ completion-coupled) and the
The runner has received a shutdown signalerror text are on the issue — comments 5124767273 and 5124820143.Whether this changes the CI flake rate is an empirical question and the honest answer is that it needs runs to tell. If the flake persists, the timing still points into teardown and the next suspect is whatever else happens at agent exit.