fix(herdr): release the pane when the process is signalled, not just on clean exit - #55
Open
JulienEllie wants to merge 1 commit into
Open
Conversation
…on clean exit
herdr keeps showing a dead code-puppy in its agent list after the process
goes away. The stale entry never expires -- herdr trusts a reported agent
claim indefinitely -- so the sidebar accumulates ghosts.
pane.release_agent is only sent from the `shutdown`/`session_end` callbacks,
which fire from a `finally:` in cli_runner. That block runs only when the
interpreter unwinds. Closing a terminal pane, `kill`, logout, and service
restarts all deliver SIGTERM (or SIGHUP), whose default disposition
terminates the process outright: the `finally:` never runs and the release
is never sent.
Verified against a live herdr 0.8.2 pane:
/exit -> released
Ctrl-D -> released
SIGTERM -> STUCK as codepuppy/idle, forever
SIGKILL -> STUCK as codepuppy/idle, forever
Install two guards when the plugin activates:
* atexit -- covers interpreter teardown paths that bypass the callback.
* a SIGTERM/SIGHUP handler -- covers the signal paths atexit misses.
Both are needed. atexit does NOT run on SIGTERM (confirmed empirically),
so an atexit-only fix would not have addressed the reported bug at all.
The handler releases, then chains any previously-installed handler, or
else restores the default disposition and re-raises. The process still
dies from the signal with a 128+signum exit status; shutdown semantics
are unchanged. release_and_close() is already idempotent and bounded, so
double-firing is harmless and an unreachable herdr cannot delay exit.
Failures to install (non-main thread) and failures to release are both
swallowed -- reporting must never break the agent.
SIGHUP is resolved with getattr rather than referenced directly: it does
not exist on Windows, where this module is still imported because
client.py speaks a named pipe there, and a bare signal.SIGHUP would raise
AttributeError at import time and disable the plugin outright.
SIGKILL remains uncatchable by design; reaping that case needs a
herdr-side liveness check on the reporting process.
Tests: 6 new cases covering registration, release-then-reraise, handler
chaining, release failure, unavailable signals, and platform-safe signal
resolution. The 5 behavioural cases fail without the fix. Full suite:
1983 -> 1989 passing, same 36 pre-existing failures.
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.
The bug
herdr keeps showing a dead code-puppy in its agent list after the process is gone. The entry never expires — herdr trusts a reported agent claim indefinitely — so the sidebar slowly fills with ghosts that can only be cleared by hand.
Root cause
pane.release_agentis only sent from theshutdown/session_endcallbacks, which fire from afinally:incli_runner:A
finally:only runs if the interpreter unwinds. SIGTERM and SIGHUP don't unwind — their default disposition terminates the process outright. Closing a terminal pane, a plainkill, a logout, and service restarts all send SIGTERM, so the release is simply never sent.Measured against a live herdr 0.8.2 pane, spawning a real
code-puppy -ieach time:/exitCtrl-Dcodepuppy/idle, permanentlycodepuppy/idle, permanentlyI watched a stranded pane for 30s; herdr never reaps it. Its title had already fallen back to a bare shell prompt while herdr still reported
agent=codepuppy.Notably, other plugins in this package already defend against this (
walmart_specific,conversation_scanner,browser_managerall registeratexit); herdr registers nothing, and the onlysignal.signalcalls in core are for SIGINT.The fix
Two guards installed in the existing
if _reporter.active:block.Both are required — this is the part worth reviewing. My first instinct was an
atexitone-liner. Before writing it I checked the assumption, andatexitdoes not run on SIGTERM:So an atexit-only patch would have looked perfectly reasonable in review and fixed nothing at all. Hence:
atexit— covers interpreter teardown paths that bypass the callback.atexitmisses.The handler releases, then chains any previously-installed handler, otherwise restores the default disposition and re-raises — so the process still dies from the signal with a correct
128 + signumstatus. Verifiedrc=143for SIGTERM.release_and_close()is already idempotent and bounded, so double-firing is harmless and an unreachable herdr can't delay exit.SIGHUPis resolved viagetattrrather than referenced directly: it doesn't exist on Windows, where this module is still imported (client.pyships the named-pipe transport from #695). A baresignal.SIGHUPwould raiseAttributeErrorat import and disable the plugin outright — confirmed by simulating a SIGHUP-lesssignalmodule.SIGKILL is out of scope and left explicitly documented — nothing in-process can run. Reaping that case needs a herdr-side liveness check on the reporting PID; happy to open an issue there if useful.
Verification
End-to-end against a live herdr pane, same script both ways:
pydantic-aiand are unrelated — I baselined them on cleanmainrather than assuming.ruff checkon the touched file: 4 warnings before, 4 after. No new lint.ruff formatclean.