Repository navigation
fix(native-chat): a kill that reached the agent counts as gone, so the next message starts a fresh one - #25866
Draft
brennanb2025 wants to merge 3 commits into
Draft
brennanb2025 wants to merge 3 commits into
brennanb2025 wants to merge 3 commits into
Conversation
…, so the next message starts a fresh one - POSIX close kills the agent's own process group while its supervisor is paused, then the supervisor; never the supervisor alone. - The host ends the child's record on a delivered kill (no exit evidence) and starts the next message's agent without refusing. - Codex resume waits out the previous app-server's thread lock (100/400/1000 ms), then fails that one message. - Codex waits the same 0.5 s past its supervisor's worst case that Claude does.
This branch has not been deployed
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.
ELI5
When you stop Claude or Codex in a native chat, Orca shuts down the agent's process. Before this change, Orca would not start a new process for your next message until it actually saw the old one exit. If that exit report never came, your next message failed with "Couldn't stop Claude from before. Send your message again to try once more.", even though Orca had already force-killed the old process and it could not write anything anymore.
Now, once Orca's kill has actually reached the old agent, it counts as gone, and your next message starts a fresh agent right away. Orca still refuses only when the kill itself could not be delivered, which is the one case where two agents could write to the same conversation.
While making this change I found a real hole on macOS and Linux. In some cases Orca's force-kill reached only a small wrapper process and left the agent itself running, and Orca then treated it as gone. This PR closes that hole too.
What Changed
The problem, as a user sees it. After a Stop, closing a chat tab, or the 30-minute idle rest, Orca stops the agent's process. It sends a polite stop first, then a force-kill, then waits about 1 second to see the process exit. If it did not see the exit in time, everything that needed the agent was refused, after up to about 10 seconds of waiting:
A process that has received a force-kill (SIGKILL on macOS/Linux, TerminateProcess on Windows) never runs its own code again, so it cannot add another turn to the conversation. The refusal protected Orca's bookkeeping, not the conversation. It is rare: a process stuck in the kernel (a hung network drive, for example) or an exit Orca had not processed yet. The common pattern for this does not make the next message wait on an exit report at all.
The hole I found. On macOS and Linux, each agent runs under a small supervisor process. The supervisor starts the agent in a separate process group, so killing the supervisor does not kill the agent. Two paths killed only the supervisor:
provider-process-teardown.ts), which then even reported the tree as handled;claude-agent-sdk-exit-proof.ts).In both cases the agent kept running with nobody watching it, and Orca then saw the supervisor exit and counted the agent as gone. For Codex releases without their own conversation lock, that could mean two Codex processes writing one conversation.
After this change:
provider-supervised-group-kill.ts).AgentSessionProviderKilledError.rootGone: false.deathEvidence: null, the same stored shape restart recovery already writes, so nothing new is persisted.What stays the same.
Why
The rule now asks the question that matters: can the old process still write to this conversation? It no longer asks whether Orca has processed the old process's exit event yet.
Alternatives considered.
How this differs from the common pattern, each labelled:
taskkill /T /F(tree kill) plus TerminateProcess of the root, plus Codex's own lock. That is the common pattern's mechanism. For an npm Codex install on Windows, the process Orca starts is a Node wrapper whose child does the writing. That child is reached by the tree kill, whose outcome cannot be read, and by Codex's lock. Codex releases older than the lock get the same assurance as before this PR: the wrapper's exit never said anything about its child either.thread/start) has a fresh id that no other process can hold.Linked Issue
Fixes # (none; internal follow-up to the native-chat agent lifecycle work in #24862)
Visual Proof
N/A. This adds no new UI. The change is that an existing error stops appearing in a rare fault state, which needs a process stuck in the kernel to reproduce, so it cannot be staged in the app. It is covered by host-level tests that drive the real host and adapters.
Testing
Platforms: macOS (local). Linux and Windows branches are covered by unit tests with fake kill results. Linux's ESRCH answer for an empty group was not tried on a real Linux machine.
pnpm run typecheck(CI's command) is clean.oxlinton the changed files is clean.check:code-quality:changedpassed.claude-structured-real-cli*.test.ts, run once as the repo rule asks): 9 of 10 passed. "withdraws a follow-up queued behind a turn that is stopped" failed. The CLI had already accepted the queued follow-up before the cancel (both messages came back with provider ids), so nothing was left to withdraw. That test covers turn cancellation, which this PR does not touch, but I did not re-run it on main to prove it fails there too.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
previousExitUnverifiableand its copy are kept.Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)