app-server: harden disconnect cleanup paths (#12218)

Hardens codex-rs/app-server connection lifecycle and outbound routing
for websocket clients. Fixes some FUD I was having

- Added per-connection disconnect signaling (CancellationToken) for
websocket transports.
- Split websocket handling into independent inbound/outbound tasks
coordinated by cancellation.
- Changed outbound routing so websocket connections use non-blocking
try_send; slow/full websocket writers are disconnected instead of
stalling broadcast delivery.
- Kept stdio behavior blocking-on-send (no forced disconnect) so local
stdio clients are not dropped when queues are temporarily full.
- Simplified outbound router flow by removing deferred
pending_closed_connections handling.
- Added guards to drop incoming response/notification/error messages
from unknown connections.
- Fixed listener teardown race in thread listener tasks using a
listener_generation check so stale tasks do not clear newer listeners.

Fixes
https://linear.app/openai/issue/CODEX-4966/multiclient-handle-slow-notification-consumers

  ## Tests

  Added/updated transport tests covering:

  - broadcast does not block on a slow/full websocket connection
  - stdio connection waits instead of disconnecting on full queue

I (maxj) have tested manually and will retest before landing
This commit is contained in:
Max Johnson
2026-02-20 12:35:16 -08:00
committed by GitHub
Unverified
parent d3cf8bd0fa
commit 6b1091fc92
6 changed files with 372 additions and 87 deletions
@@ -5811,7 +5811,7 @@ impl CodexMessageProcessor {
api_version: ApiVersion,
) {
let (cancel_tx, mut cancel_rx) = oneshot::channel();
let mut listener_command_rx = {
let (mut listener_command_rx, listener_generation) = {
let mut thread_state = thread_state.lock().await;
if thread_state.listener_matches(&conversation) {
return;
@@ -5927,6 +5927,11 @@ impl CodexMessageProcessor {
}
}
}
let mut thread_state = thread_state.lock().await;
if thread_state.listener_generation == listener_generation {
thread_state.clear_listener();
}
});
}
async fn git_diff_to_origin(&self, request_id: ConnectionRequestId, cwd: PathBuf) {