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

5.8 KiB
Raw Blame History

DOs

  • Name Events Precisely: Use ShutdownComplete for the completion notification; keep Op::Shutdown for requests.
// protocol.rs
pub enum Op {
    // ...
    Shutdown,
}

pub enum EventMsg {
    // ...
    ShutdownComplete,
}
  • Gracefully Drain Rollout Writer: Add a Shutdown command with a oneshot ack; dont invent “Sync/Drain” that flushes unnecessarily.
// rollout.rs
enum RolloutCmd {
    AddItems(Vec<ResponseItem>),
    UpdateState(SessionStateSnapshot),
    Shutdown { ack: oneshot::Sender<()> },
}

async fn rollout_writer(mut file: File, mut rx: Receiver<RolloutCmd>) {
    while let Some(cmd) = rx.recv().await {
        match cmd {
            RolloutCmd::AddItems(items) => { /* write items; flush as needed */ }
            RolloutCmd::UpdateState(state) => { /* write state; flush as needed */ }
            RolloutCmd::Shutdown { ack } => { let _ = ack.send(()); } // no extra flush
        }
    }
}
  • Propagate Errors Cleanly: Prefer match and return Errs instead of swallowing them.
// rollout.rs
pub async fn shutdown(&self) -> std::io::Result<()> {
    let (tx_done, rx_done) = oneshot::channel();
    match self.tx.send(RolloutCmd::Shutdown { ack: tx_done }).await {
        Ok(()) => rx_done
            .await
            .map_err(|e| IoError::other(format!("failed waiting for rollout shutdown: {e}"))),
        Err(e) => Err(IoError::other(format!("failed to send rollout shutdown command: {e}"))),
    }
}
  • Take Ownership Only When Needed: Use Option::take() when you must ensure the recorder is dropped after shutdown to unblock the writer.
// codex.rs (on Op::Shutdown)
if let Some(sess_arc) = sess {
    let recorder_opt = sess_arc.rollout.lock().unwrap().take(); // take so we can drop after await
    if let Some(rec) = recorder_opt {
        if let Err(e) = rec.shutdown().await {
            warn!("failed to shutdown rollout recorder: {e}");
            // send error EventMsg if needed
        }
        // rec drops here; tx closes; writer can finish naturally
    }
}
  • Centralize “Last Message” Writing: Keep a single helper and call it from both processors. Pass the path via constructors, not per-call.
// event_processor.rs
pub(crate) fn handle_last_message(msg: Option<&str>, path: Option<&Path>) {
    match (path, msg) {
        (Some(p), Some(m)) => { let _ = std::fs::write(p, m); }
        (Some(p), None) => {
            let _ = std::fs::write(p, "");
            eprintln!("Warning: no last agent message; wrote empty content to {}", p.display());
        }
        (None, _) => eprintln!("Warning: no file to write last message to."),
    }
}

// exec processors (constructors)
impl EventProcessorWithHumanOutput {
    pub(crate) fn create_with_ansi(with_ansi: bool, config: &Config, last_message_path: Option<PathBuf>) -> Self { /* store path */ }
}
impl EventProcessorWithJsonOutput {
    pub fn new(last_message_path: Option<PathBuf>) -> Self { /* store path */ }
}
  • Map Events To Status Clearly: Return CodexStatus from process_event; early-return for non-Running, default to Running.
// event_processor.rs
pub(crate) enum CodexStatus { Running, InitiateShutdown, Shutdown }

// event_processor_with_human_output.rs
fn process_event(&mut self, event: Event) -> CodexStatus {
    match event.msg {
        EventMsg::TaskComplete(TaskCompleteEvent { last_agent_message }) => {
            handle_last_message(last_agent_message.as_deref(), self.last_message_path.as_deref());
            return CodexStatus::InitiateShutdown;
        }
        EventMsg::ShutdownComplete => return CodexStatus::Shutdown,
        // ... print other events
        _ => {}
    }
    CodexStatus::Running
}
  • Drive Shutdown From The Main Loop: Submit Op::Shutdown on InitiateShutdown; break on Shutdown.
// exec/lib.rs
while let Some(event) = rx.recv().await {
    match event_processor.process_event(event) {
        CodexStatus::Running => continue,
        CodexStatus::InitiateShutdown => { codex.submit(Op::Shutdown).await?; }
        CodexStatus::Shutdown => break,
    }
}
  • Exit TUI On ShutdownComplete: Let Ctrl-C request shutdown; exit only after ShutdownComplete.
// chatwidget.rs (handling events)
EventMsg::ShutdownComplete => {
    self.app_event_tx.send(AppEvent::ExitRequest);
}

// chatwidget.rs (Ctrl-C path)
} else if self.bottom_pane.ctrl_c_quit_hint_visible() {
    self.submit_op(Op::Shutdown);
    true
} else {
    self.bottom_pane.show_ctrl_c_quit_hint();
    false
}
  • Keep Comments Useful: Use comments to explain intent (e.g., why take()), not to restate code.

DONTs

  • Dont Swallow Errors: Avoid returning Ok(()) on send/await failures; propagate them.
// Avoid:
if let Err(e) = self.tx.send(cmd).await { return Ok(()); }
  • Dont Over-Engineer Draining: Skip “Sync/Drain” variants that flush again; the writer already flushes on write paths. The Shutdown ack is sufficient.

  • Dont Duplicate Helper Logic: Dont inline “last message” file writes in multiple places; call handle_last_message instead.

  • Dont Pass Per-Call Paths: Dont thread last_message_file through process_event; store it in the processor at construction time.

  • Dont Extend Traits Needlessly: Dont add last_message_path getters or file-writing methods to the EventProcessor trait just to share code; use top-level helpers.

  • Dont Change UX Lightly: Dont alter Ctrl-C semantics (e.g., removing immediate ExitRequest) without manual testing and ensuring the TUI waits for ShutdownComplete.

  • Dont Add Redundant Comments: Avoid “This is a no-op.” when the code already makes that obvious.

  • Dont Force Writer Exit: Dont terminate the writer inside Shutdown; let it finish naturally when tx is dropped after shutdown completes.