Repository navigation
fix: answer pending permission requests when the client cancels or closes a session - #156
Open
EugeneTheDev wants to merge 1 commit into
Open
EugeneTheDev wants to merge 1 commit into
EugeneTheDev wants to merge 1 commit into
Conversation
EugeneTheDev
force-pushed
the
eugenethedev/permission-cancellation-fix
branch
from
November 3, 2026 17:51
3fa9ce3 to
8bbf921
Compare
…oses a session The v1 client runtime now answers every pending `session/request_permission` of a session with the `cancelled` outcome when the app calls `ClientSession.cancel()` or `close()`, as `prompt-turn.mdx` requires for `session/cancel`. It no longer waits for the agent's optional `$/cancel_request`. The v2 runtime already did this on cancel; it now does it on `close()` too. - `PendingPermissionCancellation` holds one cancel signal per session and races each permission handler against it. The race is the only answering path, so each request gets exactly one response. The app's handler is cancelled and the reply goes out once it has finished. - `cancel()` completes the signal in a `finally`, so a failed `session/cancel` send still releases pending requests. Requests that arrive after it and before the next turn are answered without reaching the app. v1 `prompt()` resets the signal only after its active-prompt check passes. - `close()` completes the signal before sending `session/close`, so an agent whose close waits for its turn cannot deadlock. A closed session stays cancelled through a `CLOSED` sentinel that no later turn replaces. A failed close still propagates its error. - KDoc for `cancel()`, `close()` and both permission handler methods describes the behaviour. `AGENTS.md` gains a lifecycle rule for it. Tests cover cancel, several pending requests, a failed send, requests after cancel, a rejected concurrent prompt, independent sessions, an app answer before cancel, close before its response, close failure, and a prompt after close, for v1. For v2 they cover close with non-compliant and compliant agents in both orders, close failure, and a prompt after close. A cross-transport test checks that an SDK agent gets the `cancelled` outcome over STDIO and WebSocket. Spec checked: `agent-client-protocol` at `dc011f0`. Design: docs/superpowers/specs/2026-11-03-permission-cancellation-design.md
EugeneTheDev
force-pushed
the
eugenethedev/permission-cancellation-fix
branch
from
November 4, 2026 23:16
8bbf921 to
9941000
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When a v1 client sends
session/cancel, it MUST answer every pendingsession/request_permissionof that session with thecancelledoutcome (docs/protocol/v1/prompt-turn.mdx). The v1 client runtime didn't: a pending request was released only if the agent sent the optional$/cancel_request, so the app had to track and answer its own requests next to everycancel().close()had the same gap in both runtimes. The v2 runtime already handled cancel; this brings v1 in line and covers close in both.Fixes
PendingPermissionCancellationholds one cancel signal per session and races each permission handler against it. The race is the only answering path, so each request gets exactly one response. The app's handler is cancelled, and thecancelledreply goes out once it has finished.cancel()(v1) completes the signal in afinally, so a failedsession/cancelsend still releases pending requests. Requests arriving after it and before the next turn are answeredcancelledwithout reaching the app. v1prompt()resets the signal only after its active-prompt check passes, so a rejected concurrent prompt can't undo a cancel.close()(v1 and v2) completes the signal before sendingsession/close. Otherwise an agent whose close waits for its turn would deadlock with a client waiting for the close response. A closed session stays cancelled through aCLOSEDsentinel that no later turn replaces. A failed close still propagates its error; the holder (v1) andonClosed()(v2) still wait for a successful response.cancel()behaviour is unchanged.cancel(),close()and both permission handler methods describes the new behaviour, including that handlers must respond to cancellation promptly.AGENTS.mdgains a lifecycle rule.acp/api/acp.apiis unchanged.Relation to #155
Independent and can merge in either order. With this change, an agent whose cooperative
AgentSession.cancel()waits for its turn receives the client'scancelledanswer instead of waiting for the user. Expect a trivialAGENTS.mdconflict in the lifecycle list; keep both bullets.Verification
ClientPermissionCancellationTest), v2 close tests inclient/v2/ClientSessionTest, and a cross-transport test inSimpleAgentTest(STDIO and WebSocket). Against the pre-fix runtime, the v1 tests (apart from the control) and the integration test fail../gradlew :acp:apiCheckand./gradlew checkpass.