Repository navigation
watcher: detach from controlling terminal to survive terminal close - #90
Merged
Merged
Conversation
Closing the terminal sent SIGHUP to the entire process group (coding agent + watcher). The watcher had no SIGHUP handler so it died instantly with the default terminate action, before its 5-second parent-PID poll could fire PostSessionEndOnParentExitAsync — leaving sessions stuck "active" in the UI forever (e.g. codex session 019e5e36ce9c7053a8ef8adfb67d7857). Call setsid() at watcher startup to move it into a new session with no controlling terminal so terminal-close SIGHUP no longer reaches it. The captured parentPid (coding agent's PGID, recorded before spawn) is unchanged and still resolves to the agent's actual PID, so the existing 5s poll detects the agent dying and runs the cleanup path. Also register a SIGHUP handler as defense-in-depth that mirrors the parent-exit cleanup (sets parentExited=1, cancels the CTS, suppresses the default terminate action) in case a shell forwards SIGHUP to its process-group children before setsid lands, or if setsid fails. Vendor-agnostic: benefits Claude too (Claude's SessionEnd hook is the primary cleanup path, but the watcher is now a reliable backstop for crashes, IDE detach, or SessionEnd hook failures). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Review Summary by QodoDetach watcher from controlling terminal to survive terminal close
WalkthroughsDescription• Detach watcher from controlling terminal via setsid() to survive terminal close • Add SIGHUP signal handler as defense-in-depth for edge cases • Prevent sessions from staying "active" when terminal closes unexpectedly • Enable reliable parent-exit detection and session-end cleanup Diagramflowchart LR
A["Terminal Close"] -->|SIGHUP to process group| B["Watcher Process"]
C["setsid() Call"] -->|Detach from terminal| B
B -->|No controlling terminal| D["SIGHUP Blocked"]
E["Parent PID Poll"] -->|Detects agent death| F["Session-End POST"]
G["SIGHUP Handler"] -->|Defense-in-depth| F
D --> E
File Changes1. src/Kapacitor.Cli/Commands/WatchCommand.cs
|
Code Review by Qodo
1.
|
Address two issues flagged by Qodo's review on #90: 1. PosixSignalRegistration.Create returns an IDisposable that owns the handler slot. Both registrations were being discarded, leaving them eligible for GC — the finalizer would then silently unregister the handler. Bind both to `using var` so they live for the duration of RunWatch and are disposed deterministically on exit. 2. For SIGTERM and SIGHUP, .NET runs the signal's default action (terminate) after the handler unless ctx.Cancel = true. The pre-existing SIGTERM handler called cts.Cancel() and immediately let the process die — the main loop never noticed and the final drain + session-end POST never ran. SIGHUP already set ctx.Cancel = true; make SIGTERM do the same for symmetry and to allow graceful drain when systemd/launchd/operators send SIGTERM. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Summary
SIGHUPto every process in the foreground group — codex/Claude and the watcher — and the watcher dies instantly (noSIGHUPhandler, default action =terminate). The 5-second parent-PID poll added in [AI-647] Close orphaned session when watcher's parent coding agent exits #75 never gets to run, soPostSessionEndOnParentExitAsyncnever fires and the session stays "active" in the UI forever. Reproduced with codex session019e5e36ce9c7053a8ef8adfb67d7857— log ends mid-stream at 11:08:08 with no shutdown markers, both watcher (22271) and parent (21667) gone.setsid()at watcher startup so the process moves to a new session with no controlling terminal, and SIGHUP from terminal close no longer reaches it. The capturedparentPid(coding agent's PGID, recorded before the watcher spawned) is unchanged and still resolves to the agent's PID, so the existing 5s poll fires when the agent dies and the cleanup path runs as designed.SIGHUPhandler that mirrors the parent-exit cleanup (setparentExited=1, cancel the CTS,ctx.Cancel = trueto suppress the default terminate). Covers the edge case where a shell explicitly forwardsSIGHUPto its process-group children beforesetsidlands, or wheresetsidfails.WatcherManager.SpawnWatcherpath runs for Claude and Codex. Claude sessions are usually rescued by Claude's ownSessionEndhook firing independently of the watcher, but the watcher is now a reliable backstop for crashes, IDE detach, orSessionEndfailures.Test plan
dotnet build src/Kapacitor.Cli/Kapacitor.Cli.csproj— cleandotnet publish src/Kapacitor.Cli/Kapacitor.Cli.csproj -c Release— noIL3050/IL2026AOT warningsProcessHelpersTests(7/7),WatchCommandTests(1/1),WatcherManagerSpawnArgsTests(4/4) passParent pid X exited; shutting down watcher(within ~5s) orReceived SIGHUP; treating as parent-exit(immediate), followed byParent-exit session-end POST succeeded, and that the session no longer shows "active" in the UI🤖 Generated with Claude Code