Repository navigation
fix(relay): forget a session's reported state when its process goes - #803
Merged
Merged
Conversation
The relay dropped a session's turn state and roster entry only on idle (SessionEnd), which a process killed or closed mid-turn never sends. The same id respawned then read as busy to every delivery and in list_sessions until its new process ended a turn of its own. SessionClosed now drops that state too, and the terminal tells the relay about a process that ended on its own once it is reaped (terminal.SetSessionExited -> relay.SessionExited).
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.
Why
The relay keeps two in-memory maps per session: the turn state that deliveries read, and the reported state that
list_sessionspublishes. It dropped a session's entry from both only onidle, which is Claude Code's SessionEnd. A process that is killed, crashes, or is closed mid-turn never sends it.The terminal side already forgets the turn on reap and on close (
forgetTurn), so the two disagreed. When the same id was respawned:list_sessionskept listing it asbusy;stampDeliverytreated it as mid-turn (skipTurns = 1, no receipt check);noteInterruptfound no open turn on the terminal side and never told the relay.Found while investigating sessions that looked stuck as busy after the backend was OOM-killed. That incident turned out to be real work: the transcripts show those sessions mid-turn, and a backend restart starts both sides empty anyway. This path is a separate bug, found reading the code.
What changes
relay.SessionClosedalso drops the session's turn and reported state. The id reopened is a new process that has reported nothing yet.relay.SessionExited, which the terminal calls once it reaps a process that ended on its own:terminal.SetSessionExited;stream's reap, outsides.mu(the relay takes its own lock and calls into the terminal under it);main.gowires it at startup, anddenyInternalkeepsrelay.SessionExitedandterminal.SetSessionExitedoff/rpc/like their siblings.CHANGELOG.md: a Fixed entry.Tests
TestAReapedOrClosedSessionForgetsItsState(relay): busy, then exit or close, then neither the roster nor the turn map has the session. It fails without the fix ("busy after its process went").TestAReapedSessionIsReportedToTheExitWatcher(terminal, real PTY): a process that exits on its own reaches the exit watcher; a close does not take that path.gofmt -l .andgo vet ./...clean,go test ./...green; the touched packages also ran with-race.