You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
tests/runtime: TestRemoteEmitterReachesDispatch (SSE) intermittently hangs in CI until the 25-minute job timeout #200
Nothing in either module touches package identity.
Why it matters
Not a correctness bug in anything shipped, but it means "run the full suite" is currently satisfiable for 3154 tests and unsatisfied for these 8. That is a standing hole in the verification story: a hang is indistinguishable from a test that would have failed, and this repo has twice shipped a defect that a suite reported green through.
CI does not surface it — these are deselected there — so it will not appear as a red X.
Suggested next step
Determine whether the hang is environmental (a stdio harness waiting on a fd that never closes on this host) or a genuine deadlock in the harness. If environmental, the fix is a bounded timeout plus a skip with a stated reason, so the count is honest rather than silently deselected. If it is a real deadlock, it is a bug in the emitter/dispatch path worth its own fix.
Either way the outcome should be that the suite's own report distinguishes "passed", "skipped for stated reason", and "hung" — a deselect that hides a hang is the failure mode to remove.
Closing — I filed this on a subagent's report without reproducing it, and it does not reproduce. Both premises are wrong.
Measured on main at 3413db4 (v2.6.0):
run
result
pytest tests/runtime
60 passed in 75s
pytest tests/runtime/test_downstream_stdio.py
2 passed in 0.96s
pytest -q (full suite, runtime included)
3189 passed, 3 skipped, 25 deselected in 6m33s
No hang, standalone or under the full suite — so it is not cross-test pollution either.
Where the "8 deselected" came from. Not from CI or config: it was the reporting agent's own -k "not stdio" filter, which it applied because it believed those tests hang. Confirmed:
tests/runtime contains zerolive-marked tests, and the config's only deselector is -m 'not live'. So the claim that CI hides these tests is false — nothing is being silently excluded, and there is no gap in the verification story.
The 25 deselected in the full run are live-marked opt-in integration tests elsewhere in the suite, which is their intended behaviour.
I have not reproduced the environment in which the original hang was observed, so I cannot say the reporter saw nothing — a transient stdio-harness stall on a loaded host is plausible. But there is no evidence of a standing defect, and an open issue asserting one is worse than no issue. If it recurs, reopen with the exact command and the pytest -q output rather than a filtered run.
Reopening with CI evidence — the earlier close was right that the stdio tests don't hang and that nothing is deselected, but the class is real: the remote/SSE emitter-harness tests intermittently hang in CI until timeout-minutes: 25 kills the job (GitHub reports it as cancelled, not failed, so it never showed as a red X on the file).
Every timed-out test (3.x) job on main in the last two weeks stalled inside tests/runtime/test_emitter_harness.py::TestRemoteEmitterReachesDispatch, at 3% of the suite, with the last PASSED line being:
run
date
job
last test that passed before the 24-minute silence
So the hanging test is the next one in collection order each time — test_emit_alone_reaches_read_sse or test_remove_tool_then_emit_reaches_read_sse — and the job log ends with Terminate orphan process: pid (…) (pytest). The other two matrix jobs on the same commit pass in 4–5 min, and re-running the job passes, so it is order/timing-dependent, not deterministic. Same uv (0.12.8) in hung and passing runs; unrelated to any dependency bump.
Working hypothesis (unverified): the sse_starlette process-global AppStatus.should_exit_event latch — once set by an earlier SSE test's shutdown on a different event loop, a later read_sse waits forever. Local runs of tests/runtime alone pass (that is what the earlier close measured), which is consistent with the latch being set by something earlier in the full-suite order.
Suggested fix: a per-test pytest-timeout (or an asyncio.wait_for) on the SSE reads in this harness so a hang is a failure with a traceback in seconds instead of a 25-minute cancel; then reset the latch (AppStatus.should_exit_event = None) in the harness fixture if the hypothesis holds.
changed the title [-]tests/runtime: stdio-harness tests hang (8 tests, pre-existing)[/-][+]tests/runtime: TestRemoteEmitterReachesDispatch (SSE) intermittently hangs in CI until the 25-minute job timeout[/+]on Sep 1, 2026
Correction to my earlier comment in this issue, and stage 1 is up as #223.
My earlier suggestion here — reset AppStatus.should_exit_event = None — cannot be implemented as written. It describes an older sse_starlette. The installed version is 3.1.1, and vars(AppStatus) there is exactly:
There is no should_exit_event. The 3.1.1 mechanism is different in two ways that matter:
A per-event-loop _ShutdownState held in a contextvars.ContextVar — so an event registered on one loop is never signalled by a watcher on another.
_get_uvicorn_server(), which reads signal.getsignal(signal.SIGTERM).__self__ and polls that object'sshould_exit — so a stale SIGTERM handler left behind by an earlier uvicorn server can make a later loop's watcher see should_exit == True.
Both are plausible paths to a stuck stream. Neither is verified, which is exactly why #223 instruments rather than fixes. (Checked in two venvs, py 3.10.12 and py 3.14.7; both resolve 3.1.1 and neither sse.py contains threading.local.)
The process-global AppStatus.should_exit class attribute, and run_fake_remote's reset of it, are unaffected by the version correction — both are still exactly as described. The stale module comments in tests/runtime/fake_remote.py and tests/runtime/test_downstream_remote.py are corrected in #223.
#223 ships stage 1 only: diagnostics. It does not fix the hang, and touches nothing under src/. A stall now produces a stack dump at 2 minutes and a hard failure at ~11.7 minutes, instead of 25 minutes of silence reported as "cancelled":
faulthandler_timeout = 120 — dumps every thread's stack and lets the run continue.
timeout = 700, timeout_method = "signal" via pytest-timeout. signal was selected empirically over thread: both cover fixture teardown, but threados._exits the session and loses the rest of the run.
run_fake_remote's unbounded await task is bounded, and reports whether the serve() task survived cancellation — the one fact no stack dump can give you, since an idle loop parked in epoll.poll() names nothing.
One thing worth flagging for whoever picks up stage 2: "the next test hangs" is an inference, not a finding. pytest prints PASSED when the test function returns, before teardown. With asyncio_mode = "auto", pytest-asyncio then tears the loop down, and an async generator that ignores cancellation hangs there — producing a log indistinguishable from "the next test hung". The _tap() spy at test_emitter_harness.py:65 is itself an async generator wrapped around the SSE read stream, so the just-passed test's teardown is a live candidate. That is why the teardown-coverage half of #223 is the half that matters.
Stage 2 is filed separately and starts when an occurrence produces a traceback.
Closed by #223 (merged as 274d3c2) — stage 1 only, as scoped.
A hang in tests/runtime is now a fast, named failure instead of a 25-minute silent job cancel: a bounded stop sequence in run_fake_remote (10s + 5s cancel grace, using asyncio.wait — wait_for cannot bound a cancellation-resistant task), an asyncio task-stack watchdog at 60s that walks the cr_await/ag_await chain (neither faulthandler nor pytest-timeout can see a suspended coroutine — both bottom out in epoll.poll), faulthandler_timeout = 120, and pytest-timeout at 700s with timeout_method = "signal" chosen by an empirical teardown-coverage proof. Nine tests pin all of it in both directions; evidence in .consiliency/evidence/hang-diagnostics-200.md.
It worked on its first CI run and produced the traceback this issue existed to capture — posted to #224, which carries the root cause from here. Two things it settled: the server is not cancellation-resistant (it ends the moment it is cancelled) but will not drain, because uvicorn.Config here has no timeout_graceful_shutdown; and underneath it the SSE transport dies mid-stream with unhandled errors in a TaskGroup (1 sub-exception) whose sub-exception is swallowed at five call sites.
Correcting my own earlier comment on this issue for the record: the should_exit_event hypothesis was wrong (3.1.1 has no such attribute), and "the next test hangs" was an inference, not a finding — the failure landed on the 4th test of TestRemoteEmitterReachesDispatch, while 3 of the 5 recorded stalls name the 3rd as their last PASSED.
Activity
ViperJuice commentedon Aug 27, 2026
Closing — I filed this on a subagent's report without reproducing it, and it does not reproduce. Both premises are wrong.
Measured on
mainat3413db4(v2.6.0):pytest tests/runtimepytest tests/runtime/test_downstream_stdio.pypytest -q(full suite, runtime included)No hang, standalone or under the full suite — so it is not cross-test pollution either.
Where the "8 deselected" came from. Not from CI or config: it was the reporting agent's own
-k "not stdio"filter, which it applied because it believed those tests hang. Confirmed:tests/runtimecontains zerolive-marked tests, and the config's only deselector is-m 'not live'. So the claim that CI hides these tests is false — nothing is being silently excluded, and there is no gap in the verification story.The 25 deselected in the full run are
live-marked opt-in integration tests elsewhere in the suite, which is their intended behaviour.I have not reproduced the environment in which the original hang was observed, so I cannot say the reporter saw nothing — a transient stdio-harness stall on a loaded host is plausible. But there is no evidence of a standing defect, and an open issue asserting one is worse than no issue. If it recurs, reopen with the exact command and the
pytest -qoutput rather than a filtered run.ViperJuice commentedon Sep 1, 2026
Reopening with CI evidence — the earlier close was right that the stdio tests don't hang and that nothing is deselected, but the class is real: the remote/SSE emitter-harness tests intermittently hang in CI until
timeout-minutes: 25kills the job (GitHub reports it as cancelled, not failed, so it never showed as a red X on the file).Every timed-out
test (3.x)job onmainin the last two weeks stalled insidetests/runtime/test_emitter_harness.py::TestRemoteEmitterReachesDispatch, at 3% of the suite, with the last PASSED line being:test_add_tool_then_emit_reaches_read_ssetest_unrecognised_method_reaches_read_ssetest_unrecognised_method_reaches_read_ssetest_add_tool_then_emit_reaches_read_ssetest_unrecognised_method_reaches_read_sseSo the hanging test is the next one in collection order each time —
test_emit_alone_reaches_read_sseortest_remove_tool_then_emit_reaches_read_sse— and the job log ends withTerminate orphan process: pid (…) (pytest). The other two matrix jobs on the same commit pass in 4–5 min, and re-running the job passes, so it is order/timing-dependent, not deterministic. Sameuv(0.12.8) in hung and passing runs; unrelated to any dependency bump.Working hypothesis (unverified): the
sse_starletteprocess-globalAppStatus.should_exit_eventlatch — once set by an earlier SSE test's shutdown on a different event loop, a laterread_ssewaits forever. Local runs oftests/runtimealone pass (that is what the earlier close measured), which is consistent with the latch being set by something earlier in the full-suite order.Suggested fix: a per-test
pytest-timeout(or anasyncio.wait_for) on the SSE reads in this harness so a hang is a failure with a traceback in seconds instead of a 25-minute cancel; then reset the latch (AppStatus.should_exit_event = None) in the harness fixture if the hypothesis holds.[-]tests/runtime: stdio-harness tests hang (8 tests, pre-existing)[/-][+]tests/runtime: TestRemoteEmitterReachesDispatch (SSE) intermittently hangs in CI until the 25-minute job timeout[/+]ViperJuice commentedon Sep 2, 2026
Correction to my earlier comment in this issue, and stage 1 is up as #223.
My earlier suggestion here — reset
AppStatus.should_exit_event = None— cannot be implemented as written. It describes an oldersse_starlette. The installed version is 3.1.1, andvars(AppStatus)there is exactly:There is no
should_exit_event. The 3.1.1 mechanism is different in two ways that matter:_ShutdownStateheld in acontextvars.ContextVar— so an event registered on one loop is never signalled by a watcher on another._get_uvicorn_server(), which readssignal.getsignal(signal.SIGTERM).__self__and polls that object'sshould_exit— so a stale SIGTERM handler left behind by an earlier uvicorn server can make a later loop's watcher seeshould_exit == True.Both are plausible paths to a stuck stream. Neither is verified, which is exactly why #223 instruments rather than fixes. (Checked in two venvs, py 3.10.12 and py 3.14.7; both resolve 3.1.1 and neither
sse.pycontainsthreading.local.)The process-global
AppStatus.should_exitclass attribute, andrun_fake_remote's reset of it, are unaffected by the version correction — both are still exactly as described. The stale module comments intests/runtime/fake_remote.pyandtests/runtime/test_downstream_remote.pyare corrected in #223.#223 ships stage 1 only: diagnostics. It does not fix the hang, and touches nothing under
src/. A stall now produces a stack dump at 2 minutes and a hard failure at ~11.7 minutes, instead of 25 minutes of silence reported as "cancelled":faulthandler_timeout = 120— dumps every thread's stack and lets the run continue.timeout = 700,timeout_method = "signal"viapytest-timeout.signalwas selected empirically overthread: both cover fixture teardown, butthreados._exits the session and loses the rest of the run.run_fake_remote's unboundedawait taskis bounded, and reports whether theserve()task survived cancellation — the one fact no stack dump can give you, since an idle loop parked inepoll.poll()names nothing.One thing worth flagging for whoever picks up stage 2: "the next test hangs" is an inference, not a finding. pytest prints
PASSEDwhen the test function returns, before teardown. Withasyncio_mode = "auto", pytest-asyncio then tears the loop down, and an async generator that ignores cancellation hangs there — producing a log indistinguishable from "the next test hung". The_tap()spy attest_emitter_harness.py:65is itself an async generator wrapped around the SSE read stream, so the just-passed test's teardown is a live candidate. That is why the teardown-coverage half of #223 is the half that matters.Stage 2 is filed separately and starts when an occurrence produces a traceback.
test(runtime): dump asyncio task stacks so an async hang names its aw…
test(runtime): make a hang fail fast with a traceback instead of a 25…
ViperJuice commentedon Sep 2, 2026
Closed by #223 (merged as 274d3c2) — stage 1 only, as scoped.
A hang in
tests/runtimeis now a fast, named failure instead of a 25-minute silent job cancel: a bounded stop sequence inrun_fake_remote(10s + 5s cancel grace, usingasyncio.wait—wait_forcannot bound a cancellation-resistant task), an asyncio task-stack watchdog at 60s that walks thecr_await/ag_awaitchain (neither faulthandler nor pytest-timeout can see a suspended coroutine — both bottom out inepoll.poll),faulthandler_timeout = 120, andpytest-timeoutat 700s withtimeout_method = "signal"chosen by an empirical teardown-coverage proof. Nine tests pin all of it in both directions; evidence in.consiliency/evidence/hang-diagnostics-200.md.It worked on its first CI run and produced the traceback this issue existed to capture — posted to #224, which carries the root cause from here. Two things it settled: the server is not cancellation-resistant (it ends the moment it is cancelled) but will not drain, because
uvicorn.Confighere has notimeout_graceful_shutdown; and underneath it the SSE transport dies mid-stream withunhandled errors in a TaskGroup (1 sub-exception)whose sub-exception is swallowed at five call sites.Correcting my own earlier comment on this issue for the record: the
should_exit_eventhypothesis was wrong (3.1.1 has no such attribute), and "the next test hangs" was an inference, not a finding — the failure landed on the 4th test ofTestRemoteEmitterReachesDispatch, while 3 of the 5 recorded stalls name the 3rd as their last PASSED.fix(client): name the cause inside an exception group in every log (see
test: put faulthandler above the kill, exit on it, and stop the root-…