mirror of
https://github.com/openai/codex.git
synced 2026-04-28 10:21:06 +03:00
5.4 KiB
5.4 KiB
DOs
- Emit
TurnAbortedon interrupt: SendEventMsg::TurnAbortedin response toOp::Interrupt, carrying a reason.
// core/src/codex.rs
let event = Event {
id: self.sub_id,
msg: EventMsg::TurnAborted(TurnAbortedEvent { reason }),
};
let tx_event = self.sess.tx_event.clone();
tokio::spawn(async move { let _ = tx_event.send(event).await; });
- Use explicit abort reasons: Distinguish between user interrupts and task replacement.
// protocol/src/protocol.rs
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct TurnAbortedEvent { pub reason: TurnAbortReason }
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum TurnAbortReason { Interrupted, Replaced }
- Abort with correct reason in session: Replace
abort()calls withabort(TurnAbortReason::...).
// Replacing a running task
if let Some(current) = state.current_task.take() {
current.abort(TurnAbortReason::Replaced);
}
// Handling an interrupt
fn interrupt_task(&self) {
info!("interrupt received: abort current task, if any");
let mut state = self.state.lock_unchecked();
state.pending_approvals.clear();
state.pending_input.clear();
if let Some(task) = state.current_task.take() {
task.abort(TurnAbortReason::Interrupted);
}
}
- Route interrupts via session entry points: Call
sess.interrupt_task()for all interrupt-like ops.
match sub.op {
Op::Interrupt => { sess.interrupt_task(); }
Op::ExecApproval { decision: ReviewDecision::Abort, .. } => { sess.interrupt_task(); }
Op::PatchApproval { decision: ReviewDecision::Abort, .. } => { sess.interrupt_task(); }
_ => { /* existing handling */ }
}
- Defer MCP interrupt responses until
TurnAborted: Queue pending requests and reply when the event arrives, including the reason.
// mcp-server: record pending interrupts
{
let mut map = self.pending_interrupts.lock().await;
map.entry(conversation_id.0).or_default().push(request_id);
}
let _ = conversation.submit(Op::Interrupt).await;
// On TurnAborted, respond to all pending
match msg {
EventMsg::TurnAborted(ev) => {
let pending = {
let mut map = pending_interrupts.lock().await;
map.remove(&conversation_id.0).unwrap_or_default()
};
let response = InterruptConversationResponse { abort_reason: ev.reason };
for rid in pending { outgoing.send_response(rid, response.clone()).await; }
}
_ => {}
}
- Expose abort reason on the wire: Extend the interrupt response payload to include
abort_reason.
// mcp-server/src/wire_format.rs
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct InterruptConversationResponse {
pub abort_reason: TurnAbortReason,
}
- Handle
TurnAbortedin all consumers: Provide clear, user-facing messages.
// exec event processor
match event.msg {
EventMsg::TurnAborted(ev) => match ev.reason {
TurnAbortReason::Interrupted => ts_println!(self, "task interrupted"),
TurnAbortReason::Replaced => ts_println!(self, "task aborted: replaced by a new task"),
},
_ => {}
}
// TUI chat widget
EventMsg::TurnAborted(_) => self.on_error("Turn interrupted".to_owned()),
- Update protocol docs/comments accordingly: Note that
Op::InterruptyieldsEventMsg::TurnAborted.
// protocol/src/protocol.rs
/// Abort current task.
/// This server sends [`EventMsg::TurnAborted`] in response.
Interrupt,
- Adjust tests to expect the event: Wait for a
turn_abortednotification instead of an immediate response/error.
// mcp-server/tests/interrupt.rs
let _turn_aborted = timeout(
DEFAULT_READ_TIMEOUT,
mcp_process.read_stream_until_notification_message("turn_aborted"),
).await??;
DON'Ts
- Don’t emit
EventMsg::Errorfor interrupts: UseEventMsg::TurnAbortedwith a preciseTurnAbortReason.
// ❌ Old
msg: EventMsg::Error(ErrorEvent { message: " Turn interrupted".to_string() })
// ✅ New
msg: EventMsg::TurnAborted(TurnAbortedEvent { reason: TurnAbortReason::Interrupted })
- Don’t reply immediately to MCP interrupt requests: Wait for
TurnAbortedbefore sendingInterruptConversationResponse.
// ❌ Old: respond right away
outgoing.send_response(request_id, InterruptConversationResponse {}).await;
// ✅ New: respond after event with reason (see DOs snippet)
- Don’t drop sessions without cleanup: Ensure
Droptriggersinterrupt_task()to clear state and abort current work.
impl Drop for Session {
fn drop(&mut self) {
self.interrupt_task();
}
}
- Don’t assume a single pending interrupt: Support multiple queued
RequestIds per conversation.
// Use a Vec<RequestId> in a HashMap keyed by conversation UUID
pending_interrupts: Arc<Mutex<HashMap<Uuid, Vec<RequestId>>>>
- Don’t short-circuit event flow in loops: Avoid stray
continue;that can suppress downstream handling.
// ❌ Avoid early `continue;` after handling an event
match event.msg {
EventMsg::ExecApprovalRequest(_) => {
// handle...
// no `continue;` here
}
_ => {}
}
- Don’t treat
TurnAbortedas a no-op in processors: Add explicit match arms in exec/TUI instead of ignoring it.
// ❌ Missing arm: `_ => {}` only
// ✅ Include `EventMsg::TurnAborted(_)` arm (see DOs)