Skip to content

fix(acp): only signal the agent's process group while the agent is alive (#271) - #284

Open
orveth wants to merge 1 commit into
devfrom
fix/271-teardown-group-signal
Open

fix(acp): only signal the agent's process group while the agent is alive (#271)#284
orveth wants to merge 1 commit into
devfrom
fix/271-teardown-group-signal

Conversation

@orveth

@orveth orveth commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

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::shutdown sent kill -TERM -<pid> unconditionally. It now sends it only when child.try_wait() reports the agent is still running.

Why — measured, not reasoned

I instrumented shutdown() locally and ran tests/acp_concurrency.rs, logging what the group TERM was about to reach:

PROBE271 target_pid=2025988 target_pid_pgid=[2025988] test_pid=2025984 test_pgid=[2018988]
         group_members_of_2025988=[] || about to run: kill -TERM -2025988
PROBE271 target_pid=2025986 target_pid_pgid=[2025986] test_pid=2025984 test_pgid=[2018988]
         group_members_of_2025986=[] || about to run: kill -TERM -2025986

Three findings, in order of importance:

  1. The target group is EMPTY. The stub agent exits on its own after reporting end_turn, so by teardown its process group has no members. The TERM was firing at a memberless group on every run, discarding the resulting ESRCH via let _ =.
  2. target_pid_pgid == target_pidprocess_group(0) works; the agent really is its own group leader, so the TERM is correctly scoped while the agent is alive.
  3. test_pgid differs 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 --pgid is not supported by this ps at 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 guard

A pid > 0 guard against kill -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, and acp_concurrency.rs exercises it on every single run because its stub always exits first.

Verification

  • cargo test -p mobee-core --features acp --test acp_concurrency1 passed, 2.01s. The feat(seller): homogeneous multi-slot execution (reserve-at-claim) [REVIEW — do not merge] #223 concurrency guarantee is unaffected.
  • Production behaviour with a live agent is unchanged: try_wait() returns Ok(None), the group TERM fires exactly as before, so a real harness's spawned children are still reaped.
  • Probe instrumentation was reverted before committing; the diff is the guard plus its comment.

Scope

The timing evidence for #271 (kill lands ~20ms past the 2.01s pass ⇒ completion-coupled) and the The runner has received a shutdown signal error 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.

…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>
@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
mobee Ready Ready Preview Jul 30, 2026 12:37am

Request Review

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.

1 participant