Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e2df0af5e0
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
e2df0af to
025294e
Compare
anishmehta24
left a comment
There was a problem hiding this comment.
One gap: if the worker task is cancelled while a command is in flight, anything still sitting in the queue is never settled, so a caller awaiting a queued cleanup() hangs forever. Draining the queue and cancelling those futures on the way out (a finally around the loop) covers it.
Repro: start a connect that blocks, queue a cleanup(), cancel worker._task, then await the cleanup future with a timeout.
|
Checked this against the two cases from #5054, at The split between the two kinds of The gap @anishmehta24 raised reproduces. With a Reproimport asyncio
from typing import Any, cast
import pytest
from agents.mcp import manager as manager_module
class BlockingConnectServer:
def __init__(self) -> None:
self.connect_started = asyncio.Event()
self.cleanup_calls = 0
async def connect(self) -> None:
self.connect_started.set()
await asyncio.sleep(3600)
async def cleanup(self) -> None:
self.cleanup_calls += 1
@pytest.mark.asyncio
async def test_queued_cleanup_is_settled_when_worker_task_is_cancelled() -> None:
server = BlockingConnectServer()
worker = manager_module._ServerWorker(cast(Any, server))
connect_caller = asyncio.create_task(worker.connect(timeout_seconds=None))
await asyncio.wait_for(server.connect_started.wait(), timeout=2)
cleanup_caller = asyncio.create_task(worker.cleanup(timeout_seconds=None))
await asyncio.sleep(0.05)
worker._task.cancel()
# On 025294e2ce this times out: the queued cleanup future is never settled.
await asyncio.wait_for(asyncio.shield(cleanup_caller), timeout=2) |
025294e to
00f613b
Compare
|
thanks for checking this and for the repro steps @anishmehta24 @Rehansanjay, the worker loop now drains the queue when it exits so queued callers get CancelledError instead of hanging, adopted in 00f613b |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 00f613b888
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
jbeckwith-oai
left a comment
There was a problem hiding this comment.
Requesting changes for the two inline findings: cancellation can abandon cleanup of a partially acquired server connection, and direct worker-task construction bypasses the configured asyncio task factory.
Reviewed the complete diff against main at 5f9899d, including independent reviews and controlled baseline-versus-PR lifecycle probes. The latest queue-draining change fixes the queued-waiter hang, but it does not address the remaining resource-cleanup issue.
Validation: all 83 manager tests passed on both Python 3.10 and Python 3.12 using the exact PR module and test snapshots. The broader MCP suite was interrupted and remains unverified.
00f613b to
87938c6
Compare
|
thanks for the review @jbeckwith-oai, both findings fixed in 87938c6, the worker now runs the server cleanup in the same task before re-raising the cancellation so a partially acquired connection gets released, and worker creation goes through the loop's task factory with the cancel counting kept on the worker side so 3.10 still works |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 87938c6429
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
87938c6 to
6590009
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6590009367
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
6590009 to
a8c0467
Compare
jbeckwith-oai
left a comment
There was a problem hiding this comment.
Reviewed a8c0467238d6b618ec1f5f119ea5a05bb4659adb in its pinned review worktree.
The external cancellation hang is real. The current head separates task cancellation from server-raised CancelledError, performs emergency cleanup in the owning task with the manager cleanup timeout, settles queued callers, and preserves the configured task factory. The earlier cleanup and task-factory findings are addressed. Integration with #5011 will need to preserve this cancellation distinction.
Validation: complete-diff and supported-path desk review, two independent fresh-context review rounds, and git diff --check. Recorded hosted checks are green. No local runtime tests were executed.
seratch
left a comment
There was a problem hiding this comment.
Thanks for addressing the earlier cleanup, task-factory, and timeout findings. One lifecycle issue remains: when emergency cleanup raises or times out, its failure is swallowed without recording a cleanup outcome. The done callback then removes the worker, and a later reconnect() can start another connection without successful cleanup of the previous one.
Please retain the emergency-cleanup failure through the existing cleanup-result mechanism while still delivering the original cancellation to the connect caller. Add a manager-level regression covering cancellation during partial startup, failed or timed-out emergency cleanup, and subsequent reconnect, asserting that the failure remains observable and no second connection starts. This preserves the existing behavior that reconnect does not proceed after cleanup failure.
_ServerWorker._run wrapped command execution in except BaseException, so an external task.cancel() was recorded on the command future and the worker went back to the queue loop while staying in the cancelling state. is_done never flipped and anything awaiting the task, including loop shutdown, hung The loop now compares the worker task cancellation count before and after each command and re-raises CancelledError only when that count grew while the command ran, which keeps a server raising CancelledError on its own recoverable through the future while an external cancel ends the worker. The count is carried by the worker itself and every cancel() on the task is counted through a shim installed at creation time, so the distinction works on Python 3.10 too, where Task.cancelling() does not exist, and the worker task is still created through the event loop's task factory so applications supervising tasks through set_task_factory keep seeing it When the external cancel lands while a command runs, the worker runs the server cleanup in the same task before re-raising, so a partially acquired connection from an interrupted connect is released instead of leaked. That emergency cleanup is bounded by the manager cleanup timeout instead of the interrupted connect command's own timeout, since connect and cleanup have separate allowances and an unbounded cleanup could still hang loop shutdown A failed or timed-out emergency cleanup used to be swallowed silently, so the worker still looked cleanly stopped: the done callback discarded it, and a later reconnect started a second connection over the transport that was never cleaned up. The failure is now recorded on the cleanup result future, which keeps the worker registered, keeps the failure observable through the manager errors, and keeps reconnect from retrying the uncleaned server The worker loop also settles the futures of commands still queued when it exits for any reason, so callers waiting on a queued command get CancelledError instead of hanging forever
a8c0467 to
ac0afe1
Compare
Summary
Cancelling the parallel mode worker task did not stop it.
_ServerWorker._runwraps command execution inexcept BaseExceptionandasyncio.CancelledErroris aBaseException, so an external cancel got recorded on the command future and the loop went back toawait self._queue.get()with the task stuck in the cancelling state.is_donestayed false and anything awaiting the task, including interpreter shutdown, hungThe fix snapshots
Task.cancelling()before each command and re-raisesCancelledErroronly when that count grew while the command ran, so a cancel delivered from outside ends the task while a server raisingCancelledErroron its own stays recoverable through the command future. That keeps the contract the existingCancelledServertests pin, and on Python 3.10, which has noTask.cancelling(), behavior stays as beforeOne interaction to flag: #5011 forwards caller cancels into the worker task for connect commands and needs the worker to survive them. If that PR merges first, its
cancelled_by_callerflag can skip the new re-raise branch. This diff is written against main without that change and passes the existing cancellation tests as isTest plan
test_worker_task_cancellation_stops_worker_and_fails_callercancels the worker task while connect runs and asserts the caller seesCancelledErrorand the task ends cancelled. on main it fails withAssertionError: the worker task survived cancellation and is still looping, with the fix it passestest_worker_survives_server_raised_cancelled_error_and_serves_next_commandpins the other side, aCancelledErrorthe server raises on its own reaches the caller through the future and the worker serves the next command and cleanuptests/mcp/475 passed, 24 skipped, the skips are live server integration. thetest_server_errors.pyfile needs httpx, an optional dep missing in my sandbox until installed, unrelated to this changeruff checkandruff format --checkclean,mypy src/agents/mcp/manager.pyclean under strict modemake format,make lintandmake typecheckpass, andmake testssplits as 9606 passed in the parallel run with 14 pre-existing failures plus 77 passed in the serial run. the 14 are unrelated to this change: running the same commands on a pristine checkout of main gives the identical failures, they live in the repo's own code change verification runner tests and this diff never touches that fileIssue number
Closes #5054
Checks
.agents/skills/code-change-verification/scripts/run.sh