Repository navigation
Fix #1877: delete a shellper row whose pid was reused, without signaling it - #1878
Merged
Merged
Conversation
…ing it After a reboot a persisted terminal_sessions row's shellper_pid can be reused by an unrelated process. reconnectSession() detects the start-time mismatch, but both #1686 guards (reconcile Phase 2 and the on-the-fly reconnect in getTerminalsForWorkspace) checked only processExists(), so the row was kept and retried on every pass, forever. Add isShellperPidReused(): true only for a KNOWN start-time mismatch (an unknown start time on either side is not proof). Inside each guard's shellperAlive branch, a reused pid now deletes the row and continues before the stale-row SIGTERM branch (shellper rows persist pid == shellper_pid), so the other process is never signaled. Every other reconnect failure keeps the #1686 WARN/keep behaviour. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
Author
|
Thanks, this is a tidy fix and the write-up made it easy to review. Integration review ran 3-way (gemini, codex, claude): all three APPROVE, none blocking. Two small things I'd like before merge, both one-liners:
Already covered elsewhere, no action here: the Phase 1 pre-filter still SIGTERMs on Waleed has the gate; I'll relay his decision. |
…-clock skew budget - shellper-husk-sweep.ts imports PID_REUSE_TOLERANCE_MS from session-manager.ts instead of declaring its own copy, so the reuse rule has one home. - isShellperPidReused's doc comment states that it compares two clocks (the shellper's Date.now() vs ps -o lstart / /proc ctime), so the tolerance is a skew budget, and that a false verdict only deletes an already-unreconnectable row, with nothing signaled. Co-Authored-By: Claude Opus 5.5 <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
After a reboot, a persisted
terminal_sessionsrow whoseshellper_pidnow belongs to an unrelated process was kept forever and retried on every reconcile and/api/statepoll. On this machine that was two log lines every ~2s, andtower.loggrew to 354 MB. Now that row is deleted the first time Tower sees it, with one INFO line, and the pid is never signaled.Fixes #1877
Root Cause
SessionManager.reconnectSession()already spots the reuse (PID 2217 reused (start time mismatch)), but it returns onlynull. Both Bugfix #1686 guards intower-terminals.tsthen checked onlyprocessExists(shellper_pid):getTerminalsForWorkspace()A reused pid is alive, so the row was kept (
unconfirmed++/ WARN) and retried, forever.Reproduced live, read-only. Row
fea2d3f9…haspid = shellper_pid = 2217with a recorded start of 2026-10-13.ps -p 2217shows/usr/libexec/diagnosticextensionsd, started 2026-10-24 after that day's boot. The socket file is gone.Fix
isShellperPidReused(pid, recordedStartTime)interminal/session-manager.ts, beside the reuse rule it shares (PID_REUSE_TOLERANCE_MS; the two2000literals now use this constant). It returns true only for a known mismatch. If the start time is unknown on either side (null recorded, orpscan't read it), it returns false, so a transientpsfailure can't switch off the tower reconcile: failed reconnect triggers delete + SIGTERM without verifying the shellper is dead (destroyed 53 live sessions in the #1629 incident) #1686 rule.shellperAlivebranch: a reused pid gets an INFO line, the row is deleted, and the loop moves on (continue). In Phase 2 this happens before the stale-row branch, which SIGTERMssession.pid. Shellper rows storepid == shellper_pid, so just skipping the guard would have signaleddiagnosticextensionsd.Departure from the issue's suggested shape. The sweep re-checks the start time with the shared helper instead of passing a
pid-reusedreason out ofreconnectSession(). Two reasons:reconnectSession's long-standingclient | nullreturn unchanged (5 callers, about 50 test mocks).reconnectSessiontreats "start time unknown" the same as "mismatch", and deleting a row on an unknown start time would weaken tower reconcile: failed reconnect triggers delete + SIGTERM without verifying the shellper is dead (destroyed 53 live sessions in the #1629 incident) #1686.The behaviour the issue prescribes is implemented as written.
Out of scope, flagged for follow-up (same hazard, not part of this issue):
shellper_pidbased onprocessExistsalone.liveSessionsForWorkspacetreats a live reused pid as a live session.Test Plan
tower-terminals.test.ts, one each for the reconcile and on-the-fly paths. Setup: pid alive, recorded start time a day before the real one. Expected: row deleted, no signal other than 0, INFO logged, no "leaving row untouched" WARN. Both fail on the unfixed code (expected "vi.fn()" to be called with arguments: [ 'bugfix-1877-reused-session' ]) and pass with the fix.Date.now()would now correctly read as reused.isShellperPidReusedtests: mismatch → true; match → false; null recorded → false; unreadable live start time → false.pnpm build)Once this ships, the stale row on this machine clears itself after the next Tower restart. No hand edit of
global.dbis needed.🤖 Generated with Claude Code