Repository navigation
Rejecting a command or file edit can only end the turn when Codex doesn't offer decline #556
Description
Summary
A client that rejects one action often wants the agent to carry on without it ("skip this, keep going"), which is what Codex's own decline decision does. codex-acp only offers that option when Codex puts decline in availableDecisions. In several common cases it doesn't:
- File changes:
fileChangeDecisionOptions()always returns allow once, allow for these files and cancel.docs/permission-extension.mdnotes thatFileChangeApprovalDecisionincludesdecline, but "the native Codex file-change prompt does not currently advertise it". - Escalation requests in
read-only: when the model asks to run a write command outside the sandbox, Codex sendsavailableDecisionsof["accept", {acceptWithExecpolicyAmendment}, "cancel"]. - Commands under
approvalPolicy: "untrusted"(see No mode asks before every command and file edit (anuntrusted-style preset) #555): the same three decisions, for every command.
In all of these, the only reject_once option is cancel. Selecting it interrupts the turn, and session/prompt returns stopReason: "cancelled" although the client never sent session/cancel.
I understand from #405 and docs/permission-extension.md that the adapter deliberately "does not invent replacement choices", and from #26 that interrupting after a rejected file edit is intended for clients where the user then types what to do differently. So the proposal below keeps that contract by default and makes the extra option opt-in.
Reproduction
A raw ACP client over stdio, a fresh git repo as cwd, fresh session per action, gpt-6-luna at low effort.
| codex-acp / Codex | Mode | Client opted in | Action | Options offered | Answer | Written? | stopReason |
|---|---|---|---|---|---|---|---|
| 1.13.1 / 0.156.1 | untrusted forced onto a mode |
– | echo hi > shell.txt |
allow_once, accept_execpolicy_amendment, cancel |
cancel |
no | cancelled |
| 1.13.1 / 0.156.1 | untrusted forced onto a mode |
– | apply_patch |
allow_once, allow_for_session, cancel |
cancel |
no | cancelled |
| 1.13.2-preview.5 / 0.157.1 | untrusted forced onto a mode |
– | echo hi > shell.txt |
allow_once, accept_execpolicy_amendment, cancel |
cancel |
no | cancelled |
main + proposed change / 0.157.0 |
read-only |
no | apply_patch |
allow_once, allow_for_session, cancel |
cancel |
no | cancelled |
main + proposed change / 0.157.0 |
read-only |
yes | apply_patch |
allow_once, allow_for_session, decline, cancel |
decline |
no | end_turn |
main + proposed change / 0.157.0 |
read-only |
yes | echo hi > shell.txt (escalation) |
allow_once, accept_execpolicy_amendment, decline, cancel |
decline |
no | end_turn |
main + both proposed changes / 0.157.0 |
ask-always |
yes | echo hi > shell.txt |
allow_once, accept_execpolicy_amendment, decline, cancel |
decline |
no | end_turn |
main + both proposed changes / 0.157.0 |
ask-always |
yes | apply_patch |
allow_once, allow_for_session, decline, cancel |
decline |
no | end_turn |
main + both proposed changes / 0.157.0 |
ask-always |
yes | echo hi > shell.txt |
allow_once, accept_execpolicy_amendment, decline, cancel |
cancel |
no | cancelled |
After decline, the model replied that the action was rejected and finished the turn normally, so the app-server accepts decline for both request types even when it doesn't advertise it. The same held on 1.13.1 / 0.156.1 and 1.13.2-preview.5 / 0.157.1, with decline added by a local adapter change. Nothing here changed between Codex 0.156.1 and 0.157.1.
Our client, Spark Outpost, found this while routing every Codex action through its own approval step: a user's "no" to one command ended the whole run.
Expected
A way to get a reject_once option that rejects the single action and lets the turn continue, as decline already does whenever Codex advertises it ("No, continue without running it").
A smaller, related point: ACP defines stopReason: "cancelled" as "The turn was cancelled by the client via session/cancel". Returning it after a permission rejection, with no session/cancel, makes it hard for clients to tell "the user stopped the run" from "the user rejected one action".
Proposal
An opt-in client capability. A client that sets clientCapabilities._meta.continueOnReject: true on initialize gets Codex's decline right before cancel:
- in command and network approvals whose decision set has
cancelbut nodecline(labelledNo, continue without running it); - in every file-change approval (labelled
No, continue without making these edits).
decline is never added twice, never to a decision set without cancel, and never to the legacy additional-permissions fallback. cancel, and an ACP cancelled outcome, still map to cancel. Without the capability nothing changes, so #405's contract and #26's interrupt flow stay the default. A draft PR implementing this, with tests, references this issue.
Alternatives we considered:
- Always offer
declinebeforecancel(no capability). Smallest change, but it changes behaviour for clients that answer with "the firstreject_onceoption", and departs from "authoritativeavailableDecisions" for everyone. - Fix it in Codex: the app-server advertises
declineinavailableDecisionsforuntrustedand escalation requests, and addsavailableDecisionsto file-change requests. Then codex-acp needs no change for commands, because it already mapsdecline. We can file this in openai/codex if you'd rather go that way.
Would you accept the opt-in, or prefer one of the alternatives?
Activity