fix(connection): preserve notification response ordering - #129
Conversation
11dcb7d to
a966b23
Compare
|
Taking this opportunity, I reread the implementation of connection & transport. It has existed since the initial version and was actually translated from TypeScript. I feel it's a bit too complicated. I'm currently discussing with my agent whether it can be simplified, and there should be a refactoring later. |
|
Please see #132 and check if the issue still exists or you need to update the PR. @hallerite |
df4063b to
d07f3e2
Compare
|
hey @frostming, |
|
This solution has a problem: you are associating all notifications received after a request is sent with that request. In reality, this is not the case. For example, the However, to achieve this kind of precise association, the I feel this is still better suited for the Agent/Client to implement and ensure that, What do you think? |
d07f3e2 to
3365b83
Compare
|
makes a lot of sense. I have refactored it following your recommendations and it's now also closer to the typescript implementation |
| except asyncio.CancelledError: | ||
| raise |
There was a problem hiding this comment.
Maybe a bit nitpicky, but this except branch can be deleted, because CancelledError won't be caught by the following except Exception.
| async def handler(method: str, params: Any, is_notification: bool) -> Any: | ||
| if is_notification and method == CLIENT_METHODS["session_update"]: | ||
| notification = SessionNotification.model_validate(params) | ||
| return await self._session_updates.handle( | ||
| notification.session_id, router(method, params, is_notification) | ||
| ) | ||
| return await router(method, params, is_notification) |
There was a problem hiding this comment.
Suggested change:
class _SessionUpdateTracker:
def __init__(self, client):
self._client = client
self._pending = {} # session_id -> set of unresolved futures
# no need to track the sequence numbers, all prompt requests of the same session
# can wait for the same set of futures.
async def session_update(self, session_id, update):
# tracking logic
return await self._client.session_update(session_id, update)
# other useful methods
def __getattr__(self, name):
"""delegate to the internal client methods"""
return getattr(self._client, name)And pass the tracker to the router builder:
handler = build_client_router(self._session_updates, ...)
we had a downstream issue in verifiers where an ACP request could complete before handlers for preceding notifications had finished. we patched it in our codebase by adding a wait, which is not optimal. this is my attempt of upstreaming a better fix.
Summary
Root cause
Connectionpublishes notifications to the asynchronous dispatcher, but handles responses directly in the receive loop. A notification followed immediately by its request response could therefore resolvesend_request()before the notification handler had updated client state. ACP consumers could observe a completed prompt with its final streamed content still missing.The response is now stored as soon as it arrives, while
send_request()waits on a per-request notification barrier before returning or raising. This keeps wire-order semantics without turning notification dispatch synchronous. Responses to nested requests exclude the notification that initiated them, avoiding a circular wait.The immediate response storage is important for transport shutdown: an EOF after a valid response can no longer replace that response with
ConnectionErrorwhile a preceding notification handler is still finishing.Downstream reproduction: PrimeIntellect-ai/verifiers#2262
Validation
make checkmake test(199 passed, 1 skipped)