mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Fix hang on turn/interrupt (#18392)
Fix a bug where the `turn/interrupt` RPC hangs when interrupting a turn that has already completed. Before this change, `turn/interrupt` requests were queued in app-server and only answered when a later TurnAborted event arrived. If the target turn was already complete, core treated Op::Interrupt as a no-op, so no abort event was emitted and the RPC could hang indefinitely. This change fixes that in two places: * Reject turn/interrupt immediately with `INVALID_REQUEST` when the requested turn is no longer the active turn. * Resolve any already-accepted pending interrupt requests when the turn reaches TurnComplete, covering the case where a turn finishes naturally after the interrupt request is accepted but before it aborts. I tested this by adding a failing test in 707487c0634834f6741986b64f61886c2dc10108. You may view the results here: https://github.com/openai/codex/actions/runs/24585182419/ <img width="1512" height="310" alt="CleanShot 2026-04-17 at 16 33 30@2x" src="https://github.com/user-attachments/assets/f4a88228-b2a4-41f4-9aaa-ec82814096af" />
This commit is contained in:
committed by
GitHub
Unverified
parent
28742866c7
commit
11806faf71
@@ -7790,11 +7790,6 @@ impl CodexMessageProcessor {
|
||||
async fn turn_interrupt(&self, request_id: ConnectionRequestId, params: TurnInterruptParams) {
|
||||
let TurnInterruptParams { thread_id, turn_id } = params;
|
||||
let is_startup_interrupt = turn_id.is_empty();
|
||||
if !is_startup_interrupt {
|
||||
self.outgoing
|
||||
.record_request_turn_id(&request_id, &turn_id)
|
||||
.await;
|
||||
}
|
||||
|
||||
let (thread_uuid, thread) = match self.load_thread(&thread_id).await {
|
||||
Ok(v) => v,
|
||||
@@ -7808,10 +7803,40 @@ impl CodexMessageProcessor {
|
||||
// interrupts do not have a turn and are acknowledged after submission.
|
||||
if !is_startup_interrupt {
|
||||
let thread_state = self.thread_state_manager.thread_state(thread_uuid).await;
|
||||
let mut thread_state = thread_state.lock().await;
|
||||
thread_state
|
||||
.pending_interrupts
|
||||
.push((request_id.clone(), ApiVersion::V2));
|
||||
let is_running = matches!(thread.agent_status().await, AgentStatus::Running);
|
||||
let interrupt_outcome = {
|
||||
let mut thread_state = thread_state.lock().await;
|
||||
if let Some(active_turn) = thread_state.active_turn_snapshot() {
|
||||
if active_turn.id != turn_id {
|
||||
Err(format!(
|
||||
"expected active turn id {turn_id} but found {}",
|
||||
active_turn.id
|
||||
))
|
||||
} else {
|
||||
thread_state
|
||||
.pending_interrupts
|
||||
.push((request_id.clone(), ApiVersion::V2));
|
||||
Ok(())
|
||||
}
|
||||
} else if thread_state.last_terminal_turn_id.as_deref() == Some(turn_id.as_str()) {
|
||||
Err("no active turn to interrupt".to_string())
|
||||
} else if is_running {
|
||||
thread_state
|
||||
.pending_interrupts
|
||||
.push((request_id.clone(), ApiVersion::V2));
|
||||
Ok(())
|
||||
} else {
|
||||
Err("no active turn to interrupt".to_string())
|
||||
}
|
||||
};
|
||||
if let Err(message) = interrupt_outcome {
|
||||
self.send_invalid_request_error(request_id, message).await;
|
||||
return;
|
||||
}
|
||||
|
||||
self.outgoing
|
||||
.record_request_turn_id(&request_id, &turn_id)
|
||||
.await;
|
||||
}
|
||||
|
||||
// Submit the interrupt. Turn interrupts respond upon TurnAborted; startup
|
||||
@@ -8074,7 +8099,7 @@ impl CodexMessageProcessor {
|
||||
// opt-in stays synchronized with the conversation.
|
||||
let raw_events_enabled = {
|
||||
let mut thread_state = thread_state.lock().await;
|
||||
thread_state.track_current_turn_event(&event.msg);
|
||||
thread_state.track_current_turn_event(&event.id, &event.msg);
|
||||
thread_state.experimental_raw_events
|
||||
};
|
||||
let subscribed_connection_ids = thread_state_manager
|
||||
@@ -11237,14 +11262,15 @@ mod tests {
|
||||
let state = manager.thread_state(thread_id).await;
|
||||
let mut state = state.lock().await;
|
||||
state.cancel_tx = Some(cancel_tx);
|
||||
state.track_current_turn_event(&EventMsg::TurnStarted(
|
||||
codex_protocol::protocol::TurnStartedEvent {
|
||||
state.track_current_turn_event(
|
||||
"turn-1",
|
||||
&EventMsg::TurnStarted(codex_protocol::protocol::TurnStartedEvent {
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
model_context_window: None,
|
||||
collaboration_mode_kind: Default::default(),
|
||||
},
|
||||
));
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
manager.remove_thread_state(thread_id).await;
|
||||
|
||||
Reference in New Issue
Block a user