Repository navigation
fix(codex): a Codex Stop is the interrupt alone, so it no longer says "Cancellation was not confirmed" - #23850
Conversation
…ot confirmed" Codex answers a turn's interrupt only as that turn ends, then sends the turn's own end. Orca also required its sweep of the turn's processes to prove them gone, and when that sweep could not read the process table it wrote "Cancellation was not confirmed." a few milliseconds before the turn's interrupted end arrived. A Stop that named its turn said "The provider had already finished this turn." in the same case. Codex's answer is now what confirms the Stop. The sweep still runs and still holds the turn's end until it finishes, but its result is logged, not shown. A Stop Codex never answers, which is a turn that never ends, still reads "Cancellation was not confirmed."
…n, for a steer and for a turn's own input
|
| if (acknowledged) { | ||
| if (!terminated) { | ||
| console.warn('[codex-structured] could not prove an interrupted turn left no processes') |
There was a problem hiding this comment.
Stop can leave a process running
The process sweep returns false both when it cannot read the process table and when it sees a turn-started process still alive at its deadline. If Codex answers the interrupt in the latter case, this branch only warns, releases the turn completion, and returns cancelled: true. There is no later per-turn sweep, so Stop can appear successful while that process keeps running. Please distinguish a proven-live process from an unreadable process table.
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Advanced Run ID: 📒 Files selected for processing (3)
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review. 📝 WalkthroughWalkthroughCodex cancellation now sends an interrupt directly and treats a successful interrupt response as confirmed cancellation. Turn-end notifications are delivered without cancellation deferral. The cancellation outcome type no longer includes an unconfirmed state, and failed cancellation uses the stop-refused status path. The adapter no longer captures or terminates turn processes. Tests cover interrupt responses, turn completion, Stop requests with and without a turn ID, and interrupt timeouts. Comments describe how completion and interruption affect pending input. Priority: ➖ Normal Merge Risk: ⚪ Minimal · up to The change makes Stop reporting more accurate by treating Codex's interrupt answer as confirmation. No concrete merge-blocking issue is established in the supplied evidence, though a narrow prompt-ownership edge case raised earlier is worth a follow-up check. Security Architecture ReviewSecurity architecture risk: 🟡 Moderate · up to Stopping work now relies on Codex to terminate commands instead of independently terminating and checking them. The inspected controls constrain which session and turn are targeted, but command cleanup after successful interruption is not independently established. No surviving command or authorization bypass was demonstrated. Retained concerns
Security review detailsSecurity Blast Radius
Security Findings and Attack Paths
Trust Boundaries and Controls
Resilience and Maintainability Implications
Hardening Proposals
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- Stop confirmation no longer depends on the process sweep.
CodexStructuredTurnCancellation.cancelnow treats a resolvedturn/interruptas proof the turn ended and returns{cancelled:true}; a sweep that cannot prove the turn's processes are gone becomes aconsole.warninstead of a false chat row. The sweep still runs and is still awaited for cleanup. unconfirmedremoved from the adapter cancel contract. It was only produced by that Codex branch; the shared wire result (AgentSessionCancelResult) is unchanged, and no other producer or consumer of the field remains insrc/ormobile/.- Host row branch removed. The
!cancelled && outcome.unconfirmedstatus row is gone; the genuinely unanswered interrupt still reaches "Cancellation was not confirmed." through the adapter-rejection catch (the timeout error rethrows because it is neither a request nor an unsupported error). - New host-level regression test (
structured-agent-session-codex-stop-row.test.ts) runs the real host, journal and Codex adapter with the sweep forced to fail andturn/completed(interrupted) arriving 4 ms after the ack; it asserts exactly["Cancellation requested."]and an interrupted turn, for both a no-turn and a named-turn Stop. Updatedcodex-structured-session-cancel.test.tsandstructured-agent-session-conversation-stop.test.tsaccordingly. - Comment-only changes in
codex-structured-turn-end-settlement.tsandcodex-structured-dispatch-echo.tsexplaining why an interrupted turn's un-echoed send is withdrawn.
I verified the load-bearing external claim ("Codex answers an interrupt only as the turn ends") against upstream openai/codex: request_processors/turn_processor.rs withholds the response for a non-empty turnId and bespoke_event_handling.rs releases it from the TurnAborted/TurnComplete arms, enqueuing the response just before turn/completed. Orca only ever sends a non-empty id, so the startup fast path is unreachable, and the normal-completion overlap is the PR's documented known cost. Ran the touched suites (codex session-cancel, prompt-ownership, conversation-stop, turn-open-wait, turn-end-settlement, the new stop-row test, host, prompt-cancel, turns), oxlint on changed files, and pnpm tc:node — all green.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
StatusReady. Head The failure: after a Codex Stop that did end the turn, the chat said "Cancellation was not confirmed." (or "The provider had already finished this turn." for a Stop naming its turn). Orca's separate check that the turn's processes were gone returned false and spoke for the Stop. The fix: Codex answers an interrupt only as the turn ends, so that answer is the confirmation. The process check still runs for cleanup but no longer writes a false line. It removes the cause, not a guard around it. Review
Validation
Known, not in this PR: the logged warning cannot tell "processes survived" from "could not check". |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to GitHub limitations.
🟠 Major · Wait for turn/completed before confirming… · codex-structured-turn-cancellation.ts:120-125
src/main/codex/codex-structured-turn-cancellation.ts:120-125
🎯 Functional Correctness | 🟠 Major | ⚡ Quick winWait for
turn/completedbefore confirming cancellation.The Codex app-server
turn/interruptresponse is an acknowledgement. It does not contain the final turn status.CodexStructuredTurnCancellation.canceltreats that response as confirmation, releases the blocked completion, and returns{ cancelled: true }even when process termination is unproven. The session layer then shows “Cancellation requested.” while the turn can still be active.Change
src/main/codex/codex-structured-turn-cancellation.tsto wait for the matchingturn/completednotification and requireturn.status === 'interrupted'before returning{ cancelled: true }. Return an unconfirmed result for a timeout or another terminal status.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: 03331e50-39cd-4043-ae6a-79cfb5e3cad3
📒 Files selected for processing (2)
src/main/native-chat/agent-session-wire/structured-agent-session-adapter.tssrc/main/native-chat/agent-session-wire/structured-agent-session-turns-cancel.ts
💤 Files with no reviewable changes (1)
- src/main/native-chat/agent-session-wire/structured-agent-session-turns-cancel.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 4 remain after this review.
|
Status update: merged current |
|
Visual Proof update: a live before/after with real Codex is now in the description. Both arms carry the same QA-only forcing patch (the process check reports failure after running), and the arms differ only by this PR's 8 files. Before shows the false "Cancellation was not confirmed." after a Stop that interrupted the turn; after shows "Cancellation requested." with the turn interrupted. The real Codex and Claude settings files were unchanged across the runs. Not merged. |
A Codex Stop sent turn/interrupt and, alongside it, swept the turn's processes: it compared a snapshot of the app-server's descendants taken at each send and compaction against a fresh one and killed what was new, then waited for those processes to exit. The turn's end was held back until the sweep finished. Codex already owns this. It kills a turn's one-shot commands on interrupt and deliberately keeps unified-exec background terminals running across one, so the sweep killed work Codex means to keep, read the process table on every send, and delayed the interrupted end behind a kill-and-wait. A Stop is now turn/interrupt only. Codex's answer confirms it and the turn's end publishes the moment it arrives, through the same path as any other notification. A long-running command started in that turn keeps running until the thread ends, as it does in Codex itself. Removed: the turn-process module and its integration test, the snapshot taken at dispatch and compaction, the held turn/completed, the adapter dependencies that injected both, and the prompt-claim re-check that only covered the wait for the snapshot.
…ted window-close burst
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: 8ed235e0-d3fc-44f2-b9de-75b83f8ff986
📒 Files selected for processing (22)
config/scripts/ci-shard-timings.jsonsrc/main/codex/codex-prompt-registry-retention.test.tssrc/main/codex/codex-prompt-registry.tssrc/main/codex/codex-structured-dispatch-boundary.test.tssrc/main/codex/codex-structured-dispatch-test-support.tssrc/main/codex/codex-structured-prompt-ownership.test.tssrc/main/codex/codex-structured-prompt-ownership.tssrc/main/codex/codex-structured-provider-events.tssrc/main/codex/codex-structured-session-acquire.tssrc/main/codex/codex-structured-session-adapter-fixture.tssrc/main/codex/codex-structured-session-adapter-lifecycle.test.tssrc/main/codex/codex-structured-session-adapter.tssrc/main/codex/codex-structured-session-cancel.test.tssrc/main/codex/codex-structured-session-state.tssrc/main/codex/codex-structured-turn-cancellation.tssrc/main/codex/codex-structured-turn-processes.integration.test.tssrc/main/codex/codex-structured-turn-processes.tssrc/main/crash-reporting/self-initiated-tree-kill-log.test.tssrc/main/crash-reporting/self-initiated-tree-kill-log.tssrc/main/native-chat/agent-session-wire/structured-agent-session-codex-stop-row.test.tssrc/main/refused-tree-kill-root-termination.test.tssrc/main/runtime/structured-agent-session-codex-turn-end-settlement.test.ts
💤 Files with no reviewable changes (7)
- config/scripts/ci-shard-timings.json
- src/main/codex/codex-structured-turn-processes.integration.test.ts
- src/main/codex/codex-prompt-registry.ts
- src/main/runtime/structured-agent-session-codex-turn-end-settlement.test.ts
- src/main/codex/codex-structured-session-state.ts
- src/main/codex/codex-structured-turn-processes.ts
- src/main/codex/codex-structured-dispatch-test-support.ts
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review.
| let interruptConfirmed = false | ||
| try { | ||
| const result = await cancellation.cancel( | ||
| const result = await interruptCodexTurn({ | ||
| session, | ||
| claim.prompt.threadId, | ||
| promptTurnId, | ||
| isCurrent, | ||
| () => { | ||
| threadId: claim.prompt.threadId, | ||
| turnId: promptTurnId, | ||
| requestTimeoutMs, | ||
| onConfirmed: () => { | ||
| interruptConfirmed = true | ||
| return session.translator?.cancelPrompt(prompt.itemId) ?? { accepted: true } | ||
| } | ||
| ) | ||
| }) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🔎 Supported by static analysis
🏁 Script executed:
sed -n '1,170p' src/main/codex/codex-structured-prompt-ownership.ts
sed -n '130,260p' src/main/codex/codex-prompt-registry.ts
rg -n 'cancelPrompt|claimBoundPrompt|ownsClaim|interruptConfirmed' src/main/codex
git diff 280733273545f0b3eeedc1be54b14d406239030e 179837a1aaecfd1f34fa9dcf30bbbbbdb6030741 -- src/main/codex/codex-structured-prompt-ownership.ts src/main/codex/codex-structured-turn-cancellation.tsRepository: stablyai/orca
Length of output: 24879
🏁 Script executed:
set -e
printf '%s\n' '--- translator contract and implementation ---'
sed -n '1,120p' src/main/codex/codex-structured-journal-contracts.ts
sed -n '220,320p' src/main/codex/codex-structured-journal-translation.ts
rg -n -C 6 'cancelPrompt|function cancel|cancel\(|bindJournalItemId|clearTurn|forget\(|replace|replacement|turn/(completed|started)|activeTurnIds' src/main/codex --glob '*.ts'
printf '%s\n' '--- current cancellation tests ---'
rg -n -C 10 'cancelCodexStructuredTurn|interruptCodexTurn|cancelPrompt|prompt.*cancel|replacement|acquisitionGeneration|ownsBoundClaim' src/main/codex/codex-structured-prompt-ownership.test.ts src/main/codex --glob '*test.ts'
printf '%s\n' '--- exact base ownership implementation ---'
git show 280733273545f0b3eeedc1be54b14d406239030e:src/main/codex/codex-structured-prompt-ownership.ts | sed -n '1,145p'
printf '%s\n' '--- relevant current ownership and registry ranges ---'
cat -n src/main/codex/codex-structured-prompt-ownership.ts | sed -n '65,125p'
cat -n src/main/codex/codex-prompt-registry.ts | sed -n '145,225p'Repository: stablyai/orca
Length of output: 45659
🏁 Script executed:
printf '%s\n' '--- cancelPrompt definitions ---'
rg -n -C 12 'cancelPrompt' src/main/codex
printf '%s\n' '--- prompt registry lifecycle calls ---'
rg -n -C 8 'bindJournalItemId|clearTurn|prompts\.forget|prompts\.clear|prompts\.cancel|cancel\(' src/main/codex --glob '*.ts'
printf '%s\n' '--- base cancellation wrapper ---'
git show 280733273545f0b3eeedc1be54b14d406239030e:src/main/codex/codex-structured-turn-cancellation.ts | sed -n '55,155p'Repository: stablyai/orca
Length of output: 41780
🏁 Script executed:
set -e
printf '%s\n' '--- prompt cancellation implementation ---'
cat -n src/main/codex/codex-structured-journal-prompts.ts | sed -n '80,145p'
printf '%s\n' '--- cancellation and turn lifecycle tests ---'
rg -n -C 14 'cancel\(|cancelled|cancelPrompt|same-item|replacement|turn/completed|clearPromptTurn' src/main/codex/codex-structured-journal-translation-turn-lifecycle.test.ts src/main/codex/codex-structured-prompt-ownership.test.ts src/main/codex/codex-prompt-registry-retention.test.tsRepository: stablyai/orca
Length of output: 42362
Preserve prompt ownership through interrupt confirmation.
turn/completed can clear the claimed prompt while turn/interrupt is pending. A replacement can reuse the same journal item ID. The onConfirmed callback then passes that ID to cancelPrompt, which can cancel the replacement and other prompts in its turn. Recheck ownership before cancelling.
Suggested fix
if (!claim || !promptTurnId) {
if (claim) {
session.prompts.releaseClaim(claim)
}
return { cancelled: false }
}
+ const acquisitionGeneration = session.acquisitionGeneration
let interruptConfirmed = false
try {
const result = await interruptCodexTurn({
session,
threadId: claim.prompt.threadId,
turnId: promptTurnId,
requestTimeoutMs,
onConfirmed: () => {
+ if (
+ sessions.get(request.sessionId) !== session ||
+ session.ended ||
+ session.fence !== request.fence ||
+ session.acquisitionGeneration !== acquisitionGeneration ||
+ providerTurnId(session, requestedTurnId) !== turnId ||
+ !session.prompts.ownsClaim(claim)
+ ) {
+ session.prompts.releaseClaim(claim)
+ return { accepted: true }
+ }
interruptConfirmed = true
return session.translator?.cancelPrompt(prompt.itemId) ?? { accepted: true }
}
})…he journal database)
|
Status update: this PR now deletes the Codex Stop process sweep and the hold on the turn's end. A Stop is the interrupt alone, as in Codex itself, and confirms as soon as Codex answers. The description is rewritten for that: a long-running command started in the stopped turn keeps running until the thread ends, which is checked live on Codex 0.159.0; the one remaining "Differences" entry cites #23618's live evidence; the Windows caveat and a possible "Stop background commands" follow-up are named. Live before/after with real Codex is in Visual Proof, the after with no patch. One review loop on the deletion: CLEAN. |
There was a problem hiding this comment.
ℹ️ No critical issues — one stale audit harness is left behind by the process-sweep removal.
Reviewed changes
Re-reviewed the two commits landed since the prior pullfrog review (e424604de): 7d845e0ef7 ("a Codex Stop is the interrupt alone") and the doc-only 179837a1aa — a 22-file delta.
- A Stop is now the interrupt alone. Deleted
codex-structured-turn-processes.tsandCodexStructuredTurnCancellation;interruptCodexTurnsends onlyturn/interruptand treats a resolved request as confirmation ({cancelled:true}). Removed thecaptureTurnProcesses/terminateTurnProcessesdeps and all adapter/fixture wiring. - The
turn/completeddeferral is gone. Notifications publish every turn end as Codex sends it, so the turn's end reaches the journal ahead of the Stop's answer. - Contracts trimmed. Dropped
ownsBoundClaim(Codex),unconfirmedfrom the adapter cancel outcome, the!cancelled && outcome.unconfirmedrow branch, and thecancellation/compactionscancel parameters;cancelCodexConversationgained a post-wait liveness guard. - Tests and comments. Pruned tests that exercised the deleted sweep, rebalanced the crash-reporting ring-size comment, and simplified the dispatch-boundary ordering test.
I verified the load-bearing external claim against upstream openai/codex: turn/interrupt cancels the turn's task token, which terminates the legacy exec process group and the unified_exec one-shot command, while background/interactive terminals are deliberately left alive until thread shutdown. Deleting Orca's descendant sweep therefore cannot orphan a one-shot command. I also traced the new ordering: a turn/completed processed before onConfirmed still converts the turn's pending prompts through settleCodexJournalTurn, so the prompt row reads cancelled either way — no wrong row. Touched suites and pnpm tc:node pass.
ℹ️ The codex-prompt-claim-retention audit harness is now stale
The new commits remove CodexStructuredTurnCancellation, CodexPromptRegistry.ownsBoundClaim, and the cancellation/compactions parameters of cancelCodexStructuredTurn, but docs/audits/codex-prompt-claim-retention/ still targets all of them. Its documented reproduce command can no longer run, and the README still names the removed class as the mechanism that confirms a cancellation.
Technical details
# Stale codex-prompt-claim-retention audit after the cancellation refactor
## Affected sites
- `docs/audits/codex-prompt-claim-retention/reproduce.cjs:18` — re-exports the removed `CodexStructuredTurnCancellation` from `codex-structured-turn-cancellation`; bundling now fails on the missing named export (and would be `undefined` at runtime).
- `docs/audits/codex-prompt-claim-retention/scenario.cjs:52` — `new api.CodexStructuredTurnCancellation({...})`; `:101`/`:102` pass `compactions`/`cancellation` to `cancelCodexStructuredTurn`; `:119` passes `turnCancellation` to `translateCodexNotification`; `:189` calls the removed `state.prompts.ownsBoundClaim(...)`.
- `docs/audits/codex-prompt-claim-retention/README.md:10` — describes "`The actual CodexStructuredTurnCancellation` invokes the confirmation callback" as the current confirmation path.
## Required outcome
- Either update the harness to the new flow (`interruptCodexTurn` + immediate notification delivery, `ownsClaim` in place of `ownsBoundClaim`), or retire the audit and remove its README instructions if the claim-lifetime reproduction is no longer meaningful.
- Keep the documented reproduce command runnable if it stays.
## Open questions for the human
- Is this audit still load-bearing (referenced from a decision record or incident), or safe to delete now that the confirmation mechanism changed?DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
Main now settles a Codex send from the turn it was answered into (#23618, #23679, #23850): an interrupted or failed turn that never echoed a send withdraws or refuses it, and a completed turn leaves it to its echo, which Codex records before turn/completed. That replaces this branch's own mechanism, so every conflict resolves to main: - journal-level terminal settlement (ownerEndedClientMessageIds, multi-row enqueueMany, terminalTurnFences, canResolveDispatch, hasTerminalTurn, turn_settled_before_acknowledgement) and the sink's onCommitted/onAbandoned/onDiscarded plumbing - the dispatch-echo ownership model (start candidates, started evidence, terminal snapshots) and the late-settlement retry - the connection's onResult response observer - the shared-predicate command admission, which main expresses with its own fence and handover rules (#23524) Codex's turn/steer, the part the host-owned queue still needs, is re-expressed on main's model in the following commit.

ELI5
When you pressed Stop in a Codex chat, the chat could say "Cancellation was not confirmed." even though the Stop had worked and the reply had stopped. It happened because, after Codex stopped the turn, Orca also tried to kill every process that turn had started and waited for that to finish. When it couldn't prove they were gone, it blamed the Stop. Now a Stop just asks Codex to stop the turn, as Codex's own app does. It confirms as soon as Codex answers, and the false line is gone.
What Changed
Before
After
Mechanism
codex-structured-turn-processes.ts), the descendant snapshot taken at dispatch and compaction, and the hold onturn/completedincodex-structured-turn-cancellation.ts. What's left there is one function,interruptCodexTurn. The turn's end now goes through the normal notification path.unconfirmedStop outcome had only one producer, the sweep, so it is removed from the adapter contract (structured-agent-session-adapter.ts), along with its row branch instructured-agent-session-turns-cancel.ts.Why
The wrong line came from a second question, "are the processes gone?", asked after Codex had already answered the first. That second question was also the wrong one: Codex deliberately keeps a turn's long-running sessions alive through an interrupt. The sweep's main effect was killing processes Codex meant to keep, and holding the Stop while it did. No reported problem motivated it. Removing it removes the false row, the wait before Stop confirms, and the process-table reads on every send, instead of adding another guard.
Alternatives considered:
Differences from the common pattern
Linked Issue
No issue. Found in live QA of #23026 against real Codex.
Visual Proof
Live on this laptop with real Codex.
Before (current
main). The wrong line needs the process check to fail, which is rare, so this arm carries a QA-only local patch, never pushed, that makes the check report failure after running. The result is a Stop that interrupted the turn, followed by the false line:After (this PR, no patch; the sweep and its check no longer exist): two Stops, each "Cancellation requested." with the turn interrupted, 1-2 ms after the turn's end:
A long-running command survives a Stop, as in Codex:
sleep 300and a local web server started in the turn were still running, and the server still answered, 30 s after the Stop. This was checked with the sweep switched off on the installed Codex 0.159.0:Testing
I manually tested these changes locally
Automated tests added/updated, or explained why not below
codex-structured-session-cancel.test.tscovers two cases. Every process read or kill is mocked as a promise that never settles.structured-agent-session-codex-stop-row.test.tsruns the real host, journal and Codex adapter. The status rows are exactly["Cancellation requested."]and the turn reads interrupted, for a Stop that names no turn and one that names its turn.The genuinely unconfirmed case, where Codex never answers the interrupt, still writes "Cancellation was not confirmed.".
Tests that pinned only the sweep or the hold are deleted. Two prompt-ownership tests that pinned the hold now expect the Stop to confirm at once.
33 related test files pass.
pnpm tc:nodeandtc:cli, oxlint,check:code-quality:changed,check:react-doctor:changed, the reliability-gate check and the anti-slop audit pass.AI Disclosure
Review
Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.Notes
Merge order: merge this PR before #23861. #23861 still reads the
unconfirmedStop outcome this PR removes; once this lands, main is merged into #23861 so it adopts the removal.unconfirmedoutcome was internal to the host.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)Author: @BrennanKB5