Skip to content

[codex] Handle Ctrl-C for non-TTY unified exec#26734

Merged
pakrym-oai merged 6 commits into
mainfrom
pakrym/unified-exec-non-tty-interrupt
Jun 9, 2026
Merged

[codex] Handle Ctrl-C for non-TTY unified exec#26734
pakrym-oai merged 6 commits into
mainfrom
pakrym/unified-exec-non-tty-interrupt

Conversation

@pakrym-oai

@pakrym-oai pakrym-oai commented Jun 6, 2026

Copy link
Copy Markdown
Collaborator

Why

A long-running unified exec process started with tty: false could not be interrupted via write_stdin: ordinary non-TTY stdin writes are rejected once stdin is closed, but an exact U+0003 payload should still map to a process interrupt. The interrupt should flow through the same process lifecycle path as a real signal so Codex preserves process-reported output and exit metadata instead of fabricating a Ctrl-C exit code or tearing down the session early.

What Changed

  • Add process/signal to exec-server with ProcessSignal::Interrupt and an empty response.
  • Add a non-consuming ProcessHandle::signal path for spawned processes; on Unix it sends SIGINT to the process group and leaves terminate/hard-kill unchanged.
  • Route non-TTY U+0003 write_stdin through process.signal(...) instead of terminate, then let the normal post-write collection path drain output and observe exit.
  • Add exec-server coverage where a shell trap INT handler prints the signal and exits with its own code.
  • Add unified exec coverage where a tty: false process traps SIGINT, emits output, and exits with its own code.

Validation

  • just test -p codex-exec-server exec_process_signal_interrupts_process
  • just test -p codex-exec-server
  • just test -p codex-core write_stdin_ctrl_c_interrupts_non_tty_session

@pakrym-oai pakrym-oai marked this pull request as ready for review June 6, 2026 04:15
@pakrym-oai pakrym-oai requested a review from a team as a code owner June 6, 2026 04:15

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f765fdce24

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread codex-rs/core/src/unified_exec/process_manager.rs Outdated
@pakrym-oai pakrym-oai force-pushed the pakrym/unified-exec-non-tty-interrupt branch from f765fdc to 5f1f3e3 Compare June 6, 2026 04:41
Comment thread codex-rs/core/src/unified_exec/process.rs Outdated
Comment thread codex-rs/core/src/unified_exec/process.rs Outdated
Comment thread codex-rs/core/src/unified_exec/process_manager.rs Outdated
@pakrym-oai pakrym-oai force-pushed the pakrym/unified-exec-non-tty-interrupt branch 4 times, most recently from 2ce392c to ae2c317 Compare June 8, 2026 18:36
@pakrym-oai pakrym-oai force-pushed the pakrym/unified-exec-non-tty-interrupt branch from ae2c317 to b564da4 Compare June 8, 2026 18:46
@pakrym-oai

Copy link
Copy Markdown
Collaborator Author

@codex review this

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b564da44fb

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread codex-rs/utils/pty/src/pipe.rs
Comment thread codex-rs/core/src/unified_exec/process.rs
Comment thread codex-rs/utils/pty/src/process.rs Outdated
@pakrym-oai

Copy link
Copy Markdown
Collaborator Author

@codex review this

@chatgpt-codex-connector

Copy link
Copy Markdown
Contributor

Codex Review: Didn't find any major issues. Swish!

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@jif-oai jif-oai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess you tried it? Should we add a small prompt in the tool def to encourage the model to use CTRL-C?


pub(super) async fn interrupt(&self) -> Result<(), UnifiedExecError> {
match &self.process_handle {
ProcessHandle::Local(process_handle) => process_handle

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(found by Codex)
This only signals the spawned zsh process group. With unified-exec zsh-fork, approved commands run under EscalateServer outside that group, and the super-exec path still does not forward signal

@pakrym-oai pakrym-oai Jun 9, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll punt on zsh-fork for now.

},
);
router.request(
EXEC_SIGNAL_METHOD,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make signal out-of-band or dispatch post-handshake requests concurrently?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to merge it as it. Signal has the same ordering semantic as write_stdin which we use for sigint in non-tty cases.

ProcessSignal::Interrupt => {
#[cfg(unix)]
{
crate::process_group::interrupt_process_group(self.process_group_id)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assumes SIGINT is default and unblocked in the child, but pipe spawn preserves the parent’s signal state. If Codex inherited SIGINT ignored, this returns success while the command keeps running

@pakrym-oai pakrym-oai Jun 9, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is fine. Model will recover from the fact the process is still running (by explicitly killing it like it does today)

}
/// Send SIGINT to a specific process group ID (best-effort).
pub fn interrupt_process_group(process_group_id: u32) -> io::Result<()> {
signal_process_group_id(process_group_id as libc::pid_t, libc::SIGINT).map(|_| ())

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(found by Codex)
Can we cover the default SIGINT path too? The pipe waiter maps code() == None to -1, so ordinary Ctrl-C reports exitCode: -1

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added

ProcessSignal::Interrupt => {
#[cfg(unix)]
if let Some(process_group_id) = self.process_group_id {
return crate::process_group::interrupt_process_group(process_group_id);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that for PTYs, this cached group is the shell/session leader, not necessarily the terminal foreground group. But not sure and very nit anyway

@pakrym-oai

Copy link
Copy Markdown
Collaborator Author

I guess you tried it? Should we add a small prompt in the tool def to encourage the model to use CTRL-C?

It doesn't need to be encouraged. That's the default behavior by the model today.

@jif-oai jif-oai left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

@pakrym-oai pakrym-oai merged commit f2969f3 into main Jun 9, 2026
31 checks passed
@pakrym-oai pakrym-oai deleted the pakrym/unified-exec-non-tty-interrupt branch June 9, 2026 22:10
@github-actions github-actions Bot locked and limited conversation to collaborators Jun 9, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants