Files
codex/prs/bolinfest/study/PR-2365-study.md
2025-09-02 15:17:45 -07:00

5.4 KiB
Raw Blame History

DOs

  • Emit TurnAborted on interrupt: Send EventMsg::TurnAborted in response to Op::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 with abort(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 TurnAborted in 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::Interrupt yields EventMsg::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_aborted notification 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

  • Dont emit EventMsg::Error for interrupts: Use EventMsg::TurnAborted with a precise TurnAbortReason.
// ❌ Old
msg: EventMsg::Error(ErrorEvent { message: " Turn interrupted".to_string() })

// ✅ New
msg: EventMsg::TurnAborted(TurnAbortedEvent { reason: TurnAbortReason::Interrupted })
  • Dont reply immediately to MCP interrupt requests: Wait for TurnAborted before sending InterruptConversationResponse.
// ❌ Old: respond right away
outgoing.send_response(request_id, InterruptConversationResponse {}).await;

// ✅ New: respond after event with reason (see DOs snippet)
  • Dont drop sessions without cleanup: Ensure Drop triggers interrupt_task() to clear state and abort current work.
impl Drop for Session {
    fn drop(&mut self) {
        self.interrupt_task();
    }
}
  • Dont 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>>>>
  • Dont 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
    }
    _ => {}
}
  • Dont treat TurnAborted as 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)