fix(acp): return JSON-RPC errors from dispatch handlers instead of killing the connection - #10499
Merged
Conversation
…lling the connection The ACP SDK treats an Err escaping a cx.spawn()ed task as fatal and tears down the entire connection, which clients surface as "ACP connection closed". Three handler paths in dispatch.rs used ? to propagate errors out of their spawned tasks: - set_config_option: build_config_update() runs after every successful provider/model/mode/thinking-effort change; if it errored (missing session, unbuildable provider, provider absent from inventory) the whole connection dropped right after a settings switch. Now responds with the error and logs a warning. - set_config_option: a value that is not a value ID killed the connection instead of returning invalid_params. Now responds with invalid_params. - close_session: on_close_session() errors were wired to tear down the connection (latent; its only fallible call currently always returns Ok). Now responds with the error, matching the list_sessions pattern. Audited the remaining dispatch.rs handlers (the only HandleDispatchFrom/cx.spawn sites in the repo): all other spawned tasks already route handler errors through respond_with_error / respond_with_result, and errors returned from the non-spawned handler path are reported back to the client by the SDK's incoming actor, so they are not fatal. Remaining ? on responder.respond*/send_notification calls only fail when the transport is already gone. Adds a regression test asserting that a set_config_option with a boolean (non-value-ID) value yields an invalid_params error response and that a subsequent request on the same connection still succeeds. Verified the test hangs on the unfixed code (the request never gets a response once the connection tears down) and passes with the fix. Verified with cargo check -p goose, cargo test -p goose --test acp_server_test (48 passed), and cargo clippy --all-targets. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Matt Toohey <contact@matttoohey.com>
lifeizhou-ap
approved these changes
Jul 17, 2026
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.
Problem
The ACP SDK treats an
Errescaping acx.spawn()ed task as fatal: the task actor sits in the sametry_join!as the connection's incoming/outgoing actors, so the whole connection tears down and clients surface "ACP connection closed". Three handler paths incrates/goose/src/acp/server/dispatch.rsused?to propagate errors out of their spawned tasks:set_config_option—build_config_update()runs after every successful provider/model/mode/thinking-effort change. If it errored (missing session, unbuildable provider, provider absent from inventory), the connection dropped right after a settings switch. Now responds with the error and logs a warning.set_config_option— a value that is not a value ID killed the connection instead of returninginvalid_params. Now responds withinvalid_params.close_session—on_close_session()errors were wired to tear down the connection (latent; its only fallible call currently always returnsOk). Now responds with the error, matching thelist_sessionspattern.How we got here
These three sites were outliers, and the history explains why they survived: the meaning of
?in these positions changed twice without the lines themselves ever changing.?s as inline handler bodies. Under thesacpSDK pinned at the time, any error escaping a handler closure was already connection-fatal — but that was true of every handler, SDK-wide.cx.spawnso slow handlers don't serialize the dispatch loop. The?s were carried verbatim into the spawned tasks. (The same diff convertedfork_sessionto the saferespond_with_result.)agent-client-protocolSDK, whose incoming actor reports non-spawned handler errors back to the client as JSON-RPC errors (report_handler_error). That silently de-fanged every inline?in the file — but not these three, which had been moved insidecx.spawna week earlier. Spawned-task errors still propagate throughtask_actorand kill the connection.By the pre-fix state,
dispatch.rshad 14 call sites correctly usingrespond_with_error/respond_with_resultversus these 3 bare?s — including five correct match arms inside the sameset_config_optionhandler, directly above the buggybuild_config_update(...).await?. This change brings the last three in line with the file's convention.I audited the remaining
dispatch.rshandlers (the onlyHandleDispatchFrom/cx.spawnsites in the repo): all other spawned tasks already route handler errors through the responder, and errors from the non-spawned handler path are reported back by the SDK's incoming actor, so they are not fatal. The remaining?s onresponder.respond*/send_notificationonly fail when the transport is already gone.One known trade-off: when
build_config_update()fails, the underlying config change has already been applied server-side, so the client receives an error for a change that actually took effect and may render a stale value. That's strictly better than tearing down the connection, and thewarn!log makes it diagnosable.Testing
set_config_optionwith a boolean (non-value-ID) value yields aninvalid_paramserror response, and a subsequent request on the same connection still succeeds. Verified the test hangs on the unfixed code (the request never gets a response once the connection tears down) and passes with the fix.cargo test -p goose --test acp_server_test(48 passed),cargo check -p goose,cargo clippy --all-targets.🤖 Generated with Claude Code