Two race conditions fixed:
1. cleanupInteractiveState timing (main issue):
- Previously: state was deleted from interactiveStates map BEFORE
closeAgentSessionWithTimeout was called synchronously
- Problem: during the blocking close (up to 130s for Stop hooks),
the map was empty but agent session was still shutting down
- Result: concurrent requests saw conflicting state - some got
"No execution in progress" while others saw blocking behavior
- Fix: delete state from map AFTER closeAgentSessionWithTimeout
completes, with re-check to avoid deleting replaced states
2. readLoop defer leak (secondary issue):
- Previously: defer had early return on ctx.Done() that skipped
closing cs.events and cs.done channels
- Problem: if Close() timed out and called cancel(), the defer
returned early without closing channels
- Result: Close() waits forever on cs.done at line 633
- Fix: remove early return from defer, always close channels
Root cause identified in issue #595:
- Session cleanup during /stop in cleanup window created conflicting
replies ("Previous request still processing" vs "No execution in progress")
- MCP servers and Stop hook subprocesses with stdio:inherit could
block scanner.Scan() indefinitely, preventing defer cleanup
Test plan:
- go build ./... passes
- go vet ./... passes
- go test ./... passes
Fixes#595
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>