[js_repl] Hard-stop active js_repl execs on explicit user interrupts (#13329)

## Summary
- hard-stop `js_repl` only for `TurnAbortReason::Interrupted`,
preserving the persistent REPL across replaced turns
- track the current top-level exec by turn and only reset when the
interrupted turn owns submitted work or a freshly started kernel for the
current exec attempt
- close both interrupt races: the write-window race by marking the exec
as submitted before async pipe writes begin, and the startup-window race
by tracking fresh-kernel ownership until submission
- add regression coverage for interrupted in-flight execs and the
pending-kernel-start window

## Why
Stopping a turn previously surfaced `aborted by user after Xs` even
though the underlying `js_repl` kernel could continue executing. Earlier
fixes also risked resetting the session-scoped REPL too broadly or
missing already-dispatched work. This change keeps cleanup scoped to
explicit stop semantics and makes the interrupt path line up with both
submitted execs and newly started kernels.

## Testing
- `just fmt`
- `cargo test -p codex-core`
- `just fix -p codex-core`

`cargo test -p codex-core` passes the updated `js_repl` coverage,
including the new startup-window regression test, but still has
unrelated integration failures in this environment outside `js_repl`.

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
aaronl-openai
2026-03-12 17:51:56 -07:00
committed by GitHub
parent 793bf32585
commit d9a403a8c0
3 changed files with 431 additions and 9 deletions

View File

@@ -227,9 +227,6 @@ impl Session {
// in-flight approval wait can surface as a model-visible rejection before TurnAborted.
active_turn.clear_pending().await;
}
if reason == TurnAbortReason::Interrupted {
self.close_unified_exec_processes().await;
}
}
pub async fn on_task_finished(
@@ -396,6 +393,16 @@ impl Session {
.await;
}
pub(crate) async fn cleanup_after_interrupt(&self, turn_context: &Arc<TurnContext>) {
self.close_unified_exec_processes().await;
if let Some(manager) = turn_context.js_repl.manager_if_initialized()
&& let Err(err) = manager.interrupt_turn_exec(&turn_context.sub_id).await
{
warn!("failed to interrupt js_repl kernel: {err}");
}
}
async fn handle_task_abort(self: &Arc<Self>, task: RunningTask, reason: TurnAbortReason) {
let sub_id = task.turn_context.sub_id.clone();
if task.cancellation_token.is_cancelled() {
@@ -425,6 +432,8 @@ impl Session {
.await;
if reason == TurnAbortReason::Interrupted {
self.cleanup_after_interrupt(&task.turn_context).await;
let marker = ResponseItem::Message {
id: None,
role: "user".to_string(),