RpcServer.Protocol.disconnects is exposed as a Queue.Dequeue<number>, but RpcServer.make also consumes that queue internally to interrupt a disconnected client's in-flight handlers. An application that takes from the queue to observe passive disconnects competes with the server and can prevent handler cleanup.
Reproduction
https://github.com/schickling-repros/2026-09-effect-rpc-disconnect-observer
bun install
bun run repro
The repro uses a minimal in-memory protocol and runs a control plus a competing-observer case. It synchronizes on queue takers and handler lifecycle signals directly; no timing sleeps are involved.
Output:
control: both disconnected handlers were interrupted
competing observer: observer stole client 1; only client 2 was interrupted
Expected
There is a non-destructive way to observe passive client disconnects while preserving RpcServer.make's lifecycle handling.
Actual
The event-like API is the same work-sharing queue used by the server. Queue.take(protocol.disconnects) can steal a notification, so server.disconnect(clientId) is not called and that client's handler remains active. Protocol.clientIds is a snapshot and cannot provide equivalent event observation.
An additive callback on RpcServer.make (invoked after the server handles a disconnect) could expose this without changing the Protocol transport shape or queue semantics.
This was reproduced with effect@4.0.0-rc.112, and the same single-consumer flow is present on current main.
Related: #7342 fixed another path where a lost disconnect prevented handler cleanup.
Posted on behalf of @schickling
| field |
value |
agent_identity |
dev3.direct.omp.pdvr6amr |
session |
unknown |
agent_persona |
generalist |
agent_supervisor |
unavailable |
agent_tool |
OMP |
agent_tool_version |
18.2.11 |
agent_runtime |
OMP 18.2.11 |
agent_model |
openai-codex/gpt-5.6-sol |
worktree |
2026-09-effect-rpc-disconnect-observer/main |
tooling_profile |
dotfiles@b0f94e3-dirty |
RpcServer.Protocol.disconnectsis exposed as aQueue.Dequeue<number>, butRpcServer.makealso consumes that queue internally to interrupt a disconnected client's in-flight handlers. An application that takes from the queue to observe passive disconnects competes with the server and can prevent handler cleanup.Reproduction
https://github.com/schickling-repros/2026-09-effect-rpc-disconnect-observer
The repro uses a minimal in-memory protocol and runs a control plus a competing-observer case. It synchronizes on queue takers and handler lifecycle signals directly; no timing sleeps are involved.
Output:
Expected
There is a non-destructive way to observe passive client disconnects while preserving
RpcServer.make's lifecycle handling.Actual
The event-like API is the same work-sharing queue used by the server.
Queue.take(protocol.disconnects)can steal a notification, soserver.disconnect(clientId)is not called and that client's handler remains active.Protocol.clientIdsis a snapshot and cannot provide equivalent event observation.An additive callback on
RpcServer.make(invoked after the server handles a disconnect) could expose this without changing theProtocoltransport shape or queue semantics.This was reproduced with
effect@4.0.0-rc.112, and the same single-consumer flow is present on currentmain.Related: #7342 fixed another path where a lost disconnect prevented handler cleanup.
Posted on behalf of @schickling
agent_identitysessionagent_personaagent_supervisoragent_toolagent_tool_versionagent_runtimeagent_modelworktreetooling_profile