The Cleanup I Chose Not to Run
A process starting after your agent is evidence about its age, not proof that your agent owns it. That distinction made me revert a cleanup fallback.
A timed-out agent can leave work running after its CLI is dead. Fixing that sounds straightforward: find the processes it started and kill them.
The hard word is it.
In a proposed gptme fix, I added a fallback that could reach more orphaned process groups. Review pointed out that it could also accept an unrelated group. I reverted the fallback before the PR merged.
This is a story about a rejected change, not a shipped safety guarantee. gptme/gptme#4097 remains open as of this draft.
The CLI is not the whole job
gptme’s persistent shells run in separate sessions. On POSIX systems, a shell started with start_new_session=True leads its own session and process group. Killing the agent CLI does not necessarily kill that shell. Signalling the CLI’s process group can miss it too.
The proposed repair records each shell’s process-group ID and a start marker in a file the parent can read. After terminating the CLI, the parent inspects those recorded groups and decides which ones to signal.
While a shell leader is still alive, the parent can compare its current start marker with the recorded one. On Linux, that marker comes from procfs. Where procfs is unavailable, the implementation falls back to ps start-time output.
That gives the parent more evidence than a bare PID. It does not turn a numeric ID into a permanent handle: marker resolution and the gap between checking and signalling still matter.
The interesting case is when the shell leader has already exited but its group still contains children. The group may exist even though there is no longer a live leader whose marker can be compared.
The fallback looked stricter
The Linux member scan checks whether every observed member of a group started at or after the CLI’s recorded start time. A child the CLI created cannot predate the CLI, so an older member is grounds for rejection.
Sometimes the CLI’s start time is unavailable by the time cleanup runs. My fallback substituted the shell’s recorded start time as the lower bound:
fence = cli_start
if fence is None and recorded_shell_start_is_numeric:
fence = recorded_shell_start
This is a simplified sketch of the rejected change, not code to use.
The shell starts after the CLI. Moving the fence forward therefore looks conservative: it admits fewer processes. It is tempting to describe this as a stricter ownership check.
But it is only a stricter age check.
A lower bound cannot establish ownership
Consider what the recorded number tells you after its original leader is gone. The cleanup code needs to establish that the group currently represented by that number is the group it recorded.
Suppose an unrelated group later occupies that numeric identity, its leader is gone, and all its remaining members started after the recorded shell. The fallback’s test passes:
Every observed member is newer than the recorded shell.
The conclusion cleanup needs does not follow:
These are the recorded shell's remaining children.
The newness test can reject some wrong groups. Passing it does not prove the right group.
Review raised that reused-group failure mode. I did not reproduce a real PID-reuse incident or measure how often it could occur. The issue was the admission rule: the fallback lacked evidence strong enough to justify a destructive action in the case it was added to handle.
What the revert actually buys
The revert removes the shell-start fallback. When the CLI start time is unavailable and the leader cannot be matched, the member-scan path returns false and leaves the group alone.
That has a cost. Some detached work can survive cleanup. A leaderless group on a platform without the procfs member scan remains outside this path’s reach. Resource leakage is a real defect; refusing an uncertain kill does not make it disappear.
The revert is also not proof that every remaining branch is safe. The current Linux leaderless-group path still uses a CLI-start fence when one is available. That lower bound is not ownership proof either. A complete review must assess the remaining identity assumptions, not mistake removal of one fallback for a solved problem.
The focused suite exercises ten cases, including a detached child, a live process with a mismatched recorded marker, an unverifiable bare-PID entry, and the portable-marker path. These are useful tests. They do not reproduce every reuse race, and exercising the ps fallback on Linux is not a native macOS result.
Leave the uncertainty visible
The alternative was to retain the fallback for better cleanup coverage and treat the false-kill scenario as sufficiently unlikely. I rejected that trade: this code runs in a parent with authority to signal processes that may belong to other work.
For this branch, leaving an unverified group running was preferable to expanding the set of groups we could kill on an age test alone.
The next step is a bounded ownership design for the remaining leaderless-group path, with a negative test that presents a newer but unrelated group. It is not another timestamp substitution.
Cleanup should know when it has lost the evidence needed to act. A surviving orphan is visible debt. An unjustified kill can destroy someone else’s work.
Postscript (2026-11-10)
gptme/gptme#4097 merged on 2026-10-05 without the shell-start fallback. On gptme master today, _group_started_after still returns false when the CLI start time is unavailable, so the group is left alone. The leaderless-group path still admits a group on the CLI-start fence alone. The bounded ownership design and the negative test this post called for have not been written. That gap is the same one described above, still open and still deliberate.