Fix missing resume hint on zero-token exits (#16987)

Addresses #16421

Problem: Resumed interactive sessions exited before new token usage
skipped all footer lines, hiding the `codex resume` continuation
command.

It's not clear whether this was an intentional design choice, but I
think it's reasonable to expect this message under these circumstances.

Solution: Compose token usage and resume hints independently so
resumable sessions still print the continuation command with zero usage.
This commit is contained in:
Eric Traut
2026-04-07 09:34:04 -07:00
committed by GitHub
Unverified
parent cd591dc457
commit 2b9bf5d3d4
2 changed files with 25 additions and 18 deletions
+6 -7
View File
@@ -438,15 +438,14 @@ fn format_exit_messages(exit_info: AppExitInfo, color_enabled: bool) -> Vec<Stri
..
} = exit_info;
if token_usage.is_zero() {
return Vec::new();
let mut lines = Vec::new();
if !token_usage.is_zero() {
lines.push(format!(
"{}",
codex_protocol::protocol::FinalOutput::from(token_usage)
));
}
let mut lines = vec![format!(
"{}",
codex_protocol::protocol::FinalOutput::from(token_usage)
)];
if let Some(resume_cmd) =
codex_core::util::resume_command(thread_name.as_deref(), conversation_id)
{
+19 -11
View File
@@ -316,12 +316,13 @@ fn session_summary(
thread_id: Option<ThreadId>,
thread_name: Option<String>,
) -> Option<SessionSummary> {
if token_usage.is_zero() {
let usage_line = (!token_usage.is_zero()).then(|| FinalOutput::from(token_usage).to_string());
let resume_command = codex_core::util::resume_command(thread_name.as_deref(), thread_id);
if usage_line.is_none() && resume_command.is_none() {
return None;
}
let usage_line = FinalOutput::from(token_usage).to_string();
let resume_command = codex_core::util::resume_command(thread_name.as_deref(), thread_id);
Some(SessionSummary {
usage_line,
resume_command,
@@ -486,7 +487,7 @@ fn emit_system_bwrap_warning(app_event_tx: &AppEventSender, config: &Config) {
#[derive(Debug, Clone, PartialEq, Eq)]
struct SessionSummary {
usage_line: String,
usage_line: Option<String>,
resume_command: Option<String>,
}
@@ -3301,7 +3302,10 @@ impl App {
"Failed to attach to fresh app-server thread: {err}"
));
} else if let Some(summary) = summary {
let mut lines: Vec<Line<'static>> = vec![summary.usage_line.clone().into()];
let mut lines: Vec<Line<'static>> = Vec::new();
if let Some(usage_line) = summary.usage_line {
lines.push(usage_line.into());
}
if let Some(command) = summary.resume_command {
let spans = vec!["To continue this session, run ".into(), command.cyan()];
lines.push(spans.into());
@@ -4114,8 +4118,10 @@ impl App {
{
Ok(()) => {
if let Some(summary) = summary {
let mut lines: Vec<Line<'static>> =
vec![summary.usage_line.clone().into()];
let mut lines: Vec<Line<'static>> = Vec::new();
if let Some(usage_line) = summary.usage_line {
lines.push(usage_line.into());
}
if let Some(command) = summary.resume_command {
let spans = vec![
"To continue this session, run ".into(),
@@ -4174,8 +4180,10 @@ impl App {
{
Ok(()) => {
if let Some(summary) = summary {
let mut lines: Vec<Line<'static>> =
vec![summary.usage_line.clone().into()];
let mut lines: Vec<Line<'static>> = Vec::new();
if let Some(usage_line) = summary.usage_line {
lines.push(usage_line.into());
}
if let Some(command) = summary.resume_command {
let spans = vec![
"To continue this session, run ".into(),
@@ -10897,7 +10905,7 @@ guardian_approval = true
}
#[tokio::test]
async fn session_summary_skip_zero_usage() {
async fn session_summary_skips_when_no_usage_or_resume_hint() {
assert!(
session_summary(
TokenUsage::default(),
@@ -10922,7 +10930,7 @@ guardian_approval = true
session_summary(usage, Some(conversation), /*thread_name*/ None).expect("summary");
assert_eq!(
summary.usage_line,
"Token usage: total=12 input=10 output=2"
Some("Token usage: total=12 input=10 output=2".to_string())
);
assert_eq!(
summary.resume_command,